paramesh 1.1.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.
- paramesh/__init__.py +1 -0
- paramesh/benchmarks.py +291 -0
- paramesh/checkpoint.py +442 -0
- paramesh/cli.py +388 -0
- paramesh/config.py +35 -0
- paramesh/coordinator.py +465 -0
- paramesh/dashboard.py +214 -0
- paramesh/demo.py +514 -0
- paramesh/events.py +197 -0
- paramesh/journal.py +79 -0
- paramesh/local_launcher.py +85 -0
- paramesh/monitor.py +391 -0
- paramesh/operations.py +159 -0
- paramesh/py.typed +0 -0
- paramesh/recovery_benchmarks.py +151 -0
- paramesh/rpc/__init__.py +1 -0
- paramesh/rpc/training_pb2.py +106 -0
- paramesh/rpc/training_pb2.pyi +370 -0
- paramesh/rpc/training_pb2_grpc.py +771 -0
- paramesh/scaling_benchmarks.py +280 -0
- paramesh/security.py +158 -0
- paramesh/services.py +210 -0
- paramesh/shard.py +376 -0
- paramesh/state.py +75 -0
- paramesh/storage.py +197 -0
- paramesh/telemetry.py +357 -0
- paramesh/tensors.py +63 -0
- paramesh/web/app.js +312 -0
- paramesh/web/favicon.svg +1 -0
- paramesh/web/index.html +65 -0
- paramesh/web/style.css +160 -0
- paramesh/worker.py +356 -0
- paramesh/workload.py +166 -0
- paramesh-1.1.0.dist-info/METADATA +574 -0
- paramesh-1.1.0.dist-info/RECORD +39 -0
- paramesh-1.1.0.dist-info/WHEEL +5 -0
- paramesh-1.1.0.dist-info/entry_points.txt +3 -0
- paramesh-1.1.0.dist-info/licenses/LICENSE +201 -0
- paramesh-1.1.0.dist-info/top_level.txt +1 -0
paramesh/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""ParamMesh parameter-server training services."""
|
paramesh/benchmarks.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Sequential warm-process measurements with retained trial and RPC records."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from paramesh.demo import MODES, run_demo
|
|
18
|
+
from paramesh.workload import TrainingWorkload
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def hardware() -> dict[str, Any]:
|
|
22
|
+
result: dict[str, Any] = {
|
|
23
|
+
"os": platform.platform(),
|
|
24
|
+
"machine": platform.machine(),
|
|
25
|
+
"logical_cpu_count": os.cpu_count(),
|
|
26
|
+
"cpu_model": None,
|
|
27
|
+
"memory_bytes": None,
|
|
28
|
+
"clock": vars(time.get_clock_info("perf_counter")),
|
|
29
|
+
"load_isolation": "uncontrolled workstation; background services may affect results",
|
|
30
|
+
}
|
|
31
|
+
try:
|
|
32
|
+
if sys.platform == "win32":
|
|
33
|
+
inventory = subprocess.check_output(
|
|
34
|
+
[
|
|
35
|
+
"powershell",
|
|
36
|
+
"-NoProfile",
|
|
37
|
+
"-NonInteractive",
|
|
38
|
+
"-Command",
|
|
39
|
+
"@{cpu_model=@((Get-CimInstance Win32_Processor).Name);"
|
|
40
|
+
"memory_bytes=(Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory}"
|
|
41
|
+
" | ConvertTo-Json -Compress",
|
|
42
|
+
],
|
|
43
|
+
text=True,
|
|
44
|
+
timeout=15,
|
|
45
|
+
)
|
|
46
|
+
result.update(json.loads(inventory))
|
|
47
|
+
elif sys.platform == "linux":
|
|
48
|
+
result["cpu_model"] = next(
|
|
49
|
+
line.split(":", 1)[1].strip()
|
|
50
|
+
for line in Path("/proc/cpuinfo").read_text().splitlines()
|
|
51
|
+
if line.startswith("model name")
|
|
52
|
+
)
|
|
53
|
+
result["memory_bytes"] = os.sysconf("SC_PHYS_PAGES") * os.sysconf("SC_PAGE_SIZE")
|
|
54
|
+
except (OSError, ValueError, StopIteration, subprocess.SubprocessError) as error:
|
|
55
|
+
result["inventory_error"] = type(error).__name__
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _json(path: Path, value: Any) -> None:
|
|
60
|
+
path.write_text(
|
|
61
|
+
json.dumps(value, indent=2, allow_nan=False) + "\n", encoding="utf-8", newline="\n"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _summary(raw: dict[str, Any], measured: int, workers: int, shards: int) -> dict[str, Any]:
|
|
66
|
+
results = raw["worker_results"]
|
|
67
|
+
if len(results) != workers or any(len(item["losses"]) != measured for item in results):
|
|
68
|
+
raise ValueError("Measured worker iteration count does not match configuration")
|
|
69
|
+
windows = [item["measurement"] for item in results]
|
|
70
|
+
elapsed = max(item["end"] for item in windows) - min(item["start"] for item in windows)
|
|
71
|
+
if not np.isfinite(elapsed) or elapsed <= 0 or any(item["sample_drops"] for item in windows):
|
|
72
|
+
raise ValueError("Invalid or incomplete measurement window")
|
|
73
|
+
samples = [sample for item in windows for sample in item["rpc_samples"]]
|
|
74
|
+
for item in windows:
|
|
75
|
+
pushes = [
|
|
76
|
+
sample
|
|
77
|
+
for sample in item["rpc_samples"]
|
|
78
|
+
if sample["method"] == "PushGradients" and sample["status"] == "OK"
|
|
79
|
+
]
|
|
80
|
+
if len(pushes) != measured * shards:
|
|
81
|
+
raise ValueError("Successful push count does not match measured contributions")
|
|
82
|
+
row = {
|
|
83
|
+
"mode": raw["mode"],
|
|
84
|
+
"commit": raw["environment"]["commit"],
|
|
85
|
+
"source_dirty": raw["environment"]["source_dirty"],
|
|
86
|
+
"window_seconds": elapsed,
|
|
87
|
+
"worker_steps_per_second": measured * workers / elapsed,
|
|
88
|
+
"examples_per_second": measured * workers * raw["workload"]["samples_per_worker"] / elapsed,
|
|
89
|
+
"protobuf_bytes": sum(item["request_bytes"] + item["response_bytes"] for item in samples),
|
|
90
|
+
"rpc_calls": len(samples),
|
|
91
|
+
"rpc_failures": sum(item["status"] != "OK" for item in samples),
|
|
92
|
+
"loss_after_warmup": raw["warmup"]["loss"],
|
|
93
|
+
"final_loss": raw["final_loss"],
|
|
94
|
+
"max_parameter_error": raw["max_parameter_error"],
|
|
95
|
+
"shard_commit_deltas": ",".join(
|
|
96
|
+
str(after - before)
|
|
97
|
+
for before, after in zip(
|
|
98
|
+
raw["warmup"]["shard_versions"], raw["shard_versions"], strict=True
|
|
99
|
+
)
|
|
100
|
+
),
|
|
101
|
+
}
|
|
102
|
+
for label, method in (("push", "PushGradients"), ("pull", "PullParameters")):
|
|
103
|
+
durations = [
|
|
104
|
+
item["duration_seconds"]
|
|
105
|
+
for item in samples
|
|
106
|
+
if item["method"] == method and item["status"] == "OK"
|
|
107
|
+
]
|
|
108
|
+
if not durations or not all(np.isfinite(value) and value > 0 for value in durations):
|
|
109
|
+
raise ValueError("Missing or invalid successful RPC observations")
|
|
110
|
+
row[f"{label}_p95_seconds"] = float(np.percentile(durations, 95))
|
|
111
|
+
row[f"{label}_mean_request_bytes"] = float(
|
|
112
|
+
np.mean([item["request_bytes"] for item in samples if item["method"] == method])
|
|
113
|
+
)
|
|
114
|
+
return row
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def render(report: dict[str, Any], output: Path) -> None:
|
|
118
|
+
import matplotlib
|
|
119
|
+
|
|
120
|
+
matplotlib.use("Agg")
|
|
121
|
+
from matplotlib import pyplot as plt
|
|
122
|
+
|
|
123
|
+
figure, axes = plt.subplots(2, 2, figsize=(11, 8))
|
|
124
|
+
modes = report["configuration"]["modes"]
|
|
125
|
+
for axis, key, title, scale in zip(
|
|
126
|
+
axes.flat,
|
|
127
|
+
("examples_per_second", "push_p95_seconds", "pull_p95_seconds", "final_loss"),
|
|
128
|
+
(
|
|
129
|
+
"Measured examples / second",
|
|
130
|
+
"Successful push p95 · ms",
|
|
131
|
+
"Successful pull p95 · ms",
|
|
132
|
+
"Final full-dataset MSE",
|
|
133
|
+
),
|
|
134
|
+
(1, 1000, 1000, 1),
|
|
135
|
+
strict=True,
|
|
136
|
+
):
|
|
137
|
+
for index, mode in enumerate(modes):
|
|
138
|
+
values = [row[key] * scale for row in report["trials"] if row["mode"] == mode]
|
|
139
|
+
median = float(np.median(values))
|
|
140
|
+
axis.errorbar(
|
|
141
|
+
index,
|
|
142
|
+
median,
|
|
143
|
+
yerr=[[median - min(values)], [max(values) - median]],
|
|
144
|
+
fmt="_",
|
|
145
|
+
color="black",
|
|
146
|
+
capsize=5,
|
|
147
|
+
markersize=18,
|
|
148
|
+
)
|
|
149
|
+
offsets = np.linspace(-0.07, 0.07, len(values))
|
|
150
|
+
offsets -= offsets.mean()
|
|
151
|
+
axis.scatter(index + offsets, values, s=28)
|
|
152
|
+
axis.set_xticks(range(len(modes)), modes)
|
|
153
|
+
axis.set_xlim(-0.5, len(modes) - 0.5)
|
|
154
|
+
axis.set_title(title)
|
|
155
|
+
axis.set_ylim(bottom=0)
|
|
156
|
+
axis.grid(axis="y", alpha=0.2)
|
|
157
|
+
figure.suptitle("ParamMesh · uncompressed CPU measurements", fontsize=16)
|
|
158
|
+
figure.text(
|
|
159
|
+
0.5,
|
|
160
|
+
0.025,
|
|
161
|
+
"Dots: recorded trials; bars: median and observed range.\n"
|
|
162
|
+
"Equal worker iterations have different SGD-commit semantics across modes.\n"
|
|
163
|
+
"Warm-process window includes registration/channel setup; background load is uncontrolled.",
|
|
164
|
+
ha="center",
|
|
165
|
+
fontsize=9,
|
|
166
|
+
)
|
|
167
|
+
figure.tight_layout(rect=(0, 0.1, 1, 0.94))
|
|
168
|
+
try:
|
|
169
|
+
figure.savefig(output / "overview.png", dpi=160)
|
|
170
|
+
figure.savefig(output / "overview.svg")
|
|
171
|
+
finally:
|
|
172
|
+
plt.close(figure)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def run_benchmarks(
|
|
176
|
+
output: Path,
|
|
177
|
+
*,
|
|
178
|
+
modes: tuple[str, ...] = ("sync", "async", "bounded"),
|
|
179
|
+
repetitions: int = 3,
|
|
180
|
+
warmup_steps: int = 20,
|
|
181
|
+
measured_steps: int = 200,
|
|
182
|
+
workers: int = 2,
|
|
183
|
+
shards: int = 2,
|
|
184
|
+
hidden_width: int = 8,
|
|
185
|
+
workload: TrainingWorkload | None = None,
|
|
186
|
+
) -> dict[str, Any]:
|
|
187
|
+
counts = (repetitions, warmup_steps, measured_steps, workers, shards, hidden_width)
|
|
188
|
+
if (
|
|
189
|
+
any(type(value) is not int for value in counts)
|
|
190
|
+
or not 1 <= repetitions <= 30
|
|
191
|
+
or not 1 <= warmup_steps <= 1000
|
|
192
|
+
or not 1 <= measured_steps <= 5000
|
|
193
|
+
or not 1 <= workers <= 8
|
|
194
|
+
or not 1 <= shards <= 4
|
|
195
|
+
or not 1 <= hidden_width <= 32768
|
|
196
|
+
or not modes
|
|
197
|
+
or len(set(modes)) != len(modes)
|
|
198
|
+
or any(mode not in MODES for mode in modes)
|
|
199
|
+
):
|
|
200
|
+
raise ValueError("Invalid benchmark modes, repetitions, warm-up, steps or topology")
|
|
201
|
+
output.mkdir(parents=True, exist_ok=False)
|
|
202
|
+
root = Path(__file__).resolve().parents[2]
|
|
203
|
+
lock = root / "uv.lock"
|
|
204
|
+
report: dict[str, Any] = {
|
|
205
|
+
"schema_version": 1,
|
|
206
|
+
"status": "running",
|
|
207
|
+
"captured_at": datetime.now(UTC).isoformat(),
|
|
208
|
+
"configuration": {
|
|
209
|
+
"modes": list(modes),
|
|
210
|
+
"repetitions": repetitions,
|
|
211
|
+
"warmup_steps": warmup_steps,
|
|
212
|
+
"measured_steps": measured_steps,
|
|
213
|
+
"workers": workers,
|
|
214
|
+
"shards": shards,
|
|
215
|
+
"hidden_width": hidden_width,
|
|
216
|
+
"compression": "none",
|
|
217
|
+
},
|
|
218
|
+
"hardware": hardware(),
|
|
219
|
+
"lock_sha256": hashlib.sha256(lock.read_bytes()).hexdigest() if lock.exists() else None,
|
|
220
|
+
"trials": [],
|
|
221
|
+
"window": (
|
|
222
|
+
"warm processes, fresh measured session including channel setup, "
|
|
223
|
+
"registration/fencing and final heartbeat"
|
|
224
|
+
),
|
|
225
|
+
}
|
|
226
|
+
_json(output / "manifest.json", report)
|
|
227
|
+
try:
|
|
228
|
+
with (
|
|
229
|
+
(output / "rpc.csv").open("w", newline="", encoding="utf-8") as rpc_file,
|
|
230
|
+
(output / "trials.csv").open("w", newline="", encoding="utf-8") as trial_file,
|
|
231
|
+
):
|
|
232
|
+
rpc_writer = csv.DictWriter(
|
|
233
|
+
rpc_file,
|
|
234
|
+
fieldnames=[
|
|
235
|
+
"trial",
|
|
236
|
+
"mode",
|
|
237
|
+
"worker_id",
|
|
238
|
+
"method",
|
|
239
|
+
"status",
|
|
240
|
+
"duration_seconds",
|
|
241
|
+
"request_bytes",
|
|
242
|
+
"response_bytes",
|
|
243
|
+
],
|
|
244
|
+
)
|
|
245
|
+
rpc_writer.writeheader()
|
|
246
|
+
trial_writer = None
|
|
247
|
+
for repeat in range(repetitions):
|
|
248
|
+
order = modes[repeat % len(modes) :] + modes[: repeat % len(modes)]
|
|
249
|
+
for mode in order:
|
|
250
|
+
index = len(report["trials"])
|
|
251
|
+
raw = run_demo(
|
|
252
|
+
mode=mode,
|
|
253
|
+
steps=warmup_steps + measured_steps,
|
|
254
|
+
warmup_steps=warmup_steps,
|
|
255
|
+
workers=workers,
|
|
256
|
+
shards=shards,
|
|
257
|
+
hidden_width=hidden_width,
|
|
258
|
+
workload=workload,
|
|
259
|
+
output=output / f"trial-{index:03d}-{mode}.json",
|
|
260
|
+
)
|
|
261
|
+
row = {
|
|
262
|
+
"trial": index,
|
|
263
|
+
"repetition": repeat + 1,
|
|
264
|
+
**_summary(raw, measured_steps, workers, shards),
|
|
265
|
+
}
|
|
266
|
+
for worker in raw["worker_results"]:
|
|
267
|
+
for sample in worker["measurement"]["rpc_samples"]:
|
|
268
|
+
rpc_writer.writerow(
|
|
269
|
+
{
|
|
270
|
+
"trial": index,
|
|
271
|
+
"mode": mode,
|
|
272
|
+
"worker_id": worker["worker_id"],
|
|
273
|
+
**sample,
|
|
274
|
+
}
|
|
275
|
+
)
|
|
276
|
+
if trial_writer is None:
|
|
277
|
+
trial_writer = csv.DictWriter(trial_file, fieldnames=list(row))
|
|
278
|
+
trial_writer.writeheader()
|
|
279
|
+
trial_writer.writerow(row)
|
|
280
|
+
rpc_file.flush()
|
|
281
|
+
trial_file.flush()
|
|
282
|
+
report["trials"].append(row)
|
|
283
|
+
_json(output / "manifest.json", report)
|
|
284
|
+
render(report, output)
|
|
285
|
+
report["status"] = "complete"
|
|
286
|
+
except BaseException as error:
|
|
287
|
+
report.update(status="failed", failure_type=type(error).__name__)
|
|
288
|
+
_json(output / "manifest.json", report)
|
|
289
|
+
raise
|
|
290
|
+
_json(output / "manifest.json", report)
|
|
291
|
+
return report
|