Files
icassp2027-experiment/test_streaming_chunk_ablation.py
T

230 lines
8.3 KiB
Python
Raw Normal View History

2026-09-17 02:32:08 +08:00
"""CPU-only schedule and feature-contract tests for Qwen chunk ablations."""
from __future__ import annotations
import tempfile
import types
import unittest
from pathlib import Path
from unittest import mock
import numpy as np
import torch
import streaming_turn_model as core
from easycom_head import load_session_data
from local_turn_features import reference_alignment
class FakeModel:
def __init__(self) -> None:
self.config = types.SimpleNamespace(
seconds_per_chunk=2.0,
position_id_per_seconds=25.0,
text_config=types.SimpleNamespace(hidden_size=3),
)
self.dtype = torch.float32
self.device = torch.device("cpu")
class FakeCache:
def __init__(self, count: int) -> None:
self.count = count
def fake_pieces() -> core.OpenMediaPieces:
return core.OpenMediaPieces(
prefix_embeddings=torch.zeros(1, 1, 3),
prefix_position_ids=torch.zeros(3, 1, 1, dtype=torch.long),
payload_embeddings=torch.zeros(1, 1, 3),
payload_position_ids=torch.zeros(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),
)
class RecordingMixin:
def _prepare_pieces(self, **kwargs): # type: ignore[no-untyped-def]
self.prepared_intervals.append(tuple(kwargs["interval"]))
return fake_pieces()
class RecordingHold(RecordingMixin, core.CompletedBlockHoldExtractor):
def __init__(self, *args, **kwargs): # type: ignore[no-untyped-def]
super().__init__(*args, **kwargs)
self.prepared_intervals: list[tuple[float, float]] = []
class RecordingMicro(RecordingMixin, core.MicroChunkReplayExtractor):
def __init__(self, *args, **kwargs): # type: ignore[no-untyped-def]
super().__init__(*args, **kwargs)
self.prepared_intervals: list[tuple[float, float]] = []
def fake_append( # type: ignore[no-untyped-def]
model,
pieces,
*,
past_key_values,
rope_offset,
include_prefix,
):
count = 0 if past_key_values is None else past_key_values.count
return FakeCache(count + 1), rope_offset + 1, pieces.closure_position_ids
def fake_read(model, cache, **kwargs): # type: ignore[no-untyped-def]
return torch.full((1, 3), float(cache.count), dtype=torch.float32)
class QwenChunkAblationTests(unittest.TestCase):
def setUp(self) -> None:
self.model = FakeModel()
self.processor = object()
self.config = core.StreamConfig(memory_seconds=4.0)
self.patches = [
mock.patch.object(
core,
"_readout_query_ids",
return_value=torch.zeros(1, 1, dtype=torch.long),
),
mock.patch.object(core, "_append_open_pieces", side_effect=fake_append),
mock.patch.object(core, "_read_open_state", side_effect=fake_read),
]
for patcher in self.patches:
patcher.start()
self.addCleanup(patcher.stop)
def test_completed_native_blocks_are_held_without_future_backfill(self) -> None:
extractor = RecordingHold(self.model, self.processor, self.config)
result = extractor.extract(
video_path="unused.mp4",
audio_path=None,
start=0.0,
end=4.0,
use_audio_in_video=True,
show_progress=False,
)
self.assertEqual(tuple(result.states.shape), (20, 3))
self.assertTrue(torch.equal(result.states[:9], torch.zeros(9, 3)))
self.assertTrue(torch.equal(result.states[9:19], torch.ones(10, 3)))
self.assertTrue(torch.equal(result.states[19], torch.full((3,), 2.0)))
self.assertEqual(
result.diagnostics["state_update_mask"].nonzero().flatten().tolist(),
[9, 19],
)
self.assertTrue(
torch.equal(
result.diagnostics["state_update_mask"],
result.diagnostics["payload_commit_mask"],
)
)
self.assertTrue(
torch.equal(
result.diagnostics["state_update_mask"],
result.diagnostics["native_boundary_mask"],
)
)
self.assertTrue(result.diagnostics["initialization_mask"][:9].all())
self.assertFalse(result.diagnostics["initialization_mask"][9:].any())
self.assertEqual(
result.diagnostics["state_source_times"].tolist(),
[0.0] * 9 + [2.0] * 10 + [4.0],
)
self.assertEqual(extractor.prepared_intervals, [(0.0, 2.0), (2.0, 4.0)])
self.assertEqual(result.diagnostics["production_processor_calls"], 2)
self.assertAlmostEqual(
result.diagnostics["production_prepared_media_seconds"], 4.0
)
def test_micro_chunks_update_every_200ms_without_growing_tail(self) -> None:
config = core.StreamConfig(memory_seconds=2.0)
extractor = RecordingMicro(self.model, self.processor, config)
result = extractor.extract(
video_path="unused.mp4",
audio_path=None,
start=0.0,
end=2.0,
use_audio_in_video=True,
show_progress=False,
)
expected = torch.arange(1, 11, dtype=torch.float32).view(-1, 1)
expected = expected.expand(-1, 3)
self.assertTrue(torch.equal(result.states, expected))
self.assertTrue(result.diagnostics["state_update_mask"].all())
self.assertTrue(result.diagnostics["payload_commit_mask"].all())
self.assertEqual(
result.diagnostics["native_boundary_mask"]
.nonzero()
.flatten()
.tolist(),
[9],
)
self.assertTrue(
torch.equal(
result.diagnostics["state_source_times"], result.times
)
)
self.assertEqual(len(extractor.prepared_intervals), 10)
for index, interval in enumerate(extractor.prepared_intervals):
self.assertAlmostEqual(interval[0], index * 0.2)
self.assertAlmostEqual(interval[1], (index + 1) * 0.2)
self.assertEqual(result.diagnostics["production_processor_calls"], 10)
self.assertAlmostEqual(
result.diagnostics["production_prepared_media_seconds"], 2.0
)
self.assertEqual(result.diagnostics["tmrope_positions_per_micro_step"], 5)
def test_micro_tmrope_step_must_tile_the_native_chunk(self) -> None:
self.assertEqual(core._tmrope_positions_per_step(self.model, 0.2), 5)
with self.assertRaisesRegex(ValueError, "Non-integral TMRoPE"):
core._tmrope_positions_per_step(self.model, 0.15)
def test_both_modes_load_through_head_and_local_alignment_contracts(self) -> None:
states = torch.randn(20, 8)
targets = np.tile(
np.asarray(
[[1, 0, 0], [0, 1, 0], [0, 0, 1], [0, 1, 1]],
dtype=np.float32,
),
(5, 1),
)
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
feature_root = root / "features"
target_root = root / "targets"
feature_root.mkdir()
target_root.mkdir()
feature_path = feature_root / "Session_4.pt"
np.save(target_root / "Session_4.npy", targets)
for mode in ("completed-block-hold", "micro-chunk-200ms"):
payload = {
"states": states,
"times": (torch.arange(20, dtype=torch.float64) + 1) / 5,
"label_indices": torch.arange(20),
"mode": mode,
"config": {"step_seconds": 0.2},
"start": 0.0,
}
torch.save(payload, feature_path)
loaded = load_session_data(
feature_root,
target_root,
4,
expected_mode=mode,
)
self.assertTrue(torch.equal(loaded.states, states))
times, indices = reference_alignment(payload)
self.assertTrue(torch.equal(times, payload["times"]))
self.assertTrue(torch.equal(indices, payload["label_indices"]))
with self.assertRaisesRegex(ValueError, "expected 'causal-prefix'"):
load_session_data(feature_root, target_root, 4)
if __name__ == "__main__":
unittest.main()