test: fxcache roundtrip tests — f64, bf16, find, validation, empty
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
237
crates/ml/tests/fxcache_roundtrip_test.rs
Normal file
237
crates/ml/tests/fxcache_roundtrip_test.rs
Normal file
@@ -0,0 +1,237 @@
|
||||
//! Roundtrip tests for the `.fxcache` binary format.
|
||||
//!
|
||||
//! Covers f64/bf16 serialization, cache-key lookup, header validation,
|
||||
//! and the empty-bars error path.
|
||||
|
||||
use ml::fxcache::{find_fxcache, load_fxcache, write_fxcache, FxCacheData};
|
||||
use tempfile::TempDir;
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Deterministic feature vector for bar `i`.
|
||||
fn make_features(n: usize) -> Vec<[f64; 42]> {
|
||||
(0..n)
|
||||
.map(|i| {
|
||||
let mut row = [0.0_f64; 42];
|
||||
for j in 0..42 {
|
||||
row[j] = (i * 42 + j) as f64 * 0.01;
|
||||
}
|
||||
row
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Deterministic target vector for bar `i`.
|
||||
fn make_targets(n: usize) -> Vec<[f64; 4]> {
|
||||
(0..n)
|
||||
.map(|i| {
|
||||
let mut row = [0.0_f64; 4];
|
||||
for j in 0..4 {
|
||||
row[j] = 5000.0 + (i * 4 + j) as f64 * 0.25;
|
||||
}
|
||||
row
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Deterministic OFI vector for bar `i`.
|
||||
fn make_ofi(n: usize) -> Vec<[f64; 8]> {
|
||||
(0..n)
|
||||
.map(|i| {
|
||||
let mut row = [0.0_f64; 8];
|
||||
for j in 0..8 {
|
||||
row[j] = (i * 8 + j) as f64 * 0.001;
|
||||
}
|
||||
row
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A fixed 32-byte cache key for tests.
|
||||
fn test_cache_key() -> [u8; 32] {
|
||||
let mut key = [0u8; 32];
|
||||
for (i, slot) in key.iter_mut().enumerate() {
|
||||
*slot = (i as u8).wrapping_mul(7).wrapping_add(13);
|
||||
}
|
||||
key
|
||||
}
|
||||
|
||||
// ── Tests ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Write 100 bars as f64 (version 1), read back, verify bit-exact match.
|
||||
#[test]
|
||||
fn test_fxcache_f64_roundtrip() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("test_f64.fxcache");
|
||||
|
||||
let n = 100;
|
||||
let features = make_features(n);
|
||||
let targets = make_targets(n);
|
||||
let ofi = make_ofi(n);
|
||||
let key = test_cache_key();
|
||||
|
||||
let bytes_written =
|
||||
write_fxcache(&path, &features, &targets, &ofi, key, false).unwrap();
|
||||
assert!(bytes_written > 0, "expected nonzero bytes written");
|
||||
|
||||
let data: FxCacheData = load_fxcache(&path).unwrap();
|
||||
assert_eq!(data.bar_count, n);
|
||||
assert_eq!(data.cache_key, key);
|
||||
assert_eq!(data.features.len(), n);
|
||||
assert_eq!(data.targets.len(), n);
|
||||
assert_eq!(data.ofi.len(), n);
|
||||
|
||||
// Bit-exact match for f64 roundtrip.
|
||||
for i in 0..n {
|
||||
assert_eq!(
|
||||
data.features[i], features[i],
|
||||
"feature mismatch at bar {i}"
|
||||
);
|
||||
assert_eq!(
|
||||
data.targets[i], targets[i],
|
||||
"target mismatch at bar {i}"
|
||||
);
|
||||
assert_eq!(data.ofi[i], ofi[i], "ofi mismatch at bar {i}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Write 50 bars as bf16 (version 2), read back, verify within tolerance.
|
||||
/// bf16 has limited precision: ~3 significant digits.
|
||||
#[test]
|
||||
fn test_fxcache_bf16_roundtrip() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("test_bf16.fxcache");
|
||||
|
||||
let n = 50;
|
||||
let features = make_features(n);
|
||||
let targets = make_targets(n);
|
||||
let ofi = make_ofi(n);
|
||||
let key = test_cache_key();
|
||||
|
||||
let bytes_written =
|
||||
write_fxcache(&path, &features, &targets, &ofi, key, true).unwrap();
|
||||
assert!(bytes_written > 0, "expected nonzero bytes written");
|
||||
|
||||
let data: FxCacheData = load_fxcache(&path).unwrap();
|
||||
assert_eq!(data.bar_count, n);
|
||||
assert_eq!(data.cache_key, key);
|
||||
|
||||
// bf16 tolerance checks.
|
||||
for i in 0..n {
|
||||
for j in 0..42 {
|
||||
let diff = (data.features[i][j] - features[i][j]).abs();
|
||||
assert!(
|
||||
diff < 0.02,
|
||||
"feature[{i}][{j}]: expected {}, got {}, diff={diff}",
|
||||
features[i][j],
|
||||
data.features[i][j]
|
||||
);
|
||||
}
|
||||
for j in 0..4 {
|
||||
let diff = (data.targets[i][j] - targets[i][j]).abs();
|
||||
// bf16 at ~5000 has resolution of ~4, so allow up to 4.0.
|
||||
assert!(
|
||||
diff <= 4.0,
|
||||
"target[{i}][{j}]: expected {}, got {}, diff={diff}",
|
||||
targets[i][j],
|
||||
data.targets[i][j]
|
||||
);
|
||||
}
|
||||
for j in 0..8 {
|
||||
let diff = (data.ofi[i][j] - ofi[i][j]).abs();
|
||||
assert!(
|
||||
diff < 0.02,
|
||||
"ofi[{i}][{j}]: expected {}, got {}, diff={diff}",
|
||||
ofi[i][j],
|
||||
data.ofi[i][j]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a file with a known cache key, verify `find_fxcache` locates it.
|
||||
/// Also verify that a different key returns `None`.
|
||||
#[test]
|
||||
fn test_fxcache_find() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let key = test_cache_key();
|
||||
let hex_key = hex::encode(key);
|
||||
let path = dir.path().join(format!("{hex_key}.fxcache"));
|
||||
|
||||
let features = make_features(5);
|
||||
let targets = make_targets(5);
|
||||
let ofi = make_ofi(5);
|
||||
|
||||
write_fxcache(&path, &features, &targets, &ofi, key, false).unwrap();
|
||||
|
||||
// Should find the file.
|
||||
let found = find_fxcache(dir.path(), &key);
|
||||
assert!(found.is_some(), "expected to find fxcache file");
|
||||
assert_eq!(found.unwrap(), path);
|
||||
|
||||
// Wrong key should miss.
|
||||
let mut wrong_key = [0xFFu8; 32];
|
||||
wrong_key[0] = 0x00;
|
||||
let miss = find_fxcache(dir.path(), &wrong_key);
|
||||
assert!(miss.is_none(), "expected miss for wrong key");
|
||||
}
|
||||
|
||||
/// Write garbage bytes to a file, verify `load_fxcache` returns an error
|
||||
/// whose message contains "magic".
|
||||
#[test]
|
||||
fn test_fxcache_header_validation() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("garbage.fxcache");
|
||||
|
||||
// Write 128 bytes of garbage (more than header size).
|
||||
std::fs::write(&path, vec![0xABu8; 128]).unwrap();
|
||||
|
||||
let err = load_fxcache(&path).unwrap_err();
|
||||
let msg = format!("{err:#}");
|
||||
assert!(
|
||||
msg.to_lowercase().contains("magic"),
|
||||
"expected error about magic bytes, got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Attempting to write 0 bars should fail (writer rejects empty data).
|
||||
/// Attempting to read a hand-crafted header with bar_count=0 should also fail.
|
||||
#[test]
|
||||
fn test_fxcache_empty() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let path = dir.path().join("empty.fxcache");
|
||||
|
||||
// Writer rejects 0 bars.
|
||||
let features: Vec<[f64; 42]> = vec![];
|
||||
let targets: Vec<[f64; 4]> = vec![];
|
||||
let ofi: Vec<[f64; 8]> = vec![];
|
||||
let key = test_cache_key();
|
||||
|
||||
let err = write_fxcache(&path, &features, &targets, &ofi, key, false).unwrap_err();
|
||||
let msg = format!("{err:#}");
|
||||
assert!(
|
||||
msg.contains("0 bars") || msg.contains("empty"),
|
||||
"expected empty/0-bars error from writer, got: {msg}"
|
||||
);
|
||||
|
||||
// Reader also rejects a header with bar_count=0.
|
||||
// Hand-craft a valid-magic header with bar_count=0.
|
||||
let mut header = [0u8; 64];
|
||||
header[0..8].copy_from_slice(b"FXCACHE\0");
|
||||
header[8..10].copy_from_slice(&1u16.to_le_bytes()); // version=1
|
||||
header[10..12].copy_from_slice(&42u16.to_le_bytes()); // feat_dim
|
||||
header[12..14].copy_from_slice(&4u16.to_le_bytes()); // target_dim
|
||||
header[14..16].copy_from_slice(&8u16.to_le_bytes()); // ofi_dim
|
||||
header[16..24].copy_from_slice(&0u64.to_le_bytes()); // bar_count=0
|
||||
// cache_key + reserved stay zeroed.
|
||||
|
||||
let path_reader = dir.path().join("empty_header.fxcache");
|
||||
std::fs::write(&path_reader, &header).unwrap();
|
||||
|
||||
let err = load_fxcache(&path_reader).unwrap_err();
|
||||
let msg = format!("{err:#}");
|
||||
assert!(
|
||||
msg.contains("zero") || msg.contains("bar_count"),
|
||||
"expected bar_count/zero error from reader, got: {msg}"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user