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

Add Job Splitting Information Methods for Better User Experience #1281

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 68 additions & 10 deletions qiskit_experiments/framework/base_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,33 +265,91 @@ def _finalize(self):
"""
pass

def _run_jobs(self, circuits: List[QuantumCircuit], **run_options) -> List[Job]:
"""Run circuits on backend as 1 or more jobs."""
def _max_circuits(self, backend: Backend = None):
"""
Calculate the maximum number of circuits per job for the experiment.
"""

# set backend
if backend is None:
if self.backend is None:
raise QiskitError("A backend must be provided.")
backend = self.backend
self.backend = backend
Copy link
Collaborator

Choose a reason for hiding this comment

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

One last thing: I don't think self.backend should be set to backend here. With the current logic, if job_info() provides a new backend, then this backend will overwrite the current backend, which isn't behavior expected from a method that should just provide information. I think the PR can be merged after this line is removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @coruscating Sorry for the delay
I think this will fix it

        if backend is None:
            if self.backend is None:
                raise QiskitError("A backend must be provided.")
            backend = self.backend
        # Get max circuits for job splitting
        max_circuits_option = getattr(self.experiment_options, "max_circuits", None)
        max_circuits_backend = backend.max_circuits

image

here are the results also I tried on test_framework.py run successfully

{'Total number of circuits in the experiment': 200, 'Maximum number of circuits per job': 100, 'Total number of jobs': 2}
{'Total number of circuits in the experiment': 200, 'Maximum number of circuits per job': 50, 'Total number of jobs': 4}
{'Total number of circuits in the experiment': 200, 'Maximum number of circuits per job': 100, 'Total number of jobs': 2}
{'Total number of circuits in the experiment': 200, 'Maximum number of circuits per job': 110, 'Total number of jobs': 2}
[<qiskit_experiments.test.utils.FakeJob object at 0x0000025549DA24D0>, <qiskit_experiments.test.utils.FakeJob object at 0x0000025549DA3250>]

let me know if need further changes else I push the changes

Copy link
Collaborator

Choose a reason for hiding this comment

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

That looks good, thanks!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry, there's one more issue. backend.max_circuits only works for V2 backends, which is why we have the BackendData adaptor class that implements the max_circuits property: https://github.com/Qiskit-Extensions/qiskit-experiments/blob/cfb47e24fae4173ea106de4b84e686a7933f8589/qiskit_experiments/framework/backend_data.py#L169-L176

So instead of max_circuits_backend = backend.max_circuits you should do max_circuits_backend = BackendData(backend).max_circuits

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed

# Get max circuits for job splitting
max_circuits_option = getattr(self.experiment_options, "max_circuits", None)
max_circuits_option = getattr(self.experiment_options,
"max_circuits",
None)
max_circuits_backend = self._backend_data.max_circuits

if max_circuits_option and max_circuits_backend:
max_circuits = min(max_circuits_option, max_circuits_backend)
return min(max_circuits_option, max_circuits_backend)
elif max_circuits_option:
max_circuits = max_circuits_option
return max_circuits_option
else:
max_circuits = max_circuits_backend
return max_circuits_backend

def job_info(self, backend: Backend = None):
"""
Get information about job distribution for the experiment on a specific
backend.

Args:
backend (Backend): The backend for which to get job distribution
information.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
backend (Backend): The backend for which to get job distribution
information.
backend: Optional, the backend for which to get job distribution
information. If not specified, the experiment must already have a set backend.


Returns:
dict: A dictionary containing information about job distribution.
- "Total number of circuits in the experiment": Total number of
circuits in the experiment.
- "Maximum number of circuits": Maximum number of circuits in
one Job
- "Total number of jobs": Number of jobs needed for
distribution.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is still not building correctly, I suggest this:

Suggested change
dict: A dictionary containing information about job distribution.
- "Total number of circuits in the experiment": Total number of
circuits in the experiment.
- "Maximum number of circuits": Maximum number of circuits in
one Job
- "Total number of jobs": Number of jobs needed for
distribution.
dict: A dictionary containing information about job distribution.
- "Total number of circuits in the experiment": Total number of
circuits in the experiment.
- "Maximum number of circuits per job": Maximum number of circuits in
one job based on backend and experiment settings.
- "Total number of jobs": Number of jobs needed to run this experiment
on the currently set backend.


Raises:
QiskitError: if backend is not specified.

"""
max_circuits = self._max_circuits(backend)
total_circuits = len(self.circuits())

if max_circuits is None:
num_jobs = 1
else:
num_jobs = (total_circuits + max_circuits - 1) // max_circuits
return {
"Total number of circuits in the experiment": total_circuits,
"Maximum number of circuits per job": max_circuits,
"Total number of jobs": num_jobs
}

def _run_jobs(
self,
circuits: List[QuantumCircuit],
backend: Backend = None,
**run_options) -> List[Job]:
"""Run circuits on backend as 1 or more jobs."""
max_circuits = self._max_circuits(backend)

# Run experiment jobs
if max_circuits and len(circuits) > max_circuits:
if max_circuits and (len(circuits) > max_circuits):
# Split jobs for backends that have a maximum job size
job_circuits = [
circuits[i : i + max_circuits] for i in range(0, len(circuits), max_circuits)
circuits[i:i + max_circuits] for i in range(0,
len(circuits),
max_circuits)
]
else:
# Run as single job
job_circuits = [circuits]

# Run jobs
jobs = [self.backend.run(circs, **run_options) for circs in job_circuits]
jobs = [self.backend.run(circs, **run_options) for circs in
job_circuits]

return jobs

@abstractmethod
def circuits(self) -> List[QuantumCircuit]:
"""Return a list of experiment circuits.
Expand Down
76 changes: 76 additions & 0 deletions test/framework/test_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,79 @@ def error_message(self):
res = expdata.analysis_results()
self.assertEqual(len(res), 0)
self.assertEqual(expdata.analysis_status(), AnalysisStatus.CANCELLED)

@ddt.data(1, 10, 100)
def test_max_circuits(self, max_experiments):
"""Test running experiment with max_circuits"""

num_circuits = 10

class MyExp(BaseExperiment):
"""Some arbitrary experiment"""

def __init__(self, physical_qubits):
super().__init__(physical_qubits)

def circuits(self):
"""Generate fake circuits"""
qc = QuantumCircuit(1)
qc.measure_all()
return num_circuits * [qc]

backend = FakeBackend(max_experiments=max_experiments)
exp = MyExp([0])

# set backend
if backend is None:
if exp.backend is None:
exp.assertRaise(QiskitError)
backend = exp.backend
exp.backend = backend
# Get max circuits for job splitting
max_circuits_option = getattr(exp.experiment_options,
"max_circuits",
None)
max_circuits_backend = exp._backend_data.max_circuits
if max_circuits_option and max_circuits_backend:
result = min(max_circuits_option, max_circuits_backend)
elif max_circuits_option:
result = max_circuits_option
else:
result = max_circuits_backend

self.assertEqual(exp._max_circuits(backend=backend), result)


@ddt.data(None, 1, 10, 100)
def test_job_info(self, max_experiments):
"""Test job_info for specific backend"""

num_circuits = 10

class MyExp(BaseExperiment):
"""Some arbitrary experiment"""

def __init__(self, physical_qubits):
super().__init__(physical_qubits)

def circuits(self):
"""Generate fake circuits"""
qc = QuantumCircuit(1)
qc.measure_all()
return num_circuits * [qc]

backend = FakeBackend(max_experiments=max_experiments)
exp = MyExp([0])

if max_experiments is None:
num_jobs = 1
else:
num_jobs = (num_circuits + max_experiments - 1) // max_experiments

job_info = {
"Total number of circuits in the experiment": num_circuits,
"Maximum number of circuits per job": max_experiments,
"Total number of jobs": num_jobs
}

self.assertEqual(exp.job_info(backend=backend), job_info)
Loading