300 lines
17 KiB
Python
300 lines
17 KiB
Python
"""CPU cache semantics, output routing and both feature-loader contracts."""
|
|||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import copy
|
||
|
|
import json
|
||
|
|
import tempfile
|
||
|
|
import types
|
||
|
|
import unittest
|
||
|
|
from pathlib import Path
|
||
|
|
from unittest import mock
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
import torch
|
||
|
|
|
||
|
|
import extract_easycom_multiwindow as driver
|
||
|
|
import streaming_turn_model as core
|
||
|
|
import streaming_turn_model_directly as direct
|
||
|
|
from easycom_head import load_session_data
|
||
|
|
from local_turn_features import LOCAL_FORMAT, load_local_features, reference_alignment
|
||
|
|
from streaming_turn_multiwindow import DEFAULT_MEMORY_SECONDS, MultiWindowOpenStreamReplayExtractor, validate_memory_seconds
|
||
|
|
|
||
|
|
|
||
|
|
class FakeModel:
|
||
|
|
config = types.SimpleNamespace(seconds_per_chunk=2.0, position_id_per_seconds=25.0,
|
||
|
|
text_config=types.SimpleNamespace(hidden_size=3))
|
||
|
|
device = torch.device("cpu")
|
||
|
|
dtype = torch.float32
|
||
|
|
|
||
|
|
|
||
|
|
class FakeCache:
|
||
|
|
def __init__(self):
|
||
|
|
self.tokens = []
|
||
|
|
|
||
|
|
|
||
|
|
def append(model, pieces, *, past_key_values, rope_offset, include_prefix):
|
||
|
|
cache = FakeCache() if past_key_values is None else past_key_values
|
||
|
|
if include_prefix != (not cache.tokens):
|
||
|
|
raise AssertionError("prefix ownership mismatch or readout committed")
|
||
|
|
values = pieces.payload_embeddings.flatten().tolist()
|
||
|
|
# Mutation exercises clone_cache isolation, not a conveniently immutable fake.
|
||
|
|
cache.tokens.append((values, rope_offset))
|
||
|
|
return cache, rope_offset + 1, pieces.closure_position_ids
|
||
|
|
|
||
|
|
|
||
|
|
def read(model, cache, *, last_n_layers=1, layer_fusion="mean", **kwargs):
|
||
|
|
value = 0.0
|
||
|
|
for values, offset in cache.tokens:
|
||
|
|
value = value * 1.17 + sum(values) + offset / 100
|
||
|
|
state = torch.tensor([[value, cache.tokens[0][0][0], cache.tokens[-1][0][1]]])
|
||
|
|
if layer_fusion == "stack":
|
||
|
|
return torch.stack([state + layer for layer in range(last_n_layers)], dim=1)
|
||
|
|
return state
|
||
|
|
|
||
|
|
|
||
|
|
class MultiwindowTests(unittest.TestCase):
|
||
|
|
def setUp(self):
|
||
|
|
self.prepared = []
|
||
|
|
self.future_change = False
|
||
|
|
|
||
|
|
def prepare(extractor, *, interval, **kwargs):
|
||
|
|
start, end = interval
|
||
|
|
self.prepared.append(interval)
|
||
|
|
signal = 1000.0 if self.future_change and end > 13.0 else 0.0
|
||
|
|
return core.OpenMediaPieces(
|
||
|
|
prefix_embeddings=torch.zeros(1, 1, 3),
|
||
|
|
prefix_position_ids=torch.zeros(3, 1, 1, dtype=torch.long),
|
||
|
|
payload_embeddings=torch.tensor([[[start, end, signal]]]),
|
||
|
|
payload_position_ids=torch.ones(3, 1, 1, dtype=torch.long),
|
||
|
|
closure_ids=torch.zeros(1, 1, dtype=torch.long),
|
||
|
|
closure_position_ids=torch.ones(3, 1, 1, dtype=torch.long),
|
||
|
|
)
|
||
|
|
|
||
|
|
for patch in (
|
||
|
|
mock.patch.object(core, "_readout_query_ids", return_value=torch.zeros(1, 1, dtype=torch.long)),
|
||
|
|
mock.patch.object(core.OpenStreamReplayExtractor, "_prepare_pieces", new=prepare),
|
||
|
|
mock.patch.object(core, "_append_open_pieces", side_effect=append),
|
||
|
|
mock.patch.object(core, "_read_open_state", side_effect=read),
|
||
|
|
):
|
||
|
|
patch.start()
|
||
|
|
self.addCleanup(patch.stop)
|
||
|
|
self.config = core.StreamConfig(memory_seconds=12, readout_last_n_layers=4, readout_layer_fusion="stack")
|
||
|
|
self.kwargs = dict(video_path="unused.mp4", audio_path=None, start=0.0, end=24.0,
|
||
|
|
use_audio_in_video=True, show_progress=False)
|
||
|
|
|
||
|
|
def multi(self, **kwargs):
|
||
|
|
extractor = MultiWindowOpenStreamReplayExtractor(
|
||
|
|
FakeModel(), object(), self.config, memory_seconds_list=DEFAULT_MEMORY_SECONDS
|
||
|
|
)
|
||
|
|
return extractor.extract(**{**self.kwargs, **kwargs})
|
||
|
|
|
||
|
|
def test_all_caps_match_separate_single_window_extractions_and_encode_once(self):
|
||
|
|
results = self.multi()
|
||
|
|
self.assertEqual(len(self.prepared), 120)
|
||
|
|
self.assertEqual(len(set(self.prepared)), 120)
|
||
|
|
for cap, result in results.items():
|
||
|
|
reference = core.OpenStreamReplayExtractor(
|
||
|
|
FakeModel(), object(), core.StreamConfig(memory_seconds=cap,
|
||
|
|
readout_last_n_layers=4, readout_layer_fusion="stack")
|
||
|
|
).extract(**self.kwargs)
|
||
|
|
self.assertTrue(torch.equal(result.times, reference.times))
|
||
|
|
self.assertTrue(torch.equal(result.states, reference.states), f"mismatch cap={cap}")
|
||
|
|
self.assertEqual(result.states.shape, (120, 4, 3))
|
||
|
|
self.assertEqual(result.diagnostics["production_processor_calls_shared"], 120)
|
||
|
|
self.assertTrue((result.diagnostics["actual_context_seconds"] <= cap + 1e-6).all())
|
||
|
|
self.assertTrue(torch.equal(result.diagnostics["state_source_times"], result.times))
|
||
|
|
self.assertEqual(result.diagnostics["payload_commit_mask"].sum().item(), 12)
|
||
|
|
# Representative physical timeline, independent of any learned scores.
|
||
|
|
index = 102 # cutoff=20.6 s
|
||
|
|
print("Timeline cutoff=20.6s, local A=[19.6,20.6), horizon0.2 target=20.8s")
|
||
|
|
for cap, result in results.items():
|
||
|
|
start = result.diagnostics["history_start_times"][index].item()
|
||
|
|
self.assertAlmostEqual(start, 22.0 - cap)
|
||
|
|
print(f" cap={cap:g}s global=[{start:g},20.6) age=0s")
|
||
|
|
|
||
|
|
def test_independent_replay_verification_and_no_future_backfill(self):
|
||
|
|
original = self.multi(verify_every=11)
|
||
|
|
for result in original.values():
|
||
|
|
self.assertEqual(len(result.verification), 10)
|
||
|
|
self.assertTrue(all(check["max_abs"] == 0 for check in result.verification))
|
||
|
|
self.future_change = True
|
||
|
|
changed = self.multi()
|
||
|
|
for cap in original:
|
||
|
|
self.assertTrue(torch.equal(original[cap].states[:65], changed[cap].states[:65]))
|
||
|
|
self.assertFalse(torch.equal(original[cap].states[65:], changed[cap].states[65:]))
|
||
|
|
|
||
|
|
def test_repeated_extract_resets_history_and_supports_mean(self):
|
||
|
|
extractor = MultiWindowOpenStreamReplayExtractor(
|
||
|
|
FakeModel(), object(), core.StreamConfig(), memory_seconds_list=[2, 4]
|
||
|
|
)
|
||
|
|
first = extractor.extract(**self.kwargs)
|
||
|
|
second = extractor.extract(**self.kwargs)
|
||
|
|
self.assertEqual(first[2].states.shape, (120, 3))
|
||
|
|
self.assertTrue(torch.equal(first[4].states, second[4].states))
|
||
|
|
for kwargs in ({"start": 0.2}, {"end": 0.1}, {"end": float("nan")}, {"verify_every": -1}):
|
||
|
|
with self.assertRaises(ValueError):
|
||
|
|
extractor.extract(**{**self.kwargs, **kwargs})
|
||
|
|
|
||
|
|
def test_invalid_caps(self):
|
||
|
|
for caps in ([], [2, 2], [2, 2.000000001], [0], [1], [3], [float("nan")], [float("inf")]):
|
||
|
|
with self.subTest(caps=caps), self.assertRaises(ValueError):
|
||
|
|
validate_memory_seconds(caps, 2)
|
||
|
|
|
||
|
|
def test_export_loads_both_head_and_same_local_audio_and_rejects_bad_alignment(self):
|
||
|
|
results = self.multi(end=4)
|
||
|
|
n = 20
|
||
|
|
payload = {"mode": "open-stream-replay", "model_id": "synthetic", "start": 0.0,
|
||
|
|
"num_steps": n, "target_path": "synthetic.npy", "config": {"step_seconds": .2},
|
||
|
|
"label_indices": torch.arange(n), "open_stream_policy": {}}
|
||
|
|
with tempfile.TemporaryDirectory() as directory:
|
||
|
|
root = Path(directory)
|
||
|
|
target_root = root / "targets"
|
||
|
|
target_root.mkdir()
|
||
|
|
targets = np.tile(np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1], [0, 1, 1]], dtype=np.float32), (5, 1))
|
||
|
|
np.save(target_root / "Session_4.npy", targets)
|
||
|
|
paths = direct.save_multiwindow_banks(payload, results, root / "Session_4.pt")
|
||
|
|
side = {"format": LOCAL_FORMAT, "dataset": "easycom", "sample_id": "Session_4",
|
||
|
|
"partial": False, "times": results[2].times.clone(), "label_indices": torch.arange(n),
|
||
|
|
"audio": torch.ones(n, 5), "audio_valid": torch.ones(n, dtype=torch.bool),
|
||
|
|
"config": {"audio_dim": 5, "causal_policy": "half-open-trailing-window",
|
||
|
|
"uses_speech_labels": False, "encoder_id": "synthetic"}}
|
||
|
|
side_path = root / "audio.pt"
|
||
|
|
torch.save(side, side_path)
|
||
|
|
for cap, path in paths.items():
|
||
|
|
bank = torch.load(path, weights_only=False)
|
||
|
|
loaded = load_session_data(path.parent, target_root, 4, expected_mode="open-stream-replay")
|
||
|
|
self.assertTrue(torch.equal(loaded.states, results[cap].states))
|
||
|
|
local, _ = load_local_features(side_path, bank, sample_id="Session_4", dataset="easycom", modalities="audio")
|
||
|
|
self.assertEqual(local.shape, (n, 6)) # encoder vector + availability
|
||
|
|
self.assertFalse(bank["persistent_thinker_kv"])
|
||
|
|
self.assertEqual(bank["config"]["memory_seconds"], cap)
|
||
|
|
self.assertFalse(bank["partial_extraction"])
|
||
|
|
for indices in (torch.arange(n) + 1, torch.arange(n - 1)):
|
||
|
|
broken = {**bank, "label_indices": indices}
|
||
|
|
torch.save(broken, path)
|
||
|
|
with self.assertRaises(ValueError):
|
||
|
|
load_session_data(path.parent, target_root, 4, expected_mode="open-stream-replay")
|
||
|
|
with self.assertRaises(ValueError):
|
||
|
|
load_local_features(side_path, broken, sample_id="Session_4", dataset="easycom", modalities="audio")
|
||
|
|
torch.save(bank, path)
|
||
|
|
for broken in ({**bank, "times": bank["times"] + .2}, {**bank, "start": .2}):
|
||
|
|
with self.assertRaises(ValueError):
|
||
|
|
reference_alignment(broken)
|
||
|
|
with self.assertRaises(ValueError):
|
||
|
|
load_session_data(path.parent, target_root, 4) # Wrong mode.
|
||
|
|
with self.assertRaises(FileExistsError):
|
||
|
|
direct.save_multiwindow_banks(payload, results, root / "Session_4.pt")
|
||
|
|
bad = copy.deepcopy(results)
|
||
|
|
bad[12].states[0, 0, 0] = float("nan")
|
||
|
|
with self.assertRaises(ValueError):
|
||
|
|
direct.save_multiwindow_banks(payload, bad, root / "fresh" / "Session_4.pt")
|
||
|
|
self.assertFalse((root / "fresh" / "context_2s" / "Session_4.pt").exists())
|
||
|
|
|
||
|
|
|
||
|
|
class DriverTests(unittest.TestCase):
|
||
|
|
def test_driver_loads_model_once_across_sessions_and_writes_summary(self):
|
||
|
|
with tempfile.TemporaryDirectory() as directory:
|
||
|
|
root = Path(directory)
|
||
|
|
target_root = root / "targets"
|
||
|
|
target_root.mkdir()
|
||
|
|
for session in ("Session_4", "Session_5"):
|
||
|
|
(root / session).mkdir()
|
||
|
|
np.save(target_root / f"{session}.npy", np.tile(np.array([[1, 0, 0]], dtype=np.float32), (20, 1)))
|
||
|
|
model, processor = object(), object()
|
||
|
|
|
||
|
|
def run(command, *, model=None, processor=None):
|
||
|
|
self.assertIs(model, loaded_model)
|
||
|
|
self.assertIs(processor, loaded_processor)
|
||
|
|
args = direct.parse_args(command)
|
||
|
|
times = (torch.arange(20, dtype=torch.float64) + 1) / 5
|
||
|
|
results = {cap: core.ExtractionResult(
|
||
|
|
times, torch.ones(20, 8), [], {"actual_context_seconds": torch.full((20,), cap)}
|
||
|
|
) for cap in args.memory_seconds_list}
|
||
|
|
payload = {"mode": "open-stream-replay", "start": 0, "num_steps": 20,
|
||
|
|
"target_path": str(args.target), "config": {"step_seconds": .2},
|
||
|
|
"label_indices": torch.arange(20), "open_stream_policy": {}}
|
||
|
|
direct.save_multiwindow_banks(payload, results, args.output)
|
||
|
|
|
||
|
|
loaded_model, loaded_processor = model, processor
|
||
|
|
with mock.patch.object(direct, "_easycom_directory_specs"), \
|
||
|
|
mock.patch.object(core, "load_qwen_omni", return_value=(model, processor)) as loader, \
|
||
|
|
mock.patch.object(direct, "main", side_effect=run) as runner:
|
||
|
|
driver.main(["--easycom-video-root", str(root), "--target-root", str(target_root),
|
||
|
|
"--sessions", "Session_4", "Session_5", "--memory-seconds-list", "2", "4",
|
||
|
|
"--output-root", str(root / "output")])
|
||
|
|
loader.assert_called_once()
|
||
|
|
self.assertEqual(runner.call_count, 2)
|
||
|
|
summary = json.loads((root / "output" / "extraction_summary.json").read_text())
|
||
|
|
self.assertEqual(set(summary["sessions"]), {"Session_4", "Session_5"})
|
||
|
|
self.assertFalse(summary["partial_extraction"])
|
||
|
|
self.assertEqual(summary["sessions"]["Session_5"]["banks"]["4s"]["shape"], [20, 8])
|
||
|
|
|
||
|
|
def test_direct_main_routes_multi_banks_and_preserves_scalar_output(self):
|
||
|
|
with tempfile.TemporaryDirectory() as directory:
|
||
|
|
root = Path(directory)
|
||
|
|
times = (torch.arange(20, dtype=torch.float64) + 1) / 5
|
||
|
|
result = core.ExtractionResult(times, torch.ones(20, 8), [], {})
|
||
|
|
reader = types.SimpleNamespace(
|
||
|
|
clips=[types.SimpleNamespace(path=Path("unused.mp4"))], metadata=lambda: {"synthetic": True}
|
||
|
|
)
|
||
|
|
model = FakeModel()
|
||
|
|
model.eval = lambda: model
|
||
|
|
model.requires_grad_ = lambda value: model
|
||
|
|
base = ["--video", "unused.mp4", "--end", "4", "--output", str(root / "Session_4.pt")]
|
||
|
|
with mock.patch.object(direct, "CausalSourceAV", return_value=reader), \
|
||
|
|
mock.patch.object(direct, "_print_media_summary"), \
|
||
|
|
mock.patch.object(core, "backbone_metadata", return_value={"loader": "unknown"}), \
|
||
|
|
mock.patch.object(MultiWindowOpenStreamReplayExtractor, "__init__", return_value=None), \
|
||
|
|
mock.patch.object(MultiWindowOpenStreamReplayExtractor, "extract", return_value={2: result, 4: result}):
|
||
|
|
direct.main(base + ["--memory-seconds-list", "2", "4"], model=model, processor=object())
|
||
|
|
self.assertFalse((root / "Session_4.pt").exists())
|
||
|
|
bank = torch.load(root / "context_2s" / "Session_4.pt", weights_only=False)
|
||
|
|
self.assertEqual(bank["config"]["memory_seconds"], 2)
|
||
|
|
self.assertFalse(bank["persistent_thinker_kv"])
|
||
|
|
self.assertTrue(bank["partial_extraction"])
|
||
|
|
with mock.patch.object(direct, "CausalSourceAV", return_value=reader), \
|
||
|
|
mock.patch.object(direct, "_print_media_summary"), \
|
||
|
|
mock.patch.object(core, "backbone_metadata", return_value={"loader": "unknown"}), \
|
||
|
|
mock.patch.object(core.OpenStreamReplayExtractor, "__init__", return_value=None), \
|
||
|
|
mock.patch.object(core.OpenStreamReplayExtractor, "extract", return_value=result):
|
||
|
|
direct.main(base + ["--memory-seconds", "4"], model=model, processor=object())
|
||
|
|
self.assertTrue((root / "Session_4.pt").exists())
|
||
|
|
|
||
|
|
def test_cli_preflight_and_partial_target_policy(self):
|
||
|
|
with tempfile.TemporaryDirectory() as directory, mock.patch.object(direct, "_easycom_directory_specs"):
|
||
|
|
root = Path(directory)
|
||
|
|
(root / "Session_4").mkdir()
|
||
|
|
common = ["--easycom-video-root", str(root), "--sessions", "Session_4",
|
||
|
|
"--output-root", str(root / "output"), "--end", "24"]
|
||
|
|
args = driver.parse_args(common)
|
||
|
|
commands = driver.session_commands(args)
|
||
|
|
self.assertNotIn("--target", commands[0][1])
|
||
|
|
self.assertIn("--memory-seconds-list", commands[0][1])
|
||
|
|
self.assertEqual(direct.parse_args(commands[0][1]).readout_last_n_layers, 4)
|
||
|
|
with mock.patch.object(core, "load_qwen_omni") as loader, mock.patch.object(direct, "main") as main:
|
||
|
|
driver.main(common + ["--media-only"])
|
||
|
|
loader.assert_not_called()
|
||
|
|
main.assert_called_once()
|
||
|
|
for extra in (["--sessions", "Session_99"], ["--sessions", "Session_4", "Session_4"],
|
||
|
|
["--memory-seconds-list", "3"], ["--end", "0.1"]):
|
||
|
|
with self.assertRaises(ValueError):
|
||
|
|
driver.session_commands(driver.parse_args(common + extra))
|
||
|
|
existing = root / "output" / "context_2s" / "Session_4.pt"
|
||
|
|
existing.parent.mkdir(parents=True)
|
||
|
|
existing.touch()
|
||
|
|
with self.assertRaises(FileExistsError):
|
||
|
|
driver.session_commands(args)
|
||
|
|
|
||
|
|
def test_direct_legacy_parser_and_multi_conflicts(self):
|
||
|
|
base = ["--video", "unused.mp4", "--output", "unused.pt", "--end", "24"]
|
||
|
|
self.assertIsNone(direct.parse_args(base).memory_seconds_list)
|
||
|
|
for extra in (["--mode", "causal-prefix"], ["--memory-seconds", "8"],
|
||
|
|
["--head-checkpoint", "head.pt"], ["--start", ".2"]):
|
||
|
|
with self.assertRaises(SystemExit):
|
||
|
|
direct.main(base + ["--memory-seconds-list", "2", "4"] + extra)
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|