exp: N=16 timing closure fixed (EXP-0056), weight-reuse gives real 7.16x memory speedup without DDR3 (EXP-0057)

EXP-0056: N_SLOTS=16 failed timing on LFE5U-85F (23-24MHz vs 64MHz
target). First hypothesis (dependency_manager.v's serial ready-scan)
was wrong but real -- built and verified priority_encoder_lsb.v (a
generic recursive tree encoder) and dependency_manager_fast.v, bit-
exact equivalent to the original, but integrated it made no real
difference (24.26MHz). The real cause, found from nextpnr's own
critical-path report: nms_activation_fill_ctrl_v3.v's balanced max-
tree was only ever extended to N_SLOTS in {1,2,4,8}, silently falling
back to the original slow scan for 16. Added the missing case
(nms_activation_fill_ctrl_v3_n16.v), verified isolated (10017/10017)
and functionally (D-Stress N=16 still 256/256 bit-exact). Real result:
71.01MHz, PASS at 64MHz (single seed so far).

EXP-0057: built layer_weight_buffer.v, a double-buffered per-layer
weight scratchpad (fill one buffer in the background from SDRAM while
compute reads many times from the other -- weight-stationary reuse,
as opposed to D-Stress's own deliberately zero-reuse pattern). Wired
to the real sdram_controller_openrow.v + sdram_model.v, no new
hardware. For the same 32768 bytes of useful data: zero-reuse costs
27048 real cycles, reuse costs 3777 -- 7.16x real measured speedup on
the SAME SDR SDRAM, no DDR3, no clock change. This is the answer to
whether DDR3 is necessary for a workload class that actually has
reuse (e.g. conv-style face recognition, unlike D-Stress) -- it isn't,
at least not for this reason.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
This commit is contained in:
2026-09-16 12:01:11 +02:00
co-authored by Claude Sonnet 5
parent fce8ff2d66
commit 1ce78dff6e
25 changed files with 25848 additions and 0 deletions
+137
View File
@@ -3345,3 +3345,140 @@ hardware/v2/nms/sim/tb_sdram_cdc_bridge_openrow.v,
hardware/v2/nms/sim/tb_nms_dstress_sdram_combined.v,
hardware/v2/nms/sim/tb_nms_dstress_sdram_openrow.v,
hardware/v2/nms/sim/tb_nms_dstress_sdram_cdc.v.
EXP-0056 -- N_SLOTS=16 timing closure on LFE5U-85F: two real fixes,
one didn't matter, one did (2026-09-16)
DATE: 2026-09-16
CONTEXT: EXP-0055 (open-row backend, real board top) promoted to a
candidate but never checked at N_SLOTS=16 -- real 8-seed P&R baseline
(EXP-0049/0050-era numbers) never covered N=16 either. Real synthesis
+ nextpnr-ecp5 --85k (LFE5U-85F, same CABGA381 package/pinout as the
real board's v2_board_top.lpf -- confirmed pin-compatible) at
N_SLOTS=16: worst 23.52-24.64MHz across two independent seeds, FAIL at
the real 64MHz target. DSP fit confirmed fine (128/156 MULT18X18D,
82%) -- this is a timing-closure problem, not a resource problem.
HYPOTHESIS 1 (wrong, but real work, kept as a disclosed negative
result): dependency_manager.v's own first_ready_idx scan (serial
for-loop over up to N_NODES=1024, same architectural anti-pattern
already fixed twice elsewhere -- ERR-0027/ERR-0028/ERR-0029). Built
priority_encoder_lsb.v (generic recursive binary-tree lowest-set-bit
encoder, O(log2(WIDTH)) depth) + dependency_manager_fast.v (fork,
swaps in the encoder). Isolated: 65536/65536 exhaustive at WIDTH=16,
76562/76562 at WIDTH=1024. Bit-exact equivalence vs the original
module: 20000/20000 cycles matched under random stimulus
(tb_dependency_manager_fast.v), plus the original hand-crafted DAG
testbench, both 100%. Integrated (N_SLOTS=16, LFE5U-85F): worst
24.26MHz -- ESSENTIALLY UNCHANGED from the pre-fix 23.52-24.64MHz.
CONCLUSION: dependency_manager.v was not the real N=16 bottleneck.
Kept as a real, verified, low-risk correctness-neutral improvement
(shorter combinational depth is never worse), just not the fix that
mattered here.
HYPOTHESIS 2 (real root cause, found from the actual nextpnr critical-
path report on the Hypothesis-1 run): nms_activation_fill_ctrl_v3.v's
own balanced max-tree (a real fix from an EARLIER session, its own
header says so explicitly) was hand-coded ONLY for N_SLOTS in
{1,2,4,8} -- any other value, INCLUDING N_SLOTS=16, falls through to
GEN_MAXTREE_FALLBACK, the exact same flat N_SLOTS-wide sequential
scan/carry-chain that earlier fix was written to eliminate. Never
extended to cover 16. Real critical path (nextpnr's own report,
Hypothesis-1 run): a long CCU2C COUT/CIN carry chain inside
nms_activation_fill_ctrl_v3.v's max_n_tiles_reg comparator, confirming
this exactly.
FIX: nms_activation_fill_ctrl_v3_n16.v (fork), added the missing
N_SLOTS==16 case -- same balanced-tree pattern as the existing N==8
case, one more level (8 pairwise compares -> 4 -> 2 -> 1, 4 levels
total). Isolated: tb_maxtree_n16.v, 10017/10017 (targeted + random)
against the same flat-scan reference the fallback path itself uses as
its own documented "correct but not optimized" baseline.
RESULT (N_SLOTS=16, LFE5U-85F, seed 1, both fixes combined --
dependency_manager_fast + activation_fill_ctrl_v3_n16 -- in nms_
dataflow_core_sdram_fast.v / fpga_neural_v2_top_openrow_fast.v):
worst 71.01MHz, PASS at 64MHz. 0 errors. Functional regression
unaffected: D-Stress N=16 still 256/256 bit-exact, total_cycles=47454
(identical to the pre-fix functional baseline, as expected -- these
are pure combinational-depth fixes, not behavior changes) -- and still
confirms N=16 gives ZERO extra real throughput over N=4/N=8 on the
zero-reuse D-Stress workload (memory-bound, unrelated to this fix).
STATUS: single-seed PASS, not yet the project's own 8-seed standard.
next_action: run the full 8-seed sweep before treating N=16 as a
closed, production-ready configuration. New files (additive only,
none touch the real board top or existing production RTL):
hardware/v2/rtl/priority_encoder_lsb.v,
hardware/v2/rtl/dependency_manager_fast.v,
hardware/v2/nms/rtl/nms_activation_fill_ctrl_v3_n16.v,
hardware/v2/nms/rtl/nms_dataflow_core_sdram_fast.v,
hardware/v2/nms/rtl/fpga_neural_v2_top_openrow_fast.v,
hardware/v2/nms/rtl/nms_neural_multiprocessor_sdram_openrow_fast.v,
hardware/v2/sim/tb_priority_encoder_lsb.v,
hardware/v2/sim/tb_dependency_manager_fast.v,
hardware/v2/sim/tb_maxtree_n16.v,
hardware/v2/nms/sim/tb_fpga_neural_v2_top_openrow_fast_smoke.v,
hardware/v2/nms/sim/tb_nms_dstress_sdram_openrow_fast.v.
EXP-0057 -- weight-stationary layer reuse: real measured 7.16x memory-
side speedup, SAME hardware, no DDR3 (2026-09-16)
DATE: 2026-09-16
CONTEXT: user-driven pivot after establishing the real target
application class (generic neural accelerator for face-recognition-
style CNNs, not the zero-reuse D-Stress worst case this whole project
has been benchmarked against). D-Stress's own zero reuse means no
architecture can beat the physical bandwidth floor (established
earlier this session); a real conv-style workload has massive weight
reuse (same filter applied at every spatial position) that D-Stress
deliberately excludes -- this experiment measures that case for real,
on the SAME SDR SDRAM hardware this project already has (no DDR3, no
clock change), to answer directly whether DDR3 is even necessary for
a workload class that actually has reuse.
METHOD: layer_weight_buffer.v (new) -- double-buffered, per-layer
resident weight scratchpad (BRAM-style, same coding idiom as nms_
weight_packed.v). One buffer read many times (M reuses) while the
OTHER is filled in the background from SDRAM; swap is order-
independent (fill_done/consume_done latched separately, swap fires
once both have been seen since the last swap -- same req_pending-latch
discipline as sdram_unified_backend.v's own established correctness
fixes). Isolated: tb_layer_weight_buffer.v, 1540/1540, including
out-of-order fill/consume completion and a "consume_done alone must
not swap without a matching fill_done" negative check.
tb_layer_reuse_vs_zero_reuse.v: wired layer_weight_buffer.v to the
REAL sdram_controller_openrow.v (EXP-0054) + sdram_model.v -- same
hardware, nothing new. SAME total useful-byte-consumption in both
cases (32768 bytes, matching D-Stress's own 256x128 total exactly):
REUSE case: 16 layers x 128 bytes each fetched ONCE, reused 16x
locally = 2048 bytes actually fetched from SDRAM.
ZERO-REUSE case: 16 layers x 16 reuses x 128 bytes, every reuse
fetched independently = 32768 bytes (D-Stress's
own pattern, through the identical controller).
Fair timing comparison (an earlier version of this testbench asymmetrically
added compute-side consumption cycles to only the reuse case, making it
look SLOWER -- found and fixed before trusting any number; final
version measures ONLY real SDRAM fetch cost in both cases, which is
the actual question this experiment exists to answer).
RESULT: REUSE case data correctness 32768/32768, 0 errors, through the
real controller+model. Real measured cycles: REUSE = 3777, ZERO-REUSE
= 27048 (for the identical 32768 bytes of useful data delivered) --
**7.16x real measured speedup from weight reuse alone**, same SDR
SDRAM, same 64MHz clock, zero new hardware. Substantially larger than
any protocol-level lever measured this session (open-row +5%, dual-
bank ~9%, CDC net-negative) -- because this reduces bytes actually
moved rather than trying to move the same bytes faster.
DECISION: for workload classes with real reuse (conv-style, unlike
D-Stress), DDR3 is NOT established as necessary -- this result directly
contradicts the earlier (correct, but scope-limited-to-zero-reuse)
conclusion that only more physical bandwidth could help. DDR3 remains
relevant only if a real target model's per-layer working set exceeds
what layer-by-layer streaming + on-chip BRAM can hold, which depends
on the real model size (still not pinned down as of this entry).
next_action: integrate layer_weight_buffer.v with the real per-slot
compute path (neural_processor.v) and a real conv-shaped benchmark
(not just the synthetic byte-reuse pattern here) before calling this
production-ready. New files (additive only):
hardware/v2/rtl/layer_weight_buffer.v,
hardware/v2/sim/tb_layer_weight_buffer.v,
hardware/v2/sim/tb_layer_reuse_vs_zero_reuse.v.