feat: add RTL tests for NPU vector, memory, and patch packer

- Introduced  for comprehensive testing of the NPU vector module, covering bias preload, lane-masked accumulation, and fault conditions.
- Created  to implement differential tests for NPU compute-core RTL using Icarus Verilog and Verilator, ensuring bit-exact results against a numerical oracle.
- Added  to validate NPU memory and patch packer functionality, including SRAM models and various packing scenarios.
This commit is contained in:
maksyuki committed 2026-09-17 09:11:56 +08:00
1 parent c8953d9558
commit f85ff59e33
19 files changed
+4595 -1

No files matched your search

+18
View File
@@ -0,0 +1,18 @@
# NPU-P1 isolated core STA constraints: single 72 MHz HP clock at the
# 1000000/72 ps target period, mirroring generate_sdc.py conventions.
# NOTE: the locked OpenSTA build has no remove_from_collection/filter_collection,
# so the input-delay exclusion of clock/reset ports is done by name iteration.
create_clock -name clk_i -period 13.888889 [get_ports clk_i]
set_clock_uncertainty -setup 0.2 [get_clocks clk_i]
set_clock_uncertainty -hold 0.1 [get_clocks clk_i]
set_clock_transition 0.1 [get_clocks clk_i]
set_false_path -from [get_ports rst_n_i]
set npu_io_inputs [list]
foreach npu_port [all_inputs] {
set npu_port_name [get_property $npu_port name]
if {$npu_port_name ne "clk_i" && $npu_port_name ne "rst_n_i"} {
lappend npu_io_inputs $npu_port
}
}
set_input_delay -clock clk_i 2.0 $npu_io_inputs
set_output_delay -clock clk_i 2.0 [all_outputs]
+54
View File
@@ -0,0 +1,54 @@
# NPU-P1 isolated OpenSTA run: parameterized top with path-based metrics.
# The locked OpenSTA 3.0.0 build reports no rows from report_checks/report_wns
# under this constraint set, so WNS/TNS are computed from find_timing_paths.
foreach variable {OPENSTA_NETLIST OPENSTA_LIBERTY OPENSTA_LINK_LIBS OPENSTA_SDC OPENSTA_REPORT OPENSTA_METRICS OPENSTA_TOP} {
if {![info exists ::env($variable)] || $::env($variable) eq ""} {
error "required environment variable is missing: $variable"
}
}
foreach liberty $::env(OPENSTA_LINK_LIBS) {
read_liberty $liberty
}
read_liberty $::env(OPENSTA_LIBERTY)
if {[info exists ::env(OPENSTA_SRAM_LIBS)] && $::env(OPENSTA_SRAM_LIBS) ne ""} {
foreach liberty $::env(OPENSTA_SRAM_LIBS) {
read_liberty $liberty
}
}
read_verilog $::env(OPENSTA_NETLIST)
link_design $::env(OPENSTA_TOP)
read_sdc $::env(OPENSTA_SDC)
proc npu_collect_paths {delay report} {
set worst 999.0
set total 0.0
set count 0
puts $report "path_delay $delay"
foreach path [find_timing_paths -path_delay $delay -slack_max 999.0 \
-group_path_count 100000 -endpoint_path_count 100000] {
set slack [get_property $path slack]
incr count
if {$slack < $worst} { set worst $slack }
if {$slack < 0.0} {
set total [expr {$total + $slack}]
puts $report "VIOLATED [get_property $path startpoint] -> [get_property $path endpoint] slack=$slack"
}
}
if {$count == 0} { set worst 0.0 }
return [list $worst $total $count]
}
set report [open $::env(OPENSTA_REPORT) "w"]
set setup [npu_collect_paths max $report]
set hold [npu_collect_paths min $report]
close $report
set metrics [open $::env(OPENSTA_METRICS) "w"]
puts $metrics "wns_max=[lindex $setup 0]"
puts $metrics "tns_max=[lindex $setup 1]"
puts $metrics "paths_max=[lindex $setup 2]"
puts $metrics "wns_min=[lindex $hold 0]"
puts $metrics "tns_min=[lindex $hold 1]"
puts $metrics "paths_min=[lindex $hold 2]"
close $metrics
+369
View File
@@ -0,0 +1,369 @@
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// SPDX-License-Identifier: MulanPSL-2.0
// Two accumulator contexts, each 8x8 checked INT32 sums for numerical
// profile 1. Acquiring a context preloads every sum from the per-channel bias
// (sum[p][c] = bias[c]); the bias is added exactly once per context lifetime,
// so multi-K-slice reductions keep accumulating rows without a new acquire.
// Accepted rows are pipelined: stage A registers the products with a one-hot
// context token, stage B performs the checked INT32 adds one cycle later, so
// the interface sustains one row per cycle. Each context owns its add array;
// there is no shared context-selected sum read. On the first overflowing
// update (lowest sum index p*8+c wins, evaluated with a log-depth encoder)
// the sticky fault and fault_sum_o are latched, the faulting sums retain
// their stored values (no wrap, no saturation), and the context moves to
// Fault. A drain request moves an Accumulate context to DrainPending; once
// its in-flight row has landed in the sums and the single drain port is free,
// the context moves to Draining and publishes the 64 sums exactly once in
// position-major order (p = 0..7, c = 0..7 inside each position) through a
// valid/ready stream. A Fault context never drains and only clear_i releases
// it. clear_i synchronously returns every context to Idle, drops in-flight
// rows, and clears the fault record; it also blocks start/row/drain
// acceptance in the same cycle.
module npu_accumulator (
input logic clk_i,
input logic rst_n_i,
input logic clear_i,
input logic start_valid_i,
output logic start_ready_o,
input logic [npu_pkg::ChannelLanes-1:0][31:0] start_bias_i,
output logic start_context_o,
input logic row_valid_i,
output logic row_ready_o,
input logic row_context_i,
input logic [npu_pkg::SpatialLanes-1:0][npu_pkg::ChannelLanes-1:0][16:0] row_products_i,
input logic drain_valid_i,
input logic drain_context_i,
output logic drain_data_valid_o,
input logic drain_data_ready_i,
output logic signed [31:0] drain_data_o,
output logic drain_last_o,
output logic busy_o,
output logic fault_sticky_o,
output logic [5:0] fault_sum_o
);
typedef enum logic [2:0] {
Idle,
Accumulate,
DrainPending,
Draining,
Fault
} context_state_e;
localparam logic [5:0] SumLastIndex = 6'(npu_pkg::AccSumCount - 1);
logic [npu_pkg::AccContextCount-1:0][2:0] s_context_state_d;
logic [npu_pkg::AccContextCount-1:0][2:0] s_context_state_q;
logic [npu_pkg::AccContextCount-1:0][npu_pkg::AccSumCount-1:0][npu_pkg::AccWidth-1:0] s_sum_q;
// Accumulate stage A: accepted-row register with a one-hot context token.
logic [npu_pkg::AccContextCount-1:0] s_rowa_valid_d;
logic [npu_pkg::AccContextCount-1:0] s_rowa_valid_q;
logic [npu_pkg::AccSumCount-1:0][npu_pkg::ProductWidth-1:0] s_rowa_products_q;
// Accumulate stage B: per-context checked adds and overflow flags.
logic [npu_pkg::AccContextCount-1:0][npu_pkg::AccSumCount-1:0][npu_pkg::AccWidth-1:0]
s_rowb_updated;
logic [npu_pkg::AccContextCount-1:0][npu_pkg::AccSumCount-1:0] s_rowb_overflow;
logic [npu_pkg::AccContextCount-1:0] s_rowb_apply;
logic [npu_pkg::AccContextCount-1:0] s_rowb_fault_ctx;
logic [npu_pkg::AccContextCount-1:0][5:0] s_rowb_index_ctx;
logic s_rowb_fault;
logic [5:0] s_rowb_fault_index;
logic [npu_pkg::AccContextCount-1:0] s_context_idle;
logic [npu_pkg::AccContextCount-1:0] s_context_accumulating;
logic [npu_pkg::AccContextCount-1:0] s_context_draining;
logic [npu_pkg::AccContextCount-1:0] s_pending_ready;
logic [npu_pkg::AccContextCount-1:0] s_drain_grant;
logic s_prior_pending_ready;
logic s_start_accept;
logic s_row_accept;
logic s_draining_any;
logic s_drain_grant_any;
logic s_drain_context_sel;
logic s_drain_done;
logic [5:0] s_drain_index_q;
logic s_fault_sticky_q;
logic [5:0] s_fault_sum_q;
function automatic logic signed [npu_pkg::AccWidth-1:0] sum_update(
input logic signed [npu_pkg::AccWidth-1:0] sum_i,
input logic signed [npu_pkg::ProductWidth-1:0] product_i);
logic signed [npu_pkg::AccWidth-1:0] s_product_ext;
begin
s_product_ext = {
{(npu_pkg::AccWidth - npu_pkg::ProductWidth) {product_i[npu_pkg::ProductWidth-1]}},
product_i
};
return sum_i + s_product_ext;
end
endfunction
function automatic logic sum_overflows(input logic signed [npu_pkg::AccWidth-1:0] sum_i,
input logic signed [npu_pkg::ProductWidth-1:0] product_i);
logic signed [npu_pkg::AccWidth-1:0] s_product_ext;
logic signed [npu_pkg::AccWidth-1:0] s_updated;
begin
s_product_ext = {
{(npu_pkg::AccWidth - npu_pkg::ProductWidth) {product_i[npu_pkg::ProductWidth-1]}},
product_i
};
s_updated = sum_i + s_product_ext;
return (sum_i[npu_pkg::AccWidth-1] == s_product_ext[npu_pkg::AccWidth-1]) &&
(s_updated[npu_pkg::AccWidth-1] != sum_i[npu_pkg::AccWidth-1]);
end
endfunction
// Log-depth lowest-set-index encoder over the 64 per-sum overflow flags:
// each level takes the lower half whenever it contains a set bit.
function automatic logic [5:0] lowest_set_index(input logic [63:0] flags_i);
logic [31:0] s_w32;
logic [15:0] s_w16;
logic [ 7:0] s_w8;
logic [ 3:0] s_w4;
logic [ 1:0] s_w2;
logic [ 5:0] s_index;
begin
s_index[5] = ~|flags_i[31:0];
s_w32 = s_index[5] ? flags_i[63:32] : flags_i[31:0];
s_index[4] = ~|s_w32[15:0];
s_w16 = s_index[4] ? s_w32[31:16] : s_w32[15:0];
s_index[3] = ~|s_w16[7:0];
s_w8 = s_index[3] ? s_w16[15:8] : s_w16[7:0];
s_index[2] = ~|s_w8[3:0];
s_w4 = s_index[2] ? s_w8[7:4] : s_w8[3:0];
s_index[1] = ~|s_w4[1:0];
s_w2 = s_index[1] ? s_w4[3:2] : s_w4[1:0];
s_index[0] = ~s_w2[0];
return s_index;
end
endfunction
for (genvar ctx = 0; ctx < npu_pkg::AccContextCount; ctx++) begin : gen_decode
assign s_context_idle[ctx] = s_context_state_q[ctx] == Idle;
assign s_context_accumulating[ctx] = s_context_state_q[ctx] == Accumulate;
assign s_context_draining[ctx] = s_context_state_q[ctx] == Draining;
// clear_i suppresses the stage-B landing of an in-flight row.
assign s_rowb_apply[ctx] = s_rowa_valid_q[ctx] && !clear_i;
assign s_rowb_fault_ctx[ctx] = s_rowb_apply[ctx] && (|s_rowb_overflow[ctx]);
assign s_rowb_index_ctx[ctx] = lowest_set_index(s_rowb_overflow[ctx]);
// A pending drain may proceed only once the context has no in-flight row.
assign s_pending_ready[ctx] = (s_context_state_q[ctx] == DrainPending) && !s_rowa_valid_q[ctx];
end
assign busy_o = !(&s_context_idle);
assign start_ready_o = (|s_context_idle) && !clear_i;
// Stage B always retires a row in one cycle, so stage A never backpressures
// and the interface sustains one row per cycle.
assign row_ready_o = s_context_accumulating[row_context_i] && !clear_i;
assign s_start_accept = start_valid_i && start_ready_o;
assign s_row_accept = row_valid_i && row_ready_o;
assign s_draining_any = |s_context_draining;
assign s_drain_grant_any = |s_drain_grant;
assign s_drain_done = s_draining_any && drain_data_ready_i && (s_drain_index_q == SumLastIndex);
assign drain_data_valid_o = s_draining_any;
assign drain_last_o = s_draining_any && (s_drain_index_q == SumLastIndex);
assign drain_data_o = s_sum_q[s_drain_context_sel][s_drain_index_q];
assign fault_sticky_o = s_fault_sticky_q;
assign fault_sum_o = s_fault_sum_q;
// One-hot acceptance token for stage A.
always_comb begin
s_rowa_valid_d = '0;
if (s_row_accept) begin
s_rowa_valid_d[row_context_i] = 1'b1;
end
end
// Lowest Idle context index is acquired; the loop ends with the lowest hit.
always_comb begin
start_context_o = 1'b0;
for (int ctx = int'(npu_pkg::AccContextCount) - 1; ctx >= 0; ctx--) begin
if (s_context_idle[ctx]) begin
start_context_o = 1'(ctx);
end
end
end
// Pending drains are granted lowest-index first once their pipeline stage is
// empty and the single drain port is free.
always_comb begin
s_prior_pending_ready = 1'b0;
s_drain_grant = '0;
for (int ctx = 0; ctx < int'(npu_pkg::AccContextCount); ctx++) begin
s_drain_grant[ctx] = s_pending_ready[ctx] && !s_draining_any && !s_prior_pending_ready;
s_prior_pending_ready = s_prior_pending_ready || s_pending_ready[ctx];
end
end
// Selects the draining context; at most one context can be Draining because
// grants are blocked while a drain is in flight.
always_comb begin
s_drain_context_sel = 1'b0;
for (int ctx = 0; ctx < int'(npu_pkg::AccContextCount); ctx++) begin
if (s_context_draining[ctx]) begin
s_drain_context_sel = 1'(ctx);
end
end
end
// Stage-B combinational checked update: each context owns its add array, so
// no context-selected read of the sum bank exists on this path.
for (genvar ctx = 0; ctx < npu_pkg::AccContextCount; ctx++) begin : gen_stage_b
for (genvar index = 0; index < npu_pkg::AccSumCount; index++) begin : gen_sum_update
assign s_rowb_overflow[ctx][index] = sum_overflows(
s_sum_q[ctx][index], s_rowa_products_q[index]
);
assign s_rowb_updated[ctx][index] = sum_update(s_sum_q[ctx][index], s_rowa_products_q[index]);
end
end
// Lowest context index wins the fault record; the loop ends with that hit.
always_comb begin
s_rowb_fault = 1'b0;
s_rowb_fault_index = 6'd0;
for (int ctx = int'(npu_pkg::AccContextCount) - 1; ctx >= 0; ctx--) begin
if (s_rowb_fault_ctx[ctx]) begin
s_rowb_fault = 1'b1;
s_rowb_fault_index = s_rowb_index_ctx[ctx];
end
end
end
always_comb begin
s_context_state_d = s_context_state_q;
for (int ctx = 0; ctx < int'(npu_pkg::AccContextCount); ctx++) begin
unique case (s_context_state_q[ctx])
Idle: begin
if (s_start_accept && (start_context_o == 1'(ctx))) begin
s_context_state_d[ctx] = Accumulate;
end
end
Accumulate: begin
if (s_rowb_fault_ctx[ctx]) begin
s_context_state_d[ctx] = Fault;
end else if (drain_valid_i && !clear_i && (drain_context_i == 1'(ctx))) begin
s_context_state_d[ctx] = DrainPending;
end
end
DrainPending: begin
if (s_rowb_fault_ctx[ctx]) begin
s_context_state_d[ctx] = Fault;
end else if (s_drain_grant[ctx]) begin
s_context_state_d[ctx] = Draining;
end
end
Draining: begin
if (s_drain_done) begin
s_context_state_d[ctx] = Idle;
end
end
Fault: begin
end
default: begin
s_context_state_d[ctx] = Idle;
end
endcase
end
if (clear_i) begin
s_context_state_d = '0;
end
end
always_ff @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
s_context_state_q <= '0;
end else begin
s_context_state_q <= s_context_state_d;
end
end
// Stage-A row register. The products register is fully overwritten on every
// acceptance, so only the one-hot token needs reset.
always_ff @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
s_rowa_valid_q <= '0;
end else if (clear_i) begin
s_rowa_valid_q <= '0;
end else begin
s_rowa_valid_q <= s_rowa_valid_d;
end
end
always_ff @(posedge clk_i) begin
if (s_row_accept) begin
s_rowa_products_q <= row_products_i;
end
end
// On acquire every sum preloads from the per-channel bias; on a stage-B row
// each non-overflowing sum takes the checked update while an overflowing sum
// retains its stored value. Sums are not cleared by clear_i because the next
// acquire fully preloads them before any read.
for (genvar ctx = 0; ctx < npu_pkg::AccContextCount; ctx++) begin : gen_context
localparam logic ContextIndex = 1'(ctx);
for (genvar index = 0; index < npu_pkg::AccSumCount; index++) begin : gen_sum
localparam int unsigned ChannelIndex = index % npu_pkg::ChannelLanes;
always_ff @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
s_sum_q[ctx][index] <= 32'd0;
end else if (s_start_accept && (start_context_o == ContextIndex)) begin
s_sum_q[ctx][index] <= start_bias_i[ChannelIndex];
end else if (s_rowb_apply[ctx] && !s_rowb_overflow[ctx][index]) begin
s_sum_q[ctx][index] <= s_rowb_updated[ctx][index];
end
end
end
end
always_ff @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
s_drain_index_q <= 6'd0;
end else if (clear_i || s_drain_grant_any || s_drain_done) begin
s_drain_index_q <= 6'd0;
end else if (s_draining_any && drain_data_ready_i) begin
s_drain_index_q <= s_drain_index_q + 6'd1;
end
end
// First fault is retained until clear_i; a later fault in the other context
// does not overwrite the record.
always_ff @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
s_fault_sticky_q <= 1'b0;
s_fault_sum_q <= 6'd0;
end else if (clear_i) begin
s_fault_sticky_q <= 1'b0;
s_fault_sum_q <= 6'd0;
end else if (s_rowb_fault && !s_fault_sticky_q) begin
s_fault_sticky_q <= 1'b1;
s_fault_sum_q <= s_rowb_fault_index;
end
end
`ifndef SV_ASSRT_DISABLE
logic s_start_stall_q;
logic s_row_stall_q;
always_ff @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
s_start_stall_q <= 1'b0;
s_row_stall_q <= 1'b0;
end else begin
s_start_stall_q <= start_valid_i && !start_ready_o;
s_row_stall_q <= row_valid_i && !row_ready_o;
if (row_valid_i) begin
assert (int'(row_context_i) < int'(npu_pkg::AccContextCount));
end
if (s_start_stall_q) begin
assert ($stable(start_bias_i));
end
if (s_row_stall_q) begin
assert ($stable(row_context_i));
assert ($stable(row_products_i));
end
end
end
`endif
endmodule
+449
View File
@@ -0,0 +1,449 @@
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// SPDX-License-Identifier: MulanPSL-2.0
// NPU private storage: sixteen tc_sram_1024x32 banks (64 KiB) with per-word
// validity metadata and half-ownership arbitration. Frozen partition map from
// npu_pkg: raw gather banks 0-3 (two 8192-byte halves), packed A banks 4-7
// (two 8192-byte halves), packed W banks 8-11 (two 8192-byte halves), output
// staging banks 12-13 (two 4096-byte halves), descriptor/parameter banks
// 14-15 (one 8192-byte region).
//
// Contract refinements relative to the phase-1 port sketch (same semantics):
// - Half-local byte addresses are widened so they span a whole half: raw and
// packed A/W addresses are [12:0] (8192-byte halves), the output-staging
// address is [11:0] (4096-byte halves) and parameter addresses are [12:0].
// - A parameter write port (param_write_*) completes the read-only parameter
// region; without it no parameter content could ever become valid.
//
// Access semantics:
// - Every port accepts one access per cycle while its *_valid_i is asserted.
// All reads are synchronous with one-cycle latency; the returning bank is
// selected by a selector registered at request time, so read data is never
// valid in the request cycle.
// - 64-bit A/W accesses drive the two banks of one half simultaneously with
// addr[12:3] as the row; addr[2:0] must be zero (misaligned 64-bit access
// is an error). 32-bit ports ignore addr[1:0] (word select). Out-of-range
// addresses are impossible by construction: each address exactly spans its
// region, so no range error exists.
// - Each accepted write sets the per-32-bit-word valid bit of every word it
// touches (any byte strobe asserted). clear_i clears all valid bits and the
// sticky error: functional invalidation only, physical SRAM contents are
// never bulk-reset. Reads of invalid words raise the sticky error; error
// read data is don't-care.
// - Port groups touch disjoint banks except raw_write vs raw_read (raw banks)
// and pack_write vs a_read/w_read (A/W banks). Concurrent same-half access
// is a scheduler programming error: the sticky error is raised and the read
// port wins the bank, dropping the write. Different-half concurrent access
// (read the active half, write the inactive half) is legal. The parameter
// write and read ports arbitrate the same way (read wins) without flagging.
// - access_err_port_o encodes the first failing port, sticky until clear_i:
// 3'd0 raw_write conflict, 3'd1 raw_read, 3'd2 pack_write, 3'd3 a_read,
// 3'd4 w_read, 3'd5 out_req, 3'd6 param_read; 3'd7 means no error latched.
module npu_local_sram (
input logic clk_i,
input logic rst_n_i,
input logic clear_i,
// raw gather: gather/DMA write port + packer read port (banks 0-3)
input logic raw_write_valid_i,
input logic raw_write_half_i,
input logic [12:0] raw_write_addr_i,
input logic [31:0] raw_write_data_i,
input logic [ 3:0] raw_write_strb_i,
input logic raw_read_valid_i,
input logic raw_read_half_i,
input logic [12:0] raw_read_addr_i,
output logic [31:0] raw_read_data_o,
// packed A/W: packer 64-bit write port + compute 64-bit read ports
input logic pack_write_valid_i,
input logic pack_write_sel_w_i,
input logic pack_write_half_i,
input logic [12:0] pack_write_addr_i,
input logic [63:0] pack_write_data_i,
input logic [ 7:0] pack_write_strb_i,
input logic a_read_valid_i,
input logic a_read_half_i,
input logic [12:0] a_read_addr_i,
output logic [63:0] a_read_data_o,
input logic w_read_valid_i,
input logic w_read_half_i,
input logic [12:0] w_read_addr_i,
output logic [63:0] w_read_data_o,
// output staging: single port (banks 12-13, halves of 4096 bytes)
input logic out_req_valid_i,
input logic out_req_write_i,
input logic out_req_half_i,
input logic [11:0] out_req_addr_i,
input logic [31:0] out_req_data_i,
input logic [ 3:0] out_req_strb_i,
output logic [31:0] out_read_data_o,
// parameter region: scheduler write port + compute read port (banks 14-15)
input logic param_write_valid_i,
input logic [12:0] param_write_addr_i,
input logic [31:0] param_write_data_i,
input logic [ 3:0] param_write_strb_i,
input logic param_read_valid_i,
input logic [12:0] param_read_addr_i,
output logic [31:0] param_read_data_o,
// validity/ownership errors
output logic access_err_sticky_o,
output logic [ 2:0] access_err_port_o
);
localparam int unsigned BankCount = npu_pkg::BankCount;
localparam int unsigned BankWords = 2 ** npu_pkg::BankAddrWidth;
localparam logic [2:0] ErrPortRawWrite = 3'd0;
localparam logic [2:0] ErrPortRawRead = 3'd1;
localparam logic [2:0] ErrPortPackWrite = 3'd2;
localparam logic [2:0] ErrPortARead = 3'd3;
localparam logic [2:0] ErrPortWRead = 3'd4;
localparam logic [2:0] ErrPortOutReq = 3'd5;
localparam logic [2:0] ErrPortParamRead = 3'd6;
localparam logic [2:0] ErrPortNone = 3'd7;
logic [BankCount-1:0] s_bank_cs;
logic [BankCount-1:0] s_bank_wren;
logic [BankCount-1:0][ 9:0] s_bank_addr;
logic [BankCount-1:0][ 31:0] s_bank_wdata;
logic [BankCount-1:0][ 3:0] s_bank_wmask;
logic [BankCount-1:0][ 31:0] s_bank_rdata;
logic [BankCount-1:0][BankWords-1:0] s_valid_q;
logic [ 1:0] s_raw_write_bank;
logic [ 9:0] s_raw_write_row;
logic [ 1:0] s_raw_read_bank;
logic [ 9:0] s_raw_read_row;
logic s_raw_conflict;
logic s_raw_write_en;
logic [ 9:0] s_pack_write_row;
logic [ 9:0] s_a_read_row;
logic [ 9:0] s_w_read_row;
logic [ 3:0] s_pack_a_bank_lo;
logic [ 3:0] s_pack_w_bank_lo;
logic [ 3:0] s_a_read_bank_lo;
logic [ 3:0] s_w_read_bank_lo;
logic s_pack_write_misaligned;
logic s_a_read_misaligned;
logic s_w_read_misaligned;
logic s_a_conflict;
logic s_w_conflict;
logic s_pack_a_en;
logic s_pack_w_en;
logic [ 3:0] s_out_bank;
logic [ 3:0] s_param_read_bank;
logic [ 3:0] s_param_write_bank;
logic [ 1:0] s_raw_read_bank_q;
logic s_a_read_half_q;
logic s_w_read_half_q;
logic s_out_read_half_q;
logic s_param_read_bank_q;
logic [ 3:0] s_a_read_bank_lo_q;
logic [ 3:0] s_w_read_bank_lo_q;
logic s_err_raw_write;
logic s_err_raw_read;
logic s_err_pack_write;
logic s_err_a_read;
logic s_err_w_read;
logic s_err_out;
logic s_err_param;
logic s_access_err;
logic [ 2:0] s_access_err_port;
logic s_access_err_sticky_q;
logic [ 2:0] s_access_err_port_q;
logic s_unused;
assign s_raw_write_bank = {raw_write_half_i, raw_write_addr_i[12]};
assign s_raw_write_row = raw_write_addr_i[11:2];
assign s_raw_read_bank = {raw_read_half_i, raw_read_addr_i[12]};
assign s_raw_read_row = raw_read_addr_i[11:2];
assign s_raw_conflict = raw_write_valid_i && raw_read_valid_i &&
(raw_write_half_i == raw_read_half_i);
assign s_raw_write_en = raw_write_valid_i && !s_raw_conflict;
assign s_pack_write_row = pack_write_addr_i[12:3];
assign s_a_read_row = a_read_addr_i[12:3];
assign s_w_read_row = w_read_addr_i[12:3];
assign s_pack_a_bank_lo = 4'(npu_pkg::PackABankFirst) + {pack_write_half_i, 1'b0};
assign s_pack_w_bank_lo = 4'(npu_pkg::PackWBankFirst) + {pack_write_half_i, 1'b0};
assign s_a_read_bank_lo = 4'(npu_pkg::PackABankFirst) + {a_read_half_i, 1'b0};
assign s_w_read_bank_lo = 4'(npu_pkg::PackWBankFirst) + {w_read_half_i, 1'b0};
assign s_pack_write_misaligned = pack_write_addr_i[2:0] != 3'd0;
assign s_a_read_misaligned = a_read_addr_i[2:0] != 3'd0;
assign s_w_read_misaligned = w_read_addr_i[2:0] != 3'd0;
assign s_a_conflict = pack_write_valid_i && !pack_write_sel_w_i && a_read_valid_i &&
(pack_write_half_i == a_read_half_i);
assign s_w_conflict = pack_write_valid_i && pack_write_sel_w_i && w_read_valid_i &&
(pack_write_half_i == w_read_half_i);
assign s_pack_a_en = pack_write_valid_i && !pack_write_sel_w_i && !s_a_conflict &&
!s_pack_write_misaligned;
assign s_pack_w_en = pack_write_valid_i && pack_write_sel_w_i && !s_w_conflict &&
!s_pack_write_misaligned;
assign s_out_bank = 4'(npu_pkg::OutBankFirst) + {3'd0, out_req_half_i};
assign s_param_read_bank = 4'(npu_pkg::ParamBankFirst) + {3'd0, param_read_addr_i[12]};
assign s_param_write_bank = 4'(npu_pkg::ParamBankFirst) + {3'd0, param_write_addr_i[12]};
// Raw gather banks 0-3: the read port wins any bank-level clash; a same-half
// concurrent write/read pair is a programming error that drops the write.
for (genvar bank = 0; bank < 4; bank++) begin : gen_raw_bank
localparam logic [1:0] BankIndex = 2'(bank);
logic s_raw_read_sel;
logic s_raw_write_sel;
assign s_raw_read_sel = raw_read_valid_i && (s_raw_read_bank == BankIndex);
assign s_raw_write_sel = s_raw_write_en && (s_raw_write_bank == BankIndex);
assign s_bank_cs[npu_pkg::RawBankFirst+bank] = s_raw_read_sel || s_raw_write_sel;
assign s_bank_wren[npu_pkg::RawBankFirst+bank] = s_raw_write_sel && !s_raw_read_sel;
assign s_bank_addr[npu_pkg::RawBankFirst+bank] =
s_raw_read_sel ? s_raw_read_row : s_raw_write_row;
assign s_bank_wdata[npu_pkg::RawBankFirst+bank] = raw_write_data_i;
assign s_bank_wmask[npu_pkg::RawBankFirst+bank] = raw_write_strb_i;
end
// Packed A banks 4-7 and packed W banks 8-11: two banks per half, low word
// in the even bank, high word in the odd bank; compute reads win conflicts.
for (genvar half = 0; half < 2; half++) begin : gen_pack_a_bank
localparam int unsigned BankLow = npu_pkg::PackABankFirst + 2 * half;
localparam logic HalfValue = 1'(half);
logic s_a_read_sel;
logic s_pack_a_sel;
assign s_a_read_sel = a_read_valid_i && (a_read_half_i == HalfValue);
assign s_pack_a_sel = s_pack_a_en && (pack_write_half_i == HalfValue);
assign s_bank_cs[BankLow] = s_a_read_sel || s_pack_a_sel;
assign s_bank_wren[BankLow] = s_pack_a_sel && !s_a_read_sel;
assign s_bank_addr[BankLow] = s_a_read_sel ? s_a_read_row : s_pack_write_row;
assign s_bank_wdata[BankLow] = pack_write_data_i[31:0];
assign s_bank_wmask[BankLow] = pack_write_strb_i[3:0];
assign s_bank_cs[BankLow+1] = s_a_read_sel || s_pack_a_sel;
assign s_bank_wren[BankLow+1] = s_pack_a_sel && !s_a_read_sel;
assign s_bank_addr[BankLow+1] = s_a_read_sel ? s_a_read_row : s_pack_write_row;
assign s_bank_wdata[BankLow+1] = pack_write_data_i[63:32];
assign s_bank_wmask[BankLow+1] = pack_write_strb_i[7:4];
end
for (genvar half = 0; half < 2; half++) begin : gen_pack_w_bank
localparam int unsigned BankLow = npu_pkg::PackWBankFirst + 2 * half;
localparam logic HalfValue = 1'(half);
logic s_w_read_sel;
logic s_pack_w_sel;
assign s_w_read_sel = w_read_valid_i && (w_read_half_i == HalfValue);
assign s_pack_w_sel = s_pack_w_en && (pack_write_half_i == HalfValue);
assign s_bank_cs[BankLow] = s_w_read_sel || s_pack_w_sel;
assign s_bank_wren[BankLow] = s_pack_w_sel && !s_w_read_sel;
assign s_bank_addr[BankLow] = s_w_read_sel ? s_w_read_row : s_pack_write_row;
assign s_bank_wdata[BankLow] = pack_write_data_i[31:0];
assign s_bank_wmask[BankLow] = pack_write_strb_i[3:0];
assign s_bank_cs[BankLow+1] = s_w_read_sel || s_pack_w_sel;
assign s_bank_wren[BankLow+1] = s_pack_w_sel && !s_w_read_sel;
assign s_bank_addr[BankLow+1] = s_w_read_sel ? s_w_read_row : s_pack_write_row;
assign s_bank_wdata[BankLow+1] = pack_write_data_i[63:32];
assign s_bank_wmask[BankLow+1] = pack_write_strb_i[7:4];
end
// Output staging banks 12-13: one request port, one bank per half.
for (genvar half = 0; half < 2; half++) begin : gen_out_bank
localparam int unsigned BankIndex = npu_pkg::OutBankFirst + half;
localparam logic HalfValue = 1'(half);
logic s_out_sel;
assign s_out_sel = out_req_valid_i && (out_req_half_i == HalfValue);
assign s_bank_cs[BankIndex] = s_out_sel;
assign s_bank_wren[BankIndex] = s_out_sel && out_req_write_i;
assign s_bank_addr[BankIndex] = out_req_addr_i[11:2];
assign s_bank_wdata[BankIndex] = out_req_data_i;
assign s_bank_wmask[BankIndex] = out_req_strb_i;
end
// Parameter banks 14-15: independent write and read ports; the read port
// wins a same-bank clash without flagging an error.
for (genvar bank = 0; bank < 2; bank++) begin : gen_param_bank
localparam int unsigned BankIndex = npu_pkg::ParamBankFirst + bank;
localparam logic BankValue = 1'(bank);
logic s_param_read_sel;
logic s_param_write_sel;
assign s_param_read_sel = param_read_valid_i && (param_read_addr_i[12] == BankValue);
assign s_param_write_sel = param_write_valid_i && (param_write_addr_i[12] == BankValue);
assign s_bank_cs[BankIndex] = s_param_read_sel || s_param_write_sel;
assign s_bank_wren[BankIndex] = s_param_write_sel && !s_param_read_sel;
assign s_bank_addr[BankIndex] =
s_param_read_sel ? param_read_addr_i[11:2] : param_write_addr_i[11:2];
assign s_bank_wdata[BankIndex] = param_write_data_i;
assign s_bank_wmask[BankIndex] = param_write_strb_i;
end
`ifdef HAVE_SRAM_MACRO
for (genvar bank = 0; bank < BankCount; bank++) begin : gen_local_bank
tc_sram_1024x32 u_local_sram (
.clk_i (clk_i),
.cs_i (s_bank_cs[bank]),
.addr_i(s_bank_addr[bank]),
.data_i(s_bank_wdata[bank]),
.mask_i(s_bank_wmask[bank]),
.wren_i(s_bank_wren[bank]),
.data_o(s_bank_rdata[bank])
);
end
`else
// Inferred behavioral banks for portable unit simulations (compile with
// +define+PDK_BEHAV), mirroring the tc_sram_1024x32 wrapper: synchronous
// read, byte write masks, no reset.
for (genvar bank = 0; bank < BankCount; bank++) begin : gen_local_bank
logic [31:0] s_storage_q[0:BankWords-1];
logic [31:0] s_data_q;
assign s_bank_rdata[bank] = s_data_q;
always_ff @(posedge clk_i) begin
if (s_bank_cs[bank]) begin
if (!s_bank_wren[bank]) begin
s_data_q <= s_storage_q[s_bank_addr[bank]];
end else begin
if (s_bank_wmask[bank][0]) begin
s_storage_q[s_bank_addr[bank]][7:0] <= s_bank_wdata[bank][7:0];
end
if (s_bank_wmask[bank][1]) begin
s_storage_q[s_bank_addr[bank]][15:8] <= s_bank_wdata[bank][15:8];
end
if (s_bank_wmask[bank][2]) begin
s_storage_q[s_bank_addr[bank]][23:16] <= s_bank_wdata[bank][23:16];
end
if (s_bank_wmask[bank][3]) begin
s_storage_q[s_bank_addr[bank]][31:24] <= s_bank_wdata[bank][31:24];
end
s_data_q <= 32'bx;
end
end
end
end
`endif
// Per-word validity metadata: set by accepted writes, functionally cleared
// by clear_i (or reset); physical SRAM contents are never bulk-reset.
always_ff @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
s_valid_q <= '0;
end else if (clear_i) begin
s_valid_q <= '0;
end else begin
if (s_raw_write_en && (raw_write_strb_i != 4'd0)) begin
s_valid_q[s_raw_write_bank][s_raw_write_row] <= 1'b1;
end
if (s_pack_a_en) begin
if (pack_write_strb_i[3:0] != 4'd0) begin
s_valid_q[s_pack_a_bank_lo][s_pack_write_row] <= 1'b1;
end
if (pack_write_strb_i[7:4] != 4'd0) begin
s_valid_q[s_pack_a_bank_lo+4'd1][s_pack_write_row] <= 1'b1;
end
end
if (s_pack_w_en) begin
if (pack_write_strb_i[3:0] != 4'd0) begin
s_valid_q[s_pack_w_bank_lo][s_pack_write_row] <= 1'b1;
end
if (pack_write_strb_i[7:4] != 4'd0) begin
s_valid_q[s_pack_w_bank_lo+4'd1][s_pack_write_row] <= 1'b1;
end
end
if (out_req_valid_i && out_req_write_i && (out_req_strb_i != 4'd0)) begin
s_valid_q[s_out_bank][out_req_addr_i[11:2]] <= 1'b1;
end
if (param_write_valid_i && (param_write_strb_i != 4'd0) &&
!(param_read_valid_i && (param_read_addr_i[12] == param_write_addr_i[12]))) begin
s_valid_q[s_param_write_bank][param_write_addr_i[11:2]] <= 1'b1;
end
end
end
// Registered read-return selectors (one-cycle synchronous read latency).
always_ff @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
s_raw_read_bank_q <= 2'd0;
s_a_read_half_q <= 1'b0;
s_w_read_half_q <= 1'b0;
s_out_read_half_q <= 1'b0;
s_param_read_bank_q <= 1'b0;
end else begin
if (raw_read_valid_i) begin
s_raw_read_bank_q <= s_raw_read_bank;
end
if (a_read_valid_i) begin
s_a_read_half_q <= a_read_half_i;
end
if (w_read_valid_i) begin
s_w_read_half_q <= w_read_half_i;
end
if (out_req_valid_i && !out_req_write_i) begin
s_out_read_half_q <= out_req_half_i;
end
if (param_read_valid_i) begin
s_param_read_bank_q <= param_read_addr_i[12];
end
end
end
assign s_a_read_bank_lo_q = 4'(npu_pkg::PackABankFirst) + {s_a_read_half_q, 1'b0};
assign s_w_read_bank_lo_q = 4'(npu_pkg::PackWBankFirst) + {s_w_read_half_q, 1'b0};
assign raw_read_data_o = s_bank_rdata[{2'b00, s_raw_read_bank_q}];
assign a_read_data_o = {s_bank_rdata[s_a_read_bank_lo_q+4'd1], s_bank_rdata[s_a_read_bank_lo_q]};
assign w_read_data_o = {s_bank_rdata[s_w_read_bank_lo_q+4'd1], s_bank_rdata[s_w_read_bank_lo_q]};
assign out_read_data_o = s_bank_rdata[4'(npu_pkg::OutBankFirst)+{3'd0, s_out_read_half_q}];
assign param_read_data_o = s_bank_rdata[4'(npu_pkg::ParamBankFirst)+{3'd0, s_param_read_bank_q}];
assign s_err_raw_write = s_raw_conflict;
assign s_err_raw_read = raw_read_valid_i && !s_valid_q[s_raw_read_bank][s_raw_read_row];
assign s_err_pack_write = pack_write_valid_i &&
(s_pack_write_misaligned || s_a_conflict || s_w_conflict);
assign s_err_a_read = a_read_valid_i &&
(s_a_read_misaligned || !s_valid_q[s_a_read_bank_lo][s_a_read_row] ||
!s_valid_q[s_a_read_bank_lo+4'd1][s_a_read_row]);
assign s_err_w_read = w_read_valid_i &&
(s_w_read_misaligned || !s_valid_q[s_w_read_bank_lo][s_w_read_row] ||
!s_valid_q[s_w_read_bank_lo+4'd1][s_w_read_row]);
assign s_err_out = out_req_valid_i && !out_req_write_i &&
!s_valid_q[s_out_bank][out_req_addr_i[11:2]];
assign s_err_param = param_read_valid_i && !s_valid_q[s_param_read_bank][param_read_addr_i[11:2]];
// First-failing-port priority encode; lowest port code wins within a cycle.
always_comb begin
s_access_err = 1'b1;
if (s_err_raw_write) begin
s_access_err_port = ErrPortRawWrite;
end else if (s_err_raw_read) begin
s_access_err_port = ErrPortRawRead;
end else if (s_err_pack_write) begin
s_access_err_port = ErrPortPackWrite;
end else if (s_err_a_read) begin
s_access_err_port = ErrPortARead;
end else if (s_err_w_read) begin
s_access_err_port = ErrPortWRead;
end else if (s_err_out) begin
s_access_err_port = ErrPortOutReq;
end else if (s_err_param) begin
s_access_err_port = ErrPortParamRead;
end else begin
s_access_err = 1'b0;
s_access_err_port = ErrPortNone;
end
end
always_ff @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
s_access_err_sticky_q <= 1'b0;
s_access_err_port_q <= ErrPortNone;
end else if (clear_i) begin
s_access_err_sticky_q <= 1'b0;
s_access_err_port_q <= ErrPortNone;
end else if (s_access_err && !s_access_err_sticky_q) begin
s_access_err_sticky_q <= 1'b1;
s_access_err_port_q <= s_access_err_port;
end
end
assign access_err_sticky_o = s_access_err_sticky_q;
assign access_err_port_o = s_access_err_port_q;
assign s_unused = ^{raw_write_addr_i[1:0], raw_read_addr_i[1:0], out_req_addr_i[1:0],
param_write_addr_i[1:0], param_read_addr_i[1:0]} && 1'b0;
endmodule
+106
View File
@@ -0,0 +1,106 @@
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// SPDX-License-Identifier: MulanPSL-2.0
// Dense numerical-profile-1 MAC row: 8 output positions x 8 output channels.
// One request presents eight raw INT8 activation bytes, eight raw INT8 weight
// bytes, the signed input zero point, and per-lane valid masks. An accepted
// request centers each activation to signed nine bits,
// a9 = sign_extend(byte) - sign_extend(zero point), range -255..255, and
// registers the 64 signed 9x8 products one cycle later. Masked lanes produce
// exactly zero and never raise arithmetic faults. Weight bytes are consumed
// directly as signed INT8 (zero point 0). The single registered output stage
// applies backpressure with req_ready_o = resp_ready_i || !resp_valid_o.
module npu_mac_array (
input logic clk_i,
input logic rst_n_i,
input logic req_valid_i,
output logic req_ready_o,
input logic [npu_pkg::SpatialLanes-1:0][ 7:0] req_a_bytes_i,
input logic [npu_pkg::ChannelLanes-1:0][ 7:0] req_w_bytes_i,
input logic signed [ 7:0] req_in_zero_i,
input logic [npu_pkg::SpatialLanes-1:0] req_a_valid_i,
input logic [npu_pkg::ChannelLanes-1:0] req_w_valid_i,
output logic resp_valid_o,
input logic resp_ready_i,
output logic [npu_pkg::SpatialLanes-1:0][npu_pkg::ChannelLanes-1:0][16:0] resp_products_o
);
logic [npu_pkg::SpatialLanes-1:0][npu_pkg::ChannelLanes-1:0][npu_pkg::ProductWidth-1:0]
s_products_d;
logic [npu_pkg::SpatialLanes-1:0][npu_pkg::ChannelLanes-1:0][npu_pkg::ProductWidth-1:0]
s_products_q;
logic s_resp_valid_q;
function automatic logic signed [npu_pkg::ProductWidth-1:0] lane_product(
input logic [7:0] a_byte_i, input logic [7:0] w_byte_i, input logic signed [7:0] in_zero_i,
input logic lanes_valid_i);
logic signed [npu_pkg::CenteredWidth-1:0] s_a_ext;
logic signed [npu_pkg::CenteredWidth-1:0] s_zero_ext;
logic signed [npu_pkg::CenteredWidth-1:0] s_centered;
logic signed [ npu_pkg::ProductWidth-1:0] s_product;
begin
s_a_ext = {a_byte_i[7], a_byte_i};
s_zero_ext = {in_zero_i[7], in_zero_i};
s_centered = s_a_ext - s_zero_ext;
s_product = s_centered * $signed(w_byte_i);
return lanes_valid_i ? s_product : '0;
end
endfunction
for (genvar position = 0; position < npu_pkg::SpatialLanes; position++) begin : gen_position
for (genvar channel = 0; channel < npu_pkg::ChannelLanes; channel++) begin : gen_channel
assign s_products_d[position][channel] = lane_product(
req_a_bytes_i[position],
req_w_bytes_i[channel],
req_in_zero_i,
req_a_valid_i[position] && req_w_valid_i[channel]
);
end
end
assign req_ready_o = resp_ready_i || !s_resp_valid_q;
assign resp_valid_o = s_resp_valid_q;
assign resp_products_o = s_products_q;
// The product register is fully overwritten on every acceptance, so only the
// handshake state needs reset.
always_ff @(posedge clk_i) begin
if (req_valid_i && req_ready_o) begin
s_products_q <= s_products_d;
end
end
always_ff @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
s_resp_valid_q <= 1'b0;
end else if (req_valid_i && req_ready_o) begin
s_resp_valid_q <= 1'b1;
end else if (resp_ready_i) begin
s_resp_valid_q <= 1'b0;
end
end
`ifndef SV_ASSRT_DISABLE
logic s_req_stall_q;
logic s_resp_stall_q;
always_ff @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
s_req_stall_q <= 1'b0;
s_resp_stall_q <= 1'b0;
end else begin
s_req_stall_q <= req_valid_i && !req_ready_o;
s_resp_stall_q <= resp_valid_o && !resp_ready_i;
if (s_req_stall_q) begin
assert ($stable(req_a_bytes_i));
assert ($stable(req_w_bytes_i));
assert ($stable(req_in_zero_i));
assert ($stable(req_a_valid_i));
assert ($stable(req_w_valid_i));
end
if (s_resp_stall_q) begin
assert ($stable(resp_products_o));
end
end
end
`endif
endmodule
+186
View File
@@ -0,0 +1,186 @@
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// SPDX-License-Identifier: MulanPSL-2.0
// NPU patch packer: bounded NHWC gather transpose from a raw gather half into
// packed A rows. Dense (PackDense): packed row k (byte address k*8) holds
// byte lane p = raw_byte(p*K + k) for p < M and the input zero point above;
// raw layout is [spatial_lane][k_in_slice]. Depthwise (PackDepthwise): packed
// row (s*K + k) holds lane c = raw_byte((s*K + k)*8 + c) for c < channels and
// the input zero point above; raw layout is [spatial_lane][kernel_k]
// [channel_lane] and K <= 9. Contract refinement: the raw read and packed
// write addresses are [12:0] so they span an 8192-byte half.
//
// Serial gather: one raw 32-bit read per produced byte, pipelined one byte
// per cycle after the one-cycle synchronous-read fill. A packed 64-bit row
// retires every (lane_count + 2) cycles, i.e. 0.8 bytes/cycle at M = 8.
module npu_patch_packer (
input logic clk_i,
input logic rst_n_i,
input logic start_valid_i,
output logic start_ready_o,
input npu_pkg::pack_mode_e start_mode_i,
input logic [10:0] start_k_i, // dense 1..1024, depthwise 1..9
input logic [ 3:0] start_positions_i, // dense: M output positions 1..8
input logic [ 3:0] start_channels_i, // depthwise: valid channel lanes 1..8
input logic signed [ 7:0] start_in_zero_i,
output logic busy_o,
output logic done_pulse_o,
output logic raw_read_valid_o,
output logic [12:0] raw_read_addr_o,
input logic [31:0] raw_read_data_i,
output logic pack_write_valid_o,
output logic [12:0] pack_write_addr_o,
output logic [63:0] pack_write_data_o,
output logic [ 7:0] pack_write_strb_o
);
typedef enum logic [1:0] {
Idle,
Gather,
Write,
Done
} state_e;
state_e s_state_d;
state_e s_state_q;
npu_pkg::pack_mode_e s_mode_q;
logic [10:0] s_k_q;
logic [ 3:0] s_positions_q;
logic [ 3:0] s_channels_q;
logic signed [ 7:0] s_in_zero_q;
logic [10:0] s_row_cnt_q;
logic [ 3:0] s_issue_lane_q;
logic [ 3:0] s_capture_cnt_q;
logic [ 2:0] s_capture_lane_q;
logic [ 1:0] s_capture_sel_q;
logic s_inflight_q;
logic [12:0] s_lane_base_q;
logic [63:0] s_row_q;
logic [ 3:0] s_lane_count;
logic [10:0] s_row_total;
logic [12:0] s_issue_addr;
logic s_issue_fire;
logic s_last_capture;
logic s_row_start;
// Valid lanes per row: M dense positions, channel lanes depthwise. Row
// count: K dense rows, M*K depthwise rows (K <= 9, so the product fits).
assign s_lane_count = (s_mode_q == npu_pkg::PackDense) ? s_positions_q : s_channels_q;
assign s_row_total = (s_mode_q == npu_pkg::PackDense) ? s_k_q :
11'(8'(s_positions_q) * 8'(s_k_q[3:0]));
// Dense byte address lane*K + row (lane base accumulated per issued lane);
// depthwise byte address row*8 + lane ([spatial][kernel_k][channel] order).
assign s_issue_addr = (s_mode_q == npu_pkg::PackDense) ?
s_lane_base_q + {2'b00, s_row_cnt_q} :
{s_row_cnt_q[9:0], 3'b000} + {10'd0, s_issue_lane_q[2:0]};
assign s_issue_fire = (s_state_q == Gather) && (s_issue_lane_q < s_lane_count);
assign s_last_capture = (s_state_q == Gather) && s_inflight_q &&
(s_capture_cnt_q == (s_lane_count - 4'd1));
always_comb begin
s_state_d = s_state_q;
s_row_start = 1'b0;
unique case (s_state_q)
Idle: begin
if (start_valid_i) begin
s_state_d = Gather;
s_row_start = 1'b1;
end
end
Gather: begin
if (s_last_capture) begin
s_state_d = Write;
end
end
Write: begin
if (s_row_cnt_q == (s_row_total - 11'd1)) begin
s_state_d = Done;
end else begin
s_state_d = Gather;
s_row_start = 1'b1;
end
end
Done: begin
s_state_d = Idle;
end
default: begin
s_state_d = Idle;
end
endcase
end
assign start_ready_o = (s_state_q == Idle);
assign busy_o = (s_state_q != Idle);
assign done_pulse_o = (s_state_q == Done);
assign raw_read_valid_o = s_issue_fire;
assign raw_read_addr_o = s_issue_addr;
assign pack_write_valid_o = (s_state_q == Write);
assign pack_write_addr_o = {s_row_cnt_q[9:0], 3'b000};
assign pack_write_data_o = s_row_q;
assign pack_write_strb_o = 8'hff;
always_ff @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
s_state_q <= Idle;
s_mode_q <= npu_pkg::PackDense;
s_k_q <= 11'd1;
s_positions_q <= 4'd1;
s_channels_q <= 4'd1;
s_in_zero_q <= 8'd0;
s_row_cnt_q <= 11'd0;
s_issue_lane_q <= 4'd0;
s_capture_cnt_q <= 4'd0;
s_capture_lane_q <= 3'd0;
s_capture_sel_q <= 2'd0;
s_inflight_q <= 1'b0;
s_lane_base_q <= 13'd0;
s_row_q <= 64'd0;
end else begin
s_state_q <= s_state_d;
s_inflight_q <= s_issue_fire;
if ((s_state_q == Idle) && start_valid_i) begin
s_mode_q <= start_mode_i;
s_k_q <= start_k_i;
s_positions_q <= start_positions_i;
s_channels_q <= start_channels_i;
s_in_zero_q <= start_in_zero_i;
s_row_cnt_q <= 11'd0;
end
if (s_row_start) begin
s_issue_lane_q <= 4'd0;
s_capture_cnt_q <= 4'd0;
s_lane_base_q <= 13'd0;
// Invalid lanes keep the input zero point; valid lanes are overwritten
// by captures.
s_row_q <= {8{(s_state_q == Idle) ? start_in_zero_i : s_in_zero_q}};
end
if (s_issue_fire) begin
s_issue_lane_q <= s_issue_lane_q + 4'd1;
s_capture_lane_q <= s_issue_lane_q[2:0];
s_capture_sel_q <= s_issue_addr[1:0];
if (s_mode_q == npu_pkg::PackDense) begin
s_lane_base_q <= s_lane_base_q + {2'b00, s_k_q};
end
end
if (s_inflight_q && (s_state_q == Gather)) begin
s_row_q[s_capture_lane_q*8+:8] <= raw_read_data_i[s_capture_sel_q*8+:8];
s_capture_cnt_q <= s_capture_cnt_q + 4'd1;
end
if ((s_state_q == Write) && (s_state_d == Gather)) begin
s_row_cnt_q <= s_row_cnt_q + 11'd1;
end
end
end
`ifndef SV_ASSRT_DISABLE
always_ff @(posedge clk_i) begin
if (rst_n_i && (s_state_q == Idle) && start_valid_i) begin
assert ((start_k_i >= 11'd1) && (start_k_i <= 11'(npu_pkg::MaxKSlice)));
assert ((start_positions_i >= 4'd1) && (start_positions_i <= 4'd8));
assert ((start_channels_i >= 4'd1) && (start_channels_i <= 4'd8));
if (start_mode_i == npu_pkg::PackDepthwise) begin
assert (start_k_i <= 11'd9);
end
end
end
`endif
endmodule
+44
View File
@@ -0,0 +1,44 @@
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// SPDX-License-Identifier: MulanPSL-2.0
package npu_pkg;
// Frozen compute geometry: 8 output positions x 8 output channels dense,
// eight depthwise lanes, one pipelined scalar requantizer.
localparam int unsigned SpatialLanes = 8;
localparam int unsigned ChannelLanes = 8;
localparam int unsigned DenseMacCount = SpatialLanes * ChannelLanes;
localparam int unsigned DepthwiseLanes = 8;
localparam int unsigned MaxTilePositions = 8;
localparam int unsigned MaxKSlice = 1024;
// Sixteen 1024x32 banks = 64 KiB private storage, frozen partition map:
// raw gather 0x0000..0x3fff, packed A 0x4000..0x7fff, packed W 0x8000..0xbfff,
// output staging 0xc000..0xdfff, descriptor/parameter 0xe000..0xffff.
localparam int unsigned BankCount = 16;
localparam int unsigned BankAddrWidth = 10;
localparam int unsigned BankBytes = 4096;
localparam int unsigned LocalBytes = BankCount * BankBytes;
localparam int unsigned RawBankFirst = 0;
localparam int unsigned PackABankFirst = 4;
localparam int unsigned PackWBankFirst = 8;
localparam int unsigned OutBankFirst = 12;
localparam int unsigned ParamBankFirst = 14;
// Two accumulator contexts, each 8x8 checked INT32 sums.
localparam int unsigned AccContextCount = 2;
localparam int unsigned AccSumCount = DenseMacCount;
// Numerical profile 1 widths: centered signed nine-bit activations,
// signed eight-bit weights, signed 9x8 products, checked INT32 sums.
localparam int unsigned CenteredWidth = 9;
localparam int unsigned WeightWidth = 8;
localparam int unsigned AccWidth = 32;
localparam int unsigned ProductWidth = 17;
typedef logic signed [CenteredWidth-1:0] centered_t;
typedef logic signed [WeightWidth-1:0] weight_t;
typedef logic signed [AccWidth-1:0] acc_t;
typedef logic signed [ProductWidth-1:0] product_t;
typedef enum logic [0:0] {
PackDense,
PackDepthwise
} pack_mode_e;
endpackage
+287
View File
@@ -0,0 +1,287 @@
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// SPDX-License-Identifier: MulanPSL-2.0
// Scalar pipelined numerical-profile-1 requantizer, bit-exact with
// scripts/npu_reference.py. Each item applies, in this exact order: checked
// left shift by max(shift,0) (an out-of-INT32 result flags the item, data
// 8'sd0, never wrapped), SaturatingRoundingDoublingHighMul with the
// multiplier (2^30 nudge for p >= 0, 1-2^30 for p < 0, division by 2^31
// truncating toward zero, INT32_MIN-times-INT32_MIN saturation), then
// RoundingDivideByPOT by max(-shift,0), then a widened add of the output zero
// point and a clamp to [act_min, act_max]. This is the pinned double-rounding
// profile; no single-rounding substitution is made. Five elastic valid/ready
// register stages (shift, 32x32 multiply, nudge+truncating divide,
// RoundingDivideByPOT, zero-point add+clamp) carry the per-item
// configuration and fault flag; req_ready_o is high only when stage 0 is
// free or shifting out.
module npu_requantizer (
input logic clk_i,
input logic rst_n_i,
input logic req_valid_i,
output logic req_ready_o,
input logic signed [31:0] req_acc_i,
input logic [31:0] req_multiplier_i,
input logic signed [ 7:0] req_shift_i,
input logic signed [ 7:0] req_zout_i,
input logic signed [ 7:0] req_act_min_i,
input logic signed [ 7:0] req_act_max_i,
output logic resp_valid_o,
input logic resp_ready_i,
output logic signed [ 7:0] resp_data_o,
output logic resp_fault_o
);
// Stage 0 (checked left shift) registers and combinational results.
logic s_shift_valid_q;
logic signed [31:0] s_shift_data_q;
logic s_shift_fault_q;
logic [31:0] s_shift_multiplier_q;
logic signed [ 7:0] s_shift_shift_q;
logic signed [ 7:0] s_shift_zout_q;
logic signed [ 7:0] s_shift_act_min_q;
logic signed [ 7:0] s_shift_act_max_q;
logic signed [31:0] s_shift_data_d;
logic s_shift_fault_d;
logic signed [63:0] s_shifted_wide;
// Stage 1 (32x32 to 64-bit signed multiply) registers and result.
logic s_mul_valid_q;
logic signed [63:0] s_mul_product_q;
logic s_mul_fault_q;
logic signed [ 7:0] s_mul_shift_q;
logic signed [ 7:0] s_mul_zout_q;
logic signed [ 7:0] s_mul_act_min_q;
logic signed [ 7:0] s_mul_act_max_q;
logic signed [63:0] s_mul_product_d;
// Stage 2 (nudge and truncating division by 2^31) registers and result.
logic s_nud_valid_q;
logic signed [31:0] s_nud_data_q;
logic s_nud_fault_q;
logic signed [ 7:0] s_nud_shift_q;
logic signed [ 7:0] s_nud_zout_q;
logic signed [ 7:0] s_nud_act_min_q;
logic signed [ 7:0] s_nud_act_max_q;
logic signed [31:0] s_nud_data_d;
// Stage 3 (RoundingDivideByPOT) registers and result.
logic s_rdp_valid_q;
logic signed [31:0] s_rdp_data_q;
logic s_rdp_fault_q;
logic signed [ 7:0] s_rdp_zout_q;
logic signed [ 7:0] s_rdp_act_min_q;
logic signed [ 7:0] s_rdp_act_max_q;
logic signed [31:0] s_rdp_data_d;
logic signed [ 7:0] s_rdp_exponent;
// Stage 4 (widened zero-point add and clamp) registers and result.
logic s_out_valid_q;
logic signed [ 7:0] s_out_data_q;
logic s_out_fault_q;
logic signed [ 7:0] s_out_data_d;
logic signed [32:0] s_zout_sum;
logic signed [32:0] s_act_min_ext;
logic signed [32:0] s_act_max_ext;
logic s_shift_ready;
logic s_mul_ready;
logic s_nud_ready;
logic s_rdp_ready;
// Pinned Q31 nudge and trunc-toward-zero division by 2^31. The
// INT32_MIN-times-INT32_MIN saturation is the only way to reach
// product_i == 2^62, so the product value identifies the special case.
function automatic logic signed [31:0] nudge_truncate(input logic signed [63:0] product_i);
logic signed [63:0] s_nudged;
logic signed [63:0] s_quotient;
begin
if (product_i == 64'sh4000_0000_0000_0000) begin
return 32'sh7fff_ffff;
end
s_nudged = product_i + ((product_i >= 64'sd0) ? 64'sd1073741824 : -64'sd1073741823);
if (s_nudged < 64'sd0) begin
s_quotient = -((-s_nudged) >>> 31);
end else begin
s_quotient = s_nudged >>> 31;
end
return 32'(s_quotient);
end
endfunction
// Pinned correctly rounded division by 2^exponent_i (exponent 0 returns value_i).
function automatic logic signed [31:0] rounding_divide_by_pot(input logic signed [31:0] value_i,
input logic [4:0] exponent_i);
logic [31:0] s_mask;
logic [31:0] s_remainder;
logic [31:0] s_threshold;
logic signed [31:0] s_quotient;
begin
if (exponent_i == 5'd0) begin
return value_i;
end
s_mask = (32'd1 << exponent_i) - 32'd1;
s_remainder = value_i & s_mask;
s_threshold = (s_mask >> 1) + ((value_i < 32'sd0) ? 32'd1 : 32'd0);
s_quotient = value_i >>> exponent_i;
return s_quotient + ((s_remainder > s_threshold) ? 32'sd1 : 32'sd0);
end
endfunction
// Stage 0: checked left shift; the 64-bit intermediate detects any result
// outside signed INT32 without wrapping. Valid shifts are -31..30.
always_comb begin
s_shift_data_d = req_acc_i;
s_shift_fault_d = 1'b0;
s_shifted_wide = 64'(req_acc_i);
if (req_shift_i > 8'sd0) begin
s_shifted_wide = 64'(req_acc_i) <<< req_shift_i[5:0];
s_shift_data_d = s_shifted_wide[31:0];
s_shift_fault_d = s_shifted_wide != 64'($signed(s_shifted_wide[31:0]));
end
end
// Stage 1: the multiplier is consumed as a nonnegative signed INT32 value;
// multiplier 0 is the flushed case. The full 64-bit product is registered.
always_comb begin
s_mul_product_d = s_shift_data_q * $signed(s_shift_multiplier_q);
end
// Stage 2: nudge and truncating division by 2^31 (SRDHM back half).
always_comb begin
s_nud_data_d = nudge_truncate(s_mul_product_q);
end
// Stage 3: RoundingDivideByPOT applies only for negative shifts, r = -shift.
always_comb begin
s_rdp_exponent = -s_nud_shift_q;
s_rdp_data_d = s_nud_data_q;
if (s_nud_shift_q < 8'sd0) begin
s_rdp_data_d = rounding_divide_by_pot(s_nud_data_q, s_rdp_exponent[4:0]);
end
end
// Stage 4: widened 33-bit zero-point add, then clamp to the signed bounds;
// a flagged item publishes 8'sd0 with resp_fault_o set.
always_comb begin
s_zout_sum = {s_rdp_data_q[31], s_rdp_data_q} + {{25{s_rdp_zout_q[7]}}, s_rdp_zout_q};
s_act_min_ext = {{25{s_rdp_act_min_q[7]}}, s_rdp_act_min_q};
s_act_max_ext = {{25{s_rdp_act_max_q[7]}}, s_rdp_act_max_q};
s_out_data_d = s_zout_sum[7:0];
if (s_zout_sum > s_act_max_ext) begin
s_out_data_d = s_rdp_act_max_q;
end else if (s_zout_sum < s_act_min_ext) begin
s_out_data_d = s_rdp_act_min_q;
end
if (s_rdp_fault_q) begin
s_out_data_d = 8'sd0;
end
end
assign s_shift_ready = !s_mul_valid_q || s_mul_ready;
assign s_mul_ready = !s_nud_valid_q || s_nud_ready;
assign s_nud_ready = !s_rdp_valid_q || s_rdp_ready;
assign s_rdp_ready = !s_out_valid_q || resp_ready_i;
assign req_ready_o = !s_shift_valid_q || s_shift_ready;
assign resp_valid_o = s_out_valid_q;
assign resp_data_o = s_out_data_q;
assign resp_fault_o = s_out_fault_q;
// Datapath registers load only when their stage transfers, so they need no
// reset; only the per-stage valid bits are reset.
always_ff @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
s_shift_valid_q <= 1'b0;
s_mul_valid_q <= 1'b0;
s_nud_valid_q <= 1'b0;
s_rdp_valid_q <= 1'b0;
s_out_valid_q <= 1'b0;
end else begin
if (req_valid_i && req_ready_o) begin
s_shift_valid_q <= 1'b1;
end else if (s_shift_ready) begin
s_shift_valid_q <= 1'b0;
end
if (s_shift_valid_q && s_shift_ready) begin
s_mul_valid_q <= 1'b1;
end else if (s_mul_ready) begin
s_mul_valid_q <= 1'b0;
end
if (s_mul_valid_q && s_mul_ready) begin
s_nud_valid_q <= 1'b1;
end else if (s_nud_ready) begin
s_nud_valid_q <= 1'b0;
end
if (s_nud_valid_q && s_nud_ready) begin
s_rdp_valid_q <= 1'b1;
end else if (s_rdp_ready) begin
s_rdp_valid_q <= 1'b0;
end
if (s_rdp_valid_q && s_rdp_ready) begin
s_out_valid_q <= 1'b1;
end else if (resp_ready_i) begin
s_out_valid_q <= 1'b0;
end
end
end
always_ff @(posedge clk_i) begin
if (req_valid_i && req_ready_o) begin
s_shift_data_q <= s_shift_data_d;
s_shift_fault_q <= s_shift_fault_d;
s_shift_multiplier_q <= req_multiplier_i;
s_shift_shift_q <= req_shift_i;
s_shift_zout_q <= req_zout_i;
s_shift_act_min_q <= req_act_min_i;
s_shift_act_max_q <= req_act_max_i;
end
if (s_shift_valid_q && s_shift_ready) begin
s_mul_product_q <= s_mul_product_d;
s_mul_fault_q <= s_shift_fault_q;
s_mul_shift_q <= s_shift_shift_q;
s_mul_zout_q <= s_shift_zout_q;
s_mul_act_min_q <= s_shift_act_min_q;
s_mul_act_max_q <= s_shift_act_max_q;
end
if (s_mul_valid_q && s_mul_ready) begin
s_nud_data_q <= s_nud_data_d;
s_nud_fault_q <= s_mul_fault_q;
s_nud_shift_q <= s_mul_shift_q;
s_nud_zout_q <= s_mul_zout_q;
s_nud_act_min_q <= s_mul_act_min_q;
s_nud_act_max_q <= s_mul_act_max_q;
end
if (s_nud_valid_q && s_nud_ready) begin
s_rdp_data_q <= s_rdp_data_d;
s_rdp_fault_q <= s_nud_fault_q;
s_rdp_zout_q <= s_nud_zout_q;
s_rdp_act_min_q <= s_nud_act_min_q;
s_rdp_act_max_q <= s_nud_act_max_q;
end
if (s_rdp_valid_q && s_rdp_ready) begin
s_out_data_q <= s_out_data_d;
s_out_fault_q <= s_rdp_fault_q;
end
end
`ifndef SV_ASSRT_DISABLE
logic s_req_stall_q;
logic s_resp_stall_q;
always_ff @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
s_req_stall_q <= 1'b0;
s_resp_stall_q <= 1'b0;
end else begin
s_req_stall_q <= req_valid_i && !req_ready_o;
s_resp_stall_q <= resp_valid_o && !resp_ready_i;
if (s_req_stall_q) begin
assert ($stable(req_acc_i));
assert ($stable(req_multiplier_i));
assert ($stable(req_shift_i));
assert ($stable(req_zout_i));
assert ($stable(req_act_min_i));
assert ($stable(req_act_max_i));
end
if (s_resp_stall_q) begin
assert ($stable(resp_data_o));
assert ($stable(resp_fault_o));
end
end
end
`endif
endmodule
+242
View File
@@ -0,0 +1,242 @@
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// SPDX-License-Identifier: MulanPSL-2.0
// NPU vector unit: one depthwise context of eight checked INT32 lane sums
// plus an independent scalar auxiliary unit (running signed-byte max and
// checked INT32 byte sum). start_valid_i acquires the idle context and
// preloads each lane sum from start_bias_i once (bias is added once, not per
// row). Each accepted depthwise row centers every valid lane byte to signed
// nine bits (byte - in_zero), forms the signed 9x8 product and performs a
// checked INT32 add; any overflow enters Fault with the lowest faulting lane
// index recorded, the sums freeze and never wrap. The scalar aux is usable in
// Idle or Accumulate and is independent of the depthwise handshake; its
// overflow reports the dedicated fault-lane encoding 3'd7 (the aux unit has
// no lane). start_valid_i has priority over a same-cycle aux operation in
// Idle; a depthwise overflow has priority over a same-cycle aux overflow for
// the recorded lane. aux_result_o combinationally reflects the register
// selected by the most recent aux operation (the sum register after
// start/clear). drain_valid_i, accepted only in Accumulate with dw_valid_i
// low, streams the eight lane sums in lane order with exactly-once
// valid/ready handshakes and drain_last_o on the eighth beat; the context
// frees after the final handshake. Fault and drain requests outside their
// accepted states are ignored. Fault releases only via clear_i, which also
// clears the sums and aux registers.
module npu_vector (
input logic clk_i,
input logic rst_n_i,
input logic clear_i,
input logic start_valid_i,
output logic start_ready_o,
input logic [npu_pkg::DepthwiseLanes-1:0][31:0] start_bias_i,
input logic dw_valid_i,
output logic dw_ready_o,
input logic [npu_pkg::DepthwiseLanes-1:0][ 7:0] dw_a_bytes_i,
input logic [npu_pkg::DepthwiseLanes-1:0][ 7:0] dw_w_bytes_i,
input logic signed [ 7:0] dw_in_zero_i,
input logic [npu_pkg::DepthwiseLanes-1:0] dw_lane_valid_i,
input logic aux_valid_i,
input logic [ 1:0] aux_op_i,
input logic signed [ 7:0] aux_data_i,
input logic drain_valid_i,
output logic drain_data_valid_o,
input logic drain_data_ready_i,
output logic signed [ 31:0] drain_data_o,
output logic drain_last_o,
output logic signed [ 31:0] aux_result_o,
output logic busy_o,
output logic fault_sticky_o,
output logic [ 2:0] fault_lane_o
);
localparam int unsigned LaneCount = npu_pkg::DepthwiseLanes;
// Aux opcodes (2'd0 is nop) and the dedicated aux fault-lane encoding.
localparam logic [1:0] AuxOpMax = 2'd1;
localparam logic [1:0] AuxOpSum = 2'd2;
localparam logic [2:0] AuxFaultLane = 3'd7;
typedef enum logic [1:0] {
Idle,
Accumulate,
Draining,
Fault
} state_e;
state_e s_state_d;
state_e s_state_q;
logic [LaneCount-1:0][31:0] s_sum_q;
logic [ 2:0] s_drain_cnt_q;
logic [ 2:0] s_fault_lane_q;
logic signed [ 7:0] s_aux_max_q;
logic signed [ 31:0] s_aux_sum_q;
logic [ 1:0] s_aux_mode_q;
logic [LaneCount-1:0][ 8:0] s_centered;
logic [LaneCount-1:0][16:0] s_product;
logic [LaneCount-1:0][32:0] s_sum_ext;
logic [LaneCount-1:0] s_lane_ovf;
logic s_dw_ovf_any;
logic [ 2:0] s_dw_fault_lane;
logic [ 32:0] s_aux_sum_ext;
logic s_aux_ovf;
logic s_dw_accept;
logic s_aux_accept;
logic s_aux_sum_ovf_evt;
logic s_drain_accept;
// Signed 9-bit centering (byte - in_zero), signed 9x8 product and checked
// 33-bit accumulation per lane; overflow when the carry into the sign bit
// differs from the new sign bit.
for (genvar lane = 0; lane < LaneCount; lane++) begin : gen_lane_datapath
assign s_centered[lane] = $signed(
{dw_a_bytes_i[lane][7], dw_a_bytes_i[lane]}
) - $signed(
{dw_in_zero_i[7], dw_in_zero_i}
);
assign s_product[lane] = $signed(s_centered[lane]) * $signed(dw_w_bytes_i[lane]);
assign s_sum_ext[lane] = {s_sum_q[lane][31], s_sum_q[lane]} +
{{16{s_product[lane][16]}}, s_product[lane]};
assign s_lane_ovf[lane] = s_sum_ext[lane][32] != s_sum_ext[lane][31];
end
assign s_dw_ovf_any = |(dw_lane_valid_i & s_lane_ovf);
assign s_aux_sum_ext = {s_aux_sum_q[31], s_aux_sum_q} + {{25{aux_data_i[7]}}, aux_data_i};
assign s_aux_ovf = s_aux_sum_ext[32] != s_aux_sum_ext[31];
assign s_dw_accept = dw_valid_i && (s_state_q == Accumulate);
assign s_aux_accept = aux_valid_i && (aux_op_i != 2'd0) &&
((s_state_q == Idle) || (s_state_q == Accumulate)) &&
!((s_state_q == Idle) && start_valid_i);
assign s_aux_sum_ovf_evt = s_aux_accept && (aux_op_i == AuxOpSum) && s_aux_ovf;
assign s_drain_accept = drain_valid_i && !dw_valid_i && (s_state_q == Accumulate);
// Lowest faulting lane wins within a cycle.
always_comb begin
s_dw_fault_lane = 3'd0;
for (int lane = LaneCount - 1; lane >= 0; lane--) begin
if (dw_lane_valid_i[lane] && s_lane_ovf[lane]) begin
s_dw_fault_lane = 3'(lane);
end
end
end
always_comb begin
s_state_d = s_state_q;
unique case (s_state_q)
Idle: begin
if (start_valid_i) begin
s_state_d = Accumulate;
end else if (s_aux_sum_ovf_evt) begin
s_state_d = Fault;
end
end
Accumulate: begin
if (s_dw_accept && s_dw_ovf_any) begin
s_state_d = Fault;
end else if (s_aux_sum_ovf_evt) begin
s_state_d = Fault;
end else if (s_drain_accept) begin
s_state_d = Draining;
end
end
Draining: begin
if (drain_data_ready_i && (s_drain_cnt_q == 3'(LaneCount - 1))) begin
s_state_d = Idle;
end
end
Fault: begin
s_state_d = Fault;
end
default: begin
s_state_d = Idle;
end
endcase
end
assign start_ready_o = (s_state_q == Idle);
assign dw_ready_o = (s_state_q == Accumulate);
assign drain_data_valid_o = (s_state_q == Draining);
assign drain_data_o = $signed(s_sum_q[s_drain_cnt_q]);
assign drain_last_o = (s_state_q == Draining) && (s_drain_cnt_q == 3'(LaneCount - 1));
assign aux_result_o = (s_aux_mode_q == AuxOpMax) ?
{{24{s_aux_max_q[7]}}, s_aux_max_q} : s_aux_sum_q;
assign busy_o = (s_state_q != Idle);
assign fault_sticky_o = (s_state_q == Fault);
assign fault_lane_o = s_fault_lane_q;
always_ff @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
s_state_q <= Idle;
s_sum_q <= '0;
s_drain_cnt_q <= 3'd0;
s_fault_lane_q <= 3'd0;
s_aux_max_q <= 8'h80;
s_aux_sum_q <= 32'd0;
s_aux_mode_q <= 2'd0;
end else if (clear_i) begin
s_state_q <= Idle;
s_sum_q <= '0;
s_drain_cnt_q <= 3'd0;
s_fault_lane_q <= 3'd0;
s_aux_max_q <= 8'h80;
s_aux_sum_q <= 32'd0;
s_aux_mode_q <= 2'd0;
end else begin
s_state_q <= s_state_d;
if ((s_state_q == Idle) && start_valid_i) begin
// Bias preloads each lane sum exactly once at context acquire.
for (int lane = 0; lane < LaneCount; lane++) begin
s_sum_q[lane] <= start_bias_i[lane];
end
s_drain_cnt_q <= 3'd0;
s_aux_max_q <= 8'h80;
s_aux_sum_q <= 32'd0;
s_aux_mode_q <= 2'd0;
end
if (s_dw_accept && !s_dw_ovf_any) begin
for (int lane = 0; lane < LaneCount; lane++) begin
if (dw_lane_valid_i[lane]) begin
s_sum_q[lane] <= s_sum_ext[lane][31:0];
end
end
end
// Depthwise overflow wins the recorded lane over a same-cycle aux
// overflow; the aux unit reports its dedicated encoding otherwise.
if (s_dw_accept && s_dw_ovf_any) begin
s_fault_lane_q <= s_dw_fault_lane;
end else if (s_aux_sum_ovf_evt) begin
s_fault_lane_q <= AuxFaultLane;
end
if (s_aux_accept) begin
if (aux_op_i == AuxOpMax) begin
if ($signed(aux_data_i) > s_aux_max_q) begin
s_aux_max_q <= aux_data_i;
end
s_aux_mode_q <= AuxOpMax;
end else if (!s_aux_ovf) begin
s_aux_sum_q <= s_aux_sum_ext[31:0];
s_aux_mode_q <= AuxOpSum;
end
end
if ((s_state_q == Accumulate) && (s_state_d == Draining)) begin
s_drain_cnt_q <= 3'd0;
end else if ((s_state_q == Draining) && drain_data_ready_i) begin
s_drain_cnt_q <= s_drain_cnt_q + 3'd1;
end
end
end
`ifndef SV_ASSRT_DISABLE
// A stalled drain beat holds its lane sums and lane index: the flag samples
// "no handshake completed last cycle", so the $stable checks compare across
// exactly the cycles where no update was allowed.
logic s_drain_stall_q;
always_ff @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
s_drain_stall_q <= 1'b0;
end else begin
s_drain_stall_q <= (s_state_q == Draining) && !drain_data_ready_i;
if (s_drain_stall_q) begin
assert ($stable(s_sum_q));
assert ($stable(s_drain_cnt_q));
end
end
end
`endif
endmodule
+8 -1
View File
@@ -195,7 +195,14 @@
"jpeg_table_cache.sv",
"jpeg_table_register_bank.sv",
"jpeg_table_store.sv",
"jpeg_transform.sv"
"jpeg_transform.sv",
"npu_accumulator.sv",
"npu_local_sram.sv",
"npu_mac_array.sv",
"npu_patch_packer.sv",
"npu_pkg.sv",
"npu_requantizer.sv",
"npu_vector.sv"
],
"rtl/ip/peripheral": [
"apb4_clint.sv",
+245
View File
@@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""Run the isolated NPU-P1 IHP130 synthesis and 72 MHz STA flow."""
from __future__ import annotations
import argparse
import glob
import json
import re
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
SYNTH_TCL = ROOT / "physical/smoke/syn/yosys/script/synth.tcl"
STA_TCL = ROOT / "physical/smoke/sta/opensta/npu_p1.tcl"
STA_SDC = ROOT / "physical/smoke/sta/opensta/npu_p1.sdc"
RUN_FLOW = ROOT / "scripts/run_flow.py"
PDK_ROOT = ROOT / "physical/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref"
STA_LIBERTY = PDK_ROOT / "sg13g2_stdcell/lib/sg13g2_stdcell_slow_1p08V_125C.lib"
STA_LINK_LIB = PDK_ROOT / "sg13g2_io/lib/sg13g2_io_slow_1p08V_3p0V_125C.lib"
TOPS = (
"npu_local_sram",
"npu_patch_packer",
"npu_mac_array",
"npu_accumulator",
"npu_vector",
"npu_requantizer",
)
NPU_RTL = (
"npu_pkg.sv",
"npu_local_sram.sv",
"npu_patch_packer.sv",
"npu_mac_array.sv",
"npu_accumulator.sv",
"npu_vector.sv",
"npu_requantizer.sv",
)
SRAM_MACRO = "RM_IHPSG13_1P_1024x32_c2_bm_bist"
EXPECTED_SRAM_MACROS = 16
TARGET_PERIOD_PS = 13889
VIOLATED = re.compile(r"VIOLATED")
LATCH = re.compile(r"\$dlatch|latch", re.IGNORECASE)
def _run_flow(tool: str, log: Path, result: Path, env: dict[str, str], command: list[str]) -> None:
log.parent.mkdir(parents=True, exist_ok=True)
args = [sys.executable, str(RUN_FLOW), "--tool", tool, "--log", str(log), "--result", str(result)]
for name, value in env.items():
args += ["--env", f"{name}={value}"]
args += ["--", *command]
completed = subprocess.run(args, cwd=ROOT)
if completed.returncode != 0:
raise RuntimeError(f"{tool} flow failed for {env.get('TOP_DESIGN', env.get('OPENSTA_TOP'))}: {log}")
def _write_filelist(path: Path) -> None:
lines = [
"+define+PDK_IHP130",
"+define+HAVE_SRAM_MACRO",
"+define+SYNTHESIS",
"+define+SV_ASSRT_DISABLE",
f"+incdir+{ROOT / 'rtl/ip/multimedia'}",
f"+incdir+{ROOT / 'rtl/managed/clusterip/common/rtl'}",
]
for name in NPU_RTL:
source = ROOT / "rtl/ip/multimedia" / name
if not source.is_file():
raise RuntimeError(f"missing NPU-P1 RTL source: {source}")
lines.append(str(source))
lines.append(str(ROOT / "rtl/tech/tc_sram.sv"))
lines.append(str(ROOT / "rtl/managed/clusterip/common/rtl/utils/register.sv"))
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
def _synth(top: str, build_root: Path, filelist: Path) -> dict[str, object]:
base = build_root / "syn/yosys-npu" / top
for sub in ("out", "tmp", "rpt"):
(base / sub).mkdir(parents=True, exist_ok=True)
env = {
"PDK": "IHP130",
"SOC": "MINI",
"SYNTH_RECIPE": "balanced",
"HAVE_SRAM_MACRO": "YES",
"SRAM_SIZE_KIB": "32",
"YOSYS_TARGET_PERIOD_PS": str(TARGET_PERIOD_PS),
"SV_FLIST": str(filelist),
"TOP_DESIGN": top,
"PROJ_NAME": top,
"BUILD": str(base / "out"),
"WORK": str(base / "tmp"),
"REPORTS": str(base / "rpt"),
"NETLIST": str(base / "out" / f"{top}_yosys.v"),
"CONFIG": str(base / "out" / f"{top}_yosys.config"),
}
log = build_root / "syn/yosys-npu" / f"{top}.log"
_run_flow("yosys", log, base / "result-synth.json", env, ["yosys", "-c", str(SYNTH_TCL)])
area = json.loads((base / "rpt" / f"{top}_area.json").read_text(encoding="utf-8"))
design = area["design"]
macro_count = design.get("num_cells_by_type", {}).get(SRAM_MACRO, 0)
latch_cells = {
name: count
for name, count in design.get("num_cells_by_type", {}).items()
if "dlatch" in name.lower() or "latch" in name.lower()
}
log_text = log.read_text(encoding="utf-8", errors="replace")
latch_lines = [
line
for line in log_text.splitlines()
if LATCH.search(line) and "PROC_DLATCH pass" not in line
]
return {
"top": top,
"area_um2": design.get("area"),
"num_cells": design.get("num_cells"),
"sram_macro_count": macro_count,
"latch_cells": latch_cells,
"latch_log_lines": latch_lines[:10],
"log": str(log),
"netlist": env["NETLIST"],
}
def _sta(top: str, build_root: Path, netlist: Path) -> dict[str, object]:
base = build_root / "sta/opensta-npu" / top
base.mkdir(parents=True, exist_ok=True)
sram_libs = sorted(
glob.glob(str(PDK_ROOT / "sg13g2_sram/lib/*_slow_1p08V_125C.lib"))
)
env = {
"OPENSTA_NETLIST": str(netlist),
"OPENSTA_LIBERTY": str(STA_LIBERTY),
"OPENSTA_LINK_LIBS": str(STA_LINK_LIB),
"OPENSTA_SRAM_LIBS": " ".join(sram_libs),
"OPENSTA_SDC": str(STA_SDC),
"OPENSTA_REPORT": str(base / f"{top}_checks.rpt"),
"OPENSTA_METRICS": str(base / f"{top}_timing_metrics.rpt"),
"OPENSTA_TOP": top,
}
log = base / f"{top}.log"
_run_flow("opensta", log, base / "result-sta.json", env, ["sta", "-no_init", "-exit", str(STA_TCL)])
metrics = {}
for line in (base / f"{top}_timing_metrics.rpt").read_text(encoding="utf-8").splitlines():
name, _, value = line.partition("=")
metrics[name] = float(value)
checks = (base / f"{top}_checks.rpt").read_text(encoding="utf-8", errors="replace")
return {
"top": top,
"wns_setup_ns": metrics.get("wns_max"),
"tns_setup_ns": metrics.get("tns_max"),
"setup_paths": metrics.get("paths_max"),
"wns_hold_ns": metrics.get("wns_min"),
"tns_hold_ns": metrics.get("tns_min"),
"hold_paths": metrics.get("paths_min"),
"violated_paths": len(VIOLATED.findall(checks)),
"log": str(log),
}
def _tool_version(command: list[str]) -> str:
completed = subprocess.run(command, capture_output=True, text=True)
return (completed.stdout or completed.stderr).splitlines()[0].strip()
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--build-root", type=Path, default=ROOT / "build/npu-p1-flow")
parser.add_argument("--top", action="append", choices=TOPS, help="limit to one top module")
parser.add_argument("--synth", action="store_true")
parser.add_argument("--sta", action="store_true")
parser.add_argument("--evidence", type=Path, help="evidence JSON output path")
args = parser.parse_args()
if not args.synth and not args.sta:
args.synth = args.sta = True
tops = tuple(args.top) if args.top else TOPS
build_root = args.build_root.resolve()
filelist = build_root / "npu_p1.fl"
_write_filelist(filelist)
evidence: dict[str, object] = {
"phase": "NPU-P1",
"verification_ids": ["NPU-V017"],
"generated_at": datetime.now(timezone.utc).isoformat(),
"git_revision": subprocess.check_output(
["git", "-C", str(ROOT), "rev-parse", "HEAD"], text=True
).strip(),
"tools": {
"yosys": _tool_version(["yosys", "--version"]),
"opensta": _tool_version(["sta", "-version"]),
},
"target_period_ps": TARGET_PERIOD_PS,
"filelist": str(filelist),
"tops": {},
"failures": [],
}
failures: list[str] = evidence["failures"]
for top in tops:
record: dict[str, object] = {}
if args.synth:
record["synth"] = _synth(top, build_root, filelist)
synth = record["synth"]
if top == "npu_local_sram" and synth["sram_macro_count"] != EXPECTED_SRAM_MACROS:
failures.append(
f"npu_local_sram maps {synth['sram_macro_count']} {SRAM_MACRO}, "
f"expected {EXPECTED_SRAM_MACROS}"
)
if synth["latch_log_lines"] or synth["latch_cells"]:
failures.append(
f"{top} synthesis shows latches: {synth['latch_log_lines']} {synth['latch_cells']}"
)
netlist = build_root / "syn/yosys-npu" / top / "out" / f"{top}_yosys.v"
if args.sta:
if not netlist.is_file():
raise RuntimeError(f"missing netlist for {top}: {netlist}; run --synth first")
record["sta"] = _sta(top, build_root, netlist)
sta = record["sta"]
if not (sta["wns_setup_ns"] >= 0.0):
failures.append(f"{top} setup WNS {sta['wns_setup_ns']} ns < 0")
if not (sta["tns_setup_ns"] == 0.0):
failures.append(f"{top} setup TNS {sta['tns_setup_ns']} ns != 0")
if sta["violated_paths"]:
failures.append(f"{top} has {sta['violated_paths']} violating paths")
if not sta["setup_paths"]:
failures.append(f"{top} has no constrained setup paths (vacuous STA)")
evidence["tops"][top] = record
evidence["verdict"] = "PASS" if not failures else "FAIL"
payload = json.dumps(evidence, indent=2, sort_keys=True) + "\n"
evidence_path = args.evidence or (build_root / "evidence-p1.json")
evidence_path.parent.mkdir(parents=True, exist_ok=True)
evidence_path.write_text(payload, encoding="utf-8")
print(payload, end="")
return 0 if not failures else 1
if __name__ == "__main__":
raise SystemExit(main())
+631
View File
@@ -0,0 +1,631 @@
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// SPDX-License-Identifier: MulanPSL-2.0
//
// Scripted testbench for npu_accumulator: two-context acquire/bias preload,
// checked row accumulation with a 64-bit TB model, position-major drain with
// stall patterns, interleaved contexts, K-chunk continuation, back-to-back
// reuse, exactly-once publication, pending drain while another drain is in
// flight, intentional positive/negative overflow faults (sticky flag, lowest
// sum index, no drain from Fault), and clear_i release.
`timescale 1ns / 1ps
module npu_accumulator_tb;
logic clk_i = 1'b0;
logic rst_n_i = 1'b0;
logic clear;
logic start_valid;
logic start_ready;
logic [ 7:0][31:0] start_bias;
logic start_context;
logic row_valid;
logic row_ready;
logic row_context;
logic [ 7:0][ 7:0][16:0] row_products;
logic [63:0][16:0] products_drive;
logic drain_valid;
logic drain_context;
logic drain_data_valid;
logic drain_data_ready;
logic signed [31:0] drain_data;
logic drain_last;
logic busy;
logic fault_sticky;
logic [ 5:0] fault_sum;
// 64-bit TB model of the 2x64 sums; index = context*64 + position*8 + channel.
longint signed model_sum [0:127];
always #5 clk_i = ~clk_i;
assign row_products = products_drive;
npu_accumulator u_dut (
.clk_i (clk_i),
.rst_n_i (rst_n_i),
.clear_i (clear),
.start_valid_i (start_valid),
.start_ready_o (start_ready),
.start_bias_i (start_bias),
.start_context_o (start_context),
.row_valid_i (row_valid),
.row_ready_o (row_ready),
.row_context_i (row_context),
.row_products_i (row_products),
.drain_valid_i (drain_valid),
.drain_context_i (drain_context),
.drain_data_valid_o(drain_data_valid),
.drain_data_ready_i(drain_data_ready),
.drain_data_o (drain_data),
.drain_last_o (drain_last),
.busy_o (busy),
.fault_sticky_o (fault_sticky),
.fault_sum_o (fault_sum)
);
function automatic longint signed product_at(input integer index_i);
logic signed [16:0] s_product;
begin
s_product = products_drive[index_i];
product_at = s_product;
end
endfunction
task automatic hard_reset;
begin
rst_n_i = 1'b0;
repeat (3) @(posedge clk_i);
rst_n_i = 1'b1;
repeat (2) @(posedge clk_i);
end
endtask
task automatic fill_products(input logic signed [16:0] value_i);
begin
products_drive = {64{value_i}};
end
endtask
task automatic set_product(input integer index_i, input logic signed [16:0] value_i);
begin
products_drive[index_i] = value_i;
end
endtask
task automatic acquire(input logic expected_context_i);
begin
@(negedge clk_i);
start_valid = 1'b1;
for (int cycle = 0; cycle < 100; cycle++) begin
@(posedge clk_i);
if (start_ready) begin
if (start_context !== expected_context_i) begin
$fatal(1, "NPU accumulator acquire context mismatch got=%0d expected=%0d",
start_context, expected_context_i);
end
for (int index = 0; index < 64; index++) begin
model_sum[expected_context_i*64+index] = $signed(start_bias[index%8]);
end
@(negedge clk_i);
start_valid = 1'b0;
return;
end
end
$fatal(1, "NPU accumulator acquire timeout context=%0d", expected_context_i);
end
endtask
// Accepted rows must never overflow the 64-bit-checked TB model here; the
// faulting-row task below is the only overflow path. When with_drain_i is
// set, the drain request for the same context is raised together with the
// row: the context enters DrainPending in the acceptance cycle and the
// drain may publish only after this row has landed in the sums.
task automatic send_row(input logic context_i, input logic with_drain_i);
begin
@(negedge clk_i);
row_context = context_i;
row_valid = 1'b1;
if (with_drain_i) begin
drain_context = context_i;
drain_valid = 1'b1;
end
for (int cycle = 0; cycle < 100; cycle++) begin
@(posedge clk_i);
if (row_ready) begin
for (int index = 0; index < 64; index++) begin
model_sum[context_i*64+index] = model_sum[context_i*64+index] + product_at(index);
if ((model_sum[context_i*64+index] > 64'sd2147483647) ||
(model_sum[context_i*64+index] < -64'sd2147483648)) begin
$fatal(1, "NPU accumulator TB model overflow on non-fault row context=%0d index=%0d",
context_i, index);
end
end
@(negedge clk_i);
row_valid = 1'b0;
if (with_drain_i) begin
drain_valid = 1'b0;
end
return;
end
end
$fatal(1, "NPU accumulator row timeout context=%0d", context_i);
end
endtask
// Drive a burst of rows accepted on consecutive cycles (row_valid stays
// high, products change every cycle) to prove the one-row-per-cycle
// sustained throughput of the pipelined accumulate: stage A must keep
// accepting while stage B accumulates the previous row. Row products are
// base_i + row*step_i replicated on all 64 lanes.
task automatic send_row_burst(input logic context_i, input integer row_count_i,
input integer base_i, input integer step_i);
begin
for (int row = 0; row < row_count_i; row++) begin
@(negedge clk_i);
row_context = context_i;
products_drive = {64{17'(base_i + row * step_i)}};
row_valid = 1'b1;
for (int cycle = 0; cycle < 100; cycle++) begin
@(posedge clk_i);
if (row_ready) begin
for (int index = 0; index < 64; index++) begin
model_sum[context_i*64+index] = model_sum[context_i*64+index] + product_at(index);
if ((model_sum[context_i*64+index] > 64'sd2147483647) ||
(model_sum[context_i*64+index] < -64'sd2147483648)) begin
$fatal(1, "NPU accumulator TB model overflow on burst row context=%0d index=%0d",
context_i, index);
end
end
break;
end
if (cycle == 99) begin
$fatal(1, "NPU accumulator burst row timeout context=%0d row=%0d", context_i, row);
end
end
end
@(negedge clk_i);
row_valid = 1'b0;
end
endtask
// Drive a row whose model predicts at least one overflowing update; the DUT
// must latch the sticky fault and the lowest overflowing sum index, and the
// stored sums (and therefore the TB model) stay untouched.
task automatic send_row_expect_fault(input logic context_i);
begin
longint signed s_trial;
integer s_fault_index;
logic s_found;
s_found = 1'b0;
s_fault_index = 0;
for (int index = 63; index >= 0; index--) begin
s_trial = model_sum[context_i*64+index] + product_at(index);
if ((s_trial > 64'sd2147483647) || (s_trial < -64'sd2147483648)) begin
s_found = 1'b1;
s_fault_index = index;
end
end
if (!s_found) begin
$fatal(1, "NPU accumulator TB expected a fault but the model does not overflow");
end
@(negedge clk_i);
row_context = context_i;
row_valid = 1'b1;
for (int cycle = 0; cycle < 100; cycle++) begin
@(posedge clk_i);
if (row_ready) begin
@(negedge clk_i);
row_valid = 1'b0;
break;
end
if (cycle == 99) begin
$fatal(1, "NPU accumulator fault row timeout context=%0d", context_i);
end
end
repeat (2) @(posedge clk_i);
if (fault_sticky !== 1'b1) begin
$fatal(1, "NPU accumulator fault_sticky not set context=%0d", context_i);
end
if (fault_sum !== 6'(s_fault_index)) begin
$fatal(1, "NPU accumulator fault_sum mismatch got=%0d expected=%0d", fault_sum,
s_fault_index);
end
end
endtask
task automatic drain_launch(input logic context_i);
begin
@(negedge clk_i);
drain_context = context_i;
drain_valid = 1'b1;
for (int cycle = 0; cycle < 100; cycle++) begin
@(posedge clk_i);
if (drain_data_valid) begin
break;
end
if (cycle == 99) begin
$fatal(1, "NPU accumulator drain launch timeout context=%0d", context_i);
end
end
@(negedge clk_i);
drain_valid = 1'b0;
drain_data_ready = 1'b0;
end
endtask
// Streams the 64 sums of a context already in Draining, checking
// position-major order, values, and drain_last; applies deterministic stall
// stretches, optionally interleaves rows to the other accumulating context
// at items 16/32/48, and optionally raises a held pending drain request for
// another context at item 40 (left asserted for the caller to observe).
task automatic drain_stream(input logic context_i, input logic interleave_en_i,
input logic interleave_context_i, input logic pending_en_i,
input logic pending_context_i);
begin
integer stalls;
for (int index = 0; index < 64; index++) begin
if (interleave_en_i && ((index == 16) || (index == 32) || (index == 48))) begin
fill_products(17'sd900);
send_row(interleave_context_i, 1'b0);
end
if (pending_en_i && (index == 40)) begin
@(negedge clk_i);
drain_context = pending_context_i;
drain_valid = 1'b1;
end
stalls = ((index % 7) == 3) ? 3 : (index % 2);
repeat (stalls) @(posedge clk_i);
@(negedge clk_i);
drain_data_ready = 1'b1;
for (int cycle = 0; cycle < 100; cycle++) begin
@(posedge clk_i);
if (drain_data_valid && drain_data_ready) begin
if ($signed(drain_data) !== model_sum[context_i*64+index]) begin
$fatal(1, "NPU accumulator drain mismatch context=%0d index=%0d got=%0d expected=%0d",
context_i, index, $signed(drain_data), model_sum[context_i*64+index]);
end
if (drain_last !== (index == 63)) begin
$fatal(1, "NPU accumulator drain_last mismatch context=%0d index=%0d", context_i,
index);
end
break;
end
if (cycle == 99) begin
$fatal(1, "NPU accumulator drain item timeout context=%0d index=%0d", context_i, index);
end
end
@(negedge clk_i);
drain_data_ready = 1'b0;
end
end
endtask
task automatic drain_and_check(input logic context_i);
begin
drain_launch(context_i);
drain_stream(context_i, 1'b0, 1'b0, 1'b0, 1'b0);
end
endtask
// A drain request naming an Idle or Fault context must never publish data.
task automatic check_drain_ignored(input logic context_i);
begin
@(negedge clk_i);
drain_context = context_i;
drain_valid = 1'b1;
repeat (4) @(posedge clk_i);
if (drain_data_valid) begin
$fatal(1, "NPU accumulator drain published from non-accumulating context=%0d", context_i);
end
@(negedge clk_i);
drain_valid = 1'b0;
end
endtask
task automatic check_row_rejected(input logic context_i);
begin
@(negedge clk_i);
row_context = context_i;
row_valid = 1'b1;
repeat (3) @(posedge clk_i);
if (row_ready) begin
$fatal(1, "NPU accumulator row accepted on non-accumulating context=%0d", context_i);
end
@(negedge clk_i);
row_valid = 1'b0;
end
endtask
task automatic check_start_blocked;
begin
@(negedge clk_i);
start_valid = 1'b1;
repeat (3) @(posedge clk_i);
if (start_ready) begin
$fatal(1, "NPU accumulator start accepted while both contexts occupied");
end
@(negedge clk_i);
start_valid = 1'b0;
end
endtask
task automatic pulse_clear;
begin
@(negedge clk_i);
clear = 1'b1;
@(negedge clk_i);
clear = 1'b0;
@(posedge clk_i);
if (fault_sticky !== 1'b0) begin
$fatal(1, "NPU accumulator fault_sticky not released by clear_i");
end
if (busy !== 1'b0) begin
$fatal(1, "NPU accumulator busy not released by clear_i");
end
if (start_ready !== 1'b1) begin
$fatal(1, "NPU accumulator start_ready not restored by clear_i");
end
end
endtask
initial begin
clear = 1'b0;
start_valid = 1'b0;
start_bias = '0;
row_valid = 1'b0;
row_context = 1'b0;
products_drive = '0;
drain_valid = 1'b0;
drain_context = 1'b0;
drain_data_ready = 1'b0;
for (int index = 0; index < 128; index++) begin
model_sum[index] = 64'sd0;
end
hard_reset();
if ((busy !== 1'b0) || (start_ready !== 1'b1) || (fault_sticky !== 1'b0)) begin
$fatal(1, "NPU accumulator bad reset state busy=%0d start_ready=%0d fault=%0d", busy,
start_ready, fault_sticky);
end
// Scenario 1: ctx0 bias preload, two acquire-free accumulate bursts
// (K-chunk continuation, including an all-zero row), drain with stalls.
for (int channel = 0; channel < 8; channel++) begin
start_bias[channel] = (channel * 12345) - 30000;
end
acquire(1'b0);
if (busy !== 1'b1) begin
$fatal(1, "NPU accumulator busy not set after acquire");
end
fill_products(17'sd300);
set_product(26, -17'sd1000);
set_product(47, 17'sd32512);
send_row(1'b0, 1'b0);
fill_products(-17'sd250);
send_row(1'b0, 1'b0);
fill_products(17'sd1000);
send_row(1'b0, 1'b0);
fill_products(17'sd0);
send_row(1'b0, 1'b0);
fill_products(-17'sd3000);
set_product(0, 17'sd32512);
send_row(1'b0, 1'b0);
drain_and_check(1'b0);
if (busy !== 1'b0) begin
$fatal(1, "NPU accumulator busy stuck after drain");
end
// Scenario 1b: back-to-back burst at one row per cycle; stage A must keep
// accepting while stage B accumulates the previous row (full-rate RAW
// chaining through the sums).
for (int channel = 0; channel < 8; channel++) begin
start_bias[channel] = (channel * 91) - 500;
end
acquire(1'b0);
send_row_burst(1'b0, 24, 200, -11);
drain_and_check(1'b0);
if (busy !== 1'b0) begin
$fatal(1, "NPU accumulator busy stuck after burst drain");
end
// Scenario 2: exactly-once publication; a repeated drain request on the
// now-Idle context publishes nothing.
check_drain_ignored(1'b0);
// Scenario 3: back-to-back reuse of ctx0 after its drain completed.
for (int channel = 0; channel < 8; channel++) begin
start_bias[channel] = 100000 - (channel * 999);
end
acquire(1'b0);
fill_products(17'sd2000);
send_row(1'b0, 1'b0);
set_product(63, -17'sd32768);
send_row(1'b0, 1'b0);
drain_and_check(1'b0);
// Scenario 4: interleave ctx1 acquire/accumulate while ctx0 drains, and
// start backpressure while both contexts are occupied.
for (int channel = 0; channel < 8; channel++) begin
start_bias[channel] = channel * 100;
end
acquire(1'b0);
for (int channel = 0; channel < 8; channel++) begin
start_bias[channel] = (channel * 50) - 400;
end
acquire(1'b1);
check_start_blocked();
fill_products(17'sd500);
send_row(1'b0, 1'b0);
fill_products(-17'sd700);
set_product(63, 17'sd12345);
send_row(1'b1, 1'b0);
drain_launch(1'b0);
drain_stream(1'b0, 1'b1, 1'b1, 1'b0, 1'b0);
drain_and_check(1'b1);
if (busy !== 1'b0) begin
$fatal(1, "NPU accumulator busy stuck after interleaved drains");
end
// Scenario 5: a held drain request for ctx1 raised mid-drain of ctx0
// launches ctx1 automatically once ctx0 finishes (single drain port).
for (int channel = 0; channel < 8; channel++) begin
start_bias[channel] = channel * 7;
end
acquire(1'b0);
for (int channel = 0; channel < 8; channel++) begin
start_bias[channel] = 50 - channel;
end
acquire(1'b1);
fill_products(17'sd33);
send_row(1'b0, 1'b0);
fill_products(-17'sd44);
send_row(1'b1, 1'b0);
drain_launch(1'b0);
drain_stream(1'b0, 1'b0, 1'b0, 1'b1, 1'b1);
for (int cycle = 0; cycle < 100; cycle++) begin
@(posedge clk_i);
if (drain_data_valid) begin
break;
end
if (cycle == 99) begin
$fatal(1, "NPU accumulator pending drain did not launch after ctx0 finished");
end
end
@(negedge clk_i);
drain_valid = 1'b0;
drain_stream(1'b1, 1'b0, 1'b0, 1'b0, 1'b0);
if (busy !== 1'b0) begin
$fatal(1, "NPU accumulator busy stuck after pending drain");
end
// Scenario 5b: a drain request raised in the same cycle as the final row
// acceptance must wait for the pipelined row to land in the sums before
// publishing (DrainPending waits for the accumulate pipeline).
for (int channel = 0; channel < 8; channel++) begin
start_bias[channel] = 1000 + channel;
end
acquire(1'b0);
fill_products(17'sd123);
send_row(1'b0, 1'b0);
fill_products(-17'sd456);
send_row(1'b0, 1'b1);
drain_stream(1'b0, 1'b0, 1'b0, 1'b0, 1'b0);
if (busy !== 1'b0) begin
$fatal(1, "NPU accumulator busy stuck after same-cycle row+drain");
end
// Scenario 6: intentional positive overflow on ctx1. Two sums overflow in
// the same row; the lowest index (10) must win the fault record. The
// faulting context rejects rows, never drains, and clear_i releases it.
// ctx0 is acquired first so the near-maximum bias lands on ctx1.
for (int channel = 0; channel < 8; channel++) begin
start_bias[channel] = channel;
end
acquire(1'b0);
for (int channel = 0; channel < 8; channel++) begin
start_bias[channel] = 100;
end
start_bias[2] = 32'h7fff_ff00;
acquire(1'b1);
fill_products(17'sd0);
set_product(10, 17'sd1000);
set_product(26, 17'sd2000);
send_row_expect_fault(1'b1);
check_row_rejected(1'b1);
check_drain_ignored(1'b1);
// The other context keeps working while ctx1 is faulted.
fill_products(17'sd7);
send_row(1'b0, 1'b0);
drain_and_check(1'b0);
pulse_clear();
// clear_i released the Fault context: acquire and drain both again.
for (int channel = 0; channel < 8; channel++) begin
start_bias[channel] = channel * 3;
end
acquire(1'b0);
for (int channel = 0; channel < 8; channel++) begin
start_bias[channel] = channel * 5;
end
acquire(1'b1);
fill_products(17'sd11);
send_row(1'b1, 1'b0);
drain_and_check(1'b1);
fill_products(17'sd13);
send_row(1'b0, 1'b0);
drain_and_check(1'b0);
// Scenario 6b: a stage-B fault landing while the context sits in
// DrainPending (row and drain request accepted in the same cycle) must win
// over the drain: the context faults and the drain never publishes.
for (int channel = 0; channel < 8; channel++) begin
start_bias[channel] = channel * 2;
end
acquire(1'b0);
for (int channel = 0; channel < 8; channel++) begin
start_bias[channel] = 50;
end
start_bias[2] = 32'h7fff_ff00;
acquire(1'b1);
fill_products(17'sd0);
set_product(10, 17'sd1000);
@(negedge clk_i);
row_context = 1'b1;
row_valid = 1'b1;
drain_context = 1'b1;
drain_valid = 1'b1;
for (int cycle = 0; cycle < 100; cycle++) begin
@(posedge clk_i);
if (row_ready) begin
break;
end
if (cycle == 99) begin
$fatal(1, "NPU accumulator pending-fault row timeout");
end
end
@(negedge clk_i);
row_valid = 1'b0;
drain_valid = 1'b0;
// The fault lands one cycle after acceptance; the drain must never publish.
repeat (6) begin
@(posedge clk_i);
if (drain_data_valid) begin
$fatal(1, "NPU accumulator drain published from faulting pending context");
end
end
if (fault_sticky !== 1'b1) begin
$fatal(1, "NPU accumulator pending fault not latched");
end
if (fault_sum !== 6'd10) begin
$fatal(1, "NPU accumulator pending fault_sum mismatch got=%0d expected=10", fault_sum);
end
check_row_rejected(1'b1);
pulse_clear();
// Both contexts are healthy again after the clear.
for (int channel = 0; channel < 8; channel++) begin
start_bias[channel] = channel;
end
acquire(1'b0);
fill_products(17'sd5);
send_row(1'b0, 1'b0);
drain_and_check(1'b0);
// Scenario 7: intentional negative overflow on ctx0 at sum index 5.
for (int channel = 0; channel < 8; channel++) begin
start_bias[channel] = 0;
end
start_bias[5] = 32'h8000_0010;
acquire(1'b0);
fill_products(17'sd0);
set_product(5, -17'sd1000);
send_row_expect_fault(1'b0);
pulse_clear();
repeat (2) @(posedge clk_i);
if ((busy !== 1'b0) || (start_ready !== 1'b1) || (fault_sticky !== 1'b0)) begin
$fatal(1, "NPU accumulator bad final state busy=%0d start_ready=%0d fault=%0d", busy,
start_ready, fault_sticky);
end
$display("NPU accumulator test passed");
$finish;
end
endmodule
+557
View File
@@ -0,0 +1,557 @@
`timescale 1ns / 1ps
// Directed unit test for npu_local_sram. Runs identically against the
// inferred behavioral banks (+define+PDK_BEHAV) and the IHP130 macro wrapper
// (+define+PDK_IHP130 +define+HAVE_SRAM_MACRO + the RM_IHPSG13_1P models).
module npu_local_sram_tb;
logic clk_i = 1'b0;
logic rst_n_i = 1'b0;
logic clear_i = 1'b0;
logic raw_write_valid_i = 1'b0;
logic raw_write_half_i = 1'b0;
logic [12:0] raw_write_addr_i = 13'd0;
logic [31:0] raw_write_data_i = 32'd0;
logic [ 3:0] raw_write_strb_i = 4'd0;
logic raw_read_valid_i = 1'b0;
logic raw_read_half_i = 1'b0;
logic [12:0] raw_read_addr_i = 13'd0;
logic [31:0] raw_read_data_o;
logic pack_write_valid_i = 1'b0;
logic pack_write_sel_w_i = 1'b0;
logic pack_write_half_i = 1'b0;
logic [12:0] pack_write_addr_i = 13'd0;
logic [63:0] pack_write_data_i = 64'd0;
logic [ 7:0] pack_write_strb_i = 8'd0;
logic a_read_valid_i = 1'b0;
logic a_read_half_i = 1'b0;
logic [12:0] a_read_addr_i = 13'd0;
logic [63:0] a_read_data_o;
logic w_read_valid_i = 1'b0;
logic w_read_half_i = 1'b0;
logic [12:0] w_read_addr_i = 13'd0;
logic [63:0] w_read_data_o;
logic out_req_valid_i = 1'b0;
logic out_req_write_i = 1'b0;
logic out_req_half_i = 1'b0;
logic [11:0] out_req_addr_i = 12'd0;
logic [31:0] out_req_data_i = 32'd0;
logic [ 3:0] out_req_strb_i = 4'd0;
logic [31:0] out_read_data_o;
logic param_write_valid_i = 1'b0;
logic [12:0] param_write_addr_i = 13'd0;
logic [31:0] param_write_data_i = 32'd0;
logic [ 3:0] param_write_strb_i = 4'd0;
logic param_read_valid_i = 1'b0;
logic [12:0] param_read_addr_i = 13'd0;
logic [31:0] param_read_data_o;
logic access_err_sticky_o;
logic [ 2:0] access_err_port_o;
integer checks = 0;
always #5 clk_i = ~clk_i;
npu_local_sram u_npu_local_sram (
.clk_i (clk_i),
.rst_n_i (rst_n_i),
.clear_i (clear_i),
.raw_write_valid_i (raw_write_valid_i),
.raw_write_half_i (raw_write_half_i),
.raw_write_addr_i (raw_write_addr_i),
.raw_write_data_i (raw_write_data_i),
.raw_write_strb_i (raw_write_strb_i),
.raw_read_valid_i (raw_read_valid_i),
.raw_read_half_i (raw_read_half_i),
.raw_read_addr_i (raw_read_addr_i),
.raw_read_data_o (raw_read_data_o),
.pack_write_valid_i (pack_write_valid_i),
.pack_write_sel_w_i (pack_write_sel_w_i),
.pack_write_half_i (pack_write_half_i),
.pack_write_addr_i (pack_write_addr_i),
.pack_write_data_i (pack_write_data_i),
.pack_write_strb_i (pack_write_strb_i),
.a_read_valid_i (a_read_valid_i),
.a_read_half_i (a_read_half_i),
.a_read_addr_i (a_read_addr_i),
.a_read_data_o (a_read_data_o),
.w_read_valid_i (w_read_valid_i),
.w_read_half_i (w_read_half_i),
.w_read_addr_i (w_read_addr_i),
.w_read_data_o (w_read_data_o),
.out_req_valid_i (out_req_valid_i),
.out_req_write_i (out_req_write_i),
.out_req_half_i (out_req_half_i),
.out_req_addr_i (out_req_addr_i),
.out_req_data_i (out_req_data_i),
.out_req_strb_i (out_req_strb_i),
.out_read_data_o (out_read_data_o),
.param_write_valid_i(param_write_valid_i),
.param_write_addr_i (param_write_addr_i),
.param_write_data_i (param_write_data_i),
.param_write_strb_i (param_write_strb_i),
.param_read_valid_i (param_read_valid_i),
.param_read_addr_i (param_read_addr_i),
.param_read_data_o (param_read_data_o),
.access_err_sticky_o(access_err_sticky_o),
.access_err_port_o (access_err_port_o)
);
function automatic logic [31:0] merge32(input logic [31:0] old_i, input logic [31:0] new_i,
input logic [3:0] strb_i);
merge32 = old_i;
for (int byte_idx = 0; byte_idx < 4; byte_idx++) begin
if (strb_i[byte_idx]) merge32[byte_idx*8+:8] = new_i[byte_idx*8+:8];
end
endfunction
function automatic logic [63:0] merge64(input logic [63:0] old_i, input logic [63:0] new_i,
input logic [7:0] strb_i);
merge64 = old_i;
for (int byte_idx = 0; byte_idx < 8; byte_idx++) begin
if (strb_i[byte_idx]) merge64[byte_idx*8+:8] = new_i[byte_idx*8+:8];
end
endfunction
task automatic raw_write(input logic half_i, input logic [12:0] addr_i, input logic [31:0] data_i,
input logic [3:0] strb_i);
@(negedge clk_i);
raw_write_valid_i = 1'b1;
raw_write_half_i = half_i;
raw_write_addr_i = addr_i;
raw_write_data_i = data_i;
raw_write_strb_i = strb_i;
@(negedge clk_i);
raw_write_valid_i = 1'b0;
raw_write_strb_i = 4'd0;
endtask
task automatic raw_read_check(input logic half_i, input logic [12:0] addr_i,
input logic [31:0] expected_i);
@(negedge clk_i);
raw_read_valid_i = 1'b1;
raw_read_half_i = half_i;
raw_read_addr_i = addr_i;
@(negedge clk_i);
raw_read_valid_i = 1'b0;
if (raw_read_data_o !== expected_i) begin
$fatal(1, "raw read half=%0d addr=%h data=%h expected=%h", half_i, addr_i, raw_read_data_o,
expected_i);
end
checks = checks + 1;
endtask
task automatic pack_write(input logic sel_w_i, input logic half_i, input logic [12:0] addr_i,
input logic [63:0] data_i, input logic [7:0] strb_i);
@(negedge clk_i);
pack_write_valid_i = 1'b1;
pack_write_sel_w_i = sel_w_i;
pack_write_half_i = half_i;
pack_write_addr_i = addr_i;
pack_write_data_i = data_i;
pack_write_strb_i = strb_i;
@(negedge clk_i);
pack_write_valid_i = 1'b0;
pack_write_strb_i = 8'd0;
endtask
task automatic a_read_check(input logic half_i, input logic [12:0] addr_i,
input logic [63:0] expected_i);
@(negedge clk_i);
a_read_valid_i = 1'b1;
a_read_half_i = half_i;
a_read_addr_i = addr_i;
@(negedge clk_i);
a_read_valid_i = 1'b0;
if (a_read_data_o !== expected_i) begin
$fatal(1, "A read half=%0d addr=%h data=%h expected=%h", half_i, addr_i, a_read_data_o,
expected_i);
end
checks = checks + 1;
endtask
task automatic w_read_check(input logic half_i, input logic [12:0] addr_i,
input logic [63:0] expected_i);
@(negedge clk_i);
w_read_valid_i = 1'b1;
w_read_half_i = half_i;
w_read_addr_i = addr_i;
@(negedge clk_i);
w_read_valid_i = 1'b0;
if (w_read_data_o !== expected_i) begin
$fatal(1, "W read half=%0d addr=%h data=%h expected=%h", half_i, addr_i, w_read_data_o,
expected_i);
end
checks = checks + 1;
endtask
task automatic out_write(input logic half_i, input logic [11:0] addr_i, input logic [31:0] data_i,
input logic [3:0] strb_i);
@(negedge clk_i);
out_req_valid_i = 1'b1;
out_req_write_i = 1'b1;
out_req_half_i = half_i;
out_req_addr_i = addr_i;
out_req_data_i = data_i;
out_req_strb_i = strb_i;
@(negedge clk_i);
out_req_valid_i = 1'b0;
out_req_write_i = 1'b0;
out_req_strb_i = 4'd0;
endtask
task automatic out_read_check(input logic half_i, input logic [11:0] addr_i,
input logic [31:0] expected_i);
@(negedge clk_i);
out_req_valid_i = 1'b1;
out_req_write_i = 1'b0;
out_req_half_i = half_i;
out_req_addr_i = addr_i;
@(negedge clk_i);
out_req_valid_i = 1'b0;
if (out_read_data_o !== expected_i) begin
$fatal(1, "out read half=%0d addr=%h data=%h expected=%h", half_i, addr_i, out_read_data_o,
expected_i);
end
checks = checks + 1;
endtask
task automatic param_write(input logic [12:0] addr_i, input logic [31:0] data_i,
input logic [3:0] strb_i);
@(negedge clk_i);
param_write_valid_i = 1'b1;
param_write_addr_i = addr_i;
param_write_data_i = data_i;
param_write_strb_i = strb_i;
@(negedge clk_i);
param_write_valid_i = 1'b0;
param_write_strb_i = 4'd0;
endtask
task automatic param_read_check(input logic [12:0] addr_i, input logic [31:0] expected_i);
@(negedge clk_i);
param_read_valid_i = 1'b1;
param_read_addr_i = addr_i;
@(negedge clk_i);
param_read_valid_i = 1'b0;
if (param_read_data_o !== expected_i) begin
$fatal(1, "param read addr=%h data=%h expected=%h", addr_i, param_read_data_o, expected_i);
end
checks = checks + 1;
endtask
task automatic check_err(input logic [2:0] port_i);
if (access_err_sticky_o !== 1'b1) begin
$fatal(1, "sticky error not raised for port %0d", port_i);
end
if (access_err_port_o !== port_i) begin
$fatal(1, "error port mismatch got=%0d expected=%0d", access_err_port_o, port_i);
end
checks = checks + 1;
endtask
task automatic sram_clear;
@(negedge clk_i);
clear_i = 1'b1;
@(negedge clk_i);
clear_i = 1'b0;
if (access_err_sticky_o !== 1'b0) $fatal(1, "sticky error survived clear_i");
if (access_err_port_o !== 3'd7) $fatal(1, "error port not cleared");
checks = checks + 1;
endtask
initial begin
repeat (4) @(negedge clk_i);
rst_n_i = 1'b1;
@(negedge clk_i);
if (access_err_sticky_o !== 1'b0) $fatal(1, "sticky error after reset");
if (access_err_port_o !== 3'd7) $fatal(1, "error port not None after reset");
// ---- write/read every partition via its ports ----
raw_write(1'b0, 13'h0000, 32'h10203040, 4'hf);
raw_write(1'b0, 13'h1004, 32'h50607080, 4'hf);
raw_write(1'b1, 13'h0004, 32'h90a0b0c0, 4'hf);
raw_write(1'b1, 13'h1ffc, 32'hd0e0f000, 4'hf);
raw_read_check(1'b0, 13'h0000, 32'h10203040);
raw_read_check(1'b0, 13'h1004, 32'h50607080);
raw_read_check(1'b1, 13'h0004, 32'h90a0b0c0);
raw_read_check(1'b1, 13'h1ffc, 32'hd0e0f000);
// ---- byte strobes: every 4-bit pattern on the raw port ----
for (int pattern = 0; pattern < 16; pattern++) begin
raw_write(1'b0, 13'h0040, 32'haabbccdd, 4'hf);
raw_write(1'b0, 13'h0040, 32'h11223344, 4'(pattern));
raw_read_check(1'b0, 13'h0040, merge32(32'haabbccdd, 32'h11223344, 4'(pattern)));
end
// ---- byte strobes on the packed A 64-bit port ----
for (int pattern = 0; pattern < 256; pattern += 85) begin
pack_write(1'b0, 1'b0, 13'h0080, 64'haabbccdd11223344, 8'hff);
pack_write(1'b0, 1'b0, 13'h0080, 64'h55667788990abcde, 8'(pattern));
a_read_check(1'b0, 13'h0080, merge64(64'haabbccdd11223344, 64'h55667788990abcde, 8'(pattern)
));
end
// ---- 64-bit A/W accesses ----
pack_write(1'b0, 1'b0, 13'h0100, 64'h1122334455667788, 8'hff);
a_read_check(1'b0, 13'h0100, 64'h1122334455667788);
pack_write(1'b1, 1'b1, 13'h0180, 64'hdeadbeefcafebabe, 8'hff);
w_read_check(1'b1, 13'h0180, 64'hdeadbeefcafebabe);
// ---- two independent A+W reads in one cycle ----
@(negedge clk_i);
a_read_valid_i = 1'b1;
a_read_half_i = 1'b0;
a_read_addr_i = 13'h0100;
w_read_valid_i = 1'b1;
w_read_half_i = 1'b1;
w_read_addr_i = 13'h0180;
@(negedge clk_i);
a_read_valid_i = 1'b0;
w_read_valid_i = 1'b0;
if (a_read_data_o !== 64'h1122334455667788) $fatal(1, "concurrent A read mismatch");
if (w_read_data_o !== 64'hdeadbeefcafebabe) $fatal(1, "concurrent W read mismatch");
checks = checks + 1;
// ---- inactive-half pack write concurrent with active-half read ----
@(negedge clk_i);
a_read_valid_i = 1'b1;
a_read_half_i = 1'b0;
a_read_addr_i = 13'h0100;
pack_write_valid_i = 1'b1;
pack_write_sel_w_i = 1'b0;
pack_write_half_i = 1'b1;
pack_write_addr_i = 13'h0200;
pack_write_data_i = 64'h0123456789abcdef;
pack_write_strb_i = 8'hff;
@(negedge clk_i);
a_read_valid_i = 1'b0;
pack_write_valid_i = 1'b0;
pack_write_strb_i = 8'd0;
if (a_read_data_o !== 64'h1122334455667788) $fatal(1, "cross-half concurrent read mismatch");
if (access_err_sticky_o !== 1'b0) $fatal(1, "legal cross-half access flagged");
a_read_check(1'b1, 13'h0200, 64'h0123456789abcdef);
checks = checks + 1;
// ---- output staging both halves ----
out_write(1'b0, 12'h000, 32'h11112222, 4'hf);
out_write(1'b1, 12'hffc, 32'h33334444, 4'hf);
out_read_check(1'b0, 12'h000, 32'h11112222);
out_read_check(1'b1, 12'hffc, 32'h33334444);
// ---- parameter region both banks ----
param_write(13'h0000, 32'h55556666, 4'hf);
param_write(13'h1004, 32'h77778888, 4'hf);
param_read_check(13'h0000, 32'h55556666);
param_read_check(13'h1004, 32'h77778888);
// ---- explicit synchronous-read latency probe ----
raw_write(1'b0, 13'h0020, 32'h5555aaaa, 4'hf);
@(negedge clk_i);
raw_read_valid_i = 1'b1;
raw_read_half_i = 1'b0;
raw_read_addr_i = 13'h0020;
#1;
if (raw_read_data_o === 32'h5555aaaa) $fatal(1, "raw read is not synchronous");
@(negedge clk_i);
raw_read_valid_i = 1'b0;
if (raw_read_data_o !== 32'h5555aaaa) $fatal(1, "raw read latency not one cycle");
checks = checks + 1;
// ---- invalid-word read errors on every read port ----
sram_clear;
@(negedge clk_i);
raw_read_valid_i = 1'b1;
raw_read_half_i = 1'b1;
raw_read_addr_i = 13'h0800;
@(negedge clk_i);
raw_read_valid_i = 1'b0;
check_err(3'd1);
sram_clear;
@(negedge clk_i);
a_read_valid_i = 1'b1;
a_read_half_i = 1'b0;
a_read_addr_i = 13'h0800;
@(negedge clk_i);
a_read_valid_i = 1'b0;
check_err(3'd3);
sram_clear;
@(negedge clk_i);
w_read_valid_i = 1'b1;
w_read_half_i = 1'b1;
w_read_addr_i = 13'h1000;
@(negedge clk_i);
w_read_valid_i = 1'b0;
check_err(3'd4);
sram_clear;
@(negedge clk_i);
out_req_valid_i = 1'b1;
out_req_write_i = 1'b0;
out_req_half_i = 1'b1;
out_req_addr_i = 12'h400;
@(negedge clk_i);
out_req_valid_i = 1'b0;
check_err(3'd5);
sram_clear;
@(negedge clk_i);
param_read_valid_i = 1'b1;
param_read_addr_i = 13'h1800;
@(negedge clk_i);
param_read_valid_i = 1'b0;
check_err(3'd6);
sram_clear;
// ---- misaligned 64-bit accesses ----
@(negedge clk_i);
a_read_valid_i = 1'b1;
a_read_half_i = 1'b0;
a_read_addr_i = 13'h0004;
@(negedge clk_i);
a_read_valid_i = 1'b0;
check_err(3'd3);
sram_clear;
@(negedge clk_i);
w_read_valid_i = 1'b1;
w_read_half_i = 1'b0;
w_read_addr_i = 13'h0001;
@(negedge clk_i);
w_read_valid_i = 1'b0;
check_err(3'd4);
sram_clear;
// misaligned pack write is suppressed and flagged
pack_write(1'b0, 1'b0, 13'h0040, 64'haaaabbbbccccdddd, 8'hff);
@(negedge clk_i);
pack_write_valid_i = 1'b1;
pack_write_sel_w_i = 1'b0;
pack_write_half_i = 1'b0;
pack_write_addr_i = 13'h0042;
pack_write_data_i = 64'h1111111111111111;
pack_write_strb_i = 8'hff;
@(negedge clk_i);
pack_write_valid_i = 1'b0;
pack_write_strb_i = 8'd0;
check_err(3'd2);
a_read_check(1'b0, 13'h0040, 64'haaaabbbbccccdddd);
sram_clear;
// ---- raw same-half conflict (same bank): read wins, write dropped ----
raw_write(1'b0, 13'h0100, 32'haaaa0001, 4'hf);
raw_write(1'b0, 13'h0104, 32'haaaa0002, 4'hf);
@(negedge clk_i);
raw_write_valid_i = 1'b1;
raw_write_half_i = 1'b0;
raw_write_addr_i = 13'h0100;
raw_write_data_i = 32'h99999999;
raw_write_strb_i = 4'hf;
raw_read_valid_i = 1'b1;
raw_read_half_i = 1'b0;
raw_read_addr_i = 13'h0100;
@(negedge clk_i);
raw_write_valid_i = 1'b0;
raw_write_strb_i = 4'd0;
raw_read_valid_i = 1'b0;
if (raw_read_data_o !== 32'haaaa0001) $fatal(1, "raw read did not win the conflict");
check_err(3'd0);
raw_read_check(1'b0, 13'h0100, 32'haaaa0001);
sram_clear;
// ---- raw same-half conflict (different banks): still an error ----
raw_write(1'b0, 13'h0000, 32'hbbbb0001, 4'hf);
raw_write(1'b0, 13'h1000, 32'hbbbb0002, 4'hf);
@(negedge clk_i);
raw_write_valid_i = 1'b1;
raw_write_half_i = 1'b0;
raw_write_addr_i = 13'h1000;
raw_write_data_i = 32'h88888888;
raw_write_strb_i = 4'hf;
raw_read_valid_i = 1'b1;
raw_read_half_i = 1'b0;
raw_read_addr_i = 13'h0000;
@(negedge clk_i);
raw_write_valid_i = 1'b0;
raw_write_strb_i = 4'd0;
raw_read_valid_i = 1'b0;
if (raw_read_data_o !== 32'hbbbb0001) $fatal(1, "raw read did not win cross-bank conflict");
check_err(3'd0);
raw_read_check(1'b0, 13'h1000, 32'hbbbb0002);
sram_clear;
// ---- pack write vs a_read same-half conflict ----
pack_write(1'b0, 1'b0, 13'h0080, 64'hcccc000000000001, 8'hff);
@(negedge clk_i);
pack_write_valid_i = 1'b1;
pack_write_sel_w_i = 1'b0;
pack_write_half_i = 1'b0;
pack_write_addr_i = 13'h0080;
pack_write_data_i = 64'h7777777777777777;
pack_write_strb_i = 8'hff;
a_read_valid_i = 1'b1;
a_read_half_i = 1'b0;
a_read_addr_i = 13'h0080;
@(negedge clk_i);
pack_write_valid_i = 1'b0;
pack_write_strb_i = 8'd0;
a_read_valid_i = 1'b0;
if (a_read_data_o !== 64'hcccc000000000001) $fatal(1, "A read did not win the conflict");
check_err(3'd2);
a_read_check(1'b0, 13'h0080, 64'hcccc000000000001);
sram_clear;
// ---- pack write vs w_read same-half conflict ----
pack_write(1'b1, 1'b1, 13'h0100, 64'hdddd000000000002, 8'hff);
@(negedge clk_i);
pack_write_valid_i = 1'b1;
pack_write_sel_w_i = 1'b1;
pack_write_half_i = 1'b1;
pack_write_addr_i = 13'h0100;
pack_write_data_i = 64'h6666666666666666;
pack_write_strb_i = 8'hff;
w_read_valid_i = 1'b1;
w_read_half_i = 1'b1;
w_read_addr_i = 13'h0100;
@(negedge clk_i);
pack_write_valid_i = 1'b0;
pack_write_strb_i = 8'd0;
w_read_valid_i = 1'b0;
if (w_read_data_o !== 64'hdddd000000000002) $fatal(1, "W read did not win the conflict");
check_err(3'd2);
w_read_check(1'b1, 13'h0100, 64'hdddd000000000002);
sram_clear;
// ---- first failing port is sticky until clear_i ----
@(negedge clk_i);
raw_read_valid_i = 1'b1;
raw_read_half_i = 1'b0;
raw_read_addr_i = 13'h0400;
@(negedge clk_i);
raw_read_valid_i = 1'b0;
check_err(3'd1);
@(negedge clk_i);
a_read_valid_i = 1'b1;
a_read_half_i = 1'b0;
a_read_addr_i = 13'h0400;
@(negedge clk_i);
a_read_valid_i = 1'b0;
if (access_err_port_o !== 3'd1) $fatal(1, "first failing port not retained");
checks = checks + 1;
sram_clear;
// ---- clear_i invalidates previously written words ----
raw_write(1'b0, 13'h0300, 32'hdddd0001, 4'hf);
raw_read_check(1'b0, 13'h0300, 32'hdddd0001);
sram_clear;
@(negedge clk_i);
raw_read_valid_i = 1'b1;
raw_read_half_i = 1'b0;
raw_read_addr_i = 13'h0300;
@(negedge clk_i);
raw_read_valid_i = 1'b0;
check_err(3'd1);
sram_clear;
$display("NPU_LOCAL_SRAM_PASS checks=%0d", checks);
$finish;
end
initial begin
#5000000;
$fatal(1, "npu_local_sram_tb timeout");
end
endmodule
+151
View File
@@ -0,0 +1,151 @@
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// SPDX-License-Identifier: MulanPSL-2.0
//
// Vector file format (flat 32-bit hex words, 5 + 64 words per item, generated
// by tests/test_npu_rtl_compute.py):
// word 0: activation bytes lanes 3..0 (lane p occupies byte p)
// word 1: activation bytes lanes 7..4
// word 2: weight bytes lanes 3..0
// word 3: weight bytes lanes 7..4
// word 4: {8'h00, in_zero[7:0], a_valid[7:0], w_valid[7:0]}
// words 5..68: expected signed 17-bit products, sign-extended signed 32,
// in position-major order (index = position*8 + channel)
`timescale 1ns / 1ps
module npu_mac_array_tb;
localparam int unsigned MaxVectorCount = 256;
localparam int unsigned WordsPerItem = 5 + 64;
logic clk_i = 1'b0;
logic rst_n_i = 1'b0;
logic req_valid;
logic req_ready;
logic [ 7:0][ 7:0] req_a_bytes;
logic [ 7:0][ 7:0] req_w_bytes;
logic signed [ 7:0] req_in_zero;
logic [ 7:0] req_a_valid;
logic [ 7:0] req_w_valid;
logic resp_valid;
logic resp_ready;
logic [ 7:0][ 7:0][16:0] resp_products;
logic [31:0] vectors [0:MaxVectorCount*WordsPerItem-1];
logic [63:0][16:0] products_flat;
integer vector_count;
string vector_path;
integer stall_count;
integer error_count;
always #5 clk_i = ~clk_i;
assign products_flat = resp_products;
npu_mac_array u_dut (
.clk_i (clk_i),
.rst_n_i (rst_n_i),
.req_valid_i (req_valid),
.req_ready_o (req_ready),
.req_a_bytes_i (req_a_bytes),
.req_w_bytes_i (req_w_bytes),
.req_in_zero_i (req_in_zero),
.req_a_valid_i (req_a_valid),
.req_w_valid_i (req_w_valid),
.resp_valid_o (resp_valid),
.resp_ready_i (resp_ready),
.resp_products_o(resp_products)
);
// Deterministic response backpressure: ready is low in two-cycle stall
// stretches inside a seven-cycle period.
always @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
stall_count <= 0;
resp_ready <= 1'b0;
end else begin
stall_count <= (stall_count == 6) ? 0 : stall_count + 1;
resp_ready <= (stall_count < 2) || (stall_count > 3);
end
end
task automatic hard_reset;
begin
rst_n_i = 1'b0;
repeat (3) @(posedge clk_i);
rst_n_i = 1'b1;
repeat (2) @(posedge clk_i);
end
endtask
initial begin
integer base;
vector_count = 0;
if (!$value$plusargs("VECTOR_COUNT=%d", vector_count)) begin
$fatal(1, "NPU mac array vector count missing");
end
if (!$value$plusargs("VECTORS=%s", vector_path)) begin
$fatal(1, "NPU mac array vector path missing");
end
if ((vector_count < 1) || (vector_count > MaxVectorCount)) begin
$fatal(1, "NPU mac array vector count out of range count=%0d", vector_count);
end
$readmemh(vector_path, vectors);
req_valid = 1'b0;
req_a_bytes = '0;
req_w_bytes = '0;
req_in_zero = 8'd0;
req_a_valid = 8'd0;
req_w_valid = 8'd0;
error_count = 0;
hard_reset();
for (int item = 0; item < vector_count; item++) begin
base = item * WordsPerItem;
// Deterministic request idle gaps, zero to two cycles.
repeat (item % 3) @(posedge clk_i);
@(negedge clk_i);
req_a_bytes = {vectors[base+1], vectors[base+0]};
req_w_bytes = {vectors[base+3], vectors[base+2]};
req_in_zero = vectors[base+4][23:16];
req_a_valid = vectors[base+4][15:8];
req_w_valid = vectors[base+4][7:0];
req_valid = 1'b1;
for (int cycle = 0; cycle < 200; cycle++) begin
@(posedge clk_i);
if (req_ready) begin
@(negedge clk_i);
req_valid = 1'b0;
break;
end
if (cycle == 199) begin
$fatal(1, "NPU mac array request timeout item=%0d", item);
end
end
for (int cycle = 0; cycle < 200; cycle++) begin
@(posedge clk_i);
if (resp_valid && resp_ready) begin
for (int index = 0; index < 64; index++) begin
if (products_flat[index] !== vectors[base+5+index][16:0]) begin
$display("NPU mac array mismatch item=%0d index=%0d got=%05x expected=%05x", item,
index, products_flat[index], vectors[base+5+index][16:0]);
error_count = error_count + 1;
end
end
break;
end
if (cycle == 199) begin
$fatal(1, "NPU mac array response timeout item=%0d", item);
end
end
end
repeat (2) @(posedge clk_i);
if (error_count != 0) begin
$fatal(1, "NPU mac array mismatches=%0d", error_count);
end
$display("NPU mac array test passed");
$finish;
end
endmodule
+142
View File
@@ -0,0 +1,142 @@
`timescale 1ns / 1ps
// Vector-driven unit test for npu_patch_packer. Raw contents and expected
// packed rows come from $readmemh files selected by plusargs; the raw read
// port is modeled behaviorally with one-cycle synchronous latency and the
// packed write port is captured and compared row by row.
module npu_patch_packer_tb;
logic clk_i = 1'b0;
logic rst_n_i = 1'b0;
logic start_valid_i = 1'b0;
logic start_ready_o;
npu_pkg::pack_mode_e start_mode_i = npu_pkg::PackDense;
logic [ 10:0] start_k_i = 11'd1;
logic [ 3:0] start_positions_i = 4'd1;
logic [ 3:0] start_channels_i = 4'd1;
logic signed [ 7:0] start_in_zero_i = 8'd0;
logic busy_o;
logic done_pulse_o;
logic raw_read_valid_o;
logic [ 12:0] raw_read_addr_o;
logic [ 31:0] raw_read_data_i;
logic pack_write_valid_o;
logic [ 12:0] pack_write_addr_o;
logic [ 63:0] pack_write_data_o;
logic [ 7:0] pack_write_strb_o;
logic [ 31:0] raw_mem [0:2047];
logic [ 31:0] raw_data_q = 32'd0;
logic [ 63:0] expected_mem [0:1023];
logic [ 63:0] actual_mem [0:1023];
logic [1023:0] written = 1024'd0;
integer write_count = 0;
integer done_count = 0;
integer mode = 0;
integer k_value = 1;
integer m_value = 1;
integer channels_value = 1;
integer in_zero_value = 0;
integer rows = 0;
string raw_path;
string expected_path;
always #5 clk_i = ~clk_i;
npu_patch_packer u_npu_patch_packer (
.clk_i (clk_i),
.rst_n_i (rst_n_i),
.start_valid_i (start_valid_i),
.start_ready_o (start_ready_o),
.start_mode_i (start_mode_i),
.start_k_i (start_k_i),
.start_positions_i (start_positions_i),
.start_channels_i (start_channels_i),
.start_in_zero_i (start_in_zero_i),
.busy_o (busy_o),
.done_pulse_o (done_pulse_o),
.raw_read_valid_o (raw_read_valid_o),
.raw_read_addr_o (raw_read_addr_o),
.raw_read_data_i (raw_read_data_i),
.pack_write_valid_o(pack_write_valid_o),
.pack_write_addr_o (pack_write_addr_o),
.pack_write_data_o (pack_write_data_o),
.pack_write_strb_o (pack_write_strb_o)
);
// Raw gather half behavioral model: one-cycle synchronous read.
assign raw_read_data_i = raw_data_q;
always_ff @(posedge clk_i) begin
if (raw_read_valid_o) begin
raw_data_q <= raw_mem[raw_read_addr_o[12:2]];
end
end
// Packed write capture with protocol checks.
always_ff @(posedge clk_i) begin
if (pack_write_valid_o) begin
if (pack_write_strb_o !== 8'hff) $fatal(1, "pack write strobe not full");
if (pack_write_addr_o[2:0] != 3'd0) $fatal(1, "pack write misaligned");
if (written[pack_write_addr_o[12:3]]) $fatal(1, "duplicate pack write row");
written[pack_write_addr_o[12:3]] <= 1'b1;
actual_mem[pack_write_addr_o[12:3]] <= pack_write_data_o;
write_count <= write_count + 1;
end
if (done_pulse_o) begin
done_count <= done_count + 1;
end
end
initial begin
integer cycles;
if (!$value$plusargs("MODE=%d", mode)) $fatal(1, "MODE plusarg missing");
if (!$value$plusargs("K=%d", k_value)) $fatal(1, "K plusarg missing");
if (!$value$plusargs("M=%d", m_value)) $fatal(1, "M plusarg missing");
if (!$value$plusargs("CHANNELS=%d", channels_value)) $fatal(1, "CHANNELS plusarg missing");
if (!$value$plusargs("IN_ZERO=%d", in_zero_value)) $fatal(1, "IN_ZERO plusarg missing");
if (!$value$plusargs("ROWS=%d", rows)) $fatal(1, "ROWS plusarg missing");
if (!$value$plusargs("RAW=%s", raw_path)) $fatal(1, "RAW plusarg missing");
if (!$value$plusargs("EXPECTED=%s", expected_path)) $fatal(1, "EXPECTED plusarg missing");
$readmemh(raw_path, raw_mem);
$readmemh(expected_path, expected_mem);
repeat (4) @(negedge clk_i);
rst_n_i = 1'b1;
@(negedge clk_i);
if (start_ready_o !== 1'b1) $fatal(1, "start_ready low in Idle");
if (busy_o !== 1'b0) $fatal(1, "busy high in Idle");
start_valid_i = 1'b1;
start_mode_i = (mode == 0) ? npu_pkg::PackDense : npu_pkg::PackDepthwise;
start_k_i = 11'(k_value);
start_positions_i = 4'(m_value);
start_channels_i = 4'(channels_value);
start_in_zero_i = 8'(in_zero_value);
@(negedge clk_i);
start_valid_i = 1'b0;
if (busy_o !== 1'b1) $fatal(1, "busy not raised after start");
if (start_ready_o !== 1'b0) $fatal(1, "start_ready high while running");
cycles = 0;
while ((done_pulse_o !== 1'b1) && (cycles < 400000)) begin
@(negedge clk_i);
cycles = cycles + 1;
end
if (done_pulse_o !== 1'b1) $fatal(1, "packer timeout after %0d cycles", cycles);
@(negedge clk_i);
if (busy_o !== 1'b0) $fatal(1, "busy stuck after done");
if (done_count != 1) $fatal(1, "done pulse count %0d != 1", done_count);
if (start_ready_o !== 1'b1) $fatal(1, "start_ready not restored");
if (write_count != rows) $fatal(1, "row count %0d != expected %0d", write_count, rows);
for (int row = 0; row < rows; row++) begin
if (written[row] !== 1'b1) $fatal(1, "row %0d never written", row);
if (actual_mem[row] !== expected_mem[row]) begin
$fatal(1, "row %0d mismatch actual=%h expected=%h", row, actual_mem[row],
expected_mem[row]);
end
end
$display("NPU_PATCH_PACKER_PASS mode=%0d k=%0d m=%0d ch=%0d zero=%0d rows=%0d cycles=%0d",
mode, k_value, m_value, channels_value, in_zero_value, rows, cycles);
$finish;
end
endmodule
+171
View File
@@ -0,0 +1,171 @@
// Copyright (c) 2026 Yuchi Miao <miaoyuchi@ict.ac.cn>
// SPDX-License-Identifier: MulanPSL-2.0
//
// Vector file format (flat 32-bit hex words, eight words per item, generated
// by tests/test_npu_rtl_compute.py from scripts/npu_reference.py):
// word 0: acc (signed INT32 accumulator input)
// word 1: multiplier (nonnegative INT32)
// word 2: shift (sign-extended signed 32, low 8 bits used, range -31..30)
// word 3: zout (sign-extended signed 32, low 8 bits used, range -128..127)
// word 4: act_min (sign-extended signed 32, low 8 bits used)
// word 5: act_max (sign-extended signed 32, low 8 bits used)
// word 6: expected output byte, sign-extended signed 32 (0 on fault items)
// word 7: expected fault flag (0 or 1)
`timescale 1ns / 1ps
module npu_requantizer_tb;
localparam int unsigned MaxVectorCount = 512;
localparam int unsigned WordsPerItem = 8;
logic clk_i = 1'b0;
logic rst_n_i = 1'b0;
logic req_valid;
logic req_ready;
logic signed [31:0] req_acc;
logic [31:0] req_multiplier;
logic signed [ 7:0] req_shift;
logic signed [ 7:0] req_zout;
logic signed [ 7:0] req_act_min;
logic signed [ 7:0] req_act_max;
logic resp_valid;
logic resp_ready;
logic signed [ 7:0] resp_data;
logic resp_fault;
logic [31:0] vectors [0:MaxVectorCount*WordsPerItem-1];
integer vector_count;
string vector_path;
integer recv_count;
integer error_count;
integer stall_count;
always #5 clk_i = ~clk_i;
npu_requantizer u_dut (
.clk_i (clk_i),
.rst_n_i (rst_n_i),
.req_valid_i (req_valid),
.req_ready_o (req_ready),
.req_acc_i (req_acc),
.req_multiplier_i(req_multiplier),
.req_shift_i (req_shift),
.req_zout_i (req_zout),
.req_act_min_i (req_act_min),
.req_act_max_i (req_act_max),
.resp_valid_o (resp_valid),
.resp_ready_i (resp_ready),
.resp_data_o (resp_data),
.resp_fault_o (resp_fault)
);
// Deterministic response backpressure: ready is low in three-cycle stall
// stretches inside an eleven-cycle period.
always @(posedge clk_i or negedge rst_n_i) begin
if (!rst_n_i) begin
stall_count <= 0;
resp_ready <= 1'b0;
end else begin
stall_count <= (stall_count == 10) ? 0 : stall_count + 1;
resp_ready <= (stall_count < 4) || (stall_count > 6);
end
end
// In-order response checker: one item retires per handshake.
always @(posedge clk_i) begin
if (rst_n_i && resp_valid && resp_ready) begin
if (recv_count >= vector_count) begin
$fatal(1, "NPU requantizer published more items than sent");
end
if ((resp_data !== vectors[recv_count*WordsPerItem+6][7:0]) ||
(resp_fault !== vectors[recv_count*WordsPerItem+7][0])) begin
$display(
"NPU requantizer mismatch item=%0d got data=%08x fault=%0d expected data=%08x fault=%08x",
recv_count, resp_data, resp_fault, vectors[recv_count*WordsPerItem+6],
vectors[recv_count*WordsPerItem+7]);
error_count <= error_count + 1;
end
recv_count <= recv_count + 1;
end
end
task automatic hard_reset;
begin
rst_n_i = 1'b0;
repeat (3) @(posedge clk_i);
rst_n_i = 1'b1;
repeat (2) @(posedge clk_i);
end
endtask
initial begin
vector_count = 0;
if (!$value$plusargs("VECTOR_COUNT=%d", vector_count)) begin
$fatal(1, "NPU requantizer vector count missing");
end
if (!$value$plusargs("VECTORS=%s", vector_path)) begin
$fatal(1, "NPU requantizer vector path missing");
end
if ((vector_count < 1) || (vector_count > MaxVectorCount)) begin
$fatal(1, "NPU requantizer vector count out of range count=%0d", vector_count);
end
$readmemh(vector_path, vectors);
req_valid = 1'b0;
req_acc = 32'd0;
req_multiplier = 32'd0;
req_shift = 8'd0;
req_zout = 8'd0;
req_act_min = 8'd0;
req_act_max = 8'd0;
recv_count = 0;
error_count = 0;
hard_reset();
for (int item = 0; item < vector_count; item++) begin
// Deterministic request idle gaps, zero to three cycles.
repeat (item % 4) @(posedge clk_i);
@(negedge clk_i);
req_acc = vectors[item*WordsPerItem+0];
req_multiplier = vectors[item*WordsPerItem+1];
req_shift = vectors[item*WordsPerItem+2][7:0];
req_zout = vectors[item*WordsPerItem+3][7:0];
req_act_min = vectors[item*WordsPerItem+4][7:0];
req_act_max = vectors[item*WordsPerItem+5][7:0];
req_valid = 1'b1;
for (int cycle = 0; cycle < 200; cycle++) begin
@(posedge clk_i);
if (req_ready) begin
@(negedge clk_i);
req_valid = 1'b0;
break;
end
if (cycle == 199) begin
$fatal(1, "NPU requantizer request timeout item=%0d", item);
end
end
end
for (int cycle = 0; cycle < 20000; cycle++) begin
@(posedge clk_i);
if (recv_count == vector_count) begin
break;
end
if (cycle == 19999) begin
$fatal(1, "NPU requantizer response timeout sent=%0d received=%0d", vector_count,
recv_count);
end
end
repeat (2) @(posedge clk_i);
if (recv_count != vector_count) begin
$fatal(1, "NPU requantizer item count mismatch sent=%0d received=%0d", vector_count,
recv_count);
end
if (error_count != 0) begin
$fatal(1, "NPU requantizer mismatches=%0d", error_count);
end
$display("NPU requantizer test passed");
$finish;
end
endmodule
+301
View File
@@ -0,0 +1,301 @@
`timescale 1ns / 1ps
// Scripted unit test for npu_vector: bias preload, lane-masked checked
// accumulation against TB-computed golden sums, exactly-once drain with
// backpressure, scalar aux max/sum (including all-negative and ties), aux and
// accumulation overflow faults, and clear_i release.
module npu_vector_tb;
logic clk_i = 1'b0;
logic rst_n_i = 1'b0;
logic clear_i = 1'b0;
logic start_valid_i = 1'b0;
logic start_ready_o;
logic [ 7:0][31:0] start_bias_i = '0;
logic dw_valid_i = 1'b0;
logic dw_ready_o;
logic [ 7:0][ 7:0] dw_a_bytes_i = '0;
logic [ 7:0][ 7:0] dw_w_bytes_i = '0;
logic signed [ 7:0] dw_in_zero_i = 8'd0;
logic [ 7:0] dw_lane_valid_i = 8'd0;
logic aux_valid_i = 1'b0;
logic [ 1:0] aux_op_i = 2'd0;
logic signed [ 7:0] aux_data_i = 8'd0;
logic drain_valid_i = 1'b0;
logic drain_data_valid_o;
logic drain_data_ready_i = 1'b0;
logic signed [31:0] drain_data_o;
logic drain_last_o;
logic signed [31:0] aux_result_o;
logic busy_o;
logic fault_sticky_o;
logic [ 2:0] fault_lane_o;
integer checks = 0;
logic signed [63:0] tb_sum [0:7];
always #5 clk_i = ~clk_i;
npu_vector u_npu_vector (
.clk_i (clk_i),
.rst_n_i (rst_n_i),
.clear_i (clear_i),
.start_valid_i (start_valid_i),
.start_ready_o (start_ready_o),
.start_bias_i (start_bias_i),
.dw_valid_i (dw_valid_i),
.dw_ready_o (dw_ready_o),
.dw_a_bytes_i (dw_a_bytes_i),
.dw_w_bytes_i (dw_w_bytes_i),
.dw_in_zero_i (dw_in_zero_i),
.dw_lane_valid_i (dw_lane_valid_i),
.aux_valid_i (aux_valid_i),
.aux_op_i (aux_op_i),
.aux_data_i (aux_data_i),
.drain_valid_i (drain_valid_i),
.drain_data_valid_o(drain_data_valid_o),
.drain_data_ready_i(drain_data_ready_i),
.drain_data_o (drain_data_o),
.drain_last_o (drain_last_o),
.aux_result_o (aux_result_o),
.busy_o (busy_o),
.fault_sticky_o (fault_sticky_o),
.fault_lane_o (fault_lane_o)
);
task automatic vec_start(input logic signed [31:0] bias_i[0:7]);
@(negedge clk_i);
start_valid_i = 1'b1;
for (int lane = 0; lane < 8; lane++) begin
start_bias_i[lane] = bias_i[lane];
// Explicit sign extension: sv2v drops task-argument signedness.
tb_sum[lane] = {{32{bias_i[lane][31]}}, bias_i[lane]};
end
@(negedge clk_i);
start_valid_i = 1'b0;
if (busy_o !== 1'b1) $fatal(1, "busy not set after start");
if (dw_ready_o !== 1'b1) $fatal(1, "dw_ready not set after start");
if (aux_result_o !== 32'd0) $fatal(1, "aux sum register not cleared at start");
checks = checks + 1;
endtask
task automatic dw_row(input logic [63:0] a_i, input logic [63:0] w_i,
input logic signed [7:0] zero_i, input logic [7:0] lanes_i);
logic signed [ 8:0] a9;
logic signed [ 7:0] w8;
logic signed [31:0] prod;
@(negedge clk_i);
dw_valid_i = 1'b1;
dw_a_bytes_i = a_i;
dw_w_bytes_i = w_i;
dw_in_zero_i = zero_i;
dw_lane_valid_i = lanes_i;
@(negedge clk_i);
dw_valid_i = 1'b0;
dw_lane_valid_i = 8'd0;
for (int lane = 0; lane < 8; lane++) begin
if (lanes_i[lane]) begin
a9 = 9'($signed(a_i[lane*8+:8])) - 9'($signed(zero_i));
w8 = $signed(w_i[lane*8+:8]);
prod = a9 * w8;
tb_sum[lane] = tb_sum[lane] + prod;
if ((tb_sum[lane] > 64'sd2147483647) || (tb_sum[lane] < -64'sd2147483648)) begin
$fatal(1, "TB golden overflow lane %0d", lane);
end
end
end
checks = checks + 1;
endtask
// Drives a row that must raise the sticky fault: the DUT freezes the sums,
// so the golden model intentionally does not update.
task automatic dw_row_fault(input logic [63:0] a_i, input logic [63:0] w_i,
input logic signed [7:0] zero_i, input logic [7:0] lanes_i);
@(negedge clk_i);
dw_valid_i = 1'b1;
dw_a_bytes_i = a_i;
dw_w_bytes_i = w_i;
dw_in_zero_i = zero_i;
dw_lane_valid_i = lanes_i;
@(negedge clk_i);
dw_valid_i = 1'b0;
dw_lane_valid_i = 8'd0;
checks = checks + 1;
endtask
task automatic drain_check;
integer idx;
logic stalled;
@(negedge clk_i);
drain_valid_i = 1'b1;
@(negedge clk_i);
drain_valid_i = 1'b0;
if (drain_data_valid_o !== 1'b1) $fatal(1, "drain did not start");
idx = 0;
stalled = 1'b0;
while (idx < 8) begin
if (drain_data_o !== 32'(tb_sum[idx])) begin
$fatal(1, "drain data mismatch lane=%0d got=%h expected=%h", idx, drain_data_o,
32'(tb_sum[idx]));
end
if (drain_last_o !== (idx == 7)) $fatal(1, "drain_last mismatch at beat %0d", idx);
if ((idx % 3 == 1) && !stalled) begin
drain_data_ready_i = 1'b0;
@(negedge clk_i);
if (drain_data_valid_o !== 1'b1) $fatal(1, "drain valid dropped during stall");
if (drain_data_o !== 32'(tb_sum[idx])) $fatal(1, "drain data changed during stall");
stalled = 1'b1;
end else begin
drain_data_ready_i = 1'b1;
stalled = 1'b0;
idx = idx + 1;
@(negedge clk_i);
end
end
drain_data_ready_i = 1'b0;
if (drain_data_valid_o !== 1'b0) $fatal(1, "drain did not complete");
if (busy_o !== 1'b0) $fatal(1, "context not freed after final drain handshake");
if (start_ready_o !== 1'b1) $fatal(1, "start_ready not restored after drain");
checks = checks + 1;
endtask
task automatic aux_op(input logic [1:0] op_i, input logic signed [7:0] data_i);
@(negedge clk_i);
aux_valid_i = 1'b1;
aux_op_i = op_i;
aux_data_i = data_i;
@(negedge clk_i);
aux_valid_i = 1'b0;
aux_op_i = 2'd0;
checks = checks + 1;
endtask
task automatic aux_check(input logic signed [31:0] expected_i);
if (aux_result_o !== expected_i) begin
$fatal(1, "aux result mismatch got=%0d expected=%0d", aux_result_o, expected_i);
end
checks = checks + 1;
endtask
task automatic vec_clear;
@(negedge clk_i);
clear_i = 1'b1;
@(negedge clk_i);
clear_i = 1'b0;
if (busy_o !== 1'b0) $fatal(1, "busy stuck after clear_i");
if (fault_sticky_o !== 1'b0) $fatal(1, "fault stuck after clear_i");
if (start_ready_o !== 1'b1) $fatal(1, "start_ready low after clear_i");
if (aux_result_o !== 32'd0) $fatal(1, "aux not cleared by clear_i");
checks = checks + 1;
endtask
initial begin
logic signed [31:0] bias[0:7];
repeat (4) @(negedge clk_i);
rst_n_i = 1'b1;
@(negedge clk_i);
if (start_ready_o !== 1'b1) $fatal(1, "start_ready low after reset");
if (busy_o !== 1'b0) $fatal(1, "busy high after reset");
if (fault_sticky_o !== 1'b0) $fatal(1, "fault high after reset");
if (dw_ready_o !== 1'b0) $fatal(1, "dw_ready high in Idle");
if (drain_data_valid_o !== 1'b0) $fatal(1, "drain valid in Idle");
// ---- scalar aux in Idle: max with negatives and ties, then sum ----
aux_op(2'd1, 8'sd3);
aux_check(32'd3);
aux_op(2'd1, -8'sd5);
aux_check(32'd3);
aux_op(2'd1, 8'sd3);
aux_check(32'd3);
aux_op(2'd1, -8'sd128);
aux_check(32'd3);
aux_op(2'd1, 8'sd7);
aux_check(32'd7);
aux_op(2'd0, 8'sd100);
aux_check(32'd7);
aux_op(2'd2, 8'sd10);
aux_check(32'd10);
aux_op(2'd2, -8'sd20);
aux_check(-32'd10);
aux_op(2'd2, 8'sd100);
aux_check(32'd90);
// ---- bias preload and lane-masked accumulation ----
bias = '{-32'd5, 32'd0, 32'd100, -32'd1000, 32'd7, 32'd64, -32'd1, 32'd255};
vec_start(bias);
if (aux_result_o !== 32'd0) $fatal(1, "start did not re-initialize aux");
dw_row(64'h0102030405060708, 64'h0202020202020202, 8'sd0, 8'hff);
dw_row(64'h7f80817f0080807f, 64'h8080808080808080, 8'sd0, 8'ha5);
dw_row(64'h0000000000000000, 64'h7f7f7f7f7f7f7f7f, -8'sd128, 8'h0f);
dw_row(64'hffffffffffffffff, 64'h0101010101010101, 8'sd127, 8'h00);
dw_row(64'h8080808080808080, 64'h8181818181818181, 8'sd127, 8'hff);
drain_check;
// ---- second context: extreme centering and single-lane rows ----
bias = '{32'd0, -32'd32640, 32'd32640, 32'd1, -32'd1, 32'd1000000, -32'd999, 32'd42};
vec_start(bias);
dw_row(64'h0000000000000080, 64'h0000000000000080, 8'sd127, 8'h80);
dw_row(64'h000000000000007f, 64'h0000000000000080, -8'sd128, 8'h01);
drain_check;
// ---- accumulation overflow: single faulting lane ----
bias = '{32'd0, 32'd0, 32'd0, 32'h7ffffff0, 32'd0, 32'd0, 32'd0, 32'd0};
vec_start(bias);
dw_row_fault(64'h0000000064000000, 64'h0000000064000000, 8'sd0, 8'h08);
if (fault_sticky_o !== 1'b1) $fatal(1, "accumulation overflow fault missing");
if (fault_lane_o !== 3'd3) $fatal(1, "fault lane %0d != 3", fault_lane_o);
if (busy_o !== 1'b1) $fatal(1, "fault context not busy");
if (dw_ready_o !== 1'b0) $fatal(1, "dw_ready high in Fault");
@(negedge clk_i);
drain_valid_i = 1'b1;
@(negedge clk_i);
drain_valid_i = 1'b0;
if (drain_data_valid_o !== 1'b0) $fatal(1, "drain accepted in Fault");
vec_clear;
// ---- accumulation overflow: lowest of two faulting lanes wins ----
bias = '{32'd0, 32'h7fffffff, 32'd0, 32'd0, 32'd0, 32'h7ffffffe, 32'd0, 32'd0};
vec_start(bias);
dw_row_fault(64'h0000010000000100, 64'h0000020000000100, 8'sd0, 8'h22);
if (fault_sticky_o !== 1'b1) $fatal(1, "two-lane overflow fault missing");
if (fault_lane_o !== 3'd1) $fatal(1, "lowest fault lane %0d != 1", fault_lane_o);
vec_clear;
// ---- aux sum positive overflow: dedicated fault-lane encoding ----
bias = '{32'd0, 32'd0, 32'd0, 32'd0, 32'd0, 32'd0, 32'd0, 32'd0};
vec_start(bias);
aux_op(2'd2, 8'sd127);
aux_check(32'd127);
force u_npu_vector.s_aux_sum_q = 32'h7ffffff8;
@(negedge clk_i);
release u_npu_vector.s_aux_sum_q;
aux_op(2'd2, 8'sd127);
if (fault_sticky_o !== 1'b1) $fatal(1, "aux positive overflow fault missing");
if (fault_lane_o !== 3'd7) $fatal(1, "aux fault lane encoding %0d != 7", fault_lane_o);
vec_clear;
// ---- aux sum negative overflow ----
vec_start(bias);
force u_npu_vector.s_aux_sum_q = -32'h7ffffff8;
@(negedge clk_i);
release u_npu_vector.s_aux_sum_q;
aux_op(2'd2, -8'sd128);
if (fault_sticky_o !== 1'b1) $fatal(1, "aux negative overflow fault missing");
if (fault_lane_o !== 3'd7) $fatal(1, "aux fault lane encoding %0d != 7", fault_lane_o);
vec_clear;
// ---- drain request in Idle is ignored ----
@(negedge clk_i);
drain_valid_i = 1'b1;
@(negedge clk_i);
drain_valid_i = 1'b0;
if (drain_data_valid_o !== 1'b0) $fatal(1, "drain accepted in Idle");
if (busy_o !== 1'b0) $fatal(1, "Idle drain request disturbed the context");
$display("NPU_VECTOR_PASS checks=%0d", checks);
$finish;
end
initial begin
#2000000;
$fatal(1, "npu_vector_tb timeout");
end
endmodule
+344
View File
@@ -0,0 +1,344 @@
"""NPU-P1 compute-core RTL differential tests on Icarus Verilog and Verilator.
Golden vectors are generated at test time from the pinned numerical oracle in
scripts/npu_reference.py, so the RTL is checked bit-exactly against numerical
profile 1. Both simulators must pass; missing tools fail the test loudly.
"""
from __future__ import annotations
import os
import random
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
from npu_reference import ( # noqa: E402
INT32_MAX,
INT32_MIN,
ArithmeticFault,
multiply_by_quantized_multiplier,
)
MULTIMEDIA = ROOT / "rtl/ip/multimedia"
NPU_PKG = MULTIMEDIA / "npu_pkg.sv"
MAC_TB = ROOT / "tests/rtl/npu_mac_array_tb.sv"
ACC_TB = ROOT / "tests/rtl/npu_accumulator_tb.sv"
REQ_TB = ROOT / "tests/rtl/npu_requantizer_tb.sv"
MAC_PASS_MARKER = "NPU mac array test passed"
ACC_PASS_MARKER = "NPU accumulator test passed"
REQ_PASS_MARKER = "NPU requantizer test passed"
def _to_int8(value: int) -> int:
return value - 256 if value >= 128 else value
def _require_tools() -> tuple[str, str, str, str]:
tools = {
"iverilog": shutil.which("iverilog"),
"vvp": shutil.which("vvp"),
"sv2v": shutil.which("sv2v"),
"verilator": shutil.which("verilator"),
}
missing = [name for name, path in tools.items() if path is None]
if missing:
pytest.fail(f"NPU RTL compute tests require missing tools: {', '.join(missing)}")
return tools["iverilog"], tools["vvp"], tools["sv2v"], tools["verilator"]
def _run_icarus(
tmp_path: Path,
iverilog: str,
vvp: str,
sv2v: str,
top: str,
sources: list[Path],
plusargs: list[str],
) -> str:
del sv2v # convt_sv2v.py resolves the executable itself
source_list = tmp_path / f"{top}.fl"
source_list.write_text(
"\n".join(
["+define+SV_ASSRT_DISABLE", *(str(source) for source in sources), ""]
),
encoding="utf-8",
)
converted = tmp_path / f"{top}.v"
subprocess.run(
[
sys.executable,
str(ROOT / "rtl/mini/script/convt_sv2v.py"),
"-f",
str(source_list),
"--output",
str(converted),
],
check=True,
)
simulation = tmp_path / f"{top}_iverilog"
subprocess.run(
[iverilog, "-g2012", "-s", top, "-o", str(simulation), str(converted)],
check=True,
)
result = subprocess.run(
[vvp, str(simulation), *plusargs],
check=True,
text=True,
capture_output=True,
)
return result.stdout
def _run_verilator(
tmp_path: Path, verilator: str, top: str, sources: list[Path], plusargs: list[str]
) -> str:
object_dir = tmp_path / f"{top}_obj"
ccache_dir = tmp_path / f"{top}_ccache"
ccache_dir.mkdir()
subprocess.run(
[
verilator,
"--binary",
"--timing",
"-Wno-fatal",
"--top-module",
top,
"-Mdir",
str(object_dir),
"-o",
"simv",
*(str(source) for source in sources),
],
check=True,
text=True,
capture_output=True,
env={
**os.environ,
"CCACHE_DIR": str(ccache_dir),
"CCACHE_TEMPDIR": str(ccache_dir),
},
)
result = subprocess.run(
[str(object_dir / "simv"), *plusargs],
check=True,
text=True,
capture_output=True,
)
return result.stdout
def _run_dual(
tmp_path: Path, top: str, rtl: list[Path], plusargs: list[str], marker: str
) -> None:
iverilog, vvp, sv2v, verilator = _require_tools()
sources = [NPU_PKG, *rtl]
icarus_stdout = _run_icarus(
tmp_path, iverilog, vvp, sv2v, top, sources + [ROOT / "tests/rtl" / f"{top}.sv"], plusargs
)
assert marker in icarus_stdout, f"Icarus run of {top} did not pass"
verilator_stdout = _run_verilator(
tmp_path, verilator, top, sources + [ROOT / "tests/rtl" / f"{top}.sv"], plusargs
)
assert marker in verilator_stdout, f"Verilator run of {top} did not pass"
def _mac_expected(
a_bytes: list[int], w_bytes: list[int], in_zero: int, a_valid: int, w_valid: int
) -> list[int]:
products = []
for position in range(8):
centered = _to_int8(a_bytes[position]) - in_zero
for channel in range(8):
if ((a_valid >> position) & 1) and ((w_valid >> channel) & 1):
products.append(centered * _to_int8(w_bytes[channel]))
else:
products.append(0)
return products
def _mac_vectors(tmp_path: Path) -> tuple[Path, int]:
"""Write 5 + 64 words per item; see tests/rtl/npu_mac_array_tb.sv for the format."""
rng = random.Random(0x9C1A55)
items: list[tuple[list[int], list[int], int, int, int]] = []
directed = [
([0x00] * 8, [0x00] * 8, 0, 0xFF, 0xFF),
([0xFF] * 8, [0xFF] * 8, 0, 0xFF, 0xFF),
([0xFF] * 8, [0xFF] * 8, -128, 0xFF, 0xFF),
([0x00] * 8, [0x80] * 8, 127, 0xFF, 0xFF),
([0x00] * 8, [0x7F] * 8, -128, 0xFF, 0xFF),
([0x80] * 8, [0x7F] * 8, 127, 0xFF, 0xFF),
([0x7F] * 8, [0x80] * 8, -128, 0xFF, 0xFF),
([0x53] * 8, [0xD6] * 8, 83, 0xFF, 0xFF),
([0x2C] * 8, [0x19] * 8, 83, 0xFF, 0xFF),
([0x11, 0xEE, 0x5A, 0xA5, 0x00, 0xFF, 0x7F, 0x80],
[0x01, 0xFE, 0x40, 0xC0, 0x10, 0xF0, 0x08, 0x88], 0, 0xFF, 0xFF),
([0x40] * 8, [0x40] * 8, 0, 0x00, 0xFF),
([0x40] * 8, [0x40] * 8, 0, 0xFF, 0x00),
([0x40] * 8, [0x40] * 8, 0, 0x00, 0x00),
([0x55] * 8, [0x2A] * 8, -5, 0x01, 0xFF),
([0x55] * 8, [0x2A] * 8, -5, 0x7F, 0xFF),
([0x55] * 8, [0x2A] * 8, -5, 0xFF, 0x01),
([0x55] * 8, [0x2A] * 8, -5, 0xFF, 0x7F),
([0x55] * 8, [0x2A] * 8, -5, 0x80, 0x80),
([0x55] * 8, [0x2A] * 8, -5, 0xAA, 0x55),
([0x55] * 8, [0x2A] * 8, -5, 0xFF, 0x09),
]
items.extend(directed)
for _ in range(40):
items.append(
(
[rng.randrange(256) for _ in range(8)],
[rng.randrange(256) for _ in range(8)],
rng.randint(-128, 127),
rng.randrange(256),
rng.randrange(256),
)
)
words: list[int] = []
for a_bytes, w_bytes, in_zero, a_valid, w_valid in items:
a_packed = sum(byte << (8 * lane) for lane, byte in enumerate(a_bytes))
w_packed = sum(byte << (8 * lane) for lane, byte in enumerate(w_bytes))
flags = ((in_zero & 0xFF) << 16) | (a_valid << 8) | w_valid
words.append(a_packed & 0xFFFFFFFF)
words.append((a_packed >> 32) & 0xFFFFFFFF)
words.append(w_packed & 0xFFFFFFFF)
words.append((w_packed >> 32) & 0xFFFFFFFF)
words.append(flags)
for product in _mac_expected(a_bytes, w_bytes, in_zero, a_valid, w_valid):
words.append(product & 0xFFFFFFFF)
vector_path = tmp_path / "npu_mac_array_vectors.hex"
vector_path.write_text("\n".join(f"{word:08x}" for word in words) + "\n", encoding="utf-8")
return vector_path, len(items)
def _requantizer_expected(
acc: int, multiplier: int, shift: int, zout: int, act_min: int, act_max: int
) -> tuple[int, int]:
try:
quantized = multiply_by_quantized_multiplier(acc, multiplier, shift)
except ArithmeticFault:
return 0, 1
return max(act_min, min(act_max, quantized + zout)), 0
def _requantizer_vectors(tmp_path: Path) -> tuple[Path, int]:
"""Write 8 words per item; see tests/rtl/npu_requantizer_tb.sv for the format."""
rng = random.Random(0x5EED1234)
full = (-128, 127)
cases: list[tuple[int, int, int, int, int, int]] = [
# acc, multiplier, shift, zout, act_min, act_max
(0, 1 << 30, 0, 0, *full),
(1, 1 << 30, 0, 0, *full),
(-1, 1 << 30, 0, 0, *full),
(INT32_MAX, INT32_MAX, 0, 0, *full),
(INT32_MIN, INT32_MAX, 0, 0, *full),
(INT32_MIN, INT32_MAX, -1, 0, *full),
(INT32_MIN, 1 << 30, 0, 0, *full),
(12345, 0, 0, 0, *full), # flushed multiplier (m = 0)
(-54321, 0, 0, 5, *full),
(1000, 0x50000000, -31, 0, *full),
(1000, 0x50000000, -1, 0, *full),
(1000, 0x50000000, 1, 0, *full),
(3, 0x60000000, 30, 0, *full),
# RoundingDivideByPOT negative ties and rounding directions.
(5, INT32_MAX, -2, 0, *full),
(-5, INT32_MAX, -2, 0, *full),
(7, INT32_MAX, -3, 0, *full),
(-7, INT32_MAX, -3, 0, *full),
(3, 1 << 29, 0, 0, *full),
(-3, 1 << 29, 0, 0, *full),
(1, 1 << 29, -1, 0, *full),
(-1, 1 << 29, -1, 0, *full),
(INT32_MIN, INT32_MAX, -31, 0, *full),
(INT32_MAX, INT32_MAX, -31, 0, *full),
# Checked left shift: boundaries and overflows.
(0x40000000, 1 << 30, 2, 0, *full), # 2^30 << 2 overflows
(INT32_MAX, 1 << 30, 1, 0, *full),
(INT32_MIN, 1 << 30, 1, 0, *full),
(-(1 << 30), 1 << 30, 1, 0, *full), # exactly -2^31, no fault
(-(1 << 30), 1 << 30, 2, 0, *full), # -2^32, fault
(1, 1 << 30, 30, 0, *full),
(-1, 1 << 30, 30, 0, *full),
# Output zero-point edges and activation clamps.
(0, 1 << 30, 0, -128, *full),
(0, 1 << 30, 0, 127, *full),
(-200, INT32_MAX, 0, 127, *full),
(200, INT32_MAX, 0, -128, *full),
(100000, INT32_MAX, 0, 0, -10, 10),
(-100000, INT32_MAX, 0, 0, -10, 10),
(50, INT32_MAX, 0, 0, 0, 0),
(-50, INT32_MAX, 0, 3, 3, 3),
(0, INT32_MAX, 0, 127, -128, 0),
(0, INT32_MAX, 0, -128, 0, 127),
]
for _ in range(200):
act_low = rng.randint(-128, 127)
act_high = rng.randint(-128, 127)
cases.append(
(
rng.randint(INT32_MIN, INT32_MAX),
rng.choice(
[0, 1, rng.randint(0, INT32_MAX), rng.randint(1 << 29, INT32_MAX), INT32_MAX]
),
rng.randint(-31, 30),
rng.randint(-128, 127),
min(act_low, act_high),
max(act_low, act_high),
)
)
words: list[int] = []
for acc, multiplier, shift, zout, act_min, act_max in cases:
expected, fault = _requantizer_expected(acc, multiplier, shift, zout, act_min, act_max)
words.extend(
value & 0xFFFFFFFF
for value in (acc, multiplier, shift, zout, act_min, act_max, expected, fault)
)
vector_path = tmp_path / "npu_requantizer_vectors.hex"
vector_path.write_text("\n".join(f"{word:08x}" for word in words) + "\n", encoding="utf-8")
return vector_path, len(cases)
def test_npu_mac_array_dual_simulator(tmp_path: Path) -> None:
vector_path, vector_count = _mac_vectors(tmp_path)
_run_dual(
tmp_path,
"npu_mac_array_tb",
[MULTIMEDIA / "npu_mac_array.sv"],
[f"+VECTORS={vector_path}", f"+VECTOR_COUNT={vector_count}"],
MAC_PASS_MARKER,
)
def test_npu_accumulator_dual_simulator(tmp_path: Path) -> None:
_run_dual(
tmp_path,
"npu_accumulator_tb",
[MULTIMEDIA / "npu_accumulator.sv"],
[],
ACC_PASS_MARKER,
)
def test_npu_requantizer_dual_simulator(tmp_path: Path) -> None:
vector_path, vector_count = _requantizer_vectors(tmp_path)
assert vector_count > 200
_run_dual(
tmp_path,
"npu_requantizer_tb",
[MULTIMEDIA / "npu_requantizer.sv"],
[f"+VECTORS={vector_path}", f"+VECTOR_COUNT={vector_count}"],
REQ_PASS_MARKER,
)
+290
View File
@@ -0,0 +1,290 @@
"""NPU-P1 memory, patch packer and vector RTL unit tests (Icarus and Verilator)."""
from __future__ import annotations
import os
import random
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
MULTIMEDIA = ROOT / "rtl/ip/multimedia"
SRAM_MODEL_ROOT = ROOT / "physical/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_sram/verilog"
SRAM_MACRO_MODELS = [
SRAM_MODEL_ROOT / "RM_IHPSG13_1P_core_behavioral_bm_bist.v",
SRAM_MODEL_ROOT / "RM_IHPSG13_1P_1024x32_c2_bm_bist.v",
]
BEHAV_DEFINES = ("PDK_BEHAV",)
MACRO_DEFINES = ("PDK_IHP130", "HAVE_SRAM_MACRO", "FUNCTIONAL", "SYNTHESIS")
def _tools() -> dict[str, str]:
tools: dict[str, str] = {}
missing = []
for name in ("iverilog", "vvp", "sv2v", "verilator"):
path = shutil.which(name)
if path is None:
missing.append(name)
else:
tools[name] = path
if missing:
pytest.fail(f"required simulation tools missing: {', '.join(missing)}")
return tools
def _build_iverilog(
tools: dict[str, str],
tmp_path: Path,
name: str,
sources: list[Path],
top: str,
defines: tuple[str, ...],
extra_sources: list[Path] | None = None,
) -> Path:
filelist = tmp_path / f"{name}.fl"
filelist.write_text(
"\n".join(
[
*(f"+define+{define}" for define in defines),
"+define+SV_ASSRT_DISABLE",
*(str(source) for source in sources),
"",
]
),
encoding="utf-8",
)
converted = tmp_path / f"{name}.v"
subprocess.run(
[
sys.executable,
str(ROOT / "rtl/mini/script/convt_sv2v.py"),
"-f",
str(filelist),
"--output",
str(converted),
],
check=True,
)
simulation = tmp_path / f"{name}.iverilog"
command = [
tools["iverilog"],
"-g2012",
"-s",
top,
"-o",
str(simulation),
str(converted),
]
if extra_sources:
command.extend(("-DFUNCTIONAL", "-DSYNTHESIS"))
command.extend(str(source) for source in extra_sources)
subprocess.run(command, check=True)
return simulation
def _build_verilator(
tools: dict[str, str],
tmp_path: Path,
name: str,
sources: list[Path],
top: str,
defines: tuple[str, ...],
) -> Path:
object_dir = tmp_path / f"obj-{name}"
ccache_dir = tmp_path / f"ccache-{name}"
ccache_dir.mkdir()
subprocess.run(
[
tools["verilator"],
"--binary",
"--timing",
"-Wno-fatal",
"--top-module",
top,
*(f"+define+{define}" for define in defines),
*(str(source) for source in sources),
"-Mdir",
str(object_dir),
"-o",
"simv",
],
check=True,
text=True,
capture_output=True,
env={
**os.environ,
"CCACHE_DIR": str(ccache_dir),
"CCACHE_TEMPDIR": str(ccache_dir),
},
)
return object_dir / "simv"
def _build(
tools: dict[str, str],
simulator: str,
tmp_path: Path,
name: str,
sources: list[Path],
top: str,
defines: tuple[str, ...],
extra_sources: list[Path] | None = None,
) -> list[str]:
if simulator == "iverilog":
simulation = _build_iverilog(
tools, tmp_path, name, sources, top, defines, extra_sources
)
return [tools["vvp"], str(simulation)]
if simulator == "verilator":
simv = _build_verilator(
tools, tmp_path, name, [*(extra_sources or []), *sources], top, defines
)
return [str(simv)]
raise AssertionError(f"unknown simulator {simulator}")
def _run(command: list[str], plusargs: tuple[str, ...] = ()) -> str:
result = subprocess.run(
[*command, *plusargs], check=True, text=True, capture_output=True
)
return result.stdout
@pytest.mark.parametrize("simulator", ("iverilog", "verilator"))
@pytest.mark.parametrize("variant", ("behav", "macro"))
def test_npu_local_sram(tmp_path: Path, simulator: str, variant: str) -> None:
tools = _tools()
for model in SRAM_MACRO_MODELS:
assert model.is_file()
sources = [
MULTIMEDIA / "npu_pkg.sv",
ROOT / "rtl/tech/tc_sram.sv",
MULTIMEDIA / "npu_local_sram.sv",
ROOT / "tests/rtl/npu_local_sram_tb.sv",
]
defines = BEHAV_DEFINES if variant == "behav" else MACRO_DEFINES
extra = [] if variant == "behav" else SRAM_MACRO_MODELS
command = _build(
tools,
simulator,
tmp_path,
f"npu_local_sram_{variant}",
sources,
"npu_local_sram_tb",
defines,
extra_sources=extra,
)
assert "NPU_LOCAL_SRAM_PASS" in _run(command)
def _pack_golden(mode: int, k: int, m: int, channels: int, in_zero: int, raw: bytes) -> list[int]:
fill = in_zero & 0xFF
rows = []
if mode == 0:
assert len(raw) == m * k
for row in range(k):
value = 0
for lane in range(8):
byte = raw[lane * k + row] if lane < m else fill
value |= byte << (8 * lane)
rows.append(value)
else:
assert len(raw) == m * k * 8
for row in range(m * k):
value = 0
for lane in range(8):
byte = raw[row * 8 + lane] if lane < channels else fill
value |= byte << (8 * lane)
rows.append(value)
return rows
def _packer_cases() -> list[tuple[int, int, int, int, int]]:
cases = []
for m in (1, 7, 8):
for k in (1, 9, 1024):
for in_zero in (-128, 0, 83, 127):
cases.append((0, k, m, 8, in_zero))
for m, channels in ((1, 8), (3, 5)):
for in_zero in (-128, 0, 83, 127):
cases.append((1, 9, m, channels, in_zero))
return cases
def _packer_plusargs(
tmp_path: Path, case_index: int, case: tuple[int, int, int, int, int]
) -> tuple[str, ...]:
mode, k, m, channels, in_zero = case
rng = random.Random(0x5EED0000 + case_index)
raw_byte_count = m * k if mode == 0 else m * k * 8
raw = bytes(rng.randrange(256) for _ in range(raw_byte_count))
words = [0] * ((raw_byte_count + 3) // 4)
for index, byte in enumerate(raw):
words[index // 4] |= byte << (8 * (index % 4))
raw_path = tmp_path / f"packer_raw_{case_index}.hex"
raw_path.write_text("\n".join(f"{word:08x}" for word in words) + "\n", encoding="utf-8")
rows = _pack_golden(mode, k, m, channels, in_zero, raw)
expected_path = tmp_path / f"packer_expected_{case_index}.hex"
expected_path.write_text(
"\n".join(f"{row:016x}" for row in rows) + "\n", encoding="utf-8"
)
return (
f"+MODE={mode}",
f"+K={k}",
f"+M={m}",
f"+CHANNELS={channels}",
f"+IN_ZERO={in_zero & 0xFF}",
f"+ROWS={len(rows)}",
f"+RAW={raw_path}",
f"+EXPECTED={expected_path}",
)
@pytest.mark.parametrize("simulator", ("iverilog", "verilator"))
def test_npu_patch_packer(tmp_path: Path, simulator: str) -> None:
tools = _tools()
sources = [
MULTIMEDIA / "npu_pkg.sv",
MULTIMEDIA / "npu_patch_packer.sv",
ROOT / "tests/rtl/npu_patch_packer_tb.sv",
]
command = _build(
tools,
simulator,
tmp_path,
"npu_patch_packer",
sources,
"npu_patch_packer_tb",
BEHAV_DEFINES,
)
cases = _packer_cases()
assert len(cases) == 44
for case_index, case in enumerate(cases):
plusargs = _packer_plusargs(tmp_path, case_index, case)
output = _run(command, plusargs)
assert "NPU_PATCH_PACKER_PASS" in output, f"case {case} failed"
@pytest.mark.parametrize("simulator", ("iverilog", "verilator"))
def test_npu_vector(tmp_path: Path, simulator: str) -> None:
tools = _tools()
sources = [
MULTIMEDIA / "npu_pkg.sv",
MULTIMEDIA / "npu_vector.sv",
ROOT / "tests/rtl/npu_vector_tb.sv",
]
command = _build(
tools,
simulator,
tmp_path,
"npu_vector",
sources,
"npu_vector_tb",
BEHAV_DEFINES,
)
assert "NPU_VECTOR_PASS" in _run(command)