Repository navigation
Multi-GPU: the session carve and the per-stage prompt loans - #216
gopinath87607 wants to merge 1 commit into
Conversation
|
The carve and the per-stage loan are exactly what the split needed. Please split this: keep the single-GPU loan identical to main's |
58adc94 to
c3108f0
Compare
Both done. The offload is gone entirely — you were right about the race: ro_collect issues the H2D from the pinned output on m.cs, and the next ro_issue/begin_rows on that rank overwrites the same buffer with nothing ordering the two. The loan is plan_lend again: the cap is an auto-chunk rule only, and a participant that can't lend falls back to its own buffers. With one participant it reduces to main exactly — verified with STRATA_PREFILL_LEND_PCT=50: auto demotes 8192→4096 (47%), explicit holds 8192 at 80.8%. Carve and loans unchanged on the rig (8192 / 2154 / 1994 of 2597). btw |
|
fixing the conflict now |
c3108f0 to
68e80d6
Compare
|
testing the quality. seems like there is quality issues above 100k updated tested like this cd /home/gopi/Strata json the response is really smooth and accurate above 100k & currently its ready to merge |
|
Thanks @gopinath87607, all our asks are in and it merges cleanly. Next: our single-GPU byte gate on 4 quants plus Windows and HIP builds. Two things from you: a split vs single-GPU greedy identity run at a fixed cache, and a word on the '100K quality' remark (is there an open problem above 100K or not?). |
hi niko i am currently focusing on adding Q8_0 quant support and above 100k its working well but still i am not satisfied with quality. give me a day i will update everything. |
|
Independent datapoint on 3x RTX 2080 Ti 22GB (the 2x16 + x8 topology, PCIe P2P, no NVLink) — Single card (GPU0, fixed cache): --expert-cache 10044 --adapt-swaps 0 --pcie-frac 0,
Split + cache-only third card: --layer-split 26 (explicit 2-stage — the auto planner gives all
The remote cache turns the second stage's host-computed misses into GPU hits over PCIe, and the Identity: run-to-run on the single card is identical (A1 vs A2, 285 chars), so the gate is On the 100K-quality question: at 97.6k tokens the split+cache output matched the single card |
|
i have pushed the support for unsloth Q8_0 . fixing the conflicts now |
Two things a layer split was paying for and not using. * THE SESSION CARVE. A stage carved state for ALL 48 layers whatever slice it ran - every QSA KV pool at max_cells, every GDN recurrence - the whole-model-sized-buffer disease the chunked-QSA-prefill PR cured upstream. `session_bytes`/`session_init` take [layer_lo, layer_hi), and a session now owns only that range: `qsa_states`/`gdn_state` keep GLOBAL ordinal indexing and simply hold fewer rows (`qsa_ord0`/`qsa_alloc`, `gdn_ord0`/`gdn_alloc`), per-layer consumers subtract `gdn_ord0`, and the model-level uses - the shared RoPE table, the prefill staging identity, the MTP rope borrow - read `qsa_states[qsa_primary()]`. A range with no QSA layer still carves ONE primary state so those uses stay valid, and the default full range is byte-identical to the old carve. On the 2x3060 + 2x5060 rig, CUDA1's [30, 48) session costs 0.93 GiB: 7.83 GiB free before the carve, 6.90 GiB after (`session [30, 48) and the head`). * PER-STAGE PROMPT LOANS. `no_prefill_borrow = true` for split stages said "each stage's prompt path has its own buffers" - true, but that is a reason to give each stage its own LOAN, not to make every stage withhold a chunk-sized reserve from its cache for the whole session, and forced on it also collapsed `--prefill auto` to 2048. Each prompt path now borrows the tail of ITS OWN stage's cache (`PfPart`), so outside the prompt the whole cache is expert cache: `--prefill auto` is the largest chunk every participant can lend (8192 tokens here), a participant refills its loan into its own slots and marks only its own layers (a slot refilled into another cache, or another stage's rows marked resident, is silent and produces plausible tokens), and the chunk is the largest one EVERY participant can lend rather than one card's answer imposed on the others. A participant that cannot lend the buffers falls back to the prompt path's own, as a lone one always could; with a single participant the plan reduces to `plan_lend` exactly, so the single-GPU loan is unchanged from main - the percentage cap stays an AUTO-chunk rule (an explicit `--prefill` is the operator's number and a loan of it only has to leave the 128-slot floor). The flat 1 GiB withheld from every stage after the first becomes what is actually held back after the weights, session and drafter exist: 96 MiB of verify windows, plus 1000 MiB of drafter and head on the ONE stage that carries them. CUDA0 lends 2162 slots (3.98 GiB), CUDA1 1993 of its 2653 (3.97 GiB). * REPORTING. `expert_slots`/`expert_cache_mib` sum every tier: a 4-GPU 256K run reported CUDA0's cache alone (3327 experts) while the four cards held 13310, so the Monitor tab was wrong by 4x for every multi-GPU config. `expert_slots_primary`/`expert_cache_primary_mib` keep the old figure. The helper ranks' ms_begin/ms_wait are cumulative since boot and were printed next to per-request deltas; they take deltas now. KV-streaming stats iterate the session's OWNED ordinals. * REFUSALS. `--gpu-only-full` and `--gpu-stages` replay every layer through CUDA0's session, which a split carves to CUDA0's range, so they are refused with a layer split rather than answering about another stage's state; the token-graph hit path is disabled under a split (such a graph cannot span stages); and the sessions are allocated AFTER the split search, because the search prices each placement by what that range's session will cost on that device (`session_bytes` is pure arithmetic). Co-Authored-By: Claude Code <noreply@anthropic.com>
68e80d6 to
c439f9b
Compare
|
@gopinath87607 your carve and per-stage loans are in the 0.1.30 release candidate, branch
And a word on the earlier 100K remark would help. Thanks! |
Tested on 1. Split vs single at the fixed cache — not identical (214 vs 205 tokens, first diff char 241). It's the cache set, not the carve. The split's two stages hold 2005 and 2849 slots filled from their own layers' pairs, with 3670 more on each 5060; the single card holds the global top-1934. Different experts on the GPU, and a GPU-cached expert doesn't round like a CPU one. Isolating it:
Limit: 2. >16K prompt + new turn — RESUME passes, the fresh-read identity does not. 3. 100K — reran on One thing I have not isolated, for what it's worth: 2 of 4 engine runs ignored SIGINT and sat in |
…rom 1024-token chunks), the resident low-RAM variant, the multi-GPU session carve (#216), AMD RDNA4, idle unload (#208); opt-ins: rope scaling (#84), conversation cache (#189), STRATA_IQ_MT_MIN=1 (#152); setup requires it Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
|
Released in 0.1.30, with the snapshot/checkpoint follow-up so a session saves and restores only the layers it owns (needed together with #189). Single-GPU output is byte-identical to 0.1.29. We also ran it on two AMD RDNA4 cards (R9700 32 GB + RX 9070 XT 16 GB): with the same experts on the GPUs the split gives exactly the tokens of one card, and checkpoints on the split (second turn, rewind, a shared prefix, a cancelled prompt retried) equal a fresh read. We don't have an NVIDIA multi-GPU rig, so please run 0.1.30 on your 2x3060 + 2x5060 and tell us if anything differs from your branch. Merged under your name. Thank you! |
…cache) into rel-0.1.30 Conflicts in generate.cpp: - against Niko1221#216's session carve: conv_state_sizes takes the session (the carve's signature) and scales the PR's whole-model GDN size to ss.gdn_alloc rows; checkpoint_save/checkpoint_restore are the PR's (conversation_state.cpp), which the next commit makes carve-aware. - STATE_HASH: the per-GDN-layer line over ss.gdn_alloc (guarded for zero rows), the QSA loop over the owned ordinals [qsa_ord0, qsa_ord0 + qsa_alloc) with the PR's dead= and pooled_full= in it, and end_cell from ss.max_cells (the primary state's max_cells, and valid with no QSA layer). - INFO: f-resram's arena_mib (resident bytes under --mmap-experts) and the PR's three conversation_cache_* fields. The --conversation-cache-* options sit in the option parser's second chain (8616c60); their value is `number`, not `parsed`, which is that chain's flag. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
…1221#216): a session saves and restores only the layers it owns conversation_session_sizes() gives one session's running-state sizes: the GDN bytes of its gdn_alloc rows, the indexer tail/dead/block_pos per owned QSA state; an inconsistent carve is rejected. Checkpoint save/validate/restore, the snapshot size estimate, capture, validation and restore iterate the owned QSA ordinals [qsa_ord0, qsa_ord0 + qsa_alloc) instead of every model layer (the spare pooled row is written back per owned layer only), and a parked snapshot records the carve it was taken from (layer_lo, layer_hi): a restore into another range is refused before any write. One GPU (the default full range: gdn_alloc = n_gdn_layers, qsa_ord0 = 0, qsa_alloc = n_qsa_layers) saves and restores exactly the bytes it did before, in the same order. generate.cpp's conv_state_sizes uses the same function. The tests' hand-built sessions carry the full-range carve, and the host-only validation test checks the carved sizes and the rejections. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
…rompt buffers); 2 x RTX 2080 Ti measured What each card holds and the prompt-buffer limit still described the state before Niko1221#216: every card now holds only its own layers' session and borrows its prompt buffers from its own cache. Adds the IQ2_XS numbers on two RTX 2080 Ti cards, split against one card and against the 8 GiB arena pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rompt buffers); 2 x RTX 2080 Ti measured What each card holds and the prompt-buffer limit still described the state before Niko1221#216: every card now holds only its own layers' session and borrows its prompt buffers from its own cache. Adds the IQ2_XS numbers on two RTX 2080 Ti cards, split against one card and against the 8 GiB arena pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rompt buffers); 2 x RTX 2080 Ti measured What each card holds and the prompt-buffer limit still described the state before Niko1221#216: every card now holds only its own layers' session and borrows its prompt buffers from its own cache. Adds the IQ2_XS numbers on two RTX 2080 Ti cards, split against one card and against the 8 GiB arena pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rompt buffers); 2 x RTX 2080 Ti measured What each card holds and the prompt-buffer limit still described the state before Niko1221#216: every card now holds only its own layers' session and borrows its prompt buffers from its own cache. Adds the IQ2_XS numbers on two RTX 2080 Ti cards, split against one card and against the 8 GiB arena pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A second visible CUDA GPU (N >= 1) holds an adaptive expert cache beside CUDA0's, filled with the ranked pairs the primary does not hold. Activations and result rows cross NVLink or another P2P path; the peer also computes its experts' rows for prompt chunks (MMQ; the fused path is not combined yet). Knobs: --peer-device N, --peer-reserve-mib, --peer-slots, --peer-adapt-swaps, --peer-prefill-rows; sizing stays manual until #216's buffer lending lands. Default output unchanged: without the flag byte-identical to v0.1.36, with it byte-identical under the exactness gate. cudaHostAllocPortable only with a peer. docs/SECOND_GPU.md documents the flags and refused combinations. Measured on 2x RTX 3090 + NVLink, IQ3_S: prefill +15/+25/+25 %, decode +19/+14/+16 %.
A second visible CUDA GPU (N >= 1) holds an adaptive expert cache beside CUDA0's, filled with the ranked pairs the primary does not hold. Activations and result rows cross NVLink or another P2P path; the peer also computes its experts' rows for prompt chunks (MMQ; the fused path is not combined yet). Knobs: --peer-device N, --peer-reserve-mib, --peer-slots, --peer-adapt-swaps, --peer-prefill-rows; sizing stays manual until #216's buffer lending lands. Default output unchanged: without the flag byte-identical to v0.1.36, with it byte-identical under the exactness gate. cudaHostAllocPortable only with a peer. docs/SECOND_GPU.md documents the flags and refused combinations. Measured on 2x RTX 3090 + NVLink, IQ3_S: prefill +15/+25/+25 %, decode +19/+14/+16 %.
…cache) into rel-0.1.30 Conflicts in generate.cpp: - against Niko1221#216's session carve: conv_state_sizes takes the session (the carve's signature) and scales the PR's whole-model GDN size to ss.gdn_alloc rows; checkpoint_save/checkpoint_restore are the PR's (conversation_state.cpp), which the next commit makes carve-aware. - STATE_HASH: the per-GDN-layer line over ss.gdn_alloc (guarded for zero rows), the QSA loop over the owned ordinals [qsa_ord0, qsa_ord0 + qsa_alloc) with the PR's dead= and pooled_full= in it, and end_cell from ss.max_cells (the primary state's max_cells, and valid with no QSA layer). - INFO: f-resram's arena_mib (resident bytes under --mmap-experts) and the PR's three conversation_cache_* fields. The --conversation-cache-* options sit in the option parser's second chain (6fb466b); their value is `number`, not `parsed`, which is that chain's flag.
…1221#216): a session saves and restores only the layers it owns conversation_session_sizes() gives one session's running-state sizes: the GDN bytes of its gdn_alloc rows, the indexer tail/dead/block_pos per owned QSA state; an inconsistent carve is rejected. Checkpoint save/validate/restore, the snapshot size estimate, capture, validation and restore iterate the owned QSA ordinals [qsa_ord0, qsa_ord0 + qsa_alloc) instead of every model layer (the spare pooled row is written back per owned layer only), and a parked snapshot records the carve it was taken from (layer_lo, layer_hi): a restore into another range is refused before any write. One GPU (the default full range: gdn_alloc = n_gdn_layers, qsa_ord0 = 0, qsa_alloc = n_qsa_layers) saves and restores exactly the bytes it did before, in the same order. generate.cpp's conv_state_sizes uses the same function. The tests' hand-built sessions carry the full-range carve, and the host-only validation test checks the carved sizes and the rejections.
…cache) into rel-0.1.30 Conflicts in generate.cpp: - against Niko1221#216's session carve: conv_state_sizes takes the session (the carve's signature) and scales the PR's whole-model GDN size to ss.gdn_alloc rows; checkpoint_save/checkpoint_restore are the PR's (conversation_state.cpp), which the next commit makes carve-aware. - STATE_HASH: the per-GDN-layer line over ss.gdn_alloc (guarded for zero rows), the QSA loop over the owned ordinals [qsa_ord0, qsa_ord0 + qsa_alloc) with the PR's dead= and pooled_full= in it, and end_cell from ss.max_cells (the primary state's max_cells, and valid with no QSA layer). - INFO: f-resram's arena_mib (resident bytes under --mmap-experts) and the PR's three conversation_cache_* fields. The --conversation-cache-* options sit in the option parser's second chain (6fb466b); their value is `number`, not `parsed`, which is that chain's flag.
…1221#216): a session saves and restores only the layers it owns conversation_session_sizes() gives one session's running-state sizes: the GDN bytes of its gdn_alloc rows, the indexer tail/dead/block_pos per owned QSA state; an inconsistent carve is rejected. Checkpoint save/validate/restore, the snapshot size estimate, capture, validation and restore iterate the owned QSA ordinals [qsa_ord0, qsa_ord0 + qsa_alloc) instead of every model layer (the spare pooled row is written back per owned layer only), and a parked snapshot records the carve it was taken from (layer_lo, layer_hi): a restore into another range is refused before any write. One GPU (the default full range: gdn_alloc = n_gdn_layers, qsa_ord0 = 0, qsa_alloc = n_qsa_layers) saves and restores exactly the bytes it did before, in the same order. generate.cpp's conv_state_sizes uses the same function. The tests' hand-built sessions carry the full-range carve, and the host-only validation test checks the carved sizes and the rejections.
…rom 1024-token chunks), the resident low-RAM variant, the multi-GPU session carve (Niko1221#216), AMD RDNA4, idle unload (Niko1221#208); opt-ins: rope scaling (Niko1221#84), conversation cache (Niko1221#189), STRATA_IQ_MT_MIN=1 (Niko1221#152); setup requires it
A second visible CUDA GPU (N >= 1) holds an adaptive expert cache beside CUDA0's, filled with the ranked pairs the primary does not hold. Activations and result rows cross NVLink or another P2P path; the peer also computes its experts' rows for prompt chunks (MMQ; the fused path is not combined yet). Knobs: --peer-device N, --peer-reserve-mib, --peer-slots, --peer-adapt-swaps, --peer-prefill-rows; sizing stays manual until Niko1221#216's buffer lending lands. Default output unchanged: without the flag byte-identical to v0.1.36, with it byte-identical under the exactness gate. cudaHostAllocPortable only with a peer. docs/SECOND_GPU.md documents the flags and refused combinations. Measured on 2x RTX 3090 + NVLink, IQ3_S: prefill +15/+25/+25 %, decode +19/+14/+16 %.
…cache) into rel-0.1.30 Conflicts in generate.cpp: - against Niko1221#216's session carve: conv_state_sizes takes the session (the carve's signature) and scales the PR's whole-model GDN size to ss.gdn_alloc rows; checkpoint_save/checkpoint_restore are the PR's (conversation_state.cpp), which the next commit makes carve-aware. - STATE_HASH: the per-GDN-layer line over ss.gdn_alloc (guarded for zero rows), the QSA loop over the owned ordinals [qsa_ord0, qsa_ord0 + qsa_alloc) with the PR's dead= and pooled_full= in it, and end_cell from ss.max_cells (the primary state's max_cells, and valid with no QSA layer). - INFO: f-resram's arena_mib (resident bytes under --mmap-experts) and the PR's three conversation_cache_* fields. The --conversation-cache-* options sit in the option parser's second chain (8616c60); their value is `number`, not `parsed`, which is that chain's flag. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
…1221#216): a session saves and restores only the layers it owns conversation_session_sizes() gives one session's running-state sizes: the GDN bytes of its gdn_alloc rows, the indexer tail/dead/block_pos per owned QSA state; an inconsistent carve is rejected. Checkpoint save/validate/restore, the snapshot size estimate, capture, validation and restore iterate the owned QSA ordinals [qsa_ord0, qsa_ord0 + qsa_alloc) instead of every model layer (the spare pooled row is written back per owned layer only), and a parked snapshot records the carve it was taken from (layer_lo, layer_hi): a restore into another range is refused before any write. One GPU (the default full range: gdn_alloc = n_gdn_layers, qsa_ord0 = 0, qsa_alloc = n_qsa_layers) saves and restores exactly the bytes it did before, in the same order. generate.cpp's conv_state_sizes uses the same function. The tests' hand-built sessions carry the full-range carve, and the host-only validation test checks the carved sizes and the rejections. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
…rom 1024-token chunks), the resident low-RAM variant, the multi-GPU session carve (Niko1221#216), AMD RDNA4, idle unload (Niko1221#208); opt-ins: rope scaling (Niko1221#84), conversation cache (Niko1221#189), STRATA_IQ_MT_MIN=1 (Niko1221#152); setup requires it Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0154bwiad8gY7wRehCXy27pN
A second visible CUDA GPU (N >= 1) holds an adaptive expert cache beside CUDA0's, filled with the ranked pairs the primary does not hold. Activations and result rows cross NVLink or another P2P path; the peer also computes its experts' rows for prompt chunks (MMQ; the fused path is not combined yet). Knobs: --peer-device N, --peer-reserve-mib, --peer-slots, --peer-adapt-swaps, --peer-prefill-rows; sizing stays manual until Niko1221#216's buffer lending lands. Default output unchanged: without the flag byte-identical to v0.1.36, with it byte-identical under the exactness gate. cudaHostAllocPortable only with a peer. docs/SECOND_GPU.md documents the flags and refused combinations. Measured on 2x RTX 3090 + NVLink, IQ3_S: prefill +15/+25/+25 %, decode +19/+14/+16 %.
これまでセッション(主のものも、--batch の会話の枠も)は、どの GPU でもモデル全体の 48 層の状態を持っていた。 iq3_s、8192 トークンで 1 つ 327 MiB のほとんど(GDN 112 MiB、QSA 206 MiB)は層ごとの状態で、 B70 と 4070、K=34、--batch 8 では 4070 のエキスパートのキャッシュが 1〜2 GiB しか残らなかった。 upstream Niko1221#216(0.1.30 の session carve と e46ccca)と同じく、セッションは [layer_lo, layer_hi) の層の状態だけを 切り出す。qsa_states と gdn_state は通し番号のまま引き、GDN は gdn_ord0 を引いた行、モデル全体で 1 つの使い方 (RoPE の表、プリフィルの段取り、MTP の借用)は qsa_primary() を読む。チェックポイントの保存と復元、 会話の枠への写しは、セッションが持つ層だけを扱う。 分割点を明示した層の分割のときだけ切り出す(XeStrata は分割点の自動決定より前にセッションを作るので、 auto と 1 つの GPU と --split-device 0 は今までどおり全部の層)。session_init で切り出していない セッション(手で組んだもの)は、全部の層を持つものとして扱う。 B70 と 4070、iq3_s、--layer-split 34、--batch 8、STRATA_STAGE_TRIM=1、tools/batch_test.py: 会話の枠 1 つ 0.32 → 0.22 GiB、4070 のキャッシュ 1〜2 → 4.41 GiB。最初の同時のトークンからの合計は、 B70 単独 55.9 tok/s に対し、まとまり 1 つ 65.4、--batch-groups 2 は 71.6 / 72.8 tok/s(前は 57.6〜57.7)。 iq2_xs の --layer-split 24 の 64 トークンは、1 つの GPU の実行と一致した。
…ranks' prompt rows
Three things a layer split was paying for and not using.
THE SESSION CARVE. A stage carved state for ALL 48 layers whatever slice it ran - every QSA KV pool at max_cells, every GDN recurrence - the whole-model-sized-buffer disease the chunked-QSA-prefill PR cured upstream.
session_bytes/session_inittake [layer_lo, layer_hi), and a session now owns only that range:qsa_states/gdn_statekeep GLOBAL ordinal indexing and simply hold fewer rows (qsa_ord0/qsa_alloc,gdn_ord0/gdn_alloc), per-layer consumers subtractgdn_ord0, and the model-level uses - the shared RoPE table, the prefill staging identity, the MTP rope borrow - readqsa_states[qsa_primary()]. A range with no QSA layer still carves ONE primary state so those uses stay valid, and the default full range is byte-identical to the old carve. On the 2x3060 + 2x5060 rig, CUDA1's [30, 48) session costs 0.93 GiB: 7.83 GiB free before the carve, 6.90 GiB after (session [30, 48) and the head).PER-STAGE PROMPT LOANS.
no_prefill_borrow = truefor split stages said "each stage's prompt path has its own buffers" - true, but that is a reason to give each stage its own LOAN, not to make every stage withhold a chunk-sized reserve from its cache for the whole session, and forced on it also collapsed--prefill autoto2048. Each prompt path now borrows the tail of ITS OWN stage's cache (
PfPart), so outside the prompt the whole cache is expert cache:--prefill autois the largest chunk every participant can lend (8192 tokens here), a participant refills its loan into its own slots and marks only its own layers (a slot refilled into another cache, or another stage's rows marked resident, is silent and produces plausible tokens), and a cache that cannot lend even a 256-token chunk is refused with both numbers rather than over-subscribing a card whose cache has already filled its VRAM. The flat 1 GiB withheld from every stage after the first becomes what is actually held back after the weights, session and drafter exist: 96 MiB of verify windows, plus 1000 MiB of drafter and head on the ONE stage that carries them. CUDA0 lends 2162 slots (3.98 GiB), CUDA1 1993 of its 2653 (3.97 GiB).THE HELPER RANKS' PROMPT ROWS - SHIPPED OFF, AND MEASURED AS A LOSS.
RemoteExperts::has/begin_rows/prompt_input/prompt_outputandkBatchRowslet a helper rank (CUDA2/3) compute prompt rows for the experts it already holds, so those blobs are never staged to a stage;Prefill::set_remotedrives it behind STRATA_PREFILL_REMOTE (STRATA_PREFILL_REMOTE_SHARE gradients it by dropping a hash-scattered share), the staging is one 4096-row batch per rank (CAP was MAXT*k for decode, now one prompt batch: ~42 MiB pinned and ~54 MiB of device scratch per rank), andgenerate.cpphands the ranks to exactly ONE prompt path - a rank owns one set of buffers and one stream, and a split runs the next stage on its own thread.IT LOSES. 23,420-token prompt, measured on the 0.1.24 tree this was developed against: 38.1 -> 83.4 s of wall, 2.2x slower. Two causes, both measured: the 3.5M rows batch into 858 fully serialized 4096-row calls at ~52 ms each where the transfer is ~7 ms (1.5 GB/s against an 11.0 GB/s probe), and STRATA_REMOTE_ZEROCOPY (default on) makes the helper quantize from, and write into, BAR1-mapped host memory 41.9 MB at a time; turning it off alone recovers 26 s of timeline (76.9 -> 51.1 s) without closing the gap. It is therefore OFF by default and nothing in the shipped path changes.
IT ALSO CORRUPTS UNDER MMQ, SO THAT COMBINATION IS NOW REFUSED. MMQ computes MMQ_GROUP=16 experts per launch and flushes the group's one GEMM from the
computecall of the group's LAST member, so an expert this stage skips because a helper holds it silently takes its whole group's output with it - a third to a half of a layer's experts missing reads as fluent garbage (!x 64). With the per-expert path (STRATA_PREFILL_MMQ=0) the two arms are BIT-IDENTICAL (sha256(reasoning) equal, 542.1 vs 549.3 tok/s), which is what proves the row staging itself is exact.Prefill::initnow prints the reason and drops the ranks instead of corrupting.REPORTING.
expert_slots/expert_cache_mibsum every tier: a 4-GPU 256K run reported CUDA0's cache alone (3327 experts) while the four cards held 13310, so the Monitor tab was wrong by 4x for every multi-GPU config.expert_slots_primary/expert_cache_primary_mibkeep the old figure. The helper ranks' ms_begin/ms_wait are cumulative since boot and were printed next to per-request deltas; they take deltas now. KV-streaming stats iterate the session's OWNED ordinals.REFUSALS.
--gpu-only-fulland--gpu-stagesreplay every layer through CUDA0's session, which a split carves to CUDA0's range, so they are refused with a layer split rather than answering about another stage's state; the token-graph hit path is disabled under a split (such a graph cannot span stages); and the sessions are allocated AFTER the split search, because the search prices each placement by what that range's session will cost on that device (session_bytesis pure arithmetic).updates
Multi-GPU: the session carve and the per-stage prompt loans
Two things a layer split was paying for and not using.
THE SESSION CARVE. A stage carved state for ALL 48 layers whatever slice it ran - every QSA KV pool at
max_cells, every GDN recurrence - the whole-model-sized-buffer disease the chunked-QSA-prefill PR cured
upstream.
session_bytes/session_inittake [layer_lo, layer_hi), and a session now owns only that range:qsa_states/gdn_statekeep GLOBAL ordinal indexing and simply hold fewer rows (qsa_ord0/qsa_alloc,gdn_ord0/gdn_alloc), per-layer consumers subtractgdn_ord0, and the model-level uses - the shared RoPEtable, the prefill staging identity, the MTP rope borrow - read
qsa_states[qsa_primary()]. A range with noQSA layer still carves ONE primary state so those uses stay valid, and the default full range is
byte-identical to the old carve. On the 2x3060 + 2x5060 rig, CUDA1's [30, 48) session costs 0.93 GiB:
7.83 GiB free before the carve, 6.90 GiB after (
session [30, 48) and the head).PER-STAGE PROMPT LOANS.
no_prefill_borrow = truefor split stages said "each stage's prompt path has itsown buffers" - true, but that is a reason to give each stage its own LOAN, not to make every stage withhold a
chunk-sized reserve from its cache for the whole session, and forced on it also collapsed
--prefill autoto2048. Each prompt path now borrows the tail of ITS OWN stage's cache (
PfPart), so outside the prompt thewhole cache is expert cache:
--prefill autois the largest chunk every participant can lend (8192 tokenshere), a participant refills its loan into its own slots and marks only its own layers (a slot refilled into
another cache, or another stage's rows marked resident, is silent and produces plausible tokens), and the
chunk is the largest one EVERY participant can lend rather than one card's answer imposed on the others.
A participant that cannot lend the buffers falls back to the prompt path's own, as a lone one always could;
with a single participant the plan reduces to
plan_lendexactly, so the single-GPU loan is unchanged frommain - the percentage cap stays an AUTO-chunk rule (an explicit
--prefillis the operator's number and aloan of it only has to leave the 128-slot floor). The flat 1 GiB withheld from every stage after the first
becomes what is actually held back after the weights, session and drafter exist: 96 MiB of verify windows,
plus 1000 MiB of drafter and head on the ONE stage that carries them. CUDA0 lends 2162 slots (3.98 GiB),
CUDA1 1993 of its 2653 (3.97 GiB).
REPORTING.
expert_slots/expert_cache_mibsum every tier: a 4-GPU 256K run reported CUDA0's cache alone(3327 experts) while the four cards held 13310, so the Monitor tab was wrong by 4x for every multi-GPU
config.
expert_slots_primary/expert_cache_primary_mibkeep the old figure. The helper ranks'ms_begin/ms_wait are cumulative since boot and were printed next to per-request deltas; they take deltas now.
KV-streaming stats iterate the session's OWNED ordinals.
REFUSALS.
--gpu-only-fulland--gpu-stagesreplay every layer through CUDA0's session, which a splitcarves to CUDA0's range, so they are refused with a layer split rather than answering about another stage's
state; the token-graph hit path is disabled under a split (such a graph cannot span stages); and the sessions
are allocated AFTER the split search, because the search prices each placement by what that range's session
will cost on that device (
session_bytesis pure arithmetic).🤖 Generated with Claude Code