CIRCT 24.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, Or, 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
84def _onehot_to_index(onehot: BitsSignal) -> BitsSignal:
85 """Encode a one-hot bit-vector to its binary index. Bit `b` of the result is
86 the OR of the one-hot bits whose index has bit `b` set."""
87 num_inputs = onehot.type.width
88 bits = []
89 for b in range(clog2(num_inputs)):
90 terms = [onehot[i] for i in range(num_inputs) if (i >> b) & 1]
91 bits.append(Or(*terms) if terms else Bits(1)(0))
92 return BitsSignal.concat(list(reversed(bits)))
93
94
95# Grant-control strategies. `GrantSchedulerMod` and `RoundRobinControlMod` are
96# interchangeable: they deliberately carry the *same* port signature, documented
97# per-port on `GrantScheduler` below, so the arbiter picks one and wires it up
98# identically. (PyCDE scans only a class's own dict for ports, so the signature
99# cannot be inherited from a common base -- it is spelled out in each and must be
100# kept in sync.) `launch` is unused by the round-robin strategy; it is present so
101# the signature stays uniform.
102#
103# A control module owns the grant FSM state (`grant`/`grant_oh`/`busy`, plus
104# whatever else the strategy needs) and exposes it for the datapath to read. Its
105# inputs are purely observations of the datapath: which inputs are offering
106# (`valids`), and whether a flit / a final flit was accepted (`launch`,
107# `msg_end`). `_build_grant_state` below builds the state common to both.
108
109
111 ports, clk: ClockSignal, rst: Signal, num_inputs: int,
112 next_grant: BitsSignal,
113 next_busy: BitsSignal) -> Tuple[BitsSignal, BitsSignal, BitsSignal]:
114 """Register `next_grant`/`next_busy` into the grant FSM state every control
115 module has, drive the `grant`/`grant_oh`/`busy` ports with it, and return
116 `(grant, grant_oh, busy)` for the strategy to compute its next state from
117 (typically via `Wire`s, since next state depends on current).
118
119 `grant_oh` is decoded *ahead* of its registers -- one flop per input -- so
120 each high-fanout per-input grant is driven straight from a flop rather than a
121 shared combinational decode of `grant`. Decoding at the instantiation site
122 would necessarily land after the register, hence it lives here. Both are fed
123 from the same next-state, so `grant_oh[i]` is high exactly when
124 `grant == i`."""
125 gw = clog2(num_inputs)
126 grant = next_grant.reg(clk, rst, name="grant")
127 grant_oh = BitsSignal.concat([
128 (next_grant == Bits(gw)(i)).reg(clk,
129 rst,
130 rst_value=(1 if i == 0 else 0),
131 name=f"grant_oh_{i}")
132 for i in reversed(range(num_inputs))
133 ])
134 busy = next_busy.reg(clk, rst, name="busy")
135 ports.grant = grant
136 ports.grant_oh = grant_oh
137 ports.busy = busy
138 return grant, grant_oh, busy
139
140
141@modparams
142def GrantSchedulerMod(num_inputs: int, queue_depth: int):
143 """Decoupled, pipelinable grant scheduler (`pipelined_scheduler=True`).
144
145 A **grant queue** holds upcoming winners for the datapath to pop, and a
146 **sweep scheduler** refills it off the critical path. That breaks the flat
147 arbiter's single-cycle `grant -> grant` loop, its dominant timing limiter at
148 high fan-in. A queued entry is a hint about who to serve next, not a promise
149 that a particular message is waiting: an entry whose input has since gone
150 idle is skipped in one cycle (`stale` below) rather than stalling the output.
151
152 Consequently service order is best-effort, and `queue_depth` bounds only how
153 far ahead of the datapath decisions are committed -- it is not a fairness
154 knob. See section 7.1 of `docs/components/ChannelArbiter.md` for why
155 committing early is safe and for the full ordering/latency caveats."""
156 assert num_inputs >= 2, "GrantSchedulerMod requires at least two inputs"
157 gw = clog2(num_inputs)
158
159 class GrantScheduler(Module):
160 clk = Clock()
161 rst = Reset()
162
163 # Per-input `valid`; bit `i` is high when input `i` is offering a flit.
164 valids = Input(Bits(num_inputs))
165 # High on cycles a flit is accepted from the granted input.
166 launch = Input(Bits(1))
167 # High on the `launch` of a message's final flit.
168 msg_end = Input(Bits(1))
169
170 # Index of the currently granted input.
171 grant = Output(Bits(gw))
172 # `grant` pre-decoded to one-hot, one register per bit.
173 grant_oh = Output(Bits(num_inputs))
174 # High while `grant` is in force, i.e. an input is currently being served.
175 busy = Output(Bits(1))
176 # High on cycles the grant is (re)loaded from the queue; telemetry only.
177 switch = Output(Bits(1))
178
179 @generator
180 def build(ports) -> None:
181 clk = ports.clk
182 rst = ports.rst
183 next_grant = Wire(Bits(gw), "next_grant")
184 next_busy = Wire(Bits(1), "next_busy")
185 grant, grant_oh, busy = _build_grant_state(ports, clk, rst, num_inputs,
186 next_grant, next_busy)
187
188 # Grant queue. `rd_latency=0` makes it show-ahead, so `q_head` is a
189 # registered value available the same cycle -- popping adds no bubble.
190 gq = SeqFIFO(Bits(gw), queue_depth, clk, rst)
191 q_pop = Wire(Bits(1), "gq_pop")
192 q_head = gq.pop(q_pop)
193 q_nonempty = ~gq.empty
194
195 # ---- Sweep scheduler (off the datapath's critical path). ----
196 pending = Reg(Bits(num_inputs),
197 clk,
198 rst,
199 rst_value=0,
200 name="sched_pending")
201 pend_nonzero = pending != Bits(num_inputs)(0)
202 # Isolate the lowest set bit: x & (-x), with -x == ~x + 1.
203 neg_pending = ((~pending).as_uint(num_inputs) +
204 UInt(num_inputs)(1)).as_bits(num_inputs)
205 low = pending & neg_pending
206 push = pend_nonzero & ~gq.full
207 gq.push(_onehot_to_index(low), push)
208
209 # Clear the bit just scheduled (or hold if the queue is full), and reload
210 # the snapshot as soon as the sweep is exhausted. The reload has to happen
211 # on the *same* cycle the last bit is pushed: deferring it to the cycle
212 # after `pending` reads zero costs one idle cycle per sweep, capping
213 # throughput at `n/(n+1)` for `n` concurrently-active inputs. That only
214 # bites for single-flit messages; with multi-flit lists the datapath is
215 # still streaming the current message while the sweep refills, so the
216 # bubble is hidden.
217 cleared = pending & ~low
218 sweep_done = push & (cleared == Bits(num_inputs)(0))
219 pending.assign(
220 Mux(~pend_nonzero | sweep_done, Mux(push, pending, cleared),
221 ports.valids))
222
223 # ---- Datapath grant FSM. ----
224 # `started` distinguishes "this grant has not delivered a flit yet" (safe
225 # to abandon) from "mid-message" (abandoning would split the message).
226 started = Reg(Bits(1), clk, rst, rst_value=0, name="grant_started")
227 sel_valid_now = (ports.valids & grant_oh).or_reduce()
228 # Abandon a grant that has not yet delivered a flit and whose input is not
229 # offering one, but only when there is someone else to serve.
230 stale = busy & ~started & ~sel_valid_now & q_nonempty
231 advance = ports.msg_end | stale
232 take_next = ~busy | advance
233 q_pop.assign(take_next & q_nonempty)
234
235 next_grant.assign(Mux(take_next & q_nonempty, grant, q_head))
236 next_busy.assign(Mux(take_next, busy, q_nonempty))
237 # Taking a new grant clears `started`; otherwise the first launched flit
238 # sets it.
239 started_next = Mux(ports.launch, started, Bits(1)(1))
240 started.assign(Mux(take_next, started_next, Bits(1)(0)))
241
242 # The grant is replaced by a queued decision exactly when it is popped.
243 ports.switch = q_pop
244
245 return GrantScheduler
246
247
248@modparams
249def RoundRobinArbiterMod(num_inputs: int):
250 """Combinational round-robin winner selection, factored into its own module
251 for waveform visibility.
252
253 Given a per-input `valids` bitmask (bit `i` is input `i`) and a `start` index,
254 `winner` is the lowest-index input that is valid and at index `>= start`
255 (cyclically), falling back to the lowest-index valid input overall; `any_valid`
256 is high when any input is valid. Purely combinational -- the owning state
257 (`rr_ptr`, `grant`/`busy`) lives in `RoundRobinControlMod`."""
258 assert num_inputs >= 2, "RoundRobinArbiterMod requires at least two inputs"
259 gw = clog2(num_inputs)
260
261 class RoundRobinArbiter(Module):
262 valids = Input(Bits(num_inputs))
263 start = Input(Bits(gw))
264 winner = Output(Bits(gw))
265 any_valid = Output(Bits(1))
266
267 @generator
268 def build(ports) -> None:
269
270 def priority_lsb(
271 bits_list: List[BitsSignal]) -> Tuple[BitsSignal, BitsSignal]:
272 """Index of the lowest-index set bit, plus an any-set flag, computed as
273 a balanced binary tree (O(log N) depth) rather than an O(N) chain. Each
274 node combines two subtrees, giving priority to the lower index, and
275 prefixes the selected sub-index with the branch bit."""
276 # Leaves carry (any, sub-index); pad up to 2**gw with never-set leaves
277 # so the tree is perfect and each level consumes one index bit.
278 level = [(b, None) for b in bits_list]
279 level += [(Bits(1)(0), None) for _ in range((1 << gw) - len(bits_list))]
280 width = 0
281 while len(level) > 1:
282 nxt = []
283 for j in range(0, len(level), 2):
284 la, li = level[j]
285 ra, ri = level[j + 1]
286 # The lower-index (left) subtree wins if it has any set bit.
287 take_right = ~la
288 if width == 0:
289 idx = take_right
290 else:
291 idx = BitsSignal.concat([take_right, Mux(take_right, li, ri)])
292 nxt.append((la | ra, idx))
293 level = nxt
294 width += 1
295 idx = level[0][1]
296 return (idx if idx is not None else Bits(gw)(0)), level[0][0]
297
298 valid_bits = [ports.valids[i] for i in range(num_inputs)]
299 start_u = ports.start.as_uint(gw)
300 # Winner among inputs at-or-after `start`, else the lowest-index winner.
301 hi = [valid_bits[i] & (UInt(gw)(i) >= start_u) for i in range(num_inputs)]
302 hi_idx, hi_any = priority_lsb(hi)
303 lo_idx, lo_any = priority_lsb(valid_bits)
304 ports.winner = Mux(hi_any, lo_idx, hi_idx)
305 ports.any_valid = hi_any | lo_any
306
307 return RoundRobinArbiter
308
309
310@modparams
311def RoundRobinControlMod(num_inputs: int):
312 """Flat round-robin grant control (the default strategy).
313
314 Answers "who is granted next?" combinationally in the cycle the current
315 message ends, using two `RoundRobinArbiter` instances -- one for picking up
316 from idle, one for the message-end turnaround -- plus the `rr_ptr` fairness
317 pointer, which is private to this strategy. See section 7 of
318 `docs/components/ChannelArbiter.md`.
319
320 `launch` is unused; it exists only to match `GrantSchedulerMod`'s
321 signature."""
322 assert num_inputs >= 2, "RoundRobinControlMod requires at least two inputs"
323 gw = clog2(num_inputs)
324
325 class RoundRobinControl(Module):
326 clk = Clock()
327 rst = Reset()
328
329 valids = Input(Bits(num_inputs))
330 launch = Input(Bits(1))
331 msg_end = Input(Bits(1))
332
333 grant = Output(Bits(gw))
334 grant_oh = Output(Bits(num_inputs))
335 busy = Output(Bits(1))
336 switch = Output(Bits(1))
337
338 @generator
339 def build(ports) -> None:
340 clk = ports.clk
341 rst = ports.rst
342 next_grant = Wire(Bits(gw), "next_grant")
343 next_busy = Wire(Bits(1), "next_busy")
344 grant, grant_oh, busy = _build_grant_state(ports, clk, rst, num_inputs,
345 next_grant, next_busy)
346 rr_ptr = Reg(Bits(gw), clk, rst, name="rr_ptr")
347 rr_arbiter = RoundRobinArbiterMod(num_inputs)
348
349 def round_robin(valids_vec: BitsSignal, start: BitsSignal,
350 name: str) -> Tuple[BitsSignal, BitsSignal]:
351 """Instantiate a RoundRobinArbiter over `valids_vec` starting from
352 `start`."""
353 inst = rr_arbiter(valids=valids_vec, start=start, instance_name=name)
354 return inst.winner, inst.any_valid
355
356 grant_u = grant.as_uint(gw)
357 is_last_idx = grant == Bits(gw)(num_inputs - 1)
358 grant_p1 = Mux(is_last_idx, (grant_u + UInt(gw)(1)).as_bits(gw),
359 Bits(gw)(0))
360
361 winner_idle, any_idle = round_robin(ports.valids, rr_ptr, "rr_idle")
362 # At a message end the just-consumed input still asserts `valid` this
363 # cycle (the flit is consumed on the clock edge), so mask it out of the
364 # re-arbitration. Otherwise the round-robin wrap-around would
365 # speculatively re-grant that stale valid and the FSM would get stuck
366 # `busy` on an input that goes empty next cycle. A genuinely backlogged
367 # input is re-selected on the following idle cycle instead.
368 valids_next = ports.valids & ~grant_oh
369 winner_next, any_next = round_robin(valids_next, grant_p1, "rr_next")
370
371 pick = ~busy & any_idle
372 reend = busy & ports.msg_end
373 grant_if_not_reend = Mux(pick, grant, winner_idle)
374 busy_if_not_reend = Mux(pick, busy, Bits(1)(1))
375
376 next_grant.assign(Mux(reend, grant_if_not_reend, winner_next))
377 next_busy.assign(Mux(reend, busy_if_not_reend, any_next))
378 rr_ptr.assign(Mux(reend, rr_ptr, grant_p1))
379 ports.switch = pick | (reend & any_next)
380
381 return RoundRobinControl
382
383
384@modparams
385def ChannelArbiterMod(channel_type: Channel, num_inputs: int,
386 output_fifo_depth: int, buffer_inputs: bool,
387 telemetry: bool, mux_pipeline_levels: Optional[int],
388 pipelined_scheduler: bool, grant_queue_depth: int):
389 """Build a pipelined, list-aware N:1 channel multiplexer module. See the
390 `ChannelArbiter` convenience function for the user-facing entry point and
391 `docs/components/ChannelArbiter.md` for the design."""
392
393 assert num_inputs >= 2, "ChannelArbiterMod requires at least two inputs"
394 inner = channel_type.inner_type
395
396 # Determine the bit width of the datapath and whether the payload is a list
397 # window (which carries a per-flit 'last' field).
398 is_window = isinstance(inner, Window)
399 if is_window:
400 lowered = inner.lowered_type
401 field_names = [n for n, _ in lowered.fields] if isinstance(
402 lowered, StructType) else None
403 if field_names is None or "last" not in field_names:
404 raise TypeError(
405 "ChannelArbiter can only auto-detect list framing for window types "
406 "whose lowered frame is a struct with a 'last' field; got lowered "
407 f"type {lowered}. (Serial/union-framed windows are not supported.)")
408 width = lowered.bitwidth
409 else:
410 width = inner.bitwidth
411 if width is None:
412 raise TypeError(
413 f"ChannelArbiter requires a fixed-width payload; got {inner}")
414
415 # The FIFO beat is just the raw payload bits (for list/window payloads the
416 # per-flit 'last' flag is already part of them). A zero-width (token) payload
417 # carries no data, so it has no beat/FIFO at all -- the output stage uses an
418 # outstanding-beat counter instead (SeqFIFO also requires a non-zero width).
419 beat_type = Bits(width)
420
421 # Input-index width. (The credit-counter width depends on the resolved
422 # output-FIFO depth and is computed in the generator.)
423 gw = clog2(num_inputs)
424
425 # Latency (cycles) added when the selection mux is pipelined into a tree.
426 tree_latency = (0 if width == 0 else _select_latency(num_inputs,
427 mux_pipeline_levels))
428 # One register latches the mux result before the FIFO, so the total
429 # launch-to-FIFO pipeline latency is the mux-tree latency plus one.
430 pipe_latency = tree_latency + 1
431 if output_fifo_depth is not None and \
432 output_fifo_depth <= pipe_latency:
433 raise ValueError(
434 f"output_fifo_depth ({output_fifo_depth}) must be > the pipeline "
435 f"latency ({pipe_latency})")
436
437 class ChannelArbiterImpl(Module):
438 # Extra output-FIFO depth over the pipeline length, covering the credit
439 # round-trip; private and class-scoped.
440 _SLACK = 2
441
442 clk = Clock()
443 rst = Reset()
444
445 inputs = Input(Array(channel_type, num_inputs))
446 output = Output(channel_type)
447
448 @generator
449 def build(ports) -> None:
450 # Resolve the output-FIFO depth (defaulting from the private,
451 # class-scoped `_SLACK`, and covering the pipeline latency) and the
452 # credit-counter width.
453 depth = (pipe_latency + ChannelArbiterImpl._SLACK
454 if output_fifo_depth is None else output_fifo_depth)
455 cw = max(1, depth.bit_length())
456 clk = ports.clk
457 rst = ports.rst
458
459 def flit_last(typed_sig: Signal) -> BitsSignal:
460 """High when 'typed_sig' is the last flit of its message."""
461 if is_window:
462 return typed_sig.unwrap()["last"]
463 return Bits(1)(1)
464
465 def to_bits(typed_sig: Signal) -> BitsSignal:
466 """Bitcast the payload to raw bits for the datapath."""
467 if is_window:
468 typed_sig = typed_sig.unwrap()
469 return typed_sig.bitcast(Bits(width))
470
471 def from_bits(bits: BitsSignal) -> Signal:
472 """Reconstruct the payload from raw bits for the output channel."""
473 if is_window:
474 return inner.wrap(bits.bitcast(inner.lowered_type))
475 return bits.bitcast(inner)
476
477 # ---- Arbiter state. `grant`/`grant_oh`/`busy` are owned and registered
478 # by the grant-control module instantiated below; these wires forward-
479 # declare them because the input stage reads them first. ----
480 grant = Wire(Bits(gw), "grant")
481 grant_oh = Wire(Bits(num_inputs), "grant_oh")
482 busy = Wire(Bits(1), "busy")
483 credit = Reg(UInt(cw), clk, rst, rst_value=depth, name="credit")
484
485 credit_gt0 = credit > UInt(cw)(0)
486
487 # ---- Inputs: optional skid buffer, then unwrap with a local ready. ----
488 valids: List[BitsSignal] = []
489 last_bits: List[BitsSignal] = []
490 data_bits: List[BitsSignal] = []
491 for i in range(num_inputs):
492 chan = ports.inputs[i]
493 if buffer_inputs:
494 chan = chan.buffer(clk, rst, stages=1)
495 # ready[i]: consume only the granted input, and only when a credit is
496 # available. Independent of valid, so no combinational ready loop.
497 ready_i = busy & grant_oh[i] & credit_gt0
498 data_i, valid_i = chan.unwrap(ready_i)
499 valids.append(valid_i)
500 last_bits.append(flit_last(data_i))
501 data_bits.append(to_bits(data_i))
502
503 # ---- Select the granted input. ----
504 sel_valid = Mux(grant, *valids)
505 sel_last = Mux(grant, *last_bits)
506 if width == 0:
507 sel_bits = Bits(0)(0)
508 else:
509 sel_bits = _select_mux(grant, data_bits, clk, rst, mux_pipeline_levels)
510
511 # A beat is launched into the pipeline when the granted input is valid and
512 # a credit is available.
513 launch = busy & sel_valid & credit_gt0
514 msg_end = launch & sel_last
515
516 # ---- Output stage (feed-forward, no backpressure). ----
517 # `pop` returns to the arbiter only through the registered credit counter,
518 # so the datapath never stalls. The zero-width case is the datapath case
519 # minus the data: no pipeline and no FIFO -- the credit counter itself is
520 # the token buffer, and a token is available whenever one is in flight.
521 if width == 0:
522 out_valid = credit < UInt(cw)(depth) # in-flight (depth - credit) > 0
523 payload_bits = Bits(0)(0)
524 fifo_pop = None
525 else:
526 # Delay the launch/valid to match the mux-tree pipeline, add one output
527 # register, then buffer the beat in the FIFO.
528 pipe_valid = launch
529 for _ in range(tree_latency):
530 pipe_valid = pipe_valid.reg(clk, rst)
531 pipe_valid = pipe_valid.reg(clk, rst, name="pipe_valid")
532 pipe_beat = sel_bits.reg(clk, rst, name="pipe_beat")
533 fifo = SeqFIFO(beat_type, depth, clk, rst)
534 fifo.push(pipe_beat, pipe_valid)
535 fifo_pop = Wire(Bits(1), "arb_pop")
536 out_valid = ~fifo.empty
537 payload_bits = fifo.pop(fifo_pop)
538
539 out_chan, out_ready = channel_type.wrap(from_bits(payload_bits),
540 out_valid)
541 ports.output = out_chan
542 pop = out_valid & out_ready
543 if fifo_pop is not None:
544 fifo_pop.assign(pop)
545
546 # ---- Credit accounting: credit = depth - in-flight. ----
547 next_credit = ((credit + pop.as_uint(cw)).as_uint(cw) -
548 launch.as_uint(cw)).as_uint(cw)
549 credit.assign(next_credit)
550
551 # ---- Arbitration. ----
552 # Either grant-control strategy presents the same ports, so the only
553 # difference here is which module gets instantiated.
554 ctrl_mod = (GrantSchedulerMod(num_inputs, grant_queue_depth)
555 if pipelined_scheduler else RoundRobinControlMod(num_inputs))
556 ctrl = ctrl_mod(clk=clk,
557 rst=rst,
558 valids=BitsSignal.concat(list(reversed(valids))),
559 launch=launch,
560 msg_end=msg_end,
561 instance_name="arb_ctrl")
562 grant.assign(ctrl.grant)
563 grant_oh.assign(ctrl.grant_oh)
564 busy.assign(ctrl.busy)
565 arb_switch = ctrl.switch
566
567 # ---- Telemetry. ----
568 if telemetry:
569 Telemetry.report_signal(clk, rst, AppID("selectedChannel"), grant)
570 Telemetry.report_signal(clk, rst, AppID("busy"), busy)
571
572 for i in range(num_inputs):
573 served = Counter(64)(clk=clk,
574 rst=rst,
575 clear=Bits(1)(0),
576 increment=launch & grant_oh[i])
577 Telemetry.report_signal(clk, rst, AppID(f"grantCount_{i}"),
578 served.out)
579
580 total_flits = Counter(64)(clk=clk,
581 rst=rst,
582 clear=Bits(1)(0),
583 increment=launch)
584 Telemetry.report_signal(clk, rst, AppID("totalFlits"), total_flits.out)
585 total_msgs = Counter(64)(clk=clk,
586 rst=rst,
587 clear=Bits(1)(0),
588 increment=msg_end)
589 Telemetry.report_signal(clk, rst, AppID("totalMessages"),
590 total_msgs.out)
591 arb_switches = Counter(64)(clk=clk,
592 rst=rst,
593 clear=Bits(1)(0),
594 increment=arb_switch)
595 Telemetry.report_signal(clk, rst, AppID("arbSwitches"),
596 arb_switches.out)
597
598 # Max per-message flit count.
599 cur_len = Counter(32)(clk=clk, rst=rst, clear=msg_end, increment=launch)
600 msg_len = (cur_len.out + UInt(32)(1)).as_uint(32)
601 max_len = Reg(UInt(32), clk, rst, rst_value=0, name="max_list_len")
602 is_new_max = msg_end & (msg_len > max_len)
603 max_len.assign(Mux(is_new_max, max_len, msg_len))
604 Telemetry.report_signal(clk, rst, AppID("maxListLen"), max_len)
605
606 # Max output in-flight occupancy (depth - credit).
607 occ = (UInt(cw)(depth) - credit).as_uint(cw)
608 inflight_hw = Reg(UInt(cw), clk, rst, rst_value=0, name="inflight_hw")
609 is_new_hw = occ > inflight_hw
610 inflight_hw.assign(Mux(is_new_hw, inflight_hw, occ))
611 Telemetry.report_signal(clk, rst, AppID("inflightHighWater"),
612 inflight_hw)
613
614 return ChannelArbiterImpl
615
616
617def ChannelArbiter(input_channels: List[ChannelSignal],
618 clk: ClockSignal,
619 rst: Signal,
620 *,
621 appid: Optional[AppID] = None,
622 output_fifo_depth: Optional[int] = None,
623 buffer_inputs: bool = True,
624 mux_pipeline_levels: Optional[int] = None,
625 pipelined_scheduler: bool = False,
626 grant_queue_depth: int = 4,
627 telemetry: bool = True) -> ChannelSignal:
628 """Build a pipelined, list-aware N:1 channel multiplexer.
629
630 Unlike the combinational `pycde.esi.ChannelMux`, this is a flat registered
631 round-robin arbiter with a feed-forward output stage (output register + FIFO
632 + credit counter), so it closes timing at high fan-in. It also keeps
633 multi-flit list messages contiguous: once an input is granted, it holds the
634 output until a flit whose 'last' field is set has been transferred. List
635 framing is auto-detected from the channel type (window payloads with a 'last'
636 field); all other payloads are treated as single-flit messages.
637
638 Arguments:
639 input_channels: the channels to multiplex. All must share the same
640 (ValidReady) type.
641 clk, rst: clock and reset.
642 appid: optional `AppID` for the arbiter instance (e.g. to address it or to
643 disambiguate its telemetry in the appid hierarchy).
644 output_fifo_depth: depth of the output FIFO; must be greater than the
645 pipeline latency (one output register plus any selection-mux pipeline
646 latency). Defaults to that plus a small internal slack.
647 buffer_inputs: insert a per-input skid buffer to localize backpressure.
648 mux_pipeline_levels: if set, build the N:1 data-selection mux as an explicit
649 binary tree and insert a pipeline register after every this-many tree
650 levels (1 = register every level). This retimes the wide selection mux
651 for very large fan-in; the added latency is absorbed by the output FIFO /
652 credit counter. `None` (default) uses a flat combinational mux.
653 pipelined_scheduler: decouple grant selection from the datapath using a
654 grant queue fed by a sweep scheduler, instead of re-arbitrating
655 combinationally at each message end. This takes the round-robin tree out
656 of the single-cycle `grant -> grant` loop, which is the Fmax limiter at
657 high fan-in. Changes the service order (see `GrantSchedulerMod`).
658 grant_queue_depth: depth of that grant queue -- how many grant decisions
659 may be committed ahead of the datapath. Must be >= 2: a single entry
660 cannot keep the datapath fed back to back, so every message would cost a
661 refill bubble. This is not a fairness knob; a newly-valid input's wait
662 also scales with the number of concurrently active inputs (see
663 `GrantSchedulerMod`).
664 telemetry: emit telemetry (selected channel, list-length stats, etc.).
665
666 See `docs/components/ChannelArbiter.md`."""
667
668 assert len(input_channels) > 0
669 num_inputs = len(input_channels)
670 if num_inputs == 1:
671 return input_channels[0]
672
673 channel_type = input_channels[0].type
674 for c in input_channels:
675 if c.type != channel_type:
676 raise TypeError("All ChannelArbiter inputs must have the same type; got "
677 f"{channel_type} and {c.type}")
678 if channel_type.signaling != ChannelSignaling.ValidReady:
679 raise TypeError("ChannelArbiter requires ValidReady channels; got "
680 f"{channel_type}")
681
682 if mux_pipeline_levels is not None and mux_pipeline_levels < 1:
683 raise ValueError(
684 f"mux_pipeline_levels must be >= 1, got {mux_pipeline_levels}")
685
686 # Validated here rather than left to the FIFO: a bad depth otherwise surfaces
687 # as a `seq.fifo` verifier error from deep inside the lowering, with no
688 # mention of the knob that caused it. Depth 1 is rejected too -- `push` is
689 # blocked whenever the queue is non-empty, so a single entry can never keep
690 # the datapath fed back to back and every message would cost a refill
691 # bubble, silently undoing the Fmax win the option exists for.
692 if pipelined_scheduler and grant_queue_depth < 2:
693 raise ValueError(f"grant_queue_depth must be >= 2, got {grant_queue_depth}")
694
695 mod = ChannelArbiterMod(channel_type, num_inputs, output_fifo_depth,
696 buffer_inputs, telemetry, mux_pipeline_levels,
697 pipelined_scheduler, grant_queue_depth)
698 inputs_array = Array(channel_type, num_inputs)(input_channels)
699 inst = mod(clk=clk, rst=rst, inputs=inputs_array, appid=appid)
700 return inst.output
BitsSignal _onehot_to_index(BitsSignal onehot)
int _select_latency(int num_inputs, Optional[int] mux_pipeline_levels)
List[int] _select_reg_levels(int num_inputs, Optional[int] mux_pipeline_levels)
Tuple[BitsSignal, BitsSignal, BitsSignal] _build_grant_state(ports, ClockSignal clk, Signal rst, int num_inputs, BitsSignal next_grant, BitsSignal next_busy)
BitsSignal _select_mux(BitsSignal sel, List[BitsSignal] values, ClockSignal clk, Signal rst, Optional[int] mux_pipeline_levels)
GrantSchedulerMod(int num_inputs, int queue_depth)