Skip to content

Commit 437443f

Browse files
committed
Add long-span recency regression
1 parent cfdbbb2 commit 437443f

3 files changed

Lines changed: 111 additions & 17 deletions

File tree

modal_test.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
# own container; everything else is batched together.
2626
# New test files are caught by the catch-all shard automatically.
2727

28-
_HEAVY_FILES: list[str] = []
28+
_HEAVY_FILES: list[str] = [
29+
"tests/compile/forward/test_most_recent_long_span.py",
30+
]
2931

3032
# Each inner list becomes one container. Splitting medium files across
3133
# multiple groups keeps any single shard from dominating wall time.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""Long-span regression for ``attend_most_recent_matching``.
2+
3+
Plan C guards the DOOM renderer's long recency reads. The important
4+
behavior is the compiled attention backend, not the small oracle
5+
``Attn.compute`` path: at 32,768 positions the oracle would build a
6+
large CPU attention matrix, while the compiled path uses the same SDPA
7+
backend the real graph uses.
8+
"""
9+
10+
import pytest
11+
import torch
12+
13+
from torchwright.compiler.device import get_device
14+
from torchwright.compiler.forward.compile import forward_compile
15+
from torchwright.ops.attention_ops import attend_most_recent_matching
16+
from torchwright.ops.inout_nodes import create_input, create_pos_encoding
17+
18+
MATCH_GAIN_LONG = 300_000.0
19+
SPANS = (8_500, 32_768)
20+
D = 64
21+
D_HEAD = 16
22+
23+
24+
@pytest.fixture(scope="module")
25+
def compiled_long_span_pick():
26+
device = get_device(verbose=False)
27+
if device.type != "cuda":
28+
pytest.skip("long-span recency regression requires the CUDA backend")
29+
30+
assert torch.get_default_dtype() == torch.float32
31+
assert torch.get_float32_matmul_precision() == "highest"
32+
assert not torch.backends.cuda.matmul.allow_tf32
33+
34+
pos = create_pos_encoding()
35+
query = create_input("query", 1, value_range=(0.0, 1.0))
36+
key = create_input("key", 1, value_range=(0.0, 1.0))
37+
value = create_input("value", 1, value_range=(0.0, float(max(SPANS))))
38+
out = attend_most_recent_matching(
39+
pos,
40+
query,
41+
key,
42+
value,
43+
match_gain=MATCH_GAIN_LONG,
44+
)
45+
46+
net = forward_compile(
47+
d=D,
48+
d_head=D_HEAD,
49+
output_node=out,
50+
pos_encoding=pos,
51+
verbose=False,
52+
max_layers=10,
53+
)
54+
assert net.device.type == "cuda"
55+
return net, out
56+
57+
58+
def _run_case(compiled_long_span_pick, n_pos: int, key: torch.Tensor) -> torch.Tensor:
59+
net, out = compiled_long_span_pick
60+
inputs = {
61+
"query": torch.ones(n_pos, 1),
62+
"key": key,
63+
"value": torch.arange(float(n_pos)).unsqueeze(1),
64+
}
65+
return net.compute(n_pos, inputs)[out].detach().cpu().squeeze(1)
66+
67+
68+
@pytest.mark.parametrize("n_pos", SPANS)
69+
def test_long_span_dense_adjacent_matches_pick_most_recent(
70+
compiled_long_span_pick,
71+
n_pos: int,
72+
) -> None:
73+
"""Dense adjacent matches still resolve the one-position recency gap."""
74+
result = _run_case(compiled_long_span_pick, n_pos, torch.ones(n_pos, 1))
75+
76+
for probe in (n_pos // 2, n_pos - 1):
77+
assert result[probe].item() == pytest.approx(float(probe), abs=0.05)
78+
79+
80+
@pytest.mark.parametrize("n_pos", SPANS)
81+
def test_long_span_sparse_match_beats_recent_non_matches(
82+
compiled_long_span_pick,
83+
n_pos: int,
84+
) -> None:
85+
"""At ``300_000`` gain, one old unit match beats newer non-matches."""
86+
match_pos = n_pos // 3
87+
key = torch.zeros(n_pos, 1)
88+
key[match_pos, 0] = 1.0
89+
90+
result = _run_case(compiled_long_span_pick, n_pos, key)
91+
92+
for probe in (match_pos, match_pos + 1, n_pos // 2, n_pos - 1):
93+
assert result[probe].item() == pytest.approx(float(match_pos), abs=0.05)

torchwright/ops/attention_ops.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,8 +1224,11 @@ def attend_most_recent_matching(
12241224
Quick sizing guide:
12251225
12261226
- Unit dot products (one-hot match, ``match_dot=1`` on match,
1227-
``0`` off) at ``max_n_pos ≈ 2000``: pick
1228-
``match_gain ≥ 20000 + margin`` (e.g. ``30000``).
1227+
``0`` off): the safe span is roughly
1228+
``match_gain / _QUERY_GAIN``. At ``match_gain = 300_000`` this
1229+
covers spans below ``37,500`` positions, including the DOOM
1230+
renderer's measured ~8,500-position rollout and the 32,768-position
1231+
regression test, but not a 65,536-position hard cap.
12291232
- 10×-scaled E8 codes (``match_dot=1600``, worst off-diagonal
12301233
``~800``) at ``max_n_pos ≈ 2000``: the ``800``-unit dot gap gives
12311234
plenty of headroom even at the default ``match_gain = 200``
@@ -1239,20 +1242,16 @@ def attend_most_recent_matching(
12391242
even dense matches resolve cleanly. Set ``assert_hardness_gt`` if
12401243
you need runtime enforcement.
12411244
1242-
**TF32 caveat.** On Ampere GPUs (A100), PyTorch's default matmul
1243-
path uses TF32 (~10-bit mantissa, ~1e-3 relative precision). When
1244-
``match_gain · match_dot`` is large and ``_QUERY_GAIN · max_n_pos``
1245-
is also large, the sum can exceed the regime where TF32 resolves
1246-
the recency-tiebreak gap cleanly. For example, at match_gain=20
1247-
with 10×-scaled E8 codes (match_dot=1600) and max_n_pos≈1000, the
1248-
peak logit is ~30000, where TF32 absolute precision ~30 eats the
1249-
`_QUERY_GAIN = 8` unit-position gap. If your callsite hits that
1250-
regime, either (a) use smaller match vectors so the match
1251-
contribution is ≲ a few thousand, or (b) switch to
1252-
:func:`attend_argmax_where` with an explicit integer score if one
1253-
is available at the callsite — its logits stay around
1254-
``_VALIDITY_DIRECT ≈ 1000`` where integer-score gaps resolve
1255-
cleanly on TF32. (M3 in the DOOM renderer chose option (b).)
1245+
**Precision policy.** Torchwright's current oracle path uses fp32
1246+
``torch.matmul`` / ``torch.softmax`` and the compiled path routes
1247+
SDPA through the MATH backend with fp32 tensors. PyTorch's default
1248+
CUDA TF32 matmul flag is also off in the supported environment.
1249+
Under that policy, ``match_gain = 300_000`` leaves ample precision
1250+
headroom for the 8-logit adjacent-position recency gap at the spans
1251+
above. If a future global precision setting enables TF32, re-run
1252+
the long-span regression before relying on this gain; TF32's
1253+
coarser mantissa can erase the 8-logit tiebreak when the total logit
1254+
is in the hundreds of thousands.
12561255
12571256
**When no position matches.** If no causal-window position has
12581257
``query_vector · key_vector`` above the unmatched baseline, the

0 commit comments

Comments
 (0)