1use tract_nnef::internal::*;
21use tract_nnef::tract_core::transform::ModelTransform;
22use tract_nnef::tract_ndarray::Ix4;
23
24use crate::ops::dyn_kv_cache::DynKeyValueCache;
25use crate::ops::flash_sdpa::FlashSdpaOp;
26use crate::ops::sdpa::Sdpa;
27
28pub fn register(registry: &mut Registry) {
30 registry.register_dumper(ser_window_kv_sdpa);
31 registry.register_primitive(
32 "tract_transformers_window_kv_sdpa",
33 &[
34 TypeName::Scalar.tensor().named("q"),
35 TypeName::Scalar.tensor().named("k"),
36 TypeName::Scalar.tensor().named("v"),
37 TypeName::Integer.named("axis"),
38 TypeName::Integer.named("window"),
39 TypeName::Scalar.named("scale"),
40 ],
41 &[("output", TypeName::Scalar.tensor())],
42 de_window_kv_sdpa,
43 );
44}
45
46fn ser_window_kv_sdpa(
47 ast: &mut IntoAst,
48 node: &TypedNode,
49 op: &WindowKvSdpa,
50) -> TractResult<Option<Arc<RValue>>> {
51 let q = ast.mapping[&node.inputs[0]].clone();
52 let k = ast.mapping[&node.inputs[1]].clone();
53 let v = ast.mapping[&node.inputs[2]].clone();
54 let mut attrs = vec![("axis", numeric(op.axis)), ("window", numeric(op.window))];
55 if let Some(scale) = op.scale {
56 attrs.push(("scale", numeric(scale)));
57 }
58 Ok(Some(invocation("tract_transformers_window_kv_sdpa", &[q, k, v], &attrs)))
59}
60
61fn de_window_kv_sdpa(
62 builder: &mut ModelBuilder,
63 invocation: &ResolvedInvocation,
64) -> TractResult<Value> {
65 let q = invocation.named_arg_as(builder, "q")?;
66 let k = invocation.named_arg_as(builder, "k")?;
67 let v = invocation.named_arg_as(builder, "v")?;
68 let axis: usize = invocation.named_arg_as(builder, "axis")?;
69 let window: usize = invocation.named_arg_as(builder, "window")?;
70 let scale: Option<f32> = invocation.get_named_arg_as(builder, "scale")?;
71 builder.wire(WindowKvSdpa { axis, window, scale }, &[q, k, v])
72}
73
74#[derive(Clone, Debug)]
76pub struct WindowKvCache {
77 pub axis: usize,
78 pub window: usize,
79 buf: Option<Tensor>, len: usize, cursor: usize, }
83
84impl WindowKvCache {
85 pub fn new(axis: usize, window: usize) -> Self {
86 assert!(window > 0, "window must be > 0");
87 WindowKvCache { axis, window, buf: None, len: 0, cursor: 0 }
88 }
89
90 pub fn len(&self) -> usize {
91 self.len
92 }
93 pub fn is_empty(&self) -> bool {
94 self.len == 0
95 }
96 pub fn capacity(&self) -> usize {
98 self.buf.as_ref().map(|b| b.shape()[self.axis]).unwrap_or(0)
99 }
100
101 fn ensure_buf(&mut self, like: &Tensor) -> TractResult<()> {
102 if self.buf.is_none() {
103 let mut shape: TVec<usize> = like.shape().into();
104 shape[self.axis] = self.window;
105 self.buf = Some(unsafe { Tensor::uninitialized_dt(like.datum_type(), &shape)? });
106 }
107 Ok(())
108 }
109
110 pub fn push(&mut self, input: &Tensor) -> TractResult<()> {
112 let new = input.shape()[self.axis];
113 if new == 0 {
114 return Ok(());
115 }
116 self.ensure_buf(input)?;
117 let w = self.window;
118
119 if new >= w {
120 let buf = self.buf.as_mut().unwrap();
122 buf.assign_slice(0..w, input, (new - w)..new, self.axis)?;
123 self.cursor = 0;
124 self.len = w;
125 return Ok(());
126 }
127
128 let end = self.cursor + new;
130 let buf = self.buf.as_mut().unwrap();
131 if end <= w {
132 buf.assign_slice(self.cursor..end, input, 0..new, self.axis)?;
133 self.cursor = if end == w { 0 } else { end };
134 } else {
135 let first = w - self.cursor;
136 buf.assign_slice(self.cursor..w, input, 0..first, self.axis)?;
137 buf.assign_slice(0..(new - first), input, first..new, self.axis)?;
138 self.cursor = new - first;
139 }
140 self.len = (self.len + new).min(w);
141 Ok(())
142 }
143
144 pub fn valid_view<T: Datum>(&self) -> TractResult<tract_ndarray::ArrayViewD<'_, T>> {
147 let buf = self.buf.as_ref().context("empty window cache")?;
148 let mut v = buf.to_plain_array_view::<T>()?;
149 v.slice_axis_inplace(tract_ndarray::Axis(self.axis), (0..self.len).into());
150 Ok(v)
151 }
152}
153
154#[derive(Clone, Debug, PartialEq)]
160pub struct WindowKvSdpa {
161 pub axis: usize,
162 pub window: usize,
163 pub scale: Option<f32>,
164}
165impl Eq for WindowKvSdpa {}
166
167impl Op for WindowKvSdpa {
168 fn name(&self) -> StaticName {
169 "WindowKvSdpa".into()
170 }
171 fn info(&self) -> TractResult<Vec<String>> {
172 Ok(vec![format!("axis={}, window={}, scale={:?}", self.axis, self.window, self.scale)])
173 }
174 op_as_typed_op!();
175}
176
177impl EvalOp for WindowKvSdpa {
178 not_out_of_plan!();
179 fn state(&self, _ctx: &EvalContext) -> TractResult<Option<Box<dyn OpState>>> {
180 Ok(Some(Box::new(WindowKvSdpaState {
181 scale: self.scale,
182 k: WindowKvCache::new(self.axis, self.window),
183 v: WindowKvCache::new(self.axis, self.window),
184 })))
185 }
186}
187
188impl TypedOp for WindowKvSdpa {
189 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
190 ensure!(inputs.len() == 3, "WindowKvSdpa expects [Q, K_new, V_new]");
191 Ok(tvec!(inputs[0].without_value()))
192 }
193 as_op!();
194}
195
196#[derive(Clone, Debug)]
197pub struct WindowKvSdpaState {
198 scale: Option<f32>,
199 k: WindowKvCache,
200 v: WindowKvCache,
201}
202
203impl OpState for WindowKvSdpaState {
204 fn eval(
205 &mut self,
206 _ctx: &EvalContext,
207 _op: &dyn Op,
208 inputs: TVec<TValue>,
209 ) -> TractResult<TVec<TValue>> {
210 ensure!(inputs.len() == 3, "WindowKvSdpa expects [Q, K_new, V_new]");
211 let input_dt = inputs[0].datum_type();
212 let k_new = inputs[1].cast_to::<f32>()?;
213 let v_new = inputs[2].cast_to::<f32>()?;
214 self.k.push(k_new.as_ref())?;
215 self.v.push(v_new.as_ref())?;
216
217 let q = inputs[0].cast_to::<f32>()?;
218 let qv = q.to_plain_array_view::<f32>()?.into_dimensionality::<Ix4>()?;
219 let kview = self.k.valid_view::<f32>()?.into_dimensionality::<Ix4>()?;
220 let vview = self.v.valid_view::<f32>()?.into_dimensionality::<Ix4>()?;
221
222 let flash = FlashSdpaOp { causal: false, scale: self.scale };
225 let o = flash.flash_attention_gqa(qv, kview, vview, None);
226 Ok(tvec!(o.into_tensor().cast_to_dt(input_dt)?.into_owned().into_tvalue()))
227 }
228
229 fn reset_lanes(&mut self, _lanes: &[LaneId]) -> TractResult<()> {
230 bail!("WindowKvSdpa is not lane-aware: the ring buffers have no lane axis")
231 }
232}
233
234pub fn fuse_window_kv_sdpa_rule(
239 window: &usize,
240 model: &TypedModel,
241 node: &TypedNode,
242 node_name: &str,
243 op: &Sdpa,
244) -> TractResult<Option<TypedModelPatch>> {
245 if node.inputs.len() != 3 {
246 return Ok(None);
247 }
248 let k_node = model.node(node.inputs[1].node);
249 let v_node = model.node(node.inputs[2].node);
250 let (Some(kc), Some(vc)) =
251 (k_node.op_as::<DynKeyValueCache>(), v_node.op_as::<DynKeyValueCache>())
252 else {
253 return Ok(None);
254 };
255 if kc.axis != vc.axis {
256 return Ok(None);
257 }
258 if k_node.outputs[0].successors.len() != 1 || v_node.outputs[0].successors.len() != 1 {
259 return Ok(None);
260 }
261 let scale = op.scale.as_ref().map(|t| t.cast_to_scalar::<f32>()).transpose()?;
262 let q_outlet = node.inputs[0];
263 let k_new = k_node.inputs[0];
264 let v_new = v_node.inputs[0];
265
266 let mut patch = TypedModelPatch::default();
267 let taps = patch.taps(model, &[q_outlet, k_new, v_new])?;
268 let fused = patch.wire_node(
269 format!("{node_name}.window_kv_sdpa"),
270 WindowKvSdpa { axis: kc.axis, window: *window, scale },
271 &taps,
272 )?;
273 patch.shunt_outside(model, node.id.into(), fused[0])?;
274 Ok(Some(patch))
275}
276
277#[derive(Debug, Clone)]
280pub struct WindowKvSdpaTransform {
281 pub window: usize,
282}
283
284impl ModelTransform for WindowKvSdpaTransform {
285 fn name(&self) -> StaticName {
286 "fuse_window_kv_sdpa".into()
287 }
288 fn transform(&self, model: &mut TypedModel) -> TractResult<()> {
289 Rewriter::default()
290 .with_rule_for("fuse-kv-broadcast", crate::ops::sdpa::fuse_kv_cache_broadcast_rule)
291 .rewrite(&(), model)?;
292 Rewriter::default()
293 .with_rule_for("fuse-window-kv-sdpa", fuse_window_kv_sdpa_rule)
294 .rewrite(&self.window, model)?;
295 model.compact()
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use tract_nnef::tract_ndarray::{Array4, ArrayView4, s};
303
304 fn tok(shape: &[usize], v: f32) -> Tensor {
305 let n: usize = shape.iter().product();
306 Tensor::from_shape(shape, &vec![v; n]).unwrap()
307 }
308
309 #[test]
311 fn window_holds_last_w_as_a_set() -> TractResult<()> {
312 let w = 4;
313 let mut c = WindowKvCache::new(2, w); let mut full: Vec<f32> = vec![];
315 for t in 0..10 {
316 c.push(&tok(&[1, 1, 1, 1], t as f32))?;
317 full.push(t as f32);
318 assert!(c.len() <= w, "len bounded by window");
319 let view = c.valid_view::<f32>()?;
321 let mut got: Vec<f32> = view.iter().copied().collect();
322 got.sort_by(|a, b| a.partial_cmp(b).unwrap());
323 let mut want: Vec<f32> = full.iter().rev().take(w).copied().collect();
324 want.sort_by(|a, b| a.partial_cmp(b).unwrap());
325 assert_eq!(got, want, "step {t}: window must hold the last {w} as a set");
326 }
327 assert_eq!(c.capacity(), w, "memory stays bounded at the window");
328 Ok(())
329 }
330
331 #[test]
332 fn prefill_chunk_larger_than_window_keeps_last_w() -> TractResult<()> {
333 let w = 3;
334 let mut c = WindowKvCache::new(2, w);
335 let chunk = Tensor::from_shape(&[1, 1, 7, 1], &[0f32, 1., 2., 3., 4., 5., 6.])?;
337 c.push(&chunk)?;
338 let mut got: Vec<f32> = c.valid_view::<f32>()?.iter().copied().collect();
339 got.sort_by(|a, b| a.partial_cmp(b).unwrap());
340 assert_eq!(got, vec![4.0, 5.0, 6.0], "keeps the last 3");
341 Ok(())
342 }
343
344 fn attention(
347 q: ArrayView4<f32>,
348 k: ArrayView4<f32>,
349 v: ArrayView4<f32>,
350 scale: f32,
351 ) -> Array4<f32> {
352 let (b, h, sq, d) = q.dim();
353 let mut out = Array4::<f32>::zeros((b, h, sq, d));
354 for bi in 0..b {
355 for hi in 0..h {
356 let qm = q.slice(s![bi, hi, .., ..]);
357 let km = k.slice(s![bi, hi, .., ..]);
358 let vm = v.slice(s![bi, hi, .., ..]);
359 let mut sc = qm.dot(&km.t());
360 sc *= scale;
361 for mut row in sc.rows_mut() {
362 let m = row.iter().copied().fold(f32::NEG_INFINITY, f32::max);
363 let mut s = 0.0;
364 row.iter_mut().for_each(|x| {
365 *x = (*x - m).exp();
366 s += *x;
367 });
368 row.iter_mut().for_each(|x| *x /= s);
369 }
370 out.slice_mut(s![bi, hi, .., ..]).assign(&sc.dot(&vm));
371 }
372 }
373 out
374 }
375
376 #[test]
377 fn windowed_attention_matches_last_w_full() -> TractResult<()> {
378 let (h, d, w) = (2usize, 8usize, 6usize);
379 let scale = 1.0 / (d as f32).sqrt();
380 let seq = |s: usize, base: f32| -> Tensor {
382 let data: Vec<f32> = (0..h * s * d).map(|i| base + (i as f32 * 0.013).sin()).collect();
383 Tensor::from_shape(&[1, h, s, d], &data).unwrap()
384 };
385 let mut kc = WindowKvCache::new(2, w);
386 let mut vc = WindowKvCache::new(2, w);
387 let mut kfull: Option<Tensor> = None;
388 let mut vfull: Option<Tensor> = None;
389 use tract_nnef::tract_core::ops::array::TypedConcat;
390 for t in 0..20 {
391 let knew = seq(1, 1.0 + t as f32 * 0.1);
392 let vnew = seq(1, 5.0 - t as f32 * 0.07);
393 kc.push(&knew)?;
394 vc.push(&vnew)?;
395 let grow = |acc: Option<Tensor>, x: Tensor| -> TractResult<Tensor> {
396 Ok(match acc {
397 None => x,
398 Some(a) => TypedConcat { axis: 2 }
399 .eval(&EvalContext::out_of_plan(), tvec![a.into(), x.into()])?
400 .remove(0)
401 .into_tensor(),
402 })
403 };
404 kfull = Some(grow(kfull.take(), knew)?);
405 vfull = Some(grow(vfull.take(), vnew)?);
406
407 let q = seq(1, 9.0 + t as f32 * 0.05);
408 let qv = q.to_plain_array_view::<f32>()?.into_dimensionality()?;
409
410 let o_win = attention(
412 qv,
413 kc.valid_view::<f32>()?.into_dimensionality()?,
414 vc.valid_view::<f32>()?.into_dimensionality()?,
415 scale,
416 );
417 let len = kc.len();
419 let kf = kfull.as_ref().unwrap();
420 let s = kf.shape()[2];
421 let kslice = kf.slice(2, s - len, s)?;
422 let vslice = vfull.as_ref().unwrap().slice(2, s - len, s)?;
423 let o_ref = attention(
424 qv,
425 kslice.to_plain_array_view::<f32>()?.into_dimensionality()?,
426 vslice.to_plain_array_view::<f32>()?.into_dimensionality()?,
427 scale,
428 );
429 let a = Tensor::from(o_win);
430 let b = Tensor::from(o_ref);
431 a.close_enough(&b, Approximation::Approximate)
432 .with_context(|| format!("windowed != last-W at step {t}"))?;
433 }
434 Ok(())
435 }
436
437 #[test]
441 fn window_sdpa_decode_matches_last_w_in_model() -> TractResult<()> {
442 use tract_nnef::tract_core::ops::array::TypedConcat;
443 let (b, h, d, w) = (1usize, 2usize, 16usize, 5usize);
444 let scale = 1.0 / (d as f32).sqrt();
445 let mut model = TypedModel::default();
446 let s = model.sym("S");
447 let dim = |x: usize| x.to_dim();
448 let f: TVec<TDim> = tvec![dim(b), dim(h), s.into(), dim(d)];
449 let q = model.add_source("q", f32::fact(&f))?;
450 let k = model.add_source("k", f32::fact(&f))?;
451 let v = model.add_source("v", f32::fact(&f))?;
452 let o =
453 model.wire_node("win", WindowKvSdpa { axis: 2, window: w, scale: None }, &[q, k, v])?;
454 model.select_output_outlets(&o)?;
455 let mut rt = model.into_runnable()?.spawn()?;
456
457 let mk = |base: f32| -> Tensor {
458 let data: Vec<f32> = (0..b * h * d).map(|i| base + (i as f32 * 0.013).sin()).collect();
459 Tensor::from_shape(&[b, h, 1, d], &data).unwrap()
460 };
461 let grow = |acc: Option<Tensor>, x: Tensor| -> TractResult<Tensor> {
462 Ok(match acc {
463 None => x,
464 Some(a) => TypedConcat { axis: 2 }
465 .eval(&EvalContext::out_of_plan(), tvec![a.into(), x.into()])?
466 .remove(0)
467 .into_tensor(),
468 })
469 };
470 let (mut kf, mut vf): (Option<Tensor>, Option<Tensor>) = (None, None);
471 for t in 0..15 {
472 let qi = mk(9.0 + t as f32 * 0.1);
473 let ki = mk(1.0 + t as f32 * 0.07);
474 let vi = mk(5.0 - t as f32 * 0.05);
475 let o_model = rt
476 .run(tvec![qi.clone().into(), ki.clone().into(), vi.clone().into()])?
477 .remove(0)
478 .into_tensor();
479 kf = Some(grow(kf.take(), ki)?);
480 vf = Some(grow(vf.take(), vi)?);
481 let fk = kf.as_ref().unwrap();
482 let sk = fk.shape()[2];
483 let len = sk.min(w);
484 let kslice = fk.slice(2, sk - len, sk)?;
485 let vslice = vf.as_ref().unwrap().slice(2, sk - len, sk)?;
486 let qv = qi.to_plain_array_view::<f32>()?.into_dimensionality()?;
487 let o_ref = attention(
488 qv,
489 kslice.to_plain_array_view::<f32>()?.into_dimensionality()?,
490 vslice.to_plain_array_view::<f32>()?.into_dimensionality()?,
491 scale,
492 );
493 o_model
494 .close_enough(&Tensor::from(o_ref), Approximation::Approximate)
495 .with_context(|| format!("window decode != last-{w} at step {t}"))?;
496 }
497 Ok(())
498 }
499
500 #[test]
504 fn transform_fuses_cache_sdpa_to_windowed_decode() -> TractResult<()> {
505 use crate::ops::dyn_kv_cache::DynKeyValueCache;
506 use crate::ops::sdpa::Sdpa;
507 use tract_nnef::tract_core::ops::array::TypedConcat;
508 let (b, h, d, w) = (1usize, 2usize, 16usize, 5usize);
509 let scale = 1.0 / (d as f32).sqrt();
510 let mut model = TypedModel::default();
511 let s = model.sym("S");
512 let p = model.sym("P");
513 let dim = |x: usize| x.to_dim();
514 let newf: TVec<TDim> = tvec![dim(b), dim(h), s.clone().into(), dim(d)];
515 let qf: TVec<TDim> = tvec![dim(b), dim(h), s.into(), dim(d)];
516 let pastf: TVec<TDim> = tvec![dim(b), dim(h), p.into(), dim(d)];
517 let q = model.add_source("q", f32::fact(&qf))?;
518 let knew = model.add_source("k", f32::fact(&newf))?;
519 let vnew = model.add_source("v", f32::fact(&newf))?;
520 let mkc = |nm: &str| DynKeyValueCache {
521 name: nm.to_string(),
522 axis: 2,
523 past_sequence_fact: f32::fact(&pastf),
524 input_sequence_fact: f32::fact(&newf),
525 };
526 let kc = model.wire_node("kc", mkc("kc"), &[knew])?;
527 let vc = model.wire_node("vc", mkc("vc"), &[vnew])?;
528 let o = model.wire_node(
529 "sdpa",
530 Sdpa {
531 scale: None,
532 datum_type: f32::datum_type(),
533 acc_datum_type: f32::datum_type(),
534 is_causal: true,
535 },
536 &[q, kc[0], vc[0]],
537 )?;
538 model.select_output_outlets(&o)?;
539
540 WindowKvSdpaTransform { window: w }.transform(&mut model)?;
541
542 assert!(model.nodes().iter().any(|n| n.op_is::<WindowKvSdpa>()), "fused to WindowKvSdpa");
543 assert!(!model.nodes().iter().any(|n| n.op_is::<DynKeyValueCache>()), "caches removed");
544 assert!(!model.nodes().iter().any(|n| n.op_is::<Sdpa>()), "sdpa removed");
545
546 let mut rt = model.into_runnable()?.spawn()?;
547 let mk = |base: f32| -> Tensor {
548 let data: Vec<f32> = (0..b * h * d).map(|i| base + (i as f32 * 0.013).sin()).collect();
549 Tensor::from_shape(&[b, h, 1, d], &data).unwrap()
550 };
551 let grow = |acc: Option<Tensor>, x: Tensor| -> TractResult<Tensor> {
552 Ok(match acc {
553 None => x,
554 Some(a) => TypedConcat { axis: 2 }
555 .eval(&EvalContext::out_of_plan(), tvec![a.into(), x.into()])?
556 .remove(0)
557 .into_tensor(),
558 })
559 };
560 let (mut kf, mut vf): (Option<Tensor>, Option<Tensor>) = (None, None);
561 for t in 0..15 {
562 let qi = mk(9.0 + t as f32 * 0.1);
563 let ki = mk(1.0 + t as f32 * 0.07);
564 let vi = mk(5.0 - t as f32 * 0.05);
565 let o_model = rt
566 .run(tvec![qi.clone().into(), ki.clone().into(), vi.clone().into()])?
567 .remove(0)
568 .into_tensor();
569 kf = Some(grow(kf.take(), ki)?);
570 vf = Some(grow(vf.take(), vi)?);
571 let fk = kf.as_ref().unwrap();
572 let sk = fk.shape()[2];
573 let len = sk.min(w);
574 let kslice = fk.slice(2, sk - len, sk)?;
575 let vslice = vf.as_ref().unwrap().slice(2, sk - len, sk)?;
576 let qv = qi.to_plain_array_view::<f32>()?.into_dimensionality()?;
577 let o_ref = attention(
578 qv,
579 kslice.to_plain_array_view::<f32>()?.into_dimensionality()?,
580 vslice.to_plain_array_view::<f32>()?.into_dimensionality()?,
581 scale,
582 );
583 o_model
584 .close_enough(&Tensor::from(o_ref), Approximation::Approximate)
585 .with_context(|| format!("rewritten windowed decode != last-{w} at step {t}"))?;
586 }
587 Ok(())
588 }
589
590 #[test]
592 fn window_kv_sdpa_nnef_round_trip() -> TractResult<()> {
593 use crate::WithTractTransformers;
594 let (b, h, d) = (1usize, 2usize, 16usize);
595 let mut model = TypedModel::default();
596 let s = model.sym("S");
597 let dim = |x: usize| x.to_dim();
598 let f: TVec<TDim> = tvec![dim(b), dim(h), s.into(), dim(d)];
599 let q = model.add_source("q", f32::fact(&f))?;
600 let k = model.add_source("k", f32::fact(&f))?;
601 let v = model.add_source("v", f32::fact(&f))?;
602 let o = model.wire_node(
603 "win",
604 WindowKvSdpa { axis: 2, window: 4096, scale: Some(0.125) },
605 &[q, k, v],
606 )?;
607 model.select_output_outlets(&o)?;
608
609 let nnef = tract_nnef::nnef().with_tract_transformers();
610 let mut buffer = vec![];
611 nnef.write_to_tar(&model, &mut buffer)?;
612 let reloaded = nnef.model_for_read(&mut &*buffer)?;
613
614 let n = reloaded
615 .nodes()
616 .iter()
617 .find(|n| n.op_is::<WindowKvSdpa>())
618 .context("WindowKvSdpa survived the round-trip")?;
619 let op = n.op_as::<WindowKvSdpa>().unwrap();
620 assert_eq!(op.axis, 2);
621 assert_eq!(op.window, 4096);
622 assert_eq!(op.scale, Some(0.125));
623 Ok(())
624 }
625}