use naga::front::wgsl;
use naga::valid::{Capabilities, ValidationFlags, Validator};
use std::time::Instant;
use wasm4pm::ml::linucb::{LinUCBAgent, N_ACTIONS, N_FEATURES};
const WGSL_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/src/gpu/linucb_kernel.wgsl");
const BATCH_SIZE: usize = 2048;
#[test]
fn wgsl_kernel_parses_without_error() {
let source = std::fs::read_to_string(WGSL_PATH)
.expect("WGSL kernel file must exist at src/gpu/linucb_kernel.wgsl");
let result = wgsl::parse_str(&source);
assert!(result.is_ok(), "WGSL parse failed: {}", result.unwrap_err());
}
#[test]
fn wgsl_kernel_validates_without_error() {
let source = std::fs::read_to_string(WGSL_PATH).expect("WGSL kernel file must exist");
let module = wgsl::parse_str(&source).expect("WGSL must parse before validation");
let mut validator = Validator::new(ValidationFlags::all(), Capabilities::empty());
let result = validator.validate(&module);
assert!(
result.is_ok(),
"WGSL validation failed: {}",
result.unwrap_err()
);
}
#[test]
fn wgsl_kernel_has_two_entry_points() {
let source = std::fs::read_to_string(WGSL_PATH).expect("WGSL kernel file must exist");
let module = wgsl::parse_str(&source).expect("must parse");
assert_eq!(
module.entry_points.len(),
2,
"Expected 2 entry points (linucb_select + linucb_update), got {}",
module.entry_points.len()
);
}
#[test]
fn wgsl_kernel_workgroup_size_is_256() {
let source = std::fs::read_to_string(WGSL_PATH).expect("WGSL kernel file must exist");
let module = wgsl::parse_str(&source).expect("must parse");
let compute_ep = module
.entry_points
.iter()
.find(|ep| ep.stage == naga::ShaderStage::Compute)
.expect("Must have at least one compute entry point");
assert_eq!(
compute_ep.workgroup_size[0], 256,
"workgroup_size[0] must be 256, got {}",
compute_ep.workgroup_size[0]
);
}
#[test]
fn wgsl_kernel_has_expected_buffer_bindings() {
let source = std::fs::read_to_string(WGSL_PATH).expect("WGSL kernel file must exist");
let module = wgsl::parse_str(&source).expect("must parse");
let binding_count = module
.global_variables
.iter()
.filter(|(_, v)| v.binding.is_some())
.count();
assert_eq!(
binding_count, 11,
"Expected 11 buffer bindings (6 select + 5 update), got {binding_count}"
);
}
#[test]
fn wgsl_kernel_vram_estimate_within_32mb() {
let vram_bytes: usize = (BATCH_SIZE * N_FEATURES * 4)
+ (N_ACTIONS * N_FEATURES * 4)
+ (N_FEATURES * N_FEATURES * 4)
+ 4
+ (BATCH_SIZE * 4)
+ (BATCH_SIZE * 4);
let total_mb = (vram_bytes as f64 * 1.10) / (1024.0 * 1024.0);
assert!(
total_mb <= 32.0,
"VRAM estimate {total_mb:.3} MB exceeds 32 MB limit"
);
}
#[test]
fn wgsl_kernel_does_not_use_reserved_keyword_thread_local() {
let source = std::fs::read_to_string(WGSL_PATH).expect("WGSL kernel file must exist");
for (line_no, line) in source.lines().enumerate() {
let code_part = if let Some(pos) = line.find("//") {
&line[..pos]
} else {
line
};
assert!(
!code_part.contains("thread_local"),
"Line {}: WGSL code uses reserved keyword 'thread_local': {}",
line_no + 1,
line
);
}
}
#[test]
fn cpu_linucb_batch_is_deterministic() {
let agent = LinUCBAgent::new();
let mut state: u64 = 0xDEADBEEFCAFEBABE_u64;
let batch: Vec<[f32; N_FEATURES]> = (0..BATCH_SIZE)
.map(|_| {
let mut features = [0.0_f32; N_FEATURES];
for f in features.iter_mut() {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
*f = (state >> 33) as f32 / u32::MAX as f32;
}
features
})
.collect();
let run1: Vec<u32> = batch.iter().map(|f| agent.select(f).0).collect();
let run2: Vec<u32> = batch.iter().map(|f| agent.select(f).0).collect();
assert_eq!(
run1, run2,
"CPU LinUCB must produce identical actions on two independent runs"
);
}
#[test]
fn cpu_linucb_actions_within_valid_range() {
let agent = LinUCBAgent::new();
let mut state: u64 = 0x1234567890ABCDEF_u64;
let batch: Vec<[f32; N_FEATURES]> = (0..BATCH_SIZE)
.map(|_| {
let mut features = [0.0_f32; N_FEATURES];
for f in features.iter_mut() {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
*f = (state >> 33) as f32 / u32::MAX as f32;
}
features
})
.collect();
for (i, features) in batch.iter().enumerate() {
let (action, _) = agent.select(features);
assert!(
(action as usize) < N_ACTIONS,
"State {i}: action {action} out of range [0, {N_ACTIONS})"
);
}
}
#[test]
fn cpu_linucb_throughput_exceeds_250k_states_per_sec() {
let agent = LinUCBAgent::new();
let mut state: u64 = 0xFEEDFACEDEADBEEF_u64;
let batch: Vec<[f32; N_FEATURES]> = (0..BATCH_SIZE)
.map(|_| {
let mut features = [0.0_f32; N_FEATURES];
for f in features.iter_mut() {
state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
*f = (state >> 33) as f32 / u32::MAX as f32;
}
features
})
.collect();
for features in batch.iter() {
let _ = agent.select(features);
}
let t0 = Instant::now();
let runs = 10usize;
for _ in 0..runs {
for features in batch.iter() {
let _ = agent.select(features);
}
}
let elapsed_sec = t0.elapsed().as_secs_f64();
let total_states = (BATCH_SIZE * runs) as f64;
let throughput = total_states / elapsed_sec;
assert!(
throughput >= 250_000.0,
"CPU LinUCB throughput {throughput:.0} states/sec below 250K minimum"
);
}
#[test]
fn cpu_linucb_converges_to_rewarded_action() {
let mut agent = LinUCBAgent::new();
let features: [f32; N_FEATURES] = [0.1, 0.9, 0.2, 0.8, 0.3, 0.7, 0.4, 0.6];
for _ in 0..500 {
agent.update(&features, 3, 1.0);
}
let (best, _) = agent.select(&features);
assert_eq!(
best, 3,
"After 500 positive rewards for action 3, argmax should be 3, got {best}"
);
}