giakhuyendihoc commited on
Commit
207fbe8
·
verified ·
1 Parent(s): 8dff78c

Upload UR5 full fine-tuned checkpoint: pi05_pour_full configs/check_policy_inference.py

Browse files
Files changed (1) hide show
  1. configs/check_policy_inference.py +568 -0
configs/check_policy_inference.py ADDED
@@ -0,0 +1,568 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dataclasses
2
+ import datetime as dt
3
+ import contextlib
4
+ import json
5
+ import logging
6
+ import pathlib
7
+ import re
8
+ import time
9
+ from typing import Any
10
+
11
+ import numpy as np
12
+ import torch
13
+ import tyro
14
+
15
+ from openpi.policies import libero_policy
16
+ from openpi.policies import policy_config as _policy_config
17
+ from openpi.policies import ur5_policy
18
+ from openpi.shared import normalize as _normalize
19
+ from serve_policy_from_checkpoint import load_train_config_from_checkpoint
20
+
21
+
22
+ @dataclasses.dataclass
23
+ class Args:
24
+ checkpoint_dir: pathlib.Path
25
+ config_name: str | None = None
26
+ default_prompt: str | None = None
27
+ pytorch_device: str | None = None
28
+ num_denoise_steps: int = 2
29
+ lora_rank: int | None = None
30
+ lora_alpha: float | None = None
31
+ warmup_runs: int = 2
32
+ timed_runs: int = 10
33
+ profile_flops: bool = False
34
+ example: str = "auto"
35
+ train_log: pathlib.Path | None = None
36
+ metrics_out_path: pathlib.Path | None = None
37
+ table_out_path: pathlib.Path | None = None
38
+ norm_stats_path: pathlib.Path | None = None
39
+ model_label: str | None = None
40
+
41
+
42
+ def main(args: Args) -> None:
43
+ checkpoint_metadata = _load_checkpoint_metadata(args.checkpoint_dir)
44
+ train_config = load_train_config_from_checkpoint(
45
+ args.checkpoint_dir,
46
+ config_name=args.config_name,
47
+ lora_rank=args.lora_rank,
48
+ lora_alpha=args.lora_alpha,
49
+ )
50
+ norm_stats = _load_norm_stats_override(args.norm_stats_path)
51
+ policy = _policy_config.create_trained_policy(
52
+ train_config,
53
+ args.checkpoint_dir,
54
+ default_prompt=args.default_prompt,
55
+ sample_kwargs={"num_steps": args.num_denoise_steps},
56
+ norm_stats=norm_stats,
57
+ pytorch_device=args.pytorch_device,
58
+ )
59
+
60
+ example = _make_example(args.example, train_config)
61
+ last_result = None
62
+ device = _policy_device(policy)
63
+ if device is not None and device.type == "cuda":
64
+ torch.cuda.reset_peak_memory_stats(device)
65
+
66
+ with torch.inference_mode():
67
+ for _ in range(args.warmup_runs):
68
+ last_result = policy.infer(example)
69
+ _assert_finite_actions(last_result)
70
+ if torch.cuda.is_available() and (args.pytorch_device is None or "cuda" in args.pytorch_device):
71
+ torch.cuda.synchronize()
72
+
73
+ model_infer_ms = []
74
+ wall_ms = []
75
+ timing_values = {
76
+ "sample_total_ms": [],
77
+ "vlm_prefix_ms": [],
78
+ "denoising_ms": [],
79
+ "denoising_ms_per_step": [],
80
+ "denoising_steps": [],
81
+ "other_ms": [],
82
+ }
83
+ with torch.inference_mode():
84
+ for _ in range(args.timed_runs):
85
+ start_time = time.monotonic()
86
+ result = policy.infer(example)
87
+ if torch.cuda.is_available() and (args.pytorch_device is None or "cuda" in args.pytorch_device):
88
+ torch.cuda.synchronize()
89
+ wall_ms.append((time.monotonic() - start_time) * 1000)
90
+ _assert_finite_actions(result)
91
+ policy_timing = result.get("policy_timing", {})
92
+ model_infer_ms.append(float(policy_timing.get("infer_ms", float("nan"))))
93
+ _append_timing_values(timing_values, policy_timing)
94
+ last_result = result
95
+
96
+ if last_result is None:
97
+ raise RuntimeError("No inference runs were executed")
98
+
99
+ actions = np.asarray(last_result["actions"])
100
+ action_horizon = actions.shape[0]
101
+ model_infer_summary = _latency_summary(model_infer_ms)
102
+ wall_summary = _latency_summary(wall_ms)
103
+ model_metrics = _collect_model_metrics(policy, args.checkpoint_dir, checkpoint_metadata)
104
+ training_metrics = _collect_training_metrics(checkpoint_metadata, args.train_log)
105
+ approx_gflops = None
106
+ flop_profile = None
107
+ if args.profile_flops:
108
+ flop_profile = _profile_policy_flops(policy, example, device)
109
+ approx_gflops = flop_profile["total_gflops"]
110
+
111
+ metrics = {
112
+ "model_label": args.model_label or train_config.name,
113
+ "config": train_config.name,
114
+ "checkpoint": str(args.checkpoint_dir),
115
+ "norm_stats_path": str(args.norm_stats_path.resolve()) if args.norm_stats_path else None,
116
+ "global_step": checkpoint_metadata.get("global_step"),
117
+ "warmup_runs": args.warmup_runs,
118
+ "timed_runs": args.timed_runs,
119
+ "num_denoise_steps": args.num_denoise_steps,
120
+ "action_horizon": action_horizon,
121
+ "actions_shape": list(actions.shape),
122
+ "actions_mean": float(actions.mean()),
123
+ "actions_std": float(actions.std()),
124
+ "latency": {
125
+ "model_infer_ms": model_infer_summary,
126
+ "wall_ms": wall_summary,
127
+ "model_infer_per_action_ms": _latency_summary([value / action_horizon for value in model_infer_ms]),
128
+ "wall_per_action_ms": _latency_summary([value / action_horizon for value in wall_ms]),
129
+ },
130
+ "latency_decomposition": _latency_decomposition_summary(timing_values),
131
+ "frequency": _frequency_metrics(model_infer_summary, wall_summary, action_horizon),
132
+ "model": model_metrics,
133
+ "training": training_metrics,
134
+ "approx_gflops": approx_gflops,
135
+ "flop_profile": flop_profile,
136
+ }
137
+ metrics["table_row"] = _build_table_row(metrics)
138
+
139
+ print(f"config={train_config.name}")
140
+ print(f"checkpoint={args.checkpoint_dir}")
141
+ print(f"warmup_runs={args.warmup_runs}")
142
+ print(f"timed_runs={args.timed_runs}")
143
+ print(f"num_denoise_steps={args.num_denoise_steps}")
144
+ print(f"actions_shape={actions.shape}")
145
+ print(f"actions_mean={actions.mean():.6f}")
146
+ print(f"actions_std={actions.std():.6f}")
147
+ _print_latency("model_infer_ms", model_infer_ms)
148
+ _print_latency("wall_ms", wall_ms)
149
+ _print_latency("model_infer_per_action_ms", [value / action_horizon for value in model_infer_ms])
150
+ _print_latency("wall_per_action_ms", [value / action_horizon for value in wall_ms])
151
+ decomposition = metrics["latency_decomposition"]
152
+ if decomposition["available"]:
153
+ if timing_values["vlm_prefix_ms"]:
154
+ _print_latency("vlm_prefix_ms", timing_values["vlm_prefix_ms"])
155
+ if timing_values["denoising_ms"]:
156
+ _print_latency("denoising_ms", timing_values["denoising_ms"])
157
+ if timing_values["denoising_ms_per_step"]:
158
+ _print_latency("denoising_ms_per_step", timing_values["denoising_ms_per_step"])
159
+ print(f"model_query_hz={metrics['frequency']['model_query_hz']:.4f}")
160
+ print(f"model_action_generation_hz={metrics['frequency']['model_action_generation_hz']:.4f}")
161
+ print(f"wall_query_hz={metrics['frequency']['wall_query_hz']:.4f}")
162
+ print(f"wall_action_generation_hz={metrics['frequency']['wall_action_generation_hz']:.4f}")
163
+ print(f"model_total_params_b={model_metrics.get('model_total_params_b', float('nan')):.4f}")
164
+ print(f"model_trainable_params_m={model_metrics.get('model_trainable_params_m', float('nan')):.2f}")
165
+ print(f"checkpoint_size_gb={model_metrics.get('checkpoint_size_gb', float('nan')):.2f}")
166
+ if training_metrics.get("training_time_hours") is not None:
167
+ print(f"training_time_hours={training_metrics['training_time_hours']:.4f}")
168
+ if approx_gflops is not None:
169
+ print(f"approx_gflops={approx_gflops:.2f}")
170
+ if flop_profile is not None:
171
+ print(f"profiler_gflops={flop_profile['profiler_gflops']:.2f}")
172
+ print(f"manual_conv2d_gflops={flop_profile['manual_conv2d_gflops']:.2f}")
173
+ print(f"conv2d_modules_counted={flop_profile['manual_conv2d_modules_counted']}")
174
+ print("table_row_markdown=" + _format_table_row(metrics["table_row"]))
175
+
176
+ if args.metrics_out_path is not None:
177
+ args.metrics_out_path.parent.mkdir(parents=True, exist_ok=True)
178
+ args.metrics_out_path.write_text(json.dumps(metrics, indent=2) + "\n")
179
+ print(f"metrics_out_path={args.metrics_out_path}")
180
+ if args.table_out_path is not None:
181
+ args.table_out_path.parent.mkdir(parents=True, exist_ok=True)
182
+ args.table_out_path.write_text(_format_table(metrics["table_row"]))
183
+ print(f"table_out_path={args.table_out_path}")
184
+
185
+
186
+ def _assert_finite_actions(result: dict) -> None:
187
+ actions = np.asarray(result["actions"])
188
+ if not np.isfinite(actions).all():
189
+ raise RuntimeError("Policy returned non-finite actions")
190
+
191
+
192
+ def _make_example(example_name: str, train_config: Any) -> dict:
193
+ example_name = example_name.lower()
194
+ if example_name == "auto":
195
+ config_text = f"{train_config.name} {type(train_config.data).__name__}".lower()
196
+ example_name = "ur5" if "ur5" in config_text or "tube" in config_text or "pour" in config_text else "libero"
197
+ if example_name == "ur5":
198
+ return ur5_policy.make_ur5_example()
199
+ if example_name == "libero":
200
+ return libero_policy.make_libero_example()
201
+ raise ValueError(f"Unknown example={example_name!r}. Expected auto, ur5, or libero.")
202
+
203
+
204
+ def _print_latency(name: str, values: list[float]) -> None:
205
+ summary = _latency_summary(values)
206
+ print(f"{name}_mean={summary['mean']:.2f}")
207
+ print(f"{name}_median={summary['median']:.2f}")
208
+ print(f"{name}_p95={summary['p95']:.2f}")
209
+ print(f"{name}_min={summary['min']:.2f}")
210
+ print(f"{name}_max={summary['max']:.2f}")
211
+
212
+
213
+ def _append_timing_values(timing_values: dict[str, list[float]], policy_timing: dict[str, Any]) -> None:
214
+ for key in timing_values:
215
+ value = _finite_float(policy_timing.get(key))
216
+ if value is not None:
217
+ timing_values[key].append(value)
218
+
219
+
220
+ def _latency_decomposition_summary(timing_values: dict[str, list[float]]) -> dict[str, Any]:
221
+ sample_total = _latency_summary_or_none(timing_values["sample_total_ms"])
222
+ vlm_prefix = _latency_summary_or_none(timing_values["vlm_prefix_ms"])
223
+ denoising = _latency_summary_or_none(timing_values["denoising_ms"])
224
+ denoising_per_step = _latency_summary_or_none(timing_values["denoising_ms_per_step"])
225
+ denoising_steps = _latency_summary_or_none(timing_values["denoising_steps"])
226
+ other = _latency_summary_or_none(timing_values["other_ms"])
227
+ return {
228
+ "available": bool(timing_values["sample_total_ms"]),
229
+ "measurement": (
230
+ "Cached-observation policy benchmark. VLM prefix is image/language prefix embedding plus "
231
+ "prefix transformer KV-cache construction. Denoising is the action denoising loop."
232
+ ),
233
+ "sample_total_ms": sample_total,
234
+ "vlm_prefix_ms": vlm_prefix,
235
+ "denoising_ms": denoising,
236
+ "denoising_ms_per_step": denoising_per_step,
237
+ "denoising_steps": denoising_steps,
238
+ "other_ms": other,
239
+ "fractions_of_mean_sample_total": {
240
+ "vlm_prefix": _fraction(vlm_prefix.get("mean"), sample_total.get("mean")),
241
+ "denoising": _fraction(denoising.get("mean"), sample_total.get("mean")),
242
+ "other": _fraction(other.get("mean"), sample_total.get("mean")),
243
+ },
244
+ }
245
+
246
+
247
+ def _latency_summary_or_none(values: list[float]) -> dict[str, float | None]:
248
+ if not values:
249
+ return {
250
+ "mean": None,
251
+ "median": None,
252
+ "p95": None,
253
+ "min": None,
254
+ "max": None,
255
+ }
256
+ return _latency_summary(values)
257
+
258
+
259
+ def _finite_float(value: Any) -> float | None:
260
+ try:
261
+ number = float(value)
262
+ except (TypeError, ValueError):
263
+ return None
264
+ return number if np.isfinite(number) else None
265
+
266
+
267
+ def _fraction(numerator: Any, denominator: Any) -> float | None:
268
+ num = _finite_float(numerator)
269
+ den = _finite_float(denominator)
270
+ if num is None or den is None or den <= 0:
271
+ return None
272
+ return float(num / den)
273
+
274
+
275
+ def _latency_summary(values: list[float]) -> dict[str, float]:
276
+ arr = np.asarray(values, dtype=np.float64)
277
+ return {
278
+ "mean": float(arr.mean()),
279
+ "median": float(np.percentile(arr, 50)),
280
+ "p95": float(np.percentile(arr, 95)),
281
+ "min": float(arr.min()),
282
+ "max": float(arr.max()),
283
+ }
284
+
285
+
286
+ def _frequency_metrics(
287
+ model_infer_summary: dict[str, float],
288
+ wall_summary: dict[str, float],
289
+ action_horizon: int,
290
+ ) -> dict[str, float]:
291
+ model_query_hz = _hz_from_ms(model_infer_summary["median"])
292
+ wall_query_hz = _hz_from_ms(wall_summary["median"])
293
+ return {
294
+ "model_query_hz": model_query_hz,
295
+ "wall_query_hz": wall_query_hz,
296
+ "model_action_generation_hz": model_query_hz * action_horizon,
297
+ "wall_action_generation_hz": wall_query_hz * action_horizon,
298
+ }
299
+
300
+
301
+ def _hz_from_ms(milliseconds: float) -> float:
302
+ return 1000.0 / milliseconds if milliseconds > 0 else float("nan")
303
+
304
+
305
+ def _load_norm_stats_override(path: pathlib.Path | None):
306
+ if path is None:
307
+ return None
308
+ path = pathlib.Path(path).expanduser().resolve()
309
+ if path.is_file():
310
+ logging.info("Loaded explicit norm stats from %s", path)
311
+ return _normalize.deserialize_json(path.read_text(encoding="utf-8"))
312
+ logging.info("Loaded explicit norm stats from %s", path / "norm_stats.json")
313
+ return _normalize.load(path)
314
+
315
+
316
+ def _load_checkpoint_metadata(checkpoint_dir: pathlib.Path) -> dict[str, Any]:
317
+ metadata_path = checkpoint_dir / "metadata.pt"
318
+ if not metadata_path.exists():
319
+ return {}
320
+ try:
321
+ return torch.load(metadata_path, map_location="cpu", weights_only=False)
322
+ except TypeError:
323
+ return torch.load(metadata_path, map_location="cpu")
324
+
325
+
326
+ def _policy_device(policy) -> torch.device | None:
327
+ device_name = getattr(policy, "_pytorch_device", None)
328
+ if device_name is not None:
329
+ return torch.device(device_name)
330
+ model = getattr(policy, "_model", None)
331
+ if model is None:
332
+ return None
333
+ try:
334
+ return next(model.parameters()).device
335
+ except StopIteration:
336
+ return None
337
+
338
+
339
+ def _collect_model_metrics(policy, checkpoint_dir: pathlib.Path, metadata: dict[str, Any]) -> dict[str, float | int | None]:
340
+ model = getattr(policy, "_model", None)
341
+ metrics = dict(metadata.get("efficiency_metrics") or {})
342
+ if model is not None:
343
+ if hasattr(model, "configure_trainable_parameters"):
344
+ model.configure_trainable_parameters()
345
+ params = list(model.parameters())
346
+ total_params = sum(param.numel() for param in params)
347
+ trainable_params = sum(param.numel() for param in params if param.requires_grad)
348
+ metrics.update(
349
+ {
350
+ "model_total_params": total_params,
351
+ "model_trainable_params": trainable_params,
352
+ "model_trainable_pct": trainable_params / total_params * 100 if total_params else 0.0,
353
+ "model_total_params_b": total_params / 1e9,
354
+ "model_trainable_params_m": trainable_params / 1e6,
355
+ }
356
+ )
357
+
358
+ weight_path = checkpoint_dir / "model.safetensors"
359
+ if weight_path.exists():
360
+ checkpoint_size_bytes = weight_path.stat().st_size
361
+ metrics["checkpoint_size_bytes"] = checkpoint_size_bytes
362
+ metrics["checkpoint_size_gb"] = checkpoint_size_bytes / 1024**3
363
+
364
+ device = _policy_device(policy)
365
+ if device is not None and device.type == "cuda" and torch.cuda.is_available():
366
+ metrics["cuda_peak_allocated_gb_inference"] = torch.cuda.max_memory_allocated(device) / 1024**3
367
+ metrics["cuda_peak_reserved_gb_inference"] = torch.cuda.max_memory_reserved(device) / 1024**3
368
+
369
+ return metrics
370
+
371
+
372
+ def _collect_training_metrics(metadata: dict[str, Any], train_log: pathlib.Path | None) -> dict[str, Any]:
373
+ efficiency_metrics = metadata.get("efficiency_metrics") or {}
374
+ training_time_hours = (
375
+ efficiency_metrics.get("elapsed_training_hours")
376
+ or efficiency_metrics.get("elapsed_after_first_step_hours")
377
+ or efficiency_metrics.get("elapsed_run_hours")
378
+ )
379
+ metrics: dict[str, Any] = {
380
+ "training_time_hours": training_time_hours,
381
+ "source": "checkpoint_metadata" if training_time_hours is not None else None,
382
+ }
383
+ if train_log is not None:
384
+ parsed = _parse_training_time_from_log(train_log, metadata.get("global_step"))
385
+ metrics.update(parsed)
386
+ if parsed.get("training_time_hours") is not None:
387
+ metrics["training_time_hours"] = parsed["training_time_hours"]
388
+ metrics["source"] = "train_log"
389
+ return metrics
390
+
391
+
392
+ def _parse_training_time_from_log(train_log: pathlib.Path, global_step: int | None) -> dict[str, Any]:
393
+ if not train_log.exists():
394
+ raise FileNotFoundError(f"Training log does not exist: {train_log}")
395
+
396
+ created = None
397
+ train_config = None
398
+ first_step = None
399
+ final_step = None
400
+ final_save = None
401
+ step_pattern = re.compile(rf"step={global_step}\s") if global_step is not None else None
402
+
403
+ for raw_line in train_log.read_text(errors="replace").replace("\r", "\n").splitlines():
404
+ timestamp = _parse_log_time(raw_line)
405
+ if timestamp is None:
406
+ continue
407
+ if "Created experiment checkpoint directory" in raw_line:
408
+ created = timestamp
409
+ if "Training config:" in raw_line:
410
+ train_config = timestamp
411
+ if re.search(r"step=1\s", raw_line):
412
+ first_step = timestamp
413
+ if step_pattern is not None and step_pattern.search(raw_line):
414
+ final_step = timestamp
415
+ if global_step is not None and f"Saved checkpoint at step {global_step}" in raw_line:
416
+ final_save = timestamp
417
+
418
+ end_time = final_save or final_step
419
+ start_time = train_config or first_step or created
420
+ training_time_hours = None if start_time is None or end_time is None else _elapsed_hours(start_time, end_time)
421
+ return {
422
+ "training_time_hours": training_time_hours,
423
+ "created_to_final_save_hours": None if created is None or final_save is None else _elapsed_hours(created, final_save),
424
+ "train_config_to_final_save_hours": (
425
+ None if train_config is None or final_save is None else _elapsed_hours(train_config, final_save)
426
+ ),
427
+ "first_step_to_final_save_hours": None if first_step is None or final_save is None else _elapsed_hours(first_step, final_save),
428
+ "first_step_to_final_step_hours": None if first_step is None or final_step is None else _elapsed_hours(first_step, final_step),
429
+ "log_path": str(train_log),
430
+ }
431
+
432
+
433
+ def _parse_log_time(line: str) -> dt.datetime | None:
434
+ match = re.search(r"(\d{2}:\d{2}:\d{2}\.\d{3})", line)
435
+ if match is None:
436
+ return None
437
+ return dt.datetime.strptime(match.group(1), "%H:%M:%S.%f")
438
+
439
+
440
+ def _elapsed_hours(start: dt.datetime, end: dt.datetime) -> float:
441
+ while end < start:
442
+ end += dt.timedelta(days=1)
443
+ return (end - start).total_seconds() / 3600
444
+
445
+
446
+ def _profile_policy_flops(policy, example: dict, device: torch.device | None) -> dict[str, Any]:
447
+ activities = [torch.profiler.ProfilerActivity.CPU]
448
+ if device is not None and device.type == "cuda" and torch.cuda.is_available():
449
+ activities.append(torch.profiler.ProfilerActivity.CUDA)
450
+ torch.cuda.synchronize(device)
451
+
452
+ conv2d_counter = _Conv2dFlopCounter(policy)
453
+ with torch.inference_mode():
454
+ with conv2d_counter:
455
+ with torch.profiler.profile(activities=activities, with_flops=True) as profiler:
456
+ result = policy.infer(example)
457
+ _assert_finite_actions(result)
458
+ if device is not None and device.type == "cuda" and torch.cuda.is_available():
459
+ torch.cuda.synchronize(device)
460
+
461
+ profiler_flops = sum((getattr(event, "flops", 0) or 0) for event in profiler.key_averages())
462
+ profiler_conv2d_flops = sum(
463
+ (getattr(event, "flops", 0) or 0)
464
+ for event in profiler.key_averages()
465
+ if "conv2d" in getattr(event, "key", "")
466
+ )
467
+ manual_conv2d_flops = conv2d_counter.total_flops
468
+
469
+ # PyTorch currently warns that some aten::conv2d FLOPs cannot be computed,
470
+ # and reports those as zero. Add our module-hook Conv2d count only when the
471
+ # profiler did not already account for Conv2d FLOPs to avoid double counting
472
+ # on future PyTorch versions.
473
+ added_manual_conv2d_flops = manual_conv2d_flops if profiler_conv2d_flops == 0 else 0
474
+ total_flops = profiler_flops + added_manual_conv2d_flops
475
+ return {
476
+ "total_flops": float(total_flops),
477
+ "total_gflops": float(total_flops) / 1e9,
478
+ "profiler_flops": float(profiler_flops),
479
+ "profiler_gflops": float(profiler_flops) / 1e9,
480
+ "profiler_conv2d_flops": float(profiler_conv2d_flops),
481
+ "profiler_conv2d_gflops": float(profiler_conv2d_flops) / 1e9,
482
+ "manual_conv2d_flops": float(manual_conv2d_flops),
483
+ "manual_conv2d_gflops": float(manual_conv2d_flops) / 1e9,
484
+ "manual_conv2d_flops_added": float(added_manual_conv2d_flops),
485
+ "manual_conv2d_gflops_added": float(added_manual_conv2d_flops) / 1e9,
486
+ "manual_conv2d_modules_counted": conv2d_counter.modules_counted,
487
+ "flop_convention": "2 FLOPs per multiply-add; bias additions are not counted.",
488
+ "note": (
489
+ "Total combines PyTorch profiler FLOPs with manual Conv2d FLOPs only "
490
+ "when profiler Conv2d FLOPs are zero."
491
+ ),
492
+ }
493
+
494
+
495
+ class _Conv2dFlopCounter(contextlib.AbstractContextManager):
496
+ def __init__(self, policy) -> None:
497
+ self.model = getattr(policy, "_model", None)
498
+ self.handles: list[Any] = []
499
+ self.total_flops = 0.0
500
+ self.modules_counted = 0
501
+
502
+ def __enter__(self):
503
+ if self.model is None:
504
+ return self
505
+ for module in self.model.modules():
506
+ if isinstance(module, torch.nn.Conv2d):
507
+ self.handles.append(module.register_forward_hook(self._hook))
508
+ return self
509
+
510
+ def __exit__(self, exc_type, exc_value, traceback) -> bool:
511
+ for handle in self.handles:
512
+ handle.remove()
513
+ self.handles.clear()
514
+ return False
515
+
516
+ def _hook(self, module: torch.nn.Conv2d, inputs: tuple[Any, ...], output: Any) -> None:
517
+ if not inputs:
518
+ return
519
+ x = inputs[0]
520
+ if not torch.is_tensor(x) or not torch.is_tensor(output) or output.ndim < 4:
521
+ return
522
+ batch = int(output.shape[0])
523
+ out_channels = int(output.shape[1])
524
+ out_h = int(output.shape[2])
525
+ out_w = int(output.shape[3])
526
+ kernel_h, kernel_w = module.kernel_size
527
+ in_channels = int(module.in_channels)
528
+ groups = int(module.groups)
529
+ macs_per_output = (in_channels // groups) * int(kernel_h) * int(kernel_w)
530
+ self.total_flops += float(batch * out_channels * out_h * out_w * macs_per_output * 2)
531
+ self.modules_counted += 1
532
+
533
+
534
+ def _build_table_row(metrics: dict[str, Any]) -> dict[str, Any]:
535
+ model_metrics = metrics["model"]
536
+ training_metrics = metrics["training"]
537
+ return {
538
+ "Model": metrics["model_label"],
539
+ "Model Size": f"{model_metrics.get('model_total_params_b', float('nan')):.2f}B",
540
+ "Trainable Params": f"{model_metrics.get('model_trainable_params_m', float('nan')):.2f}M",
541
+ "Training Time (hours)": _format_optional(training_metrics.get("training_time_hours"), precision=3),
542
+ "GFLOPs": _format_optional(metrics.get("approx_gflops"), precision=2),
543
+ "Inference Speed (ms)": f"{metrics['latency']['model_infer_ms']['median']:.2f}",
544
+ "Inference Speed (Hz)": f"{metrics['frequency']['model_query_hz']:.3f}",
545
+ "Action Gen. (Hz)": f"{metrics['frequency']['model_action_generation_hz']:.3f}",
546
+ "Checkpoint Size": f"{model_metrics.get('checkpoint_size_gb', float('nan')):.2f}GB",
547
+ }
548
+
549
+
550
+ def _format_optional(value: Any, *, precision: int) -> str:
551
+ if value is None:
552
+ return "n/a"
553
+ return f"{float(value):.{precision}f}"
554
+
555
+
556
+ def _format_table_row(row: dict[str, Any]) -> str:
557
+ return "| " + " | ".join(str(value) for value in row.values()) + " |"
558
+
559
+
560
+ def _format_table(row: dict[str, Any]) -> str:
561
+ header = "| " + " | ".join(row.keys()) + " |\n"
562
+ separator = "| " + " | ".join("---" for _ in row) + " |\n"
563
+ return header + separator + _format_table_row(row) + "\n"
564
+
565
+
566
+ if __name__ == "__main__":
567
+ logging.basicConfig(level=logging.INFO, force=True)
568
+ main(tyro.cli(Args))