Skip to content

Commit 6f3e0d5

Browse files
Allow lightning.qubit/kokkos::generate_samples to take in seeds to make the generated samples deterministic (#927)
### Before submitting Please complete the following checklist when submitting a PR: - [x] All new features must include a unit test. If you've fixed a bug or added code that should be tested, add a test to the [`tests`](../tests) directory! - [x] All new functions and code must be clearly commented and documented. If you do make documentation changes, make sure that the docs build and render correctly by running `make docs`. - [x] Ensure that the test suite passes, by running `make test`. - [x] Add a new entry to the `.github/CHANGELOG.md` file, summarizing the change, and including a link back to the PR. - [x] Ensure that code is properly formatted by running `make format`. When all the above are checked, delete everything above the dashed line and fill in the pull request template. ------------------------------------------------------------------------------------------------------------ **Context:** [A while ago](PennyLaneAI/catalyst#936) a new `seed` option to `qjit` was added. The seed was used to make measurement results deterministic, but samples were still probabilistic. This is because within a `qjit` context, [measurements were controlled from the catalyst repo](https://github.com/PennyLaneAI/catalyst/blob/a580bada575793b780d5366aa77dff6157cd4f93/runtime/lib/backend/common/Utils.hpp#L274) , but samples were controlled by lightning. To resolve stochastically failing tests (i.e. flaky tests) in catalyst, we add seeding for samples in lightning. **Description of the Change:** When `qjit(seed=...)` receives a (unsigned 32 bit int) seed value from the user, the seed gets propagated through mlir and [generates a `std::mt19937` rng instance in the catalyst execution context](https://github.com/PennyLaneAI/catalyst/blob/934726fe750043886415953dbd89a4c4ddeb9a80/runtime/lib/capi/ExecutionContext.hpp#L268). This rng instance eventually becomes a field of the `Catalyst::Runtime::Simulator::LightningSimulator` (and kokkos) class [catalyst/runtime/lib/backend/lightning/lightning_dynamic/LightningSimulator.hpp](https://github.com/PennyLaneAI/catalyst/blob/a580bada575793b780d5366aa77dff6157cd4f93/runtime/lib/backend/lightning/lightning_dynamic/LightningSimulator.hpp#L54). To seed samples, catalyst uses this device rng instance on the state vector's `generate_samples` methods: PennyLaneAI/catalyst#1164. In lightning, the `generate_samples` method now takes in a seeding number. The catalyst devices pass in a seed into the lightning `generate_samples`; this seed is created deterministically from the aforementioned already seeded catalyst context rng instance. This makes the generated samples deterministc. **Benefits:** Fewer (hopefully no) stochatically failing frontend tests in catalyst. **Possible Drawbacks:** **Related GitHub Issues:** [sc-72878] --------- Co-authored-by: ringo-but-quantum <[email protected]>
1 parent fdf09bc commit 6f3e0d5

File tree

10 files changed

+135
-60
lines changed

10 files changed

+135
-60
lines changed

.github/CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040

4141
### Improvements
4242

43+
* The `generate_samples` methods of lightning.{qubit/kokkos} can now take in a seed number to make the generated samples deterministic. This can be useful when, among other things, fixing flaky tests in CI.
44+
[(#927)](https://github.com/PennyLaneAI/pennylane-lightning/pull/927)
45+
4346
* Always decompose `qml.QFT` in Lightning.
4447
[(#924)](https://github.com/PennyLaneAI/pennylane-lightning/pull/924)
4548

@@ -111,7 +114,7 @@
111114

112115
This release contains contributions from (in alphabetical order):
113116

114-
Ali Asadi, Amintor Dusko, Luis Alfredo Nuñez Meneses, Vincent Michaud-Rioux, Lee J. O'Riordan, Mudit Pandey, Shuli Shu
117+
Ali Asadi, Amintor Dusko, Luis Alfredo Nuñez Meneses, Vincent Michaud-Rioux, Lee J. O'Riordan, Mudit Pandey, Shuli Shu, Haochen Paul Wang
115118

116119
---
117120

pennylane_lightning/core/_version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@
1616
Version number (major.minor.patch[-label])
1717
"""
1818

19-
__version__ = "0.39.0-dev37"
19+
__version__ = "0.39.0-dev38"

pennylane_lightning/core/src/measurements/MeasurementsBase.hpp

-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ template <class StateVectorT, class Derived> class MeasurementsBase {
7777
/**
7878
* @brief Randomly set the seed of the internal random generator
7979
*
80-
* @param seed Seed
8180
*/
8281
void setRandomSeed() {
8382
std::random_device rd;

pennylane_lightning/core/src/measurements/tests/Test_MeasurementsBase.cpp

+16-4
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ using Pennylane::Util::isApproxEqual;
2020
} // namespace
2121
/// @endcond
2222
#include <algorithm>
23+
#include <optional>
2324
#include <string>
2425

2526
#ifdef _ENABLE_PLQUBIT
@@ -1251,7 +1252,9 @@ TEST_CASE("Var Shot- TensorProdObs", "[MeasurementsBase][Observables]") {
12511252
testTensorProdObsVarShot<TestStateVectorBackends>();
12521253
}
12531254
}
1254-
template <typename TypeList> void testSamples() {
1255+
1256+
template <typename TypeList>
1257+
void testSamples(const std::optional<std::size_t> &seed = std::nullopt) {
12551258
if constexpr (!std::is_same_v<TypeList, void>) {
12561259
using StateVectorT = typename TypeList::Type;
12571260
using PrecisionT = typename StateVectorT::PrecisionT;
@@ -1281,7 +1284,10 @@ template <typename TypeList> void testSamples() {
12811284
std::size_t num_qubits = 3;
12821285
std::size_t N = std::pow(2, num_qubits);
12831286
std::size_t num_samples = 100000;
1284-
auto &&samples = Measurer.generate_samples(num_samples);
1287+
auto &&samples =
1288+
seed.has_value()
1289+
? Measurer.generate_samples(num_samples, seed.value())
1290+
: Measurer.generate_samples(num_samples);
12851291

12861292
std::vector<std::size_t> counts(N, 0);
12871293
std::vector<std::size_t> samples_decimal(num_samples, 0);
@@ -1307,7 +1313,7 @@ template <typename TypeList> void testSamples() {
13071313
REQUIRE_THAT(probabilities,
13081314
Catch::Approx(expected_probabilities).margin(.05));
13091315
}
1310-
testSamples<typename TypeList::Next>();
1316+
testSamples<typename TypeList::Next>(seed);
13111317
}
13121318
}
13131319

@@ -1317,6 +1323,12 @@ TEST_CASE("Samples", "[MeasurementsBase]") {
13171323
}
13181324
}
13191325

1326+
TEST_CASE("Seeded samples", "[MeasurementsBase]") {
1327+
if constexpr (BACKEND_FOUND) {
1328+
testSamples<TestStateVectorBackends>(37);
1329+
}
1330+
}
1331+
13201332
template <typename TypeList> void testSamplesCountsObs() {
13211333
if constexpr (!std::is_same_v<TypeList, void>) {
13221334
using StateVectorT = typename TypeList::Type;
@@ -1729,4 +1741,4 @@ TEST_CASE("Measure Shot - SparseHObs ", "[MeasurementsBase][Observables]") {
17291741
if constexpr (BACKEND_FOUND) {
17301742
testSparseHObsMeasureShot<TestStateVectorBackends>();
17311743
}
1732-
}
1744+
}

pennylane_lightning/core/src/simulators/lightning_gpu/measurements/MeasurementsGPU.hpp

+9-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <cuda.h>
2626
#include <cusparse.h>
2727
#include <custatevec.h> // custatevecApplyMatrix
28+
#include <optional>
2829
#include <random>
2930
#include <type_traits>
3031
#include <unordered_map>
@@ -218,7 +219,9 @@ class Measurements final
218219
* be accessed using the stride sample_id*num_qubits, where sample_id is a
219220
* number between 0 and num_samples-1.
220221
*/
221-
auto generate_samples(std::size_t num_samples) -> std::vector<std::size_t> {
222+
auto generate_samples(std::size_t num_samples,
223+
const std::optional<std::size_t> &seed = std::nullopt)
224+
-> std::vector<std::size_t> {
222225
std::vector<double> rand_nums(num_samples);
223226
custatevecSamplerDescriptor_t sampler;
224227

@@ -238,7 +241,11 @@ class Measurements final
238241
data_type = CUDA_C_32F;
239242
}
240243

241-
this->setRandomSeed();
244+
if (seed.has_value()) {
245+
this->setSeed(seed.value());
246+
} else {
247+
this->setRandomSeed();
248+
}
242249
std::uniform_real_distribution<PrecisionT> dis(0.0, 1.0);
243250
for (std::size_t n = 0; n < num_samples; n++) {
244251
rand_nums[n] = dis(this->rng);

pennylane_lightning/core/src/simulators/lightning_kokkos/catalyst/LightningKokkosSimulator.cpp

+15-24
Original file line numberDiff line numberDiff line change
@@ -342,13 +342,22 @@ void LightningKokkosSimulator::PartialProbs(
342342
std::move(dv_probs.begin(), dv_probs.end(), probs.begin());
343343
}
344344

345-
void LightningKokkosSimulator::Sample(DataView<double, 2> &samples,
346-
std::size_t shots) {
345+
std::vector<size_t> LightningKokkosSimulator::GenerateSamples(size_t shots) {
346+
// generate_samples is a member function of the Measures class.
347347
Pennylane::LightningKokkos::Measures::Measurements<StateVectorT> m{
348348
*(this->device_sv)};
349+
349350
// PL-Lightning-Kokkos generates samples using the alias method.
350351
// Reference: https://en.wikipedia.org/wiki/Inverse_transform_sampling
351-
auto li_samples = m.generate_samples(shots);
352+
if (this->gen) {
353+
return m.generate_samples(shots, (*(this->gen))());
354+
}
355+
return m.generate_samples(shots);
356+
}
357+
358+
void LightningKokkosSimulator::Sample(DataView<double, 2> &samples,
359+
std::size_t shots) {
360+
auto li_samples = this->GenerateSamples(shots);
352361

353362
RT_FAIL_IF(samples.size() != li_samples.size(),
354363
"Invalid size for the pre-allocated samples");
@@ -381,13 +390,7 @@ void LightningKokkosSimulator::PartialSample(
381390
// get device wires
382391
auto &&dev_wires = getDeviceWires(wires);
383392

384-
// generate_samples is a member function of the MeasuresKokkos class.
385-
Pennylane::LightningKokkos::Measures::Measurements<StateVectorT> m{
386-
*(this->device_sv)};
387-
388-
// PL-Lightning-Kokkos generates samples using the alias method.
389-
// Reference: https://en.wikipedia.org/wiki/Inverse_transform_sampling
390-
auto li_samples = m.generate_samples(shots);
393+
auto li_samples = this->GenerateSamples(shots);
391394

392395
// The lightning samples are layed out as a single vector of size
393396
// shots*qubits, where each element represents a single bit. The
@@ -411,13 +414,7 @@ void LightningKokkosSimulator::Counts(DataView<double, 1> &eigvals,
411414
RT_FAIL_IF(eigvals.size() != numElements || counts.size() != numElements,
412415
"Invalid size for the pre-allocated counts");
413416

414-
// generate_samples is a member function of the MeasuresKokkos class.
415-
Pennylane::LightningKokkos::Measures::Measurements<StateVectorT> m{
416-
*(this->device_sv)};
417-
418-
// PL-Lightning-Kokkos generates samples using the alias method.
419-
// Reference: https://en.wikipedia.org/wiki/Inverse_transform_sampling
420-
auto li_samples = m.generate_samples(shots);
417+
auto li_samples = this->GenerateSamples(shots);
421418

422419
// Fill the eigenvalues with the integer representation of the corresponding
423420
// computational basis bitstring. In the future, eigenvalues can also be
@@ -455,13 +452,7 @@ void LightningKokkosSimulator::PartialCounts(
455452
// get device wires
456453
auto &&dev_wires = getDeviceWires(wires);
457454

458-
// generate_samples is a member function of the MeasuresKokkos class.
459-
Pennylane::LightningKokkos::Measures::Measurements<StateVectorT> m{
460-
*(this->device_sv)};
461-
462-
// PL-Lightning-Kokkos generates samples using the alias method.
463-
// Reference: https://en.wikipedia.org/wiki/Inverse_transform_sampling
464-
auto li_samples = m.generate_samples(shots);
455+
auto li_samples = this->GenerateSamples(shots);
465456

466457
// Fill the eigenvalues with the integer representation of the corresponding
467458
// computational basis bitstring. In the future, eigenvalues can also be

pennylane_lightning/core/src/simulators/lightning_kokkos/catalyst/LightningKokkosSimulator.hpp

+2
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ class LightningKokkosSimulator final : public Catalyst::Runtime::QuantumDevice {
9696
return res;
9797
}
9898

99+
auto GenerateSamples(size_t shots) -> std::vector<size_t>;
100+
99101
public:
100102
explicit LightningKokkosSimulator(const std::string &kwargs = "{}") {
101103
auto &&args = Catalyst::Runtime::parse_kwargs(kwargs);

pennylane_lightning/core/src/simulators/lightning_kokkos/catalyst/tests/Test_LightningKokkosMeasures.cpp

+63-18
Original file line numberDiff line numberDiff line change
@@ -1754,26 +1754,71 @@ TEST_CASE("Counts and PartialCounts tests with numWires=0-4 shots=100",
17541754
}
17551755

17561756
TEST_CASE("Measurement with a seeded device", "[Measures]") {
1757-
for (std::size_t _ = 0; _ < 5; _++) {
1758-
std::unique_ptr<LKSimulator> sim = std::make_unique<LKSimulator>();
1759-
std::unique_ptr<LKSimulator> sim1 = std::make_unique<LKSimulator>();
1757+
std::array<std::unique_ptr<LKSimulator>, 2> sims;
1758+
std::vector<std::mt19937> gens{std::mt19937{37}, std::mt19937{37}};
17601759

1761-
std::mt19937 gen(37);
1762-
sim->SetDevicePRNG(&gen);
1760+
auto circuit = [](LKSimulator &sim, std::mt19937 &gen) {
1761+
sim.SetDevicePRNG(&gen);
17631762
std::vector<intptr_t> Qs;
17641763
Qs.reserve(1);
1765-
Qs.push_back(sim->AllocateQubit());
1766-
sim->NamedOperation("Hadamard", {}, {Qs[0]}, false);
1767-
auto m = sim->Measure(Qs[0]);
1768-
1769-
std::mt19937 gen1(37);
1770-
sim1->SetDevicePRNG(&gen1);
1771-
std::vector<intptr_t> Qs1;
1772-
Qs1.reserve(1);
1773-
Qs1.push_back(sim1->AllocateQubit());
1774-
sim1->NamedOperation("Hadamard", {}, {Qs1[0]}, false);
1775-
auto m1 = sim1->Measure(Qs1[0]);
1776-
1777-
CHECK(*m == *m1);
1764+
Qs.push_back(sim.AllocateQubit());
1765+
sim.NamedOperation("Hadamard", {}, {Qs[0]}, false);
1766+
auto m = sim.Measure(Qs[0]);
1767+
return m;
1768+
};
1769+
1770+
for (std::size_t trial = 0; trial < 5; trial++) {
1771+
sims[0] = std::make_unique<LKSimulator>();
1772+
sims[1] = std::make_unique<LKSimulator>();
1773+
1774+
auto m0 = circuit(*(sims[0]), gens[0]);
1775+
auto m1 = circuit(*(sims[1]), gens[1]);
1776+
1777+
CHECK(*m0 == *m1);
1778+
}
1779+
}
1780+
1781+
TEST_CASE("Sample with a seeded device", "[Measures]") {
1782+
std::size_t shots = 100;
1783+
std::array<std::unique_ptr<LKSimulator>, 2> sims;
1784+
std::vector<std::vector<double>> sample_vec(2,
1785+
std::vector<double>(shots * 4));
1786+
1787+
std::vector<MemRefT<double, 2>> buffers{
1788+
MemRefT<double, 2>{
1789+
sample_vec[0].data(), sample_vec[0].data(), 0, {shots, 1}, {1, 1}},
1790+
MemRefT<double, 2>{
1791+
sample_vec[1].data(), sample_vec[1].data(), 0, {shots, 1}, {1, 1}},
1792+
};
1793+
std::vector<DataView<double, 2>> views{
1794+
DataView<double, 2>(buffers[0].data_aligned, buffers[0].offset,
1795+
buffers[0].sizes, buffers[0].strides),
1796+
DataView<double, 2>(buffers[1].data_aligned, buffers[1].offset,
1797+
buffers[1].sizes, buffers[1].strides)};
1798+
1799+
std::vector<std::mt19937> gens{std::mt19937{37}, std::mt19937{37}};
1800+
1801+
auto circuit = [shots](LKSimulator &sim, DataView<double, 2> &view,
1802+
std::mt19937 &gen) {
1803+
sim.SetDevicePRNG(&gen);
1804+
std::vector<intptr_t> Qs;
1805+
Qs.reserve(1);
1806+
Qs.push_back(sim.AllocateQubit());
1807+
sim.NamedOperation("Hadamard", {}, {Qs[0]}, false);
1808+
sim.NamedOperation("RX", {0.5}, {Qs[0]}, false);
1809+
sim.Sample(view, shots);
1810+
};
1811+
1812+
for (std::size_t trial = 0; trial < 5; trial++) {
1813+
sims[0] = std::make_unique<LKSimulator>();
1814+
sims[1] = std::make_unique<LKSimulator>();
1815+
1816+
for (std::size_t sim_idx = 0; sim_idx < sims.size(); sim_idx++) {
1817+
circuit(*(sims[sim_idx]), views[sim_idx], gens[sim_idx]);
1818+
}
1819+
1820+
for (std::size_t i = 0; i < sample_vec[0].size(); i++) {
1821+
CHECK((sample_vec[0][i] == sample_vec[1][i]));
1822+
}
17781823
}
17791824
}

pennylane_lightning/core/src/simulators/lightning_kokkos/measurements/MeasurementsKokkos.hpp

+11-5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#pragma once
1515
#include <chrono>
1616
#include <cstdint>
17+
#include <optional>
1718

1819
#include <Kokkos_Core.hpp>
1920
#include <Kokkos_Random.hpp>
@@ -649,13 +650,16 @@ class Measurements final
649650
* Reference https://en.wikipedia.org/wiki/Inverse_transform_sampling
650651
*
651652
* @param num_samples Number of Samples
653+
* @param seed Seed to generate the samples from
652654
*
653655
* @return std::vector<std::size_t> to the samples.
654656
* Each sample has a length equal to the number of qubits. Each sample can
655657
* be accessed using the stride sample_id*num_qubits, where sample_id is a
656658
* number between 0 and num_samples-1.
657659
*/
658-
auto generate_samples(std::size_t num_samples) -> std::vector<std::size_t> {
660+
auto generate_samples(std::size_t num_samples,
661+
const std::optional<std::size_t> &seed = std::nullopt)
662+
-> std::vector<std::size_t> {
659663
const std::size_t num_qubits = this->_statevector.getNumQubits();
660664
const std::size_t N = this->_statevector.getLength();
661665
Kokkos::View<std::size_t *> samples("num_samples",
@@ -674,10 +678,12 @@ class Measurements final
674678
});
675679

676680
// Sampling using Random_XorShift64_Pool
677-
Kokkos::Random_XorShift64_Pool<> rand_pool(
678-
std::chrono::high_resolution_clock::now()
679-
.time_since_epoch()
680-
.count());
681+
auto rand_pool = seed.has_value()
682+
? Kokkos::Random_XorShift64_Pool<>(seed.value())
683+
: Kokkos::Random_XorShift64_Pool<>(
684+
std::chrono::high_resolution_clock::now()
685+
.time_since_epoch()
686+
.count());
681687

682688
Kokkos::parallel_for(
683689
Kokkos::RangePolicy<KokkosExecSpace>(0, num_samples),

pennylane_lightning/core/src/simulators/lightning_qubit/measurements/MeasurementsLQubit.hpp

+14-4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <algorithm>
2424
#include <complex>
2525
#include <cstdio>
26+
#include <optional>
2627
#include <random>
2728
#include <type_traits>
2829
#include <unordered_map>
@@ -573,30 +574,39 @@ class Measurements final
573574
* Reference: https://en.wikipedia.org/wiki/Alias_method
574575
*
575576
* @param num_samples The number of samples to generate.
577+
* @param seed Seed to generate the samples from
576578
* @return 1-D vector of samples in binary, each sample is
577579
* separated by a stride equal to the number of qubits.
578580
*/
579-
std::vector<std::size_t> generate_samples(const std::size_t num_samples) {
581+
std::vector<std::size_t>
582+
generate_samples(const std::size_t num_samples,
583+
const std::optional<std::size_t> &seed = std::nullopt) {
580584
const std::size_t num_qubits = this->_statevector.getNumQubits();
581585
std::vector<std::size_t> wires(num_qubits);
582586
std::iota(wires.begin(), wires.end(), 0);
583-
return generate_samples(wires, num_samples);
587+
return generate_samples(wires, num_samples, seed);
584588
}
585589

586590
/**
587591
* @brief Generate samples.
588592
*
589593
* @param wires Sample are generated for the specified wires.
590594
* @param num_samples The number of samples to generate.
595+
* @param seed Seed to generate the samples from
591596
* @return 1-D vector of samples in binary, each sample is
592597
* separated by a stride equal to the number of qubits.
593598
*/
594599
std::vector<std::size_t>
595600
generate_samples(const std::vector<std::size_t> &wires,
596-
const std::size_t num_samples) {
601+
const std::size_t num_samples,
602+
const std::optional<std::size_t> &seed = std::nullopt) {
597603
const std::size_t n_wires = wires.size();
598604
std::vector<std::size_t> samples(num_samples * n_wires);
599-
this->setRandomSeed();
605+
if (seed.has_value()) {
606+
this->setSeed(seed.value());
607+
} else {
608+
this->setRandomSeed();
609+
}
600610
DiscreteRandomVariable<PrecisionT> drv{this->rng, probs(wires)};
601611
// The Python layer expects a 2D array with dimensions (n_samples x
602612
// n_wires) and hence the linear index is `s * n_wires + (n_wires - 1 -

0 commit comments

Comments
 (0)