Initial pi0.7 minimal validation

This commit is contained in:
seeker
2026-07-12 00:52:00 +08:00
commit b4c8db846b
27 changed files with 3724 additions and 0 deletions
+393
View File
@@ -0,0 +1,393 @@
"""Prompt and FAST-lite tokenization for the minimal pi0.7 policy.
pi0.7 conditions the low-level policy on a richer prompt than earlier pi
models: task and subtask language, episode speed/quality/mistake metadata,
control mode, and data provenance. This tokenizer represents those concepts
with explicit marker/value pairs so the architecture can be validated without
shipping a full language tokenizer.
"""
from __future__ import annotations
import math
from dataclasses import dataclass
from typing import Iterable, Literal
import torch
from torch import Tensor
ControlMode = Literal["joint", "ee"]
@dataclass(frozen=True)
class EpisodeMetadata:
speed_steps: int | None = None
quality: int | None = None
mistake: bool | None = None
control_mode: ControlMode = "joint"
source_type: str | None = None
def __post_init__(self) -> None:
if self.speed_steps is not None and self.speed_steps <= 0:
raise ValueError("speed_steps must be positive")
if self.quality is not None and not 1 <= self.quality <= 5:
raise ValueError("quality must be in the inclusive range 1..5")
if self.control_mode not in ("joint", "ee"):
raise ValueError("control_mode must be 'joint' or 'ee'")
@dataclass(frozen=True)
class PolicySequence:
input_ids: Tensor
labels: Tensor
action_prefix_mask: Tensor
generated_token_start: int
action_start_index: int
prompt_length: int
metadata_indices: tuple[int, ...]
class _LabelVocabulary:
def __init__(self, labels: tuple[str, ...], first_token_id: int) -> None:
self.labels = labels
self.first_token_id = first_token_id
self._to_token = {
label: first_token_id + index for index, label in enumerate(labels)
}
self._to_label = {token: label for label, token in self._to_token.items()}
@property
def token_ids(self) -> tuple[int, ...]:
return tuple(self._to_token[label] for label in self.labels)
def encode(self, label: str) -> int:
try:
return self._to_token[label]
except KeyError as error:
raise ValueError(f"unknown categorical label: {label}") from error
def decode(self, token: int | Tensor) -> str:
try:
return self._to_label[int(token)]
except KeyError as error:
raise ValueError(f"unknown categorical token: {int(token)}") from error
class MiniTokenizer:
"""Explicit rich-prompt tokens plus a DCT-based FAST-lite action stream."""
BOS = 256
TASK_MARKER = 257
SUBTASK_MARKER = 258
SPEED_MARKER = 259
QUALITY_MARKER = 260
MISTAKE_MARKER = 261
CONTROL_MARKER = 262
SOURCE_MARKER = 263
TASK_LABELS = (
"drive",
"pick_place",
"fold",
"open_close",
"clean",
"cook",
"unknown",
)
TASK_TOKEN_START = 264
TASK_TOKEN_END = TASK_TOKEN_START + len(TASK_LABELS) - 1
SUBTASK_LABELS = (
"keep_lane",
"steer_left",
"steer_right",
"slow_down",
"stop",
"accelerate",
"pick_place",
"fold",
"open_close",
"unknown",
)
SUBTASK_TOKEN_START = TASK_TOKEN_END + 1
SUBTASK_TOKEN_END = SUBTASK_TOKEN_START + len(SUBTASK_LABELS) - 1
SPEED_BINS = (500, 1000, 2000, 4000, 8000, 16000)
SPEED_TOKEN_START = SUBTASK_TOKEN_END + 1
SPEED_TOKEN_END = SPEED_TOKEN_START + len(SPEED_BINS) - 1
QUALITY_TOKEN_START = SPEED_TOKEN_END + 1
QUALITY_TOKEN_END = QUALITY_TOKEN_START + 4
MISTAKE_FALSE = QUALITY_TOKEN_END + 1
MISTAKE_TRUE = MISTAKE_FALSE + 1
CONTROL_JOINT = MISTAKE_TRUE + 1
CONTROL_EE = CONTROL_JOINT + 1
SOURCE_LABELS = (
"demonstration",
"autonomous",
"intervention",
"human_video",
"web",
"unknown",
)
SOURCE_TOKEN_START = CONTROL_EE + 1
SOURCE_TOKEN_END = SOURCE_TOKEN_START + len(SOURCE_LABELS) - 1
ACTION_START = SOURCE_TOKEN_END + 1
ACTION_END = ACTION_START + 1
ACTION_TOKEN_START = ACTION_END + 1
ACTION_BINS = 64
ACTION_TOKEN_END = ACTION_TOKEN_START + ACTION_BINS - 1
ACTION_MINIMUM = -4.0
ACTION_MAXIMUM = 4.0
def __init__(self) -> None:
self.tasks = _LabelVocabulary(self.TASK_LABELS, self.TASK_TOKEN_START)
self.subtasks = _LabelVocabulary(self.SUBTASK_LABELS, self.SUBTASK_TOKEN_START)
self.sources = _LabelVocabulary(self.SOURCE_LABELS, self.SOURCE_TOKEN_START)
@property
def vocab_size(self) -> int:
return self.ACTION_TOKEN_END + 1
@property
def action_token_ids(self) -> range:
return range(self.ACTION_TOKEN_START, self.ACTION_TOKEN_END + 1)
@staticmethod
def _classify_task(description: str) -> str:
text = description.casefold()
if any(word in text for word in ("drive", "lane", "road", "steer", "vehicle")):
return "drive"
if any(word in text for word in ("fold", "shirt", "towel", "laundry")):
return "fold"
if any(word in text for word in ("open", "close", "door", "drawer", "fridge")):
return "open_close"
if any(word in text for word in ("clean", "wipe", "wash", "tidy")):
return "clean"
if any(word in text for word in ("cook", "food", "recipe", "microwave")):
return "cook"
if any(word in text for word in ("pick", "place", "move", "put")):
return "pick_place"
return description if description in MiniTokenizer.TASK_LABELS else "unknown"
@staticmethod
def _classify_subtask(description: str) -> str:
if description in MiniTokenizer.SUBTASK_LABELS:
return description
text = description.casefold()
if any(word in text for word in ("stop", "halt", "stationary")):
return "stop"
if any(word in text for word in ("slow", "brake", "deceler")):
return "slow_down"
if any(word in text for word in ("accelerat", "speed up")):
return "accelerate"
if "left" in text:
return "steer_left"
if "right" in text:
return "steer_right"
if any(word in text for word in ("lane", "straight", "continue", "follow")):
return "keep_lane"
if "fold" in text:
return "fold"
if any(word in text for word in ("open", "close")):
return "open_close"
if any(word in text for word in ("pick", "place", "put")):
return "pick_place"
return "unknown"
@staticmethod
def _classify_source(source: str) -> str:
if source in MiniTokenizer.SOURCE_LABELS:
return source
text = source.casefold()
if any(word in text for word in ("demo", "teleop")):
return "demonstration"
if any(word in text for word in ("autonomous", "rollout", "policy")):
return "autonomous"
if any(word in text for word in ("intervention", "correction")):
return "intervention"
if any(word in text for word in ("human", "ego", "video")):
return "human_video"
if any(word in text for word in ("web", "internet")):
return "web"
return "unknown"
def encode_prompt(
self,
task: str,
subtask: str | None,
metadata: EpisodeMetadata,
*,
include_episode_metadata: bool = True,
include_subtask: bool = True,
) -> tuple[Tensor, tuple[int, ...]]:
"""Encode prompt and return positions controlled by metadata CFG.
Control mode is deliberately retained even in the unconditional branch,
matching pi0.7 training where control-mode dropout is not applied.
"""
ids = [self.BOS, self.TASK_MARKER, self.tasks.encode(self._classify_task(task))]
if include_subtask and subtask is not None:
ids.extend(
(self.SUBTASK_MARKER, self.subtasks.encode(self._classify_subtask(subtask)))
)
metadata_indices: list[int] = []
def add_metadata(marker: int, value: int) -> None:
metadata_indices.extend((len(ids), len(ids) + 1))
ids.extend((marker, value))
if include_episode_metadata:
if metadata.speed_steps is not None:
bin_index = min(
range(len(self.SPEED_BINS)),
key=lambda index: abs(self.SPEED_BINS[index] - metadata.speed_steps),
)
add_metadata(self.SPEED_MARKER, self.SPEED_TOKEN_START + bin_index)
if metadata.quality is not None:
add_metadata(
self.QUALITY_MARKER, self.QUALITY_TOKEN_START + metadata.quality - 1
)
if metadata.mistake is not None:
add_metadata(
self.MISTAKE_MARKER,
self.MISTAKE_TRUE if metadata.mistake else self.MISTAKE_FALSE,
)
if metadata.source_type is not None:
source = self._classify_source(metadata.source_type)
add_metadata(self.SOURCE_MARKER, self.sources.encode(source))
control = self.CONTROL_JOINT if metadata.control_mode == "joint" else self.CONTROL_EE
ids.extend((self.CONTROL_MARKER, control))
return torch.tensor(ids, dtype=torch.long), tuple(metadata_indices)
@staticmethod
def _orthonormal_dct_matrix(
horizon: int, *, device: torch.device, dtype: torch.dtype
) -> Tensor:
if horizon <= 0:
raise ValueError("DCT horizon must be positive")
time = torch.arange(horizon, device=device, dtype=dtype)
frequency = torch.arange(horizon, device=device, dtype=dtype).unsqueeze(1)
matrix = torch.cos(math.pi / horizon * (time + 0.5) * frequency)
matrix[0] *= math.sqrt(1.0 / horizon)
if horizon > 1:
matrix[1:] *= math.sqrt(2.0 / horizon)
return matrix
def dct_actions(self, actions: Tensor | Iterable[Iterable[float]]) -> Tensor:
values = torch.as_tensor(actions, dtype=torch.float32)
if values.ndim != 2 or values.shape[-1] != 2 or values.shape[0] == 0:
raise ValueError("actions must have non-empty shape [horizon, 2]")
transform = self._orthonormal_dct_matrix(
values.shape[0], device=values.device, dtype=values.dtype
)
return transform @ values
def idct_actions(self, coefficients: Tensor | Iterable[Iterable[float]]) -> Tensor:
values = torch.as_tensor(coefficients, dtype=torch.float32)
if values.ndim != 2 or values.shape[-1] != 2 or values.shape[0] == 0:
raise ValueError("DCT coefficients must have non-empty shape [horizon, 2]")
transform = self._orthonormal_dct_matrix(
values.shape[0], device=values.device, dtype=values.dtype
)
return transform.transpose(0, 1) @ values
def encode_actions(self, actions: Tensor | Iterable[Iterable[float]]) -> Tensor:
values = torch.as_tensor(actions, dtype=torch.float32)
if values.ndim != 2 or values.shape[-1] != 2 or values.shape[0] == 0:
raise ValueError("actions must have non-empty shape [horizon, 2]")
values = values.clamp(self.ACTION_MINIMUM, self.ACTION_MAXIMUM)
coefficients = self.dct_actions(values) / math.sqrt(values.shape[0])
coefficients = torch.where(
coefficients.abs() < 1e-6, torch.zeros_like(coefficients), coefficients
)
unit = (
coefficients.clamp(self.ACTION_MINIMUM, self.ACTION_MAXIMUM)
- self.ACTION_MINIMUM
) / (self.ACTION_MAXIMUM - self.ACTION_MINIMUM)
payload = torch.round(unit * (self.ACTION_BINS - 1)).long().reshape(-1)
payload = payload + self.ACTION_TOKEN_START
boundaries = torch.tensor(
(self.ACTION_START, self.ACTION_END), dtype=torch.long, device=payload.device
)
return torch.cat((boundaries[:1], payload, boundaries[1:]))
def decode_actions(self, tokens: Tensor | Iterable[int]) -> Tensor:
encoded = torch.as_tensor(tokens, dtype=torch.long)
if encoded.ndim != 1:
raise ValueError("action tokens must be a one-dimensional sequence")
if encoded.numel() < 4 or encoded[0] != self.ACTION_START or encoded[-1] != self.ACTION_END:
raise ValueError("action tokens must be enclosed by ACTION_START and ACTION_END")
payload = encoded[1:-1]
if payload.numel() % 2:
raise ValueError("action token payload must contain complete XY pairs")
if payload.min() < self.ACTION_TOKEN_START or payload.max() > self.ACTION_TOKEN_END:
raise ValueError("action token payload is outside the FAST-lite interval")
quantized = payload - self.ACTION_TOKEN_START
coefficients = (
quantized.float()
/ (self.ACTION_BINS - 1)
* (self.ACTION_MAXIMUM - self.ACTION_MINIMUM)
+ self.ACTION_MINIMUM
)
horizon = payload.numel() // 2
return self.idct_actions(coefficients.reshape(horizon, 2) * math.sqrt(horizon))
def build_policy_sequence(
self,
task: str,
subtask: str | None,
actions: Tensor,
metadata: EpisodeMetadata,
*,
include_episode_metadata: bool = True,
include_subtask: bool = True,
) -> PolicySequence:
prompt, metadata_indices = self.encode_prompt(
task,
subtask,
metadata,
include_episode_metadata=include_episode_metadata,
include_subtask=include_subtask,
)
action_tokens = self.encode_actions(actions).cpu()
input_ids = torch.cat((prompt, action_tokens))
action_start_index = prompt.numel()
labels = torch.full_like(input_ids, -100)
labels[action_start_index - 1 : -1] = input_ids[action_start_index:]
action_prefix_mask = torch.zeros_like(input_ids, dtype=torch.bool)
action_prefix_mask[:action_start_index] = True
return PolicySequence(
input_ids=input_ids,
labels=labels,
action_prefix_mask=action_prefix_mask,
generated_token_start=0,
action_start_index=action_start_index,
prompt_length=prompt.numel(),
metadata_indices=metadata_indices,
)
def build_metadata_cfg_pair(
self,
task: str,
subtask: str | None,
actions: Tensor,
metadata: EpisodeMetadata,
) -> tuple[PolicySequence, PolicySequence]:
"""Return conditioned and episode-metadata-dropped policy sequences."""
conditioned = self.build_policy_sequence(task, subtask, actions, metadata)
unconditional = self.build_policy_sequence(
task,
subtask,
actions,
metadata,
include_episode_metadata=False,
)
return conditioned, unconditional