ReTime / deployment /model_server /policy_wrapper.py
mmmmmcyaaa's picture
Add Florence-Bridge code and handoff documentation
977b06e verified
Raw History Blame Contribute Delete
7.55 kB
# Copyright 2025 starVLA community. All rights reserved.
# Licensed under the MIT License.
"""Policy server wrapper.
Encapsulates a `baseframework` instance plus a :class:`PolicyNormProcessor`
that reuses the *training-time* :class:`ComposedModalityTransform` for action
un-normalization (no hand-rolled math). The websocket server returns
already-unnormalized actions.
Client-side responsibilities that REMAIN on the client:
- environment-specific adapters (image_history, gripper sticky, action
ensembling)
- chunk-cache scheduling (`step % chunk_size == 0` triggers a new infer)
Exposed API:
- ``metadata`` (dict, sent at handshake): ``action_chunk_size``,
``available_unnorm_keys``, ``action_keys``, ``state_keys``.
- ``predict_action(examples, unnorm_key=None, **kwargs)`` returns
``{"actions": np.ndarray[B, T, action_dim]}``.
"""
from __future__ import annotations
import logging
from typing import Any, Dict, List, Optional
import numpy as np
import torch
from starVLA.model.framework.base_framework import baseframework
from starVLA.model.framework.share_tools import read_mode_config
from deployment.model_server.policy_norm_processor import PolicyNormProcessor
class PolicyServerWrapper:
"""Wraps a `baseframework` for use as a websocket-server policy."""
def __init__(
self,
ckpt_path: str,
device: str = "cuda",
use_bf16: bool = False,
unnorm_key: Optional[str] = None,
normalized_action_clip: Optional[float] = None,
) -> None:
self._ckpt_path = str(ckpt_path)
self._normalized_action_clip = normalized_action_clip
logging.info("PolicyServerWrapper: loading framework from %s", self._ckpt_path)
framework = baseframework.from_pretrained(self._ckpt_path)
if use_bf16:
framework = framework.to(torch.bfloat16)
framework = framework.to(device).eval()
self._framework = framework
# Co-located metadata.
model_cfg, _ = read_mode_config(self._ckpt_path)
self._model_cfg = model_cfg
vla_data_cfg = model_cfg.get("datasets", {}).get("vla_data", {})
include_state = vla_data_cfg.get("include_state", False)
self._uses_state = include_state not in (False, None, 0, "False", "false", "0")
# action_chunk_size = future_action_window_size + 1 (matches old client).
action_model_cfg = model_cfg["framework"]["action_model"]
if "action_horizon" in action_model_cfg:
self._action_chunk_size = int(action_model_cfg["action_horizon"])
elif "future_action_window_size" in action_model_cfg:
self._action_chunk_size = int(action_model_cfg["future_action_window_size"]) + 1
else:
raise ValueError(
f"PolicyServerWrapper: no action_horizon or future_action_window_size found in model config for {self._ckpt_path}"
)
# Cache of PolicyNormProcessor instances per unnorm_key.
# For single-dataset ckpts unnorm_key is auto-selected; for multi-dataset
# ckpts clients must pass unnorm_key per request.
self._default_unnorm_key = unnorm_key
self._norm_processors: Dict[str, PolicyNormProcessor] = {}
# Peek at available keys without building a full processor.
_, _ns = read_mode_config(self._ckpt_path)
self._available_unnorm_keys: List[str] = list(_ns.keys())
# Eagerly build when unambiguous; defer for multi-key / no explicit key.
if unnorm_key is not None or len(self._available_unnorm_keys) == 1:
default_proc = self._get_processor(unnorm_key)
self._default_unnorm_key = default_proc.unnorm_key
logging.info(
"PolicyServerWrapper ready: action_chunk_size=%d, default_unnorm_key=%s, "
"available_unnorm_keys=%s, action_keys=%s, state_keys=%s",
self._action_chunk_size,
default_proc.unnorm_key,
default_proc.available_unnorm_keys,
default_proc.action_keys,
default_proc.state_keys,
)
else:
logging.info(
"PolicyServerWrapper ready (multi-key): action_chunk_size=%d, "
"available_unnorm_keys=%s — clients must pass unnorm_key per request.",
self._action_chunk_size,
self._available_unnorm_keys,
)
def _get_processor(self, unnorm_key: Optional[str]) -> PolicyNormProcessor:
cache_key = unnorm_key if unnorm_key is not None else "__default__"
if cache_key not in self._norm_processors:
self._norm_processors[cache_key] = PolicyNormProcessor(
self._ckpt_path, unnorm_key=unnorm_key
)
return self._norm_processors[cache_key]
@property
def metadata(self) -> Dict[str, Any]:
"""Model-invariant metadata; sent to client at websocket handshake."""
base = {
"env": "starvla_policy_server",
"ckpt_path": self._ckpt_path,
"action_chunk_size": self._action_chunk_size,
"uses_state": self._uses_state,
"available_unnorm_keys": self._available_unnorm_keys,
"default_unnorm_key": self._default_unnorm_key,
"normalized_action_clip": self._normalized_action_clip,
"action_head_attention_mask": False,
"inference_rng": "continuous_server_stream",
}
# Enrich with per-embodiment keys when a default processor already exists.
if self._default_unnorm_key is not None:
proc = self._get_processor(self._default_unnorm_key)
base["action_keys"] = proc.action_keys
base["state_keys"] = proc.state_keys
return base
def predict_action(
self,
examples: List[dict],
unnorm_key: Optional[str] = None,
**kwargs,
) -> Dict[str, np.ndarray]:
"""Run the framework, then un-normalize via training-time transforms.
Args:
examples: list of dicts (each with ``image`` / ``lang`` / optional ``state``).
unnorm_key: dataset key for un-normalization stats. ``None`` -->
use the wrapper's default (auto-picked at startup).
**kwargs: forwarded to the framework's ``predict_action``
(``do_sample``, ``use_ddim``, ``num_ddim_steps``, ...).
Returns:
``{"actions": np.ndarray[B, T, D]}`` -- un-normalized.
"""
effective_key = unnorm_key if unnorm_key is not None else self._default_unnorm_key
if effective_key is None:
if len(self._available_unnorm_keys) == 1:
effective_key = self._available_unnorm_keys[0]
else:
raise ValueError(
f"predict_action: unnorm_key not specified and no default set. "
f"Pass one of {self._available_unnorm_keys}."
)
proc = self._get_processor(effective_key)
out = self._framework.predict_action(examples=examples, **kwargs)
normalized = np.asarray(out["normalized_actions"]) # (B, T, D)
if self._normalized_action_clip is not None:
limit = float(self._normalized_action_clip)
normalized = np.clip(normalized, -limit, limit)
unnorm = np.stack(
[proc.unapply_actions(normalized[b]) for b in range(normalized.shape[0])],
axis=0,
)
return {"actions": unnorm}