CIRCT 24.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.
43# Sustained-throughput probe: enough inputs that a per-sweep scheduler bubble
44# would be clearly visible (it would cap throughput at n/(n+1) == 0.8) while
45# keeping simulation time short.
46THROUGHPUT_NUM_INPUTS = 4
47THROUGHPUT_WINDOW = 1000 # measurement window, in cycles.
48TOKENS_PER_INPUT = 8 # tokens each producer emits in the token test.
49# A fan-in wide enough to exercise large-N arbitration. The BSP instantiates
50# arbiters with ~31 inputs (one per host-memory write client), a regime none of
51# the small counts above reach.
52WIDE_NUM_INPUTS = 13
53
54# A list-window payload: a struct with a `src` tag and a variable-length list.
55# `Window.default_of` adds a per-flit `last` field to the lowered frame struct,
56# which is exactly what `ChannelArbiter` uses to keep messages contiguous.
57ListInto = StructType({'src': UInt(8), 'items': ListType(UInt(16))})
58Flit = Window.default_of(ListInto)
59FlitLowered = Flit.lowered_type # struct<src: ui8, items: ui16, last: i1>
60
61
62def HostMux(num_inputs: int,
63 mux_pipeline_levels=None,
64 pipelined_scheduler=False):
65 """A host-driven single-flit multiplexer: `num_inputs` `from_host` UInt(32)
66 channels muxed into a single `to_host` channel. `mux_pipeline_levels` pipelines
67 the selection mux tree; `pipelined_scheduler` selects the decoupled
68 grant-queue arbitration."""
69
70 class HostMux(Module):
71 clk = Clock()
72 rst = Reset()
73
74 @generator
75 def build(ports):
76 ins = [
77 esi.ChannelService.from_host(AppID(f"in_{i}"), UInt(32))
78 for i in range(num_inputs)
79 ]
80 out = ChannelArbiter(ins,
81 ports.clk,
82 ports.rst,
83 mux_pipeline_levels=mux_pipeline_levels,
84 pipelined_scheduler=pipelined_scheduler,
85 telemetry=False)
86 esi.ChannelService.to_host(AppID("out"), out)
87
88 HostMux.__name__ = (
89 f"HostMux_{num_inputs}_{mux_pipeline_levels}_{pipelined_scheduler}")
90 return HostMux
91
92
93def ListProducer(src_id: int, length: int):
94 """A module which continuously emits back-to-back list messages of `length`
95 flits tagged with `src_id`. `items` counts 0..length-1 and `last` is set on
96 the final flit. Always valid, so two of these contend for the arbiter."""
97
98 class ListProducer(Module):
99 clk = Clock()
100 rst = Reset()
101 out = Output(Channel(Flit))
102
103 @generator
104 def build(ports):
105 i = Wire(UInt(8))
106 last = i == UInt(8)(length - 1)
107 st = FlitLowered({
108 'src': UInt(8)(src_id),
109 'items': i.as_uint(16),
110 'last': last,
111 })
112 chan, ready = Channel(Flit).wrap(Flit.wrap(st), Bits(1)(1))
113 ports.out = chan
114 # valid is constant 1, so a transaction happens whenever `ready`.
115 nxt = Mux(last, (i + UInt(8)(1)).as_uint(8), UInt(8)(0))
116 i.assign(nxt.reg(ports.clk, ports.rst, ce=ready, rst_value=0))
117
118 ListProducer.__name__ = f"ListProducer_src{src_id}_len{length}"
119 return ListProducer
120
121
122class ListChecker(Module):
123 """Consumes the muxed list-window stream and verifies message contiguity.
124
125 Emits one UInt(32) report per completed message:
126 bit 24 : sticky interleave-error flag (should stay 0)
127 bits 23:16 : src of the completed message
128 bits 15:0 : running completed-message count
129 """
130
131 clk = Clock()
132 rst = Reset()
133 in_ = Input(Channel(Flit))
134 report = Output(Channel(UInt(32)))
135
136 @generator
137 def build(ports):
138 active = Wire(Bits(1))
139 err = Wire(Bits(1))
140
141 in_ready = Wire(Bits(1))
142 win, valid = ports.in_.unwrap(in_ready)
143 st = win.unwrap()
144 src = st['src']
145 last = st['last']
146
147 # A message boundary: the current flit completes a message.
148 is_completing = valid & last
149 # Emit a report exactly when a message completes; back-pressure the input
150 # on that flit until the report is accepted.
151 report_valid = is_completing
152
153 # src of the in-progress message, latched at its first flit.
154 start_any = valid & in_ready & ~active
155 cur_src = src.reg(ports.clk, ports.rst, ce=start_any, rst_value=0)
156
157 # Interleave error: a flit whose src differs from the owner mid-message.
158 interleave_err = (valid & in_ready) & active & (src != cur_src)
159 err.assign((err | interleave_err).reg(ports.clk, ports.rst, rst_value=0))
160
161 # `active` tracks whether we are mid-message (past the first flit, before
162 # `last`).
163 xact = valid & in_ready
164 begin_multi = xact & ~active & ~last
165 end_msg = xact & last
166 active_next = Mux(end_msg, Mux(begin_multi, active, Bits(1)(1)), Bits(1)(0))
167 active.assign(active_next.reg(ports.clk, ports.rst, rst_value=0))
168
169 # Completed-message counter.
170 msg_count = Counter(16)(clk=ports.clk,
171 rst=ports.rst,
172 clear=Bits(1)(0),
173 increment=end_msg)
174
175 report_data = ((err.as_uint(32) * UInt(32)(0x1000000)).as_uint(32) +
176 (cur_src.as_uint(32) * UInt(32)(0x10000)).as_uint(32) +
177 msg_count.out.as_uint(32)).as_uint(32)
178 report_chan, report_ready = Channel(UInt(32)).wrap(report_data,
179 report_valid)
180 ports.report = report_chan
181
182 # Accept every non-completing flit; on a completing flit, only accept when
183 # the report is accepted so no completion is dropped.
184 in_ready.assign(Mux(is_completing, Bits(1)(1), report_ready))
185
186
187def ChannelArbiterListTestMod(pipelined_scheduler: bool):
188 """Contending list producers -> arbiter -> contiguity checker. Message
189 atomicity is the property most at risk from any arbitration change, so it is
190 covered for both arbitration modes."""
191
192 class ChannelArbiterListTest(Module):
193 clk = Clock()
194 rst = Reset()
195
196 @generator
197 def build(ports):
198 # More producers than the grant-queue depth, so the scheduled variant
199 # exercises a queue that actually fills.
200 prods = [
201 ListProducer(src, 3 + (src % 3))(clk=ports.clk, rst=ports.rst)
202 for src in range(1, 7)
203 ] if pipelined_scheduler else [
204 ListProducer(1, 3)(clk=ports.clk, rst=ports.rst),
205 ListProducer(2, 4)(clk=ports.clk, rst=ports.rst),
206 ]
207 muxed = ChannelArbiter([p.out for p in prods],
208 ports.clk,
209 ports.rst,
210 pipelined_scheduler=pipelined_scheduler,
211 telemetry=False)
212 chk = ListChecker(clk=ports.clk, rst=ports.rst, in_=muxed)
213 esi.ChannelService.to_host(AppID("report"), chk.report)
214
215 ChannelArbiterListTest.__name__ = (
216 f"ChannelArbiterListTest_{pipelined_scheduler}")
217 return ChannelArbiterListTest
218
219
220def TokenProducer(count: int):
221 """Emits exactly `count` zero-width (`i0`) tokens then idles: `valid` stays
222 high until `count` tokens have been accepted. There is no payload -- only the
223 valid/ready handshake carries information."""
224
225 class TokenProducer(Module):
226 clk = Clock()
227 rst = Reset()
228 out = Output(Channel(Bits(0)))
229
230 @generator
231 def build(ports):
232 xact = Wire(Bits(1))
233 sent = Counter(16)(clk=ports.clk,
234 rst=ports.rst,
235 clear=Bits(1)(0),
236 increment=xact)
237 valid = sent.out < UInt(16)(count)
238 chan, ready = Channel(Bits(0)).wrap(Bits(0)(0), valid)
239 ports.out = chan
240 xact.assign(valid & ready)
241
242 TokenProducer.__name__ = f"TokenProducer_{count}"
243 return TokenProducer
244
245
246class TokenChecker(Module):
247 """Consumes the muxed zero-width token stream and emits one UInt(32) report
248 per delivered token carrying its 1-based ordinal (1, 2, 3, ...). Backpressure
249 from the report channel is fed to the arbiter, exercising its credit buffer.
250 """
251
252 clk = Clock()
253 rst = Reset()
254 in_ = Input(Channel(Bits(0)))
255 report = Output(Channel(UInt(32)))
256
257 @generator
258 def build(ports):
259 in_ready = Wire(Bits(1))
260 _tok, valid = ports.in_.unwrap(in_ready) # zero-width payload: ignore data.
261
262 # Running count of delivered tokens; this token's ordinal is count + 1.
263 count = Counter(32)(clk=ports.clk,
264 rst=ports.rst,
265 clear=Bits(1)(0),
266 increment=valid & in_ready)
267 report_data = (count.out + UInt(32)(1)).as_uint(32)
268 report_chan, report_ready = Channel(UInt(32)).wrap(report_data, valid)
269 ports.report = report_chan
270 # Accept a token exactly when its report is consumed by the host.
271 in_ready.assign(report_ready)
272
273
275 """`TOKEN_NUM_INPUTS` bounded zero-width token producers -> arbiter -> token
276 counter. Each producer emits exactly `TOKENS_PER_INPUT` tokens, so the host
277 must see exactly TOKEN_NUM_INPUTS * TOKENS_PER_INPUT tokens: no loss or
278 duplication (conservation), and every producer served (no starvation, since
279 the total can only be reached if each producer's tokens all get through)."""
280
281 clk = Clock()
282 rst = Reset()
283
284 @generator
285 def build(ports):
286 prods = [
287 TokenProducer(TOKENS_PER_INPUT)(clk=ports.clk, rst=ports.rst)
288 for _ in range(TOKEN_NUM_INPUTS)
289 ]
290 muxed = ChannelArbiter([p.out for p in prods],
291 ports.clk,
292 ports.rst,
293 telemetry=False)
294 chk = TokenChecker(clk=ports.clk, rst=ports.rst, in_=muxed)
295 esi.ChannelService.to_host(AppID("token_report"), chk.report)
296
297
299 """Never idles: `valid` is tied high, so the only thing limiting the
300 arbiter's delivery rate is the arbiter itself."""
301
302 class AlwaysValidProducer(Module):
303 clk = Clock()
304 rst = Reset()
305 out = Output(Channel(UInt(32)))
306
307 @generator
308 def build(ports):
309 chan, _ready = Channel(UInt(32)).wrap(UInt(32)(tag), Bits(1)(1))
310 ports.out = chan
311
312 AlwaysValidProducer.__name__ = f"AlwaysValidProducer_{tag}"
313 return AlwaysValidProducer
314
315
316class ThroughputProbe(Module):
317 """Drains the arbiter at full rate (`ready` tied high, so the host can never
318 backpressure it) and counts delivered beats over a fixed cycle window. Holds
319 the tally on its report channel once the window closes.
320
321 Draining in hardware is the point: the host-driven tests are rate-limited by
322 the cosim DPI, so they cannot observe sustained throughput at all."""
323
324 clk = Clock()
325 rst = Reset()
326 in_ = Input(Channel(UInt(32)))
327 report = Output(Channel(UInt(32)))
328
329 @generator
330 def build(ports):
331 _data, valid = ports.in_.unwrap(Bits(1)(1)) # never backpressure.
332 cycles = Counter(32)(clk=ports.clk,
333 rst=ports.rst,
334 clear=Bits(1)(0),
335 increment=Bits(1)(1))
336 running = cycles.out < UInt(32)(THROUGHPUT_WINDOW)
337 beats = Counter(32)(clk=ports.clk,
338 rst=ports.rst,
339 clear=Bits(1)(0),
340 increment=valid & running)
341 # Report only once the window has closed; the value is then stable, so the
342 # host can read it whenever it gets around to it.
343 chan, _ready = Channel(UInt(32)).wrap(beats.out, ~running)
344 ports.report = chan
345
346
347def ChannelArbiterThroughputTestMod(pipelined_scheduler: bool):
348 """`THROUGHPUT_NUM_INPUTS` never-idle producers -> arbiter -> throughput
349 probe. Pins the arbiter's sustained delivery rate: a scheduler which needs a
350 refill/turnaround cycle between grants shows up here as a throughput well
351 below one beat per cycle, while correctness tests stay green."""
352
353 class ChannelArbiterThroughputTest(Module):
354 clk = Clock()
355 rst = Reset()
356
357 @generator
358 def build(ports):
359 prods = [
360 AlwaysValidProducer(i)(clk=ports.clk, rst=ports.rst)
361 for i in range(THROUGHPUT_NUM_INPUTS)
362 ]
363 muxed = ChannelArbiter([p.out for p in prods],
364 ports.clk,
365 ports.rst,
366 pipelined_scheduler=pipelined_scheduler,
367 telemetry=False)
368 probe = ThroughputProbe(clk=ports.clk, rst=ports.rst, in_=muxed)
369 esi.ChannelService.to_host(AppID("throughput_report"), probe.report)
370
371 ChannelArbiterThroughputTest.__name__ = (
372 f"ChannelArbiterThroughputTest_{pipelined_scheduler}")
373 return ChannelArbiterThroughputTest
374
375
376class Top(Module):
377 clk = Clock()
378 rst = Reset()
379
380 @generator
381 def construct(ports):
382 HostMux(NUM_INPUTS)(clk=ports.clk,
383 rst=ports.rst,
384 appid=AppID("arbiter_test"))
385 HostMux(ODD_NUM_INPUTS)(clk=ports.clk,
386 rst=ports.rst,
387 appid=AppID("arbiter_test_odd"))
388 HostMux(PIPE_NUM_INPUTS,
389 mux_pipeline_levels=1)(clk=ports.clk,
390 rst=ports.rst,
391 appid=AppID("arbiter_test_pipe"))
392 HostMux(WIDE_NUM_INPUTS)(clk=ports.clk,
393 rst=ports.rst,
394 appid=AppID("arbiter_test_wide"))
395 HostMux(WIDE_NUM_INPUTS,
396 pipelined_scheduler=True)(clk=ports.clk,
397 rst=ports.rst,
398 appid=AppID("arbiter_test_sched"))
399 HostMux(ODD_NUM_INPUTS,
400 pipelined_scheduler=True)(clk=ports.clk,
401 rst=ports.rst,
402 appid=AppID("arbiter_test_sched_odd"))
403 ChannelArbiterListTestMod(False)(clk=ports.clk,
404 rst=ports.rst,
405 appid=AppID("list_test"))
406 ChannelArbiterListTestMod(True)(clk=ports.clk,
407 rst=ports.rst,
408 appid=AppID("list_test_sched"))
409 ChannelArbiterTokenTest(clk=ports.clk,
410 rst=ports.rst,
411 appid=AppID("token_test"))
412 ChannelArbiterThroughputTestMod(False)(clk=ports.clk,
413 rst=ports.rst,
414 appid=AppID("throughput_test"))
415 ChannelArbiterThroughputTestMod(True)(clk=ports.clk,
416 rst=ports.rst,
417 appid=AppID("throughput_test_sched"))
418
419
420if __name__ == "__main__":
421 bsp = get_bsp(sys.argv[2] if len(sys.argv) > 2 else None)
422 s = System(bsp(Top), name="ChannelArbiterTest", output_directory=sys.argv[1])
423 s.compile()
424 s.package()
return wrap(CMemoryType::get(unwrap(ctx), baseType, numElements))
ListProducer(int src_id, int length)
ChannelArbiterThroughputTestMod(bool pipelined_scheduler)
AlwaysValidProducer(int tag)
HostMux(int num_inputs, mux_pipeline_levels=None, pipelined_scheduler=False)
TokenProducer(int count)
ChannelArbiterListTestMod(bool pipelined_scheduler)