control_flow

Modules

SlowState #(T, int DELAY)

module SlowState #(T, int DELAY) {
   domain data
    trigger is_stable'0 : T old'0
    action set_slow'0 : T new_value_slow'DELAY
    action set_fast'0 : T new_value_fast'0
   domain reset
    action rst
}
SlowState data clock clk reset DELAY DELAY set_slow new_value_slow set_fast new_value_fast is_stable old rst

Allows us to update a blob of state with a pipelined update function. Because the new value for the register takes a few cycles to arrive, the throughput of this module is at best one per DELAY+1 cycles.

Check slow_state.is_stable to get the currently stored value. From this, you can update it with slow_state.set_slow(new_value_slow). Note that set_slow itself must be known within the same cycle as is_stable. The new_value_slow may take as many cycles as desired. When you do, is_stable will disengage until new_value_slow is computed.

You can also immediately set the state with slow_state.set_fast(new_value_fast). In this case, the new value is visible on is_stable immediately.

Example, taken from some batching memory reading code. In this code, the goal was to make sequential double (8 bytes) requests from memory, based on the counts passed via the sizes_fifo. In this example, the SlowState takes one pipeline stage for the sizes_fifo.pop(), and one for adding (size * 8) to the current address. This means it has a maximum rate of 1 update per 3 cycles.

SlowState#(T: type int#(FROM: 0, TO: 1 << 64)) current_addr
 
action set_new_addr : int#(FROM: 0, TO: 1 << 64) new_addr {
    current_addr.set_fast(new_addr)
}
 
FIFO sizes_fifo
input bool may_request_read
trigger request_read: int addr, int count
when sizes_fifo.may_pop & may_request_read {
    when current_addr.is_stable : int cur_addr {
        int size = sizes_fifo.pop() // pop()'s output may have some pipeline stages
        push_addr(cur_addr, size)
        // Add a pipeline stage for next_addr. 64-bit adds are *expensive*
        reg int next_addr = cur_addr + (size * 8) mod 1 << 64
        current_addr.set_slow(next_addr)
    }
}

SlowStateAdvanced #(T, T RESET_TO, int OLD_DELAY, int NEW_DELAY)

module SlowStateAdvanced #(T, T RESET_TO, int OLD_DELAY, int NEW_DELAY) {
  clock clk
   domain clk
    action rst
   domain data
    output state T old'-OLD_DELAY
    output bool may_update'0
    action update'0 : T new'NEW_DELAY
}
SlowStateAdvanced clk clock clk data OLD_DELAY NEW_DELAY rst update new old may_update

Allows us to update a blob of state with a pipelined update function update() may only be called if may_update, upon which may_update goes low for UPDATE_PIPELINE_DEPTH cycles

SlowPipelineEnd #(T, int PIPELINE_DEPTH)

module SlowPipelineEnd #(T, int PIPELINE_DEPTH) {
   domain main
    input T din'PIPELINE_DEPTH
    action input_changed'0
    trigger pipeline_stable'0 : T dout'0
   domain reset
    action rst
}
SlowPipelineEnd main clock clk reset PIPELINE_DEPTH PIPELINE_DEPTH din input_changed pipeline_stable dout rst

This module lets you wait until all values within a pipeline have stabilized, and use the results as if the pipeline completed instantaneously.

As opposed to SlowPipelineBegin, this module is meant to go at the end of your pipeline

Usage:

state float cur_sum'0
SlowPipelineEnd manage_adder
 
float next_sum'11 = fp32_add(cur_sum, 1.0) // Takes 11 cycles
manage_adder.din = next_sum
when manage_adder.pipeline_stable : float new_sum {
    cur_sum = new_sum
    manage_adder.input_changed()
} 

SlowPipelineBegin #(T, int PIPELINE_DEPTH)

module SlowPipelineBegin #(T, int PIPELINE_DEPTH) {
   domain main
    input T din'0
    action input_changed'0
    output T dout'-PIPELINE_DEPTH
    trigger pipeline_stable'0
   domain reset
    action rst
}
SlowPipelineBegin main clock clk reset PIPELINE_DEPTH PIPELINE_DEPTH din input_changed dout pipeline_stable rst

This module lets you wait until all values within a pipeline have stabilized, and use the results as if the pipeline completed instantaneously.

As opposed to SlowPipelineEnd, this module is meant to go at the beginning of your pipeline

Usage:

state float cur_sum'0
SlowPipeline manage_adder
manage_adder.din = cur_sum
 
float next_sum'11 = fp32_add(manage_adder.dout, 1.0) // Takes 11 cycles
when manage_adder.pipeline_stable {
    cur_sum = next_sum
    manage_adder.input_changed()
} 

SlowPut #(T, int LATENCY)

module SlowPut #(T, int LATENCY) {
   domain put_side
    output state bool may_put'0
    action put'0 : T put_data'LATENCY
   domain take_side
    trigger take_available'0 : T take_data'0
    action take'0
   domain reset
    action rst'0
}
SlowPut put_side clock clk take_side reset LATENCY put put_data may_put take take_available take_data rst

This module lets you take a value from a slow pipelined stream (where computing the value takes several pipeline stages, yet the validity of the resulting value is known immediately), and resynchronizes it to the valid wire by waiting until the pipeline is finished.

Due to the waiting, this module constricts the throughput to 1/LATENCY cycles.

Usage:

SlowPut resynchronize_fifo_output
FIFO take_stream
when take_stream.may_pop & resynchronize_fifo_output.may_put {
    float my_float = take_stream.pop()
    // Pipelined to 8 stages
    float my_float_squared = fp32_mul(my_float, my_float)
    resynchronize_fifo_output.put(my_float_squared)
}
// Once ready, we can take values from the SlowPut
when resynchronize_fifo_output.take_available: float f_sq {
    resynchronize_fifo_output.take()
    //...
}

ParallelWhile #(int COMPUTATION_LATENCY, int REQUEST_DATA_LATENCY)

module ParallelWhile #(int COMPUTATION_LATENCY, int REQUEST_DATA_LATENCY) {
  clock clk
   domain clk
   domain iter_domain
    trigger iter'0 : int #(FROM: 0, TO: TOTAL_CYCLES) cur_iter'0
    action continue'COMPUTATION_LATENCY
    trigger may_start'-REQUEST_DATA_LATENCY
    action start'0
   domain reset
    action rst
}
ParallelWhile clk clock clk iter_domain reset COMPUTATION_LATENCY COMPUTATION_LATENCY REQUEST_DATA_LATENCY continue start iter cur_iter may_start rst

This module lets you create pipelined feedback loops of variable latency.

Use as follows:

FIFO input_fifo
trigger outflow: int assoc_data
 
ParallelWhile pw
ParallelState cur_iter
ParallelStore associated_data
when pw.may_start {
    // gather up data
    when input_fifo.may_pop {
        int num_iters = input_fifo.pop()
        cur_iter.init(num_iters)
        associated_data.init(num_iters)
        pw.start()
    }
}
 
when pw.iter : int iteration {
    cur_iter.link(iteration)
    associated_data.link(iteration)
    int num_iters_left = cur_iter.old
    when num_iters_left > 0 {
        reg reg reg cur_iter.new = UnsafeIntCast#(FROM: 0, TO)(num_iters_left - 1)
        pw.continue()
    } else {
        outflow(associated_data.old)
    }
}

The order in which results arrive is not the same as the order of inputs, due to some taking more cycles to execute.

ParallelState #(T, int NUM_PARALLEL_STATES, int LATENCY)

module ParallelState #(T, int NUM_PARALLEL_STATES, int LATENCY) {
    action link'0 : int#(FROM: 0, TO: NUM_PARALLEL_STATES) cur_iter_id'0
    output T old'2
    action init'0 : T initial_data'0
    input T new'LATENCY
}
ParallelState default clock clk LATENCY - 2 link cur_iter_id init initial_data new old

See ParallelWhile

Similar to ParallelStore

Must be initialized simultaneously with [ParallelWhile::start] (with [ParallelState::init]). Old data can be read from [ParallelState::old] Data must be updated through [ParallelState::new] every cycle

ParallelStore #(T, int NUM_PARALLEL_STATES)

module ParallelStore #(T, int NUM_PARALLEL_STATES) {
    action link'0 : int#(FROM: 0, TO: NUM_PARALLEL_STATES) cur_iter_id'0
    output T old'3
    action init'0 : T initial_data'0
}
ParallelStore default clock clk link cur_iter_id init initial_data old

See ParallelWhile

Similar to ParallelState, but only initialized once, simultaneously with [ParallelWhile::start]. Data is returned every cycle on [ParallelStore::old]. Old data can be read from [ParallelState::old]

Semaphore #(int MAX, int INITIAL_PERMITS)

module Semaphore #(int MAX, int INITIAL_PERMITS) {
   domain write
    output bool may_acquire'0
    action acquire'0
   domain read
    action release'0
   domain reset
    action rst
}
Semaphore write clock clk read reset acquire may_acquire release rst

The Semaphore is a construct for distributing a limited resource, such as memory buffers or compute units. It has an internal counter called the “permits” counter which keeps track of how many of the guarded resource have been given out thus far.

With may_acquire and acquire(), you request a “permit” for a resource. You use the resource for a while, and when you’re done with it, you return it with release()

For this mode of operation, you start the Semaphore out with INITIAL_PERMITS=MAX permits.

You can also use the Semaphore in reverse mode, acting somewhat akin to a FIFO. In this case, the Semaphore should be set to INITIAL_PERMITS=0, and then when a compute job becomes available release() first.

SharedAcquireSemaphore #(int MAX, int INITIAL_PERMITS, int NUM_SEMAPHORES)

module SharedAcquireSemaphore #(int MAX, int INITIAL_PERMITS, int NUM_SEMAPHORES) {
   domain tmp
    output state int#(FROM: 0, TO: MAX+1)[NUM_SEMAPHORES] available_permits
   domain default
    input bool[NUM_SEMAPHORES] releases
    output bool may_acquire
    action acquire_all
   domain reset
    action rst
}
SharedAcquireSemaphore tmp clock clk default reset available_permits releases acquire_all may_acquire rst

Equivalent of combining multiple Semaphores together. The releases are independent, whereas may_acquire and acquire_all are shared among all.

MultiReduction #(T, T NEUTRAL_ELEMENT, int OPERATOR_LATENCY, int DATA_IN_LATENCY, int MAX_DATAS)

module MultiReduction #(T, T NEUTRAL_ELEMENT, int OPERATOR_LATENCY, int DATA_IN_LATENCY, int MAX_DATAS) {
   domain operator_dom
    trigger reduction_operator'DATA_IN_LATENCY : T op_left'DATA_IN_LATENCY, T op_right'DATA_IN_LATENCY -> T op_result'DATA_IN_LATENCY+OPERATOR_LATENCY
   domain push_dom
    output bool may_push'0
    action push'0 : T value_to_reduce'DATA_IN_LATENCY, bool last_in_wave'0
    action finish_reduction'0 : -> T reduced_data'noinfer#(V: DATA_IN_LATENCY+OPERATOR_LATENCY)
   domain reset
    action rst
}
MultiReduction operator_dom clock clk push_dom reset OPERATOR_LATENCY OPERATOR_LATENCY OPERATOR_LATENCY DATA_IN_LATENCY op_result reduction_operator op_left op_right push value_to_reduce last_in_wave finish_reduction may_push reduced_data rst

MultiReduction processes multiple concurrent reductions through a single shared reduction_operator.

New to-be-reduced data is fed in through the push() interface. To accomodate the fact that it usually takes a few cycles to produce the input data, a latency-inferred parameter DATA_IN_LATENCY is provided that separates the control flow (may_push/push()/finish_reduction), from the data.

A reduction operator is supplied by binding the reduction_operator trigger. It takes two arguments and produces a single output after exactly OPERATOR_LATENCY cycles.

In this module we use the term “wave” to describe all independent elements coming in together. There can be between 1 and MAX_DATAS elements in a wave, the size of which is determined by measuring how long the first wave is until last_in_wave is asserted. Elements in subsequent waves are assumed to correspond to the elements in the first wave.

Through the may_push backpressure interface, the module ensures that the prior reductions for the current element have finished, before allowing the next wave for this element to begin. This prevents operation on stale data. This backpressure only affects the throughput of the module in cases where the current wave is smaller than OPERATOR_LATENCY + 2. When the current wave is larger 100% throughput is possible.

To simplify implementation of the internal memory, the very first element is added on to NEUTRAL_ELEMENT.

Usage example:

module MultiplyReductions {
    MultiReduction#(NEUTRAL_ELEMENT: 1.0, MAX_DATAS: 16) mr
 
    when mr.reduction_operator : float op_left, float op_right -> float op_result {
        op_result = fp32_mul(op_left, op_right)
    }
 
    domain main
    input bool values_to_mul_valid
    input float[16] values_to_mul
    input bool values_to_mul_last
    trigger take_next_values
    state int#(FROM: 0, TO: 16) cur_idx
    state float[16] multiplied_results_store
    trigger push_multiplied_results : float[16] multiplied_results
    when values_to_mul_valid & mr.may_push {
        float input_val = fp32_add(3.14, values_to_mul[cur_idx])
 
        bool is_last_in_wave = cur_idx == 15
        mr.push(input_val, is_last_in_wave)
        when is_last_in_wave {
            take_next_values()
        }
        when values_to_mul_last {
            multiplied_results_store[cur_idx] = mr.finish_reduction()
 
            when is_last_in_wave {
                push_multiplied_results(multiplied_results_store)
            }
        }
        cur_idx = cur_idx + 1 mod 16
    }
 
    domain reset
    action rst {
        mr.rst()
    }
}