#[inline]
pub fn cosine_similarity(a: &[f32], b: &[f32]) -> Option<f32> {
assert_eq!(a.len(), b.len(), "vectors must have equal length");
let dot: f64 = a
.iter()
.zip(b.iter())
.map(|(&x, &y)| x as f64 * y as f64)
.sum();
let aa: f64 = a.iter().map(|&x| x as f64 * x as f64).sum();
let bb: f64 = b.iter().map(|&x| x as f64 * x as f64).sum();
if aa == 0.0 || bb == 0.0 {
return None;
}
let cos = dot / (aa.sqrt() * bb.sqrt());
Some(cos.clamp(-1.0, 1.0) as f32)
}
#[derive(Debug, Clone)]
pub struct BlockInfluence {
pub layer_idx: usize,
pub cosine_sim: f32,
pub angular_distance: f32,
pub influence: f32,
}
impl BlockInfluence {
pub fn from_vectors(layer_idx: usize, input: &[f32], output: &[f32]) -> Option<Self> {
let cosine_sim = cosine_similarity(input, output)?;
let angular_distance = cosine_sim.acos();
let influence = 1.0 - cosine_sim;
Some(Self {
layer_idx,
cosine_sim,
angular_distance,
influence,
})
}
}
#[derive(Debug, Clone)]
pub struct BlockInfluenceAccumulator {
layer_idx: usize,
cosine_sum: f64,
count: usize,
}
impl BlockInfluenceAccumulator {
pub fn new(layer_idx: usize) -> Self {
Self {
layer_idx,
cosine_sum: 0.0,
count: 0,
}
}
#[inline]
pub fn update(&mut self, input: &[f32], output: &[f32]) {
if let Some(cos) = cosine_similarity(input, output) {
self.cosine_sum += cos as f64;
self.count += 1;
}
}
pub fn finalize(&self) -> Option<BlockInfluence> {
if self.count == 0 {
return None;
}
let cosine_sim = (self.cosine_sum / self.count as f64) as f32;
let angular_distance = cosine_sim.acos();
let influence = 1.0 - cosine_sim;
Some(BlockInfluence {
layer_idx: self.layer_idx,
cosine_sim,
angular_distance,
influence,
})
}
pub fn count(&self) -> usize {
self.count
}
}
pub fn score_from_hidden_states(hidden_states: &[Vec<f32>]) -> Vec<BlockInfluence> {
if hidden_states.len() < 2 {
return Vec::new();
}
(0..hidden_states.len() - 1)
.filter_map(|i| BlockInfluence::from_vectors(i, &hidden_states[i], &hidden_states[i + 1]))
.collect()
}
pub fn pruning_rank(scores: &[BlockInfluence]) -> Vec<usize> {
let mut indexed: Vec<(usize, f32)> =
scores.iter().map(|s| (s.layer_idx, s.influence)).collect();
indexed.sort_by(|a, b| a.1.total_cmp(&b.1));
indexed.into_iter().map(|(idx, _)| idx).collect()
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f32 = 1e-5;
fn assert_close(a: f32, b: f32, msg: &str) {
let diff = (a - b).abs();
let scale = a.abs().max(b.abs()).max(1e-8);
assert!(
diff / scale < TOL,
"{msg}: got {a}, expected {b}, diff={diff}"
);
}
#[test]
fn test_cosine_identical() {
let v = vec![1.0, 2.0, 3.0, 4.0];
assert_close(cosine_similarity(&v, &v).unwrap(), 1.0, "identical vectors");
}
#[test]
fn test_cosine_orthogonal() {
let a = vec![1.0, 0.0, 0.0];
let b = vec![0.0, 1.0, 0.0];
assert_close(
cosine_similarity(&a, &b).unwrap(),
0.0,
"orthogonal vectors",
);
}
#[test]
fn test_cosine_opposite() {
let a = vec![1.0, 0.0];
let b = vec![-1.0, 0.0];
assert_close(cosine_similarity(&a, &b).unwrap(), -1.0, "opposite vectors");
}
#[test]
fn test_cosine_known_angle() {
let a = vec![1.0, 0.0];
let b = vec![1.0, 1.0];
let expected = 1.0 / 2.0_f32.sqrt();
assert_close(
cosine_similarity(&a, &b).unwrap(),
expected,
"45-degree angle",
);
}
#[test]
fn test_cosine_zero_vector_returns_none() {
let a = vec![0.0, 0.0, 0.0];
let b = vec![1.0, 2.0, 3.0];
assert_eq!(cosine_similarity(&a, &b), None, "zero input vector → None");
}
#[test]
fn test_cosine_both_zero_returns_none() {
let z = vec![0.0; 4];
assert_eq!(cosine_similarity(&z, &z), None, "both zero → None");
}
#[test]
fn test_cosine_high_dim() {
let v = vec![1.0_f32; 896];
assert_close(cosine_similarity(&v, &v).unwrap(), 1.0, "896-dim identical");
}
#[test]
fn test_cosine_numerical_stability() {
let a = vec![1e6_f32; 1024];
let mut b = a.clone();
b[0] += 1e-2;
let c = cosine_similarity(&a, &b).unwrap();
assert!(c <= 1.0, "cosine should be clamped to ≤ 1.0, got {c}");
assert!(
c > 0.999,
"near-identical vectors should have cos > 0.999, got {c}"
);
}
#[test]
fn test_cosine_large_magnitude_finite() {
let a = vec![1e20_f32, 0.0];
let b = vec![1e20_f32, 0.0];
let c = cosine_similarity(&a, &b);
assert!(
c.is_some(),
"large identical vectors must return Some, not None"
);
assert!(
c.unwrap().is_finite(),
"large identical cosine must be finite, got {c:?}"
);
assert_close(
c.unwrap(),
1.0,
"large identical vectors must give cos = 1.0",
);
let neg_b = vec![-1e20_f32, 0.0];
let c2 = cosine_similarity(&a, &neg_b);
assert!(c2.is_some(), "large opposite vectors must return Some");
assert_close(
c2.unwrap(),
-1.0,
"large opposite vectors must give cos = -1.0",
);
}
#[test]
#[should_panic(expected = "vectors must have equal length")]
fn test_cosine_length_mismatch_panics() {
let a = vec![1.0, 2.0, 3.0];
let b = vec![1.0, 2.0];
let _ = cosine_similarity(&a, &b);
}
#[test]
fn test_block_influence_identical() {
let v = vec![1.0, 2.0, 3.0];
let bi = BlockInfluence::from_vectors(0, &v, &v).unwrap();
assert_close(bi.cosine_sim, 1.0, "identical cosine");
assert!(
bi.angular_distance < 1e-3,
"identical angle should be near 0, got {}",
bi.angular_distance
);
assert!(
bi.influence < 1e-5,
"identical influence should be near 0, got {}",
bi.influence
);
}
#[test]
fn test_block_influence_orthogonal() {
let a = vec![1.0, 0.0, 0.0];
let b = vec![0.0, 1.0, 0.0];
let bi = BlockInfluence::from_vectors(5, &a, &b).unwrap();
assert_close(bi.cosine_sim, 0.0, "orthogonal cosine");
assert_close(
bi.angular_distance,
std::f32::consts::FRAC_PI_2,
"orthogonal angle",
);
assert_close(bi.influence, 1.0, "orthogonal influence");
}
#[test]
fn test_block_influence_zero_vector_returns_none() {
let zero = vec![0.0, 0.0, 0.0];
let nonzero = vec![1.0, 0.0, 0.0];
assert!(
BlockInfluence::from_vectors(0, &zero, &nonzero).is_none(),
"zero input → None"
);
assert!(
BlockInfluence::from_vectors(0, &nonzero, &zero).is_none(),
"zero output → None"
);
}
#[test]
fn test_score_from_hidden_states() {
let states = vec![
vec![1.0, 0.0, 0.0],
vec![1.0, 0.0, 0.0], vec![0.0, 1.0, 0.0], vec![0.0, 0.7, 0.7], ];
let scores = score_from_hidden_states(&states);
assert_eq!(scores.len(), 3);
assert_close(scores[0].influence, 0.0, "layer 0 no change");
assert_close(scores[1].influence, 1.0, "layer 1 orthogonal");
let expected_cos = 0.7 / (0.7_f32 * 0.7 + 0.7 * 0.7).sqrt();
assert_close(scores[2].cosine_sim, expected_cos, "layer 2 cosine");
}
#[test]
fn test_score_from_hidden_states_single() {
let states = vec![vec![1.0, 2.0]];
assert!(score_from_hidden_states(&states).is_empty());
}
#[test]
fn test_score_from_hidden_states_zero_layer_omitted() {
let states = vec![
vec![1.0, 0.0, 0.0],
vec![0.0, 0.0, 0.0], vec![0.0, 1.0, 0.0], ];
let scores = score_from_hidden_states(&states);
assert_eq!(scores.len(), 0, "both layers omitted due to zero norms");
}
#[test]
fn test_pruning_rank_order() {
let states = vec![
vec![1.0, 0.0, 0.0],
vec![1.0, 0.0, 0.0], vec![0.0, 1.0, 0.0], vec![0.0, 0.7, 0.7], ];
let scores = score_from_hidden_states(&states);
let rank = pruning_rank(&scores);
assert_eq!(rank[0], 0, "most prunable first");
assert_eq!(rank[2], 1, "least prunable last");
}
#[test]
fn test_accumulator_single_token_matches_from_vectors() {
let a = vec![1.0_f32, 2.0, 3.0];
let b = vec![2.0_f32, 3.0, 4.0];
let direct = BlockInfluence::from_vectors(2, &a, &b).unwrap();
let mut acc = BlockInfluenceAccumulator::new(2);
acc.update(&a, &b);
let averaged = acc.finalize().unwrap();
assert_close(
averaged.cosine_sim,
direct.cosine_sim,
"single-token acc == from_vectors",
);
assert_eq!(averaged.layer_idx, 2);
assert_eq!(acc.count(), 1);
}
#[test]
fn test_accumulator_average_over_tokens() {
let identical_a = vec![1.0_f32, 0.0, 0.0];
let orthogonal_b = vec![0.0_f32, 1.0, 0.0];
let mut acc = BlockInfluenceAccumulator::new(0);
acc.update(&identical_a, &identical_a); acc.update(&identical_a, &orthogonal_b); let bi = acc.finalize().unwrap();
assert_eq!(acc.count(), 2);
assert_close(bi.cosine_sim, 0.5, "average of 1.0 and 0.0");
assert_close(bi.influence, 0.5, "influence = 1 - 0.5");
}
#[test]
fn test_accumulator_skips_zero_norm_tokens() {
let zero = vec![0.0_f32; 4];
let nonzero = vec![1.0_f32, 0.0, 0.0, 0.0];
let mut acc = BlockInfluenceAccumulator::new(1);
acc.update(&zero, &nonzero); acc.update(&nonzero, &zero); acc.update(&nonzero, &nonzero);
assert_eq!(acc.count(), 1, "only non-zero-norm token counted");
let bi = acc.finalize().unwrap();
assert_close(bi.cosine_sim, 1.0, "only valid token was identical");
}
#[test]
fn test_accumulator_unequal_norm_tokens() {
let unit_x = vec![1.0_f32, 0.0];
let _unit_y = [0.0_f32, 1.0];
let big_x = vec![100.0_f32, 0.0];
let big_y = vec![0.0_f32, 100.0];
let mut acc = BlockInfluenceAccumulator::new(0);
acc.update(&unit_x, &unit_x); acc.update(&big_x, &big_y); let bi = acc.finalize().unwrap();
assert_eq!(acc.count(), 2);
assert_close(
bi.cosine_sim,
0.5,
"unequal-norm mean must be 0.5, not norm-weighted 0.0",
);
assert_close(bi.influence, 0.5, "influence = 1 - 0.5");
}
#[test]
fn test_accumulator_all_zero_returns_none() {
let zero = vec![0.0_f32; 4];
let mut acc = BlockInfluenceAccumulator::new(0);
acc.update(&zero, &zero);
assert_eq!(acc.count(), 0);
assert!(acc.finalize().is_none(), "all-zero tokens → None");
}
#[test]
fn test_accumulator_empty_returns_none() {
let acc = BlockInfluenceAccumulator::new(3);
assert!(acc.finalize().is_none(), "no updates → None");
}
#[test]
fn test_accumulator_many_tokens_identical() {
let v = vec![1.0_f32; 896];
let mut acc = BlockInfluenceAccumulator::new(0);
for _ in 0..512 {
acc.update(&v, &v);
}
let bi = acc.finalize().unwrap();
assert_eq!(acc.count(), 512);
assert_close(bi.cosine_sim, 1.0, "512 identical tokens");
assert!(
bi.influence < 1e-5,
"influence near 0 for 512 identical, got {}",
bi.influence
);
}
#[test]
#[should_panic(expected = "vectors must have equal length")]
fn test_accumulator_length_mismatch_panics() {
let mut acc = BlockInfluenceAccumulator::new(0);
acc.update(&[1.0, 2.0, 3.0], &[1.0, 2.0]);
}
}