1#[cfg(target_arch = "x86_64")]
18use std::arch::is_x86_feature_detected;
19
20use super::profiler::BlisProfiler;
21use super::{gemm_blis, TruenoError};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum ComputeBackend {
30 Cpu,
32 Gpu,
34 Wgpu,
36 Scalar,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum BrickLevel {
48 Nano,
50 Micro,
52 Meso,
54}
55
56#[derive(Debug, Clone)]
60pub struct BackendCostModel {
61 pub pcie_bandwidth_gbps: f64,
63 pub gpu_peak_tflops: f64,
65 pub cpu_peak_gflops: f64,
67 pub gpu_min_elements: usize,
69}
70
71const DEFAULT_CPU_PEAK_GFLOPS: f64 = 400.0;
73
74impl Default for BackendCostModel {
75 fn default() -> Self {
76 Self {
77 pcie_bandwidth_gbps: 15.75, gpu_peak_tflops: 10.0, cpu_peak_gflops: DEFAULT_CPU_PEAK_GFLOPS,
80 gpu_min_elements: 1_000_000, }
82 }
83}
84
85impl BackendCostModel {
86 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); let arithmetic_intensity = flops as f64 / bytes as f64;
96
97 let ridge_point = self.gpu_peak_tflops * 1000.0 / self.pcie_bandwidth_gbps;
99
100 let elements = m * n * k;
105 if arithmetic_intensity > ridge_point && elements > self.gpu_min_elements {
106 #[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 #[cfg(target_arch = "x86_64")]
118 {
119 if is_x86_feature_detected!("avx2") {
120 return ComputeBackend::Cpu;
121 }
122 }
123 #[cfg(target_arch = "aarch64")]
126 {
127 ComputeBackend::Cpu
128 }
129 #[cfg(not(target_arch = "aarch64"))]
130 {
131 ComputeBackend::Scalar
132 }
133 }
134 }
135
136 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 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 flops / 1e3
152 }
153 }
154 }
155}
156
157#[derive(Debug, Clone, Default)]
161pub struct UnifiedBrickProfiler {
162 pub cpu_stats: BlisProfiler,
164 pub backend: Option<ComputeBackend>,
166 pub total_elements: u64,
168 pub selection_history: Vec<(usize, usize, usize, ComputeBackend)>,
170}
171
172impl UnifiedBrickProfiler {
173 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 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 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 }
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 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#[derive(Debug, Clone, Copy)]
229pub enum RooflineResult {
230 MemoryBound {
232 ai: f64,
234 ridge_point: f64,
236 },
237 ComputeBound {
239 ai: f64,
241 ridge_point: f64,
243 },
244}
245
246impl RooflineResult {
247 pub fn arithmetic_intensity(&self) -> f64 {
249 match self {
250 RooflineResult::MemoryBound { ai, .. } => *ai,
251 RooflineResult::ComputeBound { ai, .. } => *ai,
252 }
253 }
254
255 pub fn is_compute_bound(&self) -> bool {
257 matches!(self, RooflineResult::ComputeBound { .. })
258 }
259}
260
261#[derive(Debug, Clone)]
271pub struct PtxMicrokernelSpec {
272 pub ptx_version: &'static str,
274 pub sm_target: &'static str,
276 pub registers_per_thread: u32,
278 pub smem_bytes: usize,
280 pub block_dim: (u32, u32, u32),
282 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, block_dim: (16, 16, 1),
294 tile_dim: (16, 16), }
296 }
297}
298
299#[derive(Debug, Clone)]
303pub struct WgslMicrokernelSpec {
304 pub workgroup_size: (u32, u32, u32),
306 pub tile_dim: (usize, usize),
308 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 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
408pub 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 gemm_blis(m, n, k, a, b, c, None)
431 }
432 ComputeBackend::Gpu => {
433 gemm_blis(m, n, k, a, b, c, None)
436 }
437 ComputeBackend::Wgpu => {
438 gemm_blis(m, n, k, a, b, c, None)
441 }
442 }
443}