[AR-Diffusion] Page in kernel-legal blocks so the default resolution can run (#6481)
Signed-off-by: linzhenpl07 <linzhenpl07@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Zhou Taichang <tzhouam@connect.ust.hk>
This commit is contained in:
8 files changed
+1229
-50
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():
|
def test_budget_reduced_capacity_drives_runner_lru():
|
||||||
pipeline = CapablePipeline(tiny_spec(capacity=2))
|
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(
|
runner = make_runner(
|
||||||
pipeline,
|
pipeline,
|
||||||
available_bytes=128,
|
available_bytes=2048,
|
||||||
gpu_memory_fraction=1.0,
|
gpu_memory_fraction=1.0,
|
||||||
)
|
)
|
||||||
kv = runner.kv_cache
|
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)
|
monkeypatch.setattr(runner, "execute_model", fail_if_called)
|
||||||
runner._warmup_ar_rollout()
|
runner._warmup_ar_rollout()
|
||||||
assert execute.called is False
|
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():
|
def test_staged_window_reuse_matches_a_full_gather():
|
||||||
"""Reusing the staged history must be byte-identical to re-gathering the whole window.
|
"""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 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.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
|
from vllm_omni.experimental.ar_diffusion.kv_cache.state import ARDiffusionKVState
|
||||||
|
|
||||||
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||||
@@ -36,10 +37,26 @@ NEG = "negative"
|
|||||||
|
|
||||||
|
|
||||||
def make_state(
|
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(
|
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(
|
kv = ARDiffusionKVCache(
|
||||||
cfg,
|
cfg,
|
||||||
@@ -109,11 +126,13 @@ def _commit_video_span(
|
|||||||
n_chunks: int,
|
n_chunks: int,
|
||||||
dtype: torch.dtype,
|
dtype: torch.dtype,
|
||||||
device: torch.device,
|
device: torch.device,
|
||||||
|
chunk_size: int = BLOCK,
|
||||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
) -> 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)
|
ctx.ensure_video_slots(device)
|
||||||
k = 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, n_chunks * BLOCK, 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._k_pools[0][ctx.current_video_slot_mapping] = k[0]
|
||||||
kv._v_pools[0][ctx.current_video_slot_mapping] = v[0]
|
kv._v_pools[0][ctx.current_video_slot_mapping] = v[0]
|
||||||
st.commit_paged_context(kv_branch)
|
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()
|
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.skipif(not _gpu_flash_attn_usable(), reason="usable GPU FlashAttention is required")
|
||||||
@pytest.mark.parametrize("history_chunks", [0, 1, 3])
|
@pytest.mark.parametrize("history_chunks", [0, 1, 3])
|
||||||
@pytest.mark.parametrize("action_len", [0, 3])
|
@pytest.mark.parametrize("action_len", [0, 3])
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ from vllm_omni.experimental.ar_diffusion.kv_cache.paged import (
|
|||||||
chunk_slot_mapping,
|
chunk_slot_mapping,
|
||||||
pool_write_chunk,
|
pool_write_chunk,
|
||||||
resident_block_ids,
|
resident_block_ids,
|
||||||
|
visible_window_blocks,
|
||||||
)
|
)
|
||||||
|
|
||||||
_log = init_logger(__name__)
|
_log = init_logger(__name__)
|
||||||
@@ -282,7 +283,12 @@ class ARDiffusionKVCache:
|
|||||||
# forward needs one block per current frame plus space for any
|
# forward needs one block per current frame plus space for any
|
||||||
# model-declared action/state tokens that coexist with video KV.
|
# model-declared action/state tokens that coexist with video KV.
|
||||||
declared_scratch_blocks = (max_scratch_tokens_per_branch + block_size - 1) // block_size
|
declared_scratch_blocks = (max_scratch_tokens_per_branch + block_size - 1) // block_size
|
||||||
minimum_scratch_blocks = self.frames_per_block + declared_scratch_blocks
|
# frames_per_block counts *frames*; convert to blocks rather than
|
||||||
|
# assuming one block per frame. A frame spans ceil(chunk_size /
|
||||||
|
# block_size) blocks once the two sizes are allowed to differ.
|
||||||
|
blocks_per_frame = -(-config.chunk_size // block_size)
|
||||||
|
self.blocks_per_frame = blocks_per_frame
|
||||||
|
minimum_scratch_blocks = self.frames_per_block * blocks_per_frame + declared_scratch_blocks
|
||||||
override = os.environ.get("AR_DIFFUSION_KV_SCRATCH_BLOCKS_PER_BRANCH")
|
override = os.environ.get("AR_DIFFUSION_KV_SCRATCH_BLOCKS_PER_BRANCH")
|
||||||
override_blocks = int(override) if override is not None else 0
|
override_blocks = int(override) if override is not None else 0
|
||||||
if override_blocks < 0:
|
if override_blocks < 0:
|
||||||
@@ -313,8 +319,23 @@ class ARDiffusionKVCache:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _required_managed_blocks(capacity: int) -> int:
|
def _required_managed_blocks(capacity: int) -> int:
|
||||||
resident_per_session = config.sink_chunks + config.window_chunks
|
"""Managed blocks needed to hold ``capacity`` resident sessions.
|
||||||
return self.num_local_kv_branches * (capacity * resident_per_session + self.frames_per_block) + 2
|
|
||||||
|
Every term here counts *frames*, so each is converted to blocks.
|
||||||
|
Before block size and frame size were allowed to differ this
|
||||||
|
conversion was the identity and the frame counts could be used
|
||||||
|
directly; they cannot once a frame spans several blocks.
|
||||||
|
|
||||||
|
Converting rather than rewriting keeps the budget the same number
|
||||||
|
of *tokens* it always was, so a finer block size does not inflate
|
||||||
|
the requirement -- it only stops the final block of each term from
|
||||||
|
rounding up a whole frame's worth of memory.
|
||||||
|
"""
|
||||||
|
resident_frames = config.sink_chunks + config.window_chunks
|
||||||
|
per_session_blocks = resident_frames * self.blocks_per_frame
|
||||||
|
in_flight_blocks = self.frames_per_block * self.blocks_per_frame
|
||||||
|
headroom_blocks = 2 * self.blocks_per_frame
|
||||||
|
return self.num_local_kv_branches * (capacity * per_session_blocks + in_flight_blocks) + headroom_blocks
|
||||||
|
|
||||||
# reuse_history_staging keeps one contiguous K and V buffer per layer,
|
# reuse_history_staging keeps one contiguous K and V buffer per layer,
|
||||||
# sized to the padded visible window (sink + window plus the
|
# sized to the padded visible window (sink + window plus the
|
||||||
@@ -327,9 +348,15 @@ class ARDiffusionKVCache:
|
|||||||
self.history_staging_tokens = 0
|
self.history_staging_tokens = 0
|
||||||
if config.reuse_history_staging:
|
if config.reuse_history_staging:
|
||||||
if contiguous_kv_gather_enabled():
|
if contiguous_kv_gather_enabled():
|
||||||
self.history_staging_tokens = (
|
# The same width build_block_table pads to: a sink or window
|
||||||
self.spec.sliding_window + config.sink_chunks * config.chunk_size + block_size
|
# that is not a whole number of blocks spans one more block.
|
||||||
|
window_blocks = visible_window_blocks(
|
||||||
|
chunk_size=config.chunk_size,
|
||||||
|
block_size=block_size,
|
||||||
|
sink_tokens=config.sink_chunks * config.chunk_size,
|
||||||
|
window_tokens=self.spec.sliding_window,
|
||||||
)
|
)
|
||||||
|
self.history_staging_tokens = (window_blocks + 1) * block_size
|
||||||
self.history_staging_reserved_bytes = int(
|
self.history_staging_reserved_bytes = int(
|
||||||
2 * num_layers * self.history_staging_tokens * num_kv_heads * head_size * dtype.itemsize
|
2 * num_layers * self.history_staging_tokens * num_kv_heads * head_size * dtype.itemsize
|
||||||
)
|
)
|
||||||
@@ -649,6 +676,16 @@ class ARDiffusionKVCache:
|
|||||||
def block_table(self, adapter: ARDiffusionRequestAdapter) -> list[int]:
|
def block_table(self, adapter: ARDiffusionRequestAdapter) -> list[int]:
|
||||||
return list(self.manager.get_block_ids(adapter.request_id)[0])
|
return list(self.manager.get_block_ids(adapter.request_id)[0])
|
||||||
|
|
||||||
|
def block_ids_at(self, adapter: ARDiffusionRequestAdapter, indices: Sequence[int]) -> list[int]:
|
||||||
|
"""Block ids at ``indices`` of the request's block table, without copying it.
|
||||||
|
|
||||||
|
The table keeps a null entry for every evicted position, so it grows with
|
||||||
|
the session. :meth:`block_table` reads every entry; this reads only the
|
||||||
|
ones asked for, straight from the manager's own block list.
|
||||||
|
"""
|
||||||
|
blocks = self.manager.get_blocks(adapter.request_id).blocks[0]
|
||||||
|
return [blocks[index].block_id for index in indices]
|
||||||
|
|
||||||
def chunk_write_slots(self, adapter: ARDiffusionRequestAdapter) -> torch.Tensor:
|
def chunk_write_slots(self, adapter: ARDiffusionRequestAdapter) -> torch.Tensor:
|
||||||
"""Slot mapping for the in-flight chunk — the K/V write target."""
|
"""Slot mapping for the in-flight chunk — the K/V write target."""
|
||||||
return chunk_slot_mapping(
|
return chunk_slot_mapping(
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Cosmos port) reuses unchanged. Three concerns live here:
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
@@ -196,6 +197,19 @@ def chunk_window_skipped_tokens(
|
|||||||
return (skipped // chunk_size) * chunk_size
|
return (skipped // chunk_size) * chunk_size
|
||||||
|
|
||||||
|
|
||||||
|
def visible_window_blocks(*, chunk_size: int, block_size: int, sink_tokens: int, window_tokens: int) -> int:
|
||||||
|
"""Most blocks a sink plus a recent window can span; see ``max_video_blocks``.
|
||||||
|
|
||||||
|
Each range is converted on its own: the sink ends at a fixed position, and
|
||||||
|
the recent range starts on a chunk boundary, so at most
|
||||||
|
``block_size - gcd(chunk_size, block_size)`` tokens past a block edge.
|
||||||
|
"""
|
||||||
|
worst_start_offset = block_size - math.gcd(chunk_size, block_size)
|
||||||
|
sink_blocks = -(-sink_tokens // block_size)
|
||||||
|
recent_blocks = -(-(window_tokens + worst_start_offset) // block_size)
|
||||||
|
return sink_blocks + recent_blocks
|
||||||
|
|
||||||
|
|
||||||
class ChunkWindowManager(SlidingWindowManager):
|
class ChunkWindowManager(SlidingWindowManager):
|
||||||
"""``SlidingWindowManager`` that evicts at chunk boundaries.
|
"""``SlidingWindowManager`` that evicts at chunk boundaries.
|
||||||
|
|
||||||
@@ -219,14 +233,26 @@ class ChunkWindowManager(SlidingWindowManager):
|
|||||||
Call only after commit/eviction, then shift the request's storage
|
Call only after commit/eviction, then shift the request's storage
|
||||||
position by the returned amount. Physical pages and model positions
|
position by the returned amount. Physical pages and model positions
|
||||||
are unchanged, including any allocated but not yet committed tail.
|
are unchanged, including any allocated but not yet committed tail.
|
||||||
|
|
||||||
|
The gap starts where :meth:`remove_skipped_blocks` starts freeing: after
|
||||||
|
the sink's blocks, which are ``sink_chunks`` only while a frame is a
|
||||||
|
block. Its removal is also rounded down to a whole number of chunks.
|
||||||
|
Eviction snaps to chunk boundaries and reads its position in storage
|
||||||
|
coordinates, and paged attention reads each block's offset there too;
|
||||||
|
shifting storage by whole chunks *and* whole blocks leaves both exactly
|
||||||
|
as they were in model positions. When a frame is a block, every gap
|
||||||
|
already is one.
|
||||||
"""
|
"""
|
||||||
if self.enable_caching:
|
if self.enable_caching:
|
||||||
raise RuntimeError("AR block-table compaction requires prefix caching to be disabled")
|
raise RuntimeError("AR block-table compaction requires prefix caching to be disabled")
|
||||||
blocks = self.req_to_blocks.get(request_id, [])
|
blocks = self.req_to_blocks.get(request_id, [])
|
||||||
start = self.kv_cache_spec.sink_chunks
|
spec = self.kv_cache_spec
|
||||||
|
start = -(-(spec.sink_chunks * spec.chunk_size) // self.block_size)
|
||||||
end = start
|
end = start
|
||||||
while end < len(blocks) and blocks[end] == self._null_block:
|
while end < len(blocks) and blocks[end] == self._null_block:
|
||||||
end += 1
|
end += 1
|
||||||
|
blocks_per_step = spec.chunk_size // math.gcd(spec.chunk_size, self.block_size)
|
||||||
|
end = start + (end - start) // blocks_per_step * blocks_per_step
|
||||||
if end == start:
|
if end == start:
|
||||||
return 0
|
return 0
|
||||||
del blocks[start:end]
|
del blocks[start:end]
|
||||||
@@ -241,17 +267,32 @@ class ChunkWindowManager(SlidingWindowManager):
|
|||||||
total_computed_tokens: int,
|
total_computed_tokens: int,
|
||||||
num_prompt_tokens: int | None = None,
|
num_prompt_tokens: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Free the middle gap while preserving the leading attention sink."""
|
"""Free the middle gap while preserving the leading attention sink.
|
||||||
|
|
||||||
|
``sink_chunks`` counts frames while the block table is indexed in
|
||||||
|
blocks, so the sink is converted before it is used as an index. The two
|
||||||
|
were interchangeable while a frame was a block; this commit is what
|
||||||
|
separates them.
|
||||||
|
|
||||||
|
Neither boundary is guaranteed to land on a block edge -- 832x480 is
|
||||||
|
1560 tokens per frame against a 16-token block -- so a block can hold
|
||||||
|
the end of one frame and the start of the next. The sink end rounds up
|
||||||
|
so a straddling block stays protected, and the freed range ends on a
|
||||||
|
rounded-down boundary so only wholly-skipped blocks are released. Both
|
||||||
|
are exact when the sizes divide, which is every geometry that paged one
|
||||||
|
frame per block before.
|
||||||
|
"""
|
||||||
del num_prompt_tokens
|
del num_prompt_tokens
|
||||||
num_skipped_tokens = self.get_num_skipped_tokens(total_computed_tokens)
|
num_skipped_tokens = self.get_num_skipped_tokens(total_computed_tokens)
|
||||||
if num_skipped_tokens <= 0:
|
if num_skipped_tokens <= 0:
|
||||||
return
|
return
|
||||||
sink_blocks = self.kv_cache_spec.sink_chunks
|
sink_tokens = self.kv_cache_spec.sink_chunks * self.kv_cache_spec.chunk_size
|
||||||
num_skipped_blocks = num_skipped_tokens // self.block_size
|
first_live_block = -(-sink_tokens // self.block_size)
|
||||||
|
last_freed_block = (sink_tokens + num_skipped_tokens) // self.block_size
|
||||||
self._remove_blocks_in_range(
|
self._remove_blocks_in_range(
|
||||||
request_id,
|
request_id,
|
||||||
sink_blocks,
|
first_live_block,
|
||||||
sink_blocks + num_skipped_blocks,
|
last_freed_block,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import weakref
|
import weakref
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, ClassVar, NamedTuple
|
from typing import TYPE_CHECKING, Any, ClassVar, NamedTuple
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
from vllm_omni.experimental.ar_diffusion.kv_cache.config import contiguous_kv_gather_enabled
|
from vllm_omni.experimental.ar_diffusion.kv_cache.config import contiguous_kv_gather_enabled
|
||||||
from vllm_omni.experimental.ar_diffusion.kv_cache.paged import compute_slot_mapping
|
from vllm_omni.experimental.ar_diffusion.kv_cache.paged import compute_slot_mapping, visible_window_blocks
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from vllm.v1.attention.backend import MultipleOf
|
||||||
|
|
||||||
_LAYER_IDX_TENSORS: dict[int, torch.Tensor] = {}
|
_LAYER_IDX_TENSORS: dict[int, torch.Tensor] = {}
|
||||||
|
|
||||||
@@ -91,6 +94,21 @@ class ARDiffusionPagedForwardContext:
|
|||||||
action_slot_mapping: torch.Tensor | None = None
|
action_slot_mapping: torch.Tensor | None = None
|
||||||
query_len: int = 0
|
query_len: int = 0
|
||||||
kv_len: int = 0
|
kv_len: int = 0
|
||||||
|
# Tokens already committed when this forward began. Recorded rather than
|
||||||
|
# derived from the block count: the last block of the history is only
|
||||||
|
# partly written when a chunk is not a whole number of blocks, so
|
||||||
|
# blocks * block_size overstates it and would make attention read past
|
||||||
|
# what was written.
|
||||||
|
_history_tokens: int = 0
|
||||||
|
# Scratch blocks the current video tokens occupy. Not the same as
|
||||||
|
# len(current_video_block_ids): when the history ends mid-block, the first
|
||||||
|
# entry there is the history's own tail block, which is managed, not
|
||||||
|
# scratch. Action K/V is placed after this, so it must not count it.
|
||||||
|
_scratch_blocks_used: int = 0
|
||||||
|
# Slot offset of this chunk's first token inside its first block. The
|
||||||
|
# chunk's blocks then hold positions ``history - offset + j * block_size``,
|
||||||
|
# which is how the read path places them among the history's blocks.
|
||||||
|
_current_offset: int = 0
|
||||||
_allocated_video: bool = False
|
_allocated_video: bool = False
|
||||||
_committed: bool = False
|
_committed: bool = False
|
||||||
_action_len: int = 0
|
_action_len: int = 0
|
||||||
@@ -106,14 +124,77 @@ class ARDiffusionPagedForwardContext:
|
|||||||
def block_size(self) -> int:
|
def block_size(self) -> int:
|
||||||
return int(self.kv_cache.block_size)
|
return int(self.kv_cache.block_size)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chunk_size(self) -> int:
|
||||||
|
"""Tokens in one chunk -- one latent frame. The eviction unit."""
|
||||||
|
return int(self.kv_cache.spec.chunk_size)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def start_offset(self) -> int:
|
||||||
|
"""Where in its first block this forward's tokens begin.
|
||||||
|
|
||||||
|
Non-zero exactly when the committed history does not end on a block
|
||||||
|
boundary, which is the normal case once a frame is not a whole number
|
||||||
|
of blocks: 1560 tokens per frame against 16-token blocks leaves the
|
||||||
|
history 8 slots into its last block on every odd-numbered chunk.
|
||||||
|
"""
|
||||||
|
return int(self.adapter.num_computed_tokens) % self.block_size
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sink_tokens(self) -> int:
|
||||||
|
"""Leading tokens the window keeps however far it has slid."""
|
||||||
|
return int(self.kv_cache.spec.sink_chunks) * self.chunk_size
|
||||||
|
|
||||||
|
@property
|
||||||
|
def window_tokens(self) -> int:
|
||||||
|
"""Most recent tokens the window keeps, this forward's own included."""
|
||||||
|
return self.max_video_tokens - self.sink_tokens
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_video_blocks(self) -> int:
|
||||||
|
"""Most blocks the visible window can span, which fixes the table width.
|
||||||
|
|
||||||
|
The window is two token ranges, the sink and the most recent tokens, and
|
||||||
|
each is converted to blocks on its own because each can straddle a block
|
||||||
|
edge on its own. Rounding the two ranges' *sum* up once was a block short
|
||||||
|
whenever both straddled: at 832x480 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 it was short on every tick once the window had
|
||||||
|
filled.
|
||||||
|
|
||||||
|
The sink ends at a fixed position, so its blocks are just its tokens
|
||||||
|
rounded up. The recent range starts at a whole number of chunks, so its
|
||||||
|
start sits at most ``block_size - gcd(chunk_size, block_size)`` tokens
|
||||||
|
past a block edge, and it can need that much more room. A chunk that is
|
||||||
|
a whole number of blocks never straddles, so every geometry that paged
|
||||||
|
one frame per block keeps exactly the width it had.
|
||||||
|
|
||||||
|
The width is a constant of the configuration, which is what keeps the
|
||||||
|
block table a fixed shape as the window fills.
|
||||||
|
"""
|
||||||
|
return visible_window_blocks(
|
||||||
|
chunk_size=self.chunk_size,
|
||||||
|
block_size=self.block_size,
|
||||||
|
sink_tokens=self.sink_tokens,
|
||||||
|
window_tokens=self.window_tokens,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def num_current_video_blocks(self) -> int:
|
def num_current_video_blocks(self) -> int:
|
||||||
if self.seq_len % self.block_size != 0:
|
"""Blocks this forward's tokens occupy, counted from where they start.
|
||||||
raise AssertionError(
|
|
||||||
"AR-Diffusion paged attention expects frame-aligned seq_len "
|
A chunk need not be a whole number of blocks, so the count depends on
|
||||||
f"(multiple of block_size={self.block_size}), got {self.seq_len}"
|
the offset it begins at, not only on its length: thirty tokens
|
||||||
)
|
starting at position thirty span three sixteen-token blocks, not two.
|
||||||
return self.seq_len // self.block_size
|
Rounding up leaves the tail of the last block unwritten, which is only
|
||||||
|
safe because nothing reads past ``kv_len`` -- see
|
||||||
|
:meth:`video_block_table`.
|
||||||
|
|
||||||
|
Both paths count the same way. The committing path writes straight
|
||||||
|
after the history; the scratch path is made to line up with it by
|
||||||
|
:meth:`ensure_video_slots`.
|
||||||
|
"""
|
||||||
|
return -(-(self.start_offset + self.seq_len) // self.block_size)
|
||||||
|
|
||||||
def ensure_video_slots(self, device: torch.device) -> None:
|
def ensure_video_slots(self, device: torch.device) -> None:
|
||||||
"""Allocate/write targets for the current video tokens, once per KV branch."""
|
"""Allocate/write targets for the current video tokens, once per KV branch."""
|
||||||
@@ -121,8 +202,10 @@ class ARDiffusionPagedForwardContext:
|
|||||||
return
|
return
|
||||||
|
|
||||||
n_blocks = self.num_current_video_blocks
|
n_blocks = self.num_current_video_blocks
|
||||||
|
self._history_tokens = int(self.adapter.num_computed_tokens)
|
||||||
if self.commit_current:
|
if self.commit_current:
|
||||||
start = int(self.adapter.num_computed_tokens)
|
start = int(self.adapter.num_computed_tokens)
|
||||||
|
self._current_offset = start % self.block_size
|
||||||
self.kv_cache.allocate_token_slots(self.adapter, self.seq_len)
|
self.kv_cache.allocate_token_slots(self.adapter, self.seq_len)
|
||||||
table = self.kv_cache.block_table(self.adapter)
|
table = self.kv_cache.block_table(self.adapter)
|
||||||
start_block = start // self.block_size
|
start_block = start // self.block_size
|
||||||
@@ -132,8 +215,27 @@ class ARDiffusionPagedForwardContext:
|
|||||||
compute_slot_mapping(table, positions, self.block_size), device
|
compute_slot_mapping(table, positions, self.block_size), device
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.current_video_block_ids = self.kv_cache.scratch_block_ids(self.kv_branch, 0, n_blocks)
|
# The scratch region cannot simply start at slot zero. The kernel
|
||||||
positions = torch.arange(self.seq_len, dtype=torch.long)
|
# reads the history and this chunk as one contiguous run, so if the
|
||||||
|
# history stopped mid-block, starting here at zero would leave the
|
||||||
|
# rest of that block unwritten and the kernel would read those dead
|
||||||
|
# slots as if they were tokens, shifting the whole sequence.
|
||||||
|
#
|
||||||
|
# Instead the chunk begins where the history left off, spilling its
|
||||||
|
# first few tokens into the free tail of the history's own last
|
||||||
|
# block. That block is already this session's -- blocks are
|
||||||
|
# allocated whole -- and those slots hold nothing yet; the
|
||||||
|
# committing forward overwrites the same slots with the clean K/V
|
||||||
|
# later, so the committed state is unchanged either way.
|
||||||
|
# A history whose tail block is no longer resident has nothing to
|
||||||
|
# spill into; the window then starts at this chunk and zero is right.
|
||||||
|
offset = self.start_offset if self.history_block_ids else 0
|
||||||
|
self._current_offset = offset
|
||||||
|
self._scratch_blocks_used = n_blocks - (1 if offset else 0)
|
||||||
|
scratch_ids = self.kv_cache.scratch_block_ids(self.kv_branch, 0, self._scratch_blocks_used)
|
||||||
|
tail_block = [self.history_block_ids[-1]] if offset else []
|
||||||
|
self.current_video_block_ids = tail_block + scratch_ids
|
||||||
|
positions = torch.arange(offset, offset + self.seq_len, dtype=torch.long)
|
||||||
self.current_video_slot_mapping = _to_device_async(
|
self.current_video_slot_mapping = _to_device_async(
|
||||||
compute_slot_mapping(self.current_video_block_ids, positions, self.block_size), device
|
compute_slot_mapping(self.current_video_block_ids, positions, self.block_size), device
|
||||||
)
|
)
|
||||||
@@ -152,7 +254,9 @@ class ARDiffusionPagedForwardContext:
|
|||||||
return
|
return
|
||||||
|
|
||||||
action_blocks = (action_len + self.block_size - 1) // self.block_size
|
action_blocks = (action_len + self.block_size - 1) // self.block_size
|
||||||
scratch_offset = 0 if self.commit_current else len(self.current_video_block_ids)
|
# Count scratch blocks, not entries: the video list may lead with the
|
||||||
|
# history's tail block, which lives in the managed pool.
|
||||||
|
scratch_offset = 0 if self.commit_current else self._scratch_blocks_used
|
||||||
self.action_scratch_block_ids = self.kv_cache.scratch_block_ids(
|
self.action_scratch_block_ids = self.kv_cache.scratch_block_ids(
|
||||||
self.kv_branch,
|
self.kv_branch,
|
||||||
scratch_offset,
|
scratch_offset,
|
||||||
@@ -165,23 +269,75 @@ class ARDiffusionPagedForwardContext:
|
|||||||
self._action_len = action_len
|
self._action_len = action_len
|
||||||
|
|
||||||
def video_block_table(self, device: torch.device) -> tuple[list[int], int]:
|
def video_block_table(self, device: torch.device) -> tuple[list[int], int]:
|
||||||
|
"""Blocks the attention reads, and how many of their slots it reads.
|
||||||
|
|
||||||
|
The kernel reads the listed blocks as one contiguous run and stops after
|
||||||
|
the returned length, so two things must hold: every block holding a
|
||||||
|
token the window keeps is listed, in token order, and only the last
|
||||||
|
listed block may be partly written.
|
||||||
|
|
||||||
|
Blocks are chosen by the token positions they hold rather than counted
|
||||||
|
off the ends of the resident list. The window is the sink ``[0, S)``
|
||||||
|
plus the most recent tokens, and each of those can straddle a block
|
||||||
|
edge on its own. Taking a fixed number of blocks off the tail dropped
|
||||||
|
the block holding the window's first tokens exactly when both
|
||||||
|
straddled, and a length capped by a block count rather than counted
|
||||||
|
from what was written then ran past the last written slot.
|
||||||
|
|
||||||
|
Whole blocks are read, so each boundary block contributes up to
|
||||||
|
``block_size - 1`` tokens the window has already let go of. They are
|
||||||
|
real K/V at their own positions; a slightly wider window is a far
|
||||||
|
smaller error than a truncated one.
|
||||||
|
"""
|
||||||
self.ensure_video_slots(device)
|
self.ensure_video_slots(device)
|
||||||
if self.max_video_tokens % self.block_size != 0:
|
block_size = self.block_size
|
||||||
raise AssertionError(
|
history = self._history_tokens
|
||||||
"AR-Diffusion paged attention requires max_video_tokens to be block-aligned, "
|
end = history + self.seq_len
|
||||||
f"got max_video_tokens={self.max_video_tokens}, block_size={self.block_size}"
|
sink_end = self.sink_tokens
|
||||||
)
|
recent_start = end - self.window_tokens
|
||||||
all_video_blocks = self.history_block_ids + self.current_video_block_ids
|
|
||||||
max_video_blocks = self.max_video_tokens // self.block_size
|
# Position of each block's first slot. History comes from the block
|
||||||
sink_blocks = int(self.kv_cache.spec.sink_chunks)
|
# table, where an evicted block is a null placeholder, so a block's index
|
||||||
if len(all_video_blocks) <= max_video_blocks:
|
# is its position -- in storage positions, which is what
|
||||||
visible_video_blocks = all_video_blocks
|
# num_computed_tokens counts once compaction has dropped an evicted gap.
|
||||||
else:
|
# Compaction shifts storage by whole chunks and whole blocks, so the sink,
|
||||||
tail_blocks = max_video_blocks - sink_blocks
|
# the window and every block offset sit exactly where they would in model
|
||||||
visible_video_blocks = all_video_blocks[:sink_blocks]
|
# positions. Reading the table after ensure_video_slots means a block
|
||||||
if tail_blocks:
|
# this chunk's own allocation evicted is never listed. The current
|
||||||
visible_video_blocks += all_video_blocks[-tail_blocks:]
|
# chunk's blocks follow from where its first token was placed; its first
|
||||||
video_len = len(visible_video_blocks) * self.block_size
|
# block is the history's own tail block whenever the history stopped
|
||||||
|
# part way through one, and keeps the history's position.
|
||||||
|
first_position: dict[int, int] = {}
|
||||||
|
#
|
||||||
|
# Only two index ranges can hold a token the window keeps: the sink's
|
||||||
|
# blocks, and the blocks from the recent window's start onward. Evicted
|
||||||
|
# positions stay in the table as null entries until compaction drops
|
||||||
|
# them a whole number of chunks at a time, so the table can still be
|
||||||
|
# much longer than the window; reading just these two ranges, without
|
||||||
|
# copying the table, keeps this at the window's size.
|
||||||
|
history_blocks = -(-history // block_size)
|
||||||
|
sink_blocks = min(-(-sink_end // block_size), history_blocks)
|
||||||
|
recent_first_block = max(max(recent_start, 0) // block_size, sink_blocks)
|
||||||
|
indices = [*range(sink_blocks), *range(recent_first_block, history_blocks)]
|
||||||
|
for index, block in zip(indices, self.kv_cache.block_ids_at(self.adapter, indices)):
|
||||||
|
if block != self.kv_cache.null_block_id:
|
||||||
|
first_position.setdefault(block, index * block_size)
|
||||||
|
current_start = history - self._current_offset
|
||||||
|
for index, block in enumerate(self.current_video_block_ids):
|
||||||
|
first_position.setdefault(int(block), current_start + index * block_size)
|
||||||
|
|
||||||
|
visible_video_blocks: list[int] = []
|
||||||
|
video_len = 0
|
||||||
|
for block, first in sorted(first_position.items(), key=lambda item: item[1]):
|
||||||
|
if first >= sink_end and first + block_size <= recent_start:
|
||||||
|
continue # wholly between the sink and the recent window
|
||||||
|
if video_len % block_size:
|
||||||
|
raise RuntimeError(
|
||||||
|
"AR-Diffusion paged attention would read past a partly written block: "
|
||||||
|
f"block {visible_video_blocks[-1]} is followed by block {block}."
|
||||||
|
)
|
||||||
|
visible_video_blocks.append(block)
|
||||||
|
video_len += min(block_size, end - first)
|
||||||
return visible_video_blocks, video_len
|
return visible_video_blocks, video_len
|
||||||
|
|
||||||
def build_block_table(
|
def build_block_table(
|
||||||
@@ -200,6 +356,21 @@ class ARDiffusionPagedForwardContext:
|
|||||||
``ceil(seq_lens/block_size)`` entries, so padding is never read.
|
``ceil(seq_lens/block_size)`` entries, so padding is never read.
|
||||||
"""
|
"""
|
||||||
video_blocks, video_len = self.video_block_table(device)
|
video_blocks, video_len = self.video_block_table(device)
|
||||||
|
# Action K/V is listed after the video blocks and the kernel reads the
|
||||||
|
# table as one run, so the video run has to end on a block edge. If it
|
||||||
|
# stops part way through its last block, the run reads that block's
|
||||||
|
# unwritten slots as the first action tokens and never reaches the last
|
||||||
|
# ones -- a wrong attention with every shape intact. A frame that is a
|
||||||
|
# whole number of blocks always ends on an edge, so this refuses only
|
||||||
|
# geometries that could not page at all before the paging unit was
|
||||||
|
# separated from the frame.
|
||||||
|
if action_len > 0 and video_len % self.block_size:
|
||||||
|
raise ValueError(
|
||||||
|
"AR-Diffusion paged attention cannot place action tokens after a partly written video block: "
|
||||||
|
f"the video run ends {video_len % self.block_size} slots into block {video_blocks[-1]}, so the "
|
||||||
|
"kernel would read its unwritten slots as action tokens. Action tokens need tokens_per_frame "
|
||||||
|
f"to be a whole number of blocks (block_size={self.block_size})."
|
||||||
|
)
|
||||||
self.ensure_action_slots(action_len, device)
|
self.ensure_action_slots(action_len, device)
|
||||||
action_blocks = self.action_scratch_block_ids if action_len > 0 else []
|
action_blocks = self.action_scratch_block_ids if action_len > 0 else []
|
||||||
block_ids = video_blocks + action_blocks
|
block_ids = video_blocks + action_blocks
|
||||||
@@ -208,12 +379,19 @@ class ARDiffusionPagedForwardContext:
|
|||||||
|
|
||||||
# Fixed capacity: full visible video window + one action-capacity block.
|
# Fixed capacity: full visible video window + one action-capacity block.
|
||||||
action_capacity_blocks = max(1, (action_len + self.block_size - 1) // self.block_size)
|
action_capacity_blocks = max(1, (action_len + self.block_size - 1) // self.block_size)
|
||||||
width = max(self.max_video_tokens // self.block_size + action_capacity_blocks, len(block_ids))
|
# Both of these count in blocks. Deriving them from the token count by
|
||||||
|
# flooring would understate the capacity whenever the window is not a
|
||||||
|
# whole number of blocks: the width would fall back on len(block_ids)
|
||||||
|
# and start tracking how full the window is -- the exact shape churn
|
||||||
|
# the fixed width exists to prevent -- and max_seq_len could come out
|
||||||
|
# below the kv_len actually being passed, by up to block_size - 1.
|
||||||
|
capacity_blocks = self.max_video_blocks + action_capacity_blocks
|
||||||
|
width = max(capacity_blocks, len(block_ids))
|
||||||
padded = block_ids + [0] * (width - len(block_ids))
|
padded = block_ids + [0] * (width - len(block_ids))
|
||||||
|
|
||||||
self.query_len = int(query_len)
|
self.query_len = int(query_len)
|
||||||
self.kv_len = int(video_len + action_len)
|
self.kv_len = int(video_len + action_len)
|
||||||
max_seq_len = int(self.max_video_tokens + action_capacity_blocks * self.block_size)
|
max_seq_len = int(capacity_blocks * self.block_size)
|
||||||
# Built on the host once per AR block; the copies go through the same
|
# Built on the host once per AR block; the copies go through the same
|
||||||
# pinned, non-blocking path as the slot mappings above so the CPU does
|
# pinned, non-blocking path as the slot mappings above so the CPU does
|
||||||
# not wait for the stream to drain three times before the first layer.
|
# not wait for the stream to drain three times before the first layer.
|
||||||
@@ -281,7 +459,11 @@ class ARDiffusionPagedForwardContext:
|
|||||||
int(self.seq_len),
|
int(self.seq_len),
|
||||||
)
|
)
|
||||||
same_session = state.adapter is not None and state.adapter() is self.adapter
|
same_session = state.adapter is not None and state.adapter() is self.adapter
|
||||||
reuse = same_session and state.signature == signature
|
# A chunk that is not a whole number of blocks shares its first block with the history's tail (see
|
||||||
|
# ensure_video_slots), and the attention op restages the current chunk in whole blocks of its own,
|
||||||
|
# so such a forward restages the whole window instead.
|
||||||
|
block_aligned = self.start_offset == 0 and self.seq_len % self.block_size == 0
|
||||||
|
reuse = same_session and state.signature == signature and block_aligned
|
||||||
state.adapter = weakref.ref(self.adapter)
|
state.adapter = weakref.ref(self.adapter)
|
||||||
state.signature = signature
|
state.signature = signature
|
||||||
self.staging_enabled = True
|
self.staging_enabled = True
|
||||||
@@ -461,6 +643,36 @@ def _resolve_fa_version(head_size: int) -> int:
|
|||||||
return version
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
def supported_kernel_block_sizes() -> list[int | MultipleOf]:
|
||||||
|
"""Block sizes the kernel this module dispatches to will accept.
|
||||||
|
|
||||||
|
Same shape as vLLM's ``AttentionBackend.get_supported_kernel_block_sizes``
|
||||||
|
-- a plain int is that exact size, ``MultipleOf(b)`` is any positive
|
||||||
|
multiple of ``b`` -- but answered here rather than read off a backend,
|
||||||
|
because AR-Diffusion does not go through backend selection. It calls
|
||||||
|
``flash_attn_varlen_func`` itself, choosing between vLLM's CUDA build and
|
||||||
|
ROCm's AITER a few lines below, and only this module knows which.
|
||||||
|
|
||||||
|
It lives next to that choice so there is one place to change. The
|
||||||
|
constraint is a property of the kernel, not of the card, and the kernels
|
||||||
|
reachable from here agree on 16 today: vLLM's CUDA FlashAttention, ROCm
|
||||||
|
AITER, and upstream ``flash_attn`` all advertise ``MultipleOf(16)``. Other
|
||||||
|
backends in the same tree do not -- ``hpc_attn`` accepts only 64, and
|
||||||
|
FlashInfer advertises pages of 128 or more solely on Blackwell -- so a
|
||||||
|
caller must treat this as data to be queried, never as the number 16.
|
||||||
|
|
||||||
|
vLLM's FlashAttention backend does advertise a single 128-token page when FA4
|
||||||
|
runs its dedicated head-size-256 kernel on SM100/SM110. That kernel is not
|
||||||
|
reachable from here: ``get_flash_attn_version`` selects it only when the
|
||||||
|
caller passes ``supports_fa4_hd256=True``, and :func:`_resolve_fa_version`
|
||||||
|
does not, so a head size of 256 falls back to FA2, which pages at any
|
||||||
|
multiple of 16. Passing that flag later has to change this answer as well.
|
||||||
|
"""
|
||||||
|
from vllm.v1.attention.backend import MultipleOf
|
||||||
|
|
||||||
|
return [MultipleOf(16)]
|
||||||
|
|
||||||
|
|
||||||
def _rocm_flash_attn_varlen_func():
|
def _rocm_flash_attn_varlen_func():
|
||||||
"""Resolve a ROCm varlen kernel, preferring AITER when available.
|
"""Resolve a ROCm varlen kernel, preferring AITER when available.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||||
"""Model-facing session state backed by paged AR-Diffusion KV storage."""
|
"""Model-facing session state backed by paged AR-Diffusion KV storage."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ from __future__ import annotations
|
|||||||
import dataclasses
|
import dataclasses
|
||||||
import time
|
import time
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator, Sequence
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import torch
|
import torch
|
||||||
from vllm.logger import init_logger
|
from vllm.logger import init_logger
|
||||||
@@ -29,9 +30,61 @@ from vllm_omni.experimental.ar_diffusion.kv_cache.state import ARDiffusionKVStat
|
|||||||
from vllm_omni.experimental.ar_diffusion.tick_protocol import ARDiffusionTickRequest
|
from vllm_omni.experimental.ar_diffusion.tick_protocol import ARDiffusionTickRequest
|
||||||
from vllm_omni.platforms import current_omni_platform
|
from vllm_omni.platforms import current_omni_platform
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from vllm.v1.attention.backend import MultipleOf
|
||||||
|
|
||||||
logger = init_logger(__name__)
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def paging_block_size(tokens_per_frame: int, supported: Sequence[int | MultipleOf] | None = None) -> int:
|
||||||
|
"""Choose the paging unit, which is not the eviction unit.
|
||||||
|
|
||||||
|
AR-Diffusion evicts by frame; the attention kernel tiles by a fixed number
|
||||||
|
of tokens. Using the frame as the block binds every output resolution to
|
||||||
|
that kernel constraint, so the resolutions that run today do so by
|
||||||
|
arithmetic accident -- and the LingBot World v2 checkpoint's own default
|
||||||
|
832x480 is not one of them:
|
||||||
|
|
||||||
|
(480/16) x (832/16) = 30 x 52 = 1560 tokens per frame, 1560 % 16 = 8
|
||||||
|
|
||||||
|
A frame that is already a legal block stays one, so every resolution that
|
||||||
|
works today keeps precisely the paging it has now. Anything else pages at
|
||||||
|
the finest legal unit, which costs a longer block table and gives back the
|
||||||
|
rounding waste of a block one whole frame wide -- at 832x480 that block is
|
||||||
|
over a gigabyte.
|
||||||
|
|
||||||
|
What counts as legal is asked, not assumed. ``supported`` carries vLLM's
|
||||||
|
own vocabulary: a plain int is that exact size, ``MultipleOf(b)`` is any
|
||||||
|
positive multiple of ``b``. The kernels AR-Diffusion reaches agree on
|
||||||
|
multiples of 16, but others in the same tree do not -- ``hpc_attn`` takes
|
||||||
|
only 64, and FlashInfer offers pages of 128 and up on Blackwell alone. A
|
||||||
|
constant here would be a silently wrong block size on the first machine
|
||||||
|
whose kernel differs, and a wrong block size is not an error: it is a
|
||||||
|
kernel reading tokens that were never written.
|
||||||
|
"""
|
||||||
|
if supported is None:
|
||||||
|
# Imported here, not at module scope: paged_attention registers a
|
||||||
|
# custom op on import, and the tests that pop it from sys.modules to
|
||||||
|
# re-import it depend on nothing else holding it first.
|
||||||
|
from vllm_omni.experimental.ar_diffusion.kv_cache.paged_attention import supported_kernel_block_sizes
|
||||||
|
|
||||||
|
supported = supported_kernel_block_sizes()
|
||||||
|
if not supported:
|
||||||
|
raise ValueError("The attention kernel advertises no legal block size.")
|
||||||
|
|
||||||
|
def legal(size: int) -> bool:
|
||||||
|
return size > 0 and any(
|
||||||
|
size == entry if isinstance(entry, int) else size % entry.base == 0 for entry in supported
|
||||||
|
)
|
||||||
|
|
||||||
|
if legal(tokens_per_frame):
|
||||||
|
return tokens_per_frame
|
||||||
|
finest = min(entry if isinstance(entry, int) else entry.base for entry in supported)
|
||||||
|
if not legal(finest):
|
||||||
|
raise ValueError(f"No legal paging unit among {supported!r}.")
|
||||||
|
return finest
|
||||||
|
|
||||||
|
|
||||||
def resolve_ar_diffusion_kv_config(od_config: OmniDiffusionConfig) -> ARDiffusionKVConfig:
|
def resolve_ar_diffusion_kv_config(od_config: OmniDiffusionConfig) -> ARDiffusionKVConfig:
|
||||||
"""Resolve optional deployment overrides and enable AR-Diffusion KV."""
|
"""Resolve optional deployment overrides and enable AR-Diffusion KV."""
|
||||||
raw = getattr(od_config, "ar_diffusion_kv_config", None)
|
raw = getattr(od_config, "ar_diffusion_kv_config", None)
|
||||||
@@ -139,7 +192,7 @@ class ARDiffusionModelRunner(DiffusionModelRunner):
|
|||||||
num_kv_heads=spec.num_kv_heads,
|
num_kv_heads=spec.num_kv_heads,
|
||||||
head_size=spec.head_size,
|
head_size=spec.head_size,
|
||||||
dtype=self.od_config.dtype,
|
dtype=self.od_config.dtype,
|
||||||
block_size=spec.tokens_per_frame,
|
block_size=paging_block_size(spec.tokens_per_frame),
|
||||||
max_model_len=spec.max_model_len,
|
max_model_len=spec.max_model_len,
|
||||||
available_bytes=self._available_memory_bytes() if available_bytes is None else available_bytes,
|
available_bytes=self._available_memory_bytes() if available_bytes is None else available_bytes,
|
||||||
kv_branches=spec.kv_branches,
|
kv_branches=spec.kv_branches,
|
||||||
|
|||||||
Reference in new issue
Block a user