diff --git a/cudaq/include/cudaq/Optimizer/Transforms/Passes.td b/cudaq/include/cudaq/Optimizer/Transforms/Passes.td index f8ec1580aca..6ebd42856fb 100644 --- a/cudaq/include/cudaq/Optimizer/Transforms/Passes.td +++ b/cudaq/include/cudaq/Optimizer/Transforms/Passes.td @@ -948,14 +948,32 @@ 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", "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", "stall-swap-budget-per-qubit", "unsigned", + /*default=*/"4", + "Release valve scaling: per-device-qubit stall budget. The budget " + "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 " + "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..0b7c86828e2 100644 --- a/cudaq/lib/Optimizer/Transforms/Mapping.cpp +++ b/cudaq/lib/Optimizer/Transforms/Mapping.cpp @@ -7,11 +7,14 @@ ******************************************************************************/ #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/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" @@ -35,25 +38,488 @@ 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); +} + +/// 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) + : 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; + ++adjacency[v0][v1]; + ++adjacency[v1][v0]; + ++weightedDegrees[v0]; + ++weightedDegrees[v1]; + anyInteraction = true; + } + + /// 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> adjacency; + SmallVector weightedDegrees; + bool anyInteraction = false; +}; + +/// 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, + const VirtualInteractionGraph &interactions, + ArrayRef userVirtualQubits) + : device(device), interactions(interactions), + userVirtualQubits(userVirtualQubits), n(device.getNumQubits()), + placedVirtual(n, false), vrToPhy(n, 0) {} + + /// Produce the `vrToPhy` seed layout. + SmallVector run() { + // No two-qubit interactions, so every layout routes identically; return the + // identity seed for a deterministic result. + if (!interactions.hasInteractions()) { + for (unsigned v = 0; v < n; ++v) + vrToPhy[v] = v; + return vrToPhy; + } + + computeCentrality(); + initWorklists(); + + // Seed the highest-degree virtual qubit onto the most central physical + // qubit, then grow the layout around it. + place(chooseSeedVirtual(), bestFreePhysical()); + + while (!unplacedUserVirtuals.empty()) { + unsigned v = chooseNextVirtual(); + place(v, bestPhysicalFor(v)); + } + + assignRemainingVirtuals(); + return vrToPhy; + } + +private: + /// 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()); + } + } + + /// 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]) + 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 : freePhysicals) + if (best == n || isMoreCentralPhysical(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 : unplacedUserVirtuals) + 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 + /// weighted degree, then lower index, since weighted degree is always at + /// least the placed weight. + unsigned chooseNextVirtual() const { + unsigned pick = n; + CandidateScore best; + for (unsigned u : unplacedUserVirtuals) { + 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; + best = score; + } + } + 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. 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 : freePhysicals) { + unsigned cost = 0; + 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) { + bestPhy = p; + bestCost = cost; + } + } + return bestPhy; + } + + /// 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; + 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, pairing them in ascending order. `freePhysicals` stays sorted, + /// so this reproduces the ascending virtual to ascending physical pairing. + void assignRemainingVirtuals() { + unsigned next = 0; + for (unsigned v = 0; v < n; ++v) + if (!placedVirtual[v]) + vrToPhy[v] = freePhysicals[next++]; + } + + const cudaq::Device &device; + const VirtualInteractionGraph &interactions; + ArrayRef userVirtualQubits; + const unsigned n; + + SmallVector distanceSum; + SmallVector physDegree; + + SmallVector placedVirtual; + SmallVector vrToPhy; + + // Worklists maintained by `place`, so the selection helpers iterate only the + // relevant qubits instead of rescanning the full device. Both stay sorted + // ascending. + SmallVector freePhysicals; + SmallVector unplacedUserVirtuals; +}; + +/// 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) { + SmallVector> seeds; + + if (strategy == PlacementStrategy::Auto || + strategy == PlacementStrategy::Identity) { + SmallVector identity(numV); + for (unsigned v = 0; v < numV; ++v) + 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"); + 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)); + } + + return seeds; } //===----------------------------------------------------------------------===// // 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; }; +/// 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 +/// 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(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, + // 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 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) + for (Value wire : cudaq::quake::getQuantumResults(node.op)) + recordWireUsers(wire, node.successors); + for (auto borrow : sources) + 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 +/// 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 (!shouldIncludeInReverse(node)) + continue; + fwdToRev[i] = RoutingProblem::NodeRef(reverse.nodes.size()); + 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 (!shouldIncludeInReverse(node)) + continue; + unsigned unitarySuccessors = 0; + for (RoutingProblem::NodeRef s : node.successors) { + if (!shouldIncludeInReverse(forward[s])) + 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 +551,71 @@ 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), + unsigned roundsDecayReset, unsigned minStallSwapBudget, + unsigned stallSwapBudgetPerQubit) + : device(device), problem(problem), placement(placement), extendedLayerSize(extendedLayerSize), extendedLayerWeight(extendedLayerWeight), decayDelta(decayDelta), roundsDecayReset(roundsDecayReset), - phyDecay(device.getNumQubits(), 1.0), phyToWire(device.getNumQubits()), - allowMeasurementMapping(false) {} + minStallSwapBudget(minStallSwapBudget), + stallSwapBudgetPerQubit(stallSwapBudgetPerQubit), + phyDecay(device.getNumQubits(), 1.0), allowMeasurementMapping(false) {} - /// Main entry point into SabreRouter routing algorithm - void route(Block &block, ArrayRef sources); - - /// 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(); + /// 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 LightSABRE + /// (arXiv:2409.08368). + void applyReleaseValve(SmallVectorImpl &episodeSwaps); + private: const cudaq::Device &device; - WireMap &wireToVirtualQ; + const RoutingProblem &problem; cudaq::Placement &placement; // Parameters @@ -131,19 +623,25 @@ class SabreRouter { const float extendedLayerWeight; const float decayDelta; const unsigned roundsDecayReset; - - // Internal data - SmallVector frontLayer; - SmallVector extendedLayer; - SmallVector measureLayer; - llvm::SmallPtrSet measureLayerSet; + // 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; + 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 +653,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 +729,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 +738,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 +822,93 @@ SabreRouter::Swap SabreRouter::chooseSwap() { return candidates[minIdx]; } -void SabreRouter::route(Block &block, - ArrayRef sources) { +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[] = "//===-------------------------------------------===//\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; - } - - 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); - }; - + visitSuccessors(problem.sourceUsers, frontLayer); + + // 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 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 (`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; + unsigned swapsSinceRouted = 0; + SmallVector episodeSwaps; bool done = false; while (!done) { // Once frontLayer is empty, grab everything from measureLayer and go again. @@ -408,15 +927,26 @@ 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) { + applyReleaseValve(episodeSwaps); + swapsSinceRouted = 0; + continue; + } + // Add a swap numSwapSearches++; auto [phy0, phy1] = chooseSwap(); addSwap(phy0, phy1); + episodeSwaps.push_back({phy0, phy1}); + ++swapsSinceRouted; involvedPhy.clear(); // Update decay @@ -428,8 +958,218 @@ 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 { +public: + RoutingSearchStrategy(const cudaq::Device &device, + const RoutingProblem &problem, bool refine, + unsigned extendedLayerSize, float extendedLayerWeight, + 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{}) {} + + /// 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) + routeAndRefineSeed(seed, numV, numPhy, consider); + 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; + } + + /// 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) { + // 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; + + // 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. + 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, + minStallSwapBudget, stallSwapBudgetPerQubit); + 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, + minStallSwapBudget, stallSwapBudgetPerQubit); + 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; + unsigned minStallSwapBudget; + unsigned stallSwapBudgetPerQubit; + 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[requireVirtualQ(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]; @@ -594,6 +1334,34 @@ struct MappingPrep : public cudaq::opt::impl::MappingPrepBase { } }; +//===----------------------------------------------------------------------===// +// Measurement preconditions +//===----------------------------------------------------------------------===// + +/// 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(); + for (Value result : measOp->getResults()) { + if (!isa(result.getType())) + continue; + for (Operation *user : result.getUsers()) + if (!isa(user)) { + found = measOp; + return WalkResult::interrupt(); + } + } + return WalkResult::advance(); + }); + return found; +} + struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { using MappingFuncBase::MappingFuncBase; @@ -619,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; @@ -641,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. @@ -689,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(); @@ -711,6 +1559,49 @@ 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. + std::optional interactions; + SmallVector userVirtualQubits; + if (collectInteractions) { + interactions.emplace(deviceNumQubits); + 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 +1609,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) { @@ -753,6 +1646,15 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { return; } + // Get the wire operands and their virtual qubits. + auto wireOperands = cudaq::quake::getQuantumOperands(&op); + 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. if (auto rop = dyn_cast(op)) { @@ -760,9 +1662,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 " @@ -773,20 +1674,32 @@ 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 = 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; + if (failed(recordQuantumResults(op, wireOperands, virtualOperands, + wireToVirtualQ, finalQubitWire))) + return; + } 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; } } @@ -831,7 +1744,8 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { resTy, measureOp.getMeasOut()); wireToVirtualQ.insert( - {measureOp.getWires()[0], wireToVirtualQ[finalQubitWire[i]]}); + {measureOp.getWires()[0], + requireVirtualQ(wireToVirtualQ, finalQubitWire[i])}); userQubitsMeasured.push_back(i); } @@ -859,15 +1773,29 @@ struct MappingFunc : public cudaq::opt::impl::MappingFuncBase { } } - // Place - cudaq::Placement placement(sources.size(), deviceInstance->getNumQubits()); - identityPlacement(placement); - - // Route - SabreRouter router(*deviceInstance, wireToVirtualQ, placement, - extendedLayerSize, extendedLayerWeight, decayDelta, - roundsDecayReset); - router.route(*blocks.begin(), sources); + const unsigned numV = sources.size(); + const unsigned numPhy = deviceInstance->getNumQubits(); + + 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. + RoutingProblem problem = + buildRoutingProblem(block, sources, wireToVirtualQ); + RoutingSearchStrategy search( + *deviceInstance, problem, searchStrategy == SearchStrategy::Sabre, + extendedLayerSize, extendedLayerWeight, decayDelta, roundsDecayReset, + minStallSwapBudget, stallSwapBudgetPerQubit); + 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 +1809,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 +1834,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 +1851,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, @@ -958,13 +1886,22 @@ 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); +#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"}; }; @@ -992,6 +1929,10 @@ 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); 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..c110383bbef --- /dev/null +++ b/cudaq/test/Transforms/mapping_connectivity.qke @@ -0,0 +1,40 @@ +// ========================================================================== // +// 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 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_errors.qke b/cudaq/test/Transforms/mapping_errors.qke index a8c68f7d237..eac931b9146 100644 --- a/cudaq/test/Transforms/mapping_errors.qke +++ b/cudaq/test/Transforms/mapping_errors.qke @@ -62,3 +62,46 @@ 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. 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) + %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 new file mode 100644 index 00000000000..2deb5cea40d --- /dev/null +++ b/cudaq/test/Transforms/mapping_greedy_relocated_hub.qke @@ -0,0 +1,46 @@ +// ========================================================================== // +// 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 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..414e19ee3f9 --- /dev/null +++ b/cudaq/test/Transforms/mapping_invalid_placement.qke @@ -0,0 +1,27 @@ +// ========================================================================== // +// 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. // +// ========================================================================== // + +// 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..450ac23cc0a --- /dev/null +++ b/cudaq/test/Transforms/mapping_invalid_search.qke @@ -0,0 +1,27 @@ +// ========================================================================== // +// 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. // +// ========================================================================== // + +// 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_measurement_composable.qke b/cudaq/test/Transforms/mapping_measurement_composable.qke new file mode 100644 index 00000000000..9922d7d0bfe --- /dev/null +++ b/cudaq/test/Transforms/mapping_measurement_composable.qke @@ -0,0 +1,50 @@ +// ========================================================================== // +// 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 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 -split-input-file 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 +} + +// ----- + +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 +} 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..558f636423e --- /dev/null +++ b/cudaq/test/Transforms/mapping_placement_seeds.qke @@ -0,0 +1,40 @@ +// ========================================================================== // +// 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. // +// ========================================================================== // + +// 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..a4478d61802 --- /dev/null +++ b/cudaq/test/Transforms/mapping_search_late_interactions.qke @@ -0,0 +1,48 @@ +// ========================================================================== // +// 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. // +// ========================================================================== // + +// 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..dbbd05a030c --- /dev/null +++ b/cudaq/test/Transforms/mapping_search_none.qke @@ -0,0 +1,33 @@ +// ========================================================================== // +// 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. // +// ========================================================================== // + +// 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..a4670901839 --- /dev/null +++ b/cudaq/test/Transforms/mapping_search_refinement.qke @@ -0,0 +1,42 @@ +// ========================================================================== // +// 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 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..06b43aad563 --- /dev/null +++ b/cudaq/test/Transforms/mapping_search_star.qke @@ -0,0 +1,48 @@ +// ========================================================================== // +// 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 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..63561b5a496 --- /dev/null +++ b/cudaq/test/Transforms/mapping_search_termination.qke @@ -0,0 +1,50 @@ +// ========================================================================== // +// 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 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(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() { + %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 + // 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 +} + +// 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 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} {