"""Typed YAML-driven configuration for inference and evaluation. The driver, model handle, and environment workers share one ``EvalConfig`` so configuration is parsed once and then passed through explicitly. YAML schema: model: checkpoint_path: # checkpoint directory train_config_path: null # null = /config.yml norm_key: libero_all cam_names: [face_view, right_wrist_view] action_horizon: null # null = read data.action_horizon_flow architecture: qwen2_5 # adapter registry key action_mode: flow # flow / ar / dllm / vqa / subtask env: type: libero # env registry key seed: 42 libero: task_suite_name: libero_spatial initial_states_path: DEFAULT num_trials_per_task: 50 task_indices: null # null = all tasks max_infer_times: 22 skip_intermediate_render: true force_render_task_indices: null # e.g. [5] always render that task chunk_granular_render_toggle: false rebuild_env_per_episode: false rollout_dir: null # save third-person MP4 replays per episode rollout_fps: 30 runtime: num_workers: 1 max_batch_size: 1 ws_port: 8765 log_dir: /path/to/wallx_log batch_sync_mode: false debug: deterministic_model: false Driver flow: cfg = load_eval_config(yaml_path) cfg = autofill_from_checkpoint(cfg) Unknown YAML fields are rejected to avoid silent typos. """ import dataclasses import os from dataclasses import dataclass, field from typing import Optional import yaml @dataclass class ModelSection: checkpoint_path: str = "" train_config_path: Optional[str] = None norm_key: str = "libero_all" cam_names: list = field(default_factory=lambda: ["face_view", "right_wrist_view"]) action_horizon: Optional[int] = None # Adapter implementation key in harrix.adapters.registry.ADAPTER_REGISTRY. architecture: str = "qwen2_5" # Inference algorithm. Each adapter validates its supported subset. action_mode: str = "flow" @dataclass class LiberoEnvParams: """LIBERO-specific env settings used when ``env.type == "libero"``.""" task_suite_name: str = "libero_spatial" initial_states_path: str = "DEFAULT" num_trials_per_task: int = 50 task_indices: Optional[list] = None max_infer_times: int = 22 # Render-skip is enabled by default. Listed task ids always render every # simulator step for contact-sensitive tasks. skip_intermediate_render: bool = True force_render_task_indices: Optional[list] = None # If enabled, render observables are toggled only when the chunk boundary # actually changes the desired state. chunk_granular_render_toggle: bool = False # Rebuild the simulator for every episode instead of only on task changes. # This is mainly a debugging option because it changes the simulator RNG path. rebuild_env_per_episode: bool = False # When set, save a third-person MP4 replay for each episode under this directory. # Falls back to the ``WALLX_ROLLOUT_DIR`` environment variable when null. rollout_dir: Optional[str] = None rollout_fps: int = 30 @dataclass class EnvSection: # Env implementation key in harrix.envs.registry. type: str = "libero" seed: int = 42 libero: LiberoEnvParams = field(default_factory=LiberoEnvParams) @dataclass class RuntimeSection: num_workers: int = 1 max_batch_size: int = 1 ws_port: int = 8765 log_dir: str = "/path/to/wallx_log" # Run fixed task-local frames instead of dynamic work stealing. This improves # reproducibility at the cost of possible idle workers inside a frame. batch_sync_mode: bool = False # Public Wall-X evaluation uses the in-process driver. Other drivers may be # enabled by downstream/internal integrations. driver_mode: str = "in_process" @dataclass class DebugSection: # Enable deterministic torch backend options for variance debugging. deterministic_model: bool = False @dataclass class EvalConfig: model: ModelSection = field(default_factory=ModelSection) env: EnvSection = field(default_factory=EnvSection) runtime: RuntimeSection = field(default_factory=RuntimeSection) debug: DebugSection = field(default_factory=DebugSection) def _build_dataclass(cls, raw): """Build a dataclass from a dict and reject unknown fields.""" if raw is None: return cls() if not isinstance(raw, dict): raise ValueError(f"Expected dict for {cls.__name__}, got {type(raw).__name__}") field_names = {f.name for f in dataclasses.fields(cls)} field_types = {f.name: f.type for f in dataclasses.fields(cls)} unknown = set(raw.keys()) - field_names if unknown: raise ValueError( f"Unknown field(s) {sorted(unknown)} in {cls.__name__}; " f"expected one of {sorted(field_names)}" ) built = {} for k, v in raw.items(): ft = field_types[k] if ( isinstance(ft, type) and dataclasses.is_dataclass(ft) and isinstance(v, dict) ): built[k] = _build_dataclass(ft, v) else: built[k] = v return cls(**built) def load_eval_config(yaml_path: str) -> EvalConfig: """Load ``EvalConfig`` from YAML and validate checkpoint_path.""" with open(yaml_path, "r") as f: raw = yaml.safe_load(f) if raw is None: raise ValueError(f"Empty YAML: {yaml_path}") cfg = _build_dataclass(EvalConfig, raw) if not cfg.model.checkpoint_path: raise ValueError(f"model.checkpoint_path is required in {yaml_path}") if not os.path.isdir(cfg.model.checkpoint_path): raise FileNotFoundError( f"model.checkpoint_path does not exist: {cfg.model.checkpoint_path}" ) return cfg def _load_train_config_yaml(path: str) -> dict: """Load a checkpoint-side train YAML. Training checkpoints may contain PyYAML-specific tags such as ``!!python/tuple``; ``safe_load`` cannot parse those. """ with open(path, "r") as f: return yaml.load(f, Loader=yaml.FullLoader) or {} def _read_action_horizon_flow(train_yml: dict) -> int: task = train_yml.get("task") or {} data = train_yml.get("data") or {} return int( train_yml.get("action_horizon_flow") or task.get("action_horizon_flow") or data.get("action_horizon_flow") or 32 ) def autofill_from_checkpoint(cfg: EvalConfig) -> EvalConfig: """Fill omitted model fields from the checkpoint-side train config. This resolves ``model.train_config_path`` and ``model.action_horizon``. """ if cfg.model.train_config_path is None: for fname in ("config.yml", "config.yaml"): cand = os.path.join(cfg.model.checkpoint_path, fname) if os.path.exists(cand): cfg.model.train_config_path = cand break else: raise FileNotFoundError( f"No config.yml/config.yaml in {cfg.model.checkpoint_path}; " "set model.train_config_path explicitly in YAML" ) if cfg.model.action_horizon is None: train_yml = _load_train_config_yaml(cfg.model.train_config_path) cfg.model.action_horizon = _read_action_horizon_flow(train_yml) return cfg