Skip to content

Commit 4b12ebf

Browse files
committed
Merge branch 'master' of github.com:experimaestro/experimaestro-python
2 parents 98e14cb + 06fdf7a commit 4b12ebf

File tree

9 files changed

+168
-58
lines changed

9 files changed

+168
-58
lines changed

.github/workflows/pytest.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ jobs:
2424
python-version: ${{ matrix.python-version }}
2525
- name: Install dependencies
2626
run: |
27-
python -m pip install --upgrade pip
27+
# 1. Fresh tooling
28+
python -m pip install -U pip setuptools wheel
29+
# 2. Install pytest etc.
2830
pip install flake8 pytest pytest-timeout
29-
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
31+
# 3. Install our package
3032
TEST_WORKFLOW=ON pip install .
3133
- name: Lint with flake8
3234
if: false

.readthedocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ version: 2
88
build:
99
os: ubuntu-20.04
1010
tools:
11-
python: "3.9"
11+
python: "3.10"
1212

1313
# Build documentation with MkDocs
1414
mkdocs:

docs/faq.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,33 @@ task.instance(context).execute()
2424
```
2525

2626
The main problem with this approach is that resources are shared between experimaestro and the task
27+
28+
### How to Debug a failed task ?
29+
If a task failed, you can rerun it with [debugpy](https://github.com/microsoft/debugpy).
30+
31+
#### Using vsCode
32+
If the task is already generated, you can run it with the [python debugger](https://code.visualstudio.com/docs/python/debugging) directly within vsCode.
33+
- open the task python file `.../HASHID/task_name.py`.
34+
- Run the dubugger Using the following configuration:
35+
36+
In `.vscode/launch.json` :
37+
```json5
38+
{
39+
"name": "Python: XPM Task",
40+
"type": "debugpy",
41+
"request": "launch",
42+
"module": "experimaestro",
43+
"console": "integratedTerminal",
44+
"justMyCode": false,
45+
"args": [
46+
"run",
47+
"params.json"
48+
],
49+
// "python": "${workspaceFolder}/.venv/bin/python",
50+
"cwd": "${fileDirname}",
51+
"env": {
52+
"CUDA_VISIBLE_DEVICES": "1",
53+
}
54+
}
55+
```
56+
- NOTE: if the task needs GPU support, you may need to open VS-Code on a node with access to a GPU.

docs/launchers/index.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,23 @@ The `launchers.py` file dictates how a given *requirement* (e.g., 2 CPU with
4949
::: experimaestro.launcherfinder.specs.CudaSpecification
5050
::: experimaestro.launcherfinder.specs.CPUSpecification
5151

52+
#### Parsing requirements
53+
54+
::: experimaestro.launcherfinder.parser.parse
55+
56+
```py3
57+
from experimaestro.launcherfinder.parse
58+
59+
req = parse("""duration=40h & cpu(mem=700GiB) & cuda(mem=32GiB) * 8 | duration=50h & cpu(mem=700GiB) & cuda(mem=32GiB) * 4""")
60+
```
61+
62+
Requirements can be manipulated:
63+
64+
- duration can be multiplied by a given coefficient using
65+
`req.multiply_duration`. For instance, `req.multiply_duration(2)` multiplies
66+
all the duration by 2.
67+
68+
5269

5370
### Example
5471

pyproject.toml

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
[tool.poetry]
1+
[project]
22
name = "experimaestro"
3-
authors = ["Benjamin Piwowarski <benjamin@piwowarski.fr>"]
3+
authors = [
4+
{name = "Benjamin Piwowarski", email = "benjamin@piwowarski.fr"}
5+
]
46
description = '"Experimaestro is a computer science experiment manager"'
57
readme = "README.md"
68
license = "GPL-3"
@@ -14,15 +16,38 @@ classifiers = [
1416
"Programming Language :: Python :: 3",
1517
"Topic :: Software Development :: Libraries :: Python Modules",
1618
]
17-
include = [
18-
"src/experimaestro/server/data/*",
19-
"src/experimaestro/sphinx/static/experimaestro.css",
20-
"src/experimaestro/mkdocs/style.css",
21-
{ path="src/experimaestro/server/data/*", format=['sdist', 'wheel']}
22-
]
23-
version = "0.0.0"
2419
repository = "https://github.com/experimaestro/experimaestro-python"
2520
documentation = "https://experimaestro-python.readthedocs.io/"
21+
dynamic = ["version"]
22+
requires-python = ">=3.10"
23+
dependencies = [
24+
"arpeggio >=2,<3",
25+
"attrs >=23.1.0,<24",
26+
"click >=8",
27+
"decorator >=5,<6",
28+
"docstring-parser >=0.15,<1",
29+
"fasteners >=0.19,<1",
30+
"flask >=2.3,<3",
31+
"flask-socketio >=5.3,<6",
32+
"gevent >=25",
33+
"gevent-websocket >=0.10",
34+
"humanfriendly >=10",
35+
"huggingface-hub >0.17",
36+
"marshmallow >=3.20,<4",
37+
"mkdocs >=1.5,<2",
38+
"omegaconf >=2.3,<3",
39+
"psutil >=7,<8",
40+
"pyparsing >=3.1,<4",
41+
"pytools >=2023.1.1,<2024",
42+
"pyyaml >=6.0.1,<7",
43+
"requests >=2.31,<3",
44+
"rpyc >=5,<7",
45+
"sortedcontainers >=2.4,<3",
46+
"termcolor >=2.3,<3",
47+
"tqdm >=4.66.1,<5",
48+
"typing-extensions >=4.2; python_version < \"3.12\"",
49+
"watchdog >=2"
50+
]
2651

2752
[tool.poetry-dynamic-versioning]
2853
enable = true
@@ -33,38 +58,31 @@ format-jinja = """{%- set pre = [] -%}{%- set metadata = [] -%}
3358
{%- if revision is not none -%}{{ pre.append("rc" + revision|string) or "" }}{%- endif -%}
3459
{%- if distance > 0 -%}{{ metadata.append(distance|string) or "" }}{%- endif -%}
3560
{{ serialize_semver(base, pre, metadata)}}"""
61+
62+
[tool.poetry]
63+
include = [
64+
"src/experimaestro/server/data/*",
65+
"src/experimaestro/sphinx/static/experimaestro.css",
66+
"src/experimaestro/mkdocs/style.css",
67+
{ path="src/experimaestro/server/data/*", format=['sdist', 'wheel']}
68+
]
69+
version = "0.0.0"
70+
71+
[tool.poetry-dynamic-versioning.files."src/experimaestro/version.py"]
72+
persistent-substitution = true
73+
initial-content = """
74+
# These version placeholders will be replaced later during substitution.
75+
__version__ = "0.0.0"
76+
__version_tuple__ = (0, 0, 0)
77+
"""
78+
79+
[tool.poetry.requires-plugins]
80+
poetry-dynamic-versioning = { version = ">=1.0.0,<2.0.0", extras = ["plugin"] }
81+
3682
[build-system]
3783
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
3884
build-backend = "poetry_dynamic_versioning.backend"
3985

40-
[tool.poetry.dependencies]
41-
python = "^3.9"
42-
click = ">=8"
43-
omegaconf = "^2.3"
44-
typing-extensions = {version = ">=4.2", markers = "python_version < \"3.12\""}
45-
attrs = "^23.1.0"
46-
fasteners = "^0.19"
47-
pyyaml = "^6.0.1"
48-
psutil = ">=7"
49-
pytools = "^2023.1.1"
50-
tqdm = "^4.66.1"
51-
docstring-parser = "^0.15"
52-
termcolor = ">=2.3"
53-
requests = "^2.31"
54-
sortedcontainers = "^2.4"
55-
pyparsing = "^3.1"
56-
humanfriendly = "^10"
57-
huggingface-hub = ">0.17"
58-
gevent = "^24.11.1"
59-
gevent-websocket = "^0.10"
60-
flask = "^2.3"
61-
flask-socketio = "^5.3"
62-
arpeggio = "^2"
63-
watchdog = "^2"
64-
marshmallow = "^3.20"
65-
decorator = "^5"
66-
rpyc = ">=5,<7"
67-
6886
[tool.poetry.group.ssh]
6987
optional = true
7088

@@ -79,21 +97,21 @@ optional = true
7997
docutils = "^0.18"
8098
Pygments = "^2.15"
8199

82-
[tool.poetry.scripts]
100+
[project.scripts]
83101
experimaestro = "experimaestro.__main__:main"
84102

85-
[tool.poetry.plugins."mkdocs.plugins"]
103+
[project.entry-points."mkdocs.plugins"]
86104
experimaestro = "experimaestro.mkdocs:Documentation"
87105

88-
[tool.poetry.plugins."experimaestro.process"]
106+
[project.entry-points."experimaestro.process"]
89107
local = "experimaestro.connectors.local:LocalProcess"
90108
slurm = "experimaestro.launchers.slurm:BatchSlurmProcess"
91109

92-
[tool.poetry.plugins."experimaestro.connectors"]
110+
[project.entry-points."experimaestro.connectors"]
93111
local = "experimaestro.connectors.local:LocalConnector"
94112
ssh = "experimaestro.connectors.ssh:SshConnector"
95113

96-
[tool.poetry.plugins."experimaestro.tokens"]
114+
[project.entry-points."experimaestro.tokens"]
97115
unix = "experimaestro.tokens:CounterToken"
98116

99117

@@ -122,7 +140,7 @@ warn_unused_ignores = true
122140

123141
[tool.commitizen]
124142
name = "cz_conventional_commits"
125-
version = "1.9.1"
143+
version = "1.11.0"
126144
changelog_start_rev = "v1.0.0"
127145
tag_format = "v$major.$minor.$patch$prerelease"
128146
# update_changelog_on_bump = true

src/experimaestro/launcherfinder/parser.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ def cpu():
5151

5252

5353
def duration():
54-
return "duration", "=", RegExMatch(r"\d+"), RegExMatch(r"h(ours)?|d(ays)?")
54+
return (
55+
"duration",
56+
"=",
57+
RegExMatch(r"\d+"),
58+
RegExMatch(r"h(ours?)?|d(ays?)?|m(ins?)?"),
59+
)
5560

5661

5762
def one_spec():
@@ -67,7 +72,7 @@ def grammar():
6772

6873
class Visitor(PTNodeVisitor):
6974
def visit_grammar(self, node, children):
70-
return [child for child in children]
75+
return specs.RequirementUnion(*[child for child in children])
7176

7277
def visit_one_spec(self, node, children):
7378
return reduce(lambda x, el: x & el, children)

src/experimaestro/launcherfinder/registry.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import pkg_resources
1010
from experimaestro.utils import logger
1111
from .base import ConnectorConfiguration, TokenConfiguration
12-
from .specs import HostRequirement
12+
from .specs import HostRequirement, RequirementUnion
1313

1414
if typing.TYPE_CHECKING:
1515
from experimaestro.launchers import Launcher
@@ -150,18 +150,18 @@ def find(
150150
# Parse specs
151151
from .parser import parse
152152

153-
specs = []
153+
specs = RequirementUnion()
154154
for spec in input_specs:
155155
if isinstance(spec, str):
156-
specs.extend(parse(spec))
156+
specs.add(parse(spec))
157157
else:
158-
specs.append(spec)
158+
specs.add(spec)
159159

160160
# Use launcher function
161161
from experimaestro.launchers import Launcher
162162

163163
if self.find_launcher_fn is not None:
164-
for spec in specs:
164+
for spec in specs.requirements:
165165
if launcher := self.find_launcher_fn(spec, tags):
166166
assert isinstance(
167167
launcher, Launcher

src/experimaestro/launcherfinder/specs.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
from abc import ABC, abstractmethod
12
import logging
3+
import math
24
from attr import Factory
35
from attrs import define
46
from copy import copy, deepcopy
@@ -95,7 +97,7 @@ class MatchRequirement:
9597
requirement: "HostSimpleRequirement"
9698

9799

98-
class HostRequirement:
100+
class HostRequirement(ABC):
99101
"""A requirement must be a disjunction of host requirements"""
100102

101103
requirements: List["HostSimpleRequirement"]
@@ -110,6 +112,12 @@ def __or__(self, other: "HostRequirement"):
110112
def match(self, host: HostSpecification) -> Optional[MatchRequirement]:
111113
raise NotImplementedError()
112114

115+
@abstractmethod
116+
def multiply_duration(self, coefficient: float) -> "HostRequirement":
117+
"""Returns a new HostRequirement with a duration multiplied by the
118+
provided coefficient"""
119+
...
120+
113121

114122
class RequirementUnion(HostRequirement):
115123
"""Ordered list of simple host requirements -- the first one is the priority"""
@@ -119,6 +127,16 @@ class RequirementUnion(HostRequirement):
119127
def __init__(self, *requirements: "HostSimpleRequirement"):
120128
self.requirements = list(requirements)
121129

130+
def add(self, requirement: "HostRequirement"):
131+
match requirement:
132+
case HostSimpleRequirement():
133+
self.requirements.extend(*requirement.requirements)
134+
case RequirementUnion():
135+
self.requirements.append(requirement)
136+
case _:
137+
raise RuntimeError("Cannot handle type %s", type(requirement))
138+
return self
139+
122140
def match(self, host: HostSpecification) -> Optional[MatchRequirement]:
123141
"""Returns the matched requirement (if any)"""
124142

@@ -133,6 +151,14 @@ def match(self, host: HostSpecification) -> Optional[MatchRequirement]:
133151

134152
return argmax
135153

154+
def multiply_duration(self, coefficient: float) -> "RequirementUnion":
155+
return RequirementUnion(
156+
*[r.multiply_duration(coefficient) for r in self.requirements]
157+
)
158+
159+
def __repr__(self):
160+
return " | ".join(repr(r) for r in self.requirements)
161+
136162

137163
class HostSimpleRequirement(HostRequirement):
138164
"""Simple host requirement"""
@@ -149,6 +175,11 @@ class HostSimpleRequirement(HostRequirement):
149175
def __repr__(self):
150176
return f"Req(cpu={self.cpu}, cuda={self.cuda_gpus}, duration={self.duration})"
151177

178+
def multiply_duration(self, coefficient: float) -> "HostSimpleRequirement":
179+
r = HostSimpleRequirement(self)
180+
r.duration = math.ceil(self.duration * coefficient)
181+
return r
182+
152183
def __init__(self, *reqs: "HostSimpleRequirement"):
153184
self.cuda_gpus = []
154185
self.cpu = CPUSpecification(0, 0)

0 commit comments

Comments
 (0)