Skip to main content

oxiblas_core/
blocking.rs

1//! Cache-oblivious blocking utilities.
2//!
3//! This module provides utilities for implementing cache-oblivious algorithms,
4//! which achieve near-optimal cache performance without requiring knowledge of
5//! cache sizes.
6//!
7//! # Cache-Oblivious Algorithms
8//!
9//! Cache-oblivious algorithms use recursive divide-and-conquer strategies that
10//! naturally adapt to all levels of the memory hierarchy. The key insight is that
11//! by recursively splitting the problem until the subproblems fit in cache, we get
12//! optimal cache behavior without needing to know the cache size.
13//!
14//! # Block Size Calculation
15//!
16//! For cache-aware algorithms, this module also provides utilities to calculate
17//! optimal block sizes based on:
18//! - Available cache sizes (L1, L2, L3)
19//! - SIMD register widths
20//! - Memory layout (row-major, column-major)
21
22use crate::tuning::{L1_CACHE_SIZE, L2_CACHE_SIZE};
23use core::mem::size_of;
24
25/// Base case threshold for recursive algorithms.
26///
27/// When the problem size drops below this threshold, we switch to
28/// a direct (non-recursive) implementation.
29pub const BASE_CASE_THRESHOLD: usize = 64;
30
31/// Minimum block size for tiled algorithms.
32pub const MIN_BLOCK_SIZE: usize = 16;
33
34/// Maximum block size for tiled algorithms.
35pub const MAX_BLOCK_SIZE: usize = 512;
36
37/// Calculates the optimal block size for GEMM-like operations.
38///
39/// The block size is chosen to maximize data reuse in the L2 cache.
40/// For GEMM with blocks of size M×K and K×N, we want:
41/// `2 * M * K + K * N ≈ L2_CACHE_SIZE`
42///
43/// # Arguments
44/// * `m` - Number of rows in the result
45/// * `n` - Number of columns in the result
46/// * `k` - Inner dimension
47///
48/// # Returns
49/// A tuple `(block_m, block_n, block_k)` of optimal block sizes.
50pub fn gemm_block_sizes<T>(m: usize, n: usize, k: usize) -> (usize, usize, usize) {
51    let elem_size = size_of::<T>();
52
53    // Target: fit 2 input panels + 1 output panel in L2
54    // A panel: block_m × block_k
55    // B panel: block_k × block_n
56    // C panel: block_m × block_n
57    let target_bytes = L2_CACHE_SIZE / 2;
58
59    // Start with a balanced block size
60    let max_block = ((target_bytes / elem_size / 3) as f64).sqrt() as usize;
61    let mut block = max_block.clamp(MIN_BLOCK_SIZE, MAX_BLOCK_SIZE);
62
63    // Align to SIMD-friendly boundaries
64    block = (block / 8) * 8;
65    if block < MIN_BLOCK_SIZE {
66        block = MIN_BLOCK_SIZE;
67    }
68
69    // Adjust for actual dimensions
70    let block_m = block.min(m);
71    let block_n = block.min(n);
72    let block_k = block.min(k);
73
74    (block_m, block_n, block_k)
75}
76
77/// Calculates the optimal block size for triangular solves (TRSM).
78///
79/// For TRSM, we need to balance between:
80/// - Keeping the triangular block in L1 cache
81/// - Processing multiple right-hand side columns
82pub fn trsm_block_size<T>(n: usize, nrhs: usize) -> usize {
83    let elem_size = size_of::<T>();
84
85    // Target: fit triangular block in L1
86    // Triangular block: n² / 2 elements
87    let max_block = ((2 * L1_CACHE_SIZE / elem_size) as f64).sqrt() as usize;
88    let block = max_block.clamp(MIN_BLOCK_SIZE, MAX_BLOCK_SIZE / 2);
89
90    // Align to SIMD-friendly boundaries. MIN_BLOCK_SIZE (16) is a multiple
91    // of 8, so this alignment step can never drop below MIN_BLOCK_SIZE.
92    let block = (block / 8) * 8;
93
94    // Never return a block size larger than either matrix dimension it
95    // will actually operate on: for small `n`/`nrhs` (including 0), the
96    // cache-driven heuristic above is meaningless and must be clamped
97    // down, not floored back up.
98    block.min(n).min(nrhs)
99}
100
101/// Calculates the optimal panel width for factorizations (LU, Cholesky, QR).
102///
103/// The panel width determines how many columns are processed together
104/// before updating the trailing submatrix.
105pub fn factorization_panel_width<T>(n: usize) -> usize {
106    let elem_size = size_of::<T>();
107
108    // For factorization, we want the panel to fit in L2 cache
109    // Panel size: n × panel_width
110    let max_panel = L2_CACHE_SIZE / (elem_size * n.max(1));
111    let panel = max_panel.clamp(16, 128);
112
113    // Align to SIMD boundaries. The clamp lower bound (16) is a multiple
114    // of 4, so this alignment step can never drop below 16.
115    let panel = (panel / 4) * 4;
116
117    // Never return a panel width larger than the matrix dimension itself:
118    // for small `n` (including 0), the cache-driven heuristic above is
119    // meaningless and must be clamped down, not floored back up.
120    panel.min(n)
121}
122
123/// Recursive block range for cache-oblivious algorithms.
124///
125/// This structure represents a range that can be recursively split
126/// for divide-and-conquer algorithms.
127#[derive(Debug, Clone, Copy)]
128pub struct BlockRange {
129    /// Start index (inclusive)
130    pub start: usize,
131    /// End index (exclusive)
132    pub end: usize,
133}
134
135impl BlockRange {
136    /// Creates a new block range.
137    #[inline]
138    pub const fn new(start: usize, end: usize) -> Self {
139        BlockRange { start, end }
140    }
141
142    /// Creates a range from 0 to n.
143    #[inline]
144    pub const fn from_len(n: usize) -> Self {
145        BlockRange { start: 0, end: n }
146    }
147
148    /// Returns the length of this range.
149    #[inline]
150    pub const fn len(&self) -> usize {
151        self.end.saturating_sub(self.start)
152    }
153
154    /// Returns true if this range is empty.
155    #[inline]
156    pub const fn is_empty(&self) -> bool {
157        self.start >= self.end
158    }
159
160    /// Returns true if this range is a base case (should not be split further).
161    #[inline]
162    pub fn is_base_case(&self, threshold: usize) -> bool {
163        self.len() <= threshold
164    }
165
166    /// Splits this range in half.
167    ///
168    /// Returns `(left_half, right_half)`.
169    #[inline]
170    pub fn split(&self) -> (Self, Self) {
171        let mid = self.start + self.len() / 2;
172        (
173            BlockRange::new(self.start, mid),
174            BlockRange::new(mid, self.end),
175        )
176    }
177
178    /// Splits at a specific point.
179    #[inline]
180    pub fn split_at(&self, point: usize) -> (Self, Self) {
181        let split = (self.start + point).min(self.end);
182        (
183            BlockRange::new(self.start, split),
184            BlockRange::new(split, self.end),
185        )
186    }
187}
188
189/// Task for cache-oblivious recursive algorithm.
190///
191/// This represents a subproblem in a recursive decomposition.
192#[derive(Debug, Clone, Copy)]
193pub struct RecursiveTask {
194    /// Row range
195    pub rows: BlockRange,
196    /// Column range
197    pub cols: BlockRange,
198}
199
200impl RecursiveTask {
201    /// Creates a new recursive task.
202    #[inline]
203    pub const fn new(rows: BlockRange, cols: BlockRange) -> Self {
204        RecursiveTask { rows, cols }
205    }
206
207    /// Creates a task for an m×n matrix.
208    #[inline]
209    pub const fn from_dims(m: usize, n: usize) -> Self {
210        RecursiveTask {
211            rows: BlockRange::from_len(m),
212            cols: BlockRange::from_len(n),
213        }
214    }
215
216    /// Returns the number of elements in this task.
217    #[inline]
218    pub fn size(&self) -> usize {
219        self.rows.len() * self.cols.len()
220    }
221
222    /// Returns true if this is a base case.
223    #[inline]
224    pub fn is_base_case(&self, threshold: usize) -> bool {
225        self.rows.len() <= threshold && self.cols.len() <= threshold
226    }
227
228    /// Splits along the larger dimension.
229    ///
230    /// Returns two subtasks by splitting the larger dimension in half.
231    pub fn split(&self) -> (Self, Self) {
232        if self.rows.len() >= self.cols.len() {
233            // Split rows
234            let (r1, r2) = self.rows.split();
235            (
236                RecursiveTask::new(r1, self.cols),
237                RecursiveTask::new(r2, self.cols),
238            )
239        } else {
240            // Split columns
241            let (c1, c2) = self.cols.split();
242            (
243                RecursiveTask::new(self.rows, c1),
244                RecursiveTask::new(self.rows, c2),
245            )
246        }
247    }
248
249    /// Quadrant decomposition for 2D recursive algorithms.
250    ///
251    /// Returns `(top_left, top_right, bottom_left, bottom_right)`.
252    pub fn quadrants(&self) -> (Self, Self, Self, Self) {
253        let (r1, r2) = self.rows.split();
254        let (c1, c2) = self.cols.split();
255
256        (
257            RecursiveTask::new(r1, c1), // top-left
258            RecursiveTask::new(r1, c2), // top-right
259            RecursiveTask::new(r2, c1), // bottom-left
260            RecursiveTask::new(r2, c2), // bottom-right
261        )
262    }
263}
264
265/// Visitor pattern for cache-oblivious matrix traversal.
266///
267/// Implement this trait to process matrix blocks in a cache-efficient order.
268pub trait BlockVisitor {
269    /// The error type for visit operations.
270    type Error;
271
272    /// Visits a matrix block.
273    ///
274    /// # Arguments
275    /// * `row_start`, `row_end` - Row range (exclusive end)
276    /// * `col_start`, `col_end` - Column range (exclusive end)
277    fn visit_block(
278        &mut self,
279        row_start: usize,
280        row_end: usize,
281        col_start: usize,
282        col_end: usize,
283    ) -> Result<(), Self::Error>;
284}
285
286/// Performs a cache-oblivious traversal of a matrix.
287///
288/// This recursively divides the matrix into quadrants until reaching
289/// the base case threshold, then visits each block.
290pub fn cache_oblivious_traverse<V: BlockVisitor>(
291    visitor: &mut V,
292    task: RecursiveTask,
293    threshold: usize,
294) -> Result<(), V::Error> {
295    if task.is_base_case(threshold) {
296        // Base case: visit this block directly
297        visitor.visit_block(
298            task.rows.start,
299            task.rows.end,
300            task.cols.start,
301            task.cols.end,
302        )
303    } else {
304        // Recursive case: split and process
305        let (t1, t2) = task.split();
306        cache_oblivious_traverse(visitor, t1, threshold)?;
307        cache_oblivious_traverse(visitor, t2, threshold)
308    }
309}
310
311/// Morton (Z-order) curve index calculation.
312///
313/// Morton ordering provides good cache locality for 2D data by
314/// interleaving the bits of x and y coordinates.
315#[inline]
316pub fn morton_index(x: u32, y: u32) -> u64 {
317    fn expand_bits(v: u32) -> u64 {
318        let mut v = v as u64;
319        v = (v | (v << 16)) & 0x0000_FFFF_0000_FFFF;
320        v = (v | (v << 8)) & 0x00FF_00FF_00FF_00FF;
321        v = (v | (v << 4)) & 0x0F0F_0F0F_0F0F_0F0F;
322        v = (v | (v << 2)) & 0x3333_3333_3333_3333;
323        v = (v | (v << 1)) & 0x5555_5555_5555_5555;
324        v
325    }
326    expand_bits(x) | (expand_bits(y) << 1)
327}
328
329/// Inverse Morton index: extracts (x, y) from a Morton index.
330#[inline]
331pub fn morton_decode(z: u64) -> (u32, u32) {
332    fn compact_bits(mut v: u64) -> u32 {
333        v &= 0x5555_5555_5555_5555;
334        v = (v | (v >> 1)) & 0x3333_3333_3333_3333;
335        v = (v | (v >> 2)) & 0x0F0F_0F0F_0F0F_0F0F;
336        v = (v | (v >> 4)) & 0x00FF_00FF_00FF_00FF;
337        v = (v | (v >> 8)) & 0x0000_FFFF_0000_FFFF;
338        v = (v | (v >> 16)) & 0x0000_0000_FFFF_FFFF;
339        v as u32
340    }
341    (compact_bits(z), compact_bits(z >> 1))
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_gemm_block_sizes() {
350        let (bm, bn, bk) = gemm_block_sizes::<f64>(1024, 1024, 1024);
351
352        // Block sizes should be reasonable
353        assert!(bm >= MIN_BLOCK_SIZE);
354        assert!(bn >= MIN_BLOCK_SIZE);
355        assert!(bk >= MIN_BLOCK_SIZE);
356        assert!(bm <= MAX_BLOCK_SIZE);
357        assert!(bn <= MAX_BLOCK_SIZE);
358        assert!(bk <= MAX_BLOCK_SIZE);
359
360        // Should be divisible by 8
361        assert_eq!(bm % 8, 0);
362    }
363
364    #[test]
365    fn test_trsm_block_size_clamped_to_matrix_extent() {
366        // For any n/nrhs at or below the cache-driven heuristic, the
367        // returned block size must never exceed either matrix dimension
368        // (this is the regression case: the old code re-floored the
369        // value with `.max(MIN_BLOCK_SIZE)` *after* clamping to n/nrhs,
370        // which could push the result back above a small n or nrhs).
371        for &n in &[0usize, 1, 4, 8, 15, 16] {
372            for &nrhs in &[0usize, 1, 4, 8] {
373                let block = trsm_block_size::<f64>(n, nrhs);
374                assert!(block <= n, "block {block} exceeds n={n} (nrhs={nrhs})");
375                assert!(block <= nrhs, "block {block} exceeds nrhs={nrhs} (n={n})");
376            }
377        }
378
379        // A zero-sized dimension must yield an exact zero block size, not
380        // MIN_BLOCK_SIZE.
381        assert_eq!(trsm_block_size::<f64>(0, 64), 0);
382        assert_eq!(trsm_block_size::<f64>(64, 0), 0);
383
384        // For f64 the unclamped heuristic value is 88 (derived from
385        // L1_CACHE_SIZE and MIN/MAX_BLOCK_SIZE): n = 87 is still limited
386        // by n itself, n = 88 lands exactly on the heuristic, and n = 89
387        // is no longer limited by n at all. This exercises the tier
388        // boundary where clamping stops being the binding constraint.
389        assert_eq!(trsm_block_size::<f64>(87, 4096), 87);
390        assert_eq!(trsm_block_size::<f64>(88, 4096), 88);
391        assert_eq!(trsm_block_size::<f64>(89, 4096), 88);
392
393        // Once both dimensions are large, the heuristic value (independent
394        // of n/nrhs) governs and stays within its documented bounds.
395        let unclamped = trsm_block_size::<f64>(4096, 4096);
396        assert!(unclamped >= MIN_BLOCK_SIZE);
397        assert!(unclamped <= MAX_BLOCK_SIZE / 2);
398    }
399
400    #[test]
401    fn test_factorization_panel_width_clamped_to_matrix_extent() {
402        // Same regression as trsm_block_size: the panel width must never
403        // exceed n itself, even though the heuristic's own floor is 16.
404        for &n in &[0usize, 1, 4, 8, 15, 16] {
405            let panel = factorization_panel_width::<f64>(n);
406            assert!(panel <= n, "panel {panel} exceeds n={n}");
407        }
408
409        assert_eq!(factorization_panel_width::<f64>(0), 0);
410        assert_eq!(factorization_panel_width::<f64>(1), 1);
411        assert_eq!(factorization_panel_width::<f64>(4), 4);
412
413        // Tier boundary: for f64, `L2_CACHE_SIZE / (elem_size * n)` sits
414        // at exactly 128 for n = 256, then drops (and gets aligned down to
415        // a multiple of 4) once n = 257 pushes it below 128.
416        assert_eq!(factorization_panel_width::<f64>(256), 128);
417        assert_eq!(factorization_panel_width::<f64>(257), 124);
418    }
419
420    #[test]
421    fn test_block_range() {
422        let range = BlockRange::new(0, 100);
423        assert_eq!(range.len(), 100);
424
425        let (left, right) = range.split();
426        assert_eq!(left.start, 0);
427        assert_eq!(left.end, 50);
428        assert_eq!(right.start, 50);
429        assert_eq!(right.end, 100);
430
431        assert!(BlockRange::new(0, 32).is_base_case(64));
432        assert!(!BlockRange::new(0, 100).is_base_case(64));
433    }
434
435    #[test]
436    fn test_recursive_task() {
437        let task = RecursiveTask::from_dims(100, 200);
438        assert_eq!(task.size(), 20000);
439
440        // Should split along columns (larger dimension)
441        let (t1, t2) = task.split();
442        assert_eq!(t1.cols.len(), 100);
443        assert_eq!(t2.cols.len(), 100);
444        assert_eq!(t1.rows.len(), 100);
445        assert_eq!(t2.rows.len(), 100);
446    }
447
448    #[test]
449    fn test_quadrants() {
450        let task = RecursiveTask::from_dims(100, 100);
451        let (tl, _tr, _bl, br) = task.quadrants();
452
453        assert_eq!(tl.rows.start, 0);
454        assert_eq!(tl.rows.end, 50);
455        assert_eq!(tl.cols.start, 0);
456        assert_eq!(tl.cols.end, 50);
457
458        assert_eq!(br.rows.start, 50);
459        assert_eq!(br.rows.end, 100);
460        assert_eq!(br.cols.start, 50);
461        assert_eq!(br.cols.end, 100);
462    }
463
464    #[test]
465    fn test_morton_index() {
466        // Morton index interleaves bits
467        assert_eq!(morton_index(0, 0), 0);
468        assert_eq!(morton_index(1, 0), 1);
469        assert_eq!(morton_index(0, 1), 2);
470        assert_eq!(morton_index(1, 1), 3);
471        assert_eq!(morton_index(2, 0), 4);
472
473        // Roundtrip test
474        for x in 0..100 {
475            for y in 0..100 {
476                let z = morton_index(x, y);
477                let (dx, dy) = morton_decode(z);
478                assert_eq!((dx, dy), (x, y));
479            }
480        }
481    }
482
483    struct CountingVisitor {
484        count: usize,
485        total_elements: usize,
486    }
487
488    impl BlockVisitor for CountingVisitor {
489        type Error = ();
490
491        fn visit_block(
492            &mut self,
493            row_start: usize,
494            row_end: usize,
495            col_start: usize,
496            col_end: usize,
497        ) -> Result<(), ()> {
498            self.count += 1;
499            self.total_elements += (row_end - row_start) * (col_end - col_start);
500            Ok(())
501        }
502    }
503
504    #[test]
505    fn test_cache_oblivious_traverse() {
506        let task = RecursiveTask::from_dims(128, 128);
507        let mut visitor = CountingVisitor {
508            count: 0,
509            total_elements: 0,
510        };
511
512        cache_oblivious_traverse(&mut visitor, task, 32).unwrap();
513
514        // Should visit multiple blocks
515        assert!(visitor.count > 1);
516        // Should cover all elements
517        assert_eq!(visitor.total_elements, 128 * 128);
518    }
519}