Skip to main content

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}