Redis 作者が DeepSeek V4 専用の推論エンジンを C で書いた ― DwarfStar(ds4.c) のアーキテクチャを読む【第1回/全8回】
本シリーズは、antirez(Salvatore Sanfilippo、Redis 作者)が公開した DeepSeek V4 Flash 専用推論エンジン DwarfStar(リポジトリ名
ds4)のコードを読み解く連載です。第1回は「なぜ専用エンジンを書くのか」という設計思想と、それを支える DeepSeek V4 の特異なアテンション構造の全体像を扱います。リポジトリ: https://github.com/antirez/ds4
※コードの行番号・定数は執筆時点(閲覧コミットba00a8a)のものです。実装は beta 品質(ds4-agentのみ alpha)で活発に変化するため、引用箇所は各自で再確認してください。
連載「DwarfStar(ds4) を読む」全8回
- 第1回 なぜ専用エンジンを書くのか(本記事)
- 第2回 非対称2bit量子化とimatrix
- 第3回 Metalグラフと圧縮KV
- 第4回 ディスクKVキャッシュ
- 第5回 サーバとDSMLツール呼び出し
- 第6回 TCPパイプライン分散推論
- 第7回 ネイティブエージェント
- 第8回 ステアリング・MTP・評価基盤
TL;DR
- DwarfStar は 汎用 GGUF ランナーではない。DeepSeek V4 Flash(および高メモリ機向けの Pro)1モデルだけを、ロード・プロンプト整形・ツール呼び出し・KV 永続化・HTTP API・コーディングエージェントまでエンドツーエンドで"完成"させることを狙った C 実装。
- GGML/llama.cpp にリンクしない完全自己完結。ただし量子化レイアウト・カーネルの知見は llama.cpp から借用しており、敬意を込めて LICENSE に著作権表示を残している。
- DeepSeek V4 のアテンションは MLA の発展形で、raw sliding-window KV + 学習済み圧縮器(compressor) + 疎選択器(indexer) の三層構造。これが「1M トークンコンテキストをローカル機で」を可能にする。
- 設計を貫く一文:「KV キャッシュは RAM のものという発想を捨てよ。圧縮 KV と高速 SSD の時代、KV キャッシュはディスクの一級市民だ」
1. 「1モデル特化」という賭け
ローカル推論の世界には優れたプロジェクトが無数にありますが、新モデルが出るたびに注目はそちらへ移っていきます。DwarfStar の README はこの状況に対して、意図的に狭い賭けをすると宣言します。
This project takes a deliberately narrow bet: one model at a time, official-vector validation (logits obtained with the official implementation), long-context tests, and enough agent integration to know if it really works.
つまり、
- 一度に1モデルだけを相手にする
- 公式実装で得た logits(official vector)に対して正しさを検証する
- 長文脈テストを回す
- 「本当に使えるか」を確かめられる程度にはコーディングエージェント統合まで作り込む
汎用ランナーが「広く・浅く」なりがちなのに対し、DwarfStar は「狭く・深く」を選びます。著者のビジョンは明快で、ローカル推論とは次の3つが箱から出してすぐに噛み合う状態を指す、というものです。
- A) HTTP API 付きの推論エンジン
- B) そのエンジンの前提に合わせて特別に作られた GGUF
- C) コーディングエージェント実装による検証
この A/B/C 一体という思想が、後の回で扱う「専用 GGUF 量子化」「DSML ツール呼び出し」「分散 KV 永続化」のすべての前提になっています。
もう一つ、コミュニティ的に正直で面白いのは、このコードが GPT-5.5 の強力な支援で書かれたと公言している点です。
バージョン表記「GPT-5.5」は README 原文(strong assistance from GPT 5.5)のままです。読者が誤記と取り違えやすい箇所なので、引用時は原文どおりである旨を添えると安全です。
This software is developed with strong assistance from GPT 5.5 ... If you are not happy with AI-developed code, this software is not for you.
「AI が書いたコードが嫌なら使うな」と言い切りつつ、人間がアイデア・テスト・デバッグを主導したと明記する。AI 時代の OSS のあり方としても読みどころです。
2. 対象モデル: DeepSeek V4 Flash / Pro の素性
エンジンが対象とするモデルの形状(shape)はコード内にハードコードされています。ds4.c の DS4_SHAPE_FLASH を見ると、モデルの全体像が一望できます。
// ds4.c
static const ds4_shape DS4_SHAPE_FLASH = {
.name = "DeepSeek V4 Flash",
.n_layer = 43,
.n_embd = 4096,
.n_vocab = 129280,
.n_head = 64,
.n_head_kv = 1,
.n_head_dim = 512,
.n_rot = 64, // RoPE は末尾 64 次元だけに適用
.n_expert = 256, // MoE: 256 エキスパート
.n_expert_used = 6, // うち 6 個をトークンごとにアクティブ
.n_expert_shared = 1, // + 共有エキスパート 1
.n_ff_exp = 2048,
.n_swa = 128, // raw sliding window = 128 トークン
.n_indexer_head = 64,
.n_indexer_head_dim = 128,
.n_indexer_top_k = 512, // 圧縮行から 512 行だけを選んで attend
.n_hc = 4, // Hyper-Connection: 4 本の残差ストリーム
.n_hc_sinkhorn_iter = 20,
.expert_weight_scale = 1.5f,
...
};
Pro 版(DS4_SHAPE_PRO)は 61 層 / embd 7168 / 128 ヘッド / 384 エキスパート / indexer top_k 1024 と、すべてがひと回り大きくなります。
注目すべきは MoE の構造です。256 個のルーテッドエキスパートのうち、1トークンあたり 6 個しか発火しない(n_expert_used = 6)。総パラメータは巨大(Flash で 284B 級)でも、トークンごとの実効計算量は小さい。これが「284B が 96/128GB の個人機で動くのに、27B/35B の dense モデルよりずっと"大きく感じる"」という README の主張の根拠です。
パラメータ規模は DeepSeek 公式モデルカード(HuggingFace、2026-06-01 取得時点) に基づきます。公式は Flash を 284B / アクティブ 13B、Pro を 1.6T / アクティブ 49B、コンテキスト 1M、FP4+FP8 Mixed と記載しています。モデルカードは更新され得るため、引用時は取得日を併記すると後からズレを追えます。本記事の n_expert_used 等は ds4.c のハードコード形状から確認した値です。
Being so large, Flash knows more things if you go sampling at the edge of knowledge.
3. アテンションの三層構造 ― MLA の発展形
DwarfStar の最大の見どころは、DeepSeek V4 の 圧縮アテンションです。一般的な Transformer は「全過去トークンの K/V を保持して attend する」ため、コンテキストが伸びると KV キャッシュが線形に膨張します。DeepSeek V4 はこれを三層に分けて解きます。
「直近は高精度のまま・古いものは圧縮・圧縮行は indexer で間引く」の三段構えが一目で分かります。以下、各層を順に見ます。
3-1. raw sliding-window KV(直近 128 トークン)
直近 DS4_N_SWA = 128 トークンの K/V を高精度のままリングバッファに保持します。ds4.c の kv_cache_push_raw() は、満杯になると 1 行スライドする素直な実装です。
// ds4.c: 直近 128 トークンの raw KV。満杯になると 1 行ずつスライド
static void kv_cache_push_raw(ds4_layer_cache *cache, const float *kv) {
if (cache->n_raw < cache->cap_raw) {
float *dst = cache->raw_kv + (uint64_t)cache->n_raw * DS4_N_HEAD_DIM;
for (uint32_t i = 0; i < DS4_N_HEAD_DIM; i++)
dst[i] = f16_to_f32(f32_to_f16(kv[i])); // F16 相当に丸める
cache->n_raw++;
return;
}
memmove(cache->raw_kv, cache->raw_kv + DS4_N_HEAD_DIM, ...); // スライド
...
}
3-2. compressor(学習済み圧縮器)
128 トークンより古い情報は捨てるのではなく、圧縮して保持します。ここが核心です。圧縮率は層ごとに異なり、層のパリティで 4 と 128 を交互に切り替えます。
// ds4.c: 層ごとの期待圧縮率
static uint32_t ds4_expected_layer_compress_ratio(uint32_t il) {
switch (DS4_MODEL_VARIANT) {
case DS4_VARIANT_FLASH:
if (il < 2) return 0; // 最初の2層: 圧縮なし(局所のみ)
return (il & 1u) == 0 ? 4u : 128u; // 偶数層: 4:1 / 奇数層: 128:1
case DS4_VARIANT_PRO:
if (il < 2) return 128u;
return (il & 1u) == 0 ? 4u : 128u;
...
}
}
- ratio 0(Flash の最初の2層): 圧縮器なし。raw sliding window だけ=純粋な局所アテンション。
- ratio 4(偶数層): 4トークンを1行に畳む軽い圧縮。indexer 付き。
- ratio 128(奇数層): 128トークンを1行に畳む極めて重い圧縮。indexer なし(行が少ないので全部見られる)。
「軽い圧縮で細かく見る層」と「重い圧縮で大局を見る層」を交互に積むことで、局所と大局の両方を低コストで扱う、という設計です。層ごとの圧縮率を並べると、偶奇で 4 と 128 が交互に並ぶのが分かります。
層 : 0 1 2 3 4 5 6 ... 41 42
ratio : 0 0 4 128 4 128 4 ... 128 4
└ 局所のみ │ └──────────────────────────┘
└ ratio 4 = 軽圧縮 + indexer(top-512)
ratio 128 = 重圧縮 (indexer なし / 行が少ない)
compressor は単なる平均プーリングではありません。スコアによる softmax 加重プーリング + 位置バイアスという学習済み機構です。compressor_decode_one() の本体を追うと、
// ds4.c: 1トークン分の圧縮器更新(抜粋)
// 1) 層出力 x から「KV 行」と「スコア行」を同時に射影
matvec_q8_0_pair_prequant(kv_cur, sc_cur, model, wkv, wgate, xq, xscale);
// 2) スコアに位置依存の加法バイアス(APE: additive positional encoding)を足す
for (uint32_t j = 0; j < width; j++)
sc_cur[j] += tensor_2d_value(model, ape, j, pos_mod);
// 3) ロール状態(frontier)に書き込む
memcpy(state_kv + (uint64_t)row * width, kv_cur, ...);
memcpy(state_score + (uint64_t)row * width, sc_cur, ...);
// 4) ratio 境界(4 or 128 トークンごと)で 1 圧縮行を吐く
if (should_compress) {
compressor_pool_decode_state(pooled, state_kv, state_score, head_dim, ratio);
// RMS 正規化 → tail RoPE → FP8 量子化(往復) ...
}
compressor_pool_decode_state() の中身は、スコアを logit とみなした softmax 加重平均です。
// out[j] = Σ_r softmax(score[r][j]) * kv[r][j]
for (uint32_t r = 0; r < compress_ratio; r++) {
const float w = expf(state_score[r*width + j] - max_score);
denom += w;
sum += w * state_kv[r*width + j];
}
out[j] = denom > 0.0f ? sum / denom : 0.0f;
つまり compressor は「直近 N トークンのうち、どれをどれだけ残すか」を学習されたスコアで重み付けして 1 行に畳む小さなアテンションなのです。位置バイアス(APE)により、N トークンの中の相対位置も考慮されます。
3-3. indexer(疎選択器)
ratio-4 の層では、圧縮行が大量に貯まります(1M コンテキストなら 25 万行)。全部に attend するのは重いので、indexer が「いま重要な top-k 行」だけを選びます。
indexer は本体アテンションとは独立した、細い(head_dim 128)クエリ系を持ちます。スコア上位 n_indexer_top_k = 512(Pro は 1024)行だけを本体アテンションに渡すことで、計算量を実質一定に抑えます。
// ds4.c: indexer は ratio==4 の層にだけ確保される
if (ratio == 4) {
cache->layer[il].index_comp_kv = xmalloc_zeroed(comp_cap * DS4_N_INDEXER_HEAD_DIM, ...);
cache->layer[il].index_state_kv = ...; // indexer 用の frontier
cache->layer[il].index_state_score = ...;
}
4. 「KV キャッシュはディスクの一級市民」
三層構造の帰結として、DeepSeek V4 の KV キャッシュは異常に小さいです。README の数値では、フル 1M トークンコンテキストでも約 26GB、しかもその大半(約22GB)が indexer です。
この圧縮率が効くと、世界観が変わります。従来「KV キャッシュは RAM に置くもの、コンテキストを使い切ったら捨てるもの」でした。しかし圧縮 KV と現代の高速 SSD を組み合わせれば、KV キャッシュをディスクに永続化して、セッションを跨いで再利用できる。
The KV cache is actually a first-class disk citizen.
第3回で扱う Metal グラフ上の圧縮 KV、第4回で扱う DSV4 ペイロードと SHA1 ディスク KV キャッシュ、第5回で扱うサーバのプレフィックス再利用、第7回で扱うエージェントの「セッション = オンディスク KV」は、すべてこの一文から派生しています。
5. もう一つの柱: thinking が「短い」
性能面で DwarfStar が DeepSeek V4 Flash を推す理由として、thinking(思考)セクションの短さが挙げられています。
In thinking mode, if you avoid max thinking, Flash produces a thinking section ... even 1/5 of other models in many cases, and crucially, the thinking section length is proportional to the problem complexity.
思考の長さが問題の複雑さに比例するため、簡単な問いには短く、難しい問いには長く考える。これにより、他モデルでは thinking を有効にすると実用に耐えない状況でも、Flash は thinking を有効にしたまま使える、という主張です。エンジン側も DS4_THINK_NONE / HIGH / MAX の3モードを持ち、コンテキスト長に応じて Think Max を自動的にフォールバックさせる作りになっています(ds4_think_mode_for_context())。
この「1/5」「複雑さに比例」は README 著者の定性的な観察であり、原文に比較対象・プロンプト数・temperature・コンテキスト長・平均/中央値といった測定条件は示されていません。本記事でも「作者が自プロジェクトで観察した主張」として読んでください。定量比較が必要なら、同一条件(同一プロンプト集合・同一サンプリング・同一コンテキスト)で thinking トークン数を実測する必要があります。
6. バックエンドとビルド
production パスは Apple Silicon の Metal が主ターゲット、次いで Linux の CUDA(特に DGX Spark / GB10 に配慮)。CPU パスは「参照・デバッグ用」で、しかも macOS の VM 実装のバグでカーネルクラッシュを起こすため使うなと強く警告されています。
この macOS カーネルクラッシュの記述は README 著者の警告そのままで、原文には対象 macOS バージョン・機種・再現条件・issue/radar 番号が示されていません。実害の大きい警告なので、実際に CPU パスを試す前に最新 README と issue を確認してください。本番は Metal/CUDA を使う前提なので、通常の利用では踏まない経路です。
make # macOS Metal
make cuda-spark # Linux CUDA, DGX Spark / GB10
make cuda-generic # その他の CUDA GPU
make cpu # CPU 専用(診断用)
主要バイナリは ds4(CLI)と ds4-server(HTTP API)、加えて ds4-agent / ds4-eval / ds4-bench。
次回予告
第2回は、128GB の Mac で 284B モデルを動かす核心 ―「非対称 2bit 量子化」と「imatrix(重要度行列)」を扱います。「ルーテッド MoE エキスパートだけを 2bit に落とし、それ以外は触らない」という割り切りが、なぜ品質を保ったまま巨大なサイズ削減を実現するのか。gguf-tools/ のコードを読みながら、IQ2_XXS / Q2_K のブロックレイアウトまで降りていきます。
参考: 第1回で触れた主なコード
| 項目 | 場所 |
|---|---|
| モデル形状定義 |
ds4.c DS4_SHAPE_FLASH / DS4_SHAPE_PRO
|
| 層ごとの圧縮率 |
ds4.c ds4_expected_layer_compress_ratio()
|
| KV キャッシュ確保 |
ds4.c kv_cache_init()
|
| 圧縮器本体 |
ds4.c compressor_decode_one() / compressor_pool_decode_state()
|
| 設計思想 |
README.md "Motivations" 節 |
本記事は Quick Iterate のローカル LLM 研究の一環として、公開リポジトリ antirez/ds4 のコードを読み解いたものです。行番号・定数・ベンチ値は閲覧コミット ba00a8a(2026-05-30)/README 取得日 2026-06-01 時点のものです。ds4-agent は alpha、エンジン本体は beta 品質で活発に変化するため、引用箇所は各自で最新の README / ソースに当たって再確認してください。
クイックイタレート株式会社 ― IoT / 電力監視 / AI / 衛星・無線通信 / システムインテグレーション
ローカル LLM・エージェント基盤に関するお問い合わせはお気軽にどうぞ。