feat: real first step of 4x4 hybrid systolic architecture (EXP-0089)

Adds packed_pe.v (packed_slot.v's compute+activation-fetch+writeback
subsystem, reusing ddr_prefetch_mgr.v/neural_processor_packed.v/
result_writeback.v completely unmodified, with its own private
weight-fetch removed) and systolic_group.v (one real layer_prefetch_
ctrl.v+layer_weight_buffer.v+weight_tile_gather.v shared by 4x
packed_pe.v via a real, barrier-synchronized broadcast bus).

Real design choice confirmed with the user before writing any RTL
(AskUserQuestion, concrete topology preview): shared-weight broadcast,
not a literal PE-to-PE systolic shift register -- achieves the real,
quantified rationale (4x reduction in redundant weight-fetch DDR3
traffic per group of 4 PEs) with much lower real risk than genuine
inter-PE pipeline fill/drain.

The real new design is the barrier: each PE's own tcnt is the join key
against the group's broadcast tcnt, self-synchronizing regardless of
which PE is momentarily ahead/behind (e.g. a real DDR3 row-switch
stall on one PE's own activation fetch). Found and fixed a real bug
during verification (not by inspection): the first full test run
reported every result as undefined despite every control-flow signal
tracing correctly -- root-caused via real signal tracing down to a
5-way test arbiter bus mis-sliced at the wrong slot offset (single-bit
handshake buses happened to use a correct range and masked it from the
control-flow trace; only the wide, byte-offset buses were wrong).

Verified in isolation (tb_systolic_group.v, real Icarus xsim, real
sdram_arbiter_n.v generalized to NUM_REQ=5 with zero changes): 8/8
PASS across 2 consecutive group jobs (exercising the barrier's own
per-job reset path, not just cold start).

Deliberately scoped to the isolated mechanism only, per this project's
"one variable at a time" discipline -- Director/SPI job dispatch for
group jobs, a real N=16 top-level, and real P&R are real, disclosed
next steps, not done here.

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 22:54:52 +02:00
co-authored by Claude Sonnet 5
parent 7f9ece12dc
commit 932aec2490
5 changed files with 1104 additions and 25 deletions
+131
View File
@@ -5948,3 +5948,134 @@ design confirmed by the user -- NOT a literal PE-to-PE systolic shift
register -- verified in isolation before any Director/SPI-protocol
integration, matching this project's own "one variable at a time"
discipline).
EXP-0089 -- real, isolated first step of the 4x4 hybrid systolic
architecture: shared-weight-broadcast group, built and verified
(2026-09-20, user's own explicit reprioritization: "Fai la parte
realmente mancante prima, il RESULT-WRITEBACK e poi implementa la 4x4
sistolica"; real A/B design decision confirmed by the user before any
RTL was written: shared-weight BROADCAST, not a literal PE-to-PE
systolic shift register)
CONTEXT: docs/ARCHITECTURE_ANALYSIS.md S5.6 captured this direction as
purely exploratory ("nothing in this subsection is implemented"),
explicitly flagging "intra-chain dataflow RTL... is a real, new design,
not a trivial extension" as an open question. Before writing any RTL,
the user was asked (AskUserQuestion, with a concrete side-by-side
preview of both real candidate topologies) to resolve that open
question directly: (A) one shared weight fetch per group of 4 PEs,
broadcast to all 4, each PE computing its own independent activation
positions in parallel -- achieves the doc's own quantified rationale
(4x reduction in redundant weight-fetch DDR3 traffic per group) with
much lower real risk; or (B) a literal PE-to-PE systolic shift
register (weight physically translating PE by PE, real pipeline
fill/drain at chain boundaries). User confirmed (A), explicitly noting
(B) can be revisited later if real data justifies it.
DESIGN: two new modules.
- `packed_pe.v`: packed_slot.v's own compute+activation-fetch+
writeback subsystem (ddr_prefetch_mgr.v, neural_processor_packed.v,
result_writeback.v -- all THREE reused completely unmodified),
with packed_slot.v's own private layer_prefetch_ctrl.v/layer_
weight_buffer.v/weight_tile_gather.v REMOVED -- weight tile data
arrives via a real, group-broadcast interface instead
(group_tcnt/group_tile_data/group_tile_valid).
- `systolic_group.v`: ONE real layer_prefetch_ctrl.v + layer_weight_
buffer.v + weight_tile_gather.v (group-level, shared by reference,
not duplicated -- identical instances to what packed_slot.v already
owned per-slot), driving 4x packed_pe.v.
REAL SYNCHRONIZATION (the actual new design, not a trivial extension --
confirmed by building and testing it, not asserted): each packed_pe.v's
own `tcnt` IS the join key -- a PE in S_TILEWAIT waits for
`group_tile_valid && (group_tcnt == tcnt)` before consuming, a real,
self-synchronizing comparison immune to which PE happens to be
momentarily ahead or behind (e.g. one PE's own activation fetch hit a
real DDR3 row switch the others didn't). systolic_group.v's own barrier
extends this project's established 2-source "S_TILEWAIT join"
discipline to 4 independent sources: it only advances to the next
tile's weight fetch once ALL 4 PEs have ack'd the current one
(`pe_acked`, individually latched, using a `pe_acked_next` combinational
fold-in to detect same-cycle acks without an extra latency cycle). A
similar accumulator (`pe_done_latch`) tracks all 4 PEs' own job_done
pulses, deliberately made UNCONDITIONAL (not state-gated) to correctly
catch a real race: a fast PE's own job_done can pulse the SAME cycle
the group's own barrier clears the LAST tile (i.e. the same cycle the
group transitions toward waiting for completion) -- a state-gated
accumulator would have silently dropped that pulse.
REAL BUGS FOUND AND FIXED DURING DESIGN (before compiling, via re-
deriving against already-proven code, not guessed) AND DURING
VERIFICATION (via real signal tracing, not by inspection):
1. First draft of packed_pe.v omitted the neural_processor_packed.v
job_valid/job_ready handshake's own real gating (packed_slot.v's
own proven S_JOBSTART state) -- would have raced operand_valid
against job acceptance. Caught by re-deriving against packed_
slot.v's own real sequencing before ever compiling, not by
simulation.
2. systolic_group.v's first draft indexed an expression directly
(`(tcnt + 16'd1)[BUFADDRW-1:0]`) -- not valid plain Verilog syntax
(only nets/regs, not arbitrary expressions, can be part-selected).
Fixed with an intermediate `tcnt_next` wire.
3. A duplicate `pe_acked_pulse` wire declaration (real copy-paste
leftover from restructuring the generate block).
4. THE real bug, found via real hierarchical signal tracing after the
first full test run reported EVERY result as `x` (undefined) even
though `job_done` fired and every control-flow signal traced
correctly (barrier, tcnt, state transitions all real and correct)
-- proving the bug was in DATA, not control flow. Traced down
through ddr_prefetch_mgr.v's own real, already-proven internals
(confirmed innocent) to the actual root cause: the ISOLATED
testbench's own 5-way arbiter wiring (1 group weight-fetch slot +
4 PE slots) sliced the flattened `pe_ctrl_addr/wdata/wmask/rdata`
buses starting at the WRONG offset (`4*WIDTH`, i.e. slot 4, instead
of `1*WIDTH`, slot 1 -- slot 0 is the group's own weight-fetch).
The single-bit handshake buses (req/grant/ready/busy) happened to
use a correct `[4:1]` bit range and so masked the bug from the
control-flow trace entirely -- only the wide, byte-offset-computed
buses were wrong, which is exactly why data silently corrupted
while every handshake still looked correct. A real, honest example
of why "the control flow works" is not suffient evidence that "the
data path works" -- this project's own standing discipline (root-
cause via real signal tracing, verify don't assume) is what caught
it, not luck.
VERIFICATION: new `tb_systolic_group.v`, real Icarus xsim, real
`burst_mem_model32.v` backend shared via a real `sdram_arbiter_n.v`
(NUM_REQ=5, confirmed its own NUM_REQ parameter already generalizes to
this shape with zero changes, per this project's own prior note).
Preloads 2 real layers' worth of weights + 16 real activation
positions, runs TWO consecutive group jobs (deliberately, to exercise
the barrier's own per-job reset path, not just a single cold-start).
**8/8 PASS (2 jobs x 4 PEs), 0 errors**, real, independently-reproduced
golden model (same formula convention as every other v3 testbench).
This verifies the shared-weight-broadcast mechanism itself is
functionally correct -- it does NOT re-verify result_writeback.v's own
real DDR3 read-after-write correctness, since packed_pe.v reuses that
module completely unmodified and it already has its own real
read-after-write verification (EXP-0088, tb_packed_slot.v).
DECISION: real, verified first step. This is deliberately SCOPED to the
isolated group mechanism only, per this project's own "one variable at
a time" discipline (same precedent as act_tile_fetch.v EXP-0079 or
ddr_prefetch_mgr.v EXP-0083, both built and verified standalone well
before system-level integration). NOT yet integrated: Director-level
job dispatch (neural_director_packed.v only knows how to dispatch
2-position jobs to flat slots, not 4-position jobs to a group),
SPI/WRITE_JOB protocol support for group-level job submission, a real
N=16 (4 groups x 4 PEs) top-level module, and real P&R signoff for any
of this. These are real, disclosed, deliberately deferred next steps,
not overlooked.
next_action: (1) real P&R for systolic_group.v in isolation (out-of-
context first, matching this project's own precedent for brand-new
modules before real system integration) to get a first real sense of
its area/timing cost. (2) Design the real Director/SPI protocol
extension needed for group-level job dispatch (a real, disclosed,
non-trivial addition -- WRITE_JOB's current payload only carries one
job's worth of x_base/w_base/result_addr/node_id, not 4). (3) Build a
real N=16 top-level (4x systolic_group.v) once (1) and (2) are real and
verified, with its own real P&R signoff. (4) Revisit the flat N=2/4/8/16
core-count scaling tests (deferred by the user's own explicit
reprioritization this session) once there's a real basis for comparing
flat vs. grouped scaling with real numbers from both.