atlas_core/kimi_k3/
latent_moe.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Stable LatentMoE CPU reference.
4//!
5//! ```text
6//! latent = down_proj(h)                 // hidden → moe_latent (no pre-norm)
7//! scores = sigmoid(router(h)); top-k on scores+bias  // noaux_tc
8//! y = Σ w_i expert_i(latent)            // SiTU-GLU experts
9//! y = RMSNorm(y) if latent_moe_use_norm; y = up_proj(y)
10//! out = y + shared_experts(h)           // shared stay full-width
11//! ```
12
13#![allow(clippy::too_many_arguments)]
14
15use super::attnres::rms_norm;
16use super::situ::{sigmoid, situ_glu_vec};
17use crate::config::ModelConfig;
18
19#[derive(Clone, Copy, Debug)]
20pub struct LatentMoeConfig {
21    pub hidden: usize,
22    pub latent: usize,
23    pub expert_hidden: usize,
24    pub n_routed: usize,
25    pub top_k: usize,
26    pub n_shared: usize,
27    pub situ_beta: f32,
28    pub situ_linear_beta: f32,
29    pub use_norm: bool,
30    pub renormalize: bool,
31}
32
33impl LatentMoeConfig {
34    pub fn production() -> Self {
35        Self {
36            hidden: 7168,
37            latent: 3584,
38            expert_hidden: 3072,
39            n_routed: 896,
40            top_k: 16,
41            n_shared: 2,
42            situ_beta: 4.0,
43            situ_linear_beta: 25.0,
44            use_norm: true,
45            renormalize: true,
46        }
47    }
48}
49
50pub fn moe_from(c: &ModelConfig) -> LatentMoeConfig {
51    LatentMoeConfig {
52        hidden: c.hidden_size,
53        latent: c.moe_latent_size,
54        expert_hidden: c.moe_intermediate_size,
55        n_routed: c.num_experts,
56        top_k: c.num_experts_per_tok,
57        n_shared: c.n_shared_experts,
58        situ_beta: c.activation_situ_beta,
59        situ_linear_beta: c.activation_situ_linear_beta,
60        use_norm: c.latent_moe_use_norm,
61        renormalize: c.norm_topk_prob,
62    }
63}
64
65/// noaux_tc: sigmoid(logits) for mix weights; `scores + bias` only ranks.
66pub fn sigmoid_topk(logits: &[f32], bias: &[f32], k: usize) -> (Vec<usize>, Vec<f32>) {
67    assert_eq!(logits.len(), bias.len());
68    let n = logits.len();
69    let k = k.min(n);
70    let scores: Vec<f32> = logits.iter().map(|l| sigmoid(*l)).collect();
71    let mut idx: Vec<usize> = (0..n).collect();
72    idx.sort_by(|&a, &b| {
73        let ca = scores[a] + bias[a];
74        let cb = scores[b] + bias[b];
75        cb.partial_cmp(&ca)
76            .unwrap_or(std::cmp::Ordering::Equal)
77            .then(a.cmp(&b))
78    });
79    idx.truncate(k);
80    let mut w: Vec<f32> = idx.iter().map(|&i| scores[i]).collect();
81    let z: f32 = w.iter().sum();
82    if z > 0.0 {
83        for ww in &mut w {
84            *ww /= z;
85        }
86    }
87    (idx, w)
88}
89
90/// One SiTU-GLU expert: `down( situ(w1 x, w3 x) )`. Weights are `[out, in]`.
91pub fn expert_situ(
92    x: &[f32],
93    w1: &[f32],
94    w2: &[f32],
95    w3: &[f32],
96    in_dim: usize,
97    hidden: usize,
98    beta: f32,
99    beta_lin: f32,
100) -> Vec<f32> {
101    let gate = matvec(w1, x, hidden, in_dim);
102    let up = matvec(w3, x, hidden, in_dim);
103    let mid = situ_glu_vec(&gate, &up, beta, beta_lin);
104    matvec(w2, &mid, in_dim, hidden)
105}
106
107/// Mix selected SiTU-GLU experts into latent. Empty `w1` means packed-only
108/// (no host dequant) — CUDA grouped GEMM must have run instead.
109pub fn mix_routed_experts(
110    latent: &[f32],
111    ids: &[usize],
112    weights: &[f32],
113    experts: &[(Vec<f32>, Vec<f32>, Vec<f32>)],
114    cfg: &LatentMoeConfig,
115) -> Vec<f32> {
116    let mut mixed = vec![0.0f32; cfg.latent];
117    for (&id, &w) in ids.iter().zip(weights) {
118        assert!(id < experts.len(), "expert id {id} >= {}", experts.len());
119        let (w1, w2, w3) = &experts[id];
120        assert!(
121            !w1.is_empty(),
122            "K3 packed expert {id} has no host w1; CUDA grouped GEMM required"
123        );
124        let eh = if cfg.latent == 0 {
125            cfg.expert_hidden
126        } else {
127            w1.len() / cfg.latent
128        };
129        let y = expert_situ(
130            latent,
131            w1,
132            w2,
133            w3,
134            cfg.latent,
135            eh,
136            cfg.situ_beta,
137            cfg.situ_linear_beta,
138        );
139        for (m, yy) in mixed.iter_mut().zip(y) {
140            *m += w * yy;
141        }
142    }
143    mixed
144}
145
146fn matvec(w: &[f32], x: &[f32], out: usize, inn: usize) -> Vec<f32> {
147    assert_eq!(w.len(), out * inn);
148    assert_eq!(x.len(), inn);
149    let mut y = vec![0.0f32; out];
150    for o in 0..out {
151        let mut acc = 0.0f32;
152        let row = &w[o * inn..(o + 1) * inn];
153        for i in 0..inn {
154            acc += row[i] * x[i];
155        }
156        y[o] = acc;
157    }
158    y
159}
160
161/// Routed latent path + optional shared expert (identity-scale for tests).
162pub fn latent_moe_forward(
163    h: &[f32],
164    down: &[f32],
165    up: &[f32],
166    norm_w: &[f32],
167    logits: &[f32],
168    bias: &[f32],
169    experts: &[(Vec<f32>, Vec<f32>, Vec<f32>)],
170    shared: Option<&[f32]>,
171    cfg: &LatentMoeConfig,
172    eps: f32,
173) -> (Vec<f32>, Vec<usize>) {
174    let latent = matvec(down, h, cfg.latent, cfg.hidden);
175    let (ids, weights) = sigmoid_topk(logits, bias, cfg.top_k);
176    let mut mixed = mix_routed_experts(&latent, &ids, &weights, experts, cfg);
177    if cfg.use_norm {
178        mixed = rms_norm(&mixed, norm_w, eps);
179    }
180    let mut out = matvec(up, &mixed, cfg.hidden, cfg.latent);
181    if let Some(s) = shared {
182        for (o, ss) in out.iter_mut().zip(s) {
183            *o += ss;
184        }
185    }
186    (out, ids)
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn sigmoid_topk_picks_highest() {
195        let logits = [0.1, 5.0, 0.2, 4.0];
196        let bias = [0.0, 0.0, 0.0, 0.0];
197        let (ids, w) = sigmoid_topk(&logits, &bias, 2);
198        assert_eq!(ids, vec![1, 3]);
199        assert!((w[0] + w[1] - 1.0).abs() < 1e-6);
200        assert!(w[0] > w[1]);
201    }
202
203    #[test]
204    fn noaux_tc_bias_ranks_not_weighted() {
205        // HF: scores = sigmoid(logits); top-k on scores+bias; mix uses scores.
206        let logits = [0.0f32, 0.0, 0.0];
207        let bias = [0.0, 5.0, 4.0];
208        let (ids, w) = sigmoid_topk(&logits, &bias, 2);
209        assert_eq!(ids, vec![1, 2]);
210        assert!(
211            (w[0] - w[1]).abs() < 1e-5,
212            "equal logits must keep equal mix weights, got {w:?}"
213        );
214    }
215
216    #[test]
217    fn force_expert_zero_mutant_diverges() {
218        // C6 known-bad: forcing expert 0 must change the mix vs true top-k.
219        let cfg = LatentMoeConfig {
220            hidden: 2,
221            latent: 2,
222            expert_hidden: 2,
223            n_routed: 2,
224            top_k: 1,
225            n_shared: 0,
226            situ_beta: 4.0,
227            situ_linear_beta: 25.0,
228            use_norm: false,
229            renormalize: true,
230        };
231        let h = vec![1.0, 0.0];
232        let down = vec![1.0, 0.0, 0.0, 1.0];
233        let up = down.clone();
234        let ident = |out: usize, inn: usize| -> Vec<f32> {
235            let mut w = vec![0.0; out * inn];
236            for i in 0..out.min(inn) {
237                w[i * inn + i] = 1.0;
238            }
239            w
240        };
241        let e0 = (ident(2, 2), ident(2, 2), ident(2, 2));
242        // Expert 1 scales up-branch so SiTU output differs.
243        let mut w3 = ident(2, 2);
244        w3[0] = 3.0;
245        let e1 = (ident(2, 2), ident(2, 2), w3);
246        let experts = [e0, e1];
247        let logits = [0.0, 4.0];
248        let bias = [0.0, 0.0];
249        let (y, ids) = latent_moe_forward(
250            &h,
251            &down,
252            &up,
253            &[1.0, 1.0],
254            &logits,
255            &bias,
256            &experts,
257            None,
258            &cfg,
259            1e-5,
260        );
261        assert_eq!(ids, vec![1]);
262        let (y0, _) = latent_moe_forward(
263            &h,
264            &down,
265            &up,
266            &[1.0, 1.0],
267            &[4.0, 0.0],
268            &bias,
269            &experts,
270            None,
271            &cfg,
272            1e-5,
273        );
274        let err: f32 = y.iter().zip(&y0).map(|(a, b)| (a - b).abs()).sum();
275        assert!(err > 1e-4, "forcing expert 0 must diverge, err={err}");
276    }
277}