Skip to main content

logicaffeine_compile/codegen_cuda/lane/
arena.rs

1//! The step-local memory of a lane's forms. Every array a step computes and reads within the step — a tensor's `f32`
2//! (or `i32`) buffer, or its quantized form — lives in one arena the program allocates once, placed by its lifetime:
3//! from the launch that first writes it whole to the last launch that touches it, through any view of it. A launch's
4//! scratch lives there while the launch runs. The forms share the arena, one form running at a time.
5
6use std::collections::BTreeMap;
7
8use super::kernels;
9use super::plan::{quant_in_place, Fill, Launch, Schedule, Touch, QBLOCK};
10use crate::codegen_cuda::qwen_emit::Q8MatKind;
11
12/// Every array's alignment in the arena: `cudaMalloc`'s, past every vector load the kernels make.
13pub const ALIGN: usize = 256;
14
15/// An array placed in the arena: `bytes` from `offset`, live from launch `first` to launch `last` of its form.
16#[derive(Clone, Debug, PartialEq, Eq)]
17pub struct Placed {
18    pub name: String,
19    pub touch: Touch,
20    pub offset: usize,
21    pub bytes: usize,
22    pub first: usize,
23    pub last: usize,
24}
25
26/// A launch's scratch placed in the arena: `bytes` from `offset`, live while launch `launch` of its form runs.
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct PlacedScratch {
29    pub launch: usize,
30    pub tag: &'static str,
31    pub offset: usize,
32    pub bytes: usize,
33}
34
35/// A form's arena: its placed arrays, its launches' placed scratch, and the bytes they span.
36#[derive(Clone, Debug, Default, PartialEq, Eq)]
37pub struct Arena {
38    pub placed: Vec<Placed>,
39    pub scratch: Vec<PlacedScratch>,
40    pub bytes: usize,
41}
42
43impl Arena {
44    /// Where tensor `name`'s array `touch` lives, when it is placed.
45    pub fn at(&self, name: &str, touch: Touch) -> Option<&Placed> {
46        self.placed.iter().find(|p| p.name == name && p.touch == touch)
47    }
48
49    /// Where launch `launch`'s scratch `tag` lives.
50    pub fn scratch_at(&self, launch: usize, tag: &str) -> Option<&PlacedScratch> {
51        self.scratch.iter().find(|p| p.launch == launch && p.tag == tag)
52    }
53}
54
55/// The tensor whose array a touch of tensor `name` lands in: an alias's — a view's — is the tensor it is a part of,
56/// through every alias, but the quantized form of an alias whose rows start inside a block is its own.
57pub fn home<'a>(s: &'a Schedule, name: &'a str, touch: Touch) -> &'a str {
58    let mut at = name;
59    while let Some(Fill::Alias { of, offset }) = s.buffers.get(at).map(|b| &b.fill) {
60        if touch == Touch::Quant && !quant_in_place(*offset) {
61            break;
62        }
63        at = of;
64    }
65    at
66}
67
68/// A launch's scratch: `tag` names it beside the launch's result, `ty` is its element type and `bytes` its size. A
69/// counter — an arrival count the launch's last block resets — is zero before the launch and left zero by it, so it
70/// keeps a zeroed buffer of its own; the rest live in the arena while the launch runs.
71#[derive(Clone, Debug, PartialEq, Eq)]
72pub struct Scratch {
73    pub tag: &'static str,
74    pub ty: &'static str,
75    pub bytes: usize,
76    pub counter: bool,
77}
78
79/// The scratch launch `l` takes under `numerics`: the delta rule's unnormed outputs; an expert mixture's router logits,
80/// choices and weights, and over a block's rows its choices grouped by expert, each choice's gathered activation row,
81/// hidden row (gate and up rows apart on the tensor cores, the hidden row quantized alone when they are paired) and
82/// weighed result; a tensor-core projection's parts of the tiles its last wave cuts along `k`, and their counts; an `f32`
83/// GEMM's parts of the tiles it cuts along `k`; the arg-max's block maxima; a split attention's chunk partials (none in
84/// tiles); an attention in two words' keys and values in two words and their scales — and the arrival counts.
85pub fn scratch(l: &Launch, numerics: Q8MatKind) -> Vec<Scratch> {
86    let m = kernels::mode(numerics);
87    let two = numerics == Q8MatKind::TwoPassInt8;
88    let (acc, acc_ty) = kernels::accumulator(m);
89    let t = |tag, ty, bytes| Scratch { tag, ty, bytes, counter: false };
90    let counter = |tag, bytes| Scratch { tag, ty: "int", bytes, counter: true };
91    // A quantized activation of `n` values: its words, its low words under two words, its scales.
92    let quantized = |v: &mut Vec<Scratch>, [q, lo, sc]: [&'static str; 3], n: usize| {
93        v.push(t(q, "signed char", n));
94        if two {
95            v.push(t(lo, "signed char", n));
96        }
97        v.push(t(sc, "float", n / QBLOCK * 4));
98    };
99    match l {
100        // In chunks: each chunk's decayed keys and queries, `T·diag(β)`, masked scores and decays beside the outputs.
101        Launch::Delta { hv, d, rows, .. } if l.delta_in_chunks(numerics) => {
102            let [kg, qg, tb, sc, dec] = kernels::delta_chunk_scratch(rows.div_ceil(kernels::DELTA_CHUNK), *hv, *d);
103            vec![
104                t("o", "float", rows * hv * d * 4),
105                t("dk", "float", kg * 4),
106                t("dq", "float", qg * 4),
107                t("dt", "float", tb * 4),
108                t("dp", "float", sc * 4),
109                t("dg", "float", dec * 4),
110            ]
111        }
112        // Stepped a row at a time: its unnormed outputs, and each head's count of blocks arrived — a sequence's own — for
113        // the last to norm the head.
114        Launch::Delta { hv, d, rows, seqs, .. } => vec![t("o", "float", rows * hv * d * seqs * 4), counter("dc", hv * seqs * 4)],
115        // An `f32` GEMM cut along `k`: each part's sums, and each tile's count of parts arrived.
116        Launch::GemvF32 { n, m, split, .. } if *split > 1 && l.multiplies_in_tiles(numerics) => {
117            let tiles = n.div_ceil(64) * m.div_ceil(64);
118            vec![t("fp", "float", tiles * split * 256 * 16 * 4), counter("fq", tiles * 4)]
119        }
120        Launch::Moe { d, dff, k, experts, rows, router_split, .. } if *rows > 1 => {
121            let choices = rows * k;
122            let mut v = vec![
123                t("i", "int", choices * 4),
124                t("g", "float", choices * 4),
125                t("rl", "float", rows * experts * 4),
126                counter("rc", rows * 4),
127                t("ec", "int", experts * 4),
128                t("eo", "int", (experts + 1) * 4),
129                t("el", "int", choices * 4),
130                t("ep", "int", choices * 4),
131                t("pa", acc_ty, choices * d * acc),
132            ];
133            // The hidden rows `f32`, unless the paired GEMM quantizes them in its epilogue.
134            if !l.pairs_gate_and_up(numerics) {
135                v.push(t("h", "float", choices * dff * 4));
136            }
137            if m == 0 {
138                v.push(t("gx", "float", choices * d * 4));
139            } else {
140                quantized(&mut v, ["gq", "gl", "gs"], choices * d);
141                quantized(&mut v, ["hq", "hl", "hs"], choices * dff);
142            }
143            if l.on_tensor_cores(numerics) && !l.pairs_gate_and_up(numerics) {
144                v.extend([t("hg", "float", choices * dff * 4), t("hu", "float", choices * dff * 4)]);
145            }
146            // An `f32` router's logits GEMM cut along `k`: each part's sums, and each tile's count of parts arrived.
147            if *router_split > 1 {
148                let tiles = experts.div_ceil(64) * rows.div_ceil(64);
149                v.extend([t("rp", "float", tiles * router_split * 256 * 16 * 4), counter("rq", tiles * 4)]);
150            }
151            v
152        }
153        Launch::Moe { dff, k, experts, shared, .. } => {
154            let hidden = k * dff + shared.as_ref().map_or(0, |e| e.dff);
155            let mut v = vec![
156                t("i", "int", k * 4),
157                t("g", "float", k * 4),
158                t("h", "float", hidden * 4),
159                t("rl", "float", experts * 4),
160                counter("rc", 4),
161            ];
162            if m > 0 {
163                quantized(&mut v, ["hq", "hl", "hs"], hidden);
164            }
165            v
166        }
167        // A tensor-core projection's tiles cut along `k` (a GEMM in parts' over its parts together): each part's sums,
168        // and each cut tile's count of parts arrived.
169        Launch::Gemv { m, tail: Some((whole, split)), .. }
170        | Launch::GemmParts { m, tail: Some((whole, split)), .. }
171        | Launch::GemvGlu { m, tail: Some((whole, split)), .. }
172            if l.on_tensor_cores(numerics) =>
173        {
174            let rows = l.gemm_rows(numerics);
175            let tiles = l.gemm_tiles_wide() * m.div_ceil(rows);
176            let parts = (tiles - whole) * split;
177            vec![t("tp", "float", parts * rows * kernels::GEMM_TILE_N * 4), counter("tc", (tiles - whole) * 4)]
178        }
179        Launch::Fold { first, then } => {
180            let mut v = scratch(first, numerics);
181            v.extend(match (first.as_ref(), then.as_ref()) {
182                // A gather that norms each row in the block that gathers it counts nothing.
183                (Launch::Gather { .. }, _) => None,
184                // Projections that turn their heads as they finish: each head's count of rows written.
185                (_, Launch::RopeParts { sides, .. }) => Some(counter("gc", sides.iter().map(|s| s.heads).sum::<usize>() * 4)),
186                // A mixture's sum with another array is made where the mixture writes its result.
187                (_, Launch::Binary { .. }) => None,
188                // A chunk's mixture norms each row in the block that combines it.
189                _ => match l.finished_mixture() {
190                    Some((Launch::Moe { rows, .. }, ..)) if *rows > 1 => None,
191                    // A projection or the decode step's mixture that norms in its last block: the count of its blocks arrived.
192                    _ => Some(counter("gc", 4)),
193                },
194            });
195            v
196        }
197        // A batch's gated pair on the tensor cores: its gate's and its up's sums, gated after.
198        Launch::GemvGlu { n, m, .. } if l.batch_on_tensor_cores(numerics) => vec![t("mg", "float", m * n * 4), t("mu", "float", m * n * 4)],
199        Launch::ArgMax { n, rows, .. } => {
200            let blocks = kernels::argmax_blocks(*n);
201            vec![t("av", "float", rows * blocks * 4), t("ai", "int", rows * blocks * 4), counter("ac", rows * 4)]
202        }
203        // In two words: every key and value row the attention reads, its high and low words, and each row's two scales.
204        Launch::Attention { heads, group, hd, vd, len, .. } if l.attends_in_two_words(numerics) => {
205            let [k, v, sc] = kernels::attn_i8_scratch(*len, heads / group, *hd, *vd);
206            vec![t("ak", "signed char", k), t("aw", "signed char", v), t("as", "float", sc)]
207        }
208        // Every head of the group in one block: each chunk's maximum, sum and weighed values, for the fold.
209        Launch::Attention { heads, vd, rows, shared: Some(splits), .. } if *splits > 1 => vec![t("ap", "float", rows * heads * splits * (vd + 2) * 4)],
210        Launch::Attention { shared: Some(_), .. } => Vec::new(),
211        Launch::Attention { heads, vd, rows, per_block, splits, parallel: true, shared: None, .. } if !l.attends_in_tiles(numerics) => {
212            vec![t("ap", acc_ty, rows * heads * splits * (vd + 2) * acc), counter("ac", rows * heads / per_block * 4)]
213        }
214        _ => Vec::new(),
215    }
216}
217
218fn aligned(n: usize) -> usize {
219    n.next_multiple_of(ALIGN)
220}
221
222/// The bytes of a tensor of `len` elements' array: its `f32` buffer, or its quantized form — the int8 words, then the
223/// low words under two words, then a scale a block — each part aligned.
224pub fn array_bytes(len: usize, touch: Touch, two: bool) -> usize {
225    match touch {
226        Touch::Plain => aligned(len * 4),
227        Touch::Quant => aligned(len) * if two { 2 } else { 1 } + aligned(len / QBLOCK * 4),
228    }
229}
230
231/// Where the parts of a quantized array placed at `offset` start: its words, its low words under two words, its
232/// scales.
233pub fn quant_parts(offset: usize, len: usize, two: bool) -> (usize, Option<usize>, usize) {
234    let words = if two { 2 } else { 1 };
235    (offset, two.then(|| offset + aligned(len)), offset + aligned(len) * words)
236}
237
238/// How much of an array the step has written before any launch reads it.
239#[derive(Clone, Debug)]
240enum Written {
241    /// All of it.
242    Whole,
243    /// Parts of every unit of `pitch`, these `[from, to)` so far.
244    Parts(usize, Vec<(usize, usize)>),
245    /// Read, or written some other way, before it was all written.
246    Unsafe,
247}
248
249impl Written {
250    /// The array after a launch that reads it (`read`), writes it whole (`whole`), writes these parts of every unit
251    /// (`parts`), or writes it some other way (`other`).
252    fn then(self, read: bool, whole: bool, parts: &[(usize, usize, usize)], other: bool) -> Written {
253        let open = match self {
254            Written::Parts(pitch, have) => Some((pitch, have)),
255            done => return done,
256        };
257        if read {
258            return Written::Unsafe;
259        }
260        if whole {
261            return Written::Whole;
262        }
263        if other || parts.is_empty() {
264            return Written::Unsafe;
265        }
266        let (pitch, mut have) = open.unwrap_or((parts[0].0, Vec::new()));
267        for &(p, from, width) in parts {
268            if p != pitch {
269                return Written::Unsafe;
270            }
271            have.push((from, from + width));
272        }
273        have.sort_unstable();
274        let mut reach = 0;
275        for &(from, to) in &have {
276            if from > reach {
277                break;
278            }
279            reach = reach.max(to);
280        }
281        if reach >= pitch {
282            Written::Whole
283        } else {
284            Written::Parts(pitch, have)
285        }
286    }
287}
288
289/// Place the arrays of `s` that live within its step: a computed tensor's that is not one of the step's inputs or
290/// outputs, all of it written before any launch reads it — whole, or part by part over every unit (`join_in_place`) —
291/// so nothing the step reads of it was left by another array, or by an earlier step. Each lives from its first touch to
292/// its last, through any view of it. The largest first, each at the lowest aligned offset clear of every array placed
293/// live beside it.
294pub fn place(s: &Schedule) -> Arena {
295    let two = s.numerics == Q8MatKind::TwoPassInt8;
296    let io = s.io.names();
297    // A touch through a view is a touch of the tensor it views — of its quantized form too, when the view starts on a
298    // whole block and so reads it in place (`home`).
299    let viewed = |name: &str, touch: Touch| home(s, name, touch).to_string();
300    #[derive(Default)]
301    struct Here {
302        read: bool,
303        whole: bool,
304        parts: Vec<(usize, usize, usize)>,
305        other: bool,
306    }
307    let mut life: BTreeMap<(String, Touch), (usize, usize, Written)> = BTreeMap::new();
308    for (i, l) in s.launches.iter().enumerate() {
309        let (reads, writes) = l.touches(s.numerics);
310        let mut here: BTreeMap<(String, Touch), Here> = BTreeMap::new();
311        for (t, touch) in reads {
312            here.entry((viewed(t, touch), touch)).or_default().read = true;
313        }
314        for (t, touch) in writes {
315            let at = viewed(t, touch);
316            let len = s.buffers.get(t).map_or(0, |b| b.len);
317            let own = at == t;
318            let e = here.entry((at, touch)).or_default();
319            if own && l.writes_whole(t, touch, len) {
320                e.whole = true;
321            } else if let (true, Touch::Plain, Some(part)) = (own, touch, l.writes_part(t, len)) {
322                e.parts.push(part);
323            } else {
324                e.other = true;
325            }
326        }
327        for (k, h) in here {
328            let e = life.entry(k).or_insert((i, i, Written::Parts(0, Vec::new())));
329            e.1 = i;
330            let first = e.0 == i;
331            let state = std::mem::replace(&mut e.2, Written::Unsafe);
332            e.2 = if first {
333                Written::Parts(h.parts.first().map_or(0, |p| p.0), Vec::new()).then(h.read, h.whole, &h.parts, h.other)
334            } else {
335                state.then(h.read, h.whole, &h.parts, h.other)
336            };
337        }
338    }
339    #[derive(PartialEq, Eq, PartialOrd, Ord)]
340    enum Item {
341        Array(String, Touch),
342        Scratch(usize, &'static str),
343    }
344    let mut todo: Vec<(usize, usize, usize, Item)> = life
345        .into_iter()
346        .filter(|(_, (.., written))| matches!(written, Written::Whole))
347        .filter_map(|((name, touch), (first, last, _))| {
348            let b = s.buffers.get(&name)?;
349            let allocated = if touch == Touch::Plain { b.plain } else { b.quantized };
350            (b.fill == Fill::Computed && allocated && !io.contains(&name.as_str()))
351                .then(|| (array_bytes(b.len, touch, two), first, last, Item::Array(name, touch)))
352        })
353        .collect();
354    for (i, l) in s.launches.iter().enumerate() {
355        for sc in scratch(l, s.numerics).into_iter().filter(|sc| !sc.counter) {
356            todo.push((aligned(sc.bytes), i, i, Item::Scratch(i, sc.tag)));
357        }
358    }
359    todo.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)).then(a.3.cmp(&b.3)));
360    let mut arena = Arena::default();
361    let mut spans: Vec<(usize, usize, usize, usize)> = Vec::new();
362    for (bytes, first, last, item) in todo {
363        let mut busy: Vec<(usize, usize)> =
364            spans.iter().filter(|&&(_, _, f, l)| f <= last && first <= l).map(|&(o, b, _, _)| (o, o + b)).collect();
365        busy.sort_unstable();
366        let mut offset = 0;
367        for (lo, hi) in busy {
368            if offset + bytes <= lo {
369                break;
370            }
371            offset = offset.max(hi);
372        }
373        arena.bytes = arena.bytes.max(offset + bytes);
374        spans.push((offset, bytes, first, last));
375        match item {
376            Item::Array(name, touch) => arena.placed.push(Placed { name, touch, offset, bytes, first, last }),
377            Item::Scratch(launch, tag) => arena.scratch.push(PlacedScratch { launch, tag, offset, bytes }),
378        }
379    }
380    arena
381}