Repository navigation
Let compile stages decide when kernel arguments are specialized - #5771
Conversation
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 |
4133421 to
6e1f44a
Compare
6e1f44a to
e42a8a2
Compare
|
Getting the following build error |
1284ee8 to
557596e
Compare
|
@sacpis should be fixed. Base PR is trying to merge now. |
cabreraam
left a comment
There was a problem hiding this comment.
I will defer to @schweitzpgi's sagacious review. I found one corner case that is worth addressing, but other than that, LGTM.
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>
2ec4a7b to
be373be
Compare
sacpis
left a comment
There was a problem hiding this comment.
LGTM. Thanks @taalexander. Thanks for taking care of review.
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 |
| @@ -0,0 +1,36 @@ | |||
| /****************************************************************-*- C++ -*-**** | |||
There was a problem hiding this comment.
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 |
schweitzpgi
left a comment
There was a problem hiding this comment.
Nice work pulling this together, Thomas.
…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>
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>
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.synthesizefirst.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
fullySpecializeon thetarget. Simulators clear it (
DefaultQPU), and setting it there anywayexposed a second bug. Argument synthesis and the run-time
.argsCreatorlayout some types differently (
list[bool], for one), and Python picked thelayout from whether the platform was a local simulator rather than from
whether the compile would actually substitute the arguments. With
fully_specializeset onqpp-cpu, sampling a kernel that takes alist[bool]returned{}.How this fixes it
Stages can now ask for argument values. Lowering handlers get a
requiresArgumentValuesquery, and the quake-unmapped handler says yes whenit synthesizes. A target specializes if it sets
fullySpecializeor one ofits 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
compiler-bench-ftqc-clifford-t, a kernel with live arguments nowrecompiles on every call, because the cache doesn't key on argument values.
That includes every shot of
cudaq.run. Kernels without live argumentsstill 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.
fullySpecialize, so nothingchanges for them. The simulators and
compiler-bench-ftqc-logicalstillcompile once and reuse the result.
CompileOptions. That'sright as long as
supportsonly looks at the profiles, which the built-inhandlers do and the
supportsdoc now requires.