Skip to content

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.svgpu 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