1mod 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")))]
58pub(crate) const fn gpu_backends() -> wgpu::Backends {
59 wgpu::Backends::PRIMARY
61}
62
63#[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
75pub(crate) fn shared_instance() -> wgpu::Instance {
76 use std::sync::OnceLock;
77 static INSTANCE: OnceLock<wgpu::Instance> = OnceLock::new();
78 INSTANCE
79 .get_or_init(|| {
80 let _guard = DEVICE_INIT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
82 wgpu::Instance::new(&wgpu::InstanceDescriptor {
87 backends: gpu_backends(),
88 ..Default::default()
89 })
90 })
91 .clone()
92}
93
94#[derive(Clone)]
96pub struct GpuDevice {
97 pub device: wgpu::Device,
98 pub queue: wgpu::Queue,
99}
100
101impl GpuDevice {
102 #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
104 pub fn new() -> Result<Self, String> {
105 let _guard = DEVICE_INIT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
108 runtime::block_on(async { Self::new_async().await })
109 }
110
111 pub async fn new_async() -> Result<Self, String> {
113 let instance = shared_instance();
115
116 let adapter = instance
118 .request_adapter(&wgpu::RequestAdapterOptions {
119 power_preference: wgpu::PowerPreference::HighPerformance,
120 compatible_surface: None,
121 force_fallback_adapter: false,
122 })
123 .await
124 .map_err(|e| format!("Failed to find GPU adapter: {}", e))?;
125
126 let mut limits = wgpu::Limits::default();
130 limits.max_buffer_size = adapter.limits().max_buffer_size;
131 limits.max_storage_buffer_binding_size = adapter.limits().max_storage_buffer_binding_size;
132
133 let (device, queue) = adapter
134 .request_device(&wgpu::DeviceDescriptor {
135 label: Some("Trueno GPU Device"),
136 required_features: wgpu::Features::empty(),
137 required_limits: limits,
138 memory_hints: wgpu::MemoryHints::Performance,
139 experimental_features: Default::default(),
140 trace: Default::default(),
141 })
142 .await
143 .map_err(|e| format!("Failed to create device: {}", e))?;
144
145 Ok(Self { device, queue })
146 }
147
148 #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
153 pub fn new_with_adapter_index(index: u32) -> Result<Self, String> {
154 let _guard = DEVICE_INIT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
156 runtime::block_on(async { Self::new_with_adapter_index_async(index).await })
157 }
158
159 pub async fn new_with_adapter_index_async(index: u32) -> Result<Self, String> {
164 let instance = shared_instance();
165 let adapters = instance.enumerate_adapters(gpu_backends());
167
168 if adapters.is_empty() {
169 return Err("No GPU adapters found".to_string());
170 }
171
172 let adapter = adapters
173 .into_iter()
174 .nth(index as usize)
175 .ok_or_else(|| format!("GPU adapter index {} out of range", index))?;
176
177 let mut limits = wgpu::Limits::default();
178 limits.max_buffer_size = adapter.limits().max_buffer_size;
179 limits.max_storage_buffer_binding_size = adapter.limits().max_storage_buffer_binding_size;
180
181 let (device, queue) = adapter
182 .request_device(&wgpu::DeviceDescriptor {
183 label: Some(&format!("Trueno GPU Device [{}]", index)),
184 required_features: wgpu::Features::empty(),
185 required_limits: limits,
186 memory_hints: wgpu::MemoryHints::Performance,
187 experimental_features: Default::default(),
188 trace: Default::default(),
189 })
190 .await
191 .map_err(|e| format!("Failed to create device at index {}: {}", index, e))?;
192
193 Ok(Self { device, queue })
194 }
195
196 #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
200 pub fn list_adapters() -> Vec<(u32, String, String)> {
201 let _guard = DEVICE_INIT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
204 runtime::block_on(Self::list_adapters_async())
205 }
206
207 pub async fn list_adapters_async() -> Vec<(u32, String, String)> {
209 let instance = shared_instance();
210 let adapters = instance.enumerate_adapters(gpu_backends());
212
213 adapters
214 .iter()
215 .enumerate()
216 .map(|(idx, adapter)| {
217 let info = adapter.get_info();
218 (idx as u32, info.name, format!("{:?}", info.backend))
219 })
220 .collect()
221 }
222
223 #[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
240 pub fn is_available() -> bool {
241 use std::sync::OnceLock;
242 static AVAILABLE: OnceLock<bool> = OnceLock::new();
243 *AVAILABLE.get_or_init(|| runtime::block_on(Self::is_available_async()))
244 }
245
246 pub async fn is_available_async() -> bool {
248 let instance = shared_instance();
249 instance
250 .request_adapter(&wgpu::RequestAdapterOptions {
251 power_preference: wgpu::PowerPreference::HighPerformance,
252 compatible_surface: None,
253 force_fallback_adapter: false,
254 })
255 .await
256 .is_ok()
257 }
258
259 pub(super) async fn execute_element_wise_op(
272 &self,
273 op_name: &str,
274 shader_source: &str,
275 input: &[f32],
276 result: &mut [f32],
277 uniform_data: Option<&[u8]>,
278 ) -> Result<(), String> {
279 let len = input.len();
280
281 let shader = self.device.create_shader_module(wgpu::ShaderModuleDescriptor {
283 label: Some(&format!("{} Shader", op_name)),
284 source: wgpu::ShaderSource::Wgsl(shader_source.into()),
285 });
286
287 let input_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
289 label: Some(&format!("{} Input", op_name)),
290 size: std::mem::size_of_val(input) as u64,
291 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
292 mapped_at_creation: false,
293 });
294
295 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
297 label: Some(&format!("{} Output", op_name)),
298 size: std::mem::size_of_val(result) as u64,
299 usage: wgpu::BufferUsages::STORAGE
300 | wgpu::BufferUsages::COPY_SRC
301 | wgpu::BufferUsages::COPY_DST,
302 mapped_at_creation: false,
303 });
304
305 self.queue.write_buffer(&input_buffer, 0, bytemuck::cast_slice(input));
307
308 let uniform_buffer = uniform_data.map(|data| {
310 let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
311 label: Some(&format!("{} Uniform", op_name)),
312 size: data.len() as u64,
313 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
314 mapped_at_creation: false,
315 });
316 self.queue.write_buffer(&buffer, 0, data);
317 buffer
318 });
319
320 let mut bind_group_entries = vec![
322 wgpu::BindGroupLayoutEntry {
323 binding: 0,
324 visibility: wgpu::ShaderStages::COMPUTE,
325 ty: wgpu::BindingType::Buffer {
326 ty: wgpu::BufferBindingType::Storage { read_only: true },
327 has_dynamic_offset: false,
328 min_binding_size: None,
329 },
330 count: None,
331 },
332 wgpu::BindGroupLayoutEntry {
333 binding: 1,
334 visibility: wgpu::ShaderStages::COMPUTE,
335 ty: wgpu::BindingType::Buffer {
336 ty: wgpu::BufferBindingType::Storage { read_only: false },
337 has_dynamic_offset: false,
338 min_binding_size: None,
339 },
340 count: None,
341 },
342 ];
343
344 if uniform_buffer.is_some() {
346 bind_group_entries.push(wgpu::BindGroupLayoutEntry {
347 binding: 2,
348 visibility: wgpu::ShaderStages::COMPUTE,
349 ty: wgpu::BindingType::Buffer {
350 ty: wgpu::BufferBindingType::Uniform,
351 has_dynamic_offset: false,
352 min_binding_size: None,
353 },
354 count: None,
355 });
356 }
357
358 let bind_group_layout =
360 self.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
361 label: Some(&format!("{} Bind Group Layout", op_name)),
362 entries: &bind_group_entries,
363 });
364
365 let mut bind_entries = vec![
367 wgpu::BindGroupEntry { binding: 0, resource: input_buffer.as_entire_binding() },
368 wgpu::BindGroupEntry { binding: 1, resource: output_buffer.as_entire_binding() },
369 ];
370
371 if let Some(ref uniform_buf) = uniform_buffer {
373 bind_entries.push(wgpu::BindGroupEntry {
374 binding: 2,
375 resource: uniform_buf.as_entire_binding(),
376 });
377 }
378
379 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
381 label: Some(&format!("{} Bind Group", op_name)),
382 layout: &bind_group_layout,
383 entries: &bind_entries,
384 });
385
386 let pipeline_layout = self.device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
388 label: Some(&format!("{} Pipeline Layout", op_name)),
389 bind_group_layouts: &[&bind_group_layout],
390 push_constant_ranges: &[],
391 });
392
393 let pipeline = self.device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
394 label: Some(&format!("{} Pipeline", op_name)),
395 layout: Some(&pipeline_layout),
396 module: &shader,
397 entry_point: Some("main"),
398 compilation_options: Default::default(),
399 cache: None,
400 });
401
402 let staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
404 label: Some(&format!("{} Staging Buffer", op_name)),
405 size: std::mem::size_of_val(result) as u64,
406 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
407 mapped_at_creation: false,
408 });
409
410 let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
412 label: Some(&format!("{} Encoder", op_name)),
413 });
414
415 {
416 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
417 label: Some(&format!("{} Pass", op_name)),
418 timestamp_writes: None,
419 });
420 compute_pass.set_pipeline(&pipeline);
421 compute_pass.set_bind_group(0, &bind_group, &[]);
422
423 let workgroup_size = 256;
425 let num_workgroups = (len as u32).div_ceil(workgroup_size);
426
427 compute_pass.dispatch_workgroups(num_workgroups, 1, 1);
428 }
429
430 encoder.copy_buffer_to_buffer(
432 &output_buffer,
433 0,
434 &staging_buffer,
435 0,
436 std::mem::size_of_val(result) as u64,
437 );
438
439 self.queue.submit(Some(encoder.finish()));
441
442 let buffer_slice = staging_buffer.slice(..);
444 let (sender, receiver) = futures_intrusive::channel::shared::oneshot_channel();
445 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
446 sender.send(result).ok();
447 });
448
449 self.device.poll(wgpu::PollType::Wait { submission_index: None, timeout: None }).ok();
451
452 receiver
453 .receive()
454 .await
455 .ok_or("Failed to receive mapping result")?
456 .map_err(|e| format!("Buffer mapping failed: {:?}", e))?;
457
458 {
459 let data = buffer_slice.get_mapped_range();
460 result.copy_from_slice(bytemuck::cast_slice(&data));
461 }
462
463 staging_buffer.unmap();
464
465 Ok(())
466 }
467}
468
469#[cfg(all(test, feature = "gpu", not(target_arch = "wasm32")))]
470mod tests {
471 use super::*;
472
473 #[test]
480 fn test_gpu_backends_excludes_gles() {
481 let mask = gpu_backends();
482
483 assert!(
485 !mask.contains(wgpu::Backends::GL),
486 "gpu_backends() must NOT include Backends::GL (GLES/EGL panics in Drop \
487 on Linux/AMD-RADV → SIGABRT). mask = {:?}",
488 mask
489 );
490
491 #[cfg(any(target_os = "linux", target_os = "android"))]
493 assert!(
494 mask.contains(wgpu::Backends::VULKAN),
495 "gpu_backends() must include VULKAN on Linux (AMD-RADV/NVIDIA). mask = {:?}",
496 mask
497 );
498 #[cfg(target_os = "macos")]
499 assert!(
500 mask.contains(wgpu::Backends::METAL),
501 "gpu_backends() must include METAL on macOS (Apple Silicon). mask = {:?}",
502 mask
503 );
504 #[cfg(target_os = "windows")]
505 assert!(
506 mask.contains(wgpu::Backends::VULKAN) || mask.contains(wgpu::Backends::DX12),
507 "gpu_backends() must include VULKAN or DX12 on Windows. mask = {:?}",
508 mask
509 );
510 }
511
512 #[test]
513 fn test_is_available_consistency() {
514 let available = GpuDevice::is_available();
517 let device_result = GpuDevice::new();
518
519 if available {
520 assert!(
522 device_result.is_ok(),
523 "is_available() returned true, but GpuDevice::new() failed"
524 );
525 } else {
526 eprintln!(
530 "GPU not available (is_available=false), device creation result: {:?}",
531 device_result.is_err()
532 );
533 }
534 }
535
536 #[test]
537 fn test_reduce_sum_not_hardcoded() {
538 if !GpuDevice::is_available() {
540 eprintln!("GPU not available, skipping test");
541 return;
542 }
543
544 let device = GpuDevice::new().expect("Failed to create GPU device");
545 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");
549
550 assert_ne!(result, -1.0, "reduce_sum returned hardcoded -1.0 (mutant not killed)");
552
553 let expected: f32 = input.iter().sum();
555 assert!(
556 (result - expected).abs() < 1e-4,
557 "reduce_sum({:?}) = {} (expected {})",
558 input,
559 result,
560 expected
561 );
562 }
563}