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