Repository navigation
serve --batch: a layer split reads a prompt in STRATA_SPLIT_PIECE-token runs, decided at every boundary - #1504
sanastasiou wants to merge 1 commit into
Conversation
…en runs, decided at every boundary A layer split pipelines its stages across one sp.run's chunks, so a prompt read beside a decoding slot is cut into single chunks (a decode window between each) and loses that overlap. Until now the read kept that shape to its end, even after the slots finished their short turns a few seconds in: a long read ran at about half speed. read_part now asks at every chunk boundary how much to read next (prompt_read_piece): one chunk while a slot decodes, STRATA_SPLIT_PIECE tokens (rounded down to whole chunks) once none does, the rest of the prompt when the variable is unset. A piece bounds how long a shorter request's BYIELD waits. STRATA_SPLIT_PIECE defaults to 0 = the old behaviour (whole run when idle). Same change in sycl/ (not compiled here, no oneAPI). read_piece_test: 15 checks.
|
Tested on a 4-stage split (4× RTX 5080 16 GB (PCIe Gen3), Qwen3.8-Flash-Next IQ3_S, 262K, int8 KV,
The follow-up alone answers in 0.15 s, so on this setup the read still doesn't give way between pieces. My guess is the case your PR doesn't target: here no slot is decoding when the read starts, so the read runs as one |
|
Thanks, that table answers it. With Our numbers are from Your case (an idle read and a waiting new admission, with pipelined groups) needs the pipelined path to read in pieces too. That isn't a one-line change, because the groups hand rows between stages, and I haven't measured it, so I'd keep it out of this PR. If you want to try it, the first thing to log would be when the server sends BYIELD for the follow-up and when |
|
Follow-up on v0.1.41, same rig and same scenario (4x RTX 5080 PCIe Gen3, IQ3_S,
As you explained, the pipelined read is still one |
|
Thanks for re-checking on 0.1.41; that matches the code. With Splitting the pipelined read into pieces isn't something I plan to do in this PR. The groups hand rows between stages inside one |
|
One more measurement would settle whether the PR is useful on your rig. Both cases need 1. A long read that starts next to a decoding slot (the main gain on our side, +30-40 %)
Without the PR, B stays in single chunks for the whole read even after A has finished. With it, B goes back to multi-chunk pipelined runs once A is done. With 4 stages instead of our 2, the difference should be larger. 2. Your follow-up scenario, with groups 1 If either case shows no gain on your rig, that's worth knowing too. |
|
Both measurements, as asked, on 4x RTX 5080 (PCIe Gen3), IQ3_S, 1. Fresh ~100K read starting ~1 s after A's first token (
On our rig, then:
2. Our follow-up scenario with groups 1 (
So the follow-up waits 2.5x less (10 s down to 4 s), and the long read pays 7 s for it (+65%). The engine says "the prompt was read in 2 parts, the slots decoding 550 ms between them". I expected more parts with 16384-token pieces on a 45K prompt. For us, the trade-off loses: production runs |
|
Thanks — that's the measurement we needed, and it doesn't support the PR. Case 1 gives ~7 % instead of the +30-40 % we saw, the gain is there with the variable unset, and the read never returns to longer runs, so our explanation of where the gain comes from doesn't hold on your rig. Case 2 trades a 2.5x shorter follow-up wait for a 65 % longer read, and the "2 parts" on a 45K prompt with 16K pieces says the piece logic doesn't behave as described either. On 0.1.41 we've also measured that |
… (opt-in, STRATA_PIPED_READ_PIECE) With a layer split and --batch-groups above 1 a prompt is read in one sp.run, so every decoding slot stops until it has been read and a BYIELD is never taken (Niko1221#1906, Niko1221#1504). STRATA_PIPED_READ_PIECE=N reads it in pieces of N tokens (whole chunks, 1 to 32). After each piece the slots decode for STRATA_BATCH_DECODE_SHARE times the piece's time (capped and floored by STRATA_PIPED_DECODE_MAX_MS and _MIN_MS) and the pipeline is drained; a waiting shorter request can take over there with the existing BYIELD code. A read right after another such read gives the slots one decode budget first. Unset, the read is one run as before. 2x RTX 5090, four streams decoding while cold 64K and 32K prompts join 6 s apart, median of 5 interleaved runs: the longest stop of a stream went from 12.2 to 4.2 s with PIECE=32768 and DECODE_MAX_MS=400, the prompts' TTFT from 6.9/6.1 to 8.1/8.5 s. A short question sent 1 s into a cold 45K read: 5.4 to 3.6 s. docs/BATCHING.md has the tables and tools/piped_read_join_bench.py reproduces them.
… (opt-in, STRATA_PIPED_READ_PIECE) With a layer split and --batch-groups above 1 a prompt is read in one sp.run, so every decoding slot stops until it has been read and a BYIELD is never taken (Niko1221#1906, Niko1221#1504). STRATA_PIPED_READ_PIECE=N reads it in pieces of N tokens (whole chunks, 1 to 32). After each piece the slots decode for STRATA_BATCH_DECODE_SHARE times the piece's time (capped and floored by STRATA_PIPED_DECODE_MAX_MS and _MIN_MS) and the pipeline is drained; a waiting shorter request can take over there with the existing BYIELD code. A read right after another such read gives the slots one decode budget first. Unset, the read is one run as before. 2x RTX 5090, four streams decoding while cold 64K and 32K prompts join 6 s apart, median of 5 interleaved runs: the longest stop of a stream went from 12.2 to 4.2 s with PIECE=32768 and DECODE_MAX_MS=400, the prompts' TTFT from 6.9/6.1 to 8.1/8.5 s. A short question sent 1 s into a cold 45K read: 5.4 to 3.6 s. docs/BATCHING.md has the tables and tools/piped_read_join_bench.py reproduces them.
What
On a layer split under
serve --batch, a prompt read that starts while a batch slot is decoding is cut into single chunks with a decode window between each, because the stage pipeline only overlaps within onesp.run. The read kept that shape to its last chunk, even when the slots finished their short turns a few seconds in. Long reads in a busy server ran at about half the speed of the same read on an idle server.read_partnow decides at every chunk boundary how much to read next (prompt_read_piece/prompt_read_run_endininclude/strata/program/read_piece.hpp):STRATA_SPLIT_PIECEtokens, rounded down to whole chunks (at least one);STRATA_SPLIT_PIECEunset or 0: the whole remaining prompt in one run, as before.The piece size bounds how long a shorter request's
BYIELDwaits behind a long read.Scope: this applies with
--batch-groups 1(the default). With--batch-groups> 1 on a split (piped),read_partstill reads the whole prompt in onesp.run; this PR does not change that path.Default behaviour does not change. With
STRATA_SPLIT_PIECEunset, an idle split reads the whole prompt in one run, the same as today. The only thing that differs without the variable is that a read which started beside a decoding slot goes back to one run once no slot decodes, where today it stayed on single chunks.Measured
2× RTX 3090 layer-split pair (PCIe 3.0, no NVLink), Qwen3.8-Flash-Next IQ3_S,
--batch 2 --prefill auto(8K chunks),STRATA_SPLIT_PIECE=65536. Load: 4 parallel Claude Code compaction benchmarks, each growing to ~400K tokens, through one router onto two such pairs.Over the whole 4×400K run: time to first byte p95 123 s → 76–80 s, mean wall time per conversation 1,260 s → 1,178 s. Recall 5/5 in all four conversations.
Tests
read_piece_test(new, header-only, 15 checks): piece rules, rounding to whole chunks, run end clamped at the prompt end.generate.cppcompiles; ran in production on both pairs.sycl/src/program/generate.cpp: same edit, not compiled (no oneAPI here).