| """Phase 4 acceptance for td.testing.check_block. See PLAN.md. |
| |
| Half of this file is deliberately broken blocks. A conformance suite that only |
| ever passes proves nothing, so every check gets a block built to violate it and |
| nothing else. |
| """ |
|
|
| import pytest |
| import torch |
| import torch.nn as nn |
|
|
| import torch_dimensions as td |
| from torch_dimensions import GRU, LSTM, Lattice, axial_apply |
|
|
|
|
| def rnn_factory(cls, n_layers=None): |
| def build(lat, d_model, plan=None): |
| return cls(d_model, n_layers or len(lat.axis_names), lat, plan=plan) |
|
|
| return build |
|
|
|
|
| def one_layer_reference(block, x): |
| """Pre-norm residual around the single mixer — what a 1-layer stack is.""" |
| return x + block.nd.mixers[0].rnn(block.nd.norms[0](x))[0] |
|
|
|
|
| def status(report, fragment): |
| for r in report.results: |
| if fragment in r.name: |
| return r.status |
| raise KeyError(f"no check named ~{fragment!r} in:\n{report}") |
|
|
|
|
| |
|
|
|
|
| @pytest.mark.parametrize("cls", [LSTM, GRU]) |
| def test_rnn_family_passes_every_applicable_check(cls): |
| report = td.testing.check_block(rnn_factory(cls)) |
| assert report, str(report) |
| assert not report.failed |
|
|
|
|
| def test_rank_one_equivalence_passes_when_a_reference_is_given(): |
| report = td.testing.check_block(rnn_factory(LSTM), reference=one_layer_reference) |
| assert status(report, "rank-1") == "pass", str(report) |
|
|
|
|
| def test_checks_that_cannot_run_are_reported_as_skipped_not_passed(): |
| """A skipped check must never read as a passing one.""" |
| report = td.testing.check_block(rnn_factory(LSTM)) |
| assert status(report, "rank-1") == "skip" |
| assert status(report, "Kronecker") == "skip" |
| assert status(report, "compile") == "skip" |
| assert report, "skips must not make the report falsy" |
|
|
|
|
| def test_covariance_is_skipped_when_the_factory_cannot_fix_the_plan(): |
| def no_plan_arg(lat, d_model): |
| return LSTM(d_model, len(lat.axis_names), lat) |
|
|
| report = td.testing.check_block(no_plan_arg) |
| assert status(report, "covariant") == "skip" |
|
|
|
|
| |
|
|
|
|
| class _Base(nn.Module): |
| def __init__(self, lat, d_model): |
| super().__init__() |
| self.lat, self.lin = lat, nn.Linear(d_model, d_model) |
|
|
|
|
| class ShapeBreaker(_Base): |
| def forward(self, x): |
| return self.lin(x)[..., :1] |
|
|
|
|
| class LeakyMask(_Base): |
| """Scans without zeroing absent cells, so their values ride the recurrence |
| into present ones.""" |
|
|
| def forward(self, x): |
| return axial_apply(self.lin(x), self.lat, 0, lambda s: s.cumsum(1)) |
|
|
|
|
| class HardcodedAxis(_Base): |
| """Always sweeps tensor dim 1 instead of resolving the axis, so the result |
| depends on which axis happens to be stored first.""" |
|
|
| def forward(self, x): |
| return self.lin(x).cumsum(dim=1) |
|
|
|
|
| class DeadParameter(_Base): |
| def __init__(self, lat, d_model): |
| super().__init__(lat, d_model) |
| self.unused = nn.Parameter(torch.zeros(d_model)) |
|
|
| def forward(self, x): |
| return self.lin(x) |
|
|
|
|
| def _factory(cls): |
| def build(lat, d_model, plan=None): |
| return cls(lat, d_model) |
|
|
| return build |
|
|
|
|
| def test_shape_check_catches_a_block_that_changes_width(): |
| report = td.testing.check_block(_factory(ShapeBreaker), raise_on_failure=False) |
| assert status(report, "shape") == "fail" |
| assert not report |
|
|
|
|
| def test_mask_check_catches_absent_cells_leaking_into_the_output(): |
| report = td.testing.check_block(_factory(LeakyMask), raise_on_failure=False) |
| assert status(report, "absent cells") == "fail" |
|
|
|
|
| def test_covariance_check_catches_a_hardcoded_axis(): |
| report = td.testing.check_block(_factory(HardcodedAxis), ranks=(3,), raise_on_failure=False) |
| assert status(report, "covariant") == "fail" |
|
|
|
|
| def test_gradient_check_catches_a_parameter_that_never_receives_grad(): |
| report = td.testing.check_block(_factory(DeadParameter), raise_on_failure=False) |
| assert status(report, "gradients") == "fail" |
| assert "unused" in [r for r in report.results if "gradients" in r.name][0].detail |
|
|
|
|
| def test_equivalence_check_catches_a_wrong_rank_one_result(): |
| report = td.testing.check_block( |
| rnn_factory(LSTM), |
| reference=lambda block, x: torch.zeros_like(x), |
| raise_on_failure=False, |
| ) |
| assert status(report, "rank-1") == "fail" |
|
|
|
|
| |
|
|
|
|
| def test_failures_raise_by_default_with_the_report_attached(): |
| with pytest.raises(AssertionError, match="conformance check failed"): |
| td.testing.check_block(_factory(ShapeBreaker)) |
|
|
|
|
| def test_report_renders_every_check(): |
| text = str(td.testing.check_block(rnn_factory(LSTM))) |
| assert text.count("\n") == 6 |
| assert "[ ok]" in text and "[skip]" in text |
|
|
|
|
| def test_report_is_falsy_only_when_something_failed(): |
| assert td.testing.check_block(rnn_factory(LSTM)) |
| assert not td.testing.check_block(_factory(ShapeBreaker), raise_on_failure=False) |
|
|
|
|
| def test_compile_check_runs_when_asked(): |
| report = td.testing.check_block( |
| rnn_factory(LSTM, n_layers=1), ranks=(2,), check_compile=True, raise_on_failure=False |
| ) |
| assert status(report, "compile") in {"pass", "fail"} |
|
|
|
|
| def test_sparse_can_be_turned_off_for_blocks_that_do_not_support_it(): |
| report = td.testing.check_block(rnn_factory(LSTM), sparse=False) |
| assert status(report, "absent cells") == "skip" |
|
|
|
|
| def test_time_axis_lattices_are_supported(): |
| report = td.testing.check_block(rnn_factory(LSTM), time=True, ranks=(1, 2)) |
| assert report, str(report) |
|
|
|
|
| def test_custom_nd_method_can_be_checked(): |
| """The point of shipping this: a user's own strategy gets the same checks.""" |
|
|
| def reversed_sweep(mixer, plan, lattice, d_model, **kw): |
| flipped = td.ScanPlan.from_list([(s.axis, not s.reverse) for s in plan]) |
| return td.AxialScan(mixer=mixer, plan=flipped, lattice=lattice, d_model=d_model, **kw) |
|
|
| def build(lat, d_model, plan=None): |
| return LSTM(d_model, len(lat.axis_names), lat, plan=plan, nd_method=reversed_sweep) |
|
|
| assert td.testing.check_block(build) |
|
|
|
|
| def test_lattice_helper_builds_a_genuinely_sparse_mask(): |
| lat = td.testing._lattice(3, sparse=True, seed=0) |
| assert 0 < lat.n_valid < lat.n_cells |
| assert isinstance(lat, Lattice) |
|
|
|
|
| def test_checks_run_only_at_ranks_the_caller_requested(): |
| """The gradient check hardcoded rank 2 "for speed", so ranks=(3, 4) |
| gradchecked a rank-2 block the factory never claimed to support — and a |
| factory valid only at its stated ranks failed a check it should pass.""" |
|
|
| def rank3_only(lat, d_model): |
| assert lat.rank in (3, 4), f"built at unrequested rank {lat.rank}" |
| plan = td.ScanPlan.cyclic(lat.axis_names, lat.n_axes) |
| return td.AxialScan( |
| mixer=lambda: torch.nn.Linear(d_model, d_model), |
| plan=plan, |
| lattice=lat, |
| d_model=d_model, |
| ) |
|
|
| report = td.testing.check_block( |
| rank3_only, |
| ranks=(3, 4), |
| |
| |
| |
| reference=lambda block, x: block(x), |
| raise_on_failure=False, |
| ) |
| assert not report.failed, str(report) |
| assert any("rank 1" in r.detail for r in report.skipped), str(report) |
|
|
|
|
| |
|
|
|
|
| @pytest.mark.parametrize("rank", [5, 6]) |
| def test_the_scan_family_conforms_at_ranks_five_and_six(rank): |
| """The machinery is rank-generic by construction; that is a claim, and this |
| is the test of it. Ranks 5-6 were shipped untested for one release — the |
| README said so — because "generic" felt like enough. It is not: a rank-6 |
| lattice is 5,040 cells and every axis bug that hides behind a lucky square |
| shape at rank 2 has room to show here.""" |
| report = td.testing.check_block(rnn_factory(LSTM, n_layers=3), ranks=(rank,), d_model=2) |
| assert report, str(report) |
|
|
|
|
| @pytest.mark.parametrize("rank", [5, 6]) |
| def test_the_kernel_family_conforms_at_ranks_five_and_six(rank): |
| def build(lat, d_model, plan=None): |
| return td.AxialKernel( |
| mixer=None, |
| plan=plan or td.ScanPlan.cyclic(lat.axis_names, len(lat.axis_names)), |
| lattice=lat, |
| d_model=d_model, |
| ) |
|
|
| report = td.testing.check_block(build, ranks=(rank,), d_model=2) |
| assert report, str(report) |
|
|
|
|
| def test_a_rank_five_model_trains(): |
| """Conformance says the plumbing is right; this says the thing still learns |
| when five axes have to be reached through three layers.""" |
| lat = td.Lattice(shape=(2, 2, 3, 2, 2), names=("a", "b", "c", "d", "e")) |
| model = td.LSTM(d_model=8, n_layers=5, lattice=lat, d_input=1) |
| x = torch.randn(4, *lat.shape, 1) |
| y = x.cumsum(dim=lat.tensor_dim("c")) |
| head = nn.Linear(8, 1) |
| opt = torch.optim.Adam([*model.parameters(), *head.parameters()], lr=3e-2) |
| first = last = None |
| for step in range(60): |
| loss = (head(model(x)) - y).pow(2).mean() |
| opt.zero_grad() |
| loss.backward() |
| opt.step() |
| first = float(loss.detach()) if step == 0 else first |
| last = float(loss.detach()) |
| assert last < first * 0.25, f"rank-5 model did not learn: {first:.4f} -> {last:.4f}" |
|
|
|
|
| |
|
|
|
|
| def test_recorder_answers_which_axis_each_layer_swept(): |
| """ "Which axis did layer 3 actually sweep" is the first question every |
| integration bug asks; this is the tool that answers it.""" |
| lat = td.Lattice(shape=(3, 7), names=("h", "w"), time=True) |
| plan = td.ScanPlan.from_list([("h", False), ("w", True), ("time", False)]) |
| model = td.LSTM(4, lattice=lat, plan=plan, mixer=td.testing.Recorder) |
| x = torch.randn(2, 5, 3, 7, 4) |
| out = model(x) |
|
|
| assert out.shape == x.shape |
| lengths = [m.calls[0].length for m in model.nd.mixers] |
| assert lengths == [3, 7, 5], f"layers swept axes of length {lengths}" |
| |
| assert model.nd.mixers[0].calls[0].lines == 2 * 5 * 7 |
| model.nd.mixers[0].reset() |
| assert model.nd.mixers[0].calls == [] |
|
|
|
|
| def test_recorder_leaves_the_data_alone(): |
| """It has to be the identity, or it cannot be dropped into a real model to |
| ask a question about that model.""" |
| lat = td.Lattice(shape=(4,), names=("a",)) |
| rec = td.testing.Recorder(6) |
| x = torch.randn(3, 4, 6) |
| assert torch.equal(rec(x), x) |
| assert lat.rank == 1 |
|
|
|
|
| def test_check_data_source_accepts_the_shipped_source(): |
| source = td.data.TensorSource( |
| torch.randn(9, 3, 4, 2), td.Lattice(shape=(3, 4), names=("h", "w")) |
| ) |
| report = td.testing.check_data_source(source) |
| assert report, str(report) |
|
|
|
|
| def test_check_data_source_catches_a_source_that_lies_about_its_shape(): |
| class Liar: |
| lattice = td.Lattice(shape=(3, 4), names=("h", "w")) |
|
|
| def __len__(self): |
| return 5 |
|
|
| def __getitem__(self, index): |
| return torch.randn(3, 9, 9, 2) |
|
|
| report = td.testing.check_data_source(Liar(), raise_on_failure=False) |
| assert not report |
| assert any("declared lattice" in r.name for r in report.failed) |
|
|
|
|
| def test_check_data_source_catches_a_source_that_cannot_reach_a_worker(): |
| """DEBUG.md #9: an unpicklable source does not raise under |
| DataLoader(num_workers>0) — it hangs. Finding it here is the whole point.""" |
|
|
| class Handle: |
| lattice = td.Lattice(shape=(2,), names=("a",)) |
|
|
| def __init__(self): |
| self.data = torch.randn(4, 2, 1) |
| self.lock = __import__("threading").Lock() |
|
|
| def __len__(self): |
| return 4 |
|
|
| def __getitem__(self, index): |
| return self.data[index] |
|
|
| report = td.testing.check_data_source(Handle(), raise_on_failure=False) |
| assert not report |
| assert any("worker" in r.name for r in report.failed) |
|
|