Repository navigation
feat(kimi-k3): PP8 prefill for P/D serving — KDA state under PP, PP→DCP transfer, MLA staging - #2709
wanzhenchn wants to merge 90 commits into
Conversation
🏷️ CI GuideRuns automatically on every eligible PR before approval:
Heavy model tests:
|
7832617 to
7e1bf6c
Compare
d5e502a to
1c6fbeb
Compare
3dae59f to
b5acae7
Compare
Kimi-K3 under pipeline parallelism (verified as pp2 x tp4), including DSpark speculative decoding and prefix caching of the KDA state. PP still needs --enforce-eager: decode has no CUDA graphs yet. Model and transport - Carry K3's block_residual across the stage boundary with the hidden states, and load the DSpark draft's own embedding on the last stage, where the target's embedding is a PPMissingLayer. - Relay DSpark's aux hidden states across stages (PPAuxRelay). The drafter lives on the last stage, so a hook on a tap owned by an earlier stage lands on a PPMissingLayer and never fires. Each stage captures its own taps and forwards them, concatenated in layer_ids order, in the intermediate-tensor payload. The embedding tap (-1) is resolved as the drafter resolves it: the embedding module, identity extract. Speculative decoding under PP - Let a PP engine verify its own drafts. PP output is undeferred, so: the sampler reports this step's rejection and bonus counts on the non-deferred path; they are published to every stage's attention backends, so non-sampling stages roll back linear-attention state; postprocess keeps ONE run of exactly mtp_k + 1 placeholders after every step, whatever was rejected. - Rebuild the verify window from that run. All four backends read `verify_context_shift()`: `num_rejected` under deferred output, 1 under undeferred. The arithmetic lives in token_layout.decode so it is testable without aiter. The window's positions and KV slots are anchored where the scheduler staged them. - Gate and size the verify window on the run's speculative config, not on whether this stage drafts: only the last stage has a drafter, but every stage verifies. Per-stage buffers are sized the same way. - A P/D producer still never speculates, under PP or not. - Round MLA's query-head padding to a width aiter dispatches (24 -> 32 at TP4; TP8 already pads 12 -> 16). KDA state and prefix caching under PP - Progress advances at schedule time, so a chunk ending on a checkpoint rung would let the next chunk overwrite the state slot before the checkpoint is filed. Hold the next chunk (both prefill scans skip it) until the batch that carries the rung files its hashes; the hold is released with that batch even if the seq was preempted, aborted or retired meanwhile. - The checkpoint is filed after the prompt, so its state fork lands on the first decode. Honour it there: publish the source slot and move conv and recurrent state onto the write slot before the kernels run. With DSpark that decode is a verify step, so the spec path does the same. Only a backend whose model does that copy may take a fork on the spec path (`GDNStateMixin.spec_honours_state_fork`, set by K3); every other backend keeps the assert. K3's mono plan refuses a forked step, since a mono step replaces the branch that does the copy. CI - Add a Kimi-K3 pp2 x tp4 DSpark accuracy case with prefix caching and ptpc_fp8 online quant on, as the TP8 Kimi-K3 case runs it, the only accuracy job that reaches any of the paths above. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- tokenIDProcessor.verify_ctx_lens(context_lens) replaces token_layout.decode.verify_context_shift: the attention builders now take their verify-window context lengths straight from the runner. - Rename the runner's `spec_enabled` -> `is_spec_run` (speculative decoding configured, every PP stage runs verify windows) and `use_spec` -> `has_drafter` (this stage owns the drafter and rejection sampler; under PP the last stage only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A producer never speculates, so the Kimi-K3 + DSpark gate refused GLM-5.2's PP prefill + MTP Atomesh cells for nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The kernel resumes from column num_accepted - 1, so a forked verify with a bonus read a recycled write slot; only column 0 held the source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GDNStateMixin.spec_honours_state_fork() defaults to False; only Kimi-K3's KDA forward (kimi_linear) reads spec_state_indices_in_tensor and opts in, so glm5_next, Qwen3-Next/3.5 and Qwen4Exp keep the assert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UBatchWrapper.forward takes no intermediate_tensors, so TBO never ran under PP; refuse it in config instead of keeping a grow path nothing reaches. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
seed_forked_slots indexed with a GPU bool mask, a host sync in every linear-attention layer of a fork step. The builder already knows the (write, source) pairs: ship them once per step in the pinned copy that parks write_pos, and index with them directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lowest own tap opens the forward's send buffer and every own tap writes its columns; the reset and the _filled check read state that the compiled forward bakes to the warmup trace, so they guarded nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…der PP pp_speculative_methods sits beside mono_decode_installers, so the next model wired to the relay adds a row there instead of editing Config. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_pp_checkpoint_hold -> _checkpoint_holds (keyed on advance_on_schedule), holds_checkpoint_hostage -> carries_held_checkpoint, and _defer_prefill_hashes -> _queue_or_flush_prefill_hashes, since it flushes the whole queue; mark the hold as scaffolding #2709 replaces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read seq.num_placeholder_tokens, which postprocess refills to one full window, instead of re-deriving the run's width from the scheduled count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drafter.take_relayed_aux keeps the received [N, k*H] block and aux_for serves it ahead of the own-tap buffers, so the last stage no longer splits it per tap for compute_draft_kv to concatenate back (DSpark skips the cat when it is the only block). The relay takes the drafter class's spec on every stage instead of the drafter's private _aux_capture_spec, and the runner no longer hands it _aux_buffers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_aux_capture_spec now parses dspark_target_layer_ids once, through aux_capture_spec, and drops its unused target_model parameter. PPAuxRelay keeps only spec (no layer_ids / hidden_size copies), drops the dead _armed guard, and raises the missing-payload error from one place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings #2365's fifteen review commits since c4975cd (2edbab7 through 30829a3). One conflict, in `Scheduler`'s PP state-checkpoint hold. #2365 turned `_pp_checkpoint_hold` into `_checkpoint_holds`, keyed by the holding chunk's end so only that chunk releases it; this branch files the checkpoint at schedule time where the class allows it and skips a rung at prompt end. Keep #2365's map and release, and this branch's conditions for taking a hold. The note that this branch would file at schedule time is dropped, as the merge does so. Co-authored-by: Cursor <cursoragent@cursor.com>
valar-cop
left a comment
There was a problem hiding this comment.
LGTM
ci Run non-GPU unit tests (Pre Checkin) failed — not sure yet (low): The log artifact is 0 bytes, so there is no failure text to attribute. The same job fails 4 of 8 recent main runs in the same window (14:17-14:52), while every other job at this head passes and the head is merges-only over a tree that passed 10,737 before.
log file .valar/ci/job_126043848503.log is empty — 0 bytes
same job on main: ['2026-11-03T15:45 success', '2026-11-03T15:33 success', '2026-11-03T14:52 failure', '2026-11-03T14:40 failure', '2026-11-03T14:39 failure', '2026-11-03T14:17 failure']
next: Re-run the Pre Checkin job on 1a07eb2 and read the log; if it is green, compare the passed/skipped counts against the parent head (10,739 / 1,133 on 41422091906) before merge. https://github.com/ROCm/ATOM/actions/runs/41672378533/job/126043848503
|
Thanks. The log of job 126043848803 is readable through the API, and the failure is not from this head:
A re-run reuses the same merge commit, so it would fail the same way. The next trigger against current main picks up the fix; nothing to change in this PR. |
Taps owned by an earlier PP stage arrive through the relay, so the last stage gives them a 0-row buffer and no hook (~470 MB/rank for K3 at pp2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both now ask whether the tap's module is a PPMissingLayer, the embedding included, instead of the relay keying the -1 tap on is_first_rank. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- State tier: refuse pipeline parallelism again for every layout except kimi_k3. Under PP, Qwen keeps fork checkpoints and its worker turns the tier off, so the engine side must not install an index or floor rungs to the LMCache chunk for it. - Under PP the head bounds its state index by the largest image any stage holds (`pipeline_image_bytes` in the agreed spec), not its own: each stage's codec evicts by its own image. - MLA shard staging (ATOM_PD_MLA_STAGING_CHUNK_PAGES) is now held back from the KV cache budget (`mla_shard_staging_reserve_bytes`), and its slots fit that reserve. They are handed out by the connector's `SlotPool` like the other staging pools, and a gather waits on its slot's stream. - The scheduled-checkpoint hash reuses `_chain_to`; one helper drops a completed prefill with its ready event; ModelRunner and the NUMA binding share `local_device_index`; both engine cores share the idle state publish; always-set connector fields are read directly; stale comments fixed. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Updated in 9e9f07e after another review pass over the PR:
GSM8K, which the review asked for before merge, both on the PP8×TP1 → TP8×DCP8 DSpark4 agentic cells with 128 staging pages per slot:
Both runs used this PR at bd26837, on #2787's verification branch. Until this update, the PR's own code changed after that only in merge resolutions. This update's changes have not been through GSM8K yet; #2787's cells will run them. Pre Checkin passed on 9e9f07e with 11,210 passed and 1,161 skipped (41805335038), 66 more passed than on #2365's head b62bf7a, both merged with the same |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* [feature] Support pp>1 for Kimi-K3, with DSpark speculative decoding
Kimi-K3 under pipeline parallelism (verified as pp2 x tp4), including
DSpark speculative decoding and prefix caching of the KDA state. PP still
needs --enforce-eager: decode has no CUDA graphs yet.
Model and transport
- Carry K3's block_residual across the stage boundary with the hidden
states, and load the DSpark draft's own embedding on the last stage,
where the target's embedding is a PPMissingLayer.
- Relay DSpark's aux hidden states across stages (PPAuxRelay). The
drafter lives on the last stage, so a hook on a tap owned by an earlier
stage lands on a PPMissingLayer and never fires. Each stage captures
its own taps and forwards them, concatenated in layer_ids order, in
the intermediate-tensor payload. The embedding tap (-1) is resolved
as the drafter resolves it: the embedding module, identity extract.
Speculative decoding under PP
- Let a PP engine verify its own drafts. PP output is undeferred, so:
the sampler reports this step's rejection and bonus counts on the
non-deferred path; they are published to every stage's attention
backends, so non-sampling stages roll back linear-attention state;
postprocess keeps ONE run of exactly mtp_k + 1 placeholders after
every step, whatever was rejected.
- Rebuild the verify window from that run. All four backends read
`verify_context_shift()`: `num_rejected` under deferred output, 1
under undeferred. The arithmetic lives in token_layout.decode so it
is testable without aiter. The window's positions and KV slots are
anchored where the scheduler staged them.
- Gate and size the verify window on the run's speculative config, not
on whether this stage drafts: only the last stage has a drafter, but
every stage verifies. Per-stage buffers are sized the same way.
- A P/D producer still never speculates, under PP or not.
- Round MLA's query-head padding to a width aiter dispatches (24 -> 32
at TP4; TP8 already pads 12 -> 16).
KDA state and prefix caching under PP
- Progress advances at schedule time, so a chunk ending on a checkpoint
rung would let the next chunk overwrite the state slot before the
checkpoint is filed. Hold the next chunk (both prefill scans skip it)
until the batch that carries the rung files its hashes; the hold is
released with that batch even if the seq was preempted, aborted or
retired meanwhile.
- The checkpoint is filed after the prompt, so its state fork lands on
the first decode. Honour it there: publish the source slot and move
conv and recurrent state onto the write slot before the kernels run.
With DSpark that decode is a verify step, so the spec path does the
same. Only a backend whose model does that copy may take a fork on
the spec path (`GDNStateMixin.spec_honours_state_fork`, set by K3);
every other backend keeps the assert. K3's mono plan refuses a forked
step, since a mono step replaces the branch that does the copy.
CI
- Add a Kimi-K3 pp2 x tp4 DSpark accuracy case with prefix caching and
ptpc_fp8 online quant on, as the TP8 Kimi-K3 case runs it,
the only accuracy job that reaches any of the paths above.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: move the verify-window shift into the runner, clarify spec flags
- tokenIDProcessor.verify_ctx_lens(context_lens) replaces
token_layout.decode.verify_context_shift: the attention builders now take
their verify-window context lengths straight from the runner.
- Rename the runner's `spec_enabled` -> `is_spec_run` (speculative decoding
configured, every PP stage runs verify windows) and `use_spec` ->
`has_drafter` (this stage owns the drafter and rejection sampler; under PP
the last stage only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: skip test_verify_ctx_lens without AITER, like the other model_runner tests
The non-GPU runner has no aiter, so importing model_runner aborted collection.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: derive the PP aux relay layout from ascending tap order and check it
Every stage now orders its columns by sorted(layer_ids), and a stage's incoming
taps are those below its first layer, so the layout no longer depends on the
order the drafter lists its taps in. pack/absorb reject a payload whose width
does not match the taps they expect. Round-trip test over pp 2/4/8 with
unsorted taps and stages that own none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: release a batch's checkpoint holds before any per-seq work
A filing that raised mid-batch (logged and swallowed by the PP head loop)
left the holds of every later seq in the batch set, keeping them out of
the prefill scan forever. Drop the whole batch's holds up front in both
register_prefill_hashes and postprocess.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: make the K3 DSpark skip prefixes a tuple the PP stage rebinds
The non-first PP stage overrode skip_weight_prefixes on the instance
while the class declared it a ClassVar list. It already rebound rather
than mutated, but the annotation said the opposite and a list default
invites in-place edits. Declare an immutable tuple default instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: fill the PP verify draft columns in one vectorized scatter
The PP decode path staged each row's drafts with a per-row Python loop.
Build the destination indices with a mask over the draft columns and
write them in one assign, as the scheduler's draft marshal does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: read undeferred spec counts back in one D2H
Both counts are rows of the rejection sampler's int32 verdict, already
contiguous as spec_counts, so copy that once instead of syncing per row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: pack the PP aux relay into one preallocated send buffer
pack() concatenated the received block and each own capture into a new
tensor every step. Preallocate the payload instead: own taps' capture
buffers are column views of it and received rows are copied into its
leading columns, so packing allocates nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: size the PP aux relay send buffer to the forward's rows
The preallocated send payload was max_num_batched_tokens rows wide and
resident on every non-last stage (~470 MB per rank at pp2). Allocate it
per forward instead: the first own tap's hook allocates the rows it
captures, every own tap writes its columns, and pack() copies the
received taps into the leading ones and hands the buffer to the send
path, which clones it. A stage that owns no tap forwards the received
rows as-is. PP stages run the target eagerly, so no recorded graph bakes
the per-step address.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: drop the stale read-side reason from _checkpoint_room
This PR adds the spec path's read-side state index, so the token count
is the only reason a forking class skips checkpoints on spec decode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: flush a held chunk's hashes on the PP ack path too
Under dynamic chunking a middle chunk is acked, and that branch only
queued its prefill hashes. A seq held for the chunk's checkpoint is out
of the prefill scan, so nothing flushed the queue and it stalled. Both
no-output branches now go through one helper that queues and flushes
when the batch holds a seq.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: let any PP aux relay tap allocate, and check every own tap fired
Only the stage's first own tap allocated the send buffer, so a forward
where it captured nothing left the later taps writing into the previous
forward's buffer (or into None). Whichever tap fires first now allocates
for the forward's rows, and pack() asserts every own tap was captured.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: build the PP aux relay in spec_decode, not in the runner
ModelRunner reached into the drafter's private _aux_capture_spec and
dspark_proposer's build_aux_capture_spec to assemble the relay. Move that
into PPAuxRelay.build so the runner makes one call and keeps only the
pp_aux_relay handle it uses at absorb/pack.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: size a P/D first decode's neighbours from the shifted context lens
verify_ctx_lens returns a new array, so the is_first_decode_without_local_prefill
branch read batch.context_lens unshifted: a row with num_rejected > 0 in the
same batch got r extra pages in kv_indptr while block_tables and positions
used the shifted lengths. Read the local context_lens there, as the other
two branches do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: read a forked state through the kernels' read-side index
Under pp>1 a state fork can reach decode and spec verify. Instead of the
model copying the source state onto the write slot, the builder now ships
`spec_state_indices_in_tensor` (the write table with the fork source in the
resume column), `fused_sigmoid_gating_delta_rule_update` takes
`ssm_state_indices_in` like fused_recurrent already does, and the spec conv
update gets `conv_state_indices_in`. This drops `spec_honours_state_fork`,
its assert, and both copies in KimiKDAAttention, and covers Qwen GDN too.
ReplaySSM kernels have no read index and fold records onto the write slot's
checkpoint, so its wrappers seed that checkpoint from the source
(`seed_forked_slots`) and the builder parks the forked write slot's cursor at
-1. Before, the cursor was left at the previous tenant's value and its
records were folded into the forked state.
The mono K1 refusal stays: K1 reads `st_idx` only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: refuse unwired PP combinations at config time
Under pipeline_parallel_size > 1, only Kimi-K3 with DSpark relays the
target's aux taps across stages. Every other (model, spec method) pair now
fails at config time instead of mid-serving: EAGLE3's relay zip, a Qwen GDN
prompt-end fork, MiniMax-M3's embed on a PPMissingLayer. The DSpark
confidence scheduler and a P/D kv_consumer, which the PP loop has no
remote-KV path for, are refused as well.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: plan the vLLM plugin's dense MLA work at the layer's padded width
`MLAAttention.padded_num_heads` now rounds up to a width aiter dispatches,
and the plugin's MLA layer inherits it, so K3 at TP4 pads q from 24 to 32
heads. The plugin builder still planned the work descriptors for 24. Round
it the same way, as aiter_mla.py's builder does. The sparse builder and
layer keep max(heads, 16): they pad by repeat_interleave, which only
divides evenly at that width, and they already agree with each other.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: drop the PP checkpoint hold's dead prompt-end case
PP refuses PAGE-backed checkpoints, so every class that applies forks and
keeps a positive successor_room: a rung (or the prompt-end anchor) sits
strictly inside the prompt, and `start + chunk < num_prompt_tokens` always
holds. Filing checkpoints by rung hash at schedule time, which would
remove the hold altogether, is left as a follow-up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: install the PP aux relay's taps through the drafter's own helper
`PPAuxRelay.arm` repeated `Drafter.arm_aux_capture`'s hook mechanism and
skipped its checks. Both now go through drafter.py:
- `install_aux_taps` validates the spec's ids (each in {-1} U [0, n),
strictly increasing) and hooks a subset of them. The last stage hooks
the drafter's buffers, other stages the relay's send buffer.
- `_aux_hook` skips draft forwards and hands its writer
`ctx.ubatch_token_offset`. The relay writes at that offset and grows its
buffer for a later ubatch instead of reallocating over the first.
- `Drafter.aux_capture_spec(config)` is each drafter class's config-only
spec. A drafterless stage reads it off `factory.drafter_class(config)`,
so the relay no longer imports DSpark's builder.
`PP_AUX_KEY` is now the model-neutral "pp_aux".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: send the PP aux relay's payload without a second clone
The relay allocates a fresh buffer per step and hands it to the send path,
so cloning it again only doubles the copy. async_send_intermediate_tensors
takes the keys the caller hands over and sends those as they are. The
received aux block is now a run_model local instead of runner state, and
PPAuxRelay is imported at the top of the runner.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: one use_spec on every stage, runner and scheduler alike
Every PP stage runs the verify window, so the runner's is_spec_run /
has_drafter pair collapses back to the single use_spec it had, now set on
every stage (has_drafter's readers sit on the deferred path, which PP never
takes). The scheduler's spec_decode_local was the same flag under a second
name; its readers take use_spec directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: one source each for the deferred-output mode and the verify width
The scheduler's is_deferred_out (pp_size == 1) and tokenIDProcessor's
(not PP, not a recurrent-state P/D producer) agreed only because a producer
never speculates. Both now come from defers_output(config): the runner
passes the attention family it already resolved, the scheduler reads it off
the config.
GDN sizes its verify window from model_runner.num_spec_tokens, which every
PP stage carries, instead of the drafter's mtp_k with a
num_speculative_tokens fallback; config already refuses a speculative
config without num_speculative_tokens, so the fallback's ValueError was
unreachable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: top-level imports, and document PP + DSpark for Kimi-K3
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: keep the DSpark draft out of online quant in the PP2 accuracy case
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: keep the PP aux relay's hook free of in-place set mutation under Dynamo
The stage-0 relay hook runs inside the compiled backbone. `_filled.add`
mutated a set Dynamo did not create, so Dynamo replayed it with
clear()+update(); with cudagraphs on, an `update` in the rewritten bytecode
triggers the decorator's depyf import, which crashed PP2 warmup on images
without depyf. Rebind a frozenset instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: read undeferred spec counts off the sampled ids, without a D2H
The undeferred step already brings the sampled ids to the host. Each row
keeps its accepted drafts plus one token before the -1 tail, so both counts
are its length; drop the verdict's own .cpu().numpy(). The ragged sampler
test pins that layout against the verdict.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: stage the forked write slots through pinned memory
`torch.tensor(list, device=cuda)` is a pageable H2D, which PyTorch follows
with a stream sync. Pin the list and copy it non_blocking, as the other
host-staged indices do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: the relay's docstring follows the spec's layer_ids order
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: pass the runner's attn_family to defers_output again
The merge dropped it, so defers_output re-derived the family from
config.hf_config and test_spec_deferred_status_queue's producer cases
failed. The runner stub in test_h2d_runner_publication gains attn_family.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: gate PP placeholders on the batch's frozen chunk state
Under schedule-time advancement a middle chunk's live is_partial_prefill
flips once its final chunk is scheduled, so the middle chunk's postprocess
appended mtp_k + 1 placeholders to a seq still in prefill.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: release a checkpoint hold only from the chunk that holds it
Under PP an earlier chunk of the same seq can still be in flight when the
chunk ending on a rung is scheduled; its completion released the hold, so
the next chunk could run before the rung's checkpoint was filed. The hold
now records the held chunk's end, and only a batch reaching it releases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: let a P/D producer under PP keep any drafter
A producer never speculates, so the Kimi-K3 + DSpark gate refused GLM-5.2's
PP prefill + MTP Atomesh cells for nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: read a forked spec row's source in every column
The kernel resumes from column num_accepted - 1, so a forked verify with a
bonus read a recycled write slot; only column 0 held the source.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: keep the spec-path fork assert unless the backend opts in
GDNStateMixin.spec_honours_state_fork() defaults to False; only Kimi-K3's
KDA forward (kimi_linear) reads spec_state_indices_in_tensor and opts in,
so glm5_next, Qwen3-Next/3.5 and Qwen4Exp keep the assert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: refuse TBO under PP and drop the relay's TBO grow branch
UBatchWrapper.forward takes no intermediate_tensors, so TBO never ran under
PP; refuse it in config instead of keeping a grow path nothing reaches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: seed ReplaySSM forks from host-known slot pairs, without a sync
seed_forked_slots indexed with a GPU bool mask, a host sync in every
linear-attention layer of a fork step. The builder already knows the
(write, source) pairs: ship them once per step in the pinned copy that
parks write_pos, and index with them directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: write relay captures without bookkeeping Dynamo freezes
The lowest own tap opens the forward's send buffer and every own tap
writes its columns; the reset and the _filled check read state that the
compiled forward bakes to the warmup trace, so they guarded nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: let the registry declare which spec methods an arch serves under PP
pp_speculative_methods sits beside mono_decode_installers, so the next
model wired to the relay adds a row there instead of editing Config.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: name the checkpoint hold by what it does
_pp_checkpoint_hold -> _checkpoint_holds (keyed on advance_on_schedule),
holds_checkpoint_hostage -> carries_held_checkpoint, and
_defer_prefill_hashes -> _queue_or_flush_prefill_hashes, since it flushes
the whole queue; mark the hold as scaffolding #2709 replaces.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: stage the undeferred window from the recorded placeholder width
Read seq.num_placeholder_tokens, which postprocess refills to one full
window, instead of re-deriving the run's width from the scheduled count.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: let the drafter read the relayed aux block in place
Drafter.take_relayed_aux keeps the received [N, k*H] block and aux_for
serves it ahead of the own-tap buffers, so the last stage no longer
splits it per tap for compute_draft_kv to concatenate back (DSpark skips
the cat when it is the only block). The relay takes the drafter class's
spec on every stage instead of the drafter's private _aux_capture_spec,
and the runner no longer hands it _aux_buffers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: derive DSpark's instance aux spec from its classmethod
_aux_capture_spec now parses dspark_target_layer_ids once, through
aux_capture_spec, and drops its unused target_model parameter. PPAuxRelay
keeps only spec (no layer_ids / hidden_size copies), drops the dead
_armed guard, and raises the missing-payload error from one place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: drop the CLAUDE.md key list and getattrs on always-set fields
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: allocate aux buffers only for this stage's own taps
Taps owned by an earlier PP stage arrive through the relay, so the last
stage gives them a 0-row buffer and no hook (~470 MB/rank for K3 at pp2).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* review: share one tap residency rule between the drafter and the relay
Both now ask whether the tap's module is a PPMissingLayer, the embedding
included, instead of the relay keying the -1 tap on is_first_rank.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: ganyi <ygan@amd.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: wanzhenchn <wanzhenchn@gmail.com>
#2365 has landed on main as the squash 64412f1, so this PR now targets main. 64412f1's tree is #2365's head 9fb45ff, which this branch already merges, merged with main d8525dd (#2913). The only new change this merge brings is #2913. scheduler.py and tests/test_pp.py conflicted only because the squash repeats #2365's changes that this branch edits further. They keep this branch's versions: the result is this branch merged with d8525dd, which merges cleanly, and its diff against main is this PR's diff against 9fb45ff unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
Motivation
#2365, now on
main, serves Kimi-K3 at PP > 1 in one engine. For AgentX 1M P/D at high concurrency (C32–C88), the fastest prefill measured was PP8×TP1 feeding a TP8×DCP8 decode: at C32, 10,409 vs 9,196 tok/s/GPU for a TP8EP8 prefill (+13%). Before this PR:This PR adds what that topology needs, on top of #2365. Everything in it is specific to PP or to the PP → DCP transfer.
Stack and split
#2365 has landed on
mainas 64412f1, so this PR now targetsmain. The original #2709 was split into three PRs to make review easier:main, independent of PPWhile #2365 was open, this PR merged its head each time it moved, the last 9fb45ff in b82e2d0. #2365 landed as the squash 64412f1, whose tree is 9fb45ff merged with
maind8525dd (#2913). 0ab7e65 merges 64412f1. The two files the squash repeats and this PR edits further,scheduler.pyandtests/test_pp.py, keep this PR's versions, so the diff againstmainis the one this PR had against 9fb45ff. Merging #2365's heads changed two things from #2365. Itstest_head_flushes_a_held_chunk_whether_or_not_it_is_ackedbounds the head's in-flight window throughpp_size, which this PR replaces withpp_max_inflight(item 14), so it now sets both. Its 5b0b85d drops thestart + chunk < num_prompt_tokensguard on the PP checkpoint hold because PP refused PAGE-backed checkpoints. This PR lets them span PP stages (item 1), so a rung can sit at prompt end again, and the guard stays.Changes
Feature support (each was a startup refusal or a failed first transfer)
units_per_checkpoint. Each op is checked against a joint layout id and the summed image bytes of every stage. A stage without a linear-attention layer holds no state for an image, so every stage refuses to start with one error naming it.tp_rank % remote_tp_sizeis right only for replicated KV.main's MoRIIO connector also moves K3's state, but it reads none of this placement, so it keeps refusing PP.Correctness under PP
pipeline_parallel_size; explicit settings are unchanged.pp_rank, so all eight PP8×TP1 stages bound to GPU 0's node. WithATOM_NUMA_NODE=0,0,0,0,1,1,1,1, stages 4–7 ran on the wrong node.(dp_local_rank * pp_size + pp_rank) * tp_size * pcp_size + rank. The binding andModelRunner's device choice now share onelocal_device_indexinconfig.py, so they cannot drift apart again.Performance
ATOM_PD_MLA_STAGING_CHUNK_PAGESdefaults to 0, the old path, and ci(k3-agentic): Kimi-K3 AgentX 1M P/D cells, one per concurrency on its best measured prefill #2787's PP8 cells set 128. The slots are handed out through the connector'sSlotPool, as for the other staging pools.ATOM_PD_MLA_STAGING_STREAM_PRIORITY).ATOM_PP_MAX_INFLIGHT, 0 = stage count).Tests
test_pp_paged_checkpoints.py,test_page_unit_checkpoint.py,test_kda_layout_id.py(1);test_lmcache_offload_connector.py,test_block_manager.py(2), including that a Qwen layout keeps no state tier under PP and that the index counts the largest stage image;test_pd_pp.py,test_gdn_state_relocation.py(3, 4, 8, 11, 14),entrypoints/test_pd_prompt_token_ids.py(3);test_lmcache_offload_v4_page_slot.py(5);test_state_checkpoint.py(6, 7, 10);test_pd_kv_staging.py,test_pd_producer_config.py(11). The first imports the connector throughstubbed_aiter, so its CPU cases, staging at 1 and 4 pages per chunk, run without aiter. The second checks the staging reserve, and the first that the slots fit it.tests/withouttests/plugin) fails nowhere on 9e9f07e that it passes on the base, [feature] support pp>1 for Kimi-K3 #2365's head b62bf7a. The failures it has on both need what this environment lacks (aiter, host UVA). The twotest_multiprocess_metrics.pycases that give a subprocess 30 s to import a server entry point time out here on both trees, one or the other from run to run. On 0ab7e65, merged withmain64412f1, the 18 test files this PR changes, the scheduler tests and feat(api-server): --api-server-count N, front-end worker processes for the OpenAI server #2913's front-end tests pass: 1,936 passed, 6 skipped. Black and ruff are clean on the changed files.main64412f1 (42024391441). The 66 more are this PR's tests. Its runner has no aiter, sotest_gdn_state_relocation.py,test_kda_layout_id.pyandtest_pp_paged_checkpoints.pyare skipped there as whole modules.test_pd_kv_staging.pynow runs its CPU cases there, andtest_pd_pp.pyall 155 tests, each through an aiter stub.test_pd_kv_staging.py,test_pd_pp.py,test_pp_draft_input_ids.pyandtest_pp_aux_relay.pypassed on afcaebe inrocm/atom-dev:nightly_202610291449: 250 passed, none skipped. On its first rebase ontomain, 252 passed. On bd26837, before the merges of [feature] support pp>1 for Kimi-K3 #2365's later heads, 258 passed, none skipped. They have not been run on a GPU since.