1use ferrox_core::weight_matrix::WeightMatrix;
24
25fn rms_norm_no_weight(x: &[f32], eps: f32) -> Vec<f32> {
30 let mean_sq = x.iter().map(|v| v * v).sum::<f32>() / x.len() as f32;
31 let scale = 1.0 / (mean_sq + eps).sqrt();
32 x.iter().map(|v| v * scale).collect()
33}
34
35pub const HC_MULT: usize = 4;
39
40pub struct HyperConnectionPreWeights {
44 pub fn_proj: WeightMatrix, pub scale: [f32; 3],
47 pub base_pre: [f32; HC_MULT],
48 pub base_post: [f32; HC_MULT],
49 pub base_comb: [f32; HC_MULT * HC_MULT],
50}
51
52pub struct HyperConnectionHeadWeights {
57 pub fn_proj: WeightMatrix, pub scale: f32,
59 pub base: [f32; HC_MULT],
60}
61
62fn sigmoid(x: f32) -> f32 {
63 1.0 / (1.0 + (-x).exp())
64}
65
66pub fn weighted_sum(x: &[Vec<f32>; HC_MULT], weights: &[f32; HC_MULT]) -> Vec<f32> {
69 let n_embd = x[0].len();
70 let mut out = vec![0f32; n_embd];
71 for (xh, &w) in x.iter().zip(weights.iter()) {
72 for (o, v) in out.iter_mut().zip(xh.iter()) {
73 *o += v * w;
74 }
75 }
76 out
77}
78
79#[allow(clippy::needless_range_loop)]
85pub fn sinkhorn(comb: &mut [[f32; HC_MULT]; HC_MULT], iters: u32, eps: f32) {
86 for src in 0..HC_MULT {
87 let mut col = [0f32; HC_MULT];
88 for dst in 0..HC_MULT {
89 col[dst] = comb[dst][src];
90 }
91 let max = col.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
92 let mut sum = 0f32;
93 for v in col.iter_mut() {
94 *v = (*v - max).exp();
95 sum += *v;
96 }
97 for v in col.iter_mut() {
98 *v /= sum;
99 }
100 for dst in 0..HC_MULT {
101 comb[dst][src] = col[dst] + eps;
102 }
103 }
104
105 let norm_rows_over_src = |comb: &mut [[f32; HC_MULT]; HC_MULT]| {
106 for dst in 0..HC_MULT {
107 let sum: f32 = comb[dst].iter().sum::<f32>() + eps;
108 for src in 0..HC_MULT {
109 comb[dst][src] /= sum;
110 }
111 }
112 };
113 let norm_cols_over_dst = |comb: &mut [[f32; HC_MULT]; HC_MULT]| {
114 for src in 0..HC_MULT {
115 let sum: f32 = (0..HC_MULT).map(|dst| comb[dst][src]).sum::<f32>() + eps;
116 for dst in 0..HC_MULT {
117 comb[dst][src] /= sum;
118 }
119 }
120 };
121
122 norm_rows_over_src(comb);
123 for _ in 1..iters {
124 norm_cols_over_dst(comb);
125 norm_rows_over_src(comb);
126 }
127}
128
129#[allow(clippy::type_complexity, clippy::needless_range_loop)]
134pub fn pre(
135 weights: &HyperConnectionPreWeights,
136 x: &[Vec<f32>; HC_MULT],
137 rms_norm_eps: f32,
138 sinkhorn_iters: u32,
139 hc_eps: f32,
140) -> (Vec<f32>, [f32; HC_MULT], [[f32; HC_MULT]; HC_MULT]) {
141 let n_embd = x[0].len();
142 let mut flat = Vec::with_capacity(HC_MULT * n_embd);
143 for xh in x {
144 flat.extend_from_slice(xh);
145 }
146 let flat_norm = rms_norm_no_weight(&flat, rms_norm_eps);
147 let mixes = weights.fn_proj.apply(&flat_norm); let mut pre_gate = [0f32; HC_MULT];
150 for h in 0..HC_MULT {
151 pre_gate[h] = sigmoid(mixes[h] * weights.scale[0] + weights.base_pre[h]) + hc_eps;
152 }
153
154 let mut post_gate = [0f32; HC_MULT];
155 for h in 0..HC_MULT {
156 post_gate[h] = sigmoid(mixes[HC_MULT + h] * weights.scale[1] + weights.base_post[h]) * 2.0;
157 }
158
159 let mut comb = [[0f32; HC_MULT]; HC_MULT];
160 for dst in 0..HC_MULT {
161 for src in 0..HC_MULT {
162 let idx = dst * HC_MULT + src;
163 comb[dst][src] = mixes[2 * HC_MULT + idx] * weights.scale[2] + weights.base_comb[idx];
164 }
165 }
166 sinkhorn(&mut comb, sinkhorn_iters, hc_eps);
167
168 let merged = weighted_sum(x, &pre_gate);
169 (merged, post_gate, comb)
170}
171
172pub fn post(
178 sub_layer_out: &[f32],
179 residual: &[Vec<f32>; HC_MULT],
180 post_gate: &[f32; HC_MULT],
181 comb: &[[f32; HC_MULT]; HC_MULT],
182) -> [Vec<f32>; HC_MULT] {
183 let n_embd = sub_layer_out.len();
184 std::array::from_fn(|dst| {
185 let mut out = vec![0f32; n_embd];
186 for (o, v) in out.iter_mut().zip(sub_layer_out.iter()) {
187 *o = v * post_gate[dst];
188 }
189 for src in 0..HC_MULT {
190 let w = comb[dst][src];
191 for (o, v) in out.iter_mut().zip(residual[src].iter()) {
192 *o += v * w;
193 }
194 }
195 out
196 })
197}
198
199pub fn head(
203 weights: &HyperConnectionHeadWeights,
204 x: &[Vec<f32>; HC_MULT],
205 rms_norm_eps: f32,
206 hc_eps: f32,
207) -> Vec<f32> {
208 let n_embd = x[0].len();
209 let mut flat = Vec::with_capacity(HC_MULT * n_embd);
210 for xh in x {
211 flat.extend_from_slice(xh);
212 }
213 let flat_norm = rms_norm_no_weight(&flat, rms_norm_eps);
214 let mixes = weights.fn_proj.apply(&flat_norm); let mut gate = [0f32; HC_MULT];
217 for h in 0..HC_MULT {
218 gate[h] = sigmoid(mixes[h] * weights.scale + weights.base[h]) + hc_eps;
219 }
220 weighted_sum(x, &gate)
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use ferrox_core::tensor::Tensor;
227
228 fn wm(data: &[f32], rows: usize, cols: usize) -> WeightMatrix {
229 assert_eq!(data.len(), rows * cols);
230 WeightMatrix::F32(Tensor::new(data.to_vec(), vec![rows, cols]))
231 }
232
233 #[test]
234 fn weighted_sum_with_one_hot_weights_selects_that_stream() {
235 let x: [Vec<f32>; HC_MULT] = [
236 vec![1.0, 2.0],
237 vec![10.0, 20.0],
238 vec![100.0, 200.0],
239 vec![1000.0, 2000.0],
240 ];
241 let weights = [0.0, 1.0, 0.0, 0.0];
242 let out = weighted_sum(&x, &weights);
243 assert_eq!(out, vec![10.0, 20.0]);
244 }
245
246 #[test]
247 fn sinkhorn_output_rows_and_columns_are_plausibly_normalized() {
248 let mut comb = [
249 [1.0, 0.5, 0.2, 0.1],
250 [0.3, 1.2, 0.4, 0.2],
251 [0.1, 0.2, 1.5, 0.3],
252 [0.2, 0.1, 0.3, 1.1],
253 ];
254 sinkhorn(&mut comb, 3, 1e-6);
255 for row in comb.iter() {
256 for &v in row.iter() {
257 assert!(v.is_finite() && v >= 0.0);
258 }
259 }
260 }
261
262 #[test]
263 fn pre_then_post_round_trip_preserves_finite_output_and_stream_count() {
264 let n_embd = 3;
265 let hc_mix_dim = (2 + HC_MULT) * HC_MULT;
266 let fn_proj = wm(
267 &vec![0.05; hc_mix_dim * (HC_MULT * n_embd)],
268 hc_mix_dim,
269 HC_MULT * n_embd,
270 );
271 let weights = HyperConnectionPreWeights {
272 fn_proj,
273 scale: [1.0, 1.0, 1.0],
274 base_pre: [0.0; HC_MULT],
275 base_post: [0.0; HC_MULT],
276 base_comb: [0.0; HC_MULT * HC_MULT],
277 };
278 let x: [Vec<f32>; HC_MULT] = std::array::from_fn(|h| vec![(h + 1) as f32; n_embd]);
279
280 let (merged, post_gate, comb) = pre(&weights, &x, 1e-5, 3, 1e-6);
281 assert_eq!(merged.len(), n_embd);
282 assert!(merged.iter().all(|v| v.is_finite()));
283
284 let streams = post(&merged, &x, &post_gate, &comb);
286 assert_eq!(streams.len(), HC_MULT);
287 for s in streams.iter() {
288 assert_eq!(s.len(), n_embd);
289 assert!(s.iter().all(|v| v.is_finite()));
290 }
291 }
292
293 #[test]
294 fn head_collapses_streams_to_a_single_finite_vector() {
295 let n_embd = 3;
296 let fn_proj = wm(
297 &vec![0.05; HC_MULT * (HC_MULT * n_embd)],
298 HC_MULT,
299 HC_MULT * n_embd,
300 );
301 let weights = HyperConnectionHeadWeights {
302 fn_proj,
303 scale: 1.0,
304 base: [0.0; HC_MULT],
305 };
306 let x: [Vec<f32>; HC_MULT] = std::array::from_fn(|h| vec![(h + 1) as f32; n_embd]);
307 let out = head(&weights, &x, 1e-5, 1e-6);
308 assert_eq!(out.len(), n_embd);
309 assert!(out.iter().all(|v| v.is_finite()));
310 }
311}