vllm-project--vllm-omni
224 行
8.8 KiB
Python
224 行
8.8 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""Unit tests for NaiveCache merge/split logic used in batched CFG."""
|
|
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
from vllm_omni.diffusion.models.bagel.bagel_transformer import NaiveCache
|
|
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
|
|
|
NUM_LAYERS = 2
|
|
NUM_KV_HEADS = 4
|
|
HEAD_DIM = 8
|
|
|
|
|
|
def _make_cache(num_layers, seq_len, num_kv_heads=NUM_KV_HEADS, head_dim=HEAD_DIM, seed=0):
|
|
"""Create a NaiveCache with deterministic random data. seq_len=0 returns an empty cache."""
|
|
gen = torch.Generator().manual_seed(seed)
|
|
cache = NaiveCache(num_layers)
|
|
if seq_len == 0:
|
|
return cache
|
|
for layer in range(num_layers):
|
|
cache.key_cache[layer] = torch.randn(seq_len, num_kv_heads, head_dim, generator=gen)
|
|
cache.value_cache[layer] = torch.randn(seq_len, num_kv_heads, head_dim, generator=gen)
|
|
return cache
|
|
|
|
|
|
def test_init_creates_none_entries():
|
|
"""Ensure the NaiveCache is initialized with None values per layer."""
|
|
cache = NaiveCache(NUM_LAYERS)
|
|
assert cache.num_layers == NUM_LAYERS
|
|
for layer in range(NUM_LAYERS):
|
|
assert cache.key_cache[layer] is None
|
|
assert cache.value_cache[layer] is None
|
|
|
|
|
|
@pytest.mark.parametrize("seq_len", [0, 10])
|
|
def test_seq_lens_empty(seq_len):
|
|
"""Ensure that by default, we have 0 seq lens."""
|
|
cache = _make_cache(NUM_LAYERS, seq_len=seq_len)
|
|
assert cache.seq_lens == seq_len
|
|
assert cache.num_layers == NUM_LAYERS
|
|
|
|
|
|
### Merge tests
|
|
def test_merge_two_equal_length():
|
|
"""Ensure that we can merge two NaiveCaches that are identically shaped."""
|
|
c0 = _make_cache(NUM_LAYERS, seq_len=5, seed=0)
|
|
c1 = _make_cache(NUM_LAYERS, seq_len=5, seed=1)
|
|
merged = NaiveCache.merge([c0, c1])
|
|
|
|
assert merged.key_values_lens == [5, 5]
|
|
for layer in range(NUM_LAYERS):
|
|
assert merged.key_cache[layer].shape[0] == 10
|
|
# the merged cache will just have keys and values per layer concatenated
|
|
assert torch.equal(merged.key_cache[layer][:5], c0.key_cache[layer])
|
|
assert torch.equal(merged.key_cache[layer][5:], c1.key_cache[layer])
|
|
assert torch.equal(merged.value_cache[layer][:5], c0.value_cache[layer])
|
|
assert torch.equal(merged.value_cache[layer][5:], c1.value_cache[layer])
|
|
|
|
|
|
def test_merge_three_zero_len_cache():
|
|
"""Ensure we handle zero length cache correctly in merge."""
|
|
# NOTE: This is relevant for text_cfg in Bagel, which has a len of 0 by default
|
|
gen_cache = _make_cache(NUM_LAYERS, seq_len=10, seed=0)
|
|
text_cfg_cache = _make_cache(NUM_LAYERS, seq_len=0)
|
|
img_cfg_cache = _make_cache(NUM_LAYERS, seq_len=7, seed=2)
|
|
merged = NaiveCache.merge([gen_cache, text_cfg_cache, img_cfg_cache])
|
|
|
|
assert merged.key_values_lens == [10, 0, 7]
|
|
for layer in range(NUM_LAYERS):
|
|
assert merged.key_cache[layer].shape[0] == 17
|
|
assert torch.equal(merged.key_cache[layer][:10], gen_cache.key_cache[layer])
|
|
assert torch.equal(merged.key_cache[layer][10:], img_cfg_cache.key_cache[layer])
|
|
|
|
|
|
def test_merge_all_empty():
|
|
"""Ensure that merging empty caches is well defined."""
|
|
caches = [_make_cache(NUM_LAYERS, seq_len=0) for _ in range(3)]
|
|
merged = NaiveCache.merge(caches)
|
|
|
|
assert merged.key_values_lens == [0, 0, 0]
|
|
for layer in range(NUM_LAYERS):
|
|
assert merged.key_cache[layer] is None
|
|
assert merged.value_cache[layer] is None
|
|
|
|
|
|
def test_merge_single_cache():
|
|
"""Ensure merging one cache returns an identical cache."""
|
|
c = _make_cache(NUM_LAYERS, seq_len=8, seed=42)
|
|
merged = NaiveCache.merge([c])
|
|
|
|
assert merged.key_values_lens == [8]
|
|
for layer in range(NUM_LAYERS):
|
|
assert torch.equal(merged.key_cache[layer], c.key_cache[layer])
|
|
|
|
|
|
def test_merge_preserves_dtype():
|
|
"""Ensure merging doesn't modify dtypes."""
|
|
cache = NaiveCache(NUM_LAYERS)
|
|
for layer in range(NUM_LAYERS):
|
|
cache.key_cache[layer] = torch.randn(5, NUM_KV_HEADS, HEAD_DIM, dtype=torch.bfloat16)
|
|
cache.value_cache[layer] = torch.randn(5, NUM_KV_HEADS, HEAD_DIM, dtype=torch.bfloat16)
|
|
|
|
merged = NaiveCache.merge([cache])
|
|
assert merged.key_cache[0].dtype == torch.bfloat16
|
|
assert merged.value_cache[0].dtype == torch.bfloat16
|
|
|
|
|
|
### Split tests
|
|
def test_split_all_nonzero():
|
|
"""Ensure NaiveCache splits in the simple case (all lens nonzero)."""
|
|
t = torch.randn(15, NUM_KV_HEADS, HEAD_DIM)
|
|
parts = NaiveCache.split_with_zeros(t, [5, 4, 6])
|
|
|
|
assert len(parts) == 3
|
|
assert all(p is not None for p in parts)
|
|
assert parts[0].shape[0] == 5
|
|
assert parts[1].shape[0] == 4
|
|
assert parts[2].shape[0] == 6
|
|
assert torch.equal(torch.cat(parts), t)
|
|
|
|
|
|
def test_split_with_zero():
|
|
"""Ensure NaiveCache split handles zero length correctly (used in Bagel)."""
|
|
t = torch.randn(17, NUM_KV_HEADS, HEAD_DIM)
|
|
parts = NaiveCache.split_with_zeros(t, [10, 0, 7])
|
|
|
|
assert parts[0].shape[0] == 10
|
|
assert parts[1] is None
|
|
assert parts[2].shape[0] == 7
|
|
assert torch.equal(torch.cat([parts[0], parts[2]]), t)
|
|
|
|
|
|
def test_split_wrong_sum_raises():
|
|
"""Ensure NaiveCache raises if splits don't match the sum of dims on axis 0."""
|
|
t = torch.randn(10, NUM_KV_HEADS, HEAD_DIM)
|
|
with pytest.raises(ValueError, match="dim 0"):
|
|
NaiveCache.split_with_zeros(t, [5, 3])
|
|
|
|
|
|
def test_split_negative_length_raises():
|
|
"""Ensure NaiveCache raises if splits have any negative values."""
|
|
t = torch.randn(10, NUM_KV_HEADS, HEAD_DIM)
|
|
with pytest.raises(ValueError, match="greater than or equal to zero"):
|
|
NaiveCache.split_with_zeros(t, [5, -1, 6])
|
|
|
|
|
|
def test_split_preserves_dtype():
|
|
"""Ensure NaiveCache split preserves dtype."""
|
|
t = torch.randn(10, NUM_KV_HEADS, HEAD_DIM, dtype=torch.bfloat16)
|
|
parts = NaiveCache.split_with_zeros(t, [4, 6])
|
|
assert parts[0].dtype == torch.bfloat16
|
|
assert parts[1].dtype == torch.bfloat16
|
|
|
|
|
|
### from_object tests (for kv cache transfer)
|
|
def test_from_object_passthrough():
|
|
"""Ensure a NaiveCache input is returned as is."""
|
|
cache = _make_cache(NUM_LAYERS, seq_len=5)
|
|
assert NaiveCache.from_object(cache) is cache
|
|
|
|
|
|
def test_from_object_converts_simple_namespace():
|
|
"""Ensure SimpleNamespace with list-based caches converts to NaiveCache."""
|
|
keys = [torch.randn(5, NUM_KV_HEADS, HEAD_DIM) for _ in range(NUM_LAYERS)]
|
|
values = [torch.randn(5, NUM_KV_HEADS, HEAD_DIM) for _ in range(NUM_LAYERS)]
|
|
ns = SimpleNamespace(key_cache=keys, value_cache=values)
|
|
|
|
cache = NaiveCache.from_object(ns)
|
|
|
|
assert isinstance(cache, NaiveCache)
|
|
assert cache.num_layers == NUM_LAYERS
|
|
for i in range(NUM_LAYERS):
|
|
assert torch.equal(cache.key_cache[i], keys[i])
|
|
assert torch.equal(cache.value_cache[i], values[i])
|
|
|
|
|
|
def test_from_object_mismatched_lengths_raises():
|
|
"""Ensure mismatched key/value cache lengths raise due to strict=True in zip."""
|
|
keys = [torch.randn(5, NUM_KV_HEADS, HEAD_DIM) for _ in range(2)]
|
|
values = [torch.randn(5, NUM_KV_HEADS, HEAD_DIM) for _ in range(3)]
|
|
ns = SimpleNamespace(key_cache=keys, value_cache=values)
|
|
|
|
with pytest.raises(ValueError):
|
|
NaiveCache.from_object(ns)
|
|
|
|
|
|
### End to end test for split / merge
|
|
def test_round_trip_two_populated():
|
|
"""Roundtrip test for merging and resplitting two simple caches."""
|
|
c0 = _make_cache(NUM_LAYERS, seq_len=5, seed=0)
|
|
c1 = _make_cache(NUM_LAYERS, seq_len=8, seed=1)
|
|
merged = NaiveCache.merge([c0, c1])
|
|
|
|
for layer in range(NUM_LAYERS):
|
|
k_parts = NaiveCache.split_with_zeros(merged.key_cache[layer], merged.key_values_lens)
|
|
v_parts = NaiveCache.split_with_zeros(merged.value_cache[layer], merged.key_values_lens)
|
|
assert torch.equal(k_parts[0], c0.key_cache[layer])
|
|
assert torch.equal(k_parts[1], c1.key_cache[layer])
|
|
assert torch.equal(v_parts[0], c0.value_cache[layer])
|
|
assert torch.equal(v_parts[1], c1.value_cache[layer])
|
|
|
|
|
|
def test_round_trip_three_branches_with_zero_cfg():
|
|
"""Roundtrip test with a zero entry (i.e., same as Bagel's gen/text_cfg/img_cfg case)."""
|
|
gen_cache = _make_cache(NUM_LAYERS, seq_len=10, seed=0)
|
|
text_cfg_cache = _make_cache(NUM_LAYERS, seq_len=0)
|
|
img_cfg_cache = _make_cache(NUM_LAYERS, seq_len=7, seed=2)
|
|
merged = NaiveCache.merge([gen_cache, text_cfg_cache, img_cfg_cache])
|
|
|
|
for layer in range(NUM_LAYERS):
|
|
k_parts = NaiveCache.split_with_zeros(merged.key_cache[layer], merged.key_values_lens)
|
|
v_parts = NaiveCache.split_with_zeros(merged.value_cache[layer], merged.key_values_lens)
|
|
assert torch.equal(k_parts[0], gen_cache.key_cache[layer])
|
|
assert k_parts[1] is None
|
|
assert torch.equal(k_parts[2], img_cfg_cache.key_cache[layer])
|
|
assert torch.equal(v_parts[0], gen_cache.value_cache[layer])
|
|
assert v_parts[1] is None
|
|
assert torch.equal(v_parts[2], img_cfg_cache.value_cache[layer])
|