Skip to main content

onnx_runtime_session/
sequence.rs

1//! ONNX sequence values and zero-copy split views.
2//!
3//! Sequence elements reuse the session crate's [`Tensor`] representation. A
4//! [`SeqTensor`] is an immutable view over an `Arc`-owned device allocation, so
5//! constructing, inserting, erasing, indexing, and splitting a sequence only
6//! clone handles and metadata. Tensor storage is never deep-copied by those
7//! operations.
8
9use std::sync::Arc;
10
11use onnx_runtime_ir::{DataType, TensorLayout, compute_contiguous_strides};
12
13use crate::tensor::{SharedTensorBuffer, host_bytes};
14use crate::{SessionError, Tensor};
15
16/// Result type for sequence value and byte-helper operations.
17pub type SequenceResult<T> = std::result::Result<T, SequenceError>;
18
19/// A typed failure from an ONNX sequence operation.
20#[derive(Debug, thiserror::Error)]
21pub enum SequenceError {
22    #[error(
23        "SequenceConstruct requires at least one tensor; use SequenceValue::empty(dtype) for an empty sequence"
24    )]
25    EmptyConstruct,
26
27    #[error(
28        "{op} element{index_suffix} dtype {actual:?} does not match expected {expected:?}; ONNX sequences are homogeneous. To fix: Cast the tensor to {expected:?}",
29        index_suffix = index.map(|i| format!(" {i}")).unwrap_or_default()
30    )]
31    DtypeMismatch {
32        op: &'static str,
33        index: Option<usize>,
34        expected: DataType,
35        actual: DataType,
36    },
37
38    #[error(
39        "{op} index {index} is out of bounds for a sequence of length {len} (valid range {range})",
40        range = if *insertion {
41            format!("[{}, {}]", -(*len as i128), *len)
42        } else if *len == 0 {
43            "empty (no valid indices)".to_string()
44        } else {
45            format!("[{}, {}]", -(*len as i128), *len as i128 - 1)
46        }
47    )]
48    IndexOutOfBounds {
49        op: &'static str,
50        index: i64,
51        len: usize,
52        insertion: bool,
53    },
54
55    #[error("SequenceErase cannot erase from an empty sequence")]
56    EmptyErase,
57
58    #[error("{op} cannot represent sequence length {len} as an ONNX index")]
59    LengthOverflow { op: &'static str, len: usize },
60
61    #[error(
62        "{op} axis {axis} is invalid for rank {rank}{new_axis_suffix}",
63        new_axis_suffix = if *new_axis { " with new_axis=1" } else { "" }
64    )]
65    InvalidAxis {
66        op: &'static str,
67        axis: i64,
68        rank: usize,
69        new_axis: bool,
70    },
71
72    #[error("{op} has invalid split specification: {reason}")]
73    InvalidSplit { op: &'static str, reason: String },
74
75    #[error(
76        "{op} element {index} has shape {actual:?}, incompatible with {expected:?}: {requirement}"
77    )]
78    ShapeMismatch {
79        op: &'static str,
80        index: usize,
81        expected: Vec<usize>,
82        actual: Vec<usize>,
83        requirement: &'static str,
84    },
85
86    #[error("{op} does not support byte operations for sub-byte dtype {dtype:?}")]
87    UnsupportedDtype { op: &'static str, dtype: DataType },
88
89    #[error("{op} requires host-accessible sequence tensors, but element {index} is on {device}")]
90    NonHostTensor {
91        op: &'static str,
92        index: usize,
93        device: String,
94    },
95
96    #[error(
97        "SequenceTensor cannot borrow contiguous bytes for shape {shape:?} on {device}: {reason}; use contiguous_bytes() to materialize the view"
98    )]
99    ByteBorrowUnavailable {
100        shape: Vec<usize>,
101        device: String,
102        reason: &'static str,
103    },
104
105    #[error(
106        "{op} received {actual} bytes for shape {shape:?} dtype {dtype:?}, expected {expected}"
107    )]
108    ByteLengthMismatch {
109        op: &'static str,
110        dtype: DataType,
111        shape: Vec<usize>,
112        expected: usize,
113        actual: usize,
114    },
115
116    #[error("{op} shape/offset overflow while computing {context} for shape {shape:?}")]
117    ShapeOverflow {
118        op: &'static str,
119        context: &'static str,
120        shape: Vec<usize>,
121    },
122
123    #[error("{op} cannot allocate {bytes} bytes for {context}")]
124    Allocation {
125        op: &'static str,
126        context: &'static str,
127        bytes: usize,
128    },
129
130    #[error("{op} could not create a tensor: {source}")]
131    TensorCreation {
132        op: &'static str,
133        #[source]
134        source: SessionError,
135    },
136}
137
138impl SequenceError {
139    pub(crate) fn op(&self) -> &'static str {
140        match self {
141            Self::EmptyConstruct => "SequenceConstruct",
142            Self::DtypeMismatch { op, .. }
143            | Self::IndexOutOfBounds { op, .. }
144            | Self::LengthOverflow { op, .. }
145            | Self::InvalidAxis { op, .. }
146            | Self::InvalidSplit { op, .. }
147            | Self::ShapeMismatch { op, .. }
148            | Self::UnsupportedDtype { op, .. }
149            | Self::NonHostTensor { op, .. }
150            | Self::ByteLengthMismatch { op, .. }
151            | Self::ShapeOverflow { op, .. }
152            | Self::Allocation { op, .. }
153            | Self::TensorCreation { op, .. } => op,
154            Self::EmptyErase => "SequenceErase",
155            Self::ByteBorrowUnavailable { .. } => "SequenceTensor",
156        }
157    }
158}
159
160impl From<SequenceError> for SessionError {
161    fn from(error: SequenceError) -> Self {
162        if let SequenceError::ShapeOverflow { context, shape, .. } = error {
163            return SessionError::ShapeOverflow {
164                value: context.to_string(),
165                dims: shape,
166            };
167        }
168        let op = error.op().to_string();
169        SessionError::SequenceOp {
170            op,
171            reason: error.to_string(),
172        }
173    }
174}
175
176/// An immutable tensor view used as one sequence element.
177///
178/// Cloning this type only bumps the backing allocation's `Arc` count. Shape,
179/// strides, and byte offset are metadata, so a split slice can share the source
180/// allocation even when it is not contiguous.
181#[derive(Clone, Debug)]
182pub struct SeqTensor {
183    storage: Arc<SharedTensorBuffer>,
184    pub dtype: DataType,
185    pub shape: Vec<usize>,
186    pub layout: TensorLayout,
187    byte_offset: usize,
188}
189
190impl SeqTensor {
191    /// Wrap an existing session tensor in an immutable shared handle.
192    pub fn new(tensor: Tensor) -> Self {
193        let (storage, dtype, shape, layout) = tensor.into_shared_parts();
194        Self {
195            storage,
196            dtype,
197            shape,
198            layout,
199            byte_offset: 0,
200        }
201    }
202
203    /// Build a host tensor from raw element bytes and share it as a sequence item.
204    pub fn from_raw(dtype: DataType, shape: Vec<usize>, bytes: &[u8]) -> SequenceResult<Self> {
205        validate_tensor_bytes("SequenceTensor", bytes, dtype, &shape)?;
206        let mut storage = SharedTensorBuffer::allocate_cpu(bytes.len()).map_err(|source| {
207            SequenceError::TensorCreation {
208                op: "SequenceTensor",
209                source,
210            }
211        })?;
212        let allocator = Arc::clone(storage.allocator());
213        allocator
214            .copy_from_host(
215                bytes,
216                Arc::get_mut(&mut storage)
217                    .expect("fresh sequence storage is uniquely owned")
218                    .buffer_mut(),
219            )
220            .map_err(|source| SequenceError::TensorCreation {
221                op: "SequenceTensor",
222                source: source.into(),
223            })?;
224        Ok(Self {
225            storage,
226            dtype,
227            shape,
228            layout: TensorLayout::contiguous(),
229            byte_offset: 0,
230        })
231    }
232
233    pub(crate) fn from_shared(
234        storage: Arc<SharedTensorBuffer>,
235        dtype: DataType,
236        shape: Vec<usize>,
237        layout: TensorLayout,
238        byte_offset: usize,
239    ) -> SequenceResult<Self> {
240        let strides = layout.resolved_strides(&shape);
241        validate_view_bounds(
242            "SequenceTensor",
243            &shape,
244            &strides,
245            byte_offset,
246            dtype,
247            storage.buffer().len(),
248        )?;
249        Ok(Self {
250            storage,
251            dtype,
252            shape,
253            layout,
254            byte_offset,
255        })
256    }
257
258    /// Whether two handles share the same underlying device allocation.
259    pub fn shares_storage_with(&self, other: &Self) -> bool {
260        Arc::ptr_eq(&self.storage, &other.storage)
261    }
262
263    /// Number of live handles to the shared allocation.
264    pub fn storage_strong_count(&self) -> usize {
265        Arc::strong_count(&self.storage)
266    }
267
268    pub(crate) fn storage(&self) -> &Arc<SharedTensorBuffer> {
269        &self.storage
270    }
271
272    /// Base address of the shared tensor allocation.
273    pub fn as_ptr(&self) -> *const std::ffi::c_void {
274        self.storage.buffer().as_ptr()
275    }
276
277    /// Byte offset of this view's logical origin from [`Self::as_ptr`].
278    pub fn byte_offset(&self) -> usize {
279        self.byte_offset
280    }
281
282    pub fn device(&self) -> onnx_runtime_ir::DeviceId {
283        self.storage.buffer().device()
284    }
285
286    pub fn numel(&self) -> usize {
287        self.shape.iter().product()
288    }
289
290    pub(crate) fn root_len(&self) -> usize {
291        self.storage.buffer().len()
292    }
293
294    /// Materialize this logical tensor as contiguous host bytes.
295    pub fn contiguous_bytes(&self) -> SequenceResult<Vec<u8>> {
296        const OP: &str = "SequenceTensor";
297        let esize = self.dtype.byte_size();
298        if esize == 0 {
299            return Err(SequenceError::UnsupportedDtype {
300                op: OP,
301                dtype: self.dtype,
302            });
303        }
304        let mut copied;
305        let root = if self.device().is_host_accessible() {
306            host_bytes(self.storage.buffer())
307        } else {
308            copied = zeroed_bytes(OP, "device tensor download", self.root_len(), &self.shape)?;
309            self.storage
310                .allocator()
311                .copy_to_host(self.storage.buffer(), &mut copied)
312                .map_err(|source| SequenceError::TensorCreation {
313                    op: OP,
314                    source: source.into(),
315                })?;
316            &copied
317        };
318        let strides = self.layout.resolved_strides(&self.shape);
319        if onnx_runtime_ir::is_contiguous(&self.shape, &strides) {
320            let bytes = self
321                .dtype
322                .checked_storage_bytes(self.numel())
323                .ok_or_else(|| overflow(OP, "tensor byte count", &self.shape))?;
324            let end = checked_add(
325                OP,
326                "tensor byte range",
327                self.byte_offset,
328                bytes,
329                &self.shape,
330            )?;
331            return Ok(root[self.byte_offset..end].to_vec());
332        }
333        gather_strided(
334            root,
335            &self.shape,
336            &strides,
337            self.byte_offset,
338            self.dtype,
339            esize,
340        )
341    }
342
343    fn write_contiguous_range<F>(
344        &self,
345        logical_offset: usize,
346        bytes: usize,
347        destination_offset: usize,
348        scratch: &mut [u8],
349        write: &mut F,
350        stats: &mut ConcatCopyStats,
351    ) -> crate::Result<()>
352    where
353        F: FnMut(usize, &[u8]) -> crate::Result<()>,
354    {
355        const OP: &str = "ConcatFromSequence";
356        let esize = self.dtype.byte_size();
357        if esize == 0 {
358            return Err(SequenceError::UnsupportedDtype {
359                op: OP,
360                dtype: self.dtype,
361            }
362            .into());
363        }
364        let logical_bytes = self
365            .dtype
366            .checked_storage_bytes(checked_product(OP, "source element count", &self.shape)?)
367            .ok_or_else(|| overflow(OP, "source byte count", &self.shape))?;
368        let logical_end = checked_add(
369            OP,
370            "source logical byte range",
371            logical_offset,
372            bytes,
373            &self.shape,
374        )?;
375        if logical_end > logical_bytes
376            || !logical_offset.is_multiple_of(esize)
377            || !bytes.is_multiple_of(esize)
378        {
379            return Err(SequenceError::ByteLengthMismatch {
380                op: OP,
381                dtype: self.dtype,
382                shape: self.shape.clone(),
383                expected: logical_bytes,
384                actual: logical_end,
385            }
386            .into());
387        }
388        if bytes == 0 {
389            return Ok(());
390        }
391
392        let root = if self.device().is_host_accessible() {
393            host_bytes(self.storage.buffer())
394        } else {
395            let destination =
396                scratch
397                    .get_mut(..self.root_len())
398                    .ok_or_else(|| SequenceError::Allocation {
399                        op: OP,
400                        context: "device source materialization",
401                        bytes: self.root_len(),
402                    })?;
403            self.storage
404                .allocator()
405                .copy_to_host(self.storage.buffer(), destination)
406                .map_err(|source| SequenceError::TensorCreation {
407                    op: OP,
408                    source: source.into(),
409                })?;
410            stats.source_materializations += 1;
411            destination
412        };
413        let strides = self.layout.resolved_strides(&self.shape);
414        validate_view_bounds(
415            OP,
416            &self.shape,
417            &strides,
418            self.byte_offset,
419            self.dtype,
420            root.len(),
421        )?;
422        if onnx_runtime_ir::is_contiguous(&self.shape, &strides) {
423            let source_offset = checked_add(
424                OP,
425                "contiguous source byte offset",
426                self.byte_offset,
427                logical_offset,
428                &self.shape,
429            )?;
430            let source_end = checked_add(
431                OP,
432                "contiguous source byte range",
433                source_offset,
434                bytes,
435                &self.shape,
436            )?;
437            let source = root
438                .get(source_offset..source_end)
439                .ok_or_else(|| overflow(OP, "contiguous source byte range", &self.shape))?;
440            write(destination_offset, source)?;
441            stats.destination_writes += 1;
442            return Ok(());
443        }
444
445        let logical_strides = compute_contiguous_strides(&self.shape);
446        let start_element = logical_offset / esize;
447        let elements = bytes / esize;
448        for element_offset in 0..elements {
449            let linear = checked_add(
450                OP,
451                "strided source logical index",
452                start_element,
453                element_offset,
454                &self.shape,
455            )?;
456            let mut remainder = linear;
457            let mut source_element = 0i128;
458            for dimension in 0..self.shape.len() {
459                let coordinate = if self.shape[dimension] == 0 {
460                    0
461                } else {
462                    remainder / logical_strides[dimension] as usize
463                };
464                if self.shape[dimension] != 0 {
465                    remainder %= logical_strides[dimension] as usize;
466                }
467                source_element += coordinate as i128 * strides[dimension] as i128;
468            }
469            let source_offset = (self.byte_offset as i128)
470                .checked_add(
471                    source_element
472                        .checked_mul(esize as i128)
473                        .ok_or_else(|| overflow(OP, "strided source byte offset", &self.shape))?,
474                )
475                .and_then(|offset| usize::try_from(offset).ok())
476                .ok_or_else(|| overflow(OP, "strided source byte offset", &self.shape))?;
477            let source_end = checked_add(
478                OP,
479                "strided source byte range",
480                source_offset,
481                esize,
482                &self.shape,
483            )?;
484            let destination = checked_add(
485                OP,
486                "strided destination byte offset",
487                destination_offset,
488                checked_mul(
489                    OP,
490                    "strided destination element offset",
491                    element_offset,
492                    esize,
493                    &self.shape,
494                )?,
495                &self.shape,
496            )?;
497            let source = root
498                .get(source_offset..source_end)
499                .ok_or_else(|| overflow(OP, "strided source byte range", &self.shape))?;
500            write(destination, source)?;
501            stats.destination_writes += 1;
502        }
503        Ok(())
504    }
505
506    /// Borrow bytes directly when this is a contiguous host view.
507    pub fn as_bytes(&self) -> SequenceResult<&[u8]> {
508        const OP: &str = "SequenceTensor";
509        if !self.device().is_host_accessible() {
510            return Err(SequenceError::ByteBorrowUnavailable {
511                shape: self.shape.clone(),
512                device: format!("{:?}", self.device()),
513                reason: "the storage is not host-accessible",
514            });
515        }
516        let strides = self.layout.resolved_strides(&self.shape);
517        if !onnx_runtime_ir::is_contiguous(&self.shape, &strides) {
518            return Err(SequenceError::ByteBorrowUnavailable {
519                shape: self.shape.clone(),
520                device: format!("{:?}", self.device()),
521                reason: "the view is strided",
522            });
523        }
524        validate_view_bounds(
525            OP,
526            &self.shape,
527            &strides,
528            self.byte_offset,
529            self.dtype,
530            self.root_len(),
531        )?;
532        let bytes = self
533            .dtype
534            .checked_storage_bytes(checked_product(OP, "tensor element count", &self.shape)?)
535            .ok_or_else(|| overflow(OP, "tensor byte count", &self.shape))?;
536        let end = checked_add(
537            OP,
538            "tensor byte range",
539            self.byte_offset,
540            bytes,
541            &self.shape,
542        )?;
543        host_bytes(self.storage.buffer())
544            .get(self.byte_offset..end)
545            .ok_or_else(|| overflow(OP, "tensor byte range", &self.shape))
546    }
547}
548
549/// An ordered homogeneous list of immutable, shared tensors.
550#[derive(Clone, Debug)]
551pub struct SequenceValue {
552    pub(crate) elem_dtype: DataType,
553    pub(crate) items: Vec<SeqTensor>,
554}
555
556impl SequenceValue {
557    /// Construct an empty sequence with its declared tensor element dtype.
558    pub fn empty(elem_dtype: DataType) -> Self {
559        Self {
560            elem_dtype,
561            items: Vec::new(),
562        }
563    }
564
565    /// Construct a sequence without copying any element tensor storage.
566    pub fn construct(items: Vec<SeqTensor>) -> SequenceResult<Self> {
567        let elem_dtype = items
568            .first()
569            .map(|tensor| tensor.dtype)
570            .ok_or(SequenceError::EmptyConstruct)?;
571        for (index, tensor) in items.iter().enumerate() {
572            if tensor.dtype != elem_dtype {
573                return Err(SequenceError::DtypeMismatch {
574                    op: "SequenceConstruct",
575                    index: Some(index),
576                    expected: elem_dtype,
577                    actual: tensor.dtype,
578                });
579            }
580        }
581        Ok(Self { elem_dtype, items })
582    }
583
584    /// Return a new sequence with `value` inserted at `at`.
585    ///
586    /// `None` appends. Negative positions count from the end, and `len` is also
587    /// accepted as an explicit append position.
588    pub fn insert(&self, value: SeqTensor, at: Option<i64>) -> SequenceResult<Self> {
589        if value.dtype != self.elem_dtype {
590            return Err(SequenceError::DtypeMismatch {
591                op: "SequenceInsert",
592                index: None,
593                expected: self.elem_dtype,
594                actual: value.dtype,
595            });
596        }
597        let index = match at {
598            None => self.items.len(),
599            Some(index) => resolve_index("SequenceInsert", index, self.items.len(), true)?,
600        };
601        let capacity = self
602            .items
603            .len()
604            .checked_add(1)
605            .ok_or(SequenceError::LengthOverflow {
606                op: "SequenceInsert",
607                len: self.items.len(),
608            })?;
609        let mut items = Vec::new();
610        items
611            .try_reserve_exact(capacity)
612            .map_err(|_| SequenceError::Allocation {
613                op: "SequenceInsert",
614                context: "sequence handles",
615                bytes: capacity.saturating_mul(std::mem::size_of::<SeqTensor>()),
616            })?;
617        items.extend_from_slice(&self.items[..index]);
618        items.push(value);
619        items.extend_from_slice(&self.items[index..]);
620        Ok(Self {
621            elem_dtype: self.elem_dtype,
622            items,
623        })
624    }
625
626    /// Return a new sequence with the selected element erased.
627    ///
628    /// `None` erases the last element. Negative indices count from the end.
629    pub fn erase(&self, at: Option<i64>) -> SequenceResult<Self> {
630        if self.items.is_empty() {
631            return Err(SequenceError::EmptyErase);
632        }
633        let index = match at {
634            None => self.items.len() - 1,
635            Some(index) => resolve_index("SequenceErase", index, self.items.len(), false)?,
636        };
637        let capacity = self.items.len() - 1;
638        let mut items = Vec::new();
639        items
640            .try_reserve_exact(capacity)
641            .map_err(|_| SequenceError::Allocation {
642                op: "SequenceErase",
643                context: "sequence handles",
644                bytes: capacity.saturating_mul(std::mem::size_of::<SeqTensor>()),
645            })?;
646        items.extend_from_slice(&self.items[..index]);
647        items.extend_from_slice(&self.items[index + 1..]);
648        Ok(Self {
649            elem_dtype: self.elem_dtype,
650            items,
651        })
652    }
653
654    /// Return the selected shared tensor handle. Negative indices count from the end.
655    pub fn at(&self, index: i64) -> SequenceResult<SeqTensor> {
656        let index = resolve_index("SequenceAt", index, self.items.len(), false)?;
657        Ok(self.items[index].clone())
658    }
659
660    /// Number of elements, matching ONNX `SequenceLength`.
661    pub fn length(&self) -> usize {
662        self.items.len()
663    }
664
665    /// Declared homogeneous element dtype.
666    pub fn elem_dtype(&self) -> DataType {
667        self.elem_dtype
668    }
669
670    /// Ordered shared tensor handles.
671    pub fn elements(&self) -> &[SeqTensor] {
672        &self.items
673    }
674}
675
676/// ONNX `SplitToSequence` split-input interpretation.
677#[derive(Clone, Copy, Debug)]
678pub enum SplitSpec<'a> {
679    /// No split input: emit one slice per index along the selected axis.
680    Each,
681    /// Scalar split input: repeatedly take chunks of this size.
682    Chunk(i64),
683    /// Rank-1 split input: explicit extents that must sum to the axis extent.
684    Sizes(&'a [i64]),
685}
686
687/// Split raw contiguous tensor bytes into a sequence of session tensors.
688///
689/// `keepdims` only affects [`SplitSpec::Each`], as required by ONNX; an explicit
690/// split input always retains the split axis.
691pub fn split(
692    data: &[u8],
693    dtype: DataType,
694    shape: &[usize],
695    axis: i64,
696    split: SplitSpec<'_>,
697    keepdims: bool,
698) -> SequenceResult<SequenceValue> {
699    let input = SeqTensor::from_raw(dtype, shape.to_vec(), data)?;
700    split_tensor(&input, axis, split, keepdims)
701}
702
703/// Split a shared tensor into metadata-only views over the same allocation.
704///
705/// No tensor bytes are copied. Every returned element owns an `Arc` clone of
706/// `input`'s storage and records its own shape, strides, and byte offset.
707pub fn split_tensor(
708    input: &SeqTensor,
709    axis: i64,
710    split: SplitSpec<'_>,
711    keepdims: bool,
712) -> SequenceResult<SequenceValue> {
713    const OP: &str = "SplitToSequence";
714    let rank = input.shape.len();
715    if rank == 0 {
716        return Err(SequenceError::InvalidAxis {
717            op: OP,
718            axis,
719            rank,
720            new_axis: false,
721        });
722    }
723    let axis = normalize_axis(OP, axis, rank, false)?;
724    let axis_dim = input.shape[axis];
725    let (sizes, squeeze) = split_sizes(OP, axis, axis_dim, split, keepdims, &input.shape)?;
726    let input_strides = input.layout.resolved_strides(&input.shape);
727    let esize = input.dtype.byte_size();
728    if esize == 0 {
729        return Err(SequenceError::UnsupportedDtype {
730            op: OP,
731            dtype: input.dtype,
732        });
733    }
734
735    let mut items = Vec::new();
736    items
737        .try_reserve_exact(sizes.len())
738        .map_err(|_| SequenceError::Allocation {
739            op: OP,
740            context: "sequence handles",
741            bytes: sizes.len().saturating_mul(std::mem::size_of::<SeqTensor>()),
742        })?;
743    let mut start = 0usize;
744    for size in sizes {
745        let delta_elements = (start as i128)
746            .checked_mul(input_strides[axis] as i128)
747            .ok_or_else(|| overflow(OP, "split view element offset", &input.shape))?;
748        let delta_bytes = delta_elements
749            .checked_mul(esize as i128)
750            .ok_or_else(|| overflow(OP, "split view byte offset", &input.shape))?;
751        let byte_offset = (input.byte_offset as i128)
752            .checked_add(delta_bytes)
753            .and_then(|offset| usize::try_from(offset).ok())
754            .ok_or_else(|| overflow(OP, "split view byte offset", &input.shape))?;
755
756        let mut shape = clone_shape(OP, &input.shape)?;
757        shape[axis] = size;
758        let mut strides = input_strides.clone();
759        if squeeze {
760            shape.remove(axis);
761            strides.remove(axis);
762        }
763        items.push(SeqTensor::from_shared(
764            Arc::clone(input.storage()),
765            input.dtype,
766            shape,
767            TensorLayout::strided(strides),
768            byte_offset,
769        )?);
770        start = checked_add(OP, "split axis cursor", start, size, &input.shape)?;
771    }
772    Ok(SequenceValue {
773        elem_dtype: input.dtype,
774        items,
775    })
776}
777
778fn split_sizes(
779    op: &'static str,
780    axis: usize,
781    axis_dim: usize,
782    split: SplitSpec<'_>,
783    keepdims: bool,
784    shape: &[usize],
785) -> SequenceResult<(Vec<usize>, bool)> {
786    match split {
787        SplitSpec::Each => {
788            let mut sizes = Vec::new();
789            sizes
790                .try_reserve_exact(axis_dim)
791                .map_err(|_| SequenceError::Allocation {
792                    op,
793                    context: "split sizes",
794                    bytes: axis_dim.saturating_mul(std::mem::size_of::<usize>()),
795                })?;
796            sizes.resize(axis_dim, 1);
797            Ok((sizes, !keepdims))
798        }
799        SplitSpec::Chunk(chunk) => {
800            if chunk <= 0 {
801                return Err(SequenceError::InvalidSplit {
802                    op,
803                    reason: format!("scalar chunk size {chunk} must be positive"),
804                });
805            }
806            let chunk = usize::try_from(chunk).map_err(|_| SequenceError::InvalidSplit {
807                op,
808                reason: format!("scalar chunk size {chunk} cannot be represented"),
809            })?;
810            let count = axis_dim
811                .checked_add(chunk - 1)
812                .ok_or_else(|| overflow(op, "split chunk count", shape))?
813                / chunk;
814            let mut sizes = Vec::new();
815            sizes
816                .try_reserve_exact(count)
817                .map_err(|_| SequenceError::Allocation {
818                    op,
819                    context: "split sizes",
820                    bytes: count.saturating_mul(std::mem::size_of::<usize>()),
821                })?;
822            let mut remaining = axis_dim;
823            while remaining != 0 {
824                let size = remaining.min(chunk);
825                sizes.push(size);
826                remaining -= size;
827            }
828            Ok((sizes, false))
829        }
830        SplitSpec::Sizes(values) => {
831            let mut sizes = Vec::new();
832            sizes
833                .try_reserve_exact(values.len())
834                .map_err(|_| SequenceError::Allocation {
835                    op,
836                    context: "split sizes",
837                    bytes: values.len().saturating_mul(std::mem::size_of::<usize>()),
838                })?;
839            let mut sum = 0usize;
840            for &value in values {
841                let value = usize::try_from(value).map_err(|_| SequenceError::InvalidSplit {
842                    op,
843                    reason: format!("size {value} must be non-negative"),
844                })?;
845                sum = sum
846                    .checked_add(value)
847                    .ok_or_else(|| overflow(op, "split size sum", shape))?;
848                sizes.push(value);
849            }
850            if sum != axis_dim {
851                return Err(SequenceError::InvalidSplit {
852                    op,
853                    reason: format!("sizes sum to {sum}, but axis {axis} has extent {axis_dim}"),
854                });
855            }
856            Ok((sizes, false))
857        }
858    }
859}
860
861#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
862pub(crate) struct ConcatCopyStats {
863    pub destination_writes: usize,
864    pub source_materializations: usize,
865}
866
867/// Fully validated geometry for `ConcatFromSequence`.
868pub(crate) struct ConcatPlan {
869    pub dtype: DataType,
870    pub shape: Vec<usize>,
871    pub bytes: usize,
872    axis: usize,
873    new_axis: bool,
874    outer: usize,
875    inner: usize,
876    total_axis: usize,
877}
878
879impl ConcatPlan {
880    pub(crate) fn new(sequence: &SequenceValue, axis: i64, new_axis: bool) -> SequenceResult<Self> {
881        const OP: &str = "ConcatFromSequence";
882        let first = sequence.items.first().ok_or(SequenceError::InvalidSplit {
883            op: OP,
884            reason: "cannot concatenate an empty sequence".to_string(),
885        })?;
886        let dtype = sequence.elem_dtype;
887        let esize = dtype.byte_size();
888        if esize == 0 {
889            return Err(SequenceError::UnsupportedDtype { op: OP, dtype });
890        }
891        let rank = first.shape.len();
892        let output_rank = rank
893            .checked_add(usize::from(new_axis))
894            .ok_or_else(|| overflow(OP, "concat output rank", &first.shape))?;
895        let axis = normalize_axis(OP, axis, output_rank, new_axis)?;
896
897        for (index, item) in sequence.items.iter().enumerate() {
898            if item.dtype != dtype {
899                return Err(SequenceError::DtypeMismatch {
900                    op: OP,
901                    index: Some(index),
902                    expected: dtype,
903                    actual: item.dtype,
904                });
905            }
906            let mismatch = if new_axis {
907                item.shape != first.shape
908            } else {
909                item.shape.len() != rank
910                    || item.shape.iter().enumerate().any(|(dimension, &extent)| {
911                        dimension != axis && extent != first.shape[dimension]
912                    })
913            };
914            if mismatch {
915                return Err(SequenceError::ShapeMismatch {
916                    op: OP,
917                    index,
918                    expected: clone_shape(OP, &first.shape)?,
919                    actual: clone_shape(OP, &item.shape)?,
920                    requirement: if new_axis {
921                        "new_axis=1 requires identical shapes"
922                    } else {
923                        "all dimensions except the concat axis must match"
924                    },
925                });
926            }
927            validate_view_bounds(
928                OP,
929                &item.shape,
930                &item.layout.resolved_strides(&item.shape),
931                item.byte_offset,
932                item.dtype,
933                item.root_len(),
934            )?;
935        }
936
937        if new_axis {
938            let outer = checked_product(OP, "stack outer element count", &first.shape[..axis])?;
939            let inner_elements =
940                checked_product(OP, "stack inner element count", &first.shape[axis..])?;
941            let inner = checked_mul(
942                OP,
943                "stack inner byte count",
944                inner_elements,
945                esize,
946                &first.shape,
947            )?;
948            let mut shape = Vec::new();
949            shape
950                .try_reserve_exact(output_rank)
951                .map_err(|_| SequenceError::Allocation {
952                    op: OP,
953                    context: "stack output shape",
954                    bytes: output_rank.saturating_mul(std::mem::size_of::<usize>()),
955                })?;
956            shape.extend_from_slice(&first.shape[..axis]);
957            shape.push(sequence.items.len());
958            shape.extend_from_slice(&first.shape[axis..]);
959            let bytes = checked_mul(
960                OP,
961                "stack output byte count",
962                checked_mul(
963                    OP,
964                    "stack output row count",
965                    outer,
966                    sequence.items.len(),
967                    &shape,
968                )?,
969                inner,
970                &shape,
971            )?;
972            Ok(Self {
973                dtype,
974                shape,
975                bytes,
976                axis,
977                new_axis,
978                outer,
979                inner,
980                total_axis: sequence.items.len(),
981            })
982        } else {
983            let outer = checked_product(OP, "concat outer element count", &first.shape[..axis])?;
984            let inner_elements =
985                checked_product(OP, "concat inner element count", &first.shape[axis + 1..])?;
986            let inner = checked_mul(
987                OP,
988                "concat inner byte count",
989                inner_elements,
990                esize,
991                &first.shape,
992            )?;
993            let mut total_axis = 0usize;
994            for item in &sequence.items {
995                total_axis = checked_add(
996                    OP,
997                    "concat axis extent",
998                    total_axis,
999                    item.shape[axis],
1000                    &first.shape,
1001                )?;
1002            }
1003            let mut shape = clone_shape(OP, &first.shape)?;
1004            shape[axis] = total_axis;
1005            let bytes = checked_mul(
1006                OP,
1007                "concat output byte count",
1008                checked_mul(OP, "concat output row count", outer, total_axis, &shape)?,
1009                inner,
1010                &shape,
1011            )?;
1012            Ok(Self {
1013                dtype,
1014                shape,
1015                bytes,
1016                axis,
1017                new_axis,
1018                outer,
1019                inner,
1020                total_axis,
1021            })
1022        }
1023    }
1024
1025    pub(crate) fn write<F>(
1026        &self,
1027        sequence: &SequenceValue,
1028        mut write: F,
1029    ) -> crate::Result<ConcatCopyStats>
1030    where
1031        F: FnMut(usize, &[u8]) -> crate::Result<()>,
1032    {
1033        const OP: &str = "ConcatFromSequence";
1034        let max_root = sequence
1035            .items
1036            .iter()
1037            .filter(|item| !item.device().is_host_accessible())
1038            .map(SeqTensor::root_len)
1039            .max()
1040            .unwrap_or(0);
1041        let mut scratch = if max_root == 0 {
1042            Vec::new()
1043        } else {
1044            zeroed_bytes(OP, "device source materialization", max_root, &self.shape)?
1045        };
1046        let mut stats = ConcatCopyStats::default();
1047        if self.new_axis {
1048            for outer_index in 0..self.outer {
1049                for (item_index, item) in sequence.items.iter().enumerate() {
1050                    let source_offset = checked_mul(
1051                        OP,
1052                        "stack source offset",
1053                        outer_index,
1054                        self.inner,
1055                        &item.shape,
1056                    )?;
1057                    let destination_row = checked_add(
1058                        OP,
1059                        "stack destination row",
1060                        checked_mul(
1061                            OP,
1062                            "stack destination outer offset",
1063                            outer_index,
1064                            self.total_axis,
1065                            &self.shape,
1066                        )?,
1067                        item_index,
1068                        &self.shape,
1069                    )?;
1070                    let destination_offset = checked_mul(
1071                        OP,
1072                        "stack destination byte offset",
1073                        destination_row,
1074                        self.inner,
1075                        &self.shape,
1076                    )?;
1077                    item.write_contiguous_range(
1078                        source_offset,
1079                        self.inner,
1080                        destination_offset,
1081                        &mut scratch,
1082                        &mut write,
1083                        &mut stats,
1084                    )?;
1085                }
1086            }
1087        } else {
1088            for outer_index in 0..self.outer {
1089                let mut axis_cursor = 0usize;
1090                for item in &sequence.items {
1091                    let copy_bytes = checked_mul(
1092                        OP,
1093                        "concat copy width",
1094                        item.shape[self.axis],
1095                        self.inner,
1096                        &item.shape,
1097                    )?;
1098                    let source_offset = checked_mul(
1099                        OP,
1100                        "concat source byte offset",
1101                        outer_index,
1102                        copy_bytes,
1103                        &item.shape,
1104                    )?;
1105                    let destination_row = checked_add(
1106                        OP,
1107                        "concat destination row",
1108                        checked_mul(
1109                            OP,
1110                            "concat destination outer offset",
1111                            outer_index,
1112                            self.total_axis,
1113                            &self.shape,
1114                        )?,
1115                        axis_cursor,
1116                        &self.shape,
1117                    )?;
1118                    let destination_offset = checked_mul(
1119                        OP,
1120                        "concat destination byte offset",
1121                        destination_row,
1122                        self.inner,
1123                        &self.shape,
1124                    )?;
1125                    item.write_contiguous_range(
1126                        source_offset,
1127                        copy_bytes,
1128                        destination_offset,
1129                        &mut scratch,
1130                        &mut write,
1131                        &mut stats,
1132                    )?;
1133                    axis_cursor = checked_add(
1134                        OP,
1135                        "concat axis cursor",
1136                        axis_cursor,
1137                        item.shape[self.axis],
1138                        &self.shape,
1139                    )?;
1140                }
1141            }
1142        }
1143        Ok(stats)
1144    }
1145}
1146
1147/// Concatenate a sequence along an existing axis or stack it on a new axis.
1148pub fn concat(sequence: &SequenceValue, axis: i64, new_axis: bool) -> SequenceResult<SeqTensor> {
1149    const OP: &str = "ConcatFromSequence";
1150    let plan = ConcatPlan::new(sequence, axis, new_axis)?;
1151    let mut tensor = Tensor::allocate_cpu(plan.dtype, plan.shape.clone())
1152        .map_err(|source| SequenceError::TensorCreation { op: OP, source })?;
1153    plan.write(sequence, |offset, bytes| {
1154        tensor.copy_from_host_at(offset, bytes)
1155    })
1156    .map_err(|source| SequenceError::TensorCreation { op: OP, source })?;
1157    Ok(SeqTensor::new(tensor))
1158}
1159
1160fn resolve_index(
1161    op: &'static str,
1162    index: i64,
1163    len: usize,
1164    insertion: bool,
1165) -> SequenceResult<usize> {
1166    let length = i64::try_from(len).map_err(|_| SequenceError::LengthOverflow { op, len })?;
1167    let resolved = if index < 0 {
1168        length.checked_add(index)
1169    } else {
1170        Some(index)
1171    };
1172    let valid = resolved.is_some_and(|value| {
1173        value >= 0
1174            && if insertion {
1175                value <= length
1176            } else {
1177                value < length
1178            }
1179    });
1180    if !valid {
1181        return Err(SequenceError::IndexOutOfBounds {
1182            op,
1183            index,
1184            len,
1185            insertion,
1186        });
1187    }
1188    usize::try_from(resolved.unwrap_or_default())
1189        .map_err(|_| SequenceError::LengthOverflow { op, len })
1190}
1191
1192fn normalize_axis(
1193    op: &'static str,
1194    axis: i64,
1195    rank: usize,
1196    new_axis: bool,
1197) -> SequenceResult<usize> {
1198    let rank_i64 =
1199        i64::try_from(rank).map_err(|_| SequenceError::LengthOverflow { op, len: rank })?;
1200    let normalized = if axis < 0 {
1201        rank_i64.checked_add(axis)
1202    } else {
1203        Some(axis)
1204    };
1205    match normalized {
1206        Some(axis) if axis >= 0 && axis < rank_i64 => Ok(axis as usize),
1207        _ => Err(SequenceError::InvalidAxis {
1208            op,
1209            axis,
1210            rank: rank - usize::from(new_axis),
1211            new_axis,
1212        }),
1213    }
1214}
1215
1216fn overflow(op: &'static str, context: &'static str, shape: &[usize]) -> SequenceError {
1217    SequenceError::ShapeOverflow {
1218        op,
1219        context,
1220        shape: shape.to_vec(),
1221    }
1222}
1223
1224/// Multiply dimensions while still detecting overflow hidden by a zero extent.
1225fn checked_product(
1226    op: &'static str,
1227    context: &'static str,
1228    shape: &[usize],
1229) -> SequenceResult<usize> {
1230    let mut product = 1usize;
1231    let mut has_zero = false;
1232    for &dimension in shape {
1233        if dimension == 0 {
1234            has_zero = true;
1235        } else {
1236            product = product
1237                .checked_mul(dimension)
1238                .ok_or_else(|| overflow(op, context, shape))?;
1239        }
1240    }
1241    Ok(if has_zero { 0 } else { product })
1242}
1243
1244fn checked_mul(
1245    op: &'static str,
1246    context: &'static str,
1247    lhs: usize,
1248    rhs: usize,
1249    shape: &[usize],
1250) -> SequenceResult<usize> {
1251    lhs.checked_mul(rhs)
1252        .ok_or_else(|| overflow(op, context, shape))
1253}
1254
1255fn checked_add(
1256    op: &'static str,
1257    context: &'static str,
1258    lhs: usize,
1259    rhs: usize,
1260    shape: &[usize],
1261) -> SequenceResult<usize> {
1262    lhs.checked_add(rhs)
1263        .ok_or_else(|| overflow(op, context, shape))
1264}
1265
1266fn addressable(
1267    op: &'static str,
1268    context: &'static str,
1269    bytes: usize,
1270    shape: &[usize],
1271) -> SequenceResult<usize> {
1272    if bytes > isize::MAX as usize {
1273        return Err(overflow(op, context, shape));
1274    }
1275    Ok(bytes)
1276}
1277
1278fn zeroed_bytes(
1279    op: &'static str,
1280    context: &'static str,
1281    bytes: usize,
1282    shape: &[usize],
1283) -> SequenceResult<Vec<u8>> {
1284    addressable(op, context, bytes, shape)?;
1285    let mut output = Vec::new();
1286    output
1287        .try_reserve_exact(bytes)
1288        .map_err(|_| SequenceError::Allocation { op, context, bytes })?;
1289    output.resize(bytes, 0);
1290    Ok(output)
1291}
1292
1293fn clone_shape(op: &'static str, shape: &[usize]) -> SequenceResult<Vec<usize>> {
1294    let bytes = shape
1295        .len()
1296        .checked_mul(std::mem::size_of::<usize>())
1297        .ok_or_else(|| overflow(op, "shape allocation", shape))?;
1298    let mut cloned = Vec::new();
1299    cloned
1300        .try_reserve_exact(shape.len())
1301        .map_err(|_| SequenceError::Allocation {
1302            op,
1303            context: "shape",
1304            bytes,
1305        })?;
1306    cloned.extend_from_slice(shape);
1307    Ok(cloned)
1308}
1309
1310fn validate_tensor_bytes(
1311    op: &'static str,
1312    data: &[u8],
1313    dtype: DataType,
1314    shape: &[usize],
1315) -> SequenceResult<()> {
1316    if dtype.byte_size() == 0 {
1317        return Err(SequenceError::UnsupportedDtype { op, dtype });
1318    }
1319    let numel = checked_product(op, "tensor element count", shape)?;
1320    let expected = dtype
1321        .checked_storage_bytes(numel)
1322        .ok_or_else(|| overflow(op, "tensor byte count", shape))?;
1323    addressable(op, "tensor byte count", expected, shape)?;
1324    if data.len() != expected {
1325        return Err(SequenceError::ByteLengthMismatch {
1326            op,
1327            dtype,
1328            shape: clone_shape(op, shape)?,
1329            expected,
1330            actual: data.len(),
1331        });
1332    }
1333    Ok(())
1334}
1335
1336fn validate_view_bounds(
1337    op: &'static str,
1338    shape: &[usize],
1339    strides: &[i64],
1340    byte_offset: usize,
1341    dtype: DataType,
1342    root_len: usize,
1343) -> SequenceResult<()> {
1344    if shape.len() != strides.len() {
1345        return Err(SequenceError::InvalidSplit {
1346            op,
1347            reason: format!(
1348                "view rank mismatch: shape has {} dims but strides has {}",
1349                shape.len(),
1350                strides.len()
1351            ),
1352        });
1353    }
1354    let esize = dtype.byte_size();
1355    if esize == 0 {
1356        return Err(SequenceError::UnsupportedDtype { op, dtype });
1357    }
1358    if shape.contains(&0) {
1359        return Ok(());
1360    }
1361    let mut min_element = 0i128;
1362    let mut max_element = 0i128;
1363    for (&dim, &stride) in shape.iter().zip(strides) {
1364        let span = (dim.saturating_sub(1) as i128)
1365            .checked_mul(stride as i128)
1366            .ok_or_else(|| overflow(op, "view stride span", shape))?;
1367        if span < 0 {
1368            min_element = min_element
1369                .checked_add(span)
1370                .ok_or_else(|| overflow(op, "view minimum offset", shape))?;
1371        } else {
1372            max_element = max_element
1373                .checked_add(span)
1374                .ok_or_else(|| overflow(op, "view maximum offset", shape))?;
1375        }
1376    }
1377    let origin = byte_offset as i128;
1378    let min_byte = origin
1379        .checked_add(
1380            min_element
1381                .checked_mul(esize as i128)
1382                .ok_or_else(|| overflow(op, "view minimum byte offset", shape))?,
1383        )
1384        .ok_or_else(|| overflow(op, "view minimum byte offset", shape))?;
1385    let end_byte = origin
1386        .checked_add(
1387            max_element
1388                .checked_mul(esize as i128)
1389                .ok_or_else(|| overflow(op, "view maximum byte offset", shape))?,
1390        )
1391        .and_then(|offset| offset.checked_add(esize as i128))
1392        .ok_or_else(|| overflow(op, "view byte range", shape))?;
1393    if min_byte < 0 || end_byte > root_len as i128 {
1394        return Err(SequenceError::InvalidSplit {
1395            op,
1396            reason: format!(
1397                "view byte range [{min_byte}, {end_byte}) exceeds backing allocation of {root_len} bytes"
1398            ),
1399        });
1400    }
1401    Ok(())
1402}
1403
1404fn gather_strided(
1405    root: &[u8],
1406    shape: &[usize],
1407    strides: &[i64],
1408    byte_offset: usize,
1409    dtype: DataType,
1410    esize: usize,
1411) -> SequenceResult<Vec<u8>> {
1412    const OP: &str = "SequenceTensor";
1413    validate_view_bounds(OP, shape, strides, byte_offset, dtype, root.len())?;
1414    let numel = checked_product(OP, "view element count", shape)?;
1415    let bytes = checked_mul(OP, "view byte count", numel, esize, shape)?;
1416    let mut output = zeroed_bytes(OP, "strided tensor materialization", bytes, shape)?;
1417    if numel == 0 {
1418        return Ok(output);
1419    }
1420    let logical_strides = compute_contiguous_strides(shape);
1421    for linear in 0..numel {
1422        let mut remainder = linear;
1423        let mut source_element = 0i128;
1424        for dimension in 0..shape.len() {
1425            let coordinate = if shape[dimension] == 0 {
1426                0
1427            } else {
1428                remainder / logical_strides[dimension] as usize
1429            };
1430            if shape[dimension] != 0 {
1431                remainder %= logical_strides[dimension] as usize;
1432            }
1433            source_element += coordinate as i128 * strides[dimension] as i128;
1434        }
1435        let source = (byte_offset as i128 + source_element * esize as i128) as usize;
1436        output[linear * esize..(linear + 1) * esize].copy_from_slice(&root[source..source + esize]);
1437    }
1438    Ok(output)
1439}
1440
1441/// Stack already-validated contiguous element bytes along a new axis.
1442pub(crate) fn stack_new_axis(
1443    elements: &[&[u8]],
1444    elem_shape: &[usize],
1445    axis: usize,
1446    esize: usize,
1447) -> SequenceResult<(Vec<usize>, Vec<u8>)> {
1448    const OP: &str = "ConcatFromSequence";
1449    if axis > elem_shape.len() || esize == 0 {
1450        return Err(SequenceError::InvalidSplit {
1451            op: OP,
1452            reason: "invalid new axis or element byte size".to_string(),
1453        });
1454    }
1455    let outer = checked_product(OP, "stack outer element count", &elem_shape[..axis])?;
1456    let inner_elements = checked_product(OP, "stack inner element count", &elem_shape[axis..])?;
1457    let inner = checked_mul(
1458        OP,
1459        "stack inner byte count",
1460        inner_elements,
1461        esize,
1462        elem_shape,
1463    )?;
1464    let source_bytes = checked_mul(OP, "stack source byte count", outer, inner, elem_shape)?;
1465    addressable(OP, "stack source byte count", source_bytes, elem_shape)?;
1466    for element in elements {
1467        if element.len() != source_bytes {
1468            return Err(SequenceError::ByteLengthMismatch {
1469                op: OP,
1470                dtype: DataType::Uint8,
1471                shape: clone_shape(OP, elem_shape)?,
1472                expected: source_bytes,
1473                actual: element.len(),
1474            });
1475        }
1476    }
1477    let output_rows = checked_mul(
1478        OP,
1479        "stacked tensor output row count",
1480        elements.len(),
1481        outer,
1482        elem_shape,
1483    )?;
1484    let output_bytes = checked_mul(
1485        OP,
1486        "stack output byte count",
1487        output_rows,
1488        inner,
1489        elem_shape,
1490    )?;
1491    let mut bytes = zeroed_bytes(OP, "stack output", output_bytes, elem_shape)?;
1492    if inner != 0 {
1493        for (element_index, element) in elements.iter().enumerate() {
1494            for outer_index in 0..outer {
1495                let source_offset = checked_mul(
1496                    OP,
1497                    "stack source byte offset",
1498                    outer_index,
1499                    inner,
1500                    elem_shape,
1501                )?;
1502                let source_end = checked_add(
1503                    OP,
1504                    "stack source byte range",
1505                    source_offset,
1506                    inner,
1507                    elem_shape,
1508                )?;
1509                let destination_row = checked_add(
1510                    OP,
1511                    "stack destination row",
1512                    checked_mul(
1513                        OP,
1514                        "stack destination outer offset",
1515                        outer_index,
1516                        elements.len(),
1517                        elem_shape,
1518                    )?,
1519                    element_index,
1520                    elem_shape,
1521                )?;
1522                let destination_offset = checked_mul(
1523                    OP,
1524                    "stack destination byte offset",
1525                    destination_row,
1526                    inner,
1527                    elem_shape,
1528                )?;
1529                let destination_end = checked_add(
1530                    OP,
1531                    "stack destination byte range",
1532                    destination_offset,
1533                    inner,
1534                    elem_shape,
1535                )?;
1536                bytes[destination_offset..destination_end]
1537                    .copy_from_slice(&element[source_offset..source_end]);
1538            }
1539        }
1540    }
1541    let shape_capacity = elem_shape
1542        .len()
1543        .checked_add(1)
1544        .ok_or_else(|| overflow(OP, "stack output rank", elem_shape))?;
1545    let mut output_shape = Vec::new();
1546    output_shape
1547        .try_reserve_exact(shape_capacity)
1548        .map_err(|_| SequenceError::Allocation {
1549            op: OP,
1550            context: "stack output shape",
1551            bytes: shape_capacity.saturating_mul(std::mem::size_of::<usize>()),
1552        })?;
1553    output_shape.extend_from_slice(&elem_shape[..axis]);
1554    output_shape.push(elements.len());
1555    output_shape.extend_from_slice(&elem_shape[axis..]);
1556    Ok((output_shape, bytes))
1557}
1558
1559#[cfg(test)]
1560mod tests {
1561    use super::*;
1562
1563    fn elem(dtype: DataType, shape: &[usize], bytes: &[u8]) -> SeqTensor {
1564        SeqTensor::from_raw(dtype, shape.to_vec(), bytes).expect("valid test tensor")
1565    }
1566
1567    #[test]
1568    fn value_ops_share_tensor_arcs_without_copying() {
1569        let original = elem(DataType::Uint8, &[1], &[7]);
1570        let sequence = SequenceValue::construct(vec![original.clone()]).expect("construct");
1571        assert!(original.shares_storage_with(&sequence.elements()[0]));
1572
1573        let inserted = elem(DataType::Uint8, &[1], &[9]);
1574        let sequence = sequence.insert(inserted.clone(), Some(-1)).expect("insert");
1575        assert!(inserted.shares_storage_with(&sequence.at(0).expect("at")));
1576        assert!(original.shares_storage_with(&sequence.at(1).expect("at")));
1577
1578        let erased = sequence.erase(Some(0)).expect("erase");
1579        assert!(original.shares_storage_with(&erased.at(-1).expect("negative at")));
1580    }
1581
1582    #[test]
1583    fn moving_tensor_into_sequence_preserves_allocation_pointer() {
1584        let tensor = Tensor::from_raw(DataType::Uint8, vec![2], &[4, 5]).unwrap();
1585        let pointer = tensor.device_ptr();
1586        let element = SeqTensor::new(tensor);
1587        assert_eq!(element.as_ptr(), pointer);
1588        let sequence = SequenceValue::construct(vec![element.clone()]).unwrap();
1589        assert_eq!(sequence.at(0).unwrap().as_ptr(), pointer);
1590        assert_eq!(element.storage_strong_count(), 2);
1591    }
1592
1593    #[test]
1594    fn split_produces_shared_strided_views_without_copying() {
1595        let input = elem(DataType::Uint8, &[2, 3], &[0, 1, 2, 3, 4, 5]);
1596        let sequence = split_tensor(&input, 1, SplitSpec::Sizes(&[1, 2]), true).expect("split");
1597        assert_eq!(sequence.length(), 2);
1598        assert!(input.shares_storage_with(&sequence.elements()[0]));
1599        assert!(input.shares_storage_with(&sequence.elements()[1]));
1600        assert_eq!(sequence.elements()[0].byte_offset(), 0);
1601        assert_eq!(sequence.elements()[1].byte_offset(), 1);
1602        assert_eq!(
1603            sequence.elements()[0].contiguous_bytes().unwrap(),
1604            vec![0, 3]
1605        );
1606        assert_eq!(
1607            sequence.elements()[1].contiguous_bytes().unwrap(),
1608            vec![1, 2, 4, 5]
1609        );
1610    }
1611
1612    #[test]
1613    fn strided_split_element_byte_borrow_is_fallible_without_panicking() {
1614        let input = elem(DataType::Uint8, &[2, 3], &[0, 1, 2, 3, 4, 5]);
1615        let sequence = split_tensor(&input, 1, SplitSpec::Sizes(&[1, 2]), true).expect("split");
1616        let element = &sequence.elements()[0];
1617        assert!(matches!(
1618            element.as_bytes(),
1619            Err(SequenceError::ByteBorrowUnavailable {
1620                reason: "the view is strided",
1621                ..
1622            })
1623        ));
1624        assert_eq!(element.contiguous_bytes().unwrap(), vec![0, 3]);
1625    }
1626
1627    #[test]
1628    fn empty_construct_insert_erase_at_and_length() {
1629        let empty = SequenceValue::empty(DataType::Uint8);
1630        assert_eq!(empty.length(), 0);
1631        let one = empty
1632            .insert(elem(DataType::Uint8, &[1], &[1]), None)
1633            .unwrap();
1634        let two = one
1635            .insert(elem(DataType::Uint8, &[1], &[2]), Some(-1))
1636            .unwrap();
1637        let three = two
1638            .insert(elem(DataType::Uint8, &[1], &[3]), Some(2))
1639            .unwrap();
1640        assert_eq!(three.length(), 3);
1641        assert_eq!(three.at(0).unwrap().as_bytes().unwrap(), &[2]);
1642        assert_eq!(three.at(-1).unwrap().as_bytes().unwrap(), &[3]);
1643        let erased = three.erase(Some(-2)).unwrap();
1644        assert_eq!(erased.length(), 2);
1645        assert_eq!(erased.at(0).unwrap().as_bytes().unwrap(), &[2]);
1646        assert_eq!(erased.at(1).unwrap().as_bytes().unwrap(), &[3]);
1647    }
1648
1649    #[test]
1650    fn empty_sequence_edges_are_clean_errors() {
1651        let empty = SequenceValue::empty(DataType::Uint8);
1652        assert!(matches!(empty.erase(None), Err(SequenceError::EmptyErase)));
1653        assert!(matches!(
1654            empty.at(0),
1655            Err(SequenceError::IndexOutOfBounds { len: 0, .. })
1656        ));
1657        assert!(matches!(
1658            concat(&empty, 0, false),
1659            Err(SequenceError::InvalidSplit { .. })
1660        ));
1661    }
1662
1663    #[test]
1664    fn homogeneity_violation_is_typed_error() {
1665        let error = SequenceValue::construct(vec![
1666            elem(DataType::Uint8, &[1], &[1]),
1667            elem(DataType::Int64, &[1], &1i64.to_le_bytes()),
1668        ])
1669        .unwrap_err();
1670        assert!(matches!(
1671            error,
1672            SequenceError::DtypeMismatch {
1673                op: "SequenceConstruct",
1674                index: Some(1),
1675                ..
1676            }
1677        ));
1678    }
1679
1680    #[test]
1681    fn split_concat_roundtrip_existing_axes_and_keepdims() {
1682        let data: Vec<u8> = (0..12).collect();
1683        for (shape, axis, keepdims) in [
1684            (vec![3, 4], 0, true),
1685            (vec![3, 4], 1, true),
1686            (vec![3, 4], 0, false),
1687        ] {
1688            let input = elem(DataType::Uint8, &shape, &data);
1689            let sequence = split_tensor(&input, axis, SplitSpec::Each, keepdims).unwrap();
1690            let concat_axis = if keepdims { axis } else { 0 };
1691            let rebuilt = concat(&sequence, concat_axis, !keepdims).unwrap();
1692            assert_eq!(rebuilt.shape, shape);
1693            assert_eq!(rebuilt.as_bytes().unwrap(), data);
1694        }
1695    }
1696
1697    #[test]
1698    fn split_concat_roundtrip_explicit_sizes() {
1699        let data: Vec<u8> = (0..12).collect();
1700        let input = elem(DataType::Uint8, &[3, 4], &data);
1701        let sequence = split_tensor(&input, 1, SplitSpec::Sizes(&[1, 3]), false).unwrap();
1702        let rebuilt = concat(&sequence, 1, false).unwrap();
1703        assert_eq!(rebuilt.shape, vec![3, 4]);
1704        assert_eq!(rebuilt.as_bytes().unwrap(), data);
1705    }
1706
1707    #[test]
1708    fn stack_new_axis_variants() {
1709        let a = elem(DataType::Uint8, &[2], &[1, 2]);
1710        let b = elem(DataType::Uint8, &[2], &[3, 4]);
1711        let sequence = SequenceValue::construct(vec![a, b]).unwrap();
1712        let front = concat(&sequence, 0, true).unwrap();
1713        assert_eq!(front.shape, vec![2, 2]);
1714        assert_eq!(front.as_bytes().unwrap(), &[1, 2, 3, 4]);
1715        let back = concat(&sequence, 1, true).unwrap();
1716        assert_eq!(back.shape, vec![2, 2]);
1717        assert_eq!(back.as_bytes().unwrap(), &[1, 3, 2, 4]);
1718    }
1719
1720    #[test]
1721    fn concat_plan_uses_one_final_destination_without_source_materialization() {
1722        let a = elem(DataType::Uint8, &[2], &[1, 2]);
1723        let b = elem(DataType::Uint8, &[2], &[3, 4]);
1724        let sequence = SequenceValue::construct(vec![a, b]).unwrap();
1725        let plan = ConcatPlan::new(&sequence, 0, false).unwrap();
1726        let mut destination_allocations = 0;
1727        let mut destination = {
1728            destination_allocations += 1;
1729            vec![0; plan.bytes]
1730        };
1731        let stats = plan
1732            .write(&sequence, |offset, bytes| {
1733                destination[offset..offset + bytes.len()].copy_from_slice(bytes);
1734                Ok(())
1735            })
1736            .unwrap();
1737        assert_eq!(destination_allocations, 1);
1738        assert_eq!(stats.source_materializations, 0);
1739        assert_eq!(stats.destination_writes, 2);
1740        assert_eq!(destination, vec![1, 2, 3, 4]);
1741    }
1742
1743    #[test]
1744    fn byte_count_above_isize_max_is_rejected() {
1745        let error = validate_tensor_bytes(
1746            "SplitToSequence",
1747            &[],
1748            DataType::Uint8,
1749            &[isize::MAX as usize + 1, 1],
1750        )
1751        .unwrap_err();
1752        assert!(matches!(error, SequenceError::ShapeOverflow { .. }));
1753    }
1754
1755    #[test]
1756    fn sequence_values_are_send_sync() {
1757        fn assert_send_sync<T: Send + Sync>() {}
1758        assert_send_sync::<SeqTensor>();
1759        assert_send_sync::<SequenceValue>();
1760    }
1761}