familiar の実運用では、orchestrator がワーカーの呼び出し計画、レビュー、収束判定を担う。ここで使う grandpa が遅いと、ワーカーがいくら速くてもターン全体のレイテンシが沈む。今回詰めたのは GLM-5.1 IQ3_KS を orchestrator に置いた時の TG と、Qwen3-Coder-Next を worker に並べた時に全体としてどこまで実用的な配置に持っていけるか、という 2 点だった。

先に結論だけ書くと、GLM-5.1--cpu-moe 全載せでも一部は GPU を使うが、支配項は CPU expert 評価で、2GPU にしてもほぼ速くならない。改善が効いたのは expert を GPU に逃がした時だけで、特に -ot で head 12 層 + tail 10 層を 2GPU に分散した構成では 53.09ms/token -> 45.98ms/token18.84 t/s -> 21.75 t/s まで伸びた。そこに Qwen3-Coder-Next Q4_0 --parallel 2 を 2 worker 分載せる前提で VRAM を割り当てると、最終的な常駐レイアウトもかなり明確になる。

結論

今回の測定と集計から、運用判断にそのまま使える結論は以下だった。

論点確定した判断
CPU ボトルネックexpert FFN の CPU 評価が 45-50ms/token 程度で全体の 85-90% を占める
2GPU の意味cpu-moe 全載せでは 2GPU は 1GPU と同等で、53.09 vs 53.03 ms/token と差が出ない
改善の本体n-cpu-moe 64+5.7%-ot head12+tail10+15.3%
Hot layertail 14 層だけより head+tail 22 層の方が効き方が大きく、head 側が明らかに hot
ctx 耐性TG は 61t: 53ms から 4096t: 54-55ms 程度で、長出力でもほぼ崩れない
-gerCPU expert 処理を 1-2% 改善し、L3 locality 改善の方向に効いている
-sm graphGLM-DSA では非対応で layer にフォールバックする
thinkingorchestrator 用途では不要。61 tokens の本回答に 294 tokens の思考を足すと 4.6s -> 19.1s に悪化する

この結果を踏まえると、orchestrator の単独最速を狙うなら 22 層 head+tail が最良だった。一方で実運用は worker と VRAM を奪い合うため、最終構成では Qwen3-Coder-Next の品質とスループットを優先しつつ、GLM-5.1 側は 20 層前後の GPU expert 配置に落ち着かせるのが現実的だった。

今回確定した事実

CPU が支配的

  • expert FFN の CPU 評価が 45-50ms/token で全体の大半を占める
  • cpu-moe 全載せ時の GPU utilization は 17-19% 程度
  • PCIe 転送は支配項ではない。activation はおおよそ 12KB/層 で、79 層合計でも転送時間は 19μs 級にしかならない
  • 2GPU layer-split は cpu-moe 全載せではほぼ意味がなく、1GPU と 2GPU で TG は等価だった

この時点で「GPU が遅い」のではなく、「GPU は CPU expert 評価待ちで空いている」と見るのが正しかった。

Expert 配置による TG 改善

構成GPU expert層ms/tokenTG (t/s)改善
cpu-moe 全載せ (expert 0)053.018.8baseline
n-cpu-moe 641450.219.9+5.7%
-ot head12+tail102246.021.8+15.3%

Head 層が Hot

  • tail 14 層だけを GPU に載せた時の改善は +5.7%
  • head 12 層 + tail 10 層の 22 層構成では +15.3%
  • GPU に載せた層数は 1.57x なのに改善幅は 2.5x 近く、head 側の activation 密度が高いと見るのが自然

高 ctx 耐性

  • 61t の短い出力でも 4096t の長い出力でも TG はほぼ一定
  • MLA により 32k ctx 時の KV cache は約 1.5GB に収まる
  • ボトルネックが CPU expert 側なので、ctx 増加の影響が二重に小さい

-ger の効果

  • 50.55 -> 50.16 ms/token と小さいが一貫した改善
  • expert routing のグループ化が EPYC 9175F の巨大 L3 cache にやや有利に働いている可能性が高い

-sm graph は GLM-DSA 非対応

  • 明示的に指定しても自動で layer にフォールバックする
  • このモデルで最適化対象にすべきは split mode ではなく expert の物理配置だった

Thinking はオーケストレーターに不要

  • 61 tokens の出力に対し 294 tokens の思考を挟むと、所要時間は 4.6s から 19.1s まで膨らんだ
  • --reasoning-budget 0 または chat_template_kwargs.enable_thinking=false を前提にした方がよい

背景

今回の役割分担はかなり明確だった。

Role用途モデル
grandpaorchestrator のレビュー、判定、契約整合性確認GLM-5.1 IQ3_KS
naughty-worker実コード生成、修正、ファイル出力Qwen3-Coder-Next

worker 側では「少しでもコード品質が高い量子化」を優先したい。一方 grandpa 側では「十分な品質を保ちつつ TG をできるだけ削る」が優先になる。このトレードオフを詰めるために、まず GLM-5.1 の expert 配置を実測した。

ハードウェア構成

  GPU:  NVIDIA RTX PRO 6000 Blackwell Max-Q 96GB × 2
CPU:  AMD EPYC 9175F (16コア)
RAM:  768GB DDR5 6400MT/s
  

今回の話では、2GPU の価値は主に HBM 帯域ではなく容量にある。cpu-moe 全載せ時は GPU が空いていても、head+tail のように GPU 上へ expert を戻し始めると 1GPU では乗らない構成がすぐ出てくる。

GLM-5.1 モデル概要

起動ログから読み取れた GLM-5.1 の基本仕様は次のとおり。

  llm_load_print_meta: arch                 = glm-dsa
llm_load_print_meta: model type           = 744B.A40B
llm_load_print_meta: model ftype          = IQ3_KS - 3.1875 bpw
llm_load_print_meta: model params         = 753.864 B
llm_load_print_meta: model size           = 320.216 GiB (3.649 BPW)
llm_load_print_meta: n_layer              = 79
llm_load_print_meta: n_expert             = 256
llm_load_print_meta: n_expert_used        = 8
llm_load_print_meta: n_layer_dense_lead   = 3
  

内部構成をざっくり整理するとこうなる。

  • layer 0-2: dense
  • layer 3-77: MoE
  • layer 78: nextn prediction
  • layer 79: output

MoE 層は 75 層あり、各層の expert 重みは次のサイズ感だった。

  gate_exps = 1225 MiB
down_exps = 1638 MiB
up_exps   = 1225 MiB
  

つまり 1 層あたりの expert は約 4.1GB。全 expert を合計すると約 307GB になり、これを CUDA_Host の pinned memory に置くと RAM 使用量が一気に跳ね上がる。逆に GPU 側の non-expert は約 14.5GB、KV cache は 1.5GB、compute buffer は 5.4GB 程度なので、expert を一切載せない構成だと GPU はかなり暇になる。

ベースコマンド

ik_llama.cpp で GLM-5.1 を立てるベースコマンドは次の形だった。

  podman run --rm \
  --device nvidia.com/gpu=1 \
  -p 8000:8000 \
  --cap-add=SYS_NICE \
  -v /mnt/data/models/models--ubergarm--GLM-5.1-GGUF:/models:ro,Z \
  registry.home.arpa/ik_llama.cpp:latest \
  -m /models/snapshots/.../IQ3_KS/GLM-5.1-IQ3_KS-00001-of-00008.gguf \
  --merge-qkv --ctx-size 32768 -ctk q8_0 -ctv q8_0 \
  --parallel 1 --threads 15 --threads-batch 24 \
  -b 8192 -ub 8192 -ngl 999 \
  --cpu-moe -muge -mla 3 -amb 512 \
  --jinja --host 0.0.0.0 --port 8000 \
  --warmup-batch --alias GLM-5.1
  

主要オプションの意味:

  • --cpu-moe: 全 MoE expert 重みを CPU 側の pinned memory に配置
  • -muge: ffn_upgate_exps のマージ
  • -mla 3: Multi-Latent Attention 最適化レベル 3
  • -amb 512: attention max batch size
  • -ctk q8_0 -ctv q8_0: KV cache の量子化

テスト方法

TG 比較に使ったリクエストは 2 本だけで、どちらも実運用を意識している。

  1. 短い JSON 生成
  2. 長い OpenAPI spec 生成

短い JSON はレビュー返答や contract 判定に近い。長い OpenAPI spec は長出力時の decode 安定性を見るための負荷として使った。thinking ありなしも比較し、timings.predicted_ms / predicted_nllama.cppeval time を併読している。

  # 短い出力テスト
curl -s -w "\n\nTotal time: %{time_total}s\n" \
 http://compute.home.arpa:8000/v1/chat/completions \
 -H "Content-Type: application/json" \
 -d '{
"model": "GLM-5.1",
"messages": [
{"role": "user", "content": "Write a JSON object with 5 fields describing a software project. Include name, language, version, description, and license."}
],
"max_tokens": 256,
"temperature": 0.7,
"top_p": 0.95,
"top_k": 45,
"min_p": 0.01,
"chat_template_kwargs": {"enable_thinking": false}
}'
  
  # 長い出力テスト
curl -s -w "\n\nTotal time: %{time_total}s\n" \
 http://compute.home.arpa:8000/v1/chat/completions \
 -H "Content-Type: application/json" \
 -d '{
"model": "GLM-5.1",
"messages": [
{"role": "user", "content": "Write a detailed OpenAPI 3.0 specification in JSON for a task management API. Include endpoints for CRUD operations on projects and tasks, with request/response schemas, error responses, and authentication via Bearer token."}
],
"max_tokens": 2048,
"temperature": 0.7,
"top_p": 0.95,
"top_k": 45,
"min_p": 0.01,
"chat_template_kwargs": {"enable_thinking": false}
}'
  

CPU バウンドをどう見切ったか

今回一番大きかったのは、「CPU 側の expert 評価が全体を支配している」とかなり早い段階で言い切れたことだった。

expert 計算がほぼ全て

  • baseline の TG は 53ms/token
  • そのうち 45-50ms/token が CPU 上の expert 評価コスト
  • GPU 側の non-expert、KV、compute はその残差しかない

GPU utilization が 17-19% に張り付く一方で CPU は 1500% まで上がるので、観測値としても整合している。

PCIe は犯人ではない

最初は CPU/GPU の間を往復する activation 転送が怪しく見えるが、計算すると支配項ではなかった。

  • activation は約 12KB/層
  • 79 層を跨いでも転送量はごく小さい
  • 転送時間の見積もりは 19μs

つまり TG 53ms のうち PCIe が占める割合は無視できる。遅いのは「運ぶこと」ではなく「CPU で expert を評価すること」だった。

高 ctx 耐性の理由も同じ

61 token でも 2048 token でも 4096 token でも TG がほぼ一定だったのは、ctx の伸びで attention が支配的になっていないからだ。MLA により KV cache は 32k ctx1.5GB 程度に圧縮され、しかも CPU expert が先にボトルネックになっている。ここが dense モデルや純 GPU モデルとかなり違う。

構成1: hybrid -ot exps=CPU, 2GPU layer-split

最初の計測は -ot exps=CPU で全 expert を CPU に逃がした構成だった。

  podman run --rm \
  --device nvidia.com/gpu=all \
  -p 8000:8000 \
  --cap-add=SYS_NICE \
  -v /mnt/data/models/models--ubergarm--GLM-5.1-GGUF:/models:ro,Z \
  registry.home.arpa/ik_llama.cpp:latest \
  -m /models/snapshots/a9962c23e50d9c352e09fe0d9cb131026f4e6441/IQ3_KS/GLM-5.1-IQ3_KS-00001-of-00008.gguf \
  --merge-qkv --ctx-size 32768 -ctk q8_0 -ctv q8_0 --parallel 1 --threads 15 --threads-batch 24 -b 8192 -ub 8192 -ngl 99 -ot exps=CPU -muge -mla 3 -amb 512 -sm graph --jinja --host 0.0.0.0 --port 8000 --warmup-batch --alias GLM-5.1
  

起動時に見えた設定はこうだった。

  Split mode 'graph' is not supported for this model
  => changing split mode to 'layer'

llm_load_tensors:  CUDA_Host buffer size = 307110.47 MiB
llm_load_tensors:      CUDA0 buffer size =  7378.05 MiB
llm_load_tensors:      CUDA1 buffer size =  7120.90 MiB

llama_init_from_model: grouped er    = 0
llama_init_from_model: graph splits = 190
  

TG はベースラインとして次の値になった。

  # 短い出力 thinking無し
prompt eval time =    1323.95 ms /    30 tokens (   44.13 ms per token,    22.66 tokens per second)
       eval time =    3238.59 ms /    61 tokens (   53.09 ms per token,    18.84 tokens per second)

# 長い出力 thinking無し
prompt eval time =    1831.66 ms /    43 tokens (   42.60 ms per token,    23.48 tokens per second)
       eval time =  111609.04 ms /  2048 tokens (   54.50 ms per token,    18.35 tokens per second)

# 長い出力 thinking有り
prompt eval time =    2459.53 ms /    43 tokens (   57.20 ms per token,    17.48 tokens per second)
       eval time =  226282.06 ms /  4096 tokens (   55.24 ms per token,    18.10 tokens per second)
  

nvtop はかなり分かりやすかった。

  PID USER DEV     TYPE  GPU        GPU MEM    CPU  HOST MEM
3069 ksh3   0  Compute  19%  15236MiB  16%  1500% 309454MiB
3069 ksh3   1  Compute  17%  14460MiB  15%  1075% 309454MiB
  
2GPU の exps=CPU ベースラインで GPU 利用率が 20% 未満に張り付いている nvtop
expert を全て CPU に置くと GPU はほぼ待ち状態になり、ホスト側 pinned memory が 300GB 級まで膨らむ

構成2: hybrid --cpu-moe, 2GPU

次に -ot exps=CPU ではなく --cpu-moe を使った。実測上この 2 つは同じ挙動になることを確認したかった。

  podman run --rm \
  --device nvidia.com/gpu=all \
  -p 8000:8000 \
  --cap-add=SYS_NICE \
  -v /mnt/data/models/models--ubergarm--GLM-5.1-GGUF:/models:ro,Z \
  registry.home.arpa/ik_llama.cpp:latest \
  -m /models/snapshots/a9962c23e50d9c352e09fe0d9cb131026f4e6441/IQ3_KS/GLM-5.1-IQ3_KS-00001-of-00008.gguf \
  --merge-qkv --ctx-size 32768 -ctk q8_0 -ctv q8_0 --parallel 1 --threads 15 --threads-batch 24 -b 8192 -ub 8192 -ngl 99 --cpu-moe -muge -mla 3 -amb 512 -sm graph --jinja --host 0.0.0.0 --port 8000 --warmup-batch --alias GLM-5.1 --temp 0.7 --top-k 45 --top-p 0.95 --min-p 0.01
  

確認値は完全に同じだった。

  Split mode 'graph' is not supported for this model
  => changing split mode to 'layer'

llm_load_tensors:  CUDA_Host buffer size = 307110.47 MiB
llm_load_tensors:      CUDA0 buffer size =  7378.05 MiB
llm_load_tensors:      CUDA1 buffer size =  7120.90 MiB

llama_init_from_model: graph splits = 190
  

PP/TG も構成1と一致した。

  # 短い出力 thinking無し
prompt eval time =    1323.95 ms /    30 tokens (   44.13 ms per token,    22.66 tokens per second)
       eval time =    3238.59 ms /    61 tokens (   53.09 ms per token,    18.84 tokens per second)

# 長い出力 thinking無し
prompt eval time =    1831.66 ms /    43 tokens (   42.60 ms per token,    23.48 tokens per second)
       eval time =  111609.04 ms /  2048 tokens (   54.50 ms per token,    18.35 tokens per second)
  

つまり -ot exps=CPU--cpu-moe は、少なくともこのモデルとこの build では同一バッファ配置、同一性能だった。記事中では以後まとめて「cpu-moe 全載せ」と呼んでいる。

構成3: hybrid --cpu-moe, 1GPU

2GPU layer-split の転送オーバーヘッドが少しでもあるなら、1GPU の方が速いはずだと考えて dev1 単独構成も測った。

  podman run --rm \
  --device nvidia.com/gpu=1 \
  -p 8000:8000 \
  --cap-add=SYS_NICE \
  -v /mnt/data/models/models--ubergarm--GLM-5.1-GGUF:/models:ro,Z \
  registry.home.arpa/ik_llama.cpp:latest \
  -m /models/snapshots/a9962c23e50d9c352e09fe0d9cb131026f4e6441/IQ3_KS/GLM-5.1-IQ3_KS-00001-of-00008.gguf \
  --merge-qkv --ctx-size 32768 -ctk q8_0 -ctv q8_0 --parallel 1 --threads 15 --threads-batch 24 -b 8192 -ub 8192 -ngl 999 --cpu-moe -muge -mla 3 -amb 512 --jinja --host 0.0.0.0 --port 8000 --warmup-batch --alias GLM-5.1 --temp 0.7 --top-k 45 --top-p 0.95 --min-p 0.01
  

1GPU 時の起動値。

  llm_load_tensors:  CUDA_Host buffer size = 307110.47 MiB
llm_load_tensors:      CUDA0 buffer size = 14498.95 MiB

llama_kv_cache_init:      CUDA0 KV buffer size = 1491.79 MiB
llama_init_from_model:    CUDA0 compute buffer size = 5418.03 MiB
llama_init_from_model: graph splits = 152

Allocating 299.91 GiB of pinned host memory
done allocating 299.91 GiB in 46485.7 ms
  

性能差はかなり小さかった。

  # 短い出力 thinking無し
prompt eval time =    1297.67 ms /    30 tokens (   43.26 ms per token,    23.12 tokens per second)
       eval time =    3340.94 ms /    63 tokens (   53.03 ms per token,    18.86 tokens per second)

# 長い出力 thinking無し
prompt eval time =    1823.10 ms /    43 tokens (   42.40 ms per token,    23.59 tokens per second)
       eval time =  110967.85 ms /  2048 tokens (   54.18 ms per token,    18.46 tokens per second)

# 長い出力 thinking有り
prompt eval time =    1696.37 ms /    43 tokens (   39.45 ms per token,    25.35 tokens per second)
       eval time =  222410.35 ms /  4096 tokens (   54.30 ms per token,    18.42 tokens per second)
  
  PID USER DEV     TYPE  GPU        GPU MEM    CPU  HOST MEM
3381 ksh3   1  Compute   0%  23592MiB  24%     0% 309036MiB
  

ここから読めるのは 2 点ある。

  1. graph splits190 -> 152 に減っており、構造的には 1GPU の方が単純
  2. それでも TG はほぼ据え置きで、CPU expert が律速である事実は変わらない

つまり 2GPU をやめても速くはならないが、少なくとも遅くもならない。worker 用に dev0 を空ける意味は十分にある。

1GPU の cpu-moe 構成で VRAM 使用量は 23GB 台だが TG 改善はほぼない nvtop
1GPU 化で graph splits は減るが、ボトルネックは変わらず TG は据え置きだった

構成4: hybrid --n-cpu-moe 64, 1GPU, -ger

ここからは expert を GPU に戻し始めた。--n-cpu-moe 64 は前半の expert を CPU、後半だけ GPU に置く。

  podman run --rm \
  --device nvidia.com/gpu=1 \
  -p 8000:8000 \
  --cap-add=SYS_NICE \
  -v /mnt/data/models/models--ubergarm--GLM-5.1-GGUF:/models:ro,Z \
  registry.home.arpa/ik_llama.cpp:latest \
  -m /models/snapshots/a9962c23e50d9c352e09fe0d9cb131026f4e6441/IQ3_KS/GLM-5.1-IQ3_KS-00001-of-00008.gguf \
  --merge-qkv --ctx-size 32768 -ctk q8_0 -ctv q8_0 --parallel 1 --threads 15 --threads-batch 24 -b 8192 -ub 8192 -ngl 999 --n-cpu-moe 64 -muge -mla 3 -amb 512 --jinja --host 0.0.0.0 --port 8000 --warmup-batch -ger --alias GLM-5.1 --temp 0.7 --top-k 45 --top-p 0.95 --min-p 0.01
  

この時の読み方はこうなる。

  • blk.3-63: CPU 側
  • blk.64-77: GPU 側
  • 実質的に tail 14 層だけ GPU に戻した構成
  Allocating 244.02 GiB of pinned host memory
  

TG は初めて明確に改善した。

  # 短い出力 thinking無し
prompt eval time =    1173.39 ms /    30 tokens (   39.11 ms per token,    25.57 tokens per second)
       eval time =    3109.75 ms /    62 tokens (   50.16 ms per token,    19.94 tokens per second)

# 長い出力 thinking無し
prompt eval time =    1690.07 ms /    43 tokens (   39.30 ms per token,    25.44 tokens per second)
       eval time =  103526.50 ms /  2048 tokens (   50.55 ms per token,    19.78 tokens per second)

# 長い出力 thinking有り
prompt eval time =    1664.97 ms /    43 tokens (   38.72 ms per token,    25.83 tokens per second)
       eval time =  207284.39 ms /  4096 tokens (   50.61 ms per token,    19.76 tokens per second)
  

nvtop では GPU utilization と VRAM 使用量の変化がかなりはっきり出る。

  PID USER DEV     TYPE  GPU        GPU MEM    CPU  HOST MEM
4153 ksh3   1  Compute  41%  80804MiB  83%  1432% 251746MiB
  
n-cpu-moe 64 と ger を使った 1GPU 構成で GPU utilization が 41% まで上がった nvtop
tail 14 層を GPU に戻すと VRAM 使用量は 80GB 級まで増え、TG は 19.9 t/s 台に乗った

改善率は +5.7%。大きくはないが、「GPU へ戻した expert は確実に効く」ことを示すには十分だった。

失敗: hybrid --n-cpu-moe 58, 1GPU -> OOM

さらに踏み込んで 20 層程度を 1GPU に乗せることも試したが、--n-cpu-moe 58 は明確に OOM だった。

  podman run --rm \
  --device nvidia.com/gpu=1 \
  -p 8000:8000 \
  --cap-add=SYS_NICE \
  -v /mnt/data/models/models--ubergarm--GLM-5.1-GGUF:/models:ro,Z \
  registry.home.arpa/ik_llama.cpp:latest \
  -m /models/snapshots/a9962c23e50d9c352e09fe0d9cb131026f4e6441/IQ3_KS/GLM-5.1-IQ3_KS-00001-of-00008.gguf \
  --merge-qkv --ctx-size 32768 -ctk q8_0 -ctv q8_0 --parallel 1 --threads 15 --threads-batch 24 -b 8192 -ub 8192 -ngl 999 --n-cpu-moe 58 -muge -mla 3 -amb 512 --jinja --host 0.0.0.0 --port 8000 --warmup-batch -ger --alias GLM-5.1 --temp 0.7 --top-k 45 --top-p 0.95 --min-p 0.01
  
  ggml_backend_cuda_buffer_type_alloc_buffer: allocating 5418.03 MiB on device 0: cudaMalloc failed: out of memory
ggml_gallocr_reserve_n: failed to allocate CUDA0 buffer of size 5681217536
llama_init_from_model: failed to allocate compute buffers
  

1GPU で 20 層近くを抱えるのは、expert だけで 82GB 級になり、そこへ non-expert、KV、compute が重なるので物理的に無理だった。

構成5: hybrid -ot head12+tail10, 2GPU, -ger

最終的に一番効いたのは -ot による明示配置だった。head と tail をそれぞれ別 GPU に載せる。

  OT_ARGS=""
for i in $(seq 3 14); do
  OT_ARGS="$OT_ARGS -ot blk.$i.ffn_gate_exps=CUDA0"
  OT_ARGS="$OT_ARGS -ot blk.$i.ffn_down_exps=CUDA0"
  OT_ARGS="$OT_ARGS -ot blk.$i.ffn_up_exps=CUDA0"
done
for i in $(seq 68 77); do
  OT_ARGS="$OT_ARGS -ot blk.$i.ffn_gate_exps=CUDA1"
  OT_ARGS="$OT_ARGS -ot blk.$i.ffn_down_exps=CUDA1"
  OT_ARGS="$OT_ARGS -ot blk.$i.ffn_up_exps=CUDA1"
done

podman run --rm \
  --device nvidia.com/gpu=all \
  -p 8000:8000 \
  --cap-add=SYS_NICE \
  -v /mnt/data/models/models--ubergarm--GLM-5.1-GGUF:/models:ro,Z \
  registry.home.arpa/ik_llama.cpp:latest \
  -m /models/snapshots/a9962c23e50d9c352e09fe0d9cb131026f4e6441/IQ3_KS/GLM-5.1-IQ3_KS-00001-of-00008.gguf \
  --merge-qkv --ctx-size 32768 -ctk q8_0 -ctv q8_0 \
  --parallel 1 --threads 15 --threads-batch 24 \
  -b 8192 -ub 8192 -ngl 999 \
  --cpu-moe $OT_ARGS -ger \
  -muge -mla 3 -amb 512 \
  --jinja --host 0.0.0.0 --port 8000 \
  --warmup-batch --alias GLM-5.1
  

起動時の観測値。

  Allocating 218045 MiB of pinned host memory

GPU0: 57190MiB (58%)
GPU1: 48756MiB (50%)
  

つまり配置はこうなる。

  • GPU0: head 12 層 + 前半 attention
  • GPU1: tail 10 層 + 後半 attention
  • CPU: 中間 53 層の expert

PP/TG は今回の最良値だった。

  # 短い出力 thinking無し
prompt eval time =    1111.85 ms /    30 tokens (   37.06 ms per token,    26.98 tokens per second)
       eval time =    2942.51 ms /    64 tokens (   45.98 ms per token,    21.75 tokens per second)

# 長い出力 thinking無し
prompt eval time =    2016.80 ms /    43 tokens (   46.90 ms per token,    21.32 tokens per second)
       eval time =   94650.69 ms /  2048 tokens (   46.22 ms per token,    21.64 tokens per second)

# 長い出力 thinking有り
prompt eval time =    1431.92 ms /    43 tokens (   33.30 ms per token,    30.03 tokens per second)
       eval time =  190089.65 ms /  4096 tokens (   46.41 ms per token,    21.55 tokens per second)
  

nvtop でも CPU 側の負荷低下が少し見える。

  PID USER DEV     TYPE  GPU        GPU MEM    CPU  HOST MEM
4483 ksh3   0  Compute  24%  64268MiB  66%  1355% 219523MiB
4483 ksh3   1  Compute  22%  55322MiB  57%  1194% 219523MiB
  
head 12 層と tail 10 層を 2GPU に分散配置した nvtop
head+tail 明示配置が今回の最良値。GPU が両方とも仕事を持ち、CPU 利用率も baseline より少し下がる

この構成で 18.84 -> 21.75 t/s、改善率は +15.4% になった。22 層のうち head が 12 層を占めていることを考えると、「末尾だけでなく先頭を載せる」ことが効いていると見るのが妥当だった。

Grafana モニタリング

ベンチマーク中の GPU utilization、memory copy、温度、電力は DCGM exporter 経由で見た。

Grafana の DCGM GPU Monitoring ダッシュボードで GPU utilization と memory copy utilization を見ている画面
GPU 配置を変えるたびに utilization パターンが変わる。head+tail では 2GPU の役割分担が最もはっきり出る

Node Exporter 側では CPU Busy User とメモリ消費の張り付き方を追った。

Grafana の Node Exporter ダッシュボードで CPU Busy User とホストメモリ使用量を見ている画面
pinned host memory が 200-300GB 級で確保されるため、RAM 観測は実運用でも必須になる

ベンチマーク比較サマリ

TG (eval) 比較

#構成GPU数expert GPU層短 TG2048t TG4096t TG改善
1hybrid -ot exps=CPU2018.8418.3518.10baseline
2hybrid --cpu-moe2018.8418.3518.10±0%
3hybrid --cpu-moe1018.8618.4618.42+0.1%
4hybrid --n-cpu-moe 64 -ger114 (tail)19.9419.7819.76+5.8%
5hybrid -ot head+tail -ger222 (h12+t10)21.7521.6421.55+15.4%

PP (prompt eval) 比較

#構成短 PP長 PPbest PP
1hybrid -ot exps=CPU 2GPU22.6623.4823.48
3hybrid --cpu-moe 1GPU23.1223.5925.35
4hybrid --n-cpu-moe 64 -ger25.5725.4425.83
5hybrid -ot head+tail -ger26.9821.3230.03

GPU リソース比較

#構成VRAM dev0VRAM dev1GPU utilCPU%pinned RAM
1hybrid exps=CPU 2GPU15236MiB14460MiB17-19%1500%300GB
3hybrid cpu-moe 1GPU23592MiB17-19%1500%300GB
4hybrid n-cpu-moe 6480804MiB41%1432%244GB
5hybrid -ot head+tail57190MiB48756MiB22-24% x 21355%218GB

改善率サマリ

  hybrid cpu-moe 全載せ (baseline):  18.84 tok/s
hybrid cpu-moe 1GPU:               18.86 tok/s  (+0.1%)
hybrid n-cpu-moe 64 -ger:          19.94 tok/s  (+5.8%)
hybrid -ot head12+tail10 -ger:     21.75 tok/s  (+15.4%)
  

考察

CPU バウンドの壁

expert を CPU に置く構成では、GPU の性能差や GPU 数より先に CPU 側の expert FFN が詰まる。今回の EPYC 9175F は 16 コアで L3 も大きいが、それでも 256 experts のうち 8 active を毎 token 処理するには遅い。TG の 85-90% をそこで食っているなら、GPU を何枚増やしても構図は変わらない。

head 層が hot だと見てよい

tail 14 層より head 12 + tail 10 の方が効いた。これは単に層数が増えたからでは説明しづらい。改善率の伸び方が大きすぎるので、head 側の expert activation が tail より濃いと見るのが一番自然だった。

2GPU の価値は容量

cpu-moe 全載せでは 2GPU の価値は薄い。だが expert を GPU に戻し始めると、一気に話が変わる。Qwen3-Coder-Next まで常駐させるならなおさらで、96GB x 2 の容量がないと orchestrator と worker を両立できない。

thinking は完全に割に合わない

thinking を入れると TG 自体はあまり変わらないが、出す token 数が一気に増える。orchestrator の仕事は「長考」ではなく「短い判定」と「次ターンの指示」なので、思考 token を増やしても収束にはあまり効かない。

-ger は小さいが無視しにくい

1-2% の差でも、review を何百回も回すレーンでは積み上がる。しかも CPU に残る expert はまだ大多数なので、L3 locality 改善で少しでも詰まるなら有効化しておく価値がある。

Qwen3-Coder-Next を含めた最終構成方針

ここからは GLM 単独ベンチマークではなく、実際に naughty-worker を 2 本常駐させる構成でどう切るかの話になる。

Qwen3-Coder-Next の KV 効率

Qwen3-Coder-Next の効率が高いのは KV cache の軽さが大きい。

  48層: 36層 DeltaNet(KV cache不要) + 12層 Gated Attention(KV heads=2)
256k ctx: KV ~3GB
1M ctx (YaRN): KV ~11GB
  

普通の attention-only モデルに比べると、長 ctx を持ったまま worker を並べやすい。

Naughty の --parallel 戦略

parallelper-slot ctxcoding 実用性
1256k余裕はあるが throughput が低い
2128k実用ライン
385kheavy task で溢れやすい
464kagentic coding にはかなり厳しい

--parallel 2 が実用の下限だった。128k で大半のコーディングタスクは収まる。YaRN で 1M ctx まで伸ばす余地はあるが、まずは 256k / slot ではなく 128k x 2 の実用 throughput を優先したい。

Quant 選択: Qwen3-Coder-Next は Q4_0

QuantPPLweightsgrandpa expert 予算
IQ4_KSS8.3139 GiB48GB/GPU
Q4_08.2545 GiB42GB/GPU

PPL 差は 0.06 しかないが、worker の全コード品質には効く。失うのは grandpa expert の 2-3 層分、だいたい 1000 tokens あたり 1 秒前後の差になる計算だが、このトレードオフなら worker 品質を取る方が正しいと判断した。

最終構成

  dev0 (96GB):
  naughty-worker0: Q4_0, --parallel 2, 128k x 2
    weights 45GB + KV ~5.5GB + compute ~3GB = ~54GB
  grandpa expert (head側): ~42GB -> 10層

dev1 (96GB):
  naughty-worker1: Q4_0, --parallel 2, 128k x 2
    weights 45GB + KV ~5.5GB + compute ~3GB = ~54GB
  grandpa expert (tail+mid側): ~42GB -> 10層

grandpa non-expert/KV/compute: ~12GB/GPU (layer-split)
grandpa expert 合計: ~84GB -> 20層
CPU (768GB RAM): 残り55層の expert (~225GB pinned)
  

この時点で狙う TG は ~47ms/token、つまり 21+ t/s ライン。22 層 full head+tail の 46ms には少し届かないが、worker を 2 本維持できるならこちらの方が全体 throughput は高い。

Expert 配置案(20層)

  GPU0 (head重点):
  blk.3,4,5,6,7,8,9,10  + blk.20,30 = 10層

GPU1 (tail重点):
  blk.70,71,72,73,74,75,76,77 + blk.50,60 = 10層
  

head 8 層、tail 8 層、mid 4 層を補助的に入れる。この構成で 22 層 best に近い改善が取れれば、worker と orchestrator のバランスとしてかなりきれいに落ちる。

オーケストレーター運用への影響

review を 1000 tokens 出すと仮定すると、構成差はそのまま 1 ターンの長さに直結する。

構成所要時間
cpu-moe baseline (18.8 t/s)53秒
n-cpu-moe 64 (19.9 t/s)50秒
-ot head+tail (21.6 t/s)46秒

7 秒差は単発だと小さく見えるが、2 ラウンド、3 ラウンドと積むとすぐ効いてくる。逆に head+tail 22 層のままでは worker の常駐数が減るので、Phase 0 では「grandpa の速さ」と「naughty の throughput」のどちらが収束率に効くかを実データで見る必要がある。

将来の最適化オプション

カスタム GGUF ビルド

hot layer の expert だけ量子化を引き上げる。

  # GPU層 (head 8 + tail 8)
blk\.(3|4|5|6|7|8|9|10)\.ffn_down_exps\.weight=iq6_k
blk\.(3|4|5|6|7|8|9|10)\.ffn_(gate|up)_exps\.weight=iq5_ks
blk\.(70|71|72|73|74|75|76|77)\.ffn_down_exps\.weight=iq6_k
blk\.(70|71|72|73|74|75|76|77)\.ffn_(gate|up)_exps\.weight=iq5_ks

# CPU層 (middle)
blk\..*\.ffn_down_exps\.weight=iq4_ks
blk\..*\.ffn_(gate|up)_exps\.weight=iq3_ks
  

GPU 側の hot layer 品質を上げて parse tier の strict 率を改善できる可能性がある。ただし VRAM 増加は 16 層で ~19GB 規模になるので、worker の ctx と交換になる。

Expert Activation Profiling

--metrics と verbose logging で layer ごとの activation 頻度を取る。

IQ4_K への Quant 変更(GLM側)

IQ3_KS -> smol-IQ4_K で instruction following 品質を上げる案もある。TG は -10-15% の可能性があるが、parse tier や convergence rate が改善すればトータルでは勝つかもしれない。

Raw Benchmark Appendix

ここから先は、記事本文で使った実測値の元になっている raw log をそのまま残しておく。あとから grep で引く前提で、実行コマンド、起動時ログ、PP/TG 抽出値をまとめている。

A. -ot exps=CPU 2GPU

  ggml_cuda_init: found 2 CUDA devices:
  Device 0: NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition, compute capability 12.0, VMM: yes, VRAM: 97247 MiB
  Device 1: NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition, compute capability 12.0, VMM: yes, VRAM: 97247 MiB

Split mode 'graph' is not supported for this model
  => changing split mode to 'layer'

llm_load_print_meta: model type       = 744B.A40B
llm_load_print_meta: model ftype      = IQ3_KS - 3.1875 bpw
llm_load_print_meta: model params     = 753.864 B
llm_load_print_meta: model size       = 320.216 GiB (3.649 BPW)

llm_load_tensors:  CUDA_Host buffer size = 307110.47 MiB
llm_load_tensors:      CUDA0 buffer size =  7378.05 MiB
llm_load_tensors:      CUDA1 buffer size =  7120.90 MiB

llama_init_from_model: n_ctx         = 32768
llama_init_from_model: n_batch       = 8192
llama_init_from_model: n_ubatch      = 8192
llama_init_from_model: flash_attn    = 1
llama_init_from_model: mla_attn      = 3
llama_init_from_model: attn_max_b    = 512
llama_init_from_model: fused_moe     = 1
llama_init_from_model: grouped er    = 0
llama_init_from_model: fused_up_gate = 1
llama_init_from_model: fused_mmad    = 1
llama_init_from_model: graph_reuse   = 1

llama_kv_cache_init:      CUDA0 KV buffer size =   784.15 MiB
llama_kv_cache_init:      CUDA1 KV buffer size =   707.64 MiB
llama_init_from_model: KV self size  = 1491.75 MiB

llama_init_from_model:      CUDA0 compute buffer size =  5418.03 MiB
llama_init_from_model:      CUDA1 compute buffer size =  5032.00 MiB
llama_init_from_model:  CUDA_Host compute buffer size =   704.09 MiB
llama_init_from_model: graph nodes  = 10250
llama_init_from_model: graph splits = 190

Allocating 299.91 GiB of pinned host memory
done allocating 299.91 GiB in 51786.7 ms
  
  # 短い出力 thinking無し (61 tokens)
prompt eval time =    1323.95 ms /    30 tokens (   44.13 ms per token,    22.66 tokens per second)
       eval time =    3238.59 ms /    61 tokens (   53.09 ms per token,    18.84 tokens per second)
      total time =    4562.54 ms /    91 tokens

# 短い出力 thinking有り (355 tokens = 294 thinking + 61 content)
prompt eval time =      55.51 ms /     1 tokens (   55.51 ms per token,    18.02 tokens per second)
       eval time =   19024.07 ms /   355 tokens (   53.59 ms per token,    18.66 tokens per second)
      total time =   19079.58 ms /   356 tokens

# 長い出力 thinking無し (2048 tokens)
prompt eval time =    1831.66 ms /    43 tokens (   42.60 ms per token,    23.48 tokens per second)
       eval time =  111609.04 ms /  2048 tokens (   54.50 ms per token,    18.35 tokens per second)
      total time =  113440.71 ms /  2091 tokens

# 長い出力 thinking有り (4096 tokens)
prompt eval time =    2459.53 ms /    43 tokens (   57.20 ms per token,    17.48 tokens per second)
       eval time =  226282.06 ms /  4096 tokens (   55.24 ms per token,    18.10 tokens per second)
      total time =  228741.58 ms /  4139 tokens
  

B. --cpu-moe 1GPU

  ggml_cuda_init: found 1 CUDA devices:
  Device 0: NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition, compute capability 12.0, VMM: yes, VRAM: 97247 MiB

llm_load_tensors:  CUDA_Host buffer size = 307110.47 MiB
llm_load_tensors:      CUDA0 buffer size = 14498.95 MiB

llama_kv_cache_init:      CUDA0 KV buffer size =  1491.79 MiB
llama_init_from_model:      CUDA0 compute buffer size =  5418.03 MiB
llama_init_from_model:  CUDA_Host compute buffer size =   704.09 MiB
llama_init_from_model: graph nodes  = 10250
llama_init_from_model: graph splits = 152

Allocating 299.91 GiB of pinned host memory
done allocating 299.91 GiB in 46485.7 ms
  
  # 短い出力 thinking無し (63 tokens)
prompt eval time =    1297.67 ms /    30 tokens (   43.26 ms per token,    23.12 tokens per second)
       eval time =    3340.94 ms /    63 tokens (   53.03 ms per token,    18.86 tokens per second)
      total time =    4638.61 ms /    93 tokens

# 長い出力 thinking無し (2048 tokens)
prompt eval time =    1823.10 ms /    43 tokens (   42.40 ms per token,    23.59 tokens per second)
       eval time =  110967.85 ms /  2048 tokens (   54.18 ms per token,    18.46 tokens per second)
      total time =  112790.94 ms /  2091 tokens

# 長い出力 thinking有り (4096 tokens)
prompt eval time =    1696.37 ms /    43 tokens (   39.45 ms per token,    25.35 tokens per second)
       eval time =  222410.35 ms /  4096 tokens (   54.30 ms per token,    18.42 tokens per second)
      total time =  224106.72 ms /  4139 tokens
  

C. --n-cpu-moe 64 -ger 1GPU

  ggml_cuda_init: found 1 CUDA devices:

# blk.3〜blk.63: CUDA_Host (55 MoE層 CPU)
# blk.64〜blk.77: GPU (14 MoE層)

Allocating 244.02 GiB of pinned host memory
  
  # 短い出力 thinking無し (62 tokens)
prompt eval time =    1173.39 ms /    30 tokens (   39.11 ms per token,    25.57 tokens per second)
       eval time =    3109.75 ms /    62 tokens (   50.16 ms per token,    19.94 tokens per second)
      total time =    4283.14 ms /    92 tokens

# 長い出力 thinking無し (2048 tokens)
prompt eval time =    1690.07 ms /    43 tokens (   39.30 ms per token,    25.44 tokens per second)
       eval time =  103526.50 ms /  2048 tokens (   50.55 ms per token,    19.78 tokens per second)
      total time =  105216.57 ms /  2091 tokens

# 長い出力 thinking有り (4096 tokens)
prompt eval time =    1664.97 ms /    43 tokens (   38.72 ms per token,    25.83 tokens per second)
       eval time =  207284.39 ms /  4096 tokens (   50.61 ms per token,    19.76 tokens per second)
      total time =  208949.37 ms /  4139 tokens
  

D. -ot head12+tail10 -ger 2GPU

  ggml_cuda_init: found 2 CUDA devices:

Allocating 218045 MiB of pinned host memory (HOST MEM)

# GPU VRAM:
GPU0: 57190MiB (58%)
GPU1: 48756MiB (50%)
  
  # 短い出力 thinking無し (64 tokens)
prompt eval time =    1111.85 ms /    30 tokens (   37.06 ms per token,    26.98 tokens per second)
       eval time =    2942.51 ms /    64 tokens (   45.98 ms per token,    21.75 tokens per second)
      total time =    4054.36 ms /    94 tokens

# 長い出力 thinking無し (2048 tokens)
prompt eval time =    2016.80 ms /    43 tokens (   46.90 ms per token,    21.32 tokens per second)
       eval time =   94650.69 ms /  2048 tokens (   46.22 ms per token,    21.64 tokens per second)
      total time =   96667.49 ms /  2091 tokens

# 長い出力 thinking有り (4096 tokens)
prompt eval time =    1431.92 ms /    43 tokens (   33.30 ms per token,    30.03 tokens per second)
       eval time =  190089.65 ms /  4096 tokens (   46.41 ms per token,    21.55 tokens per second)
      total time =  191521.57 ms /  4139 tokens
  

まとめ

今回の一連の実測で分かったのは、GLM-5.1 を orchestrator に置く時の速さは「2GPU か 1GPU か」ではなく「どの expert を GPU に戻すか」で決まるということだった。cpu-moe 全載せでも GPU 自体は稼働するが、支配項は CPU 側で、worker と同居させる以上それだけでは遅い。逆に head+tail の hot layer を戻すと TG はかなり改善する。

実運用で重要なのは、そこで得た 21+ t/s を単体ベンチの勝利で終わらせず、Qwen3-Coder-Next Q4_0 --parallel 2 を 2 本常駐させた全体構成に落とし込むことだ。Phase 0 では parse tier、converged<=2round、force_accepted rate まで含めて観測し、最終的に「grandpa をもう少し速くするべきか」「worker 品質をさらに優先すべきか」を決めることになる。

加えて、GLM-5.1 を単体で使う前提なら、今回の結果からは「head と tail に全 GPU expert 予算の 3 割程度を先に割り当て、残りを中間層へ加重平均的に散らす」設計もかなり筋がよさそうに見える。head+tail が明確に hot なのは今回の実測で見えている一方、完全に両端へ寄せ切ると中間層の取りこぼしが残る可能性がある。hot edge を優先しつつ middle にも薄く配る方が、単体運用では収束の良いバランスになるかもしれない。

この仮説を雑に試すだけでも価値はありそうで、中間層については odd 側を多めに拾う案と even 側を多めに拾う案の 2 パターンを回し、用途ごとに良い偏りへ切り替えるだけでも平均的な簡易調整としてはかなり現実的だと思う。精密な activation profiling を待たなくても、まずは edge を固定しつつ middle の偏りだけを 2 通り試す設計なら、手間に対して得られる学びが大きい。これはまだ仮説の段階だが、次に試す価値のある配置案として締めに残しておきたい。