-
Notifications
You must be signed in to change notification settings - Fork 370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Simulate QuantumCircuit
without QObj
#1717
Conversation
72722ce
to
17f8151
Compare
c726fb5
to
34b995f
Compare
34b995f
to
4111ce8
Compare
14b61c1
to
4c07b30
Compare
4c07b30
to
3c0e6cc
Compare
0be3e96
to
2223888
Compare
QuantumCircuit
without QObj
77b431f
to
687f2d6
Compare
687f2d6
to
a6ee7c3
Compare
@mtreinish I added |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the updates, overall this looks much better to me. I left some comments inline nothing major though. I also skimmed the C++ nothing really stood out to me, but there are a lot of small details and I might have missed something (especially all the boilerplate in pybind used to call the inner functions), hopefully we have sufficient test coverage.
config = AerConfig() | ||
config.memory_slots = memory_slots | ||
config.n_qubits = num_qubits | ||
for key, value in backend_options.__dict__.items(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should really have an API for doing this natively in the Options
class so we can either do iter(options)
or options.items()
directly. I'll open an issue on terra to add this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See: Qiskit/qiskit#9704
config.memory_slots = memory_slots | ||
config.n_qubits = num_qubits |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not that it really matters since this class really isn't user facing. I'm surprised you didn't just add these to the constructor for AerConfig
qiskit_aer/backends/aer_compiler.py
Outdated
if hasattr(circuit, "metadata") and circuit.metadata: | ||
header.metadata = copy(circuit.metadata) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can simplify this a little:
if hasattr(circuit, "metadata") and circuit.metadata: | |
header.metadata = copy(circuit.metadata) | |
if circuit.metadata is not None: | |
header.metadata = circuit.metadata.copy() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That being said do we even need to copy here? since we're just passing the header to c++ I assume it will be copied before we mutate anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not necessary now. Copy may become necessary if we need to add some additional data to it.
qiskit_aer/backends/aer_compiler.py
Outdated
# TODO parallel_map will improve performance for multi circuit assembly. | ||
# However, it calls pickling AerCircuit in Linux environment. Until AerCircuit | ||
# supports pickle, circuits are assembleed sequentially in a single thread |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will say this can be tricky in python, the pickle overhead for sending data between processes (or the spawn overhead on non-linux) can end up being slower than the actual assembly. It might be worth it for sufficiently large circuits, but I'd worry doing this in parallel will be more expensive than serially for small circuits. We probably also don't need parallel_map()
here so we can control the parallelization more directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. Pickling of pybind object includes many calls of connection between C++ and python and they are expensive also in general.
def test_parameterized_qobj_qasm_save_expval(self): | ||
"""Test parameterized qobj with Expectation Value snapshot and qasm simulator.""" | ||
shots = 1000 | ||
labels = save_expval_labels() * 3 | ||
counts_targets = save_expval_counts(shots) * 3 | ||
value_targets = save_expval_pre_meas_values() * 3 | ||
|
||
backend = AerSimulator() | ||
qobj = self.parameterized_qobj(backend=backend, | ||
shots=1000, | ||
measure=True, | ||
snapshot=True) | ||
self.assertIn('parameterizations', qobj.to_dict()['config']) | ||
with self.assertWarns(DeprecationWarning): | ||
job = backend.run(qobj, **self.BACKEND_OPTS) | ||
result = job.result() | ||
success = getattr(result, 'success', False) | ||
num_circs = len(result.to_dict()['results']) | ||
self.assertTrue(success) | ||
self.compare_counts(result, | ||
range(num_circs), | ||
counts_targets, | ||
delta=0.1 * shots) | ||
# Check snapshots | ||
for j, target in enumerate(value_targets): | ||
data = result.data(j) | ||
for label in labels: | ||
self.assertAlmostEqual( | ||
data[label], target[label], delta=1e-7) | ||
|
||
def test_parameterized_qobj_statevector(self): | ||
"""Test parameterized qobj with Expectation Value snapshot and qasm simulator.""" | ||
statevec_targets = save_expval_final_statevecs() * 3 | ||
|
||
backend = AerSimulator(method="statevector") | ||
qobj = self.parameterized_qobj( | ||
backend=backend, measure=False, snapshot=False, save_state=True, | ||
) | ||
self.assertIn('parameterizations', qobj.to_dict()['config']) | ||
with self.assertWarns(DeprecationWarning): | ||
job = backend.run(qobj, **self.BACKEND_OPTS) | ||
result = job.result() | ||
success = getattr(result, 'success', False) | ||
num_circs = len(result.to_dict()['results']) | ||
self.assertTrue(success) | ||
|
||
for j in range(num_circs): | ||
statevector = result.get_statevector(j) | ||
np.testing.assert_array_almost_equal( | ||
statevector, statevec_targets[j].data, decimal=7) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are these tests deleted? Shouldn't these still work as is because the input qobj type won't trigger the new code.
for q, c in zip(qubits, clbits): | ||
circuit.append(CircuitInstruction(Measure(), [q], [c])) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason to do this instead of just?:
for q, c in zip(qubits, clbits): | |
circuit.append(CircuitInstruction(Measure(), [q], [c])) | |
for q, c in zip(qubits, clbits): | |
circuit.measure(q, c) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because other codes in this test file call the same way :-) I will update whole measurement in this test file.
qc_2.measure_all() | ||
circuits.append(qc_2) | ||
circuits.append(qc) | ||
backend = self.backend() | ||
shots = 100 | ||
result = backend.run(circuits, shots=shots).result() | ||
result = backend.run(circuits, shots=shots, method='statevector').result() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this test specifically to run without a method set? (the method name is auto_method
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not why this test was modified :-)
// executor=None, | ||
// max_job_size=None, | ||
// max_shot_size=None, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these needed? Or is it just to document the python side fields we're not specifying here, if so can you put a comment above that says "missing options from python" or something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this LGTM now. I'm a bit concerned about the size of this PR and whether I missed something. It's hard for me to feel confident that everything looks correct at ~3k lines especially as there is a lot of code that is just mapping things from python to c++, or from json to config which is easy to typo. Hopefully our test coverage will protect against this (although it's not ever going to be perfect).
For future reference it would be really good if you could try to split this up into smaller logical changes and leave unrelated refactors out (for example the format()
to f-string). Like for me a good work flow would have been to have a series of PRs like:
- introduce the c++ Config class
- Add pybind AerConfig class
- Add pybind AerCircuit class
- Update AerBackend (and subclasses) to use AerCircuit during
run()
Smaller logical changes like this would have made this far easier to review.
config = AerConfig() | ||
config.memory_slots = memory_slots | ||
config.n_qubits = num_qubits | ||
for key, value in backend_options.__dict__.items(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See: Qiskit/qiskit#9704
self.assertEqual(result.status, 'PARTIAL COMPLETED') | ||
self.assertTrue(hasattr(result.results[1].data, 'counts')) | ||
self.assertFalse(hasattr(result.results[0].data, 'counts')) | ||
result = backend.run(circuits, shots=shots, method='statevector').result() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also this test is not asserting anything anymore. Before we merge can you please update this so it's back it's original state.
I'm sorry that I made this PR too big... I totally agree with you. Adding to the change your requested, I found some bugs when I worked for other PR to reduce warning. Let me add few more... |
qc_2.measure_all() | ||
circuits.append(qc_2) | ||
circuits.append(qc) | ||
backend = self.backend() | ||
shots = 100 | ||
result = backend.run(circuits, shots=shots).result() | ||
result = backend.run(circuits, shots=shots, method='statevector').result() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
result = backend.run(circuits, shots=shots, method='statevector').result() | |
result = backend.run(circuits, shots=shots).result() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method='statevector'
is necessary to generate PARTIAL COMPLETED
result.
This state is generated in C++ side. Originally, QuantumVolume
instruction is passed to C++ in a circuit and then simulation of the circuit is failed though other circuits succeed. With this PR, QuantumVolume
is failed to be converted to AerCircuit
in Python side and PARTIAL COMPLETED
is not generated. That is the reason why this test code changed to create 50-qubit circuit instead of QuantumVolume.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this LGTM now, given the caveats in my previous review message. I think we can move forward on merging this now. Thanks for the quick updates.
Summary
This PR produces a way to simulate circuits without calling
assemble()
Details and comments
As written in #1683, Aer has some issues (#1660, #1652) caused by calling
assemble()
. This PR will resolve them.AerCircuit
First, this introduces new classes to bind C++ structure:
AerCircuit
which is mapped toAER::Circuit
class in C++.AerCircuit
is generated fromQuantumCricuit
inaer_compiler
.execute()
forstd::vector<AerCircuit>
inaer_controller_execute
Next, this provides
execute(std::vector<AER::Circuit> &circuits, Noise::NoiseModel &noise_model, json_t& config)
interface for python to start simulation. It returns the same result with thataer_controller_execute()(qobj)
returns.In
execute()
, parameterizations and circuit initialization are performed asqobj.hpp
does.AerBackend._run_circuits()
,_execute_circuits()
, andAerJob
Finally, a job
AerJob
is submitted in_run_circuits()
andAerBackend._execute_circuits_job()
is called in the method withQuantumCircuit
,NoiseModel
, and config(dict
). TheQuantumCircuit
is converted toAerCircuit
in_execute_circuits_job()
and thenAerCircuit
s are simulated in_execute_circuits()
. Existing path to simulate QObj still remains by using_run_qobj()
,_execute_qobj_job()
, and_execute_qobj()
.Note that, in DASK execution,
qobj
is still necessary. Therefore, in this PR, ifexecutor
is specified, simulators generateqobj
and start simulation with the old path.