Skip to main content

rhdl_ed25519_scalable_core/frontier/
costs.rs

1//! Block costs: the device, each block's LUT/DSP/BRAM/URAM, the measured
2//! ([`vivado_costs`]) and projected ([`slimmed_costs`]) tables, and the
3//! adjustments routed builds taught us (multiplier choice, constant ROM).
4
5use super::*;
6
7/// Device limits of the Alveo U280 (XCU280) that [`frontier`] packs builds
8/// into. All fields except `lut` are the physical device totals.
9#[derive(Clone, Copy, Debug)]
10pub struct Device {
11    /// CLB LUT budget (the project cap, not the device total).
12    pub lut: u32,
13    /// DSP48E2 blocks.
14    pub dsp: u32,
15    /// BRAM36 blocks.
16    pub bram36: u32,
17    /// URAM blocks.
18    pub uram: u32,
19    /// Clock in MHz; converts cycles per operation into operations per second.
20    pub mhz: f64,
21}
22
23impl Device {
24    /// The device's limits as a share.
25    pub(super) fn share(&self) -> Share {
26        Share { lut: self.lut, dsp: self.dsp, bram36: self.bram36, uram: self.uram }
27    }
28    /// The U280 at a LUT budget and the 205 MHz design clock. The DSP, BRAM36
29    /// and URAM counts are the XCU280 totals (same values as [`U280_DSP`],
30    /// [`U280_BRAM36`], [`U280_URAM`]).
31    pub fn u280(lut: u32) -> Self {
32        Device {
33            lut,
34            dsp: 9_024,
35            bram36: 2_016,
36            uram: 960,
37            mhz: 205.0,
38        }
39    }
40}
41
42/// Resources of one block, or a sum of blocks.
43#[derive(Clone, Copy, Debug, Default, PartialEq)]
44pub struct Cost {
45    /// CLB LUTs.
46    pub lut: u32,
47    /// DSP48E2 blocks.
48    pub dsp: u32,
49    /// BRAM36 blocks.
50    pub bram36: u32,
51    /// URAM blocks.
52    pub uram: u32,
53}
54
55impl Cost {
56    /// `self` plus `n` instances of `other`, field by field. Used to build a
57    /// module total from per-instance block costs.
58    pub(super) fn add(self, other: Cost, n: u32) -> Cost {
59        Cost {
60            lut: self.lut + other.lut * n,
61            dsp: self.dsp + other.dsp * n,
62            bram36: self.bram36 + other.bram36 * n,
63            uram: self.uram + other.uram * n,
64        }
65    }
66    /// Whether every resource is within the device limits.
67    pub(super) fn fits(&self, d: &Device) -> bool {
68        self.lut <= d.lut && self.dsp <= d.dsp && self.bram36 <= d.bram36 && self.uram <= d.uram
69    }
70}
71
72/// Per-instance block costs used by the frontier and the system planner.
73/// Two tables exist: [`vivado_costs`] (as built) and [`slimmed_costs`]
74/// (projected memory layout).
75#[derive(Clone, Copy, Debug)]
76pub struct Costs {
77    /// Fixed-base signing machine.
78    pub fb_machine: Cost,
79    /// SHA-512 ring.
80    pub sha_ring: Cost,
81    /// Scalar ring.
82    pub scalar_ring: Cost,
83    /// Batch-compress machine.
84    pub cp_machine: Cost,
85    /// Signing controller.
86    pub sign_controller: Cost,
87    /// Signing controller of a fused build (no compress machine: no credit
88    /// pool or compress dispatch).
89    pub sign_controller_fused: Cost,
90    /// Verify field machine, any key (derived: signing machine plus a
91    /// per-context variable-base table and its write path).
92    pub verify_machine_strict: Cost,
93    /// Verify field machine for loaded keys (derived: signing machine plus a
94    /// writable key table in URAM and its write path).
95    pub verify_machine_known_key: Cost,
96    /// Verify controller (derived from the signing controller).
97    pub verify_controller: Cost,
98    /// Scalar ring of the verify module (`k = H mod l` only).
99    pub verify_scalar_ring: Cost,
100    /// Key-loader machine of a known-key verify module (decodes `A` and
101    /// builds its table; one per module).
102    pub key_loader: Cost,
103    /// The fused small signer, `Ed25519SignCoreSmall` (1/1/1/0 with its own
104    /// SHA ring), as one block: measured in Vivado (routed). It is the
105    /// cheapest way to sign at DSig's certificate rate.
106    pub small_signer: Cost,
107    /// The block-RAM layout of the field machines, if this table's own
108    /// layout is a different one. A build may put some machines in it when
109    /// UltraRAM runs out (see [`sign_cost_within`]).
110    pub bram_layout: Option<BramLayout>,
111    /// Change per field machine when it uses the DSP-lean multiplier, as
112    /// `(LUT delta, DSP delta)` (17 x 26 tiles chained by the DSP's 17-bit
113    /// cascade shift: 150 DSPs instead of 225, rows summed in fabric). `None`
114    /// when the option is unavailable for this cost table; the planners then
115    /// skip [`Multiplier::Lean150`].
116    pub lean_mul: Option<(i32, i32)>,
117    /// Where the numbers came from (printed in reports).
118    pub source: &'static str,
119}
120
121/// Field-machine costs in the block-RAM (as-built) layout: the fallback for
122/// machines that do not fit the UltraRAM layout's URAM.
123#[derive(Clone, Copy, Debug)]
124pub struct BramLayout {
125    /// Fixed-base signing machine.
126    pub fb_machine: Cost,
127    /// Batch-compress machine.
128    pub cp_machine: Cost,
129    /// Verify machine, any key.
130    pub verify_machine_strict: Cost,
131    /// Verify machine, loaded keys.
132    pub verify_machine_known_key: Cost,
133    /// Key loader of a known-key verify module.
134    pub key_loader: Cost,
135}
136
137/// Which multiplier a module's field machines use.
138#[derive(Clone, Copy, Debug, PartialEq, Eq)]
139pub enum Multiplier {
140    /// 225 DSPs (15 x 15 tiles of 17 x 22), built and measured.
141    Dsp225,
142    /// 150 DSPs (15 x 10 tiles of 17 x 26) plus fabric row adders (projected).
143    Lean150,
144}
145
146impl Multiplier {
147    /// Human-readable label for reports.
148    pub fn name(self) -> &'static str {
149        match self {
150            Multiplier::Dsp225 => "225-DSP multiplier",
151            Multiplier::Lean150 => "150-DSP Karatsuba multiplier",
152        }
153    }
154}
155
156/// The cost table `c` rebuilt for multiplier `m`. Returns `None` for
157/// [`Multiplier::Lean150`] when `c.lean_mul` is `None`, so callers can skip
158/// that option.
159/// The cost table for multiplier `m`, or `c` itself when `m` is not
160/// offered (for callers outside this module).
161pub fn with_mul_pub(c: &Costs, m: Multiplier) -> Costs {
162    with_mul(c, m).unwrap_or(*c)
163}
164
165/// LUTs the constant ROM (`machine::ConstRom`, fix14) adds to each field
166/// machine: a literal and a mux per read port instead of LUT RAM. Routed
167/// on the 1,1,1,1 verifier (three field machines: verify, compress, key
168/// loader): 84,435 LUT against 82,184 on fix12 (007zzr, 007zzi), so 750
169/// each. One core, so provisional; the signers' machines hold other
170/// constants and are not yet routed on fix14.
171pub const CONST_ROM_LUT: i32 = 750;
172
173/// `c` with every field machine's LUTs changed by `dlut` (the multiplier
174/// and the constant ROM live in every field machine; rings and
175/// controllers keep their costs).
176pub(super) fn per_field_machine(c: &Costs, dlut: i32, ddsp: i32) -> Costs {
177    let f = |x: Cost| Cost { lut: (x.lut as i32 + dlut) as u32, dsp: (x.dsp as i32 + ddsp) as u32, ..x };
178    Costs {
179        fb_machine: f(c.fb_machine),
180        cp_machine: f(c.cp_machine),
181        verify_machine_strict: f(c.verify_machine_strict),
182        verify_machine_known_key: f(c.verify_machine_known_key),
183        key_loader: f(c.key_loader),
184        // The fallback layouts hold the same machines.
185        bram_layout: c.bram_layout.map(|a| BramLayout {
186            fb_machine: f(a.fb_machine),
187            cp_machine: f(a.cp_machine),
188            verify_machine_strict: f(a.verify_machine_strict),
189            verify_machine_known_key: f(a.verify_machine_known_key),
190            key_loader: f(a.key_loader),
191        }),
192        ..*c
193    }
194}
195
196/// The cost table for RTL before fix14 (constant banks in LUT RAM), to
197/// compare routed cores of that RTL with the table.
198pub fn before_const_rom(c: &Costs) -> Costs {
199    per_field_machine(c, -CONST_ROM_LUT, 0)
200}
201
202pub(super) fn with_mul(c: &Costs, m: Multiplier) -> Option<Costs> {
203    match m {
204        Multiplier::Dsp225 => Some(*c),
205        Multiplier::Lean150 => {
206            // Per-machine LUT and DSP deltas (e.g. +2,500 LUT, -75 DSP),
207            // applied to every block that holds one field multiplier.
208            let (dlut, ddsp) = c.lean_mul?;
209            Some(per_field_machine(c, dlut, ddsp))
210        }
211    }
212}
213
214/// Vivado 2023.2 out-of-context synthesis of 15/11/3/1, per instance.
215///
216/// The signing blocks (fixed-base machine, SHA ring, scalar ring, compress
217/// machine, controller) are measured. The verify blocks are derived from the
218/// signing blocks by adding estimated LUT and memory for what they add, and
219/// are not built. The lean multiplier is not offered here.
220pub fn vivado_costs() -> Costs {
221    // Measured fixed-base signing machine; the verify machines and the key
222    // loader below are expressed relative to it.
223    let fb = Cost { lut: 10_110, dsp: 225, bram36: 72, uram: 0 };
224    Costs {
225        fb_machine: fb,
226        sha_ring: Cost { lut: 4_567, dsp: 0, bram36: 1, uram: 0 },
227        scalar_ring: Cost { lut: 5_520, dsp: 23, bram36: 0, uram: 0 },
228        cp_machine: Cost { lut: 16_800, dsp: 225, bram36: 289, uram: 32 },
229        sign_controller: Cost { lut: 15_800, dsp: 0, bram36: 4, uram: 16 },
230        // Not measured in this table; the fused build is costed as the
231        // non-fused controller.
232        sign_controller_fused: Cost { lut: 15_800, dsp: 0, bram36: 4, uram: 16 },
233        // Derived: the signing machine plus about 2,500 LUT and 22 BRAM36 for
234        // the per-context variable-base table and its write path.
235        verify_machine_strict: Cost { lut: fb.lut + 2_500, dsp: 225, bram36: fb.bram36 + 22, uram: 0 },
236        // Derived: the signing machine plus about 1,500 LUT and a 22-URAM key
237        // table.
238        verify_machine_known_key: Cost { lut: fb.lut + 1_500, dsp: 225, bram36: fb.bram36, uram: 22 },
239        // Derived: assumed equal to the signing controller.
240        verify_controller: Cost { lut: 15_800, dsp: 0, bram36: 4, uram: 16 },
241        // Derived: assumed equal to the signing scalar ring (no reduce-only
242        // variant in the as-built table).
243        verify_scalar_ring: Cost { lut: 5_520, dsp: 23, bram36: 0, uram: 0 },
244        // Derived: a fixed-base machine plus 10 BRAM36 for table building.
245        key_loader: Cost { lut: fb.lut, dsp: 225, bram36: fb.bram36 + 10, uram: 0 },
246        // Routed at 4.878 ns: 26,408 CLB LUT, 248 DSP, 88 BRAM36 (+2 BRAM18).
247        small_signer: Cost { lut: 26_408, dsp: 248, bram36: 89, uram: 0 },
248        // This table is the block-RAM layout itself.
249        bram_layout: None,
250        lean_mul: None,
251        source: "as built: Vivado 2023.2 OOC, blocks routed at 4.878 ns (compress stage and controller from 15/11/3/1 synthesis; verify blocks derived)",
252    }
253}
254
255/// Verify-controller LUTs per verify machine (dispatch, free-slot word,
256/// output offer), fitted from the 6- and 24-machine routed fix9 verifiers;
257/// costed with each machine so the controller entry stays fixed. (Fitted
258/// at 716 on the 6- and 14-machine verifiers before the timing fixes.)
259pub const VERIFY_PER_MACHINE_LUT: u32 = 228;
260
261/// LUT change of a fixed-base machine inside a whole signing core against
262/// the machine routed alone (fitted, see [`slimmed_costs`]): synthesis
263/// shares or trims logic across the machine's boundary.
264pub const FB_IN_CORE_LUT: i32 = -1_505;
265
266/// A routed fixed-base machine's cost inside a whole core.
267pub(super) fn in_core(c: Cost) -> Cost {
268    Cost { lut: (c.lut as i32 + FB_IN_CORE_LUT) as u32, ..c }
269}
270
271/// Specialized modules: the memory layout of
272/// [`MachineMemory::SPECIALIZED`](crate::machine::MachineMemory) (banks M
273/// and A in UltraRAM, constants in a LUT ROM, the compress machine's input
274/// bank in UltraRAM), as built and **routed**: Vivado 2023.2 out of context
275/// at 4.878 ns, every block meeting timing, on the fix9 RTL (the timing
276/// fixes through e0c9caa; `measure/results.tsv`, 2026-10-18):
277///
278/// | Block | LUT | DSP | BRAM36 | URAM | WNS (ns) |
279/// |---|---:|---:|---:|---:|---:|
280/// | fixed-base machine | 14,615 | 225 | 24.5 | 32 | +0.848 |
281/// | compress machine | 16,569 | 225 | 1.5 | 64 | +0.679 |
282/// | known-key verify machine | 13,863 | 225 | 24.5 | 56 | +0.609 |
283/// | aux machine (key loader) | 14,518 | 225 | 9.5 | 48 | +0.863 |
284/// | SHA ring | 4,315 | 0 | 0 | 0 | +1.995 |
285/// | small signer (1/1/1/0, fix4) | 29,055 | 248 | 40 | 32 | +0.306 |
286/// | 150-DSP Karatsuba variants (before the fixes) | +4,367..4,441 | −75 | = | = | +0.056..+0.470 |
287///
288/// BRAM18 halves are rounded up. The scalar ring and the controllers are
289/// fitted to routed whole cores (signers, verifiers; see the entries). Still
290/// **derived, not built**: the strict verify machine (fixed-base machine
291/// plus 2,500 LUT and 22 BRAM36); the reduce-only verify scalar ring is not
292/// built, so verifiers are costed with the built signing ring.
293pub fn slimmed_costs() -> Costs {
294    let v = vivado_costs();
295    // Blocks routed on the fix9 RTL (Vivado 2023.2 OOC, 4.878 ns,
296    // 2026-10-18; `measure/results.tsv` jobs 007zq-007zzb), every one with
297    // positive slack: fb machine 14,615 LUT (WNS +0.848), verify machine
298    // 13,863 (+0.609), compress machine 16,569 (+0.679), key loader 14,518
299    // (+0.863), SHA ring 4,315 (+1.995).
300    let fb = Cost { lut: 14_615, dsp: 225, bram36: 25, uram: 32 };
301    let fix13 = Costs {
302        // In a whole core a fixed-base machine costs FB_IN_CORE_LUT less
303        // than routed alone (fitted with the rings and controller below).
304        fb_machine: in_core(fb),
305        sha_ring: Cost { lut: 4_315, ..v.sha_ring },
306        cp_machine: Cost { lut: 16_569, dsp: 225, bram36: 2, uram: 64 },
307        verify_machine_strict: Cost { lut: fb.lut + 2_500, bram36: fb.bram36 + 22, ..fb },
308        // Routed block 13,863 plus 228: the verify controller's per-machine
309        // dispatch and offer logic (fitted, see `sign_controller`).
310        verify_machine_known_key: Cost { lut: 13_863 + VERIFY_PER_MACHINE_LUT, dsp: 225, bram36: 25, uram: 56 },
311        // Signing: rings, controller and the fixed-base machines' in-core
312        // change, fitted by least squares to four routed signers (2026-10-20;
313        // `measure/accuracy.tsv`): 8,0,2,1 u6l0 (fix9), 12,0,3,1 u5l2
314        // (fix10), 4,0,1,1 and 8,0,3,1 (fix13). The routed total less the
315        // routed blocks leaves 13,702, 15,708, 12,003 and 21,675 LUT; the fit
316        // is a controller of 10,099, 7,871 per scalar ring and -1,505 per
317        // fixed-base machine, and lands within 102 LUT of all four. A ring
318        // routed alone is 6,194 (fix10), so ~1,700 per ring is its dispatch
319        // and arbitration in the core. The fused 8,0,2,0 (fix13) leaves
320        // 6,817: a controller of 3,115 with no credit pool or compress
321        // dispatch (one core, in sample). The previous fit (2,408 per ring,
322        // 8,886 controller, two cores) under-predicted 8,0,3,1 by 3.6 % and
323        // over-predicted 8,0,2,0 by 5.5 %.
324        //
325        // Verifying: fitted to the fix9 RTL (2026-10-18). The routed total
326        // less the routed blocks (machines in their built layouts, compress
327        // machine, key loader, 1,500-LUT SHA client port) leaves 13,702 LUT
328        // for sign 8,0,2,1 u6l0 (two scalar rings), 16,110 for sign 12,0,3,1
329        // u5l2 (three), 34,435 for verify 6,0,1,1 (one) and 40,954 for verify
330        // 24,0,2,1 u2l19 (two). That gives a scalar ring of 2,408 LUT, a
331        // signing controller of 8,886 and a verify controller of 30,657 + 228
332        // per machine. Before the timing fixes (six cores, 2026-10-17):
333        // 1,590, 11,240 and 17,540 + 716; the verifier's limb-serial decode
334        // and registered credit, output and dispatch paths account for most
335        // of its growth. The 24-machine verifier missed timing (-5.763 ns)
336        // and may carry replication; the 9,0,2,1 signer (fix7, +0.114 ns) is
337        // the out-of-sample check.
338        scalar_ring: Cost { lut: 7_871, ..v.scalar_ring },
339        sign_controller: Cost { lut: 10_099, ..v.sign_controller },
340        sign_controller_fused: Cost { lut: 3_115, ..v.sign_controller },
341        verify_controller: Cost { lut: 30_657, ..v.verify_controller },
342        // The verifier uses the signing scalar ring (no reduce-only ring
343        // yet); its in-core cost is fitted with the verify controller, which
344        // keeps the verify fit's 2,408.
345        verify_scalar_ring: Cost { lut: 2_408, ..v.scalar_ring },
346        key_loader: Cost { lut: 14_518, dsp: 225, bram36: 10, uram: 48 },
347        // Routed 2026-10-17 (fix4: registered SHA ready), WNS +0.306.
348        small_signer: Cost { lut: 29_055, dsp: 248, bram36: 40, uram: 32 },
349        // The 150-DSP Karatsuba multiplier (`fe17_kara`), measured in all four
350        // machines before the timing fixes: +4,367 to +4,441 LUT and -75 DSP
351        // each.
352        lean_mul: Some((4_440, -75)),
353        // Replicated machines that do not fit the UltraRAM layout use the
354        // block-RAM fallback (`MachineMemory::BRAM_BANKS`): banks M and A
355        // move to block RAM (+32 BRAM36, -32 URAM); a verify machine's key
356        // tables stay in UltraRAM. Routed on the fix9 RTL (2026-10-18): fb
357        // 14,615 LUT / 56.5 BRAM36 / 0 URAM, WNS +0.544; verify 13,866 / 56.5
358        // / 24, WNS +0.790. The other entries are unused (only replicated
359        // machines fall back).
360        bram_layout: Some(BramLayout {
361            fb_machine: in_core(Cost { lut: 14_615, dsp: 225, bram36: 57, uram: 0 }),
362            cp_machine: v.cp_machine,
363            verify_machine_strict: v.verify_machine_strict,
364            verify_machine_known_key: Cost { lut: 13_866 + VERIFY_PER_MACHINE_LUT, dsp: 225, bram36: 57, uram: 24 },
365            key_loader: v.key_loader,
366        }),
367        source: "specialized memory layout: blocks routed on the fix9 RTL (Vivado 2023.2 OOC, 4.878 ns, 2026-10-18); signing rings, controllers and in-core machine change fitted to five routed signers (fix9-fix13), verify controller to the fix9 verifiers; fix14 constant ROM +750 LUT per field machine (one routed core)",
368        ..v
369    };
370    // Today's RTL (fix14) builds the constant banks as ROM logic.
371    per_field_machine(&fix13, CONST_ROM_LUT, 0)
372}