Skip to content

Commit f2113a2

Browse files
authored
CPerf and SPerf Benchmark Fixes (#4758)
* CSPerf fixes. * Correct argument hashing in file names. * Warn if BENCHMARK_DATA is not set. * SPerf combine_regions better file handling. * SPerf don't store FileMixin files long term - too much space. * More realistic SPerf file sizes, allow larger-than-memory files. * Add SPerf comment about total disk space. * Corrected CSPerf directories printout. * Remove namespace conflict in SPerf combine_regions.
1 parent 76a16f1 commit f2113a2

File tree

8 files changed

+56
-21
lines changed

8 files changed

+56
-21
lines changed

benchmarks/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ used to generate benchmark test objects/files; see
3030
but will defer to any value already set in the shell.
3131
* `BENCHMARK_DATA` - optional - path to a directory for benchmark synthetic
3232
test data, which the benchmark scripts will create if it doesn't already
33-
exist. Defaults to `<root>/benchmarks/.data/` if not set.
33+
exist. Defaults to `<root>/benchmarks/.data/` if not set. Note that some of
34+
the generated files, especially in the 'SPerf' suite, are many GB in size so
35+
plan accordingly.
3436
* `ON_DEMAND_BENCHMARKS` - optional - when set (to any value): benchmarks
3537
decorated with `@on_demand_benchmark` are included in the ASV run. Usually
3638
coupled with the ASV `--bench` argument to only run the benchmark(s) of

benchmarks/benchmarks/generate_data/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from pathlib import Path
2323
from subprocess import CalledProcessError, check_output, run
2424
from textwrap import dedent
25+
from warnings import warn
2526

2627
from iris._lazy_data import as_concrete_data
2728
from iris.fileformats import netcdf
@@ -47,6 +48,11 @@
4748
BENCHMARK_DATA = Path(environ.get("BENCHMARK_DATA", default_data_dir))
4849
if BENCHMARK_DATA == default_data_dir:
4950
BENCHMARK_DATA.mkdir(exist_ok=True)
51+
message = (
52+
f"No BENCHMARK_DATA env var, defaulting to {BENCHMARK_DATA}. "
53+
"Note that some benchmark files are GB in size."
54+
)
55+
warn(message)
5056
elif not BENCHMARK_DATA.is_dir():
5157
message = f"Not a directory: {BENCHMARK_DATA} ."
5258
raise ValueError(message)

benchmarks/benchmarks/generate_data/stock.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,31 @@
99
See :mod:`benchmarks.generate_data` for an explanation of this structure.
1010
"""
1111

12+
from hashlib import sha256
13+
import json
1214
from pathlib import Path
1315

1416
from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD, load_mesh
1517

1618
from . import BENCHMARK_DATA, REUSE_DATA, load_realised, run_function_elsewhere
1719

1820

21+
def hash_args(*args, **kwargs):
22+
"""Convert arguments into a short hash - for preserving args in filenames."""
23+
arg_string = str(args)
24+
kwarg_string = json.dumps(kwargs)
25+
full_string = arg_string + kwarg_string
26+
return sha256(full_string.encode()).hexdigest()[:10]
27+
28+
1929
def _create_file__xios_common(func_name, **kwargs):
2030
def _external(func_name_, temp_file_dir, **kwargs_):
2131
from iris.tests.stock import netcdf
2232

2333
func = getattr(netcdf, func_name_)
2434
print(func(temp_file_dir, **kwargs_), end="")
2535

26-
args_hash = hash(str(kwargs))
36+
args_hash = hash_args(**kwargs)
2737
save_path = (BENCHMARK_DATA / f"{func_name}_{args_hash}").with_suffix(
2838
".nc"
2939
)
@@ -95,7 +105,7 @@ def _external(*args, **kwargs):
95105
save_mesh(new_mesh, save_path_)
96106

97107
arg_list = [n_nodes, n_faces, n_edges]
98-
args_hash = hash(str(arg_list))
108+
args_hash = hash_args(*arg_list)
99109
save_path = (BENCHMARK_DATA / f"sample_mesh_{args_hash}").with_suffix(
100110
".nc"
101111
)
@@ -139,7 +149,7 @@ def _external(sample_mesh_kwargs_, save_path_):
139149
new_meshcoord = sample_meshcoord(mesh=input_mesh)
140150
save_mesh(new_meshcoord.mesh, save_path_)
141151

142-
args_hash = hash(str(sample_mesh_kwargs))
152+
args_hash = hash_args(**sample_mesh_kwargs)
143153
save_path = (
144154
BENCHMARK_DATA / f"sample_mesh_coord_{args_hash}"
145155
).with_suffix(".nc")

benchmarks/benchmarks/sperf/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@
2020
class FileMixin:
2121
"""For use in any benchmark classes that work on a file."""
2222

23+
# Allows time for large file generation.
24+
timeout = 3600.0
25+
# Largest file with these params: ~90GB.
26+
# Total disk space: ~410GB.
2327
params = [
2428
[12, 384, 640, 960, 1280, 1668],
2529
[1, 36, 72],
26-
[1, 3, 36, 72],
30+
[1, 3, 10],
2731
]
2832
param_names = ["cubesphere_C<N>", "N levels", "N time steps"]
2933
# cubesphere_C<N>: notation refers to faces per panel.

benchmarks/benchmarks/sperf/combine_regions.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,21 @@
1616
from iris.experimental.ugrid.utils import recombine_submeshes
1717

1818
from .. import TrackAddedMemoryAllocation, on_demand_benchmark
19-
from ..generate_data.ugrid import make_cube_like_2d_cubesphere
19+
from ..generate_data.ugrid import BENCHMARK_DATA, make_cube_like_2d_cubesphere
2020

2121

2222
class Mixin:
2323
# Characterise time taken + memory-allocated, for various stages of combine
2424
# operations on cubesphere-like test data.
25-
timeout = 180.0
25+
timeout = 300.0
2626
params = [100, 200, 300, 500, 1000, 1668]
2727
param_names = ["cubesphere_C<N>"]
2828
# Fix result units for the tracking benchmarks.
2929
unit = "Mb"
30+
temp_save_path = BENCHMARK_DATA / "tmp.nc"
3031

3132
def _parametrised_cache_filename(self, n_cubesphere, content_name):
32-
return f"cube_C{n_cubesphere}_{content_name}.nc"
33+
return BENCHMARK_DATA / f"cube_C{n_cubesphere}_{content_name}.nc"
3334

3435
def _make_region_cubes(self, full_mesh_cube):
3536
"""Make a fixed number of region cubes from a full meshcube."""
@@ -139,6 +140,9 @@ def setup(
139140
# Fix dask usage mode for all the subsequent performance tests.
140141
self.fix_dask_settings()
141142

143+
def teardown(self, _):
144+
self.temp_save_path.unlink(missing_ok=True)
145+
142146
def fix_dask_settings(self):
143147
"""
144148
Fix "standard" dask behaviour for time+space testing.
@@ -165,6 +169,9 @@ def recombine(self):
165169
)
166170
return result
167171

172+
def save_recombined_cube(self):
173+
save(self.recombined_cube, self.temp_save_path)
174+
168175

169176
@on_demand_benchmark
170177
class CreateCube(Mixin):
@@ -215,15 +222,15 @@ class SaveData(Mixin):
215222

216223
def time_save(self, n_cubesphere):
217224
# Save to disk, which must compute data + stream it to file.
218-
save(self.recombined_cube, "tmp.nc")
225+
self.save_recombined_cube()
219226

220227
@TrackAddedMemoryAllocation.decorator()
221228
def track_addedmem_save(self, n_cubesphere):
222-
save(self.recombined_cube, "tmp.nc")
229+
self.save_recombined_cube()
223230

224231
def track_filesize_saved(self, n_cubesphere):
225-
save(self.recombined_cube, "tmp.nc")
226-
return os.path.getsize("tmp.nc") * 1.0e-6
232+
self.save_recombined_cube()
233+
return self.temp_save_path.stat().st_size * 1.0e-6
227234

228235

229236
@on_demand_benchmark
@@ -243,8 +250,8 @@ def setup(
243250

244251
def time_stream_file2file(self, n_cubesphere):
245252
# Save to disk, which must compute data + stream it to file.
246-
save(self.recombined_cube, "tmp.nc")
253+
self.save_recombined_cube()
247254

248255
@TrackAddedMemoryAllocation.decorator()
249256
def track_addedmem_stream_file2file(self, n_cubesphere):
250-
save(self.recombined_cube, "tmp.nc")
257+
self.save_recombined_cube()

benchmarks/benchmarks/sperf/load.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ def time_load_cube(self, _, __, ___):
1818

1919
@on_demand_benchmark
2020
class Realise(FileMixin):
21-
# The larger files take a long time to realise.
22-
timeout = 600.0
23-
2421
def setup(self, c_size, n_levels, n_times):
2522
super().setup(c_size, n_levels, n_times)
2623
self.loaded_cube = self.load_cube()

lib/iris/tests/stock/netcdf.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from string import Template
1010
import subprocess
1111

12+
import dask
13+
from dask import array as da
1214
import netCDF4
1315
import numpy as np
1416

@@ -79,11 +81,13 @@ def _add_standard_data(nc_path, unlimited_dim_size=0):
7981
# so it can be a dim-coord.
8082
data_size = np.prod(shape)
8183
data = np.arange(1, data_size + 1, dtype=var.dtype).reshape(shape)
84+
var[:] = data
8285
else:
8386
# Fill with a plain value. But avoid zeros, so we can simulate
8487
# valid ugrid connectivities even when start_index=1.
85-
data = np.ones(shape, dtype=var.dtype) # Do not use zero
86-
var[:] = data
88+
with dask.config.set({"array.chunk-size": "2048MiB"}):
89+
data = da.ones(shape, dtype=var.dtype) # Do not use zero
90+
da.store(data, var)
8791

8892
ds.close()
8993

noxfile.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import hashlib
1010
import os
1111
from pathlib import Path
12+
import re
1213
from tempfile import NamedTemporaryFile
1314
from typing import Literal
1415

@@ -314,7 +315,7 @@ def benchmarks(
314315
----------
315316
session: object
316317
A `nox.sessions.Session` object.
317-
run_type: {"overnight", "branch", "custom"}
318+
run_type: {"overnight", "branch", "cperf", "sperf", "custom"}
318319
* ``overnight``: benchmarks all commits between the input **first
319320
commit** to ``HEAD``, comparing each to its parent for performance
320321
shifts. If a commit causes shifts, the output is saved to a file:
@@ -501,6 +502,11 @@ def asv_compare(*commits):
501502
asv_command = (
502503
asv_harness.format(posargs=commit_range) + f" --bench={run_type}"
503504
)
505+
# C/SPerf benchmarks are much bigger than the CI ones:
506+
# Don't fail the whole run if memory blows on 1 benchmark.
507+
asv_command = asv_command.replace(" --strict", "")
508+
# Only do a single round.
509+
asv_command = re.sub(r"rounds=\d", "rounds=1", asv_command)
504510
session.run(*asv_command.split(" "), *asv_args)
505511

506512
asv_command = f"asv publish {commit_range} --html-dir={publish_subdir}"
@@ -511,7 +517,6 @@ def asv_compare(*commits):
511517
print(
512518
f'New ASV results for "{run_type}".\n'
513519
f'See "{publish_subdir}",'
514-
f'\n html in "{location / "html"}".'
515520
f'\n or JSON files under "{location / "results"}".'
516521
)
517522

0 commit comments

Comments
 (0)