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::{device::WebGpuDevice, memory::WebGpuMemoryManager, shader};
15
16fn map_unary_op(op: UnaryOp) -> &'static str {
19 match op {
20 UnaryOp::Relu => "relu",
21 UnaryOp::Sigmoid => "sigmoid",
22 UnaryOp::Tanh => "tanh",
23 UnaryOp::Exp => "exp",
24 UnaryOp::Log => "log",
25 UnaryOp::Sqrt => "sqrt",
26 UnaryOp::Abs => "abs",
27 UnaryOp::Neg => "neg",
28 }
29}
30
31fn map_binary_op(op: BinaryOp) -> &'static str {
32 match op {
33 BinaryOp::Add => "add",
34 BinaryOp::Sub => "sub",
35 BinaryOp::Mul => "mul",
36 BinaryOp::Div => "div",
37 BinaryOp::Max => "max",
38 BinaryOp::Min => "min",
39 }
40}
41
42fn map_reduce_op(op: ReduceOp) -> &'static str {
43 match op {
44 ReduceOp::Sum => "sum",
45 ReduceOp::Max => "max",
46 ReduceOp::Min => "min",
47 ReduceOp::Mean => "mean",
48 }
49}
50
51fn packed_gemm_lds(
59 trans_a: BackendTranspose,
60 trans_b: BackendTranspose,
61 m: usize,
62 n: usize,
63 k: usize,
64) -> (usize, usize, usize) {
65 let lda = if trans_a == BackendTranspose::NoTrans {
66 k
67 } else {
68 m
69 };
70 let ldb = if trans_b == BackendTranspose::NoTrans {
71 n
72 } else {
73 k
74 };
75 (lda, ldb, n)
76}
77
78fn lead_dim_u32(name: &str, value: usize) -> BackendResult<u32> {
81 u32::try_from(value).map_err(|_| {
82 BackendError::InvalidArgument(format!("gemm: {name} {value} exceeds u32 range"))
83 })
84}
85
86#[derive(Debug)]
97pub struct WebGpuBackend {
98 device: Option<Arc<WebGpuDevice>>,
99 memory: Option<Arc<WebGpuMemoryManager>>,
100 initialized: bool,
101 pipeline_cache: Mutex<HashMap<String, wgpu::ComputePipeline>>,
106}
107
108impl WebGpuBackend {
109 pub fn new() -> Self {
111 Self {
112 device: None,
113 memory: None,
114 initialized: false,
115 pipeline_cache: Mutex::new(HashMap::new()),
116 }
117 }
118
119 fn cached_pipeline(
125 &self,
126 key: &str,
127 label: &str,
128 build: impl FnOnce() -> String,
129 ) -> BackendResult<wgpu::ComputePipeline> {
130 let mut cache = self
131 .pipeline_cache
132 .lock()
133 .map_err(|_| BackendError::DeviceError("pipeline cache mutex poisoned".into()))?;
134
135 if let Some(pipeline) = cache.get(key) {
136 return Ok(pipeline.clone());
137 }
138
139 let dev = self.device()?;
140 let wgsl = build();
141 let shader_mod = dev
142 .device
143 .create_shader_module(wgpu::ShaderModuleDescriptor {
144 label: Some(label),
145 source: wgpu::ShaderSource::Wgsl(wgsl.into()),
146 });
147 let pipeline = dev
148 .device
149 .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
150 label: Some(label),
151 layout: None,
152 module: &shader_mod,
153 entry_point: Some("main"),
154 compilation_options: Default::default(),
155 cache: None,
156 });
157
158 cache.insert(key.to_string(), pipeline.clone());
159 Ok(pipeline)
160 }
161
162 fn check_init(&self) -> BackendResult<()> {
164 if self.initialized {
165 Ok(())
166 } else {
167 Err(BackendError::NotInitialized)
168 }
169 }
170
171 fn memory(&self) -> BackendResult<&Arc<WebGpuMemoryManager>> {
173 self.memory.as_ref().ok_or(BackendError::NotInitialized)
174 }
175
176 fn device(&self) -> BackendResult<&Arc<WebGpuDevice>> {
178 self.device.as_ref().ok_or(BackendError::NotInitialized)
179 }
180
181 fn reduce_nd(
195 &self,
196 op: ReduceOp,
197 input_ptr: u64,
198 output_ptr: u64,
199 shape: &[usize],
200 axis: usize,
201 ) -> BackendResult<()> {
202 debug_assert!(!shape.is_empty());
206 debug_assert!(axis < shape.len());
207
208 let outer: usize = shape[..axis].iter().product();
210 let dk: usize = shape[axis];
211 let inner: usize = shape[axis + 1..].iter().product();
212
213 if outer == 0 || dk == 0 || inner == 0 {
215 return Ok(());
216 }
217
218 let total = outer.checked_mul(inner).ok_or_else(|| {
219 BackendError::InvalidArgument("reduce: outer * inner overflows usize".into())
220 })?;
221
222 let inner_stride: usize = 1;
224 let dk_stride: usize = inner;
225 let outer_stride: usize = dk
226 .checked_mul(inner)
227 .ok_or_else(|| BackendError::InvalidArgument("reduce: dk * inner overflows".into()))?;
228
229 const MAX_GRID_DIM: u32 = 32_768;
232 let total_u32: u32 = total.try_into().map_err(|_| {
233 BackendError::InvalidArgument(format!(
234 "reduce: output element count {total} exceeds u32 range"
235 ))
236 })?;
237 let grid_x: u32 = total_u32.clamp(1, MAX_GRID_DIM);
238 let grid_y: u32 = total_u32.div_ceil(grid_x);
239
240 let dev = self.device()?;
241 let mem = self.memory()?;
242 let op_str = map_reduce_op(op);
243
244 let pipeline =
245 self.cached_pipeline(&format!("reduce_nd:{op_str}"), "oxicuda-reduce-nd", || {
246 shader::reduction_nd_wgsl(op_str)
247 })?;
248
249 let mut params_bytes = [0u8; 32];
251 let outer_u32: u32 = outer
252 .try_into()
253 .map_err(|_| BackendError::InvalidArgument("reduce: outer exceeds u32 range".into()))?;
254 let dk_u32: u32 = dk
255 .try_into()
256 .map_err(|_| BackendError::InvalidArgument("reduce: dk exceeds u32 range".into()))?;
257 let inner_u32: u32 = inner
258 .try_into()
259 .map_err(|_| BackendError::InvalidArgument("reduce: inner exceeds u32 range".into()))?;
260 let outer_stride_u32: u32 = outer_stride.try_into().map_err(|_| {
261 BackendError::InvalidArgument("reduce: outer_stride exceeds u32 range".into())
262 })?;
263 let dk_stride_u32: u32 = dk_stride.try_into().map_err(|_| {
264 BackendError::InvalidArgument("reduce: dk_stride exceeds u32 range".into())
265 })?;
266 let inner_stride_u32: u32 = inner_stride.try_into().map_err(|_| {
267 BackendError::InvalidArgument("reduce: inner_stride exceeds u32 range".into())
268 })?;
269 params_bytes[0..4].copy_from_slice(&outer_u32.to_le_bytes());
270 params_bytes[4..8].copy_from_slice(&dk_u32.to_le_bytes());
271 params_bytes[8..12].copy_from_slice(&inner_u32.to_le_bytes());
272 params_bytes[12..16].copy_from_slice(&outer_stride_u32.to_le_bytes());
273 params_bytes[16..20].copy_from_slice(&dk_stride_u32.to_le_bytes());
274 params_bytes[20..24].copy_from_slice(&inner_stride_u32.to_le_bytes());
275 params_bytes[24..28].copy_from_slice(&grid_x.to_le_bytes());
276 let uniform_buf = dev.device.create_buffer(&wgpu::BufferDescriptor {
279 label: Some("oxicuda-reduce-nd-params"),
280 size: 32,
281 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
282 mapped_at_creation: false,
283 });
284 dev.queue.write_buffer(&uniform_buf, 0, ¶ms_bytes);
285
286 let bgl = pipeline.get_bind_group_layout(0);
287 let bind_group = {
288 let buffers = mem
289 .lock_buffers()
290 .map_err(|e| BackendError::DeviceError(e.to_string()))?;
291 let in_info = buffers.get(&input_ptr).ok_or_else(|| {
292 BackendError::InvalidArgument(format!("unknown handle {input_ptr}"))
293 })?;
294 let out_info = buffers.get(&output_ptr).ok_or_else(|| {
295 BackendError::InvalidArgument(format!("unknown handle {output_ptr}"))
296 })?;
297
298 dev.device.create_bind_group(&wgpu::BindGroupDescriptor {
299 label: Some("oxicuda-reduce-nd"),
300 layout: &bgl,
301 entries: &[
302 wgpu::BindGroupEntry {
303 binding: 0,
304 resource: in_info.buffer.as_entire_binding(),
305 },
306 wgpu::BindGroupEntry {
307 binding: 1,
308 resource: out_info.buffer.as_entire_binding(),
309 },
310 wgpu::BindGroupEntry {
311 binding: 2,
312 resource: uniform_buf.as_entire_binding(),
313 },
314 ],
315 })
316 };
317
318 let mut encoder = dev
319 .device
320 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
321 label: Some("oxicuda-reduce-nd"),
322 });
323 {
324 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
325 label: Some("oxicuda-reduce-nd"),
326 timestamp_writes: None,
327 });
328 pass.set_pipeline(&pipeline);
329 pass.set_bind_group(0, &bind_group, &[]);
330 pass.dispatch_workgroups(grid_x, grid_y, 1);
331 }
332
333 dev.queue.submit(std::iter::once(encoder.finish()));
334 let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
335
336 Ok(())
337 }
338}
339
340impl WebGpuBackend {
341 #[allow(clippy::too_many_arguments)]
349 pub fn gemm_f16(
350 &self,
351 m: usize,
352 n: usize,
353 k: usize,
354 alpha: f64,
355 a_ptr: u64,
356 b_ptr: u64,
357 beta: f64,
358 c_ptr: u64,
359 ) -> BackendResult<()> {
360 self.check_init()?;
361 if m == 0 || n == 0 || k == 0 {
362 return Ok(());
363 }
364
365 let dev = self.device()?;
366 let mem = self.memory()?;
367
368 if !dev.supports_f16 {
373 return Err(BackendError::Unsupported(
374 "f16 GEMM requires the SHADER_F16 device feature, \
375 which this adapter does not support"
376 .into(),
377 ));
378 }
379
380 let tile_size: u32 = 8;
381 let pipeline = self.cached_pipeline("gemm_f16", "oxicuda-gemm-f16", || {
382 shader::gemm_wgsl_f16(tile_size)
383 })?;
384
385 let bgl = pipeline.get_bind_group_layout(0);
386
387 let mut params_bytes = [0u8; 20];
389 params_bytes[0..4].copy_from_slice(&(m as u32).to_le_bytes());
390 params_bytes[4..8].copy_from_slice(&(n as u32).to_le_bytes());
391 params_bytes[8..12].copy_from_slice(&(k as u32).to_le_bytes());
392 params_bytes[12..16].copy_from_slice(&(alpha as f32).to_le_bytes());
393 params_bytes[16..20].copy_from_slice(&(beta as f32).to_le_bytes());
394
395 let uniform_buf = dev.device.create_buffer(&wgpu::BufferDescriptor {
396 label: Some("oxicuda-gemm-f16-params"),
397 size: 20,
398 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
399 mapped_at_creation: false,
400 });
401 dev.queue.write_buffer(&uniform_buf, 0, ¶ms_bytes);
402
403 let bind_group = {
404 let buffers = mem
405 .lock_buffers()
406 .map_err(|e| BackendError::DeviceError(e.to_string()))?;
407 let a_info = buffers
408 .get(&a_ptr)
409 .ok_or_else(|| BackendError::InvalidArgument(format!("unknown handle {a_ptr}")))?;
410 let b_info = buffers
411 .get(&b_ptr)
412 .ok_or_else(|| BackendError::InvalidArgument(format!("unknown handle {b_ptr}")))?;
413 let c_info = buffers
414 .get(&c_ptr)
415 .ok_or_else(|| BackendError::InvalidArgument(format!("unknown handle {c_ptr}")))?;
416
417 dev.device.create_bind_group(&wgpu::BindGroupDescriptor {
418 label: Some("oxicuda-gemm-f16"),
419 layout: &bgl,
420 entries: &[
421 wgpu::BindGroupEntry {
422 binding: 0,
423 resource: a_info.buffer.as_entire_binding(),
424 },
425 wgpu::BindGroupEntry {
426 binding: 1,
427 resource: b_info.buffer.as_entire_binding(),
428 },
429 wgpu::BindGroupEntry {
430 binding: 2,
431 resource: c_info.buffer.as_entire_binding(),
432 },
433 wgpu::BindGroupEntry {
434 binding: 3,
435 resource: uniform_buf.as_entire_binding(),
436 },
437 ],
438 })
439 };
440
441 let mut encoder = dev
442 .device
443 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
444 label: Some("oxicuda-gemm-f16"),
445 });
446
447 {
448 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
449 label: Some("oxicuda-gemm-f16"),
450 timestamp_writes: None,
451 });
452 pass.set_pipeline(&pipeline);
453 pass.set_bind_group(0, &bind_group, &[]);
454 let wg_x = (n as u32).div_ceil(tile_size);
455 let wg_y = (m as u32).div_ceil(tile_size);
456 pass.dispatch_workgroups(wg_x, wg_y, 1);
457 }
458
459 dev.queue.submit(std::iter::once(encoder.finish()));
460 let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
461
462 Ok(())
463 }
464}
465
466impl Default for WebGpuBackend {
467 fn default() -> Self {
468 Self::new()
469 }
470}
471
472impl ComputeBackend for WebGpuBackend {
475 fn name(&self) -> &str {
476 "webgpu"
477 }
478
479 fn init(&mut self) -> BackendResult<()> {
480 if self.initialized {
481 return Ok(());
482 }
483
484 match WebGpuDevice::new() {
485 Ok(dev) => {
486 let dev = Arc::new(dev);
487 tracing::info!("WebGPU backend initialised on: {}", dev.adapter_name);
488 let memory = WebGpuMemoryManager::new(Arc::clone(&dev));
489 self.device = Some(dev);
490 self.memory = Some(Arc::new(memory));
491 self.initialized = true;
492 Ok(())
493 }
494 Err(e) => Err(BackendError::from(e)),
495 }
496 }
497
498 fn is_initialized(&self) -> bool {
499 self.initialized
500 }
501
502 fn gemm(
505 &self,
506 trans_a: BackendTranspose,
507 trans_b: BackendTranspose,
508 m: usize,
509 n: usize,
510 k: usize,
511 alpha: f64,
512 a_ptr: u64,
513 lda: usize,
514 b_ptr: u64,
515 ldb: usize,
516 beta: f64,
517 c_ptr: u64,
518 ldc: usize,
519 ) -> BackendResult<()> {
520 self.check_init()?;
521 if m == 0 || n == 0 || k == 0 {
523 return Ok(());
524 }
525
526 let trans_a_flag: u32 = u32::from(trans_a != BackendTranspose::NoTrans);
530 let trans_b_flag: u32 = u32::from(trans_b != BackendTranspose::NoTrans);
531
532 let dev = self.device()?;
533 let mem = self.memory()?;
534
535 let tile_size: u32 = 8;
536 let pipeline =
537 self.cached_pipeline("gemm", "oxicuda-gemm", || shader::gemm_wgsl(tile_size))?;
538
539 let bgl = pipeline.get_bind_group_layout(0);
540
541 let (expected_lda, expected_ldb, expected_ldc) = packed_gemm_lds(trans_a, trans_b, m, n, k);
546 if lda < expected_lda || ldb < expected_ldb || ldc < expected_ldc {
547 return Err(BackendError::InvalidArgument(
548 "gemm: leading dimension smaller than matrix extent".into(),
549 ));
550 }
551 let lda_u32 = lead_dim_u32("lda", lda)?;
552 let ldb_u32 = lead_dim_u32("ldb", ldb)?;
553 let ldc_u32 = lead_dim_u32("ldc", ldc)?;
554
555 let mut params_bytes = [0u8; 48];
558 params_bytes[0..4].copy_from_slice(&(m as u32).to_le_bytes());
559 params_bytes[4..8].copy_from_slice(&(n as u32).to_le_bytes());
560 params_bytes[8..12].copy_from_slice(&(k as u32).to_le_bytes());
561 params_bytes[12..16].copy_from_slice(&(alpha as f32).to_le_bytes());
562 params_bytes[16..20].copy_from_slice(&(beta as f32).to_le_bytes());
563 params_bytes[20..24].copy_from_slice(&trans_a_flag.to_le_bytes());
564 params_bytes[24..28].copy_from_slice(&trans_b_flag.to_le_bytes());
565 params_bytes[28..32].copy_from_slice(&lda_u32.to_le_bytes());
566 params_bytes[32..36].copy_from_slice(&ldb_u32.to_le_bytes());
567 params_bytes[36..40].copy_from_slice(&ldc_u32.to_le_bytes());
568 let uniform_buf = dev.device.create_buffer(&wgpu::BufferDescriptor {
571 label: Some("oxicuda-gemm-params"),
572 size: 48,
573 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
574 mapped_at_creation: false,
575 });
576 dev.queue.write_buffer(&uniform_buf, 0, ¶ms_bytes);
577
578 let bind_group = {
580 let buffers = mem
581 .lock_buffers()
582 .map_err(|e| BackendError::DeviceError(e.to_string()))?;
583 let a_info = buffers
584 .get(&a_ptr)
585 .ok_or_else(|| BackendError::InvalidArgument(format!("unknown handle {a_ptr}")))?;
586 let b_info = buffers
587 .get(&b_ptr)
588 .ok_or_else(|| BackendError::InvalidArgument(format!("unknown handle {b_ptr}")))?;
589 let c_info = buffers
590 .get(&c_ptr)
591 .ok_or_else(|| BackendError::InvalidArgument(format!("unknown handle {c_ptr}")))?;
592
593 dev.device.create_bind_group(&wgpu::BindGroupDescriptor {
594 label: Some("oxicuda-gemm"),
595 layout: &bgl,
596 entries: &[
597 wgpu::BindGroupEntry {
598 binding: 0,
599 resource: a_info.buffer.as_entire_binding(),
600 },
601 wgpu::BindGroupEntry {
602 binding: 1,
603 resource: b_info.buffer.as_entire_binding(),
604 },
605 wgpu::BindGroupEntry {
606 binding: 2,
607 resource: c_info.buffer.as_entire_binding(),
608 },
609 wgpu::BindGroupEntry {
610 binding: 3,
611 resource: uniform_buf.as_entire_binding(),
612 },
613 ],
614 })
615 };
616
617 let mut encoder = dev
618 .device
619 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
620 label: Some("oxicuda-gemm"),
621 });
622
623 {
624 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
625 label: Some("oxicuda-gemm"),
626 timestamp_writes: None,
627 });
628 pass.set_pipeline(&pipeline);
629 pass.set_bind_group(0, &bind_group, &[]);
630 let wg_x = (n as u32).div_ceil(tile_size);
631 let wg_y = (m as u32).div_ceil(tile_size);
632 pass.dispatch_workgroups(wg_x, wg_y, 1);
633 }
634
635 dev.queue.submit(std::iter::once(encoder.finish()));
636 let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
637
638 Ok(())
639 }
640
641 #[allow(clippy::too_many_arguments)]
642 fn batched_gemm(
643 &self,
644 trans_a: BackendTranspose,
645 trans_b: BackendTranspose,
646 m: usize,
647 n: usize,
648 k: usize,
649 alpha: f64,
650 a_ptr: u64,
651 lda: usize,
652 stride_a: usize,
653 b_ptr: u64,
654 ldb: usize,
655 stride_b: usize,
656 beta: f64,
657 c_ptr: u64,
658 ldc: usize,
659 stride_c: usize,
660 batch_count: usize,
661 ) -> BackendResult<()> {
662 self.check_init()?;
663
664 if batch_count == 0 || m == 0 || n == 0 || k == 0 {
665 return Ok(());
666 }
667
668 let trans_a_flag: u32 = u32::from(trans_a != BackendTranspose::NoTrans);
672 let trans_b_flag: u32 = u32::from(trans_b != BackendTranspose::NoTrans);
673
674 let dev = self.device()?;
675 let mem = self.memory()?;
676
677 let tile_size: u32 = 8;
678 let pipeline = self.cached_pipeline("batched_gemm", "oxicuda-batched-gemm", || {
679 shader::batched_gemm_wgsl(tile_size)
680 })?;
681
682 let bgl = pipeline.get_bind_group_layout(0);
683
684 let (expected_lda, expected_ldb, expected_ldc) = packed_gemm_lds(trans_a, trans_b, m, n, k);
687 if lda < expected_lda || ldb < expected_ldb || ldc < expected_ldc {
688 return Err(BackendError::InvalidArgument(
689 "batched_gemm: leading dimension smaller than matrix extent".into(),
690 ));
691 }
692 let lda_u32 = lead_dim_u32("lda", lda)?;
693 let ldb_u32 = lead_dim_u32("ldb", ldb)?;
694 let ldc_u32 = lead_dim_u32("ldc", ldc)?;
695
696 let mut params_bytes = [0u8; 64];
700 params_bytes[0..4].copy_from_slice(&(m as u32).to_le_bytes());
701 params_bytes[4..8].copy_from_slice(&(n as u32).to_le_bytes());
702 params_bytes[8..12].copy_from_slice(&(k as u32).to_le_bytes());
703 params_bytes[12..16].copy_from_slice(&(alpha as f32).to_le_bytes());
704 params_bytes[16..20].copy_from_slice(&(beta as f32).to_le_bytes());
705 params_bytes[20..24].copy_from_slice(&(batch_count as u32).to_le_bytes());
706 params_bytes[24..28].copy_from_slice(&(stride_a as u32).to_le_bytes());
707 params_bytes[28..32].copy_from_slice(&(stride_b as u32).to_le_bytes());
708 params_bytes[32..36].copy_from_slice(&(stride_c as u32).to_le_bytes());
709 params_bytes[36..40].copy_from_slice(&trans_a_flag.to_le_bytes());
710 params_bytes[40..44].copy_from_slice(&trans_b_flag.to_le_bytes());
711 params_bytes[44..48].copy_from_slice(&lda_u32.to_le_bytes());
712 params_bytes[48..52].copy_from_slice(&ldb_u32.to_le_bytes());
713 params_bytes[52..56].copy_from_slice(&ldc_u32.to_le_bytes());
714 let uniform_buf = dev.device.create_buffer(&wgpu::BufferDescriptor {
717 label: Some("oxicuda-batched-gemm-params"),
718 size: 64,
719 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
720 mapped_at_creation: false,
721 });
722 dev.queue.write_buffer(&uniform_buf, 0, ¶ms_bytes);
723
724 let bind_group = {
725 let buffers = mem
726 .lock_buffers()
727 .map_err(|e| BackendError::DeviceError(e.to_string()))?;
728 let a_info = buffers
729 .get(&a_ptr)
730 .ok_or_else(|| BackendError::InvalidArgument(format!("unknown handle {a_ptr}")))?;
731 let b_info = buffers
732 .get(&b_ptr)
733 .ok_or_else(|| BackendError::InvalidArgument(format!("unknown handle {b_ptr}")))?;
734 let c_info = buffers
735 .get(&c_ptr)
736 .ok_or_else(|| BackendError::InvalidArgument(format!("unknown handle {c_ptr}")))?;
737
738 dev.device.create_bind_group(&wgpu::BindGroupDescriptor {
739 label: Some("oxicuda-batched-gemm"),
740 layout: &bgl,
741 entries: &[
742 wgpu::BindGroupEntry {
743 binding: 0,
744 resource: a_info.buffer.as_entire_binding(),
745 },
746 wgpu::BindGroupEntry {
747 binding: 1,
748 resource: b_info.buffer.as_entire_binding(),
749 },
750 wgpu::BindGroupEntry {
751 binding: 2,
752 resource: c_info.buffer.as_entire_binding(),
753 },
754 wgpu::BindGroupEntry {
755 binding: 3,
756 resource: uniform_buf.as_entire_binding(),
757 },
758 ],
759 })
760 };
761
762 let mut encoder = dev
763 .device
764 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
765 label: Some("oxicuda-batched-gemm"),
766 });
767
768 {
769 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
770 label: Some("oxicuda-batched-gemm"),
771 timestamp_writes: None,
772 });
773 pass.set_pipeline(&pipeline);
774 pass.set_bind_group(0, &bind_group, &[]);
775 let wg_x = (n as u32).div_ceil(tile_size);
776 let wg_y = (m as u32).div_ceil(tile_size);
777 pass.dispatch_workgroups(wg_x, wg_y, batch_count as u32);
778 }
779
780 dev.queue.submit(std::iter::once(encoder.finish()));
781 let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
782
783 Ok(())
784 }
785
786 fn conv2d_forward(
787 &self,
788 input_ptr: u64,
789 input_shape: &[usize],
790 filter_ptr: u64,
791 filter_shape: &[usize],
792 output_ptr: u64,
793 output_shape: &[usize],
794 stride: &[usize],
795 padding: &[usize],
796 ) -> BackendResult<()> {
797 self.check_init()?;
798
799 if input_shape.len() != 4 {
800 return Err(BackendError::InvalidArgument(
801 "input_shape must have 4 elements (NCHW)".into(),
802 ));
803 }
804 if filter_shape.len() != 4 {
805 return Err(BackendError::InvalidArgument(
806 "filter_shape must have 4 elements (KCFHFW)".into(),
807 ));
808 }
809 if output_shape.len() != 4 {
810 return Err(BackendError::InvalidArgument(
811 "output_shape must have 4 elements (NKOhOw)".into(),
812 ));
813 }
814 if stride.len() != 2 {
815 return Err(BackendError::InvalidArgument(
816 "stride must have 2 elements [sh, sw]".into(),
817 ));
818 }
819 if padding.len() != 2 {
820 return Err(BackendError::InvalidArgument(
821 "padding must have 2 elements [ph, pw]".into(),
822 ));
823 }
824
825 let mem = self.memory()?;
826
827 let batch = input_shape[0];
828 let c_in = input_shape[1];
829 let h_in = input_shape[2];
830 let w_in = input_shape[3];
831 let k_out = filter_shape[0];
832 let fh = filter_shape[2];
833 let fw = filter_shape[3];
834 let oh = output_shape[2];
835 let ow = output_shape[3];
836 let sh = stride[0];
837 let sw = stride[1];
838 let ph = padding[0];
839 let pw = padding[1];
840
841 let in_elems: usize = input_shape.iter().product();
842 let f_elems: usize = filter_shape.iter().product();
843 let o_elems: usize = output_shape.iter().product();
844
845 let mut in_bytes = vec![0u8; in_elems * 4];
847 let mut f_bytes = vec![0u8; f_elems * 4];
848 mem.copy_from_device(&mut in_bytes, input_ptr)
849 .map_err(BackendError::from)?;
850 mem.copy_from_device(&mut f_bytes, filter_ptr)
851 .map_err(BackendError::from)?;
852
853 let in_f32 = bytes_to_f32_vec(&in_bytes);
854 let f_f32 = bytes_to_f32_vec(&f_bytes);
855 let mut out_f32 = vec![0.0f32; o_elems];
856
857 for b in 0..batch {
858 for kf in 0..k_out {
859 for oy in 0..oh {
860 for ox in 0..ow {
861 let mut acc = 0.0f32;
862 for ci in 0..c_in {
863 for fy in 0..fh {
864 for fx in 0..fw {
865 let iy = (oy * sh + fy) as isize - ph as isize;
866 let ix = (ox * sw + fx) as isize - pw as isize;
867 if iy >= 0
868 && (iy as usize) < h_in
869 && ix >= 0
870 && (ix as usize) < w_in
871 {
872 let in_idx = ((b * c_in + ci) * h_in + iy as usize) * w_in
873 + ix as usize;
874 let f_idx = ((kf * c_in + ci) * fh + fy) * fw + fx;
875 acc += in_f32[in_idx] * f_f32[f_idx];
876 }
877 }
878 }
879 }
880 out_f32[((b * k_out + kf) * oh + oy) * ow + ox] = acc;
881 }
882 }
883 }
884 }
885
886 let out_bytes = f32_slice_to_bytes(&out_f32);
887 mem.copy_to_device(output_ptr, &out_bytes)
888 .map_err(BackendError::from)?;
889
890 Ok(())
891 }
892
893 fn attention(
894 &self,
895 q_ptr: u64,
896 k_ptr: u64,
897 v_ptr: u64,
898 o_ptr: u64,
899 batch: usize,
900 heads: usize,
901 seq_q: usize,
902 seq_kv: usize,
903 head_dim: usize,
904 scale: f64,
905 causal: bool,
906 ) -> BackendResult<()> {
907 self.check_init()?;
908
909 if seq_q == 0 || seq_kv == 0 || head_dim == 0 {
910 return Err(BackendError::InvalidArgument(
911 "seq_q, seq_kv, and head_dim must all be > 0".into(),
912 ));
913 }
914 if scale <= 0.0 || !scale.is_finite() {
915 return Err(BackendError::InvalidArgument(format!(
916 "scale must be a positive finite number, got {scale}"
917 )));
918 }
919
920 let mem = self.memory()?;
921
922 let batch_heads = batch * heads;
923 let q_elems = batch_heads * seq_q * head_dim;
924 let kv_elems = batch_heads * seq_kv * head_dim;
925 let o_elems = q_elems;
926
927 let mut q_bytes = vec![0u8; q_elems * 4];
929 let mut k_bytes = vec![0u8; kv_elems * 4];
930 let mut v_bytes = vec![0u8; kv_elems * 4];
931
932 mem.copy_from_device(&mut q_bytes, q_ptr)
933 .map_err(BackendError::from)?;
934 mem.copy_from_device(&mut k_bytes, k_ptr)
935 .map_err(BackendError::from)?;
936 mem.copy_from_device(&mut v_bytes, v_ptr)
937 .map_err(BackendError::from)?;
938
939 let q_f32 = bytes_to_f32_vec(&q_bytes);
940 let k_f32 = bytes_to_f32_vec(&k_bytes);
941 let v_f32 = bytes_to_f32_vec(&v_bytes);
942 let mut o_f32 = vec![0.0f32; o_elems];
943
944 let scale_f32 = scale as f32;
945
946 for bh in 0..batch_heads {
947 let q_off = bh * seq_q * head_dim;
948 let k_off = bh * seq_kv * head_dim;
949 let v_off = k_off;
950
951 for sq in 0..seq_q {
952 let kv_limit = if causal { (sq + 1).min(seq_kv) } else { seq_kv };
953
954 let mut max_score = f32::NEG_INFINITY;
956 for sk in 0..kv_limit {
957 let mut dot = 0.0f32;
958 for dd in 0..head_dim {
959 dot +=
960 q_f32[q_off + sq * head_dim + dd] * k_f32[k_off + sk * head_dim + dd];
961 }
962 let s = dot * scale_f32;
963 if s > max_score {
964 max_score = s;
965 }
966 }
967
968 let mut sum_exp = 0.0f32;
970 let mut acc = vec![0.0f32; head_dim];
971 for sk in 0..kv_limit {
972 let mut dot = 0.0f32;
973 for dd in 0..head_dim {
974 dot +=
975 q_f32[q_off + sq * head_dim + dd] * k_f32[k_off + sk * head_dim + dd];
976 }
977 let w = (dot * scale_f32 - max_score).exp();
978 sum_exp += w;
979 for dd in 0..head_dim {
980 acc[dd] += w * v_f32[v_off + sk * head_dim + dd];
981 }
982 }
983
984 let o_base = q_off + sq * head_dim;
986 if sum_exp > 0.0 {
987 for dd in 0..head_dim {
988 o_f32[o_base + dd] = acc[dd] / sum_exp;
989 }
990 }
991 }
992 }
993
994 let o_bytes = f32_slice_to_bytes(&o_f32);
995 mem.copy_to_device(o_ptr, &o_bytes)
996 .map_err(BackendError::from)?;
997
998 Ok(())
999 }
1000
1001 fn reduce(
1002 &self,
1003 op: ReduceOp,
1004 input_ptr: u64,
1005 output_ptr: u64,
1006 shape: &[usize],
1007 axis: usize,
1008 ) -> BackendResult<()> {
1009 self.check_init()?;
1010
1011 if shape.is_empty() {
1012 return Err(BackendError::InvalidArgument(
1013 "shape must not be empty".into(),
1014 ));
1015 }
1016 if axis >= shape.len() {
1017 return Err(BackendError::InvalidArgument(format!(
1018 "axis {axis} is out of bounds for shape of length {}",
1019 shape.len()
1020 )));
1021 }
1022
1023 if shape.len() != 1 {
1027 return self.reduce_nd(op, input_ptr, output_ptr, shape, axis);
1028 }
1029
1030 let n_elements = shape[0];
1031 if n_elements == 0 {
1032 return Ok(());
1033 }
1034
1035 let dev = self.device()?;
1036 let mem = self.memory()?;
1037 let op_str = map_reduce_op(op);
1038
1039 let wg_count = (n_elements as u32).div_ceil(256);
1041
1042 let pass1_pipeline = self.cached_pipeline(
1043 &format!("reduce_pass1:{op_str}"),
1044 "oxicuda-reduce-pass1",
1045 || shader::reduction_wgsl(op_str),
1046 )?;
1047
1048 let partial_buf = dev.device.create_buffer(&wgpu::BufferDescriptor {
1050 label: Some("oxicuda-reduce-partial"),
1051 size: (wg_count as u64) * 4, usage: wgpu::BufferUsages::STORAGE
1053 | wgpu::BufferUsages::COPY_SRC
1054 | wgpu::BufferUsages::COPY_DST,
1055 mapped_at_creation: false,
1056 });
1057
1058 let mut p1_params = [0u8; 4];
1060 p1_params[0..4].copy_from_slice(&(n_elements as u32).to_le_bytes());
1061 let p1_uniform = dev.device.create_buffer(&wgpu::BufferDescriptor {
1062 label: Some("oxicuda-reduce-p1-params"),
1063 size: 4,
1064 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1065 mapped_at_creation: false,
1066 });
1067 dev.queue.write_buffer(&p1_uniform, 0, &p1_params);
1068
1069 let bgl1 = pass1_pipeline.get_bind_group_layout(0);
1070
1071 let bg1 = {
1072 let buffers = mem
1073 .lock_buffers()
1074 .map_err(|e| BackendError::DeviceError(e.to_string()))?;
1075 let in_info = buffers.get(&input_ptr).ok_or_else(|| {
1076 BackendError::InvalidArgument(format!("unknown handle {input_ptr}"))
1077 })?;
1078
1079 dev.device.create_bind_group(&wgpu::BindGroupDescriptor {
1080 label: Some("oxicuda-reduce-pass1"),
1081 layout: &bgl1,
1082 entries: &[
1083 wgpu::BindGroupEntry {
1084 binding: 0,
1085 resource: in_info.buffer.as_entire_binding(),
1086 },
1087 wgpu::BindGroupEntry {
1088 binding: 1,
1089 resource: partial_buf.as_entire_binding(),
1090 },
1091 wgpu::BindGroupEntry {
1092 binding: 2,
1093 resource: p1_uniform.as_entire_binding(),
1094 },
1095 ],
1096 })
1097 };
1098
1099 let pass2_pipeline = self.cached_pipeline(
1101 &format!("reduce_pass2:{op_str}"),
1102 "oxicuda-reduce-pass2",
1103 || shader::reduction_final_wgsl(op_str),
1104 )?;
1105
1106 let mut p2_params = [0u8; 4];
1108 p2_params[0..4].copy_from_slice(&wg_count.to_le_bytes());
1109 let p2_uniform = dev.device.create_buffer(&wgpu::BufferDescriptor {
1110 label: Some("oxicuda-reduce-p2-params"),
1111 size: 4,
1112 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1113 mapped_at_creation: false,
1114 });
1115 dev.queue.write_buffer(&p2_uniform, 0, &p2_params);
1116
1117 let bgl2 = pass2_pipeline.get_bind_group_layout(0);
1118
1119 let bg2 = {
1120 let buffers = mem
1121 .lock_buffers()
1122 .map_err(|e| BackendError::DeviceError(e.to_string()))?;
1123 let out_info = buffers.get(&output_ptr).ok_or_else(|| {
1124 BackendError::InvalidArgument(format!("unknown handle {output_ptr}"))
1125 })?;
1126
1127 dev.device.create_bind_group(&wgpu::BindGroupDescriptor {
1128 label: Some("oxicuda-reduce-pass2"),
1129 layout: &bgl2,
1130 entries: &[
1131 wgpu::BindGroupEntry {
1132 binding: 0,
1133 resource: partial_buf.as_entire_binding(),
1134 },
1135 wgpu::BindGroupEntry {
1136 binding: 1,
1137 resource: out_info.buffer.as_entire_binding(),
1138 },
1139 wgpu::BindGroupEntry {
1140 binding: 2,
1141 resource: p2_uniform.as_entire_binding(),
1142 },
1143 ],
1144 })
1145 };
1146
1147 let mut encoder = dev
1149 .device
1150 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1151 label: Some("oxicuda-reduce"),
1152 });
1153
1154 {
1155 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1156 label: Some("oxicuda-reduce-pass1"),
1157 timestamp_writes: None,
1158 });
1159 pass.set_pipeline(&pass1_pipeline);
1160 pass.set_bind_group(0, &bg1, &[]);
1161 pass.dispatch_workgroups(wg_count, 1, 1);
1162 }
1163 {
1164 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1165 label: Some("oxicuda-reduce-pass2"),
1166 timestamp_writes: None,
1167 });
1168 pass.set_pipeline(&pass2_pipeline);
1169 pass.set_bind_group(0, &bg2, &[]);
1170 pass.dispatch_workgroups(1, 1, 1);
1171 }
1172
1173 dev.queue.submit(std::iter::once(encoder.finish()));
1174 let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
1175
1176 if op == ReduceOp::Mean && n_elements > 1 {
1178 let mut buf = [0u8; 4];
1179 mem.copy_from_device(&mut buf, output_ptr)
1180 .map_err(BackendError::from)?;
1181 let val = f32::from_le_bytes(buf);
1182 let mean = val / (n_elements as f32);
1183 mem.copy_to_device(output_ptr, &mean.to_le_bytes())
1184 .map_err(BackendError::from)?;
1185 }
1186
1187 Ok(())
1188 }
1189
1190 fn unary(&self, op: UnaryOp, input_ptr: u64, output_ptr: u64, n: usize) -> BackendResult<()> {
1191 self.check_init()?;
1192 if n == 0 {
1193 return Ok(());
1194 }
1195
1196 let dev = self.device()?;
1197 let mem = self.memory()?;
1198
1199 let op_str = map_unary_op(op);
1200 let pipeline = self.cached_pipeline(&format!("unary:{op_str}"), "oxicuda-unary", || {
1201 shader::elementwise_wgsl(op_str)
1202 })?;
1203
1204 let bgl = pipeline.get_bind_group_layout(0);
1205
1206 let bind_group = {
1207 let buffers = mem
1208 .lock_buffers()
1209 .map_err(|e| BackendError::DeviceError(e.to_string()))?;
1210 let in_info = buffers.get(&input_ptr).ok_or_else(|| {
1211 BackendError::InvalidArgument(format!("unknown handle {input_ptr}"))
1212 })?;
1213 let out_info = buffers.get(&output_ptr).ok_or_else(|| {
1214 BackendError::InvalidArgument(format!("unknown handle {output_ptr}"))
1215 })?;
1216
1217 dev.device.create_bind_group(&wgpu::BindGroupDescriptor {
1218 label: Some("oxicuda-unary"),
1219 layout: &bgl,
1220 entries: &[
1221 wgpu::BindGroupEntry {
1222 binding: 0,
1223 resource: in_info.buffer.as_entire_binding(),
1224 },
1225 wgpu::BindGroupEntry {
1226 binding: 1,
1227 resource: out_info.buffer.as_entire_binding(),
1228 },
1229 ],
1230 })
1231 };
1232
1233 let mut encoder = dev
1234 .device
1235 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1236 label: Some("oxicuda-unary"),
1237 });
1238
1239 {
1240 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1241 label: Some("oxicuda-unary"),
1242 timestamp_writes: None,
1243 });
1244 pass.set_pipeline(&pipeline);
1245 pass.set_bind_group(0, &bind_group, &[]);
1246 let workgroups = (n as u32).div_ceil(256);
1247 pass.dispatch_workgroups(workgroups, 1, 1);
1248 }
1249
1250 dev.queue.submit(std::iter::once(encoder.finish()));
1251 let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
1252
1253 Ok(())
1254 }
1255
1256 fn binary(
1257 &self,
1258 op: BinaryOp,
1259 a_ptr: u64,
1260 b_ptr: u64,
1261 output_ptr: u64,
1262 n: usize,
1263 ) -> BackendResult<()> {
1264 self.check_init()?;
1265 if n == 0 {
1266 return Ok(());
1267 }
1268
1269 let dev = self.device()?;
1270 let mem = self.memory()?;
1271
1272 let op_str = map_binary_op(op);
1273 let pipeline =
1274 self.cached_pipeline(&format!("binary:{op_str}"), "oxicuda-binary", || {
1275 shader::binary_wgsl(op_str)
1276 })?;
1277
1278 let bgl = pipeline.get_bind_group_layout(0);
1279
1280 let bind_group = {
1281 let buffers = mem
1282 .lock_buffers()
1283 .map_err(|e| BackendError::DeviceError(e.to_string()))?;
1284 let a_info = buffers
1285 .get(&a_ptr)
1286 .ok_or_else(|| BackendError::InvalidArgument(format!("unknown handle {a_ptr}")))?;
1287 let b_info = buffers
1288 .get(&b_ptr)
1289 .ok_or_else(|| BackendError::InvalidArgument(format!("unknown handle {b_ptr}")))?;
1290 let out_info = buffers.get(&output_ptr).ok_or_else(|| {
1291 BackendError::InvalidArgument(format!("unknown handle {output_ptr}"))
1292 })?;
1293
1294 dev.device.create_bind_group(&wgpu::BindGroupDescriptor {
1295 label: Some("oxicuda-binary"),
1296 layout: &bgl,
1297 entries: &[
1298 wgpu::BindGroupEntry {
1299 binding: 0,
1300 resource: a_info.buffer.as_entire_binding(),
1301 },
1302 wgpu::BindGroupEntry {
1303 binding: 1,
1304 resource: b_info.buffer.as_entire_binding(),
1305 },
1306 wgpu::BindGroupEntry {
1307 binding: 2,
1308 resource: out_info.buffer.as_entire_binding(),
1309 },
1310 ],
1311 })
1312 };
1313
1314 let mut encoder = dev
1315 .device
1316 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1317 label: Some("oxicuda-binary"),
1318 });
1319
1320 {
1321 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1322 label: Some("oxicuda-binary"),
1323 timestamp_writes: None,
1324 });
1325 pass.set_pipeline(&pipeline);
1326 pass.set_bind_group(0, &bind_group, &[]);
1327 let workgroups = (n as u32).div_ceil(256);
1328 pass.dispatch_workgroups(workgroups, 1, 1);
1329 }
1330
1331 dev.queue.submit(std::iter::once(encoder.finish()));
1332 let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
1333
1334 Ok(())
1335 }
1336
1337 fn synchronize(&self) -> BackendResult<()> {
1340 self.check_init()?;
1341 if let Some(dev) = &self.device {
1342 let _ = dev.device.poll(wgpu::PollType::wait_indefinitely());
1343 }
1344 Ok(())
1345 }
1346
1347 fn alloc(&self, bytes: usize) -> BackendResult<u64> {
1350 self.check_init()?;
1351 if bytes == 0 {
1352 return Err(BackendError::InvalidArgument(
1353 "cannot allocate 0 bytes".into(),
1354 ));
1355 }
1356 self.memory()?.alloc(bytes).map_err(BackendError::from)
1357 }
1358
1359 fn free(&self, ptr: u64) -> BackendResult<()> {
1360 self.check_init()?;
1361 self.memory()?.free(ptr).map_err(BackendError::from)
1362 }
1363
1364 fn copy_htod(&self, dst: u64, src: &[u8]) -> BackendResult<()> {
1365 self.check_init()?;
1366 if src.is_empty() {
1367 return Ok(());
1368 }
1369 self.memory()?
1370 .copy_to_device(dst, src)
1371 .map_err(BackendError::from)
1372 }
1373
1374 fn copy_dtoh(&self, dst: &mut [u8], src: u64) -> BackendResult<()> {
1375 self.check_init()?;
1376 if dst.is_empty() {
1377 return Ok(());
1378 }
1379 self.memory()?
1380 .copy_from_device(dst, src)
1381 .map_err(BackendError::from)
1382 }
1383}
1384
1385fn bytes_to_f32_vec(bytes: &[u8]) -> Vec<f32> {
1389 bytes
1390 .chunks_exact(4)
1391 .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
1392 .collect()
1393}
1394
1395fn f32_slice_to_bytes(data: &[f32]) -> Vec<u8> {
1397 data.iter().flat_map(|v| v.to_le_bytes()).collect()
1398}
1399
1400#[cfg(test)]
1405#[path = "backend_tests.rs"]
1406mod tests;