Skip to content

[Quake] Implementation, canonicalization, and lowering of quake.phase, an op designed to account for global/controlled phase - #4976

Open
cabreraam wants to merge 5 commits into
NVIDIA:mainfrom
cabreraam:global-phase-trakcing-pt-1
Open

[Quake] Implementation, canonicalization, and lowering of quake.phase, an op designed to account for global/controlled phase#4976
cabreraam wants to merge 5 commits into
NVIDIA:mainfrom
cabreraam:global-phase-trakcing-pt-1

Conversation

@cabreraam

@cabreraam cabreraam commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Overview

This PR introduces the quake.phase op, which accounts for phase corrections, both global and controlled, its canonicalization, and lowering. For lowering, we introduce two passes, NormalizePhasePlacement for normalizing the location of phase ops in the IR, and LowerPhase, which either erases uncontrolled phase ops or introduces the corresponding r1/rz rotations that map to the given phase op.

The size of this PR is a bit of a doozy (though the line count is inflated by the size of the tests), so in the following sections, I've outlined each implementation detail and the corresponding code.

Implementation

quake.phase op

  • QuakeOps.td, see the tablegen entry for PhaseOp
  • inherits from OneTargetParamOp where phase angle is the one parameter

Verification

  • in QuakeOps.cpp
  • verifies that
    • anchor (target) is either a scalar of quake.ref of quake.wire type
    • if there are any negative control qubits, there are polarity flags for each control
    • tested in phase-op-errors.qke

Matrix Implementation

  • in PhaseOp::getOperatorMatrix in QuakeOps.cpp
  • constructs $e^{i\phi}I_{2}$ as base matrix for the anchor qubit
  • if the adjoint attribute is specified, we negate the phase angle
  • phase-matrix.qke uses this matrix representation to test quake.phase ops against their lowered counterpart for equivalence

Memory Effects

Canonicalization

Two patterns registered in PhaseOp::getCanonicalizationPatterns in QuakeOps.cpp

Implemented in CanonicalPatterns.inc

Tested in phase-canonicalize.qke

Folding angles congruent to $0 \bmod 2\pi$

  • rewrite pattern that attempts to erase a quake.phase op if it`s angle is a multiple of $2/pi$

  • rejects non-finite values

  • compares how close the angle is to identify phase. If the angle is a multiple of $2\pi$, then the following is true

    $e^{i\phi}=1\quad\Longleftrightarrow\quad\phi=2\pi k$

  • I represent $2\pi$ by creating an llvm::APFloat with a string representation of $2\pi$, so if this is not the best way to go about this, I'm happy to go back to the drawing board/remove this canonicalization

Adjacent Phase Merging

  • this is an opportunistic rewrite that attempts to merge two phases if
    • they live next to each other in the IR list
    • the number of control qubits are the same and they have the same polarities (positive or negative control for each present control)
    • the actual anchor and control qubits are the same
  • merge occurs essentially by rewriting the two phase ops, e.g.,
    $Phase(a, C)Phase(b, C) = Phase(a + b, C)$
    • if one of the ops is adjoint but the other isn't, we just subtract the adjoint phase from the other phase
  • The more powerful phase merging op comes in NormalizePhasePlacement, but having this canonicalization pattern present allows us to merge any low hanging fruit when canonicalize is run

NormalizePhasePlacement

Creates a transformation pass invoked with normalize-phase-placement that attempts to sink phase operations as far down in their respective basic blocks as is safe, e.g., not crossing calls, regions, terminators, non-unitary quantum ops, and ops with unknown effects

Implemented in NormalizePhasePlacement.cpp and tested in normalize-phase-boundaries.qke

Lower Phase

Erases all uncontrolled quake.phase ops and lowers all controlled quake.phase ops into r1 rotations (rotations about the $\ket{1}$ state.

Implemented in LowerPhase.cpp and tested in lower-phase.qke.

Adjoint Handling

Negates the phase angle

Scalar-control lowering

When a control is a scalar, that control becomes the quake.phase anchor.

When there are multiple scalar controls, either

  • the last positive scalar control
  • last scalar control (if no positive controls)
    is selected as the anchor qubit with the other remaining as controls. The resulting r1 op then becomes a rotation on the anchor qubit controlled by the remaining controls.

Aggregate-only fallback

If the type of the controls is a container of unknown size i.e., veq with dynamic size, we can use the equation

$(R1(2\phi)R_z(-2\phi)=e^{i\phi}I)$

to handle this fallback condition where, even though we don't explicitly know the controls, we can just implement controlled versions of the r1 and rz rotations to achieve the same behavior as if the controls were known and scalar(s), e.g.,

$Ctrl_{C}!\left(R_1(2\phi)\right)Ctrl_{C}!\left(R_z(-2\phi)\right)= Phase(\phi, C)$

The caveat is if

Anchor alias checking

There is a check to make sure that the anchor qubit is not part of the aggregate control container. This situation shouldn't happen, so if it does, we emit an error.

Wire result restoration

When dealing with wire types, we need to map the outputs of the lowered phase op(s) to their correct destinations. For example, in the test

func.func @positive_wire(%theta: f64) {
  %control = quake.null_wire
  %anchor = quake.null_wire
  %result:2 = quake.phase (%theta) [%control] %anchor
      : (f64, !quake.wire, !quake.wire)
        -> (!quake.wire, !quake.wire)
  quake.sink %result#0 : !quake.wire
  quake.sink %result#1 : !quake.wire
  return
}

We lower the phase op to

func.func @positive_wire(%arg0: f64) {
  %0 = quake.null_wire
  %1 = quake.null_wire
  %2 = quake.r1 (%arg0) %0 : (f64, !quake.wire) -> !quake.wire
  quake.sink %2 : !quake.wire
  quake.sink %1 : !quake.wire
  return
}

where the the control and anchor values map to %2 and %1, respectively.

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown

CI Summary (push) — ✅ passed

Run #31049663702 · ✅ 6 · ⏩ 7 · ❌ 0 · ⛔ 0

Top-level jobs (13)
Job Result
binaries ⏩ skipped
build_and_test ✅ success
config_devdeps ✅ success
config_source_build ⏩ skipped
config_wheeldeps ✅ success
devdeps ✅ success
docker_image ⏩ skipped
gen_code_coverage ⏩ skipped
metadata ✅ success
python_metapackages ⏩ skipped
python_wheels ⏩ skipped
source_build ⏩ skipped
wheeldeps ✅ success
⏩ Skipped jobs (7) — intentionally skipped on PR builds; run on merge_group / workflow_dispatch
Job
binaries
config_source_build
docker_image
gen_code_coverage
python_metapackages
python_wheels
source_build
All sub-jobs (42) — every matrix leg, with links
Job Status Link
Build and test (amd64, gcc12, openmpi) / Dev environment (Debug) ✅ success view
Build and test (amd64, gcc12, openmpi) / Dev environment (Python) ✅ success view
Build and test (amd64, llvm, openmpi) / Dev environment (Debug) ✅ success view
Build and test (amd64, llvm, openmpi) / Dev environment (Python) ✅ success view
Build and test (arm64, llvm, openmpi) / Dev environment (Debug) ✅ success view
Build and test (arm64, llvm, openmpi) / Dev environment (Python) ✅ success view
CI Summary ❔ in_progress view
Configure build (devdeps) ✅ success view
Configure build (source_build) ⏩ skipped view
Configure build (wheeldeps) ✅ success view
Create CUDA Quantum installer ⏩ skipped view
Create Docker images ⏩ skipped view
Create Python metapackages ⏩ skipped view
Create Python wheels ⏩ skipped view
Gen code coverage ⏩ skipped view
Load dependencies (amd64, gcc12) / Caching ✅ success view
Load dependencies (amd64, gcc12) / Finalize ✅ success view
Load dependencies (amd64, gcc12) / Metadata ✅ success view
Load dependencies (amd64, llvm) / Caching ✅ success view
Load dependencies (amd64, llvm) / Finalize ✅ success view
Load dependencies (amd64, llvm) / Metadata ✅ success view
Load dependencies (arm64, gcc12) / Caching ✅ success view
Load dependencies (arm64, gcc12) / Finalize ✅ success view
Load dependencies (arm64, gcc12) / Metadata ✅ success view
Load dependencies (arm64, llvm) / Caching ✅ success view
Load dependencies (arm64, llvm) / Finalize ✅ success view
Load dependencies (arm64, llvm) / Metadata ✅ success view
Load source build cache ⏩ skipped view
Load wheel dependencies (amd64, 12.6) / Caching ✅ success view
Load wheel dependencies (amd64, 12.6) / Finalize ✅ success view
Load wheel dependencies (amd64, 12.6) / Metadata ✅ success view
Load wheel dependencies (amd64, 13.0) / Caching ✅ success view
Load wheel dependencies (amd64, 13.0) / Finalize ✅ success view
Load wheel dependencies (amd64, 13.0) / Metadata ✅ success view
Load wheel dependencies (arm64, 12.6) / Caching ✅ success view
Load wheel dependencies (arm64, 12.6) / Finalize ✅ success view
Load wheel dependencies (arm64, 12.6) / Metadata ✅ success view
Load wheel dependencies (arm64, 13.0) / Caching ✅ success view
Load wheel dependencies (arm64, 13.0) / Finalize ✅ success view
Load wheel dependencies (arm64, 13.0) / Metadata ✅ success view
Prepare cache clean-up ✅ success view
Retrieve PR info ✅ success view
✅ Required checks (6/6) — declared in .github/required-checks.yml for push
Required check Status Link
Build and test (amd64, llvm, openmpi) / Dev environment (Debug) ✅ success view
Build and test (amd64, llvm, openmpi) / Dev environment (Python) ✅ success view
Build and test (arm64, llvm, openmpi) / Dev environment (Debug) ✅ success view
Build and test (arm64, llvm, openmpi) / Dev environment (Python) ✅ success view
Build and test (amd64, gcc12, openmpi) / Dev environment (Debug) ✅ success view
Build and test (amd64, gcc12, openmpi) / Dev environment (Python) ✅ success view

@cabreraam
cabreraam force-pushed the global-phase-trakcing-pt-1 branch from 23e551f to c1bebd1 Compare July 23, 2026 21:48
@cabreraam
cabreraam force-pushed the global-phase-trakcing-pt-1 branch from c1bebd1 to 6c3d6f0 Compare July 24, 2026 00:50
@cabreraam
cabreraam marked this pull request as ready for review July 24, 2026 00:50
@cabreraam
cabreraam force-pushed the global-phase-trakcing-pt-1 branch from 6c3d6f0 to 40bb5d7 Compare July 24, 2026 20:58

@atgeller atgeller left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Some questions based on a quick look.

Comment thread cudaq/lib/Optimizer/Dialect/Quake/CanonicalPatterns.inc Outdated
Comment thread cudaq/lib/Optimizer/Transforms/NormalizePhasePlacement.cpp
Comment thread cudaq/lib/Optimizer/Transforms/NormalizePhasePlacement.cpp Outdated
Comment thread cudaq/lib/Optimizer/Transforms/NormalizePhasePlacement.cpp

@sacpis sacpis left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @cabreraam. Left few comments.

phase, phase.getResultTypes(), adjoint, ValueRange{mergedAngle},
input.getControls(), input.getTargets(),
input.getNegatedQubitControlsAttr());
rewriter.eraseOp(input);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

MergeAdjacentPhasePattern calls rewriter.eraseOp(input) unconditionally. It only checks that the second phase consumes the first phase's results, never that those results have no other users. Seems like verifyWireResultsAreLinear tolerates a wire result having several users when each is in a distinct mode, so such IR is legal input.

Tried running this mlir (xyz.qke)

func.func @multiuse(%a: f64, %b: f64, %cond: i1) {
  %c = quake.null_wire
  %an = quake.null_wire
  %p1:2 = quake.phase (%a) [%c] %an : (f64, !quake.wire, !quake.wire) -> (!quake.wire, !quake.wire)
  %p2:2 = quake.phase (%b) [%p1#0] %p1#1 : (f64, !quake.wire, !quake.wire) -> (!quake.wire, !quake.wire)
  cf.cond_br %cond, ^bb1, ^bb2
^bb1:
  quake.sink %p1#0 : !quake.wire
  quake.sink %p2#0 : !quake.wire
  quake.sink %p2#1 : !quake.wire
  return
^bb2:
  quake.sink %p2#0 : !quake.wire
  quake.sink %p2#1 : !quake.wire
  return
}

with this command

/cudaq-opt --canonicalize xyz.qke

and got this error

cudaq-opt: /home/.llvm-project/mlir/lib/IR/PatternMatch.cpp:156: virtual void mlir::RewriterBase::eraseOp(mlir::Operation*): Assertion `op->use_empty() && "expected 'op' to have no uses"' failed.
PLEASE submit a bug report to https://github.com/NVIDIA/cuda-quantum and include the crash backtrace.

Comment on lines +226 to +230
if (auto quantum = dyn_cast<cudaq::quake::OperatorInterface>(cursor))
if (failed(advanceAcrossOperator(quantum, controls, anchor))) {
destination = cursor;
break;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Seems like normalize-phase-placement emits use-before-def IR when one control can move past a gate but another cannot. Can you please try running a 2 control wire phase, one safe gate, then an x that targets the second control?

quake.sink %second#0 : !quake.wire
quake.sink %second#1 : !quake.wire
return
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Are we missing tests covering the adjoint sign math in the phase merges?


namespace {

static SmallVector<Type> getWireResultTypes(MLIRContext *context,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Seems like this exists in NormalizePhasePlacement as well.

if (phase.isAdj())
angle = arith::NegFOp::create(rewriter, phase.getLoc(), angle);
return angle;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Seems like similar to getSignedAngle in NormalizePhasePlacement?

assert(replacements.size() == phase.getWires().size() &&
"phase result count does not match its wire operands");
return replacements;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Seems like heavily overlapped with getWireInputs in NormalizePhasePlacement?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we move similar methods to some utility file?

break;
}

crossedOperation = true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we only set crossedOperation when the crossed op is a quake::OperatorInterface?

def LowerPhase : Pass<"lower-phase", "mlir::func::FuncOp"> {
let summary = "Lower phase bookkeeping operations to ordinary Quake gates";
let description = [{
Lowers controlled `quake.phase` operations to exact `quake.r1``quake.rz`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: missing separator (/) between quake.r1``quake.rz?

@cabreraam
cabreraam force-pushed the global-phase-trakcing-pt-1 branch from 1d0572c to 3a88202 Compare August 4, 2026 22:00
…e`, an op designed to account for global/controlled phase

Signed-off-by: Anthony Cabrera <antcabrera@nvidia.com>
Signed-off-by: Anthony Cabrera <antcabrera@nvidia.com>
…entries in the negated control boolean array

Signed-off-by: Anthony Cabrera <antcabrera@nvidia.com>
…common file `PhaseUtilities`; rename `hasUnambiguousLinearUse` --> `hasUnambiguousWireUse`

Signed-off-by: Anthony Cabrera <antcabrera@nvidia.com>
Signed-off-by: Anthony Cabrera <antcabrera@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants