Files
foxhunt/crates/ml-supervised/src/kan/network.rs
jgrusewski daf771c38d audit: annotate all remaining to_vec/memcpy_dtoh — categorized 158 sites
Every to_vec()/memcpy_dtoh across 48 files audited and annotated:
- ~100 false positives: Rust slice .to_vec() (cpu-side, never touches GPU)
- ~25 gpu-exit: legitimate scalar readbacks (loss, grad_norm, epoch state)
- ~20 test-only readbacks: gated by #[cfg(test)] scope
- ~10 cpu-side uploads: .to_vec() before from_vec() GPU upload
- ~3 checkpoint exports: export_to_host at epoch boundary

Annotations use inline comments: // cpu-side, // gpu-exit:, // test-only

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

125 lines
3.7 KiB
Rust

//! KAN network: a stack of KAN layers.
//!
//! Chains multiple KAN layers according to `layer_widths` from the config.
//! No explicit activation between layers because each KAN layer contains
//! its own learned activation functions (B-splines).
use std::sync::Arc;
use cudarc::driver::CudaStream;
use ml_core::MLError;
use crate::gpu_tensor::GpuTensor;
use super::config::KANConfig;
use super::layer::KANLayer;
/// A multi-layer Kolmogorov-Arnold Network.
#[derive(Debug)]
pub struct KANNetwork {
layers: Vec<KANLayer>,
}
impl KANNetwork {
/// Build a KAN network from config.
///
/// Creates one KAN layer per consecutive pair of widths in `config.layer_widths`.
pub fn new(config: &KANConfig, stream: &Arc<CudaStream>) -> Result<Self, MLError> {
if config.layer_widths.len() < 2 {
return Err(MLError::ConfigError(
"layer_widths must have at least 2 entries".to_owned(),
));
}
if config.layer_widths.contains(&0) {
return Err(MLError::ConfigError(
"KAN requires all layer_widths > 0".to_owned(),
));
}
let mut layers = Vec::with_capacity(config.layer_widths.len() - 1);
for i in 0..config.layer_widths.len() - 1 {
let in_dim = *config.layer_widths.get(i).ok_or_else(|| {
MLError::ConfigError(format!("Missing layer_widths index {}", i))
})?;
let out_dim = *config.layer_widths.get(i + 1).ok_or_else(|| {
MLError::ConfigError(format!("Missing layer_widths index {}", i + 1))
})?;
let layer = KANLayer::new(
in_dim,
out_dim,
config.grid_size,
config.spline_order,
stream,
)?;
layers.push(layer);
}
Ok(Self { layers })
}
/// Forward pass: chains all layers sequentially.
///
/// Input shape: `(batch, layer_widths[0])`
/// Output shape: `(batch, layer_widths[last])`
pub fn forward(&self, input: &GpuTensor) -> Result<GpuTensor, MLError> {
let mut x = input.clone();
for layer in &self.layers {
x = layer.forward(&x)?;
}
Ok(x)
}
/// Access the layers for parameter extraction (checkpoint/weight access).
pub fn layers(&self) -> &[KANLayer] {
&self.layers
}
}
#[cfg(test)]
mod tests {
use super::*;
use cudarc::driver::CudaContext;
fn test_stream() -> Arc<CudaStream> {
let ctx = CudaContext::new(0).expect("CUDA required");
ctx.new_stream().expect("Failed to create stream")
}
fn make_config() -> KANConfig {
KANConfig {
grid_size: 3,
spline_order: 3,
layer_widths: vec![8, 4, 1],
learning_rate: 1e-3,
weight_decay: 1e-4,
grad_clip: 1.0,
}
}
#[test]
fn test_kan_network_forward() {
let stream = test_stream();
let config = make_config();
let net = KANNetwork::new(&config, &stream).unwrap();
let input = GpuTensor::randn(&[2, 8], 0.5, &stream).unwrap();
let output = net.forward(&input).unwrap();
assert_eq!(output.shape, vec![2, 1]);
}
#[test]
fn test_kan_network_finite_outputs() {
let stream = test_stream();
let config = make_config();
let net = KANNetwork::new(&config, &stream).unwrap();
let input = GpuTensor::randn(&[4, 8], 0.5, &stream).unwrap();
let output = net.forward(&input).unwrap();
let v = output.to_vec().unwrap(); // test-only readback
for val in &v {
assert!(val.is_finite(), "Non-finite output: {}", val);
}
}
}