Skip to content

Jfacevedo kimi k3 - #4967

Open
entrpn wants to merge 12 commits into
mainfrom
jfacevedo_kimi_k3
Open

Jfacevedo kimi k3#4967
entrpn wants to merge 12 commits into
mainfrom
jfacevedo_kimi_k3

Conversation

@entrpn

@entrpn entrpn commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Description

Start with a short description of what the PR does and how this is a change from
the past.

The rest of the description includes relevant details and context, examples:

  • why is this change being made,
  • the problem being solved and any relevant context,
  • why this is a good solution,
  • some information about the specific implementation,
  • shortcomings of the solution and possible future improvements.

If the change fixes a bug or a Github issue, please include a link, e.g.,:
FIXES: b/123456
FIXES: #123456

You can also provide a comma-separated list. If you don't want to close a bug but
simply to reference it, use BUGS, e.g.:
BUGS: b/123456

Notice 1: Once all tests pass, the "pull ready" label will automatically be assigned.
This label is used for administrative purposes. Please do not add it manually.

Notice 2: For external contributions, our settings currently require an approval from a MaxText maintainer to trigger CI tests.

Tests

Please describe how you tested this change, and include any instructions and/or
commands to reproduce.

Checklist

Before submitting this PR, please make sure (put X in square brackets):

  • I have performed a self-review of my code. For an optional AI review, add the gemini-review label.
  • I have necessary comments in my code, particularly in hard-to-understand areas.
  • I have run end-to-end tests tests and provided workload links above if applicable.
  • I have made or will make corresponding changes to the doc if needed, including adding new documentation pages to the relevant Table of Contents (toctree directive) as explained in our documentation.

@google-cla

google-cla Bot commented Aug 22, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for the Kimi K3 model architecture in MaxText, adding configuration files, the Kimi Decoupled Attention (KDA) layer, custom activations, parameter mapping, and several diagnostic scripts and unit tests. The review feedback highlights critical correctness and compatibility issues that need to be addressed. Specifically, the ShortConv1D layer lacks a caching mechanism for autoregressive decoding, which will break generation equivalence. Additionally, changes to shared modules like MlpBlock and RoutedMoE introduce potential regressions for other models by forcing float32 activations and altering gate input dimensions. Other issues include hardcoded local paths in tests, concurrency risks in the stateful MXFP4DequantizeHook, a potential IndexError in KimiLinearModel, and missing PyTorch import guards that could crash the test suite.

Comment thread tests/unit/kda_test.py Outdated
# Load naive.py directly without triggering fla.ops.__init__ (which requires triton)
spec = importlib.util.spec_from_file_location(
"kda_naive",
"/Users/jfacevedo/.gemini/jetski/brain/0487c2aa-4e99-434c-b4e2-9147cc01875b/scratch/venv/lib/python3.12/site-packages/fla/ops/kda/naive.py",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This test contains a hardcoded absolute path to a local virtual environment directory (/Users/jfacevedo/...). This will fail on any other machine or in CI/CD pipelines. Please resolve this path dynamically or package the reference implementation/dependency properly.

Comment thread src/maxtext/layers/kda.py Outdated
Comment on lines +127 to +131
def __call__(self, x: jax.Array) -> jax.Array:
"""x: [B, T, features] -> [B, T, features]"""
# Depthwise 1D conv along sequence dimension T
# Pad left by (kernel_size - 1) to maintain causal alignment
padded = jnp.pad(x, ((0, 0), (self.kernel_size - 1, 0), (0, 0)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The ShortConv1D layer does not implement any caching mechanism for autoregressive decoding (where sequence length T = 1). During decoding, padding left with zeros (jnp.pad(x, ((0, 0), (self.kernel_size - 1, 0), (0, 0)))) on every step means the convolution will ignore the actual history of past tokens and assume they are all zeros. This breaks mathematical equivalence with the training path during generation, leading to severe model quality degradation. To support correct autoregressive generation, ShortConv1D must accept and return a conv state/cache (storing the last kernel_size - 1 inputs), and this state must be managed by the decoder layer and model.

Comment thread src/maxtext/layers/linears.py Outdated
if cfg.activations_in_float32:
x = x.astype(jnp.float32)
x = _convert_to_activation_function(act_fn)(x)
x = _convert_to_activation_function(act_fn)(x.astype(jnp.float32))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Forcing all activations to be computed in float32 by hardcoding x.astype(jnp.float32) in the shared MlpBlock affects all models in MaxText, not just Kimi K3. This can cause performance regressions (slower execution) and numerical changes for other models (like Llama or Gemma) that expect activations to run in bfloat16. Instead of hardcoding this, please revert to using the conditional cfg.activations_in_float32 check, and enable activations_in_float32: true in the Kimi K3 configuration files (kimi-k3.yml, kimi-k3-tiny.yml, etc.).

Suggested change
x = _convert_to_activation_function(act_fn)(x.astype(jnp.float32))
if cfg.activations_in_float32:
x = x.astype(jnp.float32)
x = _convert_to_activation_function(act_fn)(x)

Comment on lines +484 to +487
except ValueError as e:
if "not found in HF checkpoint index" in str(e):
return np.zeros(shape, dtype=np.float32)
raise e

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Silently returning zeros when a key is not found in the HF checkpoint index can mask parameter mapping typos or missing weights, leading to silent model corruption. It is highly recommended to log a warning when this fallback occurs.

Suggested change
except ValueError as e:
if "not found in HF checkpoint index" in str(e):
return np.zeros(shape, dtype=np.float32)
raise e
except ValueError as e:
if "not found in HF checkpoint index" in str(e):
max_logging.log(f"Warning: key {key} not found in HF checkpoint index. Initializing with zeros.")
return np.zeros(shape, dtype=np.float32)
raise e

Comment on lines +4377 to +4380
class MXFP4DequantizeHook:
"""Hook to dequantize MXFP4 weight_packed & weight_scale to bfloat16 for a single expert in MaxText.
Pads in_features or out_features to 7168 with zeros and caches scales for 100x speedup.
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The MXFP4DequantizeHook class relies on a mutable state (self.current_expert_idx) to track which expert is currently being dequantized. This assumes a strict, sequential, single-threaded execution order during checkpoint loading. If parallel loading (e.g., via a thread pool) is ever introduced, or if any expert is skipped or re-ordered, this stateful counter will get out of sync, leading to silent weight corruption (applying incorrect scales to experts). Consider making this hook stateless or adding explicit safeguards/assertions to ensure sequential consistency.

Comment thread src/maxtext/models/kimi_linear.py Outdated
# 2. Sequential Decoder Layers
kda_states = []
for i, layer in enumerate(self.layers):
init_state = initial_kda_states[i] if initial_kda_states is not None else None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Potential IndexError if initial_kda_states is shorter than self.layers. Adding a safe length check prevents potential crashes if a partial list of states is passed.

Suggested change
init_state = initial_kda_states[i] if initial_kda_states is not None else None
init_state = initial_kda_states[i] if (initial_kda_states is not None and i < len(initial_kda_states)) else None

Comment on lines +23 to +25
import os
import sys
import unittest

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

PyTorch import without pytest.importorskip('torch') can crash the entire test collection suite if PyTorch is not installed in the environment. Adding the import guard ensures the test is gracefully skipped.

Suggested change
import os
import sys
import unittest
import numpy as np
import pytest
pytest.importorskip("torch")
import torch
import torch.nn as nn

Comment thread tests/unit/kda_test.py Outdated
Comment on lines +21 to +22
import pytest
import torch

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

PyTorch import without pytest.importorskip('torch') can crash the entire test collection suite if PyTorch is not installed in the environment. Adding the import guard ensures the test is gracefully skipped.

Suggested change
import pytest
import torch
import pytest
pytest.importorskip("torch")
import torch

Comment on lines +20 to +21
import pytest
import torch

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

PyTorch import without pytest.importorskip('torch') can crash the entire test collection suite if PyTorch is not installed in the environment. Adding the import guard ensures the test is gracefully skipped.

Suggested change
import pytest
import torch
import pytest
pytest.importorskip("torch")
import torch

@classmethod
def setUpClass(cls):
cls.checkpoint_dir = "/Users/jfacevedo/apps/maxtext/scratch/kimi_k3_orbax_checkpoint"
if not os.path.exists(cls.checkpoint_dir):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The checkpoint directory path is hardcoded to a local path (/Users/jfacevedo/...). Although the test skips gracefully if the path is not found, it is better to make this path configurable via an environment variable or a command-line argument so other developers or CI/CD can run this test with their own checkpoints.

…pydantic schema

- Register DecoderBlockType.KIMI_K3 in common_types.py
- Add Kimi K3 & KDA specific fields to ModelArchitecture in types.py
- Exempt KIMI_K3 from base_mlp_dim == base_moe_mlp_dim validation check in types.py
- Create kimi-k3.yml (full 93-layer hybrid KDA/MLA + 896-expert MoE config)
- Create kimi-k3-tiny.yml (4-layer tiny config for local testing)
- Add KIMI_K3_CONFIGS test suite to configs_test.py
- Decouple optional Google-internal/uninstalled dependencies across maxtext
…ests

- Implement situ_and_mul in linears.py with beta (4.0) and linear_beta (25.0) parameters
- Register 'situ' activation in _convert_to_activation_function in linears.py
- Add situ_activation_test.py with 7 test cases verifying numerical parity against PyTorch Kimi-K3 reference across float32 and bfloat16
…unit tests

- Implement kda_recurrent_kernel in JAX using jax.lax.scan with log-space decay
- Implement ShortConv1D in JAX using jax.lax.conv_general_dilated (depthwise 1D conv with silu)
- Implement KimiDecoupledAttention NNX module in kda.py with Q/K/V projections, 1D convs, L2-normalization, gate/beta projections, A_log/dt_bias parameters, and FusedRMSNormGated
- Add kda_test.py with 6 test cases verifying ShortConv1D causality, kda_recurrent_kernel parity against fla naive_recurrent_kda across sequence lengths T=1..128, and KimiDecoupledAttention NNX module forward pass
… tests

- Add mla_use_output_gate support in MLA.__init__ to initialize g_a_proj (emb_dim -> head_dim), g_b_proj (head_dim -> (num_query_heads, v_head_dim)), and o_norm (RMSNorm)
- Add mla_use_output_gate forward pass in MLA.__call__ applying RMSNorm(out) * sigmoid(g) before out_projection
- Add mla_output_gate_test.py verifying MLA initialization and forward pass with mla_use_output_gate=True
…nit tests

- Update mlp_activations in kimi-k3.yml and kimi-k3-tiny.yml to ["situ", "linear_beta_tanh"] to cleanly support gated activations in RoutedMoE
- Add situ and linear_beta_tanh single-input activations in linears.py
- Add latent_moe_use_norm support in RoutedAndSharedMoE in moe.py to apply RMSNorm to routed experts when enabled
- Add pure JAX jax.lax.scan fallback for gmm in megablox/backend.py for CPU/macOS execution without qwix.pallas
- Add qpl, tokamax, and drjax None checks for optional Google-internal dependencies
- Add kimi_moe_test.py verifying RoutedAndSharedMoE initialization and forward pass with 896-expert MoE config
…-to-end unit tests

- Create KimiDecoderLayer in src/maxtext/layers/kimi_decoder_layer.py to dynamically select between KDA and MLA attention layers per layer_idx and pair with RoutedAndSharedMoE
- Create KimiLinearModel in src/maxtext/models/kimi_linear.py to assemble the full Kimi K3 text-only backbone in NNX (Embed -> nnx.List[KimiDecoderLayer] -> RMSNorm -> DenseGeneral)
- Add kimi_linear_model_test.py with 3 test cases verifying KimiDecoderLayer, KimiLinearModel end-to-end forward pass, and initial KDA state handling
…orous layer-by-layer logit parity unit tests (KL < 1e-4)
@entrpn
entrpn force-pushed the jfacevedo_kimi_k3 branch from a8d79f0 to 2b10f89 Compare August 22, 2026 04:51
…e cache, stateless MXFP4 dequantization, import guards, and path cleanup
@github-actions

Copy link
Copy Markdown
Contributor

🤖 Hi @entrpn, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📋 Review Summary

This Pull Request introduces comprehensive support for Kimi K3, a hybrid architecture featuring Decoupled Attention (KDA) and Multi-Head Latent Attention (MLA), along with dynamic activations (Situ / Linear Beta Tanh) and Routed/Shared MoE. The overall quality of the implementation is very high, and the inclusion of extensive unit and JAX-PyTorch parity tests is exemplary. However, there are a few critical correctness and flexibility issues that must be addressed to support standard configurations (like GQA) and prevent state-loss during sequential generation.

🔍 General Feedback

  • Exemplary Test Coverage: The addition of detailed parity and functional tests (e.g., kimi_k3_logit_parity_test.py, kimi_linear_model_test.py, kimi_moe_test.py, and situ_activation_test.py) is a major highlight, ensuring mathematical correctness and preventing regressions.
  • Stateless/Stateful Design: The Kimi Decoder layer and Kimi Linear Model backbones are beautifully modular. Standardizing state keys and names will make integration into production pipelines (like MaxEngine and vLLM) even cleaner.
  • Dynamic Dimension Routing: Avoiding hardcoded constants (like 7168) in the checkpoint conversion pipelines will make the codebase more resilient to scaled-down configurations or customized Kimi-based experiments.

Comment thread src/maxtext/layers/kda.py
Comment on lines +55 to +64
B, T, H, K = q.shape
HV, V = v.shape[2], v.shape[3]
G = HV // H
if scale is None:
scale = K**-0.5

# Repeat interleave q, k to HV if HV != H
if G > 1:
q = jnp.repeat(q, G, axis=2)
k = jnp.repeat(k, G, axis=2)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The KDA recurrent kernel will crash with a shape mismatch error if GQA/MQA is configured (where query heads outnumber key-value heads).

Suggested change
B, T, H, K = q.shape
HV, V = v.shape[2], v.shape[3]
G = HV // H
if scale is None:
scale = K**-0.5
# Repeat interleave q, k to HV if HV != H
if G > 1:
q = jnp.repeat(q, G, axis=2)
k = jnp.repeat(k, G, axis=2)
B, T, H, K = q.shape
HV, V = v.shape[2], v.shape[3]
if scale is None:
scale = K**-0.5
# Support standard Grouped Query Attention (H > HV) and other configurations (HV > H)
if H > HV:
G = H // HV
k = jnp.repeat(k, G, axis=2)
v = jnp.repeat(v, G, axis=2)
g = jnp.repeat(g, G, axis=2)
beta = jnp.repeat(beta, G, axis=2)
HV = H
elif HV > H:
G = HV // H
q = jnp.repeat(q, G, axis=2)
k = jnp.repeat(k, G, axis=2)

w_transposed = np.transpose(w_dequant, (1, 0))

w_padded = np.pad(w_transposed, ((0, 0), (0, 7168 - 3584)), mode="constant")
return w_padded.astype(ml_dtypes.bfloat16)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Hardcoding the expert MLP sizes (7168 and 3584) in top-level dequantization hooks breaks weight conversion for any non-standard model sizes. We can compute the padding dynamically based on target_shape.

Suggested change
return w_padded.astype(ml_dtypes.bfloat16)
if target_shape:
pad_0 = max(0, target_shape[0] - w_transposed.shape[0])
pad_1 = max(0, target_shape[1] - w_transposed.shape[1])
w_padded = np.pad(w_transposed, ((0, pad_0), (0, pad_1)), mode="constant")
else:
w_padded = np.pad(w_transposed, ((0, 0), (0, 7168 - 3584)), mode="constant")

Comment on lines +142 to +146
if self.is_kda:
attn_out, kda_state = self.self_attention(
normed_inputs,
initial_state=initial_kda_state,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The initial_kda_state parameter defaults to None, but NNXDecoder always passes the recurrent/KV cache state as kv_cache. Since kv_cache is ignored here, the state is completely lost during generation, causing the KDA layers to use a zero recurrent state and zero convolution history at every step of generation!

Suggested change
if self.is_kda:
attn_out, kda_state = self.self_attention(
normed_inputs,
initial_state=initial_kda_state,
)
if self.is_kda:
attn_out, kda_state = self.self_attention(
normed_inputs,
initial_state=initial_kda_state if initial_kda_state is not None else kv_cache,
)


w_dequant = w_fp * scales
w_transposed = np.transpose(w_dequant, (1, 0))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Hardcoding the expert MLP sizes (7168 and 3584) in top-level dequantization hooks breaks weight conversion for any non-standard model sizes. We can compute the padding dynamically based on target_shape.

Suggested change
if target_shape:
pad_0 = max(0, target_shape[0] - w_transposed.shape[0])
pad_1 = max(0, target_shape[1] - w_transposed.shape[1])
w_padded = np.pad(w_transposed, ((0, pad_0), (0, pad_1)), mode="constant")
else:
w_padded = np.pad(w_transposed, ((0, 7168 - 3584), (0, 0)), mode="constant")

Comment on lines +4435 to +4438

def routed_expert_norm_hook(x, target_shape=None):
if hasattr(x, "shape") and len(x.shape) == 1 and x.shape[0] < 7168:
return np.pad(x, (0, 7168 - x.shape[0]), mode="constant", constant_values=1.0).astype(np.float32)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Hardcoding the model's hidden dimension (7168) in checkpoint conversion hooks prevents converting/loading checkpoints for custom or scaled-down configs (like the tiny test config, where emb_dim is 256). Let's calculate the dimensions dynamically.

Suggested change
def routed_expert_norm_hook(x, target_shape=None):
if hasattr(x, "shape") and len(x.shape) == 1 and x.shape[0] < 7168:
return np.pad(x, (0, 7168 - x.shape[0]), mode="constant", constant_values=1.0).astype(np.float32)
def routed_expert_norm_hook(x, target_shape=None):
target_dim = target_shape[0] if target_shape else maxtext_config.emb_dim
if hasattr(x, "shape") and len(x.shape) == 1 and x.shape[0] < target_dim:
return np.pad(x, (0, target_dim - x.shape[0]), mode="constant", constant_values=1.0).astype(np.float32)
return x

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants