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:
@@ -12,12 +12,20 @@
|
||||
// to keep this module's own pipeline depth/stage count identical to
|
||||
// the V2 original for a direct structural comparison).
|
||||
//
|
||||
// Pipeline stages match V2's neural_processor.v exactly, just doubled
|
||||
// on the accumulator side (one accumulate/bias/activation/saturation
|
||||
// path per job, A and B, sharing the SAME multiply/adder-tree stages
|
||||
// since they consume the SAME weight stream):
|
||||
// Stage 0 input alignment (x0_a, x0_b, w0 -- ONE shared weight)
|
||||
// Stage 1 P_IN packed-MAC lanes: p0[i]=x0_a[i]*w0[i], p1[i]=x0_b[i]*w0[i]
|
||||
// Pipeline stages, originally matched V2's neural_processor.v exactly
|
||||
// (one accumulate/bias/activation/saturation path per job, A and B,
|
||||
// sharing the SAME multiply/adder-tree stages since they consume the
|
||||
// SAME weight stream). EXTENDED BY ONE REAL STAGE on the
|
||||
// n16-timing-closure branch (real fix for EXP-0094's own real, traced
|
||||
// N=16 P&R timing failure -- see Stage 1a/1b's own header comments for
|
||||
// the full real root-cause story):
|
||||
// Stage 0 input alignment (x0_a, x0_b, w0 -- ONE shared weight)
|
||||
// Stage 1a P_IN real DSP48E1 packed multiplies, registered raw
|
||||
// (product_reg) -- NEW real stage
|
||||
// Stage 1b unpack the two packed INT8 products from product_reg:
|
||||
// p0[i]=x0_a[i]*w0[i], p1[i]=x0_b[i]*w0[i] -- same real
|
||||
// math as the original single "Stage 1", now one real
|
||||
// cycle later
|
||||
// Stage 2..(1+TREE_LEVELS) TWO balanced adder trees (A and B)
|
||||
// Stage (2+TREE_LEVELS) TWO accumulators
|
||||
// Stage (3+TREE_LEVELS) bias add (shared bias/activation -- same
|
||||
@@ -25,6 +33,14 @@
|
||||
// + activation, per job
|
||||
// Stage (4+TREE_LEVELS) INT8 saturation / output register, per job
|
||||
//
|
||||
// Real, deliberate consequence: end-to-end per-tile latency grows by
|
||||
// exactly ONE real clock cycle versus the original design (throughput
|
||||
// is unaffected -- the pipeline still accepts one new operand per
|
||||
// cycle in steady state). Functional behavior (the actual packed-MAC
|
||||
// arithmetic) is byte-for-byte unchanged -- verified bit-exact against
|
||||
// the same real reference used since EXP-0059 (2x real
|
||||
// hardware/v2/rtl/neural_processor.v), `tb_neural_processor_packed.v`.
|
||||
//
|
||||
// job_bias/job_activation are SHARED between A and B (same resident
|
||||
// neuron), matching this project's own weight-reuse semantics (a
|
||||
// neuron/filter's bias and activation type don't vary by spatial
|
||||
@@ -116,17 +132,28 @@ module neural_processor_packed #(
|
||||
end
|
||||
|
||||
// ============================================================
|
||||
// STAGE 1 -- P_IN packed-MAC lanes (mac2_dsp_packed.v's own
|
||||
// verified combinational formula, inlined per lane)
|
||||
// STAGE 1a -- P_IN real DSP48E1 packed multiplies, registered RAW
|
||||
// (n16-timing-closure branch, real fix for EXP-0094's own real,
|
||||
// traced N=16 critical path). EXP-0093/0094's own real post-route
|
||||
// reports found the worst violated path running from a DSP48E1's
|
||||
// own (Vivado-auto-retimed) product register straight through the
|
||||
// pb_comb unpack logic below (a real, CARRY4-heavy shift + carry-
|
||||
// propagate add) into proda1/prodb1 in a SINGLE cycle -- already
|
||||
// razor-thin at N=2 (WNS=+0.0999962ns, EXP-0088) and pushed
|
||||
// negative by N=16's own extra real placement congestion (EXP-
|
||||
// 0093/0094). This stage makes the DSP's own real output register
|
||||
// explicit in RTL (captures the WHOLE raw packed product, zero
|
||||
// logic in between) instead of relying on the tool to retime one
|
||||
// in automatically -- the actual, additional real pipeline stage
|
||||
// this fix needs is STAGE 1b below, which now has its own full
|
||||
// real clock period to do the unpack work in.
|
||||
// ============================================================
|
||||
reg valid1, last1;
|
||||
reg signed [ACC_WIDTH-1:0] proda1 [0:P_IN-1];
|
||||
reg signed [ACC_WIDTH-1:0] prodb1 [0:P_IN-1];
|
||||
|
||||
localparam A_WIDTH = 3*DATA_WIDTH + 1;
|
||||
localparam PRODUCT_WIDTH = A_WIDTH + DATA_WIDTH;
|
||||
|
||||
wire signed [PROD_WIDTH-1:0] pa_comb [0:P_IN-1];
|
||||
wire signed [PROD_WIDTH-1:0] pb_comb [0:P_IN-1];
|
||||
wire signed [PRODUCT_WIDTH-1:0] product_comb [0:P_IN-1];
|
||||
reg signed [PRODUCT_WIDTH-1:0] product_reg [0:P_IN-1];
|
||||
|
||||
genvar gm;
|
||||
generate
|
||||
@@ -134,12 +161,7 @@ module neural_processor_packed #(
|
||||
wire signed [A_WIDTH-1:0] x0_sext25 = {{(A_WIDTH-DATA_WIDTH){xa0[gm][DATA_WIDTH-1]}}, xa0[gm]};
|
||||
wire signed [A_WIDTH-1:0] x1_shifted = $signed(xb0[gm]) <<< (2*DATA_WIDTH);
|
||||
wire signed [A_WIDTH-1:0] packed_a = x1_shifted + x0_sext25;
|
||||
wire signed [A_WIDTH+DATA_WIDTH-1:0] product = packed_a * w0[gm];
|
||||
|
||||
assign pa_comb[gm] = product[PROD_WIDTH-1:0];
|
||||
wire signed [A_WIDTH+DATA_WIDTH-2*DATA_WIDTH-1:0] pb_raw =
|
||||
$signed(product) >>> (2*DATA_WIDTH);
|
||||
assign pb_comb[gm] = pb_raw[PROD_WIDTH-1:0] + (pa_comb[gm][PROD_WIDTH-1] ? 1'b1 : 1'b0);
|
||||
assign product_comb[gm] = packed_a * w0[gm];
|
||||
end
|
||||
endgenerate
|
||||
|
||||
@@ -150,6 +172,45 @@ module neural_processor_packed #(
|
||||
end else begin
|
||||
valid1 <= valid0;
|
||||
last1 <= last0;
|
||||
for (gi = 0; gi < P_IN; gi = gi + 1)
|
||||
product_reg[gi] <= product_comb[gi];
|
||||
end
|
||||
end
|
||||
|
||||
// ============================================================
|
||||
// STAGE 1b -- unpack the two packed INT8 products from the
|
||||
// ALREADY-REGISTERED product_reg (real, added pipeline stage --
|
||||
// the actual timing fix). pa_comb/pb_comb's own math is byte-for-
|
||||
// byte IDENTICAL to the original single-stage version, only the
|
||||
// source (product_reg, a real register) and the register that
|
||||
// captures the result (proda1/prodb1, now one real cycle later)
|
||||
// changed -- functional behavior is unchanged, only latency grows
|
||||
// by exactly one real clock cycle.
|
||||
// ============================================================
|
||||
reg valid1b, last1b;
|
||||
reg signed [ACC_WIDTH-1:0] proda1 [0:P_IN-1];
|
||||
reg signed [ACC_WIDTH-1:0] prodb1 [0:P_IN-1];
|
||||
|
||||
wire signed [PROD_WIDTH-1:0] pa_comb [0:P_IN-1];
|
||||
wire signed [PROD_WIDTH-1:0] pb_comb [0:P_IN-1];
|
||||
|
||||
genvar gp;
|
||||
generate
|
||||
for (gp = 0; gp < P_IN; gp = gp + 1) begin : GEN_UNPACK
|
||||
assign pa_comb[gp] = product_reg[gp][PROD_WIDTH-1:0];
|
||||
wire signed [A_WIDTH+DATA_WIDTH-2*DATA_WIDTH-1:0] pb_raw =
|
||||
$signed(product_reg[gp]) >>> (2*DATA_WIDTH);
|
||||
assign pb_comb[gp] = pb_raw[PROD_WIDTH-1:0] + (pa_comb[gp][PROD_WIDTH-1] ? 1'b1 : 1'b0);
|
||||
end
|
||||
endgenerate
|
||||
|
||||
always @(posedge clk) begin
|
||||
if (rst) begin
|
||||
valid1b <= 1'b0;
|
||||
last1b <= 1'b0;
|
||||
end else begin
|
||||
valid1b <= valid1;
|
||||
last1b <= last1;
|
||||
for (gi = 0; gi < P_IN; gi = gi + 1) begin
|
||||
proda1[gi] <= {{(ACC_WIDTH-PROD_WIDTH){pa_comb[gi][PROD_WIDTH-1]}}, pa_comb[gi]};
|
||||
prodb1[gi] <= {{(ACC_WIDTH-PROD_WIDTH){pb_comb[gi][PROD_WIDTH-1]}}, pb_comb[gi]};
|
||||
@@ -183,8 +244,8 @@ module neural_processor_packed #(
|
||||
valid_tree[gl] <= 1'b0;
|
||||
last_tree[gl] <= 1'b0;
|
||||
end else begin
|
||||
valid_tree[gl] <= (gl == 0) ? valid1 : valid_tree[gl-1];
|
||||
last_tree[gl] <= (gl == 0) ? last1 : last_tree[gl-1];
|
||||
valid_tree[gl] <= (gl == 0) ? valid1b : valid_tree[gl-1];
|
||||
last_tree[gl] <= (gl == 0) ? last1b : last_tree[gl-1];
|
||||
end
|
||||
end
|
||||
for (gn = 0; gn < (P_IN >> (gl+1)); gn = gn + 1) begin : GEN_TREE_NODE
|
||||
@@ -203,8 +264,8 @@ module neural_processor_packed #(
|
||||
end
|
||||
endgenerate
|
||||
|
||||
wire valid_tree_out = (TREE_LEVELS == 0) ? valid1 : valid_tree[TREE_LEVELS-1];
|
||||
wire last_tree_out = (TREE_LEVELS == 0) ? last1 : last_tree[TREE_LEVELS-1];
|
||||
wire valid_tree_out = (TREE_LEVELS == 0) ? valid1b : valid_tree[TREE_LEVELS-1];
|
||||
wire last_tree_out = (TREE_LEVELS == 0) ? last1b : last_tree[TREE_LEVELS-1];
|
||||
wire signed [ACC_WIDTH-1:0] tile_sum_a = (TREE_LEVELS == 0) ? proda1[0] : treea[TREE_LEVELS][0];
|
||||
wire signed [ACC_WIDTH-1:0] tile_sum_b = (TREE_LEVELS == 0) ? prodb1[0] : treeb[TREE_LEVELS][0];
|
||||
|
||||
@@ -294,7 +355,7 @@ module neural_processor_packed #(
|
||||
end
|
||||
end
|
||||
|
||||
wire pipeline_busy = valid0 || valid1 || (|valid_tree) || valid5 || valid6 || valid7;
|
||||
wire pipeline_busy = valid0 || valid1 || valid1b || (|valid_tree) || valid5 || valid6 || valid7;
|
||||
assign job_ready = (np_state == NP_IDLE) && !pipeline_busy;
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user