1use std::collections::BTreeMap;
12use std::fmt::Write as _;
13
14use serde_json::{Value, json};
15
16use super::MemoryReport;
17use crate::ir::Circuit;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct Inverse {
22 pub max_concurrency: Option<(u64, u64, Option<u64>)>,
24 pub max_isl: Option<(u64, u64, Option<u64>)>,
26 pub kv_pool: Option<(u64, u64, u64)>,
29}
30
31fn mib(b: u64) -> String {
32 format!("{:.1}", b as f64 / (1u64 << 20) as f64)
33}
34
35fn fold_key(c: &Circuit, i: usize, per_node: bool) -> String {
38 let n = &c.nodes[i];
39 if per_node || n.layer.is_none() {
40 return n.id.clone();
41 }
42 format!("{}.{}", n.block, n.local)
43}
44
45fn state_key(d: &crate::state::StateDecl) -> String {
48 match d.section {
49 crate::ir::Section::Main => format!("{}.{}", d.block, d.local),
50 crate::ir::Section::Draft => format!("draft.{}.{}", d.block, d.local),
51 }
52}
53
54pub fn render_text(
56 c: &Circuit,
57 r: &MemoryReport,
58 head: &[(String, String)],
59 inverse: &Inverse,
60 per_node: bool,
61) -> String {
62 let mut s = String::from("# Circuit memory\n\n");
63 for (k, v) in head {
64 let _ = writeln!(s, "{k}: {v}");
65 }
66 let mut rows: BTreeMap<String, (usize, [u64; 5], String)> = BTreeMap::new();
67 let mut order = Vec::new();
68 for m in &r.nodes {
69 let v = [m.stored, m.derived, m.activations, m.workspace, m.state];
70 if v.iter().all(|&x| x == 0) {
71 continue;
72 }
73 let key = fold_key(c, m.node, per_node);
74 let e = rows.entry(key.clone()).or_insert_with(|| {
75 order.push(key.clone());
76 (0, [0; 5], c.nodes[m.node].op.name())
77 });
78 e.0 += 1;
79 for (j, b) in v.into_iter().enumerate() {
80 e.1[j] = if j == 3 { e.1[j].max(b) } else { e.1[j] + b };
82 }
83 }
84 let _ = writeln!(
85 s,
86 "\n## Per node (MiB; x = nodes folded)\n\n| node | op | x | stored | derived | activations | workspace | state |\n|---|---|---:|---:|---:|---:|---:|---:|"
87 );
88 for k in &order {
89 let (n, v, op) = &rows[k];
90 let _ = writeln!(
91 s,
92 "| {k} | {op} | {n} | {} | {} | {} | {} | {} |",
93 mib(v[0]),
94 mib(v[1]),
95 mib(v[2]),
96 mib(v[3]),
97 mib(v[4])
98 );
99 }
100 let _ = writeln!(
101 s,
102 "\n## States and caches (MiB; x = declarations folded)\n\n| state | kind | holding | lifetime | dtype | x | units each | MiB |\n|---|---|---|---|---|---:|---:|---:|"
103 );
104 let decl = |id: &str| c.states.iter().find(|d| d.id == id);
105 let mut srows: BTreeMap<(String, String, String, String, String), (usize, u64, u64)> =
106 BTreeMap::new();
107 for t in &r.states.terms {
108 let Some(d) = decl(&t.state) else { continue };
109 let key = (
110 state_key(d),
111 d.kind.name().to_string(),
112 format!("{:?}", t.holding).to_lowercase(),
113 t.lifetime.name().to_string(),
114 t.dtype.name().to_string(),
115 );
116 let e = srows.entry(key).or_insert((0, t.units, 0));
117 e.0 += 1;
118 e.2 += t.bytes;
119 }
120 for t in &r.caches {
121 let Some(d) = decl(&t.state) else { continue };
122 let key = (
123 state_key(d),
124 d.kind.name().to_string(),
125 if t.host { "host" } else { "cache" }.to_string(),
126 d.lifetime.name().to_string(),
127 t.dtype.name().to_string(),
128 );
129 let e = srows.entry(key).or_insert((0, t.units, 0));
130 e.0 += 1;
131 e.2 += t.bytes;
132 }
133 for ((id, kind, holding, life, dt), (n, units, bytes)) in &srows {
134 if *bytes == 0 {
135 continue;
136 }
137 let _ = writeln!(
138 s,
139 "| {id} | {kind} | {holding} | {life} | {dt} | {n} | {units} | {} |",
140 mib(*bytes)
141 );
142 }
143 if !r.runs.is_empty() {
144 let _ = writeln!(
145 s,
146 "\n## Activation runs (planner)\n\n| run | rows | arena MiB | materialized MiB |\n|---|---:|---:|---:|"
147 );
148 for a in &r.runs {
149 let _ = writeln!(
150 s,
151 "| {} | {} | {} | {} |",
152 a.label,
153 a.rows,
154 mib(a.arena),
155 mib(a.materialized)
156 );
157 }
158 }
159 if !r.workspaces.is_empty() {
160 let _ = writeln!(
161 s,
162 "\n## Workspaces (widest run)\n\n| family | workspace | in legacy arena | nodes | MiB |\n|---|---|---|---:|---:|"
163 );
164 for w in &r.workspaces {
165 let _ = writeln!(
166 s,
167 "| {} | {} | {} | {} | {} |",
168 w.family,
169 w.name,
170 if w.arena { "yes" } else { "no" },
171 w.nodes.len(),
172 mib(w.bytes)
173 );
174 }
175 }
176 let t = &r.totals;
177 let _ = writeln!(s, "\n## Totals (MiB)\n\n| term | MiB |\n|---|---:|");
178 for (k, v) in [
179 ("weights stored (declared formats)", t.weights_stored),
180 (" of which outside the circuit", t.weights_outside),
181 ("weights derived (load-time copies)", t.weights_derived),
182 (" of which leaked", t.weights_leaked),
183 ("activations planned (circuit peak)", t.activations_planned),
184 ("activations charged", t.activations),
185 ("workspace charged", t.workspace),
186 ("states (recurrent pool, KV)", t.states),
187 ("caches (device)", t.caches),
188 ("driver", t.driver),
189 ("device total", t.device),
190 ("budget", r.budget_bytes),
191 ("host caches", t.host),
192 ] {
193 let _ = writeln!(s, "| {k} | {} |", mib(v));
194 }
195 let h = r.headroom();
196 let _ = writeln!(
197 s,
198 "\nHeadroom: {:.1} MiB ({}).",
199 h as f64 / (1u64 << 20) as f64,
200 if h >= 0 { "fits" } else { "over budget" }
201 );
202 let ans = |v: &Option<u64>| v.map_or_else(|| "none fits".to_string(), |x| x.to_string());
203 if let Some((isl, osl, c)) = &inverse.max_concurrency {
204 let _ = writeln!(s, "Max concurrency at ISL={isl} OSL={osl}: {}.", ans(c));
205 }
206 if let Some((conc, osl, isl)) = &inverse.max_isl {
207 let _ = writeln!(s, "Max ISL at C={conc} OSL={osl}: {}.", ans(isl));
208 }
209 if let Some((blocks, tokens, need)) = &inverse.kv_pool {
210 let _ = writeln!(
211 s,
212 "KV pool at this budget: {blocks} blocks ({tokens} tokens); this workload needs {need}."
213 );
214 }
215 s
216}
217
218pub fn render_json(
220 c: &Circuit,
221 r: &MemoryReport,
222 head: &[(String, String)],
223 inverse: &Inverse,
224) -> Value {
225 let nodes: Vec<Value> = r
226 .nodes
227 .iter()
228 .filter(|m| m.stored + m.derived + m.activations + m.workspace + m.state > 0)
229 .map(|m| {
230 let n = &c.nodes[m.node];
231 let derived: Vec<Value> = r
232 .weights
233 .iter()
234 .find(|w| w.node == m.node)
235 .map(|w| {
236 w.derived
237 .iter()
238 .map(|d| json!({"rule": d.rule, "bytes": d.bytes, "leaked": d.leaked}))
239 .collect()
240 })
241 .unwrap_or_default();
242 json!({
243 "node": n.id, "op": n.op.name(), "layer": n.layer,
244 "stored": m.stored, "derived": derived, "activations": m.activations,
245 "workspace": m.workspace, "state": m.state,
246 })
247 })
248 .collect();
249 let states: Vec<Value> = r
250 .states
251 .terms
252 .iter()
253 .map(|t| {
254 json!({"state": t.state, "holding": format!("{:?}", t.holding).to_lowercase(),
255 "lifetime": t.lifetime.name(), "dtype": t.dtype.name(),
256 "units": t.units, "bytes": t.bytes})
257 })
258 .chain(r.caches.iter().map(|t| {
259 json!({"state": t.state, "kind": t.kind.name(), "host": t.host,
260 "dtype": t.dtype.name(), "units": t.units, "bytes": t.bytes})
261 }))
262 .collect();
263 let t = &r.totals;
264 json!({
265 "schema": 1,
266 "header": head.iter().map(|(k, v)| (k.clone(), Value::String(v.clone())))
267 .collect::<serde_json::Map<_, _>>(),
268 "nodes": nodes,
269 "states": states,
270 "runs": r.runs.iter().map(|a| json!({"label": a.label, "rows": a.rows,
271 "arena": a.arena, "materialized": a.materialized})).collect::<Vec<_>>(),
272 "workspaces": r.workspaces.iter().map(|w| json!({"family": w.family, "name": w.name,
273 "bytes": w.bytes, "arena": w.arena, "nodes": w.nodes.len()})).collect::<Vec<_>>(),
274 "totals": {
275 "weights_stored": t.weights_stored, "weights_outside": t.weights_outside,
276 "weights_derived": t.weights_derived,
277 "weights_leaked": t.weights_leaked, "activations_planned": t.activations_planned,
278 "activations": t.activations, "workspace": t.workspace, "states": t.states,
279 "caches": t.caches, "host": t.host, "driver": t.driver, "device": t.device,
280 "budget": r.budget_bytes, "headroom": r.headroom().to_string(),
281 },
282 "inverse": {
283 "max_concurrency": inverse.max_concurrency.map(|(i, o, c)|
284 json!({"isl": i, "osl": o, "concurrency": c})),
285 "max_isl": inverse.max_isl.map(|(c, o, i)|
286 json!({"concurrency": c, "osl": o, "isl": i})),
287 "kv_pool": inverse.kv_pool.map(|(b, t, n)|
288 json!({"blocks": b, "tokens": t, "needed_blocks": n})),
289 },
290 })
291}