from itertools import pairwise import pytest torch = pytest.importorskip("torch") from vons.models import ModelConfig, build_model, cosine_beta_schedule from vons.training import soft_diffusion_targets def _model(hidden_size=16): return build_model( "diffusion", option_count=32, config=ModelConfig( hidden_size=hidden_size, latent_size=8, diffusion_steps=16, inference_steps=4, attention_heads=2, ), ) def test_cosine_schedule_reaches_high_terminal_noise(): betas = cosine_beta_schedule(16) alpha_bar = torch.cumprod(1.0 - betas, dim=0) assert betas.shape == (16,) assert torch.all(betas[1:] > betas[:-1]) assert float(alpha_bar[-1]) < 1e-4 @pytest.mark.parametrize(("options", "sequence"), [(3, 7), (5, 11)]) def test_token_conditioned_head_accepts_dynamic_option_and_sequence_axes(options, sequence): torch.manual_seed(7) model = _model().eval() output = model( torch.randn(2, options), torch.randn(2, options, sequence, 16), torch.ones(2, options, sequence, dtype=torch.bool), torch.tensor([3, 4]), torch.ones(2, options, dtype=torch.bool), ) assert output["logits"].shape == (2, options) assert output["answerability"].shape == (2,) assert torch.isfinite(output["logits"]).all() def test_soft_targets_are_continuous_normalized_and_masked(): labels = torch.tensor([0, 2]) option_mask = torch.tensor([[True, True, True, False], [True, True, True, True]]) targets = soft_diffusion_targets(labels, option_mask, smoothing=0.1) torch.testing.assert_close(targets.sum(dim=-1), torch.ones(2)) assert torch.all((targets > 0) == option_mask) torch.testing.assert_close(targets[torch.arange(2), labels], torch.tensor([0.9, 0.9])) def test_token_conditioned_soft_target_loss_strictly_decreases(): torch.manual_seed(7) batch, options, sequence, hidden = 8, 4, 6, 16 model = _model(hidden) sequence_hidden = torch.randn(batch, options, sequence, hidden) attention_mask = torch.ones(batch, options, sequence, dtype=torch.bool) option_mask = torch.ones(batch, options, dtype=torch.bool) labels = torch.arange(batch) % options targets = soft_diffusion_targets(labels, option_mask, smoothing=0.1) noisy_scores = targets * 0.7 + torch.randn_like(targets) * 0.3 timestep = torch.full((batch,), 5, dtype=torch.long) optimizer = torch.optim.Adam(model.parameters(), lr=0.01) losses = [] for _ in range(12): optimizer.zero_grad() output = model( noisy_scores, sequence_hidden, attention_mask, timestep, option_mask, ) loss = torch.nn.functional.cross_entropy(output["logits"], targets) loss.backward() optimizer.step() losses.append(float(loss.detach())) assert all(current < previous for previous, current in pairwise(losses))