Files
foxhunt/crates/ml-supervised/src/kan/layer.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

127 lines
4.3 KiB
Rust

//! KAN layer implementation.
//!
//! A single KAN layer maps `(batch, in_dim)` to `(batch, out_dim)` by evaluating
//! learned B-spline activations on each input dimension and combining them with
//! learnable coefficients plus a linear residual connection.
//!
//! Uses cuBLAS sgemm via `GpuTensor` for the two matmul operations.
use std::sync::Arc;
use cudarc::driver::CudaStream;
use ml_core::MLError;
use crate::gpu_tensor::{gpu_add, gpu_matmul, GpuTensor};
use super::spline::BSplineBasis;
/// A single Kolmogorov-Arnold Network layer.
///
/// Forward pass:
/// 1. Expand each input dim through B-spline basis: `(batch, in_dim)` -> `(batch, in_dim * num_bases)`
/// 2. Multiply by coefficients: `(batch, in_dim * num_bases) @ coefficients(in_dim * num_bases, out_dim)` -> `(batch, out_dim)`
/// 3. Add residual: `input @ residual_weight(in_dim, out_dim)` -> `(batch, out_dim)`
#[derive(Debug)]
pub struct KANLayer {
/// B-spline basis evaluator (shared across all input dims)
spline_basis: BSplineBasis,
/// Learnable spline coefficients: (`in_dim` * `num_bases`, `out_dim`)
pub coefficients: GpuTensor,
/// Learnable residual weights: (`in_dim`, `out_dim`)
pub residual_weight: GpuTensor,
/// Input dimension
in_dim: usize,
/// Number of basis functions
num_bases: usize,
}
impl KANLayer {
/// Create a new KAN layer with Xavier-initialized weights.
pub fn new(
in_dim: usize,
out_dim: usize,
grid_size: usize,
spline_order: usize,
stream: &Arc<CudaStream>,
) -> Result<Self, MLError> {
let mut spline_basis = BSplineBasis::new(grid_size, spline_order, -2.0, 2.0)?;
let num_bases = spline_basis.num_bases();
// Xavier-like init
let scale = (2.0 / (in_dim * num_bases + out_dim) as f64).sqrt() as f32;
let coefficients =
GpuTensor::randn(&[in_dim * num_bases, out_dim], scale, stream)?;
let res_scale = (1.0 / in_dim as f64).sqrt() as f32;
let residual_weight = GpuTensor::randn(&[in_dim, out_dim], res_scale, stream)?;
// Pre-compute B-spline lookup table
spline_basis.precompute_gpu_grid(1024, stream)?;
Ok(Self {
spline_basis,
coefficients,
residual_weight,
in_dim,
num_bases,
})
}
/// Forward pass: `(batch, in_dim)` -> `(batch, out_dim)`.
pub fn forward(&self, input: &GpuTensor) -> Result<GpuTensor, MLError> {
let batch = input.dim(0)?;
// Flatten input to (batch * in_dim,) for basis evaluation
let flat_input = input.reshape(&[batch * self.in_dim])?;
// Evaluate B-spline basis: (batch * in_dim, num_bases)
let basis_vals = self.spline_basis.evaluate(&flat_input)?;
// Reshape to (batch, in_dim * num_bases)
let basis_flat = basis_vals.reshape(&[batch, self.in_dim * self.num_bases])?;
// Spline contribution: (batch, in_dim * num_bases) @ (in_dim * num_bases, out_dim)
let spline_out = gpu_matmul(&basis_flat, &self.coefficients)?;
// Residual contribution: (batch, in_dim) @ (in_dim, out_dim)
let residual_out = gpu_matmul(input, &self.residual_weight)?;
// Combine
gpu_add(&spline_out, &residual_out)
}
}
#[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")
}
#[test]
fn test_kan_layer_output_shape() {
let stream = test_stream();
let layer = KANLayer::new(4, 8, 5, 4, &stream).unwrap();
let input = GpuTensor::randn(&[2, 4], 0.5, &stream).unwrap();
let output = layer.forward(&input).unwrap();
assert_eq!(output.shape, vec![2, 8]);
}
#[test]
fn test_kan_layer_single_sample() {
let stream = test_stream();
let layer = KANLayer::new(4, 2, 3, 3, &stream).unwrap();
let input = GpuTensor::from_vec(vec![1.0, 0.5, -0.5, 0.0], &[1, 4], &stream).unwrap();
let output = layer.forward(&input).unwrap();
assert_eq!(output.shape, vec![1, 2]);
let v = output.to_vec().unwrap(); // test-only readback
for val in &v {
assert!(val.is_finite(), "Non-finite output: {}", val);
}
}
}