Expand description
The planner: predicts signatures per second and resources for every build and picks the best one for a LUT budget.
§The problem
A Vivado run of this design takes hours. You cannot try every size of the engine on real tools just to learn which one fits the chip and how fast it signs. So before you build anything, you want a cheap answer to two questions:
- Does this build fit? (LUTs, block RAM, UltraRAM, DSPs.)
- How many signatures per second will it make?
This module answers both with plain arithmetic. It takes costs that were measured once on real place-and-route runs, multiplies them by how many copies a build has, and compares the result with the device and with the project’s 420,000-LUT cap. Nothing here touches hardware; it is a spreadsheet written in Rust.
§The reasoning
The engine is made of identical slices (see crate::slice). Each slice
is one complete signer. So the cost of a build is close to linear:
slices × (cost of one slice) + (things that exist once). The “things that
exist once” are the Ed25519 certificate core (crate::ed25519) and the
engine glue (the certificate unit, control and HBM I/O in
crate::engine).
The one big lever besides the slice count is where the AES S-boxes live.
(An S-box is the 256-entry byte-substitution table at the heart of AES;
Haraka-512 is built from AES rounds.) A slice has three Haraka lanes
(crate::chain_step) with 494 live S-boxes each. In LUTs an S-box costs
roughly 32 LUTs (an estimate, see LUTS_PER_LUT_SBOX); in block RAM or
UltraRAM it costs one memory instead. LUTs are the scarce budget, and the
U280 has RAM that the design does not otherwise use. So moving S-boxes
into spare RAM buys LUTs, which can buy more slices.
§The decision
Model only the choices that matter (slices: 2, 4 or 8; S-boxes per lane in
UltraRAM and block RAM), list the two sensible placements per slice count
(all-LUT and RAM-filled, see Build::all), and pick the fastest one
that fits (best). The binary dsig_plan prints the answer; the chip
planner component in planner.rs (Cargo feature planner) reuses these
same constants.
measured per-block costs ──┐
(SLICE_LUT, ED25519_LUT, │
GLUE_LUT, ...) ▼
┌──────────────┐ Resources ┌──────────┐
Build {slices, ──▶ │ resources() │ ────────────▶ │ fits(%) │──┐
uram, bram} └──────────────┘ └──────────┘ │
│ ┌───────────────────────────┐ ▼
└────────────▶ │ signatures_per_second(f) │ ──▶ best(): fastest
│ = min(slices, certs, │ build that fits,
│ frame writes) │ ties → fewer LUTs
└───────────────────────────┘A Build is a slice count (DSIG_SLICES=2|4|8) plus where each
lane’s 494 live AES S-boxes live: UltraRAM, block RAM or LUTs
(DSIG_SBOX_PLACEMENT, see sbox_placement).
Budgets are percentages of LUT_CAP (420,000 CLB LUTs, the budget DSig
and Ed25519 share for the comparison).
§Throughput model
Build::signatures_per_second is the slowest of these stages:
| Stage | Signatures per second | Why |
|---|---|---|
| slices | SLICES × f / 68 | each slice makes one key per 68-cycle period (slice_schedule) |
| certificates | 128 × f / 895 | the Ed25519 core finishes one batch per ~895 cycles with ≥ 32 in flight |
| frame writes | SLICES × port bandwidth / 2,048 B | one HBM port per slice, 64 B per clock |
times STEADY_STATE_EFFICIENCY, the fraction of the slice ceiling that
the cycle-accurate throughput test measures in steady state.
§Resource model
Per-block costs come from Vivado 2023.2 out-of-context place-and-route on
the U280 part (xcu280-fsvh2892-2L-e), except where a field says
estimate:
| Block | CLB LUT | Source |
|---|---|---|
| Slice, all S-boxes in LUTs | 127,502 | first routed slices-2 link, in context (59,946 of them LUT-RAM, mostly S-box ROMs) |
| Haraka chain-step lane | 23,902 | Vivado OOC routed |
| BLAKE3 lane | 16,422 | Vivado OOC routed |
| Ed25519SignCoreSmall | 25,383 | same link, in context (26,408 OOC) |
| Engine glue (cert unit’s BLAKE3 lane, control, HBM I/O) | 20,564 | same link: kernel 301,037 − slices − Ed25519 |
S-boxes placed in RAM go in dual-port pairs (SboxPair):
two per URAM, or two per BRAM18 (four per BRAM36).
Either way it frees about LUTS_PER_LUT_SBOX LUTs. That LUT figure is an
estimate (a 256 × 8 ROM is eight 8-input functions, four LUT6s each).
Build::fill places as many S-boxes as the device’s spare UltraRAM and
block RAM allow, keeping RAM_HEADROOM of each free.
§Worked example (the measured anchor)
The all-LUT slices-2 build is the one build that was actually routed, so
the model should reproduce it:
2 × SLICE_LUT 2 × 127,502 = 255,004
+ ED25519_LUT 25,383
+ GLUE_LUT 20,564
= kernel 300,951 (routed link: 301,037)GLUE_LUT was itself derived from that same link (kernel minus slices
minus Ed25519), so matching it is a consistency check, not an independent
validation. Every other build is an extrapolation from this one point plus
the S-box estimate.
Structs§
Constants§
- DESIGN_
MHZ - Design clock in MHz.
- DEVICE_
BRAM36 - Physical BRAM36 tiles of the xcu280.
- DEVICE_
DSP - Physical DSP48E2 slices of the xcu280.
- DEVICE_
URAM - Physical UltraRAMs of the xcu280.
- ED25519_
BRAM36 - Ed25519SignCoreSmall BRAM36 tiles in context.
- ED25519_
CYCLES_ PER_ SIGN - Measured Ed25519 cycles per signature with ≥ 32 requests in flight.
- ED25519_
DSP - Ed25519SignCoreSmall DSPs.
- ED25519_
LUT - Ed25519SignCoreSmall in context (routed
slices-2link). - FRAME_
BYTES - Bytes written per signature (one 2 KiB frame slot, 26 beats used).
- GLUE_
BRAM36 - Engine glue BRAM36 (certificate records, message FIFO), same link.
- GLUE_
LUT - Engine glue outside slices and the Ed25519 core (routed
slices-2link). - KERNEL_
BRAM36 - BRAM36 tiles a kernel can actually use: the platform’s dynamic region.
- KEY_
PERIOD - Cycles per key in one slice (the modulo schedule’s period).
- LANES_
PER_ SLICE - Haraka chain-step lanes per slice.
- LOADER_
LUT - LUTs of one lane’s UltraRAM loader (counter + one LUT S-box): an estimate.
- LUTS_
PER_ LUT_ SBOX - LUTs per LUT-ROM S-box. Estimate: the routed link’s 59,946 LUT-RAM per slice over its 1,482 S-boxes is ~40; 32 (8 outputs × 4 LUT6) is the conservative floor used here.
- LUT_CAP
- Project LUT cap (100 %): the comparison budget shared with Ed25519.
- RAM_
HEADROOM - Fraction of the device’s UltraRAM and block RAM the planner leaves free.
- SBOXES_
PER_ LANE - Live S-boxes per lane.
- SLICE_
BRAM36 - BRAM36 tiles of one slice (caches in UltraRAM, S-boxes in LUTs), from two kernel syntheses: build v3 used 202 tiles at 2 slices; build v5 used 1,362.5 at 4 slices, of which 12 lanes × 88 are T-tables, leaving 306.5. The difference, (306.5 − 202) / 2 ≈ 52 per slice, is the slice’s FIFOs, frame buffers and Merkle RAMs.
- SLICE_
LUT - Measured slice cost with every S-box in LUTs (routed
slices-2link). - SLICE_
URAM - UltraRAMs of one slice (4 banks × 18), Vivado-confirmed.
- STEADY_
STATE_ EFFICIENCY - Fraction of the slice ceiling reached in steady state. Set from the
cycle-accurate throughput test (
tests/throughput.rs); 1.0 until measured.