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,232 @@
|
|
|
1
|
+
"""ResourceSampler — per-attempt machine + process-tree telemetry.
|
|
2
|
+
|
|
3
|
+
A daemon thread `flash.submit` starts next to each launched attempt. Every
|
|
4
|
+
`period_s` it appends one JSON line to `<output_dir>/telemetry.jsonl`:
|
|
5
|
+
|
|
6
|
+
{"ts": ..., "machine": {...}, "processes": [...]}
|
|
7
|
+
|
|
8
|
+
The viewer tail-reads this file exactly like metrics.jsonl (bounded window,
|
|
9
|
+
torn last line skipped), so plain appends are the right write discipline.
|
|
10
|
+
|
|
11
|
+
psutil is OPTIONAL (the `monitor` extra): with it, cpu/mem and per-process
|
|
12
|
+
stats are real; without it the machine sample carries `"limited": true` and
|
|
13
|
+
only stdlib facts (hostname, cpu_count, load_avg) — the dashboard renders
|
|
14
|
+
the gaps honestly instead of faking numbers. GPU facts come from nvidia-smi
|
|
15
|
+
when present (macOS/CPU boxes simply get `[]`).
|
|
16
|
+
|
|
17
|
+
TOTAL by contract, like everything that watches a run: a tick swallows every
|
|
18
|
+
exception, and the thread can never affect the training process it observes.
|
|
19
|
+
psutil is imported lazily inside `_psutil()` so `import flashruntime` stays
|
|
20
|
+
pydantic-only (the clean-venv core smoke pins this).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import socket
|
|
28
|
+
import subprocess
|
|
29
|
+
import threading
|
|
30
|
+
import time
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
_GPU_QUERY = [
|
|
34
|
+
"nvidia-smi",
|
|
35
|
+
"--query-gpu=name,utilization.gpu,memory.used,memory.total",
|
|
36
|
+
"--format=csv,noheader,nounits",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _psutil():
|
|
41
|
+
"""The psutil module, or None when the `monitor` extra is not installed.
|
|
42
|
+
A function (not a module-level try/import) so tests can monkeypatch the
|
|
43
|
+
seam and so the import cost is paid only inside the sampler thread."""
|
|
44
|
+
try:
|
|
45
|
+
import psutil
|
|
46
|
+
|
|
47
|
+
return psutil
|
|
48
|
+
except Exception: # noqa: BLE001 — any import failure means "not available"
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _machine_sample() -> dict:
|
|
53
|
+
"""One machine-level sample. stdlib facts always; psutil facts when
|
|
54
|
+
available; `limited` is True whenever the psutil numbers are absent
|
|
55
|
+
(missing OR failing) so the UI can say 'install flashruntime[monitor]'."""
|
|
56
|
+
sample: dict = {
|
|
57
|
+
"hostname": socket.gethostname(),
|
|
58
|
+
"cpu_count": os.cpu_count(),
|
|
59
|
+
"load_avg": list(os.getloadavg()) if hasattr(os, "getloadavg") else None,
|
|
60
|
+
"cpu_percent": None,
|
|
61
|
+
"mem_total": None,
|
|
62
|
+
"mem_used": None,
|
|
63
|
+
"gpus": _gpu_sample(),
|
|
64
|
+
"limited": True,
|
|
65
|
+
}
|
|
66
|
+
ps = _psutil()
|
|
67
|
+
if ps is not None:
|
|
68
|
+
try:
|
|
69
|
+
# Like the per-process case, psutil's *first* call to the
|
|
70
|
+
# module-level `cpu_percent(interval=None)` in this process also
|
|
71
|
+
# returns a meaningless 0.0 — but psutil keeps that reference
|
|
72
|
+
# point as module state (not per-instance), so unlike
|
|
73
|
+
# `_process_tree` there is nothing for us to cache: it is
|
|
74
|
+
# correct from the second sampler tick onward. Accepted as one
|
|
75
|
+
# honest-enough tick of staleness at process start.
|
|
76
|
+
sample["cpu_percent"] = ps.cpu_percent(interval=None)
|
|
77
|
+
vm = ps.virtual_memory()
|
|
78
|
+
sample["mem_total"] = vm.total
|
|
79
|
+
sample["mem_used"] = vm.total - vm.available
|
|
80
|
+
sample["limited"] = False
|
|
81
|
+
except Exception: # noqa: BLE001 — degrade to the stdlib-only sample
|
|
82
|
+
pass
|
|
83
|
+
return sample
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _gpu_sample() -> list[dict]:
|
|
87
|
+
"""GPU name/util/memory rows via nvidia-smi, or [] when there is no
|
|
88
|
+
nvidia-smi (macOS, CPU boxes) or it misbehaves. A subprocess (not torch)
|
|
89
|
+
so the sampler works for non-torch workloads and never imports a
|
|
90
|
+
framework."""
|
|
91
|
+
try:
|
|
92
|
+
out = subprocess.run(_GPU_QUERY, capture_output=True, text=True, timeout=2)
|
|
93
|
+
if out.returncode != 0:
|
|
94
|
+
return []
|
|
95
|
+
gpus = []
|
|
96
|
+
for line in out.stdout.strip().splitlines():
|
|
97
|
+
name, util, used, total = [p.strip() for p in line.split(",")]
|
|
98
|
+
gpus.append(
|
|
99
|
+
{
|
|
100
|
+
"name": name,
|
|
101
|
+
"util_percent": float(util),
|
|
102
|
+
"mem_used_mb": float(used),
|
|
103
|
+
"mem_total_mb": float(total),
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
return gpus
|
|
107
|
+
except Exception: # noqa: BLE001 — no GPU story is ever worth an exception
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _process_tree(root_pid: int, cache: dict[int, object]) -> list[dict]:
|
|
112
|
+
"""The launched process and all its descendants (torchrun's ranks, a
|
|
113
|
+
sweep's children), one dict per process. Without psutil we can still name
|
|
114
|
+
the root pid (the launcher knows it); with a vanished/denied root we
|
|
115
|
+
return [] — the attempt likely just finished between ticks.
|
|
116
|
+
|
|
117
|
+
`cache` (pid -> psutil.Process) is caller-owned and reused across ticks.
|
|
118
|
+
psutil.Process.cpu_percent(interval=None) is only a meaningful delta on
|
|
119
|
+
the SECOND call for a given instance — its first call just resets the
|
|
120
|
+
internal reference point and returns a bogus 0.0. Creating a fresh
|
|
121
|
+
Process every tick (the old behavior) meant every sample lied "0%"; by
|
|
122
|
+
keeping one Process instance per live pid across ticks, the second and
|
|
123
|
+
later ticks report a real delta. A freshly-seen pid still gets its
|
|
124
|
+
cpu_percent primed here (so next tick's delta is real) but is reported
|
|
125
|
+
as `None` this tick — an honest "no measurement yet" instead of a fake
|
|
126
|
+
0.0.
|
|
127
|
+
"""
|
|
128
|
+
ps = _psutil()
|
|
129
|
+
if ps is None:
|
|
130
|
+
return [
|
|
131
|
+
{
|
|
132
|
+
"pid": root_pid,
|
|
133
|
+
"ppid": None,
|
|
134
|
+
"cmd": None,
|
|
135
|
+
"cpu_percent": None,
|
|
136
|
+
"rss_bytes": None,
|
|
137
|
+
"create_time": None,
|
|
138
|
+
"status": None,
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
try:
|
|
142
|
+
# Reuse the cached root object (do NOT write it back to `cache` yet —
|
|
143
|
+
# the per-process loop below is the single place that decides
|
|
144
|
+
# first-sighting vs. cached, and pre-inserting here would make a
|
|
145
|
+
# brand-new root look "already cached" before it is ever measured).
|
|
146
|
+
root = cache.get(root_pid) or ps.Process(root_pid)
|
|
147
|
+
# Always re-walk children on the (possibly cached) root — new
|
|
148
|
+
# children can appear between ticks even though the root itself
|
|
149
|
+
# is unchanged.
|
|
150
|
+
procs = [root] + root.children(recursive=True)
|
|
151
|
+
except Exception: # noqa: BLE001 — process gone / access denied
|
|
152
|
+
return []
|
|
153
|
+
out: list[dict] = []
|
|
154
|
+
live_pids: set[int] = set()
|
|
155
|
+
for p in procs:
|
|
156
|
+
try:
|
|
157
|
+
first_sighting = p.pid not in cache
|
|
158
|
+
proc = cache.get(p.pid, p)
|
|
159
|
+
cache[p.pid] = proc
|
|
160
|
+
live_pids.add(proc.pid)
|
|
161
|
+
with proc.oneshot():
|
|
162
|
+
cpu = proc.cpu_percent(interval=None)
|
|
163
|
+
out.append(
|
|
164
|
+
{
|
|
165
|
+
"pid": proc.pid,
|
|
166
|
+
"ppid": proc.ppid(),
|
|
167
|
+
"cmd": " ".join(proc.cmdline()[:3]) or proc.name(),
|
|
168
|
+
"cpu_percent": None if first_sighting else cpu,
|
|
169
|
+
"rss_bytes": proc.memory_info().rss,
|
|
170
|
+
"create_time": proc.create_time(),
|
|
171
|
+
"status": proc.status(),
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
except Exception: # noqa: BLE001 — a process may exit mid-inspection
|
|
175
|
+
continue
|
|
176
|
+
# Prune vanished processes so the cache never grows unbounded across a
|
|
177
|
+
# long-running attempt.
|
|
178
|
+
for pid in list(cache):
|
|
179
|
+
if pid not in live_pids:
|
|
180
|
+
del cache[pid]
|
|
181
|
+
return out
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class ResourceSampler:
|
|
185
|
+
"""Sample machine + process-tree telemetry for one launched attempt.
|
|
186
|
+
|
|
187
|
+
`start()` spawns a daemon thread that ticks immediately (a sub-period
|
|
188
|
+
command must still leave one sample) and then every `period_s`;
|
|
189
|
+
`stop()` wakes and joins it. Both are idempotent-enough for the SDK's
|
|
190
|
+
best-effort use: stop on a never-started sampler is a no-op.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
def __init__(self, output_dir: Path | str, root_pid: int, period_s: float = 2.0):
|
|
194
|
+
self.output_dir = Path(output_dir)
|
|
195
|
+
self.root_pid = int(root_pid)
|
|
196
|
+
self.period_s = period_s
|
|
197
|
+
self._stop = threading.Event()
|
|
198
|
+
self._thread: threading.Thread | None = None
|
|
199
|
+
# pid -> psutil.Process, reused across ticks so cpu_percent deltas
|
|
200
|
+
# are real (see _process_tree's docstring for why).
|
|
201
|
+
self._procs: dict[int, object] = {}
|
|
202
|
+
|
|
203
|
+
def start(self) -> None:
|
|
204
|
+
self._thread = threading.Thread(target=self._run, daemon=True)
|
|
205
|
+
self._thread.start()
|
|
206
|
+
|
|
207
|
+
def stop(self) -> None:
|
|
208
|
+
self._stop.set()
|
|
209
|
+
if self._thread is not None:
|
|
210
|
+
self._thread.join(timeout=self.period_s + 1)
|
|
211
|
+
self._thread = None
|
|
212
|
+
|
|
213
|
+
def _run(self) -> None:
|
|
214
|
+
# tick FIRST, then wait: a command that finishes inside one period
|
|
215
|
+
# still gets a sample, and stop() during the wait exits promptly.
|
|
216
|
+
while True:
|
|
217
|
+
try:
|
|
218
|
+
self._tick()
|
|
219
|
+
except Exception: # noqa: BLE001 — total by contract (module docstring)
|
|
220
|
+
pass
|
|
221
|
+
if self._stop.wait(self.period_s):
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
def _tick(self) -> None:
|
|
225
|
+
sample = {
|
|
226
|
+
"ts": time.time(),
|
|
227
|
+
"machine": _machine_sample(),
|
|
228
|
+
"processes": _process_tree(self.root_pid, self._procs),
|
|
229
|
+
}
|
|
230
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
231
|
+
with open(self.output_dir / "telemetry.jsonl", "a") as f:
|
|
232
|
+
f.write(json.dumps(sample) + "\n")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""FlashRuntime strategy planner — deterministic, explainable, framework-free.
|
|
2
|
+
|
|
3
|
+
Public entry point:
|
|
4
|
+
|
|
5
|
+
from flashruntime.protocol.plan_v1alpha1 import PlanRequest
|
|
6
|
+
from flashruntime.planner import plan
|
|
7
|
+
|
|
8
|
+
report = plan(request) # PlanReport
|
|
9
|
+
print(render(report)) # human-readable explanation
|
|
10
|
+
|
|
11
|
+
Pipeline (ADR-0003): resolve the request → generate candidates from a
|
|
12
|
+
curated menu per workload class → evaluate each with static estimators
|
|
13
|
+
(memory, communication, time/cost) → apply hard constraints → rank by the
|
|
14
|
+
user's objective → mint a frozen, backend-neutral StrategyPlan and report
|
|
15
|
+
every candidate's verdict with its arithmetic.
|
|
16
|
+
|
|
17
|
+
Hard rule: this package imports **no** ML framework. It reasons about
|
|
18
|
+
PyTorch, Ray, Transformers, and DeepSpeed by name and by number — strategy
|
|
19
|
+
compilers translate the plan into real configuration at execution time.
|
|
20
|
+
|
|
21
|
+
Honesty contract: every estimate is labeled `basis: static` until a
|
|
22
|
+
profiling run or ledger history replaces it; estimates the inputs can't
|
|
23
|
+
support are omitted, never invented; plans inside the memory caution band
|
|
24
|
+
are marked `profiling_required`.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from flashruntime.planner.candidates import generate
|
|
30
|
+
from flashruntime.planner.explain import render_report as render
|
|
31
|
+
from flashruntime.planner.resolve import request_digest
|
|
32
|
+
from flashruntime.planner.selector import select
|
|
33
|
+
from flashruntime.protocol.plan_v1alpha1 import PlanReport, PlanRequest
|
|
34
|
+
|
|
35
|
+
PLANNER_VERSION = "0.1.0"
|
|
36
|
+
|
|
37
|
+
__all__ = ["plan", "render", "PLANNER_VERSION"]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def plan(request: PlanRequest) -> PlanReport:
|
|
41
|
+
"""Evaluate a PlanRequest and return the full PlanReport.
|
|
42
|
+
|
|
43
|
+
Deterministic: identical requests (and planner version) produce an
|
|
44
|
+
identical report — the request digest in the output is the identity of
|
|
45
|
+
the decision.
|
|
46
|
+
"""
|
|
47
|
+
digest = request_digest(request, PLANNER_VERSION)
|
|
48
|
+
try:
|
|
49
|
+
evaluated, notes = generate(request)
|
|
50
|
+
except ValueError as exc:
|
|
51
|
+
return PlanReport(
|
|
52
|
+
planner_version=PLANNER_VERSION,
|
|
53
|
+
request_digest=digest,
|
|
54
|
+
no_valid_strategy_hint=str(exc),
|
|
55
|
+
)
|
|
56
|
+
return select(request, evaluated, PLANNER_VERSION, digest, notes)
|