feat(pp) support dynamic chunked pipeline parallel - #2007
Conversation
🏷️ CI GuideRuns automatically on every eligible PR before approval:
Heavy model tests:
|
9006271 to
21c6b6e
Compare
…f work Dynamic chunking cost 7% of throughput at 128K ISL on Kimi-K2.5: shrinking a chunk cannot restructure a pipeline that is already full. - Solve only while fewer than pipeline_parallel_size requests are prefilling. With several prefilling, the forward count stayed flat (579 -> 582) while the prefix rebuilt per chunk doubled (34.8M -> 70.7M tokens). - Profile a (prefix, chunk) grid with real token ids, and model the per-chunk prefix cost explicitly. - Refuse a non-positive linear term instead of clamping it to 0, which had left the prediction as pure geometry. - Floor the chunk at the prefix term, and disable chunking outright when the profiled cost barely grows with the prefix. Co-authored-by: Cursor <cursoragent@cursor.com>
…efill Dynamic chunking measured flat to negative on Kimi-K2.5-MXFP4 (PP4, 128K ISL) because startup profiling never fits anything resembling serving: a single sequence over an empty block table puts gamma 24x and the attention-area term ~4000x below their served values, so the solver is clamped back to the base size. - Take coefficients from --dynamic-chunking-calibration, collected with --dynamic-chunking-calibration-logging, which times the transformer between the PP receive and the PP send. A real fit gives 13 equal-cost chunks instead of 4 growing ones: -34% mean TTFT, +44% throughput against fixed 32768 at concurrency 1. - Charge gamma*L against the budget the chunk has to match, and keep the chunk fixed when that budget is unreachable. - Floor solved chunks at --dynamic-chunking-min-chunk-size rather than a quarter of the base; over-splitting is bounded by an absolute size, not a ratio. - Add --dynamic-chunking-base-size, since inheriting --max-num-batched-tokens lets the solver only shrink, growing chunk counts that each re-pay gamma*L. - Gate on the request being the pipeline's only prefill source, as a peak over a trailing window, counted the same way on both scheduling paths. Co-authored-by: Cursor <cursoragent@cursor.com>
21c6b6e to
2b2181f
Compare
Replace manual calibration controls with a bounded, quality-gated sweep over real prefills so each deployment installs a stable latency model automatically. Co-authored-by: Cursor <cursoragent@cursor.com> fix code format
2b2181f to
5506411
Compare
| ) | ||
| max_chunk -= max_chunk % alignment | ||
| if max_chunk < 8 * alignment: | ||
| raise ValueError( |
There was a problem hiding this comment.
Maybe better to use try... except like line1367 fit_chunk_overhead function?
There was a problem hiding this comment.
I agree it should fail gracefully like fit_chunk_overhead: I’ll log a warning and return an error so the server can continue with fixed chunking instead of failing startup.
| return False | ||
| # A fit is attempted once per timing the last attempt did not see, so a | ||
| # failure costs one retry per new measurement rather than one per poll. | ||
| return self._since_fit > 0 |
There was a problem hiding this comment.
This calibration sweep has no give-up mechanism: _calibrating only flips False inside install_chunk_latency_model(), which is only reached once a fit clears ChunkLatencyCalibrator.fit()'s quality gates. If those never pass on real hardware (timing jitter, MoE routing noise, EPLB rebalancing, background KV offload), the sweep never ends — every other request stays permanently chunked at max_num_batched_tokens // 4 for the life of the process, with no way to recover short of restarting without --enable-dynamic-chunking. Suggest capping the number of failed fit attempts and giving up to fixed chunking with a warning once exceeded.
There was a problem hiding this comment.
Thanks for catching this. You’re right: repeated samples can keep triggering failed fits indefinitely while the scheduler remains in calibration mode. I’ll cap the number of failed fit attempts; once exceeded, calibration will stop, a warning will be logged, and the scheduler will fall back to fixed chunking.
- Count rejected fits against MAX_CALIBRATION_FIT_FAILURES. The sweep only ended on a fit that cleared the quality gates, so timings that never clear them left every other request at max_num_batched_tokens // 4 for the process's life. On give-up the workers stop timing, the sweep ends, and one warning says so. - Report too little room to profile like a failed overhead fit rather than raising: it means serve with fixed chunking, not fail startup. The guard now checks the sizes the sweep actually produces. - Move the profiling sweep constants beside the fit requirement they serve.
Dynamic Chunked Pipeline Parallelism
Background
feat(pp) support dynamic chunked pipeline parallel
Motivated by SGLang Dynamic Chunking and vLLM Ascend Dynamic CPP.
With fixed-size prefill chunks, attention cost grows with the cached prefix, so later chunks take longer than earlier ones. Pipeline stages fall out of sync and the pipeline develops bubbles. Dynamic chunking shrinks later chunks so that every chunk costs about the same.
Principle
Latency model
A chunk of
xnew tokens after a cached prefix ofLtokens is modeled as:Lis the cached-prefix length,xis the number of new tokens in the current chunk, andt(L, x)is the predicted chunk latency.cis the fixed per-forward overhead, paid even whenL = 0.gamma * Lcaptures prefix-dependent work such as rebuilding cached-prefix state in MLA models. It is kept separate fromcrather than folded into one constant because the two scale differently withL:gamma * Lvanishes atL = 0and grows with the cached prefix, so collapsing them would erase exactly the prefix-dependent behavior this feature acts on.b * xcaptures work that grows linearly with the new-token count.a * (2 * L * x + x^2)models self-attention work:2 * L * xfor the new chunk against its cached prefix,x^2for interactions within the new chunk.a,b,c, andgammaare learned automatically from measured forward latency, so no offline coefficients or calibration flags are required.Solving for the next chunk
Every chunk of a request should cost what the first one did. The first chunk runs at
L = 0, so its cost isc + b*x0 + a*x0^2. Sincecis paid by every forward it cancels from both sides, and the budget a later chunk has to match isgamma * Lis subtracted because it is a floor the chunk owes before any of its own tokens are attended to. Equalizing only the attention-area terms would leave every chunk paying that floor on top of an already-equal budget, making later chunks both too large to be equal-latency and too numerous.xthat spends the remaining budget, then applies the serving constraints: the smoothing factor, the minimum chunk size, an upper bound of the initial chunk size and the batch-token budget, and alignment tomax(KV block size, 64).Equal-cost chunks keep pipeline stages in sync, which is what removes the bubbles and shortens TTFT for long requests.
Performance
Kimi-K2.5-MXFP4, prefill PP4×TP1 on MI355X, PD disaggregation, OSL=128.
Each cell is throughput change / mean TTFT improvement; positive means dynamic chunking wins.
Baseline: tuned fixed chunk size
Fixed and dynamic each take their best result across 8K/16K/32K:
Against a fully tuned fixed chunk size the gains are small and workload dependent; at 256K, fixed 8K is still the best configuration in most cases.
Baseline: default
--max-num-batched-tokens=16384At the default 16K budget, dynamic chunking mainly helps 128K at conc=1, worth roughly 10% on both throughput and TTFT. At 256K or higher concurrency it is essentially flat.
Baseline:
--max-num-batched-tokens=32768(common in agentic serving)The large 32K initial chunk benefits most, especially at long context and low concurrency. Gains at higher concurrency are not consistent, and 128K fixed-length at conc=4 still regresses noticeably.
Overall, dynamic chunking reduces how sensitive a large initial chunk is to tuning at long context and low concurrency, rather than beating a fully tuned fixed chunk size everywhere.
Correctness
GSM8K, 3-shot, 1319 samples, with a 512-token budget to force multi-chunk prefills:
The accuracy difference is within run-to-run noise, so dynamic chunking does not regress accuracy.