Skip to content

[qref 3.8] Convert subroutines in plxpr to qref catxpr#2781

Merged
paul0403 merged 19 commits into
paul0403/qref_frontend_mainfrom
paul0403/qref_frontend_subroutines
May 28, 2026
Merged

[qref 3.8] Convert subroutines in plxpr to qref catxpr#2781
paul0403 merged 19 commits into
paul0403/qref_frontend_mainfrom
paul0403/qref_frontend_subroutines

Conversation

@paul0403
Copy link
Copy Markdown
Member

@paul0403 paul0403 commented May 1, 2026

Context:
Convert subroutines in plxpr to qref catxpr.

The use of QubitHandler is removed, since both plxpr and catalyst jaxpr is now reference semantics.

When calling a subroutine, there are some different cases. In python, a subroutine is always called with "wires", which can either be integers or dynamically allocated wires:

@qp.capture.subroutine
def subroutine(wires):
     qp.Toffoli(wires=[1, *wires])

@qp.qjit(capture=True, target="mlir")
@qp.qnode(qp.device("lightning.qubit", wires=3))
def circuit(i: int):
    with qp.allocate(2) as q:
        subroutine_with_allocation(wires=[q[0], 0])   # call with an allocated wire and an int

In plxpr, dynamic allocations return a specific AbstractQubit type instead of a generic integer type (PennyLaneAI/pennylane#9400). This means in the signature of the subroutine function (and the corresponding calls), the arguments can either be integers or an abstract qubit type.

When the arguments are integers, we must not implicitly convert the func args to qref.bit types, because the subroutine function can use them as actual integers as well. Therefore, on top of the integer, the global register must also be taken in.

When arguments are AbstractQubit types, the args can directly be qref.bit types. Therefore the above will compile to

    func.func public @circuit(%arg0: tensor<i64>){
      ....
      %0 = qref.alloc( 3) : !qref.reg<3>
      %1 = qref.alloc( 2) : !qref.reg<2>
      %2 = qref.get %1[ 0] : !qref.reg<2> -> !qref.bit
      call @subroutine_with_allocation(%0, %2, %c) : (!qref.reg<3>, !qref.bit, tensor<i64>) -> ()
      qref.dealloc %1 : !qref.reg<2>
      ....
    }
    func.func private @subroutine(%arg0: !qref.reg<3>, %arg1: !qref.bit, %arg2: tensor<i64>) {
      %0 = qref.get %arg0[ 1] : !qref.reg<3> -> !qref.bit
      %extracted = tensor.extract %arg2[] : tensor<i64>
      %1 = qref.get %arg0[%extracted] : !qref.reg<3>, i64 -> !qref.bit   // get global wire index 0
      qref.custom "Toffoli"() %0, %arg1, %1 : !qref.bit, !qref.bit, !qref.bit
      return
    }

In this scheme, inside the subroutine:

  • aliasing between any global wires are handled automatically because any global wire indices come in as an integer arg + the global qreg arg at the beginning, and they are automatically dynamic
  • there will be no alisaing between global wires and dynamically allocated wires because they belong in the two different type groups
  • The only potential alias is between two qref.bit args. When both extract indices are dynamic, we directly assert that they are different inside the value semantics conversion pass. In cases of dynamic indices, we add an catalyst.assert op to check they are different in runtime.

[sc-115874]

@paul0403 paul0403 requested a review from albi3ro May 1, 2026 18:00
Comment thread .github/workflows/check-catalyst.yaml Outdated
Comment thread mlir/lib/QRef/Transforms/value_semantics_conversion.cpp Outdated
@paul0403 paul0403 requested review from kipawaa, mehrdad2m and rniczh May 25, 2026 19:30
Copy link
Copy Markdown
Member

@rniczh rniczh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯

Comment thread mlir/lib/QRef/Transforms/value_semantics_conversion.cpp
@paul0403 paul0403 merged commit 46dd4d6 into paul0403/qref_frontend_main May 28, 2026
29 of 30 checks passed
@paul0403 paul0403 deleted the paul0403/qref_frontend_subroutines branch May 28, 2026 16:12
paul0403 added a commit that referenced this pull request Jun 2, 2026
…#2663)

**Context:**
Migrate capture frontend to produce reference semantics MLIR.

This is the main PR. Each item will come into this PR as its own PR.

**Description of the Change:**
- Expose python bindings for operations in the `qref` MLIR dialect, and
define their corresponding jax primitives and lowering rules on these
primitives
- The conversion from plxpr to catalyst jaxpr now produces qref
primitives whenever possible. This includes allocation, deallocation,
extract (now `qref.get`; note that reference semantics does not have an
"insert" op), observables, gates and adjoints
- For the following primitives, the conversion from the PL version to
catalyst version is no longer done. Instead, we define lowering rule on
the PL primitives directly: `for_loop`, `while_loop`, `if`, `adjoint`.
This is because with reference semantics, their MLIR operation now take
in and return purely classical values, so the special handling regarding
the quantum values in their operands is not needed anymore.
- At the beginning of the default pipeline, and at the beginning of
various xdsl tools, the following passes are added (in this order):
   - `--canonicalize`
   - `--verify-no-quantum-use-after-free`
   - `--convert-to-value-semantics`
   - `--canonicalize`

- `QubitHandler` (along with the entirety of
`from_plxpr/qubit_handler.py`, and its tests
`test_from_plxpr_qubit_handler.py`) has been removed 🥳
This was the machinery to convert reference semantics plxpr to value
semantics catxpr, but this conversion is no longer needed.
- Tests are edited correspondingly, mostly to update their expected
results to check for reference semantics, i.e. all checks that are
looking for `quantum.blah` ops in the mlir compiled from frontend now
look for `qref.blah`. There are some tests that experienced extra
updates:
1. In `pytest/from_plxpr/test_from_plxpr.py`:
- Tests comparing catxpr generated from capture and legacy frontends are
removed. They are converted to proper lit tests (under
`frontend/tests/lit/test_qref`), checking for the qref MLIR generated
from the capture path. As a result, the `compare_call_jaxpr` test util
has been removed. Note that the checks for execution results on these
tests are kept.
- Tests checking for value semantics catalyst primitives have been
updated to check for the corresponding qref primitives.
2. In `pytest/test_dynamic_qubit_alllocation.py`, dynamically allocated
qubits from outside an adjoint can now be used by the adjoint, so the
xfailed test is changed to a regular test
3. In `lit/test_from_plxpr.py`, there are many tests checking for the
value semantcis qubit extract/insert topology. I have removed these
tests as the `from_plxpr` conversion is no longer responsible for
creating the value semantics extract/insert operations.The value
semantics extract/insert op strategy is now exclusively documented and
tested in the `--convert-to-value-semantics` pass itself.

**Benefits:**
1. Significantly, and I mean truly significantly, simplifies the capture
frontend. Many plxpr primitives are used directly, which is a big step
towards lowering plxpr and removing catxpr from the pipeline altogether.
6. Many tech debts are automatically fixed. Here's an (incomplete) list:
- Adjoints can take in dynamically allocated wires [sc-102216]
- Fixes dataflow in value semantics: no longer missing an insert before
gates on dynamically indexed wires #2526
- Fixes dataflow in value semantics: no longer missing an insert before
observables on dynamically indexed wires #2527
- Passes that need to identify the qubit label does not have to walk
back all the way back up the gate chain in value semantics. For example,
judgement of non-commuting observables is much easier (to check if two
observables are on the same qubit or not).
3. Greatly improve data flow around control flow/adjoint/subroutine
regions. We no longer have to insert all qubits into their registers and
let these regions take in the registers. Frontend just generates mlir
where the regions see qref values from above via closure, and the
`--convert-to-value-semantics` pass will only create value semantics
regions that take in individual qubit values, instead of entire register
values.

**Related GitHub Issues:**
closes #2526 [sc-112704]
closes #2527 [sc-112706]

- [x] [sc-115868]: alloc and observables #2664 
- [x] [sc-115869]: gates #2672 , also handles basic dynamic qubit
allocation
- [x] [sc-115870]: for loops #2694 
- [x] [sc-115871]: while loops #2717 
- [x] [sc-115872]: if statements, also handles MCMs with reset #2740
- [x] [sc-115873]: adjoints #2720
- [x] [sc-115874]: subroutines and calls #2781 
- [x] [sc-118243] xdsl pipeline converts qref mlir to value seamntics at
the beginning #2757
- [x] [sc-119731] graph decomp frontend is reference semantics #2834 
- [x] update old frontend lit tests to check for `qref` instead of
`quantum` #2878

---------

Co-authored-by: albi3ro <chrissie.c.l@gmail.com>
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