Skip to content

Verification

Verification runs either in a Docker container (Dockerfile, Icarus Verilog 11 + cocotb 2.0.1) or, on hosts without Docker/root, against a locally-extracted toolchain — source automation/toolchain_env.sh puts a self-contained iverilog 12 + gcc 15 + cocotb 2.0.1 (Python 3.14) on PATH without touching the system Python. See automation/README.md for how that toolchain is built.


Running Simulations

# Both suites (MOESI integration + regression) in one shot
./run_sims.command

# MOESI integration tests only
docker run --rm \
    -e MODULE=tb_moesi_integration \
    -e ASM_TEST=moesi_weight_test.asm \
    -v $(pwd):/usr/src/server_dispatch \
    riscv_sim:latest

# Regression suite only (default)
source scripts/run

# Without Docker (if iverilog + cocotb installed locally)
make results.xml

# Single test
COCOTB_TESTCASE=run_npu_gemv source scripts/run

Current Test Results

Regression Suite — tb/tb_server_dispatch_unit.py

TESTS=15 PASS=15 FAIL=0

# Test What it exercises Result
1 run_npu_gemv W[r][c]=r+1, A[c]=1 → O[r]=(r+1)×16 PASS
2 run_npu_identity W=I, A=[1..16] → O[r]=A[r] (identity) PASS
3 run_npu_all_ones W=1, A=1 → O[r]=16 (all lanes) PASS
4 run_npu_random_signed Random INT8 W and A, Python scoreboard cross-check PASS
5 run_npu_negative W[r][c]=-(r+1), A=1 → negative outputs PASS
6 run_npu_max_values W=127, A=127 → O[r]=258,064 (max INT8 product) PASS
7 run_npu_zero_act A=0 → O[r]=0 regardless of weights PASS
8 run_npu_tiled_gemm Two 16-wide tiles, COMPUTE then COMPUTE_ACC PASS
9 run_npu_int16 INT16 mode: SET_PREC + LOAD_W/A at [31:18] PASS
10 run_npu_ping_pong SWAP between front/back weight banks PASS
11 run_gpu_vector_add GPU: A+B element-wise over 16 INT32 lanes PASS
12 run_gpu_vector_sub GPU: A-B, including negative results PASS
13 run_gpu_vector_bitwise GPU: AND then XOR, reusing loaded operands PASS
14 run_gpu_random_reload GPU: random signed ADD, then reload only vec_b — vec_a must persist PASS
15 run_riscv_program Full RV32IM program: MUL, DIV, branches, loads/stores PASS

Not part of the standard suite but assembled/run manually via ASM_TEST=: tb/branch_regression.s (all four branch families, caught BUG-008/BUG-009) and tb/test_rv32m_control_flow_regression.s (JAL/BNE/MUL, caught BUG-010). See toolchain.md.

MOESI Integration Suite — tb/tb_moesi_integration.py

TESTS=3 PASS=1 FAIL=2BUG-006 open (regression of BUG-003's symptom; the BUG-003 fix is still present in RTL but the tests fail again)

# Test What it exercises Result
TC-MOESI-1 test_moesi_cpu_write_npu_read CPU writes dirty line → NPU DMA triggers M→S flush FAIL — see BUG-006
TC-MOESI-2 test_moesi_npu_invalidate_on_cpu_write NPU read → CPU overwrite → second NPU read sees new value FAIL — see BUG-006
TC-MOESI-3 test_moesi_clean_npu_dma NPU reads address CPU never touched — fast path, no flush PASS

Video Stream Workload Suite — tb/tb_video_stream_workload.py

TESTS=2 PASS=2 FAIL=0 — new suite, imports its driver/monitor/scoreboard classes from tb_server_dispatch_unit.py rather than duplicating them

Turns a reported real-world scenario (phone client's local video playback buffering at 256 kbps) into a CPU+NPU chiplet throughput/concurrency test. See tb/tb_video_stream_workload.md for the full bitrate → cycle-budget derivation.

# Test What it exercises Result
1 test_video_stream_sustained_throughput 16 stream chunks back-to-back (zero inter-arrival gap, worst-case burst), CPU bumping a counter between each; NPU results scoreboard-checked; cumulative latency checked against the 256 kbps-derived budget (547.8× headroom measured) PASS
2 test_cpu_progresses_while_npu_computes CPU dispatch continues, and completes, while the NPU chiplet is still computing/draining across its own UCIe link — proves chiplet-level concurrency, not shared-pipeline stalling PASS

Combined total across all three suites: TESTS=20, PASS=18, FAIL=2 (the 2 failures are BUG-006, tracked separately in UNSOLVED-001; unrelated to the other two suites).


Historical Test Failures (resolved)

Test Failure Root cause Fix
run_npu_int16 Wrong MAC results INT16 data at [31:16] overlapped col [17:14] BUG-001: moved data to [31:18]
run_riscv_program SimFailure (DIV programs) Duplicate HALT_WORD sent after load loop BUG-002: removed extra send
TC-MOESI-1, TC-MOESI-2 NPU DMA returned 0 Stale dram_ack from flush captured by ST_NPU_DRAM BUG-003: added ST_NPU_GAP
run_riscv_program SimFailure ("shut down prematurely") RTL $finish (20 cycles post-halt) raced cocotb's own end-of-regression shutdown BUG-004: removed the RTL $finish block
run_riscv_program t4 register assertion FAIL (expected 55, got 0) .asm fixture's expected value used wrong arithmetic (88560 % 205 is actually 0, not 55) BUG-005: corrected the expected value
new branch_regression test s3 off by 3 (12 instead of 15) BLT/BGE/BLTU/BGEU decoded with ALU_SUB instead of ALU_SLT/ALU_SLTU — no test had ever exercised those four branches BUG-008: select ALU op per branch funct3
new branch_regression test (after BUG-008 fix) still off by 3, same registers The instruction after a taken branch/JAL/JALR advanced into EX and executed once before being flushed BUG-009: bubble ID/EX when flush_if_id fires
test_rv32m_control_flow_regression (after BUG-008/009 fixes) hung, then x19 wrong (9 instead of 20) No halt sentinel; separately, hand-encoded mul's hex had funct7=0x00 (ran as ADD) BUG-010: regenerated the file from real assembly via scripts/riscv_asm.py

Known Open Issues

Test(s) Failure Status
TC-MOESI-1, TC-MOESI-2 NPU DMA returns 0 instead of the CPU-written value BUG-006 — open, not yet root-caused
— (cosmetic, no test affected) cpu_peak_tests.asm INT16 NPU block's hex doesn't match its comment BUG-011 — open, low severity

Testbench Architecture

tb_server_dispatch_unit.py — UVM-style agent structure

NPUDriver      → sends instruction sequences via npu_valid/ready handshake
NPUMonitor     → captures result_valid/index/data, assembles 16-lane output
NPUScoreboard  → Python reference model, predicts expected output
NPUCoverage    → tracks exercised scenarios, logged after tiled GEMM

Reference model (scoreboard):

def predict(W, A, acc_in=None):
    out = [sum(W[r][c] * A[c] for c in range(16)) for r in range(16)]
    if acc_in:
        out = [out[r] + acc_in[r] for r in range(16)]
    return out

Coverage log (tiled GEMM):

[COVERAGE] 8 scenario(s) exercised:
  all_ones, gemv_basic, identity_matrix, max_int8,
  negative_weights, random_signed, tiled_gemm_k32, zero_activations

GPU agent structure (same file, mirrors the NPU pattern):

GPUDriver      → sends LOAD_A/LOAD_B/COMPUTE via gpu_valid/ready handshake
GPUMonitor     → captures result_valid/index/data, assembles 16-lane output
GPUScoreboard  → Python reference model (ADD/SUB/AND/XOR), predicts expected output

tb_video_stream_workload.py — real-world scenario suite

imports NPUDriver/NPUMonitor/NPUScoreboard/send_instruction/reset_dut  → from tb_server_dispatch_unit
addi()               → local RV32I I-type ADDI encoder (CPU-side per-chunk bookkeeping)
DEADLINE_CYCLES      → 256 kbps stream rate, converted to a per-chunk cycle budget
cpu_bump_counter()   → dispatches one ADDI incrementing a CPU register per chunk

See tb/tb_video_stream_workload.md for the full writeup.

tb_moesi_integration.py — coherency-focused suite

Drives both the CPU instruction NoC port and the NPU DMA port simultaneously. Uses a persistent in-simulation DRAM model (dict) that retains written values across flush + read cycles — required for coherency verification.

Clock generator  → 100 MHz
DRAM model       → reactive, 1-cycle ack latency, persistent mem dict
load_cpu_program → streams instructions via net_valid/ready
wait_for_halt    → polls halt signal + 20-cycle drain
npu_dma_read_word → drives npu_dma_addr/read, waits for npu_dma_ack pulse

Assembly Test Files

File Instructions Purpose
tb/cpu_peak_tests.asm ~20 Default regression: MUL chain, SW/LW, branches
tb/extended_cpu_test.asm 104 Full RV32IM ISA: 44 register checks, 18 sections
tb/moesi_weight_test.asm 6 MOESI TC-MOESI-1: store 42→0x1000, 99→0x1004, halt
tb/branch_regression.s 19 All four branch families (BLT/BGE/BLTU/BGEU), regenerated via scripts/riscv_asm.py
tb/test_rv32m_control_flow_regression.s 9 JAL/BNE/MUL control flow, regenerated via scripts/riscv_asm.py

moesi_weight_test.asm detail

lui  a0, 1          # a0 = 0x1000
addi a1, x0, 42     # a1 = 42
addi a2, x0, 99     # a2 = 99
sw   a1, 0(a0)      # mem[0x1000] = 42  (dirty in L2 after miss+fill)
sw   a2, 4(a0)      # mem[0x1004] = 99  (same cacheline, word 1)
jal  x0, 0          # HALT

Adding a New Test

New CPU program (preferred: write assembly, not hex): 1. Write a .s file using real mnemonics/labels — see toolchain.md 2. python3 scripts/riscv_asm.py my_test.s -o tb/my_test.asm 3. ASM_TEST=my_test.asm source scripts/run

Regression test: 1. Add @cocotb.test() + async def run_my_test(dut): to tb/tb_server_dispatch_unit.py 2. Use NPUDriver, NPUScoreboard, NPUMonitor 3. source scripts/run — appears automatically

MOESI/coherency test: 1. Add @cocotb.test() to tb/tb_moesi_integration.py 2. Use load_cpu_program(), wait_for_halt(), npu_dma_read_word() 3. Run with MODULE=tb_moesi_integration

New scenario suite (its own MODULE, doesn't touch either existing file): 1. New file tb/tb_my_scenario.py, from tb_server_dispatch_unit import (...) for shared drivers/monitors/helpers 2. @cocotb.test() functions as usual 3. Run with make MODULE=tb_my_scenario (or the equivalent docker run -e MODULE=...) 4. Add a page under docs/tb/ (see tb/index.md) and link it into mkdocs.yml's nav

New issue found: Copy docs/issues/TEMPLATE.mddocs/issues/BUG-NNN.md, fill in, add row to docs/issues/README.md.


Source Reference

For file-level documentation (every port, internal FSM, and a worked example) of each RTL module and each testbench file, see: