dsh-runnpu 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/compute-mode/preset/agent.cordis.yml +368 -0
- package/compute-mode/preset/preset.yml +3 -0
- package/compute-mode/preset/skills/runnpu/SKILL.md +233 -0
- package/compute-mode/preset/skills/runnpu/VENDOR.txt +11 -0
- package/compute-mode/preset/skills/runnpu/examples/01-dev-environment.sh +26 -0
- package/compute-mode/preset/skills/runnpu/examples/02-training-job.sh +26 -0
- package/compute-mode/preset/skills/runnpu/examples/03-inference-service.sh +24 -0
- package/compute-mode/preset/skills/runnpu/examples/04-fractional-vnpu.sh +14 -0
- package/compute-mode/preset/skills/runnpu/examples/05-troubleshoot.sh +17 -0
- package/compute-mode/preset/skills/runnpu/references/admin.md +60 -0
- package/compute-mode/preset/skills/runnpu/references/data-sources.md +35 -0
- package/compute-mode/preset/skills/runnpu/references/templates-assets.md +61 -0
- package/compute-mode/preset/skills/runnpu/references/training-runs.md +56 -0
- package/compute-mode/preset/skills/runnpu/references/troubleshooting.md +75 -0
- package/compute-mode/preset/skills/runnpu/references/workload-types.md +47 -0
- package/cordis.patch.yml +80 -0
- package/dist/chunk-JLZMOSQC.mjs +1 -0
- package/dist/chunk-JUBIWGKW.mjs +4 -0
- package/dist/chunk-JZXLCA2E.mjs +1 -0
- package/dist/chunk-KQOFBYGE.mjs +9 -0
- package/dist/chunk-XMZG2NP7.mjs +2 -0
- package/dist/chunk-XQPU3AJN.mjs +1 -0
- package/dist/chunk-YKASEMP3.mjs +1 -0
- package/dist/chunk-YPL4VUYF.mjs +4 -0
- package/dist/compute-node.mjs +1 -0
- package/dist/compute-tools.mjs +41 -0
- package/dist/connectors.mjs +3 -0
- package/dist/dsh-runnpu.mjs +43 -0
- package/dist/json.mjs +1 -0
- package/dist/pins.mjs +1 -0
- package/dist/series.mjs +1 -0
- package/dist/training-node.mjs +2 -0
- package/dist/training-tools.mjs +30 -0
- package/dist/ui-host.mjs +1 -0
- package/lib/client.js +189 -0
- package/package.json +78 -0
- package/training-mode/preset/agent.cordis.yml +343 -0
- package/training-mode/preset/preset.yml +3 -0
- package/training-mode/preset/skills/runnpu/SKILL.md +233 -0
- package/training-mode/preset/skills/runnpu/VENDOR.txt +11 -0
- package/training-mode/preset/skills/runnpu/examples/01-dev-environment.sh +26 -0
- package/training-mode/preset/skills/runnpu/examples/02-training-job.sh +26 -0
- package/training-mode/preset/skills/runnpu/examples/03-inference-service.sh +24 -0
- package/training-mode/preset/skills/runnpu/examples/04-fractional-vnpu.sh +14 -0
- package/training-mode/preset/skills/runnpu/examples/05-troubleshoot.sh +17 -0
- package/training-mode/preset/skills/runnpu/references/admin.md +60 -0
- package/training-mode/preset/skills/runnpu/references/data-sources.md +35 -0
- package/training-mode/preset/skills/runnpu/references/templates-assets.md +61 -0
- package/training-mode/preset/skills/runnpu/references/training-runs.md +56 -0
- package/training-mode/preset/skills/runnpu/references/troubleshooting.md +75 -0
- package/training-mode/preset/skills/runnpu/references/workload-types.md +47 -0
- package/training-mode/preset/skills/swanlab-skill/LICENSE +21 -0
- package/training-mode/preset/skills/swanlab-skill/SKILL.md +163 -0
- package/training-mode/preset/skills/swanlab-skill/VENDOR.txt +9 -0
- package/training-mode/preset/skills/swanlab-skill/references/ANALYSIS_GUIDE.md +83 -0
- package/training-mode/preset/skills/swanlab-skill/references/CLI_REFERENCE.md +510 -0
- package/training-mode/preset/skills/swanlab-skill/references/SDK_QUICKSTART.md +341 -0
- package/training-mode/preset/skills/swanlab-skill/references/SWANLAB_CONCEPTS.md +523 -0
- package/training-mode/preset/skills/swanlab-skill/scripts/plot_metrics.py +353 -0
- package/training-mode/preset/skills/swanlab-skill/scripts/runs_benchmark.py +550 -0
- package/training-mode/preset/skills/training-research/SKILL.md +33 -0
- package/training-mode/preset/skills/training-research/references/pipeline.md +45 -0
- package/training-mode/preset/skills/training-research/references/research.md +76 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Cross-experiment benchmark plotter (runs_benchmark.py).
|
|
4
|
+
|
|
5
|
+
Fetch the same metric from multiple SwanLab experiments via the OOP Api,
|
|
6
|
+
overlay line charts (raw trend + running-best step line) for visual
|
|
7
|
+
comparison, and print a summary statistics table.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
# Compare one metric across 3 experiments (requires `swanlab login`)
|
|
11
|
+
python runs_benchmark.py user/proj/run1 user/proj/run2 user/proj/run3 -k loss
|
|
12
|
+
|
|
13
|
+
# Lower-is-better metric with custom labels
|
|
14
|
+
python runs_benchmark.py user/proj/run1 user/proj/run2 -k latency_ms \\
|
|
15
|
+
--direction lower --labels "v1,v2"
|
|
16
|
+
|
|
17
|
+
# Multiple keys (one subplot per key)
|
|
18
|
+
python runs_benchmark.py user/proj/run1 user/proj/run2 -k loss,acc
|
|
19
|
+
|
|
20
|
+
# Normalize x-axis to 0-100% for unequal-length experiments
|
|
21
|
+
python runs_benchmark.py user/proj/run1 user/proj/run2 -k loss --normalize
|
|
22
|
+
|
|
23
|
+
# Self-hosted server
|
|
24
|
+
python runs_benchmark.py user/proj/run1 user/proj/run2 -k loss \\
|
|
25
|
+
--host https://your-swanserver --api-key YOUR_KEY
|
|
26
|
+
|
|
27
|
+
# Benchmark from a pre-fetched JSON file (skip API calls entirely)
|
|
28
|
+
python runs_benchmark.py --data benchmark_data.json -k loss
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import json
|
|
35
|
+
import math
|
|
36
|
+
import sys
|
|
37
|
+
from dataclasses import dataclass
|
|
38
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
39
|
+
|
|
40
|
+
import matplotlib
|
|
41
|
+
|
|
42
|
+
matplotlib.use("Agg")
|
|
43
|
+
import matplotlib.pyplot as plt
|
|
44
|
+
import numpy as np
|
|
45
|
+
from swanlab.api import Api
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Data model
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class ExperimentSeries:
|
|
54
|
+
"""Metric time-series for a single experiment."""
|
|
55
|
+
|
|
56
|
+
name: str
|
|
57
|
+
path: str
|
|
58
|
+
steps: List[int]
|
|
59
|
+
values: List[float]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Data fetching — one Api call per experiment, no repeated requests
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _create_api(api_key: Optional[str] = None, host: Optional[str] = None) -> Api:
|
|
68
|
+
return Api(api_key=api_key, host=host)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def fetch_series(
|
|
72
|
+
path: str,
|
|
73
|
+
key: str,
|
|
74
|
+
sample: int = 1500,
|
|
75
|
+
api_key: Optional[str] = None,
|
|
76
|
+
host: Optional[str] = None,
|
|
77
|
+
) -> ExperimentSeries:
|
|
78
|
+
"""Fetch a single metric key from one experiment."""
|
|
79
|
+
api = _create_api(api_key, host)
|
|
80
|
+
experiment = api.run(path)
|
|
81
|
+
name = experiment.name
|
|
82
|
+
raw = experiment.metrics(keys=[key], sample=sample, ignore_timestamp=True)
|
|
83
|
+
steps, values = _extract(raw, key)
|
|
84
|
+
return ExperimentSeries(name=name, path=path, steps=steps, values=values)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _unwrap_envelope(data: Any) -> Any:
|
|
88
|
+
"""Unwrap the ``{"ok": ..., "errmsg": ..., "data": ...}`` envelope written by CLI ``--save``."""
|
|
89
|
+
if (
|
|
90
|
+
isinstance(data, dict)
|
|
91
|
+
and "list" not in data
|
|
92
|
+
and "keys" not in data
|
|
93
|
+
and isinstance(data.get("ok"), bool)
|
|
94
|
+
and "data" in data
|
|
95
|
+
):
|
|
96
|
+
return data.get("data") or {}
|
|
97
|
+
return data
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _extract(metric_data: Dict[str, Any], key: str) -> Tuple[List[int], List[float]]:
|
|
101
|
+
"""Pull (steps, values) from the raw API response dict.
|
|
102
|
+
|
|
103
|
+
Handles three response shapes:
|
|
104
|
+
- Current SDK format (``Experiment.metrics()`` / ``Metrics.json()``)::
|
|
105
|
+
``{"keys": [...], "list": [{"key": k, "metrics": [{"index": i, "data": v}, ...], ...}]}``
|
|
106
|
+
- CLI ``--save`` output — the same structure wrapped in an
|
|
107
|
+
``{"ok": ..., "errmsg": ..., "data": ...}`` envelope
|
|
108
|
+
- Legacy format (pre-0.9.0)::
|
|
109
|
+
``{key: [{"step": s, "value": v}, ...]}`` (flat list or nested under "data")
|
|
110
|
+
"""
|
|
111
|
+
data = _unwrap_envelope(metric_data)
|
|
112
|
+
if not isinstance(data, dict):
|
|
113
|
+
return [], []
|
|
114
|
+
|
|
115
|
+
points: Any = None
|
|
116
|
+
entries = data.get("list")
|
|
117
|
+
if isinstance(entries, list):
|
|
118
|
+
# Current SDK format: locate the entry matching the requested key
|
|
119
|
+
for entry in entries:
|
|
120
|
+
if isinstance(entry, dict) and entry.get("key") == key:
|
|
121
|
+
points = entry.get("metrics") or []
|
|
122
|
+
break
|
|
123
|
+
elif key in data:
|
|
124
|
+
# Legacy format: flat list or nested under "data"
|
|
125
|
+
entry = data[key]
|
|
126
|
+
if isinstance(entry, dict) and "data" in entry:
|
|
127
|
+
points = entry["data"]
|
|
128
|
+
elif isinstance(entry, list):
|
|
129
|
+
points = entry
|
|
130
|
+
|
|
131
|
+
steps: List[int] = []
|
|
132
|
+
values: List[float] = []
|
|
133
|
+
for pt in points or []:
|
|
134
|
+
if not isinstance(pt, dict):
|
|
135
|
+
continue
|
|
136
|
+
step = pt.get("step")
|
|
137
|
+
if step is None:
|
|
138
|
+
step = pt.get("index")
|
|
139
|
+
value = pt.get("value")
|
|
140
|
+
if value is None:
|
|
141
|
+
value = pt.get("data")
|
|
142
|
+
if step is None or value is None:
|
|
143
|
+
continue
|
|
144
|
+
try:
|
|
145
|
+
steps.append(int(step))
|
|
146
|
+
values.append(float(value))
|
|
147
|
+
except (TypeError, ValueError):
|
|
148
|
+
continue
|
|
149
|
+
return steps, values
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
# Statistics helpers
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def compute_stats(values: List[float]) -> Dict[str, float]:
|
|
158
|
+
"""Return {min, max, mean, std, last} for a value series."""
|
|
159
|
+
if not values:
|
|
160
|
+
return {}
|
|
161
|
+
arr = np.array(values)
|
|
162
|
+
return {
|
|
163
|
+
"min": float(np.min(arr)),
|
|
164
|
+
"max": float(np.max(arr)),
|
|
165
|
+
"mean": float(np.mean(arr)),
|
|
166
|
+
"std": float(np.std(arr)),
|
|
167
|
+
"last": float(arr[-1]),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def running_best(values: List[float], direction: str) -> List[float]:
|
|
172
|
+
"""
|
|
173
|
+
Cumulative best-so-far.
|
|
174
|
+
|
|
175
|
+
direction="higher" → running max (higher is better).
|
|
176
|
+
direction="lower" → running min (lower is better).
|
|
177
|
+
"""
|
|
178
|
+
if not values:
|
|
179
|
+
return []
|
|
180
|
+
best = float("-inf") if direction == "higher" else float("inf")
|
|
181
|
+
result: List[float] = []
|
|
182
|
+
for v in values:
|
|
183
|
+
if direction == "higher":
|
|
184
|
+
best = max(best, v)
|
|
185
|
+
else:
|
|
186
|
+
best = min(best, v)
|
|
187
|
+
result.append(best)
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def normalize_steps(steps: List[int]) -> List[float]:
|
|
192
|
+
"""Map steps to [0, 100] percentage scale."""
|
|
193
|
+
if not steps:
|
|
194
|
+
return []
|
|
195
|
+
total = max(steps) if max(steps) != 0 else 1
|
|
196
|
+
return [s / total * 100.0 for s in steps]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
# Summary table
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def print_comparison_table(
|
|
205
|
+
series_list: List[ExperimentSeries],
|
|
206
|
+
key: str,
|
|
207
|
+
) -> None:
|
|
208
|
+
"""Print a formatted comparison table to stdout."""
|
|
209
|
+
header = f"{'Experiment':<24s} | {'Steps':>5s} | {'Min':>10s} | {'Max':>10s} | {'Mean':>10s} | {'Std':>10s} | {'Last':>10s}"
|
|
210
|
+
sep = "-" * len(header)
|
|
211
|
+
print(f"\n Benchmark: {key}")
|
|
212
|
+
print(f" {sep}")
|
|
213
|
+
print(f" {header}")
|
|
214
|
+
print(f" {sep}")
|
|
215
|
+
for s in series_list:
|
|
216
|
+
stats = compute_stats(s.values)
|
|
217
|
+
if not stats:
|
|
218
|
+
print(
|
|
219
|
+
f" {s.name:<24s} | {'N/A':>5s} | {'N/A':>10s} | {'N/A':>10s} | {'N/A':>10s} | {'N/A':>10s} | {'N/A':>10s}"
|
|
220
|
+
)
|
|
221
|
+
continue
|
|
222
|
+
print(
|
|
223
|
+
f" {s.name:<24s} | {len(s.steps):>5d} | {stats['min']:>10.6g} | {stats['max']:>10.6g} "
|
|
224
|
+
f"| {stats['mean']:>10.6g} | {stats['std']:>10.6g} | {stats['last']:>10.6g}"
|
|
225
|
+
)
|
|
226
|
+
print(f" {sep}\n")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
# Plotting
|
|
231
|
+
# ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
# Default color cycle
|
|
234
|
+
_COLORS = [
|
|
235
|
+
"#1f77b4",
|
|
236
|
+
"#ff7f0e",
|
|
237
|
+
"#2ca02c",
|
|
238
|
+
"#d62728",
|
|
239
|
+
"#9467bd",
|
|
240
|
+
"#8c564b",
|
|
241
|
+
"#e377c2",
|
|
242
|
+
"#7f7f7f",
|
|
243
|
+
"#bcbd22",
|
|
244
|
+
"#17becf",
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _pick_color(idx: int) -> str:
|
|
249
|
+
return _COLORS[idx % len(_COLORS)]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def plot_benchmark(
|
|
253
|
+
all_series: Dict[str, List[ExperimentSeries]],
|
|
254
|
+
keys: List[str],
|
|
255
|
+
output: str,
|
|
256
|
+
title: Optional[str],
|
|
257
|
+
direction: str,
|
|
258
|
+
normalize: bool,
|
|
259
|
+
dpi: int,
|
|
260
|
+
) -> None:
|
|
261
|
+
"""
|
|
262
|
+
Render benchmark chart — one subplot per key, overlays all experiments.
|
|
263
|
+
|
|
264
|
+
Each subplot draws:
|
|
265
|
+
- A thin line for the raw metric trend.
|
|
266
|
+
- A thicker step line for the running-best envelope.
|
|
267
|
+
"""
|
|
268
|
+
n_keys = len(keys)
|
|
269
|
+
if n_keys == 0:
|
|
270
|
+
print("No keys to plot.", file=sys.stderr)
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
ncols = min(n_keys, 2)
|
|
274
|
+
nrows = math.ceil(n_keys / ncols)
|
|
275
|
+
|
|
276
|
+
fig, axes = plt.subplots(nrows, ncols, figsize=(9 * ncols, 5.5 * nrows), squeeze=False)
|
|
277
|
+
fig.suptitle(
|
|
278
|
+
title or "Cross-Experiment Benchmark",
|
|
279
|
+
fontsize=14,
|
|
280
|
+
fontweight="bold",
|
|
281
|
+
y=0.98,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
for k_idx, key in enumerate(keys):
|
|
285
|
+
row_idx, col_idx = divmod(k_idx, ncols)
|
|
286
|
+
ax = axes[row_idx][col_idx]
|
|
287
|
+
series_list = all_series.get(key, [])
|
|
288
|
+
|
|
289
|
+
if not series_list or all(len(s.values) == 0 for s in series_list):
|
|
290
|
+
ax.set_title(key, fontsize=12)
|
|
291
|
+
ax.text(
|
|
292
|
+
0.5,
|
|
293
|
+
0.5,
|
|
294
|
+
"No data",
|
|
295
|
+
ha="center",
|
|
296
|
+
va="center",
|
|
297
|
+
transform=ax.transAxes,
|
|
298
|
+
fontsize=13,
|
|
299
|
+
color="gray",
|
|
300
|
+
)
|
|
301
|
+
ax.set_xlabel("step" if not normalize else "progress (%)")
|
|
302
|
+
ax.set_ylabel(key)
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
for exp_idx, s in enumerate(series_list):
|
|
306
|
+
if not s.steps:
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
x = normalize_steps(s.steps) if normalize else s.steps
|
|
310
|
+
color = _pick_color(exp_idx)
|
|
311
|
+
label_base = s.name
|
|
312
|
+
|
|
313
|
+
# Raw trend line
|
|
314
|
+
ax.plot(
|
|
315
|
+
x,
|
|
316
|
+
s.values,
|
|
317
|
+
linewidth=1.0,
|
|
318
|
+
alpha=0.6,
|
|
319
|
+
color=color,
|
|
320
|
+
label=label_base,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Running-best step line
|
|
324
|
+
rb = running_best(s.values, direction)
|
|
325
|
+
ax.step(
|
|
326
|
+
x,
|
|
327
|
+
rb,
|
|
328
|
+
where="post",
|
|
329
|
+
linewidth=2.2,
|
|
330
|
+
alpha=0.85,
|
|
331
|
+
color=color,
|
|
332
|
+
linestyle="--",
|
|
333
|
+
label=f"{label_base} (best)",
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Last-point marker
|
|
337
|
+
ax.scatter(
|
|
338
|
+
[x[-1]],
|
|
339
|
+
[s.values[-1]],
|
|
340
|
+
s=24,
|
|
341
|
+
color=color,
|
|
342
|
+
zorder=5,
|
|
343
|
+
edgecolors="white",
|
|
344
|
+
linewidths=0.6,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
ax.set_title(key, fontsize=12)
|
|
348
|
+
ax.set_xlabel("step" if not normalize else "progress (%)", fontsize=11)
|
|
349
|
+
ax.set_ylabel(key, fontsize=11)
|
|
350
|
+
ax.grid(True, alpha=0.3)
|
|
351
|
+
|
|
352
|
+
# De-duplicate legend entries: keep only one per experiment (raw line),
|
|
353
|
+
# add a single generic "best" proxy for the step lines.
|
|
354
|
+
handles, labels = ax.get_legend_handles_labels()
|
|
355
|
+
seen: set[str] = set()
|
|
356
|
+
unique_handles = []
|
|
357
|
+
unique_labels = []
|
|
358
|
+
best_proxy = None
|
|
359
|
+
for h, lbl in zip(handles, labels):
|
|
360
|
+
if lbl.endswith(" (best)"):
|
|
361
|
+
if best_proxy is None:
|
|
362
|
+
best_proxy = h
|
|
363
|
+
continue
|
|
364
|
+
if lbl not in seen:
|
|
365
|
+
seen.add(lbl)
|
|
366
|
+
unique_handles.append(h)
|
|
367
|
+
unique_labels.append(lbl)
|
|
368
|
+
|
|
369
|
+
# Append a single "running best" entry
|
|
370
|
+
if best_proxy is not None:
|
|
371
|
+
unique_handles.append(best_proxy)
|
|
372
|
+
unique_labels.append("running best (--)")
|
|
373
|
+
ax.legend(unique_handles, unique_labels, loc="best", frameon=True, fontsize=8)
|
|
374
|
+
|
|
375
|
+
# Stats annotation box
|
|
376
|
+
lines: List[str] = []
|
|
377
|
+
for s in series_list:
|
|
378
|
+
stats = compute_stats(s.values)
|
|
379
|
+
if stats:
|
|
380
|
+
lines.append(f"{s.name}: last={stats['last']:.4g} mean={stats['mean']:.4g}")
|
|
381
|
+
if lines:
|
|
382
|
+
ax.text(
|
|
383
|
+
0.02,
|
|
384
|
+
0.02,
|
|
385
|
+
"\n".join(lines),
|
|
386
|
+
transform=ax.transAxes,
|
|
387
|
+
fontsize=7,
|
|
388
|
+
verticalalignment="bottom",
|
|
389
|
+
fontfamily="monospace",
|
|
390
|
+
bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow", alpha=0.8),
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Hide unused subplots
|
|
394
|
+
for idx in range(n_keys, nrows * ncols):
|
|
395
|
+
r, c = divmod(idx, ncols)
|
|
396
|
+
axes[r][c].set_visible(False)
|
|
397
|
+
|
|
398
|
+
plt.tight_layout(rect=(0, 0, 1, 0.95))
|
|
399
|
+
fig.savefig(output, dpi=dpi, bbox_inches="tight")
|
|
400
|
+
plt.close(fig)
|
|
401
|
+
print(f"Chart saved to: {output}")
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
# ---------------------------------------------------------------------------
|
|
405
|
+
# CLI
|
|
406
|
+
# ---------------------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def parse_args() -> argparse.Namespace:
|
|
410
|
+
parser = argparse.ArgumentParser(
|
|
411
|
+
description="Cross-experiment benchmark: compare the same metric across multiple runs.",
|
|
412
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
413
|
+
epilog=__doc__,
|
|
414
|
+
)
|
|
415
|
+
parser.add_argument(
|
|
416
|
+
"paths",
|
|
417
|
+
nargs="*",
|
|
418
|
+
help="Experiment paths (username/project/run_id). Optional when --data is used.",
|
|
419
|
+
)
|
|
420
|
+
parser.add_argument(
|
|
421
|
+
"--keys",
|
|
422
|
+
"-k",
|
|
423
|
+
required=True,
|
|
424
|
+
help="Comma-separated metric keys to compare, e.g. 'loss,acc'.",
|
|
425
|
+
)
|
|
426
|
+
parser.add_argument(
|
|
427
|
+
"--labels",
|
|
428
|
+
default=None,
|
|
429
|
+
help="Comma-separated display names for each experiment (matched by order).",
|
|
430
|
+
)
|
|
431
|
+
parser.add_argument(
|
|
432
|
+
"--sample",
|
|
433
|
+
"-s",
|
|
434
|
+
type=int,
|
|
435
|
+
default=1500,
|
|
436
|
+
help="Sample size per metric (default: 1500).",
|
|
437
|
+
)
|
|
438
|
+
parser.add_argument(
|
|
439
|
+
"--direction",
|
|
440
|
+
choices=["higher", "lower"],
|
|
441
|
+
default="higher",
|
|
442
|
+
help="Direction for 'running best': 'higher' (max) or 'lower' (min). Default: higher.",
|
|
443
|
+
)
|
|
444
|
+
parser.add_argument(
|
|
445
|
+
"--normalize",
|
|
446
|
+
action="store_true",
|
|
447
|
+
help="Normalize x-axis to 0-100%% for unequal-length experiments.",
|
|
448
|
+
)
|
|
449
|
+
parser.add_argument(
|
|
450
|
+
"--output",
|
|
451
|
+
"-o",
|
|
452
|
+
default="benchmark.png",
|
|
453
|
+
help="Output image path (default: benchmark.png).",
|
|
454
|
+
)
|
|
455
|
+
parser.add_argument("--title", "-t", default=None, help="Chart title (default: auto).")
|
|
456
|
+
parser.add_argument("--dpi", type=int, default=150, help="Image DPI (default: 150).")
|
|
457
|
+
parser.add_argument("--api-key", default=None, help="SwanLab API key (or use swanlab login).")
|
|
458
|
+
parser.add_argument(
|
|
459
|
+
"--data",
|
|
460
|
+
default=None,
|
|
461
|
+
help="Path to a JSON file with pre-fetched benchmark data (SDK output, CLI --save files, or legacy format). Skips API calls.",
|
|
462
|
+
)
|
|
463
|
+
parser.add_argument("--host", default=None, help="SwanLab API host URL (for self-hosted).")
|
|
464
|
+
return parser.parse_args()
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def main() -> int:
|
|
468
|
+
args = parse_args()
|
|
469
|
+
keys = [k.strip() for k in args.keys.split(",") if k.strip()]
|
|
470
|
+
|
|
471
|
+
if not keys:
|
|
472
|
+
print("Error: --keys must contain at least one key.", file=sys.stderr)
|
|
473
|
+
return 1
|
|
474
|
+
|
|
475
|
+
if not args.paths and not args.data:
|
|
476
|
+
print(
|
|
477
|
+
"Error: provide either PATH arguments or --data <json_file>.",
|
|
478
|
+
file=sys.stderr,
|
|
479
|
+
)
|
|
480
|
+
return 1
|
|
481
|
+
|
|
482
|
+
all_series: Dict[str, List[ExperimentSeries]] = {k: [] for k in keys}
|
|
483
|
+
|
|
484
|
+
if args.data:
|
|
485
|
+
# Load pre-fetched benchmark data from JSON
|
|
486
|
+
with open(args.data, "r", encoding="utf-8") as f:
|
|
487
|
+
raw_data = json.load(f)
|
|
488
|
+
# Expected structure (current SDK format):
|
|
489
|
+
# {"experiments": [{"name": "...", "path": "...", "metrics": {"keys": [...], "list": [{"key": k, "metrics": [{"index": i, "data": v}, ...]}]}}]}
|
|
490
|
+
# Legacy per-key format ({<key>: [{"step": s, "value": v}, ...]}) and CLI
|
|
491
|
+
# --save envelopes ({"ok", "errmsg", "data"}) are also accepted.
|
|
492
|
+
experiments = raw_data if isinstance(raw_data, list) else raw_data.get("experiments", [raw_data])
|
|
493
|
+
for exp in experiments:
|
|
494
|
+
name = exp.get("name", "unknown")
|
|
495
|
+
path = exp.get("path", "")
|
|
496
|
+
metrics = exp.get("metrics", exp) # fallback: top-level dict if flat
|
|
497
|
+
for key in keys:
|
|
498
|
+
steps, values = _extract(metrics, key)
|
|
499
|
+
all_series[key].append(ExperimentSeries(name=name, path=path, steps=steps, values=values))
|
|
500
|
+
print(f"Loaded benchmark data from: {args.data}")
|
|
501
|
+
else:
|
|
502
|
+
# Fetch from API
|
|
503
|
+
custom_labels: List[str] = []
|
|
504
|
+
if args.labels:
|
|
505
|
+
custom_labels = [lbl.strip() for lbl in args.labels.split(",")]
|
|
506
|
+
if len(custom_labels) != len(args.paths):
|
|
507
|
+
print(
|
|
508
|
+
f"Error: --labels has {len(custom_labels)} items but {len(args.paths)} paths given.",
|
|
509
|
+
file=sys.stderr,
|
|
510
|
+
)
|
|
511
|
+
return 1
|
|
512
|
+
|
|
513
|
+
api = _create_api(args.api_key, args.host)
|
|
514
|
+
|
|
515
|
+
for i, path in enumerate(args.paths):
|
|
516
|
+
print(f"Fetching [{i + 1}/{len(args.paths)}] {path} ...")
|
|
517
|
+
experiment = api.run(path)
|
|
518
|
+
if not experiment.run_id:
|
|
519
|
+
print(
|
|
520
|
+
f"Error: Failed to fetch experiment at path '{path}'. Please verify the path and credentials.",
|
|
521
|
+
file=sys.stderr,
|
|
522
|
+
)
|
|
523
|
+
return 1
|
|
524
|
+
name = custom_labels[i] if custom_labels else experiment.name
|
|
525
|
+
raw = experiment.metrics(keys=keys, sample=args.sample, ignore_timestamp=True)
|
|
526
|
+
|
|
527
|
+
for key in keys:
|
|
528
|
+
steps, values = _extract(raw, key)
|
|
529
|
+
all_series[key].append(ExperimentSeries(name=name, path=path, steps=steps, values=values))
|
|
530
|
+
|
|
531
|
+
# Print comparison tables
|
|
532
|
+
for key in keys:
|
|
533
|
+
print_comparison_table(all_series[key], key)
|
|
534
|
+
|
|
535
|
+
# Plot
|
|
536
|
+
plot_benchmark(
|
|
537
|
+
all_series=all_series,
|
|
538
|
+
keys=keys,
|
|
539
|
+
output=args.output,
|
|
540
|
+
title=args.title,
|
|
541
|
+
direction=args.direction,
|
|
542
|
+
normalize=args.normalize,
|
|
543
|
+
dpi=args.dpi,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
return 0
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
if __name__ == "__main__":
|
|
550
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: training-research
|
|
3
|
+
description: >
|
|
4
|
+
训练工程的方法论:把本地工程在算力集群上跑通的链路纪律(上传/依赖/后台运行/密钥/
|
|
5
|
+
双信号源监控),以及自动超参研究的完整闭环(看曲线诊断 → 单变量实验 → 跨 run 对比 →
|
|
6
|
+
promote 赢家,含长程 schedule 与并行实验纪律)。平台无关:适用于任何算力平台 ×
|
|
7
|
+
任何实验跟踪平台的组合。当用户要「帮我跑起来」「自动调参 / 自动优化 / 帮我研究怎么调」
|
|
8
|
+
「对比这几轮实验」「曲线不对劲怎么办」时使用。平台具体操作(RunNPU CLI、昇腾炸点)
|
|
9
|
+
见 runnpu skill;跟踪平台 SDK/CLI 细节见 swanlab-skill。
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Training Research Skill
|
|
13
|
+
|
|
14
|
+
训练相关的**方法论**都在这里——它回答"该按什么纪律做",不回答"在这个平台上按哪个键"。
|
|
15
|
+
后者属于平台 skill(runnpu / swanlab-skill),本 skill 与它们交叉引用、互不复制。
|
|
16
|
+
|
|
17
|
+
## 路由
|
|
18
|
+
|
|
19
|
+
| 用户想要… | 读这份 |
|
|
20
|
+
|---|---|
|
|
21
|
+
| 把一个训练工程搬上集群跑通 | `references/pipeline.md`(链路纪律:上传 / 依赖 / 后台 / 密钥 / 监控) |
|
|
22
|
+
| 自动调参、优化超参、"还能更好吗" | `references/research.md`(诊断规则表 + 实验纪律 + 停止准则) |
|
|
23
|
+
| 训练要跑几小时/几天 | `references/research.md` §长程闭环(schedule 纪律) |
|
|
24
|
+
| 多卡并行做扫描 | `references/research.md` §并行实验 |
|
|
25
|
+
| 对比几轮实验、判赢家 | **先用 `training_compare` 工具**(确定性算术),解释再回到规则表 |
|
|
26
|
+
|
|
27
|
+
## 三条贯穿性的红线
|
|
28
|
+
|
|
29
|
+
1. **判断依据永远是跟踪平台上的曲线数据**,不是感觉;而"谁赢了赢多少"是算术——
|
|
30
|
+
有 `training_compare` 工具就用它,模型不徒手比数字。
|
|
31
|
+
2. **大段曲线数字不进上下文**:先出图/出统计表,再做判断。
|
|
32
|
+
3. **每个动作留痕**:假设与结论进 `.train/` 账本(`training_run_update` 记
|
|
33
|
+
decision / note),三周后仍能回答"当时为什么这么调"。
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# 跑通链路的纪律(本地工程 → 集群 → 曲线上报)
|
|
2
|
+
|
|
3
|
+
平台无关的部分在这里;「在 RunNPU 上具体敲什么命令、昇腾有哪些炸点」见 runnpu skill
|
|
4
|
+
的 `references/training-runs.md` 与 `references/troubleshooting.md`。
|
|
5
|
+
|
|
6
|
+
## 0. 先读工程自述,再动平台
|
|
7
|
+
|
|
8
|
+
README / CLAUDE.md / requirements.txt 是第一信息源——入口脚本、超参入口、数据与权重
|
|
9
|
+
是否内置、框架适配是否做过,**工程说了的不要自己猜**。
|
|
10
|
+
|
|
11
|
+
## 1. 工程搬运
|
|
12
|
+
|
|
13
|
+
- **会话里有 `training_upload` 工具就用它**,不要自己拼 scp/rsync:排除规则
|
|
14
|
+
(secrets / .git / 数据集)、传输方式探测、跨平台归档坑都在工具里作为机制存在,
|
|
15
|
+
且返回 `codeRev`——"这次跑的是哪份代码"三周后靠它回答。无该工具时的手工路径
|
|
16
|
+
见 runnpu skill。
|
|
17
|
+
- **代码和数据集是两条道**:数据集一次落盘(数据卷/工作盘),代码单独同步——
|
|
18
|
+
分开的原因是**别跟着代码反复传**,不是带宽。
|
|
19
|
+
- 大依赖(torch 级)优先预置:训练集群到公网包仓的出口往往只有几十 KiB/s,
|
|
20
|
+
不要指望容器里现场下载。
|
|
21
|
+
|
|
22
|
+
## 2. 运行
|
|
23
|
+
|
|
24
|
+
- **长训练必须放后台**(`nohup … &`),远程会话断开不能连累训练进程。
|
|
25
|
+
- **密钥注入原则**(跟踪平台 key / S3 / HF token…):只以环境变量形式在创建负载的
|
|
26
|
+
那一刻注入,值取自本地凭据;**绝不**写进工程文件、不落盘、不在命令输出里回显。
|
|
27
|
+
有 `training_submit` 工具时它就是这条原则的机制化——key 不经过工具调用参数,
|
|
28
|
+
不进会话日志。
|
|
29
|
+
- **跟踪 SDK 会在工作目录写本地日志**(如 swanlab 的 `swanlog/`):从只读挂载里
|
|
30
|
+
跑训练会直接 OSError 秒退——把日志目录指到可写处(如 `SWANLAB_LOG_DIR=/tmp/swanlog`),
|
|
31
|
+
或在可写目录运行、代码从只读卷 import。
|
|
32
|
+
- **跟踪 SDK 版本要和部署契约对齐**:本部署按 `SWANLAB_EXP_NAME`/`SWANLAB_NAME`
|
|
33
|
+
注入实验名并按名反查 run,容器内固定 `swanlab>=0.9,<0.10`(细节见 swanlab-skill)。
|
|
34
|
+
|
|
35
|
+
## 3. 监控与健康判断(双信号源)
|
|
36
|
+
|
|
37
|
+
- **信号源 A**:实验跟踪平台——在本地用同一个 key 经其 API 拉指标,不用进容器。
|
|
38
|
+
- **信号源 B**:平台的容器日志(命令见 runnpu skill)——A 断了用 B 兜底。
|
|
39
|
+
- 经验口径:loss 前几个 epoch 应明显下降;**NaN / 持续不降** → 停下检查 lr 与数据,
|
|
40
|
+
别硬等;**长时间无输出 ≠ 卡死**——加速器首个 step 常在编译算子,分钟级属正常,
|
|
41
|
+
日志有周期性输出即健康。
|
|
42
|
+
- **「秒级完成」是假象的高发区**:作业几十秒就 completed 且跟踪平台上没有 run,
|
|
43
|
+
优先怀疑**设备降级**(加速器运行时没起来、退化 CPU 跑完)或依赖没带上——先查
|
|
44
|
+
设备探测日志,别急着重提。平台侧常见成因(entrypoint 覆盖丢环境装配等)见
|
|
45
|
+
runnpu skill 的"假象"一节。
|