Skip to main content

tenferro_tensor/
config.rs

1use tinyvec::TinyVec;
2
3use crate::{Error, Result, ValidationError};
4
5const DOT_GENERAL_OP: &str = "dot_general";
6
7fn invalid_dot_general_config(message: impl Into<String>) -> Error {
8    Error::invalid_argument(DOT_GENERAL_OP, "dot_general_config", message)
9}
10
11/// DotGeneral dimension configuration.
12///
13/// Each axis list stores up to four axes inline and spills to the heap for
14/// larger contractions; this is not a tensor-rank limit.
15/// Records only the dim-numbering roles (contracting / batch; free is derived).
16/// Rank info travels with the enclosing `StdTensorOp::DotGeneral` variant at
17/// the trace/StdTensorOp layer, and with `ExecInstruction::output_shapes` at
18/// the exec layer. This separation makes it structurally impossible for
19/// stored ranks to drift from actual tensor ranks (issue #664).
20///
21/// # Output layout
22///
23/// The output shape is `[lhs_free..., rhs_free..., batch...]` (col-major
24/// batch-trailing convention): batch axes come **last**, unlike PyTorch's
25/// batch-leading `bmm`. Batch dims have the largest stride so that each batch
26/// slice occupies a contiguous block of memory. Free axes keep their input
27/// order. For attention scores, `q[d, lq, b]` against `k[d, lk, b]` with
28/// `d` contracted and `b` batched gives `[lq, lk, b]`; see the
29/// "Contraction Output Layout" section of the tensor-operations guide for a
30/// worked example. Transpose afterwards only where a consumer needs batch
31/// first.
32///
33/// # Examples
34///
35/// ```rust
36/// use tenferro_tensor::DotGeneralConfig;
37///
38/// let config = DotGeneralConfig {
39///     lhs_contracting_dims: [1].as_slice().into(),
40///     rhs_contracting_dims: [0].as_slice().into(),
41///     lhs_batch_dims: [].as_slice().into(),
42///     rhs_batch_dims: [].as_slice().into(),
43/// };
44/// ```
45#[derive(Clone, Debug, Hash, PartialEq, Eq)]
46pub struct DotGeneralConfig {
47    pub lhs_contracting_dims: TinyVec<[usize; 4]>,
48    pub rhs_contracting_dims: TinyVec<[usize; 4]>,
49    pub lhs_batch_dims: TinyVec<[usize; 4]>,
50    pub rhs_batch_dims: TinyVec<[usize; 4]>,
51}
52
53/// The degenerate algebra a `DotGeneral` actually is when it is not a
54/// contraction, classified from the config and the operand shapes alone.
55///
56/// The semantic rewrite and the eager executor share this rule so a degenerate
57/// form is rewritten once, in one place, instead of each backend implementing
58/// it implicitly (invariant I4 of the #2112 umbrella).
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub enum DegenerateDotKind {
61    /// Every axis of both operands is a batch axis, in each operand's own
62    /// order, and both operands carry the same dimensions: an elementwise
63    /// product.
64    Hadamard,
65    /// No contracting and no batch dims, free dims on both sides: an outer
66    /// product (the concatenated product).
67    OuterProduct,
68    /// No contracting and no batch dims, free dims on one side only: that
69    /// operand, permuted into the output's order and scaled by the other.
70    ScalarTranspose,
71    /// Every dimension contracts: a product reduced to rank 0.
72    FullContraction,
73}
74
75/// Classify a `DotGeneral` whose algebra is not a contraction.
76///
77/// Returns `None` when the form is a real contraction (or a shape this rule
78/// does not model), so callers fall through to their general path.
79#[must_use]
80pub fn degenerate_dot_kind<T: PartialEq>(
81    config: &DotGeneralConfig,
82    lhs_shape: &[T],
83    rhs_shape: &[T],
84) -> Option<DegenerateDotKind> {
85    if !config.lhs_contracting_dims.is_empty() || !config.rhs_contracting_dims.is_empty() {
86        let all_contract = |rank: usize, dims: &[usize]| {
87            dims.len() == rank
88                && dims
89                    .iter()
90                    .enumerate()
91                    .all(|(position, &axis)| axis == position)
92        };
93        return (config.lhs_batch_dims.is_empty()
94            && config.rhs_batch_dims.is_empty()
95            && all_contract(lhs_shape.len(), &config.lhs_contracting_dims)
96            && all_contract(rhs_shape.len(), &config.rhs_contracting_dims))
97        .then_some(DegenerateDotKind::FullContraction);
98    }
99    // Nothing contracts. Every axis of both operands being a batch axis means
100    // nothing is free either and the product is elementwise - but only when both
101    // batch lists put the axes in the operand's own order. Batch axis `k` pairs
102    // `lhs_batch_dims[k]` with `rhs_batch_dims[k]`, so a permuted list pairs
103    // different axes (`out[i, j] = a[i, j] * b[j, i]`) and a plain multiply would
104    // be wrong. The general path handles that form; measured with `[2, 2]`
105    // operands, batch `[0, 1]` against `[1, 0]`, which came back transposed.
106    let identity_ordered = |dims: &[usize], rank: usize| {
107        dims.len() == rank
108            && dims
109                .iter()
110                .enumerate()
111                .all(|(position, &axis)| axis == position)
112    };
113    if identity_ordered(&config.lhs_batch_dims, lhs_shape.len())
114        && identity_ordered(&config.rhs_batch_dims, rhs_shape.len())
115        && lhs_shape == rhs_shape
116    {
117        return Some(DegenerateDotKind::Hadamard);
118    }
119    if !config.lhs_batch_dims.is_empty() || !config.rhs_batch_dims.is_empty() {
120        return None;
121    }
122    // No batch and no contracting dims means **every** operand axis is free, so
123    // `dot_general_output_shape` makes the output `lhs ++ rhs` - an outer
124    // product, whatever the two shapes are. Equal shapes do not make it a
125    // Hadamard: that form needs every axis to be a *batch* axis, which the arm
126    // above already covers. Reading equal shapes as a Hadamard here made both
127    // consumers of this classifier produce `a.shape` where the graph declared
128    // `lhs ++ rhs` (#2165).
129    match (lhs_shape.is_empty(), rhs_shape.is_empty()) {
130        (false, false) => Some(DegenerateDotKind::OuterProduct),
131        (true, false) | (false, true) => Some(DegenerateDotKind::ScalarTranspose),
132        (true, true) => None,
133    }
134}
135
136impl DotGeneralConfig {
137    fn check_no_duplicates(dims: &[usize], label: &'static str) -> Result<()> {
138        let mut seen = std::collections::HashSet::new();
139        for &d in dims {
140            if !seen.insert(d) {
141                return Err(Error::validation(
142                    DOT_GENERAL_OP,
143                    ValidationError::DuplicateAxis {
144                        axis: d,
145                        role: label,
146                    },
147                ));
148            }
149        }
150        Ok(())
151    }
152
153    /// Validate that all dimension indices are within range for the given
154    /// explicit ranks and that no axis appears in multiple roles.
155    ///
156    /// Call sites supply the actual operand ranks (from the tensor shapes they
157    /// have in hand). The config itself carries only the dim-numbering roles.
158    ///
159    /// # Examples
160    ///
161    /// ```rust
162    /// use tenferro_tensor::DotGeneralConfig;
163    ///
164    /// let config = DotGeneralConfig {
165    ///     lhs_contracting_dims: [1].as_slice().into(),
166    ///     rhs_contracting_dims: [0].as_slice().into(),
167    ///     lhs_batch_dims: [].as_slice().into(),
168    ///     rhs_batch_dims: [].as_slice().into(),
169    /// };
170    /// config.validate_dims_with_ranks(2, 2).unwrap();
171    /// ```
172    /// # Errors
173    ///
174    /// Returns [`crate::Error::Validation`] with an axis, duplicate-axis,
175    /// or configuration source when the dimension roles are invalid.
176    pub fn validate_dims_with_ranks(&self, lhs_rank: usize, rhs_rank: usize) -> Result<()> {
177        for &d in &self.lhs_contracting_dims {
178            if d >= lhs_rank {
179                return Err(Error::validation(
180                    DOT_GENERAL_OP,
181                    ValidationError::AxisOutOfBounds {
182                        axis: d,
183                        rank: lhs_rank,
184                    },
185                ));
186            }
187        }
188        for &d in &self.rhs_contracting_dims {
189            if d >= rhs_rank {
190                return Err(Error::validation(
191                    DOT_GENERAL_OP,
192                    ValidationError::AxisOutOfBounds {
193                        axis: d,
194                        rank: rhs_rank,
195                    },
196                ));
197            }
198        }
199        for &d in &self.lhs_batch_dims {
200            if d >= lhs_rank {
201                return Err(Error::validation(
202                    DOT_GENERAL_OP,
203                    ValidationError::AxisOutOfBounds {
204                        axis: d,
205                        rank: lhs_rank,
206                    },
207                ));
208            }
209        }
210        for &d in &self.rhs_batch_dims {
211            if d >= rhs_rank {
212                return Err(Error::validation(
213                    DOT_GENERAL_OP,
214                    ValidationError::AxisOutOfBounds {
215                        axis: d,
216                        rank: rhs_rank,
217                    },
218                ));
219            }
220        }
221        Self::check_no_duplicates(&self.lhs_contracting_dims, "lhs_contracting_dims")?;
222        Self::check_no_duplicates(&self.rhs_contracting_dims, "rhs_contracting_dims")?;
223        Self::check_no_duplicates(&self.lhs_batch_dims, "lhs_batch_dims")?;
224        Self::check_no_duplicates(&self.rhs_batch_dims, "rhs_batch_dims")?;
225        for &d in &self.lhs_contracting_dims {
226            if self.lhs_batch_dims.contains(&d) {
227                return Err(Error::validation(
228                    DOT_GENERAL_OP,
229                    ValidationError::AxisRoleConflict {
230                        axis: d,
231                        first_role: "lhs contracting",
232                        second_role: "lhs batch",
233                    },
234                ));
235            }
236        }
237        for &d in &self.rhs_contracting_dims {
238            if self.rhs_batch_dims.contains(&d) {
239                return Err(Error::validation(
240                    DOT_GENERAL_OP,
241                    ValidationError::AxisRoleConflict {
242                        axis: d,
243                        first_role: "rhs contracting",
244                        second_role: "rhs batch",
245                    },
246                ));
247            }
248        }
249        if self.lhs_contracting_dims.len() != self.rhs_contracting_dims.len() {
250            return Err(invalid_dot_general_config(format!(
251                "lhs/rhs contracting dim counts differ ({} vs {})",
252                self.lhs_contracting_dims.len(),
253                self.rhs_contracting_dims.len()
254            )));
255        }
256        if self.lhs_batch_dims.len() != self.rhs_batch_dims.len() {
257            return Err(invalid_dot_general_config(format!(
258                "lhs/rhs batch dim counts differ ({} vs {})",
259                self.lhs_batch_dims.len(),
260                self.rhs_batch_dims.len()
261            )));
262        }
263        Ok(())
264    }
265}
266
267/// Comparison direction.
268///
269/// # Examples
270///
271/// ```rust
272/// use tenferro_tensor::CompareDir;
273///
274/// let dir = CompareDir::Eq;
275/// ```
276#[derive(Clone, Debug, Hash, PartialEq, Eq)]
277pub enum CompareDir {
278    Eq,
279    Lt,
280    Le,
281    Gt,
282    Ge,
283}
284
285/// StableHLO gather dimension configuration.
286///
287/// # Examples
288///
289/// ```rust
290/// use tenferro_tensor::GatherConfig;
291///
292/// let config = GatherConfig {
293///     offset_dims: vec![],
294///     collapsed_slice_dims: vec![0],
295///     start_index_map: vec![0],
296///     index_vector_dim: 1,
297///     slice_sizes: vec![1],
298/// };
299/// ```
300#[derive(Clone, Debug, Hash, PartialEq, Eq)]
301pub struct GatherConfig {
302    pub offset_dims: Vec<usize>,
303    pub collapsed_slice_dims: Vec<usize>,
304    pub start_index_map: Vec<usize>,
305    pub index_vector_dim: usize,
306    pub slice_sizes: Vec<usize>,
307}
308
309/// StableHLO scatter dimension configuration.
310///
311/// # Examples
312///
313/// ```rust
314/// use tenferro_tensor::ScatterConfig;
315///
316/// let config = ScatterConfig {
317///     update_window_dims: vec![],
318///     inserted_window_dims: vec![0],
319///     scatter_dims_to_operand_dims: vec![0],
320///     index_vector_dim: 1,
321/// };
322/// ```
323#[derive(Clone, Debug, Hash, PartialEq, Eq)]
324pub struct ScatterConfig {
325    pub update_window_dims: Vec<usize>,
326    pub inserted_window_dims: Vec<usize>,
327    pub scatter_dims_to_operand_dims: Vec<usize>,
328    pub index_vector_dim: usize,
329}
330
331/// Slice configuration.
332///
333/// # Examples
334///
335/// ```rust
336/// use tenferro_tensor::SliceConfig;
337///
338/// let config = SliceConfig {
339///     starts: vec![0],
340///     limits: vec![2],
341///     strides: vec![1],
342/// };
343/// ```
344#[derive(Clone, Debug, Hash, PartialEq, Eq)]
345pub struct SliceConfig {
346    pub starts: Vec<usize>,
347    pub limits: Vec<usize>,
348    pub strides: Vec<usize>,
349}
350
351/// StableHLO pad configuration.
352///
353/// # Examples
354///
355/// ```rust
356/// use tenferro_tensor::PadConfig;
357///
358/// let config = PadConfig {
359///     edge_padding_low: vec![1, 1],
360///     edge_padding_high: vec![1, 1],
361///     interior_padding: vec![0, 0],
362/// };
363/// ```
364#[derive(Clone, Debug, Hash, PartialEq, Eq)]
365pub struct PadConfig {
366    pub edge_padding_low: Vec<i64>,
367    pub edge_padding_high: Vec<i64>,
368    pub interior_padding: Vec<i64>,
369}