Skip to content

Commit 0d1acf6

Browse files
committed
tests: Update estimate-memory tests and consolidate boilerplate
1 parent fc86462 commit 0d1acf6

4 files changed

Lines changed: 96 additions & 106 deletions

File tree

.github/workflows/pytest-gpu.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
include:
4444
# -------------------- NVIDIA job --------------------
4545
- name: pytest-gpu-acc-nvidia
46-
test_files: "tests/test_adjoint.py tests/test_gpu_common.py tests/test_gpu_openacc.py"
46+
test_files: "tests/test_adjoint.py tests/test_gpu_common.py tests/test_gpu_openacc.py tests/test_operator.py::TestEstimateMemory"
4747
base: "devitocodes/bases:nvidia-nvc"
4848
runner_label: nvidiagpu
4949
test_drive_cmd: "nvidia-smi"
@@ -56,7 +56,7 @@ jobs:
5656
5757
# -------------------- AMD job -----------------------
5858
- name: pytest-gpu-omp-amd
59-
test_files: "tests/test_adjoint.py tests/test_gpu_common.py tests/test_gpu_openmp.py"
59+
test_files: "tests/test_adjoint.py tests/test_gpu_common.py tests/test_gpu_openmp.py tests/test_operator.py::TestEstimateMemory"
6060
runner_label: amdgpu
6161
base: "devitocodes/bases:amd"
6262
test_drive_cmd: "rocm-smi"

devito/operator/operator.py

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from devito.tools import (DAG, OrderedSet, Signer, ReducerMap, as_mapper, as_tuple,
3535
flatten, filter_sorted, frozendict, is_integer,
3636
split, timed_pass, timed_region, contains_val,
37-
CacheInstances, MemoryEstimate, humanbytes)
37+
CacheInstances, MemoryEstimate)
3838
from devito.types import (Buffer, Evaluable, host_layer, device_layer,
3939
disk_layer)
4040
from devito.types.dimension import Thickness
@@ -897,8 +897,6 @@ def estimate_memory(self, **kwargs):
897897
898898
Parameters
899899
----------
900-
human_readable: bool
901-
Return human-readable values, rather than raw byte counts. Default is False.
902900
**kwargs: dict
903901
As per `Operator.apply()`.
904902
@@ -1341,6 +1339,36 @@ def saved_mapper(self):
13411339

13421340
return mapper
13431341

1342+
@cached_property
1343+
def _op_symbols(self):
1344+
"""Symbols in the Operator which may or may not carry data"""
1345+
return FindSymbols().visit(self.op)
1346+
1347+
def _apply_override(self, i):
1348+
try:
1349+
return self.get(i.name, i)._obj
1350+
except AttributeError:
1351+
return self.get(i.name, i)
1352+
1353+
def _get_nbytes(self, i):
1354+
"""
1355+
Extract the allocated size of a symbol, accounting for any
1356+
overrides.
1357+
"""
1358+
obj = self._apply_override(i)
1359+
try:
1360+
# Non-regular AbstractFunction (compressed, etc)
1361+
nbytes = obj.nbytes_max
1362+
except AttributeError:
1363+
# Garden-variety AbstractFunction
1364+
nbytes = obj.nbytes
1365+
1366+
# Could nominally have symbolic nbytes at this point
1367+
if isinstance(nbytes, SympyBasic):
1368+
return subs_op_args(nbytes, self)
1369+
1370+
return nbytes
1371+
13441372
@cached_property
13451373
def nbytes_avail_mapper(self):
13461374
"""
@@ -1400,32 +1428,14 @@ def nbytes_consumed_functions(self):
14001428
Memory consumed on both device and host by Functions in the
14011429
corresponding Operator.
14021430
"""
1403-
def get_nbytes(obj):
1404-
if obj.is_regular:
1405-
nbytes = obj.nbytes
1406-
else:
1407-
nbytes = obj.nbytes_max
1408-
1409-
# Could nominally have symbolic nbytes at this point
1410-
if isinstance(nbytes, SympyBasic):
1411-
return subs_op_args(nbytes, self)
1412-
else:
1413-
return nbytes
1414-
14151431
host = 0
14161432
device = 0
1417-
14181433
# Filter out arrays, aliases and non-AbstractFunction objects
14191434
op_symbols = [i for i in self._op_symbols if i.is_AbstractFunction
14201435
and not i.is_ArrayBasic and not i.alias]
14211436

14221437
for i in op_symbols:
1423-
try:
1424-
# TODO: is _obj even needed?
1425-
v = get_nbytes(self[i.name]._obj)
1426-
except AttributeError:
1427-
v = get_nbytes(self.get(i.name, i))
1428-
1438+
v = self._get_nbytes(i)
14291439
if i._mem_host or i._mem_mapped:
14301440
# No need to add to device , as it will be counted
14311441
# by nbytes_consumed_memmapped
@@ -1446,7 +1456,6 @@ def nbytes_consumed_arrays(self):
14461456
"""
14471457
host = 0
14481458
device = 0
1449-
14501459
# Temporaries such as Arrays are allocated and deallocated on-the-fly
14511460
# while in C land, so they need to be accounted for as well
14521461
for i in self._op_symbols:
@@ -1492,11 +1501,7 @@ def nbytes_consumed_memmapped(self):
14921501
continue
14931502
try:
14941503
if i._mem_mapped:
1495-
try:
1496-
v = self[i.name]._obj.nbytes
1497-
except AttributeError:
1498-
v = i.nbytes
1499-
device += v
1504+
device += self._get_nbytes(i)
15001505
except AttributeError:
15011506
pass
15021507

@@ -1511,22 +1516,16 @@ def nbytes_snapshots(self):
15111516
disk = 0
15121517
for i in op_symbols:
15131518
try:
1514-
v = self[i.name]._obj
1515-
except AttributeError:
1516-
v = self.get(i.name, i)
1517-
1518-
try:
1519-
disk += v.size_snapshot*v._time_size_ideal*np.dtype(v.dtype).itemsize
1519+
if i._child not in op_symbols:
1520+
# Use only the "innermost" layer to avoid counting snapshots
1521+
# twice
1522+
v = self._apply_override(i)
1523+
disk += v.size_snapshot*v._time_size_ideal*np.dtype(v.dtype).itemsize
15201524
except AttributeError:
15211525
pass
15221526

15231527
return {disk_layer: disk, host_layer: 0, device_layer: 0}
15241528

1525-
@cached_property
1526-
def _op_symbols(self):
1527-
"""Symbols in the Operator which may or may not carry data"""
1528-
return FindSymbols().visit(self.op)
1529-
15301529

15311530
def parse_kwargs(**kwargs):
15321531
"""

devito/tools/data_structures.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -662,10 +662,16 @@ def __hash__(self):
662662

663663
class MemoryEstimate(frozendict):
664664
"""
665-
An immutable wrapper for a memory estimate, showing the
666-
various values.
665+
An immutable mapper for a memory estimate, providing the estimated memory
666+
consumption across host, device, and so forth.
667667
668-
TODO: Finish this docstring
668+
Properties
669+
----------
670+
name: str
671+
The name of the Operator for which this estimate was generated
672+
human_readable: frozendict
673+
The mapper, albeit with human-readable memory usage (MB, GB, etc)
674+
rather than raw bytes.
669675
"""
670676

671677
def __init__(self, *args, **kwargs):

tests/test_operator.py

Lines changed: 47 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
NODE, CELL, dimensions, configuration, TensorFunction,
2121
TensorTimeFunction, VectorFunction, VectorTimeFunction,
2222
div, grad, switchconfig, exp, Buffer)
23-
from devito import Inc, Le, Lt, Ge, Gt # noqa
23+
from devito import Inc, Le, Lt, Ge, Gt, sin # noqa
24+
from devito.arch.archinfo import Device
2425
from devito.exceptions import InvalidOperator
2526
from devito.finite_differences.differentiable import diff2sympy
2627
from devito.ir.equations import ClusterizedEq
@@ -2066,12 +2067,17 @@ def test_indirection(self):
20662067
class TestEstimateMemory:
20672068
"""Tests for the Operator.estimate_memory() utility"""
20682069

2069-
_array_temp = "r0L0(x, y)" if "CXX" in configuration['language'] else "r0[x][y]"
2070+
_array_temp = "r0L0(" if "CXX" in configuration['language'] else "r0["
20702071

2071-
def parse_output(self, summary, expected):
2072-
"""Parse estimate_memory machine-readable output"""
2072+
def parse_output(self, summary, check, arrays=0):
2073+
device = isinstance(configuration['platform'], Device)
2074+
expected = ((check, check + arrays) if device else (check + arrays, 0))
20732075
assert (summary['host'], summary['device']) == expected
20742076

2077+
def sum_sizes(self, funcs):
2078+
return sum(reduce(mul, func.shape_allocated)*np.dtype(func.dtype).itemsize
2079+
for func in funcs)
2080+
20752081
@pytest.mark.parametrize('shape', [(11,), (101, 101), (101, 101, 101)])
20762082
@pytest.mark.parametrize('dtype', [np.int8, np.int16, np.float32,
20772083
np.float32, np.complex64])
@@ -2088,8 +2094,7 @@ def test_basic_usage(self, caplog, shape, dtype, so):
20882094

20892095
# Check output of estimate_memory
20902096
host = reduce(mul, f.shape_allocated)*np.dtype(f.dtype).itemsize
2091-
expected = (host, 0)
2092-
self.parse_output(summary, expected)
2097+
self.parse_output(summary, host)
20932098

20942099
def test_multiple_objects(self, caplog):
20952100
grid = Grid(shape=(101, 101))
@@ -2101,10 +2106,8 @@ def test_multiple_objects(self, caplog):
21012106
summary = op.estimate_memory()
21022107
assert "Allocating" not in caplog.text
21032108

2104-
check = sum(reduce(mul, func.shape_allocated)*np.dtype(func.dtype).itemsize
2105-
for func in (f, g))
2106-
expected = (check, 0)
2107-
self.parse_output(summary, expected)
2109+
check = self.sum_sizes((f, g))
2110+
self.parse_output(summary, check)
21082111

21092112
@pytest.mark.parametrize('time', [True, False])
21102113
def test_sparse(self, caplog, time):
@@ -2121,10 +2124,8 @@ def test_sparse(self, caplog, time):
21212124
summary = op.estimate_memory()
21222125
assert "Allocating" not in caplog.text
21232126

2124-
check = sum(reduce(mul, func.shape_allocated)*np.dtype(func.dtype).itemsize
2125-
for func in (f, src, src.coordinates))
2126-
expected = (check, 0)
2127-
self.parse_output(summary, expected)
2127+
check = self.sum_sizes((f, src, src.coordinates))
2128+
self.parse_output(summary, check)
21282129

21292130
@pytest.mark.parametrize('save', [None, Buffer(3), 10])
21302131
def test_timefunction(self, caplog, save):
@@ -2136,8 +2137,7 @@ def test_timefunction(self, caplog, save):
21362137
summary = op.estimate_memory()
21372138
assert "Allocating" not in caplog.text
21382139
check = reduce(mul, f.shape_allocated)*np.dtype(f.dtype).itemsize
2139-
expected = (check, 0)
2140-
self.parse_output(summary, expected)
2140+
self.parse_output(summary, check)
21412141

21422142
def test_mashup(self, caplog):
21432143
grid = Grid(shape=(101, 101))
@@ -2158,11 +2158,8 @@ def test_mashup(self, caplog):
21582158
summary = op.estimate_memory()
21592159
assert "Allocating" not in caplog.text
21602160

2161-
check = sum(reduce(mul, func.shape_allocated)*np.dtype(func.dtype).itemsize
2162-
for func in (f, g, src0, src0.coordinates,
2163-
src1, src1.coordinates))
2164-
expected = (check, 0)
2165-
self.parse_output(summary, expected)
2161+
check = self.sum_sizes((f, g, src0, src0.coordinates, src1, src1.coordinates))
2162+
self.parse_output(summary, check)
21662163

21672164
@pytest.mark.parametrize('override', [True, False])
21682165
def test_temp_array(self, caplog, override):
@@ -2188,8 +2185,8 @@ def test_temp_array(self, caplog, override):
21882185
b = Function(name='b', grid=grid, space_order=0)
21892186

21902187
# Reuse an expensive function to encourage generation of an array temp
2191-
eq0 = Eq(f.forward, g + sympy.sin(a))
2192-
eq1 = Eq(g.forward, f + sympy.sin(a))
2188+
eq0 = Eq(f.forward, g + sin(a).dx)
2189+
eq1 = Eq(g.forward, f + sin(a).dx)
21932190

21942191
with switchconfig(log_level='DEBUG'), caplog.at_level(logging.DEBUG):
21952192
op = Operator([eq0, eq1])
@@ -2201,37 +2198,32 @@ def test_temp_array(self, caplog, override):
22012198
summary = op.estimate_memory(**kwargs)
22022199
assert "Allocating" not in caplog.text
22032200

2204-
check = sum(reduce(mul, func.shape_allocated)*np.dtype(func.dtype).itemsize
2205-
for func in funcs)
2201+
check = self.sum_sizes(funcs)
22062202

22072203
# Factor in the temp array
2208-
check += reduce(mul, b.shape_allocated)*np.dtype(b.dtype).itemsize
2209-
2210-
expected = (check, 0)
2211-
self.parse_output(summary, expected)
2204+
# Note: temp array size is incremented by one in the x dimension
2205+
# due to derivative.
2206+
array_check = (b.shape_allocated[0]+1)*b.shape_allocated[1]
2207+
array_check *= np.dtype(b.dtype).itemsize
2208+
self.parse_output(summary, check, arrays=array_check)
22122209

22132210
def test_overrides(self, caplog):
2214-
# TODO: Consolidate this boilerplate
2215-
grid0 = Grid(shape=(101, 101))
2211+
def setup(size, npoint, nt, counter):
2212+
grid = Grid(shape=(size, size))
2213+
# Original fields
2214+
f = Function(name=f'f{counter}', grid=grid, space_order=4)
2215+
tf = TimeFunction(name=f'tf{counter}', grid=grid, space_order=4)
2216+
s = SparseFunction(name=f's{counter}', grid=grid, npoint=npoint)
2217+
st = SparseTimeFunction(name=f'st{counter}', grid=grid, npoint=npoint, nt=nt)
2218+
2219+
return f, tf, s, st
2220+
22162221
# Original fields
2217-
f0 = Function(name='f0', grid=grid0, space_order=4)
2218-
tf0 = TimeFunction(name='tf0', grid=grid0, space_order=4)
2219-
s0 = SparseFunction(name='s0', grid=grid0, npoint=100)
2220-
st0 = SparseTimeFunction(name='st0', grid=grid0, npoint=100, nt=10)
2221-
2222-
grid1 = Grid(shape=(201, 201)) # Bigger grid so overrides are distinct
2223-
# Replacement fields
2224-
f1 = Function(name='f1', grid=grid1, space_order=4)
2225-
tf1 = TimeFunction(name='tf1', grid=grid1, space_order=4)
2226-
s1 = SparseFunction(name='s1', grid=grid1, npoint=200)
2227-
st1 = SparseTimeFunction(name='st1', grid=grid1, npoint=200, nt=20)
2228-
2229-
grid2 = Grid(shape=(51, 51)) # Smaller grid so overrides are distinct
2230-
# Alternative replacement fields
2231-
f2 = Function(name='f2', grid=grid2, space_order=4)
2232-
tf2 = TimeFunction(name='tf2', grid=grid2, space_order=4)
2233-
s2 = SparseFunction(name='s2', grid=grid2, npoint=50)
2234-
st2 = SparseTimeFunction(name='st2', grid=grid2, npoint=50, nt=5)
2222+
f0, tf0, s0, st0 = setup(101, 100, 10, 0)
2223+
# Replacement fields with bigger grid, etc
2224+
f1, tf1, s1, st1 = setup(201, 200, 20, 1)
2225+
# Replacement fields with smaller grid, etc
2226+
f2, tf2, s2, st2 = setup(51, 50, 5, 2)
22352227

22362228
eq0 = Eq(f0, 1)
22372229
eq1 = Eq(tf0, 1)
@@ -2244,21 +2236,15 @@ def test_overrides(self, caplog):
22442236
# Apply overrides for the check
22452237
summary0 = op.estimate_memory(f0=f1, tf0=tf1, s0=s1, st0=st1)
22462238

2247-
check0 = sum(reduce(mul, func.shape_allocated)*np.dtype(func.dtype).itemsize
2248-
for func in (f1, tf1, s1, s1.coordinates, st1, st1.coordinates))
2249-
2250-
expected0 = (check0, 0)
2251-
self.parse_output(summary0, expected0)
2239+
check0 = self.sum_sizes((f1, tf1, s1, s1.coordinates, st1, st1.coordinates))
2240+
self.parse_output(summary0, check0)
22522241

22532242
# Check with a second set of overrides
22542243
summary1 = op.estimate_memory(f0=f2, tf0=tf2, s0=s2, st0=st2)
22552244
assert "Allocating" not in caplog.text
22562245

2257-
check1 = sum(reduce(mul, func.shape_allocated)*np.dtype(func.dtype).itemsize
2258-
for func in (f2, tf2, s2, s2.coordinates, st2, st2.coordinates))
2259-
2260-
expected1 = (check1, 0)
2261-
self.parse_output(summary1, expected1)
2246+
check1 = self.sum_sizes((f2, tf2, s2, s2.coordinates, st2, st2.coordinates))
2247+
self.parse_output(summary1, check1)
22622248

22632249
def test_device(self, caplog):
22642250
# Note: this uses switchconfig and runs on all backends to reflect expected
@@ -2269,7 +2255,7 @@ def test_device(self, caplog):
22692255

22702256
f = Function(name='f', grid=grid, space_order=2)
22712257

2272-
# Compiler is never invoked, so this should be fine
2258+
# Compiler is never invoked, so this is fine
22732259
config = {'log_level': 'DEBUG', 'language': 'openacc',
22742260
'platform': 'nvidiaX'}
22752261
with switchconfig(**config), caplog.at_level(logging.DEBUG):
@@ -2281,5 +2267,4 @@ def test_device(self, caplog):
22812267
check = reduce(mul, f.shape_allocated)*np.dtype(f.dtype).itemsize
22822268

22832269
# Matching memory allocated both on host and device for memmap
2284-
expected = (check, check)
2285-
self.parse_output(summary, expected)
2270+
self.parse_output(summary, check)

0 commit comments

Comments
 (0)