Files
icassp2027-experiment/omni_cached_av.py
T

373 lines
14 KiB
Python

"""Cached open-stream AV pieces for Qwen2.5-Omni turn-taking LoRA.
The modality towers are frozen for the first fine-tuning experiment. This
module serializes their exact output embeddings and Qwen's local TMRoPE
coordinates, then reconstructs the same still-open media turn without using a
KV cache. The resulting sequence can be passed through the Thinker with
``use_cache=False`` so gradients reach LoRA adapters in every retained block.
"""
from __future__ import annotations
import json
import math
from collections import OrderedDict
from pathlib import Path
from typing import Any, Iterable, Sequence
import torch
SCHEMA_VERSION = 1
PIECE_TENSOR_NAMES = (
"prefix_embeddings",
"prefix_position_ids",
"payload_embeddings",
"payload_position_ids",
"closure_ids",
"closure_position_ids",
"closure_embeddings",
)
def load_json(path: str | Path) -> dict[str, Any]:
return json.loads(Path(path).read_text())
def save_json(path: str | Path, payload: dict[str, Any]) -> None:
Path(path).write_text(json.dumps(payload, indent=2) + "\n")
def torch_load(path: str | Path) -> Any:
"""Load tensor-only cache files across old and new PyTorch defaults."""
try:
return torch.load(Path(path), map_location="cpu", weights_only=True)
except TypeError:
return torch.load(Path(path), map_location="cpu")
def cache_block_filename(block_index: int) -> str:
return f"block_{block_index:06d}.pt"
def pieces_to_cache(
pieces: Any,
*,
closure_embeddings: torch.Tensor,
floating_dtype: torch.dtype = torch.bfloat16,
) -> dict[str, torch.Tensor]:
"""Detach one ``OpenMediaPieces`` object into a portable CPU payload."""
output: dict[str, torch.Tensor] = {}
for name in PIECE_TENSOR_NAMES:
value = closure_embeddings if name == "closure_embeddings" else getattr(pieces, name)
value = value.detach().cpu().contiguous()
if value.is_floating_point():
value = value.to(floating_dtype)
output[name] = value
validate_piece(output)
return output
def validate_piece(piece: dict[str, torch.Tensor]) -> None:
missing = [name for name in PIECE_TENSOR_NAMES if name not in piece]
if missing:
raise ValueError(f"Cached AV piece is missing {missing}")
prefix = piece["prefix_embeddings"]
payload = piece["payload_embeddings"]
closure = piece["closure_embeddings"]
if prefix.ndim != 3 or prefix.shape[0] != 1:
raise ValueError(f"Invalid prefix embeddings: {tuple(prefix.shape)}")
if payload.ndim != 3 or payload.shape[0] != 1:
raise ValueError(f"Invalid payload embeddings: {tuple(payload.shape)}")
if closure.ndim != 3 or closure.shape[0] != 1:
raise ValueError(f"Invalid closure embeddings: {tuple(closure.shape)}")
hidden_sizes = {int(prefix.shape[-1]), int(payload.shape[-1]), int(closure.shape[-1])}
if len(hidden_sizes) != 1:
raise ValueError(f"Cached piece hidden sizes disagree: {sorted(hidden_sizes)}")
expected_positions = (
("prefix_position_ids", prefix.shape[1]),
("payload_position_ids", payload.shape[1]),
("closure_position_ids", closure.shape[1]),
)
for name, length in expected_positions:
value = piece[name]
if value.shape != (3, 1, length):
raise ValueError(
f"{name} must have shape {(3, 1, length)}, got {tuple(value.shape)}"
)
if piece["closure_ids"].shape != (1, closure.shape[1]):
raise ValueError(
"closure_ids/closure_embeddings mismatch: "
f"{tuple(piece['closure_ids'].shape)} vs {tuple(closure.shape)}"
)
def make_anchor(
*,
step_index: int,
steps_per_block: int,
memory_blocks: int,
step_seconds: float,
num_target_steps: int,
num_horizons: int,
) -> dict[str, Any]:
"""Describe the exact completed-history/current-tail selection at time t."""
if step_index < 0:
raise ValueError("step_index must be non-negative")
block_index = step_index // steps_per_block
tail_steps = step_index % steps_per_block + 1
first_history = max(0, block_index - (memory_blocks - 1))
history_blocks = list(range(first_history, block_index))
return {
"step_index": int(step_index),
"time_seconds": float((step_index + 1) * step_seconds),
"block_index": int(block_index),
"tail_steps": int(tail_steps),
"history_blocks": history_blocks,
"target_index": int(step_index),
"has_future_target": bool(step_index + num_horizons < num_target_steps),
}
class CachedBlockStore:
"""Small CPU LRU for block shard files shared by adjacent samples."""
def __init__(self, cache_dir: str | Path, max_blocks: int = 16) -> None:
if max_blocks <= 0:
raise ValueError("max_blocks must be positive")
self.cache_dir = Path(cache_dir)
self.max_blocks = int(max_blocks)
self._blocks: OrderedDict[int, dict[str, Any]] = OrderedDict()
def get(self, block_index: int) -> dict[str, Any]:
cached = self._blocks.get(block_index)
if cached is not None:
self._blocks.move_to_end(block_index)
return cached
path = self.cache_dir / cache_block_filename(block_index)
if not path.is_file():
raise FileNotFoundError(path)
payload = torch_load(path)
if int(payload.get("block_index", -1)) != block_index:
raise ValueError(f"Block index mismatch in {path}")
variants = payload.get("variants")
if not isinstance(variants, dict) or not variants:
raise ValueError(f"No variants in {path}")
self._blocks[block_index] = payload
self._blocks.move_to_end(block_index)
while len(self._blocks) > self.max_blocks:
self._blocks.popitem(last=False)
return payload
def variant(self, block_index: int, tail_steps: int) -> dict[str, torch.Tensor]:
block = self.get(block_index)
variants = block["variants"]
piece = variants.get(str(tail_steps), variants.get(tail_steps))
if piece is None:
raise KeyError(
f"Block {block_index} has no tail_steps={tail_steps}; "
f"available={sorted(str(key) for key in variants)}"
)
validate_piece(piece)
return piece
def _strip_embedding_batch(value: torch.Tensor) -> torch.Tensor:
if value.ndim != 3 or value.shape[0] != 1:
raise ValueError(f"Expected [1,L,D], got {tuple(value.shape)}")
return value[0]
def _strip_position_batch(value: torch.Tensor) -> torch.LongTensor:
if value.ndim != 3 or value.shape[:2] != (3, 1):
raise ValueError(f"Expected [3,1,L], got {tuple(value.shape)}")
return value[:, 0, :].long()
def reconstruct_open_stream_sequence(
*,
store: CachedBlockStore,
static: dict[str, Any],
anchor: dict[str, Any],
current_piece: dict[str, torch.Tensor] | None = None,
) -> tuple[torch.Tensor, torch.LongTensor]:
"""Materialize one differentiable Thinker input from frozen AV embeddings.
Complete history blocks use their 2.0-second variant. Only the current
block uses its 0.2--2.0-second phase. The first retained block keeps the
chat/media prefix; later blocks append payload only. The final block's
media closure and the artificial assistant readout query are appended once.
"""
steps_per_block = int(static["steps_per_block"])
positions_per_chunk = int(static["positions_per_chunk"])
history_blocks = [int(value) for value in anchor["history_blocks"]]
current_block = int(anchor["block_index"])
selected: list[tuple[int, int]] = [
(block_index, steps_per_block) for block_index in history_blocks
]
selected.append((current_block, int(anchor["tail_steps"])))
if not selected:
raise ValueError("An anchor must select at least its current block")
embedding_parts: list[torch.Tensor] = []
position_parts: list[torch.LongTensor] = []
first_payload_base: int | None = None
last_piece: dict[str, torch.Tensor] | None = None
last_shift = 0
for sequence_index, (block_index, tail_steps) in enumerate(selected):
is_current = sequence_index == len(selected) - 1
piece = (
current_piece
if is_current and current_piece is not None
else store.variant(block_index, tail_steps)
)
validate_piece(piece)
local_payload_positions = _strip_position_batch(piece["payload_position_ids"])
local_payload_base = int(local_payload_positions.min().item())
if first_payload_base is None:
first_payload_base = local_payload_base
desired_payload_base = first_payload_base + sequence_index * positions_per_chunk
position_shift = desired_payload_base - local_payload_base
if sequence_index == 0:
embedding_parts.append(_strip_embedding_batch(piece["prefix_embeddings"]))
position_parts.append(_strip_position_batch(piece["prefix_position_ids"]))
embedding_parts.append(_strip_embedding_batch(piece["payload_embeddings"]))
position_parts.append(local_payload_positions + position_shift)
last_piece = piece
last_shift = position_shift
if last_piece is None:
raise RuntimeError("No cached AV piece was selected")
closure_embeddings = _strip_embedding_batch(last_piece["closure_embeddings"])
closure_positions = (
_strip_position_batch(last_piece["closure_position_ids"]) + last_shift
)
embedding_parts.append(closure_embeddings)
position_parts.append(closure_positions)
query_embeddings = static["query_embeddings"]
if query_embeddings.ndim != 3 or query_embeddings.shape[0] != 1:
raise ValueError(
f"static query_embeddings must be [1,Q,D], got {tuple(query_embeddings.shape)}"
)
query_embeddings = query_embeddings[0]
query_start = int(closure_positions.max().item()) + 1
query_scalar = torch.arange(
query_start,
query_start + query_embeddings.shape[0],
dtype=torch.long,
)
query_positions = query_scalar.view(1, -1).expand(3, -1)
embedding_parts.append(query_embeddings)
position_parts.append(query_positions)
embeddings = torch.cat(embedding_parts, dim=0).contiguous()
position_ids = torch.cat(position_parts, dim=1).contiguous()
if position_ids.shape != (3, embeddings.shape[0]):
raise RuntimeError(
"Reconstructed position/embedding mismatch: "
f"{tuple(position_ids.shape)} vs {tuple(embeddings.shape)}"
)
return embeddings, position_ids
def collate_cached_sequences(
batch: Sequence[dict[str, Any]],
) -> dict[str, Any]:
"""Right-pad variable token sequences while preserving each readout index."""
if not batch:
raise ValueError("Cannot collate an empty batch")
hidden_size = int(batch[0]["inputs_embeds"].shape[-1])
max_length = max(int(item["inputs_embeds"].shape[0]) for item in batch)
dtype = batch[0]["inputs_embeds"].dtype
inputs_embeds = torch.zeros(
len(batch), max_length, hidden_size, dtype=dtype
)
position_ids = torch.zeros(3, len(batch), max_length, dtype=torch.long)
attention_mask = torch.zeros(len(batch), max_length, dtype=torch.long)
readout_indices = torch.empty(len(batch), dtype=torch.long)
targets = torch.stack([item["targets"] for item in batch])
for batch_index, item in enumerate(batch):
embeddings = item["inputs_embeds"]
positions = item["position_ids"]
length = int(embeddings.shape[0])
if embeddings.shape[-1] != hidden_size:
raise ValueError("Mixed hidden sizes in one batch")
if positions.shape != (3, length):
raise ValueError(
f"Invalid position_ids {tuple(positions.shape)} for length {length}"
)
inputs_embeds[batch_index, :length] = embeddings
position_ids[:, batch_index, :length] = positions
attention_mask[batch_index, :length] = 1
readout_indices[batch_index] = length - 1
return {
"inputs_embeds": inputs_embeds,
"position_ids": position_ids,
"attention_mask": attention_mask,
"readout_indices": readout_indices,
"targets": targets,
"step_indices": torch.tensor(
[int(item["step_index"]) for item in batch], dtype=torch.long
),
}
def anchor_intervals(
anchor: dict[str, Any],
*,
block_seconds: float,
step_seconds: float,
) -> list[tuple[float, float]]:
"""Return source intervals used by the reference KV replay for an anchor."""
intervals = [
(index * block_seconds, (index + 1) * block_seconds)
for index in anchor["history_blocks"]
]
block_index = int(anchor["block_index"])
current_start = block_index * block_seconds
current_end = current_start + int(anchor["tail_steps"]) * step_seconds
intervals.append((current_start, current_end))
return intervals
def forward_last_state(
language_model: torch.nn.Module,
*,
inputs_embeds: torch.Tensor,
position_ids: torch.LongTensor,
attention_mask: torch.LongTensor,
readout_indices: torch.LongTensor,
) -> torch.Tensor:
"""Run full-sequence causal Thinker attention and gather each query state."""
outputs = language_model(
inputs_embeds=inputs_embeds,
position_ids=position_ids,
attention_mask=attention_mask,
use_cache=False,
output_hidden_states=False,
return_dict=True,
cache_position=torch.arange(
inputs_embeds.shape[1], device=inputs_embeds.device
),
)
batch_indices = torch.arange(
inputs_embeds.shape[0], device=inputs_embeds.device
)
return outputs.last_hidden_state[batch_indices, readout_indices]
def cache_size_bytes(paths: Iterable[Path]) -> int:
return sum(path.stat().st_size for path in paths if path.is_file())