Files
vllm-omni/vllm_omni/diffusion/models/bagel/bagel_transformer.py
T

2480 lines
106 KiB
Python

# Copyright 2025 Bytedance Ltd. and/or its affiliates.
# Copyright (c) 2024 The Qwen Team and The HuggingFace Inc. team.
# SPDX-License-Identifier: Apache-2.0
#
# This file has been modified by ByteDance Ltd. and/or its affiliates.
#
# 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, Sequence
from dataclasses import dataclass
import numpy as np
import torch
import torch.distributed as dist
from cache_dit import ForwardPattern
from torch import nn
from transformers.models.qwen2.configuration_qwen2 import Qwen2Config
from transformers.models.qwen2.modeling_qwen2 import (
Qwen2PreTrainedModel,
)
from transformers.utils import ModelOutput
from vllm.distributed import get_tensor_model_parallel_world_size
from vllm.logger import init_logger
from vllm.model_executor.layers.activation import SiluAndMul
from vllm.model_executor.layers.linear import (
ColumnParallelLinear,
MergedColumnParallelLinear,
RowParallelLinear,
)
from vllm.model_executor.layers.quantization.base_config import (
QuantizationConfig,
)
from vllm.model_executor.layers.vocab_parallel_embedding import VocabParallelEmbedding
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm.transformers_utils.configs.bagel import BagelConfig
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata as DiffusionAttentionMetadata
from vllm_omni.diffusion.attention.layer import Attention as DiffusionAttention
from vllm_omni.diffusion.cache.cachedit import BagelCachedAdapter, CacheDiTAdapterConfig
from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
from vllm_omni.diffusion.distributed.parallel_state import (
get_cfg_group,
get_classifier_free_guidance_world_size,
get_sequence_parallel_rank,
get_sp_group,
)
from vllm_omni.diffusion.forward_context import get_forward_context, is_forward_context_available
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__)
def patchify(imgs, p):
"""
imgs: (N, 3, H, W) or (3, H, W)
x: (N, L, patch_size**2 *3) or (L, patch_size**2 *3)
"""
is_batch = imgs.ndim == 4
if not is_batch:
imgs = imgs.unsqueeze(0)
# n: batch, c: channel, h: grid_h, p: patch_h, w: grid_w, q: patch_w
x = imgs.reshape(imgs.shape[0], 3, imgs.shape[2] // p, p, imgs.shape[3] // p, p)
# Permute to (n, grid_h, grid_w, c, patch_h, patch_w) to match Conv2d (c, h, w) flattening
x = torch.einsum("nchpwq->nhwcpq", x)
x = x.reshape(imgs.shape[0], -1, 3 * p**2)
if not is_batch:
x = x.squeeze(0)
return x
class MLPconnector(nn.Module):
def __init__(
self,
input_dim,
output_dim,
activation="gelu_pytorch_tanh",
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__()
self.fc1 = ColumnParallelLinear(
input_dim, output_dim, bias=True, gather_output=False, quant_config=quant_config, prefix=f"{prefix}.fc1"
)
if activation == "gelu":
self.act = nn.GELU()
elif activation == "gelu_pytorch_tanh":
self.act = nn.GELU(approximate="tanh")
else:
self.act = nn.ReLU()
self.fc2 = RowParallelLinear(
output_dim, output_dim, bias=True, input_is_parallel=True, quant_config=quant_config, prefix=f"{prefix}.fc2"
)
def forward(self, x):
x_parallel, _ = self.fc1(x)
x_parallel = self.act(x_parallel)
return self.fc2(x_parallel)[0]
class BagelRotaryEmbedding(nn.Module):
"""Standalone rotary embedding that generates cos/sin from position ids.
Replaces HuggingFace's Qwen2RotaryEmbedding while preserving full
``rope_scaling`` support. When ``config.rope_scaling`` is set (e.g.
linear, dynamic-NTK, YaRN, …), we delegate the ``inv_freq`` /
``attention_scaling`` computation to HF's ``ROPE_INIT_FUNCTIONS`` so
that the frequency basis and scaling factor are identical to the
original checkpoint.
For Qwen2.5-VL-style multimodal RoPE (``rope_scaling.rope_type == "mrope"``)
the ``inv_freq`` basis is the standard default-rope one; the difference
is that position ids are 3-D ``(t, h, w)`` per token and the
``mrope_section`` describes how the head dimension is split across axes.
This module accepts either 2-D scalar position ids ``(B, S)`` or 3-D
multimodal position ids ``(B, 3, S)`` and dispatches accordingly so the
same module works for both BAGEL (1-D rope) and Lance (Qwen2.5-VL mrope).
This module has no learnable parameters.
"""
def __init__(self, config):
super().__init__()
# transformers>=5.0 stores rope params under ``rope_parameters`` and
# always populates ``rope_scaling`` (even for the default type), so we
# infer ``rope_type`` from either and fall back to "default".
rope_scaling = getattr(config, "rope_scaling", None) or {}
rope_parameters = getattr(config, "rope_parameters", None) or {}
rope_type = (
rope_scaling.get("rope_type") or rope_scaling.get("type") or rope_parameters.get("rope_type") or "default"
)
# Cache mrope_section for the forward-time section-split.
self._mrope_section: list[int] | None = None
if rope_type == "mrope":
section = rope_scaling.get("mrope_section") or rope_parameters.get("mrope_section")
if section is None:
raise ValueError("rope_scaling.rope_type == 'mrope' requires 'mrope_section'.")
self._mrope_section = list(section)
if rope_type in ("default", "mrope"):
# mrope shares the default sinusoidal frequency basis; the
# multimodal split happens at forward time, not at init.
rope_theta = (
getattr(config, "rope_theta", None)
or rope_parameters.get("rope_theta")
or rope_scaling.get("rope_theta")
or 10000.0
)
dim = config.hidden_size // config.num_attention_heads
inv_freq = 1.0 / (rope_theta ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
self.attention_scaling = 1.0
else:
from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS
rope_init_fn = ROPE_INIT_FUNCTIONS[rope_type]
inv_freq, self.attention_scaling = rope_init_fn(config, device=None)
self.register_buffer("inv_freq", inv_freq, persistent=False)
@torch.no_grad()
def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""Generate cos/sin embeddings for given position ids.
Args:
x: Input tensor (only used for dtype inference).
position_ids: Either 2-D scalar ``(batch_size, seq_len)`` for plain
1-D RoPE, or 3-D multimodal ``(batch_size, 3, seq_len)`` for
Qwen2.5-VL-style mRoPE. The latter is auto-detected from
``position_ids.ndim``.
Returns:
cos, sin: Rotary embeddings, each of shape (batch_size, seq_len, dim).
"""
if position_ids.ndim == 3 and self._mrope_section is not None:
# multimodal path: position_ids is (B, 3, S) with rows = (t, h, w).
# Compute per-axis frequencies, then assemble the per-section
# rotary basis matching Qwen2-VL's ``apply_multimodal_rotary_pos_emb``.
B = position_ids.shape[0]
inv_freq_expanded = self.inv_freq[None, None, :, None].float().expand(B, 3, -1, 1)
position_ids_expanded = position_ids[:, :, None, :].float()
freqs = (inv_freq_expanded @ position_ids_expanded).transpose(2, 3)
# ``freqs`` is (B, 3, S, head_dim/2); double along the last axis
# to get the full ``head_dim`` rotary basis.
emb = torch.cat((freqs, freqs), dim=-1) # (B, 3, S, head_dim)
cos_per_axis = emb.cos() * self.attention_scaling
sin_per_axis = emb.sin() * self.attention_scaling
# ``mrope_section`` (e.g. [16, 24, 24] for Qwen2.5-VL) sums to
# head_dim/2; doubled it sums to head_dim and cycles axis = i % 3.
sec_full = self._mrope_section * 2
cos_split = cos_per_axis.split(sec_full, dim=-1)
sin_split = sin_per_axis.split(sec_full, dim=-1)
cos = torch.cat([c[:, i % 3] for i, c in enumerate(cos_split)], dim=-1)
sin = torch.cat([s[:, i % 3] for i, s in enumerate(sin_split)], dim=-1)
return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1)
position_ids_expanded = position_ids[:, None, :].float()
freqs = (inv_freq_expanded @ position_ids_expanded).transpose(1, 2)
emb = torch.cat((freqs, freqs), dim=-1)
cos = emb.cos() * self.attention_scaling
sin = emb.sin() * self.attention_scaling
return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
class BagelMLP(nn.Module):
"""FFN with Mixture-of-Tokens routing via MoT parallel linear layers.
gate_proj + up_proj are fused into a single MoTMergedColumnParallelLinear.
down_proj uses MoTRowParallelLinear. Both layers hold text weights on self
and vae weights on self.gen_exp, routing by text_indices / vae_indices.
"""
def __init__(
self,
hidden_size: int,
intermediate_size: int,
hidden_act: str = "silu",
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.intermediate_size = intermediate_size
self.gate_up_proj = MergedColumnParallelLinear(
input_size=hidden_size,
output_sizes=[intermediate_size, intermediate_size],
bias=False,
gather_output=False,
quant_config=quant_config,
prefix=f"{prefix}.gate_up_proj",
)
self.down_proj = RowParallelLinear(
input_size=intermediate_size,
output_size=hidden_size,
bias=False,
input_is_parallel=True,
quant_config=quant_config,
prefix=f"{prefix}.down_proj",
)
if hidden_act != "silu":
raise ValueError(f"Unsupported activation: {hidden_act}. Only silu is supported.")
self.act_fn = SiluAndMul()
def forward(
self,
x: torch.Tensor,
) -> torch.Tensor:
gate_up, _ = self.gate_up_proj(x)
x = self.act_fn(gate_up)
x, _ = self.down_proj(x)
return x
class Qwen2MoTConfig(Qwen2Config):
"""Configuration for Qwen2MoT (Mixture of Tokens) model.
This is fundamentally different from Qwen2, hence the distinct name.
"""
model_type = "qwen2_mot"
keys_to_ignore_at_inference = ["past_key_values"]
def __init__(
self,
vocab_size=151936,
hidden_size=4096,
intermediate_size=22016,
num_hidden_layers=32,
num_attention_heads=32,
num_key_value_heads=32,
hidden_act="silu",
max_position_embeddings=32768,
initializer_range=0.02,
rms_norm_eps=1e-6,
use_cache=True,
tie_word_embeddings=False,
rope_theta=10000.0,
rope_scaling=None,
use_sliding_window=False,
sliding_window=4096,
max_window_layers=28,
attention_dropout=0.0,
is_causal=True,
_attn_implementation="eager",
qk_norm=True,
layer_module="Qwen2MoTDecoderLayer",
freeze_und=False,
**kwargs,
):
super().__init__(
vocab_size=vocab_size,
hidden_size=hidden_size,
intermediate_size=intermediate_size,
num_hidden_layers=num_hidden_layers,
num_attention_heads=num_attention_heads,
num_key_value_heads=num_key_value_heads,
hidden_act=hidden_act,
max_position_embeddings=max_position_embeddings,
initializer_range=initializer_range,
rms_norm_eps=rms_norm_eps,
use_cache=use_cache,
tie_word_embeddings=tie_word_embeddings,
rope_theta=rope_theta,
rope_scaling=rope_scaling,
use_sliding_window=use_sliding_window,
sliding_window=sliding_window,
max_window_layers=max_window_layers,
attention_dropout=attention_dropout,
is_causal=is_causal,
_attn_implementation=_attn_implementation,
**kwargs,
)
self.qk_norm = qk_norm
self.layer_module = layer_module
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):
return len(self.key_cache)
@property
def seq_lens(self):
if self.key_cache[0] is not None:
return self.key_cache[0].shape[0]
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):
packed_query_sequence: torch.FloatTensor = None
past_key_values: NaiveCache | None = None
class PackedAttentionMoT(nn.Module):
"""Packed attention with Mixture-of-Tokens routing for understanding/generation.
Uses MoTQKVParallelLinear and MoTRowParallelLinear for tensor parallelism.
Text and vae weights are held within the same MoT layer (text on self,
vae on self.gen_exp). Token routing is driven by text_indices / vae_indices.
"""
def __init__(
self,
config,
layer_idx: int | None = None,
parallel_config: DiffusionParallelConfig | None = None,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__()
self.layer_idx = layer_idx
self.hidden_size = config.hidden_size
self.parallel_config = parallel_config
tp_size = get_tensor_model_parallel_world_size()
self.total_num_heads = config.num_attention_heads
assert self.total_num_heads % tp_size == 0
self.num_heads = self.total_num_heads // tp_size
self.total_num_kv_heads = config.num_key_value_heads
if self.total_num_kv_heads >= tp_size:
assert self.total_num_kv_heads % tp_size == 0
else:
assert tp_size % self.total_num_kv_heads == 0
self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
self.head_dim = self.hidden_size // self.total_num_heads
self.q_size = self.num_heads * self.head_dim
self.kv_size = self.num_kv_heads * self.head_dim
self.qkv_proj = MoTQKVParallelLinear(
self.hidden_size,
self.head_dim,
self.total_num_heads,
self.total_num_kv_heads,
bias=True,
vae_bias=True,
quant_config=quant_config,
prefix=f"{prefix}.qkv_proj",
)
self.o_proj = MoTRowParallelLinear(
input_size=self.total_num_heads * self.head_dim,
output_size=self.hidden_size,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.o_proj",
)
self.q_norm = MoTRMSNorm(self.head_dim, head_norm=True, eps=config.rms_norm_eps)
self.k_norm = MoTRMSNorm(self.head_dim, head_norm=True, eps=config.rms_norm_eps)
self.rotary_op = RotaryEmbedding(is_neox_style=True)
self.attn_causal = DiffusionAttention(
num_heads=self.total_num_heads,
head_size=self.head_dim,
softmax_scale=1.0 / (self.head_dim**0.5),
causal=True,
num_kv_heads=self.total_num_kv_heads,
)
self.attn_noncausal = DiffusionAttention(
num_heads=self.total_num_heads,
head_size=self.head_dim,
softmax_scale=1.0 / (self.head_dim**0.5),
causal=False,
num_kv_heads=self.total_num_kv_heads,
)
def _is_sp_active(self) -> bool:
"""Check if SP is active for this attention layer."""
if not is_forward_context_available():
return False
return get_forward_context().sp_active
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,
packed_text_indexes: torch.Tensor,
update_past_key_values: bool = False,
) -> tuple[torch.Tensor, NaiveCache | None]:
"""Forward pass for generation mode.
This path does the following:
1. Apply qkv projection to the text seq & vae seqs
2. Reshape both to 3D & apply RMS norms
3. Apply RoPE to text / VAE components independently
4. Create the full K/V; Bagel currently manages its own KV cache
(NaiveCache) independently since it is a diffusion model
5. Apply non-causal attention, while taking sequence parallelism into account
6. Apply output projections on the split parts
7. Merge back into the packed format
8. Update the NaiveCache.
TODO (Alex): it would be best to remove packing from Bagel to simplify the code.
Currently we shouldn't need it in the model, and it would be ideal to handle
packing/batching etc in a more model agnostic way.
"""
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.
qkv, _ = self.qkv_proj(packed_query_sequence, text_indices, vae_indices)
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
# Reshape to (tokens, heads, head_dim)
q = q.view(-1, self.num_heads, self.head_dim)
k = k.view(-1, self.num_kv_heads, self.head_dim)
v = v.view(-1, self.num_kv_heads, self.head_dim)
# MoT QK norms route text/vae tokens to weight/gen_weight internally.
q = self.q_norm(q.to(torch.float32), text_indices, vae_indices)
k = self.k_norm(k.to(torch.float32), text_indices, vae_indices)
cos, sin = [x[..., : self.head_dim // 2] for x in packed_query_position_embeddings]
q = self.rotary_op(q.to(cos.dtype).unsqueeze(0), cos, sin).squeeze(0)
k = self.rotary_op(k.to(cos.dtype).unsqueeze(0), cos, sin).squeeze(0)
q = q.to(torch.bfloat16)
k = k.to(torch.bfloat16)
v = v.to(torch.bfloat16)
text_q = q[text_indices]
text_k = k[text_indices]
text_v = v[text_indices]
vae_q = q[vae_indices]
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]
cache_v = past_key_values.value_cache[self.layer_idx]
ctx_k = torch.cat([cache_k, text_k], dim=0)
ctx_v = torch.cat([cache_v, text_v], dim=0)
else:
ctx_k = text_k
ctx_v = text_v
# NOTE: we reshape to batched (1, S, H, D) for diffusion Attention
# attn_out should be: (1, text_len + local_vae_len, H, D)
if self._is_sp_active():
# Joint mechanism keeps text+cache replicated across SP ranks
attn_out = self.attn_noncausal(
vae_q.unsqueeze(0),
vae_k.unsqueeze(0),
vae_v.unsqueeze(0),
DiffusionAttentionMetadata(
joint_query=text_q.unsqueeze(0),
joint_key=ctx_k.unsqueeze(0),
joint_value=ctx_v.unsqueeze(0),
joint_strategy="front",
),
)
else:
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)
# 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)
local_text_idx = torch.arange(text_len, device=local_packed.device)
local_vae_idx = torch.arange(text_len, text_len + vae_attn.shape[0], device=local_packed.device)
local_out, _ = self.o_proj(local_packed, local_text_idx, local_vae_idx)
text_out = local_out[:text_len]
vae_out = local_out[text_len:]
# Merge back into packed format
total_len = packed_query_sequence.shape[0]
full_output = text_out.new_zeros((total_len, self.hidden_size))
full_output[packed_text_indexes] = text_out
full_output[packed_vae_token_indexes] = vae_out
if update_past_key_values:
new_k = torch.cat([ctx_k, vae_k], dim=0)
new_v = torch.cat([ctx_v, vae_v], dim=0)
past_key_values.key_cache[self.layer_idx] = new_k
past_key_values.value_cache[self.layer_idx] = new_v
return full_output, past_key_values
def _forward_und(
self,
packed_query_sequence: torch.Tensor,
packed_query_position_embeddings: torch.Tensor,
past_key_values: NaiveCache | None,
is_causal: bool,
update_past_key_values: bool = True,
) -> tuple[torch.Tensor, NaiveCache | None]:
"""Forward pass for understanding mode.
This path does the following (not hard to read):
1. Apply qkv projection to the text seq
2. Reshape to 3D & apply RMS norms
3. Apply RoPE
4. Create the full K/V; Bagel currently manages its own KV cache
(NaiveCache) independently since it is a diffusion model
5. Apply attention based on causality kwarg
6. Apply output projection
7. Update the NaiveCache.
"""
qkv, _ = self.qkv_proj(packed_query_sequence)
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
q = q.view(-1, self.num_heads, self.head_dim)
k = k.view(-1, self.num_kv_heads, self.head_dim)
v = v.view(-1, self.num_kv_heads, self.head_dim)
# Pre-merge code cast to float32 before q_norm/k_norm — bf16
# RMSNorm accumulation drift compounds across segmented prefill
# (x2t builds the cache via 3+ sequential forward_cache_update_*
# calls), producing degenerate token repetition in generate_text.
q = self.q_norm(q.to(torch.float32))
k = self.k_norm(k.to(torch.float32))
cos, sin = [x[..., : self.head_dim // 2] for x in packed_query_position_embeddings]
q = self.rotary_op(q.to(cos.dtype).unsqueeze(0), cos, sin).squeeze(0)
k = self.rotary_op(k.to(cos.dtype).unsqueeze(0), cos, sin).squeeze(0)
q = q.to(torch.bfloat16)
k = k.to(torch.bfloat16)
v = v.to(torch.bfloat16)
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]
cache_v = past_key_values.value_cache[self.layer_idx]
full_k = torch.cat([cache_k, k], dim=0)
full_v = torch.cat([cache_v, v], dim=0)
cache_len = cache_k.shape[0]
else:
full_k = k
full_v = v
cache_len = 0
if is_causal and cache_len > 0:
# PyTorch SDPA's ``is_causal=True`` with ``Q != K`` uses
# ``tril(diagonal=0)`` — top-left aligned, so for Q=1 against a
# long cache only ``q_0 -> k_0`` is unmasked (degenerate
# generate_text output). Build the correct bottom-right
# aligned mask manually: ``mask[i, j] = -inf if j > cache_len
# + i`` so each new query attends to all of ``cache + self
# up to i``. Bypass ``DiffusionAttention`` entirely — its
# ``_maybe_reshape_attn_mask`` helper only handles 2-D
# ``(B, K)`` shape, not ``(Q, K)``, so the reshape path
# silently drops the per-query mask for Q>1.
Q_len = q.shape[0]
K_len = full_k.shape[0]
arange_q = torch.arange(Q_len, device=q.device).unsqueeze(1)
arange_k = torch.arange(K_len, device=q.device).unsqueeze(0)
mask = arange_k > (cache_len + arange_q) # (Q, K) bool
attn_bias = torch.zeros(Q_len, K_len, dtype=q.dtype, device=q.device)
attn_bias.masked_fill_(mask, float("-inf"))
# Permute to (B=1, H, S, D) for SDPA.
q_4d = q.unsqueeze(0).permute(0, 2, 1, 3)
k_4d = full_k.unsqueeze(0).permute(0, 2, 1, 3)
v_4d = full_v.unsqueeze(0).permute(0, 2, 1, 3)
attn_out_4d = torch.nn.functional.scaled_dot_product_attention(
q_4d,
k_4d,
v_4d,
attn_mask=attn_bias.unsqueeze(0).unsqueeze(0), # (1, 1, Q, K)
dropout_p=0.0,
is_causal=False,
scale=1.0 / (self.head_dim**0.5),
enable_gqa=(self.num_heads != self.num_kv_heads),
)
attn_out = attn_out_4d.permute(0, 2, 1, 3)
else:
attn = self.attn_causal if is_causal else self.attn_noncausal
attn_out = attn(
q.unsqueeze(0),
full_k.unsqueeze(0),
full_v.unsqueeze(0),
)
attn_out = attn_out.squeeze(0).reshape(-1, self.q_size)
attn_out, _ = self.o_proj(attn_out)
if update_past_key_values:
past_key_values.key_cache[self.layer_idx] = full_k
past_key_values.value_cache[self.layer_idx] = full_v
return attn_out, past_key_values
def forward(
self,
packed_query_sequence: torch.Tensor,
query_lens: torch.Tensor,
packed_query_position_embeddings: torch.Tensor,
past_key_values: NaiveCache | None = None,
update_past_key_values=True,
is_causal=True,
mode="und",
packed_vae_token_indexes=None,
packed_text_indexes=None,
):
if mode == "gen":
if is_causal:
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,
packed_text_indexes=packed_text_indexes,
update_past_key_values=update_past_key_values,
)
return self._forward_und(
packed_query_sequence=packed_query_sequence,
packed_query_position_embeddings=packed_query_position_embeddings,
past_key_values=past_key_values,
is_causal=is_causal,
update_past_key_values=update_past_key_values,
)
class Qwen2MoTDecoderLayer(nn.Module):
def __init__(
self,
config,
layer_idx: int | None = None,
attn_module: type[nn.Module] | None = PackedAttentionMoT,
parallel_config: DiffusionParallelConfig | None = None,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__()
self.layer_idx = layer_idx
self.hidden_size = config.hidden_size
self.self_attn = attn_module(
config, layer_idx, parallel_config=parallel_config, quant_config=quant_config, prefix=f"{prefix}.self_attn"
)
self.input_layernorm = MoTRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.mlp = BagelMLP(
config.hidden_size,
config.intermediate_size,
config.hidden_act,
quant_config=quant_config,
prefix=f"{prefix}.mlp",
)
self.mlp_moe_gen = BagelMLP(
config.hidden_size,
config.intermediate_size,
config.hidden_act,
quant_config=quant_config,
prefix=f"{prefix}.mlp_moe_gen",
)
self.post_attention_layernorm = MoTRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor | None = None,
packed_query_sequence: torch.Tensor | None = None,
query_lens: torch.Tensor = None,
packed_query_position_embeddings: torch.Tensor = None,
past_key_values: NaiveCache | None = None,
update_past_key_values=True,
is_causal=True,
mode="und",
packed_vae_token_indexes=None,
packed_text_indexes=None,
) -> BaseNavitOutputWithPast:
if packed_query_sequence is None:
packed_query_sequence = hidden_states
text_indices = packed_text_indexes if mode == "gen" else None
vae_indices = packed_vae_token_indexes if mode == "gen" else None
residual = packed_query_sequence
packed_query_sequence = self.input_layernorm(packed_query_sequence, text_indices, vae_indices)
# Self Attention
packed_query_sequence, past_key_values = self.self_attn(
packed_query_sequence=packed_query_sequence,
query_lens=query_lens,
packed_query_position_embeddings=packed_query_position_embeddings,
past_key_values=past_key_values,
update_past_key_values=update_past_key_values,
is_causal=is_causal,
mode=mode,
packed_vae_token_indexes=packed_vae_token_indexes,
packed_text_indexes=packed_text_indexes,
)
packed_query_sequence = residual + packed_query_sequence
# Fully Connected
residual = packed_query_sequence
if mode == "und":
packed_query_sequence = self.post_attention_layernorm(packed_query_sequence)
packed_query_sequence = self.mlp(packed_query_sequence)
elif mode == "gen":
packed_normed = self.post_attention_layernorm(packed_query_sequence, text_indices, vae_indices).to(
torch.bfloat16
)
packed_text_query_sequence = packed_normed[packed_text_indexes]
packed_vae_query_sequence = packed_normed[packed_vae_token_indexes]
packed_query_sequence_ = torch.zeros_like(packed_query_sequence).to(torch.bfloat16)
packed_query_sequence_[packed_text_indexes] = self.mlp(packed_text_query_sequence)
packed_query_sequence_[packed_vae_token_indexes] = self.mlp_moe_gen(packed_vae_query_sequence)
packed_query_sequence = packed_query_sequence_
packed_query_sequence = residual + packed_query_sequence
return packed_query_sequence, past_key_values
class Qwen2MoTModel(Qwen2PreTrainedModel):
_cache_dit_adapter_config = CacheDiTAdapterConfig(
block_forward_patterns={
"layers": ForwardPattern.Pattern_0,
},
cached_adapter_cls=BagelCachedAdapter,
)
_layerwise_offload_blocks_attrs = ["layers"]
@staticmethod
def _is_transformer_block(name: str, module) -> bool:
return "layers" in name and name.split(".")[-1].isdigit()
_hsdp_shard_conditions = [_is_transformer_block]
def __init__(
self,
config,
parallel_config: DiffusionParallelConfig | None = None,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__(config)
self.padding_idx = config.pad_token_id
self.vocab_size = config.vocab_size
self.use_moe = "Mo" in config.layer_module
self.embed_tokens = VocabParallelEmbedding(config.vocab_size, config.hidden_size)
self.layers = nn.ModuleList(
[
Qwen2MoTDecoderLayer(
config,
layer_idx,
attn_module=PackedAttentionMoT,
parallel_config=parallel_config,
quant_config=quant_config,
prefix=f"{prefix}.layers.{layer_idx}",
)
for layer_idx in range(config.num_hidden_layers)
]
)
self.norm = MoTRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.rotary_emb = BagelRotaryEmbedding(config=config)
# Initialize weights and apply final processing
self.post_init()
def forward(
self,
packed_query_sequence: torch.Tensor | None = None,
query_lens: torch.Tensor | None = None,
packed_query_position_ids: torch.Tensor | None = None,
past_key_values: NaiveCache | None = None,
update_past_key_values=True,
is_causal=True,
mode="und",
packed_vae_token_indexes=None,
packed_text_indexes=None,
packed_text_ids: torch.Tensor | None = None,
return_embeddings_only: bool = False,
) -> BaseNavitOutputWithPast:
if packed_query_sequence is None:
if packed_text_ids is None:
raise ValueError("Either packed_query_sequence or packed_text_ids must be provided.")
packed_query_sequence = self.embed_tokens(packed_text_ids)
if return_embeddings_only:
return BaseNavitOutputWithPast(
packed_query_sequence=packed_query_sequence,
past_key_values=past_key_values,
)
# create position embeddings to be shared across the decoder layers
cos, sin = self.rotary_emb(packed_query_sequence, packed_query_position_ids.unsqueeze(0))
cos = cos.squeeze(0)
sin = sin.squeeze(0)
packed_query_position_embeddings = (cos, sin)
extra_inputs = {}
if self.use_moe:
extra_inputs.update(mode=mode)
if mode == "gen":
assert packed_vae_token_indexes is not None
assert packed_text_indexes is not None
extra_inputs.update(
packed_vae_token_indexes=packed_vae_token_indexes,
packed_text_indexes=packed_text_indexes,
)
for layer_idx, decoder_layer in enumerate(self.layers):
# TODO (Alex): Remove encoder_hidden_states as a kwarg; currently we keep it
# for compatibility with the current custom CacheDiT adapter, as we need to be
# careful to not break the NaiveCache handling when switching from pattern
# 0 -> 4.
packed_query_sequence, past_key_values = decoder_layer(
hidden_states=packed_query_sequence,
encoder_hidden_states=None,
query_lens=query_lens,
packed_query_position_embeddings=packed_query_position_embeddings,
past_key_values=past_key_values,
update_past_key_values=update_past_key_values,
is_causal=is_causal,
**extra_inputs,
)
text_indices = packed_text_indexes if self.use_moe and mode == "gen" else None
vae_indices = packed_vae_token_indexes if self.use_moe and mode == "gen" else None
packed_query_sequence = self.norm(packed_query_sequence, text_indices, vae_indices)
return BaseNavitOutputWithPast(
packed_query_sequence=packed_query_sequence,
past_key_values=past_key_values,
)
class Qwen2MoTForCausalLM(Qwen2PreTrainedModel):
_tied_weights_keys = ["lm_head.weight"]
def __init__(
self,
config,
parallel_config: DiffusionParallelConfig | None = None,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__(config)
self.model = Qwen2MoTModel(
config, parallel_config=parallel_config, quant_config=quant_config, prefix=f"{prefix}.model"
)
self.vocab_size = config.vocab_size
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
# Initialize weights and apply final processing
self.post_init()
def get_input_embeddings(self):
return self.model.embed_tokens
def set_input_embeddings(self, value):
self.model.embed_tokens = value
def get_output_embeddings(self):
return self.lm_head
def set_output_embeddings(self, new_embeddings):
self.lm_head = new_embeddings
def set_decoder(self, decoder):
self.model = decoder
def get_decoder(self):
return self.model
def forward(
self,
packed_query_sequence: torch.Tensor | None = None,
query_lens: torch.Tensor | None = None,
packed_query_position_ids: torch.Tensor | None = None,
past_key_values: NaiveCache | None = None,
update_past_key_values=True,
is_causal=True,
mode="und",
packed_vae_token_indexes=None,
packed_text_indexes=None,
packed_text_ids: torch.Tensor | None = None,
return_embeddings_only: bool = False,
) -> BaseNavitOutputWithPast:
outputs = self.model(
packed_query_sequence=packed_query_sequence,
query_lens=query_lens,
packed_query_position_ids=packed_query_position_ids,
past_key_values=past_key_values,
update_past_key_values=update_past_key_values,
is_causal=is_causal,
mode=mode,
packed_vae_token_indexes=packed_vae_token_indexes,
packed_text_indexes=packed_text_indexes,
packed_text_ids=packed_text_ids,
return_embeddings_only=return_embeddings_only,
)
return outputs
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
"""Load weights for MoT parallel layers.
Stacked parameter remapping (checkpoint name → model parameter):
- q/k/v_proj → qkv_proj (text, shard q/k/v)
- q/k/v_proj_moe_gen → qkv_proj.gen_exp (gen, shard q/k/v)
Direct remapping (no shard dimension):
- o_proj_moe_gen → o_proj.gen_exp
- {norm}_moe_gen.weight → {norm}.gen_weight (all MoTRMSNorm layers)
Text norm weights (input_layernorm.weight, q_norm.weight, etc.) and
other names (embed_tokens, lm_head) pass through unchanged.
"""
stacked_params_mapping = [
# (param_name, weight_name, shard_id)
# _moe_gen patterns MUST come first — `.q_proj` is a substring
# of `.q_proj_moe_gen`, so the more specific pattern must match first.
(".qkv_proj.gen_exp", ".q_proj_moe_gen", "q"),
(".qkv_proj.gen_exp", ".k_proj_moe_gen", "k"),
(".qkv_proj.gen_exp", ".v_proj_moe_gen", "v"),
(".qkv_proj", ".q_proj", "q"),
(".qkv_proj", ".k_proj", "k"),
(".qkv_proj", ".v_proj", "v"),
(".mlp_moe_gen.gate_up_proj", ".mlp_moe_gen.gate_proj", 0),
(".mlp_moe_gen.gate_up_proj", ".mlp_moe_gen.up_proj", 1),
(".mlp.gate_up_proj", ".mlp.gate_proj", 0),
(".mlp.gate_up_proj", ".mlp.up_proj", 1),
]
direct_remap = [
(".o_proj_moe_gen.", ".o_proj.gen_exp."),
# Norm _moe_gen.weight → {norm_name}.gen_weight
(".input_layernorm_moe_gen.", ".input_layernorm.gen_"),
(".post_attention_layernorm_moe_gen.", ".post_attention_layernorm.gen_"),
(".q_norm_moe_gen.", ".q_norm.gen_"),
(".k_norm_moe_gen.", ".k_norm.gen_"),
(".norm_moe_gen.", ".norm.gen_"),
]
params_dict = dict(self.named_parameters())
loaded_params: set[str] = set()
def handle_weight(name, loaded_weight, shard_id=None):
param = params_dict.get(name)
if param is None:
logger.warning_once("Skipping weight %r: no matching parameter found in model.", name)
return
weight_loader = getattr(param, "weight_loader", default_weight_loader)
if shard_id is not None:
weight_loader(param, loaded_weight, shard_id)
else:
weight_loader(param, loaded_weight)
loaded_params.add(name)
for name, loaded_weight in weights:
# match direct remap
handled = False
for old_substr, new_substr in direct_remap:
if old_substr in name:
name = name.replace(old_substr, new_substr)
handle_weight(name, loaded_weight)
handled = True
break
if handled:
continue
# match stacked params mapping
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name:
continue
name = name.replace(weight_name, param_name)
handle_weight(name, loaded_weight, shard_id)
handled = True
break
if handled:
continue
# no-name-match cases are handled here
handle_weight(name, loaded_weight)
return loaded_params
def get_2d_sincos_pos_embed(embed_dim, grid_size, cls_token=False, extra_tokens=0):
grid_h = np.arange(grid_size, dtype=np.float32)
grid_w = np.arange(grid_size, dtype=np.float32)
grid = np.meshgrid(grid_w, grid_h) # here w goes first
grid = np.stack(grid, axis=0)
grid = grid.reshape([2, 1, grid_size, grid_size])
pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid)
if cls_token and extra_tokens > 0:
pos_embed = np.concatenate([np.zeros([extra_tokens, embed_dim]), pos_embed], axis=0)
return pos_embed
def get_2d_sincos_pos_embed_from_grid(embed_dim, grid):
assert embed_dim % 2 == 0
# use half of dimensions to encode grid_h
emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[0]) # (H*W, D/2)
emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[1]) # (H*W, D/2)
emb = np.concatenate([emb_h, emb_w], axis=1) # (H*W, D)
return emb
def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
"""
embed_dim: output dimension for each position
pos: a list of positions to be encoded: size (M,)
out: (M, D)
"""
assert embed_dim % 2 == 0
omega = np.arange(embed_dim // 2, dtype=np.float64)
omega /= embed_dim / 2.0
omega = 1.0 / 10000**omega # (D/2,)
pos = pos.reshape(-1) # (M,)
out = np.einsum("m,d->md", pos, omega) # (M, D/2), outer product
emb_sin = np.sin(out) # (M, D/2)
emb_cos = np.cos(out) # (M, D/2)
emb = np.concatenate([emb_sin, emb_cos], axis=1) # (M, D)
return emb
class TimestepEmbedder(nn.Module):
"""
Embeds scalar timesteps into vector representations.
"""
def __init__(self, hidden_size, frequency_embedding_size=256):
super().__init__()
self.mlp = nn.Sequential(
nn.Linear(frequency_embedding_size, hidden_size, bias=True),
nn.SiLU(),
nn.Linear(hidden_size, hidden_size, bias=True),
)
self.frequency_embedding_size = frequency_embedding_size
def forward(self, t):
t_freq = timestep_embedding(t, self.frequency_embedding_size)
t_emb = self.mlp(t_freq)
return t_emb
class PositionEmbedding(nn.Module):
def __init__(self, max_num_patch_per_side, hidden_size):
super().__init__()
self.max_num_patch_per_side = max_num_patch_per_side
self.hidden_size = hidden_size
self.pos_embed = nn.Parameter(torch.zeros(max_num_patch_per_side**2, hidden_size), requires_grad=False)
self._init_weights()
def _init_weights(self):
# Initialize (and freeze) pos_embed by sin-cos embedding:
pos_embed = get_2d_sincos_pos_embed(self.hidden_size, self.max_num_patch_per_side)
self.pos_embed.data.copy_(torch.from_numpy(pos_embed).float())
def forward(self, position_ids):
return self.pos_embed[position_ids]
def get_flattened_position_ids_extrapolate(img_h, img_w, patch_size, max_num_patches_per_side):
num_patches_h, num_patches_w = img_h // patch_size, img_w // patch_size
coords_h = torch.arange(0, num_patches_h)
coords_w = torch.arange(0, num_patches_w)
pos_ids = (coords_h[:, None] * max_num_patches_per_side + coords_w).flatten()
return pos_ids
class Bagel(CFGParallelMixin, nn.Module):
config_class = BagelConfig
base_model_prefix = "bagel"
# Flow-matching denoise schedule convention. Official BAGEL samples
# ``num_timesteps`` points over [1, 0] and drops the terminal t=0, yielding
# ``num_timesteps - 1`` Euler steps. Lance samples one extra point
# (``num_timesteps + 1``) for ``num_timesteps`` steps; ``LanceBagel`` flips
# this on. See https://github.com/vllm-project/vllm-omni/issues/4470.
_denoise_schedule_extra_step: bool = False
def __init__(
self,
language_model,
vit_model,
config: BagelConfig,
parallel_config: DiffusionParallelConfig | None = None,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__()
self.language_model = language_model
self.hidden_size = config.llm_config.hidden_size
self.use_moe = "Mo" in config.llm_config.layer_module
self.num_heads = config.llm_config.num_attention_heads
self.parallel_config = parallel_config
if config.visual_gen:
self.latent_patch_size = config.latent_patch_size
self.timestep_shift = config.timestep_shift
self.latent_downsample = config.vae_config.downsample * config.latent_patch_size
self.max_latent_size = config.max_latent_size
self.latent_channel = config.vae_config.z_channels
self.patch_latent_dim = self.latent_patch_size**2 * self.latent_channel
self.time_embedder = TimestepEmbedder(self.hidden_size)
self.vae2llm = nn.Linear(self.patch_latent_dim, self.hidden_size)
self.llm2vae = nn.Linear(self.hidden_size, self.patch_latent_dim)
self.latent_pos_embed = PositionEmbedding(self.max_latent_size, self.hidden_size)
if config.visual_und:
self.vit_model = vit_model
self.vit_patch_size = config.vit_config.patch_size
self.vit_max_num_patch_per_side = config.vit_max_num_patch_per_side
self.vit_hidden_size = config.vit_config.hidden_size
self.connector = MLPconnector(
self.vit_hidden_size,
self.hidden_size,
config.connector_act,
quant_config=quant_config,
prefix=f"{prefix}.connector",
)
self.vit_pos_embed = PositionEmbedding(self.vit_max_num_patch_per_side, self.hidden_size)
self.get_flattened_position_ids = get_flattened_position_ids_extrapolate
self.config = config
self._init_weights()
@property
def _sp_size(self) -> int:
if self.parallel_config is None:
return 1
sp = self.parallel_config.sequence_parallel_size
return sp if sp is not None and sp > 1 else 1
def _split_vae_for_sp(
self,
x_t: torch.Tensor,
packed_vae_position_ids: torch.Tensor,
packed_vae_token_indexes: torch.Tensor,
packed_text_indexes: torch.Tensor,
packed_seqlens: torch.Tensor,
packed_position_ids: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
"""Split VAE tokens across SP ranks for the denoising loop.
Returns adjusted (x_t, packed_vae_position_ids, packed_vae_token_indexes,
packed_text_indexes, packed_seqlens, packed_position_ids) for the local rank.
"""
sp_size = self._sp_size
sp_rank = get_sequence_parallel_rank()
num_vae = x_t.shape[0]
assert num_vae % sp_size == 0, f"VAE token count {num_vae} not divisible by SP size {sp_size}"
chunk = num_vae // sp_size
start = sp_rank * chunk
end = start + chunk
local_x_t = x_t[start:end]
local_vae_pos_ids = packed_vae_position_ids[start:end]
# Rebuild local packed indices:
# packed sequence = [start_of_image, local_vae_tokens..., end_of_image]
# BAGEL always has exactly 2 text markers (start/end_of_image).
num_text = packed_text_indexes.shape[0]
assert num_text == 2, f"Expected exactly 2 text markers (start/end_of_image), got {num_text}"
assert packed_seqlens.numel() == 1, (
f"SP currently supports single-image batches only, got {packed_seqlens.numel()} sequences"
)
local_vae_len = chunk
local_total = num_text + local_vae_len
local_text_indexes = torch.tensor([0, local_vae_len + 1], device=packed_text_indexes.device)
local_vae_indexes = torch.arange(1, 1 + local_vae_len, device=packed_vae_token_indexes.device)
local_seqlens = torch.tensor([local_total], device=packed_seqlens.device, dtype=packed_seqlens.dtype)
# Build local position IDs preserving global positions.
# Text markers keep their original positions; VAE tokens get
# the global positions for the local chunk.
text_pos_ids = packed_position_ids[packed_text_indexes]
vae_pos_ids_full = packed_position_ids[packed_vae_token_indexes]
local_vae_pos = vae_pos_ids_full[start:end]
local_position_ids = torch.zeros(
local_total, device=packed_position_ids.device, dtype=packed_position_ids.dtype
)
local_position_ids[local_text_indexes] = text_pos_ids
local_position_ids[local_vae_indexes] = local_vae_pos
return local_x_t, local_vae_pos_ids, local_vae_indexes, local_text_indexes, local_seqlens, local_position_ids
def _gather_vae_for_sp(self, local_v_t: torch.Tensor) -> torch.Tensor:
"""Gather VAE velocity outputs from all SP ranks."""
sp_size = self._sp_size
gathered = [torch.zeros_like(local_v_t) for _ in range(sp_size)]
sp_group = get_sp_group()
dist.all_gather(gathered, local_v_t.contiguous(), group=sp_group.device_group)
return torch.cat(gathered, dim=0)
def _init_weights(self):
if self.config.visual_gen:
nn.init.constant_(self.llm2vae.weight, 0)
nn.init.constant_(self.llm2vae.bias, 0)
def prepare_prompts(self, curr_kvlens, curr_rope, prompts, tokenizer, new_token_ids):
packed_text_ids = list()
packed_text_position_ids = list()
text_token_lens = list()
newlens, new_rope = list(), list()
for prompt, curr_kvlen, curr_position_id in zip(prompts, curr_kvlens, curr_rope):
text_ids = tokenizer.encode(prompt, add_special_tokens=False)
text_ids = [new_token_ids["bos_token_id"]] + text_ids + [new_token_ids["eos_token_id"]]
text_token_lens.append(len(text_ids))
packed_text_ids.extend(text_ids)
packed_text_position_ids.extend(range(curr_position_id, curr_position_id + len(text_ids)))
newlens.append(curr_kvlen + len(text_ids))
new_rope.append(curr_position_id + len(text_ids))
generation_input = {
"text_token_lens": torch.tensor(text_token_lens, dtype=torch.int),
"packed_text_ids": torch.tensor(packed_text_ids, dtype=torch.long),
"packed_text_position_ids": torch.tensor(packed_text_position_ids, dtype=torch.long),
}
return generation_input, newlens, new_rope
def forward_cache_update_text(
self,
past_key_values: NaiveCache,
packed_text_ids: torch.IntTensor,
packed_text_position_ids: torch.LongTensor,
text_token_lens: torch.LongTensor,
):
extra_inputs = {}
if self.use_moe:
extra_inputs = {"mode": "und"}
output = self.language_model.forward(
packed_text_ids=packed_text_ids,
query_lens=text_token_lens,
packed_query_position_ids=packed_text_position_ids,
past_key_values=past_key_values,
update_past_key_values=True,
is_causal=True,
**extra_inputs,
)
past_key_values = output.past_key_values
return past_key_values
def prepare_vae_images(self, curr_kvlens, curr_rope, images, transforms, new_token_ids, timestep=0):
patchified_vae_latent_shapes, packed_vae_position_ids = list(), list()
packed_vae_token_indexes = list()
packed_text_ids, packed_text_indexes = list(), list()
packed_seqlens, packed_position_ids = list(), list()
_curr = 0
vae_image_tensors = list()
newlens, new_rope = list(), list()
for image, curr_kvlen, curr_position_id in zip(images, curr_kvlens, curr_rope):
packed_text_ids.append(new_token_ids["start_of_image"])
packed_text_indexes.append(_curr)
_curr += 1
image_tensor = transforms(image)
vae_image_tensors.append(image_tensor)
vae_position_ids = self.get_flattened_position_ids(
image_tensor.size(1),
image_tensor.size(2),
self.latent_downsample,
max_num_patches_per_side=self.max_latent_size,
)
packed_vae_position_ids.append(vae_position_ids)
H, W = image_tensor.shape[1:]
h = H // self.latent_downsample
w = W // self.latent_downsample
patchified_vae_latent_shapes.append((h, w))
num_img_tokens = w * h
packed_vae_token_indexes.extend(range(_curr, _curr + num_img_tokens))
_curr += num_img_tokens
packed_text_ids.append(new_token_ids["end_of_image"])
packed_text_indexes.append(_curr)
_curr += 1
packed_position_ids.extend([curr_position_id] * (num_img_tokens + 2))
packed_seqlens.append(num_img_tokens + 2)
newlens.append(curr_kvlen + num_img_tokens + 2)
new_rope.append(curr_position_id + 1)
image_sizes = [item.shape for item in vae_image_tensors]
max_image_size = [max(item) for item in list(zip(*image_sizes))]
padded_images = torch.zeros(size=(len(vae_image_tensors), *max_image_size))
for i, image_tensor in enumerate(vae_image_tensors):
padded_images[i, :, : image_tensor.shape[1], : image_tensor.shape[2]] = image_tensor
generation_input = {
"padded_images": padded_images,
"patchified_vae_latent_shapes": patchified_vae_latent_shapes,
"packed_vae_position_ids": torch.cat(packed_vae_position_ids, dim=0),
"packed_timesteps": torch.tensor([timestep]),
"packed_vae_token_indexes": torch.tensor(packed_vae_token_indexes, dtype=torch.long),
"packed_text_ids": torch.tensor(packed_text_ids, dtype=torch.long),
"packed_text_indexes": torch.tensor(packed_text_indexes, dtype=torch.long),
"packed_position_ids": torch.tensor(packed_position_ids, dtype=torch.long),
"packed_seqlens": torch.tensor(packed_seqlens, dtype=torch.int),
}
return generation_input, newlens, new_rope
def forward_cache_update_vae(
self,
vae_model,
past_key_values: NaiveCache,
padded_images: torch.Tensor,
patchified_vae_latent_shapes: list,
packed_vae_position_ids: torch.LongTensor,
packed_timesteps: torch.Tensor,
packed_vae_token_indexes: torch.LongTensor,
packed_text_ids: torch.LongTensor,
packed_text_indexes: torch.LongTensor,
packed_position_ids: torch.LongTensor,
packed_seqlens: torch.IntTensor,
):
padded_latent = vae_model.encode(padded_images)
p = self.latent_patch_size
packed_latent = list()
for latent, (h, w) in zip(padded_latent, patchified_vae_latent_shapes):
latent = latent[:, : h * p, : w * p].reshape(self.latent_channel, h, p, w, p)
latent = torch.einsum("chpwq->hwpqc", latent).reshape(-1, p * p * self.latent_channel)
packed_latent.append(latent)
packed_latent = torch.cat(packed_latent, dim=0)
packed_pos_embed = self.latent_pos_embed(packed_vae_position_ids)
packed_timestep_embeds = self.time_embedder(packed_timesteps)
packed_latent = self.vae2llm(packed_latent) + packed_timestep_embeds + packed_pos_embed
# Mirror forward_cache_update_vit: build a full packed_sequence so MoE gen-mode
# indexes (packed_text_indexes / packed_vae_token_indexes) match tensor length.
packed_text_embedding = self.language_model.forward(
packed_text_ids=packed_text_ids,
return_embeddings_only=True,
).packed_query_sequence
packed_sequence = packed_text_embedding.new_zeros((sum(packed_seqlens), self.hidden_size))
packed_sequence[packed_text_indexes] = packed_text_embedding
if packed_latent.dtype != packed_sequence.dtype:
packed_latent = packed_latent.to(packed_sequence.dtype)
packed_sequence[packed_vae_token_indexes] = packed_latent
extra_inputs = {}
if self.use_moe:
extra_inputs = {
"mode": "gen",
"packed_vae_token_indexes": packed_vae_token_indexes,
"packed_text_indexes": packed_text_indexes,
}
output = self.language_model.forward(
packed_query_sequence=packed_sequence,
query_lens=packed_seqlens,
packed_query_position_ids=packed_position_ids,
past_key_values=past_key_values,
update_past_key_values=True,
is_causal=False,
**extra_inputs,
)
past_key_values = output.past_key_values
return past_key_values
def prepare_vit_images(self, curr_kvlens, curr_rope, images, transforms, new_token_ids):
packed_vit_token_indexes = list()
vit_token_seqlens, packed_vit_tokens, packed_vit_position_ids = list(), list(), list()
packed_text_ids, packed_text_indexes = list(), list()
packed_seqlens, packed_position_ids = list(), list()
_curr = 0
newlens, new_rope = list(), list()
for image, curr_kvlen, curr_position_id in zip(images, curr_kvlens, curr_rope):
packed_text_ids.append(new_token_ids["start_of_image"])
packed_text_indexes.append(_curr)
_curr += 1
image_tensor = transforms(image)
vit_position_ids = self.get_flattened_position_ids(
image_tensor.size(1),
image_tensor.size(2),
self.vit_patch_size,
max_num_patches_per_side=self.vit_max_num_patch_per_side,
)
vit_tokens = patchify(image_tensor, self.vit_patch_size)
packed_vit_tokens.append(vit_tokens)
num_img_tokens = vit_tokens.shape[0]
packed_vit_position_ids.append(vit_position_ids)
vit_token_seqlens.append(num_img_tokens)
packed_vit_token_indexes.extend(range(_curr, _curr + num_img_tokens))
_curr += num_img_tokens
packed_text_ids.append(new_token_ids["end_of_image"])
packed_text_indexes.append(_curr)
_curr += 1
packed_position_ids.extend([curr_position_id] * (num_img_tokens + 2))
packed_seqlens.append(num_img_tokens + 2)
newlens.append(curr_kvlen + num_img_tokens + 2)
new_rope.append(curr_position_id + 1)
generation_input = {
"packed_text_ids": torch.tensor(packed_text_ids, dtype=torch.long),
"packed_text_indexes": torch.tensor(packed_text_indexes, dtype=torch.long),
"vit_token_seqlens": torch.tensor(vit_token_seqlens, dtype=torch.int),
"packed_vit_tokens": torch.cat(packed_vit_tokens, dim=0),
"packed_vit_position_ids": torch.cat(packed_vit_position_ids, dim=0),
"packed_vit_token_indexes": torch.tensor(packed_vit_token_indexes, dtype=torch.long),
"packed_position_ids": torch.tensor(packed_position_ids, dtype=torch.long),
"packed_seqlens": torch.tensor(packed_seqlens, dtype=torch.int),
}
return generation_input, newlens, new_rope
def forward_cache_update_vit(
self,
past_key_values: NaiveCache,
packed_text_ids: torch.LongTensor,
packed_text_indexes: torch.LongTensor,
packed_vit_tokens: torch.Tensor,
packed_vit_token_indexes: torch.LongTensor,
packed_vit_position_ids: torch.LongTensor,
vit_token_seqlens: torch.IntTensor,
packed_position_ids: torch.LongTensor,
packed_seqlens: torch.IntTensor,
):
packed_text_embedding = self.language_model.forward(
packed_text_ids=packed_text_ids,
return_embeddings_only=True,
).packed_query_sequence
packed_sequence = packed_text_embedding.new_zeros((sum(packed_seqlens), self.hidden_size))
packed_sequence[packed_text_indexes] = packed_text_embedding
cu_seqlens = torch.nn.functional.pad(torch.cumsum(vit_token_seqlens, dim=0), (1, 0))
cu_seqlens = cu_seqlens.to(torch.int32)
max_seqlen = torch.max(vit_token_seqlens).item()
packed_vit_token_embed = self.vit_model(
packed_pixel_values=packed_vit_tokens,
packed_flattened_position_ids=packed_vit_position_ids,
cu_seqlens=cu_seqlens,
max_seqlen=max_seqlen,
)
packed_vit_token_embed = self.connector(packed_vit_token_embed)
pos_emb = self.vit_pos_embed(packed_vit_position_ids)
packed_vit_token_embed = packed_vit_token_embed + pos_emb
if packed_vit_token_embed.dtype != packed_sequence.dtype:
packed_vit_token_embed = packed_vit_token_embed.to(packed_sequence.dtype)
packed_sequence[packed_vit_token_indexes] = packed_vit_token_embed
extra_inputs = {}
if self.use_moe:
extra_inputs = {"mode": "und"}
output = self.language_model.forward(
packed_query_sequence=packed_sequence,
query_lens=packed_seqlens,
packed_query_position_ids=packed_position_ids,
past_key_values=past_key_values,
update_past_key_values=True,
is_causal=False,
**extra_inputs,
)
past_key_values = output.past_key_values
return past_key_values
def prepare_input(self, curr_kvlens, curr_rope, image_sizes, new_token_ids=None):
packed_text_ids, packed_text_indexes = list(), list()
packed_vae_position_ids, packed_vae_token_indexes, packed_init_noises = list(), list(), list()
packed_position_ids, packed_seqlens = list(), list()
query_curr = 0
for (H, W), curr_kvlen, curr_position_id in zip(image_sizes, curr_kvlens, curr_rope):
packed_text_ids.append(new_token_ids["start_of_image"])
packed_text_indexes.append(query_curr)
query_curr += 1
vae_position_ids = self.get_flattened_position_ids(
H, W, self.latent_downsample, max_num_patches_per_side=self.max_latent_size
)
packed_vae_position_ids.append(vae_position_ids)
h, w = H // self.latent_downsample, W // self.latent_downsample
num_image_tokens = h * w
packed_init_noises.append(torch.randn(num_image_tokens, self.latent_channel * self.latent_patch_size**2))
packed_vae_token_indexes.extend(range(query_curr, query_curr + num_image_tokens))
packed_seqlens.append(num_image_tokens + 2)
query_curr += num_image_tokens
packed_text_ids.append(new_token_ids["end_of_image"])
packed_text_indexes.append(query_curr)
query_curr += 1
packed_position_ids.extend([curr_position_id] * (num_image_tokens + 2))
# Construct Output
generation_input = {
"packed_text_ids": torch.tensor(packed_text_ids, dtype=torch.long),
"packed_text_indexes": torch.tensor(packed_text_indexes, dtype=torch.long),
"packed_init_noises": torch.cat(packed_init_noises, dim=0),
"packed_vae_position_ids": torch.cat(packed_vae_position_ids, dim=0),
"packed_vae_token_indexes": torch.tensor(packed_vae_token_indexes, dtype=torch.long),
"packed_seqlens": torch.tensor(packed_seqlens, dtype=torch.int),
"packed_position_ids": torch.tensor(packed_position_ids, dtype=torch.long),
}
return generation_input
def prepare_vae_latent(self, curr_kvlens, curr_rope, image_sizes, new_token_ids):
return self.prepare_input(curr_kvlens, curr_rope, image_sizes, new_token_ids)
def prepare_vae_latent_cfg(self, curr_kvlens, curr_rope, image_sizes):
packed_position_ids = list()
for (H, W), curr_kvlen, curr_position_id in zip(image_sizes, curr_kvlens, curr_rope):
h, w = H // self.latent_downsample, W // self.latent_downsample
num_image_tokens = h * w
packed_position_ids.extend([curr_position_id] * (num_image_tokens + 2))
generation_input = {
"cfg_packed_position_ids": torch.tensor(packed_position_ids, dtype=torch.long),
}
return generation_input
def prepare_start_tokens(self, curr_kvlens, curr_rope, new_token_ids):
"""Prepare start tokens for autoregressive text generation.
Ported from the original BAGEL ``Bagel.prepare_start_tokens``.
"""
packed_start_tokens = list()
packed_query_position_ids = list()
for curr_kvlen, curr_position_id in zip(curr_kvlens, curr_rope):
packed_start_tokens.append(new_token_ids["bos_token_id"])
packed_query_position_ids.append(curr_position_id)
generation_input = {
"packed_start_tokens": torch.tensor(packed_start_tokens, dtype=torch.long),
"packed_query_position_ids": torch.tensor(packed_query_position_ids, dtype=torch.long),
}
return generation_input
@torch.no_grad()
def generate_text(
self,
past_key_values: NaiveCache,
packed_start_tokens: torch.LongTensor,
packed_query_position_ids: torch.LongTensor,
max_length: int,
do_sample: bool = False,
temperature: float = 1.0,
end_token_id: int | None = None,
):
"""Autoregressive text generation (ported from original BAGEL).
Decodes tokens one at a time, appending to ``past_key_values``
until ``max_length`` is reached or ``end_token_id`` is generated.
"""
step = 0
generated_sequence = []
curr_tokens = packed_start_tokens
while step < max_length:
generated_sequence.append(curr_tokens)
query_lens = torch.ones_like(curr_tokens)
output = self.language_model(
packed_text_ids=curr_tokens,
query_lens=query_lens,
packed_query_position_ids=packed_query_position_ids,
past_key_values=past_key_values,
update_past_key_values=True,
is_causal=True,
mode="und",
)
past_key_values = output.past_key_values
packed_query_sequence = output.packed_query_sequence
pred_logits = self.language_model.lm_head(packed_query_sequence)
if do_sample:
probs = nn.functional.softmax(pred_logits / temperature, dim=-1)
curr_tokens = torch.multinomial(probs, num_samples=1).squeeze(1)
else:
curr_tokens = torch.argmax(pred_logits, dim=-1)
packed_query_position_ids = packed_query_position_ids + 1
step += 1
if end_token_id is not None and curr_tokens[0] == end_token_id:
break
output_device = generated_sequence[0].device
return torch.stack([i.to(output_device) for i in generated_sequence], dim=0)
def generate_image(
self,
packed_text_ids: torch.LongTensor,
packed_text_indexes: torch.LongTensor,
packed_init_noises: torch.Tensor,
packed_vae_position_ids: torch.LongTensor,
packed_vae_token_indexes: torch.LongTensor,
packed_seqlens: torch.IntTensor,
packed_position_ids: torch.LongTensor,
past_key_values: NaiveCache,
num_timesteps: int = 24,
timestep_shift: float = 1.0,
cfg_renorm_min: float = 0.0,
cfg_renorm_type: str = "global",
cfg_interval: tuple[float, float] = [0, 1],
# cfg_text
cfg_text_scale: float = 1.0,
cfg_text_packed_position_ids: torch.LongTensor | None = None,
cfg_text_past_key_values: NaiveCache | None = None,
# cfg_img
cfg_img_scale: float = 1.0,
cfg_img_packed_position_ids: torch.LongTensor | None = None,
cfg_img_past_key_values: NaiveCache | None = None,
return_trajectory_latents: bool = False,
scheduler: object | None = None,
scheduler_kwargs: dict | None = None,
# Lance i2v: tokens to freeze at their initial (encoded-image)
# value throughout the denoise loop. Matches upstream's
# ``mse_loss_indexes``-exclusion behaviour from PR #33: cond
# positions are never updated and get ``timestep=0``.
frame_condition_token_indexes: torch.LongTensor | None = None,
):
x_t = packed_init_noises
# Snapshot the pinned subtensor BEFORE the denoise loop touches
# x_t; the cond positions in packed_init_noises hold the
# VAE-encoded conditioning latent that must be preserved verbatim.
pinned_x_t = None
if frame_condition_token_indexes is not None:
frame_condition_token_indexes = frame_condition_token_indexes.to(x_t.device).long()
pinned_x_t = x_t[frame_condition_token_indexes].clone()
# Build the flow-matching schedule. BAGEL drops the terminal t=0 for
# ``num_timesteps - 1`` Euler steps; Lance keeps it for ``num_timesteps``.
# ``_denoise_schedule_extra_step`` (overridden by ``LanceBagel``) selects which.
num_sample_points = num_timesteps + 1 if self._denoise_schedule_extra_step else num_timesteps
timesteps = torch.linspace(1, 0, num_sample_points, device=x_t.device)
timesteps = timestep_shift * timesteps / (1 + (timestep_shift - 1) * timesteps)
dts = timesteps[:-1] - timesteps[1:]
timesteps = timesteps[:-1]
# Optional trajectory recording for RL rollout data collection
trajectory_latents: list[torch.Tensor] | None = [] if return_trajectory_latents else None
trajectory_timesteps: list[torch.Tensor] | None = [] if return_trajectory_latents else None
trajectory_log_probs: list[torch.Tensor] | None = (
[] if (return_trajectory_latents and scheduler is not None) else None
)
_sched_kw = scheduler_kwargs or {}
use_cfg_text = cfg_text_scale > 1.0
use_cfg_img = cfg_img_scale > 1.0
# ── Detect CFG parallel mode ──
cfg_parallel_ready = use_cfg_text and get_classifier_free_guidance_world_size() > 1
if cfg_parallel_ready:
return self._generate_image_parallel(
x_t=x_t,
timesteps=timesteps,
dts=dts,
packed_text_ids=packed_text_ids,
packed_text_indexes=packed_text_indexes,
packed_vae_position_ids=packed_vae_position_ids,
packed_vae_token_indexes=packed_vae_token_indexes,
packed_seqlens=packed_seqlens,
packed_position_ids=packed_position_ids,
past_key_values=past_key_values,
cfg_renorm_min=cfg_renorm_min,
cfg_renorm_type=cfg_renorm_type,
cfg_interval=cfg_interval,
cfg_text_scale=cfg_text_scale,
cfg_text_packed_position_ids=cfg_text_packed_position_ids,
cfg_text_past_key_values=cfg_text_past_key_values,
cfg_img_scale=cfg_img_scale,
cfg_img_packed_position_ids=cfg_img_packed_position_ids,
cfg_img_past_key_values=cfg_img_past_key_values,
return_trajectory_latents=return_trajectory_latents,
scheduler=scheduler,
scheduler_kwargs=scheduler_kwargs,
)
# ── SP + CFG: sequential single-branch forwards ──
use_sp = self._sp_size > 1
if use_sp and use_cfg_text:
if return_trajectory_latents and len(timesteps) > 0:
trajectory_latents.append(x_t.clone())
for i, t in enumerate(timesteps.tolist()): # host floats; a 0-d tensor t would sync each step
timestep = torch.tensor([t] * x_t.shape[0], device=x_t.device)
if frame_condition_token_indexes is not None:
# Cond positions stay at t=0 (clean signal). Matches upstream
# PR #33 lance.py line 1605:
# timestep[current_vae_mse_indexes_local_in_vae] = t
# (cond positions remain at the ``torch.zeros`` init value).
timestep[frame_condition_token_indexes] = 0.0
in_cfg_window = t > cfg_interval[0] and t <= cfg_interval[1]
cfg_text_scale_ = cfg_text_scale if in_cfg_window else 1.0
cfg_img_scale_ = cfg_img_scale if in_cfg_window else 1.0
common = dict(
x_t=x_t,
timestep=timestep,
packed_vae_token_indexes=packed_vae_token_indexes,
packed_vae_position_ids=packed_vae_position_ids,
packed_text_ids=packed_text_ids,
packed_text_indexes=packed_text_indexes,
packed_seqlens=packed_seqlens,
)
v_t = self.forward_single_branch(
**common,
packed_position_ids=packed_position_ids,
past_key_values=past_key_values,
)
if cfg_text_scale_ > 1.0:
cfg_text_v_t = self.forward_single_branch(
**common,
packed_position_ids=cfg_text_packed_position_ids,
past_key_values=cfg_text_past_key_values,
)
cfg_img_v_t = None
if cfg_img_scale_ > 1.0:
cfg_img_v_t = self.forward_single_branch(
**common,
packed_position_ids=cfg_img_packed_position_ids,
past_key_values=cfg_img_past_key_values,
)
v_t = self._combine_cfg(
v_t,
cfg_text_v_t,
cfg_img_v_t,
cfg_text_scale_,
cfg_img_scale_,
cfg_renorm_type,
cfg_renorm_min,
)
if scheduler is not None:
out = scheduler.step(v_t.to(x_t.device), timesteps[i], x_t, dts[i], **_sched_kw)
x_t = out.prev_sample
if trajectory_log_probs is not None and out.log_prob is not None:
trajectory_log_probs.append(out.log_prob)
else:
x_t = x_t - v_t.to(x_t.device) * dts[i]
if return_trajectory_latents:
trajectory_latents.append(x_t.clone())
trajectory_timesteps.append(timesteps[i])
unpacked_latent = x_t.split((packed_seqlens - 2).tolist())
return unpacked_latent, trajectory_latents, trajectory_timesteps, trajectory_log_probs
# ── SP without CFG: direct single-branch loop ──
if use_sp:
if return_trajectory_latents and len(timesteps) > 0:
trajectory_latents.append(x_t.clone())
for i, t in enumerate(timesteps.tolist()): # host floats; a 0-d tensor t would sync each step
timestep = torch.tensor([t] * x_t.shape[0], device=x_t.device)
if frame_condition_token_indexes is not None:
# Cond positions stay at t=0 (clean signal). Matches upstream
# PR #33 lance.py line 1605:
# timestep[current_vae_mse_indexes_local_in_vae] = t
# (cond positions remain at the ``torch.zeros`` init value).
timestep[frame_condition_token_indexes] = 0.0
v_t = self.forward_single_branch(
x_t=x_t,
timestep=timestep,
packed_vae_token_indexes=packed_vae_token_indexes,
packed_vae_position_ids=packed_vae_position_ids,
packed_text_ids=packed_text_ids,
packed_text_indexes=packed_text_indexes,
packed_position_ids=packed_position_ids,
packed_seqlens=packed_seqlens,
past_key_values=past_key_values,
)
if scheduler is not None:
out = scheduler.step(v_t.to(x_t.device), timesteps[i], x_t, dts[i], **_sched_kw)
x_t = out.prev_sample
out_log_prob = getattr(out, "log_prob", None)
if trajectory_log_probs is not None and out_log_prob is not None:
trajectory_log_probs.append(out_log_prob)
else:
x_t = x_t - v_t.to(x_t.device) * dts[i]
if return_trajectory_latents:
trajectory_latents.append(x_t.clone())
trajectory_timesteps.append(timesteps[i])
unpacked_latent = x_t.split((packed_seqlens - 2).tolist())
return unpacked_latent, trajectory_latents, trajectory_timesteps, trajectory_log_probs
# ── Sequential CFG mode (cfg_parallel_size=1, no SP) ──
# 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_branch_pids: list[torch.Tensor] | None = None
cfg_branch_caches: list[NaiveCache] | None = None
if use_cfg_text:
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:
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())
for i, t in enumerate(timesteps.tolist()): # host floats; a 0-d tensor t would sync each step
timestep = torch.tensor([t] * x_t.shape[0], device=x_t.device)
if frame_condition_token_indexes is not None:
# Cond positions stay at t=0 (clean signal). Matches upstream
# PR #33 lance.py line 1605:
# timestep[current_vae_mse_indexes_local_in_vae] = t
# (cond positions remain at the ``torch.zeros`` init value).
timestep[frame_condition_token_indexes] = 0.0
if t > cfg_interval[0] and t <= cfg_interval[1]:
cfg_text_scale_ = cfg_text_scale
cfg_img_scale_ = cfg_img_scale
else:
cfg_text_scale_ = 1.0
cfg_img_scale_ = 1.0
v_t = self.forward(
x_t=x_t,
timestep=timestep,
packed_vae_token_indexes=packed_vae_token_indexes,
packed_vae_position_ids=packed_vae_position_ids,
packed_text_ids=packed_text_ids,
packed_text_indexes=packed_text_indexes,
packed_position_ids=packed_position_ids,
packed_seqlens=packed_seqlens,
past_key_values=past_key_values,
cfg_renorm_min=cfg_renorm_min,
cfg_renorm_type=cfg_renorm_type,
cfg_text_scale=cfg_text_scale_,
cfg_img_scale=cfg_img_scale_,
cfg_branch_pids=cfg_branch_pids,
cfg_branch_caches=cfg_branch_caches,
)
if scheduler is not None:
out = scheduler.step(v_t.to(x_t.device), timesteps[i], x_t, dts[i], **_sched_kw)
x_t = out.prev_sample
if trajectory_log_probs is not None and out.log_prob is not None:
trajectory_log_probs.append(out.log_prob)
else:
x_t = x_t - v_t.to(x_t.device) * dts[i] # velocity pointing from data to noise
if pinned_x_t is not None:
# i2v: restore cond positions to their encoded-image
# latent. Matches upstream PR #33 lance.py line 1712:
# x_t[mse_indexes] = x_t[mse_indexes] - v_t[mse_indexes] * dts[i]
# (cond positions are excluded from the update).
x_t[frame_condition_token_indexes] = pinned_x_t
if return_trajectory_latents:
trajectory_latents.append(x_t.clone())
trajectory_timesteps.append(timesteps[i])
unpacked_latent = x_t.split((packed_seqlens - 2).tolist())
return unpacked_latent, trajectory_latents, trajectory_timesteps, trajectory_log_probs
def _generate_image_parallel(
self,
x_t: torch.Tensor,
timesteps: torch.Tensor,
dts: torch.Tensor,
packed_text_ids: torch.LongTensor,
packed_text_indexes: torch.LongTensor,
packed_vae_position_ids: torch.LongTensor,
packed_vae_token_indexes: torch.LongTensor,
packed_seqlens: torch.IntTensor,
packed_position_ids: torch.LongTensor,
past_key_values: NaiveCache,
cfg_renorm_min: float,
cfg_renorm_type: str,
cfg_interval: tuple[float, float],
cfg_text_scale: float,
cfg_text_packed_position_ids: torch.LongTensor | None,
cfg_text_past_key_values: NaiveCache | None,
cfg_img_scale: float,
cfg_img_packed_position_ids: torch.LongTensor | None,
cfg_img_past_key_values: NaiveCache | None,
return_trajectory_latents: bool = False,
scheduler: object | None = None,
scheduler_kwargs: dict | None = None,
frame_condition_token_indexes: torch.LongTensor | None = None,
):
"""CFG parallel denoising loop: each rank computes one CFG branch.
Rank 0: gen branch (full conditioning)
Rank 1: text_cfg branch (unconditional text)
Rank 2: img_cfg branch (no image condition), only when cfg_img_scale > 1.0
"""
cfg_group = get_cfg_group()
cfg_world_size = get_classifier_free_guidance_world_size()
use_cfg_img = cfg_img_scale > 1.0
# Validate cfg_parallel_size vs cfg_img_scale consistency
if cfg_world_size == 3 and not use_cfg_img:
raise ValueError(
f"cfg_parallel_size=3 requires cfg_img_scale > 1.0, "
f"but got cfg_img_scale={cfg_img_scale}. "
f"Use cfg_parallel_size=2 for text-only CFG parallel(text2img), or set cfg_img_scale > 1.0."
)
if cfg_world_size == 2 and use_cfg_img:
raise ValueError(
f"Image CFG (cfg_img_scale={cfg_img_scale}) requires cfg_parallel_size=3, "
f"but got cfg_parallel_size=2. "
f"Use cfg_parallel_size=3 to enable image CFG in parallel mode."
)
# Ensure all ranks start with the same x_t (initial noise may differ
# across ranks when no per-request seed is set).
x_t = x_t.contiguous()
cfg_group.broadcast(x_t, src=0)
trajectory_latents: list[torch.Tensor] | None = [] if return_trajectory_latents else None
trajectory_timesteps: list[torch.Tensor] | None = [] if return_trajectory_latents else None
trajectory_log_probs: list[torch.Tensor] | None = (
[] if (return_trajectory_latents and scheduler is not None) else None
)
_sched_kw = scheduler_kwargs or {}
if return_trajectory_latents and len(timesteps) > 0:
trajectory_latents.append(x_t.clone())
for i, t in enumerate(timesteps.tolist()): # host floats; a 0-d tensor t would sync each step
timestep = torch.tensor([t] * x_t.shape[0], device=x_t.device)
if frame_condition_token_indexes is not None:
# Cond positions stay at t=0 (clean signal). Matches upstream
# PR #33 lance.py line 1605:
# timestep[current_vae_mse_indexes_local_in_vae] = t
# (cond positions remain at the ``torch.zeros`` init value).
timestep[frame_condition_token_indexes] = 0.0
use_cfg_this_step = t > cfg_interval[0] and t <= cfg_interval[1] and cfg_text_scale > 1.0
# Per-branch kwargs. Branch 0 (gen, full conditioning) is also the
# branch used by all ranks when do_true_cfg is False (outside the
# CFG interval) — CFGParallelMixin only runs branches_kwargs[0] then,
# mirroring the previous "all ranks compute gen inputs, no comm" path.
common = dict(
x_t=x_t,
timestep=timestep,
packed_vae_token_indexes=packed_vae_token_indexes,
packed_vae_position_ids=packed_vae_position_ids,
packed_text_ids=packed_text_ids,
packed_text_indexes=packed_text_indexes,
packed_seqlens=packed_seqlens,
)
branches_kwargs = [
dict(**common, packed_position_ids=packed_position_ids, past_key_values=past_key_values),
dict(
**common, packed_position_ids=cfg_text_packed_position_ids, past_key_values=cfg_text_past_key_values
),
]
if use_cfg_img:
branches_kwargs.append(
dict(
**common,
packed_position_ids=cfg_img_packed_position_ids,
past_key_values=cfg_img_past_key_values,
)
)
# Each rank computes its assigned branch, then all_gather + combine
# happen inside the mixin (identical result on every rank).
v_t = self.predict_noise_with_multi_branch_cfg(
do_true_cfg=use_cfg_this_step,
true_cfg_scale={
"cfg_text_scale": cfg_text_scale,
"cfg_img_scale": cfg_img_scale,
"cfg_renorm_type": cfg_renorm_type,
"cfg_renorm_min": cfg_renorm_min,
},
branches_kwargs=branches_kwargs,
)
if scheduler is not None:
out = scheduler.step(v_t.to(x_t.device), timesteps[i], x_t, dts[i], **_sched_kw)
x_t = out.prev_sample
if trajectory_log_probs is not None and out.log_prob is not None:
trajectory_log_probs.append(out.log_prob)
else:
x_t = x_t - v_t.to(x_t.device) * dts[i]
if return_trajectory_latents:
trajectory_latents.append(x_t.clone())
trajectory_timesteps.append(timesteps[i])
unpacked_latent = x_t.split((packed_seqlens - 2).tolist())
return unpacked_latent, trajectory_latents, trajectory_timesteps, trajectory_log_probs
@staticmethod
def _combine_cfg(
v_t: torch.Tensor,
cfg_text_v_t: torch.Tensor,
cfg_img_v_t: torch.Tensor | None,
cfg_text_scale: float,
cfg_img_scale: float,
cfg_renorm_type: str,
cfg_renorm_min: float,
) -> torch.Tensor:
"""Combine 3-branch CFG predictions with renormalization.
Args:
v_t: velocity from gen branch (full conditioning)
cfg_text_v_t: velocity from text_cfg branch (unconditional text)
cfg_img_v_t: velocity from img_cfg branch (no image), or None
cfg_text_scale: text guidance scale
cfg_img_scale: image guidance scale
cfg_renorm_type: "text_channel", "global", or "channel"
cfg_renorm_min: minimum renormalization scale
"""
if cfg_renorm_type == "text_channel":
v_t_text_ = cfg_text_v_t + cfg_text_scale * (v_t - cfg_text_v_t)
norm_v_t = torch.norm(v_t, dim=-1, keepdim=True)
norm_v_t_text_ = torch.norm(v_t_text_, dim=-1, keepdim=True)
scale = (norm_v_t / (norm_v_t_text_ + 1e-8)).clamp(min=cfg_renorm_min, max=1.0)
v_t_text = v_t_text_ * scale
if cfg_img_scale > 1.0 and cfg_img_v_t is not None:
v_t = cfg_img_v_t + cfg_img_scale * (v_t_text - cfg_img_v_t)
else:
v_t = v_t_text
else:
v_t_text_ = cfg_text_v_t + cfg_text_scale * (v_t - cfg_text_v_t)
if cfg_img_scale > 1.0 and cfg_img_v_t is not None:
v_t_ = cfg_img_v_t + cfg_img_scale * (v_t_text_ - cfg_img_v_t)
else:
v_t_ = v_t_text_
# NOTE norm is computed over all dimensions, thus currently only supports batch_size = 1 with navit
if cfg_renorm_type == "global":
norm_v_t = torch.norm(v_t)
norm_v_t_ = torch.norm(v_t_)
elif cfg_renorm_type == "channel":
norm_v_t = torch.norm(v_t, dim=-1, keepdim=True)
norm_v_t_ = torch.norm(v_t_, dim=-1, keepdim=True)
else:
raise NotImplementedError(f"{cfg_renorm_type} is not supported")
scale = (norm_v_t / (norm_v_t_ + 1e-8)).clamp(min=cfg_renorm_min, max=1.0)
v_t = v_t_ * scale
return v_t
# ── CFGParallelMixin hooks ──
# Bagel mounts CFGParallelMixin (see class declaration) to reuse the shared
# N-branch CFG dispatch/all_gather logic in predict_noise_with_multi_branch_cfg.
# Only two hooks need model-specific behaviour:
# * predict_noise: one branch == one Bagel forward (per-branch KV cache).
# * combine_multi_branch_cfg_noise: Bagel's renorm-aware 3-branch combine.
def predict_noise(self, **kwargs) -> torch.Tensor:
"""Single-branch velocity prediction for CFGParallelMixin.
Each CFG branch differs only by ``packed_position_ids`` and
``past_key_values`` (carried in ``kwargs``); the heavy lifting is the
per-branch ``forward_single_branch`` pass.
"""
return self.forward_single_branch(**kwargs)
def combine_multi_branch_cfg_noise(
self,
predictions: list[torch.Tensor],
true_cfg_scale: dict[str, float],
cfg_normalize: bool = False,
) -> torch.Tensor:
"""Combine gen/text/img branch velocities via Bagel's renorm CFG.
``predictions[0]`` is the gen branch, ``[1]`` the text-CFG branch, and
``[2]`` (when present) the image-CFG branch. ``cfg_normalize`` is unused
because renormalization is folded into ``_combine_cfg`` itself.
"""
cfg_img_v_t = predictions[2] if len(predictions) > 2 else None
return self._combine_cfg(
predictions[0],
predictions[1],
cfg_img_v_t,
true_cfg_scale["cfg_text_scale"],
true_cfg_scale["cfg_img_scale"],
true_cfg_scale["cfg_renorm_type"],
true_cfg_scale["cfg_renorm_min"],
)
def forward_single_branch(
self,
x_t: torch.Tensor,
timestep: torch.LongTensor,
packed_vae_token_indexes: torch.LongTensor,
packed_vae_position_ids: torch.LongTensor,
packed_text_ids: torch.LongTensor,
packed_text_indexes: torch.LongTensor,
packed_position_ids: torch.LongTensor,
packed_seqlens: torch.IntTensor,
past_key_values: NaiveCache,
) -> torch.Tensor:
"""Run a single-branch forward pass (no CFG batching).
Used by CFG parallel mode where each rank computes one branch.
Returns the velocity v_t for the given branch.
Supports Ulysses / Ring SP when parallel_config.sequence_parallel_size > 1.
"""
use_sp = self._sp_size > 1
if use_sp:
# Split VAE tokens across SP ranks
(
local_x_t,
local_vae_pos_ids,
local_vae_indexes,
local_text_indexes,
local_seqlens,
local_position_ids,
) = self._split_vae_for_sp(
x_t,
packed_vae_position_ids,
packed_vae_token_indexes,
packed_text_indexes,
packed_seqlens,
packed_position_ids,
)
packed_text_embedding = self.language_model.forward(
packed_text_ids=packed_text_ids,
return_embeddings_only=True,
).packed_query_sequence
packed_sequence = packed_text_embedding.new_zeros((int(local_seqlens.sum()), self.hidden_size))
packed_sequence[local_text_indexes] = packed_text_embedding
# i2v relaxes this: per-token timestep (cond=0, noncond=t) is valid.
packed_pos_embed = self.latent_pos_embed(local_vae_pos_ids)
local_timestep = timestep[: local_x_t.shape[0]]
packed_timestep_embeds = self.time_embedder(local_timestep)
x_t_emb = self.vae2llm(local_x_t) + packed_timestep_embeds + packed_pos_embed
if x_t_emb.dtype != packed_sequence.dtype:
x_t_emb = x_t_emb.to(packed_sequence.dtype)
packed_sequence[local_vae_indexes] = x_t_emb
extra_inputs = {}
if self.use_moe:
extra_inputs["mode"] = "gen"
extra_inputs["packed_vae_token_indexes"] = local_vae_indexes
extra_inputs["packed_text_indexes"] = local_text_indexes
output = self.language_model.forward(
packed_query_sequence=packed_sequence,
query_lens=local_seqlens,
packed_query_position_ids=local_position_ids,
past_key_values=past_key_values,
update_past_key_values=False,
is_causal=False,
**extra_inputs,
)
local_v_t = self.llm2vae(output.packed_query_sequence)
local_v_t = local_v_t[local_vae_indexes]
return self._gather_vae_for_sp(local_v_t)
# Original non-SP path
packed_text_embedding = self.language_model.forward(
packed_text_ids=packed_text_ids,
return_embeddings_only=True,
).packed_query_sequence
packed_sequence = packed_text_embedding.new_zeros((sum(packed_seqlens), self.hidden_size))
packed_sequence[packed_text_indexes] = packed_text_embedding
# i2v relaxes this: per-token timestep (cond=0, noncond=t) is valid.
packed_pos_embed = self.latent_pos_embed(packed_vae_position_ids)
packed_timestep_embeds = self.time_embedder(timestep)
x_t_emb = self.vae2llm(x_t) + packed_timestep_embeds + packed_pos_embed
if x_t_emb.dtype != packed_sequence.dtype:
x_t_emb = x_t_emb.to(packed_sequence.dtype)
packed_sequence[packed_vae_token_indexes] = x_t_emb
extra_inputs = {}
if self.use_moe:
extra_inputs["mode"] = "gen"
extra_inputs["packed_vae_token_indexes"] = packed_vae_token_indexes
extra_inputs["packed_text_indexes"] = packed_text_indexes
output = self.language_model.forward(
packed_query_sequence=packed_sequence,
query_lens=packed_seqlens,
packed_query_position_ids=packed_position_ids,
past_key_values=past_key_values,
update_past_key_values=False,
is_causal=False,
**extra_inputs,
)
v_t = self.llm2vae(output.packed_query_sequence)
v_t = v_t[packed_vae_token_indexes]
return v_t
def forward(
self,
x_t: torch.Tensor,
timestep: torch.LongTensor,
packed_vae_token_indexes: torch.LongTensor,
packed_vae_position_ids: torch.LongTensor,
packed_text_ids: torch.LongTensor,
packed_text_indexes: torch.LongTensor,
packed_position_ids: torch.LongTensor,
packed_seqlens: torch.IntTensor,
past_key_values: NaiveCache,
cfg_renorm_min: float = 0.0,
cfg_renorm_type: str = "global",
cfg_text_scale: float = 1.0,
cfg_img_scale: float = 1.0,
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(
packed_text_ids=packed_text_ids,
return_embeddings_only=True,
).packed_query_sequence
packed_sequence = packed_text_embedding.new_zeros((sum(packed_seqlens), self.hidden_size))
packed_sequence[packed_text_indexes] = packed_text_embedding
# i2v relaxes this: per-token timestep (cond=0, noncond=t) is valid.
packed_pos_embed = self.latent_pos_embed(packed_vae_position_ids)
packed_timestep_embeds = self.time_embedder(timestep)
x_t = self.vae2llm(x_t) + packed_timestep_embeds + packed_pos_embed
if x_t.dtype != packed_sequence.dtype:
x_t = x_t.to(packed_sequence.dtype)
packed_sequence[packed_vae_token_indexes] = x_t
extra_inputs = {}
if self.use_moe:
extra_inputs["mode"] = "gen"
extra_inputs["packed_vae_token_indexes"] = packed_vae_token_indexes
extra_inputs["packed_text_indexes"] = packed_text_indexes
use_cfg = cfg_text_scale > 1.0
cfg_text_v_t = None
cfg_img_v_t = None
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())
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, dim=1 if cfg_branch_pids[0].ndim == 2 else 0)
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(
packed_query_sequence=packed_sequence,
query_lens=packed_seqlens,
packed_query_position_ids=packed_position_ids,
past_key_values=past_key_values,
update_past_key_values=False,
is_causal=False,
**extra_inputs,
)
v_t = self.llm2vae(output.packed_query_sequence)[packed_vae_token_indexes]
# ── CFG combination ──
if use_cfg:
v_t = self._combine_cfg(
v_t,
cfg_text_v_t,
cfg_img_v_t,
cfg_text_scale,
cfg_img_scale,
cfg_renorm_type,
cfg_renorm_min,
)
return v_t