Files
vllm-omni/tests/attention/test_fish_kvcache_attn.py
T

1138 lines
39 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import pytest
import torch
from vllm_omni.attention import fish_kvcache_attn, fish_kvcache_backend
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class _FakeImpl:
dcp_world_size = 1
alibi_slopes = None
sliding_window = None
scale = 1.0
def __init__(self):
self.original_calls = 0
def forward(
self,
layer,
query,
key,
value,
kv_cache,
attn_metadata,
output,
output_scale=None,
output_block_scale=None,
):
del layer, query, key, value, kv_cache, attn_metadata, output_scale, output_block_scale
self.original_calls += 1
output.fill_(2)
return output
class _FakeAttentionLayer:
def __init__(self):
self.impl = _FakeImpl()
class _FakeSelfAttn:
def __init__(self):
self.attn = _FakeAttentionLayer()
class _FakeLayer:
def __init__(self):
self.self_attn = _FakeSelfAttn()
class _FakeModel:
def __init__(self):
self.layers = [_FakeLayer()]
_DEFAULT_UPPER_BOUND = object()
def _metadata(
*,
batch_size: int = 2,
num_actual_tokens: int | None = None,
max_query_len: int = 1,
max_seq_len: int = 128,
use_cascade: bool = False,
seq_lens_cpu_upper_bound: torch.Tensor | None | object = _DEFAULT_UPPER_BOUND,
seq_lens: torch.Tensor | None = None,
):
if num_actual_tokens is None:
num_actual_tokens = batch_size
if seq_lens is None:
seq_lens = torch.ones((batch_size,), dtype=torch.int32)
if seq_lens_cpu_upper_bound is _DEFAULT_UPPER_BOUND and max_query_len == 1:
seq_lens_cpu_upper_bound = seq_lens.detach().cpu()
elif seq_lens_cpu_upper_bound is _DEFAULT_UPPER_BOUND:
seq_lens_cpu_upper_bound = None
return type(
"Metadata",
(),
{
"use_cascade": use_cascade,
"num_actual_tokens": num_actual_tokens,
"block_table": torch.zeros((batch_size, 8), dtype=torch.int32),
"seq_lens": seq_lens,
"seq_lens_cpu_upper_bound": seq_lens_cpu_upper_bound,
"max_query_len": max_query_len,
"max_seq_len": max_seq_len,
},
)()
def test_fish_kvcache_enabled_by_default(monkeypatch):
monkeypatch.delenv("VLLM_OMNI_FISH_KVCACHE_ATTN", raising=False)
assert fish_kvcache_attn.is_fish_kvcache_attn_enabled()
assert fish_kvcache_backend._fish_kvcache_enabled()
assert not fish_kvcache_attn.is_fish_kvcache_attn_required()
@pytest.mark.parametrize("value", ["", "1", "true", "yes", "on", "required"])
def test_fish_kvcache_enabled_values_are_consistent(monkeypatch, value):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", value)
assert fish_kvcache_attn.is_fish_kvcache_attn_enabled()
assert fish_kvcache_backend._fish_kvcache_enabled()
assert fish_kvcache_attn.is_fish_kvcache_attn_required() is (value == "required")
@pytest.mark.parametrize("value", ["0", "false", "no", "off", "disabled", "disable"])
def test_fish_kvcache_disabled_values_are_consistent(monkeypatch, value):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", value)
assert not fish_kvcache_attn.is_fish_kvcache_attn_enabled()
assert not fish_kvcache_backend._fish_kvcache_enabled()
assert not fish_kvcache_attn.is_fish_kvcache_attn_required()
@pytest.mark.parametrize("sliding_window", [None, (-1, -1)])
def test_fish_kvcache_attn_guard_accepts_decode_only_shape(monkeypatch, sliding_window):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "1")
monkeypatch.setattr(fish_kvcache_attn, "is_available", lambda: True)
assert fish_kvcache_attn.can_use_fish_kvcache_attn(
query=torch.empty((4, 32, 128), dtype=torch.float16),
key_cache=torch.empty((8, 16, 8, 128), dtype=torch.float16),
value_cache=torch.empty((8, 16, 8, 128), dtype=torch.float16),
block_table=torch.zeros((4, 8), dtype=torch.int32),
seq_lens=torch.ones((4,), dtype=torch.int32),
max_query_len=1,
max_seq_len=128,
dcp_world_size=1,
use_cascade=False,
alibi_slopes=None,
sliding_window=sliding_window,
)
def test_fish_kvcache_attn_guard_rejects_unsupported_block_size(monkeypatch):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "1")
monkeypatch.setattr(fish_kvcache_attn, "is_available", lambda: True)
assert not fish_kvcache_attn.can_use_fish_kvcache_attn(
query=torch.empty((4, 32, 128), dtype=torch.float16),
key_cache=torch.empty((8, 32, 8, 128), dtype=torch.float16),
value_cache=torch.empty((8, 32, 8, 128), dtype=torch.float16),
block_table=torch.zeros((4, 8), dtype=torch.int32),
seq_lens=torch.ones((4,), dtype=torch.int32),
max_query_len=1,
max_seq_len=128,
dcp_world_size=1,
use_cascade=False,
alibi_slopes=None,
sliding_window=None,
)
def test_fish_kvcache_backend_wraps_only_model_instance(monkeypatch):
fish_kvcache_backend.reset_fish_kvcache_attn_stats()
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "1")
monkeypatch.setattr(fish_kvcache_backend, "is_available", lambda: True)
monkeypatch.setattr(fish_kvcache_backend, "load_error", lambda: None)
monkeypatch.setattr(fish_kvcache_backend, "can_use_fish_kvcache_attn", lambda **_: True)
hits = []
def fake_decode(query, key_cache, value_cache, block_table, seq_lens, out, *, scale, max_seq_len):
del key_cache, value_cache, block_table, seq_lens, scale, max_seq_len
hits.append(tuple(query.shape))
out.fill_(1)
return out
monkeypatch.setattr(fish_kvcache_backend, "fish_decode_kvcache_attn", fake_decode)
model = _FakeModel()
other_impl = _FakeImpl()
assert fish_kvcache_backend.install_fish_kvcache_attn_backend(model) == 1
assert not hasattr(other_impl, "_fish_kvcache_attn_installed")
metadata = _metadata()
query = torch.zeros((2, 32, 128), dtype=torch.float16)
output = torch.zeros_like(query)
kv_cache = torch.zeros((2, 8, 16, 8, 128), dtype=torch.float16)
result = model.layers[0].self_attn.attn.impl.forward(
None,
query,
None,
None,
kv_cache,
metadata,
output,
)
assert result is output
assert hits == [(2, 32, 128)]
assert model.layers[0].self_attn.attn.impl.original_calls == 0
assert torch.equal(output, torch.ones_like(output))
assert fish_kvcache_backend.get_fish_kvcache_attn_stats()["small_hit_count"] == 1
def test_fish_kvcache_backend_install_is_idempotent(monkeypatch):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "1")
monkeypatch.setattr(fish_kvcache_backend, "is_available", lambda: True)
monkeypatch.setattr(fish_kvcache_backend, "load_error", lambda: None)
monkeypatch.setattr(fish_kvcache_backend, "can_use_fish_kvcache_attn", lambda **_: False)
model = _FakeModel()
assert fish_kvcache_backend.install_fish_kvcache_attn_backend(model) == 1
wrapped_forward = model.layers[0].self_attn.attn.impl.forward
assert fish_kvcache_backend.install_fish_kvcache_attn_backend(model) == 0
assert model.layers[0].self_attn.attn.impl.forward is wrapped_forward
query = torch.zeros((2, 32, 128), dtype=torch.float16)
output = torch.zeros_like(query)
kv_cache = torch.zeros((2, 8, 16, 8, 128), dtype=torch.float16)
model.layers[0].self_attn.attn.impl.forward(
None,
query,
None,
None,
kv_cache,
_metadata(),
output,
)
assert model.layers[0].self_attn.attn.impl.original_calls == 1
def test_fish_kvcache_backend_falls_back_to_original_forward_on_guard_miss(monkeypatch):
fish_kvcache_backend.reset_fish_kvcache_attn_stats()
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "1")
monkeypatch.setattr(fish_kvcache_backend, "is_available", lambda: True)
monkeypatch.setattr(fish_kvcache_backend, "load_error", lambda: None)
monkeypatch.setattr(fish_kvcache_backend, "can_use_fish_kvcache_attn", lambda **_: False)
decode_calls = []
def fake_decode(*args, **kwargs):
del args, kwargs
decode_calls.append(True)
monkeypatch.setattr(fish_kvcache_backend, "fish_decode_kvcache_attn", fake_decode)
model = _FakeModel()
assert fish_kvcache_backend.install_fish_kvcache_attn_backend(model) == 1
query = torch.zeros((2, 32, 128), dtype=torch.float16)
output = torch.zeros_like(query)
kv_cache = torch.zeros((2, 8, 16, 8, 128), dtype=torch.float16)
result = model.layers[0].self_attn.attn.impl.forward(
None,
query,
None,
None,
kv_cache,
_metadata(),
output,
)
assert result is output
assert model.layers[0].self_attn.attn.impl.original_calls == 1
assert decode_calls == []
assert torch.equal(output, torch.full_like(output, 2))
stats = fish_kvcache_backend.get_fish_kvcache_attn_stats()
assert stats["fallback_count_by_reason"] == {"guard_miss": 1}
def test_fish_kvcache_backend_uses_decode_seq_lens_upper_bound(monkeypatch):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "1")
monkeypatch.setattr(fish_kvcache_backend, "is_available", lambda: True)
monkeypatch.setattr(fish_kvcache_backend, "load_error", lambda: None)
guard_max_seq_lens = []
def fake_can_use(**kwargs):
guard_max_seq_lens.append(kwargs["max_seq_len"])
return True
decode_max_seq_lens = []
def fake_decode(query, key_cache, value_cache, block_table, seq_lens, out, *, scale, max_seq_len):
del query, key_cache, value_cache, block_table, seq_lens, scale
decode_max_seq_lens.append(max_seq_len)
out.fill_(1)
return out
monkeypatch.setattr(fish_kvcache_backend, "can_use_fish_kvcache_attn", fake_can_use)
monkeypatch.setattr(fish_kvcache_backend, "fish_decode_kvcache_attn", fake_decode)
model = _FakeModel()
assert fish_kvcache_backend.install_fish_kvcache_attn_backend(model) == 1
query = torch.zeros((2, 32, 128), dtype=torch.float16)
output = torch.zeros_like(query)
kv_cache = torch.zeros((2, 8, 16, 8, 128), dtype=torch.float16)
model.layers[0].self_attn.attn.impl.forward(
None,
query,
None,
None,
kv_cache,
_metadata(
max_query_len=1,
max_seq_len=16384,
seq_lens_cpu_upper_bound=torch.tensor([512, 768], dtype=torch.int32),
),
output,
)
assert guard_max_seq_lens == [1024]
assert decode_max_seq_lens == [1024]
def test_fish_kvcache_backend_uses_cpu_upper_bound_above_metadata_max(monkeypatch):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "1")
monkeypatch.setattr(fish_kvcache_backend, "is_available", lambda: True)
monkeypatch.setattr(fish_kvcache_backend, "load_error", lambda: None)
guard_max_seq_lens = []
def fake_can_use(**kwargs):
guard_max_seq_lens.append(kwargs["max_seq_len"])
return True
monkeypatch.setattr(fish_kvcache_backend, "can_use_fish_kvcache_attn", fake_can_use)
monkeypatch.setattr(
fish_kvcache_backend,
"fish_decode_kvcache_attn",
lambda query, key_cache, value_cache, block_table, seq_lens, out, *, scale, max_seq_len: out.fill_(1),
)
model = _FakeModel()
assert fish_kvcache_backend.install_fish_kvcache_attn_backend(model) == 1
query = torch.zeros((2, 32, 128), dtype=torch.float16)
output = torch.zeros_like(query)
kv_cache = torch.zeros((2, 8, 16, 8, 128), dtype=torch.float16)
model.layers[0].self_attn.attn.impl.forward(
None,
query,
None,
None,
kv_cache,
_metadata(
max_query_len=1,
max_seq_len=64,
seq_lens_cpu_upper_bound=torch.tensor([1024, 1024], dtype=torch.int32),
),
output,
)
assert guard_max_seq_lens == [1024]
def test_fish_kvcache_backend_slices_upper_bound_by_active_batch(monkeypatch):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "1")
monkeypatch.setattr(fish_kvcache_backend, "is_available", lambda: True)
monkeypatch.setattr(fish_kvcache_backend, "load_error", lambda: None)
guard_max_seq_lens = []
def fake_can_use(**kwargs):
guard_max_seq_lens.append(kwargs["max_seq_len"])
return True
def fake_decode(query, key_cache, value_cache, block_table, seq_lens, out, *, scale, max_seq_len):
del query, key_cache, value_cache, block_table, seq_lens, scale, max_seq_len
out.fill_(1)
return out
monkeypatch.setattr(fish_kvcache_backend, "can_use_fish_kvcache_attn", fake_can_use)
monkeypatch.setattr(fish_kvcache_backend, "fish_decode_kvcache_attn", fake_decode)
model = _FakeModel()
assert fish_kvcache_backend.install_fish_kvcache_attn_backend(model) == 1
query = torch.zeros((2, 32, 128), dtype=torch.float16)
output = torch.zeros_like(query)
kv_cache = torch.zeros((2, 8, 16, 8, 128), dtype=torch.float16)
model.layers[0].self_attn.attn.impl.forward(
None,
query,
None,
None,
kv_cache,
_metadata(
batch_size=4,
num_actual_tokens=2,
max_query_len=1,
max_seq_len=16384,
seq_lens_cpu_upper_bound=torch.tensor([512, 768, 4096, 4096], dtype=torch.int32),
),
output,
)
assert guard_max_seq_lens == [1024]
def test_fish_kvcache_backend_falls_back_on_short_upper_bound_shape(monkeypatch):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "1")
monkeypatch.setattr(fish_kvcache_backend, "is_available", lambda: True)
monkeypatch.setattr(fish_kvcache_backend, "load_error", lambda: None)
guard_max_seq_lens = []
def fake_can_use(**kwargs):
guard_max_seq_lens.append(kwargs["max_seq_len"])
return True
def fake_decode(query, key_cache, value_cache, block_table, seq_lens, out, *, scale, max_seq_len):
del query, key_cache, value_cache, block_table, seq_lens, scale, max_seq_len
out.fill_(1)
return out
monkeypatch.setattr(fish_kvcache_backend, "can_use_fish_kvcache_attn", fake_can_use)
monkeypatch.setattr(fish_kvcache_backend, "fish_decode_kvcache_attn", fake_decode)
model = _FakeModel()
assert fish_kvcache_backend.install_fish_kvcache_attn_backend(model) == 1
query = torch.zeros((2, 32, 128), dtype=torch.float16)
output = torch.zeros_like(query)
kv_cache = torch.zeros((2, 8, 16, 8, 128), dtype=torch.float16)
result = model.layers[0].self_attn.attn.impl.forward(
None,
query,
None,
None,
kv_cache,
_metadata(
batch_size=2,
max_query_len=1,
max_seq_len=16384,
seq_lens_cpu_upper_bound=torch.tensor([512], dtype=torch.int32),
),
output,
)
assert result is output
assert guard_max_seq_lens == []
assert model.layers[0].self_attn.attn.impl.original_calls == 1
assert torch.equal(output, torch.full_like(output, 2))
def test_fish_kvcache_backend_falls_back_on_non_cpu_upper_bound(monkeypatch):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "1")
monkeypatch.setattr(fish_kvcache_backend, "is_available", lambda: True)
monkeypatch.setattr(fish_kvcache_backend, "load_error", lambda: None)
guard_max_seq_lens = []
def fake_can_use(**kwargs):
guard_max_seq_lens.append(kwargs["max_seq_len"])
return True
def fake_decode(query, key_cache, value_cache, block_table, seq_lens, out, *, scale, max_seq_len):
del query, key_cache, value_cache, block_table, seq_lens, scale, max_seq_len
out.fill_(1)
return out
monkeypatch.setattr(fish_kvcache_backend, "can_use_fish_kvcache_attn", fake_can_use)
monkeypatch.setattr(fish_kvcache_backend, "fish_decode_kvcache_attn", fake_decode)
model = _FakeModel()
assert fish_kvcache_backend.install_fish_kvcache_attn_backend(model) == 1
query = torch.zeros((2, 32, 128), dtype=torch.float16)
output = torch.zeros_like(query)
kv_cache = torch.zeros((2, 8, 16, 8, 128), dtype=torch.float16)
result = model.layers[0].self_attn.attn.impl.forward(
None,
query,
None,
None,
kv_cache,
_metadata(
batch_size=2,
max_query_len=1,
max_seq_len=16384,
seq_lens_cpu_upper_bound=torch.empty((2,), device="meta", dtype=torch.int32),
),
output,
)
assert result is output
assert guard_max_seq_lens == []
assert model.layers[0].self_attn.attn.impl.original_calls == 1
assert torch.equal(output, torch.full_like(output, 2))
def test_fish_kvcache_backend_required_rejects_non_cpu_upper_bound(monkeypatch):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "required")
monkeypatch.setattr(fish_kvcache_backend, "is_available", lambda: True)
monkeypatch.setattr(fish_kvcache_backend, "load_error", lambda: None)
monkeypatch.setattr(fish_kvcache_backend, "can_use_fish_kvcache_attn", lambda **_: True)
model = _FakeModel()
assert fish_kvcache_backend.install_fish_kvcache_attn_backend(model) == 1
query = torch.zeros((2, 32, 128), dtype=torch.float16)
output = torch.zeros_like(query)
kv_cache = torch.zeros((2, 8, 16, 8, 128), dtype=torch.float16)
with pytest.raises(RuntimeError, match="must be a CPU tensor"):
model.layers[0].self_attn.attn.impl.forward(
None,
query,
None,
None,
kv_cache,
_metadata(
batch_size=2,
max_query_len=1,
max_seq_len=16384,
seq_lens_cpu_upper_bound=torch.empty((2,), device="meta", dtype=torch.int32),
),
output,
)
def test_fish_kvcache_backend_required_rejects_underestimated_upper_bound(monkeypatch):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "required")
monkeypatch.setattr(fish_kvcache_backend, "is_available", lambda: True)
monkeypatch.setattr(fish_kvcache_backend, "load_error", lambda: None)
monkeypatch.setattr(fish_kvcache_backend, "can_use_fish_kvcache_attn", lambda **_: True)
model = _FakeModel()
assert fish_kvcache_backend.install_fish_kvcache_attn_backend(model) == 1
query = torch.zeros((2, 32, 128), dtype=torch.float16)
output = torch.zeros_like(query)
kv_cache = torch.zeros((2, 8, 16, 8, 128), dtype=torch.float16)
with pytest.raises(RuntimeError, match="underestimates"):
model.layers[0].self_attn.attn.impl.forward(
None,
query,
None,
None,
kv_cache,
_metadata(
max_query_len=1,
max_seq_len=16384,
seq_lens=torch.tensor([512, 2048], dtype=torch.int32),
seq_lens_cpu_upper_bound=torch.tensor([512, 768], dtype=torch.int32),
),
output,
)
def test_fish_kvcache_backend_required_rejects_missing_upper_bound(monkeypatch):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "required")
monkeypatch.setattr(fish_kvcache_backend, "is_available", lambda: True)
monkeypatch.setattr(fish_kvcache_backend, "load_error", lambda: None)
monkeypatch.setattr(fish_kvcache_backend, "can_use_fish_kvcache_attn", lambda **_: True)
model = _FakeModel()
assert fish_kvcache_backend.install_fish_kvcache_attn_backend(model) == 1
query = torch.zeros((2, 32, 128), dtype=torch.float16)
output = torch.zeros_like(query)
kv_cache = torch.zeros((2, 8, 16, 8, 128), dtype=torch.float16)
with pytest.raises(RuntimeError, match="requires seq_lens_cpu_upper_bound"):
model.layers[0].self_attn.attn.impl.forward(
None,
query,
None,
None,
kv_cache,
_metadata(max_query_len=1, max_seq_len=16384, seq_lens_cpu_upper_bound=None),
output,
)
def test_fish_kvcache_backend_required_rejects_zero_installed_layers(monkeypatch):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "required")
monkeypatch.setattr(fish_kvcache_backend, "is_available", lambda: True)
monkeypatch.setattr(fish_kvcache_backend, "load_error", lambda: None)
with pytest.raises(RuntimeError, match="installed 0 attention layers"):
fish_kvcache_backend.install_fish_kvcache_attn_backend(type("EmptyModel", (), {"layers": []})())
def test_fish_kvcache_backend_required_raises_on_guard_miss(monkeypatch):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "required")
monkeypatch.setattr(fish_kvcache_backend, "is_available", lambda: True)
monkeypatch.setattr(fish_kvcache_backend, "load_error", lambda: None)
monkeypatch.setattr(fish_kvcache_backend, "can_use_fish_kvcache_attn", lambda **_: False)
model = _FakeModel()
assert fish_kvcache_backend.install_fish_kvcache_attn_backend(model) == 1
query = torch.zeros((2, 32, 128), dtype=torch.float16)
output = torch.zeros_like(query)
kv_cache = torch.zeros((2, 8, 16, 8, 128), dtype=torch.float16)
with pytest.raises(RuntimeError, match="required"):
model.layers[0].self_attn.attn.impl.forward(
None,
query,
None,
None,
kv_cache,
_metadata(),
output,
)
def test_fish_kvcache_backend_required_allows_prefill_fallback(monkeypatch):
fish_kvcache_backend.reset_fish_kvcache_attn_stats()
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "required")
monkeypatch.setattr(fish_kvcache_backend, "is_available", lambda: True)
monkeypatch.setattr(fish_kvcache_backend, "load_error", lambda: None)
monkeypatch.setattr(fish_kvcache_backend, "can_use_fish_kvcache_attn", lambda **_: False)
model = _FakeModel()
assert fish_kvcache_backend.install_fish_kvcache_attn_backend(model) == 1
query = torch.zeros((2, 32, 128), dtype=torch.float16)
output = torch.zeros_like(query)
kv_cache = torch.zeros((2, 8, 16, 8, 128), dtype=torch.float16)
result = model.layers[0].self_attn.attn.impl.forward(
None,
query,
None,
None,
kv_cache,
_metadata(max_query_len=2, max_seq_len=2),
output,
)
assert result is output
assert model.layers[0].self_attn.attn.impl.original_calls == 1
assert torch.equal(output, torch.full_like(output, 2))
stats = fish_kvcache_backend.get_fish_kvcache_attn_stats()
assert stats["fallback_count_by_reason"] == {"non_decode": 1}
def test_fish_kvcache_backend_falls_back_without_cpu_upper_bound(monkeypatch):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "1")
monkeypatch.setattr(fish_kvcache_backend, "is_available", lambda: True)
monkeypatch.setattr(fish_kvcache_backend, "load_error", lambda: None)
guard_max_seq_lens = []
def fake_can_use(**kwargs):
guard_max_seq_lens.append(kwargs["max_seq_len"])
return True
decode_max_seq_lens = []
def fake_decode(query, key_cache, value_cache, block_table, seq_lens, out, *, scale, max_seq_len):
del query, key_cache, value_cache, block_table, seq_lens, scale
decode_max_seq_lens.append(max_seq_len)
out.fill_(1)
return out
monkeypatch.setattr(fish_kvcache_backend, "can_use_fish_kvcache_attn", fake_can_use)
monkeypatch.setattr(fish_kvcache_backend, "fish_decode_kvcache_attn", fake_decode)
model = _FakeModel()
assert fish_kvcache_backend.install_fish_kvcache_attn_backend(model) == 1
query = torch.zeros((2, 32, 128), dtype=torch.float16)
output = torch.zeros_like(query)
kv_cache = torch.zeros((2, 8, 16, 8, 128), dtype=torch.float16)
result = model.layers[0].self_attn.attn.impl.forward(
None,
query,
None,
None,
kv_cache,
_metadata(max_query_len=1, max_seq_len=16384, seq_lens_cpu_upper_bound=None),
output,
)
assert result is output
assert guard_max_seq_lens == []
assert decode_max_seq_lens == []
assert model.layers[0].self_attn.attn.impl.original_calls == 1
assert torch.equal(output, torch.full_like(output, 2))
def test_fish_kvcache_attn_guard_accepts_decode_metadata_above_small_cap(monkeypatch):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "1")
monkeypatch.setattr(fish_kvcache_attn, "is_available", lambda: True)
assert fish_kvcache_attn.can_use_fish_kvcache_attn(
query=torch.empty((4, 32, 128), dtype=torch.float16),
key_cache=torch.empty((1024, 16, 8, 128), dtype=torch.float16),
value_cache=torch.empty((1024, 16, 8, 128), dtype=torch.float16),
block_table=torch.zeros((4, 1024), dtype=torch.int32),
seq_lens=torch.ones((4,), dtype=torch.int32),
max_query_len=1,
max_seq_len=16384,
dcp_world_size=1,
use_cascade=False,
alibi_slopes=None,
sliding_window=None,
)
def test_fish_kvcache_attn_guard_rejects_seq_len_above_block_table_capacity(monkeypatch):
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "1")
monkeypatch.setattr(fish_kvcache_attn, "is_available", lambda: True)
assert not fish_kvcache_attn.can_use_fish_kvcache_attn(
query=torch.empty((4, 32, 128), dtype=torch.float16),
key_cache=torch.empty((8, 16, 8, 128), dtype=torch.float16),
value_cache=torch.empty((8, 16, 8, 128), dtype=torch.float16),
block_table=torch.zeros((4, 8), dtype=torch.int32),
seq_lens=torch.ones((4,), dtype=torch.int32),
max_query_len=1,
max_seq_len=129,
dcp_world_size=1,
use_cascade=False,
alibi_slopes=None,
sliding_window=None,
)
def test_fish_kvcache_decode_reuses_long_workspace(monkeypatch):
fish_kvcache_attn._WORKSPACE_CACHE.clear()
calls = []
def fake_decode(
query,
key_cache,
value_cache,
block_table,
seq_lens,
out,
scale,
max_seq_len,
small_path_max_seq_len,
long_split_tokens,
partial_m,
partial_l,
partial_acc,
):
del query, key_cache, value_cache, block_table, seq_lens, scale, max_seq_len
del small_path_max_seq_len, long_split_tokens
calls.append((partial_m.data_ptr(), partial_l.data_ptr(), partial_acc.data_ptr(), tuple(partial_acc.shape)))
return out
monkeypatch.setattr(
fish_kvcache_attn._triton_backend(),
"fish_decode_kvcache_attn_triton",
fake_decode,
)
q = torch.zeros((2, 4, 128), dtype=torch.float16)
k_cache = torch.zeros((128, 16, 2, 128), dtype=torch.float16)
v_cache = torch.zeros_like(k_cache)
block_table = torch.zeros((2, 128), dtype=torch.int32)
seq_lens = torch.full((2,), 2048, dtype=torch.int32)
out = torch.empty_like(q)
for _ in range(2):
fish_kvcache_attn.fish_decode_kvcache_attn(
q,
k_cache,
v_cache,
block_table,
seq_lens,
out,
scale=128**-0.5,
max_seq_len=2048,
)
assert len(calls) == 2
assert calls[0] == calls[1]
assert calls[0][3] == (2, 2, 4, 128)
def test_fish_kvcache_decode_workspace_miss_rejects_cuda_graph_capture(monkeypatch):
fish_kvcache_attn._WORKSPACE_CACHE.clear()
monkeypatch.setattr(torch.cuda, "is_current_stream_capturing", lambda: True)
q = torch.zeros((2, 4, 128), dtype=torch.float16)
with pytest.raises(RuntimeError, match="workspace was not prewarmed"):
fish_kvcache_attn._get_decode_workspace(q, 2048)
def test_fish_kvcache_backend_prewarms_capture_workspaces(monkeypatch):
fish_kvcache_attn._WORKSPACE_CACHE.clear()
monkeypatch.setenv("VLLM_OMNI_FISH_KVCACHE_ATTN", "1")
monkeypatch.setattr(fish_kvcache_backend, "is_available", lambda: True)
text_config = type(
"TextConfig",
(),
{"num_attention_heads": 4, "head_dim": 128},
)()
hf_config = type("HFConfig", (), {"text_config": text_config})()
model_config = type(
"ModelConfig",
(),
{
"hf_config": hf_config,
"max_model_len": 2048,
"model_arch": "FishSpeechSlowARForConditionalGeneration",
},
)()
assert (
fish_kvcache_backend.prewarm_fish_kvcache_attn_capture_workspaces(
model_config=model_config,
device=torch.device("cpu"),
dtype=torch.float16,
capture_sizes=[1, 4],
)
== 3
)
monkeypatch.setattr(torch.cuda, "is_current_stream_capturing", lambda: True)
q = torch.zeros((4, 4, 128), dtype=torch.float16)
partial_m, partial_l, partial_acc = fish_kvcache_attn._get_decode_workspace(q, 2048)
assert tuple(partial_m.shape) == (2, 4, 4)
assert tuple(partial_l.shape) == (2, 4, 4)
assert tuple(partial_acc.shape) == (2, 4, 4, 128)
def _reference_decode_attention(
q: torch.Tensor,
k_cache: torch.Tensor,
v_cache: torch.Tensor,
block_table: torch.Tensor,
seq_lens: torch.Tensor,
scale: float,
) -> torch.Tensor:
block_size = int(k_cache.shape[1])
num_q_heads = int(q.shape[1])
num_kv_heads = int(k_cache.shape[2])
outputs = []
for batch_idx in range(q.shape[0]):
seq_len = int(seq_lens[batch_idx].item())
if seq_len <= 0:
outputs.append(torch.zeros_like(q[batch_idx], dtype=torch.float32))
continue
k_rows = []
v_rows = []
for logical_block in range((seq_len + block_size - 1) // block_size):
physical_block = int(block_table[batch_idx, logical_block].item())
start = logical_block * block_size
take = min(block_size, seq_len - start)
k_rows.append(k_cache[physical_block, :take])
v_rows.append(v_cache[physical_block, :take])
k = torch.cat(k_rows, dim=0).to(torch.float32)
v = torch.cat(v_rows, dim=0).to(torch.float32)
per_head = []
for q_head in range(num_q_heads):
kv_head = q_head // (num_q_heads // num_kv_heads)
scores = torch.matmul(k[:, kv_head, :], q[batch_idx, q_head].to(torch.float32)) * scale
weights = torch.softmax(scores, dim=0)
per_head.append(torch.matmul(weights, v[:, kv_head, :]))
outputs.append(torch.stack(per_head, dim=0))
return torch.stack(outputs, dim=0).to(q.dtype)
@pytest.mark.parametrize("seq_len", [64, 128, 256, 512, 1024, 1025, 2048])
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
@pytest.mark.parametrize("batch_size", [1, 3])
@pytest.mark.skipif(
not torch.cuda.is_available() or not fish_kvcache_attn.is_available(),
reason="Fish kvcache Triton attention is not available",
)
def test_fish_kvcache_native_op_matches_reference(seq_len, dtype, batch_size):
torch.manual_seed(0)
device = torch.device("cuda")
block_size = 16
num_q_heads = 4
num_kv_heads = 2
head_dim = 128
max_blocks = (seq_len + block_size - 1) // block_size
num_blocks = batch_size * max_blocks + 3
scale = head_dim**-0.5
q = torch.randn((batch_size, num_q_heads, head_dim), device=device, dtype=dtype)
k_cache = torch.randn((num_blocks, block_size, num_kv_heads, head_dim), device=device, dtype=dtype)
v_cache = torch.randn((num_blocks, block_size, num_kv_heads, head_dim), device=device, dtype=dtype)
block_table = torch.empty((batch_size, max_blocks), device=device, dtype=torch.int32)
for batch_idx in range(batch_size):
pages = torch.arange(batch_idx * max_blocks, (batch_idx + 1) * max_blocks, device=device, dtype=torch.int32)
block_table[batch_idx] = torch.flip(pages, dims=[0])
seq_lens = torch.full((batch_size,), seq_len, device=device, dtype=torch.int32)
out = torch.empty_like(q)
fish_kvcache_attn.fish_decode_kvcache_attn(
q,
k_cache,
v_cache,
block_table,
seq_lens,
out,
scale=scale,
max_seq_len=seq_len,
)
expected = _reference_decode_attention(q, k_cache, v_cache, block_table, seq_lens, scale)
torch.testing.assert_close(out, expected, atol=3e-2, rtol=3e-2)
@pytest.mark.skipif(
not torch.cuda.is_available() or not fish_kvcache_attn.is_available(),
reason="Fish kvcache Triton attention is not available",
)
def test_fish_kvcache_native_op_zero_seq_len_writes_zero():
device = torch.device("cuda")
dtype = torch.float16
block_size = 16
num_q_heads = 4
num_kv_heads = 2
head_dim = 128
max_seq_len = 512
max_blocks = max_seq_len // block_size
scale = head_dim**-0.5
q = torch.randn((1, num_q_heads, head_dim), device=device, dtype=dtype)
k_cache = torch.randn((max_blocks, block_size, num_kv_heads, head_dim), device=device, dtype=dtype)
v_cache = torch.randn_like(k_cache)
block_table = torch.arange(max_blocks, device=device, dtype=torch.int32).reshape(1, max_blocks)
seq_lens = torch.zeros((1,), device=device, dtype=torch.int32)
out = torch.full_like(q, float("nan"))
fish_kvcache_attn.fish_decode_kvcache_attn(
q,
k_cache,
v_cache,
block_table,
seq_lens,
out,
scale=scale,
max_seq_len=max_seq_len,
)
assert torch.isfinite(out).all()
assert torch.equal(out, torch.zeros_like(out))
@pytest.mark.skipif(
not torch.cuda.is_available() or not fish_kvcache_attn.is_available(),
reason="Fish kvcache Triton attention is not available",
)
def test_fish_kvcache_native_op_handles_mixed_short_and_long_seq_lens():
torch.manual_seed(0)
device = torch.device("cuda")
dtype = torch.float16
block_size = 16
num_q_heads = 4
num_kv_heads = 2
head_dim = 128
seq_lens = torch.tensor([512, 2048, 1025], device=device, dtype=torch.int32)
max_seq_len = int(seq_lens.max().item())
batch_size = int(seq_lens.numel())
max_blocks = (max_seq_len + block_size - 1) // block_size
num_blocks = batch_size * max_blocks
scale = head_dim**-0.5
q = torch.randn((batch_size, num_q_heads, head_dim), device=device, dtype=dtype)
k_cache = torch.randn((num_blocks, block_size, num_kv_heads, head_dim), device=device, dtype=dtype)
v_cache = torch.randn((num_blocks, block_size, num_kv_heads, head_dim), device=device, dtype=dtype)
block_table = torch.empty((batch_size, max_blocks), device=device, dtype=torch.int32)
for batch_idx in range(batch_size):
pages = torch.arange(batch_idx * max_blocks, (batch_idx + 1) * max_blocks, device=device, dtype=torch.int32)
block_table[batch_idx] = pages
out = torch.empty_like(q)
fish_kvcache_attn.fish_decode_kvcache_attn(
q,
k_cache,
v_cache,
block_table,
seq_lens,
out,
scale=scale,
max_seq_len=max_seq_len,
)
expected = _reference_decode_attention(q, k_cache, v_cache, block_table, seq_lens, scale)
torch.testing.assert_close(out, expected, atol=3e-2, rtol=3e-2)
@pytest.mark.skipif(
not torch.cuda.is_available() or not fish_kvcache_attn.is_available(),
reason="Fish kvcache Triton attention is not available",
)
def test_fish_kvcache_native_op_handles_zero_short_and_long_seq_lens():
torch.manual_seed(0)
device = torch.device("cuda")
dtype = torch.float16
block_size = 16
num_q_heads = 4
num_kv_heads = 2
head_dim = 128
seq_lens = torch.tensor([0, 512, 2048], device=device, dtype=torch.int32)
max_seq_len = int(seq_lens.max().item())
batch_size = int(seq_lens.numel())
max_blocks = (max_seq_len + block_size - 1) // block_size
num_blocks = batch_size * max_blocks
scale = head_dim**-0.5
q = torch.randn((batch_size, num_q_heads, head_dim), device=device, dtype=dtype)
k_cache = torch.randn((num_blocks, block_size, num_kv_heads, head_dim), device=device, dtype=dtype)
v_cache = torch.randn((num_blocks, block_size, num_kv_heads, head_dim), device=device, dtype=dtype)
block_table = torch.empty((batch_size, max_blocks), device=device, dtype=torch.int32)
for batch_idx in range(batch_size):
pages = torch.arange(batch_idx * max_blocks, (batch_idx + 1) * max_blocks, device=device, dtype=torch.int32)
block_table[batch_idx] = pages
out = torch.full_like(q, float("nan"))
fish_kvcache_attn.fish_decode_kvcache_attn(
q,
k_cache,
v_cache,
block_table,
seq_lens,
out,
scale=scale,
max_seq_len=max_seq_len,
)
expected = _reference_decode_attention(q, k_cache, v_cache, block_table, seq_lens, scale)
assert torch.isfinite(out).all()
torch.testing.assert_close(out, expected, atol=3e-2, rtol=3e-2)
@pytest.mark.skipif(
not torch.cuda.is_available() or not fish_kvcache_attn.is_available(),
reason="Fish kvcache Triton attention is not available",
)
def test_fish_kvcache_native_op_can_be_captured_by_cuda_graph():
device = torch.device("cuda")
dtype = torch.float16
q = torch.randn((2, 4, 128), device=device, dtype=dtype)
k_cache = torch.randn((8, 16, 2, 128), device=device, dtype=dtype)
v_cache = torch.randn((8, 16, 2, 128), device=device, dtype=dtype)
block_table = torch.arange(8, device=device, dtype=torch.int32).reshape(2, 4)
seq_lens = torch.full((2,), 64, device=device, dtype=torch.int32)
out = torch.empty_like(q)
expected = torch.empty_like(q)
fish_kvcache_attn.fish_decode_kvcache_attn(
q,
k_cache,
v_cache,
block_table,
seq_lens,
expected,
scale=128**-0.5,
max_seq_len=64,
)
torch.accelerator.synchronize()
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
fish_kvcache_attn.fish_decode_kvcache_attn(
q,
k_cache,
v_cache,
block_table,
seq_lens,
out,
scale=128**-0.5,
max_seq_len=64,
)
graph.replay()
torch.accelerator.synchronize()
torch.testing.assert_close(out, expected, atol=3e-2, rtol=3e-2)
@pytest.mark.skipif(
not torch.cuda.is_available() or not fish_kvcache_attn.is_available(),
reason="Fish kvcache Triton attention is not available",
)
def test_fish_kvcache_native_long_op_can_be_captured_by_cuda_graph():
device = torch.device("cuda")
dtype = torch.float16
seq_len = 2048
block_size = 16
num_q_heads = 4
num_kv_heads = 2
head_dim = 128
max_blocks = (seq_len + block_size - 1) // block_size
q = torch.randn((2, num_q_heads, head_dim), device=device, dtype=dtype)
k_cache = torch.randn((2 * max_blocks, block_size, num_kv_heads, head_dim), device=device, dtype=dtype)
v_cache = torch.randn((2 * max_blocks, block_size, num_kv_heads, head_dim), device=device, dtype=dtype)
block_table = torch.arange(2 * max_blocks, device=device, dtype=torch.int32).reshape(2, max_blocks)
seq_lens = torch.full((2,), seq_len, device=device, dtype=torch.int32)
out = torch.empty_like(q)
expected = torch.empty_like(q)
fish_kvcache_attn.fish_decode_kvcache_attn(
q,
k_cache,
v_cache,
block_table,
seq_lens,
expected,
scale=head_dim**-0.5,
max_seq_len=seq_len,
)
torch.accelerator.synchronize()
graph = torch.cuda.CUDAGraph()
with torch.cuda.graph(graph):
fish_kvcache_attn.fish_decode_kvcache_attn(
q,
k_cache,
v_cache,
block_table,
seq_lens,
out,
scale=head_dim**-0.5,
max_seq_len=seq_len,
)
graph.replay()
torch.accelerator.synchronize()
torch.testing.assert_close(out, expected, atol=3e-2, rtol=3e-2)