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,353 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
SwanLab metric line-chart plotter.
|
|
4
|
+
|
|
5
|
+
Fetch scalar metrics via the SwanLab OOP Api, then render line charts
|
|
6
|
+
with matplotlib. Each metric key gets its own subplot with trend line
|
|
7
|
+
and summary statistics (min / max / mean / std / last).
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
# Basic (requires `swanlab login` first)
|
|
11
|
+
python plot_metrics.py username/project_name/run_id --keys loss,acc
|
|
12
|
+
|
|
13
|
+
# Custom sample size and output path
|
|
14
|
+
python plot_metrics.py username/project_name/run_id --keys loss --sample 500 -o loss_chart.png
|
|
15
|
+
|
|
16
|
+
# Self-hosted server
|
|
17
|
+
python plot_metrics.py username/project_name/run_id --keys loss --host https://your-swanserver
|
|
18
|
+
|
|
19
|
+
# Explicit API key (skip login)
|
|
20
|
+
python plot_metrics.py username/project_name/run_id --keys loss --api-key YOUR_KEY
|
|
21
|
+
|
|
22
|
+
# Plot from a pre-fetched JSON file (skip API calls entirely)
|
|
23
|
+
python plot_metrics.py --data metrics.json --keys loss,acc -o chart.png
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import json
|
|
28
|
+
import math
|
|
29
|
+
import sys
|
|
30
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
31
|
+
|
|
32
|
+
import matplotlib
|
|
33
|
+
|
|
34
|
+
matplotlib.use("Agg")
|
|
35
|
+
import matplotlib.pyplot as plt
|
|
36
|
+
import numpy as np
|
|
37
|
+
from swanlab.api import Api
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Data fetching
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def fetch_metrics(
|
|
45
|
+
path: str,
|
|
46
|
+
keys: List[str],
|
|
47
|
+
sample: int = 1500,
|
|
48
|
+
api_key: Optional[str] = None,
|
|
49
|
+
host: Optional[str] = None,
|
|
50
|
+
) -> Dict[str, Any]:
|
|
51
|
+
"""Fetch scalar metric data via the SwanLab Api."""
|
|
52
|
+
api = Api(api_key=api_key, host=host)
|
|
53
|
+
experiment = api.run(path)
|
|
54
|
+
if not experiment.run_id:
|
|
55
|
+
raise ValueError(f"Failed to fetch experiment at path '{path}'. Please verify the path and credentials.")
|
|
56
|
+
return experiment.metrics(keys=keys, sample=sample, ignore_timestamp=True)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def fetch_summary(
|
|
60
|
+
path: str,
|
|
61
|
+
keys: List[str],
|
|
62
|
+
api_key: Optional[str] = None,
|
|
63
|
+
host: Optional[str] = None,
|
|
64
|
+
) -> Dict[str, Any]:
|
|
65
|
+
"""Fetch metric summary statistics via the SwanLab Api."""
|
|
66
|
+
api = Api(api_key=api_key, host=host)
|
|
67
|
+
experiment = api.run(path)
|
|
68
|
+
if not experiment.run_id:
|
|
69
|
+
raise ValueError(f"Failed to fetch experiment at path '{path}'. Please verify the path and credentials.")
|
|
70
|
+
return experiment.summary(keys=keys)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Data extraction — pull (steps, values) pairs from the raw API dict
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _unwrap_envelope(data: Any) -> Any:
|
|
79
|
+
"""Unwrap the ``{"ok": ..., "errmsg": ..., "data": ...}`` envelope written by CLI ``--save``."""
|
|
80
|
+
if (
|
|
81
|
+
isinstance(data, dict)
|
|
82
|
+
and "list" not in data
|
|
83
|
+
and "keys" not in data
|
|
84
|
+
and isinstance(data.get("ok"), bool)
|
|
85
|
+
and "data" in data
|
|
86
|
+
):
|
|
87
|
+
return data.get("data") or {}
|
|
88
|
+
return data
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def extract_series(metric_data: Dict[str, Any], key: str) -> Tuple[List[int], List[float]]:
|
|
92
|
+
"""
|
|
93
|
+
Extract (steps, values) for a single metric key.
|
|
94
|
+
|
|
95
|
+
Handles three response shapes:
|
|
96
|
+
- Current SDK format (``Experiment.metrics()`` / ``Metrics.json()``)::
|
|
97
|
+
``{"keys": [...], "list": [{"key": k, "metrics": [{"index": i, "data": v}, ...], ...}]}``
|
|
98
|
+
- CLI ``--save`` output — the same structure wrapped in an
|
|
99
|
+
``{"ok": ..., "errmsg": ..., "data": ...}`` envelope
|
|
100
|
+
- Legacy format (pre-0.9.0)::
|
|
101
|
+
``{key: [{"step": s, "value": v}, ...]}`` (flat list or nested under "data")
|
|
102
|
+
|
|
103
|
+
Data points use ``index``/``data`` field names in the current format and
|
|
104
|
+
``step``/``value`` in the legacy format; both are accepted.
|
|
105
|
+
"""
|
|
106
|
+
data = _unwrap_envelope(metric_data)
|
|
107
|
+
if not isinstance(data, dict):
|
|
108
|
+
return [], []
|
|
109
|
+
|
|
110
|
+
points: Any = None
|
|
111
|
+
entries = data.get("list")
|
|
112
|
+
if isinstance(entries, list):
|
|
113
|
+
# Current SDK format: locate the entry matching the requested key
|
|
114
|
+
for entry in entries:
|
|
115
|
+
if isinstance(entry, dict) and entry.get("key") == key:
|
|
116
|
+
points = entry.get("metrics") or []
|
|
117
|
+
break
|
|
118
|
+
elif key in data:
|
|
119
|
+
# Legacy format: flat list or nested under "data"
|
|
120
|
+
entry = data[key]
|
|
121
|
+
if isinstance(entry, dict) and "data" in entry:
|
|
122
|
+
points = entry["data"]
|
|
123
|
+
elif isinstance(entry, list):
|
|
124
|
+
points = entry
|
|
125
|
+
|
|
126
|
+
steps: List[int] = []
|
|
127
|
+
values: List[float] = []
|
|
128
|
+
for pt in points or []:
|
|
129
|
+
if not isinstance(pt, dict):
|
|
130
|
+
continue
|
|
131
|
+
step = pt.get("step")
|
|
132
|
+
if step is None:
|
|
133
|
+
step = pt.get("index")
|
|
134
|
+
value = pt.get("value")
|
|
135
|
+
if value is None:
|
|
136
|
+
value = pt.get("data")
|
|
137
|
+
if step is None or value is None:
|
|
138
|
+
continue
|
|
139
|
+
try:
|
|
140
|
+
steps.append(int(step))
|
|
141
|
+
values.append(float(value))
|
|
142
|
+
except (TypeError, ValueError):
|
|
143
|
+
continue
|
|
144
|
+
return steps, values
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
# Statistics helpers
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def compute_stats(values: List[float]) -> Dict[str, float]:
|
|
153
|
+
"""Compute basic descriptive statistics for a value series."""
|
|
154
|
+
if not values:
|
|
155
|
+
return {}
|
|
156
|
+
arr = np.array(values)
|
|
157
|
+
return {
|
|
158
|
+
"min": float(np.min(arr)),
|
|
159
|
+
"max": float(np.max(arr)),
|
|
160
|
+
"mean": float(np.mean(arr)),
|
|
161
|
+
"std": float(np.std(arr)),
|
|
162
|
+
"last": float(arr[-1]),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def format_stats_text(stats: Dict[str, float]) -> str:
|
|
167
|
+
"""Format statistics dict into a compact annotation string."""
|
|
168
|
+
if not stats:
|
|
169
|
+
return "N/A"
|
|
170
|
+
parts = [
|
|
171
|
+
f"min={stats['min']:.6g}",
|
|
172
|
+
f"max={stats['max']:.6g}",
|
|
173
|
+
f"mean={stats['mean']:.6g}",
|
|
174
|
+
f"std={stats['std']:.6g}",
|
|
175
|
+
f"last={stats['last']:.6g}",
|
|
176
|
+
]
|
|
177
|
+
return " ".join(parts)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
# Plotting
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def plot_line_chart(
|
|
186
|
+
metric_data: Dict[str, Any],
|
|
187
|
+
keys: List[str],
|
|
188
|
+
output: str = "metrics_chart.png",
|
|
189
|
+
title: Optional[str] = None,
|
|
190
|
+
dpi: int = 150,
|
|
191
|
+
) -> None:
|
|
192
|
+
"""
|
|
193
|
+
Render one subplot per metric key with a line chart and stats annotation.
|
|
194
|
+
|
|
195
|
+
:param metric_data: Raw dict returned by ``Api.metrics()``.
|
|
196
|
+
:param keys: Metric keys to plot.
|
|
197
|
+
:param output: Output image file path.
|
|
198
|
+
:param title: Overall chart title (auto-generated if None).
|
|
199
|
+
:param dpi: Image resolution.
|
|
200
|
+
"""
|
|
201
|
+
n = len(keys)
|
|
202
|
+
if n == 0:
|
|
203
|
+
print("No keys to plot.", file=sys.stderr)
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
# Layout: at most 2 subplots per row
|
|
207
|
+
ncols = min(n, 2)
|
|
208
|
+
nrows = math.ceil(n / ncols)
|
|
209
|
+
|
|
210
|
+
fig, axes = plt.subplots(nrows, ncols, figsize=(7 * ncols, 4.5 * nrows), squeeze=False)
|
|
211
|
+
fig.suptitle(title or "SwanLab Metrics", fontsize=14, fontweight="bold", y=0.98)
|
|
212
|
+
|
|
213
|
+
for idx, key in enumerate(keys):
|
|
214
|
+
row, col = divmod(idx, ncols)
|
|
215
|
+
ax = axes[row][col]
|
|
216
|
+
|
|
217
|
+
steps, values = extract_series(metric_data, key)
|
|
218
|
+
|
|
219
|
+
if not steps:
|
|
220
|
+
ax.set_title(key, fontsize=11)
|
|
221
|
+
ax.text(
|
|
222
|
+
0.5,
|
|
223
|
+
0.5,
|
|
224
|
+
"No data",
|
|
225
|
+
ha="center",
|
|
226
|
+
va="center",
|
|
227
|
+
transform=ax.transAxes,
|
|
228
|
+
fontsize=12,
|
|
229
|
+
color="gray",
|
|
230
|
+
)
|
|
231
|
+
ax.set_xlabel("step")
|
|
232
|
+
ax.set_ylabel(key)
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
# Line chart
|
|
236
|
+
ax.plot(steps, values, linewidth=1.0, alpha=0.85)
|
|
237
|
+
|
|
238
|
+
# Highlight the last data point
|
|
239
|
+
ax.scatter([steps[-1]], [values[-1]], s=20, zorder=5)
|
|
240
|
+
|
|
241
|
+
# Stats annotation box
|
|
242
|
+
stats = compute_stats(values)
|
|
243
|
+
stats_text = format_stats_text(stats)
|
|
244
|
+
ax.text(
|
|
245
|
+
0.02,
|
|
246
|
+
0.02,
|
|
247
|
+
stats_text,
|
|
248
|
+
transform=ax.transAxes,
|
|
249
|
+
fontsize=7,
|
|
250
|
+
verticalalignment="bottom",
|
|
251
|
+
bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow", alpha=0.8),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
ax.set_title(key, fontsize=11)
|
|
255
|
+
ax.set_xlabel("step")
|
|
256
|
+
ax.set_ylabel(key)
|
|
257
|
+
ax.grid(True, alpha=0.3)
|
|
258
|
+
|
|
259
|
+
# Hide unused subplots
|
|
260
|
+
for idx in range(n, nrows * ncols):
|
|
261
|
+
row, col = divmod(idx, ncols)
|
|
262
|
+
axes[row][col].set_visible(False)
|
|
263
|
+
|
|
264
|
+
plt.tight_layout(rect=(0, 0, 1, 0.95))
|
|
265
|
+
fig.savefig(output, dpi=dpi, bbox_inches="tight")
|
|
266
|
+
plt.close(fig)
|
|
267
|
+
print(f"Chart saved to: {output}")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ---------------------------------------------------------------------------
|
|
271
|
+
# CLI entry point
|
|
272
|
+
# ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def parse_args() -> argparse.Namespace:
|
|
276
|
+
parser = argparse.ArgumentParser(
|
|
277
|
+
description="Fetch SwanLab experiment metrics and plot line charts.",
|
|
278
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
279
|
+
epilog=__doc__,
|
|
280
|
+
)
|
|
281
|
+
parser.add_argument(
|
|
282
|
+
"path",
|
|
283
|
+
nargs="?",
|
|
284
|
+
default=None,
|
|
285
|
+
help="Experiment path: username/project_name/run_id (optional when --data is used)",
|
|
286
|
+
)
|
|
287
|
+
parser.add_argument(
|
|
288
|
+
"--data",
|
|
289
|
+
default=None,
|
|
290
|
+
help="Path to a JSON file with pre-fetched metric data (SDK output, CLI --save file, or legacy format). Skips API calls entirely.",
|
|
291
|
+
)
|
|
292
|
+
parser.add_argument(
|
|
293
|
+
"--keys",
|
|
294
|
+
"-k",
|
|
295
|
+
required=True,
|
|
296
|
+
help="Comma-separated metric keys, e.g. 'loss,acc'",
|
|
297
|
+
)
|
|
298
|
+
parser.add_argument("--sample", "-s", type=int, default=1500, help="Sample size (default: 1500)")
|
|
299
|
+
parser.add_argument(
|
|
300
|
+
"--output",
|
|
301
|
+
"-o",
|
|
302
|
+
default="metrics_chart.png",
|
|
303
|
+
help="Output image path (default: metrics_chart.png)",
|
|
304
|
+
)
|
|
305
|
+
parser.add_argument("--title", "-t", default=None, help="Chart title (default: auto)")
|
|
306
|
+
parser.add_argument("--dpi", type=int, default=150, help="Image DPI (default: 150)")
|
|
307
|
+
parser.add_argument("--api-key", default=None, help="SwanLab API key (or use swanlab login)")
|
|
308
|
+
parser.add_argument("--host", default=None, help="SwanLab API host URL (for self-hosted)")
|
|
309
|
+
return parser.parse_args()
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def main() -> None:
|
|
313
|
+
args = parse_args()
|
|
314
|
+
keys = [k.strip() for k in args.keys.split(",") if k.strip()]
|
|
315
|
+
|
|
316
|
+
if not keys:
|
|
317
|
+
print("Error: --keys must contain at least one key.", file=sys.stderr)
|
|
318
|
+
sys.exit(1)
|
|
319
|
+
|
|
320
|
+
if not args.path and not args.data:
|
|
321
|
+
print(
|
|
322
|
+
"Error: provide either a PATH argument or --data <json_file>.",
|
|
323
|
+
file=sys.stderr,
|
|
324
|
+
)
|
|
325
|
+
sys.exit(1)
|
|
326
|
+
|
|
327
|
+
if args.data:
|
|
328
|
+
# Load metric data from a pre-fetched JSON file
|
|
329
|
+
with open(args.data, "r", encoding="utf-8") as f:
|
|
330
|
+
metric_data = json.load(f)
|
|
331
|
+
print(f"Loaded metric data from: {args.data}")
|
|
332
|
+
else:
|
|
333
|
+
print(f"Fetching metrics for: {args.path}")
|
|
334
|
+
print(f"Keys: {keys}, sample: {args.sample}")
|
|
335
|
+
metric_data = fetch_metrics(
|
|
336
|
+
path=args.path,
|
|
337
|
+
keys=keys,
|
|
338
|
+
sample=args.sample,
|
|
339
|
+
api_key=args.api_key,
|
|
340
|
+
host=args.host,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
plot_line_chart(
|
|
344
|
+
metric_data=metric_data,
|
|
345
|
+
keys=keys,
|
|
346
|
+
output=args.output,
|
|
347
|
+
title=args.title or f"Metrics: {args.path}",
|
|
348
|
+
dpi=args.dpi,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
if __name__ == "__main__":
|
|
353
|
+
main()
|