calyx
calyx copied to clipboard
Memories with non-combinational reads
Update by @rachitnigam:
The memory primitives in Calyx's standard library allow for combinational reads. Different FPGAs and ASIC processes might allow for different kinds of memories, some of which might not allow for combinational reads. We should attempt to support those by either adding a new primitive, or updating the memory primitives to use a read_done
signal.
Original issue by @sgpthomas:
Change the memories and Dahlia Futil backend to use the read_done
signal to know that a value is ready to be read.
To reup this, our current memories are a lie. Combinational reads from memories don't exist. We should think more carefully about what the correct memory interface is. To elaborate the problem a little, the obvious thing to do is give memories a normal Calyx go/done interface. But it's not entirely obvious what this should look like because there are two distinct actions a memory can perform: reads and writes. When you set go
high, which one should the memory perform?
Imagine some interface like:
primitive mem(go: 1, addr: 32, write_data: 32) -> (read_data: 32, done: 1);
and a read
group read {
mem.go = 1;
mem.addr = 0x00;
read_value.in = mem.done ? mem.read_data;
read[done] = mem.done;
}
How does mem distinguish this use from:
group write {
mem.go = 1;
mem.addr = 0x00;
mem.write_data = 0x10;
read[done] = mem.done;
}
Ports always have values, so we can't look at whether write_data
is written to to decide whether to do a read or a write.
A possible answer is to have some extra "type" port which the component uses to decide, so for example
group A {
mem.go = 1;
mem.perform_write = 1; // <- go = 1 & perform_write = 1 means do write
...
}
This is a fine solution for now, but it seems like the thing that's really going on here is there are two seperate ways to call this component that each should get their own go/done
interface. So this would look something like:
group read {
mem.read.go = 1;
mem.addr = 0x00;
read_value.in = mem.read.done ? mem.read_data;
read[done] = mem.read.done;
}
and similarly for writes. However, this raises the question: what happens if you use both interfaces at once? Like:
group both {
mem.read.go = 1;
mem.write.go = 1;
...
Is this valid? For memories, maybe not (though maybe dual ported memories might want an interface like this?), but for an AXI interface this seems very natural (maybe necessary even). There's an idea of simultaneous points of control flow through a single component here that also maybe generalizes to pipelines. Obviously more thought needs to be given to this, but it's an intriguing idea.
Aside from the practical issue of supporting more realistic memories, it's an intriguing idea to make memories more "normal," i.e., to make it so using a memory is more like using any other component. Or to put it another way, what would it take to make it possible to invoke
a memory to perform a read or a write?
All of @sgpthomas's suggestions seem like possibilities for how to do that!
primitive mem(go: 1, addr: 32, write_data: 32) -> (read_data: 32, done: 1);
Why not instead an interface for dual-ported memories like this:
component mem_d1(go: 1, read_addr0: 32, write_addr0: 32, write_data: 32, write_en: 1, read_en: 1) -> (read_data: 32, done: 1) {
...
}
If you write it as a Calyx component, you can have the read and write execute in parallel. Same behavior can be executed in SV modules as well. FWIW, I think this is essentially the idea you get at in the latter half of your post.
And then to follow on @sampsyo 's comment, if a user wants to read and write to the memory,
invoke mem_d1(write_en=1'd1, read_en=1'd1, ...)(...);
This is how I model being able to write and search with the TCAM. One difference I foresee with invoke
ing memories is that we now have to save it to a register before using it in another group.
Relevant paper about BaseJump STL from Micheal Taylor’s group: http://michaeltaylor.org/papers/Taylor_DAC_BaseJump_STL_2018.pdf
Contains a section on portable memory shims. BaseJump might be interested for @sgpthomas effort to run things on FPGAs too.
@sgpthomas did we end up dealing with this for #492?
This was not dealt with there. You can get brams that have combinational reads and so our memory model is fine. I left this open though because there's a lot of room to explore different kinds of memories / memory interfaces that have different performance characteristics.