diff --git a/tensorflow_quantum/core/ops/math_ops/simulate_mps_test.py b/tensorflow_quantum/core/ops/math_ops/simulate_mps_test.py index d80629861..3ea6e4b89 100644 --- a/tensorflow_quantum/core/ops/math_ops/simulate_mps_test.py +++ b/tensorflow_quantum/core/ops/math_ops/simulate_mps_test.py @@ -437,6 +437,19 @@ def test_simulate_mps1d_samples_inputs(self): symbol_names, symbol_values_array, [[num_samples]]) + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'between 0 and .*Got 2147483647'): + # num_samples is above the supported maximum. + simulate_mps.mps_1d_sample(util.convert_to_tensor(circuit_batch), + symbol_names, symbol_values_array, + [2**31 - 1]) + + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'between 0 and .*Got -1'): + # num_samples is negative. + simulate_mps.mps_1d_sample(util.convert_to_tensor(circuit_batch), + symbol_names, symbol_values_array, [-1]) + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'Could not find symbol in parameter map'): # symbol_names tensor has the right type, but invalid value. @@ -724,6 +737,15 @@ def test_simulate_mps1d_sampled_expectation_inputs(self): util.convert_to_tensor([[x] for x in pauli_sums]), num_samples[0]) + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'less than or equal to .*Got 2147483647'): + # num_samples is above the supported maximum. + simulate_mps.mps_1d_sampled_expectation( + util.convert_to_tensor(circuit_batch), symbol_names, + symbol_values_array, + util.convert_to_tensor([[x] for x in pauli_sums]), + [[2**31 - 1]] * batch_size) + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'Unparseable proto'): # circuit tensor has the right type but invalid values. diff --git a/tensorflow_quantum/core/ops/noise/noisy_expectation_op_test.py b/tensorflow_quantum/core/ops/noise/noisy_expectation_op_test.py index 953829318..deafcd3bf 100644 --- a/tensorflow_quantum/core/ops/noise/noisy_expectation_op_test.py +++ b/tensorflow_quantum/core/ops/noise/noisy_expectation_op_test.py @@ -213,6 +213,15 @@ def test_noisy_expectation_inputs(self): [[-1]] * batch_size) # pylint: enable=too-many-function-args + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'less than or equal to .*Got 2147483647'): + # num_samples is above the supported maximum. + noisy_expectation_op.expectation( + util.convert_to_tensor(circuit_batch), symbol_names, + symbol_values_array, + util.convert_to_tensor([[x] for x in pauli_sums]), + [[2**31 - 1]] * batch_size) + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, expected_regex='do not match'): # wrong symbol_values size. diff --git a/tensorflow_quantum/core/ops/noise/noisy_sampled_expectation_op_test.py b/tensorflow_quantum/core/ops/noise/noisy_sampled_expectation_op_test.py index c76122070..2b03e0cb9 100644 --- a/tensorflow_quantum/core/ops/noise/noisy_sampled_expectation_op_test.py +++ b/tensorflow_quantum/core/ops/noise/noisy_sampled_expectation_op_test.py @@ -213,6 +213,15 @@ def test_noisy_expectation_inputs(self): [[-1]] * batch_size) # pylint: enable=too-many-function-args + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'less than or equal to .*Got 2147483647'): + # num_samples is above the supported maximum. + noisy_sampled_expectation_op.sampled_expectation( + util.convert_to_tensor(circuit_batch), symbol_names, + symbol_values_array, + util.convert_to_tensor([[x] for x in pauli_sums]), + [[2**31 - 1]] * batch_size) + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, expected_regex='do not match'): # wrong symbol_values size. diff --git a/tensorflow_quantum/core/ops/noise/noisy_samples_op_test.py b/tensorflow_quantum/core/ops/noise/noisy_samples_op_test.py index 7790bda8b..20e0ab27c 100644 --- a/tensorflow_quantum/core/ops/noise/noisy_samples_op_test.py +++ b/tensorflow_quantum/core/ops/noise/noisy_samples_op_test.py @@ -97,6 +97,34 @@ def test_simulate_samples_inputs(self): symbol_names, symbol_values_array, [[num_samples]]) + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'between 0 and .*Got 2147483647'): + # num_samples is above the supported maximum. + noisy_samples_op.samples(util.convert_to_tensor(circuit_batch), + symbol_names, symbol_values_array, + [2**31 - 1]) + + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'between 0 and .*Got -1'): + # num_samples is negative. + noisy_samples_op.samples(util.convert_to_tensor(circuit_batch), + symbol_names, symbol_values_array, [-1]) + + # num_samples of 0 is still accepted and yields empty samples. + zero_samples = noisy_samples_op.samples( + util.convert_to_tensor(circuit_batch), symbol_names, + symbol_values_array, [0]) + self.assertEqual(zero_samples.to_list(), [[]] * batch_size) + + # The maximum itself (kMaxNumSamples) is accepted. An empty batch keeps + # the output empty, so this does not actually draw any samples. + empty_circuits = tf.raw_ops.Empty(shape=(0,), dtype=tf.string) + empty_symbols = tf.raw_ops.Empty(shape=(0,), dtype=tf.string) + empty_values = tf.raw_ops.Empty(shape=(0, 0), dtype=tf.float32) + max_samples = noisy_samples_op.samples(empty_circuits, empty_symbols, + empty_values, [10000000]) + self.assertEqual(max_samples.shape[0], 0) + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'Unparseable proto'): # programs tensor has the right type, but invalid value. diff --git a/tensorflow_quantum/core/ops/parse_context.cc b/tensorflow_quantum/core/ops/parse_context.cc index 04f6324d5..6f6b1a0a1 100644 --- a/tensorflow_quantum/core/ops/parse_context.cc +++ b/tensorflow_quantum/core/ops/parse_context.cc @@ -369,10 +369,13 @@ tensorflow::Status GetNumSamples( sub_parsed_num_samples.reserve(matrix_num_samples.dimension(1)); for (unsigned int j = 0; j < matrix_num_samples.dimension(1); j++) { const int num_samples = matrix_num_samples(i, j); - if (num_samples < 1) { - return Status(static_cast( - absl::StatusCode::kInvalidArgument), - "Each element of num_samples must be greater than 0."); + if (num_samples < 1 || num_samples > kMaxNumSamples) { + return Status( + static_cast( + absl::StatusCode::kInvalidArgument), + absl::StrCat("Each element of num_samples must be greater than 0 " + "and less than or equal to ", + kMaxNumSamples, ". Got ", num_samples, ".")); } sub_parsed_num_samples.push_back(num_samples); } @@ -407,7 +410,16 @@ Status GetIndividualSample(tensorflow::OpKernelContext* context, vector_num_samples.dimension(0), ".")); } - (*n_samples) = vector_num_samples(0); + const int num_samples = vector_num_samples(0); + if (num_samples < 0 || num_samples > kMaxNumSamples) { + return Status( + static_cast( + absl::StatusCode::kInvalidArgument), + absl::StrCat("num_samples must be between 0 and ", kMaxNumSamples, + " inclusive. Got ", num_samples, ".")); + } + + (*n_samples) = num_samples; return ::tensorflow::Status(); } diff --git a/tensorflow_quantum/core/ops/parse_context.h b/tensorflow_quantum/core/ops/parse_context.h index 6a2e99315..21257169a 100644 --- a/tensorflow_quantum/core/ops/parse_context.h +++ b/tensorflow_quantum/core/ops/parse_context.h @@ -96,13 +96,26 @@ tensorflow::Status GetPauliSums( tensorflow::Status GetSymbolMaps(tensorflow::OpKernelContext* context, std::vector* maps); -// Parses the number of samples from the 'num_samples' input tensor. +// Upper bound on the number of samples accepted for any single circuit or +// pauli sum. The per-thread work split in BalanceTrajectory and in the noisy +// ops' ComputeSmall uses int arithmetic such as +// (num_samples + num_threads - 1) / num_threads, which overflows when +// num_samples is near INT_MAX; the resulting negative chunk sizes make the +// worker threads skip sampling and leave the output tensor uninitialized. +// 1e7 is far above practical shot counts while leaving more than 200x +// headroom to INT_MAX for any thread count. +inline constexpr int kMaxNumSamples = 10000000; + +// Parses the number of samples from the 'num_samples' input tensor. Every +// element must be in the range [1, kMaxNumSamples]. tensorflow::Status GetNumSamples( tensorflow::OpKernelContext* context, std::vector>* parsed_num_samples); // Parses the 'num_samples' input tensor when it is expected to only -// contain one element. +// contain one element. The element must be in the range +// [0, kMaxNumSamples]; 0 is legal because the sampling ops return an empty +// result for it. tensorflow::Status GetIndividualSample(tensorflow::OpKernelContext* context, int* n_samples); diff --git a/tensorflow_quantum/core/ops/tfq_simulate_ops_test.py b/tensorflow_quantum/core/ops/tfq_simulate_ops_test.py index 93c1770e2..e794f60f1 100644 --- a/tensorflow_quantum/core/ops/tfq_simulate_ops_test.py +++ b/tensorflow_quantum/core/ops/tfq_simulate_ops_test.py @@ -400,6 +400,20 @@ def test_simulate_samples_inputs(self): util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, [[num_samples]]) + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'between 0 and .*Got 2147483647'): + # num_samples is above the supported maximum. + tfq_simulate_ops.tfq_simulate_samples( + util.convert_to_tensor(circuit_batch), symbol_names, + symbol_values_array, [2**31 - 1]) + + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'between 0 and .*Got -1'): + # num_samples is negative. + tfq_simulate_ops.tfq_simulate_samples( + util.convert_to_tensor(circuit_batch), symbol_names, + symbol_values_array, [-1]) + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'Unparseable proto'): # programs tensor has the right type, but invalid value. @@ -672,6 +686,24 @@ def test_simulate_sampled_expectation_inputs(self): util.convert_to_tensor([[x] for x in pauli_sums]), [[-1]] * batch_size) + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'less than or equal to .*Got 2147483647'): + # num_samples is above the supported maximum. + tfq_simulate_ops.tfq_simulate_sampled_expectation( + util.convert_to_tensor(circuit_batch), symbol_names, + symbol_values_array, + util.convert_to_tensor([[x] for x in pauli_sums]), + [[2**31 - 1]] * batch_size) + + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'less than or equal to .*Got 2147483647'): + # Only the last element of num_samples is out of range. + tfq_simulate_ops.tfq_simulate_sampled_expectation( + util.convert_to_tensor(circuit_batch), symbol_names, + symbol_values_array, + util.convert_to_tensor([[x] for x in pauli_sums]), + [[10]] * (batch_size - 1) + [[2**31 - 1]]) + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, expected_regex='do not match'): # wrong symbol_values size.