1use std::sync::Arc;
7
8use oxicuda_backend::{
9 BackendError, BackendResult, BackendTranspose, BinaryOp, ComputeBackend, ReduceOp, UnaryOp,
10};
11
12use crate::{device::LevelZeroDevice, memory::LevelZeroMemoryManager};
13
14#[derive(Debug)]
32pub struct LevelZeroBackend {
33 device: Option<Arc<LevelZeroDevice>>,
34 memory: Option<Arc<LevelZeroMemoryManager>>,
35 initialized: bool,
36}
37
38impl LevelZeroBackend {
39 pub fn new() -> Self {
41 Self {
42 device: None,
43 memory: None,
44 initialized: false,
45 }
46 }
47
48 fn check_init(&self) -> BackendResult<()> {
50 if self.initialized {
51 Ok(())
52 } else {
53 Err(BackendError::NotInitialized)
54 }
55 }
56
57 fn memory(&self) -> BackendResult<&Arc<LevelZeroMemoryManager>> {
59 self.memory.as_ref().ok_or(BackendError::NotInitialized)
60 }
61}
62
63impl Default for LevelZeroBackend {
64 fn default() -> Self {
65 Self::new()
66 }
67}
68
69impl ComputeBackend for LevelZeroBackend {
72 fn name(&self) -> &str {
73 "level-zero"
74 }
75
76 fn init(&mut self) -> BackendResult<()> {
77 if self.initialized {
78 return Ok(());
79 }
80 match LevelZeroDevice::new() {
81 Ok(dev) => {
82 let dev = Arc::new(dev);
83 tracing::info!("Level Zero backend initialised on: {}", dev.name());
84 let memory = LevelZeroMemoryManager::new(Arc::clone(&dev));
85 self.device = Some(dev);
86 self.memory = Some(Arc::new(memory));
87 self.initialized = true;
88 Ok(())
89 }
90 Err(e) => Err(BackendError::from(e)),
91 }
92 }
93
94 fn is_initialized(&self) -> bool {
95 self.initialized
96 }
97
98 fn gemm(
101 &self,
102 trans_a: BackendTranspose,
103 trans_b: BackendTranspose,
104 m: usize,
105 n: usize,
106 k: usize,
107 alpha: f64,
108 a_ptr: u64,
109 lda: usize,
110 b_ptr: u64,
111 ldb: usize,
112 beta: f64,
113 c_ptr: u64,
114 ldc: usize,
115 ) -> BackendResult<()> {
116 self.check_init()?;
117 if m == 0 || n == 0 || k == 0 {
118 return Ok(());
119 }
120 Self::check_gemm_layout(trans_a, trans_b, n, k, lda, ldb, ldc)?;
125 self.dispatch_gemm(m, n, k, alpha as f32, a_ptr, b_ptr, beta as f32, c_ptr)
126 }
127
128 #[allow(clippy::too_many_arguments)]
129 fn batched_gemm(
130 &self,
131 trans_a: BackendTranspose,
132 trans_b: BackendTranspose,
133 m: usize,
134 n: usize,
135 k: usize,
136 alpha: f64,
137 a_ptr: u64,
138 lda: usize,
139 stride_a: usize,
140 b_ptr: u64,
141 ldb: usize,
142 stride_b: usize,
143 beta: f64,
144 c_ptr: u64,
145 ldc: usize,
146 stride_c: usize,
147 batch_count: usize,
148 ) -> BackendResult<()> {
149 self.check_init()?;
150 if batch_count == 0 || m == 0 || n == 0 || k == 0 {
151 return Ok(());
152 }
153 Self::check_gemm_layout(trans_a, trans_b, n, k, lda, ldb, ldc)?;
156 self.dispatch_batched_gemm(
157 m,
158 n,
159 k,
160 alpha as f32,
161 a_ptr,
162 b_ptr,
163 beta as f32,
164 c_ptr,
165 batch_count,
166 stride_a,
167 stride_b,
168 stride_c,
169 )
170 }
171
172 fn conv2d_forward(
173 &self,
174 input_ptr: u64,
175 input_shape: &[usize],
176 filter_ptr: u64,
177 filter_shape: &[usize],
178 output_ptr: u64,
179 output_shape: &[usize],
180 stride: &[usize],
181 padding: &[usize],
182 ) -> BackendResult<()> {
183 self.check_init()?;
184
185 if input_shape.len() != 4 {
186 return Err(BackendError::InvalidArgument(
187 "input_shape must have 4 elements (NCHW)".into(),
188 ));
189 }
190 if filter_shape.len() != 4 {
191 return Err(BackendError::InvalidArgument(
192 "filter_shape must have 4 elements (KCFHFW)".into(),
193 ));
194 }
195 if output_shape.len() != 4 {
196 return Err(BackendError::InvalidArgument(
197 "output_shape must have 4 elements (NKOhOw)".into(),
198 ));
199 }
200 if stride.len() != 2 {
201 return Err(BackendError::InvalidArgument(
202 "stride must have 2 elements [sh, sw]".into(),
203 ));
204 }
205 if padding.len() != 2 {
206 return Err(BackendError::InvalidArgument(
207 "padding must have 2 elements [ph, pw]".into(),
208 ));
209 }
210
211 let n = input_shape[0];
212 let c_in = input_shape[1];
213 let h_in = input_shape[2];
214 let w_in = input_shape[3];
215 let k_out = filter_shape[0];
216 let fh = filter_shape[2];
217 let fw = filter_shape[3];
218 let o_h = output_shape[2];
219 let o_w = output_shape[3];
220 let stride_h = stride[0];
221 let stride_w = stride[1];
222 let pad_h = padding[0];
223 let pad_w = padding[1];
224
225 let in_len = n * c_in * h_in * w_in;
227 let flt_len = k_out * c_in * fh * fw;
228 let out_len = n * k_out * o_h * o_w;
229
230 let mut in_bytes = vec![0u8; in_len * 4];
231 self.copy_dtoh(&mut in_bytes, input_ptr)?;
232 let inp: Vec<f32> = in_bytes
233 .chunks_exact(4)
234 .map(|c| f32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
235 .collect();
236
237 let mut flt_bytes = vec![0u8; flt_len * 4];
238 self.copy_dtoh(&mut flt_bytes, filter_ptr)?;
239 let flt: Vec<f32> = flt_bytes
240 .chunks_exact(4)
241 .map(|c| f32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
242 .collect();
243
244 let mut out = vec![0.0f32; out_len];
246 for b_idx in 0..n {
247 for kf in 0..k_out {
248 for oy in 0..o_h {
249 for ox in 0..o_w {
250 let mut acc = 0.0f32;
251 for ci in 0..c_in {
252 for fy in 0..fh {
253 for fx in 0..fw {
254 let iy = (oy * stride_h + fy) as isize - pad_h as isize;
255 let ix = (ox * stride_w + fx) as isize - pad_w as isize;
256 if iy >= 0
257 && (iy as usize) < h_in
258 && ix >= 0
259 && (ix as usize) < w_in
260 {
261 let iy = iy as usize;
262 let ix = ix as usize;
263 acc += inp[((b_idx * c_in + ci) * h_in + iy) * w_in + ix]
264 * flt[((kf * c_in + ci) * fh + fy) * fw + fx];
265 }
266 }
267 }
268 }
269 out[((b_idx * k_out + kf) * o_h + oy) * o_w + ox] = acc;
270 }
271 }
272 }
273 }
274
275 let out_bytes: Vec<u8> = out.iter().flat_map(|f| f.to_ne_bytes()).collect();
276 self.copy_htod(output_ptr, &out_bytes)
277 }
278
279 fn attention(
280 &self,
281 q_ptr: u64,
282 k_ptr: u64,
283 v_ptr: u64,
284 o_ptr: u64,
285 batch: usize,
286 heads: usize,
287 seq_q: usize,
288 seq_kv: usize,
289 head_dim: usize,
290 scale: f64,
291 causal: bool,
292 ) -> BackendResult<()> {
293 self.check_init()?;
294
295 if seq_q == 0 || seq_kv == 0 || head_dim == 0 {
296 return Err(BackendError::InvalidArgument(
297 "seq_q, seq_kv, and head_dim must all be > 0".into(),
298 ));
299 }
300 if scale <= 0.0 || !scale.is_finite() {
301 return Err(BackendError::InvalidArgument(format!(
302 "scale must be a positive finite number, got {scale}"
303 )));
304 }
305
306 let batch_heads = batch * heads;
307 let scale_f32 = scale as f32;
308
309 let q_len = batch_heads * seq_q * head_dim;
311 let kv_len = batch_heads * seq_kv * head_dim;
312
313 let mut q_bytes = vec![0u8; q_len * 4];
314 self.copy_dtoh(&mut q_bytes, q_ptr)?;
315 let q: Vec<f32> = q_bytes
316 .chunks_exact(4)
317 .map(|c| f32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
318 .collect();
319
320 let mut k_bytes = vec![0u8; kv_len * 4];
321 self.copy_dtoh(&mut k_bytes, k_ptr)?;
322 let k: Vec<f32> = k_bytes
323 .chunks_exact(4)
324 .map(|c| f32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
325 .collect();
326
327 let mut v_bytes = vec![0u8; kv_len * 4];
328 self.copy_dtoh(&mut v_bytes, v_ptr)?;
329 let v: Vec<f32> = v_bytes
330 .chunks_exact(4)
331 .map(|c| f32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
332 .collect();
333
334 let mut output = vec![0.0f32; q_len];
336 let mut scores = vec![0.0f32; seq_kv];
339
340 for bh in 0..batch_heads {
341 for sq in 0..seq_q {
342 let q_off = (bh * seq_q + sq) * head_dim;
343 let o_off = q_off;
344
345 let mut max_score = f32::NEG_INFINITY;
347 for (sk, score_slot) in scores.iter_mut().enumerate() {
348 if causal && sk > sq {
349 continue;
350 }
351 let k_off = (bh * seq_kv + sk) * head_dim;
352 let mut dot = 0.0f32;
353 for d in 0..head_dim {
354 dot += q[q_off + d] * k[k_off + d];
355 }
356 let score = dot * scale_f32;
357 *score_slot = score;
358 if score > max_score {
359 max_score = score;
360 }
361 }
362
363 if max_score == f32::NEG_INFINITY {
364 max_score = 0.0;
365 }
366
367 let mut sum_exp = 0.0f32;
371 for (sk, &score) in scores.iter().enumerate() {
372 if causal && sk > sq {
373 continue;
374 }
375 let v_off = (bh * seq_kv + sk) * head_dim;
376 let w = (score - max_score).exp();
377 sum_exp += w;
378 for d in 0..head_dim {
379 output[o_off + d] += w * v[v_off + d];
380 }
381 }
382
383 if sum_exp > 0.0 {
385 for d in 0..head_dim {
386 output[o_off + d] /= sum_exp;
387 }
388 }
389 }
390 }
391
392 let o_bytes: Vec<u8> = output.iter().flat_map(|f| f.to_ne_bytes()).collect();
393 self.copy_htod(o_ptr, &o_bytes)
394 }
395
396 fn reduce(
397 &self,
398 op: ReduceOp,
399 input_ptr: u64,
400 output_ptr: u64,
401 shape: &[usize],
402 axis: usize,
403 ) -> BackendResult<()> {
404 self.check_init()?;
405
406 if shape.is_empty() {
407 return Err(BackendError::InvalidArgument(
408 "shape must not be empty".into(),
409 ));
410 }
411 if axis >= shape.len() {
412 return Err(BackendError::InvalidArgument(format!(
413 "axis {axis} is out of bounds for shape of length {}",
414 shape.len()
415 )));
416 }
417
418 self.dispatch_reduce(op, input_ptr, output_ptr, shape, axis)
419 }
420
421 fn unary(&self, op: UnaryOp, input_ptr: u64, output_ptr: u64, n: usize) -> BackendResult<()> {
422 self.check_init()?;
423 if n == 0 {
424 return Ok(());
425 }
426 self.dispatch_unary(op, input_ptr, output_ptr, n)
427 }
428
429 fn binary(
430 &self,
431 op: BinaryOp,
432 a_ptr: u64,
433 b_ptr: u64,
434 output_ptr: u64,
435 n: usize,
436 ) -> BackendResult<()> {
437 self.check_init()?;
438 if n == 0 {
439 return Ok(());
440 }
441 self.dispatch_binary(op, a_ptr, b_ptr, output_ptr, n)
442 }
443
444 fn synchronize(&self) -> BackendResult<()> {
447 self.check_init()?;
448
449 #[cfg(any(target_os = "linux", target_os = "windows"))]
450 {
451 if let Some(dev) = &self.device {
452 let api = &dev.api;
453 let queue = dev.queue;
454 let rc = unsafe { (api.ze_command_queue_synchronize)(queue, u64::MAX) };
457 if rc != 0 {
458 return Err(BackendError::DeviceError(format!(
459 "zeCommandQueueSynchronize failed: 0x{rc:08x}"
460 )));
461 }
462 }
463 }
464
465 Ok(())
466 }
467
468 fn alloc(&self, bytes: usize) -> BackendResult<u64> {
471 self.check_init()?;
472 if bytes == 0 {
473 return Err(BackendError::InvalidArgument(
474 "cannot allocate 0 bytes".into(),
475 ));
476 }
477 self.memory()?.alloc(bytes).map_err(BackendError::from)
478 }
479
480 fn free(&self, ptr: u64) -> BackendResult<()> {
481 self.check_init()?;
482 self.memory()?.free(ptr).map_err(BackendError::from)
483 }
484
485 fn copy_htod(&self, dst: u64, src: &[u8]) -> BackendResult<()> {
486 self.check_init()?;
487 if src.is_empty() {
488 return Ok(());
489 }
490 self.memory()?
491 .copy_to_device(dst, src)
492 .map_err(BackendError::from)
493 }
494
495 fn copy_dtoh(&self, dst: &mut [u8], src: u64) -> BackendResult<()> {
496 self.check_init()?;
497 if dst.is_empty() {
498 return Ok(());
499 }
500 self.memory()?
501 .copy_from_device(dst, src)
502 .map_err(BackendError::from)
503 }
504}
505
506const WORKGROUP_SIZE: u32 = crate::spirv::WORKGROUP_SIZE;
510
511#[cfg_attr(not(any(target_os = "linux", target_os = "windows")), allow(dead_code))]
513enum KernelArg {
514 Buffer(u64),
516 U32(u32),
518 F32(f32),
520}
521
522impl LevelZeroBackend {
523 fn run_kernel(
531 &self,
532 spv_words: &[u32],
533 args: &[KernelArg],
534 workgroups: u32,
535 ) -> BackendResult<()> {
536 #[cfg(any(target_os = "linux", target_os = "windows"))]
537 {
538 use std::ffi::c_void;
539
540 use crate::device::{
541 ZE_MODULE_FORMAT_IL_SPIRV, ZE_STRUCTURE_TYPE_COMMAND_LIST_DESC,
542 ZE_STRUCTURE_TYPE_KERNEL_DESC, ZE_STRUCTURE_TYPE_MODULE_DESC, ZeCommandListDesc,
543 ZeGroupCount, ZeKernelDesc, ZeKernelHandle, ZeModuleDesc, ZeModuleHandle,
544 };
545
546 let device = self.device.as_ref().ok_or(BackendError::NotInitialized)?;
547 let memory = self.memory()?;
548 let api = &device.api;
549 let context = device.context;
550 let dev_handle = device.device;
551 let queue = device.queue;
552
553 let spv_bytes: Vec<u8> = spv_words.iter().flat_map(|w| w.to_ne_bytes()).collect();
555
556 let module_desc = ZeModuleDesc {
558 stype: ZE_STRUCTURE_TYPE_MODULE_DESC,
559 p_next: std::ptr::null(),
560 format: ZE_MODULE_FORMAT_IL_SPIRV,
561 input_size: spv_bytes.len(),
562 p_input_module: spv_bytes.as_ptr(),
563 p_build_flags: std::ptr::null(),
564 p_constants: std::ptr::null(),
565 };
566 let mut module: ZeModuleHandle = std::ptr::null_mut();
567 let rc = unsafe {
568 (api.ze_module_create)(
569 context,
570 dev_handle,
571 &module_desc,
572 &mut module as *mut ZeModuleHandle,
573 std::ptr::null_mut(),
574 )
575 };
576 if rc != 0 {
577 return Err(BackendError::DeviceError(format!(
578 "zeModuleCreate failed: 0x{rc:08x}"
579 )));
580 }
581
582 let kernel_name = b"main\0";
584 let kernel_desc = ZeKernelDesc {
585 stype: ZE_STRUCTURE_TYPE_KERNEL_DESC,
586 p_next: std::ptr::null(),
587 flags: 0,
588 p_kernel_name: kernel_name.as_ptr(),
589 };
590 let mut kernel: ZeKernelHandle = std::ptr::null_mut();
591 let rc = unsafe {
592 (api.ze_kernel_create)(module, &kernel_desc, &mut kernel as *mut ZeKernelHandle)
593 };
594 if rc != 0 {
595 unsafe { (api.ze_module_destroy)(module) };
596 return Err(BackendError::DeviceError(format!(
597 "zeKernelCreate failed: 0x{rc:08x}"
598 )));
599 }
600
601 let rc = unsafe { (api.ze_kernel_set_group_size)(kernel, WORKGROUP_SIZE, 1, 1) };
603 if rc != 0 {
604 unsafe {
605 (api.ze_kernel_destroy)(kernel);
606 (api.ze_module_destroy)(module);
607 }
608 return Err(BackendError::DeviceError(format!(
609 "zeKernelSetGroupSize failed: 0x{rc:08x}"
610 )));
611 }
612
613 for (idx, arg) in args.iter().enumerate() {
615 let rc = match arg {
616 KernelArg::Buffer(handle) => {
617 let dev_ptr = memory.device_ptr(*handle).map_err(|e| {
618 unsafe {
619 (api.ze_kernel_destroy)(kernel);
620 (api.ze_module_destroy)(module);
621 }
622 BackendError::from(e)
623 })?;
624 unsafe {
625 (api.ze_kernel_set_argument_value)(
626 kernel,
627 idx as u32,
628 std::mem::size_of::<*mut c_void>(),
629 &dev_ptr as *const *mut c_void as *const c_void,
630 )
631 }
632 }
633 KernelArg::U32(val) => unsafe {
634 (api.ze_kernel_set_argument_value)(
635 kernel,
636 idx as u32,
637 std::mem::size_of::<u32>(),
638 val as *const u32 as *const c_void,
639 )
640 },
641 KernelArg::F32(val) => unsafe {
642 (api.ze_kernel_set_argument_value)(
643 kernel,
644 idx as u32,
645 std::mem::size_of::<f32>(),
646 val as *const f32 as *const c_void,
647 )
648 },
649 };
650 if rc != 0 {
651 unsafe {
652 (api.ze_kernel_destroy)(kernel);
653 (api.ze_module_destroy)(module);
654 }
655 return Err(BackendError::DeviceError(format!(
656 "zeKernelSetArgumentValue(arg={idx}) failed: 0x{rc:08x}"
657 )));
658 }
659 }
660
661 let list_desc = ZeCommandListDesc {
663 stype: ZE_STRUCTURE_TYPE_COMMAND_LIST_DESC,
664 p_next: std::ptr::null(),
665 command_queue_group_ordinal: 0,
666 flags: 0,
667 };
668 let mut list = std::ptr::null_mut();
669 let rc =
670 unsafe { (api.ze_command_list_create)(context, dev_handle, &list_desc, &mut list) };
671 if rc != 0 {
672 unsafe {
673 (api.ze_kernel_destroy)(kernel);
674 (api.ze_module_destroy)(module);
675 }
676 return Err(BackendError::DeviceError(format!(
677 "zeCommandListCreate failed: 0x{rc:08x}"
678 )));
679 }
680
681 let group_count = ZeGroupCount {
683 group_count_x: workgroups,
684 group_count_y: 1,
685 group_count_z: 1,
686 };
687 let rc = unsafe {
688 (api.ze_command_list_append_launch_kernel)(
689 list,
690 kernel,
691 &group_count,
692 0,
693 0,
694 std::ptr::null(),
695 )
696 };
697 if rc != 0 {
698 unsafe {
699 (api.ze_command_list_destroy)(list);
700 (api.ze_kernel_destroy)(kernel);
701 (api.ze_module_destroy)(module);
702 }
703 return Err(BackendError::DeviceError(format!(
704 "zeCommandListAppendLaunchKernel failed: 0x{rc:08x}"
705 )));
706 }
707
708 let rc = unsafe { (api.ze_command_list_close)(list) };
710 if rc != 0 {
711 unsafe {
712 (api.ze_command_list_destroy)(list);
713 (api.ze_kernel_destroy)(kernel);
714 (api.ze_module_destroy)(module);
715 }
716 return Err(BackendError::DeviceError(format!(
717 "zeCommandListClose failed: 0x{rc:08x}"
718 )));
719 }
720
721 let rc = unsafe { (api.ze_command_queue_execute_command_lists)(queue, 1, &list, 0) };
722 if rc != 0 {
723 unsafe {
724 (api.ze_command_list_destroy)(list);
725 (api.ze_kernel_destroy)(kernel);
726 (api.ze_module_destroy)(module);
727 }
728 return Err(BackendError::DeviceError(format!(
729 "zeCommandQueueExecuteCommandLists failed: 0x{rc:08x}"
730 )));
731 }
732
733 let rc = unsafe { (api.ze_command_queue_synchronize)(queue, u64::MAX) };
734 if rc != 0 {
735 unsafe {
736 (api.ze_command_list_destroy)(list);
737 (api.ze_kernel_destroy)(kernel);
738 (api.ze_module_destroy)(module);
739 }
740 return Err(BackendError::DeviceError(format!(
741 "zeCommandQueueSynchronize failed: 0x{rc:08x}"
742 )));
743 }
744
745 unsafe {
747 (api.ze_command_list_destroy)(list);
748 (api.ze_kernel_destroy)(kernel);
749 (api.ze_module_destroy)(module);
750 }
751
752 Ok(())
753 }
754
755 #[cfg(not(any(target_os = "linux", target_os = "windows")))]
756 {
757 let _ = (spv_words, args, workgroups);
758 Err(BackendError::DeviceError(
759 "Level Zero requires Linux or Windows".into(),
760 ))
761 }
762 }
763
764 fn dispatch_unary(
765 &self,
766 op: UnaryOp,
767 input_ptr: u64,
768 output_ptr: u64,
769 n: usize,
770 ) -> BackendResult<()> {
771 let spv = crate::spirv::unary_compute_shader(op);
772 let n32 = Self::checked_u32(n, "element count")?;
773 let args = [
774 KernelArg::Buffer(input_ptr),
775 KernelArg::Buffer(output_ptr),
776 KernelArg::U32(n32),
777 ];
778 self.run_kernel(&spv, &args, n32.div_ceil(WORKGROUP_SIZE))
779 }
780
781 fn dispatch_binary(
782 &self,
783 op: BinaryOp,
784 a_ptr: u64,
785 b_ptr: u64,
786 output_ptr: u64,
787 n: usize,
788 ) -> BackendResult<()> {
789 let spv = crate::spirv::binary_compute_shader(op);
790 let n32 = Self::checked_u32(n, "element count")?;
791 let args = [
792 KernelArg::Buffer(a_ptr),
793 KernelArg::Buffer(b_ptr),
794 KernelArg::Buffer(output_ptr),
795 KernelArg::U32(n32),
796 ];
797 self.run_kernel(&spv, &args, n32.div_ceil(WORKGROUP_SIZE))
798 }
799
800 fn dispatch_reduce(
801 &self,
802 op: ReduceOp,
803 input_ptr: u64,
804 output_ptr: u64,
805 shape: &[usize],
806 axis: usize,
807 ) -> BackendResult<()> {
808 let outer_size: usize = shape[..axis].iter().product::<usize>().max(1);
809 let reduce_size = shape[axis];
810 let inner_size: usize = shape[axis + 1..].iter().product::<usize>().max(1);
811
812 let spv = crate::spirv::reduce_compute_shader(op);
813 let outer32 = Self::checked_u32(outer_size, "outer_size")?;
814 let reduce32 = Self::checked_u32(reduce_size, "reduce_size")?;
815 let inner32 = Self::checked_u32(inner_size, "inner_size")?;
816 let total_output = outer32.checked_mul(inner32).ok_or_else(|| {
817 BackendError::InvalidArgument(
818 "outer_size*inner_size exceeds u32::MAX (32-bit kernel indexing)".into(),
819 )
820 })?;
821 let args = [
822 KernelArg::Buffer(input_ptr),
823 KernelArg::Buffer(output_ptr),
824 KernelArg::U32(outer32),
825 KernelArg::U32(reduce32),
826 KernelArg::U32(inner32),
827 ];
828 self.run_kernel(&spv, &args, total_output.div_ceil(WORKGROUP_SIZE))
829 }
830
831 fn run_kernel_3d(
836 &self,
837 spv_words: &[u32],
838 args: &[KernelArg],
839 workgroups_x: u32,
840 workgroups_y: u32,
841 workgroups_z: u32,
842 ) -> BackendResult<()> {
843 #[cfg(any(target_os = "linux", target_os = "windows"))]
844 {
845 use std::ffi::c_void;
846
847 use crate::device::{
848 ZE_MODULE_FORMAT_IL_SPIRV, ZE_STRUCTURE_TYPE_COMMAND_LIST_DESC,
849 ZE_STRUCTURE_TYPE_KERNEL_DESC, ZE_STRUCTURE_TYPE_MODULE_DESC, ZeCommandListDesc,
850 ZeGroupCount, ZeKernelDesc, ZeKernelHandle, ZeModuleDesc, ZeModuleHandle,
851 };
852
853 let device = self.device.as_ref().ok_or(BackendError::NotInitialized)?;
854 let memory = self.memory()?;
855 let api = &device.api;
856 let context = device.context;
857 let dev_handle = device.device;
858 let queue = device.queue;
859
860 let spv_bytes: Vec<u8> = spv_words.iter().flat_map(|w| w.to_ne_bytes()).collect();
862
863 let module_desc = ZeModuleDesc {
865 stype: ZE_STRUCTURE_TYPE_MODULE_DESC,
866 p_next: std::ptr::null(),
867 format: ZE_MODULE_FORMAT_IL_SPIRV,
868 input_size: spv_bytes.len(),
869 p_input_module: spv_bytes.as_ptr(),
870 p_build_flags: std::ptr::null(),
871 p_constants: std::ptr::null(),
872 };
873 let mut module: ZeModuleHandle = std::ptr::null_mut();
874 let rc = unsafe {
875 (api.ze_module_create)(
876 context,
877 dev_handle,
878 &module_desc,
879 &mut module as *mut ZeModuleHandle,
880 std::ptr::null_mut(),
881 )
882 };
883 if rc != 0 {
884 return Err(BackendError::DeviceError(format!(
885 "zeModuleCreate failed: 0x{rc:08x}"
886 )));
887 }
888
889 let kernel_name = b"main\0";
891 let kernel_desc = ZeKernelDesc {
892 stype: ZE_STRUCTURE_TYPE_KERNEL_DESC,
893 p_next: std::ptr::null(),
894 flags: 0,
895 p_kernel_name: kernel_name.as_ptr(),
896 };
897 let mut kernel: ZeKernelHandle = std::ptr::null_mut();
898 let rc = unsafe {
899 (api.ze_kernel_create)(module, &kernel_desc, &mut kernel as *mut ZeKernelHandle)
900 };
901 if rc != 0 {
902 unsafe { (api.ze_module_destroy)(module) };
903 return Err(BackendError::DeviceError(format!(
904 "zeKernelCreate failed: 0x{rc:08x}"
905 )));
906 }
907
908 let rc = unsafe { (api.ze_kernel_set_group_size)(kernel, WORKGROUP_SIZE, 1, 1) };
910 if rc != 0 {
911 unsafe {
912 (api.ze_kernel_destroy)(kernel);
913 (api.ze_module_destroy)(module);
914 }
915 return Err(BackendError::DeviceError(format!(
916 "zeKernelSetGroupSize failed: 0x{rc:08x}"
917 )));
918 }
919
920 for (idx, arg) in args.iter().enumerate() {
922 let rc = match arg {
923 KernelArg::Buffer(handle) => {
924 let dev_ptr = memory.device_ptr(*handle).map_err(|e| {
925 unsafe {
926 (api.ze_kernel_destroy)(kernel);
927 (api.ze_module_destroy)(module);
928 }
929 BackendError::from(e)
930 })?;
931 unsafe {
932 (api.ze_kernel_set_argument_value)(
933 kernel,
934 idx as u32,
935 std::mem::size_of::<*mut c_void>(),
936 &dev_ptr as *const *mut c_void as *const c_void,
937 )
938 }
939 }
940 KernelArg::U32(val) => unsafe {
941 (api.ze_kernel_set_argument_value)(
942 kernel,
943 idx as u32,
944 std::mem::size_of::<u32>(),
945 val as *const u32 as *const c_void,
946 )
947 },
948 KernelArg::F32(val) => unsafe {
949 (api.ze_kernel_set_argument_value)(
950 kernel,
951 idx as u32,
952 std::mem::size_of::<f32>(),
953 val as *const f32 as *const c_void,
954 )
955 },
956 };
957 if rc != 0 {
958 unsafe {
959 (api.ze_kernel_destroy)(kernel);
960 (api.ze_module_destroy)(module);
961 }
962 return Err(BackendError::DeviceError(format!(
963 "zeKernelSetArgumentValue(arg={idx}) failed: 0x{rc:08x}"
964 )));
965 }
966 }
967
968 let list_desc = ZeCommandListDesc {
970 stype: ZE_STRUCTURE_TYPE_COMMAND_LIST_DESC,
971 p_next: std::ptr::null(),
972 command_queue_group_ordinal: 0,
973 flags: 0,
974 };
975 let mut list = std::ptr::null_mut();
976 let rc =
977 unsafe { (api.ze_command_list_create)(context, dev_handle, &list_desc, &mut list) };
978 if rc != 0 {
979 unsafe {
980 (api.ze_kernel_destroy)(kernel);
981 (api.ze_module_destroy)(module);
982 }
983 return Err(BackendError::DeviceError(format!(
984 "zeCommandListCreate failed: 0x{rc:08x}"
985 )));
986 }
987
988 let group_count = ZeGroupCount {
990 group_count_x: workgroups_x,
991 group_count_y: workgroups_y,
992 group_count_z: workgroups_z,
993 };
994 let rc = unsafe {
995 (api.ze_command_list_append_launch_kernel)(
996 list,
997 kernel,
998 &group_count,
999 0,
1000 0,
1001 std::ptr::null(),
1002 )
1003 };
1004 if rc != 0 {
1005 unsafe {
1006 (api.ze_command_list_destroy)(list);
1007 (api.ze_kernel_destroy)(kernel);
1008 (api.ze_module_destroy)(module);
1009 }
1010 return Err(BackendError::DeviceError(format!(
1011 "zeCommandListAppendLaunchKernel failed: 0x{rc:08x}"
1012 )));
1013 }
1014
1015 let rc = unsafe { (api.ze_command_list_close)(list) };
1017 if rc != 0 {
1018 unsafe {
1019 (api.ze_command_list_destroy)(list);
1020 (api.ze_kernel_destroy)(kernel);
1021 (api.ze_module_destroy)(module);
1022 }
1023 return Err(BackendError::DeviceError(format!(
1024 "zeCommandListClose failed: 0x{rc:08x}"
1025 )));
1026 }
1027
1028 let rc = unsafe { (api.ze_command_queue_execute_command_lists)(queue, 1, &list, 0) };
1029 if rc != 0 {
1030 unsafe {
1031 (api.ze_command_list_destroy)(list);
1032 (api.ze_kernel_destroy)(kernel);
1033 (api.ze_module_destroy)(module);
1034 }
1035 return Err(BackendError::DeviceError(format!(
1036 "zeCommandQueueExecuteCommandLists failed: 0x{rc:08x}"
1037 )));
1038 }
1039
1040 let rc = unsafe { (api.ze_command_queue_synchronize)(queue, u64::MAX) };
1041 if rc != 0 {
1042 unsafe {
1043 (api.ze_command_list_destroy)(list);
1044 (api.ze_kernel_destroy)(kernel);
1045 (api.ze_module_destroy)(module);
1046 }
1047 return Err(BackendError::DeviceError(format!(
1048 "zeCommandQueueSynchronize failed: 0x{rc:08x}"
1049 )));
1050 }
1051
1052 unsafe {
1054 (api.ze_command_list_destroy)(list);
1055 (api.ze_kernel_destroy)(kernel);
1056 (api.ze_module_destroy)(module);
1057 }
1058
1059 Ok(())
1060 }
1061
1062 #[cfg(not(any(target_os = "linux", target_os = "windows")))]
1063 {
1064 let _ = (spv_words, args, workgroups_x, workgroups_y, workgroups_z);
1065 Err(BackendError::DeviceError(
1066 "Level Zero requires Linux or Windows".into(),
1067 ))
1068 }
1069 }
1070
1071 #[allow(clippy::too_many_arguments)]
1072 fn dispatch_batched_gemm(
1073 &self,
1074 m: usize,
1075 n: usize,
1076 k: usize,
1077 alpha: f32,
1078 a_ptr: u64,
1079 b_ptr: u64,
1080 beta: f32,
1081 c_ptr: u64,
1082 batch_count: usize,
1083 stride_a: usize,
1084 stride_b: usize,
1085 stride_c: usize,
1086 ) -> BackendResult<()> {
1087 let spv = crate::spirv::batched_gemm_compute_shader();
1088 let m32 = Self::checked_u32(m, "m")?;
1089 let n32 = Self::checked_u32(n, "n")?;
1090 let k32 = Self::checked_u32(k, "k")?;
1091 let batch32 = Self::checked_u32(batch_count, "batch_count")?;
1092 let stride_a32 = Self::checked_u32(stride_a, "stride_a")?;
1093 let stride_b32 = Self::checked_u32(stride_b, "stride_b")?;
1094 let stride_c32 = Self::checked_u32(stride_c, "stride_c")?;
1095 let total_per_batch = m32.checked_mul(n32).ok_or_else(|| {
1096 BackendError::InvalidArgument("m*n exceeds u32::MAX (32-bit kernel indexing)".into())
1097 })?;
1098 let workgroups_x = total_per_batch.div_ceil(WORKGROUP_SIZE);
1099 let args = [
1100 KernelArg::Buffer(a_ptr),
1101 KernelArg::Buffer(b_ptr),
1102 KernelArg::Buffer(c_ptr),
1103 KernelArg::U32(m32),
1104 KernelArg::U32(n32),
1105 KernelArg::U32(k32),
1106 KernelArg::F32(alpha),
1107 KernelArg::F32(beta),
1108 KernelArg::U32(batch32),
1109 KernelArg::U32(stride_a32),
1110 KernelArg::U32(stride_b32),
1111 KernelArg::U32(stride_c32),
1112 ];
1113 self.run_kernel_3d(&spv, &args, workgroups_x, 1, batch32)
1114 }
1115
1116 #[allow(clippy::too_many_arguments)]
1117 fn dispatch_gemm(
1118 &self,
1119 m: usize,
1120 n: usize,
1121 k: usize,
1122 alpha: f32,
1123 a_ptr: u64,
1124 b_ptr: u64,
1125 beta: f32,
1126 c_ptr: u64,
1127 ) -> BackendResult<()> {
1128 let spv = crate::spirv::gemm_compute_shader();
1129 let m32 = Self::checked_u32(m, "m")?;
1130 let n32 = Self::checked_u32(n, "n")?;
1131 let k32 = Self::checked_u32(k, "k")?;
1132 let total = m32.checked_mul(n32).ok_or_else(|| {
1133 BackendError::InvalidArgument("m*n exceeds u32::MAX (32-bit kernel indexing)".into())
1134 })?;
1135 let args = [
1136 KernelArg::Buffer(a_ptr),
1137 KernelArg::Buffer(b_ptr),
1138 KernelArg::Buffer(c_ptr),
1139 KernelArg::U32(m32),
1140 KernelArg::U32(n32),
1141 KernelArg::U32(k32),
1142 KernelArg::F32(alpha),
1143 KernelArg::F32(beta),
1144 ];
1145 self.run_kernel(&spv, &args, total.div_ceil(WORKGROUP_SIZE))
1146 }
1147
1148 fn checked_u32(value: usize, what: &str) -> BackendResult<u32> {
1153 u32::try_from(value).map_err(|_| {
1154 BackendError::InvalidArgument(format!(
1155 "{what} ({value}) exceeds u32::MAX; Level Zero compute kernels use 32-bit indexing"
1156 ))
1157 })
1158 }
1159
1160 fn check_gemm_layout(
1163 trans_a: BackendTranspose,
1164 trans_b: BackendTranspose,
1165 n: usize,
1166 k: usize,
1167 lda: usize,
1168 ldb: usize,
1169 ldc: usize,
1170 ) -> BackendResult<()> {
1171 if trans_a != BackendTranspose::NoTrans || trans_b != BackendTranspose::NoTrans {
1172 return Err(BackendError::InvalidArgument(
1173 "Level Zero GEMM kernel supports only BackendTranspose::NoTrans for both operands"
1174 .into(),
1175 ));
1176 }
1177 if lda != k || ldb != n || ldc != n {
1178 return Err(BackendError::InvalidArgument(format!(
1179 "Level Zero GEMM kernel requires packed leading dimensions \
1180 (lda=k={k}, ldb=n={n}, ldc=n={n}); got lda={lda}, ldb={ldb}, ldc={ldc}"
1181 )));
1182 }
1183 Ok(())
1184 }
1185}
1186
1187#[cfg(test)]
1190mod tests {
1191 use super::*;
1192 use oxicuda_backend::{BackendTranspose, BinaryOp, ComputeBackend, ReduceOp, UnaryOp};
1193
1194 #[test]
1197 fn level_zero_backend_new_uninitialized() {
1198 let b = LevelZeroBackend::new();
1199 assert!(!b.is_initialized());
1200 }
1201
1202 #[test]
1203 fn level_zero_backend_name() {
1204 let b = LevelZeroBackend::new();
1205 assert_eq!(b.name(), "level-zero");
1206 }
1207
1208 #[test]
1209 fn level_zero_backend_default() {
1210 let b = LevelZeroBackend::default();
1211 assert!(!b.is_initialized());
1212 assert_eq!(b.name(), "level-zero");
1213 }
1214
1215 #[test]
1216 fn backend_debug_impl() {
1217 let b = LevelZeroBackend::new();
1218 let s = format!("{b:?}");
1219 assert!(s.contains("LevelZeroBackend"));
1220 }
1221
1222 #[test]
1225 fn backend_object_safe() {
1226 let b: Box<dyn ComputeBackend> = Box::new(LevelZeroBackend::new());
1227 assert_eq!(b.name(), "level-zero");
1228 }
1229
1230 #[test]
1233 fn backend_not_initialized_gemm() {
1234 let b = LevelZeroBackend::new();
1235 let result = b.gemm(
1236 BackendTranspose::NoTrans,
1237 BackendTranspose::NoTrans,
1238 4,
1239 4,
1240 4,
1241 1.0,
1242 0,
1243 4,
1244 0,
1245 4,
1246 0.0,
1247 0,
1248 4,
1249 );
1250 assert_eq!(result, Err(BackendError::NotInitialized));
1251 }
1252
1253 #[test]
1254 fn backend_not_initialized_batched_gemm() {
1255 let b = LevelZeroBackend::new();
1256 let result = b.batched_gemm(
1257 BackendTranspose::NoTrans,
1258 BackendTranspose::NoTrans,
1259 4,
1260 4,
1261 4,
1262 1.0,
1263 0,
1264 4,
1265 16,
1266 0,
1267 4,
1268 16,
1269 0.0,
1270 0,
1271 4,
1272 16,
1273 2,
1274 );
1275 assert_eq!(result, Err(BackendError::NotInitialized));
1276 }
1277
1278 #[test]
1279 fn backend_not_initialized_alloc() {
1280 let b = LevelZeroBackend::new();
1281 assert_eq!(b.alloc(1024), Err(BackendError::NotInitialized));
1282 }
1283
1284 #[test]
1285 fn backend_not_initialized_synchronize() {
1286 let b = LevelZeroBackend::new();
1287 assert_eq!(b.synchronize(), Err(BackendError::NotInitialized));
1288 }
1289
1290 #[test]
1291 fn backend_not_initialized_free() {
1292 let b = LevelZeroBackend::new();
1293 assert_eq!(b.free(1), Err(BackendError::NotInitialized));
1294 }
1295
1296 #[test]
1297 fn backend_not_initialized_copy_htod() {
1298 let b = LevelZeroBackend::new();
1299 assert_eq!(b.copy_htod(1, b"hello"), Err(BackendError::NotInitialized));
1300 }
1301
1302 #[test]
1303 fn backend_not_initialized_copy_dtoh() {
1304 let b = LevelZeroBackend::new();
1305 let mut buf = [0u8; 4];
1306 assert_eq!(b.copy_dtoh(&mut buf, 1), Err(BackendError::NotInitialized));
1307 }
1308
1309 fn try_init() -> Option<LevelZeroBackend> {
1312 let mut b = LevelZeroBackend::new();
1313 match b.init() {
1314 Ok(()) => Some(b),
1315 Err(_) => None,
1316 }
1317 }
1318
1319 #[test]
1322 fn init_graceful_failure() {
1323 let mut b = LevelZeroBackend::new();
1325 let _result = b.init();
1326 }
1328
1329 #[test]
1332 fn alloc_zero_bytes_error() {
1333 let Some(b) = try_init() else {
1334 return;
1335 };
1336 assert_eq!(
1337 b.alloc(0),
1338 Err(BackendError::InvalidArgument(
1339 "cannot allocate 0 bytes".into()
1340 ))
1341 );
1342 }
1343
1344 #[test]
1345 fn copy_htod_empty_noop() {
1346 let Some(b) = try_init() else {
1347 return;
1348 };
1349 assert_eq!(b.copy_htod(0, &[]), Ok(()));
1350 }
1351
1352 #[test]
1353 fn copy_dtoh_empty_noop() {
1354 let Some(b) = try_init() else {
1355 return;
1356 };
1357 assert_eq!(b.copy_dtoh(&mut [], 0), Ok(()));
1358 }
1359
1360 #[test]
1361 fn gemm_zero_dims_noop() {
1362 let Some(b) = try_init() else {
1363 return;
1364 };
1365 assert_eq!(
1366 b.gemm(
1367 BackendTranspose::NoTrans,
1368 BackendTranspose::NoTrans,
1369 0,
1370 0,
1371 0,
1372 1.0,
1373 0,
1374 1,
1375 0,
1376 1,
1377 0.0,
1378 0,
1379 1
1380 ),
1381 Ok(())
1382 );
1383 }
1384
1385 #[test]
1386 fn batched_gemm_zero_batch_noop() {
1387 let Some(b) = try_init() else {
1388 return;
1389 };
1390 assert_eq!(
1391 b.batched_gemm(
1392 BackendTranspose::NoTrans,
1393 BackendTranspose::NoTrans,
1394 4,
1395 4,
1396 4,
1397 1.0,
1398 0,
1399 4,
1400 16,
1401 0,
1402 4,
1403 16,
1404 0.0,
1405 0,
1406 4,
1407 16,
1408 0,
1409 ),
1410 Ok(())
1411 );
1412 }
1413
1414 #[test]
1415 fn batched_gemm_zero_dims_noop() {
1416 let Some(b) = try_init() else {
1417 return;
1418 };
1419 assert_eq!(
1420 b.batched_gemm(
1421 BackendTranspose::NoTrans,
1422 BackendTranspose::NoTrans,
1423 0,
1424 0,
1425 0,
1426 1.0,
1427 0,
1428 1,
1429 0,
1430 0,
1431 1,
1432 0,
1433 0.0,
1434 0,
1435 1,
1436 0,
1437 3,
1438 ),
1439 Ok(())
1440 );
1441 }
1442
1443 #[test]
1444 fn unary_zero_n_noop() {
1445 let Some(b) = try_init() else {
1446 return;
1447 };
1448 assert_eq!(b.unary(UnaryOp::Relu, 0, 0, 0), Ok(()));
1449 }
1450
1451 #[test]
1452 fn binary_zero_n_noop() {
1453 let Some(b) = try_init() else {
1454 return;
1455 };
1456 assert_eq!(b.binary(BinaryOp::Add, 0, 0, 0, 0), Ok(()));
1457 }
1458
1459 #[test]
1460 fn synchronize_after_init() {
1461 let Some(b) = try_init() else {
1462 return;
1463 };
1464 assert_eq!(b.synchronize(), Ok(()));
1465 }
1466
1467 #[test]
1470 fn reduce_empty_shape_error() {
1471 let Some(b) = try_init() else {
1472 return;
1473 };
1474 assert_eq!(
1475 b.reduce(ReduceOp::Sum, 0, 0, &[], 0),
1476 Err(BackendError::InvalidArgument(
1477 "shape must not be empty".into()
1478 ))
1479 );
1480 }
1481
1482 #[test]
1483 fn reduce_axis_out_of_bounds_error() {
1484 let Some(b) = try_init() else {
1485 return;
1486 };
1487 assert_eq!(
1488 b.reduce(ReduceOp::Sum, 0, 0, &[4, 4], 5),
1489 Err(BackendError::InvalidArgument(
1490 "axis 5 is out of bounds for shape of length 2".into()
1491 ))
1492 );
1493 }
1494
1495 #[test]
1496 fn attention_zero_seq_error() {
1497 let Some(b) = try_init() else {
1498 return;
1499 };
1500 assert_eq!(
1501 b.attention(0, 0, 0, 0, 1, 1, 0, 8, 64, 0.125, false),
1502 Err(BackendError::InvalidArgument(
1503 "seq_q, seq_kv, and head_dim must all be > 0".into()
1504 ))
1505 );
1506 }
1507
1508 #[test]
1509 fn attention_invalid_scale_error() {
1510 let Some(b) = try_init() else {
1511 return;
1512 };
1513 assert_eq!(
1514 b.attention(0, 0, 0, 0, 1, 1, 8, 8, 64, 0.0, false),
1515 Err(BackendError::InvalidArgument(
1516 "scale must be a positive finite number, got 0".into()
1517 ))
1518 );
1519 assert_eq!(
1520 b.attention(0, 0, 0, 0, 1, 1, 8, 8, 64, -1.0, false),
1521 Err(BackendError::InvalidArgument(
1522 "scale must be a positive finite number, got -1".into()
1523 ))
1524 );
1525 assert!(
1526 b.attention(0, 0, 0, 0, 1, 1, 8, 8, 64, f64::INFINITY, false)
1527 .is_err()
1528 );
1529 }
1530
1531 #[test]
1532 fn conv2d_wrong_input_rank() {
1533 let Some(b) = try_init() else {
1534 return;
1535 };
1536 assert_eq!(
1537 b.conv2d_forward(
1538 0,
1539 &[1, 3, 32],
1540 0,
1541 &[16, 3, 3, 3],
1542 0,
1543 &[1, 16, 30, 30],
1544 &[1, 1],
1545 &[0, 0]
1546 ),
1547 Err(BackendError::InvalidArgument(
1548 "input_shape must have 4 elements (NCHW)".into()
1549 ))
1550 );
1551 }
1552
1553 #[test]
1554 fn conv2d_wrong_filter_rank() {
1555 let Some(b) = try_init() else {
1556 return;
1557 };
1558 assert_eq!(
1559 b.conv2d_forward(
1560 0,
1561 &[1, 3, 32, 32],
1562 0,
1563 &[16, 3, 3],
1564 0,
1565 &[1, 16, 30, 30],
1566 &[1, 1],
1567 &[0, 0]
1568 ),
1569 Err(BackendError::InvalidArgument(
1570 "filter_shape must have 4 elements (KCFHFW)".into()
1571 ))
1572 );
1573 }
1574
1575 #[test]
1578 fn init_idempotent() {
1579 let Some(mut b) = try_init() else {
1580 return;
1581 };
1582 assert_eq!(b.init(), Ok(()));
1583 assert!(b.is_initialized());
1584 }
1585
1586 #[test]
1589 fn alloc_copy_roundtrip() {
1590 let Some(b) = try_init() else {
1591 return;
1592 };
1593 let src: Vec<u8> = (0u8..64).collect();
1594 let handle = match b.alloc(src.len()) {
1595 Ok(h) => h,
1596 Err(_) => return,
1597 };
1598 b.copy_htod(handle, &src).expect("copy_htod");
1599 let mut dst = vec![0u8; src.len()];
1600 b.copy_dtoh(&mut dst, handle).expect("copy_dtoh");
1601 assert_eq!(src, dst);
1602 b.free(handle).expect("free");
1603 }
1604
1605 #[test]
1608 fn double_init_is_noop() {
1609 let Some(mut b) = try_init() else {
1610 return;
1611 };
1612 let first = b.is_initialized();
1613 let _ = b.init();
1614 assert_eq!(first, b.is_initialized());
1615 }
1616
1617 #[test]
1620 fn alloc_and_free_basic() {
1621 let Some(b) = try_init() else {
1622 return;
1623 };
1624 match b.alloc(128) {
1625 Ok(handle) => {
1626 assert!(handle > 0);
1627 b.free(handle).expect("free should succeed");
1628 }
1629 Err(_) => {
1630 }
1632 }
1633 }
1634
1635 fn upload_f32(b: &LevelZeroBackend, data: &[f32]) -> Option<u64> {
1639 let bytes: Vec<u8> = data.iter().flat_map(|f| f.to_ne_bytes()).collect();
1640 let handle = b.alloc(bytes.len()).ok()?;
1641 b.copy_htod(handle, &bytes).ok()?;
1642 Some(handle)
1643 }
1644
1645 fn download_f32(b: &LevelZeroBackend, handle: u64, len: usize) -> Option<Vec<f32>> {
1647 let mut bytes = vec![0u8; len * 4];
1648 b.copy_dtoh(&mut bytes, handle).ok()?;
1649 Some(
1650 bytes
1651 .chunks_exact(4)
1652 .map(|c| f32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
1653 .collect(),
1654 )
1655 }
1656
1657 #[test]
1658 fn l0_conv2d_identity_1x1() {
1659 let Some(b) = try_init() else {
1660 return;
1661 };
1662 let input: Vec<f32> = (0..16).map(|i| i as f32).collect();
1664 let filter = vec![1.0f32];
1665 let output_len = 16;
1666
1667 let Some(in_h) = upload_f32(&b, &input) else {
1668 return;
1669 };
1670 let Some(flt_h) = upload_f32(&b, &filter) else {
1671 return;
1672 };
1673 let Some(out_h) = b.alloc(output_len * 4).ok() else {
1674 return;
1675 };
1676
1677 let result = b.conv2d_forward(
1678 in_h,
1679 &[1, 1, 4, 4],
1680 flt_h,
1681 &[1, 1, 1, 1],
1682 out_h,
1683 &[1, 1, 4, 4],
1684 &[1, 1],
1685 &[0, 0],
1686 );
1687 assert!(result.is_ok(), "conv2d_forward failed: {result:?}");
1688
1689 if let Some(out) = download_f32(&b, out_h, output_len) {
1690 for (i, &val) in out.iter().enumerate() {
1691 assert!(
1692 (val - input[i]).abs() < 1e-5,
1693 "mismatch at {i}: expected {}, got {val}",
1694 input[i]
1695 );
1696 }
1697 }
1698
1699 let _ = b.free(in_h);
1700 let _ = b.free(flt_h);
1701 let _ = b.free(out_h);
1702 }
1703
1704 #[test]
1705 fn l0_conv2d_3x3_basic() {
1706 let Some(b) = try_init() else {
1707 return;
1708 };
1709 let input: Vec<f32> = (0..16).map(|i| i as f32).collect();
1711 let filter = vec![1.0f32; 9];
1713 let output_len = 4;
1714
1715 let Some(in_h) = upload_f32(&b, &input) else {
1716 return;
1717 };
1718 let Some(flt_h) = upload_f32(&b, &filter) else {
1719 return;
1720 };
1721 let Some(out_h) = b.alloc(output_len * 4).ok() else {
1722 return;
1723 };
1724
1725 let result = b.conv2d_forward(
1726 in_h,
1727 &[1, 1, 4, 4],
1728 flt_h,
1729 &[1, 1, 3, 3],
1730 out_h,
1731 &[1, 1, 2, 2],
1732 &[1, 1],
1733 &[0, 0],
1734 );
1735 assert!(result.is_ok());
1736
1737 let expected = [45.0f32, 54.0, 81.0, 90.0];
1743 if let Some(out) = download_f32(&b, out_h, output_len) {
1744 for (i, &val) in out.iter().enumerate() {
1745 assert!(
1746 (val - expected[i]).abs() < 1e-4,
1747 "mismatch at {i}: expected {}, got {val}",
1748 expected[i]
1749 );
1750 }
1751 }
1752
1753 let _ = b.free(in_h);
1754 let _ = b.free(flt_h);
1755 let _ = b.free(out_h);
1756 }
1757
1758 #[test]
1759 fn l0_conv2d_with_padding() {
1760 let Some(b) = try_init() else {
1761 return;
1762 };
1763 let input: Vec<f32> = (1..=9).map(|i| i as f32).collect();
1765 let filter = vec![1.0f32; 9];
1766 let output_len = 9;
1767
1768 let Some(in_h) = upload_f32(&b, &input) else {
1769 return;
1770 };
1771 let Some(flt_h) = upload_f32(&b, &filter) else {
1772 return;
1773 };
1774 let Some(out_h) = b.alloc(output_len * 4).ok() else {
1775 return;
1776 };
1777
1778 let result = b.conv2d_forward(
1779 in_h,
1780 &[1, 1, 3, 3],
1781 flt_h,
1782 &[1, 1, 3, 3],
1783 out_h,
1784 &[1, 1, 3, 3],
1785 &[1, 1],
1786 &[1, 1],
1787 );
1788 assert!(result.is_ok());
1789
1790 if let Some(out) = download_f32(&b, out_h, output_len) {
1792 assert!(
1793 (out[4] - 45.0).abs() < 1e-4,
1794 "center expected 45, got {}",
1795 out[4]
1796 );
1797 assert!(
1799 (out[0] - 12.0).abs() < 1e-4,
1800 "corner expected 12, got {}",
1801 out[0]
1802 );
1803 }
1804
1805 let _ = b.free(in_h);
1806 let _ = b.free(flt_h);
1807 let _ = b.free(out_h);
1808 }
1809
1810 #[test]
1813 fn l0_attention_uniform() {
1814 let Some(b) = try_init() else {
1815 return;
1816 };
1817 let seq_q = 2;
1820 let seq_kv = 2;
1821 let head_dim = 2;
1822 let q = vec![0.0f32; seq_q * head_dim];
1823 let k = vec![0.0f32; seq_kv * head_dim];
1824 let v = vec![1.0f32, 2.0, 3.0, 4.0]; let o_len = seq_q * head_dim;
1826
1827 let Some(q_h) = upload_f32(&b, &q) else {
1828 return;
1829 };
1830 let Some(k_h) = upload_f32(&b, &k) else {
1831 return;
1832 };
1833 let Some(v_h) = upload_f32(&b, &v) else {
1834 return;
1835 };
1836 let Some(o_h) = b.alloc(o_len * 4).ok() else {
1837 return;
1838 };
1839 let zeros = vec![0u8; o_len * 4];
1841 let _ = b.copy_htod(o_h, &zeros);
1842
1843 let scale = 1.0 / (head_dim as f64).sqrt();
1844 let result = b.attention(
1845 q_h, k_h, v_h, o_h, 1, 1, seq_q, seq_kv, head_dim, scale, false,
1846 );
1847 assert!(result.is_ok(), "attention failed: {result:?}");
1848
1849 if let Some(out) = download_f32(&b, o_h, o_len) {
1852 for sq_idx in 0..seq_q {
1854 let off = sq_idx * head_dim;
1855 assert!(
1856 (out[off] - 2.0).abs() < 1e-4,
1857 "q{sq_idx}[0] expected 2.0, got {}",
1858 out[off]
1859 );
1860 assert!(
1861 (out[off + 1] - 3.0).abs() < 1e-4,
1862 "q{sq_idx}[1] expected 3.0, got {}",
1863 out[off + 1]
1864 );
1865 }
1866 }
1867
1868 let _ = b.free(q_h);
1869 let _ = b.free(k_h);
1870 let _ = b.free(v_h);
1871 let _ = b.free(o_h);
1872 }
1873
1874 #[test]
1875 fn l0_attention_causal() {
1876 let Some(b) = try_init() else {
1877 return;
1878 };
1879 let seq_q = 2;
1881 let seq_kv = 2;
1882 let head_dim = 2;
1883 let q = vec![0.0f32; seq_q * head_dim];
1884 let k = vec![0.0f32; seq_kv * head_dim];
1885 let v = vec![1.0f32, 2.0, 3.0, 4.0];
1886 let o_len = seq_q * head_dim;
1887
1888 let Some(q_h) = upload_f32(&b, &q) else {
1889 return;
1890 };
1891 let Some(k_h) = upload_f32(&b, &k) else {
1892 return;
1893 };
1894 let Some(v_h) = upload_f32(&b, &v) else {
1895 return;
1896 };
1897 let Some(o_h) = b.alloc(o_len * 4).ok() else {
1898 return;
1899 };
1900 let zeros = vec![0u8; o_len * 4];
1901 let _ = b.copy_htod(o_h, &zeros);
1902
1903 let scale = 1.0 / (head_dim as f64).sqrt();
1904 let result = b.attention(
1905 q_h, k_h, v_h, o_h, 1, 1, seq_q, seq_kv, head_dim, scale, true,
1906 );
1907 assert!(result.is_ok());
1908
1909 if let Some(out) = download_f32(&b, o_h, o_len) {
1910 assert!(
1912 (out[0] - 1.0).abs() < 1e-4,
1913 "q0[0] expected 1.0, got {}",
1914 out[0]
1915 );
1916 assert!(
1917 (out[1] - 2.0).abs() < 1e-4,
1918 "q0[1] expected 2.0, got {}",
1919 out[1]
1920 );
1921 assert!(
1923 (out[2] - 2.0).abs() < 1e-4,
1924 "q1[0] expected 2.0, got {}",
1925 out[2]
1926 );
1927 assert!(
1928 (out[3] - 3.0).abs() < 1e-4,
1929 "q1[1] expected 3.0, got {}",
1930 out[3]
1931 );
1932 }
1933
1934 let _ = b.free(q_h);
1935 let _ = b.free(k_h);
1936 let _ = b.free(v_h);
1937 let _ = b.free(o_h);
1938 }
1939
1940 #[test]
1941 fn l0_attention_dominant_key() {
1942 let Some(b) = try_init() else {
1943 return;
1944 };
1945 let seq_q = 1;
1947 let seq_kv = 3;
1948 let head_dim = 2;
1949 let q = vec![10.0f32, 0.0];
1951 let k = vec![10.0f32, 0.0, 0.0, 0.0, 0.0, 0.0];
1953 let v = vec![1.0f32, 0.0, 0.0, 1.0, 0.0, 0.0]; let o_len = seq_q * head_dim;
1955
1956 let Some(q_h) = upload_f32(&b, &q) else {
1957 return;
1958 };
1959 let Some(k_h) = upload_f32(&b, &k) else {
1960 return;
1961 };
1962 let Some(v_h) = upload_f32(&b, &v) else {
1963 return;
1964 };
1965 let Some(o_h) = b.alloc(o_len * 4).ok() else {
1966 return;
1967 };
1968 let zeros = vec![0u8; o_len * 4];
1969 let _ = b.copy_htod(o_h, &zeros);
1970
1971 let scale = 1.0;
1972 let result = b.attention(
1973 q_h, k_h, v_h, o_h, 1, 1, seq_q, seq_kv, head_dim, scale, false,
1974 );
1975 assert!(result.is_ok());
1976
1977 if let Some(out) = download_f32(&b, o_h, o_len) {
1978 assert!(out[0] > 0.99, "expected output[0] ≈ 1.0, got {}", out[0]);
1981 assert!(out[1] < 0.01, "expected output[1] ≈ 0.0, got {}", out[1]);
1982 }
1983
1984 let _ = b.free(q_h);
1985 let _ = b.free(k_h);
1986 let _ = b.free(v_h);
1987 let _ = b.free(o_h);
1988 }
1989}