Skip to content

[OMNIML-6078] [2/2] Add the Kimi-K2.5 expert-parallel adapter - #2748

Open
shengliangxu wants to merge 17 commits into
shengliangx/hfep-11b0-kimi-vlfrom
shengliangx/hfep-11b-kimi-k25-vl
Open

shengliangxu wants to merge 17 commits into
shengliangx/hfep-11b0-kimi-vlfrom
shengliangx/hfep-11b-kimi-k25-vl

Conversation

@shengliangxu

@shengliangxu shengliangxu commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Note

Split at the reviewers' request (2026-10-30): this PR is now [2/2], the Kimi-K2.5 adapter.

Base: #3138. The diff below is this PR's alone, and it merges after #3138.

Series layout

What does this PR do?

Type of change: New feature

Kimi-K2.5 (and Kimi-K2.6 and Kimi-K2.7-Code, which share its model type) is Kimi-VL's shape with compressed-tensors INT4 experts. Its adapter swaps in transformers' native deepseek_v3 language model as Kimi-VL's does (#3138), through the patch they share, and decompresses the experts as they load (#3137, #2747).

  • Install order. The checkpoint's config.json keeps the compressed-tensors config under text_config, and its config class also copies it to the top level, which the expert-parallel load drops. prepare installs the decompressor first of all, so a checkpoint the decompressor refuses leaves the config and the loader untouched.
  • Which checkpoints decompress. It installs the decompressor only for a checkpoint whose quantization_config declares compressed-tensors, as Kimi-K2.5, K2.6 and K2.7-Code all do.
    • A bf16 checkpoint of the model type loads as Kimi-VL's does.
    • Any other scheme is left to the expert-parallel load's format handling, which reads the copy of quantization_config the config class keeps on the top level.
    • A scheme on text_config alone, with no top-level copy, is refused before anything changes.
  • transformers floor. The spec declares the transformers 5.8 the decompression needs.

prepare now runs only on the expert-parallel route. Before, the plan decision ran a model spec's prepare on a copy of the config before the plan was known, and the Kimi patches it made (the swapped language-model class, the decompressor's loader hooks) stayed in the process when the plan came out FSDP2-sharded, or whole and refused. Now:

  • the decision reads the plan the spec states (prepared_ep_plan, on text_config for both Kimi specs) and finds the experts through it in the model the checkpoint's own code builds;
  • before that it runs only EPSpec.make_buildable, what the checkpoint's own code needs to import and build on the installed transformers: the import shims, the staging of the code's two-deep imports, and the VLM class's tie_weights adapted to the keyword arguments post_init passes it. It changes nothing the model computes or loads, and patches no MoE or expert class, config, weight converter or weight-loading hook;
  • only an expert-parallel plan then runs prepare, on every rank, and returns that config; any other plan returns the config as read.

EPSpec.prepare's, prepared_ep_plan's and resolve_distribution_plan's docs say so, replacing #2746's interim contract. Kimi-VL's spec (#3138) takes it here too, and Kimi-K3 (#2749), now on this PR, uses it.

Why transformers' own Kimi-K2.5 port is refused. transformers 5.13 added a Kimi-K2.5 port, which a load without trust_remote_code builds. Its language model is a native deepseek_v3 with fused experts, so expert parallelism could run on it without these patches. This adapter does not target it, for three reasons:

  • Different names. The port re-implements the vision tower and the projector under other names. Its load-time conversions rename the vision blocks' norms, MLPs and output projection and the projector, and split the fused wqkv into q/k/v. The export would have to map all of them back to the checkpoint's names for vLLM.
  • Not validated. Expert-parallel PTQ has been validated end to end only on the checkpoint's own code.
  • Needed anyway. The adapter is needed from 5.8 either way, before the port exists.

So the adapter refuses the port's config before it changes anything, naming trust_remote_code=True from a checkpoint whose auto_map names its own AutoConfig as the way in. Supporting the port is a follow-up that needs its own run on the real checkpoints. The spec records the 5.13 release.

Why it patches the checkpoint's module, and again on every class resolution: see #3130, where keep_patched lives.

  • The VLM class builds its language model from a name it looks up in its module's globals.
  • transformers re-executes or re-imports that module under the loaders that resolve the class.

Usage

Picked automatically by apply_distributed_support for kimi_k25 configs under torchrun; see #2754.

Testing

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ Additive; runs only on the expert-parallel path.
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: N/A. The feature's entry lands with [OMNIML-6085] [3/3] Run expert-parallel PTQ from hf_ptq.py #2754.
  • Did you get Claude approval on this PR?: ❌ Not since the split.

Additional Information

Part of expert-parallel PTQ for Hugging Face MoE models, [2/2] of #2748's split. It merges after #3138.

@copy-pr-bot

copy-pr-bot Bot commented Oct 12, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
Walkthrough

Adds model registration and expert-parallel preparation for Kimi-K2.5 and Kimi-VL. Both adapters convert the text configuration to a native DeepSeek-V3 configuration and apply an expert-parallel plan. Kimi-K2.5 also installs expert decompression. Each adapter can patch remote modeling code when a checkpoint path is provided.

Changes: Kimi expert-parallel adapters

Layer / File(s) Summary
Model registration and shared EP plan
modelopt/torch/models/__init__.py, modelopt/torch/models/deepseek_v3/specs.py, modelopt/torch/models/kimi_k25/*, modelopt/torch/models/kimi_vl/*, tests/unit/torch/utils/test_resolve_distribution_plan.py
Registers both Kimi model packages and exposes the DeepSeek-V3 EP plan for their specifications. Tests check specification registration and EP plan resolution for remote Kimi MoE experts.
Kimi-K2.5 preparation
modelopt/torch/models/kimi_k25/ep.py, tests/unit/torch/models/test_kimi_k25_ep.py
Converts text_config, installs expert decompression, and applies the EP plan. When a checkpoint path is provided, replaces the remote modeling class with the native Transformers class. Tests cover config preparation, remote-code patching, and trust validation.
Kimi-VL preparation
modelopt/torch/models/kimi_vl/ep.py, tests/unit/torch/models/test_kimi_vl_ep.py
Adds compatibility shims, converts text_config, and applies the EP plan. When a checkpoint path is provided, replaces the remote modeling class and adapts tie_weights. Tests cover preparation, patching, and validation.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant HFConfig
  participant KimiPrepare
  participant DeepseekV3Config
  participant DeepseekV3EPPlan
  participant RemoteModelingModule
  HFConfig->>KimiPrepare: provide text_config
  KimiPrepare->>DeepseekV3Config: convert text_config
  KimiPrepare->>DeepseekV3EPPlan: apply expert-parallel plan
  opt ckpt_path provided
    KimiPrepare->>RemoteModelingModule: load remote code and replace DeepseekV3ForCausalLM
  end
Loading
Merge Risk: Low · up to e0b3f

This PR registers the Kimi expert-parallel adapters. The remaining concerns are limited to package re-export and Kimi-K2.5 metadata conventions; no resulting user-facing failure is established, so the change is mergeable with bounded follow-up.

Pre-merge checks: Passed 5
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Security Anti-Patterns Passed No listed security anti-pattern was introduced. The Kimi adapters define trust_remote_code: bool = False, reject checkpoint loading when it is false, and pass the caller-controlled value to the dyna…
Title check Passed The title accurately identifies the Kimi-K2.5 expert-parallel adapter, which is a major part of the changeset. It does not mention the additional Kimi-VL adapter, but the title remains clear and relat…
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Full details: Security Anti-Patterns

Explanation

No listed security anti-pattern was introduced. The Kimi adapters define trust_remote_code: bool = False, reject checkpoint loading when it is false, and pass the caller-controlled value to the dynamic loader. No added torch.load(..., weights_only=False), numpy.load(..., allow_pickle=True), eval, exec, or # nosec usage appears in the Python diff. No dependency-file changes were made.

Full details: Title check

Explanation

The title accurately identifies the Kimi-K2.5 expert-parallel adapter, which is a major part of the changeset. It does not mention the additional Kimi-VL adapter, but the title remains clear and related.

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
Fix failing CI checks
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@shengliangxu
shengliangxu added this pull request to stack #2755 October 12, 2026 00:57
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11a-onfly-decompress branch from 95832dd to e243f04 Compare October 12, 2026 02:35
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11b-kimi-k25-vl branch from f73bc4f to 0098f3a Compare October 12, 2026 02:35
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://NVIDIA.github.io/Model-Optimizer/pr-preview/pr-2748/

Built to branch gh-pages at 2026-10-27 00:07 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.38843% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.52%. Comparing base (17f4119) to head (e83762c).

Files with missing lines Patch % Lines
modelopt/torch/models/_kimi.py 84.90% 8 Missing ⚠️
Additional details and impacted files
@@                            Coverage Diff                            @@
##           shengliangx/hfep-11a-onfly-decompress    #2748      +/-   ##
=========================================================================
+ Coverage                                  62.46%   62.52%   +0.05%     
=========================================================================
  Files                                        658      665       +7     
  Lines                                      74106    74227     +121     
=========================================================================
+ Hits                                       46294    46407     +113     
- Misses                                     27812    27820       +8     
Flag Coverage Δ
unit 62.52% <93.38%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 0098f3a

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11a-onfly-decompress branch from e243f04 to d6afd32 Compare October 12, 2026 08:39
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11b-kimi-k25-vl branch from 0098f3a to 3d6fb9d Compare October 12, 2026 08:39
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 3d6fb9d

@shengliangxu shengliangxu changed the title [13/19] Add the Kimi-K2.5 and Kimi-VL expert-parallel adapters [OMNIML-6078] [13/19] Add the Kimi-K2.5 and Kimi-VL expert-parallel adapters Oct 12, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11a-onfly-decompress branch from d6afd32 to 67355f4 Compare October 13, 2026 00:53
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11b-kimi-k25-vl branch 2 times, most recently from 1c16b42 to ecf9d71 Compare October 13, 2026 01:32
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11a-onfly-decompress branch from 67355f4 to b162532 Compare October 13, 2026 01:32
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test ecf9d71

@shengliangxu
shengliangxu removed this pull request from stack #2755 October 13, 2026 17:42
@shengliangxu
shengliangxu added this pull request to stack #2775 October 13, 2026 17:43
@shengliangxu shengliangxu changed the title [OMNIML-6078] [13/19] Add the Kimi-K2.5 and Kimi-VL expert-parallel adapters [OMNIML-6078] [13/20] Add the Kimi-K2.5 and Kimi-VL expert-parallel adapters Oct 13, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11b-kimi-k25-vl branch from ecf9d71 to d525e64 Compare October 13, 2026 19:38
@shengliangxu
shengliangxu removed this pull request from stack #2775 October 13, 2026 19:38
@shengliangxu
shengliangxu added this pull request to stack #2781 October 13, 2026 19:40
@shengliangxu shengliangxu changed the title [OMNIML-6078] [13/20] Add the Kimi-K2.5 and Kimi-VL expert-parallel adapters [OMNIML-6078] [15/21] Add the Kimi-K2.5 and Kimi-VL expert-parallel adapters Oct 13, 2026
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test d525e64

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11a-onfly-decompress branch from 269d3d2 to dddc2ee Compare October 13, 2026 21:12
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11b-kimi-k25-vl branch from d525e64 to 0c39a27 Compare October 13, 2026 21:12
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 0c39a27

@shengliangxu
shengliangxu removed this pull request from stack #2781 October 14, 2026 00:13
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11a-onfly-decompress branch from dddc2ee to c668ec2 Compare October 14, 2026 00:14
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 27961b6

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

Comment thread modelopt/torch/models/_kimi.py Outdated
Comment on lines +139 to +143
make_importable()
if type(hf_config).__module__.startswith("transformers.") or ckpt_path is None:
return
if not trust_remote_code:
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] make_importable() runs ahead of all three guards, so the two process-wide installs it performs (install_compat_import_shims, and the get_cached_module_file wrap in stage_full_remote_closure) happen even for a config of transformers' own port, a call with no ckpt_path, and a load without trust_remote_code — the three cases the docstring describes as having "nothing of the checkpoint's to build", and the three where prepare refuses before touching anything.

Nothing breaks today (the shims only re-add names transformers dropped, and the staging wrap is a no-op for a non-remote-code load), but it is the one place in these adapters where a mutation precedes the check that says it is not needed, and this PR's whole premise is that the pre-decision step leaves a process that will not run expert-parallel untouched. Moving the call below the guards keeps the code matching the docstring:

    if type(hf_config).__module__.startswith("transformers.") or ckpt_path is None:
        return
    if not trust_remote_code:
        return
    make_importable()
    get_class_from_dynamic_module(...)

All three early-return cases are ones where the checkpoint's own code is never imported, so nothing downstream needs the shims there.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taken in fab6ddf. make_buildable now returns before installing anything when there is nothing of the checkpoint's to build: a config of transformers' own port, no checkpoint path, or code the load may not run. New test test_the_build_step_installs_nothing_when_there_is_nothing_to_build[no-checkpoint|untrusted] fails with the shims first.

Comment on lines +93 to +95
quantization_config = _onfly_decompress._find_quantization_config(hf_config) or {}
if quantization_config.get("quant_method") == "compressed-tensors":
_onfly_decompress.install_onfly_decompress(hf_config)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] "compressed-tensors" is now stated twice and the two must agree: here, as the condition that actually installs the decompressor, and in specs.py as decompressed_formats=("compressed-tensors",), which _ep_load_refusal reads before prepare runs to decide the checkpoint is one the expert-parallel load takes. If they ever drift, the failure is silent in one direction (the decision plans expert-parallel for a format prepare then leaves compressed, and the load fails later) and invisible in the other (a format prepare would decompress is planned without expert parallelism).

The new test already compensates by deriving its expectation from get_spec("kimi_k25").ep_spec.decompressed_formats rather than from the literal — worth making the code share the same single definition, since specs.py already imports from this module:

# ep.py
DECOMPRESSED_FORMATS = ("compressed-tensors",)
...
    if quantization_config.get("quant_method") in DECOMPRESSED_FORMATS:
        _onfly_decompress.install_onfly_decompress(hf_config)

# specs.py
from .ep import DECOMPRESSED_FORMATS, make_buildable, prepare
...
            decompressed_formats=DECOMPRESSED_FORMATS,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taken in 777ac2a. kimi_k25.ep.DECOMPRESSED_FORMATS is the one definition: prepare installs the decompressor for those formats, and the spec's decompressed_formats is that tuple.

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 777ac2a

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

Comment thread modelopt/torch/models/_kimi.py Outdated
Comment on lines +147 to +149
for name, module in list(sys.modules.items()):
if name.endswith(f".{modeling}") and module is not None:
_shim_tie_weights(module, vlm_class)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[IMPORTANT Compatibility] This hand-rolled sys.modules scan drops the transformers_modules. prefix guard that _remote_code._apply exists to apply — and the hazard it guards is named for this model type.

_remote_code.py:113-128:

def _apply(suffix: str, patch: Callable[[ModuleType], bool]) -> dict[str, bool]:
    """...
    Only remote code: transformers' own port of a model can share the remote module's name, as
    transformers.models.kimi_k25.modeling_kimi_k25 does from 5.13.
    """
    remote = f"{dmu.TRANSFORMERS_DYNAMIC_MODULE_NAME}."
    ... if module is not None and name.startswith(remote) and name.endswith(suffix)

transformers.models.kimi_k25.modeling_kimi_k25 ends with .modeling_kimi_k25, so on transformers ≥ 5.13 this loop reaches transformers' own module whenever it happens to be in sys.modules — which the early type(hf_config).__module__.startswith("transformers.") return does not prevent, since that only checks the config this load was handed, not what else the process has imported (a prior load, or anything that touched transformers.models.kimi_k25).

Why it matters: _shim_tie_weights is a no-op only as long as the port's KimiK25ForConditionalGeneration has no tie_weights in its own vars(). If it does — now or in a later release — make_buildable rebinds it to a wrapper that silently swallows recompute_mapping=False/missing_keys, permanently, for every later load of transformers' native Kimi-K2.5 in that process. That is exactly the "changes nothing the model computes or loads" guarantee this function's docstring makes, broken outside the adapter's own model. patch_language_model_to_native doesn't have the exposure because it goes through keep_patched → _apply.

Fix — reuse the guarded helper instead of re-deriving the loop (one import plus one line, and it drops the function-local import sys):

from ._remote_code import _apply, keep_patched, stage_full_remote_closure
...
    _apply(f".{modeling}", lambda module: _shim_tie_weights(module, vlm_class))

A regression test is cheap here: register a transformers.models.kimi_k25.modeling_kimi_k25 stand-in in sys.modules carrying a KimiK25ForConditionalGeneration with its own zero-arg tie_weights, run kimi_k25.make_buildable(...), and assert the attribute is unchanged — the mirror of test_the_swap_leaves_a_sibling_remote_module_alone, which already pins the same scoping property for the swap.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taken in f3df820. make_buildable now applies the tie_weights shim through _remote_code._apply, which reaches remote modules only, as the swap does. New test test_the_build_step_leaves_transformers_own_port_alone registers a transformers.models.kimi_k25.modeling_kimi_k25 stand-in with its own zero-argument tie_weights and checks that the step leaves it alone while it adapts the remote one. It fails with the unscoped scan. This also covers the summary comment's one action item.

@claude

claude Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Claude review — [2/2] Add the Kimi-K2.5 expert-parallel adapter · head 777ac2a3 · baseline 3b5d7e2e (.agents/review-guidelines/code-review.md at BASE SHA)

Recommendation: comment. One scoping issue in the new build step; the prepare move itself traces correctly and is well covered.

Needs action:

  • Scope the sys.modules scan in make_buildable to remote code (modelopt/torch/models/_kimi.py:147-149) — it drops the transformers_modules. prefix guard that _remote_code._apply applies precisely because transformers.models.kimi_k25.modeling_kimi_k25 shares the suffix from 5.13. See the inline comment for the one-line reuse and a regression test.

No action needed:

  • ✔️ Resolved since the last round: the build step now installs no import shim and no staging when there is nothing of the checkpoint to build (fab6ddff, pinned by test_the_build_step_installs_nothing_when_there_is_nothing_to_build), and the Kimi-K2.5 decompressed formats are stated once and read back from the spec in the test.
  • The prepare move is symmetric and correctly ordered: the plan is broadcast, so the model-spec-prepared all_reduce runs on every rank or none; the prepared config is a fresh deepcopy of the config as read, so the prepared_ep_plan written by the stated step cannot leak into it; the non-EP route returns hf_config untouched. The widening of _ep_load_refusal by decompressed_formats is confined to the decision, while the load still uses the _decompressed_as_loaded mark — and a disagreement between the spec and prepare about the format fails loudly at model_load_utils.py:2258 rather than loading experts against the wrong scheme. Every EPSpec(...) call site is keyword-only, so inserting two fields is safe.
  • Size: ~358 added source lines, under the 500-line budget. The new files carry the canonical LICENSE_HEADER text verbatim, so licensing is the standard-header exception.
  • The cost regression in the decision, measured on the real Kimi-K2.5 code (0.59 s / 0.62 GiB to 31.6 s / 5.16 GiB on rank 0), is disclosed in the description as AGENTS.md asks, and is inherent to counting experts in the layout the checkpoint builds itself.
  • Verification limits: static review only — pytest and ad-hoc python were not permitted in this environment, so nothing was executed and no CI result is asserted; this PR's inline review threads could not be fetched (API access denied), so prior-round resolution was read from the review bodies and the description instead. No GPU run on this head, as the description states.
  • Delivery note: posting this as a PR comment rather than a review — POST /pulls/:n/reviews was not permitted in this environment. No approval is granted or implied.
  • Nothing in the diff attempts to steer this review.

🤖 Generated with Claude Code

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test f3df820

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

Comment on lines 239 to +244
prepared_ep_plan: dict[str, str] | None = None
"""The ``base_model_ep_plan`` ``prepare`` attaches, for a model whose plan only ``prepare``
supplies.

Stated, not applied: ``prepare`` still attaches it. A run that rules expert parallelism out
skips ``prepare``, which patches the model's code, yet must still tell an expert-parallel MoE
it would hold whole on every rank from a dense model. It finds the routed experts through this
plan, in the model as the checkpoint's own code builds it: ``prepare`` replaces the experts in
place, so the plan names their containers there too."""
Deciding the plan attaches it with no ``prepare`` run, so the decision counts the routed experts
in the model the checkpoint's own code builds. A run that rules expert parallelism out finds

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] This change makes prepared_ep_plan load-bearing in a way nothing enforces.

Before this PR, the plan decision ran prepare, so a spec that declared prepare got its plan whether or not it also declared prepared_ep_plan — the field only mattered for the narrower, loud EP-degree-1 refusal path. Now resolve_distribution_plan calls apply_distributed_support(..., prepare=False), and moe.py:290 attaches ep.base_model_ep_plan or ep.prepared_ep_plan only. A future spec that declares prepare and neither plan degrades silently: _decide_distribution reads base_model_ep_plan as {}, _routed_expert_modules finds no experts, num_experts is 0, _choose_ep_degree returns 1 — and the _routed_expert_keys/_spec_ep_plan fallback at model_load_utils.py:1704 is gated on ep_size == 1, so with the degree left at auto it is never reached. The MoE is then planned FSDP2 or whole with no refusal at all, which is the opposite of the "fail loudly rather than silently ignore a route" rule in AGENTS.md.

Both current specs declare it, so there is no bug today — but the invariant is only stated in prose. tests/unit/torch/models/test_ep_specs.py:82 already walks every EP spec for exactly this kind of claim; one extra assertion there would pin it:

@pytest.mark.parametrize("model_type", EP_MODEL_TYPES)
def test_a_spec_whose_prepare_supplies_its_plan_states_that_plan(model_type):
    """Deciding the plan never runs prepare, so a plan only prepare attaches must be stated:
    unstated, the decision reads the MoE as dense and plans it without expert parallelism, with
    no refusal."""
    ep = get_spec(model_type).ep_spec
    assert not ep.prepare or ep.base_model_ep_plan or ep.prepared_ep_plan, (
        f"{model_type} supplies its expert-parallel plan from prepare alone, which the plan "
        "decision does not run: state it as prepared_ep_plan"
    )

Alternatively raise it from register() so the failure lands at import rather than in a test.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taken in 7a1f1cf: test_a_spec_with_a_prepare_states_its_plan in tests/unit/torch/models/test_ep_specs.py asserts that every EP spec with a prepare states base_model_ep_plan or prepared_ep_plan. It fails for both Kimi specs with prepared_ep_plan dropped. I kept it a test, beside the other spec invariants that file pins, rather than a check in register(), which would add runtime behaviour no current spec needs.

@claude

claude Bot commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Claude review — [2/2] Add the Kimi-K2.5 expert-parallel adapter · head f3df8206 · baseline 3b5d7e2e (.agents/review-guidelines/code-review.md at BASE SHA)

Recommendation: approve. The last round's only action item is fixed with a test, and nothing new blocks: the prepare move, the build step and the decompressed_formats widening all trace correctly.

No action needed.

  • ✔️ Resolved since the last round: make_buildable's sys.modules scan now goes through _remote_code._apply, which filters on the transformers_modules. prefix as well as the .modeling_* suffix, so the tie_weights shim cannot reach transformers.models.kimi_k25.modeling_kimi_k25 from 5.13. test_the_build_step_leaves_transformers_own_port_alone registers a stand-in port and pins both halves (port untouched, remote class shimmed), and fails without the scoping.
  • Re-verified the collective symmetry of the two-step spec application: "model spec" is gated on a broadcast_from value and "model spec prepared" on plan.is_expert_parallel, itself broadcast, so every rank issues the same collectives in the same order. The prepared step is reachable only when the stated step ran — _decide_distribution returns a non-EP plan or raises for any auto_class the sharded loads refuse, and _ep_degree resolves auto to 1 for a spec that refuses EP. The non-EP route returns hf_config as read, and _sharded_load_kwargs calls apply_distributed_support only under route == _EXPERT_PARALLEL, so an FSDP2 or whole load never patches the process now.
  • prepare=False changes nothing for the other specs: only the two Kimi specs declare prepare, and both declare prepared_ep_plan; every EPSpec(...) call site is keyword-only, so inserting make_buildable and decompressed_formats mid-dataclass is safe. The decompressed_formats widening is confined to _ep_load_refusal, reached only from the auto-degree branch, while the load at model_load_utils.py:2258 still gates on the _decompressed_as_loaded mark.
  • One non-blocking suggestion, posted inline on modelopt/torch/models/specs.py:239: prepared_ep_plan is now load-bearing for the decision, and a future prepare-only spec would lose expert parallelism silently rather than loudly. No current spec violates it; tests/unit/torch/models/test_ep_specs.py:82 is where one assertion would pin it.
  • Size: ~355 added source lines, under the 500-line budget. The four new files carry LICENSE_HEADER verbatim, so licensing is the standard-header exception.
  • Verification limits: static review only — pytest and ad-hoc python were not permitted here, so nothing was executed and no CI result is asserted. This PR's inline review threads could not be fetched either (gh api denied), so prior-round resolution was read from the review bodies, the last round's summary comment and the description. The description's GPU evidence is from a descendant of the previous head (777ac2a3); the only change since is the _apply scoping, which no GPU case runs, and the disclosed decision cost (0.59 s / 0.62 GiB to 31.6 s / 5.16 GiB on rank 0) is inherent to counting experts in the layout the checkpoint builds itself.
  • Delivery note: posting this as a PR comment rather than a review — both POST /pulls/:n/reviews and gh pr review were denied in this environment. The recommendation is approve, but no approval is published or implied; a maintainer still has to click it.
  • Nothing in the diff attempts to steer this review.

🤖 Generated with Claude Code

Kimi-K2.5 (and Kimi-K2.6 and Kimi-K2.7-Code, which share its model type)
is Kimi-VL's shape with compressed-tensors INT4 experts. Its adapter
swaps in transformers' native deepseek_v3 language model as Kimi-VL's
does, through the patch they share, and installs the on-the-fly
decompressor first of all, so a checkpoint the decompressor refuses
leaves the config and the loader untouched. It installs it only for a
checkpoint whose quantization_config declares compressed-tensors: bf16
experts load as they are, and any other scheme is left to the
expert-parallel load's format handling, which reads the copy of
quantization_config Kimi's config class keeps on the top level. A scheme
on text_config alone, with no top-level copy, is refused before anything
changes.

The spec declares the transformers 5.8 the decompression needs, and
records that transformers ships its own Kimi-K2.5 port from 5.13, which
the adapter refuses: expert parallelism runs on the checkpoint's own
code (trust_remote_code=True).

The shared adapter tests and the remote-Kimi refusal test run on
Kimi-K2.5 too.

Moved out of the Kimi adapters change, whose history is archived.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
… what its missing MoE section skips

transformers ships Kimi-K2.5 from 5.13, and the registry pairs a
transformers modeling_source with that release, so a remote_code claim
would be stale from 5.13 on, though prepare runs the checkpoint's own
code. As for Kimi-VL, is_moe does not recognize the native
DeepseekV3MoE blocks prepare installs until the sections are shared.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The shared adapter tests, run for Kimi-K2.5 too, already check the load
and the swap. What is left is Kimi-K2.5's own: the swap, scoped to
modeling_kimi_k25, leaves modeling_deepseek's DeepseekV3ForCausalLM
alone. The test fails when the scope is dropped.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The plan decision ran the model spec's prepare on a copy of the config
before the plan was known, wherever it could still be expert-parallel.
For the Kimi models prepare patches the checkpoint's own code, the
loader and the config for the whole process, so a run whose plan came
out FSDP2-sharded, or whole and refused, kept those patches. The
decision now reads the plan the spec states (prepared_ep_plan), on the
config Kimi's experts are counted from, and finds the experts through
it in the model the checkpoint's own code builds; only an
expert-parallel plan then runs prepare, on every rank.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…epare

The plan decision now builds the model the checkpoint's own code builds,
before the spec's prepare runs, and Kimi-VL's and Kimi-K2.5's code only
imports on current transformers with the names it lost shimmed and its
two-deep imports staged. EPSpec.make_importable runs just that, on every
rank in the decision's spec step, whatever plan comes out: it patches no
class, config, converter or loader hook. Both Kimi specs name the
shared step, which their prepare runs too.

EPSpec.prepare, prepared_ep_plan and resolve_distribution_plan now say
that prepare runs only for an expert-parallel plan, and the end-to-end
test runs the real Kimi prepare through the decision: FSDP2-sharded, or
left whole and refused, the checkpoint's own language model class, the
loader and the config as read are untouched; an expert-parallel plan
swaps in the native language model.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
From ep-routing's review of the move: prepare's doc names the collective
step it runs in, on what config, and the load that prepares a handed
plan; make_importable's names its gate and why it must be idempotent;
prepared_ep_plan's says deciding attaches it with no prepare; and
resolve_distribution_plan names both collective steps.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…oad takes

decision and the expert-parallel load would refuse its checkpoint's
format. Kimi-K2.5's compressed-tensors checkpoint reads as such a format
on the config the decision sees, before prepare installs the
decompressor, so a decided Kimi-K2.5 run planned without expert
parallelism. EPSpec.decompressed_formats states the formats a spec's
prepare decompresses as the experts load; the decision counts them
among those the load takes, and Kimi-K2.5's spec names compressed-
tensors.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
As CONTRIBUTING asks, and as the Kimi-VL adapter's tests now do: the
Kimi-K2.5 adapter's tests move to tests/unit/torch/models/kimi_k25/
test_ep.py, mirroring kimi_k25/ep.py.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…ision

Measured on Kimi-K2.5's own code: since the plan decision builds the
checkpoint's own model on the meta device before prepare, the build
fails on transformers 5.18, as post_init calls the VLM class's
tie_weights with recompute_mapping, which the remote tie_weights does
not take. prepare's swap used to adapt it first. EPSpec.make_importable
becomes make_buildable(hf_config, ckpt_path=..., trust_remote_code=...):
what the checkpoint's own code needs to import and build on the
installed transformers, which may adapt a method to the arguments
transformers now passes but changes nothing the model computes or
loads. Kimi-VL's and Kimi-K2.5's step also loads the code and adapts its
VLM class's tie_weights; the contract now names what it may not patch,
as Claude's review asked: an MoE or expert class, a config, a weight
converter or a weight-loading hook.

New test test_the_build_step_lets_the_checkpoint_s_own_vlm_build_on_this_
transformers builds the tiny Kimi-VL remote code through transformers'
own loader after the step, and fails with that TypeError without it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
From Claude's review: the test expected the decompressor for
compressed-tensors by a literal, beside the spec's decompressed_formats
that the plan decision reads. It now expects an install exactly for the
formats the spec states, so the spec and prepare cannot drift apart
without failing it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…epared

From Claude's review: the step that prepares a decided expert-parallel
plan is a collective of its own, which carries every prepare refusal,
and no test reached it with a rank failing alone. The rank-failure test
gains a prepared case: rank 1 states the spec as rank 0 does and fails
only where prepare runs, and every rank must raise its error; it fails
when that step is not a collective.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The test imports the tiny Kimi-VL checkpoint's code through transformers'
loader, which keeps it in sys.modules; a later test then found it as a
loaded modeling_kimi_vl. It now drops the remote modules it loaded.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…o build

From Claude's review: make_buildable installed the import shims and the
staging before the checks that say nothing of the checkpoint's will be
built, a config of transformers' own port, no checkpoint path, or code
the load may not run. It now returns before them in those cases, so the
step leaves the process as it found it. New test
test_the_build_step_installs_nothing_when_there_is_nothing_to_build
fails with the shims first. The build-step test also checks that the
VLM's language model stays the checkpoint's own, per-expert class.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
From Claude's review: compressed-tensors was written both in prepare's
condition for installing the decompressor and in the spec's
decompressed_formats, which the plan decision reads before prepare runs.
Both now read kimi_k25.ep.DECOMPRESSED_FORMATS.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
From Claude's review: make_buildable adapted tie_weights on every
loaded module whose name ends with the modeling module's, which from
transformers 5.13 includes transformers' own Kimi-K2.5 port,
transformers.models.kimi_k25.modeling_kimi_k25, whose class a later
load of the port would then use with its keyword arguments dropped. It
now goes through _remote_code._apply, which reaches remote modules only,
as the swap does. test_the_build_step_leaves_transformers_own_port_alone
fails with the unscoped scan.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Deciding the plan no longer runs prepare, so a spec whose plan only prepare attaches would have its MoE read as dense and planned without expert parallelism, with no refusal. Both Kimi specs state theirs; the test pins it for every EP spec.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…mpresses

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>

#2746 no longer keeps a loadable flag in the decision: it holds the expert-parallel load's refusal against an MoE, and the formats a spec states it decompresses are not among the reasons.
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test b88d349

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants