427 lines
15 KiB
Python
427 lines
15 KiB
Python
#!/usr/bin/env python3
|
|||
|
|
"""Cache exact Qwen2.5-Omni AV pieces for Thinker LoRA training.
|
||
|
|
|
||
|
|
For every source-aligned 2-second block this script caches ten causal phases:
|
||
|
|
0.2, 0.4, ..., 2.0 seconds. The 2.0-second phase is reused as completed
|
||
|
|
history; phases 0.2--1.8 are used only for the current partial block. No
|
||
|
|
future samples enter a phase.
|
||
|
|
|
||
|
|
The optional verification compares a full-sequence reconstruction from the
|
||
|
|
cache with the existing open-stream KV replay. Run this 24-second pilot before
|
||
|
|
caching a full EasyCom session.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import math
|
||
|
|
import platform
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Any, Sequence
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
import torch
|
||
|
|
import torch.nn.functional as F
|
||
|
|
from tqdm.auto import tqdm
|
||
|
|
|
||
|
|
import streaming_turn_model as core
|
||
|
|
from easycom_head import NUM_HORIZONS
|
||
|
|
from omni_cached_av import (
|
||
|
|
SCHEMA_VERSION,
|
||
|
|
CachedBlockStore,
|
||
|
|
anchor_intervals,
|
||
|
|
cache_block_filename,
|
||
|
|
cache_size_bytes,
|
||
|
|
forward_last_state,
|
||
|
|
make_anchor,
|
||
|
|
pieces_to_cache,
|
||
|
|
reconstruct_open_stream_sequence,
|
||
|
|
save_json,
|
||
|
|
)
|
||
|
|
from streaming_turn_model_directly import CausalSourceAV, _easycom_directory_specs
|
||
|
|
|
||
|
|
|
||
|
|
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
||
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
||
|
|
parser.add_argument("--model-id", required=True)
|
||
|
|
parser.add_argument("--easycom-session-dir", type=Path, required=True)
|
||
|
|
parser.add_argument("--target", type=Path, required=True)
|
||
|
|
parser.add_argument("--output-dir", type=Path, required=True)
|
||
|
|
parser.add_argument(
|
||
|
|
"--end",
|
||
|
|
type=float,
|
||
|
|
default=24.0,
|
||
|
|
help="Dense session endpoint in seconds; use 24 for the first pilot",
|
||
|
|
)
|
||
|
|
parser.add_argument("--step-seconds", type=float, default=0.2)
|
||
|
|
parser.add_argument("--native-block-seconds", type=float, default=2.0)
|
||
|
|
parser.add_argument("--memory-seconds", type=float, default=8.0)
|
||
|
|
parser.add_argument("--video-fps", type=float, default=2.0)
|
||
|
|
parser.add_argument("--video-min-pixels", type=int, default=64 * 28 * 28)
|
||
|
|
parser.add_argument("--video-max-pixels", type=int, default=128 * 28 * 28)
|
||
|
|
parser.add_argument("--frame-cache-size", type=int, default=64)
|
||
|
|
parser.add_argument("--decoder", choices=("decord", "pynvcodec"), default="decord")
|
||
|
|
parser.add_argument("--decoder-gpu-id", type=int, default=0)
|
||
|
|
parser.add_argument("--decoder-threads", type=int, default=1)
|
||
|
|
parser.add_argument("--max-open-readers", type=int, default=1)
|
||
|
|
parser.add_argument(
|
||
|
|
"--dtype",
|
||
|
|
choices=("auto", "bfloat16", "float16", "float32"),
|
||
|
|
default="bfloat16",
|
||
|
|
)
|
||
|
|
parser.add_argument("--device", default="cuda:0")
|
||
|
|
parser.add_argument("--flash-attention-2", action="store_true")
|
||
|
|
parser.add_argument(
|
||
|
|
"--verify-every",
|
||
|
|
type=int,
|
||
|
|
default=10,
|
||
|
|
help="Compare every Nth 5-Hz state with exact KV replay; 0 disables",
|
||
|
|
)
|
||
|
|
parser.add_argument(
|
||
|
|
"--minimum-cosine",
|
||
|
|
type=float,
|
||
|
|
default=0.999,
|
||
|
|
help="Fail if cached full-forward/reference KV cosine is below this",
|
||
|
|
)
|
||
|
|
parser.add_argument(
|
||
|
|
"--overwrite",
|
||
|
|
action="store_true",
|
||
|
|
help="Overwrite this cache's known block/static/manifest files",
|
||
|
|
)
|
||
|
|
return parser.parse_args(argv)
|
||
|
|
|
||
|
|
|
||
|
|
def _integer_ratio(numerator: float, denominator: float, name: str) -> int:
|
||
|
|
ratio = numerator / denominator
|
||
|
|
rounded = round(ratio)
|
||
|
|
if not math.isclose(ratio, rounded, rel_tol=0.0, abs_tol=1e-6):
|
||
|
|
raise ValueError(f"{name} must be integral, got {ratio}")
|
||
|
|
return int(rounded)
|
||
|
|
|
||
|
|
|
||
|
|
def _load_targets(path: Path) -> np.ndarray:
|
||
|
|
targets = np.load(path, mmap_mode="r")
|
||
|
|
if targets.ndim != 2 or targets.shape[1] != 3:
|
||
|
|
raise ValueError(f"Expected target [T,3], got {targets.shape}: {path}")
|
||
|
|
return targets
|
||
|
|
|
||
|
|
|
||
|
|
def _safe_output_path(path: Path, overwrite: bool) -> None:
|
||
|
|
if path.exists() and not overwrite:
|
||
|
|
raise FileExistsError(f"Refusing to overwrite {path}; pass --overwrite")
|
||
|
|
|
||
|
|
|
||
|
|
def _model_device(model: torch.nn.Module) -> torch.device:
|
||
|
|
try:
|
||
|
|
return next(model.parameters()).device
|
||
|
|
except StopIteration as exc:
|
||
|
|
raise RuntimeError("Model has no parameters") from exc
|
||
|
|
|
||
|
|
|
||
|
|
@torch.inference_mode()
|
||
|
|
def _cache_blocks(
|
||
|
|
*,
|
||
|
|
extractor: core.OpenStreamReplayExtractor,
|
||
|
|
model: torch.nn.Module,
|
||
|
|
output_dir: Path,
|
||
|
|
num_steps: int,
|
||
|
|
steps_per_block: int,
|
||
|
|
step_seconds: float,
|
||
|
|
block_seconds: float,
|
||
|
|
overwrite: bool,
|
||
|
|
) -> list[dict[str, Any]]:
|
||
|
|
num_blocks = math.ceil(num_steps / steps_per_block)
|
||
|
|
records: list[dict[str, Any]] = []
|
||
|
|
input_embeddings = model.get_input_embeddings()
|
||
|
|
for block_index in tqdm(range(num_blocks), desc="cache AV blocks", unit="block"):
|
||
|
|
first_step = block_index * steps_per_block
|
||
|
|
phase_count = min(steps_per_block, num_steps - first_step)
|
||
|
|
block_start = block_index * block_seconds
|
||
|
|
variants: dict[str, dict[str, torch.Tensor]] = {}
|
||
|
|
token_lengths: dict[str, int] = {}
|
||
|
|
for tail_steps in range(1, phase_count + 1):
|
||
|
|
interval_end = block_start + tail_steps * step_seconds
|
||
|
|
pieces = extractor._prepare_pieces(
|
||
|
|
video_path=None,
|
||
|
|
audio_path=None,
|
||
|
|
interval=(block_start, interval_end),
|
||
|
|
use_audio_in_video=True,
|
||
|
|
)
|
||
|
|
closure_embeddings = input_embeddings(pieces.closure_ids)
|
||
|
|
cached = pieces_to_cache(
|
||
|
|
pieces,
|
||
|
|
closure_embeddings=closure_embeddings,
|
||
|
|
floating_dtype=torch.bfloat16,
|
||
|
|
)
|
||
|
|
variants[str(tail_steps)] = cached
|
||
|
|
token_lengths[str(tail_steps)] = int(
|
||
|
|
cached["prefix_embeddings"].shape[1]
|
||
|
|
+ cached["payload_embeddings"].shape[1]
|
||
|
|
+ cached["closure_embeddings"].shape[1]
|
||
|
|
)
|
||
|
|
path = output_dir / cache_block_filename(block_index)
|
||
|
|
_safe_output_path(path, overwrite)
|
||
|
|
torch.save(
|
||
|
|
{
|
||
|
|
"schema_version": SCHEMA_VERSION,
|
||
|
|
"block_index": block_index,
|
||
|
|
"start_seconds": block_start,
|
||
|
|
"end_seconds": min(
|
||
|
|
block_start + block_seconds, num_steps * step_seconds
|
||
|
|
),
|
||
|
|
"variants": variants,
|
||
|
|
},
|
||
|
|
path,
|
||
|
|
)
|
||
|
|
records.append(
|
||
|
|
{
|
||
|
|
"block_index": block_index,
|
||
|
|
"path": path.name,
|
||
|
|
"phase_count": phase_count,
|
||
|
|
"token_lengths_without_query": token_lengths,
|
||
|
|
"size_bytes": path.stat().st_size,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
return records
|
||
|
|
|
||
|
|
|
||
|
|
@torch.inference_mode()
|
||
|
|
def _verify_cache(
|
||
|
|
*,
|
||
|
|
model: torch.nn.Module,
|
||
|
|
extractor: core.OpenStreamReplayExtractor,
|
||
|
|
cache_dir: Path,
|
||
|
|
static: dict[str, Any],
|
||
|
|
anchors: list[dict[str, Any]],
|
||
|
|
verify_every: int,
|
||
|
|
minimum_cosine: float,
|
||
|
|
block_seconds: float,
|
||
|
|
step_seconds: float,
|
||
|
|
) -> list[dict[str, float]]:
|
||
|
|
if verify_every <= 0:
|
||
|
|
return []
|
||
|
|
selected = [
|
||
|
|
anchor
|
||
|
|
for anchor in anchors
|
||
|
|
if (int(anchor["step_index"]) + 1) % verify_every == 0
|
||
|
|
]
|
||
|
|
if anchors and anchors[-1] not in selected:
|
||
|
|
selected.append(anchors[-1])
|
||
|
|
store = CachedBlockStore(cache_dir, max_blocks=max(2, int(static["memory_blocks"])))
|
||
|
|
device = _model_device(model)
|
||
|
|
checks: list[dict[str, float]] = []
|
||
|
|
for anchor in tqdm(selected, desc="verify cached replay", unit="state"):
|
||
|
|
embeddings, positions = reconstruct_open_stream_sequence(
|
||
|
|
store=store, static=static, anchor=anchor
|
||
|
|
)
|
||
|
|
embeddings = embeddings.unsqueeze(0).to(device=device, dtype=model.dtype)
|
||
|
|
positions = positions.unsqueeze(1).to(device=device)
|
||
|
|
attention_mask = torch.ones(
|
||
|
|
1, embeddings.shape[1], dtype=torch.long, device=device
|
||
|
|
)
|
||
|
|
cached_state = forward_last_state(
|
||
|
|
model.model,
|
||
|
|
inputs_embeds=embeddings,
|
||
|
|
position_ids=positions,
|
||
|
|
attention_mask=attention_mask,
|
||
|
|
readout_indices=torch.tensor(
|
||
|
|
[embeddings.shape[1] - 1], dtype=torch.long, device=device
|
||
|
|
),
|
||
|
|
)
|
||
|
|
exact_state = extractor.state_from_scratch(
|
||
|
|
video_path=None,
|
||
|
|
audio_path=None,
|
||
|
|
intervals=anchor_intervals(
|
||
|
|
anchor,
|
||
|
|
block_seconds=block_seconds,
|
||
|
|
step_seconds=step_seconds,
|
||
|
|
),
|
||
|
|
use_audio_in_video=True,
|
||
|
|
)
|
||
|
|
cached_float = cached_state.float()
|
||
|
|
exact_float = exact_state.float()
|
||
|
|
check = {
|
||
|
|
"time_seconds": float(anchor["time_seconds"]),
|
||
|
|
"cosine": float(
|
||
|
|
F.cosine_similarity(cached_float, exact_float, dim=-1).item()
|
||
|
|
),
|
||
|
|
"mean_abs": float((cached_float - exact_float).abs().mean().item()),
|
||
|
|
"max_abs": float((cached_float - exact_float).abs().max().item()),
|
||
|
|
}
|
||
|
|
checks.append(check)
|
||
|
|
print(
|
||
|
|
f"verify t={check['time_seconds']:.1f}s "
|
||
|
|
f"cosine={check['cosine']:.8f} "
|
||
|
|
f"mean_abs={check['mean_abs']:.6f} "
|
||
|
|
f"max_abs={check['max_abs']:.6f}",
|
||
|
|
flush=True,
|
||
|
|
)
|
||
|
|
if check["cosine"] < minimum_cosine:
|
||
|
|
raise RuntimeError(
|
||
|
|
"Cached full-sequence reconstruction disagrees with open-stream "
|
||
|
|
f"KV replay at t={check['time_seconds']:.1f}s: "
|
||
|
|
f"cosine={check['cosine']:.8f} < {minimum_cosine:.8f}"
|
||
|
|
)
|
||
|
|
return checks
|
||
|
|
|
||
|
|
|
||
|
|
def main(argv: Sequence[str] | None = None) -> None:
|
||
|
|
args = parse_args(argv)
|
||
|
|
core._configure_qwen_logging()
|
||
|
|
if args.end <= 0:
|
||
|
|
raise ValueError("--end must be positive")
|
||
|
|
steps_per_block = _integer_ratio(
|
||
|
|
args.native_block_seconds, args.step_seconds, "native block / step"
|
||
|
|
)
|
||
|
|
memory_blocks = _integer_ratio(
|
||
|
|
args.memory_seconds, args.native_block_seconds, "memory / native block"
|
||
|
|
)
|
||
|
|
num_steps = _integer_ratio(args.end, args.step_seconds, "end / step")
|
||
|
|
targets = _load_targets(args.target)
|
||
|
|
if num_steps > len(targets):
|
||
|
|
raise ValueError(
|
||
|
|
f"Requested {num_steps} cached steps but target has only {len(targets)}"
|
||
|
|
)
|
||
|
|
|
||
|
|
output_dir = args.output_dir.expanduser().resolve()
|
||
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||
|
|
manifest_path = output_dir / "manifest.json"
|
||
|
|
static_path = output_dir / "static.pt"
|
||
|
|
_safe_output_path(manifest_path, args.overwrite)
|
||
|
|
_safe_output_path(static_path, args.overwrite)
|
||
|
|
|
||
|
|
config = core.StreamConfig(
|
||
|
|
step_seconds=args.step_seconds,
|
||
|
|
native_block_seconds=args.native_block_seconds,
|
||
|
|
memory_seconds=args.memory_seconds,
|
||
|
|
video_fps=args.video_fps,
|
||
|
|
video_min_pixels=args.video_min_pixels,
|
||
|
|
video_max_pixels=args.video_max_pixels,
|
||
|
|
)
|
||
|
|
config.validate()
|
||
|
|
specs = _easycom_directory_specs(args.easycom_session_dir)
|
||
|
|
media = CausalSourceAV(
|
||
|
|
specs,
|
||
|
|
output_fps=args.video_fps,
|
||
|
|
required_duration=args.end,
|
||
|
|
frame_cache_size=args.frame_cache_size,
|
||
|
|
decoder_backend=args.decoder,
|
||
|
|
decoder_gpu_id=args.decoder_gpu_id,
|
||
|
|
decoder_threads=args.decoder_threads,
|
||
|
|
max_open_readers=args.max_open_readers,
|
||
|
|
)
|
||
|
|
print(
|
||
|
|
f"Direct media ready: duration={args.end:.1f}s, "
|
||
|
|
f"grid={media.expected_grid_frames} frames, audio={len(media.audio)} samples"
|
||
|
|
)
|
||
|
|
|
||
|
|
model, processor = core.load_qwen_omni(
|
||
|
|
args.model_id,
|
||
|
|
dtype=args.dtype,
|
||
|
|
device_map=args.device,
|
||
|
|
use_flash_attention_2=args.flash_attention_2,
|
||
|
|
)
|
||
|
|
model.requires_grad_(False)
|
||
|
|
extractor = core.OpenStreamReplayExtractor(
|
||
|
|
model, processor, config, fixed_grid_av=media
|
||
|
|
)
|
||
|
|
with torch.inference_mode():
|
||
|
|
query_ids = extractor.query_ids
|
||
|
|
query_embeddings = model.get_input_embeddings()(query_ids)
|
||
|
|
positions_per_chunk = core._tmrope_positions_per_chunk(model)
|
||
|
|
static = {
|
||
|
|
"schema_version": SCHEMA_VERSION,
|
||
|
|
"steps_per_block": steps_per_block,
|
||
|
|
"memory_blocks": memory_blocks,
|
||
|
|
"positions_per_chunk": positions_per_chunk,
|
||
|
|
"query_ids": query_ids.detach().cpu().long(),
|
||
|
|
"query_embeddings": query_embeddings.detach().cpu().to(torch.bfloat16),
|
||
|
|
}
|
||
|
|
torch.save(static, static_path)
|
||
|
|
|
||
|
|
block_records = _cache_blocks(
|
||
|
|
extractor=extractor,
|
||
|
|
model=model,
|
||
|
|
output_dir=output_dir,
|
||
|
|
num_steps=num_steps,
|
||
|
|
steps_per_block=steps_per_block,
|
||
|
|
step_seconds=args.step_seconds,
|
||
|
|
block_seconds=args.native_block_seconds,
|
||
|
|
overwrite=args.overwrite,
|
||
|
|
)
|
||
|
|
anchors = [
|
||
|
|
make_anchor(
|
||
|
|
step_index=step_index,
|
||
|
|
steps_per_block=steps_per_block,
|
||
|
|
memory_blocks=memory_blocks,
|
||
|
|
step_seconds=args.step_seconds,
|
||
|
|
num_target_steps=len(targets),
|
||
|
|
num_horizons=NUM_HORIZONS,
|
||
|
|
)
|
||
|
|
for step_index in range(num_steps)
|
||
|
|
]
|
||
|
|
checks = _verify_cache(
|
||
|
|
model=model,
|
||
|
|
extractor=extractor,
|
||
|
|
cache_dir=output_dir,
|
||
|
|
static=static,
|
||
|
|
anchors=anchors,
|
||
|
|
verify_every=args.verify_every,
|
||
|
|
minimum_cosine=args.minimum_cosine,
|
||
|
|
block_seconds=args.native_block_seconds,
|
||
|
|
step_seconds=args.step_seconds,
|
||
|
|
)
|
||
|
|
|
||
|
|
manifest = {
|
||
|
|
"schema_version": SCHEMA_VERSION,
|
||
|
|
"cache_kind": "qwen2.5-omni-frozen-av-open-stream-phases",
|
||
|
|
"session": args.easycom_session_dir.resolve().name,
|
||
|
|
"model_id": args.model_id,
|
||
|
|
"target_path": str(args.target.expanduser().resolve()),
|
||
|
|
"target_steps": int(len(targets)),
|
||
|
|
"cached_steps": num_steps,
|
||
|
|
"cached_end_seconds": args.end,
|
||
|
|
"hidden_size": int(query_embeddings.shape[-1]),
|
||
|
|
"floating_dtype": "bfloat16",
|
||
|
|
"step_seconds": args.step_seconds,
|
||
|
|
"native_block_seconds": args.native_block_seconds,
|
||
|
|
"memory_seconds": args.memory_seconds,
|
||
|
|
"steps_per_block": steps_per_block,
|
||
|
|
"memory_blocks": memory_blocks,
|
||
|
|
"positions_per_chunk": positions_per_chunk,
|
||
|
|
"video_fps": args.video_fps,
|
||
|
|
"video_min_pixels": args.video_min_pixels,
|
||
|
|
"video_max_pixels": args.video_max_pixels,
|
||
|
|
"use_audio_in_video": True,
|
||
|
|
"interval_convention": "half_open_[start,end)",
|
||
|
|
"phase_policy": "cache_0.2_to_2.0s;_2.0s_is_completed_history",
|
||
|
|
"label_policy": "target[i+1:i+11]",
|
||
|
|
"static_path": static_path.name,
|
||
|
|
"blocks": block_records,
|
||
|
|
"anchors": anchors,
|
||
|
|
"verification": checks,
|
||
|
|
"media": media.metadata(),
|
||
|
|
"software": {
|
||
|
|
"python": platform.python_version(),
|
||
|
|
"torch": torch.__version__,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
save_json(manifest_path, manifest)
|
||
|
|
size = cache_size_bytes(
|
||
|
|
[static_path, *[output_dir / record["path"] for record in block_records]]
|
||
|
|
)
|
||
|
|
print(
|
||
|
|
f"Saved {len(block_records)} blocks / {len(anchors)} anchors to {output_dir}"
|
||
|
|
)
|
||
|
|
print(f"Cache tensor size: {size / (1024 ** 3):.3f} GiB")
|
||
|
|
if checks:
|
||
|
|
print(
|
||
|
|
"Verification: "
|
||
|
|
f"lowest cosine={min(item['cosine'] for item in checks):.8f}, "
|
||
|
|
f"worst max_abs={max(item['max_abs'] for item in checks):.6f}"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|