Skip to main content

trueno/blis/
backend_selection.rs

1//! Backend Selection and Cost Model
2//!
3//! Automatic selection between CPU (SIMD), CUDA (PTX), and wgpu (WGSL) backends
4//! based on the 5× PCIe rule and roofline analysis.
5//!
6//! # Philosophy
7//!
8//! Uses Gregg & Hazelwood (2011) "5× PCIe rule": GPU worthwhile when
9//! compute time exceeds 5× data transfer time.
10//!
11//! # References
12//!
13//! - Gregg, C., & Hazelwood, K. (2011). Where is the Data? Why You Cannot
14//!   Debate CPU vs. GPU Performance Without the Answer. IEEE ISPASS.
15//! - Volkov, V. (2010). Better Performance at Lower Occupancy.
16
17#[cfg(target_arch = "x86_64")]
18use std::arch::is_x86_feature_detected;
19
20use super::profiler::BlisProfiler;
21use super::{gemm_blis, TruenoError};
22
23///
24/// Maps to different ISA targets:
25/// - Cpu: x86 asm (AVX2/AVX-512), ARM asm (NEON)
26/// - Gpu: PTX (CUDA), wgpu compute shaders
27/// - Wgpu: WGSL for cross-platform GPU (Vulkan/Metal/DX12/WebGPU)
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum ComputeBackend {
30    /// CPU SIMD backend (AVX2, AVX-512, NEON, SSE2)
31    Cpu,
32    /// NVIDIA GPU backend (PTX)
33    Gpu,
34    /// Cross-platform GPU backend (wgpu/WGSL)
35    Wgpu,
36    /// Scalar fallback (no SIMD)
37    Scalar,
38}
39
40/// ComputeBrick hierarchy level
41///
42/// Maps BLIS loop structure to brick abstraction:
43/// - Nano: Microkernel (MR×NR×K) - register file
44/// - Micro: Midi loop (MC×NC×KC) - L1/L2 cache
45/// - Meso: Macro loop (full M×N×K) - L3/DRAM
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum BrickLevel {
48    /// Register-level compute (MR×NR tile)
49    Nano,
50    /// Cache-level compute (MC×NC block)
51    Micro,
52    /// Memory-level compute (full matrix)
53    Meso,
54}
55
56/// Cost model for backend selection
57///
58/// Based on Gregg & Hazelwood (2011): GPU worthwhile when compute > 5× transfer
59#[derive(Debug, Clone)]
60pub struct BackendCostModel {
61    /// PCIe bandwidth in GB/s (e.g., 15.75 for PCIe 3.0 x16)
62    pub pcie_bandwidth_gbps: f64,
63    /// GPU peak TFLOP/s
64    pub gpu_peak_tflops: f64,
65    /// CPU peak GFLOP/s
66    pub cpu_peak_gflops: f64,
67    /// Minimum problem size for GPU (elements)
68    pub gpu_min_elements: usize,
69}
70
71/// Modern AVX2 CPU peak compute in GFLOP/s
72const DEFAULT_CPU_PEAK_GFLOPS: f64 = 400.0;
73
74impl Default for BackendCostModel {
75    fn default() -> Self {
76        Self {
77            pcie_bandwidth_gbps: 15.75, // PCIe 3.0 x16
78            gpu_peak_tflops: 10.0,      // Mid-range GPU
79            cpu_peak_gflops: DEFAULT_CPU_PEAK_GFLOPS,
80            gpu_min_elements: 1_000_000, // ~1M elements
81        }
82    }
83}
84
85impl BackendCostModel {
86    /// Select optimal backend based on 5× PCIe rule
87    ///
88    /// # References
89    ///
90    /// Gregg, C., & Hazelwood, K. (2011). Where is the Data? Why You Cannot
91    /// Debate CPU vs. GPU Performance Without the Answer. IEEE ISPASS.
92    pub fn select_backend(&self, m: usize, n: usize, k: usize) -> ComputeBackend {
93        let flops = 2 * m * n * k;
94        let bytes = 4 * (m * k + k * n + m * n); // f32 = 4 bytes
95        let arithmetic_intensity = flops as f64 / bytes as f64;
96
97        // Ridge point: where compute = memory bandwidth
98        let ridge_point = self.gpu_peak_tflops * 1000.0 / self.pcie_bandwidth_gbps;
99
100        // GPU worthwhile if:
101        // 1. High arithmetic intensity (compute-bound)
102        // 2. Problem size exceeds minimum threshold
103        // 3. Transfer time is amortized (5× rule)
104        let elements = m * n * k;
105        if arithmetic_intensity > ridge_point && elements > self.gpu_min_elements {
106            // Check if wgpu available at runtime
107            #[cfg(feature = "wgpu")]
108            return ComputeBackend::Wgpu;
109
110            #[cfg(all(not(feature = "wgpu"), feature = "cuda"))]
111            return ComputeBackend::Gpu;
112
113            #[allow(unreachable_code)]
114            ComputeBackend::Cpu
115        } else {
116            // CPU is better for small problems or memory-bound workloads
117            #[cfg(target_arch = "x86_64")]
118            {
119                if is_x86_feature_detected!("avx2") {
120                    return ComputeBackend::Cpu;
121                }
122            }
123            // aarch64 always has NEON, so it always earns the CPU path. Written as a
124            // `return`, it left the Scalar tail unreachable there (gx10 lint, PMAT-1102).
125            #[cfg(target_arch = "aarch64")]
126            {
127                ComputeBackend::Cpu
128            }
129            #[cfg(not(target_arch = "aarch64"))]
130            {
131                ComputeBackend::Scalar
132            }
133        }
134    }
135
136    /// Estimate execution time in microseconds
137    pub fn estimate_time_us(&self, m: usize, n: usize, k: usize, backend: ComputeBackend) -> f64 {
138        let flops = 2.0 * m as f64 * n as f64 * k as f64;
139        let bytes = 4.0 * (m * k + k * n + m * n) as f64;
140
141        match backend {
142            ComputeBackend::Gpu | ComputeBackend::Wgpu => {
143                // Transfer time + compute time
144                let transfer_us = bytes / (self.pcie_bandwidth_gbps * 1e3);
145                let compute_us = flops / (self.gpu_peak_tflops * 1e6);
146                transfer_us + compute_us
147            }
148            ComputeBackend::Cpu => flops / (self.cpu_peak_gflops * 1e3),
149            ComputeBackend::Scalar => {
150                // Assume 1 GFLOP/s for scalar
151                flops / 1e3
152            }
153        }
154    }
155}
156
157/// Unified profiler for all backends
158///
159/// Collects metrics across CPU (RDTSC), GPU (CUDA events), and wgpu (timestamp queries)
160#[derive(Debug, Clone, Default)]
161pub struct UnifiedBrickProfiler {
162    /// CPU profiling stats
163    pub cpu_stats: BlisProfiler,
164    /// Selected backend for this run
165    pub backend: Option<ComputeBackend>,
166    /// Total elements processed
167    pub total_elements: u64,
168    /// Backend selection decisions
169    pub selection_history: Vec<(usize, usize, usize, ComputeBackend)>,
170}
171
172impl UnifiedBrickProfiler {
173    /// Create a new unified profiler
174    pub fn new() -> Self {
175        Self {
176            cpu_stats: BlisProfiler::enabled(),
177            backend: None,
178            total_elements: 0,
179            selection_history: Vec::new(),
180        }
181    }
182
183    /// Record backend selection
184    pub fn record_selection(&mut self, m: usize, n: usize, k: usize, backend: ComputeBackend) {
185        self.backend = Some(backend);
186        self.total_elements += (m * n) as u64;
187        self.selection_history.push((m, n, k, backend));
188    }
189
190    /// Get roofline analysis for current backend
191    pub fn roofline_analysis(&self, m: usize, n: usize, k: usize) -> RooflineResult {
192        let cost = BackendCostModel::default();
193        let flops = 2.0 * m as f64 * n as f64 * k as f64;
194        let bytes = 4.0 * (m * k + k * n + m * n) as f64;
195        let ai = flops / bytes;
196
197        let ridge_point = match self.backend.unwrap_or(ComputeBackend::Cpu) {
198            ComputeBackend::Gpu | ComputeBackend::Wgpu => {
199                cost.gpu_peak_tflops * 1000.0 / cost.pcie_bandwidth_gbps
200            }
201            ComputeBackend::Cpu | ComputeBackend::Scalar => {
202                cost.cpu_peak_gflops / 50.0 // ~50 GB/s memory bandwidth
203            }
204        };
205
206        if ai < ridge_point {
207            RooflineResult::MemoryBound { ai, ridge_point }
208        } else {
209            RooflineResult::ComputeBound { ai, ridge_point }
210        }
211    }
212
213    /// Generate summary report
214    pub fn summary(&self) -> String {
215        let mut s = String::new();
216        s.push_str("Unified Brick Profiler Summary\n");
217        s.push_str("==============================\n");
218        s.push_str(&format!("Backend: {:?}\n", self.backend.unwrap_or(ComputeBackend::Scalar)));
219        s.push_str(&format!("Total elements: {}\n", self.total_elements));
220        s.push_str(&format!("Selections: {} decisions\n", self.selection_history.len()));
221        s.push_str("\nCPU Stats:\n");
222        s.push_str(&self.cpu_stats.summary());
223        s
224    }
225}
226
227/// Roofline model result
228#[derive(Debug, Clone, Copy)]
229pub enum RooflineResult {
230    /// Workload is memory-bound (AI < ridge point)
231    MemoryBound {
232        /// Arithmetic intensity (FLOP/byte)
233        ai: f64,
234        /// Ridge point where compute = memory
235        ridge_point: f64,
236    },
237    /// Workload is compute-bound (AI > ridge point)
238    ComputeBound {
239        /// Arithmetic intensity (FLOP/byte)
240        ai: f64,
241        /// Ridge point where compute = memory
242        ridge_point: f64,
243    },
244}
245
246impl RooflineResult {
247    /// Get arithmetic intensity
248    pub fn arithmetic_intensity(&self) -> f64 {
249        match self {
250            RooflineResult::MemoryBound { ai, .. } => *ai,
251            RooflineResult::ComputeBound { ai, .. } => *ai,
252        }
253    }
254
255    /// Check if compute-bound
256    pub fn is_compute_bound(&self) -> bool {
257        matches!(self, RooflineResult::ComputeBound { .. })
258    }
259}
260
261/// PTX microkernel definition (for documentation and future CUDA support)
262///
263/// This is a specification for the GPU microkernel. Actual PTX code generation
264/// would be done by the trueno-ptx crate.
265///
266/// # References
267///
268/// - NVIDIA PTX ISA Reference Manual
269/// - Volkov, V. (2010). Better Performance at Lower Occupancy.
270#[derive(Debug, Clone)]
271pub struct PtxMicrokernelSpec {
272    /// PTX version (e.g., "8.0")
273    pub ptx_version: &'static str,
274    /// Target SM architecture (e.g., "sm_80")
275    pub sm_target: &'static str,
276    /// Register count per thread
277    pub registers_per_thread: u32,
278    /// Shared memory bytes per block
279    pub smem_bytes: usize,
280    /// Thread block dimensions
281    pub block_dim: (u32, u32, u32),
282    /// Tile dimensions (MR, NR)
283    pub tile_dim: (usize, usize),
284}
285
286impl Default for PtxMicrokernelSpec {
287    fn default() -> Self {
288        Self {
289            ptx_version: "8.0",
290            sm_target: "sm_80",
291            registers_per_thread: 64,
292            smem_bytes: 48 * 1024, // 48KB shared memory
293            block_dim: (16, 16, 1),
294            tile_dim: (16, 16), // 16x16 output tile per warp
295        }
296    }
297}
298
299/// WGSL microkernel specification (for wgpu backend)
300///
301/// Defines the compute shader for matrix multiplication.
302#[derive(Debug, Clone)]
303pub struct WgslMicrokernelSpec {
304    /// Workgroup size (x, y, z)
305    pub workgroup_size: (u32, u32, u32),
306    /// Tile dimensions (MR, NR)
307    pub tile_dim: (usize, usize),
308    /// Use shared memory for tiling
309    pub use_shared_memory: bool,
310}
311
312impl Default for WgslMicrokernelSpec {
313    fn default() -> Self {
314        Self { workgroup_size: (8, 8, 1), tile_dim: (8, 8), use_shared_memory: true }
315    }
316}
317
318impl WgslMicrokernelSpec {
319    /// Generate WGSL shader source
320    ///
321    /// This generates a basic tiled GEMM shader. For production use,
322    /// this would be optimized with coalesced memory access and bank conflict avoidance.
323    pub fn generate_wgsl(&self) -> String {
324        format!(
325            r#"// WGSL GEMM Microkernel
326// Generated by trueno BLIS module
327// Tile: {}x{}, Workgroup: {}x{}x{}
328
329struct GemmParams {{
330    m: u32,
331    n: u32,
332    k: u32,
333    alpha: f32,
334    beta: f32,
335}}
336
337@group(0) @binding(0) var<uniform> params: GemmParams;
338@group(0) @binding(1) var<storage, read> a: array<f32>;
339@group(0) @binding(2) var<storage, read> b: array<f32>;
340@group(0) @binding(3) var<storage, read_write> c: array<f32>;
341
342var<workgroup> tile_a: array<f32, {tile_a_size}>;
343var<workgroup> tile_b: array<f32, {tile_b_size}>;
344
345@compute @workgroup_size({wx}, {wy}, {wz})
346fn main(
347    @builtin(global_invocation_id) global_id: vec3<u32>,
348    @builtin(local_invocation_id) local_id: vec3<u32>,
349    @builtin(workgroup_id) group_id: vec3<u32>,
350) {{
351    let row = global_id.y;
352    let col = global_id.x;
353
354    if (row >= params.m || col >= params.n) {{
355        return;
356    }}
357
358    var sum: f32 = 0.0;
359
360    // Tile over K dimension
361    let num_tiles = (params.k + {tile_k}u - 1u) / {tile_k}u;
362
363    for (var t: u32 = 0u; t < num_tiles; t++) {{
364        let k_base = t * {tile_k}u;
365
366        // Load tile_a and tile_b into shared memory
367        // (simplified - production code would have proper coalescing)
368        let k_idx = k_base + local_id.x;
369        if (row < params.m && k_idx < params.k) {{
370            tile_a[local_id.y * {tile_k}u + local_id.x] = a[row * params.k + k_idx];
371        }}
372        if (k_idx < params.k && col < params.n) {{
373            tile_b[local_id.y * {tile_k}u + local_id.x] = b[k_idx * params.n + col];
374        }}
375
376        workgroupBarrier();
377
378        // Compute partial sum
379        for (var kk: u32 = 0u; kk < {tile_k}u; kk++) {{
380            if (k_base + kk < params.k) {{
381                sum += tile_a[local_id.y * {tile_k}u + kk] * tile_b[kk * {tile_k}u + local_id.x];
382            }}
383        }}
384
385        workgroupBarrier();
386    }}
387
388    // Store result
389    let c_idx = row * params.n + col;
390    c[c_idx] = params.alpha * sum + params.beta * c[c_idx];
391}}
392"#,
393            self.tile_dim.0,
394            self.tile_dim.1,
395            self.workgroup_size.0,
396            self.workgroup_size.1,
397            self.workgroup_size.2,
398            tile_a_size = self.tile_dim.0 * self.tile_dim.0,
399            tile_b_size = self.tile_dim.0 * self.tile_dim.1,
400            wx = self.workgroup_size.0,
401            wy = self.workgroup_size.1,
402            wz = self.workgroup_size.2,
403            tile_k = self.tile_dim.0,
404        )
405    }
406}
407
408/// GEMM with automatic backend selection
409///
410/// Uses the 5× PCIe rule to select between CPU (asm) and GPU (PTX/WGSL) backends.
411pub fn gemm_auto(
412    m: usize,
413    n: usize,
414    k: usize,
415    a: &[f32],
416    b: &[f32],
417    c: &mut [f32],
418    profiler: Option<&mut UnifiedBrickProfiler>,
419) -> Result<(), TruenoError> {
420    let cost_model = BackendCostModel::default();
421    let backend = cost_model.select_backend(m, n, k);
422
423    if let Some(prof) = profiler {
424        prof.record_selection(m, n, k, backend);
425    }
426
427    match backend {
428        ComputeBackend::Cpu | ComputeBackend::Scalar => {
429            // Use BLIS CPU implementation
430            gemm_blis(m, n, k, a, b, c, None)
431        }
432        ComputeBackend::Gpu => {
433            // PTX backend (stub - requires CUDA support)
434            // For now, fall back to CPU
435            gemm_blis(m, n, k, a, b, c, None)
436        }
437        ComputeBackend::Wgpu => {
438            // WGSL backend (stub - requires wgpu support)
439            // For now, fall back to CPU
440            gemm_blis(m, n, k, a, b, c, None)
441        }
442    }
443}