1use std::collections::{BTreeMap, BTreeSet};
21
22use super::ir::*;
23use super::target::{precision_bytes, Interconnect, SpatialTarget};
24
25pub fn tensor_bytes(p: &SpatialProgram, t: &TensorDecl) -> u64 {
27 (t.tiles.rows_t * t.tiles.cols_t * p.tile.elems() * precision_bytes(t.dtype)) as u64
28}
29
30pub fn buffer_footprint(p: &SpatialProgram) -> BTreeMap<u16, u64> {
32 let mut high: BTreeMap<Site, u64> = BTreeMap::new();
33 if let Some(mm) = &p.memory {
34 for at in mm.buffer_at.iter().filter(|a| a.space != MemSpace::Acc) {
35 let top = high.entry(at.site).or_default();
36 *top = (*top).max(at.offset_bytes as u64 + at.bytes as u64);
37 }
38 }
39 let mut out = BTreeMap::new();
40 for (site, top) in high {
41 *out.entry(site.chip).or_default() += top;
42 }
43 out
44}
45
46fn tensor_chips(p: &SpatialProgram) -> Vec<BTreeSet<u16>> {
48 let mut out = vec![BTreeSet::new(); p.tensors.len()];
49 let pl = p.placement.as_ref();
50 for s in &p.stages {
51 let (StageKind::Load { tensor, .. } | StageKind::Store { tensor, .. }) = &s.kind else {
52 continue;
53 };
54 let site = pl
55 .and_then(|pl| pl.site_of.get(s.id.idx()).copied())
56 .or(s.site);
57 out[tensor.idx()].insert(site.map(|x| x.chip).unwrap_or(0));
58 }
59 out
60}
61
62fn live_ranges(p: &SpatialProgram) -> BTreeMap<TensorId, (usize, usize)> {
65 let phases = p.section_phases();
66 let phase_of = |s: StageId| {
67 p.section_of_stage(s)
68 .and_then(|k| phases.get(k).copied())
69 .unwrap_or(0)
70 };
71 let mut out: BTreeMap<TensorId, (usize, usize)> = BTreeMap::new();
72 for s in &p.stages {
73 let (tensor, stores) = match &s.kind {
74 StageKind::Load { tensor, .. } => (*tensor, false),
75 StageKind::Store { tensor, .. } => (*tensor, true),
76 _ => continue,
77 };
78 if p.tensor(tensor).role != TensorRole::Activation {
79 continue;
80 }
81 let ph = phase_of(s.id);
82 let r = out.entry(tensor).or_insert((ph, ph));
83 if stores {
84 r.0 = r.0.min(ph);
85 }
86 r.1 = r.1.max(ph);
87 }
88 out
89}
90
91pub fn holds_tensors_on_chip(target: &SpatialTarget) -> bool {
93 target.dram.sram_kib > 0 && target.provides_interconnect(Interconnect::Crossbar)
94}
95
96fn held_on(chips: &[BTreeSet<u16>], t: TensorId) -> Vec<u16> {
98 if chips[t.idx()].is_empty() {
99 vec![0]
100 } else {
101 chips[t.idx()].iter().copied().collect()
102 }
103}
104
105pub fn place_activations(p: &mut SpatialProgram, target: &SpatialTarget) {
115 let ranges = live_ranges(p);
116 let chips = tensor_chips(p);
117 let mut spilled: BTreeSet<TensorId> = BTreeSet::new();
118 if holds_tensors_on_chip(target) {
119 let sram = target.dram.sram_kib as u64 * 1024;
120 let buffers = buffer_footprint(p);
121 let mut by_chip: BTreeMap<u16, Vec<(TensorId, (usize, usize), u64)>> = BTreeMap::new();
122 for (t, r) in &ranges {
123 let b = tensor_bytes(p, p.tensor(*t));
124 for c in held_on(&chips, *t) {
125 by_chip.entry(c).or_default().push((*t, *r, b));
126 }
127 }
128 for (chip, mut live) in by_chip {
129 let room = sram.saturating_sub(buffers.get(&chip).copied().unwrap_or(0));
130 live.sort_by_key(|(t, r, _)| (r.0, t.idx()));
131 let mut active: BTreeSet<(usize, u64, std::cmp::Reverse<usize>)> = BTreeSet::new();
134 let mut ending: BTreeSet<(usize, usize, u64)> = BTreeSet::new();
135 let mut sum = 0u64;
136 let mut next = 0usize;
137 let mut phases: Vec<usize> = live.iter().map(|(_, r, _)| r.0).collect();
138 phases.dedup();
139 for ph in phases {
140 while let Some(&(last, id, b)) = ending.first() {
141 if last >= ph {
142 break;
143 }
144 ending.pop_first();
145 if active.remove(&(last, b, std::cmp::Reverse(id))) {
146 sum -= b;
147 }
148 }
149 while next < live.len() && live[next].1 .0 == ph {
150 let (t, r, b) = live[next];
151 next += 1;
152 if spilled.contains(&t) {
153 continue;
154 }
155 active.insert((r.1, b, std::cmp::Reverse(t.idx())));
156 ending.insert((r.1, t.idx(), b));
157 sum += b;
158 }
159 while sum > room {
160 let (last, b, std::cmp::Reverse(id)) = active
161 .pop_last()
162 .expect("an overflowing phase holds an intermediate");
163 ending.remove(&(last, id, b));
164 sum -= b;
165 spilled.insert(TensorId::new(id));
166 }
167 }
168 }
169 }
170 let on_chip = holds_tensors_on_chip(target);
171 for t in &mut p.tensors {
172 if t.role == TensorRole::Activation {
173 t.space = if on_chip && !spilled.contains(&t.id) {
174 TensorSpace::Sram
175 } else {
176 TensorSpace::Dram
177 };
178 }
179 }
180}
181
182pub fn sram_footprint(p: &SpatialProgram) -> BTreeMap<u16, u64> {
185 let ranges = live_ranges(p);
186 let chips = tensor_chips(p);
187 let mut out = buffer_footprint(p);
188 let mut deltas: BTreeMap<u16, BTreeMap<usize, i128>> = BTreeMap::new();
189 for (t, r) in &ranges {
190 let d = p.tensor(*t);
191 if d.space != TensorSpace::Sram {
192 continue;
193 }
194 let b = tensor_bytes(p, d) as i128;
195 for c in held_on(&chips, *t) {
196 let e = deltas.entry(c).or_default();
197 *e.entry(r.0).or_default() += b;
198 *e.entry(r.1 + 1).or_default() -= b;
199 }
200 }
201 for (c, d) in deltas {
202 let (mut sum, mut peak) = (0i128, 0i128);
203 for (_, delta) in d {
204 sum += delta;
205 peak = peak.max(sum);
206 }
207 *out.entry(c).or_default() += peak as u64;
208 }
209 out
210}
211
212#[derive(Clone, Debug, PartialEq, Eq)]
215pub struct StateClass {
216 pub program: String,
217 pub tensor: String,
218 pub chip: u16,
219 pub instances: usize,
220 pub bytes: u64,
221 pub moved: u64,
222}
223
224impl StateClass {
225 pub fn total(&self) -> u64 {
227 self.bytes * self.instances as u64
228 }
229}
230
231#[derive(Clone, Debug, PartialEq, Eq, Default)]
234pub struct ResidencyPlan {
235 pub sram_bytes: u64,
236 pub hbm_bytes: u64,
238 pub reserve: BTreeMap<u16, u64>,
239 pub placed: Vec<(StateClass, TensorSpace)>,
241 pub off_chip: Vec<(StateClass, TensorSpace)>,
243}
244
245impl ResidencyPlan {
246 pub fn on_chip(&self, chip: u16) -> u64 {
248 self.placed
249 .iter()
250 .filter(|(c, s)| c.chip == chip && *s == TensorSpace::Sram)
251 .map(|(c, _)| c.total())
252 .sum()
253 }
254
255 pub fn tier_of(&self, program: &str, tensor: &str) -> Option<TensorSpace> {
257 self.placed
258 .iter()
259 .find(|(c, _)| c.program == program && c.tensor == tensor)
260 .map(|(_, s)| *s)
261 }
262}
263
264pub fn plan_states(
270 classes: Vec<StateClass>,
271 sram_bytes: u64,
272 reserve: &BTreeMap<u16, u64>,
273) -> ResidencyPlan {
274 let mut order: Vec<usize> = (0..classes.len()).collect();
275 order.sort_by(|&a, &b| {
276 let (x, y) = (&classes[a], &classes[b]);
277 (y.moved as u128 * x.bytes as u128)
278 .cmp(&(x.moved as u128 * y.bytes as u128))
279 .then(x.total().cmp(&y.total()))
280 .then((&x.program, &x.tensor).cmp(&(&y.program, &y.tensor)))
281 });
282 let mut room: BTreeMap<u16, u64> = BTreeMap::new();
283 let mut tier = vec![TensorSpace::Dram; classes.len()];
284 for i in order {
285 let c = &classes[i];
286 let left = room.entry(c.chip).or_insert_with(|| {
287 sram_bytes.saturating_sub(reserve.get(&c.chip).copied().unwrap_or(0))
288 });
289 if c.total() <= *left {
290 *left -= c.total();
291 tier[i] = TensorSpace::Sram;
292 }
293 }
294 ResidencyPlan {
295 sram_bytes,
296 hbm_bytes: 0,
297 reserve: reserve.clone(),
298 placed: classes.into_iter().zip(tier).collect(),
299 off_chip: Vec::new(),
300 }
301}
302
303pub fn plan_off_chip(classes: &[StateClass], hbm_bytes: u64) -> Vec<TensorSpace> {
306 let mut order: Vec<usize> = (0..classes.len()).collect();
307 order.sort_by(|&a, &b| {
308 let (x, y) = (&classes[a], &classes[b]);
309 (y.moved as u128 * x.bytes as u128)
310 .cmp(&(x.moved as u128 * y.bytes as u128))
311 .then(x.total().cmp(&y.total()))
312 .then((&x.program, &x.tensor).cmp(&(&y.program, &y.tensor)))
313 });
314 let mut room: BTreeMap<u16, u64> = BTreeMap::new();
315 let mut tier = vec![TensorSpace::Dram; classes.len()];
316 for i in order {
317 let c = &classes[i];
318 let left = room.entry(c.chip).or_insert(hbm_bytes);
319 if hbm_bytes > 0 && c.total() <= *left {
320 *left -= c.total();
321 tier[i] = TensorSpace::Hbm;
322 }
323 }
324 tier
325}
326
327pub fn moved_bytes(p: &SpatialProgram) -> Vec<u64> {
329 let mut out = vec![0u64; p.tensors.len()];
330 let tile = p.tile.elems() as u64;
331 if let Some(sch) = &p.schedule {
332 for prog in &sch.programs {
333 walk_tagged(&prog.ops, 1, &mut |t, m| {
334 if let ChoreoOp::ReadTiles {
335 tensor,
336 block_h,
337 block_w,
338 ..
339 }
340 | ChoreoOp::WriteTiles {
341 tensor,
342 block_h,
343 block_w,
344 ..
345 } = &t.op
346 {
347 out[tensor.idx()] += m
348 * (*block_h * *block_w) as u64
349 * tile
350 * precision_bytes(p.tensor(*tensor).dtype) as u64;
351 }
352 });
353 }
354 }
355 out
356}
357
358pub type DeployedProgram<'a> = (
362 &'a str,
363 usize,
364 &'a mut SpatialProgram,
365 Option<&'a BTreeSet<String>>,
366);
367
368pub fn plan_deployment(programs: &mut [DeployedProgram], target: &SpatialTarget) -> ResidencyPlan {
373 let sram = if holds_tensors_on_chip(target) {
374 target.dram.sram_kib as u64 * 1024
375 } else {
376 0
377 };
378 let mut reserve: BTreeMap<u16, u64> = BTreeMap::new();
379 let mut classes = Vec::new();
380 for (name, instances, p, carried) in programs.iter_mut() {
381 let chips = tensor_chips(p);
382 let chip_of = |t: &TensorDecl| chips[t.id.idx()].iter().next().copied().unwrap_or(0);
383 let own = |t: &TensorDecl| {
384 t.role == TensorRole::State && carried.is_some_and(|c| !c.contains(&t.name))
385 };
386 let mut footprint = sram_footprint(p);
387 let mut tables: BTreeMap<u16, u64> = BTreeMap::new();
388 for t in p.tensors.iter().filter(|t| own(t)) {
389 *tables.entry(chip_of(t)).or_default() += tensor_bytes(p, t);
390 }
391 let fits = tables
392 .iter()
393 .all(|(c, b)| footprint.get(c).copied().unwrap_or(0) + b <= sram);
394 let table_ids: Vec<TensorId> = p.tensors.iter().filter(|t| own(t)).map(|t| t.id).collect();
395 for id in table_ids {
396 p.tensors[id.idx()].space = if fits {
397 TensorSpace::Sram
398 } else {
399 TensorSpace::Dram
400 };
401 }
402 if fits {
403 for (c, b) in tables {
404 *footprint.entry(c).or_default() += b;
405 }
406 }
407 for (c, b) in footprint {
408 let r = reserve.entry(c).or_default();
409 *r = (*r).max(b);
410 }
411 let moved = moved_bytes(p);
412 for t in p
413 .tensors
414 .iter()
415 .filter(|t| t.role == TensorRole::State && !own(t))
416 {
417 classes.push(StateClass {
418 program: name.to_string(),
419 tensor: t.name.clone(),
420 chip: chip_of(t),
421 instances: *instances,
422 bytes: tensor_bytes(p, t),
423 moved: moved[t.id.idx()],
424 });
425 }
426 }
427 let mut plan = plan_states(classes, sram, &reserve);
428 let hbm = target.dram.hbm_gib as u64 * (1 << 30);
431 let placed_at: std::collections::HashMap<(&str, &str), usize> = plan
432 .placed
433 .iter()
434 .enumerate()
435 .map(|(i, (c, _))| ((c.program.as_str(), c.tensor.as_str()), i))
436 .collect();
437 let mut rest: Vec<(StateClass, Option<usize>)> = Vec::new();
438 for (name, instances, p, _) in programs.iter() {
439 let chips = tensor_chips(p);
440 let moved = moved_bytes(p);
441 for t in &p.tensors {
442 let carried = placed_at.get(&(*name, t.name.as_str())).copied();
443 let on_chip = match carried {
444 Some(i) => plan.placed[i].1 == TensorSpace::Sram,
445 None => t.space == TensorSpace::Sram,
446 };
447 if !on_chip {
448 let chip = chips[t.id.idx()].iter().next().copied().unwrap_or(0);
449 rest.push((
450 StateClass {
451 program: name.to_string(),
452 tensor: t.name.clone(),
453 chip,
454 instances: *instances,
455 bytes: tensor_bytes(p, t),
456 moved: moved[t.id.idx()],
457 },
458 carried,
459 ));
460 }
461 }
462 }
463 drop(placed_at);
464 let classes: Vec<StateClass> = rest.iter().map(|(c, _)| c.clone()).collect();
465 let tiers = plan_off_chip(&classes, hbm);
466 plan.hbm_bytes = hbm;
467 for ((c, carried), tier) in rest.into_iter().zip(tiers) {
468 match carried {
469 Some(i) => plan.placed[i].1 = tier,
470 None => plan.off_chip.push((c, tier)),
471 }
472 }
473 let tier_at: std::collections::HashMap<(&str, &str), TensorSpace> = plan
474 .placed
475 .iter()
476 .chain(plan.off_chip.iter())
477 .map(|(c, s)| ((c.program.as_str(), c.tensor.as_str()), *s))
478 .collect();
479 for (name, _, p, _) in programs.iter_mut() {
480 for t in p.tensors.iter_mut() {
481 if let Some(tier) = tier_at.get(&(*name, t.name.as_str())) {
482 t.space = *tier;
483 }
484 }
485 }
486 drop(tier_at);
487 plan
488}