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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +16 -0
  3. package/compute-mode/preset/agent.cordis.yml +368 -0
  4. package/compute-mode/preset/preset.yml +3 -0
  5. package/compute-mode/preset/skills/runnpu/SKILL.md +233 -0
  6. package/compute-mode/preset/skills/runnpu/VENDOR.txt +11 -0
  7. package/compute-mode/preset/skills/runnpu/examples/01-dev-environment.sh +26 -0
  8. package/compute-mode/preset/skills/runnpu/examples/02-training-job.sh +26 -0
  9. package/compute-mode/preset/skills/runnpu/examples/03-inference-service.sh +24 -0
  10. package/compute-mode/preset/skills/runnpu/examples/04-fractional-vnpu.sh +14 -0
  11. package/compute-mode/preset/skills/runnpu/examples/05-troubleshoot.sh +17 -0
  12. package/compute-mode/preset/skills/runnpu/references/admin.md +60 -0
  13. package/compute-mode/preset/skills/runnpu/references/data-sources.md +35 -0
  14. package/compute-mode/preset/skills/runnpu/references/templates-assets.md +61 -0
  15. package/compute-mode/preset/skills/runnpu/references/training-runs.md +56 -0
  16. package/compute-mode/preset/skills/runnpu/references/troubleshooting.md +75 -0
  17. package/compute-mode/preset/skills/runnpu/references/workload-types.md +47 -0
  18. package/cordis.patch.yml +80 -0
  19. package/dist/chunk-JLZMOSQC.mjs +1 -0
  20. package/dist/chunk-JUBIWGKW.mjs +4 -0
  21. package/dist/chunk-JZXLCA2E.mjs +1 -0
  22. package/dist/chunk-KQOFBYGE.mjs +9 -0
  23. package/dist/chunk-XMZG2NP7.mjs +2 -0
  24. package/dist/chunk-XQPU3AJN.mjs +1 -0
  25. package/dist/chunk-YKASEMP3.mjs +1 -0
  26. package/dist/chunk-YPL4VUYF.mjs +4 -0
  27. package/dist/compute-node.mjs +1 -0
  28. package/dist/compute-tools.mjs +41 -0
  29. package/dist/connectors.mjs +3 -0
  30. package/dist/dsh-runnpu.mjs +43 -0
  31. package/dist/json.mjs +1 -0
  32. package/dist/pins.mjs +1 -0
  33. package/dist/series.mjs +1 -0
  34. package/dist/training-node.mjs +2 -0
  35. package/dist/training-tools.mjs +30 -0
  36. package/dist/ui-host.mjs +1 -0
  37. package/lib/client.js +189 -0
  38. package/package.json +78 -0
  39. package/training-mode/preset/agent.cordis.yml +343 -0
  40. package/training-mode/preset/preset.yml +3 -0
  41. package/training-mode/preset/skills/runnpu/SKILL.md +233 -0
  42. package/training-mode/preset/skills/runnpu/VENDOR.txt +11 -0
  43. package/training-mode/preset/skills/runnpu/examples/01-dev-environment.sh +26 -0
  44. package/training-mode/preset/skills/runnpu/examples/02-training-job.sh +26 -0
  45. package/training-mode/preset/skills/runnpu/examples/03-inference-service.sh +24 -0
  46. package/training-mode/preset/skills/runnpu/examples/04-fractional-vnpu.sh +14 -0
  47. package/training-mode/preset/skills/runnpu/examples/05-troubleshoot.sh +17 -0
  48. package/training-mode/preset/skills/runnpu/references/admin.md +60 -0
  49. package/training-mode/preset/skills/runnpu/references/data-sources.md +35 -0
  50. package/training-mode/preset/skills/runnpu/references/templates-assets.md +61 -0
  51. package/training-mode/preset/skills/runnpu/references/training-runs.md +56 -0
  52. package/training-mode/preset/skills/runnpu/references/troubleshooting.md +75 -0
  53. package/training-mode/preset/skills/runnpu/references/workload-types.md +47 -0
  54. package/training-mode/preset/skills/swanlab-skill/LICENSE +21 -0
  55. package/training-mode/preset/skills/swanlab-skill/SKILL.md +163 -0
  56. package/training-mode/preset/skills/swanlab-skill/VENDOR.txt +9 -0
  57. package/training-mode/preset/skills/swanlab-skill/references/ANALYSIS_GUIDE.md +83 -0
  58. package/training-mode/preset/skills/swanlab-skill/references/CLI_REFERENCE.md +510 -0
  59. package/training-mode/preset/skills/swanlab-skill/references/SDK_QUICKSTART.md +341 -0
  60. package/training-mode/preset/skills/swanlab-skill/references/SWANLAB_CONCEPTS.md +523 -0
  61. package/training-mode/preset/skills/swanlab-skill/scripts/plot_metrics.py +353 -0
  62. package/training-mode/preset/skills/swanlab-skill/scripts/runs_benchmark.py +550 -0
  63. package/training-mode/preset/skills/training-research/SKILL.md +33 -0
  64. package/training-mode/preset/skills/training-research/references/pipeline.md +45 -0
  65. 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 的"假象"一节。