Files
foxhunt/crates/ml-supervised/src/tft/variable_selection.rs
jgrusewski 33864badf3 perf(cuda): TFT + Mamba2 forward passes fully GPU-native
TFT (10 downloads eliminated):
- lstm_encoder: gpu_select_dim1 per-timestep, gpu_cat_dim0 assembly
- variable_selection: gpu_narrow_2d column select, gpu_broadcast_mul_col
  weighted sum, gpu_mean_all per-column stats
- quantile_outputs: gpu_select_dim1 last timestep, gpu_stack_2d output,
  GPU quantile loss (sub→abs→scale→add→mean_all)
- mod.rs: GPU 3D broadcast for static context

Mamba2 (10 downloads eliminated):
- loss.rs: GPU MSE (sub→sqr→mean_all), GPU directional MSE with sign
  detection (mul→abs→div→scalar_sub→scale→mean_all)
- scan_algorithms: GPU sequential scan via gpu_select_dim1 per-step
- selective_state: GPU importance scoring (abs→mean_all)
- mod.rs: GPU state-space recurrence (select_dim1→matmul→add),
  GPU accuracy computation (sub→abs→relu→clamp→mean_all)

169/169 ml-supervised tests pass.

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

213 lines
7.7 KiB
Rust

//! Variable Selection Network for TFT
//!
//! Implements learnable feature selection using gated linear units and
//! soft feature selection weights for improved interpretability.
use std::collections::HashMap;
use std::sync::Arc;
use cudarc::driver::CudaStream;
use ml_core::MLError;
use super::GatedResidualNetwork;
use crate::gpu_tensor::{
gpu_add, gpu_cat_dim1, gpu_narrow_2d, gpu_softmax, GpuLinear, GpuTensor,
};
/// Variable Selection Network for feature importance learning
#[derive(Debug)]
pub struct VariableSelectionNetwork {
pub input_size: usize,
pub hidden_size: usize,
// Gated Linear Units for variable selection
single_var_grns: Vec<GatedResidualNetwork>,
// Soft attention weights
attention_weights: GpuLinear,
// Feature importance tracking
importance_scores: HashMap<usize, f64>,
stream: Arc<CudaStream>,
}
impl VariableSelectionNetwork {
pub fn new(input_size: usize, hidden_size: usize, stream: &Arc<CudaStream>) -> Result<Self, MLError> {
// Create GRN for flattened inputs (keeps param symmetry with original)
let _flattened_grn = GatedResidualNetwork::new(input_size, hidden_size, stream)?;
// Create individual GRNs for each variable
let mut single_var_grns = Vec::new();
for _i in 0..input_size {
let grn = GatedResidualNetwork::new(1, hidden_size, stream)?;
single_var_grns.push(grn);
}
// Attention layer for variable selection
let attention_weights = GpuLinear::new(hidden_size * input_size, input_size, stream)?;
Ok(Self {
input_size,
hidden_size,
single_var_grns,
attention_weights,
importance_scores: HashMap::new(),
stream: Arc::clone(stream),
})
}
pub fn forward(
&mut self,
inputs: &GpuTensor,
context: Option<&GpuTensor>,
) -> Result<GpuTensor, MLError> {
let batch_size = inputs.dim(0)?;
let is_2d = inputs.shape.len() == 2;
let seq_len = if is_2d { 1 } else { inputs.dim(1)? };
let input_feat = if is_2d { inputs.dim(1)? } else { inputs.dim(2)? };
// Reshape to 2D for processing: [batch * seq_len, input_feat]
let flat_input = if is_2d {
inputs.reshape(&[batch_size, input_feat])?
} else {
inputs.reshape(&[batch_size * seq_len, input_feat])?
};
let flat_rows = flat_input.dim(0)?;
// Process individual variables on GPU
let mut var_outputs: Vec<GpuTensor> = Vec::with_capacity(self.input_size);
for (i, grn) in self.single_var_grns.iter().enumerate() {
// Extract variable i from flat_input: column i -> [flat_rows, 1]
let var_tensor = gpu_narrow_2d(&flat_input, 1, i, 1)?;
let var_output = grn.forward(&var_tensor, context)?; // [flat_rows, hidden_size]
var_outputs.push(var_output);
}
// Concatenate all variable outputs along dim1: [flat_rows, hidden_size * input_size]
let mut cat_tensor = var_outputs.first().ok_or_else(|| {
MLError::InvalidInput("No variable outputs".to_owned())
})?.clone();
for var_out in var_outputs.iter().skip(1) {
cat_tensor = gpu_cat_dim1(&cat_tensor, var_out)?;
}
// Compute attention weights
let raw_weights = self.attention_weights.forward(&cat_tensor)?; // [flat_rows, input_size]
let attention = gpu_softmax(&raw_weights)?;
// Update importance scores (single scalar download per variable -- acceptable for monitoring)
self.update_importance_scores(&attention)?;
// Apply variable selection: weighted sum of per-variable GRN outputs
// attention: [flat_rows, input_size], var_outputs[i]: [flat_rows, hidden_size]
// result = sum_i(attention[:, i:i+1] * var_outputs[i])
let mut result = GpuTensor::zeros(&[flat_rows, self.hidden_size], &self.stream)?;
for (i, var_out) in var_outputs.iter().enumerate() {
// Extract attention weight for variable i: [flat_rows, 1]
let attn_col = gpu_narrow_2d(&attention, 1, i, 1)?;
// Broadcast multiply: [flat_rows, 1] * [flat_rows, hidden_size] -> [flat_rows, hidden_size]
// We need to broadcast attn_col across hidden_size columns.
// Use gpu_broadcast_mul_col: a[rows, cols] * b[rows, 1] -> [rows, cols]
let weighted = crate::gpu_tensor::gpu_broadcast_mul_col(var_out, &attn_col)?;
result = gpu_add(&result, &weighted)?;
}
// Reshape to [batch, seq_len, hidden_size]
result.reshape(&[batch_size, seq_len, self.hidden_size])
}
fn update_importance_scores(&mut self, attention_weights: &GpuTensor) -> Result<(), MLError> {
// Use gpu_mean_all on per-column slices to avoid bulk download
let cols = attention_weights.dim(1)?;
self.importance_scores.clear();
for c in 0..cols {
let col = gpu_narrow_2d(attention_weights, 1, c, 1)?;
let mean_val = crate::gpu_tensor::gpu_mean_all(&col)?;
self.importance_scores.insert(c, mean_val as f64);
}
Ok(())
}
pub fn get_importance_scores(&self) -> Result<Vec<f64>, MLError> {
let mut scores = vec![0.0; self.input_size];
for (i, &score) in &self.importance_scores {
if *i < self.input_size {
scores[*i] = score;
}
}
let sum: f64 = scores.iter().sum();
if sum == 0.0 {
let uniform_score = 1.0 / self.input_size as f64;
scores.fill(uniform_score);
}
Ok(scores)
}
pub fn get_top_features(&self, k: usize) -> Vec<(usize, f64)> {
let mut features: Vec<(usize, f64)> = self
.importance_scores
.iter()
.map(|(&idx, &score)| (idx, score))
.collect();
features.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
features.truncate(k);
features
}
}
#[cfg(test)]
mod tests {
use super::*;
use cudarc::driver::CudaContext;
fn test_stream() -> Arc<CudaStream> {
let ctx = CudaContext::new(0).expect("CUDA context required");
ctx.new_stream().expect("Failed to create CUDA stream")
}
#[test]
fn test_variable_selection_network_creation() -> Result<(), MLError> {
let stream = test_stream();
let vsn = VariableSelectionNetwork::new(10, 64, &stream)?;
assert_eq!(vsn.input_size, 10);
assert_eq!(vsn.hidden_size, 64);
Ok(())
}
#[test]
fn test_variable_selection_forward_2d() -> Result<(), MLError> {
let stream = test_stream();
let mut vsn = VariableSelectionNetwork::new(5, 32, &stream)?;
let input = GpuTensor::from_vec(
vec![1.0, 2.0, 3.0, 4.0, 5.0, 2.0, 3.0, 4.0, 5.0, 6.0],
&[2, 5],
&stream,
)?;
let output = vsn.forward(&input, None)?;
assert_eq!(output.shape, vec![2, 1, 32]);
Ok(())
}
#[test]
fn test_variable_selection_forward_3d() -> Result<(), MLError> {
let stream = test_stream();
let mut vsn = VariableSelectionNetwork::new(3, 16, &stream)?;
let input = GpuTensor::from_vec(vec![1.0_f32; 24], &[2, 4, 3], &stream)?;
let output = vsn.forward(&input, None)?;
assert_eq!(output.shape, vec![2, 4, 16]);
Ok(())
}
#[test]
fn test_importance_scores() -> Result<(), MLError> {
let stream = test_stream();
let vsn = VariableSelectionNetwork::new(5, 32, &stream)?;
let scores = vsn.get_importance_scores()?;
assert_eq!(scores.len(), 5);
let sum: f64 = scores.iter().sum();
assert!((sum - 1.0).abs() < 1e-6);
Ok(())
}
}