From 6cff2c8a7c822d20a4a0e83c9a2153b5cfabb47f Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Sat, 5 Sep 2026 15:13:11 +0200 Subject: [PATCH] feat(v2): M8 PSRAM integration - real V1 backend shared across concurrent slots neural_multiprocessor.v wraps dataflow_core.v (M7, unmodified) around the real, unmodified V1 PSRAM backend chain (int8_memory_access -> memory_interface -> psram_controller), funneling N_SLOTS independent Memory Backend Interface ports through a new generic N-port arbiter (slot_mem_arbiter.v) inspired by (not copied from) V1's own mem_arbiter.v. Real concurrent-slot simulation immediately surfaced a genuine bug (ERR-0008): memory_manager/prefetch_engine's byte-level backend protocol is fire-and-forget (a single-cycle mem_req pulse with no accept handshake) - correct for M4's direct 1:1 connection, but a naive arbiter silently drops a pulse arriving while the shared bus is owned by another slot, hanging that slot forever. Fixed with a per-port pending-request latch, the same "queue, don't drop" idiom already used by memory_manager's own pf_pending register (ERR-0006). Verified (Verilator): 4/4 PASS with 2 slots genuinely contending for one real PSRAM port (444 cycles). No regression on M4's own testbench. Real synthesis + nextpnr-ecp5 P&R (no harness needed - real PSRAM pins keep the top-level at 157 pins): 0 problems, Fmax 142.45 MHz, PASS at 80MHz. Arbitration policy is fixed lowest-index priority, not fairness- balanced (DEC-0010) - consistent with every other "simplest correct policy first" scheduling choice in this roadmap, revisited only if M9's real measurement shows starvation matters. Logged: simulation/synthesis/timing/benchmark/decisions (DEC-0010)/ experiments (EXP-0009)/errors (ERR-0008)/development.log, ROADMAP.md updated. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_013xXuuRUWZScuo1DeYJxs3v --- hardware/v2/docs/ROADMAP.md | 13 +- hardware/v2/logs/benchmark.log | 14 ++ hardware/v2/logs/decisions.log | 61 +++++++ hardware/v2/logs/development.log | 24 +++ hardware/v2/logs/errors.log | 51 ++++++ hardware/v2/logs/experiments.log | 32 ++++ hardware/v2/logs/simulation.log | 20 +++ hardware/v2/logs/synthesis.log | 13 ++ hardware/v2/logs/timing.log | 5 + hardware/v2/rtl/neural_multiprocessor.v | 144 ++++++++++++++++ hardware/v2/rtl/slot_mem_arbiter.v | 166 ++++++++++++++++++ hardware/v2/sim/tb_neural_multiprocessor.v | 190 +++++++++++++++++++++ 12 files changed, 732 insertions(+), 1 deletion(-) create mode 100644 hardware/v2/rtl/neural_multiprocessor.v create mode 100644 hardware/v2/rtl/slot_mem_arbiter.v create mode 100644 hardware/v2/sim/tb_neural_multiprocessor.v diff --git a/hardware/v2/docs/ROADMAP.md b/hardware/v2/docs/ROADMAP.md index 324361f..b155270 100644 --- a/hardware/v2/docs/ROADMAP.md +++ b/hardware/v2/docs/ROADMAP.md @@ -46,7 +46,18 @@ reali, non solo scritto). (N_SLOTS=2), 133.19 MHz (N_SLOTS=4). Buffer M3 e arbitraggio PSRAM condiviso rimandati esplicitamente a M8 (`logs/decisions.log` DEC-0009). -- [ ] **M8 — PSRAM integration**, controller V1 non modificato, misura reale. +- [x] **M8 — PSRAM integration** (`neural_multiprocessor.v`, + `slot_mem_arbiter.v`), controller V1 riusato SENZA MODIFICHE, + condiviso tra N_SLOTS memory_manager concorrenti reali. Trovato e + risolto un bug RTL reale: il primo arbitro perdeva silenziosamente + una richiesta arrivata durante la contesa (protocollo byte-level + "fire-and-forget", mai esposto da M4 che collega un solo master + direttamente) — vedi `logs/errors.log` ERR-0008. Dopo il fix: 4/4 + test PASS (2 slot in vera contesa concorrente sulla stessa PSRAM + reale). Sintesi reale 0 problemi (nessun harness necessario — pin + reali PSRAM tengono il top-level a 157 pin). Fmax reale 142.45 + MHz. Politica di arbitraggio a priorità fissa, non ancora fair + (`logs/decisions.log` DEC-0010). - [ ] **M9 — Full benchmark**, tabella V1 vs V2 (§32 del mandato). - [ ] **M10 — Optimization**, solo sulla base dei dati raccolti in M1-M9. diff --git a/hardware/v2/logs/benchmark.log b/hardware/v2/logs/benchmark.log index eca1c6b..263e49f 100644 --- a/hardware/v2/logs/benchmark.log +++ b/hardware/v2/logs/benchmark.log @@ -103,3 +103,17 @@ uses 32/72 (44%), consistent with DEC-0005's finding that DSP, not LUT/FF, is the first resource to saturate as concurrency grows (M2's own N_PROCESSORS=8 measurement: 88%). BRAM=0 on both is expected -- M3's buffers are not wired into dataflow_core yet (DEC-0009). + +[2026-09-05] M8 Neural Multiprocessor top (real standalone synthesis + +P&R, no harness needed -- real PSRAM pins keep the bare top-level +pin count at 157, under the TRELLIS_IO budget) + +| Module (config) | Fmax (POST-P&R) | LUT4 | CCU2C | FF | DSP | BRAM | +|--------------------------------------|------------------|------|-------|------|-----|------| +| neural_multiprocessor (N_SLOTS=2) | 142.45 MHz | 3145 | 388 | 3659 | 16 | 0 | + +Compare to M7's dataflow_core alone (N_SLOTS=2): 165.15 MHz / LUT4=2127 +/ CCU2C=248 / FF=2505 / DSP=16. Adding the real V1 PSRAM chain + +slot_mem_arbiter costs ~1000 LUT4/140 CCU2C/1150 FF and drops Fmax by +~23 MHz (165.15 -> 142.45) -- both real, measured costs of real PSRAM +integration, not assumed. diff --git a/hardware/v2/logs/decisions.log b/hardware/v2/logs/decisions.log index 5a6a4c1..bc9b348 100644 --- a/hardware/v2/logs/decisions.log +++ b/hardware/v2/logs/decisions.log @@ -521,3 +521,64 @@ oversight. STATUS: ACCEPTED +DEC-0010 + +DATE: 2026-09-05 + +DECISION: +slot_mem_arbiter.v (M8) arbitrates dataflow_core's N_SLOTS independent +Memory Backend Interface ports down to the ONE real PSRAM port using +FIXED, lowest-port-index priority (not round-robin/least-loaded/ +fair-share), with a per-port single-entry pending-request latch (see +errors.log ERR-0008) so a fire-and-forget request pulse arriving +during contention is queued, never dropped. + +WHY: +Fixed lowest-index priority is the same "first-found, simplest +correct policy first" starting point already chosen for +neural_director's first-free slot scheduling (decisions.log DEC-0007) +and dependency_manager's first-ready dispatch -- consistent with this +whole roadmap's own pattern of shipping the simplest policy that is +provably correct, then revisiting fairness/throughput ONLY once real +measured data (M9) shows it actually matters for a real workload. +Under sustained heavy contention a low-index slot COULD in principle +starve a higher-index one (an unfair, but not incorrect, outcome); +this is an honestly-acknowledged limitation of a first cut, not an +oversight. The pending-latch discipline (ERR-0008) is not a policy +choice but a correctness requirement -- discovered empirically via +real concurrent-slot simulation, not designed in from the start (an +example of the mandate's own point, §22/§30: real measurement finds +real problems that a purely theoretical design would not). + +EVIDENCE: +hardware/v2/sim/tb_neural_multiprocessor.v -- 4/4 PASS with N_SLOTS=2 +genuinely concurrent slots (node0/node1, no dependencies, dispatched +back-to-back) contending for the one real PSRAM port through the real +V1 backend chain; both complete correctly and node2 (depends on both) +dispatches only once they genuinely do. No starvation observed in this +small a test (2 slots, one short job each) -- a real starvation +measurement would need a longer-running, higher-N_SLOTS workload, +deferred to M9's own benchmark. + +ALTERNATIVES: +1. Round-robin or least-recently-served fairness now. Rejected: no + measured evidence yet (M9 not run) that fixed-priority starvation + is a real problem for the graph workloads this system targets -- + adding fairness logic before a measured need is speculative + complexity, the same reasoning DEC-0007 already applied to + neural_director's own scheduling policy. +2. Give each slot its own dedicated PSRAM port (no arbitration at + all). Rejected: real PSRAM hardware has exactly one physical port + (the whole reason this module exists) -- not an option on real + hardware, only in simulation. + +RESULT: +slot_mem_arbiter.v as implemented: fixed lowest-index priority, +single-entry pending-request latch per port (mandatory for +correctness, not a policy choice). Fairness/throughput-aware +scheduling explicitly deferred to a future measurement-driven +decision, not missing by oversight. + +STATUS: +ACCEPTED + diff --git a/hardware/v2/logs/development.log b/hardware/v2/logs/development.log index badb8ad..020d276 100644 --- a/hardware/v2/logs/development.log +++ b/hardware/v2/logs/development.log @@ -206,3 +206,27 @@ decision: see decisions.log DEC-0009 (M3 buffers not wired in yet, no next_action: M8 -- PSRAM integration. Wire the real (unmodified) V1 PSRAM backend chain through dataflow_core end-to-end and measure/ design whatever N_SLOTS>1 arbitration real contention requires. + +[2026-09-05] M8 -- hardware/v2/rtl/neural_multiprocessor.v + +hardware/v2/rtl/slot_mem_arbiter.v +reason: roadmap M8 -- "Integrare il controller V1 senza modificarlo + inizialmente. Misurare il comportamento reale." dataflow_core.v (M7, + untouched) now shares the real, unmodified V1 PSRAM backend chain + (int8_memory_access -> memory_interface -> psram_controller) across + N_SLOTS genuinely concurrent memory_manager instances, via a new + generic N-port arbiter inspired by (not copied from) V1's own + mem_arbiter.v. +result: real concurrent-slot simulation immediately surfaced a real + bug (errors.log ERR-0008: the byte-level backend's fire-and-forget + request pulse gets silently dropped by a naive arbiter under + contention) -- fixed with a pending-request latch. After the fix: + 4/4 test PASS (2 concurrent slots genuinely contending for one real + PSRAM port, 444 cycles). Real synthesis: 0 problems, LUT4=3145/ + CCU2C=388/FF=3659/DSP=16, real Fmax 142.45 MHz (PASS at 80MHz, no + harness needed -- real PSRAM pins keep the top-level pin count at + 157). No regression on M4's own testbench. +errors: see errors.log ERR-0008 (real RTL bug, found and fixed). +decision: see decisions.log DEC-0010 (fixed-priority arbitration, no + fairness yet -- deferred pending real measured need). +next_action: M9 -- Full benchmark (§32): V1-vs-V2 comparison table, + every number labeled THEORETICAL/SIMULATED/SYNTHESIZED/POST-P&R. diff --git a/hardware/v2/logs/errors.log b/hardware/v2/logs/errors.log index a081256..75b090d 100644 --- a/hardware/v2/logs/errors.log +++ b/hardware/v2/logs/errors.log @@ -203,3 +203,54 @@ WORKAROUND: set the parameter on the TOP module being synthesized STATUS: WORKED AROUND. A build-script ordering detail, not a defect in dataflow_core.v or harness_dataflow_core.v themselves -- noted here so a future N_SLOTS sweep (M9/M10) does not re-trip over it. + +ERR-0008 (real RTL bug in the first draft of hardware/v2/rtl/slot_mem_arbiter.v, FOUND AND FIXED) +DATE: 2026-09-05 +MODULE: hardware/v2/rtl/slot_mem_arbiter.v (M8, new) +SYMPTOM: hardware/v2/sim/tb_neural_multiprocessor.v -- node0 (slot 0) + completes correctly (result=48), but node1 (slot 1, dispatched + concurrently to node0) never completes; its byte-level backend + request appears to simply vanish, and the slot hangs forever + (watchdog timeout at 20000 cycles, result stays 0). +ROOT CAUSE: memory_manager.v/prefetch_engine.v's own byte-level + backend protocol (mem_req/mem_wr/mem_addr/mem_wdata/mem_rdata/ + mem_ready) is FIRE-AND-FORGET: mem_req is asserted for exactly ONE + clock cycle per byte transaction, with no separate "request + accepted" acknowledgment -- only `mem_ready` (transaction + COMPLETION) exists. M4's own testbench (tb_memory_manager.v) never + exposed this because it connects exactly ONE memory_manager directly + to int8_memory_access, which is always idle and therefore always + able to accept that single pulse the instant it fires. The first + arbiter draft only granted a port while its s_req was LIVE that same + cycle -- if slot 1's one-cycle pulse arrived on a cycle where the + arbiter was already owned by slot 0, the pulse was gone the very + next cycle with no record of it ever having happened, and slot 1's + prefetch_engine sat in ST_READ_X/ST_READ_W waiting forever for a + mem_ready that could never arrive (its request never reached + int8_memory_access at all). +DIAGNOSIS METHOD: ran the M8 testbench, observed node0 (whichever slot + the Director happened to grant the shared bus to first) complete + while node1 (the other, concurrently-dispatched slot) hung; traced + the fire-and-forget nature of mem_req directly in + prefetch_engine.v's own state machine (`mem_req <= 1'b1;` appearing + only inside single-cycle state-transition branches, unconditionally + cleared to 0 every other cycle) -- confirmed the arbiter's naive + "grant only while req is live" logic could not possibly catch a + pulse arriving during contention. +FIX: every incoming s_req pulse is now LATCHED into a per-port + `pending` register (capturing wr/addr/wdata the same cycle), + regardless of arbiter state -- the same single-entry "queue, don't + drop the request" idiom already used by memory_manager's own + pf_pending register (ERR-0006 fix #1). Grants are drawn from + `pending`, never from a live s_req directly. This adds a uniform + minimum 1-cycle latency to every byte transaction (a real, honestly + measured cost of sharing one PSRAM port across N_SLOTS -- see + timing.log/benchmark.log EXP-0009), but never drops a request + regardless of contention. +VERIFICATION: hardware/v2/sim/tb_neural_multiprocessor.v -- 4/4 PASS + after the fix (444 cycles end-to-end, vs the buggy draft's 20000- + cycle watchdog timeout). hardware/v2/sim/tb_memory_manager.v (M4, + untouched) re-run unchanged -- still 3/3 PASS, confirming the fix is + entirely contained inside the new arbiter module. +STATUS: FIXED, verified end-to-end with the real (unmodified) V1 + PSRAM backend chain and real concurrent multi-slot contention. diff --git a/hardware/v2/logs/experiments.log b/hardware/v2/logs/experiments.log index 1f94646..363b4bc 100644 --- a/hardware/v2/logs/experiments.log +++ b/hardware/v2/logs/experiments.log @@ -401,3 +401,35 @@ next_action: M8 -- PSRAM integration. Wire the real (unmodified) V1 psram_controller) end-to-end through dataflow_core, and design/ measure whatever N_SLOTS>1 arbitration across ONE physical PSRAM port actually requires. + +[2026-09-05] EXP-0009 -- hardware/v2/rtl/neural_multiprocessor.v (M8, +real PSRAM integration) +test: hardware/v2/sim/tb_neural_multiprocessor.v -- same 3-node DAG as + EXP-0008, now routed through the REAL, UNMODIFIED V1 PSRAM backend + chain shared across N_SLOTS=2 genuinely concurrent memory_manager + instances via the new slot_mem_arbiter.v +simulator: Verilator 5.050 (--binary --timing) +PASS/FAIL: + SIMULATED: 4/4 PASS after fixing a real dropped-request bug in the + arbiter's first draft (errors.log ERR-0008) -- 444 cycles + end-to-end. hardware/v2/sim/tb_memory_manager.v (M4, untouched) + re-confirmed 3/3 PASS, no regression. + SYNTHESIZED (real standalone top-level, no harness needed -- 157 + port bits, real PSRAM pins keep it under the TRELLIS_IO budget): + 0 CHECK problems, LUT4=3145, CCU2C=388, TRELLIS_FF=3659, + MULT18X18D=16, DP16KD=0. + POST-P&R (real): Fmax=142.45 MHz -- PASS at 80MHz. +errors: one real RTL bug (errors.log ERR-0008) -- the first arbiter + draft silently dropped a request pulse arriving during contention; + fixed with a per-port pending-request latch, the same "queue, don't + drop" idiom already used by memory_manager's own pf_pending register + (ERR-0006). +decision: see decisions.log DEC-0010 (fixed lowest-index priority + arbitration, not fairness-balanced -- consistent with every other + scheduling policy chosen so far in this roadmap; revisit only if + M9's real measurement shows starvation actually matters). +next_action: M9 -- Full benchmark. Produce the V1-vs-V2 comparison + table mandated by §32 (Fmax, LUT, FF, DSP, BRAM, MAC/cycle, + cycles/neuron, neurons/s, stall %, memory/processor utilization, + effective MAC/s), each number labeled THEORETICAL/SIMULATED/ + SYNTHESIZED/POST-P&R per §30. diff --git a/hardware/v2/logs/simulation.log b/hardware/v2/logs/simulation.log index 8594c7e..fade6c7 100644 --- a/hardware/v2/logs/simulation.log +++ b/hardware/v2/logs/simulation.log @@ -75,3 +75,23 @@ PASS/FAIL: 4/4 PASS -- node0=48, node1=8 (both real neural_processor dependency-manager-to-director wake-up loop closes correctly end-to-end with real hardware in between, not just in isolation (M6's own testbench already proved the wake-up logic alone) + +[2026-09-05] EXP-0009 -- hardware/v2/sim/tb_neural_multiprocessor.v +test: same 3-node DAG as EXP-0008 (node0/node1 independent, node2 + depends on both), routed through neural_multiprocessor.v (M8): the + REAL, UNMODIFIED V1 PSRAM backend chain (int8_memory_access -> + memory_interface -> psram_controller -> psram_model) shared across + N_SLOTS=2 genuinely concurrent memory_manager instances via the new + slot_mem_arbiter.v -- node0 and node1 are registered back-to-back + with no dependencies, so both dispatch to their slots essentially + simultaneously and genuinely contend for the one real PSRAM port. +simulator: Verilator 5.050 (--binary --timing) +PASS/FAIL: 4/4 PASS after fixing a real dropped-request bug in the + first arbiter draft (errors.log ERR-0008) -- node0=48, node1=8 + (concurrent, real PSRAM, real arbitration), node2=40 dispatched only + after both genuinely completed. 444 cycles end-to-end (vs 20000-cycle + watchdog timeout with the buggy first draft, where node1's request + was silently dropped and its slot hung forever). +regression: hardware/v2/sim/tb_memory_manager.v (M4) re-run unchanged + (no M4 file touched) -- still 3/3 PASS, identical cycle counts + (446/166/728), confirming slot_mem_arbiter.v is purely additive. diff --git a/hardware/v2/logs/synthesis.log b/hardware/v2/logs/synthesis.log index c5724c0..d3617a1 100644 --- a/hardware/v2/logs/synthesis.log +++ b/hardware/v2/logs/synthesis.log @@ -63,3 +63,16 @@ CHECK: 0 problems on both configs (same 32 benign "multiple conflicting deliberately NOT instantiated inside dataflow_core yet (decisions.log DEC-0009). DSP scales exactly 8/slot (matches P_IN=8, consistent with every prior per-processor DSP measurement since M1/M2). + +[2026-09-05] EXP-0009 -- neural_multiprocessor (M8, N_SLOTS=2, real +standalone top-level synthesis -- no timing harness needed: real PSRAM +pins replace dataflow_core's wide per-slot arrays, total 157 port bits, +well under the LFE5U-45F-8BG381's ~245 TRELLIS_IO budget) +LUT4=3145 CCU2C=388 TRELLIS_FF=3659 MULT18X18D=16 DP16KD=0 +TRELLIS_DPR16X4=45 (small LUT-based distributed RAM, inferred from + neural_director's shallow QUEUE_DEPTH-entry job queue -- not BRAM, + same primitive class already seen in M7's own harness stat) +$_TBUF_=16 (tri-state buffers for the bidirectional psram_dq bus, + from V1's own unmodified psram_controller.v) +CHECK: 0 problems (same 32 benign "multiple conflicting drivers for + ...neural_processor.\gi" warnings documented since EXP-0001). diff --git a/hardware/v2/logs/timing.log b/hardware/v2/logs/timing.log index ec89de5..2677cad 100644 --- a/hardware/v2/logs/timing.log +++ b/hardware/v2/logs/timing.log @@ -65,3 +65,8 @@ Fmax drops as N_SLOTS grows (more concurrent memory_manager+ neural_processor instances competing for the same routing fabric around the shared neural_director/dependency_manager hub) -- both configs still clear the 80MHz target with real margin. + +[2026-09-05] EXP-0009 -- neural_multiprocessor (M8, N_SLOTS=2, real +standalone synthesis, no harness needed), real nextpnr-ecp5 --45k +--package CABGA381 --speed 8 --freq 80 --lpf-allow-unconstrained +Fmax: 142.45 MHz -- PASS at 80MHz (real place&route measurement) diff --git a/hardware/v2/rtl/neural_multiprocessor.v b/hardware/v2/rtl/neural_multiprocessor.v new file mode 100644 index 0000000..996d7b8 --- /dev/null +++ b/hardware/v2/rtl/neural_multiprocessor.v @@ -0,0 +1,144 @@ +`timescale 1ns/1ps + +// ================================================================ +// FPGA-Neural V2 -- Neural Multiprocessor top (M8, docs/v2-description.md +// §15/§16: "Integrare il controller V1 senza modificarlo inizialmente. +// Misurare il comportamento reale.") +// +// The real, hardware-facing top-level: dataflow_core.v (M7) with its +// N_SLOTS independent Memory Backend Interface ports funneled through +// a new generic arbiter (slot_mem_arbiter.v, M8) down to the REAL, +// UNMODIFIED hardware/v1 PSRAM backend chain -- +// int8_memory_access -> memory_interface -> psram_controller +// -- exactly the chain hardware/v2/sim/tb_memory_manager.v (M4) +// already proved correct for ONE memory_manager port. This module is +// the first point M3 (per DEC-0009) and M2 (per DEC-0006) BOTH +// deferred to: N_SLOTS memory_manager instances genuinely sharing one +// physical PSRAM port. +// +// dataflow_core.v itself is NOT modified -- its per-slot interface +// (DEC-0009) is exactly what makes it pluggable into an arbiter here +// without touching M7's own file. +// ================================================================ + +module neural_multiprocessor #( + parameter DATA_WIDTH = 8, + parameter P_IN = 8, + parameter ACC_WIDTH = 32, + parameter ADDR_WIDTH = 23, + parameter N_SLOTS = 4, + parameter N_NODES = 16, + parameter MAX_DEPS = 4, + parameter QUEUE_DEPTH = 8, + parameter PSRAM_DATA_WIDTH = 16, + parameter CLK_FREQ_MHZ = 80 +)( + input wire clk, + input wire rst, + + // ---- node registration (host / graph loader -> Dependency Manager) ---- + input wire reg_valid, + output wire reg_ready, + input wire [$clog2(N_NODES)-1:0] reg_node_id, + input wire [$clog2(MAX_DEPS+1)-1:0] reg_required, + input wire [MAX_DEPS*$clog2(N_NODES)-1:0] reg_producer_ids, + input wire [ADDR_WIDTH-1:0] reg_x_base, + input wire [ADDR_WIDTH-1:0] reg_w_base, + input wire [15:0] reg_n_tiles, + input wire [ADDR_WIDTH-1:0] reg_result_addr, + + // ---- real PSRAM pins (hardware/v1/rtl/psram_controller.v's own + // contract, unmodified) ---- + output wire [ADDR_WIDTH-1:0] psram_a, + inout wire [PSRAM_DATA_WIDTH-1:0] psram_dq, + output wire psram_ce_n, + output wire psram_oe_n, + output wire psram_we_n, + output wire psram_lb_n, + output wire psram_ub_n, + output wire psram_zz_n +); + + // ---- dataflow_core (M7, unmodified) ---- + wire [N_SLOTS-1:0] slot_mem_req, slot_mem_wr; + wire [ADDR_WIDTH*N_SLOTS-1:0] slot_mem_addr; + wire signed [8*N_SLOTS-1:0] slot_mem_wdata, slot_mem_rdata; + wire [N_SLOTS-1:0] slot_mem_ready; + + dataflow_core #( + .DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH), .ADDR_WIDTH(ADDR_WIDTH), + .N_SLOTS(N_SLOTS), .N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS), .QUEUE_DEPTH(QUEUE_DEPTH) + ) u_dataflow_core ( + .clk(clk), .rst(rst), + .reg_valid(reg_valid), .reg_ready(reg_ready), .reg_node_id(reg_node_id), + .reg_required(reg_required), .reg_producer_ids(reg_producer_ids), + .reg_x_base(reg_x_base), .reg_w_base(reg_w_base), .reg_n_tiles(reg_n_tiles), + .reg_result_addr(reg_result_addr), + .slot_mem_req(slot_mem_req), .slot_mem_wr(slot_mem_wr), .slot_mem_addr(slot_mem_addr), + .slot_mem_wdata(slot_mem_wdata), .slot_mem_rdata(slot_mem_rdata), .slot_mem_ready(slot_mem_ready) + ); + + // ---- N_SLOTS -> 1 arbiter (M8, new) ---- + wire arb_m_req, arb_m_wr; + wire [ADDR_WIDTH-1:0] arb_m_addr; + wire signed [7:0] arb_m_wdata; + wire signed [7:0] arb_m_rdata; + wire arb_m_ready; + + slot_mem_arbiter #( + .ADDR_WIDTH(ADDR_WIDTH), .N_PORTS(N_SLOTS) + ) u_arbiter ( + .clk(clk), .rst(rst), + .s_req(slot_mem_req), .s_wr(slot_mem_wr), .s_addr(slot_mem_addr), + .s_wdata(slot_mem_wdata), .s_rdata(slot_mem_rdata), .s_ready(slot_mem_ready), + .m_req(arb_m_req), .m_wr(arb_m_wr), .m_addr(arb_m_addr), .m_wdata(arb_m_wdata), + .m_rdata(arb_m_rdata), .m_ready(arb_m_ready) + ); + + // ---- real, unmodified V1 PSRAM backend chain ---- + wire if_mem_req, if_mem_wr; + wire [ADDR_WIDTH-1:0] if_mem_addr; + wire [PSRAM_DATA_WIDTH-1:0] if_mem_wdata; + wire if_mem_lb_n, if_mem_ub_n; + wire [PSRAM_DATA_WIDTH-1:0] if_mem_rdata; + wire if_mem_ready; + + int8_memory_access #(.ADDR_WIDTH(ADDR_WIDTH)) u_int8 ( + .clk(clk), .rst(rst), + .req(arb_m_req), .wr(arb_m_wr), .addr(arb_m_addr), .wdata(arb_m_wdata), + .rdata(arb_m_rdata), .ready(arb_m_ready), + .mem_req(if_mem_req), .mem_wr(if_mem_wr), .mem_addr(if_mem_addr), .mem_wdata(if_mem_wdata), + .mem_lb_n(if_mem_lb_n), .mem_ub_n(if_mem_ub_n), + .mem_rdata(if_mem_rdata), .mem_ready(if_mem_ready) + ); + + wire pc_mem_req, pc_mem_wr; + wire [ADDR_WIDTH-1:0] pc_mem_addr; + wire [PSRAM_DATA_WIDTH-1:0] pc_mem_wdata; + wire pc_mem_lb_n, pc_mem_ub_n; + wire [PSRAM_DATA_WIDTH-1:0] pc_mem_rdata; + wire pc_mem_ready; + + memory_interface #(.ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(PSRAM_DATA_WIDTH)) u_memif ( + .clk(clk), .rst(rst), + .req(if_mem_req), .wr(if_mem_wr), .addr(if_mem_addr), .wdata(if_mem_wdata), + .lb_n(if_mem_lb_n), .ub_n(if_mem_ub_n), + .rdata(if_mem_rdata), .ready(if_mem_ready), + .mem_req(pc_mem_req), .mem_wr(pc_mem_wr), .mem_addr(pc_mem_addr), .mem_wdata(pc_mem_wdata), + .mem_lb_n(pc_mem_lb_n), .mem_ub_n(pc_mem_ub_n), + .mem_rdata(pc_mem_rdata), .mem_ready(pc_mem_ready) + ); + + psram_controller #( + .ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(PSRAM_DATA_WIDTH), .CLK_FREQ_MHZ(CLK_FREQ_MHZ) + ) u_psram_ctrl ( + .clk(clk), .rst(rst), + .mem_req(pc_mem_req), .mem_wr(pc_mem_wr), .mem_addr(pc_mem_addr), .mem_wdata(pc_mem_wdata), + .mem_lb_n(pc_mem_lb_n), .mem_ub_n(pc_mem_ub_n), + .mem_rdata(pc_mem_rdata), .mem_ready(pc_mem_ready), + .psram_a(psram_a), .psram_dq(psram_dq), + .psram_ce_n(psram_ce_n), .psram_oe_n(psram_oe_n), .psram_we_n(psram_we_n), + .psram_lb_n(psram_lb_n), .psram_ub_n(psram_ub_n), .psram_zz_n(psram_zz_n) + ); + +endmodule diff --git a/hardware/v2/rtl/slot_mem_arbiter.v b/hardware/v2/rtl/slot_mem_arbiter.v new file mode 100644 index 0000000..d0aeb56 --- /dev/null +++ b/hardware/v2/rtl/slot_mem_arbiter.v @@ -0,0 +1,166 @@ +`timescale 1ns/1ps + +// ================================================================ +// FPGA-Neural V2 -- Slot Memory Arbiter (M8, docs/v2-description.md §15) +// +// Generic N_PORTS-way arbiter for dataflow_core.v's per-slot Memory +// Backend Interface ports (docs/v2-description.md §15's "Memory +// Manager -> Memory Backend Interface -> PSRAM Controller" layering), +// funneling N_SLOTS independent memory_manager backend ports down to +// the ONE physical PSRAM port a real chip actually has. +// +// Inspired by (NOT copied from -- see hardware/v2/logs/decisions.log +// DEC-0006's own note) hardware/v1/rtl/mem_arbiter.v: same +// single-owner-until-ready-pulse discipline (a port, once granted, +// holds the shared master port until ITS OWN transaction's m_ready +// pulse, then releases -- no queuing/pipelining needed, since every +// requester already issues a clean one-cycle req pulse matching +// int8_memory_access's own contract). Generalized from V1's fixed +// 4 named ports (A/B/C/D) to a parametric N_PORTS array, since +// dataflow_core.v's N_SLOTS is itself a parameter. +// +// Priority: fixed, lowest port index wins on a cycle where more than +// one port requests simultaneously while the arbiter is idle -- same +// "first-found, lowest index" convention already used by +// neural_director's free-slot scan and dependency_manager's +// first-ready scan (not fairness-balanced; see decisions.log DEC-0010 +// for why that is an acceptable starting point, same rationale as +// neural_director's own "first-free, not load-balanced" choice). +// +// IMPORTANT (found via real concurrent-slot simulation, see +// hardware/v2/logs/errors.log ERR-0008): each port's own s_req is a +// FIRE-AND-FORGET single-cycle pulse (prefetch_engine.v/ +// memory_manager.v's own byte-level backend protocol -- M4 verified +// it only against a DIRECT 1:1 connection to int8_memory_access, +// which is always free to accept it since there is exactly one +// requester). A naive "grant only while req is live" arbiter silently +// DROPS a pulse that arrives while the shared bus is owned by another +// port, hanging that slot's prefetch/writeback forever. Every +// incoming s_req is therefore LATCHED into a per-port `pending` +// register (capturing wr/addr/wdata the same cycle) regardless of +// arbiter state -- the same single-entry "queue, don't drop the +// request" idiom already used by memory_manager's own pf_pending +// register (ERR-0006 fix #1). Grants are drawn from `pending`, never +// from a live s_req directly, which adds a uniform minimum 1-cycle +// latency to every byte transaction (a real, measured cost of sharing +// one PSRAM port -- see timing.log/benchmark.log EXP-0009) but never +// drops a request. +// ================================================================ + +module slot_mem_arbiter #( + parameter ADDR_WIDTH = 23, + parameter N_PORTS = 4 +)( + input wire clk, + input wire rst, + + // ---- N_PORTS requester side (one per dataflow_core slot) ---- + input wire [N_PORTS-1:0] s_req, + input wire [N_PORTS-1:0] s_wr, + input wire [ADDR_WIDTH*N_PORTS-1:0] s_addr, + input wire signed [8*N_PORTS-1:0] s_wdata, + output reg signed [8*N_PORTS-1:0] s_rdata, + output reg [N_PORTS-1:0] s_ready, + + // ---- single shared master port (-> int8_memory_access) ---- + output reg m_req, + output reg m_wr, + output reg [ADDR_WIDTH-1:0] m_addr, + output reg signed [7:0] m_wdata, + input wire signed [7:0] m_rdata, + input wire m_ready +); + + localparam PIDXW = $clog2(N_PORTS+1); + localparam OWNER_NONE = {PIDXW{1'b0}}; // 0 = no owner; port i owned = i+1 + + reg [PIDXW-1:0] owner; + + // Per-port pending-request latch (see file header/ERR-0008): every + // s_req pulse is captured here, regardless of arbiter state, so it + // is never silently dropped while the bus is owned by another port. + reg [N_PORTS-1:0] pending; + reg [ADDR_WIDTH*N_PORTS-1:0] pending_addr; + reg signed [8*N_PORTS-1:0] pending_wdata; + reg [N_PORTS-1:0] pending_wr; + + // Fixed lowest-index-wins priority scan over PENDING requests (not + // raw s_req -- see file header). + reg [PIDXW-1:0] grant_idx; + reg any_pending; + integer ri; + always @(*) begin + grant_idx = {PIDXW{1'b0}}; + any_pending = 1'b0; + for (ri = N_PORTS-1; ri >= 0; ri = ri - 1) begin + if (pending[ri]) begin + grant_idx = ri[PIDXW-1:0]; + any_pending = 1'b1; + end + end + end + + integer pi; + + always @(posedge clk) begin + if (rst) begin + owner <= OWNER_NONE; + pending <= {N_PORTS{1'b0}}; + pending_addr <= {(ADDR_WIDTH*N_PORTS){1'b0}}; + pending_wdata <= {(8*N_PORTS){1'b0}}; + pending_wr <= {N_PORTS{1'b0}}; + m_req <= 1'b0; + m_wr <= 1'b0; + m_addr <= {ADDR_WIDTH{1'b0}}; + m_wdata <= 8'sd0; + s_rdata <= {(8*N_PORTS){1'b0}}; + s_ready <= {N_PORTS{1'b0}}; + end else begin + m_req <= 1'b0; + s_ready <= {N_PORTS{1'b0}}; + + // Latch every incoming request pulse. Safe against a + // same-cycle collision with the grant-clear write below: + // a port only ever becomes grant_idx while its OWN pending + // bit is already 1 (latched on an earlier cycle), and its + // requester (memory_manager/prefetch_engine) never issues + // a NEW s_req for that port until THIS transaction's + // s_ready arrives -- so s_req[grant_idx] is guaranteed low + // the cycle it is granted. + for (pi = 0; pi < N_PORTS; pi = pi + 1) begin + if (s_req[pi]) begin + pending[pi] <= 1'b1; + pending_wr[pi] <= s_wr[pi]; + pending_addr[pi*ADDR_WIDTH +: ADDR_WIDTH] <= s_addr[pi*ADDR_WIDTH +: ADDR_WIDTH]; + pending_wdata[pi*8 +: 8] <= s_wdata[pi*8 +: 8]; + end + end + + if (owner == OWNER_NONE) begin + if (any_pending) begin + owner <= grant_idx + 1'b1; + m_req <= 1'b1; + m_wr <= pending_wr[grant_idx]; + m_addr <= pending_addr[grant_idx*ADDR_WIDTH +: ADDR_WIDTH]; + m_wdata <= pending_wdata[grant_idx*8 +: 8]; + pending[grant_idx] <= 1'b0; + end + end else begin + if (m_ready) begin + // owner is (port_index+1); vectorized single-write + // so exactly one s_rdata/s_ready lane updates (no + // per-bit loop last-write-wins hazard -- same class + // of bug already hit/fixed at ERR-0006/M2/M6). + for (pi = 0; pi < N_PORTS; pi = pi + 1) begin + if (owner == pi[PIDXW-1:0] + 1'b1) begin + s_rdata[pi*8 +: 8] <= m_rdata; + s_ready[pi] <= 1'b1; + end + end + owner <= OWNER_NONE; + end + end + end + end + +endmodule diff --git a/hardware/v2/sim/tb_neural_multiprocessor.v b/hardware/v2/sim/tb_neural_multiprocessor.v new file mode 100644 index 0000000..651e8ad --- /dev/null +++ b/hardware/v2/sim/tb_neural_multiprocessor.v @@ -0,0 +1,190 @@ +`timescale 1ns/1ps + +// ============================================================ +// M8 testbench (docs/v2-description.md §15/§16/§20): +// neural_multiprocessor.v -- dataflow_core.v (M7, unmodified) sharing +// the REAL, UNMODIFIED V1 PSRAM backend chain (int8_memory_access -> +// memory_interface -> psram_controller -> real psram_model) across +// N_SLOTS=2 concurrent memory_manager instances for the first time, +// through the new slot_mem_arbiter.v (M8). +// +// Same DAG shape as tb_dataflow_core.v (M7's own test), REPLACING the +// per-slot behavioral memories with the single real PSRAM chain -- +// this is the actual M8 measurement: does real arbitration/contention +// across genuinely-concurrent slots work correctly against real PSRAM +// timing (not an idealized 2-cycle behavioral model)? +// +// node0 (x=2,w=3,8in -> acc=48) --+ +// +--> node2 (x=1,w=5,8in -> acc=40) +// node1 (x=1,w=1,8in -> acc=8) --+ +// +// node0 and node1 are registered back-to-back with NO dependencies, +// so both are dispatched to the two available slots essentially +// simultaneously -- both memory_manager instances will genuinely +// contend for the one real PSRAM port at the same time, exercising +// slot_mem_arbiter.v's arbitration for real (not just in isolation). +// +// Verified with Verilator (decisions.log DEC-0004). +// ============================================================ + +module tb; + + localparam ADDR_WIDTH = 23; + localparam DATA_WIDTH = 8; + localparam P_IN = 8; + localparam ACC_WIDTH = 32; + localparam N_SLOTS = 2; + localparam N_NODES = 8; + localparam MAX_DEPS = 4; + localparam QUEUE_DEPTH = 4; + localparam NODE_IDW = $clog2(N_NODES); + localparam PSRAM_DATA_WIDTH = 16; + localparam CLK_PERIOD = 12.5; // 80 MHz, matches psram_controller's CLK_FREQ_MHZ + + reg clk, rst; + initial begin clk = 1'b0; forever #(CLK_PERIOD/2.0) clk = ~clk; end + + reg reg_valid; + wire reg_ready; + reg [NODE_IDW-1:0] reg_node_id; + reg [$clog2(MAX_DEPS+1)-1:0] reg_required; + reg [MAX_DEPS*NODE_IDW-1:0] reg_producer_ids; + reg [ADDR_WIDTH-1:0] reg_x_base, reg_w_base, reg_result_addr; + reg [15:0] reg_n_tiles; + + wire [ADDR_WIDTH-1:0] psram_a; + wire [PSRAM_DATA_WIDTH-1:0] psram_dq; + wire psram_ce_n, psram_oe_n, psram_we_n, psram_lb_n, psram_ub_n, psram_zz_n; + + neural_multiprocessor #( + .DATA_WIDTH(DATA_WIDTH), .P_IN(P_IN), .ACC_WIDTH(ACC_WIDTH), .ADDR_WIDTH(ADDR_WIDTH), + .N_SLOTS(N_SLOTS), .N_NODES(N_NODES), .MAX_DEPS(MAX_DEPS), .QUEUE_DEPTH(QUEUE_DEPTH), + .PSRAM_DATA_WIDTH(PSRAM_DATA_WIDTH), .CLK_FREQ_MHZ(80) + ) u_nmp ( + .clk(clk), .rst(rst), + .reg_valid(reg_valid), .reg_ready(reg_ready), .reg_node_id(reg_node_id), + .reg_required(reg_required), .reg_producer_ids(reg_producer_ids), + .reg_x_base(reg_x_base), .reg_w_base(reg_w_base), .reg_n_tiles(reg_n_tiles), + .reg_result_addr(reg_result_addr), + .psram_a(psram_a), .psram_dq(psram_dq), + .psram_ce_n(psram_ce_n), .psram_oe_n(psram_oe_n), .psram_we_n(psram_we_n), + .psram_lb_n(psram_lb_n), .psram_ub_n(psram_ub_n), .psram_zz_n(psram_zz_n) + ); + + psram_model #(.ADDR_WIDTH(ADDR_WIDTH), .DATA_WIDTH(PSRAM_DATA_WIDTH), .DEPTH(16384)) u_psram ( + .clk(clk), .a(psram_a), .dq(psram_dq), + .ce_n(psram_ce_n), .oe_n(psram_oe_n), .we_n(psram_we_n), + .lb_n(psram_lb_n), .ub_n(psram_ub_n), .zz_n(psram_zz_n) + ); + + task automatic poke_byte(input [ADDR_WIDTH-1:0] byte_addr, input [7:0] val); + reg [ADDR_WIDTH-2:0] word_addr; + begin + word_addr = byte_addr[ADDR_WIDTH-1:1]; + if (byte_addr[0] == 1'b0) + u_psram.mem[word_addr][7:0] = val; + else + u_psram.mem[word_addr][15:8] = val; + end + endtask + + function automatic signed [7:0] peek_byte(input [ADDR_WIDTH-1:0] byte_addr); + reg [ADDR_WIDTH-2:0] word_addr; + begin + word_addr = byte_addr[ADDR_WIDTH-1:1]; + peek_byte = (byte_addr[0] == 1'b0) ? u_psram.mem[word_addr][7:0] : u_psram.mem[word_addr][15:8]; + end + endfunction + + task automatic register_node( + input [NODE_IDW-1:0] nid, + input [$clog2(MAX_DEPS+1)-1:0] required, + input [NODE_IDW-1:0] p0, input [NODE_IDW-1:0] p1, + input [ADDR_WIDTH-1:0] xb, input [ADDR_WIDTH-1:0] wb, + input [15:0] nt, input [ADDR_WIDTH-1:0] resaddr + ); + begin + @(posedge clk); + reg_node_id = nid; + reg_required = required; + reg_producer_ids = {NODE_IDW*MAX_DEPS{1'b0}}; + reg_producer_ids[0*NODE_IDW +: NODE_IDW] = p0; + reg_producer_ids[1*NODE_IDW +: NODE_IDW] = p1; + reg_x_base = xb; reg_w_base = wb; reg_n_tiles = nt; reg_result_addr = resaddr; + reg_valid = 1'b1; + while (!reg_ready) @(posedge clk); + @(posedge clk); + reg_valid = 1'b0; + end + endtask + + integer errors, tests; + integer i, wd; + + initial begin + errors = 0; tests = 0; + rst = 1; reg_valid = 0; reg_node_id = 0; reg_required = 0; reg_producer_ids = 0; + reg_x_base = 0; reg_w_base = 0; reg_n_tiles = 0; reg_result_addr = 0; + repeat(5) @(posedge clk); + rst = 0; + + // Real PSRAM power-up sequence (~150us @ 80MHz) -- same + // requirement/convention as tb_memory_manager.v (M4). + wait (u_nmp.u_psram_ctrl.state == u_nmp.u_psram_ctrl.STATE_IDLE); + @(posedge clk); + + for (i = 0; i < 8; i = i + 1) begin + poke_byte(23'h10+i, 8'sd2); poke_byte(23'h20+i, 8'sd3); // node0: x=2,w=3 + poke_byte(23'h30+i, 8'sd1); poke_byte(23'h40+i, 8'sd1); // node1: x=1,w=1 + poke_byte(23'h50+i, 8'sd1); poke_byte(23'h60+i, 8'sd5); // node2: x=1,w=5 + end + poke_byte(23'h70, 8'sd0); poke_byte(23'h71, 8'sd0); poke_byte(23'h72, 8'sd0); + + // node0, node1: no dependencies -- dispatched back-to-back, so + // BOTH slots start genuinely concurrent PSRAM traffic through + // the shared arbiter at essentially the same time. + register_node(0, 0, 0, 0, 23'h10, 23'h20, 16'd1, 23'h70); + register_node(1, 0, 0, 0, 23'h30, 23'h40, 16'd1, 23'h71); + register_node(2, 2, 0, 1, 23'h50, 23'h60, 16'd1, 23'h72); + + tests = tests + 1; + wd = 0; + while ((peek_byte(23'h70)==0 || peek_byte(23'h71)==0) && wd < 20000) begin + if (peek_byte(23'h72) !== 8'sd0) begin + $display("FAIL: node2 completed before both node0 and node1 finished"); + errors = errors + 1; + end + @(posedge clk); wd = wd + 1; + end + $display("PASS: node2 did not complete before both its dependencies did (checked every cycle up to wd=%0d)", wd); + + wd = 0; + while (peek_byte(23'h72)==0 && wd < 20000) begin @(posedge clk); wd = wd + 1; end + repeat(10) @(posedge clk); + + tests = tests + 3; + if (peek_byte(23'h70) !== 8'sd48) begin + $display("FAIL node0: result=%0d expected 48", peek_byte(23'h70)); + errors = errors + 1; + end else $display("PASS node0: result=48 via real PSRAM + shared arbiter"); + + if (peek_byte(23'h71) !== 8'sd8) begin + $display("FAIL node1: result=%0d expected 8", peek_byte(23'h71)); + errors = errors + 1; + end else $display("PASS node1: result=8 via real PSRAM + shared arbiter (concurrent with node0)"); + + if (peek_byte(23'h72) !== 8'sd40) begin + $display("FAIL node2: result=%0d expected 40", peek_byte(23'h72)); + errors = errors + 1; + end else $display("PASS node2: result=40, dispatched only after BOTH producers genuinely completed, real PSRAM end-to-end"); + + $display("========================================"); + if (errors == 0) + $display("ALL %0d TESTS PASSED (neural_multiprocessor, real V1 PSRAM chain shared across N_SLOTS=%0d via slot_mem_arbiter)", tests, N_SLOTS); + else + $display("FAILED: %0d/%0d test(s) had errors -- see messages above", errors, tests); + $display("========================================"); + $finish; + end + +endmodule