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,169 @@
1
+ """Deterministic selection: filter → rank by objective → mint the plan.
2
+
3
+ Identical inputs always produce the identical plan (the report carries the
4
+ planner version and a request digest so decisions are reproducible and
5
+ auditable — same principle as the recovery policy engine).
6
+
7
+ Ranking is lexicographic per objective mode. Hard constraints (deadline,
8
+ budget, allow_* flags) were already applied as `rejected_policy` during
9
+ evaluation — the ranker only orders survivors:
10
+
11
+ cheapest : (cost, time) — unknown values sort last, never win by omission
12
+ fastest : (time, cost)
13
+ balanced : cost first (deadline is already a hard gate), time tiebreak
14
+ reliable : fewest moving parts — smallest world size, no offload, no
15
+ quantization, most memory headroom
16
+
17
+ When nothing survives, the report still answers usefully: the nearest-miss
18
+ hint names the minimal relaxation that would unlock a plan.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import math
24
+
25
+ from flashruntime.planner.candidates import Evaluated
26
+ from flashruntime.protocol.plan_v1alpha1 import (
27
+ CheckpointPolicy,
28
+ PlanReport,
29
+ PlanRequest,
30
+ StrategyPlan,
31
+ )
32
+
33
+ _INF = math.inf
34
+
35
+
36
+ def _cost(e: Evaluated) -> float:
37
+ return e.verdict.est_cost_usd.value if e.verdict.est_cost_usd else _INF
38
+
39
+
40
+ def _time(e: Evaluated) -> float:
41
+ return e.verdict.est_time_min.value if e.verdict.est_time_min else _INF
42
+
43
+
44
+ def _rank_key(e: Evaluated, mode: str):
45
+ if mode == "cheapest":
46
+ return (_cost(e), _time(e), e.verdict.workers)
47
+ if mode == "fastest":
48
+ return (_time(e), _cost(e), e.verdict.workers)
49
+ if mode == "reliable":
50
+ return (
51
+ e.verdict.workers,
52
+ 0 if e.offload == "none" else 1,
53
+ 0 if e.quantization is None else 1,
54
+ -e.headroom_gb,
55
+ )
56
+ # balanced
57
+ return (_cost(e), _time(e), e.verdict.workers)
58
+
59
+
60
+ def select(
61
+ req: PlanRequest,
62
+ evaluated: list[Evaluated],
63
+ planner_version: str,
64
+ request_digest: str,
65
+ notes: list[str],
66
+ ) -> PlanReport:
67
+ feasible = [e for e in evaluated if e.verdict.status == "feasible"]
68
+ report = PlanReport(
69
+ planner_version=planner_version,
70
+ request_digest=request_digest,
71
+ candidates=[e.verdict for e in evaluated],
72
+ warnings=list(notes),
73
+ )
74
+
75
+ if not feasible:
76
+ report.no_valid_strategy_hint = _nearest_miss(req, evaluated)
77
+ return report
78
+
79
+ feasible.sort(key=lambda e: _rank_key(e, req.objective.mode))
80
+ winner = feasible[0]
81
+ winner.verdict.status = "selected"
82
+ report.selected = _mint_plan(req, winner, planner_version, request_digest, feasible)
83
+ return report
84
+
85
+
86
+ def _mint_plan(
87
+ req: PlanRequest,
88
+ e: Evaluated,
89
+ planner_version: str,
90
+ request_digest: str,
91
+ feasible: list[Evaluated],
92
+ ) -> StrategyPlan:
93
+ v = e.verdict
94
+ because = list(v.reasons)
95
+ because.append(f"objective '{req.objective.mode}': best-ranked of {len(feasible)} feasible candidate(s)")
96
+ runner_up = feasible[1] if len(feasible) > 1 else None
97
+ if runner_up is not None:
98
+ because.append(
99
+ f"runner-up was '{runner_up.verdict.name}' "
100
+ f"(time {_fmt(runner_up.verdict.est_time_min and runner_up.verdict.est_time_min.value, 'min')}, "
101
+ f"cost {_fmt(runner_up.verdict.est_cost_usd and runner_up.verdict.est_cost_usd.value, 'usd')})"
102
+ )
103
+
104
+ checkpoint = None
105
+ if e.workload_mode == "coordinated_training":
106
+ # Static default; Young–Daly (τ* = √(2·C·MTBF)) needs a measured
107
+ # checkpoint duration and pool failure rate — profiling/ledger work.
108
+ checkpoint = CheckpointPolicy(
109
+ backend="pytorch_dcp",
110
+ interval_seconds=300,
111
+ note="static default; tune with Young–Daly once checkpoint duration is measured",
112
+ )
113
+
114
+ return StrategyPlan(
115
+ plan_id=f"pl_{request_digest}",
116
+ planner_version=planner_version,
117
+ workload_mode=e.workload_mode, # type: ignore[arg-type]
118
+ strategy_family=v.strategy_family,
119
+ launcher=e.launcher,
120
+ workers=v.workers,
121
+ gpus_per_worker=e.gpus_per_worker,
122
+ colocated=e.colocated,
123
+ precision=e.precision,
124
+ quantization=e.quantization,
125
+ peft=e.peft,
126
+ micro_batch_per_gpu=e.micro_batch,
127
+ grad_accum=e.grad_accum,
128
+ activation_checkpointing=e.act_ckpt,
129
+ offload=e.offload, # type: ignore[arg-type]
130
+ libraries=e.libraries,
131
+ checkpoint=checkpoint,
132
+ memory=v.memory,
133
+ est_time_min=v.est_time_min,
134
+ est_cost_usd=v.est_cost_usd,
135
+ scaling_efficiency=v.scaling_efficiency,
136
+ profiling_required=v.profiling_required,
137
+ selected_because=because,
138
+ )
139
+
140
+
141
+ def _nearest_miss(req: PlanRequest, evaluated: list[Evaluated]) -> str:
142
+ """Turn a dead end into the minimal unlocking relaxation."""
143
+ hints: list[str] = []
144
+ mem_missed = [e for e in evaluated if e.vram_deficit_gb > 0]
145
+ if mem_missed:
146
+ closest = min(mem_missed, key=lambda e: e.vram_deficit_gb)
147
+ hints.append(
148
+ f"closest candidate '{closest.verdict.name}' missed VRAM by "
149
+ f"{closest.vram_deficit_gb:.1f} GB/GPU — add GPUs or larger GPUs"
150
+ )
151
+ if not req.objective.allow_quantization:
152
+ hints.append("allowing quantization (objective.allow_quantization) may unlock QLoRA")
153
+ if not req.objective.allow_cpu_offload:
154
+ hints.append("allowing CPU offload (objective.allow_cpu_offload) may unlock ZeRO-3 offload")
155
+ policy_missed = [e for e in evaluated if e.verdict.status == "rejected_policy"]
156
+ if policy_missed:
157
+ hints.append(
158
+ f"{len(policy_missed)} candidate(s) were feasible but violated the "
159
+ "deadline/budget — relaxing objective constraints would unlock them"
160
+ )
161
+ if not hints:
162
+ hints.append("no candidate came close — this workload is outside the supported envelope")
163
+ return "; ".join(hints)
164
+
165
+
166
+ def _fmt(value: float | None, unit: str) -> str:
167
+ if value is None:
168
+ return "unknown"
169
+ return f"${value:.2f}" if unit == "usd" else f"{value:.0f} {unit}"
@@ -0,0 +1,81 @@
1
+ """Static time and cost estimation.
2
+
3
+ Transformer training throughput uses the standard FLOPs identity:
4
+ FLOPs per token (forward + backward) ≈ 6 · parameters
5
+ tokens/s/GPU ≈ MFU · peak_FLOPS / (6 · parameters)
6
+ with an assumed MFU (catalog.ASSUMED_MFU). Multiplied by GPU count and
7
+ scaling efficiency for the fleet rate; penalized for slow strategy knobs.
8
+ LoRA/QLoRA save optimizer *memory*, not backward FLOPs — activation
9
+ gradients still flow through every frozen layer, so 6·P stands.
10
+
11
+ Cost = wall-clock × GPUs × hourly rate (when the user provided one).
12
+ Everything here is `basis: static`; a profiling run replaces these numbers
13
+ with measurements, and honest absence beats invented precision — when
14
+ inputs are missing, estimates are omitted, never guessed silently.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from flashruntime.planner.catalog import ASSUMED_MFU
20
+ from flashruntime.protocol.plan_v1alpha1 import Estimate
21
+
22
+ # Multiplicative step-time penalties for strategy knobs. [assumption]
23
+ PENALTY_ACT_CKPT = 1.30 # recompute in backward
24
+ PENALTY_QLORA = 1.35 # dequantization overhead
25
+ PENALTY_CPU_OFFLOAD = 4.0 # optimizer step across PCIe — order-of-magnitude, profile it
26
+
27
+
28
+ def tokens_per_second_per_gpu(params: float, gpu_tflops: float) -> float:
29
+ """Static single-GPU throughput from the 6·P FLOPs identity."""
30
+ if gpu_tflops <= 0 or params <= 0:
31
+ return 0.0
32
+ return ASSUMED_MFU * gpu_tflops * 1e12 / (6.0 * params)
33
+
34
+
35
+ def compute_time_per_micro_step_s(
36
+ params: float, seq_len: int, micro_batch: int, gpu_tflops: float
37
+ ) -> float:
38
+ """Seconds of pure compute for one micro-batch on one GPU (feeds the
39
+ communication overlap model)."""
40
+ tps = tokens_per_second_per_gpu(params, gpu_tflops)
41
+ if tps <= 0:
42
+ return 0.0
43
+ return seq_len * micro_batch / tps
44
+
45
+
46
+ def training_time_minutes(
47
+ params: float,
48
+ train_tokens: float,
49
+ gpus: int,
50
+ gpu_tflops: float,
51
+ scaling_efficiency: float,
52
+ *,
53
+ activation_checkpointing: bool = False,
54
+ qlora: bool = False,
55
+ cpu_offload: bool = False,
56
+ ) -> Estimate | None:
57
+ """Wall-clock estimate for the whole run, or None when inputs are missing."""
58
+ tps = tokens_per_second_per_gpu(params, gpu_tflops)
59
+ if tps <= 0 or train_tokens <= 0:
60
+ return None
61
+ fleet = tps * max(1, gpus) * max(0.01, scaling_efficiency)
62
+ penalty = 1.0
63
+ notes = [f"MFU {ASSUMED_MFU} assumed"]
64
+ if activation_checkpointing:
65
+ penalty *= PENALTY_ACT_CKPT
66
+ notes.append("activation-checkpointing recompute +30%")
67
+ if qlora:
68
+ penalty *= PENALTY_QLORA
69
+ notes.append("QLoRA dequantization +35%")
70
+ if cpu_offload:
71
+ penalty *= PENALTY_CPU_OFFLOAD
72
+ notes.append("CPU-offload optimizer ×4 — profile before trusting")
73
+ minutes = train_tokens * penalty / fleet / 60.0
74
+ return Estimate(value=round(minutes, 1), unit="min", basis="static", note="; ".join(notes))
75
+
76
+
77
+ def cost_usd(time_min: Estimate | None, gpus: int, hourly_per_gpu: float | None) -> Estimate | None:
78
+ if time_min is None or hourly_per_gpu is None:
79
+ return None
80
+ usd = time_min.value / 60.0 * max(1, gpus) * hourly_per_gpu
81
+ return Estimate(value=round(usd, 2), unit="usd", basis="static", note=time_min.note)
@@ -0,0 +1,113 @@
1
+ """Profiling: measured numbers that replace the planner's assumptions.
2
+
3
+ The planner's tier-2 upgrade (FLASHRUNTIME_EVALUATION §F): run a few real
4
+ steps of the real workload and replace the `[assumption]` constants in
5
+ `planner/catalog.py` (activation bytes, MFU, offload penalty) with
6
+ measurements. A `ProfileResult` is born with `basis="profiled"` — the
7
+ whole honesty ladder (`static → profiled → ledger`) hangs on never faking
8
+ that field.
9
+
10
+ Isolation invariants (every implementation MUST honor all four — they are
11
+ what makes profiling safe to run against production jobs):
12
+ 1. Separate namespace: profile runs use their own run-id/prefix; they
13
+ never write under a real job's artifact prefix.
14
+ 2. No commits: a profile run never creates ArtifactRecords, never commits
15
+ checkpoints to the catalog, never counts as accepted work.
16
+ 3. Independent RNG: profiling must not consume the training run's seed
17
+ stream (the sgd_trainer's step-indexed batching makes this trivial —
18
+ keep that property in future trainers).
19
+ 4. Bounded cost: hard wall-clock + step budgets; a hung profile is
20
+ cancelled, and "no measurement" is reported honestly rather than a
21
+ fabricated number.
22
+
23
+ Measurement protocol (defaults from the evaluation): `warmup_steps=3`
24
+ (skip compile/cache effects), `measure_steps=20`, peak memory via the
25
+ framework's allocator stats plus process RSS, one checkpoint save/restore
26
+ cycle — which doubles as free validation of the checkpoint contract
27
+ before the real run depends on it.
28
+
29
+ Status: interface complete (final surface); first concrete implementation
30
+ targets the LoRA recipe (SPRINT_PLAN Days 8–10) — profile locally via the
31
+ subprocess runner before wiring cluster profiling.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from abc import ABC, abstractmethod
37
+ from typing import Literal, Protocol
38
+
39
+ from pydantic import BaseModel, Field
40
+
41
+ from flashruntime.protocol.plan_v1alpha1 import PlanRequest, StrategyPlan
42
+
43
+ __all__ = ["ProfileResult", "ProfileCache", "Profiler", "ProfileError"]
44
+
45
+
46
+ class ProfileError(Exception):
47
+ """The profile run could not produce trustworthy numbers (crashed,
48
+ exceeded budget, measured nonsense). Callers fall back to static
49
+ estimates — with the plan still labeled `basis: static`, never
50
+ upgraded on a failed profile."""
51
+
52
+
53
+ class ProfileResult(BaseModel):
54
+ """What a profile run measured. Every field is optional except the
55
+ bookkeeping — a partial profile is still useful (peak memory without
56
+ throughput beats assumptions), but absent numbers stay absent."""
57
+
58
+ basis: Literal["profiled"] = "profiled"
59
+ peak_vram_gb: float | None = Field(default=None, ge=0)
60
+ host_ram_gb: float | None = Field(default=None, ge=0)
61
+ step_time_s: float | None = Field(default=None, gt=0)
62
+ tokens_per_s: float | None = Field(default=None, gt=0)
63
+ dataloader_wait_fraction: float | None = Field(default=None, ge=0, le=1)
64
+ checkpoint_save_s: float | None = Field(default=None, ge=0)
65
+ checkpoint_restore_s: float | None = Field(default=None, ge=0)
66
+ checkpoint_size_gb: float | None = Field(default=None, ge=0)
67
+ measured_steps: int = Field(ge=1)
68
+ warmup_steps: int = Field(ge=0)
69
+ notes: list[str] = Field(default_factory=list)
70
+
71
+
72
+ class ProfileCache(Protocol):
73
+ """Profiles are expensive; identical questions must be answered once.
74
+
75
+ The cache key is the tuple that actually determines the numbers:
76
+ (model digest, strategy family + knobs, GPU class, framework versions,
77
+ seq-len bucket, batch bucket) — see FLASHRUNTIME_EVALUATION §F. Real
78
+ runs later back-feed the same table with `basis: ledger` at higher
79
+ trust; the cache interface stays identical.
80
+ """
81
+
82
+ def key(self, request: PlanRequest, plan: StrategyPlan) -> str: ...
83
+
84
+ def get(self, key: str) -> ProfileResult | None: ...
85
+
86
+ def put(self, key: str, result: ProfileResult) -> None: ...
87
+
88
+
89
+ class Profiler(ABC):
90
+ """Run a bounded, isolated measurement of one candidate plan.
91
+
92
+ Skip policy lives in the *caller* (the planner), not here: the planner
93
+ skips profiling when a cache hit exists, when static margins are
94
+ comfortable (≤ 0.6·VRAM), or when the profile would cost more than
95
+ 2–5% of the job budget. The profiler's only judgment is "did I measure
96
+ something trustworthy" — everything else is planning.
97
+ """
98
+
99
+ @abstractmethod
100
+ def profile(
101
+ self,
102
+ request: PlanRequest,
103
+ plan: StrategyPlan,
104
+ budget_seconds: float = 300.0,
105
+ ) -> ProfileResult:
106
+ """Execute warmup + measured steps of `plan` for `request`.
107
+
108
+ Inputs: the user's request (data/model identity), the candidate
109
+ plan (strategy + knobs to measure under), a hard wall-clock budget.
110
+ Output: a ProfileResult (partial allowed; see model notes).
111
+ Raises: ProfileError when nothing trustworthy was measured.
112
+ Must honor all four isolation invariants in the module docstring.
113
+ """
@@ -0,0 +1,18 @@
1
+ """Versioned public schemas shared by FlashNode and FlashML Cloud.
2
+
3
+ Current version: v1alpha1 (`flashruntime.protocol.v1alpha1`) — job
4
+ specification, job state, event vocabulary, artifact records, and node
5
+ registration/heartbeat messages. Every schema carries an explicit version
6
+ field; security-relevant fields fail closed on unknown values.
7
+
8
+ Planning contract: `flashruntime.protocol.plan_v1alpha1` — PlanRequest
9
+ (workload + resources + objective), StrategyPlan, PlanReport with candidate
10
+ verdicts and explanations.
11
+
12
+ Future (still scaffolds elsewhere): lease and attempt payloads, checkpoint
13
+ manifests.
14
+ """
15
+
16
+ from flashruntime.protocol import plan_v1alpha1, v1alpha1
17
+
18
+ __all__ = ["v1alpha1", "plan_v1alpha1"]