Skip to content

Commit ac24906

Browse files
committed
Merge kimi-k3-stack/pr3..pr6 (integration base for PR8)
4 parents 893ac7f + 90fd652 + 9d24120 + 8a0d413 commit ac24906

16 files changed

Lines changed: 1560 additions & 21 deletions

‎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: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ attn_res_block_size: 12
4646

4747
# MLP and MoE Specifications
4848
first_num_dense_layers: 1
49-
mlp_activations: ["situ", "linear"]
49+
# SiTU on both branches: situ_gate(wi_0 x) * situ_linear(wi_1 x).
50+
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.
5253
base_mlp_dim: 33792
@@ -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/attention_mla.py‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -522,13 +522,18 @@ def __init__(
522522
rope_factor: float = 40.0, # rotary embedding factor
523523
name: str | None = None,
524524
rngs: Optional[nnx.Rngs] = None,
525+
mla_use_output_gate: bool = False,
525526
):
526527
"""Initializes the MLA module.
527528
528529
Args:
529530
config: The model configuration.
530531
... and other configuration parameters for MLA attention.
532+
is_nope_layer: If True, the rotary transform is skipped for the `qk_rope_head_dim`
533+
slices of the query and key (the slices are still projected and attended over).
531534
rngs: The random number generators for initialization, passed by the nnx.to_linen wrapper.
535+
mla_use_output_gate: If True, adds a per-head sigmoid output gate before the output
536+
projection: `out = out_proj(attn * sigmoid(g_proj(inputs_q)))`.
532537
"""
533538
base_kv_cache = config.attention != "paged" and config.mla_naive_kvcache
534539

@@ -543,6 +548,7 @@ def __init__(
543548
self.original_max_position_embeddings = original_max_position_embeddings
544549
self.mscale = mscale
545550
self.rope_factor = rope_factor
551+
self.use_output_gate = mla_use_output_gate
546552

547553
self.qk_head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
548554

@@ -765,6 +771,23 @@ def _init_projections(self, inputs_q_shape: Tuple, inputs_kv_shape: Tuple) -> No
765771
mscale = 0.1 * self.mscale * math.log(self.rope_factor) + 1.0
766772
self.softmax_scale = self.softmax_scale * mscale * mscale
767773

774+
if self.use_output_gate:
775+
self.g_proj = DenseGeneral(
776+
in_features_shape=self.config.emb_dim,
777+
out_features_shape=(self.num_query_heads, self.v_head_dim),
778+
axis=-1,
779+
kernel_init=self.kernel_init,
780+
kernel_axes=("embed_attn", "q_heads", "kv"),
781+
dtype=self.dtype,
782+
weight_dtype=self.weight_dtype,
783+
quant=self.quant,
784+
matmul_precision=self.config.matmul_precision,
785+
shard_mode=self.config.shard_mode,
786+
rngs=self.rngs,
787+
)
788+
else:
789+
self.g_proj = None
790+
768791
self.out = self.init_out_w(output_dim=inputs_q_shape[-1])
769792

770793
def _attention_op_num_kv_heads(self) -> int:
@@ -866,7 +889,10 @@ def mla_query_projection(
866889
# Partial RoPE: Split into non-positional and rotary parts.
867890
# last dimension: qk_nope_head_dim, qk_rope_head_dim
868891
q_nope = self._maybe_shard_with_logical(q_nope, query_logical_name)
869-
q_pe = self.apply_rotary_embedding(q_pe, inputs_positions=inputs_positions)
892+
# NoPE layers (e.g. Kimi-K3) still carry a `qk_rope_head_dim` slice through the
893+
# projections, but never apply the rotary transform to it.
894+
if not self.is_nope_layer:
895+
q_pe = self.apply_rotary_embedding(q_pe, inputs_positions=inputs_positions)
870896
q_pe = self._maybe_shard_with_logical(q_pe, query_logical_name)
871897
# Query projection is scaled by self.softmax_scale to be consistent MaxText implementation.
872898
# DeepSeek v3 was doing it in attention score computation.
@@ -1024,9 +1050,10 @@ def mla_kv_projection(self, inputs: Array, inputs_positions: Array, decoder_segm
10241050
low_rank_main, low_rank_rope = jnp.split(low_rank, [self.kv_lora_rank], axis=-1)
10251051
low_rank_main = self.kv_norm(low_rank_main)
10261052
low_rank_main = checkpoint_name(low_rank_main, "mla_kv")
1027-
# Apply rotary embedding to key_rope.
1053+
# Apply rotary embedding to key_rope (skipped entirely for NoPE layers).
10281054
key_rope = jnp.expand_dims(low_rank_rope, axis=2)
1029-
key_rope = self.apply_rotary_embedding(key_rope, inputs_positions=inputs_positions)
1055+
if not self.is_nope_layer:
1056+
key_rope = self.apply_rotary_embedding(key_rope, inputs_positions=inputs_positions)
10301057

10311058
key, value = self._build_key_value(low_rank_main, key_rope, model_mode)
10321059
cached_values = [None, None]
@@ -1300,6 +1327,12 @@ def __call__(
13001327
out = self._maybe_shard_with_logical(out, self.out_axis_names)
13011328
out = jax.ad_checkpoint.checkpoint_name(out, "attention_out")
13021329

1330+
if self.use_output_gate:
1331+
# Matches the HF reference: the sigmoid is computed in the activation dtype.
1332+
g = jax.nn.sigmoid(self.g_proj(inputs_q))
1333+
g = self._maybe_shard_with_logical(g, self.out_axis_names)
1334+
out = out * g
1335+
13031336
out_sharding = create_sharding(self.mesh, out_logical_name)
13041337
out = self.out_projection(out, out_sharding=out_sharding)
13051338
out = checkpoint_name(out, "out_proj")

‎src/maxtext/layers/attn_res.py‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
"""Attention Residuals (AttnRes) softmax pooling for Kimi-K3.
16+
17+
Kimi-K3 replaces the plain residual stream with a learned softmax pooling over
18+
a list of block residuals plus the running prefix sum. This module provides only
19+
the pooling primitive; the decoder layer owns the per-site weights (RMSNorm scale
20+
and a [D, 1] projection) and decides when a new block residual is committed.
21+
22+
V = [block_residual_0, ..., block_residual_{K-1}, prefix_sum]
23+
k = V * rsqrt(mean(V**2, -1) + eps) (RMSNorm without scale, fp32)
24+
scores = sum(k * (norm_weight * proj_weight), -1)
25+
out = sum(softmax(scores) * V, over K+1)
26+
"""
27+
28+
from typing import Optional
29+
30+
import jax
31+
from jax import lax
32+
import jax.numpy as jnp
33+
34+
35+
def _apply_attn_res(
36+
prefix_sum: jnp.ndarray,
37+
block_residual: Optional[jnp.ndarray],
38+
proj_weight: jnp.ndarray,
39+
norm_weight: jnp.ndarray,
40+
epsilon: float = 1e-5,
41+
num_blocks: None = None,
42+
) -> jnp.ndarray:
43+
"""Softmax-pools `block_residual` and `prefix_sum` (port of HF `_apply_attn_res`).
44+
45+
Args:
46+
prefix_sum: Running prefix sum of shape [..., D].
47+
block_residual: Committed block residuals of shape [..., K, D], or None.
48+
With no blocks (None or K == 0) the pooling is the identity and
49+
`prefix_sum` is returned unchanged.
50+
proj_weight: Projection kernel of shape [D, 1], [1, D] or [D].
51+
norm_weight: RMSNorm scale of shape [D].
52+
epsilon: RMSNorm variance epsilon.
53+
num_blocks: Must be None. Accepted only for call-site compatibility; the
54+
blocks are always taken from the (dynamically grown) `block_residual`.
55+
56+
Returns:
57+
Pooled hidden states of shape [..., D] in `prefix_sum.dtype`.
58+
"""
59+
if num_blocks is not None:
60+
raise NotImplementedError("Static-padded AttnRes (num_blocks != None) is not supported.")
61+
# No blocks: softmax over a single slot is 1, so the result is prefix_sum itself.
62+
if block_residual is None or block_residual.shape[-2] == 0:
63+
return prefix_sum
64+
65+
orig_dtype = prefix_sum.dtype
66+
v = jnp.concatenate([block_residual, jnp.expand_dims(prefix_sum, axis=-2)], axis=-2) # [..., K+1, D]
67+
68+
# fp32 materialization of V is deliberate, to match the HF reference numerics.
69+
v_float = v.astype(jnp.float32)
70+
k = v_float * lax.rsqrt(jnp.mean(jnp.square(v_float), axis=-1, keepdims=True) + epsilon)
71+
72+
score_weight = norm_weight.astype(jnp.float32).reshape(-1) * proj_weight.astype(jnp.float32).reshape(-1) # [D]
73+
scores = jnp.sum(k * score_weight, axis=-1) # [..., K+1]
74+
probs = jax.nn.softmax(scores, axis=-1)
75+
hidden_states = jnp.sum(jnp.expand_dims(probs, axis=-1) * v_float, axis=-2)
76+
return hidden_states.astype(orig_dtype)

0 commit comments

Comments
 (0)