File size: 11,182 Bytes
bcc8d35 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 | """Backend probe/select/benchmark guards, both directions.
Run: uv run pytest tests/test_attention_backends.py -v -m "not gpu"
Every test here runs the probe on device="cpu" on purpose: the CPU suite must
not initialise a CUDA context on a shared box, and the selection LOGIC is
device-independent. The device-specific numbers are the gpu-marked benchmark.
"""
from __future__ import annotations
import math
import os
import sys
import pytest
from align.model import attention as attn
from align.model.attention import (
TRITON_FA2_GATE_MODULE,
BackendReport,
benchmark_backends,
probe_backends,
select_attn_implementation,
)
from align.model.gates import load_sdpa_baseline
@pytest.fixture
def no_triton_kernel():
"""Make section-06's gate module unimportable for the duration of a test.
The 'kernel absent' branch is a permanent code path -- it is what any fresh
checkout, and any box where the Triton build fails, actually hits -- so it
is tested by BLOCKING the import rather than by relying on section-06 not
having landed yet. A test that silently stops testing anything the day a
sibling lane ships is worse than no test.
"""
class _Block:
def find_spec(self, name, path=None, target=None):
if name == TRITON_FA2_GATE_MODULE:
raise ImportError(f"blocked by test fixture: {name}")
return None
blocker = _Block()
saved = sys.modules.pop(TRITON_FA2_GATE_MODULE, None)
sys.meta_path.insert(0, blocker)
try:
yield
finally:
sys.meta_path.remove(blocker)
if saved is not None:
sys.modules[TRITON_FA2_GATE_MODULE] = saved
@pytest.fixture
def gate_dir(tmp_workdir):
"""A scratch equivalence-gate directory, so these tests never read -- or
write -- the real artifact under .cache/kernel_gates."""
return os.path.join(tmp_workdir, "kernel_gates")
def _write_passing_gate(gate_dir):
"""Write a REAL gate artifact for the current kernel source and
environment, using section-06's own writer. Stubbing a boolean here would
test my mock; this tests the contract the two sections actually share."""
from align.kernels import gate as gate_mod
gate_mod.write_equivalence_gate(
tolerances={"out": 1e-3},
shapes_covered=["backend-selection test"],
gate_dir=gate_dir,
)
def _reports(device="cpu", gate_dir=None):
return {r.name: r for r in probe_backends(device=device, gate_dir=gate_dir)}
def test_probe_reports_every_backend_with_a_reason(gate_dir):
"""A probe that omits a backend is indistinguishable from one that found it
unavailable. Every declared backend gets a row, and every row carries a
non-empty reason -- the reason string is what ends up in the launch banner
when a run cannot use the accelerator someone expected."""
reports = _reports(gate_dir=gate_dir)
assert set(reports) == set(attn.ALL_BACKENDS)
for name, r in reports.items():
assert isinstance(r, BackendReport)
assert r.reason.strip(), name
def test_select_triton_fa2_raises_before_equivalence_gate(gate_dir):
"""select_attn_implementation("triton_fa2") RAISES while the kernel's
SDPA-equivalence gate (section-06) has not passed. It must never silently
fall back -- a run must not claim an accelerator it never used. (Both
directions: with a valid gate artifact it returns the backend.)"""
# direction 1: no gate artifact -> refuse
with pytest.raises(RuntimeError, match="equivalence gate"):
select_attn_implementation("triton_fa2", device="cpu", gate_dir=gate_dir)
# direction 2: a gate artifact for this kernel source and environment
_write_passing_gate(gate_dir)
assert select_attn_implementation("triton_fa2", device="cpu", gate_dir=gate_dir) == "triton_fa2"
def test_select_triton_fa2_raises_when_the_kernel_is_absent(no_triton_kernel):
"""The fresh-checkout path: section-06's module is not importable at all.
That is a different branch from 'gate not passed' and it must also refuse
rather than fall back."""
with pytest.raises(RuntimeError, match="not yet implemented"):
select_attn_implementation("triton_fa2", device="cpu")
def test_select_triton_fa2_refuses_a_gate_that_no_longer_matches_the_kernel(gate_dir):
"""THE STALE-GATE INCIDENT, at the selector: an artifact written before a
kernel edit would bless code that was never run against SDPA. The gate
pins the kernel source hash; the selector must surface that refusal rather
than treat 'an artifact exists' as availability."""
from align.kernels import gate as gate_mod
_write_passing_gate(gate_dir)
assert select_attn_implementation("triton_fa2", device="cpu", gate_dir=gate_dir) == "triton_fa2"
original = gate_mod.kernel_source_hash
gate_mod.kernel_source_hash = lambda: "0" * 64
try:
with pytest.raises(RuntimeError, match="kernel source changed"):
select_attn_implementation("triton_fa2", device="cpu", gate_dir=gate_dir)
finally:
gate_mod.kernel_source_hash = original
def test_triton_fa2_is_never_auto_selected(gate_dir):
"""Even with the gate passed, auto-selection stays on sdpa. A custom
kernel is opted INTO explicitly so the banner records a deliberate choice;
a kernel that switches itself on turns every later benchmark into a
comparison against an unknown baseline."""
_write_passing_gate(gate_dir)
assert select_attn_implementation(None, device="cpu", gate_dir=gate_dir) == "sdpa"
def test_select_unavailable_backend_raises_with_reason(gate_dir):
"""Explicitly requesting any unavailable backend raises RuntimeError
carrying the probe's reason string; auto-selection (requested=None) picks
the best available and logs which."""
reports = _reports(gate_dir=gate_dir)
unavailable = [n for n, r in reports.items() if not r.available]
assert unavailable, "probe found nothing unavailable -- it is not probing"
for name in unavailable:
with pytest.raises(RuntimeError) as exc:
select_attn_implementation(name, device="cpu", gate_dir=gate_dir)
# the probe's own reason must survive into the error the user sees
assert reports[name].reason[:40] in str(exc.value), name
assert select_attn_implementation(None, device="cpu", gate_dir=gate_dir) == "sdpa"
def test_flashmla_refused_even_if_it_were_available():
"""FlashMLA is refused for structural reasons, not availability ones: its
kernel requires head_dim_v=512 MLA tensors and Mistral is GQA head_dim
128, so no GPU -- Hopper included -- can serve this model with it. The
refusal must not be reachable-around by a future SM90 box."""
with pytest.raises(RuntimeError, match="head_dim"):
select_attn_implementation("flashmla", device="cpu")
def test_select_rejects_an_unknown_backend_name():
"""A typo'd backend name must not fall through to the default. The
predecessor's whole failure class is a plausible substitute quietly
standing in for what was asked for."""
with pytest.raises(RuntimeError, match="unknown backend"):
select_attn_implementation("sdpa_flash", device="cpu")
def test_benchmark_records_nan_not_zero_for_unusable_backend():
"""A backend that throws at these shapes records float('nan'), never 0.0
and never a swallowed pass (the EFFICIENT-on-GQA incident: NaN is a real
finding, recorded not hidden). Undefined stats are NaN, never 0.
Run on CPU at tiny shapes, where EFFICIENT_ATTENTION and CUDNN_ATTENTION
genuinely have no kernel -- the same shape of unavailability the pinned
A10G baseline recorded for EFFICIENT on GQA inputs.
"""
results = benchmark_backends(
batch=1,
seq=16,
n_heads=4,
n_kv_heads=2,
head_dim=16,
iters=2,
device="cpu",
)
assert set(results) >= {"FLASH_ATTENTION", "EFFICIENT_ATTENTION", "MATH"}
unusable = [k for k, v in results.items() if math.isnan(v)]
assert unusable, "no backend was unusable at CPU tiny shapes -- expected at least one"
for name, ms in results.items():
assert ms != 0.0, f"{name} recorded 0.0; an unusable backend is NaN, never 0"
assert math.isnan(ms) or ms > 0.0, f"{name}={ms}"
def test_benchmark_does_not_swallow_the_reason(monkeypatch):
"""The exception a backend raises is a finding. It is logged and attached
to the result, not discarded -- 'NaN' with no reason cannot be triaged
six weeks later when a torch upgrade changes which kernels exist."""
results, reasons = attn.benchmark_backends_with_reasons(
batch=1,
seq=16,
n_heads=4,
n_kv_heads=2,
head_dim=16,
iters=2,
device="cpu",
)
for name, ms in results.items():
if math.isnan(ms):
assert reasons[name] and reasons[name] != "ok", name
else:
assert reasons[name] == "ok", name
def test_pinned_sdpa_baseline_is_the_bar_and_records_nan():
"""The measured A10G baseline is pinned in-repo so section-06's 'beat the
bar' comparison and a torch/driver regression of the SDPA path itself are
both detectable. EFFICIENT is NaN in the pinned artifact for the same
reason it is NaN at runtime -- that is the recorded finding, not a hole."""
base = load_sdpa_baseline()
assert set(base) == {"512", "1024", "2048"}
for seq, row in base.items():
assert row["FLASH_ATTENTION"] > 0.0
assert math.isnan(row["EFFICIENT_ATTENTION"]), (
"EFFICIENT must be NaN, not 0 and not absent, in the pinned baseline"
)
# flash must beat math by a wide margin or the pin is of the wrong run
assert row["MATH"] > 10 * row["FLASH_ATTENTION"], seq
def test_banner_states_which_backend_actually_ran():
"""Every training/inference job's banner reports the backend that was
resolved, so 'we ran the custom kernel' is a recorded fact rather than an
intention. attention_backend is already a mandatory banner key; this test
pins that the selector's output is what fills it."""
from align.registry import MANDATORY_BANNER_KEYS
assert "attention_backend" in MANDATORY_BANNER_KEYS
resolved = select_attn_implementation(None, device="cpu")
assert attn.banner_value(resolved, device="cpu").startswith(resolved)
@pytest.mark.gpu
def test_benchmark_reproduces_the_pinned_a10g_baseline():
"""On a real A10G, re-running the benchmark at the pinned shapes must land
within 2x of the recorded numbers and must still record EFFICIENT as NaN.
A drastic regression of the SDPA path (torch upgrade, driver change) is
exactly what this catches."""
import torch
if not torch.cuda.is_available():
pytest.skip("no CUDA device visible")
base = load_sdpa_baseline()
for seq in (512, 1024, 2048):
got = benchmark_backends(batch=4, seq=seq, device="cuda")
ref = base[str(seq)]
assert math.isnan(got["EFFICIENT_ATTENTION"])
assert got["FLASH_ATTENTION"] < 2.0 * ref["FLASH_ATTENTION"], (seq, got, ref)
|