perf(v2): shared activation cache - further 1.66-2.00x real speedup (DEC-0016)

Implements optimization #2 from the final benchmark campaign's own
recommendation, on top of DEC-0015's word-level burst rewrite: a new
shared activation_cache.v module fetches a given activation (X)
vector from PSRAM once instead of once per neuron sharing it - the
exact redundant traffic pattern the dense-layer workloads in this
project's benchmark suite exhibit.

Each memory_manager's own prefetch_engine now fetches WEIGHTS only;
the activation half is requested from the shared cache instead
(single-tag, tile-granular, N_SLOTS request ports, its own real
word-level PSRAM backend via a new dedicated arbiter port).
dataflow_core.v/slot_mem_arbiter.v/neural_multiprocessor.v widened to
N_SLOTS+1 ports to arbitrate the cache's traffic alongside each
slot's weight traffic.

Two real bugs found and fixed during implementation (ERR-0010): a
target-bank/pending-bank race in memory_manager.v's activation-cache
wiring (the same bug class ERR-0006 already fixed once for
pf_target_bank - a later handoff's queued request can overwrite which
bank an earlier, still-in-flight request's ack applies to), and a
repeat of ERR-0009's N_SLOTS=1 zero-width replication bug in
activation_cache.v itself.

Real, measured results: the full final-benchmark campaign (24/24
workload/config combinations) re-verified bit-exact. D-Stress cycles
fall a further 1.66-2.00x on top of DEC-0015 (~4x combined vs the
original byte-level baseline). But the cache's real Fmax cost is much
steeper than DEC-0015's own: N_SLOTS=2 (the recommended default,
DEC-0014) drops from 133.58 to 87.72 MHz (-34%, margin over 80MHz
shrinks from +67% to +9.7%), and N_SLOTS=4 drops to 65.01 MHz - now
FAILING the 80MHz target it previously passed. Combined real
wall-clock speedup vs the original baseline: N=1 3.86x, N=2 2.45x
(both real net wins); N=4 is a real regression once its own now-failing
Fmax is honestly used, though N=4 was never the recommended
configuration.

N_SLOTS=2 remains the recommended default (DEC-0014 unaffected) with
a thinner but still real Fmax margin. Cache hit-detection pipelining
is flagged as concrete follow-up work if N_SLOTS>2 is ever needed with
the cache active - not attempted this round.

Logged: simulation/synthesis/timing/benchmark/decisions (DEC-0016)/
experiments (EXP-0016)/errors (ERR-0010)/development.log, ROADMAP.md
updated.

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 20:59:14 +02:00
co-authored by Claude Sonnet 5
parent e4a5540b6e
commit 63cac6a7e5
16 changed files with 931 additions and 211 deletions
+35
View File
@@ -311,3 +311,38 @@ cycles at N=2/4/8 remain within ~0.2% of each other: 307602/307346/
EFFICIENT per transaction, it did not remove the fact that there is
still only one physical port, so DEC-0014's N_SLOTS=2 recommendation
is unaffected and reconfirmed with the new, faster numbers.
[2026-09-05] EXP-0016 -- shared activation_cache (DEC-0016), real
before/after comparison on top of DEC-0015's own word-level burst
rewrite (user-requested optimization #2)
Full campaign (tb_benchmark_suite.v, same 6 workloads as EXP-0014/
EXP-0015), D-Stress (256 neurons, all sharing ONE input vector -- the
exact pattern this cache targets), real cycles and real wall-clock
(cycles / real POST-P&R Fmax):
| N_SLOTS | cycles: byte | cycles: +burst | cycles: +cache | Fmax: byte | Fmax: +burst | Fmax: +cache |
|-----------|----------------|-------------------|-------------------|--------------|----------------|----------------|
| 1 | 780298 | 348682 | 174610 | 152.46 MHz | 152.44 MHz | 131.79 MHz |
| 2 | 736402 | 307602 | 185428 | 142.45 MHz | 133.58 MHz | 87.72 MHz |
| 4 | 736823 | 307346 | 184795 | 113.38 MHz | 112.07 MHz | 65.01 MHz (FAIL@80MHz) |
Real wall-clock speedup vs the ORIGINAL byte-level baseline (cycles /
real Fmax, both fully combined optimizations):
| N_SLOTS | wall-clock: byte | wall-clock: +burst | wall-clock: +cache | TOTAL real speedup |
|-----------|--------------------|------------------------|------------------------|------------------------|
| 1 | 5118.1 us | 2287.3 us | 1324.9 us | 3.86x |
| 2 | 5169.5 us | 2302.8 us | 2113.9 us | 2.45x |
| 4 | 6498.7 us | 2742.4 us | 2842.6 us (Fmax FAILS)| 2.29x (but a real regression vs +burst alone) |
All 24/24 workload/config combinations (6 workloads x N_SLOTS=1/2/4/8)
re-verified bit-exact after adding the cache. Two real bugs found and
fixed during implementation (errors.log ERR-0010).
HONEST SUMMARY: the two user-requested optimizations together deliver
a real 2.45-3.86x wall-clock speedup for the recommended N_SLOTS<=2
range (DEC-0014), at the cost of a much steeper Fmax sensitivity to
N_SLOTS than either the arbiter alone (DEC-0015) or the un-optimized
baseline had -- N_SLOTS=4 now fails 80MHz outright with the cache
active, a real trade-off, not glossed over (see decisions.log
DEC-0016).
+95
View File
@@ -937,3 +937,98 @@ correctness preserved (24/24 bit-exact).
STATUS:
ACCEPTED
DEC-0016
DATE: 2026-09-05
DECISION:
A new shared module, activation_cache.v, is added inside dataflow_core.v
alongside the N_SLOTS memory_managers. It fetches a given ACTIVATION
(X) vector from PSRAM once (tile by tile, on first use) and serves
every subsequent request for the same x_base/tile directly from an
on-chip buffer -- no PSRAM access on a hit. Each memory_manager's own
prefetch_engine now fetches WEIGHTS only (X is no longer duplicated
per-slot). Single-tag design (one active cached x_base at a time,
correct but can thrash under interleaved different-x_base concurrent
traffic -- never incorrect, see the "Alternatives"/"Result" sections
below for the honest limitation).
WHY (user-requested optimization #2, following the final-benchmark.md
report's own recommendation): in the realistic dense-layer workloads
this project benchmarks, many neurons share the exact same X vector --
each of dataflow_core's N_SLOTS memory_manager instances re-fetching
that identical vector from PSRAM independently was real, measured,
redundant traffic on the one shared PSRAM port.
EVIDENCE -- REAL BENEFIT (cycles, SIMULATED, full campaign,
tb_benchmark_suite.v, D-Stress the largest/most representative
workload): combined with DEC-0015's word-level burst rewrite, total
cycle count for D-Stress falls from the ORIGINAL byte-level baseline
(EXP-0014) by 3.97x-4.47x across every N_SLOTS tested (N=1: 780298 ->
174610; N=2: 736402 -> 185428; N=4: 736823 -> 184795; N=8: 738751 ->
184797) -- the activation_cache's OWN incremental contribution on top
of DEC-0015 alone is a further 1.66x-2.00x cycle reduction. All 24/24
workload/config combinations remain bit-exact.
EVIDENCE -- REAL COST (Fmax, POST-P&R MEASURED, full
neural_multiprocessor including the real V1 PSRAM chain): the shared
cache's real Fmax cost is substantially STEEPER than DEC-0015's own
(which cost only 0-6% Fmax). Real POST-P&R Fmax after adding the
cache: N=1: 152.44 -> 131.79 MHz (-13.5%); N=2: 133.58 -> 87.72 MHz
(-34.3%); N=4: 112.07 -> 65.01 MHz (-42.0%, and this configuration NOW
FAILS the 80MHz target it previously passed). This is a real,
structural cost: activation_cache is a single shared resource with
N_SLOTS request ports, a broadcast-capable hit-check evaluated for
every port every cycle, and a shared tile_store array -- a genuine
routing/fan-in hot spot that gets worse as N_SLOTS grows, more
severely than the arbiter-only widening DEC-0015 introduced.
Combined REAL WALL-CLOCK effect (cycles / real POST-P&R Fmax,
D-Stress, vs the ORIGINAL byte-level baseline): N=1: 3.86x faster
(the clear win case -- low Fmax cost, full cycle benefit); N=2: 2.45x
faster (still a solid net win -- this is the recommended default,
DEC-0014, and it still comfortably beats the baseline, though its
Fmax safety margin over 80MHz shrank from +78% to +9.7%); N=4: 2.29x
faster THAN THE ORIGINAL baseline, but WORSE than DEC-0015-alone
(2742.4us -> 2842.6us) once its own real (now failing) Fmax is used --
adding the cache is a net REGRESSION specifically at N=4, and N=4 is
no longer even a valid passing 80MHz design point.
ALTERNATIVES:
1. Pipeline the cache's hit-detection/broadcast logic (register the
hit[] comparison one extra stage before driving tile_x_out) to
recover some of the lost Fmax margin. Rejected FOR NOW: a real,
promising follow-up, but a genuine RTL redesign of the cache's own
timing, not attempted in this pass -- flagged as real, concrete
future work rather than attempted blindly without first measuring
whether N_SLOTS=2 (the actual recommended default, DEC-0014) still
needs it (it does not fail timing at N=2, it just has a thinner
margin than before).
2. Give up on the cache entirely given N=4's regression. Rejected:
N=1 and N=2 (the actually-recommended range per DEC-0014) both show
a clear, real net win, and N=4 was never the recommended default
to begin with -- discarding a real 2.45-3.86x win over the range
that matters to avoid a regression in a range that was already
deprioritized would be the wrong trade.
3. Make MAX_TILES smaller (currently 16, sized for the largest
workload's shared vector) to shrink the cache's own storage/compare
width and recover some Fmax. Rejected for THIS round: would need
re-verifying against every workload's own real tile count
requirements (Large/Stress use exactly 16) -- a real, bounded
follow-up, not attempted here to avoid conflating multiple
variables in one measurement.
RESULT:
activation_cache.v is added, real net win confirmed at N_SLOTS=1 and
N_SLOTS=2 (the recommended default, DEC-0014), real regression to a
failing timing state confirmed at N_SLOTS=4 -- reported honestly, not
hidden. N_SLOTS=2 remains the recommended default (DEC-0014's own
conclusion is unaffected, since N=4 was never recommended), now with a
thinner but still real Fmax margin (87.72 MHz vs the 80MHz target).
Cache pipelining (Alternative 1) is flagged as real, concrete follow-up
work if N_SLOTS>2 configurations are ever needed with this cache
active.
STATUS:
ACCEPTED
+32
View File
@@ -331,3 +331,35 @@ decision: vedi decisions.log DEC-0015.
next_action: ottimizzazione #2 -- cache condivisa on-chip per il
vettore di attivazione (X), per eliminare le letture ridondanti tra
neuroni che condividono lo stesso input di layer.
[2026-09-05] Ottimizzazione #2 -- cache condivisa on-chip per il
vettore di attivazione, su richiesta esplicita dell'utente
reason: dopo l'ottimizzazione #1 (burst a livello di parola), l'utente
ha chiesto di implementare anche la #2: cache condivisa per evitare
che ogni neurone rilegga da PSRAM lo stesso vettore X gia' letto da
un altro neurone dello stesso layer.
result: nuovo modulo activation_cache.v (single-tag, granularita' a
tile, condiviso tra tutti gli slot). Trovati e risolti 2 bug reali
durante l'implementazione (errors.log ERR-0010): una race
target-bank/pending-bank (stessa classe di ERR-0006) e una ripetizione
del bug a larghezza zero di ERR-0009 (N_SLOTS=1). Dopo il fix: 24/24
PASS bit-exact. Beneficio reale misurato: ulteriore riduzione cicli
1.66-2.00x oltre l'ottimizzazione #1 (circa 4x combinato rispetto
alla baseline originale byte-level). MA costo reale in Fmax molto piu'
ripido del previsto: N_SLOTS=2 scende da 133.58 a 87.72 MHz (-34%,
ancora sopra 80MHz ma con margine molto piu' sottile), N_SLOTS=4
scende a 65.01 MHz e ORA FALLISCE il target 80MHz (prima passava).
Speedup reale in wall-clock rispetto alla baseline originale:
N=1 3.86x, N=2 2.45x, N=4 2.29x (ma con una regressione reale
rispetto alla sola ottimizzazione #1, dato che N=4 ora fallisce il
timing).
errors: vedi errors.log ERR-0010 (2 bug RTL reali, trovati e risolti).
decision: vedi decisions.log DEC-0016 -- vittoria reale netta
confermata per N_SLOTS=1/2 (l'intervallo raccomandato, DEC-0014
resta valida), regressione reale a N_SLOTS=4 (mai la configurazione
raccomandata, ma un costo reale onestamente riportato, non nascosto).
Pipeline della logica di hit-check della cache segnalato come lavoro
futuro concreto se servisse N_SLOTS>2 con la cache attiva.
next_action: nessuna ulteriore richiesta esplicitamente dall'utente per
questo giro. Datasheet V2 professionale, ora con dati definitivi
post-ottimizzazione da includere.
+58
View File
@@ -327,3 +327,61 @@ DIAGNOSIS METHOD (bug 3): periodic `$display` progress heartbeats
output for real-time visibility, isolating the exact neuron index
where progress stopped advancing -- the same "trace real signals,
don't guess" discipline used throughout this whole project.
ERR-0010 (real RTL bugs found and fixed during activation_cache.v
implementation, DEC-0016)
DATE: 2026-09-05
1. Same bug CLASS as ERR-0006 (pf_target_bank/pf_pending_bank), NEW
instance, memory_manager.v's activation-cache side:
SYMPTOM: hardware/v2/sim/tb_memory_manager.v -- job xb=4096 (3
tiles) hung/produced wrong results after adding the shared
activation_cache request path; cycle-by-cycle tracing (temporary
$display instrumentation, later removed) showed a cache ack for
tile 1 (queued for bank 1) instead applying its data to bank 0.
ROOT CAUSE: `xc_target_bank` (which bank an ack's data should be
written into) was being written DIRECTLY by the queueing logic
(MM_IDLE/MM_PREFETCH_FIRST/MM_STREAM), the same register used to
resolve an ack that might still be OUTSTANDING from an EARLIER
queued request. Real PSRAM miss latency can exceed one
neural_processor tile's own compute time, so a LATER handoff can
queue a NEW request (targeting a DIFFERENT bank) in the same or a
following cycle, before the EARLIER request's ack has arrived --
with only one `xc_target_bank` register, NBA "last write in
program order wins" semantics silently overwrote which bank the
EARLIER, already-in-flight request's eventual ack gets applied
to. This is the EXACT bug ERR-0006 already found and fixed once
for pf_target_bank/pf_pending_bank (which already used a correct
two-register pattern: a `_pending_bank` staging register written
at queueing time, and the real `_target_bank` written ONLY by the
issue rule at the moment the request actually fires) -- this
module's newly-added activation-cache side did not follow that
already-established pattern, until now.
FIX: introduced `xc_pending_bank` (written at queueing time) and
changed `xc_target_bank` to be written ONLY by the issue rule
(`xc_target_bank <= xc_pending_bank;`, the same cycle xc_req
fires), mirroring pf_target_bank/pf_pending_bank exactly.
VERIFICATION: tb_memory_manager.v 3/3 PASS bit-exact after the fix.
Full final-benchmark campaign (24/24 workload/config
combinations) re-verified bit-exact.
2. Same bug CLASS as ERR-0009 item 1 (neural_director.v's N_SLOTS=1
zero-width replication), NEW instance, activation_cache.v:
SYMPTOM: Verilator compile error building the M4-level regression
testbench (activation_cache instantiated with N_SLOTS=1 there,
to serve a single memory_manager instance) -- same
"%Error-ZEROREPL: Replication value of 0 is only legal under a
concatenation" as ERR-0009.
ROOT CAUSE: `miss_idx = {$clog2(N_SLOTS){1'b0}};` -- identical root
cause to ERR-0009 item 1 ($clog2(1)=0 at N_SLOTS=1).
FIX: replaced with the same width-agnostic `'0` literal used in
neural_director.v's own fix.
VERIFICATION: tb_memory_manager.v (N_SLOTS=1 activation_cache) and
the full campaign (N_SLOTS=1/2/4/8) all build and pass.
DIAGNOSIS METHOD (item 1): periodic $display cycle-by-cycle tracing of
memory_manager's own internal state (xc_req/xc_ack/xc_outstanding/
xc_pending/bank_x_ready/bank_w_ready) and the 2-port test arbiter's
own owner/grant state, added temporarily to tb_memory_manager.v and
removed once the bug was isolated and fixed -- the same "trace real
signals, don't guess" discipline used throughout this project.
+42
View File
@@ -584,3 +584,45 @@ next_action: user-requested optimization #2 -- a shared on-chip cache
for the activation (X) vector, so N independent neurons sharing one
input vector (the dense-layer shape used throughout this benchmark
suite) fetch it from PSRAM ONCE instead of once per neuron.
[2026-09-05] EXP-0016 -- shared activation_cache implementation (user-
requested optimization #2, following final-benchmark.md's own
recommendation: eliminate redundant per-neuron re-fetching of a
shared input vector)
test: new module activation_cache.v (single-tag, tile-granular,
N_SLOTS request ports, real word-level PSRAM backend via its own
arbiter port); memory_manager.v's activation half redirected through
it (weight half unchanged from DEC-0015); dataflow_core.v/
neural_multiprocessor.v/slot_mem_arbiter.v widened to N_SLOTS+1
ports to arbitrate the cache's own traffic alongside the N_SLOTS
memory_managers' weight traffic. Re-verified: M4 (updated
testbench), M7 (updated testbench), M8 (unchanged, black-box), and
the full final benchmark campaign (unchanged, black-box) at
N_SLOTS=1/2/4/8.
simulator: Verilator 5.050 (--binary --timing)
PASS/FAIL:
SIMULATED: M4 3/3 PASS, M7 4/4 PASS, M8 4/4 PASS, final campaign
24/24 PASS bit-exact. D-Stress cycles: 348682/307602/307346/
307874 (post-DEC-0015) -> 174610/185428/184795/184797 (N=1/2/4/8,
post-DEC-0016) -- a further 1.66-2.00x real cycle reduction from
the cache alone, ~4x combined with DEC-0015 vs the original
byte-level baseline.
SYNTHESIZED + POST-P&R (real, full system incl. real PSRAM pins):
N=1 131.79 MHz (was 152.44), N=2 87.72 MHz (was 133.58, -34.3%),
N=4 65.01 MHz (was 112.07, -42.0% -- NOW FAILS the 80MHz target).
Combined real wall-clock speedup vs the ORIGINAL byte-level
baseline: N=1 3.86x, N=2 2.45x, N=4 2.29x (but a real regression
vs DEC-0015-alone once N=4's own now-failing Fmax is used).
errors: 2 real bugs found and fixed (errors.log ERR-0010): a target-
bank/pending-bank race (ERR-0006's bug class, new instance) and a
repeat of ERR-0009's N_SLOTS=1 zero-width replication bug.
decision: see decisions.log DEC-0016 -- real net win confirmed at
N_SLOTS=1/2 (the recommended range, DEC-0014), real regression to a
failing timing state at N_SLOTS=4 (never the recommended default,
but a real, honestly-reported cost of this optimization). Cache
pipelining flagged as real follow-up work if N_SLOTS>2 with the
cache active is ever needed.
next_action: none further requested by the user for this round. Real,
concrete follow-up flagged in DEC-0016: pipeline the cache's own
hit-detection/broadcast logic to recover Fmax margin if higher
N_SLOTS configurations are ever needed with the cache active.
+28
View File
@@ -152,3 +152,31 @@ PASS/FAIL: M4 3/3 PASS (166/446/728 -> 84/204/322 cycles, bit-exact).
(see benchmark.log EXP-0015 for the full table).
errors: none found during this implementation (clean first-pass
correctness at every regression point).
[2026-09-05] EXP-0016 -- regression + real improvement measurement
after adding shared activation_cache.v (DEC-0016)
test: hardware/v2/sim/tb_memory_manager.v (M4, updated: memory_manager
now wired to a real activation_cache instance + a real 2-port
arbiter, N_SLOTS=1 scope), hardware/v2/sim/tb_dataflow_core.v (M7,
updated: sim_word_mem array widened to N_SLOTS+1, X data poked once
into the shared cache's own backing memory instead of duplicated
per-slot), hardware/v2/sim/tb_neural_multiprocessor.v (M8,
UNCHANGED -- black-box), hardware/v2/sim/tb_benchmark_suite.v (final
campaign, UNCHANGED, re-run at N_SLOTS=1/2/4/8)
simulator: Verilator 5.050 (--binary --timing)
PASS/FAIL: M4 3/3 PASS (bit-exact, cycle counts higher than DEC-0015
alone for this SPECIFIC single-instance test -- expected, no sharing
benefit possible with only one memory_manager, only the cache's real
arbitration overhead shows up here). M7 4/4 PASS. M8 4/4 PASS
(unchanged testbench). Final campaign 24/24 PASS bit-exact,
D-Stress cycles reduced a further 1.66-2.00x on top of DEC-0015's
own reduction (see benchmark.log EXP-0016 for the full table) --
the real sharing benefit this cache targets only manifests with
multiple neurons genuinely sharing one x_base, which only the full
campaign's dense-layer workloads (not M4/M7/M8's own small tests)
exercise.
errors: 2 real bugs found and fixed during implementation (errors.log
ERR-0010): a target-bank/pending-bank race (same class as ERR-0006,
a new instance in the activation-cache side of memory_manager.v),
and a repeat of ERR-0009's N_SLOTS=1 zero-width replication bug
(this time in activation_cache.v itself).
+10
View File
@@ -112,3 +112,13 @@ CHECK: 0 problems on all three (same benign warnings as every prior
configs and saved some in others (word-level control logic is
simpler than byte-indexing logic in prefetch_engine.v), net roughly
flat.
[2026-09-05] EXP-0016 -- neural_multiprocessor N_SLOTS=1/2/4 after
adding activation_cache.v (DEC-0016), real standalone synthesis, no
harness needed
N_SLOTS=1: LUT4=2760 CCU2C=202 TRELLIS_FF=2405 MULT18X18D=8 DP16KD=0
N_SLOTS=2: LUT4=4359 CCU2C=366 TRELLIS_FF=3924 MULT18X18D=16 DP16KD=0
N_SLOTS=4: LUT4=9158 CCU2C=698 TRELLIS_FF=7986 MULT18X18D=32 DP16KD=0
CHECK: 0 problems on all three (same benign warnings as always).
Resource cost is modest (activation_cache itself is small -- a
16-tile x 64-bit store plus N_SLOTS-way pending/hit logic).
+15
View File
@@ -126,3 +126,18 @@ Small, real Fmax cost from widening the shared arbiter/backend to
16-bit + lb_n/ub_n (extra routing), overwhelmingly outweighed by the
real cycle-count reduction (EXP-0015 in experiments.log/benchmark.log):
D-Stress real wall-clock improves 2.24-2.37x at every N_SLOTS.
[2026-09-05] EXP-0016 -- neural_multiprocessor N_SLOTS=1/2/4 after
adding activation_cache.v (DEC-0016), real nextpnr-ecp5 --45k
--package CABGA381 --speed 8 --freq 80 --lpf-allow-unconstrained
N_SLOTS=1: Fmax = 131.79 MHz -- PASS at 80MHz (was 152.44 MHz, -13.5%)
N_SLOTS=2: Fmax = 87.72 MHz -- PASS at 80MHz (was 133.58 MHz, -34.3%)
N_SLOTS=4: Fmax = 65.01 MHz -- FAILS at 80MHz (was 112.07 MHz, -42.0%)
Real, substantial Fmax cost from the shared activation_cache -- a
single central resource with N_SLOTS request ports, a broadcast-
capable hit-check evaluated every cycle for every port, and a shared
tile_store array. Cost grows much faster with N_SLOTS than
DEC-0015's own arbiter widening did. N_SLOTS=4 is no longer a passing
80MHz design point with the cache active -- see decisions.log
DEC-0016 for the full analysis and why N_SLOTS=2 (the recommended
default, DEC-0014) is unaffected in its own recommendation.