Skip to content

Commit 72e36a1

Browse files
Generate complete benchmark reports
1 parent 1f48ec9 commit 72e36a1

4 files changed

Lines changed: 167 additions & 9 deletions

File tree

benchmarks/kamino_dvi/README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Kamino DVI runtime benchmark
2+
3+
This package runs the approved RSL-RL comparison across current Kamino, Newton PR 3570 P-ADMM and DVI,
4+
MJWarp, and PhysX. The matrix uses 300 training iterations and seeds 42–46. It starts at 4096 environments and
5+
only moves down the declared capacity ladder after an explicit capacity failure.
6+
7+
## Locked environments
8+
9+
Create `.venv-current` from the project environment with Newton at the `newton_current` revision in `matrix.yaml`.
10+
Create `.venv-pr3570` from the same environment, then install the exact PR revision without applying IsaacLab's
11+
baseline package-source override:
12+
13+
```bash
14+
uv pip install --python .venv-pr3570/bin/python --no-deps --reinstall --no-cache --no-config \
15+
"git+https://github.com/newton-physics/newton.git@7906676b2e5061273db96af179d7081fc6cbbba0"
16+
```
17+
18+
For PhysX, `_isaac_sim` must point to a working Isaac Sim binary installation. The runner probes and validates both
19+
Newton revisions before executing any selected identity.
20+
21+
## Execute
22+
23+
Run five-iteration construction preflights first:
24+
25+
```bash
26+
./isaaclab.sh -p -m benchmarks.kamino_dvi.run --preflight-only --resume
27+
```
28+
29+
Then execute the full single-GPU matrix. Output is streamed to ignored per-run directories under
30+
`benchmark_artifacts/kamino_dvi/runs`; atomic manifests make the command safe to resume.
31+
32+
```bash
33+
./isaaclab.sh -p -m benchmarks.kamino_dvi.run --full-only --resume
34+
```
35+
36+
Use `--task`, `--variant`, and `--seed` to select a subset. `--dry-run` prints the exact commands without probing or
37+
launching an environment.
38+
39+
## Analyze
40+
41+
Generate compact JSON, a 95% CI runtime figure, and Markdown/PDF reports:
42+
43+
```bash
44+
./isaaclab.sh -p -m benchmarks.kamino_dvi.analyze \
45+
--artifact-root benchmark_artifacts/kamino_dvi/runs \
46+
--logs-root logs \
47+
--output-dir benchmarks/kamino_dvi/results
48+
```
49+
50+
Runtime summaries exclude iterations 1–10. Reward, episode length, and success summaries average the final 20
51+
iterations. Confidence intervals use the two-sided five-seed Student-t critical value. Reward and episode length are
52+
read from schema v1.1; success is read from the matching TensorBoard trace because the original v1.1 generator stored
53+
live step averages instead of the logged per-iteration values. That generator bug is fixed separately in PR 6624.

benchmarks/kamino_dvi/analysis.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class RunMetrics:
3030
reward: tuple[float, ...]
3131
ep_length: tuple[float, ...]
3232
success_rate: tuple[float, ...] | None
33+
success_schema_mismatch: bool = False
3334

3435

3536
@dataclass(frozen=True)
@@ -82,6 +83,17 @@ def summarize_records(records: list[RunMetrics]) -> list[VariantSummary]:
8283
return summaries
8384

8485

86+
def complete_five_seed_records(records: list[RunMetrics]) -> list[RunMetrics]:
87+
"""Return only task/variant groups with all five unique approved seeds."""
88+
complete: list[RunMetrics] = []
89+
ordered = sorted(records, key=lambda record: (record.task, record.variant, record.seed))
90+
for _, grouped in groupby(ordered, key=lambda record: (record.task, record.variant)):
91+
runs = list(grouped)
92+
if len(runs) == 5 and len({run.seed for run in runs}) == 5:
93+
complete.extend(runs)
94+
return complete
95+
96+
8597
def load_records(artifact_root: Path, logs_root: Path, task: str | None = None) -> list[RunMetrics]:
8698
"""Load every completed full-run manifest and its matched TensorBoard trace."""
8799
records: list[RunMetrics] = []
@@ -91,10 +103,11 @@ def load_records(artifact_root: Path, logs_root: Path, task: str | None = None)
91103
if manifest.get("state") != "completed" or (task is not None and identity.get("task") != task):
92104
continue
93105
bundles = tuple(manifest_path.parent.glob("benchmark_training_*.json"))
94-
if len(bundles) != 1:
95-
raise ValueError(f"{manifest_path.parent} must contain exactly one schema bundle")
96-
event_path = locate_rsl_rl_events(bundles[0], logs_root)
97-
trace = parse_training_trace(bundles[0], event_path)
106+
if not bundles:
107+
raise ValueError(f"{manifest_path.parent} contains no schema bundle")
108+
bundle = max(bundles, key=lambda path: path.stat().st_mtime)
109+
event_path = locate_rsl_rl_events(bundle, logs_root)
110+
trace = parse_training_trace(bundle, event_path)
98111
records.append(
99112
RunMetrics(
100113
task=trace.task,
@@ -106,6 +119,7 @@ def load_records(artifact_root: Path, logs_root: Path, task: str | None = None)
106119
reward=trace.reward,
107120
ep_length=trace.ep_length,
108121
success_rate=trace.success_rate,
122+
success_schema_mismatch=trace.success_schema_mismatch,
109123
)
110124
)
111125
return records

benchmarks/kamino_dvi/analyze.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
"""Generate compact results, figures, and reports from completed benchmark runs."""
7+
8+
from __future__ import annotations
9+
10+
import argparse
11+
import json
12+
from dataclasses import asdict
13+
from pathlib import Path
14+
15+
from .analysis import VariantSummary, complete_five_seed_records, load_records, summarize_records
16+
from .plotting import VARIANT_LABELS, plot_runtime
17+
from .reporting import write_reports
18+
19+
20+
def quality_issues(records, summaries: list[VariantSummary], artifact_root: Path) -> list[str]:
21+
"""Return explicit schema, capacity, failure, and learning-quality warnings."""
22+
issues: list[str] = []
23+
if any(record.success_schema_mismatch for record in records):
24+
issues.append(
25+
"Schema v1.1 success series differs from TensorBoard for at least one run; report uses TensorBoard success."
26+
)
27+
for summary in summaries:
28+
if summary.num_envs < 4096:
29+
issues.append(f"{summary.task} used {summary.num_envs} environments after a documented capacity fallback.")
30+
for manifest_path in sorted(artifact_root.glob("*/manifest.json")):
31+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
32+
if manifest.get("state") == "failed":
33+
identity = manifest["identity"]
34+
issues.append(
35+
f"Failed {identity['task']} / {identity['variant']} / seed {identity['seed']}: "
36+
f"{manifest.get('failure_category')}."
37+
)
38+
by_task = {summary.task for summary in summaries}
39+
for task in sorted(by_task):
40+
rows = {summary.variant: summary for summary in summaries if summary.task == task}
41+
baseline = rows.get("kamino_current")
42+
if baseline is None:
43+
continue
44+
baseline_floor = baseline.reward.mean - baseline.reward.half_width
45+
for variant, row in rows.items():
46+
if variant == "kamino_current":
47+
continue
48+
if row.reward.mean + row.reward.half_width < baseline_floor:
49+
issues.append(
50+
f"{task} {VARIANT_LABELS.get(variant, variant)} has materially lower final-window reward than "
51+
"current Kamino."
52+
)
53+
return issues
54+
55+
56+
def build_parser() -> argparse.ArgumentParser:
57+
"""Build the report generator command-line parser."""
58+
parser = argparse.ArgumentParser(description=__doc__)
59+
parser.add_argument("--artifact-root", type=Path, default=Path("benchmark_artifacts/kamino_dvi/runs"))
60+
parser.add_argument("--logs-root", type=Path, default=Path("logs"))
61+
parser.add_argument("--output-dir", type=Path, default=Path("benchmarks/kamino_dvi/results"))
62+
return parser
63+
64+
65+
def main(argv: list[str] | None = None) -> int:
66+
"""Load validated runs and generate all compact report artifacts."""
67+
args = build_parser().parse_args(argv)
68+
records = load_records(args.artifact_root, args.logs_root)
69+
summaries = summarize_records(complete_five_seed_records(records))
70+
if not summaries:
71+
raise RuntimeError("no complete five-seed task/variant groups are available")
72+
args.output_dir.mkdir(parents=True, exist_ok=True)
73+
runtime_figure = args.output_dir / "runtime.png"
74+
plot_runtime(summaries, runtime_figure)
75+
issues = quality_issues(records, summaries, args.artifact_root)
76+
(args.output_dir / "summary.json").write_text(
77+
json.dumps([asdict(summary) for summary in summaries], indent=2, sort_keys=True) + "\n",
78+
encoding="utf-8",
79+
)
80+
write_reports(
81+
summaries,
82+
issues,
83+
[runtime_figure],
84+
args.output_dir / "kamino_dvi_benchmark.md",
85+
args.output_dir / "kamino_dvi_benchmark.pdf",
86+
)
87+
return 0
88+
89+
90+
if __name__ == "__main__":
91+
raise SystemExit(main())

benchmarks/kamino_dvi/reporting.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,17 @@ def _markdown(summaries: list[VariantSummary], issues: list[str], figure_paths:
5858
"## Protocol",
5959
"",
6060
"- RSL-RL, 300 training iterations, seeds 42–46.",
61-
"- A common environment count is selected per task from 4096 downward only after explicit capacity failures.",
62-
"- Runs are sequential on one GPU and validated against immutable IsaacLab/Newton revisions and schema v1.1.",
61+
"- A common environment count is selected per task from 4096 downward only after explicit capacity "
62+
"failures.",
63+
"- Runs are sequential on one GPU and validated against immutable IsaacLab/Newton revisions and schema "
64+
"v1.1.",
6365
"- Reward and episode length use schema series; success rate uses the matching TensorBoard trace.",
6466
]
6567
)
6668
return "\n".join(lines) + "\n"
6769

6870

69-
def _write_pdf(
70-
summaries: list[VariantSummary], issues: list[str], figure_paths: list[Path], output_path: Path
71-
) -> None:
71+
def _write_pdf(summaries: list[VariantSummary], issues: list[str], figure_paths: list[Path], output_path: Path) -> None:
7272
with PdfPages(output_path) as pdf:
7373
figure = plt.figure(figsize=(11.69, 8.27))
7474
figure.text(0.07, 0.93, "Kamino DVI Solver Benchmark", fontsize=22, fontweight="bold")

0 commit comments

Comments
 (0)