1use super::from_qwen_ops::{elem_precision, index_type, relayout_rows_to_dense, row_block_tiles};
17use super::from_tensor_op::{ident_map, init_of, page_width, per_col_map, MatmulNames, Template};
18use super::{SpatialReject, LV_GEN_COL, LV_GEN_K, LV_GEN_ROW, LV_ITEM_X, LV_ITEM_Y, LV_KBLOCK};
19use crate::codegen_sva::fabric_target::Precision;
20use crate::spatial::ir::*;
21use crate::spatial::target::SpatialTarget;
22use crate::tensor_recognize::{Fill, MatmulSpec};
23
24pub const Q8_BLOCK: usize = 32;
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum Q8Part {
30 Values,
32 Scales,
34}
35
36impl Q8Part {
37 pub fn repack(self) -> &'static str {
39 match self {
40 Q8Part::Values => "q8_0.values",
41 Q8Part::Scales => "q8_0.scales",
42 }
43 }
44}
45
46pub fn stored_name(weight: &str, part: Q8Part) -> String {
48 match part {
49 Q8Part::Values => format!("{weight}__q8"),
50 Q8Part::Scales => format!("{weight}__q8_scale"),
51 }
52}
53
54pub fn q8_rows_name(x: &str, writes: usize) -> String {
59 format!("{x}__q8_rows{writes}")
60}
61
62pub fn q8_rows_name_on(x: &str, writes: usize, chip: Option<u16>) -> String {
65 match chip {
66 Some(c) => format!("{}_c{c}", q8_rows_name(x, writes)),
67 None => q8_rows_name(x, writes),
68 }
69}
70
71pub fn stored_part(name: &str) -> Option<(&str, Q8Part)> {
73 match (name.strip_suffix("__q8_scale"), name.strip_suffix("__q8")) {
74 (Some(w), _) => Some((w, Q8Part::Scales)),
75 (None, Some(w)) => Some((w, Q8Part::Values)),
76 (None, None) => None,
77 }
78}
79
80pub fn q8_gemv_applies(spec: &MatmulSpec, target: &SpatialTarget) -> bool {
85 let (tr, tw) = (target.tile.rows, target.tile.cols);
86 (1..=tr).contains(&spec.m) && tr == Q8_BLOCK && tw == tr && spec.k > 0 && spec.k % Q8_BLOCK == 0 && spec.n > 0
87}
88
89fn quantize(w: &[f64], k: usize, n: usize) -> Option<(Vec<i64>, Vec<f64>)> {
92 let (mut q, mut s) = (vec![0i64; k * n], vec![0.0; (k / Q8_BLOCK) * n]);
93 for b in 0..k / Q8_BLOCK {
94 for col in 0..n {
95 let at = |i: usize| (Q8_BLOCK * b + i) * n + col;
96 let amax = (0..Q8_BLOCK).map(|i| w[at(i)].abs()).fold(0.0, f64::max);
97 if amax == 0.0 {
98 continue;
99 }
100 let d = amax / 127.0;
101 if (d as f32) as f64 != d {
103 return None;
104 }
105 for i in 0..Q8_BLOCK {
106 let v = w[at(i)] / d;
107 let r = v.round();
108 if (v - r).abs() > 1e-9 || r.abs() > 127.0 || r * d != w[at(i)] {
109 return None;
110 }
111 q[at(i)] = r as i64;
112 }
113 s[b * n + col] = d;
114 }
115 }
116 Some((q, s))
117}
118
119pub fn q8_gemv(
128 spec: &MatmulSpec,
129 names: &MatmulNames,
130 fills: (&Fill, &Fill),
131 view: &str,
132 target: &SpatialTarget,
133) -> Result<Option<Vec<SpatialProgram>>, SpatialReject> {
134 let (m, k, n) = (spec.m, spec.k, spec.n);
135 let tw = target.tile.cols;
136 let precision = elem_precision(target)?;
137 let acc = AccPolicy::F32.interm_precision();
138 let (values_init, scales_init) = match fills.1 {
139 Fill::Unknown => (TensorInit::External, TensorInit::External),
140 Fill::Const(0) => (TensorInit::Zero, TensorInit::Zero),
142 Fill::ConstF(b) if f64::from_bits(*b) == 0.0 => (TensorInit::Zero, TensorInit::Zero),
143 fill => {
144 let init = init_of(fill, k * n);
145 let w: Vec<f64> = (0..k * n)
146 .map(|p| init.element_f64(p).unwrap_or(0.0))
147 .collect();
148 let (q, s) = quantize(&w, k, n).ok_or_else(|| SpatialReject::NotQ8_0 {
149 weight: names.b.clone(),
150 })?;
151 (
152 TensorInit::Values(q),
153 TensorInit::FloatValues(s.iter().map(|d| d.to_bits()).collect()),
154 )
155 }
156 };
157 let mut out = if m == 1 {
160 relayout_rows_to_dense(
161 (&names.a, fills.0),
162 view,
163 k / Q8_BLOCK,
164 Q8_BLOCK,
165 precision,
166 target,
167 )?
168 } else {
169 relayout_rows_to_q8_view((&names.a, fills.0), view, m, k, precision, target)?
170 };
171 let budget = target.max_local_mem_bytes();
172 let n_t = n.div_ceil(tw);
173 let mut chosen = None;
177 for wb in (1..=n_t.min(row_block_tiles(target)))
178 .rev()
179 .filter(|w| n_t % w == 0)
180 {
181 let fold = q8_fold(
182 names,
183 view,
184 (m, k, n),
185 wb,
186 (&values_init, &scales_init),
187 precision,
188 acc,
189 target,
190 )?;
191 if fold
192 .buffers
193 .iter()
194 .map(|b| fold.buffer_bytes(b))
195 .sum::<usize>()
196 <= budget
197 {
198 chosen = Some((wb, fold));
199 break;
200 }
201 }
202 let Some((wb, fold)) = chosen else {
203 return Ok(None);
204 };
205 let (tile, padded) = (target.tile.rows * tw, n_t * tw);
207 let weight = k * padded + 4 * (k / Q8_BLOCK) * padded;
208 let view_reads = (n_t / wb)
209 * k.div_ceil(tile)
210 * m
211 * tile
212 * crate::spatial::target::precision_bytes(precision);
213 if weight + view_reads >= 2 * k * padded {
214 return Ok(None);
215 }
216 out.push(fold);
217 Ok(Some(out))
218}
219
220fn relayout_rows_to_q8_view(
224 x: (&str, &Fill),
225 view: &str,
226 m: usize,
227 k: usize,
228 dt: Precision,
229 target: &SpatialTarget,
230) -> Result<Vec<SpatialProgram>, SpatialReject> {
231 let block = target.tile.rows * Q8_BLOCK;
232 let (nb, tail) = (k / block, k % block);
233 let part = nb > 0 && tail > 0;
234 let mut out = Vec::new();
235 if nb > 0 {
236 out.push(q8_view_section(
237 x,
238 view,
239 (m, k),
240 (0, nb, block),
241 part,
242 dt,
243 target,
244 )?);
245 }
246 if tail > 0 {
247 out.push(q8_view_section(
248 x,
249 view,
250 (m, k),
251 (nb, 1, tail),
252 part,
253 dt,
254 target,
255 )?);
256 }
257 Ok(out)
258}
259
260fn q8_view_section(
268 x: (&str, &Fill),
269 view: &str,
270 (m, k): (usize, usize),
271 (kb0, kbs, width): (usize, usize, usize),
272 part: bool,
273 dt: Precision,
274 target: &SpatialTarget,
275) -> Result<SpatialProgram, SpatialReject> {
276 let (tr, tw) = (target.tile.rows, target.tile.cols);
277 let (x_t, page_t, g) = (k / tw, width / tw, width / Q8_BLOCK);
278 let page_w = page_t * tw;
279 let rows = (1..=m.min(row_block_tiles(target)))
280 .rev()
281 .find(|d| m % d == 0)
282 .unwrap_or(1);
283 let chunks = m / rows;
284 let ix = index_type(target, (m * page_w) as u64)?;
285 let mut t = Template::new(target)?;
286 let src = t.tensor(x.0, TensorRole::Input, m, k, dt, init_of(x.1, m * k));
287 let dst = t.tensor(
288 view,
289 if part {
290 TensorRole::State
291 } else {
292 TensorRole::Output
293 },
294 q8_view_rows(m, k),
295 Q8_BLOCK,
296 dt,
297 TensorInit::Zero,
298 );
299 let lanes: Vec<i64> = (0..chunks * rows * tw)
300 .map(|c| ((c / tw) * page_w + c % tw) as i64)
301 .collect();
302 let table = t.tensor(
303 &format!("{view}__lanes{kb0}"),
304 TensorRole::Param,
305 1,
306 chunks * rows * tw,
307 ix,
308 TensorInit::Values(lanes),
309 );
310 let x_in = t.buffer("in_block", page_t, dt);
311 let lane_in = t.buffer_deep("in_lanes", 1, rows, ix);
312 let out = t.buffer("out", rows, dt);
313 let l_x = t.load_block_at(
314 "load_block",
315 src,
316 kb0 * (tr * Q8_BLOCK / tw),
317 vec![(LV_ITEM_Y, page_t)],
318 1,
319 page_t,
320 x_t,
321 x_in,
322 );
323 let l_lanes = t.load_block_at(
324 "load_lanes",
325 table,
326 0,
327 vec![(LV_ITEM_X, rows)],
328 1,
329 rows,
330 chunks * rows,
331 lane_in,
332 );
333 let lift = ElemAccess {
334 base: 0,
335 terms: vec![(LV_GEN_ROW, tw as i64)],
336 indirect: Some(IndirectTerm {
337 operand: 1,
338 stride: 1,
339 modulus: m * page_w,
340 }),
341 };
342 let s = t.stage(
343 "lift_rows",
344 StageKind::Compute(ComputeOp::Generic {
345 iters: vec![
346 IterSpec {
347 var: LV_GEN_ROW,
348 extent: g,
349 kind: IterKind::Parallel,
350 },
351 IterSpec {
352 var: LV_GEN_COL,
353 extent: rows * tw,
354 kind: IterKind::Parallel,
355 },
356 ],
357 ins: vec![
358 lift,
359 ElemAccess {
360 base: 0,
361 terms: vec![(LV_GEN_COL, 1)],
362 indirect: None,
363 },
364 ],
365 out: ident_map(page_width(rows, target)),
366 body: TensorBody::Operand(0),
367 }),
368 vec![x_in, lane_in],
369 vec![out],
370 );
371 let st = t.store_block(
372 "store_view",
373 dst,
374 kb0 * m,
375 vec![(LV_ITEM_Y, m), (LV_ITEM_X, rows)],
376 rows,
377 1,
378 1,
379 out,
380 );
381 t.edge(l_x, x_in, s);
382 t.edge(l_lanes, lane_in, s);
383 t.edge(s, out, st);
384 let items: Vec<WorkItem> = (0..kbs)
385 .flat_map(|kb| (0..chunks).map(move |c| (kb, c)))
386 .enumerate()
387 .map(|(id, (kb, c))| WorkItem {
388 id: WorkItemId::new(id),
389 coord: (kb, c),
390 out: TileRect {
391 row0_t: (kb0 + kb) * m + c * rows,
392 col0_t: 0,
393 h_t: rows,
394 w_t: 1,
395 },
396 })
397 .collect();
398 let n_items = items.len();
399 let mut p = t.finish(
400 format!("q8_view_{m}x{k}_from{kb0}"),
401 items,
402 AccPolicy::for_result(dt),
403 NumericsClaim::Exact,
404 target,
405 )?;
406 p.work.plan = Some(BlockPlan {
407 per_site_m: rows,
408 per_site_n: 1,
409 grid: if part {
411 (1, n_items)
412 } else {
413 (kbs * chunks, 1)
414 },
415 inner: InnerLoop {
416 num_blocks: 1,
417 block_k_tiles: 1,
418 },
419 block: BlockShape {
420 osh: rows,
421 osw: 1,
422 ibw: 1,
423 },
424 acc: AccPolicy::for_result(dt),
425 pack_accumulate: false,
426 engines: EnginePacking::LoadsTogether,
427 granularity: Granularity::PerTile,
428 });
429 Ok(p)
430}
431
432#[allow(clippy::too_many_arguments)]
440fn q8_fold(
441 names: &MatmulNames,
442 x_tiles: &str,
443 (m, k, n): (usize, usize, usize),
444 wb: usize,
445 (values_init, scales_init): (&TensorInit, &TensorInit),
446 precision: Precision,
447 acc: Precision,
448 target: &SpatialTarget,
449) -> Result<SpatialProgram, SpatialReject> {
450 let (tr, tw) = (target.tile.rows, target.tile.cols);
451 let block_rows = tr * Q8_BLOCK;
452 let (n_t, items, nb, g_tail) = (
453 n.div_ceil(tw),
454 n.div_ceil(tw) / wb,
455 k / block_rows,
456 (k % block_rows) / Q8_BLOCK,
457 );
458 let (w1, wn) = (page_width(1, target), page_width(wb, target));
459 let mut t = Template::new(target)?;
460 let xt = t.tensor(
461 x_tiles,
462 TensorRole::Input,
463 q8_view_rows(m, k),
464 Q8_BLOCK,
465 precision,
466 TensorInit::External,
467 );
468 let qt = t.tensor(
469 &stored_name(&names.b, Q8Part::Values),
470 TensorRole::Param,
471 k,
472 n,
473 Precision::Int8,
474 values_init.clone(),
475 );
476 let st = t.tensor(
477 &stored_name(&names.b, Q8Part::Scales),
478 TensorRole::Param,
479 k / Q8_BLOCK,
480 n,
481 acc,
482 scales_init.clone(),
483 );
484 let yt = t.tensor(&names.c, TensorRole::Output, m, n, acc, TensorInit::Zero);
485 let load = |t: &mut Template,
486 name: &str,
487 tensor: TensorId,
488 (base, steps): (usize, Vec<(LoopVar, usize)>),
489 (h, w, stride): (usize, usize, usize),
490 blocks: usize,
491 into: BufferId| {
492 let origin = TileAddr {
493 base,
494 steps,
495 indirect: None,
496 };
497 t.stage(
498 name,
499 StageKind::Load {
500 tensor,
501 pattern: AccessPattern {
502 origin,
503 block_h: h,
504 block_w: w,
505 stride_w: 1,
506 stride_h: stride,
507 },
508 num_blocks: blocks,
509 multicast: None,
510 },
511 Vec::new(),
512 vec![into],
513 )
514 };
515 let map = |base: i64, terms: &[(LoopVar, i64)]| ElemAccess {
516 base,
517 terms: terms.to_vec(),
518 indirect: None,
519 };
520 let cols = wb * tw;
521 let named = |stem: &str, r: usize| {
522 if m == 1 {
523 stem.to_string()
524 } else {
525 format!("{stem}_{r}")
526 }
527 };
528 let block_sums = |t: &mut Template,
533 stem: &str,
534 (x_in, q_in, s_in): (BufferId, BufferId, BufferId),
535 g: usize|
536 -> Vec<(StageId, BufferId)> {
537 let q_plane = (g * wb * tw) as i64;
538 (0..m)
539 .map(|r| {
540 let y_blk = t.scratch_deep(&named(stem, r), 1, wb, acc);
541 let iters = vec![
542 IterSpec {
543 var: LV_GEN_ROW,
544 extent: g,
545 kind: IterKind::Reduce {
546 pool: ReducePool::Sum,
547 },
548 },
549 IterSpec {
550 var: LV_GEN_COL,
551 extent: cols,
552 kind: IterKind::Parallel,
553 },
554 IterSpec {
555 var: LV_GEN_K,
556 extent: Q8_BLOCK,
557 kind: IterKind::Reduce {
558 pool: ReducePool::Sum,
559 },
560 },
561 ];
562 let s_blk = t.stage(
563 &named(stem, r),
564 StageKind::Compute(ComputeOp::Generic {
565 iters,
566 ins: vec![
567 map(
568 (r * w1) as i64,
569 &[(LV_GEN_ROW, (m * w1) as i64), (LV_GEN_K, 1)],
570 ),
571 map(
572 0,
573 &[
574 (LV_GEN_K, q_plane),
575 (LV_GEN_ROW, cols as i64),
576 (LV_GEN_COL, 1),
577 ],
578 ),
579 map(0, &[(LV_GEN_ROW, cols as i64), (LV_GEN_COL, 1)]),
580 ],
581 out: per_col_map(),
582 body: TensorBody::Mul(
583 Box::new(TensorBody::Mul(
584 Box::new(TensorBody::Operand(0)),
585 Box::new(TensorBody::Operand(1)),
586 )),
587 Box::new(TensorBody::Operand(2)),
588 ),
589 }),
590 vec![x_in, q_in, s_in],
591 vec![y_blk],
592 );
593 (s_blk, y_blk)
594 })
595 .collect()
596 };
597 let row_of = |r: usize, at: usize| {
599 let op = Box::new(TensorBody::Operand(at));
600 let zero = || Box::new(TensorBody::Const(0.0f64.to_bits()));
601 if m == 1 {
602 op
603 } else {
604 Box::new(TensorBody::Select {
605 cond: Pred::AxisLt(LV_GEN_ROW, PredRhs::Const(r + 1)),
606 then: Box::new(TensorBody::Select {
607 cond: Pred::AxisGe(LV_GEN_ROW, PredRhs::Const(r)),
608 then: op,
609 otherwise: zero(),
610 }),
611 otherwise: zero(),
612 })
613 }
614 };
615 let pointwise = vec![
616 IterSpec {
617 var: LV_GEN_ROW,
618 extent: tr,
619 kind: IterKind::Parallel,
620 },
621 IterSpec {
622 var: LV_GEN_COL,
623 extent: cols,
624 kind: IterKind::Parallel,
625 },
626 ];
627 let mut edges = Vec::new();
628 let run = (nb > 0).then(|| {
631 let x_in = t.buffer("in_x", m, precision);
632 let q_in = t.buffer("in_q8", tr * wb, Precision::Int8);
633 let s_in = t.buffer("in_scale", wb, acc);
634 let l_x = load(
635 &mut t,
636 "load_x",
637 xt,
638 (0, vec![(LV_KBLOCK, m)]),
639 (m, 1, 1),
640 nb,
641 x_in,
642 );
643 let l_q = load(
644 &mut t,
645 "load_q8",
646 qt,
647 (0, vec![(LV_KBLOCK, tr * n_t), (LV_ITEM_Y, wb)]),
648 (tr, wb, n_t),
649 nb,
650 q_in,
651 );
652 let l_s = load(
653 &mut t,
654 "load_scale",
655 st,
656 (0, vec![(LV_KBLOCK, n_t), (LV_ITEM_Y, wb)]),
657 (1, wb, n_t),
658 nb,
659 s_in,
660 );
661 let sums = block_sums(&mut t, "block", (x_in, q_in, s_in), Q8_BLOCK);
662 edges.extend([
663 (l_x, x_in, sums[0].0),
664 (l_q, q_in, sums[0].0),
665 (l_s, s_in, sums[0].0),
666 ]);
667 let run = t.scratch_deep("run_sum", 2, wb, acc);
668 t.stage(
669 "init_sum",
670 StageKind::Compute(ComputeOp::Generic {
671 iters: pointwise.clone(),
672 ins: Vec::new(),
673 out: ident_map(wn),
674 body: TensorBody::Const(0.0f64.to_bits()),
675 }),
676 Vec::new(),
677 vec![run],
678 );
679 let update = (0..m).fold(TensorBody::Operand(0), |body, r| {
680 TensorBody::Add(Box::new(body), row_of(r, r + 1))
681 });
682 let mut fold_ins = vec![run];
683 fold_ins.extend(sums.iter().map(|(_, b)| *b));
684 t.stage(
685 "fold_sum",
686 StageKind::Compute(ComputeOp::Generic {
687 iters: pointwise.clone(),
688 ins: vec![ident_map(wn); m + 1],
689 out: ident_map(wn),
690 body: update,
691 }),
692 fold_ins,
693 vec![run],
694 );
695 run
696 });
697 let tail = (g_tail > 0).then(|| {
700 let x_in = t.buffer_deep("in_x_tail", 1, m, precision);
701 let q_in = t.buffer_deep("in_q8_tail", 1, g_tail * wb, Precision::Int8);
702 let s_in = t.buffer_deep("in_scale_tail", 1, wb, acc);
703 let l_x = load(
704 &mut t,
705 "load_x_tail",
706 xt,
707 (nb * m, Vec::new()),
708 (m, 1, 1),
709 1,
710 x_in,
711 );
712 let l_q = load(
713 &mut t,
714 "load_q8_tail",
715 qt,
716 (tr * nb * n_t, vec![(LV_ITEM_Y, wb)]),
717 (g_tail, wb, n_t),
718 1,
719 q_in,
720 );
721 let l_s = load(
722 &mut t,
723 "load_scale_tail",
724 st,
725 (nb * n_t, vec![(LV_ITEM_Y, wb)]),
726 (1, wb, n_t),
727 1,
728 s_in,
729 );
730 let sums = block_sums(&mut t, "tail", (x_in, q_in, s_in), g_tail);
731 edges.extend([
732 (l_x, x_in, sums[0].0),
733 (l_q, q_in, sums[0].0),
734 (l_s, s_in, sums[0].0),
735 ]);
736 sums
737 });
738 let mut ins = Vec::new();
740 let mut body = run.map(|r| {
741 ins.push(r);
742 TensorBody::Operand(0)
743 });
744 for (r, (_, b)) in tail.iter().flatten().enumerate() {
745 ins.push(*b);
746 let term = *row_of(r, ins.len() - 1);
747 body = Some(match body {
748 Some(sum) => TensorBody::Add(Box::new(sum), Box::new(term)),
749 None => term,
750 });
751 }
752 let out = t.buffer("out", wb, acc);
753 let body = body.expect("a product has whole blocks or a tail");
754 let s_out = t.stage(
755 "result",
756 StageKind::Compute(ComputeOp::Generic {
757 iters: pointwise,
758 ins: vec![ident_map(wn); ins.len()],
759 out: ident_map(wn),
760 body,
761 }),
762 ins,
763 vec![out],
764 );
765 let st_y = t.store_block("store_y", yt, 0, vec![(LV_ITEM_Y, wb)], 1, wb, wb, out);
766 edges.push((s_out, out, st_y));
767 for (from, buf, to) in edges {
768 t.edge(from, buf, to);
769 }
770 let work: Vec<WorkItem> = (0..items)
771 .map(|y| WorkItem {
772 id: WorkItemId::new(y),
773 coord: (y, 0),
774 out: TileRect {
775 row0_t: 0,
776 col0_t: y * wb,
777 h_t: 1,
778 w_t: wb,
779 },
780 })
781 .collect();
782 let name = if m == 1 {
783 format!("q8_gemv_{k}x{n}_w{wb}")
784 } else {
785 format!("q8_gemm_{m}x{k}x{n}_w{wb}")
786 };
787 let mut p = t.finish(
788 name,
789 work,
790 AccPolicy::F32,
791 NumericsClaim::Approx {
792 reason: format!("a Q8_0 product over {m} {precision:?} row(s)"),
793 },
794 target,
795 )?;
796 p.work.plan = Some(BlockPlan {
797 per_site_m: 1,
798 per_site_n: wb,
799 grid: (1, items),
800 inner: InnerLoop {
801 num_blocks: nb.max(1),
802 block_k_tiles: 1,
803 },
804 block: BlockShape {
805 osh: 1,
806 osw: wb,
807 ibw: 1,
808 },
809 acc: AccPolicy::F32,
810 pack_accumulate: false,
811 engines: EnginePacking::LoadsTogether,
812 granularity: Granularity::PerTile,
813 });
814 p.work.k_tiles = nb.max(1);
815 Ok(p)
816}
817
818fn q8_view_rows(m: usize, k: usize) -> usize {
821 if m == 1 {
822 k / Q8_BLOCK
823 } else {
824 m * k.div_ceil(Q8_BLOCK * Q8_BLOCK) * Q8_BLOCK
825 }
826}