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:
@@ -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.
|
||||||
|
|||||||
@@ -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,8 +240,14 @@ 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;
|
||||||
pf_start <= 1'b1;
|
state <= S_MEMWAIT;
|
||||||
state <= S_PREFETCH;
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
S_MEMWAIT: begin
|
||||||
|
if (mem_grant) begin
|
||||||
|
pf_start <= 1'b1;
|
||||||
|
state <= S_PREFETCH;
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user