Skip to content

Let compile stages decide when kernel arguments are specialized - #5771

Merged
taalexander merged 15 commits into
NVIDIA:mainfrom
taalexander:feature/stages-decide-specialization
Oct 30, 2026
Merged

taalexander merged 15 commits into
NVIDIA:mainfrom
taalexander:feature/stages-decide-specialization

Conversation

@taalexander

Copy link
Copy Markdown
Collaborator

Closes #5754. This lets a compile stage ask for the values of a kernel's arguments, so the
Clifford+T benchmark target can synthesize rotations whose angles depend on an
argument.

Before, a QFT whose width is an argument failed to compile on that
target unless you called cudaq.synthesize first.

Stacked on #5758. Until that merges, only these two commits are new here:

The problem

On simulators, a Python kernel is compiled once and gets its arguments at run
time, so the compile stages never see the argument values. That's usually what
you want, but some stages can't do their job without them. Clifford+T
synthesis has to know every rotation angle at compile time, and angles often
come from arguments, like the width of a QFT. On
compiler-bench-ftqc-clifford-t, those kernels failed to compile.

The only way to get values into the compile was fullySpecialize on the
target. Simulators clear it (DefaultQPU), and setting it there anyway
exposed a second bug. Argument synthesis and the run-time .argsCreator lay
out some types differently (list[bool], for one), and Python picked the
layout from whether the platform was a local simulator rather than from
whether the compile would actually substitute the arguments. With
fully_specialize set on qpp-cpu, sampling a kernel that takes a
list[bool] returned {}.

How this fixes it

Stages can now ask for argument values. Lowering handlers get a
requiresArgumentValues query, and the quake-unmapped handler says yes when
it synthesizes. A target specializes if it sets fullySpecialize or one of
its stages asks, which specializesArguments(target) computes.

The compiler, the Python compiled-module cache, and Python argument packing
all use that one answer, so they can't disagree. That also fixes the
list[bool] layout.

The second commit fixes two misleading errors: the synthesis dynamic-angle
error no longer suggests a pass option users can't set, and local pipeline
failures no longer say "Remote rest platform".

Things to be aware of

  • On compiler-bench-ftqc-clifford-t, a kernel with live arguments now
    recompiles on every call, because the cache doesn't key on argument values.
    That includes every shot of cudaq.run. Kernels without live arguments
    still cache there. Before this change, kernels with angle arguments didn't
    compile at all, so I think this is fine for a benchmark target, but a cache
    keyed on argument values would fix it if it matters.
  • Remote and emulated targets already set fullySpecialize, so nothing
    changes for them. The simulators and compiler-bench-ftqc-logical still
    compile once and reuse the result.
  • Handler selection for this query uses default CompileOptions. That's
    right as long as supports only looks at the profiles, which the built-in
    handlers do and the supports doc now requires.

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

CI Summary (push) — ✅ passed

Run #40972130018 · ✅ 8 · ⏩ 10 · ❌ 0 · ⛔ 0

Top-level jobs (18)
Job Result
binaries ⏩ skipped
build_and_test ✅ success
changes ✅ success
config_devdeps ✅ success
config_source_build ⏩ skipped
config_wheeldeps ✅ success
devdeps ✅ success
docker_image ⏩ skipped
documentation ✅ success
gen_code_coverage ⏩ skipped
metadata ✅ success
python_devel_wheel ⏩ skipped
python_metapackages ⏩ skipped
python_wheels ⏩ skipped
python_wheels_cu12_validation ⏩ skipped
python_wheels_cu13 ⏩ skipped
source_build ⏩ skipped
wheeldeps ✅ success
⏩ Skipped jobs (10) — intentionally skipped on PR builds; run on merge_group / workflow_dispatch
Job
binaries
config_source_build
docker_image
gen_code_coverage
python_devel_wheel
python_metapackages
python_wheels_cu12_validation
python_wheels_cu13
python_wheels
source_build
All sub-jobs (47) — 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
Check for stable CUDA-Q changes ✅ success view
Configure build (devdeps) ✅ success view
Configure build (source_build) ⏩ skipped view
Configure build (wheeldeps) ✅ success view
Create CUDA 12 Python wheels ⏩ skipped view
Create CUDA 13 Python wheels ⏩ skipped view
Create CUDA Quantum installer ⏩ skipped view
Create Docker images ⏩ skipped view
Create Python metapackages ⏩ skipped view
Create shared Python development wheel ⏩ skipped view
Documentation / Build and check docs ✅ success 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 ❔ in_progress view
Retrieve PR info ✅ success view
Validate CUDA 12 Python wheels ⏩ skipped view
✅ Required checks (7/7) — 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
Documentation / Build and check docs ✅ success view

@taalexander taalexander assigned cabreraam and unassigned cabreraam Oct 29, 2026
@taalexander
taalexander requested a review from cabreraam October 29, 2026 16:49
@taalexander
taalexander force-pushed the feature/stages-decide-specialization branch from 4133421 to 6e1f44a Compare October 29, 2026 16:55
@taalexander
taalexander marked this pull request as ready for review October 29, 2026 16:59
@taalexander
taalexander force-pushed the feature/stages-decide-specialization branch from 6e1f44a to e42a8a2 Compare October 29, 2026 17:01
@github-actions github-actions Bot added documentation Improvements or additions to documentation build The issue related to the CUDA Quantum build runtime CUDA quantum runtime python-lang Anything related to the Python CUDA Quantum language implementation testing Relates to testing codegen issue with generating any codegen output (QIR, OpenQASM etc.) core compiler labels Oct 29, 2026
@sacpis

sacpis commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator

Getting the following build error

[741/1824] Linking CXX executable bin/profile_stages
FAILED: bin/profile_stages 
: && /usr/local/toolchain/bin/c++ -O3 -DNDEBUG -fuse-ld=lld -B/usr/local/llvm/bin -Wl,--dependency-file=runtime/test/CMakeFiles/profile_stages.dir/link.d runtime/test/CMakeFiles/profile_stages.dir/Regress/profile_stages.cpp.o runtime/test/CMakeFiles/profile_stages.dir/__/internal/compiler/JITTargetPipeline.cpp.o runtime/test/CMakeFiles/profile_stages.dir/__/internal/compiler/LoweringHandler.cpp.o runtime/test/CMakeFiles/profile_stages.dir/__/internal/compiler/QuakeReferenceSemanticsHandler.cpp.o runtime/test/CMakeFiles/profile_stages.dir/__/internal/compiler/QuakeUnmappedHandler.cpp.o -o bin/profile_stages -L/workspaces/cuda-quantum/build/lib -Wl,-rpath,"\$ORIGIN:\$ORIGIN/lib:\$ORIGIN/lib/plugins:\$ORIGIN/../lib:\$ORIGIN/../lib/plugins:\$ORIGIN/../cudaq/mlir/_mlir_libs:\$ORIGIN/../python/cudaq/mlir/_mlir_libs"  cudaq/lib/Target/libCUDAQTargetConfig.a  lib/libcudaq-logger.so && :
ld.lld: error: undefined symbol: cudaq::opt::needsCliffordTSynthesis(std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>>> const&)
>>> referenced by QuakeUnmappedHandler.cpp
>>>               runtime/test/CMakeFiles/profile_stages.dir/__/internal/compiler/QuakeUnmappedHandler.cpp.o:((anonymous namespace)::QuakeUnmappedHandler::requiresArgumentValues(std::variant<cudaq::config::UncheckedProfile, cudaq::config::QuakeReferenceSemanticsProfile, cudaq::config::QuakeUnmappedProfile> const&, std::variant<cudaq::config::UncheckedProfile, cudaq::config::QuakeReferenceSemanticsProfile, cudaq::config::QuakeUnmappedProfile> const&) const)
collect2: error: ld returned 1 exit status
[774/1824] Linking CXX executable bin/cudaq-lsp-server
ninja: build stopped: subcommand failed.

@schweitzpgi
schweitzpgi self-requested a review October 29, 2026 17:28
@taalexander
taalexander force-pushed the feature/stages-decide-specialization branch 2 times, most recently from 1284ee8 to 557596e Compare October 29, 2026 18:06
@taalexander

Copy link
Copy Markdown
Collaborator Author

@sacpis should be fixed. Base PR is trying to merge now.

@cabreraam

Copy link
Copy Markdown
Collaborator

@sacpis should be fixed. Base PR is trying to merge now.

Yep. This and this should have fixed it. Was experiencing same issue but had just added in dependency locally.

@cabreraam cabreraam 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.

I will defer to @schweitzpgi's sagacious review. I found one corner case that is worth addressing, but other than that, LGTM.

Comment thread runtime/internal/compiler/Compiler.cpp Resolved
Comment thread runtime/test/CMakeLists.txt Resolved
On simulators a Python kernel is compiled once and takes its arguments at
run time, so a stage never sees their values. A stage that cannot work
without them failed or degraded: the Clifford+T benchmark target could not
synthesize a rotation whose angle depends on an argument, so a QFT whose
width is an argument failed to compile.

Each lowering handler now reports, through a new
`LoweringHandler::requiresArgumentValues` query that reads only the
profiles, whether its stage needs argument values. The quake-unmapped
handler answers yes when `clifford-t-synthesis-pipeline` will approximate
rotations for the stage's basis. That decision moves into
`cudaq::opt::needsCliffordTSynthesis`, declared beside
`cliffordTRotations` without MLIR types, so the pipeline and the handler
share it. The planning tests `profile_stages` and `profile_providers`
build the handlers without the MLIR runtime, so they now link
`OptTransforms` for it. `specializesArguments(target)` returns true when
the target sets `fullySpecialize` or a handler selected for an inferred
stage requires values. Custom and override stages answer no, as before, so
a custom pipeline that needs values still sets `fullySpecialize`.
`fullySpecialize` keeps its meaning, and `DefaultQPU` still clears it.

The compiler, the Python compiled-module cache, and Python argument
packing all read that one function, so they cannot disagree. Packing
matters because argument synthesis and the run-time `.argsCreator` lay out
some types differently. Before, packing chose its layout from whether the
platform was a local simulator and never read the flag. With a compile
target whose `fully_specialize` was set on `qpp-cpu`, `sample` of a kernel
taking a `list[bool]` returned `{}`; it now returns the expected bitstring.
`marshal_arguments_for_module_launch` takes the decision from its caller,
and each launch passes the answer for the QPU it launches on, including the
asynchronous paths, which pack before their `CompileOptions` exist. The
Python launch also compiles for the current QPU's target instead of QPU 0's.

Two callers always compile with every argument substituted, so they now
always pack for argument synthesis: `translate`, whose compile sets
`fullySpecialize`, and `marshal_and_retain_module` for a kernel that is not
an entry point. Both previously used the run-time layout on a local
simulator.

Remote and emulated targets set `fullySpecialize`, so their packing,
specialization, and cache behavior do not change. Targets whose stages need
no values, including the simulators and `compiler-bench-ftqc-logical`,
still compile once and reuse the result. On a target that specializes only
because of a stage, which after this change is
`compiler-bench-ftqc-clifford-t`, a kernel with live arguments recompiles on
every call because the cache does not key on argument values. That includes
every shot of `cudaq.run`. A kernel without live arguments compiles the same
way for any values, so it stays cacheable there. Platforms that set
`fullySpecialize` still never cache.

Handler selection here uses default `CompileOptions`, which picks the same
handler as `getPassPipeline` while `supports` depends only on the profiles.
The built-in handlers satisfy that, and `supports` now documents it as part
of its contract.

Tests: `compile_target` shows a synthesizing stage specializing on a target
built without `DefaultQPU`, and a stage that keeps rotations leaving the
argument to run time. The decision reads only the target, so one level is
enough. On the Clifford+T target an argument-sized QFT counts the same as
the constant QFT, and a `list[bool]` argument samples correctly through
`sample` and `sample_async`. The compiled-module cache
on the Clifford+T target reuses a kernel without arguments but never one
that takes them, and on the logical target it reuses one compile across
argument values. `translate` of a `list[bool]` argument stores the expected
values. The non-entry-point path of `marshal_and_retain_module` and launches
on a QPU other than 0 are not covered by a test.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
After stages decide argument specialization, a Clifford+T target compiles
with every argument value substituted, so an angle that is still dynamic
comes from a value the kernel computes while it runs, such as an angle
inside a loop longer than the unroll limit. The error suggested
`--on-dynamic-angle=skip`, which a target user cannot set. It now says why
the angle cannot be synthesized. The pass option itself is unchanged and
still listed in the pass help.

`Compiler::applyPipeline` reported every pipeline failure as coming from a
"Remote rest platform", which was wrong for local targets such as the
Clifford+T benchmark target. Its messages no longer name a platform.

Tests: `dynamic_angle_error.qke` checks the full new message.
Signed-off-by: Thomas Alexander <talexander@nvidia.com>
The error already says synthesis cannot handle an angle computed at run
time. Naming one case, a loop too long to unroll, suggested it was the
only one.

Tests: `dynamic_angle_error.qke` checks the shortened message.
Signed-off-by: Thomas Alexander <talexander@nvidia.com>
The doc comment stated the rule that the answer depends only on the
profiles, but not what a yes means or why the rule matters. It now says
that a yes makes the compiler replace each argument with its value, gives
Clifford+T synthesis as the example, and explains that Python uses the
same answer to choose how to pack arguments before a launch's options
exist, so an answer that read the options could disagree with the
compile.

Comment-only change.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
`LoweringHandler::supports` took the launch's `CompileOptions`, and its
default implementation answered by calling `getStagePasses` with them.
Since stages decide argument specialization, the handler for a stage is
looked up twice: once by `specializesArguments`, before a launch's
options exist, and once by `getPassPipeline` with the real options. Both
lookups have to find the same handler, or Python packs the arguments for
one behavior while the compiler does the other, which silently produces
wrong argument values. That held only because the built-in handlers
ignored the options, which a doc comment asked of new handlers.

`supports` now takes only the input and output profiles and is pure
virtual, and `selectLoweringHandler` takes no options, so both lookups
always agree. A handler still chooses its passes from the options in
`getStagePasses`. `specializesArguments` no longer builds default options.

Every handler already overrode `supports` except two test handlers. The
`profile_stages` test handler now counts `supports` calls as well, so the
selection test keeps its expected counts. The fake handler in
`compile_target` gains a `supports` for its output profile.

Tests: each changed translation unit, including both tests, compiles
against this branch's headers. The tests were not run, because the local
build does not contain this branch.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
The cache kept a mutex-guarded map keyed by the basis as an ordered
vector, so the same gates in another order missed it. Measured without
the cache, the check takes about 35 us per call for the Clifford+T
benchmark target's basis and about 18 us for the logical target's, and a
Python launch asks it at most three times. That cost does not justify the
shared state.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
Name the pass after the profile type it checks, `QuakeUnmappedProfile`.
The pass is now `VerifyQuakeUnmappedProfile`, its flag is
`--verify-quake-unmapped-profile`, and its source and lit test are renamed
to match. Nothing outside the pass and its test refers to it.

The pass description and the helper that picks which functions to check
now call those functions kernels: an entry point or a function that takes
qubits. `isConvertedKernel` becomes `isKernel`.

`VerifyProfileQuakeReferenceSemantics` on `main` keeps its name.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
Each profile type has a `std::hash` because a stage's profile is part of
the compile target's hash, which keys the compiled-module cache. The
three definitions were split across a header and two source files that
held nothing else. They now sit in `CompileTarget.cpp` beside the hashes
for stages, lowerings, and codegen, and the two source files are removed.
The profile types themselves are unchanged.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
The comment on `getRegisteredGraph` now says that the graph is built once from every pattern in the decomposition-pattern registry, and that pattern selection and `canDecomposeTo` both read it.

Comment-only change.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
`CliffordTRotations.h` exported the rotations that the Clifford+T
pipelines merge, snap, and synthesize, and `needsCliffordTSynthesis`,
from a header among the Transforms that held no pass. The list is an
implementation detail of those pipelines, so it is now a static array in
`Pipelines.cpp`, and the header is removed.

The quake-unmapped handler needs two answers from the pipelines, and
both are now declared in `Passes.h` beside `registerCliffordTPipelines`:
`cliffordTDecompositionBasis(basis)`, the basis to decompose to before
the pipelines, and `needsCliffordTSynthesis(basis)`. Both take an
`llvm::ArrayRef<std::string>`, so the synthesis pipeline passes its
`basis` option without copying it. The handler builds the same
decomposition basis as before, so the planned pipelines do not change.

Tests: the handler, `Pipelines.cpp`, and `Compiler.cpp` compile against
this branch's headers, and `profile_stages` links with `OptTransforms`
and passes its FileCheck. `profile_providers` could not be checked
against the local build, whose target libraries come from a different
checkout.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
The previous commit put `cliffordTDecompositionBasis` and
`needsCliffordTSynthesis` in `Passes.h`, the core header of the
optimizer, which made the quake-unmapped handler include MLIR to plan
pipelines it writes as text. They now live in `CliffordTPipelines.h`,
which says that it is pipeline support for target lowering rather than a
pass, and includes no LLVM or MLIR header. The rotation list stays
private to `Pipelines.cpp`.

Both functions take `std::span<const std::string>`, so the handler passes
its vector and the synthesis pipeline passes its `basis` option through
`ArrayRef` without copying.

Tests: the handler, `Pipelines.cpp`, and `Compiler.cpp` compile, and
`profile_stages` links with `OptTransforms` and passes its FileCheck.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
A comment in the quake-unmapped handler still said "these rotations" after the rotation list became private to `Pipelines.cpp`. It now names the rotations the synthesis pipeline synthesizes. The `CliffordTPipelines.h` preamble drops a sentence that only answered the review.

Comment-only change.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
clifford-t-synthesis rejected any bad numeric option with one message
that listed every requirement, so a Clifford+T profile without an
approximation tolerance failed with a wall of text that never said
which value was wrong. It now reports epsilon and skip-below on their
own, with the value it got, and keeps one message for the search
budgets.

The check still runs before the pass looks at the IR, so a stage
without a positive tolerance fails every kernel, not only the first
one with a rotation to synthesize.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>

@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.

LGTM. Thanks @taalexander. Thanks for taking care of review.

@taalexander
taalexander added this pull request to the merge queue Oct 30, 2026
@github-actions

Copy link
Copy Markdown

CI Summary (merge_group) — ✅ passed

Run #41002822832 · ✅ 5 · ⏩ 13 · ❌ 0 · ⛔ 0

⚡ Fast merge queue — only the llvm dev-environment build+test legs ran.
Wheels, metapackages, installers, Docker images, and the gcc12 legs were skipped.
Set the repository variable MERGE_QUEUE_FAST to false to restore the full matrix.

Top-level jobs (18)
Job Result
binaries ⏩ skipped
build_and_test ✅ success
changes ✅ success
config_devdeps ✅ success
config_source_build ⏩ skipped
config_wheeldeps ⏩ skipped
devdeps ✅ success
docker_image ⏩ skipped
documentation ⏩ skipped
gen_code_coverage ⏩ skipped
metadata ✅ success
python_devel_wheel ⏩ skipped
python_metapackages ⏩ skipped
python_wheels ⏩ skipped
python_wheels_cu12_validation ⏩ skipped
python_wheels_cu13 ⏩ skipped
source_build ⏩ skipped
wheeldeps ⏩ skipped
⏩ Skipped jobs (13) — intentionally skipped on PR builds; run on merge_group / workflow_dispatch
Job
binaries
config_source_build
config_wheeldeps
docker_image
documentation
gen_code_coverage
python_devel_wheel
python_metapackages
python_wheels_cu12_validation
python_wheels_cu13
python_wheels
source_build
wheeldeps
All sub-jobs (28) — every matrix leg, with links
Job 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
CI Summary ❔ in_progress view
Check for stable CUDA-Q changes ✅ success view
Configure build (devdeps) ✅ success view
Configure build (source_build) ⏩ skipped view
Configure build (wheeldeps) ⏩ skipped view
Create CUDA 12 Python wheels ⏩ skipped view
Create CUDA 13 Python wheels ⏩ skipped view
Create CUDA Quantum installer ⏩ skipped view
Create Docker images ⏩ skipped view
Create Python metapackages ⏩ skipped view
Create shared Python development wheel ⏩ skipped view
Documentation ⏩ skipped view
Gen code coverage ⏩ skipped 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, 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 ⏩ skipped view
Prepare cache clean-up ❔ in_progress view
Retrieve PR info ✅ success view
Validate CUDA 12 Python wheels ⏩ skipped view
✅ Required checks (4/4) — declared in .github/required-checks.yml for merge_group_fast
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

Merged via the queue into NVIDIA:main with commit 5556a16 Oct 30, 2026
87 checks passed
@taalexander
taalexander deleted the feature/stages-decide-specialization branch October 30, 2026 05:38
github-actions Bot pushed a commit that referenced this pull request Oct 30, 2026
@@ -0,0 +1,36 @@
/****************************************************************-*- C++ -*-****

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.

I feel like this header belongs in a different folder as the interface isn't about a transformation. It looks more analysis like?

// RUN: cudaq-opt --clifford-t-synthesis='epsilon=0' -verify-diagnostics %s
// RUN: cudaq-opt --clifford-t-synthesis='epsilon=inf' -verify-diagnostics %s
// RUN: cudaq-opt --clifford-t-synthesis='skip-below=inf' -verify-diagnostics %s
// RUN: not cudaq-opt --clifford-t-synthesis='epsilon=0' %s 2>&1 | FileCheck --check-prefix=EPSILON %s

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.

+1

@schweitzpgi schweitzpgi 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.

Nice work pulling this together, Thomas.

anpaz pushed a commit to justinlietz/cuda-quantum that referenced this pull request Oct 30, 2026
…IA#5771)

Closes NVIDIA#5754. This lets a compile stage ask for the values of a kernel's
arguments, so the
Clifford+T benchmark target can synthesize rotations whose angles depend
on an
argument.

Before, a QFT whose width is an argument failed to compile on that
target unless you called `cudaq.synthesize` first.

Stacked on NVIDIA#5758. Until that merges, only these two commits are new
here:

- 6c12716
- 4133421

## The problem

On simulators, a Python kernel is compiled once and gets its arguments
at run
time, so the compile stages never see the argument values. That's
usually what
you want, but some stages can't do their job without them. Clifford+T
synthesis has to know every rotation angle at compile time, and angles
often
come from arguments, like the width of a QFT. On
`compiler-bench-ftqc-clifford-t`, those kernels failed to compile.

The only way to get values into the compile was `fullySpecialize` on the
target. Simulators clear it (`DefaultQPU`), and setting it there anyway
exposed a second bug. Argument synthesis and the run-time `.argsCreator`
lay
out some types differently (`list[bool]`, for one), and Python picked
the
layout from whether the platform was a local simulator rather than from
whether the compile would actually substitute the arguments. With
`fully_specialize` set on `qpp-cpu`, sampling a kernel that takes a
`list[bool]` returned `{}`.

## How this fixes it

Stages can now ask for argument values. Lowering handlers get a
`requiresArgumentValues` query, and the quake-unmapped handler says yes
when
it synthesizes. A target specializes if it sets `fullySpecialize` or one
of
its stages asks, which `specializesArguments(target)` computes.

The compiler, the Python compiled-module cache, and Python argument
packing
all use that one answer, so they can't disagree. That also fixes the
`list[bool]` layout.

The second commit fixes two misleading errors: the synthesis
dynamic-angle
error no longer suggests a pass option users can't set, and local
pipeline
failures no longer say "Remote rest platform".

## Things to be aware of

- On `compiler-bench-ftqc-clifford-t`, a kernel with live arguments now
recompiles on every call, because the cache doesn't key on argument
values.
That includes every shot of `cudaq.run`. Kernels without live arguments
still cache there. Before this change, kernels with angle arguments
didn't
compile at all, so I think this is fine for a benchmark target, but a
cache
  keyed on argument values would fix it if it matters.
- Remote and emulated targets already set `fullySpecialize`, so nothing
changes for them. The simulators and `compiler-bench-ftqc-logical` still
  compile once and reuse the result.
- Handler selection for this query uses default `CompileOptions`. That's
right as long as `supports` only looks at the profiles, which the
built-in
  handlers do and the `supports` doc now requires.

---------

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
taalexander added a commit to taalexander/cuda-quantum that referenced this pull request Oct 30, 2026
Stacked on NVIDIA#5771 so the diff includes its commits until it merges. To
review only them, start at `d3368c1b97`, the first commit after NVIDIA#5771's
head `be373be079`.

## Summary

This adds a `quake-mapped` compile stage, so a staged compile target can
route a kernel onto a device's coupling map. `compiler-bench-nisq` is
the first user. Its custom pipeline becomes two stages, so the
optimization level now changes its output as it does for the Clifford+T
targets:

```yaml
compile-target:
  stages:
    - ir-output-profile:
        name: quake-unmapped
        options:
          basis-gates: [h, rx, ry, rz, x, "z(1)"]
          cost-model: two-qubit-count
    - ir-output-profile:
        name: quake-mapped
        options:
          basis-gates: [h, rx, ry, rz, x, "z(1)"]
          device: grid(3,3)   # optional; without it nothing is routed
```

Simulator targets such as `qpp-cpu` and `nvidia` still don't route and
are unchanged. Routing happens only when a target lists a `quake-mapped`
stage with a device, as `compiler-bench-nisq` can for benchmarking. Such
a kernel can still run on a simulator, and `sample` returns results in
the kernel's qubit order, as on emulated hardware.

## What's added

- **`quake-mapped` profile.** Its options are `basis-gates` and an
optional `device` in the `qubit-mapping` grammar, such as `path(N)`,
`grid(w,h)`, or `file(<path>)`. It has a YAML mapping, a hash, and a
`cudaq._experimental.QuakeMappedProfile` binding.
- **`two-qubit-count` cost model for `quake-unmapped`.** It runs a new
registered `two-qubit-optimization-pipeline`, the counterpart of
`clifford-t-optimization-pipeline`.
- Level 1 runs `canonicalize`, `quake-simplify`, and `dqe`, the cleanup
the Clifford+T pipeline already uses.
  - Level 2 adds `phase-folding`.
  - Level 3 runs level 2.
- **Handler from `quake-unmapped` to `quake-mapped`.** It runs these
passes:
  - `prepare-for-wireset{add-wireset=true}`;
- `qubit-mapping{device=... raise-fatal-errors=true}` when a device is
set;
  - the level-1 cleanup, from level 1;
  - a strict `basis-conversion`.

A kernel that can't be routed, such as one with more qubits than the
device, is now a compile error instead of compiling unrouted.
- **Mapper fix.** Under `raise-fatal-errors`, `qubit-mapping` used to
fail every function that borrows no wires. That included callee kernels
that take qubit arguments, which stay in the module until finalize. It
now fails only an entry point that uses qubits and borrows none.
- **Routed kernels on simulators.**
- Full-QIR codegen can't lower borrowed wires, so the JIT and
`translate` first replace them with null wires, in device-qubit order.
Unlike `regtomem`, this handles wires carried through a loop that isn't
unrolled.
- `DefaultQPU` applies the mapper's reorder index to `sample` results,
as `BaseRemoteRESTQPU` does, except with `explicit_measurements`.
- **Bug fix: `sample` dropped idle qubits on cost-model targets.** From
level 1, `dqe` removes a qubit that no operation touches. A kernel
sampled without measurements then returned shorter bitstrings, whose
positions no longer matched the kernel's qubits. On
`compiler-bench-ftqc-clifford-t`, a 3-qubit kernel with `q[1]` idle
compiled to two qubits. `sample` now measures every qubit before the
pipeline when a stage is `quake-mapped`, or is `quake-unmapped` with a
cost model. The same measurements let the mapper record the reorder
index.
- **`translate` uses the installed compile target.** It used to build
the target's default compile target, so a device set with
`set_target(compile_target=...)` never reached it.
- **Level 0 now differs from level 1 only by optimization.** A
`quake-unmapped` stage with a cost model now converts to wires at level
0 too, and level 0 just skips the optimizer. Before this, level 0 also
skipped the conversion, and with it loop unrolling and the expansion of
register measurements.

## Setting a device

`compiler-bench-nisq` loses its `device=` keyword. Its YAML lives in
`python/tests/targets/`, and the tests register it. To route, derive its
compile target:

```python
import cudaq
from cudaq._experimental import QuakeMappedProfile, get_compile_target

cudaq.set_target("compiler-bench-nisq")
target = get_compile_target()
stages = target.stages
for stage in stages:
    profile = stage.ir_output_profile
    if isinstance(profile, QuakeMappedProfile):
        profile.device = "grid(3,3)"
        stage.ir_output_profile = profile
target.stages = stages
cudaq.set_target("compiler-bench-nisq", compile_target=target)
```

This is temporary as we find a better way to set this. I'm not sure
convinced it should live on the profile.

## Behavior changes

- **`compiler-bench-nisq`:**
- At Python's default level 1, phases are no longer folded. This is
temporary while targets are converted and the levels are enabled.
- `mx` and `my` stay measurements instead of being rewritten into
rotations followed by `mz`.
- From level 1, `dqe` removes idle qubits before mapping, so the
remaining qubits are placed next to each other. Routed counts for
kernels with idle qubits can differ from the old target's.
- **Clifford+T and logical targets:**
- `sample` no longer drops idle qubits from its results (the bug fix
above). Kernels that measure explicitly, or touch every qubit, are
unaffected.
  - Level-0 output now unrolls loops and expands register measurements.
- **`translate`** now uses the installed compile target for every
target. That includes remote targets whose QPU supplies its own, and I
haven't tried one.

## Not in this PR, and follow-ups

- **Verifier.** This PR has no `quake-mapped` verifier pass, to keep it
small. Nothing in production runs a profile verifier until
stage-boundary verification, which will be its first user. Until then,
routing correctness rests on the mapper, and on a test that every routed
two-qubit gate acts on neighboring qubits.
- **Device order.** This affects only a kernel that a target routes and
then runs on a simulator, as `compiler-bench-nisq` with a device does.
On such a run, `observe`, `get_state`, and `get_unitary` still return
device order; only `sample` is reordered. On REST hardware, `observe`
goes through `observe-ansatz` and state access isn't offered.
`FermioniqQPU` and the multi-QPU platforms don't apply the reorder index
either, but no routed target runs on them today. The plan's follow-up is
to run routed simulator targets the way REST backends do.
- **Layout.** The layout is the identity. A TODO links NVIDIA#4753 for a
layout search.
- **Compile time.** The mapped stage runs `prepare-for-wireset` even
when its input is already on wires. That keeps it independent of how
stage 1 was lowered, but costs about 0.7 s on a 60-qubit QFT, about a
third of the two stages at level 1. A follow-up removes it: the
`semantics: value` option that the CUDA-Q Logical work adds to
`quake-unmapped` will let stage 1 declare that its output is on wires,
and the mapped stage will then only add the wire set.
- **Interaction with NVIDIA#5777.** Adam's NVIDIA#5777 moves `sample` measurement
insertion after the pipeline, keyed on `quake.evince`. Whichever of the
two merges second adapts. If NVIDIA#5777 lands first, this PR drops its
measure-before-the-pipeline check, because `quake.evince` keeps idle
qubits alive through `dqe`, and `DefaultQPU` takes the reorder index
from the compiler instead of the mapper's attribute.
- **Existing mapper bug.** A kernel with a long loop and an idle qubit
fails in the mapper at level 0. It fails on the old target too.

---------

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

Labels

build The issue related to the CUDA Quantum build codegen issue with generating any codegen output (QIR, OpenQASM etc.) core compiler documentation Improvements or additions to documentation python-lang Anything related to the Python CUDA Quantum language implementation runtime CUDA quantum runtime testing Relates to testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Simulator targets don't specialize kernel arguments before running the target pipeline

5 participants