-
Notifications
You must be signed in to change notification settings - Fork 42
[FTD] Introduce Single-Occurrence MUX Trees for Suppression #613
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
base: main
Are you sure you want to change the base?
Conversation
|
Thanks, Yuan! As discussed in our meeting, let's review more literature to determine whether we can replace your Read-Once BDD with the standard Reduced-Order BDD. After that, I will start reviewing the PR. :) |
|
@AyaElAkhras This is ready for review. |
|
I haven’t merged with the main branch yet because my changes would remove the However, that behavior was based on the old approach for constructing circuits from BDDs, and I’ve now replaced all the related functions. I have to figure out what that actually means before merging. |
| /// Variables in `varOrder` that do not appear in the expression are ignored. | ||
| /// Returns `success()` on success, or `failure()` if the input is invalid. | ||
| /// | ||
| /// This is a simple method that assumes each variable appears only once in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this comment somewhat ambiguous. The function itself creates the ROBDD, so it doesn’t make any assumptions about it. Did you mean something like “the function constructs an ROBDD structure in which each node is unique, with every node representing a single variable and no two nodes representing the same variable”?
I also find the connection to “whether a basic block in the CFG can be reached” unclear and in need of further elaboration. You might want to refer to the properties of an ROBDD—specifically, that it contains no duplicate equivalent subtrees and no two nodes with identical children. By construction, this ensures that no two nodes can correspond to the same variable (i.e., CFG basic block), assuming it’s not possible for two nodes representing the same variable to have different children, which is a valid assumption given that we start from CFGs? Does that interpretation make sense, or am I misunderstanding your point?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right that a general ROBDD allows multiple nodes for the same variable (representing different sub-functions). However, for this specific use case (CFG reachability analysis), the structure is simpler.
I implemented a lightweight version where each variable in the order maps to exactly one node in the BDD structure (or is skipped). This exploits the property that in our CFG analysis, we don't need the full expressiveness of a general ROBDD, allowing for a more efficient, linear-sized construction. I will clarify this 'single-node-per-variable' design constraint in the comments.
| // Minimize the whole expression once before starting. | ||
| BoolExpression *rootExpr = expr->boolMinimize(); | ||
|
|
||
| // If the expression itself is constant, build only two terminals. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused here: if the expression is a constant, why do you create two nodes instead of just one?
And, why do you write to trueSucc and falseSucc? Shouldn't it be a single node with no predecessors or successors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch regarding the successors. Terminals indeed act as leaves, so I've updated the code to set their successors to an invalid marker (UINT_MAX) instead of creating self-loops.
Regarding the two nodes: While strictly speaking only one node is reachable for a constant expression, I instantiate both 0 and 1 terminals to maintain internal consistency for the zeroIndex and oneIndex members of the ROBDD class. This ensures that helper methods accessing these terminals always find valid nodes, avoiding edge-case logic elsewhere.
I think it will be easier to discuss this when we meet, so please bring it up in our next meeting. |
Thanks @QinYuan2000! I did an initial round and left a few comments. I still plan to do another round tomorrow because I did not get to go through everything now. |
AyaElAkhras
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A tiny second round :)
I still need to review buildMuxTree and then should be done. Will try to do it in the next few hours...
| continue; | ||
|
|
||
| // Abort if we accidentally reach the global terminals. | ||
| if (u == zeroIndex || u == oneIndex) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this redundant with the u >= nodes.size() done above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it is not redundant. In my implementation, the global terminals (zeroIndex and oneIndex) are stored as the last two elements of the nodes vector, so they are valid indices and satisfy u < nodes.size().
| zeroIndex = n; | ||
| oneIndex = n + 1; | ||
| for (unsigned i = 0; i < n; ++i) | ||
| nodes[i] = BDDNode{order[i], zeroIndex, oneIndex, {}}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worth mentioning that here you just create a new node for every entry in order and that you initialize the trueSucc and falseSucc with some dummy values that you later overwrite in expandFrom.
Why do you even initialize with zeroIndex and oneIndex? Why not with -1, for instance? This is also related to my earlier comment about what should be the value of trueSucc and falseSucc for leaf nodes. Why do not we use -1 for all these cases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Using zeroIndex/oneIndex as default values was indeed misleading as it implied a default connectivity that doesn't exist.
I have updated the initialization to use UINT_MAX (acting as -1 for unsigned) for both the temporary internal node values and the terminal successors. This explicitly marks them as 'uninitialized' or 'invalid' until they are properly set during expansion. I also added a comment clarifying that these internal node values are placeholders.
| // Wrap the two vertices in the pair into InputSpec objects. | ||
| InputSpec A = nodeToSpec(u), B = nodeToSpec(v); | ||
|
|
||
| // Direct edge -> successor becomes constant. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we guaranteed that u and v will always be directly connected?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not necessarily. u and v might not be directly connected. I added a comment to clarify that this if-else block specifically handles the direct edge case. If there is no direct edge, this block is skipped.
| chosenP = predU[iu]; | ||
| ++iu; | ||
| ++iv; | ||
| } else if (predU[iu] < predV[iv]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check the coding style conventions since they omit braces from bodies made up of a single line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the comment. I double-checked the LLVM Coding Standards, which state that we must avoid 'Inconsistent bracing within if/else if/else chains (if one block requires braces, all must)'.
| std::vector<MuxSpec> muxChain; | ||
| muxChain.reserve(pairs.size()); | ||
|
|
||
| for (auto [u, v] : pairs) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe each element in pairs is a path-covering pair at a different point in the recursion, correct? In other words, pairs does not contain all alternative path-covering pairs for the global ROBDD; rather, it includes the path-covering pairs for the global ROBDD as well as those from other recursive regions.
If my understanding is correct, I would suggest adding a brief comment to clarify this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are absolutely correct. The pairs vector represents a topological sequence of path-covering pairs within the current subgraph. They are not alternatives but sequential stages used to build a cascaded MUX chain, where each MUX feeds the selection signal of the next.
I have added a comment to clarify that this loop constructs a sequential chain based on these sorted pairs.
Motivation
This PR redesigns the suppression logic used in the Fast Token Delivery (FTD) flow.
Previously, the suppression circuit was generated from a Shannon-expansion–based MUX tree,
where the same condition variable could appear multiple times along different paths.
This duplication required additional “suppression” steps to eliminate redundant conditions.
We introduce a new Single-Occurrence MUX Tree built from a read-once Reduced Ordered Binary
Decision Diagram (ROBDD):
A minimized BDD where each boolean variable is represented by exactly one node (read-once property).
It is constructed from an already minimized
BoolExpressionunder a user-provided variable order.Using the ROBDD, we generate a MUX tree in which each condition variable
appears exactly once. The resulting circuit requires only a single suppression step
for each token delivery, simplifying both the FTD logic and the generated hardware.
Implementation
BDD.{h,cpp}to build and analyze ROBDDs.BDD subgraph to guide MUX tree construction.
FtdImplementationto build the new single-occurrence MUX tree directly from theROBDD, avoiding repeated conditions and redundant suppression.