Maestro 0.3.11
Unified interface for quantum circuit simulation
Loading...
Searching...
No Matches
ForestContractor.h
Go to the documentation of this file.
1
18
19#pragma once
20
21#ifndef __FOREST_CONTRACTOR_H_
22#define __FOREST_CONTRACTOR_H_ 1
23
24#include "BaseContractor.h"
25
26#include <cmath>
27#include <list>
28#include <queue>
29
30namespace TensorNetworks
31{
32
36{
37 public:
38 virtual ~ForestContractor() = default;
39
40 double Contract(const TensorNetwork &network, Types::qubit_t qubit) override
41 {
42 CachedPlan plan;
43 const auto &original = network.GetTensors();
44 const auto &group = network.GetQubitGroup(qubit);
45 for (size_t i = 0; i < original.size(); ++i)
46 if (original[i] && !original[i]->qubits.empty() && group.count(original[i]->qubits[0]))
47 plan.inputs.push_back(i);
48 if (plan.inputs.empty())
49 throw std::invalid_argument("Cannot contract an empty tensor network");
50
51 bool cacheable = MakeSignature(network, qubit, plan);
52 if (cacheable)
53 {
54 for (auto it = plans.begin(); it != plans.end(); ++it)
55 {
56 if (it->fingerprint == plan.fingerprint && it->signature == plan.signature)
57 {
58 plans.splice(plans.begin(), plans, it);
59 ++planCacheHits;
60 if (!plans.front().preparationAttempted)
61 PreparePlan(network, plans.front());
62 return ExecutePlan(network, plans.front());
63 }
64 }
65 }
66
67 TensorsMap tensors;
68 std::unordered_map<Eigen::Index, size_t> slots;
69 std::unordered_map<Eigen::Index, size_t> versions;
70 tensors.reserve(plan.inputs.size());
71 if (cacheable)
72 slots.reserve(plan.inputs.size());
73 versions.reserve(plan.inputs.size());
74 maxTensorRank = 0;
75 for (size_t slot = 0; slot < plan.inputs.size(); ++slot)
76 {
77 const auto &node = original[plan.inputs[slot]];
78 tensors.emplace(node->GetId(), node->CloneWithoutTensorCopy());
79 if (cacheable)
80 slots.emplace(node->GetId(), slot);
81 versions.emplace(node->GetId(), 0);
82 maxTensorRank = std::max(maxTensorRank, node->GetRank());
83 }
84 std::priority_queue<Candidate, std::vector<Candidate>, CandidateLater> candidates;
85 for (const auto &entry : tensors)
86 AddCandidates(entry.first, tensors, versions, candidates, true);
87
88 Eigen::Index resultId = tensors.begin()->first;
89 while (tensors.size() > 1)
90 {
91 while (!candidates.empty())
92 {
93 const auto &candidate = candidates.top();
94 const auto a = versions.find(candidate.a), b = versions.find(candidate.b);
95 if (a != versions.end() && b != versions.end() && a->second == candidate.versionA && b->second == candidate.versionB)
96 break;
97 candidates.pop();
98 }
99 if (candidates.empty())
100 throw std::invalid_argument("Tensor network has no connected contraction pair");
101 const Candidate candidate = candidates.top();
102 candidates.pop();
103
104 if (cacheable)
105 {
106 Step step;
107 step.left = slots.at(candidate.a);
108 step.right = slots.at(candidate.b);
109 const auto &node = tensors.at(candidate.a);
110 for (size_t i = 0; i < node->connections.size(); ++i)
111 if (node->connections[i] == candidate.b)
112 step.axes.emplace_back(i, node->connectionsIndices[i]);
113 plan.axisBytes += step.axes.capacity() * sizeof(step.axes[0]);
114 plan.steps.push_back(std::move(step));
115 if (PlanBytes(plan) > planCacheByteLimit)
116 {
117 cacheable = false;
118 plan.signature.clear();
119 plan.steps.clear();
120 }
121 }
122
123 resultId = ContractNodes(qubit, tensors, candidate.a, candidate.b, candidate.resultRank);
124 versions.erase(candidate.b);
125 ++versions.at(candidate.a);
126 if (candidate.resultRank == 0)
127 {
128 if (tensors.size() == 1 || tensors.at(resultId)->contractsTheNeededQubit)
129 break;
130 tensors.erase(resultId);
131 versions.erase(resultId);
132 }
133 else
134 {
135 AddCandidates(resultId, tensors, versions, candidates, false);
136 }
137 }
138
139 const double result = std::real(tensors.at(resultId)->tensor->atOffset(0));
140 if (cacheable)
141 {
142 plan.resultSlot = slots.at(resultId);
143 plan.maxRank = maxTensorRank;
144 plan.bytes = PlanBytes(plan);
145 if (plan.bytes <= planCacheByteLimit)
146 {
147 while (!plans.empty() && cachedBytes > planCacheByteLimit - plan.bytes)
148 if (!DropLastPreparedPlan())
149 EvictLastPlan();
150 cachedBytes += plan.bytes;
151 plans.push_front(std::move(plan));
152 }
153 }
154 return result;
155 }
156
157 // The cache holds metadata; the separate workspace holds reusable storage.
158 // Tensor values are recomputed for every evaluation, including cache hits.
159 void SetPlanCacheByteLimit(size_t bytes)
160 {
161 planCacheByteLimit = bytes;
162 while (!plans.empty() && cachedBytes > bytes)
163 if (!DropLastPreparedPlan())
164 EvictLastPlan();
165 for (auto &plan : plans)
166 if (!plan.prepared)
167 plan.preparationAttempted = false;
168 if (plans.empty())
169 ClearWorkspace();
170 }
171
173 {
174 return planCacheByteLimit;
175 }
176
177 size_t GetCachedPlanBytes() const
178 {
179 return cachedBytes;
180 }
181
182 size_t GetCachedPlanCount() const
183 {
184 return plans.size();
185 }
186
187 size_t GetPlanCacheHits() const
188 {
189 return planCacheHits;
190 }
191
192 size_t GetPreparedPlanCount() const
193 {
194 return std::count_if(plans.begin(), plans.end(), [](const auto &plan) { return bool(plan.prepared); });
195 }
196
197 // Per-contractor retained output and packing storage. Zero disables buffer
198 // retention, while still allowing prepared execution with fresh allocations.
199 void SetWorkspaceByteLimit(size_t bytes)
200 {
201 workspaceByteLimit = bytes;
202 if (GetWorkspaceBytes() > bytes)
203 ClearWorkspace();
204 }
205
207 {
208 return workspaceByteLimit;
209 }
210
211 size_t GetWorkspaceBytes() const
212 {
213 return workspace.capacity() * sizeof(Complex);
214 }
215
217 {
218 plans.clear();
219 cachedBytes = 0;
220 planCacheHits = 0;
221 ClearWorkspace();
222 }
223
224 std::shared_ptr<ITensorContractor> Clone() const override
225 {
226 auto cloned = std::make_shared<ForestContractor>();
227 cloned->maxTensorRank = maxTensorRank;
228 cloned->enableMultithreading = enableMultithreading;
229 cloned->planCacheByteLimit = planCacheByteLimit;
230 cloned->workspaceByteLimit = workspaceByteLimit;
231 // Clones start with an independent empty cache, avoiding metadata copies
232 // for each simulator/trajectory clone.
233 return cloned;
234 }
235
236 private:
237 using Complex = std::complex<double>;
238
239 struct PreparedStep
240 {
242 size_t outputOffset = 0;
243 };
244
245 struct PreparedPlan
246 {
247 std::vector<PreparedStep> steps;
248 size_t bytes = 0, workspaceSize = 0, packingSize = 0;
249 };
250
251 struct Step
252 {
253 size_t left = 0, right = 0;
254 std::vector<std::pair<size_t, size_t>> axes;
255 };
256
257 struct CachedPlan
258 {
259 std::vector<size_t> signature, inputs;
260 std::vector<Step> steps;
261 size_t fingerprint = 0, resultSlot = 0, maxRank = 0;
262 size_t axisBytes = 0, bytes = 0;
263 // Immutable after construction, including when the contractor is copied.
264 std::shared_ptr<const PreparedPlan> prepared;
265 bool preparationAttempted = false;
266 };
267
268 struct Candidate
269 {
270 Eigen::Index a, b;
271 size_t versionA, versionB, resultRank, edge;
272 double outputSize, work;
273 };
274
275 struct CandidateLater
276 {
277 bool operator()(const Candidate &a, const Candidate &b) const
278 {
279 if (a.outputSize != b.outputSize)
280 return a.outputSize > b.outputSize;
281 if (a.work != b.work)
282 return a.work > b.work;
283 // Prefer the circuit's later tensors on exact ties, as the forest
284 // contractor traditionally does. Make ordering independent of hash maps.
285 if (a.a != b.a)
286 return a.a < b.a;
287 if (a.edge != b.edge)
288 return a.edge > b.edge;
289 return a.b < b.b;
290 }
291 };
292
293 using Candidates = std::priority_queue<Candidate, std::vector<Candidate>, CandidateLater>;
294
295 static void AddCandidates(Eigen::Index id, const TensorsMap &tensors, const std::unordered_map<Eigen::Index, size_t> &versions, Candidates &candidates,
296 bool initial)
297 {
298 const auto &node = tensors.at(id);
299 for (size_t i = 0; i < node->connections.size(); ++i)
300 {
301 const auto next = node->connections[i];
302 if (next == TensorNode::NotConnected || next == id || (initial && id < next))
303 continue;
304 // Parallel edges describe one contraction over all shared axes.
305 if (std::find(node->connections.begin(), node->connections.begin() + i, next) != node->connections.begin() + i)
306 continue;
307 const auto aId = std::max(id, next), bId = std::min(id, next);
308 const auto &a = tensors.at(aId), &b = tensors.at(bId);
309 Candidate candidate{aId, bId, versions.at(aId), versions.at(bId), 0, 0, 0, 0};
310 double reductionSize = 0;
311 bool first = true;
312 for (size_t axis = 0; axis < a->connections.size(); ++axis)
313 {
314 const double dimension = std::log2(double(a->tensor->GetDim(axis)));
315 if (a->connections[axis] == bId)
316 {
317 reductionSize += dimension;
318 if (first)
319 {
320 candidate.edge = axis;
321 first = false;
322 }
323 }
324 else
325 {
326 ++candidate.resultRank;
327 candidate.outputSize += dimension;
328 }
329 }
330 for (size_t axis = 0; axis < b->connections.size(); ++axis)
331 if (b->connections[axis] != aId)
332 {
333 ++candidate.resultRank;
334 candidate.outputSize += std::log2(double(b->tensor->GetDim(axis)));
335 }
336 // Logarithmic sizes avoid overflow for plans containing high-rank nodes.
337 candidate.work = candidate.outputSize + reductionSize;
338 candidates.push(candidate);
339 }
340 }
341
342 bool MakeSignature(const TensorNetwork &network, Types::qubit_t qubit, CachedPlan &plan) const
343 {
344 if (!planCacheByteLimit)
345 return false;
346 auto append = [&](size_t value) {
347 plan.signature.push_back(value);
348 plan.fingerprint ^= value + size_t(0x9e3779b97f4a7c15ULL) + (plan.fingerprint << 6) + (plan.fingerprint >> 2);
349 };
350 append(static_cast<size_t>(qubit));
351 for (auto index : plan.inputs)
352 {
353 const auto &node = network.GetTensors()[index];
354 append(index);
355 append(static_cast<size_t>(node->GetId()));
356 append(node->GetRank());
357 append(node->tensor->IsDummy());
358 for (auto dimension : node->tensor->GetDims())
359 append(dimension);
360 append(node->connections.size());
361 for (auto connection : node->connections)
362 append(static_cast<size_t>(connection));
363 append(node->connectionsIndices.size());
364 for (auto axis : node->connectionsIndices)
365 append(static_cast<size_t>(axis));
366 append(node->qubits.size());
367 for (auto q : node->qubits)
368 append(static_cast<size_t>(q));
369 if (PlanBytes(plan) > planCacheByteLimit)
370 return false;
371 }
372 return true;
373 }
374
375 double ExecutePlan(const TensorNetwork &network, const CachedPlan &plan)
376 {
377 if (plan.prepared)
378 return ExecutePrepared(network, plan);
379 std::vector<std::shared_ptr<const Utils::Tensor<>>> values;
380 values.reserve(plan.inputs.size());
381 for (auto index : plan.inputs)
382 values.push_back(network.GetTensors()[index]->tensor);
383 for (const auto &step : plan.steps)
384 {
385 values[step.left] = std::make_shared<Utils::Tensor<>>(values[step.left]->Contract(*values[step.right], step.axes, enableMultithreading));
386 values[step.right].reset();
387 }
388 maxTensorRank = plan.maxRank;
389 return std::real((*values[plan.resultSlot])[size_t(0)]);
390 }
391
392 // Build only on reuse. Keep the lightweight order cache if prepared metadata
393 // would exceed the budget, and check offset-table sizes before allocation.
394 void PreparePlan(const TensorNetwork &network, CachedPlan &plan)
395 {
396 plan.preparationAttempted = true;
397 // Prepared metadata may replace older prepared metadata, but must not
398 // evict contraction orders. This protects multi-qubit query workloads.
399 size_t limit = planCacheByteLimit - cachedBytes;
400 for (const auto &cached : plans)
401 if (cached.prepared)
402 limit += cached.prepared->bytes;
403 size_t bytes = sizeof(PreparedPlan) + 2 * sizeof(void *);
404 if (!Utils::detail::TensorAddBytes(bytes, plan.steps.size(), sizeof(PreparedStep), limit))
405 return;
406 auto prepared = std::make_shared<PreparedPlan>();
407 prepared->steps.reserve(plan.steps.size());
408 bytes = sizeof(PreparedPlan) + 2 * sizeof(void *);
409 if (!Utils::detail::TensorAddBytes(bytes, prepared->steps.capacity(), sizeof(PreparedStep), limit))
410 return;
411
412 std::vector<const std::vector<size_t> *> dimensions;
413 dimensions.reserve(plan.inputs.size());
414 for (auto index : plan.inputs)
415 {
416 const auto &tensor = network.GetTensors()[index]->tensor;
417 if (tensor->IsDummy())
418 return;
419 dimensions.push_back(&tensor->GetDims());
420 }
421 const size_t none = std::numeric_limits<size_t>::max();
422 std::vector<size_t> active(plan.inputs.size(), none), free, capacities;
423 for (const auto &step : plan.steps)
424 {
425 prepared->steps.emplace_back();
426 auto &item = prepared->steps.back();
427 if (!item.contraction.Prepare(*dimensions[step.left], *dimensions[step.right], step.axes, limit - bytes))
428 return;
429 bytes += item.contraction.ExtraBytes();
430 prepared->packingSize = std::max(prepared->packingSize, item.contraction.GetPackingSize());
431 dimensions[step.left] = &item.contraction.GetDims();
432 dimensions[step.right] = nullptr;
433 const size_t size = item.contraction.GetSize();
434 size_t buffer;
435 if (free.empty())
436 {
437 buffer = capacities.size();
438 capacities.push_back(size);
439 }
440 else
441 {
442 const auto best = std::min_element(free.begin(), free.end(), [&](size_t a, size_t b) {
443 const bool fitA = capacities[a] >= size, fitB = capacities[b] >= size;
444 if (fitA != fitB)
445 return fitA;
446 return fitA ? capacities[a] < capacities[b] : capacities[a] > capacities[b];
447 });
448 buffer = *best;
449 free.erase(best);
450 capacities[buffer] = std::max(capacities[buffer], size);
451 }
452 item.outputOffset = buffer;
453 // Choose the destination before releasing either live input.
454 if (active[step.left] != none)
455 free.push_back(active[step.left]);
456 if (active[step.right] != none)
457 free.push_back(active[step.right]);
458 active[step.left] = buffer;
459 active[step.right] = none;
460 }
461 // Place non-overlapping buffers in one arena, retaining only the offsets.
462 size_t arenaSize = 0;
463 bool fits = true;
464 for (auto &capacity : capacities)
465 {
466 const size_t size = capacity;
467 capacity = arenaSize;
468 if (!Utils::detail::TensorAddBytes(arenaSize, size, 1, std::numeric_limits<size_t>::max() / sizeof(Complex)))
469 {
470 fits = false;
471 break;
472 }
473 }
474 if (fits)
475 {
476 prepared->workspaceSize = arenaSize;
477 for (auto &step : prepared->steps)
478 step.outputOffset = capacities[step.outputOffset];
479 }
480 prepared->bytes = bytes;
481 if (bytes > planCacheByteLimit)
482 return;
483 while (cachedBytes > planCacheByteLimit - bytes)
484 if (!DropLastPreparedPlan())
485 return;
486 plan.prepared = std::move(prepared);
487 plan.bytes += bytes;
488 cachedBytes += bytes;
489 }
490
491 bool PrepareWorkspace(size_t size)
492 {
493 if (!size || size > workspaceByteLimit / sizeof(Complex))
494 return false;
495 if (workspace.size() < size)
496 {
497 // Exact construction avoids vector growth retaining more than requested.
498 std::vector<Complex> replacement(size);
499 if (replacement.capacity() > workspaceByteLimit / sizeof(Complex))
500 return false;
501 workspace.swap(replacement);
502 }
503 return true;
504 }
505
506 void ClearWorkspace()
507 {
508 std::vector<Complex>().swap(workspace);
509 }
510
511 double ExecutePrepared(const TensorNetwork &network, const CachedPlan &plan)
512 {
513 const auto &prepared = *plan.prepared;
514 // Sequential steps share one packing area after the live output buffers.
515 // If it exceeds the limit, retain output reuse and pack into fresh buffers.
516 size_t size = prepared.workspaceSize;
517 const bool reusePacking = size && prepared.packingSize &&
518 Utils::detail::TensorAddBytes(size, prepared.packingSize, 1, workspaceByteLimit / sizeof(Complex)) && PrepareWorkspace(size);
519 const bool reuse = reusePacking || PrepareWorkspace(prepared.workspaceSize);
520 Complex *packing = reusePacking ? workspace.data() + prepared.workspaceSize : nullptr;
521 std::vector<const Complex *> values;
522 values.reserve(plan.inputs.size());
523 for (auto index : plan.inputs)
524 values.push_back(&(*network.GetTensors()[index]->tensor)[size_t(0)]);
525 std::vector<std::unique_ptr<Complex[]>> owned(reuse ? 0 : plan.inputs.size());
526 for (size_t i = 0; i < plan.steps.size(); ++i)
527 {
528 const auto &step = plan.steps[i];
529 const auto &item = prepared.steps[i];
530 std::unique_ptr<Complex[]> fresh;
531 Complex *dest;
532 if (reuse)
533 dest = workspace.data() + item.outputOffset;
534 else
535 {
536 fresh.reset(new Complex[item.contraction.GetSize()]);
537 dest = fresh.get();
538 }
539 item.contraction.Execute(values[step.left], values[step.right], dest, enableMultithreading, packing);
540 if (!reuse)
541 {
542 owned[step.left] = std::move(fresh);
543 owned[step.right].reset();
544 }
545 values[step.left] = dest;
546 values[step.right] = nullptr;
547 }
548 maxTensorRank = plan.maxRank;
549 return values[plan.resultSlot][0].real();
550 }
551
552 static size_t PlanBytes(const CachedPlan &plan)
553 {
554 return sizeof(CachedPlan) + 2 * sizeof(void *) + plan.axisBytes + (plan.signature.capacity() + plan.inputs.capacity()) * sizeof(size_t) +
555 plan.steps.capacity() * sizeof(Step) + (plan.prepared ? plan.prepared->bytes : 0);
556 }
557
558 void EvictLastPlan()
559 {
560 cachedBytes -= plans.back().bytes;
561 plans.pop_back();
562 }
563
564 bool DropLastPreparedPlan()
565 {
566 for (auto it = plans.rbegin(); it != plans.rend(); ++it)
567 if (it->prepared)
568 {
569 cachedBytes -= it->prepared->bytes;
570 it->bytes -= it->prepared->bytes;
571 it->prepared.reset();
572 // Do not repeatedly promote/demote the same plan at a tight budget.
573 // A budget change or a new order-cache entry allows another attempt.
574 return true;
575 }
576 return false;
577 }
578
579 size_t planCacheByteLimit = 8 * 1024 * 1024;
580 size_t cachedBytes = 0, planCacheHits = 0;
581 size_t workspaceByteLimit = 16 * 1024 * 1024;
582 std::vector<Complex> workspace;
583 std::list<CachedPlan> plans;
584};
585
586} // namespace TensorNetworks
587
588#endif // __FOREST_CONTRACTOR_H_
The Base Class Tensor Contractor.
bool enableMultithreading
A flag to indicate if multithreading should be enabled.
ITensorContractor::TensorsMap TensorsMap
Eigen::Index ContractNodes(Types::qubit_t qubit, PassedTensorsMap &tensors, Eigen::Index tensor1Id, Eigen::Index tensor2Id, Eigen::Index resultRank)
size_t maxTensorRank
The maximum rank of the tensors in the network.
Greedy, memory-first contraction with incremental candidates and plan reuse.
virtual ~ForestContractor()=default
double Contract(const TensorNetwork &network, Types::qubit_t qubit) override
Contract the tensor network.
std::shared_ptr< ITensorContractor > Clone() const override
Clone the tensor contractor.
const std::unordered_set< Types::qubit_t > & GetQubitGroup(Types::qubit_t q) const
const std::vector< std::shared_ptr< TensorNode > > & GetTensors() const
static constexpr Index NotConnected
Definition TensorNode.h:173
uint_fast64_t qubit_t
The type of a qubit.
Definition Types.h:23
bool TensorAddBytes(size_t &used, size_t count, size_t elementSize, size_t limit)