Files
rustytorch/crates/core/rtx-backend-cpu/src/device.rs
T
Claude CodeandClaude Opus 4.8 97ef5efe0b feat(rtx-backend-cpu): generic ops over element type + coexisting CpuBackendF64
Phase 1 of the rustytorch f32→f64 plan, backend layer. All 11 ops modules
(basic/creation/unary/gemm/reduction/activation/shape/conv/pooling/normalization/
attention) are now generic over the element via a `CpuFloat` bound
(`num_traits::Float + Send + Sync + 'static`); f32/Vec<f32> → E/Vec<E>, literals →
E::zero()/one()/from(..). The ops were already pure scalar + rayon (no SIMD), so
the f32 path is byte-identical (E inferred as f32 under CpuBackend) — no SIMD/BLAS
dual-path needed.

Adds `CpuBackendF64` (FloatElem = f64, TensorPrimitive = CpuTensorPrimitive<D,f64>)
delegating to the same generic ops, plus the DeviceOps<CpuBackendF64> impl.
CpuBackend (f32) untouched.

Validated: 35 tests pass (33 original f32 + 2 new f64); `cpu_backend_f64_exceeds_
f32_precision` preserves 1+2^-30 (f32 rounds to 1.0) — proves genuine f64. rtx-tensor
(dependent) still builds. clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-26 22:08:18 -07:00

132 lines
3.0 KiB
Rust

//! CPU device abstraction.
use rtx_backend::{DeviceId, DeviceOps};
use crate::{CpuBackend, CpuBackendF64};
/// CPU device for the CPU backend.
///
/// Represents the system CPU. There is typically only one logical
/// CPU device, though it may have multiple cores.
#[derive(Clone)]
pub struct CpuDevice {
/// Device index (always 0 for CPU)
index: usize,
/// Number of available threads
num_threads: usize,
}
impl CpuDevice {
/// Create a new CPU device.
pub fn new() -> Self {
let num_threads = rayon::current_num_threads();
Self {
index: 0,
num_threads,
}
}
/// Get the number of available threads.
pub fn num_threads(&self) -> usize {
self.num_threads
}
}
impl Default for CpuDevice {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for CpuDevice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CpuDevice")
.field("index", &self.index)
.field("num_threads", &self.num_threads)
.finish()
}
}
impl PartialEq for CpuDevice {
fn eq(&self, other: &Self) -> bool {
self.index == other.index
}
}
impl Eq for CpuDevice {}
impl std::hash::Hash for CpuDevice {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.index.hash(state);
}
}
impl DeviceOps<CpuBackend> for CpuDevice {
fn id(&self) -> DeviceId {
DeviceId::Cpu
}
fn memory_capacity(&self) -> usize {
// Return system memory (rough estimate)
// In production, would use sys_info crate
16 * 1024 * 1024 * 1024 // 16GB default
}
fn memory_available(&self) -> usize {
// Estimate available memory
(16 * 1024 * 1024 * 1024_usize) / 2
}
fn compute_capability(&self) -> Option<(u32, u32)> {
// CPU doesn't have compute capability
None
}
fn synchronize(&self) {
// CPU operations are synchronous
}
fn is_available(&self) -> bool {
true // CPU is always available
}
}
/// Same device, viewed through the f64 backend (CPU has one logical device).
impl DeviceOps<CpuBackendF64> for CpuDevice {
fn id(&self) -> DeviceId {
DeviceId::Cpu
}
fn memory_capacity(&self) -> usize {
16 * 1024 * 1024 * 1024 // 16GB default
}
fn memory_available(&self) -> usize {
(16 * 1024 * 1024 * 1024_usize) / 2
}
fn compute_capability(&self) -> Option<(u32, u32)> {
None
}
fn synchronize(&self) {}
fn is_available(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_device_creation() {
let device = CpuDevice::new();
// `is_available` is shared by both DeviceOps impls (f32/f64); pick one.
assert!(DeviceOps::<CpuBackend>::is_available(&device));
assert!(device.num_threads() > 0);
println!("CPU threads: {}", device.num_threads());
}
}