tpu-runner 0.1.0__py3-none-any.whl
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.
- tpu_runner/Dockerfile +10 -0
- tpu_runner/__init__.py +18 -0
- tpu_runner/__main__.py +4 -0
- tpu_runner/capacity_policy.py +248 -0
- tpu_runner/cli.py +1344 -0
- tpu_runner/cloudbuild.yaml +11 -0
- tpu_runner/controller-requirements.txt +2 -0
- tpu_runner/controller.py +1680 -0
- tpu_runner/deploy.sh +263 -0
- tpu_runner/deployment.example.yaml +57 -0
- tpu_runner/distributed.py +1017 -0
- tpu_runner/gcp.py +454 -0
- tpu_runner/placement.py +137 -0
- tpu_runner/region_pools.py +24 -0
- tpu_runner/runtime.py +994 -0
- tpu_runner/specs.py +527 -0
- tpu_runner/startup.sh +49 -0
- tpu_runner-0.1.0.dist-info/METADATA +140 -0
- tpu_runner-0.1.0.dist-info/RECORD +23 -0
- tpu_runner-0.1.0.dist-info/WHEEL +5 -0
- tpu_runner-0.1.0.dist-info/entry_points.txt +2 -0
- tpu_runner-0.1.0.dist-info/licenses/LICENSE +21 -0
- tpu_runner-0.1.0.dist-info/top_level.txt +1 -0
tpu_runner/Dockerfile
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
FROM gcr.io/google.com/cloudsdktool/google-cloud-cli:slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
COPY tpu_runner/controller-requirements.txt ./
|
|
5
|
+
COPY tpu_runner ./tpu_runner
|
|
6
|
+
|
|
7
|
+
RUN python3 -m pip install --no-cache-dir --break-system-packages \
|
|
8
|
+
-r controller-requirements.txt
|
|
9
|
+
|
|
10
|
+
ENTRYPOINT ["python3", "-m", "tpu_runner.cli"]
|
tpu_runner/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""GCP TPU fleet and job orchestration helpers."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from .specs import FleetSpec, JobSpec, load_fleet_spec, load_job_specs
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
__version__ = version("tpu-runner")
|
|
9
|
+
except PackageNotFoundError: # Source tree imported without installation.
|
|
10
|
+
__version__ = "0+unknown"
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"FleetSpec",
|
|
14
|
+
"JobSpec",
|
|
15
|
+
"__version__",
|
|
16
|
+
"load_fleet_spec",
|
|
17
|
+
"load_job_specs",
|
|
18
|
+
]
|
tpu_runner/__main__.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Pure placement and capacity policy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .gcp import generated_resource_names
|
|
6
|
+
from .region_pools import region_is_in_pool
|
|
7
|
+
from .runtime import JobRecord, ResourceRecord
|
|
8
|
+
from .specs import FleetSpec, TPUEntry, region_from_zone
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
JOB_PRIORITY_RANK = {"low": 0, "normal": 1, "high": 2}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def pending_job_accepts_entry(job: JobRecord, entry) -> bool:
|
|
15
|
+
if job.status != "pending":
|
|
16
|
+
return False
|
|
17
|
+
if job.spec.zone and job.spec.zone != entry.zone:
|
|
18
|
+
return False
|
|
19
|
+
entry_region = region_from_zone(entry.zone)
|
|
20
|
+
if not job.spec.accepts_region(entry_region):
|
|
21
|
+
return False
|
|
22
|
+
if job.spec.storage_region and (
|
|
23
|
+
not job.spec.region or not region_is_in_pool(entry_region, job.spec.region)
|
|
24
|
+
):
|
|
25
|
+
return False
|
|
26
|
+
if job.spec.tpu_name:
|
|
27
|
+
if entry.adopted:
|
|
28
|
+
return (
|
|
29
|
+
job.spec.accepts_tpu_type(entry.type)
|
|
30
|
+
and job.spec.tpu_name == entry.existing
|
|
31
|
+
)
|
|
32
|
+
return job.spec.accepts_tpu_type(entry.type) and any(
|
|
33
|
+
generated_resource_names(entry, ordinal)[1] == job.spec.tpu_name
|
|
34
|
+
for ordinal in range(1, entry.count + 1)
|
|
35
|
+
)
|
|
36
|
+
return (entry.adopted or entry.count > 0) and job.spec.accepts_tpu_type(
|
|
37
|
+
entry.type
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def pending_job_accepts_resource(job: JobRecord, resource: ResourceRecord) -> bool:
|
|
42
|
+
"""Match compatible idle capacity while preserving exact TPU pins."""
|
|
43
|
+
|
|
44
|
+
if job.status != "pending":
|
|
45
|
+
return False
|
|
46
|
+
if not job.spec.accepts_tpu_type(resource.tpu_type):
|
|
47
|
+
return False
|
|
48
|
+
if job.spec.zone and job.spec.zone != resource.zone:
|
|
49
|
+
return False
|
|
50
|
+
resource_region = region_from_zone(resource.zone)
|
|
51
|
+
if not job.spec.accepts_region(resource_region):
|
|
52
|
+
return False
|
|
53
|
+
if job.spec.storage_region and (
|
|
54
|
+
not job.spec.region or not region_is_in_pool(resource_region, job.spec.region)
|
|
55
|
+
):
|
|
56
|
+
return False
|
|
57
|
+
return not job.spec.tpu_name or job.spec.tpu_name == resource.tpu_name
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def resource_is_busy(resource: ResourceRecord) -> bool:
|
|
61
|
+
return bool(
|
|
62
|
+
resource.status == "busy"
|
|
63
|
+
or resource.current_job_id
|
|
64
|
+
or resource.current_attempt_id
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def pending_job_entry_constraint_key(job: JobRecord, entries: list) -> tuple:
|
|
69
|
+
compatible = sum(pending_job_accepts_entry(job, entry) for entry in entries)
|
|
70
|
+
return (
|
|
71
|
+
compatible == 0,
|
|
72
|
+
-JOB_PRIORITY_RANK[job.spec.priority],
|
|
73
|
+
compatible,
|
|
74
|
+
job.submitted_at,
|
|
75
|
+
job.spec.id,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def pending_job_resource_constraint_key(
|
|
80
|
+
job: JobRecord,
|
|
81
|
+
resources: list[ResourceRecord],
|
|
82
|
+
) -> tuple:
|
|
83
|
+
compatible = sum(
|
|
84
|
+
pending_job_accepts_resource(job, resource) for resource in resources
|
|
85
|
+
)
|
|
86
|
+
return (
|
|
87
|
+
compatible == 0,
|
|
88
|
+
-JOB_PRIORITY_RANK[job.spec.priority],
|
|
89
|
+
compatible,
|
|
90
|
+
job.submitted_at,
|
|
91
|
+
job.spec.id,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def plan_idle_assignments(
|
|
96
|
+
jobs: list[JobRecord],
|
|
97
|
+
resources: list[ResourceRecord],
|
|
98
|
+
*,
|
|
99
|
+
entries: list[TPUEntry] | None = None,
|
|
100
|
+
) -> list[tuple[JobRecord, ResourceRecord]]:
|
|
101
|
+
"""Match idle resources without displacing an earlier pending job.
|
|
102
|
+
|
|
103
|
+
Jobs are considered by priority, constraint count, and FIFO order. An
|
|
104
|
+
augmenting path may move an earlier flexible job to another idle resource,
|
|
105
|
+
but it never drops that job merely to fit a later one.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
idle_resources = sorted(
|
|
109
|
+
(
|
|
110
|
+
resource
|
|
111
|
+
for resource in resources
|
|
112
|
+
if resource.status == "idle" and not resource_is_busy(resource)
|
|
113
|
+
),
|
|
114
|
+
key=lambda resource: (not resource.adopted, resource.id),
|
|
115
|
+
)
|
|
116
|
+
resources_by_id = {resource.id: resource for resource in idle_resources}
|
|
117
|
+
ordered_jobs = sorted(
|
|
118
|
+
(job for job in jobs if job.status == "pending"),
|
|
119
|
+
key=lambda job: (
|
|
120
|
+
pending_job_entry_constraint_key(job, entries)
|
|
121
|
+
if entries is not None
|
|
122
|
+
else pending_job_resource_constraint_key(job, idle_resources)
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
matched_by_resource: dict[str, JobRecord] = {}
|
|
126
|
+
|
|
127
|
+
def match(job: JobRecord, visited: set[str]) -> bool:
|
|
128
|
+
candidates = sorted(
|
|
129
|
+
(
|
|
130
|
+
resource
|
|
131
|
+
for resource in idle_resources
|
|
132
|
+
if resource.id not in visited
|
|
133
|
+
and pending_job_accepts_resource(job, resource)
|
|
134
|
+
),
|
|
135
|
+
key=lambda resource: (
|
|
136
|
+
resource.id in matched_by_resource,
|
|
137
|
+
not resource.adopted,
|
|
138
|
+
resource.id,
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
for resource in candidates:
|
|
142
|
+
visited.add(resource.id)
|
|
143
|
+
previous = matched_by_resource.get(resource.id)
|
|
144
|
+
if previous is None or match(previous, visited):
|
|
145
|
+
matched_by_resource[resource.id] = job
|
|
146
|
+
return True
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
for job in ordered_jobs:
|
|
150
|
+
match(job, set())
|
|
151
|
+
|
|
152
|
+
resources_by_job = {
|
|
153
|
+
job.spec.id: resources_by_id[resource_id]
|
|
154
|
+
for resource_id, job in matched_by_resource.items()
|
|
155
|
+
}
|
|
156
|
+
return [
|
|
157
|
+
(job, resources_by_job[job.spec.id])
|
|
158
|
+
for job in ordered_jobs
|
|
159
|
+
if job.spec.id in resources_by_job
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def allocate_managed_pending_demand(
|
|
164
|
+
jobs: list[JobRecord],
|
|
165
|
+
*,
|
|
166
|
+
fleet: FleetSpec,
|
|
167
|
+
resources: tuple[ResourceRecord, ...],
|
|
168
|
+
) -> dict[str, int]:
|
|
169
|
+
"""Request every pending job in every compatible Spot entry.
|
|
170
|
+
|
|
171
|
+
These are capacity races, not duplicate executions. Assignment remains a
|
|
172
|
+
Firestore transaction, and the losing entry's demand disappears on the
|
|
173
|
+
next reconciliation. Every physical entry count remains a hard ceiling.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
entries = [entry for entry in fleet.tpus if not entry.adopted]
|
|
177
|
+
busy = {
|
|
178
|
+
entry.id: sum(
|
|
179
|
+
1
|
|
180
|
+
for resource in resources
|
|
181
|
+
if resource.fleet_entry_id == entry.id
|
|
182
|
+
and not resource.adopted
|
|
183
|
+
and resource_is_busy(resource)
|
|
184
|
+
)
|
|
185
|
+
for entry in entries
|
|
186
|
+
}
|
|
187
|
+
demand = {entry.id: 0 for entry in entries}
|
|
188
|
+
for job in sorted(
|
|
189
|
+
jobs,
|
|
190
|
+
key=lambda candidate: pending_job_entry_constraint_key(candidate, entries),
|
|
191
|
+
):
|
|
192
|
+
for entry in entries:
|
|
193
|
+
available = max(0, entry.count - busy[entry.id])
|
|
194
|
+
if demand[entry.id] >= available:
|
|
195
|
+
continue
|
|
196
|
+
if pending_job_accepts_entry(job, entry):
|
|
197
|
+
demand[entry.id] += 1
|
|
198
|
+
return demand
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def desired_managed_capacity_counts(
|
|
202
|
+
jobs: list[JobRecord],
|
|
203
|
+
*,
|
|
204
|
+
fleet: FleetSpec,
|
|
205
|
+
resources: tuple[ResourceRecord, ...],
|
|
206
|
+
) -> dict[str, int]:
|
|
207
|
+
"""Return busy plus raced pending demand, capped by physical ceilings."""
|
|
208
|
+
|
|
209
|
+
idle_adopted = [
|
|
210
|
+
resource
|
|
211
|
+
for resource in resources
|
|
212
|
+
if resource.adopted and resource.status == "idle"
|
|
213
|
+
]
|
|
214
|
+
adopted_job_ids = {
|
|
215
|
+
job.spec.id
|
|
216
|
+
for job, _ in plan_idle_assignments(
|
|
217
|
+
jobs,
|
|
218
|
+
idle_adopted,
|
|
219
|
+
entries=list(fleet.tpus),
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
jobs_requiring_managed = [
|
|
223
|
+
job
|
|
224
|
+
for job in jobs
|
|
225
|
+
if job.status == "pending" and job.spec.id not in adopted_job_ids
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
pending_demand = allocate_managed_pending_demand(
|
|
229
|
+
jobs_requiring_managed,
|
|
230
|
+
fleet=fleet,
|
|
231
|
+
resources=resources,
|
|
232
|
+
)
|
|
233
|
+
desired: dict[str, int] = {}
|
|
234
|
+
for entry in fleet.tpus:
|
|
235
|
+
if entry.adopted:
|
|
236
|
+
continue
|
|
237
|
+
busy_count = sum(
|
|
238
|
+
1
|
|
239
|
+
for resource in resources
|
|
240
|
+
if resource.fleet_entry_id == entry.id
|
|
241
|
+
and not resource.adopted
|
|
242
|
+
and resource_is_busy(resource)
|
|
243
|
+
)
|
|
244
|
+
desired[entry.id] = min(
|
|
245
|
+
entry.count,
|
|
246
|
+
max(int(entry.keep_warm), busy_count + pending_demand.get(entry.id, 0)),
|
|
247
|
+
)
|
|
248
|
+
return desired
|