use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_finetune_method_parse() {
assert!(matches!(
"auto".parse::<FinetuneMethod>(),
Ok(FinetuneMethod::Auto)
));
assert!(matches!(
"full".parse::<FinetuneMethod>(),
Ok(FinetuneMethod::Full)
));
assert!(matches!(
"lora".parse::<FinetuneMethod>(),
Ok(FinetuneMethod::LoRA)
));
assert!(matches!(
"qlora".parse::<FinetuneMethod>(),
Ok(FinetuneMethod::QLoRA)
));
assert!("unknown".parse::<FinetuneMethod>().is_err());
}
#[test]
fn test_finetune_method_to_entrenar() {
assert!(matches!(Method::from(FinetuneMethod::Auto), Method::Auto));
assert!(matches!(Method::from(FinetuneMethod::LoRA), Method::LoRA));
assert!(matches!(Method::from(FinetuneMethod::QLoRA), Method::QLoRA));
assert!(matches!(Method::from(FinetuneMethod::Full), Method::Full));
}
#[test]
fn test_parse_model_size() {
assert_eq!(parse_model_size("7B").expect("7B"), 7_000_000_000);
assert_eq!(parse_model_size("1.5B").expect("1.5B"), 1_500_000_000);
assert_eq!(parse_model_size("135M").expect("135M"), 135_000_000);
assert!(parse_model_size("invalid").is_err());
}
#[test]
fn test_format_params() {
assert_eq!(format_params(7_000_000_000), "7.0B");
assert_eq!(format_params(135_000_000), "135.0M");
assert_eq!(format_params(1000), "1000");
}
#[test]
fn test_run_no_model() {
let result = run(
None,
"auto",
None,
16.0,
false,
None,
None,
None,
false,
3,
2e-4,
None,
None,
5,
"apr,safetensors",
false,
None,
false,
None,
"cuda",
None,
None,
None,
None,
0,
&[],
None,
false,
false,
0,
);
assert!(result.is_err());
}
#[test]
fn test_run_plan_with_model_size() {
let result = run(
None,
"lora",
None,
16.0,
true,
None,
None,
None,
false,
3,
2e-4,
Some("7B"),
None,
5,
"apr,safetensors",
false,
None,
false,
None,
"cuda",
None,
None,
None,
None,
0,
&[],
None,
false,
false,
0,
);
assert!(result.is_ok());
}
#[test]
fn test_run_plan_json() {
let result = run(
None,
"qlora",
None,
24.0,
true,
None,
None,
None,
false,
3,
2e-4,
Some("14B"),
None,
5,
"apr,safetensors",
false,
None,
false,
None,
"cuda",
None,
None,
None,
None,
0,
&[],
None,
true,
false,
0,
);
assert!(result.is_ok());
}
#[test]
fn test_run_with_model_file() {
let mut input = NamedTempFile::with_suffix(".apr").expect("create input");
input.write_all(&[0u8; 4096]).expect("write");
let result = run(
Some(input.path()),
"auto",
None,
16.0,
true,
None,
None,
None,
false,
3,
2e-4,
None,
None,
5,
"apr,safetensors",
false,
None,
false,
None,
"cuda",
None,
None,
None,
None,
0,
&[],
None,
false,
false,
0,
);
assert!(result.is_ok());
}
#[test]
fn test_merge_no_model() {
let result = run_merge(None, None, None, false);
assert!(result.is_err());
}
#[test]
fn test_merge_no_adapter() {
let input = NamedTempFile::with_suffix(".apr").expect("create input");
let result = run_merge(Some(input.path()), None, None, false);
assert!(result.is_err());
}
#[test]
fn test_merge_model_not_found() {
let result = run_merge(
Some(Path::new("/nonexistent.apr")),
Some(Path::new("/nonexistent_adapter/")),
None,
false,
);
assert!(result.is_err());
}
#[test]
fn test_is_lora_eligible() {
assert!(is_lora_eligible("model.layers.0.self_attn.q_proj.weight"));
assert!(is_lora_eligible("model.layers.0.self_attn.v_proj.weight"));
assert!(is_lora_eligible("model.layers.0.mlp.gate_proj.weight"));
assert!(is_lora_eligible("model.layers.0.mlp.up_proj.weight"));
assert!(is_lora_eligible("model.layers.0.mlp.down_proj.weight"));
assert!(is_lora_eligible("blk.0.attn_q.weight"));
assert!(is_lora_eligible("blk.0.ffn_gate.weight"));
assert!(!is_lora_eligible("model.embed_tokens.weight"));
assert!(!is_lora_eligible("model.norm.weight"));
assert!(!is_lora_eligible("lm_head.weight"));
assert!(!is_lora_eligible("model.layers.0.self_attn.q_proj.bias"));
assert!(!is_lora_eligible("token_embd.weight"));
}
#[test]
fn test_hash_seed_deterministic() {
let s1 = hash_seed("test.weight", 0);
let s2 = hash_seed("test.weight", 0);
assert_eq!(s1, s2, "Same inputs must produce same output");
let s3 = hash_seed("test.weight", 1);
assert_ne!(s1, s3, "Different index must produce different output");
let s4 = hash_seed("other.weight", 0);
assert_ne!(s1, s4, "Different name must produce different output");
}
#[test]
fn test_run_training_creates_adapter() {
let mut writer = aprender::serialization::apr::AprWriter::new();
writer.set_metadata("model_type", serde_json::json!("qwen2"));
writer.set_metadata("hidden_size", serde_json::json!(8));
writer.set_metadata("num_hidden_layers", serde_json::json!(1));
writer.set_metadata("num_attention_heads", serde_json::json!(1));
writer.set_metadata("num_key_value_heads", serde_json::json!(1));
writer.set_metadata("vocab_size", serde_json::json!(10));
writer.set_metadata("intermediate_size", serde_json::json!(16));
let q_data: Vec<f32> = (0..64).map(|i| (i as f32) * 0.01).collect();
writer.add_tensor_f32(
"model.layers.0.self_attn.q_proj.weight",
vec![8, 8],
&q_data,
);
let v_data: Vec<f32> = (0..64).map(|i| (i as f32) * 0.02).collect();
writer.add_tensor_f32(
"model.layers.0.self_attn.v_proj.weight",
vec![8, 8],
&v_data,
);
writer.add_tensor_f32("model.embed_tokens.weight", vec![10, 8], &vec![0.1; 80]);
let input_file = NamedTempFile::with_suffix(".apr").expect("create input");
let bytes = writer.to_bytes().expect("serialize");
std::fs::write(input_file.path(), bytes).expect("write");
let data_file = NamedTempFile::with_suffix(".jsonl").expect("create data");
std::fs::write(
data_file.path(),
"{\"instruction\": \"Say hello\", \"response\": \"Hello world\"}\n",
)
.expect("write data");
let output_file = NamedTempFile::with_suffix(".apr").expect("create output");
let result = run(
Some(input_file.path()),
"lora",
None,
16.0,
false,
Some(data_file.path()),
Some(output_file.path()),
None,
false,
3,
2e-4,
Some("0.5B"),
None,
5,
"apr,safetensors",
false,
None,
false,
None,
"cuda",
None,
None,
None,
None,
0,
&[],
None,
true,
false,
0,
);
match &result {
Ok(()) => {
let adapter = aprender::serialization::apr::AprReader::open(output_file.path())
.expect("adapter should be valid APR");
assert!(!adapter.tensors.is_empty(), "Adapter should have tensors");
}
Err(e) => {
let msg = format!("{e}");
assert!(
msg.contains("Missing model.norm.weight")
|| msg.contains("pipeline")
|| msg.contains("Configuration error"),
"Unexpected error (expected pipeline/config issue): {msg}"
);
}
}
}
#[test]
fn test_merge_creates_merged_model() {
let mut base_writer = aprender::serialization::apr::AprWriter::new();
base_writer.set_metadata("model_type", serde_json::json!("test"));
let q_data: Vec<f32> = vec![1.0; 64];
base_writer.add_tensor_f32(
"model.layers.0.self_attn.q_proj.weight",
vec![8, 8],
&q_data,
);
base_writer.add_tensor_f32("model.norm.weight", vec![8], &vec![1.0; 8]);
let base_file = NamedTempFile::with_suffix(".apr").expect("create base");
std::fs::write(base_file.path(), base_writer.to_bytes().expect("serialize")).expect("write");
let mut adapter_writer = aprender::serialization::apr::AprWriter::new();
adapter_writer.set_metadata("lora_rank", serde_json::json!(4));
adapter_writer.set_metadata("lora_alpha", serde_json::json!(8.0));
let lora_a: Vec<f32> = vec![0.1; 4 * 8]; adapter_writer.add_tensor_f32(
"model.layers.0.self_attn.q_proj.weight.lora_a",
vec![4, 8],
&lora_a,
);
let lora_b: Vec<f32> = vec![0.05; 8 * 4]; adapter_writer.add_tensor_f32(
"model.layers.0.self_attn.q_proj.weight.lora_b",
vec![8, 4],
&lora_b,
);
let adapter_file = NamedTempFile::with_suffix(".apr").expect("create adapter");
std::fs::write(
adapter_file.path(),
adapter_writer.to_bytes().expect("serialize"),
)
.expect("write");
let output_file = NamedTempFile::with_suffix(".apr").expect("create output");
let result = run_merge(
Some(base_file.path()),
Some(adapter_file.path()),
Some(output_file.path()),
true,
);
assert!(result.is_ok(), "Merge should succeed: {result:?}");
let merged = aprender::serialization::apr::AprReader::open(output_file.path())
.expect("merged should be valid APR");
assert_eq!(merged.tensors.len(), 2); let q_merged = merged
.read_tensor_f32("model.layers.0.self_attn.q_proj.weight")
.expect("should have q_proj");
assert!(
q_merged.iter().any(|&v| (v - 1.0).abs() > 1e-6),
"Merged weights should differ from base"
);
}
#[cfg(test)]
fn build_tiny_qwen2_base_v2(hidden: usize) -> Vec<u8> {
use aprender::format::v2::{AprV2Metadata, AprV2Writer};
let mut md = AprV2Metadata::new("pmat712-base");
md.architecture = Some("qwen2".to_string());
md.hidden_size = Some(hidden);
md.vocab_size = Some(64);
md.num_layers = Some(1);
md.num_heads = Some(4);
md.num_kv_heads = Some(2);
md.intermediate_size = Some(hidden);
md.max_position_embeddings = Some(128);
md.rope_theta = Some(1_000_000.0);
md.rms_norm_eps = Some(1e-6);
let mut w = AprV2Writer::new(md);
let sq = |n: usize| vec![0.02_f32; n];
w.add_f32_tensor(
"model.embed_tokens.weight",
vec![64, hidden],
&sq(64 * hidden),
);
w.add_f32_tensor("model.norm.weight", vec![hidden], &vec![1.0; hidden]);
w.add_f32_tensor(
"model.layers.0.self_attn.q_proj.weight",
vec![hidden, hidden],
&vec![1.0_f32; hidden * hidden],
);
w.add_f32_tensor(
"model.layers.0.self_attn.k_proj.weight",
vec![hidden, hidden],
&sq(hidden * hidden),
);
w.add_f32_tensor(
"model.layers.0.self_attn.v_proj.weight",
vec![hidden, hidden],
&sq(hidden * hidden),
);
w.add_f32_tensor(
"model.layers.0.self_attn.o_proj.weight",
vec![hidden, hidden],
&sq(hidden * hidden),
);
w.add_f32_tensor(
"model.layers.0.input_layernorm.weight",
vec![hidden],
&vec![1.0; hidden],
);
w.add_f32_tensor(
"model.layers.0.post_attention_layernorm.weight",
vec![hidden],
&vec![1.0; hidden],
);
w.add_f32_tensor(
"model.layers.0.mlp.gate_proj.weight",
vec![hidden, hidden],
&sq(hidden * hidden),
);
w.add_f32_tensor(
"model.layers.0.mlp.up_proj.weight",
vec![hidden, hidden],
&sq(hidden * hidden),
);
w.add_f32_tensor(
"model.layers.0.mlp.down_proj.weight",
vec![hidden, hidden],
&sq(hidden * hidden),
);
w.write().expect("write base v2")
}
#[cfg(test)]
fn build_tiny_lora_adapter_v2(hidden: usize, rank: usize, alpha: f64) -> Vec<u8> {
use aprender::format::v2::{AprV2Metadata, AprV2Writer};
let mut md = AprV2Metadata::new("pmat712-adapter");
md.custom
.insert("lora_rank".to_string(), serde_json::json!(rank));
md.custom
.insert("lora_alpha".to_string(), serde_json::json!(alpha));
let mut w = AprV2Writer::new(md);
w.add_f32_tensor(
"model.layers.0.self_attn.q_proj.weight.lora_a",
vec![rank, hidden],
&vec![0.3_f32; rank * hidden],
);
w.add_f32_tensor(
"model.layers.0.self_attn.q_proj.weight.lora_b",
vec![hidden, rank],
&vec![0.5_f32; hidden * rank],
);
w.write().expect("write adapter v2")
}
#[test]
fn test_lora_to_gguf_export_roundtrip_pmat712() {
use aprender::format::gguf::{load_gguf_tensors, GgufReader};
use aprender::format::{apr_export, ExportFormat, ExportOptions};
let hidden = 256usize;
let rank = 8usize;
let alpha = 16.0f64;
let base_file = NamedTempFile::with_suffix(".apr").expect("base tmp");
std::fs::write(base_file.path(), build_tiny_qwen2_base_v2(hidden)).expect("write base");
let adapter_file = NamedTempFile::with_suffix(".apr").expect("adapter tmp");
std::fs::write(
adapter_file.path(),
build_tiny_lora_adapter_v2(hidden, rank, alpha),
)
.expect("write adapter");
let merged_apr = NamedTempFile::with_suffix(".apr").expect("merged tmp");
let merge_res = run_merge(
Some(base_file.path()),
Some(adapter_file.path()),
Some(merged_apr.path()),
true,
);
assert!(merge_res.is_ok(), "LoRA merge must succeed: {merge_res:?}");
let gguf_file = NamedTempFile::with_suffix(".gguf").expect("gguf tmp");
let opts = ExportOptions {
format: ExportFormat::Gguf,
quantize: None,
include_tokenizer: false,
include_config: false,
skip_completeness_check: true,
};
let report =
apr_export(merged_apr.path(), gguf_file.path(), opts).expect("GGUF export must succeed");
assert_eq!(report.format, ExportFormat::Gguf);
assert!(gguf_file.path().exists(), "GGUF file must be written");
let gguf = GgufReader::from_file(gguf_file.path())
.expect("F-LORA-GGUF-001: GGUF must be parseable (valid magic/version)");
assert!(
gguf.version >= 2,
"GGUF version must be >= 2, got {}",
gguf.version
);
assert_eq!(
gguf.architecture().as_deref(),
Some("qwen2"),
"F-LORA-GGUF-002: architecture must round-trip as qwen2"
);
assert_eq!(
gguf.hidden_size(),
Some(hidden),
"F-LORA-GGUF-002: embedding_length must match base hidden_size"
);
assert_eq!(
gguf.num_layers(),
Some(1),
"F-LORA-GGUF-002: block_count must match base num_layers"
);
assert_eq!(
gguf.tensor_count as usize,
gguf.tensors.len(),
"tensor_count header must match parsed tensors"
);
let q_name = "blk.0.attn_q.weight";
let q_meta = gguf
.tensors
.iter()
.find(|t| t.name == q_name)
.unwrap_or_else(|| {
panic!(
"F-LORA-GGUF-003: GGUF must contain {q_name}; got {:?}",
gguf.tensors
.iter()
.map(|t| t.name.clone())
.collect::<Vec<_>>()
)
});
assert_eq!(
q_meta.dims,
vec![hidden as u64, hidden as u64],
"q_proj dims must be [hidden,hidden]"
);
assert_eq!(
q_meta.dtype, 0,
"F32 GgmlType is 0 (no quantization requested)"
);
let _ = q_meta; let tensors = load_gguf_tensors(gguf_file.path())
.expect("F-LORA-GGUF-004: GGUF tensors must load back as F32");
let (q_data, q_shape) = tensors
.get(q_name)
.expect("loaded GGUF must contain blk.0.attn_q.weight");
assert_eq!(
q_data.len(),
hidden * hidden,
"q_proj element count must be hidden^2"
);
assert_eq!(
q_shape.iter().product::<usize>(),
hidden * hidden,
"q_proj shape product"
);
let delta_present = q_data.iter().any(|&v| (v - 1.0).abs() > 1e-4);
assert!(
delta_present,
"F-LORA-GGUF-004: merged q_proj must differ from base (LoRA delta survived merge→GGUF)"
);
}
#[cfg(test)]
fn build_tiny_qwen2_base_beat(hidden: usize) -> Vec<u8> {
use aprender::format::v2::{AprV2Metadata, AprV2Writer};
let mut md = AprV2Metadata::new("pmat712-beat-base");
md.architecture = Some("qwen2".to_string());
md.hidden_size = Some(hidden);
md.vocab_size = Some(64);
md.num_layers = Some(1);
md.num_heads = Some(4);
md.num_kv_heads = Some(2);
md.intermediate_size = Some(hidden);
md.max_position_embeddings = Some(128);
md.rope_theta = Some(1_000_000.0);
md.rms_norm_eps = Some(1e-6);
let mut qw = vec![0.0_f32; hidden * hidden];
for r in 0..hidden {
for c in 0..hidden {
qw[r * hidden + c] = (((2 * r) as f32) - (c as f32) + 0.5).sin() * 0.5;
}
}
let sq = |n: usize| vec![0.02_f32; n];
let mut w = AprV2Writer::new(md);
w.add_f32_tensor(
"model.embed_tokens.weight",
vec![64, hidden],
&sq(64 * hidden),
);
w.add_f32_tensor("model.norm.weight", vec![hidden], &vec![1.0; hidden]);
w.add_f32_tensor(
"model.layers.0.self_attn.q_proj.weight",
vec![hidden, hidden],
&qw,
);
w.add_f32_tensor(
"model.layers.0.self_attn.k_proj.weight",
vec![hidden, hidden],
&sq(hidden * hidden),
);
w.add_f32_tensor(
"model.layers.0.self_attn.v_proj.weight",
vec![hidden, hidden],
&sq(hidden * hidden),
);
w.add_f32_tensor(
"model.layers.0.self_attn.o_proj.weight",
vec![hidden, hidden],
&sq(hidden * hidden),
);
w.add_f32_tensor(
"model.layers.0.input_layernorm.weight",
vec![hidden],
&vec![1.0; hidden],
);
w.add_f32_tensor(
"model.layers.0.post_attention_layernorm.weight",
vec![hidden],
&vec![1.0; hidden],
);
w.add_f32_tensor(
"model.layers.0.mlp.gate_proj.weight",
vec![hidden, hidden],
&sq(hidden * hidden),
);
w.add_f32_tensor(
"model.layers.0.mlp.up_proj.weight",
vec![hidden, hidden],
&sq(hidden * hidden),
);
w.add_f32_tensor(
"model.layers.0.mlp.down_proj.weight",
vec![hidden, hidden],
&sq(hidden * hidden),
);
w.write().expect("write beat base v2")
}
#[cfg(test)]
fn build_tiny_lora_adapter_beat(hidden: usize, rank: usize, alpha: f64) -> Vec<u8> {
use aprender::format::v2::{AprV2Metadata, AprV2Writer};
let mut md = AprV2Metadata::new("pmat712-beat-adapter");
md.custom
.insert("lora_rank".to_string(), serde_json::json!(rank));
md.custom
.insert("lora_alpha".to_string(), serde_json::json!(alpha));
let mut a = vec![0.0_f32; rank * hidden];
for k in 0..rank {
for c in 0..hidden {
a[k * hidden + c] = (((k + 1) as f32) * 0.013 + (c as f32) * 0.0007).cos() * 0.1;
}
}
let mut b = vec![0.0_f32; hidden * rank];
for r in 0..hidden {
for k in 0..rank {
b[r * rank + k] = (((r + 3) as f32) * 0.011 - ((k + 1) as f32) * 0.019).sin() * 0.1;
}
}
let mut w = AprV2Writer::new(md);
w.add_f32_tensor(
"model.layers.0.self_attn.q_proj.weight.lora_a",
vec![rank, hidden],
&a,
);
w.add_f32_tensor(
"model.layers.0.self_attn.q_proj.weight.lora_b",
vec![hidden, rank],
&b,
);
w.write().expect("write beat adapter v2")
}
#[cfg(test)]
fn rowmajor_matvec(w: &[f32], x: &[f32], rows: usize, cols: usize) -> Vec<f32> {
assert_eq!(w.len(), rows * cols, "weight element count");
assert_eq!(x.len(), cols, "input length must equal cols");
(0..rows)
.map(|r| {
let row = &w[r * cols..r * cols + cols];
row.iter().zip(x).map(|(wv, xv)| wv * xv).sum::<f32>()
})
.collect()
}
#[test]
fn beat_lora_gguf_lossless_deploy_pmat712() {
use aprender::format::gguf::load_gguf_tensors;
use aprender::format::rosetta::RosettaStone;
use aprender::format::{apr_export, ExportFormat, ExportOptions};
let hidden = 256usize;
let rank = 8usize;
let alpha = 16.0f64;
let base_file = NamedTempFile::with_suffix(".apr").expect("base tmp");
std::fs::write(base_file.path(), build_tiny_qwen2_base_beat(hidden)).expect("write base");
let adapter_file = NamedTempFile::with_suffix(".apr").expect("adapter tmp");
std::fs::write(
adapter_file.path(),
build_tiny_lora_adapter_beat(hidden, rank, alpha),
)
.expect("write adapter");
let merged_apr = NamedTempFile::with_suffix(".apr").expect("merged tmp");
let merge_res = run_merge(
Some(base_file.path()),
Some(adapter_file.path()),
Some(merged_apr.path()),
true,
);
assert!(merge_res.is_ok(), "LoRA merge must succeed: {merge_res:?}");
let gguf_file = NamedTempFile::with_suffix(".gguf").expect("gguf tmp");
let opts = ExportOptions {
format: ExportFormat::Gguf,
quantize: None, include_tokenizer: false,
include_config: false,
skip_completeness_check: true,
};
apr_export(merged_apr.path(), gguf_file.path(), opts).expect("F32 GGUF export must succeed");
let rosetta = RosettaStone::new();
let w_apr = rosetta
.load_tensor_f32(merged_apr.path(), "model.layers.0.self_attn.q_proj.weight")
.expect("load merged q_proj from apr");
let q_name = "blk.0.attn_q.weight";
let gguf_tensors =
load_gguf_tensors(gguf_file.path()).expect("load merged q_proj from exported GGUF");
let (w_gguf, gguf_shape) = gguf_tensors
.get(q_name)
.expect("exported GGUF must contain blk.0.attn_q.weight");
assert_eq!(
w_apr.len(),
hidden * hidden,
"F-LOSSLESS-001: apr q_proj must be hidden^2"
);
assert_eq!(
w_gguf.len(),
hidden * hidden,
"F-LOSSLESS-001: gguf q_proj must be hidden^2"
);
assert_eq!(
gguf_shape.iter().product::<usize>(),
hidden * hidden,
"F-LOSSLESS-001: gguf q_proj shape product"
);
let x: Vec<f32> = (0..hidden)
.map(|i| ((i as f32) * 0.017 + 0.31).sin())
.collect();
let y_apr = rowmajor_matvec(&w_apr, &x, hidden, hidden);
let y_gguf = rowmajor_matvec(w_gguf, &x, hidden, hidden);
let y_min = y_apr.iter().cloned().fold(f32::INFINITY, f32::min);
let y_max = y_apr.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
assert!(
(y_max - y_min) > 1e-2,
"F-LOSSLESS-002: forward output must be non-degenerate (spread {} too small)",
y_max - y_min
);
let max_abs_dlogit = y_apr
.iter()
.zip(&y_gguf)
.map(|(a, b)| (a - b).abs())
.fold(0.0_f32, f32::max);
let y_scale = y_max.abs().max(y_min.abs()).max(1.0);
eprintln!(
"[BEAT pmat712] forward equivalence: max|Δy| = {max_abs_dlogit:.3e} \
(output scale ≈ {y_scale:.3}, tolerance 1e-4)"
);
assert!(
max_abs_dlogit <= 1e-4,
"F-LOSSLESS-003 (BEAT): F32 GGUF export must be forward-lossless — \
max|y_apr − y_gguf| = {max_abs_dlogit:.3e} exceeds 1e-4. A lost/garbled \
weight, layout transpose, or shape/metadata mismatch in export breaks this."
);
let mut w_apr_t = vec![0.0_f32; hidden * hidden];
for r in 0..hidden {
for c in 0..hidden {
w_apr_t[c * hidden + r] = w_apr[r * hidden + c];
}
}
let y_apr_t = rowmajor_matvec(&w_apr_t, &x, hidden, hidden);
let max_abs_transpose_gap = y_apr
.iter()
.zip(&y_apr_t)
.map(|(a, b)| (a - b).abs())
.fold(0.0_f32, f32::max);
assert!(
max_abs_transpose_gap > 100.0 * max_abs_dlogit.max(1e-6),
"F-LOSSLESS-004: test must be transpose-sensitive — y(W) vs y(Wᵀ) gap \
({max_abs_transpose_gap:.3e}) should dwarf the apr↔gguf gap \
({max_abs_dlogit:.3e}); otherwise the BEAT could not catch a transpose bug"
);
}