mmmmmcyaaa's picture
Complete shared baseline runtime dependencies
5cd1287 verified
Raw History Blame Contribute Delete
18.5 kB
#!/usr/bin/env python3
"""Build APE weights on the frozen Florence-Bridge valid-anchor support."""
from __future__ import annotations
import argparse
import json
from collections import OrderedDict, defaultdict
from pathlib import Path
import numpy as np
import pyarrow.parquet as parquet
import torch
from torch.utils.data import DataLoader, TensorDataset
from starVLA.dataloader.infoflow_sampler import LogicalDraw, ValidAnchorManifest
from external_baselines.keyframe_focused_adapted.keyframe_cache import (
ActionCopycat,
history_target,
q99_normalize,
)
def episode_path(data_root: Path, dataset_name: str, trajectory_id: int) -> Path:
return (
data_root
/ dataset_name
/ "data"
/ f"chunk-{trajectory_id // 1000:03d}"
/ f"episode_{trajectory_id:06d}.parquet"
)
def read_actions(
data_root: Path,
dataset_name: str,
trajectory_id: int,
action_dim: int | None,
) -> np.ndarray:
path = episode_path(data_root, dataset_name, trajectory_id)
values = parquet.read_table(path, columns=["action"])["action"].to_pylist()
actions = np.asarray(values, dtype=np.float32)
if actions.ndim != 2 or (action_dim is not None and actions.shape[1] != action_dim):
raise RuntimeError(f"unexpected action shape {actions.shape} in {path}")
return actions
def resolve_statistics_key(
dataset_name: str,
statistics_payload: dict,
explicit_mapping: dict[str, str],
) -> str:
"""Resolve Bridge aliases and arbitrary RoboTwin dataset statistics."""
aliases = {
"bridge_orig_lerobot": "oxe_bridge",
"fractal20220817_data_lerobot": "oxe_rt1",
}
candidates = (
explicit_mapping.get(dataset_name),
aliases.get(dataset_name),
dataset_name,
)
for candidate in candidates:
if candidate and candidate in statistics_payload:
return candidate
if len(statistics_payload) == 1:
return next(iter(statistics_payload))
raise KeyError(
f"no statistics entry for {dataset_name!r}; provide --statistics-key-map"
)
def requested_support(manifest, args):
if args.scope == "full":
return {
dataset.name: OrderedDict(
(int(episode.trajectory_id), tuple(int(x) for x in episode.anchors))
for episode in dataset.episodes
)
for dataset in manifest.datasets
}
selected = defaultdict(lambda: defaultdict(set))
for draw_id in range(args.draw_start, args.draw_start + args.draw_count):
trace = manifest.sample(args.draw_seed, LogicalDraw(0, draw_id))
selected[trace.dataset_name][int(trace.trajectory_id)].add(int(trace.base_index))
return {
dataset.name: OrderedDict(
(
int(episode.trajectory_id),
tuple(sorted(selected[dataset.name][int(episode.trajectory_id)])),
)
for episode in dataset.episodes
if selected[dataset.name].get(int(episode.trajectory_id))
)
for dataset in manifest.datasets
}
def fitting_support(manifest, max_anchors: int | None):
result = {}
for dataset in manifest.datasets:
episodes = OrderedDict()
count = 0
for episode in dataset.episodes:
anchors = tuple(int(x) for x in episode.anchors if int(x) >= 9)
if max_anchors is not None:
anchors = anchors[: max(0, max_anchors - count)]
if anchors:
episodes[int(episode.trajectory_id)] = anchors
count += len(anchors)
if max_anchors is not None and count >= max_anchors:
break
if count == 0:
raise RuntimeError(f"no copycat fitting anchors for {dataset.name}")
result[dataset.name] = episodes
return result
def examples_for_support(
data_root,
dataset_name,
support,
statistics,
*,
action_dim,
binary_action_indices,
history=9,
):
inputs, targets = [], []
for trajectory_id, anchors in support.items():
actions = q99_normalize(
read_actions(data_root, dataset_name, trajectory_id, action_dim),
statistics,
binary_action_indices=binary_action_indices,
)
for anchor in anchors:
x, y = history_target(actions, anchor, history=history)
inputs.append(x)
targets.append(y)
return np.asarray(inputs, dtype=np.float32), np.asarray(targets, dtype=np.float32)
def train_copycat(inputs, targets, *, action_dim, epochs, device, seed=0):
generator = torch.Generator().manual_seed(seed)
permutation = torch.randperm(len(inputs), generator=generator)
split = int(len(inputs) * 0.8)
train_indices = permutation[:split]
val_indices = permutation[split:]
x = torch.from_numpy(inputs)
y = torch.from_numpy(targets)
train = TensorDataset(x[train_indices], y[train_indices])
val = TensorDataset(x[val_indices], y[val_indices])
train_loader = DataLoader(
train, batch_size=512, shuffle=True, generator=generator, num_workers=0
)
val_loader = DataLoader(val, batch_size=512, shuffle=False, num_workers=0)
torch.manual_seed(seed)
model = ActionCopycat(action_dim=action_dim).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, betas=(0.9, 0.999))
history = []
for epoch in range(1, epochs + 1):
model.train()
losses = []
for batch_x, batch_y in train_loader:
batch_x = batch_x.to(device)
batch_y = batch_y.to(device)
optimizer.zero_grad(set_to_none=True)
loss = (model(batch_x) - batch_y).square().mean(dim=-1).mean()
loss.backward()
optimizer.step()
losses.append(float(loss.detach().cpu()))
model.eval()
val_losses = []
with torch.no_grad():
for batch_x, batch_y in val_loader:
batch_x = batch_x.to(device)
batch_y = batch_y.to(device)
loss = (model(batch_x) - batch_y).square().mean(dim=-1)
val_losses.extend(loss.cpu().tolist())
history.append(
{
"epoch": epoch,
"train_loss": float(np.mean(losses)),
"validation_loss": float(np.mean(val_losses)),
}
)
print(json.dumps(history[-1], sort_keys=True), flush=True)
if epoch % max(1, epochs // 3) == 0:
for group in optimizer.param_groups:
group["lr"] *= 0.1
return model, history
def prediction_errors(model, inputs, targets, device):
errors = []
loader = DataLoader(
TensorDataset(torch.from_numpy(inputs), torch.from_numpy(targets)),
batch_size=4096,
shuffle=False,
num_workers=0,
)
model.eval()
with torch.no_grad():
for batch_x, batch_y in loader:
batch_x = batch_x.to(device)
batch_y = batch_y.to(device)
errors.append(
(model(batch_x) - batch_y).square().mean(dim=-1).cpu().numpy()
)
return np.concatenate(errors)
def score_support(
model,
data_root,
dataset_name,
support,
statistics,
device,
*,
action_dim,
binary_action_indices,
):
trajectory_ids, offsets, anchors_out, ape_out = [], [0], [], []
model.eval()
with torch.no_grad():
for trajectory_id, anchors in support.items():
actions = q99_normalize(
read_actions(data_root, dataset_name, trajectory_id, action_dim),
statistics,
binary_action_indices=binary_action_indices,
)
batch_x, batch_y = [], []
for anchor in anchors:
x, y = history_target(actions, anchor, history=9)
batch_x.append(x)
batch_y.append(y)
x = torch.from_numpy(np.asarray(batch_x, dtype=np.float32)).to(device)
y = torch.from_numpy(np.asarray(batch_y, dtype=np.float32)).to(device)
ape = (model(x) - y).square().mean(dim=-1).cpu().numpy()
trajectory_ids.append(trajectory_id)
anchors_out.extend(anchors)
ape_out.extend(ape.tolist())
offsets.append(len(anchors_out))
return {
"trajectory_ids": np.asarray(trajectory_ids, dtype=np.int64),
"offsets": np.asarray(offsets, dtype=np.int64),
"anchors": np.asarray(anchors_out, dtype=np.int32),
"ape": np.asarray(ape_out, dtype=np.float32),
}
def verify_reference_overlap(reference, dataset_index, scored, weights):
prefix = f"d{dataset_index}_"
reference_trajectory_ids = reference[prefix + "trajectory_ids"]
reference_offsets = reference[prefix + "offsets"]
reference_anchors = reference[prefix + "anchors"]
reference_ape = reference[prefix + "ape"]
reference_weights = reference[prefix + "weights"]
new_trajectory_positions = {
int(trajectory_id): position
for position, trajectory_id in enumerate(scored["trajectory_ids"])
}
checked = 0
max_ape_difference = 0.0
for reference_position, trajectory_id in enumerate(reference_trajectory_ids):
new_position = new_trajectory_positions.get(int(trajectory_id))
if new_position is None:
raise RuntimeError(
f"full cache is missing reference trajectory {int(trajectory_id)}"
)
reference_slice = slice(
int(reference_offsets[reference_position]),
int(reference_offsets[reference_position + 1]),
)
new_slice = slice(
int(scored["offsets"][new_position]),
int(scored["offsets"][new_position + 1]),
)
new_anchors = scored["anchors"][new_slice]
positions = np.searchsorted(new_anchors, reference_anchors[reference_slice])
if np.any(positions >= len(new_anchors)) or not np.array_equal(
new_anchors[positions], reference_anchors[reference_slice]
):
raise RuntimeError(
f"full cache does not contain the reference anchor support for "
f"trajectory {int(trajectory_id)}"
)
new_ape = scored["ape"][new_slice][positions]
ape_difference = np.abs(new_ape - reference_ape[reference_slice])
if len(ape_difference):
max_ape_difference = max(max_ape_difference, float(ape_difference.max()))
if not np.array_equal(
weights[new_slice][positions], reference_weights[reference_slice]
):
raise RuntimeError(
f"full cache changed reference weights for trajectory "
f"{int(trajectory_id)}"
)
checked += len(positions)
if max_ape_difference > 1e-6:
raise RuntimeError(
f"full cache changed reference APE scores: max_abs_diff={max_ape_difference}"
)
return {
"checked_anchor_count": checked,
"weights_exact_match": True,
"max_ape_abs_difference": max_ape_difference,
}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--manifest", type=Path, required=True)
parser.add_argument("--data-root", type=Path, required=True)
parser.add_argument("--statistics", type=Path, required=True)
parser.add_argument(
"--statistics-key-map",
type=Path,
help="Optional JSON mapping from manifest dataset name to statistics key.",
)
parser.add_argument(
"--action-dim",
type=int,
choices=(7, 14),
help="Expected action dimension; inferred from statistics when omitted.",
)
parser.add_argument(
"--binary-action-indices",
help=(
"Comma-separated zero-based binary action indices. Defaults to index 6 "
"for 7D Bridge and none for 14D RoboTwin absolute qpos."
),
)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--scope", choices=("full", "draw-range"), default="full")
parser.add_argument("--draw-seed", type=int, default=117465)
parser.add_argument("--draw-start", type=int, default=1_280_000)
parser.add_argument("--draw-count", type=int, default=128)
parser.add_argument("--fit-max-anchors", type=int)
parser.add_argument("--epochs", type=int, default=300)
parser.add_argument("--device", default="cuda")
parser.add_argument("--reference-cache", type=Path)
args = parser.parse_args()
manifest = ValidAnchorManifest.load(args.manifest)
statistics_payload = json.loads(args.statistics.read_text(encoding="utf-8"))
statistics_key_map = (
json.loads(args.statistics_key_map.read_text(encoding="utf-8"))
if args.statistics_key_map is not None
else {}
)
reference = None
reference_metadata = None
if args.reference_cache is not None:
if args.scope != "full":
raise ValueError("--reference-cache requires --scope full")
reference = np.load(args.reference_cache, allow_pickle=False)
reference_metadata = json.loads(str(reference["metadata_json"]))
if int(reference_metadata["copycat_epochs"]) != args.epochs:
raise RuntimeError("reference cache copycat epoch count does not match")
scoring = requested_support(manifest, args)
fitting = fitting_support(manifest, args.fit_max_anchors)
arrays = {}
dataset_reports = []
for dataset_index, dataset in enumerate(manifest.datasets):
dataset_name = dataset.name
statistics_key = resolve_statistics_key(
dataset_name, statistics_payload, statistics_key_map
)
action_statistics = statistics_payload[statistics_key]["action"]
inferred_action_dim = len(action_statistics["q99"])
action_dim = args.action_dim or inferred_action_dim
if inferred_action_dim != action_dim:
raise RuntimeError(
f"statistics action dimension {inferred_action_dim} != {action_dim} "
f"for {dataset_name}"
)
if args.binary_action_indices is None:
binary_action_indices = (6,) if action_dim == 7 else ()
else:
binary_action_indices = tuple(
int(value)
for value in args.binary_action_indices.split(",")
if value.strip()
)
inputs, targets = examples_for_support(
args.data_root,
dataset_name,
fitting[dataset_name],
action_statistics,
action_dim=action_dim,
binary_action_indices=binary_action_indices,
)
print(
f"training copycat for {dataset_name}: {len(inputs)} anchors",
flush=True,
)
model, train_history = train_copycat(
inputs,
targets,
action_dim=action_dim,
epochs=args.epochs,
device=torch.device(args.device),
)
fitted_ape = prediction_errors(
model, inputs, targets, torch.device(args.device)
)
scored = score_support(
model,
args.data_root,
dataset_name,
scoring[dataset_name],
action_statistics,
torch.device(args.device),
action_dim=action_dim,
binary_action_indices=binary_action_indices,
)
if reference_metadata is None:
threshold_values = scored["ape"] if args.scope == "full" else fitted_ape
threshold = float(np.quantile(threshold_values, 0.9))
else:
reference_report = reference_metadata["datasets"][dataset_index]
if reference_report["name"] != dataset_name:
raise RuntimeError("reference cache dataset order does not match")
if int(reference_report["fit_anchor_count"]) != len(inputs):
raise RuntimeError("reference cache fitting support does not match")
threshold = float(reference_report["ape_threshold_90th_percentile"])
weights = np.where(scored["ape"] >= threshold, 5.0, 1.0).astype(np.float32)
overlap_report = None
if reference is not None:
overlap_report = verify_reference_overlap(
reference, dataset_index, scored, weights
)
for key, value in scored.items():
arrays[f"d{dataset_index}_{key}"] = value
arrays[f"d{dataset_index}_weights"] = weights
dataset_reports.append(
{
"name": dataset_name,
"statistics_key": statistics_key,
"action_dim": action_dim,
"binary_action_indices": list(binary_action_indices),
"fit_anchor_count": len(inputs),
"scored_anchor_count": len(scored["anchors"]),
"ape_threshold_90th_percentile": threshold,
"upweighted_count": int((weights > 1).sum()),
"copycat_training": train_history,
"reference_overlap": overlap_report,
}
)
metadata = {
"schema_version": 1,
"method": "Keyframe-Focused-adapted",
"history_length": 9,
"copycat_hidden_units": [300],
"copycat_optimizer": {"name": "Adam", "lr": 1e-3, "betas": [0.9, 0.999]},
"copycat_epochs": args.epochs,
"ape_metric": "mean squared error over normalized action dimensions",
"boundary_padding": "repeat earliest physical action",
"threshold_scope": "within dataset source",
"threshold_percentile": 90,
"high_weight": 5.0,
"low_weight": 1.0,
"cache_scope": args.scope,
"threshold_source": (
"reference_cache" if args.reference_cache is not None else "computed"
),
"dataset_names": [dataset.name for dataset in manifest.datasets],
"datasets": dataset_reports,
}
arrays["metadata_json"] = np.asarray(json.dumps(metadata, sort_keys=True))
args.output.parent.mkdir(parents=True, exist_ok=True)
temporary = args.output.with_suffix(args.output.suffix + ".tmp")
with temporary.open("wb") as destination:
np.savez_compressed(destination, **arrays)
temporary.replace(args.output)
print(f"wrote {args.output}", flush=True)
if __name__ == "__main__":
main()