From 8a7bebf96102c82d247cc04349ea0d6ad73dbe76 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 3 Aug 2026 15:17:12 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20Map=20QCO=20programs=20to=20com?= =?UTF-8?q?piler=20targets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5.6 via Codex --- .agent/plans/1687-map-01-target-mapping.md | 433 ++++++++++++++++ CHANGELOG.md | 10 +- .../Dialect/QCO/Transforms/Mapping/Mapping.h | 25 +- .../mlir/Dialect/QCO/Transforms/Passes.td | 17 +- .../lib/Dialect/QCO/Transforms/CMakeLists.txt | 1 + .../QCO/Transforms/Mapping/Mapping.cpp | 395 ++++++++------ .../QCO/Transforms/Mapping/CMakeLists.txt | 5 +- .../QCO/Transforms/Mapping/test_mapping.cpp | 481 ++++++++++++++---- 8 files changed, 1102 insertions(+), 265 deletions(-) create mode 100644 .agent/plans/1687-map-01-target-mapping.md diff --git a/.agent/plans/1687-map-01-target-mapping.md b/.agent/plans/1687-map-01-target-mapping.md new file mode 100644 index 0000000000..1a10f03ccd --- /dev/null +++ b/.agent/plans/1687-map-01-target-mapping.md @@ -0,0 +1,433 @@ +# Map QCO programs to compiler targets + +This ExecPlan is a living document. The sections `Progress`, +`Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must +be kept up to date as work proceeds. + +This ExecPlan must be maintained in accordance with `.agent/PLANS.md` from the +repository root. + +## Purpose / Big Picture + +After this change, callers can run the QCO `place-and-route` pass against an +immutable `mlir::CompilerTarget` instead of constructing a second graph wrapper +from a dense coupling set. The mapper uses the target's validated topology for +placement and routing, emits provider-defined site identifiers in `qco.static` +operations, and continues to insert ordinary `qco.swap` operations without +making target-native gate or direction decisions. + +The behavior is visible in the existing mapping unit-test binary. Tests +construct targets with noncontiguous site identifiers, map scalar and tensor +allocations, and verify that every routed two-qubit operation lies on a target +edge. A small program mapped to a much larger target also demonstrates that +unused target sites are not all materialized as IR operations. + +This task is based on the merged compiler-target foundation at +`f775395a25fddba0a3b54996416d1311bc6ebe71`. It changes the mapper and its tests +only. The high-level `QCOProgram::placeAndRoute` coupling-set API remains in +place for a later pipeline integration task. + +## Progress + +- [x] (2026-08-03 12:29Z) Verified the initial stacked worktree was clean, on + the expected branch, and exactly at the pre-squash compiler-target head + `b6eb95521cb76224c137496f113f38b9ce295854`. +- [x] (2026-08-03 12:29Z) Read the repository agent policy, AI-usage policy, + ExecPlan requirements, MQT review workflow, remediation protocol, and + review rubric. +- [x] (2026-08-03 12:29Z) Audited the pre-MAP mapper, its vote-and-restore + behavior from pull request #1951, the compiler-target API, and existing + mapping tests. +- [x] (2026-08-03 12:31Z) Created this living plan and passed the complete + targeted pre-commit hook set on it. +- [x] (2026-08-03 13:09Z) Replaced the mapper's private graph wrapper with the + `CompilerTarget` topology contract; the old coupling-set entry point is + now only a temporary forwarder for PIPE to delete. +- [x] (2026-08-03 13:09Z) Added one recursive discovery/planning walk that + supports top-level scalar and tensor allocations, rejects nested dynamic + allocations and higher-arity unitaries before mutation, and reuses its + records for routing and placement. +- [x] (2026-08-03 13:09Z) Materialized only program qubits and vacant target + vertices used by the selected routing plan, with provider site IDs in + output. +- [x] (2026-08-03 13:09Z) Added focused mapping, diagnostic, allocation, + sparse-workspace, empty-operation-set, and noncontiguous-ID tests while + preserving all #1951 regressions. +- [x] (2026-08-03 13:09Z) Updated the existing `Unreleased` mapping changelog + entry without adding a pull-request number or upgrade note. +- [x] (2026-08-03 13:09Z) Built the focused targets; passed 27 mapping tests, + 218 compiler tests, changed-source clang-tidy 22.1.8, repository lint, and + `git diff --check`. +- [x] (2026-08-03 13:17Z) Recorded the outcome and created the focused signed + commit with the required AI-assistance trailer. +- [x] (2026-08-03 15:13Z) Restacked the sole MAP commit without conflicts onto + the merged compiler-target squash + `f775395a25fddba0a3b54996416d1311bc6ebe71`; stable patch IDs confirm the + non-plan implementation patch is unchanged. +- [x] (2026-08-03 15:18Z) Reconfigured and rebuilt the target, transforms, and + test interfaces against the merged base; passed 27 mapping tests, all 218 + compiler tests, changed-source clang-tidy 22.1.8, targeted hooks, full + repository lint, and `git diff --check`. + +## Surprises & Discoveries + +- Observation: the pre-MAP mapper's `AugmentedDevice` duplicated topology + storage, neighbour lookup, distances, and maximum degree that `CompilerTarget` + already validates and caches. Evidence: before this task, + `mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp` defined the wrapper, + while `mlir/include/mlir/Compiler/Target.h` exposes `numQubits`, + `areAdjacent`, `forEachNeighbour`, `distanceBetween`, and `maxDegree`. +- Observation: the pre-MAP mapper intentionally maintained a full permutation + over all target vertices, even when the program used fewer qubits. Its + placement phase therefore materialized every target site. The implemented cold + routing preview retains the full virtual permutation while identifying only + the vacant program indices whose qubits are touched by real SWAPs. +- Observation: pull request #1951 added branch-layout voting and restoration to + `qco.index_switch`. That implementation is already on the task base and must + remain the source of truth; importing the older pull request #1687 mapper + would risk dropping those regressions. +- Observation: the compiler-target foundation merged as squash + `f775395a25fddba0a3b54996416d1311bc6ebe71` after MAP-01 was developed on its + pre-squash stack. Replaying the sole MAP commit onto that squash required no + conflict resolution. Before this plan-only evidence update, `git range-diff` + marked the old and new patches with `=`; in the final state, the non-plan + stable patch IDs match, so the current-main target implementation was + preserved unchanged. +- Observation: the installed clang-tidy 22.1.8 does not infer Apple libc++ from + the AppleClang compilation command. The first invocation therefore failed on + `` and was not treated as validation. Re-running with the Xcode + SDK and libc++ include paths made explicit produced clean results for both + changed translation units and the public mapping header. +- Observation: after the restack, CMake did not recognize the cached MLIR + version when invoked without `MLIR_DIR`. Re-running the same release + configuration with the installed LLVM 22.1.3 MLIR package path explicit + succeeded without a source or build-system change. + +## Decision Log + +- Decision: store a cheap value copy of `CompilerTarget` in the pass. Rationale: + the type shares immutable storage, so pass instances own a stable target + contract without rebuilding or duplicating topology. Date/Author: 2026-08-03, + Codex. +- Decision: use dense zero-based compiler vertices for `Layout`, A* search, and + restoration, and translate through `CompilerTarget::siteForVertex` only when + creating `qco.static`. Rationale: layout algorithms require dense indices, + while output IR must retain provider-defined identifiers. Date/Author: + 2026-08-03, Codex. +- Decision: route every one-qubit unitary directly and every two-qubit unitary + solely according to undirected target adjacency. Do not query operation + support, native bases, calibration, or direction. Rationale: MAP-01 owns + topology mapping only; native synthesis belongs to a separate task. + Date/Author: 2026-08-03, Codex. +- Decision: keep the complete target-sized virtual layout, perform a + deterministic cold preview for the chosen initial layout, and materialize only + active program indices plus vacant indices touched by preview SWAPs. + Rationale: the routing algorithm can use workspace without growing small + programs to the complete target in IR. Date/Author: 2026-08-03, Codex. +- Decision: use one recursive discovery/planning walk to validate nested dynamic + allocations and higher-arity unitaries, collect top-level allocation handles, + and record whether two-qubit routing is needed. Store each discovered tensor + chain once. Reuse these records for initial wires, sparse preview, and + allocation replacement; the later recursive walk is mutation-only for + structured-control extension. Rationale: unsupported input is diagnosed + atomically without redundant whole-function diagnostic walks. Date/Author: + 2026-08-03, Codex. +- Decision: preserve the current vote, restore, converge, and region-dispatch + implementation unchanged except where the target topology abstraction or + sparse-wire bookkeeping requires mechanical adaptation. Rationale: the #1951 + regressions are the current mapping contract. Date/Author: 2026-08-03, Codex. +- Decision: add the target-taking factory while retaining the coupling-set + factory as a compatibility adapter that constructs a `CompilerTarget`. + Rationale: MAP-01 must expose a benchmarkable target factory, while the + existing high-level `QCOProgram::placeAndRoute` API remains owned by the later + pipeline task. This is not a new supported compatibility path: PIPE must + delete the coupling-set declaration, adapter, and associated includes as soon + as `Programs.cpp` is migrated. No MAP-01 documentation or test promotes the + adapter. Date/Author: 2026-08-03, Codex. + +## Outcomes & Retrospective + +Implementation and validation are complete. The mapper now owns a cheap +`CompilerTarget` value, uses its dense topology throughout placement and +routing, translates to provider site IDs only for `qco.static`, supports scalar +and mixed allocation forms, and materializes only active or preview-touched +workspace qubits. The single discovery/planning walk makes unsupported nested +allocations and higher-arity operations fail before mutation. + +The existing mapping suite, including #1951 vote-and-restore behavior, passes +with the focused new coverage. The compiler suite, changed-source clang-tidy, +and repository lint also pass. High-level pipeline ownership remains unchanged, +and the temporary coupling-set forwarder is explicitly left for PIPE to delete. +The conflict-free, patch-equivalent restack and fresh validation demonstrate the +same result against the merged compiler-target foundation. + +## Context and Orientation + +`mlir::CompilerTarget`, declared in `mlir/include/mlir/Compiler/Target.h` and +implemented in `mlir/lib/Compiler/Target.cpp`, is an immutable target model. +Provider site identifiers can be sparse or noncontiguous, but the target stores +them in a stable site order and exposes dense compiler vertices for algorithms. +An absent topology denotes all-to-all connectivity; an explicit topology is +validated as a connected undirected graph and has cached distances. + +The QCO mapping pass lives in +`mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp`, with its public factory +in `mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h`. The +target-taking factory stores a cheap `CompilerTarget` value, runs SABRE-style +layout refinement and A* routing against its topology, and rewrites dynamic +qubits to `qco.static`. A temporary legacy factory converts its symmetric +`llvm::DenseSet` input into a target only to keep `Programs.cpp` building until +PIPE removes that seam. A `Layout` remains a complete virtual permutation, but +the IR materializes only active program qubits and vacant indices touched by +routing. + +The mapper walks linear qubit SSA chains using `WireIterator`. Scalar +`qco.alloc` produces one chain directly. A `qtensor.alloc` produces a tensor; +top-level `qtensor.extract` operations establish tensor-backed qubit chains, and +matching insertions return them. Structured `scf.for`, `scf.while`, `qco.if`, +and `qco.index_switch` operations carry qubit chains through nested regions. The +mapper extends these operations with any extra vacant routing wires it +materializes. + +`mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp` owns mapper +behavior tests. Its executable is +`build/release/mlir/unittests/Dialect/QCO/Transforms/Mapping/mqt-core-mlir-unittest-mapping`. +The current suite covers straight-line programs, nested structured control flow, +layout convergence, and the #1951 index-switch vote-and-restore regression. + +The task may modify the mapper header and implementation, their CMake link +dependencies, the mapping unit tests, this ExecPlan, and the existing mapping +entry in `CHANGELOG.md`. It must not remove or redesign +`QCOProgram::placeAndRoute`; pipeline integration is owned by a later task. No +other worktree may be modified, and no GitHub action is authorized. + +## Plan of Work + +First, add a public mapping factory taking `const CompilerTarget&`. Retain the +dense coupling-set overload as a compatibility adapter that validates its legacy +symmetric input and constructs a target before creating the same pass. Link the +QCO transforms library and mapping unit test to `MQTCompilerTarget` without +creating a dependency cycle. Retain the pass options so benchmark code can +construct a pass directly for a given target. + +In `Mapping.cpp`, remove `AugmentedDevice` and store `CompilerTarget` directly. +Adapt every algorithmic query to the target's dense-vertex API: `numQubits`, +`areAdjacent`, `distanceBetween`, `forEachNeighbour`, and `maxDegree`. For the +F-graph, construct dense vertices with `llvm::seq(target.numQubits())`. Do not +call `supports`, inspect operations, or filter neighbours by native-gate +direction. + +Before mutation, recursively walk the entry function once. Reject any scalar or +tensor allocation outside the entry function's top-level body with an +operation-local diagnostic. Reject every non-barrier `UnitaryOpInterface` with +more than two input qubits and explain that it must be decomposed first. During +the same discovery, record top-level allocation handles and whether two-qubit +routing is required. Traverse each recorded tensor chain once and retain the +operations for placement. These records become the initial routing wires, drive +the sparse preview decision, and replace allocations without another discovery +walk. + +Extend computation discovery to append top-level scalar allocation results +before tensor extracts. Extend placement to replace scalar allocations with +their assigned static values and erase them. Tensor allocation, extraction, +insertion, and deallocation behavior remains otherwise unchanged, including the +existing extraction-before-insertion restriction. This order creates one stable +program index sequence for scalar and tensor qubits and supports programs +produced by the qubit-reuse pass. + +Add touched-program tracking to cold and hot SWAP insertion. After choosing an +initial layout, run one cold forward preview. Start the materialization set with +all active program indices, add every vacant index touched by preview SWAPs, +sort it, and create `qco.static` only for those entries. Use +`target.siteForVertex(layout.getHardwareIndex(program))` as each static +operation's index. Add sinks and wire metadata only for materialized vacant +programs. The following hot route must reproduce the preview and can assert that +both operands of every emitted SWAP have wires. + +Keep `qco.swap` emission in its existing ordinary, symmetric form. Preserve all +current route, vote, restore, converge, and structured-region behavior. Update +comments, pass documentation, and diagnostics so they describe target topology +and the supported one-/two-qubit boundary accurately. + +Refactor the mapping test fixture to hold a `CompilerTarget`. Adapt executable +checking to translate static provider IDs back to dense vertices before testing +adjacency. Preserve the existing nine-qubit grid and every current regression. +Add focused tests that demonstrate arbitrary one- and two-qubit unitary names +are routed without native-capability checks; nested higher-arity operations and +nested scalar/tensor allocations fail with diagnostics; scalar-only, mixed +scalar/tensor, and reuse-shaped programs map successfully; noncontiguous target +IDs appear in `qco.static`; and a small routed program on a large target creates +only the needed static workspace rather than one operation per target site. + +Finally, extend the existing `Unreleased` place-and-route changelog entry to +mention compiler-target topology and scalar allocation support. Do not invent a +pull-request reference and do not add an upgrade note because the high-level API +has not changed yet. + +## Concrete Steps + +Run all commands from the repository root. Configure the task worktree with the +repository's release preset and LLVM/MLIR 22 toolchain: + + ./.agent/run.sh cmake --preset release + +Build the target model, QCO transforms, and mapping test executable: + + ./.agent/run.sh cmake --build --preset release \ + --target MQTCompilerTarget MLIRQCOTransforms \ + mqt-core-mlir-unittest-mapping + +Run the mapping tests directly while iterating: + + ./.agent/run.sh \ + ./build/release/mlir/unittests/Dialect/QCO/Transforms/Mapping/\ + mqt-core-mlir-unittest-mapping + +Run the compiler-target and compiler-pipeline tests because the public factory +and link graph depend on their target contract: + + ./.agent/run.sh \ + ./build/release/mlir/unittests/Compiler/\ + mqt-core-mlir-unittests-compiler + +Run targeted pre-commit hooks on this plan before committing: + + ./.agent/run.sh prek run --files \ + .agent/plans/1687-map-01-target-mapping.md + +Use the configured compilation database for changed-source clang-tidy, then run +the repository lint session: + + ./.agent/run.sh uvx nox -s lint + +Finish with: + + git diff --check + git status --short + +Record exact pass counts and any environment-limited check in this plan before +the final commit. + +## Validation and Acceptance + +The implementation is accepted when the mapping test binary passes every +pre-existing test, including index-switch vote-and-restore, and the following +new behavior is observed. + +A `CompilerTarget` with noncontiguous provider IDs maps using dense internal +vertices, while every emitted `qco.static` index is one of the provider IDs. +Arbitrary one- and two-qubit QCO unitaries pass through the topology-only +mapper; two-qubit operations are adjacent after routing regardless of target +operation metadata or operand direction. Inserted routing operations remain +ordinary `qco.swap`. + +Top-level scalar allocations, tensor allocations, mixed allocation forms, and +reuse-shaped scalar programs map and verify. Scalar and tensor allocations +inside nested regions fail before placement with a specific diagnostic. A +non-barrier unitary acting on more than two qubits fails even when nested inside +structured control flow, with a diagnostic requiring one-/two-qubit +decomposition. + +For a small program on a large connected target, the number of `qco.static` +operations is smaller than the target size and equals the active qubits plus +only vacant vertices actually touched by routing. The result verifies, all +routed two-qubit operations are adjacent, and every materialized qubit has a +terminal use. + +The factory remains directly callable as +`qco::createMappingPass(const CompilerTarget&, MappingPassOptions)`. The +high-level `QCOProgram::placeAndRoute` API is still present. The changelog entry +is updated without a fabricated pull-request link and `UPGRADING.md` remains +unchanged. + +All focused mapping and compiler tests pass. Changed-source clang-tidy, +repository lint, and `git diff --check` pass, or an environmental boundary is +recorded with exact evidence rather than hidden. + +## Idempotence and Recovery + +Configuration, builds, and tests are repeatable inside the task worktree. +Generated build output stays under `build/` and caches stay under `.cache/`. +Formatting hooks may update the files in scope; inspect and include only +intentional changes. If a build fails partway, rerun the same target build +without deleting another worktree or shared resource. + +The cold preview does not mutate IR. If preview routing fails, the pass reports +failure before dynamic allocation replacement. Hot routing begins only after the +exact materialized workspace is known. + +No external GitHub mutation is authorized. Do not fetch into another worker's +branch, push, open or edit a pull request, comment, resolve threads, merge, or +remove any worktree. + +## Artifacts and Notes + +The current restack evidence is: + + HEAD parent and merge-base: + f775395a25fddba0a3b54996416d1311bc6ebe71 + Branch delta relative to that head: + 1 commit, clean worktree before the plan evidence update + Patch comparison: + final range-diff differs only in this plan; non-plan stable patch IDs match + Current target API: + dense vertices plus provider site IDs, validated connected topology, + cached adjacency, neighbours, distances, and maximum degree + Current mapper: + CompilerTarget directly, with a temporary coupling-set forwarding adapter + +Fresh validation evidence after the restack: + + configure: + MLIR_DIR=.../llvm-22.1.3/lib/cmake/mlir cmake --preset release + passed after the cached invocation omitted the required version hint + focused build: + MQTCompilerTarget, MLIRQCOTransforms, + mqt-core-mlir-unittest-mapping, + mqt-core-mlir-unittests-compiler + passed in 261 build steps + mapping tests: + 27 tests from 1 suite passed + compiler tests: + 218 tests from 8 suites passed + clang-tidy: + LLVM 22.1.8, explicit Xcode SDK/libc++ paths + Mapping.cpp and test_mapping.cpp passed with scoped header filters + targeted hooks: + the exact eight-file MAP patch passed after formatting this plan + repository lint: + uvx nox -s lint passed the complete all-file hook set + whitespace: + git diff --check passed + +## Interfaces and Dependencies + +At completion, `mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h` +declares: + + std::unique_ptr + createMappingPass(const CompilerTarget& target, + MappingPassOptions options); + +It also temporarily retains the existing coupling-set overload solely to keep +the current `QCOProgram::placeAndRoute` implementation building; the adapter +creates a `CompilerTarget`, and no mapping algorithm consumes the set directly. +PIPE must remove this declaration and implementation rather than preserve or +document it as a supported compatibility API. + +`MappingPass` owns a `CompilerTarget` value. It uses only `numQubits`, +`siteForVertex`, `areAdjacent`, `distanceBetween`, `forEachNeighbour`, and +`maxDegree` from that target. Operation-capability, native-gate, calibration, +duration, fidelity, and directed-locus APIs are deliberately out of scope. + +`MLIRQCOTransforms` depends on `MQTCompilerTarget`. The mapping test target also +links the target library directly when needed. No new third-party dependency is +introduced. + +Revision note: the initial plan recorded the exact pre-squash stacked base, +current vote-and-restore contract, approved MAP-01 scope, implementation +strategy, and required validation before feature edits began. This revision +records the conflict-free, patch-equivalent restack onto the merged +compiler-target squash and the temporary compatibility forwarder that PIPE must +delete. It also records the single discovery/planning traversal, completed +implementation, and validation evidence. diff --git a/CHANGELOG.md b/CHANGELOG.md index d422bb4b47..7e8effdc91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,10 +66,12 @@ releases may include breaking changes. - ✨ Add conversions between `jeff` and QCO ([#1479], [#1548], [#1565], [#1637], [#1676], [#1706], [#1776], [#1836], [#1934]) ([**@denialhaag**], [**@burgholzer**]) -- ✨ Add a `place-and-route` pass for mapping circuits to architectures with - restricted topologies ([#1537], [#1547], [#1568], [#1581], [#1583], [#1588], - [#1600], [#1664], [#1709], [#1716], [#1748], [#1805], [#1870], [#1904], - [#1911], [#1951]) ([**@MatthiasReumann**], [**@burgholzer**]) +- ✨ Add a `place-and-route` pass for mapping scalar- and tensor-allocated + circuits to compiler-target topologies while preserving provider site IDs and + materializing routing workspace on demand ([#1537], [#1547], [#1568], [#1581], + [#1583], [#1588], [#1600], [#1664], [#1709], [#1716], [#1748], [#1805], + [#1870], [#1904], [#1911], [#1951]) ([**@MatthiasReumann**], + [**@burgholzer**]) - ✨ Add a pass for qubit reuse in quantum programs, as well as related auxiliary passes and patterns ([#1705], [#1755], [#1756], [#1923], [#1924]) ([**@DRovara**], [**@burgholzer**]) diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h b/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h index 544f1872c7..3956c2e707 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h @@ -12,20 +12,35 @@ #include "mlir/Dialect/QCO/Transforms/Passes.h" +#include #include #include #include +#include #include +#include -namespace mlir::qco { +namespace mlir { + +class CompilerTarget; + +namespace qco { + +/** + * @brief Create a mapping pass instance for a compiler target. + * @returns a pass object. + */ +std::unique_ptr createMappingPass(const CompilerTarget& target, + MappingPassOptions options); /** - * @brief Create a mapping pass instance with the given target architecture. + * @brief Create a mapping pass instance for a legacy symmetric coupling set. * @returns a pass object. */ std::unique_ptr -createMappingPass(const llvm::DenseSet>&, - MappingPassOptions); +createMappingPass(const llvm::DenseSet>& couplingSet, + MappingPassOptions options); -} // namespace mlir::qco +} // namespace qco +} // namespace mlir diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 19d2546c5e..544689e71d 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -168,13 +168,20 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { let summary = "This pass ensures that a program meets the connectivity " "constraints of a target architecture."; let description = [{ - This pass maps the dynamically allocated qubits in a quantum program to the static qubits of a target architecture. - The pass performs both placement and routing of the qubits to ensure that all two-qubit operations in the program - can be executed on the target architecture. + This pass maps top-level scalar and tensor-backed dynamically allocated + qubits in a quantum program to the static sites of a `CompilerTarget`. It + uses only the target's undirected coupling topology: arbitrary one-qubit + operations pass through, and every two-qubit operation is routed to adjacent + target vertices. Higher-arity unitaries must be decomposed before mapping. + Provider-defined site identifiers are retained in the resulting + `qco.static` operations. First, the pass assigns static qubits to the dynamically allocated ones by creating an initial dynamic-to-static - mapping, which is referred to as the initial layout. Then, it traverses the circuit and inserts `SWAP` operations to - ensure that all two-qubit operations are executable on the target architecture, a process known as routing. + mapping, which is referred to as the initial layout. The layout covers the + complete target virtually, while only program qubits and vacant sites used + as routing workspace are materialized. Then, the pass traverses the circuit + and inserts `qco.swap` operations to ensure that all two-qubit operations + are executable on the target architecture, a process known as routing. For routing, the pass first divides the circuit into layers. A layer is a set of independently executable (sequences or blocks of) two-qubit operations. Subsequently, the pass performs an A* search for each layer to find and insert a diff --git a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt index 0f20eec298..3427ad5280 100644 --- a/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt +++ b/mlir/lib/Dialect/QCO/Transforms/CMakeLists.txt @@ -13,6 +13,7 @@ add_mlir_library( ${PASSES_SOURCES} LINK_LIBS PRIVATE + MQTCompilerTarget MLIRQCODialect MLIRQCOUtils MLIRQTensorUtils diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 72a440c643..fd16e806e0 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -10,6 +10,7 @@ #include "mlir/Dialect/QCO/Transforms/Mapping/Mapping.h" +#include "mlir/Compiler/Target.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" #include "mlir/Dialect/QCO/IR/QCOOps.h" @@ -21,11 +22,11 @@ #include "mlir/Dialect/QTensor/Utils/TensorIterator.h" #include "mlir/Dialect/Utils/Utils.h" +#include #include #include #include #include -#include #include #include #include @@ -53,7 +54,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -83,51 +86,10 @@ struct MappingPass : impl::MappingPassBase { enum class RoutingMode : bool { Cold, Hot }; - class AugmentedDevice { - public: - explicit AugmentedDevice( - const DenseSet>& couplingSet) - : coupling_(couplingSet), dist_(coupling_.getDistMatrix()) {} - - /// Return the device's number of qubits. - [[nodiscard]] size_t nqubits() const { return coupling_.getNumNodes(); } - - /// Return true if two qubits are adjacent. - [[nodiscard]] bool areAdjacent(const size_t u, const size_t v) const { - return dist_[u][v] == 1UL; - } - - /// Return the length of the shortest path between two qubits. - [[nodiscard]] size_t distanceBetween(const size_t u, const size_t v) const { - const auto dist = dist_[u][v]; - if (dist == UINT64_MAX) { - report_fatal_error("Failed to compute the distance between qubits " + - Twine(u) + " and " + Twine(v)); - } - return dist; - } - - /// Return the qubit identifiers. - [[nodiscard]] SmallVector qubits() const { - return coupling_.getNodes(); - } - - /// Return all neighbours of a qubit. - [[nodiscard]] ArrayRef neighboursOf(const size_t u) const { - return coupling_.getNeighbours(u); - } - - /// Return the max degree (connectivity) of any qubit of the device. - [[nodiscard]] size_t maxDegree() const { return coupling_.getMaxDegree(); } - - private: - Graph coupling_; - Graph::DistanceMatrix dist_; - }; - struct WireInfos { /// Return the mapped wire index of a program index. [[nodiscard]] size_t lookupIndex(const size_t prog) const { + assert(containsProgram(prog) && "program index is not mapped"); return programToIndex_[prog]; } @@ -147,6 +109,12 @@ struct MappingPass : impl::MappingPassBase { } indexToProgram_[index] = prog; programToIndex_[prog] = index; + programs_.insert(prog); + } + + /// Return whether a program index has a corresponding wire. + [[nodiscard]] bool containsProgram(const size_t prog) const { + return programs_.contains(prog); } /// Swap two program indices. @@ -165,11 +133,27 @@ struct MappingPass : impl::MappingPassBase { SmallVector indexToProgram_; /// Maps a program index to the i-th wire index. SmallVector programToIndex_; + /// Program indices that have corresponding wires. + DenseSet programs_; + }; + + struct TensorAllocation { + qtensor::AllocOp allocation; + SmallVector operations; + }; + + struct Computation { + Wires wires; + WireInfos infos; + SmallVector scalarAllocations; + SmallVector tensorAllocations; + bool hasTwoQubitOperations{false}; }; /// Statistics collected while routing. struct Statistics { size_t nswaps{0}; + DenseSet touchedPrograms; }; /// Parameters influencing the behavior of the A* search algorithm. @@ -207,20 +191,20 @@ struct MappingPass : impl::MappingPassBase { /// Construct a non-root node from its parent node. Apply the given swap to /// the layout of the parent node. Node(Node* parent, const IndexPairType& swap, const Window& window, - const AugmentedDevice& device, const Parameters& params) + const CompilerTarget& target, const Parameters& params) : layout(parent->layout), swap(swap), parent(parent), depth(parent->depth + 1), f(0) { layout.swap(swap.first, swap.second); - f = g(params.alpha) + h(window, device, params); // NOLINT + f = g(params.alpha) + h(window, target, params); // NOLINT } /// Return true, if the current SWAP sequence makes all gates in the front /// executable. [[nodiscard]] bool isGoal(const IndexPairType& front, - const AugmentedDevice& device) const { + const CompilerTarget& target) const { const auto [hw0, hw1] = layout.getHardwareIndices(front.first, front.second); - return device.areAdjacent(hw0, hw1); + return target.areAdjacent(hw0, hw1); } private: @@ -237,7 +221,7 @@ struct MappingPass : impl::MappingPassBase { /// between its hardware qubits. Intuitively, this is the number of SWAPs /// that a naive router would insert to route the layers (with a constant /// layout). - [[nodiscard]] float h(const Window& window, const AugmentedDevice& device, + [[nodiscard]] float h(const Window& window, const CompilerTarget& target, const Parameters& params) const { float costs{0}; float decay{1.}; @@ -245,7 +229,7 @@ struct MappingPass : impl::MappingPassBase { for (const auto& [i, progs] : enumerate(window)) { const auto [prog0, prog1] = progs; const auto [hw0, hw1] = layout.getHardwareIndices(prog0, prog1); - const size_t nswaps = device.distanceBetween(hw0, hw1) - 1; + const size_t nswaps = target.distanceBetween(hw0, hw1) - 1; costs += decay * static_cast(nswaps); decay *= params.lambda; } @@ -255,19 +239,20 @@ struct MappingPass : impl::MappingPassBase { /// Describes the graph F of arXiv:1602.05150v3. struct FGraph { - explicit FGraph(std::shared_ptr device) - : f_(device->qubits()), device_(std::move(device)) {}; + explicit FGraph(const CompilerTarget& target) + : f_(llvm::to_vector(llvm::seq(target.numQubits()))), + target_(&target) {}; /// Build F-graph: Add edges to F for each edge in the coupling graph. /// Note that this assumes that the coupling graph is directed, but /// symmetric (essentially: undirected). void construct(const Layout& from, const Layout& to) { - for (const auto u : device_->qubits()) { - for (const auto v : device_->neighboursOf(u)) { + for (size_t u = 0; u < target_->numQubits(); ++u) { + target_->forEachNeighbour(u, [&](const auto v) { if (shouldAddEdge(u, v, from, to)) { f_.addEdge(u, v); } - } + }); } } @@ -316,12 +301,12 @@ struct MappingPass : impl::MappingPassBase { const Layout& from, const Layout& to) const { const auto dest = to.getHardwareIndex(from.getProgramIndex(u)); - return device_->distanceBetween(v, dest) < - device_->distanceBetween(u, dest); + return target_->distanceBetween(v, dest) < + target_->distanceBetween(u, dest); } Graph f_; - std::shared_ptr device_; + const CompilerTarget* target_; }; public: @@ -332,11 +317,10 @@ struct MappingPass : impl::MappingPassBase { explicit MappingPass(const MappingPassOptions& options) : MappingPassBase(options) {} - /// Construct mapping from coupling set. - explicit MappingPass(const DenseSet>& couplingSet, + /// Construct mapping for a compiler target. + explicit MappingPass(const CompilerTarget& compilerTarget, const MappingPassOptions& options) - : MappingPassBase(options), - device(std::make_shared(couplingSet)) {} + : MappingPassBase(options), target(compilerTarget) {} protected: void runOnOperation() override { @@ -344,8 +328,8 @@ struct MappingPass : impl::MappingPassBase { assert(niterations > 0 && "expected niterations > 0"); assert(ntrials > 0 && "expected ntrials > 0"); - if (!device) { - llvm::reportFatalUsageError("No device specified!"); + if (!target) { + llvm::reportFatalUsageError("No compiler target specified!"); } IRRewriter rewriter(&getContext()); @@ -358,20 +342,21 @@ struct MappingPass : impl::MappingPassBase { return; } - auto comp = getComputation(func); + auto comp = discoverComputation(func); if (failed(comp)) { signalPassFailure(); return; } auto& body = func.getFunctionBody(); - auto& [wires, infos] = *comp; + auto& wires = comp->wires; + auto& infos = comp->infos; - if (wires.size() > device->nqubits()) { + if (wires.size() > target->numQubits()) { func.emitError() << "requires " + Twine(wires.size()) + " qubits. However, the architecture only supports " + - Twine(device->nqubits()) + "qubits."; + Twine(target->numQubits()) + " qubits."; signalPassFailure(); return; } @@ -383,7 +368,26 @@ struct MappingPass : impl::MappingPassBase { return; } - std::tie(wires, infos) = std::move(place(body, *layout, rewriter)); + SmallVector materializedPrograms(wires.size()); + std::iota(materializedPrograms.begin(), materializedPrograms.end(), 0); + if (comp->hasTwoQubitOperations) { + RoutingBundle preview{.wires = wires, .infos = infos, .layout = *layout}; + Statistics previewStats; + if (failed(route(preview, previewStats))) { + func.emitError() << "failed to plan target routing"; + signalPassFailure(); + return; + } + for (const auto prog : previewStats.touchedPrograms) { + if (prog >= wires.size()) { + materializedPrograms.emplace_back(prog); + } + } + std::ranges::sort(materializedPrograms); + } + + std::tie(wires, infos) = std::move( + place(body, *layout, *target, materializedPrograms, *comp, rewriter)); Statistics stats; RoutingBundle bundle{.wires = std::move(wires), @@ -545,11 +549,11 @@ struct MappingPass : impl::MappingPassBase { } /// Return the wires of a dynamic computation. - /// The mapping pass currently assumes that - /// - there are no `qco.alloc` operation - /// - there is an "extraction" and "insertion" phase, where the i-th extract - /// defines the i-th program qubit - /// Thus, supported programs have the following structure: + /// Scalar `qco.alloc` operations define program qubits directly. For + /// `qtensor` allocations, the mapping pass assumes an extraction and + /// insertion phase where the i-th extract defines the i-th tensor-backed + /// program qubit. Thus, supported tensor programs have the following + /// structure: /// /// T ⨉ [qtensor::AllocOp] /// → N ⨉ [qtensor::ExtractOp] @@ -559,81 +563,130 @@ struct MappingPass : impl::MappingPassBase { /// /// If any of the above assumptions are violated, the function returns /// failure. - static FailureOr> - getComputation(func::FuncOp func) { - if (!func.getOps().empty()) { - return func.emitError() << "must not contain qco.alloc operations"; + static FailureOr discoverComputation(func::FuncOp func) { + Computation computation; + + const auto discovery = func.walk([&](Operation* op) { + if (auto unitary = dyn_cast(op)) { + if (isa(op)) { + return WalkResult::advance(); + } + if (unitary.getNumQubits() > 2) { + unitary.emitError() + << "cannot route an operation acting on " + << unitary.getNumQubits() + << " qubits; decompose it to one- and two-qubit operations " + "first"; + return WalkResult::interrupt(); + } + computation.hasTwoQubitOperations |= unitary.getNumQubits() == 2; + } + + if (!isa(op)) { + return WalkResult::advance(); + } + if (op->getParentRegion() == &func.getFunctionBody()) { + TypeSwitch(op) + .Case([&](AllocOp alloc) { + computation.scalarAllocations.emplace_back(alloc); + }) + .Case([&](qtensor::AllocOp alloc) { + computation.tensorAllocations.emplace_back( + TensorAllocation{.allocation = alloc}); + }); + return WalkResult::advance(); + } + + op->emitError() + << "target mapping requires dynamic qubit allocations in the entry " + "function body"; + return WalkResult::interrupt(); + }); + if (discovery.wasInterrupted()) { + return failure(); } - Wires wires; - WireInfos infos; + for (auto alloc : computation.scalarAllocations) { + const auto index = computation.wires.size(); + computation.wires.emplace_back(alloc.getResult()); + computation.infos.insertOrUpdate(index, index); + } - for (auto alloc : func.getOps()) { + for (auto& tensor : computation.tensorAllocations) { bool isInitPhase = true; - TensorIterator it(alloc.getResult()); + TensorIterator it(tensor.allocation.getResult()); for (; it != std::default_sentinel; ++it) { - if (auto extract = dyn_cast(it.operation())) { + Operation* const operation = it.operation(); + tensor.operations.emplace_back(operation); + + if (auto extract = dyn_cast(operation)) { if (!isInitPhase) { return func.emitError() << "must extract and insert all qubits at once."; } const auto qubit = extract.getResult(); - const auto index = wires.size(); + const auto index = computation.wires.size(); - wires.emplace_back(qubit); - infos.insertOrUpdate(index, index); + computation.wires.emplace_back(qubit); + computation.infos.insertOrUpdate(index, index); continue; } - if (isa(it.operation())) { + if (isa(operation)) { isInitPhase = false; continue; } } } - return std::make_pair(wires, infos); + return computation; } - /// Perform placement by - /// - initializing as many hardware qubits as the architecture supports - /// - replacing dynamic with static qubits - /// - extending the inputs of `scf::ForOp` to all hardware qubits. + /// Perform placement by replacing dynamic qubits with static target sites + /// and extending control-flow operations with target sites used for routing. /// - /// Analogously to the getComputation function, the i-th extract + /// Analogously to the discoverComputation function, the i-th extract /// operation defines the i-th program qubit. - static std::pair place(Region& body, const Layout& layout, - IRRewriter& rewriter) { - SmallVector staticOps; - staticOps.reserve(layout.nqubits()); + static std::pair + place(Region& body, const Layout& layout, + const CompilerTarget& compilerTarget, + const ArrayRef materializedPrograms, Computation& computation, + IRRewriter& rewriter) { + SmallVector staticQubits(layout.nqubits()); - // Create and save static qubit operations. rewriter.setInsertionPointToStart(&body.front()); - for (size_t i = 0; i < layout.nqubits(); ++i) { - const auto op = StaticOp::create(rewriter, body.getLoc(), i); - staticOps.emplace_back(op); + for (const auto prog : materializedPrograms) { + const auto hw = layout.getHardwareIndex(prog); + const auto site = compilerTarget.siteForVertex(hw); + auto op = StaticOp::create(rewriter, body.getLoc(), site); + staticQubits[prog] = op.getQubit(); rewriter.setInsertionPointAfter(op); } - // Replace extract ops and collect in program-qubit order. - Wires wires; WireInfos infos; - for (auto alloc : make_early_inc_range(body.getOps())) { - TensorIterator it(alloc.getResult()); - while (it != std::default_sentinel) { - // Get the operation and early increment to avoid issues after erasure. - Operation* curr = it.operation(); - ++it; + for (auto alloc : computation.scalarAllocations) { + const auto prog = wires.size(); + const auto qubit = staticQubits[prog]; + assert(qubit && "expected program qubit to be materialized"); + + rewriter.replaceAllUsesWith(alloc.getResult(), qubit); + rewriter.eraseOp(alloc); + + wires.emplace_back(qubit); + infos.insertOrUpdate(prog, prog); + } - TypeSwitch(curr) + for (auto& tensor : computation.tensorAllocations) { + for (Operation* const operation : tensor.operations) { + TypeSwitch(operation) .Case([&](auto op) { const auto prog = wires.size(); - const auto hw = layout.getHardwareIndex(prog); - const auto qubit = staticOps[hw].getQubit(); + const auto qubit = staticQubits[prog]; + assert(qubit && "expected program qubit to be materialized"); rewriter.replaceAllUsesWith(op.getResult(), qubit); rewriter.replaceAllUsesWith(op.getOutTensor(), op.getTensor()); @@ -651,26 +704,23 @@ struct MappingPass : impl::MappingPassBase { .Case([&](auto op) { rewriter.eraseOp(op); }); } - rewriter.eraseOp(alloc); + rewriter.eraseOp(tensor.allocation); } - // Create sinks for remaining, unused, static qubits. - + const auto numProgramQubits = wires.size(); rewriter.setInsertionPoint(body.back().getTerminator()); - for (size_t prog = wires.size(); prog < layout.nqubits(); ++prog) { - const auto hw = layout.getHardwareIndex(prog); - const auto qubit = staticOps[hw].getQubit(); + for (const auto prog : materializedPrograms) { + if (prog < numProgramQubits) { + continue; + } + const auto qubit = staticQubits[prog]; wires.emplace_back(qubit); - infos.insertOrUpdate(prog, prog); + infos.insertOrUpdate(wires.size() - 1, prog); SinkOp::create(rewriter, body.getLoc(), qubit); } - // Finally, update the SCF operations such that they take all static qubits - // as input. To handle recursively nested SCF operations, use a stack of - // (region, mapping) pairs. - SmallVector>> stack; stack.emplace_back(body, DenseSet{}); @@ -688,7 +738,7 @@ struct MappingPass : impl::MappingPassBase { } }) .Case([&](scf::ForOp forOp) { - assert(qubits.size() == layout.nqubits()); + assert(qubits.size() == materializedPrograms.size()); llvm::for_each(getQubitValues(forOp.getInits()), [&](Value v) { qubits.erase(v); }); @@ -709,7 +759,7 @@ struct MappingPass : impl::MappingPassBase { DenseSet(regionQubits.begin(), regionQubits.end())); }) .Case([&](scf::WhileOp whileOp) { - assert(qubits.size() == layout.nqubits()); + assert(qubits.size() == materializedPrograms.size()); llvm::for_each(getQubitValues(whileOp.getInits()), [&](Value v) { qubits.erase(v); }); @@ -735,7 +785,7 @@ struct MappingPass : impl::MappingPassBase { DenseSet(afterArgs.begin(), afterArgs.end())); }) .Case([&](IfOp ifOp) { - assert(qubits.size() == layout.nqubits()); + assert(qubits.size() == materializedPrograms.size()); llvm::for_each(ifOp.getQubits(), [&](Value v) { qubits.erase(v); }); @@ -758,7 +808,7 @@ struct MappingPass : impl::MappingPassBase { DenseSet(elseArgs.begin(), elseArgs.end())); }) .Case([&](IndexSwitchOp switchOp) { - assert(qubits.size() == layout.nqubits()); + assert(qubits.size() == materializedPrograms.size()); llvm::for_each(switchOp.getTargets(), [&](Value value) { qubits.erase(value); }); @@ -813,7 +863,7 @@ struct MappingPass : impl::MappingPassBase { trials.emplace_back( RoutingBundle{.wires = wires, .infos = infos, - .layout = Layout::random(device->nqubits(), rng())}); + .layout = Layout::random(target->numQubits(), rng())}); } parallelForEach(&getContext(), trials, [&, this](Trial& t) { @@ -859,7 +909,7 @@ struct MappingPass : impl::MappingPassBase { const Layout& layout) const { constexpr size_t cap = 25'000'000UL; - const size_t b = device->maxDegree() * ((device->nqubits() + 1) / 2); + const size_t b = target->maxDegree() * ((target->numQubits() + 1) / 2); const size_t budget = std::min(b * b * b, cap); const Parameters params{.alpha = alpha, .lambda = lambda}; @@ -870,7 +920,7 @@ struct MappingPass : impl::MappingPassBase { // Early exit, if the root node is a goal node already. Node* root = std::construct_at(arena.Allocate(), layout); - if (root->isGoal(window.front(), *device)) { + if (root->isGoal(window.front(), *target)) { return SmallVector{}; } @@ -904,7 +954,7 @@ struct MappingPass : impl::MappingPassBase { // If the currently visited node is a goal node, reconstruct the // sequence of SWAPs from this node to the root. - if (curr->isGoal(window.front(), *device)) { + if (curr->isGoal(window.front(), *target)) { SmallVector seq(curr->depth); size_t j = seq.size() - 1; for (const Node* n = curr; n->parent != nullptr; n = n->parent) { @@ -920,18 +970,18 @@ struct MappingPass : impl::MappingPassBase { expansionSet.clear(); for (const auto& [q0, q1] = window.front(); const auto prog : {q0, q1}) { - for (const auto hw0 = curr->layout.getHardwareIndex(prog); - const auto hw1 : device->neighboursOf(hw0)) { + const auto hw0 = curr->layout.getHardwareIndex(prog); + target->forEachNeighbour(hw0, [&](const auto hw1) { // Ensure consistent hashing/comparison. const IndexPairType swap = std::minmax(hw0, hw1); if (is_contained(expansionSet, swap)) { - continue; + return; } expansionSet.push_back(swap); frontier.emplace(std::construct_at(arena.Allocate(), curr, swap, - window, *device, params)); - } + window, *target, params)); + }); } ++i; @@ -945,7 +995,7 @@ struct MappingPass : impl::MappingPassBase { [[nodiscard]] SmallVector restore(const Layout& from, const Layout& to) const { Layout curr(from); - FGraph f(device); + FGraph f(*target); SmallVector swaps; while (true) { @@ -984,7 +1034,7 @@ struct MappingPass : impl::MappingPassBase { SmallVector> converge(const Layout& lhs, const Layout& rhs) const { std::array layouts{Layout(lhs), Layout(rhs)}; - std::array graphs{FGraph(device), FGraph(device)}; + std::array graphs{FGraph(*target), FGraph(*target)}; std::array, 2> swaps{}; std::mt19937 gen(seed); @@ -1148,9 +1198,13 @@ struct MappingPass : impl::MappingPassBase { Statistics& stats, IRRewriter* rewriter) { auto& [wires, infos, layout] = bundle; for (const auto& [hw0, hw1] : swaps) { - if constexpr (Mode == RoutingMode::Hot) { - const auto [prog0, prog1] = layout.getProgramIndices(hw0, hw1); + const auto [prog0, prog1] = layout.getProgramIndices(hw0, hw1); + stats.touchedPrograms.insert(prog0); + stats.touchedPrograms.insert(prog1); + if constexpr (Mode == RoutingMode::Hot) { + assert(infos.containsProgram(prog0) && infos.containsProgram(prog1) && + "expected the routing preview to materialize SWAP operands"); const auto i0 = infos.lookupIndex(prog0); const auto i1 = infos.lookupIndex(prog1); @@ -1211,7 +1265,7 @@ struct MappingPass : impl::MappingPassBase { const auto prog0 = infos.lookupProgram(indices[0]); const auto prog1 = infos.lookupProgram(indices[1]); if (const auto [hw0, hw1] = layout.getHardwareIndices(prog0, prog1); - device->areAdjacent(hw0, hw1)) { + target->areAdjacent(hw0, hw1)) { released.emplace_back(op); } continue; @@ -1581,32 +1635,63 @@ struct MappingPass : impl::MappingPassBase { return success(); } - std::shared_ptr device; + std::optional target; }; } // namespace -std::unique_ptr -createMappingPass(const DenseSet>& couplingSet, - MappingPassOptions options) { - - // Verify the assumption that the coupling set is symmetric: - // For every edge (u, v) in the set, (v, u) must also be present. +[[nodiscard]] static CompilerTarget +makeCompilerTarget(const DenseSet>& couplingSet) { + if (couplingSet.empty()) { + llvm::reportFatalUsageError("Expected a non-empty coupling set."); + } - for (const auto& [u, v] : couplingSet) { - if (u == v) { + DenseSet uniqueSites; + std::vector couplings; + couplings.reserve(couplingSet.size() / 2); + for (const auto& [source, target] : couplingSet) { + if (source == target) { llvm::reportFatalUsageError("Found an invalid (u, u) edge."); } - - if (!couplingSet.contains({v, u})) { + if (!couplingSet.contains({target, source})) { llvm::reportFatalUsageError("Expected symmetric coupling set: edge (" + - Twine(u) + ", " + Twine(v) + - ") exists but (" + Twine(v) + ", " + - Twine(u) + ") does not."); + Twine(source) + ", " + Twine(target) + + ") exists but (" + Twine(target) + ", " + + Twine(source) + ") does not."); + } + if (source > static_cast(std::numeric_limits::max()) || + target > static_cast(std::numeric_limits::max())) { + llvm::reportFatalUsageError( + "Coupling-set site ID exceeds the nonnegative i64 domain."); + } + + uniqueSites.insert(source); + uniqueSites.insert(target); + if (source < target) { + couplings.emplace_back(static_cast(source), + static_cast(target)); } } - return std::make_unique(couplingSet, options); + auto siteIds = llvm::to_vector(uniqueSites); + std::ranges::sort(siteIds); + std::vector sites; + sites.reserve(siteIds.size()); + for (const auto site : siteIds) { + sites.emplace_back(static_cast(site)); + } + return CompilerTarget(std::move(sites), std::move(couplings)); +} + +std::unique_ptr createMappingPass(const CompilerTarget& target, + MappingPassOptions options) { + return std::make_unique(target, options); +} + +std::unique_ptr +createMappingPass(const DenseSet>& couplingSet, + MappingPassOptions options) { + return createMappingPass(makeCompilerTarget(couplingSet), options); } } // namespace mlir::qco diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/CMakeLists.txt b/mlir/unittests/Dialect/QCO/Transforms/Mapping/CMakeLists.txt index 35abaebfcf..44b204232d 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/CMakeLists.txt +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/CMakeLists.txt @@ -9,8 +9,9 @@ set(target_name mqt-core-mlir-unittest-mapping) add_executable(${target_name} test_mapping.cpp) -target_link_libraries(${target_name} PRIVATE GTest::gtest_main MLIRParser MLIRQCOProgramBuilder - MLIRQTensorUtils MLIRQCOTransforms) +target_link_libraries( + ${target_name} PRIVATE GTest::gtest_main MLIRParser MQTCompilerTarget MLIRQCOProgramBuilder + MLIRQTensorUtils MLIRQCOTransforms) mqt_mlir_configure_unittest_target(${target_name}) diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index bd784e3791..ded44fc35d 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -8,6 +8,7 @@ * Licensed under the MIT License */ +#include "mlir/Compiler/Target.h" #include "mlir/Dialect/QCO/Builder/QCOProgramBuilder.h" #include "mlir/Dialect/QCO/IR/QCODialect.h" #include "mlir/Dialect/QCO/IR/QCOInterfaces.h" @@ -15,6 +16,7 @@ #include "mlir/Dialect/QCO/Transforms/Mapping/Mapping.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Dialect/QTensor/IR/QTensorDialect.h" +#include "mlir/Dialect/QTensor/IR/QTensorOps.h" #include "mlir/Dialect/Utils/Utils.h" #include @@ -28,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -46,19 +49,16 @@ #include #include #include +#include #include #include +#include using namespace mlir; using namespace mlir::qco; using namespace mlir::utils; namespace { -struct Device { - size_t nqubits{}; - DenseSet> couplingSet; -}; - // NOLINTNEXTLINE(llvm-prefer-static-over-anonymous-namespace) SmallVector getQubitValues(ValueRange values) { return to_vector(llvm::make_filter_range( @@ -68,9 +68,9 @@ SmallVector getQubitValues(ValueRange values) { /// Return true, if the operations within a region fulfill the given coupling /// constraints. -static bool -isExecutable(Region& body, DenseMap& m, - const DenseSet>& couplingSet) { +static bool isExecutable(Region& body, + DenseMap& m, + const CompilerTarget& target) { for (Operation& op : body.getOps()) { if (auto staticOp = dyn_cast(op)) { m.try_emplace(staticOp.getQubit(), staticOp.getIndex()); @@ -81,10 +81,12 @@ isExecutable(Region& body, DenseMap& m, if (!isa(op) && unitaryOp.getNumQubits() > 1) { assert(unitaryOp.getNumQubits() <= 2 && "expected two-qubit decomp."); - const auto hwA = m.at(unitaryOp.getInputQubit(0)); - const auto hwB = m.at(unitaryOp.getInputQubit(1)); - if (!couplingSet.contains(std::make_pair(hwA, hwB))) { - llvm::dbgs() << "The two-qubit gate (" << hwA << ", " << hwB + const auto siteA = m.at(unitaryOp.getInputQubit(0)); + const auto siteB = m.at(unitaryOp.getInputQubit(1)); + const auto vertexA = target.vertexForSite(siteA); + const auto vertexB = target.vertexForSite(siteB); + if (!vertexA || !vertexB || !target.areAdjacent(*vertexA, *vertexB)) { + llvm::dbgs() << "The two-qubit gate (" << siteA << ", " << siteB << ") is not executable: \n"; unitaryOp->dump(); return false; @@ -134,13 +136,13 @@ isExecutable(Region& body, DenseMap& m, const auto qubitArgs = getQubitValues(region.getArguments()); - DenseMap localM; + DenseMap localM; for (const auto [arg, hw] : llvm::zip_equal(qubitArgs, initialHardwareOrder)) { localM.try_emplace(arg, hw); } - if (!isExecutable(region, localM, couplingSet)) { + if (!isExecutable(region, localM, target)) { return false; } @@ -208,20 +210,25 @@ isExecutable(Region& body, DenseMap& m, } /// Return true, if the entry point fulfills the given coupling constraints. -static bool -isExecutable(func::FuncOp entry, - const DenseSet>& couplingSet) { - DenseMap m; - return isExecutable(entry.getFunctionBody(), m, couplingSet); +static bool isExecutable(func::FuncOp entry, const CompilerTarget& target) { + DenseMap m; + return isExecutable(entry.getFunctionBody(), m, target); } -/// Return a 9x9 square-grid coupling set. -static Device getNineQubitSquareGrid() { - return {.nqubits = 9, - .couplingSet = {{0, 3}, {3, 0}, {0, 1}, {1, 0}, {1, 4}, {4, 1}, - {1, 2}, {2, 1}, {2, 5}, {5, 2}, {3, 6}, {6, 3}, - {3, 4}, {4, 3}, {4, 7}, {7, 4}, {4, 5}, {5, 4}, - {5, 8}, {8, 5}, {6, 7}, {7, 6}, {7, 8}, {8, 7}}}; +/// Return a 3x3 square-grid compiler target. +static CompilerTarget getNineQubitSquareGrid() { + return CompilerTarget(9, std::vector{{0, 1}, + {0, 3}, + {1, 2}, + {1, 4}, + {2, 5}, + {3, 4}, + {3, 6}, + {4, 5}, + {4, 7}, + {5, 8}, + {6, 7}, + {7, 8}}); } /// Creates an N-qubit GHZ state, where N = `qubits.size()` using @@ -268,7 +275,7 @@ static void loopGHZ(QCOProgramBuilder& builder, Value& tensor, namespace { class MappingPassTest : public testing::Test, - public testing::WithParamInterface { + public testing::WithParamInterface { protected: void SetUp() override { DialectRegistry registry; @@ -279,11 +286,10 @@ class MappingPassTest : public testing::Test, context->loadAllAvailableDialects(); } - static LogicalResult - runPass(ModuleOp m, const DenseSet>& couplingSet, - const MappingPassOptions& options) { + static LogicalResult runPass(ModuleOp m, const CompilerTarget& target, + const MappingPassOptions& options) { PassManager pm(m->getContext()); - pm.addPass(createMappingPass(couplingSet, options)); + pm.addPass(createMappingPass(target, options)); return pm.run(m); } @@ -293,15 +299,15 @@ class MappingPassTest : public testing::Test, }; // namespace TEST_P(MappingPassTest, FailNoEntryPoint) { - const auto& device = GetParam(); + const auto& target = GetParam(); OwningOpRef m = ModuleOp::create(UnknownLoc::get(context.get())); - auto res = runPass(m.get(), device.couplingSet, MappingPassOptions{}); + auto res = runPass(m.get(), target, MappingPassOptions{}); ASSERT_TRUE(res.failed()); } -TEST_P(MappingPassTest, FailNoQubitAllocations) { - const auto& device = GetParam(); +TEST_P(MappingPassTest, MapScalarAllocation) { + const auto& target = GetParam(); QCOProgramBuilder builder(context.get()); builder.initialize({builder.getI1Type()}); @@ -314,13 +320,209 @@ TEST_P(MappingPassTest, FailNoQubitAllocations) { builder.sink(q0); auto m = builder.finalize(c0); - auto res = runPass(m.get(), device.couplingSet, MappingPassOptions{}); + auto res = runPass(m.get(), target, MappingPassOptions{}); - ASSERT_TRUE(res.failed()); + ASSERT_TRUE(res.succeeded()); + ASSERT_TRUE(succeeded(verify(*m))); + EXPECT_TRUE(isExecutable(getEntryPoint(m.get()), target)); + + size_t numAllocations = 0; + size_t numStatics = 0; + m->walk([&](AllocOp) { ++numAllocations; }); + m->walk([&](StaticOp) { ++numStatics; }); + EXPECT_EQ(numAllocations, 0); + EXPECT_EQ(numStatics, 1); +} + +TEST_P(MappingPassTest, MapMixedScalarAndTensorAllocations) { + const auto& target = GetParam(); + + QCOProgramBuilder builder(context.get()); + builder.initialize(); + + Value scalar = builder.allocQubit(); + Value tensor = builder.qtensorAlloc(2); + Value tensorQubit0; + Value tensorQubit1; + std::tie(tensor, tensorQubit0) = builder.qtensorExtract(tensor, 0); + std::tie(tensor, tensorQubit1) = builder.qtensorExtract(tensor, 1); + + scalar = builder.h(scalar); + std::tie(scalar, tensorQubit0) = builder.cx(scalar, tensorQubit0); + std::tie(tensorQubit0, tensorQubit1) = + builder.rzx(0.5, tensorQubit0, tensorQubit1); + + builder.sink(scalar); + tensor = builder.qtensorInsert(tensorQubit0, tensor, 0); + tensor = builder.qtensorInsert(tensorQubit1, tensor, 1); + builder.qtensorDealloc(tensor); + + auto module = builder.finalize(); + ASSERT_TRUE(runPass(module.get(), target, MappingPassOptions{.ntrials = 1}) + .succeeded()); + ASSERT_TRUE(succeeded(verify(*module))); + EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); + + size_t numScalarAllocations = 0; + size_t numTensorAllocations = 0; + module->walk([&](AllocOp) { ++numScalarAllocations; }); + module->walk([&](qtensor::AllocOp) { ++numTensorAllocations; }); + EXPECT_EQ(numScalarAllocations, 0); + EXPECT_EQ(numTensorAllocations, 0); +} + +TEST_P(MappingPassTest, MapProgramAfterQubitReuse) { + const auto& target = GetParam(); + + QCOProgramBuilder builder(context.get()); + builder.initialize({builder.getI1Type(), builder.getI1Type()}); + + Value q0 = builder.allocQubit(); + q0 = builder.h(q0); + Value bit0; + std::tie(q0, bit0) = builder.measure(q0); + builder.sink(q0); + + Value q1 = builder.allocQubit(); + q1 = builder.x(q1); + Value bit1; + std::tie(q1, bit1) = builder.measure(q1); + builder.sink(q1); + + auto module = builder.finalize({bit0, bit1}); + PassManager pm(context.get()); + pm.addPass(createReuseQubits()); + pm.addPass(createCanonicalizerPass()); + pm.addPass(createMappingPass(target, MappingPassOptions{.ntrials = 1})); + ASSERT_TRUE(pm.run(module.get()).succeeded()); + ASSERT_TRUE(succeeded(verify(*module))); + EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); + + size_t numStatics = 0; + size_t numResets = 0; + module->walk([&](StaticOp) { ++numStatics; }); + module->walk([&](ResetOp) { ++numResets; }); + EXPECT_EQ(numStatics, 1); + EXPECT_EQ(numResets, 1); +} + +TEST_P(MappingPassTest, FailNestedScalarAllocation) { + const auto& target = GetParam(); + constexpr StringLiteral source = R"mlir( + module { + func.func @main() attributes {passthrough = ["entry_point"]} { + %condition = arith.constant true + %q0 = qco.alloc : !qco.qubit + %q1 = qco.if %condition args(%arg0 = %q0) -> (!qco.qubit) { + %nested = qco.alloc : !qco.qubit + qco.sink %nested : !qco.qubit + qco.yield %arg0 : !qco.qubit + } else args(%arg0 = %q0) { + qco.yield %arg0 : !qco.qubit + } + qco.sink %q1 : !qco.qubit + return + } + } + )mlir"; + + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE(failed(runPass(module.get(), target, MappingPassOptions{}))); + EXPECT_TRUE( + StringRef(diagnostics) + .contains( + "target mapping requires dynamic qubit allocations in the entry " + "function body")) + << diagnostics; +} + +TEST_P(MappingPassTest, FailNestedTensorAllocation) { + const auto& target = GetParam(); + constexpr StringLiteral source = R"mlir( + module { + func.func @main() attributes {passthrough = ["entry_point"]} { + %condition = arith.constant true + %c1 = arith.constant 1 : index + %q0 = qco.alloc : !qco.qubit + %q1 = qco.if %condition args(%arg0 = %q0) -> (!qco.qubit) { + %nested = qtensor.alloc(%c1) : tensor<1x!qco.qubit> + qtensor.dealloc %nested : tensor<1x!qco.qubit> + qco.yield %arg0 : !qco.qubit + } else args(%arg0 = %q0) { + qco.yield %arg0 : !qco.qubit + } + qco.sink %q1 : !qco.qubit + return + } + } + )mlir"; + + auto module = parseSourceString(source, context.get()); + ASSERT_TRUE(module); + ASSERT_TRUE(succeeded(verify(*module))); + + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE(failed(runPass(module.get(), target, MappingPassOptions{}))); + EXPECT_TRUE( + StringRef(diagnostics) + .contains( + "target mapping requires dynamic qubit allocations in the entry " + "function body")) + << diagnostics; +} + +TEST_P(MappingPassTest, FailNestedHigherArityUnitary) { + const auto& target = GetParam(); + + QCOProgramBuilder builder(context.get()); + builder.initialize(); + SmallVector qubits{builder.allocQubit(), builder.allocQubit(), + builder.allocQubit()}; + qubits = llvm::to_vector(builder.qcoIf( + true, qubits, + [&](ValueRange args) { + auto [controls, targetQubit] = builder.mcx({args[0], args[1]}, args[2]); + return SmallVector{controls[0], controls[1], targetQubit}; + }, + [](ValueRange args) { return llvm::to_vector(args); })); + for (const auto qubit : qubits) { + builder.sink(qubit); + } + + auto module = builder.finalize(); + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE(failed(runPass(module.get(), target, MappingPassOptions{}))); + EXPECT_TRUE( + StringRef(diagnostics) + .contains("decompose it to one- and two-qubit operations first")) + << diagnostics; + + size_t numAllocations = 0; + size_t numStatics = 0; + module->walk([&](AllocOp) { ++numAllocations; }); + module->walk([&](StaticOp) { ++numStatics; }); + EXPECT_EQ(numAllocations, 3); + EXPECT_EQ(numStatics, 0); } TEST_P(MappingPassTest, FailNoExtractAfterInsert) { - const auto& device = GetParam(); + const auto& target = GetParam(); QCOProgramBuilder builder(context.get()); builder.initialize({builder.getI1Type()}); @@ -341,14 +543,14 @@ TEST_P(MappingPassTest, FailNoExtractAfterInsert) { builder.qtensorDealloc(tensor0); auto m = builder.finalize(c0); - auto res = runPass(m.get(), device.couplingSet, MappingPassOptions{}); + auto res = runPass(m.get(), target, MappingPassOptions{}); ASSERT_TRUE(res.failed()); } TEST_P(MappingPassTest, FailTooManyQubitsForArch) { - const auto& device = GetParam(); - const auto size = static_cast(device.nqubits) + 1; + const auto& target = GetParam(); + const auto size = static_cast(target.numQubits()) + 1; SmallVector bits(size); SmallVector qubits(size); @@ -371,13 +573,112 @@ TEST_P(MappingPassTest, FailTooManyQubitsForArch) { builder.qtensorDealloc(tensor); auto m = builder.finalize(bits); - auto res = runPass(m.get(), device.couplingSet, MappingPassOptions{}); + auto res = runPass(m.get(), target, MappingPassOptions{}); ASSERT_TRUE(res.failed()); } +TEST_P(MappingPassTest, MapTopologyOnlyWithEmptyOperationSet) { + const CompilerTarget target( + 3, std::vector{{0, 1}, {1, 2}}, + std::vector{}); + + QCOProgramBuilder builder(context.get()); + builder.initialize(); + SmallVector qubits{builder.allocQubit(), builder.allocQubit(), + builder.allocQubit()}; + + qubits[0] = builder.x(qubits[0]); + std::tie(qubits[0], qubits[1]) = builder.rxx(0.25, qubits[0], qubits[1]); + std::tie(qubits[1], qubits[2]) = builder.rzx(0.5, qubits[1], qubits[2]); + std::tie(qubits[0], qubits[2]) = builder.cx(qubits[0], qubits[2]); + for (const auto qubit : qubits) { + builder.sink(qubit); + } + + auto module = builder.finalize(); + ASSERT_TRUE(runPass(module.get(), target, MappingPassOptions{.ntrials = 1}) + .succeeded()); + ASSERT_TRUE(succeeded(verify(*module))); + EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); + + size_t numSwaps = 0; + module->walk([&](SWAPOp) { ++numSwaps; }); + EXPECT_GT(numSwaps, 0); +} + +TEST_P(MappingPassTest, PreserveNoncontiguousProviderSiteIds) { + std::vector sites; + sites.emplace_back(7); + sites.emplace_back(19); + sites.emplace_back(42); + const CompilerTarget target( + std::move(sites), + std::vector{{7, 19}, {19, 42}}, + std::vector{}); + + QCOProgramBuilder builder(context.get()); + builder.initialize(); + SmallVector qubits{builder.allocQubit(), builder.allocQubit(), + builder.allocQubit()}; + std::tie(qubits[0], qubits[1]) = builder.cx(qubits[0], qubits[1]); + std::tie(qubits[1], qubits[2]) = builder.cz(qubits[1], qubits[2]); + std::tie(qubits[0], qubits[2]) = builder.cx(qubits[0], qubits[2]); + for (const auto qubit : qubits) { + builder.sink(qubit); + } + + auto module = builder.finalize(); + ASSERT_TRUE(runPass(module.get(), target, MappingPassOptions{.ntrials = 1}) + .succeeded()); + ASSERT_TRUE(succeeded(verify(*module))); + EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); + + const DenseSet expectedSites{7, 19, 42}; + size_t numStatics = 0; + module->walk([&](StaticOp op) { + ++numStatics; + EXPECT_TRUE(expectedSites.contains(op.getIndex())); + }); + EXPECT_EQ(numStatics, 3); +} + +TEST_P(MappingPassTest, KeepWorkspaceSparseOnLargeTarget) { + constexpr size_t numTargetQubits = 64; + std::vector couplings; + couplings.reserve(numTargetQubits - 1); + for (size_t site = 1; site < numTargetQubits; ++site) { + couplings.emplace_back(0, static_cast(site)); + } + const CompilerTarget target(numTargetQubits, std::move(couplings)); + + QCOProgramBuilder builder(context.get()); + builder.initialize(); + Value q0 = builder.allocQubit(); + Value q1 = builder.allocQubit(); + std::tie(q0, q1) = builder.cx(q0, q1); + builder.sink(q0); + builder.sink(q1); + + auto module = builder.finalize(); + ASSERT_TRUE(runPass(module.get(), target, + MappingPassOptions{.niterations = 1, .ntrials = 1}) + .succeeded()); + ASSERT_TRUE(succeeded(verify(*module))); + EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); + + size_t numStatics = 0; + size_t numSinks = 0; + module->walk([&](StaticOp) { ++numStatics; }); + module->walk([&](SinkOp) { ++numSinks; }); + EXPECT_GE(numStatics, 2); + EXPECT_LE(numStatics, 3); + EXPECT_LT(numStatics, numTargetQubits); + EXPECT_EQ(numSinks, numStatics); +} + TEST_P(MappingPassTest, MapFlatGHZ) { - const auto& device = GetParam(); + const auto& target = GetParam(); const int64_t size = 3; SmallVector qubits(size); @@ -406,16 +707,16 @@ TEST_P(MappingPassTest, MapFlatGHZ) { builder.qtensorDealloc(tensor); auto m = builder.finalize(bits); - auto res = runPass(m.get(), device.couplingSet, MappingPassOptions{}); + auto res = runPass(m.get(), target, MappingPassOptions{}); auto entry = getEntryPoint(m.get()); ASSERT_TRUE(res.succeeded()); - EXPECT_TRUE(isExecutable(entry, device.couplingSet)); + EXPECT_TRUE(isExecutable(entry, target)); } TEST_P(MappingPassTest, MapLoopBasedGHZByUnrolling) { - const auto& device = GetParam(); - const auto size = static_cast(device.nqubits); + const auto& target = GetParam(); + const auto size = static_cast(target.numQubits()); SmallVector qubits(size); SmallVector bits(size); @@ -424,7 +725,7 @@ TEST_P(MappingPassTest, MapLoopBasedGHZByUnrolling) { pm.addNestedPass(createQuantumLoopUnroll()); pm.addPass(createCSEPass()); pm.addPass(createCanonicalizerPass()); - pm.addPass(createMappingPass(device.couplingSet, MappingPassOptions{})); + pm.addPass(createMappingPass(target, MappingPassOptions{})); QCOProgramBuilder builder(context.get()); builder.initialize(SmallVector(size, builder.getI1Type())); @@ -454,18 +755,18 @@ TEST_P(MappingPassTest, MapLoopBasedGHZByUnrolling) { auto entry = getEntryPoint(m.get()); ASSERT_TRUE(res.succeeded()); - EXPECT_TRUE(isExecutable(entry, device.couplingSet)); + EXPECT_TRUE(isExecutable(entry, target)); } TEST_P(MappingPassTest, MapGroverLike) { - const auto& device = GetParam(); + const auto& target = GetParam(); const int64_t size = 5; SmallVector qubits(size); SmallVector bits(size); PassManager pm(context.get()); - pm.addPass(createMappingPass(device.couplingSet, MappingPassOptions{})); + pm.addPass(createMappingPass(target, MappingPassOptions{})); QCOProgramBuilder builder(context.get()); builder.initialize(SmallVector(5, builder.getI1Type())); @@ -520,18 +821,18 @@ TEST_P(MappingPassTest, MapGroverLike) { auto entry = getEntryPoint(m.get()); ASSERT_TRUE(res.succeeded()); - EXPECT_TRUE(isExecutable(entry, device.couplingSet)); + EXPECT_TRUE(isExecutable(entry, target)); } TEST_P(MappingPassTest, MapParallelLoops) { - const auto& device = GetParam(); + const auto& target = GetParam(); constexpr int64_t size = 6; SmallVector qubits(size); SmallVector bits(size); PassManager pm(context.get()); - pm.addPass(createMappingPass(device.couplingSet, MappingPassOptions{})); + pm.addPass(createMappingPass(target, MappingPassOptions{})); QCOProgramBuilder builder(context.get()); builder.initialize(SmallVector(size, builder.getI1Type())); @@ -599,11 +900,11 @@ TEST_P(MappingPassTest, MapParallelLoops) { auto entry = getEntryPoint(m.get()); ASSERT_TRUE(res.succeeded()); - EXPECT_TRUE(isExecutable(entry, device.couplingSet)); + EXPECT_TRUE(isExecutable(entry, target)); } TEST_P(MappingPassTest, MapForWithClassicalIterArg) { - const auto& device = GetParam(); + const auto& target = GetParam(); constexpr StringLiteral source = R"mlir( module { func.func @main() -> i64 attributes {passthrough = ["entry_point"]} { @@ -641,15 +942,14 @@ TEST_P(MappingPassTest, MapForWithClassicalIterArg) { ASSERT_TRUE(module); ASSERT_TRUE(verify(*module).succeeded()); - ASSERT_TRUE(runPass(module.get(), device.couplingSet, - MappingPassOptions{.ntrials = 1}) + ASSERT_TRUE(runPass(module.get(), target, MappingPassOptions{.ntrials = 1}) .succeeded()); EXPECT_TRUE(verify(*module).succeeded()); - EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), device.couplingSet)); + EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); } TEST_P(MappingPassTest, MapTypeChangingWhileWithClassicalState) { - const auto& device = GetParam(); + const auto& target = GetParam(); constexpr StringLiteral source = R"mlir( module { func.func @main() -> i64 attributes {passthrough = ["entry_point"]} { @@ -693,15 +993,14 @@ TEST_P(MappingPassTest, MapTypeChangingWhileWithClassicalState) { ASSERT_TRUE(module); ASSERT_TRUE(verify(*module).succeeded()); - ASSERT_TRUE(runPass(module.get(), device.couplingSet, - MappingPassOptions{.ntrials = 1}) + ASSERT_TRUE(runPass(module.get(), target, MappingPassOptions{.ntrials = 1}) .succeeded()); EXPECT_TRUE(verify(*module).succeeded()); - EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), device.couplingSet)); + EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); } TEST_P(MappingPassTest, MapIfWithClassicalResult) { - const auto& device = GetParam(); + const auto& target = GetParam(); constexpr StringLiteral source = R"mlir( module { func.func @main() -> i64 attributes {passthrough = ["entry_point"]} { @@ -742,11 +1041,10 @@ TEST_P(MappingPassTest, MapIfWithClassicalResult) { ASSERT_TRUE(module); ASSERT_TRUE(succeeded(verify(*module))); - ASSERT_TRUE(runPass(module.get(), device.couplingSet, - MappingPassOptions{.ntrials = 1}) + ASSERT_TRUE(runPass(module.get(), target, MappingPassOptions{.ntrials = 1}) .succeeded()); ASSERT_TRUE(succeeded(verify(*module))); - EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), device.couplingSet)); + EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); IfOp ifOp; module->walk([&](IfOp candidate) { ifOp = candidate; }); @@ -760,7 +1058,7 @@ TEST_P(MappingPassTest, MapIfWithClassicalResult) { } TEST_P(MappingPassTest, MapIndexSwitchWithClassicalResult) { - const auto& device = GetParam(); + const auto& target = GetParam(); constexpr StringLiteral source = R"mlir( module { func.func @main(%selector: index) -> i64 @@ -808,11 +1106,10 @@ TEST_P(MappingPassTest, MapIndexSwitchWithClassicalResult) { ASSERT_TRUE(module); ASSERT_TRUE(succeeded(verify(*module))); - ASSERT_TRUE(runPass(module.get(), device.couplingSet, - MappingPassOptions{.ntrials = 1}) + ASSERT_TRUE(runPass(module.get(), target, MappingPassOptions{.ntrials = 1}) .succeeded()); ASSERT_TRUE(succeeded(verify(*module))); - EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), device.couplingSet)); + EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); IndexSwitchOp switchOp; module->walk([&](IndexSwitchOp candidate) { switchOp = candidate; }); @@ -827,7 +1124,7 @@ TEST_P(MappingPassTest, MapIndexSwitchWithClassicalResult) { } TEST_P(MappingPassTest, RouteIndexSwitchRegions) { - const auto& device = GetParam(); + const auto& target = GetParam(); constexpr StringLiteral source = R"mlir( module { func.func @main(%selector: index) @@ -879,8 +1176,7 @@ TEST_P(MappingPassTest, RouteIndexSwitchRegions) { ASSERT_TRUE(module); ASSERT_TRUE(succeeded(verify(*module))); - ASSERT_TRUE(runPass(module.get(), device.couplingSet, - MappingPassOptions{.ntrials = 1}) + ASSERT_TRUE(runPass(module.get(), target, MappingPassOptions{.ntrials = 1}) .succeeded()); ASSERT_TRUE(succeeded(verify(*module))); @@ -890,7 +1186,7 @@ TEST_P(MappingPassTest, RouteIndexSwitchRegions) { } TEST_P(MappingPassTest, RouteNestedOperationOnceWhileIndependentWiresAdvance) { - const auto& device = GetParam(); + const auto& target = GetParam(); constexpr StringLiteral source = R"mlir( module { func.func @main(%selector: index) @@ -941,8 +1237,7 @@ TEST_P(MappingPassTest, RouteNestedOperationOnceWhileIndependentWiresAdvance) { ASSERT_TRUE(module); ASSERT_TRUE(succeeded(verify(*module))); - ASSERT_TRUE(runPass(module.get(), device.couplingSet, - MappingPassOptions{.ntrials = 1}) + ASSERT_TRUE(runPass(module.get(), target, MappingPassOptions{.ntrials = 1}) .succeeded()); EXPECT_TRUE(succeeded(verify(*module))); @@ -952,7 +1247,7 @@ TEST_P(MappingPassTest, RouteNestedOperationOnceWhileIndependentWiresAdvance) { } TEST_P(MappingPassTest, MapSABRECircuit) { - const auto& device = GetParam(); + const auto& target = GetParam(); constexpr int64_t size = 6; SmallVector qubits(size); @@ -1016,15 +1311,15 @@ TEST_P(MappingPassTest, MapSABRECircuit) { builder.qtensorDealloc(tensorDown); auto m = builder.finalize(bits); - auto res = runPass(m.get(), device.couplingSet, MappingPassOptions{}); + auto res = runPass(m.get(), target, MappingPassOptions{}); auto entry = getEntryPoint(m.get()); ASSERT_TRUE(res.succeeded()); - EXPECT_TRUE(isExecutable(entry, device.couplingSet)); + EXPECT_TRUE(isExecutable(entry, target)); } TEST_P(MappingPassTest, MapBranchingGHZ) { - const auto& device = GetParam(); + const auto& target = GetParam(); constexpr int64_t size = 7; SmallVector qubits(size); @@ -1069,16 +1364,15 @@ TEST_P(MappingPassTest, MapBranchingGHZ) { builder.qtensorDealloc(tensor); auto m = builder.finalize(bits); - auto res = - runPass(m.get(), device.couplingSet, MappingPassOptions{.ntrials = 1}); + auto res = runPass(m.get(), target, MappingPassOptions{.ntrials = 1}); auto entry = getEntryPoint(m.get()); ASSERT_TRUE(res.succeeded()); - EXPECT_TRUE(isExecutable(entry, device.couplingSet)); + EXPECT_TRUE(isExecutable(entry, target)); } TEST_P(MappingPassTest, MapDoUntil) { - const auto& device = GetParam(); + const auto& target = GetParam(); const auto size = 4; QCOProgramBuilder builder(context.get()); @@ -1132,16 +1426,15 @@ TEST_P(MappingPassTest, MapDoUntil) { builder.qtensorDealloc(tensor); auto m = builder.finalize(); - auto res = - runPass(m.get(), device.couplingSet, MappingPassOptions{.ntrials = 1}); + auto res = runPass(m.get(), target, MappingPassOptions{.ntrials = 1}); auto entry = getEntryPoint(m.get()); ASSERT_TRUE(res.succeeded()); - EXPECT_TRUE(isExecutable(entry, device.couplingSet)); + EXPECT_TRUE(isExecutable(entry, target)); } TEST_P(MappingPassTest, MapNestedForSwitch) { - const auto& device = GetParam(); + const auto& target = GetParam(); const auto size = 9; std::mt19937 gen(42); @@ -1227,16 +1520,16 @@ TEST_P(MappingPassTest, MapNestedForSwitch) { builder.qtensorDealloc(tensor); auto m = builder.finalize(); - auto res = runPass(m.get(), device.couplingSet, MappingPassOptions{}); + auto res = runPass(m.get(), target, MappingPassOptions{}); auto entry = getEntryPoint(m.get()); ASSERT_TRUE(res.succeeded()); - EXPECT_TRUE(isExecutable(entry, device.couplingSet)); + EXPECT_TRUE(isExecutable(entry, target)); } TEST_P(MappingPassTest, MapIndexSwitchUsesVotedLayout) { - const DenseSet> line = { - {0, 1}, {1, 0}, {1, 2}, {2, 1}}; + const CompilerTarget target( + 3, std::vector{{0, 1}, {1, 2}}); QCOProgramBuilder builder(context.get()); builder.initialize(); @@ -1267,7 +1560,7 @@ TEST_P(MappingPassTest, MapIndexSwitchUsesVotedLayout) { auto m = builder.finalize(); ASSERT_TRUE( - runPass(m.get(), line, MappingPassOptions{.ntrials = 1}).succeeded()); + runPass(m.get(), target, MappingPassOptions{.ntrials = 1}).succeeded()); size_t numSwaps = 0; m->walk([&](SWAPOp) { ++numSwaps; }); From 1b8c4a2abf63d72385cfb5bcc032a0c4c1c1dbbc Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 3 Aug 2026 17:43:41 +0200 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=9D=20Link=20MAP-01=20in=20the=20c?= =?UTF-8?q?hangelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5.6 via Codex --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e8effdc91..b0fdc3c2a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,7 +70,7 @@ releases may include breaking changes. circuits to compiler-target topologies while preserving provider site IDs and materializing routing workspace on demand ([#1537], [#1547], [#1568], [#1581], [#1583], [#1588], [#1600], [#1664], [#1709], [#1716], [#1748], [#1805], - [#1870], [#1904], [#1911], [#1951]) ([**@MatthiasReumann**], + [#1870], [#1904], [#1911], [#1951], [#1997]) ([**@MatthiasReumann**], [**@burgholzer**]) - ✨ Add a pass for qubit reuse in quantum programs, as well as related auxiliary passes and patterns ([#1705], [#1755], [#1756], [#1923], [#1924]) @@ -709,6 +709,7 @@ for previous changelogs._ +[#1997]: https://github.com/munich-quantum-toolkit/core/pull/1997 [#1992]: https://github.com/munich-quantum-toolkit/core/pull/1992 [#1986]: https://github.com/munich-quantum-toolkit/core/pull/1986 [#1984]: https://github.com/munich-quantum-toolkit/core/pull/1984 From dfa37b1ea107de0834ac4ed3a5ba248ad8133115 Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Mon, 3 Aug 2026 20:02:53 +0200 Subject: [PATCH 3/3] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Drop=20legacy=20mappin?= =?UTF-8?q?g=20factory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assisted-by: GPT-5.6 via Codex --- .agent/plans/1687-map-01-target-mapping.md | 119 ++++++++++++------ .../Dialect/QCO/Transforms/Mapping/Mapping.h | 13 -- mlir/lib/Compiler/CMakeLists.txt | 1 + mlir/lib/Compiler/Programs.cpp | 38 +++++- .../QCO/Transforms/Mapping/Mapping.cpp | 51 -------- .../Compiler/test_compiler_pipeline.cpp | 4 +- 6 files changed, 118 insertions(+), 108 deletions(-) diff --git a/.agent/plans/1687-map-01-target-mapping.md b/.agent/plans/1687-map-01-target-mapping.md index 1a10f03ccd..fa73df0dcf 100644 --- a/.agent/plans/1687-map-01-target-mapping.md +++ b/.agent/plans/1687-map-01-target-mapping.md @@ -24,8 +24,9 @@ unused target sites are not all materialized as IR operations. This task is based on the merged compiler-target foundation at `f775395a25fddba0a3b54996416d1311bc6ebe71`. It changes the mapper and its tests -only. The high-level `QCOProgram::placeAndRoute` coupling-set API remains in -place for a later pipeline integration task. +plus the compiler pipeline's sole mapping-pass call site. The high-level +`QCOProgram::placeAndRoute` coupling-set API remains in place for a later +pipeline integration task. ## Progress @@ -68,6 +69,18 @@ place for a later pipeline integration task. test interfaces against the merged base; passed 27 mapping tests, all 218 compiler tests, changed-source clang-tidy 22.1.8, targeted hooks, full repository lint, and `git diff --check`. +- [x] (2026-08-03 17:51Z) Confirmed the sole unresolved review thread requested + deletion of the coupling-set mapping factory, removed its declaration, + conversion helper, and implementation, and migrated `Programs.cpp` to + construct and pass a `CompilerTarget` directly. +- [x] (2026-08-03 17:57Z) Rebuilt the target, transforms, compiler pipeline, and + affected tests; passed 27 mapping tests, all 218 compiler tests, the + focused high-level mapping test, changed-file clang-tidy 22.1.8, targeted + hooks, full repository lint, and `git diff --check`. +- [x] (2026-08-03 18:01Z) An independent read-only review approved the exact + worktree diff with no required changes or new findings. +- [x] (2026-08-03 18:02Z) Created the focused signed review follow-up commit + with the required AI-assistance trailer. ## Surprises & Discoveries @@ -102,6 +115,12 @@ place for a later pipeline integration task. version when invoked without `MLIR_DIR`. Re-running the same release configuration with the installed LLVM 22.1.3 MLIR package path explicit succeeded without a source or build-system change. +- Observation: the coupling-set factory was used only by + `QCOProgram::placeAndRoute`; all mapping tests and every other pass caller + already used `CompilerTarget`. Moving the unavoidable coupling-to-target + conversion to that high-level API boundary removes 50 lines from the mapping + implementation and 13 lines from its public header without changing mapping + behavior. ## Decision Log @@ -136,14 +155,13 @@ place for a later pipeline integration task. implementation unchanged except where the target topology abstraction or sparse-wire bookkeeping requires mechanical adaptation. Rationale: the #1951 regressions are the current mapping contract. Date/Author: 2026-08-03, Codex. -- Decision: add the target-taking factory while retaining the coupling-set - factory as a compatibility adapter that constructs a `CompilerTarget`. - Rationale: MAP-01 must expose a benchmarkable target factory, while the - existing high-level `QCOProgram::placeAndRoute` API remains owned by the later - pipeline task. This is not a new supported compatibility path: PIPE must - delete the coupling-set declaration, adapter, and associated includes as soon - as `Programs.cpp` is migrated. No MAP-01 documentation or test promotes the - adapter. Date/Author: 2026-08-03, Codex. +- Decision: expose only the target-taking mapping factory. Convert the current + high-level `QCOProgram::placeAndRoute` coupling input directly into a + `CompilerTarget` inside `Programs.cpp`, where that legacy surface is already + isolated, and link `MQTCompilerPipeline` to `MQTCompilerTarget`. Rationale: + the mapping library should have one target-native contract and no forwarding + shim; PIPE can later remove the remaining high-level coupling API without + touching Mapping again. Date/Author: 2026-08-03, Codex. ## Outcomes & Retrospective @@ -157,8 +175,8 @@ allocations and higher-arity operations fail before mutation. The existing mapping suite, including #1951 vote-and-restore behavior, passes with the focused new coverage. The compiler suite, changed-source clang-tidy, and repository lint also pass. High-level pipeline ownership remains unchanged, -and the temporary coupling-set forwarder is explicitly left for PIPE to delete. -The conflict-free, patch-equivalent restack and fresh validation demonstrate the +while the mapping library now exposes only its target-native factory. The +conflict-free, patch-equivalent restack and fresh validation demonstrate the same result against the merged compiler-target foundation. ## Context and Orientation @@ -175,11 +193,10 @@ The QCO mapping pass lives in in `mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h`. The target-taking factory stores a cheap `CompilerTarget` value, runs SABRE-style layout refinement and A* routing against its topology, and rewrites dynamic -qubits to `qco.static`. A temporary legacy factory converts its symmetric -`llvm::DenseSet` input into a target only to keep `Programs.cpp` building until -PIPE removes that seam. A `Layout` remains a complete virtual permutation, but -the IR materializes only active program qubits and vacant indices touched by -routing. +qubits to `qco.static`. `QCOProgram::placeAndRoute` constructs a target at its +existing coupling-input boundary and calls this same factory. A `Layout` remains +a complete virtual permutation, but the IR materializes only active program +qubits and vacant indices touched by routing. The mapper walks linear qubit SSA chains using `WireIterator`. Scalar `qco.alloc` produces one chain directly. A `qtensor.alloc` produces a tensor; @@ -195,19 +212,20 @@ behavior tests. Its executable is The current suite covers straight-line programs, nested structured control flow, layout convergence, and the #1951 index-switch vote-and-restore regression. -The task may modify the mapper header and implementation, their CMake link -dependencies, the mapping unit tests, this ExecPlan, and the existing mapping -entry in `CHANGELOG.md`. It must not remove or redesign -`QCOProgram::placeAndRoute`; pipeline integration is owned by a later task. No -other worktree may be modified, and no GitHub action is authorized. +The task may modify the mapper header and implementation, the compiler +pipeline's direct mapping caller and target link, their CMake link dependencies, +the mapping unit tests, this ExecPlan, and the existing mapping entry in +`CHANGELOG.md`. It must not remove or redesign `QCOProgram::placeAndRoute`; +pipeline integration is owned by a later task. No other worktree may be +modified, and no GitHub action is authorized. ## Plan of Work -First, add a public mapping factory taking `const CompilerTarget&`. Retain the -dense coupling-set overload as a compatibility adapter that validates its legacy -symmetric input and constructs a target before creating the same pass. Link the +First, add one public mapping factory taking `const CompilerTarget&`. Link the QCO transforms library and mapping unit test to `MQTCompilerTarget` without -creating a dependency cycle. Retain the pass options so benchmark code can +creating a dependency cycle. At the existing high-level coupling-input boundary +in `Programs.cpp`, construct a validated `CompilerTarget` and call the +target-taking factory directly. Retain the pass options so benchmark code can construct a pass directly for a given target. In `Mapping.cpp`, remove `AugmentedDevice` and store `CompilerTarget` directly. @@ -374,7 +392,7 @@ The current restack evidence is: dense vertices plus provider site IDs, validated connected topology, cached adjacency, neighbours, distances, and maximum degree Current mapper: - CompilerTarget directly, with a temporary coupling-set forwarding adapter + CompilerTarget directly, with no coupling-set overload or adapter Fresh validation evidence after the restack: @@ -400,6 +418,37 @@ Fresh validation evidence after the restack: whitespace: git diff --check passed +Focused validation evidence after removing the legacy factory: + + live PR head: + 1b8c4a2abf63d72385cfb5bcc032a0c4c1c1dbbc + live base and refreshed origin/main: + f775395a25fddba0a3b54996416d1311bc6ebe71 + focused build: + MQTCompilerTarget, MLIRQCOTransforms, MQTCompilerPipeline, + mqt-core-mlir-unittest-mapping, + mqt-core-mlir-unittests-compiler + passed + focused compiler API test: + CompilerPipelineTest.QCOProgramOptimizationAPIs passed + mapping tests: + 27 tests from 1 suite passed + compiler tests: + 218 tests from 8 suites passed + clang-tidy: + LLVM 22.1.8, explicit Xcode SDK/libc++ paths; + Programs.cpp, Mapping.cpp, Mapping.h, and + test_compiler_pipeline.cpp passed without diagnostics + targeted hooks and repository lint: + passed + source audit: + every createMappingPass call passes CompilerTarget and no coupling-set + factory declaration, definition, adapter, or helper remains + independent review: + approved with no required changes or new findings + whitespace: + git diff --check passed + ## Interfaces and Dependencies At completion, `mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h` @@ -409,25 +458,19 @@ declares: createMappingPass(const CompilerTarget& target, MappingPassOptions options); -It also temporarily retains the existing coupling-set overload solely to keep -the current `QCOProgram::placeAndRoute` implementation building; the adapter -creates a `CompilerTarget`, and no mapping algorithm consumes the set directly. -PIPE must remove this declaration and implementation rather than preserve or -document it as a supported compatibility API. - `MappingPass` owns a `CompilerTarget` value. It uses only `numQubits`, `siteForVertex`, `areAdjacent`, `distanceBetween`, `forEachNeighbour`, and `maxDegree` from that target. Operation-capability, native-gate, calibration, duration, fidelity, and directed-locus APIs are deliberately out of scope. -`MLIRQCOTransforms` depends on `MQTCompilerTarget`. The mapping test target also -links the target library directly when needed. No new third-party dependency is -introduced. +`MLIRQCOTransforms` and `MQTCompilerPipeline` depend on `MQTCompilerTarget`. The +mapping test target also links the target library directly when needed. No new +third-party dependency is introduced. Revision note: the initial plan recorded the exact pre-squash stacked base, current vote-and-restore contract, approved MAP-01 scope, implementation strategy, and required validation before feature edits began. This revision records the conflict-free, patch-equivalent restack onto the merged -compiler-target squash and the temporary compatibility forwarder that PIPE must -delete. It also records the single discovery/planning traversal, completed +compiler-target squash and the review-driven removal of the compatibility +forwarder. It also records the single discovery/planning traversal, completed implementation, and validation evidence. diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h b/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h index 3956c2e707..1daa17bdd9 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h @@ -12,14 +12,9 @@ #include "mlir/Dialect/QCO/Transforms/Passes.h" -#include -#include -#include #include -#include #include -#include namespace mlir { @@ -34,13 +29,5 @@ namespace qco { std::unique_ptr createMappingPass(const CompilerTarget& target, MappingPassOptions options); -/** - * @brief Create a mapping pass instance for a legacy symmetric coupling set. - * @returns a pass object. - */ -std::unique_ptr -createMappingPass(const llvm::DenseSet>& couplingSet, - MappingPassOptions options); - } // namespace qco } // namespace mlir diff --git a/mlir/lib/Compiler/CMakeLists.txt b/mlir/lib/Compiler/CMakeLists.txt index 5b7c8de4c6..d6d12c883e 100644 --- a/mlir/lib/Compiler/CMakeLists.txt +++ b/mlir/lib/Compiler/CMakeLists.txt @@ -50,6 +50,7 @@ add_mlir_library( MLIRTargetLLVMIRExport MLIRBuiltinToLLVMIRTranslation MLIRLLVMToLLVMIRTranslation + MQTCompilerTarget MQT::MLIRSupport) mqt_mlir_target_use_project_options(MQTCompilerPipeline) diff --git a/mlir/lib/Compiler/Programs.cpp b/mlir/lib/Compiler/Programs.cpp index d5a8e3e751..b1fae369cc 100644 --- a/mlir/lib/Compiler/Programs.cpp +++ b/mlir/lib/Compiler/Programs.cpp @@ -11,6 +11,7 @@ #include "mlir/Compiler/Programs.h" #include "ir/QuantumComputation.hpp" +#include "mlir/Compiler/Target.h" #include "mlir/Conversion/JeffToQCO/JeffToQCO.h" #include "mlir/Conversion/QCOToJeff/QCOToJeff.h" #include "mlir/Conversion/QCOToQC/QCOToQC.h" @@ -58,6 +59,7 @@ #include #include +#include #include #include #include @@ -65,9 +67,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -417,8 +421,34 @@ bool QCOProgram::placeAndRoute( const std::size_t nlookahead, const float alpha, const float lambda, const std::size_t niterations, const std::size_t ntrials, const std::size_t seed) { - DenseSet> couplingSet; - couplingSet.insert(coupling.begin(), coupling.end()); + std::vector siteIds; + siteIds.reserve(coupling.size() * 2); + std::vector couplings; + couplings.reserve(coupling.size()); + for (const auto [source, target] : coupling) { + if (source > static_cast( + std::numeric_limits::max()) || + target > static_cast( + std::numeric_limits::max())) { + throw std::invalid_argument( + "Coupling site ID exceeds the nonnegative i64 domain"); + } + const auto sourceId = static_cast(source); + const auto targetId = static_cast(target); + siteIds.emplace_back(sourceId); + siteIds.emplace_back(targetId); + couplings.emplace_back(sourceId, targetId); + } + std::ranges::sort(siteIds); + const auto [duplicates, end] = std::ranges::unique(siteIds); + siteIds.erase(duplicates, end); + std::vector sites; + sites.reserve(siteIds.size()); + for (const auto site : siteIds) { + sites.emplace_back(site); + } + const CompilerTarget target(std::move(sites), std::move(couplings)); + qco::MappingPassOptions options; options.nlookahead = nlookahead; options.alpha = alpha; @@ -428,8 +458,8 @@ bool QCOProgram::placeAndRoute( options.seed = seed; return succeeded(runPasses( mod(), - [&couplingSet, &options](OpPassManager& pm) { - pm.addPass(qco::createMappingPass(couplingSet, options)); + [&target, &options](OpPassManager& pm) { + pm.addPass(qco::createMappingPass(target, options)); }, "failed to place and route the QCO program")); } diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index fd16e806e0..a47f7fd46c 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -52,9 +52,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -1640,58 +1638,9 @@ struct MappingPass : impl::MappingPassBase { } // namespace -[[nodiscard]] static CompilerTarget -makeCompilerTarget(const DenseSet>& couplingSet) { - if (couplingSet.empty()) { - llvm::reportFatalUsageError("Expected a non-empty coupling set."); - } - - DenseSet uniqueSites; - std::vector couplings; - couplings.reserve(couplingSet.size() / 2); - for (const auto& [source, target] : couplingSet) { - if (source == target) { - llvm::reportFatalUsageError("Found an invalid (u, u) edge."); - } - if (!couplingSet.contains({target, source})) { - llvm::reportFatalUsageError("Expected symmetric coupling set: edge (" + - Twine(source) + ", " + Twine(target) + - ") exists but (" + Twine(target) + ", " + - Twine(source) + ") does not."); - } - if (source > static_cast(std::numeric_limits::max()) || - target > static_cast(std::numeric_limits::max())) { - llvm::reportFatalUsageError( - "Coupling-set site ID exceeds the nonnegative i64 domain."); - } - - uniqueSites.insert(source); - uniqueSites.insert(target); - if (source < target) { - couplings.emplace_back(static_cast(source), - static_cast(target)); - } - } - - auto siteIds = llvm::to_vector(uniqueSites); - std::ranges::sort(siteIds); - std::vector sites; - sites.reserve(siteIds.size()); - for (const auto site : siteIds) { - sites.emplace_back(static_cast(site)); - } - return CompilerTarget(std::move(sites), std::move(couplings)); -} - std::unique_ptr createMappingPass(const CompilerTarget& target, MappingPassOptions options) { return std::make_unique(target, options); } -std::unique_ptr -createMappingPass(const DenseSet>& couplingSet, - MappingPassOptions options) { - return createMappingPass(makeCompilerTarget(couplingSet), options); -} - } // namespace mlir::qco diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index 48ddc1c017..8f5cdec5f7 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -921,8 +921,8 @@ cx q[0], q[2]; const auto beforeTwoQubitFusion = qco.str(); EXPECT_TRUE(qco.fuseTwoQubitUnitaryRuns("u,cx")); EXPECT_NE(qco.str(), beforeTwoQubitFusion); - const std::vector> coupling = { - {0, 1}, {1, 0}, {1, 2}, {2, 1}}; + const std::vector> coupling = {{0, 1}, + {1, 2}}; EXPECT_TRUE(qco.placeAndRoute(coupling)); EXPECT_TRUE(qco.runPassPipeline("mqt-qco-default", true, true));