From 057fc186db7e1a0d990d6fcfdb1163434daa9425 Mon Sep 17 00:00:00 2001 From: Seemanta Bhattacharjee Date: Tue, 16 Jun 2026 04:25:19 +0600 Subject: [PATCH 1/7] add reverse-traversal initial-mapping search to qubit mapping plus tests Signed-off-by: Seemanta Bhattacharjee --- .../cudaq/Optimizer/Transforms/Passes.td | 8 + cudaq/lib/Optimizer/Transforms/Mapping.cpp | 992 +++++++++++++++--- .../test/Transforms/mapping_connectivity.qke | 40 + .../mapping_greedy_relocated_hub.qke | 46 + .../Transforms/mapping_invalid_placement.qke | 27 + .../Transforms/mapping_invalid_search.qke | 27 + .../test/Transforms/mapping_non_unitaries.qke | 2 +- cudaq/test/Transforms/mapping_phased_rx.qke | 2 +- .../Transforms/mapping_placement_seeds.qke | 40 + cudaq/test/Transforms/mapping_qspan.qke | 12 +- .../mapping_search_late_interactions.qke | 48 + cudaq/test/Transforms/mapping_search_none.qke | 33 + .../Transforms/mapping_search_refinement.qke | 42 + cudaq/test/Transforms/mapping_search_star.qke | 48 + .../Transforms/mapping_search_termination.qke | 263 +++++ cudaq/test/Transforms/mapping_unitaries.qke | 2 +- cudaq/test/Transforms/mapping_with_meas-1.qke | 2 +- 17 files changed, 1475 insertions(+), 159 deletions(-) create mode 100644 cudaq/test/Transforms/mapping_connectivity.qke create mode 100644 cudaq/test/Transforms/mapping_greedy_relocated_hub.qke create mode 100644 cudaq/test/Transforms/mapping_invalid_placement.qke create mode 100644 cudaq/test/Transforms/mapping_invalid_search.qke create mode 100644 cudaq/test/Transforms/mapping_placement_seeds.qke create mode 100644 cudaq/test/Transforms/mapping_search_late_interactions.qke create mode 100644 cudaq/test/Transforms/mapping_search_none.qke create mode 100644 cudaq/test/Transforms/mapping_search_refinement.qke create mode 100644 cudaq/test/Transforms/mapping_search_star.qke create mode 100644 cudaq/test/Transforms/mapping_search_termination.qke diff --git a/cudaq/include/cudaq/Optimizer/Transforms/Passes.td b/cudaq/include/cudaq/Optimizer/Transforms/Passes.td index f8ec1580aca..bd41bd2e055 100644 --- a/cudaq/include/cudaq/Optimizer/Transforms/Passes.td +++ b/cudaq/include/cudaq/Optimizer/Transforms/Passes.td @@ -956,6 +956,14 @@ def MappingFunc: Pass<"qubit-mapping-func", "mlir::func::FuncOp"> { "Decay delta">, Option<"roundsDecayReset", "roundsDecayReset", "unsigned", /*default=*/"5", "Number of rounds before decay is reset">, + Option<"placement", "placement", "std::string", + /*default=*/"\"auto\"", + "Initial placement strategy: auto (propose identity and greedy " + "seeds, select by routed swap count), identity, greedy">, + Option<"search", "search", "std::string", /*default=*/"\"sabre\"", + "Layout search strategy: sabre (SABRE forward-backward-forward " + "reverse-traversal refinement of each seed), none (single forward " + "route per seed; legacy mapping is placement=identity search=none)">, Option<"nonComposable", "raise-fatal-errors", "bool", /*default=*/"false", "Run the pass in a non-composable way, which may cause immediate " "internal compiler errors"> diff --git a/cudaq/lib/Optimizer/Transforms/Mapping.cpp b/cudaq/lib/Optimizer/Transforms/Mapping.cpp index af09fbab16e..8dcf84beb09 100644 --- a/cudaq/lib/Optimizer/Transforms/Mapping.cpp +++ b/cudaq/lib/Optimizer/Transforms/Mapping.cpp @@ -9,6 +9,7 @@ #include "PassDetails.h" #include "cudaq/Optimizer/Transforms/Passes.h" #include "cudaq/Support/Device.h" +#include "cudaq/Support/Handle.h" #include "cudaq/Support/Placement.h" #include "llvm/ADT/SmallSet.h" #include "llvm/ADT/StringSwitch.h" @@ -35,25 +36,383 @@ constexpr StringRef mappedWireSetName("mapped_wireset"); // Placement //===----------------------------------------------------------------------===// -void identityPlacement(cudaq::Placement &placement) { - for (unsigned i = 0, end = placement.getNumVirtualQubits(); i < end; ++i) - placement.map(cudaq::Placement::VirtualQ(i), cudaq::Placement::DeviceQ(i)); +/// Initial-layout strategy selected by the `placement` pass option. +enum class PlacementStrategy { Auto, Identity, Greedy }; + +/// Parse the `placement` option string, or nullopt for an unknown value. +std::optional parsePlacementStrategy(llvm::StringRef name) { + return llvm::StringSwitch>(name) + .Case("auto", PlacementStrategy::Auto) + .Case("identity", PlacementStrategy::Identity) + .Case("greedy", PlacementStrategy::Greedy) + .Default(std::nullopt); +} + +/// Builds a deterministic topology-aware initial layout by assigning highly +/// interacting virtual qubits to central physical qubits first. The greedy +/// growth and its tie-breaks make the layout a deterministic function of the +/// interaction counts and the device, so it is reproducible across runs. +class GreedyInitialPlacer { +public: + GreedyInitialPlacer(const cudaq::Device &device, + ArrayRef> interactions, + ArrayRef userVirtualQubits) + : device(device), interactions(interactions), + userVirtualQubits(userVirtualQubits), n(device.getNumQubits()), + vrToPhy(n, 0), placedVirtual(n, false), usedPhysical(n, false) {} + + /// Produce the `vrToPhy` seed layout. + SmallVector run() { + computeDegrees(); + + // No two-qubit interactions, so every layout routes identically; return the + // identity seed for a deterministic result. + if (!hasInteraction) { + for (unsigned v = 0; v < n; ++v) + vrToPhy[v] = v; + return vrToPhy; + } + + computeCentrality(); + + // Seed the highest-degree virtual qubit onto the most central physical + // qubit, then grow the layout around it. + place(chooseSeedVirtual(), bestFreePhysical()); + + unsigned remaining = 0; + for (unsigned u = 0; u < n; ++u) + if (userVirtualQubits[u] && !placedVirtual[u]) + ++remaining; + while (remaining > 0) { + unsigned v = chooseNextVirtual(); + place(v, bestPhysicalFor(v)); + --remaining; + } + + assignRemainingVirtuals(); + return vrToPhy; + } + +private: + /// Weighted degree of each virtual qubit: the sum of its two-qubit + /// interaction counts. Also records whether any interaction exists at all. + void computeDegrees() { + weightedDegree.assign(n, 0); + for (unsigned u = 0; u < n; ++u) { + for (unsigned v = 0; v < n; ++v) + weightedDegree[u] += interactions[u][v]; + if (weightedDegree[u] > 0) + hasInteraction = true; + } + } + + /// Physical centrality used to break ties: total distance to every other + /// qubit, and connectivity degree. + void computeCentrality() { + using Qubit = cudaq::Device::Qubit; + distanceSum.assign(n, 0); + physDegree.assign(n, 0); + for (unsigned p = 0; p < n; ++p) { + for (unsigned q = 0; q < n; ++q) + distanceSum[p] += device.getDistance(Qubit(p), Qubit(q)); + physDegree[p] = + static_cast(device.getNeighbours(Qubit(p)).size()); + } + } + + /// More central first: lower distance-sum, then higher degree, then lower + /// index. + bool physBetter(unsigned a, unsigned b) const { + if (distanceSum[a] != distanceSum[b]) + return distanceSum[a] < distanceSum[b]; + if (physDegree[a] != physDegree[b]) + return physDegree[a] > physDegree[b]; + return a < b; + } + + /// The most central physical qubit not yet used. + unsigned bestFreePhysical() const { + unsigned best = n; + for (unsigned p = 0; p < n; ++p) + if (!usedPhysical[p] && (best == n || physBetter(p, best))) + best = p; + return best; + } + + /// The highest weighted-degree virtual qubit, breaking ties toward the lower + /// index. + unsigned chooseSeedVirtual() const { + unsigned seed = n; + for (unsigned u = 0; u < n; ++u) + if (userVirtualQubits[u] && + (seed == n || weightedDegree[u] > weightedDegree[seed])) + seed = u; + return seed; + } + + /// The unplaced virtual qubit most connected to the placed ones, falling back + /// to weighted degree for disconnected components. + unsigned chooseNextVirtual() const { + unsigned pick = n; + unsigned pickWeight = 0; + for (unsigned u = 0; u < n; ++u) { + if (!userVirtualQubits[u] || placedVirtual[u]) + continue; + unsigned placedWeight = 0; + for (unsigned v = 0; v < n; ++v) + if (placedVirtual[v]) + placedWeight += interactions[u][v]; + if (pick == n || placedWeight > pickWeight) { + pick = u; + pickWeight = placedWeight; + } + } + if (pickWeight == 0) { + pick = n; + unsigned pickDegree = 0; + for (unsigned u = 0; u < n; ++u) { + if (!userVirtualQubits[u] || placedVirtual[u]) + continue; + if (pick == n || weightedDegree[u] > pickDegree) { + pick = u; + pickDegree = weightedDegree[u]; + } + } + } + return pick; + } + + /// The free physical qubit minimizing weighted distance from `v` to its + /// placed partners, breaking ties by centrality. + unsigned bestPhysicalFor(unsigned v) const { + using Qubit = cudaq::Device::Qubit; + unsigned bestPhy = n; + unsigned bestCost = 0; + for (unsigned p = 0; p < n; ++p) { + if (usedPhysical[p]) + continue; + unsigned cost = 0; + for (unsigned w = 0; w < n; ++w) + if (placedVirtual[w] && interactions[v][w] > 0) + cost += interactions[v][w] * + device.getDistance(Qubit(p), Qubit(vrToPhy[w])); + bool better = bestPhy == n || cost < bestCost || + (cost == bestCost && physBetter(p, bestPhy)); + if (better) { + bestPhy = p; + bestCost = cost; + } + } + return bestPhy; + } + + /// Map virtual `v` onto physical `p` and mark both as taken. + void place(unsigned v, unsigned p) { + vrToPhy[v] = p; + placedVirtual[v] = true; + usedPhysical[p] = true; + } + + /// Assign any still-unplaced virtuals (non-user qubits) to the remaining free + /// physicals in order. + void assignRemainingVirtuals() { + unsigned nextPhy = 0; + for (unsigned v = 0; v < n; ++v) { + if (placedVirtual[v]) + continue; + while (nextPhy < n && usedPhysical[nextPhy]) + ++nextPhy; + place(v, nextPhy); + } + } + + const cudaq::Device &device; + ArrayRef> interactions; + ArrayRef userVirtualQubits; + const unsigned n; + + SmallVector weightedDegree; + bool hasInteraction = false; + SmallVector distanceSum; + SmallVector physDegree; + + SmallVector placedVirtual; + SmallVector usedPhysical; + SmallVector vrToPhy; +}; + +/// Greedy initial placement over the circuit interaction graph. Returns a +/// `vrToPhy` array proposing a starting layout for the router (the greedy +/// seed). +SmallVector +interactionPlacement(const cudaq::Device &device, + ArrayRef> interactions, + ArrayRef userVirtualQubits) { + return GreedyInitialPlacer(device, interactions, userVirtualQubits).run(); } //===----------------------------------------------------------------------===// // Routing //===----------------------------------------------------------------------===// -/// This class encapsulates an quake operation that uses wires with information -/// about the virtual qubits these wires correspond. -struct VirtualOp { +/// The dependency DAG the router walks. It is built once from the IR and never +/// modified, so a layout can be routed without touching the circuit. Each node +/// is a routable operation. Its successors are the operations that consume its +/// result wires. +struct RoutingProblem { + /// A handle to a node in the DAG, i.e. an index into `nodes`. + struct NodeRef : cudaq::Handle { + using Handle::Handle; + }; + + struct Node { + mlir::Operation *op; + /// Virtual qubits used by `op`, in quantum-operand order. + SmallVector qubits; + /// Routable users of `op`'s result wires, in use-list order. A user appears + /// once per result wire it consumes, so a node becomes ready once it has + /// been visited as many times as it has wire operands. + SmallVector successors; + bool isMeasure = false; + /// A gate (not a measurement, sink, or return). Only these participate in + /// the reverse-traversal pass. + bool isUnitary = false; + /// Two-qubit unitaries are the only nodes that join the extended layer. + bool isTwoQ = false; + }; + + /// Routable operations, in program order. + SmallVector nodes; + /// Routable users of the source wires, in source order then use-list order. + /// These seed the first front layer. + SmallVector sourceUsers; + + const Node &operator[](NodeRef n) const { + assert(n.isValid() && "invalid node handle"); + return nodes[n.index]; + } +}; + +/// A single routing decision: a gate mapped onto physical qubits, or a swap +/// inserted between them. The router records these as it walks the circuit and +/// the emitter replays them to rewrite the IR. +struct RoutingEvent { + enum class Kind { Gate, Swap }; + + /// A gate mapped onto the physical qubits `phys`, in operand order. + static RoutingEvent gate(mlir::Operation *op, + ArrayRef phys) { + return RoutingEvent{ + Kind::Gate, op, + SmallVector(phys.begin(), phys.end())}; + } + /// A swap inserted between physical qubits `q0` and `q1`. + static RoutingEvent swap(cudaq::Placement::DeviceQ q0, + cudaq::Placement::DeviceQ q1) { + return RoutingEvent{Kind::Swap, nullptr, {q0, q1}}; + } + + Kind kind; mlir::Operation *op; - SmallVector qubits; + SmallVector phys; +}; - VirtualOp(mlir::Operation *op, ArrayRef qubits) - : op(op), qubits(qubits) {} +/// The outcome of routing one layout. The emitter replays `trace` onto the IR. +/// `swapCount` is the metric used to compare layouts. +struct RoutingResult { + /// Virtual-to-physical layout at the start of the walk, before any swap. + SmallVector initialLayout; + SmallVector trace; + unsigned swapCount = 0; }; +/// Build the routing problem from `block`. The nodes are the routable +/// operations that `isSupportedMappingOperation` accepts, other than the source +/// borrows. Edges and source successors are captured in MLIR use-list order so +/// the walk visits successors in the same order as the SSA use-def chains. +RoutingProblem buildRoutingProblem( + Block &block, ArrayRef sources, + const DenseMap &wireToVirtualQ) { + RoutingProblem problem; + DenseMap nodeIndex; + + for (Operation &op : block) { + if (isa(op) || + !cudaq::quake::isSupportedMappingOperation(&op)) + continue; + RoutingProblem::Node node; + node.op = &op; + for (auto wire : cudaq::quake::getQuantumOperands(&op)) + node.qubits.push_back(wireToVirtualQ.lookup(wire)); + node.isMeasure = op.hasTrait(); + node.isUnitary = isa(op); + // A two-qubit gate the router has to make adjacent: a unitary on two wires, + // not a measurement or a sink. + node.isTwoQ = node.isUnitary && node.qubits.size() == 2; + nodeIndex[&op] = RoutingProblem::NodeRef(problem.nodes.size()); + problem.nodes.push_back(std::move(node)); + } + + // Record successor edges in use-list order, keeping the routable users only. + // A user is listed once per result wire it consumes, so a node's visit count + // reaches its wire-operand count exactly when all of its inputs are ready. + auto recordUsers = [&](Operation *producer, + SmallVectorImpl &out) { + for (Operation *user : producer->getUsers()) + if (auto it = nodeIndex.find(user); it != nodeIndex.end()) + out.push_back(it->second); + }; + for (auto &node : problem.nodes) + recordUsers(node.op, node.successors); + for (auto borrow : sources) + recordUsers(borrow.getOperation(), problem.sourceUsers); + + return problem; +} + +/// Build the transposed problem over the unitary gates only, for the SABRE +/// reverse-traversal pass. Routing this forward is equivalent to routing the +/// original circuit in reverse: a node's successors here are its forward +/// predecessors, and result wires that do not feed another unitary (the +/// circuit's outputs) seed the walk. Measurements, sinks, and returns are not +/// unitary nodes, so they drop out, which is the paper's "skip measurements in +/// the reverse pass". Readiness is unchanged: a unitary has equal operand and +/// result arity, so its threshold is `qubits.size()` in both directions. +RoutingProblem buildReverseProblem(const RoutingProblem &forward) { + RoutingProblem reverse; + SmallVector fwdToRev(forward.nodes.size()); + for (unsigned i = 0, end = forward.nodes.size(); i < end; ++i) { + const RoutingProblem::Node &node = forward.nodes[i]; + if (!node.isUnitary) + continue; + fwdToRev[i] = RoutingProblem::NodeRef(reverse.nodes.size()); + RoutingProblem::Node rev; + rev.op = node.op; + rev.qubits = node.qubits; + rev.isUnitary = true; + rev.isTwoQ = node.isTwoQ; + reverse.nodes.push_back(std::move(rev)); + } + + for (unsigned i = 0, end = forward.nodes.size(); i < end; ++i) { + const RoutingProblem::Node &node = forward.nodes[i]; + if (!node.isUnitary) + continue; + unsigned unitarySuccessors = 0; + for (RoutingProblem::NodeRef s : node.successors) { + if (!forward[s].isUnitary) + continue; + ++unitarySuccessors; + // Processing the consumer in reverse makes this producer ready. + reverse.nodes[fwdToRev[s.index].index].successors.push_back(fwdToRev[i]); + } + // Each result wire that does not feed a unitary is a reverse-circuit input. + for (unsigned k = unitarySuccessors; k < node.qubits.size(); ++k) + reverse.sourceUsers.push_back(fwdToRev[i]); + } + return reverse; +} + /// The `SabreRouter` class is modified implementation of the following paper: /// Li, Gushu, Yufei Ding, and Yuan Xie. "Tackling the qubit mapping problem for /// NISQ-era quantum devices." In Proceedings of the Twenty-Fourth International @@ -85,45 +444,46 @@ struct VirtualOp { /// measurement mapping until the end, which is required for QIR Base Profile /// programs (see the `allowMeasurementMapping` member variable). class SabreRouter { - using WireMap = DenseMap; using Swap = std::pair; + using NodeRef = RoutingProblem::NodeRef; public: - SabreRouter(const cudaq::Device &device, WireMap &wireMap, + SabreRouter(const cudaq::Device &device, const RoutingProblem &problem, cudaq::Placement &placement, unsigned extendedLayerSize, float extendedLayerWeight, float decayDelta, unsigned roundsDecayReset) - : device(device), wireToVirtualQ(wireMap), placement(placement), + : device(device), problem(problem), placement(placement), extendedLayerSize(extendedLayerSize), extendedLayerWeight(extendedLayerWeight), decayDelta(decayDelta), roundsDecayReset(roundsDecayReset), - phyDecay(device.getNumQubits(), 1.0), phyToWire(device.getNumQubits()), - allowMeasurementMapping(false) {} - - /// Main entry point into SabreRouter routing algorithm - void route(Block &block, ArrayRef sources); + phyDecay(device.getNumQubits(), 1.0), allowMeasurementMapping(false) {} - /// After routing, this contains the final values for all the qubits - ArrayRef getPhyToWire() { return phyToWire; } + /// Main entry point into SabreRouter routing algorithm. Walks the DAG without + /// modifying the IR and returns the decisions for the emitter to apply. + RoutingResult route(); private: - void visitUsers(ResultRange::user_range users, - SmallVectorImpl &layer, - SmallVectorImpl *incremented = nullptr); + /// Visit each node in `successors` and bump its count. A node that has been + /// visited once per wire operand joins `layer`, or `measureLayer` if it is a + /// deferred measurement. `incremented` records the bumps so a lookahead can + /// undo them. + void visitSuccessors(ArrayRef successors, + SmallVectorImpl &layer, + SmallVectorImpl *incremented = nullptr); - LogicalResult mapOperation(VirtualOp &virtOp); + LogicalResult mapOperation(NodeRef node); LogicalResult mapFrontLayer(); void selectExtendedLayer(); - double computeLayerCost(ArrayRef layer); + double computeLayerCost(ArrayRef layer); Swap chooseSwap(); private: const cudaq::Device &device; - WireMap &wireToVirtualQ; + const RoutingProblem &problem; cudaq::Placement &placement; // Parameters @@ -132,18 +492,20 @@ class SabreRouter { const float decayDelta; const unsigned roundsDecayReset; - // Internal data - SmallVector frontLayer; - SmallVector extendedLayer; - SmallVector measureLayer; - llvm::SmallPtrSet measureLayerSet; + // Internal data. The layers hold handles into `problem.nodes`. + SmallVector frontLayer; + SmallVector extendedLayer; + SmallVector measureLayer; + SmallVector measureLayerSeen; llvm::SmallSet involvedPhy; SmallVector phyDecay; - SmallVector phyToWire; + /// The routing decisions accumulated during the current walk. + RoutingResult result; - /// Keeps track of how many times an operation was visited. - DenseMap visited; + /// How many times each node has been visited. A node is ready once this + /// reaches its wire-operand count. + SmallVector visitCount; /// Keep track of whether or not we're in the phase that allows measurements /// to be mapped @@ -155,99 +517,74 @@ class SabreRouter { #endif }; -void SabreRouter::visitUsers(ResultRange::user_range users, - SmallVectorImpl &layer, - SmallVectorImpl *incremented) { - for (auto user : users) { - auto [entry, created] = visited.try_emplace(user, 1); - if (!created) - entry->second += 1; +void SabreRouter::visitSuccessors(ArrayRef successors, + SmallVectorImpl &layer, + SmallVectorImpl *incremented) { + for (NodeRef s : successors) { + unsigned count = ++visitCount[s.index]; if (incremented) - incremented->push_back(user); + incremented->push_back(s); - if (!cudaq::quake::isSupportedMappingOperation(user)) { - LLVM_DEBUG({ - auto *tmpOp = dyn_cast(user); - logger.getOStream() << "WARNING: unsupported op: " << *tmpOp << '\n'; - }); - } else { - auto wires = cudaq::quake::getQuantumOperands(user); - if (entry->second == wires.size()) { - SmallVector qubits; - for (auto wire : wires) - qubits.push_back(wireToVirtualQ[wire]); - // Don't process measurements until we're ready - if (allowMeasurementMapping || - !user->hasTrait()) { - layer.emplace_back(user, qubits); - } else { - // Add to measureLayer. Don't add duplicates. - if (measureLayerSet.find(user) == measureLayerSet.end()) { - measureLayer.emplace_back(user, qubits); - measureLayerSet.insert(user); - } - } - } + const RoutingProblem::Node &node = problem[s]; + if (count != node.qubits.size()) + continue; + // Don't process measurements until we're ready. + if (allowMeasurementMapping || !node.isMeasure) { + layer.push_back(s); + } else if (!measureLayerSeen[s.index]) { + // Add to measureLayer. Don't add duplicates. + measureLayerSeen[s.index] = true; + measureLayer.push_back(s); } } } -LogicalResult SabreRouter::mapOperation(VirtualOp &virtOp) { +LogicalResult SabreRouter::mapOperation(NodeRef nodeRef) { + const RoutingProblem::Node &node = problem[nodeRef]; + // Take the device qubits from this operation. SmallVector deviceQubits; - for (auto vr : virtOp.qubits) + for (auto vr : node.qubits) deviceQubits.push_back(placement.getPhy(vr)); // An operation cannot be mapped if it is not a measurement and uses two - // qubits virtual qubit that are no adjacently placed. - if (!virtOp.op->hasTrait() && - deviceQubits.size() == 2 && + // virtual qubits that are not adjacently placed. + if (!node.isMeasure && deviceQubits.size() == 2 && !device.areConnected(deviceQubits[0], deviceQubits[1])) return failure(); - // Rewire the operation. - SmallVector newOpWires; - for (auto phy : deviceQubits) - newOpWires.push_back(phyToWire[phy.index]); - if (failed(cudaq::quake::setQuantumOperands(virtOp.op, newOpWires))) - return failure(); - - if (isa(virtOp.op)) - return success(); - - // Update the mapping between device qubits and wires. - for (auto &&[w, q] : llvm::zip_equal( - cudaq::quake::getQuantumResults(virtOp.op), deviceQubits)) - phyToWire[q.index] = w; - + // Record the placement. The emitter rewires the operation when it applies + // the result. + result.trace.push_back(RoutingEvent::gate(node.op, deviceQubits)); return success(); } LogicalResult SabreRouter::mapFrontLayer() { bool mappedAtLeastOne = false; - SmallVector newFrontLayer; + SmallVector newFrontLayer; LLVM_DEBUG({ logger.startLine() << "Mapping front layer:\n"; logger.indent(); }); - for (auto virtOp : frontLayer) { + for (NodeRef n : frontLayer) { + const RoutingProblem::Node &node = problem[n]; LLVM_DEBUG({ logger.startLine() << "* "; - virtOp.op->print(logger.getOStream(), - OpPrintingFlags().printGenericOpForm()); + node.op->print(logger.getOStream(), + OpPrintingFlags().printGenericOpForm()); }); - if (failed(mapOperation(virtOp))) { + if (failed(mapOperation(n))) { LLVM_DEBUG(logger.getOStream() << " --> FAILURE\n"); - newFrontLayer.push_back(virtOp); - for (auto vr : virtOp.qubits) + newFrontLayer.push_back(n); + for (auto vr : node.qubits) involvedPhy.insert(placement.getPhy(vr)); LLVM_DEBUG({ - auto phy0 = placement.getPhy(virtOp.qubits[0]); - auto phy1 = placement.getPhy(virtOp.qubits[1]); + auto phy0 = placement.getPhy(node.qubits[0]); + auto phy1 = placement.getPhy(node.qubits[1]); logger.indent(); - logger.startLine() << "+ virtual qubits: " << virtOp.qubits[0] << ", " - << virtOp.qubits[1] << '\n'; + logger.startLine() << "+ virtual qubits: " << node.qubits[0] << ", " + << node.qubits[1] << '\n'; logger.startLine() << "+ device qubits: " << phy0 << ", " << phy1 << '\n'; logger.unindent(); @@ -256,7 +593,7 @@ LogicalResult SabreRouter::mapFrontLayer() { } LLVM_DEBUG(logger.getOStream() << " --> SUCCESS\n"); mappedAtLeastOne = true; - visitUsers(virtOp.op->getUsers(), newFrontLayer); + visitSuccessors(node.successors, newFrontLayer); } LLVM_DEBUG(logger.unindent()); frontLayer = std::move(newFrontLayer); @@ -265,30 +602,30 @@ LogicalResult SabreRouter::mapFrontLayer() { void SabreRouter::selectExtendedLayer() { extendedLayer.clear(); - SmallVector incremented; - SmallVector tmpLayer = frontLayer; + SmallVector incremented; + SmallVector tmpLayer = frontLayer; while (!tmpLayer.empty() && extendedLayer.size() < extendedLayerSize) { - SmallVector newTmpLayer; - for (VirtualOp &virtOp : tmpLayer) - visitUsers(virtOp.op->getUsers(), newTmpLayer, &incremented); - for (VirtualOp &virtOp : newTmpLayer) + SmallVector newTmpLayer; + for (NodeRef n : tmpLayer) + visitSuccessors(problem[n].successors, newTmpLayer, &incremented); + for (NodeRef n : newTmpLayer) // We only add operations that can influence placement to the extended // frontlayer, i.e., quantum operators that use two qubits. - if (!virtOp.op->hasTrait() && - cudaq::quake::getQuantumOperands(virtOp.op).size() == 2) - extendedLayer.emplace_back(virtOp); + if (problem[n].isTwoQ) + extendedLayer.push_back(n); tmpLayer = std::move(newTmpLayer); } - for (auto virtOp : incremented) - visited[virtOp] -= 1; + for (NodeRef n : incremented) + --visitCount[n.index]; } -double SabreRouter::computeLayerCost(ArrayRef layer) { +double SabreRouter::computeLayerCost(ArrayRef layer) { double cost = 0.0; - for (VirtualOp const &virtOp : layer) { - auto phy0 = placement.getPhy(virtOp.qubits[0]); - auto phy1 = placement.getPhy(virtOp.qubits[1]); + for (NodeRef n : layer) { + const RoutingProblem::Node &node = problem[n]; + auto phy0 = placement.getPhy(node.qubits[0]); + auto phy1 = placement.getPhy(node.qubits[1]); cost += device.getDistance(phy0, phy1) - 1; } return cost / layer.size(); @@ -349,47 +686,84 @@ SabreRouter::Swap SabreRouter::chooseSwap() { return candidates[minIdx]; } -void SabreRouter::route(Block &block, - ArrayRef sources) { +RoutingResult SabreRouter::route() { #ifndef NDEBUG constexpr char logLineComment[] = "//===-------------------------------------------===//\n"; #endif + // Record the initial layout before routing starts moving qubits around. + result = RoutingResult{}; + result.initialLayout.resize(placement.getNumVirtualQubits()); + for (unsigned v = 0, end = placement.getNumVirtualQubits(); v < end; ++v) + result.initialLayout[v] = + placement.getPhy(cudaq::Placement::VirtualQ(v)).index; + + visitCount.assign(problem.nodes.size(), 0); + measureLayerSeen.assign(problem.nodes.size(), false); + LLVM_DEBUG({ logger.getOStream() << "\n"; logger.startLine() << logLineComment; logger.startLine() << "Mapping front layer:\n"; - logger.indent(); - for (auto virtOp : sources) - logger.startLine() << "* " << *virtOp << " --> SUCCESS\n"; - logger.unindent(); logger.startLine() << logLineComment; }); // The source ops can always be mapped. - for (auto borrowWire : sources) { - visitUsers(borrowWire->getUsers(), frontLayer); - Value wire = borrowWire.getResult(); - auto phy = placement.getPhy(wireToVirtualQ[wire]); - phyToWire[phy.index] = wire; - } + visitSuccessors(problem.sourceUsers, frontLayer); - OpBuilder builder(&block, block.begin()); - auto wireType = builder.getType(); auto addSwap = [&](cudaq::Placement::DeviceQ q0, cudaq::Placement::DeviceQ q1) { placement.swap(q0, q1); - auto swap = cudaq::quake::SwapOp::create( - builder, builder.getUnknownLoc(), TypeRange{wireType, wireType}, false, - ValueRange{}, ValueRange{}, - ValueRange{phyToWire[q0.index], phyToWire[q1.index]}, - DenseBoolArrayAttr{}); - phyToWire[q0.index] = swap.getResult(0); - phyToWire[q1.index] = swap.getResult(1); + result.trace.push_back(RoutingEvent::swap(q0, q1)); + ++result.swapCount; }; + // Release valve: bring the closest front-layer gate together along a shortest + // path, ignoring the heuristic. SABRE's decay only softly discourages the + // local minima the heuristic can fall into, so a stuck front layer would + // otherwise loop forever; forcing the gate guarantees progress. This follows + // the release-valve idea from Qiskit/LightSABRE (arXiv:2409.08368). + auto forceClosestGate = [&]() { + NodeRef closest; + unsigned bestDist = ~0u; + for (NodeRef n : frontLayer) { + const RoutingProblem::Node &node = problem[n]; + if (!node.isTwoQ) + continue; + unsigned d = device.getDistance(placement.getPhy(node.qubits[0]), + placement.getPhy(node.qubits[1])); + if (d < bestDist) { + bestDist = d; + closest = n; + } + } + assert(closest.isValid() && + "a stalled front layer must hold a 2-qubit gate"); + const RoutingProblem::Node &node = problem[closest]; + cudaq::Device::Path path = device.getShortestPath( + placement.getPhy(node.qubits[0]), placement.getPhy(node.qubits[1])); + // Move one qubit along the path until it is adjacent to the other. + for (unsigned i = 0; i + 2 < path.size(); ++i) + addSwap(path[i], path[i + 1]); + }; + + // SABRE's cost function is a heuristic. If it emits a long run of swaps + // without making any front-layer gate executable, discard that local episode + // and force one gate along a shortest path. This budget is deliberately + // loose: bringing one front-layer gate adjacent costs at most the device + // diameter, and the qubit count upper-bounds the diameter of a connected + // device. The multiplier gives the heuristic several times that worst-case + // direct-routing cost to explore and recover before the release valve fires. + // The floor keeps a usable budget on small devices, where the scaled term + // would otherwise be too tight. + constexpr unsigned minStallSwapBudget = 64; + constexpr unsigned stallSwapBudgetPerQubit = 4; + const unsigned stallSwapLimit = std::max( + minStallSwapBudget, stallSwapBudgetPerQubit * device.getNumQubits()); std::size_t numSwapSearches = 0; + unsigned swapsSinceRouted = 0; + SmallVector episodeSwaps; bool done = false; while (!done) { // Once frontLayer is empty, grab everything from measureLayer and go again. @@ -408,15 +782,36 @@ void SabreRouter::route(Block &block, logger.startLine() << logLineComment; }); - if (succeeded(mapFrontLayer())) + if (succeeded(mapFrontLayer())) { + swapsSinceRouted = 0; + episodeSwaps.clear(); continue; + } LLVM_DEBUG(logger.getOStream() << "\n";); + if (swapsSinceRouted >= stallSwapLimit) { + // Unwind the heuristic swaps back to the last routed gate, then force the + // closest gate together so the walk always makes progress. The decay + // state is left as is; it is a soft heuristic and resets on its own + // cycle. + for (unsigned i = episodeSwaps.size(); i-- > 0;) + placement.swap(episodeSwaps[i].first, episodeSwaps[i].second); + result.trace.pop_back_n(episodeSwaps.size()); + result.swapCount -= episodeSwaps.size(); + episodeSwaps.clear(); + forceClosestGate(); + swapsSinceRouted = 0; + involvedPhy.clear(); + continue; + } + // Add a swap numSwapSearches++; auto [phy0, phy1] = chooseSwap(); addSwap(phy0, phy1); + episodeSwaps.push_back({phy0, phy1}); + ++swapsSinceRouted; involvedPhy.clear(); // Update decay @@ -428,8 +823,205 @@ void SabreRouter::route(Block &block, } } LLVM_DEBUG(logger.startLine() << '\n' << logLineComment << '\n';); + return std::move(result); +} + +//===----------------------------------------------------------------------===// +// Search +//===----------------------------------------------------------------------===// + +/// Layout search strategy selected by the `search` pass option. +enum class SearchStrategy { Sabre, None }; + +/// Parse the `search` option string, or nullopt for an unknown value. +std::optional parseSearchStrategy(llvm::StringRef name) { + return llvm::StringSwitch>(name) + .Case("sabre", SearchStrategy::Sabre) + .Case("none", SearchStrategy::None) + .Default(std::nullopt); } +/// Owns the layout search. For each seed it routes forward and, under `sabre`, +/// applies the paper's forward-backward-forward reverse-traversal refinement, +/// keeping both the unrefined and refined results as candidates so refinement +/// can never select a worse layout than the seed alone. The candidate with the +/// fewest routed swaps wins, compared through `isBetter`. No IR is touched +/// here. +class RoutingSearchStrategy { + /// Reverse-traversal initial-mapping refinement from the SABRE paper (Li et + /// al. 2019, Sec. IV-C2): route forward, route backward from the resulting + /// layout, then forward again. + static constexpr unsigned numTraversals = 3; + +public: + RoutingSearchStrategy(const cudaq::Device &device, + const RoutingProblem &problem, bool refine, + unsigned extendedLayerSize, float extendedLayerWeight, + float decayDelta, unsigned roundsDecayReset) + : device(device), problem(problem), refine(refine), + extendedLayerSize(extendedLayerSize), + extendedLayerWeight(extendedLayerWeight), decayDelta(decayDelta), + roundsDecayReset(roundsDecayReset), + reverseProblem(refine ? buildReverseProblem(problem) + : RoutingProblem{}) {} + + /// The selected routing result and the final placement it produced (used for + /// the mapping_v2p attributes). + struct Selection { + RoutingResult result; + cudaq::Placement finalLayout; + }; + + /// Route every seed and return the candidate with the fewest swaps. + Selection run(ArrayRef> seeds, unsigned numV, + unsigned numPhy) { + Selection best{RoutingResult{}, cudaq::Placement(numV, numPhy)}; + bool haveBest = false; + auto consider = [&](RoutingResult result, + const cudaq::Placement &finalPlace) { + if (!haveBest || isBetter(result, best.result)) { + best.result = std::move(result); + best.finalLayout = finalPlace; + haveBest = true; + } + }; + + for (ArrayRef seed : seeds) { + cudaq::Placement finalPlace(numV, numPhy); + consider(routeSeed(seed, numV, numPhy, finalPlace), finalPlace); + if (!refine) + continue; + // Forward is done. Alternate backward/forward for the remaining + // traversals, keeping every forward result as a candidate. + SmallVector current(seed.begin(), seed.end()); + cudaq::Placement currentFinal = finalPlace; + for (unsigned t = 2; t <= numTraversals; ++t) { + if (t % 2 == 0) { + current = reverseRefine(currentFinal, numV); + } else { + cudaq::Placement nextFinal(numV, numPhy); + consider(routeSeed(current, numV, numPhy, nextFinal), nextFinal); + currentFinal = nextFinal; + } + } + } + return best; + } + +private: + /// Fewer routed swaps wins. Ties keep the earlier candidate. + static bool isBetter(const RoutingResult &a, const RoutingResult &b) { + return a.swapCount < b.swapCount; + } + + /// Forward-route a seed layout. Returns its result and final placement. + RoutingResult routeSeed(ArrayRef seed, unsigned numV, + unsigned numPhy, cudaq::Placement &finalOut) { + cudaq::Placement layout(numV, numPhy); + for (unsigned v = 0; v < numV; ++v) + layout.map(cudaq::Placement::VirtualQ(v), + cudaq::Placement::DeviceQ(seed[v])); + SabreRouter router(device, problem, layout, extendedLayerSize, + extendedLayerWeight, decayDelta, roundsDecayReset); + RoutingResult result = router.route(); + finalOut = layout; + return result; + } + + /// Route the reverse circuit from a forward pass's final mapping. The place + /// each virtual qubit lands becomes the refined seed. + SmallVector reverseRefine(const cudaq::Placement &startFinal, + unsigned numV) { + cudaq::Placement layout = startFinal; + SabreRouter router(device, reverseProblem, layout, extendedLayerSize, + extendedLayerWeight, decayDelta, roundsDecayReset); + router.route(); + SmallVector refined(numV); + for (unsigned v = 0; v < numV; ++v) + refined[v] = layout.getPhy(cudaq::Placement::VirtualQ(v)).index; + return refined; + } + + const cudaq::Device &device; + const RoutingProblem &problem; + bool refine; + unsigned extendedLayerSize; + float extendedLayerWeight; + float decayDelta; + unsigned roundsDecayReset; + RoutingProblem reverseProblem; +}; + +//===----------------------------------------------------------------------===// +// Emission +//===----------------------------------------------------------------------===// + +/// Applies a RoutingResult to the IR. This is the only place routing rewrites +/// the circuit. It rewires each mapped operation and inserts the swaps, +/// threading the current wire on each physical qubit. +class RoutingEmitter { +public: + RoutingEmitter(DenseMap &wireMap, + unsigned numPhysical) + : wireToVirtualQ(wireMap), phyToWire(numPhysical) {} + + /// Apply `result` to `block`. Returns the final wire on each physical qubit, + /// which the caller uses to create the return_wire ops. + ArrayRef emit(Block &block, + ArrayRef sources, + const RoutingResult &result) { + // Place each source wire on its physical qubit under the initial layout. + // Backends read a wire's physical qubit from its borrow identity, so the + // layout has to be written into that identity here: a non-identity layout + // is not materialized by phyToWire tracking alone. The layout is a + // permutation, so the rewritten identities stay distinct and within the + // device range. + for (auto borrowWire : sources) { + Value wire = borrowWire.getResult(); + unsigned phy = result.initialLayout[wireToVirtualQ[wire].index]; + borrowWire.setIdentity(phy); + phyToWire[phy] = wire; + } + + OpBuilder builder(&block, block.begin()); + auto wireType = builder.getType(); + for (const RoutingEvent &ev : result.trace) { + if (ev.kind == RoutingEvent::Kind::Gate) { + // Rewire the operation onto its physical qubits. + SmallVector newOpWires; + for (auto phy : ev.phys) + newOpWires.push_back(phyToWire[phy.index]); + // The operand count is unchanged, so this cannot fail. + [[maybe_unused]] LogicalResult rewired = + cudaq::quake::setQuantumOperands(ev.op, newOpWires); + assert(succeeded(rewired) && + "rewiring with a fixed operand count cannot fail"); + if (isa(ev.op)) + continue; + for (auto &&[w, q] : + llvm::zip_equal(cudaq::quake::getQuantumResults(ev.op), ev.phys)) + phyToWire[q.index] = w; + } else { + // Insert the swap and advance both wires past it. + auto q0 = ev.phys[0]; + auto q1 = ev.phys[1]; + auto swap = cudaq::quake::SwapOp::create( + builder, builder.getUnknownLoc(), TypeRange{wireType, wireType}, + false, ValueRange{}, ValueRange{}, + ValueRange{phyToWire[q0.index], phyToWire[q1.index]}, + DenseBoolArrayAttr{}); + phyToWire[q0.index] = swap.getResult(0); + phyToWire[q1.index] = swap.getResult(1); + } + } + return phyToWire; + } + +private: + DenseMap &wireToVirtualQ; + SmallVector phyToWire; +}; + std::pair> deviceFromString(llvm::StringRef deviceString) { std::size_t deviceDim[2]; @@ -711,6 +1303,50 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { SmallVector userQubitsMeasured; DenseMap finalQubitWire; Operation *lastSource = nullptr; + + // Resolve the placement and search strategies before deciding whether to + // collect interaction data. + std::optional parsedPlacement = + parsePlacementStrategy(this->placement); + if (!parsedPlacement) { + if (nonComposable) { + func.emitError("unknown qubit-mapping placement strategy '" + + this->placement + "'"); + signalPassFailure(); + return; + } + func.emitWarning("unknown qubit-mapping placement strategy '" + + this->placement + "'; using 'auto'"); + } + PlacementStrategy placementStrategy = + parsedPlacement.value_or(PlacementStrategy::Auto); + + std::optional parsedSearch = + parseSearchStrategy(this->search); + if (!parsedSearch) { + if (nonComposable) { + func.emitError("unknown qubit-mapping search strategy '" + + this->search + "'"); + signalPassFailure(); + return; + } + func.emitWarning("unknown qubit-mapping search strategy '" + + this->search + "'; using 'sabre'"); + } + SearchStrategy searchStrategy = + parsedSearch.value_or(SearchStrategy::Sabre); + + bool collectInteractions = placementStrategy != PlacementStrategy::Identity; + + // Two-qubit interaction data for placement, collected during the scan. + SmallVector> interactions; + SmallVector userVirtualQubits; + if (collectInteractions) { + interactions.assign(deviceNumQubits, + SmallVector(deviceNumQubits, 0)); + userVirtualQubits.assign(deviceNumQubits, false); + } + for (Operation &op : block.getOperations()) { if (auto qop = dyn_cast(op)) { // Assign a new virtual qubit to the resulting wire. @@ -718,6 +1354,8 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { wireToVirtualQ[qop.getResult()] = cudaq::Placement::VirtualQ(id); finalQubitWire[id] = qop.getResult(); sources[id] = qop; + if (collectInteractions) + userVirtualQubits[id] = true; lastSource = &op; } else if (dyn_cast(op)) { if (nonComposable) { @@ -778,6 +1416,18 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { for (const auto &wire : wireOperands) userQubitsMeasured.push_back(wireToVirtualQ[wire].index); + // Record two-qubit interactions for placement. + if (collectInteractions && + !isa(op) && + wireOperands.size() == 2) { + unsigned v0 = wireToVirtualQ[wireOperands[0]].index; + unsigned v1 = wireToVirtualQ[wireOperands[1]].index; + if (v0 != v1) { + interactions[v0][v1] += 1; + interactions[v1][v0] += 1; + } + } + // Map the result wires to the appropriate virtual qubits. for (auto &&[wire, newWire] : llvm::zip_equal( wireOperands, cudaq::quake::getQuantumResults(&op))) { @@ -859,15 +1509,55 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { } } - // Place - cudaq::Placement placement(sources.size(), deviceInstance->getNumQubits()); - identityPlacement(placement); + const unsigned numV = sources.size(); + const unsigned numPhy = deviceInstance->getNumQubits(); + + // Generate the seed layouts to try, in deterministic order. Each seed only + // proposes a starting vrToPhy. The router decides the rest. + SmallVector> seeds; + auto identitySeed = [&]() { + SmallVector seed(numV); + for (unsigned v = 0; v < numV; ++v) + seed[v] = v; + return seed; + }; + auto greedySeed = [&]() { + return interactionPlacement(*deviceInstance, interactions, + userVirtualQubits); + }; + switch (placementStrategy) { + case PlacementStrategy::Auto: { + seeds.push_back(identitySeed()); + // Greedy degenerates to identity when there are no interactions to place; + // routing it again would just repeat the identity pass. + SmallVector greedy = greedySeed(); + if (greedy != seeds.front()) + seeds.push_back(std::move(greedy)); + break; + } + case PlacementStrategy::Identity: + seeds.push_back(identitySeed()); + break; + case PlacementStrategy::Greedy: + seeds.push_back(greedySeed()); + break; + } - // Route - SabreRouter router(*deviceInstance, wireToVirtualQ, placement, - extendedLayerSize, extendedLayerWeight, decayDelta, - roundsDecayReset); - router.route(*blocks.begin(), sources); + // Build the routing problem once (it does not depend on the layout), then + // search over the seeds for the result with the fewest swaps. + RoutingProblem problem = + buildRoutingProblem(block, sources, wireToVirtualQ); + RoutingSearchStrategy search( + *deviceInstance, problem, searchStrategy == SearchStrategy::Sabre, + extendedLayerSize, extendedLayerWeight, decayDelta, roundsDecayReset); + RoutingSearchStrategy::Selection selection = + search.run(seeds, numV, numPhy); + RoutingResult &best = selection.result; + cudaq::Placement &bestLayout = selection.finalLayout; + + // Emit the selected result onto the IR exactly once. + RoutingEmitter emitter(wireToVirtualQ, numPhy); + auto phyToWire = emitter.emit(block, sources, best); sortTopologically(&block); // Ensure that the original measurement ordering is still honored by moving @@ -881,17 +1571,17 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { block.getOperations(), op->getIterator()); } - // Remove any unused BorrowWireOps and add ReturnWireOp's where needed - // unsigned highestMappedQubit = 0; + // Remove any unused BorrowWireOps and add ReturnWireOp's where needed. Each + // source starts on physical qubit `initialLayout[i]`, so its final wire is + // the one threaded onto that track. builder.setInsertionPoint(block.getTerminator()); - auto phyToWire = router.getPhyToWire(); for (const auto &[i, s] : llvm::enumerate(sources)) { if (s->getUsers().empty()) { s->erase(); } else { - // highestMappedQubit = i; - cudaq::quake::ReturnWireOp::create(builder, phyToWire[i].getLoc(), - phyToWire[i]); + Value finalWire = phyToWire[best.initialLayout[i]]; + cudaq::quake::ReturnWireOp::create(builder, finalWire.getLoc(), + finalWire); } } @@ -906,7 +1596,7 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { for (unsigned int v = 0; v < *highestIdentity + 1; v++) attrs[v] = IntegerAttr::get( builder.getIntegerType(64), - placement.getPhy(cudaq::Placement::VirtualQ(v)).index); + bestLayout.getPhy(cudaq::Placement::VirtualQ(v)).index); func->setAttr("mapping_v2p", builder.getArrayAttr(attrs)); @@ -923,7 +1613,7 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { measuredQubits.reserve(userQubitsMeasured.size()); for (auto mq : userQubitsMeasured) { measuredQubits.emplace_back( - mq, placement.getPhy(cudaq::Placement::VirtualQ(mq)).index); + mq, bestLayout.getPhy(cudaq::Placement::VirtualQ(mq)).index); } // First sort the pairs according to the physical qubits. llvm::sort(measuredQubits, @@ -965,6 +1655,8 @@ struct MappingPipelineOptions DECLARE_SUB_OPTION(MappingFuncOptions, extendedLayerWeight); DECLARE_SUB_OPTION(MappingFuncOptions, decayDelta); DECLARE_SUB_OPTION(MappingFuncOptions, roundsDecayReset); + DECLARE_SUB_OPTION(MappingFuncOptions, placement); + DECLARE_SUB_OPTION(MappingFuncOptions, search); PassOptions::Option nonComposable{*this, "raise-fatal-errors"}; }; @@ -992,6 +1684,8 @@ void registerMappingPipeline() { setIt(funcOpts.extendedLayerWeight, opt.extendedLayerWeight); setIt(funcOpts.decayDelta, opt.decayDelta); setIt(funcOpts.roundsDecayReset, opt.roundsDecayReset); + setIt(funcOpts.placement, opt.placement); + setIt(funcOpts.search, opt.search); setIt(funcOpts.nonComposable, opt.nonComposable); pm.addNestedPass(cudaq::opt::createMappingFunc(funcOpts)); }); diff --git a/cudaq/test/Transforms/mapping_connectivity.qke b/cudaq/test/Transforms/mapping_connectivity.qke new file mode 100644 index 00000000000..eaea8e3565d --- /dev/null +++ b/cudaq/test/Transforms/mapping_connectivity.qke @@ -0,0 +1,40 @@ +// ========================================================================== // +// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +// A non-identity initial layout must be written into the borrow-wire identities, +// because backends read a wire's physical qubit from its identity. Here greedy +// placement puts the hub-pattern's center on the physical hub of star(5,2), so +// every two-qubit gate is threaded onto hub-adjacent qubits. CircuitCheck only +// compares logical unitaries up to a permutation, so it does not catch a layout +// that fails to materialize. This test pins the physical wires directly. + +// RUN: cudaq-opt '--qubit-mapping=device=star(5,2) placement=greedy search=none' %s | FileCheck %s + +quake.wire_set @wires[2147483647] + +func.func @hub_pattern() { + %v0 = quake.borrow_wire @wires[0] : !quake.wire + %v1 = quake.borrow_wire @wires[1] : !quake.wire + %v2 = quake.borrow_wire @wires[2] : !quake.wire + %a:2 = quake.x [%v0] %v1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %b:2 = quake.x [%a#0] %v2 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %c:2 = quake.x [%a#1] %b#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + quake.return_wire %b#0 : !quake.wire + quake.return_wire %c#0 : !quake.wire + quake.return_wire %c#1 : !quake.wire + return +} + +// The center qubit is borrowed on the physical hub (identity 2) and every gate +// threads it, so each two-qubit op acts on hub-adjacent qubits. +// CHECK: %[[V0:.*]] = quake.borrow_wire @mapped_wireset[2] +// CHECK: %[[V1:.*]] = quake.borrow_wire @mapped_wireset[0] +// CHECK: %[[A:.*]]:2 = quake.x [%[[V0]]] %[[V1]] +// CHECK: %[[B:.*]]:2 = quake.x [%[[A]]#0] %{{.*}} +// CHECK: %[[SW:.*]]:2 = quake.swap %[[A]]#1, %[[B]]#0 +// CHECK: quake.x [%[[SW]]#1] %[[B]]#1 diff --git a/cudaq/test/Transforms/mapping_greedy_relocated_hub.qke b/cudaq/test/Transforms/mapping_greedy_relocated_hub.qke new file mode 100644 index 00000000000..407782ef4e8 --- /dev/null +++ b/cudaq/test/Transforms/mapping_greedy_relocated_hub.qke @@ -0,0 +1,46 @@ +// ========================================================================== // +// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +// The interaction hub is virtual q3, not q0. Greedy placement should choose the +// high-degree virtual qubit from the interaction graph rather than relying on +// virtual-qubit index order. + +// RUN: cudaq-opt '--qubit-mapping=device=star(5,2) placement=identity search=none' %s | FileCheck --check-prefix=IDENT52 %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,2) placement=greedy search=none' %s | FileCheck --check-prefix=GREEDY52 %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,0) placement=greedy search=none' %s | FileCheck --check-prefix=GREEDY50 %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,2) placement=greedy search=none' %s | CircuitCheck --up-to-mapping %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,0) placement=greedy search=none' %s | CircuitCheck --up-to-mapping %s + +quake.wire_set @wires[2147483647] + +func.func @relocated_hub() { + %v0 = quake.borrow_wire @wires[0] : !quake.wire + %v1 = quake.borrow_wire @wires[1] : !quake.wire + %v2 = quake.borrow_wire @wires[2] : !quake.wire + %v3 = quake.borrow_wire @wires[3] : !quake.wire + %v4 = quake.borrow_wire @wires[4] : !quake.wire + %a:2 = quake.x [%v3] %v0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %b:2 = quake.x [%a#0] %v1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %c:2 = quake.x [%b#0] %v2 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %d:2 = quake.x [%c#0] %v4 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + quake.return_wire %d#0 : !quake.wire + quake.return_wire %a#1 : !quake.wire + quake.return_wire %b#1 : !quake.wire + quake.return_wire %c#1 : !quake.wire + quake.return_wire %d#1 : !quake.wire + return +} + +// IDENT52-COUNT-1: quake.swap +// IDENT52-NOT: quake.swap + +// GREEDY52-LABEL: func.func @relocated_hub +// GREEDY52-NOT: quake.swap + +// GREEDY50-LABEL: func.func @relocated_hub +// GREEDY50-NOT: quake.swap diff --git a/cudaq/test/Transforms/mapping_invalid_placement.qke b/cudaq/test/Transforms/mapping_invalid_placement.qke new file mode 100644 index 00000000000..e5ab758c7f6 --- /dev/null +++ b/cudaq/test/Transforms/mapping_invalid_placement.qke @@ -0,0 +1,27 @@ +// ========================================================================== // +// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +// An unknown placement strategy is fatal under raise-fatal-errors and otherwise +// warns and falls back to the default. + +// RUN: cudaq-opt '--qubit-mapping=device=path(3) placement=bogus raise-fatal-errors=1' %s -split-input-file -verify-diagnostics +// RUN: cudaq-opt '--qubit-mapping=device=path(3) placement=bogus' %s 2>&1 | FileCheck --check-prefix=WARN %s + +quake.wire_set @wires[2147483647] + +// expected-error @+1 {{unknown qubit-mapping placement strategy 'bogus'}} +func.func @bad_placement() { + %0 = quake.borrow_wire @wires[0] : !quake.wire + %1 = quake.borrow_wire @wires[1] : !quake.wire + %2:2 = quake.x [%0] %1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + quake.return_wire %2#0 : !quake.wire + quake.return_wire %2#1 : !quake.wire + return +} + +// WARN: warning: unknown qubit-mapping placement strategy 'bogus'; using 'auto' diff --git a/cudaq/test/Transforms/mapping_invalid_search.qke b/cudaq/test/Transforms/mapping_invalid_search.qke new file mode 100644 index 00000000000..3d65be18093 --- /dev/null +++ b/cudaq/test/Transforms/mapping_invalid_search.qke @@ -0,0 +1,27 @@ +// ========================================================================== // +// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +// An unknown search strategy is fatal under raise-fatal-errors and otherwise +// warns and falls back to the default. + +// RUN: cudaq-opt '--qubit-mapping=device=path(3) search=bogus raise-fatal-errors=1' %s -split-input-file -verify-diagnostics +// RUN: cudaq-opt '--qubit-mapping=device=path(3) search=bogus' %s 2>&1 | FileCheck --check-prefix=WARN %s + +quake.wire_set @wires[2147483647] + +// expected-error @+1 {{unknown qubit-mapping search strategy 'bogus'}} +func.func @bad_search() { + %0 = quake.borrow_wire @wires[0] : !quake.wire + %1 = quake.borrow_wire @wires[1] : !quake.wire + %2:2 = quake.x [%0] %1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + quake.return_wire %2#0 : !quake.wire + quake.return_wire %2#1 : !quake.wire + return +} + +// WARN: warning: unknown qubit-mapping search strategy 'bogus'; using 'sabre' diff --git a/cudaq/test/Transforms/mapping_non_unitaries.qke b/cudaq/test/Transforms/mapping_non_unitaries.qke index 353e8599016..a21c0090a9a 100644 --- a/cudaq/test/Transforms/mapping_non_unitaries.qke +++ b/cudaq/test/Transforms/mapping_non_unitaries.qke @@ -15,7 +15,7 @@ // RUN: cudaq-opt --qubit-mapping=device=grid\(5,1\) %s | CircuitCheck --up-to-mapping %s // RUN: cudaq-opt --qubit-mapping=device=path\(5\) %s | FileCheck %s // RUN: cudaq-opt --qubit-mapping=device=ring\(5\) %s | FileCheck %s -// RUN: cudaq-opt --qubit-mapping=device=star\(5,2\) %s | FileCheck --check-prefix=STAR52 %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,2) placement=identity search=none' %s | FileCheck --check-prefix=STAR52 %s // RUN: cudaq-opt --qubit-mapping=device=star\(5,0\) %s | FileCheck --check-prefix=STAR50 %s // RUN: cudaq-opt --qubit-mapping=device=grid\(3,3\) %s | FileCheck %s // RUN: cudaq-opt --qubit-mapping=device=grid\(1,5\) %s | FileCheck %s diff --git a/cudaq/test/Transforms/mapping_phased_rx.qke b/cudaq/test/Transforms/mapping_phased_rx.qke index d087099111d..489ed86eef6 100644 --- a/cudaq/test/Transforms/mapping_phased_rx.qke +++ b/cudaq/test/Transforms/mapping_phased_rx.qke @@ -6,7 +6,7 @@ // the terms of the Apache License 2.0 which accompanies this distribution. // // ========================================================================== // -// RUN: cudaq-opt '--qubit-mapping=device=star(5,2)' %s | FileCheck %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,2) placement=identity search=none' %s | FileCheck %s module { quake.wire_set @wires[2147483647] diff --git a/cudaq/test/Transforms/mapping_placement_seeds.qke b/cudaq/test/Transforms/mapping_placement_seeds.qke new file mode 100644 index 00000000000..c7db56a90fe --- /dev/null +++ b/cudaq/test/Transforms/mapping_placement_seeds.qke @@ -0,0 +1,40 @@ +// ========================================================================== // +// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +// Placement seeds and selection. Identity and greedy are proposed as starting +// layouts, and the default `auto` keeps whichever seed routes with fewer swaps. + +// RUN: cudaq-opt '--qubit-mapping=device=star(5,2) placement=identity search=none' %s | FileCheck --check-prefix=IDENT52 %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,2) placement=greedy search=none' %s | FileCheck --check-prefix=GREEDY52 %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,2) search=none' %s | FileCheck --check-prefix=GREEDY52 %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,0) placement=greedy search=none' %s | FileCheck --check-prefix=GREEDY50 %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,2) search=none' %s | CircuitCheck --up-to-mapping %s + +quake.wire_set @wires[2147483647] + +func.func @hub_pattern() { + %v0 = quake.borrow_wire @wires[0] : !quake.wire + %v1 = quake.borrow_wire @wires[1] : !quake.wire + %v2 = quake.borrow_wire @wires[2] : !quake.wire + %a:2 = quake.x [%v0] %v1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %b:2 = quake.x [%a#0] %v2 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %c:2 = quake.x [%a#1] %b#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + quake.return_wire %b#0 : !quake.wire + quake.return_wire %c#0 : !quake.wire + quake.return_wire %c#1 : !quake.wire + return +} + +// IDENT52-COUNT-2: quake.swap +// IDENT52-NOT: quake.swap + +// GREEDY52-COUNT-1: quake.swap +// GREEDY52-NOT: quake.swap + +// GREEDY50-COUNT-1: quake.swap +// GREEDY50-NOT: quake.swap diff --git a/cudaq/test/Transforms/mapping_qspan.qke b/cudaq/test/Transforms/mapping_qspan.qke index 91e1ff3cd4f..6ceff4096f6 100644 --- a/cudaq/test/Transforms/mapping_qspan.qke +++ b/cudaq/test/Transforms/mapping_qspan.qke @@ -6,12 +6,12 @@ // the terms of the Apache License 2.0 which accompanies this distribution. // // ========================================================================== // -// RUN: cudaq-opt --qubit-mapping=device=path\(5\) --delay-measurements %s | FileCheck %s -// RUN: cudaq-opt --qubit-mapping=device=ring\(5\) --delay-measurements %s | FileCheck --check-prefix RING %s -// RUN: cudaq-opt --qubit-mapping=device=grid\(3,3\) --delay-measurements %s | FileCheck --check-prefix GRID %s -// RUN: cudaq-opt --qubit-mapping=device=star\(5\) --delay-measurements %s | FileCheck --check-prefix STAR50 %s -// RUN: cudaq-opt --qubit-mapping=device=star\(5,0\) --delay-measurements %s | FileCheck --check-prefix STAR50 %s -// RUN: cudaq-opt --qubit-mapping=device=star\(5,2\) --delay-measurements %s | FileCheck --check-prefix STAR52 %s +// RUN: cudaq-opt '--qubit-mapping=device=path(5) placement=identity search=none' --delay-measurements %s | FileCheck %s +// RUN: cudaq-opt '--qubit-mapping=device=ring(5) search=none' --delay-measurements %s | FileCheck --check-prefix RING %s +// RUN: cudaq-opt '--qubit-mapping=device=grid(3,3) placement=identity search=none' --delay-measurements %s | FileCheck --check-prefix GRID %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5) search=none' --delay-measurements %s | FileCheck --check-prefix STAR50 %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,0) search=none' --delay-measurements %s | FileCheck --check-prefix STAR50 %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,2) placement=identity search=none' --delay-measurements %s | FileCheck --check-prefix STAR52 %s module { quake.wire_set @wires[2147483647] diff --git a/cudaq/test/Transforms/mapping_search_late_interactions.qke b/cudaq/test/Transforms/mapping_search_late_interactions.qke new file mode 100644 index 00000000000..040e7045d4f --- /dev/null +++ b/cudaq/test/Transforms/mapping_search_late_interactions.qke @@ -0,0 +1,48 @@ +// ========================================================================== // +// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +// An early cluster (q1-q2-q3) and a heavy late pair (q0-q4) pull a fixed seed in +// different directions. The default search routes both seeds, refines them, and +// keeps the best, so its swap count is at most either fixed seed's. Here it +// reaches a swap-free layout. + +// RUN: cudaq-opt '--qubit-mapping=device=path(5) placement=identity search=none' %s | FileCheck --check-prefix=IDENT %s +// RUN: cudaq-opt '--qubit-mapping=device=path(5) placement=greedy search=none' %s | FileCheck --check-prefix=GREEDY %s +// RUN: cudaq-opt '--qubit-mapping=device=path(5)' %s | FileCheck --check-prefix=DEFAULT %s +// RUN: cudaq-opt '--qubit-mapping=device=path(5)' %s | CircuitCheck --up-to-mapping %s + +quake.wire_set @wires[2147483647] + +func.func @late_interactions() { + %v0 = quake.borrow_wire @wires[0] : !quake.wire + %v1 = quake.borrow_wire @wires[1] : !quake.wire + %v2 = quake.borrow_wire @wires[2] : !quake.wire + %v3 = quake.borrow_wire @wires[3] : !quake.wire + %v4 = quake.borrow_wire @wires[4] : !quake.wire + // Early cluster among q1, q2, q3. + %f:2 = quake.x [%v1] %v2 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g:2 = quake.x [%f#1] %v3 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + // Heavy late interaction between the far-apart q0 and q4. + %c:2 = quake.x [%v0] %v4 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %d:2 = quake.x [%c#0] %c#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %e:2 = quake.x [%d#0] %d#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + quake.return_wire %f#0 : !quake.wire + quake.return_wire %g#0 : !quake.wire + quake.return_wire %g#1 : !quake.wire + quake.return_wire %e#0 : !quake.wire + quake.return_wire %e#1 : !quake.wire + return +} + +// IDENT-COUNT-3: quake.swap +// IDENT-NOT: quake.swap + +// GREEDY-COUNT-2: quake.swap +// GREEDY-NOT: quake.swap + +// DEFAULT-NOT: quake.swap diff --git a/cudaq/test/Transforms/mapping_search_none.qke b/cudaq/test/Transforms/mapping_search_none.qke new file mode 100644 index 00000000000..87a15e3e957 --- /dev/null +++ b/cudaq/test/Transforms/mapping_search_none.qke @@ -0,0 +1,33 @@ +// ========================================================================== // +// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +// Option matrix with refinement off. `placement=auto search=none` routes both +// seeds once and keeps the better, while `placement=greedy search=none` routes +// the greedy seed alone. For this gate-order circuit the greedy seed is worse, +// so the two configurations select different layouts. + +// RUN: cudaq-opt '--qubit-mapping=device=star(5,2) placement=auto search=none' %s | FileCheck --check-prefix=AUTO %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,2) placement=greedy search=none' %s | FileCheck --check-prefix=GREEDY %s + +quake.wire_set @wires[2147483647] + +func.func @gate_order() { + %v0 = quake.borrow_wire @wires[0] : !quake.wire + %v1 = quake.borrow_wire @wires[1] : !quake.wire + %v2 = quake.borrow_wire @wires[2] : !quake.wire + %a:2 = quake.x [%v1] %v2 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %b:2 = quake.x [%v0] %a#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %c:2 = quake.x [%b#0] %a#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + quake.return_wire %b#1 : !quake.wire + quake.return_wire %c#0 : !quake.wire + quake.return_wire %c#1 : !quake.wire + return +} + +// AUTO: mapping_v2p = [2, 1, 0] +// GREEDY: mapping_v2p = [2, 0, 1] diff --git a/cudaq/test/Transforms/mapping_search_refinement.qke b/cudaq/test/Transforms/mapping_search_refinement.qke new file mode 100644 index 00000000000..483261c4fc6 --- /dev/null +++ b/cudaq/test/Transforms/mapping_search_refinement.qke @@ -0,0 +1,42 @@ +// ========================================================================== // +// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +// A fan centered on q0 placed at the end of a path routes poorly from the +// identity seed. The backward pass lands q0 near the middle, so the refined +// forward pass needs fewer swaps than the single forward pass from the same +// seed. + +// RUN: cudaq-opt '--qubit-mapping=device=path(5) placement=identity search=none' %s | FileCheck --check-prefix=NONE %s +// RUN: cudaq-opt '--qubit-mapping=device=path(5) placement=identity search=sabre' %s | FileCheck --check-prefix=SABRE %s +// RUN: cudaq-opt '--qubit-mapping=device=path(5) placement=identity search=sabre' %s | CircuitCheck --up-to-mapping %s + +quake.wire_set @wires[2147483647] + +func.func @fan_on_path() { + %v0 = quake.borrow_wire @wires[0] : !quake.wire + %v1 = quake.borrow_wire @wires[1] : !quake.wire + %v2 = quake.borrow_wire @wires[2] : !quake.wire + %v3 = quake.borrow_wire @wires[3] : !quake.wire + %v4 = quake.borrow_wire @wires[4] : !quake.wire + %a:2 = quake.x [%v0] %v1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %b:2 = quake.x [%a#0] %v2 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %c:2 = quake.x [%b#0] %v3 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %d:2 = quake.x [%c#0] %v4 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + quake.return_wire %d#0 : !quake.wire + quake.return_wire %a#1 : !quake.wire + quake.return_wire %b#1 : !quake.wire + quake.return_wire %c#1 : !quake.wire + quake.return_wire %d#1 : !quake.wire + return +} + +// NONE-COUNT-3: quake.swap +// NONE-NOT: quake.swap + +// SABRE-COUNT-2: quake.swap +// SABRE-NOT: quake.swap diff --git a/cudaq/test/Transforms/mapping_search_star.qke b/cudaq/test/Transforms/mapping_search_star.qke new file mode 100644 index 00000000000..d93c7ea0717 --- /dev/null +++ b/cudaq/test/Transforms/mapping_search_star.qke @@ -0,0 +1,48 @@ +// ========================================================================== // +// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +// The issue (#4289): identity placement ties the routed swap count to the +// physical numbering of an isomorphic device. The default search removes that +// dependence by selecting the seed that routes best. + +// RUN: cudaq-opt '--qubit-mapping=device=star(5,0) placement=identity search=none' %s | FileCheck --check-prefix=IDENT50 %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,2) placement=identity search=none' %s | FileCheck --check-prefix=IDENT52 %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,0)' %s | FileCheck --check-prefix=DEFAULT50 %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,2)' %s | FileCheck --check-prefix=DEFAULT52 %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,0)' %s | CircuitCheck --up-to-mapping %s +// RUN: cudaq-opt '--qubit-mapping=device=star(5,2)' %s | CircuitCheck --up-to-mapping %s + +quake.wire_set @wires[2147483647] + +func.func @fan() { + %v0 = quake.borrow_wire @wires[0] : !quake.wire + %v1 = quake.borrow_wire @wires[1] : !quake.wire + %v2 = quake.borrow_wire @wires[2] : !quake.wire + %v3 = quake.borrow_wire @wires[3] : !quake.wire + %v4 = quake.borrow_wire @wires[4] : !quake.wire + %a:2 = quake.x [%v0] %v1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %b:2 = quake.x [%a#0] %v2 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %c:2 = quake.x [%b#0] %v3 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %d:2 = quake.x [%c#0] %v4 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + quake.return_wire %d#0 : !quake.wire + quake.return_wire %a#1 : !quake.wire + quake.return_wire %b#1 : !quake.wire + quake.return_wire %c#1 : !quake.wire + quake.return_wire %d#1 : !quake.wire + return +} + +// Identity routes the hub-numbering for free but pays for the other numbering. +// IDENT50-NOT: quake.swap + +// IDENT52-COUNT-1: quake.swap +// IDENT52-NOT: quake.swap + +// The default search routes both numberings identically. +// DEFAULT50-NOT: quake.swap +// DEFAULT52-NOT: quake.swap diff --git a/cudaq/test/Transforms/mapping_search_termination.qke b/cudaq/test/Transforms/mapping_search_termination.qke new file mode 100644 index 00000000000..3f66685ccba --- /dev/null +++ b/cudaq/test/Transforms/mapping_search_termination.qke @@ -0,0 +1,263 @@ +// ========================================================================== // +// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +// A dense quantum-volume-style circuit on a 20-qubit path. The reverse-traversal +// refinement can hand the router a layout where the SABRE heuristic stalls in a +// local minimum. Without the release valve the search loops forever on this +// input. The check is simply that mapping terminates and emits a mapped kernel. +// CircuitCheck is omitted: equivalence checking builds the 2^20 unitary. + +// RUN: cudaq-opt '--qubit-mapping=device=path(20) placement=auto search=sabre' %s | FileCheck %s + +quake.wire_set @wires[2147483647] +func.func @qv_path20() { + %v0 = quake.borrow_wire @wires[0] : !quake.wire + %v1 = quake.borrow_wire @wires[1] : !quake.wire + %v2 = quake.borrow_wire @wires[2] : !quake.wire + %v3 = quake.borrow_wire @wires[3] : !quake.wire + %v4 = quake.borrow_wire @wires[4] : !quake.wire + %v5 = quake.borrow_wire @wires[5] : !quake.wire + %v6 = quake.borrow_wire @wires[6] : !quake.wire + %v7 = quake.borrow_wire @wires[7] : !quake.wire + %v8 = quake.borrow_wire @wires[8] : !quake.wire + %v9 = quake.borrow_wire @wires[9] : !quake.wire + %v10 = quake.borrow_wire @wires[10] : !quake.wire + %v11 = quake.borrow_wire @wires[11] : !quake.wire + %v12 = quake.borrow_wire @wires[12] : !quake.wire + %v13 = quake.borrow_wire @wires[13] : !quake.wire + %v14 = quake.borrow_wire @wires[14] : !quake.wire + %v15 = quake.borrow_wire @wires[15] : !quake.wire + %v16 = quake.borrow_wire @wires[16] : !quake.wire + %v17 = quake.borrow_wire @wires[17] : !quake.wire + %v18 = quake.borrow_wire @wires[18] : !quake.wire + %v19 = quake.borrow_wire @wires[19] : !quake.wire + %g0:2 = quake.x [%v10] %v18 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g1:2 = quake.x [%v16] %v14 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g2:2 = quake.x [%v0] %v17 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g3:2 = quake.x [%v11] %v2 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g4:2 = quake.x [%v3] %v9 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g5:2 = quake.x [%v5] %v7 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g6:2 = quake.x [%v4] %v19 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g7:2 = quake.x [%v6] %v15 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g8:2 = quake.x [%v8] %v1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g9:2 = quake.x [%v13] %v12 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g10:2 = quake.x [%g9#0] %g3#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g11:2 = quake.x [%g1#0] %g2#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g12:2 = quake.x [%g2#1] %g6#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g13:2 = quake.x [%g1#1] %g5#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g14:2 = quake.x [%g0#1] %g6#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g15:2 = quake.x [%g9#1] %g7#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g16:2 = quake.x [%g5#0] %g8#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g17:2 = quake.x [%g8#0] %g7#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g18:2 = quake.x [%g0#0] %g3#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g19:2 = quake.x [%g4#0] %g4#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g20:2 = quake.x [%g14#0] %g14#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g21:2 = quake.x [%g12#0] %g13#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g22:2 = quake.x [%g12#1] %g11#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g23:2 = quake.x [%g15#1] %g17#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g24:2 = quake.x [%g19#1] %g13#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g25:2 = quake.x [%g16#1] %g10#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g26:2 = quake.x [%g16#0] %g10#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g27:2 = quake.x [%g19#0] %g18#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g28:2 = quake.x [%g17#1] %g11#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g29:2 = quake.x [%g15#0] %g18#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g30:2 = quake.x [%g21#1] %g28#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g31:2 = quake.x [%g29#0] %g29#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g32:2 = quake.x [%g21#0] %g25#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g33:2 = quake.x [%g23#0] %g20#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g34:2 = quake.x [%g20#1] %g26#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g35:2 = quake.x [%g24#1] %g25#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g36:2 = quake.x [%g26#1] %g22#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g37:2 = quake.x [%g23#1] %g24#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g38:2 = quake.x [%g27#0] %g28#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g39:2 = quake.x [%g22#1] %g27#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g40:2 = quake.x [%g32#1] %g33#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g41:2 = quake.x [%g33#0] %g32#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g42:2 = quake.x [%g37#0] %g34#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g43:2 = quake.x [%g36#0] %g35#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g44:2 = quake.x [%g39#0] %g36#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g45:2 = quake.x [%g38#1] %g30#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g46:2 = quake.x [%g35#0] %g38#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g47:2 = quake.x [%g31#1] %g37#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g48:2 = quake.x [%g30#0] %g39#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g49:2 = quake.x [%g31#0] %g34#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g50:2 = quake.x [%g49#1] %g40#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g51:2 = quake.x [%g43#1] %g45#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g52:2 = quake.x [%g47#0] %g42#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g53:2 = quake.x [%g46#0] %g45#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g54:2 = quake.x [%g44#1] %g43#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g55:2 = quake.x [%g41#0] %g47#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g56:2 = quake.x [%g48#1] %g46#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g57:2 = quake.x [%g40#0] %g48#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g58:2 = quake.x [%g42#0] %g44#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g59:2 = quake.x [%g49#0] %g41#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g60:2 = quake.x [%g58#1] %g59#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g61:2 = quake.x [%g58#0] %g57#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g62:2 = quake.x [%g50#0] %g53#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g63:2 = quake.x [%g59#1] %g55#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g64:2 = quake.x [%g52#0] %g52#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g65:2 = quake.x [%g56#1] %g51#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g66:2 = quake.x [%g54#0] %g54#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g67:2 = quake.x [%g51#1] %g57#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g68:2 = quake.x [%g55#0] %g56#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g69:2 = quake.x [%g50#1] %g53#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g70:2 = quake.x [%g67#1] %g61#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g71:2 = quake.x [%g60#1] %g66#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g72:2 = quake.x [%g64#1] %g69#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g73:2 = quake.x [%g60#0] %g61#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g74:2 = quake.x [%g68#0] %g62#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g75:2 = quake.x [%g66#1] %g63#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g76:2 = quake.x [%g65#0] %g68#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g77:2 = quake.x [%g62#0] %g67#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g78:2 = quake.x [%g64#0] %g63#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g79:2 = quake.x [%g69#0] %g65#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g80:2 = quake.x [%g70#0] %g76#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g81:2 = quake.x [%g79#0] %g74#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g82:2 = quake.x [%g73#1] %g78#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g83:2 = quake.x [%g72#0] %g71#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g84:2 = quake.x [%g77#0] %g75#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g85:2 = quake.x [%g71#1] %g73#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g86:2 = quake.x [%g75#1] %g78#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g87:2 = quake.x [%g70#1] %g77#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g88:2 = quake.x [%g79#1] %g74#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g89:2 = quake.x [%g72#1] %g76#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g90:2 = quake.x [%g89#0] %g87#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g91:2 = quake.x [%g80#1] %g82#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g92:2 = quake.x [%g86#1] %g87#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g93:2 = quake.x [%g88#1] %g81#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g94:2 = quake.x [%g82#1] %g86#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g95:2 = quake.x [%g80#0] %g84#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g96:2 = quake.x [%g83#0] %g85#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g97:2 = quake.x [%g84#0] %g83#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g98:2 = quake.x [%g89#1] %g88#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g99:2 = quake.x [%g81#1] %g85#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g100:2 = quake.x [%g95#1] %g97#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g101:2 = quake.x [%g94#0] %g97#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g102:2 = quake.x [%g99#1] %g95#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g103:2 = quake.x [%g98#1] %g93#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g104:2 = quake.x [%g91#1] %g92#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g105:2 = quake.x [%g96#1] %g90#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g106:2 = quake.x [%g94#1] %g93#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g107:2 = quake.x [%g91#0] %g90#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g108:2 = quake.x [%g92#1] %g96#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g109:2 = quake.x [%g99#0] %g98#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g110:2 = quake.x [%g103#1] %g108#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g111:2 = quake.x [%g106#1] %g101#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g112:2 = quake.x [%g109#0] %g104#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g113:2 = quake.x [%g102#0] %g101#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g114:2 = quake.x [%g100#0] %g107#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g115:2 = quake.x [%g106#0] %g108#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g116:2 = quake.x [%g102#1] %g100#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g117:2 = quake.x [%g109#1] %g105#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g118:2 = quake.x [%g105#1] %g104#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g119:2 = quake.x [%g103#0] %g107#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g120:2 = quake.x [%g119#0] %g116#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g121:2 = quake.x [%g113#1] %g118#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g122:2 = quake.x [%g117#1] %g113#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g123:2 = quake.x [%g112#0] %g111#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g124:2 = quake.x [%g114#0] %g115#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g125:2 = quake.x [%g112#1] %g110#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g126:2 = quake.x [%g114#1] %g118#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g127:2 = quake.x [%g117#0] %g116#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g128:2 = quake.x [%g115#1] %g119#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g129:2 = quake.x [%g111#0] %g110#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g130:2 = quake.x [%g126#0] %g122#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g131:2 = quake.x [%g120#0] %g124#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g132:2 = quake.x [%g127#0] %g129#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g133:2 = quake.x [%g126#1] %g120#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g134:2 = quake.x [%g128#1] %g129#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g135:2 = quake.x [%g124#1] %g125#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g136:2 = quake.x [%g125#1] %g122#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g137:2 = quake.x [%g127#1] %g123#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g138:2 = quake.x [%g121#1] %g121#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g139:2 = quake.x [%g123#0] %g128#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g140:2 = quake.x [%g130#0] %g136#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g141:2 = quake.x [%g139#0] %g135#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g142:2 = quake.x [%g137#1] %g138#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g143:2 = quake.x [%g137#0] %g131#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g144:2 = quake.x [%g135#0] %g131#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g145:2 = quake.x [%g132#0] %g136#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g146:2 = quake.x [%g139#1] %g132#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g147:2 = quake.x [%g134#0] %g133#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g148:2 = quake.x [%g134#1] %g130#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g149:2 = quake.x [%g138#1] %g133#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g150:2 = quake.x [%g145#1] %g147#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g151:2 = quake.x [%g140#1] %g146#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g152:2 = quake.x [%g144#0] %g144#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g153:2 = quake.x [%g141#0] %g142#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g154:2 = quake.x [%g142#1] %g149#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g155:2 = quake.x [%g141#1] %g143#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g156:2 = quake.x [%g143#0] %g148#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g157:2 = quake.x [%g148#0] %g149#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g158:2 = quake.x [%g147#1] %g140#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g159:2 = quake.x [%g145#0] %g146#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g160:2 = quake.x [%g158#0] %g151#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g161:2 = quake.x [%g159#0] %g159#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g162:2 = quake.x [%g155#1] %g156#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g163:2 = quake.x [%g157#1] %g153#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g164:2 = quake.x [%g150#0] %g156#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g165:2 = quake.x [%g153#0] %g158#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g166:2 = quake.x [%g157#0] %g155#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g167:2 = quake.x [%g152#0] %g152#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g168:2 = quake.x [%g150#1] %g154#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g169:2 = quake.x [%g154#0] %g151#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g170:2 = quake.x [%g164#1] %g161#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g171:2 = quake.x [%g168#1] %g169#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g172:2 = quake.x [%g167#0] %g165#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g173:2 = quake.x [%g163#0] %g164#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g174:2 = quake.x [%g163#1] %g166#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g175:2 = quake.x [%g162#0] %g167#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g176:2 = quake.x [%g165#0] %g160#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g177:2 = quake.x [%g161#1] %g168#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g178:2 = quake.x [%g169#0] %g160#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g179:2 = quake.x [%g162#1] %g166#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g180:2 = quake.x [%g174#0] %g170#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g181:2 = quake.x [%g176#1] %g171#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g182:2 = quake.x [%g177#0] %g177#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g183:2 = quake.x [%g174#1] %g176#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g184:2 = quake.x [%g175#0] %g178#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g185:2 = quake.x [%g173#0] %g178#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g186:2 = quake.x [%g179#1] %g173#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g187:2 = quake.x [%g175#1] %g172#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g188:2 = quake.x [%g170#1] %g172#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g189:2 = quake.x [%g179#0] %g171#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g190:2 = quake.x [%g184#1] %g188#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g191:2 = quake.x [%g188#1] %g180#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g192:2 = quake.x [%g181#0] %g182#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g193:2 = quake.x [%g185#0] %g189#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g194:2 = quake.x [%g186#0] %g181#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g195:2 = quake.x [%g183#0] %g187#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g196:2 = quake.x [%g183#1] %g180#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g197:2 = quake.x [%g184#0] %g186#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g198:2 = quake.x [%g182#1] %g185#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g199:2 = quake.x [%g187#1] %g189#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + quake.return_wire %g194#0 : !quake.wire + quake.return_wire %g197#0 : !quake.wire + quake.return_wire %g193#0 : !quake.wire + quake.return_wire %g190#1 : !quake.wire + quake.return_wire %g193#1 : !quake.wire + quake.return_wire %g192#1 : !quake.wire + quake.return_wire %g194#1 : !quake.wire + quake.return_wire %g196#1 : !quake.wire + quake.return_wire %g190#0 : !quake.wire + quake.return_wire %g191#0 : !quake.wire + quake.return_wire %g198#0 : !quake.wire + quake.return_wire %g195#1 : !quake.wire + quake.return_wire %g192#0 : !quake.wire + quake.return_wire %g195#0 : !quake.wire + quake.return_wire %g196#0 : !quake.wire + quake.return_wire %g199#0 : !quake.wire + quake.return_wire %g197#1 : !quake.wire + quake.return_wire %g199#1 : !quake.wire + quake.return_wire %g198#1 : !quake.wire + quake.return_wire %g191#1 : !quake.wire + return +} + +// CHECK: func.func @qv_path20() attributes +// CHECK-SAME: mapping_v2p diff --git a/cudaq/test/Transforms/mapping_unitaries.qke b/cudaq/test/Transforms/mapping_unitaries.qke index 66c1e046adc..fcc28569618 100644 --- a/cudaq/test/Transforms/mapping_unitaries.qke +++ b/cudaq/test/Transforms/mapping_unitaries.qke @@ -6,7 +6,7 @@ // the terms of the Apache License 2.0 which accompanies this distribution. // // ========================================================================== // -// RUN: cudaq-opt --qubit-mapping=device=path\(10\) %s | CircuitCheck --up-to-mapping %s +// RUN: cudaq-opt '--qubit-mapping=device=path(10) placement=identity' %s | CircuitCheck --up-to-mapping %s quake.wire_set @wires[2147483647] diff --git a/cudaq/test/Transforms/mapping_with_meas-1.qke b/cudaq/test/Transforms/mapping_with_meas-1.qke index 74687c4ce14..8a568f5a89a 100644 --- a/cudaq/test/Transforms/mapping_with_meas-1.qke +++ b/cudaq/test/Transforms/mapping_with_meas-1.qke @@ -6,7 +6,7 @@ // the terms of the Apache License 2.0 which accompanies this distribution. // // ========================================================================== // -// RUN: cudaq-opt --qubit-mapping=device=path\(3\) %s | FileCheck %s +// RUN: cudaq-opt '--qubit-mapping=device=path(3) placement=identity search=none' %s | FileCheck %s module { quake.wire_set @wires[2147483647] func.func @__nvqpp__mlirgen__function_foo._Z3foov() attributes {"cudaq-entrypoint", "cudaq-kernel", no_this} { From 1e68cbd558bee780534428d69064e8dac2c0a6f6 Mon Sep 17 00:00:00 2001 From: Seemanta Bhattacharjee Date: Thu, 18 Jun 2026 01:11:45 +0600 Subject: [PATCH 2/7] refactor placement/routing, guard mid-circuit measurements Signed-off-by: Seemanta Bhattacharjee --- .../cudaq/Optimizer/Transforms/Passes.td | 10 + cudaq/lib/Optimizer/Transforms/Mapping.cpp | 727 ++++++++++++------ 2 files changed, 521 insertions(+), 216 deletions(-) diff --git a/cudaq/include/cudaq/Optimizer/Transforms/Passes.td b/cudaq/include/cudaq/Optimizer/Transforms/Passes.td index bd41bd2e055..cc75b3a6cd8 100644 --- a/cudaq/include/cudaq/Optimizer/Transforms/Passes.td +++ b/cudaq/include/cudaq/Optimizer/Transforms/Passes.td @@ -956,6 +956,16 @@ def MappingFunc: Pass<"qubit-mapping-func", "mlir::func::FuncOp"> { "Decay delta">, Option<"roundsDecayReset", "roundsDecayReset", "unsigned", /*default=*/"5", "Number of rounds before decay is reset">, + Option<"minStallSwapBudget", "minStallSwapBudget", "unsigned", + /*default=*/"64", + "Release valve floor: minimum number of consecutive swaps that route " + "no gate before a stuck front layer is force-routed along a shortest " + "path (advanced)">, + Option<"stallSwapBudgetPerQubit", "stallSwapBudgetPerQubit", "unsigned", + /*default=*/"4", + "Release valve scaling: per-device-qubit stall budget. The budget " + "used is max(minStallSwapBudget, stallSwapBudgetPerQubit * numQubits) " + "(advanced)">, Option<"placement", "placement", "std::string", /*default=*/"\"auto\"", "Initial placement strategy: auto (propose identity and greedy " diff --git a/cudaq/lib/Optimizer/Transforms/Mapping.cpp b/cudaq/lib/Optimizer/Transforms/Mapping.cpp index 8dcf84beb09..29aba6295c8 100644 --- a/cudaq/lib/Optimizer/Transforms/Mapping.cpp +++ b/cudaq/lib/Optimizer/Transforms/Mapping.cpp @@ -11,12 +11,15 @@ #include "cudaq/Support/Device.h" #include "cudaq/Support/Handle.h" #include "cudaq/Support/Placement.h" +#include "llvm/ADT/SmallPtrSet.h" #include "llvm/ADT/SmallSet.h" #include "llvm/ADT/StringSwitch.h" +#include "llvm/Support/ErrorHandling.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/ScopedPrinter.h" #include "mlir/Analysis/TopologicalSortUtils.h" #include "mlir/Dialect/Func/IR/FuncOps.h" +#include "mlir/Interfaces/CallInterfaces.h" namespace cudaq::opt { #define GEN_PASS_DEF_MAPPINGFUNC @@ -48,6 +51,30 @@ std::optional parsePlacementStrategy(llvm::StringRef name) { .Default(std::nullopt); } +/// A symmetric weighted interaction graph over virtual qubits. Each entry +/// counts the two-qubit gates acting on a pair of virtual qubits. It is stored +/// as a dense `n x n` matrix so the placer can query any pair in O(1). +class VirtualInteractionGraph { +public: + explicit VirtualInteractionGraph(unsigned numQubits) + : counts(numQubits, SmallVector(numQubits, 0)) {} + + /// Record one two-qubit interaction between virtual qubits `v0` and `v1`. + /// Self-interactions are ignored. + void addInteraction(unsigned v0, unsigned v1) { + if (v0 == v1) + return; + ++counts[v0][v1]; + ++counts[v1][v0]; + } + + /// The number of recorded interactions between virtual qubits `u` and `v`. + unsigned count(unsigned u, unsigned v) const { return counts[u][v]; } + +private: + SmallVector> counts; +}; + /// Builds a deterministic topology-aware initial layout by assigning highly /// interacting virtual qubits to central physical qubits first. The greedy /// growth and its tie-breaks make the layout a deterministic function of the @@ -55,11 +82,11 @@ std::optional parsePlacementStrategy(llvm::StringRef name) { class GreedyInitialPlacer { public: GreedyInitialPlacer(const cudaq::Device &device, - ArrayRef> interactions, + const VirtualInteractionGraph &interactions, ArrayRef userVirtualQubits) : device(device), interactions(interactions), userVirtualQubits(userVirtualQubits), n(device.getNumQubits()), - vrToPhy(n, 0), placedVirtual(n, false), usedPhysical(n, false) {} + vrToPhy(n, 0), placedVirtual(n, false) {} /// Produce the `vrToPhy` seed layout. SmallVector run() { @@ -74,19 +101,15 @@ class GreedyInitialPlacer { } computeCentrality(); + initWorklists(); // Seed the highest-degree virtual qubit onto the most central physical // qubit, then grow the layout around it. place(chooseSeedVirtual(), bestFreePhysical()); - unsigned remaining = 0; - for (unsigned u = 0; u < n; ++u) - if (userVirtualQubits[u] && !placedVirtual[u]) - ++remaining; - while (remaining > 0) { + while (!unplacedUserVirtuals.empty()) { unsigned v = chooseNextVirtual(); place(v, bestPhysicalFor(v)); - --remaining; } assignRemainingVirtuals(); @@ -100,7 +123,7 @@ class GreedyInitialPlacer { weightedDegree.assign(n, 0); for (unsigned u = 0; u < n; ++u) { for (unsigned v = 0; v < n; ++v) - weightedDegree[u] += interactions[u][v]; + weightedDegree[u] += interactions.count(u, v); if (weightedDegree[u] > 0) hasInteraction = true; } @@ -120,9 +143,22 @@ class GreedyInitialPlacer { } } - /// More central first: lower distance-sum, then higher degree, then lower - /// index. - bool physBetter(unsigned a, unsigned b) const { + /// Seed the worklists for the placement walk: every physical is free and + /// every user virtual is unplaced, both in ascending order. + void initWorklists() { + freePhysicals.reserve(n); + for (unsigned p = 0; p < n; ++p) + freePhysicals.push_back(p); + for (unsigned u = 0; u < n; ++u) + if (userVirtualQubits[u]) + unplacedUserVirtuals.push_back(u); + } + + /// True when physical qubit `a` is more central than `b` and should be + /// preferred. The primary key is the total distance to every other qubit + /// (smaller is more central). Ties break toward the higher connectivity + /// degree, then toward the lower index so the layout stays deterministic. + bool isMoreCentralPhysical(unsigned a, unsigned b) const { if (distanceSum[a] != distanceSum[b]) return distanceSum[a] < distanceSum[b]; if (physDegree[a] != physDegree[b]) @@ -133,8 +169,8 @@ class GreedyInitialPlacer { /// The most central physical qubit not yet used. unsigned bestFreePhysical() const { unsigned best = n; - for (unsigned p = 0; p < n; ++p) - if (!usedPhysical[p] && (best == n || physBetter(p, best))) + for (unsigned p : freePhysicals) + if (best == n || isMoreCentralPhysical(p, best)) best = p; return best; } @@ -143,61 +179,56 @@ class GreedyInitialPlacer { /// index. unsigned chooseSeedVirtual() const { unsigned seed = n; - for (unsigned u = 0; u < n; ++u) - if (userVirtualQubits[u] && - (seed == n || weightedDegree[u] > weightedDegree[seed])) + for (unsigned u : unplacedUserVirtuals) + if (seed == n || weightedDegree[u] > weightedDegree[seed]) seed = u; return seed; } - /// The unplaced virtual qubit most connected to the placed ones, falling back - /// to weighted degree for disconnected components. + /// The unplaced virtual qubit most connected to the placed set. Ties break by + /// total weighted degree, then by lower virtual index for determinism. The + /// disconnected case (no interaction with the placed set) reduces to highest + /// weighted degree, then lower index, since weighted degree is always at least + /// the placed weight. unsigned chooseNextVirtual() const { unsigned pick = n; - unsigned pickWeight = 0; - for (unsigned u = 0; u < n; ++u) { - if (!userVirtualQubits[u] || placedVirtual[u]) - continue; + unsigned pickPlacedWeight = 0; + unsigned pickDegree = 0; + for (unsigned u : unplacedUserVirtuals) { unsigned placedWeight = 0; - for (unsigned v = 0; v < n; ++v) - if (placedVirtual[v]) - placedWeight += interactions[u][v]; - if (pick == n || placedWeight > pickWeight) { + for (unsigned v : placedVirtuals) + placedWeight += interactions.count(u, v); + unsigned degree = weightedDegree[u]; + bool better = pick == n || placedWeight > pickPlacedWeight || + (placedWeight == pickPlacedWeight && degree > pickDegree) || + (placedWeight == pickPlacedWeight && degree == pickDegree && + u < pick); + if (better) { pick = u; - pickWeight = placedWeight; - } - } - if (pickWeight == 0) { - pick = n; - unsigned pickDegree = 0; - for (unsigned u = 0; u < n; ++u) { - if (!userVirtualQubits[u] || placedVirtual[u]) - continue; - if (pick == n || weightedDegree[u] > pickDegree) { - pick = u; - pickDegree = weightedDegree[u]; - } + pickPlacedWeight = placedWeight; + pickDegree = degree; } } + assert(pick != n && "chooseNextVirtual called with no unplaced user qubits"); return pick; } /// The free physical qubit minimizing weighted distance from `v` to its - /// placed partners, breaking ties by centrality. + /// placed partners, breaking ties by centrality. When `v` has no interaction + /// with any placed qubit every cost is zero, so this returns the most central + /// free physical, exactly as `bestFreePhysical` would. unsigned bestPhysicalFor(unsigned v) const { using Qubit = cudaq::Device::Qubit; unsigned bestPhy = n; unsigned bestCost = 0; - for (unsigned p = 0; p < n; ++p) { - if (usedPhysical[p]) - continue; + for (unsigned p : freePhysicals) { unsigned cost = 0; - for (unsigned w = 0; w < n; ++w) - if (placedVirtual[w] && interactions[v][w] > 0) - cost += interactions[v][w] * + for (unsigned w : placedVirtuals) + if (interactions.count(v, w) > 0) + cost += interactions.count(v, w) * device.getDistance(Qubit(p), Qubit(vrToPhy[w])); bool better = bestPhy == n || cost < bestCost || - (cost == bestCost && physBetter(p, bestPhy)); + (cost == bestCost && isMoreCentralPhysical(p, bestPhy)); if (better) { bestPhy = p; bestCost = cost; @@ -206,28 +237,30 @@ class GreedyInitialPlacer { return bestPhy; } - /// Map virtual `v` onto physical `p` and mark both as taken. + /// Map virtual `v` onto physical `p`, marking `v` placed and `p` taken. The + /// sorted worklists keep their order across the erases. void place(unsigned v, unsigned p) { vrToPhy[v] = p; placedVirtual[v] = true; - usedPhysical[p] = true; + placedVirtuals.push_back(v); + freePhysicals.erase(llvm::lower_bound(freePhysicals, p)); + if (auto it = llvm::lower_bound(unplacedUserVirtuals, v); + it != unplacedUserVirtuals.end() && *it == v) + unplacedUserVirtuals.erase(it); } /// Assign any still-unplaced virtuals (non-user qubits) to the remaining free - /// physicals in order. + /// physicals, pairing them in ascending order. `freePhysicals` stays sorted, + /// so this reproduces the ascending virtual to ascending physical pairing. void assignRemainingVirtuals() { - unsigned nextPhy = 0; - for (unsigned v = 0; v < n; ++v) { - if (placedVirtual[v]) - continue; - while (nextPhy < n && usedPhysical[nextPhy]) - ++nextPhy; - place(v, nextPhy); - } + unsigned next = 0; + for (unsigned v = 0; v < n; ++v) + if (!placedVirtual[v]) + vrToPhy[v] = freePhysicals[next++]; } const cudaq::Device &device; - ArrayRef> interactions; + const VirtualInteractionGraph &interactions; ArrayRef userVirtualQubits; const unsigned n; @@ -237,8 +270,15 @@ class GreedyInitialPlacer { SmallVector physDegree; SmallVector placedVirtual; - SmallVector usedPhysical; SmallVector vrToPhy; + + // Worklists maintained by `place`, so the selection helpers iterate only the + // relevant qubits instead of rescanning the full device. `freePhysicals` and + // `unplacedUserVirtuals` stay sorted ascending, while `placedVirtuals` is in + // placement order. + SmallVector freePhysicals; + SmallVector placedVirtuals; + SmallVector unplacedUserVirtuals; }; /// Greedy initial placement over the circuit interaction graph. Returns a @@ -246,11 +286,52 @@ class GreedyInitialPlacer { /// seed). SmallVector interactionPlacement(const cudaq::Device &device, - ArrayRef> interactions, + const VirtualInteractionGraph &interactions, ArrayRef userVirtualQubits) { return GreedyInitialPlacer(device, interactions, userVirtualQubits).run(); } +/// Generate the seed layouts to try, in deterministic order. Each seed only +/// proposes a starting vrToPhy. The router decides the rest. `interactions` is +/// required for the greedy strategies and ignored for identity. +SmallVector> +buildPlacementSeeds(PlacementStrategy strategy, unsigned numV, + const cudaq::Device &device, + const std::optional &interactions, + ArrayRef userVirtualQubits) { + auto identitySeed = [&]() { + SmallVector seed(numV); + for (unsigned v = 0; v < numV; ++v) + seed[v] = v; + return seed; + }; + auto greedySeed = [&]() { + assert(interactions.has_value() && + "greedy placement requires collected interactions"); + return interactionPlacement(device, *interactions, userVirtualQubits); + }; + + SmallVector> seeds; + switch (strategy) { + case PlacementStrategy::Auto: { + seeds.push_back(identitySeed()); + // Greedy degenerates to identity when there are no interactions to place, + // so routing it again would just repeat the identity pass. + SmallVector greedy = greedySeed(); + if (greedy != seeds.front()) + seeds.push_back(std::move(greedy)); + break; + } + case PlacementStrategy::Identity: + seeds.push_back(identitySeed()); + break; + case PlacementStrategy::Greedy: + seeds.push_back(greedySeed()); + break; + } + return seeds; +} + //===----------------------------------------------------------------------===// // Routing //===----------------------------------------------------------------------===// @@ -326,6 +407,26 @@ struct RoutingResult { unsigned swapCount = 0; }; +/// Look up a wire without letting DenseMap default a missing entry to virtual +/// qubit 0. +std::optional lookupVirtualQ( + const DenseMap &wireToVirtualQ, + Value wire) { + auto it = wireToVirtualQ.find(wire); + if (it == wireToVirtualQ.end()) + return std::nullopt; + return it->second; +} + +cudaq::Placement::VirtualQ requireVirtualQ( + const DenseMap &wireToVirtualQ, + Value wire) { + if (auto virtualQ = lookupVirtualQ(wireToVirtualQ, wire)) + return *virtualQ; + llvm::report_fatal_error( + "mapper invariant violated: quantum wire has no virtual qubit"); +} + /// Build the routing problem from `block`. The nodes are the routable /// operations that `isSupportedMappingOperation` accepts, other than the source /// borrows. Edges and source successors are captured in MLIR use-list order so @@ -343,7 +444,7 @@ RoutingProblem buildRoutingProblem( RoutingProblem::Node node; node.op = &op; for (auto wire : cudaq::quake::getQuantumOperands(&op)) - node.qubits.push_back(wireToVirtualQ.lookup(wire)); + node.qubits.push_back(requireVirtualQ(wireToVirtualQ, wire)); node.isMeasure = op.hasTrait(); node.isUnitary = isa(op); // A two-qubit gate the router has to make adjacent: a unitary on two wires, @@ -353,23 +454,45 @@ RoutingProblem buildRoutingProblem( problem.nodes.push_back(std::move(node)); } - // Record successor edges in use-list order, keeping the routable users only. - // A user is listed once per result wire it consumes, so a node's visit count - // reaches its wire-operand count exactly when all of its inputs are ready. - auto recordUsers = [&](Operation *producer, - SmallVectorImpl &out) { - for (Operation *user : producer->getUsers()) - if (auto it = nodeIndex.find(user); it != nodeIndex.end()) + // Record successor edges by walking the uses of each quantum result wire. A + // consumer is listed once per result wire it consumes, so a node's visit + // count reaches its wire-operand count exactly when all of its inputs are + // ready. Walking wire uses directly, rather than `Operation::getUsers`, makes + // that multiplicity explicit and ignores classical results such as + // measurement bits. + auto recordWireUsers = [&](Value wire, + SmallVectorImpl &out) { + for (OpOperand &use : wire.getUses()) + if (auto it = nodeIndex.find(use.getOwner()); it != nodeIndex.end()) out.push_back(it->second); }; for (auto &node : problem.nodes) - recordUsers(node.op, node.successors); + for (Value wire : cudaq::quake::getQuantumResults(node.op)) + recordWireUsers(wire, node.successors); for (auto borrow : sources) - recordUsers(borrow.getOperation(), problem.sourceUsers); + recordWireUsers(borrow.getResult(), problem.sourceUsers); return problem; } +/// Only unitary gates take part in the reverse-traversal pass. See +/// `buildReverseProblem` for why measurements, sinks, and returns drop out. +bool shouldIncludeInReverse(const RoutingProblem::Node &node) { + return node.isUnitary; +} + +/// Copy the routing-relevant fields of a forward unitary node into its reverse +/// counterpart. Successor and source-user edges are filled in afterwards, once +/// every included node has been assigned a reverse handle. +RoutingProblem::Node makeReverseNode(const RoutingProblem::Node &node) { + RoutingProblem::Node rev; + rev.op = node.op; + rev.qubits = node.qubits; + rev.isUnitary = true; + rev.isTwoQ = node.isTwoQ; + return rev; +} + /// Build the transposed problem over the unitary gates only, for the SABRE /// reverse-traversal pass. Routing this forward is equivalent to routing the /// original circuit in reverse: a node's successors here are its forward @@ -383,24 +506,19 @@ RoutingProblem buildReverseProblem(const RoutingProblem &forward) { SmallVector fwdToRev(forward.nodes.size()); for (unsigned i = 0, end = forward.nodes.size(); i < end; ++i) { const RoutingProblem::Node &node = forward.nodes[i]; - if (!node.isUnitary) + if (!shouldIncludeInReverse(node)) continue; fwdToRev[i] = RoutingProblem::NodeRef(reverse.nodes.size()); - RoutingProblem::Node rev; - rev.op = node.op; - rev.qubits = node.qubits; - rev.isUnitary = true; - rev.isTwoQ = node.isTwoQ; - reverse.nodes.push_back(std::move(rev)); + reverse.nodes.push_back(makeReverseNode(node)); } for (unsigned i = 0, end = forward.nodes.size(); i < end; ++i) { const RoutingProblem::Node &node = forward.nodes[i]; - if (!node.isUnitary) + if (!shouldIncludeInReverse(node)) continue; unsigned unitarySuccessors = 0; for (RoutingProblem::NodeRef s : node.successors) { - if (!forward[s].isUnitary) + if (!shouldIncludeInReverse(forward[s])) continue; ++unitarySuccessors; // Processing the consumer in reverse makes this producer ready. @@ -451,11 +569,14 @@ class SabreRouter { SabreRouter(const cudaq::Device &device, const RoutingProblem &problem, cudaq::Placement &placement, unsigned extendedLayerSize, float extendedLayerWeight, float decayDelta, - unsigned roundsDecayReset) + unsigned roundsDecayReset, unsigned minStallSwapBudget, + unsigned stallSwapBudgetPerQubit) : device(device), problem(problem), placement(placement), extendedLayerSize(extendedLayerSize), extendedLayerWeight(extendedLayerWeight), decayDelta(decayDelta), roundsDecayReset(roundsDecayReset), + minStallSwapBudget(minStallSwapBudget), + stallSwapBudgetPerQubit(stallSwapBudgetPerQubit), phyDecay(device.getNumQubits(), 1.0), allowMeasurementMapping(false) {} /// Main entry point into SabreRouter routing algorithm. Walks the DAG without @@ -481,6 +602,28 @@ class SabreRouter { Swap chooseSwap(); + /// Record a swap between two physical qubits: apply it to the placement and + /// append it to the trace. + void addSwap(cudaq::Placement::DeviceQ q0, cudaq::Placement::DeviceQ q1); + + /// Bring the closest front-layer two-qubit gate together along a shortest + /// path, ignoring the heuristic. This is the action the release valve takes + /// to guarantee forward progress. + void forceClosestGate(); + + /// Undo the swaps inserted since the last routed gate: revert the placement, + /// drop the recorded events, and restore the swap count. + void rewindEpisode(SmallVectorImpl &episodeSwaps); + + /// Release valve for a stalled front layer. SABRE's decay only softly + /// discourages the local minima the heuristic can fall into, so a stuck + /// front layer would otherwise loop forever. Discard the current episode's + /// swaps and force the closest gate together so the walk always makes + /// progress. The decay state is left as is. It is a soft heuristic and resets + /// on its own cycle. This follows the release-valve idea from Qiskit and + /// LightSABRE (arXiv:2409.08368). + void applyReleaseValve(SmallVectorImpl &episodeSwaps); + private: const cudaq::Device &device; const RoutingProblem &problem; @@ -491,6 +634,10 @@ class SabreRouter { const float extendedLayerWeight; const float decayDelta; const unsigned roundsDecayReset; + // Release-valve stall budget: force a gate once this many consecutive swaps + // route nothing. See `route` for how the floor and per-qubit terms combine. + const unsigned minStallSwapBudget; + const unsigned stallSwapBudgetPerQubit; // Internal data. The layers hold handles into `problem.nodes`. SmallVector frontLayer; @@ -686,6 +833,51 @@ SabreRouter::Swap SabreRouter::chooseSwap() { return candidates[minIdx]; } +void SabreRouter::addSwap(cudaq::Placement::DeviceQ q0, + cudaq::Placement::DeviceQ q1) { + placement.swap(q0, q1); + result.trace.push_back(RoutingEvent::swap(q0, q1)); + ++result.swapCount; +} + +void SabreRouter::forceClosestGate() { + NodeRef closest; + unsigned bestDist = ~0u; + for (NodeRef n : frontLayer) { + const RoutingProblem::Node &node = problem[n]; + if (!node.isTwoQ) + continue; + unsigned d = device.getDistance(placement.getPhy(node.qubits[0]), + placement.getPhy(node.qubits[1])); + if (d < bestDist) { + bestDist = d; + closest = n; + } + } + assert(closest.isValid() && + "a stalled front layer must hold a 2-qubit gate"); + const RoutingProblem::Node &node = problem[closest]; + cudaq::Device::Path path = device.getShortestPath( + placement.getPhy(node.qubits[0]), placement.getPhy(node.qubits[1])); + // Move one qubit along the path until it is adjacent to the other. + for (unsigned i = 0; i + 2 < path.size(); ++i) + addSwap(path[i], path[i + 1]); +} + +void SabreRouter::rewindEpisode(SmallVectorImpl &episodeSwaps) { + for (unsigned i = episodeSwaps.size(); i-- > 0;) + placement.swap(episodeSwaps[i].first, episodeSwaps[i].second); + result.trace.pop_back_n(episodeSwaps.size()); + result.swapCount -= episodeSwaps.size(); + episodeSwaps.clear(); +} + +void SabreRouter::applyReleaseValve(SmallVectorImpl &episodeSwaps) { + rewindEpisode(episodeSwaps); + forceClosestGate(); + involvedPhy.clear(); +} + RoutingResult SabreRouter::route() { #ifndef NDEBUG constexpr char logLineComment[] = @@ -712,53 +904,18 @@ RoutingResult SabreRouter::route() { // The source ops can always be mapped. visitSuccessors(problem.sourceUsers, frontLayer); - auto addSwap = [&](cudaq::Placement::DeviceQ q0, - cudaq::Placement::DeviceQ q1) { - placement.swap(q0, q1); - result.trace.push_back(RoutingEvent::swap(q0, q1)); - ++result.swapCount; - }; - - // Release valve: bring the closest front-layer gate together along a shortest - // path, ignoring the heuristic. SABRE's decay only softly discourages the - // local minima the heuristic can fall into, so a stuck front layer would - // otherwise loop forever; forcing the gate guarantees progress. This follows - // the release-valve idea from Qiskit/LightSABRE (arXiv:2409.08368). - auto forceClosestGate = [&]() { - NodeRef closest; - unsigned bestDist = ~0u; - for (NodeRef n : frontLayer) { - const RoutingProblem::Node &node = problem[n]; - if (!node.isTwoQ) - continue; - unsigned d = device.getDistance(placement.getPhy(node.qubits[0]), - placement.getPhy(node.qubits[1])); - if (d < bestDist) { - bestDist = d; - closest = n; - } - } - assert(closest.isValid() && - "a stalled front layer must hold a 2-qubit gate"); - const RoutingProblem::Node &node = problem[closest]; - cudaq::Device::Path path = device.getShortestPath( - placement.getPhy(node.qubits[0]), placement.getPhy(node.qubits[1])); - // Move one qubit along the path until it is adjacent to the other. - for (unsigned i = 0; i + 2 < path.size(); ++i) - addSwap(path[i], path[i + 1]); - }; - // SABRE's cost function is a heuristic. If it emits a long run of swaps // without making any front-layer gate executable, discard that local episode - // and force one gate along a shortest path. This budget is deliberately - // loose: bringing one front-layer gate adjacent costs at most the device - // diameter, and the qubit count upper-bounds the diameter of a connected - // device. The multiplier gives the heuristic several times that worst-case - // direct-routing cost to explore and recover before the release valve fires. - // The floor keeps a usable budget on small devices, where the scaled term - // would otherwise be too tight. - constexpr unsigned minStallSwapBudget = 64; - constexpr unsigned stallSwapBudgetPerQubit = 4; + // and force one gate along a shortest path. This is the release valve from + // LightSABRE (arXiv:2409.08368), triggered by the swap budget computed here. + // The budget is deliberately loose: bringing one front-layer gate adjacent + // costs at most the device diameter, and the qubit count upper-bounds the + // diameter of a connected device. The per-qubit multiplier gives the + // heuristic several times that worst-case direct-routing cost to explore and + // recover before the valve fires. The floor keeps a usable budget on small + // devices, where the scaled term would otherwise be too tight. Both terms are + // pass options (`minStallSwapBudget`, `stallSwapBudgetPerQubit`) defaulting to + // 64 and 4. const unsigned stallSwapLimit = std::max( minStallSwapBudget, stallSwapBudgetPerQubit * device.getNumQubits()); std::size_t numSwapSearches = 0; @@ -791,18 +948,8 @@ RoutingResult SabreRouter::route() { LLVM_DEBUG(logger.getOStream() << "\n";); if (swapsSinceRouted >= stallSwapLimit) { - // Unwind the heuristic swaps back to the last routed gate, then force the - // closest gate together so the walk always makes progress. The decay - // state is left as is; it is a soft heuristic and resets on its own - // cycle. - for (unsigned i = episodeSwaps.size(); i-- > 0;) - placement.swap(episodeSwaps[i].first, episodeSwaps[i].second); - result.trace.pop_back_n(episodeSwaps.size()); - result.swapCount -= episodeSwaps.size(); - episodeSwaps.clear(); - forceClosestGate(); + applyReleaseValve(episodeSwaps); swapsSinceRouted = 0; - involvedPhy.clear(); continue; } @@ -857,11 +1004,15 @@ class RoutingSearchStrategy { RoutingSearchStrategy(const cudaq::Device &device, const RoutingProblem &problem, bool refine, unsigned extendedLayerSize, float extendedLayerWeight, - float decayDelta, unsigned roundsDecayReset) + float decayDelta, unsigned roundsDecayReset, + unsigned minStallSwapBudget, + unsigned stallSwapBudgetPerQubit) : device(device), problem(problem), refine(refine), extendedLayerSize(extendedLayerSize), extendedLayerWeight(extendedLayerWeight), decayDelta(decayDelta), roundsDecayReset(roundsDecayReset), + minStallSwapBudget(minStallSwapBudget), + stallSwapBudgetPerQubit(stallSwapBudgetPerQubit), reverseProblem(refine ? buildReverseProblem(problem) : RoutingProblem{}) {} @@ -886,25 +1037,8 @@ class RoutingSearchStrategy { } }; - for (ArrayRef seed : seeds) { - cudaq::Placement finalPlace(numV, numPhy); - consider(routeSeed(seed, numV, numPhy, finalPlace), finalPlace); - if (!refine) - continue; - // Forward is done. Alternate backward/forward for the remaining - // traversals, keeping every forward result as a candidate. - SmallVector current(seed.begin(), seed.end()); - cudaq::Placement currentFinal = finalPlace; - for (unsigned t = 2; t <= numTraversals; ++t) { - if (t % 2 == 0) { - current = reverseRefine(currentFinal, numV); - } else { - cudaq::Placement nextFinal(numV, numPhy); - consider(routeSeed(current, numV, numPhy, nextFinal), nextFinal); - currentFinal = nextFinal; - } - } - } + for (ArrayRef seed : seeds) + routeAndRefineSeed(seed, numV, numPhy, consider); return best; } @@ -914,6 +1048,37 @@ class RoutingSearchStrategy { return a.swapCount < b.swapCount; } + /// Route one seed and, when refining, run the SABRE reverse-traversal passes + /// over it. Every forward route is offered to `consider` as a candidate. + /// Reverse routes only refine the layout into the next seed and are never + /// candidates themselves. + void routeAndRefineSeed( + ArrayRef seed, unsigned numV, unsigned numPhy, + llvm::function_ref + consider) { + // Traversal 1 (forward): route the seed as given. A candidate. + cudaq::Placement finalPlace(numV, numPhy); + consider(routeSeed(seed, numV, numPhy, finalPlace), finalPlace); + if (!refine) + return; + + // The remaining traversals alternate reverse and forward. A reverse pass + // only refines the current layout into the next seed. The forward pass that + // follows routes that seed and is the actual candidate. + SmallVector current(seed.begin(), seed.end()); + cudaq::Placement currentFinal = finalPlace; + for (unsigned t = 2; t <= numTraversals; ++t) { + bool isReversePass = (t % 2 == 0); + if (isReversePass) { + current = reverseRefine(currentFinal, numV); + } else { + cudaq::Placement nextFinal(numV, numPhy); + consider(routeSeed(current, numV, numPhy, nextFinal), nextFinal); + currentFinal = nextFinal; + } + } + } + /// Forward-route a seed layout. Returns its result and final placement. RoutingResult routeSeed(ArrayRef seed, unsigned numV, unsigned numPhy, cudaq::Placement &finalOut) { @@ -922,7 +1087,8 @@ class RoutingSearchStrategy { layout.map(cudaq::Placement::VirtualQ(v), cudaq::Placement::DeviceQ(seed[v])); SabreRouter router(device, problem, layout, extendedLayerSize, - extendedLayerWeight, decayDelta, roundsDecayReset); + extendedLayerWeight, decayDelta, roundsDecayReset, + minStallSwapBudget, stallSwapBudgetPerQubit); RoutingResult result = router.route(); finalOut = layout; return result; @@ -934,7 +1100,8 @@ class RoutingSearchStrategy { unsigned numV) { cudaq::Placement layout = startFinal; SabreRouter router(device, reverseProblem, layout, extendedLayerSize, - extendedLayerWeight, decayDelta, roundsDecayReset); + extendedLayerWeight, decayDelta, roundsDecayReset, + minStallSwapBudget, stallSwapBudgetPerQubit); router.route(); SmallVector refined(numV); for (unsigned v = 0; v < numV; ++v) @@ -949,6 +1116,8 @@ class RoutingSearchStrategy { float extendedLayerWeight; float decayDelta; unsigned roundsDecayReset; + unsigned minStallSwapBudget; + unsigned stallSwapBudgetPerQubit; RoutingProblem reverseProblem; }; @@ -978,7 +1147,8 @@ class RoutingEmitter { // device range. for (auto borrowWire : sources) { Value wire = borrowWire.getResult(); - unsigned phy = result.initialLayout[wireToVirtualQ[wire].index]; + unsigned phy = + result.initialLayout[requireVirtualQ(wireToVirtualQ, wire).index]; borrowWire.setIdentity(phy); phyToWire[phy] = wire; } @@ -1186,6 +1356,107 @@ struct MappingPrep : public cudaq::opt::impl::MappingPrepBase { } }; +//===----------------------------------------------------------------------===// +// Measurement preconditions +//===----------------------------------------------------------------------===// + +/// Walk back through pointer arithmetic and casts to the `cc.alloca` a pointer +/// ultimately refers to, or null. Mirrors the helper in AddMetadata. +cudaq::cc::AllocaOp seekAllocaFrom(Value v); +cudaq::cc::AllocaOp seekAllocaFrom(Operation *op) { + if (!op) + return {}; + if (auto alloca = dyn_cast(op)) + return alloca; + if (auto cp = dyn_cast(op)) + return seekAllocaFrom(cp.getBase()); + if (auto castOp = dyn_cast(op)) + if (isa(castOp.getOperand().getType())) + return seekAllocaFrom(castOp.getValue()); + return {}; +} +cudaq::cc::AllocaOp seekAllocaFrom(Value v) { + if (!v) + return {}; + return seekAllocaFrom(v.getDefiningOp()); +} + +/// Return true if a measurement-derived classical value reaches control flow, +/// quantum work, or a call. This mirrors AddMetadata's measurement-dependence +/// check, including simple store/load forwarding through allocas. +bool measurementFeedsControlOrQuantum(Operation *meas) { + SmallVector worklist; + llvm::SmallPtrSet seen; + auto enqueue = [&](Operation *op) { + if (op && seen.insert(op).second) + worklist.push_back(op); + }; + auto enqueueUsers = [&](Value v) { + for (Operation *user : v.getUsers()) + enqueue(user); + }; + for (Value result : meas->getResults()) + if (!isa(result.getType())) + enqueueUsers(result); + while (!worklist.empty()) { + Operation *op = worklist.pop_back_val(); + if (isa(op)) + return true; + // A store has no SSA result, so follow the value through memory: a later + // load from the same alloca can carry the measurement into control flow. + if (auto storeOp = dyn_cast(op)) { + if (auto alloca = seekAllocaFrom(storeOp.getPtrvalue())) + enqueue(alloca.getOperation()); + continue; + } + for (Value result : op->getResults()) + enqueueUsers(result); + } + return false; +} + +/// A measurement the mapper cannot support, paired with a diagnostic message. +struct UnsupportedMeasurement { + Operation *op; + const char *message; +}; + +/// The first measurement in `func` whose results are used in a way the mapper +/// cannot support, or nullopt when every measurement is a terminal readout. +/// Because the mapper defers measurements to the end of the circuit, it only +/// supports measured wires that flow to a terminal consumer (`return_wire` or +/// `sink`) and measurement results that do not feed control flow, quantum +/// dataflow, or calls. +std::optional +findUnsupportedMeasurement(func::FuncOp func) { + std::optional found; + func.walk([&](cudaq::quake::MeasurementInterface meas) { + Operation *measOp = meas.getOperation(); + // A measured wire may only flow to a terminal wire consumer. Anything else + // is a mid-circuit measurement the deferral cannot preserve. + for (Value result : measOp->getResults()) { + if (!isa(result.getType())) + continue; + for (Operation *user : result.getUsers()) + if (!isa(user)) { + found = {measOp, "unsupported mid-circuit measurement: a measured " + "wire is used by a later operation"}; + return WalkResult::interrupt(); + } + } + if (measurementFeedsControlOrQuantum(measOp)) { + found = {measOp, "unsupported measurement-controlled operation: a " + "measurement result feeds control flow, a quantum " + "operation, or a call"}; + return WalkResult::interrupt(); + } + return WalkResult::advance(); + }); + return found; +} + struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { using MappingFuncBase::MappingFuncBase; @@ -1339,11 +1610,10 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { bool collectInteractions = placementStrategy != PlacementStrategy::Identity; // Two-qubit interaction data for placement, collected during the scan. - SmallVector> interactions; + std::optional interactions; SmallVector userVirtualQubits; if (collectInteractions) { - interactions.assign(deviceNumQubits, - SmallVector(deviceNumQubits, 0)); + interactions.emplace(deviceNumQubits); userVirtualQubits.assign(deviceNumQubits, false); } @@ -1391,6 +1661,27 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { return; } + // Get the wire operands and their virtual qubits. If a supported op + // consumes an untracked wire, some earlier unsupported operation must + // have produced it. Do not let DenseMap default that to virtual qubit 0. + auto wireOperands = cudaq::quake::getQuantumOperands(&op); + SmallVector virtualOperands; + virtualOperands.reserve(wireOperands.size()); + for (Value wire : wireOperands) { + auto virtualQ = lookupVirtualQ(wireToVirtualQ, wire); + if (!virtualQ) { + if (nonComposable) { + op.emitOpError("has a quantum operand that is not tracked by " + "the mapper"); + signalPassFailure(); + } + LLVM_DEBUG(llvm::dbgs() + << "untracked quantum operand in mapper\n"); + return; + } + virtualOperands.push_back(*virtualQ); + } + // Since `quake.return_wire` operations do not generate new wires, we // don't need to further analyze. if (auto rop = dyn_cast(op)) { @@ -1398,9 +1689,8 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { continue; } - // Get the wire operands and check if the operators uses at most two - // qubits. N.B: Measurements do not have this restriction. - auto wireOperands = cudaq::quake::getQuantumOperands(&op); + // Check if the operator uses at most two qubits. N.B: Measurements do + // not have this restriction. if (!op.hasTrait() && wireOperands.size() > 2) { if (nonComposable) { func.emitError("Cannot map a kernel with operators that use more " @@ -1411,32 +1701,46 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { return; } - // Save which qubits are measured + // Save which qubits are measured. if (isa(op)) - for (const auto &wire : wireOperands) - userQubitsMeasured.push_back(wireToVirtualQ[wire].index); + for (auto virtualQ : virtualOperands) + userQubitsMeasured.push_back(virtualQ.index); // Record two-qubit interactions for placement. if (collectInteractions && !isa(op) && wireOperands.size() == 2) { - unsigned v0 = wireToVirtualQ[wireOperands[0]].index; - unsigned v1 = wireToVirtualQ[wireOperands[1]].index; - if (v0 != v1) { - interactions[v0][v1] += 1; - interactions[v1][v0] += 1; - } + unsigned v0 = virtualOperands[0].index; + unsigned v1 = virtualOperands[1].index; + interactions->addInteraction(v0, v1); } // Map the result wires to the appropriate virtual qubits. - for (auto &&[wire, newWire] : llvm::zip_equal( - wireOperands, cudaq::quake::getQuantumResults(&op))) { - // Don't use wireToVirtualQ[a] = wireToVirtualQ[b]. It will work - // *most* of the time but cause memory corruption other times because - // DenseMap references can be invalidated upon insertion of new pairs. - wireToVirtualQ.insert({newWire, wireToVirtualQ[wire]}); - finalQubitWire[wireToVirtualQ[wire].index] = newWire; + auto wireResults = cudaq::quake::getQuantumResults(&op); + if (!wireResults.empty() && wireResults.size() != wireOperands.size()) { + if (nonComposable) { + op.emitOpError("has a different number of quantum operands and " + "quantum results"); + signalPassFailure(); + } + LLVM_DEBUG(llvm::dbgs() + << "quantum operand/result arity mismatch\n"); + return; } + for (auto &&[index, newWire] : llvm::enumerate(wireResults)) { + cudaq::Placement::VirtualQ virtualQ = virtualOperands[index]; + wireToVirtualQ.insert({newWire, virtualQ}); + finalQubitWire[virtualQ.index] = newWire; + } + } else if (!cudaq::quake::getQuantumOperands(&op).empty() || + !cudaq::quake::getQuantumResults(&op).empty()) { + if (nonComposable) { + op.emitOpError("is not supported by the mapper"); + signalPassFailure(); + } + LLVM_DEBUG(llvm::dbgs() + << "unsupported quantum operation in mapper\n"); + return; } } @@ -1451,6 +1755,18 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { return; } + // Measurement deferral is only safe for terminal readout. Reject + // unsupported mid-circuit/adaptive uses before mutating IR. + if (auto unsupported = findUnsupportedMeasurement(func)) { + if (nonComposable) { + unsupported->op->emitOpError(unsupported->message); + signalPassFailure(); + } + LLVM_DEBUG(llvm::dbgs() + << "unsupported measurement use; skipping mapping\n"); + return; + } + // Make all existing borrow_wire ops use the mapped wire set. func.walk([&](cudaq::quake::BorrowWireOp borrowOp) { borrowOp.setSetName(mappedWireSetName); @@ -1480,8 +1796,9 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { finalQubitWire[i].getLoc(), resTy, measureOp.getMeasOut()); - wireToVirtualQ.insert( - {measureOp.getWires()[0], wireToVirtualQ[finalQubitWire[i]]}); + wireToVirtualQ.insert({measureOp.getWires()[0], + requireVirtualQ(wireToVirtualQ, + finalQubitWire[i])}); userQubitsMeasured.push_back(i); } @@ -1512,36 +1829,9 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { const unsigned numV = sources.size(); const unsigned numPhy = deviceInstance->getNumQubits(); - // Generate the seed layouts to try, in deterministic order. Each seed only - // proposes a starting vrToPhy. The router decides the rest. - SmallVector> seeds; - auto identitySeed = [&]() { - SmallVector seed(numV); - for (unsigned v = 0; v < numV; ++v) - seed[v] = v; - return seed; - }; - auto greedySeed = [&]() { - return interactionPlacement(*deviceInstance, interactions, - userVirtualQubits); - }; - switch (placementStrategy) { - case PlacementStrategy::Auto: { - seeds.push_back(identitySeed()); - // Greedy degenerates to identity when there are no interactions to place; - // routing it again would just repeat the identity pass. - SmallVector greedy = greedySeed(); - if (greedy != seeds.front()) - seeds.push_back(std::move(greedy)); - break; - } - case PlacementStrategy::Identity: - seeds.push_back(identitySeed()); - break; - case PlacementStrategy::Greedy: - seeds.push_back(greedySeed()); - break; - } + SmallVector> seeds = buildPlacementSeeds( + placementStrategy, numV, *deviceInstance, interactions, + userVirtualQubits); // Build the routing problem once (it does not depend on the layout), then // search over the seeds for the result with the fewest swaps. @@ -1549,7 +1839,8 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { buildRoutingProblem(block, sources, wireToVirtualQ); RoutingSearchStrategy search( *deviceInstance, problem, searchStrategy == SearchStrategy::Sabre, - extendedLayerSize, extendedLayerWeight, decayDelta, roundsDecayReset); + extendedLayerSize, extendedLayerWeight, decayDelta, roundsDecayReset, + minStallSwapBudget, stallSwapBudgetPerQubit); RoutingSearchStrategy::Selection selection = search.run(seeds, numV, numPhy); RoutingResult &best = selection.result; @@ -1655,6 +1946,8 @@ struct MappingPipelineOptions DECLARE_SUB_OPTION(MappingFuncOptions, extendedLayerWeight); DECLARE_SUB_OPTION(MappingFuncOptions, decayDelta); DECLARE_SUB_OPTION(MappingFuncOptions, roundsDecayReset); + DECLARE_SUB_OPTION(MappingFuncOptions, minStallSwapBudget); + DECLARE_SUB_OPTION(MappingFuncOptions, stallSwapBudgetPerQubit); DECLARE_SUB_OPTION(MappingFuncOptions, placement); DECLARE_SUB_OPTION(MappingFuncOptions, search); PassOptions::Option nonComposable{*this, "raise-fatal-errors"}; @@ -1684,6 +1977,8 @@ void registerMappingPipeline() { setIt(funcOpts.extendedLayerWeight, opt.extendedLayerWeight); setIt(funcOpts.decayDelta, opt.decayDelta); setIt(funcOpts.roundsDecayReset, opt.roundsDecayReset); + setIt(funcOpts.minStallSwapBudget, opt.minStallSwapBudget); + setIt(funcOpts.stallSwapBudgetPerQubit, opt.stallSwapBudgetPerQubit); setIt(funcOpts.placement, opt.placement); setIt(funcOpts.search, opt.search); setIt(funcOpts.nonComposable, opt.nonComposable); From 4642738b0aad68d3673a06a68102c5f7f236307b Mon Sep 17 00:00:00 2001 From: Seemanta Bhattacharjee Date: Thu, 18 Jun 2026 01:11:45 +0600 Subject: [PATCH 3/7] copyrights, measurement-guard tests, valve contrast test Signed-off-by: Seemanta Bhattacharjee --- .../test/Transforms/mapping_connectivity.qke | 2 +- cudaq/test/Transforms/mapping_errors.qke | 42 +++ .../mapping_greedy_relocated_hub.qke | 2 +- .../Transforms/mapping_invalid_placement.qke | 2 +- .../Transforms/mapping_invalid_search.qke | 2 +- .../Transforms/mapping_placement_seeds.qke | 2 +- .../mapping_search_late_interactions.qke | 2 +- cudaq/test/Transforms/mapping_search_none.qke | 2 +- .../Transforms/mapping_search_refinement.qke | 2 +- cudaq/test/Transforms/mapping_search_star.qke | 2 +- .../Transforms/mapping_search_termination.qke | 275 ++---------------- 11 files changed, 82 insertions(+), 253 deletions(-) diff --git a/cudaq/test/Transforms/mapping_connectivity.qke b/cudaq/test/Transforms/mapping_connectivity.qke index eaea8e3565d..c110383bbef 100644 --- a/cudaq/test/Transforms/mapping_connectivity.qke +++ b/cudaq/test/Transforms/mapping_connectivity.qke @@ -1,5 +1,5 @@ // ========================================================================== // -// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// Copyright (c) 2026 NVIDIA Corporation & Affiliates. // // All rights reserved. // // // // This source code and the accompanying materials are made available under // diff --git a/cudaq/test/Transforms/mapping_errors.qke b/cudaq/test/Transforms/mapping_errors.qke index a8c68f7d237..da8e95aa997 100644 --- a/cudaq/test/Transforms/mapping_errors.qke +++ b/cudaq/test/Transforms/mapping_errors.qke @@ -62,3 +62,45 @@ quake.wire_set @wires_03[10] func.func @test_03() { return } + +// ----- + +quake.wire_set @wires_04[10] + +// A measured wire reused by a later quantum op is a mid-circuit measurement the +// mapper cannot preserve while it defers measurements to the end. +func.func @test_04() { + %0 = quake.borrow_wire @wires_04[0] : !quake.wire + %1 = quake.borrow_wire @wires_04[1] : !quake.wire + %2:2 = quake.x [%0] %1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + // expected-error @+1 {{unsupported mid-circuit measurement}} + %meas, %wire = quake.mz %2#0 : (!quake.wire) -> (!quake.measure, !quake.wire) + %3 = quake.h %wire : (!quake.wire) -> !quake.wire + quake.return_wire %3 : !quake.wire + quake.return_wire %2#1 : !quake.wire + return +} + +// ----- + +quake.wire_set @wires_05[10] + +// A measurement result feeding control flow through a store/load is the +// adaptive/feed-forward shape CUDA-Q lowering produces, and is unsupported. +func.func @test_05() { + %0 = quake.borrow_wire @wires_05[0] : !quake.wire + %1 = quake.borrow_wire @wires_05[1] : !quake.wire + %2:2 = quake.x [%0] %1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + // expected-error @+1 {{unsupported measurement-controlled operation}} + %meas, %wire = quake.mz %2#0 : (!quake.wire) -> (!quake.measure, !quake.wire) + %b = quake.discriminate %meas : (!quake.measure) -> i1 + %slot = cc.alloca i1 + cc.store %b, %slot : !cc.ptr + %cond = cc.load %slot : !cc.ptr + cc.if(%cond) { + %tmp = cc.alloca i1 + } + quake.return_wire %wire : !quake.wire + quake.return_wire %2#1 : !quake.wire + return +} diff --git a/cudaq/test/Transforms/mapping_greedy_relocated_hub.qke b/cudaq/test/Transforms/mapping_greedy_relocated_hub.qke index 407782ef4e8..2deb5cea40d 100644 --- a/cudaq/test/Transforms/mapping_greedy_relocated_hub.qke +++ b/cudaq/test/Transforms/mapping_greedy_relocated_hub.qke @@ -1,5 +1,5 @@ // ========================================================================== // -// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// Copyright (c) 2026 NVIDIA Corporation & Affiliates. // // All rights reserved. // // // // This source code and the accompanying materials are made available under // diff --git a/cudaq/test/Transforms/mapping_invalid_placement.qke b/cudaq/test/Transforms/mapping_invalid_placement.qke index e5ab758c7f6..414e19ee3f9 100644 --- a/cudaq/test/Transforms/mapping_invalid_placement.qke +++ b/cudaq/test/Transforms/mapping_invalid_placement.qke @@ -1,5 +1,5 @@ // ========================================================================== // -// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// Copyright (c) 2026 NVIDIA Corporation & Affiliates. // // All rights reserved. // // // // This source code and the accompanying materials are made available under // diff --git a/cudaq/test/Transforms/mapping_invalid_search.qke b/cudaq/test/Transforms/mapping_invalid_search.qke index 3d65be18093..450ac23cc0a 100644 --- a/cudaq/test/Transforms/mapping_invalid_search.qke +++ b/cudaq/test/Transforms/mapping_invalid_search.qke @@ -1,5 +1,5 @@ // ========================================================================== // -// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// Copyright (c) 2026 NVIDIA Corporation & Affiliates. // // All rights reserved. // // // // This source code and the accompanying materials are made available under // diff --git a/cudaq/test/Transforms/mapping_placement_seeds.qke b/cudaq/test/Transforms/mapping_placement_seeds.qke index c7db56a90fe..558f636423e 100644 --- a/cudaq/test/Transforms/mapping_placement_seeds.qke +++ b/cudaq/test/Transforms/mapping_placement_seeds.qke @@ -1,5 +1,5 @@ // ========================================================================== // -// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// Copyright (c) 2026 NVIDIA Corporation & Affiliates. // // All rights reserved. // // // // This source code and the accompanying materials are made available under // diff --git a/cudaq/test/Transforms/mapping_search_late_interactions.qke b/cudaq/test/Transforms/mapping_search_late_interactions.qke index 040e7045d4f..a4478d61802 100644 --- a/cudaq/test/Transforms/mapping_search_late_interactions.qke +++ b/cudaq/test/Transforms/mapping_search_late_interactions.qke @@ -1,5 +1,5 @@ // ========================================================================== // -// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// Copyright (c) 2026 NVIDIA Corporation & Affiliates. // // All rights reserved. // // // // This source code and the accompanying materials are made available under // diff --git a/cudaq/test/Transforms/mapping_search_none.qke b/cudaq/test/Transforms/mapping_search_none.qke index 87a15e3e957..dbbd05a030c 100644 --- a/cudaq/test/Transforms/mapping_search_none.qke +++ b/cudaq/test/Transforms/mapping_search_none.qke @@ -1,5 +1,5 @@ // ========================================================================== // -// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// Copyright (c) 2026 NVIDIA Corporation & Affiliates. // // All rights reserved. // // // // This source code and the accompanying materials are made available under // diff --git a/cudaq/test/Transforms/mapping_search_refinement.qke b/cudaq/test/Transforms/mapping_search_refinement.qke index 483261c4fc6..a4670901839 100644 --- a/cudaq/test/Transforms/mapping_search_refinement.qke +++ b/cudaq/test/Transforms/mapping_search_refinement.qke @@ -1,5 +1,5 @@ // ========================================================================== // -// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// Copyright (c) 2026 NVIDIA Corporation & Affiliates. // // All rights reserved. // // // // This source code and the accompanying materials are made available under // diff --git a/cudaq/test/Transforms/mapping_search_star.qke b/cudaq/test/Transforms/mapping_search_star.qke index d93c7ea0717..06b43aad563 100644 --- a/cudaq/test/Transforms/mapping_search_star.qke +++ b/cudaq/test/Transforms/mapping_search_star.qke @@ -1,5 +1,5 @@ // ========================================================================== // -// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// Copyright (c) 2026 NVIDIA Corporation & Affiliates. // // All rights reserved. // // // // This source code and the accompanying materials are made available under // diff --git a/cudaq/test/Transforms/mapping_search_termination.qke b/cudaq/test/Transforms/mapping_search_termination.qke index 3f66685ccba..9931ea8f5dc 100644 --- a/cudaq/test/Transforms/mapping_search_termination.qke +++ b/cudaq/test/Transforms/mapping_search_termination.qke @@ -1,263 +1,50 @@ // ========================================================================== // -// Copyright (c) 2022 - 2026 NVIDIA Corporation & Affiliates. // +// Copyright (c) 2026 NVIDIA Corporation & Affiliates. // // All rights reserved. // // // // This source code and the accompanying materials are made available under // // the terms of the Apache License 2.0 which accompanies this distribution. // // ========================================================================== // -// A dense quantum-volume-style circuit on a 20-qubit path. The reverse-traversal -// refinement can hand the router a layout where the SABRE heuristic stalls in a -// local minimum. Without the release valve the search loops forever on this -// input. The check is simply that mapping terminates and emits a mapped kernel. -// CircuitCheck is omitted: equivalence checking builds the 2^20 unitary. +// The release valve force-routes a front-layer gate when the SABRE heuristic +// reaches the stall budget without routing one. Pinning the budget to one swap +// makes the valve fire, and contrasting that against a budget high enough that +// it never fires shows the routing is observably the valve's work, not ordinary +// SABRE: on this circuit the forced route uses 5 swaps where the unrestricted +// heuristic uses 3. CircuitCheck confirms both outputs are correct mappings. -// RUN: cudaq-opt '--qubit-mapping=device=path(20) placement=auto search=sabre' %s | FileCheck %s +// RUN: cudaq-opt '--qubit-mapping=device=path(6) placement=identity search=none minStallSwapBudget=1 stallSwapBudgetPerQubit=0' %s | FileCheck --check-prefix=VALVE %s +// RUN: cudaq-opt '--qubit-mapping=device=path(6) placement=identity search=none minStallSwapBudget=1 stallSwapBudgetPerQubit=0' %s | CircuitCheck --up-to-mapping %s +// RUN: cudaq-opt '--qubit-mapping=device=path(6) placement=identity search=none minStallSwapBudget=16 stallSwapBudgetPerQubit=0' %s | FileCheck --check-prefix=NOVALVE %s +// RUN: cudaq-opt '--qubit-mapping=device=path(6) placement=identity search=none minStallSwapBudget=16 stallSwapBudgetPerQubit=0' %s | CircuitCheck --up-to-mapping %s quake.wire_set @wires[2147483647] -func.func @qv_path20() { +func.func @release_valve() { %v0 = quake.borrow_wire @wires[0] : !quake.wire %v1 = quake.borrow_wire @wires[1] : !quake.wire %v2 = quake.borrow_wire @wires[2] : !quake.wire %v3 = quake.borrow_wire @wires[3] : !quake.wire %v4 = quake.borrow_wire @wires[4] : !quake.wire %v5 = quake.borrow_wire @wires[5] : !quake.wire - %v6 = quake.borrow_wire @wires[6] : !quake.wire - %v7 = quake.borrow_wire @wires[7] : !quake.wire - %v8 = quake.borrow_wire @wires[8] : !quake.wire - %v9 = quake.borrow_wire @wires[9] : !quake.wire - %v10 = quake.borrow_wire @wires[10] : !quake.wire - %v11 = quake.borrow_wire @wires[11] : !quake.wire - %v12 = quake.borrow_wire @wires[12] : !quake.wire - %v13 = quake.borrow_wire @wires[13] : !quake.wire - %v14 = quake.borrow_wire @wires[14] : !quake.wire - %v15 = quake.borrow_wire @wires[15] : !quake.wire - %v16 = quake.borrow_wire @wires[16] : !quake.wire - %v17 = quake.borrow_wire @wires[17] : !quake.wire - %v18 = quake.borrow_wire @wires[18] : !quake.wire - %v19 = quake.borrow_wire @wires[19] : !quake.wire - %g0:2 = quake.x [%v10] %v18 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g1:2 = quake.x [%v16] %v14 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g2:2 = quake.x [%v0] %v17 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g3:2 = quake.x [%v11] %v2 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g4:2 = quake.x [%v3] %v9 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g5:2 = quake.x [%v5] %v7 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g6:2 = quake.x [%v4] %v19 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g7:2 = quake.x [%v6] %v15 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g8:2 = quake.x [%v8] %v1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g9:2 = quake.x [%v13] %v12 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g10:2 = quake.x [%g9#0] %g3#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g11:2 = quake.x [%g1#0] %g2#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g12:2 = quake.x [%g2#1] %g6#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g13:2 = quake.x [%g1#1] %g5#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g14:2 = quake.x [%g0#1] %g6#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g15:2 = quake.x [%g9#1] %g7#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g16:2 = quake.x [%g5#0] %g8#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g17:2 = quake.x [%g8#0] %g7#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g18:2 = quake.x [%g0#0] %g3#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g19:2 = quake.x [%g4#0] %g4#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g20:2 = quake.x [%g14#0] %g14#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g21:2 = quake.x [%g12#0] %g13#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g22:2 = quake.x [%g12#1] %g11#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g23:2 = quake.x [%g15#1] %g17#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g24:2 = quake.x [%g19#1] %g13#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g25:2 = quake.x [%g16#1] %g10#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g26:2 = quake.x [%g16#0] %g10#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g27:2 = quake.x [%g19#0] %g18#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g28:2 = quake.x [%g17#1] %g11#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g29:2 = quake.x [%g15#0] %g18#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g30:2 = quake.x [%g21#1] %g28#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g31:2 = quake.x [%g29#0] %g29#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g32:2 = quake.x [%g21#0] %g25#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g33:2 = quake.x [%g23#0] %g20#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g34:2 = quake.x [%g20#1] %g26#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g35:2 = quake.x [%g24#1] %g25#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g36:2 = quake.x [%g26#1] %g22#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g37:2 = quake.x [%g23#1] %g24#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g38:2 = quake.x [%g27#0] %g28#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g39:2 = quake.x [%g22#1] %g27#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g40:2 = quake.x [%g32#1] %g33#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g41:2 = quake.x [%g33#0] %g32#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g42:2 = quake.x [%g37#0] %g34#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g43:2 = quake.x [%g36#0] %g35#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g44:2 = quake.x [%g39#0] %g36#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g45:2 = quake.x [%g38#1] %g30#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g46:2 = quake.x [%g35#0] %g38#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g47:2 = quake.x [%g31#1] %g37#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g48:2 = quake.x [%g30#0] %g39#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g49:2 = quake.x [%g31#0] %g34#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g50:2 = quake.x [%g49#1] %g40#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g51:2 = quake.x [%g43#1] %g45#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g52:2 = quake.x [%g47#0] %g42#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g53:2 = quake.x [%g46#0] %g45#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g54:2 = quake.x [%g44#1] %g43#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g55:2 = quake.x [%g41#0] %g47#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g56:2 = quake.x [%g48#1] %g46#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g57:2 = quake.x [%g40#0] %g48#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g58:2 = quake.x [%g42#0] %g44#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g59:2 = quake.x [%g49#0] %g41#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g60:2 = quake.x [%g58#1] %g59#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g61:2 = quake.x [%g58#0] %g57#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g62:2 = quake.x [%g50#0] %g53#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g63:2 = quake.x [%g59#1] %g55#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g64:2 = quake.x [%g52#0] %g52#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g65:2 = quake.x [%g56#1] %g51#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g66:2 = quake.x [%g54#0] %g54#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g67:2 = quake.x [%g51#1] %g57#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g68:2 = quake.x [%g55#0] %g56#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g69:2 = quake.x [%g50#1] %g53#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g70:2 = quake.x [%g67#1] %g61#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g71:2 = quake.x [%g60#1] %g66#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g72:2 = quake.x [%g64#1] %g69#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g73:2 = quake.x [%g60#0] %g61#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g74:2 = quake.x [%g68#0] %g62#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g75:2 = quake.x [%g66#1] %g63#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g76:2 = quake.x [%g65#0] %g68#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g77:2 = quake.x [%g62#0] %g67#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g78:2 = quake.x [%g64#0] %g63#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g79:2 = quake.x [%g69#0] %g65#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g80:2 = quake.x [%g70#0] %g76#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g81:2 = quake.x [%g79#0] %g74#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g82:2 = quake.x [%g73#1] %g78#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g83:2 = quake.x [%g72#0] %g71#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g84:2 = quake.x [%g77#0] %g75#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g85:2 = quake.x [%g71#1] %g73#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g86:2 = quake.x [%g75#1] %g78#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g87:2 = quake.x [%g70#1] %g77#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g88:2 = quake.x [%g79#1] %g74#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g89:2 = quake.x [%g72#1] %g76#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g90:2 = quake.x [%g89#0] %g87#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g91:2 = quake.x [%g80#1] %g82#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g92:2 = quake.x [%g86#1] %g87#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g93:2 = quake.x [%g88#1] %g81#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g94:2 = quake.x [%g82#1] %g86#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g95:2 = quake.x [%g80#0] %g84#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g96:2 = quake.x [%g83#0] %g85#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g97:2 = quake.x [%g84#0] %g83#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g98:2 = quake.x [%g89#1] %g88#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g99:2 = quake.x [%g81#1] %g85#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g100:2 = quake.x [%g95#1] %g97#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g101:2 = quake.x [%g94#0] %g97#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g102:2 = quake.x [%g99#1] %g95#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g103:2 = quake.x [%g98#1] %g93#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g104:2 = quake.x [%g91#1] %g92#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g105:2 = quake.x [%g96#1] %g90#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g106:2 = quake.x [%g94#1] %g93#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g107:2 = quake.x [%g91#0] %g90#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g108:2 = quake.x [%g92#1] %g96#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g109:2 = quake.x [%g99#0] %g98#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g110:2 = quake.x [%g103#1] %g108#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g111:2 = quake.x [%g106#1] %g101#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g112:2 = quake.x [%g109#0] %g104#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g113:2 = quake.x [%g102#0] %g101#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g114:2 = quake.x [%g100#0] %g107#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g115:2 = quake.x [%g106#0] %g108#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g116:2 = quake.x [%g102#1] %g100#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g117:2 = quake.x [%g109#1] %g105#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g118:2 = quake.x [%g105#1] %g104#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g119:2 = quake.x [%g103#0] %g107#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g120:2 = quake.x [%g119#0] %g116#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g121:2 = quake.x [%g113#1] %g118#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g122:2 = quake.x [%g117#1] %g113#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g123:2 = quake.x [%g112#0] %g111#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g124:2 = quake.x [%g114#0] %g115#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g125:2 = quake.x [%g112#1] %g110#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g126:2 = quake.x [%g114#1] %g118#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g127:2 = quake.x [%g117#0] %g116#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g128:2 = quake.x [%g115#1] %g119#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g129:2 = quake.x [%g111#0] %g110#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g130:2 = quake.x [%g126#0] %g122#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g131:2 = quake.x [%g120#0] %g124#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g132:2 = quake.x [%g127#0] %g129#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g133:2 = quake.x [%g126#1] %g120#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g134:2 = quake.x [%g128#1] %g129#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g135:2 = quake.x [%g124#1] %g125#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g136:2 = quake.x [%g125#1] %g122#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g137:2 = quake.x [%g127#1] %g123#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g138:2 = quake.x [%g121#1] %g121#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g139:2 = quake.x [%g123#0] %g128#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g140:2 = quake.x [%g130#0] %g136#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g141:2 = quake.x [%g139#0] %g135#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g142:2 = quake.x [%g137#1] %g138#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g143:2 = quake.x [%g137#0] %g131#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g144:2 = quake.x [%g135#0] %g131#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g145:2 = quake.x [%g132#0] %g136#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g146:2 = quake.x [%g139#1] %g132#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g147:2 = quake.x [%g134#0] %g133#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g148:2 = quake.x [%g134#1] %g130#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g149:2 = quake.x [%g138#1] %g133#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g150:2 = quake.x [%g145#1] %g147#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g151:2 = quake.x [%g140#1] %g146#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g152:2 = quake.x [%g144#0] %g144#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g153:2 = quake.x [%g141#0] %g142#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g154:2 = quake.x [%g142#1] %g149#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g155:2 = quake.x [%g141#1] %g143#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g156:2 = quake.x [%g143#0] %g148#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g157:2 = quake.x [%g148#0] %g149#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g158:2 = quake.x [%g147#1] %g140#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g159:2 = quake.x [%g145#0] %g146#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g160:2 = quake.x [%g158#0] %g151#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g161:2 = quake.x [%g159#0] %g159#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g162:2 = quake.x [%g155#1] %g156#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g163:2 = quake.x [%g157#1] %g153#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g164:2 = quake.x [%g150#0] %g156#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g165:2 = quake.x [%g153#0] %g158#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g166:2 = quake.x [%g157#0] %g155#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g167:2 = quake.x [%g152#0] %g152#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g168:2 = quake.x [%g150#1] %g154#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g169:2 = quake.x [%g154#0] %g151#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g170:2 = quake.x [%g164#1] %g161#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g171:2 = quake.x [%g168#1] %g169#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g172:2 = quake.x [%g167#0] %g165#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g173:2 = quake.x [%g163#0] %g164#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g174:2 = quake.x [%g163#1] %g166#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g175:2 = quake.x [%g162#0] %g167#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g176:2 = quake.x [%g165#0] %g160#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g177:2 = quake.x [%g161#1] %g168#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g178:2 = quake.x [%g169#0] %g160#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g179:2 = quake.x [%g162#1] %g166#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g180:2 = quake.x [%g174#0] %g170#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g181:2 = quake.x [%g176#1] %g171#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g182:2 = quake.x [%g177#0] %g177#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g183:2 = quake.x [%g174#1] %g176#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g184:2 = quake.x [%g175#0] %g178#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g185:2 = quake.x [%g173#0] %g178#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g186:2 = quake.x [%g179#1] %g173#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g187:2 = quake.x [%g175#1] %g172#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g188:2 = quake.x [%g170#1] %g172#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g189:2 = quake.x [%g179#0] %g171#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g190:2 = quake.x [%g184#1] %g188#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g191:2 = quake.x [%g188#1] %g180#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g192:2 = quake.x [%g181#0] %g182#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g193:2 = quake.x [%g185#0] %g189#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g194:2 = quake.x [%g186#0] %g181#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g195:2 = quake.x [%g183#0] %g187#0 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g196:2 = quake.x [%g183#1] %g180#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g197:2 = quake.x [%g184#0] %g186#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g198:2 = quake.x [%g182#1] %g185#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - %g199:2 = quake.x [%g187#1] %g189#1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - quake.return_wire %g194#0 : !quake.wire - quake.return_wire %g197#0 : !quake.wire - quake.return_wire %g193#0 : !quake.wire - quake.return_wire %g190#1 : !quake.wire - quake.return_wire %g193#1 : !quake.wire - quake.return_wire %g192#1 : !quake.wire - quake.return_wire %g194#1 : !quake.wire - quake.return_wire %g196#1 : !quake.wire - quake.return_wire %g190#0 : !quake.wire - quake.return_wire %g191#0 : !quake.wire - quake.return_wire %g198#0 : !quake.wire - quake.return_wire %g195#1 : !quake.wire - quake.return_wire %g192#0 : !quake.wire - quake.return_wire %g195#0 : !quake.wire - quake.return_wire %g196#0 : !quake.wire - quake.return_wire %g199#0 : !quake.wire - quake.return_wire %g197#1 : !quake.wire - quake.return_wire %g199#1 : !quake.wire - quake.return_wire %g198#1 : !quake.wire - quake.return_wire %g191#1 : !quake.wire + // Two overlapping long-range gates on a path. With the budget pinned to 1 the + // heuristic is cut off and the valve force-routes them along shortest paths; + // with a high budget the heuristic routes them with fewer swaps. + %g0:2 = quake.x [%v0] %v3 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + %g1:2 = quake.x [%v2] %v5 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + quake.return_wire %g0#0 : !quake.wire + quake.return_wire %v1 : !quake.wire + quake.return_wire %g1#0 : !quake.wire + quake.return_wire %g0#1 : !quake.wire + quake.return_wire %v4 : !quake.wire + quake.return_wire %g1#1 : !quake.wire return } -// CHECK: func.func @qv_path20() attributes -// CHECK-SAME: mapping_v2p +// The release valve force-routes both gates, at a higher swap count. +// VALVE-COUNT-5: quake.swap +// VALVE-NOT: quake.swap + +// With a budget high enough that the valve never fires, the heuristic routes the +// same circuit with fewer swaps. +// NOVALVE-COUNT-3: quake.swap +// NOVALVE-NOT: quake.swap From dec2106153c089a836509b034d80746dd1c63c8d Mon Sep 17 00:00:00 2001 From: Seemanta Bhattacharjee Date: Thu, 18 Jun 2026 18:19:58 +0600 Subject: [PATCH 4/7] address mapping review feedback Signed-off-by: Seemanta Bhattacharjee --- .../cudaq/Optimizer/Transforms/Passes.td | 16 +- cudaq/lib/Optimizer/Transforms/Mapping.cpp | 440 ++++++++---------- cudaq/test/Transforms/mapping_errors.qke | 5 +- .../mapping_measurement_composable.qke | 28 ++ .../Transforms/mapping_search_termination.qke | 8 +- 5 files changed, 237 insertions(+), 260 deletions(-) create mode 100644 cudaq/test/Transforms/mapping_measurement_composable.qke diff --git a/cudaq/include/cudaq/Optimizer/Transforms/Passes.td b/cudaq/include/cudaq/Optimizer/Transforms/Passes.td index cc75b3a6cd8..6ebd42856fb 100644 --- a/cudaq/include/cudaq/Optimizer/Transforms/Passes.td +++ b/cudaq/include/cudaq/Optimizer/Transforms/Passes.td @@ -948,24 +948,24 @@ def MappingFunc: Pass<"qubit-mapping-func", "mlir::func::FuncOp"> { Option<"device", "device", "std::string", /*default=*/"\"-\"", "Device topology: path(N), ring(N), star(N), star(N,c), grid(w,h), " "file(/path/to/file), bypass">, - Option<"extendedLayerSize", "extendedLayerSize", "unsigned", + Option<"extendedLayerSize", "extended-layer-size", "unsigned", /*default=*/"20", "Extended layer size">, - Option<"extendedLayerWeight", "extendedLayerWeight", "float", + Option<"extendedLayerWeight", "extended-layer-weight", "float", /*default=*/"0.5", "Extended layer weight">, - Option<"decayDelta", "decayDelta", "float", /*default=*/"0.5", + Option<"decayDelta", "decay-delta", "float", /*default=*/"0.5", "Decay delta">, - Option<"roundsDecayReset", "roundsDecayReset", "unsigned", /*default=*/"5", + Option<"roundsDecayReset", "rounds-decay-reset", "unsigned", /*default=*/"5", "Number of rounds before decay is reset">, - Option<"minStallSwapBudget", "minStallSwapBudget", "unsigned", + Option<"minStallSwapBudget", "min-stall-swap-budget", "unsigned", /*default=*/"64", "Release valve floor: minimum number of consecutive swaps that route " "no gate before a stuck front layer is force-routed along a shortest " "path (advanced)">, - Option<"stallSwapBudgetPerQubit", "stallSwapBudgetPerQubit", "unsigned", + Option<"stallSwapBudgetPerQubit", "stall-swap-budget-per-qubit", "unsigned", /*default=*/"4", "Release valve scaling: per-device-qubit stall budget. The budget " - "used is max(minStallSwapBudget, stallSwapBudgetPerQubit * numQubits) " - "(advanced)">, + "used is max(min-stall-swap-budget, stall-swap-budget-per-qubit * " + "numQubits) (advanced)">, Option<"placement", "placement", "std::string", /*default=*/"\"auto\"", "Initial placement strategy: auto (propose identity and greedy " diff --git a/cudaq/lib/Optimizer/Transforms/Mapping.cpp b/cudaq/lib/Optimizer/Transforms/Mapping.cpp index 29aba6295c8..2d1410cc5a3 100644 --- a/cudaq/lib/Optimizer/Transforms/Mapping.cpp +++ b/cudaq/lib/Optimizer/Transforms/Mapping.cpp @@ -7,11 +7,11 @@ ******************************************************************************/ #include "PassDetails.h" +#include "cudaq/Optimizer/Transforms/AddMetadata.h" #include "cudaq/Optimizer/Transforms/Passes.h" #include "cudaq/Support/Device.h" #include "cudaq/Support/Handle.h" #include "cudaq/Support/Placement.h" -#include "llvm/ADT/SmallPtrSet.h" #include "llvm/ADT/SmallSet.h" #include "llvm/ADT/StringSwitch.h" #include "llvm/Support/ErrorHandling.h" @@ -19,7 +19,6 @@ #include "llvm/Support/ScopedPrinter.h" #include "mlir/Analysis/TopologicalSortUtils.h" #include "mlir/Dialect/Func/IR/FuncOps.h" -#include "mlir/Interfaces/CallInterfaces.h" namespace cudaq::opt { #define GEN_PASS_DEF_MAPPINGFUNC @@ -51,28 +50,44 @@ std::optional parsePlacementStrategy(llvm::StringRef name) { .Default(std::nullopt); } -/// A symmetric weighted interaction graph over virtual qubits. Each entry -/// counts the two-qubit gates acting on a pair of virtual qubits. It is stored -/// as a dense `n x n` matrix so the placer can query any pair in O(1). +/// A symmetric weighted interaction graph over virtual qubits. Each edge counts +/// the two-qubit gates acting on a pair of virtual qubits. It is stored as +/// per-virtual sparse adjacency, since a circuit touches far fewer than the +/// `n^2` possible pairs, and the weighted degree of each virtual is cached as +/// interactions are recorded. class VirtualInteractionGraph { public: explicit VirtualInteractionGraph(unsigned numQubits) - : counts(numQubits, SmallVector(numQubits, 0)) {} + : adjacency(numQubits), weightedDegrees(numQubits, 0) {} /// Record one two-qubit interaction between virtual qubits `v0` and `v1`. /// Self-interactions are ignored. void addInteraction(unsigned v0, unsigned v1) { if (v0 == v1) return; - ++counts[v0][v1]; - ++counts[v1][v0]; + ++adjacency[v0][v1]; + ++adjacency[v1][v0]; + ++weightedDegrees[v0]; + ++weightedDegrees[v1]; + anyInteraction = true; } - /// The number of recorded interactions between virtual qubits `u` and `v`. - unsigned count(unsigned u, unsigned v) const { return counts[u][v]; } + /// The interaction neighbors of `u`, each mapped to the number of recorded + /// interactions on that edge. + const DenseMap &neighbors(unsigned u) const { + return adjacency[u]; + } + + /// The weighted degree of `u`: the total interaction count incident to it. + unsigned weightedDegree(unsigned u) const { return weightedDegrees[u]; } + + /// Whether any interaction was recorded. + bool hasInteractions() const { return anyInteraction; } private: - SmallVector> counts; + SmallVector> adjacency; + SmallVector weightedDegrees; + bool anyInteraction = false; }; /// Builds a deterministic topology-aware initial layout by assigning highly @@ -90,11 +105,9 @@ class GreedyInitialPlacer { /// Produce the `vrToPhy` seed layout. SmallVector run() { - computeDegrees(); - // No two-qubit interactions, so every layout routes identically; return the // identity seed for a deterministic result. - if (!hasInteraction) { + if (!interactions.hasInteractions()) { for (unsigned v = 0; v < n; ++v) vrToPhy[v] = v; return vrToPhy; @@ -117,18 +130,6 @@ class GreedyInitialPlacer { } private: - /// Weighted degree of each virtual qubit: the sum of its two-qubit - /// interaction counts. Also records whether any interaction exists at all. - void computeDegrees() { - weightedDegree.assign(n, 0); - for (unsigned u = 0; u < n; ++u) { - for (unsigned v = 0; v < n; ++v) - weightedDegree[u] += interactions.count(u, v); - if (weightedDegree[u] > 0) - hasInteraction = true; - } - } - /// Physical centrality used to break ties: total distance to every other /// qubit, and connectivity degree. void computeCentrality() { @@ -180,11 +181,26 @@ class GreedyInitialPlacer { unsigned chooseSeedVirtual() const { unsigned seed = n; for (unsigned u : unplacedUserVirtuals) - if (seed == n || weightedDegree[u] > weightedDegree[seed]) + if (seed == n || + interactions.weightedDegree(u) > interactions.weightedDegree(seed)) seed = u; return seed; } + struct CandidateScore { + unsigned placedWeight = 0; + unsigned degree = 0; + unsigned index = 0; + + bool isBetterThan(const CandidateScore &other) const { + if (placedWeight != other.placedWeight) + return placedWeight > other.placedWeight; + if (degree != other.degree) + return degree > other.degree; + return index < other.index; + } + }; + /// The unplaced virtual qubit most connected to the placed set. Ties break by /// total weighted degree, then by lower virtual index for determinism. The /// disconnected case (no interaction with the placed set) reduces to highest @@ -192,21 +208,15 @@ class GreedyInitialPlacer { /// the placed weight. unsigned chooseNextVirtual() const { unsigned pick = n; - unsigned pickPlacedWeight = 0; - unsigned pickDegree = 0; + CandidateScore best; for (unsigned u : unplacedUserVirtuals) { - unsigned placedWeight = 0; - for (unsigned v : placedVirtuals) - placedWeight += interactions.count(u, v); - unsigned degree = weightedDegree[u]; - bool better = pick == n || placedWeight > pickPlacedWeight || - (placedWeight == pickPlacedWeight && degree > pickDegree) || - (placedWeight == pickPlacedWeight && degree == pickDegree && - u < pick); - if (better) { + CandidateScore score{0, interactions.weightedDegree(u), u}; + for (const auto &edge : interactions.neighbors(u)) + if (placedVirtual[edge.first]) + score.placedWeight += edge.second; + if (pick == n || score.isBetterThan(best)) { pick = u; - pickPlacedWeight = placedWeight; - pickDegree = degree; + best = score; } } assert(pick != n && "chooseNextVirtual called with no unplaced user qubits"); @@ -223,10 +233,10 @@ class GreedyInitialPlacer { unsigned bestCost = 0; for (unsigned p : freePhysicals) { unsigned cost = 0; - for (unsigned w : placedVirtuals) - if (interactions.count(v, w) > 0) - cost += interactions.count(v, w) * - device.getDistance(Qubit(p), Qubit(vrToPhy[w])); + for (const auto &edge : interactions.neighbors(v)) + if (placedVirtual[edge.first]) + cost += edge.second * + device.getDistance(Qubit(p), Qubit(vrToPhy[edge.first])); bool better = bestPhy == n || cost < bestCost || (cost == bestCost && isMoreCentralPhysical(p, bestPhy)); if (better) { @@ -242,7 +252,6 @@ class GreedyInitialPlacer { void place(unsigned v, unsigned p) { vrToPhy[v] = p; placedVirtual[v] = true; - placedVirtuals.push_back(v); freePhysicals.erase(llvm::lower_bound(freePhysicals, p)); if (auto it = llvm::lower_bound(unplacedUserVirtuals, v); it != unplacedUserVirtuals.end() && *it == v) @@ -264,8 +273,6 @@ class GreedyInitialPlacer { ArrayRef userVirtualQubits; const unsigned n; - SmallVector weightedDegree; - bool hasInteraction = false; SmallVector distanceSum; SmallVector physDegree; @@ -273,24 +280,12 @@ class GreedyInitialPlacer { SmallVector vrToPhy; // Worklists maintained by `place`, so the selection helpers iterate only the - // relevant qubits instead of rescanning the full device. `freePhysicals` and - // `unplacedUserVirtuals` stay sorted ascending, while `placedVirtuals` is in - // placement order. + // relevant qubits instead of rescanning the full device. Both stay sorted + // ascending. SmallVector freePhysicals; - SmallVector placedVirtuals; SmallVector unplacedUserVirtuals; }; -/// Greedy initial placement over the circuit interaction graph. Returns a -/// `vrToPhy` array proposing a starting layout for the router (the greedy -/// seed). -SmallVector -interactionPlacement(const cudaq::Device &device, - const VirtualInteractionGraph &interactions, - ArrayRef userVirtualQubits) { - return GreedyInitialPlacer(device, interactions, userVirtualQubits).run(); -} - /// Generate the seed layouts to try, in deterministic order. Each seed only /// proposes a starting vrToPhy. The router decides the rest. `interactions` is /// required for the greedy strategies and ignored for identity. @@ -299,36 +294,29 @@ buildPlacementSeeds(PlacementStrategy strategy, unsigned numV, const cudaq::Device &device, const std::optional &interactions, ArrayRef userVirtualQubits) { - auto identitySeed = [&]() { - SmallVector seed(numV); + SmallVector> seeds; + + if (strategy == PlacementStrategy::Auto || + strategy == PlacementStrategy::Identity) { + SmallVector identity(numV); for (unsigned v = 0; v < numV; ++v) - seed[v] = v; - return seed; - }; - auto greedySeed = [&]() { + identity[v] = v; + seeds.push_back(std::move(identity)); + } + + if (strategy == PlacementStrategy::Auto || + strategy == PlacementStrategy::Greedy) { assert(interactions.has_value() && "greedy placement requires collected interactions"); - return interactionPlacement(device, *interactions, userVirtualQubits); - }; - - SmallVector> seeds; - switch (strategy) { - case PlacementStrategy::Auto: { - seeds.push_back(identitySeed()); - // Greedy degenerates to identity when there are no interactions to place, - // so routing it again would just repeat the identity pass. - SmallVector greedy = greedySeed(); - if (greedy != seeds.front()) + SmallVector greedy = + GreedyInitialPlacer(device, *interactions, userVirtualQubits).run(); + // For `auto`, greedy degenerates to identity when there are no interactions + // to place, so skip the duplicate rather than route the identity layout + // twice. + if (strategy == PlacementStrategy::Greedy || greedy != seeds.front()) seeds.push_back(std::move(greedy)); - break; - } - case PlacementStrategy::Identity: - seeds.push_back(identitySeed()); - break; - case PlacementStrategy::Greedy: - seeds.push_back(greedySeed()); - break; } + return seeds; } @@ -620,8 +608,8 @@ class SabreRouter { /// front layer would otherwise loop forever. Discard the current episode's /// swaps and force the closest gate together so the walk always makes /// progress. The decay state is left as is. It is a soft heuristic and resets - /// on its own cycle. This follows the release-valve idea from Qiskit and - /// LightSABRE (arXiv:2409.08368). + /// on its own cycle. This follows the release-valve idea from LightSABRE + /// (arXiv:2409.08368). void applyReleaseValve(SmallVectorImpl &episodeSwaps); private: @@ -914,8 +902,8 @@ RoutingResult SabreRouter::route() { // heuristic several times that worst-case direct-routing cost to explore and // recover before the valve fires. The floor keeps a usable budget on small // devices, where the scaled term would otherwise be too tight. Both terms are - // pass options (`minStallSwapBudget`, `stallSwapBudgetPerQubit`) defaulting to - // 64 and 4. + // pass options (`min-stall-swap-budget`, `stall-swap-budget-per-qubit`) + // defaulting to 64 and 4. const unsigned stallSwapLimit = std::max( minStallSwapBudget, stallSwapBudgetPerQubit * device.getNumQubits()); std::size_t numSwapSearches = 0; @@ -995,11 +983,6 @@ std::optional parseSearchStrategy(llvm::StringRef name) { /// fewest routed swaps wins, compared through `isBetter`. No IR is touched /// here. class RoutingSearchStrategy { - /// Reverse-traversal initial-mapping refinement from the SABRE paper (Li et - /// al. 2019, Sec. IV-C2): route forward, route backward from the resulting - /// layout, then forward again. - static constexpr unsigned numTraversals = 3; - public: RoutingSearchStrategy(const cudaq::Device &device, const RoutingProblem &problem, bool refine, @@ -1056,27 +1039,22 @@ class RoutingSearchStrategy { ArrayRef seed, unsigned numV, unsigned numPhy, llvm::function_ref consider) { + // SABRE's forward-reverse-forward reverse-traversal refinement (Li et al. + // 2019, Sec. IV-C2). + // Traversal 1 (forward): route the seed as given. A candidate. cudaq::Placement finalPlace(numV, numPhy); consider(routeSeed(seed, numV, numPhy, finalPlace), finalPlace); if (!refine) return; - // The remaining traversals alternate reverse and forward. A reverse pass - // only refines the current layout into the next seed. The forward pass that - // follows routes that seed and is the actual candidate. - SmallVector current(seed.begin(), seed.end()); - cudaq::Placement currentFinal = finalPlace; - for (unsigned t = 2; t <= numTraversals; ++t) { - bool isReversePass = (t % 2 == 0); - if (isReversePass) { - current = reverseRefine(currentFinal, numV); - } else { - cudaq::Placement nextFinal(numV, numPhy); - consider(routeSeed(current, numV, numPhy, nextFinal), nextFinal); - currentFinal = nextFinal; - } - } + // Traversal 2 (reverse): route the reverse circuit from that layout to + // refine it into the next seed. Not a candidate. + SmallVector refined = reverseRefine(finalPlace, numV); + + // Traversal 3 (forward): route the refined seed. A candidate. + cudaq::Placement refinedFinal(numV, numPhy); + consider(routeSeed(refined, numV, numPhy, refinedFinal), refinedFinal); } /// Forward-route a seed layout. Returns its result and final placement. @@ -1360,98 +1338,25 @@ struct MappingPrep : public cudaq::opt::impl::MappingPrepBase { // Measurement preconditions //===----------------------------------------------------------------------===// -/// Walk back through pointer arithmetic and casts to the `cc.alloca` a pointer -/// ultimately refers to, or null. Mirrors the helper in AddMetadata. -cudaq::cc::AllocaOp seekAllocaFrom(Value v); -cudaq::cc::AllocaOp seekAllocaFrom(Operation *op) { - if (!op) - return {}; - if (auto alloca = dyn_cast(op)) - return alloca; - if (auto cp = dyn_cast(op)) - return seekAllocaFrom(cp.getBase()); - if (auto castOp = dyn_cast(op)) - if (isa(castOp.getOperand().getType())) - return seekAllocaFrom(castOp.getValue()); - return {}; -} -cudaq::cc::AllocaOp seekAllocaFrom(Value v) { - if (!v) - return {}; - return seekAllocaFrom(v.getDefiningOp()); -} - -/// Return true if a measurement-derived classical value reaches control flow, -/// quantum work, or a call. This mirrors AddMetadata's measurement-dependence -/// check, including simple store/load forwarding through allocas. -bool measurementFeedsControlOrQuantum(Operation *meas) { - SmallVector worklist; - llvm::SmallPtrSet seen; - auto enqueue = [&](Operation *op) { - if (op && seen.insert(op).second) - worklist.push_back(op); - }; - auto enqueueUsers = [&](Value v) { - for (Operation *user : v.getUsers()) - enqueue(user); - }; - for (Value result : meas->getResults()) - if (!isa(result.getType())) - enqueueUsers(result); - while (!worklist.empty()) { - Operation *op = worklist.pop_back_val(); - if (isa(op)) - return true; - // A store has no SSA result, so follow the value through memory: a later - // load from the same alloca can carry the measurement into control flow. - if (auto storeOp = dyn_cast(op)) { - if (auto alloca = seekAllocaFrom(storeOp.getPtrvalue())) - enqueue(alloca.getOperation()); - continue; - } - for (Value result : op->getResults()) - enqueueUsers(result); - } - return false; -} - -/// A measurement the mapper cannot support, paired with a diagnostic message. -struct UnsupportedMeasurement { - Operation *op; - const char *message; -}; - -/// The first measurement in `func` whose results are used in a way the mapper -/// cannot support, or nullopt when every measurement is a terminal readout. -/// Because the mapper defers measurements to the end of the circuit, it only -/// supports measured wires that flow to a terminal consumer (`return_wire` or -/// `sink`) and measurement results that do not feed control flow, quantum -/// dataflow, or calls. -std::optional -findUnsupportedMeasurement(func::FuncOp func) { - std::optional found; +/// The first measurement in `func` whose measured wire flows to a non-terminal +/// user, or null. Because the mapper defers measurements to the end of the +/// circuit, a measured wire may only flow to a terminal consumer (`return_wire` +/// or `sink`). Any other use is a mid-circuit measurement the deferral cannot +/// preserve. Classical measurement feedback is detected separately, via +/// `QuakeFunctionAnalysis`. +Operation *findNonTerminalMeasuredWireUse(func::FuncOp func) { + Operation *found = nullptr; func.walk([&](cudaq::quake::MeasurementInterface meas) { Operation *measOp = meas.getOperation(); - // A measured wire may only flow to a terminal wire consumer. Anything else - // is a mid-circuit measurement the deferral cannot preserve. for (Value result : measOp->getResults()) { if (!isa(result.getType())) continue; for (Operation *user : result.getUsers()) if (!isa(user)) { - found = {measOp, "unsupported mid-circuit measurement: a measured " - "wire is used by a later operation"}; + found = measOp; return WalkResult::interrupt(); } } - if (measurementFeedsControlOrQuantum(measOp)) { - found = {measOp, "unsupported measurement-controlled operation: a " - "measurement result feeds control flow, a quantum " - "operation, or a call"}; - return WalkResult::interrupt(); - } return WalkResult::advance(); }); return found; @@ -1482,6 +1387,58 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { addOpAndUsersToList(user, opsToMoveToEnd); } + /// Resolve `op`'s quantum operands to their virtual qubits. Returns nullopt + /// after diagnosing (under `nonComposable`) when a supported op consumes an + /// untracked wire, which means some earlier unsupported op produced it. Do + /// not let DenseMap default a missing entry to virtual qubit 0. + std::optional> + lookupVirtualOperands( + Operation &op, ValueRange wireOperands, + const DenseMap &wireToVirtualQ) { + SmallVector virtualOperands; + virtualOperands.reserve(wireOperands.size()); + for (Value wire : wireOperands) { + auto virtualQ = lookupVirtualQ(wireToVirtualQ, wire); + if (!virtualQ) { + if (nonComposable) { + op.emitOpError("has a quantum operand that is not tracked by " + "the mapper"); + signalPassFailure(); + } + LLVM_DEBUG(llvm::dbgs() << "untracked quantum operand in mapper\n"); + return std::nullopt; + } + virtualOperands.push_back(*virtualQ); + } + return virtualOperands; + } + + /// Map `op`'s result wires onto the virtual qubits carried by its operands, + /// updating `wireToVirtualQ` and `finalQubitWire`. Fails (diagnosing under + /// `nonComposable`) when the operand and result wire counts disagree. + LogicalResult recordQuantumResults( + Operation &op, ValueRange wireOperands, + ArrayRef virtualOperands, + DenseMap &wireToVirtualQ, + DenseMap &finalQubitWire) { + auto wireResults = cudaq::quake::getQuantumResults(&op); + if (!wireResults.empty() && wireResults.size() != wireOperands.size()) { + if (nonComposable) { + op.emitOpError("has a different number of quantum operands and " + "quantum results"); + signalPassFailure(); + } + LLVM_DEBUG(llvm::dbgs() << "quantum operand/result arity mismatch\n"); + return failure(); + } + for (auto &&[index, newWire] : llvm::enumerate(wireResults)) { + cudaq::Placement::VirtualQ virtualQ = virtualOperands[index]; + wireToVirtualQ.insert({newWire, virtualQ}); + finalQubitWire[virtualQ.index] = newWire; + } + return success(); + } + void runOnOperation() override { if (deviceBypass) return; @@ -1661,26 +1618,14 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { return; } - // Get the wire operands and their virtual qubits. If a supported op - // consumes an untracked wire, some earlier unsupported operation must - // have produced it. Do not let DenseMap default that to virtual qubit 0. + // Get the wire operands and their virtual qubits. auto wireOperands = cudaq::quake::getQuantumOperands(&op); - SmallVector virtualOperands; - virtualOperands.reserve(wireOperands.size()); - for (Value wire : wireOperands) { - auto virtualQ = lookupVirtualQ(wireToVirtualQ, wire); - if (!virtualQ) { - if (nonComposable) { - op.emitOpError("has a quantum operand that is not tracked by " - "the mapper"); - signalPassFailure(); - } - LLVM_DEBUG(llvm::dbgs() - << "untracked quantum operand in mapper\n"); - return; - } - virtualOperands.push_back(*virtualQ); - } + auto maybeVirtualOperands = + lookupVirtualOperands(op, wireOperands, wireToVirtualQ); + if (!maybeVirtualOperands) + return; + SmallVector virtualOperands = + std::move(*maybeVirtualOperands); // Since `quake.return_wire` operations do not generate new wires, we // don't need to further analyze. @@ -1716,22 +1661,9 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { } // Map the result wires to the appropriate virtual qubits. - auto wireResults = cudaq::quake::getQuantumResults(&op); - if (!wireResults.empty() && wireResults.size() != wireOperands.size()) { - if (nonComposable) { - op.emitOpError("has a different number of quantum operands and " - "quantum results"); - signalPassFailure(); - } - LLVM_DEBUG(llvm::dbgs() - << "quantum operand/result arity mismatch\n"); + if (failed(recordQuantumResults(op, wireOperands, virtualOperands, + wireToVirtualQ, finalQubitWire))) return; - } - for (auto &&[index, newWire] : llvm::enumerate(wireResults)) { - cudaq::Placement::VirtualQ virtualQ = virtualOperands[index]; - wireToVirtualQ.insert({newWire, virtualQ}); - finalQubitWire[virtualQ.index] = newWire; - } } else if (!cudaq::quake::getQuantumOperands(&op).empty() || !cudaq::quake::getQuantumResults(&op).empty()) { if (nonComposable) { @@ -1755,15 +1687,27 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { return; } - // Measurement deferral is only safe for terminal readout. Reject - // unsupported mid-circuit/adaptive uses before mutating IR. - if (auto unsupported = findUnsupportedMeasurement(func)) { - if (nonComposable) { - unsupported->op->emitOpError(unsupported->message); - signalPassFailure(); - } - LLVM_DEBUG(llvm::dbgs() - << "unsupported measurement use; skipping mapping\n"); + // Measurement deferral is only safe for terminal readout. Reject unsupported + // mid-circuit/adaptive uses before mutating IR, even in composable mode. + if (Operation *measOp = findNonTerminalMeasuredWireUse(func)) { + measOp->emitOpError("unsupported mid-circuit measurement: a measured wire " + "is used by a later operation"); + signalPassFailure(); + return; + } + // Measurement-dependent behavior is the adaptive shape the mapper cannot + // preserve, so use AddMetadata's conservative measurement-dependence + // analysis. + const auto &measAnalysis = + getAnalysis(); + const auto &measInfo = measAnalysis.getAnalysisInfo(); + auto measIt = measInfo.find(func); + assert(measIt != measInfo.end() && "missing measurement analysis for func"); + if (measIt->second.hasConditionalsOnMeasure) { + func.emitOpError("unsupported measurement-dependent behavior: " + "measurement-dependent control flow, quantum operations, " + "calls, or resets cannot be preserved by qubit mapping"); + signalPassFailure(); return; } @@ -1939,17 +1883,21 @@ namespace cudaq::opt { struct MappingPipelineOptions : public PassPipelineOptions { -#define DECLARE_SUB_OPTION(_PARENT_STRUCT, _FIELD) \ - PassOptions::Option _FIELD{*this, #_FIELD} - DECLARE_SUB_OPTION(MappingPrepOptions, device); - DECLARE_SUB_OPTION(MappingFuncOptions, extendedLayerSize); - DECLARE_SUB_OPTION(MappingFuncOptions, extendedLayerWeight); - DECLARE_SUB_OPTION(MappingFuncOptions, decayDelta); - DECLARE_SUB_OPTION(MappingFuncOptions, roundsDecayReset); - DECLARE_SUB_OPTION(MappingFuncOptions, minStallSwapBudget); - DECLARE_SUB_OPTION(MappingFuncOptions, stallSwapBudgetPerQubit); - DECLARE_SUB_OPTION(MappingFuncOptions, placement); - DECLARE_SUB_OPTION(MappingFuncOptions, search); +#define DECLARE_SUB_OPTION(_PARENT_STRUCT, _FIELD, _NAME) \ + PassOptions::Option _FIELD{*this, _NAME} + DECLARE_SUB_OPTION(MappingPrepOptions, device, "device"); + DECLARE_SUB_OPTION(MappingFuncOptions, extendedLayerSize, + "extended-layer-size"); + DECLARE_SUB_OPTION(MappingFuncOptions, extendedLayerWeight, + "extended-layer-weight"); + DECLARE_SUB_OPTION(MappingFuncOptions, decayDelta, "decay-delta"); + DECLARE_SUB_OPTION(MappingFuncOptions, roundsDecayReset, "rounds-decay-reset"); + DECLARE_SUB_OPTION(MappingFuncOptions, minStallSwapBudget, + "min-stall-swap-budget"); + DECLARE_SUB_OPTION(MappingFuncOptions, stallSwapBudgetPerQubit, + "stall-swap-budget-per-qubit"); + DECLARE_SUB_OPTION(MappingFuncOptions, placement, "placement"); + DECLARE_SUB_OPTION(MappingFuncOptions, search, "search"); PassOptions::Option nonComposable{*this, "raise-fatal-errors"}; }; diff --git a/cudaq/test/Transforms/mapping_errors.qke b/cudaq/test/Transforms/mapping_errors.qke index da8e95aa997..eac931b9146 100644 --- a/cudaq/test/Transforms/mapping_errors.qke +++ b/cudaq/test/Transforms/mapping_errors.qke @@ -86,12 +86,13 @@ func.func @test_04() { quake.wire_set @wires_05[10] // A measurement result feeding control flow through a store/load is the -// adaptive/feed-forward shape CUDA-Q lowering produces, and is unsupported. +// adaptive/feed-forward shape CUDA-Q lowering produces, and is unsupported. The +// feedback diagnostic is reported on the function, not the measurement op. +// expected-error @+1 {{unsupported measurement-dependent behavior}} func.func @test_05() { %0 = quake.borrow_wire @wires_05[0] : !quake.wire %1 = quake.borrow_wire @wires_05[1] : !quake.wire %2:2 = quake.x [%0] %1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) - // expected-error @+1 {{unsupported measurement-controlled operation}} %meas, %wire = quake.mz %2#0 : (!quake.wire) -> (!quake.measure, !quake.wire) %b = quake.discriminate %meas : (!quake.measure) -> i1 %slot = cc.alloca i1 diff --git a/cudaq/test/Transforms/mapping_measurement_composable.qke b/cudaq/test/Transforms/mapping_measurement_composable.qke new file mode 100644 index 00000000000..baac8c07b46 --- /dev/null +++ b/cudaq/test/Transforms/mapping_measurement_composable.qke @@ -0,0 +1,28 @@ +// ========================================================================== // +// Copyright (c) 2026 NVIDIA Corporation & Affiliates. // +// All rights reserved. // +// // +// This source code and the accompanying materials are made available under // +// the terms of the Apache License 2.0 which accompanies this distribution. // +// ========================================================================== // + +// The mapper defers measurements to the end of the circuit, so an unsupported +// mid-circuit measurement must fail the pass even in composable mode (with +// raise-fatal-errors unset) rather than silently passing through an unmapped +// circuit whose semantics it cannot preserve. + +// RUN: not cudaq-opt '--qubit-mapping=device=path(3)' %s 2>&1 | FileCheck %s + +quake.wire_set @wires[10] + +func.func @midcircuit_measure_default() { + %0 = quake.borrow_wire @wires[0] : !quake.wire + %1 = quake.borrow_wire @wires[1] : !quake.wire + %2:2 = quake.x [%0] %1 : (!quake.wire, !quake.wire) -> (!quake.wire, !quake.wire) + // CHECK: unsupported mid-circuit measurement + %meas, %wire = quake.mz %2#0 : (!quake.wire) -> (!quake.measure, !quake.wire) + %3 = quake.h %wire : (!quake.wire) -> !quake.wire + quake.return_wire %3 : !quake.wire + quake.return_wire %2#1 : !quake.wire + return +} diff --git a/cudaq/test/Transforms/mapping_search_termination.qke b/cudaq/test/Transforms/mapping_search_termination.qke index 9931ea8f5dc..63561b5a496 100644 --- a/cudaq/test/Transforms/mapping_search_termination.qke +++ b/cudaq/test/Transforms/mapping_search_termination.qke @@ -13,10 +13,10 @@ // SABRE: on this circuit the forced route uses 5 swaps where the unrestricted // heuristic uses 3. CircuitCheck confirms both outputs are correct mappings. -// RUN: cudaq-opt '--qubit-mapping=device=path(6) placement=identity search=none minStallSwapBudget=1 stallSwapBudgetPerQubit=0' %s | FileCheck --check-prefix=VALVE %s -// RUN: cudaq-opt '--qubit-mapping=device=path(6) placement=identity search=none minStallSwapBudget=1 stallSwapBudgetPerQubit=0' %s | CircuitCheck --up-to-mapping %s -// RUN: cudaq-opt '--qubit-mapping=device=path(6) placement=identity search=none minStallSwapBudget=16 stallSwapBudgetPerQubit=0' %s | FileCheck --check-prefix=NOVALVE %s -// RUN: cudaq-opt '--qubit-mapping=device=path(6) placement=identity search=none minStallSwapBudget=16 stallSwapBudgetPerQubit=0' %s | CircuitCheck --up-to-mapping %s +// RUN: cudaq-opt '--qubit-mapping=device=path(6) placement=identity search=none min-stall-swap-budget=1 stall-swap-budget-per-qubit=0' %s | FileCheck --check-prefix=VALVE %s +// RUN: cudaq-opt '--qubit-mapping=device=path(6) placement=identity search=none min-stall-swap-budget=1 stall-swap-budget-per-qubit=0' %s | CircuitCheck --up-to-mapping %s +// RUN: cudaq-opt '--qubit-mapping=device=path(6) placement=identity search=none min-stall-swap-budget=16 stall-swap-budget-per-qubit=0' %s | FileCheck --check-prefix=NOVALVE %s +// RUN: cudaq-opt '--qubit-mapping=device=path(6) placement=identity search=none min-stall-swap-budget=16 stall-swap-budget-per-qubit=0' %s | CircuitCheck --up-to-mapping %s quake.wire_set @wires[2147483647] func.func @release_valve() { From ae79a1d6626213ce3425416bb296a89efa8b4d69 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Thu, 18 Jun 2026 21:34:30 -0300 Subject: [PATCH 5/7] Format mapping pass changes Signed-off-by: Thomas Alexander --- cudaq/lib/Optimizer/Transforms/Mapping.cpp | 45 ++++++++++++---------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/cudaq/lib/Optimizer/Transforms/Mapping.cpp b/cudaq/lib/Optimizer/Transforms/Mapping.cpp index 2d1410cc5a3..d76c68d5821 100644 --- a/cudaq/lib/Optimizer/Transforms/Mapping.cpp +++ b/cudaq/lib/Optimizer/Transforms/Mapping.cpp @@ -204,8 +204,8 @@ class GreedyInitialPlacer { /// The unplaced virtual qubit most connected to the placed set. Ties break by /// total weighted degree, then by lower virtual index for determinism. The /// disconnected case (no interaction with the placed set) reduces to highest - /// weighted degree, then lower index, since weighted degree is always at least - /// the placed weight. + /// weighted degree, then lower index, since weighted degree is always at + /// least the placed weight. unsigned chooseNextVirtual() const { unsigned pick = n; CandidateScore best; @@ -219,7 +219,8 @@ class GreedyInitialPlacer { best = score; } } - assert(pick != n && "chooseNextVirtual called with no unplaced user qubits"); + assert(pick != n && + "chooseNextVirtual called with no unplaced user qubits"); return pick; } @@ -842,8 +843,7 @@ void SabreRouter::forceClosestGate() { closest = n; } } - assert(closest.isValid() && - "a stalled front layer must hold a 2-qubit gate"); + assert(closest.isValid() && "a stalled front layer must hold a 2-qubit gate"); const RoutingProblem::Node &node = problem[closest]; cudaq::Device::Path path = device.getShortestPath( placement.getPhy(node.qubits[0]), placement.getPhy(node.qubits[1])); @@ -1670,8 +1670,7 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { op.emitOpError("is not supported by the mapper"); signalPassFailure(); } - LLVM_DEBUG(llvm::dbgs() - << "unsupported quantum operation in mapper\n"); + LLVM_DEBUG(llvm::dbgs() << "unsupported quantum operation in mapper\n"); return; } } @@ -1687,11 +1686,13 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { return; } - // Measurement deferral is only safe for terminal readout. Reject unsupported - // mid-circuit/adaptive uses before mutating IR, even in composable mode. + // Measurement deferral is only safe for terminal readout. Reject + // unsupported mid-circuit/adaptive uses before mutating IR, even in + // composable mode. if (Operation *measOp = findNonTerminalMeasuredWireUse(func)) { - measOp->emitOpError("unsupported mid-circuit measurement: a measured wire " - "is used by a later operation"); + measOp->emitOpError( + "unsupported mid-circuit measurement: a measured wire " + "is used by a later operation"); signalPassFailure(); return; } @@ -1704,9 +1705,10 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { auto measIt = measInfo.find(func); assert(measIt != measInfo.end() && "missing measurement analysis for func"); if (measIt->second.hasConditionalsOnMeasure) { - func.emitOpError("unsupported measurement-dependent behavior: " - "measurement-dependent control flow, quantum operations, " - "calls, or resets cannot be preserved by qubit mapping"); + func.emitOpError( + "unsupported measurement-dependent behavior: " + "measurement-dependent control flow, quantum operations, " + "calls, or resets cannot be preserved by qubit mapping"); signalPassFailure(); return; } @@ -1740,9 +1742,9 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { finalQubitWire[i].getLoc(), resTy, measureOp.getMeasOut()); - wireToVirtualQ.insert({measureOp.getWires()[0], - requireVirtualQ(wireToVirtualQ, - finalQubitWire[i])}); + wireToVirtualQ.insert( + {measureOp.getWires()[0], + requireVirtualQ(wireToVirtualQ, finalQubitWire[i])}); userQubitsMeasured.push_back(i); } @@ -1773,9 +1775,9 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { const unsigned numV = sources.size(); const unsigned numPhy = deviceInstance->getNumQubits(); - SmallVector> seeds = buildPlacementSeeds( - placementStrategy, numV, *deviceInstance, interactions, - userVirtualQubits); + SmallVector> seeds = + buildPlacementSeeds(placementStrategy, numV, *deviceInstance, + interactions, userVirtualQubits); // Build the routing problem once (it does not depend on the layout), then // search over the seeds for the result with the fewest swaps. @@ -1891,7 +1893,8 @@ struct MappingPipelineOptions DECLARE_SUB_OPTION(MappingFuncOptions, extendedLayerWeight, "extended-layer-weight"); DECLARE_SUB_OPTION(MappingFuncOptions, decayDelta, "decay-delta"); - DECLARE_SUB_OPTION(MappingFuncOptions, roundsDecayReset, "rounds-decay-reset"); + DECLARE_SUB_OPTION(MappingFuncOptions, roundsDecayReset, + "rounds-decay-reset"); DECLARE_SUB_OPTION(MappingFuncOptions, minStallSwapBudget, "min-stall-swap-budget"); DECLARE_SUB_OPTION(MappingFuncOptions, stallSwapBudgetPerQubit, From b1675536e76f3c88257b26bf68a7fec91ea4cc63 Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Thu, 18 Jun 2026 21:47:05 -0300 Subject: [PATCH 6/7] Reject adaptive measurement mapping before CFG bypass Signed-off-by: Thomas Alexander --- cudaq/lib/Optimizer/Transforms/Mapping.cpp | 75 ++++++++++--------- .../mapping_measurement_composable.qke | 32 ++++++-- 2 files changed, 65 insertions(+), 42 deletions(-) diff --git a/cudaq/lib/Optimizer/Transforms/Mapping.cpp b/cudaq/lib/Optimizer/Transforms/Mapping.cpp index d76c68d5821..77f1ae06a7b 100644 --- a/cudaq/lib/Optimizer/Transforms/Mapping.cpp +++ b/cudaq/lib/Optimizer/Transforms/Mapping.cpp @@ -1461,16 +1461,6 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { return; } - // FIXME: Add the ability to handle multiple blocks. - if (blocks.size() > 1) { - if (nonComposable) { - func.emitError("The mapper cannot handle multiple blocks"); - signalPassFailure(); - } - LLVM_DEBUG(llvm::dbgs() << "NYI: mapping with multiple blocks"); - return; - } - // Verify that the function contains wiresets and return if it does not. // Also populate the highest identity borrow up as long as we're traversing // them. @@ -1509,6 +1499,44 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { return; } + // Measurement deferral is only safe for terminal readout. Reject + // unsupported mid-circuit/adaptive uses before mutating IR, even in + // composable mode. This must precede the multi-block limitation below, so + // CFG-shaped adaptive measurements cannot pass through unmapped. + if (Operation *measOp = findNonTerminalMeasuredWireUse(func)) { + measOp->emitOpError( + "unsupported mid-circuit measurement: a measured wire " + "is used by a later operation"); + signalPassFailure(); + return; + } + // Measurement-dependent behavior is the adaptive shape the mapper cannot + // preserve, so use AddMetadata's conservative measurement-dependence + // analysis. + const auto &measAnalysis = + getAnalysis(); + const auto &measInfo = measAnalysis.getAnalysisInfo(); + auto measIt = measInfo.find(func); + assert(measIt != measInfo.end() && "missing measurement analysis for func"); + if (measIt->second.hasConditionalsOnMeasure) { + func.emitOpError( + "unsupported measurement-dependent behavior: " + "measurement-dependent control flow, quantum operations, " + "calls, or resets cannot be preserved by qubit mapping"); + signalPassFailure(); + return; + } + + // FIXME: Add the ability to handle multiple blocks. + if (blocks.size() > 1) { + if (nonComposable) { + func.emitError("The mapper cannot handle multiple blocks"); + signalPassFailure(); + } + LLVM_DEBUG(llvm::dbgs() << "NYI: mapping with multiple blocks"); + return; + } + // Sanity checks and create a wire to virtual qubit mapping. Block &block = *blocks.begin(); @@ -1686,33 +1714,6 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { return; } - // Measurement deferral is only safe for terminal readout. Reject - // unsupported mid-circuit/adaptive uses before mutating IR, even in - // composable mode. - if (Operation *measOp = findNonTerminalMeasuredWireUse(func)) { - measOp->emitOpError( - "unsupported mid-circuit measurement: a measured wire " - "is used by a later operation"); - signalPassFailure(); - return; - } - // Measurement-dependent behavior is the adaptive shape the mapper cannot - // preserve, so use AddMetadata's conservative measurement-dependence - // analysis. - const auto &measAnalysis = - getAnalysis(); - const auto &measInfo = measAnalysis.getAnalysisInfo(); - auto measIt = measInfo.find(func); - assert(measIt != measInfo.end() && "missing measurement analysis for func"); - if (measIt->second.hasConditionalsOnMeasure) { - func.emitOpError( - "unsupported measurement-dependent behavior: " - "measurement-dependent control flow, quantum operations, " - "calls, or resets cannot be preserved by qubit mapping"); - signalPassFailure(); - return; - } - // Make all existing borrow_wire ops use the mapped wire set. func.walk([&](cudaq::quake::BorrowWireOp borrowOp) { borrowOp.setSetName(mappedWireSetName); diff --git a/cudaq/test/Transforms/mapping_measurement_composable.qke b/cudaq/test/Transforms/mapping_measurement_composable.qke index baac8c07b46..9922d7d0bfe 100644 --- a/cudaq/test/Transforms/mapping_measurement_composable.qke +++ b/cudaq/test/Transforms/mapping_measurement_composable.qke @@ -6,12 +6,12 @@ // the terms of the Apache License 2.0 which accompanies this distribution. // // ========================================================================== // -// The mapper defers measurements to the end of the circuit, so an unsupported -// mid-circuit measurement must fail the pass even in composable mode (with -// raise-fatal-errors unset) rather than silently passing through an unmapped -// circuit whose semantics it cannot preserve. +// The mapper defers measurements to the end of the circuit, so unsupported +// mid-circuit/adaptive measurements must fail the pass even in composable mode +// (with raise-fatal-errors unset) rather than silently passing through an +// unmapped circuit whose semantics it cannot preserve. -// RUN: not cudaq-opt '--qubit-mapping=device=path(3)' %s 2>&1 | FileCheck %s +// RUN: not cudaq-opt '--qubit-mapping=device=path(3)' %s -split-input-file 2>&1 | FileCheck %s quake.wire_set @wires[10] @@ -26,3 +26,25 @@ func.func @midcircuit_measure_default() { quake.return_wire %2#1 : !quake.wire return } + +// ----- + +quake.wire_set @wires[10] + +func.func @adaptive_cfg_feedback_default() { + %0 = quake.borrow_wire @wires[0] : !quake.wire + %meas, %wire = quake.mz %0 : (!quake.wire) -> (!quake.measure, !quake.wire) + %bit = quake.discriminate %meas : (!quake.measure) -> i1 + cf.cond_br %bit, ^then, ^else + +^then: + cf.br ^done + +^else: + cf.br ^done + +^done: + // CHECK: unsupported measurement-dependent behavior + quake.return_wire %wire : !quake.wire + return +} From 822d0d1f07cd2d61dcc23e1c73d7a216873eb7cc Mon Sep 17 00:00:00 2001 From: Thomas Alexander Date: Thu, 18 Jun 2026 22:33:40 -0300 Subject: [PATCH 7/7] Fix greedy placer initializer order Signed-off-by: Thomas Alexander --- cudaq/lib/Optimizer/Transforms/Mapping.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cudaq/lib/Optimizer/Transforms/Mapping.cpp b/cudaq/lib/Optimizer/Transforms/Mapping.cpp index 77f1ae06a7b..0b7c86828e2 100644 --- a/cudaq/lib/Optimizer/Transforms/Mapping.cpp +++ b/cudaq/lib/Optimizer/Transforms/Mapping.cpp @@ -101,7 +101,7 @@ class GreedyInitialPlacer { ArrayRef userVirtualQubits) : device(device), interactions(interactions), userVirtualQubits(userVirtualQubits), n(device.getNumQubits()), - vrToPhy(n, 0), placedVirtual(n, false) {} + placedVirtual(n, false), vrToPhy(n, 0) {} /// Produce the `vrToPhy` seed layout. SmallVector run() {