Skip to main content

ferrox_models/
latent_moe.rs

1//! Kimi K3's "latent MoE" block (`KimiSparseMoeBlock` in the real
2//! `modeling_kimi_linear.py`), a real architectural detail beyond a
3//! standard top-k MoE FFN, discovered by reading the real source rather
4//! than assuming the more common DeepSeek-V3-style MoE this crate's
5//! `ferrox_moe::route_top_k` was originally written for: routed experts
6//! operate on a *down-projected* latent space
7//! (`routed_expert_hidden_size` = 3584, half of `hidden_size` = 7168 in
8//! Kimi K3's real config — `use_latent_moe`/`latent_moe_use_norm` are
9//! real, confirmed-active config fields, not a rare/optional path), not
10//! the full hidden dimension. Real per-layer flow:
11//!
12//! ```text
13//! identity = hidden
14//! (topk_idx, topk_weight) = gate(hidden)              // on FULL hidden, not the latent
15//! latent = down_proj(hidden)
16//! combined = sum_i topk_weight[i] * expert[topk_idx[i]](latent)  // in latent space
17//! combined = rms_norm(combined)                        // iff latent_moe_use_norm
18//! routed_out = up_proj(combined)                        // back to hidden_size
19//! output = routed_out + shared_expert(identity)         // shared expert on FULL hidden
20//! ```
21//!
22//! Routing uses `ferrox_moe::route_top_k_sigmoid_with_bias` (the real
23//! "aux-loss-free" per-expert bias affects selection only, not the
24//! final combine weight — see that function's doc comment). Every
25//! expert (`KimiBlockSparseMLP`) and the shared expert (`KimiMLP`) use
26//! Kimi K3's real `situ` activation (`ferrox_core::situ_and_mul`), not
27//! the more common SwiGLU.
28//!
29//! Not yet wired into `Decoder`. Tested here against synthetic weights,
30//! cross-validated against an independent Python transcription of the
31//! same real algorithm.
32
33use ferrox_core::matmul::{rms_norm, situ_and_mul};
34use ferrox_core::weight_matrix::WeightMatrix;
35use ferrox_moe::route_top_k_sigmoid_with_bias;
36
37/// One expert's gate/up/down projections
38/// (`w1`=gate, `w3`=up, `w2`=down, matching `KimiBlockSparseMLP`'s real
39/// naming). Used both for routed experts (in the latent `moe_hidden_dim`
40/// space) and the shared expert (in the full `hidden_dim` space).
41pub struct KimiExpertWeights {
42    pub w1: WeightMatrix, // gate: [ffn_dim, in_dim]
43    pub w2: WeightMatrix, // down: [in_dim, ffn_dim]
44    pub w3: WeightMatrix, // up: [ffn_dim, in_dim]
45}
46
47impl KimiExpertWeights {
48    pub fn forward(&self, x: &[f32], situ_beta: f32, situ_linear_beta: f32) -> Vec<f32> {
49        let gate = self.w1.apply(x);
50        let up = self.w3.apply(x);
51        let combined = situ_and_mul(&gate, &up, situ_beta, situ_linear_beta);
52        self.w2.apply(&combined)
53    }
54}
55
56pub struct KimiLatentMoeWeights {
57    pub router_weight: WeightMatrix,       // [n_experts, hidden_dim]
58    pub e_score_correction_bias: Vec<f32>, // [n_experts]
59    pub down_proj: WeightMatrix,           // [moe_hidden_dim, hidden_dim]
60    pub up_proj: WeightMatrix,             // [hidden_dim, moe_hidden_dim]
61    /// Present iff `latent_moe_use_norm` (true for Kimi K3's real
62    /// config).
63    pub routed_expert_norm_weight: Option<Vec<f32>>, // [moe_hidden_dim]
64    pub experts: KimiExpertBacking,        // moe_hidden_dim <-> moe_intermediate_dim
65    pub shared_expert: KimiExpertWeights,  // hidden_dim <-> (moe_intermediate_dim*num_shared)
66}
67
68/// How a Kimi layer's routed experts are held: `Resident` is the
69/// original always-constructed form (zero-copy MXFP4 mmap views);
70/// `Stored` holds only the per-layer byte layout and materializes one
71/// expert at a time from a bounded, lease-protected store shared by
72/// every layer -- same design (and same bit-equivalence argument) as
73/// the GGUF path's `ferrox_models::decoder::ExpertBacking`. Every
74/// expert in a Kimi layer has identical dims, so one layout serves the
75/// whole layer.
76pub enum KimiExpertBacking {
77    Resident(Vec<KimiExpertWeights>),
78    Stored {
79        store: std::sync::Arc<
80            ferrox_core::expert_store::ExpertStore<crate::kimi_loader::KimiExpertSource>,
81        >,
82        layout: crate::kimi_loader::KimiStoredExpertLayout,
83        n_experts: usize,
84        layer: u32,
85    },
86}
87
88impl KimiExpertBacking {
89    pub fn n_experts(&self) -> usize {
90        match self {
91            KimiExpertBacking::Resident(v) => v.len(),
92            KimiExpertBacking::Stored { n_experts, .. } => *n_experts,
93        }
94    }
95
96    /// Runs `f` against expert `e`, materializing it from the store
97    /// first when store-backed (the lease pins the cache entry for
98    /// exactly `f`'s borrow).
99    pub fn with_expert<R>(&self, e: usize, f: impl FnOnce(&KimiExpertWeights) -> R) -> R {
100        match self {
101            KimiExpertBacking::Resident(v) => f(&v[e]),
102            KimiExpertBacking::Stored {
103                store,
104                layout,
105                layer,
106                ..
107            } => {
108                let lease = store
109                    .acquire(ferrox_core::expert_store::ExpertKey {
110                        layer: *layer,
111                        expert: e as u32,
112                    })
113                    .unwrap_or_else(|err| {
114                        panic!(
115                            "kimi expert store read failed for layer {layer} expert {e}: {err} \
116                             (checkpoint file unreadable mid-decode)"
117                        )
118                    });
119                let tmp = layout.materialize(&lease);
120                f(&tmp)
121            }
122        }
123    }
124}
125
126pub struct KimiMoeConfig {
127    pub n_experts_active: usize,
128    pub moe_renormalize: bool,
129    pub routed_scaling_factor: f32,
130    pub situ_beta: f32,
131    pub situ_linear_beta: f32,
132    pub rms_norm_eps: f32,
133}
134
135pub fn kimi_latent_moe_forward(
136    weights: &KimiLatentMoeWeights,
137    cfg: &KimiMoeConfig,
138    hidden: &[f32],
139) -> Vec<f32> {
140    let router_logits = weights.router_weight.apply(hidden);
141    let decision = route_top_k_sigmoid_with_bias(
142        &router_logits,
143        &weights.e_score_correction_bias,
144        cfg.n_experts_active,
145        cfg.moe_renormalize,
146        cfg.routed_scaling_factor,
147    );
148
149    let latent = weights.down_proj.apply(hidden);
150
151    let mut combined = vec![0f32; latent.len()];
152    for (&eid, &w) in decision.expert_ids.iter().zip(decision.weights.iter()) {
153        let out = weights.experts.with_expert(eid, |ex| {
154            ex.forward(&latent, cfg.situ_beta, cfg.situ_linear_beta)
155        });
156        for (c, o) in combined.iter_mut().zip(out.iter()) {
157            *c += w * o;
158        }
159    }
160
161    let normed = match &weights.routed_expert_norm_weight {
162        Some(w) => rms_norm(&combined, w, cfg.rms_norm_eps),
163        None => combined,
164    };
165
166    let mut out = weights.up_proj.apply(&normed);
167    let shared = weights
168        .shared_expert
169        .forward(hidden, cfg.situ_beta, cfg.situ_linear_beta);
170    for (o, s) in out.iter_mut().zip(shared.iter()) {
171        *o += s;
172    }
173    out
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use ferrox_core::tensor::Tensor;
180
181    const HIDDEN_DIM: usize = 6;
182    const MOE_HIDDEN_DIM: usize = 4;
183    const MOE_INTERMEDIATE: usize = 3;
184    const N_EXPERTS: usize = 4;
185    const TOP_K: usize = 2;
186    const SHARED_INTERMEDIATE: usize = 3; // MOE_INTERMEDIATE * num_shared(1)
187    const SITU_BETA: f32 = 4.0;
188    const SITU_LINEAR_BETA: f32 = 25.0;
189    const NORM_EPS: f32 = 1e-5;
190
191    fn wm(data: &[f32], rows: usize, cols: usize) -> WeightMatrix {
192        assert_eq!(data.len(), rows * cols);
193        WeightMatrix::F32(Tensor::new(data.to_vec(), vec![rows, cols]))
194    }
195
196    // Generated by an independent Python reference -- do not hand-edit.
197    const LMOE_ROUTER_WEIGHT: [f32; 24] = [
198        0.0102578, 0.407924, 0.367416, -0.153092, -0.0893909, -0.158215, 0.170918, -0.0168193,
199        0.224066, -0.554197, 0.469965, -0.0289296, 0.204114, -0.0409699, -0.11373, 0.138933,
200        0.247354, -0.060759, -0.0458359, 0.20571, -0.261102, -0.454315, 0.118495, -0.20117,
201    ];
202    const LMOE_BIAS: [f32; 4] = [-0.96017, -0.407027, -0.233799, -0.596601];
203    const LMOE_DOWN_PROJ: [f32; 24] = [
204        -0.447739, 0.0109913, 0.269175, -0.0699396, -0.223079, 0.115498, 0.215171, -0.0900032,
205        0.1634, 0.312863, -0.0620869, -0.244055, 0.104295, 0.0742637, 0.329644, -0.385374,
206        -0.198484, -0.25145, -0.520204, 0.0379304, 0.158341, -0.221637, 0.415694, 0.246577,
207    ];
208    const LMOE_UP_PROJ: [f32; 24] = [
209        0.188213,
210        0.120512,
211        0.286701,
212        -0.399594,
213        0.184179,
214        0.180833,
215        -0.530316,
216        0.104109,
217        -0.0751264,
218        0.234457,
219        -0.131719,
220        -0.00547226,
221        0.102855,
222        -0.262879,
223        0.179579,
224        -0.031489,
225        0.147745,
226        -0.156551,
227        0.32586,
228        0.181561,
229        -0.0534075,
230        0.189587,
231        0.377927,
232        0.537353,
233    ];
234    const LMOE_ROUTED_NORM_W: [f32; 4] = [0.842642, 1.08831, 1.04651, 0.990614];
235    const LMOE_E0_W1: [f32; 12] = [
236        -0.302, 0.377157, -0.378521, 0.170084, 0.39056, -0.479901, -0.0907554, -0.392765,
237        0.0732162, 0.454313, 0.607067, -0.533434,
238    ];
239    const LMOE_E0_W2: [f32; 12] = [
240        -0.172485,
241        0.211064,
242        0.473812,
243        0.126363,
244        -0.223846,
245        0.0891395,
246        -0.00498576,
247        -0.0611222,
248        -0.220341,
249        0.116178,
250        0.0923639,
251        -0.0278952,
252    ];
253    const LMOE_E0_W3: [f32; 12] = [
254        -0.0665064, -0.385475, -0.145853, 0.361935, -0.0571675, -0.431909, 0.400333, 0.15908,
255        0.632426, 0.0187537, -0.138415, -0.434293,
256    ];
257    const LMOE_E1_W1: [f32; 12] = [
258        0.397152, 0.770853, -0.24628, -0.194126, 0.178857, -0.249104, -0.0811675, -0.104951,
259        0.0575877, 0.328446, 0.00662028, 0.275673,
260    ];
261    const LMOE_E1_W2: [f32; 12] = [
262        -0.125967, 0.0983396, -0.641468, -0.434984, 0.238774, -0.177045, 0.173974, 0.162703,
263        0.396684, 0.243558, 0.305097, -0.0335014,
264    ];
265    const LMOE_E1_W3: [f32; 12] = [
266        -0.209486, -0.219468, -0.146413, -0.338949, -0.164233, -0.0277707, 0.0754836, -0.101667,
267        -0.577105, -0.0216849, 0.0676037, 0.325343,
268    ];
269    const LMOE_E2_W1: [f32; 12] = [
270        0.173359, -0.193068, -0.217133, 0.603179, 0.226991, 0.549432, 0.638833, -0.245425,
271        0.115581, 0.13745, 0.167884, 0.162571,
272    ];
273    const LMOE_E2_W2: [f32; 12] = [
274        0.0606164, 0.0522388, -0.45075, -0.0496204, -0.224286, 0.0378262, -0.140264, 0.185556,
275        0.245723, 0.0926077, 0.0948504, 0.027884,
276    ];
277    const LMOE_E2_W3: [f32; 12] = [
278        -0.134339, -0.0493504, -0.148694, 0.116396, 0.00423402, 0.174387, -0.398595, 0.266334,
279        -0.228809, -0.220285, -0.0592329, -0.169015,
280    ];
281    const LMOE_E3_W1: [f32; 12] = [
282        0.0873376, -0.172249, -0.320843, -0.253741, 0.393595, 0.0132775, -0.350259, 0.00251322,
283        -0.466784, 0.53687, -0.457343, 0.143626,
284    ];
285    const LMOE_E3_W2: [f32; 12] = [
286        0.163088, -0.435002, 0.090083, 0.299152, 0.140273, 0.0783049, 0.284729, 0.0482714,
287        0.100928, -0.0380135, 0.189542, -0.282415,
288    ];
289    const LMOE_E3_W3: [f32; 12] = [
290        0.237529, -0.15159, -0.327196, 0.109575, 0.507887, 0.288503, -0.154695, 0.209418,
291        -0.136263, -0.0372058, 0.0268585, -0.695166,
292    ];
293    const LMOE_SHARED_W1: [f32; 18] = [
294        0.0574844,
295        -0.308883,
296        -0.20922,
297        -0.442317,
298        -0.454967,
299        -0.282979,
300        0.247679,
301        0.499812,
302        -0.00756153,
303        0.327537,
304        -0.0791942,
305        -0.573592,
306        0.044975,
307        0.133694,
308        -0.128585,
309        0.0906673,
310        0.171767,
311        -0.259022,
312    ];
313    const LMOE_SHARED_W2: [f32; 18] = [
314        -0.44305, -0.0663763, -0.0633182, -0.105799, 0.296174, 0.517677, -0.124994, 0.209666,
315        0.282279, 0.213947, 0.314246, -0.116773, 0.221942, 0.607345, 0.2411, -0.185968, 0.182314,
316        0.378714,
317    ];
318    const LMOE_SHARED_W3: [f32; 18] = [
319        0.110696,
320        -0.16902,
321        0.462924,
322        -0.37493,
323        0.151028,
324        -0.00485267,
325        -0.3581,
326        0.365835,
327        0.104366,
328        -0.350039,
329        0.180519,
330        -0.12948,
331        -0.570837,
332        -0.206672,
333        0.0790654,
334        0.190711,
335        0.0496695,
336        0.0135495,
337    ];
338
339    const LMOE_HIDDEN: [f32; 6] = [0.234995, -0.105488, 0.588477, 0.0743208, 0.146961, 0.270399];
340    const LMOE_GOLDEN_OUT: [f32; 6] = [
341        0.061351, 0.0969791, 0.0499038, -0.052412, -0.0592386, -0.0464002,
342    ];
343
344    fn expert(
345        w1: &[f32],
346        w2: &[f32],
347        w3: &[f32],
348        ffn_dim: usize,
349        in_dim: usize,
350    ) -> KimiExpertWeights {
351        KimiExpertWeights {
352            w1: wm(w1, ffn_dim, in_dim),
353            w2: wm(w2, in_dim, ffn_dim),
354            w3: wm(w3, ffn_dim, in_dim),
355        }
356    }
357
358    fn make_weights() -> KimiLatentMoeWeights {
359        KimiLatentMoeWeights {
360            router_weight: wm(&LMOE_ROUTER_WEIGHT, N_EXPERTS, HIDDEN_DIM),
361            e_score_correction_bias: LMOE_BIAS.to_vec(),
362            down_proj: wm(&LMOE_DOWN_PROJ, MOE_HIDDEN_DIM, HIDDEN_DIM),
363            up_proj: wm(&LMOE_UP_PROJ, HIDDEN_DIM, MOE_HIDDEN_DIM),
364            routed_expert_norm_weight: Some(LMOE_ROUTED_NORM_W.to_vec()),
365            experts: KimiExpertBacking::Resident(vec![
366                expert(
367                    &LMOE_E0_W1,
368                    &LMOE_E0_W2,
369                    &LMOE_E0_W3,
370                    MOE_INTERMEDIATE,
371                    MOE_HIDDEN_DIM,
372                ),
373                expert(
374                    &LMOE_E1_W1,
375                    &LMOE_E1_W2,
376                    &LMOE_E1_W3,
377                    MOE_INTERMEDIATE,
378                    MOE_HIDDEN_DIM,
379                ),
380                expert(
381                    &LMOE_E2_W1,
382                    &LMOE_E2_W2,
383                    &LMOE_E2_W3,
384                    MOE_INTERMEDIATE,
385                    MOE_HIDDEN_DIM,
386                ),
387                expert(
388                    &LMOE_E3_W1,
389                    &LMOE_E3_W2,
390                    &LMOE_E3_W3,
391                    MOE_INTERMEDIATE,
392                    MOE_HIDDEN_DIM,
393                ),
394            ]),
395            shared_expert: expert(
396                &LMOE_SHARED_W1,
397                &LMOE_SHARED_W2,
398                &LMOE_SHARED_W3,
399                SHARED_INTERMEDIATE,
400                HIDDEN_DIM,
401            ),
402        }
403    }
404
405    fn cfg() -> KimiMoeConfig {
406        KimiMoeConfig {
407            n_experts_active: TOP_K,
408            moe_renormalize: true,
409            routed_scaling_factor: 1.0,
410            situ_beta: SITU_BETA,
411            situ_linear_beta: SITU_LINEAR_BETA,
412            rms_norm_eps: NORM_EPS,
413        }
414    }
415
416    #[test]
417    fn matches_independent_python_reference() {
418        let weights = make_weights();
419        let cfg = cfg();
420        let out = kimi_latent_moe_forward(&weights, &cfg, &LMOE_HIDDEN);
421        assert_eq!(out.len(), LMOE_GOLDEN_OUT.len());
422        for (i, (a, b)) in out.iter().zip(LMOE_GOLDEN_OUT.iter()).enumerate() {
423            assert!((a - b).abs() < 1e-3, "element {i}: rust={a} python={b}");
424        }
425    }
426
427    #[test]
428    fn without_routed_norm_output_still_finite_and_differs() {
429        let mut weights = make_weights();
430        weights.routed_expert_norm_weight = None;
431        let cfg = cfg();
432        let out = kimi_latent_moe_forward(&weights, &cfg, &LMOE_HIDDEN);
433        assert!(out.iter().all(|v| v.is_finite()));
434        assert!((out[0] - LMOE_GOLDEN_OUT[0]).abs() > 1e-6);
435    }
436}