1use crate::error::TruenoError;
7
8use super::compute::{gemm_blis, gemm_blis_with_prepacked_b};
9use super::prepacked::PrepackedB;
10#[cfg(feature = "parallel")]
11use super::{MC, MR};
12
13#[derive(Debug, Clone)]
15pub struct HeijunkaScheduler {
16 pub num_threads: usize,
18 pub variance_threshold: f32,
20}
21
22impl Default for HeijunkaScheduler {
23 fn default() -> Self {
24 #[cfg(feature = "parallel")]
25 let threads = rayon::current_num_threads();
26 #[cfg(not(feature = "parallel"))]
27 let threads = 1;
28
29 Self {
30 num_threads: threads,
31 variance_threshold: 0.05, }
33 }
34}
35
36impl HeijunkaScheduler {
37 pub fn partition_m(&self, m: usize, mc: usize) -> Vec<std::ops::Range<usize>> {
39 let num_blocks = (m + mc - 1) / mc;
40 let blocks_per_thread = num_blocks / self.num_threads;
41 let remainder = num_blocks % self.num_threads;
42
43 let mut partitions = Vec::with_capacity(self.num_threads);
44 let mut start_block = 0;
45
46 for t in 0..self.num_threads {
47 let extra = if t < remainder { 1 } else { 0 };
48 let thread_blocks = blocks_per_thread + extra;
49
50 let start_row = start_block * mc;
51 let end_row = ((start_block + thread_blocks) * mc).min(m);
52
53 if start_row < end_row {
54 partitions.push(start_row..end_row);
55 }
56
57 start_block += thread_blocks;
58 }
59
60 partitions
61 }
62}
63
64#[cfg(feature = "parallel")]
73pub(crate) fn gemm_should_run_serial(m: usize, n: usize, k: usize) -> bool {
74 let flops = m * n * k;
75 flops < 8_000_000 || (flops < 64_000_000 && n < 192)
76}
77
78#[cfg(feature = "parallel")]
80pub fn gemm_blis_parallel(
81 m: usize,
82 n: usize,
83 k: usize,
84 a: &[f32],
85 b: &[f32],
86 c: &mut [f32],
87) -> Result<(), TruenoError> {
88 use rayon::prelude::*;
89 contract_pre_amdahl_speedup!();
90
91 if a.len() != m * k || b.len() != k * n || c.len() != m * n {
93 return Err(TruenoError::InvalidInput("Dimension mismatch".to_string()));
94 }
95
96 let flops = m * n * k;
100 if gemm_should_run_serial(m, n, k) {
101 return gemm_blis(m, n, k, a, b, c, None);
102 }
103
104 let phys_cores = num_cpus::get_physical();
116 let max_threads = if flops < 64_000_000 {
117 2.min(phys_cores)
119 } else if flops < 512_000_000 {
120 4.min(phys_cores)
122 } else if flops < 4_000_000_000 {
123 8.min(phys_cores)
129 } else {
130 (phys_cores / 2).max(8).min(phys_cores)
133 };
134
135 let mut scheduler = HeijunkaScheduler::default();
136 scheduler.num_threads = scheduler.num_threads.min(max_threads);
137 let ps = if m <= MC { MR.max(m / scheduler.num_threads) } else { MC };
138 let partitions = scheduler.partition_m(m, ps);
139
140 let c_ptr = c.as_mut_ptr() as usize;
147
148 partitions.into_par_iter().for_each(|m_range| {
149 let m_local = m_range.len();
150 let m_start = m_range.start;
151
152 let a_local = &a[m_start * k..(m_start + m_local) * k];
153
154 let c_local = unsafe {
156 let ptr = c_ptr as *mut f32;
157 std::slice::from_raw_parts_mut(ptr.add(m_start * n), m_local * n)
158 };
159
160 let _ = gemm_blis(m_local, n, k, a_local, b, c_local, None);
161 });
162
163 Ok(())
164}
165
166#[cfg(feature = "parallel")]
177pub fn gemm_blis_parallel_shared_b(
178 m: usize,
179 n: usize,
180 k: usize,
181 a: &[f32],
182 b: &[f32],
183 c: &mut [f32],
184) -> Result<(), TruenoError> {
185 use rayon::prelude::*;
186
187 if a.len() != m * k || b.len() != k * n || c.len() != m * n {
188 return Err(TruenoError::InvalidInput("Dimension mismatch".to_string()));
189 }
190
191 let flops = m * n * k;
192 if !shared_b_path_available(flops) {
193 return gemm_blis(m, n, k, a, b, c, None);
194 }
195
196 let num_threads = shared_b_thread_count(flops).min(rayon::current_num_threads());
197 let blk = super::cache_topology::blocking_8x32();
198 let geo = SharedBGeometry {
199 mr: blk.mr, nr: blk.nr, mc: blk.mc.min(m),
202 nc: blk.nc.min(n),
203 kc: blk.kc,
204 };
205
206 let b_panels = geo.nc.div_ceil(geo.nr);
208 let mut packed_b = vec![0.0f32; b_panels * geo.nr * geo.kc];
209
210 let c_ptr = c.as_mut_ptr() as usize;
211
212 for jc in (0..n).step_by(geo.nc) {
213 let nc_block = geo.nc.min(n - jc);
214
215 for pc in (0..k).step_by(geo.kc) {
216 let kc_block = geo.kc.min(k - pc);
217
218 super::compute::pack_b_block_generic(
220 b,
221 n,
222 pc,
223 jc,
224 kc_block,
225 nc_block,
226 geo.nr,
227 &mut packed_b,
228 );
229 let block = SharedBBlock {
230 a,
231 k,
232 n,
233 c_ptr,
234 jc,
235 pc,
236 nc_block,
237 kc_block,
238 geo: &geo,
239 shared_b: &packed_b,
240 };
241
242 let m_per_thread = m.div_ceil(num_threads).div_ceil(geo.mr) * geo.mr;
244
245 (0..num_threads).into_par_iter().for_each(|tid| {
246 let ic_start = tid * m_per_thread;
247 if ic_start < m {
248 block.run_slice(ic_start, (ic_start + m_per_thread).min(m), m_per_thread);
249 }
250 });
251 }
252 }
253
254 Ok(())
255}
256
257#[cfg(feature = "parallel")]
269fn shared_b_path_available(flops: usize) -> bool {
270 if flops < 8_000_000 {
271 return false;
272 }
273 #[cfg(target_arch = "x86_64")]
274 {
275 std::arch::is_x86_feature_detected!("avx512f")
276 }
277 #[cfg(not(target_arch = "x86_64"))]
278 {
279 false
280 }
281}
282
283#[cfg(feature = "parallel")]
286fn shared_b_thread_count(flops: usize) -> usize {
287 let phys_cores = num_cpus::get_physical();
288 if flops < 64_000_000 {
289 2.min(phys_cores)
290 } else if flops < 512_000_000 {
291 4.min(phys_cores)
292 } else {
293 (phys_cores / 2).max(8).min(phys_cores)
294 }
295}
296
297#[cfg(feature = "parallel")]
299struct SharedBGeometry {
300 mr: usize,
301 nr: usize,
302 mc: usize,
303 nc: usize,
304 kc: usize,
305}
306
307#[cfg(feature = "parallel")]
310struct SharedBBlock<'a> {
311 a: &'a [f32],
312 k: usize,
313 n: usize,
314 c_ptr: usize,
317 jc: usize,
318 pc: usize,
319 nc_block: usize,
320 kc_block: usize,
321 geo: &'a SharedBGeometry,
322 shared_b: &'a [f32],
323}
324
325#[cfg(feature = "parallel")]
326impl SharedBBlock<'_> {
327 fn run_slice(&self, ic_start: usize, ic_end: usize, m_per_thread: usize) {
331 thread_local! {
332 static TL_A: std::cell::RefCell<Vec<f32>> =
333 const { std::cell::RefCell::new(Vec::new()) };
334 }
335 TL_A.with(|tl| {
336 let geo = self.geo;
337 let needed = m_per_thread.div_ceil(geo.mr) * geo.mr * self.kc_block;
338 let mut packed_a = tl.borrow_mut();
339 if packed_a.len() < needed {
340 packed_a.resize(needed, 0.0);
341 }
342
343 for ic in (ic_start..ic_end).step_by(geo.mc) {
344 let mc_block = geo.mc.min(ic_end - ic);
345 super::packing::pack_a_block(
346 self.a,
347 self.k,
348 ic,
349 self.pc,
350 mc_block,
351 self.kc_block,
352 &mut packed_a,
353 );
354 self.run_panels(&packed_a, ic, mc_block);
355 }
356 });
357 }
358
359 fn run_panels(&self, packed_a: &[f32], ic: usize, mc_block: usize) {
361 let geo = self.geo;
362 let panels_n = self.nc_block.div_ceil(geo.nr);
363 for ir_panel in 0..mc_block.div_ceil(geo.mr) {
364 let ir = ir_panel * geo.mr;
365 let mr_block = geo.mr.min(mc_block - ir);
366 for jr_panel in 0..panels_n {
367 let jr = jr_panel * geo.nr;
368 let nr_block = geo.nr.min(self.nc_block - jr);
369 let a_panel = &packed_a[ir_panel * geo.mr * self.kc_block..];
370 let b_panel = &self.shared_b[jr_panel * geo.nr * self.kc_block..];
371 self.tile(a_panel, b_panel, ic + ir, self.jc + jr, mr_block, nr_block);
372 }
373 }
374 }
375
376 fn tile(
381 &self,
382 a_panel: &[f32],
383 b_panel: &[f32],
384 row: usize,
385 col: usize,
386 mr_block: usize,
387 nr_block: usize,
388 ) {
389 #[cfg(target_arch = "x86_64")]
390 if mr_block == 8 && nr_block == 32 {
391 unsafe {
396 super::compute::avx512_microkernel_8x32_rowmajor(
397 self.kc_block,
398 a_panel.as_ptr(),
399 b_panel.as_ptr(),
400 (self.c_ptr as *mut f32).add(row * self.n + col),
401 self.n,
402 );
403 }
404 return;
405 }
406 self.scalar_tile(a_panel, b_panel, row, col, mr_block, nr_block);
407 }
408
409 fn scalar_tile(
411 &self,
412 a_panel: &[f32],
413 b_panel: &[f32],
414 row: usize,
415 col: usize,
416 mr_block: usize,
417 nr_block: usize,
418 ) {
419 let geo = self.geo;
420 for ir_local in 0..mr_block {
421 for jr_local in 0..nr_block {
422 let mut sum = 0.0f32;
423 for p in 0..self.kc_block {
424 sum += a_panel[p * geo.mr + ir_local] * b_panel[p * geo.nr + jr_local];
425 }
426 unsafe {
429 let c = self.c_ptr as *mut f32;
430 *c.add((row + ir_local) * self.n + (col + jr_local)) += sum;
431 }
432 }
433 }
434 }
435}
436
437#[cfg(not(feature = "parallel"))]
439pub fn gemm_blis_parallel(
440 m: usize,
441 n: usize,
442 k: usize,
443 a: &[f32],
444 b: &[f32],
445 c: &mut [f32],
446) -> Result<(), TruenoError> {
447 gemm_blis(m, n, k, a, b, c, None)
448}
449
450#[cfg(feature = "parallel")]
461pub fn gemm_blis_parallel_with_prepacked_b(
462 m: usize,
463 n: usize,
464 k: usize,
465 a: &[f32],
466 prepacked_b: &PrepackedB,
467 c: &mut [f32],
468) -> Result<(), TruenoError> {
469 use rayon::prelude::*;
470
471 if a.len() != m * k || c.len() != m * n {
472 return Err(TruenoError::InvalidInput("Dimension mismatch".to_string()));
473 }
474 if prepacked_b.k != k || prepacked_b.n != n {
475 return Err(TruenoError::InvalidInput(format!(
476 "PrepackedB dimension mismatch: expected ({}, {}), got ({}, {})",
477 k, n, prepacked_b.k, prepacked_b.n
478 )));
479 }
480
481 if m * n * k < 1_000_000 {
483 return gemm_blis_with_prepacked_b(m, n, k, a, prepacked_b, c, None);
484 }
485
486 let scheduler = HeijunkaScheduler::default();
487 let partitions = scheduler.partition_m(m, MC);
488
489 let c_ptr = c.as_mut_ptr() as usize;
490
491 partitions.into_par_iter().for_each(|m_range| {
493 let m_local = m_range.len();
494 let m_start = m_range.start;
495
496 let a_local = &a[m_start * k..(m_start + m_local) * k];
497
498 let c_local = unsafe {
501 let ptr = c_ptr as *mut f32;
502 std::slice::from_raw_parts_mut(ptr.add(m_start * n), m_local * n)
503 };
504
505 let _ = gemm_blis_with_prepacked_b(m_local, n, k, a_local, prepacked_b, c_local, None);
506 });
507
508 Ok(())
509}
510
511#[cfg(not(feature = "parallel"))]
513pub fn gemm_blis_parallel_with_prepacked_b(
514 m: usize,
515 n: usize,
516 k: usize,
517 a: &[f32],
518 prepacked_b: &PrepackedB,
519 c: &mut [f32],
520) -> Result<(), TruenoError> {
521 gemm_blis_with_prepacked_b(m, n, k, a, prepacked_b, c, None)
522}