Default-stream kernels run in order and every host read goes through a stream-ordered cudaMemcpy (to_device), so the per-op cudaDeviceSynchronize after each kernel was pure overhead — remove all 21 in tensor.rs. Host data is still correctly ordered by the D2H memcpy that reads it. Also zero op-output buffers with cudaMemset (device-side, async) instead of a blocking H2D memcpy of a host zero buffer on every allocation — that copy was itself a hidden per-op sync point. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
112 lines
3.7 KiB
Rust
112 lines
3.7 KiB
Rust
//! 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) => {
|
|
// Device-side memset (Phase T7): avoids a blocking H2D memcpy of a
|
|
// host zero buffer on every op-output allocation. cudaMemset is
|
|
// async on the default stream, so it doesn't serialize the stream.
|
|
let mut buf = GpuBuffer::alloc(len_bytes)?;
|
|
buf.memset(0)?;
|
|
Ok(Storage::cuda(buf, dev))
|
|
}
|
|
}
|
|
}
|
|
}
|