ferrox_models/block_residual.rs
1//! Kimi K3's cross-layer "block residual" mixing mechanism
2//! (`_apply_attn_res` in the real `modeling_kimi_linear.py`), a real
3//! architectural feature discovered by reading `KimiDecoderLayer.forward`
4//! in full rather than assuming a standard pre-norm residual: every
5//! layer blends its running residual stream with a growing set of
6//! saved checkpoints from earlier layers in the same block, using a
7//! learned RMSNorm-projected softmax score to weight the blend --
8//! structurally a tiny self-attention over `{checkpoints..., current}`.
9//!
10//! Confirmed real and active (not a dead/optional code path) via
11//! `config.json`'s `attn_res_block_size`=12: every layer calls this
12//! twice (once before self-attention using `self_attention_res_norm`/
13//! `self_attention_res_proj`, once before the FFN using
14//! `mlp_res_norm`/`mlp_res_proj`), every 12th layer (0-indexed
15//! `layer_idx % 12 == 0`) additionally commits the layer's pre-blend
16//! input as a new checkpoint into the block's growing residual set, and
17//! the whole model applies one final blend
18//! (`output_attn_res_norm`/`output_attn_res_proj`) before the final
19//! norm. Real per-layer tensor names (`self_attention_res_norm.weight`,
20//! `self_attention_res_proj.weight`, `mlp_res_norm.weight`,
21//! `mlp_res_proj.weight`) confirmed directly against a real Kimi K3
22//! shard header fetched earlier this session -- this isn't a rarely-used
23//! feature, every layer has these weights.
24
25/// One `_apply_attn_res` call: blends `prefix_sum` (`[hidden_dim]`, the
26/// layer's current running residual) with `block_residual` (`[n_blocks,
27/// hidden_dim]` flattened, the checkpoints saved so far in this block --
28/// may be empty). `norm_weight`/`proj_weight` are both `[hidden_dim]`
29/// (the real `proj` is `Linear(hidden_dim, 1, bias=false)`, so its
30/// weight is a single `[hidden_dim]` row, not a matrix).
31///
32/// Real math: normalize each candidate (`prefix_sum` and every saved
33/// checkpoint) by its own RMS scale factor only (no elementwise weight
34/// yet), score it by dotting with `norm_weight * proj_weight`
35/// (mathematically identical to `proj(rms_norm(candidate))` since `proj`
36/// has no bias), softmax the scores across candidates, then return the
37/// softmax-weighted sum of the *raw* (non-normalized) candidates.
38pub fn apply_attn_res(
39 prefix_sum: &[f32],
40 block_residual: &[f32],
41 norm_weight: &[f32],
42 proj_weight: &[f32],
43 eps: f32,
44) -> Vec<f32> {
45 let score_weight: Vec<f32> = norm_weight
46 .iter()
47 .zip(proj_weight.iter())
48 .map(|(n, p)| n * p)
49 .collect();
50 apply_attn_res_prescored(prefix_sum, block_residual, &score_weight, eps)
51}
52
53/// Same computation as `apply_attn_res`, but takes the already-fused
54/// `norm_weight * proj_weight` product directly instead of the two
55/// separate factors -- what a real Kimi K3 GGUF checkpoint actually
56/// stores (`blk.{bid}.attn_res_score`/`ffn_res_score`/
57/// `output_res_score`, real names confirmed against
58/// `ggml-org/llama.cpp#26185`'s `conversion/kimi_k3.py`, whose
59/// `_try_fuse_res` fuses this exact product at GGUF-conversion time
60/// since `_apply_attn_res` in the real `modeling_kimi_linear.py` never
61/// uses the two factors separately -- see docs/MODELS.md). The
62/// safetensors checkpoint stores the two factors separately instead, so
63/// `apply_attn_res` fuses them itself and delegates here.
64pub fn apply_attn_res_prescored(
65 prefix_sum: &[f32],
66 block_residual: &[f32],
67 score_weight: &[f32],
68 eps: f32,
69) -> Vec<f32> {
70 let hidden_dim = prefix_sum.len();
71 assert_eq!(score_weight.len(), hidden_dim);
72 assert!(block_residual.len().is_multiple_of(hidden_dim));
73 let n_blocks = block_residual.len() / hidden_dim;
74 let n_candidates = n_blocks + 1;
75
76 let candidate = |c: usize| -> &[f32] {
77 if c < n_blocks {
78 &block_residual[c * hidden_dim..(c + 1) * hidden_dim]
79 } else {
80 prefix_sum
81 }
82 };
83
84 let mut scores = vec![0f32; n_candidates];
85 for (c, score) in scores.iter_mut().enumerate() {
86 let v = candidate(c);
87 let mean_sq: f32 = v.iter().map(|x| x * x).sum::<f32>() / hidden_dim as f32;
88 let rstd = 1.0 / (mean_sq + eps).sqrt();
89 *score = v
90 .iter()
91 .zip(score_weight.iter())
92 .map(|(x, w)| (x * rstd) * w)
93 .sum();
94 }
95
96 let max = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
97 let mut probs = vec![0f32; n_candidates];
98 let mut sum = 0f32;
99 for (c, p) in probs.iter_mut().enumerate() {
100 *p = (scores[c] - max).exp();
101 sum += *p;
102 }
103 for p in probs.iter_mut() {
104 *p /= sum;
105 }
106
107 let mut out = vec![0f32; hidden_dim];
108 for (c, &w) in probs.iter().enumerate() {
109 let v = candidate(c);
110 for (o, x) in out.iter_mut().zip(v.iter()) {
111 *o += w * x;
112 }
113 }
114 out
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 // Generated by an independent Python reference -- do not hand-edit.
122 // Golden values computed from the same formula
123 // transcribed from the real `_apply_attn_res` source.
124 const PREFIX_SUM: [f32; 4] = [0.00123015, 0.298746, -0.274138, -0.890592];
125 const BLOCK_RESIDUAL: [f32; 8] = [
126 -0.454671, -0.991647, 0.0601436, 1.34022, -0.492207, -0.620475, 0.489842, 0.356887,
127 ];
128 const NORM_WEIGHT: [f32; 4] = [1.01054, 0.906953, 0.997075, 1.06953];
129 const PROJ_WEIGHT: [f32; 4] = [-1.34421, -0.457616, -1.90122, -1.28954];
130 const EXPECTED: [f32; 4] = [-0.0107297, 0.271161, -0.26009, -0.847388];
131
132 #[test]
133 fn matches_independent_python_reference() {
134 let got = apply_attn_res(
135 &PREFIX_SUM,
136 &BLOCK_RESIDUAL,
137 &NORM_WEIGHT,
138 &PROJ_WEIGHT,
139 1e-5,
140 );
141 for (i, (a, b)) in got.iter().zip(EXPECTED.iter()).enumerate() {
142 assert!((a - b).abs() < 1e-4, "element {i}: rust={a} python={b}");
143 }
144 }
145
146 #[test]
147 fn with_no_saved_checkpoints_softmax_over_one_candidate_is_the_identity() {
148 // n_blocks=0 -> exactly one candidate (prefix_sum itself) ->
149 // softmax over a single score is always 1.0 -> output must equal
150 // prefix_sum unchanged, regardless of norm/proj weights.
151 let prefix_sum = [1.0f32, -2.0, 3.5];
152 let norm_weight = [0.7f32, 1.3, -0.2];
153 let proj_weight = [2.0f32, -1.0, 0.5];
154 let got = apply_attn_res(&prefix_sum, &[], &norm_weight, &proj_weight, 1e-5);
155 for (a, b) in got.iter().zip(prefix_sum.iter()) {
156 assert!((a - b).abs() < 1e-5);
157 }
158 }
159}