feat: DDRManager phase 1 - single-slot look-ahead activation prefetch (EXP-0083)

New ddr_prefetch_mgr.v wraps act_tile_fetch.v with a depth-2 ping-pong
buffer, issuing the next tile's DDR3 fetch as soon as the fetch engine
is free instead of waiting for packed_slot.v to finish consuming the
current tile. Wired into packed_slot.v's tile loop (job-level start
instead of per-tile req), simplifying the S_TILEWAIT join in the process
(ddrpf_tile_valid is level-held, no separate act_seen latch needed).

Verification: new tb_ddr_prefetch_mgr.v (25/25 PASS after fixing a real
testbench polling race found via iteration-tagged tracing, not an RTL
bug), tb_packed_slot.v re-run unmodified (9/9 PASS, bit-identical
results), tb_n2_system_ddr3.v re-run via real xsim against real
ddr3_model.sv (8/8 PASS). Real P&R: WNS +0.073ns (up from EXP-0082's
+0.068ns), LUTs 5644, DSP48E1 16 unchanged, 0 failing endpoints.

Honest result: real A/B on the actual DDR3 backend (same testbench,
before/after) shows a real but modest 2.86% reduction in total
simulated time - smaller than the original hypothesis suggested, because
neural_processor_packed.v already accepts one operand per cycle, so the
per-tile dead time being removed was already small relative to real DDR3
fetch latency. Docs updated to report this honestly rather than oversell
it; the larger multi-slot DDRManager is deferred pending re-measurement
against the (still pending, user-gated) 32-bit channel widening.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUG92aM9m68TRc4rG55BcC
This commit is contained in:
2026-09-20 10:59:45 +02:00
co-authored by Claude Sonnet 5
parent cbd16dd727
commit fa327b75ca
6 changed files with 801 additions and 121 deletions
+100 -59
View File
@@ -1,15 +1,18 @@
# FPGA-Neural V3 — Architecture Analysis: Timing, Bottlenecks, and Recommended Interventions
Scope: the current, real, P&R-verified V3 design (`hardware/v3/`, branch
`v3-artix7`), updated through EXP-0082 (denser activation packing, real
P&R: WNS +0.068ns). Every number in this document is either directly
measured (real simulation trace, real P&R report) or a calculation built
from directly-measured building blocks — the two are labeled explicitly
throughout. Nothing here is guessed.
`v3-artix7`), updated through EXP-0083 (DDRManager phase 1, real P&R: WNS
+0.073ns). Every number in this document is either directly measured (real
simulation trace, real P&R report) or a calculation built from directly-
measured building blocks — the two are labeled explicitly throughout.
Nothing here is guessed.
**Status note (post EXP-0082)**: §5.1 (denser activation packing) described
below as a *recommendation* is now **DONE and real-P&R-verified** — see the
"DONE" marker in that section and the updated bandwidth numbers in §3. The
**Status note (post EXP-0083)**: §5.1 (denser activation packing) described
below as a *recommendation* is **DONE and real-P&R-verified** (EXP-0081/
0082), and §5.2 (DDRManager) phase 1 is **also DONE and real-measured**
(EXP-0083, a genuinely modest ~2.9% real benefit — see that section for the
honest number and why the original hypothesis overstated it). See the
"DONE" markers in those sections and the updated bandwidth numbers in §3. The
document originally analyzed the pre-fix state; it's kept below (marked
historical) because the comparison is itself informative, then updated with
the real post-fix numbers throughout.
@@ -31,7 +34,8 @@ DDR3 channel.
| Real DDR3 bandwidth needed for ONE core at peak DSP throughput | **4.96 GB/s** | calculated from measured DSP rate + memory layout (§3.2) |
| → DDR3 can sustain, pre-EXP-0081 packing (1 tile/burst) | **~25%** of one core's peak compute throughput | §3.2, historical |
| → DDR3 can sustain, post-EXP-0081/0082 packing (2 tiles/burst, DONE) | **~50%** of one core's peak compute throughput | §3.2, current, real |
| Real P&R timing margin (WNS) | **+0.068 ns** | measured, EXP-0082 real P&R (improved from +0.030ns pre-packing) |
| Real P&R timing margin (WNS) | **+0.073 ns** | measured, EXP-0083 real P&R (improved further from EXP-0082's +0.068ns, and EXP-0079's +0.030ns before that) |
| DDRManager phase 1 (single-slot look-ahead prefetch) real benefit | **2.86%** reduction in total real simulated time | measured, real xsim A/B on `tb_n2_system_ddr3.v` (§5.2, EXP-0083) — modest, honestly reported, not oversold |
| DSP48E1 headroom for scaling | 224/240 free (93%) | measured, real P&R utilization |
The DSP headroom is real and large. The memory-bandwidth ceiling is real,
@@ -60,10 +64,11 @@ against a 16-bit channel still hits the same physical bandwidth wall.
| 0076 | + register file, + pin constraints, + SPI physical-layer fix | +0.056 | 5173 | 16 | margin improved slightly (P&R is not perfectly monotonic run to run) |
| 0078 | + config-flash bridge (real STARTUPE2 placement) | +0.013 | 5213 | 16 | margin dropped — real added logic |
| 0079 | + real activation-fetch engine (`act_tile_fetch.v`) | +0.030 | 5379 | 16 | pre-packing baseline |
| 0082 | + denser activation packing (2 tiles/burst, EXP-0081) | **+0.068** | 5437 | 16 | current, final, trustworthy number — margin IMPROVED despite added mux logic |
| 0082 | + denser activation packing (2 tiles/burst, EXP-0081) | +0.068 | 5437 | 16 | pre-DDRManager baseline |
| 0083 | + DDRManager phase 1 (`ddr_prefetch_mgr.v`, single-slot look-ahead prefetch) | **+0.073** | 5644 | 16 | current, final, trustworthy number — margin IMPROVED again despite +207 LUTs |
**Observation**: WNS does not move monotonically with LUT count (0.056 →
0.013 → 0.030 → 0.068 while LUTs only ever grow) — this is normal P&R behavior
0.013 → 0.030 → 0.068 → 0.073 while LUTs only ever grow) — this is normal P&R behavior
(placer/router heuristics find different solutions each run, small logic
changes can shift which path is critical). **Do not extrapolate a trend
line from 3-4 data points** — the only safe practice is a fresh real P&R
@@ -293,43 +298,82 @@ burst, new-burst crossing, back-to-back alternation), `tb_packed_slot.v`
`tb_n2_system_ddr3.v` (8/8 PASS, real xsim against real `ddr3_model.sv`,
JEDEC trace confirmed to show no more half-burst zero-padding).
### 5.2 [Complementary, addresses latency not bandwidth] DDRManager with orchestrator-driven prefetch (user's proposal)
### 5.2 [Complementary, addresses latency not bandwidth] DDRManager with orchestrator-driven prefetch (user's proposal) — **phase 1 DONE (EXP-0083), real benefit smaller than the original hypothesis below predicted**
**The real problem this solves**: even within whatever bandwidth ceiling
§5.1 establishes, the CURRENT design only ever requests a tile the moment
`packed_slot.v`'s own FSM reaches `S_TILEREQ` for it — meaning the DSPs
stall waiting for that fetch's real latency (§3.3: 12.9-30+ ns) every
single tile, with no overlap between "fetching tile N+1" and "computing on
tile N". A DDRManager that issues tile N+1's fetch WHILE tile N is still
computing would hide that latency almost entirely (compute time per tile,
1/155.039MHz ≈ 6.4ns per cycle, vs a real fetch latency of 12.9-30+ns —
today's design is very likely stalling the DSPs for the majority of real
time, an real, additional cost on top of §3's raw bandwidth ceiling).
**The original hypothesis** (written before building anything, now corrected
by real measurement — kept here so the correction is visible, not silently
edited away): the CURRENT design only ever requests a tile the moment
`packed_slot.v`'s own FSM reaches `S_TILEREQ` for it, so a DDRManager that
issues tile N+1's fetch WHILE tile N is still being consumed should hide
"today's design is very likely stalling ... for the majority of real time".
**What it does NOT solve**: §3.2's bandwidth ceiling is a hard physical
limit (bytes/second the DDR3 channel can physically move) — prefetching
earlier doesn't move more bytes per second, it only avoids IDLE gaps where
the channel is free but nothing is queued to use it. **§5.1 and §5.2 are
complementary, not alternatives** — §5.1 reduces bytes needed per MAC, §5.2
ensures the channel is never idle when there's real bandwidth budget
available and useful work queued. Do both, in this order (§5.1 first, since
it raises the ceiling §5.2 will then use more fully).
**What the real EXP-0083 measurement actually found**: this hypothesis
overstated the achievable benefit, for a specific, now-confirmed reason —
`neural_processor_packed.v`'s own pipeline accepts one operand **per cycle**
whenever it's in `NP_WAIT_OPERANDS` (`operand_ready` is state-only, not
gated on any internal pipeline stall). The old design's real "dead time"
between one tile's fetch completing and the next one's fetch being issued
was therefore only the ~2-cycle request/consume handshake overhead
(`S_TILEREQ` + `S_OPERAND`), not a large compute-bound stall — and that
small overhead is what look-ahead prefetch can actually remove, not the
DDR3 fetch latency itself (which is dominated by row activation/precharge,
§3.3, and look-ahead cannot make a single fetch faster, only start it
earlier).
**Concrete design sketch** (informed by what already exists in this
codebase, not a from-scratch proposal):
**Real, measured result** (`ddr_prefetch_mgr.v`, real P&R WNS +0.073ns, up
from EXP-0082's +0.068ns, LUTs 5644, DSP48E1 16 unchanged):
- Real apples-to-apples comparison on the **real DDR3 backend**
(`tb_n2_system_ddr3.v` via real xsim, same N=2/8-position workload,
before vs after, same `ddr3_model.sv`): **2.86% reduction in total real
simulated time** (108370.88ns → 105268.43ns). This is the trustworthy
headline number.
- On the fast SDR placeholder backend (used for isolated glue-logic
testing, `tb_ddr_prefetch_mgr.v`): 0.9% reduction in a row-switch-heavy
scenario, and -1.4% (i.e. not faster) in an isolated same-row best case —
that placeholder model's own per-fetch cost turned out to be dominated by
a near-fixed protocol cost regardless of address locality, so it doesn't
cleanly isolate the mechanism the real DDR3 backend's own row/bank timing
does. Full detail: EXP-0083 in `hardware/v2/logs/experiments.log`.
**What it does NOT solve** (this part of the original reasoning holds):
§3.2's bandwidth ceiling is a hard physical limit (bytes/second the DDR3
channel can physically move) — prefetching earlier doesn't move more bytes
per second, it only avoids idle gaps. §5.1 (done) reduced bytes needed per
MAC; §5.4 (32-bit widening, decided, pending) raises the physical ceiling
itself; this phase-1 DDRManager only removes a small, now-quantified,
per-tile dead-time — real, free (zero timing cost, margin still improving),
but genuinely modest, not the larger win a first-principles estimate
suggested before it was actually built and measured.
**What was built** (`ddr_prefetch_mgr.v`, real RTL, not a sketch): wraps
`act_tile_fetch.v` (unmodified) with a depth-2 ping-pong buffer scoped to
ONE slot's own activation-tile look-ahead, exactly the validated,
scoped-first approach recommended below before this experiment ran. Bank
selection uses a registered index bit at both fill and read time, same
"known long before the data it gates" discipline as `act_tile_fetch.v`'s
own EXP-0081 layout — confirmed timing-safe by real P&R, not asserted.
**Full multi-slot / whole-Director-queue scheduler — still NOT built,
and now a more deliberate call, not just deferred**: given phase 1's real
measured benefit was modest, the cost/benefit case for the larger design
below should be re-examined against the 32-bit-widened channel's real
numbers (§5.4) before committing more engineering time to it — building it
now, on the still-16-bit channel, risks the same gap between hypothesis and
measurement this phase-1 experiment just corrected.
**Concrete design sketch for the full version** (informed by what already
exists in this codebase; kept for when it's revisited):
- `neural_director_packed.v` already queues up to `QUEUE_DEPTH=8` pending
jobs, each with a known `x_base`/`w_base`/`n_tiles` — this is exactly the
"reservation" information a DDRManager needs. No new bookkeeping is
required at the Director level; a DDRManager would READ this existing
queue, not need the Director to change its own job-acceptance logic.
- A new module (name suggestion: `ddr_prefetch_mgr.v`) would sit between
`sdram_arbiter_n.v` and the per-slot `act_tile_fetch.v`/
`layer_prefetch_ctrl.v` instances, with a small staging buffer per slot
(double-buffered, matching `layer_weight_buffer.v`'s own already-proven
double-buffer pattern) — while `packed_slot.v` computes on the CURRENT
tile, the manager issues the request for the NEXT tile into the "other"
buffer, swapping on completion.
"reservation" information a full DDRManager needs. No new bookkeeping is
required at the Director level; it would READ this existing queue, not
need the Director to change its own job-acceptance logic.
- A cross-slot manager would sit between `sdram_arbiter_n.v` and each
slot's own `ddr_prefetch_mgr.v`/`layer_prefetch_ctrl.v` instances,
scheduling across slots (not just within one slot's own tile loop as
phase 1 does) — e.g. prioritizing requests that share an already-open
DDR3 row across DIFFERENT slots, which phase 1 cannot see or exploit.
- **Real caveat, not glossed over**: this adds real arbitration complexity
— a prefetched-but-not-yet-consumed request competing with another slot's
genuinely urgent request needs a real priority policy, not just
@@ -337,13 +381,6 @@ codebase, not a from-scratch proposal):
speculative prefetch that turns out to be wrong (e.g., the Director
reorders/never dispatches that queued job) also wastes real bandwidth —
needs a real cancellation/staleness mechanism, not assumed away.
- **Recommended validation before committing engineering time**: build a
minimal version scoped to ONE slot's OWN activation-tile look-ahead
(prefetch tile N+1 while computing tile N, using the double-buffer
pattern above) before attempting the full "reserve across the whole
Director queue" version — matches this project's own "one variable at a
time" discipline, and would give a real, measured stall-reduction number
to justify (or not) the added complexity of the full design.
### 5.3 [Blocking for any real scaling] Result-writeback engine
@@ -380,7 +417,7 @@ this package (the DQS-capable pins exist), but:
| Pin cost | Reuses/extends the existing MIG's own bank(s); no new bank claimed | Would claim banks 14 **and/or** 15 (the only banks with free DQS-capable pins) |
| **Conflict with already-placed I/O** | None | **Real, direct**: the management-SPI bus (bank 15: A15/B16/B17/A16) and the config-flash bus (bank 14: K17/K18/L13) are already placed in exactly the banks that would need to host a second channel. Only bank 16 (11 pins) would remain free — not enough margin for either SPI bus, let alone both. |
| Controller logic cost | One MIG instance, wider data path (mostly automatic — the MIG wizard regenerates CAS/CWL/MMCM ratios for the new width) | A full second MIG instance: second calibration sequence, second `ui_clk` domain, and critically the **DDRManager/arbiter would need to become channel-aware**, not just requester-aware — real added complexity on top of §5.2's own design, not a simplification |
| Real risk given current thin margin (+0.068ns) | Lower — one controller, one clock domain, incremental change to an already-proven design | Higher — two independent PHYs, two calibration state machines, cross-channel coordination logic, all new |
| Real risk given current thin margin (+0.073ns) | Lower — one controller, one clock domain, incremental change to an already-proven design | Higher — two independent PHYs, two calibration state machines, cross-channel coordination logic, all new |
| PCB impact | None (same pins, same DDR3 part, different bus width usage — **the physical board the user is designing does not need to change** for this) | Would require re-routing/relocating whichever board-level bus (SPI mgmt or flash) currently occupies bank 14/15 pins — real PCB-level rework, not just RTL |
**Recommendation (honest, not deferential, as requested)**: **32-bit single-
@@ -420,10 +457,10 @@ case on the current 16-bit channel. **Building N=4/8/16 before §5.2/§5.4 are
in place would very likely show near-IDENTICAL real throughput to N=2** — a
real, wasted engineering cycle the analysis recommends avoiding.
**Decided real order of work** (§5.1 already done; this reflects the user's
own explicit final direction — 32-bit widening, then a complete DDRManager,
then N=2/4/8/16 tests, real target N=8, N=16 built specifically to document
where/how it breaks rather than to succeed):
**Decided real order of work** (§5.1 and §5.2-phase-1 already done; this
reflects the user's own explicit final direction — 32-bit widening, then
the DDRManager, then N=2/4/8/16 tests, real target N=8, N=16 built
specifically to document where/how it breaks rather than to succeed):
1. ~~§5.3 (result-writeback)~~ / ~~§5.1 (denser activation packing)~~ — §5.1
**DONE** (EXP-0081/0082). §5.3 remains a genuine blocker for N>2 and must
land before any scaling test that needs real result data out of more than
@@ -432,9 +469,12 @@ where/how it breaks rather than to succeed):
interactive MIG wizard session (Data Width + Input Clock Period changed
together). Doubles the physical ceiling itself, which §5.1 alone could
not do.
3. §5.2 (DDRManager, "evoluto e completo" per the user's own spec) — build
against the widened channel so its benefit is measured against the real
final bandwidth budget, not the pre-widening one.
3. §5.2 (DDRManager) — phase 1 (single-slot look-ahead prefetch) is
**DONE** (EXP-0083, real but modest ~2.9% benefit on the still-16-bit
channel). Re-measure this SAME real A/B once §5.4 lands, since a wider
channel may change how much idle-channel time there is left to fill —
only build the larger multi-slot scheduler version if that re-measurement
justifies it, not on the original (now-corrected) hypothesis alone.
4. Real N=2/4/8/16 tests, each with its own real P&R signoff (margin is
thin, §2 — do not assume a prior N's timing closure predicts the next).
N=8 is the real target configuration; N=16 is expected to expose real
@@ -447,7 +487,7 @@ where/how it breaks rather than to succeed):
| Claim | Status |
|---|---|
| WNS/WHS/LUT/DSP numbers throughout | **Measured** (real Vivado P&R reports, current: EXP-0082) |
| WNS/WHS/LUT/DSP numbers throughout | **Measured** (real Vivado P&R reports, current: EXP-0083) |
| DDR3 back-to-back burst throughput (1.24 GB/s) | **Measured** (real `ddr3_model.sv` JEDEC trace) — physical channel limit, unchanged by §5.1's packing fix |
| Real Activate→Read latency (16.125 ns) | **Measured** (same trace) |
| Per-core compute throughput (2.48 GMAC/s) | **Calculated** from measured Fmax (155.039MHz) + known, fixed DSP-packing factor |
@@ -456,5 +496,6 @@ where/how it breaks rather than to succeed):
| "~25% of peak sustainable" (pre-packing) / "~50%" (post-packing, current) | **Calculated** ratios; post-packing figure re-verified against real P&R (EXP-0082) and real simulation (§5.1) |
| I/O bank/DQS pin counts for XC7A100T-CSG324 (banks 14/15/16/34/35) | **Measured** — queried directly from the real Vivado part database for this exact part/package, used in §5.4's dual-channel-vs-widening analysis |
| Row-switch penalty as a fraction of real workloads | **Not measured** — depends on host-chosen memory layout, flagged as an open question, not asserted |
| DDRManager's real stall-reduction benefit | **Not measured**no prototype exists yet; §5.2 recommends building a minimal version specifically to get this real number |
| DDRManager phase-1 real stall-reduction benefit (2.86%) | **Measured**real xsim A/B on `tb_n2_system_ddr3.v`, real DDR3 backend, before vs after `ddr_prefetch_mgr.v` (EXP-0083). Modest, not the larger figure the original hypothesis (§5.2) suggested before it was built. |
| Full multi-slot DDRManager's real benefit | **Not measured** — not built; §5.2 recommends re-measuring phase 1 against the 32-bit-widened channel before deciding whether to build it |
| 32-bit widening's real post-change bandwidth/timing numbers | **Not measured** — requires the user's own interactive MIG wizard session (§5.4); this document's ~2.48 GB/s figure is a doubling projection, not yet re-verified by real P&R/simulation |
+20 -6
View File
@@ -118,7 +118,7 @@ these from an ESP32 doing real JTAG bit-banging (TAP state machine, IR/DR
shifting) rather than a bench programmer — that firmware is separate,
software-side work, not covered here.
## 3. Real timing signoff (EXP-0082, the current, trustworthy number)
## 3. Real timing signoff (EXP-0083, the current, trustworthy number)
Real in-context Vivado place-and-route (not out-of-context, not estimated):
@@ -126,9 +126,10 @@ Real in-context Vivado place-and-route (not out-of-context, not estimated):
|---|---|
| DDR3 PHY clock (sys_clk_i) | **310.078 MHz** (3.225ns period) |
| Compute domain clock (ui_clk, PLL-derived 2:1 from sys_clk_i) | **155.039 MHz** |
| WNS (setup slack) | **+0.068 ns** — real, still thin but improved vs EXP-0078/0079. Re-verify with a fresh P&R after ANY further logic addition. |
| WNS (setup slack) | **+0.073 ns** — real, still thin but improved vs every prior real run. Re-verify with a fresh P&R after ANY further logic addition. |
| WHS (hold slack) | +0.036 ns |
| Failing endpoints | 0 (setup), 0 (hold) |
| LUTs used | 5437 / 63400 (8.58%) |
| LUTs used | 5644 / 63400 (8.90%) |
| DSP48E1 used | 16 / 240 (6.67%) — 8 per compute core × 2 cores, unchanged since EXP-0059 |
| Block RAM used | 0 |
| STARTUPE2 used | 1 / 1 (100%) — the config-flash bridge |
@@ -141,7 +142,8 @@ Signoff history (every real change, same target, in-context P&R):
| 0076 (+regfile, +pins, +SPI fix) | +0.056 | 5173 | 16 |
| 0078 (+flash bridge, STARTUPE2) | +0.013 | 5213 | 16 |
| 0079 (+real activation engine) | +0.030 | 5379 | 16 |
| 0082 (+denser activation packing) | **+0.068** | 5437 | 16 |
| 0082 (+denser activation packing) | +0.068 | 5437 | 16 |
| 0083 (+DDRManager phase 1, `ddr_prefetch_mgr.v`) | **+0.073** | 5644 | 16 |
## 4. Real DDR3 memory layout convention
@@ -243,11 +245,23 @@ table is a summary/index.
## 7. Known-open items (honestly disclosed, not hidden)
- Scaling past N=2 compute cores (silicon budget allows up to ~30 per the
DSP48E1 count) is not yet built or timing-verified.
DSP48E1 count) is not yet built or timing-verified. A result-writeback
engine (currently `result_data_a/b` are literal top-level pins, fine at
N=2 but a real pin-budget blocker beyond that) must land first — see
`docs/ARCHITECTURE_ANALYSIS.md` §5.3.
- The reset pin and other very-low-pin-count signals have no fixed PCB
location yet — assign once the rest of the board layout (reset circuit,
status LEDs, etc.) is decided.
- The §3 timing margin (+0.013ns) is real but thin — do not add logic
- The §3 timing margin (+0.073ns) is real but thin — do not add logic
without a fresh real P&R to confirm it still closes.
- ESP32-side JTAG bit-banging firmware (§5.1) does not exist yet — it's
software work on the host side, not part of this FPGA RTL.
- A 32-bit-wide DDR3 channel (decided, see `docs/ARCHITECTURE_ANALYSIS.md`
§5.4) requires the user's own real interactive Vivado MIG "Customize IP"
wizard session (Data Width + Input Clock Period changed together) —
not yet done; every timing/bandwidth number in this document still
reflects the current 16-bit channel.
- `ddr_prefetch_mgr.v` (DDRManager phase 1, EXP-0083) is real, built, and
timing-verified, but its measured benefit (2.86% real reduction in total
simulated time) is modest — see `docs/ARCHITECTURE_ANALYSIS.md` §5.2 for
the honest number and why. The larger multi-slot DDRManager is not built.