Skip to main content

cubecl_wgpu/compute/
storage.rs

1use cubecl_core::server::IoError;
2use cubecl_environment::backtrace::BackTrace;
3use cubecl_environment::collections::HashMap;
4use cubecl_server::storage::{ComputeStorage, StorageHandle, StorageId, StorageUtilization};
5use std::{num::NonZeroU64, ptr::NonNull};
6use wgpu::BufferUsages;
7
8/// Minimum buffer size in bytes. The WebGPU spec requires buffer sizes > 0, and shaders
9/// declare typed arrays (e.g. `array<vec4<f32>>`) that impose a minimum binding size.
10/// 32 bytes covers the largest possible binding type (`vec4<f64>`).
11const MIN_BUFFER_SIZE: u64 = 32;
12
13/// Buffer storage for wgpu.
14pub struct WgpuStorage {
15    memory: HashMap<StorageId, WgpuMemory>,
16    /// The bytes `memory` holds.
17    allocated: u64,
18    device: wgpu::Device,
19    buffer_usages: BufferUsages,
20    mem_alignment: usize,
21    #[allow(unused, reason = "keep it simple")]
22    vk_storage: bool,
23}
24
25impl core::fmt::Debug for WgpuStorage {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        f.write_str(format!("WgpuStorage {{ device: {:?} }}", self.device).as_str())
28    }
29}
30
31/// A buffer's persistent host mapping. Crate private: writes through it need an idle queue.
32#[derive(Debug, Clone, Copy)]
33pub(crate) struct HostPtr(pub(crate) NonNull<u8>);
34
35// SAFETY: Only the stream owning the buffer writes through it.
36unsafe impl Send for HostPtr {}
37unsafe impl Sync for HostPtr {}
38
39/// The memory resource that can be allocated for wgpu.
40#[derive(new, Debug, Clone)]
41pub struct WgpuResource {
42    /// The wgpu buffer.
43    pub buffer: wgpu::Buffer,
44    /// The buffer device address, if supported
45    pub address: Option<NonZeroU64>,
46    /// The buffer host mapping, if host visible.
47    #[new(default)]
48    pub(crate) host_ptr: Option<HostPtr>,
49    /// The buffer offset.
50    pub offset: u64,
51    /// The size of the resource.
52    ///
53    /// # Notes
54    ///
55    /// The result considers the offset.
56    pub size: u64,
57}
58
59/// The memory that can be allocated for wgpu.
60#[derive(new, Debug)]
61pub struct WgpuMemory {
62    /// The wgpu buffer.
63    pub buffer: wgpu::Buffer,
64    /// The buffer device address, if supported
65    pub address: Option<NonZeroU64>,
66    /// The buffer host mapping, if host visible.
67    #[new(default)]
68    pub(crate) host_ptr: Option<HostPtr>,
69}
70
71impl WgpuResource {
72    /// Return the binding view of the buffer.
73    pub fn as_wgpu_bind_resource(&self) -> wgpu::BindingResource<'_> {
74        // wgpu enforces 4-byte alignment for buffer binding sizes per the WebGPU spec.
75        // - https://github.com/gfx-rs/wgpu/pull/8041
76        //
77        // This padding is safe because:
78        // 1. In checked mode, bounds checks prevent reading beyond the logical size.
79        // 2. In unchecked mode, OOB access is already undefined behavior.
80        //
81        // For zero-sized resources, pass None (use rest of buffer from offset).
82        // The allocator guarantees the buffer is at least MIN_BUFFER_SIZE bytes.
83        let size = NonZeroU64::new(self.size.next_multiple_of(4));
84
85        let binding = wgpu::BufferBinding {
86            buffer: &self.buffer,
87            offset: self.offset,
88            size,
89        };
90        wgpu::BindingResource::Buffer(binding)
91    }
92}
93
94/// Keeps actual wgpu buffer references in a hashmap with ids as key.
95impl WgpuStorage {
96    /// Create a new storage on the given [device](wgpu::Device).
97    pub fn new(
98        mem_alignment: usize,
99        device: wgpu::Device,
100        usages: BufferUsages,
101        vk_storage: bool,
102    ) -> Self {
103        Self {
104            memory: HashMap::new(),
105            allocated: 0,
106            device,
107            buffer_usages: usages,
108            mem_alignment,
109            vk_storage,
110        }
111    }
112}
113
114impl ComputeStorage for WgpuStorage {
115    type Resource = WgpuResource;
116
117    fn alignment(&self) -> usize {
118        self.mem_alignment
119    }
120
121    fn get(&mut self, handle: &StorageHandle) -> Result<Self::Resource, IoError> {
122        let memory = self
123            .memory
124            .get(&handle.id)
125            .ok_or_else(|| IoError::StorageHandleNotFound {
126                reason: format!("{} in the wgpu buffer storage", handle.id).into(),
127                backtrace: BackTrace::capture(),
128            })?;
129        Ok(WgpuResource {
130            host_ptr: memory.host_ptr,
131            ..WgpuResource::new(
132                memory.buffer.clone(),
133                memory.address,
134                handle.offset(),
135                handle.size(),
136            )
137        })
138    }
139
140    #[cfg_attr(
141        feature = "tracing",
142        tracing::instrument(level = "trace", skip(self, size))
143    )]
144    fn alloc(&mut self, size: u64) -> Result<StorageHandle, IoError> {
145        let id = StorageId::new();
146
147        let alloc_size = size.max(MIN_BUFFER_SIZE);
148
149        let memory = self.create_buffer(&wgpu::BufferDescriptor {
150            label: None,
151            size: alloc_size,
152            usage: self.buffer_usages,
153            mapped_at_creation: false,
154        })?;
155
156        self.memory.insert(id, memory);
157        self.allocated += alloc_size;
158        Ok(StorageHandle::new(
159            id,
160            StorageUtilization { offset: 0, size },
161        ))
162    }
163
164    #[cfg_attr(feature = "tracing", tracing::instrument(level = "trace", skip(self)))]
165    fn dealloc(&mut self, id: StorageId) {
166        if let Some(memory) = self.memory.remove(&id) {
167            self.allocated -= memory.buffer.size();
168        }
169    }
170
171    fn flush(&mut self) {
172        // We don't wait for dealloc
173    }
174
175    fn bytes_allocated(&self) -> u64 {
176        self.allocated
177    }
178}
179
180impl WgpuStorage {
181    #[cfg(feature = "spirv")]
182    fn create_buffer(&self, desc: &wgpu::BufferDescriptor<'_>) -> Result<WgpuMemory, IoError> {
183        if self.vk_storage {
184            // wgpu currently doesn't expose this, even though it's used internally for acceleration
185            // structures. While we could use the acceleration structure input flag, that would
186            // then require ray tracing to be supported. So we need to allocate manually, then import
187            // the native buffer into wgpu using `from_raw_managed`.
188            // This actually skips some of the buffer batching stuff we don't really want in `gpu_allocator`.
189            crate::backend::vulkan::create_storage_buffer(&self.device, desc)
190        } else {
191            Ok(WgpuMemory::new(self.device.create_buffer(desc), None))
192        }
193    }
194
195    #[cfg(not(feature = "spirv"))]
196    fn create_buffer(&self, desc: &wgpu::BufferDescriptor<'_>) -> Result<WgpuMemory, IoError> {
197        Ok(WgpuMemory::new(self.device.create_buffer(desc), None))
198    }
199}