Skip to content

Commit

Permalink
Fix performance regression from QuantumError.to_dict (Qiskit#1408)
Browse files Browse the repository at this point in the history
Fixes issue with QuantumError.to_dict causing a huge performance regression in noisy simulations from its calling of `qiskit.assemble` inside Pybind11 code, which resulted in unintended nested python multiprocessing processes.
  • Loading branch information
chriseclectic committed Dec 16, 2021
1 parent 61b028b commit 273fc3a
Show file tree
Hide file tree
Showing 8 changed files with 104 additions and 83 deletions.
6 changes: 2 additions & 4 deletions qiskit/providers/aer/noise/errors/quantum_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from qiskit.circuit.exceptions import CircuitError
from qiskit.circuit.library.generalized_gates import PauliGate
from qiskit.circuit.library.standard_gates import IGate
from qiskit.compiler import assemble
from qiskit.exceptions import QiskitError
from qiskit.extensions import UnitaryGate
from qiskit.quantum_info.operators.base_operator import BaseOperator
Expand Down Expand Up @@ -453,13 +452,12 @@ def error_term(self, position):

def to_dict(self):
"""Return the current error as a dictionary."""
qobj = assemble(self.circuits)
instructions = [exp.to_dict()['instructions'] for exp in qobj.experiments]
error = {
"type": "qerror",
"id": self.id,
"operations": [],
"instructions": instructions,
"instructions": [[op[0].assemble().to_dict() for op in circ.data]
for circ in self._circs],
"probabilities": list(self.probabilities)
}
return error
Expand Down
8 changes: 8 additions & 0 deletions releasenotes/notes/fix-qerror-assemble-9919a93b210ca776.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
fixes:
- |
Fix performance regression in noisy simulations due to large increase in
serialization overhead for loading noise models from Python into C++
resulting from unintended nested Python multiprocessing calls.
See `issue 1407 <https://github.com/Qiskit/qiskit-aer/issues/1407>`__
for details.
40 changes: 15 additions & 25 deletions src/controllers/aer_controller.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,6 @@ class Controller {
}
return max_memory_mb_;
}
// Truncate and remap input qubits
bool enable_truncation_ = true;

// The maximum number of threads to use for various levels of parallelization
int max_parallel_threads_;
Expand Down Expand Up @@ -390,8 +388,6 @@ class Controller {
//-------------------------------------------------------------------------

void Controller::set_config(const json_t &config) {
// Load validation threshold
JSON::get_value(enable_truncation_, "enable_truncation", config);

// Load validation threshold
JSON::get_value(validation_threshold_, "validation_threshold", config);
Expand Down Expand Up @@ -846,37 +842,31 @@ Result Controller::execute(const inputdata_t &input_qobj) {
// Start QOBJ timer
auto timer_start = myclock_t::now();

Noise::NoiseModel noise_model;
json_t config;
// Initialize QOBJ
Qobj qobj(input_qobj);
auto qobj_time_taken =
std::chrono::duration<double>(myclock_t::now() - timer_start).count();

// Check for config
if (Parser<inputdata_t>::get_value(config, "config", input_qobj)) {
// Set config
set_config(config);
// Load noise model
Parser<json_t>::get_value(noise_model, "noise_model", config);
}
// Set config
set_config(qobj.config);

// Initialize qobj
Qobj qobj;
if (noise_model.has_nonlocal_quantum_errors()) {
// Non-local noise does not work with optimized initialization
qobj = Qobj(input_qobj, false);
} else {
qobj = Qobj(input_qobj, enable_truncation_);
}
// Run qobj circuits
auto result = execute(qobj.circuits, qobj.noise_model, qobj.config);

// Add QOBJ loading time
result.metadata.add(qobj_time_taken, "time_taken_load_qobj");

auto result = execute(qobj.circuits, noise_model, config);
// Get QOBJ id and pass through header to result
result.qobj_id = qobj.id;
if (!qobj.header.empty()) {
result.header = qobj.header;
}

// Stop the timer and add total timing data including qobj parsing
auto timer_stop = myclock_t::now();
auto time_taken =
std::chrono::duration<double>(timer_stop - timer_start).count();
std::chrono::duration<double>(myclock_t::now() - timer_start).count();
result.metadata.add(time_taken, "time_taken");

return result;
} catch (std::exception &e) {
// qobj was invalid, return valid output containing error message
Expand Down Expand Up @@ -1007,7 +997,7 @@ Result Controller::execute(std::vector<Circuit> &circuits,
auto timer_stop = myclock_t::now();
auto time_taken =
std::chrono::duration<double>(timer_stop - timer_start).count();
result.metadata.add(time_taken, "time_taken");
result.metadata.add(time_taken, "time_taken_execute");
}
// If execution failed return valid output reporting error
catch (std::exception &e) {
Expand Down
32 changes: 27 additions & 5 deletions src/framework/qobj.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <vector>

#include "framework/circuit.hpp"
#include "noise/noise_model.hpp"

namespace AER {

Expand All @@ -40,7 +41,7 @@ class Qobj {

// Deserialization constructor
template <typename inputdata_t>
Qobj(const inputdata_t &input, bool truncation = false);
Qobj(const inputdata_t &input);

//----------------------------------------------------------------
// Data
Expand All @@ -50,6 +51,7 @@ class Qobj {
std::vector<Circuit> circuits; // List of circuits
json_t header; // (optional) passed through to result
json_t config; // (optional) qobj level config data
Noise::NoiseModel noise_model; // (optional) noise model
};

//============================================================================
Expand All @@ -60,7 +62,7 @@ class Qobj {
inline void from_json(const json_t &js, Qobj &qobj) { qobj = Qobj(js); }

template <typename inputdata_t>
Qobj::Qobj(const inputdata_t &input, bool truncation) {
Qobj::Qobj(const inputdata_t &input) {
// Check required fields
if (Parser<inputdata_t>::get_value(id, "qobj_id", input) == false) {
throw std::invalid_argument(R"(Invalid qobj: no "qobj_id" field)");
Expand All @@ -73,9 +75,29 @@ Qobj::Qobj(const inputdata_t &input, bool truncation) {
throw std::invalid_argument(R"(Invalid qobj: no "experiments" field.)");
}

// Get header and config;
Parser<inputdata_t>::get_value(config, "config", input);
Parser<inputdata_t>::get_value(header, "header", input);
// Apply qubit truncation
bool truncation = true;

// Parse config
if (Parser<inputdata_t>::get_value(config, "config", input)) {
// Check for truncation option
Parser<json_t>::get_value(truncation, "enable_truncation", config);

// Load noise model
if (Parser<json_t>::get_value(noise_model, "noise_model", config)) {
// If noise model has non-local errors disable trunction
if (noise_model.has_nonlocal_quantum_errors()) {
truncation = false;
}
}
} else {
config = json_t::object();
}

// Parse header
if (!Parser<inputdata_t>::get_value(header, "header", input)) {
header = json_t::object();
}

// Check for fixed simulator seed
// If simulator seed is set, each experiment will be set to a fixed (but different) seed
Expand Down
34 changes: 28 additions & 6 deletions test/terra/backends/aer_simulator/test_noise.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
from qiskit.providers.aer import noise

import qiskit.quantum_info as qi
from qiskit import transpile
from qiskit.circuit import QuantumCircuit, Reset
from qiskit.circuit.library import QFT
from qiskit.circuit.library.standard_gates import IGate, HGate
from qiskit.quantum_info.states.densitymatrix import DensityMatrix
from test.terra.backends.simulator_test_case import (
SimulatorTestCase, supported_methods)
from test.terra.reference import ref_kraus_noise
Expand Down Expand Up @@ -156,15 +158,35 @@ def test_kraus_gate_noise(self, method, device):
def test_kraus_gate_noise_on_QFT(self, method, device):
"""Test Kraus noise on a QFT circuit"""
shots = 10000
noise_model = ref_kraus_noise.kraus_gate_error_noise_models_full()

# Build noise model
error1 = noise.amplitude_damping_error(0.2)
error2 = error1.tensor(error1)
noise_model = noise.NoiseModel()
noise_model.add_all_qubit_quantum_error(error1, ['h'])
noise_model.add_all_qubit_quantum_error(error2, ['cp', 'swap'])

backend = self.backend(
method=method, device=device, noise_model=noise_model)
circuit = QFT(3).decompose()
circuit.measure_all()
ref_target = ref_kraus_noise.kraus_gate_error_counts_on_QFT(shots)
result = backend.run(circuit, shots=shots).result()
ideal_circuit = transpile(QFT(3), backend)

# manaully build noise circuit
noise_circuit = QuantumCircuit(3)
for inst, qargs, cargs in ideal_circuit.data:
noise_circuit.append(inst, qargs, cargs)
if inst.name == "h":
noise_circuit.append(error1, qargs)
elif inst.name in ["cp", "swap"]:
noise_circuit.append(error2, qargs)
# compute target counts
noise_state = DensityMatrix(noise_circuit)
ref_target = {i: shots * p for i, p in noise_state.probabilities_dict().items()}

# Run sim
ideal_circuit.measure_all()
result = backend.run(ideal_circuit, shots=shots).result()
self.assertSuccess(result)
self.compare_counts(result, [circuit], [ref_target], delta=0.1 * shots)
self.compare_counts(result, [ideal_circuit], [ref_target], hex_counts=False, delta=0.1 * shots)

@supported_methods(ALL_METHODS)
def test_clifford_circuit_noise(self, method, device):
Expand Down
8 changes: 4 additions & 4 deletions test/terra/backends/aer_simulator/test_truncate.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def create_circuit_for_truncate(self):
circuit.measure(1, 1)
return circuit

def deveice_backend(self):
def device_backend(self):
return mock.FakeQuito()

def test_truncate_ideal_sparse_circuit(self):
Expand All @@ -65,7 +65,7 @@ def test_truncate_default(self):
[0, 1], [1, 2], [2, 3], [3, 4], [4, 5],
[5, 6], [6, 7], [7, 8], [8, 9], [9, 0]
]
noise_model = NoiseModel.from_backend(self.deveice_backend())
noise_model = NoiseModel.from_backend(self.device_backend())
backend = self.backend(noise_model=noise_model)
circuit = transpile(
self.create_circuit_for_truncate(),
Expand All @@ -78,7 +78,7 @@ def test_truncate_default(self):

def test_truncate_non_measured_qubits(self):
"""Test truncation of non-measured uncoupled qubits."""
noise_model = NoiseModel.from_backend(self.deveice_backend())
noise_model = NoiseModel.from_backend(self.device_backend())
backend = self.backend(noise_model=noise_model)
circuit = transpile(
self.create_circuit_for_truncate(),
Expand All @@ -95,7 +95,7 @@ def test_truncate_disable_noise(self):
[0, 1], [1, 2], [2, 3], [3, 4], [4, 5],
[5, 6], [6, 7], [7, 8], [8, 9], [9, 0]
]
noise_model = NoiseModel.from_backend(self.deveice_backend())
noise_model = NoiseModel.from_backend(self.device_backend())
backend = self.backend(noise_model=noise_model, enable_truncation=False)
circuit = transpile(
self.create_circuit_for_truncate(),
Expand Down
Loading

0 comments on commit 273fc3a

Please sign in to comment.