feat: MILESTONE - real activation-fetch engine, full N=2 system verified on real DDR3 (EXP-0079)

Closes the last major disclosed functional gap: packed_slot.v's
activation data was read through a combinational stand-in since
EXP-0062. New act_tile_fetch.v reads activation tiles directly from
DDR3 (no on-chip buffering needed, unlike weights -- activation data
has no reuse), sharing each slot's existing ctrl port with its own
weight-prefetch engine. Real memory layout: one full BURST_LEN=8-word
burst per tile, deliberately avoiding any runtime-indexed part-select
given this project's thin P&R timing margin (EXP-0078).

Verified at three levels: act_tile_fetch.v alone (6/6), packed_slot.v
with real preloaded activation data (9/9), and the full N=2 system
against real DDR3 via xsim (8/8, 0 errors) -- the first time this
project's compute path has been verified end-to-end with real DDR3
for both weights and activations.

Retired hardware/v3/rtl/n2_system_top.v and its testbench (pre-DDR3
SDR-placeholder era, fully superseded by n2_system_ddr3_top.v).

Also: docs/PHYSICAL_REALIZATION.md (real pinout/parts/timing/protocol
reference for the physical board) and CLAUDE.md (persistent project
instructions for future Claude Code sessions).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
This commit is contained in:
2026-09-20 08:13:15 +02:00
co-authored by Claude Sonnet 5
parent 78577dde59
commit 43a12379a5
11 changed files with 946 additions and 682 deletions
+98
View File
@@ -4933,3 +4933,101 @@ checkpoint. Remaining open work (scaling past N=2, a real activation-
fetch engine, PCB-specific pin constraints once the board layout is
underway, ESP32-side JTAG bootstrap firmware) is all disclosed and
outside this experiment's own scope.
EXP-0079 -- MILESTONE: real activation-fetch engine built, closing
the last major disclosed functional gap; full N=2 system re-verified
end-to-end with REAL DDR3 for BOTH weights and activations
(2026-09-20, same autonomous continuation, user's own explicit
request: "completiamo quello che manca per avere un codice ready to
use nell'hardware fisico")
CONTEXT: packed_slot.v's own header had disclosed, since EXP-0062,
that activation data was read through a combinational stand-in port
(act_tile_addr_a/b -> act_tile_data_a/b), with a real fetch engine
explicitly deferred. This was the single largest remaining gap between
"a verified compute architecture" and "a system that can actually run
on real data in real DDR3".
DESIGN: new hardware/v3/rtl/act_tile_fetch.v -- unlike the weight path
(prefetched once into an on-chip buffer, reused across many read-outs
per job), activation data has NO reuse (read exactly once per
position), so this engine reads DIRECTLY from DDR3 per tile, no
on-chip buffering. Reuses the SAME per-slot ctrl_req/addr/etc port
layer_prefetch_ctrl.v already owns (mutually exclusive in time by FSM
construction -- weight prefetch always fully completes before the
tile loop that needs activation data starts), muxed inside packed_slot.v
on a new act_mem_active signal. Two sequential burst reads per tile
request (lane A then lane B), with an explicit ctrl_busy wait between
them (mig_native_adapter.v's own S_DONE tail can keep busy asserted
one cycle past ready -- checked explicitly, not assumed safe).
MEMORY LAYOUT (a new, real, disclosed requirement): each tile occupies
its own full BURST_LEN=8-word burst slot (P_IN=8 bytes in the low 64
bits, upper 64 bits padding) -- deliberately 2x wasteful of DDR3
capacity, in exchange for needing ZERO runtime-indexed part-select in
the fetch logic (weight_tile_gather.v, EXP-0061, already flagged that
pattern as a real Fmax risk, and this project's P&R margin is
currently thin, EXP-0078 WNS +0.013ns -- not the moment to introduce a
new critical path). Documented in the new hardware/v3/constraints
physical doc for whoever prepares host-side data layout.
packed_slot.v's own S_TILEWAIT state was restructured into a real
two-source JOIN: latches (tile_seen/act_seen) independently track
whether the (fast, on-chip) weight tile and the (real-DDR3-latency)
activation tile have each arrived, proceeding to S_OPERAND only once
BOTH have been seen, correctly handling either arrival order (not just
the expected common case of weight-first).
VERIFICATION (three levels, matching this project's own "one variable
at a time" discipline):
1. hardware/v3/sim/tb_act_tile_fetch.v -- act_tile_fetch.v alone
against the SDR SDRAM placeholder: 6/6 PASS on the first real run
(no bugs found -- the nonblocking-assignment stimulus idiom,
already standard practice since EXP-0073/0075/0077, avoided the
testbench-race class that has bitten every PREVIOUS new module's
first draft in this project).
2. hardware/v3/sim/tb_packed_slot.v -- rewritten to preload REAL
activation data into the SDR placeholder (same technique already
used for weights) instead of a combinational lookup stand-in;
the OLD decimal-encoded x_base convention (li*100000+pos*1000)
was replaced by the new real word-address convention. 9/9 PASS,
0 errors, on the first real run after fixing one Verilog syntax
issue (can't part-select a function call's return value inline
in this dialect -- assign to a temp variable first).
3. hardware/v3/sim/tb_n2_system_ddr3.v -- the full real N=2 system
(Director + 2 packed_slot + arbiter + MIG + real ddr3_model.sv),
same update pattern, re-run via real xsim. **8/8 tests, 0 errors,
8/8 positions completed, bit-exact against the golden model --
the first time this project's compute path has been verified
end-to-end against REAL DDR3 for BOTH weights and activations,
not just weights.**
INTEGRATION: n2_system_ddr3_top.v (the real synthesis target) updated
to remove the old activation-stub top-level wiring entirely (the
free-running-counter stand-in from EXP-0074, itself a fix for an
earlier mistake of exposing act_addr/data as literal chip pins) --
activation fetch is now fully internal to each packed_slot instance,
using ports that already existed for other reasons. Net effect: FEWER
top-level signals than before, not more.
RETIRED (superseded, not fixed-in-place): hardware/v3/rtl/n2_system_top.v
and hardware/v3/sim/tb_np_director_n2_system.v (the pre-DDR3, SDR-
placeholder-era N=2 top/test, EXP-0066/0067) -- fully superseded by
n2_system_ddr3_top.v/tb_n2_system_ddr3.v, would have needed the exact
same class of update for zero forward benefit. Removed via `git rm`,
fully recoverable from git history if ever needed.
DECISION: this closes the last major disclosed FUNCTIONAL gap in the
V3 compute pipeline -- real DSP-packed cores, real weight-reuse
scheduling, real N-way arbitration, real DDR3 for BOTH weights and
activations, real host SPI protocol (jobs/registers/raw memory/config-
flash), all verified together end to end. What remains open (scaling
past N=2, PCB-specific pin finalization, ESP32 firmware) is genuinely
separate, disclosed, non-blocking work -- not a hidden correctness gap.
next_action: real in-context P&R re-verification (the activation
engine adds real logic on a path that matters -- EXP-0078's own margin
was already thin, +0.013ns, before this addition) -- must re-confirm
timing still closes before calling this "ready to use in physical
hardware". Also: finalize and commit docs/PHYSICAL_REALIZATION.md
(drafted this session, real pin/part/protocol/layout data).
+154
View File
@@ -0,0 +1,154 @@
`timescale 1ns/1ps
// ============================================================
// V3 -- act_tile_fetch.v: REAL activation-tile fetch engine, closing
// the gap packed_slot.v's own header has disclosed since EXP-0062
// ("a real activation fetch engine ... is a separate, later
// deliverable, NOT built here"). This is that deliverable.
//
// WHY A SEPARATE, SIMPLE ENGINE (not a prefetch/buffer pair like the
// weight path): weights are reused across M reuse-positions per
// Director-dispatched pair, so prefetching them once into an on-chip
// buffer (layer_prefetch_ctrl.v/layer_weight_buffer.v) amortizes real
// DDR3 latency across many reads. Activation data has NO such reuse
// -- each position's activation tile is read exactly once per job --
// so buffering it on-chip would only add complexity for zero benefit.
// This engine reads DIRECTLY from DDR3 per tile instead.
//
// MEMORY LAYOUT CONVENTION (real, disclosed, and REQUIRED of whoever
// prepares activation data in DDR3 -- documented in the physical
// realization doc too): each activation tile (P_IN=8 INT8 values)
// occupies its OWN full BURST_LEN=8-word (128-bit) burst slot, in the
// LOW 64 bits, upper 64 bits unused padding. Tile index t's word
// address is therefore `base + t*BURST_LEN`, always burst-aligned by
// construction. This is DELIBERATELY wasteful of DDR3 capacity (2x)
// in exchange for AVOIDING a runtime-indexed part-select to pick
// which half of a shared burst holds the tile -- weight_tile_gather.v
// already established (EXP-0061) that pattern is a real Fmax risk,
// and this project's own P&R margin is currently thin (EXP-0078,
// WNS +0.013ns) -- not the moment to introduce a new critical path.
// A future denser packing (2 tiles/burst, real part-select) is a
// disclosed, deliberate follow-up, not done here.
//
// PROTOCOL: one request (`req` pulse + base_a/base_b/tcnt) triggers
// TWO SEQUENTIAL burst reads (lane A then lane B) over the SAME
// shared ctrl port packed_slot.v already owns -- reusing the EXACT
// port layer_prefetch_ctrl.v uses during S_PREFETCH, since that
// phase has already finished (weight data is on-chip by the time
// this engine runs) and the port is genuinely free. Follows the same
// combinational-first-grant discipline as every other one-shot-pulse
// requester in this project (EXP-0066): `mem_active` must be visible
// to the arbiter the SAME cycle it asserts, `ctrl_req` is only issued
// after `mem_grant` is observed, never blind.
// ============================================================
module act_tile_fetch #(
parameter DATA_WIDTH = 8,
parameter P_IN = 8,
parameter BURST_LEN = 8,
parameter ADDR_WIDTH = 25 // word address, matches the shared ctrl port's own convention
)(
input wire clk,
input wire rst,
input wire req, // one-shot pulse
input wire [ADDR_WIDTH-1:0] base_a,
input wire [ADDR_WIDTH-1:0] base_b,
input wire [15:0] tcnt,
output reg valid, // one-cycle pulse, data_a/data_b valid
output reg signed [DATA_WIDTH*P_IN-1:0] data_a,
output reg signed [DATA_WIDTH*P_IN-1:0] data_b,
output wire mem_active,
input wire mem_grant,
output reg ctrl_req,
output reg ctrl_wr,
output reg [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
);
assign ctrl_wdata = {(16*BURST_LEN){1'b0}};
assign ctrl_wmask = {(2*BURST_LEN){1'b0}}; // read-only engine, mask unused
localparam S_IDLE = 3'd0,
S_MEMWAIT = 3'd1,
S_REQ_A = 3'd2,
S_GAP = 3'd3, // wait for ctrl_busy to clear before firing lane B's request
S_REQ_B = 3'd4;
reg [2:0] state;
reg [ADDR_WIDTH-1:0] base_a_lat, base_b_lat;
reg [15:0] tcnt_lat;
assign mem_active = (state != S_IDLE);
wire [ADDR_WIDTH-1:0] tile_offset = {{(ADDR_WIDTH-16){1'b0}}, tcnt_lat} * BURST_LEN[ADDR_WIDTH-1:0];
always @(posedge clk) begin
if (rst) begin
state <= S_IDLE;
ctrl_req <= 1'b0; ctrl_wr <= 1'b0; ctrl_addr <= {ADDR_WIDTH{1'b0}};
valid <= 1'b0; data_a <= {(DATA_WIDTH*P_IN){1'b0}}; data_b <= {(DATA_WIDTH*P_IN){1'b0}};
base_a_lat <= {ADDR_WIDTH{1'b0}}; base_b_lat <= {ADDR_WIDTH{1'b0}}; tcnt_lat <= 16'd0;
end else begin
ctrl_req <= 1'b0;
valid <= 1'b0;
case (state)
S_IDLE: begin
if (req) begin
base_a_lat <= base_a;
base_b_lat <= base_b;
tcnt_lat <= tcnt;
state <= S_MEMWAIT;
end
end
S_MEMWAIT: begin
if (mem_grant) begin
ctrl_addr <= base_a_lat + tile_offset;
ctrl_wr <= 1'b0;
ctrl_req <= 1'b1;
state <= S_REQ_A;
end
end
S_REQ_A: begin
if (ctrl_ready) begin
data_a <= ctrl_rdata[0 +: DATA_WIDTH*P_IN];
ctrl_addr <= base_b_lat + tile_offset;
ctrl_wr <= 1'b0;
state <= S_GAP;
end
end
S_GAP: begin
// the shared controller may still be finishing its
// own internal completion sequence for lane A's
// request for one more cycle after ctrl_ready
// pulsed (mig_native_adapter.v's own S_DONE state
// keeps `busy` asserted through it) -- wait for
// !ctrl_busy before firing lane B's request,
// instead of assuming back-to-back is safe.
if (!ctrl_busy) begin
ctrl_req <= 1'b1;
state <= S_REQ_B;
end
end
S_REQ_B: begin
if (ctrl_ready) begin
data_b <= ctrl_rdata[0 +: DATA_WIDTH*P_IN];
valid <= 1'b1;
state <= S_IDLE;
end
end
default: state <= S_IDLE;
endcase
end
end
endmodule
+8 -28
View File
@@ -260,30 +260,14 @@ module n2_system_ddr3_top #(
wire [15:0] s0_nid_a, s0_nid_b, s1_nid_a, s1_nid_b;
wire [JOB_ADDR_WIDTH-1:0] s0_raddr_a, s0_raddr_b, s1_raddr_a, s1_raddr_b;
// ---- activation-fetch STUB (disclosed gap, see packed_slot.v's
// own header: no real activation-fetch engine exists yet). Kept
// fully INTERNAL rather than exposed as top-level chip pins --
// exposing act_addr/act_data literally as pins was a real mistake
// caught by this same P&R run: s0/s1's act_addr_a/b (JOB_ADDR_
// WIDTH=26 bits x4) + act_data_a/b (DATA_WIDTH*P_IN=64 bits x4)
// alone demand ~360 I/O, but XC7A100T-CSG324 has only 324 pins
// total (DDR3 alone already uses ~53) -- place_design failed with
// "IO Clock Placer failed" for exactly this reason. A free-
// running counter-addressed pattern stands in for real activation
// data until a real fetch engine (DDR3-backed, like the weight
// path) is built; this keeps real timing/placement meaningful for
// everything else in this P&R run without claiming activation
// fetch is solved.
wire [JOB_ADDR_WIDTH-1:0] s0_act_addr_a, s0_act_addr_b, s1_act_addr_a, s1_act_addr_b;
reg [DATA_WIDTH*P_IN-1:0] act_stub_reg;
always @(posedge ui_clk)
if (ui_clk_sync_rst) act_stub_reg <= {(DATA_WIDTH*P_IN){1'b0}};
else act_stub_reg <= act_stub_reg + 1'b1;
wire signed [DATA_WIDTH*P_IN-1:0] s0_act_data_a = act_stub_reg;
wire signed [DATA_WIDTH*P_IN-1:0] s0_act_data_b = act_stub_reg;
wire signed [DATA_WIDTH*P_IN-1:0] s1_act_data_a = act_stub_reg;
wire signed [DATA_WIDTH*P_IN-1:0] s1_act_data_b = act_stub_reg;
// ---- activation fetch: REAL now (EXP-0079) -- each packed_slot
// instance owns its own act_tile_fetch.v internally, sharing that
// SAME slot's existing ctrl_req/addr/etc port (already wired to
// the arbiter below) with its own weight-prefetch engine. No
// top-level activation ports exist any more -- the old stand-in
// (act_tile_addr_a/b -> act_tile_data_a/b, and before that, a
// free-running counter stub that nearly blew the package's whole
// I/O budget, see git history) is gone; this is fully internal.
packed_slot #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH),
.BURST_LEN(BURST_LEN), .ADDR_WIDTH(JOB_ADDR_WIDTH), .LAYER_BYTES(LAYER_BYTES)
@@ -302,8 +286,6 @@ module n2_system_ddr3_top #(
.result_node_id_a(s0_nid_a), .result_node_id_b(s0_nid_b),
.result_addr_a_out(s0_raddr_a), .result_addr_b_out(s0_raddr_b),
.mem_active(req_active[0]), .mem_grant(req_grant[0]),
.act_tile_addr_a(s0_act_addr_a), .act_tile_addr_b(s0_act_addr_b),
.act_tile_data_a(s0_act_data_a), .act_tile_data_b(s0_act_data_b),
.ctrl_req(req_req[0]), .ctrl_wr(req_wr[0]),
.ctrl_addr(req_addr[0*MEM_ADDR_WIDTH +: MEM_ADDR_WIDTH]),
.ctrl_wdata(req_wdata[0*16*BURST_LEN +: 16*BURST_LEN]),
@@ -330,8 +312,6 @@ module n2_system_ddr3_top #(
.result_node_id_a(s1_nid_a), .result_node_id_b(s1_nid_b),
.result_addr_a_out(s1_raddr_a), .result_addr_b_out(s1_raddr_b),
.mem_active(req_active[1]), .mem_grant(req_grant[1]),
.act_tile_addr_a(s1_act_addr_a), .act_tile_addr_b(s1_act_addr_b),
.act_tile_data_a(s1_act_data_a), .act_tile_data_b(s1_act_data_b),
.ctrl_req(req_req[1]), .ctrl_wr(req_wr[1]),
.ctrl_addr(req_addr[1*MEM_ADDR_WIDTH +: MEM_ADDR_WIDTH]),
.ctrl_wdata(req_wdata[1*16*BURST_LEN +: 16*BURST_LEN]),
-192
View File
@@ -1,192 +0,0 @@
`timescale 1ns/1ps
// ============================================================
// V3 -- synthesis top for the EXP-0066 verified N=2 multi-core
// system: neural_director_packed.v + 2 real packed_slot.v instances
// + sdram_slot_arbiter2.v + real sdram_controller.v, flat structural
// wiring, for a real P&R resource/timing check (same out-of-context
// methodology as EXP-0059/0063).
//
// Activation stand-in ports (see packed_slot.v's own header) are
// exposed per-slot at the top level, matching this module's own
// still-declared scope limit (no real activation fetch engine yet).
// ============================================================
module n2_system_top #(
parameter DATA_WIDTH = 8,
parameter P_IN = 8,
parameter ACC_WIDTH = 32,
parameter BURST_LEN = 8,
parameter ROW_BITS = 13,
parameter COL_BITS = 10,
parameter BANK_BITS = 2,
parameter SDRAM_ADDR_WIDTH = BANK_BITS + ROW_BITS + COL_BITS,
parameter ADDR_WIDTH = 26,
parameter LAYER_BYTES = 128,
parameter N_SLOTS = 2,
parameter QUEUE_DEPTH = 8
)(
input wire clk,
input wire rst,
// ---- Director job submission ----
input wire job_in_valid,
output wire job_in_ready,
input wire [ADDR_WIDTH-1:0] job_in_x_base,
input wire [ADDR_WIDTH-1:0] job_in_w_base,
input wire [15:0] job_in_n_tiles,
input wire [ADDR_WIDTH-1:0] job_in_result_addr,
input wire [15:0] job_in_node_id,
output wire job_out_done,
output wire [$clog2(N_SLOTS)-1:0] job_out_slot,
// ---- activation stand-ins, slot 0 ----
output wire [ADDR_WIDTH-1:0] s0_act_addr_a,
output wire [ADDR_WIDTH-1:0] s0_act_addr_b,
input wire signed [DATA_WIDTH*P_IN-1:0] s0_act_data_a,
input wire signed [DATA_WIDTH*P_IN-1:0] s0_act_data_b,
output wire signed [DATA_WIDTH-1:0] s0_result_data_a,
output wire signed [DATA_WIDTH-1:0] s0_result_data_b,
// ---- activation stand-ins, slot 1 ----
output wire [ADDR_WIDTH-1:0] s1_act_addr_a,
output wire [ADDR_WIDTH-1:0] s1_act_addr_b,
input wire signed [DATA_WIDTH*P_IN-1:0] s1_act_data_a,
input wire signed [DATA_WIDTH*P_IN-1:0] s1_act_data_b,
output wire signed [DATA_WIDTH-1:0] s1_result_data_a,
output wire signed [DATA_WIDTH-1:0] s1_result_data_b,
// ---- real SDRAM pins ----
output wire sdram_cke,
output wire sdram_cs_n,
output wire sdram_ras_n,
output wire sdram_cas_n,
output wire sdram_we_n,
output wire [BANK_BITS-1:0] sdram_ba,
output wire [ROW_BITS-1:0] sdram_a,
inout wire [15:0] sdram_dq,
output wire [1:0] sdram_dqm
);
localparam BUFADDRW = $clog2(LAYER_BYTES);
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 [3:0] dir_state;
wire dir_error;
wire queue_empty;
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), .queue_empty(queue_empty)
);
wire [1:0] mem_active, mem_grant;
wire [1:0] s_ctrl_req, s_ctrl_wr;
wire [SDRAM_ADDR_WIDTH-1:0] s0_ctrl_addr, s1_ctrl_addr;
wire [16*BURST_LEN-1:0] s0_ctrl_wdata, s1_ctrl_wdata;
wire [2*BURST_LEN-1:0] s0_ctrl_wmask, s1_ctrl_wmask;
wire [16*BURST_LEN-1:0] s0_ctrl_rdata, s1_ctrl_rdata;
wire [1:0] s_ctrl_ready, s_ctrl_busy;
wire ctrl_req, ctrl_wr;
wire [SDRAM_ADDR_WIDTH-1:0] ctrl_addr;
wire [16*BURST_LEN-1:0] ctrl_wdata, ctrl_rdata;
wire [2*BURST_LEN-1:0] ctrl_wmask;
wire ctrl_ready, 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(s0_ctrl_addr), .slot0_wdata(s0_ctrl_wdata), .slot0_wmask(s0_ctrl_wmask),
.slot0_rdata(s0_ctrl_rdata), .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(s1_ctrl_addr), .slot1_wdata(s1_ctrl_wdata), .slot1_wmask(s1_ctrl_wmask),
.slot1_rdata(s1_ctrl_rdata), .slot1_ready(s_ctrl_ready[1]), .slot1_busy(s_ctrl_busy[1]),
.ctrl_req(ctrl_req), .ctrl_wr(ctrl_wr), .ctrl_addr(ctrl_addr),
.ctrl_wdata(ctrl_wdata), .ctrl_wmask(ctrl_wmask),
.ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy)
);
sdram_controller #(
.CLK_FREQ_MHZ(64), .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(sdram_cke), .sdram_cs_n(sdram_cs_n), .sdram_ras_n(sdram_ras_n),
.sdram_cas_n(sdram_cas_n), .sdram_we_n(sdram_we_n),
.sdram_ba(sdram_ba), .sdram_a(sdram_a), .sdram_dq(sdram_dq), .sdram_dqm(sdram_dqm)
);
wire [15:0] s0_nid_a, s0_nid_b, s1_nid_a, s1_nid_b;
wire [ADDR_WIDTH-1:0] s0_raddr_a, s0_raddr_b, s1_raddr_a, s1_raddr_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_slot0 (
.clk(clk), .rst(rst),
.job_start(slot_job_start[0]),
.x_base_a(slot_x_base_a[0*ADDR_WIDTH +: ADDR_WIDTH]),
.x_base_b(slot_x_base_b[0*ADDR_WIDTH +: ADDR_WIDTH]),
.w_base(slot_w_base[0*ADDR_WIDTH +: ADDR_WIDTH]),
.n_tiles(slot_n_tiles[0*16 +: 16]),
.result_addr_a(slot_result_addr_a[0*ADDR_WIDTH +: ADDR_WIDTH]),
.result_addr_b(slot_result_addr_b[0*ADDR_WIDTH +: ADDR_WIDTH]),
.node_id_a(slot_node_id_a[0*16 +: 16]), .node_id_b(slot_node_id_b[0*16 +: 16]),
.job_done(slot_job_done[0]),
.result_data_a(s0_result_data_a), .result_data_b(s0_result_data_b),
.result_node_id_a(s0_nid_a), .result_node_id_b(s0_nid_b),
.result_addr_a_out(s0_raddr_a), .result_addr_b_out(s0_raddr_b),
.mem_active(mem_active[0]), .mem_grant(mem_grant[0]),
.act_tile_addr_a(s0_act_addr_a), .act_tile_addr_b(s0_act_addr_b),
.act_tile_data_a(s0_act_data_a), .act_tile_data_b(s0_act_data_b),
.ctrl_req(s_ctrl_req[0]), .ctrl_wr(s_ctrl_wr[0]), .ctrl_addr(s0_ctrl_addr),
.ctrl_wdata(s0_ctrl_wdata), .ctrl_wmask(s0_ctrl_wmask),
.ctrl_rdata(s0_ctrl_rdata), .ctrl_ready(s_ctrl_ready[0]), .ctrl_busy(s_ctrl_busy[0])
);
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_slot1 (
.clk(clk), .rst(rst),
.job_start(slot_job_start[1]),
.x_base_a(slot_x_base_a[1*ADDR_WIDTH +: ADDR_WIDTH]),
.x_base_b(slot_x_base_b[1*ADDR_WIDTH +: ADDR_WIDTH]),
.w_base(slot_w_base[1*ADDR_WIDTH +: ADDR_WIDTH]),
.n_tiles(slot_n_tiles[1*16 +: 16]),
.result_addr_a(slot_result_addr_a[1*ADDR_WIDTH +: ADDR_WIDTH]),
.result_addr_b(slot_result_addr_b[1*ADDR_WIDTH +: ADDR_WIDTH]),
.node_id_a(slot_node_id_a[1*16 +: 16]), .node_id_b(slot_node_id_b[1*16 +: 16]),
.job_done(slot_job_done[1]),
.result_data_a(s1_result_data_a), .result_data_b(s1_result_data_b),
.result_node_id_a(s1_nid_a), .result_node_id_b(s1_nid_b),
.result_addr_a_out(s1_raddr_a), .result_addr_b_out(s1_raddr_b),
.mem_active(mem_active[1]), .mem_grant(mem_grant[1]),
.act_tile_addr_a(s1_act_addr_a), .act_tile_addr_b(s1_act_addr_b),
.act_tile_data_a(s1_act_data_a), .act_tile_data_b(s1_act_data_b),
.ctrl_req(s_ctrl_req[1]), .ctrl_wr(s_ctrl_wr[1]), .ctrl_addr(s1_ctrl_addr),
.ctrl_wdata(s1_ctrl_wdata), .ctrl_wmask(s1_ctrl_wmask),
.ctrl_rdata(s1_ctrl_rdata), .ctrl_ready(s_ctrl_ready[1]), .ctrl_busy(s_ctrl_busy[1])
);
endmodule
+97 -32
View File
@@ -14,17 +14,30 @@
// packed.v already expects (job_start/x_base_a/b/w_base/n_tiles/
// node_id_a/b -> job_done/result_data_a/b/result_node_id_a/b).
//
// SCOPE LIMITATION (disclosed, matches this project's own established
// precedent -- EXP-0058/0062's own header comments: "activation data
// ... representing the activation/sliding-window path, which is a
// separate, already-existing memory path not the subject of this
// test"): activations are read through a WIDE, per-tile, combinational
// stand-in port (act_tile_addr_a/b -> act_tile_data_a/b), mirroring
// this project's own earlier ideal_memory_model.v-style staging
// (establish the architectural contract before committing to a
// specific real fetch engine). A real activation fetch engine
// (analogous to weight_tile_gather.v, but for the sliding-window/
// activation path) is a separate, later deliverable, NOT built here.
// ACTIVATION FETCH (EXP-0079, real, closes the gap this header used to
// disclose as deferred): act_tile_fetch.v reads each tile's activation
// data DIRECTLY from the shared DDR3 bus, one tile at a time -- no
// on-chip buffering/prefetch (unlike weights, activation data is read
// exactly once per job, so buffering it would add complexity for zero
// reuse benefit). It shares THIS slot's own single ctrl_req/addr/etc
// port with layer_prefetch_ctrl.v (u_pf): the two are mutually
// exclusive in time by FSM construction (weight prefetch always fully
// completes, including its own consume_done, before the tile loop
// that needs activation data ever starts), muxed below on act_mem_
// active. The outer arbiter's grant (mem_active/mem_grant, this
// module's own top-level ports) is now also needed during activation
// fetch, not just weight prefetch -- held PER TILE (one 2-burst fetch,
// lane A then lane B), released between tiles, matching this
// project's own established "lock the grant for one whole logical
// fetch, not longer" discipline (avoids starving the other slot for
// the whole tile loop's duration).
//
// MEMORY LAYOUT this requires of activation data in DDR3: each tile
// occupies its own full BURST_LEN=8-word burst slot (see act_tile_
// fetch.v's own header for why -- avoiding a runtime-indexed part-
// select, a known Fmax risk this project's already-thin P&R margin,
// EXP-0078, can't afford right now). Documented for whoever prepares
// host-side data layout in the physical realization doc.
//
// Also disclosed: no result-writeback engine exists yet either --
// result_addr_a/b are passed through unused, for a future writeback
@@ -72,18 +85,11 @@ module packed_slot #(
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.
// shared SDRAM controller (its own weight-fetch OR activation-
// 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
// deferred) ----
output reg [ADDR_WIDTH-1:0] act_tile_addr_a,
output reg [ADDR_WIDTH-1:0] act_tile_addr_b,
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,
// 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
@@ -118,7 +124,6 @@ module packed_slot #(
S_DONE = 4'd9;
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 [15:0] n_tiles_lat;
reg [ADDR_WIDTH-1:0] result_addr_a_lat, result_addr_b_lat;
@@ -132,17 +137,58 @@ module packed_slot #(
wire [BUFADDRW-1:0] pf_fill_addr;
wire [DATA_WIDTH-1:0] pf_fill_data;
wire pf_ctrl_req, pf_ctrl_wr;
wire [ADDR_WIDTH-2:0] pf_ctrl_addr;
wire [16*BURST_LEN-1:0] pf_ctrl_wdata;
wire [2*BURST_LEN-1:0] pf_ctrl_wmask;
layer_prefetch_ctrl #(
.DATA_WIDTH(DATA_WIDTH), .LAYER_BYTES(LAYER_BYTES), .BURST_LEN(BURST_LEN), .ADDR_WIDTH(ADDR_WIDTH-1)
) u_pf (
.clk(clk), .rst(rst),
.start(pf_start), .layer_base(w_base_lat[ADDR_WIDTH-2:0]), .busy(pf_busy), .done(pf_done),
.fill_we(pf_fill_we), .fill_addr(pf_fill_addr), .fill_data(pf_fill_data),
.ctrl_req(ctrl_req), .ctrl_wr(ctrl_wr), .ctrl_addr(ctrl_addr),
.ctrl_wdata(ctrl_wdata), .ctrl_wmask(ctrl_wmask),
.ctrl_req(pf_ctrl_req), .ctrl_wr(pf_ctrl_wr), .ctrl_addr(pf_ctrl_addr),
.ctrl_wdata(pf_ctrl_wdata), .ctrl_wmask(pf_ctrl_wmask),
.ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy)
);
// ---- act_tile_fetch.v (EXP-0079): real activation fetch, shares
// this slot's own ctrl port with u_pf above (mutually exclusive in
// time -- see header) ----
reg act_req;
wire act_valid;
wire signed [DATA_WIDTH*P_IN-1:0] act_data_a_w, act_data_b_w;
wire act_mem_active;
wire act_ctrl_req, act_ctrl_wr;
wire [ADDR_WIDTH-2:0] act_ctrl_addr;
wire [16*BURST_LEN-1:0] act_ctrl_wdata;
wire [2*BURST_LEN-1:0] act_ctrl_wmask;
act_tile_fetch #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .BURST_LEN(BURST_LEN), .ADDR_WIDTH(ADDR_WIDTH-1)
) u_act (
.clk(clk), .rst(rst),
.req(act_req), .base_a(x_base_a_lat[ADDR_WIDTH-2:0]), .base_b(x_base_b_lat[ADDR_WIDTH-2:0]),
.tcnt(tcnt), .valid(act_valid), .data_a(act_data_a_w), .data_b(act_data_b_w),
.mem_active(act_mem_active), .mem_grant(mem_grant),
.ctrl_req(act_ctrl_req), .ctrl_wr(act_ctrl_wr), .ctrl_addr(act_ctrl_addr),
.ctrl_wdata(act_ctrl_wdata), .ctrl_wmask(act_ctrl_wmask),
.ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy)
);
// mutually exclusive by FSM construction (weight prefetch always
// fully completes, incl. consume_done, before the tile loop that
// triggers act_req ever starts) -- safe to select on act_mem_active alone.
assign ctrl_req = act_mem_active ? act_ctrl_req : pf_ctrl_req;
assign ctrl_wr = act_mem_active ? act_ctrl_wr : pf_ctrl_wr;
assign ctrl_addr = act_mem_active ? act_ctrl_addr : pf_ctrl_addr;
assign ctrl_wdata = act_mem_active ? act_ctrl_wdata : pf_ctrl_wdata;
assign ctrl_wmask = act_mem_active ? act_ctrl_wmask : pf_ctrl_wmask;
assign mem_active = (state == S_MEMWAIT) || (state == S_PREFETCH) || act_mem_active;
// ---- layer_weight_buffer.v ----
wire [BUFADDRW-1:0] lwb_rd_addr;
wire [DATA_WIDTH-1:0] lwb_rd_data;
@@ -158,6 +204,7 @@ module packed_slot #(
// ---- weight_tile_gather.v ----
reg tile_req;
reg [BUFADDRW-1:0] tile_base;
reg tile_seen, act_seen; // S_TILEWAIT join latches (weight vs activation, see header)
wire tile_valid;
wire [DATA_WIDTH*P_IN-1:0] tile_data;
@@ -214,6 +261,9 @@ module packed_slot #(
pf_start <= 1'b0;
consume_done <= 1'b0;
tile_req <= 1'b0;
act_req <= 1'b0;
tile_seen <= 1'b0;
act_seen <= 1'b0;
job_valid_np <= 1'b0;
operand_valid<= 1'b0;
tile_last <= 1'b0;
@@ -226,6 +276,7 @@ module packed_slot #(
pf_start <= 1'b0;
consume_done <= 1'b0;
tile_req <= 1'b0;
act_req <= 1'b0;
case (state)
S_IDLE: begin
@@ -278,19 +329,33 @@ module packed_slot #(
S_TILEREQ: begin
tile_req <= 1'b1;
tile_base <= tcnt[BUFADDRW-1:0]*P_IN[BUFADDRW-1:0];
act_tile_addr_a <= x_base_a_lat + {{(ADDR_WIDTH-16){1'b0}}, tcnt};
act_tile_addr_b <= x_base_b_lat + {{(ADDR_WIDTH-16){1'b0}}, tcnt};
act_req <= 1'b1;
tile_seen <= 1'b0;
act_seen <= 1'b0;
state <= S_TILEWAIT;
end
// Real join: weight_tile_gather.v's tile_valid (fast,
// on-chip) and act_tile_fetch.v's act_valid (real
// DDR3 latency, 2 bursts) do NOT arrive on the same
// cycle in general -- latch whichever comes first,
// proceed only once BOTH have been seen. Handles
// either arrival order correctly, not just the
// expected-common one (weight first).
S_TILEWAIT: begin
if (tile_valid) begin
weight_data_r <= tile_data;
input_data_a_r <= act_tile_data_a;
input_data_b_r <= act_tile_data_b;
tile_last <= (tcnt == n_tiles_lat - 16'd1);
operand_valid <= 1'b1;
state <= S_OPERAND;
weight_data_r <= tile_data;
tile_seen <= 1'b1;
end
if (act_valid) begin
input_data_a_r <= act_data_a_w;
input_data_b_r <= act_data_b_w;
act_seen <= 1'b1;
end
if ((tile_valid || tile_seen) && (act_valid || act_seen)) begin
tile_last <= (tcnt == n_tiles_lat - 16'd1);
operand_valid <= 1'b1;
state <= S_OPERAND;
end
end
+189
View File
@@ -0,0 +1,189 @@
`timescale 1ns/1ps
// ============================================================
// Isolated correctness test for act_tile_fetch.v -- real SDR SDRAM
// placeholder backend (same precedent as tb_host_mem_bridge.v/
// tb_sdram_arbiter_n.v: verify new glue logic against the fast
// backend first). Checks: (1) both lanes read back bit-exact from
// their own burst-aligned tile slot; (2) different tile indices
// correctly compute different burst addresses (tile_offset =
// tcnt*BURST_LEN); (3) back-to-back requests (multiple tiles in a
// row) all stay correct, exercising the S_GAP busy-wait logic.
// ============================================================
module tb;
localparam BURST_LEN = 8;
localparam ROW_BITS = 13;
localparam COL_BITS = 10;
localparam BANK_BITS = 2;
localparam 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;
reg clk = 0;
always #(CLK_PERIOD_NS/2.0) clk = ~clk;
reg rst;
wire ctrl_req, ctrl_wr;
wire [ADDR_WIDTH-1:0] ctrl_addr;
wire [16*BURST_LEN-1:0] ctrl_wdata, ctrl_rdata;
wire [2*BURST_LEN-1:0] ctrl_wmask;
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;
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)
);
// single requester -> tie grant = active, same precedent as
// tb_host_mem_bridge.v (a 1-requester arbiter would produce this).
wire req_active_dut;
wire mem_grant = req_active_dut;
reg req;
reg [ADDR_WIDTH-1:0] base_a, base_b;
reg [15:0] tcnt;
wire valid;
wire signed [DATA_WIDTH*P_IN-1:0] data_a, data_b;
wire dut_ctrl_req, dut_ctrl_wr;
wire [ADDR_WIDTH-1:0] dut_ctrl_addr;
wire [16*BURST_LEN-1:0] dut_ctrl_wdata;
wire [2*BURST_LEN-1:0] dut_ctrl_wmask;
act_tile_fetch #(
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .BURST_LEN(BURST_LEN), .ADDR_WIDTH(ADDR_WIDTH)
) u_dut (
.clk(clk), .rst(rst),
.req(req), .base_a(base_a), .base_b(base_b), .tcnt(tcnt),
.valid(valid), .data_a(data_a), .data_b(data_b),
.mem_active(req_active_dut), .mem_grant(mem_grant),
.ctrl_req(dut_ctrl_req), .ctrl_wr(dut_ctrl_wr), .ctrl_addr(dut_ctrl_addr),
.ctrl_wdata(dut_ctrl_wdata), .ctrl_wmask(dut_ctrl_wmask),
.ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy)
);
// ---- preload path: direct access to the SDRAM controller,
// bypassing act_tile_fetch.v entirely, same "pre_active" mux
// pattern as every other testbench in this project ----
reg pre_active;
reg pre_req, pre_wr;
reg [ADDR_WIDTH-1:0] pre_addr;
reg [16*BURST_LEN-1:0] pre_wdata;
// reroute: real DUT ctrl_* wires go through a mux so the testbench
// can preload memory directly before act_tile_fetch.v ever runs.
// (Re-declare the connection: DUT was wired directly above for
// simplicity of the DUT instantiation; use force-free approach by
// instead having the DUT's own ctrl_req/wr/addr/wdata feed the mux
// inputs below and the mux feed the real controller.)
assign ctrl_req = pre_active ? pre_req : dut_ctrl_req;
assign ctrl_wr = pre_active ? pre_wr : dut_ctrl_wr;
assign ctrl_addr = pre_active ? pre_addr : dut_ctrl_addr;
assign ctrl_wdata = pre_active ? pre_wdata : dut_ctrl_wdata;
assign ctrl_wmask = pre_active ? {(2*BURST_LEN){1'b0}} : dut_ctrl_wmask;
task automatic sdram_write_burst(input [ADDR_WIDTH-1:0] word_addr, input [16*BURST_LEN-1:0] data);
begin
@(posedge clk); while (ctrl_busy) @(posedge clk);
pre_req = 1'b1; pre_wr = 1'b1; pre_addr = word_addr; pre_wdata = data;
@(posedge clk); pre_req = 1'b0;
while (!ctrl_ready) @(posedge clk);
end
endtask
function automatic signed [7:0] act_byte(input integer base, input integer t, input integer k);
act_byte = $signed(8'((base*13 + t*31 + k*7 + 5) & 8'hFF));
endfunction
integer errors, tests;
task automatic check(input cond, input [255:0] name);
begin
tests = tests + 1;
if (!cond) begin errors = errors + 1; $display("FAIL: %0s", name); end
else $display("PASS: %0s", name);
end
endtask
task automatic do_fetch(input [ADDR_WIDTH-1:0] ba, input [ADDR_WIDTH-1:0] bb, input [15:0] tc);
begin
@(posedge clk);
base_a <= ba; base_b <= bb; tcnt <= tc;
req <= 1'b1;
@(posedge clk);
req <= 1'b0;
while (!valid) @(posedge clk);
@(posedge clk);
end
endtask
reg signed [DATA_WIDTH*P_IN-1:0] exp_a, exp_b;
integer k, wi;
reg [16*BURST_LEN-1:0] burst;
initial begin
errors = 0; tests = 0;
rst = 1; pre_active = 1'b1; pre_req = 0; pre_wr = 0; pre_addr = 0; pre_wdata = 0;
req = 0; base_a = 0; base_b = 0; tcnt = 0;
repeat(5) @(posedge clk);
rst = 0;
@(posedge clk); while (ctrl_busy) @(posedge clk);
$display("=== preload 4 burst-aligned tile slots (2 lanes x 2 tiles) ===");
// lane A base = 0, lane B base = 100 (arbitrary, word-address units)
for (wi = 0; wi < 2; wi = wi + 1) begin // wi = tile index
for (k = 0; k < BURST_LEN; k = k + 1)
burst[k*16 +: 16] = (k < P_IN/2) ? {act_byte(0, wi, 2*k+1), act_byte(0, wi, 2*k)} : 16'h0000;
sdram_write_burst(0 + wi*BURST_LEN, burst);
for (k = 0; k < BURST_LEN; k = k + 1)
burst[k*16 +: 16] = (k < P_IN/2) ? {act_byte(100, wi, 2*k+1), act_byte(100, wi, 2*k)} : 16'h0000;
sdram_write_burst(100 + wi*BURST_LEN, burst);
end
@(posedge clk);
pre_active = 1'b0;
$display("=== TEST 1: fetch tile 0, both lanes ===");
do_fetch(25'd0, 25'd100, 16'd0);
for (k = 0; k < P_IN; k = k + 1) exp_a[k*DATA_WIDTH +: DATA_WIDTH] = act_byte(0, 0, k);
for (k = 0; k < P_IN; k = k + 1) exp_b[k*DATA_WIDTH +: DATA_WIDTH] = act_byte(100, 0, k);
check(data_a === exp_a, "T1: lane A tile 0 bit-exact");
check(data_b === exp_b, "T1: lane B tile 0 bit-exact");
$display("=== TEST 2: fetch tile 1, both lanes (different burst address) ===");
do_fetch(25'd0, 25'd100, 16'd1);
for (k = 0; k < P_IN; k = k + 1) exp_a[k*DATA_WIDTH +: DATA_WIDTH] = act_byte(0, 1, k);
for (k = 0; k < P_IN; k = k + 1) exp_b[k*DATA_WIDTH +: DATA_WIDTH] = act_byte(100, 1, k);
check(data_a === exp_a, "T2: lane A tile 1 bit-exact");
check(data_b === exp_b, "T2: lane B tile 1 bit-exact");
$display("=== TEST 3: back-to-back fetches (tile 0 then tile 1 immediately) ===");
do_fetch(25'd0, 25'd100, 16'd0);
for (k = 0; k < P_IN; k = k + 1) exp_a[k*DATA_WIDTH +: DATA_WIDTH] = act_byte(0, 0, k);
check(data_a === exp_a, "T3a: back-to-back fetch 1, lane A correct");
do_fetch(25'd0, 25'd100, 16'd1);
for (k = 0; k < P_IN; k = k + 1) exp_a[k*DATA_WIDTH +: DATA_WIDTH] = act_byte(0, 1, k);
check(data_a === exp_a, "T3b: back-to-back fetch 2, lane A correct");
$display("=== %0d/%0d tests, %0d errors ===", tests-errors, tests, errors);
if (errors == 0) $display("ALL TESTS PASSED (tb_act_tile_fetch)");
$finish;
end
endmodule
+35 -21
View File
@@ -15,8 +15,12 @@
// memory backend is swapped, isolating that as the one variable
// under test.
//
// Activation stand-in (see packed_slot.v's own header) is unchanged
// too -- still a disclosed, separate gap, not addressed here.
// EXP-0079 UPDATE: activations are now fetched via a REAL act_tile_
// fetch.v inside each packed_slot.v instance (real DDR3 reads, same
// physical bus each slot already uses for weights) -- no more stand-
// in. This test now preloads real activation data into the SAME real
// DDR3 model too (preload_ddr3_activations), on top of the weight
// preload that was already here.
//
// Uses mig_7series_0_mig_sim (SIM_BYPASS_INIT_CAL="FAST" default,
// EXP-0068's own real vendor-shipped fast-calibration simulation
@@ -236,19 +240,34 @@ module tb;
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
// ---- real activation preload (EXP-0079: packed_slot.v now wraps
// a real act_tile_fetch.v, no more stand-in) -- same convention as
// tb_packed_slot.v/tb_act_tile_fetch.v: one full BURST_LEN=8-word
// burst per tile, P_IN=8 bytes in the low 64 bits. ----
localparam [MIG_ADDR_WIDTH-1:0] ACT_MEM_BASE = 25'h10000;
function automatic [ADDR_WIDTH-1:0] act_x_base(input integer li, input integer pos);
act_x_base = {{(ADDR_WIDTH-MIG_ADDR_WIDTH){1'b0}}, ACT_MEM_BASE} + (li*M + pos) * (N_TILES*BURST_LEN);
endfunction
task automatic preload_ddr3_activations;
integer li, pos, t, k;
reg [16*BURST_LEN-1:0] burst_data;
reg [ADDR_WIDTH-1:0] base;
begin
for (li = 0; li < L; li = li + 1) begin
for (pos = 0; pos < M; pos = pos + 1) begin
base = act_x_base(li, pos);
for (t = 0; t < N_TILES; t = t + 1) begin
burst_data = {(16*BURST_LEN){1'b0}};
for (k = 0; k < P_IN/2; k = k + 1)
burst_data[k*16 +: 16] = {input_byte(li, pos, t*P_IN + 2*k+1), input_byte(li, pos, t*P_IN + 2*k)};
sdram_write_burst(base[MIG_ADDR_WIDTH-1:0] + t*BURST_LEN, burst_data);
end
end
end
end
endtask
// ---- neural_director_packed.v ----
reg job_in_valid;
wire job_in_ready;
@@ -308,11 +327,6 @@ module tb;
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),
@@ -332,8 +346,6 @@ module tb;
.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_flat[gi*MIG_ADDR_WIDTH +: MIG_ADDR_WIDTH]),
.ctrl_wdata(s_ctrl_wdata_flat[gi*16*BURST_LEN +: 16*BURST_LEN]),
@@ -429,6 +441,8 @@ module tb;
$display("=== preload SDRAM with %0d resident-filter weight sets ===", L);
preload_sdram_layers;
$display("=== preload SDRAM with real activation data (EXP-0079) ===");
preload_ddr3_activations;
@(posedge ui_clk);
pre_active = 1'b0;
repeat (5) @(posedge ui_clk);
@@ -436,7 +450,7 @@ module tb;
$display("=== N=2 system on REAL DDR3: submitting %0d layers x %0d positions ===", 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],
submit_job(act_x_base(li_i, pp_i), 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);
-373
View File
@@ -1,373 +0,0 @@
`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
+39 -36
View File
@@ -9,11 +9,12 @@
// promotion from testbench-sequence to real RTL (EXP-0062 -> this)
// preserves bit-exact correctness.
//
// Activation stand-in (see packed_slot.v's own header): a simple
// combinational behavioral memory here, addressed by act_tile_addr_a/b
// (tile-index-based, matching packed_slot.v's own addressing:
// x_base + tile_count), standing in for the real (not yet built)
// activation fetch engine.
// EXP-0079 UPDATE: packed_slot.v now wraps a REAL act_tile_fetch.v
// (real DDR3 reads, no stand-in port left) -- this test now preloads
// activation data into the SAME real SDR SDRAM placeholder backend
// already used for weights (preload_sdram_activations, matching
// act_tile_fetch.v's own real memory layout: one full BURST_LEN=8-word
// burst per tile), instead of a combinational behavioral lookup.
// ============================================================
module tb;
localparam BURST_LEN = 8;
@@ -119,15 +120,36 @@ module tb;
end
endtask
// ---- activation stand-in: act_tile_addr = x_base + tile_index
// (packed_slot.v's own addressing) -- x_base itself is chosen as
// li*1000 + pos*100 below so a simple decode recovers (li,pos,t) ----
reg signed [DATA_WIDTH*P_IN-1:0] act_data_a, act_data_b;
wire [ADDR_WIDTH-1:0] act_addr_a, act_addr_b;
// ---- real activation preload (EXP-0079: act_tile_fetch.v replaces
// the old combinational stand-in) -- one full BURST_LEN=8-word
// burst PER TILE (act_tile_fetch.v's own real memory layout
// convention, see that module's header), P_IN=8 bytes in the low
// 64 bits, upper 64 bits padding. x_base(li,pos) = ACT_MEM_BASE +
// (li*M+pos)*(N_TILES*BURST_LEN), well clear of the weight region
// (word addresses 0..L*WORDS_PER_LAYER-1). ----
localparam [ADDR_WIDTH-1:0] ACT_MEM_BASE = 26'h10000;
function automatic [ADDR_WIDTH-1:0] act_x_base(input integer li, input integer pos);
act_x_base = ACT_MEM_BASE + (li*M + pos) * (N_TILES*BURST_LEN);
endfunction
// act_tile_addr = x_base + tile_index (packed_slot.v's own
// addressing); x_base itself encodes (li,pos) as li*100000+pos*1000
// so tile_index occupies the low 3 decimal digits directly.
task automatic preload_sdram_activations;
integer li, pos, t, k;
reg [16*BURST_LEN-1:0] burst_data;
reg [ADDR_WIDTH-1:0] base;
begin
for (li = 0; li < L; li = li + 1) begin
for (pos = 0; pos < M; pos = pos + 1) begin
base = act_x_base(li, pos);
for (t = 0; t < N_TILES; t = t + 1) begin
burst_data = {(16*BURST_LEN){1'b0}};
for (k = 0; k < P_IN/2; k = k + 1)
burst_data[k*16 +: 16] = {input_byte(li, pos, t*P_IN + 2*k+1), input_byte(li, pos, t*P_IN + 2*k)};
sdram_write_burst(base[SDRAM_ADDR_WIDTH-1:0] + t*BURST_LEN, burst_data);
end
end
end
end
endtask
// ---- packed_slot.v (DUT) ----
reg job_start;
@@ -151,33 +173,12 @@ module tb;
.result_data_a(result_data_a), .result_data_b(result_data_b),
.result_node_id_a(result_node_id_a), .result_node_id_b(result_node_id_b),
.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_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_wdata(slot_ctrl_wdata), .ctrl_wmask(slot_ctrl_wmask),
.ctrl_rdata(ctrl_rdata), .ctrl_ready(ctrl_ready), .ctrl_busy(ctrl_busy)
);
// real activation decode: x_base encodes (li,pos) as li*100000+pos*1000;
// act_tile_addr = x_base + tile_index (0..N_TILES-1), so
// tile_index = act_addr % 1000, pos = (act_addr/1000) % 100, li = act_addr/100000
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
always @(*) act_data_a = act_lookup(act_addr_a);
always @(*) act_data_b = act_lookup(act_addr_b);
integer errors, tests;
integer li_i, pp_i;
integer acc_a, acc_b, s_a, s_b, k, tt;
@@ -189,8 +190,8 @@ module tb;
tests = tests + 1;
@(posedge clk);
job_start = 1'b1;
x_base_a = li*100000 + pos_a*1000;
x_base_b = li*100000 + pos_b*1000;
x_base_a = act_x_base(li, pos_a);
x_base_b = act_x_base(li, pos_b);
w_base = li*WORDS_PER_LAYER; // WORD address, matching layer_prefetch_ctrl.v's
// own convention (EXP-0057/58/62) and this
// testbench's own preload_sdram_layers addressing
@@ -245,6 +246,8 @@ module tb;
$display("=== preload SDRAM with %0d resident-filter weight sets ===", L);
preload_sdram_layers;
$display("=== preload SDRAM with real activation data (EXP-0079) ===");
preload_sdram_activations;
@(posedge clk);
pre_active = 1'b0;