1use std::fmt;
17
18use logicaffeine_proof::cycle_ratio::{Bound, GraphError};
19
20use super::quartus::{Ledger, RegionUsage, Row};
21use super::timing_graph::{Area, AreaFit, Calibration, TimingError, TimingGraph};
22
23#[derive(Clone, Copy, Debug, PartialEq)]
25pub enum LoopBound {
26 Acyclic,
27 Combinational,
28 Ratio(f64),
29}
30
31#[derive(Clone, Debug, PartialEq)]
33pub struct Design {
34 pub area: Area,
35 pub depth: i64,
36 pub loop_bound: LoopBound,
37}
38
39#[derive(Debug)]
41pub enum DesignError {
42 Timing(TimingError),
43 Graph(GraphError),
44}
45
46impl fmt::Display for DesignError {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 DesignError::Timing(e) => write!(f, "{e}"),
50 DesignError::Graph(e) => write!(f, "{e:?}"),
51 }
52 }
53}
54
55impl Design {
56 pub fn of(graph: &TimingGraph) -> Result<Design, DesignError> {
59 let loop_bound = match graph.loop_bound().map_err(DesignError::Graph)? {
60 Bound::Acyclic => LoopBound::Acyclic,
61 Bound::CombinationalLoop(_) => LoopBound::Combinational,
62 Bound::Ratio { delay, registers, .. } => LoopBound::Ratio(delay as f64 / registers as f64),
63 };
64 let depth = match graph.depth() {
65 Ok(d) => d.depth,
66 Err(TimingError::CombinationalLoop(_)) if loop_bound == LoopBound::Combinational => 0,
67 Err(e) => return Err(DesignError::Timing(e)),
68 };
69 Ok(Design { area: graph.area_under(""), depth, loop_bound })
70 }
71
72 pub fn with_loop(self, loop_bound: LoopBound) -> Design {
74 Design { loop_bound, ..self }
75 }
76}
77
78#[derive(Clone, Copy, Debug, PartialEq)]
80pub struct Target {
81 pub period_ns: f64,
82 pub ns_per_unit: f64,
83}
84
85#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct Capacity {
88 pub row: String,
89 pub alms: u64,
90 pub registers: u64,
91 pub m20k: u64,
92 pub dsp: u64,
93}
94
95const PR_SLOT: &str = "afu_top|port_gasket|pr_slot|afu_main";
97
98fn persona_region(r: &Row) -> Option<&RegionUsage> {
102 if r.build.flow.revision.as_deref() != Some("iofs_pr_afu") {
103 return None;
104 }
105 r.build.regions.iter().find(|g| g.name == PR_SLOT)
106}
107
108pub fn capacity(ledger: &Ledger) -> Option<Capacity> {
110 ledger.rows.iter().rev().find_map(|r| {
111 let region = persona_region(r)?;
112 Some(Capacity {
113 row: r.name.clone(),
114 alms: region.alms,
115 registers: region.registers.map_or(0, |x| x.total),
116 m20k: region.m20k.map_or(0, |x| x.total),
117 dsp: region.dsp.map_or(0, |x| x.total),
118 })
119 })
120}
121
122#[derive(Clone, Copy, Debug, PartialEq)]
125pub struct Packing {
126 pub per_alut: f64,
127 pub per_register: Option<f64>,
128}
129
130impl Packing {
131 pub fn alms(&self, aluts: f64, registers: f64) -> f64 {
133 let by_aluts = aluts * self.per_alut;
134 self.per_register.map_or(by_aluts, |r| by_aluts.max(registers * r))
135 }
136}
137
138pub fn packing(ledger: &Ledger) -> Option<Packing> {
142 let rows: Vec<(f64, f64, f64)> = ledger
143 .rows
144 .iter()
145 .filter(|r| r.synthesis_sources().is_some())
146 .filter_map(|r| {
147 let alms = r.build.synthesis.first()?.estimate.alms?;
148 let top = r.build.top.iter().find(|n| n.path == "|")?;
149 (alms > 0 && top.aluts > 0).then(|| (alms as f64, top.aluts as f64, top.registers as f64))
150 })
151 .collect();
152 let geomean = |xs: Vec<f64>| (!xs.is_empty()).then(|| (xs.iter().map(|x| x.ln()).sum::<f64>() / xs.len() as f64).exp());
153 let fit = |by_registers: &[bool]| -> Option<Packing> {
154 let lut = rows.iter().zip(by_registers).filter(|(_, r)| !**r).map(|(&(a, l, _), _)| a / l).collect();
155 let reg = rows.iter().zip(by_registers).filter(|(_, r)| **r).map(|(&(a, _, g), _)| a / g).collect();
156 Some(Packing { per_alut: geomean(lut)?, per_register: geomean(reg) })
157 };
158 let mut sides: Vec<bool> = rows.iter().map(|&(_, l, g)| g > l).collect();
159 let mut p = fit(&sides).or_else(|| fit(&vec![false; rows.len()]))?;
160 for _ in 0..=rows.len() {
161 let next: Vec<bool> = rows.iter().map(|&(_, l, g)| p.per_register.is_some_and(|r| g * r > l * p.per_alut)).collect();
162 if next == sides {
163 break;
164 }
165 p = fit(&next).unwrap_or(p);
166 sides = next;
167 }
168 Some(p)
169}
170
171pub fn alms_per_alut(ledger: &Ledger) -> Option<f64> {
173 packing(ledger).map(|p| p.per_alut)
174}
175
176pub fn fullest(ledger: &Ledger) -> Option<(String, f64)> {
178 ledger
179 .rows
180 .iter()
181 .filter(|r| r.verdict.passed)
182 .filter_map(|r| persona_region(r).map(|g| (r.name.clone(), g.alms_needed)))
183 .max_by(|a, b| a.1.total_cmp(&b.1))
184}
185
186pub fn fitter_seconds(ledger: &Ledger, alms: f64) -> Option<f64> {
189 let points: Vec<(f64, f64)> = ledger
190 .rows
191 .iter()
192 .filter_map(|r| {
193 let needed = persona_region(r)?.alms_needed;
194 let seconds = r.build.flow.phase("Fitter")?.seconds;
195 (needed > 0.0 && seconds > 0).then(|| (needed.ln(), (seconds as f64).ln()))
196 })
197 .collect();
198 let n = points.len() as f64;
199 let (mx, my) = (points.iter().map(|p| p.0).sum::<f64>() / n, points.iter().map(|p| p.1).sum::<f64>() / n);
200 let sxx: f64 = points.iter().map(|p| (p.0 - mx).powi(2)).sum();
201 if points.len() < 2 || sxx < 1e-12 {
202 return None;
203 }
204 let b = points.iter().map(|p| (p.0 - mx) * (p.1 - my)).sum::<f64>() / sxx;
205 Some((my + b * (alms.ln() - mx)).exp())
206}
207
208#[derive(Clone, Copy, Debug)]
210pub struct Timed<'a> {
211 pub name: &'a str,
212 pub design: &'a Design,
213 pub cycles: u32,
214}
215
216#[derive(Clone, Debug, Default, PartialEq)]
218pub struct Predicted {
219 pub aluts: f64,
220 pub alms: f64,
221 pub dsp: u64,
222 pub registers: u64,
223 pub timing: Vec<PartTiming>,
226 pub fitter_seconds: Option<f64>,
227}
228
229#[derive(Clone, Debug, Default, PartialEq)]
231pub struct PartTiming {
232 pub name: String,
233 pub cycles: u32,
234 pub depth_ns: f64,
235 pub loop_ns: Option<f64>,
236}
237
238#[derive(Clone, Debug, PartialEq)]
240pub enum Refusal {
241 Over { what: &'static str, predicted: f64, capacity: u64 },
243 Device { what: &'static str, needs: u64, holds: u64 },
245 Depth { part: String, ns: f64, budget: f64 },
247 Loop { part: String, ns: Option<f64>, budget: f64 },
249 Unmeasured { what: &'static str },
251}
252
253impl Refusal {
254 pub fn what(&self) -> &'static str {
256 match self {
257 Refusal::Over { what, .. } | Refusal::Device { what, .. } | Refusal::Unmeasured { what } => what,
258 Refusal::Depth { .. } => "the deepest path",
259 Refusal::Loop { .. } => "a loop",
260 }
261 }
262}
263
264fn of(part: &str) -> String {
266 if part.is_empty() {
267 String::new()
268 } else {
269 format!("{part}: ")
270 }
271}
272
273impl fmt::Display for Refusal {
274 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275 match self {
276 Refusal::Over { what, predicted, capacity } => write!(f, "{predicted:.0} {what}, where the region holds {capacity}"),
277 Refusal::Device { what, needs, holds } => write!(f, "{needs} {what}, where the device holds {holds}"),
278 Refusal::Depth { part, ns, budget } => {
279 write!(f, "{}the deepest path takes {ns:.2} ns, past the {budget} ns its paths have: pipeline it", of(part))
280 }
281 Refusal::Loop { part, ns: Some(ns), budget } => {
282 write!(f, "{}a loop takes {ns:.2} ns a register, past the {budget} ns its paths have: no retiming reaches it", of(part))
283 }
284 Refusal::Loop { part, ns: None, .. } => write!(f, "{}a loop through no register", of(part)),
285 Refusal::Unmeasured { what } => write!(f, "no {what} to predict from"),
286 }
287 }
288}
289
290#[derive(Clone, Debug, PartialEq)]
293pub struct Preflight {
294 pub predicted: Predicted,
295 pub capacity: Option<Capacity>,
296 pub refusals: Vec<Refusal>,
297 pub tight: Vec<String>,
298}
299
300pub fn preflight(design: &Design, ledger: &Ledger, calibration: &Calibration, target: &Target) -> Preflight {
303 preflight_domains(&design.area, &[Timed { name: "", design, cycles: 1 }], ledger, calibration, target)
304}
305
306pub fn preflight_domains(area: &Area, parts: &[Timed], ledger: &Ledger, calibration: &Calibration, target: &Target) -> Preflight {
308 let mut refusals = Vec::new();
309 let mut tight = Vec::new();
310 let mut predicted = Predicted { dsp: area.half_dsps.div_ceil(2), registers: area.flops, ..Predicted::default() };
311 let fit: Option<&AreaFit> = calibration.area.as_ref();
312 match (fit, packing(ledger)) {
313 (Some(fit), Some(packing)) => {
314 predicted.aluts = fit.aluts(area);
315 predicted.alms = packing.alms(predicted.aluts, area.flops as f64);
316 }
317 _ => refusals.push(Refusal::Unmeasured { what: "the area fit" }),
318 }
319 let capacity = capacity(ledger);
320 match &capacity {
321 Some(c) => {
322 if predicted.alms > c.alms as f64 {
323 refusals.push(Refusal::Over { what: "ALMs", predicted: predicted.alms, capacity: c.alms });
324 } else if let Some((row, alms)) = fullest(ledger).filter(|(_, a)| predicted.alms > *a) {
325 tight.push(format!("{:.0} ALMs, past {alms:.0}, the fullest persona that fit ({row})", predicted.alms));
326 }
327 if predicted.dsp > c.dsp {
328 refusals.push(Refusal::Over { what: "DSP blocks", predicted: predicted.dsp as f64, capacity: c.dsp });
329 }
330 if predicted.registers > c.registers {
331 refusals.push(Refusal::Over { what: "registers", predicted: predicted.registers as f64, capacity: c.registers });
332 }
333 predicted.fitter_seconds = fitter_seconds(ledger, predicted.alms.max(1.0));
334 }
335 None => refusals.push(Refusal::Unmeasured { what: "the PR region" }),
336 }
337 for part in parts {
338 let budget = target.period_ns * part.cycles as f64;
339 let timing = PartTiming {
340 name: part.name.to_string(),
341 cycles: part.cycles,
342 depth_ns: part.design.depth as f64 * target.ns_per_unit,
343 loop_ns: match part.design.loop_bound {
344 LoopBound::Ratio(r) => Some(r * target.ns_per_unit),
345 _ => None,
346 },
347 };
348 if timing.depth_ns > budget {
349 refusals.push(Refusal::Depth { part: timing.name.clone(), ns: timing.depth_ns, budget });
350 }
351 match part.design.loop_bound {
352 LoopBound::Combinational => refusals.push(Refusal::Loop { part: timing.name.clone(), ns: None, budget }),
353 LoopBound::Ratio(_) if timing.loop_ns.is_some_and(|ns| ns > budget) => {
354 refusals.push(Refusal::Loop { part: timing.name.clone(), ns: timing.loop_ns, budget })
355 }
356 _ => {}
357 }
358 predicted.timing.push(timing);
359 }
360 Preflight { predicted, capacity, refusals, tight }
361}
362
363pub fn device_library(verilog: &str) -> String {
366 const FP_MAC: &str = "fourteennm_fp_mac";
367 let (mut defined, mut used) = (false, false);
368 for line in verilog.lines() {
369 let words: Vec<&str> = line.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '$')).filter(|w| !w.is_empty()).collect();
370 for (i, w) in words.iter().enumerate() {
371 if *w == FP_MAC {
372 if i > 0 && words[i - 1] == "module" {
373 defined = true;
374 } else {
375 used = true;
376 }
377 }
378 }
379 }
380 if used && !defined {
381 super::fp_oracle::fp_mac_blackbox()
382 } else {
383 String::new()
384 }
385}
386
387#[derive(Clone, Copy, Debug, PartialEq, Eq)]
389pub struct Holds {
390 pub alms: u64,
391 pub dsp: u64,
392 pub m20k: u64,
393}
394
395pub const DEVICE: Holds = Holds { alms: 933_120, dsp: 5_760, m20k: 11_721 };
397
398pub fn synthesized_holds(syn: &str) -> Option<Holds> {
403 let partitions = super::quartus::parse_synthesis(syn);
404 let sum = |of: fn(&super::quartus::Estimate) -> Option<u64>| -> Option<u64> {
405 let each: Vec<u64> = partitions.iter().filter_map(|p| of(&p.estimate)).collect();
406 (!each.is_empty()).then(|| each.iter().sum())
407 };
408 Some(Holds {
409 alms: sum(|e| e.alms)?,
410 dsp: sum(|e| e.dsp_blocks)?,
411 m20k: super::floorplan::synthesized_rams(syn).iter().map(super::floorplan::Ram::m20k).sum(),
412 })
413}
414
415pub fn over(needs: Holds, device: Holds) -> Vec<Refusal> {
417 [("ALMs", needs.alms, device.alms), ("DSP blocks", needs.dsp, device.dsp), ("M20K blocks", needs.m20k, device.m20k)]
418 .into_iter()
419 .filter(|&(_, n, d)| n > d)
420 .map(|(what, needs, holds)| Refusal::Device { what, needs, holds })
421 .collect()
422}
423
424#[cfg(test)]
425#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_sva/preflight/tests.rs"]
426mod tests;