#!/usr/bin/env python3 """Validate pinned X-VLA weights and LeRobot training YAML files without loading the model.""" from __future__ import annotations import json from pathlib import Path from typing import Any import draccus import numpy as np import yaml from safetensors import safe_open from lerobot.configs import parser from lerobot.configs.train import TrainPipelineConfig import lerobot.configs.policies as policy_configs from lerobot.policies.xvla.configuration_xvla import XVLAConfig # noqa: F401 MODEL_SHA256 = "f05bc0fab1c9523d7f5d6b41a651313641ca2227a88822249845da8e20e036c9" def parse_training_config(path: Path) -> TrainPipelineConfig: # Config parsing normally replaces unavailable CUDA with CPU. This check is # intentionally hardware-independent, so preserve the requested CUDA device. original_device_check = policy_configs.is_torch_device_available policy_configs.is_torch_device_available = lambda _device: True try: clean_path = parser.extract_path_fields_from_config( str(path), TrainPipelineConfig.__get_path_fields__() ) cfg = draccus.parse(TrainPipelineConfig, config_path=clean_path, args=[]) cfg.validate() return cfg finally: policy_configs.is_torch_device_available = original_device_check def validate_config( path: Path, expected_steps: int, expected_batch_size: int, expected_save_checkpoint: bool = True, ) -> dict[str, Any]: raw_config = yaml.safe_load(path.read_text(encoding="utf-8")) raw_policy = raw_config.get("policy") or {} for feature_field in ("input_features", "output_features"): if feature_field not in raw_policy or raw_policy[feature_field] is not None: raise ValueError( f"{path}: policy.{feature_field} must be an unquoted YAML null so " "the production CLI infers dataset features" ) cfg = parse_training_config(path) policy = cfg.policy if policy is None or policy.type != "xvla": raise ValueError(f"{path}: did not resolve to an XVLA policy") expected = { "device": "cuda", "dtype": "bfloat16", "action_mode": "auto", "max_action_dim": 20, "max_state_dim": 20, "num_image_views": 3, "freeze_vision_encoder": False, "freeze_language_encoder": False, "train_policy_transformer": True, "train_soft_prompts": True, } for key, value in expected.items(): if getattr(policy, key) != value: raise ValueError(f"{path}: policy.{key} did not resolve to {value!r}") if policy.input_features is not None or policy.output_features is not None: raise ValueError(f"{path}: policy feature dictionaries must be inferred from the dataset") if cfg.steps != expected_steps or cfg.batch_size != expected_batch_size: raise ValueError(f"{path}: unexpected steps or batch size") if cfg.save_checkpoint is not expected_save_checkpoint: raise ValueError(f"{path}: unexpected checkpoint-saving setting") if cfg.tolerance_s != 0.001 or cfg.dataset.eval_split != 0.1: raise ValueError(f"{path}: unexpected timestamp tolerance or eval split") if cfg.dataset.use_imagenet_stats: raise ValueError( f"{path}: dataset.use_imagenet_stats must be false because X-VLA normalizes " "images in its policy processor and merged camera stats are intentionally absent" ) return { "path": str(path), "steps": cfg.steps, "batch_size_per_process": cfg.batch_size, "save_checkpoint": cfg.save_checkpoint, "dataset_root": str(cfg.dataset.root), "policy_path": str(policy.pretrained_path), "dtype": policy.dtype, "action_mode": policy.action_mode, "num_image_views": policy.num_image_views, "use_imagenet_stats": cfg.dataset.use_imagenet_stats, "eval_split": cfg.dataset.eval_split, "tolerance_s": cfg.tolerance_s, } def main() -> None: project_root = Path(__file__).resolve().parents[1] model_root = project_root / "models" / "xvla-base" model_manifest = json.loads( (model_root / "download_manifest.json").read_text(encoding="utf-8") ) if model_manifest["model_sha256"] != MODEL_SHA256: raise ValueError("Pinned X-VLA model manifest SHA256 mismatch") tensor_count = 0 parameter_count = 0 dtype_counts: dict[str, int] = {} with safe_open(model_root / "model.safetensors", framework="pt", device="cpu") as stream: for key in stream.keys(): tensor = stream.get_slice(key) count = int(np.prod(tensor.get_shape())) dtype = str(tensor.get_dtype()) tensor_count += 1 parameter_count += count dtype_counts[dtype] = dtype_counts.get(dtype, 0) + count merged_validation = json.loads( (project_root / "data" / "merged" / "validation_report.json").read_text(encoding="utf-8") ) if merged_validation["status"] != "passed" or merged_validation["frames"] != 120_469: raise ValueError("Merged training dataset validation report is not usable") configs = [ validate_config(project_root / "configs" / "xvla_smoke.yaml", 20, 1), validate_config( project_root / "configs" / "xvla_pilot.yaml", 100, 16, expected_save_checkpoint=False, ), validate_config(project_root / "configs" / "xvla_full.yaml", 20_000, 4), validate_config(project_root / "configs" / "xvla_full_1gpu.yaml", 20_000, 16), ] report = { "status": "passed", "lerobot_version": "0.6.0", "model": { "root": str(model_root), "revision": model_manifest["revision"], "sha256": model_manifest["model_sha256"], "tensor_count": tensor_count, "parameter_count": parameter_count, "dtype_parameter_counts": dtype_counts, }, "dataset": { "root": merged_validation["root"], "episodes": merged_validation["episodes"], "frames": merged_validation["frames"], "tasks": len(merged_validation["tasks"]), }, "configs": configs, } report_path = project_root / "configs" / "train_setup_validation.json" report_path.write_text( json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) print( f"[passed] X-VLA setup: {parameter_count:,} parameters, " f"{merged_validation['frames']:,} data frames, {len(configs)} configs", flush=True, ) print(f"Validation report: {report_path}", flush=True) if __name__ == "__main__": main()