Skip to content

NPU v2 — Neural Processing Unit

The NPU v2 (rtl/npu.sv) is a 16×16 weight-stationary fully-parallel MAC array for INT8 GEMV and tiled GEMM. All 256 INT8 multiplications are computed combinationally every cycle; results are registered in a single clock edge.

Peak throughput: 0.512 TOPS @ 1 GHz


What It Does

Operation Instruction Result
Matrix-vector multiply COMPUTE acc[r] = Σ W[r][c] · A[c] for all 16 rows
Tile accumulate COMPUTE_ACC acc[r] += Σ W[r][c] · A[c] (adds to existing acc)
Load weight LOAD_W W[row][col] = data (one INT8 element)
Load activation LOAD_A A[col] = data (one INT8 element)
No-op NOP

Instruction Encoding (opcode 0x6B, RISC-V custom-1)

 Bit: 31    26 25    18 17   14 13   10 9   7 6       0
      +-------+---------+-------+-------+-----+---------+
      | unused|  data   |  col  |  row  | cmd | 0x6B    |
      | [31:26]| [25:18]|[17:14]|[13:10]|[9:7]| [6:0]  |
      +-------+---------+-------+-------+-----+---------+
cmd [9:7] Mnemonic Fields used Action
3'b000 LOAD_W row[13:10], col[17:14], data[25:18] W[row][col] ← data
3'b001 LOAD_A col[17:14], data[25:18] A[col] ← data
3'b010 COMPUTE acc ← W·A (overwrite)
3'b011 COMPUTE_ACC acc += W·A (accumulate)
3'b111 NOP idle

data is INT8 signed — sign-extended automatically by $signed(npu_instruction[25:18]).


Hardware Architecture

The Parallel MAC Array

256 independent multipliers, all running simultaneously every cycle:

// Stage 1: 256 multiplications (INT8 × INT8 → INT16)
logic signed [15:0] mac_prod [0:15][0:15];

generate
    for (gr = 0; gr < 16; gr++) begin : gen_mac_row
        for (gc = 0; gc < 16; gc++) begin : gen_mac_col
            assign mac_prod[gr][gc] =
                $signed(weights[gr][gc]) * $signed(activations[gc]);
        end
    end
endgenerate

Explicit $signed() casts prevent Icarus Verilog from treating the multiply as unsigned when both operands are logic signed arrays.

The Adder Tree

One 16-input adder tree per row sums the INT16 products to INT32:

// Stage 2: 16-input adder tree per row (→ 4-level balanced tree at synthesis)
logic signed [31:0] row_sum [0:15];

generate
    for (rr = 0; rr < 16; rr++) begin : gen_rowsum
        assign row_sum[rr] =
            mac_prod[rr][0] + mac_prod[rr][1] + ... + mac_prod[rr][15];
    end
endgenerate

Synthesisers map this to a 4-level balanced adder tree (log₂(16) = 4). Maximum sum = 16 × 127 × 127 = 258,064 — fits in INT32 with no overflow risk.

Accumulators

16 × INT32 accumulators (acc[0:15]) hold the running results. In COMPUTE they are overwritten; in COMPUTE_ACC the new row_sum is added to the existing value.


State Machine

         npu_valid + cmd=000
   ┌──────────────────────────► LOAD_W_ST ──────────────────────► IDLE
   │                                                           (1 cycle)
   │     npu_valid + cmd=001
   ├──────────────────────────► LOAD_A_ST ──────────────────────► IDLE
   │                                                           (1 cycle)
   │     npu_valid + cmd=010
   ├──────────────────────────► COMPUTE_ST ──────────────────────┐
   │                                    (acc = row_sum, 1 cycle)  │
IDLE                                                              ▼
   │     npu_valid + cmd=011                               OUTPUT_ST
   ├──────────────────────────► COMP_ACC_ST ─────────────────────┤
   │                                 (acc += row_sum, 1 cycle)   │
   │                                                             │
   │     npu_valid + cmd=111                                     │
   └──────────────────────────► IDLE (NOP)      out_cnt==15 + ready
                                                     └──────────► IDLE
State Cycles Action
IDLE Accept instruction, latch fields, assert npu_ready
LOAD_W_ST 1 weights[lat_row][lat_col] ← lat_data
LOAD_A_ST 1 activations[lat_col] ← lat_data
COMPUTE_ST 1 acc[r] ← row_sum[r] for all 16 rows
COMP_ACC_ST 1 acc[r] += row_sum[r] for all 16 rows
OUTPUT_ST 16 Stream acc[0..15] one per cycle with backpressure

npu_ready is asserted only in IDLE — the SDU cannot dispatch a new instruction while any other state is active.


Instruction Latch Hazard

The SDU can overwrite npu_instruction in the same clock cycle that the NPU accepts it and transitions out of IDLE. To prevent this:

In the IDLE default case of the datapath always_ff, all instruction fields are captured before the state change:

default: begin   // IDLE
    out_cnt <= 4'd0;
    if (npu_valid) begin
        lat_cmd  <= cmd;
        lat_row  <= row_idx;
        lat_col  <= col_idx;
        lat_data <= data_byte;
    end
end

LOAD_W_ST and LOAD_A_ST then use lat_row, lat_col, lat_data — not the live npu_instruction signals.


Tiled GEMM (K > 16)

The COMPUTE_ACC instruction enables arbitrary-depth GEMM by tiling along the K (inner) dimension.

Example: K=32 dot product (two 16-wide tiles)

# Tile 0 — establish baseline
for r in 0..15:
    for c in 0..15:
        LOAD_W  row=r, col=c, data=W0[r][c]
for c in 0..15:
    LOAD_A  col=c, data=A0[c]
COMPUTE          # acc = W0·A0

# Tile 1 — accumulate
for r in 0..15:
    for c in 0..15:
        LOAD_W  row=r, col=c, data=W1[r][c]
for c in 0..15:
    LOAD_A  col=c, data=A1[c]
COMPUTE_ACC      # acc += W1·A1

# Collect results — 16 INT32 lanes
# acc[r] = Σ_{c=0}^{31} W_full[r][c] · A_full[c]

For K=N×16, chain N-1 COMPUTE_ACC instructions after the first COMPUTE.


Result Bus

After COMPUTE or COMPUTE_ACC, the NPU streams 16 INT32 results one per cycle:

Signal Direction Description
result_valid out High during OUTPUT_ST
result_index [3:0] out Current lane index (0–15)
result_data [31:0] out acc[result_index] — INT32 signed
result_ready in Backpressure — hold result_index when low

result_index only increments when result_ready is asserted. The NPU stays in OUTPUT_ST until all 16 lanes have been acknowledged.


Processing Element — rtl/pe.sv

The architectural atom of the NPU. Every conceptual position in the 16×16 array corresponds to one PE:

              weight (stationary)
                   │
act_in ──────────► [×] ──► (+) ──► sum_out
                           ▲
                        sum_in
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || clear) begin
        act_out <= 8'sb0;
        sum_out <= 32'sb0;
    end else begin
        act_out <= act_in;                                // pass activation right
        sum_out <= sum_in + (weight_sext * act_in_sext); // accumulate
    end
end

pe.sv exists as a standalone module for future systolic tiling and FPGA DSP-slice mapping. The NPU v2 inlines equivalent combinational logic for maximum Icarus Verilog simulation throughput.


Performance Numbers

Metric Value
MACs per compute cycle 256 (all 16×16 simultaneously)
Cycles per GEMV (16×16) 256 LOAD_W + 16 LOAD_A + 1 COMPUTE + 16 OUTPUT = 289
Peak MAC throughput 256 MACs/cycle
Peak TOPS @ 1 GHz 0.512 TOPS
Accumulator width INT32 (no overflow for INT8 inputs)
Max safe accumulation 16 × 127 × 127 = 258,064 per lane
Tiled GEMM depth Unlimited (COMPUTE_ACC chaining)