Barriers and binding¶
Quantum basic blocks and barriers¶
Quantum programming languages often provide circuit barriers which prevent
optimization across a boundary. That is, a barrier guarantees that all gates
that come before it are are executed before any of the gates that come after. In
the Intel® Quantum SDK, every two top-level quantum_kernel
function
calls or FLEQ evaluation calls are separated implicitly by a barrier;
in other words, barriers separate any two
QBBs. Each QBB is a standalone block of quantum logic, and does not know a priori
what quantum logic is executed before or after it. As a result, each QBB must be
implicitly bookended by circuit barriers.
Consider an example of two separate evaluation calls to a unitary not gate.
qexpr::eval_hold(qexpr::_X(q)); // 1 QBB with 1 gate
qexpr::eval_hold(qexpr::_X(q)); // 1 QBB with 1 gate
These two evaluation calls produce two separate QBBs, each containing one gate. If, however, these quantum kernel expressions were joined together and executed in a single evaluation call, and thus a single QBB, the QBB would be optimized by the compiler, canceling out both gates and producing an empty QBB with zero gates.
qexpr::eval_hold(qexpr::_X(q) + qexpr::_X(q)); // 1 optimized QBB with 0 gates
Barriers in the SDK have the additional property that measurement outcomes
executed before the barrier are not returned from the backend to the classical
runtime (i.e. accessible by the program) until after the barrier. In this sense,
separate quantum_kernel
function calls or FLEQ evaluation calls also act as a
“measurement return barrier”. For example, a measurement in a quantum kernel
expression joined with a conditional cIf
statement (see Branching) will
not correctly propagate the measurement result to the conditional because they
occur in the same QBB. Consider the following example:
bool b = true;
// WRONG: Always executes H(q2) because the measurement result of q1
// is not written to b until the end of the current QBB.
qexpr::eval_hold(qexpr::_MeasZ(q1, b)
+
qexpr::cIf(b, qexpr::_H(q2), qexpr::identity()));
On the other hand, if the measurement and conditional calls occur in separate evaluation calls, there is an implicit barrier between them, which will produce correct results.
bool b = true;
qexpr::eval_hold(qexpr::_MeasZ(q1, b));
// Measurement results are written to b a the end of the above QBB,
// and so are available by the start of the next QBB.
qexpr::eval_hold(qexpr::cIf(b, qexpr::_H(q2), qexpr::identity());
Bind¶
While separate quantum_kernel
functions and evaluation calls act as a
barrier, it can also be useful for programmers to insert their own barriers
within a single quantum kernel expression, for the purposes of expressivity,
debugging, or branching control. This is provided by the qexpr::bind
functionality, which is also referred to as a “barriered join”. A bind
combines
two quantum kernel expressions much in the same way as an ordinary join
, but
acts as a barrier between them. Under the hood, the bind
function produces
two separate QBBs, one for each quantum kernel expression, which implicitly imposes
the barrier.
There are four variations on the bind
method.
QExpr qexpr::bind(QExpr e1, QExpr e2)
Returns the barriered join of
e1
withe2
in sequential order, meaning that it evaluatese1
followed bye2
.
e1 << e2
Shorthand for
qexpr::bind(e1, e2)
. Analogous toe1 + e2
.
e1 >> e2
Binds the quantum kernel expressions in composition order, meaning that it evaluates
e2
followed bye1
. Analogous toe1 * e2
.
QExpr qexpr::fence(QExpr e)
Shorthand for
qexpr::bind(qexpr::identity(), e)
.
To remember the difference between left shift e1 << e2
and right shift
e1 >> e2
operators for quantum kernel expressions, recall that sequential
composition aligns with the left shift operator for stream output e.g.
std::cout << "Hello " << "World!"
where “Hello “ and “World!” are composed
in sequential order.
A bind
call is logically equivalent to a join
call in nearly every case except
when acting as a measurement barrier. However,
whereas the exclusive use of join
ensures that at runtime, exactly one QBB is issued
per evaluation call, even with unresolved branching (see Branching), each call to bind
increases the number of QBBs issued at runtime per evaluation call by one (for that branch) with the order of
the issuing as specified by the ordering or directionality of the bind
. Because each QBB
is bookened by circuit barriers and the end barrier is also a measurement return barrier,
bind
is the direct analog of the barrier for QExpr
. There are three common reasons
to use bind
over join
:
Debugging. As discussed in Printing, FLEQ compilation uses the PCOAST graph as a quantum IR. Due to its level of abstraction, it can be difficult to determine the gate sequence used to form the PCOAST graph representation, obscuring errors in the gate logic.
bind
can be used to prevent some of the implicit optimizations to better debug gate logic errors. When convinced of the logical correctness, a programmer can change thebind
’s tojoin
’s to regain the optimization and efficiency.Branching on measurement outcomes. As discussed in Quantum basic blocks and barriers, barriers can be necessary to separate measurements from conditionals that depend on those measurements, like a
cIf
. For example, consider a qubit reset function (logically equivalent to_PrepZ
) which measures the qubit and conditioned on the outcome, applies an_X
gate to thetrue
case:PROTECT QExpr resetQubit(qbit &q) { cbit c = false; return qexpr::_MeasZ(q, c) << qexpr::cIf(c, qexpr::_X(q), qexpr::identity()); } PROTECT QExpr NOT_A_RESET(qbit &q) { cbit c = false; return qexpr::_MeasZ(q, c) + qexpr::cIf(c, qexpr::_X(q), qexpr::identity()); }
The first function uses
bind
to fuse the measurement to the conditional, thus ensuring the measurement is performed and stored toc
before evaluating thecIf
. The second case does not usebind
and is logically equivalent to only the measurement sincec
remains its initial value offalse
when thecIf
is evaluated.Unresolved branching control. As discussed in Branching, FLEQ compilation handles unresolved branching (in the absence of
bind
) by combinatorially building all possible QBBs and inserting classical branching to select exactly one at runtime. This means that everyjoin
between two unresolvedcIf
calls doubles the number of QBBs. This can cause an exponential increase in the number and complexity of that branching, although FLEQ compilation performs admirably in generating these branches (on-the-order of thousands without a prohibitive increase in compile-time and binary size). However, this exponential increase eventually will become problematic if not controlled. One way to avoid this is by replacingjoin
’s withbind
’s. By issuing multiple QBBs per evaluation, FLEQ compilation no longer needs to generate every unique combination, but only those bookended by thebind
.
Each core FLEQ function has a different distributive or associative behavior with regards to bind
:
qexp::control
,qexpr::power
,qexpr::printQuantumLogic
, andqexpr::printDataList
all distribute overbind
, i.e.f(e1 << e2)
is equivalent tof(e1) << f(e2)
forf
being one of these four functions.All of these are intuitive except for
power
. Consider thatpower(2, e1 << e2)
is equivalent topower(2, e1) << power(2, e2)
– that is,e1 + e1 << e2 + e2
– and note1 << e2 + e1 << e2
. This is a known issue which can be overcome by using a recursive version of power:QExpr recursivePower(const int n, QExpr e) { return qexpr::cIf(n == 0, qexpr::identity(), qexpr::cIf(n > 0, e + recursivePower(n-1, e), qexpr::invert(e) + recursivePower(n+1, e) )); }
qexpr::invert
distributes in the analogous way toqexpr::join
, i.e.qexpr::invert(e1 << e2)
is equivalent toqexpr::invert(e2) << qexpr::invert(e1)
. Note, this inversion of ordering also holds for classical instructions attached to the quantum kernel expressionse1
ande2
; see Ordering of classical and quantum operations.
qexpr::join
is associative withbind
, not distributive. For example:
e1 + (e2 << e3)
is equivalent to(e1 + e2) << e3
(e1 << e2) + e3
is equivalent toe1 << (e2 + e3)
e1 * (e2 << e3)
is equivalent toe2 << (e1 * e3)
qexpr::cIf
does not distribute over or associate withbind
. If the conditional is resolved, then just as described, the resolved branchQExpr
,bind
-ed or not, is returned. If the conditional is not resolved, then thebind
behavior is dependent on the branch as expected. For example,cIf(b, e1, e2 << e3)
generates atrue
branch which only represents the QBB(s) fore1
whereas thefalse
branch represents the consecutive QBB(s) fore2
followed by those fore3
.bind
effectively distributes overcIf
, i.e.e1 << cIf(b, e2, e3)
is logically equivalent tocIf(b, e1 << e2, e1 << e3)
but the branching in the IR will be different as the former represents the QBB(s) ofe1
which then branch betweene2
ande3
whereas the latter branches to one of two copies ofe1
which then branches to eithere2
ore3
based on the condition.
Custom functions inherent their behavior with respects to bind
from their constituent core function calls.