tensor: minimal Tensor crate over xtrain-cuda

New xtrain-tensor crate: DType (F32), shape/stride helpers, Arc-counted
host/device Storage with CPU↔CUDA copy, and a contiguous Tensor with
creation, host↔device transfer, and a scale() op driving the elementwise
kernel. GPU integration tests (host↔device roundtrip + scale correctness)
gated behind not(no_cuda); a thin build.rs emits the no_cuda cfg so the
kernel call sites compile out locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 15:13:06 +08:00
parent 63dc05fd10
commit fbd07a578c
10 changed files with 613 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
//! Tensor storage: host (CPU) bytes or a GPU buffer, reference-counted so
//! views (clones with different shape/strides) can share the backing data.
use std::sync::Arc;
use xtrain_cuda::{GpuBuffer, Result as CudaResult};
enum StorageInner {
Cpu { data: Vec<u8> },
Cuda { buffer: GpuBuffer, device: u32 },
}
/// Reference-counted tensor storage. Cloning is cheap (bumps the `Arc`).
#[derive(Clone)]
pub struct Storage(Arc<StorageInner>);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Device {
Cpu,
Cuda(u32),
}
impl std::fmt::Display for Device {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Device::Cpu => write!(f, "cpu"),
Device::Cuda(i) => write!(f, "cuda:{i}"),
}
}
}
impl Storage {
pub fn cpu(data: Vec<u8>) -> Self {
Self(Arc::new(StorageInner::Cpu { data }))
}
pub fn cuda(buffer: GpuBuffer, device: u32) -> Self {
Self(Arc::new(StorageInner::Cuda { buffer, device }))
}
pub fn device(&self) -> Device {
match self.0.as_ref() {
StorageInner::Cpu { .. } => Device::Cpu,
StorageInner::Cuda { device, .. } => Device::Cuda(*device),
}
}
pub fn len_bytes(&self) -> usize {
match self.0.as_ref() {
StorageInner::Cpu { data } => data.len(),
StorageInner::Cuda { buffer, .. } => buffer.len(),
}
}
/// Read-only view of CPU bytes. Panics if the storage lives on the GPU.
pub fn as_cpu_bytes(&self) -> &[u8] {
match self.0.as_ref() {
StorageInner::Cpu { data } => data,
StorageInner::Cuda { .. } => panic!("cannot read GPU storage as CPU bytes"),
}
}
/// Borrow the GPU buffer. Panics if the storage lives on the CPU.
pub fn gpu_buffer(&self) -> &GpuBuffer {
match self.0.as_ref() {
StorageInner::Cuda { buffer, .. } => buffer,
StorageInner::Cpu { .. } => panic!("cannot read CPU storage as GPU buffer"),
}
}
/// Copy to another device. Returns a clone of the `Arc` when already there.
/// T2 supports CPU↔CUDA(0); device-to-device copy across GPUs is out of scope.
pub fn to_device(&self, target: Device) -> CudaResult<Self> {
let current = self.device();
if current == target {
return Ok(self.clone());
}
match (current, target) {
(Device::Cpu, Device::Cuda(dev)) => {
let host = self.as_cpu_bytes();
let mut buf = GpuBuffer::alloc(host.len())?;
buf.copy_from_host(host)?;
Ok(Storage::cuda(buf, dev))
}
(Device::Cuda(_), Device::Cpu) => {
let src = self.gpu_buffer();
let mut host = vec![0u8; src.len()];
src.copy_to_host(&mut host)?;
Ok(Storage::cpu(host))
}
(Device::Cuda(_), Device::Cuda(_)) => {
panic!("cross-GPU storage transfer is not supported in T2")
}
_ => unreachable!(),
}
}
/// Zeroed storage on the given device.
pub fn zeros(len_bytes: usize, device: Device) -> CudaResult<Self> {
match device {
Device::Cpu => Ok(Storage::cpu(vec![0u8; len_bytes])),
Device::Cuda(dev) => {
// No device memset in T2: stage zeros from the host.
let mut buf = GpuBuffer::alloc(len_bytes)?;
buf.copy_from_host(&vec![0u8; len_bytes])?;
Ok(Storage::cuda(buf, dev))
}
}
}
}