Skip to content

Support compile targets with several stages - #5751

Merged
taalexander merged 10 commits into
NVIDIA:mainfrom
taalexander:feature/composable-stages
Oct 29, 2026
Merged

taalexander merged 10 commits into
NVIDIA:mainfrom
taalexander:feature/composable-stages

Conversation

@taalexander

@taalexander taalexander commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

What changes

  • Lowering handlers now return only their own passes, as a StagePasses.
    LoweringHandler::getPipeline is renamed getStagePasses.
  • The compiler builds canonicalize, prep, jit-deploy-pipeline, and
    jit-finalize-pipeline once, around the handlers' passes. The handler
    factories no longer take emulate or the emission format.
  • A target can list several stages. Their passes run in stage order, and each
    stage's input profile is the previous stage's output profile.
  • Only the first stage may add passes before deploy, and only the last may add
    passes after finalize. An override pipeline must be the target's only stage.
    A target that breaks either rule fails to compile with an error that names
    the stage.
  • disable_qubit_mapping now sets device=bypass on every qubit-mapping,
    including in override pipelines. It now handles devices such as grid(3,3)
    and keeps options written after the device.
  • The version 2 YAML parser no longer warns that multi-stage targets are
    unsupported.

Implications

  • Existing pipelines do not change. Every in-tree target has one stage.
  • One behavior change: with disable_qubit_mapping set, override pipelines are
    now rewritten too. quantum_machines now keeps placement=greedy search=sabre after device=bypass. A bypassed mapping ignores both options.
  • Any out-of-tree lowering handler must implement getStagePasses.
  • nvq++ still runs only override pipelines, and the Python CompileTarget
    accessors still accept one stage.
  • The reference-semantics handler adds expand-measurements before deploy and
    its reset passes after finalize. So a stage with that profile must come first
    if it requests single-qubit measurements or reset-after-measure, and last
    if it requests reset-after-measure.
  • This makes two-stage targets possible for the optimization levels work, such
    as optimizing on abstract qubits and then mapping to a device.

@taalexander
taalexander requested review from cabreraam, lmondada and sacpis and removed request for cabreraam October 28, 2026 15:54
@taalexander
taalexander marked this pull request as ready for review October 28, 2026 15:54
@github-actions github-actions Bot added 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 28, 2026
@taalexander taalexander changed the title Feature/composable stages Support compile targets with several stages Oct 28, 2026
`LoweringHandler::getPipeline` becomes `getStagePasses` and returns a
`StagePasses` holding only the handler's own passes. `getPassPipeline`
builds `canonicalize`, prep, `jit-deploy-pipeline`, and
`jit-finalize-pipeline` around them with the code that custom-pipeline
stages already used. A custom stage's high-, mid-, and low-level
pipelines fill the same three places. The handler factories no longer
take `emulate` or the emission format, because the compiler sets the
options of the shared steps itself.

`QuakeReferenceSemanticsHandler` used to build the shared steps from
settings captured at construction. That duplicated the custom path's
text and would repeat the shared steps for each stage of a target
with several stages. It now returns `expand-measurements`,
`basis-conversion`, and its reset passes, which the compiler places
where the handler placed them before.

No pipeline text changes. The `Compiler` built the handler from the
same `emulate` and emission format that `getPassPipeline` reads, and
neither changes after construction. Targets still have at most one
stage. The `disable_qubit_mapping` rewrite now also runs for inferred
stages, where it finds no `qubit-mapping`, so only its info log line
is new.

`profile_providers` now also pins the pipelines of `quantum_machines`,
with and without emulation. Those checks pass before and after the
change, as do the existing `ionq`, `fermioniq`, and `quantinuum`
checks. `profile_stages` drops `test_single_inferred`, which planned
the same target as `test_first_supporting_handler_wins`. The runtime
lit tests and the Quantinuum and IonQ local emulation tests pass.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
The `disable_qubit_mapping` target option sets `device=bypass` on a
target's `qubit-mapping` passes. Its rewrite skipped override
pipelines, changed only the last `qubit-mapping` in the pipeline, and
took everything up to the next comma or closing brace as the device.
So `device=grid(3,3)` became `device=bypass,3)`, which does not parse,
and an option written after the device, such as
`extended-layer-size=4`, was dropped. Targets with several stages can
put a `qubit-mapping` in more than one stage, so the rewrite has to
reach each of them.

Behavior change: the rewrite now also applies to override pipelines,
sets `device=bypass` on every `qubit-mapping`, replaces a device value
that contains commas in full, and keeps the pass's other options. The
one in-tree pipeline this changes is `quantum_machines` with the option
set. Its server helper substitutes
`qubit-mapping{device=file('...') placement=greedy search=sabre}`, so
the rewritten pass now keeps `placement=greedy search=sabre` after
`device=bypass`. A bypassed `qubit-mapping` returns before it reads
either option. No in-tree override contains `qubit-mapping`, and no
in-tree pipeline contains more than one.

`profile_stages` checks a custom stage with two `qubit-mapping` passes,
one on `grid(3,3)` and one with `extended-layer-size=4` after its
device, and an override pipeline with one `qubit-mapping`. The check
fails before this change and passes after it. The runtime lit tests
pass.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
`getPassPipeline` now accepts a target with more than one stage. It
runs `canonicalize`, prep, and `jit-deploy-pipeline` once, joins the
stages' passes in stage order, and ends with `jit-finalize-pipeline`.
Each stage's input profile is the previous stage's output profile, so
handler selection works as it did for one stage.

Only the first stage may add passes before deploy, and only the last
may add passes after finalize. A later stage's pre-deploy passes would
run before the earlier stages' passes, and an earlier stage's
post-finalize passes would run after the later stages' passes, so
either would break stage order. An override pipeline replaces the
whole pipeline, including the other stages' passes, so it must be the
target's only stage. A target that breaks either rule fails to compile
with a diagnostic that names the stage, instead of losing passes
silently. Because `QuakeReferenceSemanticsHandler` adds
`expand-measurements` before deploy and its reset passes after
finalize, a stage with that profile must come first when it requests
single-qubit measurements or `reset-after-measure`, and last when it
requests `reset-after-measure`.

The quantum optimization levels need targets that, for example,
optimize on abstract qubits in one stage and map to a device in the
next. No in-tree target has more than one stage, so no pipeline
changes. The version 2 YAML parser no longer warns that the compiler
does not support multi-stage targets. `nvq++` still runs only override
pipelines, and the Python `CompileTarget` accessors still accept one
stage.

`profile_stages` replaces the test that rejected two stages with a
three-stage pipeline check, and checks both stage-order rejections and
an override after another stage. These checks fail before this change
and pass after it. The runtime lit tests, the target configuration lit
tests and unit tests, and the Quantinuum and IonQ local emulation tests
pass.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
The `getPassPipeline` comment now lists how an override pipeline, a
custom pipeline, and an inferred lowering each supply a stage's
passes, then describes the shared steps, the `disable_qubit_mapping`
rewrite, and the diagnostics.

`getInferredStagePasses` now selects the handler for an inferred stage
and builds its diagnostic, so the stage loop reads as finding a stage's
passes, checking where they may run, and adding them. The diagnostic
text is unchanged.

No behavior changes. The runtime lit tests and the Quantinuum and IonQ
local emulation tests pass.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
@taalexander
taalexander force-pushed the feature/composable-stages branch from 3517885 to 4628cf3 Compare October 28, 2026 17:23

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

Overall LGTM. Thanks @taalexander. Left few comments.

Comment thread runtime/internal/compiler/Compiler.cpp Resolved
Comment thread runtime/internal/compiler/Compiler.cpp Resolved
`__disable_quantum_optimization__` on a C++ kernel marks the source file's
module with `quake.noOptimization`, and `device-code-loader` copied that
module's attributes into the module it registers for every kernel in the
file. Since NVIDIA#5742 the JIT compiles a marked module at level 0, so an
unmarked kernel in the same file lost the target pipeline's
`quake-simplify`, `phase-folding`, and `dqe`. JIT codegen read the module
marker before NVIDIA#5742 too, so those kernels also skipped the conversion to
wires.

`device-code-loader` now keeps the marker in a kernel's registered module
only when the kernel or a function it includes is marked. nvq++ inlines
kernels before it registers them, which would hide a marked callee from
that check, so the bridge also marks every function that refers to a
marked function, directly or through other functions.

Behavior changes:
- An unmarked C++ kernel that shares a source file with a marked kernel
  compiles at the requested level on the JIT path again, and JIT codegen
  now also converts it to wires and optimizes it.
- A kernel that calls a marked kernel now carries the marker itself. On
  the JIT path it already compiled at level 0 because of the file's
  marker. When a target's nvq++ pipeline adds passes that check the
  function, such as `quake-simplify`, `phase-folding`, or `dqe`, they now
  skip it as well.

Ahead-of-time compilation through `cudaq-translate` still reads the file's
module marker, so there it still applies to every kernel in the file.
Python and Logical kernels each have their own module and do not change.

The JIT still optimizes a marked callee that the bridge cannot see: a
kernel from another C++ source file, whose declaration carries no marker,
a marked Python kernel called from another Python kernel, and a kernel
passed in only as a callable argument. The JIT copies such a callee into
the caller's module without marking the module. This was already the case
before NVIDIA#5742.

The `no_optimization` frontend test now also inlines and registers a file
with a marked kernel, a kernel that calls it, and a kernel that does not,
and checks which registered modules keep the marker. Both new checks fail
without this change.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
State where the reference-semantics handler places each pass before the
first- and last-stage rule that follows from it, and note that
`getPassPipeline` also reports a selected handler that returns no passes.
No behavior change.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
@github-actions github-actions Bot added the c++ bridge CUDA-Q in C++ bridge to MLIR compiler label Oct 28, 2026
With `disable_qubit_mapping` set, the device rewrite from b48d764
stopped a device value at the first parenthesis inside it. So
`device=file('/home/abc/xyz (1).json')` became
`device=bypass('/home/abc/xyz (1).json')`, and an unquoted path with
parentheses such as `file(/a/b(1).json)` broke the same way. The
`qubit-mapping` pass accepts both forms: it strips the single quotes that
let a path with spaces through MLIR's pass option parser. Given the
rewritten text, the pass prints an error and maps nothing, and the compile
fails when `raise-fatal-errors` is set.

The rewrite now ends a device value as MLIR's pass option parser does: at
the first space outside a pair of single or double quotes, with an
unpaired quote treated as an ordinary character, as in
`file(/home/o'brien/arch.txt)`. It also ends at `}`, so a quoted span
never runs into the next pass. Parentheses need no special case.

Before b48d764 the rewrite turned such a pass into a valid
`device=bypass` but dropped the pass's other options.

The `profile_stages` test now rewrites a quoted device path with a space
and parentheses, which failed before this change, and an unquoted path
with an apostrophe and parentheses.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
@taalexander
taalexander enabled auto-merge October 28, 2026 18:40
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

CI Summary (push) — ✅ passed

Run #40778598354 · ✅ 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 added this pull request to the merge queue Oct 28, 2026

@lmondada lmondada 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! You can just fold the comment below (just some documentation changes) into one of your following PRs.

@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

CI Summary (merge_group) — ✅ passed

Run #40787164310 · ✅ 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

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 28, 2026
Brings in NVIDIA#5746, whose Python test of multi-stage targets the next commit updates.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
NVIDIA#5746 added `test_multiple_stages_are_rejected`, which expects the
"Multi-stage compilation is currently not supported" error that this
branch removes, so the merge queue failed it on every Python job.

The test's target is still rejected, for a different reason: both
stages ask for single-qubit measurements, so the second stage adds
`expand-measurements` before deploy, and only the first stage may add
passes there. Rename the test for that rule and match its message,
which is the one the merge-queue run reported.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
The `StagePasses` comment now shows the pipeline with the place of each
field. It explains that only the first stage may add passes before
deploy and only the last after finalize, because otherwise a later
stage's passes would run before an earlier stage's. It also says that
`getPassPipeline` checks this for each stage and uses the same struct to
collect all stages' passes.

The `getPassPipeline` comment now starts with how the passes of several
stages interleave, gives an example, and lists how each kind of lowering
supplies a stage's passes. Comments only.

Signed-off-by: Thomas Alexander <talexander@nvidia.com>
@taalexander
taalexander enabled auto-merge October 28, 2026 22:25
@taalexander
taalexander added this pull request to the merge queue Oct 28, 2026
Merged via the queue into NVIDIA:main with commit 3c775df Oct 29, 2026
87 checks passed
@taalexander
taalexander deleted the feature/composable-stages branch October 29, 2026 02:12
github-actions Bot pushed a commit that referenced this pull request Oct 29, 2026
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 c++ bridge CUDA-Q in C++ bridge to MLIR compiler codegen issue with generating any codegen output (QIR, OpenQASM etc.) core compiler 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.

3 participants