メインコンテンツへ移動
AIチュートリアル

nesformerでコンパイル済みQwen3.5モデル内でNESゲームを実行する

このチュートリアルでは、nesformerを使ってNESの6502 CPU、メモリ、カートリッジROMを標準的なQwen3.5チェックポイントにコンパイルする方法を説明します。CLIのインストール、対応ゲームの実行、コントローラー入力の自動化、アニメーションのエクスポート、リファレンスCPUとの実行比較、代替エンジンの検討を行います。

nesformerでコンパイル済みQwen3.5モデル内でNESゲームを実行する

nesformerとは?

nesformerは実験的なNESランタイムです。コンソールの6502 CPU、2 KBのワークRAM、カートリッジのプログラムROMが、標準のQwen3_5ForCausalLMチェックポイントの重みと状態の中で動作します。重みは学習されていません。代わりに、コンパイラーがCPU回路を実装するよう計算して生成しています。

ゲーム中、モデルはCPUの実行ログを1トークンずつ予測します。Pythonホストプログラムは残りのコンソール部品を担当します。グラフィックスの描画、コントローラー入力の読み取り、タイミング処理、I/O読み取りへの応答、垂直ブランキングNMI割り込みの注入を行います。

このプロジェクトはQwenputerを基盤とし、そのコンパイラーと中核的なメモリ技術を再利用しています。各Gated DeltaNetヘッドは状態行列を保持します。ワンホットキーとクエリにより、その行列の行をメモリセルとして扱い、レジスター、RAM、カートリッジデータを格納できます。

主な機能

  • 学習ではなくコンパイル:ゼロではない重みはすべて、回路仕様から生成されます。
  • 標準チェックポイント形式:チェックポイントは通常のHugging Faceフォルダーであり、カスタムモデルコードなしでQwen3_5ForCausalLM.from_pretrainedから読み込めます。
  • 公式6502命令セットを完全実装:回路は151個の公式オペコード、アドレッシングモード、フラグ、スタック操作、BRK、NMIエントリーをすべて実装します。
  • 再利用可能なNROM対応:起動時に選択したカートリッジをモデル状態へ書き込むため、同じ重みで異なるiNES mapper 0 ROMを実行できます。
  • スクリプト制御:コマンドラインからフレーム単位でボタン入力をスケジュールできます。
  • 映像エクスポート:実行結果を対応ターミナルに表示し、GIFまたはPNGフレームとして保存できます。
  • 実行検証:checkコマンドはモデル出力を同梱のリファレンスCPUと比較し、最初の差分で停止します。
  • 複数の実行エンジン:nesformerは高速なnumbaエンジン、通常のNumPyエンジン、標準のTransformersエンジンを提供します。

モデル内部で動作するもの

チェックポイントには6502コア、CPUレジスターとフラグ、2 KBのワークRAM、固定長32 KBのプログラムROMテーブルが含まれます。プログラムフェッチ、データ読み取り、スタック操作、OAM DMA読み取りにも対応します。

ホストは引き続き、PPUレンダラー、パレット、コントローラー、vblank NMIのタイミング、サイクルカウント、APUレジスター処理を担当します。サウンドは実装されていません。ホストはI/O読み取りとNMIイベントの値をトークンストリームへ注入しますが、出力されたトレース以外でモデルのレジスターやRAMを直接調べることはありません。

nesformerはモデル推論によってCPUをエミュレートしますが、重みだけで完全なNESを実装しているわけではありません。

必要条件とフォルダー構成

Python 3.11以降、nesformerのソースアーカイブ、別途提供される重みアーカイブが必要です。両方のアーカイブを同じトップレベルフォルダーに展開してください。weightsディレクトリはPythonパッケージの中ではなく、その隣に配置する必要があります。

nesformer/
├── nesformer/
├── chipformer/
├── roms/
├── examples/
├── tests/
├── pyproject.toml
└── weights/
    ├── config.json
    ├── generation_config.json
    ├── model.safetensors
    ├── nesformer.json
    └── SHA256SUMS

以降のコマンドはすべて、上記のトップレベルディレクトリから実行してください。CLIはデフォルトで、nesformer/パッケージの隣にあるweights/を探します。

ダウンロードした重みを検証する

同梱のチェックサムファイルを使用して、重みが正しく展開されたことを確認します。

shasum -a 256 -c weights/SHA256SUMS

チェックポイントを別の場所に保存している場合は、run、check、traceの実行時に--weights /path/to/weightsで場所を指定してください。infoコマンドではチェックポイントのパスを直接指定できます。

nesformerをインストールする

仮想環境を作成して有効化し、必要な数値計算および画像処理パッケージをインストールします。

python3 -m venv .venv
source .venv/bin/activate
pip install numpy scipy numba pillow

Windowsでは、次のコマンドで環境を有効化します。

.venv\Scripts\activate

デフォルトのfastエンジンはnumbaを使用します。初回実行時はエンジンのコンパイルに約20秒かかります。2回目以降はそのコンパイル結果が再利用されます。

最初のゲームを実行する

このリポジトリには、自由にライセンスされたテストROMが含まれています。次のコマンドはThwaiteを600フレーム実行し、ターミナルにフレームを表示し、StartボタンとAボタンの入力シーケンスをスクリプトで指定します。

python -m nesformer run roms/thwaite.nes --frames 600 --show \
  --buttons "start@90-95+200-205,a@230-233+250-253+270-273+290-293+310-313+330-333+350-353+370-373+390-393+410-413+430-433+450-453+470-473+490-493+510-513+530-533+550-553+570-573+590-593"

--showオプションは、iTerm2やWindows Terminalなど、24ビットカラーに対応したターミナルに各生成フレームを描画します。実行が完了すると、CLIはROM名にちなんだアニメーションGIF(例:thwaite.gif)を保存します。

実機のNESよりも大幅に低速です。初回コンパイル後も、1フレームの処理には通常約1~6秒かかります。600フレームの実行はNES時間で10秒に相当し、約25分かかる場合があります。

互換性のあるROMを実行する

合法的に入手したROMをroms/ディレクトリに配置するか、別のパスを指定してください。ROMはiNES mapper 0として識別されるNROMボードを使用している必要があります。互換性のある例としてSuper Mario Bros.、Donkey Kong、Excitebikeがありますが、これらの商用ROMは同梱されていません。

この例ではSuper Mario Bros.を起動し、右を押し続け、複数回のジャンプをスケジュールします。

python -m nesformer run roms/smb.nes --frames 600 --show \
  --buttons "start@60-65,right@200-600,a@250-265+300-315+360-375+420-435+480-495+540-555"

起動時にROMがモデルのDeltaNet状態へ読み込まれます。互換性のあるゲームごとにチェックポイントを再構築する必要はありません。

スクリプトでコントローラー入力を指定する

--buttonsオプションは、カンマ区切りのエントリを受け取ります。各エントリには、1つ以上のボタン名、@記号、1つ以上のフレーム範囲を指定します。

start@60-65
right@200-600
a@250-265+300-315
right+b@200-600
  • start@60-65は、フレーム60からフレーム65までStartを押します。
  • right@200-600は、指定された区間でRightを押し続けます。
  • a@250-265+300-315は、Aボタンを2回押すようスケジュールします。
  • right+b@200-600は、RightとBを同時に押し続けます。

使用できる名前はa、b、select、start、up、down、left、rightです。フレームは電源投入時を起点とし、1秒あたり60フレーム進みます。したがって、フレーム600はNES時間で10秒に相当します。

移動できない画面ではゲームが操作を無視するため、通常は少し早めに押し始めても問題ありません。ボタンを押す長さが影響することもあります。たとえば、Super Mario Bros.ではAを長く押すほど高くジャンプします。

フレーム出力を制御する

デフォルトでは、runは120フレームを実行し、ROM名に基づく名前のGIFを書き出します。出力オプションで結果をカスタマイズできます。

python -m nesformer run roms/thwaite.nes \
  --frames 300 \
  --every 2 \
  --scale 3 \
  --gif output/thwaite.gif
  • --frames NはNフレーム後に停止します。
  • --gif PATHはアニメーションGIFの出力先を指定します。
  • --png DIRは保持したフレームを個別のPNGファイルとして保存します。
  • --every NはNフレームごとに1フレームを保持します。
  • --scale Nは保存するフレームを整数倍に拡大します。
  • --showは生成したフレームをターミナルに表示します。

--pngだけを使用するとGIF出力は無効になります。PNGフレームとアニメーションの両方が必要な場合は、明示的に--gifオプションを追加してください。

python -m nesformer run roms/thwaite.nes \
  --frames 120 \
  --png frames \
  --gif thwaite.gif

実行を調査・検証する

参照CPUと比較する

checkコマンドは、チェックポイントとプロジェクト標準の参照用6502を並行して実行します。最初のトークン差分で停止します。

python -m nesformer check roms/thwaite.nes --tokens 50000

デフォルトの比較長は50,000トークンです。他の実行コマンドで使用できる--buttons、--weights、--engine、--no-skipオプションも指定できます。

モデルのトークンストリームを表示する

traceを使用すると、モデルが出力したCPUレコードを調査できます。

python -m nesformer trace roms/thwaite.nes --tokens 100

トレース長のデフォルトは40トークンです。ほとんどのCPU命令は1トークンを必要としますが、I/O、ポインター読み取り、スタック操作、DMAを伴う命令では複数トークンのレコードが生成されることがあります。

実行中にトークンIDを保存する

runコマンドでは、生成されたトークンIDをファイルに書き出して後で分析できます。

python -m nesformer run roms/thwaite.nes \
  --frames 60 \
  --trace trace.txt

--max-tokens Nを使用して、フレーム数の制限だけに頼らず、生成トークン数が一定数に達した時点で実行を停止することもできます。

実行エンジンを選択する

nesformerには、性能と依存関係が異なる3つのエンジンがあります。

  • fast: デフォルトのnumba実装です。READMEによると、開発マシンのCPU 1コアで毎秒約1,650トークンです。
  • numpy: より単純なNumPy実装で、毎秒約210トークンと報告されています。
  • hf: 標準のHugging Face Transformersで、毎秒約10トークンと報告されています。

--engineでエンジンを選択します。

python -m nesformer run roms/thwaite.nes --frames 3 --engine numpy

標準のTransformersを使用する

Hugging Faceエンジンを選択する前に、追加の依存関係をインストールしてください。

pip install "torch>=2.6" "transformers>=5.22"
python -m nesformer run roms/thwaite.nes --frames 3 --engine hf

チェックポイントにカスタムモデル実装は必要ありません。内部ではnesformerがチェックポイントを読み込み、パディングトークンを1つデコードし、初期マシン状態をキャッシュのDeltaNet状態へ書き込んだ後、生成されたトークンを1つずつ入力し直します。

高度なヒント

重みをリポジトリの外に置く

チェックポイントが別の場所に保存されている場合は、--weightsを使用します。

python -m nesformer run roms/thwaite.nes \
  --weights /path/to/weights \
  --frames 120

チェックポイントのメタデータを調べる

情報コマンドを使用して、チェックポイントの情報を表示します。

python -m nesformer info

重みがデフォルトのディレクトリにない場合は、infoにそのパスを指定してください。

チェックポイントを再構築する

リポジトリには、回路チェックポイントを再生成するために必要なコンパイラーが含まれています。通常のビルドではweights/に出力され、READMEによると約10秒かかります。

python -m nesformer build

次のコマンドで別の出力ディレクトリを選択できます。

python -m nesformer build --out compiled-weights

アイドルループのスキップについて理解する

NESゲームでは、vblankを待つためにタイトなループを頻繁に実行します。ホストがRAMやI/Oへの書き込みを伴わない完全に反復するループを検出すると、クロックを次のPPUイベントの直前まで進め、残りの反復を実行します。この最適化により、表示フレームとI/Oのタイミングを維持しながら、生成されるトークン数を大幅に削減できます。

--no-skipを渡すと、アイドルループの各反復をすべて実行します。処理は遅くなりますが、完全なトークン列を調べる場合に役立ちます。

python -m nesformer trace roms/thwaite.nes --tokens 100 --no-skip

テストスイートを実行する

テスト用の依存関係をインストールして、pytestを実行します。

pip install pytest py65
python -m pytest -q tests

テストには数分かかります。リファレンスCPUとnestestログを比較し、コンパイル済み回路をリファレンスと照合し、フリーランニング重みを検証するとともに、アイドルループ最適化をテストします。

重要な制限事項

  • NROMのみ: ゲームはiNESマッパー0を使用する必要があります。MMC1やMMC3などのバンク切り替えマッパーはサポートされません。
  • 公式オペコードのみ: 非公式の6502オペコードに遭遇すると、HALTを出力します。
  • ROMからの実行: コードはカートリッジROMから実行する必要があります。RAMへのジャンプはHALTを出力します。
  • IRQ非対応: BRKとNMIは動作しますが、APUフレームIRQとDMC IRQには対応していません。
  • I/O動作の制限: I/Oレジスタに対するリード・モディファイ・ライト命令ではI/O書き込みが発生せず、$4020から$7FFFまでのアドレスは0として読み取られます。
  • グラフィックスの詳細は近似: 描画は1スキャンラインずつ行われ、スキャンライン途中の効果は再現されません。また、スプライト0ヒットとスプライトオーバーフローも近似です。
  • 音声なし: APUレジスタへの書き込みは受け付けますが、音声は生成されません。
  • 実行速度が遅い: アイドルループのスキップを有効にしても、実機よりおよそ150~290倍遅く動作します。

まとめ

nesformerは、Transformerチェックポイントが学習済み言語モデルではなく、正確にコンパイルされたコンピューターとして機能できることを示します。ソースと重みを正しく配置すれば、対応するNROMゲームを実行し、コントローラー入力を自動化し、フレームをエクスポートし、CPUトークンを調べ、リファレンス6502との実行結果を比較できます。マッパー、グラフィックス、音声、速度に制限があるため実験的なシステムではありますが、標準のQwen3.5チェックポイントと検証ツールにより、モデルの重みとDeltaNet状態に直接エンコードされた計算を実際に研究できます。