1use std::str::FromStr;
13
14use tract_nnef::internal::*;
15use tract_nnef::prelude::tract_itertools::Itertools;
16use tract_nnef::ser::{datum_type, tdims};
17use tract_nnef::tract_core::ops::array::MultiBroadcastTo;
18use tract_nnef::tract_core::ops::cast::Cast;
19use tract_nnef::tract_core::ops::change_axes::AxisOp;
20use tract_nnef::tract_core::transform::ModelTransform;
21use tract_nnef::tract_ndarray::{Array2, Ix4, s};
22
23use crate::ops::apply_rope::ApplyRope;
24use crate::ops::dyn_kv_cache::DynKeyValueCache;
25use crate::ops::sdpa::Sdpa;
26
27pub fn register(registry: &mut Registry) {
28 registry.register_dumper(ser_quant_dyn_kv_cache);
29 registry.register_primitive(
30 "tract_transformers_quantized_dyn_kv_cache",
31 &[
32 TypeName::Scalar.tensor().named("input"),
33 TypeName::String.named("name"),
34 TypeName::Integer.named("axis"),
35 TypeName::Integer.named("bits"),
36 TypeName::Integer.named("per_channel"),
37 TypeName::String.named("datum_type"),
38 TypeName::Integer.array().named("past_sequence_shape"),
39 TypeName::Integer.array().named("input_sequence_shape"),
40 ],
41 &[("output", TypeName::Scalar.tensor())],
42 de_quant_dyn_kv_cache,
43 );
44}
45
46#[inline]
50fn max_code(bits: u32) -> f32 {
51 ((1u32 << bits) - 1) as f32
52}
53
54#[inline]
56fn row_bytes(d: usize, bits: u32) -> usize {
57 (d * bits as usize).div_ceil(8)
58}
59
60fn pack_codes(codes: &[u8], bits: u32, out: &mut Vec<u8>) {
63 match bits {
64 8 => out.extend_from_slice(codes),
65 4 => {
66 for pair in codes.chunks(2) {
67 let lo = pair[0] & 0x0F;
68 let hi = pair.get(1).map(|c| c & 0x0F).unwrap_or(0);
69 out.push(lo | (hi << 4));
70 }
71 }
72 _ => unreachable!("bits must be 4 or 8"),
73 }
74}
75
76#[inline]
78fn unpack_code(bytes: &[u8], c: usize, bits: u32) -> u8 {
79 match bits {
80 8 => bytes[c],
81 4 => {
82 let b = bytes[c >> 1];
83 if c & 1 == 0 { b & 0x0F } else { b >> 4 }
84 }
85 _ => unreachable!("bits must be 4 or 8"),
86 }
87}
88
89const SINK_TOKENS: usize = 4;
92
93#[derive(Clone, Debug, Default)]
98struct PerTokenStore {
99 d: usize,
100 bits: u32,
101 row_bytes: usize,
102 sink: Vec<f32>, packed: Vec<u8>,
104 params: Vec<(f32, f32)>,
105}
106
107impl PerTokenStore {
108 fn with_bits(d: usize, bits: u32) -> Self {
109 PerTokenStore { d, bits, row_bytes: row_bytes(d, bits), ..Default::default() }
110 }
111 fn n_sink(&self) -> usize {
112 self.sink.len() / self.d
113 }
114 fn len(&self) -> usize {
115 self.n_sink() + self.params.len()
116 }
117 fn push_token(&mut self, v: &[f32]) {
118 if self.n_sink() < SINK_TOKENS {
119 self.sink.extend_from_slice(v);
120 return;
121 }
122 let lv = max_code(self.bits);
123 let lo = v.iter().copied().fold(f32::INFINITY, f32::min);
124 let hi = v.iter().copied().fold(f32::NEG_INFINITY, f32::max);
125 let scale = if hi > lo { (hi - lo) / lv } else { 1.0 };
126 let codes: Vec<u8> =
127 v.iter().map(|&x| ((x - lo) / scale).round().clamp(0.0, lv) as u8).collect();
128 pack_codes(&codes, self.bits, &mut self.packed);
129 self.params.push((lo, scale));
130 }
131 fn dequant_all(&self) -> Array2<f32> {
132 let (d, rb, bits) = (self.d, self.row_bytes, self.bits);
133 let ns = self.n_sink();
134 let mut out = Array2::<f32>::zeros((self.len(), d));
135 for t in 0..ns {
136 for c in 0..d {
137 out[(t, c)] = self.sink[t * d + c];
138 }
139 }
140 for (i, &(lo, scale)) in self.params.iter().enumerate() {
141 let src = &self.packed[i * rb..i * rb + rb];
142 for c in 0..d {
143 out[(ns + i, c)] = lo + unpack_code(src, c, bits) as f32 * scale;
144 }
145 }
146 out
147 }
148}
149
150const KEY_BLOCK: usize = 32;
153
154#[derive(Clone, Debug, Default)]
159struct BlockChannelStore {
160 d: usize,
161 bits: u32,
162 row_bytes: usize,
163 sink: Vec<f32>, packed: Vec<u8>, block_lo: Vec<f32>, block_scale: Vec<f32>, residual: Vec<f32>, n_res: usize,
169 n_finalized: usize,
170}
171
172impl BlockChannelStore {
173 fn with_bits(d: usize, bits: u32) -> Self {
174 BlockChannelStore { d, bits, row_bytes: row_bytes(d, bits), ..Default::default() }
175 }
176 fn n_sink(&self) -> usize {
177 self.sink.len() / self.d
178 }
179 fn len(&self) -> usize {
180 self.n_sink() + self.n_finalized * KEY_BLOCK + self.n_res
181 }
182 fn push_token(&mut self, k: &[f32]) {
183 if self.n_sink() < SINK_TOKENS {
184 self.sink.extend_from_slice(k);
185 return;
186 }
187 self.residual.extend_from_slice(k);
188 self.n_res += 1;
189 if self.n_res == KEY_BLOCK {
190 self.finalize_block();
191 }
192 }
193 fn finalize_block(&mut self) {
194 let (d, bits) = (self.d, self.bits);
195 let lv = max_code(bits);
196 let mut lo = vec![f32::INFINITY; d];
197 let mut hi = vec![f32::NEG_INFINITY; d];
198 for t in 0..self.n_res {
199 for c in 0..d {
200 let v = self.residual[t * d + c];
201 lo[c] = lo[c].min(v);
202 hi[c] = hi[c].max(v);
203 }
204 }
205 let scale: Vec<f32> =
206 (0..d).map(|c| if hi[c] > lo[c] { (hi[c] - lo[c]) / lv } else { 1.0 }).collect();
207 let mut codes = vec![0u8; d];
208 for t in 0..self.n_res {
209 for c in 0..d {
210 codes[c] =
211 ((self.residual[t * d + c] - lo[c]) / scale[c]).round().clamp(0.0, lv) as u8;
212 }
213 pack_codes(&codes, bits, &mut self.packed);
214 }
215 self.block_lo.extend_from_slice(&lo);
216 self.block_scale.extend_from_slice(&scale);
217 self.residual.clear();
218 self.n_res = 0;
219 self.n_finalized += 1;
220 }
221 fn dequant_all(&self) -> Array2<f32> {
222 let (d, rb, bits) = (self.d, self.row_bytes, self.bits);
223 let ns = self.n_sink();
224 let mut out = Array2::<f32>::zeros((self.len(), d));
225 for t in 0..ns {
226 for c in 0..d {
227 out[(t, c)] = self.sink[t * d + c];
228 }
229 }
230 for bi in 0..self.n_finalized {
231 let lo = &self.block_lo[bi * d..bi * d + d];
232 let sc = &self.block_scale[bi * d..bi * d + d];
233 for ti in 0..KEY_BLOCK {
234 let g = bi * KEY_BLOCK + ti;
235 let src = &self.packed[g * rb..g * rb + rb];
236 for c in 0..d {
237 out[(ns + g, c)] = lo[c] + unpack_code(src, c, bits) as f32 * sc[c];
238 }
239 }
240 }
241 let base = ns + self.n_finalized * KEY_BLOCK;
242 for ti in 0..self.n_res {
243 for c in 0..d {
244 out[(base + ti, c)] = self.residual[ti * d + c];
245 }
246 }
247 out
248 }
249}
250
251#[derive(Clone, Debug)]
253enum HeadCache {
254 Key(BlockChannelStore),
255 Value(PerTokenStore),
256}
257
258impl HeadCache {
259 fn new(per_channel: bool, d: usize, bits: u32) -> Self {
260 if per_channel {
261 HeadCache::Key(BlockChannelStore::with_bits(d, bits))
262 } else {
263 HeadCache::Value(PerTokenStore::with_bits(d, bits))
264 }
265 }
266 fn push(&mut self, row: &[f32]) {
267 match self {
268 HeadCache::Key(c) => c.push_token(row),
269 HeadCache::Value(c) => c.push_token(row),
270 }
271 }
272 fn dequant_all(&self) -> Array2<f32> {
273 match self {
274 HeadCache::Key(c) => c.dequant_all(),
275 HeadCache::Value(c) => c.dequant_all(),
276 }
277 }
278}
279
280#[derive(Clone, Debug, PartialEq, Eq)]
286pub struct QuantizedDynKeyValueCache {
287 pub name: String,
288 pub axis: usize,
289 pub bits: u32,
290 pub per_channel: bool,
291 pub past_sequence_fact: TypedFact,
292 pub input_sequence_fact: TypedFact,
293}
294
295impl Op for QuantizedDynKeyValueCache {
296 fn name(&self) -> StaticName {
297 "QuantizedDynKeyValueCache".to_string().into()
298 }
299 fn info(&self) -> TractResult<Vec<String>> {
300 Ok(vec![format!(
301 "bits={}, per_channel={}, axis={}",
302 self.bits, self.per_channel, self.axis
303 )])
304 }
305 op_as_typed_op!();
306}
307
308impl EvalOp for QuantizedDynKeyValueCache {
309 not_out_of_plan!();
310 fn state(&self, _ctx: &EvalContext) -> TractResult<Option<Box<dyn OpState>>> {
311 Ok(Some(Box::new(QuantizedDynKvCacheState {
312 name: self.name.clone(),
313 axis: self.axis,
314 bits: self.bits,
315 per_channel: self.per_channel,
316 past_sequence_fact: self.past_sequence_fact.clone(),
317 caches: Vec::new(),
318 lead_shape: tvec!(),
319 d: 0,
320 len: 0,
321 })))
322 }
323}
324
325impl TypedOp for QuantizedDynKeyValueCache {
326 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
327 ensure!(inputs.len() == 1);
328 let mut fact = inputs[0].without_value();
329 fact.shape.set(
330 self.axis,
331 self.past_sequence_fact.shape.dims()[self.axis].clone()
332 + self.input_sequence_fact.shape.dims()[self.axis].clone(),
333 );
334 Ok(tvec!(fact))
335 }
336
337 fn cost(&self, _inputs: &[&TypedFact]) -> TractResult<TVec<(Cost, TDim)>> {
338 let values_per_token = self
339 .past_sequence_fact
340 .shape
341 .iter()
342 .enumerate()
343 .filter(|(axis, _)| *axis != self.axis)
344 .map(|(_, d)| d)
345 .product::<TDim>();
346 let resident_bytes = values_per_token.clone() * self.bits.max(1) as i64 / 8;
349 Ok(tvec!(
350 (Cost::Custom(false, "KVCacheValuesPerToken".to_string()), values_per_token),
351 (Cost::Custom(false, "KVCacheQuantBytesPerToken".to_string()), resident_bytes),
352 ))
353 }
354
355 as_op!();
356}
357
358#[derive(Clone, Debug)]
359struct QuantizedDynKvCacheState {
360 name: String,
361 axis: usize,
362 bits: u32,
363 per_channel: bool,
364 past_sequence_fact: TypedFact,
365 caches: Vec<HeadCache>,
366 lead_shape: TVec<usize>, d: usize, len: usize, }
370
371impl QuantizedDynKvCacheState {
372 fn bind_past(&self, state: &mut TurnState, len: usize) -> TractResult<()> {
374 let unresolved = self
375 .past_sequence_fact
376 .shape
377 .iter()
378 .filter_map(|symb| match symb {
379 TDim::Sym(s) if state.resolved_symbols.get(s).is_none() => Some(s),
380 _ => None,
381 })
382 .collect_vec();
383 if unresolved.is_empty() {
384 return Ok(());
385 }
386 ensure!(unresolved.len() == 1);
387 let sym = unresolved[0];
388 state.resolved_symbols.set(sym, len as i64);
389 if state.scenario.is_none() {
390 state.scenario = sym.scope().unwrap().guess_scenario(&state.resolved_symbols)?;
391 }
392 Ok(())
393 }
394
395 fn ingest(&mut self, f32in: &Tensor) -> TractResult<()> {
397 let view = f32in.to_plain_array_view::<f32>()?;
398 ensure!(view.ndim() == 4, "quantized KV cache supports rank-4 [B, H, S, D] caches");
399 ensure!(self.axis == 2, "seq axis must be 2 ([B, H, S, D])");
400 let view = view.into_dimensionality::<Ix4>()?;
401 let (b, h, s, d) = view.dim();
402 if self.caches.is_empty() {
403 self.d = d;
404 self.lead_shape = tvec!(b, h);
405 self.caches =
406 (0..b * h).map(|_| HeadCache::new(self.per_channel, d, self.bits)).collect();
407 }
408 ensure!(
409 d == self.d && self.lead_shape.as_slice() == [b, h],
410 "cache shape changed between steps"
411 );
412 for bi in 0..b {
413 for hi in 0..h {
414 let cache = &mut self.caches[bi * h + hi];
415 for t in 0..s {
416 cache.push(view.slice(s![bi, hi, t, ..]).as_slice().unwrap());
417 }
418 }
419 }
420 self.len += s;
421 Ok(())
422 }
423
424 fn dequantized(&self) -> TractResult<Tensor> {
426 let (t, d) = (self.len, self.d);
427 let leading: usize = self.lead_shape.iter().product();
428 let mut data = vec![0f32; leading * t * d];
429 for (idx, cache) in self.caches.iter().enumerate() {
430 let deq = cache.dequant_all();
431 let base = idx * t * d;
432 for ti in 0..t {
433 for di in 0..d {
434 data[base + ti * d + di] = deq[(ti, di)];
435 }
436 }
437 }
438 let mut shape: Vec<usize> = self.lead_shape.to_vec();
439 shape.push(t);
440 shape.push(d);
441 Tensor::from_shape(&shape, &data)
442 }
443}
444
445impl OpState for QuantizedDynKvCacheState {
446 fn init_tensor_fact(&self) -> Option<(String, TypedFact)> {
449 Some((self.name.clone(), self.past_sequence_fact.clone()))
450 }
451 fn has_init_tensor_fact(&self) -> bool {
452 true
453 }
454
455 fn load_from(
456 &mut self,
457 state: &mut TurnState,
458 states: &mut dyn Iterator<Item = TValue>,
459 ) -> TractResult<()> {
460 let init = states.next().context("Not enough state initializers")?;
461 self.bind_past(state, init.shape()[self.axis])?;
462 let f32init = init.cast_to::<f32>()?;
463 self.ingest(&f32init)?;
464 Ok(())
465 }
466
467 fn save_to(&self, states: &mut Vec<TValue>) -> TractResult<()> {
468 states.push(self.dequantized()?.into_tvalue());
469 Ok(())
470 }
471
472 fn resolve_symbols(&mut self, state: &mut TurnState) -> TractResult<()> {
473 self.bind_past(state, self.len)
474 }
475
476 fn eval(
477 &mut self,
478 _ctx: &EvalContext,
479 _op: &dyn Op,
480 inputs: TVec<TValue>,
481 ) -> TractResult<TVec<TValue>> {
482 let input = args_1!(inputs);
483 let input_dt = input.datum_type();
484 let f32in = input.cast_to::<f32>()?;
485 self.ingest(&f32in)?;
486 Ok(tvec!(self.dequantized()?.cast_to_dt(input_dt)?.into_owned().into_tvalue()))
487 }
488
489 fn reset_lanes(&mut self, _lanes: &[LaneId]) -> TractResult<()> {
490 bail!("QuantizedDynKvCache is not lane-aware: the cache has no lane axis")
491 }
492}
493
494fn ser_quant_dyn_kv_cache(
497 ast: &mut IntoAst,
498 node: &TypedNode,
499 op: &QuantizedDynKeyValueCache,
500) -> TractResult<Option<Arc<RValue>>> {
501 let input = ast.mapping[&node.inputs[0]].clone();
502 Ok(Some(invocation(
503 "tract_transformers_quantized_dyn_kv_cache",
504 &[input],
505 &[
506 ("name", string(&op.name)),
507 ("axis", numeric(op.axis)),
508 ("bits", numeric(op.bits)),
509 ("per_channel", numeric(op.per_channel as i64)),
510 ("datum_type", datum_type(op.past_sequence_fact.datum_type)),
511 ("past_sequence_shape", tdims(op.past_sequence_fact.shape.dims())),
512 ("input_sequence_shape", tdims(op.input_sequence_fact.shape.dims())),
513 ],
514 )))
515}
516
517fn de_quant_dyn_kv_cache(
518 builder: &mut ModelBuilder,
519 invocation: &ResolvedInvocation,
520) -> TractResult<Value> {
521 let input = invocation.named_arg_as(builder, "input")?;
522 let name: String = invocation.named_arg_as(builder, "name")?;
523 let axis: usize = invocation.named_arg_as(builder, "axis")?;
524 let bits: i64 = invocation.named_arg_as(builder, "bits")?;
525 let per_channel: i64 = invocation.named_arg_as(builder, "per_channel")?;
526 let dt = DatumType::from_str(&invocation.named_arg_as::<String>(builder, "datum_type")?)?;
527 let past_sequence_shape: TVec<TDim> = builder
528 .allowing_new_symbols(|builder| invocation.named_arg_as(builder, "past_sequence_shape"))?;
529 let input_sequence_shape: TVec<TDim> = builder
530 .allowing_new_symbols(|builder| invocation.named_arg_as(builder, "input_sequence_shape"))?;
531 builder.wire(
532 QuantizedDynKeyValueCache {
533 name,
534 axis,
535 bits: bits as u32,
536 per_channel: per_channel != 0,
537 past_sequence_fact: dt.fact(&*past_sequence_shape),
538 input_sequence_fact: dt.fact(&*input_sequence_shape),
539 },
540 &[input],
541 )
542}
543
544fn walk_to_cache_node(model: &TypedModel, start: OutletId) -> Option<usize> {
550 let mut outlet = start;
551 loop {
552 let n = model.node(outlet.node);
553 if n.outputs[outlet.slot].successors.len() != 1 {
554 return None;
555 }
556 if n.op_is::<DynKeyValueCache>() {
557 return Some(n.id);
558 } else if n.op_is::<ApplyRope>()
559 || n.op_is::<MultiBroadcastTo>()
560 || n.op_is::<AxisOp>()
561 || n.op_is::<Cast>()
562 {
563 outlet = n.inputs[0];
564 } else {
565 return None;
566 }
567 }
568}
569
570fn replace_with_quant_cache(
571 patch: &mut TypedModelPatch,
572 model: &TypedModel,
573 cache_node_id: usize,
574 per_channel: bool,
575 bits: u32,
576) -> TractResult<()> {
577 let cnode = model.node(cache_node_id);
578 let dkv = cnode.op_as::<DynKeyValueCache>().context("expected DynKeyValueCache")?;
579 let tap = patch.taps(model, &[cnode.inputs[0]])?;
580 let new = patch.wire_node(
581 format!("{}.quant", cnode.name),
582 QuantizedDynKeyValueCache {
583 name: dkv.name.clone(),
584 axis: dkv.axis,
585 bits,
586 per_channel,
587 past_sequence_fact: dkv.past_sequence_fact.clone(),
588 input_sequence_fact: dkv.input_sequence_fact.clone(),
589 },
590 &tap,
591 )?;
592 patch.shunt_outside(model, cache_node_id.into(), new[0])?;
593 Ok(())
594}
595
596fn quantize_kv_storage_rule(
599 ctx: &u32,
600 model: &TypedModel,
601 node: &TypedNode,
602 _node_name: &str,
603 _op: &Sdpa,
604) -> TractResult<Option<TypedModelPatch>> {
605 if node.inputs.len() != 3 && node.inputs.len() != 4 {
606 return Ok(None);
607 }
608 let (Some(k_cache), Some(v_cache)) =
609 (walk_to_cache_node(model, node.inputs[1]), walk_to_cache_node(model, node.inputs[2]))
610 else {
611 return Ok(None);
612 };
613 if k_cache == v_cache {
614 return Ok(None);
615 }
616 let mut patch = TypedModelPatch::default();
617 replace_with_quant_cache(&mut patch, model, k_cache, true, *ctx)?;
618 replace_with_quant_cache(&mut patch, model, v_cache, false, *ctx)?;
619 Ok(Some(patch))
620}
621
622#[derive(Debug, Clone, Copy)]
626pub struct QuantizeKvStorageTransform {
627 pub bits: u32,
628}
629
630impl Default for QuantizeKvStorageTransform {
631 fn default() -> Self {
632 QuantizeKvStorageTransform { bits: 8 }
633 }
634}
635
636impl ModelTransform for QuantizeKvStorageTransform {
637 fn name(&self) -> StaticName {
638 "quantize_kv_storage".into()
639 }
640 fn transform(&self, model: &mut TypedModel) -> TractResult<()> {
641 ensure!(self.bits == 4 || self.bits == 8, "KV quantization bits must be 4 or 8");
642 crate::rewriter::ApplyRopeTransform.transform(model)?;
645 crate::rewriter::KeyValueCacheTransform.transform(model)?;
646 Rewriter::default()
647 .with_rule_for("quantize-kv-storage", quantize_kv_storage_rule)
648 .rewrite(&self.bits, model)?;
649 model.compact()
650 }
651}
652
653#[cfg(test)]
654mod tests {
655 use super::*;
656 use tract_nnef::tract_core::ops::array::TypedConcat;
657
658 fn model_with_cache(bits: u32, per_channel: bool) -> TractResult<TypedModel> {
659 let mut model = TypedModel::default();
660 let s = model.sym("S");
661 let p = model.sym("P");
662 let (b, h, d) = (1usize, 2usize, 8usize);
663 let inf: TVec<TDim> = tvec![b.to_dim(), h.to_dim(), s.into(), d.to_dim()];
664 let pf: TVec<TDim> = tvec![b.to_dim(), h.to_dim(), p.into(), d.to_dim()];
665 let input = model.add_source("input", f32::fact(&inf))?;
666 let op = QuantizedDynKeyValueCache {
667 name: "kv0".to_string(),
668 axis: 2,
669 bits,
670 per_channel,
671 past_sequence_fact: f32::fact(&pf),
672 input_sequence_fact: f32::fact(&inf),
673 };
674 let out = model.wire_node("kv", op, &[input])?;
675 model.select_output_outlets(&out)?;
676 Ok(model)
677 }
678
679 #[test]
681 fn quant_cache_accumulates_near_lossless_int8() -> TractResult<()> {
682 for per_channel in [false, true] {
683 let mut rt = model_with_cache(8, per_channel)?.into_runnable()?.spawn()?;
684 let (b, h, d) = (1usize, 2usize, 8usize);
685 let mut st = 3u64;
686 let mut nf = || {
687 st = st.wrapping_mul(6364136223846793005).wrapping_add(1);
688 ((st >> 40) as f32 / (1u64 << 24) as f32) - 0.5
689 };
690 let mut acc: Option<Tensor> = None;
691 for _ in 0..40 {
692 let step = Tensor::from_shape(
693 &[b, h, 1, d],
694 &(0..b * h * d).map(|_| nf()).collect::<Vec<f32>>(),
695 )?;
696 let out = rt.run(tvec![step.clone().into()])?.remove(0).into_tensor();
697 acc = Some(match acc.take() {
698 None => step,
699 Some(a) => TypedConcat { axis: 2 }
700 .eval(&EvalContext::out_of_plan(), tvec![a.into(), step.into()])?
701 .remove(0)
702 .into_tensor(),
703 });
704 acc.as_ref().unwrap().close_enough(&out, Approximation::SuperApproximate)?;
705 }
706 }
707 Ok(())
708 }
709
710 #[test]
713 fn block_per_channel_key_consistent_over_growing_range() {
714 let d = 16usize;
715 let mut kc = BlockChannelStore::with_bits(d, 8);
716 let mut rows = Vec::new();
717 for t in 0..80usize {
718 let row: Vec<f32> = (0..d)
719 .map(|c| {
720 if c == 0 { t as f32 * 2.0 } else { (((t * 7 + c) as f32) * 0.013).sin() * 0.1 }
721 })
722 .collect();
723 kc.push_token(&row);
724 rows.push(row);
725 }
726 assert_eq!(kc.len(), 80);
727 let deq = kc.dequant_all();
728 let mut maxerr = 0f32;
729 for t in 0..80 {
730 for c in 0..d {
731 maxerr = maxerr.max((deq[(t, c)] - rows[t][c]).abs());
732 }
733 }
734 assert!(maxerr < 1.0, "block-consistent int8 dequant, got {maxerr}");
735 }
736
737 #[test]
738 fn quant_cache_nnef_round_trip() -> TractResult<()> {
739 use crate::WithTractTransformers;
740 let model = model_with_cache(4, true)?;
741 let nnef = tract_nnef::nnef().with_tract_transformers();
742 let mut buffer = vec![];
743 nnef.write_to_tar(&model, &mut buffer)?;
744 let reloaded = nnef.model_for_read(&mut &*buffer)?;
745 let n = reloaded
746 .nodes()
747 .iter()
748 .find_map(|n| n.op_as::<QuantizedDynKeyValueCache>())
749 .context("QuantizedDynKeyValueCache missing after round-trip")?;
750 assert_eq!(n.bits, 4);
751 assert!(n.per_channel);
752 assert_eq!(n.axis, 2);
753 Ok(())
754 }
755}