forked from NVIDIA/cuda-quantum
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDecompositionPatternSelection.cpp
More file actions
372 lines (320 loc) · 13.9 KB
/
Copy pathDecompositionPatternSelection.cpp
File metadata and controls
372 lines (320 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
/*******************************************************************************
* Copyright (c) 2025 - 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. *
******************************************************************************/
#include "DecompositionPatterns.h"
#include "PassDetails.h"
#include "llvm/ADT/Hashing.h"
#include "llvm/ADT/SetVector.h"
#include "llvm/ADT/StringMap.h"
#include "mlir/IR/PatternMatch.h"
#include "mlir/Transforms/DialectConversion.h"
#include <algorithm>
#include <queue>
#include <unordered_map>
#include <unordered_set>
using namespace mlir;
namespace {
// ConversionTarget and OperatorInfo, parsed from target basis strings such as
// ["x", "x(1)", "z"]
struct OperatorInfo {
StringRef name;
std::size_t numControls;
bool isAdj;
OperatorInfo(StringRef infoStr) : name(), numControls(0), isAdj(false) {
auto nameEnd = infoStr.find_first_of("(<");
name = infoStr.take_front(nameEnd);
if (nameEnd < infoStr.size())
infoStr = infoStr.drop_front(nameEnd);
if (infoStr.consume_front("<adj>"))
isAdj = true;
if (infoStr.consume_front("(")) {
infoStr = infoStr.ltrim();
if (infoStr.consume_front("n"))
numControls = std::numeric_limits<std::size_t>::max();
else
infoStr.consumeInteger(10, numControls);
assert(infoStr.trim().consume_front(")"));
}
}
bool operator==(const OperatorInfo &other) const {
return name == other.name && numControls == other.numControls &&
isAdj == other.isAdj;
}
bool isUnbounded() const {
return numControls == std::numeric_limits<std::size_t>::max();
}
/// Check if this gate matches another, treating unbounded (n) control
/// count as a wildcard that matches any concrete count.
bool matches(const OperatorInfo &other) const {
if (name != other.name || isAdj != other.isAdj)
return false;
constexpr auto unbounded = std::numeric_limits<std::size_t>::max();
if (numControls == unbounded || other.numControls == unbounded)
return true;
return numControls == other.numControls;
}
};
struct BasisTarget : public ConversionTarget {
BasisTarget(MLIRContext &context, ArrayRef<std::string> targetBasis)
: ConversionTarget(context) {
constexpr std::size_t unbounded = std::numeric_limits<std::size_t>::max();
// Parse the list of target operations and build a set of legal operations
for (const std::string &targetInfo : targetBasis)
legalOperatorSet.emplace_back(targetInfo);
addLegalDialect<arith::ArithDialect, cf::ControlFlowDialect,
cudaq::cc::CCDialect, func::FuncDialect,
math::MathDialect>();
addDynamicallyLegalDialect<cudaq::quake::QuakeDialect>([&](Operation *op) {
if (auto optor = dyn_cast<cudaq::quake::OperatorInterface>(op)) {
auto name = optor->getName().stripDialect();
for (auto info : legalOperatorSet) {
if (info.name != name)
continue;
if (info.numControls == unbounded ||
optor.getControls().size() == info.numControls)
return true;
}
return false;
}
// Handle quake.exp_pauli.
if (isa<cudaq::quake::ExpPauliOp>(op)) {
// If the target defines it as a legal op, return true, else false.
return std::find_if(legalOperatorSet.begin(), legalOperatorSet.end(),
[](auto &&el) { return el.name == "exp_pauli"; }) !=
legalOperatorSet.end();
}
return true;
});
}
SmallVector<OperatorInfo, 8> legalOperatorSet;
};
} // namespace
//===----------------------------------------------------------------------===//
// std::hash specialization for OperatorInfo
//===----------------------------------------------------------------------===//
namespace std {
template <>
struct hash<OperatorInfo> {
std::size_t operator()(const OperatorInfo &info) const {
return llvm::hash_combine(info.name, info.numControls, info.isAdj);
}
};
} // namespace std
// Computes a hash of the given unordered set using the hashes of the elements
// in the set.
template <typename T>
std::size_t computeSetHash(const std::unordered_set<T> &set) {
std::vector<std::size_t> hashes;
for (const auto &elem : set) {
hashes.push_back(std::hash<T>()(elem));
}
std::sort(hashes.begin(), hashes.end());
return llvm::hash_combine_range(hashes.begin(), hashes.end());
}
namespace {
//===----------------------------------------------------------------------===//
// Decomposition Graph for Pattern Selection
//===----------------------------------------------------------------------===//
/// DecompositionGraph constructs a hypergraph of decomposition patterns based
/// on pattern metadata and performs backward traversal to select patterns that
/// decompose to a basis.
///
/// Specifically, the decomposition graph is defined as a hypergraph in which
/// nodes are gate types and hyperedges are rewrite patterns connecting the
/// matched gate type to all newly inserted gate types.
class DecompositionGraph {
public:
DecompositionGraph() = default;
/// Construct a decomposition pattern graph from a collection of pattern
/// types.
DecompositionGraph(
llvm::StringMap<std::unique_ptr<cudaq::DecompositionPatternType>>
patterns)
: patternTypes(std::move(patterns)) {
// Build the graph from pattern metadata
for (const auto &pattern : patternTypes) {
auto targetGates = pattern.getValue()->getTargetOps();
for (const auto &targetGate : targetGates)
targetToPatterns[targetGate].push_back(pattern.getKey().str());
}
}
/// Create a DecompositionGraph from the registry entries.
static DecompositionGraph fromRegistry() {
llvm::StringMap<std::unique_ptr<cudaq::DecompositionPatternType>> patterns;
for (const auto &patternType :
cudaq::DecompositionPatternTypeRegistry::entries()) {
patterns.insert({patternType.getName(), patternType.instantiate()});
}
return DecompositionGraph(std::move(patterns));
}
/// Return all patterns that have the given gate as one of their targets.
/// Uses OperatorInfo::matches() to handle unbounded (n) control counts.
llvm::SmallVector<std::string>
incomingPatterns(const OperatorInfo &gate) const {
llvm::SmallVector<std::string> result;
for (const auto &[key, patterns] : targetToPatterns) {
if (key.matches(gate))
result.append(patterns.begin(), patterns.end());
}
return result;
}
/// Select subset of patterns relevant to decomposing to the given basis
/// gates.
///
/// The result of the pattern selection are cached, so that successive calls
/// with the same arguments will be O(1).
///
/// @param patterns The pattern set to add the selected patterns to
/// @param basisGates The basis gates to decompose to
/// @param disabledPatterns The patterns to disable
void selectPatterns(RewritePatternSet &patterns,
const std::unordered_set<OperatorInfo> &basisGates,
const std::unordered_set<std::string> &disabledPatterns) {
auto hashVal = llvm::hash_combine(computeSetHash(basisGates),
computeSetHash(disabledPatterns));
if (!patternSelectionCache.contains(hashVal)) {
patternSelectionCache[hashVal] =
computePatternSelection(basisGates, disabledPatterns);
}
for (const auto &patternName : patternSelectionCache[hashVal]) {
const auto &pattern = getPatternType(patternName);
// Patterns with unbounded (n) control counts get lower benefit so
// that specific patterns (e.g., CR1ToCX for r1(1)) are preferred
// when both match the same op.
OperatorInfo sourceInfo(pattern->getSourceOp());
PatternBenefit benefit = sourceInfo.isUnbounded() ? 1 : 2;
patterns.add(pattern->create(patterns.getContext(), benefit));
}
}
private:
const std::unique_ptr<cudaq::DecompositionPatternType> &
getPatternType(const std::string &patternName) const {
auto patternType = patternTypes.find(patternName);
assert(patternType != patternTypes.end() && "pattern not found");
return patternType->getValue();
}
/// Use Dijkstra's algorithm to compute the shortest decomposition path from
/// every reachable gate type to the basis gates.
///
/// This selects a unique decomposition path for each gate in the past of the
/// basis gates in the decomposition graph, such that the number of patterns
/// applied is minimized. `disabledPatterns` are ignored during the traversal
/// and hence never selected.
///
/// @param basisGates The set of basis gates to decompose to
/// @param disabledPatterns The patterns to disable
/// @return A vector of selected pattern names
std::vector<std::string> computePatternSelection(
const std::unordered_set<OperatorInfo> &basisGates,
const std::unordered_set<std::string> &disabledPatterns) const {
// An element in the priority queue of the Dijkstra algorithm (ordered by
// smallest distance)
struct GateDistancePair {
OperatorInfo gate;
std::size_t distance;
std::optional<std::string> outgoingPattern;
bool operator<(const GateDistancePair &other) const {
// We want to order by smallest distance, so we invert the comparison
return distance > other.distance;
}
};
// Map: visited gate -> distance from the basis gates
std::unordered_map<OperatorInfo, std::size_t> visitedGates;
// The set of selected patterns to return
std::vector<std::string> selectedPatterns;
// Priority queue of gates to visit, sorted by smallest distance from the
// basis gates
std::priority_queue<GateDistancePair> gatesToVisit;
// Initialize the priority queue with the basis gates
for (const auto &gate : basisGates) {
gatesToVisit.push({gate, 0, std::nullopt});
}
/// Find the distance for a gate, handling unbounded (n) control counts.
/// Exact hash lookup first for the common case, then a scan when the
/// query or any visited entry uses unbounded controls.
auto findGateDist = [&](const OperatorInfo &gate) -> std::size_t {
auto it = visitedGates.find(gate);
if (it != visitedGates.end())
return it->second;
// Scan for wildcard matches (either side could be unbounded).
std::size_t best = std::numeric_limits<std::size_t>::max();
for (const auto &[visited, dist] : visitedGates) {
if (visited.matches(gate))
best = std::min(best, dist);
}
return best;
};
/// Compute the maximum distance from a pattern's targets to the basis
/// gates.
auto getPatternDist = [&](const auto &pattern) {
auto targetGates = pattern->getTargetOps();
std::vector<std::size_t> targetDistances;
for (const auto &targetGate : targetGates)
targetDistances.push_back(findGateDist(targetGate));
return *std::max_element(targetDistances.begin(), targetDistances.end());
};
while (!gatesToVisit.empty()) {
auto [gate, dist, outgoingPattern] = gatesToVisit.top();
gatesToVisit.pop();
auto [_, success] = visitedGates.insert({gate, dist});
if (!success) {
// Gate already visited
continue;
}
if (outgoingPattern.has_value()) {
selectedPatterns.push_back(*outgoingPattern);
}
for (const auto &patternName : incomingPatterns(gate)) {
if (disabledPatterns.contains(patternName)) {
// Ignore disabled patterns
continue;
}
const auto &pattern = getPatternType(patternName);
std::size_t dist = getPatternDist(pattern);
if (dist < std::numeric_limits<std::size_t>::max()) {
gatesToVisit.push({pattern->getSourceOp(), dist + 1, patternName});
}
}
}
return selectedPatterns;
}
//===--------------------------------------------------------------------===//
// Data structures for the graph definition
//===--------------------------------------------------------------------===//
/// All pattern types in the graph, keyed by pattern name.
llvm::StringMap<std::unique_ptr<cudaq::DecompositionPatternType>>
patternTypes;
/// Map: target gate -> patterns that produce it
std::unordered_map<OperatorInfo, SmallVector<std::string>> targetToPatterns;
//===--------------------------------------------------------------------===//
// Other data (cache)
//===--------------------------------------------------------------------===//
/// Cache for `selectPatterns`: hash of basis gates, disabled patterns,
/// enabled patterns -> selected patterns
std::unordered_map<std::size_t, std::vector<std::string>>
patternSelectionCache;
};
} // namespace
std::unique_ptr<ConversionTarget>
cudaq::createBasisTarget(MLIRContext &context,
ArrayRef<std::string> targetBasis) {
return std::make_unique<BasisTarget>(context, targetBasis);
}
void cudaq::selectDecompositionPatterns(
RewritePatternSet &patterns, ArrayRef<std::string> targetBasis,
ArrayRef<std::string> disabledPatterns) {
// Static local graph - constructed once and reused
static DecompositionGraph graph = DecompositionGraph::fromRegistry();
BasisTarget target(*patterns.getContext(), targetBasis);
// Convert targetBasis, disabledPatterns and enabledPatterns to sets for O(1)
// lookup
std::unordered_set<OperatorInfo> basisGatesSet(
target.legalOperatorSet.begin(), target.legalOperatorSet.end());
std::unordered_set<std::string> disabledPatternsSet(disabledPatterns.begin(),
disabledPatterns.end());
return graph.selectPatterns(patterns, basisGatesSet, disabledPatternsSet);
}