1use tract_nnef::internal::*;
18use tract_nnef::tract_core::ops::{FrozenOpState, OpStateFreeze};
19use tract_nnef::tract_core::transform::ModelTransform;
20use tract_nnef::tract_ndarray::{Array2, Array4, ArrayView2, Ix4, s};
21
22use crate::ops::dyn_kv_cache::DynKeyValueCache;
23use crate::ops::flash_sdpa::FlashSdpaOp;
24use crate::ops::sdpa::Sdpa;
25
26pub fn register(registry: &mut Registry) {
29 registry.register_dumper(ser_quantized_kv_sdpa);
30 registry.register_primitive(
31 "tract_transformers_quantized_kv_sdpa",
32 &[
33 TypeName::Scalar.tensor().named("q"),
34 TypeName::Scalar.tensor().named("k"),
35 TypeName::Scalar.tensor().named("v"),
36 TypeName::Integer.named("axis"),
37 TypeName::Scalar.named("scale"),
38 ],
39 &[("output", TypeName::Scalar.tensor())],
40 de_quantized_kv_sdpa,
41 );
42}
43
44fn ser_quantized_kv_sdpa(
45 ast: &mut IntoAst,
46 node: &TypedNode,
47 op: &QuantizedKvSdpa,
48) -> TractResult<Option<Arc<RValue>>> {
49 let q = ast.mapping[&node.inputs[0]].clone();
50 let k = ast.mapping[&node.inputs[1]].clone();
51 let v = ast.mapping[&node.inputs[2]].clone();
52 let mut attrs = vec![("axis", numeric(op.axis))];
53 if let Some(scale) = op.scale {
54 attrs.push(("scale", numeric(scale)));
55 }
56 Ok(Some(invocation("tract_transformers_quantized_kv_sdpa", &[q, k, v], &attrs)))
57}
58
59fn de_quantized_kv_sdpa(
60 builder: &mut ModelBuilder,
61 invocation: &ResolvedInvocation,
62) -> TractResult<Value> {
63 let q = invocation.named_arg_as(builder, "q")?;
64 let k = invocation.named_arg_as(builder, "k")?;
65 let v = invocation.named_arg_as(builder, "v")?;
66 let axis: usize = invocation.named_arg_as(builder, "axis")?;
67 let scale: Option<f32> = invocation.get_named_arg_as(builder, "scale")?;
68 builder.wire(QuantizedKvSdpa { axis, scale }, &[q, k, v])
69}
70
71pub fn quant_dequant(x: ArrayView2<f32>, bits: u32, by_row: bool) -> Array2<f32> {
76 assert!((1..=16).contains(&bits), "bits must be 1..=16");
77 let levels = ((1u32 << bits) - 1) as f32;
78 let (r, c) = x.dim();
79 let mut out = Array2::<f32>::zeros((r, c));
80 let n_groups = if by_row { r } else { c };
81 for g in 0..n_groups {
82 let group = if by_row { x.row(g) } else { x.column(g) };
83 let lo = group.iter().copied().fold(f32::INFINITY, f32::min);
84 let hi = group.iter().copied().fold(f32::NEG_INFINITY, f32::max);
85 let scale = if hi > lo { (hi - lo) / levels } else { 1.0 };
86 for (k, &v) in group.iter().enumerate() {
87 let q = ((v - lo) / scale).round().clamp(0.0, levels);
88 let deq = lo + q * scale;
89 if by_row {
90 out[(g, k)] = deq;
91 } else {
92 out[(k, g)] = deq;
93 }
94 }
95 }
96 out
97}
98
99fn quant_token_to_u8(v: &[f32]) -> (Vec<u8>, f32, f32) {
105 let lo = v.iter().copied().fold(f32::INFINITY, f32::min);
106 let hi = v.iter().copied().fold(f32::NEG_INFINITY, f32::max);
107 let scale = if hi > lo { (hi - lo) / 255.0 } else { 1.0 };
108 let q: Vec<u8> =
109 v.iter().map(|&x| ((x - lo) / scale).round().clamp(0.0, 255.0) as u8).collect();
110 (q, lo, scale)
111}
112
113fn dequant_u8(q: &[u8], lo: f32, scale: f32) -> Vec<f32> {
115 q.iter().map(|&b| lo + b as f32 * scale).collect()
116}
117
118#[derive(Clone, Debug, Default)]
123pub struct QuantValueCache {
124 pub d: usize,
125 packed: Vec<u8>,
127 params: Vec<(f32, f32)>, }
130
131impl QuantValueCache {
132 pub fn new(d: usize) -> Self {
133 QuantValueCache { d, packed: Vec::new(), params: Vec::new() }
134 }
135 pub fn len(&self) -> usize {
136 self.params.len()
137 }
138 pub fn is_empty(&self) -> bool {
139 self.params.is_empty()
140 }
141 pub fn push_token(&mut self, v: &[f32]) {
143 assert_eq!(v.len(), self.d);
144 let (q, lo, scale) = quant_token_to_u8(v);
145 self.packed.extend_from_slice(&q);
146 self.params.push((lo, scale));
147 }
148 pub fn dequant_all(&self) -> Array2<f32> {
150 let t = self.len();
151 let mut out = Array2::<f32>::zeros((t, self.d));
152 for (i, &(lo, scale)) in self.params.iter().enumerate() {
153 let row = dequant_u8(&self.packed[i * self.d..(i + 1) * self.d], lo, scale);
154 for (j, v) in row.into_iter().enumerate() {
155 out[(i, j)] = v;
156 }
157 }
158 out
159 }
160 pub fn memory_bytes(&self) -> usize {
161 self.packed.len() + self.params.len() * 8
162 }
163}
164
165#[derive(Clone, Debug, Default)]
172pub struct QuantKeyCache {
173 pub d: usize,
174 packed: Vec<u8>,
176 ch_lo: Vec<f32>,
178 ch_scale: Vec<f32>,
179 len: usize,
180}
181
182impl QuantKeyCache {
183 pub fn new(d: usize) -> Self {
184 QuantKeyCache {
185 d,
186 packed: Vec::new(),
187 ch_lo: vec![f32::INFINITY; d],
188 ch_scale: vec![1.0; d],
189 len: 0,
190 }
191 }
192 pub fn len(&self) -> usize {
193 self.len
194 }
195 pub fn is_empty(&self) -> bool {
196 self.len == 0
197 }
198 pub fn push_token(&mut self, k: &[f32]) {
200 assert_eq!(k.len(), self.d);
201 for (c, &val) in k.iter().enumerate() {
203 if val < self.ch_lo[c] {
204 self.ch_lo[c] = val;
205 }
206 let hi_needed = val;
207 let range = hi_needed - self.ch_lo[c];
208 if range > 0.0 {
209 let new_scale = (hi_needed - self.ch_lo[c]) / 255.0;
210 if new_scale > self.ch_scale[c] {
211 self.ch_scale[c] = new_scale;
212 }
213 }
214 }
215 let mut row = vec![0u8; self.d];
217 for (c, &val) in k.iter().enumerate() {
218 row[c] = ((val - self.ch_lo[c]) / self.ch_scale[c]).round().clamp(0.0, 255.0) as u8;
219 }
220 self.packed.extend_from_slice(&row);
221 self.len += 1;
222 }
223 pub fn dequant_all(&self) -> Array2<f32> {
225 let t = self.len;
226 let mut out = Array2::<f32>::zeros((t, self.d));
227 for i in 0..t {
228 for c in 0..self.d {
229 let b = self.packed[i * self.d + c];
230 out[(i, c)] = self.ch_lo[c] + b as f32 * self.ch_scale[c];
231 }
232 }
233 out
234 }
235 pub fn memory_bytes(&self) -> usize {
236 self.packed.len() + self.d * 8 }
238}
239
240#[derive(Clone, Debug, PartialEq)]
246pub struct QuantizedKvSdpa {
247 pub axis: usize,
248 pub scale: Option<f32>,
249}
250impl Eq for QuantizedKvSdpa {}
251
252impl Op for QuantizedKvSdpa {
253 fn name(&self) -> StaticName {
254 "QuantizedKvSdpa".into()
255 }
256 fn info(&self) -> TractResult<Vec<String>> {
257 Ok(vec![format!("axis={}, scale={:?}", self.axis, self.scale)])
258 }
259 op_as_typed_op!();
260}
261
262impl EvalOp for QuantizedKvSdpa {
263 fn is_stateless(&self) -> bool {
264 false
265 }
266 fn state(
267 &self,
268 _session: &TurnState,
269 _node_id: usize,
270 ) -> TractResult<Option<Box<dyn OpState>>> {
271 Ok(Some(Box::new(QuantizedKvSdpaState {
272 scale: self.scale,
273 k_caches: Vec::new(),
274 v_caches: Vec::new(),
275 initialized: false,
276 })))
277 }
278}
279
280impl TypedOp for QuantizedKvSdpa {
281 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
282 ensure!(inputs.len() == 3, "QuantizedKvSdpa expects [Q, K_new, V_new]");
283 Ok(tvec!(inputs[0].without_value()))
284 }
285 as_op!();
286}
287
288#[derive(Clone, Debug)]
289pub struct QuantizedKvSdpaState {
290 scale: Option<f32>,
291 k_caches: Vec<QuantKeyCache>, v_caches: Vec<QuantValueCache>, initialized: bool,
294}
295
296impl OpState for QuantizedKvSdpaState {
297 fn eval(
298 &mut self,
299 _state: &mut TurnState,
300 _op: &dyn Op,
301 inputs: TVec<TValue>,
302 ) -> TractResult<TVec<TValue>> {
303 ensure!(inputs.len() == 3, "QuantizedKvSdpa expects [Q, K_new, V_new]");
304 let input_dt = inputs[0].datum_type();
305 let q = inputs[0].cast_to::<f32>()?;
306 let k_new = inputs[1].cast_to::<f32>()?;
307 let v_new = inputs[2].cast_to::<f32>()?;
308 let qv = q.to_plain_array_view::<f32>()?.into_dimensionality::<Ix4>()?;
309 let kv = k_new.to_plain_array_view::<f32>()?.into_dimensionality::<Ix4>()?;
310 let vv = v_new.to_plain_array_view::<f32>()?.into_dimensionality::<Ix4>()?;
311 let (b, kh, snew, d) = kv.dim();
312 let n = b * kh;
313 if !self.initialized {
314 self.k_caches = (0..n).map(|_| QuantKeyCache::new(d)).collect();
315 self.v_caches = (0..n).map(|_| QuantValueCache::new(d)).collect();
316 self.initialized = true;
317 }
318 for bi in 0..b {
320 for hi in 0..kh {
321 let idx = bi * kh + hi;
322 let ks = kv.slice(s![bi, hi, .., ..]);
323 let vs = vv.slice(s![bi, hi, .., ..]);
324 for t in 0..snew {
325 self.k_caches[idx].push_token(ks.slice(s![t, ..]).as_slice().unwrap());
326 self.v_caches[idx].push_token(vs.slice(s![t, ..]).as_slice().unwrap());
327 }
328 }
329 }
330 let (_, _, _, _d) = qv.dim();
332 let t = self.k_caches[0].len();
333 let mut k_full = Array4::<f32>::zeros((b, kh, t, d));
334 let mut v_full = Array4::<f32>::zeros((b, kh, t, d));
335 for bi in 0..b {
336 for hi in 0..kh {
337 let idx = bi * kh + hi;
338 let kd = self.k_caches[idx].dequant_all();
339 let vd = self.v_caches[idx].dequant_all();
340 k_full.slice_mut(s![bi, hi, .., ..]).assign(&kd);
341 v_full.slice_mut(s![bi, hi, .., ..]).assign(&vd);
342 }
343 }
344 let flash = FlashSdpaOp { causal: false, scale: self.scale };
346 let o = flash.flash_attention_gqa(qv, k_full.view(), v_full.view(), None);
347 Ok(tvec!(o.into_tensor().cast_to_dt(input_dt)?.into_owned().into_tvalue()))
348 }
349}
350
351#[derive(Clone, Debug)]
352struct FrozenQuantizedKvSdpaState {
353 scale: Option<f32>,
354 k_caches: Vec<QuantKeyCache>,
355 v_caches: Vec<QuantValueCache>,
356 initialized: bool,
357}
358impl OpStateFreeze for QuantizedKvSdpaState {
359 fn freeze(&self) -> Box<dyn FrozenOpState> {
360 Box::new(FrozenQuantizedKvSdpaState {
361 scale: self.scale,
362 k_caches: self.k_caches.clone(),
363 v_caches: self.v_caches.clone(),
364 initialized: self.initialized,
365 })
366 }
367}
368impl FrozenOpState for FrozenQuantizedKvSdpaState {
369 fn unfreeze(&self) -> Box<dyn OpState> {
370 Box::new(QuantizedKvSdpaState {
371 scale: self.scale,
372 k_caches: self.k_caches.clone(),
373 v_caches: self.v_caches.clone(),
374 initialized: self.initialized,
375 })
376 }
377}
378
379pub fn fuse_quantized_kv_sdpa_rule(
383 _ctx: &(),
384 model: &TypedModel,
385 node: &TypedNode,
386 node_name: &str,
387 op: &Sdpa,
388) -> TractResult<Option<TypedModelPatch>> {
389 if node.inputs.len() != 3 {
390 return Ok(None);
391 }
392 let k_node = model.node(node.inputs[1].node);
393 let v_node = model.node(node.inputs[2].node);
394 let (Some(kc), Some(vc)) =
395 (k_node.op_as::<DynKeyValueCache>(), v_node.op_as::<DynKeyValueCache>())
396 else {
397 return Ok(None);
398 };
399 if kc.axis != vc.axis {
400 return Ok(None);
401 }
402 if k_node.outputs[0].successors.len() != 1 || v_node.outputs[0].successors.len() != 1 {
403 return Ok(None);
404 }
405 let scale = op.scale.as_ref().map(|t| t.cast_to_scalar::<f32>()).transpose()?;
406 let mut patch = TypedModelPatch::default();
407 let taps = patch.taps(model, &[node.inputs[0], k_node.inputs[0], v_node.inputs[0]])?;
408 let fused = patch.wire_node(
409 format!("{node_name}.quant_kv_sdpa"),
410 QuantizedKvSdpa { axis: kc.axis, scale },
411 &taps,
412 )?;
413 patch.shunt_outside(model, node.id.into(), fused[0])?;
414 Ok(Some(patch))
415}
416
417#[derive(Debug, Default)]
419pub struct QuantizedKvSdpaTransform;
420
421impl ModelTransform for QuantizedKvSdpaTransform {
422 fn name(&self) -> StaticName {
423 "fuse_quantized_kv_sdpa".into()
424 }
425 fn transform(&self, model: &mut TypedModel) -> TractResult<()> {
426 Rewriter::default()
427 .with_rule_for("fuse-kv-broadcast", crate::ops::sdpa::fuse_kv_cache_broadcast_rule)
428 .rewrite(&(), model)?;
429 Rewriter::default()
430 .with_rule_for("fuse-quant-kv-sdpa", fuse_quantized_kv_sdpa_rule)
431 .rewrite(&(), model)?;
432 model.compact()
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use tract_nnef::tract_ndarray::{Array2, arr2};
440
441 fn max_abs(a: &Array2<f32>, b: &Array2<f32>) -> f32 {
442 a.iter().zip(b.iter()).map(|(x, y)| (x - y).abs()).fold(0.0, f32::max)
443 }
444
445 #[test]
447 fn error_decreases_with_bits() {
448 let x = arr2(&[[0.0f32, 1.0, 2.0, 3.0], [-1.0, 0.5, 4.0, 9.0], [2.0, 2.0, 2.0, 2.1]]);
449 let e4 = max_abs(&x, &quant_dequant(x.view(), 4, false));
450 let e8 = max_abs(&x, &quant_dequant(x.view(), 8, false));
451 let e16 = max_abs(&x, &quant_dequant(x.view(), 16, false));
452 assert!(e8 < e4, "more bits => less error ({e8} !< {e4})");
453 assert!(e16 < e8, "16-bit tighter than 8-bit ({e16} !< {e8})");
454 assert!(e16 < 1e-3, "16-bit near-exact, got {e16}");
455 let levels = (1u32 << 8) - 1;
457 for j in 0..x.ncols() {
458 let col = x.column(j);
459 let (lo, hi) = (
460 col.iter().copied().fold(f32::INFINITY, f32::min),
461 col.iter().copied().fold(f32::NEG_INFINITY, f32::max),
462 );
463 let step = if hi > lo { (hi - lo) / levels as f32 } else { 0.0 };
464 let q = quant_dequant(x.view(), 8, false);
465 for i in 0..x.nrows() {
466 assert!((x[(i, j)] - q[(i, j)]).abs() <= step / 2.0 + 1e-6);
467 }
468 }
469 }
470
471 #[test]
475 fn per_channel_beats_per_row_on_outlier_channel() {
476 let x = arr2(&[
478 [100.0f32, 0.10, -0.20, 0.05],
479 [-90.0, 0.02, 0.30, -0.08],
480 [120.0, -0.15, 0.10, 0.20],
481 [-110.0, 0.07, -0.05, 0.12],
482 ]);
483 let small_err = |q: &Array2<f32>| -> f32 {
486 (1..4)
487 .flat_map(|j| (0..4).map(move |i| (i, j)))
488 .map(|(i, j)| (x[(i, j)] - q[(i, j)]).abs())
489 .fold(0.0, f32::max)
490 };
491 let pc = small_err(&quant_dequant(x.view(), 4, false)); let pt = small_err(&quant_dequant(x.view(), 4, true)); assert!(pc < pt * 0.2, "per-channel ≫ better on the small dims: pc={pc} pt={pt}");
494 }
495
496 #[test]
498 fn attention_near_lossless_at_8bit() {
499 let (s, d) = (12usize, 16usize);
501 let mk = |seed: u64| -> Array2<f32> {
502 let mut st = seed;
503 Array2::from_shape_fn((s, d), |_| {
504 st = st.wrapping_mul(6364136223846793005).wrapping_add(1);
505 ((st >> 40) as f32 / (1u64 << 24) as f32) - 0.5
506 })
507 };
508 let q = mk(1).row(0).to_owned();
509 let k = mk(2);
510 let v = mk(3);
511 let scale = 1.0 / (d as f32).sqrt();
512 let attn = |k: &Array2<f32>, v: &Array2<f32>| -> Vec<f32> {
513 let mut sc: Vec<f32> = (0..s).map(|j| q.dot(&k.row(j)) * scale).collect();
514 let m = sc.iter().cloned().fold(f32::MIN, f32::max);
515 let mut sum = 0.0;
516 sc.iter_mut().for_each(|x| {
517 *x = (*x - m).exp();
518 sum += *x;
519 });
520 (0..d).map(|e| (0..s).map(|j| sc[j] / sum * v[(j, e)]).sum()).collect()
521 };
522 let full = attn(&k, &v);
523 let dev = |bits: u32| -> f32 {
524 let kq = quant_dequant(k.view(), bits, false);
526 let vq = quant_dequant(v.view(), bits, true);
527 let o = attn(&kq, &vq);
528 let num: f32 = o.iter().zip(&full).map(|(a, b)| (a - b).powi(2)).sum::<f32>().sqrt();
529 let den: f32 = full.iter().map(|x| x * x).sum::<f32>().sqrt();
530 num / den.max(1e-9)
531 };
532 let (d4, d8, d12) = (dev(4), dev(8), dev(12));
533 assert!(d8 < d4 && d12 < d8, "deviation must shrink with bits: 4={d4} 8={d8} 12={d12}");
534 assert!(d8 < 0.02, "8-bit KV near-lossless for attention, got {d8}");
535 }
536
537 #[test]
539 fn packed_u8_saves_memory_vs_f32() {
540 let (t, d) = (512usize, 64usize);
541 let mut kc = QuantKeyCache::new(d);
542 let mut vc = QuantValueCache::new(d);
543 let mut rng = 42u64;
544 let mut next = || -> f32 {
545 rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
546 ((rng >> 40) as f32 / (1u64 << 24) as f32) - 0.5
547 };
548 for _ in 0..t {
549 kc.push_token(&(0..d).map(|_| next()).collect::<Vec<_>>());
550 vc.push_token(&(0..d).map(|_| next()).collect::<Vec<_>>());
551 }
552 let f32_bytes = t * d * 4 * 2; let quant_bytes = kc.memory_bytes() + vc.memory_bytes();
554 let ratio = f32_bytes as f32 / quant_bytes as f32;
555 assert!(ratio > 3.0, "expected >3x memory saving, got {ratio:.2}x");
558 println!("f32 bytes: {f32_bytes}, quantized: {quant_bytes}, ratio: {ratio:.2}x");
559 }
560
561 #[test]
563 fn quantized_kv_sdpa_runs_in_model() -> TractResult<()> {
564 let (b, h, d) = (1usize, 2usize, 16usize);
565 let scale = 1.0 / (d as f32).sqrt();
566 let mut model = TypedModel::default();
567 let s = model.sym("S");
568 let dim = |x: usize| x.to_dim();
569 let f: TVec<TDim> = tvec![dim(b), dim(h), s.into(), dim(d)];
570 let q = model.add_source("q", f32::fact(&f))?;
571 let k = model.add_source("k", f32::fact(&f))?;
572 let v = model.add_source("v", f32::fact(&f))?;
573 let o = model.wire_node("qkv", QuantizedKvSdpa { axis: 2, scale: None }, &[q, k, v])?;
574 model.select_output_outlets(&o)?;
575 let mut rt = model.into_runnable()?.spawn()?;
576
577 use tract_nnef::tract_core::ops::array::TypedConcat;
579 use tract_nnef::tract_ndarray::{Array4 as A4, s};
580
581 let mk = |base: f32| -> Tensor {
582 let data: Vec<f32> = (0..b * h * d).map(|i| base + (i as f32 * 0.013).sin()).collect();
583 Tensor::from_shape(&[b, h, 1, d], &data).unwrap()
584 };
585 let grow = |acc: Option<Tensor>, x: Tensor| -> TractResult<Tensor> {
586 Ok(match acc {
587 None => x,
588 Some(a) => {
589 TypedConcat { axis: 2 }.eval(tvec![a.into(), x.into()])?.remove(0).into_tensor()
590 }
591 })
592 };
593 let attn = |q: A4<f32>, k: A4<f32>, v: A4<f32>| -> A4<f32> {
594 let (b, h, sq, d) = q.dim();
595 let mut out = A4::<f32>::zeros((b, h, sq, d));
596 for bi in 0..b {
597 for hi in 0..h {
598 let qm = q.slice(s![bi, hi, .., ..]);
599 let km = k.slice(s![bi, hi, .., ..]);
600 let vm = v.slice(s![bi, hi, .., ..]);
601 let mut sc = qm.dot(&km.t());
602 sc *= scale;
603 for mut row in sc.rows_mut() {
604 let m = row.iter().copied().fold(f32::NEG_INFINITY, f32::max);
605 let mut sm = 0.0f32;
606 row.iter_mut().for_each(|x| {
607 *x = (*x - m).exp();
608 sm += *x;
609 });
610 row.iter_mut().for_each(|x| *x /= sm);
611 }
612 out.slice_mut(s![bi, hi, .., ..]).assign(&sc.dot(&vm));
613 }
614 }
615 out
616 };
617 let (mut kf, mut vf): (Option<Tensor>, Option<Tensor>) = (None, None);
618 for t in 0..10 {
619 let qi = mk(9.0 + t as f32 * 0.1);
620 let ki = mk(1.0 + t as f32 * 0.07);
621 let vi = mk(5.0 - t as f32 * 0.05);
622 let o_model = rt
623 .run(tvec![qi.clone().into(), ki.clone().into(), vi.clone().into()])?
624 .remove(0)
625 .into_tensor();
626 kf = Some(grow(kf.take(), ki)?);
627 vf = Some(grow(vf.take(), vi)?);
628 let qv = qi.to_plain_array_view::<f32>()?.into_dimensionality()?;
629 let kv = kf.as_ref().unwrap().to_plain_array_view::<f32>()?.into_dimensionality()?;
630 let vv = vf.as_ref().unwrap().to_plain_array_view::<f32>()?.into_dimensionality()?;
631 let o_ref = Tensor::from(attn(qv.to_owned(), kv.to_owned(), vv.to_owned()));
632 o_model
634 .close_enough(&o_ref, Approximation::SuperApproximate)
635 .with_context(|| format!("quantized decode too far from f32 at step {t}"))?;
636 }
637 Ok(())
638 }
639
640 #[test]
642 fn transform_fuses_cache_sdpa_to_quantized() -> TractResult<()> {
643 let (b, h, d) = (1usize, 2usize, 16usize);
644 let mut model = TypedModel::default();
645 let s = model.sym("S");
646 let p = model.sym("P");
647 let dim = |x: usize| x.to_dim();
648 let newf: TVec<TDim> = tvec![dim(b), dim(h), s.into(), dim(d)];
649 let pastf: TVec<TDim> = tvec![dim(b), dim(h), p.into(), dim(d)];
650 let q = model.add_source("q", f32::fact(&newf))?;
651 let knew = model.add_source("k", f32::fact(&newf))?;
652 let vnew = model.add_source("v", f32::fact(&newf))?;
653 let mkc = |nm: &str| DynKeyValueCache {
654 name: nm.to_string(),
655 axis: 2,
656 past_sequence_fact: f32::fact(&pastf),
657 input_sequence_fact: f32::fact(&newf),
658 };
659 let kc = model.wire_node("kc", mkc("kc"), &[knew])?;
660 let vc = model.wire_node("vc", mkc("vc"), &[vnew])?;
661 let o = model.wire_node(
662 "sdpa",
663 Sdpa {
664 scale: None,
665 datum_type: f32::datum_type(),
666 acc_datum_type: f32::datum_type(),
667 is_causal: false,
668 },
669 &[q, kc[0], vc[0]],
670 )?;
671 model.select_output_outlets(&o)?;
672 QuantizedKvSdpaTransform.transform(&mut model)?;
673 assert!(model.nodes().iter().any(|n| n.op_is::<QuantizedKvSdpa>()), "fused op present");
674 assert!(!model.nodes().iter().any(|n| n.op_is::<DynKeyValueCache>()), "caches removed");
675 assert!(!model.nodes().iter().any(|n| n.op_is::<Sdpa>()), "sdpa removed");
676 Ok(())
677 }
678
679 #[test]
682 #[ignore]
683 fn bench_memory_savings() {
684 let d = 128usize;
685 let mut rng = 99u64;
686 let mut next = || -> f32 {
687 rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
688 ((rng >> 40) as f32 / (1u64 << 24) as f32) - 0.5
689 };
690 println!("\n KV cache memory (int8 u8 vs f32), H=8 heads, D={d}:");
691 println!(" T f32(MB) int8(MB) saving");
692 for &t in &[256usize, 1024, 4096, 16384] {
693 let mut kc = QuantKeyCache::new(d);
694 let mut vc = QuantValueCache::new(d);
695 for _ in 0..t {
696 kc.push_token(&(0..d).map(|_| next()).collect::<Vec<_>>());
697 vc.push_token(&(0..d).map(|_| next()).collect::<Vec<_>>());
698 }
699 let heads = 8;
700 let f32_mb = (t * d * 4 * 2 * heads) as f32 / 1e6;
701 let int8_mb = ((kc.memory_bytes() + vc.memory_bytes()) * heads) as f32 / 1e6;
702 println!(" {t:>6} {f32_mb:>9.2} {int8_mb:>9.2} {:>6.2}x", f32_mb / int8_mb);
703 }
704 }
705
706 #[test]
708 fn quantized_kv_sdpa_nnef_round_trip() -> TractResult<()> {
709 use crate::WithTractTransformers;
710 let (b, h, d) = (1usize, 2usize, 16usize);
711 let mut model = TypedModel::default();
712 let s = model.sym("S");
713 let dim = |x: usize| x.to_dim();
714 let f: TVec<TDim> = tvec![dim(b), dim(h), s.into(), dim(d)];
715 let q = model.add_source("q", f32::fact(&f))?;
716 let k = model.add_source("k", f32::fact(&f))?;
717 let v = model.add_source("v", f32::fact(&f))?;
718 let o =
719 model.wire_node("qkv", QuantizedKvSdpa { axis: 2, scale: Some(0.125) }, &[q, k, v])?;
720 model.select_output_outlets(&o)?;
721
722 let nnef = tract_nnef::nnef().with_tract_transformers();
723 let mut buffer = vec![];
724 nnef.write_to_tar(&model, &mut buffer)?;
725 let reloaded = nnef.model_for_read(&mut &*buffer)?;
726
727 let n = reloaded
728 .nodes()
729 .iter()
730 .find(|n| n.op_is::<QuantizedKvSdpa>())
731 .context("QuantizedKvSdpa not found after round-trip")?;
732 let op = n.op_as::<QuantizedKvSdpa>().unwrap();
733 assert_eq!(op.axis, 2);
734 assert_eq!(op.scale, Some(0.125));
735 Ok(())
736 }
737}