Repository navigation
Conversation
6bcb0de to
24945f1
Compare
| # The drafter runs on the last stage and shares this table, and not | ||
| # every draft checkpoint (Kimi-K3.1-DSpark) ships its own. | ||
| pp = get_pp_group() | ||
| if pp.is_first_rank or ( |
There was a problem hiding this comment.
this is the actual fix but nothing tests it? add a cpu test with/without speculative_config, last rank stub.
There was a problem hiding this comment.
Moved the check to atom.models.utils.pp_holds_embedding so a CPU test can reach it; test_pp.py::test_pp_holds_embedding covers first/middle/last stage with and without a speculative config (7d3fa50).
| target = _FakeTarget(8, (4, 8), with_embed=False) | ||
| # The last stage holds an embedding copy for its drafter, yet the `-1` | ||
| # tap still arrives through the relay. | ||
| target = _FakeTarget(8, (4, 8), with_embed=True) |
There was a problem hiding this comment.
this makes two comments stale now: _FakeTarget at line 39 and the pp_aux_relay docstring. please update both.
…P stage Kimi-K3.1-DSpark ships no embed_tokens, so under PP the last stage's draft loaded nothing into the embedding it built for itself. The target now keeps its embedding on the last stage when a drafter runs there, and the draft shares it on every stage, as it already did without PP. The `-1` aux tap stays first-stage only: the last stage's copy never runs the target forward. 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>
66a0e10 to
63e2c61
Compare
valarLip
left a comment
There was a problem hiding this comment.
The direction is right: under PP the target keeps a real embedding on the last stage and the draft always shares it, which removes #2365's own-embed build/load path and fixes K3.1-DSpark drafting from an unloaded embedding when the draft checkpoint has no embed_tokens. Deciding the -1 tap by stage rather than by module type is also correct, since the last stage's copy is never run by the target forward. I found nothing that breaks (106 + 85 tests pass, 1 skipped, on this head; ruff/black clean). The items below are about where the decisions live and what the tests cover.
Action items for the author
- [F1] Keep
tap_is_residentstructural: decide-1bylayers[0]instead of reading global PP state (drafter.py:123). - [F2] Make
pp_holds_embeddingfollow whether the drafter actually shares the embedding, notspeculative_config is not None(utils.py:139). - [F3] One way to decide "this stage holds the embedding" across models, tied to
pp_speculative_methodsor checked in PP validation (utils.py:146). - [F4] Re-wrap the module docstring (pp_aux_relay.py:10).
- [F5] Remove the now-inert
with_embedknob from_FakeTarget(test_pp_aux_relay.py:40). - [F6] Test the regression actually fixed: on the last stage the draft's
embed_tokensis the target's and is not aPPMissingLayer(test_pp.py:224). - [F7] Minor: confirm the
skip_weight_prefixestuple -> list change is intentional (kimi_k3_dspark.py:547).
| else: | ||
| return False | ||
| return not isinstance(module, PPMissingLayer) | ||
| return get_pp_group().is_first_rank |
There was a problem hiding this comment.
[F1] tap_is_resident used to be purely structural; for -1 it now reads global get_pp_group().is_first_rank. not isinstance(layers[0], PPMissingLayer) (the embedding runs on the stage that owns layer 0) gives exactly the same answer and keeps it structural. PPAuxRelay.__init__ already has pp and reads it again through this helper, and the tests need a new _pp_group ExitStack patching get_pp_group in both drafter and pp_aux_relay (test_pp_aux_relay.py:54-61); with layers[0] those patches go away.
There was a problem hiding this comment.
Done: -1 is resident where layer 0 is, so the helper reads no PP state and the double get_pp_group patch is gone (2026075).
| return start_layer, end_layer, modules | ||
|
|
||
|
|
||
| def pp_holds_embedding(atom_config) -> bool: |
There was a problem hiding this comment.
[F2] speculative_config is not None stands in for "the drafter on this stage shares the target's embedding", so the target model is guessing the drafter's behaviour. EAGLE-style drafters keep their own embedding under PP (drafter.py:643 guards on world_size == 1); once pp_speculative_methods admits eagle/mtp, the last stage would silently build and load a whole vocab x hidden table for nothing (about 2.35 GB for K3, ~590 MB per GPU at TP4). The condition should follow whether the drafter shares the embedding (e.g. share_with_target, or a registry declaration), not just whether speculation is on.
There was a problem hiding this comment.
Done: the last stage builds the embedding only when registry.serves_pp_speculation holds, i.e. the configured method is declared in pp_speculative_methods, whose comment now says each declared method shares the embedding. An undeclared eagle/mtp builds nothing (f65a847).
| from aiter.dist.parallel_state import get_pp_group | ||
|
|
||
| pp = get_pp_group() | ||
| return pp.is_first_rank or ( |
There was a problem hiding this comment.
[F3] This helper sits in the model-wide utils but only Kimi-K3 uses it; MiniMax-M3 (also a DSpark that shares the embedding) still writes is_first_rank directly, so the repo has two spellings of one concept. Any architecture later added to pp_speculative_methods must remember to switch its embedding construction to this helper, or its draft gets a PPMissingLayer on the last stage again, and that constraint is neither in the registry nor checked anywhere. Please tie it to pp_speculative_methods, or have PP validation check the draft received a real embedding.
There was a problem hiding this comment.
Done in f65a847: MiniMax-M3 builds through pp_holds_embedding too, and _check_pp_support uses the same serves_pp_speculation. The drafter also checks after share_with_target: a draft that got a PPMissingLayer fails at load, naming the registry and the helper.
| stages would never fire: their layers are ``PPMissingLayer``, and the last | ||
| stage's copy of the embedding (held for the drafter) is not run by the target | ||
| forward. That would leave zero aux buffers and garbage drafts. Earlier stages capture their own taps and forward them in the | ||
| intermediate-tensor payload, concatenated along the hidden axis in the |
There was a problem hiding this comment.
[F4] The rewritten docstring was not re-wrapped: this line is 124 characters while the surrounding lines stay within 78. black/ruff do not check docstring width, so CI won't catch it; please re-wrap to match.
| ) | ||
| # Like the real models: off the first stage the embedding is missing. | ||
| # Middle stages hold no embedding; the last may hold a drafter's copy. | ||
| self.embed_tokens = nn.Identity() if with_embed else PPMissingLayer() |
There was a problem hiding this comment.
[F5] With -1 no longer decided from target.embed_tokens, with_embed (passed as with_embed=first in _relay) is a knob that changes nothing; any value gives the same result, and only test_last_stage_arms_only_its_own_taps uses it, to assert that holding an embedding does not add a hook. It suggests to readers that relay behaviour depends on the embed module. Please drop the parameter and let _FakeTarget always hold an embedding (or follow F1).
There was a problem hiding this comment.
Dropped; _FakeTarget always holds an embedding now (2026075).
| (0, 1, True, True), | ||
| ], | ||
| ) | ||
| def test_pp_holds_embedding(monkeypatch, pp_rank, pp_size, speculative, holds): |
There was a problem hiding this comment.
[F6] This enumerates a three-line boolean helper with 7 parameter sets but does not cover the regression this PR fixes: KimiLinearModel building and loading the embedding on the last stage, and KimiK3DSpark sharing the target's embedding under PP instead of building its own. If someone reverts kimi_k3/model.py to get_pp_group().is_first_rank, or adds an own-embed branch back to the draft, this stays green while K3.1 drafts from an unloaded embedding again. Please test that on the last stage draft.embed_tokens is target.model.embed_tokens and is not a PPMissingLayer.
There was a problem hiding this comment.
Added in 9f3b5be: the K3 draft binds the target's embedding object through share_with_target, and a draft left with a PPMissingLayer is refused at load. A revert of kimi_k3/model.py to is_first_rank now fails that load check at startup instead of drafting from an unloaded table; building a whole KimiLinearModel in a unit test needs the TP group and aiter, so I stopped there.
| # Under PP, __init__ drops embed_tokens from this list on non-first stages. | ||
| # A tuple, not a ClassVar: the per-stage override rebinds, never mutates. | ||
| skip_weight_prefixes: tuple[str, ...] = ( | ||
| skip_weight_prefixes: ClassVar[list[str]] = [ |
There was a problem hiding this comment.
[F7] Minor: this goes back from a tuple to a mutable ClassVar[list[str]] shared by all instances. The tuple only existed for the removed per-stage rebinding and other models use a list, so this is fine if the intent is to match the repo convention; just confirming it is deliberate rather than a mechanical revert.
There was a problem hiding this comment.
Deliberate: the tuple only existed for the per-stage rebinding this PR removes, and a list matches the other drafts.
|
ci triage for 703592a:
|
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jasen2201
left a comment
There was a problem hiding this comment.
PP is primarily used for prefill in our P/D deployments. The DSpark prefill producer needs to populate draft context KV, but does not need to generate candidate tokens. We should avoid adding an extra embedding on the last PP stage for this case, since it consumes GPU memory unnecessarily (~2.35 GB in BF16 for K3, or ~0.59 GB per GPU when sharded at TP4).
A simple fix, following #3038:
- Keep the target embedding on the first stage, and only create the last-stage copy when that stage actually needs to generate draft tokens.
- For a context-only P/D producer, validate the vocabulary using
lm_headand allowdraft.embed_tokens = None. - Skip producer proposal and draft graph warmup, while retaining draft context-KV writes and required state/bookkeeping updates (including
commit_speculative_state()).
Please add a regression test for the PP producer without a last-stage embedding, alongside ordinary PP speculation that still requires one.
There was a problem hiding this comment.
⚠️ Action required before merge (2)
- Reply to Jasen2201's P/D-producer point. Main already builds a draft-owned embedding of the same size on the last stage under PP, so this PR adds no memory. Skipping it for a context-only
kv_produceris a reasonable follow-up. Say whether you'll do it here or open an issue. - Get a real CI run. The only full heavy run at this head (42749859685, 11-10) failed in every model on infra (
flydsl0.4.2 in the image, >=0.5.0 needed). The 11-11 run was gate-only. Re-run ATOM Test, including theKimi-K3 DSpark PP2job, and confirm it is green.
Re-reviewed at 703592a. Already fixed: F1-F7 (layers[0] residency, registry-driven serves_pp_speculation, MiniMax shares the helper, load-time PPMissingLayer refusal, docstring, no with_embed, regression test).
|
@Jasen2201 Agreed a context-only producer needs no embedding: the K3 draft only embeds in |
valarLip
left a comment
There was a problem hiding this comment.
⚠️ Action required before merge (2)
- Make the load-time
PPMissingLayerrefusal reachable, or delete it.atom/models/kimi_k3_dspark.py:614now readstarget_base.model.embed_tokens.num_embeddingsbefore it binds the embedding.PPMissingLayeris a barenn.Identity(atom/models/utils.py:29) with nonum_embeddings, soshare_with_targetraisesAttributeErrorbeforecheck_shared_embedding(atom/spec_decode/drafter.py:694) can run. MiniMax's draft (minimax_m3/dspark.py:508) reads it the same way. The DeepSeek drafts bindembed, notembed_tokens. So no real draft can reach the guard, and the reply to F6 ("a revert … fails that load check") is not what happens. Fix: restoretarget_vocab = target_base.model.vocab_size(main's line;KimiLinearModel.vocab_sizeis set atkimi_k3/model.py:1810) so the bind happens and the guard fires with its message. Also add a test that calls the realshare_with_targetwith aPPMissingLayertarget embedding and expects theTypeError. Otherwise dropcheck_shared_embeddingandtest_a_draft_sharing_a_missing_embedding_is_refused(tests/test_pp_aux_relay.py:296). - Get the heavy CI at this head green. Run 43149686406 at b12cbdf is still queued for 9 jobs,
Kimi-K3 DSpark PP2among them, the job that exercises this change. Merge only after it passes. (valarLip's ask 2 from 11-12 is still open.)
Jasen2201's P/D-producer memory point: answered and deferred to a follow-up PR on top of #3038, which is fine (valarLip ask 1 is done).
Re-reviewed at b12cbdf. Already fixed: F1-F7 (layers[0] residency, registry-driven serves_pp_speculation, MiniMax shares the helper, docstring re-wrap, no with_embed, regression test, list prefixes), reply to the P/D-producer point.
| """ | ||
| # Not embed_tokens.num_embeddings: under PP it may be a PPMissingLayer. | ||
| target_vocab = target_base.model.vocab_size | ||
| target_vocab = target_base.model.embed_tokens.num_embeddings |
There was a problem hiding this comment.
this refusal is unreachable: share_with_target reads target_base.model.embed_tokens.num_embeddings first, and a PPMissingLayer embed raises AttributeError there, so check_shared_embedding never fires. use target_base.model.vocab_size like main? and add a test with a real PPMissingLayer target embed..
There was a problem hiding this comment.
Fixed in ac819d3: the vocab check reads target_base.model.vocab_size again, so the bind happens and check_shared_embedding refuses the PPMissingLayer. test_a_k3_draft_on_a_stage_without_the_embedding_is_refused goes through the real share_with_target with a PPMissingLayer target embedding and fails if the old embed_tokens.num_embeddings read comes back.
…fires Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@valarLip 1. Fixed in ac819d3 (details in the inline reply): the vocab check reads |
|
A note from #3038 (MiniMax-M3.1 PP4 P/D with DSpark, in review), since both PRs edit the same #3038 makes context-only P/D producers skip
If #3023 lands first, we will make both changes when rebasing #3038. |
valarLip
left a comment
There was a problem hiding this comment.
⚠️ Action required before merge (1)
- Get the heavy CI at this head green before merging. ATOM Test run 43232417010 at 6ca42d1 has passed only its gate and smoke jobs. All 14 accuracy jobs are still queued,
Kimi-K3 DSpark PP2among them, and that is the only job that exercises this change end to end. Merge once it passes. If DeepSeek-V4-Pro DSpark fails again withVllmBackend can only be called once, link the same failure on main so it is clearly unrelated. (valarLip's CI ask is still open.)
The code itself is done: I found nothing else to change at this head. Note for #3038: whichever PR lands second must keep check_shared_embedding(self.model) inside the if not self.context_kv_only: branch of Drafter.load_model and make pp_holds_embedding return False on a P/D producer, as Jasen2201 noted.
Re-reviewed at 6ca42d1. Already fixed: the load-time PPMissingLayer refusal is now reachable (kimi_k3_dspark.py:616 reads target_base.model.vocab_size, and test_a_k3_draft_on_a_stage_without_the_embedding_is_refused goes through the real share_with_target). Also fixed: F1-F7 and the reply to the P/D-producer point.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No DSpark draft loads its own embedding any more, so share_with_target binds the target's and the drafter's load check refuses a PPMissingLayer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Motivation
ARCH_ALIASESmapsKimiK31ForConditionalGenerationonto Kimi-K3, so the Kimi-K3 PP DSpark path (#2365) also accepts Kimi-K3.1. Under PP, the draft on the last stage loaded the embedding from its own checkpoint.Inferact/Kimi-K3.1-DSparkhas noembed_tokens, and the loader does not fail on a missing weight, so the draft would run with an embedding that was never loaded.Technical Details
KimiLinearModelkeeps a realembed_tokenson the last stage whenspeculative_configis set, so the target checkpoint's table loads there.KimiK3DSparkalways shares the target's embedding, the same as without PP, and drops its per-stage own-embedding path.tap_is_resident(-1)is now true only on the first stage. The last stage's copy of the embedding never runs in the target forward, so the-1tap must still come through the relay.Test Plan
tests/test_pp_aux_relay.py: the last-stage test now holds a real embedding copy and checks that the-1tap still gets no buffer and no hook.test_pp.py,test_dspark.py,test_kimi_k31_config.py,models/test_kimi_k3_mono_plan.py,test_gdn_state_fork.py.Test Result
All of the files above pass in the
atom_1031container (24/99/82/3/67/7 passed). Full GSM8K (1319), Kimi-K3 pp2 × tp4 DSpark with prefix caching and ptpc_fp8 (theKimi-K3 DSpark PP2CI flags), at83e3223df(this commit before the rebase ontomain): 0.9712 / 0.9719 flexible / strict (±0.0046), acceptance 53.60% (4.75 tok/fwd), against 0.9704 / 53.7% for #2365 atf34cb8a8e. The relay and ReplaySSM markers match (4 / 8 lines) and the server log has no missing-weight warning. Kimi-K3.1 is not run: no local checkpoint.🤖 Generated with Claude Code