Skip to main content

rhdl_ed25519_scalable_core/
fe17_kara.rs

1//! The 150-DSP field multiplier: three-way subtractive Karatsuba.
2//!
3//! [`FieldMulKara`] is a drop-in for [`FieldMulPar`](super::fe17::FieldMulPar):
4//! the same ports, the same initiation interval of one and the same
5//! [`MUL_LATENCY`], with 150 DSPs instead of 225.
6//!
7//! # The split
8//!
9//! The fifteen 17-bit digits of each factor form three blocks of five,
10//! `A = A0 + A1 X + A2 X^2` with `X = 2^85`. Schoolbook needs nine block
11//! products; Karatsuba needs six, all of five by five digits:
12//!
13//! - `P00 = A0 B0`, `P11 = A1 B1`, `P22 = A2 B2`;
14//! - `D01 = (A0 - A1)(B0 - B1)`, `D02 = (A0 - A2)(B0 - B2)`,
15//!   `D12 = (A1 - A2)(B1 - B2)`, digit by digit.
16//!
17//! The coefficients of `X^u` are then `G0 = P00`, `G1 = P00 + P11 - D01`,
18//! `G2 = P00 + P11 + P22 - D02`, `G3 = P11 + P22 - D12` and `G4 = P22`.
19//! The differences are digit-wise, so each difference digit is within
20//! `±(2^17 - 1)`: an 18-bit signed operand, which is exactly what a
21//! DSP48E2's 18-bit port takes, and each product (`18 x 18` signed) is one
22//! DSP. Six block products of 25 digit products are the 150 DSPs.
23//!
24//! # Pipeline
25//!
26//! | Edge | Step |
27//! |---|---|
28//! | 1 | capture `a`, `b` |
29//! | 2 | form the six operand rows (the differences are fabric subtracts) |
30//! | 3..7 | five multiply-accumulate stages ([`KaraStage`]); stage `s` adds `x_s y_j` to columns `s + j` |
31//! | 8 | `S = P00 + P11`, `T = P11 + P22` |
32//! | 9 | `G1 = S - D01`, `G2' = S + P22`, `G3 = T - D12` |
33//! | 10 | `G2 = G2' - D02` |
34//! | 11 | the 29 product columns `C_t` from the overlapping `G_u` |
35//! | 12 | fold: `c_k = C_k + 19 C_{k+15}` (`2^255 = 19 mod p`) |
36//! | 13..18 | [`MulNorm`], shared with the 225-DSP unit |
37//! | 19..23 | result delay, so the latency matches the 225-DSP unit |
38//!
39//! Every true column `C_t` is a sum of digit products, so it is
40//! non-negative and below `15 (2^17 - 1)^2 < 2^38`, whatever signs the
41//! intermediate Karatsuba terms take; the folded columns are below `2^43`,
42//! the normalizer's input width.
43
44use rhdl::prelude::*;
45use rhdl_fpga::core::dff::DFF;
46
47use super::fe17::{FieldMulIn, FieldMulOut, Fe17, MUL_LATENCY, MulNorm};
48
49/// Operand row of one block product: the multiplier digits still to use
50/// (this stage's first) and the five multiplicand digits.
51#[derive(Copy, Clone, Debug, PartialEq, Digital)]
52pub struct KaraRow {
53    /// Multiplier digits; `x[0]` is this stage's, the rest shift down.
54    pub x: [s18; 5],
55    /// Multiplicand digits, the same at every stage.
56    pub y: [s18; 5],
57}
58
59/// What one [`KaraStage`] passes to the next, for all six block products.
60///
61/// Stage `s` owns columns `s..s + 4` of each block product in `win` (window
62/// position `j` is column `s + j`). After its products, column `s` is
63/// complete and moves to `done`, which fills from the top, and the window
64/// moves up one column.
65#[derive(Copy, Clone, Debug, PartialEq, Digital)]
66pub struct KaraIo {
67    /// Operand rows.
68    pub rows: [KaraRow; 6],
69    /// Column sums in progress.
70    pub win: [[s40; 5]; 6],
71    /// Finished columns, oldest (column 0) at index 0 after five stages.
72    pub done: [[s40; 5]; 6],
73}
74
75/// One multiply-accumulate stage of all six block products (30 DSPs).
76#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
77pub struct KaraStage {
78    /// The stage's output (data only: validity rides beside the pipeline).
79    regs: DFF<KaraIo>,
80}
81
82/// Payload registers need no reset.
83impl Default for KaraStage {
84    fn default() -> Self {
85        Self { regs: DFF::resetless() }
86    }
87}
88
89/// Port types: a stage passes a [`KaraIo`] down the chain.
90impl SynchronousIO for KaraStage {
91    type I = KaraIo;
92    type O = KaraIo;
93    type Kernel = kara_stage_kernel;
94}
95
96/// State transition for [`KaraStage`]: `win[j] += x[0] * y[j]` (one DSP each,
97/// an exact `18 x 18` signed product), then retire column 0 and shift.
98#[kernel]
99pub fn kara_stage_kernel(_cr: ClockReset, i: KaraIo, q: KaraStageQ) -> (KaraIo, KaraStageD) {
100    let mut o = i;
101    for m in 0..6 {
102        let x = i.rows[m].x[0];
103        let mut win = i.win[m];
104        for j in 0..5 {
105            let p: s36 = x.xmul(i.rows[m].y[j]).as_signed_bits();
106            win[j] = i.win[m][j] + p.resize::<40>();
107        }
108        // Column s is complete: it enters `done` at the top, the older
109        // columns move down, and the window moves up one column.
110        let mut done = i.done[m];
111        for j in 0..4 {
112            done[j] = i.done[m][j + 1];
113        }
114        done[4] = win[0];
115        let mut next = [s40(0); 5];
116        for j in 0..4 {
117            next[j] = win[j + 1];
118        }
119        let mut x_next = [s18(0); 5];
120        for j in 0..4 {
121            x_next[j] = i.rows[m].x[j + 1];
122        }
123        o.rows[m].x = x_next;
124        o.win[m] = next;
125        o.done[m] = done;
126    }
127    (q.regs, KaraStageD { regs: o })
128}
129
130/// Registers of [`FieldMulKara`] outside the stages and the normalizer.
131#[derive(Copy, Clone, Debug, PartialEq, Digital)]
132#[doc(hidden)]
133pub struct KaraData {
134    /// Edge 1: captured factors.
135    a0: [b17; 15],
136    /// Edge 1.
137    b0: [b17; 15],
138    /// Edge 2: the six operand rows (zeroed sums).
139    rows: KaraIo,
140    /// Edge 8: `S = P00 + P11`, `T = P11 + P22`, and the terms still needed
141    /// (`P00`, `P22`, `D01`, `D02`, `D12`), nine columns each.
142    sum8: [[s42; 9]; 7],
143    /// Edge 9: `G0, G1, G2', G3, G4` and `D02`.
144    sum9: [[s42; 9]; 6],
145    /// Edge 10: `G0..G4`.
146    g: [[s42; 9]; 5],
147    /// Edge 11: product columns `C_0..C_28`.
148    cols: [b39; 29],
149    /// Edge 12: folded columns, the normalizer input.
150    folded: [b43; 15],
151    /// Edges 19..23: the normalized result, delayed.
152    delay: [[b17; 15]; 5],
153}
154
155/// Validity and tags, the only registers that reset.
156#[derive(Copy, Clone, Debug, PartialEq, Digital)]
157#[doc(hidden)]
158pub struct KaraCtl {
159    /// `valid[s]`: a request was captured `s + 1` edges ago.
160    valid: [bool; 23],
161    /// Tags, aligned with `valid`.
162    tag: [b16; 23],
163}
164
165/// Initiation-interval-one, 150-DSP canonical field multiplier (see the
166/// module documentation).
167#[derive(Clone, Debug, Synchronous, SynchronousDQ)]
168#[rhdl(dq_no_prefix)]
169pub struct FieldMulKara {
170    /// Valid/tag delay line (reset).
171    ctl: DFF<KaraCtl>,
172    /// Datapath registers (no reset).
173    data: DFF<KaraData>,
174    /// The five multiply-accumulate stages.
175    stages: [KaraStage; 5],
176    /// Carry normalization and final reduction (six stages).
177    norm: MulNorm,
178}
179
180/// Resets the valid/tag line; the datapath needs no reset.
181impl Default for FieldMulKara {
182    fn default() -> Self {
183        assert!(MUL_LATENCY == 23);
184        Self {
185            ctl: DFF::new(KaraCtl { valid: [false; 23], tag: [b16(0); 23] }),
186            data: DFF::resetless(),
187            stages: std::array::from_fn(|_| KaraStage::default()),
188            norm: MulNorm::default(),
189        }
190    }
191}
192
193/// Port types: [`FieldMulIn`] in, [`FieldMulOut`] out, as for
194/// [`FieldMulPar`](super::fe17::FieldMulPar).
195impl SynchronousIO for FieldMulKara {
196    type I = FieldMulIn;
197    type O = FieldMulOut;
198    type Kernel = field_mul_kara_kernel;
199}
200
201/// A 17-bit digit as a non-negative 18-bit signed operand.
202#[kernel]
203pub fn digit_s18(x: b17) -> s18 {
204    x.resize::<18>().as_signed()
205}
206
207/// `19 x` for a product column (`16x + 2x + x`).
208#[kernel]
209pub fn times19_col(x: b39) -> b43 {
210    let w = x.resize::<43>();
211    (w << 4) + (w << 1) + w
212}
213
214/// State transition for [`FieldMulKara`].
215#[kernel]
216pub fn field_mul_kara_kernel(_cr: ClockReset, i: FieldMulIn, q: Q) -> (FieldMulOut, D) {
217    let c = q.ctl;
218    let r = q.data;
219    let mut nc = c;
220    let mut n = r;
221    nc.valid[0] = i.valid;
222    nc.tag[0] = i.tag;
223    for s in 1..23 {
224        nc.valid[s] = c.valid[s - 1];
225        nc.tag[s] = c.tag[s - 1];
226    }
227    // Edge 1: capture.
228    n.a0 = i.a.d;
229    n.b0 = i.b.d;
230    // Edge 2: operand rows. Block u is digits 5u..5u+4. Rows 0..2 are the
231    // diagonal products, rows 3..5 the differences (0,1), (0,2), (1,2).
232    let mut rows = r.rows;
233    for j in 0..5 {
234        for u in 0..3 {
235            rows.rows[u].x[j] = digit_s18(r.a0[5 * u + j]);
236            rows.rows[u].y[j] = digit_s18(r.b0[5 * u + j]);
237        }
238        rows.rows[3].x[j] = digit_s18(r.a0[j]) - digit_s18(r.a0[5 + j]);
239        rows.rows[3].y[j] = digit_s18(r.b0[j]) - digit_s18(r.b0[5 + j]);
240        rows.rows[4].x[j] = digit_s18(r.a0[j]) - digit_s18(r.a0[10 + j]);
241        rows.rows[4].y[j] = digit_s18(r.b0[j]) - digit_s18(r.b0[10 + j]);
242        rows.rows[5].x[j] = digit_s18(r.a0[5 + j]) - digit_s18(r.a0[10 + j]);
243        rows.rows[5].y[j] = digit_s18(r.b0[5 + j]) - digit_s18(r.b0[10 + j]);
244    }
245    rows.win = [[s40(0); 5]; 6];
246    rows.done = [[s40(0); 5]; 6];
247    n.rows = rows;
248    // Edges 3..7 are the stages. Their output: columns 0..4 in `done`,
249    // columns 5..8 in `win[0..3]`.
250    let st = q.stages[4];
251    let mut p = [[s42(0); 9]; 6];
252    for m in 0..6 {
253        for k in 0..5 {
254            p[m][k] = st.done[m][k].resize::<42>();
255        }
256        for k in 0..4 {
257            p[m][5 + k] = st.win[m][k].resize::<42>();
258        }
259    }
260    // Edge 8: S, T and the five terms still needed.
261    for k in 0..9 {
262        n.sum8[0][k] = p[0][k] + p[1][k];
263        n.sum8[1][k] = p[1][k] + p[2][k];
264        n.sum8[2][k] = p[0][k];
265        n.sum8[3][k] = p[2][k];
266        n.sum8[4][k] = p[3][k];
267        n.sum8[5][k] = p[4][k];
268        n.sum8[6][k] = p[5][k];
269    }
270    // Edge 9: G0, G1, G2' = S + P22, G3, G4, and D02 for edge 10.
271    for k in 0..9 {
272        n.sum9[0][k] = r.sum8[2][k];
273        n.sum9[1][k] = r.sum8[0][k] - r.sum8[4][k];
274        n.sum9[2][k] = r.sum8[0][k] + r.sum8[3][k];
275        n.sum9[3][k] = r.sum8[1][k] - r.sum8[6][k];
276        n.sum9[4][k] = r.sum8[3][k];
277        n.sum9[5][k] = r.sum8[5][k];
278    }
279    // Edge 10: G2 = G2' - D02.
280    for k in 0..9 {
281        n.g[0][k] = r.sum9[0][k];
282        n.g[1][k] = r.sum9[1][k];
283        n.g[2][k] = r.sum9[2][k] - r.sum9[5][k];
284        n.g[3][k] = r.sum9[3][k];
285        n.g[4][k] = r.sum9[4][k];
286    }
287    // Edge 11: column t = 5u + s gets G_u[s] and the tail G_{u-1}[s + 5]
288    // (columns 25..28 are G4's tail alone). Each sum is a true product
289    // column, so non-negative and below 2^38.
290    let mut cv = [s42(0); 29];
291    for u in 0..5 {
292        for s in 0..5 {
293            cv[5 * u + s] = r.g[u][s];
294        }
295    }
296    for u in 1..5 {
297        for s in 0..4 {
298            cv[5 * u + s] = cv[5 * u + s] + r.g[u - 1][s + 5];
299        }
300    }
301    for s in 0..4 {
302        cv[25 + s] = r.g[4][s + 5];
303    }
304    for t in 0..29 {
305        n.cols[t] = cv[t].as_unsigned().resize::<39>();
306    }
307    // Edge 12: fold columns 15..28 onto 0..13 with the factor 19.
308    for k in 0..14 {
309        n.folded[k] = r.cols[k].resize::<43>() + times19_col(r.cols[k + 15]);
310    }
311    n.folded[14] = r.cols[14].resize::<43>();
312    // Edges 13..18: the normalizer. Edges 19..23: delay.
313    n.delay[0] = q.norm;
314    for s in 1..5 {
315        n.delay[s] = r.delay[s - 1];
316    }
317    let d = D { ctl: nc, data: n, stages: [r.rows, q.stages[0], q.stages[1], q.stages[2], q.stages[3]], norm: r.folded };
318    let out = FieldMulOut { valid: c.valid[22], r: Fe17 { d: r.delay[4] }, tag: c.tag[22] };
319    (out, d)
320}
321
322/// Cycle-level tests against the host model and the 225-DSP unit.
323#[cfg(test)]
324mod tests {
325    use super::*;
326    use crate::fe17::FieldMulPar;
327    use crate::host::Fp;
328    use crate::simulation::CycleSim;
329
330    /// Edge cases and pseudo-random elements (fixed seed).
331    fn sample(n: usize) -> Vec<Fp> {
332        let mut s = 0x2545_f491_4f6c_dd1du64;
333        let mut next = || {
334            s ^= s << 13;
335            s ^= s >> 7;
336            s ^= s << 17;
337            s
338        };
339        // 0, 1, p - 1, p - 19, and every digit at its maximum in one block
340        // (the Karatsuba differences at their extremes).
341        let max_block = |u: usize| {
342            let mut d = [0u32; 15];
343            for j in 0..5 {
344                d[5 * u + j] = 0x1ffff;
345            }
346            Fp::from_digits17(&d)
347        };
348        let mut v = vec![
349            Fp::ZERO,
350            Fp::ONE,
351            Fp::ZERO.sub(Fp::ONE),
352            Fp::ZERO.sub(Fp([19, 0, 0, 0])),
353            max_block(0),
354            max_block(1),
355            max_block(2),
356        ];
357        while v.len() < n {
358            let bytes: [u8; 32] = std::array::from_fn(|_| next() as u8);
359            v.push(Fp::from_bytes(&bytes));
360        }
361        v
362    }
363
364    /// One request per clock, every pair checked by tag against [`Fp`] and
365    /// against [`FieldMulPar`] cycle for cycle (same latency, same outputs).
366    #[test]
367    fn kara_matches_host_and_the_225_dsp_unit() {
368        let xs = sample(48);
369        let pairs: Vec<(Fp, Fp)> = xs.iter().flat_map(|a| xs.iter().take(10).map(move |b| (*a, *b))).collect();
370        let mut kara = CycleSim::new(FieldMulKara::default());
371        let mut par = CycleSim::new(FieldMulPar::default());
372        let mut got = 0;
373        for cycle in 0..pairs.len() + 40 {
374            // A gap every seventh cycle, so idle slots pass through too.
375            let (valid, a, b) = match pairs.get(cycle) {
376                Some((a, b)) if cycle % 7 != 3 => (true, Fe17::from_fp(*a), Fe17::from_fp(*b)),
377                _ => (false, Fe17::zero(), Fe17::zero()),
378            };
379            let input = FieldMulIn { valid, a, b, tag: b16((cycle & 0xffff) as u128) };
380            let k = kara.tick(input);
381            let p = par.tick(input);
382            assert_eq!(k.valid, p.valid, "cycle {cycle}: valid");
383            if k.valid {
384                assert_eq!(k.tag, p.tag, "cycle {cycle}: tag");
385                let idx = k.tag.raw() as usize;
386                let (a, b) = pairs[idx];
387                assert_eq!(k.r.to_fp(), a.mul(b), "pair {idx}");
388                assert_eq!(k.r, p.r, "pair {idx}: same digits as FieldMulPar");
389                got += 1;
390            }
391        }
392        assert_eq!(got, (0..pairs.len()).filter(|c| c % 7 != 3).count());
393    }
394
395    /// The unit lowers to Verilog.
396    #[test]
397    fn kara_lowers_to_verilog() -> Result<(), RHDLError> {
398        let text = FieldMulKara::default().descriptor("field_mul_kara".into())?.hdl_unchecked()?.modules.pretty();
399        assert!(text.contains("module field_mul_kara"));
400        Ok(())
401    }
402}