Skip to main content

pictor_kernels/gpu_backend/
mod.rs

1//! GPU backend abstraction layer for CUDA and Metal acceleration.
2//!
3//! This module defines the [`GpuBackendTrait`] trait and provides:
4//! - [`CpuBackend`]: Always-available CPU implementation (baseline)
5//! - [`CudaBackend`]: CUDA stub (feature = "cuda", compile-only placeholder)
6//! - [`MetalBackend`]: Metal stub (feature = "metal", compile-only placeholder)
7//! - [`Scirs2Backend`]: **Real** GPU backend via scirs2-core (feature = "gpu")
8//!
9//! # Architecture
10//! All GPU operations follow the same pattern:
11//! 1. Allocate device buffers
12//! 2. Copy host → device
13//! 3. Execute kernel
14//! 4. Copy device → host
15//!
16//! The [`Scirs2Backend`] compiles Metal/CUDA kernels at runtime through
17//! scirs2-core and dispatches real GPU work.  Stub backends delegate to
18//! CPU operations.
19//!
20//! # Q1_0_g128 GPU acceleration
21//!
22//! The [`gpu_gemv_1bit`] function provides a high-level entry point for
23//! 1-bit quantised matrix-vector multiplication on the GPU.
24
25#[cfg(all(
26    feature = "native-cuda",
27    any(target_os = "linux", target_os = "windows")
28))]
29pub mod cuda_attn_kernels;
30#[cfg(all(
31    feature = "native-cuda",
32    any(target_os = "linux", target_os = "windows")
33))]
34pub mod cuda_fp8_kernels;
35#[cfg(all(
36    feature = "native-cuda",
37    any(target_os = "linux", target_os = "windows")
38))]
39pub mod cuda_fp8_prefill;
40#[cfg(all(
41    feature = "native-cuda",
42    any(target_os = "linux", target_os = "windows")
43))]
44pub mod cuda_fp8_prefill_kernels;
45#[cfg(all(
46    feature = "native-cuda",
47    any(target_os = "linux", target_os = "windows")
48))]
49pub mod cuda_full_layer;
50#[cfg(all(
51    feature = "native-cuda",
52    any(target_os = "linux", target_os = "windows")
53))]
54pub mod cuda_graph;
55#[cfg(all(
56    feature = "native-cuda",
57    any(target_os = "linux", target_os = "windows")
58))]
59pub mod cuda_imagen_attn_kernels;
60#[cfg(all(
61    feature = "native-cuda",
62    any(target_os = "linux", target_os = "windows")
63))]
64pub mod cuda_imagen_dit_glue_kernels;
65#[cfg(all(
66    feature = "native-cuda",
67    any(target_os = "linux", target_os = "windows")
68))]
69pub mod cuda_imagen_gemm_kernels;
70#[cfg(all(
71    feature = "native-cuda",
72    any(target_os = "linux", target_os = "windows")
73))]
74pub mod cuda_imagen_vae_kernels;
75#[cfg(all(
76    feature = "native-cuda",
77    any(target_os = "linux", target_os = "windows")
78))]
79pub mod cuda_k_quant_kernels;
80#[cfg(all(
81    feature = "native-cuda",
82    any(target_os = "linux", target_os = "windows")
83))]
84pub mod cuda_k_quant_prefill;
85#[cfg(all(
86    feature = "native-cuda",
87    any(target_os = "linux", target_os = "windows")
88))]
89pub mod cuda_k_quant_prefill_kernels;
90#[cfg(all(
91    feature = "native-cuda",
92    any(target_os = "linux", target_os = "windows")
93))]
94pub mod cuda_kernels;
95#[cfg(all(
96    feature = "native-cuda",
97    any(target_os = "linux", target_os = "windows")
98))]
99pub mod cuda_prefill;
100#[cfg(all(
101    feature = "native-cuda",
102    any(target_os = "linux", target_os = "windows")
103))]
104pub mod cuda_prefill_kernels;
105#[cfg(all(
106    feature = "native-cuda",
107    any(target_os = "linux", target_os = "windows")
108))]
109pub mod cuda_q_std_kernels;
110#[cfg(all(
111    feature = "native-cuda",
112    any(target_os = "linux", target_os = "windows")
113))]
114pub mod cuda_q_std_prefill;
115#[cfg(all(
116    feature = "native-cuda",
117    any(target_os = "linux", target_os = "windows")
118))]
119pub mod cuda_q_std_prefill_kernels;
120pub mod kernel_sources;
121#[cfg(all(feature = "metal", target_os = "macos"))]
122mod metal_dispatch;
123#[cfg(all(feature = "metal", target_os = "macos"))]
124pub mod metal_fp8_kernels;
125#[cfg(all(feature = "metal", target_os = "macos"))]
126pub mod metal_fp8_prefill;
127#[cfg(all(feature = "metal", target_os = "macos"))]
128pub mod metal_full_layer;
129#[cfg(all(feature = "metal", target_os = "macos"))]
130pub mod metal_graph;
131#[cfg(all(feature = "metal", target_os = "macos"))]
132mod metal_prefill;
133pub mod scirs2_backend;
134
135use thiserror::Error;
136#[allow(unused_imports)]
137use tracing::warn;
138
139#[cfg(feature = "gpu")]
140pub use scirs2_backend::Scirs2Backend;
141
142#[cfg(all(feature = "metal", target_os = "macos"))]
143pub use metal_fp8_kernels::{metal_gemv_fp8_e4m3, metal_gemv_fp8_e5m2};
144
145#[cfg(all(feature = "metal", target_os = "macos"))]
146pub use metal_fp8_prefill::{
147    metal_fused_gate_up_swiglu_fp8_e4m3, metal_fused_gate_up_swiglu_fp8_e5m2, metal_gemm_fp8_e4m3,
148    metal_gemm_fp8_e4m3_residual, metal_gemm_fp8_e5m2, metal_gemm_fp8_e5m2_residual,
149};
150
151#[cfg(all(feature = "metal", target_os = "macos"))]
152pub use metal_graph::{MetalGraph, MetalGraphError, MetalWeightHandle};
153
154#[cfg(all(feature = "metal", target_os = "macos"))]
155pub use metal_full_layer::{
156    build_cached_weights, build_cached_weights_ternary_only, print_gpu_profile_summary,
157    try_metal_ffn, try_metal_forward_greedy_ternary, try_metal_full_forward,
158    try_metal_full_forward_cached, try_metal_full_forward_ternary, try_metal_full_layer,
159    try_metal_prefill_ternary, try_metal_prefill_verify_ternary, try_metal_qkv, CachedLayerWeights,
160    CachedModelWeights, FullForwardLayerParams, FullForwardLayerParamsTernary,
161};
162
163#[cfg(all(feature = "metal", target_os = "macos"))]
164pub use metal_prefill::{
165    try_metal_full_forward_prefill, try_metal_full_forward_prefill_ternary,
166    try_metal_full_forward_prefill_verify, try_metal_full_forward_prefill_verify_ternary,
167};
168
169#[cfg(all(
170    feature = "native-cuda",
171    any(target_os = "linux", target_os = "windows")
172))]
173pub use cuda_graph::{
174    try_cuda_ffn, try_cuda_qkv, CudaGraph, CudaGraphError, DitSingleBlockWeights, NativeCudaBackend,
175};
176
177#[cfg(all(
178    feature = "native-cuda",
179    any(target_os = "linux", target_os = "windows")
180))]
181pub use cuda_full_layer::{
182    try_cuda_full_forward, try_cuda_full_forward_ternary,
183    try_cuda_full_forward_ternary_with_gpu_lm_head, try_cuda_full_forward_with_gpu_lm_head,
184    try_cuda_full_layer, CudaCachedLayerWeights, CudaFullForwardLayerParams,
185    CudaFullForwardLayerParamsTernary,
186};
187
188#[cfg(all(
189    feature = "native-cuda",
190    any(target_os = "linux", target_os = "windows")
191))]
192pub use cuda_prefill::{try_cuda_prefill, try_cuda_prefill_ternary};
193
194#[cfg(all(
195    feature = "native-cuda",
196    any(target_os = "linux", target_os = "windows")
197))]
198pub use cuda_fp8_kernels::{cuda_gemv_fp8_e4m3, cuda_gemv_fp8_e5m2};
199
200#[cfg(all(
201    feature = "native-cuda",
202    any(target_os = "linux", target_os = "windows")
203))]
204pub use cuda_k_quant_kernels::{
205    cuda_gemv_q2k, cuda_gemv_q3k, cuda_gemv_q4k, cuda_gemv_q5k, cuda_gemv_q6k, cuda_gemv_q8k,
206};
207#[cfg(all(
208    feature = "native-cuda",
209    any(target_os = "linux", target_os = "windows")
210))]
211pub use cuda_q_std_kernels::{cuda_gemv_q4_0, cuda_gemv_q8_0};
212
213#[cfg(all(
214    feature = "native-cuda",
215    any(target_os = "linux", target_os = "windows")
216))]
217pub use cuda_q_std_prefill::{try_cuda_prefill_q_std, CudaQStdPrefillLayerParams};
218
219#[cfg(all(
220    feature = "native-cuda",
221    any(target_os = "linux", target_os = "windows")
222))]
223pub use cuda_k_quant_prefill::{
224    try_cuda_prefill_k_quant, CudaKQuantPrefillLayerParams, KQuantFormat,
225};
226
227#[cfg(all(
228    feature = "native-cuda",
229    any(target_os = "linux", target_os = "windows")
230))]
231pub use cuda_fp8_prefill::{try_cuda_prefill_fp8, CudaFP8PrefillLayerParams};
232
233// ═══════════════════════════════════════════════════════════════════════════
234// DeviceBuffer
235// ═══════════════════════════════════════════════════════════════════════════
236
237/// Device memory buffer (opaque handle).
238///
239/// For CPU and stub backends this is simply a heap-allocated `Vec<f32>`.
240/// A future hardware backend would replace `data` with a raw device pointer
241/// and keep the `Vec` only as a host-side staging buffer.
242pub struct DeviceBuffer {
243    /// CPU backing store (used by all stub backends).
244    pub data: Vec<f32>,
245    /// Number of `f32` elements in the buffer.
246    pub size: usize,
247    /// Logical device index this buffer is associated with.
248    pub device_id: usize,
249}
250
251impl DeviceBuffer {
252    /// Allocate a zero-initialised buffer of `size` elements on `device_id`.
253    pub fn new(size: usize, device_id: usize) -> Self {
254        Self {
255            data: vec![0.0_f32; size],
256            size,
257            device_id,
258        }
259    }
260
261    /// Create a buffer pre-populated from a host slice.
262    pub fn from_slice(data: &[f32], device_id: usize) -> Self {
263        let size = data.len();
264        Self {
265            data: data.to_vec(),
266            size,
267            device_id,
268        }
269    }
270
271    /// Copy the buffer contents back to a host `Vec<f32>`.
272    pub fn to_vec(&self) -> Vec<f32> {
273        self.data.clone()
274    }
275
276    /// Number of `f32` elements stored in this buffer.
277    pub fn size(&self) -> usize {
278        self.size
279    }
280
281    /// Logical device index this buffer is bound to.
282    pub fn device_id(&self) -> usize {
283        self.device_id
284    }
285}
286
287// ═══════════════════════════════════════════════════════════════════════════
288// LaunchConfig
289// ═══════════════════════════════════════════════════════════════════════════
290
291/// Kernel launch configuration (CUDA-style grid/block decomposition).
292///
293/// For CPU and Metal backends these values are informational only; the actual
294/// parallelism strategy is determined by the backend itself.
295#[derive(Debug, Clone, Copy, PartialEq, Eq)]
296pub struct LaunchConfig {
297    /// Number of thread-blocks (x, y, z).
298    pub grid_dim: (u32, u32, u32),
299    /// Threads per block (x, y, z).
300    pub block_dim: (u32, u32, u32),
301    /// Dynamic shared memory per block in bytes.
302    pub shared_mem_bytes: u32,
303}
304
305/// Default block size used by `for_n_elements`.
306const DEFAULT_BLOCK_SIZE: u32 = 256;
307
308impl LaunchConfig {
309    /// Auto-compute a 1-D launch configuration for `n` elements.
310    ///
311    /// Uses a block size of 256 threads and rounds the grid up to cover all
312    /// elements.  `shared_mem_bytes` is set to zero.
313    pub fn for_n_elements(n: usize) -> Self {
314        let block = DEFAULT_BLOCK_SIZE;
315        let grid = ((n as u32).saturating_add(block - 1)) / block;
316        Self {
317            grid_dim: (grid.max(1), 1, 1),
318            block_dim: (block, 1, 1),
319            shared_mem_bytes: 0,
320        }
321    }
322
323    /// A sensible default 1-D config (1 block of 256 threads).
324    pub fn default_1d() -> Self {
325        Self {
326            grid_dim: (1, 1, 1),
327            block_dim: (DEFAULT_BLOCK_SIZE, 1, 1),
328            shared_mem_bytes: 0,
329        }
330    }
331}
332
333// ═══════════════════════════════════════════════════════════════════════════
334// GpuError
335// ═══════════════════════════════════════════════════════════════════════════
336
337/// Error type for GPU backend operations.
338#[derive(Debug, Error)]
339pub enum GpuError {
340    /// The requested GPU/backend is not present or not compiled in.
341    #[error("GPU not available: {0}")]
342    NotAvailable(String),
343
344    /// A device-side allocation failed due to insufficient memory.
345    #[error("out of device memory: requested {requested} bytes on device {device}")]
346    OutOfMemory {
347        /// Requested allocation size in bytes.
348        requested: usize,
349        /// Device index that was targeted.
350        device: usize,
351    },
352
353    /// A kernel could not be launched (bad dimensions, missing module, etc.).
354    #[error("kernel launch failed: {0}")]
355    KernelLaunch(String),
356
357    /// The device failed to synchronise after kernel execution.
358    #[error("device synchronization failed: {0}")]
359    SyncFailed(String),
360
361    /// A parameter value is out of range or logically inconsistent.
362    #[error("invalid argument: {0}")]
363    InvalidArgument(String),
364}
365
366// ═══════════════════════════════════════════════════════════════════════════
367// GpuBackendTrait
368// ═══════════════════════════════════════════════════════════════════════════
369
370/// Core GPU backend trait.
371///
372/// Implementations of this trait provide the primitive operations required by
373/// the Pictor inference engine.  The [`CpuBackend`] is always available
374/// and is used as a correctness baseline; hardware backends are feature-gated.
375///
376/// # Backwards compatibility
377///
378/// This trait was previously named `GpuBackend`.  The type alias
379/// [`GpuBackend`] preserves source compatibility.
380pub trait GpuBackendTrait: Send + Sync {
381    /// Human-readable backend identifier (e.g. `"cpu"`, `"cuda"`, `"metal"`).
382    fn name(&self) -> &'static str;
383
384    /// Returns `true` only when the backend is backed by real GPU hardware.
385    fn is_accelerated(&self) -> bool;
386
387    /// Number of logical devices available to this backend.
388    fn device_count(&self) -> usize;
389
390    /// Allocate an uninitialised (zero-filled for stubs) device buffer.
391    fn alloc(&self, size: usize, device_id: usize) -> Result<DeviceBuffer, GpuError>;
392
393    /// Copy a host slice to a new device buffer and return it.
394    fn host_to_device(&self, src: &[f32], device_id: usize) -> Result<DeviceBuffer, GpuError>;
395
396    /// Copy a device buffer to a new host `Vec<f32>`.
397    fn device_to_host(&self, buf: &DeviceBuffer) -> Result<Vec<f32>, GpuError>;
398
399    /// Matrix-vector multiply: **y = A · x**.
400    ///
401    /// - `a` — row-major matrix of shape `[m, k]`
402    /// - `x` — column vector of length `k`
403    /// - Returns a buffer of length `m`.
404    fn matvec(
405        &self,
406        a: &DeviceBuffer,
407        x: &DeviceBuffer,
408        m: usize,
409        k: usize,
410        device_id: usize,
411    ) -> Result<DeviceBuffer, GpuError>;
412
413    /// Element-wise ReLU: **y_i = max(0, x_i)**.
414    fn relu(&self, x: &DeviceBuffer, device_id: usize) -> Result<DeviceBuffer, GpuError>;
415
416    /// Softmax over the entire buffer (treated as a 1-D vector of `size` elements).
417    fn softmax(
418        &self,
419        x: &DeviceBuffer,
420        size: usize,
421        device_id: usize,
422    ) -> Result<DeviceBuffer, GpuError>;
423
424    /// Block until all previously submitted kernels on `device_id` have finished.
425    fn synchronize(&self, device_id: usize) -> Result<(), GpuError>;
426
427    /// Query device memory: returns `(free_bytes, total_bytes)`.
428    fn memory_info(&self, device_id: usize) -> Result<(usize, usize), GpuError>;
429
430    /// Q1_0_g128 matrix-vector product.
431    ///
432    /// Default implementation falls back to CPU dequant + scalar GEMV.
433    /// [`Scirs2Backend`] overrides this with a real GPU kernel.
434    fn gemv_q1_g128(
435        &self,
436        block_bytes: &[u8],
437        input: &[f32],
438        n_rows: usize,
439        k: usize,
440    ) -> Result<Vec<f32>, GpuError> {
441        cpu_gemv_1bit_fallback(block_bytes, input, n_rows, k)
442    }
443
444    /// Q1_0_g128 matrix-matrix product.
445    ///
446    /// Default implementation falls back to repeated [`gemv_q1_g128`](Self::gemv_q1_g128) calls.
447    fn gemm_q1_g128(
448        &self,
449        block_bytes: &[u8],
450        input: &[f32],
451        m: usize,
452        n_rows: usize,
453        k: usize,
454    ) -> Result<Vec<f32>, GpuError> {
455        let mut output = vec![0.0_f32; m * n_rows];
456        for i in 0..m {
457            let row_input = &input[i * k..(i + 1) * k];
458            let row_output = self.gemv_q1_g128(block_bytes, row_input, n_rows, k)?;
459            output[i * n_rows..(i + 1) * n_rows].copy_from_slice(&row_output);
460        }
461        Ok(output)
462    }
463
464    /// Upload weight block bytes to GPU memory and return a reusable handle.
465    ///
466    /// Default: not supported (returns `NotAvailable`).
467    fn upload_weights_raw(
468        &self,
469        _block_bytes: &[u8],
470    ) -> Result<crate::weight_cache::GpuWeightHandle, GpuError> {
471        Err(GpuError::NotAvailable(
472            "weight caching not supported by this backend".into(),
473        ))
474    }
475
476    /// Q1_0_g128 GEMV using a pre-uploaded GPU-resident weight buffer.
477    ///
478    /// Default: not supported (returns `NotAvailable`).
479    fn gemv_q1_g128_cached(
480        &self,
481        _handle: crate::weight_cache::GpuWeightHandle,
482        _input: &[f32],
483        _n_rows: usize,
484        _k: usize,
485    ) -> Result<Vec<f32>, GpuError> {
486        Err(GpuError::NotAvailable(
487            "cached GEMV not supported by this backend".into(),
488        ))
489    }
490
491    /// Upload TQ2_0_g128 weight blocks to GPU memory in SoA layout.
492    ///
493    /// Default: not supported (returns `NotAvailable`).
494    fn upload_weights_ternary(
495        &self,
496        _blocks: &[pictor_core::BlockTQ2_0_g128],
497    ) -> Result<crate::weight_cache::GpuWeightHandle, GpuError> {
498        Err(GpuError::NotAvailable(
499            "ternary weight upload not supported by this backend".into(),
500        ))
501    }
502
503    /// TQ2_0_g128 GEMV using a pre-uploaded GPU-resident weight buffer.
504    ///
505    /// Default: not supported (returns `NotAvailable`).
506    fn gemv_tq2_g128_cached(
507        &self,
508        _handle: crate::weight_cache::GpuWeightHandle,
509        _input: &[f32],
510        _n_rows: usize,
511        _k: usize,
512    ) -> Result<Vec<f32>, GpuError> {
513        Err(GpuError::NotAvailable(
514            "cached ternary GEMV not supported by this backend".into(),
515        ))
516    }
517
518    /// Batch-execute attention input phase (RMSNorm + QKV) in one command buffer.
519    ///
520    /// Returns `Ok(Some((q, k, v)))` if batching succeeded, or `Ok(None)` if
521    /// not supported by this backend.
522    #[allow(clippy::too_many_arguments, clippy::type_complexity)]
523    fn batch_attn_phase(
524        &self,
525        _hidden: &[f32],
526        _norm_weight: &[f32],
527        _norm_eps: f32,
528        _qkv_handle: crate::weight_cache::GpuWeightHandle,
529        _q_rows: usize,
530        _k_rows: usize,
531        _h: usize,
532    ) -> Result<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>, GpuError> {
533        Ok(None)
534    }
535
536    /// Batch-execute FFN phase in one command buffer.
537    ///
538    /// Returns `Ok(true)` if batching succeeded and `hidden` was modified
539    /// in-place, or `Ok(false)` if not supported by this backend.
540    #[allow(clippy::too_many_arguments)]
541    fn batch_ffn_phase(
542        &self,
543        _hidden: &mut [f32],
544        _attn_out: &[f32],
545        _norm_weight: &[f32],
546        _norm_eps: f32,
547        _attn_proj_handle: crate::weight_cache::GpuWeightHandle,
548        _gate_up_handle: crate::weight_cache::GpuWeightHandle,
549        _down_handle: crate::weight_cache::GpuWeightHandle,
550        _h: usize,
551        _intermediate: usize,
552        _attn_proj_k: usize,
553    ) -> Result<bool, GpuError> {
554        Ok(false)
555    }
556}
557
558/// Backwards-compatible type alias for the GPU backend trait.
559///
560/// Existing code that references `GpuBackend` as a trait will continue to
561/// compile.
562pub type GpuBackend = dyn GpuBackendTrait;
563
564// ═══════════════════════════════════════════════════════════════════════════
565// CpuBackend
566// ═══════════════════════════════════════════════════════════════════════════
567
568/// CPU backend — always available, no GPU required.
569///
570/// Implements [`GpuBackendTrait`] using plain scalar Rust operations.
571pub struct CpuBackend {
572    /// Simulated total device memory reported by `memory_info`.
573    pub simulated_memory_bytes: usize,
574}
575
576impl CpuBackend {
577    /// Create a `CpuBackend` with a default simulated memory of 4 GiB.
578    pub fn new() -> Self {
579        Self {
580            simulated_memory_bytes: 4 * 1024 * 1024 * 1024,
581        }
582    }
583
584    /// Create a `CpuBackend` with a custom simulated memory size (bytes).
585    pub fn with_memory(bytes: usize) -> Self {
586        Self {
587            simulated_memory_bytes: bytes,
588        }
589    }
590}
591
592impl Default for CpuBackend {
593    fn default() -> Self {
594        Self::new()
595    }
596}
597
598impl GpuBackendTrait for CpuBackend {
599    fn name(&self) -> &'static str {
600        "cpu"
601    }
602
603    fn is_accelerated(&self) -> bool {
604        false
605    }
606
607    fn device_count(&self) -> usize {
608        1
609    }
610
611    fn alloc(&self, size: usize, device_id: usize) -> Result<DeviceBuffer, GpuError> {
612        Ok(DeviceBuffer::new(size, device_id))
613    }
614
615    fn host_to_device(&self, src: &[f32], device_id: usize) -> Result<DeviceBuffer, GpuError> {
616        Ok(DeviceBuffer::from_slice(src, device_id))
617    }
618
619    fn device_to_host(&self, buf: &DeviceBuffer) -> Result<Vec<f32>, GpuError> {
620        Ok(buf.to_vec())
621    }
622
623    fn matvec(
624        &self,
625        a: &DeviceBuffer,
626        x: &DeviceBuffer,
627        m: usize,
628        k: usize,
629        device_id: usize,
630    ) -> Result<DeviceBuffer, GpuError> {
631        if a.size() != m * k {
632            return Err(GpuError::InvalidArgument(format!(
633                "matrix buffer size {} does not match m={} k={}",
634                a.size(),
635                m,
636                k
637            )));
638        }
639        if x.size() != k {
640            return Err(GpuError::InvalidArgument(format!(
641                "vector buffer size {} does not match k={}",
642                x.size(),
643                k
644            )));
645        }
646
647        let mut result = vec![0.0_f32; m];
648        for (row, slot) in result.iter_mut().enumerate().take(m) {
649            let mut acc = 0.0_f32;
650            for col in 0..k {
651                acc += a.data[row * k + col] * x.data[col];
652            }
653            *slot = acc;
654        }
655
656        Ok(DeviceBuffer::from_slice(&result, device_id))
657    }
658
659    fn relu(&self, x: &DeviceBuffer, device_id: usize) -> Result<DeviceBuffer, GpuError> {
660        let result: Vec<f32> = x.data.iter().map(|&v| v.max(0.0)).collect();
661        Ok(DeviceBuffer::from_slice(&result, device_id))
662    }
663
664    fn softmax(
665        &self,
666        x: &DeviceBuffer,
667        size: usize,
668        device_id: usize,
669    ) -> Result<DeviceBuffer, GpuError> {
670        if x.size() != size {
671            return Err(GpuError::InvalidArgument(format!(
672                "buffer size {} does not match size={}",
673                x.size(),
674                size
675            )));
676        }
677        if size == 0 {
678            return Ok(DeviceBuffer::new(0, device_id));
679        }
680
681        let max_val = x.data.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
682        let exps: Vec<f32> = x.data.iter().map(|&v| (v - max_val).exp()).collect();
683        let sum: f32 = exps.iter().sum();
684
685        let result: Vec<f32> = if sum == 0.0 {
686            vec![1.0 / size as f32; size]
687        } else {
688            exps.iter().map(|&e| e / sum).collect()
689        };
690
691        Ok(DeviceBuffer::from_slice(&result, device_id))
692    }
693
694    fn synchronize(&self, _device_id: usize) -> Result<(), GpuError> {
695        Ok(())
696    }
697
698    fn memory_info(&self, _device_id: usize) -> Result<(usize, usize), GpuError> {
699        let total = self.simulated_memory_bytes;
700        let free = total / 2;
701        Ok((free, total))
702    }
703}
704
705// ═══════════════════════════════════════════════════════════════════════════
706// CudaBackend (stub, feature = "cuda")
707// ═══════════════════════════════════════════════════════════════════════════
708
709/// CUDA backend stub — feature-gated, compile-only placeholder.
710///
711/// All operations delegate to `CpuBackend` and emit a `warn!` trace event.
712/// Use [`Scirs2Backend`] for real GPU acceleration.
713#[cfg(feature = "cuda")]
714pub struct CudaBackend {
715    /// Number of CUDA devices detected at construction time.
716    pub device_count: usize,
717    cpu_fallback: CpuBackend,
718}
719
720#[cfg(feature = "cuda")]
721impl CudaBackend {
722    /// Attempt to initialise the CUDA backend (stub).
723    pub fn new() -> Result<Self, GpuError> {
724        warn!("CudaBackend: CUDA stub active — no real GPU acceleration");
725        Ok(Self {
726            device_count: 1,
727            cpu_fallback: CpuBackend::new(),
728        })
729    }
730}
731
732#[cfg(feature = "cuda")]
733impl GpuBackendTrait for CudaBackend {
734    fn name(&self) -> &'static str {
735        "cuda"
736    }
737
738    fn is_accelerated(&self) -> bool {
739        false
740    }
741
742    fn device_count(&self) -> usize {
743        self.device_count
744    }
745
746    fn alloc(&self, size: usize, device_id: usize) -> Result<DeviceBuffer, GpuError> {
747        warn!("CudaBackend::alloc delegating to CPU fallback");
748        self.cpu_fallback.alloc(size, device_id)
749    }
750
751    fn host_to_device(&self, src: &[f32], device_id: usize) -> Result<DeviceBuffer, GpuError> {
752        warn!("CudaBackend::host_to_device delegating to CPU fallback");
753        self.cpu_fallback.host_to_device(src, device_id)
754    }
755
756    fn device_to_host(&self, buf: &DeviceBuffer) -> Result<Vec<f32>, GpuError> {
757        warn!("CudaBackend::device_to_host delegating to CPU fallback");
758        self.cpu_fallback.device_to_host(buf)
759    }
760
761    fn matvec(
762        &self,
763        a: &DeviceBuffer,
764        x: &DeviceBuffer,
765        m: usize,
766        k: usize,
767        device_id: usize,
768    ) -> Result<DeviceBuffer, GpuError> {
769        warn!("CudaBackend::matvec delegating to CPU fallback");
770        self.cpu_fallback.matvec(a, x, m, k, device_id)
771    }
772
773    fn relu(&self, x: &DeviceBuffer, device_id: usize) -> Result<DeviceBuffer, GpuError> {
774        warn!("CudaBackend::relu delegating to CPU fallback");
775        self.cpu_fallback.relu(x, device_id)
776    }
777
778    fn softmax(
779        &self,
780        x: &DeviceBuffer,
781        size: usize,
782        device_id: usize,
783    ) -> Result<DeviceBuffer, GpuError> {
784        warn!("CudaBackend::softmax delegating to CPU fallback");
785        self.cpu_fallback.softmax(x, size, device_id)
786    }
787
788    fn synchronize(&self, device_id: usize) -> Result<(), GpuError> {
789        warn!("CudaBackend::synchronize delegating to CPU fallback");
790        self.cpu_fallback.synchronize(device_id)
791    }
792
793    fn memory_info(&self, device_id: usize) -> Result<(usize, usize), GpuError> {
794        warn!("CudaBackend::memory_info delegating to CPU fallback");
795        self.cpu_fallback.memory_info(device_id)
796    }
797}
798
799// ═══════════════════════════════════════════════════════════════════════════
800// MetalBackend (stub, feature = "metal", macOS only)
801// ═══════════════════════════════════════════════════════════════════════════
802
803/// Metal backend stub — feature-gated, macOS only, compile-only placeholder.
804///
805/// Use [`Scirs2Backend`] for real GPU acceleration.
806#[cfg(all(feature = "metal", target_os = "macos"))]
807pub struct MetalBackend {
808    /// Number of Metal devices detected at construction time.
809    pub device_count: usize,
810    cpu_fallback: CpuBackend,
811}
812
813#[cfg(all(feature = "metal", target_os = "macos"))]
814impl MetalBackend {
815    /// Attempt to initialise the Metal backend (stub).
816    pub fn new() -> Result<Self, GpuError> {
817        warn!("MetalBackend: Metal stub active — no real GPU acceleration");
818        Ok(Self {
819            device_count: 1,
820            cpu_fallback: CpuBackend::new(),
821        })
822    }
823}
824
825#[cfg(all(feature = "metal", target_os = "macos"))]
826impl GpuBackendTrait for MetalBackend {
827    fn name(&self) -> &'static str {
828        "metal"
829    }
830
831    fn is_accelerated(&self) -> bool {
832        false
833    }
834
835    fn device_count(&self) -> usize {
836        self.device_count
837    }
838
839    fn alloc(&self, size: usize, device_id: usize) -> Result<DeviceBuffer, GpuError> {
840        warn!("MetalBackend::alloc delegating to CPU fallback");
841        self.cpu_fallback.alloc(size, device_id)
842    }
843
844    fn host_to_device(&self, src: &[f32], device_id: usize) -> Result<DeviceBuffer, GpuError> {
845        warn!("MetalBackend::host_to_device delegating to CPU fallback");
846        self.cpu_fallback.host_to_device(src, device_id)
847    }
848
849    fn device_to_host(&self, buf: &DeviceBuffer) -> Result<Vec<f32>, GpuError> {
850        warn!("MetalBackend::device_to_host delegating to CPU fallback");
851        self.cpu_fallback.device_to_host(buf)
852    }
853
854    fn matvec(
855        &self,
856        a: &DeviceBuffer,
857        x: &DeviceBuffer,
858        m: usize,
859        k: usize,
860        device_id: usize,
861    ) -> Result<DeviceBuffer, GpuError> {
862        warn!("MetalBackend::matvec delegating to CPU fallback");
863        self.cpu_fallback.matvec(a, x, m, k, device_id)
864    }
865
866    fn relu(&self, x: &DeviceBuffer, device_id: usize) -> Result<DeviceBuffer, GpuError> {
867        warn!("MetalBackend::relu delegating to CPU fallback");
868        self.cpu_fallback.relu(x, device_id)
869    }
870
871    fn softmax(
872        &self,
873        x: &DeviceBuffer,
874        size: usize,
875        device_id: usize,
876    ) -> Result<DeviceBuffer, GpuError> {
877        warn!("MetalBackend::softmax delegating to CPU fallback");
878        self.cpu_fallback.softmax(x, size, device_id)
879    }
880
881    fn synchronize(&self, device_id: usize) -> Result<(), GpuError> {
882        warn!("MetalBackend::synchronize delegating to CPU fallback");
883        self.cpu_fallback.synchronize(device_id)
884    }
885
886    fn memory_info(&self, device_id: usize) -> Result<(usize, usize), GpuError> {
887        warn!("MetalBackend::memory_info delegating to CPU fallback");
888        self.cpu_fallback.memory_info(device_id)
889    }
890}
891
892// ═══════════════════════════════════════════════════════════════════════════
893// Scirs2BackendHandle (singleton wrapper)
894// ═══════════════════════════════════════════════════════════════════════════
895
896/// Thin wrapper around `Arc<Scirs2Backend>` that implements [`GpuBackendTrait`].
897///
898/// This allows the process-wide singleton to be used wherever a
899/// `Box<dyn GpuBackendTrait>` is expected (e.g. [`select_backend`]).
900#[cfg(feature = "gpu")]
901pub(crate) struct Scirs2BackendHandle(pub(crate) std::sync::Arc<Scirs2Backend>);
902
903#[cfg(feature = "gpu")]
904impl GpuBackendTrait for Scirs2BackendHandle {
905    fn name(&self) -> &'static str {
906        self.0.name()
907    }
908    fn is_accelerated(&self) -> bool {
909        self.0.is_accelerated()
910    }
911    fn device_count(&self) -> usize {
912        self.0.device_count()
913    }
914    fn alloc(&self, size: usize, device_id: usize) -> Result<DeviceBuffer, GpuError> {
915        self.0.alloc(size, device_id)
916    }
917    fn host_to_device(&self, src: &[f32], device_id: usize) -> Result<DeviceBuffer, GpuError> {
918        self.0.host_to_device(src, device_id)
919    }
920    fn device_to_host(&self, buf: &DeviceBuffer) -> Result<Vec<f32>, GpuError> {
921        self.0.device_to_host(buf)
922    }
923    fn matvec(
924        &self,
925        a: &DeviceBuffer,
926        x: &DeviceBuffer,
927        m: usize,
928        k: usize,
929        device_id: usize,
930    ) -> Result<DeviceBuffer, GpuError> {
931        self.0.matvec(a, x, m, k, device_id)
932    }
933    fn relu(&self, x: &DeviceBuffer, device_id: usize) -> Result<DeviceBuffer, GpuError> {
934        self.0.relu(x, device_id)
935    }
936    fn softmax(
937        &self,
938        x: &DeviceBuffer,
939        size: usize,
940        device_id: usize,
941    ) -> Result<DeviceBuffer, GpuError> {
942        self.0.softmax(x, size, device_id)
943    }
944    fn synchronize(&self, device_id: usize) -> Result<(), GpuError> {
945        self.0.synchronize(device_id)
946    }
947    fn memory_info(&self, device_id: usize) -> Result<(usize, usize), GpuError> {
948        self.0.memory_info(device_id)
949    }
950    fn gemv_q1_g128(
951        &self,
952        block_bytes: &[u8],
953        input: &[f32],
954        n_rows: usize,
955        k: usize,
956    ) -> Result<Vec<f32>, GpuError> {
957        self.0.gemv_q1_g128(block_bytes, input, n_rows, k)
958    }
959    fn gemm_q1_g128(
960        &self,
961        block_bytes: &[u8],
962        input: &[f32],
963        m: usize,
964        n_rows: usize,
965        k: usize,
966    ) -> Result<Vec<f32>, GpuError> {
967        self.0.gemm_q1_g128(block_bytes, input, m, n_rows, k)
968    }
969    fn upload_weights_raw(
970        &self,
971        block_bytes: &[u8],
972    ) -> Result<crate::weight_cache::GpuWeightHandle, GpuError> {
973        self.0.upload_weights(block_bytes)
974    }
975    fn gemv_q1_g128_cached(
976        &self,
977        handle: crate::weight_cache::GpuWeightHandle,
978        input: &[f32],
979        n_rows: usize,
980        k: usize,
981    ) -> Result<Vec<f32>, GpuError> {
982        self.0.gemv_q1_g128_cached(handle, input, n_rows, k)
983    }
984
985    fn upload_weights_ternary(
986        &self,
987        blocks: &[pictor_core::BlockTQ2_0_g128],
988    ) -> Result<crate::weight_cache::GpuWeightHandle, GpuError> {
989        self.0.upload_weights_ternary(blocks)
990    }
991
992    fn gemv_tq2_g128_cached(
993        &self,
994        handle: crate::weight_cache::GpuWeightHandle,
995        input: &[f32],
996        n_rows: usize,
997        k: usize,
998    ) -> Result<Vec<f32>, GpuError> {
999        self.0.gemv_tq2_g128_cached(handle, input, n_rows, k)
1000    }
1001
1002    fn batch_attn_phase(
1003        &self,
1004        hidden: &[f32],
1005        norm_weight: &[f32],
1006        norm_eps: f32,
1007        qkv_handle: crate::weight_cache::GpuWeightHandle,
1008        q_rows: usize,
1009        k_rows: usize,
1010        h: usize,
1011    ) -> Result<Option<(Vec<f32>, Vec<f32>, Vec<f32>)>, GpuError> {
1012        match self
1013            .0
1014            .batch_attn_phase(hidden, norm_weight, norm_eps, qkv_handle, q_rows, k_rows, h)
1015        {
1016            Ok(result) => Ok(Some(result)),
1017            Err(e) => {
1018                tracing::warn!(error = %e, "batch_attn_phase failed, falling back");
1019                Ok(None)
1020            }
1021        }
1022    }
1023
1024    fn batch_ffn_phase(
1025        &self,
1026        hidden: &mut [f32],
1027        attn_out: &[f32],
1028        norm_weight: &[f32],
1029        norm_eps: f32,
1030        attn_proj_handle: crate::weight_cache::GpuWeightHandle,
1031        gate_up_handle: crate::weight_cache::GpuWeightHandle,
1032        down_handle: crate::weight_cache::GpuWeightHandle,
1033        h: usize,
1034        intermediate: usize,
1035        attn_proj_k: usize,
1036    ) -> Result<bool, GpuError> {
1037        match self.0.batch_ffn_phase(
1038            hidden,
1039            attn_out,
1040            norm_weight,
1041            norm_eps,
1042            attn_proj_handle,
1043            gate_up_handle,
1044            down_handle,
1045            h,
1046            intermediate,
1047            attn_proj_k,
1048        ) {
1049            Ok(()) => Ok(true),
1050            Err(e) => {
1051                tracing::warn!(error = %e, "batch_ffn_phase failed, falling back");
1052                Ok(false)
1053            }
1054        }
1055    }
1056}
1057
1058// ═══════════════════════════════════════════════════════════════════════════
1059// select_backend
1060// ═══════════════════════════════════════════════════════════════════════════
1061
1062/// Select the best available backend automatically.
1063///
1064/// Priority order (highest to lowest):
1065/// 1. [`Scirs2Backend`] (feature = "gpu") — Metal-accelerated via scirs2-core
1066/// 2. `NativeCudaBackend` (feature = "native-cuda") — direct cudarc CUDA
1067/// 3. CUDA stub (feature = "cuda", no "native-cuda") — falls back to CPU
1068/// 4. Metal stub (feature = "metal", macOS only) — falls back to CPU
1069/// 5. [`CpuBackend`] (always available)
1070///
1071/// If initialisation fails at any level the function falls through to the
1072/// next option, ultimately always returning a functional `CpuBackend`.
1073pub fn select_backend() -> Box<dyn GpuBackendTrait> {
1074    // `select_backend` may be called several times in a process (model load,
1075    // engine init, tests). The "scirs2 not accelerated" / "init failed" warnings
1076    // are properties of the host environment, not of any individual call site,
1077    // so emit each variant at most once per process.
1078    #[cfg(feature = "gpu")]
1079    use std::sync::atomic::{AtomicBool, Ordering};
1080    #[cfg(feature = "gpu")]
1081    fn warn_once(flag: &AtomicBool, msg: impl FnOnce()) {
1082        if !flag.swap(true, Ordering::Relaxed) {
1083            msg();
1084        }
1085    }
1086
1087    // ── 1. Try scirs2-core GPU backend (Metal on macOS) ─────────────────
1088    #[cfg(feature = "gpu")]
1089    {
1090        static SCIRS2_NOT_ACCEL: AtomicBool = AtomicBool::new(false);
1091        static SCIRS2_INIT_FAIL: AtomicBool = AtomicBool::new(false);
1092        match Scirs2Backend::global() {
1093            Ok(b) => {
1094                if b.is_accelerated() {
1095                    return Box::new(Scirs2BackendHandle(b));
1096                }
1097                // scirs2-core returned a CPU context; skip and try stubs.
1098                warn_once(&SCIRS2_NOT_ACCEL, || {
1099                    warn!(
1100                        "select_backend: Scirs2Backend is not accelerated (backend={}), trying next",
1101                        b.backend_name()
1102                    );
1103                });
1104            }
1105            Err(e) => {
1106                warn_once(&SCIRS2_INIT_FAIL, || {
1107                    warn!("select_backend: Scirs2Backend init failed ({e}), trying next");
1108                });
1109            }
1110        }
1111    }
1112
1113    // ── 2. Native CUDA backend (direct cudarc, Linux/Windows) ───────────
1114    #[cfg(all(
1115        feature = "native-cuda",
1116        any(target_os = "linux", target_os = "windows")
1117    ))]
1118    {
1119        match NativeCudaBackend::new() {
1120            Ok(b) => {
1121                tracing::info!("select_backend: NativeCudaBackend initialised");
1122                return Box::new(b);
1123            }
1124            Err(e) => {
1125                warn!("select_backend: NativeCudaBackend init failed ({e}), trying next");
1126            }
1127        }
1128    }
1129
1130    // ── 3. CUDA stub ─────────────────────────────────────────────────────
1131    #[cfg(feature = "cuda")]
1132    {
1133        match CudaBackend::new() {
1134            Ok(b) => {
1135                return Box::new(b);
1136            }
1137            Err(e) => {
1138                warn!("select_backend: CUDA init failed ({e}), trying next");
1139            }
1140        }
1141    }
1142
1143    // ── 3. Metal stub ───────────────────────────────────────────────────
1144    #[cfg(all(feature = "metal", target_os = "macos"))]
1145    {
1146        match MetalBackend::new() {
1147            Ok(b) => {
1148                return Box::new(b);
1149            }
1150            Err(e) => {
1151                warn!("select_backend: Metal init failed ({e}), trying CPU");
1152            }
1153        }
1154    }
1155
1156    // ── 4. CPU fallback ─────────────────────────────────────────────────
1157    Box::new(CpuBackend::new())
1158}
1159
1160// ═══════════════════════════════════════════════════════════════════════════
1161// gpu_matmul utility
1162// ═══════════════════════════════════════════════════════════════════════════
1163
1164/// Perform a general matrix multiplication **C = A · B** using a GPU backend.
1165///
1166/// - `a` — row-major `[m, k]` matrix (length `m * k`)
1167/// - `b` — row-major `[k, n]` matrix (length `k * n`)
1168/// - Returns a row-major `[m, n]` matrix (length `m * n`)
1169///
1170/// This is implemented as `n` calls to `backend.matvec` (one per column of B)
1171/// and is provided as a convenience for callers that do not wish to manage
1172/// `DeviceBuffer` objects directly.
1173pub fn gpu_matmul(
1174    backend: &dyn GpuBackendTrait,
1175    a: &[f32],
1176    b: &[f32],
1177    m: usize,
1178    k: usize,
1179    n: usize,
1180    device_id: usize,
1181) -> Result<Vec<f32>, GpuError> {
1182    if a.len() != m * k {
1183        return Err(GpuError::InvalidArgument(format!(
1184            "a.len()={} does not match m={} k={}",
1185            a.len(),
1186            m,
1187            k
1188        )));
1189    }
1190    if b.len() != k * n {
1191        return Err(GpuError::InvalidArgument(format!(
1192            "b.len()={} does not match k={} n={}",
1193            b.len(),
1194            k,
1195            n
1196        )));
1197    }
1198
1199    let a_buf = backend.host_to_device(a, device_id)?;
1200
1201    let mut c = vec![0.0_f32; m * n];
1202
1203    for col in 0..n {
1204        let b_col: Vec<f32> = (0..k).map(|row| b[row * n + col]).collect();
1205        let x_buf = backend.host_to_device(&b_col, device_id)?;
1206        let y_buf = backend.matvec(&a_buf, &x_buf, m, k, device_id)?;
1207        let y = backend.device_to_host(&y_buf)?;
1208
1209        for row in 0..m {
1210            c[row * n + col] = y[row];
1211        }
1212    }
1213
1214    backend.synchronize(device_id)?;
1215    Ok(c)
1216}
1217
1218// ═══════════════════════════════════════════════════════════════════════════
1219// gpu_gemv_1bit — high-level Q1_0_g128 GPU GEMV
1220// ═══════════════════════════════════════════════════════════════════════════
1221
1222/// Perform Q1_0_g128 matrix-vector multiply on the GPU.
1223///
1224/// This is the primary entry point for callers that have raw
1225/// `BlockQ1_0G128` data and want GPU-accelerated inference.
1226///
1227/// # Arguments
1228/// - `block_bytes` — `&[u8]` raw bytes of `BlockQ1_0G128[]` (18 bytes each)
1229/// - `input` — `&[f32]` of length `k`
1230/// - `n_rows` — number of weight matrix rows
1231/// - `k` — input dimension (must be a multiple of 128)
1232///
1233/// # Returns
1234/// `Vec<f32>` of length `n_rows`, or falls back to CPU dequant+GEMV if GPU
1235/// is not available.
1236///
1237/// # Feature gates
1238/// Requires the `gpu` feature.  Without it, this function is still available
1239/// but always uses the CPU fallback path.
1240pub fn gpu_gemv_1bit(
1241    block_bytes: &[u8],
1242    input: &[f32],
1243    n_rows: usize,
1244    k: usize,
1245) -> Result<Vec<f32>, GpuError> {
1246    #[cfg(feature = "gpu")]
1247    {
1248        match Scirs2Backend::global() {
1249            Ok(backend) => {
1250                if backend.is_accelerated() {
1251                    return backend.gemv_q1_g128(block_bytes, input, n_rows, k);
1252                }
1253                // Non-accelerated (CPU) scirs2 context — use our own CPU path.
1254            }
1255            Err(e) => {
1256                warn!("gpu_gemv_1bit: GPU init failed ({e}), using CPU fallback");
1257            }
1258        }
1259    }
1260
1261    // CPU fallback: dequant + scalar GEMV.
1262    cpu_gemv_1bit_fallback(block_bytes, input, n_rows, k)
1263}
1264
1265/// CPU fallback for Q1_0_g128 GEMV.
1266///
1267/// Dequantises blocks inline and computes the dot-product per row.
1268fn cpu_gemv_1bit_fallback(
1269    block_bytes: &[u8],
1270    input: &[f32],
1271    n_rows: usize,
1272    k: usize,
1273) -> Result<Vec<f32>, GpuError> {
1274    if k == 0 || k % 128 != 0 {
1275        return Err(GpuError::InvalidArgument(format!(
1276            "k={k} must be a positive multiple of 128"
1277        )));
1278    }
1279    if input.len() != k {
1280        return Err(GpuError::InvalidArgument(format!(
1281            "input.len()={} != k={}",
1282            input.len(),
1283            k
1284        )));
1285    }
1286    let blocks_per_row = k / 128;
1287    let block_size = 18_usize;
1288    let expected = n_rows * blocks_per_row * block_size;
1289    if block_bytes.len() < expected {
1290        return Err(GpuError::InvalidArgument(format!(
1291            "block_bytes too small: {} < {}",
1292            block_bytes.len(),
1293            expected,
1294        )));
1295    }
1296
1297    let mut output = vec![0.0_f32; n_rows];
1298
1299    for (row, output_val) in output.iter_mut().enumerate().take(n_rows) {
1300        let mut sum = 0.0_f32;
1301        for b in 0..blocks_per_row {
1302            let block_idx = row * blocks_per_row + b;
1303            let off = block_idx * block_size;
1304
1305            // Read FP16 scale factor (little-endian).
1306            let d_bits = u16::from_le_bytes([block_bytes[off], block_bytes[off + 1]]);
1307            let scale = half::f16::from_bits(d_bits).to_f32();
1308
1309            let input_base = b * 128;
1310            // Process 4 × u32 = 128 bits.
1311            for w in 0..4_usize {
1312                let byte_off = off + 2 + w * 4;
1313                let bits = u32::from_le_bytes([
1314                    block_bytes[byte_off],
1315                    block_bytes[byte_off + 1],
1316                    block_bytes[byte_off + 2],
1317                    block_bytes[byte_off + 3],
1318                ]);
1319                let base = input_base + w * 32;
1320                for i in 0..32_usize {
1321                    let sign = if (bits >> i) & 1 == 1 {
1322                        1.0_f32
1323                    } else {
1324                        -1.0_f32
1325                    };
1326                    sum += scale * sign * input[base + i];
1327                }
1328            }
1329        }
1330        *output_val = sum;
1331    }
1332
1333    Ok(output)
1334}
1335
1336// ═══════════════════════════════════════════════════════════════════════════
1337// Unit tests
1338// ═══════════════════════════════════════════════════════════════════════════
1339
1340#[cfg(test)]
1341mod tests {
1342    use super::*;
1343
1344    #[test]
1345    fn device_buffer_new_zeroed() {
1346        let buf = DeviceBuffer::new(4, 0);
1347        assert_eq!(buf.size(), 4);
1348        assert_eq!(buf.device_id(), 0);
1349        assert!(buf.data.iter().all(|&v| v == 0.0));
1350    }
1351
1352    #[test]
1353    fn device_buffer_from_slice_roundtrip() {
1354        let src = [1.0_f32, 2.0, 3.0];
1355        let buf = DeviceBuffer::from_slice(&src, 1);
1356        assert_eq!(buf.to_vec(), src);
1357    }
1358
1359    #[test]
1360    fn launch_config_for_zero_elements() {
1361        let cfg = LaunchConfig::for_n_elements(0);
1362        assert_eq!(cfg.grid_dim.0, 1);
1363    }
1364
1365    #[test]
1366    fn cpu_softmax_empty() {
1367        let backend = CpuBackend::new();
1368        let buf = DeviceBuffer::new(0, 0);
1369        let out = backend.softmax(&buf, 0, 0).expect("softmax empty");
1370        assert_eq!(out.size(), 0);
1371    }
1372
1373    // ── CPU fallback GEMV tests ─────────────────────────────────────────
1374
1375    #[test]
1376    fn cpu_gemv_1bit_identity_scale() {
1377        // 1 row, k=128, all bits set (weight = +1), scale = 1.0
1378        let scale = half::f16::from_f32(1.0);
1379        let scale_bytes = scale.to_bits().to_le_bytes();
1380
1381        let mut block = vec![0u8; 18];
1382        block[0] = scale_bytes[0];
1383        block[1] = scale_bytes[1];
1384        // Set all 128 bits to 1 → all weights = +scale = +1
1385        block[2..18].fill(0xFF);
1386
1387        let input: Vec<f32> = (0..128).map(|i| i as f32).collect();
1388        let expected: f32 = input.iter().sum(); // sum(0..128) = 8128
1389
1390        let result =
1391            cpu_gemv_1bit_fallback(&block, &input, 1, 128).expect("cpu_gemv_1bit_fallback");
1392        assert!(
1393            (result[0] - expected).abs() < 1e-2,
1394            "got {} expected {}",
1395            result[0],
1396            expected,
1397        );
1398    }
1399
1400    #[test]
1401    fn cpu_gemv_1bit_negative_scale() {
1402        // All bits 0 → weight = -scale.  With scale=1.0 and input=1.0:
1403        // output = -1 * 128 * 1.0 = -128
1404        let scale = half::f16::from_f32(1.0);
1405        let scale_bytes = scale.to_bits().to_le_bytes();
1406
1407        let mut block = vec![0u8; 18];
1408        block[0] = scale_bytes[0];
1409        block[1] = scale_bytes[1];
1410        // qs all zero → weight = -scale
1411
1412        let input = vec![1.0_f32; 128];
1413        let result =
1414            cpu_gemv_1bit_fallback(&block, &input, 1, 128).expect("cpu_gemv_1bit_fallback");
1415        assert!(
1416            (result[0] - (-128.0)).abs() < 1e-2,
1417            "got {} expected -128",
1418            result[0],
1419        );
1420    }
1421
1422    #[test]
1423    fn cpu_gemv_1bit_bad_k() {
1424        let result = cpu_gemv_1bit_fallback(&[], &[], 0, 64);
1425        assert!(result.is_err());
1426    }
1427
1428    #[test]
1429    fn gpu_gemv_1bit_without_gpu() {
1430        // gpu_gemv_1bit should fall back to CPU.
1431        let scale = half::f16::from_f32(1.0);
1432        let scale_bytes = scale.to_bits().to_le_bytes();
1433
1434        let mut block = vec![0u8; 18];
1435        block[0] = scale_bytes[0];
1436        block[1] = scale_bytes[1];
1437        block[2..18].fill(0xFF);
1438
1439        let input: Vec<f32> = vec![1.0_f32; 128];
1440        let result = gpu_gemv_1bit(&block, &input, 1, 128).expect("gpu_gemv_1bit");
1441        assert!((result[0] - 128.0).abs() < 1e-2, "got {}", result[0]);
1442    }
1443}