echo / code /flash-linear-attention /tests /ops /test_simple_gla.py
amonshano's picture
Add Echo-Memory codebase used for this run (CC BY 4.0, JD Echo Team) (part 4)
c335050 verified
Raw
History Blame Contribute Delete
23.7 kB
import os
import pytest
import torch
import torch.nn.functional as F
from fla.ops.simple_gla.chunk import chunk_simple_gla
from fla.ops.simple_gla.fused_chunk import fused_chunk_simple_gla
from fla.ops.simple_gla.fused_recurrent import fused_recurrent_simple_gla
from fla.ops.simple_gla.naive import naive_parallel_simple_gla, naive_recurrent_simple_gla
from fla.ops.simple_gla.parallel import parallel_simple_gla
from fla.utils import assert_close, device
@pytest.mark.parametrize(
('B', 'T', 'H', 'D', 'scale', 'gate_logit_normalizer', 'dtype'),
[
pytest.param(*test, id="B{}-T{}-H{}-D{}-scale{}-gate_logit_normalizer{}-{}".format(*test))
for test in [
(1, 63, 1, 64, 1, 1, torch.float),
(2, 500, 4, 60, 1, 1, torch.float),
(2, 1024, 8, 128, 1, 0.1, torch.float),
(2, 1024, 8, 128, 0.1, 1, torch.float),
(2, 1024, 8, 128, 1, 10, torch.float),
(4, 2048, 8, 64, 0.1, 1, torch.float),
(2, 1024, 8, 128, 1, 0.1, torch.float16),
(2, 1024, 8, 128, 1, 10, torch.float16),
]
],
)
def test_fused_recurrent(
B: int,
T: int,
H: int,
D: int,
scale: float,
gate_logit_normalizer: float,
dtype: torch.dtype,
):
torch.manual_seed(42)
q = torch.randn((B, T, H, D), dtype=dtype, device=device).requires_grad_()
k = torch.randn((B, T, H, D), dtype=dtype, device=device).requires_grad_()
v = torch.randn((B, T, H, D), dtype=dtype, device=device).requires_grad_()
g = torch.randn((B, T, H), dtype=dtype, device=device)
g = (F.logsigmoid(g) / gate_logit_normalizer).requires_grad_()
h0 = torch.randn(B, H, D, D, device=device).requires_grad_()
dht = torch.randn_like(h0)
do = torch.randn_like(v)
ref, ref_ht = naive_recurrent_simple_gla(
q=q,
k=k,
v=v,
g=g,
scale=scale,
initial_state=h0,
output_final_state=True,
)
((ref * do).sum() + (ref_ht * dht).sum()).backward()
ref_dq, q.grad = q.grad.clone(), None
ref_dk, k.grad = k.grad.clone(), None
ref_dv, v.grad = v.grad.clone(), None
ref_dg, g.grad = g.grad.clone(), None
ref_dh0, h0.grad = h0.grad.clone(), None
tri, tri_ht = fused_recurrent_simple_gla(
q=q,
k=k,
v=v,
g=g,
scale=scale,
initial_state=h0,
output_final_state=True,
)
((tri * do).sum() + (tri_ht * dht).sum()).backward()
tri_dq, q.grad = q.grad.clone(), None
tri_dk, k.grad = k.grad.clone(), None
tri_dv, v.grad = v.grad.clone(), None
tri_dg, g.grad = g.grad.clone(), None
tri_dh0, h0.grad = h0.grad.clone(), None
assert_close('o', ref, tri, 0.005)
assert_close('ht', ref_ht, tri_ht, 0.005)
assert_close('dq', ref_dq, tri_dq, 0.005)
assert_close('dk', ref_dk, tri_dk, 0.005)
assert_close('dv', ref_dv, tri_dv, 0.005)
assert_close('dg', ref_dg, tri_dg, 0.005, err_atol=2e-4)
assert_close('dh0', ref_dh0, tri_dh0, 0.005)
@pytest.mark.parametrize(
('H', 'D', 'scale', 'gate_logit_normalizer', 'cu_seqlens', 'dtype'),
[
pytest.param(*test, id="H{}-D{}-scale{}-gate_logit_normalizer{}-cu_seqlens{}-{}".format(*test))
for test in [
(4, 64, 1, 1, [0, 15], torch.float),
(4, 64, 1, 1, [0, 256, 500, 1000], torch.float),
(4, 100, 0.1, 1, [0, 15, 100, 300, 1200, 2000], torch.float),
(4, 100, 1, 1, [0, 15, 100, 300, 1200, 2000], torch.float),
(4, 100, 1, 10, [0, 15, 100, 300, 1200, 2000], torch.float),
(4, 64, 1, 1, [0, 1, 100, 300, 1200, 2048], torch.float16),
(4, 128, 1, 1, [0, 200, 512, 1200, 2048], torch.float16),
]
],
)
def test_fused_recurrent_varlen(
H: int,
D: int,
scale: float,
gate_logit_normalizer: float,
cu_seqlens: list[int],
dtype: torch.dtype,
):
torch.manual_seed(42)
N = len(cu_seqlens) - 1
T = cu_seqlens[-1]
cu_seqlens = torch.tensor(cu_seqlens, dtype=torch.int32, device=device)
q = torch.randn((1, T, H, D), dtype=dtype, device=device).requires_grad_()
k = torch.randn((1, T, H, D), dtype=dtype, device=device).requires_grad_()
v = torch.randn((1, T, H, D), dtype=dtype, device=device).requires_grad_()
g = torch.randn((1, T, H), dtype=dtype, device=device)
g = (F.logsigmoid(g) / gate_logit_normalizer).requires_grad_()
h0 = torch.randn(N, H, D, D, device=device).requires_grad_()
dht = torch.randn_like(h0)
do = torch.randn_like(v)
refs, ref_hts = [], []
for i, (bos, eos) in enumerate(zip(cu_seqlens[:-1], cu_seqlens[1:], strict=False)):
ref, ref_ht = naive_recurrent_simple_gla(
q=q[:, bos:eos],
k=k[:, bos:eos],
v=v[:, bos:eos],
g=g[:, bos:eos],
scale=scale,
initial_state=h0[i],
output_final_state=True,
)
refs.append(ref)
ref_hts.append(ref_ht)
ref = torch.cat(refs, 1)
ref_ht = torch.cat(ref_hts, 0)
((ref * do).sum() + (ref_ht * dht).sum()).backward()
ref_dq, q.grad = q.grad.clone(), None
ref_dk, k.grad = k.grad.clone(), None
ref_dv, v.grad = v.grad.clone(), None
ref_dg, g.grad = g.grad.clone(), None
ref_dh0, h0.grad = h0.grad.clone(), None
tri, tri_ht = fused_recurrent_simple_gla(
q=q,
k=k,
v=v,
g=g,
scale=scale,
initial_state=h0,
output_final_state=True,
cu_seqlens=cu_seqlens,
)
((tri * do).sum() + (tri_ht * dht).sum()).backward()
tri_dq, q.grad = q.grad.clone(), None
tri_dk, k.grad = k.grad.clone(), None
tri_dv, v.grad = v.grad.clone(), None
tri_dg, g.grad = g.grad.clone(), None
tri_dh0, h0.grad = h0.grad.clone(), None
assert_close('o', ref, tri, 0.005)
assert_close('ht', ref_ht, tri_ht, 0.005)
assert_close('dq', ref_dq, tri_dq, 0.005)
assert_close('dk', ref_dk, tri_dk, 0.005)
assert_close('dv', ref_dv, tri_dv, 0.005)
assert_close('dg', ref_dg, tri_dg, 0.005, err_atol=2e-4)
assert_close('dh0', ref_dh0, tri_dh0, 0.005)
@pytest.mark.parametrize(
('B', 'T', 'H', 'D', 'scale', 'gate_logit_normalizer', 'dtype'),
[
pytest.param(*test, id="B{}-T{}-H{}-D{}-scale{}-gate_logit_normalizer{}-{}".format(*test))
for test in [
(1, 63, 1, 64, 1, 1, torch.float16),
(2, 500, 3, 60, 1, 1, torch.float16),
(1, 1000, 4, 128, 1, 0.1, torch.float16),
(2, 1000, 4, 128, 0.1, 1, torch.float16),
(3, 1000, 4, 128, 0.1, 10, torch.float16),
(4, 2048, 8, 64, 0.1, 1, torch.float16),
]
],
)
def test_chunk(
B: int,
T: int,
H: int,
D: int,
scale: float,
gate_logit_normalizer: float,
dtype: torch.dtype,
):
torch.manual_seed(42)
os.environ['TRITON_F32_DEFAULT'] = 'ieee'
q = torch.randn((B, T, H, D), dtype=dtype, device=device).requires_grad_(True)
k = torch.randn((B, T, H, D), dtype=dtype, device=device).requires_grad_(True)
v = torch.randn((B, T, H, D), dtype=dtype, device=device).requires_grad_(True)
g = torch.randn((B, T, H), dtype=torch.float32, device=device)
h0 = torch.rand((B, H, D, D), dtype=torch.float32, device=device).requires_grad_(True)
dht = torch.randn_like(h0)
g = (F.logsigmoid(g) / gate_logit_normalizer).requires_grad_(True)
do = torch.randn_like(v)
ref, ref_ht = fused_recurrent_simple_gla(
q=q,
k=k,
v=v,
g=g,
scale=scale,
initial_state=h0,
output_final_state=True,
)
((ref * do).sum() + (dht * ref_ht).sum()).backward()
ref_dq, q.grad = q.grad.clone(), None
ref_dk, k.grad = k.grad.clone(), None
ref_dv, v.grad = v.grad.clone(), None
ref_dg, g.grad = g.grad.clone(), None
ref_dh0, h0.grad = h0.grad.clone(), None
tri, tri_ht = chunk_simple_gla(
q=q,
k=k,
v=v,
g=g,
scale=scale,
initial_state=h0,
output_final_state=True,
)
((tri * do).sum() + (dht * tri_ht).sum()).backward()
tri_dq, q.grad = q.grad.clone(), None
tri_dk, k.grad = k.grad.clone(), None
tri_dv, v.grad = v.grad.clone(), None
tri_dg, g.grad = g.grad.clone(), None
tri_dh0, h0.grad = h0.grad.clone(), None
assert_close('o', ref, tri, 0.004)
assert_close('ht', ref_ht, tri_ht, 0.005)
assert_close('dq', ref_dq, tri_dq, 0.005)
assert_close('dk', ref_dk, tri_dk, 0.005)
assert_close('dv', ref_dv, tri_dv, 0.005)
assert_close('dg', ref_dg, tri_dg, 0.005)
assert_close('dh0', ref_dh0, tri_dh0, 0.005)
@pytest.mark.parametrize(
('H', 'D', 'cu_seqlens', 'dtype'),
[
pytest.param(*test, id="H{}-D{}-cu_seqlens{}-{}".format(*test))
for test in [
(4, 64, [0, 15], torch.float16),
(4, 64, [0, 256, 500, 1000], torch.float16),
(4, 100, [0, 15, 100, 300, 1200, 2000], torch.float16),
]
],
)
@pytest.mark.skipif(
os.getenv('SKIP_TEST_CHUNK_VARLEN') == '1',
reason='Skipping test_chunk_varlen because SKIP_TEST_CHUNK_VARLEN is set',
)
def test_chunk_varlen(
H: int,
D: int,
cu_seqlens: list[int],
dtype: torch.dtype,
):
torch.manual_seed(42)
os.environ['TRITON_F32_DEFAULT'] = 'ieee'
N = len(cu_seqlens) - 1
T = cu_seqlens[-1]
cu_seqlens = torch.tensor(cu_seqlens, dtype=torch.int32, device=device)
# seq-first required for inputs with variable lengths
q = torch.randn((1, T, H, D), dtype=dtype, device=device).requires_grad_()
k = torch.randn((1, T, H, D), dtype=dtype, device=device).requires_grad_()
v = torch.randn((1, T, H, D), dtype=dtype, device=device).requires_grad_()
g = F.logsigmoid(torch.randn((1, T, H), dtype=dtype, device=device)).requires_grad_()
h0 = torch.randn((N, H, D, D), dtype=torch.float32, device=device).requires_grad_()
dht = torch.randn_like(h0)
do = torch.randn_like(v)
ref, ref_ht = fused_recurrent_simple_gla(
q=q,
k=k,
v=v,
g=g,
initial_state=h0,
output_final_state=True,
cu_seqlens=cu_seqlens,
)
((ref * do).sum() + (dht * ref_ht).sum()).backward()
ref_dq, q.grad = q.grad.clone(), None
ref_dk, k.grad = k.grad.clone(), None
ref_dv, v.grad = v.grad.clone(), None
ref_dg, g.grad = g.grad.clone(), None
ref_dh0, h0.grad = h0.grad.clone(), None
tri, tri_ht = chunk_simple_gla(
q=q,
k=k,
v=v,
g=g,
initial_state=h0,
output_final_state=True,
cu_seqlens=cu_seqlens,
)
((tri * do).sum() + (dht * tri_ht).sum()).backward()
tri_dq, q.grad = q.grad.clone(), None
tri_dk, k.grad = k.grad.clone(), None
tri_dv, v.grad = v.grad.clone(), None
tri_dg, g.grad = g.grad.clone(), None
tri_dh0, h0.grad = h0.grad.clone(), None
assert_close('o', ref, tri, 0.004)
assert_close('ht', ref_ht, tri_ht, 0.005)
assert_close('dq', ref_dq, tri_dq, 0.005)
assert_close('dk', ref_dk, tri_dk, 0.005)
assert_close('dv', ref_dv, tri_dv, 0.005)
assert_close('dg', ref_dg, tri_dg, 0.005)
assert_close('dh0', ref_dh0, tri_dh0, 0.005)
@pytest.mark.parametrize(
('B', 'T', 'H', 'D', 'scale', 'gate_logit_normalizer', 'dtype'),
[
pytest.param(*test, id="B{}-T{}-H{}-D{}-scale{}-gate_logit_normalizer{}-{}".format(*test))
for test in [
(1, 63, 1, 64, 1, 1, torch.float16),
(2, 500, 3, 60, 1, 1, torch.float16),
(1, 1000, 4, 128, 1, 0.1, torch.float16),
(2, 1000, 4, 128, 0.1, 1, torch.float16),
(3, 1000, 4, 128, 0.1, 10, torch.float16),
(4, 2048, 8, 64, 0.1, 1, torch.float16),
]
],
)
def test_fused_chunk(
B: int,
T: int,
H: int,
D: int,
dtype: torch.dtype,
scale: float,
gate_logit_normalizer: float,
):
torch.manual_seed(42)
os.environ['TRITON_F32_DEFAULT'] = 'ieee'
q = torch.randn((B, T, H, D), dtype=dtype, device=device).requires_grad_(True)
k = torch.randn((B, T, H, D), dtype=dtype, device=device).requires_grad_(True)
v = torch.randn((B, T, H, D), dtype=dtype, device=device).requires_grad_(True)
g = torch.randn((B, T, H), dtype=torch.float32, device=device)
h0 = torch.rand((B, H, D, D), dtype=torch.float32, device=device).requires_grad_(True)
dht = torch.randn_like(h0)
g = (F.logsigmoid(g) / gate_logit_normalizer).requires_grad_(True)
do = torch.randn_like(v)
ref, ref_ht = fused_recurrent_simple_gla(
q=q,
k=k,
v=v,
g=g,
scale=scale,
initial_state=h0,
output_final_state=True,
)
((ref * do).sum() + (dht * ref_ht).sum()).backward()
ref_dq, q.grad = q.grad.clone(), None
ref_dk, k.grad = k.grad.clone(), None
ref_dv, v.grad = v.grad.clone(), None
ref_dg, g.grad = g.grad.clone(), None
ref_dh0, h0.grad = h0.grad.clone(), None
tri, tri_ht = fused_chunk_simple_gla(
q=q,
k=k,
v=v,
g=g,
scale=scale,
initial_state=h0,
output_final_state=True,
)
((tri * do).sum() + (dht * tri_ht).sum()).backward()
tri_dq, q.grad = q.grad.clone(), None
tri_dk, k.grad = k.grad.clone(), None
tri_dv, v.grad = v.grad.clone(), None
tri_dg, g.grad = g.grad.clone(), None
tri_dh0, h0.grad = h0.grad.clone(), None
assert_close('o', ref, tri, 0.004)
assert_close('ht', ref_ht, tri_ht, 0.005)
assert_close('dq', ref_dq, tri_dq, 0.005)
assert_close('dk', ref_dk, tri_dk, 0.005)
assert_close('dv', ref_dv, tri_dv, 0.005)
assert_close('dg', ref_dg, tri_dg, 0.005)
assert_close('dh0', ref_dh0, tri_dh0, 0.005)
@pytest.mark.parametrize(
('H', 'D', 'cu_seqlens', 'dtype'),
[
pytest.param(*test, id="H{}-D{}-cu_seqlens{}-{}".format(*test))
for test in [
(4, 64, [0, 15], torch.float16),
(4, 64, [0, 256, 500, 1000], torch.float16),
(4, 100, [0, 15, 100, 300, 1200, 2000], torch.float16),
]
],
)
@pytest.mark.skipif(
os.getenv('SKIP_TEST_CHUNK_VARLEN') == '1',
reason='Skipping test_chunk_varlen because SKIP_TEST_CHUNK_VARLEN is set',
)
def test_fused_chunk_varlen(
H: int,
D: int,
cu_seqlens: list[int],
dtype: torch.dtype,
):
torch.manual_seed(42)
os.environ['TRITON_F32_DEFAULT'] = 'ieee'
N = len(cu_seqlens) - 1
T = cu_seqlens[-1]
cu_seqlens = torch.tensor(cu_seqlens, dtype=torch.int32, device=device)
q = torch.randn((1, T, H, D), dtype=dtype, device=device).requires_grad_()
k = torch.randn((1, T, H, D), dtype=dtype, device=device).requires_grad_()
v = torch.randn((1, T, H, D), dtype=dtype, device=device).requires_grad_()
g = F.logsigmoid(torch.randn((1, T, H), dtype=dtype, device=device)).requires_grad_()
h0 = torch.randn((N, H, D, D), dtype=torch.float32, device=device).requires_grad_()
dht = torch.randn_like(h0)
do = torch.randn_like(v)
ref, ref_ht = fused_recurrent_simple_gla(
q=q,
k=k,
v=v,
g=g,
initial_state=h0,
output_final_state=True,
cu_seqlens=cu_seqlens,
)
((ref * do).sum() + (dht * ref_ht).sum()).backward()
ref_dq, q.grad = q.grad.clone(), None
ref_dk, k.grad = k.grad.clone(), None
ref_dv, v.grad = v.grad.clone(), None
ref_dg, g.grad = g.grad.clone(), None
ref_dh0, h0.grad = h0.grad.clone(), None
tri, tri_ht = fused_chunk_simple_gla(
q=q,
k=k,
v=v,
g=g,
initial_state=h0,
output_final_state=True,
cu_seqlens=cu_seqlens,
)
((tri * do).sum() + (dht * tri_ht).sum()).backward()
tri_dq, q.grad = q.grad.clone(), None
tri_dk, k.grad = k.grad.clone(), None
tri_dv, v.grad = v.grad.clone(), None
tri_dg, g.grad = g.grad.clone(), None
tri_dh0, h0.grad = h0.grad.clone(), None
assert_close('o', ref, tri, 0.004)
assert_close('ht', ref_ht, tri_ht, 0.005)
assert_close('dq', ref_dq, tri_dq, 0.005)
assert_close('dk', ref_dk, tri_dk, 0.005)
assert_close('dv', ref_dv, tri_dv, 0.005)
assert_close('dg', ref_dg, tri_dg, 0.005)
assert_close('dh0', ref_dh0, tri_dh0, 0.005)
@pytest.mark.parametrize(
('B', 'T', 'H', 'D', 'scale', 'gate_logit_normalizer', 'dtype'),
[
pytest.param(*test, id="B{}-T{}-H{}-D{}-scale{}-gate_logit_normalizer{}-{}".format(*test))
for test in [
(1, 63, 1, 64, 1, 1, torch.float16),
(2, 500, 3, 60, 1, 1, torch.float16),
(2, 1024, 4, 128, 0.1, 1, torch.float16),
(3, 1024, 4, 128, 0.1, 10, torch.float16),
(3, 1024, 4, 256, 0.1, 0.1, torch.float16),
(4, 2048, 4, 64, 0.1, 0.1, torch.float16),
]
],
)
def test_parallel(
B: int,
T: int,
H: int,
D: int,
scale: float,
gate_logit_normalizer: float,
dtype: torch.dtype,
):
torch.manual_seed(42)
os.environ['TRITON_F32_DEFAULT'] = 'ieee'
USE_G = gate_logit_normalizer > 0
q = torch.randn((B, T, H, D), dtype=dtype, device=device).requires_grad_(True)
k = torch.randn((B, T, H, D), dtype=dtype, device=device).requires_grad_(True)
v = torch.randn((B, T, H, D), dtype=dtype, device=device).requires_grad_(True)
g = F.logsigmoid(torch.randn((B, T, H), dtype=dtype, device=device)) if USE_G else None
g = (g / gate_logit_normalizer).requires_grad_(True) if USE_G else None
do = torch.randn_like(v)
ref, _ = fused_recurrent_simple_gla(q=q, k=k, v=v, g=g, scale=scale, output_final_state=True)
_, ref_A = naive_parallel_simple_gla(q=q, k=k, v=v, g=g, scale=scale)
ref.backward(do)
ref_dq, q.grad = q.grad.clone(), None
ref_dk, k.grad = k.grad.clone(), None
ref_dv, v.grad = v.grad.clone(), None
if USE_G:
ref_dg, g.grad = g.grad.clone(), None
tri, tri_A = parallel_simple_gla(q=q, k=k, v=v, g=g, scale=scale, output_attentions=True)
tri.backward(do)
tri_dq, q.grad = q.grad.clone(), None
tri_dk, k.grad = k.grad.clone(), None
tri_dv, v.grad = v.grad.clone(), None
if USE_G:
tri_dg, g.grad = g.grad.clone(), None
assert_close('o', ref, tri, 0.005)
assert_close('A', ref_A, tri_A, 0.005)
assert_close('dq', ref_dq, tri_dq, 0.005)
assert_close('dk', ref_dk, tri_dk, 0.005)
assert_close('dv', ref_dv, tri_dv, 0.005)
if USE_G:
assert_close('dg', ref_dg, tri_dg, 0.015)
@pytest.mark.parametrize(
('H', 'D', 'cu_seqlens', 'dtype'),
[
pytest.param(*test, id="H{}-D{}-cu_seqlens{}-{}".format(*test))
for test in [
(4, 64, [0, 15], torch.float16),
(4, 64, [0, 256, 500, 1000], torch.float16),
(4, 100, [0, 15, 100, 300, 1200, 2000], torch.float16),
]
],
)
@pytest.mark.skipif(
os.getenv('SKIP_TEST_CHUNK_VARLEN') == '1',
reason='Skipping test_chunk_varlen because SKIP_TEST_CHUNK_VARLEN is set',
)
def test_parallel_varlen(
H: int,
D: int,
cu_seqlens: list[int],
dtype: torch.dtype,
):
torch.manual_seed(42)
os.environ['TRITON_F32_DEFAULT'] = 'ieee'
T = cu_seqlens[-1]
cu_seqlens = torch.tensor(cu_seqlens, dtype=torch.int32, device=device)
q = torch.randn((1, T, H, D), dtype=dtype, device=device).requires_grad_()
k = torch.randn((1, T, H, D), dtype=dtype, device=device).requires_grad_()
v = torch.randn((1, T, H, D), dtype=dtype, device=device).requires_grad_()
g = F.logsigmoid(torch.randn((1, T, H), dtype=dtype, device=device)).requires_grad_()
do = torch.randn_like(v)
ref, _ = fused_recurrent_simple_gla(
q=q,
k=k,
v=v,
g=g,
output_final_state=False,
cu_seqlens=cu_seqlens,
)
((ref * do).sum()).backward()
ref_dq, q.grad = q.grad.clone(), None
ref_dk, k.grad = k.grad.clone(), None
ref_dv, v.grad = v.grad.clone(), None
ref_dg, g.grad = g.grad.clone(), None
tri, _ = parallel_simple_gla(
q=q,
k=k,
v=v,
g=g,
cu_seqlens=cu_seqlens,
)
((tri * do).sum()).backward()
tri_dq, q.grad = q.grad.clone(), None
tri_dk, k.grad = k.grad.clone(), None
tri_dv, v.grad = v.grad.clone(), None
tri_dg, g.grad = g.grad.clone(), None
assert_close('o', ref, tri, 0.004)
assert_close('dq', ref_dq, tri_dq, 0.005)
assert_close('dk', ref_dk, tri_dk, 0.005)
assert_close('dv', ref_dv, tri_dv, 0.005)
assert_close('dg', ref_dg, tri_dg, 0.005)
@pytest.mark.parametrize(
('vary_A', 'dtype'),
[
pytest.param(True, torch.float, id=f'vary_A{True}-dtype{torch.float}'),
pytest.param(False, torch.float, id=f'vary_A{False}-dtype{torch.float}'),
pytest.param(True, torch.float16, id=f'vary_A{True}-dtype{torch.float16}'),
pytest.param(False, torch.float16, id=f'vary_A{False}-dtype{torch.float16}'),
],
)
def test_simple_gla_to_mamba2(vary_A, dtype):
try:
from mamba_ssm.modules.ssd_minimal import ssd_minimal_discrete
from mamba_ssm.ops.triton.ssd_combined import mamba_chunk_scan_combined
except ImportError:
pytest.skip('mamba_ssm is not installed.')
torch.manual_seed(42)
# Dimensions, Denoted (B, T, Q, D, P) in Mamba2 paper
batch, seq_len, chunk_size, dim, headdim = 2, 512, 8, 64, 16
n_heads = dim // headdim # (H) in the paper
ngroups = n_heads # (G) in the paper; NOTE: do not use group-query here
dstate = 64 # (N) in the paper
atol = 5e-4 if dtype == torch.float else 1e-2
x = 0.1 * torch.randn(batch, seq_len, n_heads, headdim, dtype=dtype, device=device)
dt = torch.ones(batch, seq_len, n_heads, dtype=dtype, device=device) # dt=1 can be ignored
if vary_A:
A = -0.1 * torch.rand(1, seq_len, n_heads, dtype=dtype, device=device)
else: # constant A for all position
A = -0.1 * torch.rand(n_heads, dtype=dtype, device=device)
B = 0.1 * torch.randn(batch, seq_len, ngroups, dstate, dtype=dtype, device=device)
C = 0.1 * torch.randn(batch, seq_len, ngroups, dstate, dtype=dtype, device=device)
y_ssd, final_ssd = ssd_minimal_discrete(x * dt.unsqueeze(-1), A * dt, B, C, chunk_size)
if not vary_A:
# NOTE: fused kernel does not support varying A with time
y_fuse, final_fuse = mamba_chunk_scan_combined(x, dt, A, B, C, chunk_size, D=None, return_final_states=True)
assert y_ssd.allclose(y_fuse, 0, atol), f'y diff: {torch.abs(y_ssd - y_fuse).max()}'
# fused kernel upcasts state to float32
# https://github.com/state-spaces/mamba/blob/v2.2.2/mamba_ssm/ops/triton/ssd_combined.py#L650
final_fuse = final_fuse.to(dtype)
assert final_ssd.allclose(final_fuse, 0, atol), f'final diff: {torch.abs(final_ssd - final_fuse).max()}'
# mapping inputs Mamba2 -> FLA
# FLA Now use head_first = False, therefore there is no need to transpose inputs
q = C
k = B
v = x
g = (A * dt)
# mapping outputs Mamba2 -> FLA
y_rearrange = y_ssd
final_rearrange = final_ssd.transpose(2, 3)
# comparing output results between FLA kernel and Mamba2 kernel
# final_gla_fuse :[N, H, K, V]
outputs_gla_fuse, final_gla_fuse = chunk_simple_gla(q, k, v, g, scale=1.0, output_final_state=True)
assert y_rearrange.allclose(outputs_gla_fuse, 0, atol), f'y diff: {torch.abs(y_rearrange - outputs_gla_fuse).max()}'
final_gla_fuse = final_gla_fuse.to(dtype) # states hard-coded to float32 in FLA kernel
assert final_rearrange.allclose(final_gla_fuse, 0, atol), f'final diff: {torch.abs(final_ssd - final_gla_fuse).max()}'