feat(rtx-tensor): genericize GenericTensor over B::FloatElem (f64-capable)
Phase 2 of the rustytorch f32→f64 plan. All 6 `impl<B: Backend<FloatElem = f32>>` blocks on GenericTensor relaxed to `impl<B: Backend>`, with concrete f32 → B::FloatElem (full/from_slice/to_vec/add_scalar/mul_scalar/pow/clamp/leaky_relu/ elu/layer_norm/rms_norm). The genericization was fully clean — every Backend trait scalar param was already Self::FloatElem, so no methods had to stay f32-gated. GenericTensor now works with CpuBackendF64 as well as CpuBackend. Backward-compat holds via B::FloatElem = f32 for CpuBackend: to_vec() still returns Vec<f32>, from_slice still takes &[f32]. Validated: 706 rtx-tensor tests pass (704 f32 + 2 new f64); the f64 test proves 1+2^-30 survives through from_slice/matmul/to_vec (f32 rounds to 1.0). rtx-nn builds; **QPUDIDP's qpu-didp-surrogate (external, ~125 f32 sites) still compiles**. clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2360d7bffc
commit
a25f24494c
@@ -124,11 +124,11 @@ impl<B: Backend, const D: usize> GenericTensor<B, D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== f32-Specific Creation Operations ====================
|
// ==================== Scalar / Slice Creation Operations ====================
|
||||||
|
|
||||||
impl<B: Backend<FloatElem = f32>, const D: usize> GenericTensor<B, D> {
|
impl<B: Backend, const D: usize> GenericTensor<B, D> {
|
||||||
/// Create a tensor filled with a scalar value.
|
/// Create a tensor filled with a scalar value.
|
||||||
pub fn full(shape: [usize; D], fill_value: f32, device: &B::Device) -> Self {
|
pub fn full(shape: [usize; D], fill_value: B::FloatElem, device: &B::Device) -> Self {
|
||||||
Self {
|
Self {
|
||||||
primitive: B::full(shape, fill_value, device),
|
primitive: B::full(shape, fill_value, device),
|
||||||
device: device.clone(),
|
device: device.clone(),
|
||||||
@@ -137,7 +137,7 @@ impl<B: Backend<FloatElem = f32>, const D: usize> GenericTensor<B, D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a tensor from a slice of data.
|
/// Create a tensor from a slice of data.
|
||||||
pub fn from_slice(data: &[f32], shape: [usize; D], device: &B::Device) -> Self {
|
pub fn from_slice(data: &[B::FloatElem], shape: [usize; D], device: &B::Device) -> Self {
|
||||||
Self {
|
Self {
|
||||||
primitive: B::from_data(data, shape, device),
|
primitive: B::from_data(data, shape, device),
|
||||||
device: device.clone(),
|
device: device.clone(),
|
||||||
@@ -148,11 +148,12 @@ impl<B: Backend<FloatElem = f32>, const D: usize> GenericTensor<B, D> {
|
|||||||
|
|
||||||
// ==================== Data Access ====================
|
// ==================== Data Access ====================
|
||||||
|
|
||||||
impl<B: Backend<FloatElem = f32>, const D: usize> GenericTensor<B, D> {
|
impl<B: Backend, const D: usize> GenericTensor<B, D> {
|
||||||
/// Convert tensor data to a Vec<f32>.
|
/// Convert tensor data to a `Vec<B::FloatElem>` (`Vec<f32>` for `CpuBackend`,
|
||||||
|
/// `Vec<f64>` for `CpuBackendF64`).
|
||||||
///
|
///
|
||||||
/// This may involve copying data from GPU to CPU memory.
|
/// This may involve copying data from GPU to CPU memory.
|
||||||
pub fn to_vec(&self) -> Vec<f32> {
|
pub fn to_vec(&self) -> Vec<B::FloatElem> {
|
||||||
B::to_data(&self.primitive)
|
B::to_data(&self.primitive)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,17 +207,17 @@ impl<B: Backend, const D: usize> GenericTensor<B, D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== f32-Specific Arithmetic Operations ====================
|
// ==================== Scalar Arithmetic Operations ====================
|
||||||
|
|
||||||
impl<B: Backend<FloatElem = f32>, const D: usize> GenericTensor<B, D> {
|
impl<B: Backend, const D: usize> GenericTensor<B, D> {
|
||||||
/// Add a scalar to all elements.
|
/// Add a scalar to all elements.
|
||||||
pub fn add_scalar(&self, scalar: f32) -> Self {
|
pub fn add_scalar(&self, scalar: B::FloatElem) -> Self {
|
||||||
let scalar_tensor = Self::full(self.shape(), scalar, &self.device);
|
let scalar_tensor = Self::full(self.shape(), scalar, &self.device);
|
||||||
self.add(&scalar_tensor)
|
self.add(&scalar_tensor)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Multiply all elements by a scalar.
|
/// Multiply all elements by a scalar.
|
||||||
pub fn mul_scalar(&self, scalar: f32) -> Self {
|
pub fn mul_scalar(&self, scalar: B::FloatElem) -> Self {
|
||||||
let scalar_tensor = Self::full(self.shape(), scalar, &self.device);
|
let scalar_tensor = Self::full(self.shape(), scalar, &self.device);
|
||||||
self.mul(&scalar_tensor)
|
self.mul(&scalar_tensor)
|
||||||
}
|
}
|
||||||
@@ -280,11 +281,11 @@ impl<B: Backend, const D: usize> GenericTensor<B, D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== f32-Specific Unary Math Operations ====================
|
// ==================== Parameterized Unary Math Operations ====================
|
||||||
|
|
||||||
impl<B: Backend<FloatElem = f32>, const D: usize> GenericTensor<B, D> {
|
impl<B: Backend, const D: usize> GenericTensor<B, D> {
|
||||||
/// Element-wise power.
|
/// Element-wise power.
|
||||||
pub fn pow(&self, exp: f32) -> Self {
|
pub fn pow(&self, exp: B::FloatElem) -> Self {
|
||||||
Self {
|
Self {
|
||||||
primitive: B::pow(self.primitive.clone(), exp),
|
primitive: B::pow(self.primitive.clone(), exp),
|
||||||
device: self.device.clone(),
|
device: self.device.clone(),
|
||||||
@@ -293,7 +294,7 @@ impl<B: Backend<FloatElem = f32>, const D: usize> GenericTensor<B, D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Clamp values to a range.
|
/// Clamp values to a range.
|
||||||
pub fn clamp(&self, min: f32, max: f32) -> Self {
|
pub fn clamp(&self, min: B::FloatElem, max: B::FloatElem) -> Self {
|
||||||
Self {
|
Self {
|
||||||
primitive: B::clamp(self.primitive.clone(), min, max),
|
primitive: B::clamp(self.primitive.clone(), min, max),
|
||||||
device: self.device.clone(),
|
device: self.device.clone(),
|
||||||
@@ -351,11 +352,11 @@ impl<B: Backend, const D: usize> GenericTensor<B, D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== f32-Specific Activation Functions ====================
|
// ==================== Parameterized Activation Functions ====================
|
||||||
|
|
||||||
impl<B: Backend<FloatElem = f32>, const D: usize> GenericTensor<B, D> {
|
impl<B: Backend, const D: usize> GenericTensor<B, D> {
|
||||||
/// Leaky ReLU activation: max(negative_slope * x, x).
|
/// Leaky ReLU activation: max(negative_slope * x, x).
|
||||||
pub fn leaky_relu(&self, negative_slope: f32) -> Self {
|
pub fn leaky_relu(&self, negative_slope: B::FloatElem) -> Self {
|
||||||
Self {
|
Self {
|
||||||
primitive: B::leaky_relu(self.primitive.clone(), negative_slope),
|
primitive: B::leaky_relu(self.primitive.clone(), negative_slope),
|
||||||
device: self.device.clone(),
|
device: self.device.clone(),
|
||||||
@@ -364,7 +365,7 @@ impl<B: Backend<FloatElem = f32>, const D: usize> GenericTensor<B, D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// ELU activation: x if x > 0, else alpha * (exp(x) - 1).
|
/// ELU activation: x if x > 0, else alpha * (exp(x) - 1).
|
||||||
pub fn elu(&self, alpha: f32) -> Self {
|
pub fn elu(&self, alpha: B::FloatElem) -> Self {
|
||||||
Self {
|
Self {
|
||||||
primitive: B::elu(self.primitive.clone(), alpha),
|
primitive: B::elu(self.primitive.clone(), alpha),
|
||||||
device: self.device.clone(),
|
device: self.device.clone(),
|
||||||
@@ -498,15 +499,15 @@ impl<B: Backend, const D: usize> GenericTensor<B, D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== f32-Specific LLM Operations ====================
|
// ==================== Normalization (parameterized) Operations ====================
|
||||||
|
|
||||||
impl<B: Backend<FloatElem = f32>, const D: usize> GenericTensor<B, D> {
|
impl<B: Backend, const D: usize> GenericTensor<B, D> {
|
||||||
/// Layer normalization.
|
/// Layer normalization.
|
||||||
pub fn layer_norm(
|
pub fn layer_norm(
|
||||||
&self,
|
&self,
|
||||||
weight: &GenericTensor<B, 1>,
|
weight: &GenericTensor<B, 1>,
|
||||||
bias: Option<&GenericTensor<B, 1>>,
|
bias: Option<&GenericTensor<B, 1>>,
|
||||||
eps: f32,
|
eps: B::FloatElem,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
primitive: B::layer_norm(
|
primitive: B::layer_norm(
|
||||||
@@ -521,7 +522,7 @@ impl<B: Backend<FloatElem = f32>, const D: usize> GenericTensor<B, D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// RMS normalization.
|
/// RMS normalization.
|
||||||
pub fn rms_norm(&self, weight: &GenericTensor<B, 1>, eps: f32) -> Self {
|
pub fn rms_norm(&self, weight: &GenericTensor<B, 1>, eps: B::FloatElem) -> Self {
|
||||||
Self {
|
Self {
|
||||||
primitive: B::rms_norm(self.primitive.clone(), &weight.primitive, eps),
|
primitive: B::rms_norm(self.primitive.clone(), &weight.primitive, eps),
|
||||||
device: self.device.clone(),
|
device: self.device.clone(),
|
||||||
@@ -678,3 +679,40 @@ impl<B: Backend, const D: usize> Debug for GenericTensor<B, D> {
|
|||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod f64_tests {
|
||||||
|
use super::*;
|
||||||
|
use rtx_backend_cpu::{CpuBackend, CpuBackendF64, CpuDevice};
|
||||||
|
|
||||||
|
/// `GenericTensor<CpuBackendF64, D>` runs in real f64: the API (from_slice,
|
||||||
|
/// to_vec, add_scalar, matmul) is now generic over `B::FloatElem`, and f64
|
||||||
|
/// preserves a value f32 would round away.
|
||||||
|
#[test]
|
||||||
|
fn generic_tensor_f64_exceeds_f32_precision() {
|
||||||
|
let dev = CpuDevice::new();
|
||||||
|
// 1 + 2^-30 is indistinguishable from 1.0 in f32, distinct in f64.
|
||||||
|
let v = 1.0_f64 + 2.0_f64.powi(-30);
|
||||||
|
let t = GenericTensor::<CpuBackendF64, 2>::from_slice(&[v, 2.0, 3.0, 4.0], [2, 2], &dev);
|
||||||
|
|
||||||
|
let out: Vec<f64> = t.to_vec();
|
||||||
|
assert_eq!(out[0], v, "f64 backend must preserve 1 + 2^-30");
|
||||||
|
assert_ne!(out[0], 1.0, "value must be distinct from 1.0 (would fail in f32)");
|
||||||
|
|
||||||
|
// add_scalar + matmul also run in f64.
|
||||||
|
let s = t.add_scalar(1.0_f64);
|
||||||
|
assert_eq!(s.to_vec()[0], v + 1.0);
|
||||||
|
let m = t.matmul(&t);
|
||||||
|
assert_eq!(m.to_vec().len(), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The f32 API surface is unchanged: `GenericTensor<CpuBackend, D>` still
|
||||||
|
/// takes `&[f32]` and returns `Vec<f32>` (backward compatibility).
|
||||||
|
#[test]
|
||||||
|
fn generic_tensor_f32_backward_compat() {
|
||||||
|
let dev = CpuDevice::new();
|
||||||
|
let t = GenericTensor::<CpuBackend, 2>::from_slice(&[1.0f32, 2.0, 3.0, 4.0], [2, 2], &dev);
|
||||||
|
let out: Vec<f32> = t.to_vec();
|
||||||
|
assert_eq!(out, vec![1.0f32, 2.0, 3.0, 4.0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user