Skip to main content

strided_view/
injectivity.rs

1//! Output-layout injectivity validation.
2//!
3//! Kernel families that write through a strided destination must prove that
4//! distinct logical positions map to distinct memory offsets. This is the one
5//! owner of that analysis: `strided-basic` re-exports it for its kernel
6//! families and `strided-perm` calls it before its unsafe permutation loops.
7//!
8//! The common case is validated without allocating: axes are reduced in
9//! ascending `|stride|` order and only the interleaved block of small-stride
10//! axes is compared pairwise. A two-axis block is settled exactly by a gcd
11//! argument regardless of size. A larger block above the pairwise budget
12//! cannot be settled without enumeration, so it spills to the exact
13//! (allocating) check. All three paths conservatively reject metadata whose
14//! spans do not fit the analysis, which keeps the entry points panic-free.
15
16/// Decide whether distinct logical positions map to distinct offsets.
17///
18/// Runs the allocation-free proof first (separated axes, the two-axis gcd
19/// argument, then the pairwise budget) and falls back to the exact enumeration
20/// only when the interleaved block exceeds the pairwise budget. This accepts
21/// every injective layout the exact check can settle, including the
22/// interleaved `[3, 1500]` / `[2000, 3]` case, without allocating for the
23/// common separated, small-block, or two-axis layouts.
24///
25/// # Examples
26///
27/// ```
28/// use strided_view::is_injective_layout;
29/// assert!(is_injective_layout(&[2, 3], &[1, 2]));
30/// assert!(!is_injective_layout(&[2, 3], &[0, 1]));
31/// // Interleaved but injective: 2000 = 2 (mod 3) keeps the rows apart.
32/// assert!(is_injective_layout(&[3, 1500], &[2000, 3]));
33/// ```
34pub fn is_injective_layout(dims: &[usize], strides: &[isize]) -> bool {
35    is_injective_layout_without_alloc(dims, strides) || is_injective_layout_exact(dims, strides)
36}
37
38/// Allocation-free injectivity check.
39///
40/// Exact for a two-axis interleaved block (any size) and for interleaved
41/// blocks of at most `PAIRWISE_BLOCK_BUDGET` logical elements; larger blocks
42/// with three or more axes are conservatively rejected (callers that must keep
43/// every injective layout use [`is_injective_layout`] instead, which spills to
44/// the exact enumeration). Exposed for the kernel adapters that validate a
45/// destination on a per-call path.
46#[doc(hidden)]
47pub fn is_injective_layout_without_alloc(dims: &[usize], strides: &[isize]) -> bool {
48    let Some(total) = validate_injective_layout_inputs(dims, strides) else {
49        return false;
50    };
51    if total <= 1 {
52        return true;
53    }
54    match interleaved_block(dims, strides) {
55        None => false,
56        Some(None) => true,
57        Some(Some(block)) => {
58            if !block.may_be_injective() {
59                // Pigeonhole: more elements than reachable offsets always alias.
60                return false;
61            }
62            if let Some(injective) = two_axis_block_is_injective(dims, strides, &block) {
63                return injective;
64            }
65            block.total <= PAIRWISE_BLOCK_BUDGET as u128
66                && block_offsets_unique_pairwise(dims, strides, &block)
67        }
68    }
69}
70
71/// Exact injectivity check used when the pairwise budget is exceeded.
72fn is_injective_layout_exact(dims: &[usize], strides: &[isize]) -> bool {
73    let Some(total) = validate_injective_layout_inputs(dims, strides) else {
74        return false;
75    };
76    if total <= 1 {
77        return true;
78    }
79    match interleaved_block(dims, strides) {
80        None => false,
81        Some(None) => true,
82        Some(Some(block)) => {
83            if !block.may_be_injective() {
84                // Pigeonhole: more elements than reachable offsets always alias.
85                return false;
86            }
87            if let Some(injective) = two_axis_block_is_injective(dims, strides, &block) {
88                return injective;
89            }
90            block.total <= EXACT_BLOCK_BUDGET as u128 && block_offsets_unique(dims, strides, &block)
91        }
92    }
93}
94
95/// Largest interleaved block enumerated exactly by the spill path.
96///
97/// INVARIANT: bounds the auxiliary memory of the exact check. The block is
98/// visited once in O(block) time; its seen-set is a bitmap over the block's
99/// offset span when that span is at most 64 offsets per element, otherwise a
100/// sorted offset list, so it never exceeds 8 bytes per block element
101/// (128 MiB at this bound). Separated axes never count toward the budget.
102const EXACT_BLOCK_BUDGET: usize = 1 << 24;
103
104/// Largest interleaved block compared pairwise by the allocation-free check.
105///
106/// INVARIANT: keeps the O(block^2) pairwise comparison bounded; larger blocks
107/// use the exact spill path. Separated axes never count toward the budget.
108const PAIRWISE_BLOCK_BUDGET: usize = 4096;
109
110/// The smallest-stride axes that must be checked by enumeration.
111struct InterleavedBlock {
112    last_key: (u128, usize),
113    /// Product of the block's extents.
114    total: u128,
115    /// Largest offset reachable inside the block after normalising strides to
116    /// their absolute values (the smallest is zero).
117    span: u128,
118}
119
120impl InterleavedBlock {
121    fn contains(&self, axis: usize, dim: usize, stride: isize) -> bool {
122        dim > 1 && (stride.unsigned_abs() as u128, axis) <= self.last_key
123    }
124
125    /// Pigeonhole: more elements than reachable offsets always alias.
126    fn may_be_injective(&self) -> bool {
127        self.total <= self.span + 1
128    }
129}
130
131/// Exact, allocation-free injectivity decision for a two-axis interleaved
132/// block.
133///
134/// For strides `s1 <= s2` and extents `d1`, `d2`, the sums `i*s1 + j*s2` are
135/// all distinct iff `s2/g >= d1` or `s1/g >= d2`, where `g = gcd(s1, s2)`: a
136/// non-zero `a*s1 + b*s2 = 0` must have `|a| >= s2/g` and `|b| >= s1/g`, so a
137/// collision exists exactly when both bounds fit inside the extents. The
138/// result is exact, so it is decidable without enumeration even for a block
139/// far above the enumeration budget. Returns `None` when the block does not
140/// hold exactly two axes.
141fn two_axis_block_is_injective(
142    dims: &[usize],
143    strides: &[isize],
144    block: &InterleavedBlock,
145) -> Option<bool> {
146    let mut axes = [(0usize, 0usize); 2]; // (extent, |stride|)
147    let mut count = 0;
148    for (axis, (&dim, &stride)) in dims.iter().zip(strides.iter()).enumerate() {
149        if !block.contains(axis, dim, stride) {
150            continue;
151        }
152        if count == 2 {
153            return None;
154        }
155        axes[count] = (dim, stride.unsigned_abs());
156        count += 1;
157    }
158    if count != 2 {
159        return None;
160    }
161    // Order by stride so the pairing of the gcd bounds matches the proof.
162    let (extent_lo, stride_lo, extent_hi, stride_hi) = if axes[0].1 <= axes[1].1 {
163        (
164            axes[0].0 as u128,
165            axes[0].1 as u128,
166            axes[1].0 as u128,
167            axes[1].1 as u128,
168        )
169    } else {
170        (
171            axes[1].0 as u128,
172            axes[1].1 as u128,
173            axes[0].0 as u128,
174            axes[0].1 as u128,
175        )
176    };
177    let g = gcd(stride_lo, stride_hi);
178    debug_assert!(g > 0, "stride zero on a non-singleton axis was rejected");
179    Some(stride_hi / g >= extent_lo || stride_lo / g >= extent_hi)
180}
181
182/// Greatest common divisor of two non-negative integers.
183fn gcd(mut a: u128, mut b: u128) -> u128 {
184    while b != 0 {
185        let remainder = a % b;
186        a = b;
187        b = remainder;
188    }
189    a
190}
191
192/// Find the interleaved block of a layout.
193///
194/// Returns `None` when a stride or span cannot be represented, `Some(None)`
195/// when every axis is separated (so the layout is injective), and the block
196/// otherwise. Negating a stride only reflects the coordinate along that axis
197/// and shifts all offsets by a constant, so the analysis uses `|stride|`.
198fn interleaved_block(dims: &[usize], strides: &[isize]) -> Option<Option<InterleavedBlock>> {
199    let mut covered_span = 0u128;
200    let mut covered_total = 1u128;
201    let mut previous_key = None;
202    let mut block = None;
203    let active_axes = dims.iter().filter(|&&dim| dim > 1).count();
204    for _ in 0..active_axes {
205        let mut next = None;
206        for (axis, (&dim, &stride)) in dims.iter().zip(strides.iter()).enumerate() {
207            if dim <= 1 {
208                continue;
209            }
210            let key = (stride.checked_abs()? as u128, axis);
211            if previous_key.is_some_and(|previous| key <= previous) {
212                continue;
213            }
214            if next.is_none_or(|(best, _)| key < best) {
215                next = Some((key, dim as u128));
216            }
217        }
218        let ((stride, axis), dim) = next?;
219        let separated = stride > covered_span;
220        covered_span = stride
221            .checked_mul(dim - 1)
222            .and_then(|span| covered_span.checked_add(span))?;
223        covered_total = covered_total.checked_mul(dim)?;
224        if !separated {
225            block = Some(InterleavedBlock {
226                last_key: (stride, axis),
227                total: covered_total,
228                span: covered_span,
229            });
230        }
231        previous_key = Some((stride, axis));
232    }
233    Some(block)
234}
235
236/// Enumerate the block's offsets once and report whether they are distinct.
237fn block_offsets_unique(dims: &[usize], strides: &[isize], block: &InterleavedBlock) -> bool {
238    let axes: Vec<(usize, usize)> = dims
239        .iter()
240        .zip(strides.iter())
241        .enumerate()
242        .filter(|&(axis, (&dim, &stride))| block.contains(axis, dim, stride))
243        .map(|(_, (&dim, &stride))| (dim, stride.unsigned_abs()))
244        .collect();
245    let (Ok(total), Ok(span)) = (usize::try_from(block.total), usize::try_from(block.span)) else {
246        return false;
247    };
248
249    // Every visited offset and every partial axis span lies within
250    // [0, span], which fits `usize` (checked above), so the incremental
251    // updates below cannot overflow.
252    let mut indices = vec![0usize; axes.len()];
253    let mut offset = 0usize;
254    let mut advance = |offset: &mut usize| {
255        for (index, &(dim, stride)) in indices.iter_mut().zip(axes.iter()) {
256            if *index + 1 < dim {
257                *index += 1;
258                *offset += stride;
259                return;
260            }
261            *offset -= stride * (dim - 1);
262            *index = 0;
263        }
264    };
265
266    if block.span / 64 <= block.total {
267        let mut seen = vec![0u64; span / 64 + 1];
268        for _ in 0..total {
269            let (word, bit) = (offset / 64, 1u64 << (offset % 64));
270            if seen[word] & bit != 0 {
271                return false;
272            }
273            seen[word] |= bit;
274            advance(&mut offset);
275        }
276        true
277    } else {
278        let mut offsets = Vec::with_capacity(total);
279        for _ in 0..total {
280            offsets.push(offset);
281            advance(&mut offset);
282        }
283        offsets.sort_unstable();
284        offsets.windows(2).all(|pair| pair[0] != pair[1])
285    }
286}
287
288fn block_offset_for_linear_index(
289    dims: &[usize],
290    strides: &[isize],
291    block: &InterleavedBlock,
292    mut linear: usize,
293) -> u128 {
294    let mut offset = 0u128;
295    for (axis, (&dim, &stride)) in dims.iter().zip(strides.iter()).enumerate() {
296        if !block.contains(axis, dim, stride) {
297            continue;
298        }
299        offset += stride.unsigned_abs() as u128 * (linear % dim) as u128;
300        linear /= dim;
301    }
302    offset
303}
304
305fn block_offsets_unique_pairwise(
306    dims: &[usize],
307    strides: &[isize],
308    block: &InterleavedBlock,
309) -> bool {
310    let Ok(total) = usize::try_from(block.total) else {
311        return false;
312    };
313    for lhs in 0..total {
314        let lhs_offset = block_offset_for_linear_index(dims, strides, block, lhs);
315        for rhs in (lhs + 1)..total {
316            if block_offset_for_linear_index(dims, strides, block, rhs) == lhs_offset {
317                return false;
318            }
319        }
320    }
321    true
322}
323
324fn validate_injective_layout_inputs(dims: &[usize], strides: &[isize]) -> Option<usize> {
325    if dims.len() != strides.len() {
326        return None;
327    }
328
329    // A zero-sized layout is trivially injective even when the product of its
330    // other extents would overflow.
331    let total = total_len(dims)?;
332    if total <= 1 {
333        return Some(total);
334    }
335    if dims
336        .iter()
337        .zip(strides.iter())
338        .any(|(&dim, &stride)| dim > 1 && stride == 0)
339    {
340        return None;
341    }
342
343    let mut min_offset = 0isize;
344    let mut max_offset = 0isize;
345    for (&dim, &stride) in dims.iter().zip(strides.iter()) {
346        if dim <= 1 {
347            continue;
348        }
349        let extent = isize::try_from(dim - 1).ok()?;
350        let span = stride.checked_mul(extent)?;
351        if span >= 0 {
352            max_offset = max_offset.checked_add(span)?;
353        } else {
354            min_offset = min_offset.checked_add(span)?;
355        }
356    }
357    Some(total)
358}
359
360/// Checked product of extents; `None` on overflow.
361fn total_len(dims: &[usize]) -> Option<usize> {
362    // A zero extent wins before the product, matching the kernel helper: a
363    // zero-sized layout is trivially injective even when its other extents
364    // would overflow.
365    if dims.contains(&0) {
366        return Some(0);
367    }
368    dims.iter()
369        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn disjoint_axes_are_injective() {
378        assert!(is_injective_layout(&[2, 3], &[1, 2]));
379        assert!(is_injective_layout(&[2, 3], &[3, 1]));
380        assert!(is_injective_layout(&[1, 5], &[0, 1]));
381        assert!(is_injective_layout(&[], &[]));
382    }
383
384    #[test]
385    fn overlapping_axes_are_rejected() {
386        assert!(!is_injective_layout(&[2, 3], &[0, 1]));
387        assert!(!is_injective_layout(&[2, 2], &[1, 1]));
388        assert!(!is_injective_layout(&[256, 256], &[256, 256]));
389        // Stride 0 on a non-singleton axis collapses offsets.
390        assert!(!is_injective_layout(&[3, 3], &[1, 0]));
391    }
392
393    #[test]
394    fn three_axis_interleaved_injective_layout_needs_the_exact_spill_path() {
395        // Three-axis interleaved block above the pairwise budget; no two-axis
396        // proof applies, so only the allocating exact enumeration accepts it.
397        assert!(is_injective_layout_without_alloc(&[2, 2, 2000], &[4, 5, 6]) == false);
398        assert!(is_injective_layout(&[2, 2, 2000], &[4, 5, 6]));
399        // The two-axis gcd proof now settles the old 4500-element case
400        // allocation-free.
401        assert!(is_injective_layout_without_alloc(&[3, 1500], &[2000, 3]));
402        assert!(is_injective_layout(&[1500, 3], &[3, 2000]));
403        assert!(!is_injective_layout(&[3, 1500], &[2000, 2]));
404    }
405
406    #[test]
407    fn large_two_axis_injective_layout_is_decided_without_enumeration() {
408        // Post-review regression: 6_000_001 = 1 (mod 3), so the rows stay
409        // distinct. The block has 18M elements, above the exact enumeration
410        // budget, and must be settled by the allocation-free gcd proof instead
411        // of being rejected as overlapping.
412        assert!(is_injective_layout(&[3, 6_000_000], &[6_000_001, 3]));
413        assert!(is_injective_layout_without_alloc(
414            &[3, 6_000_000],
415            &[6_000_001, 3]
416        ));
417        // The gcd proof is exact: with a stride-3 axis of extent 6M and a
418        // 6_000_003 axis of extent 3, `2000001 * 3 == 6000003`, so the layout
419        // aliases and is rejected without enumeration.
420        assert!(!is_injective_layout(&[3, 6_000_000], &[6_000_003, 3]));
421        assert!(!is_injective_layout_without_alloc(
422            &[3, 6_000_000],
423            &[6_000_003, 3]
424        ));
425    }
426
427    #[test]
428    fn checked_total_len_detects_overflow() {
429        assert_eq!(total_len(&[usize::MAX, 2]), None);
430        assert_eq!(total_len(&[3, 4]), Some(12));
431        // A zero extent wins before the overflowing product.
432        assert_eq!(total_len(&[usize::MAX, 2, 0]), Some(0));
433    }
434}