[Perf/Fix] Reimplement Batched CFG Forward for Bagel (#4098)
Signed-off-by: Alex Brooks <albrooks@redhat.com> Co-authored-by: 汪志鹏 <wangzhipeng628@gmail.com>
This commit is contained in:
1 parent
fc32d9fe6f
commit
7ff11d8d0f
7 files changed
+506
-44
No files matched your search
@@ -0,0 +1,223 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Unit tests for NaiveCache merge/split logic used in batched CFG."""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.models.bagel.bagel_transformer import NaiveCache
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
NUM_LAYERS = 2
|
||||
NUM_KV_HEADS = 4
|
||||
HEAD_DIM = 8
|
||||
|
||||
|
||||
def _make_cache(num_layers, seq_len, num_kv_heads=NUM_KV_HEADS, head_dim=HEAD_DIM, seed=0):
|
||||
"""Create a NaiveCache with deterministic random data. seq_len=0 returns an empty cache."""
|
||||
gen = torch.Generator().manual_seed(seed)
|
||||
cache = NaiveCache(num_layers)
|
||||
if seq_len == 0:
|
||||
return cache
|
||||
for layer in range(num_layers):
|
||||
cache.key_cache[layer] = torch.randn(seq_len, num_kv_heads, head_dim, generator=gen)
|
||||
cache.value_cache[layer] = torch.randn(seq_len, num_kv_heads, head_dim, generator=gen)
|
||||
return cache
|
||||
|
||||
|
||||
def test_init_creates_none_entries():
|
||||
"""Ensure the NaiveCache is initialized with None values per layer."""
|
||||
cache = NaiveCache(NUM_LAYERS)
|
||||
assert cache.num_layers == NUM_LAYERS
|
||||
for layer in range(NUM_LAYERS):
|
||||
assert cache.key_cache[layer] is None
|
||||
assert cache.value_cache[layer] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seq_len", [0, 10])
|
||||
def test_seq_lens_empty(seq_len):
|
||||
"""Ensure that by default, we have 0 seq lens."""
|
||||
cache = _make_cache(NUM_LAYERS, seq_len=seq_len)
|
||||
assert cache.seq_lens == seq_len
|
||||
assert cache.num_layers == NUM_LAYERS
|
||||
|
||||
|
||||
### Merge tests
|
||||
def test_merge_two_equal_length():
|
||||
"""Ensure that we can merge two NaiveCaches that are identically shaped."""
|
||||
c0 = _make_cache(NUM_LAYERS, seq_len=5, seed=0)
|
||||
c1 = _make_cache(NUM_LAYERS, seq_len=5, seed=1)
|
||||
merged = NaiveCache.merge([c0, c1])
|
||||
|
||||
assert merged.key_values_lens == [5, 5]
|
||||
for layer in range(NUM_LAYERS):
|
||||
assert merged.key_cache[layer].shape[0] == 10
|
||||
# the merged cache will just have keys and values per layer concatenated
|
||||
assert torch.equal(merged.key_cache[layer][:5], c0.key_cache[layer])
|
||||
assert torch.equal(merged.key_cache[layer][5:], c1.key_cache[layer])
|
||||
assert torch.equal(merged.value_cache[layer][:5], c0.value_cache[layer])
|
||||
assert torch.equal(merged.value_cache[layer][5:], c1.value_cache[layer])
|
||||
|
||||
|
||||
def test_merge_three_zero_len_cache():
|
||||
"""Ensure we handle zero length cache correctly in merge."""
|
||||
# NOTE: This is relevant for text_cfg in Bagel, which has a len of 0 by default
|
||||
gen_cache = _make_cache(NUM_LAYERS, seq_len=10, seed=0)
|
||||
text_cfg_cache = _make_cache(NUM_LAYERS, seq_len=0)
|
||||
img_cfg_cache = _make_cache(NUM_LAYERS, seq_len=7, seed=2)
|
||||
merged = NaiveCache.merge([gen_cache, text_cfg_cache, img_cfg_cache])
|
||||
|
||||
assert merged.key_values_lens == [10, 0, 7]
|
||||
for layer in range(NUM_LAYERS):
|
||||
assert merged.key_cache[layer].shape[0] == 17
|
||||
assert torch.equal(merged.key_cache[layer][:10], gen_cache.key_cache[layer])
|
||||
assert torch.equal(merged.key_cache[layer][10:], img_cfg_cache.key_cache[layer])
|
||||
|
||||
|
||||
def test_merge_all_empty():
|
||||
"""Ensure that merging empty caches is well defined."""
|
||||
caches = [_make_cache(NUM_LAYERS, seq_len=0) for _ in range(3)]
|
||||
merged = NaiveCache.merge(caches)
|
||||
|
||||
assert merged.key_values_lens == [0, 0, 0]
|
||||
for layer in range(NUM_LAYERS):
|
||||
assert merged.key_cache[layer] is None
|
||||
assert merged.value_cache[layer] is None
|
||||
|
||||
|
||||
def test_merge_single_cache():
|
||||
"""Ensure merging one cache returns an identical cache."""
|
||||
c = _make_cache(NUM_LAYERS, seq_len=8, seed=42)
|
||||
merged = NaiveCache.merge([c])
|
||||
|
||||
assert merged.key_values_lens == [8]
|
||||
for layer in range(NUM_LAYERS):
|
||||
assert torch.equal(merged.key_cache[layer], c.key_cache[layer])
|
||||
|
||||
|
||||
def test_merge_preserves_dtype():
|
||||
"""Ensure merging doesn't modify dtypes."""
|
||||
cache = NaiveCache(NUM_LAYERS)
|
||||
for layer in range(NUM_LAYERS):
|
||||
cache.key_cache[layer] = torch.randn(5, NUM_KV_HEADS, HEAD_DIM, dtype=torch.bfloat16)
|
||||
cache.value_cache[layer] = torch.randn(5, NUM_KV_HEADS, HEAD_DIM, dtype=torch.bfloat16)
|
||||
|
||||
merged = NaiveCache.merge([cache])
|
||||
assert merged.key_cache[0].dtype == torch.bfloat16
|
||||
assert merged.value_cache[0].dtype == torch.bfloat16
|
||||
|
||||
|
||||
### Split tests
|
||||
def test_split_all_nonzero():
|
||||
"""Ensure NaiveCache splits in the simple case (all lens nonzero)."""
|
||||
t = torch.randn(15, NUM_KV_HEADS, HEAD_DIM)
|
||||
parts = NaiveCache.split_with_zeros(t, [5, 4, 6])
|
||||
|
||||
assert len(parts) == 3
|
||||
assert all(p is not None for p in parts)
|
||||
assert parts[0].shape[0] == 5
|
||||
assert parts[1].shape[0] == 4
|
||||
assert parts[2].shape[0] == 6
|
||||
assert torch.equal(torch.cat(parts), t)
|
||||
|
||||
|
||||
def test_split_with_zero():
|
||||
"""Ensure NaiveCache split handles zero length correctly (used in Bagel)."""
|
||||
t = torch.randn(17, NUM_KV_HEADS, HEAD_DIM)
|
||||
parts = NaiveCache.split_with_zeros(t, [10, 0, 7])
|
||||
|
||||
assert parts[0].shape[0] == 10
|
||||
assert parts[1] is None
|
||||
assert parts[2].shape[0] == 7
|
||||
assert torch.equal(torch.cat([parts[0], parts[2]]), t)
|
||||
|
||||
|
||||
def test_split_wrong_sum_raises():
|
||||
"""Ensure NaiveCache raises if splits don't match the sum of dims on axis 0."""
|
||||
t = torch.randn(10, NUM_KV_HEADS, HEAD_DIM)
|
||||
with pytest.raises(ValueError, match="dim 0"):
|
||||
NaiveCache.split_with_zeros(t, [5, 3])
|
||||
|
||||
|
||||
def test_split_negative_length_raises():
|
||||
"""Ensure NaiveCache raises if splits have any negative values."""
|
||||
t = torch.randn(10, NUM_KV_HEADS, HEAD_DIM)
|
||||
with pytest.raises(ValueError, match="greater than or equal to zero"):
|
||||
NaiveCache.split_with_zeros(t, [5, -1, 6])
|
||||
|
||||
|
||||
def test_split_preserves_dtype():
|
||||
"""Ensure NaiveCache split preserves dtype."""
|
||||
t = torch.randn(10, NUM_KV_HEADS, HEAD_DIM, dtype=torch.bfloat16)
|
||||
parts = NaiveCache.split_with_zeros(t, [4, 6])
|
||||
assert parts[0].dtype == torch.bfloat16
|
||||
assert parts[1].dtype == torch.bfloat16
|
||||
|
||||
|
||||
### from_object tests (for kv cache transfer)
|
||||
def test_from_object_passthrough():
|
||||
"""Ensure a NaiveCache input is returned as is."""
|
||||
cache = _make_cache(NUM_LAYERS, seq_len=5)
|
||||
assert NaiveCache.from_object(cache) is cache
|
||||
|
||||
|
||||
def test_from_object_converts_simple_namespace():
|
||||
"""Ensure SimpleNamespace with list-based caches converts to NaiveCache."""
|
||||
keys = [torch.randn(5, NUM_KV_HEADS, HEAD_DIM) for _ in range(NUM_LAYERS)]
|
||||
values = [torch.randn(5, NUM_KV_HEADS, HEAD_DIM) for _ in range(NUM_LAYERS)]
|
||||
ns = SimpleNamespace(key_cache=keys, value_cache=values)
|
||||
|
||||
cache = NaiveCache.from_object(ns)
|
||||
|
||||
assert isinstance(cache, NaiveCache)
|
||||
assert cache.num_layers == NUM_LAYERS
|
||||
for i in range(NUM_LAYERS):
|
||||
assert torch.equal(cache.key_cache[i], keys[i])
|
||||
assert torch.equal(cache.value_cache[i], values[i])
|
||||
|
||||
|
||||
def test_from_object_mismatched_lengths_raises():
|
||||
"""Ensure mismatched key/value cache lengths raise due to strict=True in zip."""
|
||||
keys = [torch.randn(5, NUM_KV_HEADS, HEAD_DIM) for _ in range(2)]
|
||||
values = [torch.randn(5, NUM_KV_HEADS, HEAD_DIM) for _ in range(3)]
|
||||
ns = SimpleNamespace(key_cache=keys, value_cache=values)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
NaiveCache.from_object(ns)
|
||||
|
||||
|
||||
### End to end test for split / merge
|
||||
def test_round_trip_two_populated():
|
||||
"""Roundtrip test for merging and resplitting two simple caches."""
|
||||
c0 = _make_cache(NUM_LAYERS, seq_len=5, seed=0)
|
||||
c1 = _make_cache(NUM_LAYERS, seq_len=8, seed=1)
|
||||
merged = NaiveCache.merge([c0, c1])
|
||||
|
||||
for layer in range(NUM_LAYERS):
|
||||
k_parts = NaiveCache.split_with_zeros(merged.key_cache[layer], merged.key_values_lens)
|
||||
v_parts = NaiveCache.split_with_zeros(merged.value_cache[layer], merged.key_values_lens)
|
||||
assert torch.equal(k_parts[0], c0.key_cache[layer])
|
||||
assert torch.equal(k_parts[1], c1.key_cache[layer])
|
||||
assert torch.equal(v_parts[0], c0.value_cache[layer])
|
||||
assert torch.equal(v_parts[1], c1.value_cache[layer])
|
||||
|
||||
|
||||
def test_round_trip_three_branches_with_zero_cfg():
|
||||
"""Roundtrip test with a zero entry (i.e., same as Bagel's gen/text_cfg/img_cfg case)."""
|
||||
gen_cache = _make_cache(NUM_LAYERS, seq_len=10, seed=0)
|
||||
text_cfg_cache = _make_cache(NUM_LAYERS, seq_len=0)
|
||||
img_cfg_cache = _make_cache(NUM_LAYERS, seq_len=7, seed=2)
|
||||
merged = NaiveCache.merge([gen_cache, text_cfg_cache, img_cfg_cache])
|
||||
|
||||
for layer in range(NUM_LAYERS):
|
||||
k_parts = NaiveCache.split_with_zeros(merged.key_cache[layer], merged.key_values_lens)
|
||||
v_parts = NaiveCache.split_with_zeros(merged.value_cache[layer], merged.key_values_lens)
|
||||
assert torch.equal(k_parts[0], gen_cache.key_cache[layer])
|
||||
assert k_parts[1] is None
|
||||
assert torch.equal(k_parts[2], img_cfg_cache.key_cache[layer])
|
||||
assert torch.equal(v_parts[0], gen_cache.value_cache[layer])
|
||||
assert v_parts[1] is None
|
||||
assert torch.equal(v_parts[2], img_cfg_cache.value_cache[layer])
|
||||
Whitespace-only changes.
@@ -0,0 +1,75 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Unit tests for kv utils."""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.utils.kv_utils import left_pad_stack
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
def test_uniform_lengths():
|
||||
"""Ensure uniform 3D tensors correctly stack to 4D and have no mask."""
|
||||
tensors = [torch.randn(10, 4, 128) for _ in range(3)]
|
||||
stacked, mask = left_pad_stack(tensors)
|
||||
assert stacked.shape == (3, 10, 4, 128)
|
||||
assert mask is None
|
||||
for i in range(3):
|
||||
assert torch.equal(stacked[i], tensors[i])
|
||||
|
||||
|
||||
def test_variable_lengths():
|
||||
"""Ensure variable 3D tensors correctly stack to 4D with a mask."""
|
||||
t1 = torch.ones(5, 2, 4)
|
||||
t2 = torch.ones(8, 2, 4)
|
||||
t3 = torch.ones(3, 2, 4)
|
||||
stacked, mask = left_pad_stack([t1, t2, t3])
|
||||
|
||||
assert stacked.shape == (3, 8, 2, 4)
|
||||
assert mask is not None
|
||||
assert mask.shape == (3, 8)
|
||||
# Ensure summing over dim 1 gives our seq lens back
|
||||
assert mask.sum(dim=1).tolist() == [5, 8, 3]
|
||||
|
||||
|
||||
def test_single_tensor_is_4d():
|
||||
"""Ensure a single 3D tensor is expanded to 4D."""
|
||||
t = torch.randn(7, 4, 128)
|
||||
stacked, mask = left_pad_stack([t])
|
||||
assert stacked.shape == (1, 7, 4, 128)
|
||||
assert mask is None
|
||||
assert torch.equal(stacked[0], t)
|
||||
|
||||
|
||||
def test_preserves_device_and_dtype():
|
||||
"""Ensure device/dtype is preserved."""
|
||||
t1 = torch.randn(3, 2, dtype=torch.bfloat16)
|
||||
t2 = torch.randn(5, 2, dtype=torch.bfloat16)
|
||||
stacked, mask = left_pad_stack([t1, t2])
|
||||
assert stacked.dtype == torch.bfloat16
|
||||
assert stacked.device == t1.device
|
||||
assert mask.device == t1.device
|
||||
|
||||
|
||||
def test_mismatched_trailing_shapes_raises():
|
||||
"""Ensure that mismatched dims outside of 0 explodes."""
|
||||
t1 = torch.randn(5, 4, 128)
|
||||
t2 = torch.randn(5, 8, 128)
|
||||
with pytest.raises(ValueError):
|
||||
left_pad_stack([t1, t2])
|
||||
|
||||
|
||||
def test_mismatched_ndim_raises():
|
||||
"""Ensure that mismatched ndims explodes."""
|
||||
t1 = torch.randn(5, 4)
|
||||
t2 = torch.randn(5, 4, 128)
|
||||
with pytest.raises(ValueError):
|
||||
left_pad_stack([t1, t2])
|
||||
|
||||
|
||||
def test_empty_list_raises():
|
||||
"""Ensure that tensors must be nonempty."""
|
||||
with pytest.raises(ValueError):
|
||||
left_pad_stack([])
|
||||
@@ -7,7 +7,7 @@ End-to-end tests for Bagel with shared memory connector: img2img and text2img.
|
||||
- img2img: validates output vs reference pixels within a ±10 tolerance.
|
||||
- text2img: validates output vs reference pixels within a ±5 tolerance
|
||||
(equivalent to `examples/offline_inference/bagel/end2end.py` with
|
||||
text2img modality and 15 steps).
|
||||
text2img modality and 14 steps).
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -46,7 +46,6 @@ REFERENCE_PIXELS = [
|
||||
{"position": (256, 256), "rgb": (181, 201, 221)},
|
||||
]
|
||||
|
||||
|
||||
# text2img reference pixels (aligned with offline `bagel/end2end.py` text2img, 15 steps)
|
||||
# "Generated with seed=52, num_inference_steps=15,
|
||||
# prompt='A cute cat'"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# Original file was released under Apache-2.0, with the full license text
|
||||
# available at https://github.com/huggingface/transformers/blob/main/LICENSE.
|
||||
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Iterable, Sequence
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
@@ -51,6 +51,7 @@ from vllm_omni.diffusion.layers.mot.mot_layernorm import MoTRMSNorm
|
||||
from vllm_omni.diffusion.layers.mot.mot_qkv_parallel_linear import MoTQKVParallelLinear
|
||||
from vllm_omni.diffusion.layers.mot.mot_row_parallel_linear import MoTRowParallelLinear
|
||||
from vllm_omni.diffusion.layers.rope import RotaryEmbedding
|
||||
from vllm_omni.diffusion.utils.kv_utils import left_pad_stack
|
||||
from vllm_omni.model_executor.layers.timestep_embedding import timestep_embedding
|
||||
|
||||
logger = init_logger(__name__)
|
||||
@@ -326,6 +327,10 @@ class NaiveCache:
|
||||
def __init__(self, num_layers):
|
||||
self.key_cache = {k: None for k in range(num_layers)}
|
||||
self.value_cache = {k: None for k in range(num_layers)}
|
||||
# Track kv_lens; we need this because we pack the forward passes
|
||||
# for CFG into a single forward call and the kv length may be different,
|
||||
# e.g., due to 0 kvs for text_cfg path and nonzero for others
|
||||
self.key_values_lens: list[int] | None = None
|
||||
|
||||
@property
|
||||
def num_layers(self):
|
||||
@@ -338,6 +343,76 @@ class NaiveCache:
|
||||
else:
|
||||
return 0
|
||||
|
||||
@classmethod
|
||||
def from_object(cls, obj) -> "NaiveCache":
|
||||
"""Convert a duck-typed cache (e.g., SimpleNamespace from KV transfer)
|
||||
to NaiveCache; in the future, we should find a better way to handle this,
|
||||
e.g., a model agnostic abstraction for key cache transfer instead of having
|
||||
this cache live in bagel.
|
||||
|
||||
NOTE: If a NaiveCache is provided, the object is just returned. Otherwise,
|
||||
we enumerate over the key/value cache values and map layer indices to the
|
||||
corresponding tensors.
|
||||
"""
|
||||
if isinstance(obj, cls):
|
||||
return obj
|
||||
cache = cls(len(obj.key_cache))
|
||||
for i, (k, v) in enumerate(zip(obj.key_cache, obj.value_cache, strict=True)):
|
||||
cache.key_cache[i] = k
|
||||
cache.value_cache[i] = v
|
||||
return cache
|
||||
|
||||
@staticmethod
|
||||
def merge(caches: Sequence["NaiveCache"]) -> "NaiveCache":
|
||||
"""Merge per-branch NaiveCaches into one for batched attention;
|
||||
this lets us do the forward passes for CFG in one batched pass,
|
||||
although it's worth noting that this is currently only used
|
||||
for single request. We need this so that gen mode knows the
|
||||
respective kv lengths, and can split things back out as needed.
|
||||
"""
|
||||
num_layers = caches[0].num_layers
|
||||
merged = NaiveCache(num_layers)
|
||||
lens = [c.seq_lens for c in caches]
|
||||
merged.key_values_lens = lens
|
||||
|
||||
nonempty = [c for c in caches if c.key_cache[0] is not None]
|
||||
if not nonempty:
|
||||
return merged
|
||||
|
||||
for layer in range(num_layers):
|
||||
merged.key_cache[layer] = torch.cat([c.key_cache[layer] for c in nonempty], dim=0)
|
||||
merged.value_cache[layer] = torch.cat([c.value_cache[layer] for c in nonempty], dim=0)
|
||||
|
||||
return merged
|
||||
|
||||
@staticmethod
|
||||
def split_with_zeros(
|
||||
tensor: torch.Tensor,
|
||||
lengths: Sequence[int],
|
||||
) -> list[torch.Tensor | None]:
|
||||
"""Split tensor by lengths, which may include 0 entries, e.g., for splitting cfg
|
||||
branches out, since text_cfg may have 0 kv length.
|
||||
|
||||
0 lengths will be replaced with None in the returned list.
|
||||
"""
|
||||
# Ensure that the lengths are all nonzero and sum to the first dim of our tensor
|
||||
if not all(isinstance(ln, int) and ln >= 0 for ln in lengths):
|
||||
raise ValueError("split lengths must be greater than or equal to zero")
|
||||
|
||||
expected = sum(ln for ln in lengths if ln > 0)
|
||||
if tensor.shape[0] != expected:
|
||||
raise ValueError(f"tensor dim 0 ({tensor.shape[0]}) != sum of nonzero lengths ({expected})")
|
||||
|
||||
result: list[torch.Tensor | None] = []
|
||||
offset = 0
|
||||
for ln in lengths:
|
||||
if ln > 0:
|
||||
result.append(tensor[offset : offset + ln])
|
||||
offset += ln
|
||||
else:
|
||||
result.append(None)
|
||||
return result
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseNavitOutputWithPast(ModelOutput):
|
||||
@@ -427,6 +502,7 @@ class PackedAttentionMoT(nn.Module):
|
||||
def _forward_gen(
|
||||
self,
|
||||
packed_query_sequence: torch.Tensor,
|
||||
query_lens: torch.Tensor,
|
||||
packed_query_position_embeddings: torch.Tensor,
|
||||
past_key_values: NaiveCache | None,
|
||||
packed_vae_token_indexes: torch.Tensor,
|
||||
@@ -454,6 +530,7 @@ class PackedAttentionMoT(nn.Module):
|
||||
text_indices = packed_text_indexes
|
||||
vae_indices = packed_vae_token_indexes
|
||||
|
||||
cache_k = cache_v = None
|
||||
packed_query_sequence = packed_query_sequence.to(torch.bfloat16)
|
||||
|
||||
# MoT QKV projection routes text/vae tokens to the matching weights.
|
||||
@@ -484,6 +561,10 @@ class PackedAttentionMoT(nn.Module):
|
||||
vae_k = k[vae_indices]
|
||||
vae_v = v[vae_indices]
|
||||
|
||||
num_branches = len(query_lens)
|
||||
text_per_branch = text_q.shape[0] // num_branches
|
||||
vae_per_branch = vae_q.shape[0] // num_branches
|
||||
|
||||
# Build joint K/V: [kv_cache, text_markers] (replicated across SP ranks)
|
||||
if past_key_values is not None and past_key_values.key_cache[self.layer_idx] is not None:
|
||||
cache_k = past_key_values.key_cache[self.layer_idx]
|
||||
@@ -510,15 +591,46 @@ class PackedAttentionMoT(nn.Module):
|
||||
),
|
||||
)
|
||||
else:
|
||||
q = torch.cat([text_q, vae_q], dim=0).unsqueeze(0)
|
||||
k = torch.cat([ctx_k, vae_k], dim=0).unsqueeze(0)
|
||||
v = torch.cat([ctx_v, vae_v], dim=0).unsqueeze(0)
|
||||
attn_out = self.attn_noncausal(q, k, v)
|
||||
text_q_parts = text_q.split([text_per_branch] * num_branches)
|
||||
vae_q_parts = vae_q.split([vae_per_branch] * num_branches)
|
||||
text_k_parts = text_k.split([text_per_branch] * num_branches)
|
||||
vae_k_parts = vae_k.split([vae_per_branch] * num_branches)
|
||||
text_v_parts = text_v.split([text_per_branch] * num_branches)
|
||||
vae_v_parts = vae_v.split([vae_per_branch] * num_branches)
|
||||
|
||||
text_len = text_q.shape[0]
|
||||
attn_out = attn_out.squeeze(0)
|
||||
text_attn = attn_out[:text_len].reshape(text_len, self.q_size)
|
||||
vae_attn = attn_out[text_len:].reshape(-1, self.q_size)
|
||||
# Query lengths should not be variable since we
|
||||
# just split above, so we just concat + stack to 4D
|
||||
q_4d = torch.stack([torch.cat([t, v]) for t, v in zip(text_q_parts, vae_q_parts)])
|
||||
|
||||
if cache_k is not None and cache_v is not None:
|
||||
kv_lens = getattr(past_key_values, "key_values_lens", None)
|
||||
if kv_lens is None:
|
||||
per_branch = cache_k.shape[0] // num_branches
|
||||
kv_lens = [per_branch] * num_branches
|
||||
|
||||
ck_per_branch = NaiveCache.split_with_zeros(cache_k, kv_lens)
|
||||
cv_per_branch = NaiveCache.split_with_zeros(cache_v, kv_lens)
|
||||
k_branches = [
|
||||
torch.cat([t for t in (ck_per_branch[i], text_k_parts[i], vae_k_parts[i]) if t is not None])
|
||||
for i in range(num_branches)
|
||||
]
|
||||
v_branches = [
|
||||
torch.cat([t for t in (cv_per_branch[i], text_v_parts[i], vae_v_parts[i]) if t is not None])
|
||||
for i in range(num_branches)
|
||||
]
|
||||
k_4d, mask = left_pad_stack(k_branches)
|
||||
v_4d, _ = left_pad_stack(v_branches)
|
||||
metadata = DiffusionAttentionMetadata(attn_mask=mask) if mask is not None else None
|
||||
else:
|
||||
k_4d = torch.stack([torch.cat([t, v]) for t, v in zip(text_k_parts, vae_k_parts)])
|
||||
v_4d = torch.stack([torch.cat([t, v]) for t, v in zip(text_v_parts, vae_v_parts)])
|
||||
metadata = None
|
||||
attn_out = self.attn_noncausal(q_4d, k_4d, v_4d, metadata)
|
||||
|
||||
attn_out = attn_out.reshape(num_branches, -1, self.q_size)
|
||||
text_attn = attn_out[:, :text_per_branch].reshape(-1, self.q_size)
|
||||
vae_attn = attn_out[:, text_per_branch:].reshape(-1, self.q_size)
|
||||
text_len = text_attn.shape[0]
|
||||
|
||||
# MoT output projection over local [text, vae] order.
|
||||
local_packed = torch.cat([text_attn, vae_attn], dim=0)
|
||||
@@ -662,6 +774,7 @@ class PackedAttentionMoT(nn.Module):
|
||||
raise ValueError("Generation model for Bagel requires non-causal attention")
|
||||
return self._forward_gen(
|
||||
packed_query_sequence=packed_query_sequence,
|
||||
query_lens=query_lens,
|
||||
packed_query_position_embeddings=packed_query_position_embeddings,
|
||||
past_key_values=past_key_values,
|
||||
packed_vae_token_indexes=packed_vae_token_indexes,
|
||||
@@ -1867,15 +1980,15 @@ class Bagel(nn.Module):
|
||||
# Each CFG branch runs its own LLM forward; we just need the
|
||||
# per-branch packed_position_ids and past_key_values for
|
||||
# ``Bagel.forward`` to dispatch through.
|
||||
cfg_branches: dict | None = None
|
||||
cfg_branch_pids: list[torch.Tensor] | None = None
|
||||
cfg_branch_caches: list[NaiveCache] | None = None
|
||||
|
||||
if use_cfg_text:
|
||||
branches_pid = [packed_position_ids, cfg_text_packed_position_ids]
|
||||
branches_cache = [past_key_values, cfg_text_past_key_values]
|
||||
cfg_branch_pids = [packed_position_ids, cfg_text_packed_position_ids]
|
||||
cfg_branch_caches = [past_key_values, cfg_text_past_key_values]
|
||||
if use_cfg_img:
|
||||
branches_pid.append(cfg_img_packed_position_ids)
|
||||
branches_cache.append(cfg_img_past_key_values)
|
||||
cfg_branches = {"pids": branches_pid, "caches": branches_cache}
|
||||
cfg_branch_pids.append(cfg_img_packed_position_ids)
|
||||
cfg_branch_caches.append(cfg_img_past_key_values)
|
||||
|
||||
if return_trajectory_latents and len(timesteps) > 0:
|
||||
trajectory_latents.append(x_t.clone())
|
||||
@@ -1908,7 +2021,8 @@ class Bagel(nn.Module):
|
||||
cfg_renorm_type=cfg_renorm_type,
|
||||
cfg_text_scale=cfg_text_scale_,
|
||||
cfg_img_scale=cfg_img_scale_,
|
||||
cfg_branches=cfg_branches,
|
||||
cfg_branch_pids=cfg_branch_pids,
|
||||
cfg_branch_caches=cfg_branch_caches,
|
||||
)
|
||||
|
||||
if scheduler is not None:
|
||||
@@ -2252,7 +2366,8 @@ class Bagel(nn.Module):
|
||||
cfg_renorm_type: str = "global",
|
||||
cfg_text_scale: float = 1.0,
|
||||
cfg_img_scale: float = 1.0,
|
||||
cfg_branches: dict | None = None,
|
||||
cfg_branch_pids: list[torch.Tensor] | None = None,
|
||||
cfg_branch_caches: list[NaiveCache] | None = None,
|
||||
):
|
||||
# Build query sequence (identical for all CFG branches)
|
||||
packed_text_embedding = self.language_model.forward(
|
||||
@@ -2280,31 +2395,37 @@ class Bagel(nn.Module):
|
||||
cfg_text_v_t = None
|
||||
cfg_img_v_t = None
|
||||
|
||||
if use_cfg and cfg_branches is not None:
|
||||
# Sequential per-branch CFG forwards (matches upstream lance.py).
|
||||
# The previous batched path concatenated cond + cfg into one LLM
|
||||
# forward, but the block-diagonal attention mask was lost when
|
||||
# PR #3728 dropped flash_attn_varlen, so branches leaked into
|
||||
# each other. Running each branch through its own forward is
|
||||
# numerically identical to upstream.
|
||||
def _run_branch(branch_pkv, branch_pids):
|
||||
out = self.language_model.forward(
|
||||
packed_query_sequence=packed_sequence,
|
||||
query_lens=packed_seqlens,
|
||||
packed_query_position_ids=branch_pids,
|
||||
past_key_values=branch_pkv,
|
||||
update_past_key_values=False,
|
||||
is_causal=False,
|
||||
**extra_inputs,
|
||||
)
|
||||
return self.llm2vae(out.packed_query_sequence)[packed_vae_token_indexes]
|
||||
if use_cfg and cfg_branch_pids is not None and cfg_branch_caches is not None:
|
||||
num_branches = len(cfg_branch_pids)
|
||||
seq_len = int(packed_seqlens.sum())
|
||||
|
||||
branches_pids = cfg_branches["pids"]
|
||||
branches_caches = cfg_branches["caches"]
|
||||
v_t = _run_branch(branches_caches[0], branches_pids[0])
|
||||
cfg_text_v_t = _run_branch(branches_caches[1], branches_pids[1])
|
||||
if cfg_img_scale > 1.0 and len(branches_caches) > 2:
|
||||
cfg_img_v_t = _run_branch(branches_caches[2], branches_pids[2])
|
||||
batched_sequence = packed_sequence.repeat(num_branches, 1)
|
||||
batched_vae_indexes = torch.cat([packed_vae_token_indexes + i * seq_len for i in range(num_branches)])
|
||||
batched_position_ids = torch.cat(cfg_branch_pids)
|
||||
batched_seqlens = packed_seqlens.repeat(num_branches)
|
||||
merged_cache = NaiveCache.merge(cfg_branch_caches)
|
||||
|
||||
if self.use_moe:
|
||||
batched_text_indices = torch.cat([packed_text_indexes + i * seq_len for i in range(num_branches)])
|
||||
extra_inputs["packed_vae_token_indexes"] = batched_vae_indexes
|
||||
extra_inputs["packed_text_indexes"] = batched_text_indices
|
||||
|
||||
output = self.language_model.forward(
|
||||
packed_query_sequence=batched_sequence,
|
||||
query_lens=batched_seqlens,
|
||||
packed_query_position_ids=batched_position_ids,
|
||||
past_key_values=merged_cache,
|
||||
update_past_key_values=False,
|
||||
is_causal=False,
|
||||
**extra_inputs,
|
||||
)
|
||||
|
||||
all_vae_v_t = self.llm2vae(output.packed_query_sequence)[batched_vae_indexes]
|
||||
vae_per_branch = packed_vae_token_indexes.shape[0]
|
||||
branch_v_ts = all_vae_v_t.split(vae_per_branch)
|
||||
v_t = branch_v_ts[0]
|
||||
cfg_text_v_t = branch_v_ts[1]
|
||||
cfg_img_v_t = branch_v_ts[2] if len(branch_v_ts) > 2 else None
|
||||
else:
|
||||
# Single forward (no CFG or outside cfg_interval).
|
||||
output = self.language_model.forward(
|
||||
|
||||
@@ -395,6 +395,7 @@ class BagelPipeline(nn.Module, SupportsComponentDiscovery, DiffusionPipelineProf
|
||||
injected_kv = req.sampling_params.past_key_values
|
||||
if injected_kv is not None:
|
||||
logger.info("Using injected KV Cache (direct)")
|
||||
injected_kv = NaiveCache.from_object(injected_kv)
|
||||
gen_context["past_key_values"] = injected_kv
|
||||
seq_len = injected_kv.key_cache[0].shape[0]
|
||||
gen_context["kv_lens"] = [seq_len]
|
||||
@@ -431,6 +432,7 @@ class BagelPipeline(nn.Module, SupportsComponentDiscovery, DiffusionPipelineProf
|
||||
)
|
||||
|
||||
if cfg_text_kv is not None:
|
||||
cfg_text_kv = NaiveCache.from_object(cfg_text_kv)
|
||||
cfg_text_seq_len = cfg_text_kv.key_cache[0].shape[0]
|
||||
cfg_text_context["past_key_values"] = cfg_text_kv
|
||||
cfg_text_context["kv_lens"] = [cfg_text_seq_len]
|
||||
@@ -458,6 +460,7 @@ class BagelPipeline(nn.Module, SupportsComponentDiscovery, DiffusionPipelineProf
|
||||
else:
|
||||
cfg_img_context["ropes"] = [cfg_img_seq_len]
|
||||
else:
|
||||
cfg_img_kv = NaiveCache.from_object(cfg_img_kv)
|
||||
cfg_img_seq_len = cfg_img_kv.key_cache[0].shape[0]
|
||||
cfg_img_context["past_key_values"] = cfg_img_kv
|
||||
cfg_img_context["kv_lens"] = [cfg_img_seq_len]
|
||||
@@ -553,6 +556,8 @@ class BagelPipeline(nn.Module, SupportsComponentDiscovery, DiffusionPipelineProf
|
||||
for k, v in gen_input_img.items():
|
||||
if torch.is_tensor(v):
|
||||
gen_input_img[k] = v.to(self.device)
|
||||
for k in ("packed_indexes", "packed_key_value_indexes", "key_values_lens"):
|
||||
gen_input_img.pop(k, None)
|
||||
with torch.autocast(
|
||||
device_type=self.device.type,
|
||||
enabled=self.device.type != "cpu",
|
||||
@@ -604,7 +609,7 @@ class BagelPipeline(nn.Module, SupportsComponentDiscovery, DiffusionPipelineProf
|
||||
|
||||
# cfg_text_context: update with negative prompt (no text condition).
|
||||
# When empty, keep cfg_text_context as-is (kv_lens=0) to match
|
||||
# original BAGEL; _merge_naive_caches handles None KV entries.
|
||||
# original BAGEL.
|
||||
neg_prompt = extra_args.get("negative_prompt", "")
|
||||
if neg_prompt:
|
||||
neg_input, neg_newlens, neg_rope = self.bagel.prepare_prompts(
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Utilities for batching variable-length tensors in diffusion attention."""
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def left_pad_stack(
|
||||
tensors: list[torch.Tensor],
|
||||
) -> tuple[torch.Tensor, torch.Tensor | None]:
|
||||
"""Left-pad and stack variable-length tensors. Only dim 0 may vary,
|
||||
and it's assumed that all tensors are the same dtype & use the same
|
||||
device.
|
||||
|
||||
Returns (stacked, mask) where mask is a 2D boolean mask, and both
|
||||
tensors are on the device of the provided tensors. If all tensors are
|
||||
the same length, None is returned for mask.
|
||||
"""
|
||||
trailing_dims = {ts.shape[1:] for ts in tensors}
|
||||
if len(trailing_dims) != 1:
|
||||
raise ValueError("Tensors must be non-empty and can only vary in dim 0")
|
||||
trailing = trailing_dims.pop()
|
||||
|
||||
seq_lens = [ts.shape[0] for ts in tensors]
|
||||
max_len = max(seq_lens)
|
||||
|
||||
# If everything is the same length, we don't need a mask / varlen
|
||||
if all(sl == max_len for sl in seq_lens):
|
||||
return torch.stack(tensors), None
|
||||
|
||||
device = tensors[0].device
|
||||
dtype = tensors[0].dtype
|
||||
stacked = torch.zeros(len(tensors), max_len, *trailing, dtype=dtype, device=device)
|
||||
# Create the boolean mask for the input sequences
|
||||
mask = torch.zeros(len(tensors), max_len, dtype=torch.bool, device=device)
|
||||
for idx, (ts, sl) in enumerate(zip(tensors, seq_lens)):
|
||||
pad = max_len - sl
|
||||
stacked[idx, pad:] = ts
|
||||
mask[idx, pad:] = True
|
||||
|
||||
return stacked, mask
|
||||
Reference in new issue
Block a user