Overview of FLEQ compilation

Though not strictly required, it can be helpful to understand how the FLEQ compilation stage of the Intel® Quantum Compiler (IQC) functions in order to better understand how to best leverage the tools. By design, FLEQ compilation is one stage in the transformation of the LLVM intermediate representation (IR) (generated from the source code) into a hybrid quantum-classical binary executable.

FLEQ compilation happens just after many of the classical code IR optimizations (standard LLVM loop unrolling, inlining, and optimization), but just before most quantum code IR transformations (quantum optimization, native gate decomposition, qubit placement, routing and scheduling). As a result, FLEQ compilation must meet the IR constraints of fully formed QBBs (in IR).

In particular, FLEQ compilation produces quantum logic in the form of a PCOAST graph for each generated QBB, which is passed to a circuit synthesis stage used by the O1 optimization flag. This is why the optimization flags O0 versus O1 have no appreciable effect on the gate sequences generated by FLEQ; they pass through this synthesis step no matter the flag. The PCOAST graph was chosen as it is an efficient, easily manipulated quantum IR, but future versions of FLEQ may use different IRs.

Note that the use of O0 versus O1 does still affect the way quantum kernel functions are processed later on in the compiler, regardless of the use of FLEQ.

Before FLEQ compilation, all the user-facing FLEQ functions are replaced with IQC builtin LLVM intrinsic function equivalents (in other words, they are opaque to core LLVM). The FLEQ builtins are then processed by FLEQ compilation and ultimately removed so that none of these builtins are present in the IR after the FLEQ compilation stage.

To begin FLEQ compilation, the IQC identifies each evaluation call (eval_hold or eval_release) and from it builds a FLEQ Evaluation Graph or FLEQ graph. Each QExpr function in the dependency of the evaluation call is represented as a node in the FLEQ graph with edges to QExpr arguments to that function.

From the FLEQ graph, the quantum logic is then built in the place of the call through three primary steps:

  1. Validation and Conditioning. Each user-defined function in the FLEQ graph is checked to verify that it satisfies the conditions outlined in Basic concepts. The function is then “conditioned”, i.e. manipulated so as to be amenable to the rest of the process. As discussed in Ordering of classical and quantum operations, this identifies three sections, similar to the core IQC: the pre-quantum section, the quantum section and the post-quantum section. For functions which do not use imperative gate calls (i.e. not generated by convert or use of this_as_expr), the post-quantum section is trivial.

  2. Inlining and Unrolling. All user-defined functions are inlined in the place of the evaluation call. The structure of the FLEQ graph indicates recursion through loops in the graph. So, inlining is performed in two steps.

    1. A shallow inlining is applied bottom-up on functions that do not recurse, and do not have the PROTECT attribute.

    2. The loop unrolling step is performed top-down. In this step, recursive and PROTECT-ed functions are inlined and branching nodes are resolved if possible. A counter is added for every recursing function to keep track of the number of times they have been inlined to prevent infinite looping. This recursion limit can be adjusted via command-line flags (see Recursion).

  3. Building of Branching and Quantum Logic. The previous inlining step guarantees that the FLEQ graph has no remaining user-defined functions and contains no cycles i.e is a directed acyclic graph (DAG). As a result, it is possible to obtain a topological sort to the nodes of the FLEQ graph. The branching logic is built in LLVM IR top-down and to each (initially empty) QBB, the process assigns a list of leaf nodes that QBB is dependent on. Then, going bottom-up, the PCOAST graph for each node and each QBB is progressively built. This build order determines the order of the print buffer (see Printing). The final assignment of QBBs to PCOAST graphs for the evaluation call is then stored to be passed to the synthesis step later in IQC compilation.

The final step of the process is a clean-up phase where any remain calls to FLEQ functions are removed. This includes additional DataList and QList intrinsics which the process attempts to resolve and replace. The process of building evaluation calls often leaves behind unused and even ill-defined calls to these intrinsics, especially for recursing functions. This is because the inlining step must fully insert the recursing function even when it hit the recursion guard. Once the recursion guard condition is resolved, the FLEQ graph prunes off the unused exit, but the IR remains even if it is unused and ill-formed. A common example is indexing into an empty QList when the QList’s size is used as the exit condition. In its current form, the clean-up phase has no way to distinguish between these legal but ill-defined remnants of the evaluation build process, or illegal and ill-defined uses of the QList or DataList features generated by the user. For this reason, FLEQ compilation does not exit when such ill-formed cases are found but instead they are replaced by undefined behavior (as handled by core LLVM). Instead, warnings are thrown, but by default, these warnings are suppressed. These warning can be seen by setting the command-line flag:

-F verbose-cleanup=true

Also, the inlining process often generates trivial branching, where an LLVM Basic Block unconditionally branches to the next Basic Block and is the unique predecessor to that Basic Block. This is by virtue of the conditioning and aggregation of the pre- and post- quantum sections around newly generated QBBs. By default, the clean-up process collapses this trivial branching, but this can be stopped via the command-line flag:

-F bb-cleanup=false

For the advanced user, this generates a textual IR which is cluttered but can provide insight into the building of evaluation calls useful for debugging.