Implements M5: neural_director.v dispatches job descriptors to whichever of N_SLOTS (memory_manager, neural_processor) pairs is currently free (first-free scheduling per §9's initial policy), with a parametric-depth ready-queue FIFO for jobs arriving faster than slots can absorb them. Scope for this milestone (see decisions.log DEC-0007): a reduced 4-state FSM (DIR_IDLE/SCAN_READY/ALLOCATE/ERROR) rather than §9's full 8-state baseline -- dependency tracking, the waiting queue, and wake-up are §10's explicit responsibility (Dependency Manager, M6, not yet built), and slot-completion detection runs as an always-active per-slot tracker rather than a dedicated FSM state, for the same reason DEC-0002 already gave for the Neural Processor's own FSM (gating concurrent per-unit progress behind one shared state kills throughput). Verified with Verilator (N_SLOTS=2, each slot backed by its own independent behavioral memory rather than sharing V1's real PSRAM -- M4 already proved that path for one slot; this milestone's own concern is scheduling across multiple slots): 4/4 tests pass -- 3 jobs submitted to 2 slots (first two dispatch immediately, third correctly queues until a slot frees), and a deliberate burst that forces the ready queue to genuinely fill and recover. Real synthesis: 0 CHECK problems, 382 LUT4/366 FF/4 CCU2C/0 DSP. Real place&route (via a synthesis-only timing harness, same TRELLIS_IO pin-budget reason as M2/M4): Fmax 250.50 MHz, PASS at 80MHz. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
323 lines
14 KiB
Verilog
323 lines
14 KiB
Verilog
`timescale 1ns/1ps
|
|
|
|
// ============================================================
|
|
// M5 testbench (docs/v2-description.md §9/§20): neural_director.v
|
|
// dispatching to N_SLOTS=2 (memory_manager + neural_processor) pairs.
|
|
// Verified with Verilator (decisions.log DEC-0004).
|
|
//
|
|
// Scope decision (decisions.log DEC-0007): each slot gets its OWN
|
|
// independent simple behavioral byte memory (sim_byte_mem below,
|
|
// fixed 2-cycle latency, matching int8_memory_access.v's req/wr/addr/
|
|
// wdata -> rdata/ready contract exactly) instead of sharing V1's real
|
|
// PSRAM chain -- M4 already proved the real PSRAM path end-to-end
|
|
// with ONE slot (EXP-0005); M5's own concern is scheduling/dispatch
|
|
// across MULTIPLE slots, which is what this testbench isolates.
|
|
// Multiple slots genuinely sharing ONE physical PSRAM port is a
|
|
// backend-arbitration problem explicitly deferred (DEC-0006), not
|
|
// re-solved here.
|
|
//
|
|
// Coverage:
|
|
// - more jobs submitted (3) than slots exist (2): first two must
|
|
// dispatch immediately (first-free), the third must wait in the
|
|
// ready queue until a slot frees up, then dispatch automatically.
|
|
// - each job's result independently verified (own oracle).
|
|
// - ready-queue backpressure: fill the queue past N jobs beyond
|
|
// slot capacity and confirm job_in_ready deasserts, then confirm
|
|
// it drains and reasserts as slots complete.
|
|
// ============================================================
|
|
|
|
module sim_byte_mem #(
|
|
parameter ADDR_WIDTH = 23,
|
|
parameter DEPTH = 1024
|
|
)(
|
|
input wire clk,
|
|
input wire rst,
|
|
input wire req,
|
|
input wire wr,
|
|
input wire [ADDR_WIDTH-1:0] addr,
|
|
input wire signed [7:0] wdata,
|
|
output reg signed [7:0] rdata,
|
|
output reg ready
|
|
);
|
|
reg signed [7:0] mem [0:DEPTH-1];
|
|
reg [1:0] state;
|
|
reg [ADDR_WIDTH-1:0] addr_reg;
|
|
reg wr_reg;
|
|
localparam ST_IDLE = 0, ST_WAIT = 1;
|
|
always @(posedge clk) begin
|
|
if (rst) begin
|
|
state <= ST_IDLE; ready <= 1'b0; rdata <= 8'sd0;
|
|
end else begin
|
|
ready <= 1'b0;
|
|
case (state)
|
|
ST_IDLE: if (req) begin
|
|
addr_reg <= addr; wr_reg <= wr;
|
|
if (wr) mem[addr] <= wdata;
|
|
state <= ST_WAIT;
|
|
end
|
|
ST_WAIT: begin
|
|
rdata <= mem[addr_reg];
|
|
ready <= 1'b1;
|
|
state <= ST_IDLE;
|
|
end
|
|
endcase
|
|
end
|
|
end
|
|
endmodule
|
|
|
|
module tb;
|
|
|
|
localparam ADDR_WIDTH = 23;
|
|
localparam DATA_WIDTH = 8;
|
|
localparam P_IN = 8;
|
|
localparam ACC_WIDTH = 32;
|
|
localparam N_SLOTS = 2;
|
|
localparam QUEUE_DEPTH = 4;
|
|
|
|
reg clk, rst;
|
|
initial begin clk = 0; forever #5 clk = ~clk; end
|
|
|
|
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, slot_w_base, slot_result_addr;
|
|
wire [16*N_SLOTS-1:0] slot_n_tiles;
|
|
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 #(
|
|
.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(slot_x_base), .slot_w_base(slot_w_base),
|
|
.slot_n_tiles(slot_n_tiles), .slot_result_addr(slot_result_addr), .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)
|
|
);
|
|
|
|
genvar g;
|
|
generate
|
|
for (g = 0; g < N_SLOTS; g = g + 1) begin : GEN_SLOT
|
|
|
|
wire mm_operand_valid, mm_operand_ready;
|
|
wire signed [DATA_WIDTH*P_IN-1:0] mm_input_data, mm_weight_data;
|
|
wire mm_tile_last;
|
|
wire mm_result_valid, mm_result_ready;
|
|
wire signed [DATA_WIDTH-1:0] mm_result_data;
|
|
|
|
wire mem_req, mem_wr;
|
|
wire [ADDR_WIDTH-1:0] mem_addr;
|
|
wire signed [7:0] mem_wdata, mem_rdata;
|
|
wire mem_ready;
|
|
|
|
memory_manager #(
|
|
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ADDR_WIDTH(ADDR_WIDTH)
|
|
) u_mm (
|
|
.clk(clk), .rst(rst),
|
|
.job_start(slot_job_start[g]),
|
|
.x_base(slot_x_base[g*ADDR_WIDTH +: ADDR_WIDTH]),
|
|
.w_base(slot_w_base[g*ADDR_WIDTH +: ADDR_WIDTH]),
|
|
.n_tiles(slot_n_tiles[g*16 +: 16]),
|
|
.result_addr(slot_result_addr[g*ADDR_WIDTH +: ADDR_WIDTH]),
|
|
.job_done(slot_job_done[g]),
|
|
.operand_valid(mm_operand_valid), .operand_ready(mm_operand_ready),
|
|
.input_data(mm_input_data), .weight_data(mm_weight_data), .tile_last(mm_tile_last),
|
|
.result_valid(mm_result_valid), .result_ready(mm_result_ready), .result_data(mm_result_data),
|
|
.mem_req(mem_req), .mem_wr(mem_wr), .mem_addr(mem_addr), .mem_wdata(mem_wdata),
|
|
.mem_rdata(mem_rdata), .mem_ready(mem_ready)
|
|
);
|
|
|
|
reg job_valid_np;
|
|
wire job_ready_np;
|
|
wire result_valid_np;
|
|
wire signed [DATA_WIDTH-1:0] result_data_np;
|
|
wire [3:0] np_state;
|
|
wire np_error;
|
|
|
|
neural_processor #(
|
|
.DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH)
|
|
) u_np (
|
|
.clk(clk), .rst(rst),
|
|
.job_valid(job_valid_np), .job_ready(job_ready_np),
|
|
.job_node_id(16'h0), .job_bias(8'sd0), .job_activation(2'd1),
|
|
.operand_valid(mm_operand_valid), .operand_ready(mm_operand_ready),
|
|
.input_data(mm_input_data), .weight_data(mm_weight_data), .tile_last(mm_tile_last),
|
|
.result_valid(result_valid_np), .result_ready(mm_result_ready),
|
|
.result_data(result_data_np), .result_node_id(),
|
|
.np_state(np_state), .np_error(np_error)
|
|
);
|
|
assign mm_result_valid = result_valid_np;
|
|
assign mm_result_data = result_data_np;
|
|
|
|
always @(posedge clk) begin
|
|
if (rst) job_valid_np <= 1'b0;
|
|
else if (slot_job_start[g]) job_valid_np <= 1'b1;
|
|
else if (job_valid_np && job_ready_np) job_valid_np <= 1'b0;
|
|
end
|
|
|
|
sim_byte_mem #(.ADDR_WIDTH(ADDR_WIDTH), .DEPTH(4096)) u_mem (
|
|
.clk(clk), .rst(rst),
|
|
.req(mem_req), .wr(mem_wr), .addr(mem_addr), .wdata(mem_wdata),
|
|
.rdata(mem_rdata), .ready(mem_ready)
|
|
);
|
|
|
|
end
|
|
endgenerate
|
|
|
|
task automatic poke(input integer slot, input [ADDR_WIDTH-1:0] addr, input [7:0] val);
|
|
begin
|
|
case (slot)
|
|
0: tb.GEN_SLOT[0].u_mem.mem[addr] = val;
|
|
1: tb.GEN_SLOT[1].u_mem.mem[addr] = val;
|
|
default: ;
|
|
endcase
|
|
end
|
|
endtask
|
|
|
|
function automatic signed [7:0] peek(input integer slot, input [ADDR_WIDTH-1:0] addr);
|
|
begin
|
|
case (slot)
|
|
0: peek = tb.GEN_SLOT[0].u_mem.mem[addr];
|
|
1: peek = tb.GEN_SLOT[1].u_mem.mem[addr];
|
|
default: peek = 8'sdx;
|
|
endcase
|
|
end
|
|
endfunction
|
|
|
|
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
|
|
|
|
integer i;
|
|
integer wd;
|
|
|
|
initial begin
|
|
errors = 0; tests = 0;
|
|
rst = 1; 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(4) @(posedge clk);
|
|
rst = 0;
|
|
@(posedge clk);
|
|
|
|
// ---- prepare 3 independent jobs (2 slots, so job 2 must
|
|
// wait in the ready queue for a slot to free up) ----
|
|
// Job 0 (slot 0 pre-loaded region 0x100/0x200): 8 inputs, x=2,w=3 -> acc=48
|
|
for (i = 0; i < 8; i = i + 1) begin poke(0, 23'h100+i, 8'sd2); poke(0, 23'h200+i, 8'sd3); poke(1, 23'h100+i, 8'sd2); poke(1, 23'h200+i, 8'sd3); end
|
|
// Job 1 (either slot, region 0x300/0x400): 16 inputs, x=1,w=1 -> acc=16
|
|
for (i = 0; i < 16; i = i + 1) begin poke(0, 23'h300+i, 8'sd1); poke(0, 23'h400+i, 8'sd1); poke(1, 23'h300+i, 8'sd1); poke(1, 23'h400+i, 8'sd1); end
|
|
// Job 2 (queued until a slot frees, region 0x500/0x600): 8 inputs, x=1,w=5 -> acc=40
|
|
for (i = 0; i < 8; i = i + 1) begin poke(0, 23'h500+i, 8'sd1); poke(0, 23'h600+i, 8'sd5); poke(1, 23'h500+i, 8'sd1); poke(1, 23'h600+i, 8'sd5); end
|
|
|
|
submit_job(23'h100, 23'h200, 16'd1, 23'h700, 16'd1); // -> dispatches to slot 0 (first-free)
|
|
submit_job(23'h300, 23'h400, 16'd2, 23'h701, 16'd2); // -> dispatches to slot 1
|
|
submit_job(23'h500, 23'h600, 16'd1, 23'h702, 16'd3); // -> waits in queue
|
|
|
|
// job_in_ready should have stayed high throughout (only 3
|
|
// jobs, queue depth 4) -- confirmed implicitly: submit_job's
|
|
// own while-loop would have hung the testbench otherwise.
|
|
|
|
tests = tests + 3;
|
|
wd = 0;
|
|
begin
|
|
integer completions;
|
|
completions = 0;
|
|
while (completions < 3 && wd < 3000) begin
|
|
@(posedge clk);
|
|
wd = wd + 1;
|
|
if (job_out_done) completions = completions + 1;
|
|
end
|
|
if (completions < 3)
|
|
$display("FAIL: only %0d/3 jobs completed within watchdog", completions);
|
|
end
|
|
// give the last-completing job's write a little extra margin
|
|
repeat(5) @(posedge clk);
|
|
|
|
if (peek(0, 23'h700) !== 8'sd48) begin
|
|
$display("FAIL job0: result=%0d expected=48", peek(0,23'h700)); errors = errors + 1;
|
|
end else $display("PASS job0 (slot dispatched first-free): result=48");
|
|
|
|
if (peek(1, 23'h701) !== 8'sd16) begin
|
|
$display("FAIL job1: result=%0d expected=16", peek(1,23'h701)); errors = errors + 1;
|
|
end else $display("PASS job1 (slot dispatched first-free): result=16");
|
|
|
|
// Job 2 could have landed on either slot (whichever freed
|
|
// first) -- check both.
|
|
if (peek(0, 23'h702) !== 8'sd40 && peek(1, 23'h702) !== 8'sd40) begin
|
|
$display("FAIL job2 (queued): neither slot's result byte at 0x702 is 40 (got %0d / %0d)", peek(0,23'h702), peek(1,23'h702));
|
|
errors = errors + 1;
|
|
end else $display("PASS job2 (queued until a slot freed): result=40");
|
|
|
|
// ---- backpressure: occupy both slots with LONG jobs (many
|
|
// tiles, so they stay busy for a while and won't drain the
|
|
// queue mid-burst), then push jobs faster than they can be
|
|
// consumed and confirm job_in_ready genuinely deasserts once
|
|
// the queue fills, then recovers once slots free up again. ----
|
|
tests = tests + 1;
|
|
for (i = 0; i < 64; i = i + 1) begin poke(0, 23'h800+i, 8'sd1); poke(0, 23'h900+i, 8'sd1); poke(1, 23'h800+i, 8'sd1); poke(1, 23'h900+i, 8'sd1); end
|
|
submit_job(23'h800, 23'h900, 16'd64, 23'h704, 16'd10); // occupies slot 0/1 for a while
|
|
submit_job(23'h800, 23'h900, 16'd64, 23'h705, 16'd11); // occupies the other slot
|
|
|
|
job_in_x_base = 23'h100; job_in_w_base = 23'h200; job_in_n_tiles = 16'd1;
|
|
job_in_result_addr = 23'h706; job_in_node_id = 16'd12;
|
|
i = 0;
|
|
while (job_in_ready && i < QUEUE_DEPTH + 2) begin
|
|
@(posedge clk);
|
|
job_in_valid = 1'b1;
|
|
@(posedge clk);
|
|
i = i + 1;
|
|
end
|
|
if (i > QUEUE_DEPTH) begin
|
|
$display("FAIL backpressure: job_in_ready never deasserted after %0d pushes (QUEUE_DEPTH=%0d) -- both slots busy, queue should have filled", i, QUEUE_DEPTH);
|
|
errors = errors + 1;
|
|
end else begin
|
|
$display("PASS backpressure: job_in_ready correctly deasserted after %0d queued jobs (QUEUE_DEPTH=%0d), both slots busy", i, QUEUE_DEPTH);
|
|
end
|
|
job_in_valid = 1'b0;
|
|
|
|
// Drain: wait for everything (2 long jobs + whatever got
|
|
// queued) to finish, no watchdog failure, and confirm
|
|
// job_in_ready recovers once slots/queue free up.
|
|
wd = 0;
|
|
while (!job_in_ready && wd < 5000) begin @(posedge clk); wd = wd + 1; end
|
|
if (!job_in_ready) begin
|
|
$display("FAIL backpressure: job_in_ready never recovered within watchdog");
|
|
errors = errors + 1;
|
|
end else begin
|
|
$display("PASS backpressure: job_in_ready recovered once slots/queue drained");
|
|
end
|
|
|
|
$display("========================================");
|
|
if (errors == 0)
|
|
$display("ALL %0d TESTS PASSED (neural_director, first-free scheduling, N_SLOTS=%0d)", tests, N_SLOTS);
|
|
else
|
|
$display("FAILED: %0d/%0d test(s) had errors -- see messages above", errors, tests);
|
|
$display("========================================");
|
|
$finish;
|
|
end
|
|
|
|
endmodule
|