1use std::sync::OnceLock;
8
9const BLOCK_GROUP_SIZE: usize = 128;
11
12static GLOBAL_PROFILE: OnceLock<PlatformProfile> = OnceLock::new();
14
15static GLOBAL_THRESHOLDS: OnceLock<TunedThresholds> = OnceLock::new();
17
18#[derive(Debug, Clone)]
24pub struct PlatformProfile {
25 pub logical_cores: usize,
27 pub physical_cores: usize,
29 pub cache_line_bytes: usize,
31 pub l1_cache_bytes: usize,
33 pub l2_cache_bytes: usize,
35 pub has_avx2: bool,
37 pub has_avx512: bool,
39 pub has_neon: bool,
41}
42
43#[derive(Debug, Clone)]
48pub struct TunedThresholds {
49 pub par_gemv_min_rows: usize,
52 pub par_gemm_min_batch: usize,
54 pub tiled_gemm_block_m: usize,
56 pub tiled_gemm_block_n: usize,
58 pub tiled_gemm_block_k: usize,
61}
62
63impl PlatformProfile {
64 pub fn detect() -> Self {
70 let logical_cores = std::thread::available_parallelism()
71 .map(|p| p.get())
72 .unwrap_or(1);
73
74 let physical_cores = Self::estimate_physical_cores(logical_cores);
75 let cache_line_bytes = 64; let (l1_cache_bytes, l2_cache_bytes) = Self::estimate_cache_sizes();
79
80 let has_avx2 = Self::detect_avx2();
81 let has_avx512 = Self::detect_avx512();
82 let has_neon = Self::detect_neon();
83
84 Self {
85 logical_cores,
86 physical_cores,
87 cache_line_bytes,
88 l1_cache_bytes,
89 l2_cache_bytes,
90 has_avx2,
91 has_avx512,
92 has_neon,
93 }
94 }
95
96 pub fn global() -> &'static PlatformProfile {
98 GLOBAL_PROFILE.get_or_init(Self::detect)
99 }
100
101 pub fn compute_thresholds(&self) -> TunedThresholds {
107 let par_gemv_min_rows = self.compute_gemv_threshold();
108 let par_gemm_min_batch = self.compute_gemm_threshold();
109 let (block_m, block_n, block_k) = self.compute_tile_sizes();
110
111 TunedThresholds {
112 par_gemv_min_rows,
113 par_gemm_min_batch,
114 tiled_gemm_block_m: block_m,
115 tiled_gemm_block_n: block_n,
116 tiled_gemm_block_k: block_k,
117 }
118 }
119
120 pub fn global_thresholds() -> &'static TunedThresholds {
122 GLOBAL_THRESHOLDS.get_or_init(|| Self::global().compute_thresholds())
123 }
124
125 pub fn with_cores(logical: usize, physical: usize) -> Self {
127 Self {
128 logical_cores: logical.max(1),
129 physical_cores: physical.max(1),
130 cache_line_bytes: 64,
131 l1_cache_bytes: 32 * 1024,
132 l2_cache_bytes: 256 * 1024,
133 has_avx2: false,
134 has_avx512: false,
135 has_neon: false,
136 }
137 }
138
139 pub fn with_cache(l1_bytes: usize, l2_bytes: usize) -> Self {
141 Self {
142 logical_cores: 4,
143 physical_cores: 4,
144 cache_line_bytes: 64,
145 l1_cache_bytes: l1_bytes,
146 l2_cache_bytes: l2_bytes,
147 has_avx2: false,
148 has_avx512: false,
149 has_neon: false,
150 }
151 }
152
153 fn estimate_physical_cores(logical: usize) -> usize {
156 #[cfg(target_arch = "x86_64")]
157 {
158 (logical / 2).max(1)
160 }
161 #[cfg(target_arch = "aarch64")]
162 {
163 logical
165 }
166 #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
167 {
168 logical
170 }
171 }
172
173 fn estimate_cache_sizes() -> (usize, usize) {
174 #[cfg(target_arch = "aarch64")]
175 {
176 (64 * 1024, 512 * 1024)
180 }
181 #[cfg(not(target_arch = "aarch64"))]
182 {
183 (32 * 1024, 256 * 1024)
185 }
186 }
187
188 fn detect_avx2() -> bool {
189 #[cfg(target_arch = "x86_64")]
190 {
191 if is_x86_feature_detected!("avx2") {
193 return true;
194 }
195 cfg!(target_feature = "avx2")
197 }
198 #[cfg(not(target_arch = "x86_64"))]
199 {
200 false
201 }
202 }
203
204 fn detect_avx512() -> bool {
205 #[cfg(target_arch = "x86_64")]
206 {
207 if is_x86_feature_detected!("avx512f") {
208 return true;
209 }
210 cfg!(target_feature = "avx512f")
211 }
212 #[cfg(not(target_arch = "x86_64"))]
213 {
214 false
215 }
216 }
217
218 fn detect_neon() -> bool {
219 #[cfg(target_arch = "aarch64")]
220 {
221 true
223 }
224 #[cfg(not(target_arch = "aarch64"))]
225 {
226 false
227 }
228 }
229
230 fn compute_gemv_threshold(&self) -> usize {
242 let base = match self.physical_cores {
243 0..=2 => 256,
244 3..=4 => 128,
245 5..=8 => 64,
246 9..=15 => 48,
247 _ => 32,
248 };
249
250 let simd_factor = if self.has_avx512 {
253 3
255 } else if self.has_avx2 || self.has_neon {
256 2
257 } else {
258 1
259 };
260
261 let adjusted = base + (base * simd_factor) / 4;
263 round_up_to(adjusted, 8)
264 }
265
266 fn compute_gemm_threshold(&self) -> usize {
270 match self.physical_cores {
271 0..=2 => 16,
272 3..=4 => 8,
273 5..=8 => 4,
274 9..=15 => 3,
275 _ => 2,
276 }
277 }
278
279 fn compute_tile_sizes(&self) -> (usize, usize, usize) {
292 let l1 = self.l1_cache_bytes;
293 let sizeof_f32 = std::mem::size_of::<f32>();
294
295 let usable_l1 = (l1 * 3) / 4;
297
298 let raw_block = isqrt(usable_l1 / (3 * sizeof_f32));
300
301 let block_mn = round_down_to(raw_block, 8).max(8);
303
304 let block_k = if block_mn >= BLOCK_GROUP_SIZE {
308 round_down_to(block_mn, BLOCK_GROUP_SIZE)
309 } else {
310 BLOCK_GROUP_SIZE
311 };
312
313 (block_mn, block_mn, block_k)
314 }
315}
316
317impl TunedThresholds {
318 #[inline]
320 pub fn should_parallelize_gemv(&self, n_rows: usize) -> bool {
321 n_rows >= self.par_gemv_min_rows
322 }
323
324 #[inline]
326 pub fn should_parallelize_gemm(&self, batch_size: usize) -> bool {
327 batch_size >= self.par_gemm_min_batch
328 }
329}
330
331#[derive(Debug, Clone)]
335pub struct TuningSummary {
336 pub profile: PlatformProfile,
337 pub thresholds: TunedThresholds,
338}
339
340impl TuningSummary {
341 pub fn current() -> Self {
343 Self {
344 profile: PlatformProfile::global().clone(),
345 thresholds: PlatformProfile::global_thresholds().clone(),
346 }
347 }
348}
349
350impl std::fmt::Display for TuningSummary {
351 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
352 writeln!(f, "Platform Tuning Summary")?;
353 writeln!(
354 f,
355 " Cores: {} logical, {} physical",
356 self.profile.logical_cores, self.profile.physical_cores
357 )?;
358 writeln!(
359 f,
360 " Cache: L1={} KB, L2={} KB, line={} B",
361 self.profile.l1_cache_bytes / 1024,
362 self.profile.l2_cache_bytes / 1024,
363 self.profile.cache_line_bytes
364 )?;
365 writeln!(
366 f,
367 " SIMD: AVX2={}, AVX-512={}, NEON={}",
368 self.profile.has_avx2, self.profile.has_avx512, self.profile.has_neon
369 )?;
370 writeln!(f, " Thresholds:")?;
371 writeln!(
372 f,
373 " par_gemv_min_rows: {}",
374 self.thresholds.par_gemv_min_rows
375 )?;
376 writeln!(
377 f,
378 " par_gemm_min_batch: {}",
379 self.thresholds.par_gemm_min_batch
380 )?;
381 writeln!(
382 f,
383 " tiled_gemm_block: {}x{}x{}",
384 self.thresholds.tiled_gemm_block_m,
385 self.thresholds.tiled_gemm_block_n,
386 self.thresholds.tiled_gemm_block_k
387 )?;
388 Ok(())
389 }
390}
391
392fn isqrt(n: usize) -> usize {
396 if n == 0 {
397 return 0;
398 }
399 let mut x = n;
400 let mut y = x.div_ceil(2);
401 while y < x {
402 x = y;
403 y = (x + n / x) / 2;
404 }
405 x
406}
407
408fn round_up_to(v: usize, align: usize) -> usize {
410 debug_assert!(align > 0);
411 let rem = v % align;
412 if rem == 0 {
413 v
414 } else {
415 v + (align - rem)
416 }
417}
418
419fn round_down_to(v: usize, align: usize) -> usize {
421 debug_assert!(align > 0);
422 v - (v % align)
423}
424
425#[cfg(test)]
426mod tests {
427 use super::*;
428
429 #[test]
430 fn detect_returns_valid_profile() {
431 let profile = PlatformProfile::detect();
432 assert!(
433 profile.logical_cores > 0,
434 "must have at least 1 logical core"
435 );
436 assert!(
437 profile.physical_cores > 0,
438 "must have at least 1 physical core"
439 );
440 assert!(profile.physical_cores <= profile.logical_cores);
441 assert_eq!(profile.cache_line_bytes, 64);
442 assert!(
443 profile.l1_cache_bytes >= 16 * 1024,
444 "L1 should be at least 16KB"
445 );
446 assert!(
447 profile.l2_cache_bytes >= 64 * 1024,
448 "L2 should be at least 64KB"
449 );
450 }
451
452 #[test]
453 fn thresholds_are_reasonable() {
454 let profile = PlatformProfile::detect();
455 let thresholds = profile.compute_thresholds();
456 assert!(thresholds.par_gemv_min_rows >= 8);
457 assert!(thresholds.par_gemv_min_rows <= 1024);
458 assert!(thresholds.par_gemm_min_batch >= 1);
459 assert!(thresholds.par_gemm_min_batch <= 64);
460 }
461
462 #[test]
463 fn tile_sizes_multiple_of_8() {
464 let profile = PlatformProfile::detect();
465 let t = profile.compute_thresholds();
466 assert_eq!(t.tiled_gemm_block_m % 8, 0, "block_m must be multiple of 8");
467 assert_eq!(t.tiled_gemm_block_n % 8, 0, "block_n must be multiple of 8");
468 assert_eq!(
469 t.tiled_gemm_block_k % BLOCK_GROUP_SIZE,
470 0,
471 "block_k must be multiple of group size (128)"
472 );
473 }
474
475 #[test]
476 fn more_cores_lower_gemv_threshold() {
477 let p2 = PlatformProfile::with_cores(2, 2);
478 let p16 = PlatformProfile::with_cores(16, 16);
479 let t2 = p2.compute_thresholds();
480 let t16 = p16.compute_thresholds();
481 assert!(
482 t16.par_gemv_min_rows < t2.par_gemv_min_rows,
483 "16 cores ({}) should have lower threshold than 2 cores ({})",
484 t16.par_gemv_min_rows,
485 t2.par_gemv_min_rows
486 );
487 }
488
489 #[test]
490 fn more_cores_lower_gemm_threshold() {
491 let p2 = PlatformProfile::with_cores(2, 2);
492 let p16 = PlatformProfile::with_cores(16, 16);
493 let t2 = p2.compute_thresholds();
494 let t16 = p16.compute_thresholds();
495 assert!(
496 t16.par_gemm_min_batch < t2.par_gemm_min_batch,
497 "16 cores ({}) should have lower threshold than 2 cores ({})",
498 t16.par_gemm_min_batch,
499 t2.par_gemm_min_batch
500 );
501 }
502
503 #[test]
504 fn isqrt_correctness() {
505 assert_eq!(isqrt(0), 0);
506 assert_eq!(isqrt(1), 1);
507 assert_eq!(isqrt(4), 2);
508 assert_eq!(isqrt(9), 3);
509 assert_eq!(isqrt(10), 3); assert_eq!(isqrt(100), 10);
511 assert_eq!(isqrt(8192), 90); }
513
514 #[test]
515 fn round_helpers() {
516 assert_eq!(round_up_to(0, 8), 0);
517 assert_eq!(round_up_to(1, 8), 8);
518 assert_eq!(round_up_to(8, 8), 8);
519 assert_eq!(round_up_to(9, 8), 16);
520 assert_eq!(round_down_to(0, 8), 0);
521 assert_eq!(round_down_to(7, 8), 0);
522 assert_eq!(round_down_to(8, 8), 8);
523 assert_eq!(round_down_to(15, 8), 8);
524 }
525
526 #[test]
527 fn global_profile_consistent() {
528 let p1 = PlatformProfile::global();
529 let p2 = PlatformProfile::global();
530 assert_eq!(p1.logical_cores, p2.logical_cores);
532 assert_eq!(p1.physical_cores, p2.physical_cores);
533 }
534
535 #[test]
536 fn tuning_summary_display() {
537 let summary = TuningSummary::current();
538 let text = format!("{summary}");
539 assert!(text.contains("Platform Tuning Summary"));
540 assert!(text.contains("Cores:"));
541 assert!(text.contains("Cache:"));
542 assert!(text.contains("SIMD:"));
543 }
544
545 #[test]
546 fn should_parallelize_decisions() {
547 let p = PlatformProfile::with_cores(8, 8);
548 let t = p.compute_thresholds();
549 assert!(!t.should_parallelize_gemv(1));
551 assert!(t.should_parallelize_gemv(t.par_gemv_min_rows));
553 assert!(t.should_parallelize_gemv(t.par_gemv_min_rows + 1));
554 }
555
556 #[test]
557 fn with_cache_custom_sizes() {
558 let p = PlatformProfile::with_cache(64 * 1024, 1024 * 1024);
559 assert_eq!(p.l1_cache_bytes, 64 * 1024);
560 assert_eq!(p.l2_cache_bytes, 1024 * 1024);
561 let t = p.compute_thresholds();
562 let p_small = PlatformProfile::with_cache(16 * 1024, 128 * 1024);
564 let t_small = p_small.compute_thresholds();
565 assert!(
566 t.tiled_gemm_block_m >= t_small.tiled_gemm_block_m,
567 "larger L1 ({}) should give >= block_m than smaller L1 ({})",
568 t.tiled_gemm_block_m,
569 t_small.tiled_gemm_block_m
570 );
571 }
572}