use crate::error::InferenceError;
use crate::quant::quarot::hadamard::RandomizedHadamard;
use crate::quant::quarot::rotation::{
absorb_input_rotation, absorb_input_rotation_f64, absorb_output_rotation,
absorb_output_rotation_f64,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AbsorptionSide {
InputSide,
OutputSide,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TensorRotation {
pub side: AbsorptionSide,
pub rotation_id: RotationId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RotationId {
ResidualStream,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuleRequirement {
Required,
Optional,
}
#[derive(Debug, Clone)]
struct Rule {
pattern: String,
rotation: TensorRotation,
requirement: RuleRequirement,
}
#[derive(Debug, Clone)]
pub struct RotationPlan {
rules: Vec<Rule>,
}
impl RotationPlan {
pub fn qwen35_residual_stream_linear_layers() -> Self {
let r_in = TensorRotation {
side: AbsorptionSide::InputSide,
rotation_id: RotationId::ResidualStream,
};
let r_out = TensorRotation {
side: AbsorptionSide::OutputSide,
rotation_id: RotationId::ResidualStream,
};
let req = |pat: &str, rot: TensorRotation| Rule {
pattern: pat.into(),
rotation: rot,
requirement: RuleRequirement::Required,
};
let opt = |pat: &str, rot: TensorRotation| Rule {
pattern: pat.into(),
rotation: rot,
requirement: RuleRequirement::Optional,
};
Self {
rules: vec![
req("self_attn.q_proj.weight", r_in),
req("self_attn.k_proj.weight", r_in),
req("self_attn.v_proj.weight", r_in),
req("self_attn.o_proj.weight", r_out),
req("linear_attn.in_proj_qkv.weight", r_in),
req("linear_attn.in_proj_z.weight", r_in),
req("linear_attn.in_proj_b.weight", r_in),
req("linear_attn.in_proj_a.weight", r_in),
req("linear_attn.out_proj.weight", r_out),
req("mlp.gate_proj.weight", r_in),
req("mlp.up_proj.weight", r_in),
req("mlp.down_proj.weight", r_out),
req("embed_tokens.weight", r_in),
opt("lm_head.weight", r_in),
],
}
}
pub fn for_tensor(&self, name: &str) -> Option<TensorRotation> {
self.rules
.iter()
.find(|rule| name.ends_with(&rule.pattern))
.map(|rule| rule.rotation)
}
pub fn absorption_for_module(&self, module: &str) -> Option<AbsorptionSide> {
let suffix = format!("{module}.weight");
self.rules
.iter()
.find(|rule| rule.pattern.ends_with(&suffix))
.map(|rule| rule.rotation.side)
}
pub fn rule_count(&self) -> usize {
self.rules.len()
}
pub fn validate_coverage<'a, I>(&self, tensor_names: I) -> CoverageReport
where
I: IntoIterator<Item = &'a str>,
{
let names: Vec<&str> = tensor_names.into_iter().collect();
let mut matched_tensors: Vec<String> = Vec::new();
let mut unplanned_tensors: Vec<String> = Vec::new();
let mut ambiguous_tensors: Vec<String> = Vec::new();
let mut unmatched_required_rules: Vec<String> = Vec::new();
let mut unmatched_optional_rules: Vec<String> = Vec::new();
for rule in &self.rules {
let any = names.iter().any(|n| n.ends_with(&rule.pattern));
if !any {
match rule.requirement {
RuleRequirement::Required => {
unmatched_required_rules.push(rule.pattern.clone())
}
RuleRequirement::Optional => {
unmatched_optional_rules.push(rule.pattern.clone())
}
}
}
}
for name in &names {
let match_count = self
.rules
.iter()
.filter(|rule| name.ends_with(&rule.pattern))
.count();
match match_count {
0 => unplanned_tensors.push((*name).to_string()),
1 => matched_tensors.push((*name).to_string()),
_ => ambiguous_tensors.push((*name).to_string()),
}
}
CoverageReport {
matched_tensors,
unplanned_tensors,
ambiguous_tensors,
unmatched_required_rules,
unmatched_optional_rules,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CoverageReport {
pub matched_tensors: Vec<String>,
pub unplanned_tensors: Vec<String>,
pub ambiguous_tensors: Vec<String>,
pub unmatched_required_rules: Vec<String>,
pub unmatched_optional_rules: Vec<String>,
}
impl CoverageReport {
pub fn is_complete(&self) -> bool {
self.unmatched_required_rules.is_empty() && self.ambiguous_tensors.is_empty()
}
}
pub fn apply_tensor_rotation(
name: &str,
weight: &mut [f32],
rows: usize,
cols: usize,
plan: &RotationPlan,
residual_rotation: &RandomizedHadamard,
) -> Result<bool, InferenceError> {
let Some(tr) = plan.for_tensor(name) else {
return Ok(false);
};
let rotation = match tr.rotation_id {
RotationId::ResidualStream => residual_rotation,
};
match tr.side {
AbsorptionSide::InputSide => absorb_input_rotation(weight, rows, cols, rotation)?,
AbsorptionSide::OutputSide => absorb_output_rotation(weight, rows, cols, rotation)?,
}
Ok(true)
}
pub fn apply_tensor_rotation_f64(
name: &str,
weight: &mut [f64],
rows: usize,
cols: usize,
plan: &RotationPlan,
residual_rotation: &RandomizedHadamard,
) -> Result<bool, InferenceError> {
let Some(tr) = plan.for_tensor(name) else {
return Ok(false);
};
let rotation = match tr.rotation_id {
RotationId::ResidualStream => residual_rotation,
};
match tr.side {
AbsorptionSide::InputSide => absorb_input_rotation_f64(weight, rows, cols, rotation)?,
AbsorptionSide::OutputSide => absorb_output_rotation_f64(weight, rows, cols, rotation)?,
}
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn qwen35_plan_covers_residual_stream_tensors() {
let plan = RotationPlan::qwen35_residual_stream_linear_layers();
assert_eq!(plan.rule_count(), 14);
let cases = [
(
"model.layers.0.self_attn.q_proj.weight",
AbsorptionSide::InputSide,
),
(
"model.layers.5.self_attn.k_proj.weight",
AbsorptionSide::InputSide,
),
(
"model.layers.5.self_attn.v_proj.weight",
AbsorptionSide::InputSide,
),
(
"model.layers.5.self_attn.o_proj.weight",
AbsorptionSide::OutputSide,
),
(
"model.layers.0.linear_attn.in_proj_qkv.weight",
AbsorptionSide::InputSide,
),
(
"model.layers.0.linear_attn.in_proj_z.weight",
AbsorptionSide::InputSide,
),
(
"model.layers.0.linear_attn.in_proj_b.weight",
AbsorptionSide::InputSide,
),
(
"model.layers.0.linear_attn.in_proj_a.weight",
AbsorptionSide::InputSide,
),
(
"model.layers.0.linear_attn.out_proj.weight",
AbsorptionSide::OutputSide,
),
(
"model.layers.23.mlp.gate_proj.weight",
AbsorptionSide::InputSide,
),
(
"model.layers.23.mlp.up_proj.weight",
AbsorptionSide::InputSide,
),
(
"model.layers.23.mlp.down_proj.weight",
AbsorptionSide::OutputSide,
),
(
"model.language_model.embed_tokens.weight",
AbsorptionSide::InputSide,
),
("lm_head.weight", AbsorptionSide::InputSide),
];
for (name, expected_side) in cases {
let tr = plan
.for_tensor(name)
.unwrap_or_else(|| panic!("plan missed tensor {name}"));
assert_eq!(tr.side, expected_side, "wrong side for {name}");
assert_eq!(tr.rotation_id, RotationId::ResidualStream);
}
}
#[test]
fn qwen35_plan_misses_rmsnorm_and_scalar_weights() {
let plan = RotationPlan::qwen35_residual_stream_linear_layers();
let skipped = [
"model.layers.0.input_layernorm.weight",
"model.layers.0.post_attention_layernorm.weight",
"model.norm.weight",
"model.layers.0.linear_attn.norm.weight",
"model.layers.0.linear_attn.A_log",
"model.layers.0.linear_attn.dt_bias",
"model.layers.0.linear_attn.conv1d.weight",
];
for name in skipped {
assert!(
plan.for_tensor(name).is_none(),
"non-linear-layer tensor {name} should not match plan"
);
}
}
fn qwen35_required_residual_tensor_suffixes() -> &'static [&'static str] {
&[
"self_attn.q_proj.weight",
"self_attn.k_proj.weight",
"self_attn.v_proj.weight",
"self_attn.o_proj.weight",
"linear_attn.in_proj_qkv.weight",
"linear_attn.in_proj_z.weight",
"linear_attn.in_proj_b.weight",
"linear_attn.in_proj_a.weight",
"linear_attn.out_proj.weight",
"mlp.gate_proj.weight",
"mlp.up_proj.weight",
"mlp.down_proj.weight",
"embed_tokens.weight",
]
}
fn synthetic_qwen35_tensor_names(include_lm_head: bool) -> Vec<String> {
let mut names: Vec<String> = qwen35_required_residual_tensor_suffixes()
.iter()
.enumerate()
.map(|(i, s)| {
if s.contains("embed_tokens") {
format!("model.language_model.{s}")
} else {
format!("model.layers.{i}.{s}")
}
})
.collect();
if include_lm_head {
names.push("lm_head.weight".to_string());
}
names
}
#[test]
fn validate_coverage_tied_embeddings_is_complete_without_lm_head() {
let plan = RotationPlan::qwen35_residual_stream_linear_layers();
let names = synthetic_qwen35_tensor_names(false);
let report = plan.validate_coverage(names.iter().map(String::as_str));
assert!(
report.is_complete(),
"tied-embeddings tensor list should yield complete coverage, got: {report:?}"
);
assert_eq!(
report.unmatched_required_rules.len(),
0,
"no required rule should be missing: {:?}",
report.unmatched_required_rules
);
assert_eq!(
report.unmatched_optional_rules,
vec!["lm_head.weight".to_string()],
"lm_head should appear in unmatched_optional_rules"
);
assert_eq!(report.matched_tensors.len(), 13);
assert!(report.ambiguous_tensors.is_empty());
}
#[test]
fn validate_coverage_untied_embeddings_matches_lm_head() {
let plan = RotationPlan::qwen35_residual_stream_linear_layers();
let names = synthetic_qwen35_tensor_names(true);
let report = plan.validate_coverage(names.iter().map(String::as_str));
assert!(
report.is_complete(),
"untied case should be complete: {report:?}"
);
assert!(report.unmatched_optional_rules.is_empty());
assert_eq!(report.matched_tensors.len(), 14);
}
#[test]
fn validate_coverage_flags_unmatched_required_rule() {
let plan = RotationPlan::qwen35_residual_stream_linear_layers();
let names: Vec<String> = synthetic_qwen35_tensor_names(false)
.into_iter()
.filter(|n| !n.ends_with("self_attn.o_proj.weight"))
.collect();
let report = plan.validate_coverage(names.iter().map(String::as_str));
assert!(
!report.is_complete(),
"missing required rule should fail completeness"
);
assert_eq!(
report.unmatched_required_rules,
vec!["self_attn.o_proj.weight".to_string()]
);
}
#[test]
fn validate_coverage_lists_unplanned_tensors() {
let plan = RotationPlan::qwen35_residual_stream_linear_layers();
let mut names = synthetic_qwen35_tensor_names(false);
names.push("model.layers.0.input_layernorm.weight".to_string());
names.push("model.mtp.head.weight".to_string());
let report = plan.validate_coverage(names.iter().map(String::as_str));
assert!(
report.is_complete(),
"unplanned tensors should not break required-rule completeness"
);
assert_eq!(report.unplanned_tensors.len(), 2);
assert!(
report
.unplanned_tensors
.iter()
.any(|n| n.ends_with("input_layernorm.weight"))
);
}
#[test]
fn qwen35_plan_misses_moe_expert_weights() {
let plan = RotationPlan::qwen35_residual_stream_linear_layers();
let moe_names = [
"model.layers.0.mlp.experts.gate_up_proj.weight",
"model.layers.0.mlp.experts.down_proj.weight",
];
for name in moe_names {
assert!(
plan.for_tensor(name).is_none(),
"MoE expert tensor {name} should not match v0 plan"
);
}
}
#[test]
fn apply_tensor_rotation_skips_unplanned() {
let plan = RotationPlan::qwen35_residual_stream_linear_layers();
let hidden = 64;
let r = RandomizedHadamard::new(7, hidden).unwrap();
let mut weight = vec![1.0_f32; hidden * hidden];
let weight_copy = weight.clone();
let rotated = apply_tensor_rotation(
"model.layers.0.linear_attn.in_proj.weight",
&mut weight,
hidden,
hidden,
&plan,
&r,
)
.unwrap();
assert!(!rotated, "unplanned tensor should report not rotated");
assert_eq!(weight, weight_copy, "unplanned tensor must not be mutated");
}
#[test]
fn apply_tensor_rotation_mlp_layer_pair_rotates_output() {
let hidden = 64;
let intermediate = 128;
let plan = RotationPlan::qwen35_residual_stream_linear_layers();
let r = RandomizedHadamard::new(0xC0FFEE, hidden).unwrap();
let mut state = 1_u64;
let mut rand = || {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((state >> 11) as u32 as f32 / u32::MAX as f32) - 0.5
};
let gate_proj: Vec<f32> = (0..intermediate * hidden).map(|_| rand()).collect();
let down_proj: Vec<f32> = (0..hidden * intermediate).map(|_| rand()).collect();
let x: Vec<f32> = (0..hidden).map(|_| rand()).collect();
let matvec = |w: &[f32], r: usize, c: usize, v: &[f32]| -> Vec<f32> {
(0..r)
.map(|i| (0..c).map(|j| w[i * c + j] * v[j]).sum())
.collect()
};
let intermediate_out = matvec(&gate_proj, intermediate, hidden, &x);
let y_original = matvec(&down_proj, hidden, intermediate, &intermediate_out);
let mut y_expected = y_original.clone();
r.apply(&mut y_expected).unwrap();
let mut gate_proj_abs = gate_proj.clone();
let mut down_proj_abs = down_proj.clone();
assert!(
apply_tensor_rotation(
"model.layers.0.mlp.gate_proj.weight",
&mut gate_proj_abs,
intermediate,
hidden,
&plan,
&r,
)
.unwrap()
);
assert!(
apply_tensor_rotation(
"model.layers.0.mlp.down_proj.weight",
&mut down_proj_abs,
hidden,
intermediate,
&plan,
&r,
)
.unwrap()
);
let mut x_rotated = x.clone();
r.apply(&mut x_rotated).unwrap();
let intermediate_rot = matvec(&gate_proj_abs, intermediate, hidden, &x_rotated);
let y_rotated = matvec(&down_proj_abs, hidden, intermediate, &intermediate_rot);
let max_abs_diff = y_expected
.iter()
.zip(y_rotated.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0_f32, f32::max);
assert!(
max_abs_diff < 1e-3,
"MLP pair output should equal R · y_original: max_abs_diff={max_abs_diff}"
);
}
#[test]
fn absorption_for_module_input_side() {
let plan = RotationPlan::qwen35_residual_stream_linear_layers();
for module in [
"q_proj",
"k_proj",
"v_proj",
"gate_proj",
"up_proj",
"in_proj_qkv",
"in_proj_z",
"in_proj_b",
"in_proj_a",
] {
assert_eq!(
plan.absorption_for_module(module),
Some(AbsorptionSide::InputSide),
"{module} should be InputSide"
);
}
}
#[test]
fn absorption_for_module_output_side() {
let plan = RotationPlan::qwen35_residual_stream_linear_layers();
for module in ["o_proj", "down_proj", "out_proj"] {
assert_eq!(
plan.absorption_for_module(module),
Some(AbsorptionSide::OutputSide),
"{module} should be OutputSide"
);
}
}
#[test]
fn absorption_for_module_unknown() {
let plan = RotationPlan::qwen35_residual_stream_linear_layers();
assert_eq!(plan.absorption_for_module("conv1d"), None);
assert_eq!(plan.absorption_for_module("norm"), None);
}
#[test]
fn f64_apply_matches_f32_apply_pattern() {
let plan = RotationPlan::qwen35_residual_stream_linear_layers();
let hidden = 64;
let r = RandomizedHadamard::new(11, hidden).unwrap();
let mut weight_f32: Vec<f32> = (0..hidden * hidden)
.map(|i| (i as f32 * 0.01).sin())
.collect();
let mut weight_f64: Vec<f64> = weight_f32.iter().map(|&v| f64::from(v)).collect();
let r1 = apply_tensor_rotation(
"model.layers.0.self_attn.q_proj.weight",
&mut weight_f32,
hidden,
hidden,
&plan,
&r,
)
.unwrap();
let r2 = apply_tensor_rotation_f64(
"model.layers.0.self_attn.q_proj.weight",
&mut weight_f64,
hidden,
hidden,
&plan,
&r,
)
.unwrap();
assert!(r1 && r2);
for (i, (a, b)) in weight_f32.iter().zip(weight_f64.iter()).enumerate() {
let delta = (f64::from(*a) - b).abs();
assert!(
delta < 1e-5,
"tensor[{i}]: f32={a} vs f64={b}, delta={delta}"
);
}
}
}