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) andtb/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=2 — BUG-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.md → docs/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:
- rtl/index.md — one page per
rtl/*.svfile - tb/index.md — one page per
tb/*.pyfile