trueno/backends/gpu/device/
mod.rs1mod activations;
13mod backward;
14mod eigen;
15pub(crate) mod linalg;
16mod reductions;
17
18#[cfg(any(feature = "gpu", feature = "gpu-wasm"))]
19use super::runtime;
20
21#[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
34static DEVICE_INIT_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
35
36#[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
48pub(crate) fn shared_instance() -> wgpu::Instance {
49 use std::sync::OnceLock;
50 static INSTANCE: OnceLock<wgpu::Instance> = OnceLock::new();
51 INSTANCE
52 .get_or_init(|| {
53 let _guard = DEVICE_INIT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
55 wgpu::Instance::default()
56 })
57 .clone()
58}
59
60#[derive(Clone)]
62pub struct GpuDevice {
63 pub device: wgpu::Device,
64 pub queue: wgpu::Queue,
65}
66
67impl GpuDevice {
68 #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
70 pub fn new() -> Result<Self, String> {
71 let _guard = DEVICE_INIT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
74 runtime::block_on(async { Self::new_async().await })
75 }
76
77 pub async fn new_async() -> Result<Self, String> {
79 let instance = shared_instance();
81
82 let adapter = instance
84 .request_adapter(&wgpu::RequestAdapterOptions {
85 power_preference: wgpu::PowerPreference::HighPerformance,
86 compatible_surface: None,
87 force_fallback_adapter: false,
88 })
89 .await
90 .map_err(|e| format!("Failed to find GPU adapter: {}", e))?;
91
92 let mut limits = wgpu::Limits::default();
96 limits.max_buffer_size = adapter.limits().max_buffer_size;
97 limits.max_storage_buffer_binding_size = adapter.limits().max_storage_buffer_binding_size;
98
99 let (device, queue) = adapter
100 .request_device(&wgpu::DeviceDescriptor {
101 label: Some("Trueno GPU Device"),
102 required_features: wgpu::Features::empty(),
103 required_limits: limits,
104 memory_hints: wgpu::MemoryHints::Performance,
105 experimental_features: Default::default(),
106 trace: Default::default(),
107 })
108 .await
109 .map_err(|e| format!("Failed to create device: {}", e))?;
110
111 Ok(Self { device, queue })
112 }
113
114 #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
119 pub fn new_with_adapter_index(index: u32) -> Result<Self, String> {
120 let _guard = DEVICE_INIT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
122 runtime::block_on(async { Self::new_with_adapter_index_async(index).await })
123 }
124
125 pub async fn new_with_adapter_index_async(index: u32) -> Result<Self, String> {
130 let instance = shared_instance();
131 let adapters = instance.enumerate_adapters(wgpu::Backends::all());
132
133 if adapters.is_empty() {
134 return Err("No GPU adapters found".to_string());
135 }
136
137 let adapter = adapters
138 .into_iter()
139 .nth(index as usize)
140 .ok_or_else(|| format!("GPU adapter index {} out of range", index))?;
141
142 let mut limits = wgpu::Limits::default();
143 limits.max_buffer_size = adapter.limits().max_buffer_size;
144 limits.max_storage_buffer_binding_size = adapter.limits().max_storage_buffer_binding_size;
145
146 let (device, queue) = adapter
147 .request_device(&wgpu::DeviceDescriptor {
148 label: Some(&format!("Trueno GPU Device [{}]", index)),
149 required_features: wgpu::Features::empty(),
150 required_limits: limits,
151 memory_hints: wgpu::MemoryHints::Performance,
152 experimental_features: Default::default(),
153 trace: Default::default(),
154 })
155 .await
156 .map_err(|e| format!("Failed to create device at index {}: {}", index, e))?;
157
158 Ok(Self { device, queue })
159 }
160
161 #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
165 pub fn list_adapters() -> Vec<(u32, String, String)> {
166 let _guard = DEVICE_INIT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
169 runtime::block_on(Self::list_adapters_async())
170 }
171
172 pub async fn list_adapters_async() -> Vec<(u32, String, String)> {
174 let instance = shared_instance();
175 let adapters = instance.enumerate_adapters(wgpu::Backends::all());
176
177 adapters
178 .iter()
179 .enumerate()
180 .map(|(idx, adapter)| {
181 let info = adapter.get_info();
182 (idx as u32, info.name, format!("{:?}", info.backend))
183 })
184 .collect()
185 }
186
187 #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
204 pub fn is_available() -> bool {
205 use std::sync::OnceLock;
206 static AVAILABLE: OnceLock<bool> = OnceLock::new();
207 *AVAILABLE.get_or_init(|| runtime::block_on(Self::is_available_async()))
208 }
209
210 pub async fn is_available_async() -> bool {
212 let instance = shared_instance();
213 instance
214 .request_adapter(&wgpu::RequestAdapterOptions {
215 power_preference: wgpu::PowerPreference::HighPerformance,
216 compatible_surface: None,
217 force_fallback_adapter: false,
218 })
219 .await
220 .is_ok()
221 }
222
223 pub(super) async fn execute_element_wise_op(
236 &self,
237 op_name: &str,
238 shader_source: &str,
239 input: &[f32],
240 result: &mut [f32],
241 uniform_data: Option<&[u8]>,
242 ) -> Result<(), String> {
243 let len = input.len();
244
245 let shader = self.device.create_shader_module(wgpu::ShaderModuleDescriptor {
247 label: Some(&format!("{} Shader", op_name)),
248 source: wgpu::ShaderSource::Wgsl(shader_source.into()),
249 });
250
251 let input_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
253 label: Some(&format!("{} Input", op_name)),
254 size: std::mem::size_of_val(input) as u64,
255 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
256 mapped_at_creation: false,
257 });
258
259 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
261 label: Some(&format!("{} Output", op_name)),
262 size: std::mem::size_of_val(result) as u64,
263 usage: wgpu::BufferUsages::STORAGE
264 | wgpu::BufferUsages::COPY_SRC
265 | wgpu::BufferUsages::COPY_DST,
266 mapped_at_creation: false,
267 });
268
269 self.queue.write_buffer(&input_buffer, 0, bytemuck::cast_slice(input));
271
272 let uniform_buffer = uniform_data.map(|data| {
274 let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
275 label: Some(&format!("{} Uniform", op_name)),
276 size: data.len() as u64,
277 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
278 mapped_at_creation: false,
279 });
280 self.queue.write_buffer(&buffer, 0, data);
281 buffer
282 });
283
284 let mut bind_group_entries = vec![
286 wgpu::BindGroupLayoutEntry {
287 binding: 0,
288 visibility: wgpu::ShaderStages::COMPUTE,
289 ty: wgpu::BindingType::Buffer {
290 ty: wgpu::BufferBindingType::Storage { read_only: true },
291 has_dynamic_offset: false,
292 min_binding_size: None,
293 },
294 count: None,
295 },
296 wgpu::BindGroupLayoutEntry {
297 binding: 1,
298 visibility: wgpu::ShaderStages::COMPUTE,
299 ty: wgpu::BindingType::Buffer {
300 ty: wgpu::BufferBindingType::Storage { read_only: false },
301 has_dynamic_offset: false,
302 min_binding_size: None,
303 },
304 count: None,
305 },
306 ];
307
308 if uniform_buffer.is_some() {
310 bind_group_entries.push(wgpu::BindGroupLayoutEntry {
311 binding: 2,
312 visibility: wgpu::ShaderStages::COMPUTE,
313 ty: wgpu::BindingType::Buffer {
314 ty: wgpu::BufferBindingType::Uniform,
315 has_dynamic_offset: false,
316 min_binding_size: None,
317 },
318 count: None,
319 });
320 }
321
322 let bind_group_layout =
324 self.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
325 label: Some(&format!("{} Bind Group Layout", op_name)),
326 entries: &bind_group_entries,
327 });
328
329 let mut bind_entries = vec![
331 wgpu::BindGroupEntry { binding: 0, resource: input_buffer.as_entire_binding() },
332 wgpu::BindGroupEntry { binding: 1, resource: output_buffer.as_entire_binding() },
333 ];
334
335 if let Some(ref uniform_buf) = uniform_buffer {
337 bind_entries.push(wgpu::BindGroupEntry {
338 binding: 2,
339 resource: uniform_buf.as_entire_binding(),
340 });
341 }
342
343 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
345 label: Some(&format!("{} Bind Group", op_name)),
346 layout: &bind_group_layout,
347 entries: &bind_entries,
348 });
349
350 let pipeline_layout = self.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
352 label: Some(&format!("{} Pipeline Layout", op_name)),
353 bind_group_layouts: &[&bind_group_layout],
354 push_constant_ranges: &[],
355 });
356
357 let pipeline = self.device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
358 label: Some(&format!("{} Pipeline", op_name)),
359 layout: Some(&pipeline_layout),
360 module: &shader,
361 entry_point: Some("main"),
362 compilation_options: Default::default(),
363 cache: None,
364 });
365
366 let staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
368 label: Some(&format!("{} Staging Buffer", op_name)),
369 size: std::mem::size_of_val(result) as u64,
370 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
371 mapped_at_creation: false,
372 });
373
374 let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
376 label: Some(&format!("{} Encoder", op_name)),
377 });
378
379 {
380 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
381 label: Some(&format!("{} Pass", op_name)),
382 timestamp_writes: None,
383 });
384 compute_pass.set_pipeline(&pipeline);
385 compute_pass.set_bind_group(0, &bind_group, &[]);
386
387 let workgroup_size = 256;
389 let num_workgroups = (len as u32).div_ceil(workgroup_size);
390
391 compute_pass.dispatch_workgroups(num_workgroups, 1, 1);
392 }
393
394 encoder.copy_buffer_to_buffer(
396 &output_buffer,
397 0,
398 &staging_buffer,
399 0,
400 std::mem::size_of_val(result) as u64,
401 );
402
403 self.queue.submit(Some(encoder.finish()));
405
406 let buffer_slice = staging_buffer.slice(..);
408 let (sender, receiver) = futures_intrusive::channel::shared::oneshot_channel();
409 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
410 sender.send(result).ok();
411 });
412
413 self.device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None }).ok();
415
416 receiver
417 .receive()
418 .await
419 .ok_or("Failed to receive mapping result")?
420 .map_err(|e| format!("Buffer mapping failed: {:?}", e))?;
421
422 {
423 let data = buffer_slice.get_mapped_range();
424 result.copy_from_slice(bytemuck::cast_slice(&data));
425 }
426
427 staging_buffer.unmap();
428
429 Ok(())
430 }
431}
432
433#[cfg(all(test, feature = "gpu", not(target_arch = "wasm32")))]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn test_is_available_consistency() {
439 let available = GpuDevice::is_available();
442 let device_result = GpuDevice::new();
443
444 if available {
445 assert!(
447 device_result.is_ok(),
448 "is_available() returned true, but GpuDevice::new() failed"
449 );
450 } else {
451 eprintln!(
455 "GPU not available (is_available=false), device creation result: {:?}",
456 device_result.is_err()
457 );
458 }
459 }
460
461 #[test]
462 fn test_reduce_sum_not_hardcoded() {
463 if !GpuDevice::is_available() {
465 eprintln!("GPU not available, skipping test");
466 return;
467 }
468
469 let device = GpuDevice::new().expect("Failed to create GPU device");
470 let input = vec![1.0, 2.0, 3.0, 4.0, 5.0]; let result = runtime::block_on(device.reduce_sum(&input)).expect("reduce_sum failed");
474
475 assert_ne!(result, -1.0, "reduce_sum returned hardcoded -1.0 (mutant not killed)");
477
478 let expected: f32 = input.iter().sum();
480 assert!(
481 (result - expected).abs() < 1e-4,
482 "reduce_sum({:?}) = {} (expected {})",
483 input,
484 result,
485 expected
486 );
487 }
488}