Skip to content

layer split: overlap the stages' hand-off (STRATA_SPLIT_OVERLAP=1, opt-in) - #1120

Closed
bsvinay wants to merge 4 commits into
Niko1221:mainfrom
bsvinay:split-overlap
Closed

bsvinay wants to merge 4 commits into
Niko1221:mainfrom
bsvinay:split-overlap

Conversation

@bsvinay

@bsvinay bsvinay commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Resubmitted from #936, which GitHub closed when main's history was rewritten. Same 4 commits, cherry-picked onto the new main (no conflicts). The review discussion and test results from others are in #936.

On 2x RX 7900 XTX the layer split decoded slower than one card. The cause is the order inside every verify window: stage 1 finishes, the host syncs its stream, then stages and launches stage 2's graph. Under HIP both the sync and the graph launch are expensive, and they sit on the critical path of every window while both cards are idle.

With STRATA_SPLIT_OVERLAP=1 (off by default; stages on different devices only):

  • each hand-off buffer gets a ready word (mapped pinned memory);
  • the writing stage's graph ends with handoff_publish: the payload with volatile stores (RDNA keeps plain stores to mapped memory in L2, same reason as doorbell_ring_kernel), a system fence, and the last block raises the flag;
  • the reading stage's graph starts with a wait on that flag;
  • Verifier::run launches the next stage's window on a helper thread while it serves its own layers, and syncs the earlier stage only once the chain is done;
  • release_gpu_waits also raises the hand-in flag, and an early return after the early launch releases the next stage (Windows: the verify-window stall wedges the GPU into an unrecoverable "device lost" state (manual power cycle required), with no TDR logged #267).

Changes since #936 (from the review there):

Measured on 2x RX 7900 XTX (gfx1100, ROCm 10.0.0, Linux), Ryzen 9 9900X, 52 GB RAM, Swift 1.5 IQ3_XXS, 128K context, 8-bit KV, split 0-25 / 26-47, 512-token greedy answers:

serial (default) STRATA_SPLIT_OVERLAP=1
decode, code prompt 53 tok/s 69.6 tok/s
decode, prose prompt 45 tok/s 57.8 tok/s

fragtion pushed a commit to fragtion/StrataPool that referenced this pull request Oct 7, 2026
…eline-windows (Niko1221#859)

layer split: overlap the stages' hand-off (STRATA_SPLIT_OVERLAP=1, opt-in)

Each verify window of a layer split runs its stages strictly in turn: stage 1 finishes, the host syncs its stream,
then stages and launches stage 2's graph. Under HIP the sync and the graph launch both sit on the critical path of
every window.

With STRATA_SPLIT_OVERLAP=1 the hand-off carries a ready word (mapped, one per hand-off buffer):
- the writing stage's graph ends with handoff_publish: the payload with volatile stores (RDNA keeps plain stores to
  mapped memory in L2), a system fence per thread, and the last block raises the flag;
- the reading stage's graph starts with wait_flag_ge(flag, 1);
- Verifier::run captures the next stage's window on this thread, then stages and launches it on a helper thread
  (prelaunch) while this thread serves its own layers; it then serves the next stage's layers directly and syncs
  the earlier stage once the chain is done;
- release_gpu_waits also raises the hand-in flag; an early return after the prelaunch releases the next stage.

Batch windows and --split-device 0 keep the serial order. Off by default.

Measured on 2x RX 7900 XTX (ROCm, Linux), Swift 1.5 IQ3_XXS, 512-token greedy answers, median of 8:
decode 53 -> 70.1 tok/s (code prompt), 45 -> 55.9 tok/s (prose prompt); greedy output byte-identical with the
flag at 0 and 1; quiz 47/51 both ways. Not measured on NVIDIA.

layer split overlap: bound the GPU-side hand-off wait (STRATA_SPLIT_WAIT_MS)

Review on Niko1221#936 (justxiami): the reading stage's wait_flag_ge spun with no bound,
so a lost publish would leave that stream spinning.

- wait_handoff: the reading stage's graph now waits with a bound (default 30 s,
  STRATA_SPLIT_WAIT_MS, 0 = unbounded, GPU clock as in gpu_stamp). On timeout it
  sets a mapped error word and returns, so the graph completes; the host checks
  the word after the window and fails it with an error that names the setting,
  instead of using a stale hand-off.
- The host's per-layer wait (20 s, Niko1221#267) still normally ends a stalled window
  first; release_gpu_waits already raises the hand-in flag.
- Test hook STRATA_TEST_HANDOFF_DROP=N: the first stage's N-th window withholds
  its flag (a lost publish), to exercise both paths.

Only active with STRATA_SPLIT_OVERLAP=1; the default path is unchanged.

layer split overlap: the hand-off publish reads the test word only with STRATA_TEST_HANDOFF_DROP set

Without the hook the window graph passes no word, so the publish kernel does no
extra read of mapped memory per window.

layer split overlap: off beside --pipeline-windows (Niko1221#859)

--pipeline-windows runs its own overlapped decode loop with its own hand-offs, so
STRATA_SPLIT_OVERLAP now switches itself off when it is set (one log line says so).

Rebased onto main 1735d64: the early launch of the next stage keeps Niko1221#871's graph
variant (the next stage's refresh_ar() runs before its capture and prelaunch, and
not again in its run() when it was prelaunched), resets the plan error word like
run()'s launch, and the chain path checks that word after its sync as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157fjixQD2nRxvCvtXq66DD
@fioruccione

Copy link
Copy Markdown
Contributor

Another data point, from NVIDIA with two identical Ada cards: no change, and nothing broke.

Rig: 2x RTX 4060 Ti 16 GB (sm_89), PCIe 4.0 x8 each, layer split auto (CUDA0 layers 0-22, CUDA1 23-47), Threadripper PRO 3975WX (AVX2), 8x DDR4-2666. Unsloth UD-IQ4_XS, 262K context, --kv int8 --kv-resident 32768 --spec 4 --mtp, experts partly on the CPU (~3 CPU experts per layer-window). Linux, CUDA 12.6.

Build and method: this PR (f6196d6, on main e8ca9af), one binary for both arms, built the way setup builds (sm_89). Arms switched only by STRATA_SPLIT_OVERLAP=0|1, interleaved A B A B with a fresh server start each, STRATA_DECODE_TIMING=1 in both. benchmark.py from the RTX 5090 report at 4K and 32K (3 runs, greedy, 256 tokens), plus 4 Italian chat prompts (greedy, 256 tokens).

The engine log of the B arms confirms the hand-off ran overlapped (overlapped: STRATA_SPLIT_OVERLAP=1).

overlap off overlap on
ms per verify window 42.4 42.5
GPU-reach wait, ms per window 13.2 13.7
decode tok/s, 4K 56.2 56.0
decode tok/s, 32K 57.3 56.6
decode tok/s, Italian prompts 53.0 54.7

Every difference is within the run-to-run spread of the same arm, so the overlap makes no measurable difference here. That matches the 2x and 4x RTX 5080 reports: on these NVIDIA cards the host sync and graph launch between stages are already cheap compared with the window. No errors or stalls in any arm. I did not check byte-identical output: greedy runs already differ from run to run on this rig (GPU/CPU rounding), so a token compare would not tell the two arms apart.

Measured on my machine, with an AI coding assistant helping on the scripts and the write-up.

fragtion added a commit to fragtion/StrataPool that referenced this pull request Oct 12, 2026
…-support

Upstream now carries these of our picks, in its own versions: Niko1221#1237 (pinned stage buffers, now with fences and
the STRATA_STAGE_PIN switch), Niko1221#1332, Niko1221#1359, Niko1221#1372, Niko1221#1370, Niko1221#1251, Niko1221#1190 and Niko1221#1283.

CPU help for short prompts: Strata's own CPU prefill share (0.1.41: on by default below 1,024 tokens on one NVIDIA
GPU; -20..-34% prompt time at 512-1,000 tokens on an RTX 3060 in upstream's measurement) replaces architectds' CPU
assist, which measured +11% at ~560 tokens on our desktop.  prefill.cpp is upstream's, with the pool's hooks
(remote_next to the workers, a headless worker stage) and Niko1221#1368's quantize-once put back.  A pool PC runs its own
layers alone (no local stages), so the share's default applies on every PC.

Pipelined decode: upstream's N-stage loop (front stages + the last) with the pool's remote stage as the last
(pl_remote via stage_ver_p, pl_ns = 2 on a pool coordinator), and architectds' verified-path-first yield, the
lookup carried across windows and the pool head's penalty rows ported onto it.

Batch: upstream's batch_step (the adaptive tier in batch windows, per-stage timing, Niko1221#879 non-finite logits) with
Niko1221#1290's per-slot row width and the pool head's slot histories; the pool's batch groups keep their PoolLink stage.

Also: the conversation cache's eviction keeps both the pool-borrowed entry and pinned prefixes (evict_oldest);
the verifier's adaptive-tier gate (STRATA_ADAPT_OVERLAP) also guards the overlapped split's prelaunch (Niko1221#1120);
the server's read-once body takes upstream's size limit; replicas and the pool roles both start engines;
serve/pool.py keeps the engine's newer spawn tuple; the worker's parking reads available_host_bytes(); setup
tests that need a ready-made engine are skipped (StrataPool compiles its own).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157fjixQD2nRxvCvtXq66DD
fragtion added a commit to fragtion/StrataPool that referenced this pull request Oct 13, 2026
Upstream 0.1.44: Apple Silicon (Metal), Qwen3.6 family, NVFP4 experts, K-quant pool rows
(libstrata_kq), an image generation tab, prefill head alias, pin by need (Niko1221#1905) and
--adapt-async in the arena (Niko1221#1904, which we carried: upstream's copy kept, ours dropped).

Conflicts:
- Niko1221#1822 (gate/up MMQ reading experts in place, -0.7% read time) dropped: it patched
  llama.cpp's mmq.cuh and does not combine with NVFP4's tails and w4a4 activations.
  moe_mmq.{hpp,cu} and the group gather in prefill.cpp are upstream's.
- Niko1221#1368 kept: quantize_scatter for q8_1 activations, the per-row quantizer for NVFP4 w4a4.
- expert_source: load_experts_gguf takes the pool range and pin by need's PadMap; pin by
  need only for a whole pack (a pool range keeps the packed, biased layout); generate.cpp
  turns it off on a pool PC.
- verify.cpp: upstream's HostGraphLaunch inside our Niko1221#1120 prelaunch guard (an optional
  launcher: a prelaunched window has none).
- iq_kernels: our IQ1_S (19) beside NVFP4 (40) and the Qwen3.6 downs.
- generate.cpp: tail skip keeps from_coord and own layers beside NVFP4's default; batch
  graph limit is upstream's 64 for --batch-mtp and the pool worker's wide rows.
- server.py / web: health has image_generation and pool_role; images and pool routes; tabs
  chat, images, monitor, pool, about.
- the --prefill-help help text's "+~30% on" was a printf conversion (%%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157fjixQD2nRxvCvtXq66DD
bsvinay and others added 4 commits October 14, 2026 08:00
…t-in)

Each verify window of a layer split runs its stages strictly in turn: stage 1 finishes, the host syncs its stream,
then stages and launches stage 2's graph. Under HIP the sync and the graph launch both sit on the critical path of
every window.

With STRATA_SPLIT_OVERLAP=1 the hand-off carries a ready word (mapped, one per hand-off buffer):
- the writing stage's graph ends with handoff_publish: the payload with volatile stores (RDNA keeps plain stores to
  mapped memory in L2), a system fence per thread, and the last block raises the flag;
- the reading stage's graph starts with wait_flag_ge(flag, 1);
- Verifier::run captures the next stage's window on this thread, then stages and launches it on a helper thread
  (prelaunch) while this thread serves its own layers; it then serves the next stage's layers directly and syncs
  the earlier stage once the chain is done;
- release_gpu_waits also raises the hand-in flag; an early return after the prelaunch releases the next stage.

Batch windows and --split-device 0 keep the serial order. Off by default.

Measured on 2x RX 7900 XTX (ROCm, Linux), Swift 1.5 IQ3_XXS, 512-token greedy answers, median of 8:
decode 53 -> 70.1 tok/s (code prompt), 45 -> 55.9 tok/s (prose prompt); greedy output byte-identical with the
flag at 0 and 1; quiz 47/51 both ways. Not measured on NVIDIA.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…AIT_MS)

Review on Niko1221#936 (justxiami): the reading stage's wait_flag_ge spun with no bound,
so a lost publish would leave that stream spinning.

- wait_handoff: the reading stage's graph now waits with a bound (default 30 s,
  STRATA_SPLIT_WAIT_MS, 0 = unbounded, GPU clock as in gpu_stamp). On timeout it
  sets a mapped error word and returns, so the graph completes; the host checks
  the word after the window and fails it with an error that names the setting,
  instead of using a stale hand-off.
- The host's per-layer wait (20 s, Niko1221#267) still normally ends a stalled window
  first; release_gpu_waits already raises the hand-in flag.
- Test hook STRATA_TEST_HANDOFF_DROP=N: the first stage's N-th window withholds
  its flag (a lost publish), to exercise both paths.

Only active with STRATA_SPLIT_OVERLAP=1; the default path is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…th STRATA_TEST_HANDOFF_DROP set

Without the hook the window graph passes no word, so the publish kernel does no
extra read of mapped memory per window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--pipeline-windows runs its own overlapped decode loop with its own hand-offs, so
STRATA_SPLIT_OVERLAP now switches itself off when it is set (one log line says so).

Rebased onto main 1735d64: the early launch of the next stage keeps Niko1221#871's graph
variant (the next stage's refresh_ar() runs before its capture and prelaunch, and
not again in its run() when it was prelaunched), resets the plan error word like
run()'s launch, and the chain path checks that word after its sync as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bsvinay

bsvinay commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto 0.1.44.1 (296b59d). Only the first commit changed: upstream's
HostGraphLaunch in verify.cpp's launch block now sits inside the if (!pre)
branch (held in a std::optional; a prelaunched window never creates it and falls
back to cudaStreamQuery / no-op). The other three commits are unchanged.

What it changes and leaves alone: STRATA_SPLIT_OVERLAP=1 is still opt-in and the
default path is untouched. A prelaunched next stage uses a plain cudaGraphLaunch,
so STRATA_VERIFY_ASYNC_LAUNCH=1 (non-HIP, opt-in) applies to the first stage only.

Measured on 2x RX 7900 XTX (gfx1100, ROCm 7.15, Linux), Ryzen 9 9900X, Swift 1.5
IQ3_XXS, with --pcie-frac 0 --adapt-every 1; 3 alternating rounds, each the median
of 5 runs of a 512-token greedy answer:

  • code decode 61.1 -> 76.7 tok/s (+25%)
  • prose decode 50.2 -> 61.6 tok/s (+23%)
  • prefill 4K / 16K / 32K unchanged (about 1,445 / 2,220 / 2,790 tok/s, within 1%)
  • quiz set 47/51 with and without it.
    One card: the rebased build equals the previous one (decode 95.8 / 76.5 vs
    95.5 / 77.2, prefill within 0.2%, quiz 47/51 on both).

Not tested: CUDA (the HIP build only; the CUDA branch of the launch block was merged
by hand and has not been compiled), SYCL, NVIDIA hardware.

@shahrokhzargarpour shahrokhzargarpour mentioned this pull request Oct 14, 2026
Closed
@qiuzan2001

Copy link
Copy Markdown

The default path looks untouched (no env var, no hand-off flags). One build issue, from reading (no Mac here): Verifier::record_window in src/core/verify.cpp now calls wait_handoff (line 964) and handoff_publish (line 1757), defined only in src/kernels/cuda/verify_kernels.cu. The Mac engine compiles src/core/verify.cpp (metal/CMakeLists.txt:94) and resolves such kernel entry points through metal/src/engine/stubs.cpp, which stubs the neighbouring wait_flag_ge but has no wait_handoff / handoff_publish, so the Mac link should fail on these two symbols. Adding stubs (or regenerating with metal/tools/gen_stubs.py) would fix it.

@Niko1221

Copy link
Copy Markdown
Owner

Thanks, and thanks to the people who tested it on other cards. Ported with your authorship; one conflict was ours to resolve (0.1.44 moved the graph launch into a HostGraphLaunch object, so the early-launched window keeps no launcher). On 2x RTX A4000 the layer split with the overlap on is neutral (x1.001 over 5 pairs) and the answers are bit-identical (4/4 slots, exact settings); the default path is unchanged (identity gate). We have no RDNA3 two-card box, so the gain you measured on 2x 7900 XTX is not reproduced here; it stays opt-in and the docs name it for RDNA3.

fragtion added a commit to fragtion/StrataPool that referenced this pull request Oct 17, 2026
Upstream 0.1.45 took in ten of the PRs we carried: Niko1221#567 (prompt encoder), Niko1221#1164 (sibling
borrow), Niko1221#1166, Niko1221#1368, Niko1221#1367, Niko1221#1120 (split overlap), Niko1221#1912, Niko1221#1888, Niko1221#1949, Niko1221#1305.  Their
versions are kept and our copies dropped: verify.cpp's duplicate prelaunch/handoff block,
verify_kernels.hpp, the qsa prompt kernels, gr_upmix_parity, the tokenizer and its tests
(tools/test_strata_tokenizer_upstream.py is gone: upstream's test file is the one now),
the second quantize_scatter declaration.  README credits updated.

Pool parts kept on top:
- conversation cache: upstream renamed the borrow guard (pin -> guard); clear() resets it.
- prefill: the remote_next hand-off beside upstream's hand-off pieces/worker (a pool stage
  has no pieces: the whole chunk goes to the workers).
- generate.cpp: --pool-affinity measure over a node's own layers; the node's dense-range
  load beside STRATA_HC_REQ8; the carve on primary_device over the pool range; checkpoints
  keep pool_id (STRATA_CKPT_POOL/THREADS' parallel save is skipped on a pool PC); the
  workers' retain after eviction and after a resume; the borrow/restore of parked
  conversations on the workers with upstream's --conversation-cache-park-gain; Niko1221#652's
  emit count in upstream's merged windows.
- expert_source: our budget retry beside upstream's lend budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157fjixQD2nRxvCvtXq66DD
@bsvinay

bsvinay commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Thanks both. Ran the interleaved comparison Niko asked for in #936, on v0.1.45 (main, 477ac13), 2× RX 7900 XTX (gfx1100, ROCm 10.1), Swift 1.5 IQ3_XXS, layer split auto.

Method: 10 rounds, four arms, run order rotated every round so each arm is first/second/third/last equally often, 200 tokens, 3 runs per cell, median per round, then median across rounds.

arm code tok/s vs serial prose tok/s vs serial
S serial (no flags) 65.1 1.00 54.7 1.00
O STRATA_SPLIT_OVERLAP=1 80.9 ×1.24 67.4 ×1.23
P --pipeline-windows 2 105.8 ×1.63 86.1 ×1.57
M P + STRATA_MTP_STAGE0=1 91.8 ×1.41 69.2 ×1.27
  • Overlap reproduces my earlier gain under interleaving (×1.24 / ×1.23, range 80.6–81.0 over 10 rounds).
  • Pipelined windows give a larger gain than overlap on this pair: P/O is ×1.31 (code) and ×1.28 (prose), and P is ahead of O in all 10 rounds.
  • STRATA_MTP_STAGE0=1, which the engine's note recommends, was slower than plain P here and noisier (prose 60.6–72.8).
  • P is noisier on prose than O (77.5–91.0), but its worst round still beats O's best.
  • Cost check: prefill with P vs serial is equal within noise (4K: 1528/1510 vs 1514/1515; 32K: 2834/2836 vs 2815/2796 tok/s), and the 51-question quiz scores 47/51 on both (two borderline trap answers swapped, so outputs are not identical).
  • For reference, the same model on one card (v0.1.45, same prompts, 8 runs, not interleaved with the above) does 102.9 / 81.5 tok/s, so on this model the pipelined split is about equal to a single card; the layer split mainly pays off for models that don't fit on one card.

So --pipeline-windows 2 covers this case and does better on this pair. I'm happy for this PR to be closed as covered. Suggest the MULTI_GPU docs name --pipeline-windows 2 for gfx1100 two-card splits (it is on by default only for gfx12), with these numbers.

Mac/Metal link failure (@qiuzan2001): a small separate PR is coming with stubs for wait_handoff and handoff_publish, may need help with testing this.

@bsvinay

bsvinay commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

@qiuzan2001 , can you please try #3002 for the stubs. Thanks

@Niko1221

Copy link
Copy Markdown
Owner

Thanks for the pull request.

Your change is already in main for 0.2.0-beta, with your credit, so there is nothing left for you to do here. We're closing it because we're resetting the tracker for 0.2.0-beta: there are more than 1,300 open issues and pull requests, and we're closing everything that isn't being worked on right now.

If part of your work did not make it in and you want it considered, open a new pull request on top of main that follows CONTRIBUTING.md, or comment /reopen after fixing the title area prefix and the template.

@Niko1221 Niko1221 closed this Oct 19, 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.

4 participants