스크랩

1GB 언어 모델 안에서 슈퍼 마리오가 돌아간다고? 학습 없이 NES CPU를 가중치로 컴파일한 nesformer

1GB 언어 모델 안에서 슈퍼 마리오가 돌아간다고? 학습 없이 NES CPU를 가중치로 컴파일한 nesformer

AI 모델이 게임을 '플레이'하는 이야기는 많이 들어보셨을 텐데요. 이번에는 조금 다릅니다. 모델 자체가 게임기의 CPU가 되는 프로젝트가 Show HN에 등장했습니다. 이름은 nesformer입니다.

핵심 요약

  • nesformer는 NES의 6502 CPU, 2KB RAM, 카트리지 프로그램 ROM을 표준 Qwen3.5 체크포인트 안에서 실행합니다.
  • 가중치는 학습하지 않고 컴파일러로 계산했으며, 모델은 CPU 실행 기록을 토큰 단위로 생성합니다.
  • 체크포인트는 bf16 기준 933MB이지만, 4억 6,660만 파라미터 중 0이 아닌 값은 930만 개뿐이라 압축하면 3.3MB입니다.
  • 사용자 정의 코드 없이 표준 Qwen3_5ForCausalLM으로 로드할 수 있습니다.
  • 검증한 실행에서 실행 기록과 저장 프레임이 참조 6502 에뮬레이터와 일치했지만, 최적화 후에도 실제 NES보다 150~290배 느립니다.

학습이 아니라 '컴파일'입니다

보통 언어 모델의 가중치는 데이터를 학습해 얻습니다. nesformer는 모든 가중치를 컴파일러가 계산하며 학습 과정이 전혀 없습니다. CHIP-8과 RISC-V CPU를 Qwen3.5-4B에 컴파일한 선행 프로젝트 Qwenputer의 컴파일러와 메모리 구현 방식을 재사용했습니다.

핵심 아이디어는 Gated DeltaNet 상태를 기계의 메모리로 쓰는 것입니다. 각 DeltaNet 헤드는 128 × 128 상태 행렬을 유지하고 토큰마다 델타 규칙으로 갱신합니다. 게이트를 열어 두고 원핫 키를 쓰면 정확히 한 행을 덮어쓸 수 있고, 원핫 쿼리로 그 행을 읽을 수 있습니다. 즉 각 행이 카트리지 데이터, RAM, 레지스터를 담는 메모리 셀이 되는 셈입니다.

모델과 호스트의 역할 분담

모델이 NES 전체를 흉내 내는 것은 아닙니다. 역할이 명확히 나뉘어 있습니다.

모델이 맡는 일

  • 공식 명령어 151개, 모든 주소 지정 방식, 플래그, 스택, BRK와 NMI 진입
  • 2KB 작업 RAM과 32KB PRG-ROM 저장 (ROM은 명령어 인출용과 데이터 읽기용으로 두 벌 유지)
  • OAM DMA 읽기 (읽은 256바이트가 DM 토큰으로 출력)

호스트 프로그램이 맡는 일

  • PPU의 스캔라인 렌더링, 팔레트, 컨트롤러, 주변 장치 입출력
  • vblank 시점의 NMI 토큰 주입, I/O 읽기 응답인 IN 토큰 주입

호스트는 CPU 레지스터와 RAM을 직접 들여다보지 않고 오직 실행 기록을 통해서만 확인합니다.

7개 레이어로 구성한 실행 회로

체크포인트는 회로를 구현하는 Gated DeltaNet 레이어 7개와, Hugging Face 캐시에 필요한 값이 전부 0인 어텐션 레이어 1개로 이루어져 있습니다.

  • 레이어 1~2: CPU 상태와 프로그램 카운터 관리
  • 레이어 3~4: 명령어 인출, 디코딩, 유효 주소 계산
  • 레이어 5~7: 데이터 읽기, ADC/SBC 등 연산, 플래그 계산, 다음 토큰 결정

흥미로운 점은 PC를 직접 기록하지 않는다는 것입니다. 각 명령어의 마지막 토큰에 명령어 길이를 담고, DeltaNet 카운터 3개가 마지막 점프 이후 길이별 실행 수를 집계합니다. PC는 점프 목적지에 이 오프셋을 더해 암묵적으로 구합니다. 실제 게임 코드는 명령어당 평균 1.1~1.3토큰을 사용합니다.

어떤 게임을 돌릴 수 있나요?

NROM 보드(iNES mapper 0) 를 쓰는 .nes 파일이 대상이며, Super Mario Bros., Donkey Kong, Excitebike가 예시로 소개되어 있습니다. 시작할 때 ROM을 모델 상태에 로드하기 때문에 같은 가중치로 다른 NROM 게임을 재빌드 없이 실행할 수 있습니다.

다만 상용 게임 ROM은 사용자가 직접 준비해야 하며, 저장소에는 자유 라이선스 테스트 ROM 3개만 포함되어 있습니다. 컨트롤러 입력은 프레임 구간별 스크립트로 지정하고, Python 3.11 이상이 필요합니다.

정말 똑같이 동작할까?

검증은 꽤 꼼꼼합니다.

  • 참조 CPU는 nestest 기준 로그 3,348개 명령어, 게임 코드 570만 개 명령어에서 py65 에뮬레이터와 일치했습니다.
  • nestest 공식 명령어 테스트는 4,586토큰이 일치하고 예상한 HALT로 종료되었습니다.
  • Thwaite 600프레임 실행은 1,916,527토큰을 생성했고 저장한 300프레임이 모두 일치했습니다.
  • Full Game 420프레임 실행은 2,645,885토큰을 생성했고 저장한 210프레임이 모두 일치했습니다.

속도는 어느 정도일까?

여기서 현실이 드러납니다. 실제 NES 속도에는 약 70만 토큰/s가 필요한데, 엔진별 속도는 다음과 같습니다.

| 엔진 | 처리 속도 | | --- | --- | | 기본 엔진(numba 컴파일) | 약 1,650토큰/s (CPU 코어 1개) | | numpy | 약 210토큰/s | | 표준 transformers | 약 10토큰/s |

게임이 vblank를 기다리는 반복문을 건너뛰는 유휴 루프 최적화로 전체 토큰 수를 2.6~4.2배 줄였지만, 프레임당 1~6초가 걸립니다. NES 시간으로 10초인 600프레임 실행에 약 25분이 필요합니다.

지원 범위와 한계

  • NROM만 지원: 프로그램 ROM은 32KB까지이며, MMC1·MMC3 같은 매퍼의 뱅크 전환은 지원하지 않습니다.
  • 공식 명령어와 ROM 내 코드만 실행: 비공식 명령어나 RAM으로의 점프는 HALT를 발생시킵니다.
  • 일부 인터럽트 미지원: APU 프레임 IRQ와 DMC IRQ는 동작하지 않으며, BRK와 NMI는 동작합니다.
  • PPU와 APU는 가중치 밖: 스캔라인 중간 효과는 재현되지 않고, Sprite-0 hit와 스프라이트 오버플로는 근사 구현이며, 사운드는 없습니다.

마치며

실용적인 게임 실행 도구라기보다는, 트랜스포머 체크포인트가 범용 컴퓨터의 구성 요소를 담을 수 있음을 보여 주는 흥미로운 실험에 가깝습니다. 프로젝트는 MIT 라이선스로 배포되며, 포함된 ROM은 각각의 라이선스를 따릅니다.


원문: https://news.hada.io/topic?id=36318&utm_source=geeknews&utm_medium=rss&utm_campaign=news