feat: first genuine N=2 multi-core system, two real bugs found+fixed (EXP-0066)

New sdram_slot_arbiter2.v + tb_np_director_n2_system.v: real
neural_director_packed.v dispatching to 2 real packed_slot.v
instances sharing one real SDRAM controller. Jobs submitted one at a
time through the Director's own producer interface -- the Director's
own scheduling decisions determine slot assignment here, unlike every
prior V3 test.

Bug 1 (real, structural): the arbiter's first design registered its
grant one cycle late; layer_prefetch_ctrl.v's ctrl_req is a one-shot
pulse with no retry (every prior use wired it directly to a
controller, never behind arbitration), so a slot's first request
could be silently lost, hanging it forever. Fixed with a new
S_MEMWAIT state in packed_slot.v (wait for a combinational mem_grant
before ever pulsing layer_prefetch_ctrl's start) and a combinational-
first grant in the arbiter.

Bug 2 (testbench): node_id used a stray bit-slice (li[15:8]) instead
of a real multiply, making every layer produce the same node_ids and
silently checking results against the wrong layer's golden value.
Fixed.

Result: 12/12 PASS, 0 errors, real concurrent execution across both
slots (slot 0: positions {0,1,4,5,8,9}, slot 1: {2,3,6,7,10,11}).

Also noted (user correction): the SDR SDRAM controller used
throughout this memory path is a declared placeholder -- the real
target is DDR3 on a custom XC7A100T board, not yet built.

Full writeup in hardware/v2/logs/experiments.log EXP-0066.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
This commit is contained in:
2026-09-17 00:16:47 +02:00
co-authored by Claude Sonnet 5
parent 124a0dbca0
commit 09fbf01ea5
5 changed files with 597 additions and 10 deletions
+73
View File
@@ -4038,3 +4038,76 @@ only) and a real multi-core system.
next_action: wire N=2 packed_slot.v instances behind a shared SDRAM next_action: wire N=2 packed_slot.v instances behind a shared SDRAM
arbiter, driven by neural_director_packed.v, for the first genuine arbiter, driven by neural_director_packed.v, for the first genuine
multi-core system correctness test. multi-core system correctness test.
EXP-0066 -- first genuine N=2 multi-core system, real correctness
verification, two real integration bugs found and fixed (2026-09-17)
CONTEXT: EXP-0065's own next_action -- the final piece before a real
multi-core V3 system: N real packed_slot.v instances sharing ONE real
SDRAM controller, dispatched by the already-isolated-verified neural_
director_packed.v (EXP-0064). Note (user correction, same session):
the SDRAM controller used throughout this whole memory-path (EXP-0057
onward, reused unmodified) is a DECLARED PLACEHOLDER -- XC7A100T was
chosen specifically for real DDR3 support, this is a from-scratch
custom board (bare chip, not a dev board), and a real DDR3/MIG
interface is separate, not-yet-started work. Everything in this and
prior V3 memory-path experiments proves the COMPUTE/SCHEDULING
architecture, independent of the final physical memory technology.
METHOD: new hardware/v3/rtl/sdram_slot_arbiter2.v (2-way arbiter,
locks for a slot's whole multi-burst fetch via a new mem_active/
mem_grant handshake, not per-transaction) + hardware/v3/sim/
tb_np_director_n2_system.v (real neural_director_packed.v, 2 real
packed_slot.v instances, real sdram_controller.v+sdram_model.v shared
through the arbiter, jobs submitted ONE AT A TIME through the
Director's own producer interface -- unlike every prior V3 test, the
Director's OWN scheduling decisions determine which physical slot runs
which pair here).
BUG 1 (real, found via hierarchical dir_state/q_count/slot-state/pf-
state tracing): the arbiter's first design registered its grant
decision (valid only the cycle AFTER a slot's mem_active first went
high). layer_prefetch_ctrl.v issues ctrl_req as a genuine ONE-SHOT
pulse with NO retry -- every prior use of that module (EXP-0057
onward) wired it DIRECTLY to a controller with no arbitration delay
possible, so it was never designed to tolerate a late grant. The
result: a slot's very first ctrl_req could fire before the arbiter
had actually granted it the bus, that pulse was silently lost forever,
and the slot hung permanently in layer_prefetch_ctrl's own S_WAIT
state waiting for a ctrl_ready that would never come. FIX (structural,
not a timing patch): packed_slot.v gained a new S_MEMWAIT state
between job dispatch and S_PREFETCH -- it now asserts mem_active
(a "want the bus" signal) and WAITS for a new combinational mem_grant
input from the arbiter before ever pulsing layer_prefetch_ctrl's
start. The arbiter's own grant decision was also made combinational
(available the SAME cycle mem_active first asserts, not one cycle
later), with a registered "locked" bit only to keep the choice sticky
once made, never to delay the first grant.
BUG 2 (real, in the testbench, not the DUT): node_id was computed as
`li_i[15:8]*M + pp_i` (a stray bit-slice copied from EXP-0062's own
formula) -- for small li values (0,1,2) bits [15:8] are always 0, so
EVERY layer produced the SAME node_ids (0..3), and the scoreboard's
"first matching expect_node" lookup silently checked completed jobs
against layer 0's own expected values regardless of which layer
actually ran. Fixed: `li_i*M + pp_i` (real multiply, genuinely unique
per position).
RESULT (after both fixes): 12/12 PASS, 0 errors, all 12 positions (3
layers x 4 positions) across both physical slots, bit-exact against
an independent golden model. Real observed interleaving: slot 0 ran
positions {0,1,4,5,8,9}, slot 1 ran {2,3,6,7,10,11} -- genuine
concurrent multi-core execution, not sequential.
DECISION: this is the first real, correctness-verified V3 multi-core
system -- Director, N real compute+memory slots, shared arbitrated
SDRAM path, all composing correctly. The architecture (DSP packing,
weight-reuse, Director pairing, per-slot sequencing, shared-arbiter
memory) is now proven end-to-end at N=2.
next_action: (1) a real P&R Fmax number for this N=2 system (still not
measured -- EXP-0059/0060/0063's own isolated-core numbers are not
system-representative); (2) scale to the real target N (up to ~30
packed cores per EXP-0059's own DSP budget projection) once N=2's real
timing is known; (3) the real DDR3/MIG memory interface, replacing the
SDR SDRAM placeholder used throughout -- separate, larger, not started.
+33 -8
View File
@@ -71,6 +71,12 @@ module packed_slot #(
output reg [ADDR_WIDTH-1:0] result_addr_a_out, output reg [ADDR_WIDTH-1:0] result_addr_a_out,
output reg [ADDR_WIDTH-1:0] result_addr_b_out, output reg [ADDR_WIDTH-1:0] result_addr_b_out,
// high exactly while this slot needs exclusive access to the
// shared SDRAM controller (its own weight-fetch phase) -- a
// shared-controller arbiter uses this to lock a grant for the
// whole multi-burst fetch, not just one transaction.
output wire mem_active,
// ---- activation stand-in port (see header -- real fetch engine // ---- activation stand-in port (see header -- real fetch engine
// deferred) ---- // deferred) ----
output reg [ADDR_WIDTH-1:0] act_tile_addr_a, output reg [ADDR_WIDTH-1:0] act_tile_addr_a,
@@ -78,6 +84,17 @@ module packed_slot #(
input wire signed [DATA_WIDTH*P_IN-1:0] act_tile_data_a, input wire signed [DATA_WIDTH*P_IN-1:0] act_tile_data_a,
input wire signed [DATA_WIDTH*P_IN-1:0] act_tile_data_b, input wire signed [DATA_WIDTH*P_IN-1:0] act_tile_data_b,
// grant from a shared-controller arbiter (see mem_active's own
// comment): must be asserted before this slot may pulse its own
// layer_prefetch_ctrl.v start, since that module's ctrl_req is a
// one-shot pulse with no retry -- issuing it before the arbiter
// has actually granted this slot the bus loses it permanently
// (found empirically integrating N=2 slots behind sdram_slot_
// arbiter2.v: a slot could hang forever in S_WAIT with ctrl_req
// already dropped and ctrl_ready never coming). Tie high for a
// single-slot (N=1, no arbiter) system.
input wire mem_grant,
// ---- SDRAM controller port (connects directly, or through a // ---- SDRAM controller port (connects directly, or through a
// shared arbiter for N>1 slots) ---- // shared arbiter for N>1 slots) ----
output wire ctrl_req, output wire ctrl_req,
@@ -90,16 +107,18 @@ module packed_slot #(
input wire ctrl_busy input wire ctrl_busy
); );
localparam S_IDLE = 4'd0, localparam S_IDLE = 4'd0,
S_PREFETCH = 4'd1, S_MEMWAIT = 4'd1,
S_SWAP = 4'd2, S_PREFETCH = 4'd2,
S_JOBSTART = 4'd3, S_SWAP = 4'd3,
S_TILEREQ = 4'd4, S_JOBSTART = 4'd4,
S_TILEWAIT = 4'd5, S_TILEREQ = 4'd5,
S_OPERAND = 4'd6, S_TILEWAIT = 4'd6,
S_RESULT = 4'd7, S_OPERAND = 4'd7,
S_DONE = 4'd8; S_RESULT = 4'd8,
S_DONE = 4'd9;
reg [3:0] state; reg [3:0] state;
assign mem_active = (state == S_MEMWAIT) || (state == S_PREFETCH);
reg [ADDR_WIDTH-1:0] w_base_lat, x_base_a_lat, x_base_b_lat; reg [ADDR_WIDTH-1:0] w_base_lat, x_base_a_lat, x_base_b_lat;
reg [15:0] n_tiles_lat; reg [15:0] n_tiles_lat;
reg [ADDR_WIDTH-1:0] result_addr_a_lat, result_addr_b_lat; reg [ADDR_WIDTH-1:0] result_addr_a_lat, result_addr_b_lat;
@@ -221,6 +240,12 @@ module packed_slot #(
node_id_b_lat <= node_id_b; node_id_b_lat <= node_id_b;
job_bias <= {DATA_WIDTH{1'b0}}; job_bias <= {DATA_WIDTH{1'b0}};
job_activation <= ACT_RELU; job_activation <= ACT_RELU;
state <= S_MEMWAIT;
end
end
S_MEMWAIT: begin
if (mem_grant) begin
pf_start <= 1'b1; pf_start <= 1'b1;
state <= S_PREFETCH; state <= S_PREFETCH;
end end
+115
View File
@@ -0,0 +1,115 @@
`timescale 1ns/1ps
// ============================================================
// V3 -- 2-way arbiter between packed_slot.v's own weight-fetch ctrl
// port and ONE real, shared sdram_controller.v.
//
// Grants LOCK for the whole duration of a slot's mem_active (its
// entire multi-burst layer fetch), not per-transaction -- a slot's
// own layer_prefetch_ctrl.v issues MANY back-to-back ctrl_req bursts
// per fetch, and interleaving those with the OTHER slot's bursts
// would corrupt both (neither is designed to have its own multi-burst
// sequence interrupted mid-flight). First-active-wins priority; the
// other slot's ctrl_ready is held at 0 (never pulses) while not
// granted, so its own req/ready FSM simply waits, harmlessly, exactly
// like it already does for ordinary controller busy cycles.
// ============================================================
module sdram_slot_arbiter2 #(
parameter ADDR_WIDTH = 25,
parameter BURST_LEN = 8
)(
input wire clk,
input wire rst,
input wire slot0_active,
output wire slot0_grant,
input wire slot0_req,
input wire slot0_wr,
input wire [ADDR_WIDTH-1:0] slot0_addr,
input wire [16*BURST_LEN-1:0] slot0_wdata,
input wire [2*BURST_LEN-1:0] slot0_wmask,
output wire [16*BURST_LEN-1:0] slot0_rdata,
output wire slot0_ready,
output wire slot0_busy,
input wire slot1_active,
output wire slot1_grant,
input wire slot1_req,
input wire slot1_wr,
input wire [ADDR_WIDTH-1:0] slot1_addr,
input wire [16*BURST_LEN-1:0] slot1_wdata,
input wire [2*BURST_LEN-1:0] slot1_wmask,
output wire [16*BURST_LEN-1:0] slot1_rdata,
output wire slot1_ready,
output wire slot1_busy,
output wire ctrl_req,
output wire ctrl_wr,
output wire [ADDR_WIDTH-1:0] ctrl_addr,
output wire [16*BURST_LEN-1:0] ctrl_wdata,
output wire [2*BURST_LEN-1:0] ctrl_wmask,
input wire [16*BURST_LEN-1:0] ctrl_rdata,
input wire ctrl_ready,
input wire ctrl_busy
);
// grant_now is COMBINATIONAL, not registered: layer_prefetch_ctrl.v
// issues ctrl_req as a genuine one-shot pulse (it has only ever
// been used wired DIRECTLY to a controller before this arbiter --
// EXP-0057/58/62/65 -- so it assumes immediate visibility, not a
// registered/one-cycle-late grant). A purely-registered arbiter
// (grant decided AT the clock edge, valid only the FOLLOWING
// cycle) misses that first pulse entirely -- found empirically:
// slot1 hung forever in its own S_WAIT state, ctrl_req correctly
// pulsed for exactly one cycle then dropped, but the registered
// grant hadn't caught up yet, so the real controller never saw it
// and ctrl_ready never came. `locked`/`grant_reg` below only
// LATCH a decision already available combinationally this same
// cycle, purely to keep it sticky once BOTH slots are active
// (prevents switching mid-fetch), never to delay the FIRST grant.
reg locked;
reg grant_reg;
wire grant_now = locked ? grant_reg : (slot0_active ? 1'b0 : 1'b1);
wire either_active = slot0_active || slot1_active;
always @(posedge clk) begin
if (rst) begin
locked <= 1'b0;
grant_reg<= 1'b0;
end else begin
if (!locked) begin
if (either_active) begin
locked <= 1'b1;
grant_reg <= grant_now;
end
end else begin
if (grant_reg == 1'b0 && !slot0_active) locked <= 1'b0;
if (grant_reg == 1'b1 && !slot1_active) locked <= 1'b0;
end
end
end
wire sel0 = either_active && (grant_now == 1'b0);
wire sel1 = either_active && (grant_now == 1'b1);
// combinational grant feedback: a slot must see its OWN grant
// asserted (in response to its own mem_active going high, same
// cycle) before it may pulse layer_prefetch_ctrl.v's one-shot
// ctrl_req -- see packed_slot.v's own S_MEMWAIT state.
assign slot0_grant = sel0;
assign slot1_grant = sel1;
assign ctrl_req = sel0 ? slot0_req : (sel1 ? slot1_req : 1'b0);
assign ctrl_wr = sel0 ? slot0_wr : (sel1 ? slot1_wr : 1'b0);
assign ctrl_addr = sel0 ? slot0_addr : (sel1 ? slot1_addr : {ADDR_WIDTH{1'b0}});
assign ctrl_wdata = sel0 ? slot0_wdata : (sel1 ? slot1_wdata : {(16*BURST_LEN){1'b0}});
assign ctrl_wmask = sel0 ? slot0_wmask : (sel1 ? slot1_wmask : {(2*BURST_LEN){1'b0}});
assign slot0_rdata = ctrl_rdata;
assign slot0_ready = sel0 ? ctrl_ready : 1'b0;
assign slot0_busy = sel0 ? ctrl_busy : 1'b1;
assign slot1_rdata = ctrl_rdata;
assign slot1_ready = sel1 ? ctrl_ready : 1'b0;
assign slot1_busy = sel1 ? ctrl_busy : 1'b1;
endmodule
+373
View File
@@ -0,0 +1,373 @@
`timescale 1ns/1ps
// ============================================================
// First genuine multi-core (N=2) system correctness test: real
// neural_director_packed.v (EXP-0064) dispatching to TWO real
// packed_slot.v instances (EXP-0065), sharing ONE real SDRAM
// controller through sdram_slot_arbiter2.v. All real RTL except the
// activation stand-in (same disclosed scope as EXP-0065/packed_slot.v
// itself).
//
// Jobs are submitted ONE AT A TIME through the Director's own
// job_in_* producer interface (mimicking a host/dependency manager),
// letting the Director do its own pairing (matching w_base) and
// first-free-slot dispatch -- unlike EXP-0062/0065's own tests, which
// drove pairs/slots directly. This is the first test where the
// Director's OWN scheduling decisions (verified in isolation,
// EXP-0064) determine which physical slot executes which pair.
// ============================================================
module tb;
localparam BURST_LEN = 8;
localparam ROW_BITS = 13;
localparam COL_BITS = 10;
localparam BANK_BITS = 2;
localparam SDRAM_ADDR_WIDTH = BANK_BITS + ROW_BITS + COL_BITS; // 25
localparam CLK_FREQ_MHZ = 64;
localparam CLK_PERIOD_NS = 1000.0/CLK_FREQ_MHZ;
localparam DATA_WIDTH = 8;
localparam P_IN = 8;
localparam ACC_WIDTH = 32;
localparam ADDR_WIDTH = 26;
localparam N_INPUTS = 128;
localparam N_TILES = N_INPUTS/P_IN;
localparam LAYER_BYTES = N_INPUTS;
localparam WORDS_PER_LAYER = LAYER_BYTES/2;
localparam N_SLOTS = 2;
localparam QUEUE_DEPTH = 8;
localparam L = 3; // layers
localparam M = 4; // reuse positions per layer, paired 2 at a time
reg clk = 0;
always #(CLK_PERIOD_NS/2.0) clk = ~clk;
reg rst;
integer cyc;
always @(posedge clk) if (!rst) cyc <= cyc + 1;
// ---- real SDRAM controller + model, shared via the arbiter ----
wire ctrl_req, ctrl_wr;
wire [SDRAM_ADDR_WIDTH-1:0] ctrl_addr;
wire [16*BURST_LEN-1:0] ctrl_wdata;
wire [2*BURST_LEN-1:0] ctrl_wmask;
wire [16*BURST_LEN-1:0] ctrl_rdata;
wire ctrl_ready, ctrl_busy;
wire cke, cs_n, ras_n, cas_n, we_n;
wire [BANK_BITS-1:0] ba;
wire [ROW_BITS-1:0] a;
wire [15:0] dq;
wire [1:0] dqm;
reg wpre_req, wpre_wr;
reg [SDRAM_ADDR_WIDTH-1:0] wpre_addr;
reg [16*BURST_LEN-1:0] wpre_wdata;
reg pre_active;
wire arb_ctrl_req, arb_ctrl_wr;
wire [SDRAM_ADDR_WIDTH-1:0] arb_ctrl_addr;
wire [16*BURST_LEN-1:0] arb_ctrl_wdata;
wire [2*BURST_LEN-1:0] arb_ctrl_wmask;
assign ctrl_req = pre_active ? wpre_req : arb_ctrl_req;
assign ctrl_wr = pre_active ? wpre_wr : arb_ctrl_wr;
assign ctrl_addr = pre_active ? wpre_addr : arb_ctrl_addr;
assign ctrl_wdata = pre_active ? wpre_wdata : arb_ctrl_wdata;
assign ctrl_wmask = pre_active ? {(2*BURST_LEN){1'b0}} : arb_ctrl_wmask;
sdram_controller #(
.CLK_FREQ_MHZ(CLK_FREQ_MHZ), .BURST_LEN(BURST_LEN),
.ROW_BITS(ROW_BITS), .COL_BITS(COL_BITS), .BANK_BITS(BANK_BITS)
) u_ctrl (
.clk(clk), .rst(rst),
.req(ctrl_req), .wr(ctrl_wr), .addr(ctrl_addr), .wdata(ctrl_wdata), .wmask(ctrl_wmask),
.rdata(ctrl_rdata), .ready(ctrl_ready), .busy(ctrl_busy),
.sdram_cke(cke), .sdram_cs_n(cs_n), .sdram_ras_n(ras_n), .sdram_cas_n(cas_n), .sdram_we_n(we_n),
.sdram_ba(ba), .sdram_a(a), .sdram_dq(dq), .sdram_dqm(dqm)
);
sdram_model #(
.CLK_FREQ_MHZ(CLK_FREQ_MHZ), .ROW_BITS(ROW_BITS), .COL_BITS(COL_BITS), .BANK_BITS(BANK_BITS)
) u_mem (
.clk(clk), .cke(cke), .cs_n(cs_n), .ras_n(ras_n), .cas_n(cas_n), .we_n(we_n),
.ba(ba), .a(a), .dq(dq), .dqm(dqm)
);
function automatic signed [7:0] weight_byte(input integer li, input integer t);
weight_byte = $signed(8'((li*17 + t*29 + 13) & 8'hFF));
endfunction
function automatic signed [7:0] input_byte(input integer li, input integer pos, input integer t);
input_byte = $signed(8'((li*11 + pos*41 + t*7 + 3) & 8'hFF));
endfunction
task automatic sdram_write_burst(input [SDRAM_ADDR_WIDTH-1:0] word_addr, input [16*BURST_LEN-1:0] data);
begin
@(posedge clk); while (ctrl_busy) @(posedge clk);
wpre_req = 1'b1; wpre_wr = 1'b1; wpre_addr = word_addr; wpre_wdata = data;
@(posedge clk); wpre_req = 1'b0;
while (!ctrl_ready) @(posedge clk);
end
endtask
task automatic preload_sdram_layers;
integer li, bi, wb, tt;
reg [16*BURST_LEN-1:0] burst_data;
begin
for (li = 0; li < L; li = li + 1) begin
for (bi = 0; bi < (LAYER_BYTES/(2*BURST_LEN)); bi = bi + 1) begin
for (wb = 0; wb < BURST_LEN; wb = wb + 1) begin
tt = bi*(2*BURST_LEN) + wb*2;
burst_data[wb*16 +: 16] = {weight_byte(li, tt+1), weight_byte(li, tt)};
end
sdram_write_burst((li*WORDS_PER_LAYER + bi*BURST_LEN), burst_data);
end
end
end
endtask
function automatic signed [DATA_WIDTH*P_IN-1:0] act_lookup(input [ADDR_WIDTH-1:0] addr);
integer li_d, pos_d, tidx_d, k;
reg signed [DATA_WIDTH*P_IN-1:0] r;
begin
li_d = addr / 100000;
pos_d = (addr / 1000) % 100;
tidx_d = addr % 1000;
for (k = 0; k < P_IN; k = k + 1)
r[k*DATA_WIDTH +: DATA_WIDTH] = input_byte(li_d, pos_d, tidx_d*P_IN + k);
act_lookup = r;
end
endfunction
// ---- neural_director_packed.v ----
reg job_in_valid;
wire job_in_ready;
reg [ADDR_WIDTH-1:0] job_in_x_base, job_in_w_base, job_in_result_addr;
reg [15:0] job_in_n_tiles, job_in_node_id;
wire [N_SLOTS-1:0] slot_job_start;
wire [ADDR_WIDTH*N_SLOTS-1:0] slot_x_base_a, slot_x_base_b, slot_w_base;
wire [ADDR_WIDTH*N_SLOTS-1:0] slot_result_addr_a, slot_result_addr_b;
wire [16*N_SLOTS-1:0] slot_n_tiles, slot_node_id_a, slot_node_id_b;
wire [N_SLOTS-1:0] slot_job_done;
wire job_out_done;
wire [$clog2(N_SLOTS)-1:0] job_out_slot;
wire [3:0] dir_state;
wire dir_error;
neural_director_packed #(
.ADDR_WIDTH(ADDR_WIDTH), .N_SLOTS(N_SLOTS), .QUEUE_DEPTH(QUEUE_DEPTH)
) u_dir (
.clk(clk), .rst(rst),
.job_in_valid(job_in_valid), .job_in_ready(job_in_ready),
.job_in_x_base(job_in_x_base), .job_in_w_base(job_in_w_base),
.job_in_n_tiles(job_in_n_tiles), .job_in_result_addr(job_in_result_addr),
.job_in_node_id(job_in_node_id),
.slot_job_start(slot_job_start),
.slot_x_base_a(slot_x_base_a), .slot_x_base_b(slot_x_base_b),
.slot_w_base(slot_w_base), .slot_n_tiles(slot_n_tiles),
.slot_result_addr_a(slot_result_addr_a), .slot_result_addr_b(slot_result_addr_b),
.slot_node_id_a(slot_node_id_a), .slot_node_id_b(slot_node_id_b),
.slot_job_done(slot_job_done),
.job_out_done(job_out_done), .job_out_slot(job_out_slot),
.dir_state(dir_state), .dir_error(dir_error)
);
// ---- 2 real packed_slot.v instances + arbiter ----
wire [1:0] mem_active;
wire [1:0] mem_grant;
wire [1:0] s_ctrl_req, s_ctrl_wr;
wire [SDRAM_ADDR_WIDTH-1:0] s_ctrl_addr [0:1];
wire [16*BURST_LEN-1:0] s_ctrl_wdata [0:1];
wire [2*BURST_LEN-1:0] s_ctrl_wmask [0:1];
wire [16*BURST_LEN-1:0] s_ctrl_rdata [0:1];
wire [1:0] s_ctrl_ready, s_ctrl_busy;
sdram_slot_arbiter2 #(.ADDR_WIDTH(SDRAM_ADDR_WIDTH), .BURST_LEN(BURST_LEN)) u_arb (
.clk(clk), .rst(rst),
.slot0_active(mem_active[0]), .slot0_grant(mem_grant[0]), .slot0_req(s_ctrl_req[0]), .slot0_wr(s_ctrl_wr[0]),
.slot0_addr(s_ctrl_addr[0]), .slot0_wdata(s_ctrl_wdata[0]), .slot0_wmask(s_ctrl_wmask[0]),
.slot0_rdata(s_ctrl_rdata[0]), .slot0_ready(s_ctrl_ready[0]), .slot0_busy(s_ctrl_busy[0]),
.slot1_active(mem_active[1]), .slot1_grant(mem_grant[1]), .slot1_req(s_ctrl_req[1]), .slot1_wr(s_ctrl_wr[1]),
.slot1_addr(s_ctrl_addr[1]), .slot1_wdata(s_ctrl_wdata[1]), .slot1_wmask(s_ctrl_wmask[1]),
.slot1_rdata(s_ctrl_rdata[1]), .slot1_ready(s_ctrl_ready[1]), .slot1_busy(s_ctrl_busy[1]),
.ctrl_req(arb_ctrl_req), .ctrl_wr(arb_ctrl_wr), .ctrl_addr(arb_ctrl_addr),
.ctrl_wdata(arb_ctrl_wdata), .ctrl_wmask(arb_ctrl_wmask),
.ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy)
);
genvar gi;
generate
for (gi = 0; gi < N_SLOTS; gi = gi + 1) begin : GEN_SLOT
wire signed [DATA_WIDTH-1:0] res_a, res_b;
wire [15:0] res_nid_a, res_nid_b;
wire [ADDR_WIDTH-1:0] res_addr_a_out, res_addr_b_out;
wire [ADDR_WIDTH-1:0] act_addr_a, act_addr_b;
wire signed [DATA_WIDTH*P_IN-1:0] act_data_a, act_data_b;
assign act_data_a = act_lookup(act_addr_a);
assign act_data_b = act_lookup(act_addr_b);
packed_slot #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH),
.BURST_LEN(BURST_LEN), .ADDR_WIDTH(ADDR_WIDTH), .LAYER_BYTES(LAYER_BYTES)
) u_slot (
.clk(clk), .rst(rst),
.job_start(slot_job_start[gi]),
.x_base_a(slot_x_base_a[gi*ADDR_WIDTH +: ADDR_WIDTH]),
.x_base_b(slot_x_base_b[gi*ADDR_WIDTH +: ADDR_WIDTH]),
.w_base(slot_w_base[gi*ADDR_WIDTH +: ADDR_WIDTH]),
.n_tiles(slot_n_tiles[gi*16 +: 16]),
.result_addr_a(slot_result_addr_a[gi*ADDR_WIDTH +: ADDR_WIDTH]),
.result_addr_b(slot_result_addr_b[gi*ADDR_WIDTH +: ADDR_WIDTH]),
.node_id_a(slot_node_id_a[gi*16 +: 16]), .node_id_b(slot_node_id_b[gi*16 +: 16]),
.job_done(slot_job_done[gi]),
.result_data_a(res_a), .result_data_b(res_b),
.result_node_id_a(res_nid_a), .result_node_id_b(res_nid_b),
.result_addr_a_out(res_addr_a_out), .result_addr_b_out(res_addr_b_out),
.mem_active(mem_active[gi]), .mem_grant(mem_grant[gi]),
.act_tile_addr_a(act_addr_a), .act_tile_addr_b(act_addr_b),
.act_tile_data_a(act_data_a), .act_tile_data_b(act_data_b),
.ctrl_req(s_ctrl_req[gi]), .ctrl_wr(s_ctrl_wr[gi]), .ctrl_addr(s_ctrl_addr[gi]),
.ctrl_wdata(s_ctrl_wdata[gi]), .ctrl_wmask(s_ctrl_wmask[gi]),
.ctrl_rdata(s_ctrl_rdata[gi]), .ctrl_ready(s_ctrl_ready[gi]), .ctrl_busy(s_ctrl_busy[gi])
);
end
endgenerate
integer errors, tests;
task automatic submit_job(
input [ADDR_WIDTH-1:0] xb, input [ADDR_WIDTH-1:0] wb,
input [15:0] nt, input [ADDR_WIDTH-1:0] resaddr, input [15:0] nid
);
begin
@(posedge clk);
job_in_x_base = xb; job_in_w_base = wb; job_in_n_tiles = nt;
job_in_result_addr = resaddr; job_in_node_id = nid;
job_in_valid = 1'b1;
while (!job_in_ready) @(posedge clk);
@(posedge clk);
job_in_valid = 1'b0;
end
endtask
// ---- scoreboard: golden result per node_id, checked whenever
// EITHER slot's own job_done pulses (watching both slots directly,
// not just the Director's own lowest-index-wins job_out_done,
// per DEC-0007's own documented simplification) ----
reg [15:0] expect_node [0:63];
reg signed [7:0] expect_val [0:63];
integer n_expected;
function automatic signed [7:0] golden_result(input integer li, input integer pos);
integer t, acc;
reg signed [7:0] r;
begin
acc = 0;
for (t = 0; t < N_INPUTS; t = t + 1)
acc = acc + (input_byte(li, pos, t) * weight_byte(li, t));
if (acc <= 0) r = 0; else if (acc > 127) r = 8'sd127; else r = acc[7:0];
golden_result = r;
end
endfunction
integer completions;
integer si;
// Runs from time 0, independent of the main submission flow below
// -- a slot's job_done is a ONE-CYCLE pulse, and with QUEUE_DEPTH
// smaller than the total job count, early pairs can complete WHILE
// later jobs are still being submitted; a watcher that only starts
// AFTER all submissions finish would miss those pulses entirely
// (found empirically: only 2/12 results ever got checked, root-
// caused via hierarchical dir_state/q_count/slot state tracing
// showing the system genuinely idle by the time the old watcher
// loop started -- the real completions had already come and gone,
// unobserved).
always @(posedge clk) begin
if (!rst) begin
for (si = 0; si < N_SLOTS; si = si + 1) begin
if (slot_job_done[si]) begin
completions = completions + 2; // covers both A and B
case (si)
0: begin
check_completion(0, GEN_SLOT[0].u_slot.result_node_id_a, GEN_SLOT[0].u_slot.result_data_a);
check_completion(0, GEN_SLOT[0].u_slot.result_node_id_b, GEN_SLOT[0].u_slot.result_data_b);
end
1: begin
check_completion(1, GEN_SLOT[1].u_slot.result_node_id_a, GEN_SLOT[1].u_slot.result_data_a);
check_completion(1, GEN_SLOT[1].u_slot.result_node_id_b, GEN_SLOT[1].u_slot.result_data_b);
end
endcase
end
end
end
end
task automatic check_completion(input integer slot, input [15:0] nid, input signed [7:0] val);
integer idx, found;
begin
found = 0;
for (idx = 0; idx < n_expected; idx = idx + 1) begin
if (expect_node[idx] === nid && !found) begin
found = 1;
tests = tests + 1;
if (expect_val[idx] !== val) begin
$display("FAIL slot=%0d node_id=%0d: got=%0d expected=%0d", slot, nid, $signed(val), $signed(expect_val[idx]));
errors = errors + 1;
end else begin
$display("PASS slot=%0d node_id=%0d: result=%0d", slot, nid, $signed(val));
end
end
end
if (!found) begin
$display("FAIL slot=%0d node_id=%0d: completed but was NOT an expected pending job", slot, nid);
errors = errors + 1;
tests = tests + 1;
end
end
endtask
integer li_i, pp_i, wd;
initial begin
errors = 0; tests = 0; cyc = 0; n_expected = 0; completions = 0;
rst = 1; pre_active = 1'b1;
wpre_req = 0; wpre_wr = 0; wpre_addr = 0; wpre_wdata = 0;
job_in_valid = 0; job_in_x_base = 0; job_in_w_base = 0;
job_in_n_tiles = 0; job_in_result_addr = 0; job_in_node_id = 0;
repeat(5) @(posedge clk);
rst = 0;
@(posedge clk); while (ctrl_busy) @(posedge clk);
$display("=== preload SDRAM with %0d resident-filter weight sets ===", L);
preload_sdram_layers;
@(posedge clk);
pre_active = 1'b0;
$display("=== N=2 system: submitting %0d layers x %0d positions through neural_director_packed.v ===", L, M);
for (li_i = 0; li_i < L; li_i = li_i + 1) begin
for (pp_i = 0; pp_i < M; pp_i = pp_i + 1) begin
submit_job(li_i*100000 + pp_i*1000, li_i*WORDS_PER_LAYER, N_TILES[15:0],
26'h9000 + li_i*10 + pp_i, (li_i*M + pp_i));
expect_node[n_expected] = (li_i*M + pp_i);
expect_val[n_expected] = golden_result(li_i, pp_i);
n_expected = n_expected + 1;
end
end
wd = 0;
while (completions < n_expected && wd < 5000) begin
@(posedge clk);
wd = wd + 1;
end
if (completions < n_expected) begin
$display("FAIL: only %0d/%0d position-results completed within watchdog", completions, n_expected);
errors = errors + 1;
end
$display("=== %0d/%0d tests, %0d errors, %0d/%0d positions completed ===", tests-errors, tests, errors, completions, n_expected);
if (errors == 0 && completions == n_expected) $display("ALL TESTS PASSED (tb_np_director_n2_system)");
$finish;
end
endmodule
+1
View File
@@ -153,6 +153,7 @@ module tb;
.result_addr_a_out(result_addr_a_out), .result_addr_b_out(result_addr_b_out), .result_addr_a_out(result_addr_a_out), .result_addr_b_out(result_addr_b_out),
.act_tile_addr_a(act_addr_a), .act_tile_addr_b(act_addr_b), .act_tile_addr_a(act_addr_a), .act_tile_addr_b(act_addr_b),
.act_tile_data_a(act_data_a), .act_tile_data_b(act_data_b), .act_tile_data_a(act_data_a), .act_tile_data_b(act_data_b),
.mem_grant(1'b1), // no arbiter in this single-slot test
.ctrl_req(slot_ctrl_req), .ctrl_wr(slot_ctrl_wr), .ctrl_addr(slot_ctrl_addr), .ctrl_req(slot_ctrl_req), .ctrl_wr(slot_ctrl_wr), .ctrl_addr(slot_ctrl_addr),
.ctrl_wdata(slot_ctrl_wdata), .ctrl_wmask(slot_ctrl_wmask), .ctrl_wdata(slot_ctrl_wdata), .ctrl_wmask(slot_ctrl_wmask),
.ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy) .ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy)