atlas_core/kimi_k3/
kda.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Kimi K3 KDA CPU reference — a new backend, not GDN / Mamba-2.
4//!
5//! Production geometry: `head_dim=128`, `short_conv_kernel_size=4`,
6//! `use_full_rank_gate=true`, `gate_lower_bound=Some(-5)`. Decay stays low-rank
7//! `f_a`/`f_b`; the **output** gate is full-rank `g_proj` (unlike GLM-5.3's
8//! `g_a`/`g_b`). The 0.40B twin omits `gate_lower_bound`; HF then runs FLA's
9//! unbounded `-exp(A_log)*softplus` path (`None` here).
10//!
11//! Recurrence (decode, prenorm q/k):
12//! ```text
13//! S <- S * diag(exp(g_t))     // decay on KEY axis, per channel
14//! delta <- (v_t - S^T k_t) * sigmoid(beta_t)
15//! S <- S + k_t ⊗ delta
16//! o_t <- S^T q_t / sqrt(d)
17//! ```
18//!
19//! Conv state is `[channels, kernel]` (FLA `ShortConvolution` cache `W=kernel`).
20//! Slot 0 is shifted out. `beta` is a raw logit; the step applies `sigmoid`.
21
22#![allow(clippy::needless_range_loop)]
23
24use crate::config::ModelConfig;
25
26#[inline]
27fn sigmoid(x: f32) -> f32 {
28    1.0 / (1.0 + (-x).exp())
29}
30
31/// FLA `use_qk_l2norm_in_kernel` eps. CUDA `k3_kda_recurrent_step_f32` uses the same.
32pub const KDA_L2_EPS: f32 = 1e-6;
33
34/// KDA geometry. Tiny dims are legal for CPU tests; production is 128/4.
35#[derive(Clone, Copy, Debug)]
36pub struct KdaConfig {
37    pub heads: usize,
38    pub head_dim: usize,
39    pub conv_kernel: usize,
40    /// Production JSON is `-5`. `None` is the FLA default (twin omits the key).
41    pub gate_lower_bound: Option<f32>,
42    pub use_full_rank_gate: bool,
43}
44
45impl KdaConfig {
46    /// Official K3 KDA. Twin matches except head/heads and omitted `gate_lower_bound`.
47    pub fn production() -> Self {
48        Self {
49            heads: 96,
50            head_dim: 128,
51            conv_kernel: 4,
52            gate_lower_bound: Some(-5.0),
53            use_full_rank_gate: true,
54        }
55    }
56
57    /// `inference-optimization/Kimi-K3-0.40B` linear_attn_config (gate key omitted).
58    pub fn twin_0_40b() -> Self {
59        Self {
60            heads: 8,
61            head_dim: 32,
62            conv_kernel: 4,
63            gate_lower_bound: None,
64            use_full_rank_gate: true,
65        }
66    }
67
68    pub fn qkv_dim(&self) -> usize {
69        self.heads * self.head_dim
70    }
71
72    pub fn conv_dim(&self) -> usize {
73        3 * self.qkv_dim()
74    }
75
76    pub fn recurrent_elems(&self) -> usize {
77        self.heads * self.head_dim * self.head_dim
78    }
79
80    pub fn conv_elems(&self) -> usize {
81        self.conv_dim() * self.conv_kernel
82    }
83}
84
85/// Map parsed `ModelConfig` onto KDA geometry.
86///
87/// Twin omits `gate_lower_bound` so the factory field stays 0.0 → `None`
88/// (FLA unbounded). Production JSON supplies `-5.0` → `Some(-5)`.
89pub fn kda_from(c: &ModelConfig) -> KdaConfig {
90    KdaConfig {
91        heads: c.linear_num_key_heads,
92        head_dim: c.linear_key_head_dim,
93        conv_kernel: c.linear_conv_kernel_dim.max(1),
94        gate_lower_bound: (c.linear_gate_lower_bound != 0.0).then_some(c.linear_gate_lower_bound),
95        use_full_rank_gate: c.use_full_rank_gate,
96    }
97}
98
99/// LinearAttention BoundLayer uses CUDA `kda_decode` unless `K3_CUDA_KDA=0`.
100/// Projections, AttnRes, and MLP stay on the host either way.
101pub fn cuda_kda_enabled() -> bool {
102    !matches!(std::env::var("K3_CUDA_KDA").as_deref(), Ok("0"))
103}
104
105/// Per-sequence KDA state. Both buffers are FP32, read-modify-write.
106#[derive(Clone, Debug)]
107pub struct KdaState {
108    /// `[conv_dim, conv_kernel]` FP32.
109    pub conv: Vec<f32>,
110    /// `[heads, head_dim, head_dim]` FP32, K-major.
111    pub recurrent: Vec<f32>,
112}
113
114impl KdaState {
115    pub fn new(cfg: &KdaConfig) -> Self {
116        Self {
117            conv: vec![0.0; cfg.conv_elems()],
118            recurrent: vec![0.0; cfg.recurrent_elems()],
119        }
120    }
121}
122
123/// Causal depthwise conv + SiLU. Shifts Atlas-width state left, writes `x`
124/// into the last slot, then `y[c] = silu(dot(w[c], state[c]))`.
125pub fn conv_update(
126    state: &mut [f32],
127    x: &[f32],
128    w: &[f32],
129    channels: usize,
130    kernel: usize,
131) -> Vec<f32> {
132    assert_eq!(x.len(), channels);
133    assert_eq!(state.len(), channels * kernel);
134    assert_eq!(w.len(), channels * kernel);
135    let mut y = vec![0.0f32; channels];
136    for c in 0..channels {
137        let row = c * kernel;
138        for k in 0..kernel - 1 {
139            state[row + k] = state[row + k + 1];
140        }
141        state[row + kernel - 1] = x[c];
142        let mut acc = 0.0f32;
143        for k in 0..kernel {
144            acc += w[row + k] * state[row + k];
145        }
146        y[c] = acc * sigmoid(acc); // SiLU
147    }
148    y
149}
150
151/// Stable `log(1+exp(x))`.
152fn softplus(x: f32) -> f32 {
153    let ax = x.abs();
154    x.max(0.0) + (-ax).exp().ln_1p()
155}
156
157/// KDA forget-gate in log space (FLA `use_gate_in_kernel`).
158///
159/// * `Some(lb)`: `lb * sigmoid(exp(A_log) * (z + dt_bias))` (production `-5`)
160/// * `None`: `-exp(A_log) * softplus(z + dt_bias)` (0.40B twin / FLA default)
161pub fn bounded_gate(
162    z: &[f32],
163    dt_bias: &[f32],
164    a_log: &[f32],
165    heads: usize,
166    head_dim: usize,
167    lower_bound: Option<f32>,
168) -> Vec<f32> {
169    let mut out = vec![0.0f32; heads * head_dim];
170    for h in 0..heads {
171        let decay = a_log[h].exp();
172        for d in 0..head_dim {
173            let ch = h * head_dim + d;
174            let x = z[ch] + dt_bias[ch];
175            out[ch] = match lower_bound {
176                Some(lb) => lb * sigmoid(decay * x),
177                None => -decay * softplus(x),
178            };
179        }
180    }
181    out
182}
183
184fn l2norm_rows(x: &[f32], d: usize, eps: f32) -> Vec<f32> {
185    let mut out = vec![0.0f32; x.len()];
186    for (row_in, row_out) in x.chunks_exact(d).zip(out.chunks_exact_mut(d)) {
187        let inv = 1.0 / (row_in.iter().map(|v| v * v).sum::<f32>() + eps).sqrt();
188        for (o, i) in row_out.iter_mut().zip(row_in) {
189            *o = i * inv;
190        }
191    }
192    out
193}
194
195/// One-token KDA core. `qkv` is post-conv `[3 * qkv_dim]` (q|k|v).
196/// Updates `state.recurrent` in place. q/k are L2-normalised here.
197pub fn kda_recurrent_step(
198    qkv: &[f32],
199    gate: &[f32],
200    beta: &[f32],
201    cfg: &KdaConfig,
202    recurrent: &mut [f32],
203) -> Vec<f32> {
204    let (h_n, d) = (cfg.heads, cfg.head_dim);
205    let qkv_dim = h_n * d;
206    let q = l2norm_rows(&qkv[..qkv_dim], d, KDA_L2_EPS);
207    let k = l2norm_rows(&qkv[qkv_dim..2 * qkv_dim], d, KDA_L2_EPS);
208    let v = &qkv[2 * qkv_dim..3 * qkv_dim];
209    let scale = 1.0 / (d as f32).sqrt();
210    let mut out = vec![0.0f32; qkv_dim];
211    let mut delta = vec![0.0f32; d];
212    for h in 0..h_n {
213        let base = h * d;
214        let s = &mut recurrent[h * d * d..(h + 1) * d * d];
215        for kd in 0..d {
216            let decay = gate[base + kd].exp();
217            for vd in 0..d {
218                s[kd * d + vd] *= decay;
219            }
220        }
221        let b = sigmoid(beta[h]);
222        for vd in 0..d {
223            let mut kv = 0.0f32;
224            for kd in 0..d {
225                kv += s[kd * d + vd] * k[base + kd];
226            }
227            delta[vd] = (v[base + vd] - kv) * b;
228        }
229        for kd in 0..d {
230            let kk = k[base + kd];
231            for vd in 0..d {
232                s[kd * d + vd] += kk * delta[vd];
233            }
234        }
235        for vd in 0..d {
236            let mut acc = 0.0f32;
237            for kd in 0..d {
238                acc += s[kd * d + vd] * q[base + kd] * scale;
239            }
240            out[base + vd] = acc;
241        }
242    }
243    out
244}
245
246/// Full-rank output gate: `sigmoid(g) ⊙ RMSNorm(core)` per head.
247pub fn full_rank_output_gate(core: &[f32], g: &[f32], head_dim: usize, eps: f32) -> Vec<f32> {
248    assert_eq!(core.len(), g.len());
249    let mut out = vec![0.0f32; core.len()];
250    for (row_c, (row_g, row_o)) in core
251        .chunks_exact(head_dim)
252        .zip(g.chunks_exact(head_dim).zip(out.chunks_exact_mut(head_dim)))
253    {
254        let mean_sq = row_c.iter().map(|v| v * v).sum::<f32>() / head_dim as f32;
255        let inv = 1.0 / (mean_sq + eps).sqrt();
256        for i in 0..head_dim {
257            row_o[i] = sigmoid(row_g[i]) * row_c[i] * inv;
258        }
259    }
260    out
261}
262
263/// One decode token: conv update then recurrent step.
264pub fn kda_decode_token(
265    x_qkv: &[f32],
266    conv_w: &[f32],
267    gate: &[f32],
268    beta: &[f32],
269    cfg: &KdaConfig,
270    state: &mut KdaState,
271) -> Vec<f32> {
272    let conv_out = conv_update(
273        &mut state.conv,
274        x_qkv,
275        conv_w,
276        cfg.conv_dim(),
277        cfg.conv_kernel,
278    );
279    kda_recurrent_step(&conv_out, gate, beta, cfg, &mut state.recurrent)
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    fn tiny() -> KdaConfig {
287        KdaConfig {
288            heads: 1,
289            head_dim: 2,
290            conv_kernel: 4,
291            gate_lower_bound: Some(-5.0),
292            use_full_rank_gate: true,
293        }
294    }
295
296    #[test]
297    fn cuda_kda_env_default_on() {
298        if std::env::var_os("K3_CUDA_KDA").is_some() {
299            return;
300        }
301        assert!(
302            cuda_kda_enabled(),
303            "LinearAttention default is CUDA KDA; K3_CUDA_KDA=0 is the CPU escape"
304        );
305    }
306
307    #[test]
308    fn cuda_kda_env_opt_out() {
309        const THIS: &str = "kimi_k3::kda::tests::cuda_kda_env_opt_out";
310        const MARKER: &str = "K3_CUDA_KDA_CHILD";
311        if std::env::var_os(MARKER).is_some() {
312            assert!(!cuda_kda_enabled(), "K3_CUDA_KDA=0 must keep the CPU mixer");
313            return;
314        }
315        let output = std::process::Command::new(std::env::current_exe().unwrap())
316            .args(["--exact", THIS])
317            .env(MARKER, "1")
318            .env("K3_CUDA_KDA", "0")
319            .output()
320            .unwrap();
321        assert!(
322            output.status.success(),
323            "K3_CUDA_KDA child failed:\n{}",
324            String::from_utf8_lossy(&output.stderr)
325        );
326    }
327
328    #[test]
329    fn production_geometry() {
330        let p = KdaConfig::production();
331        assert_eq!(p.head_dim, 128);
332        assert_eq!(p.conv_kernel, 4);
333        assert!(p.use_full_rank_gate);
334        assert_eq!(p.gate_lower_bound, Some(-5.0));
335    }
336
337    #[test]
338    fn kda_from_twin_json_is_unbounded() {
339        let twin = include_str!("../../../../docs/k3/fixtures/Kimi-K3-0.40B-config.json");
340        let official = include_str!("../../../../docs/k3/fixtures/moonshotai-Kimi-K3-config.json");
341        let t = crate::config::parse_config(twin).expect("twin json");
342        let k = kda_from(&t);
343        assert_eq!(
344            k.gate_lower_bound, None,
345            "twin omits the key → FLA unbounded"
346        );
347        assert_eq!(k.heads, 8);
348        assert_eq!(k.head_dim, 32);
349        assert_eq!(k.conv_kernel, 4);
350        assert!(k.use_full_rank_gate);
351        let p = crate::config::parse_config(official).expect("official json");
352        assert_eq!(kda_from(&p).gate_lower_bound, Some(-5.0));
353        assert_eq!(kda_from(&p).heads, 96);
354        assert_eq!(kda_from(&p).head_dim, 128);
355    }
356
357    #[test]
358    fn omitted_lower_bound_is_neg_exp_a_softplus() {
359        let z = [0.5f32, -0.25];
360        let dt = [0.1, 0.0];
361        let a_log = [0.0f32]; // exp(A_log) = 1
362        let g = bounded_gate(&z, &dt, &a_log, 1, 2, None);
363        let sp = |x: f32| x.max(0.0) + (-x.abs()).exp().ln_1p();
364        assert!((g[0] - (-sp(0.6))).abs() < 1e-6);
365        assert!((g[1] - (-sp(-0.25))).abs() < 1e-6);
366        let g5 = bounded_gate(&z, &dt, &a_log, 1, 2, Some(-5.0));
367        assert!(
368            (g5[0] - g[0]).abs() > 0.1,
369            "safe-gate -5 must diverge from unbounded FLA default"
370        );
371    }
372
373    #[test]
374    fn conv_kernel_4_state_advances() {
375        let cfg = tiny();
376        let ch = cfg.conv_dim(); // 6
377        let k = cfg.conv_kernel;
378        let mut state = vec![0.0f32; ch * k];
379        let w = vec![1.0f32; ch * k];
380        for t in 0..4u32 {
381            let x = vec![(t + 1) as f32; ch];
382            let _y = conv_update(&mut state, &x, &w, ch, k);
383            // Last slot is the current sample.
384            for c in 0..ch {
385                assert_eq!(state[c * k + (k - 1)], x[c], "t={t} last slot");
386            }
387        }
388        // After 4 distinct tokens the window holds 1,2,3,4 (oldest → newest).
389        for c in 0..ch {
390            let row = &state[c * k..(c + 1) * k];
391            assert_eq!(row, &[1.0, 2.0, 3.0, 4.0]);
392        }
393    }
394
395    #[test]
396    fn prefix_hit_wrong_slot_diverges() {
397        // C4 seed: restore the conv/recurrent state from the wrong slot
398        // after a prefix hit, and the next decode must move.
399        let cfg = tiny();
400        let ch = cfg.conv_dim();
401        let mut correct = KdaState::new(&cfg);
402        let conv_w = vec![0.25f32; cfg.conv_elems()];
403        let gate = bounded_gate(
404            &[0.1, -0.2],
405            &[0.0, 0.0],
406            &[-1.0],
407            cfg.heads,
408            cfg.head_dim,
409            cfg.gate_lower_bound,
410        );
411        let beta = [0.5f32];
412        let mut snapshots = Vec::new();
413        for t in 0..2u32 {
414            let x = vec![(t + 1) as f32 * 0.1; ch];
415            let _ = kda_decode_token(&x, &conv_w, &gate, &beta, &cfg, &mut correct);
416            snapshots.push(correct.clone());
417        }
418        // Sequential token 3 from the real prefix (after two tokens).
419        let x3 = vec![0.4f32; ch];
420        let y_seq = kda_decode_token(&x3, &conv_w, &gate, &beta, &cfg, &mut correct);
421        // Prefix hit: restore snapshot after token 2, decode the same x3.
422        let mut from_prefix = snapshots[1].clone();
423        let y_hit = kda_decode_token(&x3, &conv_w, &gate, &beta, &cfg, &mut from_prefix);
424        assert_eq!(y_hit, y_seq, "correct slot must match sequential decode");
425        assert_eq!(from_prefix.conv, correct.conv);
426        // Wrong slot: restore snapshot after token 1 (prefix-hit then wrong state).
427        let mut wrong = snapshots[0].clone();
428        let y_wrong = kda_decode_token(&x3, &conv_w, &gate, &beta, &cfg, &mut wrong);
429        let err: f32 = y_hit
430            .iter()
431            .zip(&y_wrong)
432            .map(|(a, b)| (a - b).abs())
433            .fold(0.0, f32::max);
434        assert!(
435            err > 1e-4,
436            "wrong-slot restore must diverge (max abs {err}), hit={y_hit:?} wrong={y_wrong:?}"
437        );
438    }
439
440    #[test]
441    fn beta_zero_is_sigmoid_half_not_zero() {
442        // HF fused_recurrent_kda: use_beta_sigmoid_in_kernel=True.
443        // Raw beta=0 must still write a delta (sigmoid(0)=0.5), not skip.
444        let cfg = tiny();
445        let qkv = [1.0f32, 0.0, 1.0, 0.0, 1.0, 0.5];
446        let gate = [-1.0f32, -1.0];
447        let mut rec_zero = vec![0.0f32; cfg.recurrent_elems()];
448        let mut rec_raw = rec_zero.clone();
449        let o_sig = kda_recurrent_step(&qkv, &gate, &[0.0], &cfg, &mut rec_zero);
450        let o_raw_one = kda_recurrent_step(&qkv, &gate, &[20.0], &cfg, &mut rec_raw);
451        let err: f32 = o_sig.iter().map(|v| v.abs()).fold(0.0, f32::max);
452        assert!(
453            err > 1e-6,
454            "sigmoid(0)=0.5 must update the state, got {o_sig:?}"
455        );
456        assert_ne!(o_sig, o_raw_one, "saturated beta must differ from beta=0");
457    }
458}