Skip to content

Layer split: pipelined verify windows for one conversation (--pipeline-windows, opt-in) - #859

Closed
Hardin22 wants to merge 2 commits into
Niko1221:mainfrom
Hardin22:pr/pipeline-windows
Closed

Hardin22 wants to merge 2 commits into
Niko1221:mainfrom
Hardin22:pr/pipeline-windows

Conversation

@Hardin22

@Hardin22 Hardin22 commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Refs #642. These are the pipelined windows from the fork, ported onto main and opt-in (--pipeline-windows 2; the default 0 changes nothing).

With a layer split, one conversation's decode is serial: stage 0 runs its layers and hands off, then waits while the last card runs its layers, the head and the draft. The batch pipeline from #559 overlaps different conversations, so a single conversation gets nothing from it.

--pipeline-windows 2 starts window K+1 on stage 0 while stage 1 still verifies window K.

  • The guess: K+1 is guessed by running the MTP chain through K's drafts (teacher-forced), launched as soon as K's verdict is known.
  • Guess right: half of K+1 is already done.
  • Guess wrong: stage 0 restores its GDN state from a snapshot, replays K's commit with the real count, and K+1 is rebuilt from the real tokens.
  • When it speculates: only when the chain's estimate says K+1 is likely to be kept (STRATA_PIPELINE_THETA).

Two commits:

  1. MtpDrafter: a teacher-forced chain as one asynchronous launch. Nothing calls it on its own, and draft() is unchanged for the serial loop.

  2. The pipelined loop: a non-blocking form of the Verifier's run()/commit() on the batch_launch/batch_poll pattern, two verifiers per stage, the adaptive tier fenced against the windows in flight, and the VRAM for the second verifier and the snapshots. It turns itself off, with one log line, for anything it does not handle:

    • --batch;
    • --peer-device;
    • helper caches / --remote-expert-opt;
    • three or more stages, or a split on one GPU;
    • no drafter.

    Requests with penalty_last_n or coupled draft sampling decode serially.

Measured

Swift 1.5 IQ3_XXS at 160K (q4_0 KV, stock draft layer). RTX 4060 Ti (layers 0-19) + RTX 5080 (20-47), i9-14900KF, 32 GB of RAM, with the resident RAM mode on the split from #848. Greedy, 500 tokens, two interleaved serial/pipelined pairs of three rounds each, decode tok/s:

serial --pipeline-windows 2
Python code 90.4 103.1 +14%
C code 76.6 81.6 +7%
English prose 63.0 71.1 +13%
Italian prose 41.6 46.3 +11%
copy-heavy edit (a 5 KB file back with one rename) 90.5 117.9 +30%
mean 72.4 84.0 +16%

It pays when the windows are GPU-bound. On the same PC without #848 (experts read through the OS file cache, --mmap-experts), the file reads dominate and it was not measurably faster.

Correctness

  • With STRATA_IQ_MT_MIN=1 --pcie-frac 0 --adapt-every 0, the greedy text is identical bit for bit to the serial loop's, as long as both have the same expert caches: 4/4 prompts, 500 tokens. The serial run was given the pipeline's reserve through --vram-reserve-mib.
  • The same text also comes out with each of these:
    • STRATA_PIPELINE_THETA=2 (no speculative window at all);
    • STRATA_PIPELINE_FORCE_MISS=1 (every speculative window wrong and rolled back), with and without STRATA_PIPELINE_DOOM_SKIP=0;
    • STRATA_PIPELINE_PRESTAGE=0;
    • STRATA_PIPELINE_SNAP_OVERLAP=0.
  • Against a run without the flag, the pipeline's 253 MiB (first card) + 160 MiB (last card) move a few experts from a card to the CPU, which rounds them differently, so a near-tie can flip. All the flips I looked at are word-level alternatives ("components" vs "year, month, day").
  • With the adaptive tier on, no degenerate answer and no stall in either run:
    • 22 varied requests at temperature 0.6 (thinking on/off, five languages, code);
    • 11 more with STRATA_PIPELINE_FORCE_MISS=2.

Not tested

  • HIP and SYCL: not built here.
    • On HIP, the only new CUDA-specific call (the drafter's stream priority) is behind #if !defined(STRATA_USE_HIP), and the new forcing kernel is plain code.
    • SYCL's migrated verify/mtp would need the new methods before it can use the flag. It doesn't build them today.
  • The zero-doorbell graph (every expert resident in VRAM): not exercised on this PC. There, service() only raises the stage flag.

Happy to split it differently or change the defaults if you prefer.

For the pipelined verify windows of a layer split (next commit). Nothing calls
it yet, and nothing changes unless set_force_capture is set before load.

- chain_launch: the catch-up round and its steps launched back to back with no
  host wait, an event after each output (chain_outputs_ready reads them as they
  land, chain_poll once the chain is done). Its first steps can take given
  tokens as their input instead of their own picks (teacher forcing: the drafts
  of a window already in flight), through a one-thread kernel at the end of the
  round and step graphs that does nothing while its mapped token is -1; the
  outputs stay the chain's own picks.
- prepare_chain / prepare_prefill: capture every graph and size the prefill's
  input records up front, so neither a later chain nor prefill(sync = false)
  waits for the device.
- set_source_R: the rows a round reads, copied into the bound buffer first (the
  pipelined loop has one stage-1 verifier per window parity).
- with forcing on, the drafter's stream gets the highest priority (CUDA;
  STRATA_MTP_PRIORITY=0: the default).

draft() resets the forced tokens and copies the source rows the same way, so
the serial loop's drafts are unchanged.
With a split the two cards take turns on every verify window: stage 0 runs its
layers and hands off, then waits while stage 1 runs the rest, the head and the
draft. The batch pipeline (--batch-groups) overlaps different conversations;
one conversation's decode stayed serial.

--pipeline-windows 2 runs window K+1 on stage 0 while stage 1 verifies window
K. The guess for K+1 comes from the MTP chain, launched as soon as K's verdict
is known and teacher-forced through K+1's drafts: if K is accepted as guessed,
K+1 is already half done; if not, stage 0 restores its GDN state from a
snapshot, replays K's commit with the real count, and K+1 is rebuilt from the
real tokens. --pipeline-windows 1 overlaps only the prompt's short reads
through the windows (--short-read). Default 0: nothing changes.

What it adds:
- Verifier: a non-blocking form of run()/commit() (capture_all, pl_launch,
  prestage, service, done, pl_finish, pl_commit_async), the batch pipeline's
  batch_launch/batch_poll pattern for one sequence. The window, its staging,
  the PLE gather at layer 0, the zero-doorbell graph's flag, the Niko1221#267 release
  on a timeout and the commit graph are run()'s. Two verifiers per stage (one
  per window parity) share one stream of their card (set_stream) and each
  pair has its own hand-off.
- set_always_publish: the pipelined verifiers copy the token rows to the host
  at every layer, because the adaptive tier now marks experts evicted on the
  host while windows are in flight, before the device residency table follows.
- MtpDrafter: chain_launch (the round and its steps back to back, an event
  after each output, the first steps fed the in-flight window's drafts by a
  one-thread forcing kernel), prefill without a host sync, 8 rows with 2, the
  drafter's stream at the highest priority (CUDA only).
- the adaptive tier beside the pipeline: evictions are marked first, then the
  refill copies wait (on the tier's thread) for every window in flight; the
  RAM side of a swap (exchange, release, unpin) waits for the windows that may
  still pull the incoming experts over PCIe.
- an agreement gate: a speculative window is launched only when the chain's
  estimate says it is likely kept (STRATA_PIPELINE_THETA, default 0.2); a
  window known to be wrong gets empty plans for its remaining layers; a lookup
  window's continuation becomes the speculative window behind it.
- VRAM: 160 MiB more kept out of each card's cache for the second verifier,
  and the two GDN snapshots on the first card.
- off, with one log line saying why, for anything it does not support: --batch
  slots, --peer-device, the helper caches, three or more stages, a split on
  one GPU, no drafter. Requests with repetition penalties or coupled draft
  sampling decode serially. Nothing is in flight outside the two pipelined
  loops, so checkpoints, parking and the serial paths are unchanged.

Measured: Swift 1.5 IQ3_XXS at 160K (q4_0 KV, the stock draft layer), RTX 4060
Ti (layers 0-19) + RTX 5080 (20-47), i9-14900KF, 32 GB of RAM, the resident
RAM mode on the split (Niko1221#848); greedy, 500 tokens, two interleaved pairs of
three rounds, decode tok/s, serial -> pipelined: Python code 90.4 -> 103.1,
C code 76.6 -> 81.6, English prose 63.0 -> 71.1, Italian prose 41.6 -> 46.3,
a copy-heavy edit (a 5 KB file back with a rename) 90.5 -> 117.9; mean
72.4 -> 84.0 (+16%). With the experts read through the OS file cache
(--mmap-experts on that PC) the file reads dominate and it measured no faster.

Correctness: with STRATA_IQ_MT_MIN=1 --pcie-frac 0 --adapt-every 0 the
greedy text is identical to the serial loop's when both have the same expert
caches (4/4 prompts, 500 tokens; the serial run given the pipeline's reserve
through --vram-reserve-mib). The same text with STRATA_PIPELINE_THETA=2 (no
speculative window), STRATA_PIPELINE_FORCE_MISS=1 (every speculative window
wrong, rolled back) with and without DOOM_SKIP, PRESTAGE=0 and
SNAP_OVERLAP=0. Against a run without the flag a few experts move from a card
to the CPU (the pipeline's VRAM), which rounds them differently, so a near-tie
can flip. With the adaptive tier on: 22 varied requests at temperature 0.6
(thinking on and off, five languages, code), and 11 more with
STRATA_PIPELINE_FORCE_MISS=2, gave no degenerate answer, no stall.

The watchdog prints the pipelined loop's state when it fires;
STRATA_PIPELINE_TRACE=<file> writes a timeline.
@justxiami

Copy link
Copy Markdown

A data point from the shape this PR cannot reach: 4 stages on 4x V100-16GB (sm_70).

--pipeline-windows gates on two stages - generate.cpp:5159 (n_stages == 2 && !split_same && stages.size() == 1) and the off message it needs a layer split into exactly two stages. Our rig is --layer-split 12,24,36 --split-device 1,2,3 = 4 stages (0-11 CUDA0, 12-23 CUDA1, 24-35 CUDA2, 36-47 CUDA3, one hand-off per window), so the flag prints the off line and does nothing. Two things we measured that may be useful if you ever generalize it:

  1. What the stage overlap is worth here. With layer split: overlap the stages' hand-off (STRATA_SPLIT_OVERLAP=1, op… #936 (STRATA_SPLIT_OVERLAP=1) on the same binary and the same four-stage split, we measured -0.78 ms per window mean, -2.9%, across 5 paired prompts and +1.63% decode tok/s across 6 paired ladder reps (details in our layer split: overlap the stages' hand-off (STRATA_SPLIT_OVERLAP=1, op… #936 reply). That is about 0.24 ms per hand-off, and a 4-stage window has 3 hand-offs. So the bubble your pipelining removes is real on this rig but small; the speculation half (guess window K+1, roll back on a wrong guess) is untested here at 3+ stages.
  2. VRAM is the expensive part on a 16 GB card. Our per-stage expert caches are 10.87 - 11.06 GiB and CUDA3 already fits 5973 of its 6144 profiled pairs. The 160 MiB per card that the second verifier keeps out of the cache, plus the GDN snapshots, is not free here; layer split: overlap the stages' hand-off (STRATA_SPLIT_OVERLAP=1, op… #936 costs 64 bytes of pinned host memory per hand-off and does not touch the cache budget.

We applied the PR cleanly on v0.1.39 as a dry run (10 files, +1601 -33, no rejects) and it coexists with the #845 patch (offset 92 lines), so a 3+ stage version would be testable for us. Say the word and we will run it.

@Hardin22

Hardin22 commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, both points are useful.

The two-stage limit is deliberate for now. With two stages, the speculation only has to guess one window ahead on one card, and rolling back means restoring one card's GDN state. With three or more stages, each extra stage adds another window in flight, another snapshot, and a longer rollback chain. That's where it gets subtle, so I wanted the two-stage case solid first.

Your numbers also put a ceiling on it for your rig: 0.24 ms per hand-off and caches already holding 5973 of 6144 pairs on CUDA3. If the bubble is that small and VRAM is that tight, 160 MiB per card plus the snapshots might cost more than they return. On a 4060 Ti + 5080 pair the stage-0 wait was ~10 ms of a ~30 ms window, so there was much more to win. I'll keep your offer in mind. If I generalize it, I'll make the per-card VRAM cost configurable and ping you for the 4x V100 run.

@aswin-dot-R

Copy link
Copy Markdown

Tested on 2x gfx1201 (RDNA4, HIP/Linux): --pipeline-windows 2 is a clear win on decode and prompt

A two-stage data point, since the 4-stage V100 rig above cannot take this path.

Setup: AI PRO R9700 32 GB (stage 0, layers 0-29) + RX 9070 XT 16 GB (stage 1, layers 30-47), Qwen3.8-Flash-Next IQ3_S, --max-context 262144 --kv int8 --spec 4, MTP draft layer, engine = v0.1.39 + this PR. Linux 6.17, ROCm 7.2.0. R9700 on PCIe 4.0 x4, 9070 XT on 3.0 x4 (OCuLink). Both runs with STRATA_SH_STREAM=0 (#816: on this machine the fork costs ~22% on a split, so the baseline is the fast serial path, not the regressed one). Fresh server per run, engine timings:

v0.1.39 + --pipeline-windows 2
decode, 1200 tok (2 runs) 79.6 / 75.8 81.0 / 84.5 +7%
decode, 1500 tok 80.8 93.0 +15%
prompt, 40,920 tok 1,192 tok/s 1,316 tok/s +10%
prompt, 232,080 tok 1,158 tok/s 1,333 tok/s +15%
needle at 232K (record 7777) correct correct
R9700 power while busy 198 W 235 W

The power line is the clearest sign it does what it says: stage 0 stops idling while stage 1 verifies. On the serial split both cards sat ~100 W under what the same R9700 draws on a dense 27B.

Exactness: with the flag the greedy text (temperature 0, seed fixed) diverges from the serial loop at the third word ("respond" vs "answer") - consistent with your note about the 309 MiB kept out of CUDA0's cache moving a few experts to the CPU. With STRATA_IQ_MT_MIN=1 --pcie-frac 0 --adapt-every 0 on both sides the first divergence moves to char 163 of ~4.4K, but I could not make the caches equal: CUDA0 gets 13,582 vs 13,285 slots and the split's CUDA1 cache is auto-sized (4,132 vs 4,145) with no flag to pin it, so I can't confirm or refute bit-exactness on HIP. Answers stayed correct in everything I ran.

Running it as my daily server now; happy to test follow-ups on this pair.

Niko1221 added a commit that referenced this pull request Oct 6, 2026
…RATA_PIPELINE_DEBUG=1, exclusive with --adapt-async, merged with batch MTP and the async tier

Co-authored-by: Francesco Albano <119073350+Hardin22@users.noreply.github.com>

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Niko1221

Niko1221 commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Thanks, taken into 0.1.40 as opt-in (--pipeline-windows, default 0) with your commits. Changes: every STRATA_PIPELINE_* debug hook is read only under STRATA_PIPELINE_DEBUG=1, it excludes --adapt-async, and it is merged with batch MTP and the resident exchange work. It needs two GPUs and #848 to show its gain. We have one GPU, so the release notes ask two-GPU testers for 10 or more interleaved pairs, with and without #848.

@Niko1221 Niko1221 closed this Oct 6, 2026
bsvinay added a commit to bsvinay/Strata that referenced this pull request Oct 6, 2026
--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>
Niko1221 added a commit that referenced this pull request Oct 6, 2026
…RATA_PIPELINE_DEBUG=1, exclusive with --adapt-async, merged with batch MTP and the async tier

Co-authored-by: Francesco Albano <119073350+Hardin22@users.noreply.github.com>
@Hardin22

Hardin22 commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

Interleaved pairs on 0.1.40 (1735d64), as asked in the release notes.

Setup. RTX 4060 Ti 16 GB (stage 0, layers 0-19) + RTX 5080 16 GB (stage 1, layers 20-47), i9-14900KF, 32 GB DDR5, Windows 11. Swift Qwen3.8-Flash-Next IQ3_XXS, 160K context, q4_0 KV, --spec 4 with the MTP draft layer, --trim-stage-weights, --pipeline-windows 2.

Method. One server per arm, started with STRATA_PIPELINE_DEBUG=1 STRATA_PIPELINE_SWITCH=<file>, so both sides of a pair share the same expert caches (sized for the pipeline: 253 MiB of CUDA0 kept out). For each pair the script writes pw=0 or pw=2 to the file and sends the same greedy request (5 prompts in rotation, 400 tokens, thinking off). The order alternates pair by pair, after one warm-up request. 12 pairs per arm, engine-reported decode tok/s.

With #848 (--resident-experts, the whole complement in RAM, 0 file reads in the log):

pair prompt serial pipelined ratio
0 0 121.4 122.8 1.012
1 1 68.3 54.2 0.794
2 2 90.3 112.8 1.249
3 3 97.4 79.0 0.811
4 4 99.7 135.0 1.354
5 0 114.4 125.7 1.099
6 1 49.3 65.7 1.333
7 2 103.5 88.5 0.855
8 3 74.1 100.2 1.352
9 4 120.9 118.4 0.979
10 0 100.7 129.7 1.288
11 1 64.8 49.6 0.765

Mean 92.1 serial vs 98.5 pipelined: ratio x1.074 mean, x1.055 median, pipelined faster in 7 of 12 pairs.

The spread within a pair is large because the adaptive tier keeps moving experts between requests on one server, so the cache a request sees depends on what ran before it.

Without #848 (--mmap-experts): not usable on this box. Pair ratios ranged from x0.45 to x4.1 as the OS file cache warmed and evicted, and the means came out equal (72.2 / 72.2). The pipeline needs the experts off the SSD path to show anything here.

Pipeline and async tier together. 0.1.40 excludes --adapt-async when --pipeline-windows is set. #1122 (formerly #905, closed by the force-push) lets them run together. With a fresh server per run on the same five prompts (two runs of 10 requests each): --pipeline-windows 2 alone 93.5 / 94.1 tok/s, --adapt-async 1 alone 97.4 / 102.1, both 110.2 / 111.3. Its stress run (12 long requests, FORCE_MISS=2 THETA=0, an nvidia-smi query after each, as in the stall report on #905) was clean. Numbers and details are in #1122.

@andrej-reimer

Copy link
Copy Markdown

2x RTX 3090 (Linux, 250 W caps): 10+ interleaved pairs with and without #848, plus 5 pairs with the whole arena in RAM. --pipeline-windows 2 gained +20% (resident), +7% (mmap) and +14% (whole arena) on decode; prompts unchanged. Details: #1280

0FL01 pushed a commit to 0FL01/Strata that referenced this pull request Oct 7, 2026
--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>
@Niko1221 Niko1221 mentioned this pull request Oct 7, 2026
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
sskver pushed a commit to sskver/Strata that referenced this pull request Oct 7, 2026
…ehind STRATA_PIPELINE_DEBUG=1, exclusive with --adapt-async, merged with batch MTP and the async tier

Co-authored-by: Francesco Albano <119073350+Hardin22@users.noreply.github.com>
bsvinay added a commit to bsvinay/Strata that referenced this pull request Oct 9, 2026
--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>
Niko1221 pushed a commit that referenced this pull request Oct 17, 2026
--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 #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.
DmitriyW9 pushed a commit to DmitriyW9/Strata that referenced this pull request Oct 19, 2026
--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>
DmitriyW9 pushed a commit to DmitriyW9/Strata that referenced this pull request Oct 19, 2026
@noahark

noahark commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

A three-stage sm_70 data point, since the V100 numbers in this thread are either four-stage (the gate refused it) or two-stage.

Setup

  • Cards: 2x Tesla PG500-216 32 GB (72 SM) + 1x Tesla V100-PCIE-32GB (80 SM). All three sm_70, PCIe 3.0 x16 each, no NVLink.
  • Host: 2x Xeon E5-2696 v3, 128 GB DDR3L-1600, Windows.
  • Model: Qwen3.8-Flash-Next UD-IQ4_XS, --max-context 262144, --kv int8 --kv-resident 32768, --spec 4 with the MTP draft layer, STRATA_STAGE_TRIM=1, STRATA_SM70_TABLE=1.
  • Split: --layer-split 16,32 = 16/16/16, three stages, 2 hand-offs per window.
  • Engine: 0.1.48 for the table below; a 0.1.50 arm afterwards (see the end).
  • Residency: 8192/8192 profiled pairs on CUDA1 and CUDA2, 24576 in total, 100% of the experts resident. So the 234 MiB the second verifier keeps out of CUDA0's cache is free here — the opposite of the 16 GB situation @justxiami describes above.

Method

One full engine restart per arm; 3 reps x 3 legs per arm; 256 generated tokens; temperature 0; a distinct nonce in every prompt so no run reuses another's prefix; throughput from the engine's own timings (predicted_per_second, prompt_per_second). Legs: ~80-token prompt (decode only), ~4.6K, ~36.7K. Two arms of the identical config inside one session differ by at most 3.8% (4K decode) and 0.2% (32K decode), so I treat anything under ~4% as noise.

Results (tok/s)

arm slots windows short dec 4K dec 32K dec 4K prompt 32K prompt
A 2 (2 groups) off 105.6 94.0 90.1 1194.5 2673.8
C 1 off 106.0 95.5 89.8 1219.4 2707.6
D 1 2 134.5 107.1 100.8 1084.3 2544.6
F 2 (2 groups) 2 107.6 94.4 89.6 1082.3 2687.1
G 1 2 (repeat of D) 132.0 108.2 109.0 1081.1 2541.7
  • Windows 2 against no windows on one slot (C -> D, and again G): decode +13% at 4K, +25% on short prompts, +12..21% at 32K. This is the largest single win we found anywhere on this rig, and it is much larger than the 4060 Ti + 5080 pair reported above (x1.074); three stages have more bubble to remove than two.
  • Cost on this rig: prompt throughput. 4K prefill 1194-1219 -> 1081-1093 (-10%), 32K prefill 2673-2708 -> 2542-2545 (-6%). Consistent in all four window-on arms, so I do not read it as noise.
  • --batch really does switch it off. F is the two-slot config with --pipeline-windows 2 and it lands on A within noise on all three decode legs. Same finding as the earlier comment here, confirmed on a third card count.

--pipeline-windows 3 is clamped to 2

A paired A/B/A/B of --pipeline-windows 2 against --pipeline-windows 3 came out identical (all five legs within -0.1% to -3.3%, i.e. inside the noise floor). The reason is in the log: the engine receives the 3 —

engine started: ... --pipeline-windows 3 --layer-split 16,32
strata generate: --pipeline-windows 2: 234 MiB of CUDA0 kept out of the expert cache
                 (the second verifier, the GDN snapshots)

— and then runs 2 ("the second verifier", same 234 MiB in all four arms). --help describes --pipeline-windows N without an upper bound, so: is 2 the intended maximum on three or more stages? If so, a sentence in the help or in docs/MULTI_GPU.md would save the next person the 20 minutes it cost me. I am not claiming it is a bug — with the one-window lookahead you describe, a second extra window has nothing left to hide.

Engine 0.1.48 vs 0.1.50 on this config

Measured the same evening, two arms each: short 135.3 -> 134.5, 4K decode 108.7 -> 106.5, 32K decode 99.1 -> 101.0, 32K prefill 2518.9 -> 2554.6. All inside ±2%, and the pipeline line and the per-card caches are identical. So #3966's -19% does not reproduce on three sm_70 stages — the numbers above are not depressed by it.

Limits

  • Not the 10+ interleaved pairs the 0.1.40 release notes ask for. This is 4 loads x 3 reps per leg; the big deltas are far outside the noise floor, but I am not presenting it as acceptance testing.
  • No exactness check. I did not compare greedy output against the serial loop, so I cannot say anything about bit-equality on sm_70.
  • One rig, one quantization, one split, synthetic repeated-paragraph prompts.
  • Not tested: four stages, other quantizations, sampled decoding, and behaviour at 129K context (the longest leg here is 36.7K).
  • The 4K prefill cost above is the one number I would want re-measured on another rig before anyone plans around it.

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.

6 participants