rtl/npu.sv — Neural Processing Unit (v3)
A 16×16 weight-stationary, fully-parallel MAC array with software-selectable INT8/INT16 mixed precision and double-buffered ("ping-pong") weight banks for zero-bubble weight reuse across inferences. All 256 multiplications and the 16 row-sum adder trees are purely combinational; results land in an INT48 accumulator per row.
Where it fits
Instantiated once by npu_chiplet_top.sv as npu_inst, dispatched via the NPU UCIe link's npu_disp channel and drained through npu_result.
Ports
| Direction | Name | Width | Description |
|---|---|---|---|
| in | clk, rst_n |
1, 1 | Clock / reset |
| in | npu_valid |
1 | Dispatch instruction valid |
| out | npu_ready |
1 | 1 only in IDLE |
| in | npu_instruction |
32 | Encoded NPU instruction (opcode 0x6B) |
| out | result_valid |
1 | High during OUTPUT_ST |
| out | result_index |
4 | Current output lane (0–15) |
| out | result_data |
32 | acc[result_index][31:0] |
| in | result_ready |
1 | Backpressure — hold lane when low |
Instruction encoding (opcode 0x6B, custom-1)
[6:0] = 0x6B
[9:7] = cmd
[13:10] = row (LOAD_W)
[17:14] = col (LOAD_W, LOAD_A)
INT8 data : [25:18] (8-bit signed, sign-extended)
INT16 data : [31:18] (14-bit signed at bit 18 — starts above col[17:14] to avoid overlap,
sign-extended to 16; see BUG-001)
cmd[9:7] |
Mnemonic | Action |
|---|---|---|
000 |
LOAD_W | weight_bank[back_bank][row][col] ← data |
001 |
LOAD_A | activations[col] ← data |
010 |
COMPUTE | acc[r] = Σ_c W_front[r][c]·A[c] (overwrite, all 16 rows) |
011 |
COMPUTE_ACC | acc[r] += Σ_c W_front[r][c]·A[c] (tile-accumulate) |
100 |
SWAP | Flip active_bank (ping-pong, 1 cycle in IDLE) |
101 |
SET_PREC | instr[10]: 0→INT8, 1→INT16 |
111 |
NOP | idle |
Functionality
Weight banks: weight_bank[0:1][0:15][0:15], double-buffered. LOAD_W always writes the inactive ("back") bank; SWAP atomically flips active_bank, so a new weight set can be staged while the previous one is still being computed against, then swapped in with zero bubble cycles.
MAC array: mac_prod[r][c] = weight_bank[active_bank][r][c] * activations[c] — all 256 products combinational, every cycle. A 16-input adder tree per row (row_sum[r]) sums them to INT48 (sign-extended from the INT32 product).
Accumulators: acc[0:15], INT48. COMPUTE overwrites; COMPUTE_ACC adds row_sum to the existing value — this is what enables tiled GEMM for K > 16 (chain COMPUTE then N-1 COMPUTE_ACCs).
FSM: IDLE → {LOAD_W_ST, LOAD_A_ST, COMPUTE_ST, COMP_ACC_ST} → IDLE, or → OUTPUT_ST after a compute, streaming all 16 lanes with result_ready backpressure before returning to IDLE. SWAP and SET_PREC execute inline within IDLE (1 cycle, no state transition). Instruction fields are latched (lat_cmd/lat_row/lat_col/lat_data) in the IDLE default case before the state transitions, so the dispatcher can safely present a new instruction the very next cycle without a hazard on the just-departed one.
Example — 16×16 GEMV with tile reuse
# See tb/tb_server_dispatch_unit.py: NPUDriver / NPUMonitor / NPUScoreboard
W = [[r + 1 for _ in range(16)] for r in range(16)]
A = [1] * 16
await drv.load_weights(W) # 256x LOAD_W into back bank, then SWAP
await drv.load_activations(A) # 16x LOAD_A
await drv.compute() # 1x COMPUTE
results = await mon.collect() # 16 cycles, result_valid streaming
# results[r] == (r+1) * 16 for every row r
For K=32 (two 16-wide tiles): COMPUTE on tile 0, then load_weights/load_activations for tile 1 followed by COMPUTE_ACC — acc[r] ends up Σ_{c=0}^{31} W_full[r][c]·A_full[c]. See run_npu_tiled_gemm in tb_server_dispatch_unit.md.
Related
- npu.md (docs) — full FSM diagram, performance numbers, tiled-GEMM walkthrough
- pe.md — the conceptual systolic-cell equivalent of this file's inlined MAC logic
- npu_chiplet_top.md — the UCIe wrapper around this module
- BUG-001 — the INT16 field-overlap bug this encoding's bit-18 placement fixes
- reference.md — encoding quick-reference