From 5f52947580123f622513bc7e577a4e1d7ae9243e Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 1 Sep 2026 11:50:41 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Separate=20target=20pl?= =?UTF-8?q?acement=20from=20routing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use compact deterministic placement for all-to-all target compilation and reserve the existing router for explicit coupling graphs. Share the validated dynamic-to-static rewrite between both passes. Assisted-by: GPT-5.6 Sol via Codex --- .agent/plans/split-placement-routing.md | 272 +++++++++ docs/mlir/target_compilation.md | 5 +- .../Dialect/QCO/Transforms/Mapping/Mapping.h | 3 + .../mlir/Dialect/QCO/Transforms/Passes.td | 29 +- mlir/lib/Compiler/TargetCompilation.cpp | 7 +- .../QCO/Transforms/Mapping/Mapping.cpp | 560 ++++++++++-------- .../Compiler/test_compiler_pipeline.cpp | 3 +- .../QCO/Transforms/Mapping/test_mapping.cpp | 139 ++++- 8 files changed, 730 insertions(+), 288 deletions(-) create mode 100644 .agent/plans/split-placement-routing.md diff --git a/.agent/plans/split-placement-routing.md b/.agent/plans/split-placement-routing.md new file mode 100644 index 0000000000..01ce12d29e --- /dev/null +++ b/.agent/plans/split-placement-routing.md @@ -0,0 +1,272 @@ +# Split deterministic placement from topology routing + +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 + +MQT Core currently runs its complete place-and-route pass even when a compiler +target has all-to-all connectivity and no routing is necessary. After this +change, target compilation uses a small deterministic placement pass for such +targets. The placement pass converts dynamic scalar and tensor-backed qubits to +the first available static target sites without inserting routing qubits or +changing structured control flow. The existing mapping pass becomes responsible +only for targets with an explicit coupling graph and reuses the same allocation +rewrite. + +A user can observe the split by compiling a two-qubit program for an all-to-all +target with more sites than the program uses. The result contains one +`qco.static` operation per program qubit and no routing `qco.swap` operations. +An explicit-topology target still runs the existing router and preserves its +current behavior. + +## Progress + +- [x] (2026-09-01 11:23Z) Inspected the current target, mapping, placement, + tensor, pipeline, and test contracts on `main`. +- [x] (2026-09-01 11:31Z) Extracted allocation discovery, capacity checking, and + allocation rewriting into internal utilities shared by placement and + mapping. +- [x] (2026-09-01 11:31Z) Added the deterministic placement pass and dispatched + non-explicit targets to it from target compilation. +- [x] (2026-09-01 11:31Z) Made the mapping pass reject non-explicit targets + before changing the IR. +- [x] (2026-09-01 11:34Z) Added direct scalar, tensor/output, compact, + deterministic, and failure-atomicity tests plus target-pipeline coverage. +- [x] (2026-09-01 11:45Z) Updated pass and user documentation and validated the + focused binaries, MLIR docs, C++ lint, and full repository lint. + +## Surprises & Discoveries + +- Observation: On current `main`, an absent coupling topology means all-to-all + connectivity. The `Unknown`, `AllToAll`, and `Explicit` distinction is added + by the separate compiler-target work. Evidence: `CompilerTarget` documents an + absent topology as all-to-all, and `CompilerTarget::areAdjacent` returns true + for any two distinct sites when `hasExplicitTopology()` is false. +- Observation: The existing allocation rewrite also creates vacant static qubits + because the router represents every hardware site as a program token. A + compact deterministic layout can reuse the rewrite unchanged by containing + only the program qubits. Evidence: `place` iterates + `layout.nHardwareQubits()`, while `Layout::fromMapping` sizes the layout from + the supplied mapping. +- Observation: The repository's changed-file `cpp-lint` session compares commits + and therefore did not select this intentionally uncommitted diff. Evidence: it + reported `No source files need checking` after its successful lint build. The + modified translation units were instead checked directly with `clang-tidy` and + line filters from the same `build/cpp-lint` compilation database. + +## Decision Log + +- Decision: Add a dedicated `place-qubits` pass and keep `place-and-route` as + the routing pass. Rationale: Placement and routing are established compiler + stages with different information requirements. A separate pass removes the + topology-dependent router from all-to-all compilation. Date/Author: 2026-09-01 + / OpenAI Codex. +- Decision: Share internal allocation discovery and rewriting rather than + invoking one pass from another. Rationale: The router must retain the wire and + layout state returned by placement, which a nested pass invocation cannot + expose safely. Date/Author: 2026-09-01 / OpenAI Codex. +- Decision: Assign program qubit `i` to target vertex `i` in the standalone + placement pass. Rationale: The established discovery order is deterministic, + compact, and does not invent a topology-dependent optimization. Date/Author: + 2026-09-01 / OpenAI Codex. +- Decision: Keep the prerequisite change compatible with the current + all-to-all-or-explicit target model. Rationale: Unknown connectivity does not + exist on `main`; after this prerequisite merges, the compiler-target branch + must route `Unknown` through placement only after rejecting remaining + non-barrier multi-site operations before placement changes the IR. + Date/Author: 2026-09-01 / OpenAI Codex. + +## Outcomes & Retrospective + +Target compilation now runs compact deterministic placement for the current main +branch's all-to-all target representation and retains topology-aware mapping +only for explicit coupling graphs. Both passes use the same allocation +discovery, capacity validation, and rewrite. The public surface grows by one +target-aware pass factory; no new dependency, option, or target-model concept +was introduced. + +The direct mapping binary passed all 86 tests, including scalar placement, +tensor placement with classical outputs, compact noncontiguous site selection, +and both placement and direct-mapping failure atomicity. The compiler binary +passed all 139 tests, including compact all-to-all target compilation and the +existing explicit-topology pipeline. Generated MLIR documentation, repository +format/lint, focused direct `clang-tidy`, and `git diff --check` all pass. + +The only deferred integration is intentional: after the compiler-target facts +branch is rebased, its `Unknown` connectivity case must reject remaining +non-barrier multi-site operations before invoking this placement pass. That case +cannot be represented on current `main` and does not belong in this +prerequisite. + +## Context and Orientation + +`mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp` contains the current +`MappingPass`. The pass first discovers top-level `qco.alloc` and +`qtensor.alloc` roots, replaces those dynamic roots with `qco.static` target +sites, and then routes two-qubit operations by inserting `qco.swap` operations. +The discovery code enforces the tensor lifetime shape supported by mapping: all +tensor qubits are extracted before any are inserted, and allocations occur in +the entry function body. + +Placement means assigning each program qubit to one hardware site. Routing means +changing that assignment during the program so that every multi-qubit operation +acts on connected sites. A target with all-to-all connectivity needs placement +but not routing. An explicit topology is a listed set of connected site pairs +and requires the router. + +`mlir/include/mlir/Dialect/QCO/Transforms/Passes.td` declares generated MLIR +passes and their documentation. +`mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h` declares the +target-aware pass factories. `mlir/lib/Compiler/TargetCompilation.cpp` builds +the target compilation pipeline. Direct mapping tests live in +`mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp`, and end-to-end +target pipeline tests live in +`mlir/unittests/Compiler/test_compiler_pipeline.cpp`. + +This worktree contains only this task. Preserve unrelated changes and do not +modify another task's worktree. This plan does not authorize any GitHub action. + +## Plan of Work + +In `Mapping.cpp`, move the wire bookkeeping, allocation records, discovery +routine, and allocation rewrite out of `MappingPass` into the file's anonymous +namespace. Keep the code internal. Rename the rewrite to `applyPlacement` and +pass the immutable `CompilerTarget` explicitly. The function returns the wire +state required by the existing router. + +Add a generated `PlacementPass` definition in `Passes.td` and implement it in +`Mapping.cpp`. The pass obtains the entry function, discovers the dynamic +qubits, checks that the target has enough sites, constructs the identity mapping +`0..numProgramQubits-1`, applies placement, and returns. The pass must validate +all conditions before applying the first rewrite so failures leave the module +unchanged. Declare a target-aware `createPlacementPass` factory in `Mapping.h`. + +Change `MappingPass` to reject a target without an explicit topology before it +discovers or changes the program. Keep its higher-arity operation diagnostic, +layout search, routing, statistics, and control-flow handling unchanged. Call +the shared `applyPlacement` utility after the router chooses its layout. + +Change `populateTargetCompilationPipeline` to select `PlacementPass` when +`hasExplicitTopology()` is false and `MappingPass` otherwise. Include `Target.h` +because the pipeline now inspects the target. Current `main` defines the +non-explicit case as all-to-all. When the compiler-target facts branch is +rebased, retain this split: use compact placement for `AllToAll`, routing for +`Explicit`, and validate that an `Unknown` target has no remaining non-barrier +multi-site operation before compact placement. + +Extend the mapping unit test binary with direct placement tests. Prove stable +site order, compact output on a larger all-to-all target, scalar and tensor +allocation replacement, verified output, and rejection of direct mapping for a +non-explicit target without mutation. Extend the compiler pipeline test with an +all-to-all target larger than its two-qubit program and assert that target +compilation emits only the used static sites and no routing swaps. + +Update the TableGen pass description to state the exact contracts and build the +generated MLIR pass documentation. Do not add a standalone changelog entry +because MQT Core v4 target compilation is unreleased. + +## Concrete Steps + +Run all commands from the repository root. + +After each source edit, build the focused targets: + + cmake --preset release + cmake --build --preset release --target \ + mqt-core-mlir-unittest-mapping mqt-core-mlir-unittests-compiler + +Run direct placement and routing tests first: + + ./build/release/mlir/unittests/Dialect/QCO/Transforms/Mapping/\ + mqt-core-mlir-unittest-mapping + +Run the compiler tests that exercise target-pipeline dispatch: + + ./build/release/mlir/unittests/Compiler/\ + mqt-core-mlir-unittests-compiler \ + --gtest_filter='CompilerPipelineTest.*Target*' + +Build generated pass documentation and then run the required checks: + + cmake --build --preset release --target mlir-doc + uvx nox -s cpp-lint + uvx nox -s lint + +Expected focused test output ends with all selected tests passing. The final +working tree contains only the plan, pass split, tests, and pass documentation +source changes. + +## Validation and Acceptance + +The direct placement tests must verify the input module before the pass and the +output module after success. For a target whose site identifiers are not dense, +program qubit zero must use the first listed target site and program qubit one +must use the second. A target with extra sites must not materialize unused +`qco.static` operations. Scalar `qco.alloc`, `qtensor.alloc`, `qtensor.extract`, +`qtensor.insert`, and `qtensor.dealloc` operations must be removed according to +the existing supported tensor contract. + +Running `MappingPass` directly with a non-explicit target must fail before +changing the printed module. Running it with an explicit topology must retain +the existing executable routing behavior. End-to-end target compilation for an +all-to-all target must succeed, use only the static sites needed by the program, +and contain no router-inserted swap. + +After the compiler-target facts branch is rebased, an additional direct test +must prove that unknown connectivity plus a non-barrier operation on two or more +sites fails before placement changes the module. Unknown connectivity with only +single-site operations must use the same deterministic compact placement. + +## Idempotence and Recovery + +All inspection, configure, build, and test commands are repeatable. CMake can +regenerate the build tree after the TableGen pass changes. If a rewrite test +fails, rerun only the mapping test binary with a GoogleTest filter. Do not reset +or discard unrelated files. Use `git diff` to identify and repair only this +task's changes. + +## Artifacts and Notes + +Initial evidence from current `main`: + + CompilerTarget::hasExplicitTopology() == false + means all-to-all connectivity. + MappingPass::place(...) iterates layout.nHardwareQubits(). + Layout::fromMapping([0, 1]) creates a compact two-site layout. + +Final validation evidence: + + mqt-core-mlir-unittest-mapping: 86 tests passed. + mqt-core-mlir-unittests-compiler: 139 tests passed. + cmake --build --preset release --target mlir-doc: passed. + uvx nox -s cpp-lint: build passed; no uncommitted files selected. + clang-tidy with build/cpp-lint and modified-line filters: passed. + uvx nox -s lint: passed. + git diff --check: passed. + +## Interfaces and Dependencies + +The final public C++ factory is: + + std::unique_ptr + createPlacementPass(const CompilerTarget& target); + +The generated pass is named `PlacementPass` and uses the command-line argument +`place-qubits`. It has no options. `MappingPass` keeps its existing factory and +options. Both implementations use the existing `CompilerTarget`, `Layout`, +`WireIterator`, `TensorIterator`, QCO, QTensor, and MLIR rewrite APIs. No new +dependency is added. + +Plan revision 2026-09-01: Created the initial self-contained implementation and +validation plan. The plan records the coordination boundary with the separate +compiler-target facts work because current `main` does not represent unknown +connectivity. + +Plan revision 2026-09-01 11:45Z: Recorded the completed implementation, +validation workaround for an uncommitted diff, test counts, and the remaining +pull request 2218 rebase adaptation. diff --git a/docs/mlir/target_compilation.md b/docs/mlir/target_compilation.md index dfbab0d5ea..f53ed0d6e2 100644 --- a/docs/mlir/target_compilation.md +++ b/docs/mlir/target_compilation.md @@ -40,7 +40,10 @@ compilation to an existing QCO program. Compilation runs in place. If a pass fails, earlier passes may already have changed the program. Copy the program before compilation if the caller must preserve the input. For pass-level benchmarking, the C++ API exposes separate factories for pre-routing -optimization, mapping, native synthesis, and conformance verification. +optimization, deterministic placement, topology-aware mapping, native synthesis, +and conformance verification. Target compilation uses compact placement on +all-to-all targets and the mapper only when the target has an explicit coupling +graph. Target compilation preserves quantum operations even when their final qubit values are not measured or returned. This supports measurement-free programs, diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h b/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h index 1daa17bdd9..2ff0c0326f 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h @@ -22,6 +22,9 @@ class CompilerTarget; namespace qco { +/// Create a deterministic placement pass for a compiler target. +std::unique_ptr createPlacementPass(const CompilerTarget& target); + /** * @brief Create a mapping pass instance for a compiler target. * @returns a pass object. diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 5faad215e5..8e144fd91f 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -114,18 +114,33 @@ def QuantumLoopUnroll // Transpilation Passes //===----------------------------------------------------------------------===// +def PlacementPass : Pass<"place-qubits", "mlir::ModuleOp"> { + let dependentDialects = ["mlir::qco::QCODialect"]; + let summary = "Assign dynamic program qubits to static target sites"; + let description = [{ + Assigns top-level scalar and tensor-backed dynamically allocated qubits to + static sites of a `CompilerTarget`. Program qubits use the first target + sites, in target order. The result is deterministic and does not + materialize unused target sites. + + The pass performs placement only. It does not inspect connectivity, insert + routing operations, or guarantee that multi-qubit operations satisfy a + target's coupling constraints. Tensor-backed qubits must all be extracted + before any qubit is inserted into the tensor again. + }]; +} + def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { let dependentDialects = ["mlir::qco::QCODialect"]; - let summary = "This pass ensures that a program meets the connectivity " - "constraints of a target architecture."; + let summary = "Place and route a program for an explicit target topology"; let description = [{ 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. - Target-defined site identifiers are retained in the resulting - `qco.static` operations. + requires an explicit 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. Target-defined site identifiers are retained in the resulting + `qco.static` operations. Use `place-qubits` when routing is unnecessary. 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. The layout covers the diff --git a/mlir/lib/Compiler/TargetCompilation.cpp b/mlir/lib/Compiler/TargetCompilation.cpp index ee61082743..9c142e8e51 100644 --- a/mlir/lib/Compiler/TargetCompilation.cpp +++ b/mlir/lib/Compiler/TargetCompilation.cpp @@ -10,6 +10,7 @@ #include "mlir/Compiler/TargetCompilation.h" +#include "mlir/Compiler/Target.h" #include "mlir/Dialect/QCO/Transforms/Mapping/Mapping.h" #include "mlir/Dialect/QCO/Transforms/Passes.h" #include "mlir/Support/Passes.h" @@ -25,7 +26,11 @@ void populateTargetCompilationPipeline(OpPassManager& pm, populateDecomposeMultiControlledPipeline(pm, 3); populateDefaultQCOOptimizationPipeline(pm); pm.addPass(qco::createFuseTwoQubitGates()); - pm.addPass(qco::createMappingPass(target, qco::MappingPassOptions{})); + if (target.hasExplicitTopology()) { + pm.addPass(qco::createMappingPass(target, qco::MappingPassOptions{})); + } else { + pm.addPass(qco::createPlacementPass(target)); + } populateQCOCleanupPipeline(pm); pm.addPass(qco::createTargetNativeSynthesis(target)); pm.addPass(createCSEPass()); diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 497ca04bae..057476f55b 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -66,87 +66,308 @@ namespace mlir::qco { using namespace mlir::qtensor; #define GEN_PASS_DEF_MAPPINGPASS +#define GEN_PASS_DEF_PLACEMENTPASS #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" namespace { -struct MappingPass : impl::MappingPassBase { -private: - using IndexPairType = std::pair; - using Window = SmallVector; - using Wires = SmallVector; +using Wires = SmallVector; - enum class RoutingMode : bool { Cold, Hot }; +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]; + } - struct CompositeUnitary { - /// The composite op (e.g. SCF). - Operation* op = nullptr; - /// Indices into a wire vector, where the order of indices has no meaning. - SmallVector indices; - }; + /// Return the mapped program index of a wire index. + [[nodiscard]] size_t lookupProgram(const size_t index) const { + return indexToProgram_[index]; + } - 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]; + /// Bidirectionally map a wire index to a program index. + /// Overwrites existing mappings. + void insertOrUpdate(const size_t index, const size_t prog) { + if (index >= indexToProgram_.size()) { + indexToProgram_.resize(index + 1); + } + if (prog >= programToIndex_.size()) { + programToIndex_.resize(prog + 1); } + 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. + void swap(const size_t prog0, const size_t prog1) { + const auto i0 = lookupIndex(prog0); + const auto i1 = lookupIndex(prog1); + std::swap(programToIndex_[prog0], programToIndex_[prog1]); + std::swap(indexToProgram_[i0], indexToProgram_[i1]); + } + + /// Return the number of index-wire mappings. + [[nodiscard]] size_t size() const { return indexToProgram_.size(); } + +private: + /// Maps the i-th wire index to a program index. + SmallVector indexToProgram_; + /// Maps a program index to the i-th wire index. + SmallVector programToIndex_; + /// Program indices that have corresponding wires. + DenseSet programs_; +}; - /// Return the mapped program index of a wire index. - [[nodiscard]] size_t lookupProgram(const size_t index) const { - return indexToProgram_[index]; +struct TensorAllocation { + qtensor::AllocOp allocation; + SmallVector operations; +}; + +struct Computation { + Wires wires; + WireInfos infos; + SmallVector scalarAllocations; + SmallVector tensorAllocations; +}; + +} // namespace + +/// Verify that every non-barrier unitary can be routed by the mapping pass. +static LogicalResult validateRoutingOperations(func::FuncOp func) { + const auto result = + func.walk([](UnitaryOpInterface unitary) { + if (isa(unitary) || unitary.getNumQubits() <= 2) { + return WalkResult::advance(); + } + unitary.emitError() + << "cannot route an operation acting on " << unitary.getNumQubits() + << " qubits; decompose it to one- and two-qubit operations first"; + return WalkResult::interrupt(); + }); + return result.wasInterrupted() ? failure() : success(); +} + +/// Discover the dynamic qubit roots of the entry function. +/// +/// Scalar `qco.alloc` operations define program qubits directly. For +/// `qtensor` allocations, placement 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] +/// → (Computation) +/// → N ⨉ [qtensor::InsertOp] +/// → T ⨉ [qtensor::DeallocOp] +/// +/// If any of the above assumptions are violated, the function returns +/// failure without changing the IR. +static FailureOr discoverComputation(func::FuncOp func) { + Computation computation; + + const auto discovery = func.walk([&](Operation* op) { + 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 placement requires dynamic qubit allocations in the entry " + "function body"; + return WalkResult::interrupt(); + }); + + if (discovery.wasInterrupted()) { + return failure(); + } + + for (auto alloc : computation.scalarAllocations) { + const auto index = computation.wires.size(); + computation.wires.emplace_back(alloc.getResult()); + computation.infos.insertOrUpdate(index, index); + } + + for (auto& tensor : computation.tensorAllocations) { + bool isInitPhase = true; + TensorIterator it(tensor.allocation.getResult()); + for (; it != std::default_sentinel; ++it) { + Operation* 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"; + } + + auto qubit = extract.getResult(); + const auto index = computation.wires.size(); - /// Bidirectionally map a wire index to a program index. - /// Overwrites existing mappings. - void insertOrUpdate(const size_t index, const size_t prog) { - if (index >= indexToProgram_.size()) { - indexToProgram_.resize(index + 1); + computation.wires.emplace_back(qubit); + computation.infos.insertOrUpdate(index, index); + continue; } - if (prog >= programToIndex_.size()) { - programToIndex_.resize(prog + 1); + + if (isa(operation)) { + isInitPhase = false; } - indexToProgram_[index] = prog; - programToIndex_[prog] = index; - programs_.insert(prog); } + } + + return computation; +} + +/// Check that the target has one site for every discovered program qubit. +static LogicalResult checkCapacity(func::FuncOp func, + const CompilerTarget& target, + const Computation& computation) { + if (computation.wires.size() <= target.numQubits()) { + return success(); + } + return func.emitError() << "requires " << computation.wires.size() + << " qubits, but the target supports " + << target.numQubits(); +} + +/// Replace dynamic qubit roots with the target sites selected by `layout`. +/// +/// Analogously to `discoverComputation`, the i-th extract operation defines +/// the i-th program qubit. The function assumes that discovery and capacity +/// checks succeeded. +static std::pair +applyPlacement(Region& body, const CompilerTarget& target, const Layout& layout, + Computation& computation, IRRewriter& rewriter) { + SmallVector staticQubits; + staticQubits.reserve(layout.nHardwareQubits()); + + rewriter.setInsertionPointToStart(&body.front()); + for (size_t hw = 0; hw < layout.nHardwareQubits(); ++hw) { + auto op = + StaticOp::create(rewriter, body.getLoc(), target.siteForVertex(hw)); + staticQubits.emplace_back(op.getQubit()); + rewriter.setInsertionPointAfter(op); + } - /// Return whether a program index has a corresponding wire. - [[nodiscard]] bool containsProgram(const size_t prog) const { - return programs_.contains(prog); + Wires wires; + WireInfos infos; + + for (auto alloc : computation.scalarAllocations) { + const auto prog = wires.size(); + auto qubit = staticQubits[layout.getHardwareIndex(prog)]; + + rewriter.replaceAllUsesWith(alloc.getResult(), qubit); + rewriter.eraseOp(alloc); + + wires.emplace_back(qubit); + infos.insertOrUpdate(prog, prog); + } + + for (auto& tensor : computation.tensorAllocations) { + for (Operation* operation : tensor.operations) { + TypeSwitch(operation) + .Case([&](auto op) { + const auto prog = wires.size(); + auto qubit = staticQubits[layout.getHardwareIndex(prog)]; + + rewriter.replaceAllUsesWith(op.getResult(), qubit); + rewriter.replaceAllUsesWith(op.getOutTensor(), op.getTensor()); + rewriter.eraseOp(op); + + wires.emplace_back(qubit); + infos.insertOrUpdate(prog, prog); + }) + .Case([&](auto op) { + rewriter.setInsertionPointAfter(op); + SinkOp::create(rewriter, op.getLoc(), op.getScalar()); + rewriter.replaceAllUsesWith(op.getResult(), op.getDest()); + rewriter.eraseOp(op); + }) + .Case([&](auto op) { rewriter.eraseOp(op); }); } - /// Swap two program indices. - void swap(const size_t prog0, const size_t prog1) { - const auto i0 = lookupIndex(prog0); - const auto i1 = lookupIndex(prog1); - std::swap(programToIndex_[prog0], programToIndex_[prog1]); - std::swap(indexToProgram_[i0], indexToProgram_[i1]); + rewriter.eraseOp(tensor.allocation); + } + + rewriter.setInsertionPoint(body.back().getTerminator()); + for (size_t prog = wires.size(); prog < layout.nHardwareQubits(); ++prog) { + const auto hw = layout.getHardwareIndex(prog); + auto qubit = staticQubits[hw]; + + wires.emplace_back(qubit); + infos.insertOrUpdate(prog, prog); + SinkOp::create(rewriter, body.getLoc(), qubit); + } + + return {wires, infos}; +} + +namespace { + +struct PlacementPass : impl::PlacementPassBase { + PlacementPass() = default; + explicit PlacementPass(const CompilerTarget& compilerTarget) + : target(compilerTarget) {} + +protected: + void runOnOperation() override { + if (!target) { + llvm::reportFatalUsageError("No compiler target specified!"); } - /// Return the number of index-wire mappings. - [[nodiscard]] size_t size() const { return indexToProgram_.size(); } + auto moduleOp = getOperation(); + auto func = mqt::getEntryPoint(moduleOp); + if (!func) { + moduleOp.emitError() << "does not contain an entry point function"; + signalPassFailure(); + return; + } - private: - /// Maps the i-th wire index to a program index. - SmallVector indexToProgram_; - /// Maps a program index to the i-th wire index. - SmallVector programToIndex_; - /// Program indices that have corresponding wires. - DenseSet programs_; - }; + auto computation = discoverComputation(func); + if (failed(computation) || + failed(checkCapacity(func, *target, *computation))) { + signalPassFailure(); + return; + } - struct TensorAllocation { - qtensor::AllocOp allocation; - SmallVector operations; - }; + const auto layout = Layout::fromMapping( + llvm::to_vector(llvm::seq(computation->wires.size()))); + IRRewriter rewriter(&getContext()); + applyPlacement(func.getFunctionBody(), *target, layout, *computation, + rewriter); + } - struct Computation { - Wires wires; - WireInfos infos; - SmallVector scalarAllocations; - SmallVector tensorAllocations; - bool hasTwoQubitOperations{false}; +private: + std::optional target; +}; + +struct MappingPass : impl::MappingPassBase { +private: + using IndexPairType = std::pair; + using Window = SmallVector; + + enum class RoutingMode : bool { Cold, Hot }; + + struct CompositeUnitary { + /// The composite op (e.g. SCF). + Operation* op = nullptr; + /// Indices into a wire vector, where the order of indices has no meaning. + SmallVector indices; }; /// Statistics collected while routing. @@ -354,43 +575,46 @@ struct MappingPass : impl::MappingPassBase { llvm::reportFatalUsageError("No compiler target specified!"); } - IRRewriter rewriter(&getContext()); + auto moduleOp = getOperation(); + if (!target->hasExplicitTopology()) { + moduleOp.emitError() + << "place-and-route requires an explicit target topology"; + signalPassFailure(); + return; + } - auto mod = getOperation(); - auto func = mqt::getEntryPoint(mod); + auto func = mqt::getEntryPoint(moduleOp); if (!func) { - mod.emitError() << "does not contain an entry point function"; + moduleOp.emitError() << "does not contain an entry point function"; signalPassFailure(); return; } - auto comp = discoverComputation(func); - if (failed(comp)) { + if (failed(validateRoutingOperations(func))) { signalPassFailure(); return; } - auto& body = func.getFunctionBody(); - auto& wires = comp->wires; - auto& infos = comp->infos; - - if (wires.size() > target->numQubits()) { - func.emitError() - << "requires " + Twine(wires.size()) + - " qubits. However, the architecture only supports " + - Twine(target->numQubits()) + " qubits."; + auto computation = discoverComputation(func); + if (failed(computation) || + failed(checkCapacity(func, *target, *computation))) { signalPassFailure(); return; } + auto& body = func.getFunctionBody(); + auto& wires = computation->wires; + auto& infos = computation->infos; auto layout = generateLayout(wires, infos); if (failed(layout)) { - func->emitError() << "failed to refine random initial layouts."; + func.emitError() << "failed to refine random initial layouts"; signalPassFailure(); return; } - std::tie(wires, infos) = std::move(place(body, *layout, *comp, rewriter)); + IRRewriter rewriter(&getContext()); + std::tie(wires, infos) = std::move( + applyPlacement(body, *target, *layout, *computation, rewriter)); RoutingBundle bundle{.wires = std::move(wires), .infos = std::move(infos), @@ -551,181 +775,6 @@ struct MappingPass : impl::MappingPassBase { return newWhileOp; } - /// Return the wires of a dynamic computation. - /// 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] - /// → (Computation) - /// → N ⨉ [qtensor::InsertOp] - /// → T ⨉ [qtensor::DeallocOp] - /// - /// If any of the above assumptions are violated, the function returns - /// failure. - 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(); - } - - for (auto alloc : computation.scalarAllocations) { - const auto index = computation.wires.size(); - computation.wires.emplace_back(alloc.getResult()); - computation.infos.insertOrUpdate(index, index); - } - - for (auto& tensor : computation.tensorAllocations) { - bool isInitPhase = true; - TensorIterator it(tensor.allocation.getResult()); - for (; it != std::default_sentinel; ++it) { - 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."; - } - - auto qubit = extract.getResult(); - const auto index = computation.wires.size(); - - computation.wires.emplace_back(qubit); - computation.infos.insertOrUpdate(index, index); - - continue; - } - - if (isa(operation)) { - isInitPhase = false; - continue; - } - } - } - - return computation; - } - - /// Perform placement by replacing dynamic qubits with static target sites - /// and extending control-flow operations with target sites used for routing. - /// - /// Analogously to the discoverComputation function, the i-th extract - /// operation defines the i-th program qubit. - std::pair place(Region& body, const Layout& layout, - Computation& computation, - IRRewriter& rewriter) { - SmallVector staticQubits; - staticQubits.reserve(target->numQubits()); - - // Create and save static qubit operations. - rewriter.setInsertionPointToStart(&body.front()); - for (size_t hw = 0; hw < layout.nHardwareQubits(); ++hw) { - const auto site = target->siteForVertex(hw); - auto op = StaticOp::create(rewriter, body.getLoc(), site); - staticQubits.emplace_back(op.getQubit()); - rewriter.setInsertionPointAfter(op); - } - - Wires wires; - WireInfos infos; - - for (auto alloc : computation.scalarAllocations) { - const auto prog = wires.size(); - const auto hw = layout.getHardwareIndex(prog); - auto qubit = staticQubits[hw]; - - rewriter.replaceAllUsesWith(alloc.getResult(), qubit); - rewriter.eraseOp(alloc); - - wires.emplace_back(qubit); - infos.insertOrUpdate(prog, prog); - } - - 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); - auto qubit = staticQubits[hw]; - - rewriter.replaceAllUsesWith(op.getResult(), qubit); - rewriter.replaceAllUsesWith(op.getOutTensor(), op.getTensor()); - rewriter.eraseOp(op); - - wires.emplace_back(qubit); - infos.insertOrUpdate(prog, prog); - }) - .Case([&](auto op) { - rewriter.setInsertionPointAfter(op); - SinkOp::create(rewriter, op.getLoc(), op.getScalar()); - rewriter.replaceAllUsesWith(op.getResult(), op.getDest()); - rewriter.eraseOp(op); - }) - .Case([&](auto op) { rewriter.eraseOp(op); }); - } - - rewriter.eraseOp(tensor.allocation); - } - - // Create sinks for remaining, unused, static qubits. - - rewriter.setInsertionPoint(body.back().getTerminator()); - for (size_t prog = wires.size(); prog < layout.nHardwareQubits(); ++prog) { - const auto hw = layout.getHardwareIndex(prog); - auto qubit = staticQubits[hw]; - - wires.emplace_back(qubit); - infos.insertOrUpdate(prog, prog); - - SinkOp::create(rewriter, body.getLoc(), qubit); - } - - return {wires, infos}; - } - /// Execute `ntrials` many (parallel) initial layout refinement trials and /// return the heuristically best one. /// @@ -735,11 +784,6 @@ struct MappingPass : impl::MappingPassBase { /// finally find the trial with the fewest SWAPs on the final backwards pass /// and return the respective layout. FailureOr generateLayout(const Wires& wires, const WireInfos& infos) { - if (!target->hasExplicitTopology()) { - return Layout::fromMapping( - llvm::to_vector(llvm::seq(target->numQubits()))); - } - std::mt19937_64 rng{seed}; struct Trial { @@ -1682,6 +1726,10 @@ struct MappingPass : impl::MappingPassBase { } // namespace +std::unique_ptr createPlacementPass(const CompilerTarget& target) { + return std::make_unique(target); +} + std::unique_ptr createMappingPass(const CompilerTarget& target, MappingPassOptions options) { return std::make_unique(target, options); diff --git a/mlir/unittests/Compiler/test_compiler_pipeline.cpp b/mlir/unittests/Compiler/test_compiler_pipeline.cpp index c65e198041..0b2e1bd93d 100644 --- a/mlir/unittests/Compiler/test_compiler_pipeline.cpp +++ b/mlir/unittests/Compiler/test_compiler_pipeline.cpp @@ -1524,7 +1524,8 @@ c = measure q; ASSERT_TRUE(qco); std::vector sites{llvm::cantFail(CompilerTarget::Site::create(2472)), - llvm::cantFail(CompilerTarget::Site::create(18449))}; + llvm::cantFail(CompilerTarget::Site::create(18449)), + llvm::cantFail(CompilerTarget::Site::create(65535))}; const auto target = llvm::cantFail(CompilerTarget::create(std::move(sites))); ASSERT_TRUE(qco->compileForTarget(target)); diff --git a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp index c23b3af89f..9329d05383 100644 --- a/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp +++ b/mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -62,6 +63,14 @@ using namespace mlir; using namespace mlir::qco; using mlir::mqt::getEntryPoint; +static std::string printModule(ModuleOp moduleOp) { + std::string result; + llvm::raw_string_ostream stream(result); + moduleOp.print(stream); + stream.flush(); + return result; +} + static SmallVector getQubitValues(ValueRange values) { return to_vector(llvm::make_filter_range( values, [](Value value) { return isa(value.getType()); })); @@ -315,6 +324,13 @@ class MappingPassFixture : public testing::Test { return applyPatternsGreedily(m, std::move(patterns)); } + static LogicalResult runPlacement(ModuleOp moduleOp, + const CompilerTarget& target) { + PassManager pm(moduleOp->getContext()); + pm.addPass(createPlacementPass(target)); + return pm.run(moduleOp); + } + std::unique_ptr context; }; @@ -460,7 +476,7 @@ TEST_F(MappingPassFixture, PreserveNoncontiguousTargetSiteIds) { EXPECT_EQ(numStatics, 3); } -TEST_F(MappingPassFixture, MapNoncontiguousTargetWithUnusedSites) { +TEST_F(MappingPassFixture, PlaceNoncontiguousTargetCompactly) { std::vector sites; sites.emplace_back(llvm::cantFail(CompilerTarget::Site::create(7))); sites.emplace_back(llvm::cantFail(CompilerTarget::Site::create(19))); @@ -475,25 +491,106 @@ TEST_F(MappingPassFixture, MapNoncontiguousTargetWithUnusedSites) { builder.sink(qubit); auto module = builder.finalize(bit); - PassManager pm(module->getContext()); - pm.addPass(createMappingPass(target, MappingPassOptions{.ntrials = 1})); - ASSERT_TRUE(pm.run(module.get()).succeeded()); + ASSERT_TRUE(runPlacement(module.get(), target).succeeded()); ASSERT_TRUE(succeeded(verify(*module))); EXPECT_TRUE(isExecutable(getEntryPoint(module.get()), target)); - const DenseSet expectedSites{7, 19, 42}; size_t numAllocations = 0; - size_t numStatics = 0; + SmallVector staticSites; size_t numSinks = 0; module->walk([&](AllocOp) { ++numAllocations; }); - module->walk([&](StaticOp op) { - ++numStatics; - EXPECT_TRUE(expectedSites.contains(op.getIndex())); - }); + module->walk([&](StaticOp op) { staticSites.emplace_back(op.getIndex()); }); module->walk([&](SinkOp) { ++numSinks; }); EXPECT_EQ(numAllocations, 0); - EXPECT_EQ(numStatics, 3); - EXPECT_EQ(numSinks, 3); + EXPECT_EQ(staticSites, (SmallVector{7})); + EXPECT_EQ(numSinks, 1); +} + +TEST_F(MappingPassFixture, PlaceTensorOnFirstTargetSites) { + std::vector sites{llvm::cantFail(CompilerTarget::Site::create(7)), + llvm::cantFail(CompilerTarget::Site::create(19)), + llvm::cantFail(CompilerTarget::Site::create(42)), + llvm::cantFail(CompilerTarget::Site::create(81))}; + const auto target = llvm::cantFail(CompilerTarget::create(std::move(sites))); + + QCOProgramBuilder builder(context.get()); + builder.initialize({builder.getI1Type(), builder.getI1Type()}); + Value tensor = builder.qtensorAlloc(2); + Value first; + Value second; + std::tie(tensor, first) = builder.qtensorExtract(tensor, 0); + std::tie(tensor, second) = builder.qtensorExtract(tensor, 1); + std::tie(first, second) = builder.cx(first, second); + Value firstBit; + Value secondBit; + std::tie(first, firstBit) = builder.measure(first); + std::tie(second, secondBit) = builder.measure(second); + tensor = builder.qtensorInsert(first, tensor, 0); + tensor = builder.qtensorInsert(second, tensor, 1); + builder.qtensorDealloc(tensor); + auto moduleOp = builder.finalize({firstBit, secondBit}); + + ASSERT_TRUE(runPlacement(moduleOp.get(), target).succeeded()); + ASSERT_TRUE(succeeded(verify(*moduleOp))); + EXPECT_TRUE(isExecutable(getEntryPoint(moduleOp.get()), target)); + + SmallVector staticSites; + size_t tensorOperations = 0; + size_t swaps = 0; + moduleOp->walk([&](Operation* operation) { + if (auto staticOp = dyn_cast(operation)) { + staticSites.emplace_back(staticOp.getIndex()); + } + tensorOperations += isa(operation); + swaps += isa(operation); + }); + EXPECT_EQ(staticSites, (SmallVector{7, 19})); + EXPECT_EQ(tensorOperations, 0); + EXPECT_EQ(swaps, 0); +} + +TEST_F(MappingPassFixture, RejectNonExplicitTopologyBeforeMutation) { + const auto target = llvm::cantFail(CompilerTarget::create(2)); + QCOProgramBuilder builder(context.get()); + builder.initialize(); + auto qubit = builder.h(builder.allocQubit()); + builder.sink(qubit); + auto moduleOp = builder.finalize(); + const auto before = printModule(moduleOp.get()); + + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE(failed(runPass(moduleOp.get(), target, MappingPassOptions{}))); + EXPECT_EQ(printModule(moduleOp.get()), before); + EXPECT_TRUE( + StringRef(diagnostics) + .contains("place-and-route requires an explicit target topology")); +} + +TEST_F(MappingPassFixture, RejectOversizedPlacementBeforeMutation) { + const auto target = llvm::cantFail(CompilerTarget::create(1)); + QCOProgramBuilder builder(context.get()); + builder.initialize(); + auto first = builder.allocQubit(); + auto second = builder.allocQubit(); + builder.sink(first); + builder.sink(second); + auto moduleOp = builder.finalize(); + const auto before = printModule(moduleOp.get()); + + std::string diagnostics; + ScopedDiagnosticHandler handler(context.get(), [&](Diagnostic& diagnostic) { + diagnostics += diagnostic.str(); + return success(); + }); + EXPECT_TRUE(failed(runPlacement(moduleOp.get(), target))); + EXPECT_EQ(printModule(moduleOp.get()), before); + EXPECT_TRUE(StringRef(diagnostics) + .contains("requires 2 qubits, but the target supports 1")); } TEST_F(MappingPassFixture, KeepWorkspaceSparseOnLargeTarget) { @@ -675,11 +772,10 @@ TEST_P(MappingPassTest, FailNestedScalarAllocation) { return success(); }); EXPECT_TRUE(failed(runPass(m.get(), target, MappingPassOptions{}))); - EXPECT_TRUE( - StringRef(diagnostics) - .contains( - "target mapping requires dynamic qubit allocations in the entry " - "function body")) + EXPECT_TRUE(StringRef(diagnostics) + .contains("target placement requires dynamic qubit " + "allocations in the entry " + "function body")) << diagnostics; } @@ -714,11 +810,10 @@ TEST_P(MappingPassTest, FailNestedTensorAllocation) { return success(); }); EXPECT_TRUE(failed(runPass(m.get(), target, MappingPassOptions{}))); - EXPECT_TRUE( - StringRef(diagnostics) - .contains( - "target mapping requires dynamic qubit allocations in the entry " - "function body")) + EXPECT_TRUE(StringRef(diagnostics) + .contains("target placement requires dynamic qubit " + "allocations in the entry " + "function body")) << diagnostics; } From 6564278d2cfea15920ae4a5bb40bd308b50a24ca Mon Sep 17 00:00:00 2001 From: Lukas Burgholzer Date: Tue, 1 Sep 2026 12:56:00 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Keep=20placement=20tar?= =?UTF-8?q?get-bound?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the generated targetless factory and CLI registration, and document the mapper topology precondition. Assisted-by: GPT-5.6 Sol via Codex --- .agent/plans/split-placement-routing.md | 61 ++++++++++++------- .../Dialect/QCO/Transforms/Mapping/Mapping.h | 5 +- .../mlir/Dialect/QCO/Transforms/Passes.td | 18 +----- .../QCO/Transforms/Mapping/Mapping.cpp | 21 ++++--- 4 files changed, 52 insertions(+), 53 deletions(-) diff --git a/.agent/plans/split-placement-routing.md b/.agent/plans/split-placement-routing.md index 01ce12d29e..4b06b8d999 100644 --- a/.agent/plans/split-placement-routing.md +++ b/.agent/plans/split-placement-routing.md @@ -39,6 +39,9 @@ current behavior. deterministic, and failure-atomicity tests plus target-pipeline coverage. - [x] (2026-09-01 11:45Z) Updated pass and user documentation and validated the focused binaries, MLIR docs, C++ lint, and full repository lint. +- [x] (2026-09-01 12:45Z) Removed the unusable targetless TableGen factory and + command-line registration after review; placement is now constructible + only through its target-bound C++ factory. ## Surprises & Discoveries @@ -58,14 +61,22 @@ current behavior. reported `No source files need checking` after its successful lint build. The modified translation units were instead checked directly with `clang-tidy` and line filters from the same `build/cpp-lint` compilation database. +- Observation: A TableGen pass declaration necessarily generates a targetless + factory and command-line registration, but placement cannot run without a + `CompilerTarget`. Evidence: the generated default constructor left the target + empty and could only terminate through `reportFatalUsageError`. ## Decision Log -- Decision: Add a dedicated `place-qubits` pass and keep `place-and-route` as - the routing pass. Rationale: Placement and routing are established compiler - stages with different information requirements. A separate pass removes the - topology-dependent router from all-to-all compilation. Date/Author: 2026-09-01 - / OpenAI Codex. +- Decision: Add a dedicated target-bound placement pass and keep + `place-and-route` as the routing pass. Rationale: Placement and routing are + established compiler stages with different information requirements. A + separate pass removes the topology-dependent router from all-to-all + compilation. Date/Author: 2026-09-01 / OpenAI Codex. +- Decision: Implement placement directly as a `PassWrapper` and expose only + `createPlacementPass(const CompilerTarget&)`. Rationale: placement has no + valid targetless form, so TableGen would create an unusable public API and + command-line registration. Date/Author: 2026-09-01 / OpenAI Codex. - Decision: Share internal allocation discovery and rewriting rather than invoking one pass from another. Rationale: The router must retain the wire and layout state returned by placement, which a nested pass invocation cannot @@ -119,11 +130,12 @@ acts on connected sites. A target with all-to-all connectivity needs placement but not routing. An explicit topology is a listed set of connected site pairs and requires the router. -`mlir/include/mlir/Dialect/QCO/Transforms/Passes.td` declares generated MLIR -passes and their documentation. -`mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h` declares the -target-aware pass factories. `mlir/lib/Compiler/TargetCompilation.cpp` builds -the target compilation pipeline. Direct mapping tests live in +`mlir/include/mlir/Dialect/QCO/Transforms/Passes.td` declares the generated +mapping pass and its documentation. The target-bound placement pass is private +to `Mapping.cpp`. `mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h` +declares the target-aware pass factories. +`mlir/lib/Compiler/TargetCompilation.cpp` builds the target compilation +pipeline. Direct mapping tests live in `mlir/unittests/Dialect/QCO/Transforms/Mapping/test_mapping.cpp`, and end-to-end target pipeline tests live in `mlir/unittests/Compiler/test_compiler_pipeline.cpp`. @@ -139,12 +151,13 @@ namespace. Keep the code internal. Rename the rewrite to `applyPlacement` and pass the immutable `CompilerTarget` explicitly. The function returns the wire state required by the existing router. -Add a generated `PlacementPass` definition in `Passes.td` and implement it in -`Mapping.cpp`. The pass obtains the entry function, discovers the dynamic -qubits, checks that the target has enough sites, constructs the identity mapping -`0..numProgramQubits-1`, applies placement, and returns. The pass must validate -all conditions before applying the first rewrite so failures leave the module -unchanged. Declare a target-aware `createPlacementPass` factory in `Mapping.h`. +Add a target-bound `PlacementPass` in `Mapping.cpp` and expose only its +`createPlacementPass(const CompilerTarget&)` factory in `Mapping.h`. The pass +obtains the entry function, discovers the dynamic qubits, checks that the target +has enough sites, constructs the identity mapping `0..numProgramQubits-1`, +applies placement, and returns. The pass must validate all conditions before +applying the first rewrite so failures leave the module unchanged. Do not +register a targetless command-line form. Change `MappingPass` to reject a target without an explicit topology before it discovers or changes the program. Keep its higher-arity operation diagnostic, @@ -166,9 +179,10 @@ non-explicit target without mutation. Extend the compiler pipeline test with an all-to-all target larger than its two-qubit program and assert that target compilation emits only the used static sites and no routing swaps. -Update the TableGen pass description to state the exact contracts and build the -generated MLIR pass documentation. Do not add a standalone changelog entry -because MQT Core v4 target compilation is unreleased. +Update the mapping pass description and the target-bound factory documentation +to state the exact contracts, then build the generated MLIR pass documentation. +Do not add a standalone changelog entry because MQT Core v4 target compilation +is unreleased. ## Concrete Steps @@ -256,9 +270,9 @@ The final public C++ factory is: std::unique_ptr createPlacementPass(const CompilerTarget& target); -The generated pass is named `PlacementPass` and uses the command-line argument -`place-qubits`. It has no options. `MappingPass` keeps its existing factory and -options. Both implementations use the existing `CompilerTarget`, `Layout`, +The placement implementation is an internal `PassWrapper` with no targetless +factory or command-line registration. `MappingPass` keeps its existing factory +and options. Both implementations use the existing `CompilerTarget`, `Layout`, `WireIterator`, `TensorIterator`, QCO, QTensor, and MLIR rewrite APIs. No new dependency is added. @@ -270,3 +284,6 @@ connectivity. Plan revision 2026-09-01 11:45Z: Recorded the completed implementation, validation workaround for an uncommitted diff, test counts, and the remaining pull request 2218 rebase adaptation. + +Plan revision 2026-09-01 12:45Z: Recorded the review-driven removal of the +unsafe targetless placement factory and command-line registration. diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h b/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h index 2ff0c0326f..6ef068926a 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Mapping/Mapping.h @@ -25,10 +25,7 @@ namespace qco { /// Create a deterministic placement pass for a compiler target. std::unique_ptr createPlacementPass(const CompilerTarget& target); -/** - * @brief Create a mapping pass instance for a compiler target. - * @returns a pass object. - */ +/// Create a mapping pass for a compiler target with explicit topology. std::unique_ptr createMappingPass(const CompilerTarget& target, MappingPassOptions options); diff --git a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td index 8e144fd91f..ff1e049157 100644 --- a/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td +++ b/mlir/include/mlir/Dialect/QCO/Transforms/Passes.td @@ -114,22 +114,6 @@ def QuantumLoopUnroll // Transpilation Passes //===----------------------------------------------------------------------===// -def PlacementPass : Pass<"place-qubits", "mlir::ModuleOp"> { - let dependentDialects = ["mlir::qco::QCODialect"]; - let summary = "Assign dynamic program qubits to static target sites"; - let description = [{ - Assigns top-level scalar and tensor-backed dynamically allocated qubits to - static sites of a `CompilerTarget`. Program qubits use the first target - sites, in target order. The result is deterministic and does not - materialize unused target sites. - - The pass performs placement only. It does not inspect connectivity, insert - routing operations, or guarantee that multi-qubit operations satisfy a - target's coupling constraints. Tensor-backed qubits must all be extracted - before any qubit is inserted into the tensor again. - }]; -} - def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { let dependentDialects = ["mlir::qco::QCODialect"]; let summary = "Place and route a program for an explicit target topology"; @@ -140,7 +124,7 @@ def MappingPass : Pass<"place-and-route", "mlir::ModuleOp"> { operations pass through, and every two-qubit operation is routed to adjacent target vertices. Higher-arity unitaries must be decomposed before mapping. Target-defined site identifiers are retained in the resulting - `qco.static` operations. Use `place-qubits` when routing is unnecessary. + `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. The layout covers the diff --git a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp index 057476f55b..fa42ccdeb7 100644 --- a/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp +++ b/mlir/lib/Dialect/QCO/Transforms/Mapping/Mapping.cpp @@ -66,7 +66,6 @@ namespace mlir::qco { using namespace mlir::qtensor; #define GEN_PASS_DEF_MAPPINGPASS -#define GEN_PASS_DEF_PLACEMENTPASS #include "mlir/Dialect/QCO/Transforms/Passes.h.inc" namespace { @@ -319,17 +318,19 @@ applyPlacement(Region& body, const CompilerTarget& target, const Layout& layout, namespace { -struct PlacementPass : impl::PlacementPassBase { - PlacementPass() = default; +struct PlacementPass final + : PassWrapper> { + MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(PlacementPass) + explicit PlacementPass(const CompilerTarget& compilerTarget) : target(compilerTarget) {} + void getDependentDialects(DialectRegistry& registry) const override { + registry.insert(); + } + protected: void runOnOperation() override { - if (!target) { - llvm::reportFatalUsageError("No compiler target specified!"); - } - auto moduleOp = getOperation(); auto func = mqt::getEntryPoint(moduleOp); if (!func) { @@ -340,7 +341,7 @@ struct PlacementPass : impl::PlacementPassBase { auto computation = discoverComputation(func); if (failed(computation) || - failed(checkCapacity(func, *target, *computation))) { + failed(checkCapacity(func, target, *computation))) { signalPassFailure(); return; } @@ -348,12 +349,12 @@ struct PlacementPass : impl::PlacementPassBase { const auto layout = Layout::fromMapping( llvm::to_vector(llvm::seq(computation->wires.size()))); IRRewriter rewriter(&getContext()); - applyPlacement(func.getFunctionBody(), *target, layout, *computation, + applyPlacement(func.getFunctionBody(), target, layout, *computation, rewriter); } private: - std::optional target; + CompilerTarget target; }; struct MappingPass : impl::MappingPassBase {