Repository navigation
Expand file tree
/
Copy pathtest_plan.py
More file actions
345 lines (262 loc) · 10.7 KB
/
Copy pathtest_plan.py
File metadata and controls
345 lines (262 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
"""Test the memory planner: sizes, budgets, batches and tiers."""
from contextlib import contextmanager
from pathlib import Path
import numpy as np
import pytest
import src_method._plan as plan_module
from src_method import Resources
from src_method._group import DeviceGroup
from src_method._plan import (
GEMM_MULTIPLE,
Budgets,
make_plan,
parse_size,
resolve_budgets,
)
GB = 10**9
HOST = DeviceGroup("cpu")
@pytest.mark.parametrize(
("value", "expected"),
[
(123, 123),
("36GB", 36 * 10**9),
("36GiB", 36 * 2**30),
("1.5 kB", 1500),
("512", 512),
("2mib", 2 * 2**20),
],
)
def test_parse_size(value, expected):
assert parse_size(value) == expected
@pytest.mark.parametrize("value", [-1, "36 GBs", "lots", ""])
def test_parse_size_rejects_values(value):
with pytest.raises(ValueError, match="Expected"):
parse_size(value)
@pytest.mark.parametrize("value", [True, 1.5, None])
def test_parse_size_rejects_types(value):
with pytest.raises(TypeError, match="Expected"):
parse_size(value)
def test_resources_validate_at_construction():
with pytest.raises(ValueError, match="size string"):
Resources(gpu_memory="plenty")
def test_resolve_budgets_explicit_on_host(tmp_path):
budgets = resolve_budgets(Resources(host_memory="2GB", scratch_dir=tmp_path), HOST)
assert budgets.unified
assert budgets.host == budgets.device == 2 * GB
assert budgets.scratch_dir == tmp_path
assert budgets.disk > 0
def test_resolve_budgets_detects_host_memory(monkeypatch, tmp_path):
monkeypatch.setattr(plan_module, "host_memory_available", lambda: 10 * GB)
budgets = resolve_budgets(Resources(scratch_dir=tmp_path / "not" / "yet"), HOST)
assert budgets.host == 9 * GB
@pytest.fixture
def fake_gpus(monkeypatch):
"""Fake devices for the planner; returns a dict of ``id -> (free, total)``."""
memory = {0: (30 * GB, 40 * GB)}
current = [0]
@contextmanager
def use_device(_xp, device):
previous, current[0] = current[0], device
try:
yield
finally:
current[0] = previous
monkeypatch.setattr(plan_module, "is_host", lambda _xp: False)
monkeypatch.setattr(plan_module, "use_device", use_device)
monkeypatch.setattr(plan_module, "device_memory", lambda _xp: memory[current[0]])
return memory
@pytest.mark.usefixtures("fake_gpus")
def test_resolve_budgets_detects_device_memory(tmp_path):
budgets = resolve_budgets(Resources(host_memory="1GB", scratch_dir=tmp_path), HOST)
assert not budgets.unified
assert budgets.device == 26 * GB # minus max(10% of 40 GB, 1 GiB)
assert budgets.device_cap == 30 * GB # the pool may grow into the margin
@pytest.mark.usefixtures("fake_gpus")
def test_resolve_budgets_caps_explicit_device_memory_with_margin(tmp_path):
budgets = resolve_budgets(
Resources(gpu_memory="10GB", host_memory="1GB", scratch_dir=tmp_path), HOST
)
assert budgets.device == 10 * GB
assert budgets.device_cap == 14 * GB
def test_resolve_budgets_takes_the_smallest_device(fake_gpus, tmp_path):
fake_gpus[1] = (20 * GB, 40 * GB)
budgets = resolve_budgets(
Resources(host_memory="1GB", scratch_dir=tmp_path), DeviceGroup("cpu", 2)
)
assert budgets.device == 16 * GB
assert budgets.device_cap == 20 * GB
def test_resolve_budgets_splits_the_host_among_simulated_devices(tmp_path):
budgets = resolve_budgets(
Resources(host_memory="8GB", scratch_dir=tmp_path), DeviceGroup("cpu", 4)
)
assert budgets.host == 8 * GB
assert budgets.device == 2 * GB
def test_resolve_budgets_leaves_the_host_uncapped(tmp_path):
budgets = resolve_budgets(Resources(host_memory="1GB", scratch_dir=tmp_path), HOST)
assert budgets.device_cap is None
def test_resolve_budgets_detects_disk(monkeypatch, tmp_path):
usage = type("Usage", (), {"free": 100 * GB})
monkeypatch.setattr(plan_module.shutil, "disk_usage", lambda _path: usage)
budgets = resolve_budgets(Resources(host_memory="1GB", scratch_dir=tmp_path), HOST)
assert budgets.disk == 95 * GB
def mpo_stack_shapes(n_sites, bonds, phys=4):
"""Padded shapes of a stack of MPOs, one bond dimension per layer."""
def shape(j, bond):
return (1 if j == 0 else bond, 1 if j == n_sites - 1 else bond, phys, phys)
return [tuple(shape(j, bond) for bond in bonds) for j in range(n_sites)]
def site_bytes(shapes, itemsize=16):
return [sum(int(np.prod(s)) * itemsize for s in site) for site in shapes]
def budgets(device, host=None, disk=10**15, *, unified=False):
return Budgets(
device, device if host is None else host, disk, Path("/scratch"), unified
)
def test_small_problem_is_one_batch_on_the_device():
shapes = mpo_stack_shapes(6, [2, 3, 2])
plan = make_plan(shapes, site_bytes(shapes), 64, np.complex128, budgets(GB))
assert plan.prefetch == 1
assert all(site.tier == "device" for site in plan.sites)
assert plan.sites[0].env_batch == 64
assert plan.sites[-1].env_batch == 0
assert plan.sites[0].sketch_batch == 0
assert all(site.sketch_batch == 64 for site in plan.sites[1:])
assert all(site.project_batch == 64 for site in plan.sites)
assert plan.disk_bytes == 0
def test_tight_budget_shrinks_batches_to_gemm_multiples():
shapes = mpo_stack_shapes(6, [4, 4, 64, 4])
chi = 512
loose = make_plan(shapes, site_bytes(shapes), chi, np.complex128, budgets(10 * GB))
tight = make_plan(
shapes, site_bytes(shapes), chi, np.complex128, budgets(loose.device_peak // 4)
)
batches = [s.sketch_batch for s in tight.sites[1:]]
assert min(batches) < chi
assert all(b % GEMM_MULTIPLE == 0 for b in batches if b >= GEMM_MULTIPLE)
assert tight.device_peak <= loose.device_peak // 4
def test_tiers_go_newest_first():
shapes = mpo_stack_shapes(8, [4, 4, 64, 4])
chi = 256
env = chi * 4 * 4 * 64 * 4 * 16 # one bulk environment, complex128
roomy = make_plan(shapes, site_bytes(shapes), chi, np.complex128, budgets(10 * GB))
seen = set()
for extra in range(8):
plan = make_plan(
shapes,
site_bytes(shapes),
chi,
np.complex128,
budgets(roomy.device_peak - extra * env, host=roomy.host_peak + 7 * env),
)
tiers = [site.tier for site in plan.sites[1:]]
# Oldest sites on the slowest tier: disk, then host, then device.
assert tiers == sorted(tiers, key=["disk", "host", "device"].index)
assert plan.disk_bytes == env * tiers.count("disk")
seen.update(tiers)
assert seen == {"device", "host", "disk"}
def test_unified_memory_has_no_host_tier():
shapes = mpo_stack_shapes(8, [4, 4, 64, 4])
chi = 256
roomy = make_plan(
shapes, site_bytes(shapes), chi, np.complex128, budgets(10 * GB, unified=True)
)
plan = make_plan(
shapes,
site_bytes(shapes),
chi,
np.complex128,
budgets(roomy.device_peak // 2, unified=True),
)
tiers = {site.tier for site in plan.sites[1:]}
assert "host" not in tiers
assert "disk" in tiers
def test_infeasible_site_names_the_site():
shapes = mpo_stack_shapes(6, [4, 4, 64, 4])
with pytest.raises(MemoryError, match=r"Site \d+: the \w+ step needs \d+ bytes"):
make_plan(shapes, site_bytes(shapes), 256, np.complex128, budgets(10**6))
def test_environments_must_fit_on_disk():
shapes = mpo_stack_shapes(8, [4, 4, 64, 4])
chi = 256
roomy = make_plan(shapes, site_bytes(shapes), chi, np.complex128, budgets(10 * GB))
work = roomy.device_peak - 7 * chi * 4 * 4 * 64 * 4 * 16
with pytest.raises(MemoryError, match="bytes on disk"):
make_plan(
shapes,
site_bytes(shapes),
chi,
np.complex128,
budgets(work, host=roomy.host_peak, disk=1000),
)
def test_prefetch_is_dropped_before_giving_up():
shapes = mpo_stack_shapes(6, [4, 4, 64, 4])
chi = 64
cores = max(site_bytes(shapes))
with_prefetch = make_plan(
shapes, site_bytes(shapes), chi, np.complex128, budgets(10 * GB)
)
# Remove about one site of cores from the smallest budget that fits one column.
minimum = with_prefetch.device_peak
lo, hi = 0, minimum
while lo < hi: # the smallest budget that plans with prefetching
mid = (lo + hi) // 2
try:
make_plan(shapes, site_bytes(shapes), chi, np.complex128, budgets(mid))
hi = mid
except MemoryError:
lo = mid + 1
plan = make_plan(shapes, site_bytes(shapes), chi, np.complex128, budgets(lo))
assert plan.prefetch in {0, 1}
if plan.prefetch == 1:
dropped = make_plan(
shapes, site_bytes(shapes), chi, np.complex128, budgets(lo - cores // 2)
)
assert dropped.prefetch == 0
def test_devices_split_the_environments_and_the_batches():
shapes = mpo_stack_shapes(8, [4, 4, 64, 4])
chi = 256
env = chi * 4 * 4 * 64 * 4 * 16 # one bulk environment on one device
one = make_plan(shapes, site_bytes(shapes), chi, np.complex128, budgets(10 * GB))
# A device budget that holds half of one device's environments on four devices
# holds all of them.
tight = one.device_peak - 3 * env
single = make_plan(
shapes, site_bytes(shapes), chi, np.complex128, budgets(tight, host=10 * GB)
)
four = make_plan(
shapes,
site_bytes(shapes),
chi,
np.complex128,
budgets(tight, host=10 * GB),
devices=4,
)
assert "host" in {s.tier for s in single.sites}
assert {s.tier for s in four.sites} == {"device"}
for site in four.sites[1:]:
assert site.sketch_batch <= chi // 4
assert site.project_batch <= chi // 4
assert four.sites[0].env_batch <= chi // 4
# The first site runs on rank 0 alone, on every row.
assert four.sites[0].project_batch == chi
def test_devices_share_host_memory_and_disk():
shapes = mpo_stack_shapes(8, [4, 4, 64, 4])
chi = 256
env = chi * 4 * 4 * 64 * 4 * 16 // 2 # one bulk environment on each of 2 devices
roomy = make_plan(
shapes, site_bytes(shapes), chi, np.complex128, budgets(10 * GB), devices=2
)
device = roomy.device_peak - 6 * env
# Each device stages four batches of 128 columns, one environment each.
staging = 4 * env
host = roomy.host_peak + 2 * staging + 4 * env
plan = make_plan(
shapes,
site_bytes(shapes),
chi,
np.complex128,
budgets(device, host=host),
devices=2,
)
tiers = [s.tier for s in plan.sites[1:]]
# Host room for four environments in all: two per device.
assert tiers.count("host") == 2
assert plan.disk_bytes == 2 * env * tiers.count("disk")
assert plan.host_peak <= host