Skip to main content

rhdl_fpga_planner/
delivered.rs

1//! Step 4 of a plan: what the chip delivers per side ([`deliver`]).
2
3use crate::*;
4
5/// Demand and supply of one service.
6#[derive(Clone, Debug, PartialEq)]
7pub struct Service {
8    /// Service name.
9    pub name: &'static str,
10    /// Demand per second at the delivered rates.
11    pub demand: f64,
12    /// Supply per second.
13    pub supply: f64,
14}
15
16/// What one side delivers.
17#[derive(Clone, Debug, PartialEq)]
18pub struct SideRate {
19    /// Delivered operations per second.
20    pub rate: f64,
21    /// What stops more.
22    pub bottleneck: String,
23    /// Idle latency of one operation in microseconds: the slowest module of
24    /// the side (DSig's Ed25519 work runs ahead of or beside DSig's).
25    pub latency_us: f64,
26}
27
28/// Delivered rates of the chip.
29#[derive(Clone, Debug, PartialEq)]
30pub struct Delivered {
31    /// The sign side.
32    pub sign: SideRate,
33    /// The verify side.
34    pub verify: SideRate,
35    /// Delivered operations per second of each role.
36    pub per_role: Vec<(Role, f64)>,
37    /// Idle latency of each role's slowest module in microseconds (plus the
38    /// SHA line's passes for modules that hash on it).
39    pub latency_us: Vec<(Role, f64)>,
40    /// Services at the delivered rates.
41    pub services: Vec<Service>,
42    /// The SHA-512 line limits the chip (shrinking it lowers both sides).
43    pub sha_bound: bool,
44    /// SHA-512 blocks per second both sides would hash at the rates they
45    /// deliver before any SHA-512 limit: what a line must supply so it is
46    /// not the bottleneck.
47    pub sha_wanted: f64,
48    /// DSig's batch size used for the needs.
49    pub batch: f64,
50    /// DSig's sign and verify modules report different batch sizes (the
51    /// signer's is used).
52    pub batch_mismatch: bool,
53    /// Operations of each role per delivered operation of its side: the
54    /// paper's [`Mode::need`], or zero for a role another module
55    /// [covers](Plan::covers).
56    pub needs: Vec<(Role, f64)>,
57}
58
59/// Lookups by side and role.
60impl Delivered {
61    /// The delivered rate of `side`.
62    pub fn side(&self, side: Side) -> &SideRate {
63        match side {
64            Side::Sign => &self.sign,
65            Side::Verify => &self.verify,
66        }
67    }
68    /// Delivered rate of `role` (0 if absent).
69    pub fn of(&self, role: Role) -> f64 {
70        self.per_role.iter().find(|(r, _)| *r == role).map_or(0.0, |(_, x)| *x)
71    }
72    /// Operations of `role` per delivered operation of its side (0 if the
73    /// role takes no part).
74    pub fn need(&self, role: Role) -> f64 {
75        self.needs.iter().find(|(r, _)| *r == role).map_or(0.0, |(_, x)| *x)
76    }
77    /// Idle latency of `role` in microseconds (0 if absent).
78    pub fn latency_of(&self, role: Role) -> f64 {
79        self.latency_us.iter().find(|(r, _)| *r == role).map_or(0.0, |(_, x)| *x)
80    }
81}
82
83/// Delivered rates of `parts` (the plans of `slots`) in `mode`.
84pub fn deliver(mode: Mode, cx: &Context, slots: &[Slot], parts: &[(Share, Option<Plan>)]) -> Delivered {
85    // Per operation role (indexed like `Role::OPS`): summed capacity, SHA-512
86    // blocks each operation hashes on the shared line, and idle latency.
87    let mut capacity = [0.0f64; 4];
88    let mut blocks_per_op = [0.0f64; 4];
89    let mut latency = [0.0f64; 4];
90    // Blocks per second the SHA-512 line(s) supply, and one pass through it.
91    let mut supply = 0.0;
92    let mut sha_latency_us = 0.0f64;
93    // DSig's batch size, from its sign module if it reports one.
94    let mut batch = DSIG_BATCH;
95    let mut verify_batch = None;
96    // Roles some module does itself (their need is zero).
97    let mut covered: Vec<Role> = Vec::new();
98    for (slot, (_, plan)) in slots.iter().zip(parts) {
99        let Some(p) = plan else { continue };
100        covered.extend(p.covers.iter().copied());
101        if slot.component.role() == Role::Sha512 {
102            sha_latency_us = sha_latency_us.max(p.idle_latency_us(cx.mhz));
103        }
104        if slot.component.role() == Role::DsigSign {
105            if let Some(k) = p.batch {
106                batch = k;
107            }
108        }
109        if slot.component.role() == Role::DsigVerify {
110            verify_batch = p.batch;
111        }
112        supply += p.supplies.iter().filter(|f| f.service == SHA512_BLOCKS).map(|f| f.per_second).sum::<f64>();
113    }
114    for (slot, (_, plan)) in slots.iter().zip(parts) {
115        let Some(p) = plan else { continue };
116        let Some(k) = Role::OPS.iter().position(|r| *r == slot.component.role()) else { continue };
117        capacity[k] += p.rate;
118        let blocks: f64 = p.demands.iter().filter(|f| f.service == SHA512_BLOCKS).map(|f| f.per_second).sum();
119        if p.rate > 0.0 {
120            blocks_per_op[k] = blocks / p.rate;
121        }
122        // A module on the shared line waits for its blocks in turn: one
123        // pipeline pass per block of the operation (they are dependent).
124        let sha = if blocks > 0.0 { blocks_per_op[k] * sha_latency_us } else { 0.0 };
125        latency[k] = latency[k].max(p.idle_latency_us(cx.mhz) + sha);
126    }
127    // What each role needs per delivered operation of its side.
128    let need = |role: Role| if covered.contains(&role) { 0.0 } else { mode.need(role, cx, batch) };
129    // Each side alone: the tightest of its roles' capacity / need.
130    let mut sides = Vec::new();
131    let mut blocks_per_side_op = [0.0f64; 2];
132    for (s, side) in Side::BOTH.iter().enumerate() {
133        let mut rate = f64::INFINITY;
134        let mut bottleneck = String::from("nothing");
135        let mut lat = 0.0f64;
136        for role in side.roles() {
137            let need = need(role);
138            if need <= 0.0 {
139                continue;
140            }
141            let k = Role::OPS.iter().position(|r| *r == role).unwrap();
142            let cap = capacity[k] / need;
143            if cap < rate {
144                rate = cap;
145                bottleneck = role.name().to_string();
146            }
147            blocks_per_side_op[s] += need * blocks_per_op[k];
148            lat = lat.max(latency[k]);
149        }
150        if !rate.is_finite() {
151            rate = 0.0;
152        }
153        sides.push(SideRate { rate, bottleneck, latency_us: lat });
154    }
155    // The shared line: if both sides together want more blocks than it
156    // supplies, both slow down in proportion.
157    let demand = sides[0].rate * blocks_per_side_op[0] + sides[1].rate * blocks_per_side_op[1];
158    let mut sha_bound = false;
159    if demand > supply && demand > 0.0 {
160        let scale = supply / demand;
161        for s in sides.iter_mut() {
162            if s.rate > 0.0 {
163                s.rate *= scale;
164                s.bottleneck = "SHA-512 line".into();
165            }
166        }
167        sha_bound = true;
168    }
169    let verify = sides.pop().unwrap();
170    let sign = sides.pop().unwrap();
171    let per_role = Role::OPS
172        .iter()
173        .map(|r| {
174            let side_rate = match r.side() {
175                Some(Side::Sign) => sign.rate,
176                _ => verify.rate,
177            };
178            (*r, side_rate * need(*r))
179        })
180        .collect();
181    let latency_us = Role::OPS.iter().enumerate().filter(|(_, r)| need(**r) > 0.0).map(|(k, r)| (*r, latency[k])).collect();
182    let needs = Role::OPS.iter().map(|r| (*r, need(*r))).collect();
183    let used = sign.rate * blocks_per_side_op[0] + verify.rate * blocks_per_side_op[1];
184    let services = if used > 0.0 || supply > 0.0 { vec![Service { name: SHA512_BLOCKS, demand: used, supply }] } else { Vec::new() };
185    let batch_mismatch = verify_batch.is_some_and(|k| k != batch);
186    Delivered { sign, verify, per_role, latency_us, services, sha_bound, sha_wanted: demand, batch, batch_mismatch, needs }
187}