-
Notifications
You must be signed in to change notification settings - Fork 10
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
Pytest benchmark override with cpu and gpu times #21
Open
MrRobot2211
wants to merge
20
commits into
qutip:main
Choose a base branch
from
MrRobot2211:pytest_benchmark_override_with_cpu_and_gpu_times
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
7d601f0
basic directory structure
MrRobot2211 6d27ed4
adds basic implementation for wrapper
MrRobot2211 5eb0159
adds basic test functionality
MrRobot2211 8262871
corrects pedantioc cupy arguments
MrRobot2211 0986f40
working first example
MrRobot2211 5c913a6
Merge branch 'main' of https://github.com/qutip/qutip-cupy into pytes…
MrRobot2211 c6352f8
make pedanticupy arguments
MrRobot2211 fef8bec
add benchmark plot generation
MrRobot2211 a6217ca
add groups dynamically in benchmark tests
MrRobot2211 7adf443
working with one benchmark
MrRobot2211 357db49
add device_id settings as settable
MrRobot2211 48dcb16
fix benchmark device setup
MrRobot2211 9da6dad
add array creation by dtype
MrRobot2211 9d541b5
Merge branch 'main' of https://github.com/qutip/qutip-cupy into pytes…
MrRobot2211 8f0d041
aadd device half precision option
MrRobot2211 f950d20
format
MrRobot2211 1d647ba
format
MrRobot2211 bda4d2b
add benchmarks to setup.cfg
MrRobot2211 f4006ef
correct plot labeling
MrRobot2211 7eac576
add extra-dependencies for benchmark
MrRobot2211 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# Remove this file after adding actual benchmarks | ||
import pytest | ||
import numpy as np | ||
import cupy as cp | ||
from qutip.core.data import Dense | ||
|
||
from qutip_cupy import CuPyDense | ||
|
||
import benchmark_tools | ||
from benchmark_tools.cpu_gpu_times_wrapper import GpuWrapper | ||
|
||
# Set device_id | ||
cp.cuda.Device(benchmark_tools._DEVICE).use() | ||
|
||
# Supported dtypes | ||
dtype_list = ["CuPyDense", "CuPyDense_half_precision", "Dense"] | ||
dtype_ids = ["CuPy", "CuPy_half", "qutip(Dense)"] | ||
|
||
|
||
@pytest.fixture(params=dtype_list, ids=dtype_ids) | ||
def dtype(request): | ||
return request.param | ||
|
||
|
||
@pytest.fixture(scope="function", params=[50, 100, 1000]) # , 4000]) | ||
def size(request): | ||
return request.param | ||
|
||
|
||
@pytest.mark.benchmark() | ||
def test_matmul(dtype, size, benchmark, request): | ||
# Group benchmark by operation, density and size. | ||
group = request.node.callspec.id # noqa:F821 | ||
group = group.split("-") | ||
benchmark.group = "-".join(group[1:]) | ||
benchmark.extra_info["dtype"] = group[0] | ||
|
||
array = np.random.uniform(size=(size, size)) + 1.0j * np.random.uniform( | ||
size=(size, size) | ||
) | ||
|
||
if dtype == "CuPyDense": | ||
arr = CuPyDense(array) | ||
elif dtype == "CuPyDense_half_precision": | ||
arr = CuPyDense(array, dtype=cp.complex64) | ||
|
||
elif dtype == "Dense": | ||
arr = Dense(array) | ||
|
||
def matmul_(arr): | ||
return arr @ arr | ||
|
||
benchmark2 = GpuWrapper(benchmark) | ||
cp_mult = benchmark2.pedanticupy(matmul_, (arr,)) | ||
|
||
np_mult = matmul_(array) | ||
|
||
np.testing.assert_array_almost_equal(cp_mult.to_array(), np_mult) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
_DEVICE = 0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
# this has benen mostly borrowed from the qutip-tensorflow implementation | ||
import cupy as cp | ||
import json | ||
import pandas as pd | ||
import matplotlib.pyplot as plt | ||
import pytest | ||
import argparse | ||
import glob | ||
from pathlib import Path | ||
|
||
import benchmark_tools | ||
|
||
|
||
def unravel(data, key): | ||
"""Transforms {key:{another_key: values, another_key2: value2}} into | ||
{key_another_key:value, key_another_key2:value}""" | ||
for d in data: | ||
values = d.pop(key) | ||
for k, v in values.items(): | ||
d[key + "_" + k] = v | ||
return data | ||
|
||
|
||
def benchmark_to_dataframe(filepath): | ||
"""Loads a JSON file where the benchmark is stored and returns a dataframe | ||
with the benchmar information.""" | ||
with open(filepath) as f: | ||
data = json.load(f) | ||
data = data["benchmarks"] | ||
data = unravel(data, "options") | ||
data = unravel(data, "stats") | ||
data = unravel(data, "params") | ||
data = unravel(data, "extra_info") | ||
data = pd.DataFrame(data) | ||
|
||
# Set operation properly (for example: matmul instead of: | ||
# UNSERIALIZABLE[<function Qobj.__matmul__ at 0x...) | ||
# The name of the operation is obtained from the group name | ||
data["params_get_operation"] = data.group.str.split("-") | ||
data["params_get_operation"] = [d[-1] for d in data.params_get_operation] | ||
print(data.params_get_operation) | ||
return data | ||
|
||
|
||
def plot_benchmark(df, destination_folder): | ||
"""Plots results using matplotlib. It iterates params_get_operation and | ||
params_density and plots time vs N (for NxN matrices)""" | ||
grouped = df.groupby(["params_get_operation"]) | ||
for operation, group in grouped: | ||
for dtype, g in group.groupby("extra_info_dtype"): | ||
plt.errorbar( | ||
g.params_size, g.stats_mean, g.stats_stddev, fmt=".-", label=dtype | ||
) | ||
|
||
plt.title(f"{operation}") | ||
plt.legend() | ||
plt.yscale("log") | ||
plt.xscale("log") | ||
plt.savefig(f".benchmarks/figures/{operation}.png") | ||
plt.xlabel("Size") | ||
plt.ylabel("Time (s)") | ||
plt.close() | ||
|
||
|
||
def run_benchmarks(args): | ||
"Run pytest benchmark with sensible defaults." | ||
pytest.main( | ||
[ | ||
"benchmarks", | ||
"--benchmark-only", | ||
"--benchmark-columns=Mean,StdDev,rounds,Iterations", | ||
"--benchmark-sort=name", | ||
"--benchmark-autosave", | ||
"-Wdefault", | ||
] | ||
+ args | ||
) | ||
|
||
|
||
def get_latest_benchmark_path(): | ||
"""Returns the path to the latest benchmark run from `./.benchmarks/`""" | ||
|
||
benchmark_paths = glob.glob("./.benchmarks/*/*.json") | ||
dates = ["".join(_b.split("/")[-1].split("_")[2:4]) for _b in benchmark_paths] | ||
benchmarks = {date: value for date, value in zip(dates, benchmark_paths)} | ||
|
||
dates.sort() | ||
latest = dates[-1] | ||
benchmark_latest = benchmarks[latest] | ||
|
||
return benchmark_latest | ||
|
||
|
||
def main(args=[]): | ||
parser = argparse.ArgumentParser( | ||
description="""Run and plot the benchmarks. | ||
The script also accepts the same arguments | ||
as pytest/pytest-benchmark. The script must be run | ||
from the root of the repository.""" | ||
) | ||
parser.add_argument( | ||
"--save_csv", | ||
default=".benchmarks/latest.csv", | ||
help="""Path where the latest benchmark resulst will be | ||
stored as csv. If empty it will not store results as | ||
csv. Default: .benchmarks/latest.csv""", | ||
) | ||
parser.add_argument( | ||
"--save_plots", | ||
default=".benchmarks/figures", | ||
help="""Path where the plots will be saved. If empty, | ||
it will not save the plots. Default: | ||
.benchmarks/figures""", | ||
) | ||
parser.add_argument( | ||
"--plot_only", | ||
action="store_true", | ||
help="""If included, it will not run the benchmarks but | ||
just plot the latest results from .benchmaks/ folder. | ||
""", | ||
) | ||
|
||
parser.add_argument( | ||
"--device_id", | ||
default=0, | ||
help="""Device id for benchmarking. | ||
""", | ||
) | ||
|
||
if args: | ||
args, other_args = parser.parse_known_args([]) | ||
else: | ||
args, other_args = parser.parse_known_args() | ||
|
||
benchmark_tools._DEVICE = args.device_id | ||
|
||
if not args.plot_only: | ||
run_benchmarks(other_args) | ||
|
||
with cp.cuda.device.Device(benchmark_tools._DEVICE) as device: | ||
|
||
print("The sepcifications for your current device are:") | ||
print(device.attributes) | ||
|
||
benchmark_latest = get_latest_benchmark_path() | ||
benchmark_latest = benchmark_to_dataframe(benchmark_latest) | ||
|
||
# Save results as csv | ||
if args.save_csv: | ||
benchmark_latest.to_csv(args.save_csv) | ||
|
||
if args.save_plots: | ||
Path(args.save_plots).mkdir(parents=True, exist_ok=True) | ||
plot_benchmark(benchmark_latest, args.save_plots) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
import warnings | ||
|
||
# from pytest_benchmark.fixture import BenchmarkFixture | ||
from pytest_benchmark.fixture import FixtureAlreadyUsed | ||
from pytest_benchmark.stats import Metadata | ||
from cupyx.time import repeat as cp_repeat | ||
|
||
|
||
class GpuWrapper(object): | ||
""" | ||
This class will wrap a pytest-benchmark type Fixture | ||
in order to provide separate CPU and GPU timings. | ||
The GpuWrapper instance will share all its attributes | ||
transparently with the pytest-benchmark fixture instance. | ||
Ths class relies on CuPy's experimental timing | ||
utility ``cupyx.time.repeat`` | ||
""" | ||
|
||
def __init__(self, wrapped_class, iterations=30, rounds=5, warmup_rounds=10): | ||
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. I think "wrapped_class" is really just a "wrapped_obj" here, so we should name it appropriately. |
||
# we need to initialize wrapped_class this way | ||
# since we are overriding set_attr | ||
self.__dict__["wrapped_class"] = wrapped_class | ||
self.iterations = iterations | ||
self.rounds = rounds | ||
self.warmup_rounds = warmup_rounds | ||
|
||
def __getattr__(self, attr): | ||
# orig_attr = self.wrapped_class.__getattribute__(attr) | ||
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. Also delete this commented out code. |
||
orig_attr = getattr(self.wrapped_class, attr) | ||
if callable(orig_attr): | ||
|
||
def hooked(*args, **kwargs): | ||
result = orig_attr(*args, **kwargs) | ||
# prevent wrapped_class from becoming unwrapped | ||
if result == self.wrapped_class: | ||
return self | ||
return result | ||
|
||
return hooked | ||
else: | ||
return orig_attr | ||
|
||
def __setattr__(self, attr, value): | ||
setattr(self.wrapped_class, attr, value) | ||
|
||
def pedanticupy( | ||
self, | ||
function_to_benchmark, | ||
args=(), | ||
kwargs={}, | ||
iterations=30, | ||
rounds=5, | ||
warmup_rounds=10, | ||
): | ||
""" | ||
By using this method you have the same control on the benchmark as when using | ||
pytest-benchamrk's own ``pedantic`` | ||
with the exception of providing an special setup function. | ||
""" | ||
if self._mode: | ||
self.has_error = True | ||
raise FixtureAlreadyUsed( | ||
"Fixture can only be used once. Previously it was used in %s mode." | ||
% self._mode | ||
) | ||
try: | ||
self._mode = "benchmark.pedantic(...)" | ||
return self._raw_pedantic( | ||
function_to_benchmark, | ||
args, | ||
kwargs, | ||
iterations=iterations, | ||
rounds=rounds, | ||
warmup_rounds=warmup_rounds, | ||
) | ||
except Exception: | ||
self.has_error = True | ||
raise | ||
|
||
def _raw_pedantic( | ||
self, | ||
function_to_benchmark, | ||
args=(), | ||
kwargs={}, | ||
iterations=30, | ||
rounds=5, | ||
warmup_rounds=10, | ||
): | ||
|
||
if not isinstance(iterations, int) or iterations < 1: | ||
raise ValueError("Must have positive int for `iterations`.") | ||
|
||
if not isinstance(rounds, int) or rounds < 1: | ||
raise ValueError("Must have positive int for `rounds`.") | ||
|
||
if not isinstance(warmup_rounds, int) or warmup_rounds < 0: | ||
raise ValueError("Must have positive int for `warmup_rounds`.") | ||
|
||
iterations = self.iterations | ||
rounds = self.rounds | ||
warmup_rounds = self.warmup_rounds | ||
|
||
if self.enabled: | ||
self.stats = self._make_stats(iterations) | ||
self.stats.group = "device_all" | ||
self.statscpu = self._make_stats(iterations) | ||
self.statscpu.group = "device_cpu" | ||
self.statsgpu = self._make_stats(iterations) | ||
self.statsgpu.group = "device_gpu" | ||
|
||
self._logger.debug( | ||
" Running %s rounds x %s iterations ..." % (rounds, iterations), | ||
yellow=True, | ||
bold=True, | ||
) | ||
|
||
for _ in range(rounds): | ||
with warnings.catch_warnings(): | ||
warnings.filterwarnings( | ||
action="ignore", | ||
category=FutureWarning, | ||
message=r"cupyx.time.repeat is experimental.", | ||
) | ||
|
||
results = cp_repeat( | ||
function_to_benchmark, | ||
args, | ||
kwargs, | ||
n_warmup=warmup_rounds, | ||
max_duration=self._max_time, | ||
n_repeat=iterations, | ||
) | ||
|
||
for tim_cpu, tim_gpu in zip(results.cpu_times, results.gpu_times[0]): | ||
|
||
self.stats.update(tim_cpu + tim_gpu) | ||
self.statscpu.update(tim_cpu) | ||
self.statsgpu.update(tim_gpu) | ||
|
||
function_result = function_to_benchmark(*args, **kwargs) | ||
return function_result | ||
|
||
def _make_stats(self, iterations): | ||
bench_stats = Metadata( | ||
self, | ||
iterations=iterations, | ||
options={ | ||
"disable_gc": self._disable_gc, | ||
"timer": self._timer, | ||
"min_rounds": self._min_rounds, | ||
"max_time": self._max_time, | ||
"min_time": self._min_time, | ||
"warmup": self._warmup, | ||
}, | ||
) | ||
self._add_stats(bench_stats) | ||
|
||
return bench_stats |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Delete this commented out line.