Files
FPGA-Neural/hardware/v2/sim/tb_neural_director.v
micheleandClaude Sonnet 5 2e4cedc761 feat(v2): M5 Neural Director, first-free job scheduling
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
2026-09-05 14:46:32 +02:00

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