Skip to content

Commit a3efec6

Browse files
authored
Merge pull request #750 from DiamondLightSource/data_reducer_order
corrections around data checker GPU and transfer optimisation for save_intermediate
2 parents 121ba53 + c632119 commit a3efec6

10 files changed

Lines changed: 156 additions & 79 deletions

File tree

‎httomo/method_wrappers/generic.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ def execute(self, block: T) -> T:
334334
block = self._run_method(block, args)
335335
block = self._postprocess_data(block)
336336

337-
if gpu_enabled:
337+
if self.is_gpu:
338338
self._gpu_time_info.kernel = t.elapsed
339339
else:
340340
self._gpu_time_info.kernel = 0

‎httomo/method_wrappers/save_intermediate.py‎

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import os
2-
import pathlib
3-
from typing import Any, Dict, Optional, Union
2+
from typing import Dict, Optional
43
import weakref
54
from mpi4py.MPI import Comm, MIN
65
import httomo
@@ -9,10 +8,9 @@
98
from httomo.runner.loader import LoaderInterface
109
from httomo.runner.method_wrapper import GpuTimeInfo, MethodWrapper
1110
from httomo.runner.methods_repository_interface import MethodRepository
12-
from httomo.utils import catchtime, xp
11+
from httomo.utils import catchtime, xp, gpu_enabled
1312

1413
import h5py
15-
import numpy as np
1614

1715

1816
class SaveIntermediateFilesWrapper(GenericMethodWrapper):
@@ -30,6 +28,7 @@ def __init__(
3028
output_mapping: Dict[str, str] = {},
3129
out_dir: Optional[os.PathLike] = None,
3230
prev_method: Optional[MethodWrapper] = None,
31+
next_method_is_cpu: bool = False,
3332
loader: Optional[LoaderInterface] = None,
3433
**kwargs,
3534
):
@@ -45,6 +44,7 @@ def __init__(
4544
assert loader is not None
4645
self._loader = loader
4746
assert prev_method is not None
47+
self._next_method_is_cpu = next_method_is_cpu
4848

4949
filename = f"{prev_method.task_id}-{prev_method.package_name}-{prev_method.method_name}"
5050
is_saving_recon = prev_method.module_path.endswith(".algorithm")
@@ -62,21 +62,34 @@ def __init__(
6262
# make sure file gets closed properly
6363
weakref.finalize(self, self._file.close)
6464

65+
def _transfer_data(self, block: T) -> T:
66+
if block.is_cpu:
67+
return block
68+
if self._next_method_is_cpu:
69+
# convert the whole (GPU) block to CPU if the next method is CPU
70+
self._gpu_time_info = GpuTimeInfo()
71+
with catchtime() as t:
72+
block.to_cpu()
73+
self._gpu_time_info.device2host = t.elapsed
74+
return block
75+
return block
76+
6577
def execute(self, block: T) -> T:
66-
# we overwrite the whole execute method here, as we do not need any of the helper
67-
# methods from the Generic Wrapper
68-
# What we know:
69-
# - we do not transfer the dataset as a whole to CPU - only the data and angles locally
70-
# (and never back)
71-
# - the user does not insert this method - it's automatic - so no config params are
72-
# relevant
73-
# - we return just the input as it is
74-
self._gpu_time_info = GpuTimeInfo()
75-
with catchtime() as t:
76-
data = (
77-
block.data_unpadded if block.is_cpu else xp.asnumpy(block.data_unpadded)
78-
)
79-
self._gpu_time_info.device2host += t.elapsed
78+
# we overwrite the most of the execute method here
79+
# we transfer the data to CPU only if the next method is CPU, otherwise we keep it on GPU
80+
# in case if save_intermediate is the last method we also keep the data on GPU
81+
block = self._transfer_data(block)
82+
83+
if self._next_method_is_cpu or not gpu_enabled:
84+
data = block.data_unpadded
85+
else:
86+
# Transfer data to CPU while the main block stays on GPU
87+
self._gpu_time_info = GpuTimeInfo()
88+
89+
with catchtime() as t:
90+
data = xp.asnumpy(block.data_unpadded)
91+
92+
self._gpu_time_info.device2host += t.elapsed
8093

8194
MIN_BLOCK_LEN_PARAM = "minimum_block_length"
8295
if block.chunk_index_unpadded[block.slicing_dim] == 0 and self.comm.size > 1:

‎httomo/method_wrappers/stats_calc.py‎

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from httomo.block_interfaces import T
22
from httomo.method_wrappers.generic import GenericMethodWrapper
33
from httomo.runner.methods_repository_interface import MethodRepository
4-
from httomo.utils import catchtime, log_rank, xp, gpu_enabled
4+
from httomo.utils import log_rank
55

66

77
from mpi4py.MPI import Comm
@@ -51,13 +51,6 @@ def _transfer_data(self, dataset: T) -> T:
5151
return dataset
5252

5353
def _run_method(self, dataset: T, args: Dict[str, Any]) -> T:
54-
# transfer data to GPU if we can / have it available (always faster),
55-
# but don't want to fail if we don't have a GPU (underlying method works for both)
56-
# and don't touch original dataset
57-
if gpu_enabled and dataset.is_cpu:
58-
with catchtime() as t:
59-
args[self._parameters[0]] = xp.asarray(dataset.data)
60-
self._gpu_time_info.host2device += t.elapsed
6154
ret = self._method(**args)
6255
return self._process_return_type(ret, dataset)
6356

‎httomo/methods.py‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,6 @@ def calculate_stats(
3535
Returns:
3636
tuple[(float, float, float, int)]: (min, max, sum, total_elements)
3737
"""
38-
39-
# do this whereever the data is at the moment (GPU/CPU)
40-
if data.device != "cpu":
41-
# GPU
42-
data = xp.nan_to_num(data, copy=False, nan=0.0, posinf=0, neginf=0)
43-
else:
44-
# CPU
45-
data = np.nan_to_num(data, copy=False, nan=0.0, posinf=0, neginf=0)
46-
4738
return (float(data.min()), float(data.max()), float(data.sum()), data.size)
4839

4940

‎httomo/runner/task_runner.py‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -269,15 +269,12 @@ def _setup_source_sink(self, section: Section, idx: int):
269269
def _execute_section_block(
270270
self, section: Section, block: DataSetBlock
271271
) -> DataSetBlock:
272-
if_previous_block_is_on_gpu = False
273272
convert_gpu_block_to_cpu = False
274273
if_current_block_is_on_gpu = False
275274

276275
for ind, method in enumerate(section):
277276
if method.implementation == "gpu_cupy":
278277
if_current_block_is_on_gpu = True
279-
if method.method_name == "calculate_stats" and if_previous_block_is_on_gpu:
280-
if_current_block_is_on_gpu = True
281278

282279
if ind == len(section) - 1 and if_current_block_is_on_gpu:
283280
convert_gpu_block_to_cpu = True
@@ -309,7 +306,6 @@ def _execute_section_block(
309306
method.gpu_time.device2host,
310307
)
311308

312-
if_previous_block_is_on_gpu = if_current_block_is_on_gpu
313309
return block
314310

315311
def _get_methods_name_for_snapshot(self, section: Section) -> str:

‎httomo/transform_layer.py‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import os
22
from typing import Optional
33
from httomo.method_wrappers import make_method_wrapper
4-
from httomo.runner.output_ref import OutputRef
54
from httomo.method_wrappers.datareducer import DatareducerWrapper
65
from httomo.method_wrappers.generic import GenericMethodWrapper
76
from httomo.method_wrappers.images import ImagesWrapper
8-
from httomo.method_wrappers.stats_calc import StatsCalcWrapper
97
from httomo.method_wrappers.save_intermediate import SaveIntermediateFilesWrapper
108
from httomo.runner.pipeline import Pipeline
119
from mpi4py import MPI
@@ -43,25 +41,29 @@ def transform(self, pipeline: Pipeline) -> Pipeline:
4341
pipeline = self.insert_data_reducer(pipeline)
4442
if pipeline_is_sweep:
4543
pipeline = self.remove_redundant_method_in_sweep(pipeline)
46-
pipeline = self.insert_data_checker(pipeline)
4744

4845
if pipeline_is_sweep:
4946
pipeline = self.insert_save_images_after_sweep(pipeline)
5047
else:
5148
pipeline = self.insert_save_methods(pipeline)
5249

50+
pipeline = self.insert_data_checker(pipeline)
5351
return pipeline
5452

5553
def insert_save_methods(self, pipeline: Pipeline) -> Pipeline:
5654
loader = pipeline.loader
5755
methods = []
58-
for m in pipeline:
56+
for i, m in enumerate(pipeline):
5957
methods.append(m)
6058
if (
6159
(m.save_result or self._save_all)
6260
and m.method_name not in ["save_to_images", "data_checker"]
6361
and "center" not in m.method_name
6462
):
63+
if i + 1 < len(pipeline):
64+
next_method_is_cpu = pipeline[i + 1].is_cpu
65+
else:
66+
next_method_is_cpu = False
6567
methods.append(
6668
SaveIntermediateFilesWrapper(
6769
self._repo,
@@ -71,6 +73,7 @@ def insert_save_methods(self, pipeline: Pipeline) -> Pipeline:
7173
save_result=False,
7274
loader=loader,
7375
prev_method=m,
76+
next_method_is_cpu=next_method_is_cpu,
7477
task_id=f"save_{m.task_id}",
7578
out_dir=self._out_dir,
7679
)
@@ -108,6 +111,7 @@ def insert_data_checker(self, pipeline: Pipeline) -> Pipeline:
108111
"calculate_stats",
109112
"rescale_to_int",
110113
"save_to_images",
114+
"save_intermediate_data",
111115
]
112116
if (
113117
m.method_name not in exceptions_methods

‎tests/method_wrappers/test_save_intermediate.py‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,18 @@ def save_intermediate_data(
130130
assert wrp._file.filename.startswith(str(tmp_path))
131131

132132

133-
@pytest.mark.parametrize("gpu", [False, True], ids=["CPU", "GPU"])
133+
@pytest.mark.parametrize("gpu", [False, True], ids=["data_CPU", "data_GPU"])
134+
@pytest.mark.parametrize(
135+
"next_method_is_cpu",
136+
[False, True],
137+
ids=["Next Method is GPU", "Next Method is CPU"],
138+
)
134139
def test_save_intermediate_leaves_gpu_data(
135-
mocker: MockerFixture, dummy_block: DataSetBlock, tmp_path: Path, gpu: bool
140+
mocker: MockerFixture,
141+
dummy_block: DataSetBlock,
142+
tmp_path: Path,
143+
gpu: bool,
144+
next_method_is_cpu: bool,
136145
):
137146
if gpu and not gpu_enabled:
138147
pytest.skip("No GPU available")
@@ -179,6 +188,7 @@ def save_intermediate_data(
179188
loader=loader,
180189
out_dir=tmp_path,
181190
prev_method=prev_method,
191+
next_method_is_cpu=next_method_is_cpu,
182192
minimum_block_length=dummy_block.shape[0],
183193
)
184194

@@ -189,7 +199,10 @@ def save_intermediate_data(
189199
with mock.patch("httomo.globals.FRAMES_PER_CHUNK", FRAMES_PER_CHUNK):
190200
res = wrp.execute(dummy_block)
191201

192-
assert res.is_gpu == gpu
202+
if next_method_is_cpu and gpu:
203+
assert res.is_cpu == True
204+
else:
205+
assert res.is_gpu == gpu
193206

194207

195208
@pytest.mark.parametrize(

‎tests/method_wrappers/test_stats_calc.py‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def calculate_stats(data, comm):
118118

119119

120120
@pytest.mark.parametrize("gpu", [False, True], ids=["CPU-input", "GPU-input"])
121-
def test_calculate_stats_uses_gpu_if_available(
121+
def test_calculate_stats_agnostic(
122122
mocker: MockerFixture, dummy_block: DataSetBlock, gpu: bool
123123
):
124124
if gpu and not gpu_enabled:
@@ -127,10 +127,6 @@ def test_calculate_stats_uses_gpu_if_available(
127127
class FakeModule:
128128
def calculate_stats(data, comm):
129129
# regardless of dataset input, we want device data if gpu enabled
130-
if gpu_enabled:
131-
assert data.device != "cpu"
132-
else:
133-
assert data.device == "cpu"
134130
return (1.2, 3.1, 42.0, 10)
135131

136132
mocker.patch(
@@ -149,5 +145,7 @@ def calculate_stats(data, comm):
149145
dummy_block.to_gpu()
150146

151147
res = wrp.execute(dummy_block)
152-
153-
assert res.is_gpu == gpu
148+
if gpu is True:
149+
assert res.is_gpu is True
150+
else:
151+
assert res.is_cpu is True

‎tests/test_methods.py‎

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,6 @@ def test_calculate_stats_simple():
2121
assert ret == (-10.0, 19.0, np.sum(data), 30)
2222

2323

24-
def test_calculate_stats_with_nan_and_inf():
25-
data = np.arange(30, dtype=np.float32).reshape((2, 3, 5)) - 10.0
26-
expected = np.sum(data) - data[1, 1, 1] - data[0, 2, 3] - data[1, 2, 3]
27-
data[1, 1, 1] = float("inf")
28-
data[0, 2, 3] = float("-inf")
29-
data[1, 2, 3] = float("nan")
30-
ret = calculate_stats(data)
31-
32-
assert ret == (-10.0, 19.0, expected, 30)
33-
34-
3524
@pytest.mark.skipif(
3625
not gpu_enabled or xp.cuda.runtime.getDeviceCount() == 0,
3726
reason="skipped as cupy is not available",

0 commit comments

Comments
 (0)