Summary
adjoint-lowering aborts with an LLVM assertion (SIGABRT) instead of emitting a diagnostic, when applied to a module containing non-trivial adjoint regions.
In a reduced case the same pass correctly reports error: Unhandled operation in adjoint region, so the abort looks like a missing bail-out path rather than an intended rejection.
Crash
catalyst failed with error code -6:
Assertion failed: (result && "expected 'from' to be contained within the map"),
function lookup, file IRMapping.h, line 74.
Stack dump:
9 (anonymous namespace)::AdjointGenerator::generateImpl(mlir::Region&, mlir::OpBuilder&) + 5728
10 catalyst::quantum::generateAdjointReversePass(mlir::Region&, mlir::OpBuilder&, mlir::IRMapping&, catalyst::quantum::QuantumCache&) + 40
11 (anonymous namespace)::AdjointSingleOpRewritePattern::matchAndRewrite(catalyst::quantum::AdjointOp, mlir::PatternRewriter&) const + 748
16 catalyst::quantum::AdjointLoweringPass::runOnOperation() + 644
The pipeline is
Quantum(canonicalize;builtin.module(apply-transform-sequence);inline-nested-module)
with the QNode decorated by @qp.transform(pass_name="adjoint-lowering").
Reproducer
The circuit is qp.TrotterVibronic (no4_anthracene, 16 grid points, 1 Trotter step) decomposed with
catalyst_decompose(target_gates={"TemporaryAND": 500, "Adjoint(TemporaryAND)": 1} | Clifford | ctrl-flow | measurements),
then adjoint-lowering → to-ppr → ppr-to-ppm, evaluated via qp.specs(circuit, level="all-mlir")().
TrotterVibronic emits lazy adjoint(...) wrappers around subroutines that themselves contain
for_loop/cond regions (adjoint(SignedOutSquare(...)), adjoint(SignedOutMultiplier(...)),
adjoint(_aqft(...)) in pennylane/templates/subroutines/time_evolution/trotter_vibronic.py),
so the adjoint regions contain nested control flow.
I can share the full workflow script if useful; it is not yet reduced to a standalone snippet, since a
smaller hand-written case takes the clean-diagnostic path instead:
import pennylane as qp
from catalyst.device.decomposition import catalyst_decompose
qp.decomposition.enable_graph()
gates = {"TemporaryAND": 500, "Hadamard": 10, "S": 10, "Adjoint(S)": 10, "CNOT": 50,
"PauliX": 1, "GlobalPhase": 0, "ForLoop": 1, "Cond": 1, "HybridAdjoint": 1,
"MidCircuitMeasure": 1}
pipelines = [("Quantum", ["canonicalize",
"builtin.module(apply-transform-sequence)",
"inline-nested-module"])]
@qp.qjit(capture=False, target="mlir", pipelines=pipelines)
@qp.transform(pass_name="adjoint-lowering")
@catalyst_decompose(capabilities=None, target_gates=gates)
@qp.qnode(qp.device("null.qubit", wires=4))
def circuit():
def body():
@qp.for_loop(2)
def loop(i):
qp.TemporaryAND([0, 1, 2])
qp.adjoint(qp.TemporaryAND([0, 1, 2]))
loop()
qp.adjoint(body)()
return qp.expval(qp.Z(0))
qp.specs(circuit, level="all-mlir")()
This one fails cleanly with
error: Unhandled operation in adjoint region
%36:2 = "quantum.measure"(%35) : (!quantum.bit) -> (i1, !quantum.bit)
(reaching the pass requires relaxing the frontend is_invertible check, since
verify_operations otherwise rejects the measurement first).
Expected
adjoint-lowering should emit a diagnostic (like Unhandled operation in adjoint region) rather than
tripping the IRMapping assertion and aborting the compiler.
Related question
Is measurement-based uncompute inside adjoint regions intended to be supported at all? Catalyst
currently rejects it in three separate places:
_check_no_measurements — Mid-circuit measurements cannot be used within an adjoint() or ctrl() region.
(catalyst/api_extensions/quantum_operators.py:911)
verify_operations / _inv_op_checker — MidCircuitMeasure is not invertible on 'null.qubit' device
adjoint-lowering — Unhandled operation in adjoint region on quantum.measure
This matters for TemporaryAND (elbow) uncompute, whose cheap form is measurement-based. When such an
elbow ends up inside an adjoint region the only alternatives are the unitary Clifford+T uncompute
(worse T counts) or keeping Adjoint(TemporaryAND) opaque, which lowers to a Toffoli and is then
rejected by to-ppr (Unsupported gate for PBC conversion). Guidance on the intended path here would
be very welcome.
Versions
- pennylane
0.46.0.dev (editable, Xanadu/pennylane)
- pennylane-catalyst (editable,
Xanadu/catalyst)
- macOS arm64, Python 3.13
Summary
adjoint-loweringaborts with an LLVM assertion (SIGABRT) instead of emitting a diagnostic, when applied to a module containing non-trivial adjoint regions.In a reduced case the same pass correctly reports
error: Unhandled operation in adjoint region, so the abort looks like a missing bail-out path rather than an intended rejection.Crash
The pipeline is
Quantum(canonicalize;builtin.module(apply-transform-sequence);inline-nested-module)with the QNode decorated by
@qp.transform(pass_name="adjoint-lowering").Reproducer
The circuit is
qp.TrotterVibronic(no4_anthracene, 16 grid points, 1 Trotter step) decomposed withcatalyst_decompose(target_gates={"TemporaryAND": 500, "Adjoint(TemporaryAND)": 1} | Clifford | ctrl-flow | measurements),then
adjoint-lowering→to-ppr→ppr-to-ppm, evaluated viaqp.specs(circuit, level="all-mlir")().TrotterVibronicemits lazyadjoint(...)wrappers around subroutines that themselves containfor_loop/condregions (adjoint(SignedOutSquare(...)),adjoint(SignedOutMultiplier(...)),adjoint(_aqft(...))inpennylane/templates/subroutines/time_evolution/trotter_vibronic.py),so the adjoint regions contain nested control flow.
I can share the full workflow script if useful; it is not yet reduced to a standalone snippet, since a
smaller hand-written case takes the clean-diagnostic path instead:
This one fails cleanly with
(reaching the pass requires relaxing the frontend
is_invertiblecheck, sinceverify_operationsotherwise rejects the measurement first).Expected
adjoint-loweringshould emit a diagnostic (likeUnhandled operation in adjoint region) rather thantripping the
IRMappingassertion and aborting the compiler.Related question
Is measurement-based uncompute inside adjoint regions intended to be supported at all? Catalyst
currently rejects it in three separate places:
_check_no_measurements—Mid-circuit measurements cannot be used within an adjoint() or ctrl() region.(
catalyst/api_extensions/quantum_operators.py:911)verify_operations/_inv_op_checker—MidCircuitMeasure is not invertible on 'null.qubit' deviceadjoint-lowering—Unhandled operation in adjoint regiononquantum.measureThis matters for
TemporaryAND(elbow) uncompute, whose cheap form is measurement-based. When such anelbow ends up inside an adjoint region the only alternatives are the unitary Clifford+T uncompute
(worse T counts) or keeping
Adjoint(TemporaryAND)opaque, which lowers to aToffoliand is thenrejected by
to-ppr(Unsupported gate for PBC conversion). Guidance on the intended path here wouldbe very welcome.
Versions
0.46.0.dev(editable,Xanadu/pennylane)Xanadu/catalyst)