Skip to content

Commit cd34297

Browse files
kiukchungfacebook-github-bot
authored andcommitted
Create torchx.specs.overlays module (#1171)
Summary: Create `torchx.specs.overlays` module to standardize how scheduler request overlays work. Currently overlays are JSON (`dict[str, object]`) that follows the same structure as the scheduler's request type. Overlays are stored as metadata in two places: 1. `AppDef.metadata`: job level overlays 2. `Role.metadata`: gang-level overlays Overlays follow the rules below: 1. value types must be the same as the scheduler's request type at the same attribute. 2. dicts get upserted 3. nested dicts get recursively overlayed 4. lists get appended, no deduplication attempts are made 5. lists are NOT recursed, that is, lists for which elements are containers are not recursively overlayed, they are simply treated as primitives. 6. primitive attributes are replaced Differential Revision: D88545380
1 parent 2b2af7c commit cd34297

File tree

3 files changed

+177
-1
lines changed

3 files changed

+177
-1
lines changed

docs/source/specs.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ Macros
4646
.. autoclass:: macros
4747
:members:
4848

49-
5049
Run Configs
5150
--------------
5251
.. autoclass:: runopts
@@ -78,6 +77,11 @@ Mounts
7877
.. autoclass:: DeviceMount
7978
:members:
8079

80+
Overlays
81+
------------
82+
83+
.. automodule:: torchx.specs.overlays
84+
:members:
8185

8286
Component Linter
8387
-----------------

torchx/specs/overlays.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
# pyre-strict
8+
9+
"""
10+
Overlays are JSON structs applied to :py:class:`~torchx.specs.AppDef` and :py:class:`~torchx.specs.Role`
11+
to specify attributes of the scheduler's submit-job request that are not currently representable
12+
as attributes of :py:class:`~torchx.specs.AppDef` and :py:class:`~torchx.specs.Role`.
13+
14+
For end-uses, here are a few use-cases of overlays:
15+
16+
1. A new version of the scheduler has concepts/features that have not yet been added to TorchX.
17+
2. A bespoke internal scheduler has custom features that do not generalize hence not in TorchX.
18+
3. Re-using a pre-built ``AppDef`` but need to make a small change to the resulting scheduler request.
19+
20+
And for scheduler authors:
21+
22+
1. Scheduler setting needs to be applied to a ``Role``, which makes it hard to add as ``runopts``
23+
since ``runopts`` apply at the ``AppDef`` level.
24+
2. Scheduler setting cannot be represented naturally as the types supported by ``runopts``.
25+
3. Exposing the setting as a ``runopts`` obfuscates things.
26+
27+
See :py:func:`~torchx.specs.overlays.apply_overlay` for rules on how overlays are applied.
28+
"""
29+
30+
from typing import Any
31+
32+
Json = dict[str, Any]
33+
34+
35+
def apply_overlay(base: Json, overlay: Json) -> None:
36+
"""Applies ``overlay`` on ``base``.
37+
38+
.. note:: this function mutates the ``base``!
39+
40+
Overlays follow these rules:
41+
42+
1. Dicts, upsert key, value in base with the ones in overlay.
43+
2. Nested dicts, overlay recursively.
44+
3. Lists, append the overlay values to the base values.
45+
3. Nested lists DO NOT append recursively.
46+
4. Primitives (bool, str, int, float), replace base with the value in overlay.
47+
48+
.. doctest::
49+
from torchx.specs.overlays import apply_overlay
50+
51+
base = {
52+
"scheduler": {"policy": "default"},
53+
"resources": {"limits": {"cpu": "500m"}},
54+
"tolerations": [{"key": "gpu"}],
55+
"nodeSelectorTerms": [
56+
[{"matchExpressions": []}]
57+
],
58+
"maxPods": 110,
59+
}
60+
overlay = {
61+
"scheduler": {"policy": "binpacking"},
62+
"resources": {"limits": {"memory": "1Gi"}},
63+
"tolerations": [{"key": "spot"}],
64+
"nodeSelectorTerms": [
65+
[{"matchExpressions": [{"key": "disk"}]}]
66+
],
67+
"maxPods": 250,
68+
}
69+
70+
apply_overlay(base, overlay)
71+
72+
assert {
73+
"scheduler": {"policy": "binpacking"},
74+
"resources": {"limits": {"cpu": "500m", "memory": "1Gi"}},
75+
"tolerations": [{"key": "gpu"}, {"key": "spot"}],
76+
"nodeSelectorTerms": [
77+
[{"matchExpressions": []}],
78+
[{"matchExpressions": [{"key": "disk"}]}],
79+
],
80+
"maxPods": 250,
81+
} == base
82+
83+
"""
84+
85+
def assert_type_equal(key: str, o1: object, o2: object) -> None:
86+
o1_type = type(o1)
87+
o2_type = type(o2)
88+
assert (
89+
o1_type == o2_type
90+
), f"Type mismatch for attr: `{key}`. {o1_type.__qualname__} != {o2_type.__qualname__}"
91+
92+
for key, overlay_value in overlay.items():
93+
if key in base:
94+
base_value = base[key]
95+
96+
assert_type_equal(key, base_value, overlay_value)
97+
98+
if isinstance(base_value, dict) and isinstance(overlay_value, dict):
99+
apply_overlay(base_value, overlay_value)
100+
elif isinstance(base_value, list) and isinstance(overlay_value, list):
101+
base_value.extend(overlay_value)
102+
else:
103+
base[key] = overlay_value
104+
else:
105+
base[key] = overlay_value

torchx/specs/test/overlays_test.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
# pyre-strict
8+
import unittest
9+
10+
from torchx.specs.overlays import apply_overlay
11+
12+
13+
class OverlaysTest(unittest.TestCase):
14+
def test_apply_overlay_empty_base(self) -> None:
15+
base = {}
16+
overlay = {
17+
"a0": {"b": "c"},
18+
"a1": ["b", "c"],
19+
"a2": "b",
20+
}
21+
apply_overlay(base, overlay)
22+
self.assertDictEqual(base, overlay)
23+
24+
def test_apply_overlay_dict_attr(self) -> None:
25+
base = {"a0": {"d": "e"}}
26+
overlay = {"a0": {"b": "c"}}
27+
28+
apply_overlay(base, overlay)
29+
self.assertDictEqual(
30+
base,
31+
{
32+
"a0": {
33+
"b": "c",
34+
"d": "e",
35+
},
36+
},
37+
)
38+
39+
base = {"a0": {"b": "d"}}
40+
apply_overlay(base, overlay)
41+
self.assertDictEqual(base, {"a0": {"b": "c"}})
42+
43+
def test_apply_overlay_list_attr(self) -> None:
44+
base = {"a0": []}
45+
overlay = {"a0": ["b", "c"]}
46+
47+
apply_overlay(base, overlay)
48+
self.assertDictEqual(base, {"a0": ["b", "c"]})
49+
50+
base = {"a0": ["1", "b", "2"]}
51+
apply_overlay(base, overlay)
52+
# lists simply append - they do not dedup
53+
self.assertDictEqual(base, {"a0": ["1", "b", "2", "b", "c"]})
54+
55+
base = {"a0": ["1", ["2", "3"]]}
56+
overlay = {"a0": ["b", ["c", "d"]]}
57+
apply_overlay(base, overlay)
58+
# lists simply append - they do recusively apply
59+
self.assertDictEqual(base, {"a0": ["1", ["2", "3"], "b", ["c", "d"]]})
60+
61+
def test_overlay_type_mismatch(self) -> None:
62+
63+
with self.assertRaises(AssertionError):
64+
apply_overlay({"a": [1, 2]}, {"a": "b"})
65+
66+
with self.assertRaises(AssertionError):
67+
apply_overlay({"a": {"b": 1}}, {"a": {"b": "c"}})

0 commit comments

Comments
 (0)