Expand description
DeepSeek-style Multi-head Latent Attention (MLA): low-rank Q/KV
compression, with an optional sigmoid output gate (Kimi K3’s real
addition) and an optional RoPE rotation of the decoupled q_rot/
k_rot slices (MlaConfig::rope; GLM-5.2’s real addition – see
below). Transcribed directly from real reference code, not guessed
or derived by analogy:
- Kimi K3 (
moonshotai/Kimi-K3’smodeling_kimi_linear.py,KimiMLAAttention.forward, fetched live from the model repo): no rotary embedding is actually applied.q_rot/k_rotare named for the historical DeepSeek “rope part” split, but the real module assertsself.use_nopeand never calls a rotary embedding function inforward()— the “rot” slice is just extra head-dim content, never position-rotated. Represented here asMlaConfig::rope: None. - GLM-5.2 (
zai-org/GLM-5.2’s realconfig.json, confirmed against llama.cpp PR #25407’ssrc/models/glm-dsa.cpp) DOES rotate its decoupledq_rot/k_rotslices, with the interleaved convention (rope_interleave: true,ferrox_core::attention::apply_rope_interleaved) — the opposite of the natural-but-wrong assumption the Kimi K3 module doc above warns against, for a different real architecture. Represented here asMlaConfig::rope: Some(MlaRopeConfig { theta }).k_rotis MQA-style (one shared vector per position, broadcast to every head — see point 4) so it’s rotated once, before broadcasting; rotating a shared vector once then copying it into every head is exactly equivalent to rotating each head’s copy separately, since RoPE’s rotation angle depends only on position, not on the vector’s per-head value. kv_b_projexpands tonum_heads * (...), notnum_key_value_heads * (...), despitenum_key_value_heads/num_key_value_groupsbeing computed in Kimi K3’s real__init__— they go unused inforward(). Every query head gets its own decompressed K/V; there is no GQA-style grouping layered on top of the latent compression, which is why this module usesferrox_core::attention::causal_mla_attentionrather thancausal_gqa_attention.
When rope is None (Kimi K3’s real path), a further simplification
applies, not present in the reference code’s literal structure but
mathematically identical to it: the real forward() splits
q_b_proj’s output into q_pass/q_rot and immediately
re-concatenates them in the same order to form query_states. Since
nothing is inserted between the split and the concat (no rotation),
that round-trip is a no-op — concat(x[..a], x[a..]) == x — so this
implementation uses q_b_proj’s raw output directly as the query in
that case. When rope is Some (GLM-5.2’s real path), the split is
no longer a no-op (rotation happens in between), so the q_rot
slice is rotated in place before attention runs.
Not yet wired into Decoder’s forward pass (AttentionKind doesn’t
dispatch to this yet) or into ferrox_core::cache::KvCache (which
assumes K and V share one head_dim, whereas MLA’s K head dim
qk_nope_head_dim + qk_rope_head_dim and V head dim v_head_dim
generally differ) — both are handled by kimi_decoder (Kimi K3;
rope: None) and glm_dsa/glm52_decoder (GLM-5.2; rope: Some),
the dedicated decoders that consume this module. Tested here against
synthetic weights, cross-validated against independent Python
transcriptions of the same real reference algorithms for both
rope-disabled and rope-enabled paths.
Structs§
Functions§
- mla_
forward_ token - One decode step.
k_cache/v_cacheare growable, caller-owned buffers in[seq_len_so_far, n_heads, head_dim]layout (head_dim =qk_nope_head_dim + qk_rope_head_dimfork,v_head_dimforv) — plainVec<f32>, not yetferrox_core::cache::KvCache(see module doc comment). This function appends the current position’s K/V to both before running attention over every position pushed so far.