baladithyab
Wave 3: integration architecture + spike-005 trainer skeleton (16 tests pass)
fd77f74
Raw
History Blame Contribute Delete
4.67 kB
"""test_opsd_loss.py — unit test for the lifted OPSD loss.
Verifies:
1. Loss is differentiable.
2. Loss is 0 when student == teacher (sanity).
3. Loss is positive when student != teacher.
4. Forward KL (beta=0), reverse KL (beta=1), and JSD (beta=0.5) all run
and produce finite values.
5. Label masking zeros out ignored positions.
6. top_k restriction reduces compute and gives a valid result.
Run: pytest spikes/005-integrated-trainer-skeleton/tests/test_opsd_loss.py -v
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
import torch
# Make sibling modules importable without packaging the skeleton
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from opsd_loss import generalized_jsd_loss # noqa: E402
# ----------------------------------------------------------------------------
# Test fixtures
# ----------------------------------------------------------------------------
@pytest.fixture
def small_logits():
"""B=2, T=4, V=8 — small enough to debug if anything fails."""
torch.manual_seed(0)
return torch.randn(2, 4, 8, requires_grad=True), torch.randn(2, 4, 8)
# ----------------------------------------------------------------------------
# Tests
# ----------------------------------------------------------------------------
def test_loss_is_finite_and_positive(small_logits):
student, teacher = small_logits
loss = generalized_jsd_loss(student, teacher, beta=0.5)
assert torch.isfinite(loss).all(), "JSD loss is NaN or Inf"
assert loss.item() > 0, "JSD loss should be positive when distributions differ"
def test_loss_is_zero_when_student_equals_teacher():
"""If student_logits == teacher_logits, JSD == 0 (within numeric tolerance)."""
torch.manual_seed(1)
logits = torch.randn(2, 4, 8, requires_grad=True)
loss = generalized_jsd_loss(logits, logits.detach().clone(), beta=0.5)
# Some tiny float noise from log_softmax round-trips → tolerance, not exact
assert loss.abs().item() < 1e-5, f"Expected ~0 loss, got {loss.item()}"
def test_loss_is_differentiable(small_logits):
student, teacher = small_logits
loss = generalized_jsd_loss(student, teacher, beta=0.5)
loss.backward()
assert student.grad is not None
assert torch.isfinite(student.grad).all(), "Gradient has NaN/Inf"
# Teacher should NOT receive gradient (it had requires_grad=False from fixture)
assert teacher.grad is None or teacher.requires_grad is False
@pytest.mark.parametrize("beta", [0.0, 0.5, 1.0])
def test_all_betas_run(small_logits, beta):
student, teacher = small_logits
loss = generalized_jsd_loss(student, teacher, beta=beta)
assert torch.isfinite(loss).all(), f"Loss not finite at beta={beta}"
assert loss.item() > 0, f"Loss not positive at beta={beta}"
def test_label_mask_excludes_ignored_positions():
"""Positions with label == -100 should not contribute to the loss."""
torch.manual_seed(2)
student = torch.randn(2, 4, 8, requires_grad=True)
teacher = torch.randn(2, 4, 8)
# Mask: include only position 0 in batch element 0; nothing else.
labels = torch.full((2, 4), -100, dtype=torch.long)
labels[0, 0] = 1 # one valid token
loss_with_mask = generalized_jsd_loss(student, teacher, labels=labels, reduction="sum")
# Compare to unmasked
loss_unmasked = generalized_jsd_loss(student, teacher, labels=None, reduction="sum")
# Masked loss must be strictly smaller (ignored positions zero out)
assert loss_with_mask < loss_unmasked, (
"Masked loss should be smaller than unmasked when most positions are masked"
)
assert loss_with_mask.item() > 0, "At least one valid token should give positive loss"
def test_top_k_restriction(small_logits):
"""top_k restricts the KL to the teacher's top-k tokens."""
student, teacher = small_logits
loss_full = generalized_jsd_loss(student, teacher, beta=0.5)
loss_topk = generalized_jsd_loss(student, teacher, beta=0.5, top_k=4)
assert torch.isfinite(loss_topk).all()
# top-k loss should typically be smaller (fewer terms in the sum) but not strictly so
# because the renormalization can flip relative magnitudes. Just check finite + positive.
assert loss_topk.item() > 0
def test_token_clip(small_logits):
"""Per-token clip caps individual token contributions."""
student, teacher = small_logits
loss_unclipped = generalized_jsd_loss(student, teacher, beta=0.5)
loss_clipped = generalized_jsd_loss(student, teacher, beta=0.5, token_clip=0.001)
assert loss_clipped <= loss_unclipped, "Clipping should reduce or equal loss"