CIRCT 23.0.0git
Loading...
Searching...
No Matches
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
5# Hardware for the ChannelArbiter cosim integration tests. Builds two
6# independent DUTs under a single top:
7#
8# * ChannelArbiterTest ("arbiter_test"): NUM_INPUTS host-driven `from_host`
9# UInt(32) channels multiplexed into a single `to_host` channel. The host
10# checks that every value is delivered exactly once (no loss / duplication).
11# Instantiated twice: a power-of-two ("balanced") count and a
12# non-power-of-two ("unbalanced") count.
13#
14# * ChannelArbiterListTest ("list_test"): two in-hardware list-window
15# producers (distinct `src` ids and message lengths) contend for the
16# arbiter. A hardware checker verifies that, once a message is granted, its
17# flits arrive contiguously (no interleaving) up to the flit tagged `last`,
18# and reports (sticky-error, src, count) per completed message on a
19# `to_host` channel.
20#
21# * ChannelArbiterTokenTest ("token_test"): TOKEN_NUM_INPUTS in-hardware
22# zero-width (`i0`) token producers, each emitting exactly TOKENS_PER_INPUT
23# tokens, contend for the arbiter. A hardware counter reports a running
24# 1-based ordinal per delivered token, so the host can confirm every token
25# is delivered exactly once (conservation) and every producer is served
26# (no starvation) -- exercising the credit-counter-as-buffer i0 path.
27
28import sys
29
30from pycde import (AppID, Clock, Input, Module, Output, Reset, System,
31 generator)
32from pycde.constructs import Counter, Mux, Wire
33from pycde.types import Bits, Channel, List as ListType, StructType, UInt, Window
34from pycde import esi
35
36from esiaccel.bsp import get_bsp
37from esiaccel.components import ChannelArbiter
38
39NUM_INPUTS = 4 # power-of-two ("balanced") input count.
40ODD_NUM_INPUTS = 3 # non-power-of-two ("unbalanced") input count.
41PIPE_NUM_INPUTS = 6 # input count for the pipelined-mux-tree variant.
42TOKEN_NUM_INPUTS = 5 # input count for the zero-width (i0) token variant.
43TOKENS_PER_INPUT = 8 # tokens each producer emits in the token test.
44
45# A list-window payload: a struct with a `src` tag and a variable-length list.
46# `Window.default_of` adds a per-flit `last` field to the lowered frame struct,
47# which is exactly what `ChannelArbiter` uses to keep messages contiguous.
48ListInto = StructType({'src': UInt(8), 'items': ListType(UInt(16))})
49Flit = Window.default_of(ListInto)
50FlitLowered = Flit.lowered_type # struct<src: ui8, items: ui16, last: i1>
51
52
53def HostMux(num_inputs: int, mux_pipeline_levels=None):
54 """A host-driven single-flit multiplexer: `num_inputs` `from_host` UInt(32)
55 channels muxed into a single `to_host` channel. `mux_pipeline_levels` pipelines
56 the selection mux tree."""
57
58 class HostMux(Module):
59 clk = Clock()
60 rst = Reset()
61
62 @generator
63 def build(ports):
64 ins = [
65 esi.ChannelService.from_host(AppID(f"in_{i}"), UInt(32))
66 for i in range(num_inputs)
67 ]
68 out = ChannelArbiter(ins,
69 ports.clk,
70 ports.rst,
71 mux_pipeline_levels=mux_pipeline_levels,
72 telemetry=False)
73 esi.ChannelService.to_host(AppID("out"), out)
74
75 HostMux.__name__ = f"HostMux_{num_inputs}_{mux_pipeline_levels}"
76 return HostMux
77
78
79def ListProducer(src_id: int, length: int):
80 """A module which continuously emits back-to-back list messages of `length`
81 flits tagged with `src_id`. `items` counts 0..length-1 and `last` is set on
82 the final flit. Always valid, so two of these contend for the arbiter."""
83
84 class ListProducer(Module):
85 clk = Clock()
86 rst = Reset()
87 out = Output(Channel(Flit))
88
89 @generator
90 def build(ports):
91 i = Wire(UInt(8))
92 last = i == UInt(8)(length - 1)
93 st = FlitLowered({
94 'src': UInt(8)(src_id),
95 'items': i.as_uint(16),
96 'last': last,
97 })
98 chan, ready = Channel(Flit).wrap(Flit.wrap(st), Bits(1)(1))
99 ports.out = chan
100 # valid is constant 1, so a transaction happens whenever `ready`.
101 nxt = Mux(last, (i + UInt(8)(1)).as_uint(8), UInt(8)(0))
102 i.assign(nxt.reg(ports.clk, ports.rst, ce=ready, rst_value=0))
103
104 ListProducer.__name__ = f"ListProducer_src{src_id}_len{length}"
105 return ListProducer
106
107
108class ListChecker(Module):
109 """Consumes the muxed list-window stream and verifies message contiguity.
110
111 Emits one UInt(32) report per completed message:
112 bit 24 : sticky interleave-error flag (should stay 0)
113 bits 23:16 : src of the completed message
114 bits 15:0 : running completed-message count
115 """
116
117 clk = Clock()
118 rst = Reset()
119 in_ = Input(Channel(Flit))
120 report = Output(Channel(UInt(32)))
121
122 @generator
123 def build(ports):
124 active = Wire(Bits(1))
125 err = Wire(Bits(1))
126
127 in_ready = Wire(Bits(1))
128 win, valid = ports.in_.unwrap(in_ready)
129 st = win.unwrap()
130 src = st['src']
131 last = st['last']
132
133 # A message boundary: the current flit completes a message.
134 is_completing = valid & last
135 # Emit a report exactly when a message completes; back-pressure the input
136 # on that flit until the report is accepted.
137 report_valid = is_completing
138
139 # src of the in-progress message, latched at its first flit.
140 start_any = valid & in_ready & ~active
141 cur_src = src.reg(ports.clk, ports.rst, ce=start_any, rst_value=0)
142
143 # Interleave error: a flit whose src differs from the owner mid-message.
144 interleave_err = (valid & in_ready) & active & (src != cur_src)
145 err.assign((err | interleave_err).reg(ports.clk, ports.rst, rst_value=0))
146
147 # `active` tracks whether we are mid-message (past the first flit, before
148 # `last`).
149 xact = valid & in_ready
150 begin_multi = xact & ~active & ~last
151 end_msg = xact & last
152 active_next = Mux(end_msg, Mux(begin_multi, active, Bits(1)(1)), Bits(1)(0))
153 active.assign(active_next.reg(ports.clk, ports.rst, rst_value=0))
154
155 # Completed-message counter.
156 msg_count = Counter(16)(clk=ports.clk,
157 rst=ports.rst,
158 clear=Bits(1)(0),
159 increment=end_msg)
160
161 report_data = ((err.as_uint(32) * UInt(32)(0x1000000)).as_uint(32) +
162 (cur_src.as_uint(32) * UInt(32)(0x10000)).as_uint(32) +
163 msg_count.out.as_uint(32)).as_uint(32)
164 report_chan, report_ready = Channel(UInt(32)).wrap(report_data,
165 report_valid)
166 ports.report = report_chan
167
168 # Accept every non-completing flit; on a completing flit, only accept when
169 # the report is accepted so no completion is dropped.
170 in_ready.assign(Mux(is_completing, Bits(1)(1), report_ready))
171
172
174 """Two contending list producers -> arbiter -> contiguity checker."""
175
176 clk = Clock()
177 rst = Reset()
178
179 @generator
180 def build(ports):
181 p0 = ListProducer(1, 3)(clk=ports.clk, rst=ports.rst)
182 p1 = ListProducer(2, 4)(clk=ports.clk, rst=ports.rst)
183 muxed = ChannelArbiter([p0.out, p1.out],
184 ports.clk,
185 ports.rst,
186 telemetry=False)
187 chk = ListChecker(clk=ports.clk, rst=ports.rst, in_=muxed)
188 esi.ChannelService.to_host(AppID("report"), chk.report)
189
190
191def TokenProducer(count: int):
192 """Emits exactly `count` zero-width (`i0`) tokens then idles: `valid` stays
193 high until `count` tokens have been accepted. There is no payload -- only the
194 valid/ready handshake carries information."""
195
196 class TokenProducer(Module):
197 clk = Clock()
198 rst = Reset()
199 out = Output(Channel(Bits(0)))
200
201 @generator
202 def build(ports):
203 xact = Wire(Bits(1))
204 sent = Counter(16)(clk=ports.clk,
205 rst=ports.rst,
206 clear=Bits(1)(0),
207 increment=xact)
208 valid = sent.out < UInt(16)(count)
209 chan, ready = Channel(Bits(0)).wrap(Bits(0)(0), valid)
210 ports.out = chan
211 xact.assign(valid & ready)
212
213 TokenProducer.__name__ = f"TokenProducer_{count}"
214 return TokenProducer
215
216
217class TokenChecker(Module):
218 """Consumes the muxed zero-width token stream and emits one UInt(32) report
219 per delivered token carrying its 1-based ordinal (1, 2, 3, ...). Backpressure
220 from the report channel is fed to the arbiter, exercising its credit buffer.
221 """
222
223 clk = Clock()
224 rst = Reset()
225 in_ = Input(Channel(Bits(0)))
226 report = Output(Channel(UInt(32)))
227
228 @generator
229 def build(ports):
230 in_ready = Wire(Bits(1))
231 _tok, valid = ports.in_.unwrap(in_ready) # zero-width payload: ignore data.
232
233 # Running count of delivered tokens; this token's ordinal is count + 1.
234 count = Counter(32)(clk=ports.clk,
235 rst=ports.rst,
236 clear=Bits(1)(0),
237 increment=valid & in_ready)
238 report_data = (count.out + UInt(32)(1)).as_uint(32)
239 report_chan, report_ready = Channel(UInt(32)).wrap(report_data, valid)
240 ports.report = report_chan
241 # Accept a token exactly when its report is consumed by the host.
242 in_ready.assign(report_ready)
243
244
246 """`TOKEN_NUM_INPUTS` bounded zero-width token producers -> arbiter -> token
247 counter. Each producer emits exactly `TOKENS_PER_INPUT` tokens, so the host
248 must see exactly TOKEN_NUM_INPUTS * TOKENS_PER_INPUT tokens: no loss or
249 duplication (conservation), and every producer served (no starvation, since
250 the total can only be reached if each producer's tokens all get through)."""
251
252 clk = Clock()
253 rst = Reset()
254
255 @generator
256 def build(ports):
257 prods = [
258 TokenProducer(TOKENS_PER_INPUT)(clk=ports.clk, rst=ports.rst)
259 for _ in range(TOKEN_NUM_INPUTS)
260 ]
261 muxed = ChannelArbiter([p.out for p in prods],
262 ports.clk,
263 ports.rst,
264 telemetry=False)
265 chk = TokenChecker(clk=ports.clk, rst=ports.rst, in_=muxed)
266 esi.ChannelService.to_host(AppID("token_report"), chk.report)
267
268
269class Top(Module):
270 clk = Clock()
271 rst = Reset()
272
273 @generator
274 def construct(ports):
275 HostMux(NUM_INPUTS)(clk=ports.clk,
276 rst=ports.rst,
277 appid=AppID("arbiter_test"))
278 HostMux(ODD_NUM_INPUTS)(clk=ports.clk,
279 rst=ports.rst,
280 appid=AppID("arbiter_test_odd"))
281 HostMux(PIPE_NUM_INPUTS,
282 mux_pipeline_levels=1)(clk=ports.clk,
283 rst=ports.rst,
284 appid=AppID("arbiter_test_pipe"))
285 ChannelArbiterListTest(clk=ports.clk,
286 rst=ports.rst,
287 appid=AppID("list_test"))
288 ChannelArbiterTokenTest(clk=ports.clk,
289 rst=ports.rst,
290 appid=AppID("token_test"))
291
292
293if __name__ == "__main__":
294 bsp = get_bsp(sys.argv[2] if len(sys.argv) > 2 else None)
295 s = System(bsp(Top), name="ChannelArbiterTest", output_directory=sys.argv[1])
296 s.compile()
297 s.package()
return wrap(CMemoryType::get(unwrap(ctx), baseType, numElements))
ListProducer(int src_id, int length)
TokenProducer(int count)
HostMux(int num_inputs, mux_pipeline_levels=None)