1#![allow(clippy::needless_range_loop)]
16
17use super::cache::MlaKv;
18use crate::config::ModelConfig;
19
20#[inline]
21fn sigmoid(x: f32) -> f32 {
22 1.0 / (1.0 + (-x).exp())
23}
24
25#[derive(Clone, Copy, Debug)]
26pub struct MlaConfig {
27 pub heads: usize,
28 pub qk_nope_head_dim: usize,
29 pub qk_rope_head_dim: usize,
30 pub v_head_dim: usize,
31 pub q_lora_rank: usize,
32 pub kv_lora_rank: usize,
33 pub mla_use_nope: bool,
34 pub mla_use_output_gate: bool,
35}
36
37impl MlaConfig {
38 pub fn production() -> Self {
39 Self {
40 heads: 96,
41 qk_nope_head_dim: 128,
42 qk_rope_head_dim: 64,
43 v_head_dim: 128,
44 q_lora_rank: 1536,
45 kv_lora_rank: 512,
46 mla_use_nope: true,
47 mla_use_output_gate: true,
48 }
49 }
50
51 pub fn qk_head_dim(&self) -> usize {
52 self.qk_nope_head_dim + self.qk_rope_head_dim
53 }
54
55 pub fn twin_0_40b() -> Self {
57 Self {
58 heads: 8,
59 qk_nope_head_dim: 64,
60 qk_rope_head_dim: 32,
61 v_head_dim: 64,
62 q_lora_rank: 256,
63 kv_lora_rank: 128,
64 mla_use_nope: true,
65 mla_use_output_gate: true,
66 }
67 }
68}
69
70pub fn mla_from(c: &ModelConfig) -> MlaConfig {
71 MlaConfig {
72 heads: c.num_attention_heads,
73 qk_nope_head_dim: c.qk_nope_head_dim,
74 qk_rope_head_dim: c.qk_rope_head_dim,
75 v_head_dim: c.v_head_dim,
76 q_lora_rank: c.q_lora_rank,
77 kv_lora_rank: c.kv_lora_rank,
78 mla_use_nope: c.mla_use_nope,
79 mla_use_output_gate: c.mla_use_output_gate,
80 }
81}
82
83pub fn cuda_mla_enabled() -> bool {
86 !matches!(std::env::var("K3_CUDA_MLA").as_deref(), Ok("0"))
87}
88
89pub fn maybe_rope(x: &mut [f32], nope: usize, rope: usize, pos: usize, theta: f32, use_nope: bool) {
92 if use_nope || rope == 0 {
93 return;
94 }
95 let dim = nope + rope;
96 for head in x.chunks_exact_mut(dim) {
97 let r = &mut head[nope..];
98 for i in 0..rope / 2 {
99 let freq = (pos as f32) / theta.powf(2.0 * i as f32 / rope as f32);
100 let (s, c) = freq.sin_cos();
101 let a = r[i];
102 let b = r[i + rope / 2];
103 r[i] = a * c - b * s;
104 r[i + rope / 2] = a * s + b * c;
105 }
106 }
107}
108
109pub fn sdpa(
111 q: &[f32],
112 k: &[f32],
113 v: &[f32],
114 t: usize,
115 heads: usize,
116 dq: usize,
117 dv: usize,
118) -> Vec<f32> {
119 let scale = 1.0 / (dq as f32).sqrt();
120 let mut out = vec![0.0f32; t * heads * dv];
121 for h in 0..heads {
122 for qi in 0..t {
123 let qrow = &q[(qi * heads + h) * dq..(qi * heads + h) * dq + dq];
124 let mut scores = vec![0.0f32; qi + 1];
125 let mut m = f32::NEG_INFINITY;
126 for kj in 0..=qi {
127 let krow = &k[(kj * heads + h) * dq..(kj * heads + h) * dq + dq];
128 let s: f32 = qrow.iter().zip(krow).map(|(a, b)| a * b).sum::<f32>() * scale;
129 scores[kj] = s;
130 if s > m {
131 m = s;
132 }
133 }
134 let mut z = 0.0f32;
135 for s in &mut scores {
136 *s = (*s - m).exp();
137 z += *s;
138 }
139 let orow = &mut out[(qi * heads + h) * dv..(qi * heads + h) * dv + dv];
140 for kj in 0..=qi {
141 let a = scores[kj] / z;
142 let vrow = &v[(kj * heads + h) * dv..(kj * heads + h) * dv + dv];
143 for d in 0..dv {
144 orow[d] += a * vrow[d];
145 }
146 }
147 }
148 }
149 out
150}
151
152pub fn sdpa_one(
154 q: &[f32],
155 k: &[f32],
156 v: &[f32],
157 t: usize,
158 heads: usize,
159 dq: usize,
160 dv: usize,
161) -> Vec<f32> {
162 assert_eq!(q.len(), heads * dq);
163 assert_eq!(k.len(), t * heads * dq);
164 assert_eq!(v.len(), t * heads * dv);
165 let scale = 1.0 / (dq as f32).sqrt();
166 let mut out = vec![0.0f32; heads * dv];
167 for h in 0..heads {
168 let qrow = &q[h * dq..(h + 1) * dq];
169 let mut scores = vec![0.0f32; t];
170 let mut m = f32::NEG_INFINITY;
171 for kj in 0..t {
172 let krow = &k[(kj * heads + h) * dq..(kj * heads + h) * dq + dq];
173 let s: f32 = qrow.iter().zip(krow).map(|(a, b)| a * b).sum::<f32>() * scale;
174 scores[kj] = s;
175 if s > m {
176 m = s;
177 }
178 }
179 let mut z = 0.0f32;
180 for s in &mut scores {
181 *s = (*s - m).exp();
182 z += *s;
183 }
184 let orow = &mut out[h * dv..(h + 1) * dv];
185 for kj in 0..t {
186 let a = scores[kj] / z;
187 let vrow = &v[(kj * heads + h) * dv..(kj * heads + h) * dv + dv];
188 for d in 0..dv {
189 orow[d] += a * vrow[d];
190 }
191 }
192 }
193 out
194}
195
196pub fn apply_output_gate(attn: &[f32], g: &[f32], enabled: bool) -> Vec<f32> {
198 if !enabled {
199 return attn.to_vec();
200 }
201 assert_eq!(attn.len(), g.len());
202 attn.iter().zip(g).map(|(a, gg)| a * sigmoid(*gg)).collect()
203}
204
205#[allow(clippy::too_many_arguments)]
207pub fn gated_mla_attend(
208 q: &mut [f32],
209 k: &mut [f32],
210 v: &[f32],
211 g: &[f32],
212 t: usize,
213 cfg: &MlaConfig,
214 pos0: usize,
215 theta: f32,
216) -> Vec<f32> {
217 for p in 0..t {
218 let dim = cfg.qk_head_dim();
219 let qh = &mut q[p * cfg.heads * dim..(p + 1) * cfg.heads * dim];
220 let kh = &mut k[p * cfg.heads * dim..(p + 1) * cfg.heads * dim];
221 maybe_rope(
222 qh,
223 cfg.qk_nope_head_dim,
224 cfg.qk_rope_head_dim,
225 pos0 + p,
226 theta,
227 cfg.mla_use_nope,
228 );
229 maybe_rope(
230 kh,
231 cfg.qk_nope_head_dim,
232 cfg.qk_rope_head_dim,
233 pos0 + p,
234 theta,
235 cfg.mla_use_nope,
236 );
237 }
238 let attn = sdpa(q, k, v, t, cfg.heads, cfg.qk_head_dim(), cfg.v_head_dim);
239 apply_output_gate(&attn, g, cfg.mla_use_output_gate)
240}
241
242#[allow(clippy::too_many_arguments)]
245pub fn mla_decode_token(
246 q: &mut [f32],
247 k: &mut [f32],
248 v: &[f32],
249 g: &[f32],
250 kv: &mut MlaKv,
251 cfg: &MlaConfig,
252 pos: usize,
253 theta: f32,
254) -> Vec<f32> {
255 maybe_rope(
256 q,
257 cfg.qk_nope_head_dim,
258 cfg.qk_rope_head_dim,
259 pos,
260 theta,
261 cfg.mla_use_nope,
262 );
263 maybe_rope(
264 k,
265 cfg.qk_nope_head_dim,
266 cfg.qk_rope_head_dim,
267 pos,
268 theta,
269 cfg.mla_use_nope,
270 );
271 kv.append(k, v);
272 let attn = sdpa_one(
273 q,
274 &kv.k,
275 &kv.v,
276 kv.seq_len,
277 cfg.heads,
278 cfg.qk_head_dim(),
279 cfg.v_head_dim,
280 );
281 apply_output_gate(&attn, g, cfg.mla_use_output_gate)
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 fn tiny_cfg(nope: bool, gate: bool) -> MlaConfig {
289 MlaConfig {
290 heads: 1,
291 qk_nope_head_dim: 2,
292 qk_rope_head_dim: 2,
293 v_head_dim: 2,
294 q_lora_rank: 4,
295 kv_lora_rank: 4,
296 mla_use_nope: nope,
297 mla_use_output_gate: gate,
298 }
299 }
300
301 #[test]
302 fn production_keeps_rope_slots_but_nope() {
303 let p = MlaConfig::production();
304 assert!(p.mla_use_nope);
305 assert!(p.mla_use_output_gate);
306 assert_eq!(p.qk_head_dim(), 192);
307 assert_eq!(p.v_head_dim, 128);
308 }
309
310 #[test]
311 fn nope_does_not_rotate() {
312 let mut x = vec![1.0, 0.0, 1.0, 0.0];
313 let orig = x.clone();
314 maybe_rope(&mut x, 2, 2, 3, 10000.0, true);
315 assert_eq!(x, orig);
316 maybe_rope(&mut x, 2, 2, 3, 10000.0, false);
317 assert_ne!(x, orig, "RoPE on must move the rope slice");
318 }
319
320 #[test]
321 fn output_gate_mutates() {
322 let attn = vec![1.0, 2.0];
323 let g = vec![0.0, 10.0];
324 let off = apply_output_gate(&attn, &g, false);
325 let on = apply_output_gate(&attn, &g, true);
326 assert_eq!(off, attn);
327 assert!((on[0] - 0.5).abs() < 1e-6);
328 assert!(on[1] > 1.9);
329 assert_ne!(on, off);
330 }
331
332 #[test]
333 fn gated_nope_path_runs() {
334 let cfg = tiny_cfg(true, true);
335 let mut q = vec![1.0, 0.0, 0.0, 1.0];
336 let mut k = q.clone();
337 let v = vec![0.5, -0.5];
338 let g = vec![0.0, 0.0];
339 let out = gated_mla_attend(&mut q, &mut k, &v, &g, 1, &cfg, 0, 10000.0);
340 assert_eq!(out.len(), 2);
341 }
342
343 #[test]
344 fn twin_0_40b_geometry() {
345 let t = MlaConfig::twin_0_40b();
346 assert_eq!(t.heads, 8);
347 assert_eq!(t.qk_head_dim(), 96);
348 assert_eq!(t.v_head_dim, 64);
349 assert!(t.mla_use_nope);
350 assert!(t.mla_use_output_gate);
351 }
352
353 #[test]
354 fn decode_token_matches_attend_t1() {
355 let cfg = tiny_cfg(true, true);
356 let mut q = vec![1.0, 0.0, 0.0, 1.0];
357 let mut k = q.clone();
358 let v = vec![0.5, -0.5];
359 let g = vec![0.0, 0.0];
360 let mut q2 = q.clone();
361 let mut k2 = k.clone();
362 let attend = gated_mla_attend(&mut q2, &mut k2, &v, &g, 1, &cfg, 0, 10000.0);
363 let mut kv = MlaKv::default();
364 let dec = mla_decode_token(&mut q, &mut k, &v, &g, &mut kv, &cfg, 0, 10000.0);
365 assert_eq!(attend, dec);
366 assert_eq!(kv.seq_len, 1);
367 }
368
369 #[test]
370 fn cuda_mla_env_default_on() {
371 if std::env::var_os("K3_CUDA_MLA").is_some() {
372 return;
373 }
374 assert!(
375 cuda_mla_enabled(),
376 "FullAttention default is CUDA MLA; K3_CUDA_MLA=0 is the CPU escape"
377 );
378 }
379
380 #[test]
381 fn cuda_mla_env_opt_out() {
382 const THIS: &str = "kimi_k3::mla::tests::cuda_mla_env_opt_out";
383 const MARKER: &str = "K3_CUDA_MLA_CHILD";
384 if std::env::var_os(MARKER).is_some() {
385 assert!(!cuda_mla_enabled(), "K3_CUDA_MLA=0 must keep the CPU mixer");
386 return;
387 }
388 let output = std::process::Command::new(std::env::current_exe().unwrap())
389 .args(["--exact", THIS])
390 .env(MARKER, "1")
391 .env("K3_CUDA_MLA", "0")
392 .output()
393 .unwrap();
394 assert!(
395 output.status.success(),
396 "K3_CUDA_MLA child failed:\n{}",
397 String::from_utf8_lossy(&output.stderr)
398 );
399 }
400}