Self-Forcing / tests /test_atc_fusion.py
Cccccz's picture
Upload tests
ebad435 verified
Raw History Blame Contribute Delete
6.22 kB
import unittest
from unittest.mock import patch
import torch
import torch.nn.functional as F
from torch import nn
from predictor_training.atc_fusion import ATCFusion, MultiInputMLP, StateTransport
from predictor_training.single_block import SingleBlockPredictor
from wan.modules.model import rope_params
def small_freqs(head_dim: int = 8) -> torch.Tensor:
return torch.cat(
[
rope_params(32, head_dim - 4 * (head_dim // 6)),
rope_params(32, 2 * (head_dim // 6)),
rope_params(32, 2 * (head_dim // 6)),
],
dim=1,
)
def reference_attention(
q: torch.Tensor, k: torch.Tensor, v: torch.Tensor
) -> torch.Tensor:
scores = torch.einsum("blhd,bshd->bhls", q, k) / q.shape[-1] ** 0.5
probability = scores.float().softmax(dim=-1).to(v.dtype)
return torch.einsum("bhls,bshd->blhd", probability, v)
class DummyBlock(nn.Module):
num_heads = 1
def forward(self, value: torch.Tensor, **_kwargs) -> torch.Tensor:
return value
class ATCFusionTest(unittest.TestCase):
def setUp(self) -> None:
torch.manual_seed(0)
self.grid = torch.tensor([[2, 2, 2]], dtype=torch.long)
self.freqs = small_freqs()
self.current = torch.randn(1, 8, 8)
self.previous = torch.randn(1, 8, 8)
def test_multi_input_mlp_matches_explicit_concat(self) -> None:
module = MultiInputMLP(3, dim=8, hidden_dim=12)
values = [torch.randn(2, 5, 8) for _ in range(3)]
combined_weight = torch.cat(
[layer.weight for layer in module.inputs], dim=1
)
combined_bias = sum(layer.bias for layer in module.inputs)
reference_hidden = F.linear(
torch.cat(values, dim=-1), combined_weight, combined_bias
)
reference = module.out(module.activation(reference_hidden))
torch.testing.assert_close(module(*values), reference)
@patch(
"predictor_training.atc_fusion.attention",
side_effect=reference_attention,
)
def test_last_frame_scope_cannot_read_earlier_frames(self, _mock) -> None:
transport = StateTransport(
dim=8,
num_heads=1,
previous_scope="last_frame",
gradient_checkpointing=False,
diagnostic_queries=4,
).eval()
first, _ = transport(
self.current, self.previous, self.grid, self.freqs, current_start=8
)
perturbed_early = self.previous.clone()
perturbed_early[:, :4] += torch.randn_like(perturbed_early[:, :4])
second, _ = transport(
self.current,
perturbed_early,
self.grid,
self.freqs,
current_start=8,
)
torch.testing.assert_close(first, second, rtol=0, atol=0)
perturbed_last = self.previous.clone()
perturbed_last[:, 4:] += torch.randn_like(perturbed_last[:, 4:])
third, _ = transport(
self.current,
perturbed_last,
self.grid,
self.freqs,
current_start=8,
)
self.assertFalse(torch.equal(first, third))
@patch(
"predictor_training.atc_fusion.attention",
side_effect=reference_attention,
)
def test_zero_initialized_correction_bypass(self, _mock) -> None:
fusion = ATCFusion(
dim=8,
num_heads=1,
freq_dim=8,
mlp_hidden_dim=16,
gate_hidden_dim=4,
previous_scope="chunk",
gradient_checkpointing=False,
).eval()
condition = torch.randn_like(self.current)
backbone, gated_correction = fusion(
self.current,
torch.randn_like(self.current),
self.previous,
condition,
torch.tensor([62.5]),
self.grid,
self.freqs,
current_start=8,
)
self.assertEqual(backbone.shape, self.current.shape)
torch.testing.assert_close(
gated_correction,
torch.zeros_like(gated_correction),
rtol=0,
atol=0,
)
self.assertAlmostEqual(float(fusion.last_gate.mean()), 0.3, delta=0.01)
def test_atc_token_gate_is_a_stage1_parameter(self) -> None:
predictor = SingleBlockPredictor(
DummyBlock(),
dim=8,
gradient_checkpointing=False,
input_variant="atc",
atc_freq_dim=8,
atc_mlp_hidden_dim=16,
atc_gate_hidden_dim=4,
)
token_gate_ids = {
id(parameter)
for parameter in predictor.fusion.token_gate.parameters()
}
stage1_ids = {
id(parameter) for parameter in predictor.stage1_input_parameters()
}
self.assertTrue(token_gate_ids)
self.assertTrue(token_gate_ids.issubset(stage1_ids))
self.assertEqual(predictor.gate_parameters(), [])
@patch(
"predictor_training.atc_fusion.attention",
side_effect=reference_attention,
)
def test_zero_initialized_predictor_returns_anchor(self, _mock) -> None:
predictor = SingleBlockPredictor(
DummyBlock(),
dim=8,
gradient_checkpointing=False,
input_variant="atc",
atc_freq_dim=8,
atc_mlp_hidden_dim=16,
atc_gate_hidden_dim=4,
).eval()
anchor = torch.randn_like(self.current)
empty_history = torch.empty(1, 0, 1, 8)
prediction = predictor(
current_tokens=self.current,
anchor_hidden=anchor,
previous_hidden=self.previous,
timestep_modulation=torch.randn(1, 2, 6, 8),
grid_sizes=self.grid,
freqs=self.freqs,
history_k=empty_history,
history_v=empty_history,
cross_k=torch.empty(1, 0, 1, 8),
cross_v=torch.empty(1, 0, 1, 8),
current_start=8,
condition_tokens=torch.randn_like(self.current),
anchor_distance=torch.tensor([62.5]),
)
torch.testing.assert_close(prediction, anchor, rtol=0, atol=0)
if __name__ == "__main__":
unittest.main()