File size: 21,898 Bytes
5cd1287 977b06e 5cd1287 977b06e 5cd1287 977b06e 5cd1287 977b06e 5cd1287 977b06e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 | # Modified in the ReTime research artifact; see NOTICE.
"""
Shared configuration / utility helpers for framework components:
- NamespaceWithGet: lightweight namespace behaving like a dict
- OmegaConf conversion helpers
- Config merging decorator for model __init__
- Checkpoint config/statistics loading
"""
import functools
import inspect
import json
import os
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from omegaconf import OmegaConf
from starVLA.training.trainer_utils import initialize_overwatch
# Initialize Overwatch =>> Wraps `logging.Logger`
overwatch = initialize_overwatch(__name__)
class NamespaceWithGet(SimpleNamespace):
def get(self, key, default=None):
"""
Return attribute value if present, else default (dict-like API).
Args:
key: Attribute name.
default: Fallback if attribute missing.
Returns:
Any: Stored value or default.
"""
return getattr(self, key, default)
def items(self):
"""
Iterate (key, value) pairs like dict.items().
Returns:
Generator[Tuple[str, Any], None, None]
"""
return ((key, getattr(self, key)) for key in self.__dict__)
def __iter__(self):
"""
Return iterator over attribute keys (enables dict unpacking **obj).
Returns:
Iterator[str]
"""
return iter(self.__dict__)
def to_dict(self):
"""
Recursively convert nested NamespaceWithGet objects into plain dicts.
Returns:
dict: Fully materialized dictionary structure.
"""
return {key: value.to_dict() if isinstance(value, NamespaceWithGet) else value for key, value in self.items()}
def dict_to_namespace(d):
"""
Create an OmegaConf config from a plain dictionary.
Args:
d: Input dictionary.
Returns:
OmegaConf: DictConfig instance.
"""
return OmegaConf.create(d)
def _to_omegaconf(x: Any):
"""
Convert diverse input types into an OmegaConf object.
Accepted types:
- None -> empty DictConfig
- str path -> load YAML/JSON via OmegaConf.load
- dict -> DictConfig
- DictConfig / ListConfig -> returned unchanged
- NamespaceWithGet / SimpleNamespace -> converted via vars()/to_dict()
Args:
x: Input candidate.
Returns:
OmegaConf: Normalized configuration node.
"""
if x is None:
return OmegaConf.create({})
if isinstance(x, OmegaConf.__class__): # fallback, typically not hit
return x
try:
# OmegaConf node detection
from omegaconf import DictConfig, ListConfig
if isinstance(x, (DictConfig, ListConfig)):
return x
except Exception:
pass
if isinstance(x, str):
# treat as path
return OmegaConf.load(x)
if isinstance(x, dict):
return OmegaConf.create(x)
if isinstance(x, NamespaceWithGet) or isinstance(x, SimpleNamespace):
# convert to plain dict
try:
d = x.to_dict() if hasattr(x, "to_dict") else vars(x)
except Exception:
d = vars(x)
return OmegaConf.create(d)
# fallback: try to create
return OmegaConf.create(x)
def merge_pram_config(init):
"""
Decorator for __init__ to unify config handling.
Behavior:
1. Extract 'config' kwarg / arg (path | dict | OmegaConf | namespace)
2. Convert to OmegaConf
3. Merge with explicitly passed init parameters (explicit overrides file)
4. Attach merged config to self.config
5. Call original __init__ with merged config
Args:
init: Original __init__ function.
Returns:
Wrapped initializer.
"""
@functools.wraps(init)
def wrapper(self, *args, **kwargs):
# Map positional args to parameter names (excluding self)
sig = inspect.signature(init)
param_names = [name for i, (name, p) in enumerate(sig.parameters.items()) if i > 0]
init_kwargs = {}
for name, val in zip(param_names, args):
init_kwargs[name] = val
# override with explicit kwargs
init_kwargs.update(kwargs)
# get provided config (if any)
provided_config = init_kwargs.get("config", None)
loaded_cfg = _to_omegaconf(provided_config)
# build params cfg from explicit init args (other than config)
params = {k: v for k, v in init_kwargs.items() if k != "config"}
params_cfg = OmegaConf.create(params) if params else OmegaConf.create({})
# merge: loaded_cfg <- params_cfg (params override file)
merged = OmegaConf.merge(loaded_cfg, params_cfg)
# set on instance
try:
# prefer attaching OmegaConf directly
self.config = merged
except Exception:
# fallback to dict
self.config = OmegaConf.to_container(merged, resolve=True)
# prepare kwargs for original init: ensure config is the merged OmegaConf
call_kwargs = dict(init_kwargs)
call_kwargs["config"] = merged
# call original __init__ using keyword args only (safer)
return init(self, **call_kwargs)
return wrapper
def merge_framework_config(default_config_cls, cfg):
"""
Merge a framework's default config (dataclass) with the incoming YAML config.
Rules:
- default_config_cls provides documented defaults for `cfg.framework`
- YAML values (cfg.framework) override matching defaults
- Extra YAML keys not in defaults are preserved (Config-as-API flexibility)
- Missing YAML keys fall back to defaults (less YAML boilerplate)
The merge only touches the `cfg.framework` sub-tree; datasets / trainer / etc.
are left untouched.
Args:
default_config_cls: A dataclass **class** (not instance) whose fields() define
the default framework config with type hints and comments.
cfg: The full OmegaConf config (must contain cfg.framework).
Returns:
cfg: The same config object with cfg.framework replaced by the merged result.
"""
import dataclasses
from omegaconf import DictConfig, OmegaConf
# 1. Instantiate defaults and convert to OmegaConf
defaults_instance = default_config_cls()
defaults_dict = dataclasses.asdict(defaults_instance)
defaults_omega = OmegaConf.create(defaults_dict)
# 2. Extract the YAML framework section
if hasattr(cfg, "framework"):
# Unwrap AccessTrackedConfig if needed
yaml_fw = cfg.framework
if hasattr(yaml_fw, "_cfg"):
yaml_fw = yaml_fw._cfg
if not isinstance(yaml_fw, DictConfig):
yaml_fw = OmegaConf.create(yaml_fw if isinstance(yaml_fw, dict) else {})
else:
yaml_fw = OmegaConf.create({})
# 3. Merge: defaults first, YAML overrides (YAML wins on conflicts)
merged_fw = OmegaConf.merge(defaults_omega, yaml_fw)
# 4. Write back into the original cfg
# Handle both OmegaConf and AccessTrackedConfig transparently
if hasattr(cfg, "_cfg") and isinstance(cfg._cfg, DictConfig):
# AccessTrackedConfig caches child wrappers in _children dict.
# After replacing the underlying DictConfig node, the old child wrapper
# still points to the pre-merge node (stale data). We must invalidate
# the cache so the next attribute access creates a fresh wrapper around
# the merged node.
#
# However, the old child's _local_accessed set records which keys were
# already read (e.g. "name" from build_framework). Deleting the child
# would lose that tracking info, causing save_accessed_config to omit
# those keys from config.yaml. So we preserve and restore it.
cfg._cfg.framework = merged_fw
if hasattr(cfg, "_children") and "framework" in cfg._children:
old_accessed = cfg._children["framework"]._local_accessed.copy()
del cfg._children["framework"] # invalidate stale cache
new_child = cfg.framework # re-create child around merged_fw
new_child._local_accessed.update(old_accessed) # restore tracking
elif isinstance(cfg, DictConfig):
cfg.framework = merged_fw
else:
# Fallback β try direct attribute setting
try:
cfg.framework = merged_fw
except Exception:
overwatch.warning("Could not write merged framework config back to cfg.")
return cfg
def populate_layerwise_dit_cfg(cfg, *, dit_hidden_dim: int, num_dit_layers: int):
"""
Populate ``framework.action_model.diffusion_model_cfg`` with the DiT shape
fields required by ``LayerwiseFlowmatchingActionHead``.
Why this helper exists:
The action head is intentionally agnostic of the VLM backbone β it only
consumes ``diffusion_model_cfg``. Each framework (QwenPI, QwenPI_v3,
...) is responsible for deciding the DiT shape (depth + hidden) from
whatever source it likes (LLM hidden, a compressed projector dim, ...)
and writing it here BEFORE calling ``get_action_model``.
Fields written (override any stale YAML values):
- num_layers = num_dit_layers
- input_embedding_dim = dit_hidden_dim
- cross_attention_dim = dit_hidden_dim (encoder is pre-projected)
- num_attention_heads = dit_hidden_dim // attention_head_dim
(uses existing attention_head_dim if set, else 64)
Args:
cfg: Full OmegaConf config.
dit_hidden_dim: DiT internal hidden dim.
num_dit_layers: Number of DiT cross-attention layers.
Returns:
The (mutated) diffusion_model_cfg node.
"""
dit_cfg = cfg.framework.action_model.diffusion_model_cfg
head_dim = dit_cfg.get("attention_head_dim", None) or 64
dit_cfg.attention_head_dim = head_dim
dit_cfg.num_layers = int(num_dit_layers)
dit_cfg.input_embedding_dim = int(dit_hidden_dim)
dit_cfg.cross_attention_dim = int(dit_hidden_dim)
dit_cfg.num_attention_heads = int(dit_hidden_dim) // int(head_dim)
return dit_cfg
def read_model_config(pretrained_checkpoint):
"""
Load global model configuration and dataset normalization statistics
associated with a saved checkpoint (.pt).
Expected directory layout:
<run_dir>/checkpoints/<name>.pt
<run_dir>/config.json
<run_dir>/dataset_statistics.json
Args:
pretrained_checkpoint: Path to a .pt checkpoint file.
Returns:
tuple:
global_cfg (dict): Loaded config.json contents.
norm_stats (dict): Dataset statistics for (de)normalization.
Raises:
FileNotFoundError: If checkpoint or required JSON files are missing.
AssertionError: If file suffix or structure invalid.
"""
if os.path.isfile(pretrained_checkpoint):
overwatch.info(f"Loading from local checkpoint path `{(checkpoint_pt := Path(pretrained_checkpoint))}`")
# [Validate] Checkpoint Path should look like
# `.../<RUN_ID>/checkpoints/<CHECKPOINT_PATH>.pt|.safetensors`
assert checkpoint_pt.suffix in {".pt", ".safetensors"}
run_dir = checkpoint_pt.parents[1]
# Get paths for `config.json`, `dataset_statistics.json` and pretrained checkpoint
config_json, dataset_statistics_json = run_dir / "config.json", run_dir / "dataset_statistics.json"
assert config_json.exists(), f"Missing `config.json` for `{run_dir = }`"
assert dataset_statistics_json.exists(), f"Missing `dataset_statistics.json` for `{run_dir = }`"
# Otherwise =>> try looking for a match on `model_id_or_path` on the HF Hub (`model_id_or_path`)
# Load VLA Config (and corresponding base VLM `ModelConfig`) from `config.json`
with open(config_json, "r") as f:
global_cfg = json.load(f)
# Normalise legacy / pre-v0.21 configs to current schema (idempotent;
# also ensures `past_action_window_size`, `action_horizon`,
# `future_action_window_size`, etc. are all present for downstream code).
try:
_oc = OmegaConf.create(global_cfg)
apply_config_compat(_oc)
global_cfg = OmegaConf.to_container(_oc, resolve=True)
except Exception as e:
overwatch.warning(f"apply_config_compat failed on `{config_json}`: {e}")
# Load Dataset Statistics for Action Denormalization
with open(dataset_statistics_json, "r") as f:
norm_stats = json.load(f)
else:
overwatch.error(f"Pretrained checkpoint `{pretrained_checkpoint}` does not exist.")
raise FileNotFoundError(f"Pretrained checkpoint `{pretrained_checkpoint}` does not exist.")
return global_cfg, norm_stats
def read_mode_config(pretrained_checkpoint):
"""
Same as read_model_config (legacy duplicate kept for backward compatibility).
Args:
pretrained_checkpoint: Path to a .pt checkpoint file.
Returns:
tuple:
vla_cfg (dict)
norm_stats (dict)
"""
if os.path.isfile(pretrained_checkpoint):
overwatch.info(f"Loading from local checkpoint path `{(checkpoint_pt := Path(pretrained_checkpoint))}`")
# [Validate] Checkpoint Path should look like
# `.../<RUN_ID>/checkpoints/<CHECKPOINT_PATH>.pt|.safetensors`
assert checkpoint_pt.suffix in {".pt", ".safetensors"}
run_dir = checkpoint_pt.parents[1]
# Get paths for `config.json`, `dataset_statistics.json` and pretrained checkpoint
config_yaml, dataset_statistics_json = run_dir / "config.yaml", run_dir / "dataset_statistics.json"
assert config_yaml.exists(), f"Missing `config.yaml` for `{run_dir = }`"
assert dataset_statistics_json.exists(), f"Missing `dataset_statistics.json` for `{run_dir = }`"
# Otherwise =>> try looking for a match on `model_id_or_path` on the HF Hub (`model_id_or_path`)
# Load VLA Config (and corresponding base VLM `ModelConfig`) from `config.json`
try:
ocfg = OmegaConf.load(str(config_yaml))
# Normalise legacy / pre-v0.21 configs to current schema (idempotent).
apply_config_compat(ocfg)
global_cfg = OmegaConf.to_container(ocfg, resolve=True)
except Exception as e:
overwatch.error(f"Failed to load YAML config `{config_yaml}`: {e}")
raise
# Load Dataset Statistics for Action Denormalization
with open(dataset_statistics_json, "r") as f:
norm_stats = json.load(f)
else:
overwatch.error(f"Pretrained checkpoint `{pretrained_checkpoint}` does not exist.")
raise FileNotFoundError(f"Pretrained checkpoint `{pretrained_checkpoint}` does not exist.")
return global_cfg, norm_stats
# Normalize legacy configuration keys before model construction. The
# transformation is idempotent and leaves framework class signatures intact.
CONFIG_VERSION = "0.21"
def apply_config_compat(cfg, *, strict: bool = False):
"""
Normalise an arbitrary (old or new) starVLA training config into the
current `version_id == "0.21"` schema.
Performed transformations (each applied only when needed):
1. `framework.action_model.action_horizon` β `future_action_window_size`
- `action_horizon` is canonical (preferred user-facing name).
- `future_action_window_size = action_horizon - 1` is auto-filled so
framework code that still reads the old key keeps working.
- If both are present and inconsistent, a warning is emitted and
`action_horizon` wins.
2. `framework.action_model.diffusion_model_cfg.output_dim`
- Auto-filled from `framework.action_model.hidden_size` when missing.
3. `framework.action_model.diffusion_model_cfg.cross_attention_dim`
- Auto-filled from `framework.qwenvl.vl_hidden_dim` when missing.
Frameworks that further override this at runtime (e.g. QwenGR00T)
are unaffected.
4. `framework.action_model.action_hidden_dim`
- Auto-filled from `hidden_size` when missing. OFT-family frameworks
still overwrite this from VLM hidden_size at runtime.
5. `framework.action_model.past_action_window_size`
- Auto-filled to `0` when missing. All released starVLA frameworks
run with past=0; the field is therefore dropped from user YAMLs
and only re-materialised here for legacy code that still reads it.
6. `cfg.version_id` is stamped to `"0.21"`.
Args:
cfg: An OmegaConf DictConfig (or anything _to_omegaconf can wrap).
strict: If True, raise on inconsistent values instead of warning.
Returns:
The same `cfg` object (mutated in place) for chaining convenience.
"""
from omegaconf import OmegaConf
if cfg is None:
return cfg
src_version = OmegaConf.select(cfg, "version_id", default=None)
# ---- 1. action_horizon β future_action_window_size ----
am_path = "framework.action_model"
am = OmegaConf.select(cfg, am_path, default=None)
if am is not None:
ah = OmegaConf.select(am, "action_horizon", default=None)
fw = OmegaConf.select(am, "future_action_window_size", default=None)
if ah is None and fw is not None:
ah = int(fw) + 1
OmegaConf.update(cfg, f"{am_path}.action_horizon", ah, force_add=True)
elif ah is not None and fw is None:
fw = int(ah) - 1
OmegaConf.update(cfg, f"{am_path}.future_action_window_size", fw, force_add=True)
elif ah is not None and fw is not None and int(ah) != int(fw) + 1:
msg = (
f"[apply_config_compat] inconsistent action_horizon={ah} vs "
f"future_action_window_size={fw}; expected action_horizon == future + 1. "
"Using action_horizon as canonical."
)
if strict:
raise ValueError(msg)
overwatch.warning(msg)
OmegaConf.update(cfg, f"{am_path}.future_action_window_size", int(ah) - 1, force_add=True)
# ---- 2 & 3. diffusion_model_cfg auto-fill ----
dm_path = f"{am_path}.diffusion_model_cfg"
dm = OmegaConf.select(cfg, dm_path, default=None)
if dm is not None:
hidden_size = OmegaConf.select(am, "hidden_size", default=None)
if OmegaConf.select(dm, "output_dim", default=None) is None and hidden_size is not None:
OmegaConf.update(cfg, f"{dm_path}.output_dim", int(hidden_size), force_add=True)
if OmegaConf.select(dm, "cross_attention_dim", default=None) is None:
vl_hidden = OmegaConf.select(cfg, "framework.qwenvl.vl_hidden_dim", default=None)
if vl_hidden is not None:
OmegaConf.update(cfg, f"{dm_path}.cross_attention_dim", int(vl_hidden), force_add=True)
# else: leave None β framework __init__ may auto-bind it
# ---- 4. action_hidden_dim fallback ----
if OmegaConf.select(am, "action_hidden_dim", default=None) is None:
hidden_size = OmegaConf.select(am, "hidden_size", default=None)
if hidden_size is not None:
OmegaConf.update(cfg, f"{am_path}.action_hidden_dim", int(hidden_size), force_add=True)
# ---- 5. past_action_window_size default ----
if OmegaConf.select(am, "past_action_window_size", default=None) is None:
OmegaConf.update(cfg, f"{am_path}.past_action_window_size", 0, force_add=True)
# ---- 6. stamp version ----
if src_version != CONFIG_VERSION:
try:
OmegaConf.update(cfg, "version_id", CONFIG_VERSION, force_add=True)
except Exception:
try:
cfg.version_id = CONFIG_VERSION
except Exception:
pass
overwatch.info(f"[apply_config_compat] normalised config from version_id={src_version!r} to {CONFIG_VERSION!r}")
return cfg
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Discretised proprioceptive state β instruction prefix (Οβ.5 style)
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
import numpy as _np
from typing import List as _List
def state2str_transform(state: "_np.ndarray", num_bins: int = 256) -> str:
"""Quantise a state vector into ``num_bins`` uniform bins over [-1, 1]
and return space-separated bin indices.
Example: [-0.5, 0.1, 0.8] -> "95 133 203"
"""
discretized_state = _np.digitize(state, bins=_np.linspace(-1, 1, num_bins + 1)[:-1]) - 1
return " ".join(map(str, discretized_state))
def add_discretized_state_to_instruction(
instructions: "_List[str]",
states: "_List[_np.ndarray]",
num_bins: int = 256,
) -> "_List[str]":
"""Append discretised proprioceptive state tokens to each instruction.
Format: ``<original instruction> [STATE] <bin indices> [ACTION]``
Lets the VLM attend to the robot state purely through its existing
text-token pathway β no extra encoder required (Οβ.5 style).
"""
updated_instructions = []
for instr, state in zip(instructions, states):
state_str = state2str_transform(state[0], num_bins=num_bins)
updated_instructions.append(f"{instr} [STATE] {state_str} [ACTION]")
return updated_instructions
|