Skip to content

[OMNIML-6078] [1/2] Add the Kimi-VL expert-parallel adapter - #3138

Open
shengliangxu wants to merge 9 commits into
shengliangx/hfep-base-11b-kimifrom
shengliangx/hfep-11b0-kimi-vl
Open

shengliangxu wants to merge 9 commits into
shengliangx/hfep-base-11b-kimifrom
shengliangx/hfep-11b0-kimi-vl

Conversation

@shengliangxu

@shengliangxu shengliangxu commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Note

Split out of #2748 at the reviewers' request (2026-10-30): the Kimi-VL adapter, the patch it shares with the Kimi-K2.5 adapter, and the shared deepseek_v3 plan. #2748 keeps the Kimi-K2.5 adapter.

Base: shengliangx/hfep-base-11b-kimi, a branch (not a PR) that merges the PRs this one builds on, #2746 (the model specs) and #2747 with #3137 (on-the-fly decompression), with main, which holds the remote-code helpers (#3130) since they merged. The diff below is this PR's alone, and it merges after #2746 and #2747.

Series layout

What does this PR do?

Type of change: New feature

Kimi-VL ships its own modeling. It builds the language model's MoE from an nn.ModuleList of per-expert Linears, which transformers' expert-parallel machinery cannot shard. Its text backbone is DeepSeek-V3, so the adapter swaps in transformers' native deepseek_v3 language model (fused gate_up_proj/down_proj experts, and the plan to shard them). It keeps the bundled vision tower and projector from the checkpoint's own code.

  • The patch Kimi-VL and Kimi-K2.5 share, in modelopt/torch/models/_kimi.py:
    • Refusals before anything changes: transformers' own Kimi-K2.5 port (from 5.13), which the patch never reaches; a config with no text_config; a missing checkpoint path; untrusted remote code (get_class_from_dynamic_module runs the checkpoint's code without checking trust_remote_code); and a quantization scheme on text_config alone, which the native text config drops.
    • Import shims for the symbols the checkpoint's modeling imports that transformers 5 dropped (is_torch_fx_available, ...). They stub only code paths the native language model never runs.
    • The swap itself: the checkpoint's module-level DeepseekV3ForCausalLM builds the native class from the native DeepseekV3Config prepare puts on text_config, and the checkpoint's own class from its own config. A tie_weights wrapper covers the recompute_mapping newer transformers passes. A remote module that defines no module-level DeepseekV3ForCausalLM stops the load instead of passing for already patched.
    • Why the swap reads the config: deciding the plan runs prepare on a copy of the config on every rank before the plan is known, and the patch lasts for the process. A plan that then keeps the load off expert parallelism (replicated, or FSDP2) builds from the config as read, so it gets the checkpoint's own language model, as with no patch. Before, the native class was handed the checkpoint's own text config, which it cannot take. The other effects of prepare are harmless off expert parallelism: the import shims only add symbols the remote code imports, the staging copies its sibling modules, the tie_weights wrapper only drops keyword arguments for these untied checkpoints, and Kimi-K2.5's decompressor rewrites only the configs it marked, which are the prepared copies.
    • The swap is kept, through keep_patched ([OMNIML-6063] [1/3] Stage and keep patched a checkpoint's own modeling code #3130), on a module transformers executes again or loads again under another name. The reasons are in [OMNIML-6063] [1/3] Stage and keep patched a checkpoint's own modeling code #3130's description.
    • Rank 0 only: the adapters' notes and the shims' warnings print there, labeled kimi.
  • The adapter is an EPSpec.prepare hook on the kimi_vl spec, so nothing outside modelopt/torch/models learns about Kimi.
    • The spec registers only the expert-parallel section. The MoE and export sections stay deepseek_v3's, and sharing them is left to a follow-up.
    • The spec states the plan prepare attaches (EPSpec.prepared_ep_plan, [OMNIML-6076] [3/3] Apply the model spec when deciding and loading expert-parallel #2746): deepseek_v3's, now a shared BASE_MODEL_EP_PLAN. So a run at an EP degree of 1, which skips prepare, still finds the experts the checkpoint's own code builds, and refuses to hold them whole on every rank.
The adapter's walkthrough (moved from kimi_vl/ep.py's module docstring)
DP=EP a2a support for Kimi-VL-A3B (``kimi_vl``).

a remote-code VLM whose text backbone is a Moonlight (DeepSeek-V3-lite: MLA, ``q_lora_rank=None``, 64 routed experts, 2
shared) MoE and whose vision tower is MoonViT. The bf16 sibling of ``kimi_k25`` -- SAME shape, but NO compressed-tensors
decompressor (the checkpoint ships plain bf16 per-expert weights).

Challenge (identical to kimi_k25): the bundled ``modeling_kimi_vl.py`` builds its language model from
an INLINE ``DeepseekV3ForCausalLM`` whose ``DeepseekV3MoE`` is an ``nn.ModuleList`` of per-expert
``gate_proj``/``up_proj``/``down_proj`` Linears -- incompatible with transformers' EP machinery, which
needs the FUSED 3-D ``gate_up_proj``/``down_proj`` Parameters the transformers-native ``deepseek_v3``
modeling ships.

Solution (mirrors kimi_k25):
  1. Shim the import symbols ``modeling_kimi_vl`` needs that newer transformers dropped (e.g.
     ``is_torch_fx_available``) so the module IMPORTS -- they only guard torch.fx / legacy attn-mask
     paths that our native-LM + MoonViT forward never execute.
  2. Swap ``hf_config.text_config`` to a transformers-native ``DeepseekV3Config`` (drops the remote
     ``auto_map`` so the native class -- and its ``base_model_ep_plan`` -- is used for the LM).
  3. Apply the deepseek_v3 EP plan to that native ``text_config`` (post_init prefixes it to the VLM
     root as ``language_model.model.layers.*.mlp.experts.{gate_up_proj,down_proj}``).
  4. Monkey-patch ``modeling_kimi_vl.DeepseekV3ForCausalLM`` -> the transformers-native class, so
     ``KimiVLForConditionalGeneration.__init__`` (``self.language_model = DeepseekV3ForCausalLM(
     config.text_config)``) builds the EP-capable native LM while MoonViT + the KimiVL wrapper stay
     from the bundled code. Native deepseek_v3's per-expert WeightConverter fuses the checkpoint's
     per-expert ``.weight`` tensors into the 3-D params on load (no decompressor -- weights are bf16).

Version: no floor beyond the EP framework's 5.7; validated on transformers 5.9.0. The compat shims
stand in for the symbols the remote code imports that later releases dropped.

Usage

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

Testing

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ Additive. Registering the Kimi-VL spec reaches beyond the expert-parallel load in the plan decision of a multi-rank run where expert parallelism is still a candidate: every rank runs prepare before the plan is decided, which shims the remote code's imports, patches its modeling module (which builds the native language model only for the config prepare made, so a load the plan keeps off expert parallelism still builds the checkpoint's own), and can refuse the checkpoint, failing the decision. A run that would leave the MoE whole on every rank, with nothing asking for that, is refused, as other expert-parallel MoEs are. A single-process load is unchanged. [OMNIML-6078] [2/2] Add the Kimi-K2.5 expert-parallel adapter #2748 later runs prepare only once the plan is decided expert-parallel.
  • 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 yet.

Additional Information

Part of expert-parallel PTQ for Hugging Face MoE models, [1/2] of #2748's split:

@shengliangxu
shengliangxu requested review from a team as code owners October 30, 2026 00:31
@shengliangxu
shengliangxu requested review from cjluo-nv and meenchen and removed request for a team October 30, 2026 00:31
@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review: 3
  • main
  • release/.*
  • feature/.*

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 3dbb3e2

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

Comment on lines +118 to +128
def _drop_tie_weights_kwargs(orig_tie):
"""Wrap a remote ``tie_weights()`` that predates the kwargs newer transformers pass to it.

Dropping ``missing_keys`` as well as ``recompute_mapping`` is safe because the remote
``tie_weights`` only calls its language model's, and the Kimi checkpoints set
``tie_word_embeddings=False``: nothing is tied, so no tied key is left to report to the load.
"""

def _tie_weights(self, *args, **kwargs): # drop recompute_mapping etc.
return orig_tie(self)

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] The shim's safety rests on tie_word_embeddings=False, which nothing checks at runtime.

Dropping recompute_mapping is unconditionally fine, but dropping missing_keys is only fine under the docstring's premise that nothing is tied. to_native_backbone_config carries tie_word_embeddings over from the remote text_config verbatim, so a Kimi-VL/K2.5 variant (or a user-edited config) that ties embeddings would silently lose the tied-key bookkeeping: the native LM ties the weight but lm_head.weight is never removed from the load's missing_keys, so a strict load reports a spurious missing key — or, depending on the release's ordering, re-initializes it.

Cheap guard inside the wrapper, where the config is reachable:

def _tie_weights(self, *args, **kwargs):  # drop recompute_mapping etc.
    text_config = getattr(self.config, "text_config", self.config)
    if getattr(text_config, "tie_word_embeddings", False):
        raise RuntimeError(
            "kimi: the tie_weights shim drops missing_keys, which is only safe for an untied "
            "checkpoint, but this one sets tie_word_embeddings=True."
        )
    return orig_tie(self)

That turns an assumption documented in a comment into one the code enforces, at no cost on the supported checkpoints.

(2) Why it matters: the failure is silent-to-confusing at load time rather than at patch time, on a path that only triggers for a checkpoint nobody tested. (3) Fix: the guard above, or state the assumption as a refusal in checkpoint_to_patch next to the other pre-patch refusals.

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.

Declined. I'm not adding the guard, because a tied checkpoint loads correctly through the shim. I documented what the shim loses instead, in d6ca10f.

I checked this with a small VLM whose tie_weights() takes no kwargs, as the Kimi remote code's does, wrapped by this shim:

  • Its native DeepSeek-V3 language model has tie_word_embeddings=True.
  • I saved it without lm_head.weight and loaded it with from_pretrained.
  • On transformers 5.18 and 5.5.4, lm_head.weight comes back as the same tensor as the embedding, holding the saved values.
  • The only loss is the bookkeeping: lm_head.weight stays in the load's missing-keys report. Nothing is re-initialized.

A refusal would therefore block a load that works. No supported checkpoint is tied in any case: Kimi-VL-A3B (Instruct and Thinking-2506) and Kimi-K2.5, K2.6 and K2.7-Code all set tie_word_embeddings=false at the top level and on text_config.

model_type="kimi_vl",
modeling_source="remote_code",
# prepare swaps a native deepseek_v3 in as the language model, with deepseek_v3's plan.
ep_spec=EPSpec(prepare=prepare, prepared_ep_plan=BASE_MODEL_EP_PLAN),

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] With only the EP section registered, is_moe(block, "kimi_vl") returns False for the very blocks prepare installs.

The docstring says the MoE/export sections "stay deepseek_v3's, so lookups by kimi_vl find neither yet", but it's worth being explicit about what that costs, because is_moe has fallbacks that do not rescue this case. After prepare, the language model's MoE blocks are native DeepseekV3MoE, and:

  • match_moe_block(module, "kimi_vl") → the spec has no moe_spec → None;
  • the name fallback misses ("deepseekv3moe" ends in neither sparsemoeblock nor contains moelayer);
  • the structural fallback misses (the router is gate, not router).

That is exactly what deepseek_v3/specs.py documents as the reason it needs block_names=("DeepseekV3MoE",). So modelopt/torch/export/unified_export_hf.py:565/:949 and layer_utils.py:369 will skip the experts of a kimi_vl model rather than fail loudly. get_expert_linear_names happens to survive via its fused-container fallback, which makes the gap quieter, not smaller.

Nothing in this PR reaches those call sites, and kimi_vl was unregistered before, so there's no regression — hence a suggestion rather than a blocker. But since the follow-up that wires the PTQ entrypoint (#2754) will need it, and BASE_MODEL_EP_PLAN already establishes the precedent of sharing deepseek_v3's constants, reusing its sections here is a two-line change that closes the hole before anything can silently take the non-MoE path:

from ..deepseek_v3.specs import BASE_MODEL_EP_PLAN
from ..specs import EPSpec, ExportSpec, ModelSpec, MoESpec, register

register(
    ModelSpec(
        model_type="kimi_vl",
        modeling_source="remote_code",
        # prepare swaps a native deepseek_v3 in as the language model, with deepseek_v3's plan.
        ep_spec=EPSpec(prepare=prepare, prepared_ep_plan=BASE_MODEL_EP_PLAN),
        # prepare's language model is native deepseek_v3: its MoE blocks and export are too.
        moe_spec=MoESpec(
            block_names=("DeepseekV3MoE",),
            expert_linear_names=("gate_proj", "down_proj", "up_proj"),
            ...,  # deepseek_v3's, ideally hoisted to a shared constant as the plan was
        ),
        export_spec=ExportSpec(grouped_expert_export=True),
    )
)

If you'd rather keep this PR to the EP section, consider making the gap loud instead of silent — e.g. have the follow-up tracked in the docstring name the lookups that miss (is_moe, hence the HF export's expert handling), so the next reader doesn't read "find neither yet" as "falls back to the generic detection".

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.

Taking the docstring half in d6ca10f; the shared sections stay with the follow-up.

The docstring now names the miss: is_moe's name and structure fallbacks do not recognize the native DeepseekV3MoE blocks that prepare installs, so the HF export's MoE-block handling skips them. #2748 adds the same sentence to Kimi-K2.5's spec, which has the same gap.

I'm keeping the sections out of this PR because registering them changes how the HF export treats these models:

  • It enables the MoE pre-processing at the two export call sites you name.
  • It enables grouped expert export.

That needs its own real-model export check, for both Kimi-VL and Kimi-K2.5 together. Today, for the fused experts these models load with, the skipped input-preparation handler is a no-op, since the amax fallback for fused experts runs during export. The AWQ pre_quant_scale averaging is the step that is actually skipped.

@claude claude Bot left a comment

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.

Claude review passed — no blocking issues found. LGTM

Scope: full review (the trigger comment carried no extra scoping). All 8 changed files opened (638 added lines): modelopt/torch/models/{_kimi.py, kimi_vl/{__init__,ep,specs}.py, deepseek_v3/specs.py, __init__.py} and both test files. Also read the base-branch machinery the adapter rides on — _remote_code.py (keep_patched, stage_full_remote_closure), _onfly_decompress.to_native_backbone_config, specs.py (EPSpec), utils/moe.py (apply_distributed_support, _ep_config, _routed_expert_keys, is_moe) — since the correctness of this PR is almost entirely in how it composes with them.

Findings

CRITICAL: 0 · IMPORTANT: 0 · SUGGESTION: 2

  1. _kimi.py:118 — the tie_weights shim drops missing_keys on the premise tie_word_embeddings=False, which nothing enforces; to_native_backbone_config carries that flag over from the remote config verbatim. A one-line guard in the wrapper turns the documented assumption into an enforced one.
  2. kimi_vl/specs.py:37 — registering only the EP section means is_moe(block, "kimi_vl") is False for the native DeepseekV3MoE blocks prepare installs: both of is_moe's fallbacks miss it (name doesn't end in sparsemoeblock; router is gate, not router), which is precisely why deepseek_v3 needs block_names. Not a regression — kimi_vl was unregistered before, and no path here reaches the export call sites — but the follow-up will need it and it's a two-line reuse.

What I checked and found correct

  • Plan plumbing. prepare attaches the plan to text_config, and _ep_config reads the nested text config when the root has none, so the globs land on the tree they're written against. plan_config_attr being unset is right: base_model_ep_plan is None on this spec, so the attr never selects a target.
  • prepared_ep_plan vs. what prepare actually attaches. The stated plan's routed-expert keys reduce to {layers.*.mlp.experts} under _routed_expert_keys — identical whether modelopt's BASE_MODEL_EP_PLAN or transformers' own 5.13 plan wins, since the extra ep_router gate entry is in neither _EXPERTS_STYLES nor grouped_gemm. So the EP-degree-1 refusal finds the ModuleList experts the checkpoint's own code builds (language_model.model.layers.*.mlp.experts, matched by endswith), and the test asserts exactly this equivalence rather than the literal dict.
  • No aliasing through the shared constant. BASE_MODEL_EP_PLAN is now one dict referenced by two specs, but every consumer copies (dict(ep.base_model_ep_plan) in apply_distributed_support, dict(...) in _spec_ep_plan), so neither spec can mutate the other's plan.
  • Refuse-before-mutate. All five refusals in checkpoint_to_patch / refuse_quantization_only_on_text_config run before the shims, the staging wrapper, the config swap and the remote import — and the test asserts the loader is left unwrapped and the checkpoint's code unimported. The trust_remote_code refusal in particular is load-bearing, since get_class_from_dynamic_module doesn't check it itself. The startswith("transformers.") port test doesn't false-positive on transformers_modules.* (the trailing dot).
  • Shim targets match the remote import sites. The three stubs are set on the submodules DeepSeek's remote modeling imports from directly (transformers.utils.import_utils, transformers.pytorch_utils, transformers.modeling_attn_mask_utils), not on the transformers.utils namespace — which is what makes from X import Y see them. is_torch_fx_available() -> False keeps the torch.fx.wrap branch (and hence the _prepare_4d_causal_attention_mask stub) unreached; the stub raises rather than returning something plausible, which is the right call.
  • Patch durability. keep_patched is given the module suffix, so the swap survives transformers re-executing the module or loading it afresh under another hash — and since re-execution rebinds the VLM class too, to_native's early return False on an already-native module can't leave a fresh unshimmed tie_weights behind. Failing loudly when there's no module-level DeepseekV3ForCausalLM, rather than treating it as "already patched", is the correct choice: silence there would build the per-expert layout EP cannot shard.
  • Version floor. ep.py's "no floor beyond the EP framework's 5.7" matches _EP_MIN_TRANSFORMERS = "5.7", so the omitted requires_transformers is right, and the test's module-level skip agrees.
  • Lazy imports. modelopt.torch.utils{,.moe} are imported inside functions with the circularity reason named; _remote_code is stdlib-only at module scope, so the registry import stays clean.
  • Tests. The refusal table, both loud-failure modes, shim installation, idempotency of the tie_weights wrap, and the EP-degree-1 refusal against a ModuleList stand-in are all covered, with process-wide patches restored per test. The install_onfly_decompress monkeypatch in the first test is vacuous for Kimi-VL (bf16, nothing calls it) but reads as a deliberate guard on the shared path, so I left it alone.

Risk

Low. Additive and confined to the expert-parallel path: a new model type, one shared helper module, and a constant hoisted out of deepseek_v3's spec with no behavior change (the comment that moved with it is intact and still accurate). The only process-wide mutations are idempotent and installed after the refusals. The 184-line _kimi.py is shared with #2748, so its two parametrize lists standing at one entry each is expected rather than dead scaffolding.

🤖 Generated with Claude Code

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11b0-kimi-vl branch from 3dbb3e2 to e41e06d Compare October 30, 2026 00:59
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-base-11b-kimi branch from 6c49640 to 0864f57 Compare October 30, 2026 00:59
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test e41e06d

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test d6ca10f

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-base-11b-kimi branch from 0864f57 to a5d243e Compare October 30, 2026 01:52
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11b0-kimi-vl branch from d6ca10f to 4a459dc Compare October 30, 2026 01:52
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 4a459dc

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 9e2cd9a

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

Comment on lines +134 to +149
def _native_for_the_prepared_config(own_class, native_class):
"""The ``DeepseekV3ForCausalLM`` the patched module builds its language model with.

transformers' native class for the native ``DeepseekV3Config`` ``prepare`` puts on
``text_config``, and the checkpoint's own class for its own config. Deciding the plan prepares a
copy of the config before the plan is known, and the patch outlives that copy: a load the plan
keeps off expert parallelism builds from the config as read, and so gets the checkpoint's own
language model, as it would with no patch.
"""
from transformers import DeepseekV3Config

def build(config, *args, **kwargs):
built = native_class if isinstance(config, DeepseekV3Config) else own_class
return built(config, *args, **kwargs)

setattr(build, "_kimi_native_swap", True)

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] The swap now binds a plain function where a class used to live, so modeling_kimi_vl.DeepseekV3ForCausalLM stops behaving like a class for anything other than a direct call.

Under the two routes this PR exercises that holds — the LM is only ever built by the VLM's self.language_model = DeepseekV3ForCausalLM(config.text_config) — but any class-shaped access on that name now fails with an opaque AttributeError/TypeError instead of working: .from_pretrained, ._from_config, .config_class, issubclass, or AutoModelForCausalLM.from_config(text_config) resolving a text_config.auto_map["AutoModelForCausalLM"] that names modeling_kimi_vl.DeepseekV3ForCausalLM (a non-EP run keeps the checkpoint's auto_map on text_config, and keep_patched's get_class_in_module wrapper hands the patched binding back to from_pretrained). _BaseAutoModelClass.register would even accept it, since hasattr(build, "config_class") is False, and only fail a line later on _from_config.

A callable object that forwards attribute lookups to the native class keeps the dispatch and the class-like surface, for about the same number of lines:

def _native_for_the_prepared_config(own_class, native_class):
    """..."""
    from transformers import DeepseekV3Config

    class _Build:
        _kimi_native_swap = True

        def __call__(self, config, *args, **kwargs):
            built = native_class if isinstance(config, DeepseekV3Config) else own_class
            return built(config, *args, **kwargs)

        def __getattr__(self, name):  # from_pretrained, config_class, ... land on the native class
            return getattr(native_class, name)

    return _Build()

getattr(current, "_kimi_native_swap", False) in to_native keeps working unchanged. If you'd rather keep the function, a sentence in the docstring saying the binding is no longer a class, and that only a direct call is supported, would at least make the constraint explicit for the next reader.

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.

Kept as a function: nothing reaches this name except the direct call.

  • The checkpoints' own code. In both Kimi-VL's modeling_kimi_vl.py and Kimi-K2.5's modeling_kimi_k25.py, the module-level name's only use is self.language_model = DeepseekV3ForCausalLM(config.text_config). Nothing takes isinstance/issubclass of it, subclasses it, or reads an attribute from it.
  • transformers' auto paths. get_class_in_module, including the keep_patched wrapper, is only asked for names an auto_map declares.
    • The top-level auto_maps name the VLM classes.
    • Kimi-VL's text_config has no auto_map.
    • Kimi-K2.5, K2.6 and K2.7-Code name modeling_deepseek.DeepseekV3ForCausalLM. That module's class is the one the swap leaves alone: the swap rebinds only the name modeling_kimi_k25 imported, and keep_patched applies only to modules named *.modeling_kimi_k25 / *.modeling_kimi_vl.
  • Our code. ModelOpt refers to DeepseekV3ForCausalLM only as an architecture string.

The forwarding object would also make config_class, from_pretrained and similar answer for the native class even when the call would build the checkpoint's own. A docstring line stating the call-only contract is owed for this PR's next push.

Comment thread modelopt/torch/models/kimi_vl/ep.py Outdated
Comment on lines +32 to +42
2. Swap ``hf_config.text_config`` to a transformers-native ``DeepseekV3Config`` (drops the remote
``auto_map`` so the native class -- and its ``base_model_ep_plan`` -- is used for the LM).
3. Apply the deepseek_v3 EP plan to that native ``text_config`` (post_init prefixes it to the VLM
root as ``language_model.model.layers.*.mlp.experts.{gate_up_proj,down_proj}``).
4. Monkey-patch ``modeling_kimi_vl.DeepseekV3ForCausalLM`` -> the transformers-native class, so
``KimiVLForConditionalGeneration.__init__`` (``self.language_model = DeepseekV3ForCausalLM(
config.text_config)``) builds the EP-capable native LM while MoonViT + the KimiVL wrapper stay
from the bundled code. Native deepseek_v3's per-expert WeightConverter fuses the checkpoint's
per-expert ``.weight`` tensors into the 3-D params on load (no decompressor -- weights are bf16).

Version: no floor beyond the EP framework's 5.7; validated on transformers 5.9.0. The compat shims

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] Three lines of this module docstring now describe the swap as it was before _native_for_the_prepared_config landed, and one records a transformers version the PR no longer tests:

  • Step 2 (line 32-33) and the comment on line 67 both say dropping the remote auto_map is what makes the native class get used for the LM. It is not, any more: the VLM's __init__ calls the module-level DeepseekV3ForCausalLM, and which class that builds is now decided by isinstance(config, DeepseekV3Config). Dropping auto_map still matters (it keeps text_config from naming remote classes), but it is no longer the mechanism.
  • Step 4 (line 36) says the patch points the name at "the transformers-native class". It points it at a factory that picks the native class for the prepared config and the checkpoint's own class for the config as read — which is the whole point of the load-off-EP fix, and the one thing a reader of this file would most want to know.
  • Line 42 says "validated on transformers 5.9.0", while the PR reports 5.5.4 and 5.18.

Per CLAUDE.md's "Keep skill/doc edits concise … comments say why, not what", this is a tightening pass rather than new text — e.g. fold steps 2 and 4 into one line saying the native config selects the native LM, and update the version to the pair actually run.

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.

Agreed; postponed to this PR's next push to save a CI round on an approved PR. The pass will:

  • fold steps 2 and 4 into one line saying the native config selects the native language model, and that dropping auto_map only keeps text_config from naming remote classes;
  • describe step 4 as the per-config build;
  • replace "validated on transformers 5.9.0" with the versions this PR runs.

Kimi-K2.5's module docstring in #2748 says the same about the swap, so it gets the same pass.

@claude claude Bot left a comment

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.

Claude review passed — no blocking issues found. LGTM

Scope: full review (the trigger comment carried no extra scoping). All 8 changed files opened (686 added lines): modelopt/torch/models/{_kimi.py, kimi_vl/{__init__,ep,specs}.py, deepseek_v3/specs.py, __init__.py} and both test files. The checkout is shallow, so I could not diff against the previously-approved head (3dbb3e2) directly and re-reviewed the whole PR diff instead; per the description the only code delta since that approval is _native_for_the_prepared_config, plus docstrings answering last round's two suggestions.

Findings

CRITICAL: 0 · IMPORTANT: 0 · SUGGESTION: 2

  1. _kimi.py:134 — the swap now binds a plain function where a class was, so .from_pretrained / ._from_config / .config_class / issubclass on modeling_kimi_vl.DeepseekV3ForCausalLM fail opaquely. Correct for the two routes here (the LM is only ever built by a direct call), but a callable object forwarding __getattr__ to the native class keeps the class-like surface for the same line count.
  2. kimi_vl/ep.py:32 — three docstring lines (steps 2 and 4, and the comment on line 67) still say dropping the remote auto_map is what selects the native LM; the factory's isinstance check is. And line 42's "validated on transformers 5.9.0" is behind the 5.5.4/5.18 the PR reports.

What I checked and found correct

  • The new fix is right, and necessary. resolve_distribution_plan returns prepared[0] only when plan.is_expert_parallel, and the unprepared hf_config otherwise (model_load_utils.py:1789) — while the patch is process-wide, installed by the apply_distributed_support every rank ran before the plan was known. So a replicate or FSDP2 plan builds the VLM from the config as read, and previously handed the native class the checkpoint's own text config. Dispatching on isinstance(config, DeepseekV3Config) picks the checkpoint's own class there, which is what no patch at all would have done. The FSDP2 route confirms it independently: model_load_utils.py:862 re-reads the config fresh and _sharded_load_kwargs prepares only for _EXPERT_PARALLEL.
  • The EP path keeps the native type end to end. Nothing between prepare and the build serializes the config: _decide_distribution and load_pretrained:877 use copy.deepcopy (type-preserving), the plan broadcast carries the plan only (each rank prepares its own copy), and _from_config's own deepcopy preserves it too. A to_dict()/from_dict() round trip would have rebuilt text_config through the remote KimiVLConfig.__init__ and silently selected own_class under an EP plan; there is no such round trip on these paths.
  • Idempotency and durability of the swap. prepare runs twice in a real EP load (deciding, then load_pretrained:883); the second to_native returns False on the _kimi_native_swap marker, and keep_patched still reports a non-empty patched list (it lists matched module names, not changed ones), so no spurious RuntimeError. On re-execution the module-level name is the fresh remote class, unmarked, so both the swap and the tie_weights shim re-apply together — they are coupled on the same branch, which is what keeps a re-executed module from ending up swapped but unshimmed.
  • Plan plumbing. plan_config_attr being unset is right: base_model_ep_plan is None on this spec, so the attrs branch in apply_distributed_support never selects a target, and prepare attaches the plan to text_config itself where _ep_config reads it. apply_distributed_support("deepseek_v3", text_config) cannot recurse (deepseek_v3 has no prepare), and its floor is the framework's 5.7, matching ep.py's claim and the test's module-level skip.
  • prepared_ep_plan matches what the model actually builds. _routed_expert_keys(BASE_MODEL_EP_PLAN) reduces to {layers.*.mlp.experts} — identical whether modelopt's plan or transformers' own 5.13 plan wins, since the extra ep_router gate entry is in neither _EXPERTS_STYLES nor grouped_gemm — and _routed_expert_modules matches language_model.model.layers.*.mlp.experts by endswith. So the EP-degree-1 refusal finds the ModuleList experts the checkpoint's own code builds, and test_model_load_utils.py's new test exercises exactly that with prepare stubbed to fail if it runs.
  • No aliasing through the hoisted constant. BASE_MODEL_EP_PLAN is one dict referenced by two specs, but apply_distributed_support and _spec_ep_plan both copy it, so neither spec can mutate the other's plan. The comment that moved with it is intact and still accurate.
  • Refuse-before-mutate. All five refusals run before the shims, the staging wrapper, the config swap and the remote import, and the test asserts the loader is left unwrapped and the checkpoint's code unimported. The trust_remote_code refusal is load-bearing, since get_class_from_dynamic_module does not check it. The startswith("transformers.") port test does not false-positive on transformers_modules.* (the trailing dot).
  • Registering kimi_vl is safe for the existing spec suites. modeling_source="remote_code" with min_transformers_version=None satisfies test_specs_vs_transformers.py:119, and transformers ships no kimi_vl, so test_remote_code_models_are_really_absent holds. The EP_MODEL_TYPES sweeps in test_ep_specs.py pass: the plan is well formed, prepare satisfies the declares-something check, and plan_config_attr is empty. deepseek_v3 is imported before kimi_vl in the isort: skip block, and the transitive import from kimi_vl.specs could not double-register either way.
  • Lazy imports. modelopt.torch.utils{,.moe} are imported inside functions with the circularity reason named; _remote_code is stdlib-only at module scope, so the registry import stays clean.
  • Tests. The _restore helper's absent-attribute branch unwinds correctly (monkeypatch undoes in reverse, leaving the attr deleted), so the compat shims do not leak across tests. The two single-entry parametrize lists are scaffolding for #2748's cases, as the description says, not dead code.

Last round's two suggestions were answered by docstrings rather than code — the tie_weights shim now states that a tied checkpoint still loads with lm_head.weight reported missing, and kimi_vl/specs.py states that is_moe misses the native DeepseekV3MoE blocks until the MoE and export sections are shared. Both are the author's call and I am not re-raising them.

Risk

Low. Additive and confined to the expert-parallel path. The one new code path narrows a previously unconditional monkey-patch, and the narrowing is in the safe direction: off expert parallelism the model is now built exactly as it would be with no patch at all, which is what the new test pins.

🤖 Generated with Claude Code

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11b0-kimi-vl branch from 9e2cd9a to c22b9b2 Compare October 30, 2026 19:22
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-base-11b-kimi branch from a5d243e to a4cfa16 Compare October 30, 2026 19:22
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test c22b9b2

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-base-11b-kimi branch from 3c641ac to 8bb2e35 Compare October 31, 2026 00:02
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11b0-kimi-vl branch from b4847b8 to 1c2910a Compare October 31, 2026 00:02
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 1c2910a

@meenchen meenchen left a comment

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.

Bot review (gpt-6.1-sol) — DM the bot to share feedback.

Changes requested: the prior loading-coverage gap and unusable quantization remedy remain unresolved.

Needs action:

  • Add bounded Kimi-VL loading/logit-parity coverage in tests/gpu/torch/utils/test_expert_parallel_load.py, including expert slices, preserved dense/vision weights, and a non-EP forward after preparation.
  • Fix _kimi.py’s quantization remedy for Kimi-VL; recommend an unquantized/dequantized checkpoint rather than copying the scheme to the root.
  • Move test_kimi_vl_ep.py to tests/unit/torch/models/kimi_vl/test_ep.py, following CONTRIBUTING.md.
  • 💬 Author postponed the documentation pass — shorten/correct kimi_vl/ep.py and fix the PR’s “EP-only” claim to reflect preparation before plan selection.
  • Document the optional-Transformers rationale for _kimi.py’s local imports; retain the existing circular-import explanation.

No action needed:

  • ✔️ Earlier suggestions about tied-key bookkeeping and the MoE/export detection gap are now explicitly documented.

with torch.device("meta"):
assert type(remote.vlm(prepared).language_model) is DeepseekV3ForCausalLM
assert type(remote.vlm(as_read).language_model) is remote.remote_lm

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.

Bot comment.

The previous critical coverage request is still unresolved. The positive fixture replaces get_class_from_dynamic_module with an in-memory module, and this test constructs on meta without loading tensors or running a forward. I rechecked tests/gpu/torch/utils/test_expert_parallel_load.py: its loading/parity cases still use Qwen3-MoE, not this nested DeepSeek-V3 adapter. Extend it with a tiny local remote-code VLM checkpoint that exercises the real adapter and Transformers loader. Verify each rank's expected expert slices, dense/vision/projector values, and logits against an unpatched whole-model reference, with bounded collectives and rank-safe failure reporting. Include a real non-EP load/forward after preparation to protect the process-wide fallback. This is the central behavior, not extra helper coverage; CONTRIBUTING.md requires exercising the actual runtime path.

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.

Added in 3925d67: test_kimi_vl_loads_expert_parallel_and_matches_the_whole_model in tests/gpu/torch/utils/test_expert_parallel_load.py.

  • It writes a tiny Kimi-VL-shaped remote checkpoint (tests/_test_utils/torch/kimi_vl_tiny_modeling.py), with its experts stored per expert, and loads it expert-parallel through the spec's prepare and transformers' loader.
  • Each rank must hold its own experts, equal to the per-expert tensors fused.
  • The vision tower and projector must load as stored, and nothing may be left unplaced.
  • Each rank's logits must match the whole model's on one GPU.
  • Every rank's checks are gathered under bounded collectives.

Its non-distributed parts pass on CPU on transformers 5.18: the swap, the exact fusion, the nested plan at the VLM root as language_model.model.layers.*, and the logits. A run on 2 and 4 GPUs is requested; the description will record it. A non-expert-parallel load after preparation is left out: the tiny code's own language model is transformers' DeepSeek-V3, so it would not exercise the fallback.

Comment thread modelopt/torch/models/_kimi.py Outdated
raise ValueError(
f"{label}: the checkpoint declares quantization_config on text_config only, which the "
"native deepseek_v3 config replacing it drops, so the expert-parallel load would read "
"its experts as stored. Declare it on the top-level config too, or start from an "

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.

Bot comment.

The earlier remedy issue remains unchanged. Copying quantization_config to the root gets past this guard, but load_and_prepare_ep then reads it through _ckpt_quant_method and refuses it: _EP_SOURCE_FORMATS contains only None, and kimi_vl.prepare installs no decompressor. Could Kimi-VL recommend starting from an unquantized/dequantized checkpoint instead, reserving root-config advice for an adapter that actually supports decompressing the scheme? Cover the supported remedy through loading rather than only asserting the first refusal.

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.

Fixed in fed6e2f. The refusal now names the way in, an unquantized copy of the model. It no longer suggests copying the scheme to the top-level config, which only leads to the expert-parallel load's own refusal of a quantized checkpoint.

# See the License for the specific language governing permissions and
# limitations under the License.

"""Expert-parallel adapter for Kimi-VL-A3B (``kimi_vl``), a remote-code VLM on a Moonlight MoE.

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.

Bot comment.

The previous filename request is still open. These cases primarily exercise modelopt/torch/models/kimi_vl/ep.py::prepare; CONTRIBUTING.md says test filenames must mirror their production module. Move the adapter tests to tests/unit/torch/models/kimi_vl/test_ep.py. If shared _kimi.py cases become direct helper tests, place those separately in test_kimi.py rather than retaining the adapter-specific name.

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.

Moved in 6a84c62 to tests/unit/torch/models/kimi_vl/test_ep.py, mirroring kimi_vl/ep.py. The directory is a package, so it can hold a test_ep.py beside the other adapters'.

Comment thread modelopt/torch/models/kimi_vl/ep.py Outdated
decompressor (the checkpoint ships plain bf16 per-expert weights).

Challenge (identical to kimi_k25): the bundled ``modeling_kimi_vl.py`` builds its language model from
an INLINE ``DeepseekV3ForCausalLM`` whose ``DeepseekV3MoE`` is an ``nn.ModuleList`` of per-expert

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.

Bot comment.

The agreed documentation pass has not landed. Please replace the challenge/solution walkthrough with a short adapter contract and keep the design discussion in the PR description, per CONTRIBUTING.md. The current explanation also remains stale: _native_for_the_prepared_config dispatches on the native config's type, not on dropping auto_map, and the patched binding is a per-config function, not simply the native class. Remove the old validation-version prose or put actual testing information in the PR body.

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.

Done in 6a84c62. The module docstring is now the adapter's contract, close to your wording, and the walkthrough is in the PR description.

keeps off expert parallelism builds from the config as read, and so gets the checkpoint's own
language model, as it would with no patch.
"""
from transformers import DeepseekV3Config

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.

Bot comment.

The earlier local-import documentation request is still open. The module-scope comment explains the local ModelOpt imports, but not these Transformers imports or those in the shim/patch helpers. pyproject.toml makes Transformers an optional [hf] dependency, and model registration imports this module. Keep these imports lazy and add a brief comment naming that concrete reason, as CONTRIBUTING.md requires.

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.

Done in fed6e2f. _kimi's module-scope comment now says why both modelopt.torch.utils and transformers are imported inside its functions: the model registry imports this module, and it must import without transformers, the optional [hf] extra.

modeling_source="remote_code",
# prepare swaps a native deepseek_v3 in as the language model, with deepseek_v3's plan.
ep_spec=EPSpec(prepare=prepare, prepared_ep_plan=BASE_MODEL_EP_PLAN),
)

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.

Bot comment.

The PR checklist still says this is additive and 'runs only on the expert-parallel path', but registering this hook makes resolve_distribution_plan run it on every rank before deciding the plan whenever EP remains a candidate. It can therefore install shims/patches or raise during a decision that ultimately selects a non-EP layout. The main description already explains the per-config fallback; please make the checklist consistent with it. The later #2748 change cannot establish an EP-only guarantee for this independently landable PR.

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.

Fixed in the description. The checklist now says what registering the spec reaches beyond the expert-parallel load in this PR on its own:

  • In a multi-rank run where expert parallelism is still a candidate, every rank runs prepare before the plan is decided. That shims the code's imports and patches its modeling module, which builds the native class only for the config prepare made. It can also refuse the checkpoint, failing the decision.
  • A run that would leave the MoE whole on every rank, with nothing asking for that, is refused.
  • A single-process load is unchanged.

#2748 later runs prepare only once the plan is decided expert-parallel.

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11b0-kimi-vl branch from 1c2910a to 3925d67 Compare October 31, 2026 00:42
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-base-11b-kimi branch from 8bb2e35 to cb62c2b Compare October 31, 2026 00:42
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 3925d67

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

meenchen-nv, on your two reviews: every item is answered in its thread. The duplicates the second review re-posted point to the same commits.

  • The loading and logit-parity GPU case for the Kimi-VL adapter is added (3925d67), with the cluster run requested.
  • The quantization remedy now names an unquantized copy (fed6e2f).
  • The tests mirror kimi_vl/ep.py (6a84c62).
  • The module docstring is the adapter's contract, and the walkthrough is in the description (6a84c62).
  • The local transformers imports are documented (fed6e2f).
  • The checklist now says what registering the spec reaches beyond the expert-parallel load.

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

Comment on lines +21 to +26
checkpoint's own language model is a DeepSeek-V3 whose MoE holds per-expert Linears; here the name is
transformers' DeepSeek-V3 itself, since only the native class the adapter swaps in is built.
"""

from torch import nn
from transformers import DeepseekV3Config, DeepseekV3ForCausalLM, PretrainedConfig, PreTrainedModel

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] The stand-in's module-level DeepseekV3ForCausalLM is transformers' native class, so the new GPU test cannot detect a regression in the one thing the adapter exists to do.

What. patch_language_model_to_native replaces this name, and _native_for_the_prepared_config is then built with own_class is native_class — both isinstance branches return the same class. So test_kimi_vl_loads_expert_parallel_and_matches_the_whole_model would pass byte-for-byte if the swap (and the tie_weights shim, since it rides the same branch) became a no-op: language_model.model.layers[1].mlp.experts would already be the fused native experts, the per-expert tensors would still fuse via the native WeightConverter, and the logits would still match.

Why it matters. What the test does prove end-to-end is real and worth having — per-expert → fused 3-D fusion on each rank's own slice, vision tower/projector preserved, nothing unplaced, logit parity. But the swap itself is still covered only by the unit stand-in in tests/unit/torch/models/kimi_vl/test_ep.py, where the remote LM is a stub that is never loaded or forwarded. The no-class loud-failure case is likewise the only guard that the real Kimi-VL layout (an nn.ModuleList of per-expert Linears) never reaches the EP load. That leaves the reviewer's "end-to-end loading coverage" ask only partly closed, and in particular nothing exercises a real load of the per-expert module layout.

Suggested fix. Give the stand-in a module-level name the swap must actually replace, so building it is the regression signal. It never gets constructed on the passing path, so it needs no working forward:

class _OwnDeepseekV3ForCausalLM(DeepseekV3ForCausalLM):
    """The checkpoint's own: routed experts as per-expert Linears, which EP cannot shard."""

    def __init__(self, config):
        super().__init__(config)
        for layer in self.model.layers:
            if hasattr(layer.mlp, "experts"):
                layer.mlp.experts = nn.ModuleList(
                    nn.Linear(config.hidden_size, config.moe_intermediate_size, bias=False)
                    for _ in range(config.n_routed_experts)
                )


# The name the adapter swaps for transformers' native class.
DeepseekV3ForCausalLM = _OwnDeepseekV3ForCausalLM

With that, a no-op swap builds the ModuleList layout and the expert-slice assertions fail loudly instead of passing. Keep the docstring note, but say the name is the checkpoint's own class rather than the native one.

Non-blocking — the current combination of unit and GPU coverage is defensible, but this is a few lines for a materially stronger 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 3b5d7e2. The stand-in's module-level DeepseekV3ForCausalLM is now the checkpoint's own class, which holds its routed experts as per-expert Linears. Only a swap that misses builds it, so a no-op swap would fail the GPU case's expert checks. Its CPU dry run still passes: the swap, the exact fusion and the logits.

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 3b5d7e2

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11b0-kimi-vl branch from 3b5d7e2 to d2321c1 Compare October 31, 2026 04:56
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-base-11b-kimi branch from cb62c2b to b6e1f97 Compare October 31, 2026 04:56
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test d2321c1

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11b0-kimi-vl branch from d2321c1 to cb90789 Compare October 31, 2026 19:53
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-base-11b-kimi branch from b6e1f97 to abca417 Compare October 31, 2026 19:53
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test cb90789

Kimi-VL ships its own modeling, which builds the language model's MoE
from an nn.ModuleList of per-expert Linears that transformers' expert-
parallel machinery cannot shard. Its text backbone is DeepSeek-V3, so
the adapter swaps in transformers' native deepseek_v3 language model
(fused gate_up_proj/down_proj experts, and the plan to shard them) and
keeps the bundled vision tower and projector from the checkpoint's code.

The patch Kimi-VL and Kimi-K2.5 share lives in models/_kimi.py: the
refusals prepare runs before it changes anything (transformers' own
port, no text_config, no checkpoint path, untrusted code, a scheme on
text_config alone), the import shims for symbols transformers 5 dropped,
and the swap of the module-level DeepseekV3ForCausalLM, kept on a module
reload through keep_patched, with a tie_weights wrapper for the kwargs
newer transformers passes.

deepseek_v3's expert-parallel plan becomes a shared BASE_MODEL_EP_PLAN,
which the spec states as the plan prepare attaches, so a run at an EP
degree of 1, which skips prepare, still finds the experts the
checkpoint's own code builds and refuses to hold them whole on every
rank.

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

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Both Kimi checkpoints' code builds its routed experts at .mlp.experts,
so the moe parameter varied nothing. The stand-in builds them there,
and the test takes the model type only.

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

The spec registers only its expert-parallel section, and is_moe's
fallbacks do not recognize the native DeepseekV3MoE blocks prepare
installs, so the HF export's MoE-block handling skips them until the
sections are shared. The tie_weights shim drops missing_keys: a tied
checkpoint would still load its embedding into both, with lm_head.weight
reported missing (checked on transformers 5.18 and 5.5.4).

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Deciding the plan applies the model spec to a copy of the config on
every rank before the plan is known, and the Kimi adapters' prepare
patches the checkpoint's modeling module for the process. A plan that
then keeps the load off expert parallelism (replicated, or FSDP2) builds
from the config as read, and the patched VLM class built transformers'
native DeepseekV3ForCausalLM from the checkpoint's own text config,
which it cannot take (on a stand-in, an AttributeError on pad_token_id).

The swapped name now builds the native class only for the native
DeepseekV3Config prepare puts on text_config, and the checkpoint's own
class for its own config, so that load gets the language model it would
with no patch. Both checkpoints' VLM classes use the name only to build
their language model.

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

keeps the RuntimeError for a transformers below what the model's spec
needs, which names the release. The remote-Kimi case expects whichever
arm its transformers takes.

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

From meenchen-nv's review: the refusal of a quantization_config that
only text_config declares advised declaring it on the top-level config
too, which gets past this refusal only to meet the expert-parallel
load's own refusal of a quantized checkpoint. It now names the way in,
an unquantized copy. The module-scope comment also says why transformers
is imported inside _kimi's functions: the optional [hf] extra, and the
model registry imports this module.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
From meenchen-nv's review: kimi_vl/ep.py's module docstring is now the
adapter's contract, and its walkthrough moved to the PR description.
The adapter's tests move to tests/unit/torch/models/kimi_vl/test_ep.py,
mirroring kimi_vl/ep.py, as CONTRIBUTING asks; the directory is a
package, so it can hold a test_ep.py beside the other adapters'.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
From meenchen-nv's review: no test loaded checkpoint tensors through the
Kimi-VL adapter. New 2-GPU case test_kimi_vl_loads_expert_parallel_and_
matches_the_whole_model writes a tiny Kimi-VL-shaped remote checkpoint
(tests/_test_utils/torch/kimi_vl_tiny_modeling.py), its experts stored per
expert, and loads it expert-parallel through the spec's prepare: each
rank's native experts must equal the per-expert tensors fused, the vision
tower and projector must load as stored, nothing may be left unplaced,
and each rank's logits must match the whole model's on one GPU, with
every rank's checks gathered under bounded collectives. Its
non-distributed parts were checked on CPU: the swap, the exact fusion,
the nested plan at the VLM root and the logits.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
From Claude's review: the stand-in's module-level DeepseekV3ForCausalLM
was transformers' native class, so the GPU case would pass even if the
adapter's swap did nothing. It is now the checkpoint's own: routed
experts as per-expert Linears, which only a swap that misses builds, and
the case's expert checks then fail.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-base-11b-kimi branch from abca417 to dc200c3 Compare October 31, 2026 20:06
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-11b0-kimi-vl branch from cb90789 to 7849516 Compare October 31, 2026 20:06
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 7849516

@meenchen meenchen left a comment

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.

Bot review (gpt-6.1-sol) — DM the bot to share feedback.

Changes requested: EP-sharded loading coverage is fixed, but real non-EP loading and forwarding after the process-wide patch remain untested.

Needs action:

  • 💬 Author added the EP GPU case — extend test_expert_parallel_load.py with a real unprepared-config non-EP load/forward after preparation, checking checkpoint-owned weights and logits against an unpatched reference; see inline.

No action needed:

  • ✔️ Resolved since the previous review: EP fusion/slice/logit coverage, the vacuous swap fixture, quantization advice, test placement, concise adapter contract, local-import rationale, and the description’s preparation scope.

with torch.device("meta"):
assert type(remote.vlm(prepared).language_model) is DeepseekV3ForCausalLM
assert type(remote.vlm(as_read).language_model) is remote.remote_lm

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.

Bot comment.

The added GPU case closes the EP-sharded loading part of the previous request, but this non-EP path still checks only class identity on meta. Its remote LM has no weights or forward, while the GPU reference loads copy.deepcopy(prepared) and therefore selects the native LM too. Moreover, the tiny checkpoint's as-read text config is already a native DeepseekV3Config, so simply loading it without prepared would not exercise own_class.

This matters in this PR independently: resolve_distribution_plan can prepare a copy and then return the original config for a non-EP plan. The compatibility shims and tie wrapper remain installed, and the checkpoint-owned LM—not the native LM used to justify the unreachable legacy-mask stub—then runs.

Please extend the existing loading case with a working checkpoint-owned backbone and a distinct remote text-config type. After preparing a copy, load the original config without EP, assert the checkpoint-owned class and saved values survive, and run logits against an unpatched reference. That would settle the remaining backward-compatibility coverage request without adding more mock-only tests.

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.

On the transformers releases this adapter supports, a later load off expert parallelism builds and runs the checkpoint's own language model, unchanged, so I have not added a real-load comparison:

  • The swap chooses by config type (_native_for_the_prepared_config): transformers' native class only for transformers' own DeepseekV3Config, which only the copy prepare makes carries. Kimi-VL's configuration_kimi_vl.py, like Kimi-K2.5's configuration_deepseek.py, defines its own DeepseekV3Config(PretrainedConfig) and builds text_config from it, so the config as read builds the checkpoint's own class. This test pins that with a remote text-config type of its own: after prepare on a copy, the as-read config builds exactly the checkpoint's language-model class, so its __init__, forward and state-dict names are the checkpoint's.
  • The legacy-mask stub never lands there: transformers 5.5.4 through 5.18.0 still define _prepare_4d_causal_attention_mask, and the shims only fill names a release lacks, so the checkpoint's own DeepseekV3Model.forward calls transformers' function. On those releases the shims add only is_torch_fx_available, returning False, which makes the remote code skip torch.fx.wrap.
  • Nothing else prepare installs reaches that load's weights or forward. Kimi-VL installs no decompressor and registers no weight conversion, and the tie_weights wrapper drops the keyword arguments and calls the checkpoint's own tie_weights, which the test above this one pins.
  • From [OMNIML-6078] [2/2] Add the Kimi-K2.5 expert-parallel adapter #2748, the plan decision runs no prepare for a plan off expert parallelism at all, only the build step, which installs no swap: test_only_an_expert_parallel_plan_patches_the_checkpoint_s_code[fsdp2|whole] pins that the module keeps its own DeepseekV3ForCausalLM and that the loader hooks are unchanged.

A real load and forward would run that same class through the same loader, comparing it with itself.

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