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