Skip to main content

rlx_runtime/
kv_cache.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5//! Per-layer K/V cache for autoregressive decode (Whisper, Qwen, Gemma, …).
6
7use crate::compile_cache::pad_rows;
8
9/// Layer-wise past K/V tensors in row-major `[past_len * kv_dim]` layout per layer.
10#[derive(Debug, Clone)]
11pub struct LayerKvCache {
12    pub past_len: usize,
13    pub layers_k: Vec<Vec<f32>>,
14    pub layers_v: Vec<Vec<f32>>,
15    /// Absolute token index of row `0` in each layer's K/V buffer after
16    /// sliding-window trim (Gemma 3/4 ISWA). Zero when the buffer holds
17    /// the full prefix from position `0`.
18    pub layers_kv_base: Vec<usize>,
19}
20
21impl LayerKvCache {
22    pub fn from_layer_outputs(
23        num_layers: usize,
24        batch: usize,
25        past_seq: usize,
26        kv_dim: usize,
27        outputs: &[Vec<f32>],
28    ) -> Result<Self, String> {
29        let dims: Vec<usize> = vec![kv_dim; num_layers];
30        Self::from_layer_outputs_per_layer(num_layers, batch, past_seq, &dims, outputs)
31    }
32
33    /// Like [`Self::from_layer_outputs`] but accepts a per-layer
34    /// `kv_dim` vector. Gemma 4 12B's full-attention layers have
35    /// `kv_dim = 1 * 512 = 512` while sliding layers have `8 * 256 =
36    /// 2048`; this constructor handles that heterogeneity.
37    pub fn from_layer_outputs_per_layer(
38        num_layers: usize,
39        batch: usize,
40        past_seq: usize,
41        kv_dims: &[usize],
42        outputs: &[Vec<f32>],
43    ) -> Result<Self, String> {
44        if outputs.len() != 2 * num_layers {
45            return Err(format!(
46                "from_layer_outputs_per_layer: expected {} K/V tensors, got {}",
47                2 * num_layers,
48                outputs.len()
49            ));
50        }
51        if kv_dims.len() != num_layers {
52            return Err(format!(
53                "from_layer_outputs_per_layer: expected {} kv_dims, got {}",
54                num_layers,
55                kv_dims.len()
56            ));
57        }
58        let mut layers_k = Vec::with_capacity(num_layers);
59        let mut layers_v = Vec::with_capacity(num_layers);
60        for layer in 0..num_layers {
61            let kv_dim = kv_dims[layer];
62            let expected = batch * past_seq * kv_dim;
63            let k = &outputs[2 * layer];
64            let v = &outputs[2 * layer + 1];
65            if k.len() != expected || v.len() != expected {
66                return Err(format!(
67                    "layer {layer}: k.len={} v.len={} expected {expected} (kv_dim={kv_dim})",
68                    k.len(),
69                    v.len()
70                ));
71            }
72            layers_k.push(k.clone());
73            layers_v.push(v.clone());
74        }
75        Ok(Self {
76            past_len: past_seq,
77            layers_k,
78            layers_v,
79            layers_kv_base: vec![0; num_layers],
80        })
81    }
82
83    /// Pad each layer's K/V to `upper` rows along the sequence axis (`kv_dim` inner).
84    pub fn pad_layers_to_upper(&self, upper: u64, kv_dim: usize) -> (Vec<Vec<f32>>, Vec<Vec<f32>>) {
85        let dims: Vec<usize> = vec![kv_dim; self.layers_k.len()];
86        self.pad_layers_to_upper_per_layer(upper, &dims)
87    }
88
89    /// Like [`Self::pad_layers_to_upper`] but pads each layer to its
90    /// own `kv_dim`. The number of dims must equal the number of
91    /// cached layers.
92    pub fn pad_layers_to_upper_per_layer(
93        &self,
94        upper: u64,
95        kv_dims: &[usize],
96    ) -> (Vec<Vec<f32>>, Vec<Vec<f32>>) {
97        assert_eq!(
98            kv_dims.len(),
99            self.layers_k.len(),
100            "pad_layers_to_upper_per_layer: kv_dims len {} != layers {}",
101            kv_dims.len(),
102            self.layers_k.len(),
103        );
104        let padded_k = self
105            .layers_k
106            .iter()
107            .zip(kv_dims.iter())
108            .map(|(k, &d)| pad_rows(k, d, upper))
109            .collect();
110        let padded_v = self
111            .layers_v
112            .iter()
113            .zip(kv_dims.iter())
114            .map(|(v, &d)| pad_rows(v, d, upper))
115            .collect();
116        (padded_k, padded_v)
117    }
118
119    /// Update cache from decode outputs: `[logits, k0, v0, k1, v1, …]` (bucket-padded).
120    pub fn advance_from_decode_outputs(
121        &mut self,
122        outputs: Vec<Vec<f32>>,
123        batch: usize,
124        kv_dim: usize,
125    ) -> Result<(), String> {
126        let dims: Vec<usize> = vec![kv_dim; self.layers_k.len()];
127        self.advance_from_decode_outputs_per_layer(outputs, batch, &dims)
128    }
129
130    /// Trim each layer's K/V history to at most `window` rows on
131    /// the sequence axis, keeping the most recent rows. Used by
132    /// Gemma 3/4 sliding-attention layers — long contexts can keep
133    /// only the last `window` (e.g. 1024) tokens per sliding layer
134    /// without affecting attention semantics (those layers mask out
135    /// older positions anyway).
136    ///
137    /// `kv_dims_keep` selects which layers to trim and at what dim:
138    /// `kv_dims_keep[i] = Some((dim, window))` trims layer `i`,
139    /// `None` leaves the layer untouched. Pass-through for layers
140    /// whose attention is full-causal.
141    ///
142    /// Note: `past_len` is unchanged — the per-layer K/V buffers
143    /// just hold fewer real rows now; the decode flow's per-layer
144    /// `past_k_{i}` input shape will see the trimmed length. Caller
145    /// is responsible for ensuring the graph's declared `past_seq`
146    /// matches the trimmed length OR the trimmed layer is bound
147    /// dynamically.
148    pub fn trim_sliding_window_per_layer(
149        &mut self,
150        kv_dims_keep: &[Option<(usize, usize)>],
151    ) -> Result<(), String> {
152        if kv_dims_keep.len() != self.layers_k.len() {
153            return Err(format!(
154                "trim_sliding_window_per_layer: kv_dims_keep len {} != layers {}",
155                kv_dims_keep.len(),
156                self.layers_k.len(),
157            ));
158        }
159        for (i, spec) in kv_dims_keep.iter().enumerate() {
160            let Some((kv_dim, window)) = spec else {
161                continue;
162            };
163            let kv_dim = *kv_dim;
164            let window = *window;
165            if window == 0 || kv_dim == 0 {
166                continue;
167            }
168            let rows = self.layers_k[i].len() / kv_dim;
169            if rows <= window {
170                continue;
171            }
172            let drop_rows = rows - window;
173            let drop_bytes = drop_rows * kv_dim;
174            self.layers_k[i].drain(..drop_bytes);
175            self.layers_v[i].drain(..drop_bytes);
176            if self.layers_kv_base.len() <= i {
177                self.layers_kv_base.resize(self.layers_k.len(), 0);
178            }
179            self.layers_kv_base[i] = self.layers_kv_base[i].saturating_add(drop_rows);
180        }
181        Ok(())
182    }
183
184    /// Per-layer variant of [`Self::advance_from_decode_outputs`].
185    pub fn advance_from_decode_outputs_per_layer(
186        &mut self,
187        outputs: Vec<Vec<f32>>,
188        _batch: usize,
189        kv_dims: &[usize],
190    ) -> Result<(), String> {
191        let n = self.layers_k.len();
192        if outputs.len() != 1 + 2 * n {
193            return Err(format!(
194                "advance_from_decode_outputs_per_layer: expected {} outputs, got {}",
195                1 + 2 * n,
196                outputs.len()
197            ));
198        }
199        if kv_dims.len() != n {
200            return Err(format!(
201                "advance_from_decode_outputs_per_layer: kv_dims len {} != layers {n}",
202                kv_dims.len()
203            ));
204        }
205        let new_len = self.past_len + 1;
206        let mut iter = outputs.into_iter();
207        let _logits = iter.next().ok_or("missing logits")?;
208        for i in 0..n {
209            let k = iter.next().ok_or("missing k")?;
210            let v = iter.next().ok_or("missing v")?;
211            let real_len = new_len * kv_dims[i];
212            self.layers_k[i] = k[..real_len.min(k.len())].to_vec();
213            self.layers_v[i] = v[..real_len.min(v.len())].to_vec();
214        }
215        self.past_len = new_len;
216        Ok(())
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn sliding_window_trim_keeps_last_w_rows() {
226        // 3 layers, each storing 6 rows of kv_dim=4 = 24 floats.
227        let kv_dim = 4;
228        let rows = 6;
229        let mut cache = LayerKvCache {
230            past_len: rows,
231            layers_k: vec![(0..(rows * kv_dim)).map(|x| x as f32).collect(); 3],
232            layers_v: vec![(0..(rows * kv_dim)).map(|x| x as f32).collect(); 3],
233            layers_kv_base: vec![0; 3],
234        };
235        // Trim layer 0 to last 2 rows; layer 1 untouched; layer 2 to last 4.
236        let spec = [Some((kv_dim, 2)), None, Some((kv_dim, 4))];
237        cache.trim_sliding_window_per_layer(&spec).unwrap();
238        assert_eq!(cache.layers_k[0].len(), 2 * kv_dim);
239        assert_eq!(cache.layers_kv_base[0], 4);
240        assert_eq!(cache.layers_kv_base[1], 0);
241        assert_eq!(cache.layers_kv_base[2], 2);
242        // Layer 0 should now hold the LAST 2 rows: rows 4 and 5.
243        assert_eq!(
244            cache.layers_k[0],
245            vec![16., 17., 18., 19., 20., 21., 22., 23.]
246        );
247        assert_eq!(
248            cache.layers_k[1].len(),
249            6 * kv_dim,
250            "untouched layer keeps full history"
251        );
252        assert_eq!(cache.layers_k[2].len(), 4 * kv_dim);
253    }
254
255    #[test]
256    fn sliding_window_trim_no_op_when_under_window() {
257        let kv_dim = 4;
258        let rows = 3;
259        let mut cache = LayerKvCache {
260            past_len: rows,
261            layers_k: vec![vec![1.0f32; rows * kv_dim]],
262            layers_v: vec![vec![2.0f32; rows * kv_dim]],
263            layers_kv_base: vec![0],
264        };
265        cache
266            .trim_sliding_window_per_layer(&[Some((kv_dim, 10))])
267            .unwrap();
268        assert_eq!(cache.layers_k[0].len(), rows * kv_dim);
269    }
270}