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"
40WIDE_FANIN_NUM_INPUTS = 17
43THROUGHPUT_NUM_INPUTS = 4
44THROUGHPUT_WINDOW = 1000
50 max_in_flight: int = 2) ->
None:
51 """Drive an N-input host mux and check every value is delivered exactly
52 once and every input is served."""
53 acc = conn.build_accelerator()
54 dut = acc.children[esiaccel.AppID(dut_name)]
55 ins = [dut.ports[esiaccel.AppID(f
"in_{i}")]
for i
in range(num_inputs)]
56 out = dut.ports[esiaccel.AppID(
"out")]
65 ((i << 16) | r, ins[i])
for r
in range(rounds)
for i
in range(num_inputs)
75 while len(recv) < len(writes):
76 while wi < len(writes)
and (len(sent) - len(recv)) < max_in_flight:
77 value, port = writes[wi]
81 recv.append(out.read().result())
83 assert sorted(recv) == sorted(sent), \
84 "arbiter dropped, duplicated or corrupted a value"
87 by_src = Counter(v >> 16
for v
in recv)
88 for i
in range(num_inputs):
89 assert by_src[i] == rounds, \
90 f
"input {i} served {by_src[i]} times, expected {rounds}"
93@cosim_test(HW_DIR / "channel_arbiter.py")
97 """Balanced (power-of-two) input count: every value appears once."""
101 conn: AcceleratorConnection) ->
None:
102 """Unbalanced (non-power-of-two) input count: the array-indexed mux over
103 N < 2**clog2(N) elements and the round-robin wrap still deliver every
104 value exactly once."""
105 _check_mux(conn,
"arbiter_test_odd", ODD_NUM_INPUTS)
108 """Pipelined selection mux tree: the multi-cycle mux latency (absorbed by a
109 deeper output FIFO + credit counter) must still deliver every value exactly
111 _check_mux(conn,
"arbiter_test_pipe", PIPE_NUM_INPUTS)
114 """Wide fan-in: the BSP instantiates arbiters with ~31 inputs, a regime the
115 small counts above never reach. Every value must still be delivered exactly
116 once and every input served."""
117 _check_mux(conn,
"arbiter_test_wide", WIDE_NUM_INPUTS)
120 """Decoupled grant-queue scheduler at wide fan-in: grants are chosen ahead
121 of time and buffered, so this exercises the queue, the sweep reload and the
122 stale-entry skip. Delivery must still be exactly-once and every input
124 _check_mux(conn,
"arbiter_test_sched", WIDE_NUM_INPUTS)
127 self, conn: AcceleratorConnection) ->
None:
128 """The scheduler with a non-power-of-two input count: the one-hot->index
129 encode and the sweep must not produce an out-of-range grant."""
130 _check_mux(conn,
"arbiter_test_sched_odd", ODD_NUM_INPUTS)
133 conn: AcceleratorConnection) ->
None:
134 """`wide_fanin` structures, with a throttled output so credits run out."""
135 _check_mux(conn,
"arbiter_test_widefanin", WIDE_FANIN_NUM_INPUTS, 8)
138 self, conn: AcceleratorConnection) ->
None:
139 """As above, under the grant-queue scheduler."""
140 _check_mux(conn,
"arbiter_test_widefanin_sched", WIDE_FANIN_NUM_INPUTS, 8)
143 self, conn: AcceleratorConnection) ->
None:
144 """`wide_fanin=True` below the threshold."""
145 _check_mux(conn,
"arbiter_test_forced_on", NUM_INPUTS)
148 self, conn: AcceleratorConnection) ->
None:
149 """`wide_fanin=False` above the threshold."""
150 _check_mux(conn,
"arbiter_test_forced_off", WIDE_FANIN_NUM_INPUTS)
153 """Contending multi-flit list messages are never interleaved."""
157 """Message atomicity under the decoupled grant-queue scheduler, with more
158 contending producers than the grant queue is deep."""
162 conn: AcceleratorConnection) ->
None:
163 """Contiguity with `wide_fanin` forced on."""
167 self, conn: AcceleratorConnection) ->
None:
168 """As above, under the grant-queue scheduler."""
173 expected_src: set[int]) ->
None:
174 acc = conn.build_accelerator()
175 dut = acc.children[esiaccel.AppID(dut_name)]
176 report = dut.ports[esiaccel.AppID(
"report")]
179 seen_src: set[int] = set()
181 num_reports = 20 * len(expected_src)
182 for _
in range(num_reports):
183 value = report.read().result()
184 err = (value >> 24) & 0x1
185 src = (value >> 16) & 0xff
187 f
"hardware detected interleaved list flits (report {value:#010x})"
191 assert seen_src == expected_src, \
192 f
"expected sources {sorted(expected_src)}, saw {sorted(seen_src)}"
195 """Zero-width (`i0`) token payloads: the credit-counter-as-buffer path
196 delivers every token exactly once (no loss/duplication/reorder) and serves
197 every producer -- the total is only reachable if no producer is starved."""
198 acc = conn.build_accelerator()
199 dut = acc.children[esiaccel.AppID(
"token_test")]
200 report = dut.ports[esiaccel.AppID(
"token_report")]
203 total = TOKEN_NUM_INPUTS * TOKENS_PER_INPUT
204 for expected
in range(1, total + 1):
205 value = report.read().result()
206 assert value == expected, \
207 f
"token {expected} arrived as {value} (loss / duplication / reorder)"
210 """The flat round-robin arbiter sustains ~one beat per cycle."""
214 """The decoupled grant-queue scheduler must sustain ~one beat per cycle
215 too. Regression test: reloading the sweep snapshot a cycle after it drains
216 (rather than on the cycle the last entry is queued) costs one idle cycle
217 per sweep, which caps throughput at `n/(n+1)` -- 0.8 here. Correctness
218 tests do not notice that, only this one does.
220 The producers emit **single-flit** messages deliberately: with multi-flit
221 lists the datapath keeps streaming while the sweep refills, which hides the
222 bubble entirely (measured: no loss at list length >= 2)."""
227 acc = conn.build_accelerator()
228 dut = acc.children[esiaccel.AppID(dut_name)]
229 report = dut.ports[esiaccel.AppID(
"throughput_report")]
232 beats = report.read().result()
233 throughput = beats / THROUGHPUT_WINDOW
236 assert throughput >= 0.95, (
237 f
"{dut_name}: {beats} beats in {THROUGHPUT_WINDOW} cycles "
238 f
"({throughput:.3f}/cycle); expected >= 0.95. A throughput of about "
239 f
"{THROUGHPUT_NUM_INPUTS / (THROUGHPUT_NUM_INPUTS + 1):.3f} means the "
240 "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_list_contiguity_wide_fanin(self, AcceleratorConnection conn)
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_wide_fanin_scheduled(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_wide_fanin_scheduled(self, AcceleratorConnection conn)
None test_mux_correctness_wide_fanin_forced_off(self, AcceleratorConnection conn)
None test_mux_correctness_wide_fanin_forced_on(self, AcceleratorConnection conn)
None test_mux_correctness_wide_fanin(self, AcceleratorConnection conn)
None test_list_contiguity_scheduled(self, AcceleratorConnection conn)
None _check_mux(AcceleratorConnection conn, str dut_name, int num_inputs, int max_in_flight=2)