perf: fix N=4@64MHz 7/8->8/8 by removing any_pending fan-out (user's idea)

Real critical-path trace after the flash #1 revert showed a NEW
bottleneck (neural_director.job_out_slot -> dependency_manager.
node_resolved/node_state, 76-84% routing) distinct from every prior
fix this session -- already a flat, parallel 64-way compare, not a
serial chain, so the established restructuring fix class doesn't
apply. Root cause: any_pending (added for FPGA_DATA_READY) reads
node_state[0:N_NODES-1] combinationally every cycle, adding real
fan-out onto the same congested signal.

User's own suggestion: replace the combinational scan with a
synchronous up/down counter. pending_count +1 on registration
acceptance, -1 on dispatch acceptance; any_pending = (pending_count
!= 0) -- mathematically identical (DEC-0008: nodes never reclaimed
mid-run) but reads one small register instead of scanning a 16-wide
array every cycle.

Verified: D-Stress N=4 bit-exact (49927 cycles, data_ready PASS).
Fresh 8-seed P&R: N_SLOTS=4 @ 64MHz now 8/8 PASS (was 7/8 after the
flash revert), worst seed1 64.55MHz, best seed0 72.37MHz.

See decisions.log DEC-0042.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
This commit is contained in:
2026-09-07 19:16:01 +02:00
co-authored by Claude Sonnet 5
parent ee708775c7
commit 481b5d223f
2 changed files with 91 additions and 11 deletions
+31 -11
View File
@@ -118,16 +118,26 @@ module dependency_manager #(
end
end
// ---- FPGA_DATA_READY support (see any_pending port comment above) ----
reg any_pending_r;
integer pi;
always @(*) begin
any_pending_r = 1'b0;
for (pi = 0; pi < N_NODES; pi = pi + 1)
if (node_state[pi] == ST_WAITING || node_state[pi] == ST_READY)
any_pending_r = 1'b1;
end
assign any_pending = any_pending_r;
// ---- FPGA_DATA_READY support (see any_pending port comment above).
// Originally a combinational OR-reduce over node_state[0:N_NODES-1]
// (16-wide), which added real fan-out load onto node_state -- a
// signal this session's own real P&R critical-path traces later
// showed sitting on the SAME already-congested job_out_slot ->
// node_resolved/node_state broadcast path (routing-dominated,
// 76-84%). Replaced with a synchronous up/down counter: +1 on a
// node's own registration acceptance (reg_valid&&reg_ready --
// exactly when it enters WAITING/READY), -1 on its own dispatch
// acceptance (ready_valid&&ready_ready -- exactly when it leaves
// WAITING/READY for DISPATCHED). registered-minus-dispatched is
// mathematically identical to the original OR-reduce's own
// "any node currently WAITING or READY" condition (DEC-0008: nodes
// are never reclaimed mid-run, so every node visits EMPTY ->
// {WAITING or READY} -> DISPATCHED exactly once), but reads a
// single small registered counter instead of scanning a wide array
// every cycle -- zero added fan-out on the congested signals. ----
localparam PENDW = $clog2(N_NODES+1);
reg [PENDW-1:0] pending_count;
assign any_pending = (pending_count != {PENDW{1'b0}});
integer ni, di;
@@ -138,9 +148,19 @@ module dependency_manager #(
node_required[ni] <= {REQW{1'b0}};
node_resolved[ni] <= {REQW{1'b0}};
end
ready_valid <= 1'b0;
ready_valid <= 1'b0;
pending_count <= {PENDW{1'b0}};
end else begin
// pending_count: +1 on registration acceptance, -1 on
// dispatch acceptance; a same-cycle occurrence of both is a
// net zero change (no assignment needed, old value holds).
case ({(reg_valid && reg_ready), (ready_valid && ready_ready)})
2'b10: pending_count <= pending_count + 1'b1;
2'b01: pending_count <= pending_count - 1'b1;
default: ; // 00 or 11: no net change
endcase
// ---- registration: create a new WAITING (or immediately
// READY, if required==0) node entry. ----
if (reg_valid && reg_ready) begin