Add encoder-only Ego4D comparison workflow

This commit is contained in:
Xin Wang
2026-08-28 09:36:35 +08:00
parent 18e8773685
commit ee5e735fc2
6 changed files with 608 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
"""Pack Ego4D local Qwen encoder vectors as standalone head inputs on CPU.
No model, raw media, Thinker states or symlink index is loaded. All manifest
train/test sidecars are validated before creating a fresh output directory.
"""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import tempfile
import numpy as np
import torch
from tqdm.auto import tqdm
from easycom_head import load_ego4d_manifest_split, resolve_feature_path
from local_turn_features import MODALITIES, load_local_features
DEFAULT_DATA_ROOT = Path("/data1/yfzx/wangx/Datasets/ego4d")
PACKING = "native-vectors-plus-validity-bits"
def encoder_only_mode(modalities):
if modalities not in MODALITIES:
raise ValueError(f"Unknown encoder modalities: {modalities}")
return f"encoder-only-{modalities}"
def parse_args(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--data-root", type=Path, default=DEFAULT_DATA_ROOT)
parser.add_argument("--manifest", type=Path)
parser.add_argument("--local-feature-root", type=Path)
parser.add_argument("--target-root", type=Path)
parser.add_argument("--output-root", type=Path)
parser.add_argument("--modalities", choices=tuple(MODALITIES), default="av")
parser.add_argument("--dry-run", action="store_true", help="Validate all inputs; no output files or directory")
parser.add_argument("--no-progress", action="store_true")
args = parser.parse_args(argv)
args.manifest = args.manifest or args.data_root / "ego4d_egospeak.tsv"
args.local_feature_root = args.local_feature_root or args.data_root / "omni_features/local_qwen_1s"
args.target_root = args.target_root or args.data_root / "ego4d_target_frame"
args.output_root = args.output_root or args.data_root / f"omni_features/encoder_only_{args.modalities}_1s"
for key in ("data_root", "manifest", "local_feature_root", "target_root", "output_root"):
setattr(args, key, getattr(args, key).expanduser().resolve())
if args.output_root == args.local_feature_root:
parser.error("Encoder-only output must be separate from local source features")
return args
def pack_sample(path, target_path, sample_id, modalities, expected_contract=None):
"""Read only selected encoder vectors; even a sidecar's reference path is unused."""
source = torch.load(path, map_location="cpu", weights_only=False)
if not isinstance(source, dict):
raise ValueError(f"Invalid local sidecar: {path}")
anchor = MODALITIES[modalities][0]
reference = {
"states": source.get(anchor), "times": source.get("times"),
"label_indices": source.get("label_indices"), "start": 0.,
}
states, contract = load_local_features(
path, reference, sample_id=sample_id, dataset="ego4d", modalities=modalities,
expected_contract=expected_contract,
)
target = np.load(target_path, allow_pickle=False, mmap_mode="r")
if target.shape != (len(states), 3):
raise ValueError(f"{sample_id}: feature/target mismatch: {tuple(states.shape)} vs {target.shape}")
if len(states) <= 10:
raise ValueError(f"{sample_id}: no examples with all ten future horizons")
states = states.to(torch.bfloat16)
if not torch.isfinite(states).all():
raise ValueError(f"{sample_id}: encoder vectors overflow bfloat16 storage")
return {
"format": "qwen-encoder-only-v1", "dataset": "ego4d", "sample_id": sample_id,
"states": states, "times": source["times"], "label_indices": source["label_indices"],
"mode": encoder_only_mode(modalities), "start": 0., "end": len(states) / 5,
"num_steps": len(states), "source_local_features": str(path),
"config": {
"input_source": "qwen-encoders-only", "uses_thinker_states": False,
"packing": PACKING, "encoder_contract": contract,
},
}, contract
def main(argv=None):
args = parse_args(argv)
names = load_ego4d_manifest_split(args.manifest, "all")
if args.output_root.exists() and not args.dry_run:
raise FileExistsError(f"Refusing to overwrite {args.output_root}; use a fresh output directory")
paths = {name: resolve_feature_path(args.local_feature_root, name, dataset="ego4d") for name in names}
targets = {name: args.target_root / f"{name}.npy" for name in names}
for path in targets.values():
if not path.is_file():
raise FileNotFoundError(path)
contract, total, width = None, 0, None
# Two passes keep memory bounded and prevent bad/partial test inputs from
# leaving an apparently ready export. No full-dataset tensors are retained.
for name in tqdm(names, desc="encoder-only preflight", unit="clip", disable=args.no_progress):
packed, contract = pack_sample(paths[name], targets[name], name, args.modalities, contract)
total += len(packed["states"])
width = packed["states"].shape[-1]
del packed
print(f"Validated {len(names)} clips / {total} states; {encoder_only_mode(args.modalities)}; "
f"input width={width}; no Thinker features", flush=True)
if args.dry_run:
return
args.output_root.mkdir(parents=True, exist_ok=False)
rows = {}
for name in tqdm(names, desc="pack encoder-only", unit="clip", disable=args.no_progress):
packed, _ = pack_sample(paths[name], targets[name], name, args.modalities, contract)
destination = args.output_root / f"{name}.pt"
with tempfile.NamedTemporaryFile(dir=args.output_root, prefix=f".{name}.", suffix=".tmp", delete=False) as handle:
temporary = Path(handle.name)
try:
torch.save(packed, temporary)
os.replace(temporary, destination)
finally:
temporary.unlink(missing_ok=True)
rows[name] = len(packed["states"])
del packed
(args.output_root / "export_manifest.json").write_text(json.dumps({
"manifest": str(args.manifest), "mode": encoder_only_mode(args.modalities),
"local_feature_root": str(args.local_feature_root), "target_root": str(args.target_root),
"encoder_contract": contract, "input_width": width, "num_states": total, "clips": rows,
}, indent=2) + "\n")
# Written last. The runner also validates every train/test input instead of
# trusting this marker alone. Compatible with the original inline command.
(args.output_root / "complete.txt").write_text(f"{len(names)} clips\n")
print(f"Complete: {args.output_root}", flush=True)
if __name__ == "__main__":
main()