CIRCT 24.0.0git
Loading...
Searching...
No Matches
test_channel_arbiter.py
Go to the documentation of this file.
1# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
2# See https://llvm.org/LICENSE.txt for license information.
3# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4"""Cosim integration tests for `esiaccel.components.ChannelArbiter`.
5
6Runs under Verilator via the `@cosim_test` harness. Two DUTs (built by
7`hw/channel_arbiter.py`) are exercised:
8
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.
14
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.
18
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.
23"""
24
25from __future__ import annotations
26
27from collections import Counter
28from pathlib import Path
29
30import esiaccel
31from esiaccel.accelerator import AcceleratorConnection
32from esiaccel.cosim.pytest import cosim_test
33
34HW_DIR = Path(__file__).resolve().parent / "hw"
35
36NUM_INPUTS = 4 # power-of-two ("balanced") input count.
37ODD_NUM_INPUTS = 3 # non-power-of-two ("unbalanced") input count.
38PIPE_NUM_INPUTS = 6 # input count for the pipelined-mux-tree variant.
39WIDE_NUM_INPUTS = 13 # wide fan-in; must match hw/channel_arbiter.py.
40TOKEN_NUM_INPUTS = 5 # input count for the zero-width (i0) token variant.
41TOKENS_PER_INPUT = 8 # tokens each producer emits in the token test.
42THROUGHPUT_NUM_INPUTS = 4 # must match hw/channel_arbiter.py.
43THROUGHPUT_WINDOW = 1000 # measurement window in cycles; must match hw.
44
45
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")]
54 for p in ins:
55 p.connect()
56 out.connect()
57
58 # The value encodes its source input in the upper bits (i << 16) and a round
59 # counter in the low bits, so the received multiset uniquely identifies every message.
60 rounds = 6
61 writes = [
62 ((i << 16) | r, ins[i]) for r in range(rounds) for i in range(num_inputs)
63 ]
64
65 # Keep the number of in-flight (written-but-not-yet-read) messages strictly
66 # below the output FIFO depth. This keeps a couple of inputs backlogged at
67 # once (so the round-robin arbiter has to choose between them) while
68 # avoiding the write-a-burst-before-reading deadlock.
69 max_in_flight = 2
70 sent: list[int] = []
71 recv: list[int] = []
72 wi = 0
73 while len(recv) < len(writes):
74 while wi < len(writes) and (len(sent) - len(recv)) < max_in_flight:
75 value, port = writes[wi]
76 port.write(value)
77 sent.append(value)
78 wi += 1
79 recv.append(out.read().result())
80
81 assert sorted(recv) == sorted(sent), \
82 "arbiter dropped, duplicated or corrupted a value"
83
84 # Every input (decoded by v >> 16) is served exactly `rounds` times.
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}"
89
90
91@cosim_test(HW_DIR / "channel_arbiter.py")
93
94 def test_mux_correctness(self, conn: AcceleratorConnection) -> None:
95 """Balanced (power-of-two) input count: every value appears once."""
96 _check_mux(conn, "arbiter_test", NUM_INPUTS)
97
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)
104
105 def test_mux_correctness_pipelined(self, conn: AcceleratorConnection) -> None:
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
108 once."""
109 _check_mux(conn, "arbiter_test_pipe", PIPE_NUM_INPUTS)
110
111 def test_mux_correctness_wide(self, conn: AcceleratorConnection) -> None:
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)
116
117 def test_mux_correctness_scheduled(self, conn: AcceleratorConnection) -> None:
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
121 served."""
122 _check_mux(conn, "arbiter_test_sched", WIDE_NUM_INPUTS)
123
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)
129
130 def test_list_contiguity(self, conn: AcceleratorConnection) -> None:
131 """Contending multi-flit list messages are never interleaved."""
132 self._check_contiguity(conn, "list_test", {1, 2})
133
134 def test_list_contiguity_scheduled(self, conn: AcceleratorConnection) -> None:
135 """Message atomicity under the decoupled grant-queue scheduler, with more
136 contending producers than the grant queue is deep."""
137 self._check_contiguity(conn, "list_test_sched", set(range(1, 7)))
138
139 @staticmethod
140 def _check_contiguity(conn: AcceleratorConnection, dut_name: str,
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")]
145 report.connect()
146
147 seen_src: set[int] = set()
148 # Enough reports to see every producer served several times over.
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
154 assert err == 0, \
155 f"hardware detected interleaved list flits (report {value:#010x})"
156 seen_src.add(src)
157
158 # Every contending producer must get through -- i.e. no starvation.
159 assert seen_src == expected_src, \
160 f"expected sources {sorted(expected_src)}, saw {sorted(seen_src)}"
161
162 def test_token_conservation(self, conn: AcceleratorConnection) -> None:
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")]
169 report.connect()
170
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)"
176
177 def test_throughput_flat(self, conn: AcceleratorConnection) -> None:
178 """The flat round-robin arbiter sustains ~one beat per cycle."""
179 self._check_throughput(conn, "throughput_test")
180
181 def test_throughput_scheduled(self, conn: AcceleratorConnection) -> None:
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.
187
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)."""
191 self._check_throughput(conn, "throughput_test_sched")
192
193 @staticmethod
194 def _check_throughput(conn: AcceleratorConnection, dut_name: str) -> None:
195 acc = conn.build_accelerator()
196 dut = acc.children[esiaccel.AppID(dut_name)]
197 report = dut.ports[esiaccel.AppID("throughput_report")]
198 report.connect()
199
200 beats = report.read().result()
201 throughput = beats / THROUGHPUT_WINDOW
202 # Allow for pipeline fill at the start of the window; well above the 0.8
203 # a per-sweep bubble would produce with this input count.
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_mux_correctness_pipelined(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)