Skip to main content

myelin_accelerator/
gpu_stub.rs

1// Copyright 2026 Raul Montoya Cardenas
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4pub use crate::capability::{
5    Backend, CapabilityFacts, CapabilityReport, ComputeCapability, ExecutionPolicy, FallbackReason,
6    FallbackRecord, KernelAvailability,
7};
8use crate::capability::{apply_failure_to_facts, evaluate_capabilities};
9pub use crate::error::{GpuError, GpuResult};
10
11pub(crate) fn host_facts() -> CapabilityFacts {
12    CapabilityFacts::not_built()
13}
14
15#[derive(Debug)]
16pub struct GpuContext;
17impl GpuContext {
18    pub fn init() -> GpuResult<Self> {
19        Err(GpuError::NoGpu)
20    }
21    pub fn is_available() -> bool {
22        false
23    }
24
25    /// Device 0 compute capability; unavailable in a CPU-only build.
26    pub fn compute_capability(&self) -> Option<ComputeCapability> {
27        None
28    }
29}
30
31#[derive(Debug)]
32pub struct KernelModule;
33#[derive(Debug)]
34pub struct Function;
35impl KernelModule {
36    pub fn load() -> GpuResult<Self> {
37        Err(GpuError::NoGpu)
38    }
39    pub fn load_satsolver() -> GpuResult<Self> {
40        Err(GpuError::NoGpu)
41    }
42    pub fn get_function(&self, _: &str) -> GpuResult<Function> {
43        Err(GpuError::NoGpu)
44    }
45}
46
47pub struct GpuBuffer<T> {
48    data: Vec<T>,
49}
50impl<T: Default + Clone> GpuBuffer<T> {
51    /// Allocate a buffer of `len` elements, initialised to `T::default()`.
52    ///
53    /// The buffer is readable immediately: [`Self::to_vec`] after `alloc`
54    /// returns `len` copies of `T::default()` without any prior upload.
55    /// `alloc(0)` succeeds and yields an empty, readable buffer. This mirrors
56    /// the CUDA backend's initialisation contract so callers behave identically
57    /// on either path.
58    ///
59    /// Oversized requests fail with [`GpuError::MemoryError`] instead of
60    /// panicking: the byte size is checked with the same
61    /// `size_of::<T>() * len` arithmetic and `isize::MAX` ceiling as the CUDA
62    /// backend, and the backing `Vec` is reserved fallibly (GH #48).
63    pub fn alloc(len: usize) -> GpuResult<Self> {
64        crate::error::checked_alloc_bytes::<T>(len)?;
65        let mut data: Vec<T> = Vec::new();
66        data.try_reserve_exact(len)
67            .map_err(|e| GpuError::MemoryError(format!("alloc({len}): reserve failed: {e:?}")))?;
68        data.resize(len, T::default());
69        Ok(Self { data })
70    }
71    /// Allocate a buffer holding a copy of `data`. `from_slice(&[])` yields an
72    /// empty buffer.
73    pub fn from_slice(data: &[T]) -> GpuResult<Self> {
74        Ok(Self {
75            data: data.to_vec(),
76        })
77    }
78    /// Copy the buffer's current contents into a fresh `Vec<T>`.
79    pub fn to_vec(&self) -> GpuResult<Vec<T>> {
80        Ok(self.data.clone())
81    }
82    /// Upload from a host slice, replacing the buffer's contents.
83    ///
84    /// `data.len()` must equal [`Self::len`]; a mismatch returns
85    /// [`GpuError::MemoryError`] before mutating anything, leaving the existing
86    /// contents intact. Empty-to-empty uploads succeed as a no-op. The error
87    /// payload matches the CUDA backend exactly.
88    pub fn upload(&mut self, data: &[T]) -> GpuResult<()> {
89        if data.len() != self.data.len() {
90            return Err(GpuError::MemoryError(format!(
91                "upload: length mismatch, buffer has {} elements but input has {}",
92                self.data.len(),
93                data.len()
94            )));
95        }
96        self.data.clone_from_slice(data);
97        Ok(())
98    }
99    pub fn len(&self) -> usize {
100        self.data.len()
101    }
102    pub fn is_empty(&self) -> bool {
103        self.data.is_empty()
104    }
105    pub fn as_device_ptr(&self) -> *const T {
106        self.data.as_ptr()
107    }
108}
109
110impl GpuBuffer<f32> {
111    /// Zero the first `count` elements; leaves any tail untouched (host stub).
112    ///
113    /// Matches the CUDA `GpuBuffer<f32>::zero_prefix` surface so callers do not
114    /// depend on feature-specific method resolution.
115    pub fn zero_prefix(&mut self, count: usize) -> GpuResult<()> {
116        if count > self.data.len() {
117            return Err(GpuError::MemoryError(format!(
118                "zero_prefix: count {count} > buffer len {}",
119                self.data.len()
120            )));
121        }
122        for slot in &mut self.data[..count] {
123            *slot = 0.0;
124        }
125        Ok(())
126    }
127}
128
129pub struct GpuAccelerator {
130    capabilities: CapabilityReport,
131}
132
133impl GpuAccelerator {
134    /// Construct with [`ExecutionPolicy::PreferGpu`] (caller-approved CPU fallback).
135    pub fn new() -> Self {
136        match Self::with_policy(ExecutionPolicy::PreferGpu) {
137            Ok(acc) => acc,
138            Err(_) => unreachable!("PreferGpu construction is infallible"),
139        }
140    }
141
142    /// Fail closed: never return a CPU-backend accelerator.
143    pub fn require_gpu() -> GpuResult<Self> {
144        Self::with_policy(ExecutionPolicy::RequireGpu)
145    }
146
147    /// Construct under an explicit execution policy.
148    pub fn with_policy(policy: ExecutionPolicy) -> GpuResult<Self> {
149        let mut facts = host_facts();
150        apply_failure_to_facts(&mut facts, FallbackReason::CudaFeatureNotBuilt);
151        let capabilities = evaluate_capabilities(&facts);
152        match policy {
153            ExecutionPolicy::PreferGpu => Ok(Self { capabilities }),
154            ExecutionPolicy::RequireGpu => {
155                let fb = capabilities.fallback.clone().unwrap_or_else(|| {
156                    FallbackRecord::cpu(
157                        FallbackReason::CudaFeatureNotBuilt,
158                        "GPU was required but the cuda feature is not enabled",
159                    )
160                });
161                Err(GpuError::unavailable(fb.reason, fb.detail))
162            }
163        }
164    }
165
166    pub fn is_ready(&self) -> bool {
167        false
168    }
169
170    pub fn capabilities(&self) -> &CapabilityReport {
171        &self.capabilities
172    }
173
174    pub fn selected_backend(&self) -> Backend {
175        self.capabilities.selected_backend
176    }
177
178    pub fn fallback(&self) -> Option<&FallbackRecord> {
179        self.capabilities.fallback.as_ref()
180    }
181
182    fn unavailable_error(&self) -> GpuError {
183        match &self.capabilities.fallback {
184            Some(fb) => GpuError::unavailable(fb.reason, fb.detail.clone()),
185            None => GpuError::NoGpu,
186        }
187    }
188
189    pub fn kernels(&self) -> GpuResult<&KernelModule> {
190        Err(self.unavailable_error())
191    }
192    #[allow(clippy::too_many_arguments)]
193    pub fn stdp_update(
194        &self,
195        _: &mut GpuBuffer<f32>,
196        _: &GpuBuffer<f32>,
197        _: &GpuBuffer<f32>,
198        _: &mut GpuBuffer<f32>,
199        _: &mut GpuBuffer<f32>,
200        _: i32,
201        _: i32,
202        _: f32,
203    ) -> GpuResult<()> {
204        Err(self.unavailable_error())
205    }
206    #[allow(clippy::too_many_arguments)]
207    pub fn stdp_update_async(
208        &self,
209        _: &mut GpuBuffer<f32>,
210        _: &GpuBuffer<f32>,
211        _: &GpuBuffer<f32>,
212        _: &mut GpuBuffer<f32>,
213        _: &mut GpuBuffer<f32>,
214        _: i32,
215        _: i32,
216        _: f32,
217    ) -> GpuResult<()> {
218        Err(self.unavailable_error())
219    }
220    pub fn satsolver_extract(
221        &self,
222        _: &GpuBuffer<u8>,
223        _: &GpuBuffer<i32>,
224        _: &mut GpuBuffer<u8>,
225        _: i32,
226        _: i32,
227    ) -> GpuResult<()> {
228        Err(self.unavailable_error())
229    }
230    #[allow(clippy::too_many_arguments)]
231    pub fn satsolver_aux_reduce_best(
232        &self,
233        _: &GpuBuffer<u8>,
234        _: &mut GpuBuffer<u8>,
235        _: &GpuBuffer<i32>,
236        _: &mut GpuBuffer<i32>,
237        _: &mut GpuBuffer<i32>,
238        _: &GpuBuffer<i32>,
239        _: i32,
240        _: i32,
241        _: i32,
242        _: i32,
243    ) -> GpuResult<()> {
244        Err(self.unavailable_error())
245    }
246    pub fn poisson_encode(
247        &self,
248        _: &GpuBuffer<f32>,
249        _: &mut GpuBuffer<u32>,
250        _: u32,
251    ) -> GpuResult<()> {
252        Err(self.unavailable_error())
253    }
254
255    pub fn satsolver_extract_async(
256        &self,
257        _: &GpuBuffer<u8>,
258        _: &GpuBuffer<i32>,
259        _: &mut GpuBuffer<u8>,
260        _: i32,
261        _: i32,
262    ) -> GpuResult<()> {
263        Err(self.unavailable_error())
264    }
265
266    #[allow(clippy::too_many_arguments)]
267    pub fn satsolver_aux_reduce_best_async(
268        &self,
269        _: &GpuBuffer<u8>,
270        _: &mut GpuBuffer<u8>,
271        _: &GpuBuffer<i32>,
272        _: &mut GpuBuffer<i32>,
273        _: &mut GpuBuffer<i32>,
274        _: &GpuBuffer<i32>,
275        _: i32,
276        _: i32,
277        _: i32,
278        _: i32,
279    ) -> GpuResult<()> {
280        Err(self.unavailable_error())
281    }
282
283    pub fn poisson_encode_async(
284        &self,
285        _: &GpuBuffer<f32>,
286        _: &mut GpuBuffer<u32>,
287        _: u32,
288    ) -> GpuResult<()> {
289        Err(self.unavailable_error())
290    }
291
292    #[allow(clippy::too_many_arguments)]
293    pub fn ternary_gemv(
294        &self,
295        _: &GpuBuffer<u32>,
296        _: &GpuBuffer<f32>,
297        _: &GpuBuffer<f32>,
298        _: &mut GpuBuffer<f32>,
299        _: i32,
300        _: i32,
301        _: i32,
302        _: bool,
303    ) -> GpuResult<()> {
304        Err(self.unavailable_error())
305    }
306
307    #[allow(clippy::too_many_arguments)]
308    pub fn ternary_gemv_async(
309        &self,
310        _: &GpuBuffer<u32>,
311        _: &GpuBuffer<f32>,
312        _: &GpuBuffer<f32>,
313        _: &mut GpuBuffer<f32>,
314        _: i32,
315        _: i32,
316        _: i32,
317        _: bool,
318    ) -> GpuResult<()> {
319        Err(self.unavailable_error())
320    }
321
322    #[allow(clippy::too_many_arguments)]
323    pub fn ternary_gemm(
324        &self,
325        _: &GpuBuffer<u32>,
326        _: &GpuBuffer<f32>,
327        _: &GpuBuffer<f32>,
328        _: &mut GpuBuffer<f32>,
329        _: i32,
330        _: i32,
331        _: i32,
332        _: i32,
333        _: bool,
334    ) -> GpuResult<()> {
335        Err(self.unavailable_error())
336    }
337
338    #[allow(clippy::too_many_arguments)]
339    pub fn ternary_gemm_async(
340        &self,
341        _: &GpuBuffer<u32>,
342        _: &GpuBuffer<f32>,
343        _: &GpuBuffer<f32>,
344        _: &mut GpuBuffer<f32>,
345        _: i32,
346        _: i32,
347        _: i32,
348        _: i32,
349        _: bool,
350    ) -> GpuResult<()> {
351        Err(self.unavailable_error())
352    }
353
354    pub fn synchronize(&self) -> GpuResult<()> {
355        Err(self.unavailable_error())
356    }
357}
358impl Default for GpuAccelerator {
359    fn default() -> Self {
360        Self::new()
361    }
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn context_api_reports_no_compute_capability() {
370        let ctx = GpuContext;
371        assert_eq!(ctx.compute_capability(), None);
372    }
373
374    // ── GpuError Display ────────────────────────────────────────────────────
375
376    #[test]
377    fn error_display_nogpu() {
378        let err = GpuError::NoGpu;
379        assert_eq!(
380            err.to_string(),
381            "No GPU available (built without `cuda` feature)"
382        );
383    }
384
385    #[test]
386    fn error_display_init_failed() {
387        let err = GpuError::InitFailed("device busy".into());
388        assert_eq!(err.to_string(), "GPU init failed: device busy");
389    }
390
391    #[test]
392    fn error_display_module_load_failed() {
393        let err = GpuError::ModuleLoadFailed("bad ptx".into());
394        assert_eq!(err.to_string(), "PTX module load failed: bad ptx");
395    }
396
397    #[test]
398    fn error_display_kernel_not_found() {
399        let err = GpuError::KernelNotFound("foo_kernel".into());
400        assert_eq!(err.to_string(), "Kernel not found: foo_kernel");
401    }
402
403    #[test]
404    fn error_display_memory_error() {
405        let err = GpuError::MemoryError("OOM".into());
406        assert_eq!(err.to_string(), "GPU memory error: OOM");
407    }
408
409    #[test]
410    fn error_display_launch_failed() {
411        let err = GpuError::LaunchFailed("grid too large".into());
412        assert_eq!(err.to_string(), "Kernel launch failed: grid too large");
413    }
414
415    #[test]
416    fn error_display_cuda_error() {
417        let err = GpuError::CudaError("illegal memory".into());
418        assert_eq!(err.to_string(), "CUDA error: illegal memory");
419    }
420
421    #[test]
422    fn error_display_unavailable() {
423        let err = GpuError::unavailable(
424            FallbackReason::CudaFeatureNotBuilt,
425            "crate built without the cuda feature",
426        );
427        assert_eq!(
428            err.to_string(),
429            "GPU unavailable (cuda_feature_not_built): crate built without the cuda feature"
430        );
431    }
432
433    #[test]
434    fn error_display_invalid_input() {
435        let err = GpuError::invalid_input("n_vars must be >= 0, got -1");
436        assert_eq!(
437            err.to_string(),
438            "invalid input: n_vars must be >= 0, got -1"
439        );
440    }
441
442    #[test]
443    fn error_is_std_error() {
444        let err: &dyn std::error::Error = &GpuError::NoGpu;
445        assert!(!err.to_string().is_empty());
446    }
447
448    // ── GpuContext ──────────────────────────────────────────────────────────
449
450    #[test]
451    fn context_init_returns_no_gpu() {
452        let result = GpuContext::init();
453        assert!(result.is_err());
454        assert!(matches!(result.unwrap_err(), GpuError::NoGpu));
455    }
456
457    #[test]
458    fn context_is_available_returns_false() {
459        assert!(!GpuContext::is_available());
460    }
461
462    // ── KernelModule ────────────────────────────────────────────────────────
463
464    #[test]
465    fn kernel_module_load_returns_no_gpu() {
466        let result = KernelModule::load();
467        assert!(matches!(result.unwrap_err(), GpuError::NoGpu));
468    }
469
470    #[test]
471    fn kernel_module_load_satsolver_returns_no_gpu() {
472        let result = KernelModule::load_satsolver();
473        assert!(matches!(result.unwrap_err(), GpuError::NoGpu));
474    }
475
476    // ── GpuBuffer ───────────────────────────────────────────────────────────
477
478    #[test]
479    fn buffer_alloc_zero_length() {
480        let buf = GpuBuffer::<u8>::alloc(0).unwrap();
481        assert_eq!(buf.len(), 0);
482    }
483
484    #[test]
485    fn buffer_alloc_u8() {
486        let buf = GpuBuffer::<u8>::alloc(100).unwrap();
487        assert_eq!(buf.len(), 100);
488        let data = buf.to_vec().unwrap();
489        assert_eq!(data, vec![0u8; 100]);
490    }
491
492    #[test]
493    fn buffer_alloc_f32() {
494        let buf = GpuBuffer::<f32>::alloc(64).unwrap();
495        assert_eq!(buf.len(), 64);
496        let data = buf.to_vec().unwrap();
497        assert!(data.iter().all(|&v| v == 0.0f32));
498    }
499
500    /// Oversized requests return `MemoryError` instead of panicking (GH #48),
501    /// mirroring the CUDA backend. All cases fail checked arithmetic before
502    /// any physical allocation.
503    #[test]
504    fn buffer_alloc_overflow_returns_memory_error() {
505        // u64: `8 * len` overflows `usize` / exceeds the `isize::MAX` ceiling.
506        for len in [usize::MAX, isize::MAX as usize] {
507            assert_alloc_overflow::<u64>(len);
508        }
509        // u8: one byte per element, so `usize::MAX` / past-ceiling byte counts.
510        for len in [usize::MAX, isize::MAX as usize + 1] {
511            assert_alloc_overflow::<u8>(len);
512        }
513        // Wide element type: overflow with a smaller element count.
514        assert_alloc_overflow::<Wide512>(usize::MAX);
515    }
516
517    /// Assert an oversized `alloc` fails with the overflow `MemoryError`.
518    fn assert_alloc_overflow<T: Default + Clone>(len: usize) {
519        match GpuBuffer::<T>::alloc(len) {
520            Err(GpuError::MemoryError(msg)) => {
521                assert!(msg.contains("size overflow"), "msg: {msg}")
522            }
523            Err(other) => panic!("expected MemoryError, got {other}"),
524            Ok(_) => panic!("oversized alloc unexpectedly succeeded"),
525        }
526    }
527
528    /// 4 KiB element used to probe overflow with a wide element size.
529    #[derive(Clone)]
530    struct Wide512 {
531        _payload: [u64; 512],
532    }
533
534    impl Default for Wide512 {
535        fn default() -> Self {
536            Self {
537                _payload: [0u64; 512],
538            }
539        }
540    }
541
542    /// Zero-sized types allocate without bytes; zero and ordinary lengths work.
543    #[test]
544    fn buffer_alloc_zero_sized_type() {
545        let empty = GpuBuffer::<()>::alloc(0).unwrap();
546        assert_eq!(empty.len(), 0);
547        assert!(empty.to_vec().unwrap().is_empty());
548        let buf = GpuBuffer::<()>::alloc(8).unwrap();
549        assert_eq!(buf.len(), 8);
550        assert_eq!(buf.to_vec().unwrap(), vec![(); 8]);
551    }
552
553    #[test]
554    fn buffer_from_slice_roundtrip() {
555        let input = vec![1i32, 2, 3, 4, 5];
556        let buf = GpuBuffer::from_slice(&input).unwrap();
557        assert_eq!(buf.len(), 5);
558        let output = buf.to_vec().unwrap();
559        assert_eq!(input, output);
560    }
561
562    #[test]
563    fn buffer_from_slice_empty() {
564        let buf = GpuBuffer::<u8>::from_slice(&[]).unwrap();
565        assert_eq!(buf.len(), 0);
566    }
567
568    #[test]
569    fn buffer_upload_ok() {
570        let mut buf = GpuBuffer::<i32>::alloc(4).unwrap();
571        buf.upload(&[10, 20, 30, 40]).unwrap();
572        assert_eq!(buf.to_vec().unwrap(), vec![10, 20, 30, 40]);
573    }
574
575    #[test]
576    fn buffer_upload_length_mismatch() {
577        let mut buf = GpuBuffer::<i32>::alloc(4).unwrap();
578        let result = buf.upload(&[1, 2]);
579        assert!(result.is_err());
580        match result.unwrap_err() {
581            GpuError::MemoryError(msg) => {
582                assert!(msg.contains("length mismatch"));
583                assert!(msg.contains("4"));
584                assert!(msg.contains("2"));
585            }
586            other => panic!("expected MemoryError, got: {other}"),
587        }
588    }
589
590    // ── LIM-1302 buffer-contract regression (CPU stub) ──────────────────────
591
592    /// Assert `err` is the length-mismatch `MemoryError` naming both lengths.
593    fn assert_length_mismatch(err: GpuError, buffer_len: usize, input_len: usize) {
594        match err {
595            GpuError::MemoryError(msg) => {
596                assert!(msg.contains("length mismatch"), "msg: {msg}");
597                assert!(
598                    msg.contains(&format!("buffer has {buffer_len} elements")),
599                    "msg: {msg}"
600                );
601                assert!(
602                    msg.contains(&format!("input has {input_len}")),
603                    "msg: {msg}"
604                );
605            }
606            other => panic!("expected MemoryError, got {other}"),
607        }
608    }
609
610    /// Allocation lengths 0, 1, and representative boundary sizes all read back
611    /// as `T::default()` immediately after `alloc`, with no prior upload.
612    #[test]
613    fn buffer_alloc_boundary_lengths_read_back_default() {
614        for len in [0usize, 1, 2, 15, 16, 17, 256, 257, 4096] {
615            let buf = GpuBuffer::<i32>::alloc(len).unwrap();
616            assert_eq!(buf.len(), len, "len {len}");
617            assert_eq!(buf.is_empty(), len == 0, "is_empty at len {len}");
618            assert_eq!(
619                buf.to_vec().unwrap(),
620                vec![0i32; len],
621                "readback at len {len}"
622            );
623        }
624    }
625
626    /// `from_slice` then `to_vec` round-trips exact values across boundary sizes.
627    #[test]
628    fn buffer_from_slice_to_vec_roundtrip_boundaries() {
629        for len in [0usize, 1, 16, 17, 128, 257] {
630            let input: Vec<u32> = (0..len as u32)
631                .map(|i| i.wrapping_mul(2_654_435_761))
632                .collect();
633            let buf = GpuBuffer::from_slice(&input).unwrap();
634            assert_eq!(buf.to_vec().unwrap(), input, "roundtrip at len {len}");
635        }
636    }
637
638    /// An equal-length upload replaces every element.
639    #[test]
640    fn buffer_upload_equal_length_replaces_all() {
641        let mut buf = GpuBuffer::from_slice(&[9i32, 9, 9, 9]).unwrap();
642        buf.upload(&[1, 2, 3, 4]).unwrap();
643        assert_eq!(buf.to_vec().unwrap(), vec![1, 2, 3, 4]);
644    }
645
646    /// A too-short upload is rejected and leaves existing contents unchanged;
647    /// the error payload names both the buffer and input lengths.
648    #[test]
649    fn buffer_upload_too_short_rejected_and_non_mutating() {
650        let mut buf = GpuBuffer::from_slice(&[1i32, 2, 3, 4]).unwrap();
651        assert_length_mismatch(buf.upload(&[7, 8]).unwrap_err(), 4, 2);
652        assert_eq!(buf.to_vec().unwrap(), vec![1, 2, 3, 4], "contents mutated");
653    }
654
655    /// A too-long upload is rejected and leaves existing contents unchanged.
656    #[test]
657    fn buffer_upload_too_long_rejected_and_non_mutating() {
658        let mut buf = GpuBuffer::from_slice(&[1i32, 2, 3, 4]).unwrap();
659        assert_length_mismatch(buf.upload(&[7, 8, 9, 10, 11]).unwrap_err(), 4, 5);
660        assert_eq!(buf.to_vec().unwrap(), vec![1, 2, 3, 4], "contents mutated");
661    }
662
663    /// Empty-to-empty upload succeeds as a no-op.
664    #[test]
665    fn buffer_upload_empty_into_empty_ok() {
666        let mut buf = GpuBuffer::<i32>::alloc(0).unwrap();
667        buf.upload(&[]).unwrap();
668        assert!(buf.to_vec().unwrap().is_empty());
669    }
670
671    /// Empty/nonempty length mismatches fail in both directions.
672    #[test]
673    fn buffer_upload_empty_nonempty_mismatches_fail() {
674        let mut empty = GpuBuffer::<i32>::alloc(0).unwrap();
675        assert_length_mismatch(empty.upload(&[1]).unwrap_err(), 0, 1);
676
677        let mut nonempty = GpuBuffer::<i32>::alloc(3).unwrap();
678        assert_length_mismatch(nonempty.upload(&[]).unwrap_err(), 3, 0);
679    }
680
681    #[test]
682    fn buffer_zero_prefix_preserves_tail() {
683        let mut buf = GpuBuffer::<f32>::from_slice(&[1.0, 2.0, 3.0, 4.0]).unwrap();
684        buf.zero_prefix(2).unwrap();
685        assert_eq!(buf.to_vec().unwrap(), vec![0.0, 0.0, 3.0, 4.0]);
686    }
687
688    #[test]
689    fn buffer_zero_prefix_rejects_oversize() {
690        let mut buf = GpuBuffer::<f32>::from_slice(&[1.0, 2.0]).unwrap();
691        let err = buf.zero_prefix(3).unwrap_err();
692        match err {
693            GpuError::MemoryError(msg) => assert!(msg.contains("zero_prefix")),
694            other => panic!("expected MemoryError, got {other}"),
695        }
696        assert_eq!(buf.to_vec().unwrap(), vec![1.0, 2.0]);
697    }
698
699    #[test]
700    fn buffer_as_device_ptr_non_null() {
701        let buf = GpuBuffer::<u8>::alloc(16).unwrap();
702        let ptr = buf.as_device_ptr();
703        assert!(!ptr.is_null());
704    }
705
706    // ── GpuAccelerator ──────────────────────────────────────────────────────
707
708    #[test]
709    fn accelerator_new() {
710        let acc = GpuAccelerator::new();
711        assert!(!acc.is_ready());
712        assert_eq!(acc.selected_backend(), Backend::Cpu);
713        let fb = acc.fallback().expect("CPU stub records a fallback");
714        assert_eq!(fb.reason, FallbackReason::CudaFeatureNotBuilt);
715        assert_eq!(fb.selected_backend, Backend::Cpu);
716        assert!(!fb.detail.contains("/home/"));
717        assert!(!acc.capabilities().cuda_built);
718    }
719
720    #[test]
721    fn accelerator_default() {
722        let acc = GpuAccelerator::default();
723        assert!(!acc.is_ready());
724    }
725
726    #[test]
727    fn accelerator_require_gpu_fails_closed() {
728        let err = match GpuAccelerator::require_gpu() {
729            Err(e) => e,
730            Ok(_) => panic!("require_gpu must fail on the CPU stub"),
731        };
732        assert_eq!(
733            err.fallback_reason(),
734            Some(FallbackReason::CudaFeatureNotBuilt)
735        );
736        match err {
737            GpuError::Unavailable { reason, detail } => {
738                assert_eq!(reason, FallbackReason::CudaFeatureNotBuilt);
739                assert!(!detail.contains("/home/"));
740            }
741            other => panic!("expected Unavailable, got {other}"),
742        }
743    }
744
745    #[test]
746    fn accelerator_with_policy_prefer_gpu_is_cpu() {
747        let acc = GpuAccelerator::with_policy(ExecutionPolicy::PreferGpu).unwrap();
748        assert_eq!(acc.selected_backend(), Backend::Cpu);
749        assert!(!acc.is_ready());
750    }
751
752    #[test]
753    fn accelerator_kernels_returns_unavailable() {
754        let acc = GpuAccelerator::new();
755        assert_unavailable_not_built(acc.kernels().unwrap_err());
756    }
757
758    #[test]
759    fn accelerator_synchronize_returns_unavailable() {
760        let acc = GpuAccelerator::new();
761        assert_unavailable_not_built(acc.synchronize().unwrap_err());
762    }
763
764    fn assert_unavailable_not_built(err: GpuError) {
765        match err {
766            GpuError::Unavailable { reason, detail } => {
767                assert_eq!(reason, FallbackReason::CudaFeatureNotBuilt);
768                assert!(!detail.contains("/home/"));
769                assert!(!detail.contains("/Users/"));
770            }
771            other => panic!("expected Unavailable(cuda_feature_not_built), got {other}"),
772        }
773    }
774
775    #[test]
776    fn accelerator_satsolver_extract_returns_unavailable() {
777        let acc = GpuAccelerator::new();
778        let assignment = GpuBuffer::<u8>::alloc(10).unwrap();
779        let best_walker = GpuBuffer::<i32>::alloc(1).unwrap();
780        let mut output = GpuBuffer::<u8>::alloc(10).unwrap();
781        assert_unavailable_not_built(
782            acc.satsolver_extract(&assignment, &best_walker, &mut output, 10, 1)
783                .unwrap_err(),
784        );
785    }
786
787    #[test]
788    fn accelerator_poisson_encode_returns_unavailable() {
789        let acc = GpuAccelerator::new();
790        let stimuli = GpuBuffer::<f32>::alloc(10).unwrap();
791        let mut spikes = GpuBuffer::<u32>::alloc(10).unwrap();
792        assert_unavailable_not_built(acc.poisson_encode(&stimuli, &mut spikes, 42).unwrap_err());
793    }
794
795    #[test]
796    fn accelerator_satsolver_extract_async_returns_unavailable() {
797        let acc = GpuAccelerator::new();
798        let assignment = GpuBuffer::<u8>::alloc(10).unwrap();
799        let best_walker = GpuBuffer::<i32>::alloc(1).unwrap();
800        let mut output = GpuBuffer::<u8>::alloc(10).unwrap();
801        assert_unavailable_not_built(
802            acc.satsolver_extract_async(&assignment, &best_walker, &mut output, 10, 1)
803                .unwrap_err(),
804        );
805    }
806
807    #[test]
808    fn accelerator_poisson_encode_async_returns_unavailable() {
809        let acc = GpuAccelerator::new();
810        let stimuli = GpuBuffer::<f32>::alloc(10).unwrap();
811        let mut spikes = GpuBuffer::<u32>::alloc(10).unwrap();
812        assert_unavailable_not_built(
813            acc.poisson_encode_async(&stimuli, &mut spikes, 42)
814                .unwrap_err(),
815        );
816    }
817
818    #[test]
819    fn accelerator_ternary_gemv_returns_unavailable() {
820        let acc = GpuAccelerator::new();
821        let w = GpuBuffer::<u32>::alloc(1).unwrap();
822        let s = GpuBuffer::<f32>::alloc(1).unwrap();
823        let x = GpuBuffer::<f32>::alloc(1).unwrap();
824        let mut y = GpuBuffer::<f32>::alloc(1).unwrap();
825        assert_unavailable_not_built(
826            acc.ternary_gemv(&w, &s, &x, &mut y, 1, 1, 1, false)
827                .unwrap_err(),
828        );
829    }
830
831    #[test]
832    fn accelerator_ternary_gemm_returns_unavailable() {
833        let acc = GpuAccelerator::new();
834        let w = GpuBuffer::<u32>::alloc(1).unwrap();
835        let s = GpuBuffer::<f32>::alloc(1).unwrap();
836        let b = GpuBuffer::<f32>::alloc(1).unwrap();
837        let mut c = GpuBuffer::<f32>::alloc(1).unwrap();
838        assert_unavailable_not_built(
839            acc.ternary_gemm(&w, &s, &b, &mut c, 1, 1, 1, 1, false)
840                .unwrap_err(),
841        );
842    }
843
844    #[test]
845    fn accelerator_ternary_gemv_async_returns_unavailable() {
846        let acc = GpuAccelerator::new();
847        let w = GpuBuffer::<u32>::alloc(1).unwrap();
848        let s = GpuBuffer::<f32>::alloc(1).unwrap();
849        let x = GpuBuffer::<f32>::alloc(1).unwrap();
850        let mut y = GpuBuffer::<f32>::alloc(1).unwrap();
851        assert_unavailable_not_built(
852            acc.ternary_gemv_async(&w, &s, &x, &mut y, 1, 1, 1, false)
853                .unwrap_err(),
854        );
855    }
856
857    #[test]
858    fn accelerator_ternary_gemm_async_returns_unavailable() {
859        let acc = GpuAccelerator::new();
860        let w = GpuBuffer::<u32>::alloc(1).unwrap();
861        let s = GpuBuffer::<f32>::alloc(1).unwrap();
862        let b = GpuBuffer::<f32>::alloc(1).unwrap();
863        let mut c = GpuBuffer::<f32>::alloc(1).unwrap();
864        assert_unavailable_not_built(
865            acc.ternary_gemm_async(&w, &s, &b, &mut c, 1, 1, 1, 1, false)
866                .unwrap_err(),
867        );
868    }
869
870    // ── Property-based tests ────────────────────────────────────────────────
871
872    use proptest::prelude::*;
873
874    proptest! {
875        #[test]
876        fn buffer_from_slice_roundtrip_prop(data in proptest::collection::vec(any::<i32>(), 0..=256)) {
877            let buf = GpuBuffer::from_slice(&data).unwrap();
878            prop_assert_eq!(buf.len(), data.len());
879            prop_assert_eq!(buf.to_vec().unwrap(), data);
880        }
881
882        #[test]
883        fn buffer_alloc_has_correct_len(n in 0usize..=1024) {
884            let buf = GpuBuffer::<u8>::alloc(n).unwrap();
885            prop_assert_eq!(buf.len(), n);
886        }
887
888        #[test]
889        fn buffer_upload_roundtrip(
890            // Use finite floats only — NaN != NaN breaks equality assertions.
891            data in proptest::collection::vec(
892                any::<f32>().prop_filter("finite", |v| v.is_finite()),
893                1..=128
894            )
895        ) {
896            let mut buf = GpuBuffer::<f32>::alloc(data.len()).unwrap();
897            buf.upload(&data).unwrap();
898            prop_assert_eq!(buf.to_vec().unwrap(), data);
899        }
900    }
901}