4"""Cosim integration tests for `esiaccel.components.ChannelArbiter`.
6Runs under Verilator via the `@cosim_test` harness. Two DUTs (built by
7`hw/channel_arbiter.py`) are exercised:
9 * `arbiter_test` / `arbiter_test_odd`: host-driven single-flit multiplexers
10 with a power-of-two ('balanced') and non-power-of-two ('unbalanced') input
11 count. The host writes distinct tagged values to each input and checks that
12 every value comes out of the single output exactly once (no loss /
13 duplication) and that every input is served.
15 * `list_test`: two in-hardware list-window producers contend for the
16 arbiter; a hardware checker verifies message contiguity and streams a
17 per-message report back to the host.
19 * `token_test`: several in-hardware zero-width (`i0`) token producers, each
20 emitting a fixed number of tokens, contend for the arbiter. A hardware
21 counter reports a running ordinal per delivered token so the host can
22 confirm every token is delivered exactly once and every producer is served.
25from __future__
import annotations
27from collections
import Counter
28from pathlib
import Path
32from esiaccel.cosim.pytest
import cosim_test
34HW_DIR = Path(__file__).
resolve().parent /
"hw"
42THROUGHPUT_NUM_INPUTS = 4
43THROUGHPUT_WINDOW = 1000
46def _check_mux(conn: AcceleratorConnection, dut_name: str,
47 num_inputs: int) ->
None:
48 """Drive an N-input host mux and check every value is delivered exactly
49 once and every input is served."""
50 acc = conn.build_accelerator()
51 dut = acc.children[esiaccel.AppID(dut_name)]
52 ins = [dut.ports[esiaccel.AppID(f
"in_{i}")]
for i
in range(num_inputs)]
53 out = dut.ports[esiaccel.AppID(
"out")]
62 ((i << 16) | r, ins[i])
for r
in range(rounds)
for i
in range(num_inputs)
73 while len(recv) < len(writes):
74 while wi < len(writes)
and (len(sent) - len(recv)) < max_in_flight:
75 value, port = writes[wi]
79 recv.append(out.read().result())
81 assert sorted(recv) == sorted(sent), \
82 "arbiter dropped, duplicated or corrupted a value"
85 by_src = Counter(v >> 16
for v
in recv)
86 for i
in range(num_inputs):
87 assert by_src[i] == rounds, \
88 f
"input {i} served {by_src[i]} times, expected {rounds}"
91@cosim_test(HW_DIR / "channel_arbiter.py")
95 """Balanced (power-of-two) input count: every value appears once."""
99 conn: AcceleratorConnection) ->
None:
100 """Unbalanced (non-power-of-two) input count: the array-indexed mux over
101 N < 2**clog2(N) elements and the round-robin wrap still deliver every
102 value exactly once."""
103 _check_mux(conn,
"arbiter_test_odd", ODD_NUM_INPUTS)
106 """Pipelined selection mux tree: the multi-cycle mux latency (absorbed by a
107 deeper output FIFO + credit counter) must still deliver every value exactly
109 _check_mux(conn,
"arbiter_test_pipe", PIPE_NUM_INPUTS)
112 """Wide fan-in: the BSP instantiates arbiters with ~31 inputs, a regime the
113 small counts above never reach. Every value must still be delivered exactly
114 once and every input served."""
115 _check_mux(conn,
"arbiter_test_wide", WIDE_NUM_INPUTS)
118 """Decoupled grant-queue scheduler at wide fan-in: grants are chosen ahead
119 of time and buffered, so this exercises the queue, the sweep reload and the
120 stale-entry skip. Delivery must still be exactly-once and every input
122 _check_mux(conn,
"arbiter_test_sched", WIDE_NUM_INPUTS)
125 self, conn: AcceleratorConnection) ->
None:
126 """The scheduler with a non-power-of-two input count: the one-hot->index
127 encode and the sweep must not produce an out-of-range grant."""
128 _check_mux(conn,
"arbiter_test_sched_odd", ODD_NUM_INPUTS)
131 """Contending multi-flit list messages are never interleaved."""
135 """Message atomicity under the decoupled grant-queue scheduler, with more
136 contending producers than the grant queue is deep."""
141 expected_src: set[int]) ->
None:
142 acc = conn.build_accelerator()
143 dut = acc.children[esiaccel.AppID(dut_name)]
144 report = dut.ports[esiaccel.AppID(
"report")]
147 seen_src: set[int] = set()
149 num_reports = 20 * len(expected_src)
150 for _
in range(num_reports):
151 value = report.read().result()
152 err = (value >> 24) & 0x1
153 src = (value >> 16) & 0xff
155 f
"hardware detected interleaved list flits (report {value:#010x})"
159 assert seen_src == expected_src, \
160 f
"expected sources {sorted(expected_src)}, saw {sorted(seen_src)}"
163 """Zero-width (`i0`) token payloads: the credit-counter-as-buffer path
164 delivers every token exactly once (no loss/duplication/reorder) and serves
165 every producer -- the total is only reachable if no producer is starved."""
166 acc = conn.build_accelerator()
167 dut = acc.children[esiaccel.AppID(
"token_test")]
168 report = dut.ports[esiaccel.AppID(
"token_report")]
171 total = TOKEN_NUM_INPUTS * TOKENS_PER_INPUT
172 for expected
in range(1, total + 1):
173 value = report.read().result()
174 assert value == expected, \
175 f
"token {expected} arrived as {value} (loss / duplication / reorder)"
178 """The flat round-robin arbiter sustains ~one beat per cycle."""
182 """The decoupled grant-queue scheduler must sustain ~one beat per cycle
183 too. Regression test: reloading the sweep snapshot a cycle after it drains
184 (rather than on the cycle the last entry is queued) costs one idle cycle
185 per sweep, which caps throughput at `n/(n+1)` -- 0.8 here. Correctness
186 tests do not notice that, only this one does.
188 The producers emit **single-flit** messages deliberately: with multi-flit
189 lists the datapath keeps streaming while the sweep refills, which hides the
190 bubble entirely (measured: no loss at list length >= 2)."""
195 acc = conn.build_accelerator()
196 dut = acc.children[esiaccel.AppID(dut_name)]
197 report = dut.ports[esiaccel.AppID(
"throughput_report")]
200 beats = report.read().result()
201 throughput = beats / THROUGHPUT_WINDOW
204 assert throughput >= 0.95, (
205 f
"{dut_name}: {beats} beats in {THROUGHPUT_WINDOW} cycles "
206 f
"({throughput:.3f}/cycle); expected >= 0.95. A throughput of about "
207 f
"{THROUGHPUT_NUM_INPUTS / (THROUGHPUT_NUM_INPUTS + 1):.3f} means the "
208 "scheduler is losing a cycle per sweep.")
static mlir::Operation * resolve(Context &context, mlir::SymbolRefAttr sym)
None _check_contiguity(AcceleratorConnection conn, str dut_name, set[int] expected_src)
None test_throughput_flat(self, AcceleratorConnection conn)
None test_mux_correctness_wide(self, AcceleratorConnection conn)
None test_throughput_scheduled(self, AcceleratorConnection conn)
None test_mux_correctness_pipelined(self, AcceleratorConnection conn)
None test_mux_correctness(self, AcceleratorConnection conn)
None test_list_contiguity(self, AcceleratorConnection conn)
None test_token_conservation(self, AcceleratorConnection conn)
None _check_throughput(AcceleratorConnection conn, str dut_name)
None test_mux_correctness_scheduled_unbalanced(self, AcceleratorConnection conn)
None test_mux_correctness_scheduled(self, AcceleratorConnection conn)
None test_mux_correctness_unbalanced(self, AcceleratorConnection conn)
None test_list_contiguity_scheduled(self, AcceleratorConnection conn)
None _check_mux(AcceleratorConnection conn, str dut_name, int num_inputs)