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,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)