Files
foxhunt/crates/ml-core/src/tensor_ops.rs
jgrusewski 22004a7368 refactor(cuda): eliminate candle from ml-core, ml-ppo, and 4 thin crates
Hard refactor — no shims, no compat layers. Candle removed from Cargo.toml
and all source files in 6 crates:

- ml-core: MlDevice enum, checkpoint.rs (safetensors direct), cudarc imports
  fixed from candle re-export to direct, AdamWConfig lr_decay, cuda_compat
  gutted. Net -7,341 lines.
- ml-ppo: All 16 files rewritten. LSTM→CudaLSTM, VarMap→GpuVarStore,
  PPOAgent 2306→700 lines, checkpoint→binary format.
- ml-ensemble: GPU-resident sigmoid via custom CUDA kernel.
- ml-explainability: Integrated gradients via GPU finite-difference kernels.
- ml-labeling: Device→MlDevice.
- ml-hyperopt: Cargo.toml only.

Remaining: ml-dqn (24 files), ml-supervised (4 files), ml crate (104 files).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 22:27:56 +01:00

60 lines
1.9 KiB
Rust

//! Tensor operations and utilities for ML models (Candle-free).
//!
//! With the Candle elimination, tensor operations now live in
//! `cuda_autograd::GpuTensor` and `cuda_autograd::ActivationKernels`.
//!
//! This module retains only lightweight CPU-side helper functions that
//! downstream crates use and that do not require a GPU tensor backend.
/// Tensor operation utilities (CPU-side helpers).
#[derive(Debug)]
pub struct TensorOps;
impl TensorOps {
/// Apply softmax to a slice with numerical stability.
///
/// Returns a new vector of the same length where values sum to 1.
pub fn stable_softmax_cpu(input: &[f64]) -> Vec<f64> {
if input.is_empty() {
return Vec::new();
}
let max_val = input.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let exp_vals: Vec<f64> = input.iter().map(|&x| (x - max_val).exp()).collect();
let sum_exp: f64 = exp_vals.iter().sum();
if sum_exp == 0.0 {
return vec![1.0 / input.len() as f64; input.len()];
}
exp_vals.iter().map(|&x| x / sum_exp).collect()
}
/// Clamp values in a slice between min and max.
pub fn clamp_cpu(input: &[f64], min_val: f64, max_val: f64) -> Vec<f64> {
input.iter().map(|&x| x.clamp(min_val, max_val)).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stable_softmax_cpu() {
let data = vec![1.0, 2.0, 3.0];
let result = TensorOps::stable_softmax_cpu(&data);
let sum: f64 = result.iter().sum();
assert!((sum - 1.0).abs() < 1e-10);
// Probabilities should be monotonically increasing
assert!(result[0] < result[1]);
assert!(result[1] < result[2]);
}
#[test]
fn test_clamp_cpu() {
let data = vec![-2.0, -1.0, 0.0, 1.0, 2.0];
let result = TensorOps::clamp_cpu(&data, -1.0, 1.0);
for val in &result {
assert!(*val >= -1.0 && *val <= 1.0);
}
}
}