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

EXL3とvLLMでDeepSeek V4.1 Flashを2台のDGX Sparkでローカル実行

2台のNVIDIA GB10 DGX SparkシステムでDeepSeek V4.1 Flash EXL3をダウンロード、設定、起動、テスト、最適化する方法を解説します。自動ランチャー、Engramストレージ、DSpark投機的デコード、メモリ保護、同期オプション、APIリクエスト、テスト、パフォーマンス調整について説明します。

EXL3とvLLMでDeepSeek V4.1 Flashを2台のDGX Sparkでローカル実行

概要

このプロジェクトは、2台のNVIDIA GB10 DGX Sparkシステム上でDeepSeek V4.1 Flashをローカル提供します。2.9 bits-per-weightのEXL3チェックポイント、両ノードにまたがるテンソル並列処理、ネイティブのsm_121aカーネル、ポート8888で動作するOpenAI互換のvLLM APIを使用します。

提供されるモデル識別子はDeepSeek-v4.1-Flash-EXL3です。このデプロイメントはテキスト専用で、CX7経由で接続された2ノードのGB10キット向けに設計されています。

コンテナは、必要なDeepseekV41アーキテクチャを含むARM64 vLLMイメージvllm/vllm-openai:deepseekv41-flash-0909にEXL3サポートを重ねて提供します。

プロジェクトが提供する機能

  • ローカルのOpenAI互換サービング: アプリケーションは標準の/v1/chat/completionsエンドポイントを呼び出せます。
  • 2ノードのテンソル並列処理: モデルはCX7接続上でテンソル並列サイズ2として動作します。
  • EXL3量子化: チェックポイントは平均2.9 bpwで、mul1コードブックとテンソル単位の量子化設定を使用します。
  • ネイティブDSpark投機実行: ドラフトエキスパートはチェックポイント内のmtp.*にすでに保存されているため、別のドラフトモデルは必要ありません。
  • 長いコンテキストへの対応: 同梱の設定ではMAX_MODEL_LEN=600000を指定し、2.5 GiBのKVキャッシュプールを使用します。
  • ファイルバックドEngramテーブル: 量子化されていないEngramデータはホストメモリに固定せず、元のモデルシャード47および48から読み込みます。
  • 自動起動: start.shは重みをダウンロードし、イメージを取得またはビルドし、ワーカーとファイルを共有し、GPUチェックを実行してAPIを起動します。
  • ランタイム保護: ランチャーは起動時の余裕を確認し、プリフィルフェーズ間でアロケーターを解放し、ロード後にページキャッシュを破棄し、コンテナのOOMスコアを調整します。

ハードウェアとストレージの要件

このリポジトリは、正確に2台のNVIDIA GB10 DGX Sparkノードを対象としています。デフォルトのヘッドノードアドレスは10.0.0.1で、ノード間はCX7経由で通信します。READMEでは、テスト済みのインターフェースとして、1台目のノードにenp1s0f1np1/rocep1s0f1、2台目のノードにenp1s0f0np0/rocep1s0f0を指定しています。

モデルデータには約387 GiBを見込んでください。

  • Mia-AiLab/DeepSeek-V4.1-Flash-EXL3-2.9bpwのEXL3シャード39個に約197 GiB。
  • 元のDeepSeekシャード47および48とインデックスに約190 GiB。これらにはネイティブEngramテーブルが含まれます。
  • 一致するイメージをビルドせずに取得できる場合、公開コンテナイメージに約9 GiB。

すでに完全なDeepSeek-V4.1-Flashディレクトリがある場合は、ENGRAM_DIRにそのディレクトリを指定して、Engramのソースを再度ダウンロードしないようにできます。この目的ではシャード47、48とインデックスのみを読み込みます。

DGX Sparkでは、ホストRAMもGPUメモリです。すべてのCUDAアロケーションは共有メモリを直ちに消費するため、ストレージ設定とメモリ設定は個別に変更し、長いプロンプトでテストしてください。

ヘッドノードを準備する

1. リポジトリをクローンして環境ファイルを作成する

ヘッドSparkのリポジトリディレクトリからデプロイメントを実行します。

cp .env.example .env

ランチャーは、.envが存在しない場合、初回実行時にも作成します。ファイルを確認し、システムに応じて必要であればWORKER_USERやGIDなどの値を更新してください。

.envで明示的に指定した値は、コマンドラインの環境変数プレフィックスより優先されます。これはstart.shが継承した環境より先にファイルを読み込むためです。たとえば、WEIGHT_SYNC=zfs ./start.shを実行するのではなく、.env内のWEIGHT_SYNCを変更してください。

2. Hugging Faceのダウンロードツールをインストールする

モデルの自動ダウンロードには、転送サポート付きのHugging Face CLIが必要です。

pip install -U 'huggingface_hub[hf_transfer]'

ダウンロードは再開可能です。中断された実行によってモデルが不完全な状態で残った場合は、ランチャーまたはダウンローダーをもう一度実行してください。

3. サーバーを起動せずにダウンロードする(任意)

vLLMを起動する前にEXL3ファイルとEngramファイルを準備するには、次を実行します。

./download.sh

自動ダウンロードはデフォルトで有効です。ファイルを自分で管理する場合は、設定でAUTO_DOWNLOAD=0を指定してください。

サーバーを起動する

ヘッドノード上のリポジトリから次を実行します。

./start.sh

初回実行時、ランチャーは次の処理を大まかに実行します。

  1. 利用可能なメモリの余裕とノード設定を確認します。
  2. 不足しているEXL3ファイルとEngramファイルをダウンロードします。
  3. 公開イメージghcr.io/miaai-lab/deepseek-v4.1-flash-exl3-2x-dgx-sparks:2.9bpwを取得します。
  4. レシピスタンプが変更されている場合、またはレジストリを利用できない場合にのみローカルでビルドします。
  5. ワーカーが重みを利用できるようにします。
  6. EXL3 GEMMとEngramのデ量子化GPUチェックを実行します。
  7. 2つのテンソル並列ランクを起動し、ポート8888でAPIを公開します。

クリーンチェックアウトは公開イメージ内のレシピスタンプと一致するため、通常はコンパイルは必要ありません。

イメージのビルドと転送を制御する

  • BUILD=1 ./start.sh は常にイメージをローカルでビルドします。
  • SKIP_BUILD=1 ./start.sh は、レシピのスタンプが異なる場合でも、取得済みのイメージをそのまま使用します。
  • SKIP_PULL=1 ./start.sh は、コンテナレジストリへの接続を回避します。
  • PULL=1 ./start.sh は、ローカルのスタンプが一致していてもイメージを再取得します。
  • SKIP_SHIP=1 ./start.sh は、head がイメージを worker にコピーするのを防ぎます。

デフォルトでは、イメージの転送に、中断・再開可能な rsync で転送するステージ済みの tar ファイルを使用します。IMAGE_SHIP=stream を設定すると、docker save の出力を worker の docker load にパイプします。ストリーミングには一時領域が不要ですが、転送が中断されると最初から再開されます。auto モードでは、再開可能な転送を優先し、worker のディスク容量が限られている場合はストリーミングにフォールバックします。

デプロイメントの確認と管理

2 つのコンテナを個別に操作するのではなく、ランチャーの管理コマンドを使用します。

./start.sh status
./start.sh logs
./start.sh logs worker
./start.sh stop
./start.sh restart

worker に同期済みの重みがすでにある場合は、同期手順を繰り返さずに再起動します。

SKIP_SYNC=1 ./start.sh restart

起動中、420 秒間ログが出力されない状態を検出すると、両方の rank から Python のスタックトレースを収集します。診断ファイルは logs/ 配下に書き込まれます。

最初の API リクエストを送信する

サーバーが正常な状態になったら、head ノードにチャット補完リクエストを送信します。このスモークテスト用リクエストでは、直接的な回答を確認しやすくするため、思考を無効にしています。

curl -s http://127.0.0.1:8888/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"DeepSeek-v4.1-Flash-EXL3","messages":[{"role":"user","content":"What is 17*19? Reply with the integer only."}],"max_tokens":32,"temperature":0,"chat_template_kwargs":{"enable_thinking":false}}'

期待される回答は 323 です。OpenAI 互換クライアントであれば、同じベース URL とモデル識別子を使用できます。

推奨されるサンプリング設定を使用する

通常のモデル利用では、公式のサンプリング設定は次のとおりです。

  • temperature=1.0
  • top_p=0.95
  • 思考を有効化

チャットテンプレートのデフォルトは高い推論強度で、75 として表されます。また、50 を表す low、100 を表す max、または 1 から 100 までの整数も受け付けます。

推奨されるサンプリング設定を使用したリクエストは、次のようになります。

curl -s http://127.0.0.1:8888/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"DeepSeek-v4.1-Flash-EXL3","messages":[{"role":"user","content":"Explain how tensor parallel inference works."}],"max_tokens":512,"temperature":1.0,"top_p":0.95,"reasoning_effort":"high"}'

DSpark 投機的デコーディングを理解する

このデプロイメントでは、チェックポイントに埋め込まれたドラフトエキスパートを使用して DSpark 投機的デコーディングを実行します。別途ドラフターをダウンロード、読み込み、提供する必要はありません。

基盤となる vLLM 設定は次と同等です。

--speculative-config '{"method":"dspark","num_speculative_tokens":3}'

チェックポイントのドラフトブロックサイズ上限は 5 ですが、文章では 3 個の投機トークンの方が高速だったため、提供時の設定には 3 個を使用しています。キャプチャサイズには 1 2 3 4 6 8 12 18 24 が含まれます。3 個の投機トークンを生成するシーケンスを 2 つ処理するためのサイズ 6 も含まれます。

報告された測定では、DSpark により単一ストリームのデコーディングが改善されます。より大きなバッチでは、SPEC_METHOD=none で無効にしてください。これにより約 3.5 GiB も解放されます。README では、投機を有効にした 1 ストリームで毎秒約 31.6 トークンを報告しています。一方、投機を無効にしたサービスでは、4 ストリームでより高い合計スループットを達成しました。

メモリを安全に管理する

ノードごとのおおよその割り当てには、rank ごとに 99.5 GiB の EXL3 重み、2.5 GiB の KV プール、コンテキスト、NCCL、ワークスペース、CUDA グラフ用の約 5~7 GiB、さらに vLLM プロセス、Docker、オペレーティングシステム、デスクトップ用の約 9 GiB が含まれます。

提供時の上限は次のとおりです。

  • MAX_MODEL_LEN=600000
  • MAX_NUM_SEQS=2
  • MAX_NUM_BATCHED_TOKENS=1024
  • 2.5 GiB の KV キャッシュプール

KV のレイアウトには、DeepSeek の fp8_ds_mla が自動的に選択されます。--kv-cache-dtype は指定しないでください。測定時のプールは、コンテキスト上限 614,400 で 774,400 トークンを保持し、コストは 1 トークンあたり約 3.4 KiB でした。

このチェックポイントの量子化を、2.9 を整数 2 に変換して推定しないでください。量子化パラメーターはテンソルごとに選択され、ルーティングされる各エキスパート、共有エキスパート、アテンション行列、Engram テンソル、MTP テンソル、言語モデルヘッドでは異なる値が使用されます。

ウォームアップ後、報告されたテストでは head に約 4.07~4.21 GiB の空きメモリが残りました。601,000 トークンのプリフィルでは、この余裕が約 2.1 GiB まで減少しました。そのため、起動直後ではなく、長いプリフィルの後に変更を検証してください。

メモリ関連の設定は、一度に 1 つだけ増やしてください。README では、KV プールに 0.5 GiB を追加すると、プリフィルの余裕が約 1 GiB 減少する可能性があると警告しています。3 GiB のプールは短いスモークテストには合格しましたが、600,000 トークンのプロンプトで約 470,000 トークンに達した時点で失敗しました。

オプションのメモリガードを慎重に使用する

scripts/memguard.sh は /proc/meminfo を 1 秒ごとに確認し、DSV41_MEM_GUARD_GIB(デフォルトのしきい値は 1.5 GiB)を下回るサンプルが 2 回連続すると、ローカルのサービングコンテナを強制終了します。

このガードは、メモリを消費したプロセスを特定できないため、デフォルトでは無効になっています。無関係なホスト上のワークロードがメモリ圧迫を引き起こした場合でも、必ず自身の vLLM コンテナを停止します。明示的に有効にするには、次を設定します。

DSV41_MEM_GUARD=1

ホスト側で負荷の大きいジョブを実行する場合、README では代わりに原因となっているジョブを制限することを推奨しています。

systemd-run --scope -p MemoryMax=2G <job>

デプロイでは、両方のコンテナに引き続き --oom-score-adj 1000 を使用し、ブート時の余裕を確認する事前チェック、prefill ごとのアロケーター解放、ロード後のページキャッシュ破棄も行います。

重み同期戦略を選択する

NFS:デフォルト

NFS は、テスト済みの構成に適した方式です。vllm-fn-nfs エクスポーターを再利用し、EXL3 ディレクトリと slim Engram ディレクトリを公開して、ワーカーコンテナにそれぞれ /model と /engram-src としてマウントします。

./start.sh share
./start.sh

start.sh が共有手順を自動的に実行するため、明示的な share コマンドは、主にエクスポートの準備やトラブルシューティングで使用します。

ZFS:ノードローカルレプリカ

.env に WEIGHT_SYNC=zfs を設定すると、各ノード上にローカルデータセットを保持できます。ZFS は耐障害性を高め、初回レプリカ作成後は変更されたブロックのみを送信できます。また、スナップショットベースのロールバックも可能です。

代償は大きく、各ノードにおよそ 385 GiB の空きプール容量が必要です。README によると、テストで使用した 2 台目の Spark は空き容量が約 41 GiB しかなかったため、そこで ZFS は使用できませんでした。

各ノードでの代表的な設定は次のとおりです。

sudo apt-get install -y zfsutils-linux
sudo zpool create -f models <vdev>
sudo zfs create -o recordsize=1M -o atime=off -o compression=lz4 \
  -o mountpoint=/path/to/model models/dsv41-exl3
sudo zfs create -o recordsize=1M -o atime=off -o compression=lz4 \
  -o mountpoint=/path/to/engram models/dsv41-engram
sudo zfs allow -u "$WORKER_USER" create,destroy,mount,receive,snapshot,rollback models

Engram データセットには、シャード 47 と 48、およびインデックスだけを配置します。476 GiB のネイティブモデルツリー全体をコピーしないでください。プールまたはワーカーの zfs recv が利用できない場合、ランチャーはストリーミングにフォールバックします。

Rsync:より簡単なローカルコピー

WEIGHT_SYNC=rsync は、ZFS プールを必要とせずに、通常のノードローカルコピーを作成します。ZFS のブロックレベル差分更新には対応していません。

Prefill を高速化するために Engram データをパックする

Engram テーブルはネイティブの FP8 形式のまま維持され、量子化された EXL3 ツリーには含まれません。デフォルトでは、ファイルバックの埋め込み実装が、レイヤーごとに約 47 GiB をメモリへ固定せずに行を取得します。

ランク固有の Engram 行ファイルを各ノードのローカル NVMe に移動するには、次を実行します。

./start.sh pack

パッキングにより、テンソル並列サイズ 2 の場合、ノードごとに約 94 GiB が書き込まれます。これは任意であり、起動に必須ではありません。報告されたテストでは、行のミス時に NFS ではなくローカルのダイレクト I/O が使用されたため、ローカルにパックした Engram ファイルによって prefill の性能が約 25~50 パーセント向上しました。

Engram の行を非量子化カーネルに渡す際は、fp8_e4m3fn 型のままにする必要があります。uint8 として扱うと、もっともらしく見えても正しくない出力が生成される可能性があります。

検証テストを実行する

リポジトリには、PyTorch や vLLM を必要としないソースレベルのチェックが含まれています。

python3 tests/test_numeric_config.py
python3 tests/test_engram_src.py
python3 tests/test_engram_secondary.py
python3 tests/test_k_map.py
python3 tests/test_memory_log.py
python3 tests/test_exl3_lm_head.py
python3 tests/test_sm120_block64.py
python3 tests/test_h2d_stage.py
python3 scripts/weight_budget.py --tp 2

チャットテンプレートの整合性テストには、transformers と元モデルの encoding/encoding.py が必要です。

python3 tests/test_chat_template.py --src /path/to/DeepSeek-V4.1-Flash

API の稼働後、エンドツーエンドのスモークテストを実行します。

bash tests/test_smoke.sh

スモークテストではモデルに 17*19 の計算を求め、323 を期待します。サービスを起動する前に、start.sh は一時コンテナ内で GPU の整合性チェックと Engram の非量子化チェックも実行します。結果は logs/overlay-verify.log に書き込まれます。

高度な運用上のヒント

  • 1~2ストリームではDSparkを優先: 報告された単一ストリームのデコード速度は毎秒31.6トークンで、2ストリーム時の合計は毎秒42.5トークンでした。
  • バッチ幅を広げる場合は投機実行を無効化: SPEC_METHOD=none では単一ストリームのスループットが低下しましたが、報告されたテストでは4ストリームの合計スループットが毎秒53.7トークンに達しました。
  • 長いプロンプトを繰り返し使用する場合はEngramをパック: ローカルNVMeの行ファイルにより、ヘッドノードのNFSエクスポートへの依存を減らし、プリフィル速度を大幅に向上できる場合があります。
  • デプロイメントはテキスト専用に維持: SM120カーネルの対応範囲には、モデルのビジョントークン幅に必要なsparse-MLAカーネルがありません。
  • EXL3の補助ストリームを安易に再有効化しない: 異なるCUDAストリーム上の2つのカーネルが、デバイスごとに1つしかないランタイムのロックバッファでデッドロックする可能性があるため、デプロイメントではEXL3ストリームを直列化しています。
  • 正しいネットワークインターフェースを監視: NCCLは10.0.0.xのループバックエイリアスを使用できません。NICごとのGID選択を誤ると、約1分後にibv_modify_qpのerrno 61で失敗することがあります。
  • 長時間のプリフィル時のメモリを確認: 起動に成功し、短いスモークテストを通過しても、より大きなKVプールやコンテキスト設定が安全だとは限りません。
  • リポジトリのランチャーを使用: NFSまたはローカル同期、事前チェック、イメージ検証、2ランク起動、診断を調整します。

予想される性能

公開された測定では、1つのデコードストリームで毎秒31.6トークン、2ストリームの合計で毎秒42.5トークンが報告されています。パック済みEngramデータとグループ化されたエキスパートカーネルを使用した短いプロンプトのプリフィルは、毎秒約965~1,041トークンでした。

長いプロンプトのプリフィルは、当然ながら低速になります。報告例では、100,000トークンのプロンプトで毎秒約979トークン、455,000トークンで毎秒802トークン、601,000トークンで毎秒810トークンでした。実際の結果は、プロンプトの形状、ストレージ構成、投機実行、バッチ幅、設定したプリフィルチャンクサイズによって異なります。

結論

このプロジェクトには、2台のGB10 DGX SparkでDeepSeek V4.1 Flashを提供するために必要な専用カーネル、EXL3ローダー、ファイルバックドEngram実装、ネットワーク設定、メモリ制御、ランチャーがまとめられています。付属のデフォルト設定から始め、スモークテストでAPIを確認してから、一度に1つの設定だけを調整してください。最も安全に運用するには、現実的な長時間プリフィルのワークロードでスループットと利用可能なメモリの両方を評価してください。