1use std::mem::MaybeUninit;
4use std::ops::Range;
5
6use rayon::prelude::*;
7use rten_base::iter::range_chunks;
8use rten_base::num::AsUsize;
9use rten_parallel::par_iter::ParIter;
10use rten_simd::ops::{BitOps, Extend, IntOps, Interleave, NumOps, ToFloat};
11use rten_simd::{Isa, Simd, SimdOp};
12use rten_tensor::{AsView, AssumeInit, Contiguous, Layout, NdTensor, NdTensorView};
13
14use crate::GemmResult;
15use crate::errors::{BlockQuantizedError, GemmError};
16use crate::i8dot::{Int8DotIsa, SimdInt8DotOp};
17
18#[derive(Copy, Clone, Debug, PartialEq)]
24pub enum ComputeMode {
25 Int8,
27 Float,
29}
30
31pub struct BlockQuantizedGemm {
34 mode: ComputeMode,
35}
36
37impl BlockQuantizedGemm {
38 pub fn new() -> Self {
39 BlockQuantizedGemm {
40 mode: ComputeMode::Float,
41 }
42 }
43
44 pub fn with_compute(mut self, mode: ComputeMode) -> Self {
46 self.mode = mode;
47 self
48 }
49
50 pub fn is_compute_optimized(mode: ComputeMode) -> bool {
53 match mode {
54 ComputeMode::Float => true,
55 ComputeMode::Int8 => is_int8_compute_optimized(),
56 }
57 }
58
59 pub fn batched_gemm_uninit<'a>(
61 &self,
62 out: &'a mut [MaybeUninit<f32>],
63 lhs: NdTensorView<f32, 3>,
64 rhs: BlockQuantizedMatrix<f32>,
65 ) -> GemmResult<&'a mut [f32]> {
66 let [batch, m, lhs_k] = lhs.shape();
67 let n = rhs.cols();
68
69 if out.len() != n * m * batch {
70 return Err(GemmError::OutputSizeMismatch);
71 }
72 if lhs_k != rhs.rows() {
73 return Err(GemmError::KSizeMismatch);
74 }
75
76 if rhs.bits != 4 {
77 return Err(GemmError::QuantBitsNotSupported);
78 }
79
80 if lhs_k == 0 {
82 out.fill(MaybeUninit::new(0.));
83 return Ok(unsafe { out.assume_init() });
84 }
85
86 enum LhsRow<'a> {
87 Float(&'a [f32]),
88 Quant {
89 data: Contiguous<NdTensorView<'a, i8, 2>>,
90 scales: &'a [f32],
91 },
92 }
93
94 let lhs = lhs.to_contiguous();
95
96 let lhs_quant: Option<(NdTensor<i8, 4>, NdTensor<f32, 3>)> =
97 if matches!(self.mode, ComputeMode::Int8) && m == 1 {
98 Some(quantize(lhs.view(), rhs.elements_per_block()))
99 } else {
100 None
101 };
102
103 let col_block = 16;
104 for (b, out_mat) in out.chunks_mut(n * m).enumerate() {
105 for (row, out_row) in out_mat.chunks_mut(n).enumerate() {
108 let lhs_row = if let Some((lhs_data, lhs_scales)) = &lhs_quant {
109 LhsRow::Quant {
110 data: Contiguous::new(lhs_data.slice((b, row))).unwrap(),
111 scales: lhs_scales.slice((b, row)).data().unwrap(),
112 }
113 } else {
114 LhsRow::Float(lhs.slice((b, row)).data().unwrap())
115 };
116
117 ParIter::from(range_chunks(0..n, col_block))
118 .zip(out_row.par_chunks_mut(col_block))
119 .for_each(|(col_range, out_row_chunk)| match lhs_row {
120 LhsRow::Quant { data, scales } => {
121 let op = VecDotMatrixQuant {
122 lhs_data: data,
123 lhs_scales: scales,
124 rhs: rhs.slice(col_range),
125 out: out_row_chunk,
126 };
127 op.dispatch();
128 }
129 LhsRow::Float(lhs) => {
130 let op = VecDotMatrix {
131 lhs,
132 rhs: rhs.slice(col_range),
133 out: out_row_chunk,
134 };
135 op.dispatch();
136 }
137 });
138 }
139 }
140
141 Ok(unsafe { out.assume_init() })
142 }
143}
144
145impl Default for BlockQuantizedGemm {
146 fn default() -> Self {
147 Self::new()
148 }
149}
150
151fn is_int8_compute_optimized() -> bool {
154 struct HasInt8Simd;
155 impl SimdInt8DotOp for HasInt8Simd {
156 type Output = bool;
157
158 fn eval<I: Int8DotIsa>(self, _isa: I) -> Self::Output {
159 I::SIMD
160 }
161 }
162 HasInt8Simd.dispatch()
163}
164
165struct VecDotMatrix<'a> {
168 lhs: &'a [f32],
169 rhs: BlockQuantizedMatrix<'a, f32>,
170 out: &'a mut [MaybeUninit<f32>],
171}
172
173impl<'a> VecDotMatrix<'a> {
174 #[inline(always)]
175 fn eval_impl<I: Isa, const SCALES_PER_VBLOCK: usize>(self, isa: I) -> &'a mut [f32] {
176 let ops = isa.f32();
177 let i16_ops = isa.i16();
178 let i32_ops = isa.i32();
179 let i8_ops = isa.i8();
180 let u8_ops = isa.u8();
181
182 let elements_per_vec = u8_ops.len() * 2;
186
187 let VecDotMatrix { lhs, rhs, out } = self;
188 let vecs_per_block = rhs.elements_per_block() / elements_per_vec;
189
190 let vecs_per_block_log2 = if vecs_per_block != 0 {
192 debug_assert!(vecs_per_block.is_power_of_two());
193 vecs_per_block.ilog2()
194 } else {
195 0
196 };
197
198 assert!(ops.len() <= 16);
202
203 let n_tail_scales = rhs.blocks_per_column() % SCALES_PER_VBLOCK;
204
205 let rhs_data = rhs.quant.data();
206 let rhs_cols = rhs_data.chunks_exact(rhs.blocks_per_column() * rhs.bytes_per_block());
207 let scale_blocks = rhs.scales.data().chunks_exact(rhs.blocks_per_column());
208
209 for ((col, col_scales), out) in rhs_cols.zip(scale_blocks).zip(out.iter_mut()) {
210 let mut acc = [ops.zero(); 4];
211
212 let mut row_vblocks = lhs.chunks_exact(elements_per_vec);
213 let mut col_vblocks = col.chunks_exact(u8_ops.len());
214
215 let zero_point = i8_ops.splat(8);
216 let lo_mask = u8_ops.splat(0x0F);
217
218 for (vblock_idx, (row_vblock, col_vblock)) in
220 row_vblocks.by_ref().zip(col_vblocks.by_ref()).enumerate()
221 {
222 let rhs_vblock = u8_ops.load(col_vblock);
224
225 let lo = u8_ops.and(rhs_vblock, lo_mask);
227 let hi = u8_ops.shift_right::<4>(rhs_vblock);
228 let (lo, hi) = (
229 u8_ops.interleave_low(lo, hi),
230 u8_ops.interleave_high(lo, hi),
231 );
232
233 let lo = i8_ops.from_bits(lo.to_bits());
235 let hi = i8_ops.from_bits(hi.to_bits());
236
237 let lo = i8_ops.sub(lo, zero_point);
239 let hi = i8_ops.sub(hi, zero_point);
240
241 let a_i16 = i8_ops.extend_low(lo);
243 let b_i16 = i8_ops.extend_high(lo);
244 let c_i16 = i8_ops.extend_low(hi);
245 let d_i16 = i8_ops.extend_high(hi);
246 let a_i32 = i16_ops.extend_low(a_i16);
247 let b_i32 = i16_ops.extend_high(a_i16);
248 let c_i32 = i16_ops.extend_low(b_i16);
249 let d_i32 = i16_ops.extend_high(b_i16);
250 let e_i32 = i16_ops.extend_low(c_i16);
251 let f_i32 = i16_ops.extend_high(c_i16);
252 let g_i32 = i16_ops.extend_low(d_i16);
253 let h_i32 = i16_ops.extend_high(d_i16);
254 let rhs_i32 = [a_i32, b_i32, c_i32, d_i32, e_i32, f_i32, g_i32, h_i32];
255
256 let vlen = ops.len();
258 match SCALES_PER_VBLOCK {
259 1 => {
260 let scale = ops.splat(col_scales[vblock_idx >> vecs_per_block_log2]);
261 for i in 0..8 {
262 let rhs_f32 = i32_ops.to_float(rhs_i32[i]);
263 let rhs_scaled = ops.mul(rhs_f32, scale);
264 let lhs = ops.load(&row_vblock[i * vlen..][..vlen]);
265 acc[i % acc.len()] = ops.mul_add(lhs, rhs_scaled, acc[i % acc.len()]);
266 }
267 }
268 2 => {
269 let block_idx = vblock_idx * 2;
270 let scales = [
271 ops.splat(col_scales[block_idx]),
272 ops.splat(col_scales[block_idx + 1]),
273 ];
274 for i in 0..8 {
275 let rhs_f32 = i32_ops.to_float(rhs_i32[i]);
276 let rhs_scaled = ops.mul(rhs_f32, scales[i / 4]);
277 let lhs = ops.load(&row_vblock[i * vlen..][..vlen]);
278 acc[i % acc.len()] = ops.mul_add(lhs, rhs_scaled, acc[i % acc.len()]);
279 }
280 }
281 4 => {
282 let block_idx = vblock_idx * 4;
283 let scales = [
284 ops.splat(col_scales[block_idx]),
285 ops.splat(col_scales[block_idx + 1]),
286 ops.splat(col_scales[block_idx + 2]),
287 ops.splat(col_scales[block_idx + 3]),
288 ];
289 for i in 0..8 {
290 let rhs_f32 = i32_ops.to_float(rhs_i32[i]);
291 let rhs_scaled = ops.mul(rhs_f32, scales[i / 2]);
292 let lhs = ops.load(&row_vblock[i * vlen..][..vlen]);
293 acc[i % acc.len()] = ops.mul_add(lhs, rhs_scaled, acc[i % acc.len()]);
294 }
295 }
296 8 => {
297 let block_idx = vblock_idx * 8;
298 for i in 0..8 {
299 let scale = ops.splat(col_scales[block_idx + i]);
300 let rhs_f32 = i32_ops.to_float(rhs_i32[i]);
301 let rhs_scaled = ops.mul(rhs_f32, scale);
302 let lhs = ops.load(&row_vblock[i * vlen..][..vlen]);
303 acc[i % acc.len()] = ops.mul_add(lhs, rhs_scaled, acc[i % acc.len()]);
304 }
305 }
306 _ => unreachable!(),
307 }
308 }
309
310 let acc_01 = ops.add(acc[0], acc[1]);
312 let acc_23 = ops.add(acc[2], acc[3]);
313 let acc = ops.add(acc_01, acc_23);
314 let mut acc = ops.sum(acc);
315
316 if !row_vblocks.remainder().is_empty() {
318 let mut tail_acc = 0.;
319
320 let lhs_tail_pairs = row_vblocks.remainder().as_chunks::<2>().0;
321 let tail_scales = &col_scales[col_scales.len() - n_tail_scales..];
322 let elements_per_scale = lhs_tail_pairs.len() / tail_scales.len();
323
324 debug_assert_eq!(lhs_tail_pairs.len(), col_vblocks.remainder().len());
325 for (i, (lhs, rhs)) in lhs_tail_pairs
326 .iter()
327 .zip(col_vblocks.remainder())
328 .enumerate()
329 {
330 let zero_point = 8;
331 let rhs_lo = (rhs & 0x0F) as i32 - zero_point;
332 let rhs_hi = (rhs >> 4) as i32 - zero_point;
333
334 let scale = tail_scales[i / elements_per_scale];
335 let rhs_lo_scaled = (rhs_lo as f32) * scale;
336 let rhs_hi_scaled = (rhs_hi as f32) * scale;
337
338 tail_acc += lhs[0] * rhs_lo_scaled + lhs[1] * rhs_hi_scaled;
339 }
340
341 acc += tail_acc;
342 }
343
344 out.write(acc);
345 }
346
347 unsafe { out.assume_init() }
348 }
349}
350
351impl<'a> SimdOp for VecDotMatrix<'a> {
352 type Output = &'a mut [f32];
353
354 #[inline(always)]
355 fn eval<I: Isa>(self, isa: I) -> Self::Output {
356 let u8_ops = isa.u8();
357
358 let elements_per_vec = u8_ops.len() * 2;
362
363 let scales_per_vblock = (elements_per_vec / self.rhs.elements_per_block()).max(1);
367
368 match scales_per_vblock {
369 1 => self.eval_impl::<I, 1>(isa),
370 2 => self.eval_impl::<I, 2>(isa),
371 4 => self.eval_impl::<I, 4>(isa),
372 8 => self.eval_impl::<I, 8>(isa),
373 _ => unreachable!("unsupported scales_per_vblock"),
374 }
375 }
376}
377
378fn quantize(
386 data: Contiguous<NdTensorView<f32, 3>>,
387 block_size: usize,
388) -> (NdTensor<i8, 4>, NdTensor<f32, 3>) {
389 let [batch, rows, k] = data.shape();
390
391 assert!(block_size >= 16 && block_size.is_power_of_two());
392 assert!(k.is_multiple_of(block_size));
393
394 let n_blocks = k / block_size;
395 let mut output = Vec::with_capacity(n_blocks * block_size);
396 let mut scales = Vec::with_capacity(n_blocks);
397
398 for block in data.data().chunks_exact(block_size) {
399 let abs_max = block.iter().fold(0., |max, x| x.abs().max(max));
400 let inv_scale = i8::MAX as f32 / abs_max;
401
402 for &x in block {
403 let qx = (x * inv_scale).round() as i8;
404 output.push(qx);
405 }
406
407 scales.push(1. / inv_scale);
408 }
409
410 let quant_data = NdTensor::from_data([batch, rows, n_blocks, block_size], output);
411 let scales = NdTensor::from_data([batch, rows, n_blocks], scales);
412 (quant_data, scales)
413}
414
415struct VecDotMatrixQuant<'a> {
417 lhs_data: Contiguous<NdTensorView<'a, i8, 2>>,
418 lhs_scales: &'a [f32],
419 rhs: BlockQuantizedMatrix<'a, f32>,
420 out: &'a mut [MaybeUninit<f32>],
421}
422
423impl<'a> VecDotMatrixQuant<'a> {
424 #[inline(always)]
425 fn eval_impl<I: Int8DotIsa, const SCALES_PER_VBLOCK: usize>(self, isa: I) -> &'a mut [f32] {
426 let ops = isa.isa().f32();
427 let i8_ops = isa.isa().i8();
428 let u8_ops = isa.isa().u8();
429 let i32_ops = isa.isa().i32();
430
431 let VecDotMatrixQuant {
432 lhs_data,
433 lhs_scales,
434 rhs,
435 out,
436 } = self;
437
438 let rhs_data = rhs.quant.data();
439 let rhs_cols = rhs_data.chunks_exact(rhs.blocks_per_column() * rhs.bytes_per_block());
440 let scale_blocks = rhs.scales.data().chunks_exact(rhs.blocks_per_column());
441
442 let elements_per_vec = u8_ops.len() * 2;
446 let elements_per_block = rhs.elements_per_block();
447 let vecs_per_block = elements_per_block / elements_per_vec;
448 let blocks_per_vec = elements_per_vec.div_ceil(elements_per_block);
449 let n_tail_blocks = rhs.blocks_per_column() % blocks_per_vec;
450
451 let vecs_per_block_log2 = if vecs_per_block != 0 {
453 debug_assert!(vecs_per_block.is_power_of_two());
454 vecs_per_block.ilog2()
455 } else {
456 0
457 };
458
459 let lo_half_mask = ops.first_n_mask(ops.len() / 2);
461 let lo_quad_mask = ops.first_n_mask(ops.len() / 4);
462 let lo_three_quads_mask = ops.first_n_mask(3 * ops.len() / 4);
463
464 for ((col, col_scales), out) in rhs_cols.zip(scale_blocks).zip(out.iter_mut()) {
465 let mut acc = [ops.zero(); 2];
466
467 let zero_point = i8_ops.splat(8);
468 let lo_mask = u8_ops.splat(0x0F);
469 let vlen = u8_ops.len();
470 let zero_i32 = i32_ops.zero();
471
472 let mut col_vblocks = col.chunks_exact(vlen);
473 let mut row_vblocks = lhs_data.data().chunks_exact(vlen * 2);
474
475 for (vblock_idx, vblocks) in row_vblocks.by_ref().zip(col_vblocks.by_ref()).enumerate()
476 {
477 let (row_vblock, col_vblock) = vblocks;
478
479 let rhs_vblock = u8_ops.load(col_vblock);
481
482 let lo = u8_ops.and(rhs_vblock, lo_mask);
484 let hi = u8_ops.shift_right::<4>(rhs_vblock);
485 let (lo, hi) = (
486 u8_ops.interleave_low(lo, hi),
487 u8_ops.interleave_high(lo, hi),
488 );
489
490 let mut lo = i8_ops.from_bits(lo.to_bits());
492 let mut hi = i8_ops.from_bits(hi.to_bits());
493
494 if !I::LHS_UNSIGNED {
496 lo = i8_ops.sub(lo, zero_point);
497 hi = i8_ops.sub(hi, zero_point);
498 }
499
500 let [lhs_lo, lhs_hi] = i8_ops.load_many::<2>(row_vblock);
502
503 let mut dot_lo = isa.dot(lo, lhs_lo, zero_i32);
505 let mut dot_hi = isa.dot(hi, lhs_hi, zero_i32);
506
507 if I::LHS_UNSIGNED {
510 let zero_point_i32 = i32_ops.splat(8);
511 let lhs_lo_sum = isa.dot(i8_ops.splat(1), lhs_lo, zero_i32);
512 let lhs_hi_sum = isa.dot(i8_ops.splat(1), lhs_hi, zero_i32);
513 dot_lo = i32_ops.sub(dot_lo, i32_ops.mul(zero_point_i32, lhs_lo_sum));
514 dot_hi = i32_ops.sub(dot_hi, i32_ops.mul(zero_point_i32, lhs_hi_sum));
515 }
516
517 let float_lo = i32_ops.to_float(dot_lo);
518 let float_hi = i32_ops.to_float(dot_hi);
519
520 match SCALES_PER_VBLOCK {
521 1 => {
522 let col_scale = col_scales[vblock_idx >> vecs_per_block_log2];
523 let row_scale = lhs_scales[vblock_idx >> vecs_per_block_log2];
524
525 let scale = ops.splat(col_scale * row_scale);
526
527 acc[0] = ops.mul_add(float_lo, scale, acc[0]);
528 acc[1] = ops.mul_add(float_hi, scale, acc[1]);
529 }
530 2 => {
531 let block_idx = vblock_idx * 2;
532
533 let col_scale_lo = col_scales[block_idx];
534 let col_scale_hi = col_scales[block_idx + 1];
535
536 let row_scale_lo = lhs_scales[block_idx];
537 let row_scale_hi = lhs_scales[block_idx + 1];
538
539 let scale_lo = ops.splat(col_scale_lo * row_scale_lo);
540 let scale_hi = ops.splat(col_scale_hi * row_scale_hi);
541
542 acc[0] = ops.mul_add(float_lo, scale_lo, acc[0]);
543 acc[1] = ops.mul_add(float_hi, scale_hi, acc[1]);
544 }
545 4 => {
546 let block_idx = vblock_idx * 4;
547
548 let col_scale_a = col_scales[block_idx];
549 let col_scale_b = col_scales[block_idx + 1];
550 let col_scale_c = col_scales[block_idx + 2];
551 let col_scale_d = col_scales[block_idx + 3];
552
553 let row_scale_a = lhs_scales[block_idx];
554 let row_scale_b = lhs_scales[block_idx + 1];
555 let row_scale_c = lhs_scales[block_idx + 2];
556 let row_scale_d = lhs_scales[block_idx + 3];
557
558 let scale_a = ops.splat(col_scale_a * row_scale_a);
559 let scale_b = ops.splat(col_scale_b * row_scale_b);
560 let scale_c = ops.splat(col_scale_c * row_scale_c);
561 let scale_d = ops.splat(col_scale_d * row_scale_d);
562
563 let scale_ab = ops.select(scale_a, scale_b, lo_half_mask);
564 let scale_cd = ops.select(scale_c, scale_d, lo_half_mask);
565
566 acc[0] = ops.mul_add(float_lo, scale_ab, acc[0]);
567 acc[1] = ops.mul_add(float_hi, scale_cd, acc[1]);
568 }
569 8 => {
570 let block_idx = vblock_idx * 8;
571
572 let scales: [<I::Isa as Isa>::F32; 8] = std::array::from_fn(|i| {
573 ops.splat(col_scales[block_idx + i] * lhs_scales[block_idx + i])
574 });
575
576 let scales_01 = ops.select(scales[0], scales[1], lo_quad_mask);
580 let scales_23 = ops.select(scales[2], scales[3], lo_three_quads_mask);
581 let scales_0123 = ops.select(scales_01, scales_23, lo_half_mask);
582 let scales_45 = ops.select(scales[4], scales[5], lo_quad_mask);
583 let scales_67 = ops.select(scales[6], scales[7], lo_three_quads_mask);
584 let scales_4567 = ops.select(scales_45, scales_67, lo_half_mask);
585
586 acc[0] = ops.mul_add(float_lo, scales_0123, acc[0]);
587 acc[1] = ops.mul_add(float_hi, scales_4567, acc[1]);
588 }
589 _ => unreachable!(),
590 }
591 }
592
593 let acc = ops.add(acc[0], acc[1]);
594 let mut acc = ops.sum(acc);
595
596 if n_tail_blocks > 0 {
599 let row_tail_blocks = row_vblocks.remainder().chunks_exact(elements_per_block);
600 let col_tail_blocks = col_vblocks.remainder().chunks_exact(elements_per_block / 2);
601 debug_assert_eq!(row_tail_blocks.len(), n_tail_blocks);
602
603 let mut tail_acc = 0.;
604 for (i, (lhs_block, rhs_block)) in row_tail_blocks.zip(col_tail_blocks).enumerate()
605 {
606 let col_scale = col_scales[col_scales.len() - n_tail_blocks + i];
607 let row_scale = lhs_scales[lhs_scales.len() - n_tail_blocks + i];
608 let scale = col_scale * row_scale;
609
610 let mut acc = 0.;
611 for ([x_lo, x_hi], y) in lhs_block.as_chunks::<2>().0.iter().zip(rhs_block) {
612 let y_lo = (y & 0x0F) as i32 - 8;
613 let y_hi = (y >> 4) as i32 - 8;
614 acc += (*x_lo as i32 * y_lo + *x_hi as i32 * y_hi) as f32 * scale;
615 }
616 tail_acc += acc;
617 }
618 acc += tail_acc;
619 }
620
621 out.write(acc);
622 }
623
624 unsafe { out.assume_init() }
625 }
626}
627
628impl<'a> SimdInt8DotOp for VecDotMatrixQuant<'a> {
629 type Output = &'a mut [f32];
630
631 #[inline(always)]
632 fn eval<I: Int8DotIsa>(self, isa: I) -> Self::Output {
633 let u8_ops = isa.isa().u8();
634
635 let elements_per_vec = u8_ops.len() * 2;
639
640 let scales_per_vblock = (elements_per_vec / self.rhs.elements_per_block()).max(1);
644
645 match scales_per_vblock {
646 1 => self.eval_impl::<I, 1>(isa),
647 2 => self.eval_impl::<I, 2>(isa),
648 4 => self.eval_impl::<I, 4>(isa),
649 8 => self.eval_impl::<I, 8>(isa),
650 _ => unreachable!("unsupported scales_per_vblock"),
651 }
652 }
653}
654
655#[derive(Copy, Clone)]
660pub struct BlockQuantizedMatrix<'a, T> {
661 quant: Contiguous<NdTensorView<'a, u8, 3>>,
663
664 scales: Contiguous<NdTensorView<'a, T, 2>>,
666
667 bits: u8,
671}
672
673impl<'a, T: Copy> BlockQuantizedMatrix<'a, T> {
674 pub const MIN_BLOCK_SIZE: usize = 16;
676
677 pub fn new(
687 quant: Contiguous<NdTensorView<'a, u8, 3>>,
688 scales: Contiguous<NdTensorView<'a, T, 2>>,
689 bits: u8,
690 ) -> Result<Self, BlockQuantizedError> {
691 if !matches!(bits, 4 | 8) {
695 return Err(BlockQuantizedError::UnsupportedElementSize);
696 }
697 let n_elem = 8 / bits;
698
699 let [_batch, _k_blocks, block_bytes] = quant.shape();
700
701 let block_size = block_bytes * n_elem.as_usize();
702 if !block_size.is_power_of_two() || block_size < Self::MIN_BLOCK_SIZE {
703 return Err(BlockQuantizedError::UnsupportedBlockSize);
704 }
705
706 Ok(Self {
707 quant,
708 scales,
709 bits,
710 })
711 }
712
713 pub fn rows(&self) -> usize {
715 self.blocks_per_column() * self.elements_per_block()
716 }
717
718 pub fn cols(&self) -> usize {
720 self.quant.size(0)
721 }
722
723 pub(crate) fn slice(&self, col_range: Range<usize>) -> BlockQuantizedMatrix<'a, T> {
725 BlockQuantizedMatrix {
726 quant: Contiguous::new(self.quant.slice(col_range.clone())).unwrap(),
727 scales: Contiguous::new(self.scales.slice(col_range)).unwrap(),
728 bits: self.bits,
729 }
730 }
731
732 pub fn n_bits(&self) -> u8 {
734 self.bits
735 }
736
737 pub(crate) fn blocks_per_column(&self) -> usize {
739 self.quant.size(1)
740 }
741
742 pub(crate) fn elements_per_block(&self) -> usize {
743 (self.bytes_per_block() * 8) / self.bits.as_usize()
744 }
745
746 pub(crate) fn bytes_per_block(&self) -> usize {
747 self.quant.size(2)
748 }
749
750 pub(crate) fn column_data(
752 &self,
753 col: usize,
754 start_block: usize,
755 n_blocks: usize,
756 ) -> Option<&[u8]> {
757 if self.quant.size(1) < start_block + n_blocks {
758 return None;
759 }
760 let offset = self.quant.offset([col, start_block, 0])?;
761 let len = self.quant.size(2);
762 Some(&self.quant.data()[offset..offset + len * n_blocks])
763 }
764
765 pub(crate) fn column_scales(
767 &self,
768 col: usize,
769 start_block: usize,
770 n_blocks: usize,
771 ) -> Option<&[T]> {
772 if self.scales.size(1) < start_block + n_blocks {
773 return None;
774 }
775 let offset = self.scales.offset([col, start_block])?;
776 Some(&self.scales.data()[offset..offset + n_blocks])
777 }
778}
779
780pub const fn nbit_zero_point(n_bits: u8) -> i16 {
788 assert!(n_bits >= 2 && n_bits <= 8);
789 1 << (n_bits - 1)
790}
791
792#[cfg(test)]
793pub fn pack_4bit_elements(vals: &[i8], zero_point: i8) -> Vec<u8> {
794 let (chunks, tail) = vals.as_chunks::<2>();
795 assert!(tail.is_empty());
796 chunks
797 .iter()
798 .copied()
799 .map(|[even, odd]| {
800 let lo = (even + zero_point) as u8;
801 let hi = (odd + zero_point) as u8;
802 (lo & 0x0F) | (hi << 4)
803 })
804 .collect()
805}
806
807#[cfg(test)]
808mod tests {
809 use std::mem::MaybeUninit;
810
811 use rten_tensor::rng::XorShiftRng;
812 use rten_tensor::test_util::{expect_equal, expect_equal_with_tolerance};
813 use rten_tensor::{AsView, Contiguous, Layout, NdTensor, NdTensorView};
814 use rten_testing::TestCases;
815
816 use super::{
817 BlockQuantizedGemm, BlockQuantizedMatrix, ComputeMode, nbit_zero_point, pack_4bit_elements,
818 quantize,
819 };
820
821 fn reference_gemm_f32_with_block_quantized_rhs(
822 lhs: NdTensorView<f32, 2>,
823 rhs: NdTensorView<u8, 3>,
824 rhs_scales: NdTensorView<f32, 2>,
825 ) -> NdTensor<f32, 2> {
826 let [m, k] = lhs.shape();
827 let [n, _k_blocks, block_size] = rhs.shape();
828 let elems_per_block = block_size * 2;
829 let zero_point = 8;
830
831 let mut out = NdTensor::zeros([m, n]);
832
833 for row in 0..m {
834 for col in 0..n {
835 let mut acc = 0.;
836 for ki in 0..k {
837 let k_block = ki / elems_per_block;
838 let block_idx = ki % elems_per_block;
839 let scale = rhs_scales[[col, k_block]];
840
841 let byte = rhs[[col, k_block, block_idx / 2]];
842 let elem = if ki % 2 == 0 { byte & 0x0F } else { byte >> 4 };
843 let dequantized_elem = (elem as i32 - zero_point) as f32 * scale;
844
845 acc += lhs[[row, ki]] * dequantized_elem;
846 }
847 out[[row, col]] = acc;
848 }
849 }
850
851 out
852 }
853
854 #[test]
855 fn test_block_quantized_matrix() {
856 let zero_point = nbit_zero_point(4) as i8;
857 let elems: Vec<i8> = (-8..8).cycle().take(256).collect();
858 let packed = pack_4bit_elements(&elems, zero_point);
859
860 let block_bytes = 16;
861 let cols = 4;
862 let k_blocks = 2;
863 let n_bits = 4;
864
865 let quants = NdTensor::from_data([cols, k_blocks, block_bytes], packed);
866 let scales = NdTensorView::from_data([cols, k_blocks], &[1., 2., 3., 4., 5., 6., 7., 8.]);
867 let mat = BlockQuantizedMatrix::new(
868 Contiguous::new(quants.view()).unwrap(),
869 Contiguous::new(scales.view()).unwrap(),
870 n_bits,
871 )
872 .unwrap();
873
874 assert_eq!(mat.rows(), 64);
875 assert_eq!(mat.cols(), cols);
876 assert_eq!(mat.elements_per_block(), 32);
877 assert_eq!(mat.blocks_per_column(), k_blocks);
878
879 assert_eq!(
880 mat.column_data(0, 0, 1).unwrap(),
881 pack_4bit_elements(&elems[..32], zero_point)
882 );
883 assert_eq!(mat.column_scales(0, 0, 2), Some([1.0, 2.0].as_slice()));
884 assert_eq!(
885 mat.column_data(3, 1, 1).unwrap(),
886 pack_4bit_elements(&elems[256 - 32..], zero_point)
887 );
888 assert_eq!(mat.column_scales(3, 1, 1), Some([8.0].as_slice()));
889
890 assert_eq!(mat.column_data(4, 1, 1), None);
892 assert_eq!(mat.column_data(3, 2, 1), None);
894 }
895
896 const BLOCK_SIZES: [usize; 5] = [16, 32, 64, 128, 256];
902
903 #[test]
904 fn test_quantize() {
905 let mut rng = XorShiftRng::new(1234);
906 for block_size in BLOCK_SIZES {
907 let batch = 2;
908 let rows = 3;
909 let n_blocks = 2;
910 let k = block_size * n_blocks;
911
912 let mut data = NdTensor::rand([batch, rows, k], &mut rng);
913 data.apply(|x| (x - 0.5) * 2.);
915
916 let (quantized, scales) = quantize(Contiguous::new(data.view()).unwrap(), block_size);
917
918 let dequantized: Vec<f32> = quantized
919 .inner_iter::<1>()
920 .zip(scales.iter())
921 .flat_map(|(block, scale)| block.iter().map(move |x| *x as f32 * scale))
922 .collect();
923
924 let max_err = data
925 .iter()
926 .zip(dequantized)
927 .map(|(x, y)| (x - y).abs())
928 .fold(f32::MIN, |max, x| x.max(max));
929 let threshold = 0.004;
930
931 assert!(
932 max_err <= threshold,
933 "max_err {max_err} exceeds {threshold}"
934 );
935 }
936 }
937
938 #[test]
939 fn test_block_quantized_gemm() {
940 #[derive(Clone, Debug)]
941 struct Case {
942 block_size: usize,
943 n_rows: usize,
944 n_cols: usize,
945 n_blocks: usize,
946 compute: ComputeMode,
947 tolerance: Option<f32>,
948 }
949
950 let max_vblock_size = 128;
952
953 let mut cases = Vec::new();
954
955 for block_size in BLOCK_SIZES {
956 cases.push(Case {
957 n_rows: 1,
958 n_cols: 3,
959 n_blocks: (max_vblock_size / block_size).max(1),
960 block_size,
961 compute: ComputeMode::Float,
962 tolerance: None,
963 });
964 }
965
966 cases.push(Case {
968 n_rows: 1,
969 n_cols: 1,
970 block_size: 16,
972 n_blocks: (max_vblock_size / 16) + 1,
974 compute: ComputeMode::Float,
975 tolerance: None,
976 });
977
978 for (block_size, atol) in [(16, 0.1), (32, 0.1), (64, 0.2), (128, 0.2), (256, 0.3)] {
980 cases.push(Case {
981 n_rows: 1,
982 n_cols: 3,
983 block_size,
984 n_blocks: (max_vblock_size / block_size).max(1),
985 compute: ComputeMode::Int8,
986 tolerance: Some(atol),
987 });
988 }
989
990 cases.push(Case {
992 n_rows: 1,
993 n_cols: 1,
994 block_size: 16,
996 n_blocks: (max_vblock_size / 16) + 1,
998 compute: ComputeMode::Int8,
999 tolerance: Some(0.1),
1000 });
1001
1002 cases.push(Case {
1005 n_rows: 1,
1006 n_cols: 1,
1007 block_size: 32,
1008 n_blocks: 0,
1009 compute: ComputeMode::Int8,
1010 tolerance: None,
1011 });
1012 cases.push(Case {
1013 n_rows: 1,
1014 n_cols: 1,
1015 block_size: 32,
1016 n_blocks: 0,
1017 compute: ComputeMode::Float,
1018 tolerance: None,
1019 });
1020
1021 cases.test_each_clone(|case| {
1022 let Case {
1023 n_rows,
1024 n_cols,
1025 n_blocks,
1026 block_size,
1027 compute,
1028 tolerance,
1029 } = case;
1030
1031 let mut rng = XorShiftRng::new(1234);
1032
1033 let gemm = BlockQuantizedGemm::new().with_compute(compute);
1034 let lhs = NdTensor::<f32, 2>::rand([n_rows, n_blocks * block_size], &mut rng);
1035 let rhs_data = NdTensor::<u8, 3>::rand([n_cols, n_blocks, block_size / 2], &mut rng);
1036 let rhs_scales = NdTensor::<f32, 2>::rand([n_cols, n_blocks], &mut rng);
1037 let bqm = BlockQuantizedMatrix::new(
1038 Contiguous::new(rhs_data.view()).unwrap(),
1039 Contiguous::new(rhs_scales.view()).unwrap(),
1040 4,
1041 )
1042 .unwrap();
1043
1044 let expected = reference_gemm_f32_with_block_quantized_rhs(
1045 lhs.view(),
1046 rhs_data.view(),
1047 rhs_scales.view(),
1048 );
1049
1050 let mut out = Vec::with_capacity(n_cols);
1051
1052 out.spare_capacity_mut().fill(MaybeUninit::new(f32::NAN));
1055
1056 let result = gemm
1057 .batched_gemm_uninit(
1058 out.spare_capacity_mut(),
1059 lhs.reshaped([1, lhs.size(0), lhs.size(1)]).view(),
1060 bqm,
1061 )
1062 .unwrap();
1063 let result_matrix = NdTensorView::from_data([1, result.len()], result.as_ref());
1064
1065 if let Some(atol) = tolerance {
1066 let rtol = 0.;
1067 expect_equal_with_tolerance(&result_matrix, &expected.view(), atol, rtol).unwrap();
1068 } else {
1069 expect_equal(&result_matrix, &expected.view()).unwrap();
1070 }
1071 });
1072 }
1073}