Skip to content

Commit b73c2bb

Browse files
authored
Benchmark refactoring (#101)
* cleanup: remove duplication of absolute path check * convenience functionality to validate results * set upper limit on pandas version since pandas 3.0.0 causes some DrHook tests to fail. * addressing some PR comments. * make reference_path Path not str * fix doc string * fix tests that were failing with pandas 3.0.0 * Fix frame close validation checks and add more log output. Update tests to match. * remove some excessively verbose output. * refactoring of benchmark class: setup_rundir is called as part of run to make it less errorprone if forgotten. Add job as field directly to benchmark, can still be overridden if using same benchmark with different job. This will make it easier to run ensembles of benchmarks with different combinations of setups and jobs * benchmark.run tests to also test that the TechSetup application is used if it exists
1 parent 8ba5e2e commit b73c2bb

2 files changed

Lines changed: 110 additions & 49 deletions

File tree

ifsbench/benchmark.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from ifsbench.launch import Launcher
2626

2727

28-
__all__ = ['ScienceSetup', 'TechSetup', 'Benchmark']
28+
__all__ = ['ScienceSetup', 'TechSetup', 'Benchmark', 'BenchmarkSetup']
2929

3030

3131
class ScienceSetup(SerialisationMixin):
@@ -82,6 +82,18 @@ class TechSetup(SerialisationMixin):
8282
env_handlers: List[EnvHandler] = Field(default_factory=list)
8383

8484

85+
class BenchmarkSetup(SerialisationMixin):
86+
#: The main (scientific) benchmark setup that describes _what_ gets benchmarked
87+
# and _which data_ is used.
88+
science: ScienceSetup
89+
90+
#: The parallel setup for the benchmark.
91+
job: Job
92+
93+
#: Additional technical details that don't alter the results.
94+
tech: Optional[TechSetup] = None
95+
96+
8597
class BenchmarkSummary(SerialisationMixin):
8698
"""
8799
Summary of a benchmark run.
@@ -107,12 +119,8 @@ class Benchmark(SerialisationMixin):
107119
3. Create a result object, using the data in the run directory.
108120
"""
109121

110-
#: The main (scientific) benchmark setup that describes _what_ gets benchmarked
111-
# and _which data_ is used.
112-
science: ScienceSetup
113-
114-
#: Additional technical details that don't alter the results.
115-
tech: Optional[TechSetup] = None
122+
#: The science and tech setup to run with the given job. The job can be overridden when running.
123+
setup: BenchmarkSetup
116124

117125
def setup_rundir(self, run_dir: Path, force: bool = False):
118126
"""
@@ -137,17 +145,17 @@ def setup_rundir(self, run_dir: Path, force: bool = False):
137145
if exists and not force:
138146
return
139147

140-
handlers = self.science.data_handlers_init
141-
if self.tech:
142-
handlers += self.tech.data_handlers_init
148+
handlers = self.setup.science.data_handlers_init
149+
if self.setup.tech:
150+
handlers += self.setup.tech.data_handlers_init
143151

144152
for handler in handlers:
145153
handler.execute(run_dir)
146154

147155
def run(
148156
self,
149157
run_dir: Path,
150-
job: Job,
158+
job: Optional[Job] = None,
151159
arch: Optional[Arch] = None,
152160
launcher: Optional[Launcher] = None,
153161
launcher_flags: Optional[List[str]] = None,
@@ -160,7 +168,7 @@ def run(
160168
run_dir: pathlib.Path
161169
The path to the run directory.
162170
job: Job
163-
The parallel setup for the benchmark.
171+
The parallel setup for the benchmark. If None, the Job from the BenchmarkSetup is used.
164172
arch: Arch
165173
A specific architecture that is used.
166174
launcher: Launcher
@@ -175,11 +183,17 @@ def run(
175183
of the benchmark.
176184
"""
177185

186+
# Setup run directory without replacing the current contents if it already exists.
187+
self.setup_rundir(run_dir, force=False)
188+
178189
env_pipeline = DefaultEnvPipeline(
179-
handlers=self.science.env_handlers, env_initial=os.environ
190+
handlers=self.setup.science.env_handlers, env_initial=os.environ
180191
)
181-
if self.tech:
182-
env_pipeline.add(self.tech.env_handlers)
192+
if self.setup.tech:
193+
env_pipeline.add(self.setup.tech.env_handlers)
194+
195+
if not job:
196+
job = self.setup.job
183197

184198
if arch:
185199
arch_result = arch.process_job(job)
@@ -196,17 +210,17 @@ def run(
196210
if launcher is None:
197211
raise ValueError('No launcher was specified!')
198212

199-
application = self.science.application
200-
if self.tech is not None and self.tech.application is not None:
201-
application = self.tech.application
213+
application = self.setup.science.application
214+
if self.setup.tech is not None and self.setup.tech.application is not None:
215+
application = self.setup.tech.application
202216

203217
cmd = application.get_command(run_dir, job)
204218

205219
library_paths = application.get_library_paths(run_dir, job)
206220

207-
data_handlers = list(self.science.data_handlers_runtime)
208-
if self.tech:
209-
data_handlers += self.tech.data_handlers_runtime
221+
data_handlers = list(self.setup.science.data_handlers_runtime)
222+
if self.setup.tech:
223+
data_handlers += self.setup.tech.data_handlers_runtime
210224
data_handlers += application.get_data_handlers(run_dir, job)
211225

212226
for handler in data_handlers:

ifsbench/tests/test_benchmark.py

Lines changed: 75 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@
1010
"""
1111

1212
from pathlib import Path
13+
from typing import List
1314
from time import sleep
1415
import sys
16+
import yaml
1517

1618
import pytest
1719
from typing_extensions import Literal
1820

1921
from ifsbench import (
2022
Benchmark,
23+
BenchmarkSetup,
2124
ScienceSetup,
2225
TechSetup,
2326
DefaultApplication,
@@ -74,11 +77,13 @@ def test_defaultbenchmark_setup_rundir(tmp_path, test_setup_files, force, use_te
7477
"""
7578

7679
science, tech, science_list, tech_list = test_setup_files
80+
job = Job(tasks=2)
7781

7882
if use_tech:
79-
benchmark = Benchmark(science=science, tech=tech)
83+
setup = BenchmarkSetup(science=science, job=job, tech=tech)
8084
else:
81-
benchmark = Benchmark(science=science)
85+
setup = BenchmarkSetup(science=science, job=job)
86+
benchmark = Benchmark(setup=setup)
8287

8388
# Create the run directory and check that all files in file list
8489
# actually exist.
@@ -111,28 +116,49 @@ def test_defaultbenchmark_setup_rundir(tmp_path, test_setup_files, force, use_te
111116
assert stat.st_atime == stats[file].st_atime
112117

113118

114-
@pytest.fixture(name='test_run_setup')
115-
def fixture_test_run_setup():
116-
env_handlers = [
117-
EnvHandler(mode=EnvOperation.CLEAR),
118-
]
119+
class DummyApplication(DefaultApplication):
120+
def get_command(self, run_dir: Path, job: Job) -> List[str]:
121+
job_config = job.dump_config()
119122

120-
application = DefaultApplication(
121-
command=[
123+
command = [
122124
sys.executable,
123125
'-c',
124-
"from pathlib import Path; Path('test.txt').touch()",
126+
'from pathlib import Path; import yaml; '
127+
f'f = open("{self.command[0]}", "w"); '
128+
f'yaml.dump({job_config}, f); '
129+
'f.close()',
125130
]
131+
return command
132+
133+
134+
@pytest.fixture(name='tech_setup_application')
135+
def fixture_tech_setup_application(request):
136+
tech_application = None
137+
if request.param:
138+
tech_application = DummyApplication(command=['tech.txt'])
139+
140+
tech = TechSetup(
141+
application=tech_application,
142+
env_handlers=[EnvHandler(mode=EnvOperation.SET, key='KEY', value='VALUE')],
126143
)
127144

145+
return tech
146+
147+
148+
@pytest.fixture(name='test_run_science_setup')
149+
def fixture_test_run_science_setup():
150+
env_handlers = [
151+
EnvHandler(mode=EnvOperation.CLEAR),
152+
]
153+
154+
science_application = DummyApplication(command=['science.txt'])
155+
128156
science = ScienceSetup(
129-
application=application,
157+
application=science_application,
130158
env_handlers=env_handlers,
131159
)
132160

133-
tech = TechSetup(env_handlers=[EnvHandler(mode=EnvOperation.SET, key='KEY', value='VALUE')])
134-
135-
return science, tech
161+
return science
136162

137163

138164
class _DummyLauncher(Launcher):
@@ -155,15 +181,26 @@ def prepare(
155181
return LaunchData(run_dir=run_dir, cmd=cmd)
156182

157183

158-
@pytest.mark.parametrize('job', [Job(tasks=2)])
184+
@pytest.mark.parametrize('job_override', [None, Job(tasks=2, account='override')])
159185
@pytest.mark.parametrize('use_arch', [False, True])
160186
@pytest.mark.parametrize(
161187
'use_launcher, launcher_flags',
162188
[(False, None), (True, None), (True, ['something'])],
163189
)
164-
@pytest.mark.parametrize('use_tech', [True, False])
190+
@pytest.mark.parametrize(
191+
'use_tech, tech_setup_application',
192+
[(True, True), (True, False), (False, False)],
193+
indirect=['tech_setup_application'],
194+
)
165195
def test_defaultbenchmark_run(
166-
tmp_path, test_run_setup, job, use_arch, use_launcher, launcher_flags, use_tech
196+
tmp_path,
197+
test_run_science_setup,
198+
job_override,
199+
use_arch,
200+
use_launcher,
201+
launcher_flags,
202+
use_tech,
203+
tech_setup_application,
167204
):
168205
"""
169206
Test the Benchmark.run function.
@@ -181,24 +218,22 @@ def test_defaultbenchmark_run(
181218
else None
182219
)
183220

184-
science, tech = test_run_setup
221+
science = test_run_science_setup
222+
tech = tech_setup_application
223+
job = Job(tasks=5, account='default')
185224

186225
if use_tech:
187-
benchmark = Benchmark(science=science, tech=tech)
188-
226+
setup = BenchmarkSetup(science=science, job=job, tech=tech)
189227
else:
190-
benchmark = Benchmark(science=science)
191-
192-
# Create the run directory and check that all files in file list
193-
# actually exist.
194-
benchmark.setup_rundir(tmp_path)
228+
setup = BenchmarkSetup(science=science, job=job)
229+
benchmark = Benchmark(setup=setup)
195230

196231
if arch is None and launcher is None:
197232
with pytest.raises(ValueError):
198-
benchmark.run(tmp_path, job, arch, launcher, launcher_flags)
233+
benchmark.run(tmp_path, job_override, arch, launcher, launcher_flags)
199234
return
200235

201-
benchmark.run(tmp_path, job, arch, launcher, launcher_flags)
236+
benchmark.run(tmp_path, job_override, arch, launcher, launcher_flags)
202237

203238
if launcher is not None:
204239
assert launcher._prepare_called is True
@@ -207,4 +242,16 @@ def test_defaultbenchmark_run(
207242
elif arch is not None:
208243
assert arch.get_default_launcher()._prepare_called is True
209244

210-
assert (tmp_path / 'test.txt').exists()
245+
# Confirm the correct application was run, ScienceSetup vs TechSetup
246+
out_file = 'tech.txt' if tech.application else 'science.txt'
247+
output_path = tmp_path / out_file
248+
assert (output_path).exists()
249+
250+
# Confirm the correct job was run, the benchmark member or the override
251+
with output_path.open('r') as f:
252+
config = yaml.safe_load(f)
253+
executed_job = Job.from_config(config)
254+
if job_override:
255+
assert executed_job == job_override
256+
else:
257+
assert executed_job == job

0 commit comments

Comments
 (0)