Skip to content

adjoint-lowering aborts with IRMapping assertion instead of emitting a diagnostic #3217

Description

@Qottmann

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-loweringto-pprppr-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:

  1. _check_no_measurementsMid-circuit measurements cannot be used within an adjoint() or ctrl() region.
    (catalyst/api_extensions/quantum_operators.py:911)
  2. verify_operations / _inv_op_checkerMidCircuitMeasure is not invertible on 'null.qubit' device
  3. adjoint-loweringUnhandled 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions