1use rhdl_fpga_planner::{BuildFlags, Component, Context, Cost, Flow, Knob, Plan, Role, SHA512_BLOCKS, Share};
39
40use crate::{
41 config::{CP_BUNDLES, CP_CONTEXTS, CREDIT_OVERHEAD, FB_BUNDLES, SIGN_CONTEXTS},
42 frontier::{self, Compress, Costs, Multiplier, Shape, VerifyMode},
43};
44
45const COMPRESS_LABELS: [&str; 3] = ["auto", "on (compress machine)", "off (each machine encodes; model)"];
47const COMPRESS: [Compress; 3] = [Compress::Auto, Compress::On, Compress::Off];
49
50#[derive(Clone, Copy, Debug, PartialEq)]
53pub struct ShapeKnobs {
54 pub manual: bool,
56 pub weights: [f64; 3],
58 pub compress: Compress,
60}
61
62impl ShapeKnobs {
63 const fn auto(weights: [f64; 3]) -> Self {
65 ShapeKnobs { manual: false, weights, compress: Compress::Auto }
66 }
67 fn knobs(&self, machine: &str) -> Vec<Knob> {
70 vec![
71 Knob::choice("block ratio", usize::from(self.manual), &["auto (best mix)", "manual (weights below)"]),
72 Knob::range(&format!(" {machine} weight"), self.weights[0], 1.0, 32.0, 1.0),
73 Knob::range(" SHA ring weight", self.weights[1], 1.0, 32.0, 1.0),
74 Knob::range(" scalar ring weight", self.weights[2], 1.0, 32.0, 1.0),
75 Knob::choice("batch compress", COMPRESS.iter().position(|c| *c == self.compress).unwrap_or(0), &COMPRESS_LABELS),
76 ]
77 }
78 fn set(&mut self, index: usize, value: f64) {
80 match index {
81 0 => self.manual = value >= 1.0,
82 1..=3 => self.weights[index - 1] = value.clamp(1.0, 32.0),
83 _ => self.compress = COMPRESS[(value.max(0.0) as usize).min(2)],
84 }
85 }
86 fn shape(&self) -> Shape {
88 Shape { ratio: self.manual.then_some(self.weights), compress: self.compress }
89 }
90 fn note(&self) -> String {
92 let mut n = String::new();
93 if self.manual {
94 let [a, b, c] = self.weights;
95 n.push_str(&format!(", ratio {a}:{b}:{c}"));
96 }
97 if self.compress != Compress::Auto {
98 n.push_str(if self.compress == Compress::On { ", batch compress on" } else { ", batch compress off" });
99 }
100 n
101 }
102}
103
104pub const MAX_CORES: u32 = 4;
111
112fn per_core(s: &Share, n: u32) -> frontier::Share {
114 frontier::Share { lut: s.lut / n, dsp: s.dsp / n, bram36: s.bram36 / n, uram: s.uram / n }
115}
116
117fn best_cores<P: Copy>(
121 share: &Share,
122 plan: impl Fn(frontier::Share) -> Option<frontier::ModulePlan<P>>,
123) -> Option<(u32, frontier::ModulePlan<P>)> {
124 let mut best: Option<(u32, frontier::ModulePlan<P>)> = None;
125 for n in 1..=MAX_CORES {
126 let Some(p) = plan(per_core(share, n)) else { continue };
127 let (rate, lut) = (n as f64 * p.rate, n * p.cost.lut);
128 let better = best.as_ref().is_none_or(|(m, b)| {
129 let (r, l) = (*m as f64 * b.rate, m * b.cost.lut);
130 rate > r + 1.0 || (rate > r - 1.0 && lut < l)
131 });
132 if better {
133 best = Some((n, p));
134 }
135 }
136 best
137}
138
139fn times(c: frontier::Cost, n: u32) -> Cost {
141 Cost { lut: c.lut * n, dsp: c.dsp * n, bram36: c.bram36 * n, uram: c.uram * n }
142}
143
144pub const SMALL_SIGNER_LATENCY: f64 = 22_240.0;
146
147pub fn sign_latency_cycles(own_sha: bool) -> f64 {
159 let sha = 3.0 * if own_sha { 81.0 } else { LINE_BLOCK_CYCLES };
162 let fb = FB_BUNDLES as f64 * 32.0;
165 let compress = compress_cycles();
166 sha + 3.0 * 32.0 + fb + compress
168}
169
170pub fn verify_latency_cycles(mode: VerifyMode, own_sha: bool) -> f64 {
175 let sha = 2.0 * if own_sha { 81.0 } else { LINE_BLOCK_CYCLES };
178 let machine = mode.bundles() * 32.0;
180 let compress = compress_cycles();
182 sha + 32.0 + machine + compress + 2.0
186}
187
188pub const LINE_BLOCK_CYCLES: f64 = 210.0;
193
194fn compress_cycles() -> f64 {
198 64.0 + CP_BUNDLES as f64 * CP_CONTEXTS as f64
199}
200
201fn loaded_in_flight(contexts: f64, cycles_per_op: f64, after_machine: f64) -> f64 {
219 (contexts + after_machine / cycles_per_op).min(SIGN_CONTEXTS as f64)
220}
221
222fn profile_token(detail: &str) -> String {
224 detail.split_whitespace().next().unwrap_or("").replace('/', ",")
225}
226
227fn multiplier_flag(detail: &str) -> &'static str {
230 if detail.contains("150-DSP") { "\"LEAN150\"" } else { "\"DSP225\"" }
231}
232
233fn buildable(gaps: &[&str]) -> BuildFlags {
237 let mut flags = vec![("buildable".into(), gaps.is_empty().to_string())];
238 if !gaps.is_empty() {
239 flags.push(("not_buildable".into(), format!("\"{}\"", gaps.join("; "))));
240 }
241 flags
242}
243
244fn memory_label(place: [u32; 3], n: u32) -> String {
246 match place {
247 [u, 0, 0] if u == n => "all machines in UltraRAM".into(),
248 [u, l, b] => format!("{u} of {n} machines in UltraRAM, {l} with LUT-RAM banks, {b} with block-RAM banks"),
249 }
250}
251
252struct Routed {
256 side: &'static str,
257 profile: [u32; 4],
258 lean: bool,
259 uram: u32,
260 lut: u32,
261 wns: f64,
262 job: &'static str,
263 rtl: &'static str,
264}
265
266const ROUTED: &[Routed] = &[
275 Routed { side: "sign", profile: [4, 0, 1, 1], lean: false, uram: 4, lut: 0, wns: 0.024, job: "007zzl", rtl: "fix13" },
276 Routed { side: "sign", profile: [8, 0, 2, 0], lean: false, uram: 8, lut: 0, wns: 0.397, job: "007zzn", rtl: "fix13" },
277 Routed { side: "sign", profile: [8, 0, 2, 1], lean: false, uram: 6, lut: 0, wns: 0.218, job: "007zp", rtl: "fix9" },
278 Routed { side: "sign", profile: [8, 0, 2, 1], lean: true, uram: 8, lut: 0, wns: 0.103, job: "007zzp", rtl: "fix13" },
279 Routed { side: "sign", profile: [8, 0, 3, 1], lean: false, uram: 8, lut: 0, wns: 0.159, job: "007zzm", rtl: "fix13" },
280 Routed { side: "sign", profile: [9, 0, 2, 1], lean: false, uram: 9, lut: 0, wns: 0.114, job: "007zl", rtl: "fix7" },
281 Routed { side: "sign", profile: [12, 0, 3, 1], lean: false, uram: 5, lut: 2, wns: 0.050, job: "007zzd", rtl: "fix10" },
282 Routed { side: "verify", profile: [1, 1, 1, 1], lean: false, uram: 1, lut: 0, wns: 0.072, job: "007zzr", rtl: "fix14" },
283 Routed { side: "verify", profile: [6, 0, 1, 1], lean: false, uram: 6, lut: 0, wns: 0.019, job: "007zzh", rtl: "fix12" },
284 Routed { side: "verify", profile: [12, 0, 1, 1], lean: false, uram: 12, lut: 0, wns: 0.031, job: "007zzo", rtl: "fix13" },
285 Routed { side: "verify", profile: [24, 0, 2, 1], lean: false, uram: 2, lut: 19, wns: 0.008, job: "007zzq", rtl: "fix13, hard route" },
286];
287
288fn routed_note(side: &str, profile: [u32; 4], lean: bool, place: [u32; 3]) -> String {
293 let verdict = |r: &Routed| {
294 if r.wns >= 0.0 {
295 format!("meets 205 MHz (+{:.3} ns, {} {})", r.wns, r.job, r.rtl)
296 } else {
297 format!("misses 205 MHz by {:.3} ns ({} {})", -r.wns, r.job, r.rtl)
298 }
299 };
300 let same = |r: &&Routed| r.side == side && r.profile == profile && r.lean == lean;
301 if let Some(r) = ROUTED.iter().filter(same).find(|r| r.uram == place[0] && r.lut == place[1]) {
302 format!("; routed: {}", verdict(r))
303 } else if let Some(r) = ROUTED.iter().find(same) {
304 format!("; routed in another layout ({} UltraRAM, {} LUT-RAM): {}", r.uram, r.lut, verdict(r))
305 } else {
306 "; timing: not routed".into()
307 }
308}
309
310fn placement_flags(detail: &str, machines: u32) -> (u32, u32) {
313 let Some(t) = detail.split("memory ").nth(1) else { return (machines, 0) };
314 if t.starts_with("all machines in UltraRAM") {
315 return (machines, 0);
316 }
317 let num = |s: &str| s.trim().split(' ').next().and_then(|x| x.parse::<u32>().ok()).unwrap_or(0);
318 (num(t), t.split("UltraRAM, ").nth(1).map_or(0, num))
319}
320
321fn machines_of(detail: &str) -> u32 {
323 profile_token(detail).split(',').next().and_then(|t| t.parse().ok()).unwrap_or(1)
324}
325
326fn cores_of(detail: &str) -> u32 {
328 detail.split(" x ").nth(1).and_then(|t| t.split_whitespace().next()).and_then(|n| n.parse().ok()).unwrap_or(1)
329}
330
331fn costs(specialized: bool) -> Costs {
336 if specialized { frontier::slimmed_costs() } else { frontier::vivado_costs() }
337}
338
339const MODULES: [&str; 2] = ["specialized (routed)", "as built (Vivado)"];
341const SHA: [&str; 2] = ["shared SHA-512 line", "own SHA rings"];
343
344#[derive(Clone, Debug)]
347pub struct Ed25519Sign {
348 pub specialized: bool,
350 pub shared_sha: bool,
352 pub shape: ShapeKnobs,
354}
355
356impl Default for Ed25519Sign {
360 fn default() -> Self {
361 Ed25519Sign { specialized: true, shared_sha: true, shape: ShapeKnobs::auto([15.0, 11.0, 3.0]) }
362 }
363}
364
365impl Component for Ed25519Sign {
367 fn name(&self) -> String {
369 "Ed25519 sign".into()
370 }
371 fn role(&self) -> Role {
373 Role::Ed25519Sign
374 }
375 fn knobs(&self) -> Vec<Knob> {
377 let mut k = vec![
378 Knob::choice("modules", usize::from(!self.specialized), &MODULES),
379 Knob::choice("SHA-512", usize::from(!self.shared_sha), &SHA),
380 ];
381 k.extend(self.shape.knobs("FB machine"));
382 k
383 }
384 fn set_knob(&mut self, index: usize, value: f64) {
388 match index {
389 0 => self.specialized = value == 0.0,
390 1 => self.shared_sha = value == 0.0,
391 _ => self.shape.set(index - 2, value),
392 }
393 }
394 fn plan(&self, share: &Share, cx: &Context) -> Option<Plan> {
398 let c = costs(self.specialized);
399 let shape = self.shape.shape();
400 let (n, p) = best_cores(share, |s| frontier::plan_sign_shaped(s, &c, cx.mhz, self.shared_sha, &shape))?;
401 let f = p.profile;
402 let small = f.fused && p.cost == c.small_signer;
404 let note = self.shape.note();
405 let cores = if n > 1 { format!(" x {n} cores") } else { String::new() };
406 let on_line = self.shared_sha && !small;
409 let sha = if on_line { "shared SHA".to_string() } else { format!("{} SHA", f.sha) };
410 let detail = if small {
411 format!("1/1/1/0 profile{cores}: Ed25519SignCoreSmall (fused, own SHA ring)")
412 } else {
413 let cp = if f.fused { "no compress machine (model)" } else { "1 compress" };
415 let pc = frontier::with_mul_pub(&c, p.multiplier);
416 let port = if self.shared_sha { frontier::SHA_CLIENT_LUT } else { 0 };
417 let place = frontier::sign_placement(&f, &pc, &p.share, port);
418 let routed = if self.specialized {
419 routed_note("sign", [f.fb, f.sha, f.sc, u32::from(!f.fused)], p.multiplier == Multiplier::Lean150, place)
420 } else {
421 String::new()
422 };
423 format!(
424 "{}/{}/{}/{} profile{cores}: {} FB, {sha}, {} scalar, {cp}, {}, memory {}{note}{routed}",
425 f.fb,
426 f.sha,
427 f.sc,
428 u32::from(!f.fused),
429 f.fb,
430 f.sc,
431 p.multiplier.name(),
432 memory_label(place, f.fb as u32)
433 )
434 };
435 Some(Plan {
436 cost: times(p.cost, n),
437 rate: n as f64 * p.rate,
438 unit: "sign",
439 limit: p.limit.into(),
440 detail,
441 demands: if on_line { vec![Flow { service: SHA512_BLOCKS, per_second: 3.0 * p.rate }] } else { Vec::new() },
442 supplies: Vec::new(),
443 measured: small || !f.fused,
446 latency_cycles: if small { SMALL_SIGNER_LATENCY } else { sign_latency_cycles(!on_line) },
448 in_flight: n as f64
454 * if f.fused {
455 32.0 * f.fb as f64
456 } else {
457 loaded_in_flight(32.0 * f.fb as f64, cx.mhz * 1e6 / p.rate, compress_cycles() + CREDIT_OVERHEAD)
458 },
459 batch: None,
460 covers: Vec::new(),
461 })
462 }
463 fn build_flags(&self, plan: &Plan) -> BuildFlags {
467 let profile = profile_token(&plan.detail);
468 let gaps: Vec<&str> = Vec::new();
469 let mut flags = vec![
470 ("ED25519_SCALABLE_PROFILE".into(), format!("\"{profile}\"")),
471 ("cores".into(), cores_of(&plan.detail).to_string()),
472 ("SH".into(), u8::from(self.shared_sha && profile != "1,1,1,0").to_string()),
473 ("memory".into(), if self.specialized { "\"SPECIALIZED\"" } else { "\"AS_BUILT\"" }.into()),
474 ("multiplier".into(), multiplier_flag(&plan.detail).into()),
475 ("uram_machines".into(), placement_flags(&plan.detail, machines_of(&plan.detail)).0.to_string()),
476 ("lut_machines".into(), placement_flags(&plan.detail, machines_of(&plan.detail)).1.to_string()),
477 ];
478 flags.extend(buildable(&gaps));
479 flags
480 }
481}
482
483#[derive(Clone, Debug)]
486pub struct Ed25519Verify {
487 pub specialized: bool,
489 pub shared_sha: bool,
491 pub mode: VerifyMode,
494 pub shape: ShapeKnobs,
496}
497
498impl Default for Ed25519Verify {
501 fn default() -> Self {
502 Ed25519Verify { specialized: true, shared_sha: true, mode: VerifyMode::KnownKey, shape: ShapeKnobs::auto([16.0, 2.0, 1.0]) }
503 }
504}
505
506const MODES: [VerifyMode; 3] = [VerifyMode::KnownKey, VerifyMode::Strict, VerifyMode::Batch];
508
509impl Component for Ed25519Verify {
511 fn name(&self) -> String {
513 "Ed25519 verify".into()
514 }
515 fn role(&self) -> Role {
517 Role::Ed25519Verify
518 }
519 fn knobs(&self) -> Vec<Knob> {
521 vec![
522 Knob::choice("modules", usize::from(!self.specialized), &MODULES),
523 Knob::choice("SHA-512", usize::from(!self.shared_sha), &SHA),
524 Knob::choice(
525 "mode",
526 MODES.iter().position(|m| *m == self.mode).unwrap(),
527 &["known-key", "strict (any key)", "batch (model, not strict)"],
528 ),
529 ]
530 .into_iter()
531 .chain(self.shape.knobs("verify machine"))
532 .collect()
533 }
534 fn set_knob(&mut self, index: usize, value: f64) {
537 match index {
538 0 => self.specialized = value == 0.0,
539 1 => self.shared_sha = value == 0.0,
540 2 => self.mode = MODES[value as usize],
541 _ => self.shape.set(index - 3, value),
542 }
543 }
544 fn plan(&self, share: &Share, cx: &Context) -> Option<Plan> {
548 let c = costs(self.specialized);
549 let shape = self.shape.shape();
550 let (n, p) = best_cores(share, |s| frontier::plan_verify_shaped(s, self.mode, &c, cx.mhz, self.shared_sha, &shape))?;
551 let note = self.shape.note();
552 let v = p.profile;
553 let cores = if n > 1 { format!(" x {n} cores") } else { String::new() };
554 let sha = if self.shared_sha { "shared SHA".to_string() } else { format!("{} SHA", v.sha) };
555 let pc = frontier::with_mul_pub(&c, p.multiplier);
556 let port = if self.shared_sha { frontier::SHA_CLIENT_LUT } else { 0 };
557 let place = frontier::verify_placement(&v, self.mode, &pc, &p.share, port);
558 let routed = if self.specialized && self.mode == VerifyMode::KnownKey {
559 routed_note("verify", [v.machines, v.sha, v.sc, v.cp], p.multiplier == Multiplier::Lean150, place)
560 } else {
561 String::new()
562 };
563 Some(Plan {
564 cost: times(p.cost, n),
565 rate: n as f64 * p.rate,
566 unit: "verify",
567 limit: p.limit.into(),
568 detail: format!(
570 "{}/{}/{}/{} profile{cores}: {} VM, {sha}, {} scalar, {} CP{}, {}, memory {}{note}{routed}",
571 v.machines,
572 v.sha,
573 v.sc,
574 v.cp,
575 v.machines,
576 v.sc,
577 v.cp,
578 if v.cp == 0 { " (machines encode; model)" } else { "" },
579 p.multiplier.name(),
580 memory_label(place, v.machines)
581 ),
582 demands: if self.shared_sha { vec![Flow { service: SHA512_BLOCKS, per_second: 2.0 * p.rate }] } else { Vec::new() },
583 supplies: Vec::new(),
584 measured: self.specialized && self.mode == VerifyMode::KnownKey && v.cp != 0,
588 latency_cycles: verify_latency_cycles(self.mode, !self.shared_sha),
589 in_flight: n as f64 * loaded_in_flight(32.0 * v.machines as f64, cx.mhz * 1e6 / p.rate, compress_cycles() + 2.0),
592 batch: None,
593 covers: Vec::new(),
594 })
595 }
596 fn build_flags(&self, plan: &Plan) -> BuildFlags {
598 let mut flags: BuildFlags = vec![
599 ("verify_profile".into(), format!("\"{}\"", profile_token(&plan.detail))),
601 ("cores".into(), cores_of(&plan.detail).to_string()),
602 ("verify_mode".into(), format!("\"{}\"", self.mode.name())),
603 ("SH".into(), u8::from(self.shared_sha).to_string()),
604 ("memory".into(), if self.specialized { "\"SPECIALIZED\"" } else { "\"AS_BUILT\"" }.into()),
605 ("multiplier".into(), multiplier_flag(&plan.detail).into()),
606 ("uram_machines".into(), placement_flags(&plan.detail, machines_of(&plan.detail)).0.to_string()),
607 ("lut_machines".into(), placement_flags(&plan.detail, machines_of(&plan.detail)).1.to_string()),
608 ];
609 let mut gaps = Vec::new();
610 if !profile_token(&plan.detail).ends_with(",1") {
611 gaps.push("KnownKeyVerifyCore has exactly one compress stage");
612 }
613 if self.mode != VerifyMode::KnownKey {
614 gaps.push("only the known-key verifier is built");
615 }
616 flags.extend(buildable(&gaps));
617 flags
618 }
619}
620
621#[derive(Clone, Debug)]
628pub struct Sha512Estimate {
629 pub lut: u32,
631 pub dsp: u32,
634 pub auto: bool,
637}
638
639pub const SHA_ESTIMATE_MIN_LUT: u32 = 5_500;
642
643pub const SHA_ESTIMATE_MAX_LUT: u32 = 432_000;
646
647impl Default for Sha512Estimate {
650 fn default() -> Self {
651 Sha512Estimate { lut: 65_000, dsp: 20, auto: true }
652 }
653}
654
655impl Sha512Estimate {
657 pub fn blocks_per_clock(&self) -> f64 {
659 self.blocks_per_clock_in(self.lut)
660 }
661 pub fn blocks_per_clock_in(&self, lut: u32) -> f64 {
664 lut as f64 / self.lut_per_block()
665 }
666 fn lut_per_block(&self) -> f64 {
671 87_200.0
672 }
673 pub fn largest_lut(&self) -> u32 {
677 (2.0 * self.lut_per_block()).ceil() as u32
678 }
679}
680
681impl Component for Sha512Estimate {
683 fn name(&self) -> String {
685 "SHA-512 line (estimate)".into()
686 }
687 fn role(&self) -> Role {
689 Role::Sha512
690 }
691 fn knobs(&self) -> Vec<Knob> {
694 vec![
695 Knob::range("LUT allowance", self.lut as f64, 0.0, f64::from(SHA_ESTIMATE_MAX_LUT), 1_000.0),
696 Knob::range("DSP allowance", self.dsp as f64, 0.0, 20.0, 1.0),
697 Knob::choice("size", usize::from(!self.auto), &["auto (never limits)", "manual (LUT allowance)"]),
698 ]
699 }
700 fn set_knob(&mut self, index: usize, value: f64) {
703 match index {
704 0 => {
705 self.lut = (value.max(0.0) as u32).min(SHA_ESTIMATE_MAX_LUT);
706 self.auto = false;
707 }
708 1 => self.dsp = value as u32,
709 _ => self.auto = value < 0.5,
710 }
711 }
712 fn plan(&self, share: &Share, cx: &Context) -> Option<Plan> {
718 let base = if self.auto { share.lut } else { self.lut };
720 if base == 0 || share.lut < base || share.dsp < self.dsp {
721 return None;
722 }
723 let lut = share.lut.min(self.largest_lut().max(base));
724 let cost = Cost { lut, dsp: self.dsp, bram36: 0, uram: 0 };
725 let per_clock = self.blocks_per_clock_in(lut);
726 let rate = per_clock * cx.mhz * 1e6;
727 Some(Plan {
728 cost,
729 rate,
730 unit: "block",
731 limit: "allowance".into(),
732 detail: format!("{per_clock:.2} blocks/clock"),
733 demands: Vec::new(),
734 supplies: vec![Flow { service: SHA512_BLOCKS, per_second: rate }],
735 measured: false,
736 latency_cycles: 82.0,
738 in_flight: 82.0 * per_clock,
740 batch: None,
741 covers: Vec::new(),
742 })
743 }
744 fn fixed(&self) -> Option<Share> {
747 Some(Share { lut: self.lut, dsp: self.dsp, ..Share::default() })
748 }
749 fn size_for(&self, per_second: f64, cx: &Context) -> Option<Share> {
752 if !self.auto {
753 return None;
754 }
755 let lut = if per_second <= 0.0 {
756 0
757 } else {
758 ((per_second / (cx.mhz * 1e6) * self.lut_per_block()).ceil() as u32).max(SHA_ESTIMATE_MIN_LUT)
759 };
760 Some(Share { lut, dsp: if lut > 0 { self.dsp } else { 0 }, ..Share::default() })
761 }
762 fn resize(&mut self, allowance: Share) {
764 if self.auto {
765 self.lut = allowance.lut;
766 }
767 }
768 fn build_flags(&self, plan: &Plan) -> BuildFlags {
771 vec![("sha_lut".into(), plan.cost.lut.to_string()), ("sha_dsp".into(), self.dsp.to_string())]
772 }
773}
774
775#[cfg(test)]
777mod tests {
778 use super::*;
779 use rhdl_fpga_planner::{Budget, Mode, Powers, Side, Slot, apply_powers, balance_mix};
780
781 fn chip(shared: bool) -> Vec<Slot> {
784 vec![
785 Slot::new(Box::new(Ed25519Sign { shared_sha: shared, ..Default::default() }), 0.0),
786 Slot::new(Box::new(Ed25519Verify { shared_sha: shared, ..Default::default() }), 0.0),
787 Slot::new(Box::new(Sha512Estimate::default()), 0.0),
788 ]
789 }
790
791 #[test]
795 fn ed25519_paper_balanced() {
796 let b = Budget::default();
797 let cx = Context::default();
798 for shared in [false, true] {
799 let mut slots = chip(shared);
800 let powers = balance_mix(Mode::Ed25519, &b, &cx, &mut slots);
801 let p = apply_powers(Mode::Ed25519, &b, &cx, &mut slots, powers);
802 let d = &p.delivered;
803 println!(
804 "{}: {:.2}M sign + {:.2}M verify (powers {:.0}/{:.0}, limited by {} / {}), {} LUT {} DSP, latency {:.0}/{:.0} us",
805 if shared { "shared SHA line" } else { "own rings" },
806 d.sign.rate / 1e6,
807 d.verify.rate / 1e6,
808 powers.sign,
809 powers.verify,
810 d.sign.bottleneck,
811 d.verify.bottleneck,
812 p.total.lut,
813 p.total.dsp,
814 d.sign.latency_us,
815 d.verify.latency_us
816 );
817 assert!(d.sign.rate > 0.0 && d.verify.rate > 0.0);
818 }
819 }
820
821 #[test]
824 fn sides_are_independent() {
825 let b = Budget::default();
826 let cx = Context::default();
827 let mut slots = chip(true);
828 let even = apply_powers(Mode::Ed25519, &b, &cx, &mut slots, Powers::default()).delivered;
829 let sign_only = apply_powers(Mode::Ed25519, &b, &cx, &mut slots, Powers { sign: 100.0, verify: 0.0 }).delivered;
830 assert_eq!(sign_only.verify.rate, 0.0);
831 assert!(sign_only.sign.rate > even.sign.rate);
832 }
833
834 #[test]
839 fn memory_moves_to_bram_and_says_how_many() {
840 let cx = Context::default();
841 let sign = Ed25519Sign::default();
842 let full = Share { lut: 420_000, dsp: 4_500, bram36: 2_016, uram: 960 };
843 let rate = |s: Share| sign.plan(&s, &cx).map(|p| (p.rate, p.cost, p.detail));
844 let (r_full, c_full, d_full) = rate(full).unwrap();
845 assert!(d_full.contains("all machines in UltraRAM"), "{d_full}");
846 let little_uram = Share { uram: c_full.uram / 3, ..full };
847 let (r_lu, c_lu, d_lu) = rate(little_uram).unwrap();
848 println!("little URAM: {:.2}M {c_lu:?} {d_lu}", r_lu / 1e6);
849 assert!(c_lu.uram <= little_uram.uram && d_lu.contains("machines in UltraRAM,") && r_lu > 0.5 * r_full, "{d_lu}");
850 let flags = sign.build_flags(&sign.plan(&little_uram, &cx).unwrap());
851 let kept: u32 = flags.iter().find(|(k, _)| k == "uram_machines").unwrap().1.parse().unwrap();
852 assert!(kept < d_lu.split('/').next().unwrap().parse::<u32>().unwrap(), "{flags:?}");
853 let little_ram = Share { uram: 40, bram36: 400, ..full };
854 let (r_lr, c_lr, d_lr) = rate(little_ram).unwrap();
855 println!("little RAM: {:.2}M {c_lr:?} {d_lr}", r_lr / 1e6);
856 assert!(c_lr.uram <= 40 && c_lr.bram36 <= 400 && r_lr < r_lu, "{d_lr}");
857 }
858
859 #[test]
861 fn ratio_and_batch_compress_knobs() {
862 let cx = Context::default();
863 let share = Share { lut: 210_000, dsp: 2_250, bram36: 1_008, uram: 480 };
864 let mut sign = Ed25519Sign { shared_sha: false, ..Ed25519Sign::default() };
865 sign.set_knob(2, 1.0); sign.set_knob(3, 4.0); sign.set_knob(4, 2.0); sign.set_knob(5, 1.0); let p = sign.plan(&share, &cx).unwrap();
870 let t: Vec<u32> = profile_token(&p.detail).split(',').map(|x| x.parse().unwrap()).collect();
871 assert_eq!((t[1], t[2]), ((t[0] as f64 / 2.0).round().max(1.0) as u32, (t[0] as f64 / 4.0).round().max(1.0) as u32), "{}", p.detail);
872 sign.set_knob(6, 2.0); let off = sign.plan(&share, &cx).unwrap();
874 assert!(profile_token(&off.detail).ends_with(",0"), "{}", off.detail);
875 let mut verify = Ed25519Verify::default();
879 verify.set_knob(7, 2.0);
880 assert!(verify.plan(&share, &cx).is_none());
881 verify.set_knob(2, 1.0); let v = verify.plan(&share, &cx).unwrap();
883 assert!(profile_token(&v.detail).ends_with(",0"), "{}", v.detail);
884 verify.set_knob(2, 0.0); verify.set_knob(7, 1.0);
886 assert!(profile_token(&verify.plan(&share, &cx).unwrap().detail).ends_with(",1"));
887 }
888
889 #[test]
900 fn sha_line_size_trades_against_the_engines() {
901 let b = Budget::default();
902 let cx = Context::default();
903 let mut last = (0.0_f64, 0.0_f64);
904 for lut in [65_000.0, 40_000.0, 10_000.0, 2_000.0] {
905 let mut slots = chip(true);
906 slots[2].component.set_knob(0, lut);
907 let p = apply_powers(Mode::Ed25519, &b, &cx, &mut slots, Powers::default());
908 let d = &p.delivered;
909 let line = p.parts[2].1.as_ref().map_or(0, |x| x.cost.lut);
910 println!(
911 "SHA allowance {lut} LUT (built {line}): {:.2}M sign / {:.2}M verify, limited by {} / {}",
912 d.sign.rate / 1e6,
913 d.verify.rate / 1e6,
914 d.side(Side::Sign).bottleneck,
915 d.side(Side::Verify).bottleneck
916 );
917 assert!(p.total.fits(&b.chip()));
918 if d.sha_bound {
919 assert_eq!(lut, 2_000.0, "only the 2k line may bind (built {line})");
921 assert!(d.side(Side::Sign).bottleneck.contains("SHA") && d.side(Side::Verify).bottleneck.contains("SHA"));
922 continue;
923 }
924 let total = d.sign.rate + d.verify.rate;
930 assert!(total >= 0.95 * (last.0 + last.1), "the total fell at {lut}");
931 assert!(d.sign.rate.min(d.verify.rate) >= 0.85 * last.0.min(last.1), "the lower rate fell far at {lut}");
932 last = (d.sign.rate, d.verify.rate);
933 }
934 let mut slots = chip(true);
937 let p = apply_powers(Mode::Ed25519, &b, &cx, &mut slots, Powers::default());
938 let d = &p.delivered;
939 println!("SHA auto: {:.2}M sign / {:.2}M verify", d.sign.rate / 1e6, d.verify.rate / 1e6);
940 assert!(!d.sha_bound && p.total.fits(&b.chip()));
941 assert!(d.sign.rate + d.verify.rate >= 0.95 * (last.0 + last.1), "auto delivers less than 10k manual");
942 }
943}
944
945#[cfg(feature = "ubft")]
960pub mod ubft {
961 use rhdl_fpga_planner::{Component, Context, Cost, Knob, Plan, Role, Share, Slot, ubft::UbftReplica};
962
963 use super::{SMALL_SIGNER_LATENCY, costs, per_core, times, verify_latency_cycles};
964 use crate::frontier::{self, SMALL_SIGNER_CYCLES, VerifyMode, VerifyProfile};
965
966 pub const UBFT_WRAPPER_LUT: u32 = 6_000;
970 pub const KEY_SLOTS_16_BRAM36: u32 = 57;
973
974 #[derive(Clone, Debug, Default)]
976 pub struct Ed25519UbftSign;
977
978 impl Component for Ed25519UbftSign {
979 fn name(&self) -> String {
980 "Ed25519 uBFT sign".into()
981 }
982 fn role(&self) -> Role {
983 Role::Ed25519Sign
984 }
985 fn knobs(&self) -> Vec<Knob> {
986 Vec::new()
987 }
988 fn set_knob(&mut self, _index: usize, _value: f64) {}
989 fn plan(&self, share: &Share, cx: &Context) -> Option<Plan> {
991 let core = times(costs(true).small_signer, 1);
992 let cost = Cost { lut: core.lut + UBFT_WRAPPER_LUT / 2, ..core };
993 cost.fits(share).then(|| Plan {
994 cost,
995 rate: cx.mhz * 1e6 / SMALL_SIGNER_CYCLES,
996 unit: "sign",
997 limit: cost.limit(share).into(),
998 detail: format!("1/1/1/0 small signer, own SHA ring; + {} LUT service logic (estimate)", UBFT_WRAPPER_LUT / 2),
999 demands: Vec::new(),
1000 supplies: Vec::new(),
1001 measured: false,
1003 latency_cycles: SMALL_SIGNER_LATENCY,
1004 in_flight: 32.0,
1005 batch: None,
1006 covers: Vec::new(),
1007 })
1008 }
1009 fn ubft(&self) -> bool {
1010 true
1011 }
1012 }
1013
1014 #[derive(Clone, Debug)]
1017 pub struct Ed25519UbftVerify {
1018 pub machines: u32,
1020 }
1021
1022 impl Default for Ed25519UbftVerify {
1023 fn default() -> Self {
1024 Ed25519UbftVerify { machines: 1 }
1025 }
1026 }
1027
1028 impl Component for Ed25519UbftVerify {
1029 fn name(&self) -> String {
1030 "Ed25519 uBFT verify".into()
1031 }
1032 fn role(&self) -> Role {
1033 Role::Ed25519Verify
1034 }
1035 fn knobs(&self) -> Vec<Knob> {
1036 vec![Knob::range("verify machines (NV)", self.machines as f64, 1.0, 4.0, 1.0)]
1037 }
1038 fn set_knob(&mut self, _index: usize, value: f64) {
1039 self.machines = value as u32;
1040 }
1041 fn plan(&self, share: &Share, cx: &Context) -> Option<Plan> {
1044 let c = costs(true);
1045 let v = VerifyProfile { machines: self.machines, sha: 1, sc: 1, cp: 1 };
1046 let core = times(frontier::verify_cost_within(&v, VerifyMode::KnownKey, &c, &per_core(share, 1), 0), 1);
1047 let cost = Cost {
1048 lut: core.lut + UBFT_WRAPPER_LUT / 2,
1049 bram36: core.bram36 + self.machines * KEY_SLOTS_16_BRAM36,
1050 ..core
1051 };
1052 let latency = verify_latency_cycles(VerifyMode::KnownKey, true);
1056 let one = VerifyProfile { machines: 1, sha: 1, sc: 1, cp: 1 };
1057 let rate_bound = (cx.mhz * 1e6 / one.cycles(VerifyMode::KnownKey)).min(cx.mhz * 1e6 * 16.0 / latency);
1058 cost.fits(share).then(|| Plan {
1059 cost,
1060 rate: rate_bound,
1061 unit: "verify",
1062 limit: format!("{}; 16 IDs, fixed-priority dispatch", cost.limit(share)),
1063 detail: format!(
1064 "{}/1/1/1 known-key, own SHA ring, 16 key slots (+{} BRAM, model); + {} LUT service logic (estimate)",
1065 self.machines,
1066 self.machines * KEY_SLOTS_16_BRAM36,
1067 UBFT_WRAPPER_LUT / 2
1068 ),
1069 demands: Vec::new(),
1070 supplies: Vec::new(),
1071 measured: false,
1072 latency_cycles: latency,
1073 in_flight: 16.0,
1074 batch: None,
1075 covers: Vec::new(),
1076 })
1077 }
1078 fn build_flags(&self, _plan: &Plan) -> rhdl_fpga_planner::BuildFlags {
1079 vec![("RHDL_UBFT_VERIFY_MACHINES".into(), self.machines.to_string())]
1080 }
1081 fn ubft(&self) -> bool {
1082 true
1083 }
1084 }
1085
1086 pub fn slots() -> Vec<Slot> {
1088 vec![
1089 Slot::new(Box::new(UbftReplica::default()), 0.0),
1090 Slot::new(Box::new(Ed25519UbftSign), 0.0),
1091 Slot::new(Box::new(Ed25519UbftVerify::default()), 0.0),
1092 ]
1093 }
1094}
1095
1096#[cfg(all(test, feature = "ubft"))]
1100mod ubft_tests {
1101 use rhdl_fpga_planner::{Budget, Context, Mode, Powers, apply_powers, ubft as u};
1102
1103 #[test]
1104 fn ubft_chip_plans_serial_crypto_without_claiming_a_baseline_ratio() {
1105 let mut slots = super::ubft::slots();
1106 let cx = Context::default();
1107 let plan = apply_powers(Mode::UbftEd25519, &Budget::whole_device(), &cx, &mut slots, Powers::default());
1108 assert!(plan.parts.iter().all(|p| p.1.is_some()), "{:?}", plan.parts);
1109 let d = &plan.delivered;
1110 for n in u::UBFT_REPLICAS {
1111 let t = u::serial_slot_crypto_us(d, n);
1112 assert_eq!(t, d.sign.latency_us + f64::from(u::quorum_verifies(n)) * d.verify.latency_us);
1113 assert!(t.is_finite() && t > 0.0);
1114 }
1115 let expect = (super::SMALL_SIGNER_LATENCY + super::verify_latency_cycles(crate::frontier::VerifyMode::KnownKey, true)) / 205.0
1118 + 1e6 / d.verify.rate;
1119 assert!((u::slot_crypto_us(d, 4) - expect).abs() < 1e-6);
1120 println!("total {:?}", plan.total);
1121 }
1122 #[test]
1123 fn ubft_extra_verifiers_add_cost_without_inventing_service_capacity() {
1124 use rhdl_fpga_planner::{Component, Cost};
1125 let share = Cost { lut: 2_000_000, dsp: 20_000, bram36: 20_000, uram: 20_000 };
1126 let cx = Context::default();
1127 let one = super::ubft::Ed25519UbftVerify { machines: 1 }.plan(&share, &cx).unwrap();
1128 let four = super::ubft::Ed25519UbftVerify { machines: 4 }.plan(&share, &cx).unwrap();
1129 assert_eq!(one.rate, four.rate);
1130 assert_eq!((one.in_flight, four.in_flight), (16.0, 16.0));
1131 assert!(four.cost.lut > one.cost.lut);
1132 let flags = super::ubft::Ed25519UbftVerify { machines: 4 }.build_flags(&four);
1133 assert!(flags.contains(&("RHDL_UBFT_VERIFY_MACHINES".into(), "4".into())));
1134 }
1135
1136}