Skip to content

Conversation

@QinYuan2000
Copy link
Collaborator

@QinYuan2000 QinYuan2000 commented Oct 7, 2025

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)
:

  • The ROBDD
    A minimized BDD where each boolean variable is represented by exactly one node (read-once property).
    It is constructed from an already minimized BoolExpression under a user-provided variable order.
  • Single-Occurrence MUX Tree
    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

  • Added a class with several functions in BDD.{h,cpp} to build and analyze ROBDDs.
  • Implemented enumeration of vertex pairs that cover all the paths from the root to either terminal in the
    BDD subgraph to guide MUX tree construction.
  • Updated FtdImplementation to build the new single-occurrence MUX tree directly from the
    ROBDD, avoiding repeated conditions and redundant suppression.

@QinYuan2000 QinYuan2000 marked this pull request as draft October 7, 2025 01:01
@QinYuan2000 QinYuan2000 changed the title [Experimental][FTD] Add Read-Once BDD builder and Single-Occurrence MUX Tree for Suppression [Experimental][FTD] Introduce Single-Occurrence MUX Trees for Suppression Oct 7, 2025
@QinYuan2000 QinYuan2000 marked this pull request as ready for review October 7, 2025 23:33
@QinYuan2000 QinYuan2000 changed the title [Experimental][FTD] Introduce Single-Occurrence MUX Trees for Suppression [FTD] Introduce Single-Occurrence MUX Trees for Suppression Oct 22, 2025
@AyaElAkhras
Copy link
Member

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. :)

@QinYuan2000
Copy link
Collaborator Author

@AyaElAkhras This is ready for review.
In this update, I’ve merged the ROBDD class into BDD.h. For simplicity, I kept the original lightweight ROBDD construction approach, instead of the canonical one that starts from an OBDD and merges identical subgraphs.

@QinYuan2000
Copy link
Collaborator Author

I haven’t merged with the main branch yet because my changes would remove the channelification logic introduced in #604.
According to that update,

In addition, in boolVariableToCircuit, conditions generated for gates with multiple exiting blocks do not require channelification.

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
Copy link
Member

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?

Copy link
Collaborator Author

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.
Copy link
Member

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?

Copy link
Collaborator Author

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.

@AyaElAkhras
Copy link
Member

I haven’t merged with the main branch yet because my changes would remove the channelification logic introduced in #604. According to that update,

In addition, in boolVariableToCircuit, conditions generated for gates with multiple exiting blocks do not require channelification.

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.

I think it will be easier to discuss this when we meet, so please bring it up in our next meeting.

@AyaElAkhras
Copy link
Member

@AyaElAkhras This is ready for review. In this update, I’ve merged the ROBDD class into BDD.h. For simplicity, I kept the original lightweight ROBDD construction approach, instead of the canonical one that starts from an OBDD and merges identical subgraphs.

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.

Copy link
Member

@AyaElAkhras AyaElAkhras left a 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) {
Copy link
Member

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?

Copy link
Collaborator Author

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, {}};
Copy link
Member

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?

Copy link
Collaborator Author

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.
Copy link
Member

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?

Copy link
Collaborator Author

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]) {
Copy link
Member

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.

Copy link
Collaborator Author

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) {
Copy link
Member

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.

Copy link
Collaborator Author

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.

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.

2 participants