タワーディフェンスのハーネスに戻したQwen3-27B

780mlocal-llmcoding-agents

暗い机の上の光るタワーディフェンスのミニチュア戦場が、はるかに大きな青緑色の発光エンジンブロックに配線でつながれ、1本のレーンだけ琥珀色の継ぎ目が修復されている

前回の記事では、Radeon 780M上でブラウザ向けタワーディフェンスゲームを生成してテストできるローカルのハーネスを作りました。

最終的なゲームは、ブラウザと静的チェックを合わせて38項目に通りました。ハーネスは、壊れたパッチを隔離し、動いているページを検査し、失敗を正しいファイルへ振り分けることを学んでいました。最後まで残った不具合は1つの幾何的な述語に絞り込まれ、そこは手作業で直しました。

そこで残ったのは、「ローカルモデルはゲームを書けるのか」よりも面白い問いでした。

もっと大きなローカルモデルをこのループに戻したら、どうなるのか。

前回の構成では、計画とレビューにgpt-oss-20b-16k、ソースの大量生成にLing-3.0-tinyを使っていました。今回はPCにすでにあるQwen3.8-27BのGGUF(以前の実験と同じ量子化)を、Unsloth Desktopのローカルllama.cppランタイム経由で使い、本物の修復タスクを与えました。

結果は予想より良いものでした。Q3量子化は780M上で実用的な24Kコンテキストを載せられました。組み込みのマルチトークン予測により、デコード速度は4.29から7.18トークン/秒に上がりました。Qwenは、意図的に壊したゲームを最小のパッチ1つで修復しました。同時に、ベンチマークの見出しほど魔法めいてもいませんでした。同じマルチトークンの仕掛けでも、コールドなリクエスト全体では約13%しか短縮されず、まっさらな20Kトークンのプロンプトは、モデルが何かを出力するまで依然として約6分かかりました。

テスト環境

テスト環境は、Ryzen 7 8845HS、Radeon 780Mグラフィックス、32GBのRAMを積んだGMKtec K8 Plusです。780Mは統合GPUです。メモリはCPUと共有しています。推論に割り当てる量を増やすと、Windowsで使えるメモリも変わります。

モデルは既存の13.1GBのQwen3.8-27B-UD-Q3_K_XL.ggufです。ランタイムにキャッシュ済みのファイルを直接参照させました。コピーも削除も変換もしておらず、既存のOllama、Open WebUI、ROCm、ComfyUIのインストールにも手を付けていません。

Unslothはバンドルされたllama.cppサーバーをVulkanで起動しました。動いているプロセスはggml-vulkan.dllvulkan-1.dll、AMDのamdvlk64.dllを読み込み、リクエスト実行中はWindowsもGPU計算がアクティブだと報告しました。GPU計算はアクティブで、共有メモリを通じてCPUも処理に参加しました。

最初の設定は意図的にありふれたものにしました。フルGPUオフロード、スロット1、Q8のKVキャッシュ、16Kコンテキスト。これは動きました。有用な驚きは、24Kも動いたことです。

構成を探る

同じ3,811トークンの入力と128トークンの応答に対して、1度に1つの変数だけを振りました。各構成はそのつどロードし直し、リクエスト、プロセスのコマンドライン、エンジンのタイミング、応答、生のSSE、メモリのテレメトリを保存しました。

構成プロンプト tok/sデコード tok/sリクエスト時間
16K、フルGPU、8スレッド62.024.3090.98 s
16K、12スレッド62.214.2791.02 s
16K、16スレッド58.884.2894.44 s
16K、65層中56層をGPU48.403.30117.24 s
16K、マイクロバッチ51256.374.2797.39 s
24K、MTP無効59.114.2994.10 s
24K、MTPドラフトトークン259.237.1882.07 s
24K、n-gram投機61.384.0893.21 s
24K、MTPドラフトトークン457.733.7599.92 s

8スレッドは、退屈であることで勝ちました。12と16はデコードを改善しませんでした。9層をCPUに戻すと、プロンプト処理も生成も遅くなりました。マイクロバッチを大きくするのも、このワークロードでは遅くなりました。

際立っていた設定は、Qwen組み込みのMTPヘッドです。ドラフトトークン2で、サーバーは100個のドラフトトークンを提案し、76個を受理し、7.18トークン/秒に達しました。管理された比較では、通常の応答とMTPの応答は同一でした。

ドラフトトークン4はより悪くなりました。提案された155個のうち87個を受理しましたが、デコードは3.75トークン/秒に落ちました。教訓はごく局所的なものです。このPCとワークロードでは2が良く、4は適しませんでした。

最終的なプロファイルはこうです。

Qwen3.8-27B UD-Q3_K_XL
Vulkan llama.cpp、65層すべてをGPUに
24,576 コンテキスト
Q8_0 K/V キャッシュ、flash attention 有効
8 CPUスレッド
バッチ 512、マイクロバッチ 256
推論スロット 1
MTP、ドラフトトークン 2
1 GiB のホストプロンプトキャッシュ
コンテキストチェックポイント 4
ハーネス向けに推論(reasoning)は無効

正確なリロードのリクエストは、ローカルの評価アーティファクトと並べて、機械可読のbest-config.jsonとして保存してあります。実験の最後にこのプロファイルを再ロードし、動いているプロセスのコマンドラインで、コンテキストサイズ、GPU層数、MTPモード、キャッシュ上限、スレッド数を確認しました。

2万トークンは載る。それでも高くつく。

コンテキストの設定値は容量を示します。実用的なレイテンシは別に測る必要があります。

長コンテキストのテストでは、繰り返されるソースコーパスの先頭付近に1つ、末尾に1つマーカーを置き、コンテキストウィンドウを埋めたうえで、両方の値を返すようQwenに求めました。

16Kでは、12,532トークンの入力が正しく取り出されました。最初のトークンまでのレイテンシは221秒、プロンプトは56.79トークン/秒でした。

24Kでは、20,032トークンの入力も正しく取り出されました。最終的なMTPプロファイルのもとで、最初のトークンまでのレイテンシは361秒。プロンプト処理は55.60トークン/秒、短い答えのデコードは6.67トークン/秒でした。それでもプロセスには少なくとも5.93GiBの物理RAMの空きが残っていました。

これが、統合GPU上の大きなローカルモデルの実際的な姿です。

短いキャッシュ済み会話:実用的
コールドな 3.8K トークンのプロンプト:デコード開始まで約1分
コールドな 20K トークンのプロンプト:デコード開始まで約6分
プロンプト後のデコード:MTP-2 でおよそ 7 tok/s

24Kコンテキストがリポジトリ修復に有用なのは、ソース、仕様、ブラウザの診断結果を一緒に見せられるからです。リポジトリの読み込みには時間がかかります。モデルが技術的に載せられる最大のコンテキスト数を謳うより、プロンプトの選び方とコンテキストの再利用が重要です。

キャッシュのベンチマークは、理由がわかる前は間違っていた

最初の10ターンのテストは、キャッシュされたプロンプトトークンがゼロだと報告しました。これは、ランタイムがリクエストのたびに会話を捨てているように見えました。

間違っていたのは私のテストでした。

固定シードを渡していたのです。インストール済みのUnslothバックエンドは、シードがあると再現性を保つために意図的にプロンプトキャッシュを無効化します。シードを外すと、挙動はすぐに変わりました。以降のターンは前のプロンプトの大部分を再利用し、おおむね1.6〜2.0秒でトークンを出し始めました。

10ターンすべてが、記憶したビルドキーを保持していました。この実行では4,669プロンプトトークンを再利用し、1応答あたり平均10.61秒で、物理RAMの空きは5.91GiB以上を保ちました。

コンテキストチェックポイントをより頻繁にすることも試しました。デフォルトの間隔は、条件をそろえた後続ターンで最初のトークンまで平均1.77秒。256トークン間隔では平均1.82秒でした。より積極的な設定のほうが良いということはなかったので、デフォルトの間隔を保ち、代わりに保持するチェックポイントの数を制限しました。

これはローカル推論の作業で有用な戒めでした。驚くような結果が出たら、新しいモデルや量子化やバックエンドを試す前に、まずソースを見るべきです。

タイムアウトしていたのはモデルではなかった

元の修復アダプターは、Unsloth APIの呼び出しにNodeのfetchを使っていました。自前のAbortControllerは15分を許容していましたが、長い呼び出しは依然として5分前後でfetch failedで失敗していました。

モデルなしで、応答前に310秒待つローカルサーバーを立てて、この挙動を再現しました。

Node fetch / Undici:  305.426 秒で失敗、UND_ERR_HEADERS_TIMEOUT
明示的な Node HTTP:   310.025 秒で成功

HTTPクライアントには別個のヘッダータイムアウトがありました。アプリケーション側のタイムアウトを上げても、それを上書きできません。

これで、一見失敗していたQwenの修復のいくつかが説明できました。モデルはまだ動いていました。待つのをやめたのはクライアントでした。

プライベートなハーネスアダプターのUnsloth側を、明示的なストリーミングHTTPクライアントに置き換えました。自前の総デッドラインを持ち、Server-Sent Eventsを逐次消費し、使用量とタイミングのデータを保持し、不完全なストリームを拒否し、出力上限に達して終わった応答は使わないようにします。

トランスポートのテストは、分割されたSSEチャンク、不完全なストリーム、停止した応答をカバーしています。修正は小さいものですが、ハーネスが計測できるものを変えました。7分かかったモデル呼び出しは、いまや「遅い結果」として記録されます。

既知のバグを使った、本物の修復

ベンチマークが「その修復が必要だった」と証明できる形を先に作りました。プロンプトで指示したパッチを「自律修復」と呼ぶための条件です。

隔離したテストは3段階にしました。

  1. 動作するNeon Bastionをブラウザのバリデーターに通す。
  2. 既知の弾ダメージのリグレッションを1つ入れ、バリデーターが失敗することを確認する。
  3. 壊れたソースとブラウザの失敗レポートをQwenに渡し、その候補を別ディレクトリで検証する。

動作するベースラインは38/38に通りました。入れたリグレッションは、ちょうど1つのチェックtower-attack-damages-enemyで失敗しました。

Qwenは完全なJavaScriptソースと失敗したブラウザ出力を受け取りました。そして正確なパッチを1つ返しました。

-      target.hp -= 0;
+      target.hp -= proj.damage;

候補は構文チェックと38項目すべてのブラウザチェックに通りました。そのSHA-256は既知の正しいソースと一致しました。元のゲームは元の状態を保ちました。

このリクエストは入力8,068トークン、出力35トークンを使い、140秒かかりました。プロンプトは60.13トークン/秒、デコードは6.59トークン/秒です。時間のほとんどはソースとテストのコンテキストを読むのに費やされ、答え自体はごく小さいものでした。

これは、モデルに「自分のコードは正しいか」と尋ねるよりずっと良い結果です。Qwenは、ブラウザが実行できるパッチを出さなければなりませんでした。テストはまず不具合が存在することを証明し、最終的な検証がパッチで直ったことを証明しました。

不具合は意図的に入れました。このテストの対象は、既知の失敗に対する修復です。Neon Bastionでの新しいバグ発見は対象外です。結果が示すのは、実行可能な失敗レポート、モデルが出した最小の編集、独立したブラウザ検証による修復ループです。

ハーネスで変わったこと

公開した実験は、ローカルモデルが説得力のあるゲームを生成できること、そしてバリデーターが見た目と挙動の差を暴けることを示しました。

この続編では、計画役を大きなモデルにし、長い推論の計測と前提の検証も加えました。

計画役を大きなモデルにしながら、境界のあるパッチインターフェースを保ちました。Qwenはscript.jsに対する小さな構造化編集を返しました。

ハーネスは長いローカル推論をシステムの問題として扱うようになりました。実際のバックエンド、モデルのプロセス、キャッシュ設定、ストリームのタイミング、メモリの下限を記録し、計画役をGPUの片側に置いて他のワークロードを退けます。

ハーネスは自分自身の前提もテストします。シードがキャッシュを無効化していました。HTTPクライアントが5分のタイムアウトを課していました。初期の修復実験では、間違ったディレクトリを検証するというまずい比較もしていました。どのミスも、観測可能にした時点で、より良い実験になりました。

結果

Radeon 780M上のQwen3.8-27Bは、最初の数字が示唆したよりも実用的です。

Q3 XL量子化は24Kコンテキストに収まります。Vulkanは本当にアクティブです。MTP-2は、テストしたワークロードでデコード速度を約67%上げます。コールドなリクエスト全体では約13%速くなるだけですが。キャッシュ済みの会話は数秒で応答し、既知のゲームのリグレッションは修復され、38/38で検証されました。

限界も同じくらいはっきりしています。コールドな20Kプロンプトは約6分かかります。利用可能なメモリで快適に常駐できる大きなモデルは1つです。成功した修復は、管理された1つのリグレッションを対象にしました。一晩の自律コーディングセッションは、まだ試していません。27Bモデルは、証拠にもとづくレビュアーおよび修復役としては優秀です。統合型のミニPCには、ワークステーション級のレイテンシ上の制約が残ります。

公開した記事は、生成されたコードの誤りをハーネスが見抜けるかを問いました。続編では、大きなローカルモデルが修復の候補を強くしました。合否を決めたのは、今回もハーネスの検証です。