The span contract: deciding whether an AI training job may cross a data hall
When a job asks to run across more than one hall, who decides, and on what facts?
Today, three systems decide, and none of them talks to the others. The scheduler knows how many accelerators are free. The compiler knows what plan it emitted and what topology it assumed. The circuit controller knows what glass is lit. Each holds a different half-truth, and the job discovers which one was wrong at step time — or worse, at checkpoint time, three hours in.
This repository is one object that makes them agree before the job starts: twenty-one fields, six decisions, three conditions under which the contract refuses rather than guesses, and a reference validator that turns the first into the second and prints its reasons. It is a research artifact, not a product.
What it is not
It is not a fabric, a scheduler, or a network model. It decides admission — may this job cross — and holds no opinion on capacity — what the job would retain if it did. That second question is answered by the latency-regime atlas in network-vs-more-gpus, and this repository deliberately does not offer a second answer to it. The two are designed to be read together: the atlas says a tensor-parallel cut retains 0.004 across any stitch at any distance, and the contract is what refuses to launch it.
The shape of it
from spancontract import SpanEnvelope, validate
verdict = validate(envelope)
verdict.decision # Decision.DENY
verdict.reasons() # ['[FC2] path measurement is 7200s old, past the 300s TTL']
verdict.not_checked # what the validator could not check, printed rather than omitted
$ spancontract validate examples/long-haul-training-too-far.json
decision: DENY
regime: region (2958 us RTT)
findings:
- [LR1] synchronous train at 2958 us RTT (region) exceeds the 2000 us limit;
the default policy is that training stays hall-local
not checked:
? XP1 declared-vs-computed blast radius: no plant graph supplied, so the
envelope's blast_radius was taken on trust
? XP2 topology currency: no live topology hash supplied, so the envelope's
topology_hash was taken on trust
The three conditions that fail closed
Each describes a state in which the contract does not know something it needs, and in each case not knowing is a refusal. A validator that read silence as health would be worse than no validator, because it would be trusted.
| Condition | Why it is a refusal and not a warning | |
|---|---|---|
| FC1 | The circuit API is dark | The state of the stitch is unknown. Unknown is not healthy. |
| FC2 | The measured path is stale past its TTL | A declared topology is a claim. Only a fresh measurement is an observation. |
| FC3 | The compile cache was keyed on a topology that no longer exists | The binary was built for a placement that is gone. |
The six decisions
local · span · shrink · move · escalate · deny, on a severity ladder.
Rules join with a maximum, so a rule can only push a verdict toward refusal and
never away from it — which is what makes the order the rules run in irrelevant to
the answer. The registry proves that by shuffling the rule list over 400 random
envelopes and requiring every verdict to be identical.
What the validator refuses to pretend
make validate prints the registry, and then prints the list of what it
declined to check. That second list is the more useful one. It says, among
other things, that no published figure fixes any threshold in this repository,
that no verdict here has been checked against a job that actually ran, and that
the emulated circuit models amplifier gain without the optical signal-to-noise
cost that comes with it, so it can demonstrate a refusal but cannot support a
claim of health.
There is exactly one calibrated point in the registry. That is the honest
count, not a gap. See docs/the-contract.md.
Install and run
git clone https://github.com/dimaggi-ai/span-contract
cd span-contract
make venv
make smoke-test # tests, mutation tests, registry, examples — under a minute
Or from PyPI:
pip install span-contract
spancontract example > envelope.json
spancontract validate envelope.json
| Target | What it does |
|---|---|
make test |
91 tests, including 12 mutation tests that delete machinery and require the registry to go red |
make validate |
the validation registry, and the nine things it declines to check |
make examples |
eleven example envelopes, each checked against its documented verdict |
make schema |
regenerate schema/span_contract.schema.json from the code |
Repository map
| Path | What is in it |
|---|---|
src/spancontract/envelope.py |
the twenty-one fields, inert by design |
src/spancontract/rules.py |
every rule and every threshold, none of them inline |
src/spancontract/validator.py |
the join, the plant cross-checks, the hash-chained record |
src/spancontract/plant.py |
halls, circuits, and what one failure actually costs |
src/spancontract/compile_cache.py |
keyed as specified; refuses entries from a dead topology |
src/spancontract/adapters/delay_node.py |
an emulated circuit, and the tc line that reproduces it |
validation/validate_contract.py |
the registry: one calibrated, seven emergent, ten sanity, nine declined |
tests/test_mutations.py |
delete a piece, name the points that must go red |
docs/the-contract.md |
the specification as implemented, including where it contradicts itself |
docs/integration.md |
the surface an existing policy engine would bind to |
The discrepancy that was carried rather than fixed
The specification lists six decisions, and then enumerates only five of them in
the span_mode field: escalate is missing. Both are implemented exactly as
written — a verdict can be escalate, and span_mode cannot — and a registry
point asserts the gap is still there, so that quietly closing it in a future
commit shows up as a failing check rather than a tidy-up. DECISIONS.md D3 has
the reasoning.
Series
Part of a program on the usable capacity of large accelerator fleets: dimaggi-ai.github.io/research.
License
MIT. Copyright (c) 2026 Margaret Nanyonga.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file span_contract-1.0.1.tar.gz.
File metadata
- Download URL: span_contract-1.0.1.tar.gz
- Upload date:
- Size: 33.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
172168aff874f76882f0ca65e03070b770da6c77379909c2dd11ffcbbcba75c8
|
|
| MD5 |
cfbfcd89bf2769a6413fb6f47f364e62
|
|
| BLAKE2b-256 |
ccdd15c25a6e938dc2312a19cb466e1b56872bb93a7dc768d64fd507aa632f23
|
File details
Details for the file span_contract-1.0.1-py3-none-any.whl.
File metadata
- Download URL: span_contract-1.0.1-py3-none-any.whl
- Upload date:
- Size: 32.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ad6bd1e741b4411a67875974553bb6aae87a466f60b577118a689a4051b477bb
|
|
| MD5 |
91f65b7f97c62a5a70f4511dec61c4c7
|
|
| BLAKE2b-256 |
621a2b7a2d9c8172c6728f015b56512081cd6dba84ca2849b152aafe95a8f9c4
|