feat(poly_mul): implement synchronous PolyMul with base-case multiply
Phase 2.2: NTT-domain polynomial pointwise multiplication. - basecase_mul.v: degree-1 base-case multiply (c0,c1) with Barrett - poly_mul_zeta_rom.v: 128-entry zeta ROM for PolyMul - poly_mul_sync.v: FSM (IDLE→LOAD 256 cycles→COMPUTE 256 cycles→DONE) Verified: 5/5 vectors bit-exact vs Python PolyMul reference
This commit is contained in:
186
sync_rtl/poly_mul/TB/tb_poly_mul.cpp
Normal file
186
sync_rtl/poly_mul/TB/tb_poly_mul.cpp
Normal file
@@ -0,0 +1,186 @@
|
||||
// tb_poly_mul.cpp - Verilator C++ testbench for poly_mul_sync
|
||||
//
|
||||
// Reads test vectors from +VECTOR_FILE= plusarg.
|
||||
// Format: one line with 512 space-separated hex values:
|
||||
// A[0] A[1] ... A[255] B[0] B[1] ... B[255]
|
||||
// Each value is a 3-digit hex (12-bit).
|
||||
//
|
||||
// Drives DUT with 256 paired (A,B) coefficients, waits for valid_o,
|
||||
// then reads 256 output coefficients via valid/ready handshake.
|
||||
// Prints "RESULT: COEFF0 COEFF1 ... COEFF255\n" to stdout.
|
||||
//
|
||||
// Clock: 10ns period. Reset: 2 cycles.
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include "Vpoly_mul_sync.h"
|
||||
#include "verilated.h"
|
||||
|
||||
#define CLK_PERIOD_NS 10.0
|
||||
#define TIMEOUT_CYCLES 500000
|
||||
|
||||
static vluint64_t main_time = 0;
|
||||
|
||||
double sc_time_stamp() {
|
||||
return main_time;
|
||||
}
|
||||
|
||||
// Toggle clock: both edges + eval (one full cycle)
|
||||
static void posedge(Vpoly_mul_sync* dut) {
|
||||
dut->clk = 1;
|
||||
main_time += (vluint64_t)(CLK_PERIOD_NS / 2.0);
|
||||
dut->eval();
|
||||
dut->clk = 0;
|
||||
main_time += (vluint64_t)(CLK_PERIOD_NS / 2.0);
|
||||
dut->eval();
|
||||
}
|
||||
|
||||
static int hex_char_to_nibble(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Parse 3-char hex token to 12-bit value.
|
||||
static uint16_t hex3_to_val(const std::string& tok) {
|
||||
uint16_t val = 0;
|
||||
for (size_t i = 0; i < tok.length() && i < 3; i++) {
|
||||
val = (val << 4) | hex_char_to_nibble(tok[i]);
|
||||
}
|
||||
return val & 0xFFF;
|
||||
}
|
||||
|
||||
// Format 12-bit value as 3-char hex.
|
||||
static std::string val_to_hex3(uint16_t val) {
|
||||
char buf[4];
|
||||
snprintf(buf, sizeof(buf), "%03X", val & 0xFFF);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
Verilated::commandArgs(argc, argv);
|
||||
|
||||
// Parse +VECTOR_FILE= plusarg
|
||||
const char* vector_file = NULL;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
std::string arg(argv[i]);
|
||||
if (arg.rfind("+VECTOR_FILE=", 0) == 0) {
|
||||
vector_file = argv[i] + 13;
|
||||
}
|
||||
}
|
||||
|
||||
if (!vector_file) {
|
||||
std::cerr << "ERROR: +VECTOR_FILE= not specified" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::ifstream infile(vector_file);
|
||||
if (!infile.is_open()) {
|
||||
std::cerr << "ERROR: Cannot open vector file: " << vector_file << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Instantiate DUT
|
||||
Vpoly_mul_sync* dut = new Vpoly_mul_sync;
|
||||
|
||||
// Initialize
|
||||
dut->clk = 0;
|
||||
dut->rst_n = 0;
|
||||
dut->coeff_a_in = 0;
|
||||
dut->coeff_b_in = 0;
|
||||
dut->valid_i = 0;
|
||||
dut->ready_i = 0;
|
||||
|
||||
// Reset: 2 full cycles
|
||||
for (int i = 0; i < 2; i++) posedge(dut);
|
||||
dut->rst_n = 1;
|
||||
|
||||
std::string line;
|
||||
vluint64_t cycle = 0;
|
||||
int vec_count = 0;
|
||||
|
||||
while (std::getline(infile, line)) {
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
|
||||
// Parse: A[0] A[1] ... A[255] B[0] B[1] ... B[255]
|
||||
// Total: 512 hex values, one line
|
||||
std::istringstream iss(line);
|
||||
std::string tok;
|
||||
|
||||
std::vector<uint16_t> input_coeffs(512);
|
||||
int coeff_idx = 0;
|
||||
while (iss >> tok && coeff_idx < 512) {
|
||||
input_coeffs[coeff_idx] = hex3_to_val(tok);
|
||||
coeff_idx++;
|
||||
}
|
||||
if (coeff_idx != 512) {
|
||||
std::cerr << "ERROR: Expected 512 coefficients, got " << coeff_idx
|
||||
<< " (vec " << vec_count << ")" << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---- Load 256 A+B pairs ----
|
||||
while (!dut->ready_o) {
|
||||
posedge(dut); cycle++;
|
||||
if (cycle > TIMEOUT_CYCLES) goto timeout_err;
|
||||
}
|
||||
for (int i = 0; i < 256; i++) {
|
||||
dut->coeff_a_in = input_coeffs[i];
|
||||
dut->coeff_b_in = input_coeffs[256 + i];
|
||||
dut->valid_i = 1;
|
||||
posedge(dut); cycle++;
|
||||
dut->valid_i = 0;
|
||||
if (cycle > TIMEOUT_CYCLES) goto timeout_err;
|
||||
}
|
||||
|
||||
// ---- Wait for valid_o (DUT computing) ----
|
||||
dut->ready_i = 1;
|
||||
while (!dut->valid_o) {
|
||||
posedge(dut);
|
||||
cycle++;
|
||||
if (cycle > TIMEOUT_CYCLES) goto timeout_err;
|
||||
}
|
||||
|
||||
// ---- Read 256 output coefficients ----
|
||||
printf("RESULT: ");
|
||||
for (int i = 0; i < 256; i++) {
|
||||
// Wait for valid_o to be asserted (data is valid NOW)
|
||||
while (!dut->valid_o) {
|
||||
posedge(dut);
|
||||
cycle++;
|
||||
if (cycle > TIMEOUT_CYCLES) goto timeout_err;
|
||||
}
|
||||
|
||||
// Capture coefficient BEFORE consuming posedge
|
||||
uint16_t coeff_val = (uint16_t)(dut->coeff_out & 0xFFF);
|
||||
printf("%s%s", val_to_hex3(coeff_val).c_str(),
|
||||
(i < 255) ? " " : "");
|
||||
|
||||
// Consume this coefficient: posedge with ready_i=1
|
||||
posedge(dut);
|
||||
cycle++;
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
vec_count++;
|
||||
}
|
||||
|
||||
std::cout << "Processed " << vec_count << " vectors" << std::endl;
|
||||
infile.close();
|
||||
delete dut;
|
||||
return (vec_count > 0) ? 0 : 1;
|
||||
|
||||
timeout_err:
|
||||
std::cerr << "ERROR: Timeout at cycle " << cycle
|
||||
<< " (vec " << vec_count << ")" << std::endl;
|
||||
infile.close();
|
||||
delete dut;
|
||||
return 1;
|
||||
}
|
||||
45
sync_rtl/poly_mul/basecase_mul.v
Normal file
45
sync_rtl/poly_mul/basecase_mul.v
Normal file
@@ -0,0 +1,45 @@
|
||||
// basecase_mul.v - NTT-domain degree-1 polynomial multiplication
|
||||
//
|
||||
// Computes pointwise product of two degree-1 NTT-domain polynomials:
|
||||
// c0 = (a0*b0 + a1*b1*zeta) mod Q
|
||||
// c1 = (a0*b1 + a1*b0) mod Q
|
||||
//
|
||||
// Uses three barrett_mul instances for modular multiplications
|
||||
// and pure combinational modular addition for the final sums.
|
||||
//
|
||||
// All inputs/outputs are 12-bit values in [0, Q-1] where Q = 3329.
|
||||
|
||||
module basecase_mul (
|
||||
input [11:0] a0, a1, // first polynomial: degree-0 and degree-1 coeffs
|
||||
input [11:0] b0, b1, // second polynomial: degree-0 and degree-1 coeffs
|
||||
input [11:0] zeta, // precomputed twiddle factor (zeta^2 * 17 mod Q)
|
||||
output [11:0] c0, c1 // result polynomial coefficients
|
||||
);
|
||||
|
||||
localparam Q = 3329;
|
||||
|
||||
// Barrett modular multiplication outputs
|
||||
wire [11:0] t1; // a0 * b0 mod Q
|
||||
wire [11:0] t2; // a1 * b1 mod Q
|
||||
wire [11:0] t3; // a1 * b0 mod Q
|
||||
wire [11:0] t4; // a0 * b1 mod Q
|
||||
wire [11:0] t2_zeta; // (a1 * b1) * zeta mod Q
|
||||
|
||||
// Four Barrett multiplications for the scalar products
|
||||
barrett_mul u_mul1 (.a(a0), .b(b0), .product(t1));
|
||||
barrett_mul u_mul2 (.a(a1), .b(b1), .product(t2));
|
||||
barrett_mul u_mul3 (.a(a1), .b(b0), .product(t3));
|
||||
barrett_mul u_mul4 (.a(a0), .b(b1), .product(t4));
|
||||
|
||||
// Multiply t2 by zeta for the zeta term in c0
|
||||
barrett_mul u_mul_zeta (.a(t2), .b(zeta), .product(t2_zeta));
|
||||
|
||||
// Combinational modular addition: each sum is in [0, 2Q-1)
|
||||
// Subtract Q once if >= Q
|
||||
wire [12:0] sum_c0 = {1'b0, t1} + {1'b0, t2_zeta};
|
||||
wire [12:0] sum_c1 = {1'b0, t3} + {1'b0, t4};
|
||||
|
||||
assign c0 = (sum_c0 >= Q) ? sum_c0[11:0] - Q[11:0] : sum_c0[11:0];
|
||||
assign c1 = (sum_c1 >= Q) ? sum_c1[11:0] - Q[11:0] : sum_c1[11:0];
|
||||
|
||||
endmodule
|
||||
166
sync_rtl/poly_mul/poly_mul_sync.v
Normal file
166
sync_rtl/poly_mul/poly_mul_sync.v
Normal file
@@ -0,0 +1,166 @@
|
||||
// poly_mul_sync.v - Synchronous NTT-domain polynomial multiplier
|
||||
//
|
||||
// Computes pointwise (Karatsuba-like base-case) multiplication of two
|
||||
// 256-coefficient NTT-domain polynomials.
|
||||
//
|
||||
// Operation flow:
|
||||
// IDLE → LOAD (256× A+B pairs) → COMP_CALC (read+compute, 1 cycle)
|
||||
// → COMP_C0 (output c0) → COMP_C1 (output c1) → DONE → IDLE
|
||||
//
|
||||
// The LOAD phase accepts both A and B coefficients simultaneously
|
||||
// (one pair per cycle) on coeff_a_in/coeff_b_in.
|
||||
//
|
||||
// The COMPUTE phase outputs the 256 result coefficients one per cycle
|
||||
// via valid/ready handshake on coeff_out/valid_o/ready_i.
|
||||
//
|
||||
// Memory: 256×12-bit register arrays for A and B coefficients.
|
||||
//
|
||||
// Interface:
|
||||
// clk, rst_n - Clock, active-low reset
|
||||
// coeff_a_in[11:0]- Polynomial A coefficient input
|
||||
// coeff_b_in[11:0]- Polynomial B coefficient input
|
||||
// valid_i - Input valid
|
||||
// ready_o - Ready to accept input (high in IDLE/LOAD)
|
||||
// coeff_out[11:0] - Result coefficient output
|
||||
// valid_o - Output valid (high in COMP_C0/COMP_C1)
|
||||
// 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
|
||||
);
|
||||
|
||||
// State definitions
|
||||
localparam S_IDLE = 3'd0;
|
||||
localparam S_LOAD = 3'd1;
|
||||
localparam S_COMP_CALC = 3'd2;
|
||||
localparam S_COMP_C0 = 3'd3;
|
||||
localparam S_COMP_C1 = 3'd4;
|
||||
localparam S_DONE = 3'd5;
|
||||
|
||||
reg [2:0] state, next_state;
|
||||
|
||||
// Coefficient storage (register arrays)
|
||||
reg [11:0] mem_A [0:255];
|
||||
reg [11:0] mem_B [0:255];
|
||||
|
||||
// Counters
|
||||
reg [7:0] load_cnt; // 0..256 for loading 256 pairs
|
||||
reg [6:0] comp_k; // 0..127, current base-case index
|
||||
|
||||
// Registered basecase_mul results
|
||||
reg [11:0] c0_reg, c1_reg;
|
||||
|
||||
// Combinational read signals for COMP_CALC
|
||||
wire [7:0] addr_even = {comp_k, 1'b0}; // comp_k * 2 (7+1 = 8 bits)
|
||||
wire [7:0] addr_odd = {comp_k, 1'b1}; // comp_k * 2 + 1
|
||||
wire [11:0] mem_a0 = mem_A[addr_even];
|
||||
wire [11:0] mem_a1 = mem_A[addr_odd];
|
||||
wire [11:0] mem_b0 = mem_B[addr_even];
|
||||
wire [11:0] mem_b1 = mem_B[addr_odd];
|
||||
|
||||
// Zeta ROM
|
||||
wire [11:0] zeta;
|
||||
poly_mul_zeta_rom u_zeta (
|
||||
.addr (comp_k),
|
||||
.zeta (zeta)
|
||||
);
|
||||
|
||||
// Basecase multiply
|
||||
wire [11:0] bc_c0, bc_c1;
|
||||
basecase_mul u_bc (
|
||||
.a0 (mem_a0),
|
||||
.a1 (mem_a1),
|
||||
.b0 (mem_b0),
|
||||
.b1 (mem_b1),
|
||||
.zeta(zeta),
|
||||
.c0 (bc_c0),
|
||||
.c1 (bc_c1)
|
||||
);
|
||||
|
||||
// Output interface
|
||||
assign ready_o = (state == S_IDLE) || (state == S_LOAD);
|
||||
assign valid_o = (state == S_COMP_C0) || (state == S_COMP_C1);
|
||||
assign coeff_out = (state == S_COMP_C0) ? c0_reg : c1_reg;
|
||||
|
||||
// State transition logic (combinational)
|
||||
always @* begin
|
||||
next_state = state;
|
||||
case (state)
|
||||
S_IDLE: if (valid_i && ready_o) next_state = S_LOAD;
|
||||
S_LOAD: if (load_cnt >= 255 && valid_i && ready_o)
|
||||
next_state = S_COMP_CALC;
|
||||
S_COMP_CALC: next_state = S_COMP_C0;
|
||||
S_COMP_C0: if (valid_o && ready_i) next_state = S_COMP_C1;
|
||||
S_COMP_C1: if (valid_o && ready_i) begin
|
||||
if (comp_k >= 127) next_state = S_DONE;
|
||||
else next_state = S_COMP_CALC;
|
||||
end
|
||||
S_DONE: next_state = S_IDLE;
|
||||
default: next_state = S_IDLE;
|
||||
endcase
|
||||
end
|
||||
|
||||
// Sequential logic
|
||||
integer i;
|
||||
always @(posedge clk or negedge rst_n) begin
|
||||
if (!rst_n) begin
|
||||
state <= S_IDLE;
|
||||
load_cnt <= 8'd0;
|
||||
comp_k <= 7'd0;
|
||||
c0_reg <= 12'd0;
|
||||
c1_reg <= 12'd0;
|
||||
for (i = 0; i < 256; i = i + 1) begin
|
||||
mem_A[i] <= 12'd0;
|
||||
mem_B[i] <= 12'd0;
|
||||
end
|
||||
end else begin
|
||||
state <= next_state;
|
||||
|
||||
// ---- LOAD phase ----
|
||||
// First coefficient captured on IDLE → LOAD transition
|
||||
if (state == S_IDLE && valid_i && ready_o) begin
|
||||
mem_A[0] <= coeff_a_in;
|
||||
mem_B[0] <= coeff_b_in;
|
||||
load_cnt <= 8'd1;
|
||||
end
|
||||
|
||||
// Subsequent coefficients in LOAD state
|
||||
if (state == S_LOAD && valid_i && ready_o) begin
|
||||
mem_A[load_cnt] <= coeff_a_in;
|
||||
mem_B[load_cnt] <= coeff_b_in;
|
||||
load_cnt <= load_cnt + 8'd1;
|
||||
end
|
||||
|
||||
// ---- COMPUTE phase ----
|
||||
// COMP_CALC: capture basecase_mul results
|
||||
if (state == S_COMP_CALC) begin
|
||||
c0_reg <= bc_c0;
|
||||
c1_reg <= bc_c1;
|
||||
end
|
||||
|
||||
// COMP_C0 → COMP_C1: c0 was consumed, increment comp_k
|
||||
if (state == S_COMP_C0 && valid_o && ready_i) begin
|
||||
// comp_k stays same, c1 still to output
|
||||
end
|
||||
|
||||
// COMP_C1 → COMP_CALC: c1 was consumed, advance to next pair
|
||||
if (state == S_COMP_C1 && valid_o && ready_i) begin
|
||||
comp_k <= comp_k + 7'd1;
|
||||
end
|
||||
|
||||
// ---- DONE → IDLE: reset counters ----
|
||||
if (state == S_DONE) begin
|
||||
load_cnt <= 8'd0;
|
||||
comp_k <= 7'd0;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
endmodule
|
||||
58
sync_rtl/poly_mul/poly_mul_zeta_rom.v
Normal file
58
sync_rtl/poly_mul/poly_mul_zeta_rom.v
Normal file
@@ -0,0 +1,58 @@
|
||||
// poly_mul_zeta_rom.v - Read-only memory for PolyMul zeta values
|
||||
//
|
||||
// Stores precomputed zeta values for base-case NTT-domain polynomial
|
||||
// multiplication. Each entry zeta[k] = (zeta_bitRev[k]^2 * 17) mod Q.
|
||||
//
|
||||
// 128 entries × 12 bits. Pure combinational read.
|
||||
//
|
||||
// zeta_bitRev values are the same bit-reversed twiddle factors used
|
||||
// in NTT. The PolyMul formula squares them and multiplies by 17 mod Q.
|
||||
|
||||
module poly_mul_zeta_rom (
|
||||
input [6:0] addr, // 0..127, selects which zeta to read
|
||||
output [11:0] zeta // zeta = (zeta_bitRev[addr]^2 * 17) mod Q
|
||||
);
|
||||
|
||||
reg [11:0] rom [0:127];
|
||||
|
||||
// Initialize ROM with precomputed zeta values
|
||||
// Generated from Python: zeta = (zeta_bitRev[i]**2 * 17) % 3329
|
||||
initial begin
|
||||
rom[ 0] = 12'd17; rom[ 1] = 12'd3312; rom[ 2] = 12'd2761; rom[ 3] = 12'd568;
|
||||
rom[ 4] = 12'd583; rom[ 5] = 12'd2746; rom[ 6] = 12'd2649; rom[ 7] = 12'd680;
|
||||
rom[ 8] = 12'd1637; rom[ 9] = 12'd1692; rom[ 10] = 12'd723; rom[ 11] = 12'd2606;
|
||||
rom[ 12] = 12'd2288; rom[ 13] = 12'd1041; rom[ 14] = 12'd1100; rom[ 15] = 12'd2229;
|
||||
rom[ 16] = 12'd1409; rom[ 17] = 12'd1920; rom[ 18] = 12'd2662; rom[ 19] = 12'd667;
|
||||
rom[ 20] = 12'd3281; rom[ 21] = 12'd48; rom[ 22] = 12'd233; rom[ 23] = 12'd3096;
|
||||
rom[ 24] = 12'd756; rom[ 25] = 12'd2573; rom[ 26] = 12'd2156; rom[ 27] = 12'd1173;
|
||||
rom[ 28] = 12'd3015; rom[ 29] = 12'd314; rom[ 30] = 12'd3050; rom[ 31] = 12'd279;
|
||||
rom[ 32] = 12'd1703; rom[ 33] = 12'd1626; rom[ 34] = 12'd1651; rom[ 35] = 12'd1678;
|
||||
rom[ 36] = 12'd2789; rom[ 37] = 12'd540; rom[ 38] = 12'd1789; rom[ 39] = 12'd1540;
|
||||
rom[ 40] = 12'd1847; rom[ 41] = 12'd1482; rom[ 42] = 12'd952; rom[ 43] = 12'd2377;
|
||||
rom[ 44] = 12'd1461; rom[ 45] = 12'd1868; rom[ 46] = 12'd2687; rom[ 47] = 12'd642;
|
||||
rom[ 48] = 12'd939; rom[ 49] = 12'd2390; rom[ 50] = 12'd2308; rom[ 51] = 12'd1021;
|
||||
rom[ 52] = 12'd2437; rom[ 53] = 12'd892; rom[ 54] = 12'd2388; rom[ 55] = 12'd941;
|
||||
rom[ 56] = 12'd733; rom[ 57] = 12'd2596; rom[ 58] = 12'd2337; rom[ 59] = 12'd992;
|
||||
rom[ 60] = 12'd268; rom[ 61] = 12'd3061; rom[ 62] = 12'd641; rom[ 63] = 12'd2688;
|
||||
rom[ 64] = 12'd1584; rom[ 65] = 12'd1745; rom[ 66] = 12'd2298; rom[ 67] = 12'd1031;
|
||||
rom[ 68] = 12'd2037; rom[ 69] = 12'd1292; rom[ 70] = 12'd3220; rom[ 71] = 12'd109;
|
||||
rom[ 72] = 12'd375; rom[ 73] = 12'd2954; rom[ 74] = 12'd2549; rom[ 75] = 12'd780;
|
||||
rom[ 76] = 12'd2090; rom[ 77] = 12'd1239; rom[ 78] = 12'd1645; rom[ 79] = 12'd1684;
|
||||
rom[ 80] = 12'd1063; rom[ 81] = 12'd2266; rom[ 82] = 12'd319; rom[ 83] = 12'd3010;
|
||||
rom[ 84] = 12'd2773; rom[ 85] = 12'd556; rom[ 86] = 12'd757; rom[ 87] = 12'd2572;
|
||||
rom[ 88] = 12'd2099; rom[ 89] = 12'd1230; rom[ 90] = 12'd561; rom[ 91] = 12'd2768;
|
||||
rom[ 92] = 12'd2466; rom[ 93] = 12'd863; rom[ 94] = 12'd2594; rom[ 95] = 12'd735;
|
||||
rom[ 96] = 12'd2804; rom[ 97] = 12'd525; rom[ 98] = 12'd1092; rom[ 99] = 12'd2237;
|
||||
rom[100] = 12'd403; rom[101] = 12'd2926; rom[102] = 12'd1026; rom[103] = 12'd2303;
|
||||
rom[104] = 12'd1143; rom[105] = 12'd2186; rom[106] = 12'd2150; rom[107] = 12'd1179;
|
||||
rom[108] = 12'd2775; rom[109] = 12'd554; rom[110] = 12'd886; rom[111] = 12'd2443;
|
||||
rom[112] = 12'd1722; rom[113] = 12'd1607; rom[114] = 12'd1212; rom[115] = 12'd2117;
|
||||
rom[116] = 12'd1874; rom[117] = 12'd1455; rom[118] = 12'd1029; rom[119] = 12'd2300;
|
||||
rom[120] = 12'd2110; rom[121] = 12'd1219; rom[122] = 12'd2935; rom[123] = 12'd394;
|
||||
rom[124] = 12'd885; rom[125] = 12'd2444; rom[126] = 12'd2154; rom[127] = 12'd1175;
|
||||
end
|
||||
|
||||
// Combinational read
|
||||
assign zeta = rom[addr];
|
||||
|
||||
endmodule
|
||||
128
test_framework/modules/poly_mul/gen_vectors.py
Normal file
128
test_framework/modules/poly_mul/gen_vectors.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""gen_vectors.py - Test vector generator for poly_mul module.
|
||||
|
||||
Generates random NTT-domain polynomial pairs and computes expected
|
||||
pointwise base-case multiplication results using embedded Python reference.
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'lib'))
|
||||
from vector_gen import VectorGenerator
|
||||
|
||||
Q = 3329
|
||||
N = 256
|
||||
|
||||
# Bit-reversed zeta values (same as NTT zeta_bitRev)
|
||||
zeta_bitRev = [1, 1729, 2580, 3289, 2642, 630, 1897, 848,
|
||||
1062, 1919, 193, 797, 2786, 3260, 569, 1746,
|
||||
296, 2447, 1339, 1476, 3046, 56, 2240, 1333,
|
||||
1426, 2094, 535, 2882, 2393, 2879, 1974, 821,
|
||||
289, 331, 3253, 1756, 1197, 2304, 2277, 2055,
|
||||
650, 1977, 2513, 632, 2865, 33, 1320, 1915,
|
||||
2319, 1435, 807, 452, 1438, 2868, 1534, 2402,
|
||||
2647, 2617, 1481, 648, 2474, 3110, 1227, 910,
|
||||
17, 2761, 583, 2649, 1637, 723, 2288, 1100,
|
||||
1409, 2662, 3281, 233, 756, 2156, 3015, 3050,
|
||||
1703, 1651, 2789, 1789, 1847, 952, 1461, 2687,
|
||||
939, 2308, 2437, 2388, 733, 2337, 268, 641,
|
||||
1584, 2298, 2037, 3220, 375, 2549, 2090, 1645,
|
||||
1063, 319, 2773, 757, 2099, 561, 2466, 2594,
|
||||
2804, 1092, 403, 1026, 1143, 2150, 2775, 886,
|
||||
1722, 1212, 1874, 1029, 2110, 2935, 885, 2154]
|
||||
|
||||
# Precomputed PolyMul zeta values: zeta_sq_mul[i] = (zeta_bitRev[i]^2 * 17) % Q
|
||||
zeta_sq_mul = [((z * z) * 17) % Q for z in zeta_bitRev]
|
||||
|
||||
|
||||
def basecase_multiply(a0, a1, b0, b1, zeta):
|
||||
"""Degree-1 NTT-domain base-case multiply.
|
||||
|
||||
Computes:
|
||||
c0 = (a0*b0 + a1*b1*zeta) mod Q
|
||||
c1 = (a0*b1 + a1*b0) mod Q
|
||||
|
||||
Uses modular arithmetic to match Barrett reduction semantics.
|
||||
"""
|
||||
c0 = ((a0 * b0) % Q + (((a1 * b1) % Q) * zeta) % Q) % Q
|
||||
c1 = ((a0 * b1) % Q + (a1 * b0) % Q) % Q
|
||||
return c0, c1
|
||||
|
||||
|
||||
def poly_mul(f_hat, g_hat):
|
||||
"""Full NTT-domain polynomial multiplication.
|
||||
|
||||
Given two 256-coefficient NTT-domain polynomials f_hat and g_hat,
|
||||
compute pointwise product using 128 degree-1 base-case multiplies.
|
||||
|
||||
Args:
|
||||
f_hat: list of 256 coefficients in [0, Q-1]
|
||||
g_hat: list of 256 coefficients in [0, Q-1]
|
||||
|
||||
Returns:
|
||||
list of 256 result coefficients in [0, Q-1]
|
||||
"""
|
||||
h_hat = []
|
||||
for i in range(128):
|
||||
zeta = zeta_sq_mul[i]
|
||||
h1, h2 = basecase_multiply(
|
||||
f_hat[2 * i], f_hat[2 * i + 1],
|
||||
g_hat[2 * i], g_hat[2 * i + 1],
|
||||
zeta
|
||||
)
|
||||
h_hat.append(h1)
|
||||
h_hat.append(h2)
|
||||
return h_hat
|
||||
|
||||
|
||||
class PolyMulVectorGenerator(VectorGenerator):
|
||||
"""Generates test vectors for the poly_mul_sync module."""
|
||||
|
||||
def generate_one(self, params: dict) -> dict:
|
||||
"""Generate a random polynomial pair with expected result.
|
||||
|
||||
Args:
|
||||
params: Unused for basic case.
|
||||
|
||||
Returns:
|
||||
dict with 'input' (A coeffs, B coeffs) and 'expected' (C coeffs).
|
||||
"""
|
||||
# Generate random NTT-domain polynomials
|
||||
A = [random.randint(0, Q - 1) for _ in range(N)]
|
||||
B = [random.randint(0, Q - 1) for _ in range(N)]
|
||||
|
||||
# Compute expected result via Python reference
|
||||
C = poly_mul(A, B)
|
||||
|
||||
return {
|
||||
'input': {'A': A, 'B': B},
|
||||
'expected': {'C': C}
|
||||
}
|
||||
|
||||
def _format_coeffs(self, coeffs: list[int]) -> str:
|
||||
"""Format a coefficient list as space-separated 3-digit hex."""
|
||||
return ' '.join(f'{c:03X}' for c in coeffs)
|
||||
|
||||
def write_hex_file(self, vectors: list[dict], filepath: str) -> None:
|
||||
"""Write input vectors: one line per vector with 512 hex values.
|
||||
|
||||
Format: A[0] A[1] ... A[255] B[0] B[1] ... B[255]
|
||||
"""
|
||||
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||
with open(filepath, 'w') as f:
|
||||
for v in vectors:
|
||||
all_coeffs = v['input']['A'] + v['input']['B']
|
||||
hex_str = self._format_coeffs(all_coeffs)
|
||||
f.write(f'{hex_str}\n')
|
||||
|
||||
def write_expected_file(self, vectors: list[dict], filepath: str) -> None:
|
||||
"""Write expected output: one line per vector with 256 hex values.
|
||||
|
||||
Format: C[0] C[1] ... C[255]
|
||||
"""
|
||||
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||
with open(filepath, 'w') as f:
|
||||
for v in vectors:
|
||||
hex_str = self._format_coeffs(v['expected']['C'])
|
||||
f.write(f'{hex_str}\n')
|
||||
21
test_framework/modules/poly_mul/test_plan.json
Normal file
21
test_framework/modules/poly_mul/test_plan.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"module": "poly_mul",
|
||||
"rtl_top": "sync_rtl/poly_mul/poly_mul_sync.v",
|
||||
"rtl_deps": [
|
||||
"sync_rtl/poly_mul/basecase_mul.v",
|
||||
"sync_rtl/poly_mul/poly_mul_zeta_rom.v",
|
||||
"sync_rtl/ntt/barrett_mul.v"
|
||||
],
|
||||
"tb_cpp": "sync_rtl/poly_mul/TB/tb_poly_mul.cpp",
|
||||
"simulator": "verilator",
|
||||
"timeout_s": 300,
|
||||
"cases": [
|
||||
{
|
||||
"id": "basic",
|
||||
"description": "Pointwise multiply of two random NTT-domain polynomials, vs Python basecase_mul reference",
|
||||
"params": {},
|
||||
"num_vectors": 5,
|
||||
"tolerance": "bit_exact"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user