Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lower functions to FuTIL's invoke. #340

Merged
merged 46 commits into from
Jan 30, 2021
Merged

Lower functions to FuTIL's invoke. #340

merged 46 commits into from
Jan 30, 2021

Conversation

cgyurgyik
Copy link
Member

So I've spent some time going over the FuTIL backend for Dahlia, as well as Scala. There's still a few things I'm unsure about. I'll try to lay out my thoughts below, and hopefully receive some guidance.

  • So within the invoke, we need to connect Ports and PortDefs, e.g. param = x.out. I've created a new class InvokeConnect for this, since Connect is only for Ports. I'm not entirely sure what the comparisons are for either.
  • I've then created an Invoke class, which takes in a CompVar and a List[InvokeConnect].

However, this still doesn't do two things. First, give us the input parameters to a function. For example,

def foo (x: ubit<32>){ ... }
  1. We need a list of the input parameters, which in this case would simply be List(PortDef(CompVar("x"), 32)).
  2. We also need to create a separate component for the body of foo (or import it if foo is imported).

I've created a toy program with void output since I'm unsure of the syntax for returning anything other than void:

def timestwo (x: ubit<32>[1]){ x[0] := 1; }

decl A: ubit<32>[1];
let _x = timestwo(A);

This lowers to:

import "primitives/std.lib";
component main() -> () {
  cells {
    A = prim std_mem_d1_ext(32,1,1);
    _x0 = prim std_reg(0);
    timestwo0 = timestwo;
  }
  wires {
    group let0 {
      _x0.in = timestwo0.out;
      _x0.write_en = timestwo0.done;
      let0[done] = _x0.done;
    }
    invoke timestwo0(input = A.out)() // Here, `input` is hardcoded.
    // ^ Unsure why this is after the group. I presume it should in the control.
  }
  control {
    let0;
  }
}

Thanks for the help :)

@cgyurgyik cgyurgyik linked an issue Dec 28, 2020 that may be closed by this pull request
@rachitnigam
Copy link
Member

So, I think there is a top-level confusion about invoke and FuTIL-defined components here:

Components defined by FuTIL (currently) do not support parameters. This means when you see something like:

def bar(x: ubit<32>) { ... }
bar(y)

it should become:

component bar(x: 32) -> (ret: 32) { ... }
component main() {
  cells { 
    b = bar; // <- Takes no PortDefs
    y = std_reg(32);
  }
  wires { ... }
  control {
    invoke bar(x = y.out)();
  }
}

Few code related things that are tripping you up:

  1. Invoke should be a Control, not a Structure.
  2. invoke does not need anything in wire, it just takes port connections.

Let me know if that makes sense. FuTIL has an invoke test in test/correctness/invoke.futil.

@cgyurgyik
Copy link
Member Author

cgyurgyik commented Dec 29, 2020

In your example:
I meant

component bar(x: 32) -> (ret: 32) { ... }
component main() {
  cells { 
    b = bar; 
    y = std_reg(32);
  }
  wires { ... }
  control {
    invoke bar0(x = y.out)(); // <- `src` = PortDef, `dest` = Port
  }
}

This is why I created an InvokeConnection, which allowed for this (more below).

The bit about it being a Control is helpful!
Following your initial steps, I now have:

def two(x: ubit<32>){ x := 2; }
let y: ubit<32> = (1 as ubit<32>);
---
two(y);

Lowered to:

import "primitives/std.lib";
component main() -> () {
  cells {
    const0 = prim std_const(32,1);
    y0 = prim std_reg(32);
    two0 = two;
  }
  wires {
    group let0<"static"=1> {
      y0.in = const0.out;
      y0.write_en = 1'd1;
      let0[done] = y0.done;
    }
  }
  control {
    seq {
      let0;
      invoke two0(y0.out)() 
    }
  }
}

In the following line:

invoke two0(y0.out)() // <- Just passing in the output ports, but we want `x = y0.out`

I'm just passing in the emitted ports from the inputs to the EApp, but what we really want is x = y0.out. I created an InvokeConnection for this (Port, PortDef) representation, but perhaps that's wrong. The second issue is, how to access the parameter names of the component, in this case, x? Presumably from the component that is emitted?

@rachitnigam
Copy link
Member

You will have to walk over the definitions in the program and build a mapping from name to the signature. The Prog structure has defs which is not correctly handled by the FuTIL backend:
https://github.com/cucapra/dahlia/blob/master/src/main/scala/backends/futil/FutilBackend.scala#L766

@rachitnigam
Copy link
Member

We should probably remove the magic compilation for sqrt, exp, etc: https://github.com/cucapra/dahlia/pull/340/files#diff-85f1b567d1bc7b96825cab43f656b8e562a1561c8a0cc1fbf25765e9b6f586d3R559

@rachitnigam
Copy link
Member

We should attempt to fix #333 while we're at this.

@cgyurgyik
Copy link
Member Author

cgyurgyik commented Dec 30, 2020

Please forgive the Scala lack-of-knowledge hackups. Here's current progress:

def two(x: ubit<32>){ }
let y: ubit<32> = (1 as ubit<32>);
---
two(y);

=>

import "primitives/std.lib";
component main() -> () {
  cells {
    const0 = prim std_const(32,1);
    y0 = prim std_reg(32);
    two0 = two;
  }
  wires {
    group let0<"static"=1> {
      y0.in = const0.out;
      y0.write_en = 1'd1;
      let0[done] = y0.done;
    }
  }
  control {
    seq {
      let0;
      invoke two0(x=y0.out)()
    }
  }
}
component two() -> () {
  cells {
  }
  wires {
  }
  control {
    empty
  }
}

Question: How does one write a return type in Dahlia?
For example, def two(x: ubit<32>) -> ubit<32> { ... } // return type is ubit<32>

@rachitnigam
Copy link
Member

This is looking good! Dahlia return types are written as:

def foo(x: ubit<32>): ubit<32> { }

There is going to be some impedance mismatch between invoke-able components and values returned by Dahlia. There is no way to simultaneously invoke something and save the value generated on the output port.

To "return" values, invoke states that certain output ports should keep outputting values after the invoke statement is done executing. This way, you can save the value in a group that executes after the invoke. The invoke test in the FuTIL repo uses this to save the value generated through an invoke.

@cgyurgyik
Copy link
Member Author

This is looking good! Dahlia return types are written as:

def foo(x: ubit<32>): ubit<32> { }

There is going to be some impedance mismatch between invoke-able components and values returned by Dahlia. There is no way to simultaneously invoke something and save the value generated on the output port.

To "return" values, invoke states that certain output ports should keep outputting values after the invoke statement is done executing. This way, you can save the value in a group that executes after the invoke. The invoke test in the FuTIL repo uses this to save the value generated through an invoke.

Ok noted. I've added signatures for the invoked components.

case EApp(id, inputs@_) => {
        val function_name = id.toString()
        val decl = CompDecl(genName(function_name), CompVar(function_name))
        // TODO: How to add `invoke` to control {} ?
        EmitOutput(decl.id.port("out"), decl.id.port("done"), List(decl), None)
      }

So the above case is hit when I have an expression such as let y: ubit<32> = foo(x);.
How would I emit the invoke code that is part of the control, usually done in emitCmd? Perhaps I'm missing something simple here.

@rachitnigam
Copy link
Member

Hm, this is going to require some deeper thinking. I think we'll have to make some assumptions. For example, we can say that EApp is only supported when it's inside a let expression by explicitly matching on:

case CLet(name, typ, EApp(...)) => ...

And throw an error if we encounter EApp anywhere else. This is also how we handle memory reads which need to be "hoisted" before hand in a different pass (it's called HoistMemoryReads). We can implement a similar pass for applications.


Extra compilers sermon: In general, when building compiler backends, we can make these sorts of assumptions and then write "lowering" passes that bring programs in a desired format. A good way to visualize compilers is a funnel: as we continue compiling programs, they become restricted to smaller and smaller languages and have stronger and stronger assumptions.

@cgyurgyik
Copy link
Member Author

Hm, this is going to require some deeper thinking. I think we'll have to make some assumptions. For example, we can say that EApp is only supported when it's inside a let expression by explicitly matching on:

case CLet(name, typ, EApp(...)) => ...

And throw an error if we encounter EApp anywhere else. This is also how we handle memory reads which need to be "hoisted" before hand in a different pass (it's called HoistMemoryReads). We can implement a similar pass for applications.

Extra compilers sermon: In general, when building compiler backends, we can make these sorts of assumptions and then write "lowering" passes that bring programs in a desired format. A good way to visualize compilers is a funnel: as we continue compiling programs, they become restricted to smaller and smaller languages and have stronger and stronger assumptions.

Great thanks Rachit! Compiler sermons are welcome here.

@cgyurgyik
Copy link
Member Author

Progress:

def two(a: ubit<32>, b: ubit<32>): ubit<32> {
  let R: ubit<32> = (2 as ubit<32>);
  return R;
}

let y: ubit<32> = (1 as ubit<32>);
---
let z: ubit<32> = two(y, y);

=>

import "primitives/std.lib";
component two(a: 32, b: 32) -> (out: 32) {
  cells {
    R0 = prim std_reg(32);
    const1 = prim std_const(32,2);
  }
  wires {
    group let2<"static"=1> {
      R0.in = const1.out;
      R0.write_en = 1'd1;
      let2[done] = R0.done;
    }
    out = R0.out;
  }
  control {
    let2;
  }
}
component main() -> () {
  cells {
    const0 = prim std_const(32,1);
    y0 = prim std_reg(32);
    z0 = prim std_reg(32);
    two0 = two;
  }
  wires {
    group let0<"static"=1> {
      y0.in = const0.out;
      y0.write_en = 1'd1;
      let0[done] = y0.done;
    }
    group let1 {
      z0.in = two0.out;
      z0.write_en = 1'd1;
      let1[done] = z0.done;
    }
  }
  control {
    seq {
      let0;
      invoke two0(a=y0.out, b=y0.out)();
      let1;
    }
  }
}

There's one TODO that needs some more thinking.

          // TODO(cgyurgyik):
          // Currently, we store the parameters in functionStore as CompVars.
          // However, when reading their values, we are using an out port, which shouldn't be necessary.
          // e.g.,
          //
          // foo(x: 32) -> () {
          //   someRegister.in = x.out // We simply want `x` here.
          // }

Perhaps we should have another store for component port definitions?

@sampsyo
Copy link
Contributor

sampsyo commented Dec 31, 2020

Super cool. Just wanted to endorse @rachitnigam's idea here:

This is also how we handle memory reads which need to be "hoisted" before hand in a different pass (it's called HoistMemoryReads). We can implement a similar pass for applications.

Indeed, since we already have in-Dahlia pass infrastructure to hoist these things into something approximating A-normal form, it makes sense to do it again for invocations. This frees us from needing to invent names for the intermediate state when generating the FuTIL. 👍

@rachitnigam
Copy link
Member

There's one TODO that needs some more thinking.

Can you provide some context about this? I don't remember the details of functionStore etc.

@rachitnigam
Copy link
Member

Ah, I see. The problem is that function arguments are treated as just another let bound variable and the compiler tries to hook up their .out ports.

One possible trick is to make the Store data structure more generic:

case class Store(localVars: Map[CompVar, CompVar], args: Map[CompVar, CompVar])

and change the EVar handling code.

An alternative design could be changing Store to carry this information for each binding:

sealed trait VType
case object LocalVar extends VType
case object Arg extends VType

type Store = Map[CompVar, (CompVar, VType)]

One thing to do regardless is to make Store a case class and define interface methods on it like .get so changes like these are easier to make in the future.


I misunderstood your question initially so I wrote a long explanation of how invoke's interface is supposed to work. Leaving it here to reinforce our understanding:

Let me clarify: There is no way to "return" values from an invoke. Here is how the invoke interface is supposed to be called.

  1. Define a component with at least one port that outputs values after the invoke statement is done executing. This is done by having a "continuous assignment" to an output port of a component. Let's call such output ports "stable"
component bar(in: 32) -> (out: 32) {
  cells { x = prim std_reg(32); }
  wires { 
    group incr_x { /* do something to x */ }
    // This is outside a group which means it will stay activate 
    // even after the `go` signal to bar is low.
    out = x.out;
  }
  control { incr_x; }
}
  1. In the control program, invoke the cell and after the invoke, use the values from one of the "stable" output ports
component main() -> () {
  cells { b = bar; }
  wires {
    group use_out {
       // use the output from b.out **after** invoking the component
       k.in = b.out;
    }
  }
  control {
    // Invoke the statement for stateful update
    invoke(in = y.out)(/* no assignment to out yet */);
    // Use the stateful update
    use_out;
  }

@cgyurgyik
Copy link
Member Author

cgyurgyik commented Jan 1, 2021

Ok great, that's what I'm doing. I initially was using an object that extends an Enumeration; I'll look into sealed traits after I get that functioning.

I think the example of progress I provided above is aligned with what you're saying above. My logic says that if the return value is non-void, add an out port to the component signature with the corresponding bit width.

@rachitnigam
Copy link
Member

rachitnigam commented Jan 1, 2021

Oh interesting! I didn't know about Enumeration. Scala 2 (current version of Scala), annoyingly, does not have proper Enumerations with pattern match exhaustive checking. Instead people use the sealed trait, case object pattern to implement enumerations.

The soon-to-be-released Scala 3 has proper enumerations.

@cgyurgyik cgyurgyik changed the title (WIP) Lower functions to FuTIL's invoke. Lower functions to FuTIL's invoke. Jan 1, 2021
@rachitnigam
Copy link
Member

rachitnigam commented Jan 3, 2021

We need to do a few things before merging:

  • Test that none of the benchmarks break when we do this. They will probably break because we haven't come up with a linking scheme for dahlia's import statements (what does import "math.h" in Dahlia turn into).
  • Add a correctness test in the FuTIL repository to make sure function compilation is working. (futil #346)
  • Emit reasonable error message when one of the arguments is a memory (probably just a NotYetImplemented error like in other places). ed2a006

@cgyurgyik
Copy link
Member Author

def bar(A: ubit<32>[1]): ubit<32> = { }

Corresponding error:

[Error] [Line 1, Column 9] Memory as a parameter is not supported yet.
def bar(A: ubit<32>[1]): ubit<32> = { }
        ^
 This feature is not yet implemented. Please open a feature request for it.

@cgyurgyik
Copy link
Member Author

cgyurgyik commented Jan 6, 2021

For import, I presume this is the same thing as what is discussed by Adrian in Futil #299?

If I'm understanding @sampsyo correctly, math.h would contain Futil components rather than Dahlia functions.

A naive approach would just be the preprocessor way of copy-and-paste into the outputted Futil. However, we'd have to take into account the different types.

For example, if math.h supports some arbitrary component mathOp(x), how do we (cleanly) generalize it to support any input and type?

e.g.

import "math.h"
let a: ubit<32> = mathOp((42 as ubit<32>));
let b: ubit<8> = mathOp((42 as ubit<8>));
// Two different `mathOp` Futil components needed here.

@sampsyo
Copy link
Contributor

sampsyo commented Jan 7, 2021

While this statement obviously requires further discussion, what we want is parametric polymorphism.

@rachitnigam
Copy link
Member

@cgyurgyik once #346 is implemented, can you add support for generating import statements in the Futil backend?

@cgyurgyik
Copy link
Member Author

@cgyurgyik once #346 is implemented, can you add support for generating import statements in the Futil backend?

Sure thing.

@cgyurgyik
Copy link
Member Author

cgyurgyik commented Jan 28, 2021

Supports Futil imports, making two assumptions:

  1. The imported function is always invokeable. (Currently not the case; blocked on Futil #331)
  2. It is always prefixed with the prim keyword, i.e. it is a LibDecl. The end goal is to unify primitives and components, so blockled on Futil #372.

@rachitnigam
Copy link
Member

Fixes #339.

@rachitnigam rachitnigam merged commit 9a2ad3e into master Jan 30, 2021
@rachitnigam rachitnigam deleted the invoke branch January 30, 2021 16:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add FuTIL lowering for invoke.
3 participants