Skip to main content

strided_perm/
fuse.rs

1//! Dimension fusion logic ported from Strided.jl/src/mapreduce.jl
2//!
3//! This module implements the core dimension fusion algorithm that merges
4//! contiguous dimensions to reduce iteration complexity.
5
6use strided_view::INLINE_RANK;
7use tinyvec::TinyVec;
8
9/// Fused axis extents, inline up to [`INLINE_RANK`].
10///
11/// Part of the public [`BilateralFusionPlan`]; a rank above the inline
12/// capacity spills to the heap instead of erroring.
13pub type FusionDimVec = TinyVec<[usize; INLINE_RANK]>;
14
15/// Fused source or destination strides, inline up to [`INLINE_RANK`].
16pub type FusionStrideVec = TinyVec<[isize; INLINE_RANK]>;
17
18/// Fuse contiguous dimensions across multiple arrays.
19///
20/// This function fuses subsequent dimensions that are contiguous in memory
21/// for all arrays. If `strides[k][i] == dims[i-1] * strides[k][i-1]` for all k,
22/// dimensions i-1 and i can be merged.
23#[cfg(test)]
24pub fn fuse_dims(dims: &[usize], all_strides: &[&[isize]]) -> Vec<usize> {
25    let n = dims.len();
26    if n <= 1 || all_strides.is_empty() {
27        return dims.to_vec();
28    }
29
30    let mut result = dims.to_vec();
31
32    // Work from the end towards the beginning (Julia: for i in length(dims):-1:2)
33    for i in (1..n).rev() {
34        let mut can_merge = true;
35
36        // Check all arrays for contiguity
37        for strides in all_strides {
38            // s[i] should equal dims[i-1] * s[i-1] for fusion
39            let expected = result[i - 1] as isize * strides[i - 1];
40            if strides[i] != expected {
41                can_merge = false;
42                break;
43            }
44        }
45
46        if can_merge {
47            // Fuse dimensions: merge dimension i into i-1
48            result[i - 1] *= result[i];
49            result[i] = 1;
50        }
51    }
52
53    result
54}
55
56/// Remove size-1 dimensions from fused dims and all corresponding strides.
57///
58/// After `fuse_dims()`, many dimensions may be 1 (either originally size-1
59/// or merged into a neighbor). These contribute nothing to iteration but
60/// increase loop depth. This function strips them out.
61///
62/// If ALL dimensions are 1 (scalar-like), a single dimension of size 1
63/// is preserved so the kernel has something to iterate over.
64#[cfg(test)]
65pub fn compress_dims(dims: &[usize], all_strides: &[Vec<isize>]) -> (Vec<usize>, Vec<Vec<isize>>) {
66    let kept: Vec<usize> = (0..dims.len()).filter(|&i| dims[i] != 1).collect();
67
68    if kept.is_empty() {
69        // All dims are 1 (or empty). Preserve a single trivial dimension.
70        if dims.is_empty() {
71            return (vec![], all_strides.to_vec());
72        }
73        let new_strides = all_strides.iter().map(|s| vec![s[0]]).collect();
74        return (vec![1], new_strides);
75    }
76
77    let new_dims: Vec<usize> = kept.iter().map(|&i| dims[i]).collect();
78    let new_strides: Vec<Vec<isize>> = all_strides
79        .iter()
80        .map(|s| kept.iter().map(|&i| s[i]).collect())
81        .collect();
82
83    (new_dims, new_strides)
84}
85
86/// Compute the "importance" of each dimension for loop ordering.
87///
88/// This encodes stride order information into importance scores that determine
89/// the optimal iteration order. The output array's strides are weighted 2x.
90#[cfg(test)]
91pub fn compute_importance(
92    dims: &[usize],
93    all_strides: &[&[isize]],
94    index_orders: &[Vec<usize>],
95) -> Vec<u64> {
96    let n = dims.len();
97    let m = all_strides.len();
98
99    if n == 0 || m == 0 {
100        return vec![];
101    }
102
103    // g = ceil(log2(M + 2)) = number of bits needed to encode array count
104    let g = (64 - (m as u64 + 1).leading_zeros()) as u64;
105
106    let mut importance = vec![0u64; n];
107
108    // First array (output) is weighted 2x
109    for i in 0..n {
110        let shift = g * (n - index_orders[0][i]) as u64;
111        importance[i] = 2 * (1u64 << shift);
112    }
113
114    // Add contributions from remaining arrays
115    #[allow(clippy::needless_range_loop)]
116    for k in 1..m {
117        for i in 0..n {
118            let shift = g * (n - index_orders[k][i]) as u64;
119            importance[i] += 1u64 << shift;
120        }
121    }
122
123    // Zero importance for size-1 dimensions (put them at the back)
124    for i in 0..n {
125        if dims[i] <= 1 {
126            importance[i] = 0;
127        }
128    }
129
130    importance
131}
132
133/// Get the permutation that sorts by importance (descending).
134#[cfg(test)]
135pub fn sort_by_importance(importance: &[u64]) -> Vec<usize> {
136    let mut indices: Vec<usize> = (0..importance.len()).collect();
137    indices.sort_by(|&a, &b| importance[b].cmp(&importance[a]));
138    indices
139}
140
141/// Compute the minimum stride cost for each dimension.
142#[cfg(test)]
143pub fn compute_costs<S: AsRef<[isize]>>(all_strides: &[S]) -> Vec<isize> {
144    if all_strides.is_empty() {
145        return vec![];
146    }
147
148    let n = all_strides[0].as_ref().len();
149    let mut costs = vec![isize::MAX; n];
150
151    for strides in all_strides {
152        let strides = strides.as_ref();
153        for i in 0..n {
154            costs[i] = costs[i].min(strides[i].abs());
155        }
156    }
157
158    // Transform: zero -> 1, nonzero -> 2*abs
159    for cost in &mut costs {
160        if *cost == 0 {
161            *cost = 1;
162        } else {
163            *cost *= 2;
164        }
165    }
166
167    costs
168}
169
170/// Validated result of fusing adjacent dimensions that are contiguous in
171/// both source and destination layouts.
172#[derive(Clone, Debug, PartialEq, Eq)]
173pub struct BilateralFusionPlan {
174    /// Fused non-trivial dimensions in iteration order.
175    pub dims: FusionDimVec,
176    /// Source strides corresponding to [`Self::dims`].
177    pub src_strides: FusionStrideVec,
178    /// Destination strides corresponding to [`Self::dims`].
179    pub dst_strides: FusionStrideVec,
180}
181
182/// Failure while validating or constructing a bilateral fusion plan.
183#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
184pub enum FusionPlanError {
185    /// Shape and stride ranks do not agree.
186    #[error(
187        "metadata lengths differ: dims={dims}, src_strides={src_strides}, \
188         dst_strides={dst_strides}"
189    )]
190    LengthMismatch {
191        /// Number of dimensions.
192        dims: usize,
193        /// Number of source strides.
194        src_strides: usize,
195        /// Number of destination strides.
196        dst_strides: usize,
197    },
198    /// A dimension cannot be represented for stride arithmetic, or a fused
199    /// dimension product exceeds `usize`.
200    #[error("fused dimension product overflows usize")]
201    DimensionOverflow,
202}
203
204/// Plan bilateral dimension fusion for source and destination layouts.
205///
206/// Size-one axes are removed. Two remaining adjacent axes are fused only when
207/// each layout is affine-contiguous across the boundary. Negative strides are
208/// supported; stride multiplication overflow simply prevents fusion.
209///
210/// # Errors
211///
212/// Returns [`FusionPlanError::LengthMismatch`] when the metadata ranks differ,
213/// and [`FusionPlanError::DimensionOverflow`] when a dimension cannot
214/// participate safely in `isize` stride arithmetic or a fused extent
215/// overflows `usize`.
216pub fn plan_bilateral_fusion(
217    dims: &[usize],
218    src_strides: &[isize],
219    dst_strides: &[isize],
220) -> Result<BilateralFusionPlan, FusionPlanError> {
221    if dims.len() != src_strides.len() || dims.len() != dst_strides.len() {
222        return Err(FusionPlanError::LengthMismatch {
223            dims: dims.len(),
224            src_strides: src_strides.len(),
225            dst_strides: dst_strides.len(),
226        });
227    }
228
229    let mut plan = BilateralFusionPlan {
230        dims: FusionDimVec::with_capacity(dims.len()),
231        src_strides: FusionStrideVec::with_capacity(dims.len()),
232        dst_strides: FusionStrideVec::with_capacity(dims.len()),
233    };
234
235    for ((&dim, &src_stride), &dst_stride) in dims.iter().zip(src_strides).zip(dst_strides) {
236        if dim == 1 {
237            continue;
238        }
239        isize::try_from(dim).map_err(|_| FusionPlanError::DimensionOverflow)?;
240
241        if let Some(last) = plan.dims.len().checked_sub(1) {
242            let current_dim =
243                isize::try_from(plan.dims[last]).map_err(|_| FusionPlanError::DimensionOverflow)?;
244            let src_contiguous = plan.src_strides[last]
245                .checked_mul(current_dim)
246                .is_some_and(|expected| src_stride == expected);
247            let dst_contiguous = plan.dst_strides[last]
248                .checked_mul(current_dim)
249                .is_some_and(|expected| dst_stride == expected);
250
251            if src_contiguous && dst_contiguous {
252                plan.dims[last] = plan.dims[last]
253                    .checked_mul(dim)
254                    .ok_or(FusionPlanError::DimensionOverflow)?;
255                continue;
256            }
257        }
258
259        plan.dims.push(dim);
260        plan.src_strides.push(src_stride);
261        plan.dst_strides.push(dst_stride);
262    }
263
264    Ok(plan)
265}
266
267#[cfg(test)]
268fn fuse_dims_bilateral(
269    dims: &[usize],
270    src_strides: &[isize],
271    dst_strides: &[isize],
272) -> (FusionDimVec, FusionStrideVec, FusionStrideVec) {
273    let plan = plan_bilateral_fusion(dims, src_strides, dst_strides).unwrap();
274    (plan.dims, plan.src_strides, plan.dst_strides)
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn test_fuse_dims_contiguous() {
283        let dims = [3, 4];
284        let strides1 = [1isize, 3];
285        let strides2 = [1isize, 3];
286        let all_strides: Vec<&[isize]> = vec![&strides1, &strides2];
287        let fused = fuse_dims(&dims, &all_strides);
288        assert_eq!(fused, vec![12, 1]);
289    }
290
291    #[test]
292    fn test_fuse_dims_non_contiguous() {
293        let dims = [3, 4];
294        let strides1 = [1isize, 10];
295        let all_strides: Vec<&[isize]> = vec![&strides1];
296        let fused = fuse_dims(&dims, &all_strides);
297        assert_eq!(fused, vec![3, 4]);
298    }
299
300    #[test]
301    fn test_fuse_dims_bilateral_all_contiguous() {
302        // 24-dim all-size-2 col-major: all contiguous -> fuses to single dim
303        let dims = vec![2, 2, 2, 2];
304        let src_strides = vec![1, 2, 4, 8];
305        let dst_strides = vec![1, 2, 4, 8];
306        let (fd, fs, fds) = fuse_dims_bilateral(&dims, &src_strides, &dst_strides);
307        assert_eq!(&fd[..], &[16]);
308        assert_eq!(&fs[..], &[1]);
309        assert_eq!(&fds[..], &[1]);
310    }
311
312    #[test]
313    fn test_fuse_dims_bilateral_partial() {
314        // src: contiguous 0-1, not 1-2
315        // dst: contiguous 0-1-2
316        let dims = vec![2, 3, 4];
317        let src_strides = vec![1, 2, 100]; // 0-1 contiguous, 1-2 not
318        let dst_strides = vec![1, 2, 6]; // all contiguous
319        let (fd, fs, fds) = fuse_dims_bilateral(&dims, &src_strides, &dst_strides);
320        assert_eq!(&fd[..], &[6, 4]); // first two fuse
321        assert_eq!(&fs[..], &[1, 100]);
322        assert_eq!(&fds[..], &[1, 6]);
323    }
324
325    #[test]
326    fn test_fuse_dims_bilateral_scattered() {
327        // The benchmark case: scattered strides, nothing fuses
328        let dims = vec![2, 2, 2];
329        let src_strides = vec![1, 4194304, 2]; // scattered
330        let dst_strides = vec![1, 2, 4]; // contiguous
331        let (fd, fs, fds) = fuse_dims_bilateral(&dims, &src_strides, &dst_strides);
332        assert_eq!(&fd[..], &[2, 2, 2]); // nothing fuses
333        assert_eq!(&fs[..], &[1, 4194304, 2]);
334        assert_eq!(&fds[..], &[1, 2, 4]);
335    }
336
337    #[test]
338    fn test_compress_dims_removes_fused() {
339        let dims = vec![12usize, 1];
340        let strides = vec![vec![1isize, 3]];
341        let (cd, cs) = compress_dims(&dims, &strides);
342        assert_eq!(cd, vec![12]);
343        assert_eq!(cs, vec![vec![1]]);
344    }
345
346    #[test]
347    fn test_compute_costs() {
348        let strides1 = [1isize, 4, 0];
349        let strides2 = [2isize, 1, 0];
350        let all_strides: Vec<&[isize]> = vec![&strides1, &strides2];
351        let costs = compute_costs(&all_strides);
352        assert_eq!(costs, vec![2, 2, 1]);
353    }
354}