Maestro 0.3.11
Unified interface for quantum circuit simulation
Loading...
Searching...
No Matches
GateFusion.h
Go to the documentation of this file.
1#pragma once
2
3#include "../../Types.h"
4#include <Eigen/Core>
5#include <algorithm>
6#include <cstdint>
7#include <map>
8#include <stdexcept>
9#include <unordered_map>
10#include <vector>
11
12namespace Simulators
13{
14
15// Backend-independent cache. Gate supplies Targets(), Matrix() and
16// SchmidtRank(); the same algorithm runs without matrices when preparing a
17// routing sequence.
18template <class Gate> class GateFusion
19{
20 public:
21 // Constant-size provenance: submissions are checked against the prepared
22 // source operations before entering the cache. The first/last IDs and count
23 // then detect an early flush without copying a growing gate list on every
24 // merge (which would make a long same-qubit sequence quadratic).
25 struct Sources
26 {
27 uint64_t first, last, count;
28
29 bool operator==(const Sources &other) const
30 {
31 return first == other.first && last == other.last && count == other.count;
32 }
33
34 bool operator!=(const Sources &other) const
35 {
36 return !(*this == other);
37 }
38 };
39
40 struct Block
41 {
43 Eigen::MatrixXcd matrix;
46 bool preserveStructure = false;
47 // Routing estimate without the matrix: the product of the merged gates'
48 // effective Schmidt ranks, capped at 4. Exact when one-qubit gates join a
49 // single two-qubit gate, since local gates leave the rank unchanged.
50 double schmidtRank = 1.;
51
52 bool IsSingle() const
53 {
54 return sources.count == 1;
55 }
56 };
57
58 explicit GateFusion(unsigned width = 2, bool matrices = true) : width_(width), matrices_(matrices)
59 {
60 }
61
62 bool Empty() const
63 {
64 return blocks_.empty();
65 }
66
67 size_t Size() const
68 {
69 return blocks_.size();
70 }
71
72 void Clear()
73 {
74 blocks_.clear();
75 owners_.clear();
76 }
77
78 void SetWidth(unsigned width)
79 {
80 if (!Empty())
81 throw std::logic_error("Flush before changing fusion width");
82 width_ = width;
83 }
84
85 // First target is the least-significant local matrix bit. Also performs
86 // target permutations when from and to contain the same qubits.
87 static Eigen::MatrixXcd Embed(const Eigen::MatrixXcd &matrix, const Types::qubits_vector &from, const Types::qubits_vector &to)
88 {
89 if (from == to)
90 return matrix;
91 const size_t dim = size_t{1} << to.size(), sub = size_t{1} << from.size();
92 std::vector<size_t> offsets(sub);
93 const size_t mask = Offsets(from, to, offsets.data());
94 // Only entries whose bits outside `from` agree are non-zero.
95 Eigen::MatrixXcd result = Eigen::MatrixXcd::Zero(dim, dim);
96 for (size_t base = 0; base < dim; ++base)
97 {
98 if (base & mask)
99 continue;
100 for (size_t col = 0; col < sub; ++col)
101 for (size_t row = 0; row < sub; ++row)
102 result(base | offsets[row], base | offsets[col]) = matrix(row, col);
103 }
104 return result;
105 }
106
107 // matrix <- embed(gate on `order`) * matrix, without forming the embedding:
108 // the gate acts on the row index bits of `order` within `to`, costing
109 // dim^2 * 2^k instead of dim^3 multiply-adds.
110 static void ApplyLeft(const Eigen::MatrixXcd &gate, const Types::qubits_vector &order, const Types::qubits_vector &to, Eigen::MatrixXcd &matrix)
111 {
112 const size_t k = order.size(), sub = size_t{1} << k;
113 if (k > 3)
114 throw std::logic_error("Fused gates act on at most 3 qubits");
115 size_t offsets[8] = {};
116 const size_t mask = Offsets(order, to, offsets);
117 // Explicit real arithmetic: std::complex multiplication goes through a
118 // NaN/Inf-checking library call without -ffast-math.
119 double gr[64], gi[64];
120 for (size_t r = 0; r < sub; ++r)
121 for (size_t l = 0; l < sub; ++l)
122 {
123 gr[r * sub + l] = gate(r, l).real();
124 gi[r * sub + l] = gate(r, l).imag();
125 }
126 auto *data = matrix.data();
127 switch (matrix.rows() * 4 + k)
128 { // fixed sizes let the loops unroll
129 case 2 * 4 + 1:
130 return ApplyLeftFixed<1, 2>(gr, gi, offsets, mask, data);
131 case 4 * 4 + 1:
132 return ApplyLeftFixed<1, 4>(gr, gi, offsets, mask, data);
133 case 4 * 4 + 2:
134 return ApplyLeftFixed<2, 4>(gr, gi, offsets, mask, data);
135 case 8 * 4 + 1:
136 return ApplyLeftFixed<1, 8>(gr, gi, offsets, mask, data);
137 case 8 * 4 + 2:
138 return ApplyLeftFixed<2, 8>(gr, gi, offsets, mask, data);
139 case 8 * 4 + 3:
140 return ApplyLeftFixed<3, 8>(gr, gi, offsets, mask, data);
141 default:
142 throw std::logic_error("Fused blocks act on at most 3 qubits");
143 }
144 }
145
146 template <class Emit> void Submit(const Gate &gate, uint64_t source, Emit &&emit, bool preserveStructure = false, bool standalone = false)
147 {
148 auto targets = gate.Targets();
149 if (targets.empty() || targets.size() > width_)
150 throw std::invalid_argument("Gate exceeds fusion width");
151 std::sort(targets.begin(), targets.end());
152 if (std::adjacent_find(targets.begin(), targets.end()) != targets.end())
153 throw std::invalid_argument("Gate targets must be distinct");
154
155 std::vector<uint64_t> overlapping;
156 for (auto q : targets)
157 {
158 auto it = owners_.find(q);
159 if (it != owners_.end())
160 overlapping.push_back(it->second);
161 }
162 std::sort(overlapping.begin(), overlapping.end());
163 overlapping.erase(std::unique(overlapping.begin(), overlapping.end()), overlapping.end());
164 // A backend may implement this gate without touching tensor data.
165 // Keep it named, while unrelated cached operations can still fuse.
166 if (standalone)
167 {
168 for (auto id : overlapping)
169 EmitOne(id, emit);
170 emit(Block{std::move(targets), {}, gate, {source, source, 1}, preserveStructure, gate.SchmidtRank()});
171 return;
172 }
173 // Keep native structured operations when a merge would only replace
174 // diagonal/permutation kernels with a dense one. Mixed groups can still
175 // benefit from fusion. Emitting only overlaps preserves disjoint caching.
176 if (preserveStructure && !overlapping.empty() &&
177 std::all_of(overlapping.begin(), overlapping.end(), [&](auto id) { return blocks_.at(id).preserveStructure; }))
178 {
179 for (auto id : overlapping)
180 EmitOne(id, emit);
181 overlapping.clear();
182 }
183 auto support = targets;
184 for (auto id : overlapping)
185 {
186 const auto &old = blocks_.at(id);
187 support.insert(support.end(), old.targets.begin(), old.targets.end());
188 }
189 std::sort(support.begin(), support.end());
190 support.erase(std::unique(support.begin(), support.end()), support.end());
191 if (support.size() > width_)
192 {
193 // Keep any block that still fits. In particular, an unrelated partner
194 // on one target must not force a cached single-qubit gate on the other
195 // target to be emitted as well. Disjoint cached blocks commute, so the
196 // rejected blocks can be emitted before the retained ones.
197 support = targets;
198 std::vector<uint64_t> retained;
199 for (auto id : overlapping)
200 {
201 auto candidate = support;
202 const auto &old = blocks_.at(id);
203 candidate.insert(candidate.end(), old.targets.begin(), old.targets.end());
204 std::sort(candidate.begin(), candidate.end());
205 candidate.erase(std::unique(candidate.begin(), candidate.end()), candidate.end());
206 if (candidate.size() <= width_)
207 {
208 retained.push_back(id);
209 support = std::move(candidate);
210 }
211 else
212 EmitOne(id, emit);
213 }
214 overlapping = std::move(retained);
215 }
216
217 if (overlapping.empty())
218 {
219 for (auto q : support)
220 owners_[q] = source;
221 blocks_.emplace(source, Block{std::move(support), {}, gate, {source, source, 1}, preserveStructure, gate.SchmidtRank()});
222 return;
223 }
224 // A merged block is emitted through its matrix, never as `original`.
225 Block next{{}, {}, Gate{}, {source, source, 1}, false, gate.SchmidtRank()};
226 {
227 // Start from the first block's matrix and left-apply the other
228 // (disjoint, hence commuting) blocks and the new gate as small gates,
229 // instead of multiplying full dense embeddings into an identity.
230 bool first = true;
231 for (auto id : overlapping)
232 {
233 const auto &old = blocks_.at(id);
234 next.sources.first = std::min(next.sources.first, old.sources.first);
235 next.sources.count += old.sources.count;
236 next.schmidtRank = std::min(4., next.schmidtRank * old.schmidtRank);
237 if (!matrices_)
238 continue;
239 if (old.IsSingle())
240 {
241 const auto order = old.original.Targets();
242 if (first)
243 next.matrix = Embed(old.original.Matrix(), order, support);
244 else
245 ApplyLeft(old.original.Matrix(), order, support, next.matrix);
246 }
247 else if (first)
248 next.matrix = Embed(old.matrix, old.targets, support);
249 else
250 ApplyLeft(old.matrix, old.targets, support, next.matrix);
251 first = false;
252 }
253 if (matrices_)
254 ApplyLeft(gate.Matrix(), gate.Targets(), support, next.matrix);
255 }
256 // Compute the replacement before removing anything, so allocation/matrix
257 // errors cannot lose pending operations. The merged block keeps the key
258 // of the oldest overlapping block (keys are each block's first source),
259 // and every qubit of the merged blocks is in `support`, so owner entries
260 // are overwritten instead of erased and reinserted.
261 const auto id = next.sources.first;
262 for (auto q : support)
263 owners_[q] = id;
264 next.targets = std::move(support);
265 bool reuse = false;
266 for (auto old : overlapping)
267 if (old == id)
268 reuse = true;
269 else
270 blocks_.erase(old);
271 if (reuse)
272 blocks_.at(id) = std::move(next);
273 else // a source older than every merged block
274 blocks_.emplace(id, std::move(next));
275 }
276
277 template <class Emit> void Flush(Emit &&emit)
278 {
279 while (!blocks_.empty())
280 EmitOne(blocks_.begin()->first, emit);
281 }
282
283 private:
284 void Remove(uint64_t id)
285 {
286 for (auto q : blocks_.at(id).targets)
287 owners_.erase(q);
288 blocks_.erase(id);
289 }
290
291 // Row offset of every local index of `from` within `to`; returns the mask.
292 static size_t Offsets(const Types::qubits_vector &from, const Types::qubits_vector &to, size_t *offsets)
293 {
294 const size_t sub = size_t{1} << from.size();
295 size_t mask = 0;
296 std::fill(offsets, offsets + sub, 0);
297 for (size_t i = 0; i < from.size(); ++i)
298 {
299 const auto pos = std::find(to.begin(), to.end(), from[i]);
300 if (pos == to.end())
301 throw std::logic_error("Invalid fusion embedding");
302 const size_t bit = size_t{1} << (pos - to.begin());
303 mask |= bit;
304 for (size_t l = 0; l < sub; ++l)
305 if (l & (size_t{1} << i))
306 offsets[l] |= bit;
307 }
308 return mask;
309 }
310
311 template <size_t K, size_t D> static void ApplyLeftFixed(const double *gr, const double *gi, const size_t *offsets, size_t mask, std::complex<double> *data)
312 {
313 constexpr size_t S = size_t{1} << K;
314 for (size_t col = 0; col < D; ++col)
315 {
316 auto *column = data + col * D;
317 for (size_t base = 0; base < D; ++base)
318 {
319 if (base & mask)
320 continue;
321 double inr[S], ini[S];
322 for (size_t l = 0; l < S; ++l)
323 {
324 inr[l] = column[base | offsets[l]].real();
325 ini[l] = column[base | offsets[l]].imag();
326 }
327 for (size_t r = 0; r < S; ++r)
328 {
329 double re = 0, im = 0;
330 for (size_t l = 0; l < S; ++l)
331 {
332 re += gr[r * S + l] * inr[l] - gi[r * S + l] * ini[l];
333 im += gr[r * S + l] * ini[l] + gi[r * S + l] * inr[l];
334 }
335 column[base | offsets[r]] = {re, im};
336 }
337 }
338 }
339 }
340
341 template <class Emit> void EmitOne(uint64_t id, Emit &emit)
342 {
343 emit(blocks_.at(id));
344 Remove(id);
345 }
346
347 unsigned width_;
348 bool matrices_;
349 std::map<uint64_t, Block> blocks_;
350 std::unordered_map<Types::qubit_t, uint64_t> owners_;
351};
352} // namespace Simulators
void SetWidth(unsigned width)
Definition GateFusion.h:78
GateFusion(unsigned width=2, bool matrices=true)
Definition GateFusion.h:58
size_t Size() const
Definition GateFusion.h:67
static Eigen::MatrixXcd Embed(const Eigen::MatrixXcd &matrix, const Types::qubits_vector &from, const Types::qubits_vector &to)
Definition GateFusion.h:87
static void ApplyLeft(const Eigen::MatrixXcd &gate, const Types::qubits_vector &order, const Types::qubits_vector &to, Eigen::MatrixXcd &matrix)
Definition GateFusion.h:110
void Flush(Emit &&emit)
Definition GateFusion.h:277
void Submit(const Gate &gate, uint64_t source, Emit &&emit, bool preserveStructure=false, bool standalone=false)
Definition GateFusion.h:146
Bounded online routing based on current physical bond dimensions.
std::vector< qubit_t > qubits_vector
The type of a vector of qubits.
Definition Types.h:24
Types::qubits_vector targets
Definition GateFusion.h:42
bool operator!=(const Sources &other) const
Definition GateFusion.h:34
bool operator==(const Sources &other) const
Definition GateFusion.h:29