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}