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. `m` bounds the row count but not a
115    // row's length, which the only caller builds as `n` entries per row. Neither
116    // mismatch indexes past either side: a short row contributes the orders it has
117    // and nothing to the remaining dimensions, where the index-based loop this
118    // replaced (`for i in 0..n { index_orders[k][i] }`) panicked on the first
119    // missing entry; a row longer than `n` has its suffix ignored, as before.
120    // Measured (`dims = [2, 2]`, strides `[1, 2], [1, 2], [0]`): orders
121    // `[[1, 2], [1, 2], [1]]` -> `[32, 3]` (short), `[[1, 2], [1, 2], [1, 2, 1]]`
122    // -> `[32, 4]` (long, suffix ignored).
123    for row in &index_orders[1..m] {
124        for (i, slot) in importance.iter_mut().enumerate() {
125            if let Some(&order) = row.get(i) {
126                *slot += 1u64 << (g * (n - order) as u64);
127            }
128        }
129    }
130
131    // Zero importance for size-1 dimensions (put them at the back)
132    for i in 0..n {
133        if dims[i] <= 1 {
134            importance[i] = 0;
135        }
136    }
137
138    importance
139}
140
141/// Get the permutation that sorts by importance (descending).
142#[cfg(test)]
143pub fn sort_by_importance(importance: &[u64]) -> Vec<usize> {
144    let mut indices: Vec<usize> = (0..importance.len()).collect();
145    indices.sort_by(|&a, &b| importance[b].cmp(&importance[a]));
146    indices
147}
148
149/// Compute the minimum stride cost for each dimension.
150#[cfg(test)]
151pub fn compute_costs<S: AsRef<[isize]>>(all_strides: &[S]) -> Vec<isize> {
152    if all_strides.is_empty() {
153        return vec![];
154    }
155
156    let n = all_strides[0].as_ref().len();
157    let mut costs = vec![isize::MAX; n];
158
159    for strides in all_strides {
160        let strides = strides.as_ref();
161        for i in 0..n {
162            costs[i] = costs[i].min(strides[i].checked_abs().unwrap_or(isize::MAX));
163        }
164    }
165
166    // Transform: zero -> 1, nonzero -> 2*abs. Costs only rank axes, so a
167    // stride beyond isize::MAX / 2 (reachable with zero-sized elements)
168    // saturates instead of overflowing.
169    for cost in &mut costs {
170        if *cost == 0 {
171            *cost = 1;
172        } else {
173            *cost = cost.saturating_mul(2);
174        }
175    }
176
177    costs
178}
179
180/// Validated result of fusing adjacent dimensions that are contiguous in
181/// both source and destination layouts.
182#[derive(Clone, Debug, PartialEq, Eq)]
183pub struct BilateralFusionPlan {
184    /// Fused non-trivial dimensions in iteration order.
185    pub dims: FusionDimVec,
186    /// Source strides corresponding to [`Self::dims`].
187    pub src_strides: FusionStrideVec,
188    /// Destination strides corresponding to [`Self::dims`].
189    pub dst_strides: FusionStrideVec,
190}
191
192/// Failure while validating or constructing a bilateral fusion plan.
193#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
194pub enum FusionPlanError {
195    /// Shape and stride ranks do not agree.
196    #[error(
197        "metadata lengths differ: dims={dims}, src_strides={src_strides}, \
198         dst_strides={dst_strides}"
199    )]
200    LengthMismatch {
201        /// Number of dimensions.
202        dims: usize,
203        /// Number of source strides.
204        src_strides: usize,
205        /// Number of destination strides.
206        dst_strides: usize,
207    },
208    /// A dimension cannot be represented for stride arithmetic, or a fused
209    /// dimension product exceeds `usize`.
210    #[error("fused dimension product overflows usize")]
211    DimensionOverflow,
212}
213
214/// Plan bilateral dimension fusion for source and destination layouts.
215///
216/// Size-one axes are removed. Two remaining adjacent axes are fused only when
217/// each layout is affine-contiguous across the boundary. Negative strides are
218/// supported; stride multiplication overflow simply prevents fusion.
219///
220/// # Errors
221///
222/// Returns [`FusionPlanError::LengthMismatch`] when the metadata ranks differ,
223/// and [`FusionPlanError::DimensionOverflow`] when a dimension cannot
224/// participate safely in `isize` stride arithmetic or a fused extent
225/// overflows `usize`.
226pub fn plan_bilateral_fusion(
227    dims: &[usize],
228    src_strides: &[isize],
229    dst_strides: &[isize],
230) -> Result<BilateralFusionPlan, FusionPlanError> {
231    if dims.len() != src_strides.len() || dims.len() != dst_strides.len() {
232        return Err(FusionPlanError::LengthMismatch {
233            dims: dims.len(),
234            src_strides: src_strides.len(),
235            dst_strides: dst_strides.len(),
236        });
237    }
238
239    let mut plan = BilateralFusionPlan {
240        dims: FusionDimVec::with_capacity(dims.len()),
241        src_strides: FusionStrideVec::with_capacity(dims.len()),
242        dst_strides: FusionStrideVec::with_capacity(dims.len()),
243    };
244
245    for ((&dim, &src_stride), &dst_stride) in dims.iter().zip(src_strides).zip(dst_strides) {
246        if dim == 1 {
247            continue;
248        }
249        isize::try_from(dim).map_err(|_| FusionPlanError::DimensionOverflow)?;
250
251        if let Some(last) = plan.dims.len().checked_sub(1) {
252            let current_dim =
253                isize::try_from(plan.dims[last]).map_err(|_| FusionPlanError::DimensionOverflow)?;
254            let src_contiguous = plan.src_strides[last]
255                .checked_mul(current_dim)
256                .is_some_and(|expected| src_stride == expected);
257            let dst_contiguous = plan.dst_strides[last]
258                .checked_mul(current_dim)
259                .is_some_and(|expected| dst_stride == expected);
260
261            if src_contiguous && dst_contiguous {
262                plan.dims[last] = plan.dims[last]
263                    .checked_mul(dim)
264                    .ok_or(FusionPlanError::DimensionOverflow)?;
265                continue;
266            }
267        }
268
269        plan.dims.push(dim);
270        plan.src_strides.push(src_stride);
271        plan.dst_strides.push(dst_stride);
272    }
273
274    Ok(plan)
275}
276
277#[cfg(test)]
278fn fuse_dims_bilateral(
279    dims: &[usize],
280    src_strides: &[isize],
281    dst_strides: &[isize],
282) -> (FusionDimVec, FusionStrideVec, FusionStrideVec) {
283    let plan = plan_bilateral_fusion(dims, src_strides, dst_strides).unwrap();
284    (plan.dims, plan.src_strides, plan.dst_strides)
285}
286
287#[cfg(test)]
288mod tests;