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
26pub 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
36pub 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
334pub async fn gather_tensor_async(
339 handle: &runmat_accelerate_api::GpuTensorHandle,
340) -> crate::BuiltinResult<Tensor> {
341 #[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
374pub async fn gather_value_async(value: &Value) -> crate::BuiltinResult<Value> {
376 crate::dispatcher::gather_if_needed_async(value).await
377}
378
379pub 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
429pub struct HostNativeTensorOwned {
431 pub data: Vec<NumericScalar>,
432 pub shape: Vec<usize>,
433}
434
435pub 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
487pub struct HostTruthTensorOwned {
489 pub data: Vec<u8>,
490 pub shape: Vec<usize>,
491}
492
493pub 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
510pub 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
661pub 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
702pub 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
745pub 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
956pub 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
1012pub 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
1022pub fn logical_gpu_value(handle: GpuTensorHandle) -> Value {
1025 runmat_accelerate_api::set_handle_logical(&handle, true);
1026 resident_gpu_value(handle)
1027}
1028
1029pub 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}