Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
7440b62
Add the Kimi-K3 expert-parallel adapter
shengliangxu Oct 11, 2026
0d705d3
Size Kimi-K3's fused experts from the MoE block's latent decision
shengliangxu Oct 11, 2026
8749a69
Read KDA's beta-sigmoid flag off the call's keywords
shengliangxu Oct 11, 2026
1ac6768
Skip the tests below the transformers release the code requires
shengliangxu Oct 11, 2026
47dea2c
Leave Kimi-K3's latent-expert projections and residual gates unquanti…
shengliangxu Oct 11, 2026
04d2298
Keep Kimi's mm_projector in BF16 by default, as the vision block intends
shengliangxu Oct 11, 2026
76176b3
Patch each Kimi-K3 checkpoint's own remote module
shengliangxu Oct 15, 2026
b59a381
State the expert plan Kimi-K3's prepare attaches
shengliangxu Oct 16, 2026
09e370f
Run the Kimi-K3 bf16-experts test without compressed-tensors
shengliangxu Oct 16, 2026
7cc71c3
Trim the Kimi-K3 adapter tests: fold duplicates into covering tests
shengliangxu Oct 16, 2026
5613527
Refuse to import an untrusted checkpoint's code in the Kimi-K3 adapter
shengliangxu Oct 16, 2026
ffa1882
Let a missing conversion API stop the Kimi-K3 expert-parallel load
shengliangxu Oct 22, 2026
7142db1
Decompress Kimi-K3 only with a quantization_config, and let its error…
shengliangxu Oct 23, 2026
f760d5f
Refuse a Kimi-K3 prepare without the checkpoint's path, and declare t…
shengliangxu Oct 24, 2026
ffdc408
Stop a Kimi-K3 load whose remote code drifts from what prepare patches
shengliangxu Oct 24, 2026
3d74422
Register torch_a2a in Kimi-K3's prepare, before the plan decision bui…
shengliangxu Oct 25, 2026
9df995b
Give Kimi-K3's cache the query offset transformers 5.15 asks for
shengliangxu Oct 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modelopt/torch/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
minimax_m3_vl,
llama4_text,
kimi_vl,
kimi_k3,
kimi_k25,
nemotron_h_omni,
qwen2_moe,
Expand Down
18 changes: 18 additions & 0 deletions modelopt/torch/models/kimi_k3/__init__.py
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_k3 specs (HF model type ``kimi_k3``)."""

from . import specs
646 changes: 646 additions & 0 deletions modelopt/torch/models/kimi_k3/ep.py

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions modelopt/torch/models/kimi_k3/specs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 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_k3 specs (HF model type ``kimi_k3``).

Kimi-K3: a remote-code VLM on the kimi_linear backbone, with packed mxfp4 experts that are
decompressed at load.
"""

from ..specs import EPSpec, ModelSpec, register
from .ep import BASE_MODEL_EP_PLAN, prepare

__all__: list[str] = []

register(
ModelSpec(
model_type="kimi_k3",
modeling_source="remote_code",
ep_spec=EPSpec(
prepare=prepare,
prepared_ep_plan=BASE_MODEL_EP_PLAN,
# prepare's patches to the checkpoint's remote code were validated on this release.
requires_transformers="5.13",
),
)
)
Comment on lines +28 to +38

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] EPSpec already has a declarative slot for the transformers floor — requires_transformers ("Earliest transformers release that supports THIS model expert-parallel, when it is later than the framework floor", used by check_distributed_support, moe.py:170-174). minimax_m3_vl and qwen3_moe use it; here the floor is instead enforced imperatively by require_transformers("5.8", ...) inside ep.prepare.

Two consequences:

  • check_distributed_support("kimi_k3") — called on its own at model_load_utils.py:478 to refuse a run "before any weight is loaded" — does not refuse, and the user misses the "Or quantize it without expert parallelism, e.g. with FSDP2" hint that wrapper appends.
  • The floor is invisible to anything reading the registry.
        ep_spec=EPSpec(
            prepare=prepare,
            prepared_ep_plan=BASE_MODEL_EP_PLAN,
            requires_transformers="5.8",
        ),

Separately, worth reconciling the number: the declared floor is 5.8 (for the decompressor), but ep.py is written against 5.13 throughout — USER_REGISTERED_MAPPINGS is 5.13+ (the test fixture gates on exactly that, test_kimi_k3_ep.py:69), and the create_causal_mask / get_mask_sizes / tie_weights shims all say "tf>=5.13". The shims are conditional so they're no-ops on older releases, but if 5.8-5.12 is genuinely supported it would help to say so (and ideally to have run a load there); if it isn't, 5.13 is the honest floor.

@shengliangxu shengliangxu Oct 24, 2026 •

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 ebad207: the spec declares requires_transformers="5.13", the release the adapter's patches were developed and validated against, and prepare no longer checks a floor itself; a test sets 5.12 and checks check_distributed_support refuses it.

5 changes: 3 additions & 2 deletions modelopt/torch/utils/moe.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,8 +514,9 @@ def expert_fn(sorted_tok, offsets):
def register_torch_a2a():
"""Register everything the torch_a2a path needs into stock transformers (idempotent).

Done lazily from ``load_and_prepare_ep``, not at import time, so importing this module has no
transformers side effect:
Done lazily, not at import time, so importing this module has no transformers side effect: by
``load_and_prepare_ep``, and by a spec's ``prepare`` that names ``torch_a2a`` on the config a
model is built from before that load (Kimi-K3's). It registers:

- the ``torch_a2a`` experts forward into ``ExpertsInterface`` (``ALL_EXPERTS_FUNCTIONS``);
- two EP parallel styles into ``ALL_PARALLEL_STYLES``:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,25 @@
enable: false
- quantizer_name: 'output.*'
enable: false
# Kimi-K3: vLLM builds the latent-expert projections around the routed experts and the
# scalar residual gates unquantized (quant_config=None), so it cannot load a checkpoint
# that quantizes them. The [1, hidden] gates gain nothing from quantization either.
- quantizer_name: '*routed_expert_down_proj*'
enable: false
- quantizer_name: '*routed_expert_up_proj*'
enable: false
- quantizer_name: '*self_attention_res_proj*'
enable: false
- quantizer_name: '*mlp_res_proj*'
enable: false
- quantizer_name: '*output_attn_res_proj*'
enable: false
# Multimodal vision branch: keep the vision encoder (SigLIP / ViT) and any
# multimodal embedding projection in BF16 by default. Recipes that enable bare
# `*weight_quantizer` / `*input_quantizer` or `*mlp*` wildcards otherwise also
# match the vision tower (`model.vision_tower.*`, `model.visual.*`,
# Llama-4 `vision_model.*`) and the embedding projection (`model.embed_vision.*`,
# Llama-4 `multi_modal_projector.*`); quantizing the vision branch crashes
# Llama-4 `multi_modal_projector.*`, Kimi `mm_projector.*`); quantizing the vision branch crashes
# export / produces garbage image embeddings on VL models (gemma-4,
# Qwen3.5-VL — NVBugs 6293731, 6293762, 6294017; Llama-4-Scout — NVBugs
# 6359097, where `vision_model.patch_embedding.linear` has in_features=588,
Expand All @@ -71,6 +84,8 @@
enable: false
- quantizer_name: '*multi_modal_projector*'
enable: false
- quantizer_name: '*mm_projector*'
enable: false
- parent_class: 'nn.BatchNorm1d'
quantizer_name: '*'
enable: false
Expand Down
44 changes: 44 additions & 0 deletions tests/unit/recipe/test_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
"""

import argparse
import copy

import pytest
import torch
import torch.nn as nn

import modelopt.torch.quantization as mtq
from modelopt.recipe import load_recipe, presets
Expand Down Expand Up @@ -116,6 +118,48 @@ def test_default_exclusions_disable_mtp_at_any_depth():
assert not model.language_model.mtp.proj.input_quantizer.is_enabled


class _KimiK3Layer(nn.Module):
"""Kimi-K3's decoder-layer projections that vLLM builds unquantized, beside one it quantizes."""

def __init__(self, hidden=32, latent=16):
super().__init__()
self.self_attn = nn.Module()
self.self_attn.o_proj = nn.Linear(hidden, hidden, bias=False)
self.self_attention_res_proj = nn.Linear(hidden, 1, bias=False)
self.mlp_res_proj = nn.Linear(hidden, 1, bias=False)
self.block_sparse_moe = nn.Module()
self.block_sparse_moe.routed_expert_down_proj = nn.Linear(hidden, latent, bias=False)
self.block_sparse_moe.routed_expert_up_proj = nn.Linear(latent, hidden, bias=False)


@pytest.mark.parametrize("qformat", ["fp8", "nvfp4"])
def test_default_presets_leave_the_kimi_k3_projections_vllm_builds_unquantized(qformat):
# vLLM's Kimi-K3 model passes quant_config=None for these, so it cannot load them quantized.
# Kimi-K2.5 and Kimi-K3 also name their multimodal projector ``mm_projector``; like the other
# projector spellings it stays BF16 unless a recipe re-enables it.
model = nn.Module()
model.layers = nn.ModuleList([_KimiK3Layer()])
model.output_attn_res_proj = nn.Linear(32, 1, bias=False)
model.mm_projector = nn.Sequential(nn.Linear(32, 32), nn.GELU(), nn.Linear(32, 32))
config = copy.deepcopy(presets.QUANT_CFG_CHOICES[qformat])
config["algorithm"] = None # placement only, no calibration forward
mtq.quantize(model, config)

layer = model.layers[0]
for module in (
layer.block_sparse_moe.routed_expert_down_proj,
layer.block_sparse_moe.routed_expert_up_proj,
layer.self_attention_res_proj,
layer.mlp_res_proj,
model.output_attn_res_proj,
model.mm_projector[0],
model.mm_projector[2],
):
assert not module.weight_quantizer.is_enabled
assert not module.input_quantizer.is_enabled
assert layer.self_attn.o_proj.weight_quantizer.is_enabled


def test_local_hessian_layerwise_is_scoped_to_the_model_recipe():
# Layerwise calibration needs identifiable decoder layers, so it belongs to the model
# recipe the published numbers come from -- not to the shared preset, which also backs
Expand Down
Loading
Loading