calyxir / calyx

Intermediate Language (IL) for Hardware Accelerator Generators
https://calyxir.org
MIT License
453 stars 45 forks source link

rethink about the static interface #1754

Closed paili0628 closed 5 months ago

paili0628 commented 7 months ago

Our current decision on the static interface is that a go signal is triggered for exactly 1 cycle and an output is produced n cycles later, where n is the static<n> annotation for the static component.

From our synchronous meeting, we decided the way we could implement this was the following: once we have compiled the inlined the control into a single static group, we make the assignments continuous and add the following guards:

We think this implementation would work but it incurs a cost of an ‘or’ per assignment, which could potentially increase LUT usage. There are also other concerns/ to think about:

The way we currently compile static invoke statements is turning the invoke into a static group with an annotated latency n that sets the component’s go signal high for n cycles (which is just an unguarded assignment inside the invoke group). The group also contains the assignments for the wiring of i-o ports active for n cycles. Regarding how pipelines would work, here is how static<n> pipelined_component might work. You should assert pipelined_component.go for n cycles to complete one stage of the pipeline. We think this idea may be workable?

(edit by Caleb for formatting)

rachitnigam commented 7 months ago

All other assignments that were in the single static group should by guarded by comp.go | fsm > 0

This part doesn't make sense to me. Can we write down an example static group and work through what the compilation should look like?

Also, re: static invoke. It seems that you might just want something like:

static group inv_group<n> {
  comp.go = %0 ? 1'd1;
  comp.in0 = in0; comp.in1 = in1; ...
}

Said differently, the interface should make it trivial to reason about invoke compilation: it should just be a simple group.

paili0628 commented 7 months ago

re: re: static invoke. This is exactly how the invoke is compiled in the current implementation of the static interface. I guess my point is, if users want to structurally invoke a component for some reason, then our interface creates a weird discrepancy(?) between the time that the go signal is held and the inputs are held, which is kind of unnecessary.

rachitnigam commented 7 months ago

Ah, I see! I think this is okay, however, because a static interface supports pipelining. Each time we set, static_comp.go = 1, we are sending a "token" that says, "please start a new computation." If we asserted the go signal for n cycles, we would be starting n new computations.

Asserting for go for 1 cycle and inputs for n cycles is a conservative way interface that says, "we don't know when exactly the inputs are used so we're going to assert them for the full latency". I think you should read the Filament paper to get a better sense of these kinds of interfaces. Filament's types really make it clear what is going on with interfaces like these.

calebmkim commented 7 months ago

All other assignments that were in the single static group should by guarded by comp.go | fsm > 0

This part doesn't make sense to me. Can we write down an example static group and work through what the compilation should look like?

After thinking about this, I think we should adjust our idea slightly. @paili0628 feel free to chime in on this.

Dynamic Component Compilation

Suppose this is our component, just before wire-inliner runs

component comp() {
  wires {
    group seq0 {
      ... 
    } 
  } 
  control {
    seq0;
  } 
} 

When wire-inliner runs, it compiles to:

component comp(go: 1) {
  wires {
    seq0_go.in = go; 
    // bunch of other assignments guarded by seq0_go.out 
  } 
  // control is empty 
} 

For static components, we don't want this to be the case: there are some cases where the comp.go signal is not asserted (but was asserted a few cycles ago), but we still want to the component to be running.

Proposed Static Component Compilation

component comp(go: 1) {
  wires {
    seq0_go.in = go | fsm > 0; // if seq0's fsm > 0, then that means the fsm is "already running" 
    // bunch of other assignments guarded by seq0_go.out 
  } 
  // control is empty 
} 

seq0_go.out is still guarding all of seq0's assignments. So the comp.go | fsm > 0 is indirectly (through a wire) guarding seq0's assignments. Of course, this shouldn't include the assignments to the fsm, which should be guarded by comp.go for 0->1 and unconditional for everything else.

There will be some coding/engineering challenges with this, but does this make sense overall @rachitnigam @paili0628?

paili0628 commented 7 months ago

This makes sense but the guard has to be constructed at pass wire-inliner which makes it hard to pull out the fsm for the static group, (which is why my original implementation had the extra counter).

rachitnigam commented 7 months ago

seq0_go.in = go | fsm > 0

I still don't understand what's going on here. If seq0 used to be a static group, then this will attempt to continuously execute seq0 every cycle right?

paili0628 commented 7 months ago

It will not. seq0 will execute for n cycles every time the go signal is up, where n is the annotated latency.

rachitnigam commented 7 months ago

From the synchronous discussion:

static component comp() {
  wires {
    static<4> group seq0 {
        mult.go = %1 ? 1'd1;
        mult.left = 1;
        mult.right = 2;
    }
  }
  control {
    seq0
  }
}

// =>

static component comp() {
  cells {
    s_fsm = std_reg(4);
    @control start = std_wire(1);
  }
  wires {
    start.in = go;
    mult.go = %1 ? 1'd1;
    mult.left = %[0:4] ? 1;
    // =>
    mult.go = s_fsm.out == 1 ? 1'd1;
    mult.left = %0 | %[1:4] ? 1;
    // =>
    mult.left = start.out | (s_fsm.out >= 1 & s_fsm.out < 4) ? 1;
  }
  control {
    seq0;
  }
}

The compilation strategy needs to treat %0 as a special step because it needs to forward the value from the go signal.

rachitnigam commented 7 months ago

@calebmkim could you share your notes from the synchronous meeting today? Also, if we have made a decision on how to proceed, let's change the labels.

calebmkim commented 7 months ago

Static islands (i.e., static control within a dynamic component) compilation doesn’t need to change for now, since they will be compiled with a wrapper and will be treated like any other dynamic group. Note that the assignments in a static islands will (at least indirectly) be guarded by the (dynamic) component’s go signal. Eventually we want to change this: we only want to have to trigger a static island’s go signal for one cycle, and the FSM will control execution from there.

Static components get a slightly fancier FSM. The 0->1 is guarded by the static_comp.go signal, all other FSM transitions are unguarded. Also, if a guard includes %[0:n], then we have to separate it out into %0 | %[1:n], and then replace %0 with static_comp.go.

calebmkim commented 7 months ago

@paili0628 and I just met to discuss some implementation details. We have a more concrete plan now so I removed discussion needed tag.

rachitnigam commented 5 months ago

Fixed by https://github.com/cucapra/calyx/pull/1759