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]>
132 lines
3.0 KiB
Rust
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());
|
|
}
|
|
}
|