1use ferrox_core::matmul::{rms_norm, silu};
57use ferrox_core::weight_matrix::WeightMatrix;
58
59use crate::config::KdaConfig;
60
61pub struct KdaAttnWeights {
62 pub q_proj: WeightMatrix, pub k_proj: WeightMatrix, pub v_proj: WeightMatrix, pub q_conv_weight: Vec<f32>,
68 pub k_conv_weight: Vec<f32>,
69 pub v_conv_weight: Vec<f32>,
70 pub a_log: Vec<f32>, pub f_a_proj: WeightMatrix, pub f_b_proj: WeightMatrix, pub dt_bias: Vec<f32>, pub b_proj: WeightMatrix, pub g_proj: WeightMatrix, pub o_norm_weight: Vec<f32>, pub o_proj: WeightMatrix, }
79
80pub struct KdaState {
87 conv_hist_q: Vec<f32>,
88 conv_hist_k: Vec<f32>,
89 conv_hist_v: Vec<f32>,
90 recurrent: Vec<f32>,
91}
92
93impl KdaState {
94 pub fn new(cfg: &KdaConfig) -> Self {
95 KdaState {
96 conv_hist_q: Vec::new(),
97 conv_hist_k: Vec::new(),
98 conv_hist_v: Vec::new(),
99 recurrent: vec![0f32; cfg.num_heads * cfg.head_dim * cfg.head_dim],
100 }
101 }
102}
103
104fn causal_conv_step(
116 weight: &[f32],
117 history: &mut Vec<f32>,
118 current: &[f32],
119 kernel_size: usize,
120 dim: usize,
121) -> Vec<f32> {
122 let hist_len = history.len() / dim;
123 let missing = (kernel_size - 1).saturating_sub(hist_len);
124
125 let mut y = vec![0f32; dim];
126 for j in 0..kernel_size {
127 if j < missing {
128 continue; }
130 let src: &[f32] = if j == kernel_size - 1 {
131 current
132 } else {
133 let hist_idx = j - missing;
134 &history[hist_idx * dim..(hist_idx + 1) * dim]
135 };
136 for d in 0..dim {
137 y[d] += weight[d * kernel_size + j] * src[d];
138 }
139 }
140 for v in y.iter_mut() {
141 *v = silu(*v);
142 }
143
144 history.extend_from_slice(current);
145 let max_hist_len = (kernel_size - 1) * dim;
146 if history.len() > max_hist_len {
147 let excess = history.len() - max_hist_len;
148 history.drain(0..excess);
149 }
150 y
151}
152
153fn l2_normalize(v: &mut [f32], eps: f32) {
154 let norm_sq: f32 = v.iter().map(|x| x * x).sum();
155 let scale = 1.0 / (norm_sq + eps).sqrt();
156 for x in v.iter_mut() {
157 *x *= scale;
158 }
159}
160
161fn sigmoid(x: f32) -> f32 {
162 1.0 / (1.0 + (-x).exp())
163}
164
165pub fn kda_forward_token(
167 weights: &KdaAttnWeights,
168 cfg: &KdaConfig,
169 hidden: &[f32],
170 rms_norm_eps: f32,
171 state: &mut KdaState,
172) -> Vec<f32> {
173 let projection_size = cfg.num_heads * cfg.head_dim;
174 let k_size = cfg.short_conv_kernel_size;
175
176 let q_lin = weights.q_proj.apply(hidden);
177 let k_lin = weights.k_proj.apply(hidden);
178 let v_lin = weights.v_proj.apply(hidden);
179
180 let q = causal_conv_step(
181 &weights.q_conv_weight,
182 &mut state.conv_hist_q,
183 &q_lin,
184 k_size,
185 projection_size,
186 );
187 let k = causal_conv_step(
188 &weights.k_conv_weight,
189 &mut state.conv_hist_k,
190 &k_lin,
191 k_size,
192 projection_size,
193 );
194 let v = causal_conv_step(
195 &weights.v_conv_weight,
196 &mut state.conv_hist_v,
197 &v_lin,
198 k_size,
199 projection_size,
200 );
201
202 let f_a = weights.f_a_proj.apply(hidden); let g_raw_full = weights.f_b_proj.apply(&f_a); let beta_raw = weights.b_proj.apply(hidden); let scale = 1.0 / (cfg.head_dim as f32).sqrt();
207 let mut attn_out = vec![0f32; projection_size];
208
209 #[allow(clippy::needless_range_loop)]
213 for h in 0..cfg.num_heads {
214 let base = h * cfg.head_dim;
215 let mut q_h = q[base..base + cfg.head_dim].to_vec();
216 let mut k_h = k[base..base + cfg.head_dim].to_vec();
217 let v_h = &v[base..base + cfg.head_dim];
218
219 l2_normalize(&mut q_h, 1e-6);
220 l2_normalize(&mut k_h, 1e-6);
221 for x in q_h.iter_mut() {
222 *x *= scale;
223 }
224
225 let a_log_h_exp = weights.a_log[h].exp();
226 let beta_h = sigmoid(beta_raw[h]);
227
228 let s_base = h * cfg.head_dim * cfg.head_dim;
229 let s = &mut state.recurrent[s_base..s_base + cfg.head_dim * cfg.head_dim];
230
231 let mut retrieval = vec![0f32; cfg.head_dim];
232 for k_idx in 0..cfg.head_dim {
233 let raw_gate = g_raw_full[base + k_idx] + weights.dt_bias[base + k_idx];
234 let gate = cfg.gate_lower_bound * sigmoid(a_log_h_exp * raw_gate);
235 let decay = gate.exp();
236 for v_idx in 0..cfg.head_dim {
237 let cell = &mut s[k_idx * cfg.head_dim + v_idx];
238 *cell *= decay;
239 retrieval[v_idx] += k_h[k_idx] * *cell;
240 }
241 }
242
243 let mut v_scaled = vec![0f32; cfg.head_dim];
244 for v_idx in 0..cfg.head_dim {
245 v_scaled[v_idx] = (v_h[v_idx] - retrieval[v_idx]) * beta_h;
246 }
247
248 for k_idx in 0..cfg.head_dim {
249 for v_idx in 0..cfg.head_dim {
250 s[k_idx * cfg.head_dim + v_idx] += k_h[k_idx] * v_scaled[v_idx];
251 }
252 }
253
254 for v_idx in 0..cfg.head_dim {
255 let mut acc = 0f32;
256 for k_idx in 0..cfg.head_dim {
257 acc += q_h[k_idx] * s[k_idx * cfg.head_dim + v_idx];
258 }
259 attn_out[base + v_idx] = acc;
260 }
261 }
262
263 let g_out_raw = weights.g_proj.apply(hidden); let mut gated = vec![0f32; projection_size];
265 for h in 0..cfg.num_heads {
266 let base = h * cfg.head_dim;
267 let o_h = &attn_out[base..base + cfg.head_dim];
268 let o_normed = rms_norm(o_h, &weights.o_norm_weight, rms_norm_eps);
269 for d in 0..cfg.head_dim {
270 gated[base + d] = o_normed[d] * sigmoid(g_out_raw[base + d]);
271 }
272 }
273
274 weights.o_proj.apply(&gated)
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use ferrox_core::tensor::Tensor;
281
282 const HIDDEN_SIZE: usize = 8;
283 const NUM_HEADS: usize = 2;
284 const HEAD_DIM: usize = 3;
285 const PROJECTION_SIZE: usize = NUM_HEADS * HEAD_DIM;
286 const CONV_SIZE: usize = 4;
287 const GATE_LOWER_BOUND: f32 = -5.0;
288 const EPS: f32 = 1e-5;
289
290 fn wm(data: &[f32], rows: usize, cols: usize) -> WeightMatrix {
291 assert_eq!(data.len(), rows * cols);
292 WeightMatrix::F32(Tensor::new(data.to_vec(), vec![rows, cols]))
293 }
294
295 fn cfg() -> KdaConfig {
296 KdaConfig {
297 num_heads: NUM_HEADS,
298 head_dim: HEAD_DIM,
299 short_conv_kernel_size: CONV_SIZE,
300 gate_lower_bound: GATE_LOWER_BOUND,
301 use_full_rank_gate: true,
302 }
303 }
304
305 const KDA_Q_PROJ: [f32; 48] = [
307 -0.237937, 0.0721714, -0.568898, 0.418732, 0.191488, -0.0876142, -0.0935848, 0.0911506,
308 -0.0802981, -0.0677727, 0.21602, 0.154412, -0.0192384, -0.025643, 0.0482749, -0.184206,
309 -0.121125, 0.164478, -0.0391448, -0.412328, -0.143184, 0.196986, -0.0696848, -0.0446198,
310 0.192551, 0.547383, -0.213957, 0.404462, -0.369004, 0.0524933, -0.350859, 0.405437,
311 0.250177, 0.341315, -0.26566, 0.205367, -0.155704, -0.137216, 0.151961, 0.263015,
312 0.0613261, -0.188396, -0.247745, 0.433295, 0.178184, 0.215918, 0.655046, -0.244759,
313 ];
314 const KDA_K_PROJ: [f32; 48] = [
315 0.767837,
316 0.945273,
317 0.485524,
318 0.248132,
319 -0.199147,
320 0.298346,
321 -0.132808,
322 -0.00649524,
323 -0.08713,
324 0.085149,
325 0.386423,
326 -0.166675,
327 -0.295621,
328 -0.300887,
329 -0.290483,
330 -0.429331,
331 -0.27388,
332 0.38798,
333 -0.177994,
334 0.0771323,
335 -0.365068,
336 0.0508952,
337 -0.522234,
338 -0.209627,
339 0.676362,
340 -0.174891,
341 0.335993,
342 0.136516,
343 -0.0458957,
344 -0.195632,
345 0.386073,
346 -0.053215,
347 0.458227,
348 -0.215724,
349 0.0172017,
350 0.13965,
351 0.111948,
352 -0.37014,
353 -0.199219,
354 -0.0587941,
355 -0.25611,
356 0.203198,
357 0.176406,
358 -0.587125,
359 -0.541576,
360 -0.384469,
361 0.0351779,
362 0.609952,
363 ];
364 const KDA_V_PROJ: [f32; 48] = [
365 -0.114707,
366 0.0751952,
367 -0.318934,
368 -0.314051,
369 -0.587168,
370 -0.00850327,
371 0.284165,
372 -0.107014,
373 0.418936,
374 0.0593565,
375 -0.0109237,
376 0.155176,
377 0.146213,
378 0.344342,
379 -0.240586,
380 -0.686415,
381 0.034424,
382 -0.183503,
383 -0.00815316,
384 0.49929,
385 -0.330853,
386 0.229439,
387 0.28376,
388 0.138221,
389 0.335552,
390 -0.137509,
391 -0.204453,
392 0.311695,
393 0.21609,
394 0.417113,
395 0.0636869,
396 0.487069,
397 -0.0846241,
398 -0.318099,
399 -0.611127,
400 -0.33042,
401 0.245551,
402 -0.439479,
403 -0.135709,
404 0.631975,
405 0.254039,
406 0.537295,
407 -0.297469,
408 -0.737993,
409 0.454914,
410 -0.425083,
411 0.0272771,
412 0.065128,
413 ];
414 const KDA_Q_CONV_W: [f32; 24] = [
415 -0.379559, 0.621053, 0.619735, 0.220578, -0.0117963, 0.0765747, -0.438363, -0.0956255,
416 -0.0419733, -0.340893, 0.339686, -0.570593, -0.181368, -0.901151, 0.198727, 0.297042,
417 0.248517, 1.16723, 0.375916, -0.376796, 1.06542, -0.368127, 0.315495, 0.297251,
418 ];
419 const KDA_K_CONV_W: [f32; 24] = [
420 -0.351889, 0.55814, 0.141837, 0.228382, -0.270293, 0.521053, -0.344375, -0.711664,
421 0.570051, -0.170692, 0.017348, 0.769103, 0.454597, 0.253084, 0.427966, -0.291003, 0.301235,
422 0.443975, 0.302169, -0.0816539, -1.49897, 0.222921, 0.0459506, -0.147821,
423 ];
424 const KDA_V_CONV_W: [f32; 24] = [
425 0.311239, 0.106281, 0.0815823, -0.593436, -0.583729, 0.148214, 0.0304228, 0.0554964,
426 0.0665052, 0.164583, 0.0144263, 0.0631083, 0.102446, 0.371764, -0.205876, 0.314558,
427 0.254553, 0.0421608, -0.190045, 0.147656, 0.345589, -0.429991, 0.0248583, 0.0258976,
428 ];
429 const KDA_A_LOG: [f32; 2] = [1.39784, 2.63702];
430 const KDA_F_A_PROJ: [f32; 24] = [
431 -0.465344, -0.330938, -0.461243, -0.184972, -0.167996, -0.162386, 0.111553, -0.05627,
432 -0.218674, -0.216565, -0.339453, -0.0588504, -0.0642164, 0.486887, 0.460246, 0.443371,
433 0.623773, -0.36512, 0.201895, 0.228316, 0.142865, 0.638001, 0.761363, 0.29381,
434 ];
435 const KDA_F_B_PROJ: [f32; 18] = [
436 0.195944, -0.336719, -0.336108, 0.210067, -0.0226898, -0.399168, -0.0372073, 0.108002,
437 0.280778, -0.0323038, -0.0807652, 0.00186235, 0.051895, -0.34894, 0.249993, 0.657098,
438 0.24426, -0.434459,
439 ];
440 const KDA_DT_BIAS: [f32; 6] = [
441 0.0189017, -0.353126, -0.365842, 0.13185, 0.0155298, -0.137422,
442 ];
443 const KDA_B_PROJ: [f32; 16] = [
444 -0.626234, -0.38068, 0.0486588, 0.00956812, 0.20691, 0.457018, -0.101777, 0.14069,
445 -0.0413395, -0.148962, -0.0734372, 0.288226, -0.308526, 0.342312, 0.443186, 0.0107199,
446 ];
447 const KDA_G_PROJ: [f32; 48] = [
448 0.0764755, -0.42354, 0.70816, 0.293666, 0.0516828, 0.0172313, 0.135292, -0.195371,
449 0.0849785, -0.277073, 0.149196, -0.203464, 0.268656, -0.0361107, 0.0806238, 0.888267,
450 0.24127, 0.0803401, 0.0133166, -0.311749, 0.325266, 0.480702, 0.0193065, 0.503025,
451 -0.0336833, -0.531916, -0.195972, -0.317098, -0.082362, 0.188094, -0.278692, -0.0240735,
452 0.0598056, -0.219908, -0.272796, 0.0261548, 0.261711, -0.180968, -0.225418, -0.042376,
453 0.0869846, -0.162905, -0.100244, -0.0693806, -0.325737, -0.0532611, -0.168014, 0.475017,
454 ];
455 const KDA_O_NORM_W: [f32; 3] = [0.948268, 1.04571, 1.03795];
456 const KDA_O_PROJ: [f32; 48] = [
457 -0.33541, -0.374685, -0.133646, 0.198772, -0.171972, -0.196846, -0.870066, -0.119052,
458 0.0937437, 0.133406, 0.128758, -0.0983533, -0.0681474, 0.249683, -0.0407133, -0.690913,
459 0.306003, -0.323304, 0.412587, 0.508244, 0.228732, 0.271235, 0.55734, -0.330637, 0.0761556,
460 0.0717198, -0.290133, 0.603544, -0.174208, -0.219374, 0.0803143, 0.309335, -0.180314,
461 -0.17987, 0.151233, -0.427684, -0.231776, -0.251471, -0.1303, -0.0426799, 0.553263,
462 0.272208, 0.00671946, -0.0674106, 0.262305, 0.12852, 0.267363, 0.414293,
463 ];
464
465 const KDA_HIDDEN_0: [f32; 8] = [
466 -0.504092, -1.12643, -0.0109012, 0.0397307, 0.143389, -0.299412, -0.11631, 0.279368,
467 ];
468 const KDA_HIDDEN_1: [f32; 8] = [
469 0.306279, 0.665489, -1.08314, -0.824329, 0.730971, 0.304099, 0.396717, -0.142417,
470 ];
471 const KDA_HIDDEN_2: [f32; 8] = [
472 -0.237078, 0.0999019, -0.469894, -0.765081, -0.555285, 0.0582631, 0.318596, 0.82563,
473 ];
474 const KDA_HIDDEN_3: [f32; 8] = [
475 0.185911, 0.236259, 0.84407, -0.388328, -0.479646, -0.268966, -0.048112, 1.01162,
476 ];
477 const KDA_HIDDEN_4: [f32; 8] = [
478 -0.17442, 0.653557, -0.23633, 0.245708, 0.340658, 0.266476, 0.375523, -0.34452,
479 ];
480
481 const KDA_GOLDEN_OUT_0: [f32; 8] = [
482 0.334593, 0.599535, -0.141337, -0.849879, 0.0520445, -0.161366, -0.356024, -0.259803,
483 ];
484 const KDA_GOLDEN_OUT_1: [f32; 8] = [
485 -0.178719, -0.104539, 0.510691, 0.283403, -0.331164, 0.229022, 0.24393, 0.0242566,
486 ];
487 const KDA_GOLDEN_OUT_2: [f32; 8] = [
488 -0.0640815, -0.296412, 0.488677, 0.684492, 0.289487, 0.664769, -0.493374, -0.528477,
489 ];
490 const KDA_GOLDEN_OUT_3: [f32; 8] = [
491 0.286145, 0.513164, -0.532459, -1.06872, -0.442514, -0.844267, 0.659196, 0.568707,
492 ];
493 const KDA_GOLDEN_OUT_4: [f32; 8] = [
494 -0.0696118, -0.371894, -0.388843, -0.283232, -0.109181, -0.399766, 0.265563, 0.319472,
495 ];
496
497 fn make_weights() -> KdaAttnWeights {
498 KdaAttnWeights {
499 q_proj: wm(&KDA_Q_PROJ, PROJECTION_SIZE, HIDDEN_SIZE),
500 k_proj: wm(&KDA_K_PROJ, PROJECTION_SIZE, HIDDEN_SIZE),
501 v_proj: wm(&KDA_V_PROJ, PROJECTION_SIZE, HIDDEN_SIZE),
502 q_conv_weight: KDA_Q_CONV_W.to_vec(),
503 k_conv_weight: KDA_K_CONV_W.to_vec(),
504 v_conv_weight: KDA_V_CONV_W.to_vec(),
505 a_log: KDA_A_LOG.to_vec(),
506 f_a_proj: wm(&KDA_F_A_PROJ, HEAD_DIM, HIDDEN_SIZE),
507 f_b_proj: wm(&KDA_F_B_PROJ, PROJECTION_SIZE, HEAD_DIM),
508 dt_bias: KDA_DT_BIAS.to_vec(),
509 b_proj: wm(&KDA_B_PROJ, NUM_HEADS, HIDDEN_SIZE),
510 g_proj: wm(&KDA_G_PROJ, PROJECTION_SIZE, HIDDEN_SIZE),
511 o_norm_weight: KDA_O_NORM_W.to_vec(),
512 o_proj: wm(&KDA_O_PROJ, HIDDEN_SIZE, PROJECTION_SIZE),
513 }
514 }
515
516 #[test]
517 fn matches_independent_python_reference_across_five_decode_steps() {
518 let weights = make_weights();
519 let cfg = cfg();
520 let mut state = KdaState::new(&cfg);
521
522 let hiddens = [
523 &KDA_HIDDEN_0[..],
524 &KDA_HIDDEN_1[..],
525 &KDA_HIDDEN_2[..],
526 &KDA_HIDDEN_3[..],
527 &KDA_HIDDEN_4[..],
528 ];
529 let goldens = [
530 &KDA_GOLDEN_OUT_0[..],
531 &KDA_GOLDEN_OUT_1[..],
532 &KDA_GOLDEN_OUT_2[..],
533 &KDA_GOLDEN_OUT_3[..],
534 &KDA_GOLDEN_OUT_4[..],
535 ];
536
537 for (pos, (hidden, golden)) in hiddens.iter().zip(goldens.iter()).enumerate() {
538 let out = kda_forward_token(&weights, &cfg, hidden, EPS, &mut state);
539 assert_eq!(out.len(), golden.len());
540 for (i, (a, b)) in out.iter().zip(golden.iter()).enumerate() {
541 assert!(
542 (a - b).abs() < 1e-3,
543 "position {pos} element {i}: rust={a} python={b}"
544 );
545 }
546 }
547 }
548
549 #[test]
550 fn causal_conv_step_zero_pads_before_the_start_of_the_sequence() {
551 let weight = vec![100.0, 100.0, 2.0]; let mut history = Vec::new();
555 let y = causal_conv_step(&weight, &mut history, &[3.0], 3, 1);
556 let expected = silu(6.0);
558 assert!((y[0] - expected).abs() < 1e-5);
559 assert_eq!(history, vec![3.0]);
560 }
561
562 #[test]
563 fn causal_conv_step_history_caps_at_kernel_size_minus_one() {
564 let weight = vec![1.0, 1.0, 1.0];
565 let mut history = Vec::new();
566 for x in [1.0, 2.0, 3.0, 4.0] {
567 causal_conv_step(&weight, &mut history, &[x], 3, 1);
568 }
569 assert_eq!(history, vec![3.0, 4.0]);
571 }
572}