Skip to content

feat(attention_mla): optional sigmoid output gate and NoPE support - #5543

Draft
snehalv2002 wants to merge 1 commit into
kimi-k3-stack/pr1-foundationfrom
kimi-k3-stack/pr4-mla-gate
Draft

snehalv2002 wants to merge 1 commit into
kimi-k3-stack/pr1-foundationfrom
kimi-k3-stack/pr4-mla-gate

Conversation

@snehalv2002

@snehalv2002 snehalv2002 commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

MLA: optional sigmoid output gate and NoPE (skip-RoPE) support

What

Two opt-in changes to the shared MLA layer (attention_mla.py), both needed by Kimi-K3 full-attention layers:

  1. Output gate. New constructor arg MLA(mla_use_output_gate=False). When it is True, the layer creates g_proj (embed_attn → q_heads × v_head_dim) and computes out = out_proj(attn * sigmoid(g_proj(inputs_q))). The gate gets the same logical sharding as the attention output. As in the HF reference, the sigmoid runs in the activation dtype. The constructor arg is the only switch: there is no config field, which is the same pattern as is_nope_layer. The Kimi-K3 decoder layer (PR8) passes mla_use_output_gate=True.
  2. NoPE. MLA now honours the existing is_nope_layer flag. It skips apply_rotary_embedding on the q_pe / k_pe slices in mla_query_projection and mla_kv_projection. The slices are still projected and still take part in attention. Kimi-K3 sets HF mla_use_nope: true. Before this PR, MLA silently ignored the flag.

Both features are off by default. No existing MLA caller (DeepSeek, Kimi-K2, …) passes either flag, so their behaviour and params are unchanged.

Tests

New tests/unit/kimi_k3_mla_gate_nope_test.py:

  • Hermetic (runs in CI, no torch needed):
    • The gate is off by default.
    • With zero g_proj weights the output is exactly 0.5× the ungated output.
    • With is_nope_layer=True at non-zero positions, the output doesn't depend on position and equals the RoPE layer's output at position 0. RoPE at non-zero positions gives a different output.
  • HF parity (small dims): gated NoPE MLA vs HF KimiMLAAttention at positions arange(seq), with weights loaded through PR1's kimi-k3 param mapping. Every expected key must convert. A negative check confirms that the RoPE variant diverges. This test skips unless torch, transformers, einops and the untracked kimi-k3-hf-reference/ checkout are all present, and it restores sys.path / sys.modules afterwards.
JAX_PLATFORMS=cpu PYTHONPATH=./src:. python -m pytest -q tests/unit/kimi_k3_mla_gate_nope_test.py
# 4 passed with the reference; 3 passed, 1 skipped without it
JAX_PLATFORMS=cpu PYTHONPATH=./src:. python -m pytest -q tests/unit/attention_test.py -k "MLA and not tpu"
# 33 passed

Size: 36 src / 289 test lines (vs. its stack base).

Known limitations

  • The gate is tested in train mode only. Prefill/decode share the same code path.
  • The gated activation has no remat checkpoint name of its own. It sits between the existing attention_out and out_proj names.

Stack: Kimi-K3 series, this is PR4; merge order PR0, PR1, PR2..PR7, PR8, PR9, PR10 (#6014).

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.

@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 in MaxText, adding the model configuration, parameter mapping, transformation hooks, and corresponding unit tests. The reviewer feedback highlights several critical issues: in param_mapping.py, the config parameter needs to be safely converted to a dictionary to prevent AttributeError when a PretrainedConfig object is passed. In the unit tests, there is duplicated code for convert_pytorch_module_to_maxtext_params that should be imported from the utility module, and the tests should pass self.hf_config["text_config"] instead of self.hf_config to avoid falling back to default configuration values.

`kimi-k3.yml` and by test configs, 0-indexed.
Falls back to the production 93-layer layout if neither is present.
"""
num_layers = config.get("num_hidden_layers", 93)

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 config parameter can be either a dictionary or a Hugging Face PretrainedConfig object (such as KimiK3Config). Since PretrainedConfig does not have a .get() method, calling config.get(...) directly will raise an AttributeError during actual checkpoint conversion. Converting config to a dictionary at the beginning of the function ensures compatibility with both dictionary and configuration object inputs.

  if not isinstance(config, dict):\n    config = config.to_dict() if hasattr(config, "to_dict") else getattr(config, "__dict__", {})\n  num_layers = config.get("num_hidden_layers", 93)


def KIMI_K3_MAXTEXT_TO_HF_PARAM_MAPPING(config, maxtext_config, scan_layers=False):
"""Generates parameter mapping from MaxText to Hugging Face for Kimi-K3."""
num_main_layers = config.get("num_hidden_layers", 93)

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 config parameter can be a Hugging Face PretrainedConfig object which does not support the .get() method. Converting config to a dictionary at the beginning of the function ensures compatibility with both dictionary and configuration object inputs.

  if not isinstance(config, dict):\n    config = config.to_dict() if hasattr(config, "to_dict") else getattr(config, "__dict__", {})\n  num_main_layers = config.get("num_hidden_layers", 93)


def KIMI_K3_MAXTEXT_TO_HF_PARAM_HOOK_FN(config, maxtext_config, scan_layers=False, saving_to_hf=False):
"""Transformation hooks for Kimi-K3 parameters."""
hidden_size = config.get("hidden_size", 7168)

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 config parameter can be a Hugging Face PretrainedConfig object which does not support the .get() method. Converting config to a dictionary at the beginning of the function ensures compatibility with both dictionary and configuration object inputs.

  if not isinstance(config, dict):\n    config = config.to_dict() if hasattr(config, "to_dict") else getattr(config, "__dict__", {})\n  hidden_size = config.get("hidden_size", 7168)

Comment thread tests/unit/test_kimi_k3_g2_mla.py Outdated
Comment on lines +34 to +60
def convert_pytorch_module_to_maxtext_params(pt_state_dict, hf_config, maxtext_config, prefix_filter=None):
"""Converts PyTorch state dict tensors to MaxText param arrays using Kimi-K3 mappings and hooks."""
mapping_fn = PARAM_MAPPING["kimi-k3"]
hook_fn_factory = HOOK_FNS["kimi-k3"]

param_map = mapping_fn(hf_config, maxtext_config, scan_layers=False)
hook_map = hook_fn_factory(hf_config, maxtext_config, scan_layers=False, saving_to_hf=False)

converted_params = {}
for mt_key, hf_target in param_map.items():
if prefix_filter and not mt_key.startswith(prefix_filter):
continue

if isinstance(hf_target, str):
matching_key = None
for k in pt_state_dict.keys():
if hf_target.endswith(k):
matching_key = k
break

if matching_key in pt_state_dict:
tensor_np = pt_state_dict[matching_key].detach().cpu().float().numpy()
if mt_key in hook_map:
tensor_np = hook_map[mt_key](tensor_np)
converted_params[mt_key] = tensor_np

return converted_params

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 convert_pytorch_module_to_maxtext_params function is duplicated here and in tests/utils/kimi_k3_conversion_utils.py. To improve maintainability and avoid code duplication, import the canonical version from the utility module instead.

from tests.utils.kimi_k3_conversion_utils import convert_pytorch_module_to_maxtext_params

Comment thread tests/unit/test_kimi_k3_g2_mla.py Outdated
Comment on lines +138 to +143
converted_params = convert_pytorch_module_to_maxtext_params(
pt_mla.state_dict(),
self.hf_config,
None,
prefix_filter="params-decoder-layers_3-self_attention",
)

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 PARAM_MAPPING functions expect the flat configuration dictionary (or configuration object). Passing self.hf_config directly (which contains "text_config" as a nested key) causes the mapping functions to fall back to default values (e.g., defaulting num_hidden_layers to 93 instead of 4). Pass self.hf_config["text_config"] instead.

    converted_params = convert_pytorch_module_to_maxtext_params(\n        pt_mla.state_dict(),\n        self.hf_config["text_config"],\n        None,\n        prefix_filter="params-decoder-layers_3-self_attention",\n    )

Comment thread tests/unit/test_kimi_k3_g2_mla.py Outdated

def test_param_mapping_shapes(self):
"""Verifies that Group 2 parameter mappings and transformation hooks produce correct MaxText shapes."""
hooks = HOOK_FNS["kimi-k3"](self.hf_config, None, scan_layers=False, saving_to_hf=False)

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 HOOK_FNS factory expects the flat configuration dictionary (or configuration object). Passing self.hf_config directly (which contains "text_config" as a nested key) causes the hook functions to fall back to default values. Pass self.hf_config["text_config"] instead.

Suggested change
hooks = HOOK_FNS["kimi-k3"](self.hf_config, None, scan_layers=False, saving_to_hf=False)
hooks = HOOK_FNS["kimi-k3"](self.hf_config["text_config"], None, scan_layers=False, saving_to_hf=False)

@codecov

codecov Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@snehalv2002
snehalv2002 changed the base branch from main to kimi-k3-stack/pr1-foundation October 6, 2026 00:04
@snehalv2002
snehalv2002 force-pushed the kimi-k3-stack/pr1-foundation branch from b332c56 to f341ad9 Compare October 19, 2026 18:45
@snehalv2002
snehalv2002 force-pushed the kimi-k3-stack/pr4-mla-gate branch from 59b2dc6 to d17c8e2 Compare October 19, 2026 18:45
@snehalv2002
snehalv2002 force-pushed the kimi-k3-stack/pr4-mla-gate branch from d17c8e2 to 5f04dee Compare October 28, 2026 19:53
@snehalv2002
snehalv2002 force-pushed the kimi-k3-stack/pr1-foundation branch from f341ad9 to 1af3969 Compare October 28, 2026 19:53
@snehalv2002 snehalv2002 changed the title Kimi k3 stack/pr4 mla gate feat(attention_mla): optional sigmoid output gate and NoPE support Oct 28, 2026
Add optional sigmoid output gate to MLA attention layer controlled by mla_use_output_gate configuration.
@snehalv2002
snehalv2002 force-pushed the kimi-k3-stack/pr4-mla-gate branch from 5f04dee to 90fd652 Compare October 29, 2026 22:32
@snehalv2002
snehalv2002 force-pushed the kimi-k3-stack/pr1-foundation branch from 1af3969 to 38df8a0 Compare October 29, 2026 22:32

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant