CIRCT 23.0.0git
Loading...
Searching...
No Matches
channel_arbiter.py
Go to the documentation of this file.
1# ===- channel_arbiter.py - pipelined list-aware channel mux -------------===//
2#
3# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4# See https://llvm.org/LICENSE.txt for license information.
5# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6#
7# ===----------------------------------------------------------------------===//
8#
9# A high-performance, pipelined, list-aware N:1 ESI channel multiplexer. See
10# `docs/components/ChannelArbiter.md` for the design details.
11#
12# ===----------------------------------------------------------------------===//
13
14from typing import List, Optional, Tuple
15
16from pycde import AppID, Clock, Input, Module, Output, Reset, generator
17from pycde.constructs import Counter, Mux, Reg, Wire
18from pycde.esi import Telemetry
19from pycde.module import modparams
20from pycde.seq import FIFO as SeqFIFO
21from pycde.signals import BitsSignal, ChannelSignal, ClockSignal, Signal
22from pycde.support import clog2
23from pycde.types import (Array, Bits, Channel, ChannelSignaling, StructType,
24 UInt, Window)
25
26
27def _select_reg_levels(num_inputs: int,
28 mux_pipeline_levels: Optional[int]) -> List[int]:
29 """Tree levels after which `_select_mux` inserts a pipeline register.
30
31 A register is placed after every `mux_pipeline_levels` levels, except after
32 the final (root) level -- its result is registered downstream. This is the
33 single source of truth for the mux-tree pipelining: `_select_mux` builds the
34 registers at these levels and `_select_latency` just counts them."""
35 if num_inputs <= 1 or not mux_pipeline_levels:
36 return []
37 gw = clog2(num_inputs)
38 return [
39 level for level in range(gw)
40 if (level + 1) % mux_pipeline_levels == 0 and level < gw - 1
41 ]
42
43
44def _select_latency(num_inputs: int, mux_pipeline_levels: Optional[int]) -> int:
45 """Pipeline-register latency (cycles) that `_select_mux` inserts."""
46 return len(_select_reg_levels(num_inputs, mux_pipeline_levels))
47
48
49def _select_mux(sel: BitsSignal, values: List[BitsSignal], clk: ClockSignal,
50 rst: Signal, mux_pipeline_levels: Optional[int]) -> BitsSignal:
51 """Return `values[sel]`.
52
53 With `mux_pipeline_levels` falsy this is a flat combinational mux (a single
54 `hw.array_get`, which CIRCT lowers to an unpipelined mux tree). Otherwise it
55 is built as an explicit balanced binary mux tree -- 2:1 nodes consuming one
56 `sel` bit per level -- with a pipeline register inserted after every
57 `mux_pipeline_levels` levels. This lets a large/wide selection mux (the
58 Fmax bottleneck of a big fan-in mux) be retimed across registers. The
59 remaining `sel` bits are pipelined alongside the partial results so each
60 level selects with the correctly-delayed index. The added latency is
61 `_select_latency(len(values), mux_pipeline_levels)` cycles."""
62 n = len(values)
63 if n == 1:
64 return values[0]
65 if not mux_pipeline_levels:
66 return Mux(sel, *values)
67 gw = clog2(n)
68 reg_levels = set(_select_reg_levels(n, mux_pipeline_levels))
69 # Pad to a full 2**gw-leaf tree; padded leaves carry a never-selected copy
70 # (the index is always < n).
71 cur = list(values) + [values[0]] * ((1 << gw) - n)
72 rem = sel
73 for level in range(gw):
74 bit = rem[0]
75 cur = [Mux(bit, cur[2 * i], cur[2 * i + 1]) for i in range(len(cur) // 2)]
76 if rem.type.width > 1:
77 rem = rem[1:]
78 if level in reg_levels:
79 cur = [c.reg(clk, rst) for c in cur]
80 rem = rem.reg(clk, rst)
81 return cur[0]
82
83
84@modparams
85def RoundRobinArbiterMod(num_inputs: int):
86 """Combinational round-robin winner selection, factored into its own module
87 for waveform visibility.
88
89 Given a per-input `valids` bitmask (bit `i` is input `i`) and a `start` index,
90 `winner` is the lowest-index input that is valid and at index `>= start`
91 (cyclically), falling back to the lowest-index valid input overall; `any_valid`
92 is high when any input is valid. Purely combinational -- the owning state
93 (`grant`/`busy`/`rr_ptr`) lives in the arbiter."""
94 assert num_inputs >= 2, "RoundRobinArbiterMod requires at least two inputs"
95 gw = clog2(num_inputs)
96
97 class RoundRobinArbiter(Module):
98 valids = Input(Bits(num_inputs))
99 start = Input(Bits(gw))
100 winner = Output(Bits(gw))
101 any_valid = Output(Bits(1))
102
103 @generator
104 def build(ports) -> None:
105
106 def priority_lsb(
107 bits_list: List[BitsSignal]) -> Tuple[BitsSignal, BitsSignal]:
108 """Index of the lowest-index set bit, plus an any-set flag, computed as
109 a balanced binary tree (O(log N) depth) rather than an O(N) chain. Each
110 node combines two subtrees, giving priority to the lower index, and
111 prefixes the selected sub-index with the branch bit."""
112 # Leaves carry (any, sub-index); pad up to 2**gw with never-set leaves
113 # so the tree is perfect and each level consumes one index bit.
114 level = [(b, None) for b in bits_list]
115 level += [(Bits(1)(0), None) for _ in range((1 << gw) - len(bits_list))]
116 width = 0
117 while len(level) > 1:
118 nxt = []
119 for j in range(0, len(level), 2):
120 la, li = level[j]
121 ra, ri = level[j + 1]
122 # The lower-index (left) subtree wins if it has any set bit.
123 take_right = ~la
124 if width == 0:
125 idx = take_right
126 else:
127 idx = BitsSignal.concat([take_right, Mux(take_right, li, ri)])
128 nxt.append((la | ra, idx))
129 level = nxt
130 width += 1
131 idx = level[0][1]
132 return (idx if idx is not None else Bits(gw)(0)), level[0][0]
133
134 valid_bits = [ports.valids[i] for i in range(num_inputs)]
135 start_u = ports.start.as_uint(gw)
136 # Winner among inputs at-or-after `start`, else the lowest-index winner.
137 hi = [valid_bits[i] & (UInt(gw)(i) >= start_u) for i in range(num_inputs)]
138 hi_idx, hi_any = priority_lsb(hi)
139 lo_idx, lo_any = priority_lsb(valid_bits)
140 ports.winner = Mux(hi_any, lo_idx, hi_idx)
141 ports.any_valid = hi_any | lo_any
142
143 return RoundRobinArbiter
144
145
146@modparams
147def ChannelArbiterMod(channel_type: Channel, num_inputs: int,
148 output_fifo_depth: int, buffer_inputs: bool,
149 telemetry: bool, mux_pipeline_levels: Optional[int]):
150 """Build a pipelined, list-aware N:1 channel multiplexer module. See the
151 `ChannelArbiter` convenience function for the user-facing entry point and
152 `docs/components/ChannelArbiter.md` for the design."""
153
154 assert num_inputs >= 2, "ChannelArbiterMod requires at least two inputs"
155 inner = channel_type.inner_type
156
157 # Determine the bit width of the datapath and whether the payload is a list
158 # window (which carries a per-flit 'last' field).
159 is_window = isinstance(inner, Window)
160 if is_window:
161 lowered = inner.lowered_type
162 field_names = [n for n, _ in lowered.fields] if isinstance(
163 lowered, StructType) else None
164 if field_names is None or "last" not in field_names:
165 raise TypeError(
166 "ChannelArbiter can only auto-detect list framing for window types "
167 "whose lowered frame is a struct with a 'last' field; got lowered "
168 f"type {lowered}. (Serial/union-framed windows are not supported.)")
169 width = lowered.bitwidth
170 else:
171 width = inner.bitwidth
172 if width is None:
173 raise TypeError(
174 f"ChannelArbiter requires a fixed-width payload; got {inner}")
175
176 # The FIFO beat is just the raw payload bits (for list/window payloads the
177 # per-flit 'last' flag is already part of them). A zero-width (token) payload
178 # carries no data, so it has no beat/FIFO at all -- the output stage uses an
179 # outstanding-beat counter instead (SeqFIFO also requires a non-zero width).
180 beat_type = Bits(width)
181
182 # Input-index width. (The credit-counter width depends on the resolved
183 # output-FIFO depth and is computed in the generator.)
184 gw = clog2(num_inputs)
185
186 # Latency (cycles) added when the selection mux is pipelined into a tree.
187 tree_latency = (0 if width == 0 else _select_latency(num_inputs,
188 mux_pipeline_levels))
189 # One register latches the mux result before the FIFO, so the total
190 # launch-to-FIFO pipeline latency is the mux-tree latency plus one.
191 pipe_latency = tree_latency + 1
192 if output_fifo_depth is not None and \
193 output_fifo_depth <= pipe_latency:
194 raise ValueError(
195 f"output_fifo_depth ({output_fifo_depth}) must be > the pipeline "
196 f"latency ({pipe_latency})")
197
198 class ChannelArbiterImpl(Module):
199 # Extra output-FIFO depth over the pipeline length, covering the credit
200 # round-trip; private and class-scoped.
201 _SLACK = 2
202
203 clk = Clock()
204 rst = Reset()
205
206 inputs = Input(Array(channel_type, num_inputs))
207 output = Output(channel_type)
208
209 @generator
210 def build(ports) -> None:
211 # Resolve the output-FIFO depth (defaulting from the private,
212 # class-scoped `_SLACK`, and covering the pipeline latency) and the
213 # credit-counter width.
214 depth = (pipe_latency + ChannelArbiterImpl._SLACK
215 if output_fifo_depth is None else output_fifo_depth)
216 cw = max(1, depth.bit_length())
217 clk = ports.clk
218 rst = ports.rst
219
220 def flit_last(typed_sig: Signal) -> BitsSignal:
221 """High when 'typed_sig' is the last flit of its message."""
222 if is_window:
223 return typed_sig.unwrap()["last"]
224 return Bits(1)(1)
225
226 def to_bits(typed_sig: Signal) -> BitsSignal:
227 """Bitcast the payload to raw bits for the datapath."""
228 if is_window:
229 typed_sig = typed_sig.unwrap()
230 return typed_sig.bitcast(Bits(width))
231
232 def from_bits(bits: BitsSignal) -> Signal:
233 """Reconstruct the payload from raw bits for the output channel."""
234 if is_window:
235 return inner.wrap(bits.bitcast(inner.lowered_type))
236 return bits.bitcast(inner)
237
238 # ---- Registered arbiter state (Reg: read now, next-state assigned
239 # below). ----
240 grant = Reg(Bits(gw), clk, rst, name="grant")
241 busy = Reg(Bits(1), clk, rst, name="busy")
242 rr_ptr = Reg(Bits(gw), clk, rst, name="rr_ptr")
243 credit = Reg(UInt(cw), clk, rst, rst_value=depth, name="credit")
244
245 credit_gt0 = credit > UInt(cw)(0)
246
247 # ---- Inputs: optional skid buffer, then unwrap with a local ready. ----
248 # Per-input one-hot grant, registered below (one register each) so these
249 # potentially high-fanout signals are not a shared combinational decode.
250 grant_is = [
251 Reg(Bits(1),
252 clk,
253 rst,
254 rst_value=(1 if i == 0 else 0),
255 name=f"grant_oh_{i}") for i in range(num_inputs)
256 ]
257 valids: List[BitsSignal] = []
258 last_bits: List[BitsSignal] = []
259 data_bits: List[BitsSignal] = []
260 for i in range(num_inputs):
261 chan = ports.inputs[i]
262 if buffer_inputs:
263 chan = chan.buffer(clk, rst, stages=1)
264 # ready[i]: consume only the granted input, and only when a credit is
265 # available. Independent of valid, so no combinational ready loop.
266 ready_i = busy & grant_is[i] & credit_gt0
267 data_i, valid_i = chan.unwrap(ready_i)
268 valids.append(valid_i)
269 last_bits.append(flit_last(data_i))
270 data_bits.append(to_bits(data_i))
271
272 # ---- Select the granted input. ----
273 sel_valid = Mux(grant, *valids)
274 sel_last = Mux(grant, *last_bits)
275 if width == 0:
276 sel_bits = Bits(0)(0)
277 else:
278 sel_bits = _select_mux(grant, data_bits, clk, rst, mux_pipeline_levels)
279
280 # A beat is launched into the pipeline when the granted input is valid and
281 # a credit is available.
282 launch = busy & sel_valid & credit_gt0
283 msg_end = launch & sel_last
284
285 # ---- Output stage (feed-forward, no backpressure). ----
286 # `pop` returns to the arbiter only through the registered credit counter,
287 # so the datapath never stalls. The zero-width case is the datapath case
288 # minus the data: no pipeline and no FIFO -- the credit counter itself is
289 # the token buffer, and a token is available whenever one is in flight.
290 if width == 0:
291 out_valid = credit < UInt(cw)(depth) # in-flight (depth - credit) > 0
292 payload_bits = Bits(0)(0)
293 fifo_pop = None
294 else:
295 # Delay the launch/valid to match the mux-tree pipeline, add one output
296 # register, then buffer the beat in the FIFO.
297 pipe_valid = launch
298 for _ in range(tree_latency):
299 pipe_valid = pipe_valid.reg(clk, rst)
300 pipe_valid = pipe_valid.reg(clk, rst, name="pipe_valid")
301 pipe_beat = sel_bits.reg(clk, rst, name="pipe_beat")
302 fifo = SeqFIFO(beat_type, depth, clk, rst)
303 fifo.push(pipe_beat, pipe_valid)
304 fifo_pop = Wire(Bits(1), "arb_pop")
305 out_valid = ~fifo.empty
306 payload_bits = fifo.pop(fifo_pop)
307
308 out_chan, out_ready = channel_type.wrap(from_bits(payload_bits),
309 out_valid)
310 ports.output = out_chan
311 pop = out_valid & out_ready
312 if fifo_pop is not None:
313 fifo_pop.assign(pop)
314
315 # ---- Credit accounting: credit = depth - in-flight. ----
316 next_credit = ((credit + pop.as_uint(cw)).as_uint(cw) -
317 launch.as_uint(cw)).as_uint(cw)
318 credit.assign(next_credit)
319
320 # ---- Round-robin arbitration (own module for waveform visibility). ----
321 rr_arbiter = RoundRobinArbiterMod(num_inputs)
322
323 def round_robin(valid_list: List[BitsSignal], start: BitsSignal,
324 name: str) -> Tuple[BitsSignal, BitsSignal]:
325 """Instantiate a RoundRobinArbiter over `valid_list` (packed into a
326 bit-bus, bit `i` == input `i`) starting from `start`."""
327 valids_vec = BitsSignal.concat(list(reversed(valid_list)))
328 inst = rr_arbiter(valids=valids_vec, start=start, instance_name=name)
329 return inst.winner, inst.any_valid
330
331 grant_u = grant.as_uint(gw)
332 is_last_idx = grant == Bits(gw)(num_inputs - 1)
333 grant_p1 = Mux(is_last_idx, (grant_u + UInt(gw)(1)).as_bits(gw),
334 Bits(gw)(0))
335
336 winner_idle, any_idle = round_robin(valids, rr_ptr, "rr_idle")
337 # At a message end the just-consumed input still asserts `valid` this
338 # cycle (the flit is consumed on the clock edge), so mask it out of the
339 # re-arbitration. Otherwise the round-robin wrap-around would
340 # speculatively re-grant that stale valid and the FSM would get stuck
341 # `busy` on an input that goes empty next cycle. A genuinely backlogged
342 # input is re-selected on the following idle cycle instead.
343 valids_next = [valids[i] & ~grant_is[i] for i in range(num_inputs)]
344 winner_next, any_next = round_robin(valids_next, grant_p1, "rr_next")
345
346 pick = ~busy & any_idle
347 reend = busy & msg_end
348 grant_if_not_reend = Mux(pick, grant, winner_idle)
349 next_grant = Mux(reend, grant_if_not_reend, winner_next)
350 busy_if_not_reend = Mux(pick, busy, Bits(1)(1))
351 next_busy = Mux(reend, busy_if_not_reend, any_next)
352 next_rr = Mux(reend, rr_ptr, grant_p1)
353
354 grant.assign(next_grant)
355 busy.assign(next_busy)
356 rr_ptr.assign(next_rr)
357
358 # Register the one-hot grant decode -- one register per input -- so each
359 # (potentially high-fanout) per-input grant signal is driven by its own
360 # register instead of a shared combinational decode of `grant`. Fed from
361 # the same next-state, so it stays coherent with `grant` (== grant == i).
362 for i in range(num_inputs):
363 grant_is[i].assign(next_grant == Bits(gw)(i))
364
365 # ---- Telemetry. ----
366 if telemetry:
367 Telemetry.report_signal(clk, rst, AppID("selectedChannel"), grant)
368 Telemetry.report_signal(clk, rst, AppID("busy"), busy)
369
370 for i in range(num_inputs):
371 served = Counter(64)(clk=clk,
372 rst=rst,
373 clear=Bits(1)(0),
374 increment=launch & grant_is[i])
375 Telemetry.report_signal(clk, rst, AppID(f"grantCount_{i}"),
376 served.out)
377
378 total_flits = Counter(64)(clk=clk,
379 rst=rst,
380 clear=Bits(1)(0),
381 increment=launch)
382 Telemetry.report_signal(clk, rst, AppID("totalFlits"), total_flits.out)
383 total_msgs = Counter(64)(clk=clk,
384 rst=rst,
385 clear=Bits(1)(0),
386 increment=msg_end)
387 Telemetry.report_signal(clk, rst, AppID("totalMessages"),
388 total_msgs.out)
389 arb_switches = Counter(64)(clk=clk,
390 rst=rst,
391 clear=Bits(1)(0),
392 increment=pick | (reend & any_next))
393 Telemetry.report_signal(clk, rst, AppID("arbSwitches"),
394 arb_switches.out)
395
396 # Max per-message flit count.
397 cur_len = Counter(32)(clk=clk, rst=rst, clear=msg_end, increment=launch)
398 msg_len = (cur_len.out + UInt(32)(1)).as_uint(32)
399 max_len = Reg(UInt(32), clk, rst, rst_value=0, name="max_list_len")
400 is_new_max = msg_end & (msg_len > max_len)
401 max_len.assign(Mux(is_new_max, max_len, msg_len))
402 Telemetry.report_signal(clk, rst, AppID("maxListLen"), max_len)
403
404 # Max output in-flight occupancy (depth - credit).
405 occ = (UInt(cw)(depth) - credit).as_uint(cw)
406 inflight_hw = Reg(UInt(cw), clk, rst, rst_value=0, name="inflight_hw")
407 is_new_hw = occ > inflight_hw
408 inflight_hw.assign(Mux(is_new_hw, inflight_hw, occ))
409 Telemetry.report_signal(clk, rst, AppID("inflightHighWater"),
410 inflight_hw)
411
412 return ChannelArbiterImpl
413
414
415def ChannelArbiter(input_channels: List[ChannelSignal],
416 clk: ClockSignal,
417 rst: Signal,
418 *,
419 appid: Optional[AppID] = None,
420 output_fifo_depth: Optional[int] = None,
421 buffer_inputs: bool = True,
422 mux_pipeline_levels: Optional[int] = None,
423 telemetry: bool = True) -> ChannelSignal:
424 """Build a pipelined, list-aware N:1 channel multiplexer.
425
426 Unlike the combinational `pycde.esi.ChannelMux`, this is a flat registered
427 round-robin arbiter with a feed-forward output stage (output register + FIFO
428 + credit counter), so it closes timing at high fan-in. It also keeps
429 multi-flit list messages contiguous: once an input is granted, it holds the
430 output until a flit whose 'last' field is set has been transferred. List
431 framing is auto-detected from the channel type (window payloads with a 'last'
432 field); all other payloads are treated as single-flit messages.
433
434 Arguments:
435 input_channels: the channels to multiplex. All must share the same
436 (ValidReady) type.
437 clk, rst: clock and reset.
438 appid: optional `AppID` for the arbiter instance (e.g. to address it or to
439 disambiguate its telemetry in the appid hierarchy).
440 output_fifo_depth: depth of the output FIFO; must be greater than the
441 pipeline latency (one output register plus any selection-mux pipeline
442 latency). Defaults to that plus a small internal slack.
443 buffer_inputs: insert a per-input skid buffer to localize backpressure.
444 mux_pipeline_levels: if set, build the N:1 data-selection mux as an explicit
445 binary tree and insert a pipeline register after every this-many tree
446 levels (1 = register every level). This retimes the wide selection mux
447 for very large fan-in; the added latency is absorbed by the output FIFO /
448 credit counter. `None` (default) uses a flat combinational mux.
449 telemetry: emit telemetry (selected channel, list-length stats, etc.).
450
451 See `docs/components/ChannelArbiter.md`."""
452
453 assert len(input_channels) > 0
454 num_inputs = len(input_channels)
455 if num_inputs == 1:
456 return input_channels[0]
457
458 channel_type = input_channels[0].type
459 for c in input_channels:
460 if c.type != channel_type:
461 raise TypeError("All ChannelArbiter inputs must have the same type; got "
462 f"{channel_type} and {c.type}")
463 if channel_type.signaling != ChannelSignaling.ValidReady:
464 raise TypeError("ChannelArbiter requires ValidReady channels; got "
465 f"{channel_type}")
466
467 if mux_pipeline_levels is not None and mux_pipeline_levels < 1:
468 raise ValueError(
469 f"mux_pipeline_levels must be >= 1, got {mux_pipeline_levels}")
470
471 mod = ChannelArbiterMod(channel_type, num_inputs, output_fifo_depth,
472 buffer_inputs, telemetry, mux_pipeline_levels)
473 inputs_array = Array(channel_type, num_inputs)(input_channels)
474 inst = mod(clk=clk, rst=rst, inputs=inputs_array, appid=appid)
475 return inst.output
int _select_latency(int num_inputs, Optional[int] mux_pipeline_levels)
List[int] _select_reg_levels(int num_inputs, Optional[int] mux_pipeline_levels)
BitsSignal _select_mux(BitsSignal sel, List[BitsSignal] values, ClockSignal clk, Signal rst, Optional[int] mux_pipeline_levels)