Skip to main content

runmat_runtime/builtins/common/
gpu_helpers.rs

1use runmat_accelerate_api::{
2    AccelProvider, GpuTensorHandle, GpuTensorStorage, HostIntegerDataOwned, HostNumericDataOwned,
3    HostNumericDataView, HostNumericTensorOwned, HostNumericTensorView, HostTensorOwned,
4    IntegerElementType, NumericElementType, ProviderPrecision,
5};
6use runmat_value::{
7    ComplexStorage, ComplexTensor, IntegerStorage, LogicalArray, NumericDType, Tensor, Value,
8};
9use runmat_value::{IntegerComplexStorage, NumericScalar, NumericStorage};
10
11use crate::build_runtime_error;
12
13pub fn integer_storage_from_owned(data: HostIntegerDataOwned) -> IntegerStorage {
14    match data {
15        HostIntegerDataOwned::I8(values) => IntegerStorage::I8(values),
16        HostIntegerDataOwned::I16(values) => IntegerStorage::I16(values),
17        HostIntegerDataOwned::I32(values) => IntegerStorage::I32(values),
18        HostIntegerDataOwned::I64(values) => IntegerStorage::I64(values),
19        HostIntegerDataOwned::U8(values) => IntegerStorage::U8(values),
20        HostIntegerDataOwned::U16(values) => IntegerStorage::U16(values),
21        HostIntegerDataOwned::U32(values) => IntegerStorage::U32(values),
22        HostIntegerDataOwned::U64(values) => IntegerStorage::U64(values),
23    }
24}
25
26/// Resolve the provider that actually owns `handle`.
27///
28/// `provider_for_handle` retains a legacy active-provider fallback for older
29/// callers. Handle operations must additionally prove that the provider's
30/// device namespace matches the durable handle identity before touching it.
31pub fn exact_provider_for_handle(handle: &GpuTensorHandle) -> Option<&'static dyn AccelProvider> {
32    runmat_accelerate_api::provider_for_handle(handle)
33        .filter(|provider| provider.device_id() == handle.device_id)
34}
35
36/// Select one owning provider for a set of resident inputs, with explicit
37/// gpuArray provenance taking precedence over automatic residency.
38pub fn select_resident_output_source(
39    handles: impl IntoIterator<Item = GpuTensorHandle>,
40    builtin: &str,
41) -> crate::BuiltinResult<Option<GpuTensorHandle>> {
42    let mut selected: Option<(GpuTensorHandle, &'static dyn AccelProvider)> = None;
43    for handle in handles {
44        let owner = exact_provider_for_handle(&handle).ok_or_else(|| {
45            build_runtime_error(format!(
46                "{builtin}: no acceleration provider owns a resident input"
47            ))
48            .with_identifier("RunMat:gpu:ProviderOwnershipMismatch")
49            .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
50            .build()
51        })?;
52        if let Some((current, current_owner)) = &selected {
53            if current.device_id != handle.device_id || !std::ptr::eq(*current_owner, owner) {
54                return Err(build_runtime_error(format!(
55                    "{builtin}: resident inputs must share one owning provider"
56                ))
57                .with_identifier("RunMat:gpu:MixedProviders")
58                .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
59                .build());
60            }
61            if !runmat_accelerate_api::handle_is_explicit(current)
62                && runmat_accelerate_api::handle_is_explicit(&handle)
63            {
64                selected = Some((handle, owner));
65            }
66        } else {
67            selected = Some((handle, owner));
68        }
69    }
70    Ok(selected.map(|(handle, _)| handle))
71}
72
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub struct GpuHandleMetadataSnapshot {
75    descriptor: runmat_accelerate_api::GpuTensorDescriptor,
76    logical: bool,
77    transpose: Option<runmat_accelerate_api::TransposeInfo>,
78    class_name: Option<String>,
79    provenance: Option<runmat_accelerate_api::GpuHandleProvenance>,
80}
81
82pub fn snapshot_handle_metadata(handle: &GpuTensorHandle) -> GpuHandleMetadataSnapshot {
83    GpuHandleMetadataSnapshot {
84        descriptor: handle.descriptor,
85        logical: runmat_accelerate_api::handle_is_logical(handle),
86        transpose: runmat_accelerate_api::handle_transpose_info(handle),
87        class_name: runmat_accelerate_api::handle_class_name(handle),
88        provenance: runmat_accelerate_api::handle_provenance(handle),
89    }
90}
91
92pub fn restore_handle_metadata(handle: &GpuTensorHandle, snapshot: &GpuHandleMetadataSnapshot) {
93    debug_assert_eq!(handle.descriptor, snapshot.descriptor);
94    runmat_accelerate_api::set_handle_logical(handle, snapshot.logical);
95    match snapshot.transpose {
96        Some(info) => {
97            runmat_accelerate_api::record_handle_transpose(handle, info.base_rows, info.base_cols)
98        }
99        None => runmat_accelerate_api::clear_handle_transpose(handle),
100    }
101    match snapshot.class_name.as_deref() {
102        Some(class_name) => runmat_accelerate_api::set_handle_class_name(handle, class_name),
103        None => runmat_accelerate_api::clear_handle_class_name(handle),
104    }
105    runmat_accelerate_api::mark_residency(handle);
106}
107
108struct HandleMetadataRestoreGuard<'a> {
109    handle: &'a GpuTensorHandle,
110    snapshot: GpuHandleMetadataSnapshot,
111}
112
113impl<'a> HandleMetadataRestoreGuard<'a> {
114    fn new(handle: &'a GpuTensorHandle) -> Self {
115        Self {
116            handle,
117            snapshot: snapshot_handle_metadata(handle),
118        }
119    }
120}
121
122impl Drop for HandleMetadataRestoreGuard<'_> {
123    fn drop(&mut self) {
124        restore_handle_metadata(self.handle, &self.snapshot);
125    }
126}
127
128pub fn expected_gpu_class_name(
129    precision: Option<ProviderPrecision>,
130    integer: Option<IntegerElementType>,
131    logical: bool,
132) -> Option<&'static str> {
133    if logical {
134        return Some("logical");
135    }
136    if let Some(integer) = integer {
137        return Some(match integer {
138            IntegerElementType::I8 => "int8",
139            IntegerElementType::I16 => "int16",
140            IntegerElementType::I32 => "int32",
141            IntegerElementType::I64 => "int64",
142            IntegerElementType::U8 => "uint8",
143            IntegerElementType::U16 => "uint16",
144            IntegerElementType::U32 => "uint32",
145            IntegerElementType::U64 => "uint64",
146        });
147    }
148    precision.map(|precision| match precision {
149        ProviderPrecision::F32 => "single",
150        ProviderPrecision::F64 => "double",
151    })
152}
153
154pub fn gpu_class_metadata_matches(
155    handle: &GpuTensorHandle,
156    precision: Option<ProviderPrecision>,
157    integer: Option<IntegerElementType>,
158    logical: bool,
159) -> bool {
160    let expected = expected_gpu_class_name(precision, integer, logical);
161    runmat_accelerate_api::handle_class_name(handle)
162        .as_deref()
163        .is_none_or(|actual| expected == Some(actual))
164}
165
166fn integer_element_type(storage: &IntegerStorage) -> IntegerElementType {
167    match storage {
168        IntegerStorage::I8(_) => IntegerElementType::I8,
169        IntegerStorage::I16(_) => IntegerElementType::I16,
170        IntegerStorage::I32(_) => IntegerElementType::I32,
171        IntegerStorage::I64(_) => IntegerElementType::I64,
172        IntegerStorage::U8(_) => IntegerElementType::U8,
173        IntegerStorage::U16(_) => IntegerElementType::U16,
174        IntegerStorage::U32(_) => IntegerElementType::U32,
175        IntegerStorage::U64(_) => IntegerElementType::U64,
176    }
177}
178
179pub(crate) fn expected_handle_numeric_element_type(
180    handle: &GpuTensorHandle,
181) -> Result<NumericElementType, String> {
182    let precision = runmat_accelerate_api::handle_precision(handle);
183    let integer = runmat_accelerate_api::handle_integer_type(handle);
184    let logical = runmat_accelerate_api::handle_is_logical(handle);
185    let expected_class = expected_gpu_class_name(precision, integer, logical);
186    if runmat_accelerate_api::handle_class_name(handle)
187        .as_deref()
188        .is_some_and(|actual| expected_class != Some(actual))
189    {
190        return Err("class metadata contradicts the physical payload metadata".into());
191    }
192    if logical && integer.is_some() {
193        return Err("logical metadata cannot annotate native integer storage".into());
194    }
195    match (precision, integer) {
196        (Some(ProviderPrecision::F64), None) => Ok(NumericElementType::F64),
197        (Some(ProviderPrecision::F32), None) => Ok(NumericElementType::F32),
198        (None, Some(integer)) => Ok(integer.into()),
199        (Some(_), Some(_)) => {
200            Err("floating precision and integer class metadata are both present".into())
201        }
202        (None, None) => Err("physical numeric element metadata is missing".into()),
203    }
204}
205
206pub(crate) fn numeric_descriptor_matches(
207    handle: &GpuTensorHandle,
208    element_type: NumericElementType,
209    storage: GpuTensorStorage,
210) -> bool {
211    handle.descriptor.element_type == Some(element_type)
212        && handle.descriptor.storage == Some(storage)
213}
214
215pub(crate) fn numeric_descriptor_matches_source(
216    source: &GpuTensorHandle,
217    output: &GpuTensorHandle,
218) -> bool {
219    expected_handle_numeric_element_type(source).is_ok_and(|element_type| {
220        numeric_descriptor_matches(
221            output,
222            element_type,
223            runmat_accelerate_api::handle_storage(source),
224        )
225    })
226}
227
228fn deinterleave<T: Copy>(values: &[T]) -> (Vec<T>, Vec<T>) {
229    let mut real = Vec::with_capacity(values.len() / 2);
230    let mut imag = Vec::with_capacity(values.len() / 2);
231    for pair in values.chunks_exact(2) {
232        real.push(pair[0]);
233        imag.push(pair[1]);
234    }
235    (real, imag)
236}
237
238pub(crate) fn value_from_numeric_download(
239    host: HostNumericTensorOwned,
240    logical: bool,
241) -> Result<Value, String> {
242    host.validate().map_err(|error| error.to_string())?;
243    if logical {
244        if host.storage != GpuTensorStorage::Real {
245            return Err("logical payload must use real storage".into());
246        }
247        let bits = match host.data {
248            HostNumericDataOwned::F64(values) => values
249                .into_iter()
250                .map(|value| u8::from(value != 0.0))
251                .collect(),
252            HostNumericDataOwned::F32(values) => values
253                .into_iter()
254                .map(|value| u8::from(value != 0.0))
255                .collect(),
256            HostNumericDataOwned::I8(_)
257            | HostNumericDataOwned::I16(_)
258            | HostNumericDataOwned::I32(_)
259            | HostNumericDataOwned::I64(_)
260            | HostNumericDataOwned::U8(_)
261            | HostNumericDataOwned::U16(_)
262            | HostNumericDataOwned::U32(_)
263            | HostNumericDataOwned::U64(_) => {
264                return Err("logical payload cannot use native integer storage".into())
265            }
266        };
267        return LogicalArray::new(bits, host.shape).map(Value::LogicalArray);
268    }
269
270    if host.storage == GpuTensorStorage::Real {
271        let storage = match host.data {
272            HostNumericDataOwned::F64(values) => NumericStorage::F64(values),
273            HostNumericDataOwned::F32(values) => NumericStorage::F32(values),
274            HostNumericDataOwned::I8(values) => NumericStorage::I8(values),
275            HostNumericDataOwned::I16(values) => NumericStorage::I16(values),
276            HostNumericDataOwned::I32(values) => NumericStorage::I32(values),
277            HostNumericDataOwned::I64(values) => NumericStorage::I64(values),
278            HostNumericDataOwned::U8(values) => NumericStorage::U8(values),
279            HostNumericDataOwned::U16(values) => NumericStorage::U16(values),
280            HostNumericDataOwned::U32(values) => NumericStorage::U32(values),
281            HostNumericDataOwned::U64(values) => NumericStorage::U64(values),
282        };
283        return Tensor::from_numeric_storage(storage, host.shape).map(Value::Tensor);
284    }
285
286    macro_rules! integer_complex {
287        ($values:expr, $variant:ident) => {{
288            let (real, imag) = deinterleave(&$values);
289            ComplexStorage::Integer(IntegerComplexStorage::new(
290                IntegerStorage::$variant(real),
291                IntegerStorage::$variant(imag),
292            )?)
293        }};
294    }
295    let storage = match host.data {
296        HostNumericDataOwned::F64(values) => {
297            let (real, imag) = deinterleave(&values);
298            ComplexStorage::F64(real.into_iter().zip(imag).collect())
299        }
300        HostNumericDataOwned::F32(values) => {
301            let (real, imag) = deinterleave(&values);
302            ComplexStorage::F32(real.into_iter().zip(imag).collect())
303        }
304        HostNumericDataOwned::I8(values) => integer_complex!(values, I8),
305        HostNumericDataOwned::I16(values) => integer_complex!(values, I16),
306        HostNumericDataOwned::I32(values) => integer_complex!(values, I32),
307        HostNumericDataOwned::I64(values) => integer_complex!(values, I64),
308        HostNumericDataOwned::U8(values) => integer_complex!(values, U8),
309        HostNumericDataOwned::U16(values) => integer_complex!(values, U16),
310        HostNumericDataOwned::U32(values) => integer_complex!(values, U32),
311        HostNumericDataOwned::U64(values) => integer_complex!(values, U64),
312    };
313    ComplexTensor::from_complex_storage(storage, host.shape).map(Value::ComplexTensor)
314}
315
316pub fn same_gpu_handle(left: &GpuTensorHandle, right: &GpuTensorHandle) -> bool {
317    left.device_id == right.device_id && left.buffer_id == right.buffer_id
318}
319
320pub fn free_unprotected_exact_owner(handle: &GpuTensorHandle, protected: &[&GpuTensorHandle]) {
321    if protected
322        .iter()
323        .any(|protected| same_gpu_handle(handle, protected))
324    {
325        return;
326    }
327    if let Some(owner) = exact_provider_for_handle(handle) {
328        if owner.free(handle).is_ok() {
329            runmat_accelerate_api::clear_handle_metadata(handle);
330        }
331    }
332}
333
334/// Download a GPU tensor handle to host memory, returning a dense `Tensor`.
335///
336/// This helper routes through the dispatcher so residency hooks and provider
337/// semantics stay consistent with the rest of the runtime.
338pub async fn gather_tensor_async(
339    handle: &runmat_accelerate_api::GpuTensorHandle,
340) -> crate::BuiltinResult<Tensor> {
341    // Ensure the correct provider is active for WGPU-backed handles when tests run in parallel.
342    // This mirrors the guard used in test_support::gather.
343    #[cfg(all(test, feature = "wgpu"))]
344    {
345        let active_owner = runmat_accelerate_api::provider()
346            .is_some_and(|provider| provider.device_id() == handle.device_id);
347        if handle.device_id != 0 && !active_owner {
348            let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
349                runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
350            );
351        }
352    }
353    let value = Value::GpuTensor(handle.clone());
354    let gathered = crate::dispatcher::gather_if_needed_async(&value).await?;
355    match gathered {
356        Value::Tensor(t) => Ok(t),
357        Value::Num(n) => Tensor::new(vec![n], vec![1, 1])
358            .map_err(|e| build_runtime_error(format!("gather: {e}")).build()),
359        Value::LogicalArray(la) => {
360            let data: Vec<f64> = la
361                .data
362                .iter()
363                .map(|&b| if b != 0 { 1.0 } else { 0.0 })
364                .collect();
365            Tensor::new(data, la.shape.clone())
366                .map_err(|e| build_runtime_error(format!("gather: {e}")).build())
367        }
368        other => {
369            Err(build_runtime_error(format!("gather: unexpected value kind {other:?}")).build())
370        }
371    }
372}
373
374/// Gather an arbitrary value, returning a host-side `Value`.
375pub async fn gather_value_async(value: &Value) -> crate::BuiltinResult<Value> {
376    crate::dispatcher::gather_if_needed_async(value).await
377}
378
379/// Download a handle through its owner without changing the handle's residency or metadata.
380pub async fn download_value_preserving_residency_async(
381    provider: &dyn AccelProvider,
382    handle: &GpuTensorHandle,
383) -> crate::BuiltinResult<Value> {
384    let source_guard = HandleMetadataRestoreGuard::new(handle);
385    if provider.device_id() != handle.device_id {
386        return Err(
387            build_runtime_error("gpu download: provider does not own the input handle")
388                .with_identifier("RunMat:gpu:ProviderOwnershipMismatch")
389                .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
390                .build(),
391        );
392    }
393    let expected_element = expected_handle_numeric_element_type(handle).map_err(|error| {
394        build_runtime_error(format!("gpu download: {error}"))
395            .with_identifier("RunMat:gpu:ProviderPayloadMismatch")
396            .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
397            .build()
398    })?;
399    let host = provider.download_numeric(handle).await.map_err(|error| {
400        build_runtime_error(format!("gpu download: {error}"))
401            .with_identifier("RunMat:gpu:DownloadFailed")
402            .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
403            .build()
404    })?;
405    if host.shape != handle.shape
406        || Some(host.storage) != source_guard.snapshot.descriptor.storage
407        || host.data.element_type() != expected_element
408    {
409        return Err(provider_payload_mismatch(
410            handle,
411            &host.shape,
412            format!(
413                "{:?} {:?}, expected {:?} {:?}",
414                host.data.element_type(),
415                host.storage,
416                expected_element,
417                source_guard.snapshot.descriptor.storage
418            ),
419        ));
420    }
421    value_from_numeric_download(host, source_guard.snapshot.logical).map_err(|error| {
422        build_runtime_error(format!("gpu download: {error}"))
423            .with_identifier("RunMat:gpu:ProviderPayloadMismatch")
424            .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
425            .build()
426    })
427}
428
429/// Exact native scalar values decoded from a provider-owned real numeric payload.
430pub struct HostNativeTensorOwned {
431    pub data: Vec<NumericScalar>,
432    pub shape: Vec<usize>,
433}
434
435/// Download a real numeric handle without changing its native class or source residency.
436pub async fn download_native_values_async(
437    provider: &dyn AccelProvider,
438    handle: &GpuTensorHandle,
439) -> crate::BuiltinResult<HostNativeTensorOwned> {
440    let value = download_value_preserving_residency_async(provider, handle).await?;
441    match value {
442        Value::Tensor(tensor) => {
443            let data = (0..tensor.len())
444                .map(|index| {
445                    tensor.numeric_value_at(index).ok_or_else(|| {
446                        build_runtime_error(
447                            "native download: numeric payload storage is inconsistent",
448                        )
449                        .with_identifier("RunMat:gpu:ProviderPayloadMismatch")
450                        .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
451                        .build()
452                    })
453                })
454                .collect::<crate::BuiltinResult<Vec<_>>>()?;
455            Ok(HostNativeTensorOwned {
456                data,
457                shape: tensor.shape,
458            })
459        }
460        Value::LogicalArray(logical) => Ok(HostNativeTensorOwned {
461            data: logical.data.into_iter().map(NumericScalar::U8).collect(),
462            shape: logical.shape,
463        }),
464        Value::Num(value) => Ok(HostNativeTensorOwned {
465            data: vec![NumericScalar::F64(value)],
466            shape: vec![1, 1],
467        }),
468        Value::Bool(value) => Ok(HostNativeTensorOwned {
469            data: vec![NumericScalar::U8(u8::from(value))],
470            shape: vec![1, 1],
471        }),
472        Value::ComplexTensor(_) | Value::Complex(_, _) => Err(build_runtime_error(
473            "native download: provider output must use real storage",
474        )
475        .with_identifier("RunMat:gpu:ProviderPayloadMismatch")
476        .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
477        .build()),
478        other => Err(build_runtime_error(format!(
479            "native download: unexpected provider value {other:?}"
480        ))
481        .with_identifier("RunMat:gpu:ProviderPayloadMismatch")
482        .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
483        .build()),
484    }
485}
486
487/// Exact host truth values decoded from a provider-owned real numeric payload.
488pub struct HostTruthTensorOwned {
489    pub data: Vec<u8>,
490    pub shape: Vec<usize>,
491}
492
493/// Download a real numeric handle and classify each native value as zero or
494/// nonzero without entering a floating representation.
495pub async fn download_truth_values_async(
496    provider: &dyn AccelProvider,
497    handle: &GpuTensorHandle,
498) -> crate::BuiltinResult<HostTruthTensorOwned> {
499    let native = download_native_values_async(provider, handle).await?;
500    Ok(HostTruthTensorOwned {
501        data: native
502            .data
503            .into_iter()
504            .map(|value| u8::from(!value.is_zero()))
505            .collect(),
506        shape: native.shape,
507    })
508}
509
510/// Project a provider-owned floating payload into binary64 host lanes for an
511/// operation whose declared computation boundary is floating point.
512///
513/// This is intentionally not a generic gather API. It accepts physical `f64`
514/// or `f32` storage, validates the complete handle/payload contract, preserves
515/// source metadata and residency, and rejects every native integer class.
516pub async fn download_floating_projection_async(
517    provider: &dyn AccelProvider,
518    handle: &GpuTensorHandle,
519) -> crate::BuiltinResult<HostTensorOwned> {
520    let source_guard = HandleMetadataRestoreGuard::new(handle);
521    if provider.device_id() != handle.device_id {
522        return Err(build_runtime_error(
523            "floating projection: provider does not own the input handle",
524        )
525        .with_identifier("RunMat:gpu:ProviderOwnershipMismatch")
526        .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
527        .build());
528    }
529    let expected_element = expected_handle_numeric_element_type(handle).map_err(|error| {
530        build_runtime_error(format!("floating projection: {error}"))
531            .with_identifier("RunMat:gpu:ProviderPayloadMismatch")
532            .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
533            .build()
534    })?;
535    let host = provider.download_numeric(handle).await.map_err(|error| {
536        build_runtime_error(format!("floating projection: {error}"))
537            .with_identifier("RunMat:gpu:DownloadFailed")
538            .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
539            .build()
540    })?;
541    host.validate().map_err(|error| {
542        build_runtime_error(format!("floating projection: {error}"))
543            .with_identifier("RunMat:gpu:ProviderPayloadMismatch")
544            .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
545            .build()
546    })?;
547    if host.shape != handle.shape
548        || Some(host.storage) != source_guard.snapshot.descriptor.storage
549        || host.data.element_type() != expected_element
550    {
551        return Err(provider_payload_mismatch(
552            handle,
553            &host.shape,
554            format!(
555                "{:?} {:?}, expected {:?} {:?}",
556                host.data.element_type(),
557                host.storage,
558                expected_element,
559                source_guard.snapshot.descriptor.storage
560            ),
561        ));
562    }
563    let data = match host.data {
564        HostNumericDataOwned::F64(values) => values,
565        HostNumericDataOwned::F32(values) => values.into_iter().map(f64::from).collect(),
566        HostNumericDataOwned::I8(_)
567        | HostNumericDataOwned::I16(_)
568        | HostNumericDataOwned::I32(_)
569        | HostNumericDataOwned::I64(_)
570        | HostNumericDataOwned::U8(_)
571        | HostNumericDataOwned::U16(_)
572        | HostNumericDataOwned::U32(_)
573        | HostNumericDataOwned::U64(_) => {
574            return Err(build_runtime_error(
575                "floating projection cannot consume native integer storage",
576            )
577            .with_identifier("RunMat:gpu:IntegerFloatingProjection")
578            .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
579            .build())
580        }
581    };
582    Ok(HostTensorOwned {
583        data,
584        shape: host.shape,
585        storage: host.storage,
586    })
587}
588
589fn provider_payload_mismatch(
590    handle: &GpuTensorHandle,
591    actual_shape: &[usize],
592    detail: String,
593) -> crate::RuntimeError {
594    build_runtime_error(format!(
595        "gpu download: provider payload mismatch ({detail}; shape {actual_shape:?}, expected {:?})",
596        handle.shape
597    ))
598    .with_identifier("RunMat:gpu:ProviderPayloadMismatch")
599    .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
600    .build()
601}
602
603fn interleave<T: Copy>(real: &[T], imag: &[T]) -> Vec<T> {
604    let mut values = Vec::with_capacity(real.len().saturating_mul(2));
605    for (&real, &imag) in real.iter().zip(imag) {
606        values.push(real);
607        values.push(imag);
608    }
609    values
610}
611
612fn interleaved_complex_data(tensor: &ComplexTensor) -> Result<HostNumericDataOwned, String> {
613    macro_rules! integer_interleaved {
614        ($real:expr, $imag:expr, $variant:ident) => {
615            HostNumericDataOwned::$variant(interleave($real, $imag))
616        };
617    }
618    match tensor.complex_storage() {
619        ComplexStorage::F64(values) => Ok(HostNumericDataOwned::F64(
620            values
621                .iter()
622                .flat_map(|&(real, imag)| [real, imag])
623                .collect(),
624        )),
625        ComplexStorage::F32(values) => Ok(HostNumericDataOwned::F32(
626            values
627                .iter()
628                .flat_map(|&(real, imag)| [real, imag])
629                .collect(),
630        )),
631        ComplexStorage::Integer(storage) => match (&storage.real, &storage.imag) {
632            (IntegerStorage::I8(real), IntegerStorage::I8(imag)) => {
633                Ok(integer_interleaved!(real, imag, I8))
634            }
635            (IntegerStorage::I16(real), IntegerStorage::I16(imag)) => {
636                Ok(integer_interleaved!(real, imag, I16))
637            }
638            (IntegerStorage::I32(real), IntegerStorage::I32(imag)) => {
639                Ok(integer_interleaved!(real, imag, I32))
640            }
641            (IntegerStorage::I64(real), IntegerStorage::I64(imag)) => {
642                Ok(integer_interleaved!(real, imag, I64))
643            }
644            (IntegerStorage::U8(real), IntegerStorage::U8(imag)) => {
645                Ok(integer_interleaved!(real, imag, U8))
646            }
647            (IntegerStorage::U16(real), IntegerStorage::U16(imag)) => {
648                Ok(integer_interleaved!(real, imag, U16))
649            }
650            (IntegerStorage::U32(real), IntegerStorage::U32(imag)) => {
651                Ok(integer_interleaved!(real, imag, U32))
652            }
653            (IntegerStorage::U64(real), IntegerStorage::U64(imag)) => {
654                Ok(integer_interleaved!(real, imag, U64))
655            }
656            _ => Err("complex integer components must have matching classes".into()),
657        },
658    }
659}
660
661/// Upload a host complex tensor through the shared native numeric transfer contract.
662pub fn upload_complex_tensor(
663    provider: &dyn AccelProvider,
664    tensor: &ComplexTensor,
665) -> crate::BuiltinResult<GpuTensorHandle> {
666    let data = interleaved_complex_data(tensor)
667        .map_err(|error| build_runtime_error(format!("gpu upload: {error}")).build())?;
668    let transfer = HostNumericTensorView {
669        data: data.as_view(),
670        shape: &tensor.shape,
671        storage: GpuTensorStorage::ComplexInterleaved,
672    };
673    let handle = provider
674        .upload_numeric(&transfer)
675        .map_err(|error| build_runtime_error(format!("gpu upload: {error}")).build())?;
676    validate_numeric_upload(provider, handle, transfer)
677        .map_err(|error| build_runtime_error(format!("gpu upload: {error}")).build())
678}
679
680fn tensor_numeric_view(tensor: &Tensor) -> HostNumericDataView<'_> {
681    if let Some(values) = tensor.as_f64_slice() {
682        return HostNumericDataView::F64(values);
683    }
684    if let Some(values) = tensor.as_f32_slice() {
685        return HostNumericDataView::F32(values);
686    }
687    match tensor
688        .integer_storage()
689        .expect("non-floating tensor has integer storage")
690    {
691        IntegerStorage::I8(values) => HostNumericDataView::I8(values),
692        IntegerStorage::I16(values) => HostNumericDataView::I16(values),
693        IntegerStorage::I32(values) => HostNumericDataView::I32(values),
694        IntegerStorage::I64(values) => HostNumericDataView::I64(values),
695        IntegerStorage::U8(values) => HostNumericDataView::U8(values),
696        IntegerStorage::U16(values) => HostNumericDataView::U16(values),
697        IntegerStorage::U32(values) => HostNumericDataView::U32(values),
698        IntegerStorage::U64(values) => HostNumericDataView::U64(values),
699    }
700}
701
702/// Upload a host tensor through the shared native numeric transfer contract.
703pub fn upload_tensor(
704    provider: &dyn AccelProvider,
705    tensor: &Tensor,
706) -> Result<GpuTensorHandle, String> {
707    let transfer = HostNumericTensorView {
708        data: tensor_numeric_view(tensor),
709        shape: &tensor.shape,
710        storage: GpuTensorStorage::Real,
711    };
712    let handle = provider
713        .upload_numeric(&transfer)
714        .map_err(|error| error.to_string())?;
715    validate_numeric_upload(provider, handle, transfer)
716}
717
718fn validate_numeric_upload(
719    provider: &dyn AccelProvider,
720    handle: GpuTensorHandle,
721    transfer: HostNumericTensorView<'_>,
722) -> Result<GpuTensorHandle, String> {
723    let valid = handle.device_id == provider.device_id()
724        && handle.shape == transfer.shape
725        && handle.descriptor.element_type == Some(transfer.element_type())
726        && handle.descriptor.storage == Some(transfer.storage);
727    if valid {
728        return Ok(handle);
729    }
730    let detail = format!(
731        "provider returned descriptor {:?}, shape {:?}, and device {} for {:?} {:?} upload with shape {:?} on device {}",
732        handle.descriptor,
733        handle.shape,
734        handle.device_id,
735        transfer.element_type(),
736        transfer.storage,
737        transfer.shape,
738        provider.device_id()
739    );
740    let _ = provider.free(&handle);
741    runmat_accelerate_api::clear_handle_metadata(&handle);
742    Err(detail)
743}
744
745/// Restore a class-preserving host value to the provider that owns `source`.
746///
747/// If the owner cannot physically represent the floating class, the host value
748/// is returned instead of relabelling it. Integer and logical storage retain
749/// their exact metadata independently of the provider's floating precision.
750pub fn restore_class_preserving_value(
751    source: &GpuTensorHandle,
752    value: Value,
753    builtin: &str,
754) -> crate::BuiltinResult<Value> {
755    let value = match value {
756        Value::Num(number) => {
757            let tensor = Tensor::new(vec![number], vec![1, 1]).map_err(|error| {
758                build_runtime_error(format!("{builtin}: invalid scalar result: {error}"))
759                    .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
760                    .build()
761            })?;
762            return match restore_class_preserving_value(source, Value::Tensor(tensor), builtin)? {
763                Value::Tensor(_) => Ok(Value::Num(number)),
764                restored => Ok(restored),
765            };
766        }
767        Value::Complex(real, imag) => {
768            let tensor = ComplexTensor::new(vec![(real, imag)], vec![1, 1]).map_err(|error| {
769                build_runtime_error(format!("{builtin}: invalid complex scalar result: {error}"))
770                    .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
771                    .build()
772            })?;
773            return match restore_class_preserving_value(
774                source,
775                Value::ComplexTensor(tensor),
776                builtin,
777            )? {
778                Value::ComplexTensor(_) => Ok(Value::Complex(real, imag)),
779                restored => Ok(restored),
780            };
781        }
782        Value::Bool(bit) => {
783            let logical = LogicalArray::new(vec![u8::from(bit)], vec![1, 1]).map_err(|error| {
784                build_runtime_error(format!("{builtin}: invalid logical scalar result: {error}"))
785                    .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
786                    .build()
787            })?;
788            return match restore_class_preserving_value(
789                source,
790                Value::LogicalArray(logical),
791                builtin,
792            )? {
793                Value::LogicalArray(_) => Ok(Value::Bool(bit)),
794                restored => Ok(restored),
795            };
796        }
797        value => value,
798    };
799    let provider = exact_provider_for_handle(source).ok_or_else(|| {
800        build_runtime_error(format!(
801            "{builtin}: no acceleration provider owns the input handle"
802        ))
803        .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
804        .build()
805    })?;
806    let source_guard = HandleMetadataRestoreGuard::new(source);
807
808    let (
809        mut output,
810        expected_shape,
811        expected_storage,
812        expected_precision,
813        expected_integer,
814        logical,
815    ) = match &value {
816        Value::Tensor(tensor) => {
817            let expected_integer = tensor.integer_storage().map(integer_element_type);
818            let expected_precision = if expected_integer.is_some() {
819                None
820            } else {
821                Some(match tensor.numeric_dtype() {
822                    NumericDType::F32 => ProviderPrecision::F32,
823                    _ => ProviderPrecision::F64,
824                })
825            };
826            let output = match upload_tensor(provider, tensor) {
827                Ok(output) => output,
828                Err(_)
829                    if expected_integer.is_none()
830                        && !runmat_accelerate_api::handle_is_explicit(source) =>
831                {
832                    return Ok(value)
833                }
834                Err(error) => {
835                    return Err(build_runtime_error(format!(
836                        "{builtin}: failed to restore GPU result: {error}"
837                    ))
838                    .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
839                    .build())
840                }
841            };
842            (
843                output,
844                tensor.shape.clone(),
845                GpuTensorStorage::Real,
846                expected_precision,
847                expected_integer,
848                false,
849            )
850        }
851        Value::LogicalArray(array) => {
852            let storage = match provider.precision() {
853                ProviderPrecision::F32 => {
854                    NumericStorage::F32(array.data.iter().map(|bit| f32::from(*bit != 0)).collect())
855                }
856                ProviderPrecision::F64 => {
857                    NumericStorage::F64(array.data.iter().map(|bit| f64::from(*bit != 0)).collect())
858                }
859            };
860            let tensor =
861                Tensor::from_numeric_storage(storage, array.shape.clone()).map_err(|error| {
862                    build_runtime_error(format!("{builtin}: invalid logical result: {error}"))
863                        .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
864                        .build()
865                })?;
866            let output = upload_tensor(provider, &tensor).map_err(|error| {
867                build_runtime_error(format!("{builtin}: failed to restore GPU result: {error}"))
868                    .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
869                    .build()
870            })?;
871            runmat_accelerate_api::set_handle_logical(&output, true);
872            (
873                output,
874                array.shape.clone(),
875                GpuTensorStorage::Real,
876                Some(provider.precision()),
877                None,
878                true,
879            )
880        }
881        Value::ComplexTensor(tensor) => {
882            let expected_integer = tensor
883                .integer_storage()
884                .map(|storage| integer_element_type(&storage.real));
885            let expected_precision =
886                expected_integer
887                    .is_none()
888                    .then(|| match tensor.numeric_dtype() {
889                        NumericDType::F32 => ProviderPrecision::F32,
890                        _ => ProviderPrecision::F64,
891                    });
892            let output = match upload_complex_tensor(provider, tensor) {
893                Ok(output) => output,
894                Err(_)
895                    if expected_integer.is_none()
896                        && !runmat_accelerate_api::handle_is_explicit(source) =>
897                {
898                    return Ok(value)
899                }
900                Err(error) => {
901                    return Err(build_runtime_error(format!(
902                        "{builtin}: failed to restore GPU result: {error}"
903                    ))
904                    .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
905                    .build())
906                }
907            };
908            (
909                output,
910                tensor.shape.clone(),
911                GpuTensorStorage::ComplexInterleaved,
912                expected_precision,
913                expected_integer,
914                false,
915            )
916        }
917        _ => return Ok(value),
918    };
919
920    let aliases_source = same_gpu_handle(&output, source);
921    if aliases_source {
922        return Err(build_runtime_error(format!(
923            "{builtin}: provider aliased the protected input while restoring the result"
924        ))
925        .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
926        .build());
927    }
928
929    let valid = output.shape == expected_shape
930        && output.device_id == provider.device_id()
931        && exact_provider_for_handle(&output).is_some_and(|owner| std::ptr::eq(owner, provider))
932        && runmat_accelerate_api::handle_storage(&output) == expected_storage
933        && expected_precision.is_none_or(|precision| {
934            runmat_accelerate_api::handle_precision(&output) == Some(precision)
935        })
936        && runmat_accelerate_api::handle_integer_type(&output) == expected_integer
937        && runmat_accelerate_api::handle_is_logical(&output) == logical
938        && gpu_class_metadata_matches(&output, expected_precision, expected_integer, logical);
939    if !valid {
940        free_unprotected_exact_owner(&output, &[source]);
941        return Err(build_runtime_error(format!(
942            "{builtin}: provider returned an invalid restored result"
943        ))
944        .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
945        .build());
946    }
947    let provenance = source_guard
948        .snapshot
949        .provenance
950        .unwrap_or(runmat_accelerate_api::GpuHandleProvenance::Automatic);
951    output.descriptor.provenance = Some(provenance);
952    runmat_accelerate_api::mark_residency(&output);
953    Ok(Value::GpuTensor(output))
954}
955
956/// Upload a finite integral scalar in the native integer class of `prototype`.
957/// Returns `None` when preserving MATLAB's typed-integer scalar semantics would
958/// require the host extended-precision path instead.
959pub fn upload_exact_integer_scalar_like(
960    provider: &dyn AccelProvider,
961    prototype: &GpuTensorHandle,
962    scalar: f64,
963) -> Option<GpuTensorHandle> {
964    if !scalar.is_finite() || scalar.fract() != 0.0 {
965        return None;
966    }
967    let element_type = runmat_accelerate_api::handle_integer_type(prototype)?;
968    let shape = [1usize, 1usize];
969    macro_rules! upload {
970        ($value:expr, $variant:ident) => {{
971            let values = [$value];
972            provider
973                .upload_numeric(&HostNumericTensorView {
974                    data: HostNumericDataView::$variant(&values),
975                    shape: &shape,
976                    storage: GpuTensorStorage::Real,
977                })
978                .ok()
979        }};
980    }
981    match element_type {
982        IntegerElementType::I8 if scalar >= i8::MIN as f64 && scalar <= i8::MAX as f64 => {
983            upload!(scalar as i8, I8)
984        }
985        IntegerElementType::I16 if scalar >= i16::MIN as f64 && scalar <= i16::MAX as f64 => {
986            upload!(scalar as i16, I16)
987        }
988        IntegerElementType::I32 if scalar >= i32::MIN as f64 && scalar <= i32::MAX as f64 => {
989            upload!(scalar as i32, I32)
990        }
991        IntegerElementType::I64
992            if scalar >= i64::MIN as f64 && scalar < 9_223_372_036_854_775_808.0 =>
993        {
994            upload!(scalar as i64, I64)
995        }
996        IntegerElementType::U8 if scalar >= 0.0 && scalar <= u8::MAX as f64 => {
997            upload!(scalar as u8, U8)
998        }
999        IntegerElementType::U16 if scalar >= 0.0 && scalar <= u16::MAX as f64 => {
1000            upload!(scalar as u16, U16)
1001        }
1002        IntegerElementType::U32 if scalar >= 0.0 && scalar <= u32::MAX as f64 => {
1003            upload!(scalar as u32, U32)
1004        }
1005        IntegerElementType::U64 if (0.0..18_446_744_073_709_551_616.0).contains(&scalar) => {
1006            upload!(scalar as u64, U64)
1007        }
1008        _ => None,
1009    }
1010}
1011
1012/// Wrap a GPU tensor handle, marking it as resident for downstream fusion-aware
1013/// consumers and tests.
1014pub fn resident_gpu_value(mut handle: GpuTensorHandle) -> Value {
1015    let provenance = runmat_accelerate_api::handle_provenance(&handle)
1016        .unwrap_or(runmat_accelerate_api::GpuHandleProvenance::Automatic);
1017    handle.descriptor.provenance = Some(provenance);
1018    runmat_accelerate_api::mark_residency(&handle);
1019    Value::GpuTensor(handle)
1020}
1021
1022/// Wrap a GPU tensor handle as a logical gpuArray value, recording metadata so that
1023/// predicates like `islogical` can inspect the handle without downloading it.
1024pub fn logical_gpu_value(handle: GpuTensorHandle) -> Value {
1025    runmat_accelerate_api::set_handle_logical(&handle, true);
1026    resident_gpu_value(handle)
1027}
1028
1029/// Wrap a GPU tensor handle as a complex gpuArray value.
1030pub fn complex_gpu_value(mut handle: GpuTensorHandle) -> Value {
1031    runmat_accelerate_api::set_handle_logical(&handle, false);
1032    handle.descriptor.storage = Some(GpuTensorStorage::ComplexInterleaved);
1033    resident_gpu_value(handle)
1034}
1035
1036#[cfg(test)]
1037mod preserving_download_tests {
1038    use super::*;
1039    use crate::builtins::common::test_support;
1040    use futures::executor::block_on;
1041
1042    #[test]
1043    fn central_runtime_transfer_round_trips_every_native_real_and_complex_class() {
1044        test_support::with_test_provider(|provider| {
1045            let real_cases = vec![
1046                NumericStorage::F64(vec![-1.25, 2.5]),
1047                NumericStorage::F32(vec![-1.25, 2.5]),
1048                NumericStorage::I8(vec![i8::MIN, i8::MAX]),
1049                NumericStorage::I16(vec![i16::MIN, i16::MAX]),
1050                NumericStorage::I32(vec![i32::MIN, i32::MAX]),
1051                NumericStorage::I64(vec![i64::MIN, i64::MAX]),
1052                NumericStorage::U8(vec![0, u8::MAX]),
1053                NumericStorage::U16(vec![0, u16::MAX]),
1054                NumericStorage::U32(vec![0, u32::MAX]),
1055                NumericStorage::U64(vec![1_u64 << 63, u64::MAX]),
1056            ];
1057            for storage in real_cases {
1058                let expected = storage.clone();
1059                let tensor = Tensor::from_numeric_storage(storage, vec![1, 2]).unwrap();
1060                let handle = upload_tensor(provider, &tensor).expect("native real upload");
1061                let gathered = block_on(crate::dispatcher::gather_if_needed_async(
1062                    &Value::GpuTensor(handle.clone()),
1063                ))
1064                .expect("native real gather");
1065                let Value::Tensor(gathered) = gathered else {
1066                    panic!("real transfer reconstructed the wrong value kind")
1067                };
1068                assert_eq!(gathered.into_numeric_storage().unwrap(), expected);
1069                provider.free(&handle).unwrap();
1070                runmat_accelerate_api::clear_handle_metadata(&handle);
1071            }
1072
1073            macro_rules! integer_complex_case {
1074                ($variant:ident, $real:expr, $imag:expr) => {
1075                    ComplexStorage::Integer(
1076                        IntegerComplexStorage::new(
1077                            IntegerStorage::$variant($real),
1078                            IntegerStorage::$variant($imag),
1079                        )
1080                        .unwrap(),
1081                    )
1082                };
1083            }
1084            let complex_cases = vec![
1085                ComplexStorage::F64(vec![(-1.25, 3.5), (2.5, -4.75)]),
1086                ComplexStorage::F32(vec![(-1.25, 3.5), (2.5, -4.75)]),
1087                integer_complex_case!(I8, vec![i8::MIN, i8::MAX], vec![1, -1]),
1088                integer_complex_case!(I16, vec![i16::MIN, i16::MAX], vec![1, -1]),
1089                integer_complex_case!(I32, vec![i32::MIN, i32::MAX], vec![1, -1]),
1090                integer_complex_case!(I64, vec![i64::MIN, i64::MAX], vec![1, -1]),
1091                integer_complex_case!(U8, vec![0, u8::MAX], vec![1, 2]),
1092                integer_complex_case!(U16, vec![0, u16::MAX], vec![1, 2]),
1093                integer_complex_case!(U32, vec![0, u32::MAX], vec![1, 2]),
1094                integer_complex_case!(U64, vec![1_u64 << 63, u64::MAX], vec![1, 2]),
1095            ];
1096            for storage in complex_cases {
1097                let expected = storage.clone();
1098                let tensor = ComplexTensor::from_complex_storage(storage, vec![1, 2]).unwrap();
1099                let handle =
1100                    upload_complex_tensor(provider, &tensor).expect("native complex upload");
1101                let gathered = block_on(crate::dispatcher::gather_if_needed_async(
1102                    &Value::GpuTensor(handle.clone()),
1103                ))
1104                .expect("native complex gather");
1105                let Value::ComplexTensor(gathered) = gathered else {
1106                    panic!("complex transfer reconstructed the wrong value kind")
1107                };
1108                assert_eq!(gathered.into_complex_storage(), expected);
1109                provider.free(&handle).unwrap();
1110                runmat_accelerate_api::clear_handle_metadata(&handle);
1111            }
1112        });
1113    }
1114
1115    #[test]
1116    fn class_preserving_restore_uses_shared_single_and_complex_integer_storage() {
1117        test_support::with_test_provider(|provider| {
1118            let source =
1119                upload_tensor(provider, &Tensor::new(vec![0.0, 0.0], vec![1, 2]).unwrap()).unwrap();
1120            let source =
1121                source.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Automatic);
1122
1123            let single = Tensor::from_f32(vec![1.25, -2.5], vec![1, 2]).unwrap();
1124            let restored =
1125                restore_class_preserving_value(&source, Value::Tensor(single.clone()), "test")
1126                    .expect("native single restore");
1127            let Value::GpuTensor(single_handle) = restored else {
1128                panic!("shared provider should preserve native single residency")
1129            };
1130            assert_eq!(
1131                runmat_accelerate_api::handle_precision(&single_handle),
1132                Some(ProviderPrecision::F32)
1133            );
1134            let gathered = block_on(crate::dispatcher::gather_if_needed_async(
1135                &Value::GpuTensor(single_handle.clone()),
1136            ))
1137            .unwrap();
1138            assert_eq!(gathered, Value::Tensor(single));
1139
1140            let complex = ComplexTensor::new_integer(
1141                IntegerComplexStorage::new(
1142                    IntegerStorage::U64(vec![1_u64 << 63, u64::MAX]),
1143                    IntegerStorage::U64(vec![3, 4]),
1144                )
1145                .unwrap(),
1146                vec![1, 2],
1147            )
1148            .unwrap();
1149            let restored = restore_class_preserving_value(
1150                &source,
1151                Value::ComplexTensor(complex.clone()),
1152                "test",
1153            )
1154            .expect("complex integer restore");
1155            let Value::GpuTensor(complex_handle) = restored else {
1156                panic!("shared provider should preserve complex integer residency")
1157            };
1158            assert_eq!(
1159                runmat_accelerate_api::handle_integer_type(&complex_handle),
1160                Some(IntegerElementType::U64)
1161            );
1162            assert_eq!(
1163                runmat_accelerate_api::handle_storage(&complex_handle),
1164                GpuTensorStorage::ComplexInterleaved
1165            );
1166            let gathered = block_on(crate::dispatcher::gather_if_needed_async(
1167                &Value::GpuTensor(complex_handle.clone()),
1168            ))
1169            .unwrap();
1170            assert_eq!(gathered, Value::ComplexTensor(complex));
1171
1172            let logical = LogicalArray::new(vec![1, 0], vec![1, 2]).unwrap();
1173            let restored = restore_class_preserving_value(
1174                &source,
1175                Value::LogicalArray(logical.clone()),
1176                "test",
1177            )
1178            .expect("logical restore");
1179            let Value::GpuTensor(logical_handle) = restored else {
1180                panic!("logical restoration should retain residency")
1181            };
1182            assert!(runmat_accelerate_api::handle_is_logical(&logical_handle));
1183            assert_eq!(
1184                runmat_accelerate_api::handle_class_name(&logical_handle).as_deref(),
1185                Some("logical")
1186            );
1187            let gathered = block_on(crate::dispatcher::gather_if_needed_async(
1188                &Value::GpuTensor(logical_handle.clone()),
1189            ))
1190            .unwrap();
1191            assert_eq!(gathered, Value::LogicalArray(logical));
1192
1193            for handle in [&single_handle, &complex_handle, &logical_handle, &source] {
1194                provider.free(handle).unwrap();
1195                runmat_accelerate_api::clear_handle_metadata(handle);
1196            }
1197        });
1198    }
1199
1200    #[test]
1201    fn class_preserving_logical_restore_uses_owner_physical_precision() {
1202        test_support::with_f32_test_provider(|provider| {
1203            let source = upload_tensor(
1204                provider,
1205                &Tensor::from_f32(vec![0.0, 0.0], vec![1, 2]).unwrap(),
1206            )
1207            .unwrap();
1208            let source =
1209                source.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Automatic);
1210            let logical = LogicalArray::new(vec![1, 0], vec![1, 2]).unwrap();
1211            let restored = restore_class_preserving_value(
1212                &source,
1213                Value::LogicalArray(logical.clone()),
1214                "test",
1215            )
1216            .expect("logical restore");
1217            let Value::GpuTensor(handle) = restored else {
1218                panic!("logical restoration should retain residency")
1219            };
1220            assert_eq!(
1221                runmat_accelerate_api::handle_precision(&handle),
1222                Some(ProviderPrecision::F32)
1223            );
1224            assert!(runmat_accelerate_api::handle_is_logical(&handle));
1225            let gathered = block_on(crate::dispatcher::gather_if_needed_async(
1226                &Value::GpuTensor(handle.clone()),
1227            ))
1228            .unwrap();
1229            assert_eq!(gathered, Value::LogicalArray(logical));
1230            for handle in [&handle, &source] {
1231                provider.free(handle).unwrap();
1232                runmat_accelerate_api::clear_handle_metadata(handle);
1233            }
1234        });
1235    }
1236
1237    #[test]
1238    fn resident_output_source_prefers_explicit_intent_independent_of_order() {
1239        test_support::with_test_provider(|provider| {
1240            let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
1241            let automatic = upload_tensor(provider, &tensor).unwrap();
1242            let explicit = upload_tensor(provider, &tensor).unwrap();
1243            let automatic =
1244                automatic.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Automatic);
1245            let explicit =
1246                explicit.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
1247            for handles in [
1248                vec![automatic.clone(), explicit.clone()],
1249                vec![explicit.clone(), automatic.clone()],
1250            ] {
1251                let selected = select_resident_output_source(handles, "test")
1252                    .unwrap()
1253                    .expect("resident source");
1254                assert!(same_gpu_handle(&selected, &explicit));
1255                assert!(runmat_accelerate_api::handle_is_explicit(&selected));
1256            }
1257            runmat_accelerate_api::clear_handle_metadata(&automatic);
1258            runmat_accelerate_api::clear_handle_metadata(&explicit);
1259        });
1260    }
1261
1262    #[test]
1263    fn resident_output_source_rejects_a_stale_or_wrong_owner() {
1264        test_support::with_test_provider(|_| {
1265            let stale = GpuTensorHandle {
1266                shape: vec![1, 1],
1267                device_id: u32::MAX,
1268                buffer_id: u64::MAX - 426,
1269                descriptor: Default::default(),
1270            };
1271            let error = select_resident_output_source([stale], "test")
1272                .expect_err("unowned handle must reject");
1273            assert_eq!(
1274                error.identifier(),
1275                Some("RunMat:gpu:ProviderOwnershipMismatch")
1276            );
1277            assert_eq!(error.gpu_gather_retry(), crate::GpuGatherRetry::Never);
1278        });
1279    }
1280
1281    struct MalformedDownloadProvider;
1282
1283    struct MalformedUploadProvider;
1284
1285    impl runmat_accelerate_api::AccelProvider for MalformedUploadProvider {
1286        fn upload(
1287            &self,
1288            _host: &runmat_accelerate_api::HostTensorView,
1289        ) -> anyhow::Result<GpuTensorHandle> {
1290            anyhow::bail!("unused")
1291        }
1292
1293        fn upload_numeric(
1294            &self,
1295            host: &runmat_accelerate_api::HostNumericTensorView,
1296        ) -> anyhow::Result<GpuTensorHandle> {
1297            Ok(GpuTensorHandle::new(host.shape.to_vec(), 0, u64::MAX - 9))
1298        }
1299
1300        fn download<'a>(
1301            &'a self,
1302            _handle: &'a GpuTensorHandle,
1303        ) -> runmat_accelerate_api::AccelDownloadFuture<'a> {
1304            Box::pin(async { anyhow::bail!("unused") })
1305        }
1306
1307        fn free(&self, _handle: &GpuTensorHandle) -> anyhow::Result<()> {
1308            Ok(())
1309        }
1310
1311        fn device_info(&self) -> String {
1312            "malformed upload test provider".into()
1313        }
1314    }
1315
1316    #[test]
1317    fn shared_upload_boundary_rejects_descriptor_empty_provider_handles() {
1318        let tensor = Tensor::from_f32(vec![1.0, 2.0], vec![1, 2]).unwrap();
1319        let error = upload_tensor(&MalformedUploadProvider, &tensor)
1320            .expect_err("provider upload must publish its physical descriptor");
1321        assert!(error.contains("provider returned descriptor"));
1322        assert!(error.contains("F32 Real upload"));
1323    }
1324
1325    impl runmat_accelerate_api::AccelProvider for MalformedDownloadProvider {
1326        fn upload(
1327            &self,
1328            _host: &runmat_accelerate_api::HostTensorView,
1329        ) -> anyhow::Result<GpuTensorHandle> {
1330            anyhow::bail!("unused")
1331        }
1332
1333        fn download<'a>(
1334            &'a self,
1335            _handle: &'a GpuTensorHandle,
1336        ) -> runmat_accelerate_api::AccelDownloadFuture<'a> {
1337            Box::pin(async {
1338                Ok(runmat_accelerate_api::HostTensorOwned {
1339                    data: vec![1.0, 2.0],
1340                    shape: vec![2, 1],
1341                    storage: GpuTensorStorage::Real,
1342                })
1343            })
1344        }
1345
1346        fn free(&self, _handle: &GpuTensorHandle) -> anyhow::Result<()> {
1347            Ok(())
1348        }
1349
1350        fn device_info(&self) -> String {
1351            "malformed download test provider".into()
1352        }
1353    }
1354
1355    struct MutatingDownloadProvider;
1356
1357    impl runmat_accelerate_api::AccelProvider for MutatingDownloadProvider {
1358        fn upload(
1359            &self,
1360            _host: &runmat_accelerate_api::HostTensorView,
1361        ) -> anyhow::Result<GpuTensorHandle> {
1362            anyhow::bail!("unused")
1363        }
1364
1365        fn download<'a>(
1366            &'a self,
1367            handle: &'a GpuTensorHandle,
1368        ) -> runmat_accelerate_api::AccelDownloadFuture<'a> {
1369            Box::pin(async move {
1370                runmat_accelerate_api::set_handle_logical(handle, true);
1371                runmat_accelerate_api::set_handle_class_name(handle, "logical");
1372                Ok(runmat_accelerate_api::HostTensorOwned {
1373                    data: vec![1.0],
1374                    shape: handle.shape.clone(),
1375                    storage: GpuTensorStorage::Real,
1376                })
1377            })
1378        }
1379
1380        fn free(&self, _handle: &GpuTensorHandle) -> anyhow::Result<()> {
1381            Ok(())
1382        }
1383
1384        fn device_info(&self) -> String {
1385            "mutating download test provider".into()
1386        }
1387    }
1388
1389    #[test]
1390    fn owner_download_preserves_integer_source_residency_and_metadata() {
1391        test_support::with_test_provider(|provider| {
1392            let source = Tensor::new_integer(IntegerStorage::U64(vec![3, 4]), vec![1, 2])
1393                .expect("integer source");
1394            let handle = upload_tensor(provider, &source).expect("upload integer source");
1395            runmat_accelerate::ensure_residency_hooks();
1396            runmat_accelerate_api::mark_residency(&handle);
1397            runmat_accelerate_api::record_handle_transpose(&handle, 2, 1);
1398            let snapshot = snapshot_handle_metadata(&handle);
1399            runmat_accelerate_api::clear_residency(&handle);
1400            runmat_accelerate_api::clear_handle_transpose(&handle);
1401            restore_handle_metadata(&handle, &snapshot);
1402            assert!(runmat_accelerate::fusion_residency::is_resident(&handle));
1403            assert_eq!(
1404                runmat_accelerate_api::handle_transpose_info(&handle),
1405                Some(runmat_accelerate_api::TransposeInfo {
1406                    base_rows: 2,
1407                    base_cols: 1,
1408                })
1409            );
1410            let metadata = (
1411                runmat_accelerate_api::handle_storage(&handle),
1412                runmat_accelerate_api::handle_precision(&handle),
1413                runmat_accelerate_api::handle_integer_type(&handle),
1414                runmat_accelerate_api::handle_is_logical(&handle),
1415            );
1416            let downloaded = block_on(download_value_preserving_residency_async(provider, &handle))
1417                .expect("non-destructive owner download");
1418            let Value::Tensor(downloaded) = downloaded else {
1419                panic!("expected integer tensor")
1420            };
1421            assert_eq!(
1422                downloaded.into_numeric_storage().unwrap(),
1423                runmat_value::NumericStorage::U64(vec![3, 4])
1424            );
1425            assert!(runmat_accelerate_api::provider_for_handle(&handle).is_some());
1426            assert_eq!(runmat_accelerate_api::handle_storage(&handle), metadata.0);
1427            assert_eq!(runmat_accelerate_api::handle_precision(&handle), metadata.1);
1428            assert_eq!(
1429                runmat_accelerate_api::handle_integer_type(&handle),
1430                metadata.2
1431            );
1432            assert_eq!(
1433                runmat_accelerate_api::handle_is_logical(&handle),
1434                metadata.3
1435            );
1436        });
1437    }
1438
1439    #[test]
1440    fn native_truth_download_keeps_wide_unsigned_values_exact_and_resident() {
1441        test_support::with_test_provider(|provider| {
1442            let source = Tensor::new_integer(
1443                IntegerStorage::U64(vec![0, 1, 1_u64 << 63, u64::MAX]),
1444                vec![1, 4],
1445            )
1446            .expect("wide integer source");
1447            let handle = upload_tensor(provider, &source).expect("wide integer upload");
1448            let handle =
1449                handle.with_provenance(runmat_accelerate_api::GpuHandleProvenance::Automatic);
1450
1451            let native = block_on(download_native_values_async(provider, &handle))
1452                .expect("native-value download");
1453            assert_eq!(
1454                native.data,
1455                vec![
1456                    NumericScalar::U64(0),
1457                    NumericScalar::U64(1),
1458                    NumericScalar::U64(1_u64 << 63),
1459                    NumericScalar::U64(u64::MAX),
1460                ]
1461            );
1462            let truth = block_on(download_truth_values_async(provider, &handle))
1463                .expect("truth-value download");
1464            assert_eq!(truth.data, vec![0, 1, 1, 1]);
1465            assert_eq!(truth.shape, vec![1, 4]);
1466            assert!(runmat_accelerate_api::provider_for_handle(&handle).is_some());
1467            assert_eq!(
1468                runmat_accelerate_api::handle_provenance(&handle),
1469                Some(runmat_accelerate_api::GpuHandleProvenance::Automatic)
1470            );
1471
1472            provider.free(&handle).unwrap();
1473            runmat_accelerate_api::clear_handle_metadata(&handle);
1474        });
1475    }
1476
1477    #[test]
1478    fn preserving_download_contract_errors_are_terminal_to_dispatcher_retry() {
1479        let error =
1480            build_runtime_error("gpu download: complex-interleaved buffer has odd scalar length")
1481                .with_gpu_gather_retry(crate::GpuGatherRetry::Never)
1482                .build();
1483        assert_eq!(error.gpu_gather_retry(), crate::GpuGatherRetry::Never);
1484    }
1485
1486    #[test]
1487    fn preserving_download_restores_metadata_mutated_by_provider() {
1488        let handle = GpuTensorHandle {
1489            shape: vec![1, 1],
1490            device_id: 0,
1491            buffer_id: u64::MAX - 8,
1492            descriptor: runmat_accelerate_api::GpuTensorDescriptor::numeric(
1493                NumericElementType::F64,
1494                GpuTensorStorage::Real,
1495            ),
1496        };
1497        runmat_accelerate_api::set_handle_logical(&handle, false);
1498        runmat_accelerate_api::set_handle_class_name(&handle, "double");
1499        let result = block_on(download_value_preserving_residency_async(
1500            &MutatingDownloadProvider,
1501            &handle,
1502        ))
1503        .expect("valid payload should download");
1504        assert!(matches!(result, Value::Tensor(_)));
1505        assert!(!runmat_accelerate_api::handle_is_logical(&handle));
1506        assert_eq!(
1507            runmat_accelerate_api::handle_class_name(&handle).as_deref(),
1508            Some("double")
1509        );
1510    }
1511
1512    #[test]
1513    fn preserving_download_rejects_provider_payload_shape_mismatch() {
1514        let handle = GpuTensorHandle {
1515            shape: vec![1, 2],
1516            device_id: 0,
1517            buffer_id: u64::MAX - 7,
1518            descriptor: Default::default(),
1519        };
1520        let error = block_on(download_value_preserving_residency_async(
1521            &MalformedDownloadProvider,
1522            &handle,
1523        ))
1524        .expect_err("provider payload metadata must match the source handle");
1525        assert_eq!(
1526            error.identifier(),
1527            Some("RunMat:gpu:ProviderPayloadMismatch")
1528        );
1529        assert_eq!(error.gpu_gather_retry(), crate::GpuGatherRetry::Never);
1530    }
1531
1532    #[test]
1533    fn preserving_download_requires_durable_physical_metadata() {
1534        test_support::with_test_provider(|provider| {
1535            let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
1536            let handle = upload_tensor(provider, &tensor).expect("upload");
1537            let downloaded = block_on(download_value_preserving_residency_async(provider, &handle))
1538                .expect("durable descriptor must be accepted");
1539            assert!(matches!(downloaded, Value::Tensor(_)));
1540
1541            let mut descriptorless = handle.clone();
1542            descriptorless.descriptor.element_type = None;
1543            let missing = block_on(download_value_preserving_residency_async(
1544                provider,
1545                &descriptorless,
1546            ))
1547            .expect_err("missing physical precision must reject");
1548            assert_eq!(
1549                missing.identifier(),
1550                Some("RunMat:gpu:ProviderPayloadMismatch")
1551            );
1552        });
1553    }
1554}
1555
1556#[cfg(all(test, feature = "wgpu"))]
1557mod tests {
1558    use super::*;
1559    use futures::executor::block_on;
1560    use runmat_accelerate_api::{HostIntegerDataOwned, HostIntegerDataView, HostIntegerTensorView};
1561
1562    #[test]
1563    fn exact_integer_scalar_upload_preserves_64_bit_class_and_admission_rules() {
1564        if runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
1565            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
1566        )
1567        .is_err()
1568        {
1569            return;
1570        }
1571        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
1572        let shape = [1usize, 1usize];
1573        let signed = provider
1574            .upload_integer(&HostIntegerTensorView {
1575                data: HostIntegerDataView::I64(&[0]),
1576                shape: &shape,
1577            })
1578            .expect("upload int64 prototype");
1579        let unsigned = provider
1580            .upload_integer(&HostIntegerTensorView {
1581                data: HostIntegerDataView::U64(&[0]),
1582                shape: &shape,
1583            })
1584            .expect("upload uint64 prototype");
1585        let signed_scalar = upload_exact_integer_scalar_like(provider, &signed, -7.0)
1586            .expect("representable int64 scalar");
1587        let unsigned_scalar = upload_exact_integer_scalar_like(provider, &unsigned, 7.0)
1588            .expect("representable uint64 scalar");
1589        assert_eq!(
1590            runmat_accelerate_api::handle_integer_type(&signed_scalar),
1591            Some(IntegerElementType::I64)
1592        );
1593        assert_eq!(
1594            runmat_accelerate_api::handle_integer_type(&unsigned_scalar),
1595            Some(IntegerElementType::U64)
1596        );
1597        assert_eq!(
1598            block_on(provider.download_integer(&signed_scalar))
1599                .expect("download int64")
1600                .data,
1601            HostIntegerDataOwned::I64(vec![-7])
1602        );
1603        assert_eq!(
1604            block_on(provider.download_integer(&unsigned_scalar))
1605                .expect("download uint64")
1606                .data,
1607            HostIntegerDataOwned::U64(vec![7])
1608        );
1609        assert!(upload_exact_integer_scalar_like(provider, &signed, 1.5).is_none());
1610        assert!(upload_exact_integer_scalar_like(provider, &unsigned, -1.0).is_none());
1611        for handle in [&signed, &unsigned, &signed_scalar, &unsigned_scalar] {
1612            provider.free(handle).expect("free integer scalar handle");
1613        }
1614    }
1615}