//! 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, ) -> Result { 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 { 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 { 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); } } }