#![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 to verify TFT attention cache LRU behavior /// /// This test ensures the unbounded HashMap memory leak fix (2025-10-25) /// works correctly by validating: /// - Cache size never exceeds MAX_CACHE_ENTRIES (2000) /// - LRU eviction happens automatically /// - Memory is bounded even with many insertions use anyhow::Result; use ml::tft::{TFTConfig, TFTState}; use ml_supervised::gpu_tensor::GpuTensor; use std::sync::Arc; /// Shared CUDA stream for test GpuTensor creation. fn test_stream() -> Arc { let ctx = cudarc::driver::CudaContext::new(0).expect("CUDA device required"); ctx.new_stream().expect("CUDA stream required") } #[test] fn test_tft_state_lru_cache_bounds() -> Result<()> { let config = TFTConfig::default(); let mut state = TFTState::zeros(&config)?; let stream = test_stream(); // Verify initial state assert_eq!(state.attention_cache.len(), 0, "Cache should start empty"); // Insert 3000 entries (exceeds MAX_CACHE_ENTRIES of 2000) for i in 0..3000 { let key = format!("cache_key_{}", i); let value = GpuTensor::zeros(&[8, 64], &stream)?; state.attention_cache.put(key, value); } // Cache should be capped at MAX_CACHE_ENTRIES (2000) assert_eq!( state.attention_cache.len(), TFTState::MAX_CACHE_ENTRIES, "Cache should never exceed MAX_CACHE_ENTRIES (2000)" ); // Oldest entries (0-999) should be evicted by LRU policy assert!( state.attention_cache.get("cache_key_0").is_none(), "Oldest entry should be evicted" ); assert!( state.attention_cache.get("cache_key_999").is_none(), "Old entries should be evicted" ); // Newest entries (1000-2999) should still be present assert!( state.attention_cache.get("cache_key_1000").is_some(), "Recent entry should be retained" ); assert!( state.attention_cache.get("cache_key_2999").is_some(), "Newest entry should be retained" ); Ok(()) } #[test] fn test_tft_state_cache_max_entries_constant() { // Verify MAX_CACHE_ENTRIES is sensible assert_eq!( TFTState::MAX_CACHE_ENTRIES, 2000, "MAX_CACHE_ENTRIES should be 2000 (chosen for ~48MB overhead, 60% speedup)" ); } #[test] fn test_tft_state_creation_with_lru() -> Result<()> { let config = TFTConfig::default(); let state = TFTState::zeros(&config)?; // Verify state is created with correct initial values assert!(state.hidden_state.is_none(), "Hidden state should be None"); assert_eq!(state.last_update, 0, "Last update should be 0"); assert_eq!(state.attention_cache.len(), 0, "Cache should be empty"); Ok(()) } #[test] fn test_lru_eviction_order() -> Result<()> { let config = TFTConfig::default(); let mut state = TFTState::zeros(&config)?; let stream = test_stream(); // Insert exactly MAX_CACHE_ENTRIES items for i in 0..TFTState::MAX_CACHE_ENTRIES { let key = format!("key_{}", i); let value = GpuTensor::zeros(&[4, 32], &stream)?; state.attention_cache.put(key, value); } assert_eq!(state.attention_cache.len(), TFTState::MAX_CACHE_ENTRIES); // Access key_500 to make it recently used let _ = state.attention_cache.get("key_500"); // Insert one more item (should evict key_0, the least recently used) let new_value = GpuTensor::zeros(&[4, 32], &stream)?; state.attention_cache.put("new_key".to_string(), new_value); // key_0 should be evicted (oldest) assert!( state.attention_cache.get("key_0").is_none(), "key_0 should be evicted as LRU" ); // key_500 should still be present (was accessed recently) assert!( state.attention_cache.get("key_500").is_some(), "key_500 should remain (recently accessed)" ); // new_key should be present assert!( state.attention_cache.get("new_key").is_some(), "new_key should be present" ); Ok(()) }