Skip to main content

cubecl_std/quant/
view.rs

1use std::marker::PhantomData;
2
3use super::*;
4use crate::tensor::{
5    View, ViewExpand, ViewOperations, ViewOperationsExpand,
6    launch::{ScaleBindings, ScaleBindingsCompilationArg, ViewArg, ViewCompilationArg},
7    layout::Coordinates,
8};
9use cubecl::prelude::*;
10use cubecl_common::{
11    e2m1x2, e4m3, e5m2,
12    quant::scheme::{QuantScheme, QuantStore, QuantValue, ScaleDtype},
13    ue8m0,
14};
15use cubecl_core::{
16    self as cubecl, define_size,
17    ir::{ElemType, FloatKind, VectorSize},
18    prelude::barrier::Barrier,
19    unexpanded,
20};
21use half::{bf16, f16};
22
23/// The part of each read's scale the caller already knew when it built the view, held in one
24/// register rather than read per position; whatever is not known up front is read through the
25/// scales view. The discriminant is comptime, so each variant compiles its own kernel with
26/// nothing of the others in it.
27#[derive(Clone, Copy, CubeType, CubeLaunch)]
28#[expand(derive(Clone, Copy))]
29pub enum KnownScale {
30    /// Nothing known up front: each read looks its whole scale up at its position.
31    None,
32    /// The per-tensor scale of a two-level scheme; each read still looks its block scale up and
33    /// multiplies this in.
34    Global(f32),
35    /// The whole scale, whatever the caller multiplied into it. The scales view is never read:
36    /// its address arithmetic and its load leave the kernel.
37    Whole(f32),
38}
39
40#[cube]
41impl KnownScale {
42    /// The scale a value dequantizes against once `scale`, looked up for its position, meets what
43    /// this register holds.
44    pub fn effective(&self, scale: f32) -> f32 {
45        #[comptime]
46        match self {
47            KnownScale::None => scale,
48            KnownScale::Global(global) => global * scale,
49            KnownScale::Whole(whole) => *whole,
50        }
51    }
52}
53
54/// View that dequantizes after loads. Scales layout should take values coordinates and map them
55/// to the corresponding scale.
56///
57/// # Warning
58/// Assumes only one scale maps to a single load. Adjust vector size of values or block size to ensure
59/// this.
60/// Must ensure `block_size.is_multiple_of(vector_size * scheme.num_quants())`.
61#[expect(dead_code, reason = "only used in expand")]
62#[derive(CubeType, CubeLaunch, Clone)]
63pub struct QuantizedView<
64    'a,
65    Q: Scalar,
66    NQ: Size,
67    S: Scalar,
68    F: Numeric,
69    NF: Size,
70    C: Coordinates + 'static,
71> {
72    values: View<'a, Vector<Q, NQ>, C>,
73    scales: View<'a, S, C>,
74    known_scale: KnownScale,
75    /// A lookup scheme's `2^bits`-entry table, present exactly under
76    /// [`QuantMode::Lookup`](cubecl_common::quant::scheme::QuantMode).
77    table: ComptimeOption<Box<[f32]>>,
78    #[cube(comptime)]
79    scheme: QuantScheme,
80    #[cube(comptime)]
81    _ty: PhantomData<(F, NF)>,
82}
83
84#[cube]
85impl<'a, Q: Scalar, NQ: Size, S: Scalar, F: Numeric, NF: Size, C: Coordinates + 'static>
86    QuantizedView<'a, Q, NQ, S, F, NF, C>
87{
88    /// A view reading every scale per position through `scales`.
89    ///
90    /// Takes a one-level scheme: the per-tensor scale of a two-level one never rides a signature
91    /// here, it is either bound at launch or already held by a caller as a [`KnownScale`] passed
92    /// to [`new_with_known_scale`](Self::new_with_known_scale).
93    pub fn new(
94        values: View<'a, Vector<Q, NQ>, C>,
95        scales: View<'a, S, C>,
96        table: ComptimeOption<Box<[f32]>>,
97        #[comptime] scheme: QuantScheme,
98    ) -> Self {
99        comptime!(crate::quant::check_scale_bindings(&scheme, 1));
100        QuantizedView::<'a, Q, NQ, S, F, NF, C> {
101            values,
102            scales,
103            known_scale: KnownScale::new_None(),
104            table,
105            scheme,
106            _ty: PhantomData,
107        }
108    }
109
110    /// [`new`](Self::new) with whatever the caller already knows of the scale, in whichever
111    /// [`KnownScale`] form it holds it. Only a caller that knows what its reads share can say
112    /// so, which is why this exists on the cube side. Reads assert the register agrees with the
113    /// scheme.
114    pub fn new_with_known_scale(
115        values: View<'a, Vector<Q, NQ>, C>,
116        scales: View<'a, S, C>,
117        known_scale: KnownScale,
118        table: ComptimeOption<Box<[f32]>>,
119        #[comptime] scheme: QuantScheme,
120    ) -> Self {
121        QuantizedView::<'a, Q, NQ, S, F, NF, C> {
122            values,
123            scales,
124            known_scale,
125            table,
126            scheme,
127            _ty: PhantomData,
128        }
129    }
130}
131
132impl<'a, Q: Scalar, NQ: Size, S: Scalar, F: Numeric, NF: Size, C: Coordinates + 'static>
133    QuantizedView<'a, Q, NQ, S, F, NF, C>
134{
135    pub fn view(self) -> View<'a, Vector<F, NF>, C> {
136        unexpanded!()
137    }
138
139    pub fn __expand_view(
140        scope: &Scope,
141        this: QuantizedViewExpand<'a, Q, NQ, S, F, NF, C>,
142    ) -> ViewExpand<'a, Vector<F, NF>, C> {
143        this.__expand_view_method(scope)
144    }
145}
146
147impl<'a, Q: Scalar, NQ: Size, S: Scalar, F: Numeric, NF: Size, C: Coordinates + 'static>
148    QuantizedViewExpand<'a, Q, NQ, S, F, NF, C>
149{
150    pub fn new(
151        values: ViewExpand<'a, Vector<Q, NQ>, C>,
152        scales: ViewExpand<'a, S, C>,
153        known_scale: KnownScaleExpand,
154        table: ComptimeOptionExpand<Box<[f32]>>,
155        scheme: QuantScheme,
156    ) -> Self {
157        QuantizedViewExpand::<'a, Q, NQ, S, F, NF, C> {
158            values,
159            scales,
160            known_scale,
161            table,
162            scheme,
163            _ty: PhantomData,
164        }
165    }
166
167    /// Dequantize `value` with the effective scale this view assigns it, reading the per-position
168    /// scale through `read_scale` unless the register holds the whole scale.
169    fn dequant(
170        &self,
171        scope: &Scope,
172        value: NativeExpand<Vector<Q, NQ>>,
173        read_scale: impl FnOnce(&Scope) -> NativeExpand<S>,
174    ) -> NativeExpand<Vector<F, NF>> {
175        // The reading variants are where the register can disagree with the scheme: the static
176        // constructors take a scheme without inspecting it. A whole scale stands for whatever the
177        // caller multiplied into it, so the scheme says nothing about it.
178        check_table_bindings(&self.scheme, self.table.is_some());
179        match self.known_scale {
180            KnownScaleExpand::None => {
181                assert!(
182                    self.scheme.num_levels() == 1,
183                    "every scale is read from the scales view, but {:?} has a per-tensor scale nothing multiplies in",
184                    self.scheme,
185                );
186                let scale = read_scale(scope);
187                dequantize_aligned::expand::<Q, S, F, NQ, NF>(
188                    scope,
189                    value,
190                    scale,
191                    self.table.clone(),
192                    self.scheme,
193                )
194            }
195            KnownScaleExpand::Global(global_scale) => {
196                assert!(
197                    self.scheme.num_levels() > 1,
198                    "an global scale rides in a register, but {:?} has no per-tensor level over its blocks it could hold",
199                    self.scheme,
200                );
201                check_global_levels(&self.scheme);
202                let scale = read_scale(scope);
203                let scale = multiply_global_scale::expand::<S>(scope, global_scale, scale);
204                dequantize_aligned_wide::expand::<Q, F, NQ, NF>(
205                    scope,
206                    value,
207                    scale,
208                    self.table.clone(),
209                    self.scheme,
210                )
211            }
212            KnownScaleExpand::Whole(scale) => dequantize_aligned_wide::expand::<Q, F, NQ, NF>(
213                scope,
214                value,
215                scale,
216                self.table.clone(),
217                self.scheme,
218            ),
219        }
220    }
221
222    pub fn __expand_view_method(self, scope: &Scope) -> ViewExpand<'a, Vector<F, NF>, C> {
223        ViewExpand::new(scope, self)
224    }
225}
226
227impl<'a, Q: Scalar, NQ: Size, S: Scalar, F: Numeric, NF: Size, C: Coordinates + 'static> Vectorized
228    for QuantizedView<'a, Q, NQ, S, F, NF, C>
229{
230}
231impl<'a, Q: Scalar, NQ: Size, S: Scalar, F: Numeric, NF: Size, C: Coordinates + 'static>
232    VectorizedExpand for QuantizedViewExpand<'a, Q, NQ, S, F, NF, C>
233{
234    fn __expand_vector_size_method(&self, scope: &Scope) -> VectorSize {
235        self.values.__expand_vector_size_method(scope) * self.scheme.num_quants()
236    }
237}
238
239impl<'a, Q: Scalar, NQ: Size, S: Scalar, F: Numeric, NF: Size, C: Coordinates + 'static>
240    ViewOperations<Vector<F, NF>, C> for QuantizedView<'a, Q, NQ, S, F, NF, C>
241{
242}
243
244impl<'a, Q: Scalar, NQ: Size, S: Scalar, F: Numeric, NF: Size, C: Coordinates + 'static>
245    ViewOperationsExpand<Vector<F, NF>, C> for QuantizedViewExpand<'a, Q, NQ, S, F, NF, C>
246{
247    fn __expand_read_method(
248        &self,
249        scope: &Scope,
250        pos: <C>::ExpandType,
251    ) -> NativeExpand<Vector<F, NF>> {
252        let value = self.values.clone().__expand_read_method(scope, pos.clone());
253        self.dequant(scope, value, |scope| {
254            self.scales.clone().__expand_read_method(scope, pos)
255        })
256    }
257
258    fn __expand_read_checked_method(
259        &self,
260        scope: &Scope,
261        pos: <C>::ExpandType,
262    ) -> NativeExpand<Vector<F, NF>> {
263        let value = self
264            .values
265            .clone()
266            .__expand_read_checked_method(scope, pos.clone());
267        self.dequant(scope, value, |scope| {
268            self.scales.clone().__expand_read_checked_method(scope, pos)
269        })
270    }
271
272    fn __expand_read_masked_method(
273        &self,
274        scope: &Scope,
275        pos: <C>::ExpandType,
276        mask_value: NativeExpand<Vector<F, NF>>,
277    ) -> NativeExpand<Vector<F, NF>> {
278        let value = self
279            .values
280            .clone()
281            .__expand_read_checked_method(scope, pos.clone());
282        let in_bounds = self.__expand_is_in_bounds_method(scope, pos.clone());
283
284        let value = self.dequant(scope, value, |scope| {
285            self.scales.clone().__expand_read_checked_method(scope, pos)
286        });
287        select::expand::<Vector<F, NF>>(scope, in_bounds, value, mask_value)
288    }
289
290    fn __expand_read_unchecked_method(
291        &self,
292        scope: &Scope,
293        pos: <C>::ExpandType,
294    ) -> NativeExpand<Vector<F, NF>> {
295        let value = self
296            .values
297            .clone()
298            .__expand_read_unchecked_method(scope, pos.clone());
299        self.dequant(scope, value, |scope| {
300            self.scales
301                .clone()
302                .__expand_read_unchecked_method(scope, pos)
303        })
304    }
305
306    fn __expand_as_linear_slice_method(
307        &self,
308        _scope: &Scope,
309        _pos: <C>::ExpandType,
310        _end: <C>::ExpandType,
311    ) -> &SliceExpand<Vector<F, NF>> {
312        panic!("Can't create raw slice for quantized view")
313    }
314
315    fn __expand_shape_method(&self, scope: &Scope) -> <C>::ExpandType {
316        self.values.clone().__expand_shape_method(scope)
317    }
318
319    fn __expand_is_in_bounds_method(
320        &self,
321        scope: &Scope,
322        pos: C::ExpandType,
323    ) -> NativeExpand<bool> {
324        self.values.clone().__expand_is_in_bounds_method(scope, pos)
325    }
326
327    fn __expand_tensor_map_load_method(
328        &self,
329        _scope: &Scope,
330        _barrier: &NativeExpand<Barrier>,
331        _shared_memory: &mut SliceExpand<Vector<F, NF>>,
332        _pos: C::ExpandType,
333    ) {
334        panic!("Can't use tensor map functions on quantized view");
335    }
336}
337
338/// Storage (values) vector size: the float vector size divided by `num_quants`. Asserts the float
339/// vector size is a multiple of `num_quants`, so a violation reports clearly here, not a cryptic cast error.
340fn quant_vector_size_q(vector_size: usize, num_quants: usize) -> usize {
341    assert!(
342        vector_size >= num_quants && vector_size.is_multiple_of(num_quants),
343        "quantized view float vector size {vector_size} must be a positive multiple of num_quants {num_quants}"
344    );
345    vector_size / num_quants
346}
347
348/// Register the per-tensor scale binding. Registered as f32 to match the element type
349/// [`expand_known_scale`] reads it back with.
350fn register_global_scale<R: Runtime>(
351    global_scale: Option<BufferArg<R>>,
352    launcher: &mut KernelLauncher<R>,
353) -> Option<BufferCompilationArg> {
354    global_scale.map(|global_scale| <[f32] as LaunchArg>::register(global_scale, launcher))
355}
356
357/// Register the lookup table's binding, checking it against the scheme so the two cannot
358/// register apart. Registered as f32 to match the element type [`expand_table`] reads it back
359/// with.
360fn register_table<R: Runtime>(
361    table: Option<BufferArg<R>>,
362    scheme: &QuantScheme,
363    launcher: &mut KernelLauncher<R>,
364) -> Option<BufferCompilationArg> {
365    check_table_bindings(scheme, table.is_some());
366    table.map(|table| {
367        // The mask bounds every index to `2^bits`, so a shorter table reads out of bounds and a
368        // longer one was built for another width; both decode as garbage, so both are refused
369        // here, the one place that holds the buffer and the scheme together on the host.
370        let entries = 1usize << scheme.size_bits_value();
371        assert_eq!(
372            table.len(),
373            entries,
374            "a {}-bit lookup scheme indexes a table of exactly {entries} entries",
375            scheme.size_bits_value()
376        );
377        <[f32] as LaunchArg>::register(table, launcher)
378    })
379}
380
381/// Expand the lookup table into the scope the view is built in, checking it against the scheme so
382/// the two cannot expand apart. Unlike the per-tensor scale this stays a buffer: the field read
383/// from each packed word picks the entry, so there is nothing to hoist.
384fn expand_table(
385    table: Option<&BufferCompilationArg>,
386    scheme: &QuantScheme,
387    builder: &mut KernelBuilder,
388) -> ComptimeOptionExpand<Box<[f32]>> {
389    check_table_bindings(scheme, table.is_some());
390    match table {
391        Some(table) => {
392            ComptimeOptionExpand::Some(<Box<[f32]> as LaunchArg>::expand(table, builder))
393        }
394        None => ComptimeOptionExpand::None,
395    }
396}
397
398/// The known scale a launch's bindings expand to.
399///
400/// An global scale is read once for the whole kernel: it is a single value for the entire tensor,
401/// and a read per element would be a global load the optimizer cannot hoist back out of a loop.
402/// Reading it as f32 is what keeps the two scales multiplying in f32 later, since a block scale
403/// alone can overflow a narrow `F`.
404fn expand_known_scale(
405    global_scale: Option<&BufferCompilationArg>,
406    builder: &mut KernelBuilder,
407) -> KnownScaleExpand {
408    match global_scale {
409        Some(global_scale) => {
410            let buffer = <[f32] as LaunchArg>::expand(global_scale, builder);
411            let pos = NativeExpand::<usize>::from_lit(&builder.scope, 0);
412            KnownScaleExpand::Global(*buffer.__expand_index_method(&builder.scope, pos))
413        }
414        None => KnownScaleExpand::None,
415    }
416}
417
418struct ExpandDynamic<'a, E: Numeric, N: Size, C: Coordinates + 'static> {
419    values: &'a ViewCompilationArg<C>,
420    scales: &'a ScaleBindingsCompilationArg<C>,
421    scheme: QuantScheme,
422    builder: &'a mut KernelBuilder,
423    _ty: PhantomData<(E, N)>,
424}
425
426impl<'a, E: Numeric, N: Size, C: Coordinates + 'static> RunWithQuantType
427    for ExpandDynamic<'a, E, N, C>
428{
429    type Output = ViewExpand<'static, Vector<E, N>, C>;
430
431    fn execute<Q: Scalar, S: Scalar>(self) -> Self::Output {
432        define_size!(NQ);
433
434        let vector_size = N::__expand_value(&self.builder.scope);
435        let vector_size_q = quant_vector_size_q(vector_size, self.scheme.num_quants());
436        self.builder.scope.register_size::<NQ>(vector_size_q);
437
438        check_scale_bindings(&self.scheme, self.scales.len());
439
440        let values = View::<Vector<Q, NQ>, C>::expand(self.values, self.builder);
441        let scales = View::<S, C>::expand(&self.scales.inner, self.builder);
442        let known_scale = expand_known_scale(self.scales.global_scale.as_ref(), self.builder);
443        let table = expand_table(self.scales.table.as_ref(), &self.scheme, self.builder);
444        let view = QuantizedViewExpand::new(values, scales, known_scale, table, self.scheme);
445        ViewExpand::new(&self.builder.scope, view)
446    }
447}
448
449pub(crate) struct RegisterDynamic<'a, E: CubePrimitive, C: Coordinates + 'static, R: Runtime> {
450    pub values: ViewArg<C, R>,
451    pub scales: ScaleBindings<C, R>,
452    pub scheme: QuantScheme,
453    pub launcher: &'a mut KernelLauncher<R>,
454    pub _ty: PhantomData<E>,
455}
456
457impl<'a, E: CubePrimitive, C: Coordinates + 'static, R: Runtime> RunWithQuantType
458    for RegisterDynamic<'a, E, C, R>
459{
460    type Output = ViewCompilationArg<C>;
461
462    fn execute<Q: Scalar, S: Scalar>(self) -> Self::Output {
463        define_size!(NQ);
464
465        self.launcher.with_scope(|scope| {
466            let vector_size_q =
467                quant_vector_size_q(E::__expand_vector_size(scope), self.scheme.num_quants());
468            scope.register_size::<NQ>(vector_size_q);
469        });
470
471        check_scale_bindings(&self.scheme, self.scales.len());
472
473        let values = View::<Vector<Q, NQ>, C>::register(self.values, self.launcher);
474        let inner = View::<S, C>::register(*self.scales.inner, self.launcher);
475        let global_scale = register_global_scale(self.scales.global_scale, self.launcher);
476        let table = register_table(self.scales.table, &self.scheme, self.launcher);
477        ViewCompilationArg::Quantized {
478            values: Box::new(values),
479            scales: ScaleBindingsCompilationArg {
480                inner: Box::new(inner),
481                global_scale,
482                table,
483            },
484            scheme: self.scheme,
485        }
486    }
487}
488
489/// Run a function with the quantization storage type and scale. Useful when concrete types are
490/// required but aren't available, and only the dynamic schema is known.
491pub fn run_with_quant_type<F: RunWithQuantType>(func: F, scheme: QuantScheme) -> F::Output {
492    fn run_with_q<F: RunWithQuantType, Q: Scalar>(func: F, scheme: QuantScheme) -> F::Output {
493        match scheme.scale_dtype() {
494            ScaleDtype::F32 => func.execute::<Q, f32>(),
495            ScaleDtype::F16 => func.execute::<Q, f16>(),
496            ScaleDtype::BF16 => func.execute::<Q, bf16>(),
497            ScaleDtype::UE8M0 => func.execute::<Q, ue8m0>(),
498            ScaleDtype::UE4M3 => func.execute::<Q, e4m3>(),
499        }
500    }
501
502    let run_q = match scheme.store {
503        QuantStore::Native => match scheme.value {
504            QuantValue::Q8F => run_with_q::<F, i8>,
505            QuantValue::Q8S => run_with_q::<F, i8>,
506            QuantValue::E5M2 => run_with_q::<F, e5m2>,
507            QuantValue::E4M3 => run_with_q::<F, e4m3>,
508            QuantValue::Q4F
509            | QuantValue::Q4S
510            | QuantValue::Q2F
511            | QuantValue::Q2S
512            | QuantValue::E2M1 => {
513                panic!("Sub-byte quantization can't be native")
514            }
515        },
516        QuantStore::PackedU32(_) => run_with_q::<F, u32>,
517        QuantStore::PackedNative(_) => run_with_q::<F, e2m1x2>,
518    };
519    run_q(func, scheme)
520}
521
522/// Dynamically expand based on the quantization scheme. Ugly, but the only way to fully hide the
523/// quantization from the kernel using the view.
524pub(crate) fn expand_dynamic<E: CubePrimitive, C: Coordinates + 'static>(
525    values: &ViewCompilationArg<C>,
526    scales: &ScaleBindingsCompilationArg<C>,
527    scheme: QuantScheme,
528    builder: &mut KernelBuilder,
529) -> ViewExpand<'static, E, C> {
530    use core::mem::transmute as t;
531
532    // To specify tighter trait bounds
533    fn expand_dynamic_f<F: Numeric, NF: Size, C: Coordinates + 'static>(
534        values: &ViewCompilationArg<C>,
535        scales: &ScaleBindingsCompilationArg<C>,
536        scheme: QuantScheme,
537        builder: &mut KernelBuilder,
538    ) -> ViewExpand<'static, Vector<F, NF>, C> {
539        let func = ExpandDynamic {
540            values,
541            scales,
542            scheme,
543            builder,
544            _ty: PhantomData::<(F, NF)>,
545        };
546        run_with_quant_type(func, scheme)
547    }
548
549    define_size!(NF);
550
551    let vector_size = E::__expand_vector_size(builder);
552
553    builder.scope.register_size::<NF>(vector_size);
554
555    #[allow(clippy::missing_transmute_annotations)]
556    unsafe {
557        match E::Scalar::elem_type(builder) {
558            ElemType::Float(ty) => match ty {
559                FloatKind::F16 => t(expand_dynamic_f::<f16, NF, C>(
560                    values, scales, scheme, builder,
561                )),
562                FloatKind::BF16 => t(expand_dynamic_f::<bf16, NF, C>(
563                    values, scales, scheme, builder,
564                )),
565                FloatKind::Flex32 => t(expand_dynamic_f::<flex32, NF, C>(
566                    values, scales, scheme, builder,
567                )),
568                FloatKind::F32 => t(expand_dynamic_f::<f32, NF, C>(
569                    values, scales, scheme, builder,
570                )),
571                FloatKind::TF32 => t(expand_dynamic_f::<tf32, NF, C>(
572                    values, scales, scheme, builder,
573                )),
574                FloatKind::F64 => t(expand_dynamic_f::<f64, NF, C>(
575                    values, scales, scheme, builder,
576                )),
577                FloatKind::E2M1
578                | FloatKind::E2M1x2
579                | FloatKind::E2M3
580                | FloatKind::E3M2
581                | FloatKind::E4M3
582                | FloatKind::E5M2
583                | FloatKind::UE8M0 => unreachable!("Minifloats don't implement `Float` ops"),
584            },
585            _ => unreachable!("Quantized view should only be used with floats"),
586        }
587    }
588}
589
590#[cfg(test)]
591mod tests {
592    use super::{RunWithQuantType, quant_vector_size_q, run_with_quant_type};
593    use cubecl_common::quant::scheme::{QuantScheme, ScaleDtype};
594    use cubecl_core::prelude::Scalar;
595
596    struct Dispatched;
597
598    impl RunWithQuantType for Dispatched {
599        type Output = bool;
600
601        fn execute<Q: Scalar, S: Scalar>(self) -> bool {
602            true
603        }
604    }
605
606    #[test]
607    fn one_level_scheme_dispatches() {
608        assert!(run_with_quant_type(Dispatched, QuantScheme::default()));
609    }
610
611    /// The per-tensor scale is read through a binding of its own, so it does not change how the
612    /// value and block scale types dispatch.
613    #[test]
614    fn two_level_scheme_dispatches() {
615        let scheme = QuantScheme::default()
616            .per_block([32], ScaleDtype::F32)
617            .per_tensor(ScaleDtype::F32);
618        assert!(run_with_quant_type(Dispatched, scheme));
619    }
620
621    #[test]
622    fn vector_size_q_exact_multiple() {
623        assert_eq!(quant_vector_size_q(8, 8), 1);
624        assert_eq!(quant_vector_size_q(16, 8), 2);
625        assert_eq!(quant_vector_size_q(16, 16), 1);
626    }
627
628    #[test]
629    #[should_panic(expected = "positive multiple of num_quants")]
630    fn vector_size_q_non_multiple_panics() {
631        let _ = quant_vector_size_q(8, 16);
632    }
633}