Download tests/test_sparse_cmix.py from Hakureirm/rwkv7-kernel: direct link, hf CLI and curl.
- Browser
- Download file 17.8 kB
-
https://huggingface.co/Hakureirm/rwkv7-kernel/resolve/main/tests/test_sparse_cmix.py
- Command line
-
hf download hf://Hakureirm/rwkv7-kernel/tests/test_sparse_cmix.py
-
curl -L -o test_sparse_cmix.py https://huggingface.co/Hakureirm/rwkv7-kernel/resolve/main/tests/test_sparse_cmix.py
17.8 kB
| """The fused projection must agree with the dense one it replaces. | |
| Skipping a zero row is exact, so the only difference allowed is the fp32 atomicAdd | |
| combine's arrival order — ~1 ULP, the split-K GEMM class. The tests therefore compare | |
| against a dense fp32 reference rather than against fp16 `linear`, and check that the | |
| fused path is no further from the truth than the dense path is. | |
| """ | |
| import pytest | |
| import torch | |
| cuda = pytest.mark.skipif(not torch.cuda.is_available(), reason="needs CUDA") | |
| def _acc(hidden, device="cuda"): | |
| """The caller-owned fp32 accumulator the op now writes into, one per module.""" | |
| return torch.zeros(hidden, device=device, dtype=torch.float32) | |
| def _sparse_activation(inter, density, device, generator): | |
| """A squared-ReLU activation: mostly exact zeros, the rest strictly positive.""" | |
| raw = torch.randn(inter, device=device, generator=generator, dtype=torch.float16) | |
| keep = torch.rand(inter, device=device, generator=generator) < density | |
| return torch.where(keep, raw.abs(), torch.zeros_like(raw)) | |
| def test_matches_dense_reference(density): | |
| from rwkv7_kernel._ext import load_extension | |
| from rwkv7_kernel.sparse_ffn import _tile_value_weight | |
| load_extension() | |
| torch.manual_seed(0) | |
| g = torch.Generator(device="cuda").manual_seed(0) | |
| hidden, inter = 512, 1024 | |
| weight = (torch.randn(hidden, inter, device="cuda", generator=g, dtype=torch.float16) * 0.02) | |
| act = _sparse_activation(inter, density, "cuda", g) | |
| got = torch.ops.rwkv7_kernel.sparse_cmix(act, _tile_value_weight(weight), _acc(hidden)) | |
| dense = torch.nn.functional.linear(act, weight) | |
| truth = torch.nn.functional.linear(act.float(), weight.float()) | |
| fused_err = (got.float().squeeze(0) - truth).abs().max() | |
| dense_err = (dense.float() - truth).abs().max() | |
| # Not "fused == dense" -- neither is exact in fp16. The bar is that swapping in | |
| # the kernel does not move the answer further from the fp32 truth than the | |
| # implementation it replaces already is. | |
| assert fused_err <= dense_err * 1.5 + 1e-4, (fused_err.item(), dense_err.item()) | |
| def test_all_zero_activation_gives_zero(): | |
| """The density-0 case has no rows to accumulate; the accumulator must still be | |
| clean, which is only true if the previous call re-zeroed it.""" | |
| from rwkv7_kernel._ext import load_extension | |
| from rwkv7_kernel.sparse_ffn import _tile_value_weight | |
| load_extension() | |
| g = torch.Generator(device="cuda").manual_seed(1) | |
| hidden, inter = 512, 1024 | |
| weight = torch.randn(hidden, inter, device="cuda", generator=g, dtype=torch.float16) | |
| tiled = _tile_value_weight(weight) | |
| acc = _acc(hidden) | |
| torch.ops.rwkv7_kernel.sparse_cmix(_sparse_activation(inter, 0.5, "cuda", g), tiled, acc) | |
| zeros = torch.zeros(inter, device="cuda", dtype=torch.float16) | |
| got = torch.ops.rwkv7_kernel.sparse_cmix(zeros, tiled, acc) | |
| assert torch.equal(got, torch.zeros_like(got)), got.abs().max().item() | |
| def test_repeated_calls_do_not_accumulate(): | |
| """The persistent accumulator is reused across layers and steps. If the finalize | |
| pass failed to re-zero it, the second identical call would return roughly twice | |
| the first -- which no correctness test on a single call would notice.""" | |
| from rwkv7_kernel._ext import load_extension | |
| from rwkv7_kernel.sparse_ffn import _tile_value_weight | |
| load_extension() | |
| g = torch.Generator(device="cuda").manual_seed(2) | |
| hidden, inter = 512, 1024 | |
| weight = (torch.randn(hidden, inter, device="cuda", generator=g, dtype=torch.float16) * 0.02) | |
| tiled = _tile_value_weight(weight) | |
| acc = _acc(hidden) | |
| act = _sparse_activation(inter, 0.0667, "cuda", g) | |
| first = torch.ops.rwkv7_kernel.sparse_cmix(act, tiled, acc).clone() | |
| for _ in range(5): | |
| again = torch.ops.rwkv7_kernel.sparse_cmix(act, tiled, acc) | |
| torch.testing.assert_close(first, again, rtol=0, atol=0) | |
| def test_tiled_weight_follows_weight_updates(): | |
| """A cached tile that survives a weight edit projects with the old weights and | |
| says nothing about it.""" | |
| from rwkv7_kernel.sparse_ffn import _tiled_weight | |
| hidden, inter = 512, 1024 | |
| module = torch.nn.Module() | |
| module.value = torch.nn.Linear(inter, hidden, bias=False).half().cuda() | |
| before = _tiled_weight(module).clone() | |
| with torch.no_grad(): | |
| module.value.weight.mul_(2.0) | |
| after = _tiled_weight(module) | |
| torch.testing.assert_close(after, before * 2, rtol=1e-3, atol=1e-3) | |
| def test_two_modules_do_not_share_an_accumulator(): | |
| """The bug that made the accumulator caller-owned. | |
| It used to be a process-wide map keyed on (device, hidden), so two modules of the | |
| same width shared one buffer. On a single stream that is still correct -- kernel | |
| completion orders the reuse -- and off it the partial sums interleave and the | |
| logits come back wrong with no NaN and no error. Two modules of equal width must | |
| now hold two distinct buffers; the same object would put the old hazard back. | |
| """ | |
| from rwkv7_kernel.sparse_ffn import _tiled_weight | |
| hidden, inter = 512, 1024 | |
| modules = [] | |
| for _ in range(2): | |
| m = torch.nn.Module() | |
| m.value = torch.nn.Linear(inter, hidden, bias=False).half().cuda() | |
| _tiled_weight(m) | |
| modules.append(m) | |
| a, b = (m._rwkv_hf_accumulator for m in modules) | |
| assert a.data_ptr() != b.data_ptr() | |
| assert a.shape == (hidden,) and a.dtype == torch.float32 | |
| def test_mismatched_devices_are_refused(): | |
| """A device_map split would otherwise launch on one GPU with another's pointers.""" | |
| if torch.cuda.device_count() < 2: | |
| pytest.skip("needs two CUDA devices") | |
| from rwkv7_kernel._ext import load_extension | |
| from rwkv7_kernel.sparse_ffn import _tile_value_weight | |
| load_extension() | |
| hidden, inter = 512, 1024 | |
| weight = torch.randn(hidden, inter, device="cuda:0", dtype=torch.float16) * 0.02 | |
| act = torch.zeros(inter, device="cuda:1", dtype=torch.float16) | |
| with pytest.raises(RuntimeError, match="share a device"): | |
| torch.ops.rwkv7_kernel.sparse_cmix(act, _tile_value_weight(weight), _acc(hidden, "cuda:0")) | |
| def test_grad_enabled_forward_is_refused_rather_than_silently_ungraded(): | |
| """There is no backward, so a grad-enabled forward used to return a value whose | |
| gradient never arrives -- `.backward()` succeeds and the input grad is None.""" | |
| from rwkv7_kernel.sparse_ffn import sparse_project | |
| hidden, inter = 512, 1024 | |
| module = torch.nn.Module() | |
| module.value = torch.nn.Linear(inter, hidden, bias=False).half().cuda() | |
| act = torch.zeros(1, 1, inter, device="cuda", dtype=torch.float16, requires_grad=True) | |
| with pytest.raises(RuntimeError, match="no backward"): | |
| sparse_project(module, act) | |
| with torch.no_grad(): # the documented way through | |
| sparse_project(module, act) | |
| def test_register_with_a_model_it_cannot_pre_tile_raises(): | |
| """Passing a model means "build the tiles now". Finding none used to return | |
| quietly, costing the caller the CUDA-graph cliff the eager build exists to avoid | |
| with nothing said. Runs on CPU: the argument check now fires before load_extension().""" | |
| from rwkv7_kernel import sparse_ffn | |
| class NotAnRwkv(torch.nn.Module): | |
| pass | |
| with pytest.raises(ValueError, match="no blocks to pre-tile"): | |
| sparse_ffn.register(NotAnRwkv()) | |
| def test_invalidate_forces_a_rebuild(): | |
| """The escape hatch for the edits the fingerprint cannot see.""" | |
| from rwkv7_kernel.sparse_ffn import invalidate | |
| module = torch.nn.Module() | |
| module._rwkv_hf_tiled_fp = ("pretend a tile was built",) | |
| invalidate(module) | |
| assert module._rwkv_hf_tiled_fp is None | |
| def test_register_that_raises_leaves_the_class_untouched(): | |
| """The raise above exists to guarantee this, and nothing was checking it. | |
| `register` promises that a rejected call "leaves nothing half-applied". The | |
| existing test asserts the exception and stops there, so the promise the exception | |
| is there to keep was never exercised: an implementation that patched the class | |
| first and validated second would pass it. | |
| """ | |
| from rwkv7_kernel import sparse_ffn | |
| # Guard on the module the import below needs; `transformers` alone is satisfied by a | |
| # stock install that has no RWKV-7, and the test then errors instead of skipping. | |
| pytest.importorskip("transformers.models.rwkv7") | |
| from transformers.models.rwkv7.modeling_rwkv7 import Rwkv7FeedForward | |
| before = Rwkv7FeedForward._project | |
| with pytest.raises(ValueError, match="no blocks to pre-tile"): | |
| sparse_ffn.register(torch.nn.Module()) | |
| assert Rwkv7FeedForward._project is before | |
| assert sparse_ffn._ORIGINAL_PROJECT is None | |
| def test_unregister_restores_the_projection_and_is_idempotent(): | |
| """`unregister` had no test at all, on either of its two paths. | |
| Driven by setting `_ORIGINAL_PROJECT` directly rather than by calling `register`, | |
| which would pull in the CUDA extension; the restore path is plain Python and this | |
| is how `test_invalidate_forces_a_rebuild` already exercises its own state. | |
| """ | |
| from rwkv7_kernel import sparse_ffn | |
| pytest.importorskip("transformers.models.rwkv7") | |
| from transformers.models.rwkv7.modeling_rwkv7 import Rwkv7FeedForward | |
| original = Rwkv7FeedForward._project | |
| try: | |
| sparse_ffn._ORIGINAL_PROJECT = original | |
| Rwkv7FeedForward._project = sparse_ffn.sparse_project | |
| sparse_ffn.unregister() | |
| assert Rwkv7FeedForward._project is original | |
| assert sparse_ffn._ORIGINAL_PROJECT is None | |
| # Calling it again must not re-restore a stale function or raise. This is the | |
| # branch that returns early, and it is the one a caller hits by unregistering | |
| # in a `finally` that also runs on the path where register never happened. | |
| sparse_ffn.unregister() | |
| assert Rwkv7FeedForward._project is original | |
| finally: | |
| Rwkv7FeedForward._project = original | |
| sparse_ffn._ORIGINAL_PROJECT = None | |
| def test_a_non_conforming_input_takes_the_dense_path_and_builds_no_tile(): | |
| """The fallback branch was uncovered, and it is the one most callers hit first. | |
| Bit-exact against `linear` rather than close to it: the branch does not | |
| approximate the dense projection, it *is* the dense projection, so any tolerance | |
| would also pass an implementation that quietly ran the kernel. The tile assertion | |
| is what actually pins the branch -- a conforming call would have built one. | |
| """ | |
| from rwkv7_kernel.sparse_ffn import sparse_project | |
| hidden, inter = 256, 512 | |
| module = torch.nn.Module() | |
| module.value = torch.nn.Linear(inter, hidden, bias=False) | |
| act = torch.randn(1, 1, inter) # CPU, fp32: non-conforming on two counts | |
| got = sparse_project(module, act) | |
| torch.testing.assert_close(got, torch.nn.functional.linear(act, module.value.weight), rtol=0, atol=0) | |
| assert getattr(module, "_rwkv_hf_tiled_fp", None) is None | |
| assert not hasattr(module, "_rwkv_hf_tiled") | |
| class _FakeAdapterWrapper(torch.nn.Module): | |
| """Minimal stand-in for what PEFT's `lora.Linear` actually looks like from the | |
| outside: a `.weight` attribute that proxies to a frozen base layer (so shape | |
| checks upstream still work), plus a `forward()` that adds something the base | |
| layer alone does not compute. Not importing peft here on purpose -- the bug | |
| this guards is about *any* module with its own forward() sitting where a bare | |
| nn.Linear was assumed, not specifically about LoRA's internals. | |
| """ | |
| def __init__(self, base: torch.nn.Linear, delta: torch.nn.Linear): | |
| super().__init__() | |
| self.base = base | |
| self.delta = delta | |
| def weight(self): | |
| return self.base.weight | |
| def forward(self, x): | |
| return self.base(x) + self.delta(x) | |
| def test_a_wrapped_value_module_goes_through_its_own_forward_not_raw_weight(): | |
| """Regression: an earlier version of both fallback branches did | |
| `F.linear(activated, module.value.weight)`, which reaches past whatever | |
| `module.value` actually is straight to the frozen base weight. That is exactly | |
| what `Rwkv7FeedForward._project` (`return self.value(inner)`) does not do, so it | |
| silently stopped being a drop-in the moment `module.value` was anything other | |
| than a bare `nn.Linear` -- confirmed against a real PEFT LoRA adapter on | |
| `ffn.value`: `lora_A`/`lora_B` came back with `grad=None` (never entered the | |
| graph at all) on every training-shaped call, because `_conforms` requires | |
| single-token input and a normal training forward never qualifies. This uses a | |
| hand-rolled wrapper instead of peft so the test has no extra dependency, but the | |
| property under test -- "does the module's own forward() get called" -- is the | |
| same one that broke. | |
| """ | |
| from rwkv7_kernel.sparse_ffn import sparse_project | |
| hidden, inter = 256, 512 | |
| base = torch.nn.Linear(inter, hidden, bias=False) | |
| base.weight.requires_grad_(False) # PEFT freezes the base layer; mirror that | |
| delta = torch.nn.Linear(inter, hidden, bias=False) | |
| module = torch.nn.Module() | |
| module.value = _FakeAdapterWrapper(base, delta) | |
| act = torch.randn(1, 1, inter, requires_grad=True) # non-conforming: CPU + fp32 | |
| got = sparse_project(module, act) | |
| expected = module.value(act) # what the original _project would have produced | |
| torch.testing.assert_close(got, expected, rtol=0, atol=0) | |
| assert not torch.allclose(got, torch.nn.functional.linear(act, base.weight)), ( | |
| "the fallback produced the same answer as skipping the wrapper entirely -- " | |
| "delta's contribution did not survive, so this test is not actually checking " | |
| "what it claims to" | |
| ) | |
| got.sum().backward() | |
| assert delta.weight.grad is not None and delta.weight.grad.abs().sum() > 0, ( | |
| "the wrapper's own trainable parameter got no gradient -- the bug this " | |
| "guards against is back" | |
| ) | |
| def test_conforming_shape_also_declines_a_wrapped_value_module(): | |
| """The fused-kernel path itself reads `module.value.weight` directly too (for | |
| tiling), which for a wrapped module is the unadapted base weight -- silently | |
| wrong under `torch.no_grad()` single-token inference with an unmerged adapter, | |
| not just a training-time gradient bug. A wrapped module must decline the fused | |
| path even when the shape/dtype would otherwise conform, and take the module's | |
| own forward() instead, same as the non-conforming branch.""" | |
| from rwkv7_kernel.sparse_ffn import sparse_project | |
| hidden, inter = 256, 512 | |
| base = torch.nn.Linear(inter, hidden, bias=False).half().cuda() | |
| base.weight.requires_grad_(False) | |
| delta = torch.nn.Linear(inter, hidden, bias=False).half().cuda() | |
| module = torch.nn.Module() | |
| module.value = _FakeAdapterWrapper(base, delta) | |
| act = torch.zeros(1, 1, inter, device="cuda", dtype=torch.float16) # conforming shape/dtype | |
| with torch.no_grad(): | |
| got = sparse_project(module, act) | |
| expected = module.value(act) | |
| torch.testing.assert_close(got, expected, rtol=0, atol=0) | |
| assert getattr(module, "_rwkv_hf_tiled_fp", None) is None, ( | |
| "a tile got built for a wrapped module -- the fused path should have " | |
| "declined before ever reaching _tiled_weight" | |
| ) | |
| def test_each_non_conforming_reason_falls_back_on_its_own(reason, shape, dtype): | |
| """On CPU every input is non-conforming for the same reason, so the individual | |
| conditions can only be separated on a device where the others are satisfied.""" | |
| from rwkv7_kernel.sparse_ffn import sparse_project | |
| hidden, inter = 512, 1024 | |
| module = torch.nn.Module() | |
| module.value = torch.nn.Linear(inter, hidden, bias=False).to("cuda", dtype) | |
| act = torch.randn(*shape, inter, device="cuda", dtype=dtype) | |
| with torch.no_grad(): | |
| got = sparse_project(module, act) | |
| want = torch.nn.functional.linear(act, module.value.weight) | |
| torch.testing.assert_close(got, want, rtol=0, atol=0), reason | |
| assert getattr(module, "_rwkv_hf_tiled_fp", None) is None, reason | |
| def test_a_non_default_tile_size_is_read_and_the_layout_still_inverts(monkeypatch): | |
| """No tile size other than the default was covered anywhere. | |
| This pins the Python half only: `_tiles` reading the environment at call time, | |
| and `_tile_value_weight` staying a pure permutation at that size. It deliberately | |
| does *not* claim the kernel agrees -- the tile shape is a compile-time constant | |
| on that side, so an env var changed after the extension is built gives the two | |
| halves different constants, which is exactly why `_tiles` reads late. Checking | |
| kernel agreement at a second size means rebuilding the extension, which belongs | |
| in a build matrix rather than here. | |
| """ | |
| from rwkv7_kernel.sparse_ffn import _tile_value_weight, _tiles | |
| monkeypatch.setenv("RWKV_HF_KERNELS_C_TILE", "128") | |
| tile_h, tile_i = _tiles() | |
| assert (tile_h, tile_i) == (128, 64) | |
| hidden, inter = 256, 128 | |
| weight = torch.randn(hidden, inter) | |
| tiled = _tile_value_weight(weight) | |
| assert tiled.shape == (inter // tile_i, hidden // tile_h, tile_i, tile_h) | |
| # A permutation loses nothing: undo it and the original must come back exactly. | |
| back = tiled.permute(0, 2, 1, 3).reshape(inter, hidden).t() | |
| torch.testing.assert_close(back, weight, rtol=0, atol=0) | |