1use smallvec::SmallVec;
28use std::mem::MaybeUninit;
29use strided_kernel::ExecContext;
30use strided_view::{RawStridedMut, StridedView, StridedViewMut};
31
32use crate::backend::Backend;
33use crate::{einsum2_dispatch, Einsum2Plan, EinsumError, Result, ScalarBase};
34
35#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
40pub struct DotGeneralConfig<'a> {
41 pub lhs_contracting_dims: &'a [usize],
42 pub rhs_contracting_dims: &'a [usize],
43 pub lhs_batch_dims: &'a [usize],
44 pub rhs_batch_dims: &'a [usize],
45}
46
47#[derive(Clone, Debug)]
48pub(crate) struct DotGeneralLabels {
49 pub lhs_labels: SmallVec<[usize; 8]>,
50 pub rhs_labels: SmallVec<[usize; 8]>,
51 pub out_labels: SmallVec<[usize; 8]>,
52}
53
54fn check_no_duplicates(dims: &[usize], label: &str) -> Result<()> {
55 for (i, &dim) in dims.iter().enumerate() {
56 if dims[..i].contains(&dim) {
57 return Err(EinsumError::InvalidDotGeneralConfig(format!(
58 "{label} contains duplicate dim {dim}"
59 )));
60 }
61 }
62 Ok(())
63}
64
65fn check_bounds(dims: &[usize], rank: usize, label: &str) -> Result<()> {
66 for &dim in dims {
67 if dim >= rank {
68 return Err(EinsumError::InvalidDotGeneralConfig(format!(
69 "{label} dim {dim} out of bounds for rank {rank}"
70 )));
71 }
72 }
73 Ok(())
74}
75
76fn free_dims(rank: usize, contracting: &[usize], batch: &[usize]) -> SmallVec<[usize; 8]> {
77 (0..rank)
78 .filter(|dim| !contracting.contains(dim) && !batch.contains(dim))
79 .collect()
80}
81
82impl DotGeneralConfig<'_> {
83 pub fn validate_dims_with_ranks(&self, lhs_rank: usize, rhs_rank: usize) -> Result<()> {
85 check_bounds(&self.lhs_contracting_dims, lhs_rank, "lhs_contracting")?;
86 check_bounds(&self.rhs_contracting_dims, rhs_rank, "rhs_contracting")?;
87 check_bounds(&self.lhs_batch_dims, lhs_rank, "lhs_batch")?;
88 check_bounds(&self.rhs_batch_dims, rhs_rank, "rhs_batch")?;
89 check_no_duplicates(&self.lhs_contracting_dims, "lhs_contracting_dims")?;
90 check_no_duplicates(&self.rhs_contracting_dims, "rhs_contracting_dims")?;
91 check_no_duplicates(&self.lhs_batch_dims, "lhs_batch_dims")?;
92 check_no_duplicates(&self.rhs_batch_dims, "rhs_batch_dims")?;
93
94 for &dim in self.lhs_contracting_dims {
95 if self.lhs_batch_dims.contains(&dim) {
96 return Err(EinsumError::InvalidDotGeneralConfig(format!(
97 "lhs dim {dim} appears in both contracting and batch dims"
98 )));
99 }
100 }
101 for &dim in self.rhs_contracting_dims {
102 if self.rhs_batch_dims.contains(&dim) {
103 return Err(EinsumError::InvalidDotGeneralConfig(format!(
104 "rhs dim {dim} appears in both contracting and batch dims"
105 )));
106 }
107 }
108 if self.lhs_contracting_dims.len() != self.rhs_contracting_dims.len() {
109 return Err(EinsumError::InvalidDotGeneralConfig(format!(
110 "lhs/rhs contracting dim counts differ ({} vs {})",
111 self.lhs_contracting_dims.len(),
112 self.rhs_contracting_dims.len()
113 )));
114 }
115 if self.lhs_batch_dims.len() != self.rhs_batch_dims.len() {
116 return Err(EinsumError::InvalidDotGeneralConfig(format!(
117 "lhs/rhs batch dim counts differ ({} vs {})",
118 self.lhs_batch_dims.len(),
119 self.rhs_batch_dims.len()
120 )));
121 }
122 Ok(())
123 }
124
125 pub fn expected_output_shape(
127 &self,
128 lhs_shape: &[usize],
129 rhs_shape: &[usize],
130 ) -> Result<Vec<usize>> {
131 self.validate_dims_with_ranks(lhs_shape.len(), rhs_shape.len())?;
132 self.validate_pair_shapes(lhs_shape, rhs_shape)?;
133
134 let lhs_free = free_dims(
135 lhs_shape.len(),
136 &self.lhs_contracting_dims,
137 &self.lhs_batch_dims,
138 );
139 let rhs_free = free_dims(
140 rhs_shape.len(),
141 &self.rhs_contracting_dims,
142 &self.rhs_batch_dims,
143 );
144
145 let mut out =
146 Vec::with_capacity(lhs_free.len() + rhs_free.len() + self.lhs_batch_dims.len());
147 out.extend(lhs_free.iter().map(|&dim| lhs_shape[dim]));
148 out.extend(rhs_free.iter().map(|&dim| rhs_shape[dim]));
149 out.extend(self.lhs_batch_dims.iter().map(|&dim| lhs_shape[dim]));
150 Ok(out)
151 }
152
153 fn validate_pair_shapes(&self, lhs_shape: &[usize], rhs_shape: &[usize]) -> Result<()> {
154 for (&lhs_dim, &rhs_dim) in self.lhs_batch_dims.iter().zip(self.rhs_batch_dims) {
155 if lhs_shape[lhs_dim] != rhs_shape[rhs_dim] {
156 return Err(EinsumError::DimensionMismatch {
157 axis: format!("batch lhs {lhs_dim} rhs {rhs_dim}"),
158 dim_a: lhs_shape[lhs_dim],
159 dim_b: rhs_shape[rhs_dim],
160 });
161 }
162 }
163 for (&lhs_dim, &rhs_dim) in self
164 .lhs_contracting_dims
165 .iter()
166 .zip(self.rhs_contracting_dims)
167 {
168 if lhs_shape[lhs_dim] != rhs_shape[rhs_dim] {
169 return Err(EinsumError::DimensionMismatch {
170 axis: format!("contract lhs {lhs_dim} rhs {rhs_dim}"),
171 dim_a: lhs_shape[lhs_dim],
172 dim_b: rhs_shape[rhs_dim],
173 });
174 }
175 }
176 Ok(())
177 }
178
179 pub(crate) fn labels_for_shapes(
180 &self,
181 lhs_shape: &[usize],
182 rhs_shape: &[usize],
183 out_shape: &[usize],
184 ) -> Result<DotGeneralLabels> {
185 self.validate_dims_with_ranks(lhs_shape.len(), rhs_shape.len())?;
186 self.validate_pair_shapes(lhs_shape, rhs_shape)?;
187
188 let lhs_free = free_dims(
189 lhs_shape.len(),
190 &self.lhs_contracting_dims,
191 &self.lhs_batch_dims,
192 );
193 let rhs_free = free_dims(
194 rhs_shape.len(),
195 &self.rhs_contracting_dims,
196 &self.rhs_batch_dims,
197 );
198
199 let mut lhs_labels = smallvec::smallvec![usize::MAX; lhs_shape.len()];
200 let mut rhs_labels = smallvec::smallvec![usize::MAX; rhs_shape.len()];
201 let mut next_label = 0usize;
202
203 for (&lhs_dim, &rhs_dim) in self.lhs_batch_dims.iter().zip(self.rhs_batch_dims) {
204 lhs_labels[lhs_dim] = next_label;
205 rhs_labels[rhs_dim] = next_label;
206 next_label += 1;
207 }
208 for &lhs_dim in &lhs_free {
209 lhs_labels[lhs_dim] = next_label;
210 next_label += 1;
211 }
212 for &rhs_dim in &rhs_free {
213 rhs_labels[rhs_dim] = next_label;
214 next_label += 1;
215 }
216 for (&lhs_dim, &rhs_dim) in self
217 .lhs_contracting_dims
218 .iter()
219 .zip(self.rhs_contracting_dims)
220 {
221 lhs_labels[lhs_dim] = next_label;
222 rhs_labels[rhs_dim] = next_label;
223 next_label += 1;
224 }
225
226 let expected_out_shape = self.expected_output_shape(lhs_shape, rhs_shape)?;
227 if expected_out_shape.as_slice() != out_shape {
228 return Err(EinsumError::OutputShapeMismatch {
229 expected: expected_out_shape,
230 got: out_shape.to_vec(),
231 });
232 }
233
234 let mut out_labels = SmallVec::<[usize; 8]>::new();
235 out_labels.extend(lhs_free.iter().map(|&dim| lhs_labels[dim]));
236 out_labels.extend(rhs_free.iter().map(|&dim| rhs_labels[dim]));
237 out_labels.extend(self.lhs_batch_dims.iter().map(|&dim| lhs_labels[dim]));
238
239 Ok(DotGeneralLabels {
240 lhs_labels,
241 rhs_labels,
242 out_labels,
243 })
244 }
245}
246
247pub fn dot_general_with_backend_into<T, B>(
249 c: StridedViewMut<T>,
250 a: &StridedView<T>,
251 b: &StridedView<T>,
252 config: &DotGeneralConfig<'_>,
253 alpha: T,
254 beta: T,
255) -> Result<()>
256where
257 T: ScalarBase,
258 B: Backend<T>,
259{
260 let labels = config.labels_for_shapes(a.dims(), b.dims(), c.dims())?;
261 let plan = Einsum2Plan::new(
262 labels.lhs_labels.as_slice(),
263 labels.rhs_labels.as_slice(),
264 labels.out_labels.as_slice(),
265 )?;
266 einsum2_dispatch::<T, B, _>(c, a, b, &plan, alpha, beta, false, false, None)
267}
268
269#[cfg(any(feature = "faer", feature = "blas", feature = "blas-inject"))]
271pub fn dot_general_into<T: crate::Scalar>(
272 c: StridedViewMut<T>,
273 a: &StridedView<T>,
274 b: &StridedView<T>,
275 config: &DotGeneralConfig<'_>,
276 alpha: T,
277 beta: T,
278) -> Result<()>
279where
280 crate::backend::ActiveBackend: Backend<T>,
281{
282 dot_general_with_backend_into::<T, crate::backend::ActiveBackend>(c, a, b, config, alpha, beta)
283}
284
285pub fn dot_general_into_uninit<T: crate::Scalar>(
290 c: &mut RawStridedMut<'_, MaybeUninit<T>>,
291 a: &StridedView<'_, T>,
292 b: &StridedView<'_, T>,
293 config: &DotGeneralConfig<'_>,
294 alpha: T,
295 ctx: &ExecContext,
296) -> Result<()> {
297 let labels = config.labels_for_shapes(a.dims(), b.dims(), c.dims())?;
298 crate::einsum2_into_uninit(
299 c,
300 a,
301 b,
302 labels.out_labels.as_slice(),
303 labels.lhs_labels.as_slice(),
304 labels.rhs_labels.as_slice(),
305 alpha,
306 ctx,
307 )
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use std::mem::MaybeUninit;
314
315 #[test]
316 fn uninit_entry_point_routes_through_einsum_validation() {
317 let a = strided_view::StridedArray::from_fn_col_major(&[2, 3], |idx| {
318 (idx[0] + 2 * idx[1] + 1) as f64
319 });
320 let b = strided_view::StridedArray::from_fn_col_major(&[3, 2], |idx| {
321 (idx[0] + 3 * idx[1] + 1) as f64
322 });
323 let mut storage = vec![MaybeUninit::<f64>::uninit(); 4];
324 let mut c = RawStridedMut::new(&mut storage, &[2, 2], &[2, 1], 0).unwrap();
325 let config = DotGeneralConfig {
326 lhs_contracting_dims: &[1],
327 rhs_contracting_dims: &[0],
328 lhs_batch_dims: &[],
329 rhs_batch_dims: &[],
330 };
331
332 let err = dot_general_into_uninit(
333 &mut c,
334 &a.view(),
335 &b.view(),
336 &config,
337 1.0,
338 &ExecContext::serial(),
339 )
340 .unwrap_err();
341 assert!(matches!(err, crate::EinsumError::Unsupported(_)));
342 }
343}
344
345#[cfg(not(any(feature = "faer", feature = "blas", feature = "blas-inject")))]
347pub fn dot_general_into<T: crate::Scalar>(
348 c: StridedViewMut<T>,
349 a: &StridedView<T>,
350 b: &StridedView<T>,
351 config: &DotGeneralConfig<'_>,
352 alpha: T,
353 beta: T,
354) -> Result<()> {
355 let labels = config.labels_for_shapes(a.dims(), b.dims(), c.dims())?;
356 crate::einsum2_naive_into(
357 c,
358 a,
359 b,
360 labels.out_labels.as_slice(),
361 labels.lhs_labels.as_slice(),
362 labels.rhs_labels.as_slice(),
363 alpha,
364 beta,
365 |x| x,
366 |x| x,
367 )
368}