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.
Files changed (95) hide show
  1. flashml_workloads/__init__.py +7 -0
  2. flashml_workloads/fedavg_driver.py +569 -0
  3. flashml_workloads/fedavg_weights.py +223 -0
  4. flashml_workloads/fedavg_worker.py +166 -0
  5. flashml_workloads/kmeans_driver.py +134 -0
  6. flashml_workloads/kmeans_shard.py +69 -0
  7. flashml_workloads/sgd_trainer.py +127 -0
  8. flashml_workloads/sharded_kmeans.py +323 -0
  9. flashml_workloads/sklearn_trial.py +89 -0
  10. flashruntime/__init__.py +125 -0
  11. flashruntime/artifacts/__init__.py +25 -0
  12. flashruntime/artifacts/store.py +228 -0
  13. flashruntime/backends/__init__.py +26 -0
  14. flashruntime/backends/base.py +63 -0
  15. flashruntime/backends/kuberay.py +465 -0
  16. flashruntime/checkpoint/__init__.py +20 -0
  17. flashruntime/checkpoint/catalog.py +198 -0
  18. flashruntime/checkpoint/local.py +109 -0
  19. flashruntime/checkpoint/store.py +86 -0
  20. flashruntime/integrations/__init__.py +5 -0
  21. flashruntime/integrations/huggingface.py +59 -0
  22. flashruntime/integrations/pytorch.py +52 -0
  23. flashruntime/integrations/sklearn.py +42 -0
  24. flashruntime/launchers/__init__.py +130 -0
  25. flashruntime/launchers/local.py +126 -0
  26. flashruntime/leases/__init__.py +27 -0
  27. flashruntime/leases/manager.py +365 -0
  28. flashruntime/leases/sqlite_store.py +169 -0
  29. flashruntime/leases/store.py +103 -0
  30. flashruntime/monitor/__init__.py +7 -0
  31. flashruntime/monitor/sampler.py +232 -0
  32. flashruntime/planner/__init__.py +56 -0
  33. flashruntime/planner/candidates.py +597 -0
  34. flashruntime/planner/catalog.py +129 -0
  35. flashruntime/planner/comm.py +95 -0
  36. flashruntime/planner/explain.py +109 -0
  37. flashruntime/planner/memory.py +166 -0
  38. flashruntime/planner/resolve.py +120 -0
  39. flashruntime/planner/selector.py +169 -0
  40. flashruntime/planner/timecost.py +81 -0
  41. flashruntime/profiling/__init__.py +113 -0
  42. flashruntime/protocol/__init__.py +18 -0
  43. flashruntime/protocol/plan_v1alpha1.py +320 -0
  44. flashruntime/protocol/v1alpha1.py +465 -0
  45. flashruntime/providers/__init__.py +138 -0
  46. flashruntime/py.typed +0 -0
  47. flashruntime/recipes/__init__.py +135 -0
  48. flashruntime/recipes/command.py +166 -0
  49. flashruntime/recovery/__init__.py +21 -0
  50. flashruntime/recovery/policy.py +170 -0
  51. flashruntime/recovery/signals.py +135 -0
  52. flashruntime/recovery/taxonomy.py +91 -0
  53. flashruntime/scheduler/__init__.py +170 -0
  54. flashruntime/sdk.py +402 -0
  55. flashruntime/service/__init__.py +3 -0
  56. flashruntime/service/app.py +391 -0
  57. flashruntime/service/auth.py +180 -0
  58. flashruntime/service/checkpoints.py +90 -0
  59. flashruntime/service/cli.py +167 -0
  60. flashruntime/service/dashboard.py +193 -0
  61. flashruntime/service/ledger.py +101 -0
  62. flashruntime/service/modea.py +821 -0
  63. flashruntime/strategies/__init__.py +156 -0
  64. flashruntime/strategies/command.py +56 -0
  65. flashruntime/torch/__init__.py +274 -0
  66. flashruntime/viewer/__init__.py +20 -0
  67. flashruntime/viewer/_docs/benchmarks.html +771 -0
  68. flashruntime/viewer/_docs/concepts/architecture.html +302 -0
  69. flashruntime/viewer/_docs/get-started.html +263 -0
  70. flashruntime/viewer/_docs/guides/federated-averaging.html +363 -0
  71. flashruntime/viewer/_docs/guides/huggingface.html +223 -0
  72. flashruntime/viewer/_docs/guides/jobspec-and-isolation.html +271 -0
  73. flashruntime/viewer/_docs/guides/pytorch.html +313 -0
  74. flashruntime/viewer/_docs/guides/sklearn.html +232 -0
  75. flashruntime/viewer/_docs/index.html +251 -0
  76. flashruntime/viewer/_docs/reference/cli.html +254 -0
  77. flashruntime/viewer/_docs/reference/integrations.html +240 -0
  78. flashruntime/viewer/_docs/reference/sdk.html +341 -0
  79. flashruntime/viewer/_docs/reference/torch-helper.html +244 -0
  80. flashruntime/viewer/_docs/search-index.json +1 -0
  81. flashruntime/viewer/_docs/tutorials/convnet.html +571 -0
  82. flashruntime/viewer/_docs/tutorials/fault-tolerance.html +375 -0
  83. flashruntime/viewer/_docs/tutorials/sklearn-sweeps.html +278 -0
  84. flashruntime/viewer/flowmap.py +307 -0
  85. flashruntime/viewer/page.py +594 -0
  86. flashruntime/viewer/server.py +134 -0
  87. flashruntime/viewer/state.py +250 -0
  88. flashruntime/workloads/__init__.py +6 -0
  89. flashruntime/workloads/command.py +127 -0
  90. flashruntime-0.3.0.dist-info/METADATA +365 -0
  91. flashruntime-0.3.0.dist-info/RECORD +95 -0
  92. flashruntime-0.3.0.dist-info/WHEEL +5 -0
  93. flashruntime-0.3.0.dist-info/entry_points.txt +2 -0
  94. flashruntime-0.3.0.dist-info/licenses/LICENSE +202 -0
  95. 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]: ...