P0 fixes (blocking usability): - FIX-01: thread-local cuBLAS handle (was creating/destroying per matmul) - FIX-16: EOS token no longer leaks into API responses - FIX-17: max_seq_len configurable via --max-seq-len (default 2048, was hardcoded 256) - FIX-18: max_tokens clamped to available seq space, prompt overflow returns 400 P1 fixes (bugs & performance): - FIX-07: CachingAllocator wired into all hot paths (to_device, embedding, rope, concat) - FIX-08: CudaDeviceProp buffer increased to 32KB for CUDA 12.9 safety - FIX-09: tokenizer byte_fallback graceful degradation (was panic) - FIX-19: causal mask uses -INFINITY instead of -1e9 (BF16 supports inf) - FIX-20: LayerNorm rewritten to numerically stable two-pass algorithm - FIX-21: min block size guard (32 threads) for LayerNorm/RMSNorm launches P2 fixes (improvements): - FIX-22: Option<GpuKVCache> + take() eliminates dummy KV cache allocations - FIX-23: RoPE cache no longer artificially capped at 8192 positions Verified on dash5 (RTX 5090): 51 tok/s batch=1, 74 tok/s 2-concurrent, 1.7-3.3x HF transformers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
134 lines
4.5 KiB
Rust
134 lines
4.5 KiB
Rust
use std::sync::Arc;
|
|
use xserv_cuda::{GpuBuffer, Result as CudaResult};
|
|
|
|
enum StorageInner {
|
|
Cpu { data: Vec<u8> },
|
|
Cuda { buffer: GpuBuffer, device: u32 },
|
|
}
|
|
|
|
/// Reference-counted storage for tensor data. Multiple tensors can share
|
|
/// the same storage (e.g., after transpose or slice — view semantics).
|
|
#[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(),
|
|
}
|
|
}
|
|
|
|
/// Get a read-only view of CPU data. Panics if storage is on GPU.
|
|
pub fn as_cpu_bytes(&self) -> &[u8] {
|
|
match self.0.as_ref() {
|
|
StorageInner::Cpu { data } => data,
|
|
StorageInner::Cuda { .. } => panic!("cannot access GPU storage as CPU bytes"),
|
|
}
|
|
}
|
|
|
|
pub fn gpu_buffer(&self) -> &GpuBuffer {
|
|
match self.0.as_ref() {
|
|
StorageInner::Cuda { buffer, .. } => buffer,
|
|
StorageInner::Cpu { .. } => panic!("cannot access CPU storage as GPU buffer"),
|
|
}
|
|
}
|
|
|
|
/// Copy to a different device. If already on the target device, clones the Arc (no copy).
|
|
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 cpu_data = self.as_cpu_bytes();
|
|
let mut buf = xserv_cuda::allocator::cached_alloc(cpu_data.len())?;
|
|
buf.copy_from_host(cpu_data)?;
|
|
Ok(Storage::cuda(buf, dev))
|
|
}
|
|
(Device::Cuda(_), Device::Cpu) => {
|
|
let gpu_buf = self.gpu_buffer();
|
|
let mut data = vec![0u8; gpu_buf.len()];
|
|
gpu_buf.copy_to_host(&mut data)?;
|
|
Ok(Storage::cpu(data))
|
|
}
|
|
(Device::Cuda(_), Device::Cuda(dev)) => {
|
|
let src = self.gpu_buffer();
|
|
let mut dst = xserv_cuda::allocator::cached_alloc(src.len())?;
|
|
dst.copy_from_device(src)?;
|
|
Ok(Storage::cuda(dst, dev))
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
/// Create a new owned copy of the storage on the same device.
|
|
pub fn deep_copy(&self) -> CudaResult<Self> {
|
|
match self.0.as_ref() {
|
|
StorageInner::Cpu { data } => Ok(Storage::cpu(data.clone())),
|
|
StorageInner::Cuda { buffer, device } => {
|
|
let mut dst = xserv_cuda::allocator::cached_alloc(buffer.len())?;
|
|
dst.copy_from_device(buffer)?;
|
|
Ok(Storage::cuda(dst, *device))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Allocate 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) => {
|
|
let mut buf = xserv_cuda::allocator::cached_alloc(len_bytes)?;
|
|
buf.zero()?;
|
|
Ok(Storage::cuda(buf, dev))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Allocate storage **without zeroing** on the given device.
|
|
/// The buffer may contain stale data from the caching allocator's pool.
|
|
/// Only use when the caller guarantees the kernel will fully overwrite
|
|
/// every element before any read.
|
|
pub fn empty(len_bytes: usize, device: Device) -> CudaResult<Self> {
|
|
match device {
|
|
Device::Cpu => Ok(Storage::cpu(vec![0u8; len_bytes])), // CPU still zeros (cheap)
|
|
Device::Cuda(dev) => {
|
|
let buf = xserv_cuda::allocator::cached_alloc(len_bytes)?;
|
|
Ok(Storage::cuda(buf, dev))
|
|
}
|
|
}
|
|
}
|
|
}
|