feat(v2): M4 Memory Manager + Prefetch Engine, real V1 PSRAM backend

Implements M4: memory_manager.v (arbitration/buffering/forwarding/
latency hiding/double buffering, §12) + prefetch_engine.v
(double-buffered tile fetch, §13), sitting on the REAL, UNMODIFIED V1
PSRAM backend chain (int8_memory_access.v -> memory_interface.v ->
psram_controller.v, per §15's explicit mandate not to touch the
controller).

Verified fully end-to-end with Verilator: real neural_processor (M1)
fed entirely by memory_manager, computing against PSRAM-resident X/W
tiles (double-buffered prefetch across up to 5 tiles) and writing its
result back to PSRAM -- checked via an independent PSRAM read-back,
with poison bytes around the operand regions to catch addressing
errors. 3/3 jobs pass (1/3/5-tile configurations).

Three real RTL bugs found and fixed during integration (full
diagnostic trail in errors.log ERR-0006): prefetch_engine had no
single-in-flight-request discipline, letting a queued request corrupt
the bank bookkeeping of a fetch already running; the fix's own
!pf_busy guard had a one-cycle blind spot (pf_busy lags pf_start by a
clock) that needed an explicit !pf_start term; and a state-based mux
for the shared backend port was off by one cycle, silently dropping
the PSRAM result write entirely.

Real synthesis: 0 CHECK problems, 851 LUT4/789 FF/108 CCU2C/0 DSP
(expected, no multiplication in this module). Real place&route (via a
synthesis-only timing harness, needed for the same TRELLIS_IO pin-
budget reason as M2's array): Fmax 165.86 MHz, PASS at 80MHz.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
This commit is contained in:
2026-09-05 14:39:29 +02:00
co-authored by Claude Sonnet 5
parent 5f0d7f101c
commit 175f697ae1
20 changed files with 137935 additions and 2 deletions
+15
View File
@@ -59,3 +59,18 @@ wide) scale as expected with depth. Confirms §14's warning literally:
"non assumere che buffer piu' grandi siano automaticamente migliori"
-- here, smaller was not cheaper either, because depth was the wrong
lever for this specific buffer's cost.
[2026-09-05] M4 Memory Manager + Prefetch Engine (standalone resource
count; Fmax via timing harness -- see errors.log ERR-0005)
| Module | Fmax (POST-P&R) | LUT | FF | DSP | CCU2C |
|----------------------------------|------------------|-----|-----|-----|-------|
| memory_manager + prefetch_engine | 165.86 MHz | 851 | 789 | 0 | 108 |
End-to-end (real PSRAM + real neural_processor, SIMULATED only, no
system-level P&R yet -- deferred to M7/M9): 3-tile job = 446 cycles,
1-tile job = 166 cycles, 5-tile job = 728 cycles. ~140-150 cycles/tile,
dominated by psram_model.v's real ~70ns TAA access latency, not by
memory_manager's own control overhead (its bank-swap turnaround is
documented as a fixed +1 cycle/tile in decisions.log DEC-0006, a small
fraction of the ~140-cycle PSRAM-dominated total).
+71
View File
@@ -248,3 +248,74 @@ not merely a stylistic choice.
STATUS:
ACCEPTED
---
DEC-0006
DATE: 2026-09-05
DECISION:
memory_manager.v (M4) uses a SINGLE prefetch_engine instance,
retargeted per bank via a depth-1 pending-request register, rather
than multiple engines or a general request queue. The result
write-back (one byte per job, after the last tile) shares the same
backend port via a simple state-based mux, not a general arbiter --
because prefetch and write-back are temporally disjoint by
construction (the write only happens after prefetch_engine has
nothing left to fetch for that job).
WHY:
§13's double-buffering strategy needs at most ONE fetch "in flight"
and at most ONE fetch "queued" at any time for a SINGLE Neural
Processor consuming tiles sequentially (proven by construction: a new
prefetch is only ever queued on a tile handoff, and at most one
handoff can be pending completion of the previous prefetch before the
next one is even requested). A general multi-entry queue or a second
engine would add complexity with no present benefit. Likewise,
because this Memory Manager currently serves exactly one Neural
Processor and one job at a time, no concurrent second requester can
ever contend for the backend port with prefetch reads -- a real
mem_arbiter-style arbiter (as V1 uses for ITS OWN multi-master case)
is deferred until a scenario that actually needs it exists (multiple
Neural Processors or overlapping jobs sharing one memory_manager,
not yet built).
EVIDENCE:
errors.log ERR-0006 -- the single-entry pending register, once
correctly gated (see ERR-0006 items 1-2), handled 1-tile, 3-tile, and
5-tile jobs correctly with no queue overflow in
hardware/v2/sim/tb_memory_manager.v.
ALTERNATIVES:
1. Multiple prefetch_engine instances (one per bank), letting both
banks fetch fully in parallel. Rejected for M4: doubles DSP-free
logic for a benefit only realized when compute-tile time is
SHORTER than 2x fetch-tile time for a single engine -- not yet
measured to be the case (§22, deferred to M9), and the single-
engine design already fully hides fetch latency behind neural_
processor's own per-tile compute time in the cases tested (see
experiments.log EXP-0005 cycle counts).
2. General N-entry FIFO for pending requests. Rejected: no scenario
in the current single-processor, single-job design can ever
generate more than one pending request before the in-flight one
completes -- an N-entry queue would be complexity with no
reachable use.
3. Reuse V1's mem_arbiter.v as-is for the prefetch-vs-writeback
sharing. Rejected: mem_arbiter.v's four ports are hardcoded to
specific V1 module names/priorities (§1 already established this
pattern in DEC-0001 for the broader V1-freeze decision) -- and
prefetch/write-back are provably never simultaneous here anyway,
so even a generic 2-port arbiter would be unexercised complexity.
RESULT:
memory_manager.v as implemented. A NOTED, NOT-YET-OPTIMIZED
characteristic (documented in the module's own header comment): the
bank-swap-and-check control path costs a minimum 1 idle cycle per
tile handoff even when the next bank was already prefetched in time,
unlike neural_processor.v's own zero-gap tile acceptance -- left for
M10 (Optimization) to revisit using real stall-percentage data (§22)
rather than optimized blindly now.
STATUS:
ACCEPTED
+27
View File
@@ -110,3 +110,30 @@ decision: vedi benchmark.log -- il dimensionamento di weight_buffer
andra' guidato da P_IN, non solo da DEPTH, quando si arrivera' a
M4/M9.
next_action: M4 -- memory_manager.v + prefetch_engine.v.
[2026-09-05T16:00:00Z] commit=5f0d7f1 session=v2-M4-memory-manager
module: hardware/v2/rtl/memory_manager.v, prefetch_engine.v
action: implementato M4 -- Memory Manager (arbitraggio/buffering/
forwarding/gestione latenza/double buffering, §12) + Prefetch Engine
(fetch a doppio buffer con retargeting di una singola istanza per
banco, §13). Backend PSRAM V1 riusato SENZA MODIFICHE
(int8_memory_access.v -> memory_interface.v -> psram_controller.v,
§15), integrato end-to-end con un vero hardware/v2/rtl/
neural_processor.v (M1).
reason: roadmap M4.
result: 3/3 job PASS end-to-end (1/3/5 tile), risultato verificato con
RILETTURA INDIPENDENTE da PSRAM (non solo ispezione di segnali
interni), con byte "poison" attorno alle regioni operando per
catturare eventuali errori di indirizzamento off-by-one (nessuno
trovato). 3 bug RTL reali trovati e risolti durante l'integrazione
(vedi errors.log ERR-0006): mancava una disciplina "una sola
richiesta di prefetch in volo", un buco di un ciclo nel check
!pf_busy, un mux di stato disallineato di un ciclo che faceva
silenziosamente perdere la scrittura del risultato su PSRAM.
Sintesi reale: 0 problemi, 851 LUT4/789 FF/108 CCU2C/0 DSP (atteso).
Fmax reale (via harness, stesso motivo pin-count di ERR-0005):
165.86 MHz, PASS a 80MHz.
errors: vedi errors.log ERR-0005 (ricorrenza), ERR-0006 (3 bug nuovi).
decision: vedi decisions.log DEC-0006 (motore di prefetch singolo +
registro pendente, nessun arbitro backend ancora necessario).
next_action: M5 -- neural_director.v, scheduling first-free.
+52
View File
@@ -124,3 +124,55 @@ WORKAROUND: hardware/v2/synthesis/harness_neural_processor_array.v --
STATUS: WORKED AROUND. Will become moot once M4/M5 exist and the array
is synthesized as part of a larger design with on-chip ports instead
of a bare top-level module.
ERR-0006 (real RTL bugs in hardware/v2/rtl/memory_manager.v, FOUND AND FIXED)
DATE: 2026-09-05
MODULE: hardware/v2/rtl/memory_manager.v
SYMPTOM: end-to-end M4 testbench (real V1 PSRAM backend + real M1
neural_processor) hung permanently partway through the first
multi-tile job -- bank_ready for the "current" bank never became 1,
even though the byte-level backend clearly kept completing read
transactions (observed via cycle-by-cycle hierarchical tracing of
memory_manager/prefetch_engine internal state).
ROOT CAUSES (three, found together during the same debugging session):
1. The tile-N+2 prefetch request, queued on tile-N's handoff, could
be issued (pf_start asserted) while prefetch_engine was STILL
mid-fetch for tile-N+1 -- there was no single-in-flight-request
discipline at all in the first draft. Fixed by adding a
single-entry pf_pending register: requests are queued, not
issued directly, and a dedicated rule launches the queued
request only once the engine reports free.
2. Even with that queue, pf_busy does not read 1 until the cycle
AFTER pf_start was first observed by prefetch_engine (its own
fetch_busy<=1 lags its own fetch_start sampling by one clock) --
checking only `!pf_busy` left a genuine one-cycle window where a
second queued request would fire on top of the one just
launched, silently overwriting pf_target_bank (and pf_x_addr/
pf_w_addr) for the fetch already in flight. The address corruption
was harmless (prefetch_engine had already latched the correct
address into its own state that same edge), but pf_target_bank
corruption meant the eventually-completed fetch's real data got
filed into the WRONG bank's bank_ready/bank_x/bank_w, permanently
starving the bank actually needed next. Fixed by gating the issue
rule on `!pf_busy && !pf_start` (the extra term closes exactly
this one-cycle window).
3. A combinational mux selecting between prefetch_engine's own
backend wires and the result-write-back FSM's wires was gated on
`state == MM_WRITE_RESULT`, but wr_mem_req (asserted while
state==MM_WRITE_RESULT) only becomes valid the FOLLOWING cycle,
i.e. while state==MM_DONE -- the mux therefore selected the wrong
source for the one cycle the write request pulse was actually
high, silently dropping the PSRAM write entirely. Fixed by
widening the mux's select condition to cover both states.
DIAGNOSIS METHOD: cycle-by-cycle hierarchical signal dumps (mm.state,
tile_idx, bank_ready, pf_busy, pf_pending, pf_target_bank,
prefetch_engine's own state) under Verilator, printed only on
signal-change to keep the trace readable, comparing against the
hand-derived expected sequence of events for a 3-tile job.
VERIFICATION: hardware/v2/sim/tb_memory_manager.v -- 3/3 tests PASS
after all three fixes, including a 5-tile job (steady-state
double-buffer swap across more than 2 tiles) and independent
PSRAM read-back of the written-back result byte (not just internal
signal inspection).
STATUS: FIXED, verified end-to-end with the real (unmodified) V1
PSRAM backend chain and a real M1 neural_processor.
+56
View File
@@ -218,3 +218,59 @@ decision: keep DEPTH parametric as specified, but document (this
next_action: M4 -- memory_manager.v + prefetch_engine.v (wires these
three buffers + the array together, PSRAM backend reused unmodified
from V1 per §15).
EXP-0005
timestamp: 2026-09-05T16:00:00Z
git_commit: 5f0d7f1 (+ uncommitted M4 work)
session: v2-M4-memory-manager
module: hardware/v2/rtl/memory_manager.v, prefetch_engine.v
configuration: P_IN=8, ADDR_WIDTH=23, real V1 backend chain
(int8_memory_access -> memory_interface -> psram_controller ->
psram_model, ALL unmodified), real M1 neural_processor
action: M4 -- end-to-end integration: memory_manager double-buffers
prefetch of X/W tiles from PSRAM, feeds a real neural_processor,
writes the computed result back to PSRAM.
command (sim, Verilator): verilator --binary --timing -j 0 -Wno-fatal
--top-module tb -o /tmp/vtb_mm hardware/v1/rtl/int8_memory_access.v
hardware/v1/rtl/memory_interface.v hardware/v1/rtl/psram_controller.v
hardware/v1/sim/psram_model.v hardware/v2/rtl/neural_processor.v
hardware/v2/rtl/prefetch_engine.v hardware/v2/rtl/memory_manager.v
hardware/v2/sim/tb_memory_manager.v && /tmp/vtb_mm
command (synth): yosys -p "synth_ecp5 -json .../top.json -top
memory_manager" hardware/v2/rtl/memory_manager.v
hardware/v2/rtl/prefetch_engine.v (standalone, for real resource
counts); harness_memory_manager.v (see errors.log ERR-0005 pattern)
for real P&R Fmax, since the bare module exceeds the device's
TRELLIS_IO budget as a top-level (same class of artifact as the
Processor Array, not a logic limit).
result:
SIMULATED: 3/3 jobs PASS end-to-end -- 3-tile (bias=0, ACT_RELU,
saturates to 127), 1-tile (no saturation, y=32), and 5-tile
(steady-state double-buffer swap across more than 2 tiles, y=40)
-- each verified by an INDEPENDENT PSRAM read-back of the
written result byte (not just internal signal inspection), with
"poison" bytes surrounding the real operand regions to catch any
off-by-one addressing (none found).
Cycle counts (real, PSRAM power-up already excluded): 3-tile job =
446 cycles, 1-tile job = 166 cycles, 5-tile job = 728 cycles.
Roughly ~140-150 cycles/tile at this PSRAM timing (dominated by
the real ~70ns TAA access latency modeled in psram_model.v, not
by memory_manager's own control overhead) -- a real, measured
number, not estimated.
SYNTHESIZED (standalone, real resource count): 0 CHECK problems,
851 LUT4, 789 TRELLIS_FF, 108 CCU2C, 0 DSP (expected -- no
multiplication in this module).
POST-P&R (via harness, real Fmax): 165.86 MHz, PASS at 80MHz with
large margin.
errors: ERR-0006 (three real RTL bugs found and fixed during bring-up
-- see errors.log for full detail: a missing single-in-flight-
request discipline, a one-cycle pf_busy blind spot, and an
off-by-one state mux for the write-back path). ERR-0005's pin-count
artifact recurred for this module too (worked around the same way).
decision: see decisions.log DEC-0006 (single prefetch engine + pending
register is sufficient for this milestone's scope; a real backend
arbiter is deferred until multiple processors/jobs actually need to
share one memory_manager).
next_action: M5 -- neural_director.v (first-free scheduling), wiring
job dispatch to potentially multiple (memory_manager, neural_
processor) pairs instead of the single hardcoded pair tested here.
+12
View File
@@ -36,3 +36,15 @@ vectors: extreme INT8 (-128, 127, -100, 100), regular values, full
writes undisturbed
simulator: Verilator 5.050 (--binary --timing)
PASS/FAIL: 10/10 PASS
[2026-09-05] EXP-0005 -- hardware/v2/sim/tb_memory_manager.v
test: 3 end-to-end jobs (3-tile/saturating, 1-tile/non-saturating,
5-tile/steady-state-swap), real V1 PSRAM backend chain (unmodified),
real M1 neural_processor
simulator: Verilator 5.050 (--binary --timing)
PASS/FAIL: 3/3 PASS
bit-exact result: PSRAM-read-back result byte matches hand-computed
expectation in every case (independent oracle, not derived from the
RTL under test)
cycles: 446 (3 tiles), 166 (1 tile), 728 (5 tiles) -- real PSRAM
latency dominates, not memory_manager's own control overhead
+4
View File
@@ -36,3 +36,7 @@ result_buffer D=4096: LUT=37 FF=30 DP16KD=2 (D=256: LUT=21 FF=26 DP16KD=1)
CHECK: 0 problems, all 6 configs correctly infer DP16KD (no LUT-RAM
fallback). weight_buffer's DP16KD count does NOT drop with depth
(width-bound, not depth-bound -- see decisions.log / benchmark.log).
[2026-09-05] EXP-0005 -- memory_manager + prefetch_engine (standalone)
LUT4=851 TRELLIS_FF=789 CCU2C=108 MULT18X18D=0 (expected, no
multiplication in this module). CHECK: 0 problems.
+6
View File
@@ -38,3 +38,9 @@ weight_buffer (D=512): Fmax=339.67 MHz PASS
result_buffer (D=4096): Fmax=325.20 MHz PASS
All far above the 80MHz target -- buffers are not a timing concern in
isolation at these depths.
[2026-09-05] EXP-0005 -- memory_manager + prefetch_engine (via
harness_memory_manager.v, see errors.log ERR-0005 for why a harness
was needed), real nextpnr-ecp5 --45k --package CABGA381 --speed 8
--freq 80 --lpf-allow-unconstrained
Fmax: 165.86 MHz -- PASS at 80MHz (real place&route measurement)