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

Functions vs Macros #140

Open
rachitnigam opened this issue Mar 17, 2019 · 4 comments
Open

Functions vs Macros #140

rachitnigam opened this issue Mar 17, 2019 · 4 comments
Labels
Discussion Needed Language features that need more discussion before implementation

Comments

@rachitnigam
Copy link
Member

Macros

In the current semantics of Fuse, there are no true "functions". All defs in the semantics should be thought of simple macros that just get expanded at call locations. This notion corresponds to having
k copies of RTL if there are k calls to a functions. Furthermore, an unroll context also increases the number of RTL blocks for a functions.

For example, in this code:

void foo(...) { ... }
foo(...);
for (...) unroll k { foo(...) }
foo(...);

there should be exactly k + 2 copies of foo in the hardware design.

Functions

On the other hand, a true function in Fuse would represent exactly one RTL block regardless of the number of syntactic calls. This has a few implication for the semantics.

  1. This code shouldn't work: foo(...); foo(...) since the same RTL block is being sent two signals in parallel.
  2. For the same reason, a call to foo(...) inside an unrolled loop is also incorrect, because the same RTL block is being invoked k times in a single cycle.

Obviously, this notion of functions is very restrictive.

Vivado's default

According to this SDAccel page, syntactic functions have the following defaults:

By default:

  • Functions remain as separate hierarchy blocks in the RTL.
  • All instances of a function, at the same level of hierarchy, make use of a single RTL implementation (block).

The notion of a hierarchy here is fuzzy to me (maybe @sa2257 can clarify). However, this seems to imply that all functions in emitted C++ code are true functions according to our definition.

Things to consider

We have to consider a few things before we add true functions to the language:

  1. The current implementation needs to change to emit the inline pragma for all functions.
  2. We need to understand the Function instantiate pragma and see if we want to use it.
  3. We have to figure out the interaction of the dataflow pragma and the pipelining pragma and see how they interact with true functions at the C++ level.

All of these considerations will inform the design of fuse-to-rtl in the future. I suggest that for the first paper, we only think about defs as macros and not have any support for actual functions till Fuse 2.0.

@rachitnigam rachitnigam added the Discussion Needed Language features that need more discussion before implementation label Mar 17, 2019
@sampsyo
Copy link
Contributor

sampsyo commented Mar 17, 2019

Great writeup! Thanks for crafting a ticket for this frequent discussion point. Here are a few quick notes:

  • I agree that we should stick with the "inline everything" semantics (i.e., all defs behave macro-like) for now. This strategy seems like the most useful one; separate hardware modules strike me as necessary in larger designs but less useful in small- to medium-scale accelerators.
  • The analogy between our current defs and macros is not quite perfect: unlike a typical macro, our defs have dynamic arguments. Imagining typical macro system, you might write defmacro add(x, y) => x + y and call it like add(50 - 8, 2) to generate the syntax (50 - 8) + 2. The arguments to such a macro are static, i.e., syntax fragments, and the macro builds up larger syntax out of the smaller syntax pieces. In our language, however, you don't pass syntax into our "functions"—you pass in memory resources and possibly wires (scalar values). These don't quite resemble argument passing in traditional languages, but it's also not the same as passing in static syntax. That, I think, represents the difference between macros and inlined functions—where we are currently somewhat closer to the latter.
  • In a typical PL-person reaction, my mind immediately goes to thinking about distilling the distinction down to two orthogonal, compositional concepts. One possible strategy would be to keep our defs fully inlined and invent a new, separate concept for defining separate hardware modules that communicate. This new thing would not contain any code of its own; it would just wrap a def to provide shareable, synchronized access to the underlying hardware. Seems like a fun topic for deep thinking!

@rachitnigam
Copy link
Member Author

Spatial inlines functions. See under "Using Functions".

@rachitnigam
Copy link
Member Author

rachitnigam commented Aug 25, 2019

Affine functions

Real reusable functions which specify how many instances of RTL blocks implement them. For example (made up syntax):

def foo{N}(...)

creates N instances of the function foo (using the allocation pragma). Next, calls to foo consume
one instance of foo:

foo(a,b); foo(a, b); // two copies consumed

And the reasoning extends to unrolled loops:

for (...) unroll 4 {
  foo(a, b); // 4 copies consumed
}

--- regenerates copies of foo. This claim is slightly sketchy because function calls may take multiple cycles.

foo(a, b); // N copies available
---
foo(a, b); // N copies available

The type theoretic ideas might be related to graded modalities (I might be wrong about this, but the idea of affine resources consumable a finite number of things exists).

Bonus implementation points: Multi ported memories will already need a similar style of reasoning to work.

DSE

Allowing source level reasoning for these resources will help with area-efficiency tradeoffs and maybe we can eventually infer the N in the polymorphism extension.

@sampsyo
Copy link
Contributor

sampsyo commented Aug 25, 2019

Neat! I do think time steps should indeed replenish function resources, as they do memory banks—the reason being that function calls (unless we do something drastic) are synchronous, i.e., a function call waits for the entire function to finish. If we allowed async calls, this would get more complicated—the function resource would probably need to stay consumed past the time step and only get released when synchronizing the result (like forcing a future).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Discussion Needed Language features that need more discussion before implementation
Projects
None yet
Development

No branches or pull requests

2 participants