Skip to main content

runmat_accelerate_api/
lib.rs

1use anyhow::anyhow;
2use once_cell::sync::{Lazy, OnceCell};
3use serde::{Deserialize, Serialize};
4#[cfg(not(target_arch = "wasm32"))]
5use std::cell::Cell;
6use std::collections::{HashMap, HashSet};
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::atomic::{AtomicU32, Ordering};
10#[cfg(feature = "wgpu")]
11use std::sync::Arc;
12#[cfg(target_arch = "wasm32")]
13use std::sync::Mutex;
14use std::sync::RwLock;
15
16mod placement;
17
18pub use placement::*;
19
20type ResidencyMarkFn = fn(&GpuTensorHandle);
21type ResidencyClearFn = fn(&GpuTensorHandle);
22type SequenceThresholdFn = fn() -> Option<usize>;
23type WorkgroupSizeHintFn = fn() -> Option<u32>;
24
25static RESIDENCY_MARK: OnceCell<ResidencyMarkFn> = OnceCell::new();
26static RESIDENCY_CLEAR: OnceCell<ResidencyClearFn> = OnceCell::new();
27static SEQUENCE_THRESHOLD_PROVIDER: OnceCell<SequenceThresholdFn> = OnceCell::new();
28static WORKGROUP_SIZE_HINT_PROVIDER: OnceCell<WorkgroupSizeHintFn> = OnceCell::new();
29
30pub type GpuHandleIdentity = (u32, u64);
31
32static LOGICAL_HANDLES: Lazy<RwLock<HashSet<GpuHandleIdentity>>> =
33    Lazy::new(|| RwLock::new(HashSet::new()));
34static LOGICAL_HANDLE_HITS: Lazy<RwLock<HashMap<GpuHandleIdentity, u64>>> =
35    Lazy::new(|| RwLock::new(HashMap::new()));
36static TRANSPOSED_HANDLES: Lazy<RwLock<HashMap<GpuHandleIdentity, TransposeInfo>>> =
37    Lazy::new(|| RwLock::new(HashMap::new()));
38
39static HANDLE_CLASS_NAMES: Lazy<RwLock<HashMap<GpuHandleIdentity, String>>> =
40    Lazy::new(|| RwLock::new(HashMap::new()));
41
42pub const fn handle_identity(handle: &GpuTensorHandle) -> GpuHandleIdentity {
43    (handle.device_id, handle.buffer_id)
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct TransposeInfo {
48    pub base_rows: usize,
49    pub base_cols: usize,
50}
51
52/// Register a callback used to mark residency tracking when GPU tensors are
53/// created or returned by device-side execution paths.
54pub fn register_residency_mark(handler: ResidencyMarkFn) {
55    let _ = RESIDENCY_MARK.set(handler);
56}
57
58/// Mark residency metadata for the provided GPU tensor handle, if a backend
59/// has registered a handler via [`register_residency_mark`].
60pub fn mark_residency(handle: &GpuTensorHandle) {
61    if let Some(handler) = RESIDENCY_MARK.get() {
62        handler(handle);
63    }
64}
65
66/// Register a callback used to clear residency tracking when GPU tensors are
67/// gathered back to the host. Backends that maintain residency metadata should
68/// install this hook during initialization.
69pub fn register_residency_clear(handler: ResidencyClearFn) {
70    let _ = RESIDENCY_CLEAR.set(handler);
71}
72
73/// Clear residency metadata for the provided GPU tensor handle, if a backend
74/// has registered a handler via [`register_residency_clear`].
75pub fn clear_residency(handle: &GpuTensorHandle) {
76    if let Some(handler) = RESIDENCY_CLEAR.get() {
77        handler(handle);
78    }
79}
80
81/// Register a callback that exposes the current sequence length threshold
82/// derived from the auto-offload planner. Array constructors can use this hint
83/// to decide when to prefer GPU residency automatically.
84pub fn register_sequence_threshold_provider(provider: SequenceThresholdFn) {
85    let _ = SEQUENCE_THRESHOLD_PROVIDER.set(provider);
86}
87
88/// Query the currently registered sequence threshold hint, if any.
89pub fn sequence_threshold_hint() -> Option<usize> {
90    SEQUENCE_THRESHOLD_PROVIDER
91        .get()
92        .and_then(|provider| provider())
93}
94
95/// Register a callback that reports the calibrated workgroup size selected by
96/// the active acceleration provider (if any). Plotting kernels can reuse this
97/// hint to match backend tuning.
98pub fn register_workgroup_size_hint_provider(provider: WorkgroupSizeHintFn) {
99    let _ = WORKGROUP_SIZE_HINT_PROVIDER.set(provider);
100}
101
102/// Query the current workgroup size hint exposed by the provider.
103pub fn workgroup_size_hint() -> Option<u32> {
104    WORKGROUP_SIZE_HINT_PROVIDER
105        .get()
106        .and_then(|provider| provider())
107}
108
109/// Export a shared acceleration context (e.g., the active WGPU device) when the
110/// current provider exposes one.
111pub fn export_context(kind: AccelContextKind) -> Option<AccelContextHandle> {
112    provider().and_then(|p| p.export_context(kind))
113}
114
115/// Request a provider-owned WGPU buffer for zero-copy consumers. Returns `None`
116/// when the active provider does not expose buffers or does not support the
117/// supplied handle.
118#[cfg(feature = "wgpu")]
119pub fn export_wgpu_buffer(handle: &GpuTensorHandle) -> Option<WgpuBufferRef> {
120    provider_for_handle(handle).and_then(|p| p.export_wgpu_buffer(handle))
121}
122
123/// Return the floating precision encoded by the handle's durable element type.
124pub fn handle_precision(handle: &GpuTensorHandle) -> Option<ProviderPrecision> {
125    handle
126        .descriptor
127        .element_type
128        .and_then(NumericElementType::precision)
129}
130
131/// Record the MATLAB underlying class associated with a GPU tensor handle.
132///
133/// Precision alone cannot represent integer gpuArray classes, so runtime
134/// introspection and validation use this metadata when the upload path knows
135/// the exact requested or inferred class.
136pub fn set_handle_class_name(handle: &GpuTensorHandle, class_name: impl Into<String>) {
137    if let Ok(mut guard) = HANDLE_CLASS_NAMES.write() {
138        guard.insert(handle_identity(handle), class_name.into());
139    }
140}
141
142/// Look up the recorded MATLAB underlying class for a GPU tensor handle.
143pub fn handle_class_name(handle: &GpuTensorHandle) -> Option<String> {
144    HANDLE_CLASS_NAMES
145        .read()
146        .ok()
147        .and_then(|guard| guard.get(&handle_identity(handle)).cloned())
148        .or_else(|| {
149            handle
150                .descriptor
151                .element_type
152                .map(|element_type| element_type.class_name().to_string())
153        })
154}
155
156/// Clear any recorded MATLAB underlying class metadata for a GPU tensor handle.
157pub fn clear_handle_class_name(handle: &GpuTensorHandle) {
158    if let Ok(mut guard) = HANDLE_CLASS_NAMES.write() {
159        guard.remove(&handle_identity(handle));
160    }
161}
162
163/// Annotate a GPU tensor handle as logically-typed (`logical` in MATLAB terms)
164/// or clear the logical flag when `logical` is `false`.
165pub fn set_handle_logical(handle: &GpuTensorHandle, logical: bool) {
166    let identity = handle_identity(handle);
167    if let Ok(mut guard) = LOGICAL_HANDLES.write() {
168        if logical {
169            guard.insert(identity);
170            if let Ok(mut hits) = LOGICAL_HANDLE_HITS.write() {
171                *hits.entry(identity).or_insert(0) += 1;
172            }
173        } else {
174            guard.remove(&identity);
175            if let Ok(mut hits) = LOGICAL_HANDLE_HITS.write() {
176                hits.remove(&identity);
177            }
178        }
179    }
180    if logical {
181        if let Ok(mut classes) = HANDLE_CLASS_NAMES.write() {
182            classes.insert(identity, "logical".into());
183        }
184    } else if let Ok(mut classes) = HANDLE_CLASS_NAMES.write() {
185        if classes
186            .get(&identity)
187            .is_some_and(|class| class == "logical")
188        {
189            classes.remove(&identity);
190        }
191    }
192}
193
194/// Convenience helper for clearing logical annotations explicitly.
195pub fn clear_handle_logical(handle: &GpuTensorHandle) {
196    set_handle_logical(handle, false);
197}
198
199/// Returns true when the supplied handle has been marked as logical.
200pub fn handle_is_logical(handle: &GpuTensorHandle) -> bool {
201    LOGICAL_HANDLES
202        .read()
203        .map(|guard| guard.contains(&handle_identity(handle)))
204        .unwrap_or(false)
205}
206
207pub fn handle_logical_hits(buffer_id: u64) -> Option<u64> {
208    LOGICAL_HANDLE_HITS.read().ok().and_then(|guard| {
209        let mut matches = guard
210            .iter()
211            .filter_map(|((_, candidate), hits)| (*candidate == buffer_id).then_some(*hits));
212        let first = matches.next()?;
213        Some(matches.fold(first, u64::saturating_add))
214    })
215}
216
217pub fn handle_logical_hits_for_handle(handle: &GpuTensorHandle) -> Option<u64> {
218    LOGICAL_HANDLE_HITS
219        .read()
220        .ok()
221        .and_then(|guard| guard.get(&handle_identity(handle)).copied())
222}
223
224pub fn record_handle_transpose(handle: &GpuTensorHandle, base_rows: usize, base_cols: usize) {
225    if let Ok(mut guard) = TRANSPOSED_HANDLES.write() {
226        guard.insert(
227            handle_identity(handle),
228            TransposeInfo {
229                base_rows,
230                base_cols,
231            },
232        );
233    }
234}
235
236pub fn clear_handle_transpose(handle: &GpuTensorHandle) {
237    if let Ok(mut guard) = TRANSPOSED_HANDLES.write() {
238        guard.remove(&handle_identity(handle));
239    }
240}
241
242pub fn handle_transpose_info(handle: &GpuTensorHandle) -> Option<TransposeInfo> {
243    TRANSPOSED_HANDLES
244        .read()
245        .ok()
246        .and_then(|guard| guard.get(&handle_identity(handle)).copied())
247}
248
249pub fn handle_is_transposed(handle: &GpuTensorHandle) -> bool {
250    handle_transpose_info(handle).is_some()
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
254pub enum GpuTensorStorage {
255    Real,
256    ComplexInterleaved,
257}
258
259/// Exact native integer element storage for host/device transfer contracts.
260///
261/// This intentionally lives in the acceleration API instead of depending on
262/// `runmat-builtins`, so providers can move integer buffers without first
263/// materializing RunMat's lossy floating compatibility view.
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
265pub enum IntegerElementType {
266    I8,
267    I16,
268    I32,
269    I64,
270    U8,
271    U16,
272    U32,
273    U64,
274}
275
276impl IntegerElementType {
277    pub const fn element_size(self) -> usize {
278        match self {
279            Self::I8 | Self::U8 => 1,
280            Self::I16 | Self::U16 => 2,
281            Self::I32 | Self::U32 => 4,
282            Self::I64 | Self::U64 => 8,
283        }
284    }
285}
286
287/// Look up the exact native integer class stored by a GPU tensor handle.
288pub fn handle_integer_type(handle: &GpuTensorHandle) -> Option<IntegerElementType> {
289    handle
290        .descriptor
291        .element_type
292        .and_then(NumericElementType::integer_type)
293}
294
295impl Default for GpuTensorStorage {
296    fn default() -> Self {
297        Self::Real
298    }
299}
300
301/// Durable physical description of a provider-owned numeric buffer.
302///
303/// Provider-created numeric handles populate element type and storage at
304/// allocation time so cloning or serializing a handle cannot discard its
305/// physical interpretation. A missing physical field is invalid at numeric
306/// provider boundaries. Provenance is populated by ownership-aware runtime
307/// boundaries.
308#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
309pub struct GpuTensorDescriptor {
310    pub element_type: Option<NumericElementType>,
311    pub storage: Option<GpuTensorStorage>,
312    pub provenance: Option<GpuHandleProvenance>,
313}
314
315impl GpuTensorDescriptor {
316    pub const fn numeric(element_type: NumericElementType, storage: GpuTensorStorage) -> Self {
317        Self {
318            element_type: Some(element_type),
319            storage: Some(storage),
320            provenance: None,
321        }
322    }
323}
324
325#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
326pub struct GpuTensorHandle {
327    pub shape: Vec<usize>,
328    /// Stable provider/device namespace identifier. Distinct live provider
329    /// instances that can own handles must use distinct identifiers because
330    /// provider routing is keyed by this value.
331    pub device_id: u32,
332    pub buffer_id: u64,
333    #[serde(default)]
334    pub descriptor: GpuTensorDescriptor,
335}
336
337impl GpuTensorHandle {
338    pub fn new(shape: Vec<usize>, device_id: u32, buffer_id: u64) -> Self {
339        Self {
340            shape,
341            device_id,
342            buffer_id,
343            descriptor: GpuTensorDescriptor::default(),
344        }
345    }
346
347    pub fn with_numeric_descriptor(
348        mut self,
349        element_type: NumericElementType,
350        storage: GpuTensorStorage,
351    ) -> Self {
352        self.descriptor.element_type = Some(element_type);
353        self.descriptor.storage = Some(storage);
354        self
355    }
356
357    pub fn with_provenance(mut self, provenance: GpuHandleProvenance) -> Self {
358        self.descriptor.provenance = Some(provenance);
359        self
360    }
361}
362
363/// Semantic origin of a resident handle.
364///
365/// Automatic residency is an implementation detail and may transparently return
366/// to the host. Explicit residency represents user-visible `gpuArray` intent and
367/// must be retained by operations that preserve gpuArray semantics.
368#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
369pub enum GpuHandleProvenance {
370    Automatic,
371    Explicit,
372}
373
374pub fn set_handle_provenance(handle: &mut GpuTensorHandle, provenance: GpuHandleProvenance) {
375    handle.descriptor.provenance = Some(provenance);
376}
377
378pub fn handle_provenance(handle: &GpuTensorHandle) -> Option<GpuHandleProvenance> {
379    handle.descriptor.provenance
380}
381
382pub fn mark_handle_explicit(handle: &mut GpuTensorHandle) {
383    set_handle_provenance(handle, GpuHandleProvenance::Explicit);
384}
385
386pub fn mark_handle_automatic(handle: &mut GpuTensorHandle) {
387    set_handle_provenance(handle, GpuHandleProvenance::Automatic);
388}
389
390pub fn handle_is_explicit(handle: &GpuTensorHandle) -> bool {
391    handle_provenance(handle) == Some(GpuHandleProvenance::Explicit)
392}
393
394/// Clear mutable API side annotations for a released or freshly reused handle,
395/// including backend residency tracking registered through
396/// [`register_residency_clear`]. The handle-owned physical descriptor remains
397/// available on existing handle values.
398pub fn clear_handle_metadata(handle: &GpuTensorHandle) {
399    clear_residency(handle);
400    clear_handle_class_name(handle);
401    clear_handle_logical(handle);
402    clear_handle_transpose(handle);
403}
404
405#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
406pub enum ProviderSpectralRange {
407    Onesided,
408    Twosided,
409    Centered,
410}
411
412#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
413pub enum ProviderSpectralFrameMode {
414    Sliding {
415        hop: usize,
416    },
417    ColumnSliding {
418        hop: usize,
419        input_rows: usize,
420        frames_per_column: usize,
421    },
422    FoldedColumns {
423        input_rows: usize,
424    },
425}
426
427#[derive(Clone, Debug)]
428pub struct ProviderSpectralRequest<'a> {
429    pub input: &'a GpuTensorHandle,
430    pub input_len: usize,
431    pub input_complex: bool,
432    pub window: &'a [f64],
433    pub nfft: usize,
434    pub frame_count: usize,
435    pub frame_mode: ProviderSpectralFrameMode,
436    pub range: ProviderSpectralRange,
437    pub denominator: f64,
438}
439
440#[derive(Clone, Debug)]
441pub struct ProviderSpectralResult {
442    pub s: GpuTensorHandle,
443    pub ps: GpuTensorHandle,
444    pub rows: usize,
445    pub cols: usize,
446}
447
448#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
449pub enum ProviderEnvelopeMethod {
450    Analytic,
451    AnalyticFir { filter_len: usize },
452    Rms { window_len: usize },
453}
454
455#[derive(Clone, Debug)]
456pub struct ProviderEnvelopeRequest<'a> {
457    pub input: &'a GpuTensorHandle,
458    pub channel_len: usize,
459    pub channel_count: usize,
460    pub output_shape: &'a [usize],
461    pub method: ProviderEnvelopeMethod,
462}
463
464#[derive(Clone, Debug)]
465pub struct ProviderEnvelopeResult {
466    pub upper: GpuTensorHandle,
467    pub lower: GpuTensorHandle,
468}
469
470#[derive(Clone, Debug)]
471pub struct ProviderHilbertRequest<'a> {
472    pub input: &'a GpuTensorHandle,
473    /// Optional FFT length along `dim`.
474    pub length: Option<usize>,
475    /// Zero-based transform dimension.
476    pub dim: usize,
477}
478
479#[derive(Clone, Debug)]
480pub struct ProviderModulationRequest<'a> {
481    pub input: &'a GpuTensorHandle,
482    /// `(real, imag)` pairs interleaved by symbol index.
483    pub constellation: &'a [f64],
484}
485
486#[derive(Clone, Debug)]
487pub struct ProviderBitModulationRequest<'a> {
488    pub input: &'a GpuTensorHandle,
489    /// Number of bit rows in the input before grouping.
490    pub input_rows: usize,
491    /// Number of input bits that form one output symbol.
492    pub bits_per_symbol: usize,
493    /// `(real, imag)` pairs interleaved by symbol index.
494    pub constellation: &'a [f64],
495}
496
497pub async fn uniform_spectral_estimate(
498    request: ProviderSpectralRequest<'_>,
499) -> anyhow::Result<ProviderSpectralResult> {
500    validate_uniform_spectral_request(&request)?;
501
502    let provider = provider_for_handle(request.input)
503        .ok_or_else(|| anyhow!("uniform_spectral_estimate: GPU provider unavailable"))?;
504    provider.uniform_spectral_estimate(&request).await
505}
506
507fn validate_uniform_spectral_request(request: &ProviderSpectralRequest<'_>) -> anyhow::Result<()> {
508    let invalid_frame_mode = matches!(
509        request.frame_mode,
510        ProviderSpectralFrameMode::Sliding { hop: 0 }
511            | ProviderSpectralFrameMode::ColumnSliding { hop: 0, .. }
512    );
513    let invalid_input_coverage = match request.frame_mode {
514        ProviderSpectralFrameMode::Sliding { hop } => {
515            let required = request
516                .frame_count
517                .checked_sub(1)
518                .and_then(|frames| frames.checked_mul(hop))
519                .and_then(|offset| offset.checked_add(request.window.len()));
520            required.is_none_or(|required| required > request.input_len)
521        }
522        ProviderSpectralFrameMode::ColumnSliding {
523            hop,
524            input_rows,
525            frames_per_column,
526        } => {
527            if input_rows == 0 || frames_per_column == 0 {
528                true
529            } else {
530                let last_frame = request.frame_count - 1;
531                let source_col = last_frame / frames_per_column;
532                let segment = last_frame % frames_per_column;
533                source_col
534                    .checked_mul(input_rows)
535                    .and_then(|base| {
536                        segment
537                            .checked_mul(hop)
538                            .and_then(|step| base.checked_add(step))
539                    })
540                    .and_then(|offset| offset.checked_add(request.window.len()))
541                    .is_none_or(|required| required > request.input_len)
542            }
543        }
544        ProviderSpectralFrameMode::FoldedColumns { input_rows } => {
545            input_rows == 0
546                || input_rows
547                    .checked_mul(request.frame_count)
548                    .is_none_or(|required| required > request.input_len)
549        }
550    };
551    if request.window.is_empty()
552        || request.nfft == 0
553        || request.frame_count == 0
554        || invalid_frame_mode
555        || invalid_input_coverage
556        || !request.denominator.is_finite()
557        || request.denominator <= 0.0
558    {
559        return Err(anyhow!("uniform_spectral_estimate: invalid request"));
560    }
561
562    Ok(())
563}
564
565pub async fn signal_envelope(
566    request: ProviderEnvelopeRequest<'_>,
567) -> anyhow::Result<ProviderEnvelopeResult> {
568    let expected_len = request
569        .channel_len
570        .checked_mul(request.channel_count)
571        .ok_or_else(|| anyhow!("signal_envelope: invalid request"))?;
572    let output_len = request
573        .output_shape
574        .iter()
575        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
576        .ok_or_else(|| anyhow!("signal_envelope: invalid request"))?;
577    let input_len = request
578        .input
579        .shape
580        .iter()
581        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
582        .ok_or_else(|| anyhow!("signal_envelope: invalid request"))?;
583    if request.channel_len == 0
584        || request.channel_count == 0
585        || request.output_shape.is_empty()
586        || output_len != expected_len
587        || input_len != expected_len
588        || !provider_envelope_input_shape_matches(
589            &request.input.shape,
590            request.channel_len,
591            request.channel_count,
592        )
593    {
594        return Err(anyhow!("signal_envelope: invalid request"));
595    }
596
597    match request.method {
598        ProviderEnvelopeMethod::AnalyticFir { filter_len }
599        | ProviderEnvelopeMethod::Rms {
600            window_len: filter_len,
601        } if filter_len == 0 => return Err(anyhow!("signal_envelope: invalid request")),
602        _ => {}
603    }
604
605    let provider = provider_for_handle(request.input)
606        .ok_or_else(|| anyhow!("signal_envelope: GPU provider unavailable"))?;
607    provider.signal_envelope(&request).await
608}
609
610fn provider_envelope_input_shape_matches(
611    shape: &[usize],
612    channel_len: usize,
613    channel_count: usize,
614) -> bool {
615    if channel_count == 1 {
616        return match shape {
617            [len] => *len == channel_len,
618            [rows, cols] => {
619                (*rows == channel_len && *cols == 1) || (*rows == 1 && *cols == channel_len)
620            }
621            _ => false,
622        };
623    }
624
625    matches!(shape, [rows, cols] if *rows == channel_len && *cols == channel_count)
626}
627
628pub async fn signal_hilbert(
629    request: ProviderHilbertRequest<'_>,
630) -> anyhow::Result<GpuTensorHandle> {
631    if request.length == Some(0) {
632        return Err(anyhow!("signal_hilbert: invalid request"));
633    }
634    if request.dim >= request.input.shape.len() {
635        return Err(anyhow!("signal_hilbert: invalid request"));
636    }
637
638    let provider = provider_for_handle(request.input)
639        .ok_or_else(|| anyhow!("signal_hilbert: GPU provider unavailable"))?;
640    provider.signal_hilbert(&request).await
641}
642
643#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
644pub struct ApiDeviceInfo {
645    pub device_id: u32,
646    pub name: String,
647    pub vendor: String,
648    pub memory_bytes: Option<u64>,
649    pub backend: Option<String>,
650}
651
652#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
653pub struct ReduceDimResult {
654    pub values: GpuTensorHandle,
655    pub indices: GpuTensorHandle,
656}
657
658#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
659pub struct ProviderCumminResult {
660    pub values: GpuTensorHandle,
661    pub indices: GpuTensorHandle,
662}
663
664/// Result payload returned by provider-side `cummax` scans.
665///
666/// Alias of [`ProviderCumminResult`] because both operations return the same pair of tensors
667/// (running values and MATLAB-compatible indices).
668pub type ProviderCummaxResult = ProviderCumminResult;
669
670/// Names a shared acceleration context that callers may request (e.g. plotting).
671#[derive(Debug, Clone, Copy, PartialEq, Eq)]
672pub enum AccelContextKind {
673    Plotting,
674}
675
676/// Handle returned by [`export_context`] that describes a shared GPU context.
677#[derive(Clone)]
678pub enum AccelContextHandle {
679    #[cfg(feature = "wgpu")]
680    Wgpu(WgpuContextHandle),
681}
682
683impl AccelContextHandle {
684    /// Returns the underlying WGPU context when available.
685    #[cfg(feature = "wgpu")]
686    pub fn as_wgpu(&self) -> Option<&WgpuContextHandle> {
687        match self {
688            AccelContextHandle::Wgpu(ctx) => Some(ctx),
689        }
690    }
691}
692
693/// Shared WGPU device/queue pair exported by the acceleration provider.
694#[cfg(feature = "wgpu")]
695#[derive(Clone)]
696pub struct WgpuContextHandle {
697    pub instance: Arc<wgpu::Instance>,
698    pub device: Arc<wgpu::Device>,
699    pub queue: Arc<wgpu::Queue>,
700    pub adapter: Arc<wgpu::Adapter>,
701    pub adapter_info: wgpu::AdapterInfo,
702    pub limits: wgpu::Limits,
703    pub features: wgpu::Features,
704}
705
706/// Borrowed reference to a provider-owned WGPU buffer corresponding to a `GpuTensorHandle`.
707///
708/// Providers that expose this handle must ensure the buffer was created with
709/// `wgpu::BufferUsages::COPY_SRC`, so zero-copy consumers can export or inspect
710/// tensor data without depending on provider-specific readback APIs.
711#[cfg(feature = "wgpu")]
712#[derive(Clone)]
713pub struct WgpuBufferRef {
714    pub buffer: Arc<wgpu::Buffer>,
715    pub len: usize,
716    pub shape: Vec<usize>,
717    pub element_size: usize,
718    pub precision: ProviderPrecision,
719}
720
721pub fn handle_storage(handle: &GpuTensorHandle) -> GpuTensorStorage {
722    handle.descriptor.storage.unwrap_or_default()
723}
724
725#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
726pub enum PagefunOp {
727    Mtimes,
728}
729
730#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
731pub struct PagefunRequest {
732    pub op: PagefunOp,
733    pub inputs: Vec<GpuTensorHandle>,
734    pub output_shape: Vec<usize>,
735    pub page_dims: Vec<usize>,
736    pub input_page_dims: Vec<Vec<usize>>,
737}
738
739#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
740pub enum FindDirection {
741    First,
742    Last,
743}
744
745#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
746pub struct ProviderFindResult {
747    pub linear: GpuTensorHandle,
748    pub rows: GpuTensorHandle,
749    pub cols: GpuTensorHandle,
750    pub values: Option<GpuTensorHandle>,
751}
752
753#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
754pub struct ProviderBandwidth {
755    pub lower: u32,
756    pub upper: u32,
757}
758
759#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
760pub enum ProviderSymmetryKind {
761    Symmetric,
762    Skew,
763}
764
765#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
766pub enum ProviderHermitianKind {
767    Hermitian,
768    Skew,
769}
770
771#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
772pub struct ProviderLuResult {
773    pub combined: GpuTensorHandle,
774    pub lower: GpuTensorHandle,
775    pub upper: GpuTensorHandle,
776    pub perm_matrix: GpuTensorHandle,
777    pub perm_vector: GpuTensorHandle,
778}
779
780#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
781pub struct ProviderCholResult {
782    pub factor: GpuTensorHandle,
783    /// MATLAB-compatible failure index (0 indicates success).
784    pub info: u32,
785}
786
787#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
788pub struct ProviderQrResult {
789    pub q: GpuTensorHandle,
790    pub r: GpuTensorHandle,
791    pub perm_matrix: GpuTensorHandle,
792    pub perm_vector: GpuTensorHandle,
793}
794
795#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
796pub struct ProviderQrPowerIterResult {
797    pub q: GpuTensorHandle,
798    pub r: GpuTensorHandle,
799    pub perm_matrix: GpuTensorHandle,
800    pub perm_vector: GpuTensorHandle,
801}
802
803#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
804pub struct ProviderLinsolveOptions {
805    pub lower: bool,
806    pub upper: bool,
807    pub rectangular: bool,
808    pub transposed: bool,
809    pub conjugate: bool,
810    pub symmetric: bool,
811    pub posdef: bool,
812    pub need_rcond: bool,
813    pub rcond: Option<f64>,
814}
815
816#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
817pub struct ProviderLinsolveResult {
818    pub solution: GpuTensorHandle,
819    pub reciprocal_condition: f64,
820}
821
822#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
823pub struct ProviderPinvOptions {
824    pub tolerance: Option<f64>,
825}
826
827#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
828pub struct ProviderPolyvalMu {
829    pub mean: f64,
830    pub scale: f64,
831}
832
833#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
834pub struct ProviderPolyvalOptions {
835    pub mu: Option<ProviderPolyvalMu>,
836}
837
838#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
839pub struct ProviderInvOptions {}
840
841#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
842pub struct ProviderPolyfitResult {
843    pub coefficients: Vec<f64>,
844    pub r_matrix: Vec<f64>,
845    pub normr: f64,
846    pub df: f64,
847    pub mu: [f64; 2],
848}
849
850/// Numerator/denominator payload returned by provider-backed `polyder` quotient rule.
851#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
852pub struct ProviderPolyderQuotient {
853    pub numerator: GpuTensorHandle,
854    pub denominator: GpuTensorHandle,
855}
856
857/// Supported norm specifications for the `cond` builtin.
858#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
859pub enum ProviderCondNorm {
860    Two,
861    One,
862    Inf,
863    Fro,
864}
865
866/// Supported norm orders for the `norm` builtin.
867#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
868pub enum ProviderNormOrder {
869    Two,
870    One,
871    Inf,
872    NegInf,
873    Zero,
874    Fro,
875    Nuc,
876    P(f64),
877}
878
879#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
880pub enum ProviderInterp1Method {
881    Linear,
882    Nearest,
883}
884
885#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
886pub enum ProviderInterp1Extrapolation {
887    Nan,
888    Extrapolate,
889    Value,
890}
891
892#[derive(Debug, Clone)]
893pub struct ProviderInterp1Request<'a> {
894    /// Strictly increasing, finite sample coordinates validated by the runtime
895    /// before dispatch. Providers may assume this monotonic domain invariant.
896    pub x: &'a GpuTensorHandle,
897    pub y: &'a GpuTensorHandle,
898    pub xq: &'a GpuTensorHandle,
899    pub sample_len: usize,
900    pub series_count: usize,
901    pub query_len: usize,
902    pub output_shape: &'a [usize],
903    pub method: ProviderInterp1Method,
904    pub extrapolation: ProviderInterp1Extrapolation,
905    pub extrapolation_value: f64,
906}
907
908#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
909pub struct ProviderEigResult {
910    pub eigenvalues: GpuTensorHandle,
911    pub diagonal: GpuTensorHandle,
912    pub right: GpuTensorHandle,
913    pub left: Option<GpuTensorHandle>,
914}
915
916#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
917pub enum ProviderQrPivot {
918    Matrix,
919    Vector,
920}
921
922#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
923pub struct ProviderQrOptions {
924    pub economy: bool,
925    pub pivot: ProviderQrPivot,
926}
927
928impl Default for ProviderQrOptions {
929    fn default() -> Self {
930        Self {
931            economy: false,
932            pivot: ProviderQrPivot::Matrix,
933        }
934    }
935}
936
937#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
938pub enum ProviderPrecision {
939    F32,
940    F64,
941}
942
943/// Declares how provider-owned GPU handles may cross async spawn boundaries.
944///
945/// This is a runtime/provider policy surface (not a semantic type fact) used by
946/// VM/runtime spawn handling to prevent unsynchronized device-handle races.
947#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
948pub enum SpawnHandleConcurrency {
949    /// Provider supports immutable sharing of handle-backed values across spawned tasks.
950    ImmutableShare,
951    /// Provider supports copy-on-write semantics when spawned and parent tasks diverge.
952    CopyOnWrite,
953    /// Provider supports synchronized mutation for shared handles.
954    SynchronizedMutation,
955    /// Provider rejects spawned sharing of raw handles.
956    Reject,
957}
958
959impl SpawnHandleConcurrency {
960    pub fn as_str(self) -> &'static str {
961        match self {
962            SpawnHandleConcurrency::ImmutableShare => "immutable_share",
963            SpawnHandleConcurrency::CopyOnWrite => "copy_on_write",
964            SpawnHandleConcurrency::SynchronizedMutation => "synchronized_mutation",
965            SpawnHandleConcurrency::Reject => "reject",
966        }
967    }
968}
969
970#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
971pub enum ReductionTwoPassMode {
972    Auto,
973    ForceOn,
974    ForceOff,
975}
976
977impl ReductionTwoPassMode {
978    pub fn as_str(self) -> &'static str {
979        match self {
980            ReductionTwoPassMode::Auto => "auto",
981            ReductionTwoPassMode::ForceOn => "force_on",
982            ReductionTwoPassMode::ForceOff => "force_off",
983        }
984    }
985}
986
987#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
988pub enum ReductionFlavor {
989    Sum,
990    Mean,
991    CustomScale(f64),
992}
993
994impl ReductionFlavor {
995    pub fn is_mean(self) -> bool {
996        matches!(self, ReductionFlavor::Mean)
997    }
998
999    pub fn scale(self, reduce_len: usize) -> f64 {
1000        match self {
1001            ReductionFlavor::Sum => 1.0,
1002            ReductionFlavor::Mean => {
1003                if reduce_len == 0 {
1004                    1.0
1005                } else {
1006                    1.0 / reduce_len as f64
1007                }
1008            }
1009            ReductionFlavor::CustomScale(scale) => scale,
1010        }
1011    }
1012}
1013
1014/// Normalisation mode for correlation coefficients.
1015#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1016pub enum CorrcoefNormalization {
1017    Unbiased,
1018    Biased,
1019}
1020
1021/// Row-selection strategy for correlation coefficients.
1022#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1023pub enum CorrcoefRows {
1024    All,
1025    Complete,
1026    Pairwise,
1027}
1028
1029/// Options controlling provider-backed correlation coefficient computation.
1030#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1031pub struct CorrcoefOptions {
1032    pub normalization: CorrcoefNormalization,
1033    pub rows: CorrcoefRows,
1034}
1035
1036impl Default for CorrcoefOptions {
1037    fn default() -> Self {
1038        Self {
1039            normalization: CorrcoefNormalization::Unbiased,
1040            rows: CorrcoefRows::All,
1041        }
1042    }
1043}
1044
1045/// Normalisation mode used by covariance computations.
1046#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1047pub enum CovNormalization {
1048    Unbiased,
1049    Biased,
1050}
1051
1052/// Row handling strategy for covariance computations.
1053#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1054pub enum CovRows {
1055    All,
1056    OmitRows,
1057    PartialRows,
1058}
1059
1060/// Options controlling provider-backed covariance computation.
1061#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1062pub struct CovarianceOptions {
1063    pub normalization: CovNormalization,
1064    pub rows: CovRows,
1065    pub has_weight_vector: bool,
1066}
1067
1068impl Default for CovarianceOptions {
1069    fn default() -> Self {
1070        Self {
1071            normalization: CovNormalization::Unbiased,
1072            rows: CovRows::All,
1073            has_weight_vector: false,
1074        }
1075    }
1076}
1077
1078/// Normalization strategy used by provider-backed standard deviation reductions.
1079#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1080pub enum ProviderStdNormalization {
1081    Sample,
1082    Population,
1083}
1084
1085/// NaN handling mode for provider-backed reductions.
1086#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1087pub enum ProviderNanMode {
1088    Include,
1089    Omit,
1090}
1091
1092/// Moving-window reduction operation executed by acceleration providers.
1093#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1094pub enum ProviderMovingWindowOp {
1095    Sum,
1096    Mean,
1097    Prod,
1098    Min,
1099    Max,
1100    Median,
1101    Std,
1102    Var,
1103}
1104
1105/// Endpoint handling for provider-backed moving-window reductions.
1106#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1107pub enum ProviderMovingWindowEndpoints {
1108    Shrink,
1109    Discard,
1110    Fill(f64),
1111}
1112
1113/// Request for provider-backed count-window moving reductions.
1114#[derive(Debug, Clone, Copy)]
1115pub struct ProviderMovingWindowRequest<'a> {
1116    pub input: &'a GpuTensorHandle,
1117    pub output_shape: &'a [usize],
1118    /// Zero-based dimension along which the moving window is applied.
1119    pub dim: usize,
1120    pub before: usize,
1121    pub after: usize,
1122    pub op: ProviderMovingWindowOp,
1123    pub endpoints: ProviderMovingWindowEndpoints,
1124    pub nan_mode: ProviderNanMode,
1125    pub normalization: ProviderStdNormalization,
1126}
1127
1128/// Dimension selection for provider-backed `mode` reductions.
1129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1130pub enum ProviderModeAxes {
1131    /// First non-singleton dimension selected by MATLAB default rules.
1132    Default,
1133    /// Zero-based dimension supplied by `mode(A, dim)`.
1134    Dim(usize),
1135    /// Collapse every element, as in `mode(A, "all")`.
1136    All,
1137}
1138
1139/// Request for provider-backed modal reductions.
1140#[derive(Debug, Clone, Copy)]
1141pub struct ProviderModeRequest<'a> {
1142    pub input: &'a GpuTensorHandle,
1143    pub axes: ProviderModeAxes,
1144    pub want_frequency: bool,
1145    pub want_ties: bool,
1146}
1147
1148/// Sorted tied modal values for each output slice.
1149///
1150/// `values` stores all tied values for all slices in ascending per-slice order.
1151/// `offsets` and `counts` are one entry per output slice and describe the
1152/// slice-local range inside `values`. Runtime callers convert this ragged
1153/// representation into MATLAB cell output `C`.
1154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1155pub struct ProviderModeTiedSets {
1156    pub values: HostTensorOwned,
1157    pub offsets: Vec<usize>,
1158    pub counts: Vec<usize>,
1159}
1160
1161/// Result of a provider-backed `mode` reduction.
1162///
1163/// `values` is the MATLAB `M` output and stays resident. `frequencies`, when
1164/// requested, is the MATLAB `F` output and stays resident. `ties`, when
1165/// requested, carries the MATLAB `C` tied-set data in a host ragged form because
1166/// the public runtime value is a cell array.
1167#[derive(Debug, Clone, PartialEq)]
1168pub struct ProviderModeResult {
1169    pub values: GpuTensorHandle,
1170    pub frequencies: Option<GpuTensorHandle>,
1171    pub ties: Option<ProviderModeTiedSets>,
1172}
1173
1174/// Direction used when computing prefix sums on the device.
1175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1176pub enum ProviderScanDirection {
1177    Forward,
1178    Reverse,
1179}
1180
1181/// Spacing input for provider-backed trapezoidal integration.
1182#[derive(Debug, Clone, Copy)]
1183pub enum ProviderTrapezoidSpacing<'a> {
1184    Unit,
1185    Scalar(f64),
1186    ScalarHandle(&'a GpuTensorHandle),
1187    Vector(&'a GpuTensorHandle),
1188    Tensor(&'a GpuTensorHandle),
1189}
1190
1191/// Sort direction used by acceleration providers.
1192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1193pub enum SortOrder {
1194    Ascend,
1195    Descend,
1196}
1197
1198/// Comparison strategy applied during sorting.
1199#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1200pub enum SortComparison {
1201    Auto,
1202    Real,
1203    Abs,
1204}
1205
1206/// Host-resident outputs returned by provider-backed sort operations.
1207#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1208pub struct SortResult {
1209    pub values: HostTensorOwned,
1210    pub indices: HostTensorOwned,
1211}
1212
1213#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1214pub struct SortRowsColumnSpec {
1215    pub index: usize,
1216    pub order: SortOrder,
1217}
1218
1219/// Ordering applied by provider-backed `unique` operations.
1220#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1221pub enum UniqueOrder {
1222    Sorted,
1223    Stable,
1224}
1225
1226/// Occurrence selection for provider-backed `unique` operations.
1227#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1228pub enum UniqueOccurrence {
1229    First,
1230    Last,
1231}
1232
1233/// Options controlling provider-backed `unique` operations.
1234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1235pub struct UniqueOptions {
1236    pub rows: bool,
1237    pub order: UniqueOrder,
1238    pub occurrence: UniqueOccurrence,
1239    pub treat_missing_as_distinct: bool,
1240    pub explicit_order: bool,
1241    pub explicit_occurrence: bool,
1242}
1243
1244/// Host-resident outputs returned by provider-backed `unique` operations.
1245#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1246pub struct UniqueResult {
1247    pub values: HostTensorOwned,
1248    pub ia: HostTensorOwned,
1249    pub ic: HostTensorOwned,
1250}
1251
1252/// Ordering applied by provider-backed `union` operations.
1253#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1254pub enum UnionOrder {
1255    Sorted,
1256    Stable,
1257}
1258
1259/// Options controlling provider-backed `union` operations.
1260#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1261pub struct UnionOptions {
1262    pub rows: bool,
1263    pub order: UnionOrder,
1264}
1265
1266/// Host-resident outputs returned by provider-backed `union` operations.
1267#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1268pub struct UnionResult {
1269    pub values: HostTensorOwned,
1270    pub ia: HostTensorOwned,
1271    pub ib: HostTensorOwned,
1272}
1273
1274/// Parameterisation of 2-D filters generated by `fspecial`.
1275#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1276pub enum FspecialFilter {
1277    Average {
1278        rows: u32,
1279        cols: u32,
1280    },
1281    Disk {
1282        radius: f64,
1283        size: u32,
1284    },
1285    Gaussian {
1286        rows: u32,
1287        cols: u32,
1288        sigma: f64,
1289    },
1290    Laplacian {
1291        alpha: f64,
1292    },
1293    Log {
1294        rows: u32,
1295        cols: u32,
1296        sigma: f64,
1297    },
1298    Motion {
1299        length: u32,
1300        kernel_size: u32,
1301        angle_degrees: f64,
1302        oversample: u32,
1303    },
1304    Prewitt,
1305    Sobel,
1306    Unsharp {
1307        alpha: f64,
1308    },
1309}
1310
1311/// Request dispatched to acceleration providers for `fspecial` kernels.
1312#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1313pub struct FspecialRequest {
1314    pub filter: FspecialFilter,
1315}
1316
1317/// Padding strategy used by `imfilter`.
1318#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1319pub enum ImfilterPadding {
1320    Constant,
1321    Replicate,
1322    Symmetric,
1323    Circular,
1324}
1325
1326/// Output sizing mode used by `imfilter`.
1327#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1328pub enum ImfilterShape {
1329    Same,
1330    Full,
1331    Valid,
1332}
1333
1334/// Correlation vs convolution behaviour for `imfilter`.
1335#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1336pub enum ImfilterMode {
1337    Correlation,
1338    Convolution,
1339}
1340
1341/// Options supplied to acceleration providers for `imfilter`.
1342#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1343pub struct ImfilterOptions {
1344    pub padding: ImfilterPadding,
1345    pub constant_value: f64,
1346    pub shape: ImfilterShape,
1347    pub mode: ImfilterMode,
1348}
1349
1350impl Default for ImfilterOptions {
1351    fn default() -> Self {
1352        Self {
1353            padding: ImfilterPadding::Constant,
1354            constant_value: 0.0,
1355            shape: ImfilterShape::Same,
1356            mode: ImfilterMode::Correlation,
1357        }
1358    }
1359}
1360
1361/// Ordering applied by provider-backed `setdiff` operations.
1362#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1363pub enum SetdiffOrder {
1364    Sorted,
1365    Stable,
1366}
1367
1368/// Options controlling provider-backed `setdiff` operations.
1369#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1370pub struct SetdiffOptions {
1371    pub rows: bool,
1372    pub order: SetdiffOrder,
1373}
1374
1375/// Host-resident outputs returned by provider-backed `setdiff` operations.
1376#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1377pub struct SetdiffResult {
1378    pub values: HostTensorOwned,
1379    pub ia: HostTensorOwned,
1380}
1381
1382/// Options controlling provider-backed `ismember` operations.
1383#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1384pub struct IsMemberOptions {
1385    pub rows: bool,
1386}
1387
1388/// Host-resident logical output returned by providers.
1389#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1390pub struct HostLogicalOwned {
1391    pub data: Vec<u8>,
1392    pub shape: Vec<usize>,
1393}
1394
1395/// Host-resident outputs returned by provider-backed `ismember` operations.
1396#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1397pub struct IsMemberResult {
1398    pub mask: HostLogicalOwned,
1399    pub loc: HostTensorOwned,
1400}
1401
1402#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1403pub enum ProviderConvMode {
1404    Full,
1405    Same,
1406    Valid,
1407}
1408
1409#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1410pub enum ProviderConvOrientation {
1411    Row,
1412    Column,
1413}
1414
1415#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1416pub struct ProviderConv1dOptions {
1417    pub mode: ProviderConvMode,
1418    pub orientation: ProviderConvOrientation,
1419}
1420
1421#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1422pub struct ProviderIirFilterOptions {
1423    /// Zero-based dimension along which filtering should be applied.
1424    pub dim: usize,
1425    /// Optional initial conditions (state vector) residing on the device.
1426    pub zi: Option<GpuTensorHandle>,
1427    /// Caller has already validated that `a` is the scalar denominator `[1]`.
1428    ///
1429    /// Providers may use this FIR-only path to avoid reading coefficient buffers
1430    /// back to the host for normalization.
1431    pub unit_denominator: bool,
1432}
1433
1434#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1435pub struct ProviderIirFilterResult {
1436    /// Filtered output tensor, matching the input signal shape.
1437    pub output: GpuTensorHandle,
1438    /// Final conditions for the filter state (same shape as the requested `zi` layout).
1439    pub final_state: Option<GpuTensorHandle>,
1440}
1441
1442#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1443pub struct ProviderMoments2 {
1444    pub mean: GpuTensorHandle,
1445    pub ex2: GpuTensorHandle,
1446}
1447
1448#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1449pub struct ProviderDispatchStats {
1450    /// Number of GPU dispatches recorded for this category.
1451    pub count: u64,
1452    /// Accumulated wall-clock time of dispatches in nanoseconds (host measured).
1453    pub total_wall_time_ns: u64,
1454}
1455
1456#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1457pub struct ProviderFallbackStat {
1458    pub reason: String,
1459    pub count: u64,
1460}
1461
1462#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1463pub struct ProviderTelemetry {
1464    pub fused_elementwise: ProviderDispatchStats,
1465    pub fused_reduction: ProviderDispatchStats,
1466    pub matmul: ProviderDispatchStats,
1467    pub linsolve: ProviderDispatchStats,
1468    pub mldivide: ProviderDispatchStats,
1469    pub mrdivide: ProviderDispatchStats,
1470    pub upload_bytes: u64,
1471    pub download_bytes: u64,
1472    pub solve_fallbacks: Vec<ProviderFallbackStat>,
1473    pub fusion_cache_hits: u64,
1474    pub fusion_cache_misses: u64,
1475    pub bind_group_cache_hits: u64,
1476    pub bind_group_cache_misses: u64,
1477    /// Optional per-layout bind group cache counters (layout tags and their hit/miss counts)
1478    pub bind_group_cache_by_layout: Option<Vec<BindGroupLayoutTelemetry>>,
1479    /// Recent kernel launch metadata (bounded log; newest last)
1480    pub kernel_launches: Vec<KernelLaunchTelemetry>,
1481}
1482
1483#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1484pub struct BindGroupLayoutTelemetry {
1485    pub tag: String,
1486    pub hits: u64,
1487    pub misses: u64,
1488}
1489
1490#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1491pub struct KernelAttrTelemetry {
1492    pub key: String,
1493    pub value: u64,
1494}
1495
1496#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1497pub struct KernelLaunchTelemetry {
1498    pub kernel: String,
1499    pub precision: Option<String>,
1500    pub shape: Vec<KernelAttrTelemetry>,
1501    pub tuning: Vec<KernelAttrTelemetry>,
1502}
1503
1504pub type AccelProviderFuture<'a, T> = Pin<Box<dyn Future<Output = anyhow::Result<T>> + 'a>>;
1505pub type AccelDownloadFuture<'a> = AccelProviderFuture<'a, crate::HostTensorOwned>;
1506pub type AccelIntegerDownloadFuture<'a> = AccelProviderFuture<'a, crate::HostIntegerTensorOwned>;
1507pub type AccelNumericDownloadFuture<'a> = AccelProviderFuture<'a, crate::HostNumericTensorOwned>;
1508
1509fn unsupported_future<T>(message: &'static str) -> AccelProviderFuture<'static, T> {
1510    Box::pin(async move { Err(anyhow::anyhow!(message)) })
1511}
1512
1513/// Device/provider interface that backends implement and register into the runtime layer
1514pub trait AccelProvider: Send + Sync {
1515    fn upload(&self, host: &crate::HostTensorView) -> anyhow::Result<GpuTensorHandle>;
1516    fn download<'a>(&'a self, h: &'a GpuTensorHandle) -> AccelDownloadFuture<'a>;
1517
1518    /// Upload one native numeric payload through the shared all-class transfer
1519    /// contract. Providers should override this method as they adopt native
1520    /// single and complex integer storage. The default adapter preserves exact
1521    /// existing double and real-integer routes and rejects every representation
1522    /// those routes cannot express.
1523    fn upload_numeric(
1524        &self,
1525        host: &crate::HostNumericTensorView,
1526    ) -> anyhow::Result<GpuTensorHandle> {
1527        host.validate()?;
1528        let handle = match (host.data, host.storage) {
1529            (crate::HostNumericDataView::F64(data), GpuTensorStorage::Real) => {
1530                self.upload(&crate::HostTensorView {
1531                    data,
1532                    shape: host.shape,
1533                })
1534            }
1535            (data, GpuTensorStorage::Real) if data.element_type().integer_type().is_some() => {
1536                let data = match data {
1537                    crate::HostNumericDataView::I8(data) => crate::HostIntegerDataView::I8(data),
1538                    crate::HostNumericDataView::I16(data) => crate::HostIntegerDataView::I16(data),
1539                    crate::HostNumericDataView::I32(data) => crate::HostIntegerDataView::I32(data),
1540                    crate::HostNumericDataView::I64(data) => crate::HostIntegerDataView::I64(data),
1541                    crate::HostNumericDataView::U8(data) => crate::HostIntegerDataView::U8(data),
1542                    crate::HostNumericDataView::U16(data) => crate::HostIntegerDataView::U16(data),
1543                    crate::HostNumericDataView::U32(data) => crate::HostIntegerDataView::U32(data),
1544                    crate::HostNumericDataView::U64(data) => crate::HostIntegerDataView::U64(data),
1545                    crate::HostNumericDataView::F64(_) | crate::HostNumericDataView::F32(_) => {
1546                        unreachable!("guarded integer numeric transfer")
1547                    }
1548                };
1549                self.upload_integer(&crate::HostIntegerTensorView {
1550                    data,
1551                    shape: host.shape,
1552                })
1553            }
1554            (data, storage) => Err(anyhow!(
1555                "provider must implement native {:?} {:?} uploads through upload_numeric",
1556                data.element_type(),
1557                storage
1558            )),
1559        }?;
1560        let expected_element = host.data.element_type();
1561        if handle.shape != host.shape
1562            || handle.descriptor.element_type != Some(expected_element)
1563            || handle.descriptor.storage != Some(host.storage)
1564        {
1565            let _ = self.free(&handle);
1566            return Err(anyhow!(
1567                "provider returned an invalid numeric upload descriptor: expected shape {:?}, element {:?}, storage {:?}; got shape {:?}, element {:?}, storage {:?}",
1568                host.shape,
1569                expected_element,
1570                host.storage,
1571                handle.shape,
1572                handle.descriptor.element_type,
1573                handle.descriptor.storage
1574            ));
1575        }
1576        Ok(handle)
1577    }
1578
1579    /// Download one native numeric payload through the shared all-class
1580    /// transfer contract. The default adapter keeps exact real-integer and
1581    /// native-double downloads available while refusing to present widened
1582    /// single data as native `f32`.
1583    fn download_numeric<'a>(&'a self, h: &'a GpuTensorHandle) -> AccelNumericDownloadFuture<'a> {
1584        Box::pin(async move {
1585            let element_type = h.descriptor.element_type.ok_or_else(|| {
1586                anyhow!("numeric handle is missing its durable element descriptor")
1587            })?;
1588            let storage = h.descriptor.storage.ok_or_else(|| {
1589                anyhow!("numeric handle is missing its durable storage descriptor")
1590            })?;
1591            if element_type.integer_type().is_some() {
1592                if storage != GpuTensorStorage::Real {
1593                    return Err(anyhow!(
1594                        "provider must implement native complex integer downloads through download_numeric"
1595                    ));
1596                }
1597                let downloaded = self.download_integer(h).await?;
1598                let numeric: crate::HostNumericTensorOwned = downloaded.into();
1599                numeric.validate()?;
1600                return Ok(numeric);
1601            }
1602            if element_type == NumericElementType::F32 {
1603                return Err(anyhow!(
1604                    "provider must implement native single downloads through download_numeric"
1605                ));
1606            }
1607            let downloaded = self.download(h).await?;
1608            let numeric = crate::HostNumericTensorOwned {
1609                data: crate::HostNumericDataOwned::F64(downloaded.data),
1610                shape: downloaded.shape,
1611                storage: downloaded.storage,
1612            };
1613            numeric.validate()?;
1614            Ok(numeric)
1615        })
1616    }
1617
1618    /// Upload exact native integer storage without converting through f64.
1619    /// Providers that do not expose native integer buffers must reject this
1620    /// operation rather than silently changing the value class or precision.
1621    fn upload_integer(
1622        &self,
1623        _host: &crate::HostIntegerTensorView,
1624    ) -> anyhow::Result<GpuTensorHandle> {
1625        Err(anyhow::anyhow!(
1626            "provider does not support exact native integer buffers"
1627        ))
1628    }
1629
1630    /// Download exact native integer storage without converting through f64.
1631    fn download_integer<'a>(&'a self, _h: &'a GpuTensorHandle) -> AccelIntegerDownloadFuture<'a> {
1632        Box::pin(async {
1633            Err(anyhow::anyhow!(
1634                "provider does not support exact native integer buffers"
1635            ))
1636        })
1637    }
1638
1639    fn free(&self, h: &GpuTensorHandle) -> anyhow::Result<()>;
1640    fn device_info(&self) -> String;
1641    /// Returns the stable identifier used to route this provider's handles.
1642    /// Distinct concurrently registered provider instances must return distinct
1643    /// identifiers; reusing an identifier would make handle ownership ambiguous.
1644    fn device_id(&self) -> u32 {
1645        0
1646    }
1647
1648    /// Declares provider policy for sharing `GpuTensorHandle` values across
1649    /// spawned async boundaries.
1650    ///
1651    /// Default is conservative rejection. Providers that can safely support
1652    /// cross-task sharing should override this.
1653    fn spawn_handle_concurrency(&self) -> SpawnHandleConcurrency {
1654        SpawnHandleConcurrency::Reject
1655    }
1656
1657    /// Returns a versioned, side-effect-free description of the provider
1658    /// surfaces placement may consider. The default advertises only the
1659    /// mandatory floating-point transfer boundary.
1660    fn capability_snapshot(&self) -> ProviderCapabilitySnapshot {
1661        ProviderCapabilitySnapshot::conservative(self)
1662    }
1663
1664    /// Determines whether a representation-specific operation can execute
1665    /// without attempting allocation, compilation, transfer, or dispatch.
1666    fn query_feasibility(&self, query: &ProviderFeasibilityQuery) -> ProviderFeasibility {
1667        query.conservative_transfer_feasibility(self.precision())
1668    }
1669
1670    /// Returns a side-effect-free component cost estimate for an already
1671    /// feasible operation. Providers may return `None` until they have a
1672    /// trustworthy prior or observation; placement then supplies a bounded
1673    /// low-confidence policy prior rather than probing by execution.
1674    fn estimate_cost(&self, _query: &ProviderCostQuery) -> Option<ProviderCostEstimate> {
1675        None
1676    }
1677
1678    /// Returns a side-effect-free resource snapshot for placement admission.
1679    /// Providers with allocator/queue telemetry should override this default;
1680    /// the conservative snapshot exposes declared capacity when available,
1681    /// leaves unknown queue state explicit, and never probes the device.
1682    fn placement_resources(&self) -> runmat_execution::ProviderResourceSnapshot {
1683        let info = self.device_info_struct();
1684        let capacity = info.memory_bytes;
1685        runmat_execution::ProviderResourceSnapshot {
1686            device_id: self.device_id(),
1687            capacity_bytes: capacity,
1688            live_bytes: 0,
1689            reclaimable_bytes: 0,
1690            scratch_available_bytes: capacity,
1691            queue_depth: None,
1692            queue_limit: None,
1693            lost: false,
1694            epoch: 0,
1695        }
1696    }
1697
1698    /// Export a shared GPU context handle, allowing downstream systems (plotting, visualization)
1699    /// to reuse the same device/queue without copying tensor data back to the host.
1700    fn export_context(&self, _kind: AccelContextKind) -> Option<AccelContextHandle> {
1701        None
1702    }
1703
1704    /// Export a provider-owned WGPU buffer for zero-copy integrations.
1705    #[cfg(feature = "wgpu")]
1706    fn export_wgpu_buffer(&self, _handle: &GpuTensorHandle) -> Option<WgpuBufferRef> {
1707        let _ = _handle;
1708        None
1709    }
1710
1711    /// Gather logical elements from `source` at the provided zero-based linear `indices`,
1712    /// materialising a dense tensor with the specified `output_shape`.
1713    ///
1714    /// Indices address MATLAB logical elements, not raw provider storage lanes. Providers that
1715    /// store complex values as interleaved lanes must copy both lanes for each selected element
1716    /// and preserve the source storage kind on the output handle.
1717    fn gather_linear(
1718        &self,
1719        _source: &GpuTensorHandle,
1720        _indices: &[u32],
1721        _output_shape: &[usize],
1722    ) -> anyhow::Result<GpuTensorHandle> {
1723        Err(anyhow::anyhow!("gather_linear not supported by provider"))
1724    }
1725
1726    /// Scatter logical elements from `values` into `target` at the provided zero-based linear
1727    /// `indices`.
1728    ///
1729    /// Indices address MATLAB logical elements, not raw provider storage lanes. Providers must
1730    /// ensure `values` has one logical element per index, copy all lanes for the handle storage
1731    /// kind, and update `target` in place without changing its shape or storage.
1732    fn scatter_linear(
1733        &self,
1734        _target: &GpuTensorHandle,
1735        _indices: &[u32],
1736        _values: &GpuTensorHandle,
1737    ) -> anyhow::Result<()> {
1738        Err(anyhow::anyhow!("scatter_linear not supported by provider"))
1739    }
1740
1741    /// Structured device information (optional to override). Default adapts from `device_info()`.
1742    fn device_info_struct(&self) -> ApiDeviceInfo {
1743        ApiDeviceInfo {
1744            device_id: 0,
1745            name: self.device_info(),
1746            vendor: String::new(),
1747            memory_bytes: None,
1748            backend: None,
1749        }
1750    }
1751
1752    fn precision(&self) -> ProviderPrecision {
1753        ProviderPrecision::F64
1754    }
1755
1756    /// Read a single scalar at linear index from a device tensor, returning it as f64.
1757    fn read_scalar(&self, _h: &GpuTensorHandle, _linear_index: usize) -> anyhow::Result<f64> {
1758        Err(anyhow::anyhow!("read_scalar not supported by provider"))
1759    }
1760
1761    /// Allocate a zero-initialised tensor with the provided shape on the device.
1762    fn zeros(&self, _shape: &[usize]) -> anyhow::Result<GpuTensorHandle> {
1763        Err(anyhow::anyhow!("zeros not supported by provider"))
1764    }
1765
1766    /// Allocate a zero-initialised tensor with the provided shape and storage layout.
1767    ///
1768    /// `shape` is the logical MATLAB shape. Providers must allocate enough raw lanes for the
1769    /// requested storage kind, e.g. two interleaved numeric lanes per logical element for complex
1770    /// tensors.
1771    fn zeros_with_storage(
1772        &self,
1773        shape: &[usize],
1774        storage: GpuTensorStorage,
1775    ) -> anyhow::Result<GpuTensorHandle> {
1776        if storage == GpuTensorStorage::Real {
1777            self.zeros(shape)
1778        } else {
1779            Err(anyhow::anyhow!(
1780                "zeros_with_storage not supported by provider for {storage:?}"
1781            ))
1782        }
1783    }
1784
1785    /// Allocate a one-initialised tensor with the provided shape on the device.
1786    fn ones(&self, _shape: &[usize]) -> anyhow::Result<GpuTensorHandle> {
1787        Err(anyhow::anyhow!("ones not supported by provider"))
1788    }
1789
1790    /// Allocate a zero-initialised tensor matching the prototype tensor.
1791    fn zeros_like(&self, prototype: &GpuTensorHandle) -> anyhow::Result<GpuTensorHandle> {
1792        self.zeros(&prototype.shape)
1793    }
1794
1795    /// Allocate an exact native-integer zero buffer using the element class of
1796    /// `prototype`. Implementations must reject non-native-integer prototypes
1797    /// rather than allocating a floating compatibility buffer.
1798    fn zeros_integer_like(
1799        &self,
1800        _prototype: &GpuTensorHandle,
1801        _shape: &[usize],
1802    ) -> anyhow::Result<GpuTensorHandle> {
1803        Err(anyhow::anyhow!(
1804            "zeros_integer_like not supported by provider"
1805        ))
1806    }
1807
1808    /// Allocate a tensor filled with a constant value on the device.
1809    fn fill(&self, shape: &[usize], value: f64) -> anyhow::Result<GpuTensorHandle> {
1810        if value == 0.0 {
1811            return self.zeros(shape);
1812        }
1813        if let Ok(base) = self.zeros(shape) {
1814            match self.scalar_add(&base, value) {
1815                Ok(out) => {
1816                    let _ = self.free(&base);
1817                    return Ok(out);
1818                }
1819                Err(_) => {
1820                    let _ = self.free(&base);
1821                }
1822            }
1823        }
1824        let len: usize = shape.iter().copied().product();
1825        let data = vec![value; len];
1826        let view = HostTensorView { data: &data, shape };
1827        self.upload(&view)
1828    }
1829
1830    /// Allocate a tensor filled with a constant value, matching a prototype's residency.
1831    fn fill_like(
1832        &self,
1833        prototype: &GpuTensorHandle,
1834        value: f64,
1835    ) -> anyhow::Result<GpuTensorHandle> {
1836        if value == 0.0 {
1837            return self.zeros_like(prototype);
1838        }
1839        if let Ok(base) = self.zeros_like(prototype) {
1840            match self.scalar_add(&base, value) {
1841                Ok(out) => {
1842                    let _ = self.free(&base);
1843                    return Ok(out);
1844                }
1845                Err(_) => {
1846                    let _ = self.free(&base);
1847                }
1848            }
1849        }
1850        self.fill(&prototype.shape, value)
1851    }
1852
1853    /// Allocate a one-initialised tensor matching the prototype tensor.
1854    fn ones_like(&self, prototype: &GpuTensorHandle) -> anyhow::Result<GpuTensorHandle> {
1855        self.ones(&prototype.shape)
1856    }
1857
1858    /// Allocate an identity tensor with ones along the leading diagonal of the first two axes.
1859    fn eye(&self, _shape: &[usize]) -> anyhow::Result<GpuTensorHandle> {
1860        Err(anyhow::anyhow!("eye not supported by provider"))
1861    }
1862
1863    /// Allocate an identity tensor matching the prototype tensor's shape.
1864    fn eye_like(&self, prototype: &GpuTensorHandle) -> anyhow::Result<GpuTensorHandle> {
1865        self.eye(&prototype.shape)
1866    }
1867
1868    /// Construct MATLAB-style coordinate grids from axis vectors.
1869    fn meshgrid(&self, _axes: &[MeshgridAxisView<'_>]) -> anyhow::Result<ProviderMeshgridResult> {
1870        Err(anyhow::anyhow!("meshgrid not supported by provider"))
1871    }
1872
1873    /// Construct MATLAB-style N-D coordinate grids from resident GPU axis vectors.
1874    fn ndgrid(&self, _request: &ProviderNdgridRequest<'_>) -> anyhow::Result<ProviderNdgridResult> {
1875        Err(anyhow::anyhow!("ndgrid not supported by provider"))
1876    }
1877
1878    /// Compute vectorized Black-Scholes European call and put prices on resident GPU inputs.
1879    fn black_scholes_price(
1880        &self,
1881        _request: &ProviderBlackScholesPriceRequest<'_>,
1882    ) -> anyhow::Result<ProviderBlackScholesPriceResult> {
1883        Err(anyhow::anyhow!(
1884            "black_scholes_price not supported by provider"
1885        ))
1886    }
1887
1888    /// Apply the Adam optimizer update to resident parameter and state tensors.
1889    fn adam_update(
1890        &self,
1891        _request: &ProviderAdamUpdateRequest<'_>,
1892    ) -> anyhow::Result<ProviderAdamUpdateResult> {
1893        Err(anyhow::anyhow!("adam_update not supported by provider"))
1894    }
1895
1896    /// Compute per-element cross-entropy loss terms for resident prediction and target tensors.
1897    fn crossentropy_terms(
1898        &self,
1899        _request: &ProviderCrossentropyRequest<'_>,
1900    ) -> anyhow::Result<ProviderCrossentropyResult> {
1901        Err(anyhow::anyhow!(
1902            "crossentropy_terms not supported by provider"
1903        ))
1904    }
1905
1906    /// Construct a diagonal matrix from a vector-like tensor. `offset` matches MATLAB semantics.
1907    fn diag_from_vector(
1908        &self,
1909        _vector: &GpuTensorHandle,
1910        _offset: isize,
1911    ) -> anyhow::Result<GpuTensorHandle> {
1912        Err(anyhow::anyhow!(
1913            "diag_from_vector not supported by provider"
1914        ))
1915    }
1916
1917    /// Construct a diagonal matrix with an explicit shape from a vector-like tensor.
1918    /// `offset` matches MATLAB semantics; values that do not fit inside the requested
1919    /// rectangle are ignored.
1920    fn diag_from_vector_sized(
1921        &self,
1922        _vector: &GpuTensorHandle,
1923        _offset: isize,
1924        _rows: usize,
1925        _cols: usize,
1926    ) -> anyhow::Result<GpuTensorHandle> {
1927        Err(anyhow::anyhow!(
1928            "diag_from_vector_sized not supported by provider"
1929        ))
1930    }
1931
1932    /// Extract a diagonal from a matrix-like tensor. The result is always a column vector.
1933    fn diag_extract(
1934        &self,
1935        _matrix: &GpuTensorHandle,
1936        _offset: isize,
1937    ) -> anyhow::Result<GpuTensorHandle> {
1938        Err(anyhow::anyhow!("diag_extract not supported by provider"))
1939    }
1940
1941    /// Apply a lower-triangular mask to the first two dimensions of a tensor.
1942    fn tril<'a>(
1943        &'a self,
1944        _matrix: &'a GpuTensorHandle,
1945        _offset: isize,
1946    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
1947        Box::pin(async move { Err(anyhow!("tril not supported by provider")) })
1948    }
1949
1950    /// Apply an upper-triangular mask to the first two dimensions of a tensor.
1951    fn triu<'a>(
1952        &'a self,
1953        _matrix: &'a GpuTensorHandle,
1954        _offset: isize,
1955    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
1956        Box::pin(async move { Err(anyhow!("triu not supported by provider")) })
1957    }
1958
1959    /// Evaluate a polynomial expressed by `coefficients` at each element in `points`.
1960    fn polyval(
1961        &self,
1962        _coefficients: &GpuTensorHandle,
1963        _points: &GpuTensorHandle,
1964        _options: &ProviderPolyvalOptions,
1965    ) -> anyhow::Result<GpuTensorHandle> {
1966        Err(anyhow::anyhow!("polyval not supported by provider"))
1967    }
1968
1969    /// Fit a polynomial of degree `degree` to `(x, y)` samples. Optional weights must match `x`.
1970    fn polyfit<'a>(
1971        &'a self,
1972        _x: &'a GpuTensorHandle,
1973        _y: &'a GpuTensorHandle,
1974        _degree: usize,
1975        _weights: Option<&'a GpuTensorHandle>,
1976    ) -> AccelProviderFuture<'a, ProviderPolyfitResult> {
1977        Box::pin(async move { Err(anyhow::anyhow!("polyfit not supported by provider")) })
1978    }
1979
1980    /// Differentiate a polynomial represented as a vector of coefficients.
1981    fn polyder_single<'a>(
1982        &'a self,
1983        _polynomial: &'a GpuTensorHandle,
1984    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
1985        Box::pin(async move { Err(anyhow::anyhow!("polyder_single not supported by provider")) })
1986    }
1987
1988    /// Apply the product rule to polynomials `p` and `q`.
1989    fn polyder_product<'a>(
1990        &'a self,
1991        _p: &'a GpuTensorHandle,
1992        _q: &'a GpuTensorHandle,
1993    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
1994        Box::pin(async move { Err(anyhow::anyhow!("polyder_product not supported by provider")) })
1995    }
1996
1997    /// Apply the quotient rule to polynomials `u` and `v`.
1998    fn polyder_quotient<'a>(
1999        &'a self,
2000        _u: &'a GpuTensorHandle,
2001        _v: &'a GpuTensorHandle,
2002    ) -> AccelProviderFuture<'a, ProviderPolyderQuotient> {
2003        Box::pin(async move {
2004            Err(anyhow::anyhow!(
2005                "polyder_quotient not supported by provider"
2006            ))
2007        })
2008    }
2009
2010    /// Integrate a polynomial represented as a vector of coefficients and append a constant term.
2011    fn polyint(
2012        &self,
2013        _polynomial: &GpuTensorHandle,
2014        _constant: f64,
2015    ) -> anyhow::Result<GpuTensorHandle> {
2016        Err(anyhow::anyhow!("polyint not supported by provider"))
2017    }
2018
2019    /// Allocate a tensor filled with random values drawn from U(0, 1).
2020    fn random_uniform(&self, _shape: &[usize]) -> anyhow::Result<GpuTensorHandle> {
2021        Err(anyhow::anyhow!("random_uniform not supported by provider"))
2022    }
2023
2024    /// Allocate a tensor filled with random values matching the prototype shape.
2025    fn random_uniform_like(&self, prototype: &GpuTensorHandle) -> anyhow::Result<GpuTensorHandle> {
2026        self.random_uniform(&prototype.shape)
2027    }
2028
2029    /// Allocate a tensor filled with standard normal (mean 0, stddev 1) random values.
2030    fn random_normal(&self, _shape: &[usize]) -> anyhow::Result<GpuTensorHandle> {
2031        Err(anyhow::anyhow!("random_normal not supported by provider"))
2032    }
2033
2034    /// Allocate a tensor of standard normal values matching a prototype's shape.
2035    fn random_normal_like(&self, prototype: &GpuTensorHandle) -> anyhow::Result<GpuTensorHandle> {
2036        self.random_normal(&prototype.shape)
2037    }
2038
2039    /// Exponentially-distributed random values with mean `mu`.
2040    fn random_exponential(&self, _mu: f64, _shape: &[usize]) -> anyhow::Result<GpuTensorHandle> {
2041        Err(anyhow::anyhow!(
2042            "random_exponential not supported by provider"
2043        ))
2044    }
2045
2046    /// Normal random values with mean `mu` and standard deviation `sigma`.
2047    fn random_normrnd(
2048        &self,
2049        _mu: f64,
2050        _sigma: f64,
2051        _shape: &[usize],
2052    ) -> anyhow::Result<GpuTensorHandle> {
2053        Err(anyhow::anyhow!("random_normrnd not supported by provider"))
2054    }
2055
2056    /// Uniform random values on the interval `[a, b)`.
2057    fn random_unifrnd(
2058        &self,
2059        _a: f64,
2060        _b: f64,
2061        _shape: &[usize],
2062    ) -> anyhow::Result<GpuTensorHandle> {
2063        Err(anyhow::anyhow!("random_unifrnd not supported by provider"))
2064    }
2065
2066    fn stochastic_evolution(
2067        &self,
2068        _state: &GpuTensorHandle,
2069        _drift: f64,
2070        _scale: f64,
2071        _steps: u32,
2072    ) -> anyhow::Result<GpuTensorHandle> {
2073        Err(anyhow::anyhow!(
2074            "stochastic_evolution not supported by provider"
2075        ))
2076    }
2077
2078    /// Set the provider RNG state to align with the host RNG.
2079    fn set_rng_state(&self, _state: u64) -> anyhow::Result<()> {
2080        Err(anyhow::anyhow!("set_rng_state not supported by provider"))
2081    }
2082
2083    /// Generate a 2-D correlation kernel matching MATLAB's `fspecial` builtin.
2084    fn fspecial(&self, _request: &FspecialRequest) -> anyhow::Result<GpuTensorHandle> {
2085        Err(anyhow::anyhow!("fspecial not supported by provider"))
2086    }
2087
2088    /// Evaluate the `peaks` test surface on an n×n grid spanning [-3,3]×[-3,3].
2089    /// Returns the Z matrix (n×n) as a GPU tensor.
2090    fn peaks(&self, _n: usize) -> anyhow::Result<GpuTensorHandle> {
2091        Err(anyhow::anyhow!("peaks not supported by provider"))
2092    }
2093
2094    /// Evaluate the `peaks` formula element-wise on caller-supplied GPU coordinate tensors.
2095    /// X and Y must have the same shape. Returns a Z tensor of the same shape.
2096    fn peaks_xy(
2097        &self,
2098        _x: &GpuTensorHandle,
2099        _y: &GpuTensorHandle,
2100    ) -> anyhow::Result<GpuTensorHandle> {
2101        Err(anyhow::anyhow!("peaks_xy not supported by provider"))
2102    }
2103
2104    fn hann_window(&self, _len: usize, _periodic: bool) -> anyhow::Result<GpuTensorHandle> {
2105        Err(anyhow::anyhow!("hann_window not supported by provider"))
2106    }
2107
2108    fn hamming_window(&self, _len: usize, _periodic: bool) -> anyhow::Result<GpuTensorHandle> {
2109        Err(anyhow::anyhow!("hamming_window not supported by provider"))
2110    }
2111
2112    fn blackman_window(&self, _len: usize, _periodic: bool) -> anyhow::Result<GpuTensorHandle> {
2113        Err(anyhow::anyhow!("blackman_window not supported by provider"))
2114    }
2115
2116    /// Apply an N-D correlation/convolution with padding semantics matching MATLAB's `imfilter`.
2117    fn imfilter<'a>(
2118        &'a self,
2119        _image: &'a GpuTensorHandle,
2120        _kernel: &'a GpuTensorHandle,
2121        _options: &'a ImfilterOptions,
2122    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2123        unsupported_future("imfilter not supported by provider")
2124    }
2125
2126    /// Allocate a random permutation of 1..=n, returning the first k elements.
2127    fn random_permutation(&self, _n: usize, _k: usize) -> anyhow::Result<GpuTensorHandle> {
2128        Err(anyhow!("random_permutation not supported by provider"))
2129    }
2130
2131    /// Allocate a random permutation matching the prototype residency.
2132    fn random_permutation_like(
2133        &self,
2134        _prototype: &GpuTensorHandle,
2135        n: usize,
2136        k: usize,
2137    ) -> anyhow::Result<GpuTensorHandle> {
2138        self.random_permutation(n, k)
2139    }
2140
2141    /// Compute a covariance matrix across the columns of `matrix`.
2142    fn covariance<'a>(
2143        &'a self,
2144        _matrix: &'a GpuTensorHandle,
2145        _second: Option<&'a GpuTensorHandle>,
2146        _weights: Option<&'a GpuTensorHandle>,
2147        _options: &'a CovarianceOptions,
2148    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2149        unsupported_future("covariance not supported by provider")
2150    }
2151
2152    /// Compute a correlation coefficient matrix across the columns of `matrix`.
2153    fn corrcoef<'a>(
2154        &'a self,
2155        _matrix: &'a GpuTensorHandle,
2156        _options: &'a CorrcoefOptions,
2157    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2158        unsupported_future("corrcoef not supported by provider")
2159    }
2160
2161    /// Convert a resident covariance matrix into correlation and sigma outputs.
2162    fn covariance_to_correlation(
2163        &self,
2164        _matrix: &GpuTensorHandle,
2165    ) -> anyhow::Result<ProviderCovarianceToCorrelationResult> {
2166        Err(anyhow::anyhow!(
2167            "covariance_to_correlation not supported by provider"
2168        ))
2169    }
2170
2171    // Optional operator hooks (default to unsupported)
2172    fn linspace(&self, _start: f64, _stop: f64, _count: usize) -> anyhow::Result<GpuTensorHandle> {
2173        Err(anyhow::anyhow!("linspace not supported by provider"))
2174    }
2175    fn elem_add<'a>(
2176        &'a self,
2177        _a: &'a GpuTensorHandle,
2178        _b: &'a GpuTensorHandle,
2179    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2180        unsupported_future("elem_add not supported by provider")
2181    }
2182    fn elem_mul<'a>(
2183        &'a self,
2184        _a: &'a GpuTensorHandle,
2185        _b: &'a GpuTensorHandle,
2186    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2187        unsupported_future("elem_mul not supported by provider")
2188    }
2189    fn elem_max<'a>(
2190        &'a self,
2191        _a: &'a GpuTensorHandle,
2192        _b: &'a GpuTensorHandle,
2193    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2194        unsupported_future("elem_max not supported by provider")
2195    }
2196    fn elem_min<'a>(
2197        &'a self,
2198        _a: &'a GpuTensorHandle,
2199        _b: &'a GpuTensorHandle,
2200    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2201        unsupported_future("elem_min not supported by provider")
2202    }
2203    fn elem_sub<'a>(
2204        &'a self,
2205        _a: &'a GpuTensorHandle,
2206        _b: &'a GpuTensorHandle,
2207    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2208        unsupported_future("elem_sub not supported by provider")
2209    }
2210    fn elem_div<'a>(
2211        &'a self,
2212        _a: &'a GpuTensorHandle,
2213        _b: &'a GpuTensorHandle,
2214    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2215        unsupported_future("elem_div not supported by provider")
2216    }
2217    /// Compute an exact MATLAB-style remainder for compatible native integer tensors.
2218    fn elem_rem<'a>(
2219        &'a self,
2220        _a: &'a GpuTensorHandle,
2221        _b: &'a GpuTensorHandle,
2222    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2223        unsupported_future("elem_rem not supported by provider")
2224    }
2225    /// Compute an exact MATLAB-style modulus for compatible native integer tensors.
2226    fn elem_mod<'a>(
2227        &'a self,
2228        _a: &'a GpuTensorHandle,
2229        _b: &'a GpuTensorHandle,
2230    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2231        unsupported_future("elem_mod not supported by provider")
2232    }
2233    fn elem_pow<'a>(
2234        &'a self,
2235        _a: &'a GpuTensorHandle,
2236        _b: &'a GpuTensorHandle,
2237    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2238        unsupported_future("elem_pow not supported by provider")
2239    }
2240
2241    /// Construct complex-interleaved GPU storage from a real-valued tensor,
2242    /// using zero for the imaginary lane.
2243    fn complex_from_real<'a>(
2244        &'a self,
2245        _real: &'a GpuTensorHandle,
2246    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2247        unsupported_future("complex_from_real not supported by provider")
2248    }
2249
2250    /// Construct complex-interleaved GPU storage from real and imaginary tensors.
2251    ///
2252    /// Implementations should support equal shapes and scalar expansion for either
2253    /// operand, matching MATLAB's `complex(real, imag)` size rules.
2254    fn complex_from_real_imag<'a>(
2255        &'a self,
2256        _real: &'a GpuTensorHandle,
2257        _imag: &'a GpuTensorHandle,
2258    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2259        unsupported_future("complex_from_real_imag not supported by provider")
2260    }
2261
2262    /// Map a resident real-valued symbol tensor through a complex constellation
2263    /// table and return complex-interleaved GPU storage.
2264    fn modulate_constellation<'a>(
2265        &'a self,
2266        _request: ProviderModulationRequest<'a>,
2267    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2268        unsupported_future("modulate_constellation not supported by provider")
2269    }
2270
2271    /// Group a resident real/logical bit tensor into symbols, map through a complex
2272    /// constellation table, and return complex-interleaved GPU storage.
2273    fn modulate_bits_constellation<'a>(
2274        &'a self,
2275        _request: ProviderBitModulationRequest<'a>,
2276    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2277        unsupported_future("modulate_bits_constellation not supported by provider")
2278    }
2279
2280    fn elem_hypot<'a>(
2281        &'a self,
2282        _a: &'a GpuTensorHandle,
2283        _b: &'a GpuTensorHandle,
2284    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2285        unsupported_future("elem_hypot not supported by provider")
2286    }
2287    fn elem_ge<'a>(
2288        &'a self,
2289        _a: &'a GpuTensorHandle,
2290        _b: &'a GpuTensorHandle,
2291    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2292        unsupported_future("elem_ge not supported by provider")
2293    }
2294    fn elem_le<'a>(
2295        &'a self,
2296        _a: &'a GpuTensorHandle,
2297        _b: &'a GpuTensorHandle,
2298    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2299        unsupported_future("elem_le not supported by provider")
2300    }
2301    fn elem_lt<'a>(
2302        &'a self,
2303        _a: &'a GpuTensorHandle,
2304        _b: &'a GpuTensorHandle,
2305    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2306        unsupported_future("elem_lt not supported by provider")
2307    }
2308    fn elem_gt<'a>(
2309        &'a self,
2310        _a: &'a GpuTensorHandle,
2311        _b: &'a GpuTensorHandle,
2312    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2313        unsupported_future("elem_gt not supported by provider")
2314    }
2315    fn elem_eq<'a>(
2316        &'a self,
2317        _a: &'a GpuTensorHandle,
2318        _b: &'a GpuTensorHandle,
2319    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2320        unsupported_future("elem_eq not supported by provider")
2321    }
2322    fn elem_ne<'a>(
2323        &'a self,
2324        _a: &'a GpuTensorHandle,
2325        _b: &'a GpuTensorHandle,
2326    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2327        unsupported_future("elem_ne not supported by provider")
2328    }
2329    fn logical_and(
2330        &self,
2331        _a: &GpuTensorHandle,
2332        _b: &GpuTensorHandle,
2333    ) -> anyhow::Result<GpuTensorHandle> {
2334        Err(anyhow::anyhow!("logical_and not supported by provider"))
2335    }
2336    fn logical_or(
2337        &self,
2338        _a: &GpuTensorHandle,
2339        _b: &GpuTensorHandle,
2340    ) -> anyhow::Result<GpuTensorHandle> {
2341        Err(anyhow::anyhow!("logical_or not supported by provider"))
2342    }
2343    fn logical_xor(
2344        &self,
2345        _a: &GpuTensorHandle,
2346        _b: &GpuTensorHandle,
2347    ) -> anyhow::Result<GpuTensorHandle> {
2348        Err(anyhow::anyhow!("logical_xor not supported by provider"))
2349    }
2350    fn logical_not(&self, _a: &GpuTensorHandle) -> anyhow::Result<GpuTensorHandle> {
2351        Err(anyhow::anyhow!("logical_not not supported by provider"))
2352    }
2353    fn logical_islogical(&self, a: &GpuTensorHandle) -> anyhow::Result<bool> {
2354        Ok(handle_is_logical(a))
2355    }
2356    fn logical_isreal(&self, _a: &GpuTensorHandle) -> anyhow::Result<bool> {
2357        Err(anyhow::anyhow!("logical_isreal not supported by provider"))
2358    }
2359    fn logical_isfinite(&self, _a: &GpuTensorHandle) -> anyhow::Result<GpuTensorHandle> {
2360        Err(anyhow::anyhow!(
2361            "logical_isfinite not supported by provider"
2362        ))
2363    }
2364    fn logical_isnan(&self, _a: &GpuTensorHandle) -> anyhow::Result<GpuTensorHandle> {
2365        Err(anyhow::anyhow!("logical_isnan not supported by provider"))
2366    }
2367    fn logical_isinf(&self, _a: &GpuTensorHandle) -> anyhow::Result<GpuTensorHandle> {
2368        Err(anyhow::anyhow!("logical_isinf not supported by provider"))
2369    }
2370    fn elem_atan2<'a>(
2371        &'a self,
2372        _y: &'a GpuTensorHandle,
2373        _x: &'a GpuTensorHandle,
2374    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2375        unsupported_future("elem_atan2 not supported by provider")
2376    }
2377    // Unary elementwise operations (optional)
2378    fn unary_sin<'a>(
2379        &'a self,
2380        _a: &'a GpuTensorHandle,
2381    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2382        unsupported_future("unary_sin not supported by provider")
2383    }
2384    fn unary_sinc<'a>(
2385        &'a self,
2386        _a: &'a GpuTensorHandle,
2387    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2388        unsupported_future("unary_sinc not supported by provider")
2389    }
2390    fn unary_gamma<'a>(
2391        &'a self,
2392        _a: &'a GpuTensorHandle,
2393    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2394        unsupported_future("unary_gamma not supported by provider")
2395    }
2396    fn unary_gammaln<'a>(
2397        &'a self,
2398        _a: &'a GpuTensorHandle,
2399    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2400        unsupported_future("unary_gammaln not supported by provider")
2401    }
2402    fn unary_erf<'a>(
2403        &'a self,
2404        _a: &'a GpuTensorHandle,
2405    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2406        unsupported_future("unary_erf not supported by provider")
2407    }
2408    fn unary_erfcinv<'a>(
2409        &'a self,
2410        _a: &'a GpuTensorHandle,
2411    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2412        unsupported_future("unary_erfcinv not supported by provider")
2413    }
2414    fn unary_factorial<'a>(
2415        &'a self,
2416        _a: &'a GpuTensorHandle,
2417    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2418        unsupported_future("unary_factorial not supported by provider")
2419    }
2420    fn unary_asinh<'a>(
2421        &'a self,
2422        _a: &'a GpuTensorHandle,
2423    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2424        unsupported_future("unary_asinh not supported by provider")
2425    }
2426    fn unary_sinh<'a>(
2427        &'a self,
2428        _a: &'a GpuTensorHandle,
2429    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2430        unsupported_future("unary_sinh not supported by provider")
2431    }
2432    fn unary_cosh<'a>(
2433        &'a self,
2434        _a: &'a GpuTensorHandle,
2435    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2436        unsupported_future("unary_cosh not supported by provider")
2437    }
2438    fn unary_asin<'a>(
2439        &'a self,
2440        _a: &'a GpuTensorHandle,
2441    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2442        unsupported_future("unary_asin not supported by provider")
2443    }
2444    fn unary_acos<'a>(
2445        &'a self,
2446        _a: &'a GpuTensorHandle,
2447    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2448        unsupported_future("unary_acos not supported by provider")
2449    }
2450    fn unary_acosh<'a>(
2451        &'a self,
2452        _a: &'a GpuTensorHandle,
2453    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2454        unsupported_future("unary_acosh not supported by provider")
2455    }
2456    fn unary_tan<'a>(
2457        &'a self,
2458        _a: &'a GpuTensorHandle,
2459    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2460        unsupported_future("unary_tan not supported by provider")
2461    }
2462    fn unary_tanh<'a>(
2463        &'a self,
2464        _a: &'a GpuTensorHandle,
2465    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2466        unsupported_future("unary_tanh not supported by provider")
2467    }
2468    fn unary_atan<'a>(
2469        &'a self,
2470        _a: &'a GpuTensorHandle,
2471    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2472        unsupported_future("unary_atan not supported by provider")
2473    }
2474    fn unary_atanh<'a>(
2475        &'a self,
2476        _a: &'a GpuTensorHandle,
2477    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2478        unsupported_future("unary_atanh not supported by provider")
2479    }
2480    fn unary_ceil<'a>(
2481        &'a self,
2482        _a: &'a GpuTensorHandle,
2483    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2484        unsupported_future("unary_ceil not supported by provider")
2485    }
2486    fn unary_floor<'a>(
2487        &'a self,
2488        _a: &'a GpuTensorHandle,
2489    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2490        unsupported_future("unary_floor not supported by provider")
2491    }
2492    fn unary_round<'a>(
2493        &'a self,
2494        _a: &'a GpuTensorHandle,
2495    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2496        unsupported_future("unary_round not supported by provider")
2497    }
2498    fn round_digits<'a>(
2499        &'a self,
2500        _a: &'a GpuTensorHandle,
2501        _digits: i32,
2502        _significant: bool,
2503    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2504        unsupported_future("round_digits not supported by provider")
2505    }
2506    fn unary_fix<'a>(
2507        &'a self,
2508        _a: &'a GpuTensorHandle,
2509    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2510        unsupported_future("unary_fix not supported by provider")
2511    }
2512    fn unary_cos<'a>(
2513        &'a self,
2514        _a: &'a GpuTensorHandle,
2515    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2516        unsupported_future("unary_cos not supported by provider")
2517    }
2518    fn unary_angle<'a>(
2519        &'a self,
2520        _a: &'a GpuTensorHandle,
2521    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2522        unsupported_future("unary_angle not supported by provider")
2523    }
2524    fn unary_imag<'a>(
2525        &'a self,
2526        _a: &'a GpuTensorHandle,
2527    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2528        unsupported_future("unary_imag not supported by provider")
2529    }
2530    fn unary_real<'a>(
2531        &'a self,
2532        _a: &'a GpuTensorHandle,
2533    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2534        unsupported_future("unary_real not supported by provider")
2535    }
2536    fn unary_conj<'a>(
2537        &'a self,
2538        _a: &'a GpuTensorHandle,
2539    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2540        unsupported_future("unary_conj not supported by provider")
2541    }
2542    fn unary_abs<'a>(
2543        &'a self,
2544        _a: &'a GpuTensorHandle,
2545    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2546        unsupported_future("unary_abs not supported by provider")
2547    }
2548    fn unary_sign<'a>(
2549        &'a self,
2550        _a: &'a GpuTensorHandle,
2551    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2552        unsupported_future("unary_sign not supported by provider")
2553    }
2554    fn unary_heaviside<'a>(
2555        &'a self,
2556        _a: &'a GpuTensorHandle,
2557    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2558        unsupported_future("unary_heaviside not supported by provider")
2559    }
2560    fn unary_exp<'a>(
2561        &'a self,
2562        _a: &'a GpuTensorHandle,
2563    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2564        unsupported_future("unary_exp not supported by provider")
2565    }
2566    /// Apply an exponential linear unit activation with the supplied alpha.
2567    fn activation_elu<'a>(
2568        &'a self,
2569        _a: &'a GpuTensorHandle,
2570        _alpha: f64,
2571    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2572        unsupported_future("activation_elu not supported by provider")
2573    }
2574    /// Normalize each row of a real two-dimensional tensor with stable softmax.
2575    fn activation_softmax_rows<'a>(
2576        &'a self,
2577        _a: &'a GpuTensorHandle,
2578    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2579        unsupported_future("activation_softmax_rows not supported by provider")
2580    }
2581    fn unary_expm1<'a>(
2582        &'a self,
2583        _a: &'a GpuTensorHandle,
2584    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2585        unsupported_future("unary_expm1 not supported by provider")
2586    }
2587    fn unary_log<'a>(
2588        &'a self,
2589        _a: &'a GpuTensorHandle,
2590    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2591        unsupported_future("unary_log not supported by provider")
2592    }
2593    fn unary_log2<'a>(
2594        &'a self,
2595        _a: &'a GpuTensorHandle,
2596    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2597        unsupported_future("unary_log2 not supported by provider")
2598    }
2599    fn unary_log10<'a>(
2600        &'a self,
2601        _a: &'a GpuTensorHandle,
2602    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2603        unsupported_future("unary_log10 not supported by provider")
2604    }
2605    fn unary_log1p<'a>(
2606        &'a self,
2607        _a: &'a GpuTensorHandle,
2608    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2609        unsupported_future("unary_log1p not supported by provider")
2610    }
2611    fn unary_sqrt<'a>(
2612        &'a self,
2613        _a: &'a GpuTensorHandle,
2614    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2615        unsupported_future("unary_sqrt not supported by provider")
2616    }
2617    fn unary_double<'a>(
2618        &'a self,
2619        _a: &'a GpuTensorHandle,
2620    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2621        unsupported_future("unary_double not supported by provider")
2622    }
2623    fn unary_single<'a>(
2624        &'a self,
2625        _a: &'a GpuTensorHandle,
2626    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2627        unsupported_future("unary_single not supported by provider")
2628    }
2629    fn unary_pow2<'a>(
2630        &'a self,
2631        _a: &'a GpuTensorHandle,
2632    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2633        unsupported_future("unary_pow2 not supported by provider")
2634    }
2635    fn unary_nextpow2<'a>(
2636        &'a self,
2637        _a: &'a GpuTensorHandle,
2638    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2639        unsupported_future("unary_nextpow2 not supported by provider")
2640    }
2641    fn pow2_scale(
2642        &self,
2643        _mantissa: &GpuTensorHandle,
2644        _exponent: &GpuTensorHandle,
2645    ) -> anyhow::Result<GpuTensorHandle> {
2646        Err(anyhow::anyhow!("pow2_scale not supported by provider"))
2647    }
2648    // Left-scalar operations (broadcast with scalar on the left)
2649    fn scalar_rsub(&self, _a: &GpuTensorHandle, _scalar: f64) -> anyhow::Result<GpuTensorHandle> {
2650        Err(anyhow::anyhow!("scalar_rsub not supported by provider"))
2651    }
2652    fn scalar_rdiv(&self, _a: &GpuTensorHandle, _scalar: f64) -> anyhow::Result<GpuTensorHandle> {
2653        Err(anyhow::anyhow!("scalar_rdiv not supported by provider"))
2654    }
2655    // Scalar operations: apply op with scalar right-hand side (broadcast over a)
2656    fn scalar_add(&self, _a: &GpuTensorHandle, _scalar: f64) -> anyhow::Result<GpuTensorHandle> {
2657        Err(anyhow::anyhow!("scalar_add not supported by provider"))
2658    }
2659    fn scalar_sub(&self, _a: &GpuTensorHandle, _scalar: f64) -> anyhow::Result<GpuTensorHandle> {
2660        Err(anyhow::anyhow!("scalar_sub not supported by provider"))
2661    }
2662    fn scalar_mul(&self, _a: &GpuTensorHandle, _scalar: f64) -> anyhow::Result<GpuTensorHandle> {
2663        Err(anyhow::anyhow!("scalar_mul not supported by provider"))
2664    }
2665    fn scalar_max(&self, _a: &GpuTensorHandle, _scalar: f64) -> anyhow::Result<GpuTensorHandle> {
2666        Err(anyhow::anyhow!("scalar_max not supported by provider"))
2667    }
2668    fn scalar_min(&self, _a: &GpuTensorHandle, _scalar: f64) -> anyhow::Result<GpuTensorHandle> {
2669        Err(anyhow::anyhow!("scalar_min not supported by provider"))
2670    }
2671    fn scalar_div(&self, _a: &GpuTensorHandle, _scalar: f64) -> anyhow::Result<GpuTensorHandle> {
2672        Err(anyhow::anyhow!("scalar_div not supported by provider"))
2673    }
2674    fn sort_dim<'a>(
2675        &'a self,
2676        _a: &'a GpuTensorHandle,
2677        _dim: usize,
2678        _order: SortOrder,
2679        _comparison: SortComparison,
2680    ) -> AccelProviderFuture<'a, SortResult> {
2681        unsupported_future("sort_dim not supported by provider")
2682    }
2683    fn sort_rows<'a>(
2684        &'a self,
2685        _a: &'a GpuTensorHandle,
2686        _columns: &'a [SortRowsColumnSpec],
2687        _comparison: SortComparison,
2688    ) -> AccelProviderFuture<'a, SortResult> {
2689        unsupported_future("sort_rows not supported by provider")
2690    }
2691    fn matmul<'a>(
2692        &'a self,
2693        _a: &'a GpuTensorHandle,
2694        _b: &'a GpuTensorHandle,
2695    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2696        unsupported_future("matmul not supported by provider")
2697    }
2698
2699    fn syrk(&self, _a: &GpuTensorHandle) -> anyhow::Result<GpuTensorHandle> {
2700        Err(anyhow::anyhow!("syrk not supported by provider"))
2701    }
2702    fn pagefun(&self, _request: &PagefunRequest) -> anyhow::Result<GpuTensorHandle> {
2703        Err(anyhow::anyhow!("pagefun not supported by provider"))
2704    }
2705
2706    /// Optional: matrix multiplication with an epilogue applied before store.
2707    ///
2708    /// The default implementation falls back to `matmul` when the epilogue is effectively a no-op
2709    /// (alpha=1, beta=0, no row/col scales), and otherwise returns `Err`.
2710    fn matmul_epilogue<'a>(
2711        &'a self,
2712        a: &'a GpuTensorHandle,
2713        b: &'a GpuTensorHandle,
2714        epilogue: &'a MatmulEpilogue,
2715    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2716        Box::pin(async move {
2717            if epilogue.is_noop() {
2718                return self.matmul(a, b).await;
2719            }
2720            Err(anyhow::anyhow!("matmul_epilogue not supported by provider"))
2721        })
2722    }
2723    fn image_normalize<'a>(
2724        &'a self,
2725        _input: &'a GpuTensorHandle,
2726        _desc: &'a ImageNormalizeDescriptor,
2727    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2728        unsupported_future("image_normalize fusion not supported by provider")
2729    }
2730    fn matmul_power_step<'a>(
2731        &'a self,
2732        _lhs: &'a GpuTensorHandle,
2733        _rhs: &'a GpuTensorHandle,
2734        _epilogue: &'a PowerStepEpilogue,
2735    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2736        unsupported_future("matmul_power_step normalization not supported by provider")
2737    }
2738    fn linsolve<'a>(
2739        &'a self,
2740        _lhs: &'a GpuTensorHandle,
2741        _rhs: &'a GpuTensorHandle,
2742        _options: &'a ProviderLinsolveOptions,
2743    ) -> AccelProviderFuture<'a, ProviderLinsolveResult> {
2744        unsupported_future("linsolve not supported by provider")
2745    }
2746    fn inv<'a>(
2747        &'a self,
2748        _matrix: &'a GpuTensorHandle,
2749        _options: ProviderInvOptions,
2750    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2751        unsupported_future("inv not supported by provider")
2752    }
2753    fn pinv<'a>(
2754        &'a self,
2755        _matrix: &'a GpuTensorHandle,
2756        _options: ProviderPinvOptions,
2757    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2758        unsupported_future("pinv not supported by provider")
2759    }
2760    fn cond<'a>(
2761        &'a self,
2762        _matrix: &'a GpuTensorHandle,
2763        _norm: ProviderCondNorm,
2764    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2765        Box::pin(async move { Err(anyhow::anyhow!("cond not supported by provider")) })
2766    }
2767    fn norm<'a>(
2768        &'a self,
2769        _tensor: &'a GpuTensorHandle,
2770        _order: ProviderNormOrder,
2771    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2772        Box::pin(async move { Err(anyhow::anyhow!("norm not supported by provider")) })
2773    }
2774    fn interp1<'a>(
2775        &'a self,
2776        _request: &'a ProviderInterp1Request<'a>,
2777    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2778        unsupported_future("interp1 not supported by provider")
2779    }
2780    fn rank<'a>(
2781        &'a self,
2782        _matrix: &'a GpuTensorHandle,
2783        _tolerance: Option<f64>,
2784    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2785        Box::pin(async move { Err(anyhow::anyhow!("rank not supported by provider")) })
2786    }
2787    fn rcond<'a>(
2788        &'a self,
2789        _matrix: &'a GpuTensorHandle,
2790    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2791        Box::pin(async move { Err(anyhow::anyhow!("rcond not supported by provider")) })
2792    }
2793    fn mldivide<'a>(
2794        &'a self,
2795        _lhs: &'a GpuTensorHandle,
2796        _rhs: &'a GpuTensorHandle,
2797    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2798        Box::pin(async move { Err(anyhow::anyhow!("mldivide not supported by provider")) })
2799    }
2800    fn mrdivide<'a>(
2801        &'a self,
2802        _lhs: &'a GpuTensorHandle,
2803        _rhs: &'a GpuTensorHandle,
2804    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2805        Box::pin(async move { Err(anyhow::anyhow!("mrdivide not supported by provider")) })
2806    }
2807    fn eig<'a>(
2808        &'a self,
2809        _a: &'a GpuTensorHandle,
2810        _compute_left: bool,
2811    ) -> AccelProviderFuture<'a, ProviderEigResult> {
2812        Box::pin(async move { Err(anyhow::anyhow!("eig not supported by provider")) })
2813    }
2814    fn lu<'a>(&'a self, _a: &'a GpuTensorHandle) -> AccelProviderFuture<'a, ProviderLuResult> {
2815        Box::pin(async move { Err(anyhow::anyhow!("lu not supported by provider")) })
2816    }
2817
2818    fn chol<'a>(
2819        &'a self,
2820        _a: &'a GpuTensorHandle,
2821        _lower: bool,
2822    ) -> AccelProviderFuture<'a, ProviderCholResult> {
2823        Box::pin(async move { Err(anyhow::anyhow!("chol not supported by provider")) })
2824    }
2825    fn qr<'a>(
2826        &'a self,
2827        _a: &'a GpuTensorHandle,
2828        _options: ProviderQrOptions,
2829    ) -> AccelProviderFuture<'a, ProviderQrResult> {
2830        Box::pin(async move { Err(anyhow::anyhow!("qr not supported by provider")) })
2831    }
2832    fn take_matmul_sources(
2833        &self,
2834        _product: &GpuTensorHandle,
2835    ) -> Option<(GpuTensorHandle, GpuTensorHandle)> {
2836        None
2837    }
2838    fn qr_power_iter<'a>(
2839        &'a self,
2840        product: &'a GpuTensorHandle,
2841        _product_lhs: Option<&'a GpuTensorHandle>,
2842        q_handle: &'a GpuTensorHandle,
2843        options: &'a ProviderQrOptions,
2844    ) -> AccelProviderFuture<'a, Option<ProviderQrPowerIterResult>> {
2845        let _ = (product, q_handle, options);
2846        Box::pin(async move { Ok(None) })
2847    }
2848    fn transpose(&self, _a: &GpuTensorHandle) -> anyhow::Result<GpuTensorHandle> {
2849        Err(anyhow::anyhow!("transpose not supported by provider"))
2850    }
2851    fn conv1d(
2852        &self,
2853        _signal: &GpuTensorHandle,
2854        _kernel: &GpuTensorHandle,
2855        _options: ProviderConv1dOptions,
2856    ) -> anyhow::Result<GpuTensorHandle> {
2857        Err(anyhow::anyhow!("conv1d not supported by provider"))
2858    }
2859    fn conv2d(
2860        &self,
2861        _signal: &GpuTensorHandle,
2862        _kernel: &GpuTensorHandle,
2863        _mode: ProviderConvMode,
2864    ) -> anyhow::Result<GpuTensorHandle> {
2865        Err(anyhow::anyhow!("conv2d not supported by provider"))
2866    }
2867    fn iir_filter<'a>(
2868        &'a self,
2869        _b: &'a GpuTensorHandle,
2870        _a: &'a GpuTensorHandle,
2871        _x: &'a GpuTensorHandle,
2872        _options: ProviderIirFilterOptions,
2873    ) -> AccelProviderFuture<'a, ProviderIirFilterResult> {
2874        Box::pin(async move { Err(anyhow::anyhow!("iir_filter not supported by provider")) })
2875    }
2876    fn uniform_spectral_estimate<'a>(
2877        &'a self,
2878        _request: &'a ProviderSpectralRequest<'a>,
2879    ) -> AccelProviderFuture<'a, ProviderSpectralResult> {
2880        unsupported_future("uniform_spectral_estimate not supported by provider")
2881    }
2882    fn signal_envelope<'a>(
2883        &'a self,
2884        _request: &'a ProviderEnvelopeRequest<'a>,
2885    ) -> AccelProviderFuture<'a, ProviderEnvelopeResult> {
2886        unsupported_future("signal_envelope not supported by provider")
2887    }
2888    fn signal_hilbert<'a>(
2889        &'a self,
2890        _request: &'a ProviderHilbertRequest<'a>,
2891    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2892        unsupported_future("signal_hilbert not supported by provider")
2893    }
2894    /// Reorder tensor dimensions according to `order`, expressed as zero-based indices.
2895    fn permute(
2896        &self,
2897        _handle: &GpuTensorHandle,
2898        _order: &[usize],
2899    ) -> anyhow::Result<GpuTensorHandle> {
2900        Err(anyhow::anyhow!("permute not supported by provider"))
2901    }
2902    fn flip(&self, _handle: &GpuTensorHandle, _axes: &[usize]) -> anyhow::Result<GpuTensorHandle> {
2903        Err(anyhow::anyhow!("flip not supported by provider"))
2904    }
2905    fn circshift(
2906        &self,
2907        _handle: &GpuTensorHandle,
2908        _shifts: &[isize],
2909    ) -> anyhow::Result<GpuTensorHandle> {
2910        Err(anyhow::anyhow!("circshift not supported by provider"))
2911    }
2912    fn diff_dim(
2913        &self,
2914        _handle: &GpuTensorHandle,
2915        _order: usize,
2916        _dim: usize,
2917    ) -> anyhow::Result<GpuTensorHandle> {
2918        Err(anyhow::anyhow!("diff_dim not supported by provider"))
2919    }
2920    fn gradient_dim(
2921        &self,
2922        _handle: &GpuTensorHandle,
2923        _dim: usize,
2924        _spacing: f64,
2925    ) -> anyhow::Result<GpuTensorHandle> {
2926        Err(anyhow::anyhow!("gradient_dim not supported by provider"))
2927    }
2928    fn gradient_dim_with_coordinates(
2929        &self,
2930        _handle: &GpuTensorHandle,
2931        _dim: usize,
2932        _coordinates: &GpuTensorHandle,
2933    ) -> anyhow::Result<GpuTensorHandle> {
2934        Err(anyhow::anyhow!(
2935            "gradient_dim_with_coordinates not supported by provider"
2936        ))
2937    }
2938    /// Perform an in-place FFT along a zero-based dimension, optionally padding/truncating to `len`.
2939    fn fft_dim<'a>(
2940        &'a self,
2941        _handle: &'a GpuTensorHandle,
2942        _len: Option<usize>,
2943        _dim: usize,
2944    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2945        unsupported_future("fft_dim not supported by provider")
2946    }
2947    fn ifft_dim<'a>(
2948        &'a self,
2949        _handle: &'a GpuTensorHandle,
2950        _len: Option<usize>,
2951        _dim: usize,
2952    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2953        unsupported_future("ifft_dim not supported by provider")
2954    }
2955    fn fft_extract_real<'a>(
2956        &'a self,
2957        _handle: &'a GpuTensorHandle,
2958    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
2959        unsupported_future("fft_extract_real not supported by provider")
2960    }
2961    fn unique<'a>(
2962        &'a self,
2963        _handle: &'a GpuTensorHandle,
2964        _options: &'a UniqueOptions,
2965    ) -> AccelProviderFuture<'a, UniqueResult> {
2966        Box::pin(async move { Err(anyhow::anyhow!("unique not supported by provider")) })
2967    }
2968    fn union<'a>(
2969        &'a self,
2970        _a: &'a GpuTensorHandle,
2971        _b: &'a GpuTensorHandle,
2972        _options: &'a UnionOptions,
2973    ) -> AccelProviderFuture<'a, UnionResult> {
2974        Box::pin(async move { Err(anyhow::anyhow!("union not supported by provider")) })
2975    }
2976    fn setdiff<'a>(
2977        &'a self,
2978        _a: &'a GpuTensorHandle,
2979        _b: &'a GpuTensorHandle,
2980        _options: &'a SetdiffOptions,
2981    ) -> AccelProviderFuture<'a, SetdiffResult> {
2982        Box::pin(async move { Err(anyhow::anyhow!("setdiff not supported by provider")) })
2983    }
2984    fn ismember<'a>(
2985        &'a self,
2986        _a: &'a GpuTensorHandle,
2987        _b: &'a GpuTensorHandle,
2988        _options: &'a IsMemberOptions,
2989    ) -> AccelProviderFuture<'a, IsMemberResult> {
2990        Box::pin(async move { Err(anyhow::anyhow!("ismember not supported by provider")) })
2991    }
2992    fn reshape(
2993        &self,
2994        handle: &GpuTensorHandle,
2995        new_shape: &[usize],
2996    ) -> anyhow::Result<GpuTensorHandle> {
2997        let mut updated = handle.clone();
2998        updated.shape = new_shape.to_vec();
2999        Ok(updated)
3000    }
3001    /// Concatenate the provided tensors along the 1-based dimension `dim`.
3002    fn cat(&self, _dim: usize, _inputs: &[GpuTensorHandle]) -> anyhow::Result<GpuTensorHandle> {
3003        Err(anyhow::anyhow!("cat not supported by provider"))
3004    }
3005    fn repmat(
3006        &self,
3007        _handle: &GpuTensorHandle,
3008        _reps: &[usize],
3009    ) -> anyhow::Result<GpuTensorHandle> {
3010        Err(anyhow::anyhow!("repmat not supported by provider"))
3011    }
3012    /// Compute the Kronecker product of two tensors, matching MATLAB semantics.
3013    fn kron(&self, _a: &GpuTensorHandle, _b: &GpuTensorHandle) -> anyhow::Result<GpuTensorHandle> {
3014        Err(anyhow::anyhow!("kron not supported by provider"))
3015    }
3016    /// Compute the cross product of 3-element vectors along a matching dimension.
3017    fn cross(
3018        &self,
3019        _lhs: &GpuTensorHandle,
3020        _rhs: &GpuTensorHandle,
3021        _dim: Option<usize>,
3022    ) -> anyhow::Result<GpuTensorHandle> {
3023        Err(anyhow::anyhow!("cross not supported by provider"))
3024    }
3025    fn reduce_sum<'a>(
3026        &'a self,
3027        _a: &'a GpuTensorHandle,
3028    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3029        unsupported_future("reduce_sum not supported by provider")
3030    }
3031    fn reduce_sum_dim<'a>(
3032        &'a self,
3033        _a: &'a GpuTensorHandle,
3034        _dim: usize,
3035    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3036        unsupported_future("reduce_sum_dim not supported by provider")
3037    }
3038    /// Reduce a native integer gpuArray while preserving its exact element
3039    /// class. This is intentionally separate from `reduce_sum`, whose MATLAB
3040    /// default output semantics are floating point for integer inputs.
3041    fn reduce_integer_sum_native<'a>(
3042        &'a self,
3043        _a: &'a GpuTensorHandle,
3044    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3045        unsupported_future("reduce_integer_sum_native not supported by provider")
3046    }
3047    /// Dimension form of [`Self::reduce_integer_sum_native`].
3048    fn reduce_integer_sum_native_dim<'a>(
3049        &'a self,
3050        _a: &'a GpuTensorHandle,
3051        _dim: usize,
3052    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3053        unsupported_future("reduce_integer_sum_native_dim not supported by provider")
3054    }
3055    fn dot<'a>(
3056        &'a self,
3057        _lhs: &'a GpuTensorHandle,
3058        _rhs: &'a GpuTensorHandle,
3059        _dim: Option<usize>,
3060    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3061        unsupported_future("dot not supported by provider")
3062    }
3063    fn reduce_nnz<'a>(
3064        &'a self,
3065        _a: &'a GpuTensorHandle,
3066    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3067        unsupported_future("reduce_nnz not supported by provider")
3068    }
3069    fn reduce_nnz_dim<'a>(
3070        &'a self,
3071        _a: &'a GpuTensorHandle,
3072        _dim: usize,
3073    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3074        unsupported_future("reduce_nnz_dim not supported by provider")
3075    }
3076    fn reduce_prod<'a>(
3077        &'a self,
3078        _a: &'a GpuTensorHandle,
3079    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3080        unsupported_future("reduce_prod not supported by provider")
3081    }
3082    fn reduce_prod_dim<'a>(
3083        &'a self,
3084        _a: &'a GpuTensorHandle,
3085        _dim: usize,
3086    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3087        unsupported_future("reduce_prod_dim not supported by provider")
3088    }
3089    /// Reduce a native integer gpuArray by product while preserving its exact
3090    /// element class, distinct from MATLAB's default floating-point output.
3091    fn reduce_integer_prod_native<'a>(
3092        &'a self,
3093        _a: &'a GpuTensorHandle,
3094    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3095        unsupported_future("reduce_integer_prod_native not supported by provider")
3096    }
3097    /// Dimension form of [`Self::reduce_integer_prod_native`].
3098    fn reduce_integer_prod_native_dim<'a>(
3099        &'a self,
3100        _a: &'a GpuTensorHandle,
3101        _dim: usize,
3102    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3103        unsupported_future("reduce_integer_prod_native_dim not supported by provider")
3104    }
3105    /// Compute MATLAB's explicit native-output mean for a native integer
3106    /// gpuArray. Providers must avoid floating-point accumulation so int64 and
3107    /// uint64 values remain exact before the final class-preserving rounding.
3108    fn reduce_integer_mean_native<'a>(
3109        &'a self,
3110        _a: &'a GpuTensorHandle,
3111    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3112        unsupported_future("reduce_integer_mean_native not supported by provider")
3113    }
3114    /// Dimension form of [`Self::reduce_integer_mean_native`].
3115    fn reduce_integer_mean_native_dim<'a>(
3116        &'a self,
3117        _a: &'a GpuTensorHandle,
3118        _dim: usize,
3119    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3120        unsupported_future("reduce_integer_mean_native_dim not supported by provider")
3121    }
3122    /// Multi-dimension form of [`Self::reduce_integer_mean_native`]. All
3123    /// requested zero-based dimensions must be reduced in one logical pass so
3124    /// native integer output rounds only once.
3125    fn reduce_integer_mean_native_dims<'a>(
3126        &'a self,
3127        _a: &'a GpuTensorHandle,
3128        _dims_zero_based: &'a [usize],
3129    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3130        unsupported_future("reduce_integer_mean_native_dims not supported by provider")
3131    }
3132    /// Convert a real gpuArray to exact native integer storage while preserving
3133    /// device residency. Floating-point inputs use MATLAB-compatible saturating
3134    /// rounding; native integer inputs use exact class-to-class clamping.
3135    fn cast_to_integer<'a>(
3136        &'a self,
3137        _a: &'a GpuTensorHandle,
3138        _target: IntegerElementType,
3139    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3140        unsupported_future("cast_to_integer not supported by provider")
3141    }
3142    fn reduce_mean<'a>(
3143        &'a self,
3144        _a: &'a GpuTensorHandle,
3145    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3146        unsupported_future("reduce_mean not supported by provider")
3147    }
3148    /// Reduce mean across multiple zero-based dimensions in one device pass.
3149    fn reduce_mean_nd<'a>(
3150        &'a self,
3151        _a: &'a GpuTensorHandle,
3152        _dims_zero_based: &'a [usize],
3153    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3154        unsupported_future("reduce_mean_nd not supported by provider")
3155    }
3156    /// Reduce moments across multiple zero-based dimensions in one device pass.
3157    /// Returns mean (E[x]) and mean of squares (E[x^2]).
3158    fn reduce_moments_nd<'a>(
3159        &'a self,
3160        _a: &'a GpuTensorHandle,
3161        _dims_zero_based: &'a [usize],
3162    ) -> AccelProviderFuture<'a, ProviderMoments2> {
3163        unsupported_future("reduce_moments_nd not supported by provider")
3164    }
3165    fn reduce_mean_dim<'a>(
3166        &'a self,
3167        _a: &'a GpuTensorHandle,
3168        _dim: usize,
3169    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3170        unsupported_future("reduce_mean_dim not supported by provider")
3171    }
3172    fn reduce_std<'a>(
3173        &'a self,
3174        _a: &'a GpuTensorHandle,
3175        _normalization: ProviderStdNormalization,
3176        _nan_mode: ProviderNanMode,
3177    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3178        unsupported_future("reduce_std not supported by provider")
3179    }
3180    fn reduce_std_dim<'a>(
3181        &'a self,
3182        _a: &'a GpuTensorHandle,
3183        _dim: usize,
3184        _normalization: ProviderStdNormalization,
3185        _nan_mode: ProviderNanMode,
3186    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3187        unsupported_future("reduce_std_dim not supported by provider")
3188    }
3189    fn reduce_any<'a>(
3190        &'a self,
3191        _a: &'a GpuTensorHandle,
3192        _omit_nan: bool,
3193    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3194        unsupported_future("reduce_any not supported by provider")
3195    }
3196    fn reduce_any_dim<'a>(
3197        &'a self,
3198        _a: &'a GpuTensorHandle,
3199        _dim: usize,
3200        _omit_nan: bool,
3201    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3202        unsupported_future("reduce_any_dim not supported by provider")
3203    }
3204    fn reduce_all<'a>(
3205        &'a self,
3206        _a: &'a GpuTensorHandle,
3207        _omit_nan: bool,
3208    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3209        unsupported_future("reduce_all not supported by provider")
3210    }
3211    fn reduce_all_dim<'a>(
3212        &'a self,
3213        _a: &'a GpuTensorHandle,
3214        _dim: usize,
3215        _omit_nan: bool,
3216    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3217        unsupported_future("reduce_all_dim not supported by provider")
3218    }
3219    fn reduce_median<'a>(
3220        &'a self,
3221        _a: &'a GpuTensorHandle,
3222    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3223        unsupported_future("reduce_median not supported by provider")
3224    }
3225    fn reduce_median_dim<'a>(
3226        &'a self,
3227        _a: &'a GpuTensorHandle,
3228        _dim: usize,
3229    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3230        unsupported_future("reduce_median_dim not supported by provider")
3231    }
3232    fn mode_values<'a>(
3233        &'a self,
3234        _request: &'a ProviderModeRequest<'a>,
3235    ) -> AccelProviderFuture<'a, ProviderModeResult> {
3236        unsupported_future("mode_values not supported by provider")
3237    }
3238    fn moving_window<'a>(
3239        &'a self,
3240        _request: &'a ProviderMovingWindowRequest<'a>,
3241    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3242        unsupported_future("moving_window not supported by provider")
3243    }
3244    fn reduce_min<'a>(
3245        &'a self,
3246        _a: &'a GpuTensorHandle,
3247    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3248        unsupported_future("reduce_min not supported by provider")
3249    }
3250    fn reduce_min_dim<'a>(
3251        &'a self,
3252        _a: &'a GpuTensorHandle,
3253        _dim: usize,
3254    ) -> AccelProviderFuture<'a, ReduceDimResult> {
3255        unsupported_future("reduce_min_dim not supported by provider")
3256    }
3257    fn reduce_max<'a>(
3258        &'a self,
3259        _a: &'a GpuTensorHandle,
3260    ) -> AccelProviderFuture<'a, GpuTensorHandle> {
3261        unsupported_future("reduce_max not supported by provider")
3262    }
3263    fn reduce_max_dim<'a>(
3264        &'a self,
3265        _a: &'a GpuTensorHandle,
3266        _dim: usize,
3267    ) -> AccelProviderFuture<'a, ReduceDimResult> {
3268        unsupported_future("reduce_max_dim not supported by provider")
3269    }
3270    fn cumsum_scan(
3271        &self,
3272        _input: &GpuTensorHandle,
3273        _dim: usize,
3274        _direction: ProviderScanDirection,
3275        _nan_mode: ProviderNanMode,
3276    ) -> anyhow::Result<GpuTensorHandle> {
3277        Err(anyhow::anyhow!("cumsum_scan not supported by provider"))
3278    }
3279    fn integer_cumsum_scan(
3280        &self,
3281        _input: &GpuTensorHandle,
3282        _dim: usize,
3283        _direction: ProviderScanDirection,
3284    ) -> anyhow::Result<GpuTensorHandle> {
3285        Err(anyhow::anyhow!(
3286            "integer_cumsum_scan not supported by provider"
3287        ))
3288    }
3289    fn trapz_dim(
3290        &self,
3291        _input: &GpuTensorHandle,
3292        _dim: usize,
3293        _spacing: ProviderTrapezoidSpacing<'_>,
3294    ) -> anyhow::Result<GpuTensorHandle> {
3295        Err(anyhow::anyhow!("trapz_dim not supported by provider"))
3296    }
3297    fn cumtrapz_dim(
3298        &self,
3299        _input: &GpuTensorHandle,
3300        _dim: usize,
3301        _spacing: ProviderTrapezoidSpacing<'_>,
3302    ) -> anyhow::Result<GpuTensorHandle> {
3303        Err(anyhow::anyhow!("cumtrapz_dim not supported by provider"))
3304    }
3305    fn cumprod_scan(
3306        &self,
3307        _input: &GpuTensorHandle,
3308        _dim: usize,
3309        _direction: ProviderScanDirection,
3310        _nan_mode: ProviderNanMode,
3311    ) -> anyhow::Result<GpuTensorHandle> {
3312        Err(anyhow::anyhow!("cumprod_scan not supported by provider"))
3313    }
3314    fn integer_cumprod_scan(
3315        &self,
3316        _input: &GpuTensorHandle,
3317        _dim: usize,
3318        _direction: ProviderScanDirection,
3319    ) -> anyhow::Result<GpuTensorHandle> {
3320        Err(anyhow::anyhow!(
3321            "integer_cumprod_scan not supported by provider"
3322        ))
3323    }
3324    fn cummin_scan(
3325        &self,
3326        _input: &GpuTensorHandle,
3327        _dim: usize,
3328        _direction: ProviderScanDirection,
3329        _nan_mode: ProviderNanMode,
3330    ) -> anyhow::Result<ProviderCumminResult> {
3331        Err(anyhow::anyhow!("cummin_scan not supported by provider"))
3332    }
3333    fn integer_cummin_scan(
3334        &self,
3335        _input: &GpuTensorHandle,
3336        _dim: usize,
3337        _direction: ProviderScanDirection,
3338    ) -> anyhow::Result<ProviderCumminResult> {
3339        Err(anyhow::anyhow!(
3340            "integer_cummin_scan not supported by provider"
3341        ))
3342    }
3343    fn cummax_scan(
3344        &self,
3345        _input: &GpuTensorHandle,
3346        _dim: usize,
3347        _direction: ProviderScanDirection,
3348        _nan_mode: ProviderNanMode,
3349    ) -> anyhow::Result<ProviderCummaxResult> {
3350        Err(anyhow::anyhow!("cummax_scan not supported by provider"))
3351    }
3352    fn integer_cummax_scan(
3353        &self,
3354        _input: &GpuTensorHandle,
3355        _dim: usize,
3356        _direction: ProviderScanDirection,
3357    ) -> anyhow::Result<ProviderCummaxResult> {
3358        Err(anyhow::anyhow!(
3359            "integer_cummax_scan not supported by provider"
3360        ))
3361    }
3362
3363    fn find(
3364        &self,
3365        _a: &GpuTensorHandle,
3366        _limit: Option<usize>,
3367        _direction: FindDirection,
3368    ) -> anyhow::Result<ProviderFindResult> {
3369        Err(anyhow::anyhow!("find not supported by provider"))
3370    }
3371
3372    fn fused_elementwise(
3373        &self,
3374        _shader: &str,
3375        _inputs: &[GpuTensorHandle],
3376        _output_shape: &[usize],
3377        _len: usize,
3378    ) -> anyhow::Result<GpuTensorHandle> {
3379        Err(anyhow::anyhow!(
3380            "fused_elementwise not supported by provider"
3381        ))
3382    }
3383
3384    /// Execute a single fused elementwise kernel that writes `num_outputs` output buffers in one
3385    /// dispatch. The shader is expected to declare `output0`, `output1`, … `output{N-1}` storage
3386    /// bindings (at binding indices `inputs.len()` through `inputs.len() + num_outputs - 1`) and a
3387    /// uniform `params` binding at `inputs.len() + num_outputs`.
3388    ///
3389    /// Providers that do not override this method fall back to calling `fused_elementwise` once
3390    /// per output, which preserves correctness at the cost of the O(N²) dispatch overhead this
3391    /// method is designed to eliminate.
3392    fn fused_elementwise_multi(
3393        &self,
3394        _shader: &str,
3395        _inputs: &[GpuTensorHandle],
3396        _output_shape: &[usize],
3397        _len: usize,
3398        _num_outputs: usize,
3399    ) -> anyhow::Result<Vec<GpuTensorHandle>> {
3400        Err(anyhow::anyhow!(
3401            "fused_elementwise_multi not supported by provider"
3402        ))
3403    }
3404
3405    /// Build a numeric tensor where NaNs in `a` are replaced with 0.0 (device side).
3406    fn map_nan_to_zero(&self, _a: &GpuTensorHandle) -> anyhow::Result<GpuTensorHandle> {
3407        Err(anyhow::anyhow!("map_nan_to_zero not supported by provider"))
3408    }
3409
3410    /// Build a numeric mask tensor with 1.0 where value is not NaN and 0.0 where value is NaN.
3411    fn not_nan_mask(&self, _a: &GpuTensorHandle) -> anyhow::Result<GpuTensorHandle> {
3412        Err(anyhow::anyhow!("not_nan_mask not supported by provider"))
3413    }
3414
3415    /// Generic fused reduction entrypoint.
3416    ///
3417    /// The shader is expected to implement a column-major reduction across `reduce_len` with
3418    /// `num_slices` independent slices (e.g., columns). Providers should create a uniform buffer
3419    /// compatible with the expected `Params/MParams` struct in the shader and dispatch
3420    /// `num_slices` workgroups with `workgroup_size` threads, or an equivalent strategy.
3421    #[allow(clippy::too_many_arguments)]
3422    fn fused_reduction(
3423        &self,
3424        _shader: &str,
3425        _inputs: &[GpuTensorHandle],
3426        _output_shape: &[usize],
3427        _reduce_len: usize,
3428        _num_slices: usize,
3429        _workgroup_size: u32,
3430        _flavor: ReductionFlavor,
3431    ) -> anyhow::Result<GpuTensorHandle> {
3432        Err(anyhow::anyhow!("fused_reduction not supported by provider"))
3433    }
3434
3435    /// Optionally pre-compile commonly used pipelines to amortize first-dispatch costs.
3436    fn warmup(&self) {}
3437
3438    /// Returns (cache_hits, cache_misses) for fused pipeline cache, if supported.
3439    fn fused_cache_counters(&self) -> (u64, u64) {
3440        (0, 0)
3441    }
3442
3443    /// Returns the duration of the last provider warmup in milliseconds, if known.
3444    fn last_warmup_millis(&self) -> Option<u64> {
3445        None
3446    }
3447
3448    /// Returns a snapshot of provider telemetry counters if supported.
3449    fn telemetry_snapshot(&self) -> ProviderTelemetry {
3450        let (hits, misses) = self.fused_cache_counters();
3451        ProviderTelemetry {
3452            fused_elementwise: ProviderDispatchStats::default(),
3453            fused_reduction: ProviderDispatchStats::default(),
3454            matmul: ProviderDispatchStats::default(),
3455            linsolve: ProviderDispatchStats::default(),
3456            mldivide: ProviderDispatchStats::default(),
3457            mrdivide: ProviderDispatchStats::default(),
3458            upload_bytes: 0,
3459            download_bytes: 0,
3460            solve_fallbacks: Vec::new(),
3461            fusion_cache_hits: hits,
3462            fusion_cache_misses: misses,
3463            bind_group_cache_hits: 0,
3464            bind_group_cache_misses: 0,
3465            bind_group_cache_by_layout: None,
3466            kernel_launches: Vec::new(),
3467        }
3468    }
3469
3470    /// Reset all telemetry counters maintained by the provider, if supported.
3471    fn reset_telemetry(&self) {}
3472
3473    /// Default reduction workgroup size the provider prefers.
3474    fn default_reduction_workgroup_size(&self) -> u32 {
3475        256
3476    }
3477
3478    /// Threshold above which provider will prefer two-pass reduction.
3479    fn two_pass_threshold(&self) -> usize {
3480        1024
3481    }
3482
3483    /// Current two-pass mode preference (auto/forced on/off).
3484    fn reduction_two_pass_mode(&self) -> ReductionTwoPassMode {
3485        ReductionTwoPassMode::Auto
3486    }
3487
3488    /// Fast-path: write a GPU column in a matrix from a GPU vector, returning a new handle.
3489    /// Expected: `values.shape == [rows, 1]` (or `[rows]`) and `col_index < cols`.
3490    fn scatter_column(
3491        &self,
3492        _matrix: &GpuTensorHandle,
3493        _col_index: usize,
3494        _values: &GpuTensorHandle,
3495    ) -> anyhow::Result<GpuTensorHandle> {
3496        Err(anyhow::anyhow!("scatter_column not supported by provider"))
3497    }
3498
3499    /// Fast-path: write a GPU row in a matrix from a GPU vector, returning a new handle.
3500    /// Expected: `values.shape == [1, cols]` (or `[cols]`) and `row_index < rows`.
3501    fn scatter_row(
3502        &self,
3503        _matrix: &GpuTensorHandle,
3504        _row_index: usize,
3505        _values: &GpuTensorHandle,
3506    ) -> anyhow::Result<GpuTensorHandle> {
3507        Err(anyhow::anyhow!("scatter_row not supported by provider"))
3508    }
3509
3510    fn sub2ind(
3511        &self,
3512        _dims: &[usize],
3513        _strides: &[usize],
3514        _inputs: &[&GpuTensorHandle],
3515        _scalar_mask: &[bool],
3516        _len: usize,
3517        _output_shape: &[usize],
3518    ) -> anyhow::Result<GpuTensorHandle> {
3519        Err(anyhow::anyhow!("sub2ind not supported by provider"))
3520    }
3521
3522    /// Returns true if the provider offers a device-side `ind2sub` implementation.
3523    fn supports_ind2sub(&self) -> bool {
3524        false
3525    }
3526
3527    /// Convert linear indices into per-dimension subscripts on the device.
3528    fn ind2sub(
3529        &self,
3530        _dims: &[usize],
3531        _strides: &[usize],
3532        _indices: &GpuTensorHandle,
3533        _total: usize,
3534        _len: usize,
3535        _output_shape: &[usize],
3536    ) -> anyhow::Result<Vec<GpuTensorHandle>> {
3537        Err(anyhow::anyhow!("ind2sub not supported by provider"))
3538    }
3539
3540    /// Determine if a matrix is symmetric (or skew-symmetric) without gathering it to the host.
3541    fn issymmetric(
3542        &self,
3543        _matrix: &GpuTensorHandle,
3544        _kind: ProviderSymmetryKind,
3545        _tolerance: f64,
3546    ) -> anyhow::Result<bool> {
3547        Err(anyhow::anyhow!(
3548            "issymmetric predicate not supported by provider"
3549        ))
3550    }
3551
3552    /// Determine if a matrix is Hermitian (or skew-Hermitian) without gathering it to the host.
3553    fn ishermitian<'a>(
3554        &'a self,
3555        _matrix: &'a GpuTensorHandle,
3556        _kind: ProviderHermitianKind,
3557        _tolerance: f64,
3558    ) -> AccelProviderFuture<'a, bool> {
3559        Box::pin(async move {
3560            Err(anyhow::anyhow!(
3561                "ishermitian predicate not supported by provider"
3562            ))
3563        })
3564    }
3565
3566    /// Inspect the bandwidth of a matrix without gathering it back to the host.
3567    fn bandwidth(&self, _matrix: &GpuTensorHandle) -> anyhow::Result<ProviderBandwidth> {
3568        Err(anyhow::anyhow!("bandwidth not supported by provider"))
3569    }
3570
3571    /// Compute the symmetric reverse Cuthill-McKee permutation for the matrix.
3572    ///
3573    /// Implementations may execute on the device or gather to the host. The permutation should be
3574    /// returned as zero-based indices.
3575    fn sym_rcm<'a>(&'a self, _matrix: &'a GpuTensorHandle) -> AccelProviderFuture<'a, Vec<usize>> {
3576        Box::pin(async move { Err(anyhow::anyhow!("sym_rcm not supported by provider")) })
3577    }
3578}
3579
3580static GLOBAL_PROVIDER: Lazy<RwLock<Option<&'static dyn AccelProvider>>> =
3581    Lazy::new(|| RwLock::new(None));
3582static PROVIDER_REGISTRY: Lazy<RwLock<HashMap<u32, &'static dyn AccelProvider>>> =
3583    Lazy::new(|| RwLock::new(HashMap::new()));
3584static DEVICE_ID_COUNTER: AtomicU32 = AtomicU32::new(1);
3585
3586#[cfg(not(target_arch = "wasm32"))]
3587thread_local! {
3588    static THREAD_PROVIDER: Cell<Option<&'static dyn AccelProvider>> = Cell::new(None);
3589}
3590
3591#[cfg(target_arch = "wasm32")]
3592static WASM_THREAD_PROVIDER: Lazy<Mutex<Option<&'static dyn AccelProvider>>> =
3593    Lazy::new(|| Mutex::new(None));
3594
3595#[cfg(not(target_arch = "wasm32"))]
3596fn replace_thread_provider(
3597    provider: Option<&'static dyn AccelProvider>,
3598) -> Option<&'static dyn AccelProvider> {
3599    THREAD_PROVIDER.with(|cell| {
3600        let prev = cell.get();
3601        cell.set(provider);
3602        prev
3603    })
3604}
3605
3606#[cfg(target_arch = "wasm32")]
3607fn replace_thread_provider(
3608    provider: Option<&'static dyn AccelProvider>,
3609) -> Option<&'static dyn AccelProvider> {
3610    let mut slot = WASM_THREAD_PROVIDER
3611        .lock()
3612        .expect("wasm provider mutex poisoned");
3613    let prev = *slot;
3614    *slot = provider;
3615    prev
3616}
3617
3618#[cfg(not(target_arch = "wasm32"))]
3619fn current_thread_provider() -> Option<&'static dyn AccelProvider> {
3620    THREAD_PROVIDER.with(|cell| cell.get())
3621}
3622
3623#[cfg(target_arch = "wasm32")]
3624fn current_thread_provider() -> Option<&'static dyn AccelProvider> {
3625    WASM_THREAD_PROVIDER
3626        .lock()
3627        .expect("wasm provider mutex poisoned")
3628        .as_ref()
3629        .copied()
3630}
3631
3632/// Register a global acceleration provider.
3633///
3634/// # Safety
3635/// - The caller must guarantee that `p` is valid for the entire program lifetime
3636///   (e.g., a `'static` singleton), as the runtime stores a raw reference globally.
3637/// - Concurrent callers must ensure registration happens once or is properly
3638///   synchronized; this function does not enforce thread-safety for re-registration.
3639/// - Distinct live provider instances that can own handles must not share a
3640///   `device_id`; the handle registry uses that identifier as its ownership key.
3641pub unsafe fn register_provider(p: &'static dyn AccelProvider) {
3642    replace_thread_provider(Some(p));
3643    if let Ok(mut guard) = GLOBAL_PROVIDER.write() {
3644        *guard = Some(p);
3645    }
3646    register_provider_for_device(p.device_id(), p);
3647}
3648
3649unsafe fn register_provider_for_device(device_id: u32, provider: &'static dyn AccelProvider) {
3650    if let Ok(mut guard) = PROVIDER_REGISTRY.write() {
3651        guard.insert(device_id, provider);
3652    }
3653}
3654
3655pub fn provider() -> Option<&'static dyn AccelProvider> {
3656    if let Some(p) = current_thread_provider() {
3657        return Some(p);
3658    }
3659    GLOBAL_PROVIDER
3660        .read()
3661        .ok()
3662        .and_then(|guard| guard.as_ref().copied())
3663}
3664
3665/// Clear the active provider selection without invalidating providers that own live handles.
3666///
3667/// Registered device owners are retained because a [`GpuTensorHandle`] may outlive its provider's
3668/// tenure as the global default. Removing its owner here would make that otherwise-valid handle
3669/// impossible to gather or operate on. Tests that need a different default can safely call this
3670/// function and register another provider; device ids keep the handle namespaces distinct.
3671pub fn clear_provider() {
3672    replace_thread_provider(None);
3673    if let Ok(mut guard) = GLOBAL_PROVIDER.write() {
3674        *guard = None;
3675    }
3676}
3677
3678pub fn provider_for_device(device_id: u32) -> Option<&'static dyn AccelProvider> {
3679    if let Some(registered) = PROVIDER_REGISTRY
3680        .read()
3681        .ok()
3682        .and_then(|guard| guard.get(&device_id).copied())
3683    {
3684        return Some(registered);
3685    }
3686    if let Some(thread_provider) = current_thread_provider() {
3687        if thread_provider.device_id() == device_id {
3688            return Some(thread_provider);
3689        }
3690    }
3691    // Preserve legacy behavior: when no explicit per-device registration exists,
3692    // fall back to the globally active provider regardless of handle device id.
3693    GLOBAL_PROVIDER
3694        .read()
3695        .ok()
3696        .and_then(|guard| guard.as_ref().copied())
3697}
3698
3699pub fn provider_for_handle(handle: &GpuTensorHandle) -> Option<&'static dyn AccelProvider> {
3700    provider_for_device(handle.device_id)
3701}
3702
3703pub fn spawn_handle_concurrency_for(handle: &GpuTensorHandle) -> Option<SpawnHandleConcurrency> {
3704    provider_for_handle(handle).map(AccelProvider::spawn_handle_concurrency)
3705}
3706
3707pub fn next_device_id() -> u32 {
3708    DEVICE_ID_COUNTER.fetch_add(1, Ordering::Relaxed)
3709}
3710
3711pub struct ThreadProviderGuard {
3712    prev: Option<&'static dyn AccelProvider>,
3713}
3714
3715impl ThreadProviderGuard {
3716    pub fn set(provider: Option<&'static dyn AccelProvider>) -> Self {
3717        let prev = replace_thread_provider(provider);
3718        ThreadProviderGuard { prev }
3719    }
3720}
3721
3722impl Drop for ThreadProviderGuard {
3723    fn drop(&mut self) {
3724        let prev = self.prev.take();
3725        replace_thread_provider(prev);
3726    }
3727}
3728
3729pub fn set_thread_provider(provider: Option<&'static dyn AccelProvider>) {
3730    replace_thread_provider(provider);
3731}
3732
3733/// Convenience: perform elementwise add via provider if possible; otherwise return None
3734pub async fn try_elem_add(a: &GpuTensorHandle, b: &GpuTensorHandle) -> Option<GpuTensorHandle> {
3735    if a.device_id == b.device_id {
3736        let p = provider_for_handle(a)?;
3737        if let Ok(h) = p.elem_add(a, b).await {
3738            return Some(h);
3739        }
3740    }
3741    None
3742}
3743
3744/// Convenience: perform elementwise hypot via provider if possible; otherwise return None
3745pub async fn try_elem_hypot(a: &GpuTensorHandle, b: &GpuTensorHandle) -> Option<GpuTensorHandle> {
3746    if a.device_id == b.device_id {
3747        let p = provider_for_handle(a)?;
3748        if let Ok(h) = p.elem_hypot(a, b).await {
3749            return Some(h);
3750        }
3751    }
3752    None
3753}
3754
3755/// Convenience: perform elementwise max via provider if possible; otherwise return None
3756pub async fn try_elem_max(a: &GpuTensorHandle, b: &GpuTensorHandle) -> Option<GpuTensorHandle> {
3757    if a.device_id == b.device_id {
3758        let p = provider_for_handle(a)?;
3759        if let Ok(h) = p.elem_max(a, b).await {
3760            return Some(h);
3761        }
3762    }
3763    None
3764}
3765
3766/// Convenience: perform elementwise min via provider if possible; otherwise return None
3767pub async fn try_elem_min(a: &GpuTensorHandle, b: &GpuTensorHandle) -> Option<GpuTensorHandle> {
3768    if a.device_id == b.device_id {
3769        let p = provider_for_handle(a)?;
3770        if let Ok(h) = p.elem_min(a, b).await {
3771            return Some(h);
3772        }
3773    }
3774    None
3775}
3776
3777/// Convenience: perform elementwise atan2 via provider if possible; otherwise return None
3778pub async fn try_elem_atan2(y: &GpuTensorHandle, x: &GpuTensorHandle) -> Option<GpuTensorHandle> {
3779    if y.device_id == x.device_id {
3780        let p = provider_for_handle(y)?;
3781        if let Ok(h) = p.elem_atan2(y, x).await {
3782            return Some(h);
3783        }
3784    }
3785    None
3786}
3787
3788/// Physical element type carried by a numeric provider transfer.
3789///
3790/// This type is exhaustive over RunMat's real numeric classes and lives in the
3791/// acceleration API so providers do not depend on runtime or builtin storage
3792/// types.
3793#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
3794pub enum NumericElementType {
3795    F64,
3796    F32,
3797    I8,
3798    I16,
3799    I32,
3800    I64,
3801    U8,
3802    U16,
3803    U32,
3804    U64,
3805}
3806
3807impl NumericElementType {
3808    pub const fn element_size(self) -> usize {
3809        match self {
3810            Self::F64 | Self::I64 | Self::U64 => 8,
3811            Self::F32 | Self::I32 | Self::U32 => 4,
3812            Self::I16 | Self::U16 => 2,
3813            Self::I8 | Self::U8 => 1,
3814        }
3815    }
3816
3817    pub const fn class_name(self) -> &'static str {
3818        match self {
3819            Self::F64 => "double",
3820            Self::F32 => "single",
3821            Self::I8 => "int8",
3822            Self::I16 => "int16",
3823            Self::I32 => "int32",
3824            Self::I64 => "int64",
3825            Self::U8 => "uint8",
3826            Self::U16 => "uint16",
3827            Self::U32 => "uint32",
3828            Self::U64 => "uint64",
3829        }
3830    }
3831
3832    pub const fn precision(self) -> Option<ProviderPrecision> {
3833        match self {
3834            Self::F64 => Some(ProviderPrecision::F64),
3835            Self::F32 => Some(ProviderPrecision::F32),
3836            Self::I8
3837            | Self::I16
3838            | Self::I32
3839            | Self::I64
3840            | Self::U8
3841            | Self::U16
3842            | Self::U32
3843            | Self::U64 => None,
3844        }
3845    }
3846
3847    pub const fn integer_type(self) -> Option<IntegerElementType> {
3848        match self {
3849            Self::F64 | Self::F32 => None,
3850            Self::I8 => Some(IntegerElementType::I8),
3851            Self::I16 => Some(IntegerElementType::I16),
3852            Self::I32 => Some(IntegerElementType::I32),
3853            Self::I64 => Some(IntegerElementType::I64),
3854            Self::U8 => Some(IntegerElementType::U8),
3855            Self::U16 => Some(IntegerElementType::U16),
3856            Self::U32 => Some(IntegerElementType::U32),
3857            Self::U64 => Some(IntegerElementType::U64),
3858        }
3859    }
3860}
3861
3862impl From<IntegerElementType> for NumericElementType {
3863    fn from(value: IntegerElementType) -> Self {
3864        match value {
3865            IntegerElementType::I8 => Self::I8,
3866            IntegerElementType::I16 => Self::I16,
3867            IntegerElementType::I32 => Self::I32,
3868            IntegerElementType::I64 => Self::I64,
3869            IntegerElementType::U8 => Self::U8,
3870            IntegerElementType::U16 => Self::U16,
3871            IntegerElementType::U32 => Self::U32,
3872            IntegerElementType::U64 => Self::U64,
3873        }
3874    }
3875}
3876
3877/// Borrowed native numeric buffer used for provider transfers.
3878#[derive(Debug, Clone, Copy)]
3879pub enum HostNumericDataView<'a> {
3880    F64(&'a [f64]),
3881    F32(&'a [f32]),
3882    I8(&'a [i8]),
3883    I16(&'a [i16]),
3884    I32(&'a [i32]),
3885    I64(&'a [i64]),
3886    U8(&'a [u8]),
3887    U16(&'a [u16]),
3888    U32(&'a [u32]),
3889    U64(&'a [u64]),
3890}
3891
3892impl HostNumericDataView<'_> {
3893    pub const fn element_type(self) -> NumericElementType {
3894        match self {
3895            Self::F64(_) => NumericElementType::F64,
3896            Self::F32(_) => NumericElementType::F32,
3897            Self::I8(_) => NumericElementType::I8,
3898            Self::I16(_) => NumericElementType::I16,
3899            Self::I32(_) => NumericElementType::I32,
3900            Self::I64(_) => NumericElementType::I64,
3901            Self::U8(_) => NumericElementType::U8,
3902            Self::U16(_) => NumericElementType::U16,
3903            Self::U32(_) => NumericElementType::U32,
3904            Self::U64(_) => NumericElementType::U64,
3905        }
3906    }
3907
3908    pub const fn len(self) -> usize {
3909        match self {
3910            Self::F64(data) => data.len(),
3911            Self::F32(data) => data.len(),
3912            Self::I8(data) => data.len(),
3913            Self::I16(data) => data.len(),
3914            Self::I32(data) => data.len(),
3915            Self::I64(data) => data.len(),
3916            Self::U8(data) => data.len(),
3917            Self::U16(data) => data.len(),
3918            Self::U32(data) => data.len(),
3919            Self::U64(data) => data.len(),
3920        }
3921    }
3922
3923    pub const fn is_empty(self) -> bool {
3924        self.len() == 0
3925    }
3926}
3927
3928/// Borrowed tensor transfer with one native numeric payload and explicit
3929/// real/complex layout.
3930#[derive(Debug, Clone, Copy)]
3931pub struct HostNumericTensorView<'a> {
3932    pub data: HostNumericDataView<'a>,
3933    pub shape: &'a [usize],
3934    pub storage: GpuTensorStorage,
3935}
3936
3937impl HostNumericTensorView<'_> {
3938    pub fn validate(self) -> anyhow::Result<()> {
3939        let logical_len = self
3940            .shape
3941            .iter()
3942            .try_fold(1usize, |len, &dim| len.checked_mul(dim))
3943            .ok_or_else(|| anyhow!("numeric transfer shape overflows usize"))?;
3944        let expected = match self.storage {
3945            GpuTensorStorage::Real => logical_len,
3946            GpuTensorStorage::ComplexInterleaved => logical_len
3947                .checked_mul(2)
3948                .ok_or_else(|| anyhow!("complex numeric transfer length overflows usize"))?,
3949        };
3950        if self.data.len() != expected {
3951            return Err(anyhow!(
3952                "{} transfer has {} elements for shape {:?} and {:?} storage; expected {}",
3953                self.data.element_type().class_name(),
3954                self.data.len(),
3955                self.shape,
3956                self.storage,
3957                expected
3958            ));
3959        }
3960        Ok(())
3961    }
3962
3963    pub const fn element_type(self) -> NumericElementType {
3964        self.data.element_type()
3965    }
3966}
3967
3968/// Owned native numeric buffer returned by a provider download.
3969#[derive(Debug, Clone, PartialEq)]
3970pub enum HostNumericDataOwned {
3971    F64(Vec<f64>),
3972    F32(Vec<f32>),
3973    I8(Vec<i8>),
3974    I16(Vec<i16>),
3975    I32(Vec<i32>),
3976    I64(Vec<i64>),
3977    U8(Vec<u8>),
3978    U16(Vec<u16>),
3979    U32(Vec<u32>),
3980    U64(Vec<u64>),
3981}
3982
3983impl HostNumericDataOwned {
3984    pub const fn element_type(&self) -> NumericElementType {
3985        match self {
3986            Self::F64(_) => NumericElementType::F64,
3987            Self::F32(_) => NumericElementType::F32,
3988            Self::I8(_) => NumericElementType::I8,
3989            Self::I16(_) => NumericElementType::I16,
3990            Self::I32(_) => NumericElementType::I32,
3991            Self::I64(_) => NumericElementType::I64,
3992            Self::U8(_) => NumericElementType::U8,
3993            Self::U16(_) => NumericElementType::U16,
3994            Self::U32(_) => NumericElementType::U32,
3995            Self::U64(_) => NumericElementType::U64,
3996        }
3997    }
3998
3999    pub fn as_view(&self) -> HostNumericDataView<'_> {
4000        match self {
4001            Self::F64(data) => HostNumericDataView::F64(data),
4002            Self::F32(data) => HostNumericDataView::F32(data),
4003            Self::I8(data) => HostNumericDataView::I8(data),
4004            Self::I16(data) => HostNumericDataView::I16(data),
4005            Self::I32(data) => HostNumericDataView::I32(data),
4006            Self::I64(data) => HostNumericDataView::I64(data),
4007            Self::U8(data) => HostNumericDataView::U8(data),
4008            Self::U16(data) => HostNumericDataView::U16(data),
4009            Self::U32(data) => HostNumericDataView::U32(data),
4010            Self::U64(data) => HostNumericDataView::U64(data),
4011        }
4012    }
4013
4014    pub fn len(&self) -> usize {
4015        self.as_view().len()
4016    }
4017
4018    pub fn is_empty(&self) -> bool {
4019        self.len() == 0
4020    }
4021}
4022
4023/// Owned tensor transfer with one native numeric payload and explicit
4024/// real/complex layout.
4025#[derive(Debug, Clone, PartialEq)]
4026pub struct HostNumericTensorOwned {
4027    pub data: HostNumericDataOwned,
4028    pub shape: Vec<usize>,
4029    pub storage: GpuTensorStorage,
4030}
4031
4032impl HostNumericTensorOwned {
4033    pub fn as_view(&self) -> HostNumericTensorView<'_> {
4034        HostNumericTensorView {
4035            data: self.data.as_view(),
4036            shape: &self.shape,
4037            storage: self.storage,
4038        }
4039    }
4040
4041    pub fn validate(&self) -> anyhow::Result<()> {
4042        self.as_view().validate()
4043    }
4044}
4045
4046// Minimal host tensor views to avoid depending on runmat-builtins and cycles
4047#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4048pub struct HostTensorOwned {
4049    pub data: Vec<f64>,
4050    pub shape: Vec<usize>,
4051    pub storage: GpuTensorStorage,
4052}
4053
4054#[derive(Debug)]
4055pub struct HostTensorView<'a> {
4056    pub data: &'a [f64],
4057    pub shape: &'a [usize],
4058}
4059
4060/// Borrowed exact native-integer buffer used for provider transfers.
4061#[derive(Debug, Clone, Copy)]
4062pub enum HostIntegerDataView<'a> {
4063    I8(&'a [i8]),
4064    I16(&'a [i16]),
4065    I32(&'a [i32]),
4066    I64(&'a [i64]),
4067    U8(&'a [u8]),
4068    U16(&'a [u16]),
4069    U32(&'a [u32]),
4070    U64(&'a [u64]),
4071}
4072
4073impl HostIntegerDataView<'_> {
4074    pub fn element_type(self) -> IntegerElementType {
4075        match self {
4076            Self::I8(_) => IntegerElementType::I8,
4077            Self::I16(_) => IntegerElementType::I16,
4078            Self::I32(_) => IntegerElementType::I32,
4079            Self::I64(_) => IntegerElementType::I64,
4080            Self::U8(_) => IntegerElementType::U8,
4081            Self::U16(_) => IntegerElementType::U16,
4082            Self::U32(_) => IntegerElementType::U32,
4083            Self::U64(_) => IntegerElementType::U64,
4084        }
4085    }
4086
4087    pub fn len(self) -> usize {
4088        match self {
4089            Self::I8(data) => data.len(),
4090            Self::I16(data) => data.len(),
4091            Self::I32(data) => data.len(),
4092            Self::I64(data) => data.len(),
4093            Self::U8(data) => data.len(),
4094            Self::U16(data) => data.len(),
4095            Self::U32(data) => data.len(),
4096            Self::U64(data) => data.len(),
4097        }
4098    }
4099
4100    pub fn is_empty(self) -> bool {
4101        self.len() == 0
4102    }
4103}
4104
4105/// Borrowed exact integer tensor transfer payload.
4106#[derive(Debug, Clone, Copy)]
4107pub struct HostIntegerTensorView<'a> {
4108    pub data: HostIntegerDataView<'a>,
4109    pub shape: &'a [usize],
4110}
4111
4112/// Owned exact native-integer buffer returned by a provider download.
4113#[derive(Debug, Clone, PartialEq, Eq)]
4114pub enum HostIntegerDataOwned {
4115    I8(Vec<i8>),
4116    I16(Vec<i16>),
4117    I32(Vec<i32>),
4118    I64(Vec<i64>),
4119    U8(Vec<u8>),
4120    U16(Vec<u16>),
4121    U32(Vec<u32>),
4122    U64(Vec<u64>),
4123}
4124
4125impl HostIntegerDataOwned {
4126    pub fn element_type(&self) -> IntegerElementType {
4127        match self {
4128            Self::I8(_) => IntegerElementType::I8,
4129            Self::I16(_) => IntegerElementType::I16,
4130            Self::I32(_) => IntegerElementType::I32,
4131            Self::I64(_) => IntegerElementType::I64,
4132            Self::U8(_) => IntegerElementType::U8,
4133            Self::U16(_) => IntegerElementType::U16,
4134            Self::U32(_) => IntegerElementType::U32,
4135            Self::U64(_) => IntegerElementType::U64,
4136        }
4137    }
4138}
4139
4140/// Exact integer tensor returned by a provider download.
4141#[derive(Debug, Clone, PartialEq, Eq)]
4142pub struct HostIntegerTensorOwned {
4143    pub data: HostIntegerDataOwned,
4144    pub shape: Vec<usize>,
4145}
4146
4147impl<'a> From<HostTensorView<'a>> for HostNumericTensorView<'a> {
4148    fn from(value: HostTensorView<'a>) -> Self {
4149        Self {
4150            data: HostNumericDataView::F64(value.data),
4151            shape: value.shape,
4152            storage: GpuTensorStorage::Real,
4153        }
4154    }
4155}
4156
4157impl<'a> From<&'a HostTensorOwned> for HostNumericTensorView<'a> {
4158    fn from(value: &'a HostTensorOwned) -> Self {
4159        Self {
4160            data: HostNumericDataView::F64(&value.data),
4161            shape: &value.shape,
4162            storage: value.storage,
4163        }
4164    }
4165}
4166
4167impl<'a> From<HostIntegerDataView<'a>> for HostNumericDataView<'a> {
4168    fn from(value: HostIntegerDataView<'a>) -> Self {
4169        match value {
4170            HostIntegerDataView::I8(data) => Self::I8(data),
4171            HostIntegerDataView::I16(data) => Self::I16(data),
4172            HostIntegerDataView::I32(data) => Self::I32(data),
4173            HostIntegerDataView::I64(data) => Self::I64(data),
4174            HostIntegerDataView::U8(data) => Self::U8(data),
4175            HostIntegerDataView::U16(data) => Self::U16(data),
4176            HostIntegerDataView::U32(data) => Self::U32(data),
4177            HostIntegerDataView::U64(data) => Self::U64(data),
4178        }
4179    }
4180}
4181
4182impl<'a> From<HostIntegerTensorView<'a>> for HostNumericTensorView<'a> {
4183    fn from(value: HostIntegerTensorView<'a>) -> Self {
4184        Self {
4185            data: value.data.into(),
4186            shape: value.shape,
4187            storage: GpuTensorStorage::Real,
4188        }
4189    }
4190}
4191
4192impl From<HostIntegerDataOwned> for HostNumericDataOwned {
4193    fn from(value: HostIntegerDataOwned) -> Self {
4194        match value {
4195            HostIntegerDataOwned::I8(data) => Self::I8(data),
4196            HostIntegerDataOwned::I16(data) => Self::I16(data),
4197            HostIntegerDataOwned::I32(data) => Self::I32(data),
4198            HostIntegerDataOwned::I64(data) => Self::I64(data),
4199            HostIntegerDataOwned::U8(data) => Self::U8(data),
4200            HostIntegerDataOwned::U16(data) => Self::U16(data),
4201            HostIntegerDataOwned::U32(data) => Self::U32(data),
4202            HostIntegerDataOwned::U64(data) => Self::U64(data),
4203        }
4204    }
4205}
4206
4207impl From<HostIntegerTensorOwned> for HostNumericTensorOwned {
4208    fn from(value: HostIntegerTensorOwned) -> Self {
4209        Self {
4210            data: value.data.into(),
4211            shape: value.shape,
4212            storage: GpuTensorStorage::Real,
4213        }
4214    }
4215}
4216
4217/// Lightweight 1-D axis view used by provider meshgrid hooks.
4218#[derive(Debug)]
4219pub struct MeshgridAxisView<'a> {
4220    pub data: &'a [f64],
4221}
4222
4223/// Provider-side meshgrid result containing coordinate tensor handles.
4224#[derive(Debug, Clone)]
4225pub struct ProviderMeshgridResult {
4226    pub outputs: Vec<GpuTensorHandle>,
4227}
4228
4229/// Single resident GPU axis supplied to provider-side `ndgrid`.
4230#[derive(Debug, Clone, Copy)]
4231pub struct ProviderNdgridAxis<'a> {
4232    pub handle: &'a GpuTensorHandle,
4233}
4234
4235/// Provider-side `ndgrid` request. `output_shape` is already MATLAB-normalized
4236/// by the runtime, including the single-axis `[n, 1]` case.
4237#[derive(Debug, Clone, Copy)]
4238pub struct ProviderNdgridRequest<'a> {
4239    pub axes: &'a [ProviderNdgridAxis<'a>],
4240    pub output_shape: &'a [usize],
4241    pub output_count: usize,
4242}
4243
4244/// Provider-side ndgrid result containing coordinate tensor handles.
4245#[derive(Debug, Clone)]
4246pub struct ProviderNdgridResult {
4247    pub outputs: Vec<GpuTensorHandle>,
4248}
4249
4250/// Single broadcastable resident GPU input supplied to provider-side Black-Scholes pricing.
4251#[derive(Debug, Clone, Copy)]
4252pub struct ProviderBlackScholesPriceInput<'a> {
4253    pub handle: &'a GpuTensorHandle,
4254    /// Input shape aligned to `output_shape` rank using MATLAB implicit-expansion rules.
4255    pub shape: &'a [usize],
4256    /// Column-major strides for `shape`.
4257    pub strides: &'a [usize],
4258}
4259
4260/// Provider-side Black-Scholes price request. Inputs are ordered as
4261/// Price, Strike, Rate, Time, Volatility, Yield.
4262#[derive(Debug, Clone, Copy)]
4263pub struct ProviderBlackScholesPriceRequest<'a> {
4264    pub inputs: &'a [ProviderBlackScholesPriceInput<'a>],
4265    pub output_shape: &'a [usize],
4266    pub len: usize,
4267}
4268
4269/// Provider-side Black-Scholes price result containing call and put handles.
4270#[derive(Debug, Clone)]
4271pub struct ProviderBlackScholesPriceResult {
4272    pub call: GpuTensorHandle,
4273    pub put: GpuTensorHandle,
4274}
4275
4276/// Provider-side covariance-to-correlation result containing the correlation
4277/// matrix and column-vector standard deviations.
4278#[derive(Debug, Clone)]
4279pub struct ProviderCovarianceToCorrelationResult {
4280    pub correlation: GpuTensorHandle,
4281    pub sigma: GpuTensorHandle,
4282}
4283
4284/// Provider-side Adam optimizer update request.
4285#[derive(Debug, Clone, Copy)]
4286pub struct ProviderAdamUpdateRequest<'a> {
4287    pub parameters: &'a GpuTensorHandle,
4288    pub gradient: &'a GpuTensorHandle,
4289    pub average_grad: Option<&'a GpuTensorHandle>,
4290    pub average_sq_grad: Option<&'a GpuTensorHandle>,
4291    pub iteration: usize,
4292    pub learn_rate: f64,
4293    pub gradient_decay_factor: f64,
4294    pub squared_gradient_decay_factor: f64,
4295    pub epsilon: f64,
4296}
4297
4298/// Provider-side Adam optimizer update result.
4299#[derive(Debug, Clone)]
4300pub struct ProviderAdamUpdateResult {
4301    pub parameters: GpuTensorHandle,
4302    pub average_grad: GpuTensorHandle,
4303    pub average_sq_grad: GpuTensorHandle,
4304}
4305
4306/// Provider-side cross-entropy mode.
4307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4308pub enum ProviderCrossentropyMode {
4309    SingleLabel,
4310    MultiLabel,
4311}
4312
4313/// Provider-side cross-entropy request.
4314#[derive(Debug, Clone, Copy)]
4315pub struct ProviderCrossentropyRequest<'a> {
4316    pub predictions: &'a GpuTensorHandle,
4317    pub targets: &'a GpuTensorHandle,
4318    pub weights: Option<&'a GpuTensorHandle>,
4319    pub mask: Option<&'a GpuTensorHandle>,
4320    pub mode: ProviderCrossentropyMode,
4321}
4322
4323/// Provider-side cross-entropy result containing per-element loss terms.
4324#[derive(Debug, Clone)]
4325pub struct ProviderCrossentropyResult {
4326    pub losses: GpuTensorHandle,
4327}
4328
4329/// Descriptor for GEMM epilogues applied to `C = A * B` before storing to `C`.
4330///
4331/// Supported operations:
4332/// - Scale by `alpha` and add scalar `beta`.
4333/// - Multiply output by per-row and/or per-column scale vectors (broadcasted).
4334#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4335pub enum ScaleOp {
4336    Multiply,
4337    Divide,
4338}
4339
4340#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4341pub struct MatmulEpilogue {
4342    /// Scalar multiply applied to each output element.
4343    pub alpha: f64,
4344    /// Scalar add applied to each output element after scaling.
4345    pub beta: f64,
4346    /// Optional per-row scale (length m). When present, output[row, col] *= row_scale[row].
4347    pub row_scale: Option<GpuTensorHandle>,
4348    /// Optional per-column scale (length n). When present, output[row, col] *= col_scale[col].
4349    pub col_scale: Option<GpuTensorHandle>,
4350    /// Row scale operation (multiply or divide). Ignored when `row_scale` is None.
4351    pub row_op: ScaleOp,
4352    /// Column scale operation (multiply or divide). Ignored when `col_scale` is None.
4353    pub col_op: ScaleOp,
4354    /// Optional lower clamp bound applied after scale/bias.
4355    #[serde(default)]
4356    pub clamp_min: Option<f64>,
4357    /// Optional upper clamp bound applied after scale/bias.
4358    #[serde(default)]
4359    pub clamp_max: Option<f64>,
4360    /// Optional power exponent applied after clamp (final operation in the epilogue).
4361    #[serde(default)]
4362    pub pow_exponent: Option<f64>,
4363    /// Optional output buffer for the diagonal of the result (length min(m, n)).
4364    #[serde(default)]
4365    pub diag_output: Option<GpuTensorHandle>,
4366}
4367
4368impl MatmulEpilogue {
4369    pub fn noop() -> Self {
4370        Self {
4371            alpha: 1.0,
4372            beta: 0.0,
4373            row_scale: None,
4374            col_scale: None,
4375            row_op: ScaleOp::Multiply,
4376            col_op: ScaleOp::Multiply,
4377            clamp_min: None,
4378            clamp_max: None,
4379            pow_exponent: None,
4380            diag_output: None,
4381        }
4382    }
4383    pub fn is_noop(&self) -> bool {
4384        self.alpha == 1.0
4385            && self.beta == 0.0
4386            && self.row_scale.is_none()
4387            && self.col_scale.is_none()
4388            && self.clamp_min.is_none()
4389            && self.clamp_max.is_none()
4390            && self.pow_exponent.is_none()
4391            && self.diag_output.is_none()
4392    }
4393}
4394
4395#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
4396pub struct PowerStepEpilogue {
4397    pub epsilon: f64,
4398}
4399
4400impl Default for PowerStepEpilogue {
4401    fn default() -> Self {
4402        Self { epsilon: 0.0 }
4403    }
4404}
4405
4406#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
4407pub struct ImageNormalizeDescriptor {
4408    pub batch: usize,
4409    pub height: usize,
4410    pub width: usize,
4411    pub epsilon: f64,
4412    #[serde(default)]
4413    pub gain: Option<f64>,
4414    #[serde(default)]
4415    pub bias: Option<f64>,
4416    #[serde(default)]
4417    pub gamma: Option<f64>,
4418    #[serde(default = "default_image_normalize_clamp_zero")]
4419    pub clamp_zero: bool,
4420}
4421
4422fn default_image_normalize_clamp_zero() -> bool {
4423    true
4424}
4425
4426#[cfg(test)]
4427mod tests {
4428    use super::*;
4429
4430    struct TestProvider {
4431        device_id: u32,
4432        name: &'static str,
4433        spawn_concurrency: SpawnHandleConcurrency,
4434    }
4435
4436    struct DefaultNumericAdapterProvider;
4437
4438    impl AccelProvider for DefaultNumericAdapterProvider {
4439        fn upload(&self, host: &HostTensorView) -> anyhow::Result<GpuTensorHandle> {
4440            assert_eq!(host.data, &[1.0, 2.0]);
4441            Ok(GpuTensorHandle {
4442                shape: host.shape.to_vec(),
4443                device_id: 404,
4444                buffer_id: 1,
4445                descriptor: GpuTensorDescriptor::numeric(
4446                    NumericElementType::F64,
4447                    GpuTensorStorage::Real,
4448                ),
4449            })
4450        }
4451
4452        fn download<'a>(&'a self, h: &'a GpuTensorHandle) -> AccelDownloadFuture<'a> {
4453            Box::pin(async move {
4454                Ok(HostTensorOwned {
4455                    data: vec![1.0, 2.0],
4456                    shape: h.shape.clone(),
4457                    storage: GpuTensorStorage::Real,
4458                })
4459            })
4460        }
4461
4462        fn upload_integer(&self, host: &HostIntegerTensorView) -> anyhow::Result<GpuTensorHandle> {
4463            assert!(matches!(host.data, HostIntegerDataView::U64([1, u64::MAX])));
4464            Ok(GpuTensorHandle {
4465                shape: host.shape.to_vec(),
4466                device_id: 404,
4467                buffer_id: 2,
4468                descriptor: GpuTensorDescriptor::numeric(
4469                    NumericElementType::U64,
4470                    GpuTensorStorage::Real,
4471                ),
4472            })
4473        }
4474
4475        fn download_integer<'a>(
4476            &'a self,
4477            h: &'a GpuTensorHandle,
4478        ) -> AccelIntegerDownloadFuture<'a> {
4479            Box::pin(async move {
4480                Ok(HostIntegerTensorOwned {
4481                    data: HostIntegerDataOwned::U64(vec![1, u64::MAX]),
4482                    shape: h.shape.clone(),
4483                })
4484            })
4485        }
4486
4487        fn free(&self, _h: &GpuTensorHandle) -> anyhow::Result<()> {
4488            Ok(())
4489        }
4490
4491        fn device_info(&self) -> String {
4492            "legacy-numeric-provider".to_string()
4493        }
4494    }
4495
4496    impl AccelProvider for TestProvider {
4497        fn upload(&self, _host: &HostTensorView) -> anyhow::Result<GpuTensorHandle> {
4498            Err(anyhow!("test provider upload should not be called"))
4499        }
4500
4501        fn download<'a>(&'a self, _h: &'a GpuTensorHandle) -> AccelDownloadFuture<'a> {
4502            unsupported_future("test provider download should not be called")
4503        }
4504
4505        fn free(&self, _h: &GpuTensorHandle) -> anyhow::Result<()> {
4506            Err(anyhow!("test provider free should not be called"))
4507        }
4508
4509        fn device_info(&self) -> String {
4510            self.name.to_string()
4511        }
4512
4513        fn device_id(&self) -> u32 {
4514            self.device_id
4515        }
4516
4517        fn spawn_handle_concurrency(&self) -> SpawnHandleConcurrency {
4518            self.spawn_concurrency
4519        }
4520    }
4521
4522    static PROVIDER_TEST_LOCK: Lazy<std::sync::Mutex<()>> = Lazy::new(|| std::sync::Mutex::new(()));
4523    static PROVIDER_A: TestProvider = TestProvider {
4524        device_id: 101,
4525        name: "provider-a",
4526        spawn_concurrency: SpawnHandleConcurrency::ImmutableShare,
4527    };
4528    static PROVIDER_B: TestProvider = TestProvider {
4529        device_id: 202,
4530        name: "provider-b",
4531        spawn_concurrency: SpawnHandleConcurrency::Reject,
4532    };
4533    static PROVIDER_C: TestProvider = TestProvider {
4534        device_id: 303,
4535        name: "provider-c",
4536        spawn_concurrency: SpawnHandleConcurrency::CopyOnWrite,
4537    };
4538
4539    fn resolve_ready<F: Future>(future: F) -> F::Output {
4540        struct NoopWake;
4541        impl std::task::Wake for NoopWake {
4542            fn wake(self: std::sync::Arc<Self>) {}
4543        }
4544        let waker = std::task::Waker::from(std::sync::Arc::new(NoopWake));
4545        let mut context = std::task::Context::from_waker(&waker);
4546        let mut future = std::pin::pin!(future);
4547        match future.as_mut().poll(&mut context) {
4548            std::task::Poll::Ready(output) => output,
4549            std::task::Poll::Pending => panic!("test adapter future unexpectedly pending"),
4550        }
4551    }
4552
4553    fn register_test_providers() {
4554        clear_provider();
4555        unsafe {
4556            register_provider(&PROVIDER_A);
4557            register_provider(&PROVIDER_B);
4558        }
4559    }
4560
4561    fn test_handle(device_id: u32) -> GpuTensorHandle {
4562        GpuTensorHandle::new(vec![1], device_id, 42)
4563    }
4564
4565    #[test]
4566    fn numeric_transfer_contract_is_exhaustive_and_native() {
4567        let f64_values = [1.0_f64, 2.0];
4568        let f32_values = [1.0_f32, 2.0];
4569        let i8_values = [1_i8, 2];
4570        let i16_values = [1_i16, 2];
4571        let i32_values = [1_i32, 2];
4572        let i64_values = [1_i64, 9_007_199_254_740_993];
4573        let u8_values = [1_u8, 2];
4574        let u16_values = [1_u16, 2];
4575        let u32_values = [1_u32, 2];
4576        let u64_values = [1_u64, 9_007_199_254_740_993];
4577        let cases = [
4578            (
4579                HostNumericDataView::F64(&f64_values),
4580                NumericElementType::F64,
4581            ),
4582            (
4583                HostNumericDataView::F32(&f32_values),
4584                NumericElementType::F32,
4585            ),
4586            (HostNumericDataView::I8(&i8_values), NumericElementType::I8),
4587            (
4588                HostNumericDataView::I16(&i16_values),
4589                NumericElementType::I16,
4590            ),
4591            (
4592                HostNumericDataView::I32(&i32_values),
4593                NumericElementType::I32,
4594            ),
4595            (
4596                HostNumericDataView::I64(&i64_values),
4597                NumericElementType::I64,
4598            ),
4599            (HostNumericDataView::U8(&u8_values), NumericElementType::U8),
4600            (
4601                HostNumericDataView::U16(&u16_values),
4602                NumericElementType::U16,
4603            ),
4604            (
4605                HostNumericDataView::U32(&u32_values),
4606                NumericElementType::U32,
4607            ),
4608            (
4609                HostNumericDataView::U64(&u64_values),
4610                NumericElementType::U64,
4611            ),
4612        ];
4613
4614        for (data, element_type) in cases {
4615            let transfer = HostNumericTensorView {
4616                data,
4617                shape: &[1, 2],
4618                storage: GpuTensorStorage::Real,
4619            };
4620            transfer.validate().expect("matching native transfer");
4621            assert_eq!(transfer.element_type(), element_type);
4622            assert_eq!(element_type.class_name(), data.element_type().class_name());
4623            assert_eq!(
4624                element_type.element_size(),
4625                data.element_type().element_size()
4626            );
4627        }
4628
4629        assert_eq!(
4630            NumericElementType::F64.precision(),
4631            Some(ProviderPrecision::F64)
4632        );
4633        assert_eq!(
4634            NumericElementType::F32.precision(),
4635            Some(ProviderPrecision::F32)
4636        );
4637        assert_eq!(
4638            NumericElementType::U64.integer_type(),
4639            Some(IntegerElementType::U64)
4640        );
4641        assert_eq!(
4642            NumericElementType::I64.integer_type(),
4643            Some(IntegerElementType::I64)
4644        );
4645        assert_eq!(NumericElementType::F64.integer_type(), None);
4646    }
4647
4648    #[test]
4649    fn numeric_transfer_contract_validates_real_and_complex_lengths() {
4650        let real = HostNumericTensorOwned {
4651            data: HostNumericDataOwned::F32(vec![1.0, 2.0]),
4652            shape: vec![2, 1],
4653            storage: GpuTensorStorage::Real,
4654        };
4655        real.validate().expect("native single real transfer");
4656
4657        let complex = HostNumericTensorOwned {
4658            data: HostNumericDataOwned::U64(vec![1, u64::MAX, 2, u64::MAX - 1]),
4659            shape: vec![1, 2],
4660            storage: GpuTensorStorage::ComplexInterleaved,
4661        };
4662        complex.validate().expect("native complex integer transfer");
4663
4664        let mismatch = HostNumericTensorView {
4665            data: HostNumericDataView::I16(&[1, 2, 3]),
4666            shape: &[2, 2],
4667            storage: GpuTensorStorage::Real,
4668        };
4669        let error = mismatch.validate().expect_err("shape mismatch must reject");
4670        assert!(error.to_string().contains("expected 4"));
4671
4672        let overflow = HostNumericTensorView {
4673            data: HostNumericDataView::F64(&[]),
4674            shape: &[usize::MAX, 2],
4675            storage: GpuTensorStorage::Real,
4676        };
4677        assert!(overflow.validate().is_err());
4678    }
4679
4680    #[test]
4681    fn gpu_handle_descriptor_survives_clone_serialization_and_semantic_metadata_cleanup() {
4682        let handle = GpuTensorHandle::new(vec![2, 3], 17, 29)
4683            .with_numeric_descriptor(
4684                NumericElementType::U64,
4685                GpuTensorStorage::ComplexInterleaved,
4686            )
4687            .with_provenance(GpuHandleProvenance::Explicit);
4688        let cloned = handle.clone();
4689        clear_handle_metadata(&handle);
4690        assert_eq!(handle_integer_type(&cloned), Some(IntegerElementType::U64));
4691        assert_eq!(handle_precision(&cloned), None);
4692        assert_eq!(
4693            handle_storage(&cloned),
4694            GpuTensorStorage::ComplexInterleaved
4695        );
4696        assert_eq!(
4697            handle_provenance(&cloned),
4698            Some(GpuHandleProvenance::Explicit)
4699        );
4700        assert_eq!(handle_class_name(&cloned).as_deref(), Some("uint64"));
4701
4702        let encoded = serde_json::to_string(&cloned).expect("serialize durable handle");
4703        let decoded: GpuTensorHandle =
4704            serde_json::from_str(&encoded).expect("deserialize durable handle");
4705        assert_eq!(decoded, cloned);
4706
4707        let legacy: GpuTensorHandle =
4708            serde_json::from_str(r#"{"shape":[1,1],"device_id":5,"buffer_id":8}"#)
4709                .expect("deserialize legacy handle");
4710        assert_eq!(legacy.descriptor, GpuTensorDescriptor::default());
4711        assert_eq!(handle_storage(&legacy), GpuTensorStorage::Real);
4712    }
4713
4714    #[test]
4715    fn durable_physical_descriptor_is_the_only_numeric_authority() {
4716        let floating = GpuTensorHandle::new(vec![1, 2], 23, 31).with_numeric_descriptor(
4717            NumericElementType::F32,
4718            GpuTensorStorage::ComplexInterleaved,
4719        );
4720
4721        assert_eq!(handle_precision(&floating), Some(ProviderPrecision::F32));
4722        assert_eq!(handle_integer_type(&floating), None);
4723        assert_eq!(
4724            handle_storage(&floating),
4725            GpuTensorStorage::ComplexInterleaved
4726        );
4727        assert_eq!(handle_class_name(&floating).as_deref(), Some("single"));
4728        clear_handle_metadata(&floating);
4729
4730        let integer = GpuTensorHandle::new(vec![1, 2], 37, 41)
4731            .with_numeric_descriptor(NumericElementType::U64, GpuTensorStorage::Real);
4732
4733        assert_eq!(handle_precision(&integer), None);
4734        assert_eq!(handle_integer_type(&integer), Some(IntegerElementType::U64));
4735        assert_eq!(handle_class_name(&integer).as_deref(), Some("uint64"));
4736        clear_handle_metadata(&integer);
4737    }
4738
4739    #[test]
4740    fn removed_physical_side_metadata_surface_stays_absent() {
4741        let source = include_str!("lib.rs");
4742        let forbidden = [
4743            ["set_handle_", "precision"].concat(),
4744            ["set_handle_", "integer_type"].concat(),
4745            ["set_handle_", "storage"].concat(),
4746            ["clear_handle_", "precision"].concat(),
4747            ["clear_handle_", "integer_type"].concat(),
4748            ["clear_handle_", "storage"].concat(),
4749            ["HANDLE_", "PRECISIONS"].concat(),
4750            ["HANDLE_", "INTEGER_TYPES"].concat(),
4751            ["HANDLE_", "STORAGES"].concat(),
4752        ];
4753        for symbol in forbidden {
4754            assert!(
4755                !source.contains(&symbol),
4756                "removed symbol returned: {symbol}"
4757            );
4758        }
4759    }
4760
4761    #[test]
4762    fn numeric_transfer_default_adapter_preserves_double_and_integer_storage() {
4763        let provider = DefaultNumericAdapterProvider;
4764        let double = HostNumericTensorView {
4765            data: HostNumericDataView::F64(&[1.0, 2.0]),
4766            shape: &[1, 2],
4767            storage: GpuTensorStorage::Real,
4768        };
4769        let double_handle = provider
4770            .upload_numeric(&double)
4771            .expect("double upload adapter");
4772        let double_download = resolve_ready(provider.download_numeric(&double_handle))
4773            .expect("double download adapter");
4774        assert_eq!(
4775            double_download.data,
4776            HostNumericDataOwned::F64(vec![1.0, 2.0])
4777        );
4778
4779        let integer = HostNumericTensorView {
4780            data: HostNumericDataView::U64(&[1, u64::MAX]),
4781            shape: &[1, 2],
4782            storage: GpuTensorStorage::Real,
4783        };
4784        let integer_handle = provider
4785            .upload_numeric(&integer)
4786            .expect("integer upload adapter");
4787        let integer_download = resolve_ready(provider.download_numeric(&integer_handle))
4788            .expect("integer download adapter");
4789        assert_eq!(
4790            integer_download.data,
4791            HostNumericDataOwned::U64(vec![1, u64::MAX])
4792        );
4793        clear_handle_metadata(&integer_handle);
4794    }
4795
4796    #[test]
4797    fn numeric_transfer_default_adapter_rejects_widened_single_and_complex_integer() {
4798        let provider = DefaultNumericAdapterProvider;
4799        let single = HostNumericTensorView {
4800            data: HostNumericDataView::F32(&[1.0, 2.0]),
4801            shape: &[1, 2],
4802            storage: GpuTensorStorage::Real,
4803        };
4804        assert!(provider.upload_numeric(&single).is_err());
4805
4806        let complex_integer = HostNumericTensorView {
4807            data: HostNumericDataView::I16(&[1, 2, 3, 4]),
4808            shape: &[1, 2],
4809            storage: GpuTensorStorage::ComplexInterleaved,
4810        };
4811        assert!(provider.upload_numeric(&complex_integer).is_err());
4812
4813        let single_handle = GpuTensorHandle {
4814            shape: vec![1, 2],
4815            device_id: 404,
4816            buffer_id: 3,
4817            descriptor: GpuTensorDescriptor::numeric(
4818                NumericElementType::F32,
4819                GpuTensorStorage::Real,
4820            ),
4821        };
4822        let error = resolve_ready(provider.download_numeric(&single_handle))
4823            .expect_err("widened single download must reject");
4824        assert!(error.to_string().contains("native single"));
4825        clear_handle_metadata(&single_handle);
4826
4827        let complex_integer_handle = GpuTensorHandle {
4828            shape: vec![1, 2],
4829            device_id: 404,
4830            buffer_id: 4,
4831            descriptor: GpuTensorDescriptor::numeric(
4832                NumericElementType::I16,
4833                GpuTensorStorage::ComplexInterleaved,
4834            ),
4835        };
4836        let error = resolve_ready(provider.download_numeric(&complex_integer_handle))
4837            .expect_err("complex integer default download must reject");
4838        assert!(error.to_string().contains("complex integer"));
4839        clear_handle_metadata(&complex_integer_handle);
4840
4841        let descriptorless = GpuTensorHandle::new(vec![1, 2], 404, 5);
4842        let error = resolve_ready(provider.download_numeric(&descriptorless))
4843            .expect_err("descriptorless numeric download must reject");
4844        assert!(error.to_string().contains("durable element descriptor"));
4845    }
4846
4847    #[test]
4848    fn conservative_feasibility_rejects_operations_without_execution() {
4849        let query = ProviderFeasibilityQuery {
4850            operation: ProviderOperationIdentity::new("legacy.elementwise"),
4851            family: ProviderOperationFamily::Elementwise,
4852            inputs: vec![ProviderRepresentation {
4853                element_type: ProviderElementType::F64,
4854                storage: ProviderStorage::DenseReal,
4855                layout: ProviderLayout::ColumnMajorContiguous,
4856                shape: vec![4, 4],
4857                residency: ProviderResidency::Host,
4858            }],
4859            outputs: Vec::new(),
4860            workload: ProviderWorkload {
4861                elements: Some(16),
4862                ..ProviderWorkload::default()
4863            },
4864        };
4865
4866        assert!(matches!(
4867            PROVIDER_A.query_feasibility(&query),
4868            ProviderFeasibility::Rejected {
4869                rejection: ProviderRejection {
4870                    code: ProviderRejectionCode::UnsupportedOperation,
4871                    ..
4872                }
4873            }
4874        ));
4875        assert!(!PROVIDER_A
4876            .capability_snapshot()
4877            .supports(ProviderOperationFamily::Elementwise));
4878
4879        let snapshot = PROVIDER_A.capability_snapshot();
4880        let encoded = serde_json::to_string(&snapshot).expect("serialize capability snapshot");
4881        let decoded: ProviderCapabilitySnapshot =
4882            serde_json::from_str(&encoded).expect("deserialize capability snapshot");
4883        assert_eq!(decoded, snapshot);
4884    }
4885
4886    #[test]
4887    fn semantic_handle_metadata_is_namespaced_by_device_and_buffer() {
4888        let mut first = test_handle(PROVIDER_A.device_id())
4889            .with_numeric_descriptor(NumericElementType::F32, GpuTensorStorage::Real);
4890        let mut second = test_handle(PROVIDER_B.device_id()).with_numeric_descriptor(
4891            NumericElementType::U64,
4892            GpuTensorStorage::ComplexInterleaved,
4893        );
4894        clear_handle_metadata(&first);
4895        clear_handle_metadata(&second);
4896
4897        set_handle_class_name(&first, "single");
4898        set_handle_class_name(&second, "uint64");
4899        set_handle_logical(&first, true);
4900        record_handle_transpose(&first, 2, 3);
4901        mark_handle_automatic(&mut first);
4902        mark_handle_explicit(&mut second);
4903
4904        assert_eq!(handle_precision(&first), Some(ProviderPrecision::F32));
4905        assert_eq!(handle_precision(&second), None);
4906        assert_eq!(handle_class_name(&first).as_deref(), Some("logical"));
4907        assert_eq!(handle_class_name(&second).as_deref(), Some("uint64"));
4908        assert_eq!(handle_integer_type(&first), None);
4909        assert_eq!(handle_integer_type(&second), Some(IntegerElementType::U64));
4910        assert!(handle_is_logical(&first));
4911        assert!(!handle_is_logical(&second));
4912        assert_eq!(
4913            handle_transpose_info(&first).map(|info| (info.base_rows, info.base_cols)),
4914            Some((2, 3))
4915        );
4916        assert_eq!(handle_transpose_info(&second), None);
4917        assert_eq!(handle_storage(&first), GpuTensorStorage::Real);
4918        assert_eq!(
4919            handle_storage(&second),
4920            GpuTensorStorage::ComplexInterleaved
4921        );
4922        assert_eq!(
4923            handle_provenance(&first),
4924            Some(GpuHandleProvenance::Automatic)
4925        );
4926        assert!(handle_is_explicit(&second));
4927
4928        clear_handle_metadata(&first);
4929        assert_eq!(handle_precision(&first), Some(ProviderPrecision::F32));
4930        assert_eq!(handle_class_name(&first).as_deref(), Some("single"));
4931        assert!(!handle_is_logical(&first));
4932        assert_eq!(handle_transpose_info(&first), None);
4933        assert_eq!(
4934            handle_provenance(&first),
4935            Some(GpuHandleProvenance::Automatic)
4936        );
4937        assert_eq!(handle_precision(&second), None);
4938        assert_eq!(handle_integer_type(&second), Some(IntegerElementType::U64));
4939        assert!(handle_is_explicit(&second));
4940        clear_handle_metadata(&second);
4941    }
4942
4943    fn spectral_request<'a>(
4944        input: &'a GpuTensorHandle,
4945        frame_mode: ProviderSpectralFrameMode,
4946    ) -> ProviderSpectralRequest<'a> {
4947        static WINDOW: [f64; 4] = [1.0, 1.0, 1.0, 1.0];
4948        ProviderSpectralRequest {
4949            input,
4950            input_len: 16,
4951            input_complex: false,
4952            window: &WINDOW,
4953            nfft: 8,
4954            frame_count: 3,
4955            frame_mode,
4956            range: ProviderSpectralRange::Onesided,
4957            denominator: 1.0,
4958        }
4959    }
4960
4961    #[test]
4962    fn provider_envelope_shape_guard_rejects_equal_len_layout_spoofing() {
4963        assert!(provider_envelope_input_shape_matches(&[2, 3], 2, 3));
4964        assert!(provider_envelope_input_shape_matches(&[6, 1], 6, 1));
4965        assert!(provider_envelope_input_shape_matches(&[1, 6], 6, 1));
4966        assert!(provider_envelope_input_shape_matches(&[6], 6, 1));
4967
4968        assert!(!provider_envelope_input_shape_matches(&[3, 2], 2, 3));
4969        assert!(!provider_envelope_input_shape_matches(&[6], 2, 3));
4970        assert!(!provider_envelope_input_shape_matches(&[2, 1, 3], 2, 3));
4971    }
4972
4973    #[test]
4974    fn provider_for_device_prefers_registered_device_over_thread_provider() {
4975        let _lock = PROVIDER_TEST_LOCK
4976            .lock()
4977            .expect("provider test lock poisoned");
4978        register_test_providers();
4979        let _thread_provider = ThreadProviderGuard::set(Some(&PROVIDER_B));
4980
4981        let provider = provider_for_device(PROVIDER_A.device_id()).expect("provider for device");
4982
4983        assert_eq!(provider.device_info(), PROVIDER_A.name);
4984        clear_provider();
4985    }
4986
4987    #[test]
4988    fn provider_for_handle_uses_handle_device_owner() {
4989        let _lock = PROVIDER_TEST_LOCK
4990            .lock()
4991            .expect("provider test lock poisoned");
4992        register_test_providers();
4993        let _thread_provider = ThreadProviderGuard::set(Some(&PROVIDER_B));
4994
4995        let provider =
4996            provider_for_handle(&test_handle(PROVIDER_A.device_id())).expect("provider for handle");
4997
4998        assert_eq!(provider.device_info(), PROVIDER_A.name);
4999        clear_provider();
5000    }
5001
5002    #[test]
5003    fn clearing_active_provider_preserves_live_handle_owner() {
5004        let _lock = PROVIDER_TEST_LOCK
5005            .lock()
5006            .expect("provider test lock poisoned");
5007        register_test_providers();
5008        let handle = test_handle(PROVIDER_A.device_id());
5009
5010        clear_provider();
5011
5012        assert!(provider().is_none());
5013        let owner = provider_for_handle(&handle).expect("registered handle owner survives clear");
5014        assert_eq!(owner.device_info(), PROVIDER_A.name);
5015    }
5016
5017    #[test]
5018    fn concurrent_registration_keeps_each_threads_active_provider() {
5019        let _lock = PROVIDER_TEST_LOCK
5020            .lock()
5021            .expect("provider test lock poisoned");
5022        clear_provider();
5023
5024        let first = std::thread::spawn(|| {
5025            unsafe { register_provider(&PROVIDER_A) };
5026            std::thread::yield_now();
5027            provider().map(AccelProvider::device_info)
5028        });
5029        let second = std::thread::spawn(|| {
5030            unsafe { register_provider(&PROVIDER_B) };
5031            std::thread::yield_now();
5032            provider().map(AccelProvider::device_info)
5033        });
5034
5035        assert_eq!(
5036            first.join().expect("first registration thread"),
5037            Some(PROVIDER_A.name.to_string())
5038        );
5039        assert_eq!(
5040            second.join().expect("second registration thread"),
5041            Some(PROVIDER_B.name.to_string())
5042        );
5043        clear_provider();
5044    }
5045
5046    #[test]
5047    fn spawn_handle_concurrency_for_uses_registered_owner() {
5048        let _lock = PROVIDER_TEST_LOCK
5049            .lock()
5050            .expect("provider test lock poisoned");
5051        register_test_providers();
5052        let _thread_provider = ThreadProviderGuard::set(Some(&PROVIDER_B));
5053
5054        let concurrency = spawn_handle_concurrency_for(&test_handle(PROVIDER_A.device_id()))
5055            .expect("spawn concurrency");
5056
5057        assert_eq!(concurrency, PROVIDER_A.spawn_concurrency);
5058        clear_provider();
5059    }
5060
5061    #[test]
5062    fn provider_keeps_thread_local_active_provider_semantics() {
5063        let _lock = PROVIDER_TEST_LOCK
5064            .lock()
5065            .expect("provider test lock poisoned");
5066        register_test_providers();
5067        let _thread_provider = ThreadProviderGuard::set(Some(&PROVIDER_A));
5068
5069        let active = provider().expect("active provider");
5070
5071        assert_eq!(active.device_info(), PROVIDER_A.name);
5072        clear_provider();
5073    }
5074
5075    #[test]
5076    fn unregistered_thread_provider_only_matches_own_device_before_global_fallback() {
5077        let _lock = PROVIDER_TEST_LOCK
5078            .lock()
5079            .expect("provider test lock poisoned");
5080        clear_provider();
5081        unsafe {
5082            register_provider(&PROVIDER_A);
5083        }
5084        let _thread_provider = ThreadProviderGuard::set(Some(&PROVIDER_C));
5085
5086        let own_device = provider_for_device(PROVIDER_C.device_id()).expect("own provider");
5087        let fallback = provider_for_device(404).expect("global fallback provider");
5088
5089        assert_eq!(own_device.device_info(), PROVIDER_C.name);
5090        assert_eq!(fallback.device_info(), PROVIDER_A.name);
5091        clear_provider();
5092    }
5093
5094    #[test]
5095    fn uniform_spectral_request_validates_sliding_input_coverage() {
5096        let input = test_handle(PROVIDER_A.device_id());
5097        let mut request = spectral_request(&input, ProviderSpectralFrameMode::Sliding { hop: 6 });
5098        assert!(validate_uniform_spectral_request(&request).is_ok());
5099
5100        request.input_len = 15;
5101        assert!(validate_uniform_spectral_request(&request).is_err());
5102    }
5103
5104    #[test]
5105    fn uniform_spectral_request_rejects_sliding_coverage_overflow() {
5106        let input = test_handle(PROVIDER_A.device_id());
5107        let mut request = spectral_request(&input, ProviderSpectralFrameMode::Sliding { hop: 2 });
5108        request.frame_count = usize::MAX;
5109
5110        assert!(validate_uniform_spectral_request(&request).is_err());
5111    }
5112
5113    #[test]
5114    fn uniform_spectral_request_validates_folded_input_coverage() {
5115        let input = test_handle(PROVIDER_A.device_id());
5116        let mut request = spectral_request(
5117            &input,
5118            ProviderSpectralFrameMode::FoldedColumns { input_rows: 5 },
5119        );
5120        assert!(validate_uniform_spectral_request(&request).is_ok());
5121
5122        request.frame_mode = ProviderSpectralFrameMode::FoldedColumns { input_rows: 0 };
5123        assert!(validate_uniform_spectral_request(&request).is_err());
5124
5125        request.frame_mode = ProviderSpectralFrameMode::FoldedColumns { input_rows: 6 };
5126        assert!(validate_uniform_spectral_request(&request).is_err());
5127    }
5128
5129    #[test]
5130    fn uniform_spectral_request_rejects_folded_coverage_overflow() {
5131        let input = test_handle(PROVIDER_A.device_id());
5132        let request = spectral_request(
5133            &input,
5134            ProviderSpectralFrameMode::FoldedColumns {
5135                input_rows: usize::MAX,
5136            },
5137        );
5138
5139        assert!(validate_uniform_spectral_request(&request).is_err());
5140    }
5141
5142    #[test]
5143    fn image_normalize_descriptor_omitted_clamp_zero_defaults_true() {
5144        let payload = r#"{
5145            "batch": 2,
5146            "height": 4,
5147            "width": 5,
5148            "epsilon": 0.000001
5149        }"#;
5150
5151        let desc: ImageNormalizeDescriptor =
5152            serde_json::from_str(payload).expect("deserialize descriptor");
5153
5154        assert!(
5155            desc.clamp_zero,
5156            "legacy serialized descriptors should default to clamped image normalize"
5157        );
5158    }
5159
5160    #[test]
5161    fn image_normalize_descriptor_explicit_false_preserves_unclamped() {
5162        let payload = r#"{
5163            "batch": 2,
5164            "height": 4,
5165            "width": 5,
5166            "epsilon": 0.000001,
5167            "clamp_zero": false
5168        }"#;
5169
5170        let desc: ImageNormalizeDescriptor =
5171            serde_json::from_str(payload).expect("deserialize descriptor");
5172
5173        assert!(
5174            !desc.clamp_zero,
5175            "explicit clamp_zero=false should preserve unclamped semantics"
5176        );
5177    }
5178}