GLM-5.1 + Qwen3-Coder-Next 構成最適化: orchestrator TG 実測と最終レイアウト設計
GLM-5.1 (744B MoE, IQ3_KS) を familiar の orchestrator として運用するための実測記録。cpu-moe 全載せ、1GPU/2GPU、n-cpu-moe、-ot head+tail 分散配置を比較し、さらに Qwen3-Coder-Next と組み合わせた最終構成方針まで整理した。
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/token、18.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 layer | tail 14 層だけより head+tail 22 層の方が効き方が大きく、head 側が明らかに hot |
| ctx 耐性 | TG は 61t: 53ms から 4096t: 54-55ms 程度で、長出力でもほぼ崩れない |
-ger | CPU expert 処理を 1-2% 改善し、L3 locality 改善の方向に効いている |
-sm graph | GLM-DSA では非対応で layer にフォールバックする |
| thinking | orchestrator 用途では不要。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/token | TG (t/s) | 改善 |
|---|---|---|---|---|
| cpu-moe 全載せ (expert 0) | 0 | 53.0 | 18.8 | baseline |
| n-cpu-moe 64 | 14 | 50.2 | 19.9 | +5.7% |
-ot head12+tail10 | 22 | 46.0 | 21.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 | 用途 | モデル |
|---|---|---|
| grandpa | orchestrator のレビュー、判定、契約整合性確認 | 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: denselayer 3-77: MoElayer 78: nextn predictionlayer 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_upとgate_expsのマージ-mla 3: Multi-Latent Attention 最適化レベル 3-amb 512: attention max batch size-ctk q8_0 -ctv q8_0: KV cache の量子化
テスト方法
TG 比較に使ったリクエストは 2 本だけで、どちらも実運用を意識している。
- 短い JSON 生成
- 長い OpenAPI spec 生成
短い JSON はレビュー返答や contract 判定に近い。長い OpenAPI spec は長出力時の decode 安定性を見るための負荷として使った。thinking ありなしも比較し、timings.predicted_ms / predicted_n と llama.cpp の eval 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 ctx で 1.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

構成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 点ある。
graph splitsは190 -> 152に減っており、構造的には 1GPU の方が単純- それでも TG はほぼ据え置きで、CPU expert が律速である事実は変わらない
つまり 2GPU をやめても速くはならないが、少なくとも遅くもならない。worker 用に dev0 を空ける意味は十分にある。

構成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

改善率は +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

この構成で 18.84 -> 21.75 t/s、改善率は +15.4% になった。22 層のうち head が 12 層を占めていることを考えると、「末尾だけでなく先頭を載せる」ことが効いていると見るのが妥当だった。
Grafana モニタリング
ベンチマーク中の GPU utilization、memory copy、温度、電力は DCGM exporter 経由で見た。

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

ベンチマーク比較サマリ
TG (eval) 比較
| # | 構成 | GPU数 | expert GPU層 | 短 TG | 2048t TG | 4096t TG | 改善 |
|---|---|---|---|---|---|---|---|
| 1 | hybrid -ot exps=CPU | 2 | 0 | 18.84 | 18.35 | 18.10 | baseline |
| 2 | hybrid --cpu-moe | 2 | 0 | 18.84 | 18.35 | 18.10 | ±0% |
| 3 | hybrid --cpu-moe | 1 | 0 | 18.86 | 18.46 | 18.42 | +0.1% |
| 4 | hybrid --n-cpu-moe 64 -ger | 1 | 14 (tail) | 19.94 | 19.78 | 19.76 | +5.8% |
| 5 | hybrid -ot head+tail -ger | 2 | 22 (h12+t10) | 21.75 | 21.64 | 21.55 | +15.4% |
PP (prompt eval) 比較
| # | 構成 | 短 PP | 長 PP | best PP |
|---|---|---|---|---|
| 1 | hybrid -ot exps=CPU 2GPU | 22.66 | 23.48 | 23.48 |
| 3 | hybrid --cpu-moe 1GPU | 23.12 | 23.59 | 25.35 |
| 4 | hybrid --n-cpu-moe 64 -ger | 25.57 | 25.44 | 25.83 |
| 5 | hybrid -ot head+tail -ger | 26.98 | 21.32 | 30.03 |
GPU リソース比較
| # | 構成 | VRAM dev0 | VRAM dev1 | GPU util | CPU% | pinned RAM |
|---|---|---|---|---|---|---|
| 1 | hybrid exps=CPU 2GPU | 15236MiB | 14460MiB | 17-19% | 1500% | 300GB |
| 3 | hybrid cpu-moe 1GPU | — | 23592MiB | 17-19% | 1500% | 300GB |
| 4 | hybrid n-cpu-moe 64 | — | 80804MiB | 41% | 1432% | 244GB |
| 5 | hybrid -ot head+tail | 57190MiB | 48756MiB | 22-24% x 2 | 1355% | 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 戦略
| parallel | per-slot ctx | coding 実用性 |
|---|---|---|
| 1 | 256k | 余裕はあるが throughput が低い |
| 2 | 128k | 実用ライン |
| 3 | 85k | heavy task で溢れやすい |
| 4 | 64k | agentic coding にはかなり厳しい |
--parallel 2 が実用の下限だった。128k で大半のコーディングタスクは収まる。YaRN で 1M ctx まで伸ばす余地はあるが、まずは 256k / slot ではなく 128k x 2 の実用 throughput を優先したい。
Quant 選択: Qwen3-Coder-Next は Q4_0
| Quant | PPL | weights | grandpa expert 予算 |
|---|---|---|---|
IQ4_KSS | 8.31 | 39 GiB | 48GB/GPU |
Q4_0 | 8.25 | 45 GiB | 42GB/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 通り試す設計なら、手間に対して得られる学びが大きい。これはまだ仮説の段階だが、次に試す価値のある配置案として締めに残しておきたい。
