Skip to content

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_ACCacc[r] ends up Σ_{c=0}^{31} W_full[r][c]·A_full[c]. See run_npu_tiled_gemm in tb_server_dispatch_unit.md.

  • 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