Update Wall-X to 1.1.0 (#104)

This commit is contained in:
Starrick Liu
2026-06-15 11:40:00 +08:00
committed by GitHub
parent e23a586846
commit 72834e7de5
200 changed files with 33916 additions and 16771 deletions
+62
View File
@@ -0,0 +1,62 @@
"""Typed training configuration system."""
from .data_config import DataConfig, LeRobotDataConfig
from .hyperparams_config import (
AdamWConfig,
ConstantSchedulerConfig,
CosineSchedulerConfig,
DMuonConfig,
OptimizerConfig,
SchedulerConfig,
StepSchedulerConfig,
TrainHyperParams,
)
from .infra_config import (
CheckpointConfig,
DebugConfig,
DistributedConfig,
LoggingConfig,
)
from .loader import load_config, save_config
from .model_config import ModelConfig, QActModelConfig
from .registry import (
register_data_config,
register_model_config,
register_optimizer_config,
register_scheduler_config,
)
from .task_config import TaskConfig
from .train_config import TrainConfig
__all__ = [
# Core
"TrainConfig",
"TaskConfig",
# Model configs
"ModelConfig",
"QActModelConfig",
# Data configs
"DataConfig",
"LeRobotDataConfig",
# Hyperparams
"TrainHyperParams",
"OptimizerConfig",
"AdamWConfig",
"DMuonConfig",
"SchedulerConfig",
"CosineSchedulerConfig",
"ConstantSchedulerConfig",
"StepSchedulerConfig",
# Infra configs
"DistributedConfig",
"LoggingConfig",
"CheckpointConfig",
"DebugConfig",
# Functions
"load_config",
"save_config",
"register_data_config",
"register_model_config",
"register_optimizer_config",
"register_scheduler_config",
]
+63
View File
@@ -0,0 +1,63 @@
"""Public data config dataclasses.
Only data backends shipped in the public package should define config classes
here. Internal backends register their dataclasses from their own packages via
``wall_x.config.registry.register_data_config``.
"""
from dataclasses import dataclass, field
from typing import Any, Dict, Optional
from .registry import register_data_config
@dataclass
class DataConfig:
"""Base fields shared by data backends.
``normalizer_config`` may contain:
- ``min_key``: stats key for the minimum value.
- ``delta_key``: stats key for the value range.
- ``customized_action_statistic_dof``: explicit action-stats JSON path.
"""
dataset_type: str = "lerobot"
resolution: Dict[str, int] = field(
default_factory=lambda: {
"face_view": 256,
"left_wrist_view": 256,
"right_wrist_view": 256,
}
)
train_test_split: float = 0.95
normalizer_config: Optional[Dict[str, Any]] = None
@register_data_config("lerobot")
@dataclass
class LeRobotDataConfig(DataConfig):
"""LeRobot data config.
``lerobot_config`` is expected to contain fields such as ``repo_id`` and
``root`` for a HuggingFace LeRobot dataset. ``norm_stats_path`` points to
explicit action normalizer stats; the core package does not bundle private
defaults.
"""
dataset_type: str = "lerobot"
lerobot_config: Optional[Dict[str, Any]] = None
key_mappings: Optional[Dict[str, Any]] = None
norm_stats_path: Optional[str] = None
priority_order: Optional[Dict[str, float]] = None
camera_name_mapping: Optional[Dict[str, str]] = None
num_workers: int = 4
action_tokenizer_path: Optional[str] = None
use_fast_tokenizer: bool = False
padding_side: str = "left"
noise_scheduler: Optional[Dict[str, Any]] = None
__all__ = [
"DataConfig",
"LeRobotDataConfig",
]
+129
View File
@@ -0,0 +1,129 @@
"""Training hyperparameter config dataclasses."""
from dataclasses import dataclass, field
from typing import List, Optional, Tuple
from .registry import register_optimizer_config, register_scheduler_config
@dataclass
class LRGroupConfig:
"""Named optimizer LR group matched by parameter-name substrings."""
name: str
lr: float
include: List[str] = field(default_factory=list)
fail_on_empty: bool = True
@dataclass
class OptimizerConfig:
"""Base optimizer config. ``optimizer_type`` selects a registered subclass."""
optimizer_type: str = "adamw"
learning_rate: float = 1e-4
max_grad_norm: float = 1.0
enable_grad_clip: bool = True
# Named parameter groups with independent learning rates. Unmatched
# trainable parameters remain in the base group using ``learning_rate``.
lr_groups: Optional[List[LRGroupConfig]] = None
# Optional action-expert LR split.
train_action_expert_only: bool = False
action_expert_learning_rate: Optional[float] = None
action_lr_keywords: Optional[List[str]] = None
@register_optimizer_config("adamw")
@dataclass
class AdamWConfig(OptimizerConfig):
"""AdamW optimizer config."""
optimizer_type: str = "adamw"
betas: Tuple[float, float] = (0.9, 0.98)
weight_decay: float = 1e-8
eps: float = 1e-8
fused: bool = True
foreach: Optional[bool] = None
@register_optimizer_config("dmuon")
@dataclass
class DMuonConfig(OptimizerConfig):
"""DMuon optimizer config."""
optimizer_type: str = "dmuon"
muon_lr: float = 0.02
momentum: float = 0.95
ns_steps: int = 5
muon_weight_decay: float = 0.0
adamw_lr: float = 1e-3
adamw_betas: Tuple[float, float] = (0.9, 0.999)
adamw_weight_decay: float = 0.01
adamw_eps: float = 1e-8
ns_backend: str = "gram"
ns_coefficients: str = "default"
nesterov: bool = True
@dataclass
class SchedulerConfig:
"""Base scheduler config. ``scheduler_type`` selects a registered subclass."""
scheduler_type: str = "cosine"
# Optional training-step cap. When > 0, the trainer sets
# loss_guard_should_stop=True once global_step >= num_training_steps,
# regardless of scheduler type. Cosine reads it for its own decay
# horizon; constant / step schedulers use it only for the stop signal.
num_training_steps: int = 0
@register_scheduler_config("cosine")
@dataclass
class CosineSchedulerConfig(SchedulerConfig):
"""Cosine annealing with warmup."""
scheduler_type: str = "cosine"
num_warmup_steps: int = 0
num_training_steps: int = 0
min_lr: Optional[float] = None # None means 0.1 * learning_rate at runtime.
@register_scheduler_config("constant")
@dataclass
class ConstantSchedulerConfig(SchedulerConfig):
"""Constant learning rate with no decay."""
scheduler_type: str = "constant"
@register_scheduler_config("step")
@dataclass
class StepSchedulerConfig(SchedulerConfig):
"""Step decay scheduler."""
scheduler_type: str = "step"
step_size: int = 10000
gamma: float = 0.1
@dataclass
class TrainHyperParams:
num_epoch: int = 1
batch_size_per_gpu: int = 1
gradient_accumulation_steps: int = 1
seed: int = 42
optimizer: OptimizerConfig = field(default_factory=AdamWConfig)
scheduler: SchedulerConfig = field(default_factory=CosineSchedulerConfig)
__all__ = [
"AdamWConfig",
"ConstantSchedulerConfig",
"CosineSchedulerConfig",
"DMuonConfig",
"LRGroupConfig",
"OptimizerConfig",
"SchedulerConfig",
"StepSchedulerConfig",
"TrainHyperParams",
]
+98
View File
@@ -0,0 +1,98 @@
"""Infrastructure config: distributed runtime, logging, checkpoints, and debug."""
from dataclasses import dataclass
from typing import Optional
@dataclass
class DistributedConfig:
# FSDP
use_fsdp: bool = False
fsdp_sharding_strategy: str = "full_shard"
fsdp_backward_prefetch: str = "backward_pre"
fsdp_cpu_offload: bool = False
fsdp_use_orig_params: bool = True
fsdp_limit_all_gathers: bool = True
fsdp_forward_prefetch: bool = False
fsdp_sync_module_states: bool = True
fsdp_save_policy: str = "full"
fsdp_hsdp_replicate_size: Optional[int] = None
# Mixed precision
use_mixed_precision: bool = True
bf16: bool = True
fsdp_reduce_dtype: str = "bf16"
use_amp: bool = False
use_gradient_checkpointing: bool = False
use_gradient_checkpointing_offload: bool = False
use_selective_recompute: bool = False
# DDP fallback
find_unused_parameters: bool = False
broadcast_buffers: bool = True
bucket_cap_mb: int = 25
@dataclass
class LoggingConfig:
log_name: str = "exp"
log_project: str = "wallx"
log_entity: Optional[str] = None
use_wandb: bool = True
wandb_offline: bool = False
log_interval: int = 1
save_interval: int = 1000
val_interval: int = 4000
epoch_save_interval: int = 1
gc_interval_steps: int = 1000
ignore_until_interval: int = 0
# Rolling-window for smoothing per-step training metrics displayed on the
# console (and reused by tqdm). 1 = raw per-step (historical behavior).
# 10 = DZ-style 10-step rolling average - diffusion losses are dominated
# by timestep-sampling noise per step; the smoothing only changes display,
# not training. Independent of log_interval (which buffers for wandb).
loss_log_smooth_window: int = 1
@dataclass
class CheckpointConfig:
"""Checkpoint save and resume options.
``resume_from`` is shorthand for setting every component-specific resume
path to the same checkpoint. Component-specific fields take precedence.
"""
save_path: str = "./ckpt"
validate_first: bool = False
# Shorthand path for all components.
resume_from: Optional[str] = None
# Component-specific resume paths.
resume_model: Optional[str] = None
resume_optimizer: Optional[str] = None
resume_scheduler: Optional[str] = None
resume_ema: Optional[str] = None
resume_rng: Optional[str] = None
resume_data: Optional[str] = None
resume_step: Optional[str] = None
def get_resume_path(self, component: str) -> Optional[str]:
"""Return the resume path for one checkpoint component."""
specific = getattr(self, f"resume_{component}", None)
if specific is not None:
return specific
return self.resume_from
@dataclass
class DebugConfig:
profile: bool = False
profile_save_path: str = "./profile"
profile_wait_iters: int = 1
profile_warmup_iters: int = 1
profile_active_iters: int = 3
show_time_details: bool = False
visualize_sample: bool = False
save_debug_batch_path: Optional[str] = None
nvtx: bool = False
# Formula MFU uses the local FLOPs estimate and measured step time.
enable_mfu: bool = False
# Optional FLOPs profiling runs an extra forward at step 0.
enable_mfu_profile: bool = False
+285
View File
@@ -0,0 +1,285 @@
"""Wall-X config loader."""
import dataclasses
import os
import shutil
from typing import Any, Type, TypeVar
import yaml
from .data_config import LeRobotDataConfig
from .hyperparams_config import (
AdamWConfig,
LRGroupConfig,
OptimizerConfig,
SchedulerConfig,
TrainHyperParams,
)
from .infra_config import (
CheckpointConfig,
DebugConfig,
DistributedConfig,
LoggingConfig,
)
from .model_config import ModelConfig, QActModelConfig
from .registry import (
get_data_config,
get_model_config,
get_optimizer_config,
get_scheduler_config,
registered_data_configs,
registered_model_configs,
registered_optimizer_configs,
registered_scheduler_configs,
)
from .task_config import TaskConfig
from .train_config import TrainConfig
T = TypeVar("T")
_CONFIG_PLUGINS_LOADED = False
class _TrainConfigSafeLoader(yaml.SafeLoader):
pass
def _construct_python_tuple(loader: yaml.SafeLoader, node: yaml.Node) -> tuple:
return tuple(loader.construct_sequence(node))
_TrainConfigSafeLoader.add_constructor(
"tag:yaml.org,2002:python/tuple", _construct_python_tuple
)
def _ensure_config_plugins_loaded() -> None:
"""Load optional internal config plugins when they are present."""
global _CONFIG_PLUGINS_LOADED
if _CONFIG_PLUGINS_LOADED:
return
_CONFIG_PLUGINS_LOADED = True
try:
from .internal_plugins import register_internal_config_plugins
except ImportError:
return
register_internal_config_plugins()
def load_config(config_path: str, cli_args: Any = None) -> TrainConfig:
"""Load a ``TrainConfig`` from a YAML file."""
_ensure_config_plugins_loaded()
with open(config_path, "r") as f:
raw = yaml.load(f, Loader=_TrainConfigSafeLoader)
if raw is None:
raise ValueError(f"Config file is empty: {config_path}")
model_type = raw.get("model_type")
if model_type is None:
raise ValueError(f"Missing required field 'model_type' in {config_path}")
config = TrainConfig(
model_type=model_type,
task=_build_dataclass(TaskConfig, raw.get("task", {})),
model=_build_model_config(model_type, raw.get("model", {})),
data=_build_data_config(raw.get("data", {})),
hyperparams=_build_hyperparams(raw.get("hyperparams", {})),
distributed=_build_dataclass(DistributedConfig, raw.get("distributed", {})),
logging=_build_dataclass(LoggingConfig, raw.get("logging", {})),
checkpoint=_build_dataclass(CheckpointConfig, raw.get("checkpoint", {})),
debug=_build_dataclass(DebugConfig, raw.get("debug", {})),
_raw_data=raw.get("data", {}),
_raw_yaml=raw,
dataset_path=raw.get("dataset_path"),
)
if cli_args is not None:
_apply_cli_overrides(config, cli_args)
_validate(config)
# Register the active data backend now that cfg is fully resolved
# (post-CLI-override, post-validate). This is the single source of
# truth for "which dataset backend is this run using".
from wall_x.data._registry import _set_data_backend
_set_data_backend(config.data.dataset_type)
return config
def save_config(config: TrainConfig, save_dir: str) -> str:
"""Save ``TrainConfig`` to ``config.yml`` under ``save_dir``."""
os.makedirs(save_dir, exist_ok=True)
config_path = os.path.join(save_dir, "config.yml")
data = _sanitize_for_yaml(dataclasses.asdict(config))
with open(config_path, "w") as f:
yaml.dump(
data, f, default_flow_style=False, allow_unicode=True, sort_keys=False
)
dataset_config_path = getattr(config.data, "dataset_config_path", None)
if dataset_config_path and os.path.exists(dataset_config_path):
dst = os.path.join(save_dir, "dataset_config.yml")
shutil.copy(dataset_config_path, dst)
return config_path
def _sanitize_for_yaml(obj: Any) -> Any:
"""Convert dataclass output to YAML-safe containers."""
if isinstance(obj, dict):
return {k: _sanitize_for_yaml(v) for k, v in obj.items()}
elif isinstance(obj, (list, tuple)):
return [_sanitize_for_yaml(v) for v in obj]
return obj
def _build_dataclass(cls: Type[T], raw: dict) -> T:
"""Build a dataclass from a dict, ignoring unknown keys."""
if not raw:
return cls()
field_names = {f.name for f in dataclasses.fields(cls)}
field_types = {f.name: f.type for f in dataclasses.fields(cls)}
filtered = {}
for k, v in raw.items():
if k not in field_names:
continue
ft = field_types[k]
if (
isinstance(ft, type)
and dataclasses.is_dataclass(ft)
and isinstance(v, dict)
):
filtered[k] = _build_dataclass(ft, v)
else:
filtered[k] = v
return cls(**filtered)
def _build_model_config(model_type: str, raw: dict) -> ModelConfig:
"""Build the registered model config for ``model_type``."""
cls = get_model_config(model_type)
if cls is None:
raise ValueError(
f"Unknown model_type: '{model_type}'. "
f"Supported: {registered_model_configs()}"
)
return _build_dataclass(cls, raw)
def _build_data_config(raw: dict):
"""Build the registered data config for ``dataset_type``."""
if not raw:
return LeRobotDataConfig()
dataset_type = raw.get("dataset_type", "lerobot")
cls = get_data_config(dataset_type)
if cls is None:
raise ValueError(
f"Unknown dataset_type: '{dataset_type}'. "
f"Supported: {registered_data_configs()}"
)
return _build_dataclass(cls, raw)
def _build_optimizer_config(raw: dict) -> OptimizerConfig:
"""Build the registered optimizer config for ``optimizer_type``."""
if not raw:
return AdamWConfig()
raw = dict(raw)
optimizer_type = raw.get("optimizer_type", "adamw")
cls = get_optimizer_config(optimizer_type)
if cls is None:
raise ValueError(
f"Unknown optimizer_type: '{optimizer_type}'. "
f"Supported: {registered_optimizer_configs()}"
)
if "betas" in raw and isinstance(raw["betas"], list):
raw["betas"] = tuple(raw["betas"])
if "adamw_betas" in raw and isinstance(raw["adamw_betas"], list):
raw["adamw_betas"] = tuple(raw["adamw_betas"])
if raw.get("lr_groups") is not None:
raw["lr_groups"] = [
_build_dataclass(LRGroupConfig, group) for group in raw["lr_groups"]
]
return _build_dataclass(cls, raw)
def _build_scheduler_config(raw: dict) -> SchedulerConfig:
"""Build the registered scheduler config for ``scheduler_type``."""
if not raw:
cls = get_scheduler_config("cosine")
if cls is None:
raise ValueError("Scheduler config 'cosine' is not registered")
return cls()
scheduler_type = raw.get("scheduler_type", "cosine")
cls = get_scheduler_config(scheduler_type)
if cls is None:
raise ValueError(
f"Unknown scheduler_type: '{scheduler_type}'. "
f"Supported: {registered_scheduler_configs()}"
)
return _build_dataclass(cls, raw)
def _build_hyperparams(raw: dict) -> TrainHyperParams:
"""Build ``TrainHyperParams`` with polymorphic optimizer/scheduler config."""
if not raw:
return TrainHyperParams()
raw = dict(raw) # shallow copy to avoid mutating caller's dict
optimizer_raw = raw.pop("optimizer", {})
scheduler_raw = raw.pop("scheduler", {})
optimizer = _build_optimizer_config(optimizer_raw)
scheduler = _build_scheduler_config(scheduler_raw)
hp = _build_dataclass(TrainHyperParams, raw)
hp.optimizer = optimizer
hp.scheduler = scheduler
return hp
def _apply_cli_overrides(config: TrainConfig, args: Any) -> None:
"""Apply CLI overrides to the loaded config."""
if getattr(args, "fsdp_sharding_strategy", None) is not None:
config.distributed.fsdp_sharding_strategy = args.fsdp_sharding_strategy
if getattr(args, "debug", False):
config.logging.log_name = "debug"
config.logging.log_project = "debug"
config.checkpoint.save_path = "./ckpt/debug"
if getattr(args, "visualize", False):
config.debug.visualize_sample = True
if getattr(args, "wandb_offline", None) is not None:
config.logging.wandb_offline = args.wandb_offline in ("true", "True", "1")
def _validate(config: TrainConfig) -> None:
"""Validate required fields and model-specific config."""
if not config.model_type:
raise ValueError("model_type is required")
if not config.task.dof_config:
raise ValueError("task.dof_config is required (cannot be empty)")
if isinstance(config.model, QActModelConfig):
model = config.model
if not model.config_path:
raise ValueError("model.config_path is required for QAct models")
if not model.processor_path:
raise ValueError("model.processor_path is required for QAct models")
+42
View File
@@ -0,0 +1,42 @@
"""Base model config dataclasses.
Optional model variants register their config dataclasses from their own
packages via ``wall_x.config.registry.register_model_config``.
"""
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
from .registry import register_model_config
@dataclass
class ModelConfig:
"""Base model architecture fields."""
use_ema: bool = False
attn_implementation: Optional[str] = None
attn_deterministic: Optional[bool] = None
ar_loss_weight: float = 1.0
@register_model_config("qwen2_5")
@dataclass
class QActModelConfig(ModelConfig):
"""QAct model config for the Qwen2.5 VLA path."""
config_path: str = ""
processor_path: str = ""
pretrained_path: Optional[str] = None
backbone: str = "qwen2_5"
action_tokenizer_type: Optional[str] = None
action_tokenizer_path: Optional[str] = None
action_tokenizer_checkpoint_path: Optional[str] = None
action_tokenizer_config_dir: Optional[str] = None
new_special_tokens: Optional[List[str]] = None
flow_loss_weight: float = 1.0
enable_customized_robot_config: bool = False
customized_robot_config: Optional[Dict[str, Any]] = None
__all__ = ["ModelConfig", "QActModelConfig"]
+99
View File
@@ -0,0 +1,99 @@
"""Runtime registries for typed config variants.
Core Wall-X keeps only base config types in ``wall_x.config``. Optional
datasets, model families, and optimizer variants register their dataclasses
from their own packages, so trimmed distributions do not leave dead config
entries behind.
"""
from __future__ import annotations
from collections.abc import Callable
from typing import Any, TypeVar
T = TypeVar("T")
_DATA_CONFIG_REGISTRY: dict[str, type[Any]] = {}
_MODEL_CONFIG_REGISTRY: dict[str, type[Any]] = {}
_OPTIMIZER_CONFIG_REGISTRY: dict[str, type[Any]] = {}
_SCHEDULER_CONFIG_REGISTRY: dict[str, type[Any]] = {}
def _register(
registry: dict[str, type[Any]], names: tuple[str, ...], cls: type[T]
) -> type[T]:
for name in names:
if not name:
raise ValueError("Config registry name must be non-empty")
existing = registry.get(name)
if existing is not None and existing is not cls:
raise ValueError(
f"Config name {name!r} is already registered by "
f"{existing.__module__}.{existing.__name__}"
)
registry[name] = cls
return cls
def register_data_config(*names: str) -> Callable[[type[T]], type[T]]:
return lambda cls: _register(_DATA_CONFIG_REGISTRY, names, cls)
def register_model_config(*names: str) -> Callable[[type[T]], type[T]]:
return lambda cls: _register(_MODEL_CONFIG_REGISTRY, names, cls)
def register_optimizer_config(*names: str) -> Callable[[type[T]], type[T]]:
return lambda cls: _register(_OPTIMIZER_CONFIG_REGISTRY, names, cls)
def register_scheduler_config(*names: str) -> Callable[[type[T]], type[T]]:
return lambda cls: _register(_SCHEDULER_CONFIG_REGISTRY, names, cls)
def get_data_config(name: str) -> type[Any] | None:
return _DATA_CONFIG_REGISTRY.get(name)
def get_model_config(name: str) -> type[Any] | None:
return _MODEL_CONFIG_REGISTRY.get(name)
def get_optimizer_config(name: str) -> type[Any] | None:
return _OPTIMIZER_CONFIG_REGISTRY.get(name)
def get_scheduler_config(name: str) -> type[Any] | None:
return _SCHEDULER_CONFIG_REGISTRY.get(name)
def registered_data_configs() -> list[str]:
return sorted(_DATA_CONFIG_REGISTRY)
def registered_model_configs() -> list[str]:
return sorted(_MODEL_CONFIG_REGISTRY)
def registered_optimizer_configs() -> list[str]:
return sorted(_OPTIMIZER_CONFIG_REGISTRY)
def registered_scheduler_configs() -> list[str]:
return sorted(_SCHEDULER_CONFIG_REGISTRY)
__all__ = [
"get_data_config",
"get_model_config",
"get_optimizer_config",
"get_scheduler_config",
"register_data_config",
"register_model_config",
"register_optimizer_config",
"register_scheduler_config",
"registered_data_configs",
"registered_model_configs",
"registered_optimizer_configs",
"registered_scheduler_configs",
]
+17
View File
@@ -0,0 +1,17 @@
"""Task config shared by model and data code."""
from dataclasses import dataclass, field
from typing import Any, Dict, Optional
@dataclass
class TaskConfig:
"""Robot task definition shared by model construction and data slicing."""
dof_config: Dict[str, int] = field(default_factory=dict)
agent_pos_config: Dict[str, int] = field(default_factory=dict)
ar_dof_config: Optional[Dict[str, int]] = None
action_horizon: int = 32
action_horizon_flow: int = 32
noise_scheduler: Optional[Dict[str, Any]] = None
use_state_string_representation: bool = False
+81
View File
@@ -0,0 +1,81 @@
"""Top-level training config."""
import dataclasses
from dataclasses import dataclass, field
from typing import Any, Dict
from .data_config import DataConfig
from .hyperparams_config import TrainHyperParams
from .infra_config import (
CheckpointConfig,
DebugConfig,
DistributedConfig,
LoggingConfig,
)
from .model_config import ModelConfig
from .task_config import TaskConfig
@dataclass
class TrainConfig:
"""Top-level Wall-X training config."""
model_type: str = "qwen2_5"
task: TaskConfig = field(default_factory=TaskConfig)
model: ModelConfig = field(default_factory=ModelConfig)
data: DataConfig = field(default_factory=DataConfig)
hyperparams: TrainHyperParams = field(default_factory=TrainHyperParams)
distributed: DistributedConfig = field(default_factory=DistributedConfig)
logging: LoggingConfig = field(default_factory=LoggingConfig)
checkpoint: CheckpointConfig = field(default_factory=CheckpointConfig)
debug: DebugConfig = field(default_factory=DebugConfig)
# Raw YAML sections preserved verbatim for backend APIs that read fields
# not captured by the typed DataConfig dataclass.
_raw_data: Dict[str, Any] = field(default_factory=dict)
# Full raw YAML dict for backend-specific compatibility paths.
_raw_yaml: Dict[str, Any] = field(default_factory=dict)
# dataset_path lives at top level in YAML, consumed by data loaders directly
dataset_path: Any = None
@property
def action_dim(self) -> int:
return sum(self.task.dof_config.values())
@property
def propri_dim(self) -> int:
return sum(self.task.agent_pos_config.values())
def build_data_loader_dict(self) -> Dict[str, Any]:
"""Build the raw dict consumed by backend-specific compatibility paths.
Merges ``_raw_data`` (verbatim YAML ``data:`` section) with task
fields (dof_config, action_horizon, etc.) and hyperparams
(batch_size). Backend compatibility APIs may read fields that the
typed DataConfig dataclass does not carry.
This keeps legacy flat configs working while the main config surface
stays typed.
"""
# Start with the raw YAML data section, then add typed task defaults.
data_dict = dict(self._raw_data)
task_dict = dataclasses.asdict(self.task)
for key in (
"dof_config",
"agent_pos_config",
"action_horizon",
"action_horizon_flow",
"ar_dof_config",
"use_state_string_representation",
):
if key in task_dict and task_dict[key] is not None:
data_dict.setdefault(key, task_dict[key])
data_dict.setdefault("batch_size_per_gpu", self.hyperparams.batch_size_per_gpu)
data_dict.setdefault("batch_size", self.hyperparams.batch_size_per_gpu)
result: Dict[str, Any] = {
"model_type": self.model_type,
"data": data_dict,
**task_dict,
}
if self.dataset_path is not None:
result["dataset_path"] = self.dataset_path
return result