Skip to main content

logicaffeine_compile/codegen_cuda/lane/
residence.rs

1//! Where a lane's arrays live. Every array the program allocates goes on the device while they fit the memory it is
2//! given; past it, constant matrices move to pinned host memory the device maps, in the order that costs a step least:
3//! a table only ever gathered from (a row a token), then each expert stack from the last layer's back (a step reads its
4//! chosen experts' rows), then the other matrices from the last read back — until the rest fits. A kernel reads a mapped
5//! matrix through the same pointer it reads one in device memory, so where a matrix lives changes no value. Matrices
6//! that read the same checkpoint rows (a tied head and the embedding table it is) share one upload.
7//!
8//! The compiler fixes the order ([`plan`]); the program decides how far down it to go when it starts, from the device
9//! memory free once its context is up (or the `--device-bytes` it is given), less its [`Plan::spare`] — [`place`] is
10//! that decision, as the program makes it. The arena is an allocation of its own; every other array the program keeps
11//! on the device is carved from one pool, and every matrix it moves from one mapped host pool, each at [`POOL_ALIGN`],
12//! so the bytes counted are the bytes the device gives up.
13
14use std::collections::{BTreeMap, BTreeSet};
15
16use super::arena;
17use super::plan::{quant_in_place, Fill, Launch, Touch, QBLOCK};
18use super::weights::{Placement, Rows};
19use super::{Form, FormKind, LaneError};
20use crate::codegen_cuda::qwen_emit::Q8MatKind;
21use crate::model::quant::GgmlType;
22
23/// The device memory a chip loses before the program allocates: what the driver holds back from every program (379 MiB
24/// of the RTX 3060's 12 GiB under driver 580) and the program's context with its modules loaded (113 MiB).
25pub const CONTEXT_RESERVE: usize = 512 << 20;
26
27/// What the program's graphs, streams and events, and the rounding of its three allocations to the driver's 2 MiB
28/// granule, take of the device beside its arrays, past [`SPARE_A_LAUNCH`] for each launch it captures.
29pub const SPARE: usize = 32 << 20;
30
31/// The device memory each captured launch's graph node takes, at twice the 8 KiB a node of a 1000-launch graph takes
32/// on the RTX 3060.
33pub const SPARE_A_LAUNCH: usize = 16 << 10;
34
35/// The alignment of every array carved from a pool: `cudaMalloc`'s, the arena's.
36pub const POOL_ALIGN: usize = arena::ALIGN;
37
38/// The bytes an array of `n` bytes takes of its pool.
39pub fn pooled(n: usize) -> usize {
40    n.next_multiple_of(POOL_ALIGN)
41}
42
43/// What a lane's program allocates, before it decides where its matrices live.
44#[derive(Clone, Debug, Default, PartialEq, Eq)]
45pub struct Plan {
46    /// The arena's bytes, an allocation of its own.
47    pub arena: usize,
48    /// The bytes of the device pool's arrays that stay on the device whatever the budget: the buffers, scratch,
49    /// computed constants, and the generation's position, run, count and prompt.
50    pub fixed: usize,
51    /// Every matrix the program uploads and the bytes it takes, in the order they leave the device.
52    pub order: Vec<(String, usize)>,
53    /// The matrices that read the rows another matrix uploads, each with the name of the one that uploads them.
54    pub shares: BTreeMap<String, String>,
55    /// The device memory the program takes beside its arrays ([`SPARE`], [`SPARE_A_LAUNCH`]).
56    pub spare: usize,
57}
58
59impl Plan {
60    /// Every array on the device: the arena, the fixed arrays and every matrix.
61    pub fn whole(&self) -> usize {
62        self.arena + self.fixed + self.order.iter().map(|(_, b)| b).sum::<usize>()
63    }
64}
65
66/// Where the program's matrices live on a budget.
67#[derive(Clone, Debug, Default, PartialEq, Eq)]
68pub struct Residence {
69    /// The matrices in mapped host memory.
70    pub host: BTreeSet<String>,
71    /// How many of the plan's order moved: the first.
72    pub moved: usize,
73    /// The bytes the program allocates on the device: its arena and its pool.
74    pub device_bytes: usize,
75    /// The bytes of its mapped host pool.
76    pub host_bytes: usize,
77}
78
79/// The constant buffers every form shares, allocated once: the constants, and the carried state the decode step holds.
80pub fn shared_names(forms: &[Form]) -> BTreeSet<String> {
81    let decode = forms.iter().find(|f| f.rows == 1);
82    forms
83        .iter()
84        .flat_map(|f| f.schedule.buffers.values())
85        .filter(|b| match b.fill {
86            Fill::Constant(_) => true,
87            Fill::Zero => decode.is_some_and(|d| d.schedule.buffers.contains_key(&b.name)),
88            _ => false,
89        })
90        .map(|b| b.name.clone())
91        .collect()
92}
93
94/// The bytes a matrix kept as rows takes of its pool: a Q8_0 matrix's levels and its half scales, each an array; any
95/// other format's blocks as the checkpoint lays them out and the 32 bytes a staged read runs past them.
96pub fn matrix_bytes(rows: usize, cols: usize, ty: GgmlType) -> usize {
97    match ty {
98        GgmlType::Q8_0 => pooled(rows * cols) + pooled(rows * (cols / QBLOCK) * 2),
99        _ => pooled(rows * ty.bytes_for(cols) + 32),
100    }
101}
102
103/// The checkpoint rows a matrix uploads, whatever name and orientation declare them: their format, width and runs.
104fn upload(r: &Rows) -> (u32, usize, Vec<(u64, usize)>) {
105    (r.ty.id(), r.cols, r.runs.iter().map(|run| (run.offset, run.rows)).collect())
106}
107
108/// What the program of `forms` allocates, its caches `ctx` positions long: the arena, the arrays it keeps on the device,
109/// its matrices in the order they leave it, and the spare its graphs take.
110pub fn plan(forms: &[Form], ctx: usize) -> Plan {
111    let two = forms[0].schedule.numerics == Q8MatKind::TwoPassInt8;
112    let shared = shared_names(forms);
113    let mut out = Plan { arena: forms.iter().map(|f| f.arena.bytes).max().unwrap_or(0), ..Plan::default() };
114    // The position, the generated run, its count and the prompt; a batch's every sequence's position, run and count.
115    out.fixed = pooled(4) + pooled(ctx * 4) + pooled(4) + pooled(ctx * 4);
116    if let Some(b) = forms.iter().find(|f| f.kind == FormKind::Batch) {
117        out.fixed += pooled(b.rows * 4) + pooled(b.rows * ctx * 4) + pooled(b.rows * 4);
118    }
119    let mut matrices: BTreeMap<String, usize> = BTreeMap::new();
120    let mut uploads: BTreeMap<(u32, usize, Vec<(u64, usize)>), String> = BTreeMap::new();
121    let mut seen = BTreeSet::new();
122    for f in forms {
123        for buf in f.schedule.buffers.values() {
124            if f.schedule.io.positions().contains(&buf.name.as_str()) || (shared.contains(&buf.name) && !seen.insert(buf.name.clone())) {
125                continue;
126            }
127            match &buf.fill {
128                Fill::Constant(Placement::Rows(r)) => match uploads.get(&upload(r)) {
129                    Some(of) => {
130                        out.shares.insert(buf.name.clone(), of.clone());
131                    }
132                    None => {
133                        uploads.insert(upload(r), buf.name.clone());
134                        matrices.insert(buf.name.clone(), matrix_bytes(r.rows, r.cols, r.ty));
135                    }
136                },
137                Fill::Constant(Placement::Values(v)) => out.fixed += pooled(v.len() * 4),
138                Fill::Alias { offset, .. } if buf.quantized && quant_in_place(*offset) => continue,
139                Fill::Alias { .. } => {}
140                Fill::Param | Fill::Zero | Fill::Computed => {
141                    if buf.plain && f.arena.at(&buf.name, Touch::Plain).is_none() {
142                        out.fixed += pooled(buf.len * buf.dtype.bytes());
143                    }
144                }
145            }
146            if buf.quantized && f.arena.at(&buf.name, Touch::Quant).is_none() {
147                out.fixed += pooled(buf.len) + pooled(buf.len / QBLOCK * 4) + if two { pooled(buf.len) } else { 0 };
148            }
149        }
150        for (i, l) in f.schedule.launches.iter().enumerate() {
151            for sc in arena::scratch(l, f.schedule.numerics) {
152                if sc.counter || f.arena.scratch_at(i, sc.tag).is_none() {
153                    out.fixed += pooled(sc.bytes);
154                }
155            }
156        }
157    }
158    out.order = eviction_order(forms, &matrices, &out.shares).into_iter().map(|m| {
159        let bytes = matrices[&m];
160        (m, bytes)
161    }).collect();
162    out.spare = SPARE + forms.iter().map(|f| f.schedule.launches.len()).sum::<usize>() * SPARE_A_LAUNCH;
163    out
164}
165
166/// Where `plan`'s matrices live on `budget` bytes of device memory: as many of its order as must move to mapped host
167/// memory for the rest to fit, and the bytes each side takes — the decision the program makes when it starts. Refused
168/// when the arrays but the matrices outrun the budget.
169pub fn place(plan: &Plan, budget: usize) -> Result<Residence, LaneError> {
170    let fixed = plan.arena + plan.fixed;
171    if fixed > budget {
172        return Err(LaneError::Memory { needed: fixed, budget });
173    }
174    let mut out = Residence { device_bytes: plan.whole(), ..Residence::default() };
175    for (name, bytes) in &plan.order {
176        if out.device_bytes <= budget {
177            break;
178        }
179        out.device_bytes -= bytes;
180        out.host_bytes += bytes;
181        out.host.insert(name.clone());
182        out.moved += 1;
183    }
184    Ok(out)
185}
186
187/// The matrices in the order they leave the device: those only gathered from, then the expert stacks, then the rest —
188/// each class from the decode step's last read back. A read of a matrix that `shares` another's upload reads that one.
189fn eviction_order(forms: &[Form], matrices: &BTreeMap<String, usize>, shares: &BTreeMap<String, String>) -> Vec<String> {
190    let decode = forms.iter().find(|f| f.rows == 1).expect("a lane has a decode step");
191    let mut class: BTreeMap<&str, u8> = BTreeMap::new();
192    let mut last: BTreeMap<&str, usize> = BTreeMap::new();
193    for f in forms {
194        for l in f.schedule.launches.iter().flat_map(|l| l.parts()) {
195            let experts: Vec<&str> = match l {
196                Launch::Moe { wg, wu, wd, .. } => vec![wg, wu, wd],
197                _ => Vec::new(),
198            };
199            for t in l.reads() {
200                let c = if matches!(l, Launch::Gather { .. }) {
201                    0
202                } else if experts.contains(&t) {
203                    1
204                } else {
205                    2
206                };
207                let t = shares.get(t).map_or(t, String::as_str);
208                if matrices.contains_key(t) {
209                    let e = class.entry(t).or_insert(c);
210                    *e = (*e).max(c);
211                }
212            }
213        }
214    }
215    for (i, l) in decode.schedule.launches.iter().enumerate() {
216        for t in l.reads() {
217            let t = shares.get(t).map_or(t, String::as_str);
218            if matrices.contains_key(t) {
219                last.insert(t, i);
220            }
221        }
222    }
223    let mut order: Vec<&String> = matrices.keys().collect();
224    order.sort_by_key(|m| (class.get(m.as_str()).copied().unwrap_or(2), std::cmp::Reverse(last.get(m.as_str()).copied().unwrap_or(0)), m.as_str()));
225    order.into_iter().cloned().collect()
226}