Repository navigation
[OMNIML-6078] [2/2] Add the Kimi-K2.5 expert-parallel adapter #2748
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: shengliangx/hfep-11b0-kimi-vl
Are you sure you want to change the base?
Changes from all commits
2a51f38
21d43db
3c0336b
15c11eb
80a8a26
bced888
3aade0d
8af85cf
1eed0d0
ea86e8f
fe075f4
978b270
c83024c
84c06e0
5ae6705
7a1f1cf
b88d349
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -92,6 +92,7 @@ | |
| minimax_m3_vl, | ||
| llama4_text, | ||
| kimi_vl, | ||
| kimi_k25, | ||
| nemotron_h_omni, | ||
| qwen2_moe, | ||
| qwen3, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """kimi_k25 specs (HF model type ``kimi_k25``).""" | ||
|
|
||
| from . import specs |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """DP=EP a2a support for Kimi-K2.5 (``kimi_k25``). | ||
|
|
||
| a remote-code VLM whose text backbone is a DeepSeek-V3 variant with compressed-tensors-compressed experts. | ||
| Kimi-K2.6 and Kimi-K2.7-Code ship the same model type and modeling classes. | ||
|
|
||
| The challenge: Kimi's bundled ``modeling_deepseek.py`` defines ``DeepseekV3MoE`` as an | ||
| ``nn.ModuleList`` of per-expert ``DeepseekV3MLP`` (separate ``gate_proj`` / ``up_proj`` / | ||
| ``down_proj`` Linears per expert). That layout is incompatible with transformers' EP machinery, | ||
| which expects the **fused** 3-D ``gate_up_proj`` / ``down_proj`` Parameters that the | ||
| transformers-native ``deepseek_v3`` modeling ships. We need the native modeling to get EP-capable | ||
| experts, but we also want the rest of the VLM (vision tower + mm projector) to come from Kimi's | ||
| custom code. | ||
|
|
||
| Solution -- monkey-patch ``modeling_kimi_k25``'s ``DeepseekV3ForCausalLM`` import to point at the | ||
| transformers-native class, AND swap ``hf_config.text_config`` to a transformers-native | ||
| ``DeepseekV3Config`` (the native class won't accept Kimi's custom config subclass). Then | ||
| ``KimiK25ForConditionalGeneration.__init__`` (which does ``self.language_model = | ||
| DeepseekV3ForCausalLM(config.text_config)``) builds the native EP-capable model for the language | ||
| sub-model, while vision_tower + mm_projector are still built from Kimi's custom code. | ||
|
|
||
| On-the-fly compressed-tensors expert decompress happens during from_pretrained's | ||
| ``WeightConverter`` machinery (installed by ``_onfly_decompress.install_onfly_decompress``); the | ||
| fused per-expert source ``.weight`` tensors are decompressed + merged into the 3-D | ||
| ``gate_up_proj`` / ``down_proj`` parameters. Format-agnostic (compressed_tensors decides the | ||
| dequant from the checkpoint's quantization_config). | ||
|
|
||
| EP plan: attached to the (now native) ``text_config``. transformers' ``post_init`` aggregates | ||
| child-module plans up to the root with name prefix, so the final root ``_ep_plan`` keys end up as | ||
| ``language_model.model.layers.*.mlp.experts.{gate_up_proj,down_proj,...}`` -- matching the actual | ||
| parameter paths in the loaded VLM. | ||
|
|
||
| Version floor: transformers >= 5.8, which the spec declares. The on-the-fly decompressor, installed | ||
| for a compressed-tensors checkpoint, needs it: earlier releases' ``Concatenate`` trips over the | ||
| sibling keys it has already consumed. The decompressor couples tightly to | ||
| transformers' still-evolving ``WeightConverter`` conversion-ops API. | ||
| """ | ||
|
|
||
| from .. import _kimi, _onfly_decompress | ||
|
|
||
| # The checkpoint formats prepare has the experts decompress as they load; the spec states them for | ||
| # the plan decision, which runs before prepare. | ||
| DECOMPRESSED_FORMATS = ("compressed-tensors",) | ||
|
|
||
|
|
||
| def make_buildable( | ||
| hf_config, *, ckpt_path: str | None = None, trust_remote_code: bool = False, **_ | ||
| ) -> None: | ||
| """Have the checkpoint's own code import and build on this transformers. | ||
|
|
||
| The plan decision needs it before ``prepare``; it changes nothing the model computes or loads. | ||
| """ | ||
| _kimi.make_buildable( | ||
| "modeling_kimi_k25", | ||
| "KimiK25ForConditionalGeneration", | ||
| hf_config, | ||
| ckpt_path, | ||
| trust_remote_code, | ||
| ) | ||
|
|
||
|
|
||
| def prepare(hf_config, *, ckpt_path: str | None = None, trust_remote_code: bool = False, **_): | ||
| """Load Kimi-K2.5's language model as the transformers-native deepseek_v3. | ||
|
|
||
| Swap text_config to native deepseek_v3 + monkey-patch Kimi's modeling to use the native | ||
| DeepseekV3ForCausalLM + apply the EP plan + install the on-the-fly decompressor for | ||
| compressed-tensors experts. | ||
| """ | ||
| # Not at module scope: the model registry imports this module, and modelopt.torch.utils.moe | ||
| # imports the registry. | ||
| from modelopt.torch.utils.moe import apply_distributed_support | ||
|
|
||
| ckpt_path = _kimi.checkpoint_to_patch("kimi_k25", hf_config, ckpt_path, trust_remote_code) | ||
|
|
||
| # 1. Install on-the-fly compressed-tensors expert decompressor. Operates on transformers-native | ||
| # deepseek_v3's per-expert WeightConverter (which fuses per-expert .weight tensors into the | ||
| # 3-D gate_up_proj/down_proj Parameters). Format-agnostic; driven by checkpoint's | ||
| # quantization_config, which Kimi-K2.5 carries only on the text_config step 2 replaces. It | ||
| # goes first, so a checkpoint it refuses leaves everything as it was. Experts stored | ||
| # otherwise are left to the expert-parallel load's format handling, which reads their scheme | ||
| # from the copy of quantization_config Kimi's config class keeps on the top-level config; | ||
| # step 2 leaves that copy alone. A checkpoint without that copy is refused. | ||
| quantization_config = _onfly_decompress._find_quantization_config(hf_config) or {} | ||
| if quantization_config.get("quant_method") in DECOMPRESSED_FORMATS: | ||
| _onfly_decompress.install_onfly_decompress(hf_config) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The decompressor is installed (process-wide hooks, and the config is marked) before the compat shims,
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Peers fail fast.
The hooks the decompressor installed before such a failure stay installed, but the run is over on every rank.
Comment on lines
+97
to
+99
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION] The new test already compensates by deriving its expectation from # 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,
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Taken in 777ac2a. |
||
| else: | ||
| _kimi.refuse_quantization_only_on_text_config("kimi_k25", hf_config) | ||
|
Comment on lines
+97
to
+101
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION] The adapter reaches into Two consequences, both mild: the A one-line predicate on # in _onfly_decompress.py
def is_compressed_tensors(hf_config) -> bool:
"""Whether the checkpoint's experts are compressed-tensors-compressed, so install_onfly_decompress applies."""
return (_find_quantization_config(hf_config) or {}).get("quant_method") == "compressed-tensors"# here
if _onfly_decompress.is_compressed_tensors(hf_config):
_onfly_decompress.install_onfly_decompress(hf_config)
else:
_kimi.refuse_quantization_only_on_text_config("kimi_k25", hf_config)Behaviour-identical; non-blocking.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not taken: the check has one caller and no twin.
Comment on lines
+97
to
+101
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION] The discriminator reads only the first
The cheap hardening is to test every level the load reads rather than only the first, e.g. levels = _onfly_decompress._config_levels(hf_config)
schemes = {
(qc.get("quant_method") if isinstance(qc, dict) else getattr(qc, "quant_method", None))
for c in levels
if (qc := getattr(c, "quantization_config", None)) is not None
}
if "compressed-tensors" in schemes:
if len(schemes) > 1:
raise ValueError(
f"kimi_k25: the checkpoint declares disagreeing quantization schemes {sorted(schemes)} "
"across its config levels; the expert-parallel load cannot tell which describes the experts."
)
_onfly_decompress.install_onfly_decompress(hf_config)
else:
_kimi.refuse_quantization_only_on_text_config("kimi_k25", hf_config)Non-blocking — it needs a self-inconsistent checkpoint — but it keeps the "a checkpoint we cannot place is refused before anything changes" property that the rest of step 1 is built around, instead of silently mis-routing. If you'd rather not grow the branch, a one-line
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a case a real checkpoint can reach, so I'm keeping it as is.
The predicate idea is reasonable if a second adapter ever needs the same question; today this is the only caller. |
||
|
|
||
| # Before any remote-code load: this plugin's get_class_from_dynamic_module below, and | ||
| # from_pretrained later in load_and_prepare_ep. | ||
| _kimi.make_importable() # the swap below loads the code and adapts its tie_weights | ||
|
|
||
| # 2. Replace text_config with a transformers-native DeepseekV3Config built from the same fields. | ||
| # Kimi's __init__ passes config.text_config to DeepseekV3ForCausalLM; the native class rejects | ||
| # Kimi's custom config subclass. to_native_backbone_config also drops auto_map + | ||
| # quantization_config so transformers builds the native modeling instead of re-routing back | ||
| # to Kimi's remote code via auto_map. | ||
| hf_config.text_config = _onfly_decompress.to_native_backbone_config(hf_config) | ||
|
shengliangxu marked this conversation as resolved.
Resolved
shengliangxu marked this conversation as resolved.
Resolved
|
||
|
|
||
| # 3. Apply the EP plan to the now-native text_config. transformers' post_init walks children | ||
| # and prefixes their _ep_plan onto the parent's: text_config's plan ends up at the VLM root | ||
| # as `language_model.model.layers.*.mlp.experts.{gate_up_proj,down_proj}`. | ||
| hf_config.text_config = apply_distributed_support("deepseek_v3", hf_config.text_config) | ||
|
|
||
| # 4. Force-load Kimi's modeling_kimi_k25 module (so we can patch its DeepseekV3ForCausalLM | ||
| # symbol) and swap to the native class. | ||
| _kimi.patch_language_model_to_native( | ||
| "kimi_k25", | ||
| "modeling_kimi_k25", | ||
| "KimiK25ForConditionalGeneration", | ||
| ckpt_path, | ||
| trust_remote_code, | ||
| ) | ||
| return hf_config | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """kimi_k25 specs (HF model type ``kimi_k25``). | ||
|
|
||
| Kimi-K2.5 (Kimi-K2.6 and Kimi-K2.7-Code share its model type): the compressed-expert sibling of | ||
| ``kimi_vl`` -- same swap to a native deepseek_v3 language model, plus on-the-fly decompression of | ||
| its packed experts at load. | ||
|
|
||
| Only the expert-parallel section is registered under ``kimi_k25``: the MoE and export sections stay | ||
| ``deepseek_v3``'s, so lookups by ``kimi_k25`` find neither yet. Nor do ``is_moe``'s name and structure | ||
| fallbacks recognize the native ``DeepseekV3MoE`` blocks ``prepare`` installs, so the HF export's | ||
| MoE-block handling skips them. Sharing the sections is left to a follow-up. | ||
| """ | ||
|
|
||
| from ..deepseek_v3.specs import BASE_MODEL_EP_PLAN | ||
| from ..specs import EPSpec, ModelSpec, register | ||
| from .ep import DECOMPRESSED_FORMATS, make_buildable, prepare | ||
|
|
||
| __all__: list[str] = [] | ||
|
|
||
| register( | ||
| ModelSpec( | ||
| model_type="kimi_k25", | ||
| # transformers ships Kimi-K2.5 itself from 5.13, so modeling_source stays "transformers": the | ||
| # registry pairs it with that release, and a "remote_code" claim would be stale from 5.13 on. | ||
| # Expert parallelism still runs on the checkpoint's own code (``trust_remote_code=True``), | ||
| # which ``prepare`` patches, refusing the port. So the version describes transformers' port, | ||
| # not the model this spec's load builds: a MoE or export section added here must name the | ||
| # native deepseek_v3 language model's modules. | ||
| min_transformers_version="5.13", | ||
|
shengliangxu marked this conversation as resolved.
Resolved
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION] Nothing consumes Worth extending the comment above to say the 5.13 claim is registry bookkeeping for the port, not the modeling this spec's EP path loads, so whoever adds the MoE section knows the version claim does not describe it.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Taken in e4762e5: the comment says the 5.13 claim describes transformers' own port, not the model this spec's load builds, and that a MoE or export section added later must name the native deepseek_v3 language model's modules.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [SUGGESTION] The comment explains why Why it matters: the field's docstring ( The choice is also forced: declaring Suggest extending the existing comment to name the consequence, e.g.: model_type="kimi_k25",
# transformers ships Kimi-K2.5 itself from 5.13, so modeling_source stays "transformers"
# (the registry pairs it with a release, and a "remote_code" claim would be stale from
# 5.13 on) -- even though this spec's load runs on the checkpoint's own code
# (``trust_remote_code=True``), which ``prepare`` patches, and refuses the port. So the
# version describes transformers' port, not the model this spec's load builds: a MoE or
# export section added here must name the native deepseek_v3 language model's modules.
min_transformers_version="5.13",
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Taken in e1c538f. The comment now says why
The docstring also gets the same note as Kimi-VL's: |
||
| # prepare swaps a native deepseek_v3 in as the language model, with deepseek_v3's plan, | ||
| # and decompresses the experts as they load, which needs transformers 5.8. | ||
| ep_spec=EPSpec( | ||
| prepare=prepare, | ||
| prepared_ep_plan=BASE_MODEL_EP_PLAN, | ||
| plan_config_attr="text_config", | ||
| make_buildable=make_buildable, | ||
| decompressed_formats=DECOMPRESSED_FORMATS, | ||
| requires_transformers="5.8", | ||
| ), | ||
| ) | ||
| ) | ||
|
Comment on lines
+34
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [IMPORTANT Compatibility] Registering
The blocks are Consequence: This is an absence rather than a regression (before this PR # deepseek_v3/specs.py
MOE_SPEC = MoESpec(
block_names=("DeepseekV3MoE",),
expert_linear_names=("gate_proj", "down_proj", "up_proj"),
gate_up_pair=("gate_proj", "up_proj"),
)
# kimi_k25/specs.py and kimi_vl/specs.py
register(
ModelSpec(
model_type="kimi_k25",
...
export_spec=ExportSpec(grouped_expert_export=True),
moe_spec=MOE_SPEC,
)
)If leaving the layout out is deliberate for this slice of the stack (e.g. export lands in a later PR), a one-line comment here saying so would keep the next reader from reading it as an omission.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Postponed: not a regression, and nothing changes on the expert-parallel path these specs serve. Before this PR neither |
||
Uh oh!
There was an error while loading. Please reload this page.