Files
foxhunt/crates/ml/tests/test_dbn_sequence_256_features.rs
jgrusewski 5d6e79263c fix: migrate remaining 8 test files to GPU types — all test errors fixed
tft_real_dbn_data: StreamTensor::from_vec, quantile loss returns f32
ppo_recurrent_integration: PPO::new() API, get_policy_state &[f32]
test_dbn_sequence_256: to_host + manual indexing instead of .i() ops
ppo_checkpoint_roundtrip: save/load_checkpoint(&PathBuf) API
mamba2_accuracy_fix: pure f64 arithmetic, no GPU tensors needed
ppo_lstm_training_loop: PPO::new() API
ppo_step_counter_fix: new checkpoint API
ppo_recurrent_performance: forward_host, LSTM batch_size arg

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 08:59:52 +01:00

468 lines
14 KiB
Rust

#![allow(
clippy::assertions_on_constants,
clippy::assertions_on_result_states,
clippy::clone_on_copy,
clippy::decimal_literal_representation,
clippy::doc_markdown,
clippy::empty_line_after_doc_comments,
clippy::field_reassign_with_default,
clippy::get_unwrap,
clippy::identity_op,
clippy::inconsistent_digit_grouping,
clippy::indexing_slicing,
clippy::integer_division,
clippy::len_zero,
clippy::let_underscore_must_use,
clippy::manual_div_ceil,
clippy::manual_let_else,
clippy::manual_range_contains,
clippy::modulo_arithmetic,
clippy::needless_range_loop,
clippy::non_ascii_literal,
clippy::redundant_clone,
clippy::shadow_reuse,
clippy::shadow_same,
clippy::shadow_unrelated,
clippy::single_match_else,
clippy::str_to_string,
clippy::string_slice,
clippy::tests_outside_test_module,
clippy::too_many_lines,
clippy::unnecessary_wraps,
clippy::unseparated_literal_suffix,
clippy::use_debug,
clippy::useless_vec,
clippy::wildcard_enum_match_arm,
clippy::else_if_without_else,
clippy::expect_used,
clippy::missing_const_for_fn,
clippy::similar_names,
clippy::type_complexity,
clippy::collapsible_else_if,
clippy::doc_lazy_continuation,
clippy::items_after_test_module,
clippy::map_clone,
clippy::multiple_unsafe_ops_per_block,
clippy::unwrap_or_default,
clippy::assign_op_pattern,
clippy::needless_borrow,
clippy::println_empty_string,
clippy::unnecessary_cast,
clippy::used_underscore_binding,
clippy::create_dir,
clippy::implicit_saturating_sub,
clippy::exit,
clippy::expect_fun_call,
clippy::too_many_arguments,
clippy::unnecessary_map_or,
clippy::unwrap_used,
dead_code,
unused_imports,
unused_variables,
clippy::cloned_ref_to_slice_refs,
clippy::neg_multiply,
clippy::while_let_loop,
clippy::bool_assert_comparison,
clippy::excessive_precision,
clippy::trivially_copy_pass_by_ref,
clippy::op_ref,
clippy::redundant_closure,
clippy::unnecessary_lazy_evaluations,
clippy::if_then_some_else_none,
clippy::unnecessary_to_owned,
clippy::single_component_path_imports,
)]
//! Test DbnSequenceLoader produces correct 256-dimensional features
//!
//! Validates that the fixed DbnSequenceLoader correctly extracts and pads
//! features to exactly 256 dimensions for MAMBA-2 training.
use anyhow::Result;
// candle eliminated — test uses native APIs
use cudarc::driver::CudaContext;
use ml::data_loaders::DbnSequenceLoader;
use std::env;
use std::path::PathBuf;
use std::sync::Arc;
use tracing::info;
use tracing::warn;
/// Get test data directory path
fn get_test_data_dir() -> PathBuf {
// Try CARGO_MANIFEST_DIR first (works in tests)
if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
PathBuf::from(manifest_dir)
.parent()
.unwrap()
.join("test_data/real/databento/ml_training_small")
} else {
// Fallback to relative path from project root
PathBuf::from("test_data/real/databento/ml_training_small")
}
}
#[tokio::test]
async fn test_feature_dimension_256() -> Result<()> {
info!("Testing DbnSequenceLoader 256-dimensional features");
let test_dir = get_test_data_dir();
if !test_dir.exists() {
warn!(test_dir = ?test_dir, "Test data not found, skipping test");
return Ok(());
}
// Create loader with d_model=256
let mut loader = DbnSequenceLoader::with_limits(60, 256, Some(10), 10).await?;
info!("Created DbnSequenceLoader (seq_len=60, d_model=256, max=10, stride=10)");
// Load sequences
info!(test_dir = ?test_dir, "Loading sequences");
let (train_data, val_data) = loader.load_sequences(&test_dir, 0.9).await?;
let total_sequences = train_data.len() + val_data.len();
info!(
total_sequences,
train = train_data.len(),
val = val_data.len(),
"Loaded sequences"
);
// Verify at least some data was loaded
assert!(total_sequences > 0, "Should load at least some sequences");
// Test 1: Verify input tensor dimensions
info!("Test 1: Verifying input tensor dimensions");
for (idx, (input, _target)) in train_data.iter().take(5).enumerate() {
let input_dims = input.dims();
info!(idx, input_dims = ?input_dims, "Sequence input shape");
// Input should be [batch=1, seq_len=60, d_model=256]
assert_eq!(
input_dims.len(),
3,
"Input should be 3D (batch, seq_len, features), got {:?}",
input_dims
);
assert_eq!(
input_dims[0], 1,
"Batch dimension should be 1, got {}",
input_dims[0]
);
assert_eq!(
input_dims[1], 60,
"Sequence length should be 60, got {}",
input_dims[1]
);
assert_eq!(
input_dims[2], 256,
"Feature dimension should be 256, got {}",
input_dims[2]
);
}
info!("All input tensors have correct shape [1, 60, 256]");
// Test 2: Verify target tensor dimensions
info!("Test 2: Verifying target tensor dimensions");
for (idx, (_input, target)) in train_data.iter().take(5).enumerate() {
let target_dims = target.dims();
info!(idx, target_dims = ?target_dims, "Sequence target shape");
// Target should be [batch=1, timesteps=1, d_model=256]
assert_eq!(
target_dims.len(),
3,
"Target should be 3D, got {:?}",
target_dims
);
assert_eq!(
target_dims[0], 1,
"Target batch should be 1, got {}",
target_dims[0]
);
assert_eq!(
target_dims[1], 1,
"Target timesteps should be 1, got {}",
target_dims[1]
);
assert_eq!(
target_dims[2], 256,
"Target feature dim should be 256, got {}",
target_dims[2]
);
}
info!("All target tensors have correct shape [1, 1, 256]");
// Test 3: Verify feature values are normalized (not all zeros/NaN)
info!("Test 3: Verifying feature normalization");
let (first_input, _) = &train_data[0];
let stream = CudaContext::new(0)
.and_then(|ctx| ctx.new_stream())
.expect("CUDA required for readback");
let values_f32 = first_input.to_host(&stream)?;
let values: Vec<f64> = values_f32.iter().map(|&v| v as f64).collect();
// Check for NaN values
let nan_count = values.iter().filter(|v| v.is_nan()).count();
assert_eq!(nan_count, 0, "Found {} NaN values in features", nan_count);
info!("No NaN values detected");
// Check for all-zero sequences (should have some variation)
let non_zero_count = values.iter().filter(|v| v.abs() > 1e-6).count();
let non_zero_ratio = non_zero_count as f64 / values.len() as f64;
info!(
non_zero_count,
total = values.len(),
non_zero_pct = non_zero_ratio * 100.0,
"Non-zero values"
);
assert!(
non_zero_ratio > 0.01,
"Features appear to be all zeros (only {:.1}% non-zero)",
non_zero_ratio * 100.0
);
// Check value range (normalized features should be roughly in [-5, 5] range)
let min_val = values.iter().cloned().fold(f64::INFINITY, f64::min);
let max_val = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let mean = values.iter().sum::<f64>() / values.len() as f64;
info!(min_val, max_val, mean, "Value range");
// Normalized features should not have extreme outliers
assert!(
min_val > -100.0 && max_val < 100.0,
"Feature values seem unnormalized: range [{:.2}, {:.2}]",
min_val,
max_val
);
info!("Features are properly normalized");
// Test 4: Verify validation data has same properties
info!("Test 4: Verifying validation data");
if !val_data.is_empty() {
let (val_input, val_target) = &val_data[0];
assert_eq!(
val_input.dims(),
&[1, 60, 256],
"Validation input should be [1, 60, 256]"
);
assert_eq!(
val_target.dims(),
&[1, 1, 256],
"Validation target should be [1, 1, 256]"
);
info!("Validation data shapes correct");
info!(val_count = val_data.len(), "Validation sequences verified");
} else {
warn!("No validation data (split ratio may be too high)");
}
info!(
total_sequences,
train = train_data.len(),
val = val_data.len(),
"ALL TESTS PASSED: feature_dim=256, input=[1,60,256], target=[1,1,256], normalization=valid"
);
Ok(())
}
#[tokio::test]
async fn test_extract_features_dimension() -> Result<()> {
info!("Testing extract_features() returns 256 dimensions");
let test_dir = get_test_data_dir();
if !test_dir.exists() {
warn!("Test data not found, skipping test");
return Ok(());
}
// Create loader
let mut loader = DbnSequenceLoader::new(60, 256).await?;
info!("Created DbnSequenceLoader");
// Load sequences
let (train_data, _) = loader.load_sequences(&test_dir, 0.9).await?;
assert!(!train_data.is_empty(), "Should have training data");
// Get first sequence and verify it was created with 256-dim features
let (input, _) = &train_data[0];
// Input is [1, 60, 256] where 256 is the feature dimension
let feature_dim = input.dims()[2];
info!(feature_dim, "Feature dimension from tensor");
assert_eq!(
feature_dim, 256,
"Feature dimension should be 256, got {}",
feature_dim
);
info!("extract_features() correctly produces 256-dimensional features");
Ok(())
}
#[tokio::test]
async fn test_different_d_model_values() -> Result<()> {
info!("Testing different d_model values (128, 256, 512)");
let test_dir = get_test_data_dir();
if !test_dir.exists() {
warn!("Test data not found, skipping test");
return Ok(());
}
// Test different d_model values
let d_models = vec![128, 256, 512];
for d_model in d_models {
info!(d_model, "Testing d_model");
let mut loader = DbnSequenceLoader::with_limits(60, d_model, Some(5), 10).await?;
let (train_data, _) = loader.load_sequences(&test_dir, 0.9).await?;
if !train_data.is_empty() {
let (input, target) = &train_data[0];
// Verify input shape
assert_eq!(
input.dims()[2],
d_model,
"Input feature dim should be {}",
d_model
);
// Verify target shape
assert_eq!(
target.dims()[2],
d_model,
"Target feature dim should be {}",
d_model
);
info!(
d_model,
input_dims = ?input.dims(),
target_dims = ?target.dims(),
"d_model dimensions verified"
);
}
}
info!("All d_model values produce correct dimensions");
Ok(())
}
#[tokio::test]
async fn test_sequence_temporal_ordering() -> Result<()> {
info!("Testing temporal ordering of sequences");
let test_dir = get_test_data_dir();
if !test_dir.exists() {
warn!("Test data not found, skipping test");
return Ok(());
}
// Create loader with stride=1 to get consecutive sequences
let mut loader = DbnSequenceLoader::with_limits(10, 256, Some(3), 1).await?;
let (train_data, _) = loader.load_sequences(&test_dir, 0.9).await?;
if train_data.len() >= 2 {
info!("Comparing consecutive sequences");
let (seq1_input, _) = &train_data[0];
let (seq2_input, _) = &train_data[1];
// With stride=1, the second sequence should be a shifted version of the first
// seq1: [t0, t1, t2, ..., t9]
// seq2: [t1, t2, t3, ..., t10]
let stream = CudaContext::new(0)
.and_then(|ctx| ctx.new_stream())
.expect("CUDA required for readback");
// Download both to host for comparison
let seq1_host = seq1_input.to_host(&stream)?;
let seq2_host = seq2_input.to_host(&stream)?;
// seq1_input shape: [1, 10, 256], seq2_input shape: [1, 10, 256]
let d_model = seq1_input.shape()[2]; // 256
// Extract last 9 timesteps from seq1 and first 9 from seq2
// seq1[0, 1..10, :] vs seq2[0, 0..9, :]
let mut max_diff: f64 = 0.0;
for t in 0..9 {
for f in 0..d_model {
let seq1_val = seq1_host[(t + 1) * d_model + f] as f64;
let seq2_val = seq2_host[t * d_model + f] as f64;
let diff = (seq1_val - seq2_val).abs();
if diff > max_diff {
max_diff = diff;
}
}
}
info!(max_diff, "Max difference between overlapping windows");
assert!(
max_diff < 1e-6,
"Consecutive sequences should overlap with stride=1, max_diff={}",
max_diff
);
}
info!("Temporal ordering verified");
Ok(())
}
#[tokio::test]
async fn test_batch_processing() -> Result<()> {
info!("Testing batch processing with multiple sequences");
let test_dir = get_test_data_dir();
if !test_dir.exists() {
warn!("Test data not found, skipping test");
return Ok(());
}
// Load multiple sequences
let mut loader = DbnSequenceLoader::with_limits(60, 256, Some(100), 10).await?;
let (train_data, val_data) = loader.load_sequences(&test_dir, 0.8).await?;
let total = train_data.len() + val_data.len();
info!(total, "Loaded sequences");
// Verify all sequences have consistent dimensions
let mut valid_count = 0;
for (input, target) in train_data.iter().chain(val_data.iter()) {
if input.dims() == &[1, 60, 256] && target.dims() == &[1, 1, 256] {
valid_count += 1;
}
}
info!(valid_count, total, "Sequences with correct dimensions");
assert_eq!(
valid_count, total,
"All sequences should have correct dimensions"
);
info!("Batch processing verified");
Ok(())
}