span-contract 1.0.1__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.
- span_contract-1.0.1/LICENSE +21 -0
- span_contract-1.0.1/PKG-INFO +165 -0
- span_contract-1.0.1/README.md +143 -0
- span_contract-1.0.1/pyproject.toml +46 -0
- span_contract-1.0.1/setup.cfg +4 -0
- span_contract-1.0.1/src/span_contract.egg-info/PKG-INFO +165 -0
- span_contract-1.0.1/src/span_contract.egg-info/SOURCES.txt +22 -0
- span_contract-1.0.1/src/span_contract.egg-info/dependency_links.txt +1 -0
- span_contract-1.0.1/src/span_contract.egg-info/entry_points.txt +2 -0
- span_contract-1.0.1/src/span_contract.egg-info/requires.txt +3 -0
- span_contract-1.0.1/src/span_contract.egg-info/top_level.txt +1 -0
- span_contract-1.0.1/src/spancontract/__init__.py +64 -0
- span_contract-1.0.1/src/spancontract/adapters/__init__.py +29 -0
- span_contract-1.0.1/src/spancontract/adapters/delay_node.py +217 -0
- span_contract-1.0.1/src/spancontract/cli.py +167 -0
- span_contract-1.0.1/src/spancontract/compile_cache.py +103 -0
- span_contract-1.0.1/src/spancontract/decisions.py +87 -0
- span_contract-1.0.1/src/spancontract/envelope.py +252 -0
- span_contract-1.0.1/src/spancontract/plant.py +146 -0
- span_contract-1.0.1/src/spancontract/rules.py +364 -0
- span_contract-1.0.1/src/spancontract/schema.py +120 -0
- span_contract-1.0.1/src/spancontract/validator.py +218 -0
- span_contract-1.0.1/tests/test_contract.py +485 -0
- span_contract-1.0.1/tests/test_mutations.py +202 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Margaret Nanyonga
|
|
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,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: span-contract
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: An admission contract for AI training jobs that cross a data hall boundary
|
|
5
|
+
Author: Margaret Nanyonga
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dimaggi-ai/span-contract
|
|
8
|
+
Project-URL: Research, https://dimaggi-ai.github.io/research
|
|
9
|
+
Keywords: distributed training,admission control,data center interconnect,optical circuit switching,capacity planning
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest==8.3.4; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# The span contract: deciding whether an AI training job may cross a data hall
|
|
24
|
+
|
|
25
|
+
**When a job asks to run across more than one hall, who decides, and on what facts?**
|
|
26
|
+
|
|
27
|
+
Today, three systems decide, and none of them talks to the others. The scheduler
|
|
28
|
+
knows how many accelerators are free. The compiler knows what plan it emitted and
|
|
29
|
+
what topology it assumed. The circuit controller knows what glass is lit. Each
|
|
30
|
+
holds a different half-truth, and the job discovers which one was wrong at step
|
|
31
|
+
time — or worse, at checkpoint time, three hours in.
|
|
32
|
+
|
|
33
|
+
This repository is one object that makes them agree before the job starts:
|
|
34
|
+
twenty-one fields, six decisions, three conditions under which the contract
|
|
35
|
+
refuses rather than guesses, and a reference validator that turns the first into
|
|
36
|
+
the second and prints its reasons. It is a research artifact, not a product.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## What it is not
|
|
41
|
+
|
|
42
|
+
It is not a fabric, a scheduler, or a network model. It decides **admission** —
|
|
43
|
+
may this job cross — and holds no opinion on **capacity** — what the job would
|
|
44
|
+
retain if it did. That second question is answered by the latency-regime atlas in
|
|
45
|
+
[network-vs-more-gpus](https://github.com/dimaggi-ai/network-vs-more-gpus), and
|
|
46
|
+
this repository deliberately does not offer a second answer to it. The two are
|
|
47
|
+
designed to be read together: the atlas says a tensor-parallel cut retains 0.004
|
|
48
|
+
across any stitch at any distance, and the contract is what refuses to launch it.
|
|
49
|
+
|
|
50
|
+
## The shape of it
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from spancontract import SpanEnvelope, validate
|
|
54
|
+
|
|
55
|
+
verdict = validate(envelope)
|
|
56
|
+
verdict.decision # Decision.DENY
|
|
57
|
+
verdict.reasons() # ['[FC2] path measurement is 7200s old, past the 300s TTL']
|
|
58
|
+
verdict.not_checked # what the validator could not check, printed rather than omitted
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
$ spancontract validate examples/long-haul-training-too-far.json
|
|
63
|
+
decision: DENY
|
|
64
|
+
regime: region (2958 us RTT)
|
|
65
|
+
|
|
66
|
+
findings:
|
|
67
|
+
- [LR1] synchronous train at 2958 us RTT (region) exceeds the 2000 us limit;
|
|
68
|
+
the default policy is that training stays hall-local
|
|
69
|
+
|
|
70
|
+
not checked:
|
|
71
|
+
? XP1 declared-vs-computed blast radius: no plant graph supplied, so the
|
|
72
|
+
envelope's blast_radius was taken on trust
|
|
73
|
+
? XP2 topology currency: no live topology hash supplied, so the envelope's
|
|
74
|
+
topology_hash was taken on trust
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## The three conditions that fail closed
|
|
78
|
+
|
|
79
|
+
Each describes a state in which the contract *does not know* something it needs,
|
|
80
|
+
and in each case not knowing is a refusal. A validator that read silence as
|
|
81
|
+
health would be worse than no validator, because it would be trusted.
|
|
82
|
+
|
|
83
|
+
| | Condition | Why it is a refusal and not a warning |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| **FC1** | The circuit API is dark | The state of the stitch is unknown. Unknown is not healthy. |
|
|
86
|
+
| **FC2** | The measured path is stale past its TTL | A declared topology is a claim. Only a fresh measurement is an observation. |
|
|
87
|
+
| **FC3** | The compile cache was keyed on a topology that no longer exists | The binary was built for a placement that is gone. |
|
|
88
|
+
|
|
89
|
+
## The six decisions
|
|
90
|
+
|
|
91
|
+
`local` · `span` · `shrink` · `move` · `escalate` · `deny`, on a severity ladder.
|
|
92
|
+
Rules join with a maximum, so a rule can only push a verdict toward refusal and
|
|
93
|
+
never away from it — which is what makes the order the rules run in irrelevant to
|
|
94
|
+
the answer. The registry proves that by shuffling the rule list over 400 random
|
|
95
|
+
envelopes and requiring every verdict to be identical.
|
|
96
|
+
|
|
97
|
+
## What the validator refuses to pretend
|
|
98
|
+
|
|
99
|
+
`make validate` prints the registry, and then prints the list of what it
|
|
100
|
+
**declined** to check. That second list is the more useful one. It says, among
|
|
101
|
+
other things, that no published figure fixes any threshold in this repository,
|
|
102
|
+
that no verdict here has been checked against a job that actually ran, and that
|
|
103
|
+
the emulated circuit models amplifier gain without the optical signal-to-noise
|
|
104
|
+
cost that comes with it, so it can demonstrate a refusal but cannot support a
|
|
105
|
+
claim of health.
|
|
106
|
+
|
|
107
|
+
There is exactly **one calibrated point** in the registry. That is the honest
|
|
108
|
+
count, not a gap. See [`docs/the-contract.md`](docs/the-contract.md).
|
|
109
|
+
|
|
110
|
+
## Install and run
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
git clone https://github.com/dimaggi-ai/span-contract
|
|
114
|
+
cd span-contract
|
|
115
|
+
make venv
|
|
116
|
+
make smoke-test # tests, mutation tests, registry, examples — under a minute
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Or from PyPI:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install span-contract
|
|
123
|
+
spancontract example > envelope.json
|
|
124
|
+
spancontract validate envelope.json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
| Target | What it does |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `make test` | 91 tests, including 12 mutation tests that delete machinery and require the registry to go red |
|
|
130
|
+
| `make validate` | the validation registry, and the nine things it declines to check |
|
|
131
|
+
| `make examples` | eleven example envelopes, each checked against its documented verdict |
|
|
132
|
+
| `make schema` | regenerate `schema/span_contract.schema.json` from the code |
|
|
133
|
+
|
|
134
|
+
## Repository map
|
|
135
|
+
|
|
136
|
+
| Path | What is in it |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `src/spancontract/envelope.py` | the twenty-one fields, inert by design |
|
|
139
|
+
| `src/spancontract/rules.py` | every rule and every threshold, none of them inline |
|
|
140
|
+
| `src/spancontract/validator.py` | the join, the plant cross-checks, the hash-chained record |
|
|
141
|
+
| `src/spancontract/plant.py` | halls, circuits, and what one failure actually costs |
|
|
142
|
+
| `src/spancontract/compile_cache.py` | keyed as specified; refuses entries from a dead topology |
|
|
143
|
+
| `src/spancontract/adapters/delay_node.py` | an emulated circuit, and the `tc` line that reproduces it |
|
|
144
|
+
| `validation/validate_contract.py` | the registry: one calibrated, seven emergent, ten sanity, nine declined |
|
|
145
|
+
| `tests/test_mutations.py` | delete a piece, name the points that must go red |
|
|
146
|
+
| `docs/the-contract.md` | the specification as implemented, including where it contradicts itself |
|
|
147
|
+
| `docs/integration.md` | the surface an existing policy engine would bind to |
|
|
148
|
+
|
|
149
|
+
## The discrepancy that was carried rather than fixed
|
|
150
|
+
|
|
151
|
+
The specification lists six decisions, and then enumerates only five of them in
|
|
152
|
+
the `span_mode` field: `escalate` is missing. Both are implemented exactly as
|
|
153
|
+
written — a verdict can be `escalate`, and `span_mode` cannot — and a registry
|
|
154
|
+
point asserts the gap is still there, so that quietly closing it in a future
|
|
155
|
+
commit shows up as a failing check rather than a tidy-up. `DECISIONS.md` D3 has
|
|
156
|
+
the reasoning.
|
|
157
|
+
|
|
158
|
+
## Series
|
|
159
|
+
|
|
160
|
+
Part of a program on the usable capacity of large accelerator fleets:
|
|
161
|
+
[dimaggi-ai.github.io/research](https://dimaggi-ai.github.io/research).
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT. Copyright (c) 2026 Margaret Nanyonga.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# The span contract: deciding whether an AI training job may cross a data hall
|
|
2
|
+
|
|
3
|
+
**When a job asks to run across more than one hall, who decides, and on what facts?**
|
|
4
|
+
|
|
5
|
+
Today, three systems decide, and none of them talks to the others. The scheduler
|
|
6
|
+
knows how many accelerators are free. The compiler knows what plan it emitted and
|
|
7
|
+
what topology it assumed. The circuit controller knows what glass is lit. Each
|
|
8
|
+
holds a different half-truth, and the job discovers which one was wrong at step
|
|
9
|
+
time — or worse, at checkpoint time, three hours in.
|
|
10
|
+
|
|
11
|
+
This repository is one object that makes them agree before the job starts:
|
|
12
|
+
twenty-one fields, six decisions, three conditions under which the contract
|
|
13
|
+
refuses rather than guesses, and a reference validator that turns the first into
|
|
14
|
+
the second and prints its reasons. It is a research artifact, not a product.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## What it is not
|
|
19
|
+
|
|
20
|
+
It is not a fabric, a scheduler, or a network model. It decides **admission** —
|
|
21
|
+
may this job cross — and holds no opinion on **capacity** — what the job would
|
|
22
|
+
retain if it did. That second question is answered by the latency-regime atlas in
|
|
23
|
+
[network-vs-more-gpus](https://github.com/dimaggi-ai/network-vs-more-gpus), and
|
|
24
|
+
this repository deliberately does not offer a second answer to it. The two are
|
|
25
|
+
designed to be read together: the atlas says a tensor-parallel cut retains 0.004
|
|
26
|
+
across any stitch at any distance, and the contract is what refuses to launch it.
|
|
27
|
+
|
|
28
|
+
## The shape of it
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from spancontract import SpanEnvelope, validate
|
|
32
|
+
|
|
33
|
+
verdict = validate(envelope)
|
|
34
|
+
verdict.decision # Decision.DENY
|
|
35
|
+
verdict.reasons() # ['[FC2] path measurement is 7200s old, past the 300s TTL']
|
|
36
|
+
verdict.not_checked # what the validator could not check, printed rather than omitted
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
$ spancontract validate examples/long-haul-training-too-far.json
|
|
41
|
+
decision: DENY
|
|
42
|
+
regime: region (2958 us RTT)
|
|
43
|
+
|
|
44
|
+
findings:
|
|
45
|
+
- [LR1] synchronous train at 2958 us RTT (region) exceeds the 2000 us limit;
|
|
46
|
+
the default policy is that training stays hall-local
|
|
47
|
+
|
|
48
|
+
not checked:
|
|
49
|
+
? XP1 declared-vs-computed blast radius: no plant graph supplied, so the
|
|
50
|
+
envelope's blast_radius was taken on trust
|
|
51
|
+
? XP2 topology currency: no live topology hash supplied, so the envelope's
|
|
52
|
+
topology_hash was taken on trust
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## The three conditions that fail closed
|
|
56
|
+
|
|
57
|
+
Each describes a state in which the contract *does not know* something it needs,
|
|
58
|
+
and in each case not knowing is a refusal. A validator that read silence as
|
|
59
|
+
health would be worse than no validator, because it would be trusted.
|
|
60
|
+
|
|
61
|
+
| | Condition | Why it is a refusal and not a warning |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| **FC1** | The circuit API is dark | The state of the stitch is unknown. Unknown is not healthy. |
|
|
64
|
+
| **FC2** | The measured path is stale past its TTL | A declared topology is a claim. Only a fresh measurement is an observation. |
|
|
65
|
+
| **FC3** | The compile cache was keyed on a topology that no longer exists | The binary was built for a placement that is gone. |
|
|
66
|
+
|
|
67
|
+
## The six decisions
|
|
68
|
+
|
|
69
|
+
`local` · `span` · `shrink` · `move` · `escalate` · `deny`, on a severity ladder.
|
|
70
|
+
Rules join with a maximum, so a rule can only push a verdict toward refusal and
|
|
71
|
+
never away from it — which is what makes the order the rules run in irrelevant to
|
|
72
|
+
the answer. The registry proves that by shuffling the rule list over 400 random
|
|
73
|
+
envelopes and requiring every verdict to be identical.
|
|
74
|
+
|
|
75
|
+
## What the validator refuses to pretend
|
|
76
|
+
|
|
77
|
+
`make validate` prints the registry, and then prints the list of what it
|
|
78
|
+
**declined** to check. That second list is the more useful one. It says, among
|
|
79
|
+
other things, that no published figure fixes any threshold in this repository,
|
|
80
|
+
that no verdict here has been checked against a job that actually ran, and that
|
|
81
|
+
the emulated circuit models amplifier gain without the optical signal-to-noise
|
|
82
|
+
cost that comes with it, so it can demonstrate a refusal but cannot support a
|
|
83
|
+
claim of health.
|
|
84
|
+
|
|
85
|
+
There is exactly **one calibrated point** in the registry. That is the honest
|
|
86
|
+
count, not a gap. See [`docs/the-contract.md`](docs/the-contract.md).
|
|
87
|
+
|
|
88
|
+
## Install and run
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
git clone https://github.com/dimaggi-ai/span-contract
|
|
92
|
+
cd span-contract
|
|
93
|
+
make venv
|
|
94
|
+
make smoke-test # tests, mutation tests, registry, examples — under a minute
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Or from PyPI:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
pip install span-contract
|
|
101
|
+
spancontract example > envelope.json
|
|
102
|
+
spancontract validate envelope.json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
| Target | What it does |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `make test` | 91 tests, including 12 mutation tests that delete machinery and require the registry to go red |
|
|
108
|
+
| `make validate` | the validation registry, and the nine things it declines to check |
|
|
109
|
+
| `make examples` | eleven example envelopes, each checked against its documented verdict |
|
|
110
|
+
| `make schema` | regenerate `schema/span_contract.schema.json` from the code |
|
|
111
|
+
|
|
112
|
+
## Repository map
|
|
113
|
+
|
|
114
|
+
| Path | What is in it |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `src/spancontract/envelope.py` | the twenty-one fields, inert by design |
|
|
117
|
+
| `src/spancontract/rules.py` | every rule and every threshold, none of them inline |
|
|
118
|
+
| `src/spancontract/validator.py` | the join, the plant cross-checks, the hash-chained record |
|
|
119
|
+
| `src/spancontract/plant.py` | halls, circuits, and what one failure actually costs |
|
|
120
|
+
| `src/spancontract/compile_cache.py` | keyed as specified; refuses entries from a dead topology |
|
|
121
|
+
| `src/spancontract/adapters/delay_node.py` | an emulated circuit, and the `tc` line that reproduces it |
|
|
122
|
+
| `validation/validate_contract.py` | the registry: one calibrated, seven emergent, ten sanity, nine declined |
|
|
123
|
+
| `tests/test_mutations.py` | delete a piece, name the points that must go red |
|
|
124
|
+
| `docs/the-contract.md` | the specification as implemented, including where it contradicts itself |
|
|
125
|
+
| `docs/integration.md` | the surface an existing policy engine would bind to |
|
|
126
|
+
|
|
127
|
+
## The discrepancy that was carried rather than fixed
|
|
128
|
+
|
|
129
|
+
The specification lists six decisions, and then enumerates only five of them in
|
|
130
|
+
the `span_mode` field: `escalate` is missing. Both are implemented exactly as
|
|
131
|
+
written — a verdict can be `escalate`, and `span_mode` cannot — and a registry
|
|
132
|
+
point asserts the gap is still there, so that quietly closing it in a future
|
|
133
|
+
commit shows up as a failing check rather than a tidy-up. `DECISIONS.md` D3 has
|
|
134
|
+
the reasoning.
|
|
135
|
+
|
|
136
|
+
## Series
|
|
137
|
+
|
|
138
|
+
Part of a program on the usable capacity of large accelerator fleets:
|
|
139
|
+
[dimaggi-ai.github.io/research](https://dimaggi-ai.github.io/research).
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT. Copyright (c) 2026 Margaret Nanyonga.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "span-contract"
|
|
7
|
+
version = "1.0.1"
|
|
8
|
+
description = "An admission contract for AI training jobs that cross a data hall boundary"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Margaret Nanyonga" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"distributed training",
|
|
15
|
+
"admission control",
|
|
16
|
+
"data center interconnect",
|
|
17
|
+
"optical circuit switching",
|
|
18
|
+
"capacity planning",
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Intended Audience :: Science/Research",
|
|
22
|
+
"Intended Audience :: System Administrators",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Scientific/Engineering",
|
|
26
|
+
"Topic :: System :: Distributed Computing",
|
|
27
|
+
]
|
|
28
|
+
dependencies = []
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = ["pytest==8.3.4"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://github.com/dimaggi-ai/span-contract"
|
|
35
|
+
Research = "https://dimaggi-ai.github.io/research"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
spancontract = "spancontract.cli:main"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
where = ["src"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
|
45
|
+
pythonpath = ["src", "validation"]
|
|
46
|
+
addopts = "-q"
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: span-contract
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: An admission contract for AI training jobs that cross a data hall boundary
|
|
5
|
+
Author: Margaret Nanyonga
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dimaggi-ai/span-contract
|
|
8
|
+
Project-URL: Research, https://dimaggi-ai.github.io/research
|
|
9
|
+
Keywords: distributed training,admission control,data center interconnect,optical circuit switching,capacity planning
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest==8.3.4; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# The span contract: deciding whether an AI training job may cross a data hall
|
|
24
|
+
|
|
25
|
+
**When a job asks to run across more than one hall, who decides, and on what facts?**
|
|
26
|
+
|
|
27
|
+
Today, three systems decide, and none of them talks to the others. The scheduler
|
|
28
|
+
knows how many accelerators are free. The compiler knows what plan it emitted and
|
|
29
|
+
what topology it assumed. The circuit controller knows what glass is lit. Each
|
|
30
|
+
holds a different half-truth, and the job discovers which one was wrong at step
|
|
31
|
+
time — or worse, at checkpoint time, three hours in.
|
|
32
|
+
|
|
33
|
+
This repository is one object that makes them agree before the job starts:
|
|
34
|
+
twenty-one fields, six decisions, three conditions under which the contract
|
|
35
|
+
refuses rather than guesses, and a reference validator that turns the first into
|
|
36
|
+
the second and prints its reasons. It is a research artifact, not a product.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## What it is not
|
|
41
|
+
|
|
42
|
+
It is not a fabric, a scheduler, or a network model. It decides **admission** —
|
|
43
|
+
may this job cross — and holds no opinion on **capacity** — what the job would
|
|
44
|
+
retain if it did. That second question is answered by the latency-regime atlas in
|
|
45
|
+
[network-vs-more-gpus](https://github.com/dimaggi-ai/network-vs-more-gpus), and
|
|
46
|
+
this repository deliberately does not offer a second answer to it. The two are
|
|
47
|
+
designed to be read together: the atlas says a tensor-parallel cut retains 0.004
|
|
48
|
+
across any stitch at any distance, and the contract is what refuses to launch it.
|
|
49
|
+
|
|
50
|
+
## The shape of it
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from spancontract import SpanEnvelope, validate
|
|
54
|
+
|
|
55
|
+
verdict = validate(envelope)
|
|
56
|
+
verdict.decision # Decision.DENY
|
|
57
|
+
verdict.reasons() # ['[FC2] path measurement is 7200s old, past the 300s TTL']
|
|
58
|
+
verdict.not_checked # what the validator could not check, printed rather than omitted
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
$ spancontract validate examples/long-haul-training-too-far.json
|
|
63
|
+
decision: DENY
|
|
64
|
+
regime: region (2958 us RTT)
|
|
65
|
+
|
|
66
|
+
findings:
|
|
67
|
+
- [LR1] synchronous train at 2958 us RTT (region) exceeds the 2000 us limit;
|
|
68
|
+
the default policy is that training stays hall-local
|
|
69
|
+
|
|
70
|
+
not checked:
|
|
71
|
+
? XP1 declared-vs-computed blast radius: no plant graph supplied, so the
|
|
72
|
+
envelope's blast_radius was taken on trust
|
|
73
|
+
? XP2 topology currency: no live topology hash supplied, so the envelope's
|
|
74
|
+
topology_hash was taken on trust
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## The three conditions that fail closed
|
|
78
|
+
|
|
79
|
+
Each describes a state in which the contract *does not know* something it needs,
|
|
80
|
+
and in each case not knowing is a refusal. A validator that read silence as
|
|
81
|
+
health would be worse than no validator, because it would be trusted.
|
|
82
|
+
|
|
83
|
+
| | Condition | Why it is a refusal and not a warning |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| **FC1** | The circuit API is dark | The state of the stitch is unknown. Unknown is not healthy. |
|
|
86
|
+
| **FC2** | The measured path is stale past its TTL | A declared topology is a claim. Only a fresh measurement is an observation. |
|
|
87
|
+
| **FC3** | The compile cache was keyed on a topology that no longer exists | The binary was built for a placement that is gone. |
|
|
88
|
+
|
|
89
|
+
## The six decisions
|
|
90
|
+
|
|
91
|
+
`local` · `span` · `shrink` · `move` · `escalate` · `deny`, on a severity ladder.
|
|
92
|
+
Rules join with a maximum, so a rule can only push a verdict toward refusal and
|
|
93
|
+
never away from it — which is what makes the order the rules run in irrelevant to
|
|
94
|
+
the answer. The registry proves that by shuffling the rule list over 400 random
|
|
95
|
+
envelopes and requiring every verdict to be identical.
|
|
96
|
+
|
|
97
|
+
## What the validator refuses to pretend
|
|
98
|
+
|
|
99
|
+
`make validate` prints the registry, and then prints the list of what it
|
|
100
|
+
**declined** to check. That second list is the more useful one. It says, among
|
|
101
|
+
other things, that no published figure fixes any threshold in this repository,
|
|
102
|
+
that no verdict here has been checked against a job that actually ran, and that
|
|
103
|
+
the emulated circuit models amplifier gain without the optical signal-to-noise
|
|
104
|
+
cost that comes with it, so it can demonstrate a refusal but cannot support a
|
|
105
|
+
claim of health.
|
|
106
|
+
|
|
107
|
+
There is exactly **one calibrated point** in the registry. That is the honest
|
|
108
|
+
count, not a gap. See [`docs/the-contract.md`](docs/the-contract.md).
|
|
109
|
+
|
|
110
|
+
## Install and run
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
git clone https://github.com/dimaggi-ai/span-contract
|
|
114
|
+
cd span-contract
|
|
115
|
+
make venv
|
|
116
|
+
make smoke-test # tests, mutation tests, registry, examples — under a minute
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Or from PyPI:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install span-contract
|
|
123
|
+
spancontract example > envelope.json
|
|
124
|
+
spancontract validate envelope.json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
| Target | What it does |
|
|
128
|
+
|---|---|
|
|
129
|
+
| `make test` | 91 tests, including 12 mutation tests that delete machinery and require the registry to go red |
|
|
130
|
+
| `make validate` | the validation registry, and the nine things it declines to check |
|
|
131
|
+
| `make examples` | eleven example envelopes, each checked against its documented verdict |
|
|
132
|
+
| `make schema` | regenerate `schema/span_contract.schema.json` from the code |
|
|
133
|
+
|
|
134
|
+
## Repository map
|
|
135
|
+
|
|
136
|
+
| Path | What is in it |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `src/spancontract/envelope.py` | the twenty-one fields, inert by design |
|
|
139
|
+
| `src/spancontract/rules.py` | every rule and every threshold, none of them inline |
|
|
140
|
+
| `src/spancontract/validator.py` | the join, the plant cross-checks, the hash-chained record |
|
|
141
|
+
| `src/spancontract/plant.py` | halls, circuits, and what one failure actually costs |
|
|
142
|
+
| `src/spancontract/compile_cache.py` | keyed as specified; refuses entries from a dead topology |
|
|
143
|
+
| `src/spancontract/adapters/delay_node.py` | an emulated circuit, and the `tc` line that reproduces it |
|
|
144
|
+
| `validation/validate_contract.py` | the registry: one calibrated, seven emergent, ten sanity, nine declined |
|
|
145
|
+
| `tests/test_mutations.py` | delete a piece, name the points that must go red |
|
|
146
|
+
| `docs/the-contract.md` | the specification as implemented, including where it contradicts itself |
|
|
147
|
+
| `docs/integration.md` | the surface an existing policy engine would bind to |
|
|
148
|
+
|
|
149
|
+
## The discrepancy that was carried rather than fixed
|
|
150
|
+
|
|
151
|
+
The specification lists six decisions, and then enumerates only five of them in
|
|
152
|
+
the `span_mode` field: `escalate` is missing. Both are implemented exactly as
|
|
153
|
+
written — a verdict can be `escalate`, and `span_mode` cannot — and a registry
|
|
154
|
+
point asserts the gap is still there, so that quietly closing it in a future
|
|
155
|
+
commit shows up as a failing check rather than a tidy-up. `DECISIONS.md` D3 has
|
|
156
|
+
the reasoning.
|
|
157
|
+
|
|
158
|
+
## Series
|
|
159
|
+
|
|
160
|
+
Part of a program on the usable capacity of large accelerator fleets:
|
|
161
|
+
[dimaggi-ai.github.io/research](https://dimaggi-ai.github.io/research).
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT. Copyright (c) 2026 Margaret Nanyonga.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/span_contract.egg-info/PKG-INFO
|
|
5
|
+
src/span_contract.egg-info/SOURCES.txt
|
|
6
|
+
src/span_contract.egg-info/dependency_links.txt
|
|
7
|
+
src/span_contract.egg-info/entry_points.txt
|
|
8
|
+
src/span_contract.egg-info/requires.txt
|
|
9
|
+
src/span_contract.egg-info/top_level.txt
|
|
10
|
+
src/spancontract/__init__.py
|
|
11
|
+
src/spancontract/cli.py
|
|
12
|
+
src/spancontract/compile_cache.py
|
|
13
|
+
src/spancontract/decisions.py
|
|
14
|
+
src/spancontract/envelope.py
|
|
15
|
+
src/spancontract/plant.py
|
|
16
|
+
src/spancontract/rules.py
|
|
17
|
+
src/spancontract/schema.py
|
|
18
|
+
src/spancontract/validator.py
|
|
19
|
+
src/spancontract/adapters/__init__.py
|
|
20
|
+
src/spancontract/adapters/delay_node.py
|
|
21
|
+
tests/test_contract.py
|
|
22
|
+
tests/test_mutations.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
spancontract
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""span-contract: one object a job carries when it wants to cross a hall.
|
|
2
|
+
|
|
3
|
+
A scale-across decision is made today by three systems that do not talk: a
|
|
4
|
+
scheduler that knows how many accelerators are free, a compiler that knows what
|
|
5
|
+
plan it emitted, and a circuit controller that knows what glass is lit. Each
|
|
6
|
+
holds a different half-truth about the topology, and the job finds out which
|
|
7
|
+
one was wrong at step time.
|
|
8
|
+
|
|
9
|
+
This package is the object that makes them agree before the job starts: a
|
|
10
|
+
twenty-one field envelope, six decisions, three conditions that fail closed,
|
|
11
|
+
and a reference validator that turns the first into the second and prints the
|
|
12
|
+
reasons. It decides admission. It does not decide capacity --- what a job
|
|
13
|
+
retains across a given cut is answered by the latency-regime atlas in the
|
|
14
|
+
``network-vs-more-gpus`` repository, and this package deliberately holds no
|
|
15
|
+
second opinion on it.
|
|
16
|
+
|
|
17
|
+
>>> from spancontract import SpanEnvelope, validate
|
|
18
|
+
>>> verdict = validate(envelope) # doctest: +SKIP
|
|
19
|
+
>>> verdict.decision, verdict.reasons() # doctest: +SKIP
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .plant import SpanGraph, Stitch, blast_radius, load_bearing_stitches
|
|
23
|
+
from .compile_cache import CompileCache, compile_cache_key
|
|
24
|
+
from .decisions import SPAN_MODES, Decision, ScaleOut
|
|
25
|
+
from .envelope import (
|
|
26
|
+
AUTONOMY_LEVELS,
|
|
27
|
+
REGIME_BOUNDS,
|
|
28
|
+
SPEC_FIELDS,
|
|
29
|
+
SliceRect,
|
|
30
|
+
SpanEnvelope,
|
|
31
|
+
latency_regime,
|
|
32
|
+
)
|
|
33
|
+
from .rules import FAIL_CLOSED_RULE_IDS, RULES, Finding, Policy
|
|
34
|
+
from .validator import Plant, Verdict, audit_record, validate, verify_chain
|
|
35
|
+
|
|
36
|
+
__version__ = "1.0.1"
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"AUTONOMY_LEVELS",
|
|
40
|
+
"CompileCache",
|
|
41
|
+
"Decision",
|
|
42
|
+
"FAIL_CLOSED_RULE_IDS",
|
|
43
|
+
"Finding",
|
|
44
|
+
"Plant",
|
|
45
|
+
"Policy",
|
|
46
|
+
"REGIME_BOUNDS",
|
|
47
|
+
"RULES",
|
|
48
|
+
"SPAN_MODES",
|
|
49
|
+
"SPEC_FIELDS",
|
|
50
|
+
"ScaleOut",
|
|
51
|
+
"SliceRect",
|
|
52
|
+
"SpanEnvelope",
|
|
53
|
+
"SpanGraph",
|
|
54
|
+
"Stitch",
|
|
55
|
+
"Verdict",
|
|
56
|
+
"audit_record",
|
|
57
|
+
"blast_radius",
|
|
58
|
+
"compile_cache_key",
|
|
59
|
+
"latency_regime",
|
|
60
|
+
"load_bearing_stitches",
|
|
61
|
+
"validate",
|
|
62
|
+
"verify_chain",
|
|
63
|
+
"__version__",
|
|
64
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Adapters between the contract and something that can answer for a circuit.
|
|
2
|
+
|
|
3
|
+
The contract needs one thing from the outside world: a measurement of the path,
|
|
4
|
+
with an age and an honest "I could not reach the controller". That is the whole
|
|
5
|
+
:class:`StitchController` surface. Everything else in this repository works off
|
|
6
|
+
the envelope.
|
|
7
|
+
|
|
8
|
+
Two adapters ship here. :mod:`spancontract.adapters.delay_node` is an emulated
|
|
9
|
+
circuit, which is what section 9's seventh sequence item asks for and what a
|
|
10
|
+
reader can run today. A real controller --- a lab OCS, a campus ROADM --- binds
|
|
11
|
+
to the same protocol; this repository does not ship one, because writing a
|
|
12
|
+
driver against a plant nobody has run it on would be fiction.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .delay_node import (
|
|
16
|
+
DelayNode,
|
|
17
|
+
EmulatedController,
|
|
18
|
+
PathMeasurement,
|
|
19
|
+
StitchController,
|
|
20
|
+
apply_measurement,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"DelayNode",
|
|
25
|
+
"EmulatedController",
|
|
26
|
+
"PathMeasurement",
|
|
27
|
+
"StitchController",
|
|
28
|
+
"apply_measurement",
|
|
29
|
+
]
|