Maestro 0.3.11
Unified interface for quantum circuit simulation
Loading...
Searching...
No Matches
Sampling.h
Go to the documentation of this file.
1#pragma once
2
3#include "../../Types.h"
4#include "../RandomStream.h"
5#include "AliasBase.h"
6#include <algorithm>
7#include <charconv>
8#include <cmath>
9#include <cstddef>
10#include <cstdint>
11#include <limits>
12#include <memory>
13#include <numeric>
14#include <stdexcept>
15#include <string_view>
16#include <type_traits>
17#include <unordered_map>
18#include <utility>
19#include <vector>
20#ifdef _OPENMP
21#include <omp.h>
22#endif
23
25{
26// Reproducible policy v1. Family/layout choices never depend on physical teams.
27// The memory limit is explicit configuration, not fluctuating free system RAM.
28enum class Family
29{
33};
34
35struct Options
36{
37 bool multithreading = false;
38 size_t memoryBytes = size_t{256} << 20;
39 size_t batchSize = size_t{1} << 20;
41};
42
43// Validate before any parent or component configuration is mutated.
44// Returns false for settings owned by another part of the simulator.
45inline bool ValidateSetting(std::string_view name, std::string_view setting)
46{
47 if (name == "sampling_policy")
48 {
49 if (setting != "reproducible_v1" && setting != "legacy")
50 throw std::invalid_argument("sampling_policy must be reproducible_v1 or legacy");
51 }
52 else if (name == "sampling_max_memory_mb")
53 {
54 size_t parsed = 0;
55 const auto result = std::from_chars(setting.data(), setting.data() + setting.size(), parsed);
56 if (result.ec != std::errc{} || result.ptr != setting.data() + setting.size() || !parsed ||
57 parsed > std::numeric_limits<size_t>::max() / (size_t{1} << 20))
58 throw std::invalid_argument("sampling_max_memory_mb must be a positive size");
59 }
60 else
61 return false;
62 return true;
63}
64
65inline int Threads(bool permitted, size_t work)
66{
67#ifdef _OPENMP
68 if (permitted && !omp_in_parallel())
69 {
70 const size_t useful = std::max<size_t>(1, work / 16384);
71 return static_cast<int>(std::min<size_t>(std::max(1, omp_get_max_threads()), useful));
72 }
73#else
74 (void)permitted;
75 (void)work;
76#endif
77 return 1;
78}
79
80inline void CheckMass(double total)
81{
82 if (!(total > 0.) || !std::isfinite(total))
83 throw std::domain_error("Sampling requires finite positive probability mass");
84}
85
87{
88 public:
89 template <class Probability>
90 ProbabilityAlias(size_t count, size_t sourceCount, double mass, const Probability &probability, size_t blockSize, const std::vector<double> &masses)
91 {
92 if (count != sourceCount)
93 sourceRows.resize(count);
94 SetAliasTable(count, [&](const auto &add) {
95 if (sourceRows.empty())
96 {
97 for (size_t i = 0; i < count; ++i)
98 add(i, (probability(i) / mass) * count);
99 }
100 else
101 {
102 size_t row = 0;
103 // Zero-mass blocks are known exactly from validation. Sparse
104 // states need no second full scan and no dense alias table.
105 for (size_t b = 0; b < masses.size(); ++b)
106 if (masses[b] > 0.)
107 for (size_t i = b * blockSize, end = std::min(sourceCount, (b + 1) * blockSize); i < end; ++i)
108 {
109 const double p = probability(i);
110 if (p > 0.)
111 {
112 sourceRows[row] = i;
113 add(row++, (p / mass) * count);
114 }
115 }
116 }
117 });
118 }
119
120 size_t Sample(double value) const
121 {
122 return SampleIndex(value);
123 }
124
125 size_t SourceRow(size_t row) const
126 {
127 return sourceRows.empty() ? row : sourceRows[row];
128 }
129
130 private:
131 std::vector<size_t> sourceRows;
132};
133
134// Probability is a nonthrowing read of immutable storage. Preparation validates
135// it before sampling or alias construction; no exceptions cross OpenMP regions.
136template <class Probability> class Prepared
137{
138 public:
139 // Reserve enough for the validation index and at least one draw batch.
140 // Composite callers use this to share one memory budget across components.
141 static size_t MinimumMemoryBytes(size_t count, size_t shots, size_t budget = Options{}.memoryBytes)
142 {
143 const size_t block = BlockSize(count, shots, budget);
144 const size_t blocks = count / block + (count % block != 0);
145 constexpr size_t bytesPerBlock = 2 * sizeof(double) + 1;
146 return std::max<size_t>(4096, blocks * bytesPerBlock + sizeof(double) + 1024);
147 }
148
149 Prepared(size_t count, size_t shots, Probability probability, Options options = {}) : count(count), probability(std::move(probability)), options(options)
150 {
151 if (!count)
152 throw std::domain_error("Sampling requires nonempty probability support");
153 if (options.memoryBytes < 4096 || !options.batchSize)
154 throw std::invalid_argument("Sampling scratch budget is too small");
155 // Each block has a fixed serial summation order, independent of teams.
156 blockSize = BlockSize(count, shots, options.memoryBytes);
157 const size_t blocks = count / blockSize + (count % blockSize != 0);
158 if (blocks > (options.memoryBytes - sizeof(double)) / (2 * sizeof(double) + 1) || blocks > static_cast<size_t>(std::numeric_limits<int64_t>::max()))
159 throw std::length_error("Sampling probability index exceeds the scratch budget");
160 prefix.resize(blocks + 1, 0.);
161 masses.resize(blocks, 0.);
162 std::vector<unsigned char> invalid(blocks, 0);
163 const int threads = Threads(options.multithreading, count);
164 size_t nonzero = 0;
165#ifdef _OPENMP
166#pragma omp parallel for if (threads > 1) num_threads(threads) schedule(static) reduction(+ : nonzero)
167#endif
168 for (int64_t b = 0; b < static_cast<int64_t>(blocks); ++b)
169 {
170 double sum = 0.;
171 size_t positive = 0;
172 const size_t begin = size_t(b) * blockSize, end = std::min(count, begin + blockSize);
173 for (size_t i = begin; i < end; ++i)
174 {
175 const double p = this->probability(i);
176 if (!std::isfinite(p) || p < 0.)
177 invalid[b] = 1;
178 sum += p;
179 positive += p > 0.;
180 }
181 masses[b] = sum;
182 nonzero += positive;
183 }
184 if (std::find(invalid.begin(), invalid.end(), 1) != invalid.end())
185 throw std::domain_error("Sampling probabilities must be finite and nonnegative");
186 for (size_t b = 0; b < blocks; ++b)
187 prefix[b + 1] = prefix[b] + masses[b];
188 mass = prefix.back();
189 CheckMass(mass);
190 std::vector<unsigned char>().swap(invalid);
191 aliasCount = nonzero <= count / 4 ? nonzero : count;
192
193 // Leave space for at least one batch, even if the table almost fills the budget.
194 const size_t available = options.memoryBytes - IndexBytes();
195 const bool fitsAlias = available >= 1024 && aliasCount <= (available - 1024) / AliasRowBytes();
196 const bool useAlias =
197 options.family == Family::Alias || (options.family == Family::Auto && shots >= std::max<size_t>(512, aliasCount / 4) && fitsAlias);
198 if (useAlias)
199 {
200 if (!fitsAlias)
201 throw std::length_error("Alias table exceeds the sampling scratch budget");
202 alias = std::make_unique<ProbabilityAlias>(aliasCount, count, mass, this->probability, blockSize, masses);
203 }
204 else if (count <= 65536 && available >= 1024 && count <= (available - 1024) / sizeof(double))
205 {
206 localCdf.resize(count);
207 for (size_t b = 0; b < blocks; ++b)
208 {
209 double sum = 0.;
210 const size_t begin = b * blockSize, end = std::min(count, begin + blockSize);
211 for (size_t i = begin; i < end; ++i)
212 localCdf[i] = sum += this->probability(i);
213 }
214 }
215 }
216
217 size_t Size() const
218 {
219 return count;
220 }
221
222 size_t Categories() const
223 {
224 return alias ? aliasCount : count;
225 }
226
227 size_t SourceRow(size_t category) const
228 {
229 return alias ? alias->SourceRow(category) : category;
230 }
231
232 size_t SampleCategory(double uniform) const
233 {
234 return alias ? alias->Sample(uniform) : Sample(uniform);
235 }
236
238 {
239 return alias ? Family::Alias : Family::Cdf;
240 }
241
242 const Options &Settings() const
243 {
244 return options;
245 }
246
247 size_t ScratchAvailable() const
248 {
249 const size_t tables = IndexBytes() + localCdf.size() * sizeof(double) + (alias ? aliasCount * AliasRowBytes() : 0);
250 return options.memoryBytes - tables;
251 }
252
253 size_t Sample(double uniform) const
254 {
255 if (alias)
256 return alias->SourceRow(alias->Sample(uniform));
257 const double target = Target(uniform);
258 const size_t b = Block(target);
259 const double local = LocalTarget(target, b);
260 const size_t begin = b * blockSize, end = std::min(count, begin + blockSize);
261 if (!localCdf.empty())
262 {
263 const auto it = std::upper_bound(localCdf.begin() + begin, localCdf.begin() + end, local);
264 if (it != localCdf.begin() + end)
265 return static_cast<size_t>(it - localCdf.begin());
266 }
267 double sum = 0.;
268 size_t lastPositive = begin;
269 for (size_t i = begin; i < end; ++i)
270 {
271 const double p = probability(i);
272 if (p > 0.)
273 lastPositive = i;
274 sum += p;
275 if (local < sum)
276 return i;
277 }
278 return lastPositive;
279 }
280
281 // Always scatter to original shot positions, including composite callers.
282 void Draw(const std::vector<double> &uniforms, std::vector<size_t> &out, int threads) const
283 {
284 out.resize(uniforms.size());
285 if (threads == 1 && !alias && count > 65536 && uniforms.size() >= 128)
286 {
287 std::vector<std::pair<double, size_t>> ordered;
288 ordered.reserve(uniforms.size());
289 for (size_t i = 0; i < uniforms.size(); ++i)
290 ordered.emplace_back(Target(uniforms[i]), i);
291 std::sort(ordered.begin(), ordered.end());
292 size_t previousBlock = std::numeric_limits<size_t>::max(), row = 0, end = 0, lastPositive = 0;
293 double sum = 0.;
294 for (const auto &[target, shot] : ordered)
295 {
296 const size_t b = Block(target);
297 if (b != previousBlock)
298 {
299 previousBlock = b;
300 row = b * blockSize;
301 end = std::min(count, row + blockSize);
302 sum = probability(row);
303 lastPositive = row;
304 }
305 const double local = LocalTarget(target, b);
306 while (sum <= local && row + 1 < end)
307 {
308 const double p = probability(++row);
309 if (p > 0.)
310 lastPositive = row;
311 sum += p;
312 }
313 out[shot] = sum > local ? row : lastPositive;
314 }
315 return;
316 }
317#ifdef _OPENMP
318#pragma omp parallel for if (threads > 1) num_threads(threads) schedule(static)
319#endif
320 for (int64_t i = 0; i < static_cast<int64_t>(uniforms.size()); ++i)
321 out[i] = Sample(uniforms[i]);
322 }
323
324 private:
325 static size_t BlockSize(size_t count, size_t shots, size_t budget)
326 {
327 if (budget < 4096)
328 throw std::invalid_argument("Sampling scratch budget is too small");
329 // Grow fixed blocks until validation and a draw batch fit. This depends
330 // only on logical inputs, never on the physical thread count. The
331 // minimum budget leaves at least 180 blocks, so doubling cannot overflow.
332 const size_t maxBlocks = (budget - 1024 - sizeof(double)) / (2 * sizeof(double) + 1);
333 size_t block = shots >= count / 16 && shots >= 65536 ? 64 : 256;
334 while (count / block + (count % block != 0) > maxBlocks)
335 block *= 2;
336 return block;
337 }
338
339 size_t AliasRowBytes() const
340 {
341 return sizeof(double) + (aliasCount - 1 <= std::numeric_limits<uint32_t>::max() ? sizeof(uint32_t) : sizeof(uint64_t)) +
342 (aliasCount != count ? sizeof(size_t) : 0);
343 }
344
345 size_t IndexBytes() const
346 {
347 return (prefix.size() + masses.size()) * sizeof(double);
348 }
349
350 double Target(double uniform) const
351 {
352 return std::min(uniform * mass, std::nextafter(mass, 0.));
353 }
354
355 size_t Block(double target) const
356 {
357 return static_cast<size_t>(std::upper_bound(prefix.begin() + 1, prefix.end(), target) - prefix.begin() - 1);
358 }
359
360 double LocalTarget(double target, size_t block) const
361 {
362 return std::min(target - prefix[block], std::nextafter(masses[block], 0.));
363 }
364
365 size_t count, blockSize = 256, aliasCount = 0;
366 Probability probability;
367 Options options;
368 double mass = 0.;
369 std::vector<double> prefix, masses, localCdf;
370 std::unique_ptr<ProbabilityAlias> alias;
371};
372
373template <bool Many> using Counts = std::unordered_map<std::conditional_t<Many, std::vector<bool>, Types::qubit_t>, Types::qubit_t>;
374
375template <bool Many, class Bit> Counts<Many> Single(size_t bits, const Bit &bit)
376{
377 if constexpr (Many)
378 {
379 std::vector<bool> outcome(bits);
380 for (size_t i = 0; i < bits; ++i)
381 outcome[i] = bit(i);
382 return {{std::move(outcome), 1}};
383 }
384 else
385 {
386 Types::qubit_t outcome = 0;
387 for (size_t i = 0; i < bits; ++i)
388 if (bit(i))
389 outcome |= Types::qubit_t{1} << i;
390 return {{outcome, 1}};
391 }
392}
393
394template <class Qubits> void ValidateQubits(const Qubits &qubits, size_t width, bool many)
395{
396 if (!many && qubits.size() > std::numeric_limits<Types::qubit_t>::digits)
397 throw std::invalid_argument("Packed sampling exceeds the result word width");
398 for (const auto q : qubits)
399 if (q >= width)
400 throw std::out_of_range("Measured qubit is outside the register");
401}
402
403// Bounded fallback for immutable, sequential probability storage whose row
404// snapshot cannot fit. Sorted targets allow one traversal per batch, with no
405// per-row table or copied labels. Counts do not need the original shot order.
406template <bool Many, class Range, class Probability, class Bit, class Uniform>
407Counts<Many> CountCdfRange(const Range &rows, size_t shots, size_t bits, const Probability &probability, const Bit &bit, Uniform &&uniform,
408 Options options = {})
409{
410 Counts<Many> result;
411 if (!shots || !bits)
412 return result;
413 if (options.memoryBytes < 4096 || !options.batchSize)
414 throw std::invalid_argument("Sampling scratch budget is too small");
415 double mass = 0.;
416 for (const auto &row : rows)
417 {
418 const double p = probability(row);
419 if (!std::isfinite(p) || p < 0.)
420 throw std::domain_error("Sampling probabilities must be finite and nonnegative");
421 mass += p;
422 }
423 CheckMass(mass);
424 const size_t batch = std::min({shots, options.batchSize, options.memoryBytes / sizeof(double)});
425 std::vector<double> targets;
426 targets.reserve(batch);
427 std::vector<bool> key;
428 if constexpr (Many)
429 key.resize(bits);
430 for (size_t begin = 0; begin < shots;)
431 {
432 const size_t count = std::min(batch, shots - begin);
433 targets.resize(count);
434 for (double &target : targets)
435 target = std::min(uniform() * mass, std::nextafter(mass, 0.));
436 std::sort(targets.begin(), targets.end());
437 size_t sampled = 0;
438 double cumulative = 0.;
439 for (const auto &row : rows)
440 {
441 cumulative += probability(row);
442 const size_t first = sampled;
443 while (sampled < count && targets[sampled] < cumulative)
444 ++sampled;
445 if (sampled != first)
446 {
447 if constexpr (Many)
448 {
449 for (size_t i = 0; i < bits; ++i)
450 key[i] = bit(row, i);
451 result[key] += sampled - first;
452 }
453 else
454 {
455 Types::qubit_t packed = 0;
456 for (size_t i = 0; i < bits; ++i)
457 if (bit(row, i))
458 packed |= Types::qubit_t{1} << i;
459 result[packed] += sampled - first;
460 }
461 }
462 if (sampled == count)
463 break;
464 }
465 begin += count;
466 }
467 return result;
468}
469
470// Bit(row, outputBit) preserves ordered/duplicate selections and wide labels.
471// Uniform belongs to the caller: this utility never seeds or clones an engine.
472template <bool Many, class Plan, class Bit, class Uniform> Counts<Many> Count(const Plan &plan, size_t shots, size_t bits, const Bit &bit, Uniform &&uniform)
473{
474 Counts<Many> result;
475 if (!shots || !bits)
476 return result;
477 const auto &options = plan.Settings();
478 const size_t count = plan.Categories();
479 const size_t available = plan.ScratchAvailable();
480 // Leave at least 128 unused bytes between workers' counters, including
481 // when vector storage is not aligned to a cache line.
482 constexpr size_t padding = 128 / sizeof(size_t);
483 const size_t stride = count <= (size_t{1} << 20) ? ((count + padding - 1) / padding + 1) * padding : 0;
484 const bool histogram = stride && shots >= std::max<size_t>(1024, count / 4) && stride <= available / (2 * sizeof(size_t));
485 int threads = Threads(options.multithreading, shots);
486 // Tiny alias tables are RNG-bound; staging uniforms and starting workers
487 // costs more than their few arithmetic operations in the serial draw loop.
488 if (count <= 64 && plan.Method() == Family::Alias)
489 threads = 1;
490 if (histogram)
491 threads = static_cast<int>(std::min({size_t(threads), available / (2 * sizeof(size_t) * stride), std::max<size_t>(1, shots / count)}));
492 std::vector<bool> key;
493 if constexpr (Many)
494 key.resize(bits);
495 auto insert = [&](size_t row, size_t amount) {
496 if constexpr (Many)
497 {
498 for (size_t i = 0; i < bits; ++i)
499 key[i] = bit(row, i);
500 result[key] += amount;
501 }
502 else
503 {
504 Types::qubit_t packed = 0;
505 for (size_t i = 0; i < bits; ++i)
506 if (bit(row, i))
507 packed |= Types::qubit_t{1} << i;
508 result[packed] += amount;
509 }
510 };
511
512 std::vector<size_t> bins(histogram ? stride * threads : 0, 0);
513 if (threads == 1 && (histogram || plan.Method() == Family::Alias || shots < 128 || count <= 65536))
514 {
515 for (size_t shot = 0; shot < shots; ++shot)
516 {
517 if (histogram)
518 ++bins[plan.SampleCategory(uniform())];
519 else
520 insert(plan.Sample(uniform()), 1);
521 }
522 }
523 else
524 {
525 const size_t scratch = available - bins.size() * sizeof(size_t);
526 const size_t batch = std::max<size_t>(1, std::min({shots, options.batchSize, scratch / 32}));
527 std::vector<double> uniforms;
528 std::vector<size_t> sampled;
529 for (size_t begin = 0; begin < shots;)
530 {
531 const size_t size = std::min(batch, shots - begin);
532 uniforms.resize(size);
533 for (double &value : uniforms)
534 value = uniform();
535 if (histogram)
536 {
537#ifdef _OPENMP
538#pragma omp parallel if (threads > 1) num_threads(threads)
539#endif
540 {
541 size_t worker = 0;
542#ifdef _OPENMP
543 worker = static_cast<size_t>(omp_get_thread_num());
544#endif
545 size_t *local = bins.data() + worker * stride;
546#ifdef _OPENMP
547#pragma omp for schedule(static)
548#endif
549 for (int64_t i = 0; i < static_cast<int64_t>(size); ++i)
550 ++local[plan.SampleCategory(uniforms[i])];
551 }
552 }
553 else
554 {
555 plan.Draw(uniforms, sampled, threads);
556 for (size_t row : sampled)
557 insert(row, 1);
558 }
559 begin += size;
560 }
561 }
562 if (histogram)
563 for (size_t row = 0; row < count; ++row)
564 {
565 size_t amount = bins[row];
566 for (int worker = 1; worker < threads; ++worker)
567 amount += bins[size_t(worker) * stride + row];
568 if (amount)
569 insert(plan.SourceRow(row), amount);
570 }
571 return result;
572}
573} // namespace Utils::Sampling
double Probability(void *sim, unsigned long long int outcome)
void SetAliasTable(size_t count, const Populate &populate)
Definition AliasBase.h:22
size_t SampleIndex(double v) const
Definition AliasBase.h:105
static size_t MinimumMemoryBytes(size_t count, size_t shots, size_t budget=Options{}.memoryBytes)
Definition Sampling.h:141
size_t SampleCategory(double uniform) const
Definition Sampling.h:232
Family Method() const
Definition Sampling.h:237
size_t ScratchAvailable() const
Definition Sampling.h:247
size_t Categories() const
Definition Sampling.h:222
void Draw(const std::vector< double > &uniforms, std::vector< size_t > &out, int threads) const
Definition Sampling.h:282
const Options & Settings() const
Definition Sampling.h:242
size_t Sample(double uniform) const
Definition Sampling.h:253
size_t Size() const
Definition Sampling.h:217
size_t SourceRow(size_t category) const
Definition Sampling.h:227
Prepared(size_t count, size_t shots, Probability probability, Options options={})
Definition Sampling.h:149
size_t SourceRow(size_t row) const
Definition Sampling.h:125
ProbabilityAlias(size_t count, size_t sourceCount, double mass, const Probability &probability, size_t blockSize, const std::vector< double > &masses)
Definition Sampling.h:90
size_t Sample(double value) const
Definition Sampling.h:120
uint_fast64_t qubit_t
The type of a qubit.
Definition Types.h:23
Counts< Many > Count(const Plan &plan, size_t shots, size_t bits, const Bit &bit, Uniform &&uniform)
Definition Sampling.h:472
bool ValidateSetting(std::string_view name, std::string_view setting)
Definition Sampling.h:45
int Threads(bool permitted, size_t work)
Definition Sampling.h:65
void CheckMass(double total)
Definition Sampling.h:80
Counts< Many > Single(size_t bits, const Bit &bit)
Definition Sampling.h:375
void ValidateQubits(const Qubits &qubits, size_t width, bool many)
Definition Sampling.h:394
std::unordered_map< std::conditional_t< Many, std::vector< bool >, Types::qubit_t >, Types::qubit_t > Counts
Definition Sampling.h:373
Counts< Many > CountCdfRange(const Range &rows, size_t shots, size_t bits, const Probability &probability, const Bit &bit, Uniform &&uniform, Options options={})
Definition Sampling.h:407