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:
@@ -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_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
|
||||
// deferred) ----
|
||||
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_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
|
||||
// shared arbiter for N>1 slots) ----
|
||||
output wire ctrl_req,
|
||||
@@ -90,16 +107,18 @@ module packed_slot #(
|
||||
input wire ctrl_busy
|
||||
);
|
||||
localparam S_IDLE = 4'd0,
|
||||
S_PREFETCH = 4'd1,
|
||||
S_SWAP = 4'd2,
|
||||
S_JOBSTART = 4'd3,
|
||||
S_TILEREQ = 4'd4,
|
||||
S_TILEWAIT = 4'd5,
|
||||
S_OPERAND = 4'd6,
|
||||
S_RESULT = 4'd7,
|
||||
S_DONE = 4'd8;
|
||||
S_MEMWAIT = 4'd1,
|
||||
S_PREFETCH = 4'd2,
|
||||
S_SWAP = 4'd3,
|
||||
S_JOBSTART = 4'd4,
|
||||
S_TILEREQ = 4'd5,
|
||||
S_TILEWAIT = 4'd6,
|
||||
S_OPERAND = 4'd7,
|
||||
S_RESULT = 4'd8,
|
||||
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;
|
||||
@@ -221,8 +240,14 @@ module packed_slot #(
|
||||
node_id_b_lat <= node_id_b;
|
||||
job_bias <= {DATA_WIDTH{1'b0}};
|
||||
job_activation <= ACT_RELU;
|
||||
pf_start <= 1'b1;
|
||||
state <= S_PREFETCH;
|
||||
state <= S_MEMWAIT;
|
||||
end
|
||||
end
|
||||
|
||||
S_MEMWAIT: begin
|
||||
if (mem_grant) begin
|
||||
pf_start <= 1'b1;
|
||||
state <= S_PREFETCH;
|
||||
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
|
||||
Reference in New Issue
Block a user