use ferrox_core::weight_matrix::WeightMatrix;
fn rms_norm_no_weight(x: &[f32], eps: f32) -> Vec<f32> {
let mean_sq = x.iter().map(|v| v * v).sum::<f32>() / x.len() as f32;
let scale = 1.0 / (mean_sq + eps).sqrt();
x.iter().map(|v| v * scale).collect()
}
pub const HC_MULT: usize = 4;
pub struct HyperConnectionPreWeights {
pub fn_proj: WeightMatrix, pub scale: [f32; 3],
pub base_pre: [f32; HC_MULT],
pub base_post: [f32; HC_MULT],
pub base_comb: [f32; HC_MULT * HC_MULT],
}
pub struct HyperConnectionHeadWeights {
pub fn_proj: WeightMatrix, pub scale: f32,
pub base: [f32; HC_MULT],
}
fn sigmoid(x: f32) -> f32 {
1.0 / (1.0 + (-x).exp())
}
pub fn weighted_sum(x: &[Vec<f32>; HC_MULT], weights: &[f32; HC_MULT]) -> Vec<f32> {
let n_embd = x[0].len();
let mut out = vec![0f32; n_embd];
for (xh, &w) in x.iter().zip(weights.iter()) {
for (o, v) in out.iter_mut().zip(xh.iter()) {
*o += v * w;
}
}
out
}
#[allow(clippy::needless_range_loop)]
pub fn sinkhorn(comb: &mut [[f32; HC_MULT]; HC_MULT], iters: u32, eps: f32) {
for src in 0..HC_MULT {
let mut col = [0f32; HC_MULT];
for dst in 0..HC_MULT {
col[dst] = comb[dst][src];
}
let max = col.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let mut sum = 0f32;
for v in col.iter_mut() {
*v = (*v - max).exp();
sum += *v;
}
for v in col.iter_mut() {
*v /= sum;
}
for dst in 0..HC_MULT {
comb[dst][src] = col[dst] + eps;
}
}
let norm_rows_over_src = |comb: &mut [[f32; HC_MULT]; HC_MULT]| {
for dst in 0..HC_MULT {
let sum: f32 = comb[dst].iter().sum::<f32>() + eps;
for src in 0..HC_MULT {
comb[dst][src] /= sum;
}
}
};
let norm_cols_over_dst = |comb: &mut [[f32; HC_MULT]; HC_MULT]| {
for src in 0..HC_MULT {
let sum: f32 = (0..HC_MULT).map(|dst| comb[dst][src]).sum::<f32>() + eps;
for dst in 0..HC_MULT {
comb[dst][src] /= sum;
}
}
};
norm_rows_over_src(comb);
for _ in 1..iters {
norm_cols_over_dst(comb);
norm_rows_over_src(comb);
}
}
#[allow(clippy::type_complexity, clippy::needless_range_loop)]
pub fn pre(
weights: &HyperConnectionPreWeights,
x: &[Vec<f32>; HC_MULT],
rms_norm_eps: f32,
sinkhorn_iters: u32,
hc_eps: f32,
) -> (Vec<f32>, [f32; HC_MULT], [[f32; HC_MULT]; HC_MULT]) {
let n_embd = x[0].len();
let mut flat = Vec::with_capacity(HC_MULT * n_embd);
for xh in x {
flat.extend_from_slice(xh);
}
let flat_norm = rms_norm_no_weight(&flat, rms_norm_eps);
let mixes = weights.fn_proj.apply(&flat_norm);
let mut pre_gate = [0f32; HC_MULT];
for h in 0..HC_MULT {
pre_gate[h] = sigmoid(mixes[h] * weights.scale[0] + weights.base_pre[h]) + hc_eps;
}
let mut post_gate = [0f32; HC_MULT];
for h in 0..HC_MULT {
post_gate[h] = sigmoid(mixes[HC_MULT + h] * weights.scale[1] + weights.base_post[h]) * 2.0;
}
let mut comb = [[0f32; HC_MULT]; HC_MULT];
for dst in 0..HC_MULT {
for src in 0..HC_MULT {
let idx = dst * HC_MULT + src;
comb[dst][src] = mixes[2 * HC_MULT + idx] * weights.scale[2] + weights.base_comb[idx];
}
}
sinkhorn(&mut comb, sinkhorn_iters, hc_eps);
let merged = weighted_sum(x, &pre_gate);
(merged, post_gate, comb)
}
pub fn post(
sub_layer_out: &[f32],
residual: &[Vec<f32>; HC_MULT],
post_gate: &[f32; HC_MULT],
comb: &[[f32; HC_MULT]; HC_MULT],
) -> [Vec<f32>; HC_MULT] {
let n_embd = sub_layer_out.len();
std::array::from_fn(|dst| {
let mut out = vec![0f32; n_embd];
for (o, v) in out.iter_mut().zip(sub_layer_out.iter()) {
*o = v * post_gate[dst];
}
for src in 0..HC_MULT {
let w = comb[dst][src];
for (o, v) in out.iter_mut().zip(residual[src].iter()) {
*o += v * w;
}
}
out
})
}
pub fn head(
weights: &HyperConnectionHeadWeights,
x: &[Vec<f32>; HC_MULT],
rms_norm_eps: f32,
hc_eps: f32,
) -> Vec<f32> {
let n_embd = x[0].len();
let mut flat = Vec::with_capacity(HC_MULT * n_embd);
for xh in x {
flat.extend_from_slice(xh);
}
let flat_norm = rms_norm_no_weight(&flat, rms_norm_eps);
let mixes = weights.fn_proj.apply(&flat_norm);
let mut gate = [0f32; HC_MULT];
for h in 0..HC_MULT {
gate[h] = sigmoid(mixes[h] * weights.scale + weights.base[h]) + hc_eps;
}
weighted_sum(x, &gate)
}
#[cfg(test)]
mod tests {
use super::*;
use ferrox_core::tensor::Tensor;
fn wm(data: &[f32], rows: usize, cols: usize) -> WeightMatrix {
assert_eq!(data.len(), rows * cols);
WeightMatrix::F32(Tensor::new(data.to_vec(), vec![rows, cols]))
}
#[test]
fn weighted_sum_with_one_hot_weights_selects_that_stream() {
let x: [Vec<f32>; HC_MULT] = [
vec![1.0, 2.0],
vec![10.0, 20.0],
vec![100.0, 200.0],
vec![1000.0, 2000.0],
];
let weights = [0.0, 1.0, 0.0, 0.0];
let out = weighted_sum(&x, &weights);
assert_eq!(out, vec![10.0, 20.0]);
}
#[test]
fn sinkhorn_output_rows_and_columns_are_plausibly_normalized() {
let mut comb = [
[1.0, 0.5, 0.2, 0.1],
[0.3, 1.2, 0.4, 0.2],
[0.1, 0.2, 1.5, 0.3],
[0.2, 0.1, 0.3, 1.1],
];
sinkhorn(&mut comb, 3, 1e-6);
for row in comb.iter() {
for &v in row.iter() {
assert!(v.is_finite() && v >= 0.0);
}
}
}
#[test]
fn pre_then_post_round_trip_preserves_finite_output_and_stream_count() {
let n_embd = 3;
let hc_mix_dim = (2 + HC_MULT) * HC_MULT;
let fn_proj = wm(
&vec![0.05; hc_mix_dim * (HC_MULT * n_embd)],
hc_mix_dim,
HC_MULT * n_embd,
);
let weights = HyperConnectionPreWeights {
fn_proj,
scale: [1.0, 1.0, 1.0],
base_pre: [0.0; HC_MULT],
base_post: [0.0; HC_MULT],
base_comb: [0.0; HC_MULT * HC_MULT],
};
let x: [Vec<f32>; HC_MULT] = std::array::from_fn(|h| vec![(h + 1) as f32; n_embd]);
let (merged, post_gate, comb) = pre(&weights, &x, 1e-5, 3, 1e-6);
assert_eq!(merged.len(), n_embd);
assert!(merged.iter().all(|v| v.is_finite()));
let streams = post(&merged, &x, &post_gate, &comb);
assert_eq!(streams.len(), HC_MULT);
for s in streams.iter() {
assert_eq!(s.len(), n_embd);
assert!(s.iter().all(|v| v.is_finite()));
}
}
#[test]
fn head_collapses_streams_to_a_single_finite_vector() {
let n_embd = 3;
let fn_proj = wm(
&vec![0.05; HC_MULT * (HC_MULT * n_embd)],
HC_MULT,
HC_MULT * n_embd,
);
let weights = HyperConnectionHeadWeights {
fn_proj,
scale: 1.0,
base: [0.0; HC_MULT],
};
let x: [Vec<f32>; HC_MULT] = std::array::from_fn(|h| vec![(h + 1) as f32; n_embd]);
let out = head(&weights, &x, 1e-5, 1e-6);
assert_eq!(out.len(), n_embd);
assert!(out.iter().all(|v| v.is_finite()));
}
}