1use std::collections::HashMap;
7use std::sync::{Arc, Mutex};
8
9use oxicuda_backend::{
10 BackendError, BackendResult, BackendTranspose, BinaryOp, ComputeBackend, ReduceOp, UnaryOp,
11};
12use wgpu;
13
14use crate::{
15 device::WebGpuDevice,
16 memory::WebGpuMemoryManager,
17 planner::{self, Limits},
18 shader,
19};
20
21#[path = "backend_gpu_ops.rs"]
26mod gpu_ops;
27use gpu_ops::{
28 attention_cpu_reference, attention_gpu_dispatch_grid, conv2d_cpu_reference,
29 conv2d_gpu_dispatch_grid, conv2d_u32_dims,
30};
31
32#[path = "backend_cache.rs"]
36mod cache;
37use cache::{BindGroupCache, CachedPipeline};
38
39fn map_unary_op(op: UnaryOp) -> &'static str {
42 match op {
43 UnaryOp::Relu => "relu",
44 UnaryOp::Sigmoid => "sigmoid",
45 UnaryOp::Tanh => "tanh",
46 UnaryOp::Exp => "exp",
47 UnaryOp::Log => "log",
48 UnaryOp::Sqrt => "sqrt",
49 UnaryOp::Abs => "abs",
50 UnaryOp::Neg => "neg",
51 }
52}
53
54fn map_binary_op(op: BinaryOp) -> &'static str {
55 match op {
56 BinaryOp::Add => "add",
57 BinaryOp::Sub => "sub",
58 BinaryOp::Mul => "mul",
59 BinaryOp::Div => "div",
60 BinaryOp::Max => "max",
61 BinaryOp::Min => "min",
62 }
63}
64
65fn map_reduce_op(op: ReduceOp) -> &'static str {
66 match op {
67 ReduceOp::Sum => "sum",
68 ReduceOp::Max => "max",
69 ReduceOp::Min => "min",
70 ReduceOp::Mean => "mean",
71 }
72}
73
74fn packed_gemm_lds(
82 trans_a: BackendTranspose,
83 trans_b: BackendTranspose,
84 m: usize,
85 n: usize,
86 k: usize,
87) -> (usize, usize, usize) {
88 let lda = if trans_a == BackendTranspose::NoTrans {
89 k
90 } else {
91 m
92 };
93 let ldb = if trans_b == BackendTranspose::NoTrans {
94 n
95 } else {
96 k
97 };
98 (lda, ldb, n)
99}
100
101fn dim_u32(context: &str, name: &str, value: usize) -> BackendResult<u32> {
109 u32::try_from(value).map_err(|_| {
110 BackendError::InvalidArgument(format!("{context}: {name} {value} exceeds u32 range"))
111 })
112}
113
114fn gpu_limits() -> Limits {
148 Limits::portable_default()
149}
150
151#[derive(Debug)]
162pub struct WebGpuBackend {
163 device: Option<Arc<WebGpuDevice>>,
164 memory: Option<Arc<WebGpuMemoryManager>>,
165 initialized: bool,
166 pipeline_cache: Mutex<HashMap<String, CachedPipeline>>,
173 bind_group_cache: Mutex<BindGroupCache>,
181}
182
183impl WebGpuBackend {
184 pub fn new() -> Self {
186 Self {
187 device: None,
188 memory: None,
189 initialized: false,
190 pipeline_cache: Mutex::new(HashMap::new()),
191 bind_group_cache: Mutex::new(BindGroupCache::new()),
192 }
193 }
194
195 fn check_init(&self) -> BackendResult<()> {
197 if self.initialized {
198 Ok(())
199 } else {
200 Err(BackendError::NotInitialized)
201 }
202 }
203
204 fn memory(&self) -> BackendResult<&Arc<WebGpuMemoryManager>> {
206 self.memory.as_ref().ok_or(BackendError::NotInitialized)
207 }
208
209 fn device(&self) -> BackendResult<&Arc<WebGpuDevice>> {
211 self.device.as_ref().ok_or(BackendError::NotInitialized)
212 }
213
214 #[must_use]
220 pub fn supports_f16(&self) -> bool {
221 self.device.as_ref().is_some_and(|d| d.supports_f16)
222 }
223
224 fn reduce_nd(
238 &self,
239 op: ReduceOp,
240 input_ptr: u64,
241 output_ptr: u64,
242 shape: &[usize],
243 axis: usize,
244 ) -> BackendResult<()> {
245 debug_assert!(!shape.is_empty());
249 debug_assert!(axis < shape.len());
250
251 let outer: usize = shape[..axis].iter().product();
253 let dk: usize = shape[axis];
254 let inner: usize = shape[axis + 1..].iter().product();
255
256 if outer == 0 || dk == 0 || inner == 0 {
258 return Ok(());
259 }
260
261 let total = outer.checked_mul(inner).ok_or_else(|| {
262 BackendError::InvalidArgument("reduce: outer * inner overflows usize".into())
263 })?;
264 let in_elems = outer
265 .checked_mul(dk)
266 .and_then(|v| v.checked_mul(inner))
267 .ok_or_else(|| {
268 BackendError::InvalidArgument("reduce: outer * dk * inner overflows usize".into())
269 })?;
270
271 let inner_stride: usize = 1;
273 let dk_stride: usize = inner;
274 let outer_stride: usize = dk
275 .checked_mul(inner)
276 .ok_or_else(|| BackendError::InvalidArgument("reduce: dk * inner overflows".into()))?;
277
278 let limits = gpu_limits();
284 let (grid, grid_x) = planner::plan_dispatch_1d(&limits, total as u64, 1)
285 .map_err(BackendError::InvalidArgument)?;
286
287 let dev = self.device()?;
288 let mem = self.memory()?;
289 let op_str = map_reduce_op(op);
290 let pipeline_key = format!("reduce_nd:{op_str}");
291
292 let cached = self.cached_pipeline(&pipeline_key, "oxicuda-reduce-nd", || {
293 shader::reduction_nd_wgsl(op_str)
294 })?;
295
296 let mut params_bytes = [0u8; 32];
298 let outer_u32: u32 = outer
299 .try_into()
300 .map_err(|_| BackendError::InvalidArgument("reduce: outer exceeds u32 range".into()))?;
301 let dk_u32: u32 = dk
302 .try_into()
303 .map_err(|_| BackendError::InvalidArgument("reduce: dk exceeds u32 range".into()))?;
304 let inner_u32: u32 = inner
305 .try_into()
306 .map_err(|_| BackendError::InvalidArgument("reduce: inner exceeds u32 range".into()))?;
307 let outer_stride_u32: u32 = outer_stride.try_into().map_err(|_| {
308 BackendError::InvalidArgument("reduce: outer_stride exceeds u32 range".into())
309 })?;
310 let dk_stride_u32: u32 = dk_stride.try_into().map_err(|_| {
311 BackendError::InvalidArgument("reduce: dk_stride exceeds u32 range".into())
312 })?;
313 let inner_stride_u32: u32 = inner_stride.try_into().map_err(|_| {
314 BackendError::InvalidArgument("reduce: inner_stride exceeds u32 range".into())
315 })?;
316 params_bytes[0..4].copy_from_slice(&outer_u32.to_le_bytes());
317 params_bytes[4..8].copy_from_slice(&dk_u32.to_le_bytes());
318 params_bytes[8..12].copy_from_slice(&inner_u32.to_le_bytes());
319 params_bytes[12..16].copy_from_slice(&outer_stride_u32.to_le_bytes());
320 params_bytes[16..20].copy_from_slice(&dk_stride_u32.to_le_bytes());
321 params_bytes[20..24].copy_from_slice(&inner_stride_u32.to_le_bytes());
322 params_bytes[24..28].copy_from_slice(&grid_x.to_le_bytes());
323 let need_in = (in_elems as u64) * 4;
331 let need_out = (total as u64) * 4;
332 let bind_group = self.cached_bind_group(
333 dev,
334 mem,
335 &cached.bind_group_layout,
336 &pipeline_key,
337 &[input_ptr, output_ptr],
338 &[need_in, need_out],
339 ¶ms_bytes,
340 "oxicuda-reduce-nd",
341 )?;
342
343 let mut encoder = dev
344 .device
345 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
346 label: Some("oxicuda-reduce-nd"),
347 });
348 {
349 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
350 label: Some("oxicuda-reduce-nd"),
351 timestamp_writes: None,
352 });
353 pass.set_pipeline(&cached.pipeline);
354 pass.set_bind_group(0, &bind_group, &[]);
355 pass.dispatch_workgroups(grid.x, grid.y, grid.z);
356 }
357
358 dev.queue.submit(std::iter::once(encoder.finish()));
359 Ok(())
370 }
371}
372
373impl WebGpuBackend {
374 #[allow(clippy::too_many_arguments)]
385 pub fn gemm_f16(
386 &self,
387 trans_a: BackendTranspose,
388 trans_b: BackendTranspose,
389 m: usize,
390 n: usize,
391 k: usize,
392 alpha: f64,
393 a_ptr: u64,
394 lda: usize,
395 b_ptr: u64,
396 ldb: usize,
397 beta: f64,
398 c_ptr: u64,
399 ldc: usize,
400 ) -> BackendResult<()> {
401 self.check_init()?;
402 if m == 0 || n == 0 || k == 0 {
403 return Ok(());
404 }
405
406 let dev = self.device()?;
407 let mem = self.memory()?;
408
409 if !dev.supports_f16 {
414 return Err(BackendError::Unsupported(
415 "f16 GEMM requires the SHADER_F16 device feature, \
416 which this adapter does not support"
417 .into(),
418 ));
419 }
420
421 let trans_a_flag: u32 = u32::from(trans_a != BackendTranspose::NoTrans);
422 let trans_b_flag: u32 = u32::from(trans_b != BackendTranspose::NoTrans);
423
424 let (expected_lda, expected_ldb, expected_ldc) = packed_gemm_lds(trans_a, trans_b, m, n, k);
425 if lda < expected_lda || ldb < expected_ldb || ldc < expected_ldc {
426 return Err(BackendError::InvalidArgument(
427 "gemm_f16: leading dimension smaller than matrix extent".into(),
428 ));
429 }
430 let m_u32 = dim_u32("gemm_f16", "m", m)?;
431 let n_u32 = dim_u32("gemm_f16", "n", n)?;
432 let k_u32 = dim_u32("gemm_f16", "k", k)?;
433 let lda_u32 = dim_u32("gemm_f16", "lda", lda)?;
434 let ldb_u32 = dim_u32("gemm_f16", "ldb", ldb)?;
435 let ldc_u32 = dim_u32("gemm_f16", "ldc", ldc)?;
436
437 let limits = gpu_limits();
438 let tile = planner::plan_workgroup_square(&limits, 16);
439 let tile_size = tile.x;
440 let grid = planner::plan_dispatch_2d(&limits, m_u32, n_u32, tile, 1)
444 .map_err(BackendError::InvalidArgument)?;
445 let pipeline_key = format!("gemm_f16:{tile_size}");
446 let cached = self.cached_pipeline(&pipeline_key, "oxicuda-gemm-f16", || {
447 shader::gemm_wgsl_f16(tile_size)
448 })?;
449
450 let mut params_bytes = [0u8; 48];
454 params_bytes[0..4].copy_from_slice(&m_u32.to_le_bytes());
455 params_bytes[4..8].copy_from_slice(&n_u32.to_le_bytes());
456 params_bytes[8..12].copy_from_slice(&k_u32.to_le_bytes());
457 params_bytes[12..16].copy_from_slice(&(alpha as f32).to_le_bytes());
458 params_bytes[16..20].copy_from_slice(&(beta as f32).to_le_bytes());
459 params_bytes[20..24].copy_from_slice(&trans_a_flag.to_le_bytes());
460 params_bytes[24..28].copy_from_slice(&trans_b_flag.to_le_bytes());
461 params_bytes[28..32].copy_from_slice(&lda_u32.to_le_bytes());
462 params_bytes[32..36].copy_from_slice(&ldb_u32.to_le_bytes());
463 params_bytes[36..40].copy_from_slice(&ldc_u32.to_le_bytes());
464 let bind_group = self.cached_bind_group(
467 dev,
468 mem,
469 &cached.bind_group_layout,
470 &pipeline_key,
471 &[a_ptr, b_ptr, c_ptr],
472 &[],
473 ¶ms_bytes,
474 "oxicuda-gemm-f16",
475 )?;
476
477 let mut encoder = dev
478 .device
479 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
480 label: Some("oxicuda-gemm-f16"),
481 });
482
483 {
484 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
485 label: Some("oxicuda-gemm-f16"),
486 timestamp_writes: None,
487 });
488 pass.set_pipeline(&cached.pipeline);
489 pass.set_bind_group(0, &bind_group, &[]);
490 pass.dispatch_workgroups(grid.x, grid.y, grid.z);
491 }
492
493 dev.queue.submit(std::iter::once(encoder.finish()));
494 Ok(())
505 }
506}
507
508impl Default for WebGpuBackend {
509 fn default() -> Self {
510 Self::new()
511 }
512}
513
514impl ComputeBackend for WebGpuBackend {
517 fn name(&self) -> &str {
518 "webgpu"
519 }
520
521 fn init(&mut self) -> BackendResult<()> {
522 if self.initialized {
523 return Ok(());
524 }
525
526 match WebGpuDevice::new() {
527 Ok(dev) => {
528 let dev = Arc::new(dev);
529 tracing::info!("WebGPU backend initialised on: {}", dev.adapter_name);
530 let memory = WebGpuMemoryManager::new(Arc::clone(&dev));
531 self.device = Some(dev);
532 self.memory = Some(Arc::new(memory));
533 self.initialized = true;
534 Ok(())
535 }
536 Err(e) => Err(BackendError::from(e)),
537 }
538 }
539
540 fn is_initialized(&self) -> bool {
541 self.initialized
542 }
543
544 fn gemm(
547 &self,
548 trans_a: BackendTranspose,
549 trans_b: BackendTranspose,
550 m: usize,
551 n: usize,
552 k: usize,
553 alpha: f64,
554 a_ptr: u64,
555 lda: usize,
556 b_ptr: u64,
557 ldb: usize,
558 beta: f64,
559 c_ptr: u64,
560 ldc: usize,
561 ) -> BackendResult<()> {
562 self.check_init()?;
563 if m == 0 || n == 0 || k == 0 {
565 return Ok(());
566 }
567
568 let trans_a_flag: u32 = u32::from(trans_a != BackendTranspose::NoTrans);
572 let trans_b_flag: u32 = u32::from(trans_b != BackendTranspose::NoTrans);
573
574 let dev = self.device()?;
575 let mem = self.memory()?;
576
577 let m_u32 = dim_u32("gemm", "m", m)?;
581 let n_u32 = dim_u32("gemm", "n", n)?;
582 let k_u32 = dim_u32("gemm", "k", k)?;
583
584 let limits = gpu_limits();
585 let tile = planner::plan_workgroup_square(&limits, 16);
586 let tile_size = tile.x;
587 let grid = planner::plan_dispatch_2d(&limits, m_u32, n_u32, tile, 1)
591 .map_err(BackendError::InvalidArgument)?;
592 let pipeline_key = format!("gemm:{tile_size}");
593 let cached = self.cached_pipeline(&pipeline_key, "oxicuda-gemm", || {
594 shader::gemm_wgsl(tile_size)
595 })?;
596
597 let (expected_lda, expected_ldb, expected_ldc) = packed_gemm_lds(trans_a, trans_b, m, n, k);
602 if lda < expected_lda || ldb < expected_ldb || ldc < expected_ldc {
603 return Err(BackendError::InvalidArgument(
604 "gemm: leading dimension smaller than matrix extent".into(),
605 ));
606 }
607 let lda_u32 = dim_u32("gemm", "lda", lda)?;
608 let ldb_u32 = dim_u32("gemm", "ldb", ldb)?;
609 let ldc_u32 = dim_u32("gemm", "ldc", ldc)?;
610
611 let mut params_bytes = [0u8; 48];
614 params_bytes[0..4].copy_from_slice(&m_u32.to_le_bytes());
615 params_bytes[4..8].copy_from_slice(&n_u32.to_le_bytes());
616 params_bytes[8..12].copy_from_slice(&k_u32.to_le_bytes());
617 params_bytes[12..16].copy_from_slice(&(alpha as f32).to_le_bytes());
618 params_bytes[16..20].copy_from_slice(&(beta as f32).to_le_bytes());
619 params_bytes[20..24].copy_from_slice(&trans_a_flag.to_le_bytes());
620 params_bytes[24..28].copy_from_slice(&trans_b_flag.to_le_bytes());
621 params_bytes[28..32].copy_from_slice(&lda_u32.to_le_bytes());
622 params_bytes[32..36].copy_from_slice(&ldb_u32.to_le_bytes());
623 params_bytes[36..40].copy_from_slice(&ldc_u32.to_le_bytes());
624 let bind_group = self.cached_bind_group(
627 dev,
628 mem,
629 &cached.bind_group_layout,
630 &pipeline_key,
631 &[a_ptr, b_ptr, c_ptr],
632 &[],
633 ¶ms_bytes,
634 "oxicuda-gemm",
635 )?;
636
637 let mut encoder = dev
638 .device
639 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
640 label: Some("oxicuda-gemm"),
641 });
642
643 {
644 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
645 label: Some("oxicuda-gemm"),
646 timestamp_writes: None,
647 });
648 pass.set_pipeline(&cached.pipeline);
649 pass.set_bind_group(0, &bind_group, &[]);
650 pass.dispatch_workgroups(grid.x, grid.y, grid.z);
651 }
652
653 dev.queue.submit(std::iter::once(encoder.finish()));
654 Ok(())
665 }
666
667 #[allow(clippy::too_many_arguments)]
668 fn batched_gemm(
669 &self,
670 trans_a: BackendTranspose,
671 trans_b: BackendTranspose,
672 m: usize,
673 n: usize,
674 k: usize,
675 alpha: f64,
676 a_ptr: u64,
677 lda: usize,
678 stride_a: usize,
679 b_ptr: u64,
680 ldb: usize,
681 stride_b: usize,
682 beta: f64,
683 c_ptr: u64,
684 ldc: usize,
685 stride_c: usize,
686 batch_count: usize,
687 ) -> BackendResult<()> {
688 self.check_init()?;
689
690 if batch_count == 0 || m == 0 || n == 0 || k == 0 {
691 return Ok(());
692 }
693
694 let trans_a_flag: u32 = u32::from(trans_a != BackendTranspose::NoTrans);
698 let trans_b_flag: u32 = u32::from(trans_b != BackendTranspose::NoTrans);
699
700 let dev = self.device()?;
701 let mem = self.memory()?;
702
703 let m_u32 = dim_u32("batched_gemm", "m", m)?;
713 let n_u32 = dim_u32("batched_gemm", "n", n)?;
714 let k_u32 = dim_u32("batched_gemm", "k", k)?;
715 let batch_u32 = dim_u32("batched_gemm", "batch_count", batch_count)?;
716 let stride_a_u32 = dim_u32("batched_gemm", "stride_a", stride_a)?;
717 let stride_b_u32 = dim_u32("batched_gemm", "stride_b", stride_b)?;
718 let stride_c_u32 = dim_u32("batched_gemm", "stride_c", stride_c)?;
719
720 let limits = gpu_limits();
721 let tile = planner::plan_workgroup_square(&limits, 16);
722 let tile_size = tile.x;
723 let grid = planner::plan_dispatch_2d(&limits, m_u32, n_u32, tile, batch_u32)
727 .map_err(BackendError::InvalidArgument)?;
728 let pipeline_key = format!("batched_gemm:{tile_size}");
729 let cached = self.cached_pipeline(&pipeline_key, "oxicuda-batched-gemm", || {
730 shader::batched_gemm_wgsl(tile_size)
731 })?;
732
733 let (expected_lda, expected_ldb, expected_ldc) = packed_gemm_lds(trans_a, trans_b, m, n, k);
736 if lda < expected_lda || ldb < expected_ldb || ldc < expected_ldc {
737 return Err(BackendError::InvalidArgument(
738 "batched_gemm: leading dimension smaller than matrix extent".into(),
739 ));
740 }
741 let lda_u32 = dim_u32("batched_gemm", "lda", lda)?;
742 let ldb_u32 = dim_u32("batched_gemm", "ldb", ldb)?;
743 let ldc_u32 = dim_u32("batched_gemm", "ldc", ldc)?;
744
745 let mut params_bytes = [0u8; 64];
749 params_bytes[0..4].copy_from_slice(&m_u32.to_le_bytes());
750 params_bytes[4..8].copy_from_slice(&n_u32.to_le_bytes());
751 params_bytes[8..12].copy_from_slice(&k_u32.to_le_bytes());
752 params_bytes[12..16].copy_from_slice(&(alpha as f32).to_le_bytes());
753 params_bytes[16..20].copy_from_slice(&(beta as f32).to_le_bytes());
754 params_bytes[20..24].copy_from_slice(&batch_u32.to_le_bytes());
755 params_bytes[24..28].copy_from_slice(&stride_a_u32.to_le_bytes());
756 params_bytes[28..32].copy_from_slice(&stride_b_u32.to_le_bytes());
757 params_bytes[32..36].copy_from_slice(&stride_c_u32.to_le_bytes());
758 params_bytes[36..40].copy_from_slice(&trans_a_flag.to_le_bytes());
759 params_bytes[40..44].copy_from_slice(&trans_b_flag.to_le_bytes());
760 params_bytes[44..48].copy_from_slice(&lda_u32.to_le_bytes());
761 params_bytes[48..52].copy_from_slice(&ldb_u32.to_le_bytes());
762 params_bytes[52..56].copy_from_slice(&ldc_u32.to_le_bytes());
763 let bind_group = self.cached_bind_group(
766 dev,
767 mem,
768 &cached.bind_group_layout,
769 &pipeline_key,
770 &[a_ptr, b_ptr, c_ptr],
771 &[],
772 ¶ms_bytes,
773 "oxicuda-batched-gemm",
774 )?;
775
776 let mut encoder = dev
777 .device
778 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
779 label: Some("oxicuda-batched-gemm"),
780 });
781
782 {
783 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
784 label: Some("oxicuda-batched-gemm"),
785 timestamp_writes: None,
786 });
787 pass.set_pipeline(&cached.pipeline);
788 pass.set_bind_group(0, &bind_group, &[]);
789 pass.dispatch_workgroups(grid.x, grid.y, grid.z);
790 }
791
792 dev.queue.submit(std::iter::once(encoder.finish()));
793 Ok(())
804 }
805
806 fn conv2d_forward(
807 &self,
808 input_ptr: u64,
809 input_shape: &[usize],
810 filter_ptr: u64,
811 filter_shape: &[usize],
812 output_ptr: u64,
813 output_shape: &[usize],
814 stride: &[usize],
815 padding: &[usize],
816 ) -> BackendResult<()> {
817 self.check_init()?;
818
819 if input_shape.len() != 4 {
820 return Err(BackendError::InvalidArgument(
821 "input_shape must have 4 elements (NCHW)".into(),
822 ));
823 }
824 if filter_shape.len() != 4 {
825 return Err(BackendError::InvalidArgument(
826 "filter_shape must have 4 elements (KCFHFW)".into(),
827 ));
828 }
829 if output_shape.len() != 4 {
830 return Err(BackendError::InvalidArgument(
831 "output_shape must have 4 elements (NKOhOw)".into(),
832 ));
833 }
834 if stride.len() != 2 {
835 return Err(BackendError::InvalidArgument(
836 "stride must have 2 elements [sh, sw]".into(),
837 ));
838 }
839 if padding.len() != 2 {
840 return Err(BackendError::InvalidArgument(
841 "padding must have 2 elements [ph, pw]".into(),
842 ));
843 }
844
845 let batch = input_shape[0];
846 let c_in = input_shape[1];
847 let h_in = input_shape[2];
848 let w_in = input_shape[3];
849 let k_out = filter_shape[0];
850 let fh = filter_shape[2];
851 let fw = filter_shape[3];
852 let oh = output_shape[2];
853 let ow = output_shape[3];
854 let sh = stride[0];
855 let sw = stride[1];
856 let ph = padding[0];
857 let pw = padding[1];
858
859 let in_elems: usize = input_shape.iter().product();
860 let f_elems: usize = filter_shape.iter().product();
861 let o_elems: usize = output_shape.iter().product();
862
863 if let Some((wg_x, wg_y)) = conv2d_gpu_dispatch_grid(batch, k_out, oh, ow) {
867 if let Some(dims) = conv2d_u32_dims(
868 batch, c_in, h_in, w_in, k_out, fh, fw, oh, ow, sh, sw, ph, pw,
869 ) {
870 return self.conv2d_forward_gpu(
871 input_ptr, filter_ptr, output_ptr, dims, in_elems, f_elems, o_elems, wg_x, wg_y,
872 );
873 }
874 }
875
876 let mem = self.memory()?;
878 let mut in_bytes = vec![0u8; in_elems * 4];
879 let mut f_bytes = vec![0u8; f_elems * 4];
880 mem.copy_from_device(&mut in_bytes, input_ptr)
881 .map_err(BackendError::from)?;
882 mem.copy_from_device(&mut f_bytes, filter_ptr)
883 .map_err(BackendError::from)?;
884
885 let in_f32 = bytes_to_f32_vec(&in_bytes);
886 let f_f32 = bytes_to_f32_vec(&f_bytes);
887 let out_f32 = conv2d_cpu_reference(
888 &in_f32, &f_f32, batch, c_in, h_in, w_in, k_out, fh, fw, oh, ow, sh, sw, ph, pw,
889 );
890
891 let out_bytes = f32_slice_to_bytes(&out_f32);
892 mem.copy_to_device(output_ptr, &out_bytes)
893 .map_err(BackendError::from)?;
894
895 Ok(())
896 }
897
898 fn attention(
899 &self,
900 q_ptr: u64,
901 k_ptr: u64,
902 v_ptr: u64,
903 o_ptr: u64,
904 batch: usize,
905 heads: usize,
906 seq_q: usize,
907 seq_kv: usize,
908 head_dim: usize,
909 scale: f64,
910 causal: bool,
911 ) -> BackendResult<()> {
912 self.check_init()?;
913
914 if seq_q == 0 || seq_kv == 0 || head_dim == 0 {
915 return Err(BackendError::InvalidArgument(
916 "seq_q, seq_kv, and head_dim must all be > 0".into(),
917 ));
918 }
919 if scale <= 0.0 || !scale.is_finite() {
920 return Err(BackendError::InvalidArgument(format!(
921 "scale must be a positive finite number, got {scale}"
922 )));
923 }
924
925 let batch_heads = batch * heads;
926 let q_elems = batch_heads * seq_q * head_dim;
927 let kv_elems = batch_heads * seq_kv * head_dim;
928 let o_elems = q_elems;
929 let scale_f32 = scale as f32;
930
931 if let (Some(wg), Some(bh_u32), Some(seq_q_u32), Some(seq_kv_u32), Some(head_dim_u32)) = (
936 attention_gpu_dispatch_grid(batch_heads, seq_q),
937 u32::try_from(batch_heads).ok(),
938 u32::try_from(seq_q).ok(),
939 u32::try_from(seq_kv).ok(),
940 u32::try_from(head_dim).ok(),
941 ) {
942 return self.attention_gpu(
943 q_ptr,
944 k_ptr,
945 v_ptr,
946 o_ptr,
947 bh_u32,
948 seq_q_u32,
949 seq_kv_u32,
950 head_dim_u32,
951 scale_f32,
952 causal,
953 q_elems,
954 kv_elems,
955 o_elems,
956 wg,
957 );
958 }
959
960 let mem = self.memory()?;
962 let mut q_bytes = vec![0u8; q_elems * 4];
963 let mut k_bytes = vec![0u8; kv_elems * 4];
964 let mut v_bytes = vec![0u8; kv_elems * 4];
965
966 mem.copy_from_device(&mut q_bytes, q_ptr)
967 .map_err(BackendError::from)?;
968 mem.copy_from_device(&mut k_bytes, k_ptr)
969 .map_err(BackendError::from)?;
970 mem.copy_from_device(&mut v_bytes, v_ptr)
971 .map_err(BackendError::from)?;
972
973 let q_f32 = bytes_to_f32_vec(&q_bytes);
974 let k_f32 = bytes_to_f32_vec(&k_bytes);
975 let v_f32 = bytes_to_f32_vec(&v_bytes);
976 let o_f32 = attention_cpu_reference(
977 &q_f32,
978 &k_f32,
979 &v_f32,
980 batch_heads,
981 seq_q,
982 seq_kv,
983 head_dim,
984 scale_f32,
985 causal,
986 );
987
988 let o_bytes = f32_slice_to_bytes(&o_f32);
989 mem.copy_to_device(o_ptr, &o_bytes)
990 .map_err(BackendError::from)?;
991
992 Ok(())
993 }
994
995 fn reduce(
996 &self,
997 op: ReduceOp,
998 input_ptr: u64,
999 output_ptr: u64,
1000 shape: &[usize],
1001 axis: usize,
1002 ) -> BackendResult<()> {
1003 self.check_init()?;
1004
1005 if shape.is_empty() {
1006 return Err(BackendError::InvalidArgument(
1007 "shape must not be empty".into(),
1008 ));
1009 }
1010 if axis >= shape.len() {
1011 return Err(BackendError::InvalidArgument(format!(
1012 "axis {axis} is out of bounds for shape of length {}",
1013 shape.len()
1014 )));
1015 }
1016
1017 if shape.len() != 1 {
1021 return self.reduce_nd(op, input_ptr, output_ptr, shape, axis);
1022 }
1023
1024 let n_elements = shape[0];
1025 if n_elements == 0 {
1026 return Ok(());
1027 }
1028
1029 let dev = self.device()?;
1030 let mem = self.memory()?;
1031 let op_str = map_reduce_op(op);
1032
1033 let limits = gpu_limits();
1041 let (wg_grid, _) = planner::plan_dispatch_1d(&limits, n_elements as u64, 256)
1042 .map_err(BackendError::InvalidArgument)?;
1043 if wg_grid.y != 1 {
1044 return Err(BackendError::InvalidArgument(format!(
1045 "reduce: {n_elements} elements need {} workgroups, which exceeds the \
1046 single-axis dispatch capacity of this 1-D reduction kernel",
1047 wg_grid.x as u64 * wg_grid.y as u64
1048 )));
1049 }
1050 let wg_count = wg_grid.x;
1051
1052 let pass1_cached = self.cached_pipeline(
1053 &format!("reduce_pass1:{op_str}"),
1054 "oxicuda-reduce-pass1",
1055 || shader::reduction_wgsl(op_str),
1056 )?;
1057
1058 let partial_buf = dev.device.create_buffer(&wgpu::BufferDescriptor {
1060 label: Some("oxicuda-reduce-partial"),
1061 size: (wg_count as u64) * 4, usage: wgpu::BufferUsages::STORAGE
1063 | wgpu::BufferUsages::COPY_SRC
1064 | wgpu::BufferUsages::COPY_DST,
1065 mapped_at_creation: false,
1066 });
1067
1068 let mut p1_params = [0u8; 4];
1070 p1_params[0..4].copy_from_slice(&(n_elements as u32).to_le_bytes());
1071 let p1_uniform = dev.device.create_buffer(&wgpu::BufferDescriptor {
1072 label: Some("oxicuda-reduce-p1-params"),
1073 size: 4,
1074 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1075 mapped_at_creation: false,
1076 });
1077 dev.queue.write_buffer(&p1_uniform, 0, &p1_params);
1078
1079 let bgl1 = &pass1_cached.bind_group_layout;
1080
1081 let bg1 = {
1082 let buffers = mem
1083 .lock_buffers()
1084 .map_err(|e| BackendError::DeviceError(e.to_string()))?;
1085 let in_info = buffers.get(&input_ptr).ok_or_else(|| {
1086 BackendError::InvalidArgument(format!("unknown handle {input_ptr}"))
1087 })?;
1088
1089 let need_in = (n_elements as u64) * 4;
1090 if in_info.size < need_in {
1091 return Err(BackendError::InvalidArgument(format!(
1092 "reduce: input buffer holds {} bytes, need {need_in} for {n_elements} f32 elements",
1093 in_info.size
1094 )));
1095 }
1096
1097 dev.device.create_bind_group(&wgpu::BindGroupDescriptor {
1098 label: Some("oxicuda-reduce-pass1"),
1099 layout: bgl1,
1100 entries: &[
1101 wgpu::BindGroupEntry {
1102 binding: 0,
1103 resource: in_info.buffer.as_entire_binding(),
1104 },
1105 wgpu::BindGroupEntry {
1106 binding: 1,
1107 resource: partial_buf.as_entire_binding(),
1108 },
1109 wgpu::BindGroupEntry {
1110 binding: 2,
1111 resource: p1_uniform.as_entire_binding(),
1112 },
1113 ],
1114 })
1115 };
1116
1117 let pass2_cached = self.cached_pipeline(
1119 &format!("reduce_pass2:{op_str}"),
1120 "oxicuda-reduce-pass2",
1121 || shader::reduction_final_wgsl(op_str),
1122 )?;
1123
1124 let mut p2_params = [0u8; 4];
1126 p2_params[0..4].copy_from_slice(&wg_count.to_le_bytes());
1127 let p2_uniform = dev.device.create_buffer(&wgpu::BufferDescriptor {
1128 label: Some("oxicuda-reduce-p2-params"),
1129 size: 4,
1130 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1131 mapped_at_creation: false,
1132 });
1133 dev.queue.write_buffer(&p2_uniform, 0, &p2_params);
1134
1135 let bgl2 = &pass2_cached.bind_group_layout;
1136
1137 let bg2 = {
1138 let buffers = mem
1139 .lock_buffers()
1140 .map_err(|e| BackendError::DeviceError(e.to_string()))?;
1141 let out_info = buffers.get(&output_ptr).ok_or_else(|| {
1142 BackendError::InvalidArgument(format!("unknown handle {output_ptr}"))
1143 })?;
1144
1145 if out_info.size < 4 {
1147 return Err(BackendError::InvalidArgument(format!(
1148 "reduce: output buffer holds {} bytes, need 4 for the scalar result",
1149 out_info.size
1150 )));
1151 }
1152
1153 dev.device.create_bind_group(&wgpu::BindGroupDescriptor {
1154 label: Some("oxicuda-reduce-pass2"),
1155 layout: bgl2,
1156 entries: &[
1157 wgpu::BindGroupEntry {
1158 binding: 0,
1159 resource: partial_buf.as_entire_binding(),
1160 },
1161 wgpu::BindGroupEntry {
1162 binding: 1,
1163 resource: out_info.buffer.as_entire_binding(),
1164 },
1165 wgpu::BindGroupEntry {
1166 binding: 2,
1167 resource: p2_uniform.as_entire_binding(),
1168 },
1169 ],
1170 })
1171 };
1172
1173 let mut encoder = dev
1175 .device
1176 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1177 label: Some("oxicuda-reduce"),
1178 });
1179
1180 {
1181 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1182 label: Some("oxicuda-reduce-pass1"),
1183 timestamp_writes: None,
1184 });
1185 pass.set_pipeline(&pass1_cached.pipeline);
1186 pass.set_bind_group(0, &bg1, &[]);
1187 pass.dispatch_workgroups(wg_count, 1, 1);
1188 }
1189 {
1190 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1191 label: Some("oxicuda-reduce-pass2"),
1192 timestamp_writes: None,
1193 });
1194 pass.set_pipeline(&pass2_cached.pipeline);
1195 pass.set_bind_group(0, &bg2, &[]);
1196 pass.dispatch_workgroups(1, 1, 1);
1197 }
1198
1199 dev.queue.submit(std::iter::once(encoder.finish()));
1200 if op == ReduceOp::Mean && n_elements > 1 {
1212 let mut buf = [0u8; 4];
1213 mem.copy_from_device(&mut buf, output_ptr)
1214 .map_err(BackendError::from)?;
1215 let val = f32::from_le_bytes(buf);
1216 let mean = val / (n_elements as f32);
1217 mem.copy_to_device(output_ptr, &mean.to_le_bytes())
1218 .map_err(BackendError::from)?;
1219 }
1220
1221 Ok(())
1222 }
1223
1224 fn unary(&self, op: UnaryOp, input_ptr: u64, output_ptr: u64, n: usize) -> BackendResult<()> {
1225 self.check_init()?;
1226 if n == 0 {
1227 return Ok(());
1228 }
1229 if input_ptr == output_ptr {
1240 return Err(BackendError::InvalidArgument(
1241 "unary: input_ptr and output_ptr must not alias (wgpu rejects binding the \
1242 same buffer as both `read` and `read_write` within one dispatch); allocate \
1243 a separate output buffer"
1244 .into(),
1245 ));
1246 }
1247
1248 let dev = self.device()?;
1249 let mem = self.memory()?;
1250
1251 let (wg_grid, _) = planner::plan_dispatch_1d(&gpu_limits(), n as u64, 256)
1255 .map_err(BackendError::InvalidArgument)?;
1256 if wg_grid.y != 1 {
1257 return Err(BackendError::InvalidArgument(format!(
1258 "unary: {n} elements exceed the single-axis dispatch capacity of this kernel"
1259 )));
1260 }
1261
1262 let op_str = map_unary_op(op);
1263 let pipeline_key = format!("unary:{op_str}");
1264 let cached = self.cached_pipeline(&pipeline_key, "oxicuda-unary", || {
1265 shader::elementwise_wgsl(op_str)
1266 })?;
1267
1268 let bind_group = self.cached_bind_group(
1271 dev,
1272 mem,
1273 &cached.bind_group_layout,
1274 &pipeline_key,
1275 &[input_ptr, output_ptr],
1276 &[],
1277 &[],
1278 "oxicuda-unary",
1279 )?;
1280
1281 let mut encoder = dev
1282 .device
1283 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1284 label: Some("oxicuda-unary"),
1285 });
1286
1287 {
1288 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1289 label: Some("oxicuda-unary"),
1290 timestamp_writes: None,
1291 });
1292 pass.set_pipeline(&cached.pipeline);
1293 pass.set_bind_group(0, &bind_group, &[]);
1294 pass.dispatch_workgroups(wg_grid.x, 1, 1);
1295 }
1296
1297 dev.queue.submit(std::iter::once(encoder.finish()));
1298 Ok(())
1309 }
1310
1311 fn binary(
1312 &self,
1313 op: BinaryOp,
1314 a_ptr: u64,
1315 b_ptr: u64,
1316 output_ptr: u64,
1317 n: usize,
1318 ) -> BackendResult<()> {
1319 self.check_init()?;
1320 if n == 0 {
1321 return Ok(());
1322 }
1323 if a_ptr == output_ptr || b_ptr == output_ptr {
1329 return Err(BackendError::InvalidArgument(
1330 "binary: a_ptr/b_ptr must not alias output_ptr (wgpu rejects binding the \
1331 same buffer as both `read` and `read_write` within one dispatch); allocate \
1332 a separate output buffer"
1333 .into(),
1334 ));
1335 }
1336
1337 let dev = self.device()?;
1338 let mem = self.memory()?;
1339
1340 let (wg_grid, _) = planner::plan_dispatch_1d(&gpu_limits(), n as u64, 256)
1342 .map_err(BackendError::InvalidArgument)?;
1343 if wg_grid.y != 1 {
1344 return Err(BackendError::InvalidArgument(format!(
1345 "binary: {n} elements exceed the single-axis dispatch capacity of this kernel"
1346 )));
1347 }
1348
1349 let op_str = map_binary_op(op);
1350 let pipeline_key = format!("binary:{op_str}");
1351 let cached = self.cached_pipeline(&pipeline_key, "oxicuda-binary", || {
1352 shader::binary_wgsl(op_str)
1353 })?;
1354
1355 let bind_group = self.cached_bind_group(
1357 dev,
1358 mem,
1359 &cached.bind_group_layout,
1360 &pipeline_key,
1361 &[a_ptr, b_ptr, output_ptr],
1362 &[],
1363 &[],
1364 "oxicuda-binary",
1365 )?;
1366
1367 let mut encoder = dev
1368 .device
1369 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1370 label: Some("oxicuda-binary"),
1371 });
1372
1373 {
1374 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1375 label: Some("oxicuda-binary"),
1376 timestamp_writes: None,
1377 });
1378 pass.set_pipeline(&cached.pipeline);
1379 pass.set_bind_group(0, &bind_group, &[]);
1380 pass.dispatch_workgroups(wg_grid.x, 1, 1);
1381 }
1382
1383 dev.queue.submit(std::iter::once(encoder.finish()));
1384 Ok(())
1395 }
1396
1397 fn synchronize(&self) -> BackendResult<()> {
1400 self.check_init()?;
1401 if let Some(dev) = &self.device {
1402 crate::memory::poll_result_to_webgpu_result(
1413 dev.device.poll(wgpu::PollType::wait_indefinitely()),
1414 )
1415 .map_err(BackendError::from)?;
1416
1417 if let Some(msg) = dev.poll_error() {
1423 return Err(BackendError::from(
1424 crate::error::WebGpuError::UncapturedError(msg),
1425 ));
1426 }
1427 }
1428 Ok(())
1429 }
1430
1431 fn alloc(&self, bytes: usize) -> BackendResult<u64> {
1434 self.check_init()?;
1435 if bytes == 0 {
1436 return Err(BackendError::InvalidArgument(
1437 "cannot allocate 0 bytes".into(),
1438 ));
1439 }
1440 self.memory()?.alloc(bytes).map_err(BackendError::from)
1441 }
1442
1443 fn free(&self, ptr: u64) -> BackendResult<()> {
1444 self.check_init()?;
1445 self.evict_bind_group_cache(ptr)?;
1452 self.memory()?.free(ptr).map_err(BackendError::from)
1453 }
1454
1455 fn copy_htod(&self, dst: u64, src: &[u8]) -> BackendResult<()> {
1456 self.check_init()?;
1457 if src.is_empty() {
1458 return Ok(());
1459 }
1460 self.memory()?
1461 .copy_to_device(dst, src)
1462 .map_err(BackendError::from)
1463 }
1464
1465 fn copy_dtoh(&self, dst: &mut [u8], src: u64) -> BackendResult<()> {
1466 self.check_init()?;
1467 if dst.is_empty() {
1468 return Ok(());
1469 }
1470 self.memory()?
1471 .copy_from_device(dst, src)
1472 .map_err(BackendError::from)
1473 }
1474}
1475
1476fn bytes_to_f32_vec(bytes: &[u8]) -> Vec<f32> {
1480 bytes
1481 .chunks_exact(4)
1482 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
1483 .collect()
1484}
1485
1486fn f32_slice_to_bytes(data: &[f32]) -> Vec<u8> {
1488 data.iter().flat_map(|v| v.to_le_bytes()).collect()
1489}
1490
1491#[cfg(test)]
1496#[path = "backend_tests.rs"]
1497mod tests;