multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
f7dc16a verified
Raw History Blame Contribute Delete
52.3 kB
import os, re, random, warnings
import torch
import numpy as np
import pandas as pd
from pathlib import Path
_REPO_DIR = str(Path(__file__).resolve().parents[2] / "asset")
_BLANK_PROMPT_EMB_FILENAME = "blank_string_prompt_emb.pth"
def _default_blank_prompt_emb_path() -> str:
return os.path.join(_REPO_DIR, _BLANK_PROMPT_EMB_FILENAME)
def _parse_int_csv(value, *, default=None, name="value"):
if value is None or value == "":
return default
if isinstance(value, (list, tuple)):
return [int(v) for v in value]
return [int(v.strip()) for v in str(value).split(",") if v.strip()]
def _prompt_emb_to_cpu(prompt_emb: dict) -> dict:
if not isinstance(prompt_emb, dict):
raise TypeError(f"prompt emb should be dict, got {type(prompt_emb)}")
return {
k: v.detach().cpu() if torch.is_tensor(v) else v for k, v in prompt_emb.items()
}
def _save_prompt_emb_atomic(prompt_emb: dict, path: str) -> None:
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
rank = os.environ.get("RANK", "0")
tmp_path = f"{path}.tmp.rank{rank}.pid{os.getpid()}"
try:
torch.save(prompt_emb, tmp_path)
os.replace(tmp_path, path)
finally:
if os.path.exists(tmp_path):
os.remove(tmp_path)
def _resolve_path(base_path, p):
"""CSV 路径可为绝对路径;否则相对 dataset_path。"""
p = str(p)
return p if os.path.isabs(p) else os.path.join(base_path, p)
def _camera12_from_absolute_c2w(
c2w_abs: torch.Tensor, pos_scale: float
) -> torch.Tensor:
"""Convert absolute cameras to the model's shared-relative representation.
Args:
c2w_abs: Float tensor ``[P,T,4,4]`` in the shared CS world frame.
pos_scale: Positive translation normalization in metres.
Returns:
Float32 tensor ``[P,T,12]``, anchored at player 0/frame 0.
"""
if c2w_abs.ndim != 4 or tuple(c2w_abs.shape[-2:]) != (4, 4):
raise ValueError(f"absolute c2w 应为 [P,T,4,4],got {tuple(c2w_abs.shape)}")
if c2w_abs.shape[0] < 1 or c2w_abs.shape[1] < 1:
raise ValueError(f"absolute c2w 的 P/T 必须为正,got {tuple(c2w_abs.shape)}")
if not torch.isfinite(c2w_abs).all():
raise ValueError("absolute c2w 包含 NaN/Inf")
if not np.isfinite(pos_scale) or float(pos_scale) <= 0:
raise ValueError(f"camera pos_scale 必须为正,got {pos_scale}")
c2w = c2w_abs.to(dtype=torch.float64)
expected_bottom = torch.tensor(
[0.0, 0.0, 0.0, 1.0], dtype=c2w.dtype, device=c2w.device
)
if not torch.allclose(c2w[..., 3, :], expected_bottom, atol=1e-09, rtol=0.0):
raise ValueError("absolute c2w 齐次矩阵最后一行不是 [0,0,0,1]")
rotations = c2w[..., :3, :3]
eye3 = torch.eye(3, dtype=c2w.dtype, device=c2w.device)
if not torch.allclose(
rotations.transpose(-1, -2) @ rotations, eye3, atol=2e-07, rtol=0.0
):
raise ValueError("absolute c2w rotation 不是正交矩阵")
det = torch.linalg.det(rotations)
if not torch.allclose(det, torch.ones_like(det), atol=2e-07, rtol=0.0):
raise ValueError("absolute c2w rotation determinant 不是 +1")
anchor = c2w[0, 0]
anchor_w2c = torch.eye(4, dtype=c2w.dtype, device=c2w.device)
anchor_w2c[:3, :3] = anchor[:3, :3].transpose(0, 1)
anchor_w2c[:3, 3] = -(anchor[:3, :3].transpose(0, 1) @ anchor[:3, 3])
c2w_rel = anchor_w2c.view(1, 1, 4, 4) @ c2w
c2w_rel[..., :3, 3] /= float(pos_scale)
return c2w_rel[..., :3, :].reshape(c2w.shape[0], c2w.shape[1], 12).float()
def _ensure_prompt_batch_dim(t: torch.Tensor, *, path: str, key: str) -> torch.Tensor:
if not torch.is_tensor(t):
raise TypeError(f"prompt emb {key!r} 不是 tensor: {path}")
if t.ndim == 2:
return t.unsqueeze(0)
if t.ndim >= 3:
return t
raise ValueError(f"prompt emb {key!r} 形状异常 {tuple(t.shape)}: {path}")
def _load_prompt_emb_many(paths: list[str]) -> dict:
"""Load per-player prompt embeddings and stack on P.
Each file is expected to contain one player's context, e.g. [1,S,4096].
Return shape per key: [P,S,4096] before DataLoader adds B -> [B,P,S,4096].
"""
embs = [torch.load(p, weights_only=True, map_location="cpu") for p in paths]
if any((not isinstance(e, dict) for e in embs)):
raise TypeError(f"prompt emb 文件应为 dict: {paths}")
keys = list(embs[0].keys())
out = {}
for k in keys:
vals = []
for path, emb in zip(paths, embs):
if k not in emb:
raise KeyError(f"prompt emb key {k!r} 不存在于 {path}")
v = _ensure_prompt_batch_dim(emb[k], path=path, key=k)
if v.shape[0] != 1:
raise ValueError(
f"prompt_emb_path* 应各自只含 1 条 player emb,但 key {k!r} 在 {path} 的形状为 {tuple(v.shape)}"
)
if vals and tuple(v.shape[1:]) != tuple(vals[0].shape[1:]):
raise ValueError(
f"prompt emb key {k!r} 除 player 维外形状不一致: {tuple(v.shape)} vs {tuple(vals[0].shape)} ({path})"
)
vals.append(v)
out[k] = torch.cat(vals, dim=0)
return out
def consecutive_precomputed_start_rows(
metadata, consecutive_rows: int, num_players: int
) -> list[int]:
"""Return CSV row starts whose following rows are the same continuous clip."""
consecutive_rows = int(consecutive_rows)
if consecutive_rows < 1:
raise ValueError(f"consecutive_rows must be >= 1, got {consecutive_rows}")
n_rows = len(metadata)
if consecutive_rows == 1:
return list(range(n_rows))
required = ["start_frame"]
for pidx in range(1, int(num_players) + 1):
required.extend((f"video_path{pidx}", f"video{pidx}_frame"))
missing = [column for column in required if column not in metadata.columns]
if missing:
raise ValueError(
f"consecutive precomputed rows require CSV columns {missing} to verify continuity"
)
valid = []
for start in range(0, n_rows - consecutive_rows + 1):
ok = True
for row in range(start, start + consecutive_rows - 1):
lengths = [
int(metadata.iloc[row][f"video{pidx}_frame"])
for pidx in range(1, num_players + 1)
]
if min(lengths) <= 0 or len(set(lengths)) != 1:
ok = False
break
if (
int(metadata.iloc[row + 1]["start_frame"])
!= int(metadata.iloc[row]["start_frame"]) + lengths[0]
):
ok = False
break
for pidx in range(1, num_players + 1):
current_dir = os.path.dirname(
str(metadata.iloc[row][f"video_path{pidx}"]).replace("\\", "/")
)
next_dir = os.path.dirname(
str(metadata.iloc[row + 1][f"video_path{pidx}"]).replace("\\", "/")
)
if current_dir != next_dir:
ok = False
break
if not ok:
break
if ok:
valid.append(start)
return valid
class PrecomputedLatentDataset(torch.utils.data.Dataset):
"""直接从预处理好的 CSV 读取压好的 VAE latent + action(跳过在线读视频 + VAE 编码)。
CSV 列(路径相对 base_path=preprocess root;绝对路径则原样用):
latent_path1..latent_pathP(或 vae_path1..P), action_path1..action_pathP
camera pose(仅 --use_camera_pose 时读取):
- 只读预计算好的 camera_pose_path/camera12_path,或 per-player camera_pose_path1..P;
- per-player 文件可为旧 [T,12],也可为 canonical absolute c2w [T,4,4];后者在
P 路全部加载后,以 compact player 0 / frame 0 统一计算 shared-relative camera12;
- 训练时不再从 json_file1/2 现场生成 camera12,避免 dataloader 反复读 action JSON。
每段(CSV 一行)对应的物理文件:
*.latent.pt : torch.save 的张量 [C=48, T_lat, h, w](bf16)
*.action.pt : torch.save({"keyboard": float[T,23], "mouse": float[T,2]})(mouse 已 compress_mouse_linear)
返回的 batch key 与 TensorDataset 默认路径下游一致:
latents[P,48,T_lat,h,w]、keyboard[P,T,23]、mouse[P,T,2]、prompt_emb[P,S,4096]。
为兼容旧 2-player 路径,当 P=2 时也会额外返回 latents1/latents2。
latent 与 action 由同一 VAE 和同一切片离线算出。
"""
def __init__(
self,
base_path,
metadata_path,
steps_per_epoch=2000,
num_players=2,
target_rgb_frames=165,
blank_prompt_emb_path=None,
vae_latent_channels=48,
debug_no_retry=False,
num_frame_per_block=3,
local_attn_size=24,
use_camera_pose=False,
use_group_labels=False,
num_groups=1,
group_path_column="group_path",
group_label_player_ids=None,
hier_group_mode="csv",
hier_group_seed=0,
hier_random_preserve_sizes=True,
hier_group_format="labels",
consecutive_rows=1,
grouping_sample_indices=None,
grouping_cache_dir="outputs/grouping_cache",
grouping_model_profile=(5, 6, 1.0, 3),
allow_first_frame_latents=False,
):
metadata = pd.read_csv(metadata_path)
self.num_players = int(num_players)
if self.num_players < 2:
raise ValueError(f"num_players must be >= 2, got {self.num_players}")
latent_cols = []
action_cols = []
missing = []
for pidx in range(1, self.num_players + 1):
latent_col = None
for candidate in (f"latent_path{pidx}", f"vae_path{pidx}"):
if candidate in metadata.columns:
latent_col = candidate
break
if latent_col is None:
missing.append(f"latent_path{pidx}/vae_path{pidx}")
latent_cols.append(latent_col)
action_col = f"action_path{pidx}"
if action_col not in metadata.columns:
missing.append(action_col)
action_cols.append(action_col)
if missing:
raise ValueError(
f"[PrecomputedLatentDataset] CSV 缺少预处理列 {missing};该 CSV 似乎不是 *.preprocessed.csv。现有列:{list(metadata.columns)}"
)
self.metadata = metadata
self.base_path = base_path
self.latent_column_names = latent_cols
self.action_column_names = action_cols
self.latent_paths = [
[_resolve_path(base_path, x) for x in metadata[col]]
for col in self.latent_column_names
]
self.action_paths = [
[_resolve_path(base_path, x) for x in metadata[col]]
for col in self.action_column_names
]
self.latent_path1 = self.latent_paths[0]
self.latent_path2 = self.latent_paths[1]
self.action_path1 = self.action_paths[0]
self.action_path2 = self.action_paths[1]
self.use_camera_pose = bool(use_camera_pose)
self.camera_pose_path = None
self.camera_pose_paths = None
combined_camera_col = next(
(c for c in ("camera_pose_path", "camera12_path") if c in metadata.columns),
None,
)
pair_camera_cols = next(
(
prefix
for prefix in ("camera_pose_path", "camera12_path")
if all(
(
f"{prefix}{pidx}" in metadata.columns
for pidx in range(1, self.num_players + 1)
)
)
),
None,
)
if combined_camera_col is not None and pair_camera_cols is not None:
raise ValueError(
"CSV camera pose 列不能同时使用 combined 和 per-player 两种格式"
)
for prefix in ("camera_pose_path", "camera12_path"):
present = [
f"{prefix}{pidx}" in metadata.columns
for pidx in range(1, self.num_players + 1)
]
if any(present) and (not all(present)):
raise ValueError(
f"CSV 若使用 per-player camera pose,必须同时包含 {prefix}1..{prefix}{self.num_players}"
)
if combined_camera_col is not None:
self.camera_pose_path = [
_resolve_path(base_path, x) for x in metadata[combined_camera_col]
]
elif pair_camera_cols is not None:
self.camera_pose_paths = [
[
_resolve_path(base_path, x)
for x in metadata[f"{pair_camera_cols}{pidx}"]
]
for pidx in range(1, self.num_players + 1)
]
if (
self.use_camera_pose
and self.camera_pose_path is None
and (self.camera_pose_paths is None)
):
raise ValueError(
f"--use_camera_pose requires camera_pose_path/camera12_path, or per-player camera_pose_path1..{self.num_players} / camera12_path1..{self.num_players}. Camera poses must already be precomputed."
)
self.prompt_emb_paths = None
prompt_cols = [
f"prompt_emb_path{pidx}" for pidx in range(1, self.num_players + 1)
]
prompt_present = [c in metadata.columns for c in prompt_cols]
if any(prompt_present) and (not all(prompt_present)):
raise ValueError(
f"CSV 若使用 prompt emb,必须同时包含 prompt_emb_path1..prompt_emb_path{self.num_players}"
)
if all(prompt_present):
self.prompt_emb_paths = [
[_resolve_path(base_path, x) for x in metadata[col]]
for col in prompt_cols
]
self.prompt_emb_path1 = (
self.prompt_emb_paths[0] if self.prompt_emb_paths else None
)
self.prompt_emb_path2 = (
self.prompt_emb_paths[1] if self.prompt_emb_paths else None
)
self.captions = None
caption_cols = [f"caption{pidx}" for pidx in range(1, self.num_players + 1)]
caption_present = [c in metadata.columns for c in caption_cols]
if any(caption_present) and (not all(caption_present)):
raise ValueError(
f"CSV 若使用 caption,必须同时包含 caption1..caption{self.num_players}"
)
if all(caption_present):
self.captions = [
[str(v).strip() if pd.notna(v) else "" for v in metadata[col]]
for col in caption_cols
]
print(
f"[PrecomputedLatentDataset] 使用 caption1..caption{self.num_players} 作为 validation prompt。"
)
self.caption1 = self.captions[0] if self.captions else None
self.caption2 = self.captions[1] if self.captions else None
self.use_group_labels = bool(use_group_labels)
self.num_groups = int(num_groups)
self.group_path_column = str(group_path_column)
self.hier_group_mode = str(hier_group_mode)
self.hier_group_seed = int(hier_group_seed)
self.hier_random_preserve_sizes = bool(hier_random_preserve_sizes)
self.hier_group_format = str(hier_group_format)
if self.hier_group_mode not in ("csv", "random_static", "random_block"):
raise ValueError(
f"hier_group_mode must be csv/random_static/random_block, got {self.hier_group_mode!r}"
)
if self.hier_group_format not in ("labels", "membership"):
raise ValueError(
f"hier_group_format must be labels or membership, got {self.hier_group_format!r}"
)
if self.hier_group_format == "membership" and self.hier_group_mode != "csv":
raise ValueError(
f"hier_group_format=membership does not support hier_group_mode={self.hier_group_mode!r}; use csv. Random modes only support the labels format."
)
self.group_label_player_ids = _parse_int_csv(
group_label_player_ids,
default=list(range(1, self.num_players + 1)),
name="group_label_player_ids",
)
if len(self.group_label_player_ids) != self.num_players:
raise ValueError(
f"group_label_player_ids length {len(self.group_label_player_ids)} must equal num_players={self.num_players}: {self.group_label_player_ids}"
)
if any((pid < 1 or pid > 10 for pid in self.group_label_player_ids)):
raise ValueError(
f"group_label_player_ids must be 1-based ids in [1,10], got {self.group_label_player_ids}"
)
self.group_paths = None
self.source_n_rows = len(metadata)
self.consecutive_rows = int(consecutive_rows)
if self.consecutive_rows < 1:
raise ValueError(
f"consecutive_rows must be >= 1, got {self.consecutive_rows}"
)
self.sample_start_rows = consecutive_precomputed_start_rows(
metadata, self.consecutive_rows, self.num_players
)
self.n_rows = len(self.sample_start_rows)
if self.n_rows == 0:
raise ValueError(
f"[PrecomputedLatentDataset] no valid {self.consecutive_rows}-row continuous windows in {metadata_path}"
)
self.allow_first_frame_latents = bool(allow_first_frame_latents)
if self.allow_first_frame_latents and self.consecutive_rows != 1:
raise ValueError("First-frame latents require consecutive_rows=1")
self.steps_per_epoch = steps_per_epoch
self.target_rgb_frames = int(target_rgb_frames)
self.vae_latent_channels = int(vae_latent_channels)
self.num_frame_per_block = int(num_frame_per_block)
self.local_attn_size = int(local_attn_size)
self.blank_prompt_emb_path = (
blank_prompt_emb_path or _default_blank_prompt_emb_path()
)
self.blank_prompt_emb = None
self.debug_no_retry = debug_no_retry
if self.use_group_labels:
from multics.grouping.prepare import prepare_dataset_groups
prepare_dataset_groups(
self, grouping_sample_indices, grouping_cache_dir,
(*grouping_model_profile[:3], self.num_frame_per_block),
)
print(
f"[PrecomputedLatentDataset] rows={self.n_rows} target_rgb_frames={self.target_rgb_frames} latent_channels={self.vae_latent_channels} use_camera_pose={self.use_camera_pose} camera_source={('path' if self.camera_pose_path or self.camera_pose_paths else 'none')} prompt_emb={self.prompt_emb_paths is not None} num_players={self.num_players} use_group_labels={self.use_group_labels} group_label_player_ids={(self.group_label_player_ids if self.use_group_labels else 'none')} hier_group_mode={(self.hier_group_mode if self.use_group_labels else 'none')} hier_group_format={(self.hier_group_format if self.use_group_labels else 'none')} hier_group_seed={self.hier_group_seed} hier_random_preserve_sizes={self.hier_random_preserve_sizes} consecutive_rows={self.consecutive_rows} valid_windows={self.n_rows}"
)
def __len__(self):
return max(self.n_rows, self.steps_per_epoch)
def _load_action(self, path):
d = torch.load(path, weights_only=True, map_location="cpu")
return (d["keyboard"].float(), d["mouse"].float())
def _load_camera_pose_file(self, path: str):
ext = os.path.splitext(path)[1].lower()
if ext == ".npy":
obj = np.load(path)
elif ext == ".npz":
with np.load(path) as npz:
if "c2w_abs" in npz and (
not any(
(
key in npz
for key in ("camera_pose", "camera12", "pose", "camera")
)
)
):
obj = {key: np.array(npz[key], copy=True) for key in npz.files}
else:
key = next(
(
k
for k in ("camera_pose", "camera12", "pose", "camera")
if k in npz
),
npz.files[0],
)
obj = np.array(npz[key], copy=True)
else:
obj = torch.load(path, weights_only=True, map_location="cpu")
if isinstance(obj, dict):
if "c2w_abs" in obj and (
not any(
(
key in obj
for key in ("camera_pose", "camera12", "pose", "camera")
)
)
):
return obj
key = next(
(k for k in ("camera_pose", "camera12", "pose", "camera") if k in obj),
None,
)
if key is None:
raise KeyError(
f"camera pose 文件缺少 camera_pose/camera12/pose/camera key: {path}"
)
obj = obj[key]
if not torch.is_tensor(obj):
obj = torch.as_tensor(obj)
return obj.float()
@staticmethod
def _camera_payload_tensor(payload: dict, key: str, *, path: str) -> torch.Tensor:
"""Read one required sidecar field as a tensor.
Args:
payload: Loaded numeric per-video camera dictionary.
key: Required dictionary field name.
path: Source path included in validation errors.
Returns:
The field as a torch tensor without changing its dtype.
"""
if key not in payload:
raise KeyError(f"per-video camera 文件缺少 {key!r}: {path}")
value = payload[key]
return value if torch.is_tensor(value) else torch.as_tensor(value)
def _camera12_from_per_video_absolute(
self, row: int, payloads: list[dict]
) -> torch.Tensor:
"""Stack one row's physical-video sidecars and choose a shared anchor.
Args:
row: Metadata row index used for CSV/path consistency checks.
payloads: Per-video absolute-camera dictionaries in compact CSV order.
Returns:
Float32 ``camera12[P,T_lat,12]`` anchored at the first compact
player's first latent frame. Physical ``player_key`` is checked
against each corresponding ``video_pathN``.
"""
paths = [player_paths[row] for player_paths in self.camera_pose_paths]
c2w_players = []
reference_ticks = None
reference_indices = None
reference_start = None
reference_stride = None
reference_scale = None
reference_unit = None
csv_start = None
if "start_frame" in self.metadata.columns:
csv_start = int(self.metadata.iloc[row]["start_frame"])
for player_index, (payload, path) in enumerate(zip(payloads, paths), start=1):
version = int(
self._camera_payload_tensor(payload, "format_version", path=path)
.reshape(-1)[0]
.item()
)
if version not in (2, 3):
raise ValueError(
f"per-video absolute camera format_version 应为 2/3,got {version}: {path}"
)
c2w = self._camera_payload_tensor(payload, "c2w_abs", path=path)
if c2w.ndim != 3 or tuple(c2w.shape[-2:]) != (4, 4):
raise ValueError(
f"per-video c2w_abs 应为 [T,4,4],got {tuple(c2w.shape)}: {path}"
)
c2w_players.append(c2w)
ticks = self._camera_payload_tensor(
payload, "latent_ticks", path=path
).long()
indices = self._camera_payload_tensor(
payload, "latent_rgb_indices", path=path
).long()
start = int(
self._camera_payload_tensor(payload, "start_frame", path=path)
.reshape(-1)[0]
.item()
)
stride = int(
self._camera_payload_tensor(payload, "latent_stride", path=path)
.reshape(-1)[0]
.item()
)
scale = float(
self._camera_payload_tensor(payload, "pos_scale", path=path)
.reshape(-1)[0]
.item()
)
unit = float(
self._camera_payload_tensor(payload, "world_unit_to_metre", path=path)
.reshape(-1)[0]
.item()
)
player_key = int(
self._camera_payload_tensor(payload, "player_key", path=path)
.reshape(-1)[0]
.item()
)
video_col = f"video_path{player_index}"
if video_col in self.metadata.columns:
video_path = str(self.metadata.iloc[row][video_col]).replace("\\", "/")
match = _CAMERA_PLAYER_PART.search(video_path)
if match is not None and int(match.group(1)) != player_key:
raise ValueError(
f"camera player_key={player_key:02d} 与 {video_col} 中的 player={int(match.group(1)):02d} 不一致: {path}"
)
if csv_start is not None and start != csv_start:
raise ValueError(
f"camera start_frame={start} 与 CSV start_frame={csv_start} 不一致: {path}"
)
if reference_ticks is None:
reference_ticks = ticks
reference_indices = indices
reference_start = start
reference_stride = stride
reference_scale = scale
reference_unit = unit
else:
if not torch.equal(ticks, reference_ticks):
raise ValueError(
f"不同 player 的 camera latent_ticks 不一致: {path}"
)
if not torch.equal(indices, reference_indices):
raise ValueError(
f"不同 player 的 camera latent_rgb_indices 不一致: {path}"
)
if start != reference_start or stride != reference_stride:
raise ValueError(
f"不同 player 的 camera start/stride 不一致: {path}"
)
if scale != reference_scale or unit != reference_unit:
raise ValueError(
f"不同 player 的 camera unit/pos_scale 不一致: {path}"
)
if len({tuple(c2w.shape) for c2w in c2w_players}) != 1:
raise ValueError(
f"不同 player 的 per-video c2w_abs shape 不一致: {[tuple(c2w.shape) for c2w in c2w_players]}"
)
c2w_abs = torch.stack(c2w_players, dim=0)
return _camera12_from_absolute_c2w(c2w_abs, float(reference_scale))
def _load_camera_pose(self, row: int, t_lat: int) -> torch.Tensor | None:
if self.camera_pose_path is not None:
cam = self._load_camera_pose_file(self.camera_pose_path[row])
if isinstance(cam, dict):
raise ValueError(
"combined camera_pose_path 不能指向 per-video absolute c2w;请使用 camera_pose_path1..N"
)
elif self.camera_pose_paths is not None:
cams = [
self._load_camera_pose_file(paths[row])
for paths in self.camera_pose_paths
]
absolute = [isinstance(cam_i, dict) for cam_i in cams]
if any(absolute) and (not all(absolute)):
raise ValueError(
"per-player camera 文件不能混用 absolute c2w 与旧 camera12 格式"
)
if all(absolute):
cam = self._camera12_from_per_video_absolute(row, cams)
else:
if any((cam_i.dim() != 2 for cam_i in cams)):
raise ValueError("旧 per-player camera pose 文件应为 [T,12]")
cam = torch.stack(cams, dim=0)
else:
return None
if cam.dim() != 3:
raise ValueError(
f"camera_pose 应为 [P,T,12] 或 [T,P,12],got {tuple(cam.shape)}"
)
if cam.shape[0] != self.num_players and cam.shape[1] == self.num_players:
cam = cam.permute(1, 0, 2).contiguous()
if cam.shape[0] != self.num_players or cam.shape[-1] != 12:
raise ValueError(
f"camera_pose 形状应为 [P={self.num_players},T,12],got {tuple(cam.shape)}"
)
if cam.shape[1] < t_lat:
raise ValueError(
f"camera_pose latent T={cam.shape[1]} 短于 latent T={t_lat}"
)
return cam[:, :t_lat, :].contiguous()
def _randomize_group_labels(
self, group_labels: torch.Tensor, row: int
) -> torch.Tensor:
"""Apply a deterministic random grouping while preserving [T,P]."""
if self.hier_group_mode == "csv":
return group_labels
t_lat, num_players = group_labels.shape
n_block = max(1, int(self.num_frame_per_block))
generator = torch.Generator()
seed = (self.hier_group_seed + int(row) * 1000003) % ((1 << 63) - 1)
generator.manual_seed(seed)
if self.hier_random_preserve_sizes:
if self.hier_group_mode == "random_static":
perm = torch.randperm(num_players, generator=generator)
return group_labels.index_select(1, perm).contiguous()
randomized = torch.empty_like(group_labels)
for start in range(0, t_lat, n_block):
end = min(start + n_block, t_lat)
perm = torch.randperm(num_players, generator=generator)
assignment = group_labels[start].index_select(0, perm)
randomized[start:end] = assignment
return randomized
randomized = torch.empty_like(group_labels)
if self.hier_group_mode == "random_static":
assignment = torch.randint(
self.num_groups, (num_players,), generator=generator
)
randomized[:] = assignment
else:
for start in range(0, t_lat, n_block):
end = min(start + n_block, t_lat)
assignment = torch.randint(
self.num_groups, (num_players,), generator=generator
)
randomized[start:end] = assignment
return randomized
def _select_group_player_columns(
self, tensor: torch.Tensor, player_dim: int, path: str, name: str
) -> torch.Tensor:
"""Select compact player columns using 1-based group_label_player_ids.
tensor player axis may be the full 10-player roster or already-compact P.
"""
cols = [pid - 1 for pid in self.group_label_player_ids]
n_src = int(tensor.shape[player_dim])
max_id = max(self.group_label_player_ids)
if n_src < max_id:
already_compact = (
n_src == self.num_players
and self.group_label_player_ids == list(range(1, self.num_players + 1))
)
if not already_compact:
raise ValueError(
f"{name} player dim {n_src} cannot cover ids {self.group_label_player_ids} at {path}"
)
return tensor
return tensor.index_select(player_dim, torch.tensor(cols, dtype=torch.long))
@staticmethod
def _assert_block_constant(
tensor: torch.Tensor, n_block: int, path: str, name: str
) -> None:
"""Grouping must be constant inside each latent temporal block (n=3)."""
if n_block <= 1:
return
t_lat = int(tensor.shape[0])
for start in range(0, t_lat, n_block):
block = tensor[start : start + n_block]
if block.numel() and (not torch.equal(block, block[:1].expand_as(block))):
raise ValueError(
f"{name} must be constant inside each latent temporal block; path={path} block=[{start},{min(start + n_block, t_lat)})"
)
def _load_group_labels(self, row: int, t_lat: int) -> dict:
"""Load grouping tensors for one CSV row.
labels mode returns:
group_labels [T,P] long in [0,N), active_group [T,N] bool
membership mode additionally returns:
membership [T,N,P] bool, global_only [T,P] bool
group_labels is debug-only (argmax; -1 = not in any local group)
eligible stays in the .pt and is only asserted, never returned.
"""
if self.group_paths is None:
raise RuntimeError(
"use_group_labels=True but group_paths is not initialized"
)
path = self.group_paths[row]
d = torch.load(path, weights_only=True, map_location="cpu")
n_block = max(1, int(self.num_frame_per_block))
if self.hier_group_format == "membership":
if not isinstance(d, dict) or "membership" not in d:
raise ValueError(
f"hier_group_format=membership requires key 'membership' in {path}; refusing to silently fall back to labels (anchors / orphans would be wrong)"
)
membership = d["membership"]
if not torch.is_tensor(membership):
membership = torch.as_tensor(membership)
if membership.dim() != 3:
raise ValueError(
f"membership must be [T,N,P] bool, got {tuple(membership.shape)} at {path}"
)
if membership.shape[0] < t_lat:
raise ValueError(
f"membership T={membership.shape[0]} shorter than latent T={t_lat}: {path}"
)
if int(membership.shape[1]) != self.num_groups:
raise ValueError(
f"membership N={int(membership.shape[1])} must equal num_groups={self.num_groups} at {path}"
)
membership = membership[:t_lat].to(dtype=torch.bool)
membership = self._select_group_player_columns(
membership, player_dim=2, path=path, name="membership"
)
if membership.shape[2] != self.num_players:
raise ValueError(
f"membership P={membership.shape[2]} must equal num_players={self.num_players} at {path}"
)
if "global_only" not in d:
raise ValueError(
f"hier_group_format=membership requires key 'global_only' in {path}"
)
global_only = d["global_only"]
if not torch.is_tensor(global_only):
global_only = torch.as_tensor(global_only)
if global_only.dim() != 2:
raise ValueError(
f"global_only must be [T,P] bool, got {tuple(global_only.shape)} at {path}"
)
if global_only.shape[0] < t_lat:
raise ValueError(
f"global_only T={global_only.shape[0]} shorter than latent T={t_lat}: {path}"
)
global_only = global_only[:t_lat].to(dtype=torch.bool)
global_only = self._select_group_player_columns(
global_only, player_dim=1, path=path, name="global_only"
)
if global_only.shape[1] != self.num_players:
raise ValueError(
f"global_only P={global_only.shape[1]} must equal num_players={self.num_players} at {path}"
)
if "eligible" not in d:
raise ValueError(
f"hier_group_format=membership requires key 'eligible' [P] in {path}"
)
eligible = d["eligible"]
if not torch.is_tensor(eligible):
eligible = torch.as_tensor(eligible)
eligible = eligible.to(dtype=torch.bool).reshape(-1)
eligible = self._select_group_player_columns(
eligible.unsqueeze(0), player_dim=1, path=path, name="eligible"
).squeeze(0)
if eligible.numel() != self.num_players:
raise ValueError(
f"eligible P={eligible.numel()} must equal num_players={self.num_players} at {path}"
)
if not bool(eligible.all().item()):
raise ValueError(
f"eligible must be all True for this training CSV (not used in the mask); got {eligible.tolist()} at {path}"
)
self._assert_block_constant(membership, n_block, path, "membership")
self._assert_block_constant(global_only, n_block, path, "global_only")
active_group = membership.any(dim=-1).contiguous()
if "labels" in d:
labels = d["labels"]
if not torch.is_tensor(labels):
labels = torch.as_tensor(labels)
group_labels = labels[:t_lat].to(dtype=torch.long)
group_labels = self._select_group_player_columns(
group_labels, player_dim=1, path=path, name="labels"
).contiguous()
else:
any_g = membership.any(dim=1)
group_labels = membership.to(dtype=torch.long).argmax(dim=1)
group_labels = torch.where(
any_g, group_labels, torch.full_like(group_labels, -1)
)
return {
"group_labels": group_labels,
"active_group": active_group,
"membership": membership.contiguous(),
"global_only": global_only.contiguous(),
}
if isinstance(d, dict) and "labels" not in d:
raise ValueError(
f"hier_group_format=labels requires key 'labels' in {path}; this looks like a membership-only file. Pass --hier_group_format membership."
)
labels = d["labels"] if isinstance(d, dict) else d
if not torch.is_tensor(labels):
labels = torch.as_tensor(labels)
if labels.dim() != 2 or labels.shape[1] < max(self.group_label_player_ids):
already_compact = (
labels.dim() == 2
and labels.shape[1] == self.num_players
and (
self.group_label_player_ids == list(range(1, self.num_players + 1))
)
)
if not already_compact:
raise ValueError(
f"group labels should be [T,10] and cover ids {self.group_label_player_ids}, got {tuple(labels.shape)} at {path}"
)
if labels.shape[0] < t_lat:
raise ValueError(
f"group labels T={labels.shape[0]} shorter than latent T={t_lat}: {path}"
)
group_labels = labels[:t_lat].to(dtype=torch.long)
group_labels = self._select_group_player_columns(
group_labels, player_dim=1, path=path, name="labels"
).contiguous()
if group_labels.numel() and (
int(group_labels.min().item()) < 0
or int(group_labels.max().item()) >= self.num_groups
):
raise ValueError(
f"group_labels values out of range [0,{self.num_groups}) at {path}: min={int(group_labels.min().item())} max={int(group_labels.max().item())}"
)
self._assert_block_constant(group_labels, n_block, path, "group_labels")
group_labels = self._randomize_group_labels(group_labels, row)
active_group = torch.zeros(t_lat, self.num_groups, dtype=torch.bool)
active_group.scatter_(1, group_labels, True)
return {"group_labels": group_labels, "active_group": active_group}
def _load_camera_pose_rows(
self, rows: list[int], latent_lengths: list[int], target_t_lat: int
) -> torch.Tensor | None:
"""Load consecutive camera segments in one shared relative frame.
Multi-row inference requires canonical per-player absolute c2w files.
Concatenating legacy camera12 tensors would silently reset the relative
anchor at every CSV boundary, so that ambiguous case is rejected.
"""
if len(rows) == 1:
return self._load_camera_pose(rows[0], target_t_lat)
if self.camera_pose_paths is None:
raise ValueError(
"consecutive_rows>1 with camera pose requires per-player canonical camera_pose_path1..P containing absolute c2w"
)
segments = []
reference_scale = None
for row, t_lat in zip(rows, latent_lengths):
payloads = [
self._load_camera_pose_file(paths[row])
for paths in self.camera_pose_paths
]
if not all((isinstance(payload, dict) for payload in payloads)):
raise ValueError(
"consecutive_rows>1 cannot concatenate legacy relative camera12; use canonical per-player absolute c2w sidecars"
)
self._camera12_from_per_video_absolute(row, payloads)
scales = [
float(
self._camera_payload_tensor(payload, "pos_scale", path=paths[row])
.reshape(-1)[0]
.item()
)
for payload, paths in zip(payloads, self.camera_pose_paths)
]
if len(set(scales)) != 1:
raise ValueError(
f"camera pos_scale differs between players at CSV row {row}: {scales}"
)
if reference_scale is None:
reference_scale = scales[0]
elif scales[0] != reference_scale:
raise ValueError(
f"camera pos_scale differs across consecutive rows: {reference_scale} vs {scales[0]}"
)
c2w = torch.stack(
[
self._camera_payload_tensor(payload, "c2w_abs", path=paths[row])
for payload, paths in zip(payloads, self.camera_pose_paths)
],
dim=0,
)
if c2w.shape[1] < t_lat:
raise ValueError(
f"camera latent T={c2w.shape[1]} shorter than row latent T={t_lat} at CSV row {row}"
)
segments.append(c2w[:, :t_lat])
absolute = torch.cat(segments, dim=1)
if absolute.shape[1] < target_t_lat:
raise ValueError(
f"combined camera latent T={absolute.shape[1]} shorter than target {target_t_lat}"
)
return _camera12_from_absolute_c2w(
absolute[:, :target_t_lat], float(reference_scale)
).contiguous()
def _load_group_rows(
self, rows: list[int], latent_lengths: list[int], target_t_lat: int
) -> dict:
parts = [
self._load_group_labels(row, t_lat)
for row, t_lat in zip(rows, latent_lengths)
]
keys = set(parts[0])
if any((set(part) != keys for part in parts[1:])):
raise ValueError("group tensor keys differ across consecutive CSV rows")
result = {key: torch.cat([part[key] for part in parts], dim=0) for key in keys}
for key, tensor in result.items():
if tensor.shape[0] < target_t_lat:
raise ValueError(
f"combined {key} T={tensor.shape[0]} shorter than target latent T={target_t_lat}"
)
result[key] = tensor[:target_t_lat].contiguous()
return result
def __getitem__(self, index):
for _ in range(20):
sample_index = index % self.n_rows
row = self.sample_start_rows[sample_index]
if self.use_group_labels and any(
r not in self.prepared_group_rows
for r in range(row, row + self.consecutive_rows)
):
raise ValueError(f"Grouping was not prepared for sample {sample_index}")
try:
rows = list(range(row, row + self.consecutive_rows))
row_latents = [
[
torch.load(paths[r], weights_only=True, map_location="cpu")
for paths in self.latent_paths
]
for r in rows
]
row_actions = [
[self._load_action(paths[r]) for paths in self.action_paths]
for r in rows
]
latent_lengths = []
for segment_idx, (
source_row,
segment_latents,
segment_actions,
) in enumerate(zip(rows, row_latents, row_actions)):
for pidx, lat in enumerate(segment_latents, start=1):
if lat.dim() != 4 or lat.shape[0] != self.vae_latent_channels:
raise RuntimeError(
f"latent{pidx} shape {tuple(lat.shape)}, expected [{self.vae_latent_channels}, T_lat, h, w]: {self.latent_paths[pidx - 1][source_row]}"
)
if tuple(lat.shape) != tuple(segment_latents[0].shape):
raise RuntimeError(
f"latent{pidx} shape {tuple(lat.shape)} differs from latent1 {tuple(segment_latents[0].shape)} at CSV row {source_row}"
)
segment_rgb_t = int(segment_actions[0][0].shape[0])
expected_latent_t = (segment_rgb_t - 1) // 4 + 1
first_frame_only = (
self.allow_first_frame_latents
and int(segment_latents[0].shape[1]) == 1
)
if (
not first_frame_only
and int(segment_latents[0].shape[1]) != expected_latent_t
):
raise RuntimeError(
f"CSV row {source_row} latent T={segment_latents[0].shape[1]} does not match action T={segment_rgb_t} (expected {expected_latent_t})"
)
for pidx, (keyboard_i, mouse_i) in enumerate(
segment_actions, start=1
):
if keyboard_i.shape != (segment_rgb_t, 23) or mouse_i.shape != (
segment_rgb_t,
2,
):
raise RuntimeError(
f"action{pidx} shape invalid at CSV row {source_row}: keyboard={tuple(keyboard_i.shape)} mouse={tuple(mouse_i.shape)}"
)
latent_lengths.append(expected_latent_t)
target_t_lat = (self.target_rgb_frames - 1) // 4 + 1
latents_per_player = [
torch.cat([segment[pidx] for segment in row_latents], dim=1)[
:, :target_t_lat
]
for pidx in range(self.num_players)
]
keyboards = [
torch.cat([segment[pidx][0] for segment in row_actions], dim=0)[
: self.target_rgb_frames
]
for pidx in range(self.num_players)
]
mice = [
torch.cat([segment[pidx][1] for segment in row_actions], dim=0)[
: self.target_rgb_frames
]
for pidx in range(self.num_players)
]
got_T = int(keyboards[0].shape[0])
if got_T < self.target_rgb_frames:
warnings.warn(
f"[PrecomputedLatentDataset] action 比 target 短:got={got_T} want={self.target_rgb_frames} (row={row}, {self.action_paths[0][row]})。"
)
for pidx, (kb, ms) in enumerate(zip(keyboards, mice), start=1):
if kb.dim() != 2 or ms.dim() != 2:
raise RuntimeError(
f"action{pidx} 形状异常 keyboard={tuple(kb.shape)} mouse={tuple(ms.shape)}"
)
if kb.shape[0] != got_T or ms.shape[0] != got_T:
raise RuntimeError(
f"action{pidx} T 与 action1 不一致: keyboard={tuple(kb.shape)} mouse={tuple(ms.shape)}, action1_T={got_T}"
)
if kb.shape[-1] != 23 or ms.shape[-1] != 2:
raise RuntimeError(
f"action{pidx} feature dim 异常,期望 keyboard[...,23] / mouse[...,2],got {tuple(kb.shape)} / {tuple(ms.shape)}"
)
exp_T_lat = (got_T - 1) // 4 + 1
if latents_per_player[0].shape[1] != exp_T_lat and not (
self.allow_first_frame_latents
and latents_per_player[0].shape[1] == 1
):
raise RuntimeError(
f"latent T({latents_per_player[0].shape[1]}) 与 action T({got_T}) 不一致:期望 latent_T=(T-1)//4+1={exp_T_lat}({self.latent_paths[0][row]})"
)
latents = torch.stack(latents_per_player, dim=0)
keyboard = torch.stack(keyboards, dim=0)
mouse = torch.stack(mice, dim=0)
data = {
"latents": latents,
"keyboard": keyboard,
"mouse": mouse,
"source_rows": torch.tensor(rows, dtype=torch.long),
}
if self.num_players == 2:
data["latents1"] = latents[0]
data["latents2"] = latents[1]
if self.use_camera_pose:
camera_pose = self._load_camera_pose_rows(
rows, latent_lengths, exp_T_lat
)
if camera_pose is None:
raise RuntimeError(
"use_camera_pose=True 但 CSV 没有可读取的 camera_pose_path/camera12_path"
)
data["camera_pose"] = camera_pose
if self.use_group_labels:
data.update(
self._load_group_rows(rows, latent_lengths, exp_T_lat)
)
if self.prompt_emb_paths is not None:
data["prompt_emb"] = _load_prompt_emb_many(
[paths[row] for paths in self.prompt_emb_paths]
)
else:
data["prompt_emb"] = {}
if self.captions is not None:
data["prompt"] = [caps[row] for caps in self.captions]
return data
except Exception as e:
print(
f"ERROR WHEN LOADING (precomputed) sample={sample_index} source_row={row}: {e}"
)
if self.debug_no_retry:
raise
if self.use_group_labels:
index = self.grouping_sample_indices[
random.randrange(len(self.grouping_sample_indices))
]
else:
index = random.randrange(self.n_rows)
raise RuntimeError("PrecomputedLatentDataset: 连续加载失败")
_CAMERA_PLAYER_PART = re.compile("(?:^|/)player=(\\d+)(?:/|$)")