1pub mod budget;
28pub mod caches;
29pub mod copies;
30pub mod render;
31pub mod weights;
32mod workspace;
33
34use std::collections::BTreeMap;
35
36pub use budget::{BudgetError, DriverTerms};
37pub use caches::{CacheInputs, CacheTerm, LookupInputs};
38pub use copies::{CopyRule, parse_copies};
39pub use weights::NodeWeights;
40pub use workspace::WorkspaceTerm;
41
42use crate::fuser::FusionPlan;
43use crate::ir::{Circuit, NodeIdx};
44use crate::planner;
45use crate::state::{StateAccess, StateError, StateInputs, StatePlan};
46use crate::venn::families::Families;
47
48#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
50pub enum MemoryError {
51 #[error("node `{node}`: the memory model needs dim `{dim}`, which the circuit does not define")]
53 MissingDim {
54 node: String,
56 dim: String,
58 },
59 #[error("node `{node}`: {what} has no byte size")]
61 Size {
62 node: String,
64 what: String,
66 },
67 #[error(transparent)]
69 State(#[from] StateError),
70 #[error(transparent)]
72 Plan(#[from] planner::PlanError),
73 #[error("family `{family}` workspace `{name}`: {detail}")]
75 Workspace {
76 family: String,
78 name: String,
80 detail: String,
82 },
83 #[error(transparent)]
85 Budget(#[from] BudgetError),
86}
87
88#[derive(Debug, Clone, Copy)]
90pub struct ActivationRun<'a> {
91 pub label: &'a str,
93 pub rows: u64,
95 pub plan: &'a FusionPlan,
97}
98
99#[derive(Clone, Copy)]
101pub struct MemoryInputs<'a> {
102 pub served: &'a Circuit,
104 pub declared: &'a Circuit,
106 pub copies: &'a [CopyRule],
108 pub settings: &'a BTreeMap<String, String>,
110 pub states: &'a StateInputs,
112 pub draft_kv_dtype: Option<crate::state::StateDtype>,
115 pub caches: &'a CacheInputs,
117 pub runs: &'a [ActivationRun<'a>],
119 pub families: Option<(&'a Families, u64)>,
121 pub legacy_arena: Option<u64>,
123 pub driver: DriverTerms,
125 pub budget_bytes: u64,
127 pub chunk_slack: u64,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct NodeMemory {
134 pub node: NodeIdx,
136 pub stored: u64,
138 pub derived: u64,
140 pub activations: u64,
142 pub workspace: u64,
144 pub state: u64,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct RunArena {
151 pub label: String,
153 pub rows: u64,
155 pub arena: u64,
157 pub materialized: u64,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
163pub struct Totals {
164 pub weights_stored: u64,
165 pub weights_outside: u64,
168 pub weights_derived: u64,
169 pub weights_leaked: u64,
171 pub activations_planned: u64,
173 pub activations: u64,
175 pub workspace: u64,
177 pub states: u64,
179 pub caches: u64,
181 pub host: u64,
183 pub driver: u64,
184 pub device: u64,
186}
187
188#[derive(Debug, Clone, PartialEq)]
190pub struct MemoryReport {
191 pub nodes: Vec<NodeMemory>,
192 pub weights: Vec<NodeWeights>,
193 pub states: StatePlan,
194 pub caches: Vec<CacheTerm>,
195 pub runs: Vec<RunArena>,
196 pub workspaces: Vec<WorkspaceTerm>,
197 pub totals: Totals,
198 pub budget_bytes: u64,
199}
200
201impl MemoryReport {
202 pub fn with_outside(mut self, bytes: u64) -> Self {
205 self.totals.weights_outside += bytes;
206 self.totals.weights_stored += bytes;
207 self.totals.device += bytes;
208 self
209 }
210
211 pub fn headroom(&self) -> i128 {
213 i128::from(self.budget_bytes) - i128::from(self.totals.device)
214 }
215}
216
217pub fn kv_blocks_for(sequences: u64, tokens: u64, block_size: u64) -> Option<u64> {
222 let per_seq = tokens.checked_div(block_size)?.checked_add(1)?;
223 sequences.checked_mul(per_seq)?.checked_add(1)
224}
225
226fn state_plan(
229 c: &Circuit,
230 inputs: &StateInputs,
231 draft_kv: Option<crate::state::StateDtype>,
232) -> Result<StatePlan, MemoryError> {
233 let Some(dt) = draft_kv else {
234 return Ok(StatePlan::new(&c.states, inputs)?);
235 };
236 let (draft, main): (Vec<_>, Vec<_>) = c
237 .states
238 .iter()
239 .cloned()
240 .partition(|s| s.section == crate::ir::Section::Draft);
241 let mut di = inputs.clone();
242 di.formats.insert("kv_cache_dtype".into(), dt);
243 let mut plan = StatePlan::new(&main, inputs)?;
244 plan.terms.extend(StatePlan::new(&draft, &di)?.terms);
245 Ok(plan)
246}
247
248pub fn evaluate(inp: &MemoryInputs<'_>) -> Result<MemoryReport, MemoryError> {
250 let c = inp.served;
251 let weights = weights::node_weights(c, inp.declared, inp.copies, inp.settings)?;
252 let states = state_plan(c, inp.states, inp.draft_kv_dtype)?;
253 let caches = caches::cache_terms(&c.states, &inp.states.formats, inp.caches)?;
254 let mut runs = Vec::new();
255 let mut peak: Option<(u64, planner::BufferPlan)> = None;
256 for r in inp.runs {
257 let b = planner::plan_buffers(c, r.plan, r.rows)?;
258 runs.push(RunArena {
259 label: r.label.to_string(),
260 rows: r.rows,
261 arena: b.arena_bytes,
262 materialized: b.materialized_bytes,
263 });
264 if peak.as_ref().is_none_or(|p| b.arena_bytes > p.0) {
265 peak = Some((b.arena_bytes, b));
266 }
267 }
268 let workspaces = match inp.families {
269 Some((fams, sm)) => {
270 let mut all = Vec::new();
271 for r in inp.runs {
272 all.push(workspace::workspace_terms(c, r.plan, fams, r.rows, sm)?);
273 }
274 workspace::widest(all)
275 }
276 None => Vec::new(),
277 };
278 let mut nodes: Vec<NodeMemory> = (0..c.nodes.len())
279 .map(|node| NodeMemory {
280 node,
281 stored: 0,
282 derived: 0,
283 activations: 0,
284 workspace: 0,
285 state: 0,
286 })
287 .collect();
288 for w in &weights {
289 nodes[w.node].stored = w.stored;
290 nodes[w.node].derived = w.derived.iter().map(|d| d.bytes).sum();
291 }
292 if let Some((_, b)) = &peak {
293 for s in &b.slots {
294 if let Some(p) = c.edges[s.edge].producer {
295 nodes[p].activations += s.bytes;
296 }
297 }
298 }
299 for w in &workspaces {
300 for &n in &w.nodes {
301 nodes[n].workspace = nodes[n].workspace.max(w.bytes);
302 }
303 }
304 for (i, n) in c.nodes.iter().enumerate() {
307 nodes[i].state = n
308 .state
309 .iter()
310 .filter(|(_, a)| matches!(a, StateAccess::Update | StateAccess::Write))
311 .map(|&(si, _)| states.bytes_where(|t| t.state == c.states[si].id))
312 .sum();
313 }
314 let mut t = Totals::default();
315 for w in &weights {
316 t.weights_stored += w.stored;
317 for d in &w.derived {
318 t.weights_derived += d.bytes;
319 if d.leaked {
320 t.weights_leaked += d.bytes;
321 }
322 }
323 }
324 t.activations_planned = peak.as_ref().map_or(0, |p| p.0);
325 t.activations = inp.legacy_arena.unwrap_or(t.activations_planned);
326 t.workspace = workspaces
327 .iter()
328 .filter(|w| inp.legacy_arena.is_none() || !w.arena)
329 .map(|w| w.bytes)
330 .sum();
331 t.states = states.bytes();
332 t.caches = caches.iter().filter(|x| !x.host).map(|x| x.bytes).sum();
333 t.host = caches.iter().filter(|x| x.host).map(|x| x.bytes).sum();
334 t.driver = inp.driver.bytes(inp.budget_bytes, inp.chunk_slack);
335 t.device = t.weights_stored
336 + t.weights_derived
337 + t.activations
338 + t.workspace
339 + t.states
340 + t.caches
341 + t.driver;
342 Ok(MemoryReport {
343 nodes,
344 weights,
345 states,
346 caches,
347 runs,
348 workspaces,
349 totals: t,
350 budget_bytes: inp.budget_bytes,
351 })
352}
353
354#[cfg(test)]
355#[path = "memory_tests.rs"]
356mod memory_tests;