|
| 1 | +"""Batched decode with non-empty past — the speculative-decoding shape. |
| 2 | +
|
| 3 | +When ``forward_cached`` is called with ``n_new > 1`` and a non-empty past |
| 4 | +(e.g. K+1 batched rows in a spec-decode step), each new row must: |
| 5 | +
|
| 6 | +1. Attend unconditionally to every past key — those are verified history. |
| 7 | +2. Attend causally among new rows — row ``i`` sees new rows ``0..i`` only. |
| 8 | +
|
| 9 | +A bug in either direction silently corrupts the rollout. This test pins |
| 10 | +both: drive a sequential per-row rollout to length L, then run one |
| 11 | +batched ``K+1``-row decode from past_len=L and check row-by-row that each |
| 12 | +batched-row output equals the corresponding sequential-step output. |
| 13 | +""" |
| 14 | + |
| 15 | +import pytest |
| 16 | +import torch |
| 17 | + |
| 18 | +from torchwright.compiler.forward.compile import forward_compile |
| 19 | + |
| 20 | +D = 1024 |
| 21 | +D_HEAD = 16 |
| 22 | + |
| 23 | + |
| 24 | +@pytest.fixture(scope="module") |
| 25 | +def compiled_calc(): |
| 26 | + from examples.calculator_v2 import create_network_parts |
| 27 | + |
| 28 | + output_node, pos_encoding, embedding = create_network_parts(1) |
| 29 | + net = forward_compile( |
| 30 | + d=D, |
| 31 | + d_head=D_HEAD, |
| 32 | + output_node=output_node, |
| 33 | + pos_encoding=pos_encoding, |
| 34 | + verbose=False, |
| 35 | + ) |
| 36 | + return net, output_node, embedding |
| 37 | + |
| 38 | + |
| 39 | +def _row_input(net, tokens, t): |
| 40 | + """Build the t-th row of the input residual stream for ``tokens``.""" |
| 41 | + full_res = net.get_input_res_stream(t + 1, {"embedding_input": tokens[: t + 1]}) |
| 42 | + return full_res[t : t + 1].to(net.device) |
| 43 | + |
| 44 | + |
| 45 | +def test_batched_decode_with_past_matches_sequential(compiled_calc): |
| 46 | + """K+1 batched rows from past_len=L equal L+1..L+K+1 sequential rows.""" |
| 47 | + net, _, _ = compiled_calc |
| 48 | + tokens = ["<bos"] + list("3+5+9\n") |
| 49 | + seed_len = 3 # past_len at the start of the batched step |
| 50 | + batch_size = len(tokens) - seed_len # K+1 rows in the batched step |
| 51 | + assert batch_size >= 2, "test requires n_new >= 2 to exercise the new mask" |
| 52 | + |
| 53 | + # Stage 1: sequential rollout up to seed_len, snapshot the past. |
| 54 | + kvs = None |
| 55 | + for t in range(seed_len): |
| 56 | + kvs, _ = _step_one(net, tokens, t, kvs) |
| 57 | + past_kvs_seed = [(K.clone(), V.clone()) for (K, V) in kvs] |
| 58 | + |
| 59 | + # Stage 2a: continue sequentially to capture per-row reference outputs. |
| 60 | + seq_outputs = [] |
| 61 | + kvs_seq = [(K.clone(), V.clone()) for (K, V) in past_kvs_seed] |
| 62 | + for t in range(seed_len, len(tokens)): |
| 63 | + kvs_seq, out = _step_one(net, tokens, t, kvs_seq) |
| 64 | + seq_outputs.append(out.detach().clone()) |
| 65 | + |
| 66 | + # Stage 2b: run the same suffix as one batched call from the snapshot. |
| 67 | + suffix_inp = torch.cat( |
| 68 | + [_row_input(net, tokens, t) for t in range(seed_len, len(tokens))], |
| 69 | + dim=0, |
| 70 | + ) |
| 71 | + batched_out, _ = net.forward_cached(suffix_inp, past_kvs_seed) |
| 72 | + |
| 73 | + # Bit-exact row-by-row equality. SDPA forces the MATH backend (see |
| 74 | + # _SDPA_BACKEND in components/attn.py), and the matmul shapes for a |
| 75 | + # given row are identical in the two paths once the mask is correct. |
| 76 | + for i, expected in enumerate(seq_outputs): |
| 77 | + actual = batched_out[i : i + 1] |
| 78 | + diff = (actual - expected).abs().max().item() |
| 79 | + assert diff == 0.0, ( |
| 80 | + f"row {i}: sequential vs batched diverged by {diff}; " |
| 81 | + f"expected bit-exact equality" |
| 82 | + ) |
| 83 | + |
| 84 | + |
| 85 | +def _step_one(net, tokens, t, kvs): |
| 86 | + """Run one cached single-row forward and return (new_kvs, output).""" |
| 87 | + new_inp = _row_input(net, tokens, t) |
| 88 | + out, new_kvs = net.forward_cached(new_inp, kvs) |
| 89 | + return new_kvs, out |
0 commit comments