Files
VLA/wall_x/_vendor/harrix/serving/rtc_wallx.py
T

120 lines
4.3 KiB
Python
Raw Normal View History

"""Wall-X Real-Time Chunking helpers.
The guided update follows LeRobot's RTC implementation, adapted to Wall-X's
flow convention: Wall-X integrates normalized actions from noise at t=0 to a
clean action at t=1 with velocity ``dx/dt``.
"""
from __future__ import annotations
from dataclasses import dataclass
import math
import torch
@dataclass(frozen=True)
class WallXRTCConfig:
enabled: bool = True
execution_horizon: int = 6
max_guidance_weight: float = 10.0
prefix_attention_schedule: str = "linear"
def __post_init__(self):
if self.execution_horizon <= 0:
raise ValueError("execution_horizon must be positive")
if self.max_guidance_weight <= 0:
raise ValueError("max_guidance_weight must be positive")
if self.prefix_attention_schedule not in {"zeros", "ones", "linear", "exp"}:
raise ValueError(
"prefix_attention_schedule must be zeros, ones, linear, or exp"
)
class WallXRTCProcessor:
"""Inference-time RTC guidance in normalized Wall-X action space."""
def __init__(self, config: WallXRTCConfig):
self.config = config
def get_prefix_weights(self, start: int, end: int, total: int) -> torch.Tensor:
start = max(0, min(int(start), int(end), int(total)))
end = max(start, min(int(end), int(total)))
schedule = self.config.prefix_attention_schedule
if schedule == "zeros":
weights = torch.zeros(total)
weights[:start] = 1.0
return weights
if schedule == "ones":
weights = torch.zeros(total)
weights[:end] = 1.0
return weights
middle_len = end - start
if middle_len:
middle = torch.linspace(1.0, 0.0, middle_len + 2)[1:-1]
if schedule == "exp":
middle = middle * torch.expm1(middle) / (math.e - 1.0)
else:
middle = torch.empty(0)
return torch.cat(
[torch.ones(start), middle, torch.zeros(total - end)], dim=0
)
def guide_increasing_flow(
self,
*,
x_t: torch.Tensor,
time: torch.Tensor | float,
predict_velocity,
prev_chunk_left_over: torch.Tensor | None,
inference_delay: int = 0,
execution_horizon: int | None = None,
) -> torch.Tensor:
"""Return RTC-guided velocity for a flow integrated from t=0 to t=1."""
if prev_chunk_left_over is None or not self.config.enabled:
return predict_velocity(x_t)
x = x_t.detach().clone().requires_grad_(True)
prefix = prev_chunk_left_over.to(device=x.device, dtype=x.dtype)
if prefix.ndim == 2:
prefix = prefix.unsqueeze(0)
if prefix.shape[0] == 1 and x.shape[0] > 1:
prefix = prefix.expand(x.shape[0], -1, -1)
padded = torch.zeros_like(x)
steps = min(prefix.shape[1], x.shape[1])
dims = min(prefix.shape[2], x.shape[2])
padded[:, :steps, :dims] = prefix[:, :steps, :dims]
horizon = execution_horizon or self.config.execution_horizon
horizon = min(int(horizon), steps, x.shape[1])
weights = self.get_prefix_weights(inference_delay, horizon, x.shape[1])
weights = weights.to(device=x.device, dtype=x.dtype).view(1, -1, 1)
with torch.enable_grad():
velocity = predict_velocity(x)
t = torch.as_tensor(time, device=x.device, dtype=x.dtype)
remaining = torch.clamp(1.0 - t, min=1e-6)
clean_estimate = x + remaining * velocity
error = (padded - clean_estimate) * weights
correction = torch.autograd.grad(
clean_estimate,
x,
grad_outputs=error.detach(),
retain_graph=False,
)[0]
# Same guidance schedule as LeRobot RTC after mapping its 1->0 time
# convention to Wall-X's 0->1 convention.
t_safe = torch.clamp(t, min=1e-6)
inv_r2 = (remaining.square() + t.square()) / remaining.square()
weight = (remaining / t_safe) * inv_r2
weight = torch.nan_to_num(
weight,
nan=self.config.max_guidance_weight,
posinf=self.config.max_guidance_weight,
).clamp(max=self.config.max_guidance_weight)
return (velocity + weight * correction).detach()