1use core::mem::MaybeUninit;
4use std::ops::Mul;
5
6use crate::view::{StridedView, StridedViewMut};
7use crate::MaybeSendSync;
8use crate::{broadcast_mul_into, broadcast_mul_into_uninit};
9use crate::{ElementOp, Result, StridedError};
10
11type AxisVec<T> = crate::inline::InlineVec<T>;
12
13pub fn batched_outer_product_into<D, A, B, OpA, OpB>(
20 dest: &mut StridedViewMut<D>,
21 lhs: &StridedView<A, OpA>,
22 rhs: &StridedView<B, OpB>,
23 lhs_free_ndim: usize,
24 rhs_free_ndim: usize,
25) -> Result<()>
26where
27 D: Copy + MaybeSendSync + 'static,
28 A: Copy + MaybeSendSync + Mul<B, Output = D> + 'static,
29 B: Copy + MaybeSendSync + 'static,
30 OpA: ElementOp<A>,
31 OpB: ElementOp<B>,
32{
33 validate_batched_outer_shape(dest, lhs, rhs, lhs_free_ndim, rhs_free_ndim)?;
34
35 let batch_ndim = lhs.ndim() - lhs_free_ndim;
36 let mut lhs_axes = AxisVec::<usize>::with_capacity(lhs.ndim());
37 let mut rhs_axes = AxisVec::<usize>::with_capacity(rhs.ndim());
38
39 lhs_axes.extend(0..lhs_free_ndim);
40 rhs_axes.extend(lhs_free_ndim..lhs_free_ndim + rhs_free_ndim);
41
42 let batch_axis_start = lhs_free_ndim + rhs_free_ndim;
43 lhs_axes.extend(batch_axis_start..batch_axis_start + batch_ndim);
44 rhs_axes.extend(batch_axis_start..batch_axis_start + batch_ndim);
45
46 broadcast_mul_into(dest, lhs, &lhs_axes, rhs, &rhs_axes)
47}
48
49pub fn batched_outer_product_into_uninit<D, A, B, OpA, OpB>(
68 dest: &mut StridedViewMut<MaybeUninit<D>>,
69 lhs: &StridedView<A, OpA>,
70 rhs: &StridedView<B, OpB>,
71 lhs_free_ndim: usize,
72 rhs_free_ndim: usize,
73) -> Result<()>
74where
75 D: Copy + MaybeSendSync + 'static,
76 A: Copy + MaybeSendSync + Mul<B, Output = D> + 'static,
77 B: Copy + MaybeSendSync + 'static,
78 OpA: ElementOp<A>,
79 OpB: ElementOp<B>,
80{
81 validate_batched_outer_shape(dest, lhs, rhs, lhs_free_ndim, rhs_free_ndim)?;
82 let batch_ndim = lhs.ndim() - lhs_free_ndim;
83 let mut lhs_axes = AxisVec::<usize>::with_capacity(lhs.ndim());
84 let mut rhs_axes = AxisVec::<usize>::with_capacity(rhs.ndim());
85 lhs_axes.extend(0..lhs_free_ndim);
86 rhs_axes.extend(lhs_free_ndim..lhs_free_ndim + rhs_free_ndim);
87 let batch_axis_start = lhs_free_ndim + rhs_free_ndim;
88 lhs_axes.extend(batch_axis_start..batch_axis_start + batch_ndim);
89 rhs_axes.extend(batch_axis_start..batch_axis_start + batch_ndim);
90 broadcast_mul_into_uninit(dest, lhs, &lhs_axes, rhs, &rhs_axes)
91}
92
93fn validate_batched_outer_shape<D, A, OpA, B, OpB>(
94 dest: &StridedViewMut<D>,
95 lhs: &StridedView<A, OpA>,
96 rhs: &StridedView<B, OpB>,
97 lhs_free_ndim: usize,
98 rhs_free_ndim: usize,
99) -> Result<()> {
100 if lhs_free_ndim > lhs.ndim() {
101 return Err(StridedError::RankMismatch(lhs_free_ndim, lhs.ndim()));
102 }
103 if rhs_free_ndim > rhs.ndim() {
104 return Err(StridedError::RankMismatch(rhs_free_ndim, rhs.ndim()));
105 }
106
107 let lhs_batch_ndim = lhs.ndim() - lhs_free_ndim;
108 let rhs_batch_ndim = rhs.ndim() - rhs_free_ndim;
109 if lhs_batch_ndim != rhs_batch_ndim {
110 return Err(StridedError::RankMismatch(lhs_batch_ndim, rhs_batch_ndim));
111 }
112
113 let expected_dest_rank = lhs_free_ndim + rhs_free_ndim + lhs_batch_ndim;
114 if dest.ndim() != expected_dest_rank {
115 return Err(StridedError::RankMismatch(dest.ndim(), expected_dest_rank));
116 }
117
118 ensure_dims(&dest.dims()[..lhs_free_ndim], &lhs.dims()[..lhs_free_ndim])?;
119 ensure_dims(
120 &dest.dims()[lhs_free_ndim..lhs_free_ndim + rhs_free_ndim],
121 &rhs.dims()[..rhs_free_ndim],
122 )?;
123 ensure_dims(
124 &dest.dims()[lhs_free_ndim + rhs_free_ndim..],
125 &lhs.dims()[lhs_free_ndim..],
126 )?;
127 ensure_dims(
128 &dest.dims()[lhs_free_ndim + rhs_free_ndim..],
129 &rhs.dims()[rhs_free_ndim..],
130 )?;
131
132 Ok(())
133}
134
135fn ensure_dims(actual: &[usize], expected: &[usize]) -> Result<()> {
136 if actual == expected {
137 Ok(())
138 } else {
139 Err(StridedError::ShapeMismatch(
140 actual.to_vec(),
141 expected.to_vec(),
142 ))
143 }
144}
145
146#[derive(Clone, Debug, PartialEq, Eq)]
155pub struct LazyOuterProductLayout {
156 pub base_dims: Vec<usize>,
158 pub output_strides: Vec<isize>,
160}
161
162pub fn plan_lazy_outer_product(
200 output_dims: &[usize],
201 lhs_dims: &[usize],
202 lhs_strides: &[isize],
203 lhs_axes: &[usize],
204 rhs_dims: &[usize],
205 rhs_strides: &[isize],
206 rhs_axes: &[usize],
207) -> Result<Option<LazyOuterProductLayout>> {
208 for (dims, strides, axes) in [
209 (lhs_dims, lhs_strides, lhs_axes),
210 (rhs_dims, rhs_strides, rhs_axes),
211 ] {
212 if dims.len() != strides.len() {
213 return Err(StridedError::RankMismatch(dims.len(), strides.len()));
214 }
215 if dims.len() != axes.len() {
216 return Err(StridedError::RankMismatch(dims.len(), axes.len()));
217 }
218 }
219 if !extents_match_output(output_dims, lhs_dims, lhs_axes)
220 || !extents_match_output(output_dims, rhs_dims, rhs_axes)
221 || lhs_strides
222 .iter()
223 .chain(rhs_strides)
224 .any(|&stride| stride < 0)
225 {
226 return Ok(None);
227 }
228 let Some(partition) = classify_outer_axes(output_dims.len(), lhs_axes, rhs_axes) else {
229 return Ok(None);
230 };
231 if checked_axes_product(lhs_dims, &partition.lhs_free)? <= 1
232 || checked_axes_product(rhs_dims, &partition.rhs_free)? <= 1
233 {
234 return Ok(None);
235 }
236
237 let output_rank = output_dims.len();
238 let lhs_prefix = partition
239 .lhs_free_out
240 .iter()
241 .chain(&partition.rhs_free_out)
242 .chain(&partition.batch_out)
243 .copied()
244 .eq(0..output_rank);
245 let rhs_prefix = !lhs_prefix
246 && partition
247 .rhs_free_out
248 .iter()
249 .chain(&partition.lhs_free_out)
250 .chain(&partition.batch_out)
251 .copied()
252 .eq(0..output_rank);
253 if !lhs_prefix && !rhs_prefix {
254 return Ok(None);
255 }
256
257 let lhs_physical = axes_by_physical_stride(lhs_strides, &partition.lhs_free);
258 let rhs_physical = axes_by_physical_stride(rhs_strides, &partition.rhs_free);
259 if lhs_physical == partition.lhs_free && rhs_physical == partition.rhs_free {
260 return Ok(None);
261 }
262
263 let mut base_out_axes = Vec::with_capacity(output_rank);
265 let (leading, leading_axes, trailing, trailing_axes) = if lhs_prefix {
266 (&lhs_physical, lhs_axes, &rhs_physical, rhs_axes)
267 } else {
268 (&rhs_physical, rhs_axes, &lhs_physical, lhs_axes)
269 };
270 base_out_axes.extend(leading.iter().map(|&axis| leading_axes[axis]));
271 base_out_axes.extend(trailing.iter().map(|&axis| trailing_axes[axis]));
272 base_out_axes.extend(partition.batch_out.iter().copied());
273
274 let base_dims: Vec<usize> = base_out_axes
275 .iter()
276 .map(|&axis| output_dims[axis])
277 .collect();
278 let mut output_strides = vec![0isize; output_rank];
279 let mut stride = 1isize;
280 for (&out_axis, &extent) in base_out_axes.iter().zip(&base_dims) {
281 output_strides[out_axis] = stride;
284 let extent = isize::try_from(extent).map_err(|_| StridedError::OffsetOverflow)?;
285 stride = stride
286 .checked_mul(extent)
287 .ok_or(StridedError::OffsetOverflow)?;
288 }
289 Ok(Some(LazyOuterProductLayout {
290 base_dims,
291 output_strides,
292 }))
293}
294
295struct OuterAxisPartition {
296 lhs_free_out: Vec<usize>,
297 rhs_free_out: Vec<usize>,
298 batch_out: Vec<usize>,
299 lhs_free: Vec<usize>,
300 rhs_free: Vec<usize>,
301}
302
303fn extents_match_output(output_dims: &[usize], dims: &[usize], axes: &[usize]) -> bool {
304 dims.iter()
305 .zip(axes)
306 .all(|(&dim, &axis)| output_dims.get(axis) == Some(&dim))
307}
308
309fn operand_axes_by_output(axes: &[usize], output_rank: usize) -> Option<Vec<Option<usize>>> {
310 let mut by_output = vec![None; output_rank];
311 for (operand_axis, &output_axis) in axes.iter().enumerate() {
312 if by_output
313 .get_mut(output_axis)?
314 .replace(operand_axis)
315 .is_some()
316 {
317 return None;
318 }
319 }
320 Some(by_output)
321}
322
323fn classify_outer_axes(
324 output_rank: usize,
325 lhs_axes: &[usize],
326 rhs_axes: &[usize],
327) -> Option<OuterAxisPartition> {
328 let lhs_by_output = operand_axes_by_output(lhs_axes, output_rank)?;
329 let rhs_by_output = operand_axes_by_output(rhs_axes, output_rank)?;
330 let mut partition = OuterAxisPartition {
331 lhs_free_out: Vec::new(),
332 rhs_free_out: Vec::new(),
333 batch_out: Vec::new(),
334 lhs_free: Vec::new(),
335 rhs_free: Vec::new(),
336 };
337 for output_axis in 0..output_rank {
338 match (lhs_by_output[output_axis], rhs_by_output[output_axis]) {
339 (Some(_), Some(_)) => partition.batch_out.push(output_axis),
340 (Some(lhs_axis), None) => {
341 partition.lhs_free_out.push(output_axis);
342 partition.lhs_free.push(lhs_axis);
343 }
344 (None, Some(rhs_axis)) => {
345 partition.rhs_free_out.push(output_axis);
346 partition.rhs_free.push(rhs_axis);
347 }
348 (None, None) => return None,
349 }
350 }
351 Some(partition)
352}
353
354fn checked_axes_product(dims: &[usize], axes: &[usize]) -> Result<usize> {
355 if axes.iter().any(|&axis| dims[axis] == 0) {
358 return Ok(0);
359 }
360 axes.iter().try_fold(1usize, |acc, &axis| {
361 acc.checked_mul(dims[axis])
362 .ok_or(StridedError::OffsetOverflow)
363 })
364}
365
366fn axes_by_physical_stride(strides: &[isize], axes: &[usize]) -> Vec<usize> {
367 let mut sorted = axes.to_vec();
368 sorted.sort_by(|&lhs, &rhs| strides[lhs].cmp(&strides[rhs]).then(lhs.cmp(&rhs)));
369 sorted
370}