mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-26 03:06:01 +00:00
fix(rtc): align trained mode with RLDX
This commit is contained in:
@@ -94,6 +94,8 @@ def _prepare_trained_rtc_prefix(
|
|||||||
)
|
)
|
||||||
|
|
||||||
previous = prev_chunk_left_over.to(device=x_t.device, dtype=x_t.dtype)
|
previous = prev_chunk_left_over.to(device=x_t.device, dtype=x_t.dtype)
|
||||||
|
if not torch.isfinite(previous).all():
|
||||||
|
raise ValueError("RTC prefix contains NaN or Inf values.")
|
||||||
if previous.ndim == 2:
|
if previous.ndim == 2:
|
||||||
previous = previous.unsqueeze(0)
|
previous = previous.unsqueeze(0)
|
||||||
if previous.ndim != 3:
|
if previous.ndim != 3:
|
||||||
|
|||||||
@@ -123,12 +123,7 @@ class PI052Config(PI05Config):
|
|||||||
|
|
||||||
# Training-time RTC (arXiv:2512.05964). Zero preserves standard flow matching.
|
# Training-time RTC (arXiv:2512.05964). Zero preserves standard flow matching.
|
||||||
rtc_training_max_delay: int = 0
|
rtc_training_max_delay: int = 0
|
||||||
"""Largest clean action-prefix length sampled during training.
|
"""Maximum clean-prefix delay sampled for training-time RTC; zero disables it."""
|
||||||
|
|
||||||
A value greater than zero enables training-time action conditioning. Each
|
|
||||||
flow draw samples a delay uniformly from ``[0, rtc_training_max_delay]``;
|
|
||||||
the corresponding action prefix stays clean and is excluded from the loss.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# PaLM-style z-loss stabilizes large-vocabulary CE; 0 disables it.
|
# PaLM-style z-loss stabilizes large-vocabulary CE; 0 disables it.
|
||||||
text_ce_z_loss_weight: float = 1e-4
|
text_ce_z_loss_weight: float = 1e-4
|
||||||
|
|||||||
@@ -177,16 +177,6 @@ def _enable_hf_kernels() -> None:
|
|||||||
logger.info("PI052: HF kernels (Liger) enabled — rope, geglu fused.")
|
logger.info("PI052: HF kernels (Liger) enabled — rope, geglu fused.")
|
||||||
|
|
||||||
|
|
||||||
def _reduce_action_loss(per_sample: Tensor, predict_actions_t: Tensor | None, reduction: str) -> Tensor:
|
|
||||||
"""Mask non-action samples and apply the requested batch reduction."""
|
|
||||||
if predict_actions_t is None:
|
|
||||||
return per_sample if reduction == "none" else per_sample.mean()
|
|
||||||
mask = predict_actions_t.to(per_sample.dtype)
|
|
||||||
if reduction == "none":
|
|
||||||
return per_sample * mask
|
|
||||||
return (per_sample * mask).sum() / mask.sum().clamp(min=1.0)
|
|
||||||
|
|
||||||
|
|
||||||
def _sample_training_rtc_prefix_mask(
|
def _sample_training_rtc_prefix_mask(
|
||||||
batch_size: int,
|
batch_size: int,
|
||||||
action_horizon: int,
|
action_horizon: int,
|
||||||
@@ -224,15 +214,42 @@ def _build_flow_matching_inputs(
|
|||||||
return x_t, model_time
|
return x_t, model_time
|
||||||
|
|
||||||
|
|
||||||
def _flow_loss_per_sample(flow_per_dim: Tensor, prefix_mask: Tensor | None) -> Tensor:
|
def _flow_loss_components(flow_per_dim: Tensor, prefix_mask: Tensor | None) -> tuple[Tensor, Tensor]:
|
||||||
"""Average each draw over postfix action positions and dimensions only."""
|
"""Return each sample's postfix loss sum and valid-element count."""
|
||||||
if prefix_mask is None:
|
if prefix_mask is None:
|
||||||
return flow_per_dim.flatten(start_dim=1).mean(dim=1)
|
flattened = flow_per_dim.flatten(start_dim=1)
|
||||||
postfix = (~prefix_mask).unsqueeze(-1).expand_as(flow_per_dim)
|
numerator = flattened.sum(dim=1)
|
||||||
|
denominator = torch.full_like(numerator, flattened.shape[1])
|
||||||
|
return numerator, denominator
|
||||||
|
|
||||||
|
postfix = (~prefix_mask).unsqueeze(-1).expand_as(flow_per_dim).to(flow_per_dim.dtype)
|
||||||
reduce_dims = tuple(range(1, flow_per_dim.ndim))
|
reduce_dims = tuple(range(1, flow_per_dim.ndim))
|
||||||
numerator = (flow_per_dim * postfix).sum(dim=reduce_dims)
|
numerator = (flow_per_dim * postfix).sum(dim=reduce_dims)
|
||||||
denominator = postfix.sum(dim=reduce_dims).clamp(min=1)
|
denominator = postfix.sum(dim=reduce_dims)
|
||||||
return numerator / denominator
|
return numerator, denominator
|
||||||
|
|
||||||
|
|
||||||
|
def _flow_loss_per_sample(flow_per_dim: Tensor, prefix_mask: Tensor | None) -> Tensor:
|
||||||
|
"""Average each sample over postfix action positions and dimensions only."""
|
||||||
|
numerator, denominator = _flow_loss_components(flow_per_dim, prefix_mask)
|
||||||
|
return numerator / denominator.clamp(min=1)
|
||||||
|
|
||||||
|
|
||||||
|
def _reduce_flow_loss(
|
||||||
|
flow_per_dim: Tensor,
|
||||||
|
prefix_mask: Tensor | None,
|
||||||
|
predict_actions_t: Tensor | None,
|
||||||
|
reduction: str,
|
||||||
|
) -> Tensor:
|
||||||
|
"""Apply action routing and RLDX-compatible postfix normalization."""
|
||||||
|
numerator, denominator = _flow_loss_components(flow_per_dim, prefix_mask)
|
||||||
|
if predict_actions_t is not None:
|
||||||
|
action_mask = predict_actions_t.to(numerator.dtype)
|
||||||
|
numerator = numerator * action_mask
|
||||||
|
denominator = denominator * action_mask
|
||||||
|
if reduction == "none":
|
||||||
|
return numerator / denominator.clamp(min=1)
|
||||||
|
return numerator.sum() / denominator.sum().clamp(min=1)
|
||||||
|
|
||||||
|
|
||||||
# Materialized logits win at VLA token counts; larger dense targets use Liger.
|
# Materialized logits win at VLA token counts; larger dense targets use Liger.
|
||||||
@@ -1223,8 +1240,7 @@ class PI052Policy(PI05Policy):
|
|||||||
# internally to max_action_dim).
|
# internally to max_action_dim).
|
||||||
original_action_dim = self.config.output_features[ACTION].shape[0]
|
original_action_dim = self.config.output_features[ACTION].shape[0]
|
||||||
flow_per_dim = flow_per_dim[:, :, :original_action_dim]
|
flow_per_dim = flow_per_dim[:, :, :original_action_dim]
|
||||||
per_sample_flow = _flow_loss_per_sample(flow_per_dim, prefix_mask)
|
flow_loss = _reduce_flow_loss(flow_per_dim, prefix_mask, predict_actions_t, reduction)
|
||||||
flow_loss = _reduce_action_loss(per_sample_flow, predict_actions_t, reduction)
|
|
||||||
return prefix_out, flow_loss
|
return prefix_out, flow_loss
|
||||||
|
|
||||||
def _ki_forward_kwargs(self, suppress_prefix_grads: bool = False, flex_masks=None) -> dict[str, Any]:
|
def _ki_forward_kwargs(self, suppress_prefix_grads: bool = False, flex_masks=None) -> dict[str, Any]:
|
||||||
@@ -1363,8 +1379,7 @@ class PI052Policy(PI05Policy):
|
|||||||
prefix_mask = None
|
prefix_mask = None
|
||||||
if prefix_mask_flat is not None:
|
if prefix_mask_flat is not None:
|
||||||
prefix_mask = prefix_mask_flat.view(k, batch_size, chunk).transpose(0, 1)
|
prefix_mask = prefix_mask_flat.view(k, batch_size, chunk).transpose(0, 1)
|
||||||
per_sample_flow = _flow_loss_per_sample(flow_per_dim, prefix_mask)
|
flow_loss = _reduce_flow_loss(flow_per_dim, prefix_mask, predict_actions_t, reduction)
|
||||||
flow_loss = _reduce_action_loss(per_sample_flow, predict_actions_t, reduction)
|
|
||||||
return prefix_out, flow_loss
|
return prefix_out, flow_loss
|
||||||
|
|
||||||
def _prefix_ce_losses(
|
def _prefix_ce_losses(
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ _RTC_MAX_CONSECUTIVE_ERRORS: int = 10
|
|||||||
_RTC_JOIN_TIMEOUT_S: float = 3.0
|
_RTC_JOIN_TIMEOUT_S: float = 3.0
|
||||||
|
|
||||||
|
|
||||||
|
class _TrainedRTCDelayExceededError(RuntimeError):
|
||||||
|
"""Raised when measured latency exceeds a trained RTC checkpoint's support."""
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# RTC helpers
|
# RTC helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -76,6 +80,22 @@ def _normalize_prev_actions_length(prev_actions: torch.Tensor, target_steps: int
|
|||||||
return padded
|
return padded
|
||||||
|
|
||||||
|
|
||||||
|
def _trained_rtc_chunk_can_merge(
|
||||||
|
*,
|
||||||
|
conditioned_delay: int,
|
||||||
|
measured_delay: int,
|
||||||
|
training_max_delay: int,
|
||||||
|
has_previous_actions: bool,
|
||||||
|
) -> bool:
|
||||||
|
"""Check that a trained RTC chunk covers the overlap observed during inference."""
|
||||||
|
if measured_delay > training_max_delay:
|
||||||
|
raise _TrainedRTCDelayExceededError(
|
||||||
|
f"Measured RTC inference delay ({measured_delay}) exceeds the checkpoint's "
|
||||||
|
f"rtc_training_max_delay ({training_max_delay})."
|
||||||
|
)
|
||||||
|
return not has_previous_actions or measured_delay <= conditioned_delay
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# RTCInferenceEngine
|
# RTCInferenceEngine
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -272,6 +292,7 @@ class RTCInferenceEngine(InferenceEngine):
|
|||||||
current_time = time.perf_counter()
|
current_time = time.perf_counter()
|
||||||
idx_before = queue.get_action_index()
|
idx_before = queue.get_action_index()
|
||||||
prev_actions = queue.get_left_over()
|
prev_actions = queue.get_left_over()
|
||||||
|
has_previous_actions = prev_actions is not None and prev_actions.numel() > 0
|
||||||
|
|
||||||
latency = latency_tracker.max()
|
latency = latency_tracker.max()
|
||||||
delay = math.ceil(latency / time_per_chunk) if latency else 0
|
delay = math.ceil(latency / time_per_chunk) if latency else 0
|
||||||
@@ -321,6 +342,24 @@ class RTCInferenceEngine(InferenceEngine):
|
|||||||
else:
|
else:
|
||||||
latency_tracker.add(new_latency)
|
latency_tracker.add(new_latency)
|
||||||
|
|
||||||
|
if not is_warmup and self._rtc_config.mode == "trained":
|
||||||
|
training_max_delay = int(
|
||||||
|
getattr(self._policy.config, "rtc_training_max_delay", 0)
|
||||||
|
)
|
||||||
|
if not _trained_rtc_chunk_can_merge(
|
||||||
|
conditioned_delay=delay,
|
||||||
|
measured_delay=new_delay,
|
||||||
|
training_max_delay=training_max_delay,
|
||||||
|
has_previous_actions=has_previous_actions,
|
||||||
|
):
|
||||||
|
logger.warning(
|
||||||
|
"Discarding trained RTC chunk: measured delay %d exceeded "
|
||||||
|
"conditioned delay %d; retrying with updated latency",
|
||||||
|
new_delay,
|
||||||
|
delay,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
queue.merge(original, processed, new_delay, idx_before)
|
queue.merge(original, processed, new_delay, idx_before)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -333,6 +372,8 @@ class RTCInferenceEngine(InferenceEngine):
|
|||||||
|
|
||||||
logger.debug("RTC inference latency=%.2fs, queue=%d", new_latency, queue.qsize())
|
logger.debug("RTC inference latency=%.2fs, queue=%d", new_latency, queue.qsize())
|
||||||
|
|
||||||
|
except _TrainedRTCDelayExceededError:
|
||||||
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
consecutive_errors += 1
|
consecutive_errors += 1
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from lerobot.policies.pi052.modeling_pi052 import ( # noqa: E402
|
|||||||
PI05Pytorch as PI052Pytorch,
|
PI05Pytorch as PI052Pytorch,
|
||||||
_build_flow_matching_inputs,
|
_build_flow_matching_inputs,
|
||||||
_flow_loss_per_sample,
|
_flow_loss_per_sample,
|
||||||
|
_reduce_flow_loss,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -49,6 +50,25 @@ def test_training_rtc_loss_averages_over_postfix_only():
|
|||||||
assert per_sample.tolist() == [3.0]
|
assert per_sample.tolist() == [3.0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_training_rtc_mean_loss_uses_global_postfix_normalization():
|
||||||
|
flow_loss = torch.tensor(
|
||||||
|
[
|
||||||
|
[[1.0], [1.0], [1.0], [1.0]],
|
||||||
|
[[9.0], [9.0], [9.0], [9.0]],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
prefix_mask = torch.tensor(
|
||||||
|
[
|
||||||
|
[False, False, False, False],
|
||||||
|
[True, True, True, False],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
loss = _reduce_flow_loss(flow_loss, prefix_mask, predict_actions_t=None, reduction="mean")
|
||||||
|
|
||||||
|
assert loss.item() == pytest.approx(13 / 5)
|
||||||
|
|
||||||
|
|
||||||
def test_per_token_time_embedding_preserves_action_axis():
|
def test_per_token_time_embedding_preserves_action_axis():
|
||||||
time = torch.tensor([[0.0, 0.5, 1.0]])
|
time = torch.tensor([[0.0, 0.5, 1.0]])
|
||||||
|
|
||||||
@@ -100,6 +120,20 @@ def test_trained_rtc_rejects_delay_outside_training_distribution():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("non_finite", [float("nan"), float("inf")])
|
||||||
|
def test_trained_rtc_rejects_non_finite_prefix(non_finite):
|
||||||
|
previous = torch.randn(4, 2)
|
||||||
|
previous[0, 0] = non_finite
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="NaN or Inf"):
|
||||||
|
_prepare_trained_rtc_prefix(
|
||||||
|
torch.randn(1, 5, 4),
|
||||||
|
previous,
|
||||||
|
inference_delay=2,
|
||||||
|
training_max_delay=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("max_delay", [-1, 5])
|
@pytest.mark.parametrize("max_delay", [-1, 5])
|
||||||
def test_pi052_config_rejects_invalid_training_rtc_delay(max_delay):
|
def test_pi052_config_rejects_invalid_training_rtc_delay(max_delay):
|
||||||
with pytest.raises(ValueError, match="rtc_training_max_delay"):
|
with pytest.raises(ValueError, match="rtc_training_max_delay"):
|
||||||
|
|||||||
@@ -98,6 +98,38 @@ def test_inference_config_types():
|
|||||||
assert rtc.rtc is not None
|
assert rtc.rtc is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_trained_rtc_retries_chunk_when_measured_delay_exceeds_conditioning():
|
||||||
|
from lerobot.rollout.inference.rtc import _trained_rtc_chunk_can_merge
|
||||||
|
|
||||||
|
assert not _trained_rtc_chunk_can_merge(
|
||||||
|
conditioned_delay=2,
|
||||||
|
measured_delay=3,
|
||||||
|
training_max_delay=4,
|
||||||
|
has_previous_actions=True,
|
||||||
|
)
|
||||||
|
assert _trained_rtc_chunk_can_merge(
|
||||||
|
conditioned_delay=2,
|
||||||
|
measured_delay=3,
|
||||||
|
training_max_delay=4,
|
||||||
|
has_previous_actions=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_trained_rtc_rejects_measured_delay_above_checkpoint_support():
|
||||||
|
from lerobot.rollout.inference.rtc import (
|
||||||
|
_trained_rtc_chunk_can_merge,
|
||||||
|
_TrainedRTCDelayExceededError,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(_TrainedRTCDelayExceededError, match="rtc_training_max_delay"):
|
||||||
|
_trained_rtc_chunk_can_merge(
|
||||||
|
conditioned_delay=3,
|
||||||
|
measured_delay=5,
|
||||||
|
training_max_delay=4,
|
||||||
|
has_previous_actions=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_sentry_config_defaults():
|
def test_sentry_config_defaults():
|
||||||
from lerobot.rollout import SentryStrategyConfig
|
from lerobot.rollout import SentryStrategyConfig
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user