Skip to content

tb/tb_server_dispatch_unit.py — Regression Testbench

The main cocotb regression suite for system_top — 15 tests covering the NPU, GPU, and full RISC-V CPU programs. This is the Makefile default (MODULE ?= tb_server_dispatch_unit), and the file every other testbench in this repo imports shared helpers from (generate_clock, dram_model, reset_dut, send_instruction, NPUDriver/NPUMonitor/NPUScoreboard, etc.) rather than re-deriving them.

What it drives

system_top's top-level ports directly (clk/rst_n, net_valid/net_ready/net_instruction, npu_result_*, gpu_result_*, npu_dma_*, dram_*, halt) — see rtl/system_top.md for the full port list.

Shared infrastructure (reused by other testbenches)

Component Purpose
generate_clock(dut) 100 MHz clock (10ns period — Timer(5, unit="ns") each half)
dram_model(dut) 1-cycle-ack DRAM stub; dict-backed memory
reset_dut(dut, cycles=6) Asserts rst_n=0, zeroes all inputs and the CPU register file, releases reset
send_instruction(dut, raw) Drives one instruction through the net_valid/net_ready/net_instruction handshake — the single primitive every test (and every other tb file) dispatches instructions with
NPUDriver / NPUMonitor / NPUScoreboard Encode+send NPU instructions (load_weights handles the ping-pong SWAP protocol), collect the 16-lane result stream, and predict expected values in Python
GPUDriver / GPUMonitor / GPUScoreboard Same shape, for the 16-lane INT32 vector ALU
npu_instr_int8/npu_instr_int16/gpu_instr Raw instruction-word encoders matching rtl/npu.md and rtl/gpu.md's bit layouts

Test list

# Test Exercises
1 run_npu_gemv Basic 16×16 GEMV, W[r][c]=r+1, A=1, with ping-pong SWAP
2 run_npu_identity Identity weight matrix — output must equal A
3 run_npu_all_ones W=1, A=1 → every lane = 16
4 run_npu_random_signed Random signed INT8, Python scoreboard cross-check
5 run_npu_negative Negative weights — signed arithmetic coverage
6 run_npu_max_values W=127, A=127 → max INT8 product, no overflow
7 run_npu_zero_act A=0 → all outputs 0 regardless of weights
8 run_npu_tiled_gemm K=32 via two tiles, COMPUTE then COMPUTE_ACC
9 run_npu_int16_mode INT16 mixed-precision (SET_PREC, data at [31:18])
10 run_npu_ping_pong Ping-pong bank isolation across two independent inferences
11 run_gpu_vector_add GPU: A+B element-wise, 16 INT32 lanes
12 run_gpu_vector_sub GPU: A-B, including negative results
13 run_gpu_vector_bitwise GPU: AND then XOR, reusing loaded operands
14 run_gpu_random_reload GPU: random ADD, reload only vec_bvec_a must persist
15 run_riscv_program Full RV32IM program (parsed from an .asm fixture): MUL/DIV chains, branches, loads/stores, register assertions

How to run

# Default program (cpu_peak_tests.asm)
docker run --rm -e MODULE=tb_server_dispatch_unit -e ASM_TEST=cpu_peak_tests.asm \
    -v "$(pwd)":/usr/src/server_dispatch riscv_sim:latest

# A single test
COCOTB_TESTCASE=run_npu_gemv source scripts/run

# Without Docker
make results.xml

Example — the send_instruction primitive every test builds on

async def send_instruction(dut, raw):
    dut.net_valid.value       = 1
    dut.net_instruction.value = raw
    await RisingEdge(dut.clk)
    while not int(dut.net_ready.value):
        await RisingEdge(dut.clk)
    dut.net_valid.value = 0

Every instruction — CPU, GPU, or NPU-bound — goes through exactly this handshake; the noc_router decodes the opcode and routes it, so the testbench never needs to know or care which engine an instruction targets.