Skip to content
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

Merged
merged 33 commits into from
Mar 8, 2023
Merged

Conversation

hhorii
Copy link
Collaborator

@hhorii hhorii commented Feb 7, 2023

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.

  1. AerCircuit
    First, this introduces new classes to bind C++ structure: AerCircuit which is mapped to AER::Circuit class in C++. AerCircuit is generated from QuantumCricuit in aer_compiler.

  2. execute() for std::vector<AerCircuit> in aer_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 that aer_controller_execute()(qobj) returns.
    In execute(), parameterizations and circuit initialization are performed as qobj.hpp does.

  3. AerBackend._run_circuits(), _execute_circuits(), and AerJob
    Finally, a job AerJob is submitted in _run_circuits() and AerBackend._execute_circuits_job() is called in the method with QuantumCircuit, NoiseModel, and config(dict). The QuantumCircuit is converted to AerCircuit in _execute_circuits_job() and then AerCircuits 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, if executor is specified, simulators generate qobj and start simulation with the old path.

@hhorii hhorii added this to the Aer 0.12.0 milestone Feb 7, 2023
@hhorii hhorii force-pushed the add_own_assemble branch 2 times, most recently from c726fb5 to 34b995f Compare February 7, 2023 14:30
@hhorii hhorii requested a review from mtreinish February 7, 2023 15:07
@hhorii hhorii changed the title Add a new way to run simulation without QObj Simulate QuantumCircuit without QObj Feb 14, 2023
@mtreinish mtreinish self-assigned this Feb 15, 2023
@mtreinish mtreinish added the Changelog: New Feature Include in the Added section of the changelog label Feb 15, 2023
@hhorii
Copy link
Collaborator Author

hhorii commented Feb 27, 2023

@mtreinish I added AerConfig to reduce json overheads. Previously json was used for configuration. Aer uses nlohmann/json.hpp which produce overheads in memory allocation. By introducing AerConfig, we can reduce their overheads. I think Aer becomes always better than reference in above @adekusar-drl 's benchmark.

Copy link
Member

@mtreinish mtreinish left a 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():
Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +356 to +357
config.memory_slots = memory_slots
config.n_qubits = num_qubits
Copy link
Member

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

Comment on lines 396 to 397
if hasattr(circuit, "metadata") and circuit.metadata:
header.metadata = copy(circuit.metadata)
Copy link
Member

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:

Suggested change
if hasattr(circuit, "metadata") and circuit.metadata:
header.metadata = copy(circuit.metadata)
if circuit.metadata is not None:
header.metadata = circuit.metadata.copy()

Copy link
Member

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.

Copy link
Collaborator Author

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 Show resolved Hide resolved
Comment on lines 553 to 555
# 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
Copy link
Member

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.

Copy link
Collaborator Author

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.

Comment on lines 70 to 119
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)
Copy link
Member

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.

Comment on lines 44 to 45
for q, c in zip(qubits, clbits):
circuit.append(CircuitInstruction(Measure(), [q], [c]))
Copy link
Member

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?:

Suggested change
for q, c in zip(qubits, clbits):
circuit.append(CircuitInstruction(Measure(), [q], [c]))
for q, c in zip(qubits, clbits):
circuit.measure(q, c)

Copy link
Collaborator Author

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()
Copy link
Member

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)

Copy link
Collaborator Author

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 :-)

src/transpile/fusion.hpp Outdated Show resolved Hide resolved
Comment on lines 105 to 107
// executor=None,
// max_job_size=None,
// max_shot_size=None,
Copy link
Member

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.

@hhorii hhorii force-pushed the add_own_assemble branch from 58b17b6 to 2b1c077 Compare March 1, 2023 05:10
mtreinish
mtreinish previously approved these changes Mar 6, 2023
Copy link
Member

@mtreinish mtreinish left a 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:

  1. introduce the c++ Config class
  2. Add pybind AerConfig class
  3. Add pybind AerCircuit class
  4. 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():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()
Copy link
Member

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.

@hhorii
Copy link
Collaborator Author

hhorii commented Mar 7, 2023

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:

  1. introduce the c++ Config class
  2. Add pybind AerConfig class
  3. Add pybind AerCircuit class
  4. Update AerBackend (and subclasses) to use AerCircuit during run()

Smaller logical changes like this would have made this far easier to review.

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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
result = backend.run(circuits, shots=shots, method='statevector').result()
result = backend.run(circuits, shots=shots).result()

Copy link
Collaborator Author

@hhorii hhorii Mar 7, 2023

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.

@hhorii hhorii force-pushed the add_own_assemble branch from 402da6f to e75676d Compare March 8, 2023 01:28
Copy link
Member

@mtreinish mtreinish left a 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: New Feature Include in the Added section of the changelog
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants