1use crate::builtins::acceleration::gpu::type_resolvers::gpuarray_type;
9use crate::builtins::common::spec::{
10 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
11 ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
12};
13use crate::builtins::common::{gpu_helpers, tensor};
14use runmat_accelerate_api::{GpuTensorHandle, HostTensorView, ProviderPrecision};
15use runmat_builtins::{
16 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
17 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
18 CharArray, ComplexTensor, IntValue, Tensor, Value,
19};
20use runmat_macros::runtime_builtin;
21
22use crate::{build_runtime_error, BuiltinResult, RuntimeError};
23
24const BUILTIN_NAME: &str = "gpuArray";
25
26const GPUARRAY_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
27 name: "G",
28 ty: BuiltinParamType::Any,
29 arity: BuiltinParamArity::Required,
30 default: None,
31 description: "GPU-resident handle containing uploaded/converted data.",
32}];
33
34const GPUARRAY_INPUTS_BASE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
35 name: "X",
36 ty: BuiltinParamType::Any,
37 arity: BuiltinParamArity::Required,
38 default: None,
39 description: "Input value to upload or recast on GPU.",
40}];
41
42const GPUARRAY_INPUTS_DIMS: [BuiltinParamDescriptor; 2] = [
43 BuiltinParamDescriptor {
44 name: "X",
45 ty: BuiltinParamType::Any,
46 arity: BuiltinParamArity::Required,
47 default: None,
48 description: "Input value to upload or recast on GPU.",
49 },
50 BuiltinParamDescriptor {
51 name: "dim",
52 ty: BuiltinParamType::SizeArg,
53 arity: BuiltinParamArity::Variadic,
54 default: None,
55 description: "Reshape dimensions (scalar dims or a single size vector tensor).",
56 },
57];
58
59const GPUARRAY_INPUTS_DTYPE: [BuiltinParamDescriptor; 2] = [
60 BuiltinParamDescriptor {
61 name: "X",
62 ty: BuiltinParamType::Any,
63 arity: BuiltinParamArity::Required,
64 default: None,
65 description: "Input value to upload or recast on GPU.",
66 },
67 BuiltinParamDescriptor {
68 name: "dtype",
69 ty: BuiltinParamType::StringScalar,
70 arity: BuiltinParamArity::Required,
71 default: Some("\"double\""),
72 description: "Class tag such as `single`, `int32`, `uint8`, `logical`, or `double`.",
73 },
74];
75
76const GPUARRAY_INPUTS_LIKE: [BuiltinParamDescriptor; 3] = [
77 BuiltinParamDescriptor {
78 name: "X",
79 ty: BuiltinParamType::Any,
80 arity: BuiltinParamArity::Required,
81 default: None,
82 description: "Input value to upload or recast on GPU.",
83 },
84 BuiltinParamDescriptor {
85 name: "like",
86 ty: BuiltinParamType::StringScalar,
87 arity: BuiltinParamArity::Required,
88 default: None,
89 description: "Literal keyword `\"like\"`.",
90 },
91 BuiltinParamDescriptor {
92 name: "prototype",
93 ty: BuiltinParamType::LikePrototype,
94 arity: BuiltinParamArity::Required,
95 default: None,
96 description: "Prototype value whose class drives output conversion.",
97 },
98];
99
100const GPUARRAY_INPUTS_DIMS_OPTIONS: [BuiltinParamDescriptor; 3] = [
101 BuiltinParamDescriptor {
102 name: "X",
103 ty: BuiltinParamType::Any,
104 arity: BuiltinParamArity::Required,
105 default: None,
106 description: "Input value to upload or recast on GPU.",
107 },
108 BuiltinParamDescriptor {
109 name: "dim",
110 ty: BuiltinParamType::SizeArg,
111 arity: BuiltinParamArity::Variadic,
112 default: None,
113 description: "Reshape dimensions (scalar dims or a single size vector tensor).",
114 },
115 BuiltinParamDescriptor {
116 name: "option",
117 ty: BuiltinParamType::Any,
118 arity: BuiltinParamArity::Variadic,
119 default: None,
120 description: "Class tags and/or `\"like\", prototype` qualifiers.",
121 },
122];
123
124const GPUARRAY_SIGNATURES: [BuiltinSignatureDescriptor; 5] = [
125 BuiltinSignatureDescriptor {
126 label: "G = gpuArray(X)",
127 inputs: &GPUARRAY_INPUTS_BASE,
128 outputs: &GPUARRAY_OUTPUT,
129 },
130 BuiltinSignatureDescriptor {
131 label: "G = gpuArray(X, dim, ...)",
132 inputs: &GPUARRAY_INPUTS_DIMS,
133 outputs: &GPUARRAY_OUTPUT,
134 },
135 BuiltinSignatureDescriptor {
136 label: "G = gpuArray(X, dtype)",
137 inputs: &GPUARRAY_INPUTS_DTYPE,
138 outputs: &GPUARRAY_OUTPUT,
139 },
140 BuiltinSignatureDescriptor {
141 label: "G = gpuArray(X, \"like\", prototype)",
142 inputs: &GPUARRAY_INPUTS_LIKE,
143 outputs: &GPUARRAY_OUTPUT,
144 },
145 BuiltinSignatureDescriptor {
146 label: "G = gpuArray(X, dim, ..., option, ...)",
147 inputs: &GPUARRAY_INPUTS_DIMS_OPTIONS,
148 outputs: &GPUARRAY_OUTPUT,
149 },
150];
151
152const GPUARRAY_ERROR_NO_PROVIDER: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
153 code: "RM.GPUARRAY.NO_PROVIDER",
154 identifier: Some("RunMat:gpuArray:NoProvider"),
155 when: "No acceleration provider is registered for host/device transfers.",
156 message: "gpuArray: no acceleration provider registered",
157};
158
159const GPUARRAY_ERROR_OPTION_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
160 code: "RM.GPUARRAY.OPTION_ARGUMENT",
161 identifier: Some("RunMat:gpuArray:OptionArgument"),
162 when: "Option tail contains non-text values where class tags/keywords are expected.",
163 message: "gpuArray: invalid option argument",
164};
165
166const GPUARRAY_ERROR_LIKE_MISSING: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
167 code: "RM.GPUARRAY.LIKE_MISSING",
168 identifier: Some("RunMat:gpuArray:LikeMissingPrototype"),
169 when: "Keyword `like` is supplied without a following prototype value.",
170 message: "gpuArray: expected a prototype value after 'like'",
171};
172
173const GPUARRAY_ERROR_LIKE_DUPLICATE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
174 code: "RM.GPUARRAY.LIKE_DUPLICATE",
175 identifier: Some("RunMat:gpuArray:LikeDuplicate"),
176 when: "Keyword `like` appears more than once.",
177 message: "gpuArray: duplicate 'like' qualifier",
178};
179
180const GPUARRAY_ERROR_CODISTRIBUTED_UNSUPPORTED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
181 code: "RM.GPUARRAY.CODISTRIBUTED_UNSUPPORTED",
182 identifier: Some("RunMat:gpuArray:CodistributedUnsupported"),
183 when: "Distributed/codistributed qualifiers are requested.",
184 message: "gpuArray: codistributed arrays are not supported yet",
185};
186
187const GPUARRAY_ERROR_CONFLICTING_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
188 code: "RM.GPUARRAY.CONFLICTING_TYPE",
189 identifier: Some("RunMat:gpuArray:ConflictingTypeQualifiers"),
190 when: "Multiple incompatible class qualifiers are supplied.",
191 message: "gpuArray: conflicting type qualifiers supplied",
192};
193
194const GPUARRAY_ERROR_UNKNOWN_OPTION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
195 code: "RM.GPUARRAY.UNKNOWN_OPTION",
196 identifier: Some("RunMat:gpuArray:UnknownOption"),
197 when: "Text option is not a recognized class/keyword token.",
198 message: "gpuArray: unrecognised option",
199};
200
201const GPUARRAY_ERROR_SIZE_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
202 code: "RM.GPUARRAY.SIZE_ARGUMENT",
203 identifier: Some("RunMat:gpuArray:InvalidSizeArgument"),
204 when: "Size arguments are malformed (not finite integers, negative, or invalid combinations).",
205 message: "gpuArray: invalid size argument",
206};
207
208const GPUARRAY_ERROR_LIKE_PROTOTYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
209 code: "RM.GPUARRAY.LIKE_PROTOTYPE",
210 identifier: Some("RunMat:gpuArray:InvalidLikePrototype"),
211 when: "`like` prototype is unsupported for type inference.",
212 message: "gpuArray: invalid 'like' prototype",
213};
214
215const GPUARRAY_ERROR_INPUT_TYPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
216 code: "RM.GPUARRAY.INPUT_TYPE",
217 identifier: Some("RunMat:gpuArray:UnsupportedInputType"),
218 when: "Input value type cannot be uploaded/coerced to supported gpuArray storage.",
219 message: "gpuArray: unsupported input type",
220};
221
222const GPUARRAY_ERROR_CONVERSION: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
223 code: "RM.GPUARRAY.CONVERSION",
224 identifier: Some("RunMat:gpuArray:ConversionFailed"),
225 when: "Requested dtype conversion cannot be performed (for example NaN->logical).",
226 message: "gpuArray: conversion failed",
227};
228
229const GPUARRAY_ERROR_RESHAPE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
230 code: "RM.GPUARRAY.RESHAPE",
231 identifier: Some("RunMat:gpuArray:ReshapeMismatch"),
232 when: "Requested shape does not preserve the element count.",
233 message: "gpuArray: cannot reshape gpuArray into requested size",
234};
235
236const GPUARRAY_ERROR_PROVIDER_IO: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
237 code: "RM.GPUARRAY.PROVIDER_IO",
238 identifier: Some("RunMat:gpuArray:ProviderIO"),
239 when: "Provider upload/download interaction fails.",
240 message: "gpuArray: provider I/O failed",
241};
242
243const GPUARRAY_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
244 code: "RM.GPUARRAY.INTERNAL",
245 identifier: Some("RunMat:gpuArray:InternalError"),
246 when: "Internal tensor/container conversion fails.",
247 message: "gpuArray: internal error",
248};
249
250const GPUARRAY_ERRORS: [BuiltinErrorDescriptor; 14] = [
251 GPUARRAY_ERROR_NO_PROVIDER,
252 GPUARRAY_ERROR_OPTION_ARGUMENT,
253 GPUARRAY_ERROR_LIKE_MISSING,
254 GPUARRAY_ERROR_LIKE_DUPLICATE,
255 GPUARRAY_ERROR_CODISTRIBUTED_UNSUPPORTED,
256 GPUARRAY_ERROR_CONFLICTING_TYPE,
257 GPUARRAY_ERROR_UNKNOWN_OPTION,
258 GPUARRAY_ERROR_SIZE_ARGUMENT,
259 GPUARRAY_ERROR_LIKE_PROTOTYPE,
260 GPUARRAY_ERROR_INPUT_TYPE,
261 GPUARRAY_ERROR_CONVERSION,
262 GPUARRAY_ERROR_RESHAPE,
263 GPUARRAY_ERROR_PROVIDER_IO,
264 GPUARRAY_ERROR_INTERNAL,
265];
266
267pub const GPUARRAY_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
268 signatures: &GPUARRAY_SIGNATURES,
269 output_mode: BuiltinOutputMode::Fixed,
270 completion_policy: BuiltinCompletionPolicy::Public,
271 errors: &GPUARRAY_ERRORS,
272};
273
274fn gpu_array_error(error: &'static BuiltinErrorDescriptor) -> RuntimeError {
275 gpu_array_error_with_message(error.message, error)
276}
277
278fn gpu_array_error_with_message(
279 message: impl Into<String>,
280 error: &'static BuiltinErrorDescriptor,
281) -> RuntimeError {
282 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
283 if let Some(identifier) = error.identifier {
284 builder = builder.with_identifier(identifier);
285 }
286 builder.build()
287}
288
289fn gpu_array_error_with_detail(
290 error: &'static BuiltinErrorDescriptor,
291 detail: impl AsRef<str>,
292) -> RuntimeError {
293 gpu_array_error_with_message(format!("{}: {}", error.message, detail.as_ref()), error)
294}
295
296#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::acceleration::gpu::gpuarray")]
297pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
298 name: "gpuArray",
299 op_kind: GpuOpKind::Custom("upload"),
300 supported_precisions: &[ScalarType::F32, ScalarType::F64],
301 broadcast: BroadcastSemantics::None,
302 provider_hooks: &[ProviderHook::Custom("upload")],
303 constant_strategy: ConstantStrategy::InlineLiteral,
304 residency: ResidencyPolicy::NewHandle,
305 nan_mode: ReductionNaN::Include,
306 two_pass_threshold: None,
307 workgroup_size: None,
308 accepts_nan_mode: false,
309 notes: "Invokes the provider `upload` hook, including complex interleaved uploads, and reuploads gpuArray inputs when dtype conversion is requested. Handles class strings, size vectors, and `'like'` prototypes.",
310};
311
312#[runmat_macros::register_fusion_spec(
313 builtin_path = "crate::builtins::acceleration::gpu::gpuarray"
314)]
315pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
316 name: "gpuArray",
317 shape: ShapeRequirements::Any,
318 constant_strategy: ConstantStrategy::InlineLiteral,
319 elementwise: None,
320 reduction: None,
321 emits_nan: false,
322 notes:
323 "Acts as a residency boundary; fusion graphs never cross explicit host↔device transfers.",
324};
325
326#[runtime_builtin(
327 name = "gpuArray",
328 category = "acceleration/gpu",
329 summary = "Move data to the GPU as gpuArray values.",
330 keywords = "gpuArray,gpu,accelerate,upload,dtype,like",
331 examples = "G = gpuArray([1 2 3], 'single');",
332 accel = "array_construct",
333 type_resolver(gpuarray_type),
334 descriptor(crate::builtins::acceleration::gpu::gpuarray::GPUARRAY_DESCRIPTOR),
335 builtin_path = "crate::builtins::acceleration::gpu::gpuarray"
336)]
337async fn gpu_array_builtin(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
338 let options = parse_options(&rest)?;
339 let incoming_precision = match &value {
340 Value::GpuTensor(handle) => runmat_accelerate_api::handle_precision(handle),
341 _ => None,
342 };
343 let dtype = resolve_dtype(&value, &options)?;
344 let dims = options.dims.clone();
345
346 let prepared = match value {
347 Value::GpuTensor(handle) => convert_device_value(handle, dtype).await?,
348 other => upload_host_value(other, dtype)?,
349 };
350
351 let mut handle = prepared.handle;
352
353 if let Some(dims) = dims.as_ref() {
354 apply_dims(&mut handle, dims)?;
355 }
356
357 let provider_precision = runmat_accelerate_api::provider()
358 .map(|p| p.precision())
359 .unwrap_or(ProviderPrecision::F64);
360 let requested_precision = match dtype {
361 DataClass::Single => Some(ProviderPrecision::F32),
362 _ => None,
363 };
364 let final_precision = requested_precision
365 .or(incoming_precision)
366 .unwrap_or(provider_precision);
367 runmat_accelerate_api::set_handle_precision(&handle, final_precision);
368
369 runmat_accelerate_api::set_handle_logical(&handle, prepared.logical);
370 runmat_accelerate_api::set_handle_class_name(&handle, dtype.class_name());
371
372 Ok(Value::GpuTensor(handle))
373}
374
375#[derive(Clone, Copy, Debug, PartialEq, Eq)]
376enum DataClass {
377 Double,
378 Single,
379 Logical,
380 Int8,
381 Int16,
382 Int32,
383 Int64,
384 UInt8,
385 UInt16,
386 UInt32,
387 UInt64,
388}
389
390impl DataClass {
391 fn from_tag(tag: &str) -> Option<Self> {
392 match tag {
393 "double" => Some(Self::Double),
394 "single" | "float32" => Some(Self::Single),
395 "logical" | "bool" | "boolean" => Some(Self::Logical),
396 "int8" => Some(Self::Int8),
397 "int16" => Some(Self::Int16),
398 "int32" | "int" => Some(Self::Int32),
399 "int64" => Some(Self::Int64),
400 "uint8" => Some(Self::UInt8),
401 "uint16" => Some(Self::UInt16),
402 "uint32" => Some(Self::UInt32),
403 "uint64" => Some(Self::UInt64),
404 "gpuarray" => None, _ => None,
406 }
407 }
408
409 fn class_name(self) -> &'static str {
410 match self {
411 Self::Double => "double",
412 Self::Single => "single",
413 Self::Logical => "logical",
414 Self::Int8 => "int8",
415 Self::Int16 => "int16",
416 Self::Int32 => "int32",
417 Self::Int64 => "int64",
418 Self::UInt8 => "uint8",
419 Self::UInt16 => "uint16",
420 Self::UInt32 => "uint32",
421 Self::UInt64 => "uint64",
422 }
423 }
424}
425
426#[derive(Debug, Default)]
427struct ParsedOptions {
428 dims: Option<Vec<usize>>,
429 explicit_dtype: Option<DataClass>,
430 prototype: Option<Value>,
431}
432
433fn parse_options(rest: &[Value]) -> BuiltinResult<ParsedOptions> {
434 let (index_after_dims, dims) = parse_size_arguments(rest)?;
435 let mut options = ParsedOptions {
436 dims,
437 ..ParsedOptions::default()
438 };
439
440 let mut idx = index_after_dims;
441 while idx < rest.len() {
442 let tag = value_to_lower_string(&rest[idx]).ok_or_else(|| {
443 gpu_array_error_with_message(
444 format!(
445 "gpuArray: unexpected argument {:?}; expected a class string or the keyword 'like'",
446 rest[idx]
447 ),
448 &GPUARRAY_ERROR_OPTION_ARGUMENT,
449 )
450 })?;
451
452 match tag.as_str() {
453 "like" => {
454 idx += 1;
455 if idx >= rest.len() {
456 return Err(gpu_array_error(&GPUARRAY_ERROR_LIKE_MISSING));
457 }
458 if options.prototype.is_some() {
459 return Err(gpu_array_error(&GPUARRAY_ERROR_LIKE_DUPLICATE));
460 }
461 options.prototype = Some(rest[idx].clone());
462 }
463 "distributed" | "codistributed" => {
464 return Err(gpu_array_error(&GPUARRAY_ERROR_CODISTRIBUTED_UNSUPPORTED));
465 }
466 tag => {
467 if let Some(class) = DataClass::from_tag(tag) {
468 if let Some(existing) = options.explicit_dtype {
469 if existing != class {
470 return Err(gpu_array_error(&GPUARRAY_ERROR_CONFLICTING_TYPE));
471 }
472 } else {
473 options.explicit_dtype = Some(class);
474 }
475 } else if tag != "gpuarray" {
476 return Err(gpu_array_error_with_detail(
477 &GPUARRAY_ERROR_UNKNOWN_OPTION,
478 format!("unrecognised option '{tag}'"),
479 ));
480 }
481 }
482 }
483
484 idx += 1;
485 }
486
487 Ok(options)
488}
489
490fn parse_size_arguments(rest: &[Value]) -> BuiltinResult<(usize, Option<Vec<usize>>)> {
491 let mut idx = 0;
492 let mut dims: Vec<usize> = Vec::new();
493 let mut vector_consumed = false;
494
495 while idx < rest.len() {
496 match &rest[idx] {
498 Value::String(_) | Value::StringArray(_) | Value::CharArray(_) => break,
499 _ => {}
500 }
501
502 match &rest[idx] {
503 Value::Int(i) => {
504 dims.push(int_to_dim(i)?);
505 }
506 Value::Num(n) => {
507 dims.push(float_to_dim(*n)?);
508 }
509 Value::Tensor(t) => {
510 if vector_consumed || !dims.is_empty() {
511 return Err(gpu_array_error_with_message(
512 "gpuArray: size vectors cannot be combined with scalar dimensions",
513 &GPUARRAY_ERROR_SIZE_ARGUMENT,
514 ));
515 }
516 dims = tensor_to_dims(t)?;
517 vector_consumed = true;
518 }
519 _ => break,
520 }
521 idx += 1;
522 }
523
524 let dims_option = if dims.is_empty() { None } else { Some(dims) };
525 Ok((idx, dims_option))
526}
527
528fn value_to_lower_string(value: &Value) -> Option<String> {
529 crate::builtins::common::tensor::value_to_string(value).map(|s| s.trim().to_ascii_lowercase())
530}
531
532fn int_to_dim(value: &IntValue) -> BuiltinResult<usize> {
533 let raw = value.to_i64();
534 if raw < 0 {
535 return Err(gpu_array_error_with_message(
536 "gpuArray: size arguments must be non-negative integers",
537 &GPUARRAY_ERROR_SIZE_ARGUMENT,
538 ));
539 }
540 Ok(raw as usize)
541}
542
543fn float_to_dim(value: f64) -> BuiltinResult<usize> {
544 if !value.is_finite() {
545 return Err(gpu_array_error_with_message(
546 "gpuArray: size arguments must be finite integers",
547 &GPUARRAY_ERROR_SIZE_ARGUMENT,
548 ));
549 }
550 let rounded = value.round();
551 if (rounded - value).abs() > f64::EPSILON {
552 return Err(gpu_array_error_with_message(
553 "gpuArray: size arguments must be integers",
554 &GPUARRAY_ERROR_SIZE_ARGUMENT,
555 ));
556 }
557 if rounded < 0.0 {
558 return Err(gpu_array_error_with_message(
559 "gpuArray: size arguments must be non-negative",
560 &GPUARRAY_ERROR_SIZE_ARGUMENT,
561 ));
562 }
563 Ok(rounded as usize)
564}
565
566fn tensor_to_dims(tensor: &Tensor) -> BuiltinResult<Vec<usize>> {
567 let mut dims = Vec::with_capacity(tensor.data.len());
568 for value in &tensor.data {
569 dims.push(float_to_dim(*value)?);
570 }
571 Ok(dims)
572}
573
574fn resolve_dtype(value: &Value, options: &ParsedOptions) -> BuiltinResult<DataClass> {
575 if let Some(explicit) = options.explicit_dtype {
576 return Ok(explicit);
577 }
578 if let Some(prototype) = options.prototype.as_ref() {
579 return infer_dtype_from_prototype(prototype);
580 }
581 if let Value::GpuTensor(handle) = value {
582 return dtype_from_gpu_handle(handle);
583 }
584 if value_defaults_to_logical(value) {
585 return Ok(DataClass::Logical);
586 }
587 Ok(DataClass::Double)
588}
589
590fn infer_dtype_from_prototype(proto: &Value) -> BuiltinResult<DataClass> {
591 match proto {
592 Value::GpuTensor(handle) => dtype_from_gpu_handle(handle),
593 Value::LogicalArray(_) | Value::Bool(_) => Ok(DataClass::Logical),
594 Value::Int(int) => Ok(match int {
595 IntValue::I8(_) => DataClass::Int8,
596 IntValue::I16(_) => DataClass::Int16,
597 IntValue::I32(_) => DataClass::Int32,
598 IntValue::I64(_) => DataClass::Int64,
599 IntValue::U8(_) => DataClass::UInt8,
600 IntValue::U16(_) => DataClass::UInt16,
601 IntValue::U32(_) => DataClass::UInt32,
602 IntValue::U64(_) => DataClass::UInt64,
603 }),
604 Value::Tensor(_) | Value::Num(_) => Ok(DataClass::Double),
605 Value::CharArray(_) => Ok(DataClass::Double),
606 Value::String(_) => Err(gpu_array_error_with_message(
607 "gpuArray: 'like' does not accept MATLAB string scalars; convert to char() first",
608 &GPUARRAY_ERROR_LIKE_PROTOTYPE,
609 )),
610 Value::StringArray(_) => Err(gpu_array_error_with_message(
611 "gpuArray: 'like' does not accept string arrays; convert to char arrays first",
612 &GPUARRAY_ERROR_LIKE_PROTOTYPE,
613 )),
614 Value::Complex(_, _) | Value::ComplexTensor(_) => Ok(DataClass::Double),
615 other => Err(gpu_array_error_with_message(
616 format!(
617 "gpuArray: unsupported 'like' prototype type {other:?}; expected numeric or logical values"
618 ),
619 &GPUARRAY_ERROR_LIKE_PROTOTYPE,
620 )),
621 }
622}
623
624fn dtype_from_gpu_handle(handle: &GpuTensorHandle) -> BuiltinResult<DataClass> {
625 if runmat_accelerate_api::handle_is_logical(handle) {
626 return Ok(DataClass::Logical);
627 }
628 if let Some(class_name) = runmat_accelerate_api::handle_class_name(handle) {
629 if let Some(dtype) = DataClass::from_tag(class_name.trim().to_ascii_lowercase().as_str()) {
630 return Ok(dtype);
631 }
632 }
633 let precision = runmat_accelerate_api::handle_precision(handle).or_else(|| {
634 runmat_accelerate_api::provider_for_handle(handle).map(|provider| provider.precision())
635 });
636 Ok(match precision {
637 Some(ProviderPrecision::F32) => DataClass::Single,
638 Some(ProviderPrecision::F64) | None => DataClass::Double,
639 })
640}
641
642fn value_defaults_to_logical(value: &Value) -> bool {
643 match value {
644 Value::LogicalArray(_) | Value::Bool(_) => true,
645 Value::GpuTensor(handle) => runmat_accelerate_api::handle_is_logical(handle),
646 _ => false,
647 }
648}
649
650struct PreparedHandle {
651 handle: GpuTensorHandle,
652 logical: bool,
653}
654
655fn upload_host_value(value: Value, dtype: DataClass) -> BuiltinResult<PreparedHandle> {
656 #[cfg(all(test, feature = "wgpu"))]
657 {
658 if runmat_accelerate_api::provider().is_none() {
659 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
660 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
661 );
662 }
663 }
664 let provider = runmat_accelerate_api::provider()
665 .ok_or_else(|| gpu_array_error(&GPUARRAY_ERROR_NO_PROVIDER))?;
666
667 match value {
668 Value::Complex(re, im) => {
669 let tensor = ComplexTensor::new(vec![(re, im)], vec![1, 1]).map_err(|err| {
670 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
671 })?;
672 upload_complex_host_value(provider, tensor, dtype)
673 }
674 Value::ComplexTensor(tensor) => upload_complex_host_value(provider, tensor, dtype),
675 value => upload_real_host_value(provider, value, dtype),
676 }
677}
678
679fn upload_real_host_value(
680 provider: &dyn runmat_accelerate_api::AccelProvider,
681 value: Value,
682 dtype: DataClass,
683) -> BuiltinResult<PreparedHandle> {
684 let tensor = coerce_host_value(value)?;
685 let (mut tensor, logical) = cast_tensor(tensor, dtype)?;
686
687 let view = HostTensorView {
688 data: &tensor.data,
689 shape: &tensor.shape,
690 };
691 let new_handle = provider.upload(&view).map_err(|err| {
692 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_PROVIDER_IO)
693 })?;
694
695 tensor.data.clear();
696
697 Ok(PreparedHandle {
698 handle: new_handle,
699 logical,
700 })
701}
702
703fn upload_complex_host_value(
704 provider: &dyn runmat_accelerate_api::AccelProvider,
705 mut tensor: ComplexTensor,
706 dtype: DataClass,
707) -> BuiltinResult<PreparedHandle> {
708 match dtype {
709 DataClass::Double => {}
710 DataClass::Single => {
711 for (re, im) in &mut tensor.data {
712 *re = (*re as f32) as f64;
713 *im = (*im as f32) as f64;
714 }
715 }
716 _ => {
717 return Err(gpu_array_error_with_message(
718 "gpuArray: complex inputs can only be uploaded as double or single precision",
719 &GPUARRAY_ERROR_INPUT_TYPE,
720 ));
721 }
722 }
723
724 let handle = gpu_helpers::upload_complex_tensor(provider, &tensor).map_err(|err| {
725 gpu_array_error_with_message(err.to_string(), &GPUARRAY_ERROR_PROVIDER_IO)
726 })?;
727 let precision = match dtype {
728 DataClass::Double => runmat_accelerate_api::ProviderPrecision::F64,
729 DataClass::Single => runmat_accelerate_api::ProviderPrecision::F32,
730 _ => unreachable!("complex dtype was validated above"),
731 };
732 runmat_accelerate_api::set_handle_precision(&handle, precision);
733 Ok(PreparedHandle {
734 handle,
735 logical: false,
736 })
737}
738
739async fn convert_device_value(
740 handle: GpuTensorHandle,
741 dtype: DataClass,
742) -> BuiltinResult<PreparedHandle> {
743 let was_logical = runmat_accelerate_api::handle_is_logical(&handle);
744 let was_complex = runmat_accelerate_api::handle_storage(&handle)
745 == runmat_accelerate_api::GpuTensorStorage::ComplexInterleaved;
746 let current_precision = runmat_accelerate_api::handle_precision(&handle);
747 match dtype {
748 DataClass::Double => {
749 if !(was_complex
750 && current_precision == Some(runmat_accelerate_api::ProviderPrecision::F32))
751 {
752 return Ok(PreparedHandle {
753 handle,
754 logical: false,
755 });
756 }
757 }
758 DataClass::Logical => {
759 if was_logical {
760 return Ok(PreparedHandle {
761 handle,
762 logical: true,
763 });
764 }
765 }
766 _ => {}
767 }
768
769 let provider = runmat_accelerate_api::provider_for_handle(&handle)
770 .or_else(runmat_accelerate_api::provider)
771 .ok_or_else(|| gpu_array_error(&GPUARRAY_ERROR_NO_PROVIDER))?;
772 if was_complex {
773 let gathered = gpu_helpers::gather_value_async(&Value::GpuTensor(handle.clone()))
774 .await
775 .map_err(|err| {
776 gpu_array_error_with_message(err.to_string(), &GPUARRAY_ERROR_PROVIDER_IO)
777 })?;
778 let Value::ComplexTensor(tensor) = gathered else {
779 return Err(gpu_array_error_with_message(
780 "gpuArray: expected complex gpuArray data during conversion",
781 &GPUARRAY_ERROR_PROVIDER_IO,
782 ));
783 };
784 let prepared = upload_complex_host_value(provider, tensor, dtype)?;
785 provider.free(&handle).ok();
786 return Ok(prepared);
787 }
788
789 let tensor = gpu_helpers::gather_tensor_async(&handle)
790 .await
791 .map_err(|err| {
792 gpu_array_error_with_message(err.to_string(), &GPUARRAY_ERROR_PROVIDER_IO)
793 })?;
794 let (mut tensor, logical) = cast_tensor(tensor, dtype)?;
795
796 let view = HostTensorView {
797 data: &tensor.data,
798 shape: &tensor.shape,
799 };
800 let new_handle = provider.upload(&view).map_err(|err| {
801 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_PROVIDER_IO)
802 })?;
803
804 provider.free(&handle).ok();
805 tensor.data.clear();
806
807 Ok(PreparedHandle {
808 handle: new_handle,
809 logical,
810 })
811}
812
813fn coerce_host_value(value: Value) -> BuiltinResult<Tensor> {
814 match value {
815 Value::Tensor(t) => Ok(t),
816 Value::LogicalArray(logical) => tensor::logical_to_tensor(&logical).map_err(|err| {
817 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
818 }),
819 Value::Bool(flag) => {
820 Tensor::new(vec![if flag { 1.0 } else { 0.0 }], vec![1, 1]).map_err(|err| {
821 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
822 })
823 }
824 Value::Num(n) => Tensor::new(vec![n], vec![1, 1]).map_err(|err| {
825 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
826 }),
827 Value::Int(i) => Tensor::new(vec![i.to_f64()], vec![1, 1]).map_err(|err| {
828 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
829 }),
830 Value::CharArray(ca) => char_array_to_tensor(&ca),
831 Value::String(text) => {
832 let ca = CharArray::new_row(&text);
833 char_array_to_tensor(&ca)
834 }
835 Value::StringArray(_) => Err(gpu_array_error_with_message(
836 "gpuArray: string arrays are not supported yet; convert to char arrays with CHAR first",
837 &GPUARRAY_ERROR_INPUT_TYPE,
838 )),
839 Value::Complex(_, _) | Value::ComplexTensor(_) => Err(gpu_array_error_with_message(
840 "gpuArray: internal complex upload routing failed",
841 &GPUARRAY_ERROR_INTERNAL,
842 )),
843 other => Err(gpu_array_error_with_detail(
844 &GPUARRAY_ERROR_INPUT_TYPE,
845 format!("unsupported input type for GPU transfer: {other:?}"),
846 )),
847 }
848}
849
850fn cast_tensor(mut tensor: Tensor, dtype: DataClass) -> BuiltinResult<(Tensor, bool)> {
851 let logical = match dtype {
852 DataClass::Logical => {
853 convert_to_logical(&mut tensor.data)?;
854 true
855 }
856 DataClass::Single => {
857 convert_to_single(&mut tensor.data);
858 false
859 }
860 DataClass::Int8 => {
861 convert_to_int_range(&mut tensor.data, i8::MIN as f64, i8::MAX as f64);
862 false
863 }
864 DataClass::Int16 => {
865 convert_to_int_range(&mut tensor.data, i16::MIN as f64, i16::MAX as f64);
866 false
867 }
868 DataClass::Int32 => {
869 convert_to_int_range(&mut tensor.data, i32::MIN as f64, i32::MAX as f64);
870 false
871 }
872 DataClass::Int64 => {
873 convert_to_int_range(&mut tensor.data, i64::MIN as f64, i64::MAX as f64);
874 false
875 }
876 DataClass::UInt8 => {
877 convert_to_int_range(&mut tensor.data, 0.0, u8::MAX as f64);
878 false
879 }
880 DataClass::UInt16 => {
881 convert_to_int_range(&mut tensor.data, 0.0, u16::MAX as f64);
882 false
883 }
884 DataClass::UInt32 => {
885 convert_to_int_range(&mut tensor.data, 0.0, u32::MAX as f64);
886 false
887 }
888 DataClass::UInt64 => {
889 convert_to_int_range(&mut tensor.data, 0.0, u64::MAX as f64);
890 false
891 }
892 DataClass::Double => false,
893 };
894
895 Ok((tensor, logical))
896}
897
898fn convert_to_logical(data: &mut [f64]) -> BuiltinResult<()> {
899 for value in data.iter_mut() {
900 if value.is_nan() {
901 return Err(gpu_array_error_with_message(
902 "gpuArray: cannot convert NaN to logical",
903 &GPUARRAY_ERROR_CONVERSION,
904 ));
905 }
906 *value = if *value != 0.0 { 1.0 } else { 0.0 };
907 }
908 Ok(())
909}
910
911fn convert_to_single(data: &mut [f64]) {
912 for value in data.iter_mut() {
913 *value = (*value as f32) as f64;
914 }
915}
916
917fn convert_to_int_range(data: &mut [f64], min: f64, max: f64) {
918 for value in data.iter_mut() {
919 if value.is_nan() {
920 *value = min;
921 continue;
922 }
923 if value.is_infinite() {
924 *value = if value.is_sign_negative() { min } else { max };
925 continue;
926 }
927 let rounded = value.round();
928 *value = rounded.clamp(min, max);
929 }
930}
931
932fn apply_dims(handle: &mut GpuTensorHandle, dims: &[usize]) -> BuiltinResult<()> {
933 let new_elems: usize = dims.iter().product();
934 let current_elems: usize = if handle.shape.is_empty() {
935 new_elems
936 } else {
937 handle.shape.iter().product()
938 };
939 if new_elems != current_elems {
940 return Err(gpu_array_error_with_message(
941 format!(
942 "gpuArray: cannot reshape gpuArray of {current_elems} elements into size {:?}",
943 dims
944 ),
945 &GPUARRAY_ERROR_RESHAPE,
946 ));
947 }
948 handle.shape = dims.to_vec();
949 Ok(())
950}
951
952fn char_array_to_tensor(ca: &CharArray) -> BuiltinResult<Tensor> {
953 let rows = ca.rows;
954 let cols = ca.cols;
955 if rows == 0 || cols == 0 {
956 return Tensor::new(Vec::new(), vec![rows, cols]).map_err(|err| {
957 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
958 });
959 }
960 let mut data = vec![0.0; rows * cols];
961 for row in 0..rows {
963 for col in 0..cols {
964 let idx_char = row * cols + col;
965 let ch = ca.data[idx_char];
966 data[row * cols + col] = ch as u32 as f64;
967 }
968 }
969 Tensor::new(data, vec![rows, cols]).map_err(|err| {
970 gpu_array_error_with_message(format!("gpuArray: {err}"), &GPUARRAY_ERROR_INTERNAL)
971 })
972}
973
974#[cfg(test)]
975pub(crate) mod tests {
976 use super::*;
977 use crate::builtins::common::test_support;
978 use futures::executor::block_on;
979 use runmat_accelerate_api::{GpuTensorStorage, HostTensorView};
980 use runmat_builtins::{ComplexTensor, IntValue, LogicalArray, ResolveContext, Type};
981
982 fn call(value: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
983 block_on(gpu_array_builtin(value, rest))
984 }
985
986 fn gather_complex(value: Value) -> ComplexTensor {
987 match block_on(crate::dispatcher::gather_if_needed_async(&value)).expect("gather complex") {
988 Value::ComplexTensor(tensor) => tensor,
989 other => panic!("expected ComplexTensor, got {other:?}"),
990 }
991 }
992
993 fn assert_complex_close(actual: &[(f64, f64)], expected: &[(f64, f64)]) {
994 assert_eq!(actual.len(), expected.len());
995 for (idx, ((ar, ai), (er, ei))) in actual.iter().zip(expected.iter()).enumerate() {
996 assert!(
997 (ar - er).abs() < 1e-12 && (ai - ei).abs() < 1e-12,
998 "at {idx}: expected ({er}, {ei}), got ({ar}, {ai})"
999 );
1000 }
1001 }
1002
1003 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1004 #[test]
1005 fn gpu_array_transfers_numeric_tensor() {
1006 test_support::with_test_provider(|_| {
1007 let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
1008 let result = call(Value::Tensor(tensor.clone()), Vec::new()).expect("gpuArray upload");
1009 let Value::GpuTensor(handle) = result else {
1010 panic!("expected gpu tensor");
1011 };
1012 assert_eq!(handle.shape, tensor.shape);
1013 let gathered =
1014 test_support::gather(Value::GpuTensor(handle.clone())).expect("gather values");
1015 assert_eq!(gathered.shape, tensor.shape);
1016 assert_eq!(gathered.data, tensor.data);
1017 });
1018 }
1019
1020 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1021 #[test]
1022 fn gpu_array_marks_logical_inputs() {
1023 test_support::with_test_provider(|_| {
1024 let logical =
1025 LogicalArray::new(vec![1, 0, 1, 1], vec![2, 2]).expect("logical construction");
1026 let result =
1027 call(Value::LogicalArray(logical.clone()), Vec::new()).expect("gpuArray logical");
1028 let Value::GpuTensor(handle) = result else {
1029 panic!("expected gpu tensor");
1030 };
1031 assert!(runmat_accelerate_api::handle_is_logical(&handle));
1032 let gathered =
1033 test_support::gather(Value::GpuTensor(handle.clone())).expect("gather logical");
1034 assert_eq!(gathered.shape, logical.shape);
1035 assert_eq!(gathered.data, vec![1.0, 0.0, 1.0, 1.0]);
1036 });
1037 }
1038
1039 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1040 #[test]
1041 fn gpu_array_uploads_complex_tensor() {
1042 test_support::with_test_provider(|_| {
1043 let complex = ComplexTensor::new(vec![(1.0, -2.0), (3.5, 4.25)], vec![1, 2]).unwrap();
1044 let result =
1045 call(Value::ComplexTensor(complex.clone()), Vec::new()).expect("gpuArray complex");
1046 let Value::GpuTensor(handle) = result else {
1047 panic!("expected gpu tensor");
1048 };
1049 assert_eq!(
1050 runmat_accelerate_api::handle_storage(&handle),
1051 GpuTensorStorage::ComplexInterleaved
1052 );
1053 let gathered = gather_complex(Value::GpuTensor(handle.clone()));
1054 assert_eq!(gathered.shape, complex.shape);
1055 assert_complex_close(&gathered.data, &complex.data);
1056 });
1057 }
1058
1059 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1060 #[test]
1061 fn gpu_array_handles_scalar_bool() {
1062 test_support::with_test_provider(|_| {
1063 let result = call(Value::Bool(true), Vec::new()).expect("gpuArray bool");
1064 let Value::GpuTensor(handle) = result else {
1065 panic!("expected gpu tensor");
1066 };
1067 assert!(runmat_accelerate_api::handle_is_logical(&handle));
1068 let gathered =
1069 test_support::gather(Value::GpuTensor(handle.clone())).expect("gather bool");
1070 assert_eq!(gathered.shape, vec![1, 1]);
1071 assert_eq!(gathered.data, vec![1.0]);
1072 });
1073 }
1074
1075 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1076 #[test]
1077 fn gpu_array_supports_char_arrays() {
1078 test_support::with_test_provider(|_| {
1079 let chars = CharArray::new("row1row2".chars().collect(), 2, 4).unwrap();
1080 let original: Vec<char> = chars.data.clone();
1081 let result =
1082 call(Value::CharArray(chars), Vec::new()).expect("gpuArray char array upload");
1083 let Value::GpuTensor(handle) = result else {
1084 panic!("expected gpu tensor");
1085 };
1086 let gathered =
1087 test_support::gather(Value::GpuTensor(handle.clone())).expect("gather chars");
1088 assert_eq!(gathered.shape, vec![2, 4]);
1089 let mut recovered = Vec::new();
1090 for col in 0..4 {
1091 for row in 0..2 {
1092 let idx = row + col * 2;
1093 let code = gathered.data[idx];
1094 let ch = char::from_u32(code as u32)
1095 .expect("valid unicode scalar from numeric code");
1096 recovered.push(ch);
1097 }
1098 }
1099 assert_eq!(recovered, original);
1100 });
1101 }
1102
1103 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1104 #[test]
1105 fn gpu_array_converts_strings() {
1106 test_support::with_test_provider(|_| {
1107 let result = call(Value::String("gpu".into()), Vec::new()).expect("gpuArray string");
1108 let Value::GpuTensor(handle) = result else {
1109 panic!("expected gpu tensor");
1110 };
1111 let gathered =
1112 test_support::gather(Value::GpuTensor(handle.clone())).expect("gather string");
1113 assert_eq!(gathered.shape, vec![1, 3]);
1114 let expected: Vec<f64> = "gpu".chars().map(|ch| ch as u32 as f64).collect();
1115 assert_eq!(gathered.data, expected);
1116 });
1117 }
1118
1119 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1120 #[test]
1121 fn gpu_array_passthrough_existing_handle() {
1122 test_support::with_test_provider(|provider| {
1123 let tensor = Tensor::new(vec![5.0, 6.0], vec![2, 1]).unwrap();
1124 let view = HostTensorView {
1125 data: &tensor.data,
1126 shape: &tensor.shape,
1127 };
1128 let handle = provider.upload(&view).expect("upload");
1129 let cloned = handle.clone();
1130 let result =
1131 call(Value::GpuTensor(handle.clone()), Vec::new()).expect("gpuArray passthrough");
1132 let Value::GpuTensor(returned) = result else {
1133 panic!("expected gpu tensor");
1134 };
1135 assert_eq!(returned.buffer_id, cloned.buffer_id);
1136 assert_eq!(returned.shape, cloned.shape);
1137 });
1138 }
1139
1140 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1141 #[test]
1142 fn gpu_array_passthrough_existing_complex_handle() {
1143 test_support::with_test_provider(|provider| {
1144 let complex = ComplexTensor::new(vec![(2.0, 3.0), (-4.0, 5.5)], vec![2, 1]).unwrap();
1145 let handle = gpu_helpers::upload_complex_tensor(provider, &complex).unwrap();
1146 let result =
1147 call(Value::GpuTensor(handle.clone()), Vec::new()).expect("gpuArray passthrough");
1148 let Value::GpuTensor(returned) = result else {
1149 panic!("expected gpu tensor");
1150 };
1151 assert_eq!(returned.buffer_id, handle.buffer_id);
1152 assert_eq!(
1153 runmat_accelerate_api::handle_storage(&returned),
1154 GpuTensorStorage::ComplexInterleaved
1155 );
1156 let gathered = gather_complex(Value::GpuTensor(returned.clone()));
1157 assert_complex_close(&gathered.data, &complex.data);
1158 });
1159 }
1160
1161 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1162 #[test]
1163 fn gpu_array_complex_gpu_to_single_reuploads_complex_handle() {
1164 test_support::with_test_provider(|provider| {
1165 let complex = ComplexTensor::new(
1166 vec![(1.234_567_89, -2.345_678_91), (3.456_789_12, 4.567_891_23)],
1167 vec![1, 2],
1168 )
1169 .unwrap();
1170 let handle = gpu_helpers::upload_complex_tensor(provider, &complex).unwrap();
1171 let result = call(
1172 Value::GpuTensor(handle.clone()),
1173 vec![Value::from("single")],
1174 )
1175 .expect("gpuArray complex single");
1176 let Value::GpuTensor(returned) = result else {
1177 panic!("expected gpu tensor");
1178 };
1179 assert_ne!(returned.buffer_id, handle.buffer_id);
1180 assert_eq!(
1181 runmat_accelerate_api::handle_storage(&returned),
1182 GpuTensorStorage::ComplexInterleaved
1183 );
1184 assert_eq!(
1185 runmat_accelerate_api::handle_precision(&returned),
1186 Some(runmat_accelerate_api::ProviderPrecision::F32)
1187 );
1188 let gathered = gather_complex(Value::GpuTensor(returned.clone()));
1189 let expected = complex
1190 .data
1191 .iter()
1192 .map(|(re, im)| ((*re as f32) as f64, (*im as f32) as f64))
1193 .collect::<Vec<_>>();
1194 assert_complex_close(&gathered.data, &expected);
1195 });
1196 }
1197
1198 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1199 #[test]
1200 fn gpu_array_casts_to_int32() {
1201 test_support::with_test_provider(|_| {
1202 let tensor = Tensor::new(vec![1.2, -3.7, 123456.0], vec![3, 1]).unwrap();
1203 let result =
1204 call(Value::Tensor(tensor), vec![Value::from("int32")]).expect("gpuArray int32");
1205 let Value::GpuTensor(handle) = result else {
1206 panic!("expected gpu tensor");
1207 };
1208 let gathered =
1209 test_support::gather(Value::GpuTensor(handle.clone())).expect("gather int32");
1210 assert_eq!(gathered.data, vec![1.0, -4.0, 123456.0]);
1211 });
1212 }
1213
1214 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1215 #[test]
1216 fn gpu_array_casts_to_uint8() {
1217 test_support::with_test_provider(|_| {
1218 let tensor = Tensor::new(vec![-12.0, 12.8, 300.4, f64::INFINITY], vec![4, 1]).unwrap();
1219 let result =
1220 call(Value::Tensor(tensor), vec![Value::from("uint8")]).expect("gpuArray uint8");
1221 let Value::GpuTensor(handle) = result else {
1222 panic!("expected gpu tensor");
1223 };
1224 let gathered =
1225 test_support::gather(Value::GpuTensor(handle.clone())).expect("gather uint8");
1226 assert_eq!(gathered.data, vec![0.0, 13.0, 255.0, 255.0]);
1227 });
1228 }
1229
1230 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1231 #[test]
1232 fn gpu_array_single_precision_rounds() {
1233 test_support::with_test_provider(|_| {
1234 let tensor = Tensor::new(vec![1.23456789, -9.87654321], vec![2, 1]).unwrap();
1235 let result =
1236 call(Value::Tensor(tensor), vec![Value::from("single")]).expect("gpuArray single");
1237 let Value::GpuTensor(handle) = result else {
1238 panic!("expected gpu tensor");
1239 };
1240 let gathered =
1241 test_support::gather(Value::GpuTensor(handle.clone())).expect("gather single");
1242 let expected = [1.234_567_9_f32 as f64, (-9.876_543_f32) as f64];
1243 for (observed, expected) in gathered.data.iter().zip(expected.iter()) {
1244 assert!((observed - expected).abs() < 1e-6);
1245 }
1246 });
1247 }
1248
1249 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1250 #[test]
1251 fn gpu_array_like_infers_logical() {
1252 test_support::with_test_provider(|_| {
1253 let tensor = Tensor::new(vec![0.0, 2.0, -3.0], vec![3, 1]).unwrap();
1254 let logical_proto =
1255 LogicalArray::new(vec![0, 1, 0], vec![3, 1]).expect("logical proto");
1256 let result = call(
1257 Value::Tensor(tensor),
1258 vec![Value::from("like"), Value::LogicalArray(logical_proto)],
1259 )
1260 .expect("gpuArray like logical");
1261 let Value::GpuTensor(handle) = result else {
1262 panic!("expected gpu tensor");
1263 };
1264 assert!(runmat_accelerate_api::handle_is_logical(&handle));
1265 let gathered = test_support::gather(Value::GpuTensor(handle.clone())).expect("gather");
1266 assert_eq!(gathered.data, vec![0.0, 1.0, 1.0]);
1267 });
1268 }
1269
1270 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1271 #[test]
1272 fn gpu_array_like_requires_argument() {
1273 test_support::with_test_provider(|_| {
1274 let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
1275 let err = call(Value::Tensor(tensor), vec![Value::from("like")]).unwrap_err();
1276 assert_eq!(err.to_string(), GPUARRAY_ERROR_LIKE_MISSING.message);
1277 assert_eq!(err.identifier(), GPUARRAY_ERROR_LIKE_MISSING.identifier);
1278 });
1279 }
1280
1281 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1282 #[test]
1283 fn gpu_array_unknown_option_errors() {
1284 test_support::with_test_provider(|_| {
1285 let tensor = Tensor::new(vec![1.0], vec![1, 1]).unwrap();
1286 let err = call(Value::Tensor(tensor), vec![Value::from("mystery")]).unwrap_err();
1287 assert!(err
1288 .to_string()
1289 .contains(GPUARRAY_ERROR_UNKNOWN_OPTION.message));
1290 assert_eq!(err.identifier(), GPUARRAY_ERROR_UNKNOWN_OPTION.identifier);
1291 });
1292 }
1293
1294 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1295 #[test]
1296 fn gpu_array_gpu_to_logical_reuploads() {
1297 test_support::with_test_provider(|provider| {
1298 let tensor = Tensor::new(vec![2.0, 0.0, -5.5], vec![3, 1]).unwrap();
1299 let view = HostTensorView {
1300 data: &tensor.data,
1301 shape: &tensor.shape,
1302 };
1303 let handle = provider.upload(&view).expect("upload");
1304 let result = call(
1305 Value::GpuTensor(handle.clone()),
1306 vec![Value::from("logical")],
1307 )
1308 .expect("gpuArray logical cast");
1309 let Value::GpuTensor(new_handle) = result else {
1310 panic!("expected gpu tensor");
1311 };
1312 assert!(runmat_accelerate_api::handle_is_logical(&new_handle));
1313 let gathered =
1314 test_support::gather(Value::GpuTensor(new_handle.clone())).expect("gather");
1315 assert_eq!(gathered.data, vec![1.0, 0.0, 1.0]);
1316 provider.free(&handle).ok();
1317 provider.free(&new_handle).ok();
1318 });
1319 }
1320
1321 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1322 #[test]
1323 fn gpu_array_gpu_logical_to_double_clears_flag() {
1324 test_support::with_test_provider(|provider| {
1325 let tensor = Tensor::new(vec![1.0, 0.0], vec![2, 1]).unwrap();
1326 let view = HostTensorView {
1327 data: &tensor.data,
1328 shape: &tensor.shape,
1329 };
1330 let handle = provider.upload(&view).expect("upload");
1331 runmat_accelerate_api::set_handle_logical(&handle, true);
1332 let result = call(
1333 Value::GpuTensor(handle.clone()),
1334 vec![Value::from("double")],
1335 )
1336 .expect("gpuArray double cast");
1337 let Value::GpuTensor(new_handle) = result else {
1338 panic!("expected gpu tensor");
1339 };
1340 assert!(!runmat_accelerate_api::handle_is_logical(&new_handle));
1341 provider.free(&handle).ok();
1342 provider.free(&new_handle).ok();
1343 });
1344 }
1345
1346 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1347 #[test]
1348 fn gpu_array_applies_size_arguments() {
1349 test_support::with_test_provider(|_| {
1350 let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![4, 1]).unwrap();
1351 let result = call(
1352 Value::Tensor(tensor),
1353 vec![Value::from(2i32), Value::from(2i32)],
1354 )
1355 .expect("gpuArray reshape");
1356 let Value::GpuTensor(handle) = result else {
1357 panic!("expected gpu tensor");
1358 };
1359 assert_eq!(handle.shape, vec![2, 2]);
1360 });
1361 }
1362
1363 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1364 #[test]
1365 fn gpu_array_gpu_size_arguments_update_shape() {
1366 test_support::with_test_provider(|provider| {
1367 let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![4, 1]).unwrap();
1368 let view = HostTensorView {
1369 data: &tensor.data,
1370 shape: &tensor.shape,
1371 };
1372 let handle = provider.upload(&view).expect("upload");
1373 let result = call(
1374 Value::GpuTensor(handle.clone()),
1375 vec![Value::from(2i32), Value::from(2i32)],
1376 )
1377 .expect("gpuArray gpu reshape");
1378 let Value::GpuTensor(new_handle) = result else {
1379 panic!("expected gpu tensor");
1380 };
1381 assert_eq!(new_handle.shape, vec![2, 2]);
1382 provider.free(&handle).ok();
1383 provider.free(&new_handle).ok();
1384 });
1385 }
1386
1387 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1388 #[test]
1389 fn gpu_array_size_mismatch_errors() {
1390 test_support::with_test_provider(|_| {
1391 let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
1392 let err = call(
1393 Value::Tensor(tensor),
1394 vec![Value::from(2i32), Value::from(2i32)],
1395 )
1396 .unwrap_err();
1397 assert!(err.to_string().contains("cannot reshape"));
1398 assert_eq!(err.identifier(), GPUARRAY_ERROR_RESHAPE.identifier);
1399 });
1400 }
1401
1402 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1403 #[test]
1404 #[cfg(feature = "wgpu")]
1405 fn gpu_array_wgpu_roundtrip() {
1406 use runmat_accelerate_api::AccelProvider;
1407
1408 match runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
1409 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
1410 ) {
1411 Ok(provider) => {
1412 let tensor = Tensor::new(vec![1.0, 2.5, 3.5], vec![3, 1]).unwrap();
1413 let result = call(Value::Tensor(tensor.clone()), vec![Value::from("int32")])
1414 .expect("wgpu upload");
1415 let Value::GpuTensor(handle) = result else {
1416 panic!("expected gpu tensor");
1417 };
1418 let gathered =
1419 test_support::gather(Value::GpuTensor(handle.clone())).expect("wgpu gather");
1420 assert_eq!(gathered.shape, vec![3, 1]);
1421 assert_eq!(gathered.data, vec![1.0, 3.0, 4.0]);
1422 provider.free(&handle).ok();
1423 }
1424 Err(err) => {
1425 tracing::warn!("Skipping gpu_array_wgpu_roundtrip: {err}");
1426 }
1427 }
1428 runmat_accelerate::simple_provider::register_inprocess_provider();
1429 }
1430
1431 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1432 #[test]
1433 #[cfg(feature = "wgpu")]
1434 fn gpu_array_wgpu_complex_roundtrip() {
1435 use runmat_accelerate_api::AccelProvider;
1436
1437 match runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
1438 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
1439 ) {
1440 Ok(provider) => {
1441 let complex =
1442 ComplexTensor::new(vec![(1.25, -0.5), (-3.0, 4.0)], vec![1, 2]).unwrap();
1443 let result =
1444 call(Value::ComplexTensor(complex.clone()), Vec::new()).expect("wgpu upload");
1445 let Value::GpuTensor(handle) = result else {
1446 panic!("expected gpu tensor");
1447 };
1448 assert_eq!(
1449 runmat_accelerate_api::handle_storage(&handle),
1450 GpuTensorStorage::ComplexInterleaved
1451 );
1452 let gathered = gather_complex(Value::GpuTensor(handle.clone()));
1453 assert_eq!(gathered.shape, vec![1, 2]);
1454 assert_complex_close(&gathered.data, &complex.data);
1455 provider.free(&handle).ok();
1456 }
1457 Err(err) => {
1458 tracing::warn!("Skipping gpu_array_wgpu_complex_roundtrip: {err}");
1459 }
1460 }
1461 runmat_accelerate::simple_provider::register_inprocess_provider();
1462 }
1463
1464 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1465 #[test]
1466 fn gpu_array_accepts_int_scalars() {
1467 test_support::with_test_provider(|_| {
1468 let value = Value::Int(IntValue::I32(7));
1469 let result = call(value, Vec::new()).expect("gpuArray int");
1470 let Value::GpuTensor(handle) = result else {
1471 panic!("expected gpu tensor");
1472 };
1473 let gathered =
1474 test_support::gather(Value::GpuTensor(handle.clone())).expect("gather int");
1475 assert_eq!(gathered.shape, vec![1, 1]);
1476 assert_eq!(gathered.data, vec![7.0]);
1477 });
1478 }
1479
1480 #[test]
1481 fn gpuarray_type_for_logical_is_logical() {
1482 assert_eq!(
1483 gpuarray_type(&[Type::logical()], &ResolveContext::new(Vec::new())),
1484 Type::logical()
1485 );
1486 }
1487}