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