Files
sglang/python/sglang/srt/models/kimi_k3.py
T
Kevin Mi 8ac19cc19f
PR Test (XPU) / finish (push) Blocked by required conditions
PR Test (Arm64) / check-changes (push) Successful in 10s
PR Test (NPU) / set-image-config (push) Successful in 1s
PR Test (NPU) / Recommend tests from coverage (push) Skipped
PR Test (NPU) / check-changes (push) Successful in 12s
PR Test (sgl-router) / gate (push) Successful in 8s
PR Test (Xeon) / check-changes (push) Successful in 9s
PR Test (XPU) / check-changes (push) Successful in 16s
pr-test-arm64.yml / pr-gate (push) Successful in 3s
PR Test (Arm64) / pr-gate (push) Successful in 3s
PR Test (Arm64) / build-test (push) Waiting to run
pr-test-npu.yml / pr-gate (push) Successful in 2s
PR Test (NPU) / pr-gate (push) Successful in 2s
PR Test (sgl-router) / tier-1 — lint (push) Failing after 33s
PR Test (sgl-router) / tier-2 — build + test (push) Skipped
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Skipped
PR Test (sgl-router) / tier-3 — k8s integration (push) Skipped
PR Test (sgl-router) / tier-3 — e2e (push) Skipped
pr-test-xpu.yml / pr-gate (push) Successful in 2s
PR Test (XPU) / pr-gate (push) Successful in 2s
pr-test-xeon.yml / pr-gate (push) Successful in 3s
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Waiting to run
PR Test (XPU) / multimodal-gen-test-1-gpu-xpu (push) Waiting to run
PR Test (Xeon) / pr-gate (push) Successful in 3s
PR Test (sgl-router) / finish (push) Successful in 1s
PR Test (Xeon) / build-test (gnr, gnr, xeon-gnr, stage-a-tp-test-cpu-intel) (push) Waiting to run
PR Test (Xeon) / build-test (spr1, 0, 3, spr, xeon-spr, stage-a-test-cpu-intel,stage-b-test-cpu-intel) (push) Waiting to run
PR Test (Xeon) / build-test (spr2, 1, 3, spr, xeon-spr, stage-a-test-cpu-intel,stage-b-test-cpu-intel) (push) Waiting to run
PR Test (Xeon) / build-test (spr3, 2, 3, spr, xeon-spr, stage-a-test-cpu-intel,stage-b-test-cpu-intel) (push) Waiting to run
Lint / lint (push) Failing after 2m51s
PR Test (NPU) / base-a-test-1-npu-a2 (push) Canceled after 0s
PR Test (NPU) / base-b-test-1-npu-a3 (push) Canceled after 0s
PR Test (NPU) / base-b-test-2-npu-a3 (push) Canceled after 0s
PR Test (NPU) / base-b-test-4-npu-a3 (push) Canceled after 0s
PR Test (NPU) / base-b-test-8-npu-a3 (push) Canceled after 0s
PR Test (NPU) / base-b-test-16-npu-a3 (push) Canceled after 0s
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (0) (push) Canceled after 0s
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (1) (push) Canceled after 0s
PR Test (NPU) / multimodal-gen-test-4-npu-a3 (0) (push) Canceled after 0s
PR Test (NPU) / multimodal-gen-test-4-npu-a3 (1) (push) Canceled after 0s
PR Test (NPU) / base-c-test-acc-2-npu-a3 (push) Canceled after 0s
PR Test (NPU) / base-c-test-acc-16-npu-a3 (push) Canceled after 0s
PR Test (NPU) / base-c-test-perf-2-npu-a3 (push) Canceled after 0s
PR Test (NPU) / base-c-test-perf-16-npu-a3 (push) Canceled after 0s
PR Test (NPU) / Analyze failure report (push) Canceled after 0s
PR Test (NPU) / setup-covstub (push) Canceled after 0s
PR Test (NPU) / pr-test-npu-finish (push) Canceled after 0s
pr-test-npu.yml / run (${{ fromJson(inputs.partitions).arr }}) (push) Canceled after 0s
[AMD][Kimi-K3] Fix deferred KDA gate projection and update DCP cookbook (#39066)
2026-09-22 06:35:12 +00:00

3920 lines
167 KiB
Python

# Kimi-K3 multimodal model: KimiLinear text backbone + MoonViT3d vision tower.
# Based on kimi_linear.py with K3-specific features:
# - Attention Residual (attn_res_block_size)
# - Latent MoE (routed_expert_hidden_size)
# - SiTU activation
# - MLA output gate (mla_use_output_gate)
# - Full-rank KDA gate (use_full_rank_gate)
import logging
import os
import re
from collections.abc import Iterable
from functools import cached_property
from types import SimpleNamespace
from typing import TYPE_CHECKING, List, Optional, Tuple
import torch
from torch import nn
from sglang.kernels.ops.attention.fla.fused_norm_gate import FusedRMSNormGated
from sglang.srt.configs.kimi_k3 import KimiK3Config
from sglang.srt.configs.kimi_linear import KimiLinearConfig
from sglang.srt.distributed import (
divide,
tensor_model_parallel_all_reduce,
)
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
use_symmetric_memory,
)
from sglang.srt.environ import envs
from sglang.srt.eplb.expert_distribution import get_global_expert_distribution_recorder
from sglang.srt.layers import (
k3_ar_fusion,
k3_gemm_ar,
k3_sp_collective,
zero_copy_context,
)
from sglang.srt.layers.activation import SiluAndMul, SituAndMul
from sglang.srt.layers.attn_residual import AttnResidual, aggregate_stream, get_cw
from sglang.srt.layers.dcp.planner import prepare_decode_context_parallel_metadata
from sglang.srt.layers.dp_attention import (
dp_gather_replicate,
dp_scatter,
get_global_dp_buffer,
get_local_dp_buffer,
is_allocation_symmetric,
is_dp_attention_enabled,
)
from sglang.srt.layers.layernorm import RMSNorm
from sglang.srt.layers.linear import (
ColumnParallelBatchedLinear,
ColumnParallelLinear,
MergedColumnParallelLinear,
MergedColumnParallelRepeatedLinear,
QKVParallelLinear,
ReplicatedLinear,
RowParallelLinear,
)
from sglang.srt.layers.logits_processor import LogitsProcessor
from sglang.srt.layers.moe import route_quant_handoff
from sglang.srt.layers.moe.ep_moe.layer import get_moe_impl_class
from sglang.srt.layers.moe.fused_moe_triton.layer import FusedMoE
from sglang.srt.layers.moe.topk import (
TopK,
TopKOutputFormat,
build_precomputed_topk_output,
precomputed_topk_postprocess_is_noop,
)
from sglang.srt.layers.moe.utils import (
RoutingMethodType,
get_moe_a2a_backend,
get_moe_runner_backend,
)
from sglang.srt.layers.quantization.base_config import QuantizationConfig
from sglang.srt.layers.quantization.fp8_utils import block_quant_dequant
from sglang.srt.layers.quantization.modelslim.modelslim import ModelSlimConfig
from sglang.srt.layers.radix_linear_attention import RadixLinearAttention
from sglang.srt.layers.utils import PPMissingLayer, get_layer_id
from sglang.srt.layers.vocab_parallel_embedding import (
ParallelLMHead,
VocabParallelEmbedding,
get_embedding_tp_kwargs,
)
from sglang.srt.managers.mm_utils import (
MultiModalityDataPaddingPatternMultimodalTokens,
general_mm_embed_routine,
)
from sglang.srt.managers.schedule_batch import (
Modality,
MultimodalDataItem,
MultimodalInputs,
)
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, PPProxyTensors
from sglang.srt.model_executor.runner import get_is_capture_mode
from sglang.srt.model_executor.runner_backend_utils.breakable_cuda_graph.context import (
is_in_breakable_cuda_graph,
)
from sglang.srt.model_loader.weight_utils import (
default_weight_loader,
maybe_remap_kv_scale_name,
sharded_weight_loader,
)
from sglang.srt.models.deepseek_common.attention_forward_methods.forward_methods import (
AttnForwardMethod,
)
from sglang.srt.models.deepseek_v2 import DeepseekV2AttentionMLA, MoEGate
from sglang.srt.models.kimi_k3_vl import (
KimiK3MultiModalProjector,
KimiK3VisionTower,
)
from sglang.srt.models.transformers import maybe_prefix
from sglang.srt.models.utils import WeightsMapper
from sglang.srt.multimodal.encoder_preprocessing import EncoderMediaProcessorConfig
from sglang.srt.multimodal.kimi_k3_image_processing import (
DEFERRED_PREPROCESSING_KEY,
fill_transparent_bg,
normalization_tensors,
to_chw_uint8,
)
from sglang.srt.multimodal.mm_utils import materialize_multimodal_features
from sglang.srt.runtime_context import (
get_exec,
get_parallel,
get_platform,
)
from sglang.srt.utils import is_hip, is_npu, make_layers
from sglang.srt.utils.common import (
BumpAllocator,
add_prefix,
get_bool_env_var,
rank0_log,
require_mlp_sync,
set_weight_attrs,
)
logger = logging.getLogger(__name__)
# `experts.<expert_id>.<w1|w2|w3>.` fragment of a checkpoint tensor name, the
# key FusedMoE.make_expert_params_mapping entries match on.
_EXPERT_WEIGHT_NAME = re.compile(r"experts\.\d+\.w[123]\.")
_is_hip = is_hip()
_is_npu = is_npu()
_aiter_k3_opt = get_bool_env_var("SGLANG_AITER_K3_OPT")
def _cdiv(a: int, b: int) -> int:
return (a + b - 1) // b
def _uses_modelopt_fp8_pb_wo(
quant_config: Optional[QuantizationConfig], prefix: str
) -> bool:
resolver = getattr(quant_config, "_resolve_quant_algo", None)
return resolver is not None and resolver(prefix) == "FP8_PB_WO"
def _uses_split_gguf_kv_b(
quant_config: Optional[QuantizationConfig],
) -> bool:
"""Whether a K3 checkpoint stores MLA K/V as separate GGUF tensors."""
return bool(getattr(quant_config, "supports_kimi_k3_split_gguf_kv_b", False))
def _maybe_map_fp8_pb_scale_name(name: str, params_dict: dict) -> str:
"""Map ModelOpt FP8_PB_WO scale keys to SGLang block-FP8 params."""
if name.endswith(".weight_scale"):
candidate = name.removesuffix(".weight_scale") + ".weight_scale_inv"
if candidate in params_dict:
return candidate
return name
def _get_k3_dense_weight(module: nn.Module) -> torch.Tensor:
"""Return a dense weight with serialized block-FP8 scales applied."""
weight = module.weight.data
if not hasattr(module, "weight_scale_inv"):
return weight
return block_quant_dequant(
weight,
module.weight_scale_inv,
module.quant_method.weight_block_size,
module.params_dtype,
)
def _k3_bf16_gemm(
x: torch.Tensor,
weight: torch.Tensor,
out: Optional[torch.Tensor] = None,
out_dtype: Optional[torch.dtype] = None,
) -> torch.Tensor:
"""F.linear / torch.mm with the same TGV dispatch module-level GEMMs get
through UnquantizedLinearMethod. The fused MoE front and the deferred
shared down GEMM call torch directly on raw merged weights, so the
--bf16-gemm-backend cutedsl selection would silently skip them."""
if out is None and out_dtype is not None and out_dtype != x.dtype:
out = torch.empty(
(x.shape[0], weight.shape[0]), dtype=out_dtype, device=x.device
)
if x.dtype == torch.bfloat16 and weight.dtype == torch.bfloat16:
from sglang.srt.layers.quantization.unquant import get_bf16_gemm_backend
if get_bf16_gemm_backend().is_cutedsl():
from sglang.kernels.ops.gemm.cutedsl_bf16_gemm import (
cutedsl_bf16_gemm,
cutedsl_bf16_gemm_out,
use_cutedsl_bf16_gemm,
)
if use_cutedsl_bf16_gemm(x.shape[0], weight.shape[0], weight.shape[1]):
if out is None:
return cutedsl_bf16_gemm(x, weight)
return cutedsl_bf16_gemm_out(x, weight, out)
if out is None:
return torch.nn.functional.linear(x, weight)
if out.dtype != x.dtype:
return torch.mm(x, weight.t(), out=out, out_dtype=out.dtype)
return torch.mm(x, weight.t(), out=out)
# Fully fused KDA decode step (kernels/ops/attention/kda_fused_decode). The
# model hands the output-norm gate to the KDA backend via an attempt-and-verify
# stash on the attention layer; unconsumed stashes fall back to the unfused
# chain + o_norm here.
def _merge_weights_as_views(
mods: list, pad_rows_to: int = 1
) -> tuple[torch.Tensor, list[int]]:
"""Cat module weights along dim 0; re-point each module's weight to a view
of the merged buffer so the original storage is freed (net extra memory ~0).
With pad_rows_to > 1 the merged buffer gets zero rows appended up to the
next multiple, so every row of the fused GEMM output stays 16-byte aligned
for vectorized consumers."""
ws = [m.weight.data for m in mods]
sizes = [w.shape[0] for w in ws]
pad = (-sum(sizes)) % pad_rows_to
if pad:
ws = ws + [ws[0].new_zeros((pad, ws[0].shape[1]))]
merged = torch.cat(ws, dim=0).contiguous()
off = 0
for m, n in zip(mods, sizes):
m.weight.data = merged[off : off + n]
off += n
return merged, sizes
# K3 cannot use LayerCommunicator: the attn-res aggregation kernels replace
# input_layernorm / post_attention_layernorm, which the communicator expects
# to own. Instead the MLP/MoE modules gather/scatter around their own body:
# attention and the attn-res buffers stay in local (per-DP-rank) token space,
# the MLP/MoE runs on the DP-gathered global batch, and the delayed prefix_sum
# add stays local, applied after the scatter back.
def _dp_local_buffer_group():
"""Symmetric-memory group for the local DP buffer (mirrors
CommunicateSummableTensorPairFn._scatter_hidden_states)."""
parallel = get_parallel()
if parallel.tp_size == parallel.attn_dp_size:
return get_parallel().tp_group
return parallel.attn_tp_group
def _sp_all_gather_rows(hidden_states: torch.Tensor) -> torch.Tensor:
"""Reassemble contiguous token shards, using the tuned K3 AG when covered."""
group = get_parallel().attn_tp_group
hidden_states = hidden_states.contiguous()
full = k3_sp_collective.all_gather(hidden_states)
if full is None:
full = torch.empty(
(hidden_states.shape[0] * group.world_size, hidden_states.shape[1]),
dtype=hidden_states.dtype,
device=hidden_states.device,
)
group.all_gather_into_tensor(full, hidden_states)
return full
def _sp_local_rows(hidden_states: torch.Tensor) -> slice:
"""Full-batch row interval owned by this rank's contiguous token shard."""
group = get_parallel().attn_tp_group
lo = group.rank_in_group * hidden_states.shape[0]
return slice(lo, lo + hidden_states.shape[0])
class KimiK3MLP(nn.Module):
"""K3 MLP; SiLU or SiTU activation."""
def __init__(
self,
hidden_size: int,
intermediate_size: int,
hidden_act: str,
quant_config: Optional[QuantizationConfig] = None,
reduce_results: bool = True,
prefix: str = "",
activation_situ_beta: float | None = None,
activation_situ_linear_beta: float | None = None,
tp_rank: Optional[int] = None,
tp_size: Optional[int] = None,
) -> None:
super().__init__()
# The Ascend path shards the dense MLP inside each attention-TP
# replica. The GPU K3 refactor instead gathers all DP rows and shards
# this one dense layer over the full TP group. Keep the GPU default,
# but allow the NPU launcher to retain the proven attention-TP layout
# without a device-type branch in shared model code.
self._dense_attn_tp = (
get_parallel().enable_dense_mlp_attn_tp
and is_dp_attention_enabled()
and tp_rank is None
and tp_size is None
)
if self._dense_attn_tp:
tp_rank = get_parallel().attn_tp_rank
tp_size = get_parallel().attn_tp_size
_tp_kwargs = (
dict(tp_rank=tp_rank, tp_size=tp_size) if tp_size is not None else {}
)
self.gate_up_proj = MergedColumnParallelLinear(
hidden_size,
[intermediate_size] * 2,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.gate_up_proj",
**_tp_kwargs,
)
self.down_proj = RowParallelLinear(
intermediate_size,
hidden_size,
bias=False,
quant_config=quant_config,
reduce_results=reduce_results,
use_dp_attention_reduce=self._dense_attn_tp,
prefix=f"{prefix}.down_proj",
**_tp_kwargs,
)
if hidden_act == "silu":
self.act_fn = SiluAndMul()
elif hidden_act == "situ":
self.act_fn = SituAndMul(
beta=activation_situ_beta or 1.0,
linear_beta=activation_situ_linear_beta,
)
else:
raise ValueError(f"Unsupported activation: {hidden_act}")
self._dp_attention = is_dp_attention_enabled()
def forward(
self,
hidden_states: torch.Tensor,
*,
prefix_sum: Optional[torch.Tensor] = None,
forward_batch: Optional[ForwardBatch] = None,
) -> torch.Tensor:
# DP attention only when driven from the decoder layer (forward_batch
# given); the shared-experts instance inside KimiK3MoE passes None and
# runs on the already-gathered buffer.
use_dp = (
self._dp_attention and forward_batch is not None and not self._dense_attn_tp
)
if use_dp:
local_hidden_states = hidden_states
hidden_states = get_global_dp_buffer(get_parallel().tp_group)
dp_gather_replicate(hidden_states, local_hidden_states, forward_batch)
gate_up, _ = self.gate_up_proj(hidden_states)
hidden_states = self.act_fn(gate_up)
hidden_states, _ = self.down_proj(hidden_states)
if use_dp:
global_out = hidden_states
hidden_states = get_local_dp_buffer(_dp_local_buffer_group())
dp_scatter(hidden_states, global_out, forward_batch)
# TODO(dark): maybe fuse residual with all reduce of down projection
if prefix_sum is not None:
hidden_states = hidden_states + prefix_sum
return hidden_states
def _add3(
a: torch.Tensor,
b: torch.Tensor,
c: Optional[torch.Tensor],
*,
prefetch_bc: bool = False,
) -> torch.Tensor:
"""bf16(a + b) [+ c]. A pending c (the attn-res delayed +prefix_sum)
collapses the two elementwise adds into the 3-way JIT kernel — one
launch and one memory pass; its double rounding matches the unfused
pair bit-for-bit. prefetch_bc loads b/c before the PDL wait: only pass
True when their producers are at least two kernels back."""
if c is None:
return a + b
from sglang.kernels.ops.elementwise import add3
if not add3.covered(a, b, c):
return (a + b) + c
return add3.add3(a, b, c, prefetch_bc=prefetch_bc)
# One-shot log guard: proves the merged front is live (see _ep_front).
_EP_FRONT_LOGGED = False
def _o_proj_takes_output(o_proj: RowParallelLinear) -> bool:
"""Whether o_proj can write into caller-owned storage. ``apply_into`` is an
optional quant-method capability; only the unquantized method has it."""
return getattr(o_proj.quant_method, "apply_into", None) is not None
def _k3_symm_o_proj_out(o_proj: RowParallelLinear, x: torch.Tensor) -> torch.Tensor:
"""Symmetric storage for o_proj's TP-partial output; the fused attention
all-reduce reduces it in place."""
return k3_ar_fusion.symm_buffer(
k3_ar_fusion.ATTN_O_PROJ, x.shape[0], o_proj.weight.shape[0], x.dtype
)
class KimiK3MoE(nn.Module):
"""K3 MoE with Latent MoE (experts run in moe_hidden_size space)."""
def __init__(
self,
config: KimiLinearConfig,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
layer_idx: int = 0,
alt_stream: Optional[torch.cuda.Stream] = None,
):
super().__init__()
hidden_size = config.hidden_size
moe_intermediate_size = config.moe_intermediate_size
moe_renormalize = config.moe_renormalize
self.tp_size = get_parallel().tp_size
self.routed_scaling_factor = config.routed_scaling_factor
self.num_shared_experts = config.num_shared_experts
self.layer_idx = layer_idx
self.alt_stream = alt_stream
self._dp_attention = is_dp_attention_enabled()
self.use_latent_moe = config.routed_expert_hidden_size is not None
# Merged front weight ([H, gate_up + E + latent]), built after weight
# loading by _merge_front_weights().
self._front_w: Optional[torch.Tensor] = None
self._front_sizes: Optional[List[int]] = None
# True when _front_w merges only [gate, routed_expert_down_proj] (the EP
# a2a pair) rather than the three-way fused-front weight.
self._front_is_ep_pair = False
self.moe_hidden_size = (
config.routed_expert_hidden_size if self.use_latent_moe else hidden_size
)
# Gate — fp32 output so routing (sigmoid, bias add, top-k) runs in
# full precision (matches GateLinear in mke). codespell:ignore mke
self.gate = MoEGate(config, quant_config=None, prefix=f"{prefix}.gate")
# For MXFP4 compressed-tensors on non-NPU, replace quant_config with
# Mxfp4Config so FusedMoE's weight_loader uses the MXFP4 fast path. On
# NPU the compressed-tensors config is kept so the scheme-based path
# selects NPUCompressedTensorsW4A8mxfp4MoE (see get_moe_scheme).
moe_quant_config = quant_config
if (
quant_config is not None
and getattr(quant_config, "quant_format", None)
and "mxfp4" in quant_config.quant_format
and not _is_npu
):
from sglang.srt.layers.quantization.mxfp4 import Mxfp4Config
moe_quant_config = Mxfp4Config(is_checkpoint_mxfp4_serialized=True)
# Routed experts (operate in moe_hidden_size space)
# gate_up_interleaved=False: K3 loads per-expert w1/w3 into non-interleaved layout
self.experts = get_moe_impl_class(moe_quant_config)(
num_experts=getattr(config, "n_routed_experts", config.num_experts),
top_k=config.num_experts_per_token,
hidden_size=self.moe_hidden_size,
intermediate_size=config.moe_intermediate_size,
layer_id=self.layer_idx,
quant_config=moe_quant_config,
routed_scaling_factor=self.routed_scaling_factor,
activation=config.hidden_act,
gemm1_alpha=config.activation_situ_beta,
gemm1_clamp_limit=config.activation_situ_linear_beta,
gate_up_interleaved=False,
# trtllm fused-routing MoE backends (e.g. nvfp4 w4a4) route inside
# the kernel and require the routing method; K3 uses DSv3-style
# grouped topk with e_score_correction_bias.
routing_method_type=getattr(
config, "routing_method_type", RoutingMethodType.DeepSeekV3
),
prefix=add_prefix("experts", prefix),
)
self.topk = TopK(
top_k=config.num_experts_per_token,
renormalize=moe_renormalize,
use_grouped_topk=True,
num_expert_group=config.num_expert_group,
topk_group=config.topk_group,
scoring_func=config.moe_router_activation_func,
correction_bias=self.gate.e_score_correction_bias,
quant_config=quant_config,
routed_scaling_factor=self.routed_scaling_factor,
apply_routed_scaling_factor_on_output=self.experts.should_fuse_routed_scaling_factor_in_topk,
# TRT-LLM cannot consume fused-front's row-strided router logits;
# keep K3's FP32 router and pass precomputed top-k instead.
output_format=(
TopKOutputFormat.STANDARD
if quant_config is None
or (
config.hidden_act == "situ"
and (
get_moe_runner_backend().is_flashinfer_mxfp4()
or get_moe_runner_backend().is_flashinfer_trtllm()
)
)
# mega pre-dispatch consumes raw topk_ids/topk_weights
or get_moe_a2a_backend().is_megamoe()
else None
),
)
# MegaMoE (deep_gemm fused a2a+GEMM over the EP symm buffer) replaces
# the routed experts call below. K3 routes ALL batches through it when
# enabled -- its non-mega fallback is a StandardDispatcher without a2a,
# wrong for scattered tokens -- so
# SGLANG_OPT_DEEPGEMM_MEGA_MOE_NUM_MAX_TOKENS_PER_RANK must cover the
# per-rank prefill chunk.
self._use_mega_moe = get_moe_a2a_backend().is_megamoe()
self._mega_intermediate_size = moe_intermediate_size
self._mega_top_k = config.num_experts_per_token
if self._use_mega_moe:
assert self.use_latent_moe and config.hidden_act == "situ"
assert (
config.activation_situ_beta,
config.activation_situ_linear_beta,
) == (4.0, 25.0), (
"MegaMoE SiTU kernel bakes beta=4.0/linear_beta=25.0; "
"got a checkpoint with different constants"
)
# EP a2a backends move each row to its experts directly, so the MoE
# region consumes whatever rows this rank holds (SP-MoE shard or
# DP-local batch) with every global token dispatched exactly once.
# No DP gather and no TP reduce anywhere in the region.
_a2a_backend = get_moe_a2a_backend()
self._ep_a2a = (
_a2a_backend.is_megamoe()
or _a2a_backend.is_deepep()
or _a2a_backend.is_mooncake()
or _a2a_backend.is_ascend_fuseep()
or _a2a_backend.is_mori()
)
# Defer the trtllm-gen finalize (top-k weighted unpermute) into the
# push all-reduce's staging pass (k3_ar_fusion.finalize_all_reduce_push_norm)
# so the rank-local latent never materializes. Sizes beyond the push
# window fall back to the in-op finalize at runtime.
self._defer_moe_finalize = (
get_moe_runner_backend().is_flashinfer_mxfp4()
and config.hidden_act == "situ"
)
# Shared experts operate on original hidden states. EP a2a gives each
# rank a token shard: either replicate the weights, or gather within
# the shared-expert TP subgroup and reduce-scatter back to those rows.
parallel = get_parallel()
requested_shared_tp = parallel.shared_experts_tp_size
shared_tp = requested_shared_tp
if shared_tp is None and parallel.enable_shared_experts_attn_tp:
shared_tp = parallel.attn_tp_size
if requested_shared_tp is not None and not self._ep_a2a:
raise ValueError("Independent shared-expert TP requires an EP a2a backend.")
self._shared_experts_tp1 = self._ep_a2a and shared_tp in (None, 1)
self._shared_experts_tp_comm = (
self._ep_a2a and shared_tp is not None and shared_tp > 1
)
self._shared_experts_tp_group = None
shared_experts_tp_kwargs = {}
if self._shared_experts_tp1:
shared_experts_tp_kwargs = dict(tp_rank=0, tp_size=1)
elif self._shared_experts_tp_comm:
group = (
parallel.shared_experts_tp_group
if requested_shared_tp is not None
else parallel.attn_tp_group
)
assert group.world_size == shared_tp
self._shared_experts_tp_group = group
shared_experts_tp_kwargs = dict(
tp_rank=group.rank_in_group, tp_size=group.world_size
)
if self.num_shared_experts is not None and self.num_shared_experts > 0:
shared_intermediate_size = moe_intermediate_size * self.num_shared_experts
if shared_tp is not None and shared_intermediate_size % shared_tp != 0:
raise ValueError(
f"Shared-expert intermediate size ({shared_intermediate_size}) "
f"must be divisible by shared-expert TP size ({shared_tp})."
)
self.shared_experts = KimiK3MLP(
hidden_size=config.hidden_size,
intermediate_size=shared_intermediate_size,
hidden_act=config.hidden_act,
quant_config=quant_config,
reduce_results=False,
prefix=f"{prefix}.shared_experts",
activation_situ_beta=config.activation_situ_beta,
activation_situ_linear_beta=config.activation_situ_linear_beta,
**shared_experts_tp_kwargs,
)
else:
self.shared_experts = None
# SBO (single batch overlap): shared experts are bf16 + tp1-replicated
# (~264 MB/layer/rank), the routed path is a2a-latency bound in decode
# with HBM idle; issue shared on the side stream, join before the tail
# add. NPU shared-expert TP can instead overlap the shared collectives
# with SGLANG_NPU_FINE_GRAINED_MOE_DUAL_STREAM.
self._sbo_shared_overlap = (
self._ep_a2a
and self.shared_experts is not None
and self.alt_stream is not None
)
if self.use_latent_moe:
latent_quant_config = (
quant_config
if getattr(
quant_config,
"supports_kimi_k3_quantized_latent_projections",
False,
)
else None
)
self.routed_expert_down_proj = ReplicatedLinear(
hidden_size,
self.moe_hidden_size,
bias=False,
quant_config=latent_quant_config,
prefix=f"{prefix}.routed_expert_down_proj",
)
self.routed_expert_norm = (
RMSNorm(self.moe_hidden_size, eps=config.rms_norm_eps)
if config.latent_moe_use_norm
else None
)
self.routed_expert_up_proj = ReplicatedLinear(
self.moe_hidden_size,
hidden_size,
bias=False,
quant_config=latent_quant_config,
prefix=f"{prefix}.routed_expert_up_proj",
)
else:
self.routed_expert_down_proj = None
self.routed_expert_norm = None
self.routed_expert_up_proj = None
# Static eligibility for fusing the latent all-reduce with the RMSNorm
# epilogue (SGLANG_K3_AR_FUSION): kernel needs latent == NORM_DIM and
# shared == 2*NORM_DIM (3584 / 7168). Decided once so the hot path
# reads a bool.
self.fuse_ar_norm = (
self.routed_expert_norm is not None
and self.moe_hidden_size == k3_ar_fusion.NORM_DIM
and hidden_size == 2 * k3_ar_fusion.NORM_DIM
)
# Static eligibility for the column-parallel up_proj tail (gemm_ag):
# 1/8-column GEMV + multicast all-gather + spin-add3 replaces the
# replicated [3584, 7168] GEMM (~1.5-2x at decode). Dims fixed to
# fuse_ar_norm's over TP8; per-batch checks in k3_ar_fusion.
self._gemm_ag_up_eligible = (
self.fuse_ar_norm
and self.tp_size == 8
and self.routed_expert_up_proj is not None
and isinstance(
getattr(self.routed_expert_up_proj, "weight", None), torch.Tensor
)
and self.routed_expert_up_proj.weight.dtype == torch.bfloat16
and self.routed_expert_up_proj.weight.is_contiguous()
)
def _merge_front_weights(self) -> None:
"""Merge shared gate_up + router gate + latent down_proj weights.
All three GEMMs consume the same hidden_states; at decode each one is a
skinny memory-bound GEMV with its own splitK epilogue. One merged
[H, gu+E+latent] GEMM reads the input once and drops 2 GEMM launches
plus their splitK-reduce tails per MoE layer.
Called once from load_weights (after all weights are loaded, before
cuda graph capture); only plain bf16/fp16 dense weights are merged —
quantized or mixed-dtype checkpoints keep the unfused path.
"""
if not self.use_latent_moe:
return
# These merged layouts feed CUDA-only fused front kernels. Keeping the
# regular parameters on other devices avoids a large transient copy
# during post-load processing and leaves their native kernels in
# control of weight layout.
if _is_npu:
return
if self.shared_experts is not None and get_moe_a2a_backend().is_none():
mods = [
self.shared_experts.gate_up_proj,
self.gate,
self.routed_expert_down_proj,
]
elif envs.SGLANG_K3_FUSED_FRONT.get():
# Merge the router gate into the latent down-proj so one GEMM reads
# hidden_states once: the 896-row gate alone is too few to use the
# machine well; folded into the 3584-row down-proj it is near-free.
mods = [self.gate, self.routed_expert_down_proj]
else:
return
if any(getattr(module, "weight", None) is None for module in mods):
return
dtypes = {m.weight.dtype for m in mods}
if len(dtypes) != 1 or dtypes.pop() not in (torch.bfloat16, torch.float16):
return
self._front_w, self._front_sizes = _merge_weights_as_views(mods)
self._front_is_ep_pair = len(mods) == 2
# Invalidate the cached properties.
for prop in (
"_eligible_for_fused_front",
"_front_fp32",
"_routing_contract_ok",
"_ep_front_eligible",
):
self.__dict__.pop(prop, None)
@cached_property
def _routed_needs_reduce(self):
return self.tp_size > 1 and get_moe_a2a_backend().is_none()
@cached_property
def _eligible_for_fused_front(self) -> bool:
"""The fused front commits to the single-collective tail (both
partial sums in one symmetric buffer), so beyond the merged front
weight it requires plain-TP routed sums (an a2a combine already
returns the complete sum — all-reducing it again would multiply by
tp_size) and a dense shared down weight for the direct out= GEMM."""
return (
self.use_latent_moe
and self.shared_experts is not None
and self._front_w is not None
and not self._front_is_ep_pair
and get_moe_a2a_backend().is_none()
and self.shared_experts.down_proj.weight.dtype
in (torch.bfloat16, torch.float16)
)
@cached_property
def _front_fp32(self) -> bool:
"""Emit the merged front in fp32 so the router reads exact logits.
The situ activation and the flashinfer_mxfp4 quantizer read the fp32
slices directly. Every other runner takes routed_input rounded back to
bf16 in _forward_fused, which is bit-identical to the bf16 front."""
return (
not _is_hip
and self._eligible_for_fused_front
and self._front_w.dtype == torch.bfloat16
)
def _forward_mega_experts(
self, routed_input: torch.Tensor, topk_output
) -> torch.Tensor:
"""Routed experts via deep_gemm MegaMoE: fused a2a dispatch + grouped
GEMMs + SiTU + combine over the EP-group symmetric buffer. Semantically
equivalent to `self.experts(routed_input, topk_output)` on an a2a
backend (combine returns fully-summed rows; `_reduce_latent` then only
applies the norm)."""
import deep_gemm
from sglang.kernels.ops.attention.dsv4 import mega_moe_pre_dispatch
from sglang.srt.environ import envs
from sglang.srt.layers.moe.mega_moe import (
_configure_mega_moe_deep_gemm_num_sms,
_get_mega_moe_symm_buffer,
)
from sglang.srt.runtime_context import get_parallel
# In SP-MoE mode (KimiK3DecoderLayer reduce-scatters the o_proj
# output) the incoming rows are already this rank's token shard, so
# the fused a2a below dispatches each token exactly once. On the
# non-scattered fallback path the rows are the full batch (redundant
# across ranks but correct).
num_tokens = routed_input.shape[0]
num_max_tokens_per_rank = (
envs.SGLANG_OPT_DEEPGEMM_MEGA_MOE_NUM_MAX_TOKENS_PER_RANK.get()
)
assert num_tokens <= num_max_tokens_per_rank, (
f"mega MoE: num_tokens={num_tokens} exceeds "
f"SGLANG_OPT_DEEPGEMM_MEGA_MOE_NUM_MAX_TOKENS_PER_RANK="
f"{num_max_tokens_per_rank}; K3 has no non-mega fallback — raise "
f"the env var to cover the per-rank rows"
)
buf = _get_mega_moe_symm_buffer(
get_parallel().moe_ep_group.device_group,
num_experts=self.experts.num_experts,
num_max_tokens_per_rank=num_max_tokens_per_rank,
num_topk=self._mega_top_k,
hidden=self.moe_hidden_size,
intermediate_hidden=self._mega_intermediate_size,
)
if num_tokens > 0:
topk_ids_in = topk_output.topk_ids.to(torch.int32)
topk_weights_in = topk_output.topk_weights.to(torch.float32)
else:
topk_ids_in = routed_input.new_empty(
(0, self._mega_top_k), dtype=torch.int32
)
topk_weights_in = routed_input.new_empty(
(0, self._mega_top_k), dtype=torch.float32
)
mega_moe_pre_dispatch(
routed_input,
topk_ids_in,
topk_weights_in,
buf.x,
buf.x_sf,
buf.topk_idx,
buf.topk_weights,
quant_group_size=32,
)
# At least one row so the tvm-ffi binding sees a non-null data_ptr.
y = torch.empty(
(max(num_tokens, 1), self.moe_hidden_size),
dtype=torch.bfloat16,
device=routed_input.device,
)
with _configure_mega_moe_deep_gemm_num_sms(deep_gemm):
deep_gemm.fp8_fp4_mega_moe(
y,
self.experts.mega_l1_weights,
self.experts.mega_l2_weights,
buf,
recipe=(1, 1, 32),
activation="situ",
fast_math=True,
)
y = y[:num_tokens]
if not self.experts.should_fuse_routed_scaling_factor_in_topk:
if (
self.routed_scaling_factor is not None
and self.routed_scaling_factor != 1.0
):
y.mul_(self.routed_scaling_factor)
return y
def _latent_norm(self, latent: torch.Tensor) -> torch.Tensor:
if self.routed_expert_norm is None:
return latent
return self.routed_expert_norm(latent)
@cached_property
def _routing_contract_ok(self) -> bool:
"""Whether a kernel may emit (weights, ids) itself and bypass
select_experts. Shared by the fused router and the merged front."""
if self._eligible_for_fused_front:
return False
cfg = self.topk.topk_config
if cfg.output_format is not TopKOutputFormat.STANDARD:
return False
# The kernel implements sigmoid scoring with bias-ranked ungrouped top-k.
# K3 passes moe_router_activation_func explicitly to TopK. The legacy
# GPU biased_grouped_topk path also hardwires sigmoid, but other platform
# implementations consume cfg.scoring_func directly.
if cfg.scoring_func != "sigmoid":
return False
if not (cfg.use_grouped_topk and cfg.correction_bias is not None):
return False
if (cfg.num_expert_group or 1) > 1 or (cfg.topk_group or 1) > 1:
return False
# A waterfill balancer rewrites the routing after the top-k; leave it on
# the layer path that supports it.
if self.topk.waterfill_balancer is not None or self.topk.enable_waterfill:
return False
if self.gate.e_score_correction_bias is None:
return False
# K3 calls self.topk() without a padding mask or EPLB dispatch info, so
# select_experts' post-processing collapses to the capture hook and the
# recorder -- both of which build_precomputed_topk_output runs. Bail out
# if that ever stops holding rather than silently dropping the remap.
if not precomputed_topk_postprocess_is_noop(cfg):
return False
if get_exec().deterministic.enable_deterministic_inference:
return False
try:
from sglang.kernels.ops.moe import moe_front
except Exception:
return False
return moe_front.available()
@cached_property
def _ep_front_eligible(self) -> bool:
"""Static eligibility for the merged EP front (gate + latent down-proj in
one GEMM). Requires the two-module merge from _merge_front_weights and the
same routing contract the single-kernel router needs."""
return (
envs.SGLANG_K3_FUSED_FRONT.get()
and self._front_w is not None
and self._front_is_ep_pair
and self.use_latent_moe
and self.routed_expert_down_proj is not None
and self._routing_contract_ok
)
def _ep_front(self, hidden_states: torch.Tensor):
"""Merged front: returns ``(topk_output, routed_input)``, or None when the
shape is not covered and the caller should run the unmerged path."""
if not self._ep_front_eligible:
return None
from sglang.kernels.ops.moe import moe_front
cfg = self.topk.topk_config
bias = self.gate.e_score_correction_bias
if (
moe_front.get_front_strategy(hidden_states.shape[0], hidden_states.device)
!= "merged_fp32"
):
return None
if not moe_front.fused_front_covered(
hidden_states, self._front_w, bias, cfg.top_k, self.moe_hidden_size
):
return None
w, i, routed = moe_front.fused_front(
hidden_states,
self._front_w,
bias,
latent=self.moe_hidden_size,
topk=cfg.top_k,
renormalize=cfg.renormalize,
routed_scaling_factor=cfg.routed_scaling_factor,
apply_routed_scaling_factor_on_output=cfg.apply_routed_scaling_factor_on_output,
)
global _EP_FRONT_LOGGED
if not _EP_FRONT_LOGGED:
# An absence of fallback warnings does not prove a fast path ran.
_EP_FRONT_LOGGED = True
logger.info(
"K3 merged MoE front active (layer %d, %d tokens)",
self.layer_idx,
hidden_states.shape[0],
)
return build_precomputed_topk_output(w, i, cfg, self.layer_idx), routed
def _ep_front_overlap(self, hidden_states: torch.Tensor):
"""Overlap the exact fp32 gate+top-k with the latent down projection.
The side stream is joined before returning. It is then free for the
existing shared-expert overlap, which is deliberately issued later.
"""
if (
not self._ep_front_eligible
or self.alt_stream is None
or hidden_states.shape[0] == 0
):
return None
from sglang.kernels.ops.moe import moe_front
if (
moe_front.get_front_strategy(hidden_states.shape[0], hidden_states.device)
!= "overlap"
):
return None
current_stream = torch.cuda.current_stream()
self.alt_stream.wait_stream(current_stream)
with torch.cuda.stream(self.alt_stream):
router_logits = self.gate(hidden_states)
topk_output = self.topk(hidden_states, router_logits)
routed_input, _ = self.routed_expert_down_proj(hidden_states)
current_stream.wait_stream(self.alt_stream)
# Top-k tensors were allocated on alt_stream but are consumed by the
# routed experts on current_stream. Tell the caching allocator about
# that lifetime before alt_stream is reused for the shared experts.
for value in topk_output:
if isinstance(value, torch.Tensor):
value.record_stream(current_stream)
return topk_output, routed_input
def _reduce_latent(self, latent: torch.Tensor) -> torch.Tensor:
"""Unfused-front latent tail: TP-partial routed sums must be reduced
in latent space BEFORE the RMSNorm (sum(norm(x_i)) != norm(sum(x_i)))."""
if not self._routed_needs_reduce:
return self._latent_norm(latent)
return self._latent_norm(tensor_model_parallel_all_reduce(latent))
def _gather_shared_expert_inputs(self, hidden_states: torch.Tensor) -> torch.Tensor:
group = self._shared_experts_tp_group
# The attention DP buffer spans the entire attention-TP replica.
# Size this buffer from the subgroup's actual token shards instead.
with use_symmetric_memory(group, disabled=not is_allocation_symmetric()):
gathered = hidden_states.new_empty(
(hidden_states.shape[0] * group.world_size, *hidden_states.shape[1:])
)
group.all_gather_into_tensor(gathered, hidden_states)
return gathered
def _reduce_scatter_shared_experts(
self, shared_output: torch.Tensor, hidden_states: torch.Tensor
) -> torch.Tensor:
output = torch.empty_like(hidden_states)
self._shared_experts_tp_group.reduce_scatter_tensor(output, shared_output)
return output
def _forward_shared_experts(self, hidden_states: torch.Tensor) -> torch.Tensor:
"""Run TP-sharded shared experts while DeepEP tokens stay scattered."""
if not self._shared_experts_tp_comm:
return self.shared_experts(hidden_states)
gathered_hidden_states = self._gather_shared_expert_inputs(hidden_states)
gathered_shared_output = self.shared_experts(gathered_hidden_states)
return self._reduce_scatter_shared_experts(
gathered_shared_output, hidden_states
)
def _can_overlap_shared_experts_npu(self, hidden_states: torch.Tensor) -> bool:
if not (
_is_npu
and envs.SGLANG_NPU_FINE_GRAINED_MOE_DUAL_STREAM.get()
and self._sbo_shared_overlap
and self._shared_experts_tp_comm
and self.use_latent_moe
and hidden_states.shape[0] > 0
and get_moe_a2a_backend().is_deepep()
):
return False
from sglang.srt.batch_overlap.two_batch_overlap import (
MaybeTboDeepEPDispatcher,
)
from sglang.srt.model_executor.runner_backend_utils.tc_piecewise_cuda_graph import (
is_in_tc_piecewise_cuda_graph,
)
# The hooks must surround the complete dispatch, including its receive
# wait. Fused EP bypasses these hooks. An eager/piecewise graph break
# must not split the side-stream event record from its wait.
return (
isinstance(self.experts.dispatcher, MaybeTboDeepEPDispatcher)
and not is_in_breakable_cuda_graph()
and not is_in_tc_piecewise_cuda_graph()
)
def _forward_unfused(
self,
hidden_states: torch.Tensor,
*,
prefix_sum: Optional[torch.Tensor],
) -> torch.Tensor:
"""Front section with three separate GEMMs, each reading
hidden_states: shared-expert MLP, router gate, latent down-proj."""
# Shared experts on original hidden_states; under SBO they go to the
# side stream, joined at the tail. CUDA issues this after the front so
# they overlap the routed a2a rather than the front GEMMs; NPU starts
# before the front. Fine-grained NPU overlap splits at the dispatch
# boundaries:
# current: front ---------- dispatch ---------- routed GEMMs -- tail
# alt: all-gather ----- shared MLP -------- reduce-scatter
fine_grained_overlap = self._can_overlap_shared_experts_npu(hidden_states)
shared_input = None
shared_output = None
shared_event = None
shared_compute_event = None
def issue_shared():
nonlocal shared_input, shared_output, shared_event
if self.shared_experts is None or hidden_states.shape[0] == 0:
return
if fine_grained_overlap:
# Fork before the routed front so HCCL's completion wait is
# queued on the side stream, leaving the front free to run.
self.alt_stream.wait_stream(torch.cuda.current_stream())
hidden_states.record_stream(self.alt_stream)
with torch.cuda.stream(self.alt_stream):
shared_input = self._gather_shared_expert_inputs(hidden_states)
shared_input.record_stream(self.alt_stream)
return
if self._sbo_shared_overlap:
current_stream = torch.cuda.current_stream()
# Keep HCCL collectives on the current stream. The alternate
# stream only executes the shared-expert MLP.
shared_input = hidden_states
if self._shared_experts_tp_comm:
shared_input = self._gather_shared_expert_inputs(hidden_states)
shared_input.record_stream(self.alt_stream)
self.alt_stream.wait_stream(current_stream)
with torch.cuda.stream(self.alt_stream):
shared_output = self.shared_experts(shared_input)
shared_event = self.alt_stream.record_event()
else:
shared_output = self._forward_shared_experts(hidden_states)
def run_experts(expert_input, topk_output):
if not fine_grained_overlap:
return (
self._forward_mega_experts(expert_input, topk_output)
if self._use_mega_moe
else self.experts(expert_input, topk_output)
)
def pre_dispatch(dispatcher, dispatch_input, dispatch_topk):
nonlocal shared_output, shared_compute_event
# AllGather is already queued. Delay shared GEMMs until the
# gate, TopK and latent down projection finish on current.
self.alt_stream.wait_stream(torch.cuda.current_stream())
with torch.cuda.stream(self.alt_stream):
shared_output = self.shared_experts(shared_input)
shared_compute_event = self.alt_stream.record_event()
def post_dispatch(dispatcher, dispatch_output):
nonlocal shared_output, shared_event
current_stream = torch.cuda.current_stream()
# Dispatch has queued its receive wait. RS waits for that
# communication and the shared MLP, while routed GEMMs wait
# only for the MLP (not for RS).
self.alt_stream.wait_stream(current_stream)
with torch.cuda.stream(self.alt_stream):
shared_output = self._reduce_scatter_shared_experts(
shared_output, hidden_states
)
shared_event = self.alt_stream.record_event()
current_stream.wait_event(shared_compute_event)
dispatcher = self.experts.dispatcher
pre_handle = dispatcher.register_pre_dispatch_hook(pre_dispatch)
try:
post_handle = dispatcher.register_post_dispatch_hook(post_dispatch)
try:
return self.experts(expert_input, topk_output)
finally:
post_handle.remove()
finally:
# Remove outside hook iteration, including on dispatch/GEMM
# failures, so closures cannot leak into the next forward.
pre_handle.remove()
def wait_and_finalize_shared_experts():
nonlocal shared_output
if shared_event is None:
return
# Join just before consuming the shared result. The legacy path
# still needs to reduce-scatter its TP-partial MLP output here.
current_stream = torch.cuda.current_stream()
current_stream.wait_event(shared_event)
shared_output.record_stream(current_stream)
if self._shared_experts_tp_comm and not fine_grained_overlap:
shared_output = self._reduce_scatter_shared_experts(
shared_output, hidden_states
)
# Give the NPU shared-expert branch a head start. At this point
# hidden_states is the decoder layer's post-attention RMSNorm output.
if _is_npu and self._sbo_shared_overlap:
issue_shared()
# Front: gate + TopK (+ latent down-proj when merged). Strategy table:
# kernels/ops/moe/moe_front.py.
routed_input = self._ep_front(hidden_states)
if routed_input is None and not fine_grained_overlap:
routed_input = self._ep_front_overlap(hidden_states)
topk_output = None
if routed_input is not None:
topk_output, routed_input = routed_input
else:
# MoEGate produces fp32 router logits on CUDA (via linear_bf16_fp32
# or tiny_gemm_bf16); non-CUDA falls back to F.linear (bf16). The
# fp32 logits reach the radix router from moe_fused_gate.
router_logits = self.gate(hidden_states)
topk_output = self.topk(hidden_states, router_logits)
if not (_is_npu and self._sbo_shared_overlap):
issue_shared()
if not self.use_latent_moe:
expert_output = self.experts(hidden_states, topk_output)
wait_and_finalize_shared_experts()
if shared_output is not None:
expert_output = expert_output + shared_output
# EP combine and the shared-expert subgroup have already completed
# each source token. A global TP reduction would mix token shards.
if self.tp_size > 1 and not self._ep_a2a:
expert_output = tensor_model_parallel_all_reduce(expert_output)
if prefix_sum is not None:
expert_output = expert_output + prefix_sum
return expert_output
# Latent MoE: compress after routing, before experts
if TYPE_CHECKING:
assert (
self.routed_expert_down_proj is not None
and self.routed_expert_up_proj is not None
)
if routed_input is None:
if hidden_states.shape[0] == 0:
# Idle DP ranks must still enter the EP dispatch below so the
# active replicas can exchange routed tokens. Ascend's
# quantized matmul does not accept an empty activation, so
# materialize its shape-only result without launching GEMM.
routed_input = hidden_states.new_empty((0, self.moe_hidden_size))
else:
routed_input, _ = self.routed_expert_down_proj(hidden_states)
expert_output = run_experts(routed_input, topk_output)
if expert_output.shape[0] == 0:
# The EP combine returns one row per source token. Keep the
# source-side empty result while avoiding empty RMSNorm/up-proj
# launches; the collective itself has already completed above.
out = hidden_states.new_empty((0, hidden_states.shape[1]))
else:
latent = self._reduce_latent(expert_output)
# up_proj is replicated, so the routed output is now fully reduced.
out, _ = self.routed_expert_up_proj(latent)
wait_and_finalize_shared_experts()
if shared_output is not None:
# tp1 shared experts (SP-MoE) are complete per-rank; TP-sharded
# ones need the partial-sum reduction.
if (
self.tp_size > 1
and not self._shared_experts_tp1
and not self._shared_experts_tp_comm
):
shared_output = tensor_model_parallel_all_reduce(shared_output)
out = _add3(out, shared_output, prefix_sum)
return out
out = out if prefix_sum is None else out + prefix_sum
return out
@cached_property
def _moe_front_needs_dense_bf16(self) -> bool:
"""Whether routed_input must be repaired into a dense bf16 buffer.
Only the SM100 trtllm-gen mxfp4 runner reads the front slice as it
comes: its group quant (route_quant_fused / per_token_group_quant)
takes both a strided row and an fp32 row. The SM90/SM120 cutlass mxfp4
kernels return from apply() before that quant, and precision="bf16"
skips it as well, so those keep the bf16 contract even though the
runner backend is the same."""
from sglang.srt.layers.quantization.mxfp4 import Mxfp4MoEMethod
method = self.experts.quant_method
return not (
isinstance(method, Mxfp4MoEMethod)
and method.use_flashinfer
and not method.use_marlin
and method._fi_kernel == "trtllm_sm100"
and method.flashinfer_mxfp4_moe_precision == "default"
and method.hidden_size == self.moe_hidden_size
)
@cached_property
def _route_quant_fuse_eligible(self) -> bool:
"""Whether to stage routed_input for the fused route+pack+quant launch
(route_quant_handoff). Only the trtllm-gen SiTU runner with mxfp8
activations consumes the staged quant, so only that runner stages."""
from sglang.srt.layers.quantization.mxfp4 import Mxfp4MoEMethod
method = self.experts.quant_method
return (
isinstance(method, Mxfp4MoEMethod)
and method.use_flashinfer
and not method.use_marlin
and method.flashinfer_mxfp4_moe_precision == "default"
and self.experts.moe_runner_config.activation == "situ"
)
def _forward_routed(self, hidden_states, router_logits, routed_input, latent):
if self._route_quant_fuse_eligible:
route_quant_handoff.stage(routed_input)
try:
topk_output = self.topk(hidden_states, router_logits)
with zero_copy_context.set_moe_output(latent):
expert_output = self.experts(routed_input, topk_output)
finally:
route_quant_handoff.clear()
if expert_output.data_ptr() != latent.data_ptr():
latent.copy_(expert_output)
def _forward_routed_deferred(self, hidden_states, router_logits, routed_input):
"""Routed experts with the in-op finalize skipped: returns the
FlashInferTrtllmDeferredFinalizeOutput triple (permuted gemm2 output,
expanded_idx_to_permuted_idx, expert_weights) for the finalize-fused
all-reduce."""
if self._route_quant_fuse_eligible:
route_quant_handoff.stage(routed_input)
try:
topk_output = self.topk(hidden_states, router_logits)
return self.experts.forward_deferred_finalize(routed_input, topk_output)
finally:
route_quant_handoff.clear()
def _forward_shared(self, gate_up, shared_output):
shared = self.shared_experts
if TYPE_CHECKING:
assert shared is not None and isinstance(
shared.down_proj.weight, torch.Tensor
)
assert shared is not None
_k3_bf16_gemm(
shared.act_fn(gate_up),
shared.down_proj.weight,
out=shared_output,
)
def _get_fused_norm_params(self) -> tuple[torch.Tensor, float]:
norm = self.routed_expert_norm
assert self.fuse_ar_norm and norm is not None
return norm.weight, norm.variance_epsilon
def _forward_fused(
self, hidden_states: torch.Tensor, *, prefix_sum: Optional[torch.Tensor]
) -> torch.Tensor:
"""Fused-front pipeline: read hidden_states once through the merged
[H, gate_up + E + latent] weight, then land both TP-partial sums in
one flat symmetric [latent | shared] buffer with zero copies — the
shared down GEMM writes its slice via out=, the MoE runner writes
its top-k sum via the zero-copy context — and all-reduce the pair
in a single collective (the symmetric mempool keeps the one-shot
allreduce path; same trick as RowParallelLinear)."""
if TYPE_CHECKING: # NOTE: precondition for this case
assert (
self._front_w is not None
and self._front_sizes is not None
and self.moe_hidden_size is not None
and self.shared_experts is not None
and isinstance(self.shared_experts.down_proj.weight, torch.Tensor)
and self.routed_expert_up_proj is not None
)
num_tokens, hidden_size = hidden_states.shape
fused = _k3_bf16_gemm(
hidden_states,
self._front_w,
out_dtype=torch.float32 if self._front_fp32 else None,
)
gate_up, router_logits, routed_input = torch.split(
fused, self._front_sizes, dim=-1
)
if num_tokens > 1 and _is_hip and not _aiter_k3_opt:
router_logits = router_logits.contiguous()
if self._moe_front_needs_dense_bf16:
# off an fp32 front the cast allocates the dense buffer, so the
# contiguous() behind it is free; off a bf16 front it is the copy
routed_input = routed_input.to(hidden_states.dtype).contiguous()
latent_numel = num_tokens * self.moe_hidden_size
if k3_ar_fusion.enabled():
# the shared-expert AR is pull-only, so its input must be a
# symm_buffer slice for every rank to resolve the same offset
buf = k3_ar_fusion.symm_buffer(
k3_ar_fusion.MOE_LATENT_SHARED,
num_tokens,
self.moe_hidden_size + hidden_size,
hidden_states.dtype,
).view(-1)
else:
with use_symmetric_memory(
get_parallel().tp_group, disabled=not is_allocation_symmetric()
):
buf = hidden_states.new_empty(latent_numel + num_tokens * hidden_size)
latent = buf[:latent_numel].view(num_tokens, self.moe_hidden_size)
shared_output = buf[latent_numel:].view(num_tokens, hidden_size)
fused_norm = False
if self.alt_stream is not None and k3_ar_fusion.enabled():
defer_finalize = (
self._defer_moe_finalize
and self.fuse_ar_norm
and k3_ar_fusion.finalize_push_fits(num_tokens)
)
current_stream = torch.cuda.current_stream()
self.alt_stream.wait_stream(current_stream)
if defer_finalize:
deferred = self._forward_routed_deferred(
hidden_states, router_logits, routed_input
)
else:
self._forward_routed(hidden_states, router_logits, routed_input, latent)
with torch.cuda.stream(self.alt_stream):
self._forward_shared(gate_up, shared_output)
# low-SM pull so the side-stream AR leaves the SMs to the
# routed GEMMs it overlaps (K3 dims are fixed; tuned here)
k3_ar_fusion.all_reduce_low_sm(shared_output, num_blocks=4, unroll=8)
current_stream.wait_stream(self.alt_stream)
# The latent AR must stay serialized after the shared AR (both
# reuse the v2 pull semaphores); the join above does it.
if defer_finalize:
# finalize folded into the push AR's staging pass; the norm
# covers every latent row
fused_norm = True
k3_ar_fusion.finalize_all_reduce_push_norm(
latent,
deferred.gemm2_out,
deferred.expanded_idx_to_permuted_idx,
deferred.expert_weights,
*self._get_fused_norm_params(),
)
elif self.fuse_ar_norm:
fused_norm = True
k3_ar_fusion.all_reduce_norm(
latent.view(-1, self.moe_hidden_size),
*self._get_fused_norm_params(),
num_tokens=num_tokens,
)
else:
k3_ar_fusion.all_reduce(latent)
# the gemm_ag tail wants the normed latent straight out of the
# fused-norm AR (its GEMV chains on it via PDL)
if (
fused_norm
and self._gemm_ag_up_eligible
and k3_ar_fusion.gemm_ag_up_fits(num_tokens)
):
return k3_ar_fusion.gemm_ag_up_proj(
latent,
self.routed_expert_up_proj.weight, # type: ignore
shared_output,
prefix_sum,
)
else: # single collective over the flat [latent | shared] pair
self._forward_shared(gate_up, shared_output)
self._forward_routed(hidden_states, router_logits, routed_input, latent)
if self.fuse_ar_norm and k3_ar_fusion.enabled():
fused_norm = True
k3_ar_fusion.all_reduce_norm(
buf.view(-1, k3_ar_fusion.NORM_DIM),
*self._get_fused_norm_params(),
num_tokens=num_tokens,
)
elif k3_ar_fusion.enabled():
k3_ar_fusion.all_reduce(buf)
else:
buf = tensor_model_parallel_all_reduce(buf)
latent = buf[:latent_numel].view(num_tokens, self.moe_hidden_size)
shared_output = buf[latent_numel:].view(num_tokens, hidden_size)
if not fused_norm:
latent = self._latent_norm(latent)
out, _ = self.routed_expert_up_proj(latent)
# prefetch_bc: b and c complete before the norm / up_proj chain
# starts; only `a`'s producer can still be in flight at PDL entry.
return _add3(out, shared_output, prefix_sum, prefetch_bc=True)
def forward(
self,
hidden_states: torch.Tensor,
*,
prefix_sum: Optional[torch.Tensor] = None,
forward_batch: Optional[ForwardBatch] = None,
) -> torch.Tensor:
"""A pending prefix_sum is always consumed here: folded into the
3-way JIT tail add when covered, plain adds otherwise (bit-identical
either way).
Under DP attention with TP-sharded experts (a2a none, forward_batch
given) the experts run on the DP-gathered global batch — the internal
reduces stay over the full TP group, which is exactly right in
gathered space — while prefix_sum stays in local token space, added
after the scatter back. EP a2a backends skip the gather entirely:
dispatching the DP-local rows (or the SP-MoE shard of them the
decoder layer already produced) covers every global token exactly
once, and prefix_sum is consumed in the tail add like the non-DP
path — gathering first would just replicate the whole batch onto
every rank (tp-fold redundant compute + a2a traffic)."""
num_tokens, hidden_size = hidden_states.shape
hidden_states = hidden_states.view(-1, hidden_size)
use_dp = self._dp_attention and forward_batch is not None and not self._ep_a2a
if use_dp:
local_hidden_states = hidden_states
hidden_states = get_global_dp_buffer(get_parallel().tp_group)
dp_gather_replicate(hidden_states, local_hidden_states, forward_batch)
dp_prefix_sum, prefix_sum = prefix_sum, None
if hidden_states.shape[0] > 0 and self._eligible_for_fused_front:
out = self._forward_fused(hidden_states, prefix_sum=prefix_sum)
else:
out = self._forward_unfused(hidden_states, prefix_sum=prefix_sum)
if use_dp:
global_out = out
out = get_local_dp_buffer(_dp_local_buffer_group())
dp_scatter(out, global_out, forward_batch)
if dp_prefix_sum is not None:
out = out + dp_prefix_sum
return out.view(num_tokens, hidden_size)
class KimiK3DeltaAttention(nn.Module):
"""KDA attention; optional full-rank gate."""
def __init__(
self,
layer_idx: int,
hidden_size: int,
config: KimiLinearConfig,
quant_config: Optional[QuantizationConfig] = None,
rms_norm_eps: float = 1e-5,
prefix: str = "",
all_reduce_fusion: bool = False,
bfa_alt_stream: Optional[torch.cuda.Stream] = None,
**kwargs,
) -> None:
super().__init__()
self.all_reduce_fusion = all_reduce_fusion
# Side stream for the [f_a|b] + f_b tiny GEMVs: they read only
# hidden_states, so they overlap the wide [q,k,v,g] GEMM on the main
# stream (graphed decode/verify only; SM bound as the MLA gate stream).
self._bfa_alt_stream = bfa_alt_stream
self._bfa_bs_limit = (
(128 if get_platform().is_blackwell else 64)
if bfa_alt_stream is not None
else 0
)
self.tp_size = get_parallel().tp_size
# KDA is an attention layer: head-sharded params follow the
# attention-TP group, matching the mamba state cache sizing.
self.attn_tp_size = get_parallel().attn_tp_size
self.attn_tp_rank = get_parallel().attn_tp_rank
self.hidden_size = hidden_size
self.config = config
self.head_dim = config.linear_attn_config["head_dim"]
self.num_heads = config.linear_attn_config["num_heads"]
self.num_k_heads = config.linear_attn_config["num_heads"]
self.num_v_heads = config.linear_attn_config["num_heads"]
self.head_k_dim = config.linear_attn_config["head_dim"]
self.head_v_dim = config.v_head_dim
self.layer_idx = layer_idx
self.prefix = prefix
assert self.num_heads % self.attn_tp_size == 0
self.local_num_heads = divide(self.num_heads, self.attn_tp_size)
projection_size = self.head_dim * self.num_heads
self.conv_size = config.linear_attn_config["short_conv_kernel_size"]
self.use_full_rank_gate = config.linear_attn_config.get(
"use_full_rank_gate", False
)
self._bfa_uses_block_fp8 = self.use_full_rank_gate and _uses_modelopt_fp8_pb_wo(
quant_config, f"{prefix}.b_proj"
)
# The full-rank [q, k, v, g] merged projection is explicitly sharded
# with attn_tp_rank/attn_tp_size, so it also supports DP attention.
# The low-rank fused path still uses full-TP-only projection helpers.
# For the full-rank gate (K3) the checkpoint quantizes only the MoE
# experts; attention linears resolve to UnquantizedLinearMethod, so a
# non-None quant_config is fine for the merged projection.
self.do_fuse_qkvbfg = quant_config is None and self.attn_tp_size == self.tp_size
if self.use_full_rank_gate:
# Fuse only the wide projections [q, k, v, g]: folding b (12/rank)
# and f_a (128, replicated) in skews the output dim and degrades
# GEMM kernel selection; they stay as separate tiny GEMVs. ROCm
# reverses this below the token threshold
# (_merge_kda_inproj_weights_hip).
self.fused_qkvg_proj = MergedColumnParallelLinear(
self.hidden_size,
[
projection_size,
projection_size,
projection_size,
projection_size,
],
bias=False,
quant_config=quant_config,
tp_rank=self.attn_tp_rank,
tp_size=self.attn_tp_size,
prefix=f"{prefix}.fused_qkvg_proj",
)
self.split_sizes = [
3 * projection_size // self.attn_tp_size,
projection_size // self.attn_tp_size,
]
self.b_proj = ColumnParallelLinear(
self.hidden_size,
self.num_heads,
bias=False,
quant_config=quant_config,
tp_rank=self.attn_tp_rank,
tp_size=self.attn_tp_size,
prefix=f"{prefix}.b_proj",
# TP8 shards K3's 96 beta rows below the 128-row FP8 block.
skip_block_quant_check=self._bfa_uses_block_fp8,
)
self.f_a_proj = ReplicatedLinear(
self.hidden_size,
self.head_dim,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.f_a_proj",
)
self.f_b_proj = ColumnParallelLinear(
self.head_dim,
projection_size,
bias=False,
quant_config=quant_config,
tp_rank=self.attn_tp_rank,
tp_size=self.attn_tp_size,
prefix=f"{prefix}.f_b_proj",
)
# Merged [f_a | b] weight, built after weight loading by
# _merge_bfa_weights().
self._bfa_w: Optional[torch.Tensor] = None
self._bfa_f_b_w: Optional[torch.Tensor] = None
if _is_hip:
# ROCm only: _merge_kda_inproj_weights_hip() may merge the
# whole [q,k,v,g | f_a | b] in-proj instead, making _bfa_w a
# tail view of that buffer. _qkvgbfa_sizes is the split of the
# buffer, and stays None when the fusion does not apply. These
# attributes exist on ROCm only; every reader is _is_hip-gated.
self._qkvgbfa_layer: Optional[SimpleNamespace] = None
self._qkvgbfa_sizes: Optional[list[int]] = None
self._qkvgbfa_bs_limit = (
envs.SGLANG_ROCM_K3_FUSE_KDA_INPROJ_MAX_TOKENS.get()
)
elif self.do_fuse_qkvbfg:
self.qkvb_sizes = [
projection_size,
projection_size,
projection_size,
self.num_heads,
]
self.fg_sizes = [self.head_dim, self.head_dim]
self.fused_qkvbfg_a_proj = MergedColumnParallelRepeatedLinear(
self.hidden_size,
self.qkvb_sizes,
self.fg_sizes,
quant_config=quant_config,
prefix=f"{prefix}.fused_qkvbfg_a_proj",
)
self.split_sizes = [
3 * projection_size // self.tp_size,
self.num_heads // self.tp_size,
2 * self.head_dim,
]
_dtype = config.dtype
if isinstance(_dtype, str):
_dtype = getattr(torch, _dtype, torch.bfloat16)
self.fused_fg_b_proj = ColumnParallelBatchedLinear(
2, self.head_dim, projection_size, dtype=_dtype
)
else:
attn_tp_rank = self.attn_tp_rank
self.qkv_proj = QKVParallelLinear(
self.hidden_size,
self.head_dim,
self.num_heads,
self.num_k_heads,
bias=False,
quant_config=quant_config,
tp_rank=attn_tp_rank,
tp_size=self.attn_tp_size,
v_head_size=self.head_v_dim,
prefix=f"{prefix}.qkv_proj",
)
self.f_a_proj = ReplicatedLinear(
self.hidden_size,
self.head_dim,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.f_a_proj",
)
self.f_b_proj = ColumnParallelLinear(
self.head_dim,
projection_size,
bias=False,
quant_config=quant_config,
tp_rank=attn_tp_rank,
tp_size=self.attn_tp_size,
prefix=f"{prefix}.f_b_proj",
)
self.b_proj = ColumnParallelLinear(
self.hidden_size,
self.num_heads,
bias=False,
quant_config=quant_config,
tp_rank=attn_tp_rank,
tp_size=self.attn_tp_size,
prefix=f"{prefix}.b_proj",
)
if self.use_full_rank_gate:
self.g_proj = ColumnParallelLinear(
self.hidden_size,
projection_size,
bias=False,
quant_config=quant_config,
tp_rank=attn_tp_rank,
tp_size=self.attn_tp_size,
prefix=f"{prefix}.g_proj",
)
else:
self.g_a_proj = ReplicatedLinear(
self.hidden_size,
self.head_dim,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.g_a_proj",
)
self.g_b_proj = ColumnParallelLinear(
self.head_dim,
projection_size,
bias=False,
quant_config=quant_config,
tp_rank=attn_tp_rank,
tp_size=self.attn_tp_size,
prefix=f"{prefix}.g_b_proj",
)
self.dt_bias = nn.Parameter(
torch.empty(divide(projection_size, self.attn_tp_size), dtype=torch.float32)
)
set_weight_attrs(self.dt_bias, {"weight_loader": sharded_weight_loader(0)})
self.qkv_conv1d = MergedColumnParallelLinear(
input_size=self.conv_size,
output_sizes=[projection_size, projection_size, projection_size],
bias=False,
params_dtype=torch.float32,
tp_rank=self.attn_tp_rank,
tp_size=self.attn_tp_size,
prefix=f"{prefix}.qkv_conv1d",
)
self.qkv_conv1d.weight.data = self.qkv_conv1d.weight.data.unsqueeze(1)
# Checkpoint stores A_log as [head_dim]; the FLA kernel expects
# local_num_heads. The loader handles both the old 4-D and the K3 1-D
# formats by narrowing to the first num_heads then TP-sharding.
self.A_log = nn.Parameter(
torch.empty(1, 1, self.local_num_heads, 1, dtype=torch.float32)
)
def _a_log_weight_loader(
param: torch.Tensor, loaded_weight: torch.Tensor
) -> None:
tp_rank = get_parallel().attn_tp_rank
shard_size = param.data.shape[2] # local_num_heads
start_idx = tp_rank * shard_size
# Handle old 4-D checkpoint format: [1, 1, H, 1] -> [H]
if loaded_weight.dim() == 4:
loaded_weight = loaded_weight.view(loaded_weight.shape[2])
# Now loaded_weight is 1-D (either [num_heads] or [head_dim]).
# Narrow to the TP shard along the head dimension.
loaded_weight = loaded_weight.narrow(0, start_idx, shard_size)
# Reshape to match param shape [1, 1, local_num_heads, 1]
param.data.copy_(loaded_weight.view(param.data.shape))
set_weight_attrs(self.A_log, {"weight_loader": _a_log_weight_loader})
self.o_norm = FusedRMSNormGated(
self.head_dim, eps=rms_norm_eps, activation="sigmoid"
)
self.o_proj = RowParallelLinear(
projection_size,
self.hidden_size,
bias=False,
# SGLANG_K3_AR_FUSION: keep the o_proj output TP-partial and
# complete the reduce at the decoder layer via the fused MNNVL
# all-reduce (which can fold the attn-res prefix add in). Only
# valid when the attn TP group is the full TP group (the fused
# comm lives there).
reduce_results=not self.all_reduce_fusion,
quant_config=quant_config,
tp_rank=self.attn_tp_rank,
tp_size=self.attn_tp_size,
# Reduce within the attn-TP group: the default full-TP collective
# is the wrong group at attn_tp>1 (sums across DP groups) and
# deadlocks idle DP ranks. Off under all_reduce_fusion: the fused
# AR reduces in place out of a symmetric buffer, and this flag
# would wrap the GEMM in its own symm_ctx, defeating the
# caller-owned buffer.
use_dp_attention_reduce=not self.all_reduce_fusion,
prefix=f"{prefix}.o_proj",
)
if self.all_reduce_fusion and not _o_proj_takes_output(self.o_proj):
# the fused AR reduces o_proj's output in place out of a symmetric
# buffer, which needs the GEMM to write into caller-owned storage
self.all_reduce_fusion = False
self.o_proj.reduce_results = True
self.o_proj.use_dp_attention_reduce = True
k3_gemm_ar.maybe_wrap_o_proj(self.o_proj)
conv_weights = self.qkv_conv1d.weight.squeeze(1)
bias = self.qkv_conv1d.bias
self.attn = RadixLinearAttention(
layer_id=self.layer_idx,
num_q_heads=self.num_k_heads // self.attn_tp_size,
num_k_heads=self.num_k_heads // self.attn_tp_size,
num_v_heads=self.num_v_heads // self.attn_tp_size,
head_q_dim=self.head_k_dim,
head_k_dim=self.head_k_dim,
head_v_dim=self.head_v_dim,
conv_weights=conv_weights,
bias=bias,
A_log=self.A_log,
dt_bias=self.dt_bias,
)
# KDA safe gate: checkpoint trained with gate_lower_bound=-5.0
self.attn.lower_bound = config.linear_attn_config.get("gate_lower_bound", None)
# Set by _prepare_fused_decode() once weights are loaded.
self._kda_fused_decode_ready = False
self._kda_hip_fused_decode_ready = False
def forward_qkvbfg(self, hidden_states: torch.Tensor):
qkv, _ = self.qkv_proj(hidden_states)
beta = self.b_proj(hidden_states)[0]
forget_gate = self.f_b_proj(self.f_a_proj(hidden_states)[0])[0]
if self.use_full_rank_gate:
g_proj_states = self.g_proj(hidden_states)[0]
else:
g_proj_states = self.g_b_proj(self.g_a_proj(hidden_states)[0])[0]
return qkv, beta, forget_gate, g_proj_states
def _merge_bfa_weights(self) -> None:
"""Merge f_a_proj (head_dim outputs) + b_proj (heads/tp outputs).
Both are skinny same-input GEMVs at decode: b lands in a cublas dot
kernel pair, f_a in a splitK GEMM. One [H, head_dim + heads/tp (+pad)]
GEMV replaces both. f_a leads so its output slice starts at offset 0,
and the width is padded to a multiple of 8 so every fused-output row
stays 16-byte aligned for vectorized consumers (tiny-GEMM on f_b).
Called once after weight loading. Block-FP8 inputs are dequantized into
the BF16 tiny-GEMM buffers here."""
if not self.use_full_rank_gate:
return
if _is_npu:
return
if _is_hip and self._merge_kda_inproj_weights_hip():
# Split-path f_b GEMM still uses this when the fused in-proj
# is above the token threshold.
self._bfa_f_b_w = self.f_b_proj.weight
return
mods = [self.f_a_proj, self.b_proj]
if self._bfa_uses_block_fp8:
weights = [_get_k3_dense_weight(mod) for mod in mods]
sizes = [weight.shape[0] for weight in weights]
pad = (-sum(sizes)) % 8
if pad:
weights.append(weights[0].new_zeros((pad, weights[0].shape[1])))
self._bfa_w = torch.cat(weights, dim=0).contiguous()
self._bfa_f_b_w = _get_k3_dense_weight(self.f_b_proj).contiguous()
else:
if any(getattr(mod, "weight", None) is None for mod in mods):
return
self._bfa_w, sizes = _merge_weights_as_views(mods, pad_rows_to=8)
self._bfa_f_b_w = self.f_b_proj.weight
self._bfa_fa_size, self._bfa_b_size = sizes
def _merge_kda_inproj_weights_hip(self) -> bool:
"""ROCm only: append the [f_a | b] tail to the wide [q,k,v,g] buffer so
one GEMM covers the whole in-proj, and take _bfa_w as a tail view of
that buffer. The merge is view-only, so the wide-only and whole-buffer
weights both stay live and forward_qkvbfg_fused picks per batch size.
Returns False when the fusion does not apply, leaving the caller to do
the plain [f_a | b] merge."""
if not self._may_fuse_kda_inproj():
return False
# [q,k,v,g | f_a | b | pad]; f_a/b keep the same relative order and the
# same pad (both widths are 4 short of a multiple of 8), so the tail
# view is byte-identical to the wide-only merge.
merged, sizes = _merge_weights_as_views(
[self.fused_qkvg_proj, self.f_a_proj, self.b_proj], pad_rows_to=8
)
self._bfa_fa_size, self._bfa_b_size = sizes[-2:]
self._bfa_w = merged[sizes[0] :]
# Stand-in "layer" so the fused GEMM goes through the same
# quant_method.apply (and therefore the same backend choice) as the
# wide projection, whose own .weight stays the 6144-row view for the
# above-threshold split path. Not an nn.Module on purpose: this must
# not add a duplicate entry to state_dict.
self._qkvgbfa_layer = SimpleNamespace(weight=merged)
self._qkvgbfa_sizes = [
*self.split_sizes, # q,k,v then g
self._bfa_fa_size,
self._bfa_b_size,
merged.shape[0] - sum(sizes), # alignment pad
]
return True
def _may_fuse_kda_inproj(self) -> bool:
"""Whether the [f_a|b] tail can share the wide projection's buffer.
Needs the wide fused projection to exist and all three weights to be
plain unquantized 2-D tensors of one dtype and width -- the checkpoint
keeps attention in bf16, but a quantized variant would carry scales
that a raw row-cat would silently drop."""
if not (_is_hip and envs.SGLANG_ROCM_K3_FUSE_KDA_INPROJ.get()):
return False
if not (self.do_fuse_qkvbfg and self.use_full_rank_gate):
return False
# Block-FP8 in-proj needs dequantized BF16 buffers; a raw row-cat
# would drop the scales. Leave fusion to the split [f_a|b] path.
if self._bfa_uses_block_fp8:
return False
ws = [m.weight for m in (self.fused_qkvg_proj, self.f_a_proj, self.b_proj)]
if not all(type(w.data) is torch.Tensor and w.dim() == 2 for w in ws):
return False
return len({(w.dtype, w.shape[1]) for w in ws}) == 1
def _prepare_fused_decode(self) -> None:
"""Static inputs for the fused KDA decode kernel
(kernels/ops/attention/kda_fused_decode): per-segment transposed fp32 conv
weights [4, seg], dense fp32 conv bias, fp32 output-norm weight. Stashed on the
attention layer for the KDA backend; when the shapes do not match
the compiled kernel the stash stays unset and decode keeps the
unfused chain. Called once from load_weights (after all weights are
loaded, before cuda graph capture)."""
if _is_hip:
from sglang.kernels.ops.attention import kda_fused_decode_aiter_hip
layer = self.attn
w = layer.conv_weights
f_b_weight = self.f_b_proj.weight
backend = os.environ.get("SGLANG_K3_KDA_FUSED_BACKEND", "").lower()
backend_available = (
backend == "aiter"
and kda_fused_decode_aiter_hip.available(f_b_weight.device)
)
if (
backend_available
and w is not None
and tuple(w.shape) == (3 * 12 * 128, 4)
and w.dtype == torch.float32
and f_b_weight.shape == (12 * 128, 128)
and f_b_weight.dtype == torch.bfloat16
and layer.A_log is not None
and layer.A_log.numel() == 12
and layer.A_log.dtype == torch.float32
and layer.dt_bias is not None
and tuple(layer.dt_bias.shape) == (12 * 128,)
and layer.dt_bias.dtype == torch.float32
and layer.lower_bound is not None
):
norm_weight = self.o_norm.weight.data.to(torch.bfloat16).contiguous()
f_b_weight = f_b_weight.view(12, 128, 128).contiguous()
a_log = layer.A_log.detach().reshape(-1).contiguous()
layer._k3_hip_fused_decode_args = (
f_b_weight,
norm_weight,
float(self.o_norm.eps),
a_log,
)
kda_fused_decode_aiter_hip.warmup(
f_b_weight=f_b_weight,
conv_weight=w,
A_log=a_log,
dt_bias=layer.dt_bias,
lower_bound=float(layer.lower_bound),
norm_weight=norm_weight,
norm_eps=float(self.o_norm.eps),
)
layer._k3_hip_fused_decode_backend = backend
self._kda_hip_fused_decode_ready = True
return
layer = self.attn
w = layer.conv_weights
if _is_npu:
return
seg = 12 * 128 # compiled for H = HV = 12 heads of 128 (TP8)
if (
w is None
or w.ndim != 2
or w.shape != (3 * seg, 4)
or w.dtype != torch.float32
or layer.A_log is None
or layer.A_log.numel() != 12
or layer.A_log.dtype != torch.float32
or layer.dt_bias is None
or tuple(layer.dt_bias.shape) != (seg,)
or layer.dt_bias.dtype != torch.float32
):
rank0_log(
"K3 fused KDA decode disabled: unexpected conv/A_log/dt_bias "
f"layout (conv {None if w is None else tuple(w.shape)}, "
f"A_log {None if layer.A_log is None else tuple(layer.A_log.shape)}, "
f"dt_bias {None if layer.dt_bias is None else tuple(layer.dt_bias.shape)})"
)
return
# Conv weights/bias stay fp32 (checkpoint dtype; the kernel loads
# them as fp32, matching the triton chain's precision exactly).
wt = w.t().contiguous() # [4, 3*seg]
bias = layer.bias
conv_bias = (
bias.float().contiguous()
if bias is not None
else torch.zeros(3 * seg, dtype=torch.float32, device=w.device)
)
layer._k3_fused_decode_args = (
wt[:, :seg].contiguous(),
wt[:, seg : 2 * seg].contiguous(),
wt[:, 2 * seg :].contiguous(),
conv_bias,
layer.A_log.detach().reshape(-1), # view; kernel wants [12]
self.o_norm.weight.data.float().contiguous(),
float(self.o_norm.eps),
)
self._kda_fused_decode_ready = True
def forward_qkvbfg_fused(
self, hidden_states: torch.Tensor, defer_f_b: bool = False
):
if self.use_full_rank_gate:
if self._bfa_w is not None:
w = self._bfa_w
n_fa, n_b = self._bfa_fa_size, self._bfa_b_size
from sglang.kernels.ops.kimi_k3 import kimi_k3_tiny_gemm as gemm
if (
_is_hip
and self._qkvgbfa_sizes is not None
and 0 < hidden_states.shape[0] <= self._qkvgbfa_bs_limit
):
# ROCm only. One GEMM for the whole in-proj: the [f_a|b]
# tail rides the wide projection's bandwidth (~30% of the
# in-proj at decode on gfx950, SGLANG_ROCM_K3_FUSE_KDA_INPROJ).
fused_states = self.fused_qkvg_proj.quant_method.apply(
self._qkvgbfa_layer, hidden_states, None
)
qkv, g_proj_states, f_a, beta, _pad = torch.split(
fused_states, self._qkvgbfa_sizes, dim=-1
)
# Fused KDA decode consumes f_a and applies f_b itself.
forget_gate = f_a if defer_f_b else gemm(f_a, self._bfa_f_b_w)
return qkv, beta, forget_gate, g_proj_states
if (
self._bfa_alt_stream is not None
and get_is_capture_mode()
and 0 < hidden_states.shape[0] <= self._bfa_bs_limit
):
# Fork before both branches; capture the main projection
# first to avoid CUDA graph replay stream expansion.
alt = self._bfa_alt_stream
cur = torch.cuda.current_stream()
alt.wait_stream(cur)
fused_states, _ = self.fused_qkvg_proj(hidden_states)
with torch.cuda.stream(alt):
bfa = gemm(hidden_states, w)
forget_gate = (
bfa[..., :n_fa]
if defer_f_b
else gemm(bfa[..., :n_fa], self._bfa_f_b_w)
)
beta = bfa[..., n_fa : n_fa + n_b]
qkv, g_proj_states = torch.split(
fused_states, self.split_sizes, dim=-1
)
cur.wait_stream(alt)
return qkv, beta, forget_gate, g_proj_states
fused_states, _ = self.fused_qkvg_proj(hidden_states)
qkv, g_proj_states = torch.split(fused_states, self.split_sizes, dim=-1)
bfa = gemm(hidden_states, w)
forget_gate = (
bfa[..., :n_fa]
if defer_f_b
else gemm(bfa[..., :n_fa], self._bfa_f_b_w)
)
beta = bfa[..., n_fa : n_fa + n_b]
else:
fused_states, _ = self.fused_qkvg_proj(hidden_states)
qkv, g_proj_states = torch.split(fused_states, self.split_sizes, dim=-1)
beta = self.b_proj(hidden_states)[0]
f_a = self.f_a_proj(hidden_states)[0]
forget_gate = f_a if defer_f_b else self.f_b_proj(f_a)[0]
else:
fused_states = self.fused_qkvbfg_a_proj(hidden_states)
qkv, beta, fg_a_states = torch.split(fused_states, self.split_sizes, dim=-1)
forget_gate, g_proj_states = self.fused_fg_b_proj(
fg_a_states.view(-1, 2, self.head_dim).transpose(0, 1)
)
return qkv, beta, forget_gate, g_proj_states
def forward(
self,
hidden_states: torch.Tensor,
positions: torch.Tensor,
forward_batch: ForwardBatch,
zero_allocator: BumpAllocator,
) -> torch.Tensor:
defer_f_b = (
self._kda_hip_fused_decode_ready and forward_batch.forward_mode.is_decode()
)
if self.do_fuse_qkvbfg or self.use_full_rank_gate:
mixed_qkv, beta, forget_gate, g_proj_states = self.forward_qkvbfg_fused(
hidden_states, defer_f_b=defer_f_b
)
else:
mixed_qkv, beta, forget_gate, g_proj_states = self.forward_qkvbfg(
hidden_states
)
if not forward_batch.forward_mode.is_decode():
forget_gate = forget_gate.unflatten(-1, (-1, self.head_dim))
if not forward_batch.forward_mode.is_target_verify():
# Only chunk_kda (extend) wants pre-activated beta; the verify
# kernel sigmoids it in-kernel like decode.
beta = beta.float().sigmoid()
forget_gate = forget_gate.unsqueeze(0)
beta = beta.unsqueeze(0)
# Fused KDA handoff (attempt-and-verify): offer the output-norm gate
# so covered decode and target-verify kernels can fold gated RMSNorm
# into the recurrence kernel. If the backend leaves the stash
# unconsumed (env off or shape not covered), apply o_norm here as
# before.
fused_onorm = (self._kda_fused_decode_ready or defer_f_b) and (
forward_batch.forward_mode.is_decode()
or forward_batch.forward_mode.is_target_verify()
)
if fused_onorm:
self.attn._k3_onorm_gate = g_proj_states
self.attn._k3_onorm_consumed = False
if defer_f_b:
self.attn._k3_deferred_f_b = True
core_attn_out = self.attn(
forward_batch,
mixed_qkv=mixed_qkv,
a=forget_gate,
b=beta,
)
if fused_onorm:
self.attn._k3_onorm_gate = None
fused_onorm = self.attn._k3_onorm_consumed
if defer_f_b:
self.attn._k3_deferred_f_b = False
if not fused_onorm:
norm_gate = g_proj_states.unflatten(-1, (-1, self.head_dim))
core_attn_out = self.o_norm(core_attn_out, norm_gate)
core_attn_out = core_attn_out.squeeze(0).flatten(-2)
if self.all_reduce_fusion:
out = _k3_symm_o_proj_out(self.o_proj, core_attn_out)
partial, _ = self.o_proj(core_attn_out, output_tensor=out)
return partial
return self.o_proj(core_attn_out)[0]
class KimiK3MLAAttention(DeepseekV2AttentionMLA):
"""MLA with output gate for K3. Gate is applied in TP-local space before o_proj."""
def __init__(
self,
config,
layer_idx: int,
quant_config: Optional[QuantizationConfig] = None,
all_reduce_fusion: bool = False,
prefix: str = "",
alt_stream: Optional[torch.cuda.Stream] = None,
gate_alt_stream: Optional[torch.cuda.Stream] = None,
) -> None:
# ModelSlim can quantize K3 latent projections while still storing
# MLA kv_b_proj as one dense tensor; only GGUF expert packs split K/V.
split_gguf_kv_b = _uses_split_gguf_kv_b(quant_config)
self.all_reduce_fusion = all_reduce_fusion
self.use_output_gate = getattr(config, "mla_use_output_gate", False)
# The fused Ascend split+RMSNorm path is not numerically equivalent for
# Kimi-K3. Other MLA models retain the existing fused fast path.
self._disable_npu_fused_split_qk_norm = True
super().__init__(
layer_id=layer_idx,
hidden_size=config.hidden_size,
num_heads=config.num_attention_heads,
quant_config=quant_config,
prefix=prefix,
config=config,
qk_nope_head_dim=config.qk_nope_head_dim,
qk_rope_head_dim=config.qk_rope_head_dim,
v_head_dim=config.v_head_dim,
q_lora_rank=config.q_lora_rank,
kv_lora_rank=config.kv_lora_rank,
skip_rope=True,
reduce_results=not self.all_reduce_fusion,
alt_stream=alt_stream,
)
if split_gguf_kv_b:
del self.fused_qkv_a_proj_with_mqa
del self.kv_b_proj
self.q_a_proj = ReplicatedLinear(
config.hidden_size,
config.q_lora_rank,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.q_a_proj",
)
self.kv_a_proj_with_mqa = ReplicatedLinear(
config.hidden_size,
config.kv_lora_rank + config.qk_rope_head_dim,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.kv_a_proj_with_mqa",
)
self.has_fused_proj = False
from sglang.srt.layers.quantization.gguf import GGUFUninitializedParameter
for role in ("k", "v"):
qweight = GGUFUninitializedParameter(requires_grad=False)
set_weight_attrs(
qweight,
{
"is_gguf_weight": True,
"weight_loader": self._split_kv_b_weight_loader,
},
)
self.register_parameter(f"{role}_b_qweight", qweight)
qweight_type = nn.Parameter(
torch.empty(1, dtype=torch.uint8), requires_grad=False
)
set_weight_attrs(
qweight_type,
{
"is_gguf_weight_type": True,
"weight_type": 0,
"ignore_warning": True,
"weight_loader": self._split_kv_b_weight_loader,
},
)
self.register_parameter(f"{role}_b_qweight_type", qweight_type)
self._kimi_split_gguf_kv_b = True
# Installed before the output-gate wrap below so the gate multiply is
# applied to x before the fused GEMM+AR sees it.
if self.all_reduce_fusion and not _o_proj_takes_output(self.o_proj):
# the fused AR reduces o_proj's output in place out of a symmetric
# buffer, which needs the GEMM to write into caller-owned storage
self.all_reduce_fusion = False
self.o_proj.reduce_results = True
self.o_proj.use_dp_attention_reduce = True
k3_gemm_ar.maybe_wrap_o_proj(self.o_proj)
if self.all_reduce_fusion:
# Hand the GEMM a slice of the persistent symmetric buffer
# (k3_ar_fusion.symm_buffer); the fused AR reduces it in place.
# The captured name must differ from the gate block's
# `_orig_o_proj_forward` — closures capture the __init__ local by
# reference, and reusing it would rebind to this wrapper (recursion).
_symm_inner_o_proj_forward = self.o_proj.forward
_symm_o_proj = self.o_proj
def _symm_o_proj_forward(x, *args, **kwargs):
return _symm_inner_o_proj_forward(
x,
*args,
output_tensor=_k3_symm_o_proj_out(_symm_o_proj, x),
**kwargs,
)
self.o_proj.forward = _symm_o_proj_forward
else:
# K3 has no LayerCommunicator, so o_proj must reduce within the
# attn-TP group itself: the default full-TP collective is the wrong
# group at attn_tp>1 and deadlocks idle DP ranks.
self.o_proj.use_dp_attention_reduce = True
if self.use_output_gate:
projection_size = config.num_attention_heads * config.v_head_dim
# Shard by attn-TP to match the attention output (DSV2 MLA shards
# heads across the attention-TP group, not the global TP group).
self.g_proj = ColumnParallelLinear(
config.hidden_size,
projection_size,
bias=False,
quant_config=quant_config,
tp_rank=get_parallel().attn_tp_rank,
tp_size=get_parallel().attn_tp_size,
prefix=f"{prefix}.g_proj",
)
# Output gate multiplies the TP-local attention output right
# before o_proj; o_proj is invoked deep inside
# DeepseekV2AttentionMLA forward cores, so wrap its forward at
# the instance level (weights, reduce_results, loading untouched).
self._gate_hidden_states = None
self._gate_pending_stream = None
self._gate_alt_stream = gate_alt_stream
# Above this token count the attention-core kernels fill the SMs
# on their own and the overlap only adds sync overhead (same
# bound as deepseek_v4).
self._gate_bs_limit = (
(128 if get_platform().is_blackwell else 64)
if self._gate_alt_stream is not None
else 0
)
_orig_o_proj_forward = self.o_proj.forward
def _gated_o_proj_forward(x, *args, **kwargs):
gate_input = self._gate_hidden_states
self._gate_hidden_states = None
if gate_input is not None and not isinstance(x, tuple):
gate = self._compute_output_gate(gate_input)
from sglang.kernels.ops.kimi_k3 import mla_output_gate
if mla_output_gate.covered(x, gate):
# One kernel for x * sigmoid(gate); double rounding
# matches the unfused pair bit-for-bit.
x = mla_output_gate.kimi_k3_mla_output_gate(x, gate)
else:
x = x * torch.sigmoid(gate)
elif self._gate_pending_stream is not None:
# Even a skipped gate must close its capture branch.
torch.cuda.current_stream().wait_stream(self._gate_pending_stream)
self._gate_pending_stream = None
return _orig_o_proj_forward(x, *args, **kwargs)
self.o_proj.forward = _gated_o_proj_forward
@staticmethod
def _split_kv_b_weight_loader(param, loaded_weight) -> None:
from torch.nn.parameter import UninitializedParameter
if getattr(param, "is_gguf_weight_type", False):
param.weight_type = int(loaded_weight.item())
param.data.copy_(loaded_weight.reshape_as(param))
return
if isinstance(param, UninitializedParameter):
param.materialize(tuple(loaded_weight.shape), dtype=loaded_weight.dtype)
param.data.copy_(loaded_weight)
def dispatch_attn_forward_method(self, forward_batch) -> AttnForwardMethod:
method = super().dispatch_attn_forward_method(forward_batch)
if getattr(self, "_kimi_split_gguf_kv_b", False):
return AttnForwardMethod.MLA
return method
def _fork_output_gate(self, hidden_states: torch.Tensor) -> None:
"""Fork early, but record the gate after attention to limit replay streams."""
self._gate_pending_stream = None
if (
self._gate_alt_stream is not None
and get_is_capture_mode()
# Keep the fork and join within one capture segment.
and not is_in_breakable_cuda_graph()
and (0 < hidden_states.shape[0] <= self._gate_bs_limit)
):
alt = self._gate_alt_stream
alt.wait_stream(torch.cuda.current_stream())
self._gate_pending_stream = alt
def _compute_output_gate(self, hidden_states: torch.Tensor) -> torch.Tensor:
alt = self._gate_pending_stream
self._gate_pending_stream = None
if alt is None:
return self.g_proj(hidden_states)[0]
with torch.cuda.stream(alt):
gate, _ = self.g_proj(hidden_states)
torch.cuda.current_stream().wait_stream(alt)
return gate
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
forward_batch: ForwardBatch,
zero_allocator: BumpAllocator,
**kwargs,
):
if self.use_output_gate:
self._gate_hidden_states = hidden_states
self._fork_output_gate(hidden_states)
return super().forward(
positions, hidden_states, forward_batch, zero_allocator, **kwargs
)
class KimiK3DecoderLayer(nn.Module):
"""Decoder layer carrying the K3 attention-residual stream."""
def __init__(
self,
config: KimiLinearConfig,
layer_idx: int,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
alt_streams: Optional[List[torch.cuda.Stream]] = None,
) -> None:
super().__init__()
self.hidden_size = config.hidden_size
self.is_moe = config.is_moe
self.layer_idx = layer_idx
self._dp_attention = is_dp_attention_enabled()
# mlp-sync (DP attention OR MoE a2a/EP) pads extend batches to
# attn_tp multiples; attention must then run on the real rows only.
self._trim_padded_attn = require_mlp_sync()
# A layer runs MoE (vs a plain dense MLP) iff it is past the dense
# prefix and on the MoE cadence — same predicate the mlp construction
# below uses.
self._is_moe_layer = (
self.is_moe
and config.num_experts is not None
and layer_idx >= config.first_k_dense_replace
and layer_idx % config.moe_layer_freq == 0
)
# SP-MoE (EP a2a backend): o_proj defers its attention-TP reduction;
# this layer completes it as a reduce-scatter so the whole MoE region
# runs on 1/attn_tp of the rows, then all-gathers back after the tail
# add. RS+AG moves the same bytes the AR did, the shared-expert AR
# disappears via tp1 weights, and each rank dispatches only its shard
# through the a2a. Same under DP attention. Dense layers excluded: no
# per-token decomposition survives a token shard.
_a2a_backend = get_moe_a2a_backend()
self._sp_moe = (
(
_a2a_backend.is_megamoe()
or _a2a_backend.is_deepep()
or _a2a_backend.is_mooncake()
or _a2a_backend.is_ascend_fuseep()
or _a2a_backend.is_mori()
)
and self._is_moe_layer
and get_parallel().attn_tp_group.world_size > 1
)
# Mutually exclusive with SP-MoE: both complete o_proj's deferred
# reduction, but SP-MoE reduce-scatters to a shard whereas the fusion
# produces the full batch in a symm buffer.
attn_tp_size = get_parallel().attn_tp_size
self.all_reduce_fusion = (
not self._sp_moe
and attn_tp_size > 1
and attn_tp_size == get_parallel().tp_size
and config.attn_res_block_size is not None
and k3_ar_fusion.enabled()
)
# Attention
if config.is_kda_layer(layer_idx):
self.self_attn = KimiK3DeltaAttention(
layer_idx=layer_idx,
hidden_size=config.hidden_size,
config=config,
quant_config=quant_config,
prefix=f"{prefix}.self_attn",
all_reduce_fusion=self.all_reduce_fusion,
# Shared with the MLA gate stream: KDA and MLA layers never
# run concurrently within one forward, so the stream is free.
bfa_alt_stream=(alt_streams[2] if alt_streams is not None else None),
)
else:
self.self_attn = KimiK3MLAAttention(
config=config,
layer_idx=layer_idx,
quant_config=quant_config,
prefix=f"{prefix}.self_attn",
all_reduce_fusion=self.all_reduce_fusion,
alt_stream=alt_streams[1] if alt_streams is not None else None,
gate_alt_stream=alt_streams[2] if alt_streams is not None else None,
)
# the attention drops the fusion when its o_proj cannot write into
# caller-owned storage; the layer's own AR call-site must agree
self.all_reduce_fusion = self.self_attn.all_reduce_fusion
# MLP / MoE
if self._is_moe_layer:
self.mlp = KimiK3MoE(
config=config,
quant_config=quant_config,
layer_idx=layer_idx,
prefix=f"{prefix}.mlp",
alt_stream=alt_streams[0] if alt_streams is not None else None,
)
else:
self.mlp = KimiK3MLP(
hidden_size=config.hidden_size,
intermediate_size=config.intermediate_size,
hidden_act=config.hidden_act,
quant_config=quant_config,
prefix=f"{prefix}.mlp",
activation_situ_beta=config.activation_situ_beta,
activation_situ_linear_beta=config.activation_situ_linear_beta,
)
self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.post_attention_layernorm = RMSNorm(
config.hidden_size, eps=config.rms_norm_eps
)
# Attention Residual
self.use_attn_residuals = config.attn_res_block_size is not None
if self.use_attn_residuals:
self.attn_res_block_size = config.attn_res_block_size
self.is_block_write_layer = layer_idx % self.attn_res_block_size == 0
self.prev_valid_blocks = _cdiv(layer_idx, self.attn_res_block_size)
self.self_attention_res_norm = RMSNorm(
config.hidden_size, eps=config.rms_norm_eps
)
self.mlp_res_norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.self_attention_res_proj = ReplicatedLinear(
config.hidden_size,
1,
bias=False,
quant_config=None,
prefix=f"{prefix}.self_attention_res_proj",
)
self.mlp_res_proj = ReplicatedLinear(
config.hidden_size,
1,
bias=False,
quant_config=None,
prefix=f"{prefix}.mlp_res_proj",
)
if self._sp_moe:
# o_proj emits TP-partial sums; _finish_attn_reduce completes the
# reduction (RS on the clean attn-res path, AR on fallbacks).
o_proj = getattr(self.self_attn, "o_proj", None)
assert o_proj is not None, "SP-MoE requires attention exposing o_proj"
o_proj.reduce_results = False
if k3_sp_collective.enabled():
# The table selects NVLS pull RS for larger token buckets.
# Only those o_proj outputs come from the persistent symmetric
# buffer; small push RS keeps the regular graph allocator.
_sp_inner_o_proj_forward = o_proj.forward
def _sp_o_proj_forward(x, *args, **kwargs):
output_rows = k3_sp_collective.get_o_proj_output_rows(x.shape[0])
if k3_sp_collective.requires_symmetric_rs(output_rows, x.device):
output = k3_sp_collective.get_o_proj_output_buffer(
output_rows, x.dtype, o_proj.output_size
)
result = _sp_inner_o_proj_forward(
x, *args, output_tensor=output[: x.shape[0]], **kwargs
)
k3_sp_collective.register_o_proj_output(result[0], output)
return result
return _sp_inner_o_proj_forward(x, *args, **kwargs)
o_proj.forward = _sp_o_proj_forward
def _finish_attn_reduce(
self,
attn_out: torch.Tensor,
allow_scatter: bool,
residual: Optional[torch.Tensor] = None,
) -> tuple[torch.Tensor, int, bool]:
"""Complete o_proj's deferred TP reduction under SP-MoE.
Returns (reduced tensor, shard row offset, residual_fused); offset is
-1 when the result covers the full batch (non-SP mode, or fallback
all-reduce for row counts not divisible by attn_tp)."""
if not self._sp_moe:
return attn_out, -1, False
group = get_parallel().attn_tp_group
num_tokens = attn_out.shape[0]
if allow_scatter and num_tokens > 0 and num_tokens % group.world_size == 0:
shard = num_tokens // group.world_size
custom_out = k3_sp_collective.reduce_scatter_res(attn_out, residual)
if custom_out is not None:
return (
custom_out,
group.rank_in_group * shard,
residual is not None,
)
out = torch.empty(
(shard, attn_out.shape[1]),
dtype=attn_out.dtype,
device=attn_out.device,
)
group.reduce_scatter_tensor(out, attn_out)
return out, group.rank_in_group * shard, False
return group.all_reduce(attn_out), -1, False
def _run_self_attn(
self,
hidden_states: torch.Tensor,
positions: torch.Tensor,
forward_batch: ForwardBatch,
zero_allocator: BumpAllocator,
) -> torch.Tensor:
# DP attention: idle ranks (padded to the global shape) have no
# attention metadata; pass hidden_states through shape-preserving
# (same as the LayerCommunicator models' is_idle skip).
if forward_batch.forward_mode.is_idle():
return hidden_states
# mlp-sync pads extend batches to a multiple of attn_tp_size, but the
# attention metadata covers only the real tokens: flashinfer ragged
# prefill rejects the row mismatch, and silent paths would write the
# padded rows' garbage KV through zero-padded out_cache_loc (clobbering
# pool slot 0 -> cross-request corruption). Run attention on the real
# rows and zero-pad the output back.
num_padded = hidden_states.shape[0]
num_real = num_padded
if self._trim_padded_attn and forward_batch.forward_mode.is_extend():
extend_lens = forward_batch.extend_seq_lens_cpu
if extend_lens is not None:
num_real = min(int(sum(extend_lens)), num_padded)
if num_real != num_padded:
with k3_sp_collective.o_proj_output_rows(num_padded):
attn_out = self._run_self_attn_inner(
hidden_states[:num_real],
positions[:num_real],
forward_batch,
zero_allocator,
)
padded_o_proj = k3_sp_collective.finish_padded_o_proj_output(
attn_out, num_padded
)
if padded_o_proj is not None:
return padded_o_proj
out = hidden_states.new_zeros(num_padded, attn_out.shape[-1])
out[:num_real] = attn_out
return out
return self._run_self_attn_inner(
hidden_states, positions, forward_batch, zero_allocator
)
def _run_self_attn_inner(
self,
hidden_states: torch.Tensor,
positions: torch.Tensor,
forward_batch: ForwardBatch,
zero_allocator: BumpAllocator,
) -> torch.Tensor:
# For MLA layers with q_lora_rank, set up communicator attn_inputs
# before the forward call (normally done by LayerCommunicator).
from sglang.srt.layers.communicator import (
AttentionInputs,
get_attn_tp_context,
)
qkv_latent_func = getattr(self.self_attn, "prepare_qkv_latent", None)
if qkv_latent_func is not None:
attn_inputs = AttentionInputs(hidden_states, forward_batch, qkv_latent_func)
get_attn_tp_context().set_attn_inputs(attn_inputs)
result = self.self_attn(
hidden_states=hidden_states,
positions=positions,
forward_batch=forward_batch,
zero_allocator=zero_allocator,
)
if qkv_latent_func is not None:
get_attn_tp_context().clear_attn_inputs()
return result
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
forward_batch: ForwardBatch,
residual: Optional[torch.Tensor],
attn_res: Optional[AttnResidual],
zero_allocator: BumpAllocator,
input_sharded: bool = False,
keep_sharded: bool = False,
) -> tuple[torch.Tensor, Optional[torch.Tensor], bool]:
if attn_res is not None:
return self._forward_attn_residual(
positions,
hidden_states,
residual,
attn_res,
forward_batch,
zero_allocator,
input_sharded,
keep_sharded,
)
assert not input_sharded
# Standard residual path
if residual is None:
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
else:
hidden_states, residual = self.input_layernorm(hidden_states, residual)
hidden_states = self._run_self_attn(
hidden_states, positions, forward_batch, zero_allocator
)
# standard path returns a full-size residual to the next layer, so
# complete the deferred o_proj reduction as a plain all-reduce
hidden_states, _, _ = self._finish_attn_reduce(
hidden_states, allow_scatter=False
)
hidden_states, residual = self.post_attention_layernorm(hidden_states, residual)
hidden_states = self.mlp(hidden_states, forward_batch=forward_batch)
return hidden_states, residual, False
def _forward_attn_residual(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
prefix_sum: Optional[torch.Tensor],
attn_res: AttnResidual,
forward_batch: ForwardBatch,
zero_allocator: BumpAllocator,
input_sharded: bool,
keep_sharded: bool,
) -> tuple[torch.Tensor, Optional[torch.Tensor], bool]:
# Between attn-res layers hidden_states carries the previous layer's
# un-added MLP delta and prefix_sum the prefix it extends (None at
# stream start / PP entry, where hidden_states already is the head).
# ---- Aggregation 1: attention side. Write layers snapshot the
# pre-attention prefix into the bank in the same call (fused into
# the fast kernel; standalone copy on other paths). ----
if input_sharded:
assert self._sp_moe
input_rows = _sp_local_rows(hidden_states)
fused_ag = attn_res.forward_sp_all_gather(
hidden_states,
prefix_sum,
self.self_attention_res_proj,
self.self_attention_res_norm,
self.input_layernorm,
rows=input_rows,
write=self.is_block_write_layer,
)
if fused_ag is not None:
hidden_states, prefix_sum = fused_ag
else:
hidden_states, prefix_sum = attn_res.forward(
hidden_states,
prefix_sum,
self.self_attention_res_proj,
self.self_attention_res_norm,
self.input_layernorm,
rows=input_rows,
write=self.is_block_write_layer,
)
# Aggregate/norm and snapshot only this rank's rows, then
# gather the normalized tensor consumed by attention.
hidden_states = _sp_all_gather_rows(hidden_states)
else:
hidden_states, prefix_sum = attn_res.forward(
hidden_states,
prefix_sum,
self.self_attention_res_proj,
self.self_attention_res_norm,
self.input_layernorm,
write=self.is_block_write_layer,
)
if self.is_block_write_layer:
prefix_sum = None
# ---- Attention ----
hidden_states = self._run_self_attn(
hidden_states, positions, forward_batch, zero_allocator
)
# ---- Complete o_proj's deferred reduction ----
# SP-MoE takes precedence (reduce-scatter to this rank's token shard);
# otherwise the fused all-reduce when enabled; otherwise o_proj already
# reduced itself (use_dp_attention_reduce, on when neither is active).
rows = None
shard_lo = -1
agg2_fused = False
if self._sp_moe:
group = get_parallel().attn_tp_group
if (
hidden_states.shape[0] > 0
and hidden_states.shape[0] % group.world_size == 0
):
shard = hidden_states.shape[0] // group.world_size
fused_rows = slice(
group.rank_in_group * shard,
(group.rank_in_group + 1) * shard,
)
fused_rs = attn_res.forward_sp_reduce_scatter(
hidden_states,
prefix_sum,
self.mlp_res_proj,
self.mlp_res_norm,
self.post_attention_layernorm,
rows=fused_rows,
)
else:
fused_rs = None
if fused_rs is not None:
hidden_states, prefix_sum = fused_rs
rows = fused_rows
shard_lo = fused_rows.start
agg2_fused = True
else:
hidden_states, shard_lo, residual_fused = self._finish_attn_reduce(
hidden_states, allow_scatter=True, residual=prefix_sum
)
if shard_lo >= 0:
rows = slice(shard_lo, shard_lo + hidden_states.shape[0])
if residual_fused:
prefix_sum = None
elif prefix_sum is not None:
# Shard carry already holds the destination-local prefix;
# the first sharded layer still holds a full-batch prefix.
if prefix_sum.shape[0] != hidden_states.shape[0]:
prefix_sum = prefix_sum[rows]
elif self.all_reduce_fusion:
# Complete the o_proj reduce here, folding the pending prefix add
# into the fused all-reduce; attn_res then takes the pre-added
# tensor through its prefix_sum=None branch (same semantics:
# (normed, new_prefix) with new_prefix = prefix + attn_out).
hidden_states = k3_ar_fusion.all_reduce(hidden_states, prefix_sum)
prefix_sum = None
# ---- Aggregation 2: MLP side (on the shard under SP-MoE) ----
if not agg2_fused:
hidden_states, prefix_sum = attn_res.forward(
hidden_states,
prefix_sum,
self.mlp_res_proj,
self.mlp_res_norm,
self.post_attention_layernorm,
rows=rows,
)
# ---- MLP (consumes +prefix_sum: MoE folds it into the 3-way tail
# add, dense adds it after down_proj) ----
out = self.mlp(
hidden_states, prefix_sum=prefix_sum, forward_batch=forward_batch
)
if shard_lo >= 0:
if keep_sharded:
return out, None, True
out = _sp_all_gather_rows(out)
return out, None, False
class KimiK3LinearModel(nn.Module):
"""K3 language-model backbone."""
def __init__(
self,
config: KimiLinearConfig,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
):
super().__init__()
self.config = config
self.pp_group = get_parallel().pp_group
self.dspark_layers_to_capture: Optional[list[int]] = None
self._dp_attention = is_dp_attention_enabled()
self._trim_padded_attn = require_mlp_sync()
if self.pp_group.is_first_rank:
embedding_quant_config = (
quant_config
if quant_config is not None and quant_config.get_name() == "expert_pack"
else None
)
self.embed_tokens = VocabParallelEmbedding(
config.vocab_size,
config.hidden_size,
quant_config=embedding_quant_config,
prefix=f"{prefix}.embed_tokens",
# Under DP attention each rank embeds only its local tokens:
# reduce within the attention-TP group, not the full TP group.
**get_embedding_tp_kwargs(),
)
else:
self.embed_tokens = PPMissingLayer()
# Alt streams threaded down to the layers. Slots:
# [0] MoE dual-stream shared-expert tail
# [1] DeepseekV2AttentionMLA base internals (forwarded; unused by K3)
# [2] MLA output-gate GEMM, overlaps the attention core
# Disable on HIP code path.
self.alt_streams = None if _is_hip else [torch.cuda.Stream() for _ in range(3)]
self.layers, self.start_layer, self.end_layer = make_layers(
config.num_hidden_layers,
lambda idx, prefix: KimiK3DecoderLayer(
layer_idx=idx,
config=config,
quant_config=quant_config,
prefix=prefix,
alt_streams=self.alt_streams,
),
pp_rank=self.pp_group.rank_in_group,
pp_size=self.pp_group.world_size,
prefix=f"{prefix}.layers",
)
if self.pp_group.is_last_rank:
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
if config.attn_res_block_size is not None:
self.output_attn_res_norm = RMSNorm(
config.hidden_size, eps=config.rms_norm_eps
)
self.output_attn_res_proj = ReplicatedLinear(
config.hidden_size,
1,
bias=False,
quant_config=None,
prefix=f"{prefix}.output_attn_res_proj",
)
else:
self.norm = PPMissingLayer()
def forward(
self,
input_ids: torch.Tensor | None,
positions: torch.Tensor,
forward_batch: ForwardBatch,
inputs_embeds: torch.Tensor | None = None,
pp_proxy_tensors: Optional[PPProxyTensors] = None,
) -> torch.Tensor:
if get_parallel().pp_group.is_first_rank:
if inputs_embeds is not None:
hidden_states = inputs_embeds
else:
hidden_states = self.embed_tokens(input_ids)
residual = None
else:
assert pp_proxy_tensors is not None
hidden_states = pp_proxy_tensors["hidden_states"]
residual = pp_proxy_tensors["residual"]
if TYPE_CHECKING:
assert isinstance(hidden_states, torch.Tensor)
assert isinstance(residual, torch.Tensor | None)
# mlp-sync (DP attention OR MoE a2a/EP) pads extend batches to a
# multiple of attn_tp_size; attention layers run on the real rows
# only (_run_self_attn trims), so the KV write locations must match
# the trimmed length. positions and hidden_states keep the padded
# length for the DP gather/scatter and the MoE.
if (
self._trim_padded_attn
and forward_batch.forward_mode.is_extend()
and forward_batch.out_cache_loc is not None
and forward_batch.extend_seq_lens_cpu is not None
):
num_real = int(sum(forward_batch.extend_seq_lens_cpu))
if forward_batch.out_cache_loc.shape[0] > num_real:
forward_batch.out_cache_loc = forward_batch.out_cache_loc[:num_real]
total_num_layers = self.end_layer - self.start_layer
device = hidden_states.device
zero_allocator = BumpAllocator(
buffer_size=total_num_layers * 2,
dtype=torch.float32,
device=device,
)
attn_res = None
if self.config.attn_res_block_size is not None:
attn_res_block_num = _cdiv(self.end_layer, self.config.attn_res_block_size)
attn_res = AttnResidual(
hidden_states,
attn_res_block_num,
block_residual=residual,
)
residual = None
# Carry the raw residual stream as a token shard across consecutive
# SP-MoE layers. PP transfer and dspark capture require full tensors,
# so those uncommon paths keep the established gather-per-layer flow.
sp_attn_res = (
attn_res is not None
and envs.SGLANG_K3_SP_ATTN_RES.get()
and self.pp_group.world_size == 1
and self.dspark_layers_to_capture is None
and k3_sp_collective.enabled()
)
sp_sharded = False
aux_hidden_states = []
if (
self.dspark_layers_to_capture is not None
and not self.pp_group.is_first_rank
):
if "dspark_hidden_states" in pp_proxy_tensors.tensors:
aux_hidden_states.append(pp_proxy_tensors["dspark_hidden_states"])
if self.start_layer - 1 in self.dspark_layers_to_capture:
aux_hidden_states.append(
self._dspark_capture_stream(
self.start_layer - 1, hidden_states, residual, attn_res
)
)
for i in range(self.start_layer, self.end_layer):
if sp_sharded and not self.layers[i]._sp_moe:
hidden_states = _sp_all_gather_rows(hidden_states)
sp_sharded = False
with get_global_expert_distribution_recorder().with_current_layer(i):
hidden_states, residual, sp_sharded = self.layers[i](
positions=positions,
hidden_states=hidden_states,
forward_batch=forward_batch,
residual=residual,
attn_res=attn_res,
zero_allocator=zero_allocator,
input_sharded=sp_sharded,
keep_sharded=sp_attn_res,
)
if (
self.dspark_layers_to_capture is not None
and i in self.dspark_layers_to_capture
and (i + 1 < self.end_layer or self.pp_group.is_last_rank)
):
aux_hidden_states.append(
self._dspark_capture_stream(i, hidden_states, residual, attn_res)
)
if not self.pp_group.is_last_rank:
assert not sp_sharded
if attn_res is not None:
if residual is not None:
# Materialize the delayed MLP add: the wire carries the
# full stream head (bit-identical to the fused fold).
hidden_states = residual + hidden_states
residual = attn_res.block_residual # raw bank across ranks
proxy_tensors = {"hidden_states": hidden_states, "residual": residual}
if aux_hidden_states:
proxy_tensors["dspark_hidden_states"] = torch.cat(
aux_hidden_states, dim=-1
)
return PPProxyTensors(proxy_tensors)
if hidden_states.shape[0] != 0:
if attn_res is not None:
# ---- Final aggregation (output side, folds delayed add) ----
if sp_sharded:
output_rows = _sp_local_rows(hidden_states)
fused_output = attn_res.forward_sp_all_gather(
hidden_states,
residual,
self.output_attn_res_proj,
self.output_attn_res_norm,
self.norm,
rows=output_rows,
)
if fused_output is not None:
hidden_states, _ = fused_output
else:
hidden_states, _ = attn_res.forward(
hidden_states,
residual,
self.output_attn_res_proj,
self.output_attn_res_norm,
self.norm,
rows=output_rows,
)
hidden_states = _sp_all_gather_rows(hidden_states)
else:
hidden_states, _ = attn_res.forward(
hidden_states,
residual,
self.output_attn_res_proj,
self.output_attn_res_norm,
self.norm,
)
else:
if residual is None:
hidden_states = self.norm(hidden_states)
else:
hidden_states, _ = self.norm(hidden_states, residual)
if self.dspark_layers_to_capture is not None:
return hidden_states, aux_hidden_states
return hidden_states
def _dspark_capture_stream(
self,
layer_idx: int,
hidden_states: torch.Tensor,
residual: Optional[torch.Tensor],
attn_res: Optional[AttnResidual],
) -> torch.Tensor:
"""Stream value after `layer_idx`: the pre-norm mixture its next
consumer would compute (next layer's attention side; output side
for the last layer)."""
if attn_res is None:
return hidden_states if residual is None else hidden_states + residual
if residual is not None:
# Materialize a delayed MLP add (mirrors the PP-wire fold).
hidden_states = residual + hidden_states
if layer_idx + 1 < self.end_layer:
next_layer = self.layers[layer_idx + 1]
score_proj = next_layer.self_attention_res_proj
score_norm = next_layer.self_attention_res_norm
nvb = next_layer.prev_valid_blocks
else:
# Last layer: the model's own output-side aggregation weights.
score_proj = self.output_attn_res_proj
score_norm = self.output_attn_res_norm
nvb = _cdiv(self.end_layer, self.config.attn_res_block_size)
return aggregate_stream(
hidden_states, attn_res.block_residual, nvb, score_proj, score_norm
)
class KimiK3LinearForCausalLM(nn.Module):
"""Text-only K3 causal LM."""
# ModelSlim describes quantization with the original checkpoint module
# names. Register the runtime fused QKVG module so it can resolve the
# q_proj scheme while the weight loader packs q/k/v/g into its shards.
packed_modules_mapping = {
"fused_qkvg_proj": ["q_proj", "k_proj", "v_proj", "g_proj"],
}
def __init__(
self,
config: KimiLinearConfig,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
) -> None:
super().__init__()
self.config = config
self.quant_config = quant_config
if quant_config is not None:
if isinstance(quant_config, ModelSlimConfig):
model_mapping = {
**quant_config.packed_modules_mapping.get("model", {}),
**self.packed_modules_mapping,
}
quant_config.update_packed_modules_mapping({"model": model_mapping})
else:
quant_config.update_packed_modules_mapping(self.packed_modules_mapping)
self.model = KimiK3LinearModel(
config, quant_config, prefix=maybe_prefix(prefix, "model")
)
self.pp_group = get_parallel().pp_group
if self.pp_group.is_last_rank:
self.lm_head = ParallelLMHead(
config.vocab_size,
config.hidden_size,
quant_config=quant_config,
prefix=maybe_prefix(prefix, "lm_head"),
use_attn_tp_group=get_parallel().enable_dp_lm_head,
)
else:
self.lm_head = PPMissingLayer()
logit_scale = getattr(config, "logit_scale", 1.0)
self.logits_processor = LogitsProcessor(config=config, logit_scale=logit_scale)
self.capture_aux_hidden_states = False
def get_input_embeddings(self):
return self.model.embed_tokens
def get_pp_proxy_dspark_hidden_size(self) -> int:
layers = self.model.dspark_layers_to_capture or []
return self.config.hidden_size * sum(
layer < self.model.start_layer - 1 for layer in layers
)
def set_dspark_layers_to_capture(self, layer_ids: list[int]) -> None:
if layer_ids is None:
raise ValueError(
"DSPARK requires explicit layer_ids for aux hidden capture."
)
self.capture_aux_hidden_states = True
self.model.dspark_layers_to_capture = list(layer_ids)
@torch.no_grad()
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
forward_batch: ForwardBatch,
input_embeds: Optional[torch.Tensor] = None,
inputs_embeds: Optional[torch.Tensor] = None,
pp_proxy_tensors: Optional[PPProxyTensors] = None,
) -> torch.Tensor:
embeds = input_embeds if input_embeds is not None else inputs_embeds
hidden_states = self.model(
input_ids, positions, forward_batch, embeds, pp_proxy_tensors
)
if self.pp_group.is_last_rank:
aux_hidden_states = None
if self.capture_aux_hidden_states:
hidden_states, aux_hidden_states = hidden_states
return self.logits_processor(
input_ids,
hidden_states,
self.lm_head,
forward_batch,
aux_hidden_states,
)
return hidden_states
def prepare_context_parallel_metadata_for_dcp(
self,
seq_lens: torch.Tensor,
extend_prefix_lens: torch.Tensor,
extend_prefix_lens_cpu: torch.Tensor,
extend_seq_lens: torch.Tensor,
req_pool_indices: torch.Tensor,
req_to_token: torch.Tensor,
seq_lens_sum: int,
kv_buffer_shape: torch.Size,
kv_cache_dtype,
kv_cache_device,
create_chunked_prefix_cache_kv_indices_fn,
):
return prepare_decode_context_parallel_metadata(
seq_lens=seq_lens,
extend_prefix_lens=extend_prefix_lens,
extend_prefix_lens_cpu=extend_prefix_lens_cpu,
extend_seq_lens=extend_seq_lens,
req_pool_indices=req_pool_indices,
req_to_token=req_to_token,
seq_lens_sum=seq_lens_sum,
kv_buffer_shape=kv_buffer_shape,
kv_cache_dtype=kv_cache_dtype,
kv_cache_device=kv_cache_device,
create_chunked_prefix_cache_kv_indices_fn=create_chunked_prefix_cache_kv_indices_fn,
)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
use_full_rank_gate = bool(
(self.config.linear_attn_config or {}).get("use_full_rank_gate", False)
)
if use_full_rank_gate:
# Fused layout (K3): [q, k, v, g] column-parallel; b / f_a / f_b
# are standalone modules loaded by name.
fused_qkvbfg_mapping = [
(".fused_qkvg_proj", ".q_proj", 0),
(".fused_qkvg_proj", ".k_proj", 1),
(".fused_qkvg_proj", ".v_proj", 2),
(".fused_qkvg_proj", ".g_proj", 3),
]
else:
# Fused layout (low-rank gate): [q, k, v, b] + [f_a, g_a]
fused_qkvbfg_mapping = [
(".fused_qkvbfg_a_proj", ".q_proj", 0),
(".fused_qkvbfg_a_proj", ".k_proj", 1),
(".fused_qkvbfg_a_proj", ".v_proj", 2),
(".fused_qkvbfg_a_proj", ".b_proj", 3),
(".fused_qkvbfg_a_proj", ".f_a_proj", 4),
(".fused_qkvbfg_a_proj", ".g_a_proj", 5),
(".fused_fg_b_proj", ".f_b_proj", 0),
(".fused_fg_b_proj", ".g_b_proj", 1),
]
stacked_params_mapping = [
(".gate_up_proj", ".gate_proj", 0),
(".gate_up_proj", ".up_proj", 1),
*fused_qkvbfg_mapping,
# Unfused QKV path
(".qkv_proj", ".q_proj", "q"),
(".qkv_proj", ".k_proj", "k"),
(".qkv_proj", ".v_proj", "v"),
# Conv1d fusion
(".qkv_conv1d", ".q_conv1d", 0),
(".qkv_conv1d", ".k_conv1d", 1),
(".qkv_conv1d", ".v_conv1d", 2),
]
if self.config.is_moe:
expert_params_mapping = FusedMoE.make_expert_params_mapping(
ckpt_gate_proj_name="w1",
ckpt_down_proj_name="w2",
ckpt_up_proj_name="w3",
num_experts=self.config.num_experts,
)
else:
expert_params_mapping = []
params_dict = dict(self.named_parameters())
loaded_params: set[str] = set()
# Keyed by the `experts.<id>.<proj>.` fragment (see _EXPERT_WEIGHT_NAME).
expert_params_lookup = {entry[1]: entry for entry in expert_params_mapping}
assert all(
_EXPERT_WEIGHT_NAME.fullmatch(key) for key in expert_params_lookup
), "ckpt expert names diverged from _EXPERT_WEIGHT_NAME; no expert would load"
num_hidden_layers = self.config.num_hidden_layers
for args in weights:
name, loaded_weight = args[:2]
kwargs = args[2] if len(args) > 2 else {}
if name.endswith(".weight_scale") and loaded_weight.ndim == 4:
loaded_weight = loaded_weight[:, 0, :, 0]
layer_id = get_layer_id(name)
if layer_id is not None and (
layer_id < self.model.start_layer or layer_id >= self.model.end_layer
):
continue
# Skip weights of layers outside a truncated config (e.g.
# num_hidden_layers override for fast testing); the checkpoint may
# carry more layers than the instantiated model.
if ".layers." in name:
_lid = name.split(".layers.")[1].split(".")[0]
if _lid.isdigit() and int(_lid) >= num_hidden_layers:
continue
# compressed-tensors MXFP4 stores as weight_packed; Mxfp4MoEMethod uses weight
# (NPU keeps weight_packed for NPUCompressedTensorsW4A8mxfp4MoE).
if "weight_packed" in name and not _is_npu:
name = name.replace("weight_packed", "weight")
# MLA: fuse q_a_proj + kv_a_proj_with_mqa → fused_qkv_a_proj_with_mqa
if ".q_a_proj." in name or ".kv_a_proj_with_mqa." in name:
is_q_a = ".q_a_proj." in name
fused_name = name.replace(".q_a_proj.", ".fused_qkv_a_proj_with_mqa.")
fused_name = fused_name.replace(
".kv_a_proj_with_mqa.", ".fused_qkv_a_proj_with_mqa."
)
fused_name = _maybe_map_fp8_pb_scale_name(fused_name, params_dict)
if fused_name in params_dict:
param = params_dict[fused_name]
if fused_name.endswith(".weight_scale_inv"):
offset = 0 if is_q_a else _cdiv(self.config.q_lora_rank, 128)
param.data[offset : offset + loaded_weight.shape[0]].copy_(
loaded_weight
)
elif is_q_a:
param.data[: loaded_weight.shape[0]].copy_(loaded_weight)
else:
q_lora_rank = self.config.q_lora_rank or 0
param.data[q_lora_rank:].copy_(loaded_weight)
loaded_params.add(fused_name)
continue
if "rotary_emb.inv_freq" in name:
continue
if "rotary_emb.cos_cached" in name or "rotary_emb.sin_cached" in name:
continue
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name:
continue
if ("mlp.experts." in name) and name not in params_dict:
continue
# Fused projections only apply to KDA layers
if param_name in {
".fused_qkvbfg_a_proj",
".fused_fg_b_proj",
".fused_qkvg_proj",
}:
layer_id = int(name.split(".")[2])
if not self.config.is_kda_layer(layer_id):
continue
layer = self.model.layers[layer_id].self_attn
# Full-rank K3 always instantiates fused_qkvg_proj, including
# ModelSlim-quantized models. The low-rank fused modules are
# still conditional on do_fuse_qkvbfg.
if param_name == ".fused_qkvg_proj":
if not getattr(layer, "use_full_rank_gate", False):
continue
elif not getattr(layer, "do_fuse_qkvbfg", False):
continue
if weight_name in {".q_proj", ".k_proj", ".v_proj"}:
layer_id = int(name.split(".")[2])
if not self.config.is_kda_layer(layer_id):
continue
name = name.replace(weight_name, param_name)
if name.endswith(".bias") and name not in params_dict:
continue
name = _maybe_map_fp8_pb_scale_name(name, params_dict)
param = params_dict[name]
weight_loader = param.weight_loader
weight_loader(param, loaded_weight, shard_id)
break
else:
expert_match = _EXPERT_WEIGHT_NAME.search(name)
expert_entry = (
expert_params_lookup.get(expert_match.group(0))
if expert_match
else None
)
if expert_entry is not None:
param_name, weight_name, expert_id, shard_id = expert_entry
name = name.replace(weight_name, param_name)
# Skip experts of layers outside a truncated config (e.g.
# num_hidden_layers override), mirroring the non-expert
# `name not in params_dict` guard below.
if name in params_dict:
param = params_dict[name]
weight_loader = param.weight_loader
weight_loader(
param,
loaded_weight,
name,
expert_id=expert_id,
shard_id=shard_id,
)
else:
if (
name.endswith(".bias")
and name not in params_dict
and not self.config.is_linear_attn
):
continue
name = maybe_remap_kv_scale_name(name, params_dict)
if name is None:
continue
name = _maybe_map_fp8_pb_scale_name(name, params_dict)
if name not in params_dict:
continue
param = params_dict[name]
if name.endswith(".b_proj.weight_scale_inv"):
# All TP ranks share K3's single beta output-scale block.
param.data.copy_(loaded_weight)
else:
weight_loader = getattr(
param, "weight_loader", default_weight_loader
)
weight_loader(param, loaded_weight, **kwargs)
loaded_params.add(name)
self.post_load_weights()
return loaded_params
def post_load_weights(self):
# Also invoked by loader post-load hooks (DummyModelLoader,
# ShardedStateLoader, remote-instance flows -- none of which call
# load_weights), so e.g. dummy-weight benchmarks get the fused buffers.
# Post-load: absorb kv_b_proj into w_kc and w_vc for MLA layers
for layer_id in self.config.full_attention_layer_ids:
if layer_id >= len(self.model.layers):
continue # truncated config (e.g. num_hidden_layers override)
layer = self.model.layers[layer_id]
if isinstance(layer, PPMissingLayer):
continue
self_attn = layer.self_attn
if getattr(self_attn, "_kimi_split_gguf_kv_b", False):
if int(self_attn.k_b_qweight_type.weight_type) != 2:
raise ValueError("Kimi-K3 MLA K projection must remain GGUF Q4_0")
if int(self_attn.v_b_qweight_type.weight_type) != 10:
raise ValueError("Kimi-K3 MLA V projection must remain GGUF Q2_K")
self_attn.use_deep_gemm_bmm = False
continue
kv_b_weight = _get_k3_dense_weight(self_attn.kv_b_proj)
w_kc, w_vc = kv_b_weight.unflatten(
0, (-1, self_attn.qk_nope_head_dim + self_attn.v_head_dim)
).split([self_attn.qk_nope_head_dim, self_attn.v_head_dim], dim=1)
self_attn.w_kc = w_kc.transpose(1, 2).contiguous().transpose(1, 2)
self_attn.w_vc = w_vc.contiguous().transpose(1, 2)
if hasattr(self_attn.kv_b_proj, "weight_scale"):
self_attn.w_scale = self_attn.kv_b_proj.weight_scale
# Post-load: precompute the attn-res combined score weights BEFORE
# cuda graph capture (a lazy first call inside get_cw would bake the
# multiply into every replay). Warm both dtypes (bf16 fast kernel,
# fp32 triton fallback).
def _warm_cw(proj, norm):
get_cw(proj, norm, dtype=torch.bfloat16)
get_cw(proj, norm)
for layer in self.model.layers:
if isinstance(layer, PPMissingLayer):
continue
if layer.use_attn_residuals:
_warm_cw(layer.self_attention_res_proj, layer.self_attention_res_norm)
_warm_cw(layer.mlp_res_proj, layer.mlp_res_norm)
if hasattr(self.model, "output_attn_res_proj"):
_warm_cw(self.model.output_attn_res_proj, self.model.output_attn_res_norm)
# Post-load: merge the horizontally-fused decode weights (views of the
# merged buffers, ~0 extra memory); must run before cuda graph capture.
for layer in self.model.layers:
if isinstance(layer, PPMissingLayer):
continue
if isinstance(layer.mlp, KimiK3MoE):
layer.mlp._merge_front_weights()
# Convert the correction bias to fp32 once so the per-call
# .to(float32) in topk is a no-op, not one upcast kernel per
# MoE layer per step.
bias = layer.mlp.gate.e_score_correction_bias
if bias.dtype != torch.float32:
bias.data = bias.data.to(torch.float32)
if isinstance(layer.self_attn, KimiK3DeltaAttention):
layer.self_attn._merge_bfa_weights()
layer.self_attn._prepare_fused_decode()
for layer in self.model.layers:
if isinstance(layer, PPMissingLayer) or not isinstance(
layer.self_attn, KimiK3DeltaAttention
):
continue
if _is_npu:
continue
from sglang.kernels.ops.attention.fla.kda import (
precompile_k3_recompute_w_u_kernel,
)
o_proj_weight = getattr(layer.self_attn.o_proj, "weight", None)
if o_proj_weight is None:
o_proj_weight = layer.self_attn.o_proj.qweight
if precompile_k3_recompute_w_u_kernel(
num_heads=layer.self_attn.local_num_heads,
dtype=getattr(layer.self_attn.o_proj, "params_dtype", None)
or o_proj_weight.dtype,
device=layer.self_attn.dt_bias.device,
):
rank0_log("Precompiled the Kimi-K3 KDA prefill kernel.")
break
class KimiK3ForConditionalGeneration(nn.Module):
"""K3 multimodal wrapper: MoonViT3d tower + KimiK3LinearForCausalLM."""
supports_cuda_vmm_feature_transport = True
# Fused runtime module -> checkpoint shard names, so quant configs can
# match fused prefixes against per-shard exclude_modules
packed_modules_mapping = {
"gate_up_proj": ["gate_proj", "up_proj"],
"qkv_proj": ["q_proj", "k_proj", "v_proj"],
"qkv_conv1d": ["q_conv1d", "k_conv1d", "v_conv1d"],
"fused_qkvg_proj": ["q_proj", "k_proj", "v_proj", "g_proj"],
"fused_qkvbfg_a_proj": [
"q_proj",
"k_proj",
"v_proj",
"b_proj",
"f_a_proj",
"g_a_proj",
],
"fused_fg_b_proj": ["f_b_proj", "g_b_proj"],
}
encoder_media_processor_config = EncoderMediaProcessorConfig(
image_decode_mode="nvjpeg_fancy",
preserve_media_metadata=True,
)
# Raw HF checkpoint prefixes, before hf_to_sglang_mapper is applied.
encoder_only_safetensors_weight_prefixes = (
"vision_tower.",
"mm_projector.",
)
hf_to_sglang_mapper = WeightsMapper(
orig_to_new_prefix={
"language_model.layers.": "language_model.model.layers.",
},
orig_to_new_substr={
"block_sparse_moe": "mlp",
},
)
def __init__(
self,
config: KimiK3Config,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
**kwargs,
) -> None:
super().__init__()
self.config = config
self.quant_config = quant_config
# The dedicated K3 tower runs replicated (per-rank full weights);
# shard work across ranks image-wise via the DP runner.
self.use_data_parallel = True
self.vision_tower = KimiK3VisionTower(config.vision_config)
self.mm_projector = KimiK3MultiModalProjector(config.vision_config)
self.language_model = None
if not config.encoder_only:
quant_description = getattr(quant_config, "quant_description", {})
uses_wrapper_quant_prefix = any(
isinstance(name, str) and name.startswith("language_model.")
for name in quant_description
)
language_prefix = (
maybe_prefix(prefix, "language_model")
if uses_wrapper_quant_prefix
else prefix
)
self.language_model = KimiK3LinearForCausalLM(
config.text_config,
quant_config,
prefix=language_prefix,
)
@property
def model(self):
return self.language_model
def __setattr__(self, name, value):
if name == "model":
return
super().__setattr__(name, value)
def post_load_weights(self):
# Delegate so DummyModelLoader's post-load hook reaches the LM tower.
if self.language_model is not None:
self.language_model.post_load_weights()
def precompile_kernels_after_loading(self) -> None:
if self.config.language_only:
return
if self.vision_tower.precompile_fused_rope():
logger.info("Precompiled dynamic-token fused K3 vision RoPE kernel")
if self.vision_tower.precompile_attention_backend():
logger.info("Precompiled Kimi-K3 vision FA4 kernel")
def get_input_embeddings(self):
if self.language_model is None:
raise AttributeError(
"get_input_embeddings() is not available in encoder-only mode"
)
return self.language_model.model.embed_tokens
@property
def lm_head(self):
if self.language_model is None:
raise AttributeError("lm_head is not available in encoder-only mode")
return self.language_model.lm_head
def get_pp_proxy_dspark_hidden_size(self) -> int:
if self.language_model is None:
return 0
return self.language_model.get_pp_proxy_dspark_hidden_size()
def set_dspark_layers_to_capture(self, layer_ids: list[int]) -> None:
if self.language_model is None:
raise AttributeError(
"DSPARK layer capture is not available in encoder-only mode"
)
self.language_model.set_dspark_layers_to_capture(layer_ids)
def preprocess_mm_for_encoder(
self,
mm_data,
modality,
config,
*,
image_processor=None,
use_gpu_preprocessing=False,
):
"""Prepare per-image raw inputs for owner-side EPD preprocessing."""
if modality != Modality.IMAGE:
raise ValueError("Kimi-K3 encoder mode supports image input only")
if image_processor is None:
raise ValueError("Kimi-K3 encoder preprocessing needs an image processor")
from sglang.srt.multimodal.kimi_k3_image_processing import (
prepare_kimi_k3_encoder_inputs,
)
self._encoder_image_processor = image_processor
return prepare_kimi_k3_encoder_inputs(
mm_data,
image_processor,
use_gpu_preprocessing=use_gpu_preprocessing,
)
def get_image_feature(self, items: List[MultimodalDataItem]) -> torch.Tensor:
device = self.vision_tower.device
target_dtype = self.vision_tower.patch_embed.proj.weight.dtype
image_grid_thws = []
for item in items:
grid_thw = item.model_specific_data.get("image_grid_thw")
if grid_thw is None:
grid_thw = item.model_specific_data["grid_thws"]
if grid_thw.shape[0] != 1:
# One item must carry exactly one logical image so the DP
# owner assignment and the bounded CUDA-IPC lease accounting
# stay per-item; aggregated encoder inputs are split upstream
# (EPD encode server) before reaching this point.
raise ValueError(
"Kimi-K3 expects one vision grid per MultimodalDataItem; "
"split aggregated encoder inputs before get_image_feature()"
)
image_grid_thws.append(grid_thw)
grid_thws_host = torch.concat(image_grid_thws, dim=0).cpu()
grid_thw_list = grid_thws_host.tolist()
def materialize_item_features(image_indices: List[int]) -> torch.Tensor:
"""Materialize only the images assigned to this vision-DP rank."""
from sglang.srt.multimodal.encoder_preprocessing import (
LOCAL_PREPROCESSED_KEY,
)
# Match the configured TP consumer count captured when the
# tokenizer creates MmItemMemoryPool. A live attention subgroup
# size could leave acknowledgements missing and strand the lease.
ipc_consumer_count = max(get_parallel().tp_size, 1)
device_index = device.index
if device.type == "cuda" and device_index is None:
device_index = torch.cuda.current_device()
selected_items = []
for image_index in image_indices:
item = items[image_index]
if device.type == "cuda":
item.reconstruct(
device_index, ipc_consumer_count=ipc_consumer_count
)
selected_items.append(item)
locally_preprocessed = [
item.model_specific_data.get(LOCAL_PREPROCESSED_KEY, False)
for item in selected_items
]
if any(locally_preprocessed):
if not all(locally_preprocessed):
raise ValueError(
"Kimi-K3 cannot mix local preprocessed and deferred images"
)
return materialize_multimodal_features(
[item.feature for item in selected_items],
device=device,
dtype=target_dtype,
)
deferred = [
item.model_specific_data.get(DEFERRED_PREPROCESSING_KEY)
for item in selected_items
]
if any(config is not None for config in deferred):
materialized = [None] * len(selected_items)
deferred_by_backend = {}
for index, (item, config) in enumerate(zip(selected_items, deferred)):
if config is None:
if not isinstance(item.feature, torch.Tensor):
raise TypeError(
"Kimi-K3 image feature must be a torch.Tensor, "
f"got {type(item.feature)}"
)
materialized[index] = item.feature
else:
deferred_by_backend.setdefault(config.backend, []).append(index)
for backend, indices in deferred_by_backend.items():
group_items = [selected_items[index] for index in indices]
group_configs = [deferred[index] for index in indices]
# Map backend-group positions through the rank-local shard to global grid rows.
global_indices = [image_indices[index] for index in indices]
first_config = group_configs[0]
if backend == "gpu":
from sglang.srt.multimodal.processors.kimi_k25 import (
_gpu_preprocess_images,
)
image_scale, image_bias = normalization_tensors(
first_config.image_mean,
first_config.image_std,
device,
)
pixel_values, produced_grids = _gpu_preprocess_images(
[item.feature for item in group_items],
[config.resize_config for config in group_configs],
image_scale,
image_bias,
self.vision_tower.patch_size,
to_chw=lambda image: to_chw_uint8(image, device=device),
post_resize=lambda x: fill_transparent_bg(
x, first_config.transparent_bg_config
),
)
expected_grids = grid_thws_host[global_indices]
if not torch.equal(produced_grids.cpu(), expected_grids):
raise ValueError(
"Kimi-K3 deferred GPU preprocessing produced wrong grids"
)
elif backend == "cpu":
from sglang.srt.multimodal.kimi_k3_image_processing import (
materialize_kimi_k3_cpu_features,
)
pixel_values = materialize_kimi_k3_cpu_features(
group_items, self._encoder_image_processor
)
else:
raise ValueError(
f"Unsupported Kimi-K3 deferred preprocessing backend: {backend}"
)
patch_counts = [
int(grid_thws_host[index].prod().item())
for index in global_indices
]
if sum(patch_counts) != pixel_values.shape[0]:
raise ValueError(
"Kimi-K3 deferred feature length does not match image grids"
)
for index, feature in zip(
indices, pixel_values.split(patch_counts), strict=True
):
materialized[index] = feature
return materialize_multimodal_features(
materialized,
device=device,
dtype=target_dtype,
)
features = []
for item in selected_items:
if not isinstance(item.feature, torch.Tensor):
raise TypeError(
"Kimi-K3 image feature must be a torch.Tensor, "
f"got {type(item.feature)}"
)
features.append(item.feature)
return materialize_multimodal_features(
features, device=device, dtype=target_dtype
)
if self.use_data_parallel:
from sglang.srt.multimodal.mm_utils import run_dp_sharded_mrope_vision_model
image_embeds = run_dp_sharded_mrope_vision_model(
self.vision_tower,
None,
grid_thw_list,
rope_type="rope_2d",
# K3's tower pools the temporal dimension away: a t>1 grid
# still yields h*w/merge_area output embeddings, so the DP
# gather length must ignore t.
pool_temporal_dimension=True,
pass_grid_thw_list=True,
load_local_pixel_values=materialize_item_features,
pixel_values_device=device,
pixel_values_dtype=target_dtype,
)
return self.mm_projector(image_embeds)
pixel_values = materialize_item_features(list(range(len(items))))
image_embeds = self.vision_tower(pixel_values, grid_thws_host.to(device))
return self.mm_projector(image_embeds)
def pad_input_ids(self, input_ids: List[int], mm_inputs: MultimodalInputs):
pattern = MultiModalityDataPaddingPatternMultimodalTokens()
return pattern.pad_input_tokens(input_ids, mm_inputs)
@property
def start_layer(self) -> int:
if self.language_model is None:
return 0
return self.language_model.model.start_layer
@property
def end_layer(self) -> int:
if self.language_model is None:
return self.config.text_config.num_hidden_layers
return self.language_model.model.end_layer
def prepare_context_parallel_metadata_for_dcp(
self,
seq_lens: torch.Tensor,
extend_prefix_lens: torch.Tensor,
extend_prefix_lens_cpu: torch.Tensor,
extend_seq_lens: torch.Tensor,
req_pool_indices: torch.Tensor,
req_to_token: torch.Tensor,
seq_lens_sum: int,
kv_buffer_shape: torch.Size,
kv_cache_dtype,
kv_cache_device,
create_chunked_prefix_cache_kv_indices_fn,
):
return self.language_model.prepare_context_parallel_metadata_for_dcp(
seq_lens=seq_lens,
extend_prefix_lens=extend_prefix_lens,
extend_prefix_lens_cpu=extend_prefix_lens_cpu,
extend_seq_lens=extend_seq_lens,
req_pool_indices=req_pool_indices,
req_to_token=req_to_token,
seq_lens_sum=seq_lens_sum,
kv_buffer_shape=kv_buffer_shape,
kv_cache_dtype=kv_cache_dtype,
kv_cache_device=kv_cache_device,
create_chunked_prefix_cache_kv_indices_fn=create_chunked_prefix_cache_kv_indices_fn,
)
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
forward_batch: ForwardBatch,
get_embedding: bool = False,
pp_proxy_tensors: Optional[PPProxyTensors] = None,
):
hidden_states = general_mm_embed_routine(
input_ids=input_ids,
forward_batch=forward_batch,
language_model=self.language_model,
data_embedding_funcs={
Modality.IMAGE: self.get_image_feature,
},
positions=positions,
pp_proxy_tensors=pp_proxy_tensors,
)
return hidden_states
def load_weights(self, weights: Iterable[Tuple[str, torch.Tensor]]):
mapper = getattr(self, "hf_to_sglang_mapper", None)
if mapper is not None:
weights = mapper.apply(weights)
vision_params = (
None
if self.config.language_only
else dict(self.named_parameters(remove_duplicate=False))
)
def stream_language_weights():
for name, loaded_weight in weights:
if "vision_tower" in name or "mm_projector" in name:
if vision_params is None:
continue
if name not in vision_params:
logger.warning("Unmapped vision weight: %s", name)
continue
param = vision_params[name]
weight_loader = getattr(
param, "weight_loader", default_weight_loader
)
weight_loader(param, loaded_weight)
continue
yield name.replace("language_model.", ""), loaded_weight
if self.language_model is not None:
self.language_model.load_weights(stream_language_weights())
else:
# The vision weights are loaded as a side effect of advancing this
# streaming iterator. Encoder-only mode must therefore drain it
# even though it discards every language-model tensor.
for _ in stream_language_weights():
pass
EntryClass = [KimiK3ForConditionalGeneration, KimiK3LinearForCausalLM]