Add Wall-X serving and Turtle2 TCP WebSocket bridge
Pre-commit / pre-commit (push) Canceled after 0s

This commit is contained in:
2026-09-23 21:04:17 +08:00
parent 6764e8f12f
commit d1cc7d96ad
40 changed files with 8591 additions and 40 deletions
+96 -6
View File
@@ -7,6 +7,8 @@ are intentionally left to the adapter.
from __future__ import annotations
import os
import json
import math
from typing import Callable, Optional
import torch
@@ -17,6 +19,58 @@ def _noop_log(_msg: str, **_kw) -> None:
pass
def resolve_lora_scale(
checkpoint_path: str,
train_config: dict,
state_dict: dict,
log_fn: Optional[Callable] = None,
) -> float:
"""Read PEFT alpha/r from an exported config and validate it against weights."""
log_fn = log_fn or _noop_log
ranks = {
int(tensor.shape[0])
for name, tensor in state_dict.items()
if name.endswith(".lora_A.default.weight")
}
if not ranks:
return 1.0
if len(ranks) != 1:
raise ValueError(f"LoRA weights contain multiple ranks: {sorted(ranks)}")
rank = ranks.pop()
candidates = [
os.path.join(checkpoint_path, "lora_config.json"),
os.path.join(checkpoint_path, "adapter_config.json"),
(train_config.get("model") or {}).get("lora_config_path"),
]
source = next((path for path in candidates if path and os.path.isfile(path)), None)
if source is None:
log_fn(
"LoRA scale metadata is missing; using legacy alpha/r=2.0. "
"Export the training LoRA JSON as checkpoint/lora_config.json "
"before deploying a checkpoint with changed LoRA settings."
)
return 2.0
with open(source, encoding="utf-8") as stream:
config = json.load(stream)
configured_rank = int(config.get("lora_r", config.get("r", rank)))
if configured_rank != rank:
raise ValueError(
f"LoRA rank mismatch: {source} has {configured_rank}, weights have {rank}"
)
if config.get("rank_pattern") or config.get("alpha_pattern"):
raise ValueError(f"Per-layer LoRA scaling in {source} is unsupported")
alpha = config.get("lora_alpha")
if alpha is None:
raise ValueError(f"LoRA config {source} lacks lora_alpha")
scale = float(alpha) / (math.sqrt(rank) if config.get("use_rslora") else rank)
if not math.isfinite(scale) or scale <= 0:
raise ValueError(f"Invalid LoRA scale {scale} from {source}")
log_fn(f"LoRA scale={scale:g} from {source} (alpha={alpha}, rank={rank})")
return scale
def _align_checkpoint_tensor(
param: torch.Tensor,
target: torch.Tensor,
@@ -64,22 +118,58 @@ def _align_checkpoint_tensor(
def reshape_compatible_state_dict(
state_dict: dict, model_sd: dict, log_fn: Optional[Callable] = None
state_dict: dict,
model_sd: dict,
log_fn: Optional[Callable] = None,
lora_scale: float = 2.0,
) -> dict:
"""Align checkpoint tensors to the target model shapes via crop / pad."""
log_fn = log_fn or _noop_log
# Training checkpoints keep PEFT modules (base_layer/lora_A/lora_B),
# while inference uses the plain fused projection tensors. Materialize
# those tensors here so serving does not silently drop the LoRA update.
merged = dict(state_dict)
for name, base in list(state_dict.items()):
suffix = ".base_layer.weight"
if not name.endswith(suffix):
continue
stem = name[: -len(suffix)]
a_name = stem + ".lora_A.default.weight"
b_name = stem + ".lora_B.default.weight"
if a_name not in state_dict or b_name not in state_dict:
continue
a = state_dict[a_name].to(dtype=torch.float32)
b = state_dict[b_name].to(dtype=torch.float32)
merged[name.replace(".base_layer.weight", ".weight")] = (
base.to(dtype=torch.float32) + lora_scale * (b @ a)
).to(dtype=base.dtype)
del merged[name]
del merged[a_name]
del merged[b_name]
log_fn(f"Merged LoRA weights for {stem}")
state_dict = merged
out = {}
for name, param in state_dict.items():
if name not in model_sd:
# Training with PEFT wraps the backbone under ``base_model.model``;
# serving instantiates the unwrapped model. Normalize that prefix so
# fine-tuned backbone weights are applied instead of being ignored.
target_name = name
if ".base_layer." in target_name:
target_name = target_name.replace(".base_layer.", ".")
if target_name not in model_sd and target_name.startswith(
"model.base_model.model."
):
target_name = "model." + target_name[len("model.base_model.model.") :]
if target_name not in model_sd:
log_fn(f"Not used parameter: {name}")
continue
target = model_sd[name]
target = model_sd[target_name]
if param.shape == target.shape:
out[name] = param
out[target_name] = param
continue
aligned = _align_checkpoint_tensor(param, target, name, log_fn)
aligned = _align_checkpoint_tensor(param, target, target_name, log_fn)
if aligned is not None:
out[name] = aligned
out[target_name] = aligned
return out
+30 -5
View File
@@ -1,8 +1,10 @@
"""Build action/proprio normalizers and resolve the effective norm key.
Public inference artifacts must carry their own normalization data. This module
uses checkpoint-local ``norm_stats.json`` first, then checkpoint-side normalizer
state dicts, and finally an explicit ``customized_action_statistic_dof`` path.
uses checkpoint-local ``norm_stats.json`` for the 7D Euler layout, and uses
checkpoint-side normalizer state dicts for checkpoints whose train config contains
6D rotation fields. It finally supports an explicit
``customized_action_statistic_dof`` path.
It does not fall back to internal default action statistics.
"""
@@ -59,6 +61,19 @@ def _missing_normalizer_error(checkpoint_path: str, train_config: dict) -> FileN
)
def _uses_rotation_6d(train_config: dict) -> bool:
"""Return whether the checkpoint was trained with 6D rotation fields."""
for layout_name in ("dof_config", "agent_pos_config", "ar_dof_config"):
layout = train_config.get(layout_name)
if layout is None and isinstance(train_config.get("task"), dict):
layout = train_config["task"].get(layout_name)
if isinstance(layout, dict) and any(
"rotation_6d" in str(key).lower() for key in layout
):
return True
return False
def build_normalizers(
checkpoint_path: str,
train_config: dict,
@@ -66,14 +81,24 @@ def build_normalizers(
) -> tuple[Normalizer, Normalizer, str]:
"""Return action/proprio normalizers and the resolved norm key."""
norm_stats_path = os.path.join(checkpoint_path, "norm_stats.json")
if os.path.exists(norm_stats_path):
action_pth = os.path.join(checkpoint_path, "normalizer_action.pth")
propri_pth = os.path.join(checkpoint_path, "normalizer_propri.pth")
prefer_checkpoint_pth = (
os.path.exists(action_pth)
and os.path.exists(propri_pth)
and _uses_rotation_6d(train_config)
)
if os.path.exists(norm_stats_path) and not prefer_checkpoint_pth:
propri_stats = _load_norm_stats(norm_stats_path, "observation.state")
action_stats = _load_norm_stats(norm_stats_path, "action")
normalizer_propri = Normalizer.from_lerobot_norm_stats(propri_stats, norm_key)
normalizer_action = Normalizer.from_lerobot_norm_stats(action_stats, norm_key)
else:
action_pth = os.path.join(checkpoint_path, "normalizer_action.pth")
propri_pth = os.path.join(checkpoint_path, "normalizer_propri.pth")
if prefer_checkpoint_pth:
logger.info(
"Using checkpoint normalizer .pth files because train config "
"contains rotation_6D; ignoring 7D norm_stats.json"
)
custom_stats = _load_custom_action_stats(train_config)
if custom_stats is None and (not os.path.exists(action_pth) or not os.path.exists(propri_pth)):
raise _missing_normalizer_error(checkpoint_path, train_config)