複数 Mac で 1 つのモデルを動かす: DwarfStar の TCP パイプライン分散推論を読む【第6回/全8回】
本シリーズは、DeepSeek V4 Flash / Pro 専用推論エンジン DwarfStar(
ds4)のコードを読み解く連載です。
第6回は、モデルを層スライスに分けて複数マシンで動かす分散推論を扱います。主な参照箇所:
README.md,ds4_distributed.c,ds4_distributed.h,ds4.h読みどころ: 分散で「prefill は速く・生成は遅い」という非対称が生まれる理由と、それでも複数台で動かす価値がどこにあるかが要点です。
連載「ds4.c を読み解く」全8回
TL;DR
- DS4 の分散推論は、巨大な GGUF を複数マシンに分割して載せるための仕組み。各プロセスは同じ GGUF を参照するが、
--layersで自分の層スライスだけをマップする。 - コーディネータはトークナイズ、サンプリング、プロンプト、先頭の層スライスを持つ。ワーカーは後続の層スライスと自分の KV キャッシュを持つ。
- 活性化はコーディネータ中継ではなくワーカー間で流せる。典型経路は
A -> B -> C -> A。 - prefill はパイプライン化できるため速くなる。README の Thunderbolt 5 / 2 台 M5 Max では 63,819 トークンのプロンプトで 1.85x。
- 生成はトークンごとに logits とサンプリングを待つ自己回帰経路なのでパイプライン化できない。同じ構成で 19.4% 遅くなる実測が載っている。
- ワイヤプロトコルは
DS4Dマジック、HELLO/WORK/RESULT/SNAPSHOT_*フレーム。WORK にはセッション ID、トークンスパン、prefix/result ハッシュ、ルート、隠れ状態ペイロードが入る。 - ワーカーはローリング 64bit トークンプレフィックスハッシュを検証し、再起動後に位置 N の作業を誤って受けない。
- 永続 KV は単一マシンと同じ
DSV4ペイロードに集約される。保存時はワーカー所有の層テンソルをコーディネータが集める。
1. 目的は「速くする」だけではない
分散推論と聞くと、まず高速化を想像します。しかし DS4 の README は、目的を 2 つに分けています。
- 1 台に載らないモデル / 量子化を複数マシンに分けて載せる
- 長い prefill を複数 GPU でパイプライン化して速くする
代表例として、フル 4bit Flash 量子化を 2 台の 128GB MacBook に分割する構成が挙げられています。各プロセスは同じ GGUF を持ちますが、--layers で読み込むテンソルスライスを限定します。
# Machine A: coordinator, layers 0..19
./ds4 \
-m gguf/DeepSeek-V4-Flash-Q4KExperts-...gguf \
--role coordinator \
--layers 0:19 \
--listen 169.254.43.68 1234
# Machine B: worker, layers 20..output
./ds4 \
-m gguf/DeepSeek-V4-Flash-Q4KExperts-...gguf \
--role worker \
--layers 20:output \
--coordinator 169.254.43.68 1234
20:output は層 20 から最終層までに加えて出力ヘッドもワーカーが持つ、という意味です。最終ワーカーが logits を直接返せるため、prefill 後に巨大な隠れ状態バッチをコーディネータに戻す必要がありません。
2. コーディネータとワーカーの役割
README のメンタルモデルは次の通りです。
- GGUF は全マシンに置く
- ただし各マシンは
--layersで部分集合だけをマップする - コーディネータはプロンプト、トークナイズ、サンプリング、通常の CLI/API 動作を持つ
- ワーカーは自分の層スライスと KV キャッシュを持つ
- 活性化の流れはコーディネータ経由に限らず、ワーカー間で流れる
ds4_distributed.c の冒頭のコメントも、ワーカーがローカルエンジンと同じグラフスライスのエントリポイントを使うと説明しています。
/* Workers execute contiguous model slices with the same graph-slice entry
* points used by the local engine. KV snapshots remain topology-independent:
* save gathers worker-owned layer tensors into the normal DSV4 payload, and
* load splits a normal DSV4 payload across the currently registered route.
*/
分散専用の別グラフを作るのではなく、既存の層スライス API をネットワーク転送でつないでいるわけです。
3. prefill は組立ラインになる
長いプロンプトの prefill は、多数のトークンを一括で処理できます。DS4 はこれをチャンクに分け、パイプライン化します。
README は「コーディネータがチャンク N+1 を処理している間にワーカーがチャンク N を処理できる」と説明しています。これは組立ラインです。
2 台構成で考えると、
time 1: A processes chunk 0 layers 0-19
time 2: B processes chunk 0 layers 20-output | A processes chunk 1 layers 0-19
time 3: B processes chunk 1 layers 20-output | A processes chunk 2 layers 0-19
...
各チャンクは順に層スライスを通る必要がありますが、異なるチャンクは異なるマシン上の異なるステージに載せられます。
README の実測では、2 台 M5 Max 128GB、Thunderbolt 5、Q4 Flash GGUF、4096 トークンの分散 prefill チャンクで、以下の数字が載っています。ここで注意したいのは、この表は厳密な同条件比較ではない点です。分散側(Two MacBooks)は Q4 Flash GGUF ですが、single-process reference 列は Q2 GGUF の単機実行で、ルーテッド MoE が小さいぶん参照側が本来やや有利です。つまり下の Speedup は「同じ量子化を 1 台 vs 2 台で比べた純粋なスケーリング」ではなく、量子化違いを含んだ目安として読む必要があります。
| Prompt | Single-process reference (Q2) | Two MacBooks (Q4) | Speedup |
|---|---|---|---|
| 9,421 tokens | 421.70 t/s | 582.22 t/s | 1.38x |
| 28,684 tokens | 405.30 t/s | 674.16 t/s | 1.66x |
| 63,819 tokens | 353.62 t/s | 654.79 t/s | 1.85x |
プロンプトが長いほどパイプラインが埋まり、高速化が伸びています。
この表は README 掲載の実測値です。再現性のため、引用時は commit / OS / GGUF ファイル名 / チャンクサイズ / power(thermal) 状態 / single-run か平均か、を脚注に固定すると安全です(README 原文では 2 台 M5 Max 128GB・Thunderbolt 5・Q4 Flash・4096 トークンチャンクと読み取れますが、commit や thermal 条件までは明示されていません)。本記事では「公開された参考値」として扱ってください。
4. 生成は速くならない
一方、デコード/生成は厳密に自己回帰です。
トークン N の logits が出て、サンプリングでトークン N+1 が決まるまで、次トークンの計算を始められません。prefill のようにチャンク N+1 を先行させることができません。
分散生成では、毎トークンの活性化がネットワークホップを通ります。
coordinator slice -> worker slice -> logits -> coordinator sampling
そのため README は、同じ Thunderbolt の構成で 12k コンテキストの対照実行が 30.59 t/s から 24.67 t/s に落ち、19.4% の損失と報告しています。
分散推論は、デコードを速くするためではなく、
- 1 台では載らないモデル / 量子化を動かす
- 長い prefill を速くする
ための機能と見るべきです。prefill と生成の非対称性を図にすると一目瞭然です。
5. ワイヤプロトコル: DS4D フレーム
ds4_distributed.c のプロトコル定数は次の通りです。
#define DS4_DIST_MAGIC 0x44533444u /* DS4D */
#define DS4_DIST_MSG_HELLO 1u
#define DS4_DIST_MSG_ERROR 2u
#define DS4_DIST_MSG_WORK 3u
#define DS4_DIST_MSG_RESULT 4u
#define DS4_DIST_MSG_SNAPSHOT_SAVE_REQ 5u
#define DS4_DIST_MSG_SNAPSHOT_BEGIN 6u
#define DS4_DIST_MSG_SNAPSHOT_CHUNK 7u
#define DS4_DIST_MSG_SNAPSHOT_DONE 8u
#define DS4_DIST_MSG_SNAPSHOT_LOAD_BEGIN 9u
フレームヘッダはマジック、タイプ、バイト数だけの固定形式です。
typedef struct {
uint32_t magic;
uint32_t type;
uint32_t bytes;
} ds4_dist_frame_header;
プロトコルはリリース安定ではなく、README でも同じ commit からビルドした信頼できるマシンで使う前提と説明されています。暗号化や認証もありません。
6. HELLO: ワーカー登録
ワーカーはコーディネータに制御接続を張り、HELLO を送ります。
ds4_dist_hello_fixed は以下のような情報を持ちます。
typedef struct {
uint32_t model_id;
uint32_t quant_bits;
uint32_t layer_start;
uint32_t layer_end;
uint32_t has_output;
uint32_t has_hidden;
uint32_t ctx_size;
uint32_t n_layers;
uint32_t listen_port;
uint32_t model_name_len;
} ds4_dist_hello_fixed;
コーディネータはこの登録を見て、全層を連続的に覆うルートを作ります。モデル ID、量子化プロファイル、コンテキスト容量、層範囲が一致しないワーカーはルートに入れません。
ルートはコーディネータのローカルスライスの後から始まる連続したチェーンです。
7. WORK: トークンスパン、ハッシュ、ルート、活性化を運ぶ
分散処理の中心は WORK フレームです。ds4_dist_work_fixed には、セッション / リクエスト ID、トークン位置、ハッシュ、ルート、ペイロードサイズが入ります。
typedef struct {
uint32_t model_id;
uint32_t session_hi;
uint32_t session_lo;
uint32_t request_hi;
uint32_t request_lo;
uint32_t prefix_hash_hi;
uint32_t prefix_hash_lo;
uint32_t result_hash_hi;
uint32_t result_hash_lo;
uint32_t pos0;
uint32_t n_tokens;
uint32_t layer_start;
uint32_t layer_end;
uint32_t flags;
uint32_t token_bytes;
uint32_t input_hc_bytes;
uint32_t input_hc_bits;
uint32_t route_count;
uint32_t route_index;
uint32_t route_bytes;
} ds4_dist_work_fixed;
ここで prefix_hash は作業適用前、result_hash はトークンスパン適用後のローリングトークンプレフィックスハッシュです。
flags には以下があります。
#define DS4_DIST_WORK_F_INPUT_HC 0x00000001u
#define DS4_DIST_WORK_F_OUTPUT_LOGITS 0x00000002u
#define DS4_DIST_WORK_F_RESET_SESSION 0x00000004u
#define DS4_DIST_WORK_F_ACK_ONLY 0x00000008u
最終ワーカーが出力ヘッドを持つ場合は logits を返します。prefill パイプラインの途中チャンクでは ACK のみのこともあります。
8. RESULT: ACK / 隠れ状態 / logits
ワーカーからの RESULT は、種別を持ちます。
#define DS4_DIST_RESULT_ACK 0u
#define DS4_DIST_RESULT_HIDDEN_STATE 1u
#define DS4_DIST_RESULT_LOGITS 2u
ds4_dist_result_fixed にはリクエスト ID、スパン後ハッシュ、ステータス、結果種別、テレメトリ、ペイロードサイズなどが入ります。
テレメトリには層範囲、トークンスパン、ローカル評価時間、下流待ち時間、転送送信時間、入出力バイト数が含まれます。
README の --debug 説明にある「per-hop telemetry」はこの情報です。層分割のバランスやネットワークボトルネックを見るための実装です。
9. 活性化の転送は 32/16/8 bit
通常、グラフスライス API は float バッファを交換します。分散転送では、ワイヤ上の活性化幅を変えられます。
ds4_distributed.c のコメント
* The graph-slice APIs exchange float buffers. Distributed transport can leave
* those buffers as 32-bit floats or pack them to 16/8 bits on the wire; workers
* decode back to float before executing the next slice.
有効なビット幅は 32, 16, 8 です。
return bits == 32u || bits == 16u || bits == 8u;
16bit は f16 変換、8bit は E4M3 風の f8 変換です。
CLI では --dist-activation-bits 16 や --dist-activation-bits 8 を使います。
README では、16bit 転送はトラフィックを半分にする最初の選択肢、8bit は近似的/実験的と説明されています。ただし実験上、活性化サイズの削減は大きな改善を出しておらず、将来削除されるかもしれないとも書かれています。
10. ローリングトークンプレフィックスハッシュで KV 整合性を見る
分散推論で最も怖いのは、
ワーカーの KV 状態がコーディネータのトークン履歴とずれることです。
DS4 は作業ごとにローリング 64bit トークンプレフィックスハッシュを検証します。実装は FNV-1a です。
#define DS4_DIST_TOKEN_HASH_INIT 1469598103934665603ull
#define DS4_DIST_TOKEN_HASH_PRIME 1099511628211ull
コメントは、これはセキュリティ要素ではなくセッション不変条件だと説明しています。
/* FNV-1a over little-endian token IDs. This is not a security primitive; it is
* a compact session invariant so distributed workers can reject same-position
* but different-prefix KV state before doing layer work. */
ワーカーは「自分が位置 N にいる」と思っていても、プレフィックスハッシュが違えば作業を拒否します。再起動したワーカーが空の KV のまま位置 N の作業を黙って受けることを防げます。
README では、ワーカーのハッシュ不一致はトークン履歴の再生で回復できる一方、転送障害はルートを破棄し、互換ワーカーの再接続を待つと説明されています。
11. スナップショットはトポロジ非依存の DSV4 ペイロード
第4回で見た DSV4 ペイロードは、分散構成でも同じです。
ds4_distributed.c の冒頭コメント:
* KV snapshots remain topology-independent:
* save gathers worker-owned layer tensors into the normal DSV4 payload, and
* load splits a normal DSV4 payload across the currently registered route.
プロトコルにはスナップショットフレームがあります。
DS4_DIST_MSG_SNAPSHOT_SAVE_REQ
DS4_DIST_MSG_SNAPSHOT_BEGIN
DS4_DIST_MSG_SNAPSHOT_CHUNK
DS4_DIST_MSG_SNAPSHOT_DONE
DS4_DIST_MSG_SNAPSHOT_LOAD_BEGIN
チャンクサイズは 8MiB です。
#define DS4_DIST_SNAPSHOT_CHUNK_BYTES (8u * 1024u * 1024u)
保存時はコーディネータがワーカーのデータ接続を開いてワーカー所有の層テンソルを取得し、通常の層順の DSV4 ペイロードにまとめます。ロード時は現在のルートに応じて層テンソルを分配します。
ファイルに「このセッションは 2 台構成で保存した」といったトポロジは残りません。残るのはモデルレイアウトに対する層ごとの状態です。
12. ネットワークリンクの影響
README のネットワーク比較は、分散推論の性能特性をよく示しています。
同じ 2 台 M5 Max、同じ 91GB Flash 量子化、8192 トークンのプロンプト、128 生成トークンで、
| Link | Ping avg | Prefill | Generation |
|---|---|---|---|
| Thunderbolt 5 | 0.45 ms | 582.99 t/s | 25.09 t/s |
| WiFi | 77.20 ms | 250.70 t/s | 10.70 t/s |
| Internet / VPN | 152.10 ms | 114.88 t/s | 3.63 t/s |
prefill は大きなチャンクを流せるため、レイテンシの影響をある程度ならせます。生成はトークンごとの往復が効くため、レイテンシにほぼ直接引っ張られます。
したがって、対話的なコーディングエージェントとして使うなら Thunderbolt / 高速 Ethernet が現実的です。Internet/VPN は「動かないモデルを共同で覗き見る」用途なら意味がある、という位置づけです。
13. まとめ
DS4 の分散推論は、テンソル並列ではなく層パイプラインです。
- 各マシンは連続した層スライスを持つ
- ワーカーは自分のスライスの KV キャッシュを保持する
- 活性化は TCP フレームで次のホップに流す
- prefill はチャンクパイプラインで速くなる
- 生成は自己回帰なので速くならず、ネットワークレイテンシ分だけ遅くなる
- プレフィックスハッシュでワーカー KV の整合性を検証する
- スナップショットはトポロジ非依存の
DSV4ペイロードとして保存される
この設計は、MacBook を複数台つないで「1 台には載らない量子化を動かす」ための実装としてかなり現実的です。特に 4bit Flash を 128GB クラスの複数マシンで見る用途には、README の数字からも意味があります。
次回は、HTTP サーバを介さずにエンジンを直接操作するネイティブコーディングエージェントを読みます。ここではセッションそのものがオンディスク KV キャッシュになり、DSML と JSON の変換境界もさらに薄くなります。
本記事は クイックイタレート株式会社 のローカル LLM 研究の一環として、
公開リポジトリ antirez/ds4 のコードを読み解いたものです。行番号・定数・ベンチ値は閲覧コミット ba00a8a(2026-05-30)/README 取得日 2026-06-01 時点のものです。ds4-agent は alpha、エンジン本体は beta 品質で活発に変化するため、引用箇所は各自で最新の README / ソースに当たって再確認してください。
クイックイタレート株式会社
IoT / 電力監視 / AI / 衛星・無線通信 / システムインテグレーション/
ローカル LLM・エージェント基盤に関するお問い合わせはお気軽にどうぞ。