feat: N=16 real timing CLOSED via extra MAC pipeline stage (EXP-0097, branch n16-timing-closure)

neural_processor_packed.v: split the original single "Stage 1" (packed
DSP48E1 multiply + INT8 unpack + register) into two real stages --
Stage 1a registers the raw DSP48E1 product with zero logic in between,
Stage 1b does the carry-heavy unpack (the real critical path EXP-0094
traced) from that already-registered value. Adds exactly one real
clock cycle of latency; throughput unaffected (real valid/ready
handshaking throughout, no fixed-latency assumption downstream).

Real verification: isolated bit-exact vs 2x real neural_processor.v
(18/18 PASS, testbench fixed to latch each core's result independently
since result_valid is a one-shot pulse and the DUT is now one cycle
deeper -- not an RTL bug). Full-system functional xsim on real DDR3:
32/32 PASS. Real, full P&R: WNS=+0.269ns, WHS=+0.026ns, 0 failing
setup or hold endpoints -- N=16 TIMING CLOSES.

Also root-caused (not an RTL bug, folded into CLAUDE.md): a real
Vivado incremental-synthesis quirk silently carried forward a
N_GROUPS=2 parameter binding from an earlier sweep run despite no
-generic override and an intervening reset_run -- fixed by always
passing -generic explicitly and confirming the real elaborated value
via a post-synth DSP48E1 count.

Isolated on this branch -- does not touch the physical board already
in fabrication on v3-artix7 (N=8, unmodified).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
This commit is contained in:
2026-09-22 00:17:18 +02:00
co-authored by Claude Sonnet 5
parent 4c59b7e7c8
commit f9b366d747
4 changed files with 271 additions and 40 deletions
+44 -16
View File
@@ -148,29 +148,57 @@ module tb;
v2_tile_last = 0;
tile_last = 0;
// real fix (n16-timing-closure branch): result_valid is a
// real ONE-SHOT pulse in every one of these FSMs (`NP_
// WRITE_RESULT: if (result_valid && result_ready)
// result_valid<=0`, identical in neural_processor.v and
// neural_processor_packed.v) -- with result_ready already
// held high before this wait begins, each core's own
// result_valid self-clears the very next cycle after it
// first asserts, independent of whether the OTHER cores
// have caught up yet. The original three-way simultaneous
// AND assumed all three cores share the exact same real
// pipeline depth -- true before this branch's own real
// extra pipeline stage in neural_processor_packed.v (added
// to fix EXP-0094's own real N=16 timing failure), no
// longer true now that the DUT is deliberately one real
// cycle deeper than the reference cores. Real fix: latch
// each core's own result independently the cycle its own
// result_valid pulses, then compare the three LATCHED
// values once all three have arrived -- correct regardless
// of real relative pipeline depth between DUT and
// reference.
v2_result_ready = 1;
result_ready = 1;
watchdog = 0;
while (!(v2a_result_valid && v2b_result_valid && result_valid) && watchdog < 300) begin
@(posedge clk);
watchdog = watchdog + 1;
end
begin : capture
reg v2a_got, v2b_got, dut_got;
reg signed [DATA_WIDTH-1:0] v2a_val, v2b_val, dut_val_a, dut_val_b;
v2a_got = 0; v2b_got = 0; dut_got = 0;
watchdog = 0;
while (!(v2a_got && v2b_got && dut_got) && watchdog < 300) begin
@(posedge clk);
if (!v2a_got && v2a_result_valid) begin v2a_got = 1; v2a_val = v2a_result_data; end
if (!v2b_got && v2b_result_valid) begin v2b_got = 1; v2b_val = v2b_result_data; end
if (!dut_got && result_valid) begin dut_got = 1; dut_val_a = result_data_a; dut_val_b = result_data_b; end
watchdog = watchdog + 1;
end
if (!v2a_result_valid || !v2b_result_valid || !result_valid) begin
$display("FAIL n=%0d: watchdog timeout waiting for results (v2a=%b v2b=%b dut=%b)",
n, v2a_result_valid, v2b_result_valid, result_valid);
errors = errors + 1;
end else begin
if (result_data_a !== v2a_result_data || result_data_b !== v2b_result_data) begin
$display("FAIL n=%0d bias=%0d act=%0d: v2a=%0d v2b=%0d dut_a=%0d dut_b=%0d MISMATCH",
n, bias, activation, v2a_result_data, v2b_result_data, result_data_a, result_data_b);
if (!v2a_got || !v2b_got || !dut_got) begin
$display("FAIL n=%0d: watchdog timeout waiting for results (v2a_got=%b v2b_got=%b dut_got=%b)",
n, v2a_got, v2b_got, dut_got);
errors = errors + 1;
end else begin
$display("PASS n=%0d bias=%0d act=%0d: a=%0d b=%0d (bit-exact vs 2x real neural_processor.v)",
n, bias, activation, result_data_a, result_data_b);
if (dut_val_a !== v2a_val || dut_val_b !== v2b_val) begin
$display("FAIL n=%0d bias=%0d act=%0d: v2a=%0d v2b=%0d dut_a=%0d dut_b=%0d MISMATCH",
n, bias, activation, v2a_val, v2b_val, dut_val_a, dut_val_b);
errors = errors + 1;
end else begin
$display("PASS n=%0d bias=%0d act=%0d: a=%0d b=%0d (bit-exact vs 2x real neural_processor.v)",
n, bias, activation, dut_val_a, dut_val_b);
end
end
@(posedge clk);
end
@(posedge clk);
while (!job_ready || np_state !== 4'd0 || !v2a_job_ready || !v2b_job_ready) @(posedge clk);
end