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/ntt/coeff_bram_4way.v
FallenSigh 78ccc34578 perf(mlkem): move NTT coeff storage to 4-way banked BRAM
Replace ntt_core's 256x12 LUT register file (multi-port async read, ~18.5k
LUT) with coeff_bram_4way: 4 sd_bram banks, conflict-free banking function
bank(a)={a[0], ^a[7:1]}, local(a)=a[7:1]. The dual-butterfly's four accesses
{j, j+1, j+len, j+1+len} map to four distinct banks for every layer_len in
{2..128}, so each bank needs only 1 read + 1 write/cycle (single buffer, no
ping-pong). Trades ~13k LUT for +4 BRAM (device BRAM at ~6%).

Pipeline gains one stage for the synchronous BRAM read; writeback address
delay deepened to 9. coeff_out is driven combinationally from the BRAM read
during OUTPUT; unused read ports mirror port 0 outside compute so the read
crossbar arbitration is not polluted by stale addresses.

Verified byte-exact vs NIST KAT: NTT standalone TB 19/19 (fwd+inv), Encaps
9/9, Decaps 9/9 (incl. reject + DECOMP), KeyGen, hello_world e2e — all PASS.
Cycle counts unchanged (ENC_N +4 for BRAM latency).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 05:16:58 +08:00

114 lines
4.5 KiB
Verilog

// coeff_bram_4way.v - 4-way conflict-free banked storage for NTT coefficients
//
// Replaces the 256x12 LUT register file (reg [11:0] cr [0:255]) with 4 BRAM
// banks (each 128x12 via sd_bram), removing the large multi-port LUT mux tree.
//
// Bank function: bank(a) = { a[0], ^a[7:1] } (bit0 XOR parity of upper 7 bits)
// local(a) = a[7:1] (7-bit index into the 128-deep bank)
//
// This function makes a dual-butterfly's four simultaneous coefficient accesses
// {j, j+1, j+len, j+1+len} land in four DISTINCT banks for every ML-KEM NTT
// layer_len in {2,4,...,128}: j is even and len is even, so
// j -> {0, P }
// j+1 -> {1, P }
// j+len -> {0, P'} (flipping bit m>=1 flips the upper-bits parity)
// j+1+len -> {1, P'}
// with P != P'. Four distinct banks => 4 conflict-free reads/cycle from four
// single-read-port banks, and likewise 4 conflict-free writes/cycle. Each bank
// therefore needs only 1 read + 1 write per cycle (an sd_bram), no ping-pong.
//
// Read latency is 1 cycle (sd_bram registered read): present rd_addr_k this
// cycle, rd_data_k valid next cycle. Writes commit on the presenting cycle.
//
// NOTE: the caller must guarantee the four read addresses map to four distinct
// banks (true for the NTT butterfly pattern) and the four write addresses too.
// Sequential access (load/output) uses a single port and is always safe.
module coeff_bram_4way (
input clk, rst_n,
// Four read addresses (global 8-bit); data returns 1 cycle later.
input [7:0] rd_addr_0, rd_addr_1, rd_addr_2, rd_addr_3,
output [11:0] rd_data_0, rd_data_1, rd_data_2, rd_data_3,
// Four write ports.
input wr_en_0, wr_en_1, wr_en_2, wr_en_3,
input [7:0] wr_addr_0, wr_addr_1, wr_addr_2, wr_addr_3,
input [11:0] wr_data_0, wr_data_1, wr_data_2, wr_data_3
);
// Bank id and local index for an address.
function [1:0] bank_id;
input [7:0] a;
bank_id = {a[0], ^a[7:1]};
endfunction
wire [1:0] rb0 = bank_id(rd_addr_0);
wire [1:0] rb1 = bank_id(rd_addr_1);
wire [1:0] rb2 = bank_id(rd_addr_2);
wire [1:0] rb3 = bank_id(rd_addr_3);
wire [1:0] wb0 = bank_id(wr_addr_0);
wire [1:0] wb1 = bank_id(wr_addr_1);
wire [1:0] wb2 = bank_id(wr_addr_2);
wire [1:0] wb3 = bank_id(wr_addr_3);
// Per-bank read-address crossbar: bank b reads the local index of whichever
// read port targets it (exactly one, by construction).
wire [6:0] bank_rd_local [0:3];
wire [6:0] bank_wr_local [0:3];
wire bank_wr_en [0:3];
wire [11:0] bank_wr_data [0:3];
wire [11:0] bank_rd_data [0:3];
genvar b;
generate
for (b = 0; b < 4; b = b + 1) begin : gen_xbar
// read address route (priority: 3>2>1>0; ties impossible for NTT)
assign bank_rd_local[b] =
(rb3 == b[1:0]) ? rd_addr_3[7:1] :
(rb2 == b[1:0]) ? rd_addr_2[7:1] :
(rb1 == b[1:0]) ? rd_addr_1[7:1] :
rd_addr_0[7:1];
// write route
wire m0 = wr_en_0 && (wb0 == b[1:0]);
wire m1 = wr_en_1 && (wb1 == b[1:0]);
wire m2 = wr_en_2 && (wb2 == b[1:0]);
wire m3 = wr_en_3 && (wb3 == b[1:0]);
assign bank_wr_en[b] = m0 | m1 | m2 | m3;
assign bank_wr_local[b] =
m3 ? wr_addr_3[7:1] :
m2 ? wr_addr_2[7:1] :
m1 ? wr_addr_1[7:1] : wr_addr_0[7:1];
assign bank_wr_data[b] =
m3 ? wr_data_3 :
m2 ? wr_data_2 :
m1 ? wr_data_1 : wr_data_0;
sd_bram #(.W(12), .D(128), .A(7)) u_bank (
.clk(clk),
.rd_addr(bank_rd_local[b]),
.rd_data(bank_rd_data[b]),
.wr_en(bank_wr_en[b]),
.wr_addr(bank_wr_local[b]),
.wr_data(bank_wr_data[b])
);
end
endgenerate
// Register the read bank-ids to align with sd_bram's 1-cycle read latency,
// then route each bank's output back to the requesting read port.
reg [1:0] rb0_r, rb1_r, rb2_r, rb3_r;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rb0_r <= 2'd0; rb1_r <= 2'd0; rb2_r <= 2'd0; rb3_r <= 2'd0;
end else begin
rb0_r <= rb0; rb1_r <= rb1; rb2_r <= rb2; rb3_r <= rb3;
end
end
assign rd_data_0 = bank_rd_data[rb0_r];
assign rd_data_1 = bank_rd_data[rb1_r];
assign rd_data_2 = bank_rd_data[rb2_r];
assign rd_data_3 = bank_rd_data[rb3_r];
endmodule