mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
429 lines
27 KiB
Python
429 lines
27 KiB
Python
import unittest
|
|
from unittest.mock import patch
|
|
import numpy as np
|
|
from tinygrad import Tensor, UOp, dtypes, nn, function, Device, TinyJit
|
|
from tinygrad.llm.kernels.amd import (Linear, amd_custom_kernels_supported, q8_quantize, flash_attention, gated_delta_prefill,
|
|
QUANT_SIZES, HALFWORD_QUANTS, _wmma_rdna4)
|
|
from tinygrad.llm.gguf import ggml_data_to_tensor
|
|
from test.runtime.test_llm_quantized import QuantLinearMixin
|
|
|
|
class TestQ8Quantize(QuantLinearMixin, unittest.TestCase):
|
|
def test_wmma_architecture_selection(self):
|
|
if not amd_custom_kernels_supported(device:=Tensor.empty(1).device): self.skipTest("RDNA3/4 required")
|
|
self.assertEqual(_wmma_rdna4(device), Device[device].target[0] == 12)
|
|
|
|
def test_values_and_scales(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
|
x = np.linspace(-3.1, 2.7, 64, dtype=np.float32).reshape(2, 32)
|
|
quant, scale, gsum = q8_quantize(Tensor(x), 2, 32)
|
|
scale_np = np.maximum(np.max(np.abs(x), axis=-1, keepdims=True) / 127, 1e-8)
|
|
expected = np.clip(np.rint(x / scale_np), -127, 127).astype(np.int8)
|
|
np.testing.assert_array_equal(quant.bitcast(dtypes.int8).reshape(2, 32).numpy(), expected)
|
|
np.testing.assert_allclose(scale.numpy(), scale_np, rtol=1e-6)
|
|
# xsum holds the two per-16 sums per 32-wide group
|
|
np.testing.assert_array_equal(gsum.numpy().reshape(2, 2), expected.reshape(2, 2, 16).sum(-1).astype(np.float32))
|
|
|
|
def test_quantize_rounding_ties(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
|
values = np.array([-127,127]+[i+0.5 for i in range(-15,15)],dtype=np.float32)
|
|
quant,_,_ = q8_quantize(Tensor(values),1,32)
|
|
np.testing.assert_array_equal(quant.bitcast(dtypes.int8).reshape(32).numpy(),np.rint(values).astype(np.int8))
|
|
|
|
def test_quantize_nonunit_rounding_ties(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3/4 required")
|
|
values = np.zeros((4, 32), dtype=np.float16)
|
|
values[:, 0] = [1.8837890625, 2.525390625, 2.23828125, 1.736328125]
|
|
values[:, 1], values[:, 2] = values[:, 0]/2, -values[:, 0]/2
|
|
values[3, 3:5] = [-0.3076171875, -0.6904296875]
|
|
expected_scale = np.abs(values.astype(np.float32)).max(-1, keepdims=True)/127
|
|
quant, scale, _ = q8_quantize(Tensor(values), 4, 32)
|
|
np.testing.assert_array_equal(scale.numpy(), expected_scale)
|
|
np.testing.assert_array_equal(quant.bitcast(dtypes.int8).reshape(4, 32).numpy(), np.rint(values/expected_scale).astype(np.int8))
|
|
|
|
def test_q6_linear_compiles_in_function(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
|
rng = np.random.default_rng(42)
|
|
packed = rng.integers(0, 256, 210, dtype=np.uint8)
|
|
packed[-2:] = np.array([0.01], dtype=np.float16).view(np.uint8)
|
|
raw = Tensor(np.pad(packed, (2, 0))).contiguous().realize()[2:]
|
|
decoded = ggml_data_to_tensor(raw, 256, 14).reshape(1, 256)
|
|
linear = Linear(256, 1, bias=False)
|
|
nn.state.load_state_dict(linear, {"weight":decoded}, verbose=False, realize=False)
|
|
@function(allow_implicit=True)
|
|
def run(x:Tensor): return linear(x)
|
|
self.assertTrue(np.isfinite(run(Tensor.randn(1, 256)).realize().item()))
|
|
self.assertEqual(linear.weight.nbytes(), 210)
|
|
self.assertEqual(linear.weight.dtype, dtypes.uint16)
|
|
|
|
def test_q4_k_linear(self): self._test_quant_linear(12, 144)
|
|
def test_iq4_linear(self): self._test_quant_linear(23, 136)
|
|
def test_q5_linear(self): self._test_quant_linear(13, 176)
|
|
def test_q6_linear_odd_blocks(self): self._test_quant_linear(14, 210, in_features=768, out_features=3, token_counts=(1, 3, 16))
|
|
def test_q2_k_linear(self): self._test_quant_linear(10, 84, in_features=768, token_counts=(1, 3, 16))
|
|
def test_q3_k_linear(self): self._test_quant_linear(11, 110, in_features=768, token_counts=(1, 3, 16))
|
|
def test_iq2_xs_linear(self): self._test_quant_linear(17, 74, in_features=768, token_counts=(1, 3, 16))
|
|
def test_iq3_xxs_linear(self): self._test_quant_linear(18, 98, in_features=768, token_counts=(1, 3, 16))
|
|
def test_iq4_nl_linear(self): self._test_quant_linear(20, 144, in_features=768, token_counts=(1, 3, 16))
|
|
def test_iq3_s_linear(self): self._test_quant_linear(21, 110, in_features=768, token_counts=(1, 3, 16))
|
|
def test_iq2_s_linear(self): self._test_quant_linear(22, 82, in_features=768, token_counts=(1, 3, 16))
|
|
|
|
def test_wmma_linear_symbolic(self):
|
|
with patch("tinygrad.llm.kernels.amd.q8_quantize", side_effect=AssertionError("expected WMMA")):
|
|
for typ in QUANT_SIZES:
|
|
self._test_quant_linear(typ, QUANT_SIZES[typ], in_features=256, out_features=48, token_counts=(3, 17), bias=True, symbolic=True)
|
|
|
|
def test_mixed_quant_linear_wmma_tiles(self):
|
|
with patch("tinygrad.llm.kernels.amd.q8_quantize", side_effect=AssertionError("expected WMMA")):
|
|
for typ in HALFWORD_QUANTS:
|
|
self._test_quant_linear(typ, QUANT_SIZES[typ], in_features=768, out_features=64, token_counts=(16, 32, 64), bias=True)
|
|
|
|
def test_wmma_dequantization_basis(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3/4 required")
|
|
from gguf import dequantize, GGMLQuantizationType
|
|
rng = np.random.default_rng(1907)
|
|
scales = np.array([0, 2**-24, -2**-24, 2**-14, -0.00035, .001, .0037, -.125, 1, -4, 8], np.float16)
|
|
identity = Tensor(np.eye(768, dtype=np.float16)).realize()
|
|
for typ, size in QUANT_SIZES.items():
|
|
with self.subTest(ggml_type=typ):
|
|
packed = rng.integers(0, 256, (48*3, size), dtype=np.uint8)
|
|
blocks = packed.reshape(-1, 18) if typ == 20 else packed
|
|
offset = size-2 if typ in (11, 14) else 80 if typ == 10 else 0
|
|
blocks[:, offset:offset+2] = np.resize(scales, len(blocks)).view(np.uint8).reshape(-1, 2)
|
|
if typ in (10, 12, 13):
|
|
offset = 82 if typ == 10 else 2
|
|
blocks[:, offset:offset+2] = np.resize(scales[::-1], len(blocks)).view(np.uint8).reshape(-1, 2)
|
|
expected = dequantize(packed.flatten(), GGMLQuantizationType(typ)).reshape(48, 768).astype(np.float16).astype(np.float32).T
|
|
# Odd super-block counts and halfword-only alignment exercise both row and block offsets.
|
|
pad = 2 if typ in HALFWORD_QUANTS else 4
|
|
raw = Tensor(np.pad(packed.flatten(), (pad, 0))).realize()[pad:]
|
|
linear = Linear(768, 48, bias=False)
|
|
linear.weight = ggml_data_to_tensor(raw, 48*768, typ).reshape(48, 768)
|
|
actual = linear(identity).numpy()
|
|
self.assertEqual(linear.ggml_type, typ)
|
|
# Bare RDNA3 WMMA has a small FP32 accumulation bias even for identity products.
|
|
np.testing.assert_array_equal(actual.astype(np.float16), expected.astype(np.float16))
|
|
np.testing.assert_allclose(actual, expected, rtol=2e-7, atol=2**-36)
|
|
|
|
def test_quant_linear_unaligned_storage_fallback(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3/4 required")
|
|
from gguf import dequantize, GGMLQuantizationType
|
|
rng = np.random.default_rng(73)
|
|
x = rng.normal(size=(16, 256)).astype(np.float32)
|
|
for typ, offset in ((12, 1), (12, 2), (12, 3), (21, 1), (21, 3)):
|
|
with self.subTest(ggml_type=typ, offset=offset):
|
|
packed = rng.integers(0, 256, (16, QUANT_SIZES[typ]), dtype=np.uint8)
|
|
packed[:, :2] = np.array([.001], np.float16).view(np.uint8)
|
|
if typ == 12: packed[:, 2:4] = np.array([.0002], np.float16).view(np.uint8)
|
|
weight = dequantize(packed.flatten(), GGMLQuantizationType(typ)).reshape(16, 256)
|
|
raw = Tensor(np.pad(packed.flatten(), (offset, 0))).realize()[offset:]
|
|
linear = Linear(256, 16, bias=False)
|
|
linear.weight = ggml_data_to_tensor(raw, 16*256, typ).reshape(16, 256)
|
|
np.testing.assert_allclose(linear(Tensor(x)).numpy(), x @ weight.T, rtol=2e-5, atol=1e-5)
|
|
self.assertIsNone(linear.ggml_type)
|
|
|
|
def test_mixed_quant_linear_split_k(self):
|
|
for typ in (10, 11, 17, 18, 20, 21, 22):
|
|
with self.subTest(ggml_type=typ):
|
|
self._test_quant_linear(typ, QUANT_SIZES[typ], in_features=1280, out_features=3, token_counts=(1,))
|
|
|
|
def test_quant_linear_decode_row_groups(self):
|
|
# One, two, and four waves per workgroup; ragged split-K and multiple tokens must stay independent.
|
|
for typ in (12, 13, 14, 23):
|
|
for outputs in (3, 6, 12):
|
|
with self.subTest(ggml_type=typ, out_features=outputs):
|
|
self._test_quant_linear(typ, QUANT_SIZES[typ], in_features=1280, out_features=outputs, token_counts=(1, 3))
|
|
|
|
def test_quant_linear_bias(self):
|
|
for typ in (12, 21, 23):
|
|
with self.subTest(ggml_type=typ):
|
|
self._test_quant_linear(typ, QUANT_SIZES[typ], in_features=256, out_features=16, token_counts=(1, 3, 16), bias=True)
|
|
|
|
def test_quant_linear_partial_output_tile(self):
|
|
# Cover a sub-tile output, a trailing tile, and IQ4's larger-output tile selection.
|
|
for typ, size, outputs, tokens in ((12, 144, 16, 16), (12, 144, 48, 32), (13, 176, 48, 16), (23, 136, 4112, 32)):
|
|
with self.subTest(ggml_type=typ, out_features=outputs):
|
|
self._test_quant_linear(typ, size, in_features=256, out_features=outputs, token_counts=(tokens,))
|
|
|
|
def test_quant_linear_preserves_rope_permutation(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
|
rng = np.random.default_rng(42)
|
|
for typ, size in ((12, 144), (13, 176), (14, 210), (23, 136)):
|
|
with self.subTest(ggml_type=typ):
|
|
packed = rng.integers(0, 256, (16, size), dtype=np.uint8)
|
|
packed[:, -2:] = np.array([0.001], dtype=np.float16).view(np.uint8)
|
|
if typ != 14: packed[:, :2] = np.array([0.001], dtype=np.float16).view(np.uint8)
|
|
if typ in (12, 13): packed[:, 2:4] = np.array([0.0002], dtype=np.float16).view(np.uint8)
|
|
raw = Tensor(np.pad(packed.flatten(), (4, 0))).contiguous().realize()[4:]
|
|
decoded = ggml_data_to_tensor(raw, 16*256, typ).reshape(16, 256).half()
|
|
original = decoded.numpy()
|
|
x = rng.normal(size=(3, 256)).astype(np.float16)
|
|
for prefix in (None, 0, 4):
|
|
with self.subTest(prefix=prefix):
|
|
w = decoded.reshape(2, 8, 256)
|
|
if prefix is None:
|
|
weight = w.rearrange("n (h two) d -> n (two h) d", two=2)
|
|
else:
|
|
weight = w[:, :prefix].cat(w[:, prefix:].rearrange("n (h two) d -> n (two h) d", two=2), dim=1)
|
|
start = prefix or 0
|
|
rows = np.arange(16).reshape(2, 8)
|
|
order = np.concatenate((rows[:, :start], rows[:, start:].reshape(2, -1, 2).transpose(0, 2, 1).reshape(2, -1)), axis=1)
|
|
linear = Linear(256, 16, bias=False)
|
|
linear.weight = weight.reshape(16, 256)
|
|
np.testing.assert_allclose(linear(Tensor(x)).numpy(), x.astype(np.float32) @ original[order.flatten()].astype(np.float32).T,
|
|
rtol=3e-3, atol=2e-2)
|
|
self.assertIsNone(linear.ggml_type)
|
|
|
|
def test_quant_linear_rejects_unaligned_rows_and_integer_casts(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
|
for width in (128, 256):
|
|
with self.subTest(width=width):
|
|
packed = np.zeros((2*width//256, 136), dtype=np.uint8)
|
|
packed[:, :2] = np.array([0.001], dtype=np.float16).view(np.uint8)
|
|
packed[:, 8:] = np.arange(128, dtype=np.uint8)
|
|
raw = Tensor(np.pad(packed.flatten(), (4, 0))).realize()[4:]
|
|
weight = ggml_data_to_tensor(raw, 2*width, 23).reshape(2, width)
|
|
if width == 256: weight = weight.int().float()
|
|
expected = weight.numpy().sum(-1)[None]
|
|
linear = Linear(width, 2, bias=False)
|
|
linear.weight = weight
|
|
np.testing.assert_allclose(linear(Tensor.ones(1, width)).numpy(), expected, rtol=1e-3, atol=1e-3)
|
|
self.assertIsNone(linear.ggml_type)
|
|
|
|
def test_dense_gemv_preserves_integer_casts(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
|
linear = Linear(128, 1)
|
|
linear.weight = Tensor.full((1, 128), 0.75).contiguous().realize().int().float()
|
|
linear.bias = Tensor.full((1,), 0.75).contiguous().realize().int().float()
|
|
np.testing.assert_array_equal(linear(Tensor.ones(1, 128)).numpy(), 0)
|
|
|
|
def test_dense_gemv_float32_range(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
|
linear = Linear(128, 1, bias=False)
|
|
linear.weight = Tensor.full((1, 128), 1/128, dtype=dtypes.float32).realize()
|
|
np.testing.assert_array_equal(linear(Tensor.full((1, 128), 65536, dtype=dtypes.float32)).numpy(), 65536)
|
|
|
|
def test_gated_delta_state_and_precision(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
|
for case in ("view", "reset", "half"):
|
|
with self.subTest(case=case):
|
|
q = Tensor.full((1, 1, 1, 32), 256 if case == "half" else 1, dtype=dtypes.half if case == "half" else dtypes.float32)
|
|
state = Tensor.full((1, 1, 32, 4), int(case == "reset"), dtype=dtypes.float32).contiguous().realize().transpose(-1, -2)
|
|
if case != "view": state = state.contiguous().realize()
|
|
start = Tensor(UOp.variable("start_pos", 0, 10).bind(0)) if case == "reset" else None
|
|
beta = Tensor.full((1, 1, 1), 1/2097152 if case == "half" else 1, dtype=dtypes.float32)
|
|
if case != "reset":
|
|
message = "recurrent state must be contiguous" if case == "view" else "recurrent Q/K must be float32"
|
|
with self.assertRaisesRegex(AssertionError, message):
|
|
gated_delta_prefill(q, q, Tensor.ones(1, 1, 1, 4), beta, Tensor.ones(1, 1, 1), state, start)
|
|
continue
|
|
out = gated_delta_prefill(q, q, Tensor.ones(1, 1, 1, 4), beta, Tensor.ones(1, 1, 1), state, start)
|
|
np.testing.assert_array_equal(out.numpy(), 32)
|
|
np.testing.assert_array_equal(state.numpy(), 1)
|
|
|
|
def test_gated_delta_random_scan(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3/4 required")
|
|
rng = np.random.default_rng(42)
|
|
q, k = (rng.normal(0, 0.1, (1, 2, 4, 32)).astype(np.float32) for _ in range(2))
|
|
v = rng.normal(size=(1, 2, 4, 8)).astype(np.float32)
|
|
beta = rng.uniform(0, 1, (1, 2, 4)).astype(np.float32)
|
|
beta[:, :, -1] = 0 # padded step must leave the recurrent state unchanged
|
|
initial = rng.normal(0, 0.1, (1, 2, 8, 32)).astype(np.float32)
|
|
for per_channel in (False, True):
|
|
alpha = rng.uniform(0.8, 1, (1, 2, 4, 32) if per_channel else (1, 2, 4)).astype(np.float32)
|
|
alpha[:, :, -1] = 1
|
|
for start in (0, 3):
|
|
with self.subTest(per_channel=per_channel, start=start):
|
|
expected_state = np.zeros_like(initial) if start == 0 else initial.copy()
|
|
outputs = []
|
|
for t in range(4):
|
|
decay = alpha[:, :, t, None, :] if per_channel else alpha[:, :, t, None, None]
|
|
expected_state *= decay
|
|
delta = (v[:, :, t] - (expected_state*k[:, :, t, None]).sum(-1)) * beta[:, :, t, None]
|
|
expected_state += delta[..., None]*k[:, :, t, None]
|
|
outputs.append((expected_state*q[:, :, t, None]).sum(-1))
|
|
state = Tensor(initial).contiguous().realize()
|
|
out = gated_delta_prefill(*map(Tensor, (q, k, v, beta, alpha)), state, Tensor(UOp.variable("start", 0, 3).bind(start)))
|
|
np.testing.assert_allclose(out.numpy(), np.stack(outputs, axis=2), rtol=2e-4, atol=2e-6)
|
|
np.testing.assert_allclose(state.numpy(), expected_state, rtol=2e-4, atol=2e-6)
|
|
|
|
def test_dense_gemv_bias(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
|
rng = np.random.default_rng(42)
|
|
w, bias = rng.normal(size=(32, 128)).astype(np.float16), rng.normal(size=32).astype(np.float16)
|
|
linear = Linear(128, 32)
|
|
linear.weight, linear.bias = Tensor(w), Tensor(bias)
|
|
for tokens in (1, 3):
|
|
with self.subTest(tokens=tokens):
|
|
x = rng.normal(size=(tokens, 128)).astype(np.float16)
|
|
np.testing.assert_allclose(linear(Tensor(x)).numpy(), x.astype(np.float32) @ w.astype(np.float32).T + bias, rtol=2e-3, atol=2e-3)
|
|
|
|
def test_q6_linear_multiple_tokens(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
|
rng = np.random.default_rng(42)
|
|
in_features, blocks = 2048, 16*2048//256
|
|
packed = rng.integers(0, 256, blocks*210, dtype=np.uint8)
|
|
for i in range(blocks): packed[i*210+208:i*210+210] = np.array([0.01], dtype=np.float16).view(np.uint8)
|
|
raw = Tensor(np.pad(packed, (4, 0))).contiguous().realize()[4:]
|
|
decoded = ggml_data_to_tensor(raw, 16*in_features, 14).reshape(16, in_features)
|
|
weight = decoded.numpy()
|
|
linear = Linear(in_features, 16, bias=False)
|
|
nn.state.load_state_dict(linear, {"weight":decoded}, verbose=False, realize=False)
|
|
x = rng.normal(size=(3, in_features)).astype(np.float32)
|
|
scale = np.maximum(np.abs(x).reshape(3, in_features//32, 32).max(-1, keepdims=True) / 127, 1e-8)
|
|
xq = np.clip(np.rint(x.reshape(3, in_features//32, 32) / scale), -127, 127) * scale
|
|
np.testing.assert_allclose(linear(Tensor(x)).numpy(), xq.reshape(3, in_features) @ weight.T, rtol=2e-3, atol=2e-2)
|
|
self.assertEqual(linear.ggml_type, 14)
|
|
|
|
# symbolic token counts take the padded kernel path and give the same results
|
|
generic = Linear(in_features, 16, bias=False)
|
|
nn.state.load_state_dict(generic, {"weight":decoded}, verbose=False, realize=False)
|
|
sym = Tensor(np.concatenate([x, np.zeros((1, in_features), np.float32)])).contiguous()[:UOp.variable("tokens", 1, 4).bind(3)]
|
|
np.testing.assert_allclose(generic(sym)[:3].numpy(), xq.reshape(3, in_features) @ weight.T, rtol=2e-3, atol=2e-2)
|
|
self.assertTrue(generic.use_custom_quant)
|
|
self.assertEqual(generic.ggml_type, 14)
|
|
|
|
def test_attention_fallback_shapes(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
|
for tokens, capacity, dim in ((1, 65, 64), (32, 64, 32), (32, 64, 384), (32, 64, 512)):
|
|
with self.subTest(tokens=tokens, capacity=capacity, dim=dim):
|
|
valid = 33
|
|
cache = np.full((2, 1, 1, capacity, dim), np.nan, dtype=np.float16)
|
|
cache[0, :, :, :valid] = 0
|
|
cache[1, :, :, :valid] = np.arange(valid)[:, None]
|
|
q = Tensor.zeros(1, 2, tokens, dim, dtype=dtypes.half)
|
|
expected = np.broadcast_to(np.arange(valid-tokens, valid)[None, None, :, None]/2, q.shape)
|
|
np.testing.assert_allclose(flash_attention(q, Tensor(cache), valid).numpy(), expected, rtol=1e-3, atol=1e-3)
|
|
|
|
def test_attention_uses_physical_cache_length(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
|
q, k, v = Tensor.zeros(1, 2, 1, 32), Tensor.randn(1, 1, 1, 32), Tensor.randn(1, 1, 1, 32)
|
|
cache = Tensor.empty(2, 1, 1, 256, 32, dtype=dtypes.half).contiguous()
|
|
assigned = Tensor(cache.uop.after(cache[:, :, :, 0:1, :].uop.store(Tensor.stack(k, v).cast(dtypes.half).uop)))
|
|
out = flash_attention(q, assigned, 1).realize()
|
|
np.testing.assert_allclose(out.numpy(), v.expand(1, 2, 1, 32).numpy(), rtol=2e-2, atol=2e-2)
|
|
|
|
def test_flash_attention_decode_symbolic_gqa(self):
|
|
with patch.object(Tensor, "scaled_dot_product_attention", side_effect=AssertionError("expected custom decode")):
|
|
self._test_flash_decode(8, 2, 256, 128, 37, symbolic=True)
|
|
|
|
def test_flash_attention_decode_gqa_tail(self): self._test_flash_decode(3, 1, 192, 64, 37)
|
|
|
|
def test_flash_attention_decode_gqa_output_layout(self): self._test_flash_decode(4, 1, 128, 256, 3)
|
|
def test_flash_attention_decode_large_gqa_group(self): self._test_flash_decode(8, 1, 256, 256, 73)
|
|
|
|
def _test_flash_decode(self, heads, kv_heads, dim, n, valid, symbolic=False):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
|
rng = np.random.default_rng(42)
|
|
q = rng.normal(size=(1, heads, 1, dim)).astype(np.float16)
|
|
cache = rng.normal(size=(2, 1, kv_heads, n, dim)).astype(np.float16)
|
|
k, v = (np.repeat(c[0, :, :valid].astype(np.float32), heads//kv_heads, axis=0) for c in cache)
|
|
scores = q[0].astype(np.float32) @ k.transpose(0, 2, 1) / np.sqrt(dim)
|
|
probs = np.exp(scores - scores.max(-1, keepdims=True))
|
|
expected = (probs / probs.sum(-1, keepdims=True)) @ v
|
|
cache_tensor = Tensor(cache)
|
|
if symbolic:
|
|
start_pos = UOp.variable("start_pos", 0, n-1).bind(valid-1)
|
|
valid = start_pos + 1
|
|
cache_tensor = cache_tensor.realize()
|
|
np.testing.assert_allclose(flash_attention(Tensor(q), cache_tensor, valid).numpy(), expected[None], rtol=2e-3, atol=2e-3)
|
|
|
|
def test_attention_symbolic_length_jit(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3/4 required")
|
|
cache = np.zeros((2, 1, 1, 128, 128), dtype=np.float16)
|
|
cache[1] = np.arange(128, dtype=np.float16)[:, None]
|
|
kv = Tensor(cache).realize()
|
|
for tokens in (1, 32):
|
|
q = Tensor(np.zeros((1, 2, tokens, 128), dtype=np.float16)).realize()
|
|
run = TinyJit(lambda q, kv, end: flash_attention(q, kv, end).realize())
|
|
for valid in (tokens, 33, 97, 128, 64, tokens):
|
|
with self.subTest(tokens=tokens, valid=valid):
|
|
end = UOp.variable("valid_length", tokens, 128).bind(valid)
|
|
expected = np.broadcast_to(np.arange(valid-tokens, valid)[None, None, :, None]/2, q.shape)
|
|
np.testing.assert_allclose(run(q, kv, end).numpy(), expected, rtol=1e-3, atol=1e-3)
|
|
|
|
def test_prefill_attention_random(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3/4 required")
|
|
rng = np.random.default_rng(42)
|
|
cases = ((2, 4, 2, 32, 64, 45, 64, False), (1, 3, 1, 64, 128, 83, 128, False),
|
|
(1, 2, 1, 96, 256, 149, 192, False), (1, 4, 2, 17, 128, 128, 128, True), (1, 2, 1, 3, 64, 33, 64, True),
|
|
(2, 3, 1, 64, 96, 99, 128, False), (1, 2, 1, 32, 320, 33, 64, False))
|
|
for batch, heads, kv_heads, tokens, dim, valid, capacity, symbolic in cases:
|
|
with self.subTest(batch=batch, tokens=tokens, dim=dim, symbolic=symbolic):
|
|
q = rng.normal(size=(batch, heads, tokens, dim)).astype(np.float16)
|
|
cache = rng.normal(size=(2, batch, kv_heads, capacity, dim)).astype(np.float16)
|
|
cache[:, :, :, valid:] = np.nan
|
|
k, v = (np.repeat(c[:, :, :valid].astype(np.float32), heads//kv_heads, axis=1) for c in cache)
|
|
scores = q.astype(np.float32) @ k.swapaxes(-1, -2) / np.sqrt(dim)
|
|
scores = np.where(np.arange(valid)[None, :] <= np.arange(valid-tokens, valid)[:, None], scores, -np.inf)
|
|
probs = np.exp(scores - scores.max(-1, keepdims=True))
|
|
expected = (probs / probs.sum(-1, keepdims=True)) @ v
|
|
query, kv = Tensor(q), Tensor(cache).realize()
|
|
end = UOp.variable("valid", 1, capacity).bind(valid)
|
|
if symbolic:
|
|
count = UOp.variable("query_tokens", 1, 32).bind(tokens)
|
|
query = Tensor(np.pad(q, ((0, 0), (0, 0), (0, 32-tokens), (0, 0)))).contiguous()[:, :, :count]
|
|
@function(allow_implicit=True)
|
|
def run(q:Tensor, kv:Tensor, end:UOp): return flash_attention(q, kv, end)
|
|
with patch.object(Tensor, "scaled_dot_product_attention", side_effect=AssertionError("expected WMMA attention")):
|
|
actual = run(query, kv, end)[:, :, :tokens].numpy()
|
|
np.testing.assert_allclose(actual, expected, rtol=3e-3, atol=2e-3)
|
|
|
|
def test_prefill_attention_nonfinite_cache_tail(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
|
rng = np.random.default_rng(42)
|
|
q = Tensor.zeros(1, 2, 32, 128, dtype=dtypes.half)
|
|
values = rng.normal(size=(33, 128)).astype(np.float16)
|
|
expected = np.stack([values[:i+2].astype(np.float32).mean(0) for i in range(32)])[None, None].repeat(2, axis=1)
|
|
for tail in (np.nan, np.inf, -np.inf):
|
|
with self.subTest(tail=tail):
|
|
cache = np.full((2, 1, 1, 64, 128), tail, dtype=np.float16)
|
|
cache[0, :, :, :33] = 0
|
|
cache[1, :, :, :33] = values
|
|
valid = UOp.variable("valid_end", 32, 64).bind(33)
|
|
cache_tensor = Tensor(cache).realize()
|
|
assigned = Tensor(cache_tensor.uop.after(Tensor(valid).uop))
|
|
out = flash_attention(q, assigned, valid)
|
|
np.testing.assert_allclose(out.numpy(), expected, rtol=2e-3, atol=2e-3)
|
|
|
|
def test_flash_attention_decode_beyond_256_chunks(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
|
n = 257 * 64
|
|
q = Tensor.zeros(1, 1, 1, 32, dtype=dtypes.half).realize()
|
|
k = Tensor.zeros(1, 1, n, 32, dtype=dtypes.half)
|
|
v = Tensor.zeros(1, 1, n-64, 32, dtype=dtypes.half).cat(Tensor.ones(1, 1, 64, 32, dtype=dtypes.half), dim=2)
|
|
cache = Tensor.stack(k, v).contiguous().realize()
|
|
for valid, expected in ((1, 0), (n, 1/257)):
|
|
with self.subTest(valid=valid):
|
|
valid_kv_len = UOp.variable("valid_kv_len", 1, n).bind(valid)
|
|
assigned = Tensor(cache.uop.after(Tensor(valid_kv_len).uop))
|
|
np.testing.assert_allclose(flash_attention(q, assigned, valid_kv_len).numpy(), expected, rtol=2e-3, atol=2e-4)
|
|
|
|
def test_flash_attention_decode_long_context_random(self):
|
|
self._test_flash_decode(8, 2, 128, 257*64, 257*64-13) # past 256 chunks, with a ragged tail
|
|
|
|
def test_flash_attention_decode_chunk_round_accumulator_range(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
|
valid_kv_len, max_kv_len = 6749, 6784 # three chunk rounds, with a ragged tail
|
|
q = Tensor.zeros(1, 8, 1, 32, dtype=dtypes.half).realize()
|
|
cache = Tensor.stack(Tensor.zeros(1, 1, max_kv_len, 32, dtype=dtypes.half),
|
|
Tensor.full((1, 1, max_kv_len, 32), 5500, dtype=dtypes.half)).contiguous().realize()
|
|
np.testing.assert_allclose(flash_attention(q, cache, valid_kv_len).numpy(), 5500, rtol=2e-3, atol=2e-3)
|
|
|
|
def test_prefill_attention_unaligned_start(self):
|
|
if not amd_custom_kernels_supported(Tensor.empty(1).device): self.skipTest("RDNA3 required")
|
|
rng = np.random.default_rng(42)
|
|
start_pos = 1718
|
|
q = Tensor.zeros(1, 8, 32, 128)
|
|
old_kv = rng.normal(size=(2, 1, 1, start_pos, 128)).astype(np.float32)
|
|
new_kv = rng.normal(size=(2, 1, 1, 32, 128)).astype(np.float32)
|
|
cache = Tensor.zeros(2, 1, 1, 2048, 128, dtype=dtypes.half).contiguous()
|
|
Tensor.realize(cache[:, :, :, :start_pos].assign(Tensor(old_kv).cast(dtypes.half)))
|
|
sp = UOp.variable("start_pos", 0, 2047).bind(start_pos)
|
|
assigned = Tensor(cache.uop.after(cache[:, :, :, sp:sp+32, :].uop.store(Tensor(new_kv).cast(dtypes.half).uop)))
|
|
out = flash_attention(q, assigned, sp+32).realize()
|
|
values = np.concatenate([old_kv[1, 0, 0], new_kv[1, 0, 0]]).astype(np.float16).astype(np.float32)
|
|
expected = np.stack([values[:start_pos+i+1].mean(0) for i in range(32)])[None, None].repeat(8, axis=1)
|
|
np.testing.assert_allclose(out.numpy(), expected, rtol=2e-3, atol=2e-3)
|
|
|
|
if __name__ == "__main__": unittest.main()
|