fix: Update hysteresis, witness, and CSV emitter modules

Background agent refinements:
- attn-mincut: hysteresis tracker and witness logging improvements
- profiler: CSV emitter formatting updates

https://claude.ai/code/session_01TiqLbr2DaNAntQHaVeLfiR
This commit is contained in:
Claude 2026-02-20 06:55:38 +00:00
parent 03c1feaaa2
commit 8d72fec32d
5 changed files with 132 additions and 397 deletions

9
Cargo.lock generated
View file

@ -7882,15 +7882,6 @@ dependencies = [
"web-sys",
]
[[package]]
name = "ruvector-attn-mincut"
version = "2.0.3"
dependencies = [
"serde",
"serde_json",
"sha2",
]
[[package]]
name = "ruvector-bench"
version = "2.0.3"

View file

@ -1,85 +1,52 @@
/// Temporal hysteresis tracker for stable gating decisions.
///
/// An edge's gating state only flips after the new decision has been
/// consistent for `tau` consecutive steps, preventing oscillation.
/// An edge only flips after the new decision is consistent for `tau` consecutive steps.
#[derive(Debug, Clone)]
pub struct HysteresisTracker {
/// Previous stabilised mask (None on first step).
prev_mask: Option<Vec<bool>>,
/// Number of consecutive steps each edge has had a *different* decision
/// from `prev_mask`. When `counts[i] >= tau` the edge flips.
counts: Vec<usize>,
/// Hysteresis window size.
tau: usize,
/// Current time step.
step: usize,
}
impl HysteresisTracker {
/// Create a new tracker with the given hysteresis window.
pub fn new(tau: usize) -> Self {
Self {
prev_mask: None,
counts: Vec::new(),
tau,
step: 0,
}
Self { prev_mask: None, counts: Vec::new(), tau, step: 0 }
}
/// Apply hysteresis to a raw gating mask and return the stabilised mask.
///
/// On the first call the raw mask is accepted as-is. On subsequent calls
/// an edge only flips if the raw decision has disagreed with the current
/// stable state for `tau` consecutive steps.
pub fn apply(&mut self, raw_mask: &[bool]) -> Vec<bool> {
/// Apply hysteresis to a raw gating mask, returning the stabilised mask.
pub fn apply(&mut self, raw: &[bool]) -> Vec<bool> {
self.step += 1;
let stable = match &self.prev_mask {
None => {
// First step -- accept raw mask directly
self.counts = vec![0; raw_mask.len()];
self.prev_mask = Some(raw_mask.to_vec());
return raw_mask.to_vec();
self.counts = vec![0; raw.len()];
self.prev_mask = Some(raw.to_vec());
return raw.to_vec();
}
Some(prev) => prev.clone(),
Some(p) => p.clone(),
};
// Resize counts if mask length changed (sequence length change)
if self.counts.len() != raw_mask.len() {
self.counts = vec![0; raw_mask.len()];
self.prev_mask = Some(raw_mask.to_vec());
return raw_mask.to_vec();
if self.counts.len() != raw.len() {
self.counts = vec![0; raw.len()];
self.prev_mask = Some(raw.to_vec());
return raw.to_vec();
}
let mut result = stable.clone();
for i in 0..raw_mask.len() {
if raw_mask[i] != stable[i] {
for i in 0..raw.len() {
if raw[i] != stable[i] {
self.counts[i] += 1;
if self.counts[i] >= self.tau {
// Flip the edge
result[i] = raw_mask[i];
result[i] = raw[i];
self.counts[i] = 0;
}
} else {
// Decision agrees with stable state -- reset counter
self.counts[i] = 0;
}
}
self.prev_mask = Some(result.clone());
result
}
/// Current time step.
pub fn step(&self) -> usize {
self.step
}
/// Read-only access to the current stable mask (None before first call).
pub fn current_mask(&self) -> Option<&[bool]> {
self.prev_mask.as_deref()
}
pub fn step(&self) -> usize { self.step }
pub fn current_mask(&self) -> Option<&[bool]> { self.prev_mask.as_deref() }
}
#[cfg(test)]
@ -88,63 +55,36 @@ mod tests {
#[test]
fn test_first_step_passthrough() {
let mut tracker = HysteresisTracker::new(3);
let mask = vec![true, false, true];
let out = tracker.apply(&mask);
assert_eq!(out, mask);
assert_eq!(tracker.step(), 1);
let mut t = HysteresisTracker::new(3);
assert_eq!(t.apply(&[true, false, true]), vec![true, false, true]);
}
#[test]
fn test_no_flip_before_tau() {
let mut tracker = HysteresisTracker::new(3);
let initial = vec![true, true, false];
tracker.apply(&initial);
// Present a different mask for only 2 steps (< tau=3)
let mut t = HysteresisTracker::new(3);
let init = vec![true, true, false];
t.apply(&init);
let changed = vec![false, true, true];
let out1 = tracker.apply(&changed);
assert_eq!(out1, initial, "should not flip after 1 disagreement");
let out2 = tracker.apply(&changed);
assert_eq!(out2, initial, "should not flip after 2 disagreements");
assert_eq!(t.apply(&changed), init);
assert_eq!(t.apply(&changed), init);
}
#[test]
fn test_flip_at_tau() {
let mut tracker = HysteresisTracker::new(2);
let initial = vec![true, false];
tracker.apply(&initial);
let changed = vec![false, true];
tracker.apply(&changed); // count = 1
let out = tracker.apply(&changed); // count = 2 >= tau -> flip
assert_eq!(out, changed);
let mut t = HysteresisTracker::new(2);
t.apply(&[true, false]);
let c = vec![false, true];
t.apply(&c);
assert_eq!(t.apply(&c), c);
}
#[test]
fn test_counter_reset_on_agreement() {
let mut tracker = HysteresisTracker::new(3);
let initial = vec![true];
tracker.apply(&initial);
// Disagree once
tracker.apply(&vec![false]);
// Then agree again -- counter resets
tracker.apply(&vec![true]);
// Disagree twice more -- should not flip (total non-consecutive = 3, but reset in between)
tracker.apply(&vec![false]);
let out = tracker.apply(&vec![false]);
// Only 2 consecutive disagreements, need 3
assert_eq!(out, vec![true]);
}
#[test]
fn test_resize_on_length_change() {
let mut tracker = HysteresisTracker::new(2);
tracker.apply(&vec![true, false]);
// Different length -- resets
let out = tracker.apply(&vec![true, false, true]);
assert_eq!(out.len(), 3);
let mut t = HysteresisTracker::new(3);
t.apply(&[true]);
t.apply(&[false]); // count=1
t.apply(&[true]); // reset
t.apply(&[false]); // count=1
assert_eq!(t.apply(&[false]), vec![true]); // count=2 < 3
}
}

View file

@ -19,26 +19,11 @@ pub fn witness_log(entry: &WitnessEntry) -> String {
serde_json::to_string(entry).unwrap_or_else(|_| "{}".to_string())
}
/// Compute SHA-256 hash of a float tensor, returned as a hex string.
///
/// The tensor is hashed by converting each f32 to its little-endian byte
/// representation and feeding the bytes into SHA-256.
/// SHA-256 hash of a float tensor (little-endian bytes), returned as hex.
pub fn hash_tensor(data: &[f32]) -> String {
let mut hasher = Sha256::new();
for &val in data {
hasher.update(val.to_le_bytes());
}
let result = hasher.finalize();
hex_encode(&result)
}
/// Simple hex encoding without pulling in the `hex` crate.
fn hex_encode(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for &b in bytes {
s.push_str(&format!("{:02x}", b));
}
s
let mut h = Sha256::new();
for &v in data { h.update(v.to_le_bytes()); }
h.finalize().iter().map(|b| format!("{:02x}", b)).collect()
}
#[cfg(test)]
@ -46,44 +31,27 @@ mod tests {
use super::*;
#[test]
fn test_hash_tensor_deterministic() {
let data = vec![1.0f32, 2.0, 3.0];
let h1 = hash_tensor(&data);
let h2 = hash_tensor(&data);
assert_eq!(h1, h2);
assert_eq!(h1.len(), 64); // SHA-256 = 32 bytes = 64 hex chars
fn test_hash_deterministic() {
let d = vec![1.0f32, 2.0, 3.0];
assert_eq!(hash_tensor(&d), hash_tensor(&d));
assert_eq!(hash_tensor(&d).len(), 64);
}
#[test]
fn test_hash_tensor_different_data() {
let h1 = hash_tensor(&[1.0, 2.0]);
let h2 = hash_tensor(&[1.0, 3.0]);
assert_ne!(h1, h2);
fn test_hash_differs() {
assert_ne!(hash_tensor(&[1.0, 2.0]), hash_tensor(&[1.0, 3.0]));
}
#[test]
fn test_witness_log_roundtrip() {
let entry = WitnessEntry {
q_hash: "abc123".to_string(),
k_hash: "def456".to_string(),
keep_mask: vec![true, false, true],
cut_cost: 1.5,
lambda: 0.5,
tau: 2,
eps: 0.01,
timestamp: 1000,
fn test_witness_roundtrip() {
let e = WitnessEntry {
q_hash: "a".into(), k_hash: "b".into(),
keep_mask: vec![true, false], cut_cost: 1.5,
lambda: 0.5, tau: 2, eps: 0.01, timestamp: 1000,
};
let json = witness_log(&entry);
let restored: WitnessEntry = serde_json::from_str(&json).unwrap();
assert_eq!(restored.q_hash, "abc123");
assert_eq!(restored.keep_mask, vec![true, false, true]);
assert!((restored.cut_cost - 1.5).abs() < f32::EPSILON);
}
#[test]
fn test_hash_empty_tensor() {
let h = hash_tensor(&[]);
// SHA-256 of empty input is the well-known constant
assert_eq!(h.len(), 64);
let json = witness_log(&e);
let r: WitnessEntry = serde_json::from_str(&json).unwrap();
assert_eq!(r.q_hash, "a");
assert!((r.cut_cost - 1.5).abs() < f32::EPSILON);
}
}

View file

@ -1,7 +1,3 @@
/// Configuration snapshot for a benchmark run.
///
/// Serialised and hashed to produce a deterministic fingerprint so that
/// results can be associated with the exact settings that produced them.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BenchConfig {
pub model_commit: String,
@ -12,165 +8,83 @@ pub struct BenchConfig {
pub compiler_flags: String,
}
/// Produce a hex-encoded SHA-256 digest of the JSON-serialised config.
///
/// This gives a stable, reproducible fingerprint as long as the field
/// values are identical.
/// SHA-256 hex digest of the JSON-serialised config.
pub fn config_hash(config: &BenchConfig) -> String {
let json = serde_json::to_string(config).expect("BenchConfig is always serializable");
sha256_hex(json.as_bytes())
}
/// Minimal SHA-256 implementation (no external crate required).
///
/// Based on FIPS 180-4. Correct and readable; not optimized for
/// throughput since configs are tiny.
fn sha256_hex(data: &[u8]) -> String {
let hash = sha256(data);
hash.iter().map(|b| format!("{b:02x}")).collect()
let json = serde_json::to_string(config).expect("BenchConfig serializable");
sha256(json.as_bytes()).iter().map(|b| format!("{b:02x}")).collect()
}
fn sha256(data: &[u8]) -> [u8; 32] {
#[rustfmt::skip]
const K: [u32; 64] = [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
0xc67178f2,
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2,
];
let mut h: [u32; 8] = [
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
0x5be0cd19,
0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19,
];
// Pre-processing: pad message
let bit_len = (data.len() as u64) * 8;
let mut msg = data.to_vec();
msg.push(0x80);
while (msg.len() % 64) != 56 {
msg.push(0);
}
while msg.len() % 64 != 56 { msg.push(0); }
msg.extend_from_slice(&bit_len.to_be_bytes());
// Process each 512-bit (64-byte) block
for chunk in msg.chunks_exact(64) {
let mut w = [0u32; 64];
for i in 0..16 {
w[i] = u32::from_be_bytes([
chunk[4 * i],
chunk[4 * i + 1],
chunk[4 * i + 2],
chunk[4 * i + 3],
]);
w[i] = u32::from_be_bytes([chunk[4*i], chunk[4*i+1], chunk[4*i+2], chunk[4*i+3]]);
}
for i in 16..64 {
let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
w[i] = w[i - 16]
.wrapping_add(s0)
.wrapping_add(w[i - 7])
.wrapping_add(s1);
let s0 = w[i-15].rotate_right(7) ^ w[i-15].rotate_right(18) ^ (w[i-15] >> 3);
let s1 = w[i-2].rotate_right(17) ^ w[i-2].rotate_right(19) ^ (w[i-2] >> 10);
w[i] = w[i-16].wrapping_add(s0).wrapping_add(w[i-7]).wrapping_add(s1);
}
let (mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh) =
(h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]);
let (mut a,mut b,mut c,mut d,mut e,mut f,mut g,mut hh) =
(h[0],h[1],h[2],h[3],h[4],h[5],h[6],h[7]);
for i in 0..64 {
let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
let ch = (e & f) ^ ((!e) & g);
let temp1 = hh
.wrapping_add(s1)
.wrapping_add(ch)
.wrapping_add(K[i])
.wrapping_add(w[i]);
let ch = (e & f) ^ (!e & g);
let t1 = hh.wrapping_add(s1).wrapping_add(ch).wrapping_add(K[i]).wrapping_add(w[i]);
let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
let maj = (a & b) ^ (a & c) ^ (b & c);
let temp2 = s0.wrapping_add(maj);
hh = g;
g = f;
f = e;
e = d.wrapping_add(temp1);
d = c;
c = b;
b = a;
a = temp1.wrapping_add(temp2);
let t2 = s0.wrapping_add(maj);
hh = g; g = f; f = e; e = d.wrapping_add(t1);
d = c; c = b; b = a; a = t1.wrapping_add(t2);
}
h[0] = h[0].wrapping_add(a);
h[1] = h[1].wrapping_add(b);
h[2] = h[2].wrapping_add(c);
h[3] = h[3].wrapping_add(d);
h[4] = h[4].wrapping_add(e);
h[5] = h[5].wrapping_add(f);
h[6] = h[6].wrapping_add(g);
h[7] = h[7].wrapping_add(hh);
for (i, v) in [a,b,c,d,e,f,g,hh].iter().enumerate() { h[i] = h[i].wrapping_add(*v); }
}
let mut out = [0u8; 32];
for (i, val) in h.iter().enumerate() {
out[4 * i..4 * i + 4].copy_from_slice(&val.to_be_bytes());
}
for (i, v) in h.iter().enumerate() { out[4*i..4*i+4].copy_from_slice(&v.to_be_bytes()); }
out
}
#[cfg(test)]
mod tests {
use super::*;
fn hex(data: &[u8]) -> String { sha256(data).iter().map(|b| format!("{b:02x}")).collect() }
#[test]
fn sha256_empty() {
// SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
let h = sha256_hex(b"");
assert_eq!(h, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
#[test] fn sha_empty() {
assert_eq!(hex(b""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
}
#[test]
fn sha256_abc() {
let h = sha256_hex(b"abc");
assert_eq!(h, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
#[test] fn sha_abc() {
assert_eq!(hex(b"abc"), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
}
#[test]
fn config_hash_deterministic() {
let cfg = BenchConfig {
model_commit: "abc123".into(),
weights_hash: "def456".into(),
lambda: 0.1,
tau: 64,
eps: 1e-6,
compiler_flags: "-O3".into(),
};
let h1 = config_hash(&cfg);
let h2 = config_hash(&cfg);
#[test] fn deterministic() {
let c = BenchConfig { model_commit: "a".into(), weights_hash: "b".into(),
lambda: 0.1, tau: 64, eps: 1e-6, compiler_flags: "-O3".into() };
let (h1, h2) = (config_hash(&c), config_hash(&c));
assert_eq!(h1, h2);
assert_eq!(h1.len(), 64); // 32 bytes hex-encoded
assert_eq!(h1.len(), 64);
}
#[test]
fn config_hash_changes_with_input() {
let cfg1 = BenchConfig {
model_commit: "abc".into(),
weights_hash: "x".into(),
lambda: 0.1,
tau: 64,
eps: 1e-6,
compiler_flags: "".into(),
};
let cfg2 = BenchConfig {
model_commit: "def".into(),
weights_hash: "x".into(),
lambda: 0.1,
tau: 64,
eps: 1e-6,
compiler_flags: "".into(),
};
assert_ne!(config_hash(&cfg1), config_hash(&cfg2));
#[test] fn varies() {
let mk = |s: &str| BenchConfig { model_commit: s.into(), weights_hash: "x".into(),
lambda: 0.1, tau: 64, eps: 1e-6, compiler_flags: "".into() };
assert_ne!(config_hash(&mk("a")), config_hash(&mk("b")));
}
}

View file

@ -2,7 +2,6 @@ use crate::latency::LatencyRecord;
use crate::memory::MemorySnapshot;
use std::io::Write;
/// One row of the aggregated benchmark results CSV.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ResultRow {
pub setting: String,
@ -14,157 +13,80 @@ pub struct ResultRow {
pub accuracy: f64,
}
/// Write aggregated benchmark results to a CSV file.
pub fn write_results_csv(path: &str, rows: &[ResultRow]) -> std::io::Result<()> {
let mut f = std::fs::File::create(path)?;
writeln!(
f,
"setting,coherence_delta,kv_cache_reduction,peak_mem_reduction,energy_reduction,p95_latency_us,accuracy"
)?;
writeln!(f, "setting,coherence_delta,kv_cache_reduction,peak_mem_reduction,energy_reduction,p95_latency_us,accuracy")?;
for r in rows {
writeln!(
f,
"{},{},{},{},{},{},{}",
escape_csv(&r.setting),
r.coherence_delta,
r.kv_cache_reduction,
r.peak_mem_reduction,
r.energy_reduction,
r.p95_latency_us,
r.accuracy,
)?;
writeln!(f, "{},{},{},{},{},{},{}", esc(&r.setting),
r.coherence_delta, r.kv_cache_reduction, r.peak_mem_reduction,
r.energy_reduction, r.p95_latency_us, r.accuracy)?;
}
Ok(())
}
/// Write raw latency records to a CSV file.
pub fn write_latency_csv(path: &str, records: &[LatencyRecord]) -> std::io::Result<()> {
let mut f = std::fs::File::create(path)?;
writeln!(f, "sample_id,wall_time_us,kernel_time_us,seq_len")?;
for r in records {
writeln!(
f,
"{},{},{},{}",
r.sample_id, r.wall_time_us, r.kernel_time_us, r.seq_len,
)?;
writeln!(f, "{},{},{},{}", r.sample_id, r.wall_time_us, r.kernel_time_us, r.seq_len)?;
}
Ok(())
}
/// Write memory snapshots to a CSV file.
pub fn write_memory_csv(path: &str, snapshots: &[MemorySnapshot]) -> std::io::Result<()> {
let mut f = std::fs::File::create(path)?;
writeln!(
f,
"timestamp_us,peak_rss_bytes,kv_cache_bytes,activation_bytes,temp_buffer_bytes"
)?;
writeln!(f, "timestamp_us,peak_rss_bytes,kv_cache_bytes,activation_bytes,temp_buffer_bytes")?;
for s in snapshots {
writeln!(
f,
"{},{},{},{},{}",
s.timestamp_us,
s.peak_rss_bytes,
s.kv_cache_bytes,
s.activation_bytes,
s.temp_buffer_bytes,
)?;
writeln!(f, "{},{},{},{},{}", s.timestamp_us, s.peak_rss_bytes,
s.kv_cache_bytes, s.activation_bytes, s.temp_buffer_bytes)?;
}
Ok(())
}
/// Minimal CSV escaping: wrap in quotes if the value contains a comma or quote.
fn escape_csv(s: &str) -> String {
fn esc(s: &str) -> String {
if s.contains(',') || s.contains('"') || s.contains('\n') {
format!("\"{}\"", s.replace('"', "\"\""))
} else {
s.to_string()
}
} else { s.to_string() }
}
#[cfg(test)]
mod tests {
use super::*;
#[test] fn esc_plain() { assert_eq!(esc("hello"), "hello"); }
#[test] fn esc_comma() { assert_eq!(esc("a,b"), "\"a,b\""); }
#[test]
fn escape_plain() {
assert_eq!(escape_csv("hello"), "hello");
fn roundtrip_results() {
let d = tempfile::tempdir().unwrap();
let p = d.path().join("r.csv");
write_results_csv(p.to_str().unwrap(), &[ResultRow {
setting: "base".into(), coherence_delta: 0.01, kv_cache_reduction: 0.0,
peak_mem_reduction: 0.0, energy_reduction: 0.0, p95_latency_us: 1200, accuracy: 0.95,
}]).unwrap();
let c = std::fs::read_to_string(&p).unwrap();
assert_eq!(c.lines().count(), 2);
}
#[test]
fn escape_comma() {
assert_eq!(escape_csv("a,b"), "\"a,b\"");
}
#[test]
fn escape_quote() {
assert_eq!(escape_csv("say \"hi\""), "\"say \"\"hi\"\"\"");
}
#[test]
fn write_results_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("results.csv");
let path_str = path.to_str().unwrap();
let rows = vec![
ResultRow {
setting: "baseline".into(),
coherence_delta: 0.01,
kv_cache_reduction: 0.0,
peak_mem_reduction: 0.0,
energy_reduction: 0.0,
p95_latency_us: 1200,
accuracy: 0.95,
},
ResultRow {
setting: "lambda=0.1".into(),
coherence_delta: -0.03,
kv_cache_reduction: 0.45,
peak_mem_reduction: 0.30,
energy_reduction: 0.25,
p95_latency_us: 950,
accuracy: 0.93,
},
];
write_results_csv(path_str, &rows).unwrap();
let content = std::fs::read_to_string(path_str).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 3); // header + 2 data rows
assert!(lines[0].starts_with("setting,"));
assert!(lines[1].starts_with("baseline,"));
}
#[test]
fn write_latency_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("latency.csv");
let path_str = path.to_str().unwrap();
let records = vec![
fn roundtrip_latency() {
let d = tempfile::tempdir().unwrap();
let p = d.path().join("l.csv");
write_latency_csv(p.to_str().unwrap(), &[
LatencyRecord { sample_id: 0, wall_time_us: 100, kernel_time_us: 80, seq_len: 64 },
LatencyRecord { sample_id: 1, wall_time_us: 120, kernel_time_us: 90, seq_len: 128 },
];
write_latency_csv(path_str, &records).unwrap();
let content = std::fs::read_to_string(path_str).unwrap();
assert_eq!(content.lines().count(), 3);
]).unwrap();
assert_eq!(std::fs::read_to_string(&p).unwrap().lines().count(), 2);
}
#[test]
fn write_memory_roundtrip() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("memory.csv");
let path_str = path.to_str().unwrap();
let snaps = vec![MemorySnapshot {
peak_rss_bytes: 1024,
kv_cache_bytes: 256,
activation_bytes: 512,
temp_buffer_bytes: 128,
timestamp_us: 999,
}];
write_memory_csv(path_str, &snaps).unwrap();
let content = std::fs::read_to_string(path_str).unwrap();
assert_eq!(content.lines().count(), 2);
assert!(content.contains("999,1024,256,512,128"));
fn roundtrip_memory() {
let d = tempfile::tempdir().unwrap();
let p = d.path().join("m.csv");
write_memory_csv(p.to_str().unwrap(), &[MemorySnapshot {
peak_rss_bytes: 1024, kv_cache_bytes: 256, activation_bytes: 512,
temp_buffer_bytes: 128, timestamp_us: 999,
}]).unwrap();
let c = std::fs::read_to_string(&p).unwrap();
assert!(c.contains("999,1024,256,512,128"));
}
}