1use crate::builtins::acceleration::gpu::type_resolvers::gpuarray_type;
7use crate::builtins::common::spec::{
8 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
9 ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
10};
11use crate::builtins::common::{gpu_helpers, tensor};
12use runmat_accelerate_api::{GpuTensorHandle, ProviderPrecision};
13#[cfg(test)]
14use runmat_accelerate_api::{HostIntegerDataView, HostIntegerTensorView};
15use runmat_builtins::{
16 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
17 BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
18 BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
19 BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
20 BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
21 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
22};
23use runmat_macros::runtime_builtin;
24use runmat_value::ComplexStorage;
25use runmat_value::{
26 CharArray, ComplexTensor, IntValue, IntegerComplexStorage, IntegerStorage, NumericDType,
27 NumericStorage, Tensor, Value,
28};
29
30use crate::{build_runtime_error, BuiltinResult, RuntimeError};
31
32const BUILTIN_NAME: &str = "gpuArray";
33
34const GPUARRAY_SIZE_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
35 id: "gpuarray-size-arguments",
36 mode: BuiltinExtensionMode::RunMatOnly,
37 description: "gpuArray size arguments are a RunMat extension",
38 error_identifier: Some("RunMat:compatibility:GpuArraySizeExtension"),
39};
40
41const GPUARRAY_DTYPE_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
42 id: "gpuarray-dtype-selector",
43 mode: BuiltinExtensionMode::RunMatOnly,
44 description: "gpuArray dtype selectors are a RunMat extension",
45 error_identifier: Some("RunMat:compatibility:GpuArrayDtypeExtension"),
46};
47
48const GPUARRAY_LIKE_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
49 id: "gpuarray-like",
50 mode: BuiltinExtensionMode::RunMatOnly,
51 description: "the gpuArray \"like\" prototype selector is a RunMat extension",
52 error_identifier: Some("RunMat:compatibility:GpuArrayLikeExtension"),
53};
54
55const GPUARRAY_TEXT_UPLOAD_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
56 id: "gpuarray-text-upload",
57 mode: BuiltinExtensionMode::RunMatOnly,
58 description:
59 "uploading character vectors or string scalars with gpuArray is a RunMat extension",
60 error_identifier: Some("RunMat:compatibility:GpuArrayTextUploadExtension"),
61};
62
63pub const GPUARRAY_EXTENSIONS: [BuiltinExtensionDescriptor; 4] = [
64 GPUARRAY_SIZE_EXTENSION,
65 GPUARRAY_DTYPE_EXTENSION,
66 GPUARRAY_LIKE_EXTENSION,
67 GPUARRAY_TEXT_UPLOAD_EXTENSION,
68];
69
70const GPUARRAY_INTEGER_INPUTS: [BuiltinIntegerInputCapability; 1] =
71 [BuiltinIntegerInputCapability {
72 name: "X",
73 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
74 availability: BuiltinIntegerInputAvailability::Documented,
75 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
76 notes: "All eight integer classes upload as exact same-class real or paired-complex gpuArray storage with the original shape.",
77 }];
78
79const GPUARRAY_INTEGER_DTYPE_INPUTS: [BuiltinIntegerInputCapability; 1] =
80 [BuiltinIntegerInputCapability {
81 name: "X",
82 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
83 availability: BuiltinIntegerInputAvailability::RunMatOnly,
84 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
85 notes: "RunMat-only dtype selectors may explicitly convert X to any supported integer gpuArray class.",
86 }];
87
88pub const GPUARRAY_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 2] = [
89 BuiltinIntegerCapabilityDescriptor {
90 form: "G = gpuArray(integer_X)",
91 inputs: &GPUARRAY_INTEGER_INPUTS,
92 computation_domain: BuiltinIntegerComputationDomain::Structural,
93 output_class: BuiltinIntegerOutputClassRule::PreserveInput,
94 overflow: BuiltinIntegerOverflowRule::NotApplicable,
95 backend: BuiltinIntegerBackendRule::HostAndGpu,
96 overload: BuiltinIntegerOverloadKind::ElementwiseShapePreserving,
97 notes: "The transfer preserves exact values, class, shape, and supported complexity. An existing gpuArray input is returned unchanged and remains valid.",
98 },
99 BuiltinIntegerCapabilityDescriptor {
100 form: "G = gpuArray(X, integer_dtype)",
101 inputs: &GPUARRAY_INTEGER_DTYPE_INPUTS,
102 computation_domain: BuiltinIntegerComputationDomain::Structural,
103 output_class: BuiltinIntegerOutputClassRule::OptionDependent,
104 overflow: BuiltinIntegerOverflowRule::Saturate,
105 backend: BuiltinIntegerBackendRule::HostAndGpu,
106 overload: BuiltinIntegerOverloadKind::ElementwiseShapePreserving,
107 notes: "RunMat-only conversion uses the requested native integer class and never consumes or invalidates a gpuArray input.",
108 },
109];
110
111const GPUARRAY_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
112 name: "G",
113 ty: BuiltinParamType::Any,
114 arity: BuiltinParamArity::Required,
115 default: None,
116 description: "GPU-resident handle containing uploaded/converted data.",
117}];
118
119const GPUARRAY_INPUTS_BASE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
120 name: "X",
121 ty: BuiltinParamType::Any,
122 arity: BuiltinParamArity::Required,
123 default: None,
124 description: "Input value to upload or recast on GPU.",
125}];
126
127const GPUARRAY_INPUTS_DIMS: [BuiltinParamDescriptor; 2] = [
128 BuiltinParamDescriptor {
129 name: "X",
130 ty: BuiltinParamType::Any,
131 arity: BuiltinParamArity::Required,
132 default: None,
133 description: "Input value to upload or recast on GPU.",
134 },
135 BuiltinParamDescriptor {
136 name: "dim",
137 ty: BuiltinParamType::SizeArg,
138 arity: BuiltinParamArity::Variadic,
139 default: None,
140 description: "Reshape dimensions (scalar dims or a single size vector tensor).",
141 },
142];
143
144const GPUARRAY_INPUTS_DTYPE: [BuiltinParamDescriptor; 2] = [
145 BuiltinParamDescriptor {
146 name: "X",
147 ty: BuiltinParamType::Any,
148 arity: BuiltinParamArity::Required,
149 default: None,
150 description: "Input value to upload or recast on GPU.",
151 },
152 BuiltinParamDescriptor {
153 name: "dtype",
154 ty: BuiltinParamType::StringScalar,
155 arity: BuiltinParamArity::Required,
156 default: Some("\"double\""),
157 description: "Class tag such as `single`, `int32`, `uint8`, `logical`, or `double`.",
158 },
159];
160
161const GPUARRAY_INPUTS_LIKE: [BuiltinParamDescriptor; 3] = [
162 BuiltinParamDescriptor {
163 name: "X",
164 ty: BuiltinParamType::Any,
165 arity: BuiltinParamArity::Required,
166 default: None,
167 description: "Input value to upload or recast on GPU.",
168 },
169 BuiltinParamDescriptor {
170 name: "like",
171 ty: BuiltinParamType::StringScalar,
172 arity: BuiltinParamArity::Required,
173 default: None,
174 description: "Literal keyword `\"like\"`.",
175 },
176 BuiltinParamDescriptor {
177 name: "prototype",
178 ty: BuiltinParamType::LikePrototype,
179 arity: BuiltinParamArity::Required,
180 default: None,
181 description: "Prototype value whose class drives output conversion.",
182 },
183];
184
185const GPUARRAY_INPUTS_DIMS_OPTIONS: [BuiltinParamDescriptor; 3] = [
186 BuiltinParamDescriptor {
187 name: "X",
188 ty: BuiltinParamType::Any,
189 arity: BuiltinParamArity::Required,
190 default: None,
191 description: "Input value to upload or recast on GPU.",
192 },
193 BuiltinParamDescriptor {
194 name: "dim",
195 ty: BuiltinParamType::SizeArg,
196 arity: BuiltinParamArity::Variadic,
197 default: None,
198 description: "Reshape dimensions (scalar dims or a single size vector tensor).",
199 },
200 BuiltinParamDescriptor {
201 name: "option",
202 ty: BuiltinParamType::Any,
203 arity: BuiltinParamArity::Variadic,
204 default: None,
205 description: "Class tags and/or `\"like\", prototype` qualifiers.",
206 },
207];
208
209const GPUARRAY_SIGNATURES: [BuiltinSignatureDescriptor; 5] = [
210 BuiltinSignatureDescriptor {
211 label: "G = gpuArray(X)",
212 inputs: &GPUARRAY_INPUTS_BASE,
213 outputs: &GPUARRAY_OUTPUT,
214 },
215 BuiltinSignatureDescriptor {
216 label: "G = gpuArray(X, dim, ...)",
217 inputs: &GPUARRAY_INPUTS_DIMS,
218 outputs: &GPUARRAY_OUTPUT,
219 },
220 BuiltinSignatureDescriptor {
221 label: "G = gpuArray(X, dtype)",
222 inputs: &GPUARRAY_INPUTS_DTYPE,
223 outputs: &GPUARRAY_OUTPUT,
224 },
225 BuiltinSignatureDescriptor {
226 label: "G = gpuArray(X, \"like\", prototype)",
227 inputs: &GPUARRAY_INPUTS_LIKE,
228 outputs: &GPUARRAY_OUTPUT,
229 },
230 BuiltinSignatureDescriptor {
231 label: "G = gpuArray(X, dim, ..., option, ...)",
232 inputs: &GPUARRAY_INPUTS_DIMS_OPTIONS,
233 outputs: &GPUARRAY_OUTPUT,
234 },
235];
236
237const GPUARRAY_ERROR_NO_PROVIDER: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
238 code: "RM.GPUARRAY.NO_PROVIDER",
239 identifier: Some("RunMat:gpuArray:NoProvider"),
240 when: "No acceleration provider is registered for host/device transfers.",
241 message: "gpuArray: no acceleration provider registered",
242};
243
244const GPUARRAY_ERROR_OPTION_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
245 code: "RM.GPUARRAY.OPTION_ARGUMENT",
246 identifier: Some("RunMat:gpuArray:OptionArgument"),
247 when: "Option tail contains non-text values where class tags/keywords are expected.",
248 message: "gpuArray: invalid option argument",
249};
250
251const GPUARRAY_ERROR_LIKE_MISSING: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
252 code: "RM.GPUARRAY.LIKE_MISSING",
253 identifier: Some("RunMat:gpuArray:LikeMissingPrototype"),
254 when: "Keyword `like` is supplied without a following prototype value.",
255 message: "gpuArray: expected a prototype value after 'like'",
256};
257
258const GPUARRAY_ERROR_LIKE_DUPLICATE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
259 code: "RM.GPUARRAY.LIKE_DUPLICATE",
260 identifier: Some("RunMat:gpuArray:LikeDuplicate"),
261 when: "Keyword `like` appears more than once.",
262 message: "gpuArray: duplicate 'like' qualifier",
263};
264
265const GPUARRAY_ERROR_CODISTRIBUTED_UNSUPPORTED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
266 code: "RM.GPUARRAY.CODISTRIBUTED_UNSUPPORTED",
267 identifier: Some("RunMat:gpuArray:CodistributedUnsupported"),
268 when: "Distributed/codistributed qualifiers are requested.",
269 message: "gpuArray: codistributed arrays are not supported yet",
270};
271
272const GPUARRAY_ERROR_CONFLICTING_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
273 code: "RM.GPUARRAY.CONFLICTING_TYPE",
274 identifier: Some("RunMat:gpuArray:ConflictingTypeQualifiers"),
275 when: "Multiple incompatible class qualifiers are supplied.",
276 message: "gpuArray: conflicting type qualifiers supplied",
277};
278
279const GPUARRAY_ERROR_UNKNOWN_OPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
280 code: "RM.GPUARRAY.UNKNOWN_OPTION",
281 identifier: Some("RunMat:gpuArray:UnknownOption"),
282 when: "Text option is not a recognized class/keyword token.",
283 message: "gpuArray: unrecognised option",
284};
285
286const GPUARRAY_ERROR_SIZE_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
287 code: "RM.GPUARRAY.SIZE_ARGUMENT",
288 identifier: Some("RunMat:gpuArray:InvalidSizeArgument"),
289 when: "Size arguments are malformed (not finite integers, negative, or invalid combinations).",
290 message: "gpuArray: invalid size argument",
291};
292
293const GPUARRAY_ERROR_LIKE_PROTOTYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
294 code: "RM.GPUARRAY.LIKE_PROTOTYPE",
295 identifier: Some("RunMat:gpuArray:InvalidLikePrototype"),
296 when: "`like` prototype is unsupported for type inference.",
297 message: "gpuArray: invalid 'like' prototype",
298};
299
300const GPUARRAY_ERROR_INPUT_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
301 code: "RM.GPUARRAY.INPUT_TYPE",
302 identifier: Some("RunMat:gpuArray:UnsupportedInputType"),
303 when: "Input value type cannot be uploaded/coerced to supported gpuArray storage.",
304 message: "gpuArray: unsupported input type",
305};
306
307const GPUARRAY_ERROR_TYPED_INTEGER: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
308 code: "RM.GPUARRAY.TYPED_INTEGER",
309 identifier: Some("RunMat:gpuArray:TypedIntegerUnsupported"),
310 when: "A native integer value or integer GPU class is requested without matching provider storage.",
311 message: "gpuArray: native integer storage is not supported by the active acceleration provider",
312};
313
314const GPUARRAY_ERROR_CONVERSION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
315 code: "RM.GPUARRAY.CONVERSION",
316 identifier: Some("RunMat:gpuArray:ConversionFailed"),
317 when: "Requested dtype conversion cannot be performed (for example NaN->logical).",
318 message: "gpuArray: conversion failed",
319};
320
321const GPUARRAY_ERROR_RESHAPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
322 code: "RM.GPUARRAY.RESHAPE",
323 identifier: Some("RunMat:gpuArray:ReshapeMismatch"),
324 when: "Requested shape does not preserve the element count.",
325 message: "gpuArray: cannot reshape gpuArray into requested size",
326};
327
328const GPUARRAY_ERROR_PROVIDER_IO: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
329 code: "RM.GPUARRAY.PROVIDER_IO",
330 identifier: Some("RunMat:gpuArray:ProviderIO"),
331 when: "Provider upload/download interaction fails.",
332 message: "gpuArray: provider I/O failed",
333};
334
335const GPUARRAY_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
336 code: "RM.GPUARRAY.INTERNAL",
337 identifier: Some("RunMat:gpuArray:InternalError"),
338 when: "Internal tensor/container conversion fails.",
339 message: "gpuArray: internal error",
340};
341
342const GPUARRAY_ERRORS: [BuiltinErrorDescriptor; 15] = [
343 GPUARRAY_ERROR_NO_PROVIDER,
344 GPUARRAY_ERROR_OPTION_ARGUMENT,
345 GPUARRAY_ERROR_LIKE_MISSING,
346 GPUARRAY_ERROR_LIKE_DUPLICATE,
347 GPUARRAY_ERROR_CODISTRIBUTED_UNSUPPORTED,
348 GPUARRAY_ERROR_CONFLICTING_TYPE,
349 GPUARRAY_ERROR_UNKNOWN_OPTION,
350 GPUARRAY_ERROR_SIZE_ARGUMENT,
351 GPUARRAY_ERROR_LIKE_PROTOTYPE,
352 GPUARRAY_ERROR_INPUT_TYPE,
353 GPUARRAY_ERROR_TYPED_INTEGER,
354 GPUARRAY_ERROR_CONVERSION,
355 GPUARRAY_ERROR_RESHAPE,
356 GPUARRAY_ERROR_PROVIDER_IO,
357 GPUARRAY_ERROR_INTERNAL,
358];
359
360pub const GPUARRAY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
361 signatures: &GPUARRAY_SIGNATURES,
362 output_mode: BuiltinOutputMode::Fixed,
363 completion_policy: BuiltinCompletionPolicy::Public,
364 errors: &GPUARRAY_ERRORS,
365};
366
367fn gpu_array_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
368 gpu_array_error_with_message(error.message, error)
369}
370
371fn gpu_array_error_with_message(
372 message: impl Into<String>,
373 error: &'static BuiltinErrorDescriptor,
374) -> RuntimeError {
375 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
376 if let Some(identifier) = error.identifier {
377 builder = builder.with_identifier(identifier);
378 }
379 builder.build()
380}
381
382fn gpu_array_error_with_detail(
383 error: &'static BuiltinErrorDescriptor,
384 detail: impl AsRef<str>,
385) -> RuntimeError {
386 gpu_array_error_with_message(format!("{}: {}", error.message, detail.as_ref()), error)
387}
388
389#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::acceleration::gpu::gpuarray")]
390pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
391 name: "gpuArray",
392 op_kind: GpuOpKind::Custom("upload"),
393 supported_precisions: &[ScalarType::F32, ScalarType::F64],
394 broadcast: BroadcastSemantics::None,
395 provider_hooks: &[ProviderHook::Custom("upload_numeric")],
396 constant_strategy: ConstantStrategy::InlineLiteral,
397 residency: ResidencyPolicy::NewHandle,
398 nan_mode: ReductionNaN::Include,
399 two_pass_threshold: None,
400 workgroup_size: None,
401 accepts_nan_mode: false,
402 notes: "Invokes the provider's native numeric upload contract for real or complex double, single, and integer storage, and reuploads gpuArray inputs when dtype conversion is requested. Handles class strings, size vectors, and `'like'` prototypes.",
403};
404
405#[runmat_macros::register_fusion_spec(
406 builtin_path = "crate::builtins::acceleration::gpu::gpuarray"
407)]
408pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
409 name: "gpuArray",
410 shape: ShapeRequirements::Any,
411 constant_strategy: ConstantStrategy::InlineLiteral,
412 elementwise: None,
413 reduction: None,
414 emits_nan: false,
415 notes:
416 "Acts as a residency boundary; fusion graphs never cross explicit host↔device transfers.",
417};
418
419#[runtime_builtin(
420 name = "gpuArray",
421 category = "acceleration/gpu",
422 summary = "Move data to the GPU as gpuArray values.",
423 keywords = "gpuArray,gpu,accelerate,upload,dtype,like",
424 examples = "G = gpuArray([1 2 3], 'single');",
425 accel = "array_construct",
426 type_resolver(gpuarray_type),
427 descriptor(crate::builtins::acceleration::gpu::gpuarray::GPUARRAY_DESCRIPTOR),
428 extensions(crate::builtins::acceleration::gpu::gpuarray::GPUARRAY_EXTENSIONS),
429 integer_capabilities(
430 crate::builtins::acceleration::gpu::gpuarray::GPUARRAY_INTEGER_CAPABILITIES
431 ),
432 builtin_path = "crate::builtins::acceleration::gpu::gpuarray"
433)]
434async fn gpu_array_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
435 let options = parse_options(&rest)?;
436 if options.dims.is_some() {
437 crate::compatibility::ensure_builtin_extension_enabled(
438 &GPUARRAY_SIZE_EXTENSION,
439 BUILTIN_NAME,
440 )?;
441 }
442 if options.explicit_dtype.is_some() {
443 crate::compatibility::ensure_builtin_extension_enabled(
444 &GPUARRAY_DTYPE_EXTENSION,
445 BUILTIN_NAME,
446 )?;
447 }
448 if options.prototype.is_some() {
449 crate::compatibility::ensure_builtin_extension_enabled(
450 &GPUARRAY_LIKE_EXTENSION,
451 BUILTIN_NAME,
452 )?;
453 }
454 if matches!(value, Value::CharArray(_) | Value::String(_)) {
455 crate::compatibility::ensure_builtin_extension_enabled(
456 &GPUARRAY_TEXT_UPLOAD_EXTENSION,
457 BUILTIN_NAME,
458 )?;
459 }
460 if rest.is_empty() {
461 if let Value::GpuTensor(handle) = &value {
462 let explicit = handle
465 .clone()
466 .with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit);
467 return Ok(Value::GpuTensor(explicit));
468 }
469 }
470 let dtype = resolve_dtype(&value, &options)?;
471 let dims = options.dims.clone();
472 let expected_shape = dims
473 .clone()
474 .unwrap_or_else(|| gpu_array_value_shape(&value));
475
476 let prepared = match value {
477 Value::GpuTensor(handle) => convert_device_value(handle, dtype).await?,
478 other => upload_host_value(other, dtype)?,
479 };
480
481 let mut handle = prepared.handle;
482 if let Some(dims) = dims.as_ref() {
483 apply_dims(&mut handle, dims)?;
484 }
485
486 if let Err(error) = validate_prepared_handle(
487 &handle,
488 prepared.provider,
489 &expected_shape,
490 prepared.storage,
491 dtype,
492 prepared.logical,
493 ) {
494 if prepared.owns_handle {
495 let _ = prepared.provider.free(&handle);
496 }
497 return Err(error);
498 }
499 runmat_accelerate_api::set_handle_logical(&handle, prepared.logical);
500 runmat_accelerate_api::set_handle_class_name(&handle, dtype.class_name());
501 handle.descriptor.provenance = Some(runmat_accelerate_api::GpuHandleProvenance::Explicit);
502
503 Ok(Value::GpuTensor(handle))
504}
505
506fn gpu_array_value_shape(value: &Value) -> Vec<usize> {
507 match value {
508 Value::Tensor(tensor) => tensor.shape.clone(),
509 Value::ComplexTensor(tensor) => tensor.shape.clone(),
510 Value::LogicalArray(array) => array.shape.clone(),
511 Value::GpuTensor(handle) => handle.shape.clone(),
512 Value::CharArray(array) => array.shape.clone(),
513 Value::String(text) => vec![1, text.chars().count()],
514 _ => vec![1, 1],
515 }
516}
517
518#[derive(Clone, Copy, Debug, PartialEq, Eq)]
519enum DataClass {
520 Double,
521 Single,
522 Logical,
523 Int8,
524 Int16,
525 Int32,
526 Int64,
527 UInt8,
528 UInt16,
529 UInt32,
530 UInt64,
531}
532
533impl DataClass {
534 fn is_integer(self) -> bool {
535 matches!(
536 self,
537 Self::Int8
538 | Self::Int16
539 | Self::Int32
540 | Self::Int64
541 | Self::UInt8
542 | Self::UInt16
543 | Self::UInt32
544 | Self::UInt64
545 )
546 }
547
548 fn from_tag(tag: &str) -> Option<Self> {
549 match tag {
550 "double" => Some(Self::Double),
551 "single" | "float32" => Some(Self::Single),
552 "logical" | "bool" | "boolean" => Some(Self::Logical),
553 "int8" => Some(Self::Int8),
554 "int16" => Some(Self::Int16),
555 "int32" | "int" => Some(Self::Int32),
556 "int64" => Some(Self::Int64),
557 "uint8" => Some(Self::UInt8),
558 "uint16" => Some(Self::UInt16),
559 "uint32" => Some(Self::UInt32),
560 "uint64" => Some(Self::UInt64),
561 "gpuarray" => None, _ => None,
563 }
564 }
565
566 fn class_name(self) -> &'static str {
567 match self {
568 Self::Double => "double",
569 Self::Single => "single",
570 Self::Logical => "logical",
571 Self::Int8 => "int8",
572 Self::Int16 => "int16",
573 Self::Int32 => "int32",
574 Self::Int64 => "int64",
575 Self::UInt8 => "uint8",
576 Self::UInt16 => "uint16",
577 Self::UInt32 => "uint32",
578 Self::UInt64 => "uint64",
579 }
580 }
581
582 fn numeric_dtype(self) -> NumericDType {
583 match self {
584 Self::Double => NumericDType::F64,
585 Self::Single => NumericDType::F32,
586 Self::Logical => NumericDType::F64,
587 Self::Int8 => NumericDType::I8,
588 Self::Int16 => NumericDType::I16,
589 Self::Int32 => NumericDType::I32,
590 Self::Int64 => NumericDType::I64,
591 Self::UInt8 => NumericDType::U8,
592 Self::UInt16 => NumericDType::U16,
593 Self::UInt32 => NumericDType::U32,
594 Self::UInt64 => NumericDType::U64,
595 }
596 }
597}
598
599#[derive(Debug, Default)]
600struct ParsedOptions {
601 dims: Option<Vec<usize>>,
602 explicit_dtype: Option<DataClass>,
603 prototype: Option<Value>,
604}
605
606fn parse_options(rest: &[Value]) -> BuiltinResult<ParsedOptions> {
607 let (index_after_dims, dims) = parse_size_arguments(rest)?;
608 let mut options = ParsedOptions {
609 dims,
610 ..ParsedOptions::default()
611 };
612
613 let mut idx = index_after_dims;
614 while idx < rest.len() {
615 let tag = value_to_lower_string(&rest[idx]).ok_or_else(|| {
616 gpu_array_error_with_message(
617 format!(
618 "gpuArray: unexpected argument {:?}; expected a class string or the keyword 'like'",
619 rest[idx]
620 ),
621 &GPUARRAY_ERROR_OPTION_ARGUMENT,
622 )
623 })?;
624
625 match tag.as_str() {
626 "like" => {
627 idx += 1;
628 if idx >= rest.len() {
629 return Err(gpu_array_error(&GPUARRAY_ERROR_LIKE_MISSING));
630 }
631 if options.prototype.is_some() {
632 return Err(gpu_array_error(&GPUARRAY_ERROR_LIKE_DUPLICATE));
633 }
634 options.prototype = Some(rest[idx].clone());
635 }
636 "distributed" | "codistributed" => {
637 return Err(gpu_array_error(&GPUARRAY_ERROR_CODISTRIBUTED_UNSUPPORTED));
638 }
639 tag => {
640 if let Some(class) = DataClass::from_tag(tag) {
641 if let Some(existing) = options.explicit_dtype {
642 if existing != class {
643 return Err(gpu_array_error(&GPUARRAY_ERROR_CONFLICTING_TYPE));
644 }
645 } else {
646 options.explicit_dtype = Some(class);
647 }
648 } else if tag != "gpuarray" {
649 return Err(gpu_array_error_with_detail(
650 &GPUARRAY_ERROR_UNKNOWN_OPTION,
651 format!("unrecognised option '{tag}'"),
652 ));
653 }
654 }
655 }
656
657 idx += 1;
658 }
659
660 Ok(options)
661}
662
663fn parse_size_arguments(rest: &[Value]) -> BuiltinResult<(usize, Option<Vec<usize>>)> {
664 let mut idx = 0;
665 let mut dims: Vec<usize> = Vec::new();
666 let mut vector_consumed = false;
667
668 while idx < rest.len() {
669 match &rest[idx] {
671 Value::String(_) | Value::StringArray(_) | Value::CharArray(_) => break,
672 _ => {}
673 }
674
675 match &rest[idx] {
676 Value::Int(i) => {
677 dims.push(int_to_dim(i)?);
678 }
679 Value::Num(n) => {
680 dims.push(float_to_dim(*n)?);
681 }
682 Value::Tensor(t) => {
683 if vector_consumed || !dims.is_empty() {
684 return Err(gpu_array_error_with_message(
685 "gpuArray: size vectors cannot be combined with scalar dimensions",
686 &GPUARRAY_ERROR_SIZE_ARGUMENT,
687 ));
688 }
689 dims = tensor_to_dims(t)?;
690 vector_consumed = true;
691 }
692 _ => break,
693 }
694 idx += 1;
695 }
696
697 let dims_option = if dims.is_empty() { None } else { Some(dims) };
698 Ok((idx, dims_option))
699}
700
701fn value_to_lower_string(value: &Value) -> Option<String> {
702 crate::builtins::common::tensor::value_to_string(value).map(|s| s.trim().to_ascii_lowercase())
703}
704
705fn int_to_dim(value: &IntValue) -> BuiltinResult<usize> {
706 value.try_to_usize().ok_or_else(|| {
707 gpu_array_error_with_message(
708 "gpuArray: size arguments must be non-negative integers",
709 &GPUARRAY_ERROR_SIZE_ARGUMENT,
710 )
711 })
712}
713
714fn float_to_dim(value: f64) -> BuiltinResult<usize> {
715 if !value.is_finite() {
716 return Err(gpu_array_error_with_message(
717 "gpuArray: size arguments must be finite integers",
718 &GPUARRAY_ERROR_SIZE_ARGUMENT,
719 ));
720 }
721 let rounded = value.round();
722 if (rounded - value).abs() > f64::EPSILON {
723 return Err(gpu_array_error_with_message(
724 "gpuArray: size arguments must be integers",
725 &GPUARRAY_ERROR_SIZE_ARGUMENT,
726 ));
727 }
728 if rounded < 0.0 {
729 return Err(gpu_array_error_with_message(
730 "gpuArray: size arguments must be non-negative",
731 &GPUARRAY_ERROR_SIZE_ARGUMENT,
732 ));
733 }
734 if rounded > usize::MAX as f64 || (usize::BITS == 64 && rounded == usize::MAX as f64) {
735 return Err(gpu_array_error_with_message(
736 "gpuArray: size arguments exceed maximum supported size",
737 &GPUARRAY_ERROR_SIZE_ARGUMENT,
738 ));
739 }
740 Ok(rounded as usize)
741}
742
743fn tensor_to_dims(tensor: &Tensor) -> BuiltinResult<Vec<usize>> {
744 if let Some(storage) = tensor.integer_storage() {
745 return storage
746 .exact_values()
747 .iter()
748 .map(int_to_dim)
749 .collect::<BuiltinResult<Vec<_>>>();
750 }
751
752 let values = tensor::tensor_values_f64_cow(tensor);
753 let mut dims = Vec::with_capacity(values.len());
754 for value in values.iter() {
755 dims.push(float_to_dim(*value)?);
756 }
757 Ok(dims)
758}
759
760fn resolve_dtype(value: &Value, options: &ParsedOptions) -> BuiltinResult<DataClass> {
761 if let Some(explicit) = options.explicit_dtype {
762 return Ok(explicit);
763 }
764 if let Some(prototype) = options.prototype.as_ref() {
765 return infer_dtype_from_prototype(prototype);
766 }
767 if let Value::GpuTensor(handle) = value {
768 return dtype_from_gpu_handle(handle);
769 }
770 if let Value::Int(value) = value {
771 return Ok(data_class_from_int_value(value));
772 }
773 if let Value::Tensor(tensor) = value {
774 return Ok(data_class_from_numeric_dtype(tensor.numeric_dtype()));
775 }
776 if let Value::ComplexTensor(tensor) = value {
777 return Ok(data_class_from_numeric_dtype(tensor.numeric_dtype()));
778 }
779 if value_defaults_to_logical(value) {
780 return Ok(DataClass::Logical);
781 }
782 Ok(DataClass::Double)
783}
784
785fn data_class_from_int_value(value: &IntValue) -> DataClass {
786 match value {
787 IntValue::I8(_) => DataClass::Int8,
788 IntValue::I16(_) => DataClass::Int16,
789 IntValue::I32(_) => DataClass::Int32,
790 IntValue::I64(_) => DataClass::Int64,
791 IntValue::U8(_) => DataClass::UInt8,
792 IntValue::U16(_) => DataClass::UInt16,
793 IntValue::U32(_) => DataClass::UInt32,
794 IntValue::U64(_) => DataClass::UInt64,
795 }
796}
797
798fn data_class_from_numeric_dtype(dtype: NumericDType) -> DataClass {
799 match dtype {
800 NumericDType::F64 => DataClass::Double,
801 NumericDType::F32 => DataClass::Single,
802 NumericDType::I8 => DataClass::Int8,
803 NumericDType::I16 => DataClass::Int16,
804 NumericDType::I32 => DataClass::Int32,
805 NumericDType::I64 => DataClass::Int64,
806 NumericDType::U8 => DataClass::UInt8,
807 NumericDType::U16 => DataClass::UInt16,
808 NumericDType::U32 => DataClass::UInt32,
809 NumericDType::U64 => DataClass::UInt64,
810 }
811}
812
813fn integer_prototype(dtype: DataClass) -> Option<IntegerStorage> {
814 Some(match dtype {
815 DataClass::Int8 => IntegerStorage::I8(Vec::new()),
816 DataClass::Int16 => IntegerStorage::I16(Vec::new()),
817 DataClass::Int32 => IntegerStorage::I32(Vec::new()),
818 DataClass::Int64 => IntegerStorage::I64(Vec::new()),
819 DataClass::UInt8 => IntegerStorage::U8(Vec::new()),
820 DataClass::UInt16 => IntegerStorage::U16(Vec::new()),
821 DataClass::UInt32 => IntegerStorage::U32(Vec::new()),
822 DataClass::UInt64 => IntegerStorage::U64(Vec::new()),
823 _ => return None,
824 })
825}
826
827fn infer_dtype_from_prototype(proto: &Value) -> BuiltinResult<DataClass> {
828 match proto {
829 Value::GpuTensor(handle) => dtype_from_gpu_handle(handle),
830 Value::LogicalArray(_) | Value::Bool(_) => Ok(DataClass::Logical),
831 Value::Int(int) => Ok(match int {
832 IntValue::I8(_) => DataClass::Int8,
833 IntValue::I16(_) => DataClass::Int16,
834 IntValue::I32(_) => DataClass::Int32,
835 IntValue::I64(_) => DataClass::Int64,
836 IntValue::U8(_) => DataClass::UInt8,
837 IntValue::U16(_) => DataClass::UInt16,
838 IntValue::U32(_) => DataClass::UInt32,
839 IntValue::U64(_) => DataClass::UInt64,
840 }),
841 Value::Tensor(tensor) => Ok(data_class_from_numeric_dtype(tensor.numeric_dtype())),
842 Value::Num(_) => Ok(DataClass::Double),
843 Value::CharArray(_) => Ok(DataClass::Double),
844 Value::String(_) => Err(gpu_array_error_with_message(
845 "gpuArray: 'like' does not accept MATLAB string scalars; convert to char() first",
846 &GPUARRAY_ERROR_LIKE_PROTOTYPE,
847 )),
848 Value::StringArray(_) => Err(gpu_array_error_with_message(
849 "gpuArray: 'like' does not accept string arrays; convert to char arrays first",
850 &GPUARRAY_ERROR_LIKE_PROTOTYPE,
851 )),
852 Value::Complex(_, _) => Ok(DataClass::Double),
853 Value::ComplexTensor(tensor) => {
854 Ok(data_class_from_numeric_dtype(tensor.numeric_dtype()))
855 }
856 other => Err(gpu_array_error_with_message(
857 format!(
858 "gpuArray: unsupported 'like' prototype type {other:?}; expected numeric or logical values"
859 ),
860 &GPUARRAY_ERROR_LIKE_PROTOTYPE,
861 )),
862 }
863}
864
865fn dtype_from_gpu_handle(handle: &GpuTensorHandle) -> BuiltinResult<DataClass> {
866 if runmat_accelerate_api::handle_is_logical(handle) {
867 return Ok(DataClass::Logical);
868 }
869 if let Some(class_name) = runmat_accelerate_api::handle_class_name(handle) {
870 if let Some(dtype) = DataClass::from_tag(class_name.trim().to_ascii_lowercase().as_str()) {
871 return Ok(dtype);
872 }
873 }
874 let precision = runmat_accelerate_api::handle_precision(handle).or_else(|| {
875 runmat_accelerate_api::provider_for_handle(handle).map(|provider| provider.precision())
876 });
877 Ok(match precision {
878 Some(ProviderPrecision::F32) => DataClass::Single,
879 Some(ProviderPrecision::F64) | None => DataClass::Double,
880 })
881}
882
883fn value_defaults_to_logical(value: &Value) -> bool {
884 match value {
885 Value::LogicalArray(_) | Value::Bool(_) => true,
886 Value::GpuTensor(handle) => runmat_accelerate_api::handle_is_logical(handle),
887 _ => false,
888 }
889}
890
891struct PreparedHandle {
892 handle: GpuTensorHandle,
893 provider: &'static dyn runmat_accelerate_api::AccelProvider,
894 logical: bool,
895 storage: runmat_accelerate_api::GpuTensorStorage,
896 owns_handle: bool,
897}
898
899fn upload_host_value(value: Value, dtype: DataClass) -> BuiltinResult<PreparedHandle> {
900 #[cfg(all(test, feature = "wgpu"))]
901 {
902 if runmat_accelerate_api::provider().is_none() {
903 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
904 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
905 );
906 }
907 }
908 let provider = runmat_accelerate_api::provider()
909 .ok_or_else(|| gpu_array_error(&GPUARRAY_ERROR_NO_PROVIDER))?;
910
911 match value {
912 Value::Complex(re, im) => {
913 let tensor = ComplexTensor::new(vec![(re, im)], vec![1, 1]).map_err(|err| {
914 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
915 })?;
916 upload_complex_host_value(provider, tensor, dtype)
917 }
918 Value::ComplexTensor(tensor) => upload_complex_host_value(provider, tensor, dtype),
919 value => upload_real_host_value(provider, value, dtype),
920 }
921}
922
923fn upload_real_host_value(
924 provider: &'static dyn runmat_accelerate_api::AccelProvider,
925 value: Value,
926 dtype: DataClass,
927) -> BuiltinResult<PreparedHandle> {
928 if dtype.is_integer() {
929 let (storage, shape) = match value {
930 Value::Int(value) => {
931 let source = IntegerStorage::from_scalar(value);
932 (cast_integer_storage(&source, dtype)?, vec![1, 1])
933 }
934 Value::Tensor(tensor) => {
935 let shape = tensor.shape.clone();
936 let storage = if let Some(storage) = tensor.integer_storage().cloned() {
937 cast_integer_storage(&storage, dtype)?
938 } else {
939 let values = tensor::tensor_into_values_f64(tensor);
940 Tensor::new_with_dtype(values, shape.clone(), dtype.numeric_dtype())
941 .map_err(|err| {
942 gpu_array_error_with_message(
943 format!("gpuArray: {err}"),
944 &GPUARRAY_ERROR_CONVERSION,
945 )
946 })?
947 .integer_storage()
948 .expect("integer dtype constructs integer storage")
949 .clone()
950 };
951 (storage, shape)
952 }
953 other => {
954 let tensor = coerce_host_value(other)?;
955 let shape = tensor.shape.clone();
956 let values = tensor::tensor_into_values_f64(tensor);
957 let storage = Tensor::new_with_dtype(values, shape.clone(), dtype.numeric_dtype())
958 .map_err(|err| {
959 gpu_array_error_with_message(
960 format!("gpuArray: {err}"),
961 &GPUARRAY_ERROR_CONVERSION,
962 )
963 })?
964 .integer_storage()
965 .expect("integer dtype constructs integer storage")
966 .clone();
967 (storage, shape)
968 }
969 };
970 let tensor = Tensor::new_integer(storage, shape).map_err(|err| {
971 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
972 })?;
973 let handle = gpu_helpers::upload_tensor(provider, &tensor).map_err(|err| {
974 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_PROVIDER_IO)
975 })?;
976 return Ok(PreparedHandle {
977 handle,
978 provider,
979 logical: false,
980 storage: runmat_accelerate_api::GpuTensorStorage::Real,
981 owns_handle: true,
982 });
983 }
984 let tensor = coerce_host_value(value)?;
985 let (tensor, logical) = cast_tensor(tensor, dtype)?;
986 let tensor = physical_logical_tensor(tensor, logical, provider.precision())?;
987 let new_handle = gpu_helpers::upload_tensor(provider, &tensor).map_err(|err| {
988 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_PROVIDER_IO)
989 })?;
990 if logical {
991 runmat_accelerate_api::set_handle_logical(&new_handle, true);
992 }
993
994 Ok(PreparedHandle {
995 handle: new_handle,
996 provider,
997 logical,
998 storage: runmat_accelerate_api::GpuTensorStorage::Real,
999 owns_handle: true,
1000 })
1001}
1002
1003fn cast_integer_storage(
1004 source: &IntegerStorage,
1005 dtype: DataClass,
1006) -> BuiltinResult<IntegerStorage> {
1007 let Some(prototype) = integer_prototype(dtype) else {
1008 return Err(gpu_array_error(&GPUARRAY_ERROR_TYPED_INTEGER));
1009 };
1010 let values = source
1011 .exact_values()
1012 .into_iter()
1013 .map(|value| prototype.cast_exact_assignment(&value))
1014 .collect();
1015 prototype.from_same_class_values(values).map_err(|err| {
1016 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
1017 })
1018}
1019
1020fn upload_complex_host_value(
1021 provider: &'static dyn runmat_accelerate_api::AccelProvider,
1022 tensor: ComplexTensor,
1023 dtype: DataClass,
1024) -> BuiltinResult<PreparedHandle> {
1025 let shape = tensor.shape.clone();
1026 let storage = tensor.into_complex_storage();
1027 let storage = match dtype {
1028 DataClass::Double => match storage {
1029 storage @ ComplexStorage::F64(_) => storage,
1030 storage => ComplexStorage::F64(storage.materialize_f64()),
1031 },
1032 DataClass::Single => match storage {
1033 storage @ ComplexStorage::F32(_) => storage,
1034 storage => ComplexStorage::F32(
1035 storage
1036 .materialize_f64()
1037 .into_iter()
1038 .map(|(real, imag)| (real as f32, imag as f32))
1039 .collect(),
1040 ),
1041 },
1042 dtype @ (DataClass::Int8
1043 | DataClass::Int16
1044 | DataClass::Int32
1045 | DataClass::Int64
1046 | DataClass::UInt8
1047 | DataClass::UInt16
1048 | DataClass::UInt32
1049 | DataClass::UInt64) => {
1050 let ComplexStorage::Integer(storage) = storage else {
1051 return Err(gpu_array_error_with_message(
1052 "gpuArray: converting floating complex input to an integer class is not supported",
1053 &GPUARRAY_ERROR_INPUT_TYPE,
1054 ));
1055 };
1056 ComplexStorage::Integer(
1057 IntegerComplexStorage::new(
1058 cast_integer_storage(&storage.real, dtype)?,
1059 cast_integer_storage(&storage.imag, dtype)?,
1060 )
1061 .map_err(|error| gpu_array_error_with_message(error, &GPUARRAY_ERROR_CONVERSION))?,
1062 )
1063 }
1064 DataClass::Logical => {
1065 return Err(gpu_array_error_with_message(
1066 "gpuArray: complex inputs cannot be uploaded as logical storage",
1067 &GPUARRAY_ERROR_INPUT_TYPE,
1068 ));
1069 }
1070 };
1071 let tensor = ComplexTensor::from_complex_storage(storage, shape)
1072 .map_err(|error| gpu_array_error_with_message(error, &GPUARRAY_ERROR_INPUT_TYPE))?;
1073
1074 let handle = gpu_helpers::upload_complex_tensor(provider, &tensor).map_err(|err| {
1075 gpu_array_error_with_message(err.to_string(), &GPUARRAY_ERROR_PROVIDER_IO)
1076 })?;
1077 Ok(PreparedHandle {
1078 handle,
1079 provider,
1080 logical: false,
1081 storage: runmat_accelerate_api::GpuTensorStorage::ComplexInterleaved,
1082 owns_handle: true,
1083 })
1084}
1085
1086async fn convert_device_value(
1087 handle: GpuTensorHandle,
1088 dtype: DataClass,
1089) -> BuiltinResult<PreparedHandle> {
1090 let provider = runmat_accelerate_api::provider_for_handle(&handle)
1091 .ok_or_else(|| gpu_array_error(&GPUARRAY_ERROR_NO_PROVIDER))?;
1092 let was_logical = runmat_accelerate_api::handle_is_logical(&handle);
1093 let was_complex = runmat_accelerate_api::handle_storage(&handle)
1094 == runmat_accelerate_api::GpuTensorStorage::ComplexInterleaved;
1095 let current_precision = runmat_accelerate_api::handle_precision(&handle);
1096 let integer_type = runmat_accelerate_api::handle_integer_type(&handle);
1097 match dtype {
1098 DataClass::Double => {
1099 if !was_logical
1100 && integer_type.is_none()
1101 && current_precision == Some(runmat_accelerate_api::ProviderPrecision::F64)
1102 {
1103 return Ok(PreparedHandle {
1104 handle,
1105 provider,
1106 logical: false,
1107 storage: if was_complex {
1108 runmat_accelerate_api::GpuTensorStorage::ComplexInterleaved
1109 } else {
1110 runmat_accelerate_api::GpuTensorStorage::Real
1111 },
1112 owns_handle: false,
1113 });
1114 }
1115 }
1116 DataClass::Logical => {
1117 if was_logical {
1118 return Ok(PreparedHandle {
1119 handle,
1120 provider,
1121 logical: true,
1122 storage: runmat_accelerate_api::GpuTensorStorage::Real,
1123 owns_handle: false,
1124 });
1125 }
1126 }
1127 dtype if dtype.is_integer() => {
1128 if integer_type.is_some()
1129 && dtype_from_gpu_handle(&handle)
1130 .ok()
1131 .is_some_and(|current| current == dtype)
1132 {
1133 return Ok(PreparedHandle {
1134 handle,
1135 provider,
1136 logical: false,
1137 storage: if was_complex {
1138 runmat_accelerate_api::GpuTensorStorage::ComplexInterleaved
1139 } else {
1140 runmat_accelerate_api::GpuTensorStorage::Real
1141 },
1142 owns_handle: false,
1143 });
1144 }
1145 }
1146 _ => {}
1147 }
1148
1149 if was_complex {
1150 let gathered = gpu_helpers::gather_value_async(&Value::GpuTensor(handle.clone()))
1151 .await
1152 .map_err(|err| {
1153 gpu_array_error_with_message(err.to_string(), &GPUARRAY_ERROR_PROVIDER_IO)
1154 })?;
1155 let Value::ComplexTensor(tensor) = gathered else {
1156 return Err(gpu_array_error_with_message(
1157 "gpuArray: expected complex gpuArray data during conversion",
1158 &GPUARRAY_ERROR_PROVIDER_IO,
1159 ));
1160 };
1161 let prepared = upload_complex_host_value(provider, tensor, dtype)?;
1162 return Ok(prepared);
1163 }
1164
1165 if let Some(integer_type) = integer_type.filter(|_| dtype.is_integer()) {
1166 let downloaded = provider.download_integer(&handle).await.map_err(|err| {
1167 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_PROVIDER_IO)
1168 })?;
1169 if downloaded.shape != handle.shape || downloaded.data.element_type() != integer_type {
1170 return Err(gpu_array_error_with_message(
1171 "gpuArray: provider returned contradictory integer payload metadata",
1172 &GPUARRAY_ERROR_PROVIDER_IO,
1173 ));
1174 }
1175 let shape = downloaded.shape;
1176 let storage = gpu_helpers::integer_storage_from_owned(downloaded.data);
1177 let cast = cast_integer_storage(&storage, dtype)?;
1178 let tensor = Tensor::new_integer(cast, shape).map_err(|err| {
1179 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
1180 })?;
1181 let new_handle = gpu_helpers::upload_tensor(provider, &tensor).map_err(|err| {
1182 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_PROVIDER_IO)
1183 })?;
1184 return Ok(PreparedHandle {
1185 handle: new_handle,
1186 provider,
1187 logical: false,
1188 storage: runmat_accelerate_api::GpuTensorStorage::Real,
1189 owns_handle: true,
1190 });
1191 }
1192
1193 let tensor = gpu_helpers::gather_tensor_async(&handle)
1194 .await
1195 .map_err(|err| {
1196 gpu_array_error_with_message(err.to_string(), &GPUARRAY_ERROR_PROVIDER_IO)
1197 })?;
1198 let (tensor, logical) = cast_tensor(tensor, dtype)?;
1199 let tensor = physical_logical_tensor(tensor, logical, provider.precision())?;
1200 let new_handle = gpu_helpers::upload_tensor(provider, &tensor).map_err(|err| {
1201 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_PROVIDER_IO)
1202 })?;
1203 if logical {
1204 runmat_accelerate_api::set_handle_logical(&new_handle, true);
1205 }
1206
1207 Ok(PreparedHandle {
1208 handle: new_handle,
1209 provider,
1210 logical,
1211 storage: runmat_accelerate_api::GpuTensorStorage::Real,
1212 owns_handle: true,
1213 })
1214}
1215
1216fn validate_prepared_handle(
1217 handle: &GpuTensorHandle,
1218 provider: &'static dyn runmat_accelerate_api::AccelProvider,
1219 expected_shape: &[usize],
1220 expected_storage: runmat_accelerate_api::GpuTensorStorage,
1221 dtype: DataClass,
1222 logical: bool,
1223) -> BuiltinResult<()> {
1224 let expected_element = match dtype {
1225 DataClass::Double => runmat_accelerate_api::NumericElementType::F64,
1226 DataClass::Single => runmat_accelerate_api::NumericElementType::F32,
1227 DataClass::Logical => match provider.precision() {
1228 ProviderPrecision::F32 => runmat_accelerate_api::NumericElementType::F32,
1229 ProviderPrecision::F64 => runmat_accelerate_api::NumericElementType::F64,
1230 },
1231 DataClass::Int8 => runmat_accelerate_api::NumericElementType::I8,
1232 DataClass::Int16 => runmat_accelerate_api::NumericElementType::I16,
1233 DataClass::Int32 => runmat_accelerate_api::NumericElementType::I32,
1234 DataClass::Int64 => runmat_accelerate_api::NumericElementType::I64,
1235 DataClass::UInt8 => runmat_accelerate_api::NumericElementType::U8,
1236 DataClass::UInt16 => runmat_accelerate_api::NumericElementType::U16,
1237 DataClass::UInt32 => runmat_accelerate_api::NumericElementType::U32,
1238 DataClass::UInt64 => runmat_accelerate_api::NumericElementType::U64,
1239 };
1240 let owner_matches = runmat_accelerate_api::provider_for_handle(handle)
1241 .is_some_and(|owner| std::ptr::eq(owner, provider));
1242 if !owner_matches
1243 || handle.device_id != provider.device_id()
1244 || handle.shape != expected_shape
1245 || handle.descriptor.storage != Some(expected_storage)
1246 || handle.descriptor.element_type != Some(expected_element)
1247 {
1248 return Err(gpu_array_error_with_message(
1249 "gpuArray: provider returned a handle with the wrong owner or shape",
1250 &GPUARRAY_ERROR_PROVIDER_IO,
1251 ));
1252 }
1253 let existing_class = runmat_accelerate_api::handle_class_name(handle);
1254 if (runmat_accelerate_api::handle_is_logical(handle) && !logical)
1255 || existing_class
1256 .as_deref()
1257 .is_some_and(|class_name| class_name != dtype.class_name())
1258 {
1259 return Err(gpu_array_error_with_message(
1260 "gpuArray: provider returned contradictory class metadata",
1261 &GPUARRAY_ERROR_PROVIDER_IO,
1262 ));
1263 }
1264 Ok(())
1265}
1266
1267fn coerce_host_value(value: Value) -> BuiltinResult<Tensor> {
1268 match value {
1269 Value::Tensor(t) => Ok(t),
1270 Value::LogicalArray(logical) => tensor::logical_to_tensor(&logical).map_err(|err| {
1271 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
1272 }),
1273 Value::Bool(flag) => {
1274 Tensor::new(vec![if flag { 1.0 } else { 0.0 }], vec![1, 1]).map_err(|err| {
1275 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
1276 })
1277 }
1278 Value::Num(n) => Tensor::new(vec![n], vec![1, 1]).map_err(|err| {
1279 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
1280 }),
1281 Value::Int(i) => Tensor::new(vec![i.to_f64()], vec![1, 1]).map_err(|err| {
1282 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
1283 }),
1284 Value::CharArray(ca) => char_array_to_tensor(&ca),
1285 Value::String(text) => {
1286 let ca = CharArray::new_row(&text);
1287 char_array_to_tensor(&ca)
1288 }
1289 Value::StringArray(_) => Err(gpu_array_error_with_message(
1290 "gpuArray: string arrays are not supported yet; convert to char arrays with CHAR first",
1291 &GPUARRAY_ERROR_INPUT_TYPE,
1292 )),
1293 Value::Complex(_, _) | Value::ComplexTensor(_) => Err(gpu_array_error_with_message(
1294 "gpuArray: internal complex upload routing failed",
1295 &GPUARRAY_ERROR_INTERNAL,
1296 )),
1297 other => Err(gpu_array_error_with_detail(
1298 &GPUARRAY_ERROR_INPUT_TYPE,
1299 format!("unsupported input type for GPU transfer: {other:?}"),
1300 )),
1301 }
1302}
1303
1304fn physical_logical_tensor(
1305 tensor: Tensor,
1306 logical: bool,
1307 precision: ProviderPrecision,
1308) -> BuiltinResult<Tensor> {
1309 if !logical || precision == ProviderPrecision::F64 {
1310 return Ok(tensor);
1311 }
1312 let shape = tensor.shape.clone();
1313 let values = tensor
1314 .into_numeric_storage()
1315 .map(numeric_storage_into_f64)
1316 .map_err(|err| {
1317 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
1318 })?;
1319 Tensor::from_f32(
1320 values.into_iter().map(|value| value as f32).collect(),
1321 shape,
1322 )
1323 .map_err(|err| {
1324 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
1325 })
1326}
1327
1328fn cast_tensor(tensor: Tensor, dtype: DataClass) -> BuiltinResult<(Tensor, bool)> {
1329 let shape = tensor.shape.clone();
1334 let storage = tensor.into_numeric_storage().map_err(|err| {
1335 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
1336 })?;
1337 let tensor = match dtype {
1338 DataClass::Logical => {
1339 let mut values = numeric_storage_into_f64(storage);
1340 convert_to_logical(&mut values)?;
1341 Tensor::new(values, shape).map_err(|err| {
1342 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
1343 })?
1344 }
1345 DataClass::Single => {
1346 let storage = match storage {
1347 NumericStorage::F32(values) => NumericStorage::F32(values),
1348 storage => {
1349 let mut values = numeric_storage_into_f64(storage);
1350 convert_to_single(&mut values);
1351 NumericStorage::F32(values.into_iter().map(|value| value as f32).collect())
1352 }
1353 };
1354 Tensor::from_numeric_storage(storage, shape).map_err(|err| {
1355 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
1356 })?
1357 }
1358 DataClass::Double => {
1359 let values = numeric_storage_into_f64(storage);
1360 Tensor::new(values, shape).map_err(|err| {
1361 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_CONVERSION)
1362 })?
1363 }
1364 dtype => {
1365 debug_assert!(dtype.is_integer());
1366 return Err(gpu_array_error_with_message(
1367 format!(
1368 "gpuArray: internal integer destination {} reached floating upload path",
1369 dtype.class_name()
1370 ),
1371 &GPUARRAY_ERROR_INTERNAL,
1372 ));
1373 }
1374 };
1375
1376 Ok((tensor, dtype == DataClass::Logical))
1377}
1378
1379fn numeric_storage_into_f64(storage: NumericStorage) -> Vec<f64> {
1380 match storage {
1381 NumericStorage::F64(values) => values,
1382 NumericStorage::F32(values) => values.into_iter().map(f64::from).collect(),
1383 NumericStorage::I8(values) => values.into_iter().map(|value| value as f64).collect(),
1384 NumericStorage::I16(values) => values.into_iter().map(|value| value as f64).collect(),
1385 NumericStorage::I32(values) => values.into_iter().map(|value| value as f64).collect(),
1386 NumericStorage::I64(values) => values.into_iter().map(|value| value as f64).collect(),
1387 NumericStorage::U8(values) => values.into_iter().map(|value| value as f64).collect(),
1388 NumericStorage::U16(values) => values.into_iter().map(|value| value as f64).collect(),
1389 NumericStorage::U32(values) => values.into_iter().map(|value| value as f64).collect(),
1390 NumericStorage::U64(values) => values.into_iter().map(|value| value as f64).collect(),
1391 }
1392}
1393
1394fn convert_to_logical(data: &mut [f64]) -> BuiltinResult<()> {
1395 for value in data.iter_mut() {
1396 if value.is_nan() {
1397 return Err(gpu_array_error_with_message(
1398 "gpuArray: cannot convert NaN to logical",
1399 &GPUARRAY_ERROR_CONVERSION,
1400 ));
1401 }
1402 *value = if *value != 0.0 { 1.0 } else { 0.0 };
1403 }
1404 Ok(())
1405}
1406
1407fn convert_to_single(data: &mut [f64]) {
1408 for value in data.iter_mut() {
1409 *value = (*value as f32) as f64;
1410 }
1411}
1412
1413fn apply_dims(handle: &mut GpuTensorHandle, dims: &[usize]) -> BuiltinResult<()> {
1414 let new_elems: usize = dims.iter().product();
1415 let current_elems: usize = if handle.shape.is_empty() {
1416 new_elems
1417 } else {
1418 handle.shape.iter().product()
1419 };
1420 if new_elems != current_elems {
1421 return Err(gpu_array_error_with_message(
1422 format!(
1423 "gpuArray: cannot reshape gpuArray of {current_elems} elements into size {:?}",
1424 dims
1425 ),
1426 &GPUARRAY_ERROR_RESHAPE,
1427 ));
1428 }
1429 handle.shape = dims.to_vec();
1430 Ok(())
1431}
1432
1433fn char_array_to_tensor(ca: &CharArray) -> BuiltinResult<Tensor> {
1434 let rows = ca.rows;
1435 let cols = ca.cols;
1436 if rows == 0 || cols == 0 {
1437 return Tensor::new(Vec::new(), vec![rows, cols]).map_err(|err| {
1438 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
1439 });
1440 }
1441 let mut data = vec![0.0; rows * cols];
1442 for row in 0..rows {
1444 for col in 0..cols {
1445 let idx_char = row * cols + col;
1446 let ch = ca.data[idx_char];
1447 data[row * cols + col] = ch as u32 as f64;
1448 }
1449 }
1450 Tensor::new(data, vec![rows, cols]).map_err(|err| {
1451 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
1452 })
1453}
1454
1455#[cfg(test)]
1456pub(crate) mod tests {
1457 use super::*;
1458 use crate::builtins::common::test_support;
1459 use futures::executor::block_on;
1460 use runmat_accelerate_api::{GpuTensorStorage, HostTensorView};
1461 use runmat_builtins::{ResolveContext, Type};
1462 use runmat_value::{ComplexTensor, IntegerComplexStorage, IntegerStorage, LogicalArray};
1463
1464 fn call(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
1465 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1466 block_on(gpu_array_builtin(value, rest))
1467 }
1468
1469 fn call_with_mode(
1470 value: Value,
1471 rest: Vec<Value>,
1472 extensions_enabled: bool,
1473 ) -> crate::BuiltinResult<Value> {
1474 let _compat = crate::compatibility::push_runmat_extensions_enabled(extensions_enabled);
1475 block_on(gpu_array_builtin(value, rest))
1476 }
1477
1478 fn gather_complex(value: Value) -> ComplexTensor {
1479 match block_on(crate::dispatcher::gather_if_needed_async(&value)).expect("gather complex") {
1480 Value::ComplexTensor(tensor) => tensor,
1481 other => panic!("expected ComplexTensor, got {other:?}"),
1482 }
1483 }
1484
1485 fn assert_complex_close(actual: &[(f64, f64)], expected: &[(f64, f64)]) {
1486 assert_eq!(actual.len(), expected.len());
1487 for (idx, ((ar, ai), (er, ei))) in actual.iter().zip(expected.iter()).enumerate() {
1488 assert!(
1489 (ar - er).abs() < 1e-12 && (ai - ei).abs() < 1e-12,
1490 "at {idx}: expected ({er}, {ei}), got ({ar}, {ai})"
1491 );
1492 }
1493 }
1494
1495 #[test]
1496 fn gpu_array_rejects_handle_mislabeled_as_another_registered_provider() {
1497 use runmat_accelerate_api::AccelProvider as _;
1498
1499 let _guard = test_support::accel_test_lock();
1500 let producer = Box::leak(Box::new(
1501 runmat_accelerate::simple_provider::InProcessProvider::new(),
1502 ));
1503 let mislabeled_owner = Box::leak(Box::new(
1504 runmat_accelerate::simple_provider::InProcessProvider::new(),
1505 ));
1506 unsafe {
1507 runmat_accelerate_api::register_provider(producer);
1508 runmat_accelerate_api::register_provider(mislabeled_owner);
1509 }
1510 let handle = GpuTensorHandle {
1511 shape: vec![1, 1],
1512 device_id: mislabeled_owner.device_id(),
1513 buffer_id: 991,
1514 descriptor: Default::default(),
1515 };
1516
1517 let error = validate_prepared_handle(
1518 &handle,
1519 producer,
1520 &[1, 1],
1521 GpuTensorStorage::Real,
1522 DataClass::Double,
1523 false,
1524 )
1525 .expect_err("a result must be owned by the producing provider");
1526
1527 assert_eq!(error.identifier(), Some("RunMat:gpuArray:ProviderIO"));
1528 }
1529
1530 #[test]
1531 fn gpu_array_ignores_stale_floating_annotation_on_durable_integer_handle() {
1532 test_support::with_test_provider(|provider| {
1533 let values = [1_i32, 2_i32];
1534 let handle = provider
1535 .upload_integer(&HostIntegerTensorView {
1536 data: HostIntegerDataView::I32(&values),
1537 shape: &[1, 2],
1538 })
1539 .expect("integer upload");
1540 validate_prepared_handle(
1541 &handle,
1542 provider,
1543 &[1, 2],
1544 GpuTensorStorage::Real,
1545 DataClass::Int32,
1546 false,
1547 )
1548 .expect("durable integer descriptor takes precedence over a stale annotation");
1549 let _ = provider.free(&handle);
1550 });
1551 }
1552
1553 #[test]
1554 fn gpu_array_extra_construction_forms_follow_compatibility_mode() {
1555 test_support::with_test_provider(|_| {
1556 call_with_mode(Value::Num(1.0), Vec::new(), false)
1557 .expect("MATLAB mode accepts documented gpuArray(X)");
1558 });
1559 for (rest, identifier) in [
1560 (
1561 vec![Value::from(1i32), Value::from(1i32)],
1562 "RunMat:compatibility:GpuArraySizeExtension",
1563 ),
1564 (
1565 vec![Value::from("uint8")],
1566 "RunMat:compatibility:GpuArrayDtypeExtension",
1567 ),
1568 (
1569 vec![Value::from("like"), Value::Num(0.0)],
1570 "RunMat:compatibility:GpuArrayLikeExtension",
1571 ),
1572 ] {
1573 let error = call_with_mode(Value::Num(1.0), rest, false)
1574 .expect_err("MATLAB mode rejects extra gpuArray construction form");
1575 assert_eq!(error.identifier(), Some(identifier));
1576 }
1577 }
1578
1579 #[test]
1580 fn gpu_array_text_upload_is_gated() {
1581 test_support::with_test_provider(|_| {
1582 let chars = CharArray::new("ab".chars().collect(), 1, 2).expect("chars");
1583 let error = call_with_mode(Value::CharArray(chars), Vec::new(), false)
1584 .expect_err("char upload is a RunMat extension");
1585 assert_eq!(
1586 error.identifier(),
1587 Some("RunMat:compatibility:GpuArrayTextUploadExtension")
1588 );
1589
1590 let error = call_with_mode(Value::String("ab".into()), Vec::new(), false)
1591 .expect_err("string upload is a RunMat extension");
1592 assert_eq!(
1593 error.identifier(),
1594 Some("RunMat:compatibility:GpuArrayTextUploadExtension")
1595 );
1596 });
1597 }
1598
1599 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1600 #[test]
1601 fn gpu_array_transfers_numeric_tensor() {
1602 test_support::with_test_provider(|_| {
1603 let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
1604 let result = call(Value::Tensor(tensor.clone()), Vec::new()).expect("gpuArray upload");
1605 let Value::GpuTensor(handle) = result else {
1606 panic!("expected gpu tensor");
1607 };
1608 assert_eq!(handle.shape, tensor.shape);
1609 assert!(runmat_accelerate_api::handle_is_explicit(&handle));
1610 let gathered =
1611 test_support::gather(Value::GpuTensor(handle.clone())).expect("gather values");
1612 assert_eq!(gathered.shape, tensor.shape);
1613 assert_eq!(gathered.materialize_f64(), tensor.materialize_f64());
1614 });
1615 }
1616
1617 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1618 #[test]
1619 fn gpu_array_infers_and_preserves_native_single_tensor_class() {
1620 test_support::with_f32_test_provider(|_| {
1621 let tensor = Tensor::from_f32(vec![1.25, -3.5], vec![2, 1]).expect("single tensor");
1622 let result = call(Value::Tensor(tensor), Vec::new()).expect("gpuArray single upload");
1623 let Value::GpuTensor(handle) = result else {
1624 panic!("expected gpu tensor");
1625 };
1626 assert_eq!(
1627 runmat_accelerate_api::handle_class_name(&handle).as_deref(),
1628 Some("single")
1629 );
1630 assert_eq!(
1631 runmat_accelerate_api::handle_precision(&handle),
1632 Some(ProviderPrecision::F32)
1633 );
1634
1635 let gathered =
1636 test_support::gather(Value::GpuTensor(handle)).expect("gather native single");
1637 assert_eq!(gathered.numeric_dtype(), NumericDType::F32);
1638 assert_eq!(
1639 gathered.into_numeric_storage().expect("single storage"),
1640 NumericStorage::F32(vec![1.25, -3.5])
1641 );
1642
1643 let prototype =
1644 Tensor::from_f32(vec![0.0], vec![1, 1]).expect("single prototype tensor");
1645 let like_result = call(
1646 Value::Tensor(Tensor::new(vec![2.5], vec![1, 1]).expect("double input")),
1647 vec![Value::from("like"), Value::Tensor(prototype)],
1648 )
1649 .expect("gpuArray like single");
1650 let Value::GpuTensor(like_handle) = like_result else {
1651 panic!("expected like gpu tensor");
1652 };
1653 assert_eq!(
1654 runmat_accelerate_api::handle_class_name(&like_handle).as_deref(),
1655 Some("single")
1656 );
1657 assert_eq!(
1658 runmat_accelerate_api::handle_precision(&like_handle),
1659 Some(ProviderPrecision::F32)
1660 );
1661 });
1662 }
1663
1664 #[test]
1665 fn gpu_array_of_existing_single_handle_is_identity() {
1666 test_support::with_f32_test_provider(|_| {
1667 let uploaded = call(
1668 Value::Tensor(Tensor::from_f32(vec![1.25, -3.5], vec![2, 1]).unwrap()),
1669 Vec::new(),
1670 )
1671 .expect("single upload");
1672 let Value::GpuTensor(handle) = uploaded else {
1673 panic!("expected gpu tensor");
1674 };
1675 let identity =
1676 call(Value::GpuTensor(handle.clone()), Vec::new()).expect("gpuArray identity");
1677 let Value::GpuTensor(identity) = identity else {
1678 panic!("expected gpu tensor");
1679 };
1680 assert_eq!(
1681 runmat_accelerate_api::handle_identity(&identity),
1682 runmat_accelerate_api::handle_identity(&handle)
1683 );
1684 assert_eq!(
1685 identity.descriptor.provenance,
1686 Some(runmat_accelerate_api::GpuHandleProvenance::Explicit)
1687 );
1688 let gathered = test_support::gather(Value::GpuTensor(handle.clone()))
1689 .expect("source remains valid");
1690 assert_eq!(gathered.numeric_dtype(), NumericDType::F32);
1691 assert_eq!(gathered.materialize_f64(), vec![1.25, -3.5]);
1692 });
1693 }
1694
1695 #[test]
1696 fn direct_gpu_builtin_results_preserve_explicit_gpuarray_intent() {
1697 test_support::with_test_provider(|_| {
1698 let input = call(
1699 Value::Tensor(Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap()),
1700 Vec::new(),
1701 )
1702 .expect("explicit gpuArray");
1703 let output =
1704 crate::call_builtin("plus", &[input.clone(), input]).expect("direct provider plus");
1705 let Value::GpuTensor(handle) = &output else {
1706 panic!("expected resident plus output");
1707 };
1708 assert!(runmat_accelerate_api::handle_is_explicit(handle));
1709 assert_eq!(
1710 handle.descriptor.provenance,
1711 Some(runmat_accelerate_api::GpuHandleProvenance::Explicit)
1712 );
1713 assert_eq!(
1714 crate::call_builtin("isgpuarray", &[output]).expect("isgpuarray"),
1715 Value::Bool(true)
1716 );
1717 });
1718 }
1719
1720 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1721 #[test]
1722 fn gpu_array_rewraps_integer_handles_without_double_roundtrip() {
1723 test_support::with_test_provider(|_| {
1724 let tensor = Tensor::new_integer(
1725 IntegerStorage::U64(vec![9_223_372_036_854_775_808, u64::MAX]),
1726 vec![1, 2],
1727 )
1728 .expect("integer tensor");
1729 let uploaded =
1730 call(Value::Tensor(tensor), Vec::new()).expect("gpuArray integer upload");
1731 let Value::GpuTensor(handle) = uploaded else {
1732 panic!("expected integer gpu tensor");
1733 };
1734
1735 let rewrapped =
1736 call(Value::GpuTensor(handle.clone()), Vec::new()).expect("gpuArray rewrap");
1737 let Value::GpuTensor(rewrapped) = rewrapped else {
1738 panic!("expected rewrapped gpu tensor");
1739 };
1740 assert_eq!(rewrapped.buffer_id, handle.buffer_id);
1741 assert_eq!(
1742 runmat_accelerate_api::handle_class_name(&rewrapped).as_deref(),
1743 Some("uint64")
1744 );
1745
1746 let gathered =
1747 test_support::gather(Value::GpuTensor(rewrapped)).expect("gather integer");
1748 assert_eq!(
1749 gathered.integer_storage(),
1750 Some(&IntegerStorage::U64(vec![
1751 9_223_372_036_854_775_808,
1752 u64::MAX,
1753 ]))
1754 );
1755 });
1756 }
1757
1758 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1759 #[test]
1760 fn gpu_array_noninteger_class_uploads_read_typed_integer_storage_exactly() {
1761 test_support::with_test_provider(|_| {
1762 for (class_name, expected, logical) in [
1763 ("double", vec![2.0, 4.0], false),
1764 ("logical", vec![1.0, 1.0], true),
1765 ] {
1766 let tensor = Tensor::new_integer(IntegerStorage::I32(vec![2, 4]), vec![1, 2])
1767 .expect("integer tensor");
1768
1769 let uploaded = call(Value::Tensor(tensor), vec![Value::from(class_name)])
1770 .expect("gpuArray class upload");
1771 let Value::GpuTensor(handle) = uploaded else {
1772 panic!("expected gpu tensor for {class_name}");
1773 };
1774 assert_eq!(runmat_accelerate_api::handle_is_logical(&handle), logical);
1775 assert!(runmat_accelerate_api::handle_integer_type(&handle).is_none());
1776 let gathered =
1777 test_support::gather(Value::GpuTensor(handle)).expect("gather uploaded tensor");
1778 assert_eq!(gathered.shape, vec![1, 2]);
1779 assert_eq!(gathered.materialize_f64(), expected, "{class_name}");
1780 }
1781 });
1782 test_support::with_f32_test_provider(|_| {
1783 let tensor = Tensor::new_integer(IntegerStorage::I32(vec![2, 4]), vec![1, 2])
1784 .expect("integer tensor");
1785 let uploaded = call(Value::Tensor(tensor), vec![Value::from("single")])
1786 .expect("gpuArray single upload");
1787 let Value::GpuTensor(handle) = uploaded else {
1788 panic!("expected single gpu tensor");
1789 };
1790 let gathered = test_support::gather(Value::GpuTensor(handle)).expect("gather single");
1791 assert_eq!(gathered.numeric_dtype(), NumericDType::F32);
1792 assert_eq!(gathered.materialize_f64(), vec![2.0, 4.0]);
1793 });
1794 }
1795
1796 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1797 #[test]
1798 fn gpu_array_integer_handle_conversion_uses_integer_buffers() {
1799 test_support::with_test_provider(|_| {
1800 let tensor = Tensor::new_integer(
1801 IntegerStorage::U64(vec![0, 9_223_372_036_854_775_808, u64::MAX]),
1802 vec![3, 1],
1803 )
1804 .expect("integer tensor");
1805 let uploaded =
1806 call(Value::Tensor(tensor), Vec::new()).expect("gpuArray integer upload");
1807 let Value::GpuTensor(handle) = uploaded else {
1808 panic!("expected integer gpu tensor");
1809 };
1810
1811 let converted = call(Value::GpuTensor(handle.clone()), vec![Value::from("int16")])
1812 .expect("gpuArray int16 conversion");
1813 let Value::GpuTensor(converted) = converted else {
1814 panic!("expected converted gpu tensor");
1815 };
1816 assert_eq!(
1817 runmat_accelerate_api::handle_class_name(&converted).as_deref(),
1818 Some("int16")
1819 );
1820 let gathered =
1821 test_support::gather(Value::GpuTensor(converted)).expect("gather converted");
1822 assert_eq!(
1823 gathered.integer_storage(),
1824 Some(&IntegerStorage::I16(vec![0, i16::MAX, i16::MAX]))
1825 );
1826 let original =
1827 test_support::gather(Value::GpuTensor(handle)).expect("source remains valid");
1828 assert_eq!(
1829 original.integer_storage(),
1830 Some(&IntegerStorage::U64(vec![
1831 0,
1832 9_223_372_036_854_775_808,
1833 u64::MAX,
1834 ]))
1835 );
1836 });
1837 }
1838
1839 #[test]
1840 fn gpu_array_dimension_preserves_representable_uint64_values() {
1841 let expected = usize::try_from(u64::MAX).ok();
1842 assert_eq!(int_to_dim(&IntValue::U64(u64::MAX)).ok(), expected);
1843 assert!(int_to_dim(&IntValue::I64(-1)).is_err());
1844 }
1845
1846 #[test]
1847 fn gpu_array_tensor_dimensions_use_exact_integer_storage() {
1848 let expected = usize::try_from(u64::MAX).ok();
1849 let dims = Tensor::new_integer(IntegerStorage::U64(vec![u64::MAX]), vec![1, 1])
1850 .expect("integer size vector");
1851 assert_eq!(tensor_to_dims(&dims).ok(), expected.map(|dim| vec![dim]));
1852
1853 let negative = Tensor::new_integer(IntegerStorage::I8(vec![-1]), vec![1, 1])
1854 .expect("integer size vector");
1855 assert!(tensor_to_dims(&negative).is_err());
1856 }
1857
1858 #[test]
1859 fn gpu_array_float_dimensions_reject_unrepresentable_usize_boundary() {
1860 let boundary = if usize::BITS == 64 {
1861 usize::MAX as f64
1862 } else {
1863 (usize::MAX as f64) + 1.0
1864 };
1865
1866 assert!(float_to_dim(boundary).is_err());
1867 }
1868
1869 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1870 #[test]
1871 fn gpu_array_marks_logical_inputs() {
1872 test_support::with_test_provider(|_| {
1873 let logical =
1874 LogicalArray::new(vec![1, 0, 1, 1], vec![2, 2]).expect("logical construction");
1875 let result =
1876 call(Value::LogicalArray(logical.clone()), Vec::new()).expect("gpuArray logical");
1877 let Value::GpuTensor(handle) = result else {
1878 panic!("expected gpu tensor");
1879 };
1880 assert!(runmat_accelerate_api::handle_is_logical(&handle));
1881 let gathered =
1882 test_support::gather(Value::GpuTensor(handle.clone())).expect("gather logical");
1883 assert_eq!(gathered.shape, logical.shape);
1884 assert_eq!(gathered.materialize_f64(), vec![1.0, 0.0, 1.0, 1.0]);
1885 });
1886 }
1887
1888 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1889 #[test]
1890 fn gpu_array_uploads_complex_tensor() {
1891 test_support::with_test_provider(|_| {
1892 let complex = ComplexTensor::new(vec![(1.0, -2.0), (3.5, 4.25)], vec![1, 2]).unwrap();
1893 let result =
1894 call(Value::ComplexTensor(complex.clone()), Vec::new()).expect("gpuArray complex");
1895 let Value::GpuTensor(handle) = result else {
1896 panic!("expected gpu tensor");
1897 };
1898 assert_eq!(
1899 runmat_accelerate_api::handle_storage(&handle),
1900 GpuTensorStorage::ComplexInterleaved
1901 );
1902 let gathered = gather_complex(Value::GpuTensor(handle.clone()));
1903 assert_eq!(gathered.shape, complex.shape);
1904 assert_complex_close(&gathered.materialize_f64(), &complex.materialize_f64());
1905 });
1906 }
1907
1908 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1909 #[test]
1910 fn gpu_array_round_trips_typed_complex_integer_tensor_exactly() {
1911 test_support::with_test_provider(|_| {
1912 let complex = ComplexTensor::new_integer(
1913 IntegerComplexStorage::new(
1914 IntegerStorage::U64(vec![1_u64 << 63, u64::MAX]),
1915 IntegerStorage::U64(vec![1, 2]),
1916 )
1917 .unwrap(),
1918 vec![1, 2],
1919 )
1920 .unwrap();
1921 let result = call(Value::ComplexTensor(complex.clone()), Vec::new())
1922 .expect("typed complex integer upload");
1923 let Value::GpuTensor(handle) = &result else {
1924 panic!("expected gpuArray handle")
1925 };
1926 assert_eq!(
1927 runmat_accelerate_api::handle_integer_type(handle),
1928 Some(runmat_accelerate_api::IntegerElementType::U64)
1929 );
1930 assert_eq!(
1931 runmat_accelerate_api::handle_storage(handle),
1932 GpuTensorStorage::ComplexInterleaved
1933 );
1934 assert_eq!(gather_complex(result), complex);
1935 });
1936 }
1937
1938 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1939 #[test]
1940 fn gpu_array_handles_scalar_bool() {
1941 test_support::with_test_provider(|_| {
1942 let result = call(Value::Bool(true), Vec::new()).expect("gpuArray bool");
1943 let Value::GpuTensor(handle) = result else {
1944 panic!("expected gpu tensor");
1945 };
1946 assert!(runmat_accelerate_api::handle_is_logical(&handle));
1947 let gathered =
1948 test_support::gather(Value::GpuTensor(handle.clone())).expect("gather bool");
1949 assert_eq!(gathered.shape, vec![1, 1]);
1950 assert_eq!(gathered.materialize_f64(), vec![1.0]);
1951 });
1952 }
1953
1954 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1955 #[test]
1956 fn gpu_array_supports_char_arrays() {
1957 test_support::with_test_provider(|_| {
1958 let chars = CharArray::new("row1row2".chars().collect(), 2, 4).unwrap();
1959 let original: Vec<char> = chars.data.clone();
1960 let result =
1961 call(Value::CharArray(chars), Vec::new()).expect("gpuArray char array upload");
1962 let Value::GpuTensor(handle) = result else {
1963 panic!("expected gpu tensor");
1964 };
1965 let gathered =
1966 test_support::gather(Value::GpuTensor(handle.clone())).expect("gather chars");
1967 assert_eq!(gathered.shape, vec![2, 4]);
1968 let mut recovered = Vec::new();
1969 for col in 0..4 {
1970 for row in 0..2 {
1971 let idx = row + col * 2;
1972 let code = gathered.materialize_f64()[idx];
1973 let ch = char::from_u32(code as u32)
1974 .expect("valid unicode scalar from numeric code");
1975 recovered.push(ch);
1976 }
1977 }
1978 assert_eq!(recovered, original);
1979 });
1980 }
1981
1982 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1983 #[test]
1984 fn gpu_array_converts_strings() {
1985 test_support::with_test_provider(|_| {
1986 let result = call(Value::String("gpu".into()), Vec::new()).expect("gpuArray string");
1987 let Value::GpuTensor(handle) = result else {
1988 panic!("expected gpu tensor");
1989 };
1990 let gathered =
1991 test_support::gather(Value::GpuTensor(handle.clone())).expect("gather string");
1992 assert_eq!(gathered.shape, vec![1, 3]);
1993 let expected: Vec<f64> = "gpu".chars().map(|ch| ch as u32 as f64).collect();
1994 assert_eq!(gathered.materialize_f64(), expected);
1995 });
1996 }
1997
1998 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1999 #[test]
2000 fn gpu_array_passthrough_existing_handle() {
2001 test_support::with_test_provider(|provider| {
2002 let tensor = Tensor::new(vec![5.0, 6.0], vec![2, 1]).unwrap();
2003 let view = HostTensorView {
2004 data: &tensor.materialize_f64(),
2005 shape: &tensor.shape,
2006 };
2007 let handle = provider.upload(&view).expect("upload");
2008 let cloned = handle.clone();
2009 let result =
2010 call(Value::GpuTensor(handle.clone()), Vec::new()).expect("gpuArray passthrough");
2011 let Value::GpuTensor(returned) = result else {
2012 panic!("expected gpu tensor");
2013 };
2014 assert_eq!(returned.buffer_id, cloned.buffer_id);
2015 assert_eq!(returned.shape, cloned.shape);
2016 });
2017 }
2018
2019 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2020 #[test]
2021 fn gpu_array_passthrough_existing_complex_handle() {
2022 test_support::with_test_provider(|provider| {
2023 let complex = ComplexTensor::new(vec![(2.0, 3.0), (-4.0, 5.5)], vec![2, 1]).unwrap();
2024 let handle = gpu_helpers::upload_complex_tensor(provider, &complex).unwrap();
2025 let result =
2026 call(Value::GpuTensor(handle.clone()), Vec::new()).expect("gpuArray passthrough");
2027 let Value::GpuTensor(returned) = result else {
2028 panic!("expected gpu tensor");
2029 };
2030 assert_eq!(returned.buffer_id, handle.buffer_id);
2031 assert_eq!(
2032 runmat_accelerate_api::handle_storage(&returned),
2033 GpuTensorStorage::ComplexInterleaved
2034 );
2035 let gathered = gather_complex(Value::GpuTensor(returned.clone()));
2036 assert_complex_close(&gathered.materialize_f64(), &complex.materialize_f64());
2037 });
2038 }
2039
2040 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2041 #[test]
2042 fn gpu_array_complex_gpu_to_single_uses_native_transfer_storage() {
2043 test_support::with_test_provider(|provider| {
2044 let complex = ComplexTensor::new(
2045 vec![(1.234_567_89, -2.345_678_91), (3.456_789_12, 4.567_891_23)],
2046 vec![1, 2],
2047 )
2048 .unwrap();
2049 let handle = gpu_helpers::upload_complex_tensor(provider, &complex).unwrap();
2050 let converted = call(
2051 Value::GpuTensor(handle.clone()),
2052 vec![Value::from("single")],
2053 )
2054 .expect("shared transfer storage preserves complex single");
2055 let gathered = gather_complex(converted);
2056 assert_eq!(gathered.numeric_dtype(), NumericDType::F32);
2057 let expected: Vec<(f64, f64)> = complex
2058 .materialize_f64()
2059 .into_iter()
2060 .map(|(real, imag)| (f64::from(real as f32), f64::from(imag as f32)))
2061 .collect();
2062 assert_complex_close(&gathered.materialize_f64(), &expected);
2063 let source = gather_complex(Value::GpuTensor(handle));
2064 assert_eq!(source, complex);
2065 });
2066 }
2067
2068 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2069 #[test]
2070 fn gpu_array_casts_to_int32() {
2071 test_support::with_test_provider(|_| {
2072 let tensor = Tensor::new(vec![1.2, -3.7, 123456.0], vec![3, 1]).unwrap();
2073 let result =
2074 call(Value::Tensor(tensor), vec![Value::from("int32")]).expect("gpuArray int32");
2075 let Value::GpuTensor(handle) = result else {
2076 panic!("expected gpu tensor");
2077 };
2078 let gathered =
2079 test_support::gather(Value::GpuTensor(handle.clone())).expect("gather int32");
2080 assert_eq!(gathered.materialize_f64(), vec![1.0, -4.0, 123456.0]);
2081 });
2082 }
2083
2084 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2085 #[test]
2086 fn gpu_array_casts_to_uint8() {
2087 test_support::with_test_provider(|_| {
2088 let tensor = Tensor::new(vec![-12.0, 12.8, 300.4, f64::INFINITY], vec![4, 1]).unwrap();
2089 let result =
2090 call(Value::Tensor(tensor), vec![Value::from("uint8")]).expect("gpuArray uint8");
2091 let Value::GpuTensor(handle) = result else {
2092 panic!("expected gpu tensor");
2093 };
2094 let gathered =
2095 test_support::gather(Value::GpuTensor(handle.clone())).expect("gather uint8");
2096 assert_eq!(gathered.materialize_f64(), vec![0.0, 13.0, 255.0, 255.0]);
2097 });
2098 }
2099
2100 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2101 #[test]
2102 fn gpu_array_single_precision_rounds() {
2103 test_support::with_f32_test_provider(|_| {
2104 let tensor = Tensor::new(vec![1.23456789, -9.87654321], vec![2, 1]).unwrap();
2105 let result =
2106 call(Value::Tensor(tensor), vec![Value::from("single")]).expect("gpuArray single");
2107 let Value::GpuTensor(handle) = result else {
2108 panic!("expected gpu tensor");
2109 };
2110 let gathered =
2111 test_support::gather(Value::GpuTensor(handle.clone())).expect("gather single");
2112 let expected = [1.234_567_9_f32 as f64, (-9.876_543_f32) as f64];
2113 for (observed, expected) in gathered.materialize_f64().iter().zip(expected.iter()) {
2114 assert!((observed - expected).abs() < 1e-6);
2115 }
2116 });
2117 }
2118
2119 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2120 #[test]
2121 fn gpu_array_like_infers_logical() {
2122 test_support::with_test_provider(|_| {
2123 let tensor = Tensor::new(vec![0.0, 2.0, -3.0], vec![3, 1]).unwrap();
2124 let logical_proto =
2125 LogicalArray::new(vec![0, 1, 0], vec![3, 1]).expect("logical proto");
2126 let result = call(
2127 Value::Tensor(tensor),
2128 vec![Value::from("like"), Value::LogicalArray(logical_proto)],
2129 )
2130 .expect("gpuArray like logical");
2131 let Value::GpuTensor(handle) = result else {
2132 panic!("expected gpu tensor");
2133 };
2134 assert!(runmat_accelerate_api::handle_is_logical(&handle));
2135 let gathered = test_support::gather(Value::GpuTensor(handle.clone())).expect("gather");
2136 assert_eq!(gathered.materialize_f64(), vec![0.0, 1.0, 1.0]);
2137 });
2138 }
2139
2140 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2141 #[test]
2142 fn gpu_array_like_requires_argument() {
2143 test_support::with_test_provider(|_| {
2144 let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
2145 let err = call(Value::Tensor(tensor), vec![Value::from("like")]).unwrap_err();
2146 assert_eq!(err.to_string(), GPUARRAY_ERROR_LIKE_MISSING.message);
2147 assert_eq!(err.identifier(), GPUARRAY_ERROR_LIKE_MISSING.identifier);
2148 });
2149 }
2150
2151 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2152 #[test]
2153 fn gpu_array_unknown_option_errors() {
2154 test_support::with_test_provider(|_| {
2155 let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
2156 let err = call(Value::Tensor(tensor), vec![Value::from("mystery")]).unwrap_err();
2157 assert!(err
2158 .to_string()
2159 .contains(GPUARRAY_ERROR_UNKNOWN_OPTION.message));
2160 assert_eq!(err.identifier(), GPUARRAY_ERROR_UNKNOWN_OPTION.identifier);
2161 });
2162 }
2163
2164 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2165 #[test]
2166 fn gpu_array_gpu_to_logical_reuploads() {
2167 test_support::with_test_provider(|provider| {
2168 let tensor = Tensor::new(vec![2.0, 0.0, -5.5], vec![3, 1]).unwrap();
2169 let view = HostTensorView {
2170 data: &tensor.materialize_f64(),
2171 shape: &tensor.shape,
2172 };
2173 let handle = provider.upload(&view).expect("upload");
2174 let result = call(
2175 Value::GpuTensor(handle.clone()),
2176 vec![Value::from("logical")],
2177 )
2178 .expect("gpuArray logical cast");
2179 let Value::GpuTensor(new_handle) = result else {
2180 panic!("expected gpu tensor");
2181 };
2182 assert!(runmat_accelerate_api::handle_is_logical(&new_handle));
2183 let gathered =
2184 test_support::gather(Value::GpuTensor(new_handle.clone())).expect("gather");
2185 assert_eq!(gathered.materialize_f64(), vec![1.0, 0.0, 1.0]);
2186 provider.free(&handle).ok();
2187 provider.free(&new_handle).ok();
2188 });
2189 }
2190
2191 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2192 #[test]
2193 fn gpu_array_gpu_logical_to_double_clears_flag() {
2194 test_support::with_test_provider(|provider| {
2195 let tensor = Tensor::new(vec![1.0, 0.0], vec![2, 1]).unwrap();
2196 let view = HostTensorView {
2197 data: &tensor.materialize_f64(),
2198 shape: &tensor.shape,
2199 };
2200 let handle = provider.upload(&view).expect("upload");
2201 runmat_accelerate_api::set_handle_logical(&handle, true);
2202 let result = call(
2203 Value::GpuTensor(handle.clone()),
2204 vec![Value::from("double")],
2205 )
2206 .expect("gpuArray double cast");
2207 let Value::GpuTensor(new_handle) = result else {
2208 panic!("expected gpu tensor");
2209 };
2210 assert!(!runmat_accelerate_api::handle_is_logical(&new_handle));
2211 provider.free(&handle).ok();
2212 provider.free(&new_handle).ok();
2213 });
2214 }
2215
2216 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2217 #[test]
2218 fn gpu_array_applies_size_arguments() {
2219 test_support::with_test_provider(|_| {
2220 let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![4, 1]).unwrap();
2221 let result = call(
2222 Value::Tensor(tensor),
2223 vec![Value::from(2i32), Value::from(2i32)],
2224 )
2225 .expect("gpuArray reshape");
2226 let Value::GpuTensor(handle) = result else {
2227 panic!("expected gpu tensor");
2228 };
2229 assert_eq!(handle.shape, vec![2, 2]);
2230 });
2231 }
2232
2233 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2234 #[test]
2235 fn gpu_array_gpu_size_arguments_update_shape() {
2236 test_support::with_test_provider(|provider| {
2237 let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![4, 1]).unwrap();
2238 let view = HostTensorView {
2239 data: &tensor.materialize_f64(),
2240 shape: &tensor.shape,
2241 };
2242 let handle = provider.upload(&view).expect("upload");
2243 let result = call(
2244 Value::GpuTensor(handle.clone()),
2245 vec![Value::from(2i32), Value::from(2i32)],
2246 )
2247 .expect("gpuArray gpu reshape");
2248 let Value::GpuTensor(new_handle) = result else {
2249 panic!("expected gpu tensor");
2250 };
2251 assert_eq!(new_handle.shape, vec![2, 2]);
2252 provider.free(&handle).ok();
2253 provider.free(&new_handle).ok();
2254 });
2255 }
2256
2257 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2258 #[test]
2259 fn gpu_array_size_mismatch_errors() {
2260 test_support::with_test_provider(|_| {
2261 let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
2262 let err = call(
2263 Value::Tensor(tensor),
2264 vec![Value::from(2i32), Value::from(2i32)],
2265 )
2266 .unwrap_err();
2267 assert!(err.to_string().contains("cannot reshape"));
2268 assert_eq!(err.identifier(), GPUARRAY_ERROR_RESHAPE.identifier);
2269 });
2270 }
2271
2272 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2273 #[test]
2274 #[cfg(feature = "wgpu")]
2275 fn gpu_array_wgpu_native_integer_roundtrip() {
2276 use runmat_accelerate_api::AccelProvider;
2277
2278 match runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
2279 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
2280 ) {
2281 Ok(provider) => {
2282 for storage in [
2283 IntegerStorage::I8(vec![i8::MIN, i8::MAX]),
2284 IntegerStorage::I16(vec![i16::MIN, i16::MAX]),
2285 IntegerStorage::I32(vec![i32::MIN, i32::MAX]),
2286 IntegerStorage::I64(vec![i64::MIN, i64::MAX]),
2287 IntegerStorage::U8(vec![0, u8::MAX]),
2288 IntegerStorage::U16(vec![0, u16::MAX]),
2289 IntegerStorage::U32(vec![0, u32::MAX]),
2290 IntegerStorage::U64(vec![1_u64 << 63, u64::MAX]),
2291 ] {
2292 let expected = storage.clone();
2293 let result = call(
2294 Value::Tensor(Tensor::new_integer(storage, vec![2, 1]).unwrap()),
2295 Vec::new(),
2296 )
2297 .expect("wgpu integer upload");
2298 let Value::GpuTensor(handle) = result else {
2299 panic!("expected gpu tensor");
2300 };
2301 assert!(runmat_accelerate_api::handle_integer_type(&handle).is_some());
2302 let gathered = test_support::gather(Value::GpuTensor(handle.clone()))
2303 .expect("wgpu integer gather");
2304 assert_eq!(gathered.shape, vec![2, 1]);
2305 assert_eq!(gathered.integer_storage(), Some(&expected));
2306 provider.free(&handle).expect("free integer gpu buffer");
2307 }
2308 }
2309 Err(err) => {
2310 tracing::warn!("Skipping gpu_array_wgpu_native_integer_roundtrip: {err}");
2311 }
2312 }
2313 runmat_accelerate::simple_provider::register_inprocess_provider();
2314 }
2315
2316 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2317 #[test]
2318 #[cfg(feature = "wgpu")]
2319 fn gpu_array_wgpu_u64_linear_gather_and_scatter_remain_exact() {
2320 use runmat_accelerate_api::AccelProvider;
2321
2322 let Ok(provider) = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
2323 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
2324 ) else {
2325 return;
2326 };
2327 let source = match call(
2328 Value::Tensor(
2329 Tensor::new_integer(
2330 IntegerStorage::U64(vec![0, 1_u64 << 63, u64::MAX]),
2331 vec![1, 3],
2332 )
2333 .unwrap(),
2334 ),
2335 Vec::new(),
2336 )
2337 .expect("upload source")
2338 {
2339 Value::GpuTensor(handle) => handle,
2340 other => panic!("expected gpu tensor, got {other:?}"),
2341 };
2342 let selected = provider
2343 .gather_linear(&source, &[2, 1], &[1, 2])
2344 .expect("exact u64 gather");
2345 let gathered = test_support::gather(Value::GpuTensor(selected.clone())).expect("gather");
2346 assert_eq!(
2347 gathered.integer_storage(),
2348 Some(&IntegerStorage::U64(vec![u64::MAX, 1_u64 << 63]))
2349 );
2350
2351 let target = match call(
2352 Value::Tensor(
2353 Tensor::new_integer(IntegerStorage::U64(vec![0, 0, 0]), vec![1, 3]).unwrap(),
2354 ),
2355 Vec::new(),
2356 )
2357 .expect("upload target")
2358 {
2359 Value::GpuTensor(handle) => handle,
2360 other => panic!("expected gpu tensor, got {other:?}"),
2361 };
2362 provider
2363 .scatter_linear(&target, &[0, 2], &selected)
2364 .expect("exact u64 scatter");
2365 let gathered = test_support::gather(Value::GpuTensor(target.clone())).expect("gather");
2366 assert_eq!(
2367 gathered.integer_storage(),
2368 Some(&IntegerStorage::U64(vec![u64::MAX, 0, 1_u64 << 63]))
2369 );
2370 for handle in [&source, &selected, &target] {
2371 provider.free(handle).expect("free gpu buffer");
2372 }
2373 runmat_accelerate::simple_provider::register_inprocess_provider();
2374 }
2375
2376 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2377 #[test]
2378 #[cfg(feature = "wgpu")]
2379 fn gpu_array_wgpu_complex_and_integer_roundtrip() {
2380 use runmat_accelerate_api::AccelProvider;
2381
2382 match runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
2383 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
2384 ) {
2385 Ok(provider) => {
2386 let complex =
2387 ComplexTensor::new(vec![(1.25, -0.5), (-3.0, 4.0)], vec![1, 2]).unwrap();
2388 let result = call(Value::ComplexTensor(complex.clone()), Vec::new())
2389 .expect("wgpu complex-double upload");
2390 let Value::GpuTensor(handle) = result else {
2391 panic!("expected gpu tensor");
2392 };
2393 assert_eq!(
2394 runmat_accelerate_api::handle_storage(&handle),
2395 GpuTensorStorage::ComplexInterleaved
2396 );
2397 let gathered = gather_complex(Value::GpuTensor(handle.clone()));
2398 assert_eq!(gathered.shape, vec![1, 2]);
2399 assert_complex_close(&gathered.materialize_f64(), &complex.materialize_f64());
2400 provider.free(&handle).ok();
2401
2402 let complex_integer = ComplexTensor::new_integer(
2403 IntegerComplexStorage::new(
2404 IntegerStorage::U64(vec![1_u64 << 63, u64::MAX]),
2405 IntegerStorage::U64(vec![3, 4]),
2406 )
2407 .unwrap(),
2408 vec![1, 2],
2409 )
2410 .unwrap();
2411 let result = call(Value::ComplexTensor(complex_integer.clone()), Vec::new())
2412 .expect("wgpu complex-integer upload");
2413 let Value::GpuTensor(handle) = result else {
2414 panic!("expected complex integer gpu tensor")
2415 };
2416 assert_eq!(
2417 runmat_accelerate_api::handle_integer_type(&handle),
2418 Some(runmat_accelerate_api::IntegerElementType::U64)
2419 );
2420 assert_eq!(
2421 gather_complex(Value::GpuTensor(handle.clone())),
2422 complex_integer
2423 );
2424 provider.free(&handle).ok();
2425 }
2426 Err(err) => {
2427 tracing::warn!("Skipping gpu_array_wgpu_complex_roundtrip: {err}");
2428 }
2429 }
2430 runmat_accelerate::simple_provider::register_inprocess_provider();
2431 }
2432
2433 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2434 #[test]
2435 fn gpu_array_roundtrips_native_integer_inputs_and_class_requests() {
2436 test_support::with_test_provider(|_| {
2437 for (storage, element_type, class_name) in [
2438 (
2439 IntegerStorage::I8(vec![i8::MIN, i8::MAX]),
2440 runmat_accelerate_api::IntegerElementType::I8,
2441 "int8",
2442 ),
2443 (
2444 IntegerStorage::I16(vec![i16::MIN, i16::MAX]),
2445 runmat_accelerate_api::IntegerElementType::I16,
2446 "int16",
2447 ),
2448 (
2449 IntegerStorage::I32(vec![i32::MIN, i32::MAX]),
2450 runmat_accelerate_api::IntegerElementType::I32,
2451 "int32",
2452 ),
2453 (
2454 IntegerStorage::I64(vec![i64::MIN, i64::MAX]),
2455 runmat_accelerate_api::IntegerElementType::I64,
2456 "int64",
2457 ),
2458 (
2459 IntegerStorage::U8(vec![0, u8::MAX]),
2460 runmat_accelerate_api::IntegerElementType::U8,
2461 "uint8",
2462 ),
2463 (
2464 IntegerStorage::U16(vec![0, u16::MAX]),
2465 runmat_accelerate_api::IntegerElementType::U16,
2466 "uint16",
2467 ),
2468 (
2469 IntegerStorage::U32(vec![0, u32::MAX]),
2470 runmat_accelerate_api::IntegerElementType::U32,
2471 "uint32",
2472 ),
2473 (
2474 IntegerStorage::U64(vec![1_u64 << 63, u64::MAX]),
2475 runmat_accelerate_api::IntegerElementType::U64,
2476 "uint64",
2477 ),
2478 ] {
2479 let expected = storage.clone();
2480 let value = Value::Tensor(Tensor::new_integer(storage, vec![1, 2]).unwrap());
2481 let handle = match call(value, Vec::new()).expect("integer gpuArray upload") {
2482 Value::GpuTensor(handle) => handle,
2483 other => panic!("expected gpu tensor, got {other:?}"),
2484 };
2485 assert_eq!(
2486 runmat_accelerate_api::handle_integer_type(&handle),
2487 Some(element_type)
2488 );
2489 assert_eq!(
2490 runmat_accelerate_api::handle_class_name(&handle).as_deref(),
2491 Some(class_name)
2492 );
2493 let gathered = test_support::gather(Value::GpuTensor(handle)).expect("gather");
2494 assert_eq!(gathered.integer_storage(), Some(&expected));
2495 }
2496
2497 let handle = match call(Value::Num(1.0), vec![Value::from("uint64")])
2498 .expect("uint64 gpuArray conversion")
2499 {
2500 Value::GpuTensor(handle) => handle,
2501 other => panic!("expected gpu tensor, got {other:?}"),
2502 };
2503 let gathered = test_support::gather(Value::GpuTensor(handle)).expect("gather");
2504 assert_eq!(
2505 gathered.integer_storage(),
2506 Some(&IntegerStorage::U64(vec![1]))
2507 );
2508 });
2509 }
2510
2511 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2512 #[test]
2513 fn gpu_array_like_integer_tensor_prototype_preserves_native_class() {
2514 test_support::with_test_provider(|_| {
2515 let prototype = Value::Tensor(
2516 Tensor::new_integer(IntegerStorage::U64(vec![u64::MAX]), vec![1, 1])
2517 .expect("prototype"),
2518 );
2519 let handle = match call(Value::Num(7.0), vec![Value::from("like"), prototype])
2520 .expect("gpuArray like integer prototype")
2521 {
2522 Value::GpuTensor(handle) => handle,
2523 other => panic!("expected gpu tensor, got {other:?}"),
2524 };
2525
2526 assert_eq!(
2527 runmat_accelerate_api::handle_integer_type(&handle),
2528 Some(runmat_accelerate_api::IntegerElementType::U64)
2529 );
2530 assert_eq!(
2531 runmat_accelerate_api::handle_class_name(&handle).as_deref(),
2532 Some("uint64")
2533 );
2534 let gathered = test_support::gather(Value::GpuTensor(handle)).expect("gather");
2535 assert_eq!(
2536 gathered.integer_storage(),
2537 Some(&IntegerStorage::U64(vec![7]))
2538 );
2539 });
2540 }
2541
2542 #[test]
2543 fn gpuarray_type_for_logical_is_logical() {
2544 assert_eq!(
2545 gpuarray_type(&[Type::logical()], &ResolveContext::new(Vec::new())),
2546 Type::logical()
2547 );
2548 }
2549}