| """Phase 6 acceptance: the kernel-family methods of multidimensionality. |
| |
| ``td.axial_attention`` and ``td.cafa`` as ``nd_method`` strategies, including |
| the hybrid form — kernels over the spatial axes, the model's own mixer along |
| time. The load-bearing property is causality along time: the kernels act |
| within a timestep, so a causal mixer must keep the whole hybrid causal, and a |
| CaFA kernel pooled across time would silently break exactly that. |
| """ |
|
|
| import pytest |
| import torch |
|
|
| import torch_dimensions as td |
|
|
| STRATEGIES = [td.axial_attention, td.cafa] |
|
|
|
|
| def _factory(strategy, **method_kwargs): |
| def build(lat, d_model, plan=None): |
| return td.LSTM(d_model, lat.n_axes, lat, nd_method=strategy, plan=plan, **method_kwargs) |
|
|
| return build |
|
|
|
|
| |
|
|
|
|
| @pytest.mark.parametrize("strategy", STRATEGIES) |
| def test_kernel_family_passes_every_applicable_check(strategy): |
| report = td.testing.check_block(_factory(strategy), ranks=(1, 2, 3), time=True) |
| assert report, str(report) |
|
|
|
|
| @pytest.mark.parametrize("strategy", STRATEGIES) |
| def test_kernel_family_with_leaky_relu_gate_passes_on_sparse(strategy): |
| """The CaFA paper's default gate is signed, which is exactly the case the |
| relative-cancellation guard exists for.""" |
| report = td.testing.check_block(_factory(strategy, gate="leaky_relu"), ranks=(2,), time=True) |
| assert report, str(report) |
|
|
|
|
| |
|
|
|
|
| @pytest.mark.parametrize("strategy", STRATEGIES) |
| def test_hybrid_is_causal_along_time(strategy): |
| """Spatial kernels act within a timestep and the LSTM is causal, so the |
| past must not see a perturbed future. For CaFA this is the test that |
| forbids pooling over time when building the kernel.""" |
| torch.manual_seed(0) |
| lat = td.Lattice(shape=(3, 4), names=("h", "w"), time=True) |
| model = td.LSTM(8, 3, lat, nd_method=strategy).double().eval() |
| x = torch.randn(2, 6, 3, 4, 8, dtype=torch.float64) |
| future = x.clone() |
| future[:, 4:] += 100.0 |
| a, b = model(x), model(future) |
| assert torch.equal(a[:, :4], b[:, :4]), "the past changed when the future did" |
| assert not torch.equal(a[:, 4:], b[:, 4:]) |
|
|
|
|
| |
|
|
|
|
| @pytest.mark.parametrize("strategy", STRATEGIES) |
| def test_kernel_family_learns_a_task_that_needs_axial_mixing(strategy): |
| result = td.testing.check_trainable(_factory(strategy), steps=150) |
| assert result["ratio"] >= 3.0, result |
|
|
|
|
| |
|
|
|
|
| def test_a_mixer_without_a_time_axis_is_refused(): |
| """On a purely spatial lattice the kernels are the whole model; accepting |
| a mixer would carry dead parameters that the user believes are running.""" |
| lat = td.Lattice(shape=(3, 4)) |
| with pytest.raises(ValueError, match="dead\\s*weight|no time axis"): |
| td.LSTM(8, 2, lat, nd_method=td.cafa) |
|
|
|
|
| def test_kernel_only_block_works_without_a_mixer(): |
| lat = td.Lattice(shape=(3, 4)) |
| block = td.AxialKernel( |
| mixer=None, plan=td.ScanPlan.cyclic(lat.axis_names, 2), lattice=lat, d_model=8 |
| ) |
| assert block(torch.randn(2, 3, 4, 8)).shape == (2, 3, 4, 8) |
|
|
|
|
| def test_the_methods_are_registered_by_name(): |
| assert td.resolve_nd_method("cafa") is td.cafa |
| assert td.resolve_nd_method("axial_attention") is td.axial_attention |
| assert td.ND_METHODS["axial_scan"] is td.axial_scan |
|
|
|
|
| def test_method_is_the_short_spelling_of_nd_method(): |
| lat = td.Lattice(shape=(2, 3), time=True) |
| model = td.S4D(8, 2, lat, method=td.cafa) |
| assert type(model.nd).__name__ == "AxialKernel" |
| with pytest.raises(ValueError, match="not both"): |
| td.S4D(8, 2, lat, method=td.cafa, nd_method=td.cafa) |
|
|
|
|
| def test_an_unknown_gate_is_refused(): |
| lat = td.Lattice(shape=(2, 3), time=True) |
| with pytest.raises(ValueError, match="gate"): |
| td.LSTM(8, 2, lat, nd_method=td.cafa, gate="sigmoid") |
|
|
|
|
| |
|
|
|
|
| def cafa_kernels(block, x): |
| """Run one CaFA layer's contractions and hand back the operators it used. |
| |
| This is the adapter shape `td.testing.check_block(kernels=...)` asks for, |
| and it is written here rather than in the library because it necessarily |
| knows a block's internals. |
| |
| Note which matrices come back: CaFA pools *the current activation*, so the |
| kernel for the second axis is built from the output of the first axis's |
| contraction. The Kronecker factors are therefore the ones actually applied, |
| not the ones a static reading of the module would predict — which is |
| exactly why the identity is worth testing rather than asserting. |
| """ |
| from torch_dimensions.compose.kernel import axial_contract |
|
|
| h = x |
| mats = [] |
| for j, axis in enumerate(block.spatial_axes): |
| kernel = block._kernel(0, j, axis, h) |
| lines = kernel.reshape(-1, kernel.shape[-2], kernel.shape[-1]) |
| assert torch.allclose(lines, lines[0].expand_as(lines)), ( |
| "pooled kernels differ between lines of the same batch element; " |
| "the joint-operator comparison assumes one operator per axis" |
| ) |
| mats.append(lines[0]) |
| h = axial_contract(h, block.lattice, axis, kernel) |
| return mats, h |
|
|
|
|
| def test_cafa_contraction_is_the_kronecker_product_it_claims_to_be(): |
| """The kernel family's central claim, checked at module level rather than |
| on hand-built matrices: contracting axis by axis equals applying the single |
| joint operator `A_0 ⊗ A_1 ⊗ …`. Until now this check was an unconditional |
| skip in the conformance report.""" |
|
|
| 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, |
| per_line=False, |
| norm=False, |
| residual=False, |
| ) |
|
|
| report = td.testing.check_block(build, ranks=(2, 3), kernels=cafa_kernels, d_model=4) |
| assert report, str(report) |
| assert ( |
| td.testing.check_block(build, ranks=(2,), kernels=cafa_kernels, raise_on_failure=False) |
| .results[3] |
| .status |
| == "pass" |
| ) |
|
|
|
|
| def test_the_kronecker_check_catches_a_contraction_that_is_not_a_product(): |
| """Negative control: an adapter that reports the wrong operators must fail |
| the check. A conformance check that has never failed is a comment.""" |
|
|
| 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, |
| per_line=False, |
| norm=False, |
| residual=False, |
| ) |
|
|
| def wrong(block, x): |
| mats, out = cafa_kernels(block, x) |
| return [m * 1.5 for m in mats], out |
|
|
| report = td.testing.check_block( |
| build, ranks=(2,), kernels=wrong, d_model=4, raise_on_failure=False |
| ) |
| assert not report |
| assert "Kronecker" in report.failed[0].name |
|
|
|
|
| |
|
|
|
|
| def test_the_new_kernel_options_default_to_the_old_behaviour(): |
| """Both are off by default, so every existing model and checkpoint is |
| numerically unchanged by their arrival.""" |
| lat = td.Lattice(shape=(4, 5), names=("h", "w"), time=True) |
| x = torch.randn(2, 3, 4, 5, 8, dtype=torch.float64) |
| torch.manual_seed(0) |
| plain = td.LSTM(8, 3, lat, method=td.cafa).double().eval() |
| torch.manual_seed(0) |
| explicit = ( |
| td.LSTM(8, 3, lat, method=td.cafa, qk_norm=False, kernel_residual=False).double().eval() |
| ) |
| assert torch.equal(plain(x), explicit(x)) |
|
|
|
|
| @pytest.mark.parametrize("strategy", [td.cafa, td.axial_attention]) |
| def test_qk_norm_changes_the_kernel_and_keeps_the_block_working(strategy): |
| lat = td.Lattice(shape=(4, 5), names=("h", "w"), time=True) |
| x = torch.randn(2, 3, 4, 5, 8, dtype=torch.float64) |
| torch.manual_seed(0) |
| off = td.LSTM(8, 3, lat, method=strategy).double().eval() |
| torch.manual_seed(0) |
| on = td.LSTM(8, 3, lat, method=strategy, qk_norm=True).double().eval() |
| assert not torch.allclose(off(x), on(x)) |
| assert on(x).shape == x.shape |
| |
| assert sum(p.numel() for p in off.parameters()) == sum(p.numel() for p in on.parameters()) |
|
|
|
|
| def test_the_kernel_residual_starts_the_contraction_near_the_identity(): |
| """CaFA's `K + gamma*I`. The point is the inductive bias: with the residual |
| a freshly built contraction is closer to leaving each cell alone than to |
| averaging the axis, and it has to learn to mix.""" |
| lat = td.Lattice(shape=(6,), names=("h",)) |
| block = td.AxialKernel( |
| mixer=None, |
| plan=td.ScanPlan.cyclic(("h",), 1), |
| lattice=lat, |
| d_model=8, |
| gate="softmax", |
| kernel_residual=True, |
| norm=False, |
| residual=False, |
| ).double() |
| with torch.no_grad(): |
| block.gamma[0].fill_(50.0) |
| h = torch.randn(1, 6, 8, dtype=torch.float64) |
| kernel = block._kernel(0, 0, 0, h) |
| eye = torch.eye(6, dtype=torch.float64) |
| assert (kernel - eye).abs().max().item() < 1e-6, kernel |
|
|
|
|
| def test_the_kernel_residual_is_added_before_the_gate(): |
| """Order matters and is not a detail: softmax is not additive, so adding |
| the identity after it would be a different operator (and would break the |
| rows-sum-to-one property the softmax gate exists for).""" |
| lat = td.Lattice(shape=(5,), names=("h",)) |
| block = td.AxialKernel( |
| mixer=None, |
| plan=td.ScanPlan.cyclic(("h",), 1), |
| lattice=lat, |
| d_model=8, |
| gate="softmax", |
| kernel_residual=True, |
| norm=False, |
| residual=False, |
| ).double() |
| with torch.no_grad(): |
| block.gamma[0].fill_(3.0) |
| kernel = block._kernel(0, 0, 0, torch.randn(1, 5, 8, dtype=torch.float64)) |
| rows = kernel.sum(-1) |
| assert torch.allclose(rows, torch.ones_like(rows), atol=1e-12), rows |
|
|
|
|
| @pytest.mark.parametrize("kw", [{"qk_norm": True}, {"kernel_residual": True}]) |
| def test_the_new_options_pass_conformance_and_learn(kw): |
| def factory(lat, d_model, plan=None): |
| return td.LSTM(d_model, lat.n_axes, lat, plan=plan, method=td.cafa, **kw) |
|
|
| report = td.testing.check_block(factory, ranks=(1, 2, 3), time=True) |
| assert report, str(report) |
| stats = td.testing.check_trainable( |
| lambda lat, d: td.LSTM(d, 4, lat, method=td.cafa, **kw), steps=150 |
| ) |
| assert stats["ratio"] > 3.0, stats |
|
|