use std::fs;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use wasm4pm::ml::LinUCBAgent;
const BATCH_SIZE: usize = 2048;
const N_FEATURES: usize = 8;
const N_ACTIONS: usize = 40;
struct Targets {
latency_ms_max: f64,
throughput_min_states_per_sec: f64,
memory_vram_max_mb: f64,
}
impl Targets {
fn phase4() -> Self {
Self {
latency_ms_max: 0.1,
throughput_min_states_per_sec: 250_000.0,
memory_vram_max_mb: 32.0,
}
}
}
struct WgslAnalysis {
parse_ok: bool,
validate_ok: bool,
error_message: Option<String>,
estimated_vram_bytes: usize,
entry_points: usize,
workgroup_size: u32,
binding_count: usize,
}
fn analyse_wgsl(source: &str) -> WgslAnalysis {
use naga::front::wgsl;
use naga::valid::{Capabilities, ValidationFlags, Validator};
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 parse_result = wgsl::parse_str(source);
let module = match parse_result {
Ok(m) => m,
Err(e) => {
return WgslAnalysis {
parse_ok: false,
validate_ok: false,
error_message: Some(format!("WGSL parse error: {}", e)),
estimated_vram_bytes: vram_bytes,
entry_points: 0,
workgroup_size: 0,
binding_count: 0,
};
}
};
let entry_count = module.entry_points.len();
let wg_size = module
.entry_points
.iter()
.find(|ep| ep.stage == naga::ShaderStage::Compute)
.map(|ep| ep.workgroup_size[0])
.unwrap_or(0);
let binding_count = module
.global_variables
.iter()
.filter(|(_, v)| v.binding.is_some())
.count();
let mut validator = Validator::new(ValidationFlags::all(), Capabilities::empty());
let validate_result = validator.validate(&module);
match validate_result {
Ok(_) => WgslAnalysis {
parse_ok: true,
validate_ok: true,
error_message: None,
estimated_vram_bytes: vram_bytes,
entry_points: entry_count,
workgroup_size: wg_size,
binding_count,
},
Err(e) => WgslAnalysis {
parse_ok: true,
validate_ok: false,
error_message: Some(format!("WGSL validation error: {}", e)),
estimated_vram_bytes: vram_bytes,
entry_points: entry_count,
workgroup_size: wg_size,
binding_count,
},
}
}
struct CpuBenchmark {
latency_ms: f64,
throughput_states_per_sec: f64,
actions: Vec<u32>,
deterministic: bool,
}
fn benchmark_cpu_linucb(
agent: &LinUCBAgent,
batch: &[[f32; N_FEATURES]],
warmup_runs: usize,
timed_runs: usize,
) -> CpuBenchmark {
for _ in 0..warmup_runs {
for features in batch.iter() {
let _ = agent.select(features);
}
}
let t0 = Instant::now();
let mut last_actions = vec![0u32; BATCH_SIZE];
for run in 0..timed_runs {
for (i, features) in batch.iter().enumerate() {
let (action, _) = agent.select(features);
if run == timed_runs - 1 {
last_actions[i] = action;
}
}
}
let elapsed = t0.elapsed();
let total_ms = elapsed.as_secs_f64() * 1000.0;
let per_batch_ms = total_ms / timed_runs as f64;
let mut check_actions = vec![0u32; BATCH_SIZE];
for (i, features) in batch.iter().enumerate() {
let (action, _) = agent.select(features);
check_actions[i] = action;
}
let deterministic = last_actions == check_actions;
let throughput = (BATCH_SIZE as f64) / (per_batch_ms / 1000.0);
CpuBenchmark {
latency_ms: per_batch_ms,
throughput_states_per_sec: throughput,
actions: last_actions,
deterministic,
}
}
fn calculate_memory_mb(vram_bytes: usize) -> f64 {
let total = vram_bytes as f64 * 1.10;
total / (1024.0 * 1024.0)
}
fn round4(v: f64) -> f64 {
(v * 10000.0).round() / 10000.0
}
fn generate_report(
wgsl: &WgslAnalysis,
cpu: &CpuBenchmark,
targets: &Targets,
gpu_device: &str,
gpu_available: bool,
timestamp: u64,
) -> serde_json::Value {
let memory_mb = calculate_memory_mb(wgsl.estimated_vram_bytes);
let latency_gate = if gpu_available {
if cpu.latency_ms <= targets.latency_ms_max {
"PASS"
} else {
"FAIL"
}
} else {
"N/A-CPU-FALLBACK"
};
let throughput_pass = cpu.throughput_states_per_sec >= targets.throughput_min_states_per_sec;
let memory_pass = memory_mb <= targets.memory_vram_max_mb;
let parity_status = if cpu.deterministic { "PASS" } else { "FAIL" };
let blocking_fail = !wgsl.parse_ok
|| !wgsl.validate_ok
|| !throughput_pass
|| !memory_pass
|| !cpu.deterministic
|| (gpu_available && latency_gate == "FAIL");
let overall = if blocking_fail { "FAIL" } else { "PASS" };
serde_json::json!({
"schema_version": "1.0.0",
"timestamp": timestamp,
"gpu_device": gpu_device,
"kernel_name": "linucb_kernel.wgsl",
"batch_size": BATCH_SIZE,
"wgsl_validation": {
"parse_ok": wgsl.parse_ok,
"validate_ok": wgsl.validate_ok,
"error": wgsl.error_message,
"entry_points": wgsl.entry_points,
"workgroup_size": wgsl.workgroup_size,
"binding_count": wgsl.binding_count,
"estimated_vram_bytes": wgsl.estimated_vram_bytes
},
"measurements": {
"latency_ms": round4(cpu.latency_ms),
"throughput_states_per_sec": cpu.throughput_states_per_sec.round() as u64,
"memory_vram_mb": round4(memory_mb),
"pcie_roundtrip_us": serde_json::Value::Null,
"gpu_utilization_percent": serde_json::Value::Null,
"power_w": serde_json::Value::Null,
"note": if gpu_available {
"GPU path active"
} else {
"CPU reference path (RTX 4090 not available on this host; latency gate is N/A)"
}
},
"targets": {
"latency_ms_max": targets.latency_ms_max,
"throughput_min": targets.throughput_min_states_per_sec,
"memory_vram_max_mb": targets.memory_vram_max_mb,
"parity_check": parity_status
},
"gate_results": {
"wgsl_parse": if wgsl.parse_ok { "PASS" } else { "FAIL" },
"wgsl_validate": if wgsl.validate_ok { "PASS" } else { "FAIL" },
"latency": latency_gate,
"throughput": if throughput_pass { "PASS" } else { "FAIL" },
"memory": if memory_pass { "PASS" } else { "FAIL" },
"determinism": parity_status
},
"status": overall
})
}
fn main() {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
println!("==========================================================");
println!("Agent 14: GPU WGSL Kernel Validation & Benchmark");
println!("==========================================================\n");
let wgsl_path = concat!(env!("CARGO_MANIFEST_DIR"), "/src/gpu/linucb_kernel.wgsl");
let wgsl_source = fs::read_to_string(wgsl_path)
.unwrap_or_else(|e| panic!("Cannot read WGSL kernel at {wgsl_path}: {e}"));
println!(
"[1/5] WGSL loaded: {} bytes from {}",
wgsl_source.len(),
wgsl_path
);
println!("[2/5] WGSL static analysis via naga...");
let wgsl = analyse_wgsl(&wgsl_source);
println!(
" Parse: {}",
if wgsl.parse_ok { "PASS" } else { "FAIL" }
);
println!(
" Validate: {}",
if wgsl.validate_ok { "PASS" } else { "FAIL" }
);
if let Some(ref err) = wgsl.error_message {
println!(" Error: {err}");
}
println!(" Entry points: {}", wgsl.entry_points);
println!(" Workgroup [0]: {}", wgsl.workgroup_size);
println!(" Bound buffers: {}", wgsl.binding_count);
println!(
" Est. VRAM: {:.2} KB ({} bytes)",
wgsl.estimated_vram_bytes as f64 / 1024.0,
wgsl.estimated_vram_bytes
);
println!("[3/5] Building batch of {BATCH_SIZE} feature vectors...");
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();
println!(
" Feature[0][0..3]: {:.4} {:.4} {:.4}",
batch[0][0], batch[0][1], batch[0][2]
);
println!("[4/5] Benchmarking CPU LinUCB (warmup=10, timed=100 passes)...");
let agent = LinUCBAgent::new();
let cpu = benchmark_cpu_linucb(&agent, &batch, 10, 100);
println!(
" Latency: {:.4} ms / batch-of-2048",
cpu.latency_ms
);
println!(
" Throughput: {:.0} states/sec",
cpu.throughput_states_per_sec
);
println!(
" Determinism: {}",
if cpu.deterministic { "PASS" } else { "FAIL" }
);
println!(" actions[0..5]: {:?}", &cpu.actions[..5]);
println!("[5/5] Generating JSON report...");
let targets = Targets::phase4();
let gpu_available = false;
let gpu_device =
"Apple M3 Max (Metal 4) — CPU reference path; RTX 4090 not available on this host";
let report = generate_report(&wgsl, &cpu, &targets, gpu_device, gpu_available, timestamp);
let json = serde_json::to_string_pretty(&report).expect("JSON serialization failed");
let output_dir = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../.wasm4pm/benchmarks/benchmarks"
);
fs::create_dir_all(output_dir).expect("Cannot create output directory");
let filename = format!("{output_dir}/gpu_wgsl_{timestamp}.json");
fs::write(&filename, &json).expect("Cannot write report");
println!("\n{json}\n");
println!("Report saved: {filename}\n");
let overall = report["status"].as_str().unwrap_or("UNKNOWN");
println!("==========================================================");
println!("FINAL STATUS: {overall}");
println!("==========================================================");
for (gate, key) in &[
("WGSL parse", "wgsl_parse"),
("WGSL validate", "wgsl_validate"),
("Latency", "latency"),
("Throughput", "throughput"),
("Memory", "memory"),
("Determinism", "determinism"),
] {
println!(
" {:<16} {}",
format!("{gate}:"),
report["gate_results"][key].as_str().unwrap_or("?")
);
}
println!();
if overall != "PASS" {
eprintln!("ERROR: One or more gates FAILED.");
std::process::exit(1);
}
}