Repository navigation
[feature] support pp>1 for Kimi-K3 #2365
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+1,855
−236
Merged
Changes from 49 commits
Commits
Show all changes
52 commits
Select commit
Hold shift + click to select a range
ba71ea2
[feature] Support pp>1 for Kimi-K3, with DSpark speculative decoding
ganyi1996ppo 0eac576
review: move the verify-window shift into the runner, clarify spec flags
ganyi1996ppo a247dab
test: skip test_verify_ctx_lens without AITER, like the other model_r…
ganyi1996ppo 539e3fe
review: derive the PP aux relay layout from ascending tap order and c…
ganyi1996ppo a913f2b
review: release a batch's checkpoint holds before any per-seq work
ganyi1996ppo 458260b
review: make the K3 DSpark skip prefixes a tuple the PP stage rebinds
ganyi1996ppo cba6844
review: fill the PP verify draft columns in one vectorized scatter
ganyi1996ppo 9981597
review: read undeferred spec counts back in one D2H
ganyi1996ppo 1f17859
review: pack the PP aux relay into one preallocated send buffer
ganyi1996ppo a128906
Merge main (0e71a5273) into guanbao/k3-pp2tp4
wanzhenchn 6808659
review: size the PP aux relay send buffer to the forward's rows
ganyi1996ppo acdf74d
review: drop the stale read-side reason from _checkpoint_room
ganyi1996ppo f34cb8a
review: flush a held chunk's hashes on the PP ack path too
ganyi1996ppo 5ab3d68
Merge main (99ebf633a) into guanbao/k3-pp2tp4
ganyi1996ppo a0b8ef0
review: let any PP aux relay tap allocate, and check every own tap fired
ganyi1996ppo f0522ff
review: build the PP aux relay in spec_decode, not in the runner
ganyi1996ppo 9bebbb3
review: size a P/D first decode's neighbours from the shifted context…
ganyi1996ppo 401bcae
review: read a forked state through the kernels' read-side index
ganyi1996ppo 549ae77
review: refuse unwired PP combinations at config time
ganyi1996ppo c823e96
review: plan the vLLM plugin's dense MLA work at the layer's padded w…
ganyi1996ppo 5b0b85d
review: drop the PP checkpoint hold's dead prompt-end case
ganyi1996ppo 7a61f50
review: install the PP aux relay's taps through the drafter's own helper
ganyi1996ppo b5821ff
review: send the PP aux relay's payload without a second clone
ganyi1996ppo 400f1f7
review: one use_spec on every stage, runner and scheduler alike
ganyi1996ppo 202906e
review: one source each for the deferred-output mode and the verify w…
ganyi1996ppo 74d743b
review: top-level imports, and document PP + DSpark for Kimi-K3
ganyi1996ppo 3fc180e
review: keep the DSpark draft out of online quant in the PP2 accuracy…
ganyi1996ppo f660797
review: keep the PP aux relay's hook free of in-place set mutation un…
ganyi1996ppo 64bda0c
review: read undeferred spec counts off the sampled ids, without a D2H
ganyi1996ppo 89b996c
review: stage the forked write slots through pinned memory
ganyi1996ppo e1c80ce
review: the relay's docstring follows the spec's layer_ids order
ganyi1996ppo c4975cd
Merge main (825b63ba4) into guanbao/k3-pp2tp4
ganyi1996ppo 2edbab7
review: pass the runner's attn_family to defers_output again
ganyi1996ppo 5661b9f
review: gate PP placeholders on the batch's frozen chunk state
ganyi1996ppo 91e51df
review: release a checkpoint hold only from the chunk that holds it
ganyi1996ppo 1a570a2
review: let a P/D producer under PP keep any drafter
ganyi1996ppo d9f26d5
review: read a forked spec row's source in every column
ganyi1996ppo 7a64ee6
review: keep the spec-path fork assert unless the backend opts in
ganyi1996ppo 7e45c5b
review: refuse TBO under PP and drop the relay's TBO grow branch
ganyi1996ppo c024c82
review: seed ReplaySSM forks from host-known slot pairs, without a sync
ganyi1996ppo cf98ae7
review: write relay captures without bookkeeping Dynamo freezes
ganyi1996ppo cd0d315
review: let the registry declare which spec methods an arch serves un…
ganyi1996ppo e65bb71
review: name the checkpoint hold by what it does
ganyi1996ppo 3a2258b
review: stage the undeferred window from the recorded placeholder width
ganyi1996ppo 50998fd
review: let the drafter read the relayed aux block in place
ganyi1996ppo 81917c0
review: derive DSpark's instance aux spec from its classmethod
ganyi1996ppo 30829a3
review: drop the CLAUDE.md key list and getattrs on always-set fields
ganyi1996ppo 337bcd5
Merge main (a8946ff73) into guanbao/k3-pp2tp4
ganyi1996ppo a1436cd
review: allocate aux buffers only for this stage's own taps
ganyi1996ppo b62bf7a
review: share one tap residency rule between the drafter and the relay
ganyi1996ppo c10069c
Merge main (ab4ada40a) into guanbao/k3-pp2tp4
ganyi1996ppo 9fb45ff
Merge main (058c21957) into guanbao/k3-pp2tp4
ganyi1996ppo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,6 +38,7 @@ | |
| pcp_round_robin_split, | ||
| ) | ||
| from atom.distributed.pp_comm import ( | ||
| PP_AUX_KEY, | ||
| async_send_intermediate_tensors, | ||
| commit_pp_send_work, | ||
| recv_intermediate_tensors, | ||
|
|
@@ -54,7 +55,11 @@ | |
| from atom.model_engine.output_transfer import D2HQueue | ||
| from atom.model_engine.page_unit_checkpoint import PagedStateCheckpointSpec | ||
| from atom.model_engine.run_labels import build_run_label | ||
| from atom.model_engine.scheduler import ScheduledBatch, ScheduledBatchOutput | ||
| from atom.model_engine.scheduler import ( | ||
| ScheduledBatch, | ||
| ScheduledBatchOutput, | ||
| defers_output, | ||
| ) | ||
| from atom.model_engine.sequence import ( | ||
| Sequence, | ||
| SequenceStatus, | ||
|
|
@@ -91,6 +96,7 @@ | |
| from atom.sampling_params import RequiredChannel | ||
| from atom.spec_decode.drafter import Drafter | ||
| from atom.spec_decode.factory import build_drafter | ||
| from atom.spec_decode.pp_aux_relay import PPAuxRelay | ||
| from atom.spec_decode.status import SpecStatusQueue | ||
| from atom.utils import ( | ||
| CpuGpuBuffer, | ||
|
|
@@ -191,17 +197,7 @@ def __init__( | |
| # The scheduler never speculates for a P/D producer, though the runner | ||
| # keeps its drafter loaded: every decode it runs is q=1. | ||
| self.is_pd_producer = _kv_config_has_producer(kv_cfg) | ||
| # P/D hands off prompt-end state plus the first sampled token. Deferred | ||
| # output makes the producer decode once more to surface that token, so | ||
| # the consumer applies T0 to a state that already has it. Only a | ||
| # recurrent state notices -- a paged write lands at T0's own position, | ||
| # so repeating it is idempotent. | ||
| hands_off_recurrent_state = ( | ||
| self.is_pd_producer and runner.attn_family.has_recurrent_state | ||
| ) | ||
| self.is_deferred_out = ( | ||
| not self.is_pipeline_parallel and not hands_off_recurrent_state | ||
| ) | ||
| self.is_deferred_out = defers_output(runner.config, runner.attn_family) | ||
|
|
||
| self.runner = runner | ||
| device = runner.device | ||
|
|
@@ -227,6 +223,8 @@ def __init__( | |
| self.batch_info = CpuGpuBuffer( | ||
| 3, dtype=torch.int32, device=device, publication_group="input_ids" | ||
| ) | ||
| # On every PP stage: each runs the verify windows (draft-id staging, | ||
| # state rollback), though only the last one owns the drafter. | ||
| self.use_spec = use_spec | ||
| self.num_spec_tokens = num_spec_tokens | ||
| # The drafter's `draft_tokens_per_seq`, set once it is loaded. | ||
|
|
@@ -339,6 +337,20 @@ def clean(self): | |
| self.num_rejected: np.ndarray | None = None | ||
| self.num_bonus: np.ndarray | None = None | ||
|
|
||
| def verify_ctx_lens(self, context_lens: np.ndarray) -> np.ndarray: | ||
| """`batch.context_lens` moved back to the start of this verify window. | ||
|
|
||
| Deferred output: the last step's `num_rejected` slots are still in | ||
| `context_lens`. Undeferred (PP): postprocess already left one run of | ||
| `mtp_k + 1` placeholders, so the shift is 1. Before any sampler output | ||
| there is nothing to shift. Only a verify step has a window, so every | ||
| caller sits behind `max_seqlen_q > 1`. | ||
| """ | ||
| if self.num_rejected is None: | ||
| return context_lens | ||
| shift = self.num_rejected if self.is_deferred_out else 1 | ||
| return context_lens - np.asarray(shift, dtype=context_lens.dtype) | ||
|
|
||
| @staticmethod | ||
| def _batch_process_token_ids(token_ids: list) -> list[tuple[int, ...]]: | ||
| """Batch process token_ids: vectorized -1 truncation using numpy.""" | ||
|
|
@@ -555,25 +567,40 @@ def _stage_input_ids( | |
| token_ids = scheduled_tokens[ | ||
| total_tokens_prefill : total_tokens_prefill + total_tokens_decode | ||
| ] | ||
| # No spec path supports pipeline parallel yet; it wants the deferred | ||
| # branch's per-request staging. Gate on PP itself rather than on | ||
| # `use_spec`: a P/D producer also reaches this branch, and the flag | ||
| # is still set there (the runner keeps the drafter loaded to write | ||
| # the draft's context KV at prefill) even though the scheduler | ||
| # turned speculation off for it. | ||
| if self.use_spec and self.is_pipeline_parallel: | ||
| raise NotImplementedError("pipeline parallel + speculative decode") | ||
| # PP verifies nothing locally and a P/D producer never speculates | ||
| # (its DP dummy included), so no row here carries drafts. One that | ||
| # did would need the deferred branch's per-request staging: under | ||
| # DP a block drafter's `max_seqlen_q` is the group's maximum, not | ||
| # this batch's row width. | ||
| assert batch.num_spec_step == 0, ( | ||
| "non-deferred decode got a speculative batch " | ||
| f"(num_spec_step={batch.num_spec_step})" | ||
| ) | ||
|
|
||
| self.input_ids.np[:total_tokens_decode] = token_ids | ||
| # A P/D producer never speculates (its DP dummy included), so no | ||
| # row here carries drafts. One that did would need the deferred | ||
| # branch's per-request staging: under DP a block drafter's | ||
| # `max_seqlen_q` is the group's maximum, not this batch's row | ||
| # width. Under PP only a producer is held to it; a PP consumer | ||
| # verifies its own drafts below. | ||
| if not self.is_pipeline_parallel or self.is_pd_producer: | ||
| assert batch.num_spec_step == 0, ( | ||
| "non-deferred decode got a speculative batch " | ||
| f"(num_spec_step={batch.num_spec_step})" | ||
| ) | ||
| if not self.is_pipeline_parallel: | ||
| return ( | ||
| self._publish_input_ids(total_tokens_decode, publication_group), | ||
| None, | ||
| ) | ||
| if self.use_spec and batch.num_spec_step > 0: | ||
| # PP: the host already holds each row's anchor, so fill only | ||
| # the draft columns. | ||
| bs, lens, cu_np = self.runner.attn_metadata_builder.decode_spans(batch) | ||
| spec = batch.scheduled_spec_decode_tokens[:bs] | ||
| cols = np.arange(spec.shape[1]) | ||
| # Row i's drafts land right after its anchor at cu_np[i]. | ||
| drafted = cols < (lens[:, None] - 1) | ||
| self.input_ids.np[(cu_np[:bs, None] + 1 + cols)[drafted]] = spec[ | ||
| drafted | ||
| ] | ||
| # Previous step's acceptance counts, read off the batch so every | ||
| # stage (not just the sampling one) rolls its linear-attention | ||
| # state back correctly; unset, GDN assumes 1 accepted token. | ||
| if self.use_spec: | ||
| self.num_rejected = batch.num_rejected | ||
| self.num_bonus = batch.num_bonus | ||
| return ( | ||
| self._publish_input_ids(total_tokens_decode, publication_group), | ||
| None, | ||
|
|
@@ -803,7 +830,9 @@ def __init__(self, rank: int, config: Config): | |
| default_dtype = self.config.torch_dtype | ||
| torch.set_default_dtype(default_dtype) | ||
| torch.set_default_device(self.device) | ||
| use_spec = bool(self.config.speculative_config) and get_pp_group().is_last_rank | ||
| # Every PP stage runs the verify window, drafter or not: its width | ||
| # sizes the KDA conv state and MLA work metadata. | ||
| use_spec = bool(self.config.speculative_config) | ||
| self.num_spec_tokens = ( | ||
| self.config.speculative_config.num_speculative_tokens if use_spec else 0 | ||
| ) | ||
|
|
@@ -919,6 +948,7 @@ def __init__(self, rank: int, config: Config): | |
| logger.info("TBO enabled: model wrapped with UBatchWrapper") | ||
| if getattr(self, "drafter", None) is not None: | ||
| self.drafter.arm_aux_capture(self.model) | ||
| self.pp_aux_relay = PPAuxRelay.build(self.config, self.model) | ||
| installer = mono_decode_installers.get(hf_config.architectures[0]) | ||
| if installer is not None: | ||
| self.model = resolve_obj_by_qualname(installer)( | ||
|
|
@@ -3151,7 +3181,7 @@ def run_model( | |
| if pp_enabled: | ||
| self._setup_pp_shared_indexer() | ||
|
|
||
| intermediate_tensors = None | ||
| intermediate_tensors = recv_aux = None | ||
| if pp_enabled and not pp_group.is_first_rank: | ||
| intermediate_tensors = recv_intermediate_tensors() | ||
| # GLM-5.2 IndexShare: load prior rank's top-k for leading | ||
|
|
@@ -3162,6 +3192,11 @@ def run_model( | |
| if recv_sparse is not None and self._pp_recv_needs_sparse: | ||
| tgt = self.attn_metadata_builder._sparse_kv_indices_gpu | ||
| tgt[: recv_sparse.numel()].copy_(recv_sparse) | ||
| # DSpark aux hidden states from target layers on earlier | ||
| # stages. Popped for the same reason as the top-k above. | ||
| recv_aux = intermediate_tensors.tensors.pop(PP_AUX_KEY, None) | ||
| if self.pp_aux_relay is not None and pp_group.is_last_rank: | ||
| self.pp_aux_relay.absorb(recv_aux, self.drafter) | ||
|
|
||
| # Time only the chunk computation between PP receive and send. | ||
| chunk_sample = self._dynamic_chunking.start_sample(batch) | ||
|
|
@@ -3188,10 +3223,18 @@ def run_model( | |
| model_output.tensors["sparse_kv_indices"] = ( | ||
| self.attn_metadata_builder._sparse_kv_indices_gpu[:n] | ||
| ) | ||
| if self.pp_aux_relay is not None: | ||
| aux = self.pp_aux_relay.pack( | ||
| recv_aux, | ||
| model_output.tensors["hidden_states"].shape[0], | ||
| ) | ||
| if aux is not None: | ||
| model_output.tensors[PP_AUX_KEY] = aux | ||
| if self._pp_pending_send: | ||
| commit_pp_send_work(self._pp_pending_send) | ||
| # The relay hands its payload over: no clone needed. | ||
| self._pp_pending_send = async_send_intermediate_tensors( | ||
| model_output | ||
| model_output, owned=(PP_AUX_KEY,) | ||
| ) | ||
| hidden_states = None | ||
| logits = None | ||
|
|
@@ -3521,8 +3564,16 @@ def postprocess( | |
| prev_rejected_num = np.zeros(0, dtype=np.int32) | ||
| prev_bonus_num = np.zeros(0, dtype=np.int32) | ||
| else: | ||
| prev_rejected_num = np.zeros(batch.total_seqs_num, dtype=np.int32) | ||
| prev_bonus_num = np.zeros(batch.total_seqs_num, dtype=np.int32) | ||
| # Undeferred: report THIS step's counts; the next batch carries them | ||
| # back for linear-attention state rollback. Zeros if no drafts scored. | ||
| if spec_decode_metadata is not None: | ||
| # From the host ids, no D2H: accepted drafts + 1, then -1s. | ||
| emitted = np.fromiter(map(len, token_ids_out), np.int32) | ||
| prev_bonus_num = emitted - 1 | ||
| prev_rejected_num = sampled_tokens.shape[-1] - emitted | ||
| else: | ||
| prev_rejected_num = np.zeros(batch.total_seqs_num, dtype=np.int32) | ||
| prev_bonus_num = np.zeros(batch.total_seqs_num, dtype=np.int32) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. two D2H syncs per step here, stack and do one .cpu()? |
||
| # PP stages and P/D producers still run the drafter. | ||
| if hasattr(self, "drafter"): | ||
| # Mid-prompt sequences get their anchor corrected inside | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing the
NotImplementedError("pipeline parallel + speculative decode"), together withspec_decode_local = self.use_specin the scheduler, opens PP for every model and every spec method. Only K3 + DSpark is wired, and nothing refuses the rest at config time:PPAuxRelay.buildonly knows DSpark. With some aux layers on earlier stages, the last stage'szip(..., strict=True)raises. With all of them earlier, the aux buffers stay zero and acceptance silently collapses.gdn_attn.py:1223's assert mid-serving. Before, it was refused cleanly at the first decode.target_base.model.embed_tokens.num_embeddingsis read on aPPMissingLayer, giving an AttributeError.Please allow only the supported (model, method) pairs at config time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 549ae77. Under
pipeline_parallel_size > 1, config now refuses every (model, spec method) pair except Kimi-K3 + DSpark, so EAGLE3, Qwen3-Next / Qwen3.5 MTP and MiniMax-M3 DSpark fail at startup. The DSpark confidence scheduler and a P/Dkv_consumerare refused too. Tests:tests/test_pp_config_refusals.py.