Skip to main content

logicaffeine_compile/spatial/lower/fabric/
budget.rs

1//! The programmable-part resources a fabric program spends, counted from exactly what the printer declares, so a
2//! program too big for its part is refused before a line of RTL is printed.
3//!
4//! Every count is a LOWER bound — the fewest of a resource the declared memories and arrays can take — so a
5//! refusal is sound: the real design needs at least this much, and this much already does not fit. A block-RAM
6//! bank cannot hold more bits than its RAMBs do, a distributed-RAM memory cannot pack its bits into fewer than
7//! one LUT per 64 PER READ PORT (each port is its own LUT copy of the data — a pool read through a gather's lanes
8//! replicates once a lane), a registers-style memory is one flip-flop a bit, and a matmul array is at least one DSP
9//! a MAC. Each is the bits of the memories the printer declares, divided down — never more than a routed image used.
10
11use std::collections::BTreeMap;
12
13use super::*;
14use crate::spatial::lower::{CapacityRefusal, FabricResource, Over};
15use crate::spatial::part::MemoryKind;
16
17/// Bits one RAMB18 holds, data and parity (a 36 Kb block RAM is two of these).
18const RAMB18_BITS: u64 = 18_432;
19/// Bits one distributed-RAM LUT holds (a 64 × 1 memory).
20const LUTRAM_BITS: u64 = 64;
21
22/// A memory the fabric prints: the arguments one `memory_decl` / `print_pool` call receives, and the read ports it
23/// is read through (each a LUT copy when it is distributed RAM — a pool's `nread`, a tensor memory's one).
24#[derive(Clone, Debug, PartialEq, Eq)]
25pub struct DeclaredMemory {
26    pub name: String,
27    pub words: usize,
28    pub width: u32,
29    pub banks: usize,
30    pub lat: u64,
31    pub read_ports: usize,
32}
33
34impl DeclaredMemory {
35    fn storage(&self) -> Storage {
36        Storage::of(self.words, self.banks, self.lat)
37    }
38
39    /// The bits the declaration holds: every bank, every row, every lane — `banks × ⌈words/banks⌉ × width`, the
40    /// shape `memory_decl` prints.
41    pub fn bits(&self) -> u64 {
42        let rows = self.words.max(1).div_ceil(self.banks) as u64;
43        self.banks as u64 * rows * self.width as u64
44    }
45}
46
47/// Every memory the fabric declares for `p` on `target`, in printer order: each site's tensor memories (as
48/// `print_site` emits them, one per tensor in the site's banks) then its pools (as `print_pool` does).
49pub fn fabric_memories(p: &SpatialProgram, target: &SpatialTarget) -> Result<Vec<DeclaredMemory>, SpatialLowerError> {
50    let ctx = context(p, target)?;
51    let plans = plans(&ctx)?;
52    let banks = ctx.banks;
53    let mut out = Vec::new();
54    for site in p.sites() {
55        for t in &p.tensors {
56            out.push(DeclaredMemory {
57                name: format!("mem_t{}", t.id.idx()),
58                words: ctx.tensor_words(t),
59                width: tensor_width(t, &ctx.ranges),
60                banks,
61                lat: ctx.lat,
62                read_ports: 1,
63            });
64        }
65        for (i, pool) in site_pools(&ctx, &plans, site).into_iter().enumerate() {
66            out.push(DeclaredMemory {
67                name: format!("pool_{}_{}_{i}", site.x, site.y),
68                words: pool.words,
69                width: pool.width,
70                banks,
71                lat: ctx.lat,
72                read_ports: pool.nread,
73            });
74        }
75    }
76    Ok(out)
77}
78
79/// What a fabric spends of each part resource: a lower bound on the RAMB18s, LUT-RAM LUTs, flip-flops and DSP
80/// slices the printed design takes.
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct FabricUsage {
83    pub ramb18: u64,
84    pub lutram_luts: u64,
85    pub flops: u64,
86    pub dsp: u64,
87    pub memories: Vec<DeclaredMemory>,
88}
89
90/// The resources `p` spends on `target`'s fabric.
91pub fn fabric_usage(p: &SpatialProgram, target: &SpatialTarget) -> Result<FabricUsage, SpatialLowerError> {
92    let memories = fabric_memories(p, target)?;
93    let (mut block_bits, mut lutram_luts, mut ff_bits) = (0u64, 0u64, 0u64);
94    for m in &memories {
95        match m.storage() {
96            Storage::Block => block_bits += m.bits(),
97            // Each read port is its own LUT copy, and a copy of B bits is at least ⌈B/64⌉ LUTs.
98            Storage::Distributed => lutram_luts += m.bits().div_ceil(LUTRAM_BITS) * m.read_ports as u64,
99            Storage::Registers | Storage::Flops => ff_bits += m.bits(),
100        }
101    }
102    let ctx = context(p, target)?;
103    let plans = plans(&ctx)?;
104    // A matmul array is at least one DSP per multiply-accumulate; wider operands than one multiplier only take more.
105    let dsp = plans
106        .iter()
107        .filter_map(|pl| pe_array(&pl.micro))
108        .map(|(k, n, _)| (k * n) as u64)
109        .sum();
110    Ok(FabricUsage {
111        ramb18: block_bits.div_ceil(RAMB18_BITS),
112        lutram_luts,
113        flops: ff_bits,
114        dsp,
115        memories,
116    })
117}
118
119/// The memories of a storage class, largest first, named for a refusal.
120fn largest_of(mems: &[DeclaredMemory], want: Storage) -> Vec<(String, u64)> {
121    let mut v: Vec<(String, u64)> = mems.iter().filter(|m| m.storage() == want).map(|m| (m.name.clone(), m.bits())).collect();
122    v.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
123    v.truncate(3);
124    v
125}
126
127/// Refuse `p` on `target` when it needs more of any of the part's resources than it has — before any RTL is
128/// printed. A target with no part ([`SpatialTarget::part`] `None`) is never refused here.
129pub fn check_capacity(p: &SpatialProgram, target: &SpatialTarget) -> Result<(), SpatialLowerError> {
130    let Some(part) = target.part else {
131        return Ok(());
132    };
133    let usage = fabric_usage(p, target)?;
134    let have = |kind: MemoryKind| part.memory(kind).map_or(0, |m| m.count.value);
135    let block_as_ramb18 = have(MemoryKind::Block36) * 2;
136    let budgets: &[(FabricResource, u64, u64, Option<Storage>)] = &[
137        (FabricResource::BlockRam18, usage.ramb18, block_as_ramb18, Some(Storage::Block)),
138        (FabricResource::LutRam, usage.lutram_luts, have(MemoryKind::LutRam), Some(Storage::Distributed)),
139        (FabricResource::Flops, usage.flops, part.flops.value, Some(Storage::Registers)),
140        (FabricResource::Dsp, usage.dsp, part.dsp.value, None),
141    ];
142    let mut over = Vec::new();
143    for &(resource, need, have, storage) in budgets {
144        if need > have {
145            let largest = storage.map(|s| largest_of(&usage.memories, s)).unwrap_or_default();
146            over.push(Over { resource, need, have, largest });
147        }
148    }
149    if over.is_empty() {
150        Ok(())
151    } else {
152        Err(SpatialLowerError::OverCapacity(CapacityRefusal { part: part.part, over }))
153    }
154}
155
156/// A map from each resource to its usage, for a test to read.
157pub fn usage_by_resource(u: &FabricUsage) -> BTreeMap<&'static str, u64> {
158    [
159        (FabricResource::BlockRam18.label(), u.ramb18),
160        (FabricResource::LutRam.label(), u.lutram_luts),
161        (FabricResource::Flops.label(), u.flops),
162        (FabricResource::Dsp.label(), u.dsp),
163    ]
164    .into_iter()
165    .collect()
166}