V2.0.0 hardware freeze - single SDRAM
FASE #1 hardware freeze for FPGA-Neural V2, N4/P8, single external SDRAM (Alliance Memory AS4C4M16SA-6TIN) serving weights, activations, and results through one physical sdram_controller.v instance. Removes the PSRAM dependency (hardware/v1/rtl/psram_controller.v + memory_interface.v) from the V2 physical path entirely -- V1 itself remains fully unmodified, the golden reference. New RTL: sdram_unified_backend.v (2-way W/AR arbitration over one SDRAM controller, real per-byte DQM write masking added to sdram_controller.v for correct single-byte result writes with no read-modify-write), nms_neural_multiprocessor_sdram_unified.v (the frozen top-level). Two real bugs found and fixed via full-system testing before being accepted (ERR-0023): a deadlock and an off-by-one data-shift bug in the new arbitration logic. Real results: N=4 and N=2 D-Stress bit-exact (256/256 neurons), 40 real AUTO REFRESH events interleaved with zero corruption, real Yosys+nextpnr-ecp5 synthesis/P&R for LFE5U-45F-8CABGA381 (149/245 TRELLIS_IO, a real 45-pin reduction from the prior dual-memory design). Timing is MARGINAL (1/8 P&R seeds >=80MHz), reported honestly rather than masked by the best seed. Real, sourced ball-level pinout for the SDRAM bus + clk/rst (39/149 signals, P&R-verified) using the official Lattice ECP5U-45 pinout CSV found on disk during this step's own pre-commit review -- corrects an earlier draft that wrongly assumed no real pinout data was available. Chip readiness: NO. Real, disclosed blockers remain (no physical host interface exists yet -- the RTL's own reg_* ports are a 110-pin raw test-harness bus; clock source/PLL decision; power/configuration component selection) -- see hardware/v2/docs/{HARDWARE_FREEZE, CHIP_READINESS,OPEN_ITEMS}.md for the complete, itemized status. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# NMS Activation Fill Controller Timing (STEP14 Part B)
|
||||
|
||||
Status: fixed, real post-P&R verified, bit-exact, adopted. Full data:
|
||||
`hardware/v2/reports/step14_activation_timing.csv`. Full narrative:
|
||||
`hardware/v2/logs/experiments.log` (EXP-0029, 0030, 0031),
|
||||
`decisions.log` (DEC-0026, DEC-0027).
|
||||
|
||||
## B1 — Exact critical path (not assumed)
|
||||
|
||||
Mined directly from the real nextpnr-ecp5 P&R report for
|
||||
`nms_neural_multiprocessor_stream.v` at N_SLOTS=4
|
||||
(Fmax=55.22 MHz, FAIL @ 80 MHz). Full path, 18.11 ns total (6.25 ns
|
||||
logic + 11.85 ns routing):
|
||||
|
||||
```
|
||||
SOURCE: u_act_fill.resident_tag[11] (register Q)
|
||||
-> COMBINATIONAL, chained, NO register in between:
|
||||
(1) max_n_tiles computation, nms_activation_fill_ctrl.v:92
|
||||
(N_SLOTS-wide running-max fold, each iteration gated by a
|
||||
23-bit tag-equality check) -- long CCU2C carry chain
|
||||
(2) resident_count < max_n_tiles comparison, line 165
|
||||
(the ST_IDLE refill/continue decision) -- ANOTHER 16-bit
|
||||
magnitude-comparison carry chain, feeding directly off (1)
|
||||
in the SAME cycle
|
||||
(3) into pf_start's own next-state logic
|
||||
DESTINATION: u_act_fill.pf_addr's clock-enable (CE) pin
|
||||
```
|
||||
|
||||
Two full 16-bit magnitude comparisons sit in **one** combinational
|
||||
cone across **one** clock edge. This confirms, at the exact RTL-line
|
||||
level, the failure class DEC-0016/EXP-0022 predicted analytically
|
||||
("O(N_SLOTS) unpipelined combinational scan feeding directly into a
|
||||
control decision") — but precisely localizes it to the comparison
|
||||
logic (lines 92 and 165), *not* the priority-encoder
|
||||
(`desired_valid`/`desired_x_base`, lines 77-86), which does not appear
|
||||
in this critical path at all.
|
||||
|
||||
## B2 — Scaling behavior
|
||||
|
||||
The bottleneck is the `max_n_tiles` running-max fold: an imperative
|
||||
`for` loop creates a data dependency between iterations (`max_n_tiles`
|
||||
after iteration *i* depends on iteration *i-1*), which Yosys
|
||||
synthesizes as a sequentially-chained carry structure — inherently
|
||||
O(N_SLOTS) deep, not O(log N_SLOTS). At N_SLOTS=4 the chain reached
|
||||
6.25 ns logic + 11.85 ns routing; at N_SLOTS=8 it doubles again (see
|
||||
below).
|
||||
|
||||
## B3 — Minimum fix (two iterations, evidence-driven)
|
||||
|
||||
**v2** (one pipeline stage: register `max_n_tiles` before its use in
|
||||
the `resident_count` comparison): Fmax 55.22 → 72.78 MHz (+31.8%) —
|
||||
real improvement, still fails 80 MHz. Re-tracing showed the *remaining*
|
||||
critical path was entirely inside `max_n_tiles`'s own computation
|
||||
(now feeding its own register), confirming the fix needed to go one
|
||||
level deeper.
|
||||
|
||||
**v3** (second stage: register each slot's tag-equality/masking result
|
||||
first — independent per-slot work, no N_SLOTS-dependent chain — *then*
|
||||
fold the already-registered, already-masked values): Fmax 55.22 →
|
||||
**106.81 MHz** (+93.4%). **PASSES** 80 MHz with real margin. Resource
|
||||
cost: LUT4 -5.5%, FF +1.4% (2 added pipeline registers), CCU2C
|
||||
unchanged.
|
||||
|
||||
## B4 — No serialization reintroduced
|
||||
|
||||
Verified directly: N_SLOTS=2 bit-exact regression test (D-Stress, real
|
||||
V1 PSRAM chain) gives **numerically identical** cycle count and
|
||||
sustained MAC/cycle before and after the fix (185270/185270 cycles,
|
||||
0.1769/0.1769 MAC/cycle). The 3 total cycles of added latency apply
|
||||
only to the rare, tile-refill-boundary-only decision — never to the
|
||||
real-time per-tile consumption path (already fully decoupled by
|
||||
STEP13's own streaming manager). Higher Fmax, zero throughput cost —
|
||||
satisfying B4's explicit requirement.
|
||||
|
||||
## N=8 (exploratory)
|
||||
|
||||
`nms_activation_fill_ctrl_v3.v` at N_SLOTS=8: DSP=64/72 (89%, FEASIBLE),
|
||||
LUT4=4653, FF=10855 (both comfortably FEASIBLE). **Fmax=52.25 MHz,
|
||||
FAILS 80 MHz** — the v3 fix's second stage (the max-fold itself) is
|
||||
still O(N_SLOTS)-deep; at N=8 it is twice as deep as at N=4 and becomes
|
||||
dominant again. This is expected: v3 shifted the crossover point, it
|
||||
did not eliminate the underlying dependency. A genuine balanced-tree
|
||||
reduction (or a pipeline scaling with log₂(N_SLOTS) rather than a flat
|
||||
2-stage split) would be required for N=8 — not undertaken this round
|
||||
(N=8 is explicitly exploratory; the limiting resource (Fmax, not
|
||||
DSP/LUT/FF/BRAM) is precisely identified and quantified, per spec).
|
||||
|
||||
## Adoption
|
||||
|
||||
`nms_activation_fill_ctrl_v3.v` is adopted as the reference activation
|
||||
fill controller for N_SLOTS≥4 configurations (DEC-0027). The original
|
||||
and the insufficient v2 are preserved for reference.
|
||||
@@ -0,0 +1,122 @@
|
||||
# NMS Continuous Tile Stream — Memory Manager Redesign (STEP13)
|
||||
|
||||
Status: implemented, bit-exact verified, synthesized. **Adopted** as
|
||||
the new reference NMS memory-manager configuration (DEC-0025). Full
|
||||
data: `hardware/v2/nms/reports/batch_processor_{sweep.csv,summary.md}`.
|
||||
Full narrative: `hardware/v2/logs/experiments.log` (EXP-0025 through
|
||||
EXP-0028), `decisions.log` (DEC-0024, DEC-0025).
|
||||
|
||||
## Why this file is not `neural_processor_batch.v`
|
||||
|
||||
The governing brief for this STEP asked for a "batch/continuous
|
||||
neuron execution model" — multiple neurons processed per dispatch, or
|
||||
a continuous neuron stream — to amortize the ~68.5-cycles/neuron
|
||||
non-memory floor found in EXP-0024. Before writing any RTL, Step 1
|
||||
required tracing the actual RTL to find exactly where those cycles
|
||||
go, rather than assuming.
|
||||
|
||||
That trace (EXP-0025, an isolated testbench with `neural_processor.v`
|
||||
+ `nms_memory_manager_pf.v` driven with zero real memory latency
|
||||
anywhere) found: **93.4% of the floor is explained by a 4-cycles/tile
|
||||
serialization bug inside the memory manager's own `ST_RUN` state**,
|
||||
not by per-job dispatch overhead (only 6.6%). `ST_RUN` implements
|
||||
operand delivery as a strictly sequential chain —
|
||||
`read_issued → read_ready → present → consumed` — with zero overlap
|
||||
between consecutive tiles, even though:
|
||||
|
||||
- the local activation/weight SRAMs (`nms_activation_replicated.v`,
|
||||
`nms_weight_packed.v`) have only a 1-cycle `rd_en`-to-data latency;
|
||||
- `neural_processor.v`'s own `operand_ready` is held continuously high
|
||||
through the whole tile-loading phase — its datapath is explicitly
|
||||
designed (per its own header comment) to accept a new tile every
|
||||
cycle while previous tiles drain through the adder tree/accumulator.
|
||||
|
||||
Neither side of this interface requires 4 cycles/tile. It is purely
|
||||
an artifact of the memory manager's own un-pipelined FSM. **The fix is
|
||||
therefore a continuous per-tile streaming redesign of the memory
|
||||
manager, not a neuron-batching scheme — hence
|
||||
`nms_memory_manager_stream.v`, not `neural_processor_batch.v`.**
|
||||
`neural_processor.v` itself required no modification.
|
||||
|
||||
## Design: `nms_memory_manager_stream.v`
|
||||
|
||||
Drop-in replacement for `nms_memory_manager_pf.v` (identical external
|
||||
interface, same `weight_prefetch_engine.v` instance, same outer job
|
||||
FSM `ST_IDLE`/`ST_WAIT_RESULT`/`ST_WRITE_RES`/`ST_DONE`). Only
|
||||
`ST_RUN`'s internal operand-delivery logic differs:
|
||||
|
||||
- `rd_ptr` — the read-**issue** pointer (which tile's SRAM read has
|
||||
been, or is about to be, issued), independent of and normally one
|
||||
tile ahead of `tile_idx` (the **consumption** pointer, i.e. how many
|
||||
tiles `neural_processor.v` has actually accepted).
|
||||
- A 1-deep skid buffer (`buf_valid`/`buf_input`/`buf_weight`/
|
||||
`buf_last`) holds one tile's fully-read SRAM data, presented to NP
|
||||
as `operand_valid`/`input_data`/`weight_data`/`tile_last`.
|
||||
- Every cycle: if a read issued last cycle is landing now (1-cycle
|
||||
SRAM latency), it's captured into the skid buffer; independently, a
|
||||
new read is issued for `rd_ptr` whenever legal (in bounds, weight +
|
||||
activation ready) **and** the buffer will not overflow (empty, or
|
||||
being drained this same cycle).
|
||||
|
||||
Since `operand_ready` stays high throughout the tile-loading phase,
|
||||
the skid buffer drains every cycle it's full, so a new read can be
|
||||
issued every cycle too — sustained ~1 cycle/tile, down from 4.
|
||||
|
||||
`tile_idx` (the consumption pointer) is still what feeds
|
||||
`weight_prefetch_engine.v`'s own `consumed_count` port — its external
|
||||
contract is unchanged; only the local SRAM read-issue pointer
|
||||
(`rd_ptr`) is new, and it can run up to one tile ahead of `tile_idx`
|
||||
(the skid buffer's own depth).
|
||||
|
||||
## Verification chain (all real, none assumed)
|
||||
|
||||
1. **EXP-0025**: isolated zero-latency trace of the *old* design —
|
||||
established the 4-cycles/tile floor and its 93.4% share of
|
||||
EXP-0024's real measured floor.
|
||||
2. **EXP-0026**: same isolated trace against the *new* design — the
|
||||
fix works exactly as designed (confirmed cycle-by-cycle), but
|
||||
total cycles barely move (81→80), because it immediately hits a
|
||||
*second*, previously-masked bottleneck: `weight_prefetch_engine.v`'s
|
||||
own word-fetch rate is *also* exactly 4 cycles/tile (P_IN=8 bytes ÷
|
||||
16-bit bus = 4 word-transactions, 1 cycle/word minimum even at
|
||||
zero real latency) — a bus-**width** ceiling, structurally
|
||||
different from an FSM-serialization ceiling, that happens to
|
||||
coincide numerically today.
|
||||
3. **EXP-0027**: a direct control experiment — a scratch variant with
|
||||
weight-fetch bypassed (always-ready) isolates the new design's
|
||||
*own* ceiling: a clean 1 cycle/tile (100% of `neural_processor.v`'s
|
||||
theoretical per-tile rate), vs. the old design's hard 4-cycles/tile
|
||||
cap under the identical bypass. This is the direct proof that the
|
||||
fix removes a real, 4× architectural ceiling — it was just masked
|
||||
by a coincidentally-equal second bottleneck.
|
||||
4. **EXP-0028**: full real-system integration
|
||||
(`nms_dataflow_core_stream.v` → `nms_neural_multiprocessor_stream.v`,
|
||||
real V1 PSRAM chain) — bit-exact PASS, 256/256 neurons, D-Stress
|
||||
workload identical to EXP-0022/0024. Real cycle count: 185270 vs.
|
||||
185398 (`_pf` baseline), -0.07% — confirms the "masked, zero net
|
||||
benefit today" prediction exactly. Real synthesis + P&R: N=1
|
||||
Fmax=142.92 MHz (+3.7% vs. baseline), N=2 Fmax=92.57 MHz (-2.8%,
|
||||
still comfortably above 80 MHz), resource cost within ±6%. N=4:
|
||||
55.22 MHz, FAILS 80 MHz — but for the *pre-existing*,
|
||||
already-documented `nms_activation_fill_ctrl.v` priority-scan
|
||||
regression (EXP-0022), unrelated to and unaffected by this fix.
|
||||
|
||||
## Outcome and adoption
|
||||
|
||||
**Outcome B** (helps, but another bottleneck appears — see
|
||||
DEC-0025 and `batch_processor_summary.md` for the full nine-question
|
||||
final decision). `nms_memory_manager_stream.v` is adopted as the new
|
||||
reference configuration: it is a strict improvement (bit-exact,
|
||||
resource-neutral, no measured downside) and is **required groundwork**
|
||||
for any future PSRAM bandwidth increase to actually translate into a
|
||||
throughput gain — without it, a wider/faster memory would immediately
|
||||
hit the old FSM's 4-cycles/tile ceiling and realize only 25% of its
|
||||
potential benefit. The original `nms_memory_manager.v` and
|
||||
`nms_memory_manager_pf.v` remain preserved, unmodified, for A/B/C
|
||||
reference. Neuron-batching (the brief's original Model B/C) was not
|
||||
pursued — evidence showed it addresses only 6.6% of the real floor and
|
||||
would deliver no measurable benefit today for the identical reason
|
||||
(weight-fetch-rate-bound). N=4/N=8 viability remains blocked by two
|
||||
independent issues neither addressed by this STEP: external PSRAM
|
||||
bandwidth, and the activation fill controller's own Fmax regression —
|
||||
both flagged as future work.
|
||||
@@ -0,0 +1,145 @@
|
||||
# NMS Real Weight Prefetch Engine (STEP11)
|
||||
|
||||
Status: implemented, bit-exact verified, benchmarked against the real
|
||||
V1 PSRAM chain, synthesized. **Not adopted as the default NMS
|
||||
configuration** — see Outcome/Recommendation below. Full data:
|
||||
`hardware/v2/nms/reports/nms_prefetch_sweep.csv`,
|
||||
`nms_prefetch_summary.md`; full narrative:
|
||||
`hardware/v2/logs/experiments.log` (EXP-0023, EXP-0024),
|
||||
`decisions.log` (DEC-0023), `errors.log` (ERR-0015).
|
||||
|
||||
## Problem
|
||||
|
||||
The "Current NMS" baseline (`nms_memory_manager.v`, backed by
|
||||
`prefetch_engine.v`) measured `prefetch_effectiveness≈0%` and
|
||||
`weight_stall≈92.5%` at N_SLOTS=2 (EXP-0022). Tracing the actual RTL
|
||||
(not assuming from filenames) showed the real gap: `prefetch_engine.v`
|
||||
is a single-shot FSM (`ST_IDLE`/`ST_READ_W`/`ST_DONE`) that can only
|
||||
have **one fetch in flight at a time**, and `nms_memory_manager.v`'s
|
||||
own restart logic only re-triggers the next tile's fetch once the
|
||||
*previous* tile's fetch has fully completed and the FSM has returned
|
||||
to idle — paying a real per-tile control-plane restart cost on every
|
||||
tile boundary. The gap was never insufficient lookahead *distance*
|
||||
(the old design already tried to fetch as far ahead as `n_tiles`
|
||||
allowed); it was zero *outstanding-request depth*.
|
||||
|
||||
## Real backend constraint
|
||||
|
||||
`memory_interface.v` → `psram_controller.v` (V1, reused verbatim,
|
||||
never modified) is a fire-and-forget, **one-transaction-in-flight**
|
||||
protocol: a single `mem_req` pulse, wait for `mem_ready`, and that IS
|
||||
the whole transaction. No wire-level pipelining is physically possible
|
||||
against a real single PSRAM port. So "multiple outstanding requests"
|
||||
cannot mean multiple simultaneous word transactions — it means
|
||||
eliminating the *control-plane* overhead paid at every tile boundary
|
||||
and letting the fetch stream run continuously across tiles, queueing
|
||||
up to `PREFETCH_DISTANCE` tiles of lookahead ahead of consumption.
|
||||
|
||||
## Design: `weight_prefetch_engine.v`
|
||||
|
||||
Two monotonic counters fully describe the engine (tiles are always
|
||||
fetched in strict sequential order, never reordered or re-fetched, so
|
||||
no per-tile state array is needed):
|
||||
|
||||
- `fetch_tile`/`fetch_word` — the next word to request (or the word
|
||||
currently in flight).
|
||||
- `ready_count` — tiles 0..`ready_count`-1 are fully resident in the
|
||||
weight SRAM.
|
||||
|
||||
`consumed_count` (the consumer's own tile index, `nms_memory_manager_pf.v`'s
|
||||
`tile_idx`) bounds a configurable lookahead window:
|
||||
`window_limit = consumed_count + PREFETCH_DISTANCE`; the engine may
|
||||
fetch tile K only if `K < n_tiles` **and** `K < window_limit`.
|
||||
|
||||
The core mechanism: on `mem_ready && req_outstanding`, the just-completed
|
||||
word is committed **and**, in the same cycle, the very next request is
|
||||
issued — either the same tile's next word, or (at a tile boundary) the
|
||||
next tile's first word — giving zero-gap streaming across tile
|
||||
boundaries against a backend that only ever has one word in flight.
|
||||
(An earlier draft used mutually-exclusive `if/else-if` branches for
|
||||
"commit" vs. "issue next", which reintroduced a 1-cycle gap between
|
||||
*every* word, not just tile boundaries; fixed by merging both into one
|
||||
branch — see `weight_prefetch_engine.v`'s own header comment.)
|
||||
|
||||
## Integration: the "_pf" A/B variants
|
||||
|
||||
Per the explicit "preserve the current NMS baseline" constraint, the
|
||||
new engine was integrated into parallel `_pf`-suffixed files, leaving
|
||||
the originals untouched:
|
||||
|
||||
- `nms_memory_manager_pf.v` — drop-in replacement for
|
||||
`nms_memory_manager.v`'s external interface; internally swaps the
|
||||
private `prefetch_engine.v` instance for `weight_prefetch_engine.v`,
|
||||
and changes `can_present`'s weight-ready check from
|
||||
`tile_idx < wgt_fetched` to `tile_idx < wgt_ready_count`.
|
||||
- `nms_dataflow_core_pf.v` — mirrors `nms_dataflow_core.v`, adds a
|
||||
`PREFETCH_DISTANCE` parameter, instantiates `nms_memory_manager_pf`.
|
||||
- `nms_neural_multiprocessor_pf.v` — mirrors
|
||||
`nms_neural_multiprocessor.v`, instantiates `nms_dataflow_core_pf`.
|
||||
|
||||
Both the baseline (`nms_neural_multiprocessor.v`) and the prefetch
|
||||
variant (`nms_neural_multiprocessor_pf.v`) remain in the repository
|
||||
side by side; neither supersedes the other.
|
||||
|
||||
## Verification
|
||||
|
||||
`hardware/v2/nms/sim/tb_weight_prefetch.v` — isolated correctness
|
||||
testbench: real `sim_word_mem` (configurable extra latency), real
|
||||
`nms_weight_packed.v` production SRAM, bit-exact fill-pattern checking.
|
||||
Covers `n_tiles ∈ {0,1,2,PFD,PFD+1,MAX_TILES-1,MAX_TILES}`, back-to-back
|
||||
jobs with no explicit reset, a dedicated windowing-cap test (frozen
|
||||
consumer, confirms `ready_count` stops exactly at
|
||||
`min(PFD,MAX_TILES)`), and (post-ERR-0015) a large-PFD regression case.
|
||||
10/10 (9/9 at PFD≥MAX_TILES) tests pass bit-exact across
|
||||
PFD∈{1,2,4,8,32} and under injected extra memory latency.
|
||||
|
||||
`hardware/v2/nms/sim/tb_nms_dstress_pf.v` — full real-integration
|
||||
benchmark: identical D-Stress workload/golden-model/correctness
|
||||
criteria as `tb_nms_dstress.v` (EXP-0022), instantiating
|
||||
`nms_neural_multiprocessor_pf` with a `PFD_CFG` parameter, plus new
|
||||
testbench-only instrumentation for `weight_stall_cycles` and
|
||||
`prefetch_effectiveness` (tiles consumed with zero weight-blocking
|
||||
cycles beforehand / total tiles consumed — the exact STEP11
|
||||
definition). All runs pass 256/256 neurons bit-exact vs. the golden
|
||||
model.
|
||||
|
||||
## ERR-0015: a real bug found and fixed
|
||||
|
||||
The initial `window_limit` computation truncated the
|
||||
`PREFETCH_DISTANCE` *parameter itself* to `CNTW` bits
|
||||
(`PREFETCH_DISTANCE[CNTW-1:0]`) before adding it to `consumed_count`.
|
||||
At `MAX_TILES=16` (`CNTW=5` bits), `PFD=32` truncates to 0, making
|
||||
`window_limit == consumed_count` forever and deadlocking the engine
|
||||
completely (0/256 neurons ever completed, 0% PSRAM utilization).
|
||||
Fixed by computing `window_limit` and its comparisons in a fixed
|
||||
32-bit width, using the untruncated parameter value. Regression-tested
|
||||
in `tb_weight_prefetch.v`. Full writeup: `errors.log` ERR-0015.
|
||||
|
||||
## Results and outcome
|
||||
|
||||
See `nms_prefetch_summary.md` for the full comparison table and the
|
||||
nine explicitly-answered final-report questions. In short:
|
||||
|
||||
- **N_SLOTS=1** (no port contention): a real, reproducible **-10.3%**
|
||||
cycle-count improvement (PFD=1 → PFD≥2), then a complete plateau —
|
||||
deeper buffering gives zero further benefit. Sustained MAC/cycle
|
||||
reaches only 2.8% of the theoretical target.
|
||||
- **N_SLOTS=2** (this project's own primary reference configuration,
|
||||
real shared-port contention via `slot_mem_arbiter`): **zero
|
||||
measurable benefit** at any PREFETCH_DISTANCE from 1 to 16 — all
|
||||
runs are statistically indistinguishable from each other and from
|
||||
the pre-STEP11 baseline. The single physical PSRAM port is already
|
||||
saturated (90.5% busy, unchanged from baseline) by natural two-slot
|
||||
contention before any lookahead scheme can act.
|
||||
|
||||
**Final decision: Outcome B (N_SLOTS=1, partial) / Outcome C
|
||||
(N_SLOTS=2, failure against the 90% criterion).** The mechanism is
|
||||
correct and does measurably hide latency when the port has spare
|
||||
capacity; it cannot manufacture bandwidth out of an already-saturated
|
||||
single physical port. Reaching the STEP11 target would require ~36×
|
||||
(N=1) to ~82× (N=2) more real PSRAM bandwidth — a hardware-level
|
||||
constraint, not an RTL-scheduling one. Per DEC-0023, the new engine is
|
||||
**not** recommended as the default NMS configuration; both variants
|
||||
are preserved for reference. The evidence-backed next step (real PSRAM
|
||||
bandwidth — wider bus, multiple independent banks, or a faster backing
|
||||
technology) is flagged as future work, not undertaken this round.
|
||||
@@ -0,0 +1,91 @@
|
||||
# NMS Weight Datapath Scaling (STEP14 Part A)
|
||||
|
||||
Status: architectural requirement established and proven (simulation),
|
||||
**not realizable on real hardware today** (fixed 16-bit physical
|
||||
PSRAM). Full data: `hardware/v2/reports/step14_weight_scaling.csv`.
|
||||
Full narrative: `hardware/v2/logs/experiments.log` (EXP-0032, EXP-0033),
|
||||
`decisions.log` (DEC-0028).
|
||||
|
||||
## Question answered
|
||||
|
||||
*At what weight-path width does the processor stop being fundamentally
|
||||
starved by weight delivery?* **64 bits** — exactly `P_IN × DATA_WIDTH`
|
||||
(8 × 8). Proven by direct cycle-exact simulation, not assumed.
|
||||
|
||||
## What was built
|
||||
|
||||
`weight_prefetch_engine_wide.v` — a parameterized (`MEM_DATA_WIDTH`)
|
||||
generalization of the real `weight_prefetch_engine.v`'s continuous
|
||||
cross-tile-boundary streaming design, simulation-only/exploratory
|
||||
(same status as `ideal_memory_model.v`). `WORDS_PER_TILE =
|
||||
ceil(P_IN*DATA_WIDTH / MEM_DATA_WIDTH)`, clamped to a minimum of 1.
|
||||
`nms_memory_manager_stream_wide.v` pairs it with STEP13's own streaming
|
||||
memory manager unchanged (A2's requirement), on a *separate* logical
|
||||
wide port from the real 16-bit result-write-back port.
|
||||
|
||||
A real bug was found and fixed during development: address stepping
|
||||
initially used `WORDS_PER_TILE × BYTES_PER_WORD` as the inter-tile
|
||||
byte stride, which over-counts whenever the bus is wider than one full
|
||||
tile (the 128-bit case, `WORDS_PER_TILE=1` but `BYTES_PER_WORD=16`
|
||||
while the tile itself is only 8 bytes) — this skips over the next
|
||||
tile's actual data in the packed backing store. Fixed by defining
|
||||
`TILE_BYTES = TILE_BITS/8` as the canonical, width-independent stride.
|
||||
|
||||
## Results (bit-exact + ideal-memory cycle count)
|
||||
|
||||
| Width | Words/tile | Steady-state cycles/tile | 16-tile job total |
|
||||
|---|---|---|---|
|
||||
| 16-bit | 4 | 4 | 80 |
|
||||
| 32-bit | 2 | 2 | 48 |
|
||||
| **64-bit** | **1** | **1** | **32** |
|
||||
| 128-bit | 1 | 1 | 32 |
|
||||
|
||||
All four widths pass bit-exact correctness (9/9 tests each, including
|
||||
under injected extra memory latency). 64-bit achieves a clean,
|
||||
cycle-exact **1 cycle/tile** — 100% of `neural_processor.v`'s own
|
||||
theoretical per-tile acceptance rate, exactly matching the streaming
|
||||
memory manager's own ceiling (EXP-0027, STEP13). 128-bit gives **zero**
|
||||
further benefit: a bus wider than one full tile still delivers exactly
|
||||
one tile per transaction in this single-tile-per-request design (no
|
||||
multi-tile bursting was attempted).
|
||||
|
||||
## The critical distinction: logical vs. physical bandwidth (A5)
|
||||
|
||||
STEP14 explicitly warned against assuming a wider logical interface
|
||||
means the real memory can deliver it. It cannot, here: **the real V1
|
||||
PSRAM chain is fixed at 16 bits** — a real chip
|
||||
(ISSI IS66WVE4M16EBLL-70BLI, x16), not an RTL parameter. The
|
||||
already-existing, already-verified `weight_prefetch_engine.v` (real,
|
||||
used throughout STEP11-13) *is* exactly what a "64-bit logical / 16-bit
|
||||
physical" packing adapter would produce: it assembles one 64-bit
|
||||
logical tile from 4 real sequential 16-bit word transactions. Its real,
|
||||
repeatedly-measured result is 4 cycles/tile — identical to the ideal
|
||||
16-bit row above, because the real transaction count is unchanged
|
||||
regardless of what the logical interface upstream claims. **A logical
|
||||
wide interface backed by a physically-narrow bus delivers exactly the
|
||||
narrow bus's own throughput.** No new "packing adapter" module was
|
||||
built for this reason — the real engine already demonstrates the
|
||||
answer, conclusively, without further RTL.
|
||||
|
||||
## Answer to the primary research questions
|
||||
|
||||
- **Is 16-bit weight delivery fundamentally insufficient for P_IN=8?**
|
||||
Yes — it costs 4 cycles/tile, 4× the achievable minimum.
|
||||
- **Is 32-bit enough?** No — still 2× the achievable minimum (2
|
||||
cycles/tile).
|
||||
- **Is 64-bit the natural architectural point?** Yes, exactly — proven
|
||||
cycle-exact, not approximate.
|
||||
- **Does wider logical delivery actually improve real throughput?**
|
||||
**Not on this hardware.** Realizing the 64-bit ideal requires a
|
||||
matching *physical* bandwidth increase (a real 64-bit-wide external
|
||||
bus, or multiple parallel 16-bit PSRAM chips banked together) — a
|
||||
board/silicon-level change, outside this project's own RTL scope.
|
||||
|
||||
## Recommendation
|
||||
|
||||
The 64-bit requirement is now precisely quantified and should inform
|
||||
any future hardware revision (wider PSRAM, multiple banks). No RTL
|
||||
change is warranted on the current board: `weight_prefetch_engine.v`
|
||||
(real, 16-bit) remains the correct, already-optimal implementation
|
||||
given the fixed physical bus width — STEP13's streaming-manager fix
|
||||
already extracts everything available from the real interface.
|
||||
Reference in New Issue
Block a user