Upload UR5 full fine-tuned checkpoint: pi05_pour_full configs/check_policy_inference.py
207fbe8 verified | import dataclasses | |
| import datetime as dt | |
| import contextlib | |
| import json | |
| import logging | |
| import pathlib | |
| import re | |
| import time | |
| from typing import Any | |
| import numpy as np | |
| import torch | |
| import tyro | |
| from openpi.policies import libero_policy | |
| from openpi.policies import policy_config as _policy_config | |
| from openpi.policies import ur5_policy | |
| from openpi.shared import normalize as _normalize | |
| from serve_policy_from_checkpoint import load_train_config_from_checkpoint | |
| class Args: | |
| checkpoint_dir: pathlib.Path | |
| config_name: str | None = None | |
| default_prompt: str | None = None | |
| pytorch_device: str | None = None | |
| num_denoise_steps: int = 2 | |
| lora_rank: int | None = None | |
| lora_alpha: float | None = None | |
| warmup_runs: int = 2 | |
| timed_runs: int = 10 | |
| profile_flops: bool = False | |
| example: str = "auto" | |
| train_log: pathlib.Path | None = None | |
| metrics_out_path: pathlib.Path | None = None | |
| table_out_path: pathlib.Path | None = None | |
| norm_stats_path: pathlib.Path | None = None | |
| model_label: str | None = None | |
| def main(args: Args) -> None: | |
| checkpoint_metadata = _load_checkpoint_metadata(args.checkpoint_dir) | |
| train_config = load_train_config_from_checkpoint( | |
| args.checkpoint_dir, | |
| config_name=args.config_name, | |
| lora_rank=args.lora_rank, | |
| lora_alpha=args.lora_alpha, | |
| ) | |
| norm_stats = _load_norm_stats_override(args.norm_stats_path) | |
| policy = _policy_config.create_trained_policy( | |
| train_config, | |
| args.checkpoint_dir, | |
| default_prompt=args.default_prompt, | |
| sample_kwargs={"num_steps": args.num_denoise_steps}, | |
| norm_stats=norm_stats, | |
| pytorch_device=args.pytorch_device, | |
| ) | |
| example = _make_example(args.example, train_config) | |
| last_result = None | |
| device = _policy_device(policy) | |
| if device is not None and device.type == "cuda": | |
| torch.cuda.reset_peak_memory_stats(device) | |
| with torch.inference_mode(): | |
| for _ in range(args.warmup_runs): | |
| last_result = policy.infer(example) | |
| _assert_finite_actions(last_result) | |
| if torch.cuda.is_available() and (args.pytorch_device is None or "cuda" in args.pytorch_device): | |
| torch.cuda.synchronize() | |
| model_infer_ms = [] | |
| wall_ms = [] | |
| timing_values = { | |
| "sample_total_ms": [], | |
| "vlm_prefix_ms": [], | |
| "denoising_ms": [], | |
| "denoising_ms_per_step": [], | |
| "denoising_steps": [], | |
| "other_ms": [], | |
| } | |
| with torch.inference_mode(): | |
| for _ in range(args.timed_runs): | |
| start_time = time.monotonic() | |
| result = policy.infer(example) | |
| if torch.cuda.is_available() and (args.pytorch_device is None or "cuda" in args.pytorch_device): | |
| torch.cuda.synchronize() | |
| wall_ms.append((time.monotonic() - start_time) * 1000) | |
| _assert_finite_actions(result) | |
| policy_timing = result.get("policy_timing", {}) | |
| model_infer_ms.append(float(policy_timing.get("infer_ms", float("nan")))) | |
| _append_timing_values(timing_values, policy_timing) | |
| last_result = result | |
| if last_result is None: | |
| raise RuntimeError("No inference runs were executed") | |
| actions = np.asarray(last_result["actions"]) | |
| action_horizon = actions.shape[0] | |
| model_infer_summary = _latency_summary(model_infer_ms) | |
| wall_summary = _latency_summary(wall_ms) | |
| model_metrics = _collect_model_metrics(policy, args.checkpoint_dir, checkpoint_metadata) | |
| training_metrics = _collect_training_metrics(checkpoint_metadata, args.train_log) | |
| approx_gflops = None | |
| flop_profile = None | |
| if args.profile_flops: | |
| flop_profile = _profile_policy_flops(policy, example, device) | |
| approx_gflops = flop_profile["total_gflops"] | |
| metrics = { | |
| "model_label": args.model_label or train_config.name, | |
| "config": train_config.name, | |
| "checkpoint": str(args.checkpoint_dir), | |
| "norm_stats_path": str(args.norm_stats_path.resolve()) if args.norm_stats_path else None, | |
| "global_step": checkpoint_metadata.get("global_step"), | |
| "warmup_runs": args.warmup_runs, | |
| "timed_runs": args.timed_runs, | |
| "num_denoise_steps": args.num_denoise_steps, | |
| "action_horizon": action_horizon, | |
| "actions_shape": list(actions.shape), | |
| "actions_mean": float(actions.mean()), | |
| "actions_std": float(actions.std()), | |
| "latency": { | |
| "model_infer_ms": model_infer_summary, | |
| "wall_ms": wall_summary, | |
| "model_infer_per_action_ms": _latency_summary([value / action_horizon for value in model_infer_ms]), | |
| "wall_per_action_ms": _latency_summary([value / action_horizon for value in wall_ms]), | |
| }, | |
| "latency_decomposition": _latency_decomposition_summary(timing_values), | |
| "frequency": _frequency_metrics(model_infer_summary, wall_summary, action_horizon), | |
| "model": model_metrics, | |
| "training": training_metrics, | |
| "approx_gflops": approx_gflops, | |
| "flop_profile": flop_profile, | |
| } | |
| metrics["table_row"] = _build_table_row(metrics) | |
| print(f"config={train_config.name}") | |
| print(f"checkpoint={args.checkpoint_dir}") | |
| print(f"warmup_runs={args.warmup_runs}") | |
| print(f"timed_runs={args.timed_runs}") | |
| print(f"num_denoise_steps={args.num_denoise_steps}") | |
| print(f"actions_shape={actions.shape}") | |
| print(f"actions_mean={actions.mean():.6f}") | |
| print(f"actions_std={actions.std():.6f}") | |
| _print_latency("model_infer_ms", model_infer_ms) | |
| _print_latency("wall_ms", wall_ms) | |
| _print_latency("model_infer_per_action_ms", [value / action_horizon for value in model_infer_ms]) | |
| _print_latency("wall_per_action_ms", [value / action_horizon for value in wall_ms]) | |
| decomposition = metrics["latency_decomposition"] | |
| if decomposition["available"]: | |
| if timing_values["vlm_prefix_ms"]: | |
| _print_latency("vlm_prefix_ms", timing_values["vlm_prefix_ms"]) | |
| if timing_values["denoising_ms"]: | |
| _print_latency("denoising_ms", timing_values["denoising_ms"]) | |
| if timing_values["denoising_ms_per_step"]: | |
| _print_latency("denoising_ms_per_step", timing_values["denoising_ms_per_step"]) | |
| print(f"model_query_hz={metrics['frequency']['model_query_hz']:.4f}") | |
| print(f"model_action_generation_hz={metrics['frequency']['model_action_generation_hz']:.4f}") | |
| print(f"wall_query_hz={metrics['frequency']['wall_query_hz']:.4f}") | |
| print(f"wall_action_generation_hz={metrics['frequency']['wall_action_generation_hz']:.4f}") | |
| print(f"model_total_params_b={model_metrics.get('model_total_params_b', float('nan')):.4f}") | |
| print(f"model_trainable_params_m={model_metrics.get('model_trainable_params_m', float('nan')):.2f}") | |
| print(f"checkpoint_size_gb={model_metrics.get('checkpoint_size_gb', float('nan')):.2f}") | |
| if training_metrics.get("training_time_hours") is not None: | |
| print(f"training_time_hours={training_metrics['training_time_hours']:.4f}") | |
| if approx_gflops is not None: | |
| print(f"approx_gflops={approx_gflops:.2f}") | |
| if flop_profile is not None: | |
| print(f"profiler_gflops={flop_profile['profiler_gflops']:.2f}") | |
| print(f"manual_conv2d_gflops={flop_profile['manual_conv2d_gflops']:.2f}") | |
| print(f"conv2d_modules_counted={flop_profile['manual_conv2d_modules_counted']}") | |
| print("table_row_markdown=" + _format_table_row(metrics["table_row"])) | |
| if args.metrics_out_path is not None: | |
| args.metrics_out_path.parent.mkdir(parents=True, exist_ok=True) | |
| args.metrics_out_path.write_text(json.dumps(metrics, indent=2) + "\n") | |
| print(f"metrics_out_path={args.metrics_out_path}") | |
| if args.table_out_path is not None: | |
| args.table_out_path.parent.mkdir(parents=True, exist_ok=True) | |
| args.table_out_path.write_text(_format_table(metrics["table_row"])) | |
| print(f"table_out_path={args.table_out_path}") | |
| def _assert_finite_actions(result: dict) -> None: | |
| actions = np.asarray(result["actions"]) | |
| if not np.isfinite(actions).all(): | |
| raise RuntimeError("Policy returned non-finite actions") | |
| def _make_example(example_name: str, train_config: Any) -> dict: | |
| example_name = example_name.lower() | |
| if example_name == "auto": | |
| config_text = f"{train_config.name} {type(train_config.data).__name__}".lower() | |
| example_name = "ur5" if "ur5" in config_text or "tube" in config_text or "pour" in config_text else "libero" | |
| if example_name == "ur5": | |
| return ur5_policy.make_ur5_example() | |
| if example_name == "libero": | |
| return libero_policy.make_libero_example() | |
| raise ValueError(f"Unknown example={example_name!r}. Expected auto, ur5, or libero.") | |
| def _print_latency(name: str, values: list[float]) -> None: | |
| summary = _latency_summary(values) | |
| print(f"{name}_mean={summary['mean']:.2f}") | |
| print(f"{name}_median={summary['median']:.2f}") | |
| print(f"{name}_p95={summary['p95']:.2f}") | |
| print(f"{name}_min={summary['min']:.2f}") | |
| print(f"{name}_max={summary['max']:.2f}") | |
| def _append_timing_values(timing_values: dict[str, list[float]], policy_timing: dict[str, Any]) -> None: | |
| for key in timing_values: | |
| value = _finite_float(policy_timing.get(key)) | |
| if value is not None: | |
| timing_values[key].append(value) | |
| def _latency_decomposition_summary(timing_values: dict[str, list[float]]) -> dict[str, Any]: | |
| sample_total = _latency_summary_or_none(timing_values["sample_total_ms"]) | |
| vlm_prefix = _latency_summary_or_none(timing_values["vlm_prefix_ms"]) | |
| denoising = _latency_summary_or_none(timing_values["denoising_ms"]) | |
| denoising_per_step = _latency_summary_or_none(timing_values["denoising_ms_per_step"]) | |
| denoising_steps = _latency_summary_or_none(timing_values["denoising_steps"]) | |
| other = _latency_summary_or_none(timing_values["other_ms"]) | |
| return { | |
| "available": bool(timing_values["sample_total_ms"]), | |
| "measurement": ( | |
| "Cached-observation policy benchmark. VLM prefix is image/language prefix embedding plus " | |
| "prefix transformer KV-cache construction. Denoising is the action denoising loop." | |
| ), | |
| "sample_total_ms": sample_total, | |
| "vlm_prefix_ms": vlm_prefix, | |
| "denoising_ms": denoising, | |
| "denoising_ms_per_step": denoising_per_step, | |
| "denoising_steps": denoising_steps, | |
| "other_ms": other, | |
| "fractions_of_mean_sample_total": { | |
| "vlm_prefix": _fraction(vlm_prefix.get("mean"), sample_total.get("mean")), | |
| "denoising": _fraction(denoising.get("mean"), sample_total.get("mean")), | |
| "other": _fraction(other.get("mean"), sample_total.get("mean")), | |
| }, | |
| } | |
| def _latency_summary_or_none(values: list[float]) -> dict[str, float | None]: | |
| if not values: | |
| return { | |
| "mean": None, | |
| "median": None, | |
| "p95": None, | |
| "min": None, | |
| "max": None, | |
| } | |
| return _latency_summary(values) | |
| def _finite_float(value: Any) -> float | None: | |
| try: | |
| number = float(value) | |
| except (TypeError, ValueError): | |
| return None | |
| return number if np.isfinite(number) else None | |
| def _fraction(numerator: Any, denominator: Any) -> float | None: | |
| num = _finite_float(numerator) | |
| den = _finite_float(denominator) | |
| if num is None or den is None or den <= 0: | |
| return None | |
| return float(num / den) | |
| def _latency_summary(values: list[float]) -> dict[str, float]: | |
| arr = np.asarray(values, dtype=np.float64) | |
| return { | |
| "mean": float(arr.mean()), | |
| "median": float(np.percentile(arr, 50)), | |
| "p95": float(np.percentile(arr, 95)), | |
| "min": float(arr.min()), | |
| "max": float(arr.max()), | |
| } | |
| def _frequency_metrics( | |
| model_infer_summary: dict[str, float], | |
| wall_summary: dict[str, float], | |
| action_horizon: int, | |
| ) -> dict[str, float]: | |
| model_query_hz = _hz_from_ms(model_infer_summary["median"]) | |
| wall_query_hz = _hz_from_ms(wall_summary["median"]) | |
| return { | |
| "model_query_hz": model_query_hz, | |
| "wall_query_hz": wall_query_hz, | |
| "model_action_generation_hz": model_query_hz * action_horizon, | |
| "wall_action_generation_hz": wall_query_hz * action_horizon, | |
| } | |
| def _hz_from_ms(milliseconds: float) -> float: | |
| return 1000.0 / milliseconds if milliseconds > 0 else float("nan") | |
| def _load_norm_stats_override(path: pathlib.Path | None): | |
| if path is None: | |
| return None | |
| path = pathlib.Path(path).expanduser().resolve() | |
| if path.is_file(): | |
| logging.info("Loaded explicit norm stats from %s", path) | |
| return _normalize.deserialize_json(path.read_text(encoding="utf-8")) | |
| logging.info("Loaded explicit norm stats from %s", path / "norm_stats.json") | |
| return _normalize.load(path) | |
| def _load_checkpoint_metadata(checkpoint_dir: pathlib.Path) -> dict[str, Any]: | |
| metadata_path = checkpoint_dir / "metadata.pt" | |
| if not metadata_path.exists(): | |
| return {} | |
| try: | |
| return torch.load(metadata_path, map_location="cpu", weights_only=False) | |
| except TypeError: | |
| return torch.load(metadata_path, map_location="cpu") | |
| def _policy_device(policy) -> torch.device | None: | |
| device_name = getattr(policy, "_pytorch_device", None) | |
| if device_name is not None: | |
| return torch.device(device_name) | |
| model = getattr(policy, "_model", None) | |
| if model is None: | |
| return None | |
| try: | |
| return next(model.parameters()).device | |
| except StopIteration: | |
| return None | |
| def _collect_model_metrics(policy, checkpoint_dir: pathlib.Path, metadata: dict[str, Any]) -> dict[str, float | int | None]: | |
| model = getattr(policy, "_model", None) | |
| metrics = dict(metadata.get("efficiency_metrics") or {}) | |
| if model is not None: | |
| if hasattr(model, "configure_trainable_parameters"): | |
| model.configure_trainable_parameters() | |
| params = list(model.parameters()) | |
| total_params = sum(param.numel() for param in params) | |
| trainable_params = sum(param.numel() for param in params if param.requires_grad) | |
| metrics.update( | |
| { | |
| "model_total_params": total_params, | |
| "model_trainable_params": trainable_params, | |
| "model_trainable_pct": trainable_params / total_params * 100 if total_params else 0.0, | |
| "model_total_params_b": total_params / 1e9, | |
| "model_trainable_params_m": trainable_params / 1e6, | |
| } | |
| ) | |
| weight_path = checkpoint_dir / "model.safetensors" | |
| if weight_path.exists(): | |
| checkpoint_size_bytes = weight_path.stat().st_size | |
| metrics["checkpoint_size_bytes"] = checkpoint_size_bytes | |
| metrics["checkpoint_size_gb"] = checkpoint_size_bytes / 1024**3 | |
| device = _policy_device(policy) | |
| if device is not None and device.type == "cuda" and torch.cuda.is_available(): | |
| metrics["cuda_peak_allocated_gb_inference"] = torch.cuda.max_memory_allocated(device) / 1024**3 | |
| metrics["cuda_peak_reserved_gb_inference"] = torch.cuda.max_memory_reserved(device) / 1024**3 | |
| return metrics | |
| def _collect_training_metrics(metadata: dict[str, Any], train_log: pathlib.Path | None) -> dict[str, Any]: | |
| efficiency_metrics = metadata.get("efficiency_metrics") or {} | |
| training_time_hours = ( | |
| efficiency_metrics.get("elapsed_training_hours") | |
| or efficiency_metrics.get("elapsed_after_first_step_hours") | |
| or efficiency_metrics.get("elapsed_run_hours") | |
| ) | |
| metrics: dict[str, Any] = { | |
| "training_time_hours": training_time_hours, | |
| "source": "checkpoint_metadata" if training_time_hours is not None else None, | |
| } | |
| if train_log is not None: | |
| parsed = _parse_training_time_from_log(train_log, metadata.get("global_step")) | |
| metrics.update(parsed) | |
| if parsed.get("training_time_hours") is not None: | |
| metrics["training_time_hours"] = parsed["training_time_hours"] | |
| metrics["source"] = "train_log" | |
| return metrics | |
| def _parse_training_time_from_log(train_log: pathlib.Path, global_step: int | None) -> dict[str, Any]: | |
| if not train_log.exists(): | |
| raise FileNotFoundError(f"Training log does not exist: {train_log}") | |
| created = None | |
| train_config = None | |
| first_step = None | |
| final_step = None | |
| final_save = None | |
| step_pattern = re.compile(rf"step={global_step}\s") if global_step is not None else None | |
| for raw_line in train_log.read_text(errors="replace").replace("\r", "\n").splitlines(): | |
| timestamp = _parse_log_time(raw_line) | |
| if timestamp is None: | |
| continue | |
| if "Created experiment checkpoint directory" in raw_line: | |
| created = timestamp | |
| if "Training config:" in raw_line: | |
| train_config = timestamp | |
| if re.search(r"step=1\s", raw_line): | |
| first_step = timestamp | |
| if step_pattern is not None and step_pattern.search(raw_line): | |
| final_step = timestamp | |
| if global_step is not None and f"Saved checkpoint at step {global_step}" in raw_line: | |
| final_save = timestamp | |
| end_time = final_save or final_step | |
| start_time = train_config or first_step or created | |
| training_time_hours = None if start_time is None or end_time is None else _elapsed_hours(start_time, end_time) | |
| return { | |
| "training_time_hours": training_time_hours, | |
| "created_to_final_save_hours": None if created is None or final_save is None else _elapsed_hours(created, final_save), | |
| "train_config_to_final_save_hours": ( | |
| None if train_config is None or final_save is None else _elapsed_hours(train_config, final_save) | |
| ), | |
| "first_step_to_final_save_hours": None if first_step is None or final_save is None else _elapsed_hours(first_step, final_save), | |
| "first_step_to_final_step_hours": None if first_step is None or final_step is None else _elapsed_hours(first_step, final_step), | |
| "log_path": str(train_log), | |
| } | |
| def _parse_log_time(line: str) -> dt.datetime | None: | |
| match = re.search(r"(\d{2}:\d{2}:\d{2}\.\d{3})", line) | |
| if match is None: | |
| return None | |
| return dt.datetime.strptime(match.group(1), "%H:%M:%S.%f") | |
| def _elapsed_hours(start: dt.datetime, end: dt.datetime) -> float: | |
| while end < start: | |
| end += dt.timedelta(days=1) | |
| return (end - start).total_seconds() / 3600 | |
| def _profile_policy_flops(policy, example: dict, device: torch.device | None) -> dict[str, Any]: | |
| activities = [torch.profiler.ProfilerActivity.CPU] | |
| if device is not None and device.type == "cuda" and torch.cuda.is_available(): | |
| activities.append(torch.profiler.ProfilerActivity.CUDA) | |
| torch.cuda.synchronize(device) | |
| conv2d_counter = _Conv2dFlopCounter(policy) | |
| with torch.inference_mode(): | |
| with conv2d_counter: | |
| with torch.profiler.profile(activities=activities, with_flops=True) as profiler: | |
| result = policy.infer(example) | |
| _assert_finite_actions(result) | |
| if device is not None and device.type == "cuda" and torch.cuda.is_available(): | |
| torch.cuda.synchronize(device) | |
| profiler_flops = sum((getattr(event, "flops", 0) or 0) for event in profiler.key_averages()) | |
| profiler_conv2d_flops = sum( | |
| (getattr(event, "flops", 0) or 0) | |
| for event in profiler.key_averages() | |
| if "conv2d" in getattr(event, "key", "") | |
| ) | |
| manual_conv2d_flops = conv2d_counter.total_flops | |
| # PyTorch currently warns that some aten::conv2d FLOPs cannot be computed, | |
| # and reports those as zero. Add our module-hook Conv2d count only when the | |
| # profiler did not already account for Conv2d FLOPs to avoid double counting | |
| # on future PyTorch versions. | |
| added_manual_conv2d_flops = manual_conv2d_flops if profiler_conv2d_flops == 0 else 0 | |
| total_flops = profiler_flops + added_manual_conv2d_flops | |
| return { | |
| "total_flops": float(total_flops), | |
| "total_gflops": float(total_flops) / 1e9, | |
| "profiler_flops": float(profiler_flops), | |
| "profiler_gflops": float(profiler_flops) / 1e9, | |
| "profiler_conv2d_flops": float(profiler_conv2d_flops), | |
| "profiler_conv2d_gflops": float(profiler_conv2d_flops) / 1e9, | |
| "manual_conv2d_flops": float(manual_conv2d_flops), | |
| "manual_conv2d_gflops": float(manual_conv2d_flops) / 1e9, | |
| "manual_conv2d_flops_added": float(added_manual_conv2d_flops), | |
| "manual_conv2d_gflops_added": float(added_manual_conv2d_flops) / 1e9, | |
| "manual_conv2d_modules_counted": conv2d_counter.modules_counted, | |
| "flop_convention": "2 FLOPs per multiply-add; bias additions are not counted.", | |
| "note": ( | |
| "Total combines PyTorch profiler FLOPs with manual Conv2d FLOPs only " | |
| "when profiler Conv2d FLOPs are zero." | |
| ), | |
| } | |
| class _Conv2dFlopCounter(contextlib.AbstractContextManager): | |
| def __init__(self, policy) -> None: | |
| self.model = getattr(policy, "_model", None) | |
| self.handles: list[Any] = [] | |
| self.total_flops = 0.0 | |
| self.modules_counted = 0 | |
| def __enter__(self): | |
| if self.model is None: | |
| return self | |
| for module in self.model.modules(): | |
| if isinstance(module, torch.nn.Conv2d): | |
| self.handles.append(module.register_forward_hook(self._hook)) | |
| return self | |
| def __exit__(self, exc_type, exc_value, traceback) -> bool: | |
| for handle in self.handles: | |
| handle.remove() | |
| self.handles.clear() | |
| return False | |
| def _hook(self, module: torch.nn.Conv2d, inputs: tuple[Any, ...], output: Any) -> None: | |
| if not inputs: | |
| return | |
| x = inputs[0] | |
| if not torch.is_tensor(x) or not torch.is_tensor(output) or output.ndim < 4: | |
| return | |
| batch = int(output.shape[0]) | |
| out_channels = int(output.shape[1]) | |
| out_h = int(output.shape[2]) | |
| out_w = int(output.shape[3]) | |
| kernel_h, kernel_w = module.kernel_size | |
| in_channels = int(module.in_channels) | |
| groups = int(module.groups) | |
| macs_per_output = (in_channels // groups) * int(kernel_h) * int(kernel_w) | |
| self.total_flops += float(batch * out_channels * out_h * out_w * macs_per_output * 2) | |
| self.modules_counted += 1 | |
| def _build_table_row(metrics: dict[str, Any]) -> dict[str, Any]: | |
| model_metrics = metrics["model"] | |
| training_metrics = metrics["training"] | |
| return { | |
| "Model": metrics["model_label"], | |
| "Model Size": f"{model_metrics.get('model_total_params_b', float('nan')):.2f}B", | |
| "Trainable Params": f"{model_metrics.get('model_trainable_params_m', float('nan')):.2f}M", | |
| "Training Time (hours)": _format_optional(training_metrics.get("training_time_hours"), precision=3), | |
| "GFLOPs": _format_optional(metrics.get("approx_gflops"), precision=2), | |
| "Inference Speed (ms)": f"{metrics['latency']['model_infer_ms']['median']:.2f}", | |
| "Inference Speed (Hz)": f"{metrics['frequency']['model_query_hz']:.3f}", | |
| "Action Gen. (Hz)": f"{metrics['frequency']['model_action_generation_hz']:.3f}", | |
| "Checkpoint Size": f"{model_metrics.get('checkpoint_size_gb', float('nan')):.2f}GB", | |
| } | |
| def _format_optional(value: Any, *, precision: int) -> str: | |
| if value is None: | |
| return "n/a" | |
| return f"{float(value):.{precision}f}" | |
| def _format_table_row(row: dict[str, Any]) -> str: | |
| return "| " + " | ".join(str(value) for value in row.values()) + " |" | |
| def _format_table(row: dict[str, Any]) -> str: | |
| header = "| " + " | ".join(row.keys()) + " |\n" | |
| separator = "| " + " | ".join("---" for _ in row) + " |\n" | |
| return header + separator + _format_table_row(row) + "\n" | |
| if __name__ == "__main__": | |
| logging.basicConfig(level=logging.INFO, force=True) | |
| main(tyro.cli(Args)) | |