Maestro 0.3.11
Unified interface for quantum circuit simulation
Loading...
Searching...
No Matches
DistributedGpuState.h
Go to the documentation of this file.
1// Statevector bridge shared by the local and MPI distributed plugins.
2#pragma once
3#if defined(__linux__) && defined(INCLUDED_BY_FACTORY)
8#include <charconv>
9#include <limits>
10#include <numeric>
11#include <sstream>
12
13namespace Simulators::Private
14{
15class DistributedGpuState : public ISimulator
16{
17 public:
18 explicit DistributedGpuState(bool mpi = false) : mpi(mpi)
19 {
20 }
21
22 void Initialize() override
23 {
24 InitializeData(nrQubits, nullptr);
25 }
26
27 void InitializeState(size_t n, std::vector<std::complex<double>> &values) override
28 {
29 InitializeVector(n, values);
30 }
31
32 void InitializeState(size_t n, Eigen::VectorXcd &values) override
33 {
34 InitializeVector(n, values);
35 }
36#ifndef NO_QISKIT_AER
37 void InitializeState(size_t n, AER::Vector<std::complex<double>> &values) override
38 {
39 InitializeVector(n, values);
40 }
41#endif
42 void Configure(const char *key, const char *value) override
43 {
44 if (!key || !value)
45 throw std::invalid_argument("Null distributed GPU configuration");
46 const std::string k(key), v(value);
47 if (k == "precision" || k == "use_double_precision")
48 {
49 Configuration::ParsePrecision(k, v);
50 // Fixed once allocated; a reapplied precision is ignored until Clear.
51 if (state)
52 return;
53 configuration.SetConfiguration(k, v);
54 return;
55 }
56 if (k != "seed" && configuration.WasApplied(k, v))
57 return;
58 static const std::unordered_set<std::string> distributionKeys{"distributed_devices",
59 "distributed_global_qubits",
60 "distributed_flags",
61 "distributed_backend",
62 "distributed_max_queued_gates",
63 "distributed_transfer_workspace_bytes",
64 "distributed_snapshot_storage",
65 "mpi_communicator",
66 "mpi_p2p_bits"};
67 if ((k.compare(0, 12, "distributed_") == 0 || k.compare(0, 4, "mpi_") == 0) && !distributionKeys.count(k))
68 throw std::invalid_argument("Unknown distributed GPU option: " + k);
69 if (!mpi && k.compare(0, 4, "mpi_") == 0)
70 throw std::invalid_argument("MPI options require DistributedMpiGpu");
71 if (k == "method" && v != "statevector")
72 throw std::invalid_argument("Distributed GPU simulators currently support only statevector");
73 const bool allocationOption =
74 k == "gpu_device" || k == "precision" || k == "use_double_precision" || k.compare(0, 12, "distributed_") == 0 || k.compare(0, 4, "mpi_") == 0;
75 if (state && allocationOption)
76 throw std::logic_error("Clear the distributed GPU state before changing allocation "
77 "settings");
78 if (k == "gpu_device")
79 Configuration::ParseGpuDevice(v);
80 if (k == "seed")
81 {
82 auto seed = ParseUnsigned(v);
83 SeedAuxiliaryRng(seed);
84 if (state)
85 state->SetSeed(seed);
86 }
87 if (k == "distributed_devices" || k == "distributed_global_qubits")
88 ParseList(v);
89 if (k == "distributed_flags" || k == "distributed_max_queued_gates" || k == "distributed_transfer_workspace_bytes" || k == "mpi_p2p_bits")
90 ParseUnsigned(v);
91 if (k == "mpi_communicator")
92 ParseCommunicator(v);
93 if (k == "distributed_snapshot_storage" && v != "host" && v != "gpu")
94 throw std::invalid_argument("distributed_snapshot_storage must be host or gpu");
95 if (k == "distributed_flags" && ParseUnsigned(v) > 31)
96 throw std::invalid_argument("Unsupported distribution flags");
97 if (k == "distributed_max_queued_gates" && (ParseUnsigned(v) < 1 || ParseUnsigned(v) > 65536))
98 throw std::invalid_argument("Queue size must be 1..65536");
99 if (k == "mpi_p2p_bits" && ParseUnsigned(v) > 5)
100 throw std::invalid_argument("mpi_p2p_bits must be 0..5");
101 if (k == "distributed_backend" && v != "ex" && v != "conventional")
102 throw std::invalid_argument("distributed_backend must be ex or conventional");
103 if (mpi && k == "distributed_backend" && v != "ex")
104 throw std::invalid_argument("MPI requires the Ex backend");
105 configuration.SetConfiguration(k, v);
106 }
107
108 std::string GetConfiguration(const char *key) const override
109 {
110 const std::string k(key);
111 if (k == "method")
112 return "statevector";
113 if (k == "precision" || k == "use_double_precision")
114 {
115 const bool useDouble = state ? state->IsDoublePrecision() : configuration.GetConfiguration("precision") == "double";
116 return k == "precision" ? (useDouble ? "double" : "single") : (useDouble ? "1" : "0");
117 }
118 if (state && (k == "distributed_shard_devices" || k == "distributed_configured_global_qubits" || k == "distributed_qubit_layout"))
119 {
120 std::vector<int32_t> values(k == "distributed_qubit_layout" ? nrQubits : 32);
121 int count = k == "distributed_shard_devices" ? state->GetShardDevices(values.data(), values.size())
122 : k == "distributed_configured_global_qubits" ? state->GetGlobalQubits(values.data(), values.size())
123 : state->GetQubitLayout(values.data(), values.size());
124 values.resize(count);
125 return Join(values);
126 }
127 return configuration.GetConfiguration(key);
128 }
129
130 const std::unordered_map<std::string, std::string> &GetConfigMap() const override
131 {
132 return configuration.GetConfigMap();
133 }
134
135 size_t AllocateQubits(size_t count) override
136 {
137 if (state)
138 throw std::logic_error("Clear before allocating distributed GPU qubits");
139 if (count >= 63 || nrQubits >= 63 - count)
140 throw std::invalid_argument("Distributed GPU requires fewer than 63 qubits");
141 auto old = nrQubits;
142 nrQubits += count;
143 return old;
144 }
145
146 size_t GetNumberOfQubits() const override
147 {
148 return nrQubits;
149 }
150
151 void Clear() override
152 {
153 state.reset();
154 nrQubits = 0;
155 }
156
157 void Reset() override
158 {
159 Native().Reset();
160 }
161
162 size_t Measure(const Types::qubits_vector &qubits) override
163 {
164 auto bits = MeasureMany(qubits);
165 size_t result = 0;
166 for (size_t i = 0; i < bits.size(); ++i)
167 if (bits[i])
168 result |= size_t{1} << i;
169 return result;
170 }
171
172 std::vector<bool> MeasureMany(const Types::qubits_vector &qubits) override
173 {
174 auto qb = Qubits(qubits);
175 if (qb.empty())
176 return {};
177 std::vector<int> values(qb.size());
178 Native().MeasureQubitsCollapse(qb.data(), values.data(), values.size());
179 NotifyObservers(qubits);
180 return std::vector<bool>(values.begin(), values.end());
181 }
182
183 void ApplyReset(const Types::qubits_vector &qubits) override
184 {
185 auto bits = MeasureMany(qubits);
186 for (size_t i = 0; i < qubits.size(); ++i)
187 if (bits[i])
188 ApplyX(qubits[i]);
189 }
190
191 double Probability(Types::qubit_t outcome) override
192 {
193 return Native().BasisStateProbability(outcome);
194 }
195
196 std::complex<double> Amplitude(Types::qubit_t outcome) override
197 {
198 double real, imag;
199 Native().Amplitude(outcome, &real, &imag);
200 return {real, imag};
201 }
202
203 std::complex<double> AmplitudeRaw(Types::qubit_t outcome) override
204 {
205 return Amplitude(outcome);
206 }
207
208 std::complex<double> ProjectOnZero() override
209 {
210 return Amplitude(0);
211 }
212
213 std::vector<double> AllProbabilities() override
214 {
215 std::vector<double> values(size_t{1} << nrQubits);
216 Native().AllProbabilities(values.data());
217 return values;
218 }
219
220 std::vector<double> Probabilities(const Types::qubits_vector &outcomes) override
221 {
222 std::vector<double> values;
223 for (auto outcome : outcomes)
224 values.push_back(Probability(outcome));
225 return values;
226 }
227
228 std::unordered_map<Types::qubit_t, Types::qubit_t> SampleCounts(const Types::qubits_vector &qubits, size_t shots = 1000) override
229 {
230 auto qb = Qubits(qubits);
231 std::unordered_map<Types::qubit_t, Types::qubit_t> result;
232 if (qb.empty() || !shots)
233 return result;
234 if (shots > std::numeric_limits<unsigned>::max())
235 throw std::invalid_argument("Too many GPU samples");
236 std::vector<long> samples(shots);
237 Native().Sample(shots, samples.data(), qb.size(), qb.data());
238 for (auto sample : samples)
239 ++result[static_cast<Types::qubit_t>(sample)];
240 return result;
241 }
242
243 std::unordered_map<std::vector<bool>, Types::qubit_t> SampleCountsMany(const Types::qubits_vector &qubits, size_t shots = 1000) override
244 {
245 std::unordered_map<std::vector<bool>, Types::qubit_t> result;
246 for (const auto &[outcome, count] : SampleCounts(qubits, shots))
247 {
248 std::vector<bool> bits(qubits.size());
249 for (size_t i = 0; i < bits.size(); ++i)
250 bits[i] = (outcome >> i) & 1;
251 result[bits] += count;
252 }
253 return result;
254 }
255
256 double ExpectationValue(const std::string &pauli) override
257 {
258 return Native().ExpectationValue(pauli.c_str(), pauli.size());
259 }
260
261 SimulatorType GetType() const override
262 {
263 return mpi ? SimulatorType::kDistMpiGpuSim : SimulatorType::kDistGpuSim;
264 }
265
266 SimulationType GetSimulationType() const override
267 {
268 return SimulationType::kStatevector;
269 }
270
271 int GetGpuDevice() const override
272 {
273 return state ? state->GetStateVectorGpuId() : -1;
274 }
275
276 void Flush() override
277 {
278 Native().Synchronize();
279 }
280
281 void SaveStateToInternalDestructive() override
282 {
283 Native().SaveStateDestructive();
284 }
285
287 {
288 Native().RestoreStateFreeSaved();
289 }
290
291 void SaveState() override
292 {
293 if (configuration.GetConfiguration("distributed_snapshot_storage") == "host")
294 Native().SaveStateToHost();
295 else
296 Native().SaveState();
297 }
298
299 void RestoreState() override
300 {
301 Native().RestoreStateNoFreeSaved();
302 }
303
304 void SetMultithreading(bool = true) override
305 {
306 }
307
308 bool GetMultithreading() const override
309 {
310 return !mpi;
311 }
312
313 bool IsQcsim() const override
314 {
315 return false;
316 }
317
319 {
320 return Native().MeasureAllQubitsNoCollapse();
321 }
322
323 std::vector<bool> MeasureNoCollapseMany() override
324 {
325 auto outcome = MeasureNoCollapse();
326 std::vector<bool> bits(nrQubits);
327 for (size_t i = 0; i < bits.size(); ++i)
328 bits[i] = (outcome >> i) & 1;
329 return bits;
330 }
331
332 protected:
333 int QubitIndex(Types::qubit_t q) const
334 {
335 if (q >= nrQubits)
336 throw std::out_of_range("Distributed GPU qubit is out of range");
337 return static_cast<int>(q);
338 }
339
340 DistributedGpuLibStateVectorSim &Native() const
341 {
342 if (!state)
343 throw std::logic_error("Distributed GPU state is not initialized");
344 return *state;
345 }
346
347 static uint64_t ParseUnsigned(const std::string &v)
348 {
349 uint64_t result = 0;
350 auto parsed = std::from_chars(v.data(), v.data() + v.size(), result);
351 if (parsed.ec != std::errc() || parsed.ptr != v.data() + v.size())
352 throw std::invalid_argument("Expected an unsigned integer: " + v);
353 return result;
354 }
355
356 static int64_t ParseCommunicator(const std::string &value)
357 {
358 size_t used = 0;
359 const auto handle = std::stoll(value, &used);
360 if (used != value.size())
361 throw std::invalid_argument("Invalid MPI communicator handle");
362 return handle;
363 }
364
365 static int ParseInt(const std::string &v)
366 {
367 int result = 0;
368 auto parsed = std::from_chars(v.data(), v.data() + v.size(), result);
369 if (parsed.ec != std::errc() || parsed.ptr != v.data() + v.size())
370 throw std::invalid_argument("Expected an integer: " + v);
371 return result;
372 }
373
374 static std::vector<int32_t> ParseList(const std::string &value)
375 {
376 std::vector<int32_t> values;
377 if (value.empty())
378 return values;
379 size_t begin = 0;
380 do
381 {
382 const auto end = value.find(',', begin);
383 int item = ParseInt(value.substr(begin, end == std::string::npos ? end : end - begin));
384 if (item < 0)
385 throw std::invalid_argument("Distributed indices must be nonnegative");
386 values.push_back(item);
387 if (end == std::string::npos)
388 break;
389 begin = end + 1;
390 } while (true);
391 return values;
392 }
393
394 static std::string Join(const std::vector<int32_t> &values)
395 {
396 std::string result;
397 for (auto v : values)
398 {
399 if (!result.empty())
400 result += ',';
401 result += std::to_string(v);
402 }
403 return result;
404 }
405
406 std::vector<int> Qubits(const Types::qubits_vector &qubits) const
407 {
408 if (qubits.size() > nrQubits)
409 throw std::invalid_argument("Too many measured qubits");
410 std::vector<int> values;
411 std::unordered_set<Types::qubit_t> seen;
412 for (auto q : qubits)
413 {
414 if (q >= nrQubits || !seen.insert(q).second)
415 throw std::invalid_argument("Invalid or duplicate qubit");
416 values.push_back(static_cast<int>(q));
417 }
418 return values;
419 }
420
421 uint64_t Option(const char *key, uint64_t fallback) const
422 {
423 return configuration.IsSet(key) ? ParseUnsigned(configuration.GetConfiguration(key)) : fallback;
424 }
425
426 template <class V> void InitializeVector(size_t n, V &values)
427 {
428 if (!n || n >= 63 || static_cast<size_t>(values.size()) != (size_t{1} << n))
429 throw std::invalid_argument("Statevector length must equal 2^num_qubits");
430 InitializeData(n, reinterpret_cast<const double *>(values.data()));
431 }
432
433 void InitializeData(size_t n, const double *values)
434 {
435 if (state)
436 throw std::logic_error("Clear before initializing a distributed GPU state again");
437 if (!n || n >= 63)
438 throw std::invalid_argument("Distributed GPU requires 1..62 qubits");
439 int device = configuration.IsSet("gpu_device") ? Configuration::ParseGpuDevice(configuration.GetConfiguration("gpu_device")) : 0;
440 auto devices = ParseList(configuration.GetConfiguration("distributed_devices"));
441 auto globals = ParseList(configuration.GetConfiguration("distributed_global_qubits"));
442 std::unique_ptr<DistributedGpuLibStateVectorSim> next;
443 if (mpi)
444 {
445 auto lib = DistributedMpiGpuLibrary::GetInstance();
446 DistributedMpiGpuLibrary::Communicator descriptor{sizeof(DistributedMpiGpuLibrary::Communicator), 0, 0};
447 const DistributedMpiGpuLibrary::Communicator *comm = nullptr;
448 if (configuration.IsSet("mpi_communicator"))
449 {
450 descriptor.fortran_handle = ParseCommunicator(configuration.GetConfiguration("mpi_communicator"));
451 comm = &descriptor;
452 }
453 const auto info = lib->GetRuntimeInfo(comm);
454 if (!devices.empty() && devices.size() != static_cast<size_t>(info.size))
455 throw std::invalid_argument("MPI distributed_devices must contain one ordinal per rank");
456 if (!configuration.IsSet("gpu_device"))
457 device = devices.empty() ? info.default_device : devices[info.rank];
458 if (devices.empty())
459 {
460 devices.resize(info.size);
461 lib->GatherDevices(comm, device, devices);
462 }
463 next = std::make_unique<DistributedMpiGpuLibStateVectorSim>(lib, comm, device, Option("mpi_p2p_bits", 0));
464 }
465 else
466 {
467 auto lib = DistributedGpuLibrary::GetInstance();
468 if (devices.empty())
469 {
470 if (configuration.IsSet("gpu_device"))
471 devices.push_back(device);
472 else
473 {
474 lib->RequireLoaded();
475 const int count = lib->GetGpuDeviceCount();
476 if (count <= 0)
477 lib->Fail("GetGpuDeviceCount");
478 int shards = 1;
479 while (shards < 32 && shards * 2 <= count && static_cast<uint64_t>(shards * 2) <= (uint64_t{1} << (n - 1)))
480 shards *= 2;
481 for (int i = 0; i < shards; ++i)
482 devices.push_back(i);
483 }
484 }
485 const int backend = configuration.GetConfiguration("distributed_backend") == "conventional" ? 0 : 1;
486 next = std::make_unique<DistributedGpuLibStateVectorSim>(lib, lib->CreateNative(device, backend));
487 }
488 if (devices.empty() || devices.size() > 32 || (devices.size() & (devices.size() - 1)))
489 throw std::invalid_argument("distributed_devices must contain a power of two shards (1..32)");
490 size_t globalBits = 0;
491 while ((size_t{1} << globalBits) < devices.size())
492 ++globalBits;
493 if (globalBits >= n)
494 throw std::invalid_argument("Distribution requires at least one local qubit");
495 if (!configuration.IsSet("distributed_global_qubits"))
496 for (size_t i = 0; i < globalBits; ++i)
497 globals.push_back(i);
498 DistributedGpuApi::MgdDistributionConfig dist{sizeof(dist),
499 static_cast<uint32_t>(Option("distributed_flags", 0)),
500 static_cast<uint32_t>(globals.size()),
501 globals.data(),
502 static_cast<uint32_t>(devices.size()),
503 devices.data()};
504 next->ConfigureDistribution(&dist);
505 const bool useDouble = configuration.IsSet("precision") ? configuration.GetConfiguration("precision") == "double"
506 : (configuration.GetConfiguration("use_double_precision") == "1" ||
507 configuration.GetConfiguration("use_double_precision") == "true");
508 next->SetDataType(useDouble ? 1 : 0);
509 if (configuration.IsSet("distributed_max_queued_gates") || configuration.IsSet("distributed_transfer_workspace_bytes"))
510 {
511 DistributedGpuApi::MgdExExecutionConfig execution{sizeof(execution), static_cast<uint32_t>(Option("distributed_max_queued_gates", 1024)),
512 Option("distributed_transfer_workspace_bytes", 16ULL * 1024 * 1024)};
513 next->SetExExecutionConfig(&execution);
514 }
515 // Direct simulator construction also needs matching auxiliary/readout and
516 // backend streams across ranks when the caller omits a seed.
517 if (mpi && !configuration.IsSet("seed"))
518 SetSeed(GenerateRandomSeed(SimulatorType::kDistMpiGpuSim, configuration.GetConfigMap()));
519 if (configuration.IsSet("seed"))
520 next->SetSeed(Option("seed", 0));
521 if (values)
522 next->CreateWithState(n, values);
523 else
524 next->Create(n);
525 nrQubits = n;
526 state = std::move(next);
527 }
528
529 bool mpi;
530 size_t nrQubits = 0;
531 Configuration configuration;
532 std::unique_ptr<DistributedGpuLibStateVectorSim> state;
533};
534
535class DistributedMpiGpuState : public DistributedGpuState
536{
537 public:
538 DistributedMpiGpuState() : DistributedGpuState(true)
539 {
540 }
541};
542} // namespace Simulators::Private
543#endif
double Probability(void *sim, unsigned long long int outcome)
char * GetConfiguration(void *sim, const char *key)
int RestoreState(void *sim)
int ApplyReset(void *sim, const unsigned long int *qubits, unsigned long int nrQubits)
int ApplyX(void *sim, int qubit)
unsigned long int AllocateQubits(void *sim, unsigned long int nrQubits)
unsigned long int GetNumberOfQubits(void *sim)
double * AllProbabilities(void *sim)
unsigned long long int MeasureNoCollapse(void *sim)
int GetMultithreading(void *sim)
unsigned long long int Measure(void *sim, const unsigned long int *qubits, unsigned long int nrQubits)
double * Amplitude(void *sim, unsigned long long int outcome)
double * Probabilities(void *sim, const unsigned long long int *qubits, unsigned long int nrQubits)
int SetMultithreading(void *sim, int multithreading)
int SaveStateToInternalDestructive(void *sim)
int GetSimulationType(void *sim)
unsigned long long int * SampleCounts(void *sim, const unsigned long long int *qubits, unsigned long int nrQubits, unsigned long int shots)
int RestoreInternalDestructiveSavedState(void *sim)
int IsQcsim(void *sim)
int SaveState(void *sim)
std::vector< int > Qubits(const Types::qubits_vector &qubits, size_t width)
uint64_t GenerateRandomSeed(SimulatorType type, const std::unordered_map< std::string, std::string > &options={})
Definition RandomSeed.h:11
SimulationType
The type of simulation.
Definition State.h:109
SimulatorType
The type of simulator.
Definition State.h:79
std::vector< qubit_t > qubits_vector
The type of a vector of qubits.
Definition Types.h:24
uint_fast64_t qubit_t
The type of a qubit.
Definition Types.h:23