Skip to content

Commit

Permalink
begin logic for generalized pifos
Browse files Browse the repository at this point in the history
  • Loading branch information
Cassandra Nicole Sziklai committed Jun 11, 2024
1 parent c8d1633 commit 0d9947a
Show file tree
Hide file tree
Showing 2 changed files with 330 additions and 0 deletions.
317 changes: 317 additions & 0 deletions calyx-py/test/correctness/queues/general_pifo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
# pylint: disable=import-error
import fifo
import calyx.builder as cb
import calyx.queue_call as qc

# This determines the maximum possible length of the queue:
# The max length of the queue will be 2^QUEUE_LEN_FACTOR.
QUEUE_LEN_FACTOR = 4


def invoke_subqueue(queue_cell, cmd, value, ans, err) -> cb.invoke:
"""Invokes the cell {queue_cell} with:
{cmd} passed by value
{value} passed by value
{ans} passed by reference
{err} passed by reference
"""
return cb.invoke(
queue_cell,
in_cmd=cmd,
in_value=value,
ref_ans=ans,
ref_err=err,
)


def insert_pifo(
prog,
name,
boundary,
n_flows, # the number of flows
queue_len_factor=QUEUE_LEN_FACTOR,
stats=None,
static=False,
):
"""Inserts the component `pifo` into the program.
The PIFO achieves a 50/50 split between two "flows" or "kinds".
That is, up to the availability of values, this PIFO seeks to alternate
between values of the two flows.
We say "up to availability" because, if one flow is silent and the other
is active, the active ones gets to emit consecutive values (in temporary
violation of the 50/50 rule) until the silent flow starts transmitting again.
At that point we go back to 50/50.
The PIFO's maximum capacity is determined by `queue_len_factor`:
max_queue_len = 2**queue_len_factor
Let's say the two flows are called `0` and `1`.
We orchestrate two sub-queues, `queue_l` and `queue_r`,
each having the same maximum capacity as the PIFO.
We maintain a register that points to which of these sub-queues is "hot".
Start off with `hot` pointing to `queue_l` (arbitrarily).
- `push(v, PIFO)`:
+ If len(PIFO) = `max_queue_len`, raise an "overflow" err and exit.
+ Otherwise, the charge is to enqueue value `v`.
* Find out which flow `f` the value `v` should go to;
`f` better be either `0` or `1`.
* Enqueue `v` into `queue_l` if `f` = `0`, and into `queue_r` if `f` = `1`.
* Note that the sub-queue's enqueue method is itself partial: it may raise
"overflow", in which case we propagate the overflow flag.
* If the enqueue succeeds, _and_ if a stats component is provided,
invoke the stats component and tell it that a value of flow `f` was enqueued.
- `pop(PIFO)`:
+ If `len(PIFO)` = 0, raise an "underflow" flag and exit.
+ Try `pop(queue_{hot})`, where we use the value of `hot` to determine
which sub-queue to pop from:
`queue_l` if `hot` = 0, and `queue_r` if `hot` = 1.
* If it succeeds it will return a value `v`; just propagate `v`.
Also flip `hot` so it points to the other sub-queue.
* If it fails because of underflow, return `pop(queue_{not-hot})`.
If the _second_ pop also fails, propagate the error.
Leave `hot` as it was.
"""

pifo: cb.ComponentBuilder = prog.component(name)
cmd = pifo.input("cmd", 2) # the size in bits is 2
# If this is 0, we pop. If it is 1, we peek.
# If it is 2, we push `value` to the queue.
value = pifo.input("value", 32) # The value to push to the queue

# Declare the two sub-queues as cells of this component.
queue_l = pifo.cell("queue_l", queue_l)
queue_r = pifo.cell("queue_r", queue_r)

# If a stats component was provided, declare it as a cell of this component.
if stats:
stats = pifo.cell("stats", stats, is_ref=True)

flow = pifo.reg(32) # The flow to push to: 0 to n.
# We will infer this using a separate component;
# it is a function of the value being pushed.

#infer_flow = insert_flow_inference(pifo, value, flow, "infer_flow", n_flows)

ans = pifo.reg(32, "ans", is_ref=True)
# If the user wants to pop, we will write the popped value to `ans`.

err = pifo.reg(1, "err", is_ref=True)
# We'll raise this as a general error flag for overflow and underflow.

len = pifo.reg(32) # The active length of the PIFO.
i = pifo.reg(32) # register to hold n, the number of flows

# A register that marks the next sub-queue to `pop` from.
hot = pifo.reg(32)

max_queue_len = 2**queue_len_factor

"""The flow is needed when the command is a push.
Takes in boundary, which in a 2-flow queue, presumably divided the total
workload evenly in two. So we do the math to evenly split the total
workload in n equally-sized flows (stored in variable divide).
While value is greater than the current value of the divider, we update the
divider += divide until the guard is false, which means we've found our flow.
At that point, for every time we've updated the divider, we have also run
the group infer_flow_grp that increments i each time. The final answer, i,
ends up in {flow}.
"""
adder = pifo.add(32)
i = pifo.reg(32) # keeps track of how many times we loop

# (boundary * 2) / n_flows will evenly divide "it" into n equal pieces
divider = pifo.reg(32) # divide + (n*divide), where n is the number of times we've looped
divide = pifo.reg(32) # will always store the boundary value

divide = (boundary * 2) / n_flows
i_lt_n = pifo.lt_use(divider.out, value)
with pifo.group("infer_flow_grp") as infer_flow_grp:
adder.left = i.out # checking if the value is < the smallest boundary (divide), if so we
#automatically know that the packet belongs in the first flow
adder.right = cb.HI
flow.write_en = 1
flow.in_ = adder.out
infer_flow_grp.done = flow.done

upd_divider, _ = pifo.add_store_in_reg(divider.out, divide.out, divider)

# Some equality checks.
hot_eq_0 = pifo.eq_use(hot.out, 0)
len_eq_0 = pifo.eq_use(len.out, 0)
len_eq_max_queue_len = pifo.eq_use(len.out, max_queue_len)
cmd_eq_0 = pifo.eq_use(cmd, 0)
cmd_eq_1 = pifo.eq_use(cmd, 1)
cmd_eq_2 = pifo.eq_use(cmd, 2)
err_eq_0 = pifo.eq_use(err.out, 0)
err_neq_0 = pifo.neq_use(err.out, 0)

flip_hot = pifo.bitwise_flip_reg(hot)
raise_err = pifo.reg_store(err, 1, "raise_err") # err := 1
lower_err = pifo.reg_store(err, 0, "lower_err") # err := 0
# flash_ans = pifo.reg_store(ans, 0, "flash_ans") # ans := 0

len_incr = pifo.incr(len) # len++
len_decr = pifo.decr(len) # len--

i_lt_n = pifo.lt_use(i.out, n_flows)

# The main logic.
pifo.control += cb.while_with(i_lt_n,
cb.par(
# Was it a pop, peek, or a push? We can do all cases in parallel.
cb.if_with(
# Did the user call pop?
cmd_eq_0,
cb.if_with(
# Yes, the user called pop. But is the queue empty?
len_eq_0,
raise_err, # The queue is empty: underflow.
[ # The queue is not empty. Proceed.
# We must check if `hot` is 0 or 1.
lower_err,
cb.if_with(
# Check if `hot` is 0.
hot_eq_0,
[ # `hot` is 0. We'll invoke `pop` on `queue_l`.
invoke_subqueue(queue_l, cmd, value, ans, err),
# Our next step depends on whether `queue_l`
# raised the error flag.
# We can check these cases in parallel.
cb.if_with(
err_neq_0,
[ # `queue_l` raised an error.
# We'll try to pop from `queue_r`.
# We'll pass it a lowered err
lower_err,
invoke_subqueue(queue_r, cmd, value, ans, err),
],
# `queue_l` succeeded.
# Its answer is our answer.
flip_hot,
# We'll just make `hot` point
# to the other sub-queue.
),
],
[ # Else: `hot` is 1. Proceed symmetrically.
invoke_subqueue(queue_r, cmd, value, ans, err),
cb.if_with(
err_neq_0,
[
lower_err,
invoke_subqueue(queue_l, cmd, value, ans, err),
],
flip_hot,
),
],
),
len_decr, # Decrement the active length.
# It is possible that an irrecoverable error was raised above,
# in which case the active length should _not_ in fact be decremented.
# However, in that case the PIFO's `err` flag would also
# have been raised, and no one will check this length anyway.
],
),
),
cb.if_with(
# Did the user call peek?
cmd_eq_1,
cb.if_with(
# Yes, the user called peek. But is the queue empty?
len_eq_0,
raise_err, # The queue is empty: underflow.
[ # The queue is not empty. Proceed.
# We must check if `hot` is 0 or 1.
lower_err,
cb.if_with(
# Check if `hot` is 0.
hot_eq_0,
[ # `hot` is 0. We'll invoke `peek` on `queue_l`.
invoke_subqueue(queue_l, cmd, value, ans, err),
# Our next step depends on whether `queue_l`
# raised the error flag.
cb.if_with(
err_neq_0,
[ # `queue_l` raised an error.
# We'll try to peek from `queue_r`.
# We'll pass it a lowered `err`.
lower_err,
invoke_subqueue(queue_r, cmd, value, ans, err),
],
),
# Peeking does not affect `hot`.
# Peeking does not affect the length.
],
[
invoke_subqueue(queue_r, cmd, value, ans, err),
cb.if_with(
err_neq_0,
[
lower_err,
invoke_subqueue(queue_l, cmd, value, ans, err),
],
),
],
),
],
),
),
cb.if_with(
# Did the user call push?
cmd_eq_2,
cb.if_with(
# Yes, the user called push. But is the queue full?
len_eq_max_queue_len,
raise_err, # The queue is full: overflow.
[ # The queue is not full. Proceed.
lower_err,
# We need to check which flow this value should be pushed to.
cb.while_with(i_lt_n, [infer_flow_grp, upd_divider]), # Infer the flow and write it to `flow`.
cb.if_(
flow.out,
# If flow = 1, value should be pushed to queue_r.
invoke_subqueue(queue_r, cmd, value, ans, err),
# If flow = 0, value should be pushed to queue_l.
invoke_subqueue(queue_l, cmd, value, ans, err),
),
cb.if_with(
err_eq_0,
# If no stats component is provided,
# just increment the active length.
(
len_incr
if not stats
else cb.par(
# If a stats component is provided,
# Increment the active length and also
# tell the stats component what flow we pushed.
len_incr,
(
cb.static_invoke(stats, in_flow=flow.out)
if static
else cb.invoke(stats, in_flow=flow.out)
),
)
),
),
],
),
),
) )

return pifo


def build():
"""Top-level function to build the program."""
prog = cb.Builder()
fifo_l = fifo.insert_fifo(prog, "fifo_l", QUEUE_LEN_FACTOR)
fifo_r = fifo.insert_fifo(prog, "fifo_r", QUEUE_LEN_FACTOR)
pifo = insert_pifo(prog, "pifo", fifo_l, fifo_r, 200)
qc.insert_main(prog, pifo)
return prog.program


if __name__ == "__main__":
build().emit()
13 changes: 13 additions & 0 deletions calyx-py/test/correctness/queues/pseudocode.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
def insert_pifo(prog, name, n_flows, queue_len_factor=QUEUE_LEN_FACTOR,
stats=None,
static=False,
):

# registers-
pifo: cb.ComponentBuilder = prog.component(name)
cmd = pifo.input("cmd", 2) # the size in bits is 2
# If this is 0, we pop. If it is 1, we peek.
# If it is 2, we push `value` to the queue.
value = pifo.input("value", 32) # The value to push to the queue


0 comments on commit 0d9947a

Please sign in to comment.