This repository has been archived on 2026-08-21. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
ciciec2026/rtl/ip/mlkem/poly_mul/poly_mul_sync.v
FallenSigh 975903000e perf(mlkem): ping-pong poly_mul + decoupled MAC load/accumulate (B2)
Double-buffer poly_mul_sync so the next MAC term's 256-cycle load overlaps
the current term's accumulate. Decouple the top-level MAC load index (u_ldj)
from the accumulate index (u_j) in the shared ENC_U/ENC_V/DEC_W engine; load
reads bank_a/bank_se while accumulate reads bank_t (disjoint ports, no
conflict). Accumulate stays strictly serial (psum order-dependent); only the
load is hidden.

Verified byte-exact vs NIST KAT in ~/Dev/mlkem: Encaps 9/9, Decaps 9/9
(incl. implicit-reject + DECOMP), KeyGen all, hello_world end-to-end.

ENC_U 5585->5073, ENC_V 2793->2537 (K=2). Larger relative win at K=4
(K^2 terms). poly_mul contract unchanged (standalone TB 11/11).
2026-07-11 02:37:28 +08:00

208 lines
8.5 KiB
Verilog

// poly_mul_sync.v - Synchronous NTT-domain polynomial multiplier (B2 ping-pong)
//
// Computes pointwise (Karatsuba-like base-case) multiplication of two
// 256-coefficient NTT-domain polynomials.
//
// Double-buffered (ping-pong) load so the caller can stream the NEXT operation's
// 256 A+B pairs into the idle buffer WHILE the current operation's results are
// still draining out. This hides the 256-cycle load behind the ~256-cycle
// compute/output of the previous multiply: back-to-back multiplies cost ~256
// cycles each instead of 512 (load THEN run).
//
// Contract (backward compatible with the single-shot testbench):
// - ready_o is high whenever a buffer is free to accept a load. A fresh op is
// accepted (load_active started) as soon as the first pair arrives.
// - Each op streams 256 pairs on coeff_a_in/coeff_b_in (one per accepted cyc).
// - When a buffer is full it is handed to the compute engine, which issues one
// base-case every two cycles and emits 256 result coeffs on coeff_out via
// valid_o/ready_i (c0 then c1 per pair).
// - A caller that only ever loads-then-drains (never loads during RUN) sees
// the exact same behaviour as before, just with an extra buffer.
//
// Interface (unchanged ports):
// clk, rst_n - Clock, active-low reset
// coeff_a_in[11:0]- Polynomial A coefficient (index = pairs accepted this load)
// coeff_b_in[11:0]- Polynomial B coefficient
// valid_i - Input valid
// ready_o - Ready to accept a load pair (a buffer is free)
// coeff_out[11:0] - Result coefficient output
// valid_o - Output valid
// ready_i - Output consumer ready
module poly_mul_sync (
input clk, rst_n,
input [11:0] coeff_a_in,
input [11:0] coeff_b_in,
input valid_i,
output ready_o,
output [11:0] coeff_out,
output valid_o,
input ready_i
);
// Two coefficient buffers (ping-pong).
reg [11:0] mem_A0 [0:255];
reg [11:0] mem_B0 [0:255];
reg [11:0] mem_A1 [0:255];
reg [11:0] mem_B1 [0:255];
// ---- Load side ----
reg ld_active; // currently accepting a load into ld_ptr
reg ld_ptr; // FIFO write buffer (toggles each completed load)
reg [8:0] load_cnt; // 0..256 pairs written this load
reg buf_full [0:1]; // buffer holds a complete (unconsumed) operand set
// ---- Compute side ----
reg run_active; // compute engine busy on rn_ptr
reg rn_ptr; // FIFO read buffer (toggles each completed run)
reg [7:0] issue_cnt; // 0..128 base-cases issued
reg [7:0] out_cnt; // 0..255 output coeffs consumed
reg issue_gap; // 2-cycle issue spacing
reg out_phase; // 0=c0, 1=c1
reg out_valid_r;
// Registered basecase inputs/results
reg [11:0] bc_a0_reg, bc_a1_reg;
reg [11:0] bc_b0_reg, bc_b1_reg;
reg [11:0] bc_zeta_reg;
reg bc_valid_reg;
reg [11:0] c0_reg, c1_reg;
// Compute-side memory read (from the buffer being consumed).
wire [6:0] issue_k = issue_cnt[6:0];
wire [7:0] addr_even = {issue_k, 1'b0};
wire [7:0] addr_odd = {issue_k, 1'b1};
wire [11:0] mem_a0 = rn_ptr ? mem_A1[addr_even] : mem_A0[addr_even];
wire [11:0] mem_a1 = rn_ptr ? mem_A1[addr_odd] : mem_A0[addr_odd];
wire [11:0] mem_b0 = rn_ptr ? mem_B1[addr_even] : mem_B0[addr_even];
wire [11:0] mem_b1 = rn_ptr ? mem_B1[addr_odd] : mem_B0[addr_odd];
wire [11:0] zeta;
poly_mul_zeta_rom u_zeta (.addr(issue_k), .zeta(zeta));
wire [11:0] bc_c0, bc_c1;
wire bc_vo;
basecase_mul_pipe u_bc (
.clk(clk), .rst_n(rst_n),
.valid_i(bc_valid_reg),
.a0(bc_a0_reg), .a1(bc_a1_reg),
.b0(bc_b0_reg), .b1(bc_b1_reg),
.zeta(bc_zeta_reg),
.c0(bc_c0), .c1(bc_c1),
.valid_o(bc_vo)
);
// Accept a load pair whenever the FIFO write buffer (ld_ptr) is free. Once a
// load is in progress it always finishes (ready stays high for its 256).
assign ready_o = ld_active ? 1'b1 : !buf_full[ld_ptr];
assign valid_o = out_valid_r;
assign coeff_out = out_phase ? c1_reg : c0_reg;
wire ld_fire = valid_i && ready_o;
integer i;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
ld_active <= 1'b0; ld_ptr <= 1'b0; load_cnt <= 9'd0;
buf_full[0] <= 1'b0; buf_full[1] <= 1'b0;
run_active <= 1'b0; rn_ptr <= 1'b0;
issue_cnt <= 8'd0; out_cnt <= 8'd0; issue_gap <= 1'b0;
out_phase <= 1'b0; out_valid_r <= 1'b0;
bc_a0_reg <= 12'd0; bc_a1_reg <= 12'd0;
bc_b0_reg <= 12'd0; bc_b1_reg <= 12'd0;
bc_zeta_reg <= 12'd0; bc_valid_reg <= 1'b0;
c0_reg <= 12'd0; c1_reg <= 12'd0;
for (i = 0; i < 256; i = i + 1) begin
mem_A0[i] = 12'd0; mem_B0[i] = 12'd0;
mem_A1[i] = 12'd0; mem_B1[i] = 12'd0;
end
end else begin
bc_valid_reg <= 1'b0;
// ================= LOAD SIDE (FIFO write into ld_ptr) =================
if (!ld_active) begin
if (ld_fire) begin
if (!ld_ptr) begin
mem_A0[0] <= coeff_a_in; mem_B0[0] <= coeff_b_in;
end else begin
mem_A1[0] <= coeff_a_in; mem_B1[0] <= coeff_b_in;
end
load_cnt <= 9'd1;
ld_active <= 1'b1;
end
end else begin
if (ld_fire) begin
if (!ld_ptr) begin
mem_A0[load_cnt[7:0]] <= coeff_a_in;
mem_B0[load_cnt[7:0]] <= coeff_b_in;
end else begin
mem_A1[load_cnt[7:0]] <= coeff_a_in;
mem_B1[load_cnt[7:0]] <= coeff_b_in;
end
if (load_cnt == 9'd255) begin
buf_full[ld_ptr] <= 1'b1; // buffer complete
ld_ptr <= ~ld_ptr; // FIFO advance
ld_active <= 1'b0;
load_cnt <= 9'd0;
end else begin
load_cnt <= load_cnt + 9'd1;
end
end
end
// ================= COMPUTE SIDE (FIFO read from rn_ptr) =============
if (!run_active) begin
// Start compute on the FIFO head buffer once it is full (and not
// still mid-load, which cannot happen for rn_ptr==ld_ptr while
// ld_active, since buf_full is set only at load completion).
if (buf_full[rn_ptr]) begin
run_active <= 1'b1;
issue_cnt <= 8'd0; out_cnt <= 8'd0;
issue_gap <= 1'b0; out_phase <= 1'b0; out_valid_r <= 1'b0;
end
end else begin
// Issue one base-case every two cycles.
if (issue_cnt < 8'd128) begin
if (!issue_gap) begin
bc_a0_reg <= mem_a0; bc_a1_reg <= mem_a1;
bc_b0_reg <= mem_b0; bc_b1_reg <= mem_b1;
bc_zeta_reg <= zeta;
bc_valid_reg <= 1'b1;
issue_cnt <= issue_cnt + 8'd1;
issue_gap <= 1'b1;
end else begin
issue_gap <= 1'b0;
end
end
if (out_valid_r && ready_i) begin
out_cnt <= out_cnt + 8'd1;
if (out_cnt == 8'd255) begin
out_valid_r <= 1'b0; out_phase <= 1'b0;
end else if (!out_phase) begin
out_phase <= 1'b1;
end else begin
out_valid_r <= 1'b0; out_phase <= 1'b0;
end
end
if (bc_vo) begin
c0_reg <= bc_c0; c1_reg <= bc_c1;
out_valid_r <= 1'b1; out_phase <= 1'b0;
end
// Final output consumed: free this buffer, advance FIFO read ptr.
if (out_valid_r && ready_i && out_cnt == 8'd255) begin
buf_full[rn_ptr] <= 1'b0;
rn_ptr <= ~rn_ptr;
run_active <= 1'b0;
issue_cnt <= 8'd0; out_cnt <= 8'd0;
issue_gap <= 1'b0; out_phase <= 1'b0; out_valid_r <= 1'b0;
end
end
end
end
endmodule