222 lines
6.9 KiB
Python
222 lines
6.9 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
from vllm_omni.diffusion.attention.backends import ring_flash_attn, ring_pytorch_attn
|
|
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
|
from vllm_omni.diffusion.attention.backends.ring.ring_selector import AttnType
|
|
from vllm_omni.diffusion.attention.backends.ring.ring_utils import ring_kv_block_valid_length
|
|
from vllm_omni.diffusion.attention.parallel import ring as ring_parallel
|
|
|
|
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
|
|
|
|
|
class _SingleRankComm:
|
|
rank = 0
|
|
world_size = 1
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class _FakeSequenceParallelGroup:
|
|
ring_group: object
|
|
|
|
|
|
def _fake_attention_result(q, _k, _v, **_kwargs):
|
|
return torch.zeros_like(q), torch.zeros(q.shape[0], q.shape[2], q.shape[1])
|
|
|
|
|
|
@pytest.mark.parametrize(("block_rank", "expected"), [(0, 4), (1, 1), (2, 0)])
|
|
def test_global_prefix_is_partitioned_across_ring_blocks(block_rank, expected):
|
|
assert ring_kv_block_valid_length(5, 4, block_rank, 3) == expected
|
|
|
|
|
|
def test_absent_prefix_length_keeps_the_full_ring_block():
|
|
assert ring_kv_block_valid_length(None, 4, 2, 3) == 4
|
|
|
|
|
|
def test_pytorch_ring_trims_padding_from_each_kv_block(monkeypatch):
|
|
captured: dict[str, Any] = {}
|
|
|
|
def fake_attention(q, k, v, **kwargs):
|
|
captured.update(k=k, v=v, **kwargs)
|
|
return _fake_attention_result(q, k, v, **kwargs)
|
|
|
|
monkeypatch.setattr(ring_pytorch_attn, "RingComm", lambda _group: _SingleRankComm())
|
|
monkeypatch.setattr(ring_pytorch_attn, "pytorch_attn_forward", fake_attention)
|
|
qkv = torch.randn(1, 4, 2, 8)
|
|
|
|
output = ring_pytorch_attn.ring_pytorch_attn_func(
|
|
qkv,
|
|
qkv,
|
|
qkv,
|
|
group=object(),
|
|
valid_kv_length=3,
|
|
)
|
|
|
|
assert output.shape == qkv.shape
|
|
assert captured["k"].shape[1] == 3
|
|
assert captured["v"].shape[1] == 3
|
|
|
|
|
|
def test_pytorch_ring_trims_circulated_blocks_by_source_rank(monkeypatch):
|
|
observed_lengths = []
|
|
# Each ring step sends K and V separately.
|
|
blocks = [torch.randn(1, 4, 2, 8) for _ in range(4)]
|
|
|
|
class FakeRingComm:
|
|
rank = 2
|
|
world_size = 3
|
|
|
|
def __init__(self, _group):
|
|
self._next_block = 0
|
|
|
|
def send_recv(self, _tensor):
|
|
block = blocks[self._next_block]
|
|
self._next_block += 1
|
|
return block
|
|
|
|
def commit(self):
|
|
pass
|
|
|
|
def wait(self):
|
|
pass
|
|
|
|
def fake_attention(q, k, v, **kwargs):
|
|
observed_lengths.append(k.shape[1])
|
|
return _fake_attention_result(q, k, v, **kwargs)
|
|
|
|
monkeypatch.setattr(ring_pytorch_attn, "RingComm", FakeRingComm)
|
|
monkeypatch.setattr(ring_pytorch_attn, "pytorch_attn_forward", fake_attention)
|
|
qkv = torch.randn(1, 4, 2, 8)
|
|
|
|
ring_pytorch_attn.ring_pytorch_attn_func(
|
|
qkv,
|
|
qkv,
|
|
qkv,
|
|
group=object(),
|
|
valid_kv_length=5,
|
|
)
|
|
|
|
# Rank 2 starts with global block 2, then receives blocks 1 and 0.
|
|
# The first block is empty and therefore does not launch attention.
|
|
assert observed_lengths == [1, 4]
|
|
|
|
|
|
def test_flash_ring_trims_padding_from_each_kv_block(monkeypatch):
|
|
captured: dict[str, Any] = {}
|
|
|
|
def fake_attention(q, k, v, **kwargs):
|
|
captured.update(k=k, v=v, **kwargs)
|
|
return _fake_attention_result(q, k, v, **kwargs)
|
|
|
|
monkeypatch.setattr(ring_flash_attn, "RingComm", lambda _group: _SingleRankComm())
|
|
monkeypatch.setattr(ring_flash_attn, "select_flash_attn_impl", lambda *_args, **_kwargs: fake_attention)
|
|
qkv = torch.randn(1, 4, 2, 8)
|
|
|
|
output, _ = ring_flash_attn.ring_flash_attn_forward(
|
|
object(),
|
|
qkv,
|
|
qkv,
|
|
qkv,
|
|
softmax_scale=0.25,
|
|
causal=False,
|
|
attn_type=AttnType.FA,
|
|
valid_kv_length=3,
|
|
)
|
|
|
|
assert output.shape == qkv.shape
|
|
assert captured["k"].shape[1] == 3
|
|
assert captured["v"].shape[1] == 3
|
|
|
|
|
|
def test_ring_dispatch_forwards_global_valid_kv_length(monkeypatch):
|
|
captured: dict[str, Any] = {}
|
|
|
|
def fake_ring_attention(*_args, **kwargs):
|
|
captured.update(kwargs)
|
|
return "output"
|
|
|
|
monkeypatch.setattr(ring_pytorch_attn, "ring_pytorch_attn_func", fake_ring_attention)
|
|
strategy = ring_parallel.RingParallelAttention(
|
|
_FakeSequenceParallelGroup(ring_group=object()),
|
|
attn_backend_pref="sdpa",
|
|
)
|
|
metadata = AttentionMetadata(extra={"valid_kv_length": 3})
|
|
query = torch.randn(1, 4, 2, 8)
|
|
|
|
assert strategy.run_attention(query, query, query, metadata) == "output"
|
|
assert captured["valid_kv_length"] == 3
|
|
|
|
|
|
def test_pytorch_ring_rejects_causal_with_a_valid_prefix(monkeypatch):
|
|
"""Trimming a K/V block under causal=True would silently shift the diagonal.
|
|
|
|
The query block keeps the full padded length while the circulated K/V block
|
|
is trimmed, and a causal mask over a rectangular tile is bottom-right
|
|
aligned, so the combination must be refused rather than computed.
|
|
"""
|
|
monkeypatch.setattr(ring_pytorch_attn, "RingComm", lambda _group: _SingleRankComm())
|
|
monkeypatch.setattr(ring_pytorch_attn, "pytorch_attn_forward", _fake_attention_result)
|
|
qkv = torch.randn(1, 4, 2, 8)
|
|
|
|
with pytest.raises(ValueError, match="valid_kv_length is not supported with causal=True"):
|
|
ring_pytorch_attn.ring_pytorch_attn_func(
|
|
qkv,
|
|
qkv,
|
|
qkv,
|
|
causal=True,
|
|
group=object(),
|
|
valid_kv_length=3,
|
|
)
|
|
|
|
|
|
def test_flash_ring_rejects_causal_with_a_valid_prefix(monkeypatch):
|
|
"""Same refusal on the Flash-Attention ring path."""
|
|
monkeypatch.setattr(ring_flash_attn, "RingComm", lambda _group: _SingleRankComm())
|
|
monkeypatch.setattr(
|
|
ring_flash_attn,
|
|
"select_flash_attn_impl",
|
|
lambda *_args, **_kwargs: _fake_attention_result,
|
|
)
|
|
qkv = torch.randn(1, 4, 2, 8)
|
|
|
|
with pytest.raises(ValueError, match="valid_kv_length is not supported with causal=True"):
|
|
ring_flash_attn.ring_flash_attn_forward(
|
|
object(),
|
|
qkv,
|
|
qkv,
|
|
qkv,
|
|
softmax_scale=0.25,
|
|
causal=True,
|
|
attn_type=AttnType.FA,
|
|
valid_kv_length=3,
|
|
)
|
|
|
|
|
|
def test_ring_paths_still_accept_causal_without_a_valid_prefix(monkeypatch):
|
|
"""The guard is scoped to the padded case: plain causal Ring is unchanged."""
|
|
monkeypatch.setattr(ring_flash_attn, "RingComm", lambda _group: _SingleRankComm())
|
|
monkeypatch.setattr(
|
|
ring_flash_attn,
|
|
"select_flash_attn_impl",
|
|
lambda *_args, **_kwargs: _fake_attention_result,
|
|
)
|
|
qkv = torch.randn(1, 4, 2, 8)
|
|
|
|
output, _ = ring_flash_attn.ring_flash_attn_forward(
|
|
object(),
|
|
qkv,
|
|
qkv,
|
|
qkv,
|
|
softmax_scale=0.25,
|
|
causal=True,
|
|
attn_type=AttnType.FA,
|
|
)
|
|
|
|
assert output.shape == qkv.shape
|