1use crate::analysis::liveness_analyse;
23use crate::error::{GraphError, GraphResult};
24use crate::graph::ComputeGraph;
25use crate::node::BufferId;
26
27#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct SlotAssignment {
34 pub buf: BufferId,
36 pub slot: usize,
38 pub offset: usize,
40 pub slot_size: usize,
42 pub buf_size: usize,
44}
45
46#[derive(Debug, Clone)]
52pub struct MemoryPlan {
53 pub assignments: Vec<SlotAssignment>,
55 pub slot_sizes: Vec<usize>,
57 pub total_bytes: usize,
59 pub num_slots: usize,
61 pub external_count: usize,
63}
64
65impl MemoryPlan {
66 pub fn assignment(&self, buf: BufferId) -> Option<&SlotAssignment> {
68 self.assignments.iter().find(|a| a.buf == buf)
69 }
70
71 pub fn compression_ratio(&self) -> f64 {
76 let naive: usize = self.assignments.iter().map(|a| a.buf_size).sum();
77 if naive == 0 {
78 return 1.0;
79 }
80 self.total_bytes as f64 / naive as f64
81 }
82}
83
84fn align_up(size: usize, align: usize) -> usize {
89 if align == 0 {
90 return size;
91 }
92 (size + align - 1) & !(align - 1)
93}
94
95pub fn analyse(graph: &ComputeGraph) -> GraphResult<MemoryPlan> {
106 if graph.is_empty() {
107 return Err(GraphError::EmptyGraph);
108 }
109
110 let liveness = liveness_analyse(graph)?;
111
112 const ALIGN: usize = 256;
114
115 let mut managed: Vec<_> = liveness
117 .sorted_by_start()
118 .into_iter()
119 .filter(|iv| !iv.external && iv.size_bytes > 0)
120 .collect();
121
122 managed.sort_by_key(|iv| (iv.start(), iv.buf.0));
124
125 let external_count = liveness.all_intervals().filter(|iv| iv.external).count();
126
127 let mut active: Vec<(usize, usize, usize)> = Vec::new(); let mut slot_sizes: Vec<usize> = Vec::new();
132 let mut assignments: Vec<SlotAssignment> = Vec::new();
133 let mut next_slot = 0usize;
134
135 for iv in &managed {
136 let start = iv.start();
137 let end = iv.end();
138 let needed = align_up(iv.size_bytes, ALIGN);
139
140 let candidate_pos = active
143 .iter()
144 .enumerate()
145 .filter(|(_, (end_step, _, sz))| *end_step < start && *sz >= needed)
146 .min_by_key(|(_, (_, _, sz))| *sz)
147 .map(|(pos, _)| pos);
148
149 let slot_id = if let Some(pos) = candidate_pos {
150 let (_, sid, sz) = active.remove(pos);
151 active.push((end, sid, sz));
153 sid
154 } else {
155 let sid = next_slot;
157 next_slot += 1;
158 slot_sizes.push(needed);
159 active.push((end, sid, needed));
160 sid
161 };
162
163 assignments.push(SlotAssignment {
164 buf: iv.buf,
165 slot: slot_id,
166 offset: 0, slot_size: slot_sizes[slot_id],
168 buf_size: iv.size_bytes,
169 });
170 }
171
172 let mut slot_offsets = vec![0usize; slot_sizes.len()];
174 let mut cursor = 0usize;
175 for (i, &sz) in slot_sizes.iter().enumerate() {
176 slot_offsets[i] = cursor;
177 cursor += sz;
178 }
179 let total_bytes = cursor;
180
181 for asgn in &mut assignments {
183 asgn.offset = slot_offsets[asgn.slot];
184 }
185
186 Ok(MemoryPlan {
187 assignments,
188 slot_sizes,
189 total_bytes,
190 num_slots: next_slot,
191 external_count,
192 })
193}
194
195#[cfg(test)]
200mod tests {
201 use super::*;
202 use crate::builder::GraphBuilder;
203
204 #[test]
205 fn memory_empty_graph() {
206 let g = ComputeGraph::new();
207 assert!(matches!(analyse(&g), Err(GraphError::EmptyGraph)));
208 }
209
210 #[test]
211 fn memory_no_buffers() {
212 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
213 b.add_barrier("n");
214 let g = b.build().expect("test graph builds successfully");
215 let plan = analyse(&g).expect("memory analysis succeeds on valid graph");
216 assert_eq!(plan.total_bytes, 0);
217 assert_eq!(plan.num_slots, 0);
218 }
219
220 #[test]
221 fn memory_single_buffer() {
222 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
223 let buf = b.alloc_buffer("x", 1024);
224 let w = b.add_barrier("w");
225 let r = b.add_barrier("r");
226 b.set_outputs(w, [buf]);
227 b.set_inputs(r, [buf]);
228 b.dep(w, r);
229 let g = b.build().expect("test graph builds successfully");
230 let plan = analyse(&g).expect("memory analysis succeeds on valid graph");
231 assert_eq!(plan.num_slots, 1);
232 assert!(plan.total_bytes >= 1024);
233 let asgn = plan.assignment(buf).expect("buffer has memory assignment");
234 assert_eq!(asgn.buf, buf);
235 assert_eq!(asgn.slot, 0);
236 }
237
238 #[test]
239 fn memory_non_overlapping_buffers_share_slot() {
240 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
242 let buf0 = b.alloc_buffer("b0", 512);
243 let buf1 = b.alloc_buffer("b1", 512);
244 let n0 = b.add_barrier("n0"); let n1 = b.add_barrier("n1"); let n2 = b.add_barrier("n2"); let n3 = b.add_barrier("n3"); b.set_outputs(n0, [buf0]);
249 b.set_inputs(n1, [buf0]);
250 b.set_outputs(n2, [buf1]);
251 b.set_inputs(n3, [buf1]);
252 b.chain(&[n0, n1, n2, n3]);
253 let g = b.build().expect("test graph builds successfully");
254 let plan = analyse(&g).expect("memory analysis succeeds on valid graph");
255 let a0 = plan.assignment(buf0);
257 let a1 = plan.assignment(buf1);
258 if let (Some(a0), Some(a1)) = (a0, a1) {
259 assert_eq!(
260 a0.slot, a1.slot,
261 "non-overlapping buffers should share a slot"
262 );
263 }
264 assert_eq!(plan.num_slots, 1);
265 }
266
267 #[test]
268 fn memory_overlapping_buffers_use_different_slots() {
269 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
271 let buf0 = b.alloc_buffer("b0", 1024);
272 let buf1 = b.alloc_buffer("b1", 2048);
273 let w0 = b.add_barrier("w0");
274 let w1 = b.add_barrier("w1");
275 let reader = b.add_barrier("reader");
276 b.set_outputs(w0, [buf0]);
277 b.set_outputs(w1, [buf1]);
278 b.set_inputs(reader, [buf0, buf1]);
279 b.fan_in(&[w0, w1], reader);
280 let g = b.build().expect("test graph builds successfully");
281 let plan = analyse(&g).expect("memory analysis succeeds on valid graph");
282 let a0 = plan.assignment(buf0).expect("buf0 has memory assignment");
283 let a1 = plan.assignment(buf1).expect("buf1 has memory assignment");
284 assert_ne!(a0.slot, a1.slot);
285 assert_eq!(plan.num_slots, 2);
286 }
287
288 #[test]
289 fn memory_external_buffer_not_counted() {
290 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
291 let ext = b.alloc_external_buffer("weights", 65536);
292 let r = b.add_barrier("r");
293 b.set_inputs(r, [ext]);
294 let g = b.build().expect("test graph builds successfully");
295 let plan = analyse(&g).expect("memory analysis succeeds on valid graph");
296 assert_eq!(plan.external_count, 1);
297 assert_eq!(plan.num_slots, 0);
298 assert_eq!(plan.total_bytes, 0);
299 }
300
301 #[test]
302 fn memory_alignment_respected() {
303 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
304 let buf = b.alloc_buffer("small", 17); let w = b.add_barrier("w");
306 b.set_outputs(w, [buf]);
307 let g = b.build().expect("test graph builds successfully");
308 let plan = analyse(&g).expect("memory analysis succeeds on valid graph");
309 assert!(plan.total_bytes >= 256);
310 let asgn = plan.assignment(buf).expect("buffer has memory assignment");
311 assert!(asgn.slot_size >= 256);
312 }
313
314 #[test]
315 fn memory_compression_ratio_improves_with_sharing() {
316 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
318 let bufs: Vec<_> = (0..3)
319 .map(|i| b.alloc_buffer(&format!("b{i}"), 1024))
320 .collect();
321 let nodes: Vec<_> = (0..6).map(|i| b.add_barrier(&format!("n{i}"))).collect();
322 for i in 0..3 {
324 b.set_outputs(nodes[2 * i], [bufs[i]]);
325 b.set_inputs(nodes[2 * i + 1], [bufs[i]]);
326 }
327 b.chain(&nodes);
328 let g = b.build().expect("test graph builds successfully");
329 let plan = analyse(&g).expect("memory analysis succeeds on valid graph");
330 assert!(
333 plan.compression_ratio() <= 1.0,
334 "ratio = {}",
335 plan.compression_ratio()
336 );
337 }
338
339 #[test]
340 fn memory_slot_offsets_non_overlapping() {
341 let mut b = GraphBuilder::new().with_auto_infer_edges(false);
343 let buf0 = b.alloc_buffer("b0", 1024);
344 let buf1 = b.alloc_buffer("b1", 2048);
345 let buf2 = b.alloc_buffer("b2", 512);
346 let w0 = b.add_barrier("w0");
347 let w1 = b.add_barrier("w1");
348 let w2 = b.add_barrier("w2");
349 let sink = b.add_barrier("sink");
350 b.set_outputs(w0, [buf0]);
351 b.set_outputs(w1, [buf1]);
352 b.set_outputs(w2, [buf2]);
353 b.set_inputs(sink, [buf0, buf1, buf2]);
354 b.fan_in(&[w0, w1, w2], sink);
355 let g = b.build().expect("test graph builds successfully");
356 let plan = analyse(&g).expect("memory analysis succeeds on valid graph");
357 let a0 = plan.assignment(buf0).expect("buf0 has memory assignment");
359 let a1 = plan.assignment(buf1).expect("buf1 has memory assignment");
360 let a2 = plan.assignment(buf2).expect("buf2 has memory assignment");
361 let ranges = [
363 (a0.offset, a0.offset + a0.slot_size),
364 (a1.offset, a1.offset + a1.slot_size),
365 (a2.offset, a2.offset + a2.slot_size),
366 ];
367 for i in 0..ranges.len() {
368 for j in (i + 1)..ranges.len() {
369 let (s0, e0) = ranges[i];
370 let (s1, e1) = ranges[j];
371 assert!(e0 <= s1 || e1 <= s0, "slots {i} and {j} overlap in memory");
372 }
373 }
374 }
375}