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,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"]
|