flashruntime 0.3.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.
- flashml_workloads/__init__.py +7 -0
- flashml_workloads/fedavg_driver.py +569 -0
- flashml_workloads/fedavg_weights.py +223 -0
- flashml_workloads/fedavg_worker.py +166 -0
- flashml_workloads/kmeans_driver.py +134 -0
- flashml_workloads/kmeans_shard.py +69 -0
- flashml_workloads/sgd_trainer.py +127 -0
- flashml_workloads/sharded_kmeans.py +323 -0
- flashml_workloads/sklearn_trial.py +89 -0
- flashruntime/__init__.py +125 -0
- flashruntime/artifacts/__init__.py +25 -0
- flashruntime/artifacts/store.py +228 -0
- flashruntime/backends/__init__.py +26 -0
- flashruntime/backends/base.py +63 -0
- flashruntime/backends/kuberay.py +465 -0
- flashruntime/checkpoint/__init__.py +20 -0
- flashruntime/checkpoint/catalog.py +198 -0
- flashruntime/checkpoint/local.py +109 -0
- flashruntime/checkpoint/store.py +86 -0
- flashruntime/integrations/__init__.py +5 -0
- flashruntime/integrations/huggingface.py +59 -0
- flashruntime/integrations/pytorch.py +52 -0
- flashruntime/integrations/sklearn.py +42 -0
- flashruntime/launchers/__init__.py +130 -0
- flashruntime/launchers/local.py +126 -0
- flashruntime/leases/__init__.py +27 -0
- flashruntime/leases/manager.py +365 -0
- flashruntime/leases/sqlite_store.py +169 -0
- flashruntime/leases/store.py +103 -0
- flashruntime/monitor/__init__.py +7 -0
- flashruntime/monitor/sampler.py +232 -0
- flashruntime/planner/__init__.py +56 -0
- flashruntime/planner/candidates.py +597 -0
- flashruntime/planner/catalog.py +129 -0
- flashruntime/planner/comm.py +95 -0
- flashruntime/planner/explain.py +109 -0
- flashruntime/planner/memory.py +166 -0
- flashruntime/planner/resolve.py +120 -0
- flashruntime/planner/selector.py +169 -0
- flashruntime/planner/timecost.py +81 -0
- flashruntime/profiling/__init__.py +113 -0
- flashruntime/protocol/__init__.py +18 -0
- flashruntime/protocol/plan_v1alpha1.py +320 -0
- flashruntime/protocol/v1alpha1.py +465 -0
- flashruntime/providers/__init__.py +138 -0
- flashruntime/py.typed +0 -0
- flashruntime/recipes/__init__.py +135 -0
- flashruntime/recipes/command.py +166 -0
- flashruntime/recovery/__init__.py +21 -0
- flashruntime/recovery/policy.py +170 -0
- flashruntime/recovery/signals.py +135 -0
- flashruntime/recovery/taxonomy.py +91 -0
- flashruntime/scheduler/__init__.py +170 -0
- flashruntime/sdk.py +402 -0
- flashruntime/service/__init__.py +3 -0
- flashruntime/service/app.py +391 -0
- flashruntime/service/auth.py +180 -0
- flashruntime/service/checkpoints.py +90 -0
- flashruntime/service/cli.py +167 -0
- flashruntime/service/dashboard.py +193 -0
- flashruntime/service/ledger.py +101 -0
- flashruntime/service/modea.py +821 -0
- flashruntime/strategies/__init__.py +156 -0
- flashruntime/strategies/command.py +56 -0
- flashruntime/torch/__init__.py +274 -0
- flashruntime/viewer/__init__.py +20 -0
- flashruntime/viewer/_docs/benchmarks.html +771 -0
- flashruntime/viewer/_docs/concepts/architecture.html +302 -0
- flashruntime/viewer/_docs/get-started.html +263 -0
- flashruntime/viewer/_docs/guides/federated-averaging.html +363 -0
- flashruntime/viewer/_docs/guides/huggingface.html +223 -0
- flashruntime/viewer/_docs/guides/jobspec-and-isolation.html +271 -0
- flashruntime/viewer/_docs/guides/pytorch.html +313 -0
- flashruntime/viewer/_docs/guides/sklearn.html +232 -0
- flashruntime/viewer/_docs/index.html +251 -0
- flashruntime/viewer/_docs/reference/cli.html +254 -0
- flashruntime/viewer/_docs/reference/integrations.html +240 -0
- flashruntime/viewer/_docs/reference/sdk.html +341 -0
- flashruntime/viewer/_docs/reference/torch-helper.html +244 -0
- flashruntime/viewer/_docs/search-index.json +1 -0
- flashruntime/viewer/_docs/tutorials/convnet.html +571 -0
- flashruntime/viewer/_docs/tutorials/fault-tolerance.html +375 -0
- flashruntime/viewer/_docs/tutorials/sklearn-sweeps.html +278 -0
- flashruntime/viewer/flowmap.py +307 -0
- flashruntime/viewer/page.py +594 -0
- flashruntime/viewer/server.py +134 -0
- flashruntime/viewer/state.py +250 -0
- flashruntime/workloads/__init__.py +6 -0
- flashruntime/workloads/command.py +127 -0
- flashruntime-0.3.0.dist-info/METADATA +365 -0
- flashruntime-0.3.0.dist-info/RECORD +95 -0
- flashruntime-0.3.0.dist-info/WHEEL +5 -0
- flashruntime-0.3.0.dist-info/entry_points.txt +2 -0
- flashruntime-0.3.0.dist-info/licenses/LICENSE +202 -0
- flashruntime-0.3.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import hashlib
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Protocol, runtime_checkable
|
|
8
|
+
|
|
9
|
+
from flashruntime.protocol.v1alpha1 import ArtifactRecord
|
|
10
|
+
|
|
11
|
+
ARTIFACT_SCHEME = "artifact://"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def artifact_uri_to_key(uri: str) -> str:
|
|
15
|
+
if not uri.startswith(ARTIFACT_SCHEME):
|
|
16
|
+
raise ValueError(f"not an artifact URI: {uri}")
|
|
17
|
+
return uri[len(ARTIFACT_SCHEME):]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def key_to_artifact_uri(object_key: str) -> str:
|
|
21
|
+
return f"{ARTIFACT_SCHEME}{object_key}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _sha256(path: Path) -> str:
|
|
25
|
+
h = hashlib.sha256()
|
|
26
|
+
with path.open("rb") as f:
|
|
27
|
+
for chunk in iter(lambda: f.read(1 << 20), b""):
|
|
28
|
+
h.update(chunk)
|
|
29
|
+
return h.hexdigest()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@runtime_checkable
|
|
33
|
+
class ArtifactStore(Protocol):
|
|
34
|
+
backend: str
|
|
35
|
+
bucket: str
|
|
36
|
+
|
|
37
|
+
async def put_file(self, local_path: Path, object_key: str) -> ArtifactRecord: ...
|
|
38
|
+
|
|
39
|
+
async def get_file(self, object_key: str, destination: Path) -> None: ...
|
|
40
|
+
|
|
41
|
+
async def exists(self, object_key: str) -> bool: ...
|
|
42
|
+
|
|
43
|
+
async def list_prefix(self, prefix: str) -> list[ArtifactRecord]: ...
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class S3CompatibleArtifactStore:
|
|
47
|
+
"""MinIO (and any S3-compatible endpoint) via the `minio` client.
|
|
48
|
+
|
|
49
|
+
Also usable against OSS's S3-compatible mode, but prefer OSSArtifactStore
|
|
50
|
+
for OSS — OSS is not a drop-in S3 (addressing style, ETag semantics, STS
|
|
51
|
+
tokens differ), so the native client is the honest integration.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
endpoint: str,
|
|
57
|
+
bucket: str,
|
|
58
|
+
access_key: str,
|
|
59
|
+
secret_key: str,
|
|
60
|
+
secure: bool = False,
|
|
61
|
+
region: str | None = None,
|
|
62
|
+
backend: str = "minio",
|
|
63
|
+
):
|
|
64
|
+
from minio import Minio
|
|
65
|
+
|
|
66
|
+
self.backend = backend
|
|
67
|
+
self.bucket = bucket
|
|
68
|
+
self._client = Minio(
|
|
69
|
+
endpoint, access_key=access_key, secret_key=secret_key,
|
|
70
|
+
secure=secure, region=region,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def ensure_bucket(self) -> None:
|
|
74
|
+
if not self._client.bucket_exists(self.bucket):
|
|
75
|
+
self._client.make_bucket(self.bucket)
|
|
76
|
+
|
|
77
|
+
def _record(self, object_key: str, etag: str | None, size: int | None,
|
|
78
|
+
sha256: str | None = None) -> ArtifactRecord:
|
|
79
|
+
return ArtifactRecord(
|
|
80
|
+
uri=key_to_artifact_uri(object_key),
|
|
81
|
+
backend=self.backend, # type: ignore[arg-type]
|
|
82
|
+
bucket=self.bucket,
|
|
83
|
+
object_key=object_key,
|
|
84
|
+
etag=etag,
|
|
85
|
+
sha256=sha256,
|
|
86
|
+
size_bytes=size,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
async def put_file(self, local_path: Path, object_key: str) -> ArtifactRecord:
|
|
90
|
+
local_path = Path(local_path)
|
|
91
|
+
|
|
92
|
+
def _put():
|
|
93
|
+
digest = _sha256(local_path)
|
|
94
|
+
result = self._client.fput_object(self.bucket, object_key, str(local_path))
|
|
95
|
+
return self._record(
|
|
96
|
+
object_key, result.etag, local_path.stat().st_size, digest
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return await asyncio.to_thread(_put)
|
|
100
|
+
|
|
101
|
+
async def get_file(self, object_key: str, destination: Path) -> None:
|
|
102
|
+
await asyncio.to_thread(
|
|
103
|
+
self._client.fget_object, self.bucket, object_key, str(destination)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
async def exists(self, object_key: str) -> bool:
|
|
107
|
+
def _stat() -> bool:
|
|
108
|
+
from minio.error import S3Error
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
self._client.stat_object(self.bucket, object_key)
|
|
112
|
+
return True
|
|
113
|
+
except S3Error as exc:
|
|
114
|
+
if exc.code in ("NoSuchKey", "NoSuchObject"):
|
|
115
|
+
return False
|
|
116
|
+
raise
|
|
117
|
+
|
|
118
|
+
return await asyncio.to_thread(_stat)
|
|
119
|
+
|
|
120
|
+
async def list_prefix(self, prefix: str) -> list[ArtifactRecord]:
|
|
121
|
+
def _list():
|
|
122
|
+
objects = self._client.list_objects(self.bucket, prefix=prefix, recursive=True)
|
|
123
|
+
return [
|
|
124
|
+
self._record(o.object_name, o.etag, o.size)
|
|
125
|
+
for o in objects
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
return await asyncio.to_thread(_list)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class OSSArtifactStore:
|
|
132
|
+
"""Alibaba OSS via the native oss2 SDK. Supports STS security tokens."""
|
|
133
|
+
|
|
134
|
+
def __init__(
|
|
135
|
+
self,
|
|
136
|
+
endpoint: str,
|
|
137
|
+
bucket: str,
|
|
138
|
+
access_key: str,
|
|
139
|
+
secret_key: str,
|
|
140
|
+
security_token: str | None = None,
|
|
141
|
+
):
|
|
142
|
+
import oss2
|
|
143
|
+
|
|
144
|
+
self.backend = "oss"
|
|
145
|
+
self.bucket = bucket
|
|
146
|
+
if security_token:
|
|
147
|
+
auth = oss2.StsAuth(access_key, secret_key, security_token)
|
|
148
|
+
else:
|
|
149
|
+
auth = oss2.Auth(access_key, secret_key)
|
|
150
|
+
self._bucket = oss2.Bucket(auth, endpoint, bucket)
|
|
151
|
+
|
|
152
|
+
def _record(self, object_key: str, etag: str | None, size: int | None,
|
|
153
|
+
sha256: str | None = None) -> ArtifactRecord:
|
|
154
|
+
return ArtifactRecord(
|
|
155
|
+
uri=key_to_artifact_uri(object_key),
|
|
156
|
+
backend="oss",
|
|
157
|
+
bucket=self.bucket,
|
|
158
|
+
object_key=object_key,
|
|
159
|
+
etag=etag,
|
|
160
|
+
sha256=sha256,
|
|
161
|
+
size_bytes=size,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
async def put_file(self, local_path: Path, object_key: str) -> ArtifactRecord:
|
|
165
|
+
local_path = Path(local_path)
|
|
166
|
+
|
|
167
|
+
def _put():
|
|
168
|
+
digest = _sha256(local_path)
|
|
169
|
+
result = self._bucket.put_object_from_file(object_key, str(local_path))
|
|
170
|
+
return self._record(
|
|
171
|
+
object_key, result.etag, local_path.stat().st_size, digest
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return await asyncio.to_thread(_put)
|
|
175
|
+
|
|
176
|
+
async def get_file(self, object_key: str, destination: Path) -> None:
|
|
177
|
+
await asyncio.to_thread(
|
|
178
|
+
self._bucket.get_object_to_file, object_key, str(destination)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
async def exists(self, object_key: str) -> bool:
|
|
182
|
+
return await asyncio.to_thread(self._bucket.object_exists, object_key)
|
|
183
|
+
|
|
184
|
+
async def list_prefix(self, prefix: str) -> list[ArtifactRecord]:
|
|
185
|
+
def _list():
|
|
186
|
+
import oss2
|
|
187
|
+
|
|
188
|
+
records = []
|
|
189
|
+
for obj in oss2.ObjectIterator(self._bucket, prefix=prefix):
|
|
190
|
+
records.append(self._record(obj.key, obj.etag, obj.size))
|
|
191
|
+
return records
|
|
192
|
+
|
|
193
|
+
return await asyncio.to_thread(_list)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def store_from_env() -> ArtifactStore:
|
|
197
|
+
"""Build the artifact store from FLASHML_ARTIFACT_* env vars.
|
|
198
|
+
|
|
199
|
+
FLASHML_ARTIFACT_BACKEND=minio (default) | oss
|
|
200
|
+
Shared: FLASHML_ARTIFACT_ENDPOINT, FLASHML_ARTIFACT_BUCKET,
|
|
201
|
+
FLASHML_ARTIFACT_ACCESS_KEY, FLASHML_ARTIFACT_SECRET_KEY
|
|
202
|
+
minio: FLASHML_ARTIFACT_SECURE=true|false, FLASHML_ARTIFACT_REGION
|
|
203
|
+
oss: FLASHML_ARTIFACT_SECURITY_TOKEN (optional STS)
|
|
204
|
+
"""
|
|
205
|
+
backend = os.environ.get("FLASHML_ARTIFACT_BACKEND", "minio")
|
|
206
|
+
endpoint = os.environ["FLASHML_ARTIFACT_ENDPOINT"]
|
|
207
|
+
bucket = os.environ["FLASHML_ARTIFACT_BUCKET"]
|
|
208
|
+
access_key = os.environ["FLASHML_ARTIFACT_ACCESS_KEY"]
|
|
209
|
+
secret_key = os.environ["FLASHML_ARTIFACT_SECRET_KEY"]
|
|
210
|
+
|
|
211
|
+
if backend == "oss":
|
|
212
|
+
return OSSArtifactStore(
|
|
213
|
+
endpoint=endpoint,
|
|
214
|
+
bucket=bucket,
|
|
215
|
+
access_key=access_key,
|
|
216
|
+
secret_key=secret_key,
|
|
217
|
+
security_token=os.environ.get("FLASHML_ARTIFACT_SECURITY_TOKEN") or None,
|
|
218
|
+
)
|
|
219
|
+
if backend == "minio":
|
|
220
|
+
return S3CompatibleArtifactStore(
|
|
221
|
+
endpoint=endpoint,
|
|
222
|
+
bucket=bucket,
|
|
223
|
+
access_key=access_key,
|
|
224
|
+
secret_key=secret_key,
|
|
225
|
+
secure=os.environ.get("FLASHML_ARTIFACT_SECURE", "false").lower() == "true",
|
|
226
|
+
region=os.environ.get("FLASHML_ARTIFACT_REGION") or None,
|
|
227
|
+
)
|
|
228
|
+
raise ValueError(f"unknown artifact backend: {backend!r} (expected minio|oss)")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Execution backends: pluggable engines that run a FlashRuntime Job.
|
|
2
|
+
|
|
3
|
+
FlashRuntime owns the public JobSpec, job state, events, and artifacts; a
|
|
4
|
+
backend owns nothing but the translation to (and observation of) one
|
|
5
|
+
concrete execution system. The first real backend is KubeRay
|
|
6
|
+
(`flashruntime.backends.kuberay`). Documented-but-unimplemented backends
|
|
7
|
+
(PAI-DLC, torchrun, Dask, DeepSpeed, Slurm) are described in
|
|
8
|
+
docs/adr/0002-pai-dlc-backend.md and must satisfy
|
|
9
|
+
`flashruntime.backends.base.ExecutionBackend`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from flashruntime.backends.base import (
|
|
13
|
+
BackendExecution,
|
|
14
|
+
BackendStatus,
|
|
15
|
+
BackendUnavailableError,
|
|
16
|
+
ExecutionBackend,
|
|
17
|
+
SpecValidationError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"BackendExecution",
|
|
22
|
+
"BackendStatus",
|
|
23
|
+
"BackendUnavailableError",
|
|
24
|
+
"ExecutionBackend",
|
|
25
|
+
"SpecValidationError",
|
|
26
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Backend-neutral execution contract.
|
|
2
|
+
|
|
3
|
+
A backend receives a validated Job (public JobSpec + runtime identity),
|
|
4
|
+
submits it to one execution system, and reports status/events/logs back in
|
|
5
|
+
FlashRuntime's vocabulary. Backends never invent job state — they map
|
|
6
|
+
observed backend signals onto `JobState` and `Event`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any, AsyncIterator, Protocol, runtime_checkable
|
|
14
|
+
|
|
15
|
+
from flashruntime.protocol.v1alpha1 import ArtifactRecord, Event, JobRecord, JobSpec, JobState
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SpecValidationError(ValueError):
|
|
19
|
+
"""The JobSpec cannot run on this backend in this deployment profile."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BackendUnavailableError(RuntimeError):
|
|
23
|
+
"""The backend's execution system cannot be reached."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class BackendExecution:
|
|
28
|
+
"""Identity of a submitted execution inside the backend."""
|
|
29
|
+
|
|
30
|
+
execution_id: str # e.g. RayJob custom-resource name
|
|
31
|
+
backend: str
|
|
32
|
+
submitted_at: datetime
|
|
33
|
+
details: dict[str, Any] = field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class BackendStatus:
|
|
38
|
+
"""Point-in-time backend view, already mapped to FlashRuntime state."""
|
|
39
|
+
|
|
40
|
+
state: JobState
|
|
41
|
+
reason: str = ""
|
|
42
|
+
raw: dict[str, Any] = field(default_factory=dict)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@runtime_checkable
|
|
46
|
+
class ExecutionBackend(Protocol):
|
|
47
|
+
name: str
|
|
48
|
+
|
|
49
|
+
async def validate(self, spec: JobSpec) -> None:
|
|
50
|
+
"""Raise SpecValidationError if this backend/profile cannot run the spec."""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
async def submit(self, job: JobRecord) -> BackendExecution: ...
|
|
54
|
+
|
|
55
|
+
async def get_status(self, execution_id: str) -> BackendStatus: ...
|
|
56
|
+
|
|
57
|
+
def stream_events(self, execution_id: str) -> AsyncIterator[Event]: ...
|
|
58
|
+
|
|
59
|
+
async def get_logs(self, execution_id: str) -> str: ...
|
|
60
|
+
|
|
61
|
+
async def cancel(self, execution_id: str) -> None: ...
|
|
62
|
+
|
|
63
|
+
async def collect_artifacts(self, execution_id: str) -> list[ArtifactRecord]: ...
|