Repository navigation
[RFC]: Wavefront Chunked Prefill Batching (WCPB) implementation #65
Description
Progress update — 2026-10-22
Baseline: main 4a350344027960a48384626aa646f1765fda6d4f.
- [FEAT] Implement Wavefront Chunked Prefill Batching(WCPB) #60 closed without merging; its prototype measurements do not establish WCPB support on main.
- [Architecture] Unify stage execution with loop-level scheduling and mixed batching #118 merged shared stage execution, split-prefill loop scheduling and compatible prefill/decode RECURRENT mixing. [Refactor] Move runner result application to Scheduler (M2/R1) #107 merged Scheduler-owned execution feedback.
- Multi-chunk WCPB remains unimplemented: [Architecture] Unify stage execution with loop-level scheduling and mixed batching #118 explicitly excludes simultaneous same-request chunk/depth wavefront scheduling.
- The remaining implementation must represent multiple ready/in-flight chunks per request, preserve per-depth causal KV readiness and contiguous completed prompt frontiers, and bound retained hidden states and cancellation lifetimes.
- Reuse the merged execution/metadata/scheduler contracts. Evaluate WCPB against current split-prefill and mixed modes, including continuous decode traffic, TTFT/ITL/TPOT, memory and scheduler costs.
- Existing claims are unchanged. The old baseline description and experimental numbers below are historical; no measurements were rerun for this refresh.
Motivation.
Motivation
Issue #2 defines the primary optimization target as faster Ouro inference on one GPU with loop-level batching and last-exited KV semantics. The current runtime already supports chunked prefill, refill scheduling, asynchronous execution, multiple attention backends, CUDA Graphs, prefix caching, and preemption. The scheduler walkthrough documents the existing separation between admission, stage selection, and batch construction, while the module-boundary RFC calls for explicit scheduling tasks and separate ownership of logical request state, KV resources, and device execution.
The current chunked prefill path leaves parallelism inside a request's recurrent-depth dimension unused:
- The scheduler selects a token range for each request.
- The runner embeds those tokens.
- The runner executes all recurrent depths for the selected range before returning.
- The engine advances the prompt frontier and requeues the request.
With LAST_EXITED, the KV written for c0@d0 is sufficient for c1@d0; c1@d0 does not depend on c0@d1. These dependencies allow a scheduler to overlap work across chunks and depths when the required state is ready.
This is useful when active prompt work is sparse or uneven. A conventional chunk batch may contain only a few rows even though several depth/chunk tasks are ready. WCPB can use those tasks to fill the same recurrent invocation. Smaller prompt tasks also produce more scheduler boundaries, allowing refill policies to admit new prompt work or schedule decode work earlier.
Benefits and Chunk-Size Trade-offs
WCPB is motivated by a real trade-off in ordinary chunked prefill. The chunk size controls both execution occupancy and scheduling granularity:
| Chunk size | Ordinary chunked prefill | Scheduling consequence |
|---|---|---|
| Large | More prompt rows are selected per batch and recurrent kernels are easier to fill | Fewer scheduler boundaries; a long prefill chunk can delay newly admitted prompt or decode work |
| Small | More frequent boundaries and smaller units of prompt work | Each ordinary recurrent invocation has fewer rows, so launch, metadata, and scheduling overhead can dominate and leave the GPU underfilled |
The small-chunk benefit for decode is a scheduling property rather than a claim that every small-chunk workload has lower decode latency. A smaller chunk gives the policy more opportunities to select a decode batch between prompt batches, but the actual ITL/TPOT improvement depends on stage policy, active decode work, and device execution overlap. The static experiments in this RFC did not include a concurrent decode stream, so they measure the occupancy/throughput side of this trade-off rather than directly qualifying decode latency.
What the baseline measurements show
The controlled A100 experiments make the ordinary-chunk trade-off visible. For four 8064-token requests with max_num_batched_tokens=1024, baseline end-to-end throughput fell as the chunk size became smaller:
| Chunk size | Baseline end-to-end tok/s | Baseline prefill rows/batch |
|---|---|---|
| 128 | 443.55 | 512.00 |
| 64 | 417.87 | 256.00 |
| 32 | 367.98 | 128.00 |
| 16 | 275.82 | 64.00 |
The same pattern appears in the mixed workloads. For the 32-request workload, baseline throughput changed from 662.25 tok/s at chunk 128 to 337.51 tok/s at chunk 16, while baseline prefill rows per batch changed from 480.00 to 60.00. For the 128-request workload, baseline throughput changed from 758.72 tok/s to 471.95 tok/s, and rows per batch changed from 451.39 to 97.57. These are paired measurements within each workload; they are not a claim that one chunk size is universally optimal.
Large chunks therefore remain useful when prompt throughput is the primary objective and enough prompt rows are available to fill the recurrent kernels. Their cost is reduced scheduling flexibility: a scheduler sees fewer points at which it can admit work or give decode a turn. Small chunks preserve those boundaries, but ordinary chunking cannot use the recurrent-depth dimension to compensate for the smaller per-chunk batch.
What WCPB adds
WCPB keeps the small chunk as the scheduling unit while allowing ready tasks from different chunks and recurrent depths to share the recurrent-core batch. This directly addresses the underfill mechanism in ordinary small-chunk prefill:
- It does not require increasing
prefill_chunk_sizeto obtain more tokens in a recurrent invocation. - It can combine
c0@d1withc1@d0and later combinations when their hidden-state and depth-specific KV dependencies are ready. - It retains more scheduler boundaries than a large ordinary chunk, while using independent ready tasks to improve the effective batch size.
- It is most useful when only a few requests are active or prompt lengths are uneven; in those cases, request-level batching alone may not provide enough rows.
The measurements support this mechanism. In the four-request 8064-token workload, WCPB raised rows per batch from 64.00 to 254.49 at chunk 16 and improved measured throughput from 275.82 to 412.01 tok/s (+49.38%). In the 32-request mixed workload, it raised rows per batch from 60.00 to 238.60 and improved throughput from 337.51 to 628.45 tok/s (+86.20%). In the 128-request mixed workload, it raised rows per batch from 97.57 to 361.96 and improved throughput from 471.95 to 740.29 tok/s (+56.86%). These chunk-16 results used one warmup and one measured run per variant, so they are directional measurements rather than three-run medians.
WCPB does not guarantee that a small chunk reaches the same absolute throughput as a large chunk. In the four-request workload, WCPB measured 465.31 tok/s at chunk 128 and 412.01 tok/s at chunk 16. Task metadata, hidden-state retention, dependency checks, and additional scheduling work remain costs. The intended benefit is to recover part of the occupancy lost by choosing a small chunk, while retaining its scheduling granularity. The appropriate acceptance target is therefore an end-to-end trade-off: higher effective rows and useful throughput at a chosen chunk size, together with bounded TTFT, decode ITL/TPOT, memory, and scheduler overhead.
For the SHARED KV layout, c1@d0 depends on the final-depth KV of the preceding prompt positions. The LAST_EXITED WCPB schedule therefore cannot be directly applied without changing the dependency graph. The first implementation supports only LAST_EXITED and rejects SHARED.
Proposed Design
Scheduler-visible phases and execution boundaries
The implementation keeps the existing prompt-prefill path and adds staged phases for mixed scheduling and WCPB.
With wavefront_chunked_prefill=False, an ordinary PREFILL batch executes its selected prompt range through the model’s full recurrent depth. In mixed mode, the scheduler can stage prompt work at boundaries needed to schedule prompt and decode requests together.
With WCPB enabled, prompt work uses two scheduler-visible phases:
PREFILLING_PRELUDE
-> embed a scheduled prompt range and prepare its inputs
PREFILLING_RECURRENT
-> execute one ready recurrent depth for scheduled prompt ranges
The prelude is a separate submission; the current implementation does not fuse it with depth 0. A WCPB prelude batch has loop_count=0, while a WCPB recurrent batch has loop_count=1. The scheduler records the execution boundary in the batch plan rather than relying on a user-facing atomic-prefill switch.
The model still processes prompt tokens through the configured recurrent depth before the prompt can proceed to CODA. WCPB changes how ready work is grouped and scheduled, not the logical number of recurrent depths.
Request state and scheduled work
The implementation does not create a persistent PrefillTask table or a general ExecutionTask runtime. The request owns its logical prompt progress; ScheduledRequest snapshots ranges and execution metadata for each batch.
The main progress state is:
- num_prefilled_tokens: prompt progress completed at the final recurrent depth, including progress within the current chunk.
- num_preluded_tokens: end of the prompt range whose prelude has been staged.
- prefill_depth_ranges: the ready token frontier for each recurrent depth.
- prefill_reclaimable_tokens: the start of the oldest resident chunk. It advances only when that chunk completes its final depth and its ring slot can be reused.
- prefill_window_origin: the origin used for ring-slot assignment, including a reused cached prefix.
Each scheduled range carries its token start and count, phase, depth, chunk start, and optional prefill_hidden_row_offset. It does not carry a persistent task ID or task-owned lifecycle state. One request may contribute multiple ranges at different depths to the same WCPB batch.
Readiness and wavefront batch construction
WCPB applies only to LAST_EXITED. Its depth-separated KV planes allow depth-0 work for a later chunk to proceed once the preceding depth-0 KV prefix is ready, even while an earlier chunk is doing deeper recurrent work.
The scheduler uses per-depth frontiers to select ready ranges:
- Depth-0 work follows the contiguous depth-0 KV prefix.
- Work at depth d + 1 follows the available hidden state from depth d.
- A recurrent range is limited by its depth frontier, staged prelude frontier, prompt end, chunk boundary, and remaining token budget.
The scheduler round-robins eligible request owners and ready ranges under max_num_batched_tokens, max_num_seqs, and KV-capacity constraints. It can place work from different chunks, requests, and recurrence depths into one recurrent-core batch. These are snapshots of ready ranges, not independently persisted tasks.
In mixed mode, the scheduler can combine decode and staged-prefill work in a batch when their execution phases are compatible. In particular, decode recurrent work and prompt recurrent work can share a recurrent batch. This behavior is implemented; it is not a non-goal.
Hidden-state storage and runner execution
The runner allocates staged prompt hidden-state storage with prefill_chunk_size * total_ut_steps rows per request. WCPB assigns chunks to ring slots relative to prefill_window_origin:
slot = chunk_index % total_ut_steps
prefill_hidden_row_offset = slot * prefill_chunk_size + chunk_offset
The runner embeds prelude ranges, gathers retained hidden rows for recurrent work, and packs them into the recurrent input batch. It scatters intermediate outputs back to their request ring rows for successor depths. The explicit row offset addresses the full request buffer, including the ring-slot offset. When it is None, the runner derives the row from token_start - chunk_start.
The final-depth output is used for prompt completion and CODA as needed; the runner does not retain a chunk’s intermediate state after that chunk has completed its final depth. Asynchronous submissions retain buffers until dependent device work has finished.
Progress, completion, and recovery
Prelude completion advances num_preluded_tokens and makes recurrent work eligible; it does not advance the fully prefilled prompt frontier.
Recurrent completion updates the corresponding depth frontier. Final-depth progress updates num_prefilled_tokens, but prefill_reclaimable_tokens advances only when the oldest resident chunk reaches its chunk end at the final depth. This keeps partial progress within a chunk separate from ring-slot reuse.
Prefix publication and PD progress are tied to completed prompt chunks. Imported requests initialize their prompt frontiers from the imported prefix. On preemption, staged progress is rewound to the request’s completed prompt frontier; unfinished staged work is recomputed after resume.
Compatibility and scope
WCPB requires LAST_EXITED; the scheduler rejects the SHARED KV layout because its position-major dependencies do not support this wavefront schedule. Unsupported staged-prefill combinations are rejected by scheduling-policy validation.
The current implementation includes paths for mixed scheduling, preemption and recovery, PD chunk transfer, speculative execution under compatible policies, asynchronous execution, and CUDA graph capture/replay. Mixed scheduling with speculative work is rejected; speculative WCPB uses a compatible non-mixed policy.
A generic task table, task-owned hidden-state handles, and out-of-order final-depth completion tracking remain future design options, not part of this implementation. The implementation enforces readiness and completion through request-owned frontiers, scheduled range snapshots, and the bounded hidden-state ring.
Goals
- Increase recurrent-core batch occupancy for chunked prompt prefill.
- Preserve
LAST_EXITEDcausal attention and depth-specific KV semantics. - Keep prompt prefill at full model depth so the first generated token has the same full-depth contract.
- Work with synchronous and asynchronous execution paths.
- Work with the existing Triton/FlashAttention execution and CUDA Graph infrastructure where a stable batch shape is available.
- Keep scheduler, logical KV management, and device execution responsibilities explicit.
- Make correctness and performance claims reproducible under controlled workloads.
- Provide an incremental path from the current minimal implementation to a more general ready-task scheduler.
- Introducing PD prefill/decode separation.
Non-goals
- Fused prefilling and decoding in the same device batch.
- Supporting WCPB with
SHAREDKV in the initial implementation. - Adding preemption or recomputation of partially completed WCPB tasks in the initial implementation.
- Changing Ouro exit policies, gate thresholds, sampling semantics, or decode loop counts.
- Reducing the model's logical recurrent work or claiming fewer FLOPs.
- Replacing the scheduler with a generic task runtime or reproducing the full vLLM scheduler hierarchy.
Feedback Period.
No response
CC List.
No response
Anything else.
No response
Before submitting a new issue...
- Make sure you already searched for relevant issues in the issue tracker, and looked for the answer in the documentation.
Activity
LyxWxj commentedon Sep 28, 2026
cc @bjf-frz
LyxWxj commentedon Oct 23, 2026
Performance and Accuracy Validation of Split Prefill, WCPB, and Atomic Prefill
cc @bjf-frz @Carlos779988
The results come from the same code(implemented on
ff05fe15d182af97a30be321c99e20d5d3b17b92, #144 ), the same model, each performance configuration was run for 3 rounds and I took the average.Conclusions
We have a performance gaining but precision divergence.
The request throughput, token throughput, and latency for fixed 512-token and variable-length 256/512/768-token requests are now shown side by side for the Split, WCPB, and Atomic paths. Under this A100 workload, WCPB generally had higher throughput than Split; Atomic throughput was close to the serial result of chunk128 Split, but under concurrent/arrival it was lower than Split/WCPB. Atomic used the
refillscheduler, while Split/WCPB used themixedscheduler, so the throughput differences also include scheduler-policy differences.In the workload, the outputs of the three paths were not always identical. Split/WCPB had 9% to 16% of request texts differing for fixed input and 15% to 20% for variable-length input; Atomic also differed from Split under concurrent/arrival, and the outputs of Atomic chunk32 and chunk128 were also not completely identical. In the single-request test with the fixed GSM8K-87 protocol and
prefill_chunk_size=64, Split/Atomic were both60/87, and WCPB was63/87; WCPB and Split/Atomic had24/87token sequences different, so the accuracy increase cannot be interpreted as semantic equivalence or model-quality improvement.On GSM8K-87, Split and Atomic both scored
60/87, and the token IDs for all 87 questions were completely identical; WCPB scored63/87, but had24/87sequences different from Split/Atomic. The Atomic test usedmode=refill,atomic_prefill=True.Validation Environment
b2ca1f5ac10f3f133c39e9df688521524c0530c1#144ByteDance/Ouro-1.4B0.28.0, client in a separate environmentnum_blocks512max_num_seqs8max_num_batched_tokens1024mixed; Atomic:refillmax_prefill_batches_before_decode1--no-atomic-prefill; Atomic: atomic prefill enabled by default--wavefront-chunked-prefill64tokens,ignore-eostemperature=0,seed=0min_loops=2,max_loops=4,exit_threshold=1100per configuration1per configurationThe paired Split/WCPB services rotated GPUs by chunk size; Atomic chunk32/128 used GPU 0/1 respectively in fixed-input tests and swapped the mapping in variable-length input tests. Each service group first completed a readiness check and a single-request warm-up; warm-up was not included in the client benchmark.
Workload Matrix
The client uses
/v1/completions;--max-concurrencyand--request-ratedefine three workload types:max-concurrencyrequest-rate1inf8inf42Each workload type was tested with
prefill_chunk_size=32and128, and Split, WCPB, and Atomic were measured separately. Split/WCPB usedmode=mixed, and Atomic usedmode=refill; the current configuration validation does not allowmode=mixedand atomic prefill to be enabled at the same time. For Atomic, chunk size controls the prompt span size, and each span runs the full recursion depth in one submission. The server-sidemax_num_seqs=8is the scheduler's request-owner limit; the client concurrency parameter only controls arrival-side pressure, and the two have different meanings.Fixed Input 512
Using the benchmark client's random dataset:
random-input-len=512,random-output-len=64,random-range-ratio=0; all 100 requests had an input length of 512.In the table, latency is in ms and formatted as
p50/p90/p99; throughput is request/s, output tok/s, and total tok/s, respectively.All fixed-512 results had
completed=100andfailed=0; the actual input length was{512: 100}, and the output length was{64: 100}.The number of generated-text differences among the three configurations is as follows. The values indicate the number of requests out of 100 whose generated text differed:
Variable-Length Input 256/512/768
A fixed JSONL custom dataset was used, with shuffle and chat template disabled. The actual tokenizer length distribution of the 100 requests was: 34 with 256 tokens, 33 with 512 tokens, and 33 with 768 tokens; each request generated 64 tokens. This distribution remained unchanged across the Split, WCPB, and Atomic runs.
All variable-length results had
completed=100andfailed=0; the input length distribution was{256: 34, 512: 33, 768: 33}, and the output length was{64: 100}.The number of generated-text differences among the three configurations is as follows:
For variable-length input, the WCPB p99 ITL under chunk128 concurrent and arrival was 191 ms and 192 ms, respectively, higher than Split's 102 ms; therefore the throughput improvement did not cover all tail-latency metrics.
Performance Chart
The chart shows request/s, output tok/s, total tok/s, and the p50/p90/p99 of TTFT, TPOT, ITL, and E2E, in seven vertical panels. Each panel uses the three workload profiles Serial, Concurrent, and Arrival as horizontal groups, and each group compares fixed 512 and variable-length input; colors distinguish paths, while solid/dashed lines and point shapes distinguish
chunk_size=32/128. Latency uses p50 polylines, p90 diamonds, and p99 interval endpoints; TTFT uses a logarithmic y-axis so that serial and concurrent workloads can be viewed together. The chart labelsmax_num_seqs=8,max_num_batched_tokens=1024, output length, and scheduler mode. Each configuration has only one round of measurement, so there are no statistical error bars.GSM8K-87 Accuracy Comparison
This is a single-request regression independent of the HTTP performance matrix, using the fixed
docs/accuracy.mdprotocol, Ouro-1.4B, BF16, 4-layer greedy decode, and 87 questions. Split/WCPB usedmode=mixed,split_prefill=true, andprefill_chunk_size=64; Atomic usedmode=refill,atomic_prefill=true, andprefill_chunk_size=64, becausemixed + atomic_prefillis not supported by the current scheduler. The native scheduler configuration for all three wasmax_num_seqs=1andmax_num_batched_tokens=128, which cannot be conflated with the HTTP matrixmax_num_batched_tokens=1024.For Atomic, the decode token depth was 4 for all 87 questions, and the token IDs were completely identical to those of Split; this result shows that under the single-request fixed protocol, atomic full-depth prefill maintained the generated sequence with Split. Between WCPB and Split, 2 questions differed starting from the first token; in the paired results, 4 questions changed from wrong to correct and 1 question changed from correct to wrong. All three results passed the minimum accuracy threshold of the fixed protocol, but WCPB did not pass the semantic acceptance criterion of "keeping the generated tokens unchanged."
Layer-wise Error Propagation Diagnostic
The following figures come from the same Split/WCPB comparison diagnostic record (prompt record
311), configured with Ouro-1.4B BF16,mode=mixed,prefill_chunk_size=64,max_num_batched_tokens=128, and fixed 4 recursions. This prompt has 303 positions. Error is defined asmax(abs(x_split - x_wcpb)); before comparison, BF16 captured values were converted to FP32. The figures are not means, RMS, or relative errors.The first figure takes the maximum error over the 303 prompt positions and the hidden dimension for each recursion depth; depth numbering starts from 0. Divergence begins at the d0 output, with a maximum value of 0.25; afterward, the maximum values at the d1/d2/d3 outputs are 2.703125, 2.75, and 4.21875, respectively. The second figure tracks the maximum error of the hidden-state input and output for d0, token position 64, across 24 transformer blocks; the block 0 output is 0.0009765625, and the block 23 output is 0.125. The curve fluctuates, and one cannot infer from this that every layer monotonically amplifies the error.
The third figure expands the attention error for the same token: the maximum difference between query and KV key is 0.0625 (block 1), the maximum KV value difference is 0.03125 (block 17), and the maximum attention output difference is 0.03125 (block 6). The block, depth/position mapping, and write-address checks for this record all showed no mismatch; this supports the existence of differences in the numerical path, but a single-sample trace cannot prove that all differences come from one particular kernel. For this sample, the first output token was Split
8696and WCPB1385, and the maximum absolute logits difference was0.1875.