Expand description
Beginner-oriented optimization workflow, resource budgeting, and experiments.
§Optimization Lab
§Start Here: FPGA In Ten Minutes
An FPGA is not a processor running Rust instructions. RHDL turns restricted
Rust functions into circuits. A b17 * b17 expression can become a DSP
multiplier; a DFF stores a value at each rising clock edge; a SyncBRAM
stores many values with a registered read. A loop with a fixed bound usually
builds repeated hardware, not a software loop that takes many cycles.
Four quantities matter:
| Quantity | Meaning | Common mistake |
|---|---|---|
| Latency | Cycles from accepting one request to its result | Calling its reciprocal throughput |
| Initiation interval (II) | Average cycles between requests in a busy pipeline | Measuring only a short completion burst |
| Clock | Rising edges per second after timing closure | Assuming the requested clock was achieved |
| Occupancy | Independent requests held in flight | Adding arithmetic lanes without enough contexts |
At an assumed 205 MHz, 2M signs/s needs II <= 102.5 cycles. 4M needs
51.25 cycles and 5M needs 41 cycles. These are design requirements, not
measurements. For latency L and target interval I, start with at least
ceil(L/I) contexts, then add room for queueing and uneven stage service.
LUTs implement logic and multiplexers. DSPs perform registered arithmetic. Flip-flops (FFs) hold individual bits. BRAM holds arrays; read ports and width packing determine its cost. More pipeline registers can increase clock but also increase latency and the contexts needed to hide it. Registers are not free, and DSPs cannot replace every LUT in a controller.
“LUT” can mean two different things here. An FPGA logic LUT implements a Boolean function; a cryptographic lookup table stores point data and can live in BRAM. Budget them separately. The wide topology has three fixed-base table instances with 9,400,320 initialized payload bits before BRAM packing.
There is no DRAM/HBM scratch port in Fast64Core. Each cached sign consumes
one 64-byte message and returns one 64-byte signature; its three SHA blocks
reuse local state. At 2M/s that is 128 MB/s of message payload and 128 MB/s of
signature payload. These are interface-level arithmetic counts, not measured
AXI/HBM traffic: command metadata, burst padding, PCIe transfers, and the
surrounding controller can add traffic.
§Implemented Experiment
The default wide-sign feature replicates only the signing bottlenecks:
| Signing stage | Compact (--no-default-features) | Wide (default) |
|---|---|---|
| SHA-512 compressors | 4 | 8 |
| Multi-comb engines | 1 | 2 |
| Pair-compression engines | 1 | 2 |
| Scalar reduction/multiply-add | shared pipelines | same pipelines |
| Strict verifier | unchanged | unchanged |
| Signing request contexts | 128 | 128 |
| Structural DSP estimate, whole core | 5,690 | 6,440 |
The DSP row counts 150 partial products per field multiplier, 576 per scalar
reducer, and 512 in the wide scalar multiplier. Constant propagation and
technology mapping can change this count. It is not a utilization report.
Wide adds five field multipliers, not a second entire signer. Completion
mergers have BRAM queues and outstanding-credit limits; tags survive arbitrary
interleaving. parallel.rs is the place to study that change.
The ideal SHA-only throughput ceiling rises from approximately 1.129M to 2.259M/s at 205 MHz, using the 242-cycle compression latency. End-to-end throughput can be lower because of acceptance bubbles, point arithmetic, compression, context starvation, or scheduler bubbles.
The first wide experiment retained 64 contexts. All 192 signatures matched
Dalek, but its short-window rate was burst-biased. The logged completion-64/128
window gives 149.75 cycles/sign. Doubling capacity to 128 then passed 384
distinct signatures; a conservative 128-interval middle window gives
90.984375 cycles/sign, or 2.253M/s at an assumed 205 MHz. Raw edges and the
corrected window analysis are in reports/bench. This is native cycle
evidence, not a routed clock or full-core resource-fit result.
§Six Tiles Instead Of Nine
The DSP48E2 multiplier accepts signed 27-bit and 18-bit operands. Nonnegative
26-bit and 17-bit operands fit after zero extension. Split a 51-bit limb as
a = a0 + 2^26*a1 and b = b0 + 2^17*b1 + 2^34*b2. Their product has only
six partial products. Each is at most 43 bits, each row at most 77 bits, and
the final product at most 102 bits. The six registered stages are split,
multiply, row-low/high, row-merge, final-merge, and output delay. Keeping the
latency unchanged preserves every downstream valid/tag alignment.
This replaces the previous nine-tile cell without changing field radix or the external interface. It reduces the wide full-core structural count from 8,540 to 6,440. A leaf mapping test must confirm inference; full-core synthesis must still measure LUTs, FFs, BRAM packing, and timing.
The first Yosys 0.62 xcup leaf experiment measured 6 DSP48E2, 155 LUT2, and
928 FDRE for this cell. LUT-only mapping measured 7,642 LUT primitives and
928 FDRE, plus carry and wide-mux primitives. These are one-cell counts,
not a full-core estimate suitable for the TUI’s measurement import.
An important mapper limitation appeared: AREG/BREG/MREG/PREG stayed zero.
The upstream Yosys 0.62 packing pass performs its general input/multiply/output
register packing for xc7, not xcup. See the
upstream pass.
Naively repeating 928 external FFs across the wide core’s 700 limb cells gives
649,600 FFs, already above the 520k cap. Do not call this mapping a full-core
fit. Check Vivado DSP-register absorption first, then reduce redundant data
delays and register-file costs if the complete mapped design still exceeds cap.
§Resource TUI
Build and launch from the project directory:
bash tools/bounded.sh cargo build --features tuner --bin resource_tuner -j1
target/debug/resource_tunerUse Up/Down to select LUT allowance, DSP allowance, assumed clock, target,
or implemented topology. Left/Right changes the selected value. L toggles
linked sliders. S saves a plan and R reloads measurements. Q exits.
Percentages are allowances of the project’s caps, not utilization claims. Independent mode permits 100% LUT plus 100% DSP. Moving below a combined 100% raises the other slider. Linked mode always totals 100%. A 0-DSP/100-LUT plan requests LUT-only synthesis; it may exceed the LUT budget and be rejected. A 100-DSP/0-LUT plan is a valid slider endpoint but cannot fit a real core’s control logic. The tool never promises otherwise.
Headless examples:
target/debug/resource_tuner --search --dsp 75 --lut 100
target/debug/resource_tuner --write --out reports/tuning/dsp75 --dsp 75 --lut 100
target/debug/resource_tuner --import reports/tuning/dsp75/measured.jsonThe saved plan.json contains the exact export command. Run it under the
bounded wrapper, then run the generated synth.tcl on a reserved Vivado host.
Vivado’s -max_dsp constrains mapping; a post-synthesis count rejects an
oversized result. Synthesis timing is not routed timing. The generated
record leaves achieved_mhz and sign_ii null until measured separately.
The TUI initially has no full-core mapped LUT/FF data. It reports unknowns.
After two observations at different DSP limits, it interpolates the LUT
tradeoff between those limits. Extrapolation is deliberately refused. Source
fingerprints stop old netlist measurements being silently reused after edits.
An interpolated candidate still requires synthesis; only an exact matching
mapped observation can receive the mapped fit label. This is a small,
auditable heuristic search, not an opaque agent that launches expensive jobs.
§Reproducible Optimization Loop
- Pick one bottleneck using sustained measurements, not intuition alone.
- State a hypothesis: e.g. doubling SHA lanes should halve the hash service interval, but cannot improve a compression-limited stream.
- Change the RHDL module and add a tag/backpressure/differential regression.
- Run native simulation under the memory/CPU cap. Use
CycleSimfor long runs; use tracing only for short waveform captures. - Measure steady state after warmup and before drain. Check every result against Dalek outside any hardware-timing interval.
- Compile the changed leaf, then the composed top. Syntax acceptance alone does not prove equivalence or timing.
- Synthesize at several DSP ceilings. Import measured counts into the tuner.
- Route promising candidates; check FF/LUT/DSP/BRAM, WNS, congestion, and actual clock. Re-run functional RTL tests after any mapping change.
- Keep the smallest candidate meeting the measured target. Archive rejected candidates’ reason, source hash, command, and resource peak.
Long native benchmark (ignored in the quick suite):
bash tools/bounded.sh cargo test --profile sim --lib --locked -j1 \
sign::tests::sustained_signing_matches_dalek -- --ignored --exact --nocaptureThis writes reports/bench/native-wide.json. Repeat with
--no-default-features for compact. The bench checks all signatures, excludes
128 warmup completions and 128 drain completions, and labels MHz as assumed.
It also records every acceptance/completion edge and context-full cycles.
The interval starts at the last warmup completion, including the boundary
gap before the first measured completion. Recheck raw records with
jq -f tools/analyze_throughput.jq reports/bench/native-wide.json.
Increase ED25519_BENCH_COUNT to check convergence of the interval, not just
the correctness of a single burst.
§Next Avenues, In Order
Before adding more lanes, resolve the FF mapping issue above. In particular, the two-write encoded-R register file has 32,768 data FFs. Serializing paired completion writes into two mirrored BRAM read copies could replace most of them, but requires a bounded completion queue and explicit write-commit acknowledgements before the scheduler exposes each context to hashing.
- Context capacity: measure the time spent at zero free contexts. A wider pipeline can be starved by the same old request-context limit. Add BRAM contexts and widen tags only with wraparound and mixed-traffic tests.
- Inversion batching: pairs amortize inversion. Batches of 4 or 8 can reduce field multiplies per encoded point more cheaply than duplicating an entire point engine. Account for tail latency and zero-Z handling.
- Table service: the constant-address multi-comb scan takes four cycles. More banks or a different comb geometry can improve it, but replicated BRAM and selection fanout must be mapped. Never use secret RAM addresses.
- Scalar overprovisioning: II-one reducers have far more throughput than a roughly 100-cycle signer needs. Time-sharing a pipelined scalar datapath could recover DSPs for point operations. Preserve reduction/canonical rules.
- DSP-aware arithmetic: compare current 17-bit tiles with DSP48E2-friendly asymmetric products and fused multiply-add stages. Prove carry bounds before changing radix. Benchmark squaring separately from multiplication.
- Scheduler timing: hierarchical arbitration and registered operand selection can shrink large mux cones. More stages require retiming tags and credits, not merely inserting DFFs into data paths.
With the six-tile cell, three signing ways give a structural estimate of 7,190 DSPs; four ways give 7,940. That is below 9,000, opening a concrete 4M/s research path that the previous nine-tile arithmetic could not afford. It does not establish LUT/FF fit. Four-way SHA alone has an ideal ceiling of about 4.52M/s at 205 MHz; 5M/s needs at least 226.875 MHz before any scheduler or point-arithmetic overhead. Research rows in the TUI are bounds, not exportable implementations. No physical fit or 2M/4M/5M claim follows from them.
§Safe Key Lifecycle
The current interface has no cancellation result. Drain accepted signing
requests before clear/reload, and do not clear while key expansion is in
flight. clear_key clears cached fields, not every intermediate pipeline
register. For tenant changes, apply a full synchronous reset after draining.
Constant-pattern arithmetic is not proof of resistance to power/EM leakage.
§DSig Integration
The companion DSig project may sign a versioned 64-byte certificate envelope:
domain8, version/profile4 LE, batch-size4 LE, epoch8 LE, batch-id8 LE, root32.
Words512::from_le_bytes packs that exact message and does not prehash it.
The caller owns domain separation and batching semantics. This is not byte
compatible with a protocol that signs only a 32-byte root. Report online
one-time signatures and background Ed25519 certifications as distinct rates.