Merge main into dev/vllm-align for vLLM 0.31 release

Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
This commit is contained in:
tzhouam committed 2026-10-03 21:05:17 +08:00
commit 008fa25263
252 files changed
+27589 -1237

No files matched your search

@@ -376,10 +376,17 @@ def test_lru_eviction_releases_blocks_and_notifies_pipeline():
def test_budget_reduced_capacity_drives_runner_lru():
pipeline = CapablePipeline(tiny_spec(capacity=2))
# One tiny LingBot-like session requires 128 bytes; two require 192.
# One tiny LingBot-like session requires 1,808 bytes; two require 2,592.
#
# These read 128 and 192 while the paging unit was the frame. tiny_spec
# declares one token per frame, so it used to page a single token at a
# time -- a block size no attention kernel accepts. Paging at 16 makes
# each page sixteen times larger. The block *counts* are unchanged, and
# so is what this test is about: a budget that fits fewer sessions than
# the pipeline asked for must drive the runner's LRU.
runner = make_runner(
pipeline,
available_bytes=128,
available_bytes=2048,
gpu_memory_fraction=1.0,
)
kv = runner.kv_cache
@@ -665,3 +672,106 @@ def test_pipeline_without_warmup_provider_is_safely_skipped(monkeypatch):
monkeypatch.setattr(runner, "execute_model", fail_if_called)
runner._warmup_ar_rollout()
assert execute.called is False
# ── paging unit vs eviction unit ────────────────────────────────────────────
def test_a_frame_that_is_already_a_legal_block_keeps_its_paging():
"""Every resolution that runs today must page exactly as it does today."""
from vllm_omni.experimental.ar_diffusion.runner import paging_block_size
for tokens_per_frame in (16, 320, 1440, 4096):
assert tokens_per_frame % 16 == 0
assert paging_block_size(tokens_per_frame) == tokens_per_frame
def test_a_frame_the_kernel_would_reject_pages_at_the_finest_legal_unit():
"""832x480 is the checkpoint's own default and the kernel rejects it.
(480/16) x (832/16) = 1560 tokens per frame, and 1560 % 16 == 8, so a
frame-sized block is not something FlashAttention's paged kernel accepts.
"""
from vllm_omni.experimental.ar_diffusion.runner import paging_block_size
assert 1560 % 16 == 8
assert paging_block_size(1560) == 16
# And whatever it returns is always something the kernel takes.
for tokens_per_frame in range(1, 200):
assert paging_block_size(tokens_per_frame) % 16 == 0
# ── the kernel's constraint is data, not a constant ─────────────────────────
def _multiple_of(base):
from vllm.v1.attention.backend import MultipleOf
return MultipleOf(base)
def test_the_dispatching_module_answers_in_vllms_own_vocabulary():
"""AR-Diffusion calls flash_attn itself, so no backend can be asked.
The answer has to come from the module that picks the kernel, and it has to
have the shape vLLM uses, so the policy that consumes it does not care
where it came from.
"""
from vllm.v1.attention.backend import MultipleOf
from vllm_omni.experimental.ar_diffusion.kv_cache.paged_attention import supported_kernel_block_sizes
advertised = supported_kernel_block_sizes()
assert advertised, "a kernel that accepts nothing cannot be paged for"
for entry in advertised:
assert isinstance(entry, int | MultipleOf)
def test_a_kernel_that_takes_one_fixed_size_gets_that_size():
"""hpc_attn advertises [64] -- not a multiple, a single legal value.
A frame of 1560 tokens is not 64, and 1560 % 64 == 24, so the frame cannot
be the block and the only thing left to page at is 64. Assuming multiples
of 16 here would hand the kernel a block size it rejects.
"""
from vllm_omni.experimental.ar_diffusion.runner import paging_block_size
assert 1560 % 64 == 24
assert paging_block_size(1560, [64]) == 64
assert paging_block_size(64, [64]) == 64
assert paging_block_size(128, [64]) == 64 # a legal multiple is still not the advertised size
def test_a_kernel_with_large_pages_still_keeps_a_whole_frame_when_it_fits():
"""FlashInfer offers 128-and-up pages on Blackwell and not on Hopper.
The same checkpoint on the two cards must therefore page differently, which
is exactly what a constant cannot express.
"""
from vllm_omni.experimental.ar_diffusion.runner import paging_block_size
hopper = [_multiple_of(16)]
blackwell = [_multiple_of(16), 128, 256]
assert paging_block_size(1440, hopper) == 1440 # a legal multiple of 16
assert paging_block_size(1440, blackwell) == 1440
assert paging_block_size(1560, hopper) == 16
assert paging_block_size(1560, blackwell) == 16 # finest legal unit, not the largest
def test_whatever_comes_back_is_always_something_the_kernel_accepts():
from vllm_omni.experimental.ar_diffusion.runner import paging_block_size
for advertised, check in (
([_multiple_of(16)], lambda n: n % 16 == 0),
([64], lambda n: n == 64),
([_multiple_of(32), 48], lambda n: n % 32 == 0 or n == 48),
):
for tokens_per_frame in range(1, 300):
assert check(paging_block_size(tokens_per_frame, advertised))
def test_a_kernel_that_advertises_nothing_is_an_error_not_a_guess():
from vllm_omni.experimental.ar_diffusion.runner import paging_block_size
with pytest.raises(ValueError, match="no legal block size"):
paging_block_size(1560, [])
@@ -625,6 +625,74 @@ def test_non_contiguous_branch_indices_rejected():
)
# --- eviction against the block table ---------------------------------------
class _RecordingManager(ChunkWindowManager):
"""Capture the block range eviction asks for, without a real block pool.
``remove_skipped_blocks`` reads three attributes and calls one method. A
pool would add nothing: the bug under test is the conversion from the
spec's units into the block table's, and that is entirely visible here.
"""
def __init__(self, spec):
self.kv_cache_spec = spec
self.sliding_window = spec.sliding_window
self.block_size = spec.block_size
self.freed: tuple[int, int] | None = None
def _remove_blocks_in_range(self, request_id, first_block, last_block):
self.freed = (first_block, last_block)
def test_sink_is_preserved_when_a_frame_spans_many_blocks():
# The shipped geometry: 832x480 is 1560 tokens per frame, 1560 % 16 == 8 is
# not a legal block size, so paging falls back to 16 and a frame spans 98
# blocks. Every other eviction test uses chunk_size == BLOCK, where a frame
# is a block and a sink counted in frames indexes the block table correctly
# by accident -- which is why this went unnoticed.
chunk_size, sink_chunks, window_chunks = 1560, 9, 9
spec = ChunkWindowSpec(
block_size=16,
num_kv_heads=4,
head_size=64,
dtype=torch.float16,
sliding_window=window_chunks * chunk_size,
chunk_size=chunk_size,
window_chunks=window_chunks,
sink_chunks=sink_chunks,
)
manager = _RecordingManager(spec)
# Far enough past sink + window that eviction has definitely started.
manager.remove_skipped_blocks("req", total_computed_tokens=30 * chunk_size)
assert manager.freed is not None
first_freed, last_freed = manager.freed
# The assertion is on the token boundary rather than a block count, so it
# stays meaningful if the paging unit changes again.
sink_tokens = sink_chunks * chunk_size
assert first_freed * spec.block_size >= sink_tokens, (
f"eviction starts at token {first_freed * spec.block_size}, inside the {sink_tokens}-token sink"
)
# And it must not over-protect: at most one straddling block beyond the sink.
assert (first_freed - 1) * spec.block_size < sink_tokens
# Only wholly-skipped blocks are released.
skipped = manager.get_num_skipped_tokens(30 * chunk_size)
assert last_freed * spec.block_size <= sink_tokens + skipped
def test_sink_conversion_is_identity_when_a_frame_is_a_block():
# Every geometry that paged one frame per block must be untouched.
spec = make_spec(chunk_size=BLOCK, window_chunks=2, sink_chunks=1)
manager = _RecordingManager(spec)
manager.remove_skipped_blocks("req", total_computed_tokens=8 * BLOCK)
assert manager.freed is not None
first_freed, _ = manager.freed
assert first_freed == spec.sink_chunks
def test_staged_window_reuse_matches_a_full_gather():
"""Reusing the staged history must be byte-identical to re-gathering the whole window.
@@ -23,6 +23,7 @@ from vllm_omni.experimental.ar_diffusion.kv_cache import (
)
from vllm_omni.experimental.ar_diffusion.kv_cache import paged_attention as paged_attention_module
from vllm_omni.experimental.ar_diffusion.kv_cache.config import KV_GATHER_ENV
from vllm_omni.experimental.ar_diffusion.kv_cache.paged import ChunkWindowManager
from vllm_omni.experimental.ar_diffusion.kv_cache.state import ARDiffusionKVState
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
@@ -36,10 +37,26 @@ NEG = "negative"
def make_state(
*, num_layers=1, window_chunks=2, dtype=torch.float32, device=torch.device("cpu"), reuse_history_staging=False
*,
num_layers=1,
window_chunks=2,
chunk_size=BLOCK,
sink_chunks=0,
reset_at_boundary=False,
dtype=torch.float32,
device=torch.device("cpu"),
reuse_history_staging=False,
):
"""Build a cache. ``chunk_size`` defaults to the block size, but the two are
independent -- the shipped 832x480 gives 1560 tokens per frame against
16-token blocks, so a frame is 97.5 blocks."""
cfg = ARDiffusionKVConfig(
enable=True, chunk_size=BLOCK, window_chunks=window_chunks, reuse_history_staging=reuse_history_staging
enable=True,
chunk_size=chunk_size,
window_chunks=window_chunks,
sink_chunks=sink_chunks,
reset_at_boundary=reset_at_boundary,
reuse_history_staging=reuse_history_staging,
)
kv = ARDiffusionKVCache(
cfg,
@@ -109,11 +126,13 @@ def _commit_video_span(
n_chunks: int,
dtype: torch.dtype,
device: torch.device,
chunk_size: int = BLOCK,
) -> tuple[torch.Tensor, torch.Tensor]:
ctx = st.get_kv_caches(kv_branch, seq_len=n_chunks * BLOCK, commit_current=True)[0].forward_ctx
span = n_chunks * chunk_size
ctx = st.get_kv_caches(kv_branch, seq_len=span, commit_current=True)[0].forward_ctx
ctx.ensure_video_slots(device)
k = torch.randn(1, n_chunks * BLOCK, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
v = torch.randn(1, n_chunks * BLOCK, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
k = torch.randn(1, span, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
v = torch.randn(1, span, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
kv._k_pools[0][ctx.current_video_slot_mapping] = k[0]
kv._v_pools[0][ctx.current_video_slot_mapping] = v[0]
st.commit_paged_context(kv_branch)
@@ -547,6 +566,644 @@ def test_custom_op_compiles_fullgraph_without_recompile_on_value_change(monkeypa
torch._dynamo.reset()
# ── a frame that is not a whole number of blocks ────────────────────────────
# 24 tokens per chunk against 16-token blocks leaves 8 over, the same remainder
# the shipped 832x480 produces (1560 % 16 == 8). Every fixture above holds
# chunk_size == BLOCK, so nothing there can reach this path.
RAGGED_CHUNK = 24
@pytest.mark.parametrize("commit_current", [False, True])
@pytest.mark.parametrize("action_len", [0, 3])
def test_a_ragged_chunk_still_matches_the_dense_reference(commit_current, action_len):
"""Attention reads a sequence, not a set of blocks.
With one committed chunk of 24 tokens the history stops 8 slots into its
last block. The committing path writes straight after it and is fine. The
scratch path used to restart at slot zero of a fresh region, which left
those 8 slots unwritten -- and since the kernel consumes the block table as
one contiguous run, it read them as if they were tokens and shifted every
token after them by 8. Shapes stayed correct throughout, so only the values
showed it.
"""
torch.manual_seed(0)
device = torch.device("cpu")
dtype = torch.float32
# window_chunks=2 with one chunk of history keeps the whole sequence
# resident, so the window does not also round up here; that case is
# test_a_ragged_window_keeps_the_block_table_a_fixed_shape below.
kv, st = make_state(dtype=dtype, device=device, window_chunks=2, chunk_size=RAGGED_CHUNK)
history_k, history_v = _commit_video_span(
kv, st, kv_branch=POS, n_chunks=1, dtype=dtype, device=device, chunk_size=RAGGED_CHUNK
)
ctx = st.get_kv_caches(POS, seq_len=RAGGED_CHUNK, commit_current=commit_current)[0].forward_ctx
ctx.ensure_video_slots(device)
assert ctx.start_offset == RAGGED_CHUNK % BLOCK == 8
current_k = torch.randn(1, RAGGED_CHUNK, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
current_v = torch.randn(1, RAGGED_CHUNK, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
kv._k_pools[0][ctx.current_video_slot_mapping] = current_k[0]
kv._v_pools[0][ctx.current_video_slot_mapping] = current_v[0]
action_k = action_v = None
if action_len:
ctx.ensure_action_slots(action_len, device)
action_k = torch.randn(1, action_len, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
action_v = torch.randn(1, action_len, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
kv._k_pools[0][ctx.action_slot_mapping] = action_k[0]
kv._v_pools[0][ctx.action_slot_mapping] = action_v[0]
query = torch.randn(1, RAGGED_CHUNK + action_len, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
block_table, query_start_loc, seq_lens, max_query_len, max_seq_len = ctx.build_block_table(
action_len=action_len, query_len=query.shape[1], device=device
)
paged = ar_diffusion_paged_attention(
query,
kv.key_cache(0),
kv.value_cache(0),
block_table=block_table,
query_start_loc=query_start_loc,
seq_lens=seq_lens,
max_query_len=max_query_len,
max_seq_len=max_seq_len,
softmax_scale=HEAD_DIM**-0.5,
causal=False,
)
new_k = torch.cat([history_k, current_k], dim=1)
new_v = torch.cat([history_v, current_v], dim=1)
if action_len:
new_k = torch.cat([new_k, action_k], dim=1)
new_v = torch.cat([new_v, action_v], dim=1)
ref = _dense_attention(query, new_k, new_v)
torch.testing.assert_close(paged, ref, rtol=1e-5, atol=1e-5)
def test_a_ragged_scratch_chunk_is_physically_continuous_with_its_history():
"""The write targets themselves must leave no hole.
This is the property the values above depend on, asserted directly so a
regression names the cause rather than showing a numeric mismatch.
"""
device = torch.device("cpu")
kv, st = make_state(device=device, window_chunks=2, chunk_size=RAGGED_CHUNK)
_commit_video_span(kv, st, kv_branch=POS, n_chunks=1, dtype=torch.float32, device=device, chunk_size=RAGGED_CHUNK)
ctx = st.get_kv_caches(POS, seq_len=RAGGED_CHUNK, commit_current=False)[0].forward_ctx
ctx.ensure_video_slots(device)
history_tail_block = ctx.history_block_ids[-1]
# The chunk starts inside the history's own last block, not at a fresh one.
assert ctx.current_video_block_ids[0] == history_tail_block
first_slot = int(ctx.current_video_slot_mapping[0])
assert first_slot == history_tail_block * BLOCK + 8
# And the action region must not be charged for that managed block.
ctx.ensure_action_slots(3, device)
assert ctx._scratch_blocks_used == len(ctx.current_video_block_ids) - 1
assert ctx.action_scratch_block_ids[0] not in ctx.current_video_block_ids
def test_a_ragged_window_keeps_the_block_table_a_fixed_shape():
"""The table's shape must not track how full the window is.
With a sink the visible window is 2*24 + 24 = 72 tokens, which is 4.5
blocks. Deriving the width by flooring that would understate the capacity,
the max() against the live block count would take over, and the shape would
change as the window filled -- recompiling the graph. The same floor would
also let max_seq_len come out under the kv_len actually passed.
"""
device = torch.device("cpu")
kv, st = make_state(device=device, window_chunks=2, sink_chunks=1, chunk_size=RAGGED_CHUNK)
# The window is deliberately not a whole number of blocks.
max_video_tokens = kv.spec.sliding_window + kv.spec.sink_chunks * kv.spec.chunk_size
assert max_video_tokens % BLOCK != 0
widths: set[int] = set()
kv_lens: list[int] = []
for _ in range(6):
ctx = st.get_kv_caches(POS, seq_len=RAGGED_CHUNK, commit_current=True)[0].forward_ctx
ctx.ensure_video_slots(device)
block_table, _, seq_lens, _, max_seq_len = ctx.build_block_table(
action_len=0, query_len=RAGGED_CHUNK, device=device
)
widths.add(int(block_table.shape[1]))
# A bound handed to the kernel has to actually bound the sequence.
assert int(seq_lens[0]) <= max_seq_len
kv_lens.append(int(seq_lens[0]))
st.commit_paged_context(POS)
assert len(widths) == 1, f"block table width varied across ticks: {sorted(widths)}"
# The window has to have actually filled, or none of the above was tested.
assert max(kv_lens) > min(kv_lens)
assert max(kv_lens) <= ctx.max_video_blocks * BLOCK
# Whole blocks are read, so each of the two boundaries -- the sink's end and
# the recent window's start -- can contribute up to BLOCK - 1 tokens the
# window no longer needs.
assert max(kv_lens) <= max_video_tokens + 2 * (BLOCK - 1)
def _poison_pools(kv: ARDiffusionKVCache) -> None:
for pool in (*kv._k_pools, *kv._v_pools):
pool.fill_(float("nan"))
def _write_prepared_chunk(
kv: ARDiffusionKVCache, ctx, *, dtype: torch.dtype, device: torch.device
) -> tuple[torch.Tensor, torch.Tensor]:
"""Write fresh K/V for a prepared chunk, poisoning the rest of its blocks first.
Blocks are handed out whole and a reused block keeps whatever it held, so a
slot read without having been written would not look unusual. Every slot of
the chunk's blocks that this chunk does not write is set to NaN first --
except the leading slots of a block the history still occupies -- so reading
one turns the attention output NaN.
"""
mapping = ctx.current_video_slot_mapping
first_slot = int(mapping[0])
for index, block in enumerate(ctx.current_video_block_ids):
start = first_slot if index == 0 else block * BLOCK
kv._k_pools[0][start : (block + 1) * BLOCK] = float("nan")
kv._v_pools[0][start : (block + 1) * BLOCK] = float("nan")
k = torch.randn(ctx.seq_len, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
v = torch.randn(ctx.seq_len, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
kv._k_pools[0][mapping] = k
kv._v_pools[0][mapping] = v
return k, v
def _window_positions(end: int, *, sink: int, window: int, written, history: int, resident: set[int]) -> list[int]:
"""Token positions attention must read at ``end``.
Every token of the current chunk, plus every written history token in a
block that is still resident and holds a sink token or one of the window's
most recent tokens.
"""
start = end - window
blocks = {p // BLOCK for p in range(min(sink, end))} | {p // BLOCK for p in range(max(start, 0), end)}
return [p for p in sorted(written) if p >= history or (p // BLOCK in blocks and p // BLOCK in resident)]
def _model_block_indices(kv, adapter) -> dict[int, int]:
"""Each resident block's index in model positions, by block id.
Compaction removes the evicted gap after the sink's blocks and shifts the
rest of the table down by it, so a table index past the sink is
``compacted_tokens`` behind the position it holds.
"""
block_size = kv.block_size
sink_blocks = -(-(kv.spec.sink_chunks * kv.spec.chunk_size) // block_size)
shift = adapter.compacted_tokens // block_size
return {
int(block): index if index < sink_blocks else index + shift
for index, block in enumerate(kv.block_table(adapter))
if block != kv.null_block_id
}
@pytest.mark.parametrize(
("sink_chunks", "window_chunks", "reset_at_boundary"),
[(1, 2, False), (0, 2, False), (1, 3, False), (2, 2, True), (0, 2, True)],
)
def test_attention_after_eviction_reads_every_kept_token_and_nothing_unwritten(
sink_chunks, window_chunks, reset_at_boundary
):
"""Once the window slides, the table must still cover the window exactly.
The sink and the recent window are separate token ranges, and each can
straddle a block edge on its own: 24-token chunks against 16-token blocks
put a boundary 8 tokens past an edge on alternating chunks. Counting blocks
off the ends of the resident list dropped the block holding the window's
first tokens whenever both boundaries straddled, and the returned length
ran past the last written slot. Neither changes a shape, so this compares
values against dense attention over the tokens the window keeps -- after
eviction, with every unwritten slot poisoned, on both the scratch and the
committing forward of every chunk.
The reset case keeps only the sink across a boundary, so the recent window
reaches back over blocks that are no longer resident. Those must be skipped
by position rather than read.
Eight chunks are enough for compaction to drop the evicted gap from the
table, after which storage positions run behind model positions. Removing
a gap that is not a whole number of chunks moves where eviction snaps, and
in the reset case the window then keeps tokens a boundary should have
dropped.
"""
torch.manual_seed(0)
device, dtype = torch.device("cpu"), torch.float32
kv, st = make_state(
device=device,
window_chunks=window_chunks,
sink_chunks=sink_chunks,
reset_at_boundary=reset_at_boundary,
chunk_size=RAGGED_CHUNK,
)
_poison_pools(kv)
sink, window = sink_chunks * RAGGED_CHUNK, int(kv.spec.sliding_window)
committed: dict[int, tuple[torch.Tensor, torch.Tensor]] = {}
evicted = compacted = False
for chunk in range(8):
history = int(st.adapter(POS).absolute_num_computed_tokens)
end = history + RAGGED_CHUNK
for commit_current in (False, True):
ctx = st.get_kv_caches(POS, seq_len=RAGGED_CHUNK, commit_current=commit_current)[0].forward_ctx
ctx.ensure_video_slots(device)
k, v = _write_prepared_chunk(kv, ctx, dtype=dtype, device=device)
written = dict(committed)
written.update({history + i: (k[i], v[i]) for i in range(RAGGED_CHUNK)})
query = torch.randn(1, RAGGED_CHUNK, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
block_table, query_start_loc, seq_lens, max_query_len, max_seq_len = ctx.build_block_table(
action_len=0, query_len=RAGGED_CHUNK, device=device
)
resident = set(_model_block_indices(kv, st.adapter(POS)).values())
positions = _window_positions(
end, sink=sink, window=window, written=written, history=history, resident=resident
)
label = f"chunk {chunk}, commit_current={commit_current}"
assert int(seq_lens[0]) == len(positions), f"{label}: kv_len does not match the written slots it covers"
paged = ar_diffusion_paged_attention(
query,
kv.key_cache(0),
kv.value_cache(0),
block_table=block_table,
query_start_loc=query_start_loc,
seq_lens=seq_lens,
max_query_len=max_query_len,
max_seq_len=max_seq_len,
softmax_scale=HEAD_DIM**-0.5,
causal=False,
)
ref = _dense_attention(
query,
torch.stack([written[p][0] for p in positions]).unsqueeze(0),
torch.stack([written[p][1] for p in positions]).unsqueeze(0),
)
torch.testing.assert_close(paged, ref, rtol=1e-5, atol=1e-5, msg=lambda m, label=label: f"{label}: {m}")
committed = written
st.commit_paged_context(POS)
compacted = compacted or st.adapter(POS).compacted_tokens > 0
evicted = evicted or compacted or kv.null_block_id in kv.block_table(st.adapter(POS))
assert evicted, "the window never slid, so nothing after eviction was tested"
assert compacted, "the table was never compacted, so storage and model positions never diverged"
@pytest.mark.parametrize(
("sink_chunks", "window_chunks", "reset_at_boundary"),
[(1, 2, False), (0, 2, False), (1, 3, False), (2, 2, True), (0, 2, True)],
)
def test_compaction_does_not_change_which_tokens_stay_resident(
monkeypatch, sink_chunks, window_chunks, reset_at_boundary
):
"""Compaction renumbers the block table; it must not change what eviction keeps.
Storage positions run ``compacted_tokens`` behind model positions, and
eviction snaps to chunk boundaries counted in storage positions. Removing a
gap that is not a whole number of chunks moves every later snap, so eviction
keeps a different set of tokens -- in the reset case, tokens a boundary
should have dropped, which the window then reads. Attention checked against
what is resident cannot see that, so the oracle here is the same rollout
with compaction switched off.
"""
def resident_blocks_per_tick(compact: bool) -> tuple[list[set[int]], int]:
with monkeypatch.context() as patch:
if not compact:
patch.setattr(ChunkWindowManager, "compact_block_table", lambda self, request_id: 0)
kv, st = make_state(
window_chunks=window_chunks,
sink_chunks=sink_chunks,
reset_at_boundary=reset_at_boundary,
chunk_size=RAGGED_CHUNK,
)
ticks = []
for _ in range(12):
ctx = st.get_kv_caches(POS, seq_len=RAGGED_CHUNK, commit_current=True)[0].forward_ctx
ctx.ensure_video_slots(torch.device("cpu"))
ticks.append(set(_model_block_indices(kv, st.adapter(POS)).values()))
st.commit_paged_context(POS)
return ticks, st.adapter(POS).compacted_tokens
expected, _ = resident_blocks_per_tick(compact=False)
actual, compacted_tokens = resident_blocks_per_tick(compact=True)
assert compacted_tokens > 0, "the table was never compacted, so nothing was tested"
for tick, (want, got) in enumerate(zip(expected, actual)):
assert got == want, f"tick {tick}: compaction kept blocks {sorted(got - want)} and dropped {sorted(want - got)}"
def test_history_staging_holds_a_ragged_window_and_restages_it_whole(monkeypatch):
"""The staging buffers must fit the table width this module pads to.
A sink or window that is not a whole number of blocks spans one more block
than its token count suggests, so sizing staging from tokens left it short
and the first prepared forward refused to run. And because such a chunk
shares its first block with the history's tail, the history a probe sees is
not left untouched by the next probe, so it is restaged rather than reused.
"""
monkeypatch.setenv(KV_GATHER_ENV, "1")
device = torch.device("cpu")
kv, st = make_state(
device=device, window_chunks=2, sink_chunks=1, chunk_size=RAGGED_CHUNK, reuse_history_staging=True
)
capacity = int(kv.history_staging[0][0].shape[0])
for chunk in range(6):
for probe in range(2):
ctx = st.get_kv_caches(POS, seq_len=RAGGED_CHUNK, commit_current=False)[0].forward_ctx
ctx.prepare(device=device, action_len=0, query_len=RAGGED_CHUNK)
assert int(ctx.max_seq_len) <= capacity
assert not ctx.reuse_history, f"chunk {chunk}, probe {probe}: a ragged chunk reused the staged history"
ctx = st.get_kv_caches(POS, seq_len=RAGGED_CHUNK, commit_current=True)[0].forward_ctx
ctx.prepare(device=device, action_len=0, query_len=RAGGED_CHUNK)
st.commit_paged_context(POS)
@pytest.mark.parametrize("commit_current", [False, True])
def test_action_tokens_after_a_partly_written_video_block_are_refused(commit_current):
"""Action K/V follows the video blocks in the table, and the kernel reads it as one run.
A 24-token chunk from an empty history ends 8 slots into its second 16-token
block, so the run would read those 8 unwritten slots as the first action
tokens and never reach the last ones. Every shape stays right, which is why
this is refused rather than computed.
"""
_, st = make_state(window_chunks=2, chunk_size=RAGGED_CHUNK)
ctx = st.get_kv_caches(POS, seq_len=RAGGED_CHUNK, commit_current=commit_current)[0].forward_ctx
with pytest.raises(ValueError, match="partly written video block"):
ctx.build_block_table(action_len=3, query_len=RAGGED_CHUNK + 3, device=torch.device("cpu"))
def test_the_shipped_geometry_reads_exactly_the_window_on_every_tick():
"""832x480 with the checkpoint's own sink and window, slot by slot.
A 9-frame sink and a 9-frame window are each 8 tokens past a 16-token edge
but 28080 tokens together -- a whole number of blocks -- so a width taken
from their sum was one block short on every tick once the window had
filled. Twelve ticks of three frames, checking every slot the kernel reads
against the token that slot is supposed to hold. No attention is computed:
at this size the bookkeeping is the whole question.
"""
from vllm_omni.experimental.ar_diffusion.runner import paging_block_size
frame, frames_per_tick = (480 // 16) * (832 // 16), 3
block = paging_block_size(frame)
seq_len = frames_per_tick * frame
cfg = ARDiffusionKVConfig(enable=True, chunk_size=frame, window_chunks=9, sink_chunks=9)
kv = ARDiffusionKVCache(
cfg,
num_layers=1,
num_kv_heads=1,
head_size=8,
dtype=torch.float16,
block_size=block,
max_model_len=1 << 20,
available_bytes=1 << 27,
kv_branches=(ARDiffusionKVBranchSpec(POS, 0),),
session_capacity=1,
frames_per_block=frames_per_tick,
max_scratch_tokens_per_branch=seq_len,
device=torch.device("cpu"),
)
st = ARDiffusionKVState(kv, "s1", {POS: kv.begin_request("r-pos")}, num_layers=1)
sink, window = 9 * frame, int(kv.spec.sliding_window)
holds: dict[int, int] = {} # slot -> token position last written there
widths: set[int] = set()
filled = compacted = False
for tick in range(12):
history = int(st.adapter(POS).absolute_num_computed_tokens)
end = history + seq_len
ctx = st.get_kv_caches(POS, seq_len=seq_len, commit_current=True)[0].forward_ctx
ctx.ensure_video_slots(torch.device("cpu"))
for offset, slot in enumerate(ctx.current_video_slot_mapping.tolist()):
holds[slot] = history + offset
table, _, seq_lens, _, _ = ctx.build_block_table(action_len=0, query_len=seq_len, device=torch.device("cpu"))
widths.add(int(table.shape[1]))
kv_len = int(seq_lens[0])
position_of_block = {b: i * block for b, i in _model_block_indices(kv, st.adapter(POS)).items()}
read = []
for b in table[0].tolist()[: -(-kv_len // block)]:
assert b in position_of_block, f"tick {tick}: read block {b}, which holds no token of this session"
read.extend((b * block + o, position_of_block[b] + o) for o in range(block))
read = read[:kv_len]
wrong = [(slot, want) for slot, want in read if holds.get(slot) != want]
assert not wrong, f"tick {tick}: {len(wrong)} slots read hold a token other than the one their position implies"
start = end - window
kept = set(range(min(sink, end))) | set(range(max(start, 0), end))
missed = kept - {want for _, want in read}
assert not missed, f"tick {tick}: {len(missed)} tokens the window keeps were not read"
filled = filled or end > sink + window
st.commit_paged_context(POS)
compacted = compacted or st.adapter(POS).compacted_tokens > 0
assert filled, "the window never filled, so the case that fails was never reached"
assert compacted, "the table was never compacted, so storage and model positions never diverged"
assert len(widths) == 1, f"block table width varied across ticks: {sorted(widths)}"
class _CountedBlockIds:
"""Wraps a vLLM block manager's ``get_blocks`` and counts the block ids read.
Every path to a request's block ids goes through ``get_blocks``, so a copy of
the whole table is counted in full even if only a few entries are indexed.
"""
def __init__(self, manager):
self.reads = 0
self._get_blocks = manager.get_blocks
def __call__(self, request_id):
from vllm.v1.core.kv_cache_manager import KVCacheBlocks
blocks = self._get_blocks(request_id)
return KVCacheBlocks(tuple([_CountedBlock(block, self) for block in group] for group in blocks.blocks))
class _CountedBlock:
def __init__(self, block, counter: _CountedBlockIds):
self._block = block
self._counter = counter
@property
def block_id(self) -> int:
self._counter.reads += 1
return self._block.block_id
def test_reading_the_window_touches_only_the_blocks_it_can_keep(monkeypatch):
"""However long the block table is, reading the window must not cost more.
Evicted positions stay in the table as null entries until compaction drops
them a whole number of chunks at a time -- up to two frames' worth of blocks
at 832x480 -- so the table can still be far longer than the window. Only the
sink's blocks and the recent window's blocks can hold a kept token, so those
are all the read may touch. Compaction is switched off here so the table
grows to many times the window, and reads are counted where the ids leave
vLLM's block manager.
"""
monkeypatch.setattr(ChunkWindowManager, "compact_block_table", lambda self, request_id: 0)
device = torch.device("cpu")
kv, st = make_state(device=device, window_chunks=2, sink_chunks=1, chunk_size=RAGGED_CHUNK)
reads: list[int] = []
table_lengths: list[int] = []
for _ in range(60):
ctx = st.get_kv_caches(POS, seq_len=RAGGED_CHUNK, commit_current=True)[0].forward_ctx
ctx.ensure_video_slots(device)
counted = _CountedBlockIds(kv.manager)
kv.manager.get_blocks = counted
try:
ctx.build_block_table(action_len=0, query_len=RAGGED_CHUNK, device=device)
finally:
del kv.manager.get_blocks
reads.append(counted.reads)
table_lengths.append(len(kv.block_table(st.adapter(POS))))
st.commit_paged_context(POS)
assert table_lengths[-1] > 10 * ctx.max_video_blocks, "the table never outgrew the window, so nothing was tested"
assert max(reads) <= ctx.max_video_blocks, (
f"read {max(reads)} table entries for a {ctx.max_video_blocks}-block window"
)
def test_the_checkpoints_own_default_resolution_can_build_a_cache():
"""832x480 is what LingBot World v2 ships as its default, and it could not run.
A frame-sized block made the resolution a kernel-compatibility question:
1560 tokens per frame is not a multiple of 16, so FlashAttention's paged
kernel rejected it and the default resolution had no realtime path at all.
"""
from vllm_omni.experimental.ar_diffusion.runner import paging_block_size
tokens_per_frame = (480 // 16) * (832 // 16)
assert tokens_per_frame == 1560
assert tokens_per_frame % 16 == 8, "the whole point is that a frame is not a legal block"
block_size = paging_block_size(tokens_per_frame)
cfg = ARDiffusionKVConfig(enable=True, chunk_size=tokens_per_frame, window_chunks=2)
kv = ARDiffusionKVCache(
cfg,
num_layers=1,
num_kv_heads=N_HEADS,
head_size=HEAD_DIM,
dtype=torch.float32,
block_size=block_size,
max_model_len=1 << 16,
available_bytes=1 << 28,
kv_branches=(ARDiffusionKVBranchSpec(POS, 0),),
session_capacity=1,
frames_per_block=2,
max_scratch_tokens_per_branch=block_size,
device=torch.device("cpu"),
)
assert kv.block_size % 16 == 0
# The eviction unit is still the frame; only the paging unit changed.
assert kv.spec.chunk_size == tokens_per_frame
assert kv.blocks_per_frame == -(-tokens_per_frame // block_size) == 98
def test_the_shipped_resolution_geometry_matches_dense_attention():
"""The real number, not a scaled-down stand-in: 1560 tokens per frame.
This is the case the issue reported failing at ~4e-02 on the scratch path.
It runs on CPU because the reference is dense attention, not a kernel.
"""
torch.manual_seed(0)
device = torch.device("cpu")
dtype = torch.float32
tokens_per_frame = (480 // 16) * (832 // 16)
assert tokens_per_frame == 1560
kv, st = make_state(dtype=dtype, device=device, window_chunks=2, chunk_size=tokens_per_frame)
history_k, history_v = _commit_video_span(
kv, st, kv_branch=POS, n_chunks=1, dtype=dtype, device=device, chunk_size=tokens_per_frame
)
# The non-committing path is the one that was wrong: four of the five
# forwards per generated block take it.
ctx = st.get_kv_caches(POS, seq_len=tokens_per_frame, commit_current=False)[0].forward_ctx
ctx.ensure_video_slots(device)
assert ctx.start_offset == 8, "1560 % 16 == 8 is the whole reason this case exists"
current_k = torch.randn(1, tokens_per_frame, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
current_v = torch.randn(1, tokens_per_frame, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
kv._k_pools[0][ctx.current_video_slot_mapping] = current_k[0]
kv._v_pools[0][ctx.current_video_slot_mapping] = current_v[0]
query = torch.randn(1, tokens_per_frame, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
block_table, query_start_loc, seq_lens, max_query_len, max_seq_len = ctx.build_block_table(
action_len=0, query_len=tokens_per_frame, device=device
)
paged = ar_diffusion_paged_attention(
query,
kv.key_cache(0),
kv.value_cache(0),
block_table=block_table,
query_start_loc=query_start_loc,
seq_lens=seq_lens,
max_query_len=max_query_len,
max_seq_len=max_seq_len,
softmax_scale=HEAD_DIM**-0.5,
causal=False,
)
ref = _dense_attention(
query,
torch.cat([history_k, current_k], dim=1),
torch.cat([history_v, current_v], dim=1),
)
torch.testing.assert_close(paged, ref, rtol=1e-5, atol=1e-5)
@pytest.mark.skipif(not _gpu_flash_attn_usable(), reason="usable GPU FlashAttention is required")
@pytest.mark.parametrize("commit_current", [False, True])
def test_the_shipped_resolution_geometry_on_the_real_kernel(commit_current):
"""The shipped 832x480 geometry through FlashAttention's paged kernel.
The CPU test above proves the addressing is right against a dense
reference. This one proves the real kernel agrees, at the real 1560
tokens per frame, through the production path -- host prep followed by
the fused write+attend op, which is what a forward actually calls.
"""
torch.manual_seed(0)
device = torch.device("cuda")
dtype = torch.float16
tokens_per_frame = (480 // 16) * (832 // 16)
assert tokens_per_frame == 1560
kv, st = make_state(dtype=dtype, device=device, window_chunks=2, chunk_size=tokens_per_frame)
history_k, history_v = _commit_video_span(
kv, st, kv_branch=POS, n_chunks=1, dtype=dtype, device=device, chunk_size=tokens_per_frame
)
layer_ctx = st.get_kv_caches(POS, seq_len=tokens_per_frame, commit_current=commit_current)[0]
ctx = layer_ctx.forward_ctx
current_k = torch.randn(1, tokens_per_frame, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
current_v = torch.randn(1, tokens_per_frame, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
query = torch.randn(1, tokens_per_frame, N_HEADS, HEAD_DIM, dtype=dtype, device=device)
ctx.prepare(device=device, action_len=0, query_len=tokens_per_frame)
assert ctx.start_offset == 8, "1560 % 16 == 8 is the whole reason this case exists"
assert kv.block_size == 16
inputs = layer_ctx.to_layer_inputs()
paged = paged_write_attn(inputs, query[0], current_k[0], current_v[0], None, None, HEAD_DIM**-0.5).unsqueeze(0)
new_k = torch.cat([history_k, current_k], dim=1)[:, -kv.spec.sliding_window :]
new_v = torch.cat([history_v, current_v], dim=1)[:, -kv.spec.sliding_window :]
ref = _dense_attention(query, new_k, new_v)
torch.testing.assert_close(paged, ref, rtol=2e-2, atol=2e-2)
@pytest.mark.skipif(not _gpu_flash_attn_usable(), reason="usable GPU FlashAttention is required")
@pytest.mark.parametrize("history_chunks", [0, 1, 3])
@pytest.mark.parametrize("action_len", [0, 3])
@@ -0,0 +1,359 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
import sys
from dataclasses import replace
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
import torch
from vllm_omni.diffusion.attention.backends.abstract import (
AttentionBackend,
AttentionMetadata,
)
from vllm_omni.diffusion.attention.backends.flash_attn import (
FlashAttentionBackend,
FlashAttentionImpl,
)
from vllm_omni.diffusion.attention.backends.utils import fa
from vllm_omni.diffusion.attention.capabilities import (
CapabilityResult,
CompilationMode,
ExecutionContext,
OuterBoundary,
ParallelStrategy,
SupportStatus,
)
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
_validate_fa4_head_dims = fa.validate_fa4_head_dims
@pytest.fixture(autouse=True)
def kernel_validator(monkeypatch):
# Capability unit tests do not require FA4 or a CUDA device. Real-kernel
# validation is exercised by test_flash_attn_compile.py.
monkeypatch.setattr(fa, "validate_fa4_head_dims", lambda *_args: True)
def _impl(*, kernel_variant: str | None = "fa4", causal: bool = False):
impl = FlashAttentionImpl.__new__(FlashAttentionImpl)
impl._kernel_variant = kernel_variant
impl.causal = causal
return impl
def _resolve(
impl,
*,
context: ExecutionContext | None = None,
attn_metadata: AttentionMetadata | None = None,
dtype=torch.bfloat16,
):
tensor = torch.empty((1, 16, 8, 64), dtype=dtype)
return impl.resolve_execution_path(
context or ExecutionContext(platform="cuda"),
tensor,
tensor,
tensor,
attn_metadata,
)
class _UnmigratedBackend(AttentionBackend):
@staticmethod
def get_name() -> str:
return "UNMIGRATED_TEST"
@staticmethod
def get_impl_cls():
return None
@staticmethod
def get_metadata_cls():
return None
@staticmethod
def get_builder_cls():
return None
@staticmethod
def get_supported_head_sizes() -> list[int]:
return []
def test_capability_result_is_tri_state():
assert CapabilityResult.supported().status is SupportStatus.SUPPORTED
assert CapabilityResult.unsupported("not available").status is SupportStatus.UNSUPPORTED
assert CapabilityResult.unmigrated().status is SupportStatus.UNMIGRATED
with pytest.raises(ValueError, match="actionable reason"):
CapabilityResult(SupportStatus.UNSUPPORTED)
def test_unmigrated_backend_default_is_conservative():
context = ExecutionContext(platform="cuda", require_fullgraph=True)
result = _UnmigratedBackend.resolve_capabilities(context)
assert result.support.status is SupportStatus.UNMIGRATED
assert result.compilation_mode is CompilationMode.EAGER_ONLY
requested = result.requested_support(context)
assert requested.status is SupportStatus.UNSUPPORTED
assert "no verified fullgraph declaration" in requested.reason
assert result.support.status is SupportStatus.UNMIGRATED
assert result.requested_support(replace(context, require_fullgraph=False)).status is SupportStatus.UNMIGRATED
@pytest.mark.parametrize("mode", list(CompilationMode))
def test_fullgraph_request_checks_compilation_mode(mode):
result = replace(_resolve(_impl()), compilation_mode=mode)
context = ExecutionContext(platform="cuda", require_fullgraph=True)
expected = SupportStatus.UNSUPPORTED if mode is CompilationMode.EAGER_ONLY else SupportStatus.SUPPORTED
assert result.requested_support(context).status is expected
assert result.requested_support(replace(context, require_fullgraph=False)).status is SupportStatus.SUPPORTED
def test_request_preserves_verified_rejection_reason():
result = replace(_resolve(_impl()), support=CapabilityResult.unsupported("unsupported head size"))
assert result.requested_support(ExecutionContext(platform="cuda", require_fullgraph=True)) == result.support
def test_exact_cuda_fa4_dense_path_is_supported():
context = ExecutionContext(
platform="cuda",
require_fullgraph=True,
)
result = _resolve(_impl(), context=context)
assert result.path == "fa4_dense"
assert result.support.status is SupportStatus.SUPPORTED
assert result.compilation_mode is CompilationMode.CUSTOM_OP
assert result.requested_support(context).status is SupportStatus.SUPPORTED
def test_causal_fa4_path_remains_unmigrated():
result = _resolve(_impl(causal=True))
assert result.support.status is SupportStatus.UNMIGRATED
def test_fa4_delegates_dimensions_to_kernel(monkeypatch):
head_dims = (80, 48)
validator = Mock(return_value=True)
monkeypatch.setattr(fa, "validate_fa4_head_dims", validator)
query = torch.empty((1, 16, 8, head_dims[0]), dtype=torch.bfloat16)
value = torch.empty((1, 16, 8, head_dims[1]), dtype=torch.bfloat16)
result = _impl().resolve_execution_path(ExecutionContext(platform="cuda"), query, query, value, None)
assert result.support.status is SupportStatus.SUPPORTED
validator.assert_called_once_with(*head_dims, 8)
def test_fa4_reports_kernel_rejection(monkeypatch):
monkeypatch.setattr(fa, "validate_fa4_head_dims", Mock(side_effect=AssertionError("kernel dimension constraint")))
result = _resolve(_impl())
assert result.support.status is SupportStatus.UNSUPPORTED
assert "kernel dimension constraint" in result.support.reason
assert "another backend" in result.support.reason
def test_fa4_missing_validator_remains_unmigrated(monkeypatch):
monkeypatch.setattr(fa, "validate_fa4_head_dims", lambda *_args: False)
result = _resolve(_impl())
assert result.support.status is SupportStatus.UNMIGRATED
assert result.compilation_mode is CompilationMode.EAGER_ONLY
def test_fa4_rejects_mismatched_query_key_dimensions():
query = torch.empty((1, 16, 8, 64), dtype=torch.bfloat16)
key = torch.empty((1, 16, 8, 32), dtype=torch.bfloat16)
result = _impl().resolve_execution_path(ExecutionContext(platform="cuda"), query, key, key, None)
assert result.support.status is SupportStatus.UNSUPPORTED
assert "Q and K head dimensions must match" in result.support.reason
@pytest.mark.parametrize("arch", [90, 100, 120])
def test_fa4_validator_adapter_uses_selected_architecture(monkeypatch, arch):
validator = Mock()
monkeypatch.setitem(
sys.modules,
"flash_attn.cute.interface",
SimpleNamespace(_get_device_arch=lambda: arch, _validate_head_dims=validator),
)
# Call the real adapter, not the fixture's stand-in.
verified = _validate_fa4_head_dims(80, 48, 8)
if arch == 120:
assert not verified
validator.assert_not_called()
else:
assert verified
validator.assert_called_once_with(80, 48, arch // 10, 8)
def test_fa4_validator_adapter_handles_missing_private_api(monkeypatch):
monkeypatch.setitem(sys.modules, "flash_attn.cute.interface", SimpleNamespace())
assert not _validate_fa4_head_dims(64, 64, 8)
def test_initialized_kernel_identity_overrides_caller_claim():
result = _resolve(
_impl(kernel_variant=None),
context=ExecutionContext(
platform="cuda",
kernel_variant="fa4",
),
)
assert result.support.status is SupportStatus.UNMIGRATED
def test_preconstruction_without_resolved_cuda_kernel_is_unmigrated():
result = FlashAttentionBackend.resolve_capabilities(ExecutionContext(platform="cuda"))
assert result.support.status is SupportStatus.UNMIGRATED
assert result.path == "unverified"
@pytest.mark.parametrize(
"changes",
[
{"platform": "rocm"},
{"paged_kv": True},
{"parallel_strategy": ParallelStrategy.ULYSSES},
{"parallel_strategy": ParallelStrategy.RING},
{"parallel_strategy": ParallelStrategy.HYBRID_ULYSSES_RING},
{"outer_boundaries": frozenset({OuterBoundary.HSDP})},
],
)
def test_unverified_flash_attention_variants_remain_unmigrated(changes):
result = _resolve(
_impl(),
context=replace(ExecutionContext(platform="cuda"), **changes),
)
assert result.support.status is SupportStatus.UNMIGRATED
assert result.compilation_mode is CompilationMode.EAGER_ONLY
def test_unverified_dtype_remains_unmigrated():
result = _resolve(_impl(), dtype=torch.float32)
assert result.support.status is SupportStatus.UNMIGRATED
@pytest.mark.parametrize(
"attn_metadata",
[
AttentionMetadata(full_attn_spans=[[(0, 8)]]),
AttentionMetadata(
extra={
"cu_seqlens_q": torch.tensor([0, 16], dtype=torch.int32),
"cu_seqlens_k": torch.tensor([0, 16], dtype=torch.int32),
"max_seqlen_q": 16,
"max_seqlen_k": 16,
}
),
AttentionMetadata(extra={"kv_cache_dtype": "fp8"}),
],
)
def test_metadata_normalization_prevents_dense_support(attn_metadata):
result = _resolve(_impl(), attn_metadata=attn_metadata)
assert result.support.status is SupportStatus.UNMIGRATED
def test_unpublished_all_true_mask_remains_runtime_dependent():
metadata = AttentionMetadata(
attn_mask=torch.ones((1, 16), dtype=torch.bool),
)
result = _resolve(_impl(), attn_metadata=metadata)
assert result.path == "runtime_mask_dependent"
assert result.support.status is SupportStatus.UNMIGRATED
def test_producer_published_noop_mask_resolves_dense_fa4():
metadata = AttentionMetadata(
attn_mask=torch.ones((1, 16), dtype=torch.bool),
extra={"attention_mask_mode": "none"},
)
result = _resolve(_impl(), attn_metadata=metadata)
assert result.path == "fa4_dense"
assert result.support.status is SupportStatus.SUPPORTED
def test_fixed_shape_mask_semantic_change_invalidates_dense_resolution():
metadata = AttentionMetadata(
attn_mask=torch.ones((1, 16), dtype=torch.bool),
extra={"attention_mask_mode": "none"},
)
impl = _impl()
dense = _resolve(impl, attn_metadata=metadata)
assert dense.support.status is SupportStatus.SUPPORTED
metadata.attn_mask[:, 8:] = False
metadata.extra["attention_mask_mode"] = "padding"
masked = _resolve(impl, attn_metadata=metadata)
assert masked.support.status is SupportStatus.UNMIGRATED
request = ExecutionContext(platform="cuda", require_fullgraph=True)
assert masked.requested_support(request).status is SupportStatus.UNSUPPORTED
def test_piecewise_dispatch_ignores_unused_incomplete_packed_metadata(monkeypatch):
from vllm_omni.diffusion.attention.backends import flash_attn
from vllm_omni.diffusion.attention.backends.utils import fa
metadata = AttentionMetadata(full_attn_spans=[[(0, 16)]], extra={"max_seqlen_q": 16})
impl = _impl()
impl.fa_deterministic = False
impl.softmax_scale = 0.125
assert _resolve(impl, attn_metadata=metadata).support.status is SupportStatus.UNMIGRATED
query = torch.empty((1, 16, 8, 64), dtype=torch.bfloat16)
monkeypatch.setattr(fa, "HAS_FLASH_ATTN", True)
monkeypatch.setattr(fa, "flash_attn_func", lambda *args, **kwargs: query)
monkeypatch.setattr(flash_attn, "piecewise_attn", lambda *args, **kwargs: query)
assert impl.forward_cuda(query, query, query, metadata) is query
@pytest.mark.parametrize("input_index", [1, 2])
@pytest.mark.parametrize("mismatch", ["dtype", "device"])
def test_fa4_rejects_incompatible_key_value(input_index, mismatch):
tensors = [torch.empty((1, 16, 8, 64), dtype=torch.bfloat16) for _ in range(3)]
tensors[input_index] = (
tensors[input_index].to(dtype=torch.float16) if mismatch == "dtype" else tensors[input_index].to("meta")
)
result = _impl().resolve_execution_path(ExecutionContext(platform="cuda"), *tensors, None)
assert result.support.status is SupportStatus.UNSUPPORTED
assert f"{mismatch}s must match" in result.support.reason
@pytest.mark.parametrize("skip_quant", [False, True])
def test_layer_resolution_uses_effective_kv_quantization(skip_quant):
from vllm_omni.diffusion.attention.layer import Attention
from vllm_omni.diffusion.attention.parallel.base import NoParallelAttention
layer = Attention.__new__(Attention)
torch.nn.Module.__init__(layer)
layer.attention = _impl(kernel_variant=None)
layer._hsdp_compile_boundary_enabled = False
layer.skip_sequence_parallel = False
layer._no_parallel_strategy = NoParallelAttention()
layer.parallel_strategy = layer._no_parallel_strategy
layer.use_ring = False
layer.paged_kv_cache_role = None
layer._kv_cache_dtype = "fp8"
layer._disable_kv_quant = False
layer._kv_cache_skip_steps = None
layer._kv_cache_skip_layers = {0} if skip_quant else None
layer.layer_idx = 0
query = torch.empty((1, 16, 8, 64), dtype=torch.bfloat16)
metadata = AttentionMetadata(extra={"kv_cache_dtype": "fp8"})
result = layer.resolve_execution_path(ExecutionContext(platform="npu"), query, query, query, metadata)
assert result.path == ("npu_dense" if skip_quant else "npu_unverified")
assert result.support.status is SupportStatus.UNMIGRATED
assert metadata.extra == {"kv_cache_dtype": "fp8"}
@@ -0,0 +1,118 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Stateful contract example, independent of an optional attention kernel.
The prepared module owns tensor state and passes it explicitly to an opaque op.
This deliberately does not model opaque Python/C++ planning handles.
"""
import gc
import weakref
import pytest
import torch
import torch.nn.functional as F
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
@torch.library.custom_op("vllm_omni_test::planned_attention", mutates_args=())
def _planned_attention(
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
query_scale: torch.Tensor,
) -> torch.Tensor:
return F.scaled_dot_product_attention(query * query_scale, key, value).contiguous()
@_planned_attention.register_fake
def _planned_attention_fake(query, key, value, query_scale):
return query.new_empty((*query.shape[:-1], value.shape[-1]))
class _PreparedAttention(torch.nn.Module):
"""Initialization/planning runs eagerly; forward only executes tensors."""
def __init__(self, query_scale: torch.Tensor):
super().__init__()
self.register_buffer("query_scale", query_scale.detach().clone())
def forward(self, query, key, value):
return _planned_attention(query, key, value, self.query_scale)
def _inputs(length):
generator = torch.Generator().manual_seed(length)
return tuple(torch.randn(2, 3, length, 8, generator=generator) for _ in range(3))
def _reference(plan, inputs):
query, key, value = inputs
return F.scaled_dot_product_attention(query * plan.query_scale, key, value)
def test_dynamic_replay_uses_each_instances_state_without_recompilation():
compile_count = 0
def counting_backend(graph_module, _example_inputs):
nonlocal compile_count
compile_count += 1
return graph_module.forward
# Same tensor metadata, different values: state must be a graph input,
# never a Python instance identifier or a captured first-instance constant.
plans = [_PreparedAttention(torch.full((1, 3, 1, 1), scale)) for scale in (0.5, 2.0)]
compiled = torch.compile(
lambda plan, q, k, v: plan(q, k, v),
backend=counting_backend,
fullgraph=True,
dynamic=True,
)
for length in (5, 9, 13):
inputs = _inputs(length)
for plan in plans:
torch.testing.assert_close(compiled(plan, *inputs), _reference(plan, inputs))
assert not torch.allclose(_reference(plans[0], inputs), _reference(plans[1], inputs))
assert compile_count == 1
def test_compiled_callable_retains_state_until_released():
plan = _PreparedAttention(torch.full((1, 3, 1, 1), 0.5))
plan_ref = weakref.ref(plan)
state_ref = weakref.ref(plan.query_scale)
inputs = _inputs(5)
expected = _reference(plan, inputs)
compiled = torch.compile(plan, backend="eager", fullgraph=True, dynamic=True)
torch.testing.assert_close(compiled(*inputs), expected)
# Deleting the caller's reference must not invalidate subsequent replay.
del plan
gc.collect()
assert plan_ref() is not None
assert state_ref() is not None
torch.testing.assert_close(compiled(*inputs), expected)
del compiled
gc.collect()
assert plan_ref() is None
assert state_ref() is None
def test_stateful_custom_op_schema_and_fake_implementation():
inputs = _inputs(5)
# Unequal Q/K lengths and V head dimensions exercise the fake output shape.
query, _, _ = inputs
key = torch.randn(2, 3, 7, 8)
value = torch.randn(2, 3, 7, 4)
scale = torch.full((1, 3, 1, 1), 0.5)
torch.library.opcheck(_planned_attention, (query, key, value, scale))
def test_stateful_attention_compiles_with_inductor():
plan = _PreparedAttention(torch.full((1, 3, 1, 1), 0.5))
compiled = torch.compile(plan, fullgraph=True, dynamic=True)
for length in (5, 9):
inputs = _inputs(length)
torch.testing.assert_close(compiled(*inputs), _reference(plan, inputs))
@@ -21,6 +21,7 @@ from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
from vllm_omni.diffusion.attention.backends.flash_attn import FlashAttentionImpl
from vllm_omni.diffusion.attention.backends.sdpa import SDPAImpl
from vllm_omni.diffusion.attention.backends.utils import fa # noqa: E402
from vllm_omni.diffusion.attention.capabilities import CompilationMode, ExecutionContext, SupportStatus
from vllm_omni.platforms import current_omni_platform
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion]
@@ -432,6 +433,35 @@ def test_varlen_masked_routing_by_role(monkeypatch):
assert cu_seqlens_k.tolist() == [0, 2, 5]
@pytest.mark.cpu
def test_published_noop_mask_uses_dense_path(monkeypatch):
calls = []
def fake_dense_func(q, k, v, **_kwargs):
calls.append("dense")
return q
def fail_varlen(*_args, **_kwargs):
raise AssertionError("published no-op mask must not use varlen attention")
monkeypatch.setattr(fa, "HAS_FLASH_ATTN", True)
monkeypatch.setattr(fa, "IS_FLASH_ATTN_4", False)
monkeypatch.setattr(fa, "flash_attn_func", fake_dense_func)
monkeypatch.setattr(fa, "flash_attn_varlen_func", fail_varlen)
impl = FlashAttentionImpl(num_heads=2, head_size=8, softmax_scale=0.5, causal=False)
query = torch.randn(1, 4, 2, 8)
metadata = AttentionMetadata(
attn_mask=torch.ones((1, 4), dtype=torch.bool),
extra={"attention_mask_mode": "none"},
)
output = impl.forward_cuda(query, query, query, metadata)
assert torch.equal(output, query)
assert calls == ["dense"]
@pytest.mark.cpu
def test_piecewise_flash_attn_uses_varlen_fallback(monkeypatch):
calls = []
@@ -550,6 +580,199 @@ def _fake_torch_npu(monkeypatch, *, npu_fusion_attention=None):
return fake
@pytest.fixture
def npu_contract(monkeypatch, mocker):
monkeypatch.delenv("MINDIE_SD_FA_TYPE", raising=False)
monkeypatch.setattr(fa, "validate_fa4_head_dims", lambda *_args: pytest.fail("NPU must not validate FA4"))
query = torch.zeros(1, 8, 2, 4)
dense = mocker.Mock(return_value=torch.zeros_like(query))
packed = mocker.Mock(return_value=torch.zeros_like(query.squeeze(0)))
_fake_mindiesd(monkeypatch, attention_forward=dense, attention_forward_varlen=packed)
return _npu_impl(), query, dense, packed
def _assert_platform_path(impl, platform, query, metadata, path):
context = ExecutionContext(platform=platform, require_fullgraph=True)
result = impl.resolve_execution_path(context, query, query, query, metadata)
assert result.path == path
assert result.kernel_variant == {"npu": "mindiesd", "rocm": "aiter"}[platform]
assert result.support.status is SupportStatus.UNMIGRATED
assert result.compilation_mode is CompilationMode.EAGER_ONLY
assert result.requested_support(context).status is SupportStatus.UNSUPPORTED
def _real_pad_metadata():
cu = torch.tensor([0, 5, 8], dtype=torch.int32)
return AttentionMetadata(
extra={
"npu_attn_varlen": True,
"cu_seqlens_q": cu,
"cu_seqlens_k": cu,
"max_seqlen_q": 5,
"max_seqlen_k": 5,
}
)
@pytest.mark.cpu
@pytest.mark.parametrize("kv_cache_dtype", [None, "float", "auto"])
def test_npu_contract_dense_dispatch(npu_contract, kv_cache_dtype):
impl, query, dense, packed = npu_contract
metadata = AttentionMetadata(extra={"kv_cache_dtype": kv_cache_dtype})
_assert_platform_path(impl, "npu", query, metadata, "npu_dense")
impl.forward_npu(query, query, query, metadata)
dense.assert_called_once()
packed.assert_not_called()
assert dense.call_args.kwargs["attn_mask"] is None
@pytest.mark.cpu
def test_npu_contract_mask_layout(npu_contract):
impl, query, dense, packed = npu_contract
mask = torch.arange(8)[None] < 5
metadata = AttentionMetadata(attn_mask=mask)
_assert_platform_path(impl, "npu", query, metadata, "npu_masked")
impl.forward_npu(query, query, query, metadata)
dense.assert_called_once()
packed.assert_not_called()
torch.testing.assert_close(dense.call_args.kwargs["attn_mask"], mask[:, None, None, :].expand(1, 1, 8, 8))
@pytest.mark.cpu
def test_npu_contract_packed_dispatch(npu_contract):
impl, query, dense, packed = npu_contract
metadata = _real_pad_metadata()
_assert_platform_path(impl, "npu", query, metadata, "npu_packed_varlen")
impl.forward_npu(query, query, query, metadata)
dense.assert_not_called()
packed.assert_called_once()
assert packed.call_args.args[3:] == ([0, 5, 8], [0, 5, 8])
@pytest.mark.cpu
def test_npu_contract_prefix_dispatch(npu_contract, monkeypatch):
impl, query, dense, packed = npu_contract
metadata = _real_pad_metadata()
_assert_platform_path(impl, "npu", query, metadata, "npu_packed_varlen")
# The same metadata resolves differently after the kernel choice changes.
monkeypatch.setenv("MINDIE_SD_FA_TYPE", "ascend_laser_attention")
_assert_platform_path(impl, "npu", query, metadata, "npu_prefix_kv_slice")
impl.forward_npu(query, query, query, metadata)
dense.assert_called_once()
packed.assert_not_called()
torch.testing.assert_close(dense.call_args.args[1], query[:, :5])
torch.testing.assert_close(dense.call_args.args[2], query[:, :5])
assert dense.call_args.kwargs["attn_mask"] is None
@pytest.mark.cpu
def test_npu_contract_incomplete_packing_rebuilds_mask(npu_contract):
impl, query, dense, packed = npu_contract
# CUDA-like normalization rejects this metadata, but NPU can rebuild a mask.
metadata = AttentionMetadata(extra={"npu_attn_varlen": True, "max_seqlen_q": 5, "valid_kv_length": 5})
_assert_platform_path(impl, "npu", query, metadata, "npu_masked_fallback")
impl.forward_npu(query, query, query, metadata)
dense.assert_called_once()
packed.assert_not_called()
mask = (torch.arange(8) < 5)[None, None, None, :].expand(1, 1, 8, 8)
torch.testing.assert_close(dense.call_args.kwargs["attn_mask"], mask)
@pytest.mark.cpu
def test_npu_contract_unusable_packing_does_not_run_unmasked(npu_contract):
impl, query, dense, packed = npu_contract
metadata = AttentionMetadata(extra={"npu_attn_varlen": True})
_assert_platform_path(impl, "npu", query, metadata, "npu_masked_fallback")
with pytest.raises(ValueError, match="refusing to run unmasked attention"):
impl.forward_npu(query, query, query, metadata)
dense.assert_not_called()
packed.assert_not_called()
@pytest.fixture
def rocm_contract(monkeypatch, mocker):
query = torch.zeros(1, 8, 2, 4)
dense = mocker.Mock(return_value=torch.zeros_like(query))
packed = mocker.Mock(side_effect=lambda *args, **kwargs: torch.zeros_like(args[0] if args else kwargs["q"]))
monkeypatch.setattr(fa, "IS_FLASH_ATTN_4", False)
monkeypatch.setattr(fa, "IS_AITER", True)
monkeypatch.setattr(fa, "HAS_FLASH_ATTN", True)
monkeypatch.setattr(fa, "flash_attn_func", dense)
monkeypatch.setattr(fa, "flash_attn_varlen_func", packed)
monkeypatch.setattr(fa, "validate_fa4_head_dims", lambda *_args: pytest.fail("ROCm must not validate FA4"))
return _npu_impl(), query, dense, packed
@pytest.mark.cpu
def test_rocm_contract_dense_dispatch(rocm_contract):
impl, query, dense, packed = rocm_contract
_assert_platform_path(impl, "rocm", query, None, "rocm_dense")
impl.forward_hip(query, query, query)
dense.assert_called_once()
packed.assert_not_called()
@pytest.mark.cpu
def test_rocm_contract_masked_unpadding(rocm_contract):
impl, query, dense, packed = rocm_contract
metadata = AttentionMetadata(attn_mask=torch.arange(8)[None] < 5, extra={"attention_mask_mode": "padding"})
_assert_platform_path(impl, "rocm", query, metadata, "rocm_masked_varlen")
impl.forward_hip(query, query, query, metadata)
dense.assert_not_called()
packed.assert_called_once()
assert packed.call_args.args[0].shape == (5, 2, 4)
torch.testing.assert_close(packed.call_args.kwargs["cu_seqlens_k"], torch.tensor([0, 5], dtype=torch.int32))
@pytest.mark.cpu
def test_rocm_contract_multiple_packed_documents(rocm_contract):
impl, query, dense, packed = rocm_contract
cu = torch.tensor([0, 2, 4, 8], dtype=torch.int32)
metadata = AttentionMetadata(
extra={
"cu_seqlens_q": cu,
"cu_seqlens_k": cu,
"max_seqlen_q": 4,
"max_seqlen_k": 4,
}
)
_assert_platform_path(impl, "rocm", query, metadata, "rocm_packed_varlen")
# NPU's [real, pad] path cannot consume this packing contract.
assert impl._resolve_packed_seq_npu(query, query, metadata.extra) is None
impl.forward_hip(query, query, query, metadata)
dense.assert_not_called()
packed.assert_called_once()
assert packed.call_args.kwargs["cu_seqlens_q"] is cu
assert packed.call_args.kwargs["cu_seqlens_k"] is cu
@pytest.mark.cpu
def test_rocm_contract_unknown_mask_stays_runtime_dependent(rocm_contract, monkeypatch):
impl, query, dense, packed = rocm_contract
metadata = AttentionMetadata(attn_mask=torch.ones(1, 8, dtype=torch.bool))
# Resolution may inspect shape/metadata, but must not read mask values.
with monkeypatch.context() as patch:
patch.setattr(torch, "any", lambda *_args, **_kwargs: pytest.fail("resolution read mask values"))
_assert_platform_path(impl, "rocm", query, metadata, "rocm_runtime_mask_dependent")
impl.forward_hip(query, query, query, metadata)
dense.assert_called_once()
packed.assert_not_called()
@pytest.mark.cpu
def test_rocm_contract_requires_initialized_aiter(monkeypatch):
monkeypatch.setattr(fa, "IS_FLASH_ATTN_4", False)
monkeypatch.setattr(fa, "IS_AITER", False)
impl = _npu_impl()
query = torch.zeros(1, 8, 2, 4)
result = impl.resolve_execution_path(
ExecutionContext(platform="rocm", kernel_variant="aiter"), query, query, query, None
)
assert result.path == "rocm_unverified"
assert result.kernel_variant is None
assert result.support.status is SupportStatus.UNMIGRATED
# --- Test group A: boundary resolution (_resolve_packed_seq_npu) -------------
@@ -0,0 +1,308 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import functools
from dataclasses import dataclass
import pytest
import torch
from tests.helpers.mark import hardware_test
from vllm_omni.diffusion.attention.backends.flash_attn import (
FlashAttentionBackend,
FlashAttentionImpl,
)
from vllm_omni.diffusion.attention.backends.utils import fa
from vllm_omni.diffusion.attention.capabilities import (
CompilationMode,
ExecutionContext,
OuterBoundary,
ParallelStrategy,
SupportStatus,
)
from vllm_omni.diffusion.attention.parallel.base import NoParallelAttention
from vllm_omni.diffusion.data import DiffusionParallelConfig, OmniDiffusionConfig
from vllm_omni.diffusion.forward_context import ForwardContext
from vllm_omni.platforms import current_omni_platform
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cuda]
@pytest.fixture(autouse=True)
def isolated_compiler_cache():
# Each case changes kernel/configuration on the same Attention.forward code
# object. Keep unrelated cases from exhausting its Dynamo recompile limit;
# retain the cache across all shapes and replays within an individual test.
torch.compiler.reset()
yield
torch.compiler.reset()
@dataclass
class _ParallelStrategyStub:
name: str
enabled: bool = True
def _make_attention_layer(monkeypatch, *, head_size=64):
from vllm_omni.diffusion.attention import layer as attention_layer
monkeypatch.setattr(
attention_layer,
"get_attn_backend_for_role",
lambda **_kwargs: (FlashAttentionBackend, None),
)
monkeypatch.setattr(
attention_layer,
"build_parallel_attention_strategy",
lambda **_kwargs: NoParallelAttention(),
)
return attention_layer.Attention(num_heads=8, head_size=head_size, softmax_scale=head_size**-0.5, causal=False)
@pytest.fixture
def fake_fa4(monkeypatch, tmp_path):
marker = tmp_path / "kernel"
marker.write_text("loaded", encoding="utf-8")
@functools.cache
def cached_kernel_loader():
with open(marker, encoding="utf-8") as handle:
handle.read()
def fake_attention(query, key, value, **kwargs):
cached_kernel_loader()
return (
torch.nn.functional.scaled_dot_product_attention(
query.transpose(1, 2),
key.transpose(1, 2),
value.transpose(1, 2),
scale=kwargs["softmax_scale"],
is_causal=kwargs["causal"],
)
.transpose(1, 2)
.contiguous()
)
monkeypatch.setattr(fa, "HAS_FLASH_ATTN", True)
monkeypatch.setattr(fa, "IS_FLASH_ATTN_4", True)
monkeypatch.setattr(fa, "flash_attn_func", fake_attention)
monkeypatch.setattr(fa, "validate_fa4_head_dims", lambda *_args: True)
return FlashAttentionImpl(
num_heads=8,
head_size=64,
softmax_scale=0.125,
causal=False,
)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.parametrize("noncontiguous", [False, True])
def test_fa4_dense_dispatch_is_opaque_to_dynamic_torch_compile(fake_fa4, noncontiguous):
"""Fast Dynamo/custom-op regression; Inductor coverage is separate."""
impl = fake_fa4
q = torch.randn(1, 16, 8, 64, device="cuda", dtype=torch.bfloat16)
if noncontiguous:
q = q.transpose(1, 2).contiguous().transpose(1, 2)
assert not q.is_contiguous()
path = impl.resolve_execution_path(
ExecutionContext(
platform="cuda",
kernel_variant="fa4",
dtype="bfloat16",
require_fullgraph=True,
),
q,
q,
q,
None,
)
assert path.support.status is SupportStatus.SUPPORTED
assert path.compilation_mode is CompilationMode.CUSTOM_OP
compile_count = 0
def counting_backend(graph_module, _example_inputs):
nonlocal compile_count
compile_count += 1
return graph_module.forward
compiled = torch.compile(
lambda query, key, value: impl.forward_cuda(query, key, value),
backend=counting_backend,
fullgraph=True,
dynamic=True,
)
out = compiled(q, q, q)
q2 = torch.randn(1, 24, 8, 64, device="cuda", dtype=torch.bfloat16)
if noncontiguous:
q2 = q2.transpose(1, 2).contiguous().transpose(1, 2)
out2 = compiled(q2, q2, q2)
assert out.is_contiguous()
assert out2.is_contiguous()
assert out.shape == q.shape
assert out2.shape == q2.shape
torch.testing.assert_close(out, impl.forward_cuda(q, q, q))
torch.testing.assert_close(out2, impl.forward_cuda(q2, q2, q2))
assert compile_count == 1
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_fa4_production_attention_entry_compiles_with_inductor(
monkeypatch,
fake_fa4,
):
"""Compile through Attention.forward with the default Inductor backend."""
from vllm_omni.diffusion.attention import layer as attention_layer
layer = _make_attention_layer(monkeypatch)
layer.attention = fake_fa4
q = torch.randn(1, 16, 8, 64, device="cuda", dtype=torch.bfloat16)
resolved_contexts = []
resolve_execution_path = fake_fa4.resolve_execution_path
def record_execution_path(context, query, key, value, attn_metadata):
resolved_contexts.append(context)
return resolve_execution_path(
context,
query,
key,
value,
attn_metadata,
)
monkeypatch.setattr(fake_fa4, "resolve_execution_path", record_execution_path)
context = ExecutionContext(
platform="cuda",
kernel_variant="fa4",
dtype="bfloat16",
paged_kv=True,
parallel_strategy=ParallelStrategy.ULYSSES,
require_fullgraph=True,
)
path = layer.resolve_execution_path(context, q, q, q, None)
assert path.support.status is SupportStatus.SUPPORTED
assert path.compilation_mode is CompilationMode.CUSTOM_OP
assert resolved_contexts[-1].paged_kv is False
assert resolved_contexts[-1].parallel_strategy is ParallelStrategy.NONE
compiled = torch.compile(layer, fullgraph=True, dynamic=True)
out = compiled(q, q, q)
assert out.shape == q.shape
torch.testing.assert_close(out, layer(q, q, q))
q2 = torch.randn(1, 24, 8, 64, device="cuda", dtype=torch.bfloat16)
torch.testing.assert_close(compiled(q2, q2, q2), layer(q2, q2, q2))
layer.paged_kv_cache_role = "self"
monkeypatch.setattr(layer, "_active_paged_kv_adapter", lambda: object())
paged_path = layer.resolve_execution_path(context, q, q, q, None)
assert resolved_contexts[-1].paged_kv is True
assert paged_path.support.status is SupportStatus.UNMIGRATED
layer.paged_kv_cache_role = None
layer.use_ring = True
layer.parallel_strategy = _ParallelStrategyStub(name="ulysses")
hybrid_path = layer.resolve_execution_path(context, q, q, q, None)
assert resolved_contexts[-1].parallel_strategy is ParallelStrategy.HYBRID_ULYSSES_RING
assert hybrid_path.support.status is SupportStatus.UNMIGRATED
layer.use_ring = False
layer.parallel_strategy = NoParallelAttention()
layer._hsdp_compile_boundary_enabled = True
hsdp_path = layer.resolve_execution_path(context, q, q, q, None)
assert OuterBoundary.HSDP in resolved_contexts[-1].outer_boundaries
assert hsdp_path.support.status is SupportStatus.UNMIGRATED
assert hsdp_path.compilation_mode is CompilationMode.EAGER_ONLY
layer._hsdp_compile_boundary_enabled = False
runtime_context = ForwardContext(
omni_diffusion_config=OmniDiffusionConfig(
parallel_config=DiffusionParallelConfig(use_hsdp=True, hsdp_shard_size=1),
),
)
monkeypatch.setattr(attention_layer, "is_forward_context_available", lambda: True)
monkeypatch.setattr(attention_layer, "get_forward_context", lambda: runtime_context)
layer.use_ring = True
layer.parallel_strategy = _ParallelStrategyStub(name="ring")
runtime_hsdp_path = layer.resolve_execution_path(context, q, q, q, None)
assert OuterBoundary.HSDP in resolved_contexts[-1].outer_boundaries
assert resolved_contexts[-1].parallel_strategy is ParallelStrategy.NONE
assert runtime_hsdp_path.support.status is SupportStatus.UNMIGRATED
@hardware_test(res={"cuda": "B200"}, num_cards=1)
@pytest.mark.parametrize("head_dims", [(32, 32), (64, 64), (80, 48), (192, 128), (256, 256)])
def test_real_fa4_fullgraph_matches_sdpa_across_shapes(monkeypatch, head_dims):
"""Validate real FA4 numerics through the complete compiled attention layer."""
capability = current_omni_platform.get_device_capability() if current_omni_platform.is_cuda() else None
if capability is None or capability.major < 10:
pytest.skip("Requires Blackwell")
if not fa.IS_FLASH_ATTN_4 or fa.flash_attn_func is None:
pytest.skip("Requires CuTe FlashAttention-4")
from torch.nn.attention import SDPBackend, sdpa_kernel
head_size, value_head_size = head_dims
layer = _make_attention_layer(monkeypatch, head_size=head_size)
compiled = torch.compile(layer, fullgraph=True, dynamic=True)
generator = torch.Generator(device="cuda").manual_seed(42)
context = ExecutionContext(platform="cuda", require_fullgraph=True)
# Tile boundaries, cross-attention lengths, batching, and a longer sequence.
for batch, q_length, kv_length in ((1, 16, 16), (1, 129, 257), (2, 257, 129), (1, 1024, 1024)):
query, key, value = (
torch.randn(batch, length, 8, dim, device="cuda", dtype=torch.bfloat16, generator=generator)
for length, dim in ((q_length, head_size), (kv_length, head_size), (kv_length, value_head_size))
)
path = layer.resolve_execution_path(context, query, key, value, None)
assert path.requested_support(context).status is SupportStatus.SUPPORTED
with sdpa_kernel(SDPBackend.MATH):
reference = torch.nn.functional.scaled_dot_product_attention(
query.transpose(1, 2).float(),
key.transpose(1, 2).float(),
value.transpose(1, 2).float(),
scale=head_size**-0.5,
).transpose(1, 2)
actual = compiled(query, key, value)
assert actual.dtype == query.dtype
assert actual.device == query.device
torch.testing.assert_close(actual.float(), reference, atol=1e-2, rtol=1e-2)
torch.testing.assert_close(actual, layer(query, key, value))
@hardware_test(res={"cuda": "B200"}, num_cards=1)
@pytest.mark.parametrize("head_dims", [(192, 192), (65, 64), (64, 65)])
def test_real_fa4_dimension_rejection_matches_kernel(monkeypatch, head_dims):
capability = current_omni_platform.get_device_capability() if current_omni_platform.is_cuda() else None
if capability is None or capability.major not in (10, 11) or not fa.IS_FLASH_ATTN_4:
pytest.skip("Requires SM100/SM110 CuTe FlashAttention-4")
head_size, value_head_size = head_dims
layer = _make_attention_layer(monkeypatch, head_size=head_size)
query = torch.randn(1, 16, 8, head_size, device="cuda", dtype=torch.bfloat16)
value = torch.randn(1, 16, 8, value_head_size, device="cuda", dtype=torch.bfloat16)
context = ExecutionContext(platform="cuda", require_fullgraph=True)
path = layer.resolve_execution_path(context, query, query, value, None)
assert path.requested_support(context).status is SupportStatus.UNSUPPORTED
with pytest.raises(AssertionError) as error:
layer(query, query, value)
assert str(error.value) in path.support.reason
@hardware_test(res={"cuda": "B200"}, num_cards=1)
@pytest.mark.parametrize("noncontiguous", [False, True])
def test_real_fa4_custom_op_unequal_value_dimension_schema(noncontiguous):
if not current_omni_platform.is_cuda() or not fa.IS_FLASH_ATTN_4:
pytest.skip("Requires CuTe FlashAttention-4")
query = torch.randn(1, 17, 8, 80, device="cuda", dtype=torch.bfloat16)
key = torch.randn(1, 25, 8, 80, device="cuda", dtype=torch.bfloat16)
value = torch.randn(1, 25, 8, 48, device="cuda", dtype=torch.bfloat16)
if noncontiguous:
query, key, value = (t.transpose(1, 2).contiguous().transpose(1, 2) for t in (query, key, value))
assert not query.is_contiguous()
torch.library.opcheck(
torch.ops.vllm_omni.fa4_dense_attention.default,
(query, key, value, 80**-0.5, False, False),
)
@@ -0,0 +1,200 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Compare full and pre-sharded Cosmos loading with real two-rank FSDP."""
from contextlib import ExitStack
from datetime import timedelta
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch
import pytest
import torch
import torch.distributed as dist
import torch.nn.functional as F
from torch import nn
from tests.helpers.mark import hardware_test
pytestmark = [pytest.mark.diffusion, pytest.mark.parallel, pytest.mark.core_model, pytest.mark.gpu]
class _SDPAAttention(nn.Module):
"""Use deterministic native attention while testing loading and FSDP."""
def __init__(self, *, causal, softmax_scale, **kwargs):
super().__init__()
self.causal = causal
self.scale = softmax_scale
def forward(self, query, key, value):
return F.scaled_dot_product_attention(
query.transpose(1, 2),
key.transpose(1, 2),
value.transpose(1, 2),
is_causal=self.causal,
scale=self.scale,
).transpose(1, 2)
def _load_cosmos_worker(rank, rendezvous, checkpoint_dir, edge):
from tests.model_executor.helpers import bootstrap_vllm_layer_custom_op_modules
bootstrap_vllm_layer_custom_op_modules()
from safetensors.torch import save_file
from vllm.config import DeviceConfig, VllmConfig, set_current_vllm_config
from vllm.config.load import LoadConfig
from vllm.model_executor import parameter
from vllm.model_executor.layers import linear
from vllm.utils.torch_utils import set_default_torch_dtype
from vllm_omni.diffusion.distributed import hsdp
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.cosmos3 import transformer_cosmos3, transformer_cosmos3_edge
from vllm_omni.platforms import current_omni_platform
device = torch.device("cuda", rank)
current_omni_platform.set_device(device)
dist.init_process_group("nccl", init_method=rendezvous, rank=rank, world_size=2, timeout=timedelta(seconds=120))
try:
with ExitStack() as stack:
stack.enter_context(set_current_vllm_config(VllmConfig(device_config=DeviceConfig(device="cuda"))))
stack.enter_context(set_default_torch_dtype(torch.bfloat16))
# HSDP spans both workers, while every vLLM linear uses TP=1.
for module in (linear, parameter):
stack.enter_context(patch.object(module, "get_tensor_model_parallel_rank", return_value=0))
stack.enter_context(patch.object(module, "get_tensor_model_parallel_world_size", return_value=1))
for module in (transformer_cosmos3, transformer_cosmos3_edge):
stack.enter_context(patch.object(module, "get_tensor_model_parallel_world_size", return_value=1))
stack.enter_context(patch.object(module, "FrameworkAttention", _SDPAAttention))
stack.enter_context(patch.object(transformer_cosmos3, "_get_ulysses_state", return_value=(1, 0, None)))
stack.enter_context(
patch.object(hsdp, "get_world_group", return_value=SimpleNamespace(world_size=2, rank_in_group=rank))
)
model_cls = (
transformer_cosmos3_edge.Cosmos3EdgeVFMTransformer
if edge
else transformer_cosmos3.Cosmos3VFMTransformer
)
tf_config = dict(
hidden_size=16,
num_hidden_layers=1,
num_attention_heads=2,
num_key_value_heads=2,
head_dim=8,
intermediate_size=32,
vocab_size=32,
latent_patch_size=2,
latent_channel=48,
temporal_compression_factor=4,
rope_scaling={"mrope_section": [2, 1, 1]},
)
if edge:
tf_config.update(
backbone_type=transformer_cosmos3_edge.COSMOS3_EDGE_BACKBONE_TYPE,
qk_norm_for_text=False,
use_und_k_norm_for_gen=True,
)
config = SimpleNamespace(
tf_model_config=tf_config,
dtype=torch.bfloat16,
quantization_config=None,
lora_path=None,
num_weight_load_threads=1,
hsdp_weight_load_strategy="full",
parallel_config=SimpleNamespace(
use_hsdp=True, hsdp_replicate_size=1, hsdp_shard_size=2, tensor_parallel_size=1
),
)
class Pipeline(nn.Module):
def __init__(self):
super().__init__()
self.transformer = model_cls(config)
self.transformer.register_buffer("scratch", torch.tensor([7.0]), persistent=False)
self.vae = nn.Linear(2, 2)
self.weights_sources = [
DiffusersPipelineLoader.ComponentSource(checkpoint_dir, None, None, "transformer.", False)
]
def load_weights(self, weights):
state = dict(weights)
self.transformer.load_state_dict(
{name.removeprefix("transformer."): t for name, t in state.items()}
)
self.transformer.post_load_weights()
self.transformer.eval()
self.transformer.validate_loaded_weights(set(state))
return set(state)
full = Pipeline()
if rank == 0:
torch.manual_seed(42)
with torch.no_grad():
for name, tensor in full.transformer.named_parameters():
tensor.copy_(torch.randn_like(tensor) * 0.02)
if "norm" in name:
tensor.fill_(1)
save_file(full.transformer.state_dict(), str(Path(checkpoint_dir) / "model.safetensors"))
dist.barrier()
def load(model):
loader = DiffusersPipelineLoader(LoadConfig(), config)
with patch.object(loader, "_init_from_load_format", return_value=model):
return loader._load_model_with_hsdp(device)
full = load(full)
config.hsdp_weight_load_strategy = "pre_sharded"
pre_sharded = load(Pipeline())
full_parameters = dict(full.transformer.named_parameters())
from torch.distributed.tensor import DTensor
for name, actual in pre_sharded.transformer.named_parameters():
expected = full_parameters[name]
if name.startswith("time_embedder."):
assert not isinstance(actual, DTensor)
assert actual.dtype == torch.float32
else:
assert isinstance(actual, DTensor)
actual = actual.full_tensor()
expected = expected.full_tensor()
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
assert pre_sharded.vae.weight.device == device
torch.testing.assert_close(pre_sharded.transformer.scratch, full.transformer.scratch)
assert all(t.device.type != "meta" for t in pre_sharded.transformer.buffers())
inputs = dict(
hidden_states=torch.linspace(-1, 1, 48 * 4, device=device).reshape(1, 48, 1, 2, 2),
timestep=torch.tensor([1.0], device=device),
text_ids=torch.tensor([[1, 2]], device=device),
text_mask=torch.ones(1, 2, dtype=torch.long, device=device),
video_shape=(1, 2, 2),
fps=24.0,
)
with torch.no_grad():
# Also exercise the cached UND path on the second denoising step.
for step in (1.0, 0.5):
inputs["timestep"].fill_(step)
expected = full.transformer(**inputs)
actual = pre_sharded.transformer(**inputs)
assert torch.isfinite(actual).all()
torch.testing.assert_close(actual, expected, rtol=0, atol=0)
finally:
dist.destroy_process_group()
@pytest.mark.parametrize("edge", [False, True], ids=["cosmos3", "cosmos3_edge"])
@hardware_test(res={"cuda": "L4"}, num_cards=2)
@pytest.mark.skipif(
not torch.cuda.is_available() or torch.accelerator.device_count() < 2 or not dist.is_nccl_available(),
reason="requires two CUDA GPUs and NCCL",
)
def test_cosmos3_pre_sharded_matches_full_hsdp(tmp_path, edge):
torch.multiprocessing.spawn(
_load_cosmos_worker,
args=(f"file://{tmp_path / 'rendezvous'}", str(tmp_path), edge),
nprocs=2,
join=True,
)
+92
View File
@@ -3,11 +3,13 @@
"""Unit tests for HSDP (Hybrid Sharded Data Parallel) configuration and utilities."""
import gc
from types import SimpleNamespace
import pytest
import torch
import torch.distributed as dist
import torch.nn as nn
from torch.distributed.fsdp import fully_shard
from torch.distributed.tensor import DeviceMesh, DTensor
from vllm.utils.network_utils import get_file_store_init_method
@@ -18,6 +20,7 @@ from vllm_omni.diffusion.distributed.hsdp import (
_unshardable_parameters,
shard_model,
)
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
pytestmark = [pytest.mark.diffusion, pytest.mark.parallel, pytest.mark.cpu, pytest.mark.core_model]
@@ -338,3 +341,92 @@ class TestHSDPShardConditions:
matched.append(name)
assert "blocks.0" in matched
assert "blocks.1" in matched
def test_pre_sharded_to_empty_materializes_fsdp_internal_storage(cpu_process_group):
with torch.device("meta"):
model = nn.Module()
model.transformer = nn.Sequential(nn.Linear(2, 2, bias=False))
model.transformer.register_buffer("scratch", torch.tensor([3.0]), persistent=False)
shard_model(
model.transformer,
mesh=DeviceMesh("cpu", [0]),
hsdp_shard_conditions=[lambda name, _module: name == "0"],
)
load_plan = SimpleNamespace(
roots=[SimpleNamespace(name="transformer", module=model.transformer)],
groups=[SimpleNamespace(module=model.transformer[0])],
binding_names=frozenset({"transformer.0.weight"}),
)
sharded_weight = model.transformer[0].weight
DiffusersPipelineLoader._materialize_pre_sharded_hsdp_state(model, load_plan, torch.device("cpu"))
fsdp_param = fully_shard.state(model.transformer[0])._fsdp_param_group.fsdp_params[0]
assert model.transformer[0].weight is sharded_weight
assert model.transformer[0].weight.to_local().device.type == "cpu"
assert fsdp_param._sharded_param_data.device.type == "cpu"
assert (
fsdp_param._sharded_param_data.untyped_storage().data_ptr()
== model.transformer[0].weight.to_local().untyped_storage().data_ptr()
)
assert torch.equal(model.transformer.scratch, torch.tensor([3.0]))
def test_pre_sharded_load_restores_checkpoint_values(cpu_process_group, tmp_path, monkeypatch):
from safetensors.torch import save_file
from vllm.config.load import LoadConfig
from vllm_omni.diffusion.distributed import hsdp as hsdp_module
from vllm_omni.diffusion.models.utils import release_module_parameters_to_meta
class Transformer(nn.Module):
_hsdp_shard_conditions = [lambda name, module: name == "block"]
_hsdp_ignored_modules = ["time_embedder"]
def __init__(self):
super().__init__()
self.block = nn.Linear(3, 4, bias=False)
self.time_embedder = nn.Linear(2, 2, bias=False, dtype=torch.bfloat16)
self.root_weight = nn.Parameter(torch.zeros(2))
self.register_buffer("scratch", torch.tensor([7.0]), persistent=False)
release_module_parameters_to_meta(self.block)
def post_load_weights(self):
self.time_embedder.to(torch.float32)
model = nn.Module()
model.transformer = Transformer()
model.vae = nn.Linear(2, 2)
tensors = {
"block.weight": torch.arange(12, dtype=torch.float32).reshape(4, 3),
"root_weight": torch.tensor([2.0, 3.0]),
"time_embedder.weight": torch.arange(4, dtype=torch.bfloat16).reshape(2, 2),
}
save_file(tensors, str(tmp_path / "model.safetensors"))
model.weights_sources = [DiffusersPipelineLoader.ComponentSource(str(tmp_path), None, None, "transformer.", False)]
config = SimpleNamespace(
dtype=torch.float32,
quantization_config=None,
lora_path=None,
hsdp_weight_load_strategy="pre_sharded",
num_weight_load_threads=1,
parallel_config=SimpleNamespace(use_hsdp=True, hsdp_replicate_size=1, hsdp_shard_size=1),
)
loader = DiffusersPipelineLoader(LoadConfig(), config)
monkeypatch.setattr(loader, "_init_from_load_format", lambda *args, **kwargs: model)
monkeypatch.setattr(hsdp_module, "get_world_group", lambda: SimpleNamespace(world_size=1, rank_in_group=0))
monkeypatch.setattr(hsdp_module, "current_omni_platform", SimpleNamespace(device_type="cpu"))
loaded = loader._load_model_with_hsdp(torch.device("cpu"))
assert loaded is model
for name, expected in tensors.items():
actual = dict(model.transformer.named_parameters())[name]
if name.startswith("time_embedder."):
assert not isinstance(actual, DTensor)
assert actual.dtype == torch.float32
torch.testing.assert_close(actual, expected.float())
else:
assert isinstance(actual, DTensor)
torch.testing.assert_close(actual.to_local(), expected)
torch.testing.assert_close(model.transformer.scratch, torch.tensor([7.0]))
@@ -27,6 +27,7 @@ from vllm_omni.diffusion.model_loader.host_weight_plan import (
HostWeightPlan,
HostWeightPlanResult,
TensorBinding,
build_checkpoint_binding_plan,
)
from vllm_omni.diffusion.model_loader.host_weights import source_identity as source_identity_module
from vllm_omni.diffusion.models.helios import HeliosPipeline
@@ -2089,3 +2090,206 @@ def test_hsdp_broadcast_weight_load_online_quant_multiprocess():
)
assert os.path.exists(os.path.join(temp_dir, "rank_0_success.flag"))
assert os.path.exists(os.path.join(temp_dir, "rank_1_success.flag"))
def test_pre_sharded_hsdp_rejects_quantization_before_loading():
od_config = SimpleNamespace(
dtype=torch.float32,
hsdp_weight_load_strategy="pre_sharded",
lora_path=None,
quantization_config=object(),
parallel_config=SimpleNamespace(
use_hsdp=True,
hsdp_replicate_size=1,
hsdp_shard_size=2,
),
)
loader = DiffusersPipelineLoader(LoadConfig(), od_config)
with pytest.raises(ValueError, match="does not support quantization"):
loader._load_model_with_hsdp(torch.device("cpu"))
def test_pre_sharded_hsdp_strategy_dispatches_without_using_full_loader(mocker):
import vllm_omni.diffusion.model_loader.diffusers_loader as loader_mod
from vllm_omni.diffusion.offloader.module_collector import PipelineModules
od_config = SimpleNamespace(
dtype=torch.float32,
hsdp_weight_load_strategy="pre_sharded",
lora_path=None,
quantization_config=None,
parallel_config=SimpleNamespace(
use_hsdp=True,
hsdp_replicate_size=1,
hsdp_shard_size=2,
),
)
loader = DiffusersPipelineLoader(LoadConfig(), od_config)
model = nn.Module()
model.transformer = nn.Linear(2, 2, bias=False)
discovered = PipelineModules(
dits=[model.transformer],
dit_names=["transformer"],
vaes=[],
encoders=[],
encoder_names=[],
resident_modules=[],
resident_names=[],
)
loader._init_from_load_format = mocker.Mock(return_value=model) # type: ignore[method-assign]
loader.load_weights = mocker.Mock(side_effect=AssertionError("full loader must not run")) # type: ignore[method-assign]
mocker.patch.object(loader_mod.ModuleDiscovery, "discover", return_value=discovered)
pre_sharded_load = mocker.patch.object(
loader,
"_load_model_with_pre_sharded_hsdp",
return_value=model,
)
assert loader._load_model_with_hsdp(torch.device("cpu")) is model
pre_sharded_load.assert_called_once()
def test_pre_sharded_hsdp_rejects_tensor_transform():
def transform(tensor):
return tensor.transpose(-2, -1)
load_plan = SimpleNamespace(
bindings={
"transformer.weight": TensorBinding(
checkpoint_key="weight",
file_path="model.safetensors",
transform=transform,
)
}
)
with pytest.raises(ValueError, match="tensor transforms are unsupported"):
DiffusersPipelineLoader._validate_pre_sharded_hsdp_bindings(load_plan)
def test_pre_sharded_hsdp_accepts_direct_safetensors_binding():
load_plan = SimpleNamespace(
bindings={
"transformer.weight": TensorBinding(
checkpoint_key="weight",
file_path="model.safetensors",
)
}
)
DiffusersPipelineLoader._validate_pre_sharded_hsdp_bindings(load_plan)
def test_pre_sharded_hsdp_uses_fsdp_destination_directly():
target = torch.empty(8, 4, 3)
load_plan = SimpleNamespace(
bindings={
"transformer.weight": TensorBinding(
checkpoint_key="source_weight",
file_path="transformer/model.safetensors",
)
}
)
checkpoint_states, local_bytes = DiffusersPipelineLoader._prepare_pre_sharded_checkpoint_states(
load_plan,
{"transformer.weight": target},
)
checkpoint_target = checkpoint_states[Path("transformer")]["source_weight"]
assert checkpoint_target is target
assert checkpoint_target.shape == (8, 4, 3)
assert checkpoint_target.device.type == "cpu"
assert checkpoint_target.untyped_storage().data_ptr() == target.untyped_storage().data_ptr()
assert local_bytes == target.numel() * target.element_size()
def test_pre_sharded_hsdp_checkpoint_writes_runtime_storage_directly():
runtime_target = torch.empty(2, 3)
load_plan = SimpleNamespace(
bindings={
"transformer.weight": TensorBinding(
checkpoint_key="source_weight",
file_path="transformer/model.safetensors",
)
}
)
checkpoint_tensor = torch.arange(6, dtype=torch.float32).reshape(2, 3)
checkpoint_states, _ = DiffusersPipelineLoader._prepare_pre_sharded_checkpoint_states(
load_plan,
{"transformer.weight": runtime_target},
)
checkpoint_states[Path("transformer")]["source_weight"].copy_(checkpoint_tensor)
assert torch.equal(runtime_target, checkpoint_tensor)
def test_pre_sharded_hsdp_rejects_duplicate_checkpoint_destinations():
binding = TensorBinding(checkpoint_key="weight", file_path="transformer/model.safetensors")
load_plan = SimpleNamespace(
bindings={
"transformer.first": binding,
"transformer.second": binding,
}
)
with pytest.raises(ValueError, match="multiple runtime tensors"):
DiffusersPipelineLoader._prepare_pre_sharded_checkpoint_states(
load_plan,
{"transformer.first": torch.empty(2, 3), "transformer.second": torch.empty(2, 3)},
)
def test_pre_sharded_hsdp_managed_module_traversal_is_child_first_and_skips_nested_roots():
root = nn.Module()
root.left = nn.Sequential(nn.Linear(2, 2), nn.ReLU())
root.right = nn.Sequential(nn.Linear(2, 2), nn.ReLU())
names = {module: name or "root" for name, module in root.named_modules()}
modules = DiffusersPipelineLoader._hsdp_managed_modules_post_order(
root,
nested_hsdp_roots={root.left},
)
assert [names[module] for module in modules] == ["right.0", "right.1", "right", "root"]
@pytest.mark.parametrize("checkpoint_key", ["renamed", "unexpected"])
def test_hsdp_checkpoint_plan_honors_remap_and_rejects_missing(tmp_path, checkpoint_key, monkeypatch):
import vllm_omni.diffusion.model_loader.host_weight_plan as plan_mod
monkeypatch.setattr(plan_mod, "get_direct_mmap_adapter", lambda _model: None)
class Pipeline(nn.Module):
def __init__(self):
super().__init__()
self.transformer = nn.Linear(2, 2, bias=False)
@staticmethod
def remap_checkpoint_key(name):
return "transformer.weight" if name == "transformer.renamed" else name
checkpoint = tmp_path / "model.safetensors"
save_file({checkpoint_key: torch.ones(2, 2)}, str(checkpoint))
source = SimpleNamespace(
model_or_path=str(tmp_path),
subfolder=None,
revision=None,
prefix="transformer.",
)
model = Pipeline()
result = build_checkpoint_binding_plan(
model,
dit_modules=(("transformer", model.transformer),),
sources=(source,),
model_path=None,
tensor_parallel_size=1,
online_quantization=False,
)
if checkpoint_key == "renamed":
assert result.plan is not None
assert result.plan.bindings["transformer.weight"].checkpoint_key == "renamed"
else:
assert result.plan is None
assert "no checkpoint binding" in result.fallback_reason
@@ -61,7 +61,10 @@ def test_single_request_graph_wrapper_cpu_falls_back_to_eager(cfg_strength: floa
# The conditioning is prepared once per request and reused by every step.
assert prepare_spy.call_count == 1
assert prepare_spy.call_args.args[-1] is (cfg_strength >= 1e-5)
# _prepare(x, x_mask, text, c_mask, ref, ref_mask, uses_cfg, timesteps)
assert prepare_spy.call_args.args[6] is (cfg_strength >= 1e-5)
# The time grid reaches prepare, so the adaLN modulations are computed once.
assert prepare_spy.call_args.args[7].numel() == 2
assert step_spy.call_count == 2
capture_spy.assert_not_called()
assert not wrapper._cache
@@ -84,7 +87,8 @@ def test_graph_inputs_use_bounded_length_buckets() -> None:
assert padded[4].shape == (1, 100, 4)
assert padded[5].shape == (1, 100)
assert [mask.sum().item() for mask in (padded[1], padded[3], padded[5])] == [65, 65, 51]
assert wrapper._key(padded[0], padded[2], padded[4], False) == (96, 96, 100, False)
assert wrapper._key(padded[0], padded[2], padded[4], False) == (96, 96, 100, False, 0)
assert wrapper._key(padded[0], padded[2], padded[4], False, 32) == (96, 96, 100, False, 32)
assert wrapper.max_graphs == 32
@@ -153,18 +157,21 @@ def test_single_request_graph_replay_matches_eager_and_updates_inputs(cfg_streng
inputs["ref"],
inputs["ref_mask"],
)
key = wrapper._key(bucketed[0], bucketed[2], bucketed[4], cfg_strength >= 1e-5)
# sample_latents passes the grid, so the key carries its step count (two steps here).
key = wrapper._key(bucketed[0], bucketed[2], bucketed[4], cfg_strength >= 1e-5, 2)
assert key in wrapper._cache
# The static context was refreshed for this request's conditioning.
entry = wrapper._cache[key]
fresh = wrapper._prepare(*bucketed, cfg_strength >= 1e-5)
grid = torch.tensor([0.0, 0.4], device="cuda")
fresh = wrapper._prepare(*bucketed, cfg_strength >= 1e-5, grid)
for static, want in zip(entry.static_ctx.tensors(), fresh.tensors(), strict=True):
if want is None:
assert static is None
else:
torch.testing.assert_close(static, want)
torch.testing.assert_close(entry.static_timestep, torch.tensor(0.4, device="cuda"))
assert int(entry.static_step) == 1
assert len(wrapper._cache) == 1
@@ -0,0 +1,139 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""The AuK DiT fast paths keep the reference numerics.
Two paths are checked: the grouped conv position embedding as one batched
GEMM, and the adaLN modulations precomputed for a whole time grid, eager and
under the per-step CUDA graph.
"""
import pytest
import torch
from vllm_omni.diffusion.models.auk import auk_transformer as T
from vllm_omni.diffusion.models.auk.auk_transformer import AuKTransformer, build_time_grid
from vllm_omni.diffusion.models.auk.cudagraph_wrapper import AuKCUDAGraphWrapper
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _make_dit(device: str = "cpu") -> AuKTransformer:
torch.manual_seed(5)
return (
AuKTransformer(
dim=32,
heads=2,
dim_head=16,
ff_mult=2,
latent_dim=4,
text_hidden_dim=8,
num_layers=2,
num_single_layers=2,
)
.eval()
.to(device)
)
def _reference_conv_pos_embed(module: T.ConvPosEmbedding, x: torch.Tensor, mask: torch.Tensor | None) -> torch.Tensor:
"""The channels-first conv stack as the reference implementation writes it."""
keep = None if mask is None else mask.unsqueeze(1)
x = x.transpose(1, 2)
if keep is not None:
x = x.masked_fill(~keep, 0.0)
for layer in module.conv1d:
x = layer(x)
if keep is not None and isinstance(layer, torch.nn.Conv1d):
x = x.masked_fill(~keep, 0.0)
return x.transpose(1, 2)
@torch.inference_mode()
def test_grouped_conv_as_batched_gemm_matches_conv1d() -> None:
torch.manual_seed(0)
conv = torch.nn.Conv1d(64, 64, 31, groups=16, padding=15)
x = torch.randn(2, 37, 64)
expected = conv(x.transpose(1, 2)).transpose(1, 2)
torch.testing.assert_close(T._grouped_conv1d_btc(x, conv), expected, atol=1e-5, rtol=1e-5)
@torch.inference_mode()
@pytest.mark.parametrize("masked", [False, True])
def test_conv_pos_embedding_matches_the_channels_first_stack(masked: bool) -> None:
torch.manual_seed(1)
module = T.ConvPosEmbedding(64).eval()
x = torch.randn(2, 37, 64)
mask = None
if masked:
mask = torch.ones(2, 37, dtype=torch.bool)
mask[1, 30:] = False
torch.testing.assert_close(module(x, mask), _reference_conv_pos_embed(module, x, mask), atol=1e-5, rtol=1e-5)
@torch.inference_mode()
@pytest.mark.parametrize("cfg_infer", [False, True])
def test_modulation_table_matches_per_step_modulation(cfg_infer: bool) -> None:
dit = _make_dit()
text = torch.randn(1, 5, 8)
ref = torch.randn(1, 3, 4)
x = torch.randn(1, 6, 4)
grid = build_time_grid(nfe=4, sway_sampling_coef=-1.0, t_grid=None, device="cpu")
plain = dit.prepare(text, target_len=6, ref=ref, cfg_infer=cfg_infer)
tabled = dit.prepare(text, target_len=6, ref=ref, cfg_infer=cfg_infer, timesteps=grid[:-1])
assert tabled.modulation is not None and tabled.modulation.shape[0] == 4
for i in range(4):
expected = dit.step(x, grid[i], plain)
torch.testing.assert_close(dit.step(x, grid[i], tabled, step_index=i), expected, atol=1e-5, rtol=1e-5)
index = torch.tensor(i)
torch.testing.assert_close(dit.step(x, grid[i], tabled, step_index=index), expected, atol=1e-5, rtol=1e-5)
def _padded_inputs(device: str) -> dict[str, torch.Tensor]:
generator = torch.Generator(device="cpu").manual_seed(3)
text = torch.randn(1, 7, 8, generator=generator)
c_mask = torch.ones(1, 7, dtype=torch.bool)
c_mask[:, 5:] = False
text[:, 5:] = 0.0
ref = torch.randn(1, 6, 4, generator=generator)
ref_mask = torch.ones(1, 6, dtype=torch.bool)
ref_mask[:, 4:] = False
mask = torch.ones(1, 9, dtype=torch.bool)
mask[:, 7:] = False
inputs = {"text": text, "c_mask": c_mask, "ref": ref, "ref_mask": ref_mask, "mask": mask}
return {name: value.to(device) for name, value in inputs.items()}
@pytest.mark.cuda
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA graph replay requires CUDA")
@torch.inference_mode()
@pytest.mark.parametrize("cfg_strength", [0.0, 2.0])
def test_graph_with_time_grid_matches_eager_with_time_grid(cfg_strength: float) -> None:
dit = _make_dit("cuda")
inputs = _padded_inputs("cuda")
grid = build_time_grid(nfe=4, sway_sampling_coef=-1.0, t_grid=None, device="cuda")
x = torch.randn(1, 9, 4, device="cuda")
wrapper = AuKCUDAGraphWrapper(dit)
common = dict(
text=inputs["text"],
c_mask=inputs["c_mask"],
ref=inputs["ref"],
ref_mask=inputs["ref_mask"],
cfg_strength=cfg_strength,
timesteps=grid[:-1],
)
for i in range(4):
graph = wrapper(x=x, timestep=grid[i], new_request=i == 0, step_index=i, **common)
ctx = dit.prepare(
inputs["text"],
target_len=9,
c_mask=inputs["c_mask"],
ref=inputs["ref"],
ref_mask=inputs["ref_mask"],
cfg_infer=cfg_strength >= 1e-5,
timesteps=grid[:-1],
)
eager = wrapper._step(x, grid[i], ctx, cfg_strength, i)
torch.testing.assert_close(graph, eager, atol=1e-4, rtol=1e-4)
# One graph serves every step of the grid.
assert len(wrapper._cache) == 1
assert next(iter(wrapper._cache))[-1] == 4
@@ -127,7 +127,9 @@ def test_hoisted_euler_loop_matches_per_step_forward(cfg_strength: float) -> Non
v = dit(x, time=timesteps[i], **inputs)
x = x + (timesteps[i + 1] - timesteps[i]) * v
torch.testing.assert_close(got, x, rtol=0, atol=0)
# sample_latents precomputes the adaLN modulations of the whole grid in one
# GEMM per layer, whose rows round differently from one-row GEMMs in fp32.
torch.testing.assert_close(got, x, rtol=0, atol=1e-6)
assert dit.text_cond is None and dit.text_uncond is None
@@ -11,10 +11,29 @@ eager off CUDA) and the tiled decode of long clips.
import pytest
import torch
from vllm_omni.diffusion.models.auk.auk_vae import AuKVAE, LowPass, SnakeBeta, Upsample
from tests.helpers.mark import hardware_test
from vllm_omni.diffusion.models.auk import auk_vae
from vllm_omni.diffusion.models.auk.auk_vae import AliasFreeActivation, AuKVAE, LowPass, SnakeBeta, Upsample
from vllm_omni.diffusion.models.auk.vae_cudagraph import AuKVAEDecodeGraph, plan_tiles
from vllm_omni.platforms import current_omni_platform
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
pytestmark = [pytest.mark.core_model]
@pytest.fixture(autouse=True)
def _bound_cpu_decode_threads(request):
if request.node.get_closest_marker("cpu") is None:
yield
return
# These narrow convolutions run in shared CI pods. A host-sized Torch
# pool spends more time coordinating workers than decoding small clips.
previous_threads = torch.get_num_threads()
torch.set_num_threads(1)
print(f"AuK CPU decode threads: {previous_threads} -> {torch.get_num_threads()}")
try:
yield
finally:
torch.set_num_threads(previous_threads)
def _small_vae() -> AuKVAE:
@@ -45,6 +64,23 @@ def _reference_snake(module: SnakeBeta, x: torch.Tensor) -> torch.Tensor:
return x + (1.0 / (beta + 1e-9)) * torch.sin(x * alpha).pow(2)
def _assert_plain_graph_matches_eager(replay: torch.Tensor, eager: torch.Tensor) -> None:
assert replay.shape == eager.shape and replay.dtype == eager.dtype
assert torch.isfinite(replay).all() and torch.isfinite(eager).all()
error = (replay - eager).abs()
rms = error.square().mean().sqrt().item()
print(f"AuK plain graph: max_abs={error.max().item():.9g}, rms={rms:.9g}")
if current_omni_platform.is_rocm():
# MI300 build 13040 measured max_abs <= 1.64e-7 and RMS <= 4.13e-8.
# Bound both peak and aggregate FP32 error; near-zero samples must
# not hide behind a relative tolerance. NVIDIA retains bit parity.
torch.testing.assert_close(replay, eager, atol=1e-6, rtol=0.0)
assert rms <= 1e-7, rms
else:
assert torch.equal(replay, eager)
@pytest.mark.cpu
@torch.inference_mode()
def test_window_padding_decodes_to_raw_zero() -> None:
"""Bucket padding is the normalized latent whose denormalized value is zero."""
@@ -57,6 +93,7 @@ def test_window_padding_decodes_to_raw_zero() -> None:
torch.testing.assert_close(raw, torch.zeros_like(raw), atol=1e-6, rtol=0.0)
@pytest.mark.cpu
@torch.inference_mode()
def test_eager_snake_matches_the_reference_formula() -> None:
module = SnakeBeta(6, alpha_logscale=True)
@@ -69,6 +106,7 @@ def test_eager_snake_matches_the_reference_formula() -> None:
assert torch.equal(module(x), _reference_snake(module, x))
@pytest.mark.cpu
@torch.inference_mode()
def test_filter_cache_leaves_the_filters_and_output_unchanged() -> None:
lowpass = LowPass(cutoff=0.25, half_width=0.3, stride=2, kernel_size=12, causal=True)
@@ -88,6 +126,7 @@ def test_filter_cache_leaves_the_filters_and_output_unchanged() -> None:
assert module._expanded.shape == (3, 1, 12)
@pytest.mark.cpu
@torch.inference_mode()
def test_decode_fast_paths_reproduce_the_plain_decode() -> None:
vae = _small_vae()
@@ -103,6 +142,7 @@ def test_decode_fast_paths_reproduce_the_plain_decode() -> None:
assert all(module._cached for module in vae.modules() if isinstance(module, SnakeBeta))
@pytest.mark.cpu
@torch.inference_mode()
def test_graph_wrapper_falls_back_to_eager_off_cuda(mocker) -> None:
vae = _small_vae()
@@ -119,6 +159,7 @@ def test_graph_wrapper_falls_back_to_eager_off_cuda(mocker) -> None:
assert not wrapper._cache and not wrapper._compiled
@pytest.mark.cpu
def test_compiled_bucket_is_the_smallest_captured_one_that_fits() -> None:
wrapper = AuKVAEDecodeGraph(_small_vae(), compile_shapes=(16, 8, 8))
assert wrapper.compile_shapes == [8, 16]
@@ -131,6 +172,7 @@ def test_compiled_bucket_is_the_smallest_captured_one_that_fits() -> None:
assert wrapper.compiled_bucket(6) == 16
@pytest.mark.cpu
def test_plan_tiles_covers_the_clip_once_with_full_context() -> None:
assert plan_tiles(40, 64, 10, 3) == [(0, 40, 0, 40)]
windows = plan_tiles(1000, 512, 53, 9)
@@ -149,10 +191,16 @@ def test_plan_tiles_covers_the_clip_once_with_full_context() -> None:
# 512 and a 256 window instead of two 512s.
assert plan_tiles(600, 512, 53, 9, sizes=(128, 256, 512)) == [(0, 512, 0, 503), (344, 256, 503, 600)]
assert plan_tiles(1000, 512, 53, 9, sizes=(128, 256, 512))[-1] == (872, 128, 953, 1000)
narrow = plan_tiles(200, 64, 53, 9)
assert len(narrow) == 69
assert [emit for _, _, emit, _ in narrow[:3]] == [0, 55, 57]
assert all(a[3] == b[2] for a, b in zip(narrow, narrow[1:]))
assert narrow[-1][3] == 200
with pytest.raises(ValueError, match="must exceed"):
plan_tiles(100, 60, 53, 9)
@pytest.mark.cpu
@torch.inference_mode()
def test_decode_context_bounds_the_measured_receptive_field() -> None:
vae = _small_vae()
@@ -170,25 +218,26 @@ def test_decode_context_bounds_the_measured_receptive_field() -> None:
assert differing and all(index < left or end - start - index <= right for index in differing)
@pytest.mark.cpu
@torch.inference_mode()
def test_tiled_decode_matches_the_whole_decode() -> None:
vae = _small_vae()
# Off the accelerator every window decodes eagerly, so this isolates the stitching.
wrapper = AuKVAEDecodeGraph(vae, compile_shapes=(64,), tile_frames=64)
assert wrapper.tile_frames == 64 and wrapper.context_frames == (53, 9)
for frames in (64, 65, 200, 331):
wrapper = AuKVAEDecodeGraph(vae, compile_shapes=(128,), tile_frames=128)
assert wrapper.tile_frames == 128 and wrapper.context_frames == (53, 9)
for frames in (128, 129, 200, 331):
latents = torch.randn(1, frames, vae.latent_dim)
whole = vae.decode(latents)
tiled = wrapper(latents)
assert tiled.shape == whole.shape
torch.testing.assert_close(tiled, whole, atol=1e-5, rtol=0.0)
assert wrapper.last_mode == ("eager" if frames == 64 else "tiled")
assert wrapper.last_mode == ("eager" if frames == 128 else "tiled")
# The streaming form yields the same audio in order.
latents = torch.randn(1, 200, vae.latent_dim)
pieces = [(start, chunk.clone()) for start, chunk in wrapper.decode_tiles(latents)]
# With a 64-frame tile and a 62-frame halo each interior tile adds two frames.
assert [start for start, _ in pieces] == [emit for _, _, emit, _ in plan_tiles(200, 64, 53, 9)]
assert [start for start, _ in pieces][:3] == [0, 55, 57]
# A 128-frame tile avoids repeatedly decoding the same 62-frame halo.
assert [start for start, _ in pieces] == [emit for _, _, emit, _ in plan_tiles(200, 128, 53, 9)]
assert [start for start, _ in pieces][:3] == [0, 119, 185]
torch.testing.assert_close(
torch.cat([chunk for _, chunk in pieces], dim=1), vae.decode(latents), atol=1e-5, rtol=0.0
)
@@ -198,20 +247,90 @@ def test_tiled_decode_matches_the_whole_decode() -> None:
AuKVAEDecodeGraph(vae, tile_frames=60)
@pytest.mark.cuda
@pytest.mark.cpu
@torch.inference_mode()
def test_fused_activation_is_skipped_off_cuda() -> None:
activation = AliasFreeActivation(SnakeBeta(4, alpha_logscale=True))
x = torch.randn(1, 4, 16)
assert not activation._fused_eligible(x)
assert activation(x).shape == (1, 4, 16)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.skipif(not torch.cuda.is_available(), reason="The fused activation kernels require CUDA")
@torch.inference_mode()
def test_fused_activation_is_skipped_without_triton(monkeypatch: pytest.MonkeyPatch) -> None:
activation = AliasFreeActivation(SnakeBeta(4, alpha_logscale=True)).to("cuda")
x = torch.randn(1, 4, 16, device="cuda")
assert activation._fused_eligible(x)
monkeypatch.setattr(auk_vae, "HAS_TRITON", False)
assert not activation._fused_eligible(x)
assert activation(x).shape == (1, 4, 16)
def _activation_input(layout: str, length: int) -> torch.Tensor:
"""A ``[2, 6, length]`` input, contiguous or as a strided view of a larger buffer."""
if layout == "contiguous":
x = torch.randn(2, 6, length, device="cuda")
elif layout == "transposed":
x = torch.randn(2, length, 6, device="cuda").transpose(1, 2)
elif layout == "sliced":
x = torch.randn(2, 8, 2 * length, device="cuda")[:, 1:7, ::2]
else:
raise ValueError(layout)
assert x.shape == (2, 6, length)
if length > 1: # contiguity of a single-sample view depends on the layout
assert x.is_contiguous() == (layout == "contiguous")
return x * 3
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@pytest.mark.skipif(not torch.cuda.is_available(), reason="The fused activation kernels require CUDA")
@pytest.mark.parametrize("layout", ["contiguous", "transposed", "sliced"])
@pytest.mark.parametrize("causal", [False, True])
# The kernels tile the output in blocks of 1024 samples: the 2x oversampled
# signal crosses a block boundary at 512 input samples and the decimated one at
# 1024, so both sides of each boundary are covered, plus short and odd lengths.
@pytest.mark.parametrize("length", [1, 7, 511, 512, 513, 1000, 1023, 1024, 1025])
@torch.inference_mode()
def test_fused_activation_matches_the_eager_modules(layout: str, causal: bool, length: int) -> None:
torch.manual_seed(0)
activation = AliasFreeActivation(SnakeBeta(6, alpha_logscale=True), causal=causal).to("cuda")
with torch.no_grad():
activation.act.alpha.normal_(0.0, 0.3)
activation.act.beta.normal_(0.0, 0.3)
x = _activation_input(layout, length)
original = x.clone()
eager = activation.downsample(activation.act(activation.upsample(x)))
assert activation._fused_eligible(x)
fused = activation(x)
# Same fp32 arithmetic, only the order in which the taps are summed differs.
assert fused.shape == eager.shape
torch.testing.assert_close(fused, eager, atol=1e-5, rtol=1e-5)
# Neither path writes into its (possibly strided) input.
torch.testing.assert_close(x, original, atol=0, rtol=0)
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA graph replay requires CUDA")
@torch.inference_mode()
def test_graph_replay_matches_eager_per_length() -> None:
vae = _small_vae().to("cuda")
wrapper = AuKVAEDecodeGraph(vae, max_graphs=2)
pools = []
saved_outputs: list[tuple[torch.Tensor, torch.Tensor]] = []
for frames in (6, 9, 6, 12, 6):
latents = torch.randn(1, frames, vae.latent_dim, device="cuda")
eager = vae.decode(latents)
replay = wrapper(latents)
# Exact-length graphs replay the very same kernels: bit-identical,
# including after the generation was retired.
assert torch.equal(replay, eager), frames
_assert_plain_graph_matches_eager(replay, eager)
# Returned waveforms own their storage and survive later replays
# and retirement of the generation that produced them.
for previous, snapshot in saved_outputs:
assert torch.equal(previous, snapshot)
saved_outputs.append((replay, replay.clone()))
pools.append(wrapper._plain_pool)
# The third distinct length found the cache full, so the whole generation
# (6 and 9) was retired together with its pool rather than one graph at a
@@ -220,7 +339,7 @@ def test_graph_replay_matches_eager_per_length() -> None:
assert pools[0] is pools[2] and pools[3] is pools[4] and pools[2] is not pools[3]
@pytest.mark.cuda
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA graph replay requires CUDA")
@torch.inference_mode()
def test_bucketed_graph_only_disturbs_the_tail() -> None:
@@ -240,7 +359,7 @@ def test_bucketed_graph_only_disturbs_the_tail() -> None:
torch.testing.assert_close(replay, eager, atol=0.1, rtol=0.0)
@pytest.mark.cuda
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@pytest.mark.skipif(not torch.cuda.is_available(), reason="torch.compile + CUDA graph capture requires CUDA")
@torch.inference_mode()
def test_compiled_buckets_replay_within_fusion_tolerance_and_leave_longer_clips_to_plain_graphs() -> None:
@@ -266,11 +385,12 @@ def test_compiled_buckets_replay_within_fusion_tolerance_and_leave_longer_clips_
# a longer clip gets its own plain graph.
assert wrapper.tile_frames == 0
long = torch.randn(1, 12, vae.latent_dim, device="cuda")
assert torch.equal(wrapper(long), vae.decode(long))
replay, eager = wrapper(long), vae.decode(long)
_assert_plain_graph_matches_eager(replay, eager)
assert wrapper.last_mode == "graph" and list(wrapper._cache) == [12]
@pytest.mark.cuda
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@pytest.mark.skipif(not torch.cuda.is_available(), reason="torch.compile + CUDA graph capture requires CUDA")
@torch.inference_mode()
def test_tiles_replay_the_compiled_bucket_for_long_clips() -> None:
@@ -291,6 +291,18 @@ class TestRequestParsing:
with pytest.raises(ValueError, match="AuK max_dit_graphs must be a positive integer"):
build_pipeline(model_config={"max_dit_graphs": max_dit_graphs})
@pytest.mark.parametrize(
"model_config, expected", [({}, 16), ({"auk_ref_cache_size": 0}, 0), ({"auk_ref_cache_size": 4}, 4)]
)
def test_ref_cache_size_is_taken_from_model_config(self, build_pipeline, model_config, expected):
pipeline, _ = build_pipeline(model_config=model_config)
assert pipeline._ref_cache_size == expected
@pytest.mark.parametrize("size", [-1, 1.9, True, False, "4", None])
def test_invalid_ref_cache_size_is_rejected(self, build_pipeline, size):
with pytest.raises(ValueError, match="AuK auk_ref_cache_size must be a non-negative integer"):
build_pipeline(model_config={"auk_ref_cache_size": size})
def test_pipeline_declares_audio_output(self, build_pipeline):
pipeline, _ = build_pipeline()
@@ -492,6 +504,40 @@ class TestRequestParsing:
assert call["sample"] is True
assert isinstance(call["generator"], torch.Generator)
def test_reference_latents_are_cached_by_content(self, build_pipeline):
pipeline, calls = build_pipeline()
clip = _silence(1.0)
for _ in range(3):
pipeline.forward(_batch(_prompt(audio=clip, knobs={"gen_seconds": 1.0}), seed=1))
# A different clip is a new entry, the first one still hits.
other = (np.full_like(clip[0], 0.25), clip[1])
pipeline.forward(_batch(_prompt(audio=other, knobs={"gen_seconds": 1.0}), seed=1))
pipeline.forward(_batch(_prompt(audio=clip, knobs={"gen_seconds": 1.0}), seed=1))
assert len(pipeline.vae.encode_calls) == 2
assert calls[0]["ref"] is calls[1]["ref"] is calls[4]["ref"]
def test_posterior_draws_and_disabled_cache_always_encode(self, build_pipeline):
pipeline, _ = build_pipeline()
for _ in range(2):
pipeline.forward(
_batch(_prompt(audio=_silence(1.0), knobs={"gen_seconds": 1.0, "vae_sample": True}), seed=1)
)
assert len(pipeline.vae.encode_calls) == 2
pipeline, _ = build_pipeline(model_config={"auk_ref_cache_size": 0})
for _ in range(2):
pipeline.forward(_batch(_prompt(audio=_silence(1.0), knobs={"gen_seconds": 1.0}), seed=1))
assert len(pipeline.vae.encode_calls) == 2
def test_reference_cache_is_bounded(self, build_pipeline):
pipeline, _ = build_pipeline(model_config={"auk_ref_cache_size": 2})
for level in (0.1, 0.2, 0.3):
clip = (np.full(SAMPLE_RATE, level, dtype=np.float32), SAMPLE_RATE)
pipeline.forward(_batch(_prompt(audio=clip, knobs={"gen_seconds": 1.0}), seed=1))
assert len(pipeline._ref_cache) == 2
def test_one_request_per_forward(self, build_pipeline):
pipeline, _ = build_pipeline()
batch = _batch(_prompt(knobs={"gen_seconds": 1.0}), seed=1)
@@ -868,6 +868,93 @@ def test_qwen_gen_mlp_remains_gated() -> None:
assert hasattr(model.gen_layers[0].mlp, "gate_proj")
@pytest.mark.parametrize("edge", [False, True])
@pytest.mark.parametrize(
"use_hsdp,strategy", [(False, "full"), (False, "pre_sharded"), (True, "full"), (True, "pre_sharded")]
)
def test_cosmos3_releases_blocks_only_for_pre_sharded_hsdp(edge, use_hsdp, strategy, monkeypatch):
from vllm_omni.diffusion.models.cosmos3 import transformer_cosmos3, transformer_cosmos3_edge
from vllm_omni.diffusion.models.utils import release_module_parameters_to_meta
released = []
def release_and_check(block):
parameters = dict(block.named_parameters())
metadata = {name: dict(parameter.__dict__) for name, parameter in parameters.items()}
release_module_parameters_to_meta(block)
for name, parameter in block.named_parameters():
assert parameter is parameters[name]
assert parameter.device.type == "meta"
assert parameter.__dict__ == metadata[name]
released.append(block)
monkeypatch.setattr(transformer_cosmos3, "release_module_parameters_to_meta", release_and_check)
monkeypatch.setattr(transformer_cosmos3_edge, "release_module_parameters_to_meta", release_and_check)
model_cls = (
transformer_cosmos3_edge.Cosmos3EdgeVFMTransformer if edge else transformer_cosmos3.Cosmos3VFMTransformer
)
config = _tiny_cosmos3_edge_config if edge else _tiny_cosmos3_config
model = model_cls(
SimpleNamespace(
tf_model_config=config(num_hidden_layers=1),
dtype=torch.float32,
parallel_config=SimpleNamespace(use_hsdp=use_hsdp),
hsdp_weight_load_strategy=strategy,
)
)
blocks = [*model.language_model.layers, *model.gen_layers]
expected_meta = use_hsdp and strategy == "pre_sharded"
assert len(released) == (len(blocks) if expected_meta else 0)
assert all((p.device.type == "meta") == expected_meta for block in blocks for p in block.parameters())
assert model.time_embedder.linear_1.weight.device.type != "meta"
assert all(buffer.device.type != "meta" for buffer in model.buffers())
@pytest.mark.parametrize("edge", [False, True])
def test_cosmos3_pre_sharded_meta_processing_accepts_real_unquantized_blocks(edge, monkeypatch):
from vllm.model_executor.layers.linear import UnquantizedLinearMethod
from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
from vllm_omni.diffusion.models.cosmos3 import Cosmos3EdgeVFMTransformer, Cosmos3VFMTransformer
model_cls = Cosmos3EdgeVFMTransformer if edge else Cosmos3VFMTransformer
config = _tiny_cosmos3_edge_config if edge else _tiny_cosmos3_config
model = model_cls(
SimpleNamespace(
tf_model_config=config(num_hidden_layers=1),
dtype=torch.float32,
parallel_config=SimpleNamespace(use_hsdp=True),
hsdp_weight_load_strategy="pre_sharded",
)
)
groups = [SimpleNamespace(module=block) for block in (*model.language_model.layers, *model.gen_layers)]
plan = SimpleNamespace(roots=[SimpleNamespace(module=model)], groups=groups)
processed = []
# Exercise model selection with real vLLM linears, without invoking the
# CPU-specific weight packing hook on meta tensors in CPU-only CI.
monkeypatch.setattr(
UnquantizedLinearMethod, "process_weights_after_loading", lambda self, layer: processed.append(layer)
)
DiffusersPipelineLoader._process_pre_sharded_hsdp_groups_on_meta(plan)
expected = [module for group in groups for module in group.module.modules() if hasattr(module, "quant_method")]
assert expected
assert processed == expected
model._hsdp_pre_sharded_meta_post_load = False
with pytest.raises(ValueError, match="has not declared"):
DiffusersPipelineLoader._process_pre_sharded_hsdp_groups_on_meta(plan)
model._hsdp_pre_sharded_meta_post_load = True
class UnsupportedMethod(UnquantizedLinearMethod):
pass
expected[-1].quant_method = UnsupportedMethod()
processed.clear()
with pytest.raises(ValueError, match="UnsupportedMethod"):
DiffusersPipelineLoader._process_pre_sharded_hsdp_groups_on_meta(plan)
assert not processed
def test_model_cpu_offload_swaps_back_to_generator(monkeypatch: pytest.MonkeyPatch) -> None:
from vllm_omni.diffusion.models.cosmos3 import transformer_cosmos3
@@ -148,7 +148,8 @@ def test_postprocess_keeps_torch_by_default_and_converts_np_when_requested():
assert array.shape == (2, 4, 4, 3)
video = post({"video": frames}, SimpleNamespace(output_type="pt"))
assert set(video) == {"video"}
assert video["video"] is frames
assert isinstance(video["video"], np.ndarray)
np.testing.assert_array_equal(video["video"], frames.numpy())
image = post(
{"image": torch.ones(4, 4, 3)},
SimpleNamespace(output_type="pt"),
@@ -162,6 +163,40 @@ def test_postprocess_keeps_torch_by_default_and_converts_np_when_requested():
assert latent_output["image"] is latent
@pytest.mark.parametrize("output_type", [None, "pt", "np"])
@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16])
def test_postprocess_preserves_video_range_for_serving(output_type: str | None, dtype: torch.dtype) -> None:
from vllm_omni.diffusion.models.lingbot_video import get_lingbot_video_post_process_func
from vllm_omni.entrypoints.openai.video_api_utils import _coerce_video_to_frames
# Include black and midtones: all-white frames cannot expose double normalization.
frames = torch.tensor([0.0, 0.25, 0.5, 0.75, 1.0], dtype=dtype)
frames = frames.reshape(5, 1, 1, 1).expand(5, 2, 4, 3).clone()
expected = frames.float().numpy().copy()
sampling = None if output_type is None else OmniDiffusionSamplingParams(output_type=output_type)
post = get_lingbot_video_post_process_func(None)
processed = post({"video": frames}, sampling)
encoded_frames = np.stack(_coerce_video_to_frames(processed["video"]))
assert set(processed) == {"video"}
assert encoded_frames.dtype == np.float32
np.testing.assert_array_equal(encoded_frames, expected)
np.testing.assert_array_equal(frames.float().numpy(), expected)
@pytest.mark.parametrize("output_key", ["image", "video"])
def test_postprocess_preserves_latent_payload(output_key: str) -> None:
from vllm_omni.diffusion.models.lingbot_video import get_lingbot_video_post_process_func
latents = torch.tensor([-2.0, 0.0, 2.0]).reshape(1, 3, 1, 1, 1)
sampling = OmniDiffusionSamplingParams(output_type="latent")
processed = get_lingbot_video_post_process_func(None)({output_key: latents}, sampling)
assert set(processed) == {output_key}
assert processed[output_key] is latents
def test_postprocess_preserves_image_contract_for_serving():
from vllm_omni.diffusion.data import DiffusionOutput
from vllm_omni.diffusion.models.lingbot_video import get_lingbot_video_post_process_func
@@ -181,7 +216,9 @@ def test_postprocess_preserves_image_contract_for_serving():
sampling_params=sampling,
request_id="lingbot-image-contract-test",
)
raw_output = {"image": torch.ones(8, 12, 3)}
image = torch.zeros(8, 12, 3)
image[4:] = 1.0
raw_output = {"image": image}
processed = get_lingbot_video_post_process_func(SimpleNamespace())(raw_output, sampling)
normalized = normalize_diffusion_postprocess_output(processed)
@@ -199,6 +236,8 @@ def test_postprocess_preserves_image_contract_for_serving():
endpoint_images = _extract_images_from_result(result)
assert len(endpoint_images) == 1
assert endpoint_images[0].size == (12, 8)
assert endpoint_images[0].getpixel((0, 0)) == (0, 0, 0)
assert endpoint_images[0].getpixel((0, 7)) == (255, 255, 255)
def test_forward_resolves_t2v_sampling_and_flow_shift_alias():
@@ -13,7 +13,6 @@ import torch
from vllm.distributed.parallel_state import cleanup_dist_env_and_memory
from tests.helpers.mark import hardware_marks
from tests.helpers.monitor import DeviceMemoryMonitor
from tests.helpers.runtime import OmniRunner
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
from vllm_omni.platforms import current_omni_platform
@@ -55,6 +54,7 @@ IMAGE_VIDEO_MODELS_PARAMS: dict[str, dict[str, Any]] = {
}
_OFFLOAD_STATE_PROBE = "vllm_omni_layerwise_offload_test"
_OFFLOAD_MEMORY_PROBE = "vllm_omni_layerwise_offload_memory_test"
# A second CI build can reverse the same exact-head measurement without a
# source edit; the default keeps the existing baseline-first workload.
_MEASUREMENT_ORDER_ENV = "VLLM_OMNI_OFFLOAD_TEST_ORDER"
@@ -65,7 +65,7 @@ _MEASUREMENT_ORDERS: dict[str, tuple[bool, bool]] = {
class OffloadStateProbe:
"""Test-only worker extension exposing the configured offloader state."""
"""Test-only worker extension exposing offloader and allocator state."""
model_runner: Any
@@ -86,6 +86,21 @@ class OffloadStateProbe:
"block_count": sum(group_sizes),
}
def reset_peak_memory_for_test(self) -> dict[str, str]:
"""Start a process-local inference peak after model initialization."""
current_omni_platform.synchronize()
torch.accelerator.reset_peak_memory_stats()
return {"probe": _OFFLOAD_MEMORY_PROBE, "operation": "reset"}
def get_peak_memory_for_test(self) -> dict[str, str | float]:
"""Return this worker's allocator peak without counting other jobs."""
current_omni_platform.synchronize()
return {
"probe": _OFFLOAD_MEMORY_PROBE,
"operation": "snapshot",
"peak_allocated_mb": torch.accelerator.max_memory_allocated() / (1024**2),
}
def check_audio_determinism(audio1: np.ndarray, audio2: np.ndarray, atol: float = 1e-2) -> bool:
if not np.allclose(audio1, audio2, atol=atol):
@@ -96,13 +111,6 @@ def check_audio_determinism(audio1: np.ndarray, audio2: np.ndarray, atol: float
return True
def _device_used_mb(device_index: int) -> float:
current_omni_platform.synchronize()
with current_omni_platform.device(device_index):
free_bytes, total_bytes = current_omni_platform.mem_get_info()
return (total_bytes - free_bytes) / (1024**2)
def _extract_audio(output: Any) -> np.ndarray | None:
if not output:
return None
@@ -117,17 +125,29 @@ def _extract_audio(output: Any) -> np.ndarray | None:
return np.asarray(audio)
def _collect_offload_states(value: Any) -> list[dict[str, Any]]:
def _collect_probe_results(value: Any, probe: str) -> list[dict[str, Any]]:
if isinstance(value, dict):
return [value] if value.get("probe") == _OFFLOAD_STATE_PROBE else []
return [value] if value.get("probe") == probe else []
if isinstance(value, (list, tuple)):
states: list[dict[str, Any]] = []
results: list[dict[str, Any]] = []
for item in value:
states.extend(_collect_offload_states(item))
return states
results.extend(_collect_probe_results(item, probe))
return results
return []
def _collect_offload_states(value: Any) -> list[dict[str, Any]]:
return _collect_probe_results(value, _OFFLOAD_STATE_PROBE)
def _collect_memory_results(value: Any, operation: str) -> list[dict[str, Any]]:
return [
result
for result in _collect_probe_results(value, _OFFLOAD_MEMORY_PROBE)
if result.get("operation") == operation
]
def _measurement_order() -> tuple[bool, bool]:
"""Return the requested order without changing the default CI workload."""
configured = os.environ.get(_MEASUREMENT_ORDER_ENV, "baseline-first")
@@ -144,8 +164,6 @@ def run_inference(
num_inference_steps: int = 3,
) -> dict[str, Any]:
current_omni_platform.empty_cache()
device_index = current_omni_platform.current_device()
initial_used_mb = _device_used_mb(device_index)
if model_name in AUDIO_MODEL:
params = AUDIO_MODEL_PARAMS
@@ -166,52 +184,45 @@ def run_inference(
if not offload_states:
raise AssertionError("The offload-state worker probe returned no diffusion worker results")
# Measure steady-state inference memory, not model construction. Enabling
# layerwise offload first loads the model and then replaces each block's
# device storage with CPU-backed weights. Monitoring that transition
# captures both the original model and temporary staging allocations,
# which is not representative of layerwise-offloaded inference.
monitor = DeviceMemoryMonitor(device_index=device_index, interval=0.02)
current_omni_platform.reset_peak_memory_stats()
monitor.start()
# Reset and read allocator peaks in the diffusion worker process. A
# device-wide mem_get_info() sample includes unrelated sibling jobs on
# the same GPU and produced negative "savings" in shared ROCm CI.
reset_results = _collect_memory_results(
runner.omni.engine.collective_rpc(method="reset_peak_memory_for_test", timeout=60),
"reset",
)
if not reset_results:
raise AssertionError("The worker peak-memory probe returned no reset acknowledgements")
try:
# Refer to tests/e2e/offline_inference/test_wan22.py
# Use minimal settings for testing
output = runner.omni.generate(
"A cat sitting on a table",
OmniDiffusionSamplingParams(
generator=torch.Generator(device=current_omni_platform.device_type).manual_seed(42),
guidance_scale=1.0,
num_inference_steps=num_inference_steps,
**params["sampler_params"],
),
)
finally:
monitor.stop()
# Refer to tests/e2e/offline_inference/test_wan22.py.
# Use minimal settings for testing.
output = runner.omni.generate(
"A cat sitting on a table",
OmniDiffusionSamplingParams(
generator=torch.Generator(device=current_omni_platform.device_type).manual_seed(42),
guidance_scale=1.0,
num_inference_steps=num_inference_steps,
**params["sampler_params"],
),
)
memory_results = _collect_memory_results(
runner.omni.engine.collective_rpc(method="get_peak_memory_for_test", timeout=60),
"snapshot",
)
if not memory_results:
raise AssertionError("The worker peak-memory probe returned no snapshots")
audio = _extract_audio(output)
del output
# DeviceMemoryMonitor reports absolute device usage. Subtract this run's
# starting usage. Each mode runs in its own spawned process below, so model
# objects and process-local allocator/compiler/backend workspace state cannot
# carry into the other measurement. Shared filesystem caches can persist;
# _MEASUREMENT_ORDER_ENV enables a reversed-order validation run for that.
peak_used_mb = monitor.peak_used_mb
incremental_peak_mb = max(0.0, peak_used_mb - initial_used_mb)
del runner
gc.collect()
cleanup_dist_env_and_memory()
current_omni_platform.empty_cache()
post_cleanup_used_mb = _device_used_mb(device_index)
return {
"initial_used_mb": initial_used_mb,
"peak_used_mb": peak_used_mb,
"incremental_peak_mb": incremental_peak_mb,
"post_cleanup_used_mb": post_cleanup_used_mb,
"peak_allocated_mb": max(float(result["peak_allocated_mb"]) for result in memory_results),
"memory_results": memory_results,
"audio": audio,
"offload_states": offload_states,
}
@@ -259,12 +270,8 @@ def _assert_audio_outputs(
def _print_measurement(label: str, measurement: dict[str, Any]) -> None:
print(
f"{label}: initial={measurement['initial_used_mb']:.1f} MB, "
f"peak={measurement['peak_used_mb']:.1f} MB, "
f"incremental_peak={measurement['incremental_peak_mb']:.1f} MB, "
f"post_cleanup={measurement['post_cleanup_used_mb']:.1f} MB"
)
print(f"{label}: worker_peak_allocated={measurement['peak_allocated_mb']:.1f} MB")
print(f"{label} worker memory: {measurement['memory_results']}")
print(f"{label} offload state: {measurement['offload_states']}")
@@ -298,6 +305,33 @@ def test_measurement_order_rejects_unknown_value(monkeypatch) -> None:
_measurement_order()
@pytest.mark.diffusion
@pytest.mark.core_model
@pytest.mark.cpu
def test_collect_memory_results_filters_nested_worker_responses() -> None:
value = [
None,
[
{"probe": _OFFLOAD_MEMORY_PROBE, "operation": "reset"},
{
"probe": _OFFLOAD_MEMORY_PROBE,
"operation": "snapshot",
"peak_allocated_mb": 123.0,
},
],
{"probe": _OFFLOAD_STATE_PROBE, "operation": "snapshot"},
]
assert _collect_memory_results(value, "reset") == [{"probe": _OFFLOAD_MEMORY_PROBE, "operation": "reset"}]
assert _collect_memory_results(value, "snapshot") == [
{
"probe": _OFFLOAD_MEMORY_PROBE,
"operation": "snapshot",
"peak_allocated_mb": 123.0,
}
]
@pytest.mark.diffusion
@pytest.mark.core_model
@pytest.mark.cpu
@@ -366,8 +400,8 @@ def test_layerwise_offload_diffusion_model(model_name: str):
# Verify that layerwise offloading significantly reduces memory usage
# Passes only if the actual savings meets the expected savings
no_offload_peak_memory = no_offload["incremental_peak_mb"]
layerwise_offload_peak_memory = layerwise_offload["incremental_peak_mb"]
no_offload_peak_memory = no_offload["peak_allocated_mb"]
layerwise_offload_peak_memory = layerwise_offload["peak_allocated_mb"]
actual_saved_memory = no_offload_peak_memory - layerwise_offload_peak_memory
assert layerwise_offload_peak_memory + expected_saved_memory <= no_offload_peak_memory, (
f"Layerwise offload peak memory {layerwise_offload_peak_memory} MB "
@@ -77,6 +77,7 @@ class _PipelineBackedEngine:
def __init__(self, pipeline: _StepStreamingPipeline | _FailingStreamingPipeline) -> None:
self.pipeline = pipeline
self.executor = SimpleNamespace(
is_dead=False,
register_failure_callback=MagicMock(),
check_health=MagicMock(),
)
@@ -127,9 +128,9 @@ def _streaming_diffusion_output(
def _streaming_payload_and_metadata(output: DiffusionOutput) -> tuple[dict, dict]:
envelope = output.output if isinstance(output.output, dict) else {}
payload = envelope.get("payload") if isinstance(envelope.get("payload"), dict) else {}
metadata = envelope.get("metadata") if isinstance(envelope.get("metadata"), dict) else {}
return payload, metadata
payload = envelope.get("payload")
metadata = envelope.get("metadata")
return payload if isinstance(payload, dict) else {}, metadata if isinstance(metadata, dict) else {}
class TestPipelineStreamingOutputToStageDiffusionClient:
@@ -230,7 +231,7 @@ class TestPipelineStreamingOutputToStageDiffusionClient:
client._response_socket.bind(response_address)
client._encoder = OmniMsgpackEncoder()
client._decoder = OmniMsgpackDecoder()
client._output_queue: asyncio.Queue[OmniRequestOutput] = asyncio.Queue()
client._output_queue = asyncio.Queue[OmniRequestOutput]()
client._rpc_results = {}
client._pending_rpcs = set()
client._tasks = {}
+7 -3
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from types import SimpleNamespace
@@ -14,6 +14,7 @@ pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
def test_attention_uses_compile_boundary_for_hsdp(monkeypatch):
attention = object.__new__(Attention)
attention._hsdp_compile_boundary_enabled = False
calls = []
def _boundary(query, key, value, attn_metadata=None):
@@ -38,6 +39,7 @@ def test_attention_uses_compile_boundary_for_hsdp(monkeypatch):
def test_attention_keeps_compiled_impl_without_hsdp(monkeypatch):
attention = object.__new__(Attention)
attention._hsdp_compile_boundary_enabled = False
calls = []
def _boundary(query, key, value, attn_metadata=None):
@@ -60,8 +62,10 @@ def test_attention_keeps_compiled_impl_without_hsdp(monkeypatch):
assert calls == ["impl"]
def test_attention_keeps_compiled_impl_without_diffusion_config(monkeypatch):
@pytest.mark.parametrize("initialized_with_hsdp", [False, True])
def test_attention_compile_boundary_without_diffusion_config(monkeypatch, initialized_with_hsdp):
attention = object.__new__(Attention)
attention._hsdp_compile_boundary_enabled = initialized_with_hsdp
calls = []
def _boundary(query, key, value, attn_metadata=None):
@@ -80,4 +84,4 @@ def test_attention_keeps_compiled_impl_without_diffusion_config(monkeypatch):
with set_forward_context(vllm_config=None):
assert Attention.forward(attention, query, query, query) is query
assert calls == ["impl"]
assert calls == (["boundary"] if initialized_with_hsdp else ["impl"])