Repository navigation
Support compile targets with several stages - #5751
Merged
Merged
Conversation
taalexander
requested review from
cabreraam,
lmondada and
sacpis
and removed request for
cabreraam
October 28, 2026 15:54
taalexander
marked this pull request as ready for review
October 28, 2026 15:54
`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
force-pushed
the
feature/composable-stages
branch
from
October 28, 2026 17:23
3517885 to
4628cf3
Compare
sacpis
approved these changes
Oct 28, 2026
sacpis
left a comment
Collaborator
There was a problem hiding this comment.
Overall LGTM. Thanks @taalexander. Left few comments.
sacpis
reviewed
Oct 28, 2026
`__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>
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
enabled auto-merge
October 28, 2026 18:40
CI Summary (
|
| 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 |
lmondada
approved these changes
Oct 28, 2026
CI Summary (
|
| 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
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
enabled auto-merge
October 28, 2026 22:25
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes
StagePasses.LoweringHandler::getPipelineis renamedgetStagePasses.canonicalize, prep,jit-deploy-pipeline, andjit-finalize-pipelineonce, around the handlers' passes. The handlerfactories no longer take
emulateor the emission format.stage's input profile is the previous stage's output profile.
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_mappingnow setsdevice=bypasson everyqubit-mapping,including in override pipelines. It now handles devices such as
grid(3,3)and keeps options written after the device.
unsupported.
Implications
disable_qubit_mappingset, override pipelines arenow rewritten too.
quantum_machinesnow keepsplacement=greedy search=sabreafterdevice=bypass. A bypassed mapping ignores both options.getStagePasses.nvq++still runs only override pipelines, and the PythonCompileTargetaccessors still accept one stage.
expand-measurementsbefore deploy andits reset passes after finalize. So a stage with that profile must come first
if it requests single-qubit measurements or
reset-after-measure, and lastif it requests
reset-after-measure.as optimizing on abstract qubits and then mapping to a device.