-
Notifications
You must be signed in to change notification settings - Fork 0
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
Fixes for speedier Python-Julia interaction #32
Changes from 12 commits
0d4d4c4
80c67f7
05870ba
58ccd81
8fd3340
5f0a2d1
88a55d4
7140d2d
509479e
f550cab
3e244c1
3c6735b
716bd9c
b62a8f9
967dea4
228932f
2280398
e18ad32
b10144a
0041a2b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
name: Benchmark | ||
|
||
on: | ||
push: | ||
branches: [ main ] | ||
pull_request: | ||
|
||
jobs: | ||
benchmark: | ||
runs-on: ubuntu-latest | ||
timeout-minutes: 30 | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- name: Install juliaup | ||
uses: julia-actions/[email protected] | ||
with: | ||
channel: '1' | ||
- name: Update Julia registry | ||
shell: julia --project=. --color=yes {0} | ||
run: | | ||
using Pkg | ||
Pkg.Registry.update() | ||
- name: Set up Python | ||
uses: actions/setup-python@v5 | ||
with: | ||
python-version: 3.9 | ||
- name: Install dependencies | ||
run: | | ||
pip install -e .[test] # to put juliapkg.json in sys.path | ||
python -c 'import juliacall' # force install of all deps | ||
- name: Benchmark | ||
run: | | ||
pytest -n 0 benchmark/benchmark.py --benchmark-json benchmark/output.json | ||
- name: Store benchmark result | ||
uses: benchmark-action/github-action-benchmark@v1 | ||
with: | ||
name: Python Benchmark with pytest-benchmark | ||
tool: 'pytest' | ||
output-file-path: benchmark/output.json | ||
github-token: ${{ secrets.GITHUB_TOKEN }} | ||
auto-push: true | ||
# Show alert with commit comment on detecting possible performance regression | ||
alert-threshold: '200%' | ||
comment-on-alert: true | ||
fail-on-alert: true |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,123 +1,97 @@ | ||
import sys | ||
import atexit | ||
import json | ||
from collections.abc import Sequence | ||
from concurrent.futures import ProcessPoolExecutor, wait | ||
from typing import List, Optional, Union | ||
from multiprocessing.pool import Pool | ||
from typing import Optional, Union | ||
|
||
import numpy as np | ||
from braket.default_simulator.simulator import BaseLocalSimulator | ||
from braket.ir.jaqcd import DensityMatrix, Probability, StateVector | ||
from braket.ir.openqasm import Program as OpenQASMProgram | ||
from braket.task_result import GateModelTaskResult | ||
|
||
from braket.simulator_v2.julia_import import setup_julia | ||
from braket.simulator_v2.julia_workers import ( | ||
_handle_julia_error, | ||
translate_and_run, | ||
translate_and_run_multiple, | ||
) | ||
|
||
__JULIA_POOL__ = None | ||
|
||
def _handle_julia_error(error): | ||
# we don't import `JuliaError` explicitly here to avoid | ||
# having to import juliacall on the main thread. we need | ||
# to call *this* function on that thread in case getting | ||
# the result from the submitted Future raises an exception | ||
if type(error).__name__ == "JuliaError": | ||
python_exception = getattr(error.exception, "alternate_type", None) | ||
if python_exception is None: | ||
py_error = error | ||
else: | ||
class_val = getattr(sys.modules["builtins"], str(python_exception)) | ||
py_error = class_val(str(error.exception.message)) | ||
raise py_error | ||
|
||
def setup_julia(): | ||
import os | ||
import sys | ||
|
||
# don't reimport if we don't have to | ||
if "juliacall" in sys.modules: | ||
os.environ["PYTHON_JULIACALL_HANDLE_SIGNALS"] = "yes" | ||
return sys.modules["juliacall"].Main | ||
else: | ||
raise error | ||
|
||
|
||
def translate_and_run( | ||
device_id: str, openqasm_ir: OpenQASMProgram, shots: int = 0 | ||
) -> str: | ||
jl = setup_julia() | ||
jl_shots = shots | ||
jl_inputs = ( | ||
jl.Dict[jl.String, jl.Any]( | ||
jl.Pair(jl.convert(jl.String, k), jl.convert(jl.Any, v)) | ||
for (k, v) in openqasm_ir.inputs.items() | ||
) | ||
if openqasm_ir.inputs | ||
else jl.Dict[jl.String, jl.Any]() | ||
) | ||
if device_id == "braket_sv_v2": | ||
device = jl.BraketSimulator.StateVectorSimulator(0, 0) | ||
elif device_id == "braket_dm_v2": | ||
device = jl.BraketSimulator.DensityMatrixSimulator(0, 0) | ||
|
||
try: | ||
result = jl.BraketSimulator.simulate._jl_call_nogil( | ||
device, | ||
openqasm_ir.source, | ||
jl_inputs, | ||
jl_shots, | ||
for k, default in ( | ||
("PYTHON_JULIACALL_HANDLE_SIGNALS", "yes"), | ||
("PYTHON_JULIACALL_THREADS", "auto"), | ||
("PYTHON_JULIACALL_OPTLEVEL", "3"), | ||
# let the user's Conda/Pip handle installing things | ||
("JULIA_CONDAPKG_BACKEND", "Null"), | ||
): | ||
os.environ[k] = os.environ.get(k, default) | ||
# install Julia and any packages as needed | ||
os.environ["PYTHON_JULIAPKG_OFFLINE"] = "yes" | ||
import juliacall | ||
|
||
jl = juliacall.Main | ||
jl.seval("using JSON3, BraketSimulator") | ||
sv_stock_oq3 = """ | ||
OPENQASM 3.0; | ||
input float theta; | ||
qubit[2] q; | ||
h q[0]; | ||
cnot q; | ||
x q[0]; | ||
xx(theta) q; | ||
yy(theta) q; | ||
zz(theta) q; | ||
#pragma braket result expectation z(q[0]) | ||
""" | ||
dm_stock_oq3 = """ | ||
OPENQASM 3.0; | ||
input float theta; | ||
qubit[2] q; | ||
h q[0]; | ||
x q[0]; | ||
cnot q; | ||
xx(theta) q; | ||
yy(theta) q; | ||
zz(theta) q; | ||
#pragma braket result probability | ||
""" | ||
r = jl.BraketSimulator.simulate( | ||
"braket_sv_v2", sv_stock_oq3, '{"theta": 0.1}', 0 | ||
) | ||
py_result = str(result) | ||
return py_result | ||
except Exception as e: | ||
_handle_julia_error(e) | ||
|
||
|
||
def translate_and_run_multiple( | ||
device_id: str, | ||
programs: Sequence[OpenQASMProgram], | ||
shots: Optional[int] = 0, | ||
inputs: Optional[Union[dict, Sequence[dict]]] = {}, | ||
) -> List[str]: | ||
jl = setup_julia() | ||
irs = jl.Vector[jl.String]() | ||
is_single_input = isinstance(inputs, dict) or len(inputs) == 1 | ||
py_inputs = {} | ||
if (is_single_input and isinstance(inputs, dict)) or not is_single_input: | ||
py_inputs = [inputs.copy() for _ in range(len(programs))] | ||
elif is_single_input and not isinstance(inputs, dict): | ||
py_inputs = [inputs[0].copy() for _ in range(len(programs))] | ||
else: | ||
py_inputs = inputs | ||
jl_inputs = jl.Vector[jl.Dict[jl.String, jl.Any]]() | ||
for p_ix, program in enumerate(programs): | ||
irs.append(program.source) | ||
if program.inputs: | ||
jl_inputs.append(program.inputs | py_inputs[p_ix]) | ||
else: | ||
jl_inputs.append(py_inputs[p_ix]) | ||
|
||
if device_id == "braket_sv_v2": | ||
device = jl.BraketSimulator.StateVectorSimulator(0, 0) | ||
elif device_id == "braket_dm_v2": | ||
device = jl.BraketSimulator.DensityMatrixSimulator(0, 0) | ||
|
||
try: | ||
results = jl.BraketSimulator.simulate._jl_call_nogil( | ||
device, | ||
irs, | ||
jl_inputs, | ||
shots, | ||
jl.JSON3.write(r) | ||
r = jl.BraketSimulator.simulate( | ||
"braket_dm_v2", dm_stock_oq3, '{"theta": 0.1}', 0 | ||
) | ||
py_results = [str(result) for result in results] | ||
except Exception as e: | ||
_handle_julia_error(e) | ||
return py_results | ||
jl.JSON3.write(r) | ||
return jl | ||
|
||
|
||
def setup_pool(): | ||
global __JULIA_POOL__ | ||
__JULIA_POOL__ = Pool(processes=1) | ||
__JULIA_POOL__.apply(setup_julia) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Assuming this will be used by the batched executions, should this take in a param for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, because Julia handles the batches itself behind the scenes. We only need one process to chat back and forth with Julia. |
||
atexit.register(__JULIA_POOL__.join) | ||
atexit.register(__JULIA_POOL__.close) | ||
return | ||
|
||
Comment on lines
+80
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this thread safe? what if two Python threads call this at the same time? (i.e. should you wrap this in some kind of lock?) |
||
|
||
class BaseLocalSimulatorV2(BaseLocalSimulator): | ||
def __init__(self, device: str): | ||
global __JULIA_POOL__ | ||
if __JULIA_POOL__ is None: | ||
setup_pool() | ||
self._device = device | ||
executor = ProcessPoolExecutor(max_workers=1, initializer=setup_julia) | ||
|
||
def no_op(): | ||
pass | ||
|
||
# trigger worker creation here, because workers are created | ||
# on an as-needed basis, *not* when the executor is created | ||
f = executor.submit(no_op) | ||
wait([f]) | ||
self._executor = executor | ||
|
||
def __del__(self): | ||
self._executor.shutdown(wait=False) | ||
|
||
def initialize_simulation(self, **kwargs): | ||
return | ||
|
@@ -143,18 +117,17 @@ def run_openqasm( | |
as a result type when shots=0. Or, if StateVector and Amplitude result types | ||
are requested when shots>0. | ||
""" | ||
f = self._executor.submit( | ||
translate_and_run, | ||
self._device, | ||
openqasm_ir, | ||
shots, | ||
) | ||
global __JULIA_POOL__ | ||
try: | ||
jl_result = f.result() | ||
jl_result = __JULIA_POOL__.apply( | ||
translate_and_run, | ||
[self._device, openqasm_ir, shots], | ||
) | ||
except Exception as e: | ||
_handle_julia_error(e) | ||
|
||
result = GateModelTaskResult.parse_raw_schema(jl_result) | ||
result = GateModelTaskResult(**json.loads(jl_result)) | ||
jl_result = None | ||
result.additionalMetadata.action = openqasm_ir | ||
|
||
# attach the result types | ||
|
@@ -183,21 +156,19 @@ def run_multiple( | |
list[GateModelTaskResult]: A list of result objects, with the ith object being | ||
the result of the ith program. | ||
""" | ||
f = self._executor.submit( | ||
translate_and_run_multiple, | ||
self._device, | ||
programs, | ||
shots, | ||
inputs, | ||
) | ||
global __JULIA_POOL__ | ||
try: | ||
jl_results = f.result() | ||
jl_results = __JULIA_POOL__.apply( | ||
translate_and_run_multiple, | ||
[self._device, programs, shots, inputs], | ||
) | ||
except Exception as e: | ||
_handle_julia_error(e) | ||
|
||
results = [ | ||
GateModelTaskResult.parse_raw_schema(jl_result) for jl_result in jl_results | ||
GateModelTaskResult(**json.loads(jl_result)) for jl_result in jl_results | ||
] | ||
jl_results = None | ||
for p_ix, program in enumerate(programs): | ||
results[p_ix].additionalMetadata.action = program | ||
|
||
|
This file was deleted.
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.
Do we want to benchmark across multiple python version?
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 certainly can, I just wanted to be sparing with the amount of resource use at first.