Skip to content

fit: size llama.cpp's layer split card by card - #217

Closed
88plug wants to merge 2 commits into
mainfrom
claude/fit-llamacpp-per-card-split
Closed

88plug wants to merge 2 commits into
mainfrom
claude/fit-llamacpp-per-card-split

Conversation

@88plug

@88plug 88plug commented Oct 29, 2026

Copy link
Copy Markdown
Owner

Problem

On several GPUs the fit sized llama.cpp's cards as one pool: the summed budgets against the summed weights, KV and overhead. It then wrote -ts as each card's budget in GiB.

llama.cpp places whole layers. With --n-cpu-moe, the layers that keep their experts on the GPU are the last ones. So the Xeon fixture's Flash-Next IQ4_NL plan, -ts 30,30 --n-cpu-moe 28 at 262144 x 4, put 43.5 GiB on the second 32 GiB card.

How llama.cpp b11749 assigns layers

src/llama-model.cpp load_tensors, with -ngl 999:

  • layer il goes to device upper_bound(cumsum(ts)/sum, il/(n_layer+1));
  • the output layer is il = n_layer, which is the last card;
  • --n-cpu-moe N adds CPU buffer overrides for blk.<i>.ffn_*_exps with i < N (common/common.h llm_add_n_cpu_ffn_overrides).

Changes

console/fit.py:

  • llama_layer_devices(ts, n_layer) follows the rule above exactly.
  • llama_layer_bytes gives each layer's GPU weights and KV:
    • weights are the class totals spread over the layers, minus the experts of the first --n-cpu-moe layers;
    • KV comes from config.json layer_types, and each layer's KV or recurrent state sits on its card;
    • the output layer holds lm_head (and the MTP and vision tensors, when the GGUF has them).
  • llama_split picks integer layer counts summing to n_layer + 1, so every card gets exactly the layers it was sized for. It keeps every card's layers plus its own overhead within the smallest budget of the cards used, because the device plugin orders the cards inside the pod. It balances the largest card's load, and returns nothing when no split fits.
  • The layer-split placement and the experts-in-DRAM placement share one search (_llama_multi): the most context first, then the fewest CPU-expert layers, then the most sequences. On one card that search gives the same arithmetic as before.
  • --explain lists each card's layers, weights, KV and total. --json gains card_bytes, and gpu_bytes is the largest card.

docs/fit.md: the per-card rule and -ts as layer counts.

Result on the Xeon fixture: Flash-Next IQ4_NL becomes -ts 35,14 --n-cpu-moe 29 at 262144 x 4, with 29.9 and 28.9 GiB on cards whose budgets are 30.4 and 30.2 GiB.

Tests

tests/fit_test.py LlamaSplit:

  • llama_layer_devices matches upper_bound, including a 0 entry;
  • the old -ts 30,30 --n-cpu-moe 28 plan, worked out independently in the test, puts more than 32 GiB on its second card;
  • for the Flash-Next IQ4_NL plan and a two-card Gemma QAT layer split (262144 x 6), every card's bytes are recomputed in the test from the emitted flags, stay within the budget, and agree with card_bytes;
  • a split that cannot fit is refused.

tests/fit_test.sh: the Flash-Next IQ4_NL --json plan has integer -ts summing to 49, is not 30,30, and every card_bytes entry is within budget.

The new tests fail on origin/main's fit.py.

An extra sweep, not committed, covered every fixture model and host x --gpus None/1/2/3 x --device None/gpu x three ctx/seqs settings: 1152 plans, no exceptions, and 92 llama.cpp split plans each checked per card against budget, with no violation.

Run before opening:

  • tests/fit_test.sh: 35 passed
  • python3 tests/fit_test.py: 61 tests OK
  • tests/model_opt_test.sh: 37 passed
  • tests/claims_check.sh, python3 tests/ledger_drift_check.py, ruff (F, E9), shellcheck v0.11.0 at warning severity: clean

Not changed, found while here

A GGUF with tied embeddings has no output.weight. Gemma 4 is one: in src/models/gemma4.cpp, output falls back to token_embd with TENSOR_DUPLICATED. llama.cpp then keeps a copy of token_embd on the output layer's device, 0.56 GiB for the Gemma QAT fixture. No placement counts that copy today, single-card included. It is left for a follow-up because it changes single-GPU plans as well.

Overlap

#214 and #216 also touch per_seq and the llama.cpp flags in console/fit.py. Whichever lands later needs a rebase. Here per_seq goes through kv_parts, so #214's swa_tokens belongs inside kv_parts.

On several GPUs the fit treated llama.cpp's cards as one pool: the summed budgets less the summed weights, KV and overhead. It then wrote -ts as each card's budget in GiB. llama.cpp places whole layers, though, and with --n-cpu-moe the layers whose experts stay on the GPU are the last ones. The Xeon fixture's Flash-Next IQ4_NL plan, -ts 30,30 --n-cpu-moe 28 at 262144 x 4, therefore put 43.5 GiB on the second 32 GiB card.

The fit now sizes each card from the layers llama.cpp b11749 gives it with -ngl 999 (src/llama-model.cpp load_tensors):
- layer il goes to the device upper_bound(cumsum(ts) / sum, il / (layers + 1));
- the output layer, holding lm_head, goes the same way and lands on the last card;
- the first --n-cpu-moe layers keep their experts in host RAM;
- each layer's KV or recurrent state sits on its card;
- every card carries its own overhead.

-ts is now integer layer counts summing to layers + 1, so each card gets exactly the layers it was sized for. Every card is held to the smallest budget of the cards used, because the device plugin orders them inside the pod. The largest card load is balanced, and a split that cannot fit is refused. The experts-in-DRAM placement uses the same per-card search, so -ts and --n-cpu-moe are chosen together. --explain lists each card, and --json has card_bytes.

Flash-Next IQ4_NL on the Xeon is now -ts 35,14 --n-cpu-moe 29 at 262144 x 4: 29.9 and 28.9 GiB on cards with 30.4 and 30.2 GiB budgets.

Tests:
- llama_layer_devices matches the upper_bound rule.
- The old -ts 30,30 --n-cpu-moe 28 plan works out to 43.5 GiB on its second card.
- For the Flash-Next IQ4_NL plan and a two-card Gemma QAT layer split, each card's bytes, recomputed in the test from the flags, stay within budget and agree with card_bytes.
- A split that cannot fit is refused.
- fit_test.sh checks the Flash-Next plan's -ts and card_bytes.
…picks its card count

The per-layer weights counted the MTP head for every format, so a model sized from
safetensors for a GGUF conversion carried 4.9 GiB (Qwen3.8-Flash-Next) on its output
layer that model_profile leaves out of the bytes to serve. One-card moe-offload plans
therefore no longer matched the old arithmetic: Intel/Qwen3.8-Flash-Next-W4A16-AutoRound
with --gpus 1 on the fixture Xeon went from --n-cpu-moe 43 to 47. The output layer now
holds the bytes to serve less the lookup tables, experts, other and vision: lm_head, plus
MTP only when the GGUF holds it. The one-card plan is --n-cpu-moe 43 again.

The vision projector was put on the output layer, the last card. llama.cpp loads the
projector through tools/mtmd/clip.cpp, which takes the first GPU backend, so it now
counts with layer 0 on the first card, and --explain says so.

moe-offload always used every free card. Each card is held to the smallest budget of the
cards used, so a smaller card beside two large ones lowers the room on all of them. On
the fixture Xeon with 0, 0, 12 and 14 GiB in use on its cards (budgets 30.4, 30.4, 18.4
and 16.4 GiB), Intel/Qwen3.8-Flash-Next-W4A16-AutoRound at 262144 x 6 with --device gpu was refused
("do not fit 4 card(s) of 16.4 GiB each") while --gpus 2 fit. The fit now tries 1, 2,
... of the free cards by budget (or the --gpus count) and keeps the plan with the most
context, then the fewest CPU-expert layers, then the most sequences, then the fewest
cards; that case is now two cards, -ts 31,18 --n-cpu-moe 41. --gpus above the free card
count is refused, as the GPU placements already refuse it, instead of running on fewer
cards.

The fixture Xeon's GGUF plan is unchanged: -ts 35,14 --n-cpu-moe 29 at 262144 x 4.

Tests (each fails on the previous commit):
- llama_layer_bytes adds up to the bytes to serve less the lookup tables, its output
  layer is lm_head alone and layer 0 carries the vision projector, for Qwen3.8-Flash-Next
  from safetensors.
- On the mixed-budget Xeon, the automatic moe-offload plan fits, has no more CPU-expert
  layers than any --gpus 1..4 plan, uses fewer than four cards and keeps every card in
  budget.
- --gpus 3 on the fixture Xeon, two cards free, is refused for moe-offload.
- The test's own per-card arithmetic follows the same vision and MTP rule.
88plug added a commit that referenced this pull request Oct 29, 2026
The textual merge keeps #217's moe-offload search, which runs through _llama_multi; the
context checkpoints #216 counted on that path are not carried over yet, and #217's
per-layer KV still counts the sliding-window cache at the window. The next commit fixes
both.
88plug added a commit that referenced this pull request Oct 29, 2026
…checkpoints

With #214, #216 and #217 together, the multi-card llama.cpp path (_llama_multi, which
now also runs every moe-offload plan) had two gaps. Its per-layer KV counted a
sliding-window layer at the window (1024 tokens for Gemma 4) while llama.cpp b11749
allocates GGML_PAD(min(n_ctx_seq, n_swa + n_ubatch), 256) = 1536 cells per slot, so
card_bytes disagreed with kv_bytes. And it left the context checkpoints out of the host
bytes and the pod limit, which #216 counts on one card: the Flash-Next IQ4_NL plan on
the Xeon fixture reported ckpt_bytes 0 with -ctxcp 4 in its flags.

kv_parts now takes the sliding-window tokens from swa_tokens, so per_seq, the per-layer
KV and the checkpoints share one cell count, and _llama_multi adds slots x checkpoints
to the host bytes. The Flash-Next plan's pod limit goes from 74Gi to 76Gi.

Tests: the Gemma split's per-card bytes are worked out at 1536 SWA cells and the KV over
the cards equals kv_bytes; a split plan counts its checkpoints in host bytes; the
Flash-Next two-card shell check asserts the checkpoints are on the host.
88plug added a commit that referenced this pull request Oct 29, 2026
…checkpoints

With #214, #216 and #217 together, the multi-card llama.cpp path (_llama_multi, which
now also runs every moe-offload plan) had two gaps. Its per-layer KV counted a
sliding-window layer at the window (1024 tokens for Gemma 4) while llama.cpp b11749
allocates GGML_PAD(min(n_ctx_seq, n_swa + n_ubatch), 256) = 1536 cells per slot, so
card_bytes disagreed with kv_bytes. And it left the context checkpoints out of the host
bytes and the pod limit, which #216 counts on one card: the Flash-Next IQ4_NL plan on
the Xeon fixture reported ckpt_bytes 0 with -ctxcp 4 in its flags.

kv_parts now takes the sliding-window tokens from swa_tokens, so per_seq, the per-layer
KV and the checkpoints share one cell count, and _llama_multi adds slots x checkpoints
to the host bytes. The Flash-Next plan's pod limit goes from 74Gi to 76Gi.

Tests: the Gemma split's per-card bytes are worked out at 1536 SWA cells and the KV over
the cards equals kv_bytes; a split plan counts its checkpoints in host bytes; the
Flash-Next two-card shell check asserts the checkpoints are on the host.
@88plug 88plug mentioned this pull request Oct 30, 2026
@88plug

88plug commented Oct 30, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #229 (merged)

@88plug 88plug closed this Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant