Skip to main content

oxicuda_levelzero/
backend.rs

1//! [`LevelZeroBackend`] — the main entry point for the oxicuda-levelzero crate.
2//!
3//! Implements the [`ComputeBackend`] trait from `oxicuda-backend` using
4//! Intel's Level Zero API for GPU compute on Linux and Windows.
5
6use 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// ─── Backend struct ───────────────────────────────────────────────────────────
15
16/// Intel Level Zero GPU compute backend.
17///
18/// On Linux and Windows this selects the first Intel GPU via the Level Zero
19/// loader library (`libze_loader.so` / `ze_loader.dll`) and allocates device
20/// memory through the Level Zero memory model.
21///
22/// On macOS every operation returns [`BackendError::DeviceError`] wrapping
23/// [`crate::error::LevelZeroError::UnsupportedPlatform`].
24///
25/// # Lifecycle
26///
27/// 1. `LevelZeroBackend::new()` — create an uninitialised backend.
28/// 2. `init()` — load the Level Zero driver and select a GPU.
29/// 3. Use `alloc`, `copy_htod`, compute ops, `copy_dtoh`, `free`.
30/// 4. `synchronize()` — wait for all pending GPU work to finish.
31#[derive(Debug)]
32pub struct LevelZeroBackend {
33    device: Option<Arc<LevelZeroDevice>>,
34    memory: Option<Arc<LevelZeroMemoryManager>>,
35    initialized: bool,
36}
37
38impl LevelZeroBackend {
39    /// Create a new, uninitialised Level Zero backend.
40    pub fn new() -> Self {
41        Self {
42            device: None,
43            memory: None,
44            initialized: false,
45        }
46    }
47
48    /// Return an error if the backend has not been initialised yet.
49    fn check_init(&self) -> BackendResult<()> {
50        if self.initialized {
51            Ok(())
52        } else {
53            Err(BackendError::NotInitialized)
54        }
55    }
56
57    /// Convenience accessor: get the memory manager or return `NotInitialized`.
58    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
69// ─── ComputeBackend impl ──────────────────────────────────────────────────────
70
71impl 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    // ── Compute operations ────────────────────────────────────────────────────
99
100    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        // The GEMM SPIR-V kernel hard-codes a packed, non-transposed layout
121        // (A: m×k lda=k, B: k×n ldb=n, C: m×n ldc=n). Reject any transpose mode
122        // or non-packed leading dimension with a loud error rather than
123        // silently computing the wrong product.
124        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        // Same packed-layout constraint as `gemm`; the batched kernel reuses the
154        // identical per-matrix indexing.
155        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        // CPU fallback: copy input + filter from device
226        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        // NCHW convolution
245        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        // CPU fallback: copy Q, K, V from device
310        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        // Numerically-stable scaled dot-product attention
335        let mut output = vec![0.0f32; q_len];
336        // Per-key scores computed in pass 1 and reused in pass 2, avoiding a
337        // redundant recomputation of the Q·K dot product for every (query,key).
338        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                // Pass 1: compute + cache each score, tracking the max.
346                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                // Pass 2: accumulate exp-weighted V, reusing the cached scores.
368                // The causal skip predicate is identical to pass 1, so only
369                // scores written above are ever read here.
370                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                // Normalize
384                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    // ── Synchronisation ───────────────────────────────────────────────────────
445
446    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                // SAFETY: `queue` is a valid command queue handle and the
455                // backend is initialized.  u64::MAX means "wait indefinitely".
456                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    // ── Memory management ─────────────────────────────────────────────────────
469
470    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
506// ─── Dispatch helpers ────────────────────────────────────────────────────────
507
508/// Workgroup size matching the SPIR-V LocalSize declaration.
509const WORKGROUP_SIZE: u32 = crate::spirv::WORKGROUP_SIZE;
510
511/// A kernel argument value for the Level Zero dispatch pipeline.
512#[cfg_attr(not(any(target_os = "linux", target_os = "windows")), allow(dead_code))]
513enum KernelArg {
514    /// Buffer handle — resolved to a raw device pointer at dispatch time.
515    Buffer(u64),
516    /// 32-bit unsigned integer scalar.
517    U32(u32),
518    /// 32-bit float scalar.
519    F32(f32),
520}
521
522impl LevelZeroBackend {
523    /// Dispatch a SPIR-V compute kernel via Level Zero.
524    ///
525    /// 1. Build a Level Zero module from `spv_words`.
526    /// 2. Create a kernel named `"main"` from the module.
527    /// 3. Set group size and kernel arguments.
528    /// 4. Append a launch to a command list, execute, and wait.
529    /// 5. Clean up all Level Zero objects.
530    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            // ── 1. SPIR-V words → bytes ──
554            let spv_bytes: Vec<u8> = spv_words.iter().flat_map(|w| w.to_ne_bytes()).collect();
555
556            // ── 2. Create module ──
557            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            // ── 3. Create kernel ──
583            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            // ── 4. Set group size ──
602            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            // ── 5. Set kernel arguments ──
614            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            // ── 6. Create command list ──
662            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            // ── 7. Append launch kernel ──
682            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            // ── 8. Close + execute + wait ──
709            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            // ── 9. Clean up ──
746            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    /// Dispatch a SPIR-V compute kernel with a 3D work group count.
832    ///
833    /// Like [`run_kernel`](Self::run_kernel) but supports 3D dispatch via
834    /// `(group_count_x, group_count_y, group_count_z)`.
835    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            // ── 1. SPIR-V words → bytes ──
861            let spv_bytes: Vec<u8> = spv_words.iter().flat_map(|w| w.to_ne_bytes()).collect();
862
863            // ── 2. Create module ──
864            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            // ── 3. Create kernel ──
890            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            // ── 4. Set group size ──
909            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            // ── 5. Set kernel arguments ──
921            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            // ── 6. Create command list ──
969            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            // ── 7. Append launch kernel (3D) ──
989            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            // ── 8. Close + execute + wait ──
1016            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            // ── 9. Clean up ──
1053            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    /// Narrow a `usize` dimension/count/stride to the `u32` the SPIR-V compute
1149    /// kernels use for indexing and workgroup counts, returning a loud
1150    /// [`BackendError::InvalidArgument`] rather than silently truncating (which
1151    /// would leave part of the buffer unprocessed).
1152    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    /// Validate that the GEMM kernel's packed, non-transposed layout assumption
1161    /// holds; the kernel indexes `A[row*k+i]`, `B[i*n+col]`, `C[row*n+col]`.
1162    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// ─── Tests ───────────────────────────────────────────────────────────────────
1188
1189#[cfg(test)]
1190mod tests {
1191    use super::*;
1192    use oxicuda_backend::{BackendTranspose, BinaryOp, ComputeBackend, ReduceOp, UnaryOp};
1193
1194    // ── Construction ──────────────────────────────────────────────────────────
1195
1196    #[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    // ── Object-safety smoke test ──────────────────────────────────────────────
1223
1224    #[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    // ── Not-initialized guards ────────────────────────────────────────────────
1231
1232    #[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    // ── Helper: try to get an initialised backend (skip if no GPU or no loader) ─
1310
1311    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    // ── Graceful init failure ─────────────────────────────────────────────────
1320
1321    #[test]
1322    fn init_graceful_failure() {
1323        // Verify that init() returns a Result and never panics.
1324        let mut b = LevelZeroBackend::new();
1325        let _result = b.init();
1326        // Ok or Err — both are acceptable.
1327    }
1328
1329    // ── Zero-size / trivial-OK paths (post-init) ──────────────────────────────
1330
1331    #[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    // ── Argument validation (post-init) ───────────────────────────────────────
1468
1469    #[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    // ── Init is idempotent ────────────────────────────────────────────────────
1576
1577    #[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    // ── alloc/free/copy roundtrip ─────────────────────────────────────────────
1587
1588    #[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    // ── Double init is a no-op ────────────────────────────────────────────────
1606
1607    #[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    // ── alloc and free basic ──────────────────────────────────────────────────
1618
1619    #[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                // Allocation failure is acceptable in environments without GPU.
1631            }
1632        }
1633    }
1634
1635    // ── Conv2D correctness tests ──────────────────────────────────────────────
1636
1637    /// Helper: allocate device memory, store f32 data, return handle.
1638    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    /// Helper: download f32 data from device.
1646    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        // 1x1x4x4 input, 1x1x1x1 filter = identity (filter=[1.0])
1663        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        // 1x1x4x4 input, 1x1x3x3 filter, stride=1, pad=0 → 1x1x2x2 output
1710        let input: Vec<f32> = (0..16).map(|i| i as f32).collect();
1711        // All-ones 3x3 filter: output[oy,ox] = sum of 3x3 window
1712        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        // Expected:
1738        // out[0,0] = 0+1+2+4+5+6+8+9+10 = 45
1739        // out[0,1] = 1+2+3+5+6+7+9+10+11 = 54
1740        // out[1,0] = 4+5+6+8+9+10+12+13+14 = 81
1741        // out[1,1] = 5+6+7+9+10+11+13+14+15 = 90
1742        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        // 1x1x3x3 input, 1x1x3x3 filter (all ones), stride=1, pad=1 → 1x1x3x3 output
1764        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        // Center element: sum of all 9 = 45
1791        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            // Corner [0,0]: sum of [1,2,4,5] = 12
1798            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    // ── Attention correctness tests ───────────────────────────────────────────
1811
1812    #[test]
1813    fn l0_attention_uniform() {
1814        let Some(b) = try_init() else {
1815            return;
1816        };
1817        // batch=1, heads=1, seq_q=2, seq_kv=2, head_dim=2
1818        // Q=K=all zeros → uniform attention → O = mean(V)
1819        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]; // V[0]=[1,2], V[1]=[3,4]
1825        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        // Zero out output
1840        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        // With uniform attention weights, output = mean(V rows)
1850        // mean = [(1+3)/2, (2+4)/2] = [2, 3]
1851        if let Some(out) = download_f32(&b, o_h, o_len) {
1852            // Both query positions should get the same result
1853            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        // batch=1, heads=1, seq_q=2, seq_kv=2, head_dim=2, causal=true
1880        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            // q=0 (causal: can only attend to k=0): output = V[0] = [1, 2]
1911            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            // q=1 (can attend to k=0,1): output = mean(V) = [2, 3]
1922            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        // One key has a very large dot product → attention should be concentrated on it
1946        let seq_q = 1;
1947        let seq_kv = 3;
1948        let head_dim = 2;
1949        // Q = [10, 0]
1950        let q = vec![10.0f32, 0.0];
1951        // K = [[10, 0], [0, 0], [0, 0]]  → dot(Q,K[0]) = 100, others = 0
1952        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]; // V[0]=[1,0], V[1]=[0,1], V[2]=[0,0]
1954        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            // With dot=100*scale=100 for K[0] vs 0 for others,
1979            // softmax should heavily favour V[0]=[1,0]
1980            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}