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 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,4 @@
1
+ from .cli import main
2
+
3
+
4
+ raise SystemExit(main())
@@ -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