NES 에뮬레이터를 신경망으로 다시 만드는 프로젝트는 여럿 있습니다. NESformer의 차별점은 게임 화면을 학습으로 흉내 내는 것이 아니라, NES의 6502 CPU와 2KB RAM, 카트리지 ROM을 Qwen3.5 체크포인트의 가중치와 상태에 컴파일했다는 주장입니다.
Drew Breunig의 NESformer는 “표준 Qwen3_5ForCausalLM 체크포인트 안에서 NES의 CPU, RAM, ROM이 동작한다”고 설명합니다. “아무도 이 가중치를 학습시키지 않았고, 컴파일러가 모든 값을 계산했다”는 문장이 이 프로젝트의 핵심입니다. 모델은 CPU 실행 로그를 한 토큰씩 예측하고, 작은 호스트 프로그램이 화면·컨트롤러·vblank 인터럽트를 담당합니다.
모델 안에 들어간 것과 호스트에 남은 것
저장소의 README는 역할을 명확히 나눕니다. 모델 안에는 6502 코어의 151개 공식 opcode, 주소 지정 방식, 플래그, 스택, BRK·NMI 진입, 2KB 작업 RAM, 32KB PRG-ROM이 들어갑니다. 호스트에는 PPU 렌더러, 팔레트, 컨트롤러, APU 레지스터 쓰기, vblank 타이밍이 남습니다.
| 모델(가중치 + DeltaNet 상태) | 호스트 프로그램 |
|---|---|
| 6502 코어와 공식 opcode | PPU 스캔라인 렌더러와 팔레트 |
| 2KB 작업 RAM | 컨트롤러 입력, APU 레지스터 기록 |
| 32KB PRG-ROM, 명령어용·데이터용 두 사본 | vblank NMI 주입 |
| OAM DMA 읽기, I/O 읽기 응답 일부 | 화면 표시와 프레임 저장 |
이 분할은 “신경망이 모든 것을 한다”는 주장과 다릅니다. CPU의 결정적 실행은 모델 안에 있고, 그래픽과 입출력은 호스트가 맡습니다. 따라서 NESformer를 볼 때는 “LLM이 게임을 플레이한다”보다 “LLM 안의 CPU가 실행 로그를 생성하고, 호스트가 그 로그를 콘솔처럼 해석한다”고 읽는 편이 정확합니다.
DeltaNet 상태의 한 행을 메모리 셀로 쓰는 법
핵심 트릭은 Qwenputer에서 이어집니다. Gated DeltaNet의 각 head는 128×128 상태 행렬을 유지하고, 토큰마다 delta rule로 갱신됩니다. 게이트를 열고 key를 one-hot으로 만들면 갱신이 정확히 한 행만 덮어쓰고, one-hot query는 그 행을 읽어옵니다. 즉 행 하나가 메모리 셀이 됩니다.
다음은 원리를 설명하기 위해 CodeBridge가 작성한 작은 NumPy 예제입니다. 실제 Qwen3.5나 NESformer의 커널이 아니라, one-hot 주소 지정이 어떻게 한 행만 갱신하는지 보여주는 교육용 코드입니다.
import numpy as np
state = np.zeros((4, 3)) # 4 rows, 3 values per row
key = np.array([0.0, 1.0, 0.0, 0.0]) # select row 1
value = np.array([7.0, 8.0, 9.0])
state += np.outer(key, value) # write row 1 only
print("read row 1:", (key @ state).tolist())
print("row 0 unchanged:", state[0].tolist())
new_value = np.array([1.0, 2.0, 3.0])
state += np.outer(key, new_value - (key @ state)) # overwrite row 1
print("read row 1 again:", (key @ state).tolist())
print("row 2 unchanged:", state[2].tolist())
로컬에서 실행한 출력입니다.
read row 1: [7.0, 8.0, 9.0]
row 0 unchanged: [0.0, 0.0, 0.0]
read row 1 again: [1.0, 2.0, 3.0]
row 2 unchanged: [0.0, 0.0, 0.0]
이 예제는 행 하나를 주소처럼 선택하고, 덮어쓴 뒤 다시 읽는 과정만 보여줍니다. NESformer는 이 원리를 여러 레이어와 head로 확장하고, nesformer.json에 어느 레이어·head가 CPU의 어느 부분을 담는지 기록합니다. 실제 주소 공간과 갱신 규칙은 저장소의 core.py와 weights.py에 구현돼 있으며, 이 글에서 그 코드를 재구현하지는 않았습니다.
검증은 “그럴듯한 화면”이 아니라 토큰 일치로 한다
README의 검증은 단계별로 나뉩니다. 참조 CPU가 nestest의 황금 로그 3,348개 명령과 일치하고, 게임 코드 570만 명령에서 py65 에뮬레이터와 일치합니다. 회로 자체는 Qwenputer 평가기에서 참조 토큰을 모두 예측하고, 체크포인트는 표준 transformers에서 2,651개 위치를 불일치 없이 예측합니다. 자유 실행에서는 nestest, Thwaite, Full Game의 추적이 참조 CPU와 동일했다고 보고합니다.
CodeBridge는 저장소를 클론해 가중치를 풀고 check 명령을 직접 실행했습니다. 이 명령은 모델과 참조 CPU를 나란히 돌리며 첫 차이에서 멈추는 교사 검증입니다. 자유 실행 게임플레이를 재현한 것은 아닙니다.
$ python -m nesformer check roms/thwaite.nes --tokens 12000
OK: the weights generated 12,000 tokens identical to the reference CPU's trace
(3 frames, frames identical: True; 4,385 tok/s)
이 환경에서는 12,000토큰과 3프레임이 참조 구현과 일치했습니다. README가 보고한 전체 실행(Thwaite 1,916,527토큰, 600프레임)을 재현한 것은 아니므로, 이 결과를 “전체 게임이 동일하다”는 독립 검증으로 확대하지 않습니다. 그래도 최소한 체크포인트가 로컬에서 로드되고, 초기 실행 구간에서 결정적으로 동작한다는 점은 확인했습니다.
속도: 700,000토큰/초와 1,650토큰/초 사이
README는 실제 NES가 초당 약 700,000토큰을 필요로 한다고 설명합니다. 기본 numba 엔진은 제작 환경의 CPU 코어 하나에서 약 1,650토큰/초를 냈다고 합니다. 단순 비율은 다음과 같습니다.
700,000 / 1,650 ≈ 424.2
즉 순수 처리량만 보면 실기보다 약 424배 느립니다. 다만 게임 코드는 vblank를 기다리는 유휴 루프를 많이 돌고, NESformer는 동일한 루프가 반복되는 것을 감지해 다음 PPU 이벤트까지 시간을 건너뜁니다. 이 최적화가 토큰 수를 2.6~4.2배 줄입니다. 그런데 이 두 수치만으로 계산하면 424를 2.6~4.2로 나눈 101~163배가 나옵니다. README가 말하는 전체 150~290배와는 맞지 않습니다. 측정 하드웨어나 기준이 다를 수 있으므로, 150~290배는 검증된 값이 아니라 원문이 제시한 별도 측정치로 표기합니다.
우리 로컬 check는 워밍업 후 4,385토큰/초를 기록했지만, 이는 교사 검증 경로의 짧은 구간이고 하드웨어·캐시·측정 조건이 README와 다릅니다. 이 숫자로 제품 속도를 일반화할 수 없습니다. 중요한 것은 “실시간 에뮬레이션”이 아니라 “결정적 실행을 모델 안에서 재현하는 연구”라는 성격입니다.
한계가 곧 연구 범위다
저장소가 공개한 한계는 분명합니다.
- NROM(iNES mapper 0) 카트리지만 지원합니다. 뱅크 스위칭이 필요한 게임은 ROM 주소를 모델 안에서 바꿀 수 없습니다.
- 공식 opcode만 지원하고, 비공식 opcode는
HALT를 냅니다. - 코드는 카트리지 ROM에서 실행돼야 하며, RAM으로 점프하면
HALT입니다. - APU frame IRQ와 DMC IRQ는 없습니다. BRK와 NMI는 동작합니다.
- PPU와 APU는 가중치에 없습니다. 스캔라인 단위 렌더링이라 중간 스캔라인 효과는 재현되지 않고, 스프라이트 0 히트와 오버플로는 근사값이며 소리는 없습니다.
이 목록은 “LLM이 NES를 완벽히 에뮬레이트한다”는 식의 요약이 왜 위험한지 보여줍니다. CPU 코어의 결정적 부분과 주변 장치를 분리했기 때문에 가능한 결과이지, 하나의 모델이 콘솔 전체를 학습한 것이 아닙니다.
개발자에게 남는 질문
NESformer는 성능 벤치마크가 아니라 아키텍처 실험입니다. 신경망의 recurrent state를 주소 가능한 메모리로 쓰고, 컴파일된 회로를 언어 모델 체크포인트에 심는 방식은 “모델이 계산을 잘하는가”와 “모델이 계산을 우연히 맞히는가”를 구분하게 합니다. 전자는 검증 가능한 실행 경로이고, 후자는 확률적 생성입니다.
실무에 바로 적용할 결론은 제한적입니다. 대부분의 제품에는 전용 CPU나 결정적 코드가 더 싸고 빠릅니다. 그러나 모델 내부 상태를 외부 메모리처럼 다루는 아이디어는 에이전트의 장기 상태, 도구 실행 기록, 검증 가능한 중간 계산을 설계할 때 참고할 만합니다. 이때도 “모델이 맞혔다”와 “회로가 보장한다”를 같은 성공 지표로 합치지 않아야 합니다.
관련 배경은 AI 벤치마크를 읽는 법에서 더 다룹니다. NESformer의 코드와 가중치는 MIT 라이선스로 공개돼 있지만, 테스트 ROM은 각자의 라이선스를 따릅니다.