feat: real result-writeback engine, removes the last hard N-scaling pin blocker (EXP-0088)

Adds result_writeback.v, one instance per packed_slot.v, writing each
completed job's result directly into DDR3 at the job's own
result_addr_a/b instead of driving literal top-level pins -- the same
architectural shape as the weight-fetch path, in reverse. job_done now
means "durably in DDR3", not "captured in a register only a pin could
see". n2_system_ddr3_top.v's own s0_result_data_a/b, s1_result_data_a/b
top-level package pins are removed (and the now-dangling XDC constraint
for them), closing the real, hard scaling blocker docs/ARCHITECTURE_
ANALYSIS.md flagged since EXP-0074/0079 (8 bits x 2 lanes x N cores ->
256 pins at N=16).

Addressing reuses the exact same JOB_ADDR_WIDTH->ctrl-bus-word
truncation x_base_a/w_base already use (verified against act_tile_
fetch.v's/layer_prefetch_ctrl.v's own real code, not guessed). The host
reads results back via the already-existing READ_MEM (0x02) SPI opcode
-- no new protocol. A real EXP-0066-class bug (issuing ctrl_req before
mem_grant) was caught and fixed before ever compiling, by re-deriving
the design against act_tile_fetch.v's own proven S_MEMWAIT/S_GAP
sequencing.

Verified two ways: tb_packed_slot.v extended with a real DDR3
read-after-write check (9/9 PASS, confirms the write actually landed,
not just that job_done pulsed); tb_n2_system_ddr3.v re-run via real
xsim to confirm correct behavior under real 2-slot shared-bus
arbitration (8/8 PASS, 0 errors, consistent timing with EXP-0087's own
baseline for this workload).

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 19:18:57 +02:00
co-authored by Claude Sonnet 5
parent 344e798ad5
commit ccaf3ee059
8 changed files with 534 additions and 51 deletions
+71 -15
View File
@@ -267,16 +267,23 @@ Real, verified, low resource cost, not on any critical performance path
(host commands are inherently much slower than the internal compute/memory
loop). No bottleneck here. Not a scaling concern.
### 4.6 Missing: result-writeback engine
### 4.6 [DONE, EXP-0088] Result-writeback engine
Still genuinely absent (disclosed since `packed_slot.v`'s own original
header, unchanged through EXP-0079). Currently `result_data_a/b` are literal
top-level pins — functional at N=2 (32 pins), but this is the **exact same
class of mistake already caught once** for activation data (EXP-0074: ~360
pins nearly exceeded the whole package's I/O budget). At N=16 this port
alone would need 8 bits × 2 lanes × 16 cores = 256 pins — **a real, hard
blocker for any scaling beyond a handful of cores**, independent of the
memory-bandwidth ceiling in §3. Recommended fix in §5.3.
**No longer absent.** `result_writeback.v` (new module) is instantiated
inside each `packed_slot.v`, sharing that slot's own ctrl port with
`layer_prefetch_ctrl.v`/`ddr_prefetch_mgr.v` exactly the way those two
already share it with each other (mutually exclusive in time by FSM
construction — writeback only starts in `S_RESULT`, strictly after the
tile loop has finished). Each completed job's result is written directly
into DDR3 at the job's own `result_addr_a`/`result_addr_b` — the SAME
architectural shape as the weight-fetch path, in reverse, exactly as this
section used to recommend before it was built. `job_done` now means "the
result is durably in DDR3", not merely "captured in a register only a
literal top-level pin could see". `n2_system_ddr3_top.v`'s own
`s0_result_data_a/b`/`s1_result_data_a/b` top-level PACKAGE PINS are
**removed** — the real, hard N-scaling blocker this section used to flag
(8 bits × 2 lanes × N cores → 256 pins at N=16) no longer exists at any N.
See §5.3 for the full real design/verification story.
---
@@ -433,13 +440,62 @@ exists in this codebase; kept for when it's revisited):
reorders/never dispatches that queued job) also wastes real bandwidth —
needs a real cancellation/staleness mechanism, not assumed away.
### 5.3 [Blocking for any real scaling] Result-writeback engine
### 5.3 [DONE, EXP-0088] Result-writeback engine
Must exist before N>2 is even attemptable (§4.6) — result data needs to go
into DDR3 (or through the SPI status/register path for small result sets),
never as N-scaled literal top-level pins again. Same architectural shape as
the weight-fetch path, in reverse (write instead of read) — a reasonable,
bounded scope, and a real prerequisite, not optional polish.
**Built and real-verified.** New module `result_writeback.v`, one instance
per `packed_slot.v` (matching how `layer_prefetch_ctrl.v`/`act_tile_
fetch.v`/`ddr_prefetch_mgr.v` are already one-per-slot, not a new
arbiter-requester count as N scales). On job completion (`S_RESULT`), it
writes BOTH lanes' results into DDR3 at the job's own `result_addr_a`/
`result_addr_b` and only THEN asserts `job_done` — real, correctness-first
sequencing (`job_done` now means "durably in DDR3", not "captured in a
register" the way it used to).
**Real addressing** (verified against `act_tile_fetch.v`'s/`layer_
prefetch_ctrl.v`'s own real address-computation code, not guessed):
`result_addr_a/b` arrive in `packed_slot.v`'s own `JOB_ADDR_WIDTH=26`-bit
convention; the low `ADDR_WIDTH=25` bits (dropping the unused top/MSB
headroom bit) are used directly as a ctrl-bus-native 32-bit-word address
— the exact same address space `x_base_a`/`w_base` already live in. One
full 32-bit ctrl-word is written per lane: `{node_id[15:0], 8'h00,
result_data[7:0]}`. The host reads results back via the ALREADY-EXISTING
`READ_MEM` (0x02) SPI opcode — no new protocol. Real, disclosed
host-firmware implication (not yet built, same class of gap as this
project's other disclosed firmware work, e.g. JTAG bit-banging): reading
a result back needs `mem_addr = result_addr[24:0]*2` for the value and
`+1` for node_id (2 host reads per lane), since `READ_MEM`'s own
`mem_addr` is 16-bit-word-granular while this engine writes a native
32-bit ctrl-word — the same real halving `host_mem_bridge.v`'s own header
already discloses for the debug raw-access path.
**Shared-bus discipline** (mirrors `act_tile_fetch.v`'s own real,
proven pattern, not reinvented): waits for `mem_grant` before ever
issuing `ctrl_req` (EXP-0066's own established rule — an early/blind
request on a shared bus can lose the request permanently); a real
`S_GAP` state waits for `!ctrl_busy` between lane A's write and lane
B's own, since `mig_native_adapter.v`'s own `busy` stays asserted one
cycle past `ctrl_ready`. `wmask` polarity matches `host_mem_bridge.v`'s
own real, already-working convention exactly (0 = write this byte, 1 =
masked, the same DQM-style polarity this project's whole memory stack
already uses end to end).
**Real, hard scaling blocker removed**: `n2_system_ddr3_top.v`'s own
`s0_result_data_a/b`/`s1_result_data_a/b` top-level PACKAGE PINS are
gone — each slot's `packed_slot.v` still exposes `result_data_a/b` etc.
as plain output ports (for debug/testbench visibility, unchanged), but
these are no longer wired to literal FPGA package pins at any N.
**Verification**: `tb_packed_slot.v` extended with a new real
read-after-write check (`verify_writeback` task) — after each job's
`job_done`, the testbench independently reads back the exact DDR3
location `result_writeback.v` should have written and confirms both the
result value AND node_id match, closing the loop (not just checking
`job_done` eventually pulses). **9/9 PASS, 0 errors**, real Icarus xsim
against `burst_mem_model32.v`. Also re-verified at the full N=2 system
level (`tb_n2_system_ddr3.v`, real xsim against the real, closed
32-bit/3225ps DDR3 model) to confirm the writeback engine behaves
correctly under real shared-bus arbitration contention between 2 slots
— see `hardware/v2/logs/experiments.log` EXP-0088 for the real result.
### 5.4 [DONE] Widening the physical DDR3 channel: 32-bit single channel vs. a second independent 16-bit channel — **functionally complete AND real timing closed (EXP-0084 → EXP-0086)**