
DGX Spark 1 台で 2.8T の Kimi K3 を動かしてみた
はじめに
こんにちは、クラスメソッド製造ビジネステクノロジー部の森茂です。
DGX Spark を使っていると、統合メモリ 121 GiB という数字がそのまま天井になります。これより大きいモデルは動かない。2 台つないでも上限は 170GB 前後で、そこから先は諦めるしかありませんでした。
そんな中、搭載メモリの天井を無視して動くエンジンを見つけました。Pulsar です。MoE の routed expert を NVMe に置いたまま、トークンごとに読み出しながら推論します。
先に結論を書いておくと、2.8 兆パラメータの Kimi K3 が 634.6GB のまま 1 台で動きました。121 GiB の 4.9 倍、従来の上限からは 3.7 倍です。GLM-5.2 744B も 211GB から 434GB まで 3 段階で動きました。動きはした、という表現の方が正しいかもしれませんが:)
ただし、そこに辿り着くまでに前提がいくつも崩れています。dGPU 向けに共有されているチューニングの定石が、統合メモリではほとんど効きません。キャッシュを増やしても速くならず、llama.cpp の --n-cpu-moe に至っては 39% 遅くなりました。
前回は同じ機体で llama.cpp を使い、DeepSeek V4 Flash を 1 台に載せています。
Pulsar は expert を NVMe に置いたまま動かす
MoE のモデルは、パラメータの大半を routed expert が占めます。しかも 1 トークンで使うのはそのうち数個だけです。DeepSeek V4 Flash なら 256 個のうち 6 個、Kimi K3 なら 896 個のうち 16 個しか触りません。
Pulsar はこの性質を使います。expert を NVMe に置いたままにして、必要になったものだけを読み出す。attention と router と shared expert、つまり毎回必ず使う部分だけを常駐させます。README の表現を借りると、ディスクの読み出し速度がそのまま生成速度になる設計です。
Rust と CUDA で書かれていて、llama.cpp 標準の GGUF を読みます。作者のベンチはこうなっています。
| モデル | 総パラメータ | active | GGUF | decode |
|---|---|---|---|---|
| Qwen3.6-35B-A3B | 35B | 3B | 22GB | 51.8 |
| Laguna-S-2.1 | 118B | 8B | 36GB | 17.3 |
| DeepSeek-V4-Flash | 284B | ~8B | 87GB | 8.2 |
| GLM-5.2 | 744B | 40B | 211GB | 2.7 |
| Kimi K2.7 | ~1T | 32B | 339GB | 1.3 |
reference box は RTX 5060 Ti 16GB と RTX 4060 Ti 16GB、Ryzen 9900X、システム RAM 30GB、Gen5 NVMe です。合計 32GB の VRAM で 339GB のモデルを回している計算になります。
この表で気になったのは Laguna-S-2.1 と DeepSeek-V4-Flash の関係です。active はどちらも 8B なのに、17.3 と 8.2 で 2 倍の差がある。違うのは GGUF のサイズだけです。作者の RAM は 30GB なので、36GB の Laguna はほぼ全部がホスト側のキャッシュに載り、87GB の V4 Flash は 3 分の 1 しか載らない。速度を決めているのは active パラメータ数ではなく、expert プールのうちどれだけキャッシュに載るか、と読めます。
だとすると、統合メモリ 121 GiB の DGX Spark は有利なはずです。作者の 4 倍のキャッシュ予算を持っているわけですから。
DGX Spark でビルドして NVMe の実力を測る
README は Linux と NVIDIA GPU を要求していて、GTX 10 シリーズ以降なら動くとあります。aarch64 の記載はありません。DGX Spark の GB10 は compute capability 12.1、いわゆる sm_121 です。
crates/kernels/build.rs を読むと、既定では sm_61 から sm_89 までの SASS を吐いて、それ以降は PTX からの JIT に落とす作りでした。コメントに PULSAR_CUDA_ARCH で上書きできると書いてあったので、sm_121 を直接指定してみます。CUDA 13.0 なら GB10 のネイティブコード生成に対応しています。
export PATH="$HOME/.cargo/bin:/usr/local/cuda/bin:$PATH"
export PULSAR_CUDA_ARCH=121
export CXX=g++-13
cargo build --release -p engine -p serve -p quant
ソースには一切手を入れず、53.5 秒で通りました。aarch64 でビルドしたという報告は upstream にも見当たらないので、そのまま動いたのは幸運だったかもしれません。
次に NVMe の速度を測ります。Pulsar はディスク速度が生成速度を決めると明言しているので、ここが分からないと結果を解釈できません。ところが DGX Spark の NVMe を実測した記録が、自分の過去の検証にもコミュニティにも見当たりませんでした。
ページキャッシュを経由すると実力より速く出てしまうので、dd の O_DIRECT で測ります。手元の GGUF を 20 GiB ぶん読み出しました。
dd if=<20GB 超の GGUF> of=/dev/null bs=16M count=1250 iflag=direct
| ブロックサイズ | 帯域 |
|---|---|
| 1 MiB | 4.9 GB/s |
| 4 MiB | 6.8 GB/s |
| 16 MiB | 11.1 GB/s |
ブロックサイズで 2.3 倍も動くのは意外でした。小さいブロックだと I/O 発行のオーバーヘッドが支配的になるようです。16 MiB まで大きくすると 11.1 GB/s に届きます。
作者の Gen5 NVMe が 7 GB/s 程度とのことなので、帯域では上回っています。ホストキャッシュも 4 倍。準備としては悪くありません。
まず llama.cpp でどこまで載るか確かめる
Pulsar を試す前に、そもそも普通に llama.cpp で何 GiB まで載るのかをはっきりさせておきます。ここが分からないと、SSD ストリーミングを使う理由も決まりません。
前回は DeepSeek V4 Flash の UD-IQ3_XXS、97.1 GiB まで載せました。今回あらためて測ると残りが 19GB あったので、もう少し上を狙えそうです。1 段階上の UD-IQ3_S は 116GB、GiB に直すと 108.1 GiB。
結果は無事に載りました。
| 量子化 | サイズ | used | 残り | ロード | decode |
|---|---|---|---|---|---|
| UD-IQ3_XXS | 97.1 GiB | 102GB | 19GB | 421s | 16.63 tok/s |
| UD-IQ3_S | 108.1 GiB | 113GB | 8GB | 482s | 16.15 tok/s |
| UD-Q3_K_XL | 119.2 GiB | — | — | — | 載らない見込み |
残り 8GB で ctx 2048 が動きます。次の量子化は 119.2 GiB なので、残りが 2GB 程度になって KV キャッシュを置く余地がありません。DGX Spark で llama.cpp が扱える実用上限は 108.1 GiB あたりと見てよさそうです。
ついでに前回と同じ UD-IQ3_XXS を同じビルド、同じベンチスクリプトで回しています。前回が 16.56 tok/s、今回が 16.63 tok/s で差は 0.4%。環境が当時から変わっていないことの確認になりました。地味ですが、以降の数字を前回の記事と並べて読めるかどうかがここで決まります。
壁を越えて 434GB まで試す
選んだのは GLM-5.2 744B です。antirez さんが 3 段階の量子化を配布しています。
| ファイル | サイズ | 121 GiB(約 130GB)比 |
|---|---|---|
| UD-IQ2_XXS_RoutedIQ2XXS_blk78Q2K | 211GB | 1.6 倍 |
| UD-Q2_K_RoutedQ2K | 262GB | 2.0 倍 |
| UD-Q4_K_RoutedQ4K | 434GB | 3.3 倍 |
211GB は 23.6 秒でロードされ、そのまま生成に入りました。2 ノードでも 170GB が上限だった機体です。拍子抜けするほどあっさり動きます。
3 段階を並べると、途中で様子が変わります。
| 量子化 | サイズ | 121 GiB(約 130GB)比 | host cache | 実効ディスク | decode | 前段比 |
|---|---|---|---|---|---|---|
| IQ2_XXS | 211GB | 1.6 倍 | 84% | 16% | 2.12 tok/s | — |
| Q2_K | 262GB | 2.0 倍 | 81% | 19% | 2.07 tok/s | −2.4% |
| Q4_K | 434GB | 3.3 倍 | 73% | 27% | 1.13 tok/s | −45% |
211GB から 262GB はサイズが 24% 増えても 2.4% しか落ちません。ところが 434GB では 45% 落ちる。線形でも対数でもない、はっきりした崖があります。
崖の位置を説明するのはヒット率でした。キャッシュ予算 80GB に対して、211GB と 262GB は人気のある expert が収まりきる。434GB では溢れて、ディスクを読む割合が 27% まで増えます。効いているのはモデル全体のサイズではなく、よく使う部分が予算に収まるかどうかです。
そしてここでは、キャッシュ予算が素直に効きます。211GB のモデルで 20GB から 80GB まで振ると 1.66 から 2.12 tok/s、+28%。モデルが物理メモリを超えると、Pulsar 本来のディスク律速に戻るわけですね。
なお予算を 100GB まで上げると機体が応答しなくなりました。MLA 系のモデルは attention stack だけで 14GB ほど常駐するので、キャッシュと足して 121 GiB を食い尽くします。この機体での実用上限は 80GB あたりでした。
2.8T の Kimi K3 が動いた
最後に Kimi K3 です。2.8 兆パラメータ、UD-IQ1_S で 634.6GB、14 shard に分かれています。121 GiB の 4.9 倍。
README のモデル表に K3 は載っていません。当初はそれを見て未対応だと思ったのですが、ソースを開くと crates/engine/src/real/k3.rs が 659 行あるようでしたが、実装はあるが作者もベンチしていない、という状態です。
634.6GB を落として動かすと、
pulsar: split gguf: 14 shards as one virtual file
pulsar-cli: blk.0.ssm_f_b.weight: expected f16, got Q8_0
ssm_f_b は KDA、Kimi Delta Attention の forget gate bias です。unsloth の配布ファイルではこれが Q8_0 になっていて、ローダーが f16 を要求して弾いています。
該当箇所を見ると、同じ構造体の中で 1 行だけ扱いが違いました。
f_a: MatW::load(&file, &gguf, &t("ssm_f_a.weight"))?,
f_b: upload_f16_as_f32(&file, &gguf, &t("ssm_f_b.weight"))?, // ここだけ f16 必須
beta_w: MatW::load(&file, &gguf, &t("ssm_beta.weight"))?,
a: upload_as_f32(&file, &gguf, &t("ssm_a"))?,
すぐ下の ssm_a は型を問わない upload_as_f32 を使っています。そしてその関数のコメントに、答えが書いてありました。
pulsar-quant falls back to q8_0 for rows that are a multiple of 32 but not 256, which is how ssm_alpha/ssm_beta arrive on some GDN models. Dequantize rather than refuse the file.
GDN、Gated DeltaNet 系のモデルでは ssm 系のテンソルが q8_0 で来る。拒否せず dequantize しろ、と作者自身が書いています。KDA は Gated DeltaNet の派生なので、K3 がまさに該当します。1 行だけ更新が漏れていたようです。
upload_as_f32 に差し替えてリビルドすると 31.6 秒。動きました。
pulsar: split gguf: 14 shards as one virtual file
pulsar: loaded in 66.4s (93 layers, 896 experts x top-16)
pulsar: unified memory detected - zero-copy expert resolve
pulsar: prefill 23 tokens in 39.17s
pulsar: 64 tokens in 91.80s (0.70 tok/s), vram cache 0% hits, host cache 37% of remainder
2.8 兆パラメータのモデルが、統合メモリ 121 GiB の機体で 0.70 tok/s です。93 層、896 expert から 16 個を選ぶ構成がそのまま表示されています。
1 トークンに 1.4 秒。人間が音読するより遅い速度で、2.8 兆パラメータが 1 台の机上マシンから絞り出されてくるのは、なかなか味わい深い眺めでした。実用かと言われると返答に困りますが、動かないはずのものが動いているので、まあいいことにします。
K3 だけは、キャッシュを増やすと逆に遅くなりました。20GB で 0.70 tok/s、40GB で 0.63 tok/s。expert が 896 個あって 1 トークンで 16 個読むので、GLM の 256 個から 8 個に比べるとプールが 3.5 倍、読み出しが 2 倍です。

モデルサイズ順に decode 速度を並べたもの。青が統合メモリ 121 GiB に収まるモデル、オレンジが超えるモデル。右端の淡い数字はホストキャッシュのヒット率で、これが落ちるほど速度も落ちている。211GB と 262GB がほぼ並ぶ一方、434GB で一段、635GB でもう一段下がる。
ただし載るモデルなら llama.cpp のほうが速い
大きいモデルが動いたので、では小さいモデルでも Pulsar を使えばいいのかというと、そうではありませんでした。
121 GiB に収まる DeepSeek V4 Flash で比べます。前回 llama.cpp で測ったのと同じ UD-IQ2_M、84.7 GiB です。
pulsar: loaded in 6.0s (43 layers, 256 experts x top-6)
pulsar: unified memory detected - zero-copy expert resolve
pulsar: 128 tokens in 18.46s (6.94 tok/s), vram cache 0% hits, host cache 90% of remainder
生成長を変えて 2 点測り、傾きから固定オーバーヘッドを分離すると定常 8.04 tok/s。まったく同じファイルなので、前回の数字とそのまま並べられます。
| エンジン | モデル | 常駐方式 | ロード | decode |
|---|---|---|---|---|
| llama.cpp | UD-IQ2_M 84.7 GiB | 全量メモリ常駐 | 6 分 22 秒 | 17.35 tok/s |
| Pulsar | UD-IQ2_M 84.7 GiB | expert ストリーム | 6.0 秒 | 8.04 tok/s |
生成速度では llama.cpp が 2.2 倍速い。やはりメモリに載るモデルにストリーミングを使う理由はありませんでした。
ついでに面白い発見もありました。同じ V4 Flash でも、Pulsar 向けに作られた ds4 recipe の GGUF なら 10.09 tok/s 出ます。unsloth 版より 25% 速い。attention を Q8_0 で高精度に残して routed expert だけを 2 bit まで落とす、という配分になっていて、expert だけをストリーミングする Pulsar とは相性がいいようです。Pulsar を使うなら量子化の選び方も変わってくる、というのは覚えておくとよさそうです。
ただしロード時間は逆転します。6 分 22 秒に対して 6.0 秒、64 倍の差です。llama.cpp は 121 GiB の統合メモリで GPU バッファとページキャッシュが食い合い、ファイルサイズの 1.4 倍を読み直す羽目になっていました。Pulsar は expert を常駐させないので、その問題自体が起きません。
常駐サーバーとして動かすなら llama.cpp、都度起動して使い捨てるなら Pulsar、という住み分けになりそうです。
統合メモリで効かない 3 つの定石
dGPU 向けに共有されているチューニングが、軒並み効きませんでした。
原因は 1 つに行き着きます。メモリ階層が 1 段少ないことです。
VRAM に入らない分を RAM へ、RAM に入らない分をディスクへ。この 2 段構えが 1 段に潰れると、既存の最適化は同じ物理メモリの中で荷物を持ち替えるだけになります。
llama.cpp の --n-cpu-moe は逆効果になる
routed expert だけを VRAM からシステム RAM へ逃がすオプションです。24GB の RTX 3090 で 108 GiB のモデルを 12.5 tok/s で動かした報告や、RTX 5070 の 12GB で 4.87 倍速くなった計測が出ています。
V4 Flash は 43 層なので、逃がす層数を振ってみました。
--n-cpu-moe |
used | buff/cache | 合計 | ロード | prefill | decode |
|---|---|---|---|---|---|---|
| 0 | 113GB | 4GB | 117GB | 482s | 394.51 | 16.15 tok/s |
| 10 | 90GB | 31GB | 121GB | 361s | 216.31 | 9.86 |
| 22 | 62GB | 59GB | 121GB | 201s | 42.84 | 6.58 |
| 43 | 13GB | 103GB | 116GB | 50s | 1.14 | 4.67 |
増やすほど遅くなりました。全層を逃がすと prefill は 394.51 から 1.14 tok/s、346 分の 1 です。
理由は合計欄に出ています。used が減った分だけ buff/cache が増えて、合計は 116 から 121GB のまま動きません。逃がした先が同じメモリなので、持ち替えの手間だけが増えます。
ただしロード時間は 482 秒から 50 秒へ、10 分の 1 になりました。起動を速くする代わりに推論を犠牲にするツマミとしてなら、使い道があるかもしれません。
キャッシュ予算を積んでも速くならない
Pulsar のチューニングは、README によればホストキャッシュの予算が最大のツマミです。87GB のモデルで 10GB から 100GB まで 10 倍に振ってみました。
| 条件 | host cache | decode |
|---|---|---|
PULSAR_CACHE_GB=10 |
60% | 6.68 tok/s |
PULSAR_CACHE_GB=100 |
86% | 6.92 tok/s |
10 倍にして +3.6%。ヒット率は 60% から 86% まで動いているのに、速度がついてきません。作者の環境で V4 Flash を 8.2 から 11.3 tok/s まで引き上げる CPU lane も、ここでは +1.0% でした。
87GB のモデルは 121 GiB の予算に丸ごと収まるので、そもそもディスクをほとんど読んでいないからです。前の章で GLM の 211GB では +28% 効いたのと同じ理屈で、物理メモリを超えて初めてディスクが律速になります。
VRAM キャッシュは使われない
vram cache 0% hits が全条件で出続けました。ソースを追うと、統合メモリを検出した時点で VRAM プールを確保する処理そのものがスキップされ、PULSAR_DEV_CACHE_GB は読まれてすらいません。
// (unified boxes keep the 1-byte cache: zero-copy resolve)
dev_cache: DeviceSlabCache::new(1, max_slab)?,
PULSAR_UNIFIED=0 で自動検出を上書きすると VRAM 経路を強制できますが、キャッシュ量を揃えて比べると zero-copy のほうが 6.8% 速いという結果でした。同じ物理メモリの中に複製を作るだけなので、コピーの手間が増えるぶん損をします。作者の自動検出は正しい判断をしていました。
まとめ
DGX Spark 1 台で、2.8 兆パラメータの Kimi K3 が動きました。634.6GB、統合メモリの 4.9 倍です。従来この機体の上限は 2 ノードで 170GB でしたから、3.7 倍まで射程が伸びたことになります。
やったことを並べると、ディスクの速度を測って、ソースコードを 1 行直して、634GB を落として、機械を 1 回沈黙させて、最後に 0.70 tok/s を得ました。夏休みの自由研究としては上出来ではないでしょうか。
手段の使い分けもはっきりしました。
| 手段 | decode | 使いどころ |
|---|---|---|
| llama.cpp 全載せ | 16.15〜16.63 tok/s | 108.1 GiB まで |
| Pulsar | 10.09 tok/s | 起動の速さが要るとき |
llama.cpp --n-cpu-moe |
9.86 tok/s 以下 | 統合メモリでは選ぶ理由がない |
| Pulsar(GLM-5.2 434GB) | 1.13 tok/s | 121 GiB 超 |
| Pulsar(Kimi K3 634.6GB) | 0.70 tok/s | 121 GiB の 4.9 倍 |
108.1 GiB までは llama.cpp が最速で、それを超えたら Pulsar の出番です。中間の逃げ道である --n-cpu-moe は、統合メモリでは成立しません。
0.70 tok/s という数字を実用と呼べるかは用途次第ですが、そもそも動かないはずのモデルが動いている状態です。バッチで長文を投げておいて後から読む使い方なら、十分に選択肢になるかなと思っています。GLM-5.2 の 211GB なら 2.12 tok/s 出るので、こちらは対話でも我慢できる範囲かもしれません。
個人的には、統合メモリは「121 GiB 全部を GPU が使える」利点と引き換えに、階層メモリの逃げ道を失った構成なのだなと納得しました。だからこそディスクに逃がす発想が効いてくるわけですね。
やり残しもあります。今回使った UD-IQ1_S は、2.78 兆パラメータを 634.6GB に押し込んでいるので 1 パラメータあたり 1.83 bit という圧縮率です。生成される文章はやや薄めでした。unsloth はもっと高ビットの版も出しているので、そちらなら実用度が変わるかもしれませんが、ディスクの都合で今回は見送っています。次は量子化を上げた K3 か、pulsar-quant で自分好みの混合量子化を作るあたりを試してみたいところです。
参考リンク
- giannisanni/pulsar — SSD ストリーミング推論エンジン
- pulsar PR #24 — 今回投げた K3 ローダーの修正
- antirez/GLM-5.2-GGUF — GLM-5.2 744B の 3 段階量子化
- unsloth/Kimi-K3-GGUF — Kimi K3 2.8T の GGUF
- unsloth/DeepSeek-V4-Flash-0731-GGUF — V4 Flash の量子化バリアント
- DeepSeek V4 Flash-0731 を llama.cpp で DGX Spark 1 台に載せてみた(2026-08-02 公開の前回記事)







