shardguard 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shardguard-1.0.0/LICENSE +21 -0
- shardguard-1.0.0/PKG-INFO +36 -0
- shardguard-1.0.0/README.md +21 -0
- shardguard-1.0.0/pyproject.toml +37 -0
- shardguard-1.0.0/setup.cfg +4 -0
- shardguard-1.0.0/src/shardguard.egg-info/PKG-INFO +36 -0
- shardguard-1.0.0/src/shardguard.egg-info/SOURCES.txt +9 -0
- shardguard-1.0.0/src/shardguard.egg-info/dependency_links.txt +1 -0
- shardguard-1.0.0/src/shardguard.egg-info/top_level.txt +1 -0
- shardguard-1.0.0/src/shardguard.py +131 -0
- shardguard-1.0.0/tests/test_shardguard.py +129 -0
shardguard-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jonas Reiter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shardguard
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Cap the GPU memory of PyTorch at the GPU shards of a Slurm job
|
|
5
|
+
Author: Jonas Reiter
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/jonaslreiter/shardguard
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# shardguard
|
|
17
|
+
|
|
18
|
+
Slurm can split a GPU into shards, but it does not limit the GPU memory of a job that holds shards, so the job can take memory that its co-tenants on the card were promised. shardguard caps the CUDA memory of PyTorch at the share of the GPU that the shards of the job stand for. Install it with
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install shardguard
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
and import it in the entry point of your program, before anything initialises CUDA:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import shardguard
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Past the cap, PyTorch raises an out-of-memory error. Outside a Slurm job that holds shards, nothing happens.
|
|
31
|
+
|
|
32
|
+
## How it works
|
|
33
|
+
|
|
34
|
+
The share is the number of shards of the job over the number of shards one GPU of the node holds, which shardguard reads from `scontrol show node`. Setting `SHARDGUARD_SHARDS_PER_GPU` gives that number instead. The cap is `per_process_memory_fraction` in `PYTORCH_ALLOC_CONF` and `PYTORCH_CUDA_ALLOC_CONF`, together with `expandable_segments`, which keeps the memory PyTorch reserves close to what it allocates.
|
|
35
|
+
|
|
36
|
+
The cap needs PyTorch 2.10 or later with its native allocator. PyTorch reads the setting when it initialises CUDA, so shardguard raises an error when it is imported after that. The cap applies to each process separately and does not cover the CUDA context, which takes a few hundred MB. shardguard also raises an error when the shards of the job are spread over several GPUs.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# shardguard
|
|
2
|
+
|
|
3
|
+
Slurm can split a GPU into shards, but it does not limit the GPU memory of a job that holds shards, so the job can take memory that its co-tenants on the card were promised. shardguard caps the CUDA memory of PyTorch at the share of the GPU that the shards of the job stand for. Install it with
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install shardguard
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
and import it in the entry point of your program, before anything initialises CUDA:
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import shardguard
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Past the cap, PyTorch raises an out-of-memory error. Outside a Slurm job that holds shards, nothing happens.
|
|
16
|
+
|
|
17
|
+
## How it works
|
|
18
|
+
|
|
19
|
+
The share is the number of shards of the job over the number of shards one GPU of the node holds, which shardguard reads from `scontrol show node`. Setting `SHARDGUARD_SHARDS_PER_GPU` gives that number instead. The cap is `per_process_memory_fraction` in `PYTORCH_ALLOC_CONF` and `PYTORCH_CUDA_ALLOC_CONF`, together with `expandable_segments`, which keeps the memory PyTorch reserves close to what it allocates.
|
|
20
|
+
|
|
21
|
+
The cap needs PyTorch 2.10 or later with its native allocator. PyTorch reads the setting when it initialises CUDA, so shardguard raises an error when it is imported after that. The cap applies to each process separately and does not cover the CUDA context, which takes a few hundred MB. shardguard also raises an error when the shards of the job are spread over several GPUs.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "shardguard"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Cap the GPU memory of PyTorch at the GPU shards of a Slurm job"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Jonas Reiter" }]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: POSIX :: Linux",
|
|
17
|
+
"Topic :: System :: Distributed Computing",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Repository = "https://github.com/jonaslreiter/shardguard"
|
|
22
|
+
|
|
23
|
+
[dependency-groups]
|
|
24
|
+
dev = ["pytest>=8"]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools]
|
|
27
|
+
package-dir = { "" = "src" }
|
|
28
|
+
py-modules = ["shardguard"]
|
|
29
|
+
|
|
30
|
+
[tool.pytest.ini_options]
|
|
31
|
+
testpaths = ["tests"]
|
|
32
|
+
|
|
33
|
+
[tool.black]
|
|
34
|
+
target-version = ["py310"]
|
|
35
|
+
|
|
36
|
+
[tool.isort]
|
|
37
|
+
profile = "black"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shardguard
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Cap the GPU memory of PyTorch at the GPU shards of a Slurm job
|
|
5
|
+
Author: Jonas Reiter
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/jonaslreiter/shardguard
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# shardguard
|
|
17
|
+
|
|
18
|
+
Slurm can split a GPU into shards, but it does not limit the GPU memory of a job that holds shards, so the job can take memory that its co-tenants on the card were promised. shardguard caps the CUDA memory of PyTorch at the share of the GPU that the shards of the job stand for. Install it with
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install shardguard
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
and import it in the entry point of your program, before anything initialises CUDA:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import shardguard
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Past the cap, PyTorch raises an out-of-memory error. Outside a Slurm job that holds shards, nothing happens.
|
|
31
|
+
|
|
32
|
+
## How it works
|
|
33
|
+
|
|
34
|
+
The share is the number of shards of the job over the number of shards one GPU of the node holds, which shardguard reads from `scontrol show node`. Setting `SHARDGUARD_SHARDS_PER_GPU` gives that number instead. The cap is `per_process_memory_fraction` in `PYTORCH_ALLOC_CONF` and `PYTORCH_CUDA_ALLOC_CONF`, together with `expandable_segments`, which keeps the memory PyTorch reserves close to what it allocates.
|
|
35
|
+
|
|
36
|
+
The cap needs PyTorch 2.10 or later with its native allocator. PyTorch reads the setting when it initialises CUDA, so shardguard raises an error when it is imported after that. The cap applies to each process separately and does not cover the CUDA context, which takes a few hundred MB. shardguard also raises an error when the shards of the job are spread over several GPUs.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
shardguard
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Importing shardguard caps the CUDA memory of PyTorch at the share of a GPU that the GPU shards of the Slurm job stand for.
|
|
2
|
+
|
|
3
|
+
The cap is ``per_process_memory_fraction`` in the allocator configuration of PyTorch, set to the shards of the job over the shards one GPU of the node holds. ``expandable_segments`` keeps what PyTorch reserves close to what it allocates. PyTorch reads the configuration when it initialises CUDA, so shardguard must be imported before that, which it checks. Outside a job holding shards, it does nothing.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from importlib import metadata
|
|
14
|
+
from typing import MutableMapping
|
|
15
|
+
|
|
16
|
+
SHARDS_PER_GPU = "SHARDGUARD_SHARDS_PER_GPU"
|
|
17
|
+
# PyTorch reads the first of these that is set, and releases before 2.9 only the second.
|
|
18
|
+
ALLOC_CONF = ("PYTORCH_ALLOC_CONF", "PYTORCH_CUDA_ALLOC_CONF")
|
|
19
|
+
FRACTION = re.compile(r"per_process_memory_fraction:([0-9.]+)")
|
|
20
|
+
# The first release whose allocator configuration takes per_process_memory_fraction.
|
|
21
|
+
OLDEST_TORCH = (2, 10)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def shards_per_gpu(gres: str) -> float | None:
|
|
25
|
+
"""Shards per GPU from the ``Gres`` of a node, e.g. 24 for ``gpu:A10:4,shard:A10:96``, or None if it lists no GPUs or no shards."""
|
|
26
|
+
counts = {"gpu": 0, "shard": 0}
|
|
27
|
+
for entry in re.sub(r"\([^)]*\)", "", gres).split(","):
|
|
28
|
+
fields = entry.split(":")
|
|
29
|
+
if fields[0] in counts and fields[-1].isdigit():
|
|
30
|
+
counts[fields[0]] += int(fields[-1])
|
|
31
|
+
if not counts["gpu"] or not counts["shard"]:
|
|
32
|
+
return None
|
|
33
|
+
return counts["shard"] / counts["gpu"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def node_gres(node: str) -> str:
|
|
37
|
+
"""The ``Gres`` that ``scontrol show node`` reports for `node`."""
|
|
38
|
+
output = subprocess.run(
|
|
39
|
+
["scontrol", "show", "node", node],
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
check=True,
|
|
43
|
+
timeout=60,
|
|
44
|
+
).stdout
|
|
45
|
+
match = re.search(r"\bGres=(\S*)", output)
|
|
46
|
+
return match.group(1) if match else ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def fraction(environ: MutableMapping[str, str] = os.environ) -> float | None:
|
|
50
|
+
"""The share of one GPU that the shards of the job stand for, or None outside a job holding shards."""
|
|
51
|
+
shards = environ.get("SLURM_SHARDS_ON_NODE")
|
|
52
|
+
if not shards:
|
|
53
|
+
return None
|
|
54
|
+
if "," in environ.get("CUDA_VISIBLE_DEVICES", ""):
|
|
55
|
+
raise RuntimeError(
|
|
56
|
+
"shardguard: the shards of this job are on several GPUs, which one fraction per GPU cannot cap"
|
|
57
|
+
)
|
|
58
|
+
per_gpu = environ.get(SHARDS_PER_GPU)
|
|
59
|
+
if not per_gpu:
|
|
60
|
+
try:
|
|
61
|
+
per_gpu = shards_per_gpu(node_gres(environ["SLURMD_NODENAME"]))
|
|
62
|
+
except (KeyError, OSError, subprocess.SubprocessError):
|
|
63
|
+
per_gpu = None
|
|
64
|
+
if not per_gpu:
|
|
65
|
+
raise RuntimeError(
|
|
66
|
+
f"shardguard: cannot tell how many shards one GPU of this node holds, so set {SHARDS_PER_GPU}"
|
|
67
|
+
)
|
|
68
|
+
environ[SHARDS_PER_GPU] = f"{per_gpu:g}"
|
|
69
|
+
return min(1.0, int(shards) / float(per_gpu))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _rounded(share: float) -> float:
|
|
73
|
+
"""`share` rounded down to four decimals."""
|
|
74
|
+
return math.floor(share * 1e4) / 1e4
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def with_cap(conf: str, share: float) -> str:
|
|
78
|
+
"""An allocator configuration capped at `share`, with expandable segments unless it sets them itself. A lower cap it already has is kept."""
|
|
79
|
+
settings = [item for item in conf.split(",") if item]
|
|
80
|
+
capped = f"per_process_memory_fraction:{_rounded(share)}"
|
|
81
|
+
match = FRACTION.search(conf)
|
|
82
|
+
if match is None:
|
|
83
|
+
settings.append(capped)
|
|
84
|
+
elif float(match.group(1)) > share:
|
|
85
|
+
settings = [capped if FRACTION.fullmatch(item) else item for item in settings]
|
|
86
|
+
if not any(item.startswith("expandable_segments:") for item in settings):
|
|
87
|
+
settings.append("expandable_segments:True")
|
|
88
|
+
return ",".join(settings)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def torch_version() -> tuple[int, int] | None:
|
|
92
|
+
"""The major and minor version of the installed PyTorch, or None if it is not installed."""
|
|
93
|
+
try:
|
|
94
|
+
version = metadata.version("torch")
|
|
95
|
+
except metadata.PackageNotFoundError:
|
|
96
|
+
return None
|
|
97
|
+
major, minor = re.match(r"(\d+)\.(\d+)", version).groups()
|
|
98
|
+
return int(major), int(minor)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cap(environ: MutableMapping[str, str] = os.environ) -> float | None:
|
|
102
|
+
"""Cap the CUDA memory of PyTorch at the shards of the job and return the share, or None outside a job holding shards."""
|
|
103
|
+
share = fraction(environ)
|
|
104
|
+
if share is None:
|
|
105
|
+
return None
|
|
106
|
+
version = torch_version()
|
|
107
|
+
if version is None or version < OLDEST_TORCH:
|
|
108
|
+
raise RuntimeError(
|
|
109
|
+
"shardguard: the cap needs PyTorch 2.10 or later, whose allocator configuration takes per_process_memory_fraction"
|
|
110
|
+
)
|
|
111
|
+
torch = sys.modules.get("torch")
|
|
112
|
+
if torch is not None and torch.cuda.is_initialized():
|
|
113
|
+
raise RuntimeError(
|
|
114
|
+
"shardguard: import it before CUDA is initialised, since PyTorch ignores the cap after that"
|
|
115
|
+
)
|
|
116
|
+
current = next((environ[name] for name in ALLOC_CONF if environ.get(name)), "")
|
|
117
|
+
if "cudaMallocAsync" in current:
|
|
118
|
+
raise RuntimeError(
|
|
119
|
+
"shardguard: the cudaMallocAsync backend ignores per_process_memory_fraction"
|
|
120
|
+
)
|
|
121
|
+
capped = with_cap(current, share)
|
|
122
|
+
for name in ALLOC_CONF:
|
|
123
|
+
environ[name] = capped
|
|
124
|
+
print(
|
|
125
|
+
f"shardguard: CUDA memory capped at {_rounded(share)} of a GPU, the share of {environ['SLURM_SHARDS_ON_NODE']} of its {environ[SHARDS_PER_GPU]} shards",
|
|
126
|
+
file=sys.stderr,
|
|
127
|
+
)
|
|
128
|
+
return share
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
cap()
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import types
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
import shardguard
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture(autouse=True)
|
|
10
|
+
def torch_2_14(monkeypatch):
|
|
11
|
+
monkeypatch.setattr(shardguard, "torch_version", lambda: (2, 14))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.parametrize(
|
|
15
|
+
"gres, expected",
|
|
16
|
+
[
|
|
17
|
+
("gpu:A10:4,shard:A10:96", 24),
|
|
18
|
+
("gpu:l40s:2(S:0-1),shard:l40s:96(S:0-1)", 48),
|
|
19
|
+
("gpu:4", None),
|
|
20
|
+
("(null)", None),
|
|
21
|
+
],
|
|
22
|
+
)
|
|
23
|
+
def test_shards_per_gpu_is_read_off_the_gres(gres, expected):
|
|
24
|
+
assert shardguard.shards_per_gpu(gres) == expected
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_outside_a_shard_job_nothing_is_capped():
|
|
28
|
+
environ = {"SLURM_JOB_ID": "1"}
|
|
29
|
+
assert shardguard.cap(environ) is None
|
|
30
|
+
assert environ == {"SLURM_JOB_ID": "1"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_the_cap_is_the_jobs_share_of_a_gpu():
|
|
34
|
+
environ = {"SLURM_SHARDS_ON_NODE": "6", shardguard.SHARDS_PER_GPU: "24"}
|
|
35
|
+
assert shardguard.cap(environ) == 0.25
|
|
36
|
+
for name in shardguard.ALLOC_CONF:
|
|
37
|
+
assert (
|
|
38
|
+
environ[name] == "per_process_memory_fraction:0.25,expandable_segments:True"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_the_shards_per_gpu_come_from_the_node_once(monkeypatch):
|
|
43
|
+
calls = []
|
|
44
|
+
monkeypatch.setattr(
|
|
45
|
+
shardguard,
|
|
46
|
+
"node_gres",
|
|
47
|
+
lambda node: calls.append(node) or "gpu:A10:4,shard:A10:96",
|
|
48
|
+
)
|
|
49
|
+
environ = {"SLURM_SHARDS_ON_NODE": "12", "SLURMD_NODENAME": "cn-1"}
|
|
50
|
+
assert shardguard.fraction(environ) == 0.5
|
|
51
|
+
assert shardguard.fraction(environ) == 0.5
|
|
52
|
+
assert calls == ["cn-1"]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.mark.parametrize("gres", ["gpu:A10:4", OSError, KeyError])
|
|
56
|
+
def test_a_node_that_cannot_say_its_shards_is_refused(monkeypatch, gres):
|
|
57
|
+
def node_gres(node):
|
|
58
|
+
if isinstance(gres, str):
|
|
59
|
+
return gres
|
|
60
|
+
raise gres
|
|
61
|
+
|
|
62
|
+
monkeypatch.setattr(shardguard, "node_gres", node_gres)
|
|
63
|
+
with pytest.raises(RuntimeError, match=shardguard.SHARDS_PER_GPU):
|
|
64
|
+
shardguard.fraction({"SLURM_SHARDS_ON_NODE": "4", "SLURMD_NODENAME": "cn-1"})
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_shards_on_several_gpus_are_refused():
|
|
68
|
+
with pytest.raises(RuntimeError, match="several GPUs"):
|
|
69
|
+
shardguard.fraction(
|
|
70
|
+
{"SLURM_SHARDS_ON_NODE": "4", "CUDA_VISIBLE_DEVICES": "0,1"}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def settings(conf):
|
|
75
|
+
return dict(item.split(":", 1) for item in conf.split(","))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_existing_settings_are_kept_and_a_lower_cap_wins():
|
|
79
|
+
assert settings(shardguard.with_cap("expandable_segments:False", 0.5)) == {
|
|
80
|
+
"expandable_segments": "False",
|
|
81
|
+
"per_process_memory_fraction": "0.5",
|
|
82
|
+
}
|
|
83
|
+
assert settings(shardguard.with_cap("per_process_memory_fraction:0.1", 0.5)) == {
|
|
84
|
+
"expandable_segments": "True",
|
|
85
|
+
"per_process_memory_fraction": "0.1",
|
|
86
|
+
}
|
|
87
|
+
assert (
|
|
88
|
+
settings(shardguard.with_cap("per_process_memory_fraction:0.9", 0.5))[
|
|
89
|
+
"per_process_memory_fraction"
|
|
90
|
+
]
|
|
91
|
+
== "0.5"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_the_cap_is_rounded_down_and_other_settings_are_left_alone():
|
|
96
|
+
conf = shardguard.with_cap("roundup_power2_divisions:[256:1,512:2]", 1 / 6)
|
|
97
|
+
assert (
|
|
98
|
+
conf
|
|
99
|
+
== "roundup_power2_divisions:[256:1,512:2],per_process_memory_fraction:0.1666,expandable_segments:True"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_old_or_missing_pytorch_is_refused(monkeypatch):
|
|
104
|
+
for version in ((2, 9), None):
|
|
105
|
+
monkeypatch.setattr(shardguard, "torch_version", lambda: version)
|
|
106
|
+
with pytest.raises(RuntimeError, match="2.10"):
|
|
107
|
+
shardguard.cap(
|
|
108
|
+
{"SLURM_SHARDS_ON_NODE": "4", shardguard.SHARDS_PER_GPU: "24"}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_the_async_allocator_is_refused():
|
|
113
|
+
with pytest.raises(RuntimeError, match="cudaMallocAsync"):
|
|
114
|
+
shardguard.cap(
|
|
115
|
+
{
|
|
116
|
+
"SLURM_SHARDS_ON_NODE": "4",
|
|
117
|
+
shardguard.SHARDS_PER_GPU: "24",
|
|
118
|
+
"PYTORCH_ALLOC_CONF": "backend:cudaMallocAsync",
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_importing_after_cuda_is_initialised_is_refused(monkeypatch):
|
|
124
|
+
torch = types.SimpleNamespace(
|
|
125
|
+
cuda=types.SimpleNamespace(is_initialized=lambda: True)
|
|
126
|
+
)
|
|
127
|
+
monkeypatch.setitem(sys.modules, "torch", torch)
|
|
128
|
+
with pytest.raises(RuntimeError, match="before CUDA"):
|
|
129
|
+
shardguard.cap({"SLURM_SHARDS_ON_NODE": "4", shardguard.SHARDS_PER_GPU: "24"})
|