「Single DGX Spark で動く」の甘い罠:Qwen3.8-Flash-Next-NVFP4 (99GB) 完全稼働までの泥臭い軌道修正と技術検証の記録
公式の謳い文句通りには動かない極限環境。AIが対症療法の蛸壺にハマる中、人間が手綱を引いてGDBでDMAスピンを突き止め、最小の1行cloneパッチと綿密なバジェット設計で完全稼働に導いた全記録
まずは人間から
手元にある NVIDIA DGX Spark(Grace Blackwell GB10 / 123 GiB Unified Memory)で、最新鋭の超巨大モデル Mia-AiLab/Qwen3.8-Flash-Next-NVFP4(計 99 GB) を常駐サービングさせようと試みました。
Qwen3.8-27B は Qwen3.8-27B-FP8 (MTP/SSM) の完全攻略記録 の通り問題なく動くようになっているのですが、Qwen3.8-Flash-Next が動いたら良いなと思いながら、量子化しても「DGX Spark 2台」でのような記事ばかり。
平日の通勤中に記事はいろいろ見ていて、非MoE層やサイドレイヤーを量子化する travelinlance/RadixArk-Hybrid-Sharp や blazux/qwen3.8-Flash-DGX は、N-gram 埋め込みテーブル(PLE)を NVMe 上のファイルを mmap でオンデマンド読み出しして、GPUに乗る量をギリギリ迄絞るとかしているのは知っていました。
で今回、GitHub リポジトリ(Qwen3.8-Flash-Next-Single-DGX-Spark)に、「Single DGX Spark (GB10) で動作!」 と書かれているのを見て。
「おお、71.84 GiB の NVFP4 本体を GPU に載せ、26.82 GiB に及ぶ n-gram PLE(Prefill Length Extension)埋め込みテーブルを CPU 側にオフロードする blazux と同じだけど、1台の Spark で 262k〜1M コンテキストが成立するのか!スクリプトを叩けば一発なのか」と期待して飛びつきました。
……が、現実はそんなに甘くありませんでした。
手元のクリーンな環境で ./start.sh を叩いてみると、全く動かない。
lazyでコールドブートすると、初期レイヤーの重みロードで完全に固まる(1テンソルあたり300秒のダンまり)- 回避策として
eagerに切り替えると、今度はホストメモリが吹き飛んで作者自作の watchdog に SIGKILL される - パッチを当ててようやく立ち上がったと思ったら、外部から推論リクエストを投げた瞬間に watchdog が安全停止を発動して落ちる
リポジトリの「動く」という言葉の裏には、「キャッシュが奇跡的に温まった状態(ウォーム状態)でのみ、ギリギリの綱渡りで動いていた」という強烈な前提が隠されていたのです。初見のコールドブート環境では地雷を踏み抜く構造になっていました。
そこで AI と一緒にトラブルシューティングを始めたのですが、ここでもう一つの戦いが始まりました。
「局所的なエラーメッセージに飛びついて、対症療法の蛸壺にハマっていく AI」を、人間が「待て待て、根本原因を見ろ」と手綱を引き戻す戦い です。
その結果のサマリがこちらです。
🎯 完全稼働達成のハイライト
- 「Single Spark で動く」の罠を看破
ARM64 64KBページ下で非アライン mmap を DMA 転送した際、ドライバがハードウェア完了待ちスピンを起こす仕様を特定。
- GDB による現行犯特定とベンチマーク実証
沈黙するプロセスへ GDB をアタッチしてcuMemcpyHtoDAsyncのスピンを特定。単体検証で「3分29秒 vs 0.25ms」の決定打を得る。
- AI の局所最適(蛸壺)を人間が軌道修正
「タイムアウト延長」「eager化」といった安易な対症療法を却下。GPU転送直前の1テンソルのみをCPU匿名ヒープに落とす最小パッチ(yield name, param.clone())へ誘導。
- 先人のインシデントログを踏襲したパッチ設計
過去のNV_ERR_NO_MEMORY記録を解読し、コンテナ直接編集を排して作者のビルドパイプライン(patch_*.py)に 100% 準拠。
- 重みロード時間の劇的短縮
1テンソル300秒スピン(数時間コース)から、全35シャード(99 GB)が 82.87 秒 で完了する爆速起動へ。
- 推論時の watchdog 誤爆を根治
推論リクエスト時のキャッシュ確保で落ちる過保護な監視を適正化(ホスト予約28 GiB、停止閾値4 GiB)。
- 実務タスクでの劇的進化
ターン処理が 20〜25% 高速化し、作業ペースは 約 2.1 倍 に倍増。ツール呼出・コード編集ともに 成功率 100%(リトライ 0 件) を達成。
やってみて思った事
今回のトラブルシューティングで最も強く痛感したのは、「AI に任せきりにすると、驚くほど簡単に局所最適(蛸壺)にハマる」 という現実でした。
ロードが止まれば「タイムアウト値を伸ばしましょう」、メモリが足りなければ「バッファサイズを弄りましょう」、監視で落ちれば「watchdog を止めましょう」。
AI は放っておくと、目の前で起きたエラー文の単語だけを拾って、場当たり的な「対症療法」を次々と繰り出してきます。
リポジトリの奥底を漁ると、files/sysctl-spark3.conf や docs/memory-incident-2026-09-04.md という生々しいインシデント記録が残されていました。そこには「MemFree が 3 GiB を切ると NVIDIA ドライバが NV_ERR_NO_MEMORY を吐いてマシンごと死ぬ」「spark1 で 3 回ハードハングした末にカーネルパラメータを弄ってようやく 6 回完走した」という、作者自身の血の滲むような格闘の痕跡が刻まれていたのです。
もし AI の言う通りに watchdog を止めて力技でメモリを突っ込んでいたら、確実にこの「死の崖」から転落していました。
AI: ロードが進まないので、まずはタイムアウト設定を増やして様子を見ましょう。あるいは
eagerモードに変更して一括で読み込みますか?
ぼぶ(心の声): 待て待て。それ、対症療法だろ。そもそも直前の行動は何だ? なんで 1 テンソルで何分も止まるんだ? ARM64 の 64KB ページカーネルで mmap してるんだから、アラインメントと DMA 転送でドライバがスピンしてるんじゃないのか? GDB でアタッチしてスタックトレースを見よう。
AI: ……ハッ!おっしゃる通りです。GDB で追うと
cuMemcpyHtoDAsync_v2の完了待ちバリアでスピンしています! 単体テンソルで試すと mmap 直転送は 3 分 29 秒かかりますが、.clone()すると 0.25 ms で終わります!
ぼぶ(心の声): ほら見ろ。じゃあ
eagerでメモリを爆発させずに、lazyのまま yield の直前で.clone()すればいい。コンテナ内を汚さずに、作者のpatch_*.pyパイプラインに組み込もう。
ってここまで完ぺきではなく、Gemini に GDBの細かい使い方などは聞きながらですけど。
AI の圧倒的なコード生成力やリファレンス探索力は本物です。しかし、「いま全体システムの中で何が起きているのか」「直前の変更がどう波及したか」を俯瞰し、安易な力技を却下して手綱を引くのは人間の仕事 だと改めて実感しました。
「木を見て森を見ない AI」を「森を見て規律を保つ人間」が方向修正し、事実(ログとコード)ベースで追い詰めていく。このペアプロの形こそが、現場で本当に動くシステムを作る最短ルートです。
以下に、直面した地雷の技術的背景から、開発したパッチ、そして実測ベンチマークまでの全記録を報告書としてまとめました。
技術報告書:DGX Spark (GB10) における Qwen3.8-Flash-Next-NVFP4 最適化と安定稼働検証
~ARM64 64KBページ環境でのDMAストール回避とUnified Memory空間の極限バジェット設計~
1. 検証環境・対象スペック
| レイヤー | 構成・仕様 | 備考 |
|---|---|---|
| ハードウェア | NVIDIA DGX Spark (Dell Pro Max with GB10 FCM1253) | 単一ノード (Single Spark) |
| プロセッサ | NVIDIA Grace Blackwell Superchip (GB10) | CPU: ARM64 72コア (Cortex-X925/A725) |
| GPU / メモリ | Blackwell Architecture (sm_121) / 123.73 GiB Unified Memory | |
| OS / カーネル | DGX OS 7.3.1 (Ubuntu 24.04 LTS) / 6.17.0-1018-nvidia-64k |
64KB ページサイズカーネル |
| サービング基盤 | Docker + vLLM (vllm/vllm-openai:qwen38-flash-next) |
v0.1.dev20073+g8e685d198 |
| 対象モデル | Mia-AiLab/Qwen3.8-Flash-Next-NVFP4 (計 98.57 GiB) |
本体 71.84 GiB + PLE 26.82 GiB |
| 実行構成 | TP=1, Context 262,144, FP8 KV, MTP=3, CUDA Graph Decode |
2. アーキテクチャの特異性:26.82 GiB の PLE オフロード機構
本モデルが Single DGX Spark(123 GiB)に収まる理由は、その極めて野心的なハイブリッド配置アーキテクチャにあります。
[ メモリ空間の分割配置アーキテクチャ (総計 123 GiB Unified Memory) ]
┌──────────────────────────── 123 GiB 統合物理メモリ ────────────────────────────┐
│ │
│ 【GPU バジェット: ~94.9 GiB】 【ホスト予約枠: 28 GiB】 │
│ ┌───────────────────────┬────────────────┐ ┌─────────────────────────────┐ │
│ │ モデル本体 (NVFP4) │ KV キャッシュ │ │ ホスト OS / Python / 各種デーモン│
│ │ 71.84 GiB │ 7.38 GiB │ │ │ │
│ │ │ (~44万 tok) │ ├─────────────────────────────┤ │
│ ├───────────────────────┴────────────────┤ │ PLE Offload Worker (CPU) │ │
│ │ 実行時オーバーヘッド + MTP (7.1 GiB) │ │ 26.82 GiB mmap テーブル │ │
│ └────────────────────────────────────────┘ │ (UNIX ドメインソケット IPC) │ │
│ └─────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────────────┘
- 本体(71.84 GiB): NVFP4 量子化されたトランスフォーマー層を GPU メモリに直接常駐。
- PLE テーブル(26.82 GiB): 3億2000万行におよぶ n-gram 埋め込みテーブル(
packed_u8)をPleOffloadWorkerがホスト CPU 側でmmap保持。GPU ワーカーとはローカル IPC(UNIX ドメインソケット)経由でゼロコピー通信を行い、プレフィックス計算を協調処理する。
この「ホストと GPU が 123 GiB をミリ単位で奪い合う」構造こそが、後述するすべてのトラブルの震源地でした。
3. 直面した課題:「動く」の裏に隠された二者択一のデッドロック
「Single Spark で動く」というリポジトリの構成をそのまま持ち込んだところ、コールドブート環境下では絶対に起動が成功しないデッドロックに直面しました。
[ safetensors-load-strategy の選択によるデッドロック ]
┌─── 【戦略 A: lazy (作者デフォルト)】
│ └──▶ 非アライン mmap 領域の DMA 転送でドライバがスピン
│ └──▶ 1 テンソルに 300 秒の HW タイムアウト ──▶ 【ロードが無限停止】
選択の岐路 ─┤
└─── 【戦略 B: eager (AI が最初に提案しがちな対症療法)】
└──▶ シャード展開でホストメモリが 39 GiB に急増
└──▶ MemAvailable < 6 GiB を割り込み ─────▶ 【watchdog が SIGKILL】
課題 1: lazy 読み込み時のドライバハング(DMA スピン待機)
- 現象:
./start.shを実行すると、重みロードの初期(0〜3% 付近)で進捗がピタリと止まる。 - GDB による C レベル追跡:
Python のログは完全に沈黙していたため、プロセスの PID を特定してgdb -pでアタッチして調査。スタックトレースの最深部は Python コードではなく、libcuda.so内のcuMemcpyHtoDAsync_v2直後、ハードウェア完了待ちバリアでスピンループしている現場 を特定。 - メカニズム:
lazyモードでは、safetensorsのsafe_openがファイルをmmapし、そのスライスポインタをそのままイテレータとして返します。しかし、Safetensors ファイル内のテンソル配置オフセットは、ARM64 カーネルの 64KB ページ境界にアラインされていません。非アラインな mmap 領域に対して NVLink-C2C 経由の DMA 転送を投げた結果、ドライバがハードウェア完了待ちでスピンし、1 テンソルあたり 300 秒のタイムアウトまで固まっていたのです。
決定打となった単体マイクロベンチマーク
問題のテンソル(model.language_model.layers.0.attn_hyper_connection.input_mix_weight_up.weight)を Python で単体抽出し、転送時間を実測比較しました。
- パッチ前(mmap 参照のまま転送): 3分29秒(209 秒) スピン
- パッチ後(
.clone()して転送): 0.25 ms(1/1000秒未満)
推測ではなく、この「3分 vs 0.25ミリ秒」というエビデンスを得たことで、解決の方向性が完全に定まりました。
課題 2: eager 読み込み時のホストメモリ爆発と強制終了
- 現象: AI が「
eagerに変えれば DMA は通る」と提案してきたため試すと、転送自体は速いものの、ロード途中でコンテナが突然消滅(Exit Code 137)。 - 原因分析:
eagerモードはシャードファイル全体を Python ホストメモリ(RAM)に一括展開します。本モデルは GPU ワーカーだけでなくPleOffloadWorkerも同時にシャードを走査するため、ホストメモリ消費量が 約 39 GiB まで急増。GPU 側が約 80 GiB を確保しているため、ホストのMemAvailableが 6 GiB を下回り、作者自作のメモリ監視スクリプト(memwatch.sh)が OOM 防止のためにコンテナを強制終了(SIGKILL)させていました。
4. 根本解決策:直前 1 テンソル匿名ヒープ化パッチの構築
「メモリを食わせない(lazy の利点)」と「DMA スピンを起こさない(eager の利点)」を両立させるため、vLLM の内部ローダーに介入するパッチを設計しました。
設計思想
mmapによるゼロコピー参照のままイテレーションを進める。- GPU へ転送する直前の 1 テンソルだけ を
.clone()し、ファイルバックではない純粋な CPU 匿名ヒープ(Anonymous Memory)に落とす。 - GPU への DMA 転送が完了すれば、直後の Python ガベージコレクションによって即座に解放される。
- ホストメモリの瞬間的な増加を「テンソル 1 個分(数 MB)」に抑えつつ、ページアライン問題を完全に解消する。
[ パッチ適用前 (直 mmap DMA) ]
Safetensors File (Disk) ──▶ mmap (Non-64KB aligned) ──[ cuMemcpyHtoDAsync ]──▶ Driver Spin (300s)
[ パッチ適用後 (.clone() 経由) ]
Safetensors File (Disk) ──▶ mmap ──▶ .clone() (CPU Heap / 匿名メモリ) ──[ cuMemcpyHtoDAsync ]──▶ 0.25 ms 完了
│
└──▶ GPU 転送直後に GC 解放(常駐メモリ増ゼロ)
作者の設計作法への敬意(行儀の良いパッチ設計)
コンテナ内に入ってファイルを直接書き換えるような「その場しのぎの力技」は排しました。作者が用意した「素のファイルを抽出し、patch_*.py で動的に書き換えて -v でマウントする」というビルドパイプラインの作法に 100% 倣い、files/patch_safetensors_clone.py を作成しました。
#!/usr/bin/env python3
import os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
ORIG = os.path.join(HERE, "weight_utils_patched.py.orig")
OUT = os.path.join(HERE, "weight_utils_patched.py")
def patch() -> None:
src = open(ORIG).read()
old_code = (
" param = f.get_tensor(name)\n"
" yield name, param\n"
)
new_code = (
" param = f.get_tensor(name)\n"
" yield name, param.clone()\n"
)
if src.count(old_code) != 1:
raise AssertionError(f"weight_utils: anchor missing or not unique (count={src.count(old_code)})")
src = src.replace(old_code, new_code, 1)
open(OUT, "w").write(src)
print("ok", OUT)
if __name__ == "__main__":
if not os.path.isfile(ORIG):
print(f"ERROR: missing {ORIG}", file=sys.stderr)
sys.exit(1)
patch()
これを start.sh のパイプラインに組み込むことで、リポジトリの整合性を保ったまま恒久的な自動適用を実現しました。
5. 第2の壁:推論時の watchdog 誤爆とメモリバジェット最適化
パッチ適用により、重みロードはわずか 110 秒 で完了し、API サーバーはポート 8888 で待機状態になりました。しかし、Windows 端末からテストリクエストを投げた直後、サーバーが突如応答不能に陥りました。
事実確認とログ分析
AI が「モデルがクラッシュした」と騒ぐのを制し、コンテナの状態とログを確認しました。
# コンテナ状態
Status: exited, ExitCode: 0, OOMKilled: false
# watchdog (memwatch-vllm-fn-tp1.log) の記録
2026-09-06 11:39:26 below MemAvailable floor 1/5: MemAvailable=6130 MiB MemFree=8870 MiB
2026-09-06 11:39:27 below MemAvailable floor 2/5: MemAvailable=6116 MiB MemFree=8847 MiB
2026-09-06 11:39:28 below MemAvailable floor 3/5: MemAvailable=6087 MiB MemFree=8800 MiB
2026-09-06 11:39:29 below MemAvailable floor 4/5: MemAvailable=6068 MiB MemFree=8763 MiB
2026-09-06 11:39:30 below MemAvailable floor 5/5: MemAvailable=6071 MiB MemFree=8751 MiB
2026-09-06 11:39:30 MemAvailable under 6 GiB for 5 samples -> stopping vllm-fn-tp1
- メカニズム:
ExitCode: 0、OOMKilled: false。エラークラッシュではなく、watchdog による正常停止シグナル(SIGTERM)。- 推論リクエストを受けた瞬間、26.82 GiB の PLE オフロードテーブルへの参照と KV キャッシュ確保が発生し、ページキャッシュとマッピングが増加。
- 物理的な
MemFreeは 8.7 GiB(8,751 MiB) も空いていたが、カーネルの空き予測値MemAvailableが 6,071 MiB まで低下。 - watchdog の閾値「
MemAvailable < 6 GiB(6,144 MiB)」を わずか 73 MiB 下回る状態 が 5 秒続いたため、システム保護が誤爆してコンテナを優しく停止させていた。
恒久対策の適用
先人のインシデントログ(min_free_kbytes=4GiB とウォーターマークの挙動)を踏まえ、監視スクリプトを殺すのではなく、パラメータを適正化しました。
# 1. ホスト予約枠を 26 GiB -> 28 GiB に拡大(GPU 側バジェットを約 2 GiB 微調整)
sed -i 's/HOST_RESERVE_GIB="${HOST_RESERVE_GIB:-26}"/HOST_RESERVE_GIB="${HOST_RESERVE_GIB:-28}"/' start.sh
# 2. watchdog の停止閾値を 6 GiB -> 4 GiB に緩和(実効空き容量に応じた現実的な値に設定)
sed -i 's/MEMWATCH_MIN_GIB="${MEMWATCH_MIN_GIB:-6}"/MEMWATCH_MIN_GIB="${MEMWATCH_MIN_GIB:-4}"/' start.sh
KV キャッシュ容量を実用上十分な 約 44 万トークン(262k コンテキストに対して約 1.7 並列分) 残したまま、推論中の MemAvailable を常時 8 GiB 前後(閾値 4 GiB に対して約 4 GiB の安全マージン)に維持することに成功しました。
6. 稼働検証・基本性能ベンチマーク結果
再起動後、Windows 開発機の Antigravity や curl からマルチターン対話および 3 並行負荷を投入し、サービング性能を測定しました。
起動およびメモリ推移
- 全シャード(35/35)ロード時間: 82.87 秒(最適化前は数時間スピン)
- 全体初期化完了時間: 約 193 秒
- 定常時メモリ安定性:
MemAvailable: 8,000〜8,300 MiB(極めて安定)MemFree: 8,400〜10,300 MiB- watchdog 警告カウント: 0 件
推論スループット(3並列リクエスト連続処理時)
(APIServer pid=1) INFO 09-06 04:32:55 [loggers.py:310] Engine 000: Avg prompt throughput: 0.0 tokens/s, Avg generation throughput: 70.5 tokens/s, Running: 3 reqs, Waiting: 0 reqs, GPU KV cache usage: 65.3%, Prefix cache hit rate: 72.4%
(APIServer pid=1) INFO 09-06 04:32:55 [metrics.py:120] SpecDecoding metrics: Mean acceptance length: 2.45, Accepted throughput: 41.69 tokens/s, Drafted throughput: 86.39 tokens/s, Accepted: 417 tokens, Drafted: 864 tokens, Per-position acceptance rate: 0.677, 0.451, 0.319, Avg Draft acceptance rate: 48.3%
| 指標 | 実測測定値 | 技術的評価 |
|---|---|---|
| 全体生成速度 | 65.0〜70.5 tokens/s | 100GB 級・単一 GPU ノードとして卓越した速度 |
| 単一リクエスト体感速度 | 約 22〜24 tokens/s | 3 多重実行時でも対話用途として非常に快適 |
| プリフィル処理速度 | 568〜762 tokens/s | 長文プロンプトの読み込み遅延がほぼ皆無 |
| MTP 平均採択長 | 2.30〜2.99 トークン | 3 トークン投機に対して約 2.5 トークンが常に採択 |
| MTP 投機採択率 (Pos 1) | 67.7%〜81.2% | 投機デコードによる実効スループット向上率 2 倍以上 |
| Prefix Cache ヒット率 | 71.3%〜78.3% | システムプロンプトや履歴の再計算が大幅にスキップ |
| KV Cache 使用率 | 63.6%〜68.2% | 3 多重の連続対話でもスラッシングなし |
7. 📊 実測データ比較: Qwen3.8-27B vs qwen3.8-flash-next
実際のソフトウェアエンジニアリング作業(MCSM-Agents による自律コーディングタスク実行)において、前世代モデルQwen3.8-27Bと今回の qwen3.8-flash-next で実務パフォーマンスを直接比較検証しました。
| 比較項目 | 従来の Qwen3.8-27B(9/5 実測) |
新モデル qwen3.8-flash-next(本日実測) |
判定・改善度 |
|---|---|---|---|
| 1ターンの平均所要時間 | 99.2 秒 | 79.1 秒 | 🚀 約 20%〜25% 高速化 |
| 単位時間(34分)のターン消化数 | 21 ターン | 45 ターン | ⚡ 約 2.1 倍のタスク処理ペース |
| ツールの実行成功率 | 概ね良好だが稀にリトライ発生 | 44 回中 44 回成功 (100%) (エラー・失敗 0件) |
🎯 ツール呼出精度が極めて正確 |
ファイル編集 (edit) 成功率 |
行ズレ等によるリトライが発生 | 4 回中 4 回すべて一発適用 (Applied cleanly) |
✨ 編集精度・追従性が向上 |
| 思考ループ / ストリーム切断 | 思考ループや失速の懸念あり | 35分間でループ・失速・切断 0 件 | 🛡️ 抜群の接続安定性 |
実務評価の総括:
単なるベンチマーク上のトークン速度向上にとどまらず、エージェントループにおける「ツール呼び出しの一発成功率」「コード編集の行ズレ解消」がターン全体の所要時間を大幅に圧縮しました。その結果、同じ作業時間(約34分)で消化できるタスク量が 2.1 倍に跳ね上がる という、開発体験上のブレイクスルーをもたらしています。
8. 運用・保守手順と実運用 Tips(ある意味自分用のメモ)
サービスの停止と起動
cd ~/Qwen3.8-Flash-Next-Single-DGX-Spark
# 正常停止(watchdog とログ退避を安全に完了)
./stop.sh
# 起動(安全のためページキャッシュクリアを挟む)
sudo sync && echo 3 | sudo tee /proc/sys/vm/drop_caches
./start.sh
リアルタイムログ監視
# vLLM の推論スループット・生成ログを追尾
docker logs -f --tail 50 vllm-fn-tp1
# watchdog のメモリマージン監視
tail -f logs/memwatch-vllm-fn-tp1.log
とりあえず、これで私が働いている間も、頑張ってエージェントに動いて貰えそうです。
悩ましいのは、このようにモデルを入れ替えると、半日はエージェントの作業が止まることです。
どちらを優先するか難しいのですが、でもこれだけ効率が上がるなら正解だと思います。
そして LLM のお陰で調査が本当に早くできるようになりました。昔だったらこの調査3日はかかると思います。
ありがたいですし、使いこなさなければです。