1use std::cmp::Ordering;
4use std::collections::BTreeSet;
5
6use runmat_accelerate_api::{AccelProvider, GpuTensorHandle, ReduceDimResult};
7use runmat_builtins::{
8 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
9 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
10 ComplexTensor, ResolveContext, Tensor, Type, Value,
11};
12use runmat_macros::runtime_builtin;
13
14use crate::{build_runtime_error, BuiltinResult, RuntimeError};
15
16const NAME: &str = "max";
17
18fn max_type(args: &[Type], ctx: &ResolveContext) -> Type {
19 min_max_type(args, ctx)
20}
21
22fn nanmax_type(args: &[Type], ctx: &ResolveContext) -> Type {
23 min_max_type(args, ctx)
24}
25
26const MAX_OUTPUT_M: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
27 name: "M",
28 ty: BuiltinParamType::NumericArray,
29 arity: BuiltinParamArity::Required,
30 default: None,
31 description: "Maximum values.",
32}];
33
34const MAX_OUTPUT_MI: [BuiltinParamDescriptor; 2] = [
35 BuiltinParamDescriptor {
36 name: "M",
37 ty: BuiltinParamType::NumericArray,
38 arity: BuiltinParamArity::Required,
39 default: None,
40 description: "Maximum values.",
41 },
42 BuiltinParamDescriptor {
43 name: "I",
44 ty: BuiltinParamType::NumericArray,
45 arity: BuiltinParamArity::Required,
46 default: None,
47 description: "One-based maximum indices/origins.",
48 },
49];
50
51const MAX_PARAM_A: BuiltinParamDescriptor = BuiltinParamDescriptor {
52 name: "A",
53 ty: BuiltinParamType::Any,
54 arity: BuiltinParamArity::Required,
55 default: None,
56 description: "Input scalar or array.",
57};
58
59const MAX_PARAM_B: BuiltinParamDescriptor = BuiltinParamDescriptor {
60 name: "B",
61 ty: BuiltinParamType::Any,
62 arity: BuiltinParamArity::Required,
63 default: None,
64 description: "Second operand for element-wise maximum.",
65};
66
67const MAX_PARAM_EMPTY: BuiltinParamDescriptor = BuiltinParamDescriptor {
68 name: "placeholder",
69 ty: BuiltinParamType::Any,
70 arity: BuiltinParamArity::Optional,
71 default: Some("[]"),
72 description: "Empty placeholder selecting reduction-argument grammar.",
73};
74
75const MAX_PARAM_DIM: BuiltinParamDescriptor = BuiltinParamDescriptor {
76 name: "dim",
77 ty: BuiltinParamType::Any,
78 arity: BuiltinParamArity::Optional,
79 default: Some("[]"),
80 description: "Reduction dimension selector (scalar or dimension vector).",
81};
82
83const MAX_PARAM_REDUCTION_FLAG: BuiltinParamDescriptor = BuiltinParamDescriptor {
84 name: "flag",
85 ty: BuiltinParamType::StringScalar,
86 arity: BuiltinParamArity::Optional,
87 default: Some("\"all\""),
88 description: "Reduction mode flag: \"all\" or \"linear\".",
89};
90
91const MAX_PARAM_NANFLAG: BuiltinParamDescriptor = BuiltinParamDescriptor {
92 name: "nanflag",
93 ty: BuiltinParamType::StringScalar,
94 arity: BuiltinParamArity::Optional,
95 default: Some("\"includenan\""),
96 description: "Missing-value mode: \"includenan\" or \"omitnan\".",
97};
98
99const MAX_PARAM_COMPARISON_NAME: BuiltinParamDescriptor = BuiltinParamDescriptor {
100 name: "optionName",
101 ty: BuiltinParamType::StringScalar,
102 arity: BuiltinParamArity::Optional,
103 default: Some("\"ComparisonMethod\""),
104 description: "Option name (currently \"ComparisonMethod\").",
105};
106
107const MAX_PARAM_COMPARISON_VALUE: BuiltinParamDescriptor = BuiltinParamDescriptor {
108 name: "method",
109 ty: BuiltinParamType::StringScalar,
110 arity: BuiltinParamArity::Optional,
111 default: Some("\"auto\""),
112 description: "Comparison method: \"auto\", \"abs\"/\"magnitude\", or \"real\".",
113};
114
115const MAX_PARAM_OPTION_NAME: BuiltinParamDescriptor = BuiltinParamDescriptor {
116 name: "optionName",
117 ty: BuiltinParamType::StringScalar,
118 arity: BuiltinParamArity::Variadic,
119 default: None,
120 description: "Name-value option name.",
121};
122
123const MAX_PARAM_OPTION_VALUE: BuiltinParamDescriptor = BuiltinParamDescriptor {
124 name: "optionValue",
125 ty: BuiltinParamType::Any,
126 arity: BuiltinParamArity::Variadic,
127 default: None,
128 description: "Name-value option value.",
129};
130
131const MAX_INPUTS_A: [BuiltinParamDescriptor; 1] = [MAX_PARAM_A];
132const MAX_INPUTS_A_B: [BuiltinParamDescriptor; 2] = [MAX_PARAM_A, MAX_PARAM_B];
133const MAX_INPUTS_A_EMPTY_DIM: [BuiltinParamDescriptor; 3] =
134 [MAX_PARAM_A, MAX_PARAM_EMPTY, MAX_PARAM_DIM];
135const MAX_INPUTS_A_EMPTY_FLAG: [BuiltinParamDescriptor; 3] =
136 [MAX_PARAM_A, MAX_PARAM_EMPTY, MAX_PARAM_REDUCTION_FLAG];
137const MAX_INPUTS_A_EMPTY_NANFLAG: [BuiltinParamDescriptor; 3] =
138 [MAX_PARAM_A, MAX_PARAM_EMPTY, MAX_PARAM_NANFLAG];
139const MAX_INPUTS_A_EMPTY_COMPARISON: [BuiltinParamDescriptor; 4] = [
140 MAX_PARAM_A,
141 MAX_PARAM_EMPTY,
142 MAX_PARAM_COMPARISON_NAME,
143 MAX_PARAM_COMPARISON_VALUE,
144];
145const MAX_INPUTS_A_B_COMPARISON: [BuiltinParamDescriptor; 4] = [
146 MAX_PARAM_A,
147 MAX_PARAM_B,
148 MAX_PARAM_COMPARISON_NAME,
149 MAX_PARAM_COMPARISON_VALUE,
150];
151const MAX_INPUTS_A_EMPTY_OPTIONS: [BuiltinParamDescriptor; 4] = [
152 MAX_PARAM_A,
153 MAX_PARAM_EMPTY,
154 MAX_PARAM_OPTION_NAME,
155 MAX_PARAM_OPTION_VALUE,
156];
157const MAX_INPUTS_A_B_OPTIONS: [BuiltinParamDescriptor; 4] = [
158 MAX_PARAM_A,
159 MAX_PARAM_B,
160 MAX_PARAM_OPTION_NAME,
161 MAX_PARAM_OPTION_VALUE,
162];
163
164const NANMAX_INPUTS_A: [BuiltinParamDescriptor; 1] = [MAX_PARAM_A];
165const NANMAX_INPUTS_A_B: [BuiltinParamDescriptor; 2] = [MAX_PARAM_A, MAX_PARAM_B];
166const NANMAX_INPUTS_A_EMPTY_DIM: [BuiltinParamDescriptor; 3] =
167 [MAX_PARAM_A, MAX_PARAM_EMPTY, MAX_PARAM_DIM];
168
169const NANMAX_SIGNATURES: [BuiltinSignatureDescriptor; 16] = [
170 BuiltinSignatureDescriptor {
171 label: "M = nanmax(A)",
172 inputs: &NANMAX_INPUTS_A,
173 outputs: &MAX_OUTPUT_M,
174 },
175 BuiltinSignatureDescriptor {
176 label: "[M, I] = nanmax(A)",
177 inputs: &NANMAX_INPUTS_A,
178 outputs: &MAX_OUTPUT_MI,
179 },
180 BuiltinSignatureDescriptor {
181 label: "M = nanmax(A, B)",
182 inputs: &NANMAX_INPUTS_A_B,
183 outputs: &MAX_OUTPUT_M,
184 },
185 BuiltinSignatureDescriptor {
186 label: "[M, I] = nanmax(A, B)",
187 inputs: &NANMAX_INPUTS_A_B,
188 outputs: &MAX_OUTPUT_MI,
189 },
190 BuiltinSignatureDescriptor {
191 label: "M = nanmax(A, [], dim)",
192 inputs: &NANMAX_INPUTS_A_EMPTY_DIM,
193 outputs: &MAX_OUTPUT_M,
194 },
195 BuiltinSignatureDescriptor {
196 label: "[M, I] = nanmax(A, [], dim)",
197 inputs: &NANMAX_INPUTS_A_EMPTY_DIM,
198 outputs: &MAX_OUTPUT_MI,
199 },
200 BuiltinSignatureDescriptor {
201 label: "M = nanmax(A, [], vecdim)",
202 inputs: &NANMAX_INPUTS_A_EMPTY_DIM,
203 outputs: &MAX_OUTPUT_M,
204 },
205 BuiltinSignatureDescriptor {
206 label: "[M, I] = nanmax(A, [], vecdim)",
207 inputs: &NANMAX_INPUTS_A_EMPTY_DIM,
208 outputs: &MAX_OUTPUT_MI,
209 },
210 BuiltinSignatureDescriptor {
211 label: "M = nanmax(A, [], \"all\")",
212 inputs: &MAX_INPUTS_A_EMPTY_FLAG,
213 outputs: &MAX_OUTPUT_M,
214 },
215 BuiltinSignatureDescriptor {
216 label: "[M, I] = nanmax(A, [], \"all\")",
217 inputs: &MAX_INPUTS_A_EMPTY_FLAG,
218 outputs: &MAX_OUTPUT_MI,
219 },
220 BuiltinSignatureDescriptor {
221 label: "M = nanmax(A, [], \"linear\")",
222 inputs: &MAX_INPUTS_A_EMPTY_FLAG,
223 outputs: &MAX_OUTPUT_M,
224 },
225 BuiltinSignatureDescriptor {
226 label: "[M, I] = nanmax(A, [], \"linear\")",
227 inputs: &MAX_INPUTS_A_EMPTY_FLAG,
228 outputs: &MAX_OUTPUT_MI,
229 },
230 BuiltinSignatureDescriptor {
231 label: "M = nanmax(A, [], \"ComparisonMethod\", method)",
232 inputs: &MAX_INPUTS_A_EMPTY_COMPARISON,
233 outputs: &MAX_OUTPUT_M,
234 },
235 BuiltinSignatureDescriptor {
236 label: "[M, I] = nanmax(A, [], \"ComparisonMethod\", method)",
237 inputs: &MAX_INPUTS_A_EMPTY_COMPARISON,
238 outputs: &MAX_OUTPUT_MI,
239 },
240 BuiltinSignatureDescriptor {
241 label: "M = nanmax(A, B, \"ComparisonMethod\", method)",
242 inputs: &MAX_INPUTS_A_B_COMPARISON,
243 outputs: &MAX_OUTPUT_M,
244 },
245 BuiltinSignatureDescriptor {
246 label: "[M, I] = nanmax(A, B, \"ComparisonMethod\", method)",
247 inputs: &MAX_INPUTS_A_B_COMPARISON,
248 outputs: &MAX_OUTPUT_MI,
249 },
250];
251
252const MAX_SIGNATURES: [BuiltinSignatureDescriptor; 22] = [
253 BuiltinSignatureDescriptor {
254 label: "M = max(A)",
255 inputs: &MAX_INPUTS_A,
256 outputs: &MAX_OUTPUT_M,
257 },
258 BuiltinSignatureDescriptor {
259 label: "[M, I] = max(A)",
260 inputs: &MAX_INPUTS_A,
261 outputs: &MAX_OUTPUT_MI,
262 },
263 BuiltinSignatureDescriptor {
264 label: "M = max(A, B)",
265 inputs: &MAX_INPUTS_A_B,
266 outputs: &MAX_OUTPUT_M,
267 },
268 BuiltinSignatureDescriptor {
269 label: "[M, I] = max(A, B)",
270 inputs: &MAX_INPUTS_A_B,
271 outputs: &MAX_OUTPUT_MI,
272 },
273 BuiltinSignatureDescriptor {
274 label: "M = max(A, [], dim)",
275 inputs: &MAX_INPUTS_A_EMPTY_DIM,
276 outputs: &MAX_OUTPUT_M,
277 },
278 BuiltinSignatureDescriptor {
279 label: "[M, I] = max(A, [], dim)",
280 inputs: &MAX_INPUTS_A_EMPTY_DIM,
281 outputs: &MAX_OUTPUT_MI,
282 },
283 BuiltinSignatureDescriptor {
284 label: "M = max(A, [], vecdim)",
285 inputs: &MAX_INPUTS_A_EMPTY_DIM,
286 outputs: &MAX_OUTPUT_M,
287 },
288 BuiltinSignatureDescriptor {
289 label: "[M, I] = max(A, [], vecdim)",
290 inputs: &MAX_INPUTS_A_EMPTY_DIM,
291 outputs: &MAX_OUTPUT_MI,
292 },
293 BuiltinSignatureDescriptor {
294 label: "M = max(A, [], \"all\")",
295 inputs: &MAX_INPUTS_A_EMPTY_FLAG,
296 outputs: &MAX_OUTPUT_M,
297 },
298 BuiltinSignatureDescriptor {
299 label: "[M, I] = max(A, [], \"all\")",
300 inputs: &MAX_INPUTS_A_EMPTY_FLAG,
301 outputs: &MAX_OUTPUT_MI,
302 },
303 BuiltinSignatureDescriptor {
304 label: "M = max(A, [], \"linear\")",
305 inputs: &MAX_INPUTS_A_EMPTY_FLAG,
306 outputs: &MAX_OUTPUT_M,
307 },
308 BuiltinSignatureDescriptor {
309 label: "[M, I] = max(A, [], \"linear\")",
310 inputs: &MAX_INPUTS_A_EMPTY_FLAG,
311 outputs: &MAX_OUTPUT_MI,
312 },
313 BuiltinSignatureDescriptor {
314 label: "M = max(A, [], nanflag)",
315 inputs: &MAX_INPUTS_A_EMPTY_NANFLAG,
316 outputs: &MAX_OUTPUT_M,
317 },
318 BuiltinSignatureDescriptor {
319 label: "[M, I] = max(A, [], nanflag)",
320 inputs: &MAX_INPUTS_A_EMPTY_NANFLAG,
321 outputs: &MAX_OUTPUT_MI,
322 },
323 BuiltinSignatureDescriptor {
324 label: "M = max(A, [], \"ComparisonMethod\", method)",
325 inputs: &MAX_INPUTS_A_EMPTY_COMPARISON,
326 outputs: &MAX_OUTPUT_M,
327 },
328 BuiltinSignatureDescriptor {
329 label: "[M, I] = max(A, [], \"ComparisonMethod\", method)",
330 inputs: &MAX_INPUTS_A_EMPTY_COMPARISON,
331 outputs: &MAX_OUTPUT_MI,
332 },
333 BuiltinSignatureDescriptor {
334 label: "M = max(A, B, \"ComparisonMethod\", method)",
335 inputs: &MAX_INPUTS_A_B_COMPARISON,
336 outputs: &MAX_OUTPUT_M,
337 },
338 BuiltinSignatureDescriptor {
339 label: "[M, I] = max(A, B, \"ComparisonMethod\", method)",
340 inputs: &MAX_INPUTS_A_B_COMPARISON,
341 outputs: &MAX_OUTPUT_MI,
342 },
343 BuiltinSignatureDescriptor {
344 label: "M = max(A, [], optionName, optionValue, ...)",
345 inputs: &MAX_INPUTS_A_EMPTY_OPTIONS,
346 outputs: &MAX_OUTPUT_M,
347 },
348 BuiltinSignatureDescriptor {
349 label: "[M, I] = max(A, [], optionName, optionValue, ...)",
350 inputs: &MAX_INPUTS_A_EMPTY_OPTIONS,
351 outputs: &MAX_OUTPUT_MI,
352 },
353 BuiltinSignatureDescriptor {
354 label: "M = max(A, B, optionName, optionValue, ...)",
355 inputs: &MAX_INPUTS_A_B_OPTIONS,
356 outputs: &MAX_OUTPUT_M,
357 },
358 BuiltinSignatureDescriptor {
359 label: "[M, I] = max(A, B, optionName, optionValue, ...)",
360 inputs: &MAX_INPUTS_A_B_OPTIONS,
361 outputs: &MAX_OUTPUT_MI,
362 },
363];
364
365const MAX_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
366 code: "RM.MAX.INVALID_ARGUMENT",
367 identifier: Some("RunMat:max:InvalidArgument"),
368 when: "Argument grammar, dimensions, or option names/values are invalid.",
369 message: "max: invalid argument",
370};
371
372const MAX_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
373 code: "RM.MAX.INVALID_INPUT",
374 identifier: Some("RunMat:max:InvalidInput"),
375 when: "Input values cannot be converted to supported max domains.",
376 message: "max: invalid input",
377};
378
379const MAX_ERROR_SIZE_MISMATCH: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
380 code: "RM.MAX.SIZE_MISMATCH",
381 identifier: Some("RunMat:max:SizeMismatch"),
382 when: "Element-wise operands are not broadcast-compatible.",
383 message: "max: size mismatch",
384};
385
386const MAX_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
387 code: "RM.MAX.INTERNAL",
388 identifier: Some("RunMat:max:Internal"),
389 when: "Execution fails due to gather, provider, allocation, or conversion internals.",
390 message: "max: internal failure",
391};
392
393const MAX_ERRORS: [BuiltinErrorDescriptor; 4] = [
394 MAX_ERROR_INVALID_ARGUMENT,
395 MAX_ERROR_INVALID_INPUT,
396 MAX_ERROR_SIZE_MISMATCH,
397 MAX_ERROR_INTERNAL,
398];
399
400pub const MAX_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
401 signatures: &MAX_SIGNATURES,
402 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
403 completion_policy: BuiltinCompletionPolicy::Public,
404 errors: &MAX_ERRORS,
405};
406
407pub const NANMAX_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
408 signatures: &NANMAX_SIGNATURES,
409 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
410 completion_policy: BuiltinCompletionPolicy::Public,
411 errors: &MAX_ERRORS,
412};
413
414fn max_descriptor_error_with_message(
415 message: impl Into<String>,
416 error: &'static BuiltinErrorDescriptor,
417) -> RuntimeError {
418 let mut builder = build_runtime_error(message).with_builtin(NAME);
419 if let Some(identifier) = error.identifier {
420 builder = builder.with_identifier(identifier);
421 }
422 builder.build()
423}
424
425fn max_descriptor_error_with_detail(
426 error: &'static BuiltinErrorDescriptor,
427 detail: impl AsRef<str>,
428) -> RuntimeError {
429 max_descriptor_error_with_message(format!("{}: {}", error.message, detail.as_ref()), error)
430}
431
432fn max_invalid_argument(detail: impl AsRef<str>) -> RuntimeError {
433 max_descriptor_error_with_detail(&MAX_ERROR_INVALID_ARGUMENT, detail)
434}
435
436fn max_invalid_input(detail: impl AsRef<str>) -> RuntimeError {
437 max_descriptor_error_with_detail(&MAX_ERROR_INVALID_INPUT, detail)
438}
439
440fn max_size_mismatch(detail: impl AsRef<str>) -> RuntimeError {
441 max_descriptor_error_with_detail(&MAX_ERROR_SIZE_MISMATCH, detail)
442}
443
444fn max_internal_error(detail: impl AsRef<str>) -> RuntimeError {
445 max_descriptor_error_with_detail(&MAX_ERROR_INTERNAL, detail)
446}
447
448use crate::builtins::common::arg_tokens::tokens_from_values;
449use crate::builtins::common::broadcast::BroadcastPlan;
450use crate::builtins::common::random_args::{complex_tensor_into_value, keyword_of};
451use crate::builtins::common::spec::{
452 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, FusionError,
453 FusionExprContext, FusionKernelTemplate, GpuOpKind, ProviderHook, ReductionNaN,
454 ResidencyPolicy, ScalarType, ShapeRequirements,
455};
456use crate::builtins::common::{
457 gpu_helpers,
458 shape::{is_scalar_shape, normalize_scalar_shape},
459 tensor,
460};
461use crate::builtins::math::reduction::type_resolvers::min_max_type;
462
463#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::reduction::max")]
464pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
465 name: "max",
466 op_kind: GpuOpKind::Reduction,
467 supported_precisions: &[ScalarType::F32, ScalarType::F64],
468 broadcast: BroadcastSemantics::Matlab,
469 provider_hooks: &[
470 ProviderHook::Reduction {
471 name: "reduce_max_dim",
472 },
473 ProviderHook::Reduction {
474 name: "reduce_max",
475 },
476 ],
477 constant_strategy: ConstantStrategy::InlineLiteral,
478 residency: ResidencyPolicy::NewHandle,
479 nan_mode: ReductionNaN::Include,
480 two_pass_threshold: Some(256),
481 workgroup_size: Some(256),
482 accepts_nan_mode: false,
483 notes:
484 "Providers should implement reduce_max_dim / reduce_max. Requests that require omitnan, comparisonmethod overrides, or complex inputs fall back to the host implementation.",
485};
486
487#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::reduction::max")]
488pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
489 name: "max",
490 shape: ShapeRequirements::BroadcastCompatible,
491 constant_strategy: ConstantStrategy::InlineLiteral,
492 elementwise: None,
493 reduction: Some(FusionKernelTemplate {
494 scalar_precisions: &[ScalarType::F32, ScalarType::F64],
495 wgsl_body: |ctx: &FusionExprContext| {
496 let input = ctx.inputs.first().ok_or(FusionError::MissingInput(0))?;
497 Ok(format!("accumulator = max(accumulator, {input});"))
498 },
499 }),
500 emits_nan: true,
501 notes: "Fusion planner emits canonical reduction kernels; providers may substitute custom WGSL via reduce_max_dim hooks.",
502};
503
504#[derive(Debug, Clone)]
506pub struct MaxEvaluation {
507 values: Value,
508 indices: Value,
509}
510
511impl MaxEvaluation {
512 pub fn into_value(self) -> Value {
514 self.values
515 }
516
517 pub fn into_pair(self) -> (Value, Value) {
519 (self.values, self.indices)
520 }
521
522 pub fn indices_value(&self) -> Value {
524 self.indices.clone()
525 }
526}
527
528#[runtime_builtin(
529 name = "max",
530 category = "math/reduction",
531 summary = "Return maximum elements along dimensions or pairwise comparisons.",
532 keywords = "max,maximum,reduction,gpu,comparisonmethod,omitnan",
533 accel = "reduction",
534 type_resolver(max_type),
535 descriptor(crate::builtins::math::reduction::max::MAX_DESCRIPTOR),
536 builtin_path = "crate::builtins::math::reduction::max"
537)]
538async fn max_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
539 if let Some(eval) = crate::builtins::table::categorical_max_evaluate(&value, &rest).await {
540 return crate::builtins::table::categorical_extrema_to_value(eval?);
541 }
542 let eval = evaluate(value, &rest).await?;
543 evaluation_to_value(eval)
544}
545
546#[runtime_builtin(
547 name = "nanmax",
548 category = "stats/summary",
549 summary = "Return maximum values while omitting NaNs.",
550 keywords = "nanmax,max,maximum,omitnan,statistics",
551 type_resolver(nanmax_type),
552 descriptor(crate::builtins::math::reduction::max::NANMAX_DESCRIPTOR),
553 builtin_path = "crate::builtins::math::reduction::max"
554)]
555async fn nanmax_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
556 let adjusted = nanmax_rest(rest);
557 let eval = evaluate(value, &adjusted).await?;
558 evaluation_to_value(eval)
559}
560
561fn evaluation_to_value(eval: MaxEvaluation) -> BuiltinResult<Value> {
562 if let Some(out_count) = crate::output_count::current_output_count() {
563 if out_count == 0 {
564 return Ok(Value::OutputList(Vec::new()));
565 }
566 if out_count == 1 {
567 return Ok(Value::OutputList(vec![eval.into_value()]));
568 }
569 let (values, indices) = eval.into_pair();
570 return Ok(crate::output_count::output_list_with_padding(
571 out_count,
572 vec![values, indices],
573 ));
574 }
575 Ok(eval.into_value())
576}
577
578fn nanmax_rest(rest: Vec<Value>) -> Vec<Value> {
579 let mut args = Vec::with_capacity(rest.len() + 2);
580 if rest.is_empty() {
581 args.push(empty_placeholder());
582 }
583 args.extend(rest);
584 args.push(Value::from("omitnan"));
585 args
586}
587
588fn empty_placeholder() -> Value {
589 Value::Tensor(Tensor::new(Vec::<f64>::new(), vec![0, 0]).expect("empty placeholder shape"))
590}
591
592pub async fn evaluate(value: Value, rest: &[Value]) -> BuiltinResult<MaxEvaluation> {
594 let parsed = parse_call(rest).await?;
595 if std::env::var("RUNMAT_DEBUG_MAX").is_ok() {
596 let call_label = match &parsed {
597 ParsedCall::Reduction(_) => "reduction",
598 ParsedCall::Elementwise(_) => "elementwise",
599 };
600 let first_arg = rest.first().map(debug_value_kind).unwrap_or("None");
601 tracing::debug!(
602 call_type = call_label,
603 rest_len = rest.len(),
604 first_arg = first_arg,
605 "[runmat-debug-max]"
606 );
607 }
608 match parsed {
609 ParsedCall::Elementwise(args) => elementwise_max(value, args).await,
610 ParsedCall::Reduction(args) => reduction_max(value, args).await,
611 }
612}
613
614#[derive(Debug, Clone)]
615enum ParsedCall {
616 Reduction(ReductionArgs),
617 Elementwise(ElementwiseArgs),
618}
619
620#[derive(Debug, Clone)]
621struct ReductionArgs {
622 selection: DimSelection,
623 nan_mode: ReductionNaN,
624 comparison: ComparisonMethod,
625 linear_index: bool,
626}
627
628impl Default for ReductionArgs {
629 fn default() -> Self {
630 Self {
631 selection: DimSelection::Auto,
632 nan_mode: ReductionNaN::Include,
633 comparison: ComparisonMethod::Auto,
634 linear_index: false,
635 }
636 }
637}
638
639#[derive(Debug, Clone)]
640enum DimSelection {
641 Auto,
642 Dim(usize),
643 Vec(Vec<usize>),
644 All,
645}
646
647#[derive(Debug, Clone, Copy, PartialEq, Eq)]
648enum ComparisonMethod {
649 Auto,
650 Real,
651 Abs,
652}
653
654#[derive(Debug, Clone)]
655struct ElementwiseArgs {
656 other: Value,
657 nan_mode: ReductionNaN,
658 comparison: ComparisonMethod,
659}
660
661async fn parse_call(rest: &[Value]) -> BuiltinResult<ParsedCall> {
662 if rest.is_empty() {
663 return Ok(ParsedCall::Reduction(ReductionArgs::default()));
664 }
665
666 let first = &rest[0];
667 if !is_empty_placeholder(first) {
668 let (nan_mode, comparison) = parse_elementwise_options(&rest[1..])?;
669 return Ok(ParsedCall::Elementwise(ElementwiseArgs {
670 other: first.clone(),
671 nan_mode,
672 comparison,
673 }));
674 }
675
676 let mut args = ReductionArgs::default();
677 parse_reduction_options(&mut args, &rest[1..]).await?;
678 Ok(ParsedCall::Reduction(args))
679}
680
681fn debug_value_kind(value: &Value) -> &'static str {
682 match value {
683 Value::Num(_) => "Num",
684 Value::Int(_) => "Int",
685 Value::Bool(_) => "Bool",
686 Value::Tensor(t) => {
687 if t.data.is_empty() {
688 "Tensor(empty)"
689 } else {
690 "Tensor"
691 }
692 }
693 Value::GpuTensor(_) => "GpuTensor",
694 Value::String(_) => "String",
695 Value::CharArray(_) => "CharArray",
696 Value::StringArray(sa) => {
697 if sa.data.is_empty() {
698 "StringArray(empty)"
699 } else {
700 "StringArray"
701 }
702 }
703 Value::LogicalArray(l) => {
704 if l.data.is_empty() {
705 "LogicalArray(empty)"
706 } else {
707 "LogicalArray"
708 }
709 }
710 Value::Cell(c) => {
711 if c.data.is_empty() {
712 "Cell(empty)"
713 } else {
714 "Cell"
715 }
716 }
717 _ => "Other",
718 }
719}
720
721fn is_empty_placeholder(value: &Value) -> bool {
722 match value {
723 Value::Tensor(t) => t.data.is_empty(),
724 Value::LogicalArray(l) => l.data.is_empty(),
725 Value::StringArray(sa) => sa.data.is_empty(),
726 Value::CharArray(ca) => ca.data.is_empty(),
727 Value::Cell(cell) => cell.data.is_empty(),
728 Value::String(s) => s.is_empty(),
729 _ => false,
730 }
731}
732
733async fn parse_reduction_options(args: &mut ReductionArgs, rest: &[Value]) -> BuiltinResult<()> {
734 let mut idx = 0usize;
735 let mut selection_set = !matches!(args.selection, DimSelection::Auto);
736 let mut comparison_set = matches!(args.comparison, ComparisonMethod::Auto);
737 let tokens = tokens_from_values(rest);
738 while idx < rest.len() {
739 if let Some(crate::builtins::common::arg_tokens::ArgToken::String(text)) = tokens.get(idx) {
740 match text.as_str() {
741 "omitnan" => {
742 args.nan_mode = ReductionNaN::Omit;
743 idx += 1;
744 continue;
745 }
746 "includenan" => {
747 args.nan_mode = ReductionNaN::Include;
748 idx += 1;
749 continue;
750 }
751 "all" => {
752 if selection_set {
753 return Err(max_invalid_argument(
754 "max: 'all' cannot be combined with an explicit dimension",
755 ));
756 }
757 args.selection = DimSelection::All;
758 selection_set = true;
759 idx += 1;
760 continue;
761 }
762 _ => {}
763 }
764 }
765 if let Some(keyword) = keyword_of(&rest[idx]) {
766 match keyword.as_str() {
767 "omitnan" => {
768 args.nan_mode = ReductionNaN::Omit;
769 idx += 1;
770 continue;
771 }
772 "includenan" => {
773 args.nan_mode = ReductionNaN::Include;
774 idx += 1;
775 continue;
776 }
777 "all" => {
778 if selection_set {
779 return Err(max_invalid_argument(
780 "max: 'all' cannot be combined with an explicit dimension",
781 ));
782 }
783 args.selection = DimSelection::All;
784 selection_set = true;
785 idx += 1;
786 continue;
787 }
788 "linear" => {
789 if selection_set {
790 return Err(max_invalid_argument(
791 "max: 'linear' cannot be combined with an explicit dimension",
792 ));
793 }
794 args.selection = DimSelection::All;
795 args.linear_index = true;
796 selection_set = true;
797 idx += 1;
798 continue;
799 }
800 "comparisonmethod" => {
801 let Some(value) = rest.get(idx + 1) else {
802 return Err(max_invalid_argument(
803 "max: expected a value after 'ComparisonMethod'",
804 ));
805 };
806 args.comparison = parse_comparison_method(value)?;
807 comparison_set = true;
808 idx += 2;
809 continue;
810 }
811 _ => {}
812 }
813 }
814
815 if !selection_set {
816 if let Some(selection) = parse_dimension_value(&rest[idx]).await? {
817 args.selection = selection;
818 selection_set = true;
819 idx += 1;
820 continue;
821 }
822 }
823
824 return Err(max_invalid_argument(format!(
825 "max: unrecognised argument {:?}",
826 rest[idx]
827 )));
828 }
829
830 if !comparison_set {
831 args.comparison = ComparisonMethod::Auto;
832 }
833
834 Ok(())
835}
836
837fn parse_elementwise_options(rest: &[Value]) -> BuiltinResult<(ReductionNaN, ComparisonMethod)> {
838 let mut nan_mode = ReductionNaN::Include;
839 let mut comparison = ComparisonMethod::Auto;
840 let mut comparison_set = false;
841 let mut idx = 0usize;
842 while idx < rest.len() {
843 if let Some(keyword) = keyword_of(&rest[idx]) {
844 match keyword.as_str() {
845 "omitnan" => {
846 nan_mode = ReductionNaN::Omit;
847 idx += 1;
848 continue;
849 }
850 "includenan" => {
851 nan_mode = ReductionNaN::Include;
852 idx += 1;
853 continue;
854 }
855 "comparisonmethod" => {
856 let Some(value) = rest.get(idx + 1) else {
857 return Err(max_invalid_argument(
858 "max: expected a value after 'ComparisonMethod'",
859 ));
860 };
861 comparison = parse_comparison_method(value)?;
862 comparison_set = true;
863 idx += 2;
864 continue;
865 }
866 "all" | "linear" => {
867 return Err(max_invalid_argument(format!(
868 "max: '{}' is only supported for reduction calls",
869 keyword
870 )));
871 }
872 _ => {}
873 }
874 }
875 return Err(max_invalid_argument(format!(
876 "max: unrecognised argument {:?}",
877 rest[idx]
878 )));
879 }
880 if !comparison_set {
881 comparison = ComparisonMethod::Auto;
882 }
883 Ok((nan_mode, comparison))
884}
885
886fn parse_comparison_method(value: &Value) -> BuiltinResult<ComparisonMethod> {
887 let Some(keyword) = keyword_of(value) else {
888 return Err(max_invalid_argument(
889 "max: 'ComparisonMethod' expects a string value",
890 ));
891 };
892 match keyword.as_str() {
893 "auto" => Ok(ComparisonMethod::Auto),
894 "abs" | "magnitude" => Ok(ComparisonMethod::Abs),
895 "real" => Ok(ComparisonMethod::Real),
896 other => Err(max_invalid_argument(format!(
897 "max: unsupported ComparisonMethod '{other}'"
898 ))),
899 }
900}
901
902async fn parse_dimension_value(value: &Value) -> BuiltinResult<Option<DimSelection>> {
903 match value {
904 Value::Int(_) | Value::Num(_) => tensor::dimension_from_value_async(value, "max", false)
905 .await
906 .map_err(map_scalar_dim_error)
907 .map(|dim| dim.map(DimSelection::Dim)),
908 Value::Tensor(t) => parse_dimension_tensor(value, &t.shape).await,
909 Value::LogicalArray(logical) => parse_dimension_tensor(value, &logical.shape).await,
910 Value::GpuTensor(_) => Err(max_invalid_argument(
911 "max: dimension arguments must reside on the host",
912 )),
913 _ => Ok(None),
914 }
915}
916
917async fn parse_dimension_tensor(
918 value: &Value,
919 shape: &[usize],
920) -> BuiltinResult<Option<DimSelection>> {
921 if tensor::element_count(shape) == 0 {
922 return Ok(Some(DimSelection::Auto));
923 }
924 let is_vector = shape.len() == 1
925 || shape.get(0).copied().unwrap_or(1) == 1
926 || shape.get(1).copied().unwrap_or(1) == 1;
927 if !is_vector {
928 return Err(max_invalid_argument(
929 "max: dimension vector must be a row or column vector",
930 ));
931 }
932 let dims = tensor::dims_from_value_async(value)
933 .await
934 .map_err(map_vector_dim_error)?;
935 let Some(dims) = dims else {
936 return Ok(None);
937 };
938 if dims.is_empty() {
939 return Ok(Some(DimSelection::Auto));
940 }
941 let mut seen = BTreeSet::new();
942 let mut uniq = Vec::with_capacity(dims.len());
943 for dim in dims {
944 if dim < 1 {
945 return Err(max_invalid_argument("max: dimension indices must be >= 1"));
946 }
947 if seen.insert(dim) {
948 uniq.push(dim);
949 }
950 }
951 Ok(Some(DimSelection::Vec(uniq)))
952}
953
954fn map_scalar_dim_error(message: String) -> RuntimeError {
955 if message.contains("integer") {
956 return max_invalid_argument("max: dimension must be integral");
957 }
958 max_invalid_argument(message)
959}
960
961fn map_vector_dim_error(message: String) -> RuntimeError {
962 if message.contains("non-negative") {
963 return max_invalid_argument("max: dimension indices must be >= 1");
964 }
965 if message.contains("finite") {
966 return max_invalid_argument("max: dimension entries must be finite");
967 }
968 if message.contains("integer") {
969 return max_invalid_argument("max: dimension entries must be integers");
970 }
971 max_invalid_argument(message)
972}
973
974async fn reduction_max(value: Value, args: ReductionArgs) -> BuiltinResult<MaxEvaluation> {
975 match value {
976 Value::GpuTensor(handle) => {
977 if let Some(eval) = reduction_max_gpu(handle.clone(), &args).await? {
978 return Ok(eval);
979 }
980 let tensor = gpu_helpers::gather_tensor_async(&handle)
982 .await
983 .map_err(|e| max_internal_error(format!("max: {e}")))?;
984 reduction_max_host(Value::Tensor(tensor), &args)
985 }
986 other => reduction_max_host(other, &args),
987 }
988}
989
990async fn reduction_max_gpu(
991 handle: GpuTensorHandle,
992 args: &ReductionArgs,
993) -> BuiltinResult<Option<MaxEvaluation>> {
994 if args.nan_mode == ReductionNaN::Omit {
995 log::trace!("max: gpu path disabled (nan_mode=omit)");
996 return Ok(None);
997 }
998 if args.comparison != ComparisonMethod::Auto {
999 log::trace!("max: gpu path disabled (comparison != auto)");
1000 return Ok(None);
1001 }
1002 if args.linear_index {
1003 log::trace!("max: gpu path disabled (linear_index=true)");
1004 return Ok(None);
1005 }
1006 let provider = match runmat_accelerate_api::provider() {
1007 Some(p) => p,
1008 None => {
1009 log::trace!(
1010 "max: gpu path unavailable (provider() is None) handle_shape={:?} device_id={}",
1011 handle.shape,
1012 handle.device_id
1013 );
1014 return Ok(None);
1015 }
1016 };
1017 let target_dim = match args.selection {
1018 DimSelection::Auto => default_dimension_from_shape(&handle.shape),
1019 DimSelection::Dim(dim) => dim,
1020 DimSelection::Vec(ref dims) if dims.len() == 1 => dims[0],
1021 DimSelection::All => {
1022 if handle.shape.len() <= 1 {
1023 1
1024 } else {
1025 return Ok(None);
1026 }
1027 }
1028 _ => return Ok(None),
1029 };
1030 if target_dim == 0 {
1031 return Ok(None);
1032 }
1033 let zero_based = target_dim.saturating_sub(1);
1035 if zero_based >= handle.shape.len() {
1036 return Ok(None);
1037 }
1038 log::trace!(
1039 "max: attempting reduce_max_dim dim={} (zero_based={}) shape={:?} device_id={}",
1040 target_dim,
1041 zero_based,
1042 handle.shape,
1043 handle.device_id
1044 );
1045 match provider.reduce_max_dim(&handle, zero_based).await {
1046 Ok(ReduceDimResult { values, indices }) => Ok(Some(MaxEvaluation {
1047 values: Value::GpuTensor(values),
1048 indices: Value::GpuTensor(indices),
1049 })),
1050 Err(err) => {
1051 log::trace!("max: reduce_max_dim failed: {err}");
1052 Ok(None)
1053 }
1054 }
1055}
1056
1057fn reduction_max_host(value: Value, args: &ReductionArgs) -> BuiltinResult<MaxEvaluation> {
1058 if let Value::Int(value) = &value {
1059 let storage = crate::builtins::math::reduction::integer_native::storage_from_scalar(value);
1060 return reduce_integer_max(&storage, vec![1, 1], args);
1061 }
1062 if let Some((storage, shape)) = native_integer_input(&value) {
1063 return reduce_integer_max(storage, shape, args);
1064 }
1065 match materialize_for_max("max", value)? {
1066 InputData::Real(tensor) => reduce_real_tensor(tensor, args),
1067 InputData::Complex(tensor) => reduce_complex_tensor(tensor, args),
1068 }
1069}
1070
1071fn native_integer_input(value: &Value) -> Option<(&runmat_builtins::IntegerStorage, Vec<usize>)> {
1072 match value {
1073 Value::Tensor(tensor) => tensor
1074 .integer_storage()
1075 .map(|storage| (storage, tensor.shape.clone())),
1076 _ => None,
1077 }
1078}
1079
1080fn reduce_integer_max(
1081 storage: &runmat_builtins::IntegerStorage,
1082 shape: Vec<usize>,
1083 args: &ReductionArgs,
1084) -> BuiltinResult<MaxEvaluation> {
1085 if storage.is_empty() {
1086 let output_shape = shape;
1090 let values = crate::builtins::math::reduction::integer_native::empty_like(
1091 storage,
1092 output_shape.clone(),
1093 )
1094 .map_err(|error| max_internal_error(format!("max: {error}")))?;
1095 let indices = Tensor::new(Vec::new(), output_shape)
1096 .map_err(|error| max_internal_error(format!("max: {error}")))?;
1097 return Ok(MaxEvaluation {
1098 values,
1099 indices: tensor::tensor_into_value(indices),
1100 });
1101 }
1102
1103 let resolved = resolve_reduction_dims(&shape, &args.selection)?;
1104 let comparison = match args.comparison {
1105 ComparisonMethod::Auto | ComparisonMethod::Real => {
1106 crate::builtins::math::reduction::integer_native::ExtremaComparison::Natural
1107 }
1108 ComparisonMethod::Abs => {
1109 crate::builtins::math::reduction::integer_native::ExtremaComparison::Absolute
1110 }
1111 };
1112 let extrema = crate::builtins::math::reduction::integer_native::extrema(
1113 storage,
1114 &shape,
1115 resolved.output_shape,
1116 &resolved.reduced_dims,
1117 &resolved.dims_mask,
1118 &resolved.reduce_strides,
1119 resolved.reduce_all,
1120 args.linear_index,
1121 crate::builtins::math::reduction::integer_native::ExtremaDirection::Max,
1122 comparison,
1123 )
1124 .map_err(|error| max_internal_error(format!("max: {error}")))?;
1125 Ok(MaxEvaluation {
1126 values: extrema.values,
1127 indices: extrema.indices,
1128 })
1129}
1130
1131enum InputData {
1132 Real(Tensor),
1133 Complex(ComplexTensor),
1134}
1135
1136fn materialize_for_max(name: &str, value: Value) -> BuiltinResult<InputData> {
1137 match value {
1138 Value::Tensor(t) => Ok(InputData::Real(t)),
1139 Value::LogicalArray(logical) => {
1140 let tensor = tensor::logical_to_tensor(&logical).map_err(max_invalid_input)?;
1141 Ok(InputData::Real(tensor))
1142 }
1143 Value::Num(n) => {
1144 let tensor = Tensor::new(vec![n], vec![1, 1])
1145 .map_err(|e| max_internal_error(format!("{name}: {e}")))?;
1146 Ok(InputData::Real(tensor))
1147 }
1148 Value::Int(i) => {
1149 let tensor = Tensor::new(vec![i.to_f64()], vec![1, 1])
1150 .map_err(|e| max_internal_error(format!("{name}: {e}")))?;
1151 Ok(InputData::Real(tensor))
1152 }
1153 Value::Bool(b) => {
1154 let tensor = Tensor::new(vec![if b { 1.0 } else { 0.0 }], vec![1, 1])
1155 .map_err(|e| max_internal_error(format!("{name}: {e}")))?;
1156 Ok(InputData::Real(tensor))
1157 }
1158 Value::Complex(re, im) => {
1159 let tensor = ComplexTensor::new(vec![(re, im)], vec![1, 1])
1160 .map_err(|e| max_internal_error(format!("{name}: {e}")))?;
1161 Ok(InputData::Complex(tensor))
1162 }
1163 Value::ComplexTensor(ct) => Ok(InputData::Complex(ct)),
1164 Value::String(_)
1165 | Value::StringArray(_)
1166 | Value::CharArray(_)
1167 | Value::SparseTensor(_)
1168 | Value::Symbolic(_)
1169 | Value::SymbolicArray(_)
1170 | Value::Cell(_) => Err(max_invalid_input(format!(
1171 "{name}: expected numeric or logical input, received non-numeric value"
1172 ))),
1173 Value::GpuTensor(_) => Err(max_internal_error(format!(
1174 "{name}: internal error – GPU tensors must be gathered before host execution"
1175 ))),
1176 Value::Object(_) | Value::HandleObject(_) | Value::Struct(_) | Value::Listener(_) => {
1177 Err(max_invalid_input(format!("{name}: unsupported input type")))
1178 }
1179 Value::FunctionHandle(_)
1180 | Value::ExternalFunctionHandle(_)
1181 | Value::MethodFunctionHandle(_)
1182 | Value::BoundFunctionHandle { .. }
1183 | Value::Closure(_)
1184 | Value::ClassRef(_)
1185 | Value::MException(_)
1186 | Value::OutputList(_) => Err(max_invalid_input(format!("{name}: unsupported input type"))),
1187 }
1188}
1189
1190fn reduce_real_tensor(tensor: Tensor, args: &ReductionArgs) -> BuiltinResult<MaxEvaluation> {
1191 let shape = tensor.shape.clone();
1192 if tensor.data.is_empty() {
1193 let output_shape = resolve_output_shape(&shape, &args.selection, &[])?;
1194 let values = Tensor::new(Vec::new(), output_shape.clone())
1195 .map_err(|e| max_internal_error(format!("max: {e}")))?;
1196 let indices = Tensor::new(Vec::new(), output_shape)
1197 .map_err(|e| max_internal_error(format!("max: {e}")))?;
1198 return Ok(MaxEvaluation {
1199 values: tensor::tensor_into_value(values),
1200 indices: tensor::tensor_into_value(indices),
1201 });
1202 }
1203 let resolved = resolve_reduction_dims(&shape, &args.selection)?;
1204 let output_shape = resolved.output_shape.clone();
1205 let output_len = tensor::element_count(&output_shape);
1206
1207 if output_len == 0 {
1208 let values = Tensor::new(Vec::new(), output_shape.clone())
1209 .map_err(|e| max_internal_error(format!("max: {e}")))?;
1210 let indices = Tensor::new(Vec::new(), output_shape)
1211 .map_err(|e| max_internal_error(format!("max: {e}")))?;
1212 return Ok(MaxEvaluation {
1213 values: tensor::tensor_into_value(values),
1214 indices: tensor::tensor_into_value(indices),
1215 });
1216 }
1217
1218 let strides = compute_strides(&shape);
1219 let output_strides = compute_strides(&output_shape);
1220 let dims_mask = resolved.dims_mask.clone();
1221 let reduce_strides = resolved.reduce_strides.clone();
1222
1223 let mut best = vec![BestReal::new(); output_len];
1224 let mut coords = vec![0usize; shape.len()];
1225 for &value in &tensor.data {
1226 let out_idx = map_output_index(&coords, &output_strides, &dims_mask);
1227 let reduce_idx = map_reduce_index(
1228 &coords,
1229 &resolved.reduced_dims,
1230 &reduce_strides,
1231 resolved.reduce_all,
1232 );
1233 let full_idx = map_linear_index(&coords, &strides);
1234
1235 update_best_real(
1236 &mut best[out_idx],
1237 value,
1238 reduce_idx,
1239 full_idx,
1240 args.nan_mode,
1241 args.comparison,
1242 );
1243 increment_coords(&mut coords, &shape);
1244 }
1245
1246 let mut values = vec![0.0f64; output_len];
1247 let mut indices = vec![0.0f64; output_len];
1248
1249 for (i, entry) in best.iter().enumerate() {
1250 if entry.nan_fixed {
1251 values[i] = f64::NAN;
1252 indices[i] = if args.linear_index || resolved.reduce_all {
1253 (entry.full_index + 1) as f64
1254 } else if resolved.reduced_dims.is_empty() {
1255 1.0
1256 } else {
1257 (entry.reduce_index + 1) as f64
1258 };
1259 continue;
1260 }
1261 if !entry.has_value {
1262 values[i] = f64::NAN;
1263 indices[i] = f64::NAN;
1264 continue;
1265 }
1266 values[i] = entry.value;
1267 indices[i] = if args.linear_index || resolved.reduce_all {
1268 (entry.full_index + 1) as f64
1269 } else if resolved.reduced_dims.is_empty() {
1270 1.0
1271 } else {
1272 (entry.reduce_index + 1) as f64
1273 };
1274 }
1275
1276 let value_tensor = Tensor::new(values, output_shape.clone())
1277 .map_err(|e| max_internal_error(format!("max: {e}")))?;
1278 let index_tensor =
1279 Tensor::new(indices, output_shape).map_err(|e| max_internal_error(format!("max: {e}")))?;
1280
1281 Ok(MaxEvaluation {
1282 values: tensor::tensor_into_value(value_tensor),
1283 indices: tensor::tensor_into_value(index_tensor),
1284 })
1285}
1286
1287fn reduce_complex_tensor(
1288 tensor: ComplexTensor,
1289 args: &ReductionArgs,
1290) -> BuiltinResult<MaxEvaluation> {
1291 let shape = tensor.shape.clone();
1292 if tensor.data.is_empty() {
1293 let output_shape = resolve_output_shape(&shape, &args.selection, &[])?;
1294 let values = ComplexTensor::new(Vec::new(), output_shape.clone())
1295 .map_err(|e| max_internal_error(format!("max: {e}")))?;
1296 let indices = Tensor::new(Vec::new(), output_shape)
1297 .map_err(|e| max_internal_error(format!("max: {e}")))?;
1298 return Ok(MaxEvaluation {
1299 values: complex_tensor_into_value(values),
1300 indices: tensor::tensor_into_value(indices),
1301 });
1302 }
1303
1304 let resolved = resolve_reduction_dims(&shape, &args.selection)?;
1305 let output_shape = resolved.output_shape.clone();
1306 let output_len = tensor::element_count(&output_shape);
1307
1308 if output_len == 0 {
1309 let values = ComplexTensor::new(Vec::new(), output_shape.clone())
1310 .map_err(|e| max_internal_error(format!("max: {e}")))?;
1311 let indices = Tensor::new(Vec::new(), output_shape)
1312 .map_err(|e| max_internal_error(format!("max: {e}")))?;
1313 return Ok(MaxEvaluation {
1314 values: complex_tensor_into_value(values),
1315 indices: tensor::tensor_into_value(indices),
1316 });
1317 }
1318
1319 let strides = compute_strides(&shape);
1320 let output_strides = compute_strides(&output_shape);
1321 let dims_mask = resolved.dims_mask.clone();
1322 let reduce_strides = resolved.reduce_strides.clone();
1323
1324 let mut best = vec![BestComplex::new(); output_len];
1325 let mut coords = vec![0usize; shape.len()];
1326
1327 for &(re, im) in &tensor.data {
1328 let out_idx = map_output_index(&coords, &output_strides, &dims_mask);
1329 let reduce_idx = map_reduce_index(
1330 &coords,
1331 &resolved.reduced_dims,
1332 &reduce_strides,
1333 resolved.reduce_all,
1334 );
1335 let full_idx = map_linear_index(&coords, &strides);
1336 update_best_complex(
1337 &mut best[out_idx],
1338 (re, im),
1339 reduce_idx,
1340 full_idx,
1341 args.nan_mode,
1342 args.comparison,
1343 );
1344 increment_coords(&mut coords, &shape);
1345 }
1346
1347 let mut values = vec![(0.0f64, 0.0f64); output_len];
1348 let mut indices = vec![0.0f64; output_len];
1349
1350 for (i, entry) in best.iter().enumerate() {
1351 if entry.nan_fixed {
1352 values[i] = (f64::NAN, f64::NAN);
1353 indices[i] = if args.linear_index || resolved.reduce_all {
1354 (entry.full_index + 1) as f64
1355 } else if resolved.reduced_dims.is_empty() {
1356 1.0
1357 } else {
1358 (entry.reduce_index + 1) as f64
1359 };
1360 continue;
1361 }
1362 if !entry.has_value {
1363 values[i] = (f64::NAN, f64::NAN);
1364 indices[i] = f64::NAN;
1365 continue;
1366 }
1367 values[i] = entry.value;
1368 indices[i] = if args.linear_index || resolved.reduce_all {
1369 (entry.full_index + 1) as f64
1370 } else if resolved.reduced_dims.is_empty() {
1371 1.0
1372 } else {
1373 (entry.reduce_index + 1) as f64
1374 };
1375 }
1376
1377 let value_tensor = ComplexTensor::new(values, output_shape.clone())
1378 .map_err(|e| max_internal_error(format!("max: {e}")))?;
1379 let index_tensor =
1380 Tensor::new(indices, output_shape).map_err(|e| max_internal_error(format!("max: {e}")))?;
1381 Ok(MaxEvaluation {
1382 values: complex_tensor_into_value(value_tensor),
1383 indices: tensor::tensor_into_value(index_tensor),
1384 })
1385}
1386
1387#[derive(Debug, Clone)]
1388struct BestReal {
1389 value: f64,
1390 reduce_index: usize,
1391 full_index: usize,
1392 has_value: bool,
1393 nan_fixed: bool,
1394}
1395
1396impl BestReal {
1397 fn new() -> Self {
1398 Self {
1399 value: 0.0,
1400 reduce_index: 0,
1401 full_index: 0,
1402 has_value: false,
1403 nan_fixed: false,
1404 }
1405 }
1406}
1407
1408#[derive(Debug, Clone)]
1409struct BestComplex {
1410 value: (f64, f64),
1411 reduce_index: usize,
1412 full_index: usize,
1413 has_value: bool,
1414 nan_fixed: bool,
1415}
1416
1417impl BestComplex {
1418 fn new() -> Self {
1419 Self {
1420 value: (0.0, 0.0),
1421 reduce_index: 0,
1422 full_index: 0,
1423 has_value: false,
1424 nan_fixed: false,
1425 }
1426 }
1427}
1428
1429fn resolve_output_shape(
1430 shape: &[usize],
1431 selection: &DimSelection,
1432 reduced_dims: &[usize],
1433) -> BuiltinResult<Vec<usize>> {
1434 if is_scalar_shape(shape) {
1435 return Ok(normalize_scalar_shape(shape));
1436 }
1437 let mut output = shape.to_vec();
1438 match selection {
1439 DimSelection::All => {
1440 output.fill(1);
1441 }
1442 _ => {
1443 for &dim in reduced_dims {
1444 if dim < output.len() {
1445 output[dim] = 1;
1446 }
1447 }
1448 }
1449 }
1450 Ok(output)
1451}
1452
1453struct ResolvedDims {
1454 output_shape: Vec<usize>,
1455 reduced_dims: Vec<usize>,
1456 reduce_all: bool,
1457 dims_mask: Vec<bool>,
1458 reduce_strides: Vec<usize>,
1459}
1460
1461fn resolve_reduction_dims(
1462 shape: &[usize],
1463 selection: &DimSelection,
1464) -> BuiltinResult<ResolvedDims> {
1465 if is_scalar_shape(shape) {
1466 return Ok(ResolvedDims {
1467 output_shape: normalize_scalar_shape(shape),
1468 reduced_dims: Vec::new(),
1469 reduce_all: true,
1470 dims_mask: Vec::new(),
1471 reduce_strides: Vec::new(),
1472 });
1473 }
1474
1475 let mut reduced_dims = match selection {
1476 DimSelection::Auto => {
1477 let mut dim = None;
1478 for (index, &len) in shape.iter().enumerate() {
1479 if len > 1 {
1480 dim = Some(index);
1481 break;
1482 }
1483 }
1484 vec![dim.unwrap_or(0)]
1485 }
1486 DimSelection::Dim(dim) => {
1487 if *dim == 0 {
1488 return Err(max_invalid_argument("max: dimension must be >= 1"));
1489 }
1490 let index = dim.saturating_sub(1);
1491 if index >= shape.len() {
1492 Vec::new()
1493 } else {
1494 vec![index]
1495 }
1496 }
1497 DimSelection::Vec(dims) => {
1498 if dims.is_empty() {
1499 Vec::new()
1500 } else {
1501 dims.iter()
1502 .filter_map(|dim| {
1503 if *dim == 0 {
1504 None
1505 } else {
1506 let idx = dim - 1;
1507 if idx < shape.len() {
1508 Some(idx)
1509 } else {
1510 None
1511 }
1512 }
1513 })
1514 .collect()
1515 }
1516 }
1517 DimSelection::All => (0..shape.len()).collect(),
1518 };
1519
1520 reduced_dims.sort_unstable();
1521 reduced_dims.dedup();
1522
1523 let reduce_all = !reduced_dims.is_empty()
1524 && reduced_dims.len() == shape.len()
1525 && reduced_dims.iter().enumerate().all(|(i, &d)| i == d);
1526
1527 let output_shape = resolve_output_shape(shape, selection, &reduced_dims)?;
1528 let mut dims_mask = vec![false; shape.len()];
1529 for &dim in &reduced_dims {
1530 if dim < dims_mask.len() {
1531 dims_mask[dim] = true;
1532 }
1533 }
1534 let reduce_strides = compute_subspace_strides(shape, &reduced_dims);
1535
1536 Ok(ResolvedDims {
1537 output_shape,
1538 reduced_dims,
1539 reduce_all,
1540 dims_mask,
1541 reduce_strides,
1542 })
1543}
1544
1545fn compute_strides(shape: &[usize]) -> Vec<usize> {
1546 let mut strides = Vec::with_capacity(shape.len());
1547 let mut stride = 1usize;
1548 for &len in shape {
1549 strides.push(stride);
1550 stride = stride.saturating_mul(len.max(1));
1551 }
1552 strides
1553}
1554
1555fn compute_subspace_strides(shape: &[usize], dims: &[usize]) -> Vec<usize> {
1556 if dims.is_empty() {
1557 return Vec::new();
1558 }
1559 let mut strides = Vec::with_capacity(dims.len());
1560 let mut accum = 1usize;
1561 for &dim in dims {
1562 let len = shape.get(dim).copied().unwrap_or(1).max(1);
1563 strides.push(accum);
1564 accum = accum.saturating_mul(len);
1565 }
1566 strides
1567}
1568
1569fn map_output_index(coords: &[usize], output_strides: &[usize], dims_mask: &[bool]) -> usize {
1570 if coords.is_empty() {
1571 return 0;
1572 }
1573 let mut index = 0usize;
1574 for (dim, stride) in output_strides.iter().enumerate() {
1575 let coord = if *dims_mask.get(dim).unwrap_or(&false) {
1576 0
1577 } else {
1578 coords[dim]
1579 };
1580 index = index.saturating_add(coord.saturating_mul(*stride));
1581 }
1582 index
1583}
1584
1585fn map_reduce_index(
1586 coords: &[usize],
1587 reduced_dims: &[usize],
1588 reduce_strides: &[usize],
1589 reduce_all: bool,
1590) -> usize {
1591 if reduced_dims.is_empty() {
1592 return 0;
1593 }
1594 if reduce_all {
1595 return 0;
1597 }
1598 let mut index = 0usize;
1599 for (pos, &dim) in reduced_dims.iter().enumerate() {
1600 if let Some(coord) = coords.get(dim) {
1601 if let Some(stride) = reduce_strides.get(pos) {
1602 index = index.saturating_add(coord.saturating_mul(*stride));
1603 }
1604 }
1605 }
1606 index
1607}
1608
1609fn map_linear_index(coords: &[usize], strides: &[usize]) -> usize {
1610 coords
1611 .iter()
1612 .zip(strides.iter())
1613 .fold(0usize, |acc, (&coord, &stride)| {
1614 acc.saturating_add(coord.saturating_mul(stride))
1615 })
1616}
1617
1618fn increment_coords(coords: &mut [usize], shape: &[usize]) {
1619 for dim in 0..coords.len() {
1620 if shape[dim] == 0 {
1621 continue;
1622 }
1623 coords[dim] += 1;
1624 if coords[dim] < shape[dim] {
1625 break;
1626 }
1627 coords[dim] = 0;
1628 }
1629}
1630
1631fn update_best_real(
1632 best: &mut BestReal,
1633 value: f64,
1634 reduce_index: usize,
1635 full_index: usize,
1636 nan_mode: ReductionNaN,
1637 comparison: ComparisonMethod,
1638) {
1639 if value.is_nan() {
1640 match nan_mode {
1641 ReductionNaN::Include => {
1642 if !best.nan_fixed {
1643 best.value = f64::NAN;
1644 best.reduce_index = reduce_index;
1645 best.full_index = full_index;
1646 best.has_value = true;
1647 best.nan_fixed = true;
1648 }
1649 }
1650 ReductionNaN::Omit => {}
1651 }
1652 return;
1653 }
1654 if best.nan_fixed {
1655 return;
1656 }
1657
1658 if !best.has_value {
1659 best.value = value;
1660 best.reduce_index = reduce_index;
1661 best.full_index = full_index;
1662 best.has_value = true;
1663 return;
1664 }
1665
1666 if should_replace_real(best.value, value, comparison) {
1667 best.value = value;
1668 best.reduce_index = reduce_index;
1669 best.full_index = full_index;
1670 }
1671}
1672
1673fn update_best_complex(
1674 best: &mut BestComplex,
1675 value: (f64, f64),
1676 reduce_index: usize,
1677 full_index: usize,
1678 nan_mode: ReductionNaN,
1679 comparison: ComparisonMethod,
1680) {
1681 if value.0.is_nan() || value.1.is_nan() {
1682 match nan_mode {
1683 ReductionNaN::Include => {
1684 if !best.nan_fixed {
1685 best.value = (f64::NAN, f64::NAN);
1686 best.reduce_index = reduce_index;
1687 best.full_index = full_index;
1688 best.has_value = true;
1689 best.nan_fixed = true;
1690 }
1691 }
1692 ReductionNaN::Omit => {}
1693 }
1694 return;
1695 }
1696 if best.nan_fixed {
1697 return;
1698 }
1699
1700 if !best.has_value {
1701 best.value = value;
1702 best.reduce_index = reduce_index;
1703 best.full_index = full_index;
1704 best.has_value = true;
1705 return;
1706 }
1707
1708 if should_replace_complex(best.value, value, comparison) {
1709 best.value = value;
1710 best.reduce_index = reduce_index;
1711 best.full_index = full_index;
1712 }
1713}
1714
1715fn should_replace_real(current: f64, candidate: f64, comparison: ComparisonMethod) -> bool {
1716 match comparison {
1717 ComparisonMethod::Auto | ComparisonMethod::Real => {
1718 if candidate > current {
1719 return true;
1720 }
1721 if candidate < current {
1722 return false;
1723 }
1724 if candidate == 0.0 && current == 0.0 {
1725 return candidate.is_sign_positive() && !current.is_sign_positive();
1726 }
1727 false
1728 }
1729 ComparisonMethod::Abs => {
1730 let curr_abs = current.abs();
1731 let cand_abs = candidate.abs();
1732 if cand_abs > curr_abs {
1733 return true;
1734 }
1735 if cand_abs < curr_abs {
1736 return false;
1737 }
1738 if candidate > current {
1739 return true;
1740 }
1741 if candidate < current {
1742 return false;
1743 }
1744 if candidate == 0.0 && current == 0.0 {
1745 return candidate.is_sign_positive() && !current.is_sign_positive();
1746 }
1747 false
1748 }
1749 }
1750}
1751
1752fn should_replace_complex(
1753 current: (f64, f64),
1754 candidate: (f64, f64),
1755 comparison: ComparisonMethod,
1756) -> bool {
1757 match comparison {
1758 ComparisonMethod::Auto | ComparisonMethod::Abs => {
1759 compare_complex_auto(current, candidate) == Ordering::Less
1760 }
1761 ComparisonMethod::Real => compare_complex_real(current, candidate) == Ordering::Less,
1762 }
1763}
1764
1765fn compare_complex_auto(a: (f64, f64), b: (f64, f64)) -> Ordering {
1766 let a_mag = magnitude_squared(a);
1767 let b_mag = magnitude_squared(b);
1768 if a_mag < b_mag {
1769 return Ordering::Less;
1770 }
1771 if a_mag > b_mag {
1772 return Ordering::Greater;
1773 }
1774 let a_angle = a.1.atan2(a.0);
1776 let b_angle = b.1.atan2(b.0);
1777 if a_angle < b_angle {
1778 Ordering::Less
1779 } else if a_angle > b_angle {
1780 Ordering::Greater
1781 } else {
1782 Ordering::Equal
1783 }
1784}
1785
1786fn compare_complex_real(a: (f64, f64), b: (f64, f64)) -> Ordering {
1787 if a.0 < b.0 {
1788 return Ordering::Less;
1789 }
1790 if a.0 > b.0 {
1791 return Ordering::Greater;
1792 }
1793 compare_complex_auto(a, b)
1795}
1796
1797fn magnitude_squared(z: (f64, f64)) -> f64 {
1798 z.0.mul_add(z.0, z.1 * z.1)
1799}
1800
1801fn default_dimension_from_shape(shape: &[usize]) -> usize {
1802 if is_scalar_shape(shape) {
1803 return 1;
1804 }
1805 for (i, &len) in shape.iter().enumerate() {
1806 if len > 1 {
1807 return i + 1;
1808 }
1809 }
1810 1
1811}
1812
1813async fn elementwise_max(value: Value, args: ElementwiseArgs) -> BuiltinResult<MaxEvaluation> {
1814 let ElementwiseArgs {
1815 other,
1816 nan_mode,
1817 comparison,
1818 } = args;
1819 match (value, other) {
1820 (Value::GpuTensor(handle_a), Value::GpuTensor(handle_b)) => {
1821 if gpu_tensor_is_scalar(&handle_b) {
1822 if let Some(num) = gpu_tensor_scalar_value(&handle_b).await {
1823 let scalar = Value::Num(num);
1824 if nan_mode == ReductionNaN::Include {
1825 if let Some(eval) =
1826 elementwise_max_gpu_scalar_left(&handle_a, &scalar, comparison).await
1827 {
1828 return Ok(eval);
1829 }
1830 }
1831 if let Ok(ta) = gpu_helpers::gather_tensor_async(&handle_a).await {
1832 if let Ok(eval) = elementwise_real_or_complex(
1833 Value::Tensor(ta),
1834 scalar.clone(),
1835 nan_mode,
1836 comparison,
1837 ) {
1838 return Ok(eval);
1839 }
1840 }
1841 return Err(max_internal_error(
1842 "max: elementwise GPU scalar path failed",
1843 ));
1844 }
1845 }
1846 if gpu_tensor_is_scalar(&handle_a) {
1847 if let Some(num) = gpu_tensor_scalar_value(&handle_a).await {
1848 let scalar = Value::Num(num);
1849 if nan_mode == ReductionNaN::Include {
1850 if let Some(eval) =
1851 elementwise_max_gpu_scalar_right(&scalar, &handle_b, comparison).await
1852 {
1853 return Ok(eval);
1854 }
1855 }
1856 if let Ok(tb) = gpu_helpers::gather_tensor_async(&handle_b).await {
1857 if let Ok(eval) = elementwise_real_or_complex(
1858 scalar.clone(),
1859 Value::Tensor(tb),
1860 nan_mode,
1861 comparison,
1862 ) {
1863 return Ok(eval);
1864 }
1865 }
1866 return Err(max_internal_error(
1867 "max: elementwise GPU scalar path failed",
1868 ));
1869 }
1870 }
1871 if nan_mode == ReductionNaN::Include {
1872 if let Some(eval) = elementwise_max_gpu_pair(&handle_a, &handle_b, comparison).await
1873 {
1874 return Ok(eval);
1875 }
1876 }
1877 if let (Ok(ta), Ok(tb)) = (
1878 gpu_helpers::gather_tensor_async(&handle_a).await,
1879 gpu_helpers::gather_tensor_async(&handle_b).await,
1880 ) {
1881 if let Ok(eval) = elementwise_real_or_complex(
1882 Value::Tensor(ta),
1883 Value::Tensor(tb),
1884 nan_mode,
1885 comparison,
1886 ) {
1887 return Ok(eval);
1888 }
1889 }
1890 Err(max_internal_error("max: elementwise GPU path failed"))
1891 }
1892 (Value::GpuTensor(handle), other) => {
1893 if nan_mode == ReductionNaN::Include {
1894 if let Some(eval) =
1895 elementwise_max_gpu_scalar_left(&handle, &other, comparison).await
1896 {
1897 return Ok(eval);
1898 }
1899 }
1900 let t = gpu_helpers::gather_tensor_async(&handle)
1901 .await
1902 .map_err(|_| max_internal_error("max: elementwise GPU scalar path failed"))?;
1903 elementwise_real_or_complex(Value::Tensor(t), other, nan_mode, comparison)
1904 }
1905 (other, Value::GpuTensor(handle)) => {
1906 if nan_mode == ReductionNaN::Include {
1907 if let Some(eval) =
1908 elementwise_max_gpu_scalar_right(&other, &handle, comparison).await
1909 {
1910 return Ok(eval);
1911 }
1912 }
1913 let t = gpu_helpers::gather_tensor_async(&handle)
1914 .await
1915 .map_err(|_| max_internal_error("max: elementwise GPU scalar path failed"))?;
1916 elementwise_real_or_complex(other, Value::Tensor(t), nan_mode, comparison)
1917 }
1918 (lhs, rhs) => elementwise_real_or_complex(lhs, rhs, nan_mode, comparison),
1919 }
1920}
1921
1922async fn elementwise_max_gpu_pair(
1923 a: &GpuTensorHandle,
1924 b: &GpuTensorHandle,
1925 comparison: ComparisonMethod,
1926) -> Option<MaxEvaluation> {
1927 if comparison != ComparisonMethod::Auto {
1928 return None;
1929 }
1930 let provider = runmat_accelerate_api::provider()?;
1931 if a.shape == b.shape {
1933 let values = provider.elem_max(a, b).await.ok()?;
1934 if let Ok(mask) = provider.elem_ge(a, b).await {
1936 let indices = gpu_mask_indices(provider, &mask)?;
1937 let _ = provider.free(&mask);
1938 return Some(MaxEvaluation {
1939 values: Value::GpuTensor(values),
1940 indices: Value::GpuTensor(indices),
1941 });
1942 } else {
1943 let ta = gpu_helpers::gather_tensor_async(a).await.ok()?;
1945 let tb = gpu_helpers::gather_tensor_async(b).await.ok()?;
1946 let mut indices = Vec::with_capacity(ta.data.len());
1947 for i in 0..ta.data.len() {
1948 indices.push(if ta.data[i] >= tb.data[i] { 1.0 } else { 2.0 });
1949 }
1950 let index_tensor = Tensor::new(indices, ta.shape.clone()).ok()?;
1951 return Some(MaxEvaluation {
1952 values: Value::GpuTensor(values),
1953 indices: tensor::tensor_into_value(index_tensor),
1954 });
1955 }
1956 }
1957 let (out_shape, reps_a, reps_b) = broadcast_reps(&a.shape, &b.shape)?;
1959 let a_exp = if reps_a.iter().any(|&r| r != 1) {
1960 provider.repmat(a, &reps_a).ok()?
1961 } else {
1962 a.clone()
1963 };
1964 let b_exp = if reps_b.iter().any(|&r| r != 1) {
1965 provider.repmat(b, &reps_b).ok()?
1966 } else {
1967 b.clone()
1968 };
1969 let values = provider.elem_max(&a_exp, &b_exp).await.ok();
1970 let mask = provider.elem_ge(&a_exp, &b_exp).await.ok();
1971 if !std::ptr::eq(&a_exp, a) {
1972 let _ = provider.free(&a_exp);
1973 }
1974 if !std::ptr::eq(&b_exp, b) {
1975 let _ = provider.free(&b_exp);
1976 }
1977 let values = values?;
1978 if values.shape != out_shape {
1979 let _ = provider.free(&values);
1980 return None;
1981 }
1982 let index_tensor = if let Some(mask) = mask {
1983 let mask_host = gpu_helpers::gather_tensor_async(&mask).await.ok()?;
1984 let _ = provider.free(&mask);
1985 let mut indices = Vec::with_capacity(mask_host.data.len());
1986 for &m in &mask_host.data {
1987 indices.push(if m != 0.0 { 1.0 } else { 2.0 });
1988 }
1989 Tensor::new(indices, out_shape).ok()?
1990 } else {
1991 let ta = gpu_helpers::gather_tensor_async(&a_exp).await.ok()?;
1993 let tb = gpu_helpers::gather_tensor_async(&b_exp).await.ok()?;
1994 let mut indices = Vec::with_capacity(ta.data.len());
1995 for i in 0..ta.data.len() {
1996 indices.push(if ta.data[i] >= tb.data[i] { 1.0 } else { 2.0 });
1997 }
1998 Tensor::new(indices, out_shape).ok()?
1999 };
2000 Some(MaxEvaluation {
2001 values: Value::GpuTensor(values),
2002 indices: tensor::tensor_into_value(index_tensor),
2003 })
2004}
2005
2006fn broadcast_reps(a: &[usize], b: &[usize]) -> Option<(Vec<usize>, Vec<usize>, Vec<usize>)> {
2007 let rank = a.len().max(b.len()).max(1);
2008 let mut out = vec![1usize; rank];
2009 let mut aa = vec![1usize; rank];
2010 let mut bb = vec![1usize; rank];
2011 for i in 0..rank {
2012 aa[i] = *a.get(i).unwrap_or(&1);
2013 bb[i] = *b.get(i).unwrap_or(&1);
2014 }
2015 for i in 0..rank {
2016 let (ad, bd) = (aa[i], bb[i]);
2017 if ad == bd {
2018 out[i] = ad;
2019 } else if ad == 1 {
2020 out[i] = bd;
2021 } else if bd == 1 {
2022 out[i] = ad;
2023 } else {
2024 return None;
2025 }
2026 }
2027 let reps_a: Vec<usize> = (0..rank)
2028 .map(|i| if aa[i] == out[i] { 1 } else { out[i] })
2029 .collect();
2030 let reps_b: Vec<usize> = (0..rank)
2031 .map(|i| if bb[i] == out[i] { 1 } else { out[i] })
2032 .collect();
2033 Some((out, reps_a, reps_b))
2034}
2035
2036async fn elementwise_max_gpu_scalar_left(
2037 a: &GpuTensorHandle,
2038 other: &Value,
2039 comparison: ComparisonMethod,
2040) -> Option<MaxEvaluation> {
2041 if comparison != ComparisonMethod::Auto {
2042 return None;
2043 }
2044 let provider = runmat_accelerate_api::provider()?;
2045 let scalar = extract_scalar(other)?;
2046 let values = if let Ok(fill) = provider.fill_like(a, scalar) {
2048 let vals = provider.elem_max(a, &fill).await.ok();
2049 let _ = provider.free(&fill);
2050 vals?
2051 } else {
2052 provider.scalar_max(a, scalar).ok()?
2053 };
2054 let index_tensor = if let Ok(fill) = provider.fill_like(a, scalar) {
2056 if let Ok(mask) = provider.elem_ge(a, &fill).await {
2057 let _ = provider.free(&fill);
2058 let indices = gpu_mask_indices(provider, &mask)?;
2059 let _ = provider.free(&mask);
2060 return Some(MaxEvaluation {
2061 values: Value::GpuTensor(values),
2062 indices: Value::GpuTensor(indices),
2063 });
2064 } else {
2065 let _ = provider.free(&fill);
2066 let ta = gpu_helpers::gather_tensor_async(a).await.ok()?;
2067 let mut indices = Vec::with_capacity(ta.data.len());
2068 for &v in &ta.data {
2069 indices.push(if v >= scalar { 1.0 } else { 2.0 });
2070 }
2071 Tensor::new(indices, ta.shape.clone()).ok()?
2072 }
2073 } else {
2074 let ta = gpu_helpers::gather_tensor_async(a).await.ok()?;
2075 let mut indices = Vec::with_capacity(ta.data.len());
2076 for &v in &ta.data {
2077 indices.push(if v >= scalar { 1.0 } else { 2.0 });
2078 }
2079 Tensor::new(indices, ta.shape.clone()).ok()?
2080 };
2081 Some(MaxEvaluation {
2082 values: Value::GpuTensor(values),
2083 indices: tensor::tensor_into_value(index_tensor),
2084 })
2085}
2086
2087async fn elementwise_max_gpu_scalar_right(
2088 other: &Value,
2089 b: &GpuTensorHandle,
2090 comparison: ComparisonMethod,
2091) -> Option<MaxEvaluation> {
2092 if comparison != ComparisonMethod::Auto {
2093 return None;
2094 }
2095 let provider = runmat_accelerate_api::provider()?;
2096 let scalar = extract_scalar(other)?;
2097 let values = if let Ok(fill) = provider.fill_like(b, scalar) {
2098 let vals = provider.elem_max(&fill, b).await.ok();
2099 let _ = provider.free(&fill);
2100 vals?
2101 } else {
2102 provider.scalar_max(b, scalar).ok()?
2103 };
2104 let index_tensor = if let Ok(fill) = provider.fill_like(b, scalar) {
2106 if let Ok(mask) = provider.elem_ge(&fill, b).await {
2107 let _ = provider.free(&fill);
2108 let indices = gpu_mask_indices(provider, &mask)?;
2109 let _ = provider.free(&mask);
2110 return Some(MaxEvaluation {
2111 values: Value::GpuTensor(values),
2112 indices: Value::GpuTensor(indices),
2113 });
2114 } else {
2115 let _ = provider.free(&fill);
2116 let tb = gpu_helpers::gather_tensor_async(b).await.ok()?;
2117 let mut indices = Vec::with_capacity(tb.data.len());
2118 for &v in &tb.data {
2119 indices.push(if scalar >= v { 1.0 } else { 2.0 });
2120 }
2121 Tensor::new(indices, tb.shape.clone()).ok()?
2122 }
2123 } else {
2124 let tb = gpu_helpers::gather_tensor_async(b).await.ok()?;
2125 let mut indices = Vec::with_capacity(tb.data.len());
2126 for &v in &tb.data {
2127 indices.push(if scalar >= v { 1.0 } else { 2.0 });
2128 }
2129 Tensor::new(indices, tb.shape.clone()).ok()?
2130 };
2131 Some(MaxEvaluation {
2132 values: Value::GpuTensor(values),
2133 indices: tensor::tensor_into_value(index_tensor),
2134 })
2135}
2136
2137fn extract_scalar(v: &Value) -> Option<f64> {
2138 match v {
2139 Value::Num(n) => Some(*n),
2140 Value::Int(i) => Some(i.to_f64()),
2141 Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
2142 Value::Tensor(t) if t.data.len() == 1 => t.data.first().copied(),
2143 Value::LogicalArray(l) if l.data.len() == 1 => Some(if l.data[0] != 0 { 1.0 } else { 0.0 }),
2144 _ => None,
2145 }
2146}
2147
2148fn gpu_tensor_is_scalar(handle: &GpuTensorHandle) -> bool {
2149 handle.shape.iter().copied().product::<usize>().max(1) == 1
2150}
2151
2152async fn gpu_tensor_scalar_value(handle: &GpuTensorHandle) -> Option<f64> {
2153 let tensor = gpu_helpers::gather_tensor_async(handle).await.ok()?;
2154 tensor.data.first().copied()
2155}
2156
2157fn gpu_mask_indices(
2158 provider: &dyn AccelProvider,
2159 mask: &GpuTensorHandle,
2160) -> Option<GpuTensorHandle> {
2161 let scaled = provider.scalar_mul(mask, -1.0).ok()?;
2162 let shifted = provider.scalar_add(&scaled, 2.0).ok()?;
2163 let _ = provider.free(&scaled);
2164 Some(shifted)
2165}
2166
2167fn elementwise_real_or_complex(
2168 lhs: Value,
2169 rhs: Value,
2170 nan_mode: ReductionNaN,
2171 comparison: ComparisonMethod,
2172) -> BuiltinResult<MaxEvaluation> {
2173 if let Some(eval) = scalar_elementwise_max(&lhs, &rhs, nan_mode, comparison) {
2174 return Ok(eval);
2175 }
2176 match (
2177 materialize_for_max("max", lhs)?,
2178 materialize_for_max("max", rhs)?,
2179 ) {
2180 (InputData::Complex(a), InputData::Complex(b)) => {
2181 elementwise_complex_max(a, b, nan_mode, comparison)
2182 }
2183 (InputData::Complex(a), InputData::Real(b)) => {
2184 let converted = promote_real_tensor_to_complex(b);
2185 elementwise_complex_max(a, converted, nan_mode, comparison)
2186 }
2187 (InputData::Real(a), InputData::Complex(b)) => {
2188 let converted = promote_real_tensor_to_complex(a);
2189 elementwise_complex_max(converted, b, nan_mode, comparison)
2190 }
2191 (InputData::Real(a), InputData::Real(b)) => {
2192 elementwise_real_max(a, b, nan_mode, comparison)
2193 }
2194 }
2195}
2196
2197fn scalar_real_value(value: &Value) -> Option<f64> {
2198 match value {
2199 Value::Num(n) => Some(*n),
2200 Value::Int(i) => Some(i.to_f64()),
2201 Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
2202 Value::Tensor(t) if t.data.len() == 1 => t.data.first().copied(),
2203 Value::LogicalArray(l) if l.data.len() == 1 => Some(if l.data[0] != 0 { 1.0 } else { 0.0 }),
2204 _ => None,
2205 }
2206}
2207
2208fn scalar_complex_value(value: &Value) -> Option<(f64, f64)> {
2209 match value {
2210 Value::Complex(re, im) => Some((*re, *im)),
2211 Value::ComplexTensor(ct) if ct.data.len() == 1 => ct.data.first().copied(),
2212 _ => None,
2213 }
2214}
2215
2216fn scalar_elementwise_max(
2217 lhs: &Value,
2218 rhs: &Value,
2219 nan_mode: ReductionNaN,
2220 comparison: ComparisonMethod,
2221) -> Option<MaxEvaluation> {
2222 let left = scalar_complex_value(lhs).or_else(|| scalar_real_value(lhs).map(|v| (v, 0.0)))?;
2223 let right = scalar_complex_value(rhs).or_else(|| scalar_real_value(rhs).map(|v| (v, 0.0)))?;
2224 let (ar, ai) = left;
2225 let (br, bi) = right;
2226 if ai != 0.0 || bi != 0.0 {
2227 let (value, origin) = choose_complex_elementwise((ar, ai), (br, bi), nan_mode, comparison);
2228 return Some(MaxEvaluation {
2229 values: Value::Complex(value.0, value.1),
2230 indices: Value::Num(origin),
2231 });
2232 }
2233 let (value, origin) = choose_real_elementwise(ar, br, nan_mode, comparison);
2234 Some(MaxEvaluation {
2235 values: Value::Num(value),
2236 indices: Value::Num(origin),
2237 })
2238}
2239
2240fn elementwise_real_max(
2241 lhs: Tensor,
2242 rhs: Tensor,
2243 nan_mode: ReductionNaN,
2244 comparison: ComparisonMethod,
2245) -> BuiltinResult<MaxEvaluation> {
2246 let plan = BroadcastPlan::new(&lhs.shape, &rhs.shape)
2247 .map_err(|err| max_size_mismatch(format!("max: {err}")))?;
2248 let mut values = vec![0.0f64; plan.len()];
2249 let mut indices = vec![0.0f64; plan.len()];
2250
2251 for (offset, index_a, index_b) in plan.iter() {
2252 let a = lhs.data.get(index_a).copied().unwrap_or(f64::NAN);
2253 let b = rhs.data.get(index_b).copied().unwrap_or(f64::NAN);
2254 let (value, origin) = choose_real_elementwise(a, b, nan_mode, comparison);
2255 values[offset] = value;
2256 indices[offset] = origin;
2257 }
2258
2259 let value_tensor = Tensor::new(values, plan.output_shape().to_vec())
2260 .map_err(|e| max_internal_error(format!("max: {e}")))?;
2261 let index_tensor = Tensor::new(indices, plan.output_shape().to_vec())
2262 .map_err(|e| max_internal_error(format!("max: {e}")))?;
2263
2264 Ok(MaxEvaluation {
2265 values: tensor::tensor_into_value(value_tensor),
2266 indices: tensor::tensor_into_value(index_tensor),
2267 })
2268}
2269
2270fn elementwise_complex_max(
2271 lhs: ComplexTensor,
2272 rhs: ComplexTensor,
2273 nan_mode: ReductionNaN,
2274 comparison: ComparisonMethod,
2275) -> BuiltinResult<MaxEvaluation> {
2276 let plan = BroadcastPlan::new(&lhs.shape, &rhs.shape)
2277 .map_err(|err| max_size_mismatch(format!("max: {err}")))?;
2278 let mut values = vec![(0.0f64, 0.0f64); plan.len()];
2279 let mut indices = vec![0.0f64; plan.len()];
2280
2281 for (offset, index_a, index_b) in plan.iter() {
2282 let a = lhs
2283 .data
2284 .get(index_a)
2285 .copied()
2286 .unwrap_or((f64::NAN, f64::NAN));
2287 let b = rhs
2288 .data
2289 .get(index_b)
2290 .copied()
2291 .unwrap_or((f64::NAN, f64::NAN));
2292 let (value, origin) = choose_complex_elementwise(a, b, nan_mode, comparison);
2293 values[offset] = value;
2294 indices[offset] = origin;
2295 }
2296
2297 let value_tensor = ComplexTensor::new(values, plan.output_shape().to_vec())
2298 .map_err(|e| max_internal_error(format!("max: {e}")))?;
2299 let index_tensor = Tensor::new(indices, plan.output_shape().to_vec())
2300 .map_err(|e| max_internal_error(format!("max: {e}")))?;
2301
2302 Ok(MaxEvaluation {
2303 values: complex_tensor_into_value(value_tensor),
2304 indices: tensor::tensor_into_value(index_tensor),
2305 })
2306}
2307
2308fn promote_real_tensor_to_complex(tensor: Tensor) -> ComplexTensor {
2309 let data = tensor
2310 .data
2311 .iter()
2312 .copied()
2313 .map(|re| (re, 0.0))
2314 .collect::<Vec<_>>();
2315 ComplexTensor {
2316 data,
2317 shape: tensor.shape.clone(),
2318 rows: tensor.rows,
2319 cols: tensor.cols,
2320 }
2321}
2322
2323fn choose_real_elementwise(
2324 a: f64,
2325 b: f64,
2326 nan_mode: ReductionNaN,
2327 comparison: ComparisonMethod,
2328) -> (f64, f64) {
2329 match (a.is_nan(), b.is_nan()) {
2330 (true, true) => (f64::NAN, 1.0),
2331 (true, false) if nan_mode == ReductionNaN::Omit => (b, 2.0),
2332 (false, true) if nan_mode == ReductionNaN::Omit => (a, 1.0),
2333 (true, false) => (f64::NAN, 1.0),
2334 (false, true) => (f64::NAN, 2.0),
2335 (false, false) => {
2336 if should_replace_real(a, b, comparison) {
2337 (b, 2.0)
2338 } else {
2339 (a, 1.0)
2340 }
2341 }
2342 }
2343}
2344
2345fn choose_complex_elementwise(
2346 a: (f64, f64),
2347 b: (f64, f64),
2348 nan_mode: ReductionNaN,
2349 comparison: ComparisonMethod,
2350) -> ((f64, f64), f64) {
2351 let a_nan = a.0.is_nan() || a.1.is_nan();
2352 let b_nan = b.0.is_nan() || b.1.is_nan();
2353 match (a_nan, b_nan) {
2354 (true, true) => ((f64::NAN, f64::NAN), 1.0),
2355 (true, false) if nan_mode == ReductionNaN::Omit => (b, 2.0),
2356 (false, true) if nan_mode == ReductionNaN::Omit => (a, 1.0),
2357 (true, false) => ((f64::NAN, f64::NAN), 1.0),
2358 (false, true) => ((f64::NAN, f64::NAN), 2.0),
2359 (false, false) => {
2360 if should_replace_complex(a, b, comparison) {
2361 (b, 2.0)
2362 } else {
2363 (a, 1.0)
2364 }
2365 }
2366 }
2367}
2368
2369#[cfg(test)]
2370pub(crate) mod tests {
2371 use super::*;
2372 #[cfg(feature = "wgpu")]
2373 use crate::builtins::common::test_support;
2374 use futures::executor::block_on;
2375 #[cfg(feature = "wgpu")]
2376 use runmat_accelerate_api::HostTensorView;
2377 use runmat_builtins::{IntValue, Tensor, Value};
2378
2379 fn max_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
2380 block_on(super::max_builtin(value, rest))
2381 }
2382
2383 fn nanmax_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
2384 block_on(super::nanmax_builtin(value, rest))
2385 }
2386
2387 #[test]
2388 fn max_type_with_two_args_returns_tensor() {
2389 let out = max_type(
2390 &[Type::Tensor { shape: None }, Type::Tensor { shape: None }],
2391 &ResolveContext::new(Vec::new()),
2392 );
2393 assert_eq!(out, Type::tensor());
2394 }
2395
2396 #[test]
2397 fn max_descriptor_signatures_cover_core_forms() {
2398 let labels: Vec<&str> = MAX_DESCRIPTOR
2399 .signatures
2400 .iter()
2401 .map(|sig| sig.label)
2402 .collect();
2403 assert!(labels.contains(&"M = max(A)"));
2404 assert!(labels.contains(&"[M, I] = max(A)"));
2405 assert!(labels.contains(&"M = max(A, B)"));
2406 assert!(labels.contains(&"[M, I] = max(A, B)"));
2407 assert!(labels.contains(&"M = max(A, [], dim)"));
2408 assert!(labels.contains(&"M = max(A, [], \"all\")"));
2409 assert!(labels.contains(&"M = max(A, [], \"ComparisonMethod\", method)"));
2410 assert!(labels.contains(&"M = max(A, B, \"ComparisonMethod\", method)"));
2411 }
2412
2413 #[test]
2414 fn nanmax_descriptor_signatures_cover_core_forms() {
2415 let labels: Vec<&str> = NANMAX_DESCRIPTOR
2416 .signatures
2417 .iter()
2418 .map(|sig| sig.label)
2419 .collect();
2420 assert!(labels.contains(&"M = nanmax(A)"));
2421 assert!(labels.contains(&"[M, I] = nanmax(A)"));
2422 assert!(labels.contains(&"M = nanmax(A, B)"));
2423 assert!(labels.contains(&"[M, I] = nanmax(A, B)"));
2424 assert!(labels.contains(&"M = nanmax(A, [], dim)"));
2425 assert!(labels.contains(&"[M, I] = nanmax(A, [], dim)"));
2426 assert!(labels.contains(&"M = nanmax(A, [], vecdim)"));
2427 assert!(labels.contains(&"M = nanmax(A, [], \"all\")"));
2428 assert!(labels.contains(&"M = nanmax(A, [], \"linear\")"));
2429 assert!(labels.contains(&"M = nanmax(A, [], \"ComparisonMethod\", method)"));
2430 assert!(labels.contains(&"M = nanmax(A, B, \"ComparisonMethod\", method)"));
2431 }
2432
2433 #[test]
2434 fn max_descriptor_errors_have_stable_codes() {
2435 assert!(MAX_DESCRIPTOR
2436 .errors
2437 .iter()
2438 .any(|error| error.code == MAX_ERROR_INVALID_ARGUMENT.code));
2439 assert!(MAX_DESCRIPTOR
2440 .errors
2441 .iter()
2442 .any(|error| error.code == MAX_ERROR_INVALID_INPUT.code));
2443 assert!(MAX_DESCRIPTOR
2444 .errors
2445 .iter()
2446 .any(|error| error.code == MAX_ERROR_SIZE_MISMATCH.code));
2447 assert!(MAX_DESCRIPTOR
2448 .errors
2449 .iter()
2450 .any(|error| error.code == MAX_ERROR_INTERNAL.code));
2451 }
2452
2453 fn evaluate(value: Value, rest: &[Value]) -> BuiltinResult<MaxEvaluation> {
2454 block_on(super::evaluate(value, rest))
2455 }
2456
2457 fn placeholder() -> Value {
2458 let tensor = Tensor::new(Vec::<f64>::new(), vec![0, 0]).unwrap();
2459 Value::Tensor(tensor)
2460 }
2461
2462 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2463 #[test]
2464 fn max_scalar_returns_input() {
2465 let result = max_builtin(Value::Num(5.0), Vec::new()).expect("max");
2466 assert_eq!(result, Value::Num(5.0));
2467 }
2468
2469 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2470 #[test]
2471 fn max_vector_with_indices() {
2472 let tensor = Tensor::new(vec![3.0, 1.0, 5.0], vec![3, 1]).unwrap();
2473 let eval = evaluate(Value::Tensor(tensor), &[]).expect("evaluate");
2474 let (values, indices) = eval.into_pair();
2475 assert_eq!(values, Value::Num(5.0));
2476 assert_eq!(indices, Value::Num(3.0));
2477 }
2478
2479 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2480 #[test]
2481 fn max_native_integer_reduction_preserves_uint64_values_and_indices() {
2482 let input = Tensor::new_integer(
2483 runmat_builtins::IntegerStorage::U64(vec![u64::MAX - 1, u64::MAX, 3, 2]),
2484 vec![2, 2],
2485 )
2486 .expect("input");
2487 let (values, indices) = evaluate(Value::Tensor(input), &[])
2488 .expect("max")
2489 .into_pair();
2490 assert_eq!(
2491 values,
2492 Value::Tensor(
2493 Tensor::new_integer(
2494 runmat_builtins::IntegerStorage::U64(vec![u64::MAX, 3]),
2495 vec![1, 2],
2496 )
2497 .expect("values"),
2498 )
2499 );
2500 assert_eq!(
2501 indices,
2502 Value::Tensor(Tensor::new(vec![2.0, 1.0], vec![1, 2]).expect("indices")),
2503 );
2504 }
2505
2506 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2507 #[test]
2508 fn max_native_integer_abs_all_uses_exact_int64_minimum() {
2509 let input = Tensor::new_integer(
2510 runmat_builtins::IntegerStorage::I64(vec![i64::MIN, -3, 3]),
2511 vec![3, 1],
2512 )
2513 .expect("input");
2514 let args = vec![
2515 placeholder(),
2516 Value::from("all"),
2517 Value::from("ComparisonMethod"),
2518 Value::from("abs"),
2519 ];
2520 let (values, indices) = evaluate(Value::Tensor(input), &args)
2521 .expect("max")
2522 .into_pair();
2523 assert_eq!(values, Value::Int(IntValue::I64(i64::MIN)));
2524 assert_eq!(indices, Value::Num(1.0));
2525 }
2526
2527 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2528 #[test]
2529 fn max_native_integer_empty_array_retains_its_class() {
2530 let input =
2531 Tensor::new_integer(runmat_builtins::IntegerStorage::U32(Vec::new()), vec![0, 0])
2532 .expect("input");
2533 let (values, indices) = evaluate(Value::Tensor(input), &[])
2534 .expect("max")
2535 .into_pair();
2536 assert_eq!(
2537 values,
2538 Value::Tensor(
2539 Tensor::new_integer(runmat_builtins::IntegerStorage::U32(Vec::new()), vec![0, 0])
2540 .expect("values"),
2541 )
2542 );
2543 assert_eq!(
2544 indices,
2545 Value::Tensor(Tensor::new(Vec::new(), vec![0, 0]).expect("indices")),
2546 );
2547 }
2548
2549 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2550 #[test]
2551 fn max_native_integer_empty_all_reduction_remains_empty() {
2552 let input =
2553 Tensor::new_integer(runmat_builtins::IntegerStorage::I16(Vec::new()), vec![0, 0])
2554 .expect("input");
2555 let values = evaluate(Value::Tensor(input), &[placeholder(), Value::from("all")])
2556 .expect("max")
2557 .into_value();
2558 assert_eq!(
2559 values,
2560 Value::Tensor(
2561 Tensor::new_integer(runmat_builtins::IntegerStorage::I16(Vec::new()), vec![0, 0])
2562 .expect("values"),
2563 )
2564 );
2565 }
2566
2567 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2568 #[test]
2569 fn max_row_vector_reduces_across_columns() {
2570 let tensor = Tensor::new(vec![3.0, 1.0, 5.0], vec![1, 3]).unwrap();
2571 let eval = evaluate(Value::Tensor(tensor), &[]).expect("evaluate");
2572 let (values, indices) = eval.into_pair();
2573 assert_eq!(values, Value::Num(5.0));
2574 assert_eq!(indices, Value::Num(3.0));
2575 }
2576
2577 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2578 #[test]
2579 fn max_single_row_vector_reduces_across_columns() {
2580 let tensor = Tensor::from_f32(vec![3.0, 1.0, 5.0], vec![1, 3]).unwrap();
2581 let eval = evaluate(Value::Tensor(tensor), &[]).expect("evaluate");
2582 let (values, indices) = eval.into_pair();
2583 assert_eq!(values, Value::Num(5.0));
2584 assert_eq!(indices, Value::Num(3.0));
2585 }
2586
2587 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2588 #[test]
2589 fn max_registered_single_row_vector_reduces_across_columns() {
2590 let tensor = Tensor::from_f32(vec![3.0, 1.0, 5.0], vec![1, 3]).unwrap();
2591 let value = block_on(crate::call_builtin_async_with_outputs(
2592 "max",
2593 &[Value::Tensor(tensor)],
2594 1,
2595 ))
2596 .expect("dispatch max");
2597 match value {
2598 Value::OutputList(values) => {
2599 assert_eq!(values.len(), 1);
2600 assert_eq!(values[0], Value::Num(5.0));
2601 }
2602 other => assert_eq!(other, Value::Num(5.0)),
2603 }
2604 }
2605
2606 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2607 #[test]
2608 fn max_matrix_default_dimension() {
2609 let tensor = Tensor::new(vec![3.0, 4.0, 1.0, 2.0, 5.0, 6.0], vec![2, 3]).unwrap();
2610 let eval = evaluate(Value::Tensor(tensor), &[]).expect("evaluate");
2611 let (values, indices) = eval.into_pair();
2612 match values {
2613 Value::Tensor(t) => {
2614 assert_eq!(t.shape, vec![1, 3]);
2615 assert_eq!(t.data, vec![4.0, 2.0, 6.0]);
2616 }
2617 other => panic!("expected tensor, got {other:?}"),
2618 }
2619 match indices {
2620 Value::Tensor(t) => {
2621 assert_eq!(t.data, vec![2.0, 2.0, 2.0]);
2622 }
2623 other => panic!("expected tensor, got {other:?}"),
2624 }
2625 }
2626
2627 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2628 #[test]
2629 fn max_all_linear_index() {
2630 let tensor =
2631 Tensor::new((1..=12).map(|v| v as f64).collect::<Vec<_>>(), vec![3, 4]).unwrap();
2632 let args = vec![placeholder(), Value::from("all")];
2633 let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
2634 let (values, indices) = eval.into_pair();
2635 assert_eq!(values, Value::Num(12.0));
2636 assert_eq!(indices, Value::Num(12.0));
2637
2638 let args_linear = vec![placeholder(), Value::from("linear")];
2639 let eval = evaluate(
2640 Value::Tensor(Tensor::new(vec![2.0, 3.0], vec![1, 2]).unwrap()),
2641 &args_linear,
2642 )
2643 .expect("evaluate");
2644 let (values, indices) = eval.into_pair();
2645 assert_eq!(values, Value::Num(3.0));
2646 assert_eq!(indices, Value::Num(2.0));
2647 }
2648
2649 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2650 #[test]
2651 fn max_with_omitnan() {
2652 let tensor = Tensor::new(vec![f64::NAN, 4.0, 2.0], vec![3, 1]).unwrap();
2653 let args = vec![placeholder(), Value::from("omitnan")];
2654 let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
2655 let (values, indices) = eval.into_pair();
2656 assert_eq!(values, Value::Num(4.0));
2657 assert_eq!(indices, Value::Num(2.0));
2658 }
2659
2660 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2661 #[test]
2662 fn max_omitnan_all_nan_slice() {
2663 let tensor = Tensor::new(vec![f64::NAN, f64::NAN], vec![2, 1]).unwrap();
2664 let args = vec![placeholder(), Value::from("omitnan")];
2665 let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
2666 let (values, indices) = eval.into_pair();
2667 match values {
2668 Value::Num(v) => assert!(v.is_nan()),
2669 other => panic!("expected scalar NaN, got {other:?}"),
2670 }
2671 match indices {
2672 Value::Num(v) => assert!(v.is_nan()),
2673 other => panic!("expected scalar NaN index, got {other:?}"),
2674 }
2675 }
2676
2677 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2678 #[test]
2679 fn nanmax_reduction_omits_nan_by_default() {
2680 let tensor = Tensor::new(vec![f64::NAN, 4.0, 2.0, f64::NAN], vec![2, 2]).unwrap();
2681 let result = nanmax_builtin(Value::Tensor(tensor), Vec::new()).expect("nanmax");
2682 match result {
2683 Value::Tensor(t) => {
2684 assert_eq!(t.shape, vec![1, 2]);
2685 assert_eq!(t.data[0], 4.0);
2686 assert_eq!(t.data[1], 2.0);
2687 }
2688 other => panic!("expected tensor, got {other:?}"),
2689 }
2690 }
2691
2692 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2693 #[test]
2694 fn nanmax_reduction_accepts_empty_placeholder_and_dim() {
2695 let tensor = Tensor::new(vec![f64::NAN, 4.0, 2.0, f64::NAN], vec![2, 2]).unwrap();
2696 let result = nanmax_builtin(Value::Tensor(tensor), vec![placeholder(), Value::Num(2.0)])
2697 .expect("nanmax dim");
2698 match result {
2699 Value::Tensor(t) => {
2700 assert_eq!(t.shape, vec![2, 1]);
2701 assert_eq!(t.data[0], 2.0);
2702 assert_eq!(t.data[1], 4.0);
2703 }
2704 other => panic!("expected tensor, got {other:?}"),
2705 }
2706 }
2707
2708 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2709 #[test]
2710 fn nanmax_output_count_returns_indices_and_zero_output_list() {
2711 let tensor = Tensor::new(vec![f64::NAN, 4.0, 2.0], vec![3, 1]).unwrap();
2712 let _guard = crate::output_count::push_output_count(Some(2));
2713 let result = nanmax_builtin(Value::Tensor(tensor), Vec::new()).expect("nanmax outputs");
2714 match result {
2715 Value::OutputList(values) => {
2716 assert_eq!(values.len(), 2);
2717 assert_eq!(values[0], Value::Num(4.0));
2718 assert_eq!(values[1], Value::Num(2.0));
2719 }
2720 other => panic!("expected output list, got {other:?}"),
2721 }
2722 drop(_guard);
2723
2724 let _guard = crate::output_count::push_output_count(Some(0));
2725 let result = nanmax_builtin(Value::Num(1.0), Vec::new()).expect("nanmax zero outputs");
2726 assert_eq!(result, Value::OutputList(Vec::new()));
2727 }
2728
2729 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2730 #[test]
2731 fn max_reduction_abs_comparison() {
2732 let tensor = Tensor::new(vec![1.0, -3.0, -2.0, 4.0], vec![2, 2]).unwrap();
2733 let args = vec![
2734 placeholder(),
2735 Value::from("ComparisonMethod"),
2736 Value::from("abs"),
2737 ];
2738 let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
2739 let (values, indices) = eval.into_pair();
2740 match values {
2741 Value::Tensor(t) => {
2742 assert_eq!(t.shape, vec![1, 2]);
2743 assert_eq!(t.data, vec![-3.0, 4.0]);
2744 }
2745 other => panic!("expected tensor result, got {other:?}"),
2746 }
2747 match indices {
2748 Value::Tensor(t) => {
2749 assert_eq!(t.data, vec![2.0, 2.0]);
2750 }
2751 other => panic!("expected tensor indices, got {other:?}"),
2752 }
2753 }
2754
2755 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2756 #[test]
2757 fn max_reduction_complex_real_comparison() {
2758 let tensor = ComplexTensor::new(vec![(1.0, 2.0), (0.5, 5.0)], vec![2, 1]).expect("tensor");
2759 let args = vec![
2760 placeholder(),
2761 Value::from("ComparisonMethod"),
2762 Value::from("real"),
2763 ];
2764 let eval = evaluate(Value::ComplexTensor(tensor), &args).expect("evaluate");
2765 let (values, indices) = eval.into_pair();
2766 match values {
2767 Value::Complex(re, im) => {
2768 assert!((re - 1.0).abs() < 1e-12);
2769 assert!((im - 2.0).abs() < 1e-12);
2770 }
2771 other => panic!("expected complex scalar, got {other:?}"),
2772 }
2773 assert_eq!(indices, Value::Num(1.0));
2774 }
2775
2776 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2777 #[test]
2778 fn max_elementwise_broadcast() {
2779 let lhs = Tensor::new(vec![1.0, 4.0, 7.0], vec![1, 3]).unwrap();
2780 let rhs = Tensor::new(vec![2.0, 3.0, 5.0], vec![3, 1]).unwrap();
2781 let eval = evaluate(Value::Tensor(lhs), &[Value::Tensor(rhs)]).expect("evaluate");
2782 let (values, indices) = eval.into_pair();
2783 match values {
2784 Value::Tensor(t) => {
2785 assert_eq!(t.shape, vec![3, 3]);
2786 assert_eq!([t.data[0], t.data[3], t.data[6]], [2.0, 4.0, 7.0]);
2787 assert_eq!([t.data[1], t.data[4], t.data[7]], [3.0, 4.0, 7.0]);
2788 assert_eq!([t.data[2], t.data[5], t.data[8]], [5.0, 5.0, 7.0]);
2789 }
2790 other => panic!("expected tensor, got {other:?}"),
2791 }
2792 match indices {
2793 Value::Tensor(t) => {
2794 assert_eq!(t.shape, vec![3, 3]);
2795 assert_eq!([t.data[0], t.data[3], t.data[6]], [2.0, 1.0, 1.0]);
2796 assert_eq!([t.data[1], t.data[4], t.data[7]], [2.0, 1.0, 1.0]);
2797 assert_eq!([t.data[2], t.data[5], t.data[8]], [2.0, 2.0, 1.0]);
2798 }
2799 other => panic!("expected tensor, got {other:?}"),
2800 }
2801 }
2802
2803 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2804 #[test]
2805 fn max_elementwise_abs_comparison() {
2806 let lhs = Tensor::new(vec![-2.0, 1.0], vec![2, 1]).unwrap();
2807 let rhs = Tensor::new(vec![1.5, -3.0], vec![2, 1]).unwrap();
2808 let args = vec![
2809 Value::Tensor(rhs),
2810 Value::from("ComparisonMethod"),
2811 Value::from("abs"),
2812 ];
2813 let eval = evaluate(Value::Tensor(lhs), &args).expect("evaluate");
2814 let (values, indices) = eval.into_pair();
2815 match values {
2816 Value::Tensor(t) => {
2817 assert_eq!(t.data, vec![-2.0, -3.0]);
2818 }
2819 other => panic!("expected tensor, got {other:?}"),
2820 }
2821 match indices {
2822 Value::Tensor(t) => {
2823 assert_eq!(t.data, vec![1.0, 2.0]);
2824 }
2825 other => panic!("expected tensor, got {other:?}"),
2826 }
2827 }
2828
2829 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2830 #[test]
2831 fn max_elementwise_omitnan_chooses_non_nan_side() {
2832 let lhs = Tensor::new(vec![f64::NAN, 2.0, f64::NAN], vec![3, 1]).unwrap();
2833 let rhs = Tensor::new(vec![3.0, f64::NAN, f64::NAN], vec![3, 1]).unwrap();
2834 let eval = evaluate(
2835 Value::Tensor(lhs),
2836 &[Value::Tensor(rhs), Value::from("omitnan")],
2837 )
2838 .expect("evaluate");
2839 let (values, indices) = eval.into_pair();
2840 match values {
2841 Value::Tensor(t) => {
2842 assert_eq!(t.shape, vec![3, 1]);
2843 assert_eq!(t.data[0], 3.0);
2844 assert_eq!(t.data[1], 2.0);
2845 assert!(t.data[2].is_nan());
2846 }
2847 other => panic!("expected tensor, got {other:?}"),
2848 }
2849 match indices {
2850 Value::Tensor(t) => {
2851 assert_eq!(t.data[0], 2.0);
2852 assert_eq!(t.data[1], 1.0);
2853 assert_eq!(t.data[2], 1.0);
2854 }
2855 other => panic!("expected tensor, got {other:?}"),
2856 }
2857 }
2858
2859 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2860 #[test]
2861 fn nanmax_elementwise_omits_nan_by_default() {
2862 let lhs = Tensor::new(vec![f64::NAN, 2.0], vec![2, 1]).unwrap();
2863 let rhs = Tensor::new(vec![3.0, f64::NAN], vec![2, 1]).unwrap();
2864 let result = nanmax_builtin(Value::Tensor(lhs), vec![Value::Tensor(rhs)]).expect("nanmax");
2865 match result {
2866 Value::Tensor(t) => assert_eq!(t.data, vec![3.0, 2.0]),
2867 other => panic!("expected tensor, got {other:?}"),
2868 }
2869 }
2870
2871 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2872 #[test]
2873 #[cfg(feature = "wgpu")]
2874 fn nanmax_gpu_omitnan_gathers_to_host_result() {
2875 let tensor = Tensor::new(vec![f64::NAN, 4.0, 2.0, f64::NAN], vec![2, 2]).unwrap();
2876 test_support::with_test_provider(|provider| {
2877 let view = HostTensorView {
2878 data: &tensor.data,
2879 shape: &tensor.shape,
2880 };
2881 let handle = provider.upload(&view).expect("upload");
2882 let result = nanmax_builtin(Value::GpuTensor(handle), Vec::new()).expect("nanmax gpu");
2883 match result {
2884 Value::Tensor(t) => {
2885 assert_eq!(t.shape, vec![1, 2]);
2886 assert_eq!(t.data, vec![4.0, 2.0]);
2887 }
2888 other => panic!("expected host tensor fallback, got {other:?}"),
2889 }
2890 });
2891 }
2892
2893 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2894 #[test]
2895 fn max_complex_real_comparison() {
2896 let lhs = ComplexTensor::new(vec![(1.0, 2.0)], vec![1, 1]).unwrap();
2897 let rhs = ComplexTensor::new(vec![(0.5, 5.0)], vec![1, 1]).unwrap();
2898 let args = vec![
2899 Value::ComplexTensor(rhs),
2900 Value::from("ComparisonMethod"),
2901 Value::from("real"),
2902 ];
2903 let eval = evaluate(Value::ComplexTensor(lhs), &args).expect("evaluate");
2904 let (values, indices) = eval.into_pair();
2905 assert_eq!(values, Value::Complex(1.0, 2.0));
2906 assert_eq!(indices, Value::Num(1.0));
2907 }
2908
2909 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2910 #[test]
2911 fn max_dimension_argument_parsing() {
2912 let tensor = Tensor::new(vec![3.0, 4.0, 1.0, 2.0], vec![2, 2]).unwrap();
2913 let dims = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
2914 let args = vec![placeholder(), Value::Tensor(dims)];
2915 let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
2916 let (values, indices) = eval.into_pair();
2917 assert_eq!(values, Value::Num(4.0));
2918 assert_eq!(indices, Value::Num(2.0));
2919 }
2920
2921 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2922 #[test]
2923 fn max_vecdim_duplicate_entries() {
2924 let tensor = Tensor::new(vec![5.0, 2.0, 7.0, 1.0], vec![2, 2]).unwrap();
2925 let dims = Tensor::new(vec![1.0, 1.0, 2.0], vec![3, 1]).unwrap();
2926 let args = vec![placeholder(), Value::Tensor(dims)];
2927 let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
2928 let (values, indices) = eval.into_pair();
2929 assert_eq!(values, Value::Num(7.0));
2930 assert_eq!(indices, Value::Num(3.0));
2931 }
2932
2933 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2934 #[test]
2935 fn max_dimension_gpu_argument_errors() {
2936 let tensor = Tensor::new(vec![3.0, 1.0], vec![2, 1]).unwrap();
2937 let dim_handle = Value::GpuTensor(runmat_accelerate_api::GpuTensorHandle {
2938 shape: vec![1, 1],
2939 device_id: 0,
2940 buffer_id: 42,
2941 });
2942 let err = evaluate(Value::Tensor(tensor), &[placeholder(), dim_handle])
2943 .expect_err("expected error");
2944 assert_eq!(err.identifier(), MAX_ERROR_INVALID_ARGUMENT.identifier);
2945 assert!(err
2946 .message()
2947 .contains("dimension arguments must reside on the host"));
2948 }
2949
2950 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2951 #[test]
2952 fn max_invalid_comparison_method_errors() {
2953 let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
2954 let args = vec![
2955 placeholder(),
2956 Value::from("ComparisonMethod"),
2957 Value::from("chebyshev"),
2958 ];
2959 let err = evaluate(Value::Tensor(tensor), &args).expect_err("expected error");
2960 assert_eq!(err.identifier(), MAX_ERROR_INVALID_ARGUMENT.identifier);
2961 assert!(err.message().contains("unsupported ComparisonMethod"));
2962 }
2963
2964 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
2965 #[test]
2966 #[cfg(feature = "wgpu")]
2967 fn max_gpu_dim1_matches_cpu() {
2968 let tensor = Tensor::new(vec![3.0, 1.0, 2.0, 4.0], vec![2, 2]).unwrap();
2969 let eval_cpu = evaluate(Value::Tensor(tensor.clone()), &[]).expect("cpu");
2970 let (values_cpu, indices_cpu) = eval_cpu.into_pair();
2971
2972 test_support::with_test_provider(|provider| {
2973 let view = HostTensorView {
2974 data: &tensor.data,
2975 shape: &tensor.shape,
2976 };
2977 let handle = provider.upload(&view).expect("upload");
2978 let eval_gpu = evaluate(Value::GpuTensor(handle), &[]).expect("gpu");
2979 let (values_gpu, indices_gpu) = eval_gpu.into_pair();
2980 match (&values_gpu, &indices_gpu) {
2981 (Value::GpuTensor(_), Value::GpuTensor(_)) => {}
2982 other => panic!("expected GPU tensors, got {other:?}"),
2983 }
2984 let gathered_vals = test_support::gather(values_gpu).expect("gather values");
2985 let gathered_idx = test_support::gather(indices_gpu).expect("gather indices");
2986 let expected_vals = match values_cpu {
2987 Value::Tensor(t) => t,
2988 other => panic!("expected tensor values from cpu eval, got {other:?}"),
2989 };
2990 let expected_idx = match indices_cpu {
2991 Value::Tensor(t) => t,
2992 other => panic!("expected tensor indices from cpu eval, got {other:?}"),
2993 };
2994 assert_eq!(gathered_vals.shape, expected_vals.shape);
2995 assert_eq!(gathered_vals.data, expected_vals.data);
2996 assert_eq!(gathered_idx.shape, expected_idx.shape);
2997 assert_eq!(gathered_idx.data, expected_idx.data);
2998 });
2999 }
3000
3001 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3002 #[test]
3003 fn max_dimension_numeric_argument() {
3004 let tensor = Tensor::new(vec![3.0, 4.0, 1.0, 2.0], vec![2, 2]).unwrap();
3005 let args = vec![placeholder(), Value::Num(2.0)];
3006 let eval = evaluate(Value::Tensor(tensor), &args).expect("evaluate");
3007 let (values, indices) = eval.into_pair();
3008 match values {
3009 Value::Tensor(t) => {
3010 assert_eq!(t.shape, vec![2, 1]);
3011 assert_eq!(t.data, vec![3.0, 4.0]);
3012 }
3013 other => panic!("expected tensor, got {other:?}"),
3014 }
3015 match indices {
3016 Value::Tensor(t) => {
3017 assert_eq!(t.data, vec![1.0, 1.0]);
3018 }
3019 other => panic!("expected tensor, got {other:?}"),
3020 }
3021 }
3022
3023 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3024 #[test]
3025 fn max_complex_auto_comparison() {
3026 let lhs = ComplexTensor::new(vec![(1.0, 2.0)], vec![1, 1]).unwrap();
3027 let rhs = ComplexTensor::new(vec![(2.0, 1.0)], vec![1, 1]).unwrap();
3028 let eval =
3029 evaluate(Value::ComplexTensor(lhs), &[Value::ComplexTensor(rhs)]).expect("evaluate");
3030 let (values, indices) = eval.into_pair();
3031 assert_eq!(values, Value::Complex(1.0, 2.0));
3032 assert_eq!(indices, Value::Num(1.0));
3033 }
3034
3035 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3036 #[test]
3037 fn max_scalar_pair_arguments() {
3038 let args = vec![Value::Num(2.0)];
3039 let result = max_builtin(Value::Num(3.0), args).expect("max");
3040 assert_eq!(result, Value::Num(3.0));
3041 }
3042
3043 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
3044 #[test]
3045 fn max_rejects_invalid_dimension() {
3046 let tensor = Tensor::new(vec![1.0, 2.0, 3.0], vec![3, 1]).unwrap();
3047 let args = vec![placeholder(), Value::Int(IntValue::I32(0))];
3048 let err = evaluate(Value::Tensor(tensor), &args).expect_err("expected error");
3049 assert_eq!(err.identifier(), MAX_ERROR_INVALID_ARGUMENT.identifier);
3050 assert!(err.message().contains("dimension must be >= 1"));
3051 }
3052}