Skip to content

Commit 9d24120

Browse files
committed
feat(kimi-k3): latent MoE block on moe.RoutedMoE
Implement the Kimi-K3 latent MoE block: the router reads the full hidden state, the routed experts run through moe.RoutedMoE on a down-projected latent (sorted/grouped dispatch via megablox / tokamax GMM / ragged_dot, expert parallelism), followed by an RMSNorm and an up-projection; plus shared experts and the dense layer-0 MLP. moe.RoutedMoE gains (backward compatible): - moe_router_input_dim: the router may read a wider input than the experts (passed as gate_inputs); defaults to the expert input dim. - mlp_activations[1] is applied to the up (wi_1) branch (every existing config uses "linear"); Kimi-K3 uses SiTU on both branches. Fused kernels (te_moe_block / vllm_rpa) reject a non-linear up activation. - DeepSeek-V3-style routing prefixes centralized and extended to kimi-k3 (router bias kept as an nnx.Param); deepseek_scale_weights also applies to the kimi_linear decoder block.
1 parent a5e8c86 commit 9d24120

10 files changed

Lines changed: 877 additions & 17 deletions

File tree

‎src/maxtext/checkpoint_conversion/utils/param_mapping.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4856,8 +4856,9 @@ def KIMI_K3_MAXTEXT_TO_HF_PARAM_MAPPING(config, maxtext_config, scan_layers=Fals
48564856
mapping.update(
48574857
{
48584858
f"{prefix}-mlp-routed_experts-gate-kernel": f"language_model.model.layers.{i}.block_sparse_moe.gate.weight",
4859-
# moe.RoutedMoE's router bias param is `gate/bias`.
4860-
f"{prefix}-mlp-routed_experts-gate-bias": (
4859+
# The router bias is `gate/bias`; for kimi-k3 it is a `moe.MoEBiasVar`, so (as for
4860+
# DeepSeek) it lives in the `MoEBiasVar` collection.
4861+
f"{prefix.replace('params-', 'MoEBiasVar-')}-mlp-routed_experts-gate-bias": (
48614862
f"language_model.model.layers.{i}.block_sparse_moe.gate.e_score_correction_bias"
48624863
),
48634864
f"{prefix}-mlp-routed_expert_down_proj-kernel": (

‎src/maxtext/configs/base.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ warmup_programs_in_init: false # before init_stop/run_start, run each precompile
308308
warmup_skip_train_dropless: false # requires warmup_programs_in_init=true; do not warm the train_dropless program (its warmup holds a second copy of the train state in HBM and can OOM). Use only when that warmup OOMs; the first dropless replay then pays the program's first load.
309309
log_step_diagnostics: false # append grad/param norms, max |grad|, routed-bias checksum, nonzero bias updates and the overflow flag to every training step log line
310310
moe_expert_input_dim: -1 # feature dimension of the tokens entering the MoE expert blocks.
311+
moe_router_input_dim: -1 # feature dimension of the router (gate) input, passed as `gate_inputs`; defaults to the expert input dim.
311312
base_moe_mlp_dim: -1 # intermediate dimension at MoE layer. For a fully MoE model, base_mlp_dim must be equal to base_moe_mlp_dim.
312313
load_balance_loss_weight: 0.0 # weight for the load balance loss
313314
moe_use_megatron_seq_aux_loss: false # when true, sigmoid and sqrtsoftplus routers use Megatron-LM's sequence-wise aux loss summed over MoE layers; false (default) uses the Switch-style loss averaged over MoE layers.

‎src/maxtext/configs/models/kimi-k3.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ attn_res_block_size: 12
4646

4747
# MLP and MoE Specifications
4848
first_num_dense_layers: 1
49+
# SiTU on both branches: situ_gate(wi_0 x) * situ_linear(wi_1 x).
4950
mlp_activations: ["situ_gate", "situ_linear"]
5051
# `base_mlp_dim` is the layer-0 dense MLP width (HF `intermediate_size`); the MoE
5152
# shared expert uses `shared_intermediate_size` = moe_intermediate_size * num_shared_experts.
@@ -63,6 +64,11 @@ num_shared_experts: 2
6364
routed_scaling_factor: 1.0
6465
routed_score_func: "sigmoid"
6566
routed_bias: true
67+
# Routed experts run through moe.RoutedMoE on the 3584-wide latent, while the router reads the full
68+
# 7168-wide hidden state (passed as `gate_inputs`); router math is fp32 as in the reference.
69+
moe_expert_input_dim: 3584
70+
moe_router_input_dim: 7168
71+
float32_gate_logits: true
6672

6773
# Sequence Length & RoPE
6874
# Kimi-K3 uses NoPE for its MLA layers (HF `mla_use_nope: true`) and ships no

‎src/maxtext/configs/types.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,6 +1273,14 @@ class MoEGeneral(BaseModel):
12731273
-1,
12741274
description="Dimension of tokens entering the MoE layer. If < 0, defaults to emb_dim.",
12751275
)
1276+
moe_router_input_dim: int = Field(
1277+
-1,
1278+
description=(
1279+
"Dimension of the router (gate) input when it differs from the expert input, e.g. a latent MoE that"
1280+
" routes on the full hidden state but runs experts on a down-projected latent (pass the router input"
1281+
" as `gate_inputs`). If < 0, defaults to the expert input dim."
1282+
),
1283+
)
12761284
base_moe_mlp_dim: int = Field(-1, description="Intermediate dimension at MoE layer.")
12771285
padded_base_moe_mlp_dim: Optional[int] = Field(
12781286
None,

‎src/maxtext/layers/latent_moe.py‎

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Latent MoE and SiTU-activated MLP layers for Kimi-K3."""
16+
17+
from typing import Optional
18+
19+
from flax import nnx
20+
import jax.numpy as jnp
21+
from jax.sharding import Mesh, NamedSharding
22+
23+
from maxtext.common.common_types import Array, Config, DType, ShardMode
24+
from maxtext.layers import moe
25+
from maxtext.layers.initializers import NdInitializer, nd_dense_init
26+
from maxtext.layers.linears import DenseGeneral, situ_gate, situ_linear
27+
from maxtext.layers.normalizations import RMSNorm
28+
from maxtext.layers.quantizations import AqtQuantization as Quant
29+
30+
31+
class KimiDenseMLP(nnx.Module):
32+
"""Dense MLP with SiTU activations (Layer 0 and shared experts)."""
33+
34+
def __init__(
35+
self,
36+
in_features: int,
37+
intermediate_dim: int,
38+
out_features: Optional[int] = None,
39+
dtype: DType = jnp.float32,
40+
weight_dtype: DType = jnp.float32,
41+
kernel_init: NdInitializer = nd_dense_init(1.0, "fan_in", "truncated_normal"),
42+
quant: Optional[Quant] = None,
43+
shard_mode: ShardMode = ShardMode.AUTO,
44+
matmul_precision: str = "default",
45+
mesh: Optional[Mesh] = None,
46+
*,
47+
rngs: nnx.Rngs,
48+
):
49+
out_features = out_features or in_features
50+
self.in_features = in_features
51+
self.intermediate_dim = intermediate_dim
52+
self.out_features = out_features
53+
self.dtype = dtype
54+
self.weight_dtype = weight_dtype
55+
56+
self.wi_0 = DenseGeneral(
57+
in_features_shape=in_features,
58+
out_features_shape=intermediate_dim,
59+
dtype=dtype,
60+
weight_dtype=weight_dtype,
61+
kernel_init=kernel_init,
62+
kernel_axes=("embed", "mlp"),
63+
quant=quant,
64+
shard_mode=shard_mode,
65+
matmul_precision=matmul_precision,
66+
mesh=mesh,
67+
rngs=rngs,
68+
)
69+
self.wi_1 = DenseGeneral(
70+
in_features_shape=in_features,
71+
out_features_shape=intermediate_dim,
72+
dtype=dtype,
73+
weight_dtype=weight_dtype,
74+
kernel_init=kernel_init,
75+
kernel_axes=("embed", "mlp"),
76+
quant=quant,
77+
shard_mode=shard_mode,
78+
matmul_precision=matmul_precision,
79+
mesh=mesh,
80+
rngs=rngs,
81+
)
82+
self.wo = DenseGeneral(
83+
in_features_shape=intermediate_dim,
84+
out_features_shape=out_features,
85+
dtype=dtype,
86+
weight_dtype=weight_dtype,
87+
kernel_init=kernel_init,
88+
kernel_axes=("mlp", "embed"),
89+
quant=quant,
90+
shard_mode=shard_mode,
91+
matmul_precision=matmul_precision,
92+
mesh=mesh,
93+
rngs=rngs,
94+
)
95+
96+
def __call__(
97+
self,
98+
x: Array,
99+
out_sharding: Optional[NamedSharding] = None,
100+
) -> Array:
101+
"""Forward pass of KimiDenseMLP."""
102+
gate = self.wi_0(x)
103+
up = self.wi_1(x)
104+
act = situ_gate(gate) * situ_linear(up)
105+
return self.wo(act.astype(self.dtype), out_sharding=out_sharding)
106+
107+
108+
# Logical sharding axes (same names as `moe.RoutedMoE` / the DeepSeek gate).
109+
ROUTER_KERNEL_AXES = ("embed_router", None)
110+
111+
112+
class KimiLatentMoEBlock(nnx.Module):
113+
"""Kimi-K3 Latent MoE Block with routed and shared experts.
114+
115+
The router reads the full hidden state; the routed experts run on a down-projected latent
116+
(7168 -> 3584), followed by an RMSNorm and an up-projection; shared experts read the full
117+
hidden state.
118+
119+
The router and routed experts are a `moe.RoutedMoE` (sorted/grouped dispatch: megablox /
120+
tokamax GMM / `ragged_dot`, expert parallelism), which needs `config` and `mesh`. The config
121+
must set `moe_expert_input_dim=routed_expert_hidden_size` and
122+
`moe_router_input_dim=hidden_size`; the latent is the expert input and the hidden state is
123+
passed as `gate_inputs`.
124+
"""
125+
126+
def __init__(
127+
self,
128+
hidden_size: int = 7168,
129+
num_experts: int = 896,
130+
top_k: int = 16,
131+
routed_expert_hidden_size: int = 3584,
132+
moe_intermediate_size: int = 3072,
133+
num_shared_experts: int = 2,
134+
shared_intermediate_size: Optional[int] = None,
135+
routed_scaling_factor: float = 1.0,
136+
moe_renormalize: bool = True,
137+
latent_moe_use_norm: bool = True,
138+
rms_norm_eps: float = 1e-5,
139+
dtype: DType = jnp.float32,
140+
weight_dtype: DType = jnp.float32,
141+
kernel_init: NdInitializer = nd_dense_init(1.0, "fan_in", "truncated_normal"),
142+
quant: Optional[Quant] = None,
143+
shard_mode: ShardMode = ShardMode.AUTO,
144+
matmul_precision: str = "default",
145+
mesh: Optional[Mesh] = None,
146+
config: Optional[Config] = None,
147+
*,
148+
rngs: nnx.Rngs,
149+
):
150+
self.hidden_size = hidden_size
151+
self.num_experts = num_experts
152+
self.top_k = top_k
153+
self.routed_expert_hidden_size = routed_expert_hidden_size
154+
self.moe_intermediate_size = moe_intermediate_size
155+
self.latent_moe_use_norm = latent_moe_use_norm
156+
self.dtype = dtype
157+
self.weight_dtype = weight_dtype
158+
159+
# Routed down projection: 7168 -> 3584
160+
self.routed_expert_down_proj = DenseGeneral(
161+
in_features_shape=hidden_size,
162+
out_features_shape=routed_expert_hidden_size,
163+
dtype=dtype,
164+
weight_dtype=weight_dtype,
165+
kernel_init=kernel_init,
166+
# The 3584-wide latent axis is deliberately unsharded (replicated); `embed` carries the sharding.
167+
kernel_axes=("embed", None),
168+
quant=quant,
169+
shard_mode=shard_mode,
170+
matmul_precision=matmul_precision,
171+
mesh=mesh,
172+
rngs=rngs,
173+
)
174+
175+
# 896 Routed Experts: 3584 -> 3072 -> 3584 (router on the 7168 hidden state)
176+
if config is None or mesh is None:
177+
raise ValueError("KimiLatentMoEBlock's routed experts are a moe.RoutedMoE, which needs `config` and `mesh`.")
178+
expected = {
179+
"moe_expert_input_dim": routed_expert_hidden_size,
180+
"moe_router_input_dim": hidden_size,
181+
"num_experts": num_experts,
182+
"num_experts_per_tok": top_k,
183+
"routed_scaling_factor": routed_scaling_factor,
184+
}
185+
mismatched = {k: (getattr(config, k), v) for k, v in expected.items() if getattr(config, k) != v}
186+
if mismatched:
187+
raise ValueError(f"config disagrees with KimiLatentMoEBlock args (config, arg): {mismatched}")
188+
if not moe_renormalize:
189+
raise ValueError("RoutedMoE always renormalizes sigmoid top-k weights; moe_renormalize=False is unsupported.")
190+
# This block returns only the MoE output (the decoder layer expects a single array), so the RoutedMoE
191+
# load-balance loss would be dropped; fail fast instead of training without it. (Router-bias updates,
192+
# `routed_bias_update_rate > 0`, are already rejected by config validation for non-DeepSeek decoder blocks.)
193+
if config.load_balance_loss_weight > 0.0:
194+
raise NotImplementedError(
195+
"KimiLatentMoEBlock does not propagate the MoE load-balance loss yet;"
196+
f" got load_balance_loss_weight={config.load_balance_loss_weight}. Set it to 0."
197+
)
198+
self.routed_experts = moe.RoutedMoE(
199+
config=config,
200+
num_experts=num_experts,
201+
num_experts_per_tok=top_k,
202+
mesh=mesh,
203+
kernel_init=kernel_init,
204+
kernel_axes=ROUTER_KERNEL_AXES,
205+
intermediate_dim=moe_intermediate_size,
206+
weight_dtype=weight_dtype,
207+
dtype=dtype,
208+
quant=quant,
209+
rngs=rngs,
210+
)
211+
212+
# Post-expert RMSNorm: 3584
213+
if self.latent_moe_use_norm:
214+
self.routed_expert_norm = RMSNorm(
215+
num_features=routed_expert_hidden_size,
216+
epsilon=rms_norm_eps,
217+
dtype=dtype,
218+
weight_dtype=weight_dtype,
219+
kernel_axes=("norm",),
220+
shard_mode=shard_mode,
221+
rngs=rngs,
222+
)
223+
else:
224+
self.routed_expert_norm = None
225+
226+
# Routed up projection: 3584 -> 7168
227+
self.routed_expert_up_proj = DenseGeneral(
228+
in_features_shape=routed_expert_hidden_size,
229+
out_features_shape=hidden_size,
230+
dtype=dtype,
231+
weight_dtype=weight_dtype,
232+
kernel_init=kernel_init,
233+
kernel_axes=(None, "embed"), # latent axis unsharded, as in the down projection.
234+
quant=quant,
235+
shard_mode=shard_mode,
236+
matmul_precision=matmul_precision,
237+
mesh=mesh,
238+
rngs=rngs,
239+
)
240+
241+
# Shared Experts: 7168 -> 6144 -> 7168
242+
if num_shared_experts > 0:
243+
shared_dim = shared_intermediate_size or (moe_intermediate_size * num_shared_experts)
244+
self.shared_expert = KimiDenseMLP(
245+
in_features=hidden_size,
246+
intermediate_dim=shared_dim,
247+
out_features=hidden_size,
248+
dtype=dtype,
249+
weight_dtype=weight_dtype,
250+
kernel_init=kernel_init,
251+
quant=quant,
252+
shard_mode=shard_mode,
253+
matmul_precision=matmul_precision,
254+
mesh=mesh,
255+
rngs=rngs,
256+
)
257+
else:
258+
self.shared_expert = None
259+
260+
def __call__(
261+
self,
262+
hidden_states: Array,
263+
out_sharding: Optional[NamedSharding] = None,
264+
) -> Array:
265+
"""Forward pass of KimiLatentMoEBlock.
266+
267+
Args:
268+
hidden_states: Input tensor of shape [..., hidden_size].
269+
out_sharding: Optional sharding specification.
270+
271+
Returns:
272+
Output tensor of shape [..., hidden_size].
273+
"""
274+
identity = hidden_states
275+
orig_shape = hidden_states.shape
276+
277+
# Down-project to the latent (7168 -> 3584); the router reads the full hidden state.
278+
x_latent = self.routed_expert_down_proj(hidden_states)
279+
y_routed, _, _ = self.routed_experts(x_latent, gate_inputs=hidden_states)
280+
281+
# Post-expert RMSNorm
282+
if self.routed_expert_norm is not None:
283+
y_routed = self.routed_expert_norm(y_routed)
284+
285+
# Up-project back to model dimension: 3584 -> 7168
286+
y_routed = self.routed_expert_up_proj(y_routed, out_sharding=out_sharding)
287+
y_routed = y_routed.reshape(orig_shape)
288+
289+
# Shared experts addition
290+
if self.shared_expert is not None:
291+
y_shared = self.shared_expert(identity, out_sharding=out_sharding)
292+
y_routed = y_routed + y_shared
293+
294+
return y_routed.astype(self.dtype)

0 commit comments

Comments
 (0)