rhdl_ed25519_scalable_core/frontier/pareto.rs
1//! The sign / verify frontier on the whole device ([`frontier`]): every build
2//! that fits, keeping the non-dominated pairs.
3
4use super::*;
5
6/// How a module's blocks are proportioned, set from the TUI.
7#[derive(Clone, Copy, Debug, PartialEq)]
8pub struct Shape {
9 /// `None`: the planner picks the best mix. `Some([machines, sha, sc])`:
10 /// field machines : SHA-512 rings : scalar rings are kept in this ratio
11 /// (rounded, at least one each), and only the size is searched. The SHA
12 /// weight is ignored on the shared SHA-512 line (no rings).
13 pub ratio: Option<[f64; 3]>,
14 /// Batch compression (one compress machine inverting and encoding 32
15 /// points at a time).
16 pub compress: Compress,
17}
18
19/// Whether a module uses a batch-compress machine.
20#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
21pub enum Compress {
22 /// Both kinds of build are tried.
23 #[default]
24 Auto,
25 /// Only builds with a compress machine.
26 On,
27 /// Only builds without one: each field machine inverts and encodes its
28 /// own point (a longer program: [`crate::config::FUSED_BUNDLES`] for
29 /// signing, the same extra bundles for verification).
30 Off,
31}
32
33impl Compress {
34 /// Whether builds with (`true`) or without a compress machine are tried.
35 pub fn allows(self, with: bool) -> bool {
36 match self {
37 Compress::Auto => true,
38 Compress::On => with,
39 Compress::Off => !with,
40 }
41 }
42}
43
44impl Default for Shape {
45 /// The planner chooses everything.
46 fn default() -> Self {
47 Shape { ratio: None, compress: Compress::Auto }
48 }
49}
50
51impl Shape {
52 /// Whether `(machines, sha, sc)` keeps the ratio (`sha` is not checked
53 /// when `shared_sha`).
54 pub(super) fn keeps(&self, machines: u32, sha: u32, sc: u32, shared_sha: bool) -> bool {
55 let Some([m, h, r]) = self.ratio else { return true };
56 let want = |w: f64| ((machines as f64 * w / m.max(1e-9)).round() as u32).max(1);
57 sc == want(r) && (shared_sha || sha == want(h))
58 }
59}
60
61/// Extra field-machine bundles per verification without a compress
62/// machine: the per-point inversion and encoding, as in signing
63/// (`FUSED_BUNDLES - FB_BUNDLES`).
64pub const VERIFY_FUSED_EXTRA: u32 = crate::config::FUSED_BUNDLES - FB_BUNDLES;
65
66/// One point: a sign build, a verify build, their rates and total cost.
67#[derive(Clone, Copy, Debug)]
68pub struct Point {
69 /// Signing build (`None`: no signer).
70 pub sign: Option<Profile>,
71 /// Verify build (`None`: no verifier).
72 pub verify: Option<VerifyProfile>,
73 /// Signatures per second.
74 pub signs_per_sec: f64,
75 /// Verifications per second.
76 pub verifies_per_sec: f64,
77 /// Total resources.
78 pub cost: Cost,
79}
80
81/// Every non-fused signing build that fits the device on its own, with its
82/// rate (signatures/s) and cost, thinned by [`pareto_by`].
83pub(super) fn sign_options(c: &Costs, d: &Device) -> Vec<(Profile, f64, Cost)> {
84 let mut out = Vec::new();
85 // Search ranges: up to 24 fixed-base machines, 16 SHA rings, 5 scalar
86 // rings. Fused profiles are not considered.
87 for fb in 1..=24 {
88 for sha in 1..=16 {
89 for sc in 1..=5 {
90 let p = Profile { fb, sha, sc, fused: false };
91 let cost = sign_cost_within(&p, c, &d.share(), 0);
92 if !cost.fits(d) {
93 continue;
94 }
95 // Clock (Hz) divided by modeled cycles per signature.
96 let rate = d.mhz * 1e6 / p.cycles_per_sign().0;
97 out.push((p, rate, cost));
98 }
99 }
100 }
101 pareto_by(out, |x| (x.1, x.2))
102}
103
104/// Every verify build in `mode` that fits the device on its own, with its
105/// rate (verifications/s) and cost, thinned by [`pareto_by`].
106pub(super) fn verify_options(mode: VerifyMode, c: &Costs, d: &Device) -> Vec<(VerifyProfile, f64, Cost)> {
107 let mut out = Vec::new();
108 // Search ranges: up to 40 machines, 12 SHA rings, 3 scalar rings; always
109 // one compress machine.
110 for machines in 1..=40 {
111 for sha in 1..=12 {
112 for sc in 1..=3 {
113 let v = VerifyProfile { machines, sha, sc, cp: 1 };
114 let cost = verify_cost_within(&v, mode, c, &d.share(), 0);
115 if !cost.fits(d) {
116 continue;
117 }
118 out.push((v, d.mhz * 1e6 / v.cycles(mode), cost));
119 }
120 }
121 }
122 pareto_by(out, |x| (x.1, x.2))
123}
124
125/// Keep, for each rate, only builds that no cheaper build beats. "Cheaper"
126/// means fewer LUTs only; DSP, BRAM and URAM do not take part in the order.
127/// `key` returns `(rate, cost)` of an item.
128pub(super) fn pareto_by<T: Copy>(mut items: Vec<T>, key: impl Fn(&T) -> (f64, Cost)) -> Vec<T> {
129 // Ascending LUT, so each kept item is the cheapest one reaching its rate.
130 items.sort_by(|a, b| key(a).1.lut.cmp(&key(b).1.lut));
131 // Highest rate kept so far.
132 let mut best = -1.0;
133 let mut out = Vec::new();
134 for item in items {
135 let (rate, _) = key(&item);
136 // Keep only items that are faster than every cheaper item by more
137 // than 1 operation/s (the margin ignores floating-point ties).
138 if rate > best + 1.0 {
139 best = rate;
140 out.push(item);
141 }
142 }
143 out
144}
145
146/// The frontier for one verification mode: for each signing rate, the best
147/// verification rate that still fits, keeping only non-dominated points.
148/// Includes sign-only and verify-only points. Sorted by descending signing
149/// rate.
150pub fn frontier(mode: VerifyMode, c: &Costs, d: &Device) -> Vec<Point> {
151 let signs = sign_options(c, d);
152 let verifies = verify_options(mode, c, d);
153 let mut points = Vec::new();
154 // Verify only and sign only.
155 for (v, rate, cost) in &verifies {
156 points.push(Point { sign: None, verify: Some(*v), signs_per_sec: 0.0, verifies_per_sec: *rate, cost: *cost });
157 }
158 for (p, s_rate, s_cost) in &signs {
159 points.push(Point { sign: Some(*p), verify: None, signs_per_sec: *s_rate, verifies_per_sec: 0.0, cost: *s_cost });
160 // Every pair of a sign build and a verify build whose combined cost
161 // fits the device.
162 for (v, v_rate, v_cost) in &verifies {
163 let total = Cost::default().add(*s_cost, 1).add(*v_cost, 1);
164 if total.fits(d) {
165 points.push(Point {
166 sign: Some(*p),
167 verify: Some(*v),
168 signs_per_sec: *s_rate,
169 verifies_per_sec: *v_rate,
170 cost: total,
171 });
172 }
173 }
174 }
175 // Non-dominated set, ordered by signing rate.
176 // Walking from the fastest signer down, a point is kept only if its
177 // verification rate beats every faster-signing point by more than 1/s.
178 points.sort_by(|a, b| b.signs_per_sec.partial_cmp(&a.signs_per_sec).unwrap());
179 let mut front: Vec<Point> = Vec::new();
180 let mut best_verify = -1.0;
181 for p in points {
182 if p.verifies_per_sec > best_verify + 1.0 {
183 best_verify = p.verifies_per_sec;
184 front.push(p);
185 }
186 }
187 front
188}
189
190/// Best verification rate at or above a signing rate: the frontier point
191/// with the highest verifications/s among those with at least `min_signs`
192/// signatures/s. `None` if no point reaches `min_signs`.
193pub fn best_verify_at(front: &[Point], min_signs: f64) -> Option<Point> {
194 front
195 .iter()
196 .filter(|p| p.signs_per_sec >= min_signs)
197 .max_by(|a, b| a.verifies_per_sec.partial_cmp(&b.verifies_per_sec).unwrap())
198 .copied()
199}