Fanout
A zync can contend on several arbiters at once: pass a list of binds, and
the stage hands off to every consumer boundary in a single grant. Two knobs
shape the behaviour (full signature on
Pipeline Basics):
mode—"all"requires every bind to acknowledge before the stage fires (lockstep broadcast);"any"fires as soon as one bind’sack & condis high (routing).- a per-bind
cond— gates that bind’s request and its grant term, so a bind whose condition is false simply doesn’t participate.
Lockstep fanout: mode="all"
Section titled “Lockstep fanout: mode="all"”Test model tc23_zync_fanout: one producer counter a feeds two independent
one-stage consumer pipelines, b0 and b1, and must not advance until both
have accepted the hand-off.
┌──> arb1a ──> [ pip(arb1a) → zync(sink0) : b0 <= a ] [ pip(arb0, auto_req) │ zync([arb1a, arb1b], ┤ mode="all") │ a <= a + 1 ] └──> arb1b ──> [ pip(arb1b) → zync(sink1) : b1 <= a ]flowchart LR p["producer<br/>zync[arb1a, arb1b]<br/>mode=all<br/>a |= a + 1"] -->|arb1a| c0["consumer 0<br/>b0 |= a"] p -->|arb1b| c1["consumer 1<br/>b1 |= a"]
# five arbiters: source, two fan-out boundaries, two sinksself.pip_cons = [PipCon() for _ in range(5)]
# producer — fires both downstream arbiters at oncewith pip(self.pip_cons[0], auto_req=True): with zync([self.pip_cons[1], self.pip_cons[2]], mode="all"): self.a |= self.a + self.v
# consumer 0with pip(self.pip_cons[1]): with zync(self.pip_cons[3], auto_ack=True): self.b0 |= self.a
# consumer 1with pip(self.pip_cons[2]): with zync(self.pip_cons[4], auto_ack=True): self.b1 |= self.aBecause mode="all" ANDs the two grant terms, a single producer grant fires
both consumers together, and the intended invariants are:
a >= b0anda >= b1at every sample (each consumer lags the producer);- the fan-out is symmetric:
b0 == b1at every sample — same producer, same latency, locked together bymode="all". If either consumer stalled, the producer would stall with it, keeping the pair in lockstep.
Routed fanout: per-bind conditions with mode="any"
Section titled “Routed fanout: per-bind conditions with mode="any"”Test model tc24_zync_parity_fanout routes each value to a different
consumer depending on its parity: even values of a go to be, odd values to
bo. Each bind carries a condition, and mode="any" lets whichever single
bind is satisfied fire the grant:
# stage 1 — route by the parity of the value the consumers will latchwith pip(self.pip_cons[0], auto_req=True): with zync( [(self.pip_cons[1], ~(self.a + self.v)[0]), # even bind: next a is even (self.pip_cons[2], (self.a + self.v)[0])], # odd bind: next a is odd mode="any", ): self.a |= self.a + self.v
# even path — only granted when the delivered a is evenwith pip(self.pip_cons[1]): with zync(self.pip_cons[3], auto_ack=True): self.be |= self.a
# odd path — only granted when the delivered a is oddwith pip(self.pip_cons[2]): with zync(self.pip_cons[4], auto_ack=True): self.bo |= self.aExactly one parity condition is true at any time, so exactly one bind fires per
grant and the producer free-runs at +1 per cycle. The intended behaviour: be
only ever captures even values, bo only odd ones, and both paths eventually
fire since a alternates parity.
flowchart LR p["producer<br/>zync mode=any<br/>a |= a + v"] -->|"(a+v)[0]==0 : even"| be["even path<br/>be |= a"] p -->|"(a+v)[0]==1 : odd"| bo["odd path<br/>bo |= a"]
Choosing a mode
Section titled “Choosing a mode”| pattern | binds | mode | result |
|---|---|---|---|
| broadcast | unconditional | "all" | every consumer gets every item, in lockstep |
| route / demux | one cond per bind, mutually exclusive | "any" | each item goes to exactly one consumer |
Fan-in (several producers contending for one consumer boundary) needs no
special mode at all: attach several pip/zync leaves to one PipCon and let
leaf priorities plus the tie policy arbitrate — see
Arbiters.