1use std::ops::Add;
2
3use num_traits::Zero;
4
5use super::indexing_alloc::pooled_uninit_tensor;
6use super::typed_host_data;
7use crate::buffer_pool::{BufferPool, PoolScalar};
8use tenferro_tensor::{GatherConfig, PadConfig, ScatterConfig, SliceConfig};
9use tenferro_tensor::{Tensor, TypedTensor};
10
11trait TensorAsTyped<T> {
18 fn as_typed(&self) -> Option<&TypedTensor<T>>;
19}
20
21macro_rules! impl_tensor_as_typed {
22 ($(($ty:ty, $variant:ident)),+ $(,)?) => {
23 $(
24 impl TensorAsTyped<$ty> for Tensor {
25 fn as_typed(&self) -> Option<&TypedTensor<$ty>> {
26 match self {
27 Tensor::$variant(tensor) => Some(tensor),
28 _ => None,
29 }
30 }
31 }
32 )+
33 };
34}
35
36impl_tensor_as_typed!(
37 (f32, F32),
38 (f64, F64),
39 (i32, I32),
40 (i64, I64),
41 (bool, Bool),
42 (num_complex::Complex<f32>, C32),
43 (num_complex::Complex<f64>, C64),
44);
45
46macro_rules! dispatch_tensor_unary_result {
47 ($input:expr, |$tensor:ident| $body:expr) => {
48 match $input {
49 Tensor::F32($tensor) => Ok(Tensor::F32($body?)),
50 Tensor::F64($tensor) => Ok(Tensor::F64($body?)),
51 Tensor::I32($tensor) => Ok(Tensor::I32($body?)),
52 Tensor::I64($tensor) => Ok(Tensor::I64($body?)),
53 Tensor::Bool($tensor) => Ok(Tensor::Bool($body?)),
54 Tensor::C32($tensor) => Ok(Tensor::C32($body?)),
55 Tensor::C64($tensor) => Ok(Tensor::C64($body?)),
56 }
57 };
58}
59
60macro_rules! dispatch_tensor_unary_with_bool_special_result {
61 ($input:expr, |$tensor:ident| $body:expr, bool |$bool_tensor:ident| $bool_body:expr) => {
62 match $input {
63 Tensor::F32($tensor) => Ok(Tensor::F32($body?)),
64 Tensor::F64($tensor) => Ok(Tensor::F64($body?)),
65 Tensor::I32($tensor) => Ok(Tensor::I32($body?)),
66 Tensor::I64($tensor) => Ok(Tensor::I64($body?)),
67 Tensor::Bool($bool_tensor) => Ok(Tensor::Bool($bool_body?)),
68 Tensor::C32($tensor) => Ok(Tensor::C32($body?)),
69 Tensor::C64($tensor) => Ok(Tensor::C64($body?)),
70 }
71 };
72}
73
74macro_rules! dispatch_same_dtype_result {
75 ($op:literal, $lhs:expr, $rhs:expr, |$lhs_t:ident, $rhs_t:ident| $body:expr) => {
76 match ($lhs, $rhs) {
77 (Tensor::F32($lhs_t), Tensor::F32($rhs_t)) => Ok(Tensor::F32($body?)),
78 (Tensor::F64($lhs_t), Tensor::F64($rhs_t)) => Ok(Tensor::F64($body?)),
79 (Tensor::I32($lhs_t), Tensor::I32($rhs_t)) => Ok(Tensor::I32($body?)),
80 (Tensor::I64($lhs_t), Tensor::I64($rhs_t)) => Ok(Tensor::I64($body?)),
81 (Tensor::Bool($lhs_t), Tensor::Bool($rhs_t)) => Ok(Tensor::Bool($body?)),
82 (Tensor::C32($lhs_t), Tensor::C32($rhs_t)) => Ok(Tensor::C32($body?)),
83 (Tensor::C64($lhs_t), Tensor::C64($rhs_t)) => Ok(Tensor::C64($body?)),
84 _ => Err(crate::Error::DTypeMismatch {
85 op: $op,
86 lhs: $lhs.dtype(),
87 rhs: $rhs.dtype(),
88 }),
89 }
90 };
91}
92
93macro_rules! dispatch_same_dtype_without_bool_result {
94 ($op:literal, $lhs:expr, $rhs:expr, $bool_message:literal, |$lhs_t:ident, $rhs_t:ident| $body:expr) => {
95 match ($lhs, $rhs) {
96 (Tensor::F32($lhs_t), Tensor::F32($rhs_t)) => Ok(Tensor::F32($body?)),
97 (Tensor::F64($lhs_t), Tensor::F64($rhs_t)) => Ok(Tensor::F64($body?)),
98 (Tensor::I32($lhs_t), Tensor::I32($rhs_t)) => Ok(Tensor::I32($body?)),
99 (Tensor::I64($lhs_t), Tensor::I64($rhs_t)) => Ok(Tensor::I64($body?)),
100 (Tensor::C32($lhs_t), Tensor::C32($rhs_t)) => Ok(Tensor::C32($body?)),
101 (Tensor::C64($lhs_t), Tensor::C64($rhs_t)) => Ok(Tensor::C64($body?)),
102 (Tensor::Bool(_), Tensor::Bool(_)) => {
103 Err(crate::Error::backend_failure($op, $bool_message))
104 }
105 _ => Err(crate::Error::DTypeMismatch {
106 op: $op,
107 lhs: $lhs.dtype(),
108 rhs: $rhs.dtype(),
109 }),
110 }
111 };
112}
113
114fn with_local_pool<T>(f: impl FnOnce(&mut BufferPool) -> T) -> T {
115 let mut buffers = BufferPool::new();
116 f(&mut buffers)
117}
118
119fn advance_col_major_index(index: &mut [usize], shape: &[usize]) {
120 debug_assert_eq!(index.len(), shape.len());
121 for axis in 0..index.len() {
122 if shape[axis] == 0 {
123 index[axis] = 0;
124 continue;
125 }
126 index[axis] += 1;
127 if index[axis] < shape[axis] {
128 break;
129 }
130 index[axis] = 0;
131 }
132}
133
134fn pooled_filled_tensor<T>(
135 buffers: &mut BufferPool,
136 shape: Vec<usize>,
137 fill: T,
138) -> crate::Result<TypedTensor<T>>
139where
140 T: Copy + Clone + PoolScalar,
141{
142 let mut out = pooled_uninit_tensor(buffers, shape)?;
144 out.host_data_mut()?.fill(fill);
145 Ok(out)
146}
147
148fn clone_host_tensor_from_pool<T>(
149 buffers: &mut BufferPool,
150 op: &'static str,
151 tensor: &TypedTensor<T>,
152) -> crate::Result<TypedTensor<T>>
153where
154 T: Copy + Clone + PoolScalar,
155{
156 let mut out = pooled_uninit_tensor(buffers, tensor.shape().to_vec())?;
158 out.host_data_mut()?
159 .copy_from_slice(typed_host_data(op, tensor)?);
160 Ok(out)
161}
162
163pub fn gather(
164 operand: &Tensor,
165 start_indices: &Tensor,
166 config: &GatherConfig,
167) -> crate::Result<Tensor> {
168 with_local_pool(|buffers| gather_with_pool(buffers, operand, start_indices, config))
169}
170
171pub(crate) fn gather_with_pool(
172 buffers: &mut BufferPool,
173 operand: &Tensor,
174 start_indices: &Tensor,
175 config: &GatherConfig,
176) -> crate::Result<Tensor> {
177 let start_indices = try_index_tensor(start_indices)?;
178 dispatch_tensor_unary_result!(operand, |t| typed_gather(
179 buffers,
180 t,
181 &start_indices,
182 config
183 ))
184}
185
186pub fn scatter(
187 operand: &Tensor,
188 scatter_indices: &Tensor,
189 updates: &Tensor,
190 config: &ScatterConfig,
191) -> crate::Result<Tensor> {
192 with_local_pool(|buffers| scatter_with_pool(buffers, operand, scatter_indices, updates, config))
193}
194
195pub(crate) fn scatter_with_pool(
196 buffers: &mut BufferPool,
197 operand: &Tensor,
198 scatter_indices: &Tensor,
199 updates: &Tensor,
200 config: &ScatterConfig,
201) -> crate::Result<Tensor> {
202 let scatter_indices = try_index_tensor(scatter_indices)?;
203 dispatch_same_dtype_without_bool_result!(
204 "scatter",
205 operand,
206 updates,
207 "Bool data tensors are not supported by additive scatter",
208 |op, upd| typed_scatter(buffers, op, &scatter_indices, upd, config)
209 )
210}
211
212pub(crate) fn try_slice_with_pool(
213 buffers: &mut BufferPool,
214 input: &Tensor,
215 config: &SliceConfig,
216) -> crate::Result<Tensor> {
217 dispatch_tensor_unary_result!(input, |t| typed_slice(buffers, t, config))
218}
219
220pub fn dynamic_slice(
221 input: &Tensor,
222 starts: &Tensor,
223 slice_sizes: &[usize],
224) -> crate::Result<Tensor> {
225 with_local_pool(|buffers| dynamic_slice_with_pool(buffers, input, starts, slice_sizes))
226}
227
228pub(crate) fn dynamic_slice_with_pool(
229 buffers: &mut BufferPool,
230 input: &Tensor,
231 starts: &Tensor,
232 slice_sizes: &[usize],
233) -> crate::Result<Tensor> {
234 let starts = try_index_tensor(starts)?;
235 dispatch_tensor_unary_result!(input, |t| typed_dynamic_slice(
236 buffers,
237 t,
238 &starts,
239 slice_sizes
240 ))
241}
242
243pub fn dynamic_update_slice(
263 operand: &Tensor,
264 update: &Tensor,
265 starts: &Tensor,
266) -> crate::Result<Tensor> {
267 with_local_pool(|buffers| dynamic_update_slice_with_pool(buffers, operand, update, starts))
268}
269
270pub(crate) fn dynamic_update_slice_with_pool(
271 buffers: &mut BufferPool,
272 operand: &Tensor,
273 update: &Tensor,
274 starts: &Tensor,
275) -> crate::Result<Tensor> {
276 let starts = try_index_tensor(starts)?;
277 dispatch_same_dtype_result!("dynamic_update_slice", operand, update, |op, upd| {
278 typed_dynamic_update_slice(buffers, op, upd, &starts)
279 })
280}
281
282pub fn pad(input: &Tensor, config: &PadConfig) -> crate::Result<Tensor> {
283 try_pad(input, config)
284}
285
286fn try_pad(input: &Tensor, config: &PadConfig) -> crate::Result<Tensor> {
287 with_local_pool(|buffers| try_pad_with_pool(buffers, input, config))
288}
289
290pub(crate) fn try_pad_with_pool(
291 buffers: &mut BufferPool,
292 input: &Tensor,
293 config: &PadConfig,
294) -> crate::Result<Tensor> {
295 dispatch_tensor_unary_with_bool_special_result!(
296 input,
297 |t| typed_pad(buffers, t, config),
298 bool | t | typed_pad_with_fill(buffers, t, config, false)
299 )
300}
301
302pub(crate) fn try_concatenate_with_pool(
303 buffers: &mut BufferPool,
304 inputs: &[&Tensor],
305 axis: usize,
306) -> crate::Result<Tensor> {
307 let first = inputs
308 .first()
309 .copied()
310 .ok_or_else(|| crate::Error::InvalidConfig {
311 op: "concatenate",
312 message: "concatenate requires at least one input".into(),
313 })?;
314 dispatch_tensor_unary_result!(first, |t| typed_concatenate_from_dyn_inputs(
315 buffers, t, inputs, axis
316 ))
317}
318
319pub(crate) fn reverse_with_pool(
320 buffers: &mut BufferPool,
321 input: &Tensor,
322 axes: &[usize],
323) -> crate::Result<Tensor> {
324 dispatch_tensor_unary_result!(input, |t| typed_reverse(buffers, t, axes))
325}
326
327fn typed_slice<T: Copy + Clone + PoolScalar>(
328 buffers: &mut BufferPool,
329 input: &TypedTensor<T>,
330 config: &SliceConfig,
331) -> crate::Result<TypedTensor<T>> {
332 let input_shape = input.shape();
333 let rank = input_shape.len();
334 if config.starts.len() != rank {
335 return Err(crate::Error::RankMismatch {
336 op: "slice",
337 expected: rank,
338 actual: config.starts.len(),
339 });
340 }
341 if config.limits.len() != rank {
342 return Err(crate::Error::RankMismatch {
343 op: "slice",
344 expected: rank,
345 actual: config.limits.len(),
346 });
347 }
348 if config.strides.len() != rank {
349 return Err(crate::Error::RankMismatch {
350 op: "slice",
351 expected: rank,
352 actual: config.strides.len(),
353 });
354 }
355
356 let out_shape: Vec<usize> = input
357 .shape()
358 .iter()
359 .enumerate()
360 .map(|(axis, &dim)| {
361 let start = config.starts[axis];
362 let limit = config.limits[axis];
363 let stride = config.strides[axis];
364 if start > limit {
365 return Err(crate::Error::InvalidConfig {
366 op: "slice",
367 message: format!("start exceeds limit on axis {axis}"),
368 });
369 }
370 if limit > dim {
371 return Err(crate::Error::AxisOutOfBounds {
372 op: "slice",
373 axis,
374 rank,
375 });
376 }
377 if stride == 0 {
378 return Err(crate::Error::InvalidConfig {
379 op: "slice",
380 message: format!("stride must be positive on axis {axis}"),
381 });
382 }
383 let span = limit - start;
384 Ok(span.div_ceil(stride))
385 })
386 .collect::<crate::Result<Vec<_>>>()?;
387
388 let mut out = pooled_uninit_tensor(buffers, out_shape.clone())?;
390 let mut out_idx = vec![0usize; rank];
391 let mut in_idx = vec![0usize; rank];
392
393 for out_value in out.host_data_mut()?.iter_mut() {
394 for axis in 0..rank {
395 in_idx[axis] = config.starts[axis] + out_idx[axis] * config.strides[axis];
396 }
397 *out_value = *input.get(&in_idx)?;
398 advance_col_major_index(&mut out_idx, &out_shape);
399 }
400
401 Ok(out)
402}
403
404fn typed_concatenate_from_dyn_inputs<T>(
405 buffers: &mut BufferPool,
406 _first: &TypedTensor<T>,
407 inputs: &[&Tensor],
408 axis: usize,
409) -> crate::Result<TypedTensor<T>>
410where
411 T: Copy + Clone + PoolScalar,
412 Tensor: TensorAsTyped<T>,
413{
414 let first_dtype = inputs
415 .first()
416 .ok_or_else(|| crate::Error::InvalidConfig {
417 op: "concatenate",
418 message: "concatenate requires at least one input".into(),
419 })?
420 .dtype();
421 let typed_inputs = collect_typed_inputs(first_dtype, inputs)?;
422 typed_concatenate(buffers, &typed_inputs, axis)
423}
424
425fn collect_typed_inputs<'a, T>(
426 first_dtype: crate::DType,
427 inputs: &[&'a Tensor],
428) -> crate::Result<Vec<&'a TypedTensor<T>>>
429where
430 Tensor: TensorAsTyped<T>,
431{
432 inputs
433 .iter()
434 .map(|tensor| {
435 TensorAsTyped::<T>::as_typed(*tensor).ok_or_else(|| crate::Error::DTypeMismatch {
436 op: "concatenate",
437 lhs: first_dtype,
438 rhs: tensor.dtype(),
439 })
440 })
441 .collect()
442}
443
444fn typed_concatenate<T: Copy + Clone + PoolScalar>(
445 buffers: &mut BufferPool,
446 inputs: &[&TypedTensor<T>],
447 axis: usize,
448) -> crate::Result<TypedTensor<T>> {
449 let first = inputs
450 .first()
451 .copied()
452 .ok_or_else(|| crate::Error::InvalidConfig {
453 op: "concatenate",
454 message: "concatenate requires at least one input".into(),
455 })?;
456 let first_shape = first.shape();
457 let rank = first_shape.len();
458 if axis >= rank {
459 return Err(crate::Error::AxisOutOfBounds {
460 op: "concatenate",
461 axis,
462 rank,
463 });
464 }
465
466 let mut out_shape = first_shape.to_vec();
467 let mut axis_extent = 0usize;
468 for input in inputs {
469 let input_shape = input.shape();
470 if input_shape.len() != rank {
471 return Err(crate::Error::RankMismatch {
472 op: "concatenate",
473 expected: rank,
474 actual: input_shape.len(),
475 });
476 }
477 for dim in 0..rank {
478 if dim == axis {
479 axis_extent = axis_extent.checked_add(input_shape[dim]).ok_or_else(|| {
480 crate::Error::InvalidConfig {
481 op: "concatenate",
482 message: "concatenate axis extent overflows usize".to_string(),
483 }
484 })?;
485 } else if input_shape[dim] != first_shape[dim] {
486 return Err(crate::Error::ShapeMismatch {
487 op: "concatenate",
488 lhs: first_shape.to_vec(),
489 rhs: input_shape.to_vec(),
490 });
491 }
492 }
493 }
494 out_shape[axis] = axis_extent;
495
496 let mut segment_ends = Vec::with_capacity(inputs.len());
497 let mut segment_end = 0usize;
498 for input in inputs {
499 segment_end = segment_end
500 .checked_add(input.shape()[axis])
501 .ok_or_else(|| crate::Error::InvalidConfig {
502 op: "concatenate",
503 message: "concatenate segment offset overflows usize".to_string(),
504 })?;
505 segment_ends.push(segment_end);
506 }
507
508 let mut out = pooled_uninit_tensor(buffers, out_shape.clone())?;
510 let mut out_idx = vec![0usize; rank];
511 let mut in_idx = vec![0usize; rank];
512
513 for out_value in out.host_data_mut()?.iter_mut() {
514 let concat_idx = out_idx[axis];
515 let input_pos = segment_ends.partition_point(|&end| concat_idx >= end);
516 if input_pos == segment_ends.len() {
517 return Err(crate::Error::InvalidConfig {
518 op: "concatenate",
519 message: "output index must map to an input".to_string(),
520 });
521 }
522 let axis_base = if input_pos == 0 {
523 0
524 } else {
525 segment_ends[input_pos - 1]
526 };
527
528 in_idx.copy_from_slice(&out_idx);
529 in_idx[axis] -= axis_base;
530 *out_value = *inputs[input_pos].get(&in_idx)?;
531 advance_col_major_index(&mut out_idx, &out_shape);
532 }
533
534 Ok(out)
535}
536
537fn typed_reverse<T: Copy + Clone + PoolScalar>(
538 buffers: &mut BufferPool,
539 input: &TypedTensor<T>,
540 axes: &[usize],
541) -> crate::Result<TypedTensor<T>> {
542 let input_shape = input.shape();
543 let rank = input_shape.len();
544 let mut reverse_axis = vec![false; rank];
545 for &axis in axes {
546 if axis >= rank {
547 return Err(crate::Error::AxisOutOfBounds {
548 op: "reverse",
549 axis,
550 rank,
551 });
552 }
553 reverse_axis[axis] = true;
554 }
555
556 let mut out = pooled_uninit_tensor(buffers, input_shape.to_vec())?;
558 let mut out_idx = vec![0usize; rank];
559 let mut in_idx = vec![0usize; rank];
560
561 for out_value in out.host_data_mut()?.iter_mut() {
562 for axis in 0..rank {
563 in_idx[axis] = if reverse_axis[axis] {
564 input_shape[axis] - 1 - out_idx[axis]
565 } else {
566 out_idx[axis]
567 };
568 }
569 *out_value = *input.get(&in_idx)?;
570 advance_col_major_index(&mut out_idx, input_shape);
571 }
572
573 Ok(out)
574}
575
576struct IndexTensor {
577 shape: Vec<usize>,
578 values: Vec<i64>,
579}
580
581const F32_MAX_EXACT_INT: f32 = 16_777_216.0;
583const F64_MAX_EXACT_INT: f64 = 9_007_199_254_740_992.0;
585
586fn f32_index_to_i64(value: f32) -> crate::Result<i64> {
587 if !value.is_finite() || value.fract() != 0.0 || value.abs() > F32_MAX_EXACT_INT {
588 return Err(crate::Error::InvalidConfig {
589 op: "index_tensor",
590 message: format!("index value {value} is not an exactly representable i64"),
591 });
592 }
593 Ok(value as i64)
594}
595
596fn f64_index_to_i64(value: f64) -> crate::Result<i64> {
597 if !value.is_finite() || value.fract() != 0.0 || value.abs() > F64_MAX_EXACT_INT {
598 return Err(crate::Error::InvalidConfig {
599 op: "index_tensor",
600 message: format!("index value {value} is not an exactly representable i64"),
601 });
602 }
603 Ok(value as i64)
604}
605
606fn try_index_tensor(tensor: &Tensor) -> crate::Result<IndexTensor> {
607 match tensor {
608 Tensor::I32(t) => Ok(IndexTensor {
609 shape: t.shape().to_vec(),
610 values: typed_host_data("index_tensor", t)?
611 .iter()
612 .map(|&value| value as i64)
613 .collect(),
614 }),
615 Tensor::I64(t) => Ok(IndexTensor {
616 shape: t.shape().to_vec(),
617 values: typed_host_data("index_tensor", t)?.to_vec(),
618 }),
619 Tensor::F32(t) => {
620 let values: crate::Result<Vec<i64>> = typed_host_data("index_tensor", t)?
621 .iter()
622 .map(|&value| f32_index_to_i64(value))
623 .collect();
624 Ok(IndexTensor {
625 shape: t.shape().to_vec(),
626 values: values?,
627 })
628 }
629 Tensor::F64(t) => {
630 let values: crate::Result<Vec<i64>> = typed_host_data("index_tensor", t)?
631 .iter()
632 .map(|&value| f64_index_to_i64(value))
633 .collect();
634 Ok(IndexTensor {
635 shape: t.shape().to_vec(),
636 values: values?,
637 })
638 }
639 Tensor::Bool(_) => Err(crate::Error::InvalidConfig {
640 op: "index_tensor",
641 message: "bool index tensors are not supported".into(),
642 }),
643 Tensor::C32(_) | Tensor::C64(_) => Err(crate::Error::InvalidConfig {
644 op: "index_tensor",
645 message: "complex index tensors are not supported".into(),
646 }),
647 }
648}
649
650fn checked_product(op: &'static str, role: &'static str, shape: &[usize]) -> crate::Result<usize> {
651 shape.iter().try_fold(1usize, |acc, &dim| {
652 acc.checked_mul(dim)
653 .ok_or_else(|| crate::Error::InvalidConfig {
654 op,
655 message: format!("{role} element count overflows usize"),
656 })
657 })
658}
659
660fn linear_offset(op: &'static str, shape: &[usize], indices: &[usize]) -> crate::Result<usize> {
661 if indices.len() != shape.len() {
662 return Err(crate::Error::RankMismatch {
663 op,
664 expected: shape.len(),
665 actual: indices.len(),
666 });
667 }
668 let mut offset = 0usize;
669 let mut stride = 1usize;
670 for (axis, &index) in indices.iter().enumerate() {
671 if index >= shape[axis] {
672 return Err(crate::Error::AxisOutOfBounds {
673 op,
674 axis,
675 rank: shape.len(),
676 });
677 }
678 let scaled = index
679 .checked_mul(stride)
680 .ok_or_else(|| crate::Error::InvalidConfig {
681 op,
682 message: format!("linear index component overflows usize on axis {axis}"),
683 })?;
684 offset = offset
685 .checked_add(scaled)
686 .ok_or_else(|| crate::Error::InvalidConfig {
687 op,
688 message: format!("linear offset overflows usize on axis {axis}"),
689 })?;
690 stride = stride
691 .checked_mul(shape[axis])
692 .ok_or_else(|| crate::Error::InvalidConfig {
693 op,
694 message: format!("linear stride overflows usize after axis {axis}"),
695 })?;
696 }
697 Ok(offset)
698}
699
700fn try_index_vector_size(
701 op: &'static str,
702 shape: &[usize],
703 index_vector_dim: usize,
704) -> crate::Result<usize> {
705 if index_vector_dim > shape.len() {
706 return Err(crate::Error::AxisOutOfBounds {
707 op,
708 axis: index_vector_dim,
709 rank: shape.len(),
710 });
711 }
712 Ok(if index_vector_dim == shape.len() {
713 1
714 } else {
715 shape[index_vector_dim]
716 })
717}
718
719fn try_index_batch_shape(
720 op: &'static str,
721 shape: &[usize],
722 index_vector_dim: usize,
723) -> crate::Result<Vec<usize>> {
724 if index_vector_dim > shape.len() {
725 return Err(crate::Error::AxisOutOfBounds {
726 op,
727 axis: index_vector_dim,
728 rank: shape.len(),
729 });
730 }
731 if index_vector_dim == shape.len() {
732 return Ok(shape.to_vec());
733 }
734 Ok(shape
735 .iter()
736 .enumerate()
737 .filter_map(|(axis, &dim)| (axis != index_vector_dim).then_some(dim))
738 .collect())
739}
740
741fn index_component(
742 op: &'static str,
743 indices: &IndexTensor,
744 batch_idx: &[usize],
745 index_vector_dim: usize,
746 component: usize,
747 index_scratch: &mut [usize],
748) -> crate::Result<i64> {
749 if index_vector_dim == indices.shape.len() {
750 if component != 0 {
751 return Err(crate::Error::InvalidConfig {
752 op,
753 message: "implicit index_vector_dim only supports scalar index vectors".into(),
754 });
755 }
756 return Ok(indices.values[linear_offset(op, &indices.shape, batch_idx)?]);
757 }
758
759 if index_scratch.len() != indices.shape.len() {
760 return Err(crate::Error::InvalidConfig {
761 op,
762 message: format!(
763 "index scratch length {} must match index tensor rank {}",
764 index_scratch.len(),
765 indices.shape.len()
766 ),
767 });
768 }
769 if batch_idx.len() + 1 != indices.shape.len() {
770 return Err(crate::Error::InvalidConfig {
771 op,
772 message: format!(
773 "batch index rank {} must be one less than index tensor rank {}",
774 batch_idx.len(),
775 indices.shape.len()
776 ),
777 });
778 }
779 let mut batch_axis = 0usize;
780 for (axis, slot) in index_scratch.iter_mut().enumerate() {
781 if axis == index_vector_dim {
782 *slot = component;
783 } else {
784 *slot = batch_idx[batch_axis];
785 batch_axis += 1;
786 }
787 }
788 Ok(indices.values[linear_offset(op, &indices.shape, index_scratch)?])
789}
790
791fn clamp_window_start(
792 op: &'static str,
793 start: i64,
794 dim_size: usize,
795 window_size: usize,
796) -> crate::Result<usize> {
797 if window_size > dim_size {
798 return Err(crate::Error::InvalidConfig {
799 op,
800 message: format!("window size {window_size} exceeds dimension size {dim_size}"),
801 });
802 }
803 let max_start = dim_size.saturating_sub(window_size) as i64;
804 Ok(start.clamp(0, max_start) as usize)
805}
806
807fn operand_window_dims(rank: usize, collapsed_or_inserted: &[usize]) -> Vec<usize> {
808 (0..rank)
809 .filter(|dim| !collapsed_or_inserted.contains(dim))
810 .collect()
811}
812
813fn typed_gather<T: Copy + Clone + PoolScalar>(
814 buffers: &mut BufferPool,
815 operand: &TypedTensor<T>,
816 start_indices: &IndexTensor,
817 config: &GatherConfig,
818) -> crate::Result<TypedTensor<T>> {
819 let operand_shape = operand.shape();
820 let rank = operand_shape.len();
821 if config.slice_sizes.len() != rank {
822 return Err(crate::Error::RankMismatch {
823 op: "gather",
824 expected: rank,
825 actual: config.slice_sizes.len(),
826 });
827 }
828
829 for &dim in &config.collapsed_slice_dims {
830 if dim >= rank {
831 return Err(crate::Error::AxisOutOfBounds {
832 op: "gather",
833 axis: dim,
834 rank,
835 });
836 }
837 }
838 {
839 let mut seen = vec![false; rank];
840 for &dim in &config.collapsed_slice_dims {
841 if seen[dim] {
842 return Err(crate::Error::DuplicateAxis {
843 op: "gather",
844 axis: dim,
845 role: "collapsed_slice_dims",
846 });
847 }
848 seen[dim] = true;
849 }
850 }
851 for &dim in &config.collapsed_slice_dims {
852 if config.slice_sizes[dim] != 1 {
853 return Err(crate::Error::InvalidConfig {
854 op: "gather",
855 message: format!(
856 "collapsed slice dimension {dim} must have slice_size == 1, got {}",
857 config.slice_sizes[dim]
858 ),
859 });
860 }
861 }
862
863 let index_size =
864 try_index_vector_size("gather", &start_indices.shape, config.index_vector_dim)?;
865 if index_size != config.start_index_map.len() {
866 return Err(crate::Error::InvalidConfig {
867 op: "gather",
868 message: format!(
869 "start_index_map length {} does not match index vector size {}",
870 config.start_index_map.len(),
871 index_size
872 ),
873 });
874 }
875 for &operand_dim in &config.start_index_map {
876 if operand_dim >= rank {
877 return Err(crate::Error::AxisOutOfBounds {
878 op: "gather",
879 axis: operand_dim,
880 rank,
881 });
882 }
883 }
884 {
885 let mut seen = vec![false; rank];
886 for &operand_dim in &config.start_index_map {
887 if seen[operand_dim] {
888 return Err(crate::Error::DuplicateAxis {
889 op: "gather",
890 axis: operand_dim,
891 role: "start_index_map",
892 });
893 }
894 seen[operand_dim] = true;
895 }
896 }
897
898 let window_dims = operand_window_dims(rank, &config.collapsed_slice_dims);
899 if config.offset_dims.len() != window_dims.len() {
900 return Err(crate::Error::InvalidConfig {
901 op: "gather",
902 message: format!(
903 "offset_dims length {} does not match window dims count {}",
904 config.offset_dims.len(),
905 window_dims.len()
906 ),
907 });
908 }
909
910 let batch_shape =
911 try_index_batch_shape("gather", &start_indices.shape, config.index_vector_dim)?;
912 let out_rank = batch_shape.len() + config.offset_dims.len();
913 for &out_axis in &config.offset_dims {
914 if out_axis >= out_rank {
915 return Err(crate::Error::AxisOutOfBounds {
916 op: "gather",
917 axis: out_axis,
918 rank: out_rank,
919 });
920 }
921 }
922 {
923 let mut seen = vec![false; out_rank];
924 for &out_axis in &config.offset_dims {
925 if seen[out_axis] {
926 return Err(crate::Error::DuplicateAxis {
927 op: "gather",
928 axis: out_axis,
929 role: "offset_dims",
930 });
931 }
932 seen[out_axis] = true;
933 }
934 }
935
936 let mut out_axis_to_operand_dim = vec![None; out_rank];
937 for (offset_axis, &out_axis) in config.offset_dims.iter().enumerate() {
938 out_axis_to_operand_dim[out_axis] = Some(window_dims[offset_axis]);
939 }
940
941 let mut out_shape = vec![0usize; out_rank];
942 let mut batch_axis = 0usize;
943 for out_axis in 0..out_rank {
944 if let Some(operand_dim) = out_axis_to_operand_dim[out_axis] {
945 out_shape[out_axis] = config.slice_sizes[operand_dim];
946 } else {
947 out_shape[out_axis] = batch_shape[batch_axis];
948 batch_axis += 1;
949 }
950 }
951
952 for &operand_dim in &config.start_index_map {
953 let _ = clamp_window_start(
954 "gather",
955 0,
956 operand_shape[operand_dim],
957 config.slice_sizes[operand_dim],
958 )?;
959 }
960
961 let mut out = pooled_uninit_tensor(buffers, out_shape.clone())?;
963 let mut out_idx = vec![0usize; out_rank];
964 let mut batch_idx = vec![0usize; batch_shape.len()];
965 let mut operand_idx = vec![0usize; rank];
966 let mut window_offsets = vec![0usize; rank];
967 let mut index_scratch = vec![0usize; start_indices.shape.len()];
968
969 for out_value in out.host_data_mut()?.iter_mut() {
970 batch_axis = 0;
971 window_offsets.fill(0);
972 for out_axis in 0..out_rank {
973 if let Some(operand_dim) = out_axis_to_operand_dim[out_axis] {
974 window_offsets[operand_dim] = out_idx[out_axis];
975 } else {
976 batch_idx[batch_axis] = out_idx[out_axis];
977 batch_axis += 1;
978 }
979 }
980
981 operand_idx.fill(0);
982 for (component, &operand_dim) in config.start_index_map.iter().enumerate() {
983 let start = index_component(
984 "gather",
985 start_indices,
986 &batch_idx,
987 config.index_vector_dim,
988 component,
989 &mut index_scratch,
990 )?;
991 operand_idx[operand_dim] = clamp_window_start(
992 "gather",
993 start,
994 operand_shape[operand_dim],
995 config.slice_sizes[operand_dim],
996 )?;
997 }
998
999 for axis in 0..operand_idx.len() {
1000 operand_idx[axis] += window_offsets[axis];
1001 }
1002
1003 *out_value = *operand.get(&operand_idx)?;
1004 advance_col_major_index(&mut out_idx, &out_shape);
1005 }
1006
1007 Ok(out)
1008}
1009
1010fn typed_scatter<T>(
1011 buffers: &mut BufferPool,
1012 operand: &TypedTensor<T>,
1013 scatter_indices: &IndexTensor,
1014 updates: &TypedTensor<T>,
1015 config: &ScatterConfig,
1016) -> crate::Result<TypedTensor<T>>
1017where
1018 T: Copy + Clone + Zero + Add<Output = T> + PoolScalar,
1019{
1020 let operand_shape = operand.shape();
1021 let updates_shape = updates.shape();
1022 let op_rank = operand_shape.len();
1023 for &dim in &config.inserted_window_dims {
1024 if dim >= op_rank {
1025 return Err(crate::Error::AxisOutOfBounds {
1026 op: "scatter",
1027 axis: dim,
1028 rank: op_rank,
1029 });
1030 }
1031 }
1032 {
1033 let mut seen = vec![false; op_rank];
1034 for &dim in &config.inserted_window_dims {
1035 if seen[dim] {
1036 return Err(crate::Error::DuplicateAxis {
1037 op: "scatter",
1038 axis: dim,
1039 role: "inserted_window_dims",
1040 });
1041 }
1042 seen[dim] = true;
1043 }
1044 }
1045
1046 let index_size =
1047 try_index_vector_size("scatter", &scatter_indices.shape, config.index_vector_dim)?;
1048 if index_size != config.scatter_dims_to_operand_dims.len() {
1049 return Err(crate::Error::InvalidConfig {
1050 op: "scatter",
1051 message: format!(
1052 "scatter_dims_to_operand_dims length {} does not match index vector size {}",
1053 config.scatter_dims_to_operand_dims.len(),
1054 index_size
1055 ),
1056 });
1057 }
1058 for &operand_dim in &config.scatter_dims_to_operand_dims {
1059 if operand_dim >= op_rank {
1060 return Err(crate::Error::AxisOutOfBounds {
1061 op: "scatter",
1062 axis: operand_dim,
1063 rank: op_rank,
1064 });
1065 }
1066 }
1067 {
1068 let mut seen = vec![false; op_rank];
1069 for &operand_dim in &config.scatter_dims_to_operand_dims {
1070 if seen[operand_dim] {
1071 return Err(crate::Error::DuplicateAxis {
1072 op: "scatter",
1073 axis: operand_dim,
1074 role: "scatter_dims_to_operand_dims",
1075 });
1076 }
1077 seen[operand_dim] = true;
1078 }
1079 }
1080
1081 let batch_shape =
1082 try_index_batch_shape("scatter", &scatter_indices.shape, config.index_vector_dim)?;
1083 let window_dims = operand_window_dims(op_rank, &config.inserted_window_dims);
1084 if config.update_window_dims.len() != window_dims.len() {
1085 return Err(crate::Error::InvalidConfig {
1086 op: "scatter",
1087 message: format!(
1088 "update_window_dims length {} does not match window dims count {}",
1089 config.update_window_dims.len(),
1090 window_dims.len()
1091 ),
1092 });
1093 }
1094
1095 let update_rank = updates_shape.len();
1096 let expected_batch_rank = update_rank
1097 .checked_sub(config.update_window_dims.len())
1098 .ok_or_else(|| crate::Error::InvalidConfig {
1099 op: "scatter",
1100 message: format!(
1101 "update_window_dims length {} exceeds update rank {}",
1102 config.update_window_dims.len(),
1103 update_rank
1104 ),
1105 })?;
1106 if expected_batch_rank != batch_shape.len() {
1107 return Err(crate::Error::InvalidConfig {
1108 op: "scatter",
1109 message: format!(
1110 "updates batch rank {} does not match index batch shape length {}",
1111 expected_batch_rank,
1112 batch_shape.len()
1113 ),
1114 });
1115 }
1116
1117 for &axis in &config.update_window_dims {
1118 if axis >= update_rank {
1119 return Err(crate::Error::AxisOutOfBounds {
1120 op: "scatter",
1121 axis,
1122 rank: update_rank,
1123 });
1124 }
1125 }
1126 {
1127 let mut seen = vec![false; update_rank];
1128 for &axis in &config.update_window_dims {
1129 if seen[axis] {
1130 return Err(crate::Error::DuplicateAxis {
1131 op: "scatter",
1132 axis,
1133 role: "update_window_dims",
1134 });
1135 }
1136 seen[axis] = true;
1137 }
1138 }
1139
1140 let mut is_update_window_dim = vec![false; update_rank];
1141 for &axis in &config.update_window_dims {
1142 is_update_window_dim[axis] = true;
1143 }
1144
1145 {
1146 let mut batch_axis = 0usize;
1147 for axis in 0..update_rank {
1148 if !is_update_window_dim[axis] {
1149 if updates_shape[axis] != batch_shape[batch_axis] {
1150 return Err(crate::Error::InvalidConfig {
1151 op: "scatter",
1152 message: format!(
1153 "updates batch dim {} extent {} does not match index batch dim {} extent {}",
1154 axis,
1155 updates_shape[axis],
1156 batch_axis,
1157 batch_shape[batch_axis]
1158 ),
1159 });
1160 }
1161 batch_axis += 1;
1162 }
1163 }
1164 }
1165
1166 let mut window_shape = vec![1usize; op_rank];
1167 let mut window_shape_updates = vec![0usize; config.update_window_dims.len()];
1168 for (pos, &update_axis) in config.update_window_dims.iter().enumerate() {
1169 let dim = updates_shape[update_axis];
1170 window_shape_updates[pos] = dim;
1171 window_shape[window_dims[pos]] = dim;
1172 }
1173 for axis in 0..op_rank {
1174 let _ = clamp_window_start("scatter", 0, operand_shape[axis], window_shape[axis])?;
1175 }
1176
1177 let batch_elems = checked_product("scatter", "batch shape", &batch_shape)?;
1178 let window_elems = checked_product("scatter", "window update shape", &window_shape_updates)?;
1179 let mut out = clone_host_tensor_from_pool(buffers, "scatter", operand)?;
1180
1181 let mut batch_idx = vec![0usize; batch_shape.len()];
1182 let mut window_idx = vec![0usize; window_shape_updates.len()];
1183 let mut update_idx = vec![0usize; update_rank];
1184 let mut operand_base = vec![0usize; op_rank];
1185 let mut operand_idx = vec![0usize; op_rank];
1186 let mut index_scratch = vec![0usize; scatter_indices.shape.len()];
1187
1188 for _ in 0..batch_elems {
1189 operand_base.fill(0);
1190 for (component, &operand_dim) in config.scatter_dims_to_operand_dims.iter().enumerate() {
1191 let start = index_component(
1192 "scatter",
1193 scatter_indices,
1194 &batch_idx,
1195 config.index_vector_dim,
1196 component,
1197 &mut index_scratch,
1198 )?;
1199 operand_base[operand_dim] = clamp_window_start(
1200 "scatter",
1201 start,
1202 operand_shape[operand_dim],
1203 window_shape[operand_dim],
1204 )?;
1205 }
1206
1207 window_idx.fill(0);
1208 for _ in 0..window_elems {
1209 let mut batch_axis = 0usize;
1210 let mut window_axis = 0usize;
1211 for axis in 0..update_rank {
1212 if is_update_window_dim[axis] {
1213 update_idx[axis] = window_idx[window_axis];
1214 window_axis += 1;
1215 } else {
1216 update_idx[axis] = batch_idx[batch_axis];
1217 batch_axis += 1;
1218 }
1219 }
1220
1221 operand_idx.copy_from_slice(&operand_base);
1222 for (window_axis, &operand_axis) in window_dims.iter().enumerate() {
1223 operand_idx[operand_axis] += window_idx[window_axis];
1224 }
1225
1226 let value = *updates.get(&update_idx)?;
1227 let slot = out.get_mut(&operand_idx)?;
1228 *slot = *slot + value;
1229 advance_col_major_index(&mut window_idx, &window_shape_updates);
1230 }
1231 advance_col_major_index(&mut batch_idx, &batch_shape);
1232 }
1233
1234 Ok(out)
1235}
1236
1237fn typed_dynamic_slice<T: Copy + Clone + PoolScalar>(
1238 buffers: &mut BufferPool,
1239 input: &TypedTensor<T>,
1240 starts: &IndexTensor,
1241 slice_sizes: &[usize],
1242) -> crate::Result<TypedTensor<T>> {
1243 let input_shape = input.shape();
1244 if slice_sizes.len() != input_shape.len() {
1245 return Err(crate::Error::RankMismatch {
1246 op: "dynamic_slice",
1247 expected: input_shape.len(),
1248 actual: slice_sizes.len(),
1249 });
1250 }
1251 if starts.shape.len() != 1 {
1252 return Err(crate::Error::InvalidConfig {
1253 op: "dynamic_slice",
1254 message: "starts must be a rank-1 tensor".into(),
1255 });
1256 }
1257 if starts.values.len() != input_shape.len() {
1258 return Err(crate::Error::InvalidConfig {
1259 op: "dynamic_slice",
1260 message: format!(
1261 "starts length {} must match input rank {}",
1262 starts.values.len(),
1263 input_shape.len()
1264 ),
1265 });
1266 }
1267
1268 let mut clamped_starts = vec![0usize; input_shape.len()];
1269 for axis in 0..input_shape.len() {
1270 clamped_starts[axis] = clamp_window_start(
1271 "dynamic_slice",
1272 starts.values[axis],
1273 input_shape[axis],
1274 slice_sizes[axis],
1275 )?;
1276 }
1277
1278 let out_shape = slice_sizes.to_vec();
1279 let mut out = pooled_uninit_tensor(buffers, out_shape.clone())?;
1281 let mut out_idx = vec![0usize; out_shape.len()];
1282 let mut input_idx = vec![0usize; out_shape.len()];
1283
1284 for out_value in out.host_data_mut()?.iter_mut() {
1285 for axis in 0..out_shape.len() {
1286 input_idx[axis] = clamped_starts[axis] + out_idx[axis];
1287 }
1288 *out_value = *input.get(&input_idx)?;
1289 advance_col_major_index(&mut out_idx, &out_shape);
1290 }
1291
1292 Ok(out)
1293}
1294
1295fn typed_dynamic_update_slice<T: Copy + Clone + PoolScalar>(
1296 buffers: &mut BufferPool,
1297 operand: &TypedTensor<T>,
1298 update: &TypedTensor<T>,
1299 starts: &IndexTensor,
1300) -> crate::Result<TypedTensor<T>> {
1301 let operand_shape = operand.shape();
1302 let update_shape = update.shape();
1303 if update_shape.len() != operand_shape.len() {
1304 return Err(crate::Error::RankMismatch {
1305 op: "dynamic_update_slice",
1306 expected: operand_shape.len(),
1307 actual: update_shape.len(),
1308 });
1309 }
1310 if starts.shape.len() != 1 {
1311 return Err(crate::Error::InvalidConfig {
1312 op: "dynamic_update_slice",
1313 message: "starts must be a rank-1 tensor".into(),
1314 });
1315 }
1316 if starts.values.len() != operand_shape.len() {
1317 return Err(crate::Error::InvalidConfig {
1318 op: "dynamic_update_slice",
1319 message: format!(
1320 "starts length {} must match operand rank {}",
1321 starts.values.len(),
1322 operand_shape.len()
1323 ),
1324 });
1325 }
1326
1327 let mut clamped_starts = vec![0usize; operand_shape.len()];
1328 for axis in 0..operand_shape.len() {
1329 clamped_starts[axis] = clamp_window_start(
1330 "dynamic_update_slice",
1331 starts.values[axis],
1332 operand_shape[axis],
1333 update_shape[axis],
1334 )?;
1335 }
1336
1337 let mut out = clone_host_tensor_from_pool(buffers, "dynamic_update_slice", operand)?;
1338 let mut update_idx = vec![0usize; update_shape.len()];
1339 let mut operand_idx = vec![0usize; operand_shape.len()];
1340
1341 for update_value in update.as_slice()? {
1342 for axis in 0..update_shape.len() {
1343 operand_idx[axis] = clamped_starts[axis] + update_idx[axis];
1344 }
1345 *out.get_mut(&operand_idx)? = *update_value;
1346 advance_col_major_index(&mut update_idx, update_shape);
1347 }
1348
1349 Ok(out)
1350}
1351
1352fn typed_pad<T: Copy + Clone + Zero + PoolScalar>(
1353 buffers: &mut BufferPool,
1354 input: &TypedTensor<T>,
1355 config: &PadConfig,
1356) -> crate::Result<TypedTensor<T>> {
1357 typed_pad_with_fill(buffers, input, config, T::zero())
1358}
1359
1360fn typed_pad_with_fill<T: Copy + Clone + PoolScalar>(
1361 buffers: &mut BufferPool,
1362 input: &TypedTensor<T>,
1363 config: &PadConfig,
1364 fill: T,
1365) -> crate::Result<TypedTensor<T>> {
1366 let input_shape = input.shape();
1367 let rank = input_shape.len();
1368 if config.edge_padding_low.len() != rank {
1369 return Err(crate::Error::RankMismatch {
1370 op: "pad",
1371 expected: rank,
1372 actual: config.edge_padding_low.len(),
1373 });
1374 }
1375 if config.edge_padding_high.len() != rank {
1376 return Err(crate::Error::RankMismatch {
1377 op: "pad",
1378 expected: rank,
1379 actual: config.edge_padding_high.len(),
1380 });
1381 }
1382 if config.interior_padding.len() != rank {
1383 return Err(crate::Error::RankMismatch {
1384 op: "pad",
1385 expected: rank,
1386 actual: config.interior_padding.len(),
1387 });
1388 }
1389
1390 let mut out_shape = Vec::with_capacity(input_shape.len());
1391 for (axis, &input_extent) in input_shape.iter().enumerate() {
1392 if config.interior_padding[axis] < 0 {
1393 return Err(crate::Error::InvalidConfig {
1394 op: "pad",
1395 message: format!("interior padding must be non-negative on axis {axis}"),
1396 });
1397 }
1398 let input_extent_i64 =
1399 i64::try_from(input_extent).map_err(|_| crate::Error::InvalidConfig {
1400 op: "pad",
1401 message: format!("input extent on axis {axis} does not fit in i64"),
1402 })?;
1403 let spacing = config.interior_padding[axis]
1404 .checked_add(1)
1405 .ok_or_else(|| crate::Error::InvalidConfig {
1406 op: "pad",
1407 message: format!("interior padding overflow on axis {axis}"),
1408 })?;
1409 let base = if input_extent == 0 {
1410 0
1411 } else {
1412 input_extent_i64
1413 .checked_sub(1)
1414 .and_then(|extent| extent.checked_mul(spacing))
1415 .and_then(|extent| extent.checked_add(1))
1416 .ok_or_else(|| crate::Error::InvalidConfig {
1417 op: "pad",
1418 message: format!("padded input extent overflow on axis {axis}"),
1419 })?
1420 };
1421 let dim = config.edge_padding_low[axis]
1422 .checked_add(config.edge_padding_high[axis])
1423 .and_then(|edge| edge.checked_add(base))
1424 .ok_or_else(|| crate::Error::InvalidConfig {
1425 op: "pad",
1426 message: format!("output dimension overflow on axis {axis}"),
1427 })?;
1428 out_shape.push(
1429 usize::try_from(dim).map_err(|_| crate::Error::InvalidConfig {
1430 op: "pad",
1431 message: format!("negative output dimension on axis {axis}"),
1432 })?,
1433 );
1434 }
1435
1436 let mut out = pooled_filled_tensor(buffers, out_shape.clone(), fill)?;
1437 let mut input_idx = vec![0usize; input_shape.len()];
1438 let mut out_idx = vec![0usize; input_shape.len()];
1439
1440 for input_value in input.as_slice()? {
1441 let mut in_bounds = true;
1442 for axis in 0..input_shape.len() {
1443 let out_pos = i128::from(config.edge_padding_low[axis])
1444 + input_idx[axis] as i128 * i128::from(config.interior_padding[axis] + 1);
1445 if !(0..out_shape[axis] as i128).contains(&out_pos) {
1446 in_bounds = false;
1447 break;
1448 }
1449 out_idx[axis] = out_pos as usize;
1450 }
1451 if in_bounds {
1452 *out.get_mut(&out_idx)? = *input_value;
1453 }
1454 advance_col_major_index(&mut input_idx, input_shape);
1455 }
1456
1457 Ok(out)
1458}
1459
1460#[cfg(test)]
1461mod tests;