llama : switch the split mode of a live context - #59
Closed
Piggidragon wants to merge 9 commits into
Closed
Conversation
An out of memory in a simple buft aborted the process instead of returning nullptr, so the caller could not fall back. Assisted-by: Claude Opus 5
The split a prompt wants and the split a single token wants are not the same, so the placement has to be able to change without dropping the model. Free the current placement, then load the weights again under the new split mode, keeping the model object. On failure the previous placement is put back. Assisted-by: Claude Opus 5
llama_context_set_split_mode places the weights for another split mode and rebuilds everything that follows the model devices - the backends, the memory module, the output buffer and the scheduler. The memory, the logits of the last decode and the output ids are carried across, so the caller can sample straight after the switch. A split mode that does not fit leaves the context running under the one it had. Assisted-by: Claude Opus 5
--prefill-split-mode takes the same values as --split-mode and applies to the prompt only. Once the prompt is in the cache the context moves to --split-mode for the generation. Assisted-by: Claude Opus 5
Checks that a context which switched into a split mode continues exactly like a context that was in that mode all along and was handed the same memory state. Skips itself when there is no GPU; --models DIR sweeps every architecture. Assisted-by: Claude Opus 5
- hand the backend samplers to set_sampler again: they are bound to the buffer type of the output device, and a tensor split does not take one at all - reset offload_attn_compute and live_context_workspace before the rebuild; both only ever grow once the context is built - keep the requested flash_attn type instead of the one llama_init_from_model promotes for a tensor split, so that a switch back can resolve it again, and refuse a switch into a tensor split when flash attention is already off - treat a zero state size as the failure it is, and bail before anything is freed - put the previous placement back whenever the model was moved, not only when the split mode differs, so that a new tensor split under the same mode also falls back - clear the memory when the state cannot be restored, so it is empty rather than half written Assisted-by: Claude Opus 5
A failed switch either leaves the context under the prefill split mode, which is worth a warning, or it lost the cache, which is not something to generate from. Reject -psm in interactive mode too: the switch happens once, after the prompt. Assisted-by: Claude Opus 5
… it costs The state that is taken out and put back covers all sequences, so a context that serves several requests keeps every slot. Tested with four sequences, with a unified cache and with one stream per sequence. Drop the --parallel 1 requirement from --prefill-split-mode, which was written for a carry of one sequence, and log the free memory per device around the switch: the two modes do not need the same bytes on the same device. Assisted-by: Claude Opus 5
Assisted-by: Claude Opus 5
Author
|
Closing this. The measurements did not support the premise. Switching the split mode only pays inside a narrow band - the prompt has to be long enough to beat staying in the tensor split, and the generation long enough to beat staying in the layer split. On a 4070 + 3060 with Qwen3.8-27B that band starts at roughly 3800 prompt and 500 generated tokens. Outside it, picking one mode and staying in it wins. For an agentic workload, where the context is largely prefix-cached and the work is decode-dominated, the tensor split wins on total throughput and there is no phase boundary to exploit. The one general fix that came out of this - the meta buffer type aborting on an allocation failure instead of reporting it - moved to #57. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Places the model weights again for a different split mode while the context stays alive, so a
request can process its prompt under one split and generate under the other.
Supersedes #58, which did the same thing entirely inside
llama-completionbytearing down and rebuilding everything. That version answered whether the idea is worth anything;
this one is the library version its "for later" section sketched.
Why
Tensor parallelism splits every layer across the devices and pays for a collective per layer. The
cost of that collective scales with the batch, the benefit does not: at a batch of one there is
almost nothing to exchange and both devices work on the same token, while at a prompt-sized batch
the exchange dominates. A layer split is the mirror image - no collective at all, but the devices
run one after the other, so a single token gets no parallelism.
Prefill wants the layer split, generation wants the tensor split, and nothing in the design forces
one choice for a whole request.
Measured on an RTX 4070 + RTX 3060,
Qwen3.8-27B-UD-IQ2_M, a 17055-token prompt plus 128 generatedtokens at
-c 20480with an f16 cache. Wall clock is the whole request, model load included:-sm layer-sm tensor-psm layer -sm tensor-psm tensor -sm layer(control, wrong way round)The switch keeps 99.1% of layer's prefill and 99.0% of tensor's generation: 27% off the wall clock
against
-sm tensorat the same generation rate, or +16.5% generation against-sm layerfor twoseconds. The reversed control is slower than either pure mode, which is the check that the gain is
the direction and not the switch.
The switch itself carried 1216 MiB of state and placed the weights of the 9.6 GB model in 2.21 s
from a warm page cache.
What this adds
llama_context_set_split_modekeeps the context object and everything the caller holds on to. Thememory, the logits of the last decode and the output ids are carried across, so the caller can
sample straight after the switch. It goes through four steps:
llama_state_get_datawrites a layout-independent form of the memory,which is what makes the carry work at all: the same bytes restore into a cache that is split by
layer or split by head. The host copy of the output buffer comes out the same way.
llama_model::set_split_modefrees the current placement first andthen loads the weights under the new one, so the devices never hold both at once. The model
object survives, only the tensors inside it are replaced.
memory module all follow the devices of the model, so all four are rebuilt through the same code
the constructor uses - it was factored into
init_backends,init_memoryandinit_schedforthat.
A mode that does not fit must not lose the request. If the new placement or the rebuild fails,
the previous split mode is placed again, the context is rebuilt on it and the state goes back in.
The call returns false and the caller still has a working context with its cache intact.
The tool
llama-completiongets--prefill-split-mode/-psm, which takes the same values as--split-mode. Against #58, the tool side is now nine lines: the model, the context, the samplerand the chat templates all survive, and the last prompt token no longer has to be held back to
produce logits, because the logits are carried.
-psmis rejected in interactive mode, where there is more than one prompt and the switch happensonly once.
Several sequences at once
The switch takes out and puts back the state of all sequences, not only the one that happens to
be generating, so a context that serves several requests keeps every slot's cache across it. Tested
with four sequences, with a unified cache and with one stream per sequence, in both directions:
each sequence continues exactly like the same sequence in a context that was in the target mode all
along and was handed the same state.
What the switch cannot do is run two split modes at once - the split mode belongs to the model - or
run while a
llama_decodeis in flight. Those are the real limits for a server, not the state carry.-psmitself gives nothing to a server, and not because of a missing loop: it fires once, at theboundary between prompt and generation of one linear request, and that boundary does not exist when
one slot prefills while another decodes. The library call is the part a server needs; the policy that
would decide when to use it is not here.
The reason to want it does survive parallelism, though.
llama-batched-bench, same model,-c 32768,f16 cache,
-npp 2048 -ntg 128:Both edges hold at every level, so the two phases still want different modes however many sequences
are in flight. Note that this does not match #58's measurement, where the decode edge shrank from
+33.5% to +23.4% as the batch grew - different quantisation and cache type, so the shape of the
tradeoff is setup-specific and worth measuring before relying on it.
On total throughput at this prompt-to-generation ratio the layer split wins everywhere (315 -> 596
t/s against 285 -> 452 t/s), because prefill dominates the mix. A server that wanted both halves
would have to batch its prefills and its decodes into separate phases first, which is a scheduler
change, not this one.
Device memory
The old placement is freed before the new one is asked for, so the devices never hold both. Peak per
device, sampled at 10 Hz over the whole run,
Qwen3.8-27B-UD-IQ2_Mat-c 20480:-sm layer-sm tensor-psm layer -sm tensorThe switch costs the per-device maximum of the two modes, not their sum: 6362 against a layer
peak of 5806 and a tensor peak of 6418 on GPU0. The largest context that survives a switch is
therefore smaller than what either pure mode could hold.
When the new mode does not fit anyway, the allocation failure is caught rather than fatal. Checked by
holding 5976 MiB on GPU0 with a helper process, which leaves room for the layer split but not for the
tensor split:
-sm layer-sm tensorcudaMalloc failed: out of memoryat load, request lost-psm layer -sm tensorThe third row is the point: the 17055-token prompt was already in the cache, the tensor placement
failed on a 505 MiB allocation, the layer placement was put back, the context was built on it again
and the cache was restored - and the request finished. That path needs the ggml change below to be
reachable at all.
What is deliberately not here
file, which is in the page cache after the first load. A true device-to-device migration would
need both placements to exist at the same time, which raises the peak device memory - the opposite
of what a switch under memory pressure needs.
exercised; it costs one host round trip of the cache and no new mechanism.
the explicit form has been used, and that a server with several slots has no single right answer.
The explicit call is here; the policy is not.
Tests
tests/test-split-mode-switch.cppruns against the dummy models thattest-llama-archsgenerates.Three of them are registered with ctest, following the pattern
test-recurrent-state-rollbackalready uses;
--models DIRsweeps the whole directory, which is how every architecture getscovered by hand. It skips itself when there is no GPU, so it costs nothing in CI.
The check that matters compares a context that switched into a mode against a context that was in
that mode all along and was handed the same memory state: both then decode the same token and
generate greedily, and the tokens must be identical. On top of that it checks that the serialized
state and the logits of the last decode are byte-identical across the switch, that a switch out and
back leaves the context exactly where it was, that a rejected split mode leaves a working context,
and that the model-level call on its own produces a model that generates like one loaded under that
split mode from the start.
The sweep over the generated models is 883 passed, 0 failed, 207 skipped over 109 models. The skips
are the architectures where a tensor split is not implemented, where the switch is refused and the
context has to keep working - that path is checked too.
One model is left out of the sweep:
qwen3-dense.ggufcannot be run at all, becausea plain
llama-bench -m qwen3-dense.gguf -sm tensor -n 4aborts onorigin/llama/devwithGGML_ASSERT(src_ss[0].axis != GGML_BACKEND_SPLIT_AXIS_0)while generating. That is a pre-existingtensor-split bug on a dummy model shape, unrelated to this change, and the only model of the 110
that is affected.
Also here
One ggml change, taken from #58: the meta buffer type asserted on an allocation failure instead of
returning nullptr, so an out of memory under
-sm tensoraborted the process and no fallback wasreachable. It now frees what it holds and returns nullptr, the same contract the CUDA buffer type
follows. Without it the fallback above cannot run.
Note on flash attention
llama_init_from_modelused to rewrite--flash-attn autotoenabledwhen the model was loadedunder a tensor split. The context now applies that rule itself, so the requested type survives and a
switch back to a layer split can resolve it again instead of running with flash attention forced on.
-sm tensor -fa autobehaves the same as before, checked against the same model.Limitations
vector loaded. Both keep tensors of their own on the devices of the old placement, and the library
cannot rebuild them.
FILE *or from metadata hasnowhere to read the weights from.
llama_memory_ttaken from the context before the switch is invalid after it.-tsmeans a share of the layers under a layer split and a share of every tensor under a tensorsplit. The library call takes a
tensor_splitfor that reason;-psmkeeps the one the model has.common_fit_paramsstill has no implementation forLLAMA_SPLIT_MODE_TENSOR, so a tensor phaseis not fitted, only caught by the fallback if it does not fit.
AI disclosure: Claude Opus 5 wrote the implementation, the test and this description from a design I
own. I have read and understand every line and can defend it without it.