Project Progression
How Duck Server evolved from a basic heterogeneous SoC to a coherent multi-master streaming chip.
Version History
v1 — Initial SoC (baseline)
Goal: Prove the heterogeneous dispatch concept works end-to-end.
Architecture: - Server Dispatch Unit (SDU) routes 32-bit instruction words to CPU, GPU, or NPU by opcode[6:0] - RV32I CPU — 5-stage pipeline, no multiply/divide - GPU/AIPU stub — interface defined at system_top, internals not implemented - NPU v1 — 16×16 weight-stationary INT8 MAC array, single weight bank - No data cache — CPU memory is a simple combinational scratchpad
Test suite: 9 tests (8 NPU, 1 CPU). TESTS=9 PASS=9 FAIL=0
v2 — RV32IM Integer Multiply/Divide
Goal: Full RV32IM compliance for real workloads (matrix index arithmetic, loop counters).
Changes:
- alu.sv — added MUL, MULH, MULHSU, MULHU (3-cycle stall), DIV, DIVU, REM, REMU (34-cycle iterative)
- riscv_core.sv — stall logic for multi-cycle ALU ops; math_busy_r drives core_stall
- tb/cpu_peak_tests.asm — extended to cover full RV32M instruction set
- tb/extended_cpu_test.asm — 104-instruction, 44 register-check full ISA coverage test
Key design decision: DIV uses a 34-cycle non-restoring iterative divider (one bit per cycle) rather than a pipelined long divider. Simple to verify, area-efficient for the target workload.
v3 — NoC Router + L2 Cache + NPU INT16 + Ping-Pong
Goal: Replace ad-hoc dispatch with a proper NoC; add a real memory hierarchy; add mixed-precision NPU.
Changes:
noc_router.sv replaces server_dispatch_unit.sv:
- Pure combinational opcode classifier (same latency as SDU)
- Explicit cpu_accept pulse: writes instruction into SRAM only once regardless of how long net_valid is held
- GPU removed from dispatch (not implemented; routing slot reserved)
l2_cache.sv — 4-way set-associative, 16 sets, 8-word lines (2 KiB total):
- Write-back + write-allocate policy
- Pseudo-LRU round-robin victim selection
- Burst fill from DRAM (8 words per miss, ack-based protocol)
- cpu_stall signal freezes the pipeline during misses
npu.sv v3 — INT16 mixed-precision + ping-pong weight banks:
- SET_PREC command selects INT8 or INT16 data mode at runtime
- INT16 encoding: data at [31:18] (14-bit, sign-extended to 16); col at [17:14] — no overlap
- SWAP command atomically exchanges front/back weight banks — zero-bubble weight reuse
- Accumulator widened to INT48 to prevent overflow at all supported precisions
Instruction SRAM model changed from combinational scratchpad to a proper registered SRAM with a write pointer (imem_wr_ptr). The CPU can now execute while instructions are still being loaded.
Test suite: 11 regression tests. TESTS=11 PASS=11 FAIL=0
v4 — MOESI Coherency Controller (current)
Goal: Let the NPU DMA read directly from CPU-owned memory without stale-data hazards.
Changes:
duck_coherency_ctrl.sv replaces l2_cache.sv:
- Same L2 geometry as v3 (4-way, 16 sets, 8-word lines, 2 KiB)
- Adds a second master port: NPU DMA (read-only, one word at a time)
- 9-state FSM covering both CPU miss path (states 0–4) and NPU DMA path (states 5–9)
- npu_present[set][way] flag tracks whether NPU holds a copy of each L2 line
MOESI protocol:
CPU write to npu_present line → clear npu_present (Invalidate)
NPU DMA read of CPU-dirty line → flush 8 words to DRAM (M→S), then serve NPU
NPU DMA read of clean line → read directly from DRAM (fast path)
BUG-003 found and fixed during integration: ST_NPU_GAP state inserted between flush completion (ST_NPU_FLWAIT) and the NPU DRAM read (ST_NPU_DRAM) to prevent stale dram_ack capture. See docs/issues/BUG-003.md.
system_top.sv v4 exposes npu_dma_* ports at the top level for testbench or DMA controller use.
Test suite: 11 regression + 3 MOESI integration tests. Regression suite is TESTS=11 PASS=11 FAIL=0; MOESI suite currently TESTS=3 PASS=1 FAIL=2 — TC-MOESI-1/2 regressed again after BUG-003 was originally fixed, tracked as open BUG-006.
BUG-004 / BUG-005 (2026-07-19): run_riscv_program was failing with a SimFailure on every run. Root cause was RTL (system_top.sv) calling $finish itself 20 cycles after halt, racing cocotb's own end-of-regression shutdown — see BUG-004. Removing that debug block let the simulation run to completion, which then exposed a second, independent bug: the remu t4, s5, s1 check in tb/cpu_peak_tests.asm asserted the wrong expected value (88560 % 205 is actually 0, the comment's hand arithmetic was wrong) — see BUG-005. Both fixed; full tb_server_dispatch_unit regression is green (TESTS=11 PASS=11 FAIL=0).
v6 — GPU / AIPU Vector ALU (current)
Goal: Implement the GPU/AIPU routing slot that had been reserved-but-unwired since v3 — a minimum-viable vector engine, not a stub.
Changes:
gpu.sv (new) — 16-lane parallel INT32 vector ALU:
- LOAD_A / LOAD_B write one lane of a 16-element operand vector each
- COMPUTE computes result[i] = vec_a[i] <alu_op> vec_b[i] for all 16 lanes
in one cycle (ADD, SUB, AND, XOR)
- vec_a/vec_b persist across instructions — reloading only one operand
keeps the other, so a fixed operand can be reused across multiple computes
- Same IDLE/LOAD/COMPUTE/OUTPUT state-machine shape as npu.sv, 16-cycle
result streaming with backpressure
noc_router.sv — GPU port restored (3rd port, was 2-port CPU+NPU since
v3): custom-0 opcode 0x57 now routes to GPU, exactly as server_dispatch_unit.sv
(the pre-v3 3-port router, kept only for reference) already encoded.
base_die_top.sv — gpu instantiated directly on the base die (no UCIe
chiplet crossing for this minimum-viable version, unlike NPU's dedicated
link); new gpu_result_* bus forwarded to system_top.
Test suite: 4 new GPU tests (vector ADD, SUB, AND/XOR, random-reload operand
persistence) added to tb_server_dispatch_unit.py. Full regression:
TESTS=15 PASS=15 FAIL=0. MOESI integration suite unchanged
(TESTS=3 PASS=1 FAIL=2, BUG-006 still open,
unrelated to this change).
See gpu.md for the full instruction encoding and wiring.
Bug History
| ID | When | Component | Impact |
|---|---|---|---|
| BUG-001 | v3 | Testbench | INT16 data field at [31:16] overlapped col at [17:14]; all INT16 tests corrupt |
| BUG-002 | v3 | Testbench | Duplicate HALT_WORD caused SimFailure during 34-cycle DIV stalls |
| BUG-003 | v4 | RTL | Stale dram_ack after flush caused NPU DMA to read 0 instead of CPU-written value |
| BUG-004 | v4 | RTL | RTL $finish raced cocotb's own shutdown, causing SimFailure on run_riscv_program |
| BUG-005 | v4 | Testbench | Wrong hand-derived expected value for remu check in cpu_peak_tests.asm |
| BUG-006 | v4 | RTL | TC-MOESI-1/2 fail again with BUG-003's symptom despite the fix being present — open |