[LingBot World] Eleven bit-identical removals of repeated work in the realtime path (#7648)
Signed-off-by: tzhouam <tzhouam@connect.ust.hk> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
a9e2117f5e
commit
86fcfe95f1
23 files changed
+1957
-200
No files matched your search
@@ -92,6 +92,13 @@ class SupportsStepExecution(Protocol):
|
||||
``prepare_encode()`` (one-time request setup), ``denoise_step()``
|
||||
(one denoise forward), ``step_scheduler()`` (one scheduler update),
|
||||
and ``post_decode()`` (final decode).
|
||||
|
||||
A pipeline may additionally set the optional class attribute
|
||||
``supports_chunk_step_grouping = True`` to declare that its request state
|
||||
may advance through every denoise step of its current chunk without a
|
||||
serving-scheduler cycle in between (a capability, not a policy: the runner
|
||||
that owns the scheduling decision chooses whether to group steps). It is
|
||||
optional so it does not become part of the runtime protocol check.
|
||||
"""
|
||||
|
||||
supports_step_execution: ClassVar[bool] = True
|
||||
|
||||
@@ -22,6 +22,7 @@ from diffusers.utils.torch_utils import randn_tensor
|
||||
|
||||
from vllm_omni.diffusion.forward_context import set_forward_context_denoise_step_idx
|
||||
from vllm_omni.diffusion.models.lingbot_world.transformer import (
|
||||
CameraModulationCache,
|
||||
LingBotAttentionCache,
|
||||
LingBotTransformerCache,
|
||||
)
|
||||
@@ -109,11 +110,16 @@ class LingBotDMDBlockRunner:
|
||||
start_frame: int,
|
||||
timestep_value: float,
|
||||
step_index: int,
|
||||
camera_cache: CameraModulationCache | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""Predict flow for one denoise step.
|
||||
|
||||
A probe never writes KV: only the clean x0 of a finished block may
|
||||
enter the cache, which is ``commit_block_kv``'s job.
|
||||
|
||||
``camera_cache`` is the block's camera-modulation cache when the caller
|
||||
holds it across separate calls (the stepwise path); ``generate_block``
|
||||
opens the window itself instead.
|
||||
"""
|
||||
if not self.enforce_eager:
|
||||
torch.compiler.cudagraph_mark_step_begin()
|
||||
@@ -130,6 +136,7 @@ class LingBotDMDBlockRunner:
|
||||
cache=self._block_cache(condition=condition, cache=cache, ar=ar, commit_current=False),
|
||||
start_frame=start_frame,
|
||||
update_cache=False,
|
||||
camera_modulation_cache=camera_cache,
|
||||
)
|
||||
if flow_prediction.shape != current_latents.shape:
|
||||
raise RuntimeError(
|
||||
@@ -172,11 +179,13 @@ class LingBotDMDBlockRunner:
|
||||
cache: LingBotTransformerCache | None,
|
||||
ar: ARBlockContext | None,
|
||||
start_frame: int,
|
||||
camera_cache: CameraModulationCache | None = None,
|
||||
) -> None:
|
||||
"""Write the finished block's clean x0 into KV and commit its pages.
|
||||
|
||||
The fifth transformer call of a block, deliberately not a denoise step:
|
||||
on the stepwise path it belongs to ``post_decode()``.
|
||||
on the stepwise path it belongs to ``post_decode()``. ``camera_cache``
|
||||
is as in ``probe_step``.
|
||||
"""
|
||||
# Commit K/V only for the final clean block, never for noisy probes.
|
||||
cache_input = torch.cat((latents.to(dtype=condition.dtype), condition), dim=1)
|
||||
@@ -190,6 +199,7 @@ class LingBotDMDBlockRunner:
|
||||
cache=self._block_cache(condition=condition, cache=cache, ar=ar, commit_current=True),
|
||||
start_frame=start_frame,
|
||||
update_cache=True,
|
||||
camera_modulation_cache=camera_cache,
|
||||
)
|
||||
if ar is not None:
|
||||
ar.state.commit_paged_context(ar.branch)
|
||||
@@ -210,6 +220,10 @@ class LingBotDMDBlockRunner:
|
||||
"""Request mode: all probes of one block, then its commit."""
|
||||
block_shape = (1, self.transformer.config.out_channels, *condition.shape[2:5])
|
||||
current_latents = randn_tensor(block_shape, generator=generator, device=self.device, dtype=torch.float32)
|
||||
# Every forward below -- each probe and the commit -- reads the same camera trajectory, so the camera
|
||||
# injector is built once per block: one cache for the whole loop, exactly as the stepwise path holds
|
||||
# one per block across its separate calls.
|
||||
camera_cache = CameraModulationCache()
|
||||
for step_index, (timestep_value, sigma) in enumerate(schedule):
|
||||
flow_prediction = self.probe_step(
|
||||
current_latents=current_latents,
|
||||
@@ -221,6 +235,7 @@ class LingBotDMDBlockRunner:
|
||||
start_frame=start_frame,
|
||||
timestep_value=timestep_value,
|
||||
step_index=step_index,
|
||||
camera_cache=camera_cache,
|
||||
)
|
||||
next_sigma = schedule[step_index + 1][1] if step_index + 1 < len(schedule) else None
|
||||
current_latents = self.apply_transition(
|
||||
@@ -235,5 +250,6 @@ class LingBotDMDBlockRunner:
|
||||
cache=cache,
|
||||
ar=ar,
|
||||
start_frame=start_frame,
|
||||
camera_cache=camera_cache,
|
||||
)
|
||||
return current_latents
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import math
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
@@ -54,6 +55,7 @@ from vllm_omni.diffusion.models.lingbot_world.camera import (
|
||||
)
|
||||
from vllm_omni.diffusion.models.lingbot_world.dmd_block import ARBlockContext, LingBotDMDBlockRunner
|
||||
from vllm_omni.diffusion.models.lingbot_world.transformer import (
|
||||
CameraModulationCache,
|
||||
CausalLingBotWorldTransformer3DModel,
|
||||
LingBotAttentionCache,
|
||||
LingBotTransformerCache,
|
||||
@@ -91,7 +93,6 @@ logger = init_logger(__name__)
|
||||
_STREAMING_DECODE_BYTES_PER_PIXEL_FP32 = 37832 * 1024 / (64 * 64)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from diffusers.video_processor import VideoProcessor
|
||||
from tqdm.std import tqdm as TqdmProgressBar
|
||||
|
||||
from vllm_omni.diffusion.worker.input_batch import InputBatch
|
||||
@@ -152,6 +153,9 @@ class _LingBotRequestInputs:
|
||||
width: int
|
||||
num_frames: int
|
||||
num_latent_frames: int
|
||||
# Request mode returns pixels through VideoProcessor in this type. Step execution (realtime sessions)
|
||||
# returns every pixel type as ``(F, H, W, 3)`` uint8 frames, the layout the streaming endpoint muxes; only
|
||||
# ``"latent"`` differs there. See ``_decode_chunk_to_pixels``.
|
||||
output_type: str
|
||||
max_sequence_length: int
|
||||
flow_shift: float
|
||||
@@ -163,10 +167,20 @@ class _LingBotARSessionState:
|
||||
"""Model-owned state; attention tensors remain runner-owned."""
|
||||
|
||||
next_chunk_index: int = 0
|
||||
prompt: str | None = None
|
||||
# The umT5 embedding itself is reused by ``encode_prompt``'s own cache.
|
||||
# ``text_cache_key`` does the same job for the runner-owned cross-attention
|
||||
# K/V built from that embedding: it holds the (prompt, max_sequence_length,
|
||||
# dtype) the K/V was built from and is only published after a successful
|
||||
# build; ``text_cache_dirty`` is raised for the duration of a build, so a
|
||||
# build that fails forces the next tick to invalidate whatever its prompt is.
|
||||
text_cache_key: tuple[str, int, str] | None = None
|
||||
text_cache_dirty: bool = False
|
||||
generator_state: torch.Tensor | None = None
|
||||
encoder_cache: list[torch.Tensor | None] | None = None
|
||||
pending_encoder_cache: list[torch.Tensor | None] | None = None
|
||||
# The condition the encoder settles on once its history has stopped
|
||||
# changing; see ``_next_condition_chunk``.
|
||||
condition_fixed_point: torch.Tensor | None = None
|
||||
camera_tail: CameraTrajectory | None = None
|
||||
camera_pitch: float = 0.0
|
||||
|
||||
@@ -284,7 +298,70 @@ def _decode_source_image(image: PIL.Image.Image) -> PIL.Image.Image:
|
||||
raise ValueError(_SOURCE_IMAGE_ERROR) from None
|
||||
|
||||
|
||||
def _source_image_fingerprint(path: str | os.PathLike[str]) -> tuple[str, int, int, int, int, int]:
|
||||
"""Identify the bytes behind ``path``: the resolved path plus the stat fields that change when it is rewritten."""
|
||||
resolved = os.path.realpath(os.fspath(path))
|
||||
stat = os.stat(resolved)
|
||||
return resolved, stat.st_dev, stat.st_ino, stat.st_size, stat.st_mtime_ns, stat.st_ctime_ns
|
||||
|
||||
|
||||
def _encoder_caches_identical(
|
||||
before: list[torch.Tensor | None] | None, after: list[torch.Tensor | None] | None
|
||||
) -> bool:
|
||||
"""Whether two encoder histories hold the same tensors bit for bit, slot by slot."""
|
||||
if before is None or after is None or len(before) != len(after):
|
||||
return False
|
||||
for x, y in zip(before, after, strict=True):
|
||||
if x is None or y is None:
|
||||
if x is not y:
|
||||
return False
|
||||
elif x.shape != y.shape or x.dtype != y.dtype or not torch.equal(x, y):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _uint8_frames(video: torch.Tensor) -> np.ndarray:
|
||||
"""Turn one decoded block ``(1, 3, F, H, W)`` in ``[-1, 1]`` into ``(F, H, W, 3)`` uint8 on the host.
|
||||
|
||||
This is the arithmetic ``VideoProcessor.postprocess_video`` applies for
|
||||
``"np"`` and ``"pil"`` -- denormalise, widen to float32, scale by 255 and
|
||||
round -- done on the device before the copy instead of on the host after
|
||||
it. The bytes are identical to the PIL path; what changes is that the
|
||||
frames cross to the host once, already in the layout and dtype the
|
||||
streaming endpoint muxes, instead of as a float32 copy that is then
|
||||
converted per frame while the next block waits.
|
||||
"""
|
||||
# Same order of operations as VideoProcessor: denormalise and clamp in
|
||||
# the decoder dtype, then widen, scale and round.
|
||||
frames = (video[0] / 2 + 0.5).clamp(0, 1).permute(1, 2, 3, 0)
|
||||
frames = frames.float().mul_(255).round_().to(torch.uint8)
|
||||
return np.ascontiguousarray(frames.cpu().numpy())
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _decode_source_image_fingerprinted(fingerprint: tuple[str, int, int, int, int, int]) -> PIL.Image.Image:
|
||||
"""The decoded image behind ``fingerprint`` (resolved path plus the stat fields that change on rewrite).
|
||||
|
||||
One entry: a request-mode caller that replays the same source path decodes it once. The realtime tick
|
||||
carries its image inline and never comes through here.
|
||||
"""
|
||||
return _decode_source_image_file(fingerprint[0])
|
||||
|
||||
|
||||
def _load_source_image(path: str | os.PathLike[str]) -> PIL.Image.Image:
|
||||
"""Decode the source image at ``path``, reusing the last decode while the file is unchanged.
|
||||
|
||||
Every caller gets its own copy, so a request that mutates its image cannot reach the next one. A path
|
||||
that cannot be stat'ed goes straight to the uncached decode, which owns the validation error contract.
|
||||
"""
|
||||
try:
|
||||
fingerprint = _source_image_fingerprint(path)
|
||||
except (OSError, TypeError, ValueError):
|
||||
return _decode_source_image_file(path)
|
||||
return _decode_source_image_fingerprinted(fingerprint).copy()
|
||||
|
||||
|
||||
def _decode_source_image_file(path: str | os.PathLike[str]) -> PIL.Image.Image:
|
||||
try:
|
||||
source_image = PIL.Image.open(path)
|
||||
except (OSError, SyntaxError, ValueError, PIL.Image.DecompressionBombError):
|
||||
@@ -483,6 +560,9 @@ class LingBotWorldCausalDMDPipeline(
|
||||
"""LingBot-World v2 I2V generation with a request-local causal cache."""
|
||||
|
||||
supports_step_execution: ClassVar[bool] = True
|
||||
# A realtime block is four probes and a commit over one prepared chunk
|
||||
# state; nothing between those steps needs the serving scheduler.
|
||||
supports_chunk_step_grouping: ClassVar[bool] = True
|
||||
_dit_modules: ClassVar[list[str]] = ["transformer"]
|
||||
_encoder_modules: ClassVar[list[str]] = ["text_encoder"]
|
||||
_vae_modules: ClassVar[list[str]] = ["vae"]
|
||||
@@ -637,12 +717,21 @@ class LingBotWorldCausalDMDPipeline(
|
||||
raise ValueError("LingBot AR-Diffusion cache needs a positive recent window after reserving sink frames.")
|
||||
condition_latent_frames = int(self.transformer.config.num_frames_per_block)
|
||||
condition_channels = self.vae_scale_factor_temporal + int(self.transformer.config.out_channels)
|
||||
element_size = torch.empty((), dtype=self.transformer.dtype).element_size()
|
||||
condition_bytes_per_session = (
|
||||
condition_channels
|
||||
* condition_latent_frames
|
||||
* latent_height
|
||||
* latent_width
|
||||
* torch.empty((), dtype=self.transformer.dtype).element_size()
|
||||
condition_channels * condition_latent_frames * latent_height * latent_width * element_size
|
||||
)
|
||||
# Each layer retains scale and shift for the rank-local camera tokens
|
||||
# across all forwards of a block, including between scheduler calls.
|
||||
camera_tokens = tokens_per_frame * condition_latent_frames
|
||||
sp_size = int(self.od_config.parallel_config.ulysses_degree)
|
||||
camera_bytes_per_session = (
|
||||
2
|
||||
* int(self.transformer.config.num_layers)
|
||||
* ((camera_tokens + sp_size - 1) // sp_size)
|
||||
* int(self.transformer.config.num_attention_heads)
|
||||
* int(self.transformer.config.attention_head_dim)
|
||||
* element_size
|
||||
)
|
||||
return ARDiffusionKVCacheSpec(
|
||||
num_layers=int(self.transformer.config.num_layers),
|
||||
@@ -658,10 +747,14 @@ class LingBotWorldCausalDMDPipeline(
|
||||
ARDiffusionCrossAttentionKVSpec(
|
||||
self._AR_TEXT_CACHE,
|
||||
_MAX_SEQUENCE_LENGTH,
|
||||
# Cross-attention keeps every local head on every rank rather than sharding heads across
|
||||
# the Ulysses group, so the text pool holds that many heads, not the self-attention share.
|
||||
num_kv_heads=int(self.transformer.blocks[0].cross_attn.num_local_heads),
|
||||
),
|
||||
),
|
||||
model_owned_state_bytes_per_session=(
|
||||
condition_bytes_per_session
|
||||
+ camera_bytes_per_session
|
||||
+ 2 * self._condition_encoder_cache_bytes() # Committed and in-flight histories.
|
||||
+ self._streaming_decode_bytes_per_session()
|
||||
),
|
||||
@@ -733,6 +826,7 @@ class LingBotWorldCausalDMDPipeline(
|
||||
if state is not None:
|
||||
state.encoder_cache = None
|
||||
state.pending_encoder_cache = None
|
||||
state.condition_fixed_point = None
|
||||
|
||||
def reset_ar_diffusion_session(self, session_id: str) -> None:
|
||||
self._release_condition_encoder_state(session_id)
|
||||
@@ -988,6 +1082,50 @@ class LingBotWorldCausalDMDPipeline(
|
||||
)
|
||||
return condition
|
||||
|
||||
def _next_condition_chunk(
|
||||
self,
|
||||
inputs: _LingBotRequestInputs,
|
||||
*,
|
||||
start_frame: int,
|
||||
session_state: _LingBotARSessionState,
|
||||
dtype: torch.dtype,
|
||||
) -> tuple[torch.Tensor, list[torch.Tensor | None]]:
|
||||
"""The session's condition for the block at ``start_frame``, encoding only while it still changes.
|
||||
|
||||
After a session's opening block the encoder is fed the same all-zero
|
||||
pixel frames on every block, so the only thing that can make one
|
||||
block's condition differ from the last is the encoder's own history.
|
||||
The history is a causal cache with finite temporal reach, and once the
|
||||
zero frames have flushed the source image out of it the encoder is at
|
||||
a fixed point: identical input and identical state produce the same
|
||||
output and the same state on every later block. That fixed point is
|
||||
detected, not assumed after some number of blocks: a block whose
|
||||
returned history is bit-identical to the history it was given is the
|
||||
proof, and from then on the stored condition is returned and the
|
||||
encoder is not run. Measured at 480x832 the encode is 178 ms of a
|
||||
block and the fixed point arrives by the eleventh; the served
|
||||
rollouts this was measured on (40 blocks) are then byte-identical.
|
||||
|
||||
The opening block resets the fixed point, so a session that restarts
|
||||
from a new source image encodes again until it settles.
|
||||
"""
|
||||
if start_frame == 0:
|
||||
session_state.condition_fixed_point = None
|
||||
elif session_state.condition_fixed_point is not None:
|
||||
assert session_state.encoder_cache is not None
|
||||
# The history is unchanged by construction; the pending copy is
|
||||
# what post_decode commits, exactly as an encoded block's would be.
|
||||
return session_state.condition_fixed_point.clone(), list(session_state.encoder_cache)
|
||||
condition, pending_cache = self._prepare_condition_chunk(
|
||||
inputs,
|
||||
start_frame=start_frame,
|
||||
encoder_cache=session_state.encoder_cache,
|
||||
dtype=dtype,
|
||||
)
|
||||
if start_frame > 0 and _encoder_caches_identical(session_state.encoder_cache, pending_cache):
|
||||
session_state.condition_fixed_point = condition.clone()
|
||||
return condition, pending_cache
|
||||
|
||||
@torch.no_grad()
|
||||
def _prepare_condition_chunk(
|
||||
self,
|
||||
@@ -1160,10 +1298,8 @@ class LingBotWorldCausalDMDPipeline(
|
||||
for block in self.transformer.blocks:
|
||||
cross_attention = block.cross_attn
|
||||
shape = (cross_attention.num_local_heads, cross_attention.head_dim)
|
||||
key = cross_attention.shard_kv_heads(
|
||||
cross_attention.norm_k(cross_attention.k(projected_text)).unflatten(2, shape)
|
||||
)
|
||||
value = cross_attention.shard_kv_heads(cross_attention.v(projected_text).unflatten(2, shape))
|
||||
key = cross_attention.norm_k(cross_attention.k(projected_text)).unflatten(2, shape)
|
||||
value = cross_attention.v(projected_text).unflatten(2, shape)
|
||||
yield key, value
|
||||
|
||||
state.populate_cross_attention(
|
||||
@@ -1243,6 +1379,34 @@ class LingBotWorldCausalDMDPipeline(
|
||||
progress_bar=progress_bar,
|
||||
)
|
||||
|
||||
def _session_text_caches(
|
||||
self,
|
||||
prompt_embeds: torch.Tensor,
|
||||
*,
|
||||
prompt: str,
|
||||
max_sequence_length: int,
|
||||
session_state: _LingBotARSessionState,
|
||||
) -> list[LingBotAttentionCache]:
|
||||
"""Runner-owned cross-attention K/V for this tick, rebuilt when its inputs change.
|
||||
|
||||
The K/V is keyed by everything that defines the prompt embedding
|
||||
(text, sequence length, dtype). The first build of a session needs no
|
||||
invalidation (the runner state is empty); a key change does. The dirty
|
||||
flag is raised for the duration of the build and cleared only after
|
||||
``_ar_text_caches`` returns, so a build that fails after invalidating
|
||||
or partially writing the caches makes the next tick, whatever its
|
||||
prompt, invalidate again instead of consuming partial K/V.
|
||||
"""
|
||||
text_key = (prompt, int(max_sequence_length), str(prompt_embeds.dtype))
|
||||
previous = session_state.text_cache_key
|
||||
invalidate = session_state.text_cache_dirty or (previous is not None and previous != text_key)
|
||||
session_state.text_cache_dirty = True
|
||||
session_state.text_cache_key = None
|
||||
caches = self._ar_text_caches(prompt_embeds, invalidate=invalidate)
|
||||
session_state.text_cache_key = text_key
|
||||
session_state.text_cache_dirty = False
|
||||
return caches
|
||||
|
||||
def encode_prompt(
|
||||
self,
|
||||
prompt: str,
|
||||
@@ -1345,6 +1509,9 @@ class LingBotWorldCausalDMDPipeline(
|
||||
)
|
||||
dtype = self.transformer.dtype
|
||||
# Phase 1: turn all three user inputs into DiT-ready conditions.
|
||||
# ``encode_prompt`` reuses an already-encoded prompt itself; the
|
||||
# runner-owned cross-attention K/V is keyed the same way in
|
||||
# ``_session_text_caches`` below.
|
||||
prompt_embeds = self.encode_prompt(
|
||||
inputs.prompt,
|
||||
max_sequence_length=inputs.max_sequence_length,
|
||||
@@ -1354,10 +1521,10 @@ class LingBotWorldCausalDMDPipeline(
|
||||
condition = self._prepare_condition(inputs, dtype=dtype)
|
||||
else:
|
||||
assert session_state is not None
|
||||
condition, pending_encoder_cache = self._prepare_condition_chunk(
|
||||
condition, pending_encoder_cache = self._next_condition_chunk(
|
||||
inputs,
|
||||
start_frame=tick.chunk_index * block_frames,
|
||||
encoder_cache=session_state.encoder_cache,
|
||||
session_state=session_state,
|
||||
dtype=dtype,
|
||||
)
|
||||
camera_pitch: float | None = None
|
||||
@@ -1396,12 +1563,13 @@ class LingBotWorldCausalDMDPipeline(
|
||||
)
|
||||
else:
|
||||
assert session_state is not None
|
||||
prompt_changed = session_state.prompt is not None and session_state.prompt != inputs.prompt
|
||||
if session_state.generator_state is not None:
|
||||
inputs.generator.set_state(session_state.generator_state)
|
||||
ar_cross_attention = self._ar_text_caches(
|
||||
ar_cross_attention = self._session_text_caches(
|
||||
prompt_embeds,
|
||||
invalidate=prompt_changed,
|
||||
prompt=inputs.prompt,
|
||||
max_sequence_length=inputs.max_sequence_length,
|
||||
session_state=session_state,
|
||||
)
|
||||
generated_blocks: list[torch.Tensor] = []
|
||||
total_steps = (inputs.num_latent_frames // block_frames) * len(LINGBOT_DMD_TIMESTEPS)
|
||||
@@ -1437,7 +1605,6 @@ class LingBotWorldCausalDMDPipeline(
|
||||
cache = None
|
||||
if tick is not None:
|
||||
assert session_state is not None
|
||||
session_state.prompt = inputs.prompt
|
||||
session_state.generator_state = inputs.generator.get_state()
|
||||
session_state.encoder_cache = pending_encoder_cache
|
||||
session_state.camera_tail = camera_tail
|
||||
@@ -1536,11 +1703,15 @@ class LingBotWorldCausalDMDPipeline(
|
||||
if state is not None:
|
||||
state.release()
|
||||
|
||||
def _decode_chunk_to_pixels(
|
||||
self, latents: torch.Tensor, *, output_type: str, session_id: str | None = None
|
||||
) -> torch.Tensor | np.ndarray | list[list[PIL.Image.Image]]:
|
||||
def _decode_chunk_to_pixels(self, latents: torch.Tensor, *, session_id: str | None = None) -> np.ndarray:
|
||||
"""Decode one AR block so streaming consumers receive pixels, not latents.
|
||||
|
||||
The block comes back as ``(F, H, W, 3)`` uint8 whatever pixel output
|
||||
type the request named: the realtime endpoint muxes uint8 frames, so
|
||||
``"pil"`` and ``"np"`` only ever differed in how much work stood
|
||||
between the decoder and those bytes. Only ``"latent"`` means
|
||||
something else, and the caller keeps latents before reaching here.
|
||||
|
||||
With a ``session_id`` the block is decoded through that session's own
|
||||
temporal cache, so chunk *N + 1* continues chunk *N*: the causal decoder
|
||||
expands the session's opening latent frame to one raw frame and every
|
||||
@@ -1566,7 +1737,7 @@ class LingBotWorldCausalDMDPipeline(
|
||||
video = self.vae.decode(vae_latents, return_dict=False)[0]
|
||||
else:
|
||||
video = self._streaming_decode_chunk(decoder, vae_latents, cast(str, session_id))
|
||||
return self._video_processor().postprocess_video(video, output_type=output_type)
|
||||
return _uint8_frames(video)
|
||||
|
||||
def _streaming_decode_chunk(
|
||||
self, decoder: WanStreamingDecoder, vae_latents: torch.Tensor, session_id: str
|
||||
@@ -1587,15 +1758,6 @@ class LingBotWorldCausalDMDPipeline(
|
||||
self._release_streaming_decode_state(session_id)
|
||||
raise
|
||||
|
||||
def _video_processor(self) -> VideoProcessor:
|
||||
processor = getattr(self, "_cached_video_processor", None)
|
||||
if processor is None:
|
||||
from diffusers.video_processor import VideoProcessor
|
||||
|
||||
processor = VideoProcessor(vae_scale_factor=self.vae_scale_factor_spatial)
|
||||
self._cached_video_processor = processor
|
||||
return processor
|
||||
|
||||
def _require_bound_ar_state(self) -> None:
|
||||
if self._ar_diffusion_kv_state is None:
|
||||
raise RuntimeError("LingBot step execution requires AR-Diffusion session binding.")
|
||||
@@ -1683,10 +1845,10 @@ class LingBotWorldCausalDMDPipeline(
|
||||
session_state = self._ar_sessions[state.request_id]
|
||||
if session_state.next_chunk_index != state.chunk_index:
|
||||
raise ValueError("LingBot condition chunks must be contiguous within the session.")
|
||||
condition, pending_encoder_cache = self._prepare_condition_chunk(
|
||||
condition, pending_encoder_cache = self._next_condition_chunk(
|
||||
inputs,
|
||||
start_frame=start_frame,
|
||||
encoder_cache=session_state.encoder_cache,
|
||||
session_state=session_state,
|
||||
dtype=extra["dtype"],
|
||||
)
|
||||
previous = extra.get("camera_tail")
|
||||
@@ -1780,6 +1942,12 @@ class LingBotWorldCausalDMDPipeline(
|
||||
raise ValueError("LingBot World requires the runner-provided torch.Generator.")
|
||||
extra["condition"] = condition
|
||||
extra["camera"] = camera
|
||||
# One camera-modulation cache per block: the four probes and the
|
||||
# commit below are separate scheduler steps, so the block runner
|
||||
# cannot open the reuse window around them the way generate_block
|
||||
# does around its own loop. Holding the cache here makes the window
|
||||
# span exactly this block, per request.
|
||||
extra["camera_cache"] = CameraModulationCache()
|
||||
extra["camera_tail"] = camera_tail
|
||||
extra["start_frame"] = start_frame
|
||||
extra["ar_cross_attention"] = self._ar_text_caches(
|
||||
@@ -1838,6 +2006,7 @@ class LingBotWorldCausalDMDPipeline(
|
||||
start_frame=int(extra["start_frame"]),
|
||||
timestep_value=float(schedule[step_in_chunk][0]),
|
||||
step_index=step_in_chunk,
|
||||
camera_cache=extra["camera_cache"],
|
||||
)
|
||||
|
||||
def step_scheduler(self, state: StepRequestState, noise_pred: torch.Tensor, **kwargs: Any) -> None:
|
||||
@@ -1880,6 +2049,7 @@ class LingBotWorldCausalDMDPipeline(
|
||||
cache=None,
|
||||
ar=self._ar_block_context(extra["ar_cross_attention"]),
|
||||
start_frame=int(extra["start_frame"]),
|
||||
camera_cache=extra.pop("camera_cache"),
|
||||
)
|
||||
completed_chunk_index = state.chunk_index
|
||||
inputs: _LingBotRequestInputs = extra["inputs"]
|
||||
@@ -1889,13 +2059,7 @@ class LingBotWorldCausalDMDPipeline(
|
||||
if inputs.output_type == "latent":
|
||||
payload: dict[str, Any] = {"latents": latents}
|
||||
else:
|
||||
payload = {
|
||||
"video": self._decode_chunk_to_pixels(
|
||||
latents,
|
||||
output_type=inputs.output_type,
|
||||
session_id=state.request_id,
|
||||
)
|
||||
}
|
||||
payload = {"video": self._decode_chunk_to_pixels(latents, session_id=state.request_id)}
|
||||
output = {
|
||||
"payload": payload,
|
||||
"metadata": {
|
||||
|
||||
@@ -24,7 +24,7 @@ from vllm.model_executor.model_loader.weight_utils import default_weight_loader
|
||||
from vllm.model_executor.utils import set_weight_attrs
|
||||
|
||||
from vllm_omni.diffusion.attention.layer import Attention
|
||||
from vllm_omni.diffusion.distributed.comm import SeqAllToAll4D
|
||||
from vllm_omni.diffusion.distributed.comm import SeqAllToAll4D, all_to_all_5D
|
||||
from vllm_omni.diffusion.distributed.parallel_state import get_sp_group
|
||||
from vllm_omni.diffusion.distributed.sp_plan import SequenceParallelInput, SequenceParallelOutput
|
||||
from vllm_omni.diffusion.layers.norm import LayerNorm
|
||||
@@ -329,9 +329,18 @@ class LingBotSelfAttention(nn.Module):
|
||||
key = self.rotary_embedding(key, cos, sin)
|
||||
|
||||
if self.ulysses_world_size > 1:
|
||||
query = SeqAllToAll4D.apply(self.ulysses_group, query, 2, 1, False)
|
||||
key = SeqAllToAll4D.apply(self.ulysses_group, key, 2, 1, False)
|
||||
value = SeqAllToAll4D.apply(self.ulysses_group, value, 2, 1, False)
|
||||
# One fused sequence->head all-to-all for q/k/v instead of three:
|
||||
# (B, S/N, 3, H, D) -> (B, S, 3, H/N, D). Same head-group assignment
|
||||
# as the per-tensor 4D exchange (rank r receives head group r), so
|
||||
# the result is a pure permutation of the three-call version, with a
|
||||
# 3x larger message and one third of the latency-bound collectives.
|
||||
qkv = all_to_all_5D(
|
||||
torch.stack((query, key, value), dim=2),
|
||||
scatter_idx=3,
|
||||
gather_idx=1,
|
||||
group=self.ulysses_group,
|
||||
)
|
||||
query, key, value = qkv.unbind(2)
|
||||
|
||||
if isinstance(cache, ARDiffusionPagedLayerInputs):
|
||||
if query.shape[0] != 1:
|
||||
@@ -421,13 +430,11 @@ class LingBotCrossAttention(nn.Module):
|
||||
self.num_heads = num_heads
|
||||
self.head_dim = dim // num_heads
|
||||
self.num_local_heads = num_heads // tp_size
|
||||
self.ulysses_world_size, self.ulysses_rank, self.ulysses_group = _ulysses_state()
|
||||
if self.num_local_heads % self.ulysses_world_size:
|
||||
raise ValueError(
|
||||
"LingBot local cross-attention heads must be divisible by the Ulysses degree: "
|
||||
f"heads={self.num_local_heads}, ulysses={self.ulysses_world_size}."
|
||||
)
|
||||
self.num_sp_heads = self.num_local_heads // self.ulysses_world_size
|
||||
# Cross-attention keeps every local head on every Ulysses rank and attends the rank's own sequence
|
||||
# shard. Each query token attends the whole text sequence independently of the others, so there is
|
||||
# nothing to exchange: the sequence split Ulysses already gives us is the only split needed, and both
|
||||
# all-to-all collectives -- one for the query, one for the output -- disappear. The cost is that the text K/V is
|
||||
# replicated per rank instead of head-sharded; ar_diffusion_kv_cache_spec sizes the pool for it.
|
||||
self.tp_inner_dim = self.num_local_heads * self.head_dim
|
||||
|
||||
self.q = ColumnParallelLinear(
|
||||
@@ -465,9 +472,9 @@ class LingBotCrossAttention(nn.Module):
|
||||
self.norm_q = _LingBotRMSNorm(self.tp_inner_dim, eps)
|
||||
self.norm_k = _LingBotRMSNorm(self.tp_inner_dim, eps)
|
||||
self.attn = Attention(
|
||||
num_heads=self.num_sp_heads,
|
||||
num_heads=self.num_local_heads,
|
||||
head_size=self.head_dim,
|
||||
num_kv_heads=self.num_sp_heads,
|
||||
num_kv_heads=self.num_local_heads,
|
||||
softmax_scale=self.head_dim**-0.5,
|
||||
causal=False,
|
||||
role="cross",
|
||||
@@ -477,14 +484,6 @@ class LingBotCrossAttention(nn.Module):
|
||||
disable_kv_quant=True,
|
||||
)
|
||||
|
||||
def shard_kv_heads(self, value: torch.Tensor) -> torch.Tensor:
|
||||
"""Select this Ulysses rank's projected K/V heads."""
|
||||
|
||||
if self.ulysses_world_size == 1:
|
||||
return value
|
||||
start = self.ulysses_rank * self.num_sp_heads
|
||||
return value[:, :, start : start + self.num_sp_heads].clone(memory_format=torch.contiguous_format)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
@@ -494,8 +493,6 @@ class LingBotCrossAttention(nn.Module):
|
||||
) -> tuple[torch.Tensor, LingBotAttentionCache]:
|
||||
query = self.norm_q(self.q(hidden_states))
|
||||
query = query.unflatten(2, (self.num_local_heads, self.head_dim))
|
||||
if self.ulysses_world_size > 1:
|
||||
query = SeqAllToAll4D.apply(self.ulysses_group, query, 2, 1, False)
|
||||
|
||||
# Text K/V is constant within a request and is projected once per layer.
|
||||
if cache is None:
|
||||
@@ -503,8 +500,8 @@ class LingBotCrossAttention(nn.Module):
|
||||
raise ValueError("encoder_hidden_states are required when the cross-attention cache is empty.")
|
||||
key = self.norm_k(self.k(encoder_hidden_states))
|
||||
value = self.v(encoder_hidden_states)
|
||||
key = self.shard_kv_heads(key.unflatten(2, (self.num_local_heads, self.head_dim)))
|
||||
value = self.shard_kv_heads(value.unflatten(2, (self.num_local_heads, self.head_dim)))
|
||||
key = key.unflatten(2, (self.num_local_heads, self.head_dim))
|
||||
value = value.unflatten(2, (self.num_local_heads, self.head_dim))
|
||||
cache = LingBotAttentionCache(
|
||||
key=key,
|
||||
value=value,
|
||||
@@ -517,8 +514,6 @@ class LingBotCrossAttention(nn.Module):
|
||||
value = cache.value[:, : cache.end]
|
||||
|
||||
output = self.attn(query, key, value)
|
||||
if self.ulysses_world_size > 1:
|
||||
output = SeqAllToAll4D.apply(self.ulysses_group, output, 1, 2, False)
|
||||
return self.o(output.flatten(2, 3)), cache
|
||||
|
||||
|
||||
@@ -592,6 +587,16 @@ class LingBotAttentionBlock(nn.Module):
|
||||
self.cam_scale_layer = nn.Linear(dim, dim)
|
||||
self.cam_shift_layer = nn.Linear(dim, dim)
|
||||
|
||||
def camera_modulation(self, camera_hidden_states: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Build this block's camera scale and shift from the patchified camera tokens.
|
||||
|
||||
Depends only on ``camera_hidden_states``, so a caller holding the camera trajectory fixed -- every
|
||||
denoise forward of one AR chunk does -- can build this once and pass it back through ``forward``.
|
||||
"""
|
||||
camera_features = self.cam_injector_layer2(F.silu(self.cam_injector_layer1(camera_hidden_states)))
|
||||
camera_features = camera_features + camera_hidden_states
|
||||
return self.cam_scale_layer(camera_features), self.cam_shift_layer(camera_features)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
@@ -605,6 +610,7 @@ class LingBotAttentionBlock(nn.Module):
|
||||
sink_tokens: int,
|
||||
update_cache: bool,
|
||||
rotary_emb: tuple[torch.Tensor, torch.Tensor],
|
||||
camera_modulation: tuple[torch.Tensor, torch.Tensor] | None = None,
|
||||
) -> tuple[torch.Tensor, LingBotAttentionCache]:
|
||||
batch_size, token_count, dim = hidden_states.shape
|
||||
if (
|
||||
@@ -638,10 +644,9 @@ class LingBotAttentionBlock(nn.Module):
|
||||
hidden_grid = hidden_grid + attention_output.unflatten(1, (num_frames, tokens_per_frame)) * gate_msa
|
||||
hidden_states = hidden_grid.flatten(1, 2).to(hidden_states.dtype)
|
||||
|
||||
camera_features = self.cam_injector_layer2(F.silu(self.cam_injector_layer1(camera_hidden_states)))
|
||||
camera_features = camera_features + camera_hidden_states
|
||||
camera_scale = self.cam_scale_layer(camera_features)
|
||||
camera_shift = self.cam_shift_layer(camera_features)
|
||||
camera_scale, camera_shift = (
|
||||
self.camera_modulation(camera_hidden_states) if camera_modulation is None else camera_modulation
|
||||
)
|
||||
hidden_states = ((1 + camera_scale) * hidden_states + camera_shift).to(hidden_states.dtype)
|
||||
|
||||
attention_output, cross_cache = self.cross_attn(
|
||||
@@ -763,6 +768,22 @@ def _rope_axis(max_seq_len: int, dim: int, *, start: int = 0) -> tuple[torch.Ten
|
||||
return phase.cos().float(), phase.sin().float()
|
||||
|
||||
|
||||
class CameraModulationCache:
|
||||
"""Per-block camera scale and shift, held by the caller across the forwards of one AR block.
|
||||
|
||||
The camera injector is four projections and a SiLU per block and reads only the camera tokens, which do
|
||||
not change between the denoise steps and the clean commit of one block. The caller that owns that
|
||||
invariant creates one of these per block and passes it to every forward of the block; ``forward`` fills a
|
||||
block's entry on first use and reads it after. Its lifetime is the caller's: the AR runner keeps one per
|
||||
block, ``generate_block`` one per loop, and nothing here checks the camera tensor for change.
|
||||
"""
|
||||
|
||||
__slots__ = ("entries",)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.entries: dict[int, tuple[torch.Tensor, torch.Tensor]] = {}
|
||||
|
||||
|
||||
class CausalLingBotWorldTransformer3DModel(nn.Module):
|
||||
"""Checkpoint-compatible causal LingBot World video transformer."""
|
||||
|
||||
@@ -922,6 +943,27 @@ class CausalLingBotWorldTransformer3DModel(nn.Module):
|
||||
cosine, sine = _rope_axis(rope_max_seq_len, axis_dim)
|
||||
self.register_buffer(f"_rope_{axis}_cosine", cosine, persistent=False)
|
||||
self.register_buffer(f"_rope_{axis}_sine", sine, persistent=False)
|
||||
# One static-address buffer per shape for the per-token timestep
|
||||
# projection; see _stage_timestep_projection.
|
||||
self._timestep_projection_buffers: dict[tuple[tuple[int, ...], torch.dtype, torch.device], torch.Tensor] = {}
|
||||
|
||||
def _stage_timestep_projection(self, projection: torch.Tensor) -> torch.Tensor:
|
||||
"""Hand the blocks the timestep projection from a buffer whose address never changes.
|
||||
|
||||
Every regional block graph takes the projection as an input. Under CUDA-graph replay an input that
|
||||
arrives in a fresh tensor each forward is copied into the graph's placeholder on every replay: at
|
||||
480x832 with four Ulysses ranks that is a 72 MB copy per block, forty times per forward, two hundred
|
||||
times per AR chunk. Copying the projection once per forward into a buffer marked static lets every
|
||||
replay read it in place. The values the blocks see are the same; only where they live changes.
|
||||
"""
|
||||
key = (tuple(projection.shape), projection.dtype, projection.device)
|
||||
buffer = self._timestep_projection_buffers.get(key)
|
||||
if buffer is None:
|
||||
buffer = torch.empty(projection.shape, dtype=projection.dtype, device=projection.device)
|
||||
torch._dynamo.mark_static_address(buffer)
|
||||
self._timestep_projection_buffers[key] = buffer
|
||||
buffer.copy_(projection)
|
||||
return buffer
|
||||
|
||||
@property
|
||||
def dtype(self) -> torch.dtype:
|
||||
@@ -1178,6 +1220,7 @@ class CausalLingBotWorldTransformer3DModel(nn.Module):
|
||||
cache: LingBotTransformerCache,
|
||||
start_frame: int,
|
||||
update_cache: bool,
|
||||
camera_modulation_cache: CameraModulationCache | None = None,
|
||||
) -> torch.Tensor:
|
||||
if not isinstance(update_cache, bool):
|
||||
raise ValueError("update_cache must be a boolean.")
|
||||
@@ -1272,16 +1315,25 @@ class CausalLingBotWorldTransformer3DModel(nn.Module):
|
||||
"LingBot SP input hooks did not shard all conditioning tensors consistently; "
|
||||
f"expected {local_tokens} tokens per rank. Check SP hook registration and input dimensions."
|
||||
)
|
||||
timestep_projection = self._stage_timestep_projection(timestep_projection)
|
||||
rotary_emb = (cosine, sine)
|
||||
# Phase 3: each layer receives its own cache entry. Text K/V is passed
|
||||
# only when absent; the returned cache is stored for subsequent DMD
|
||||
# steps and causal blocks in this request.
|
||||
# steps and causal blocks in this request. A caller-held camera cache
|
||||
# gives every block its modulation once per AR block.
|
||||
for index, block in enumerate(self.blocks):
|
||||
camera_modulation = None
|
||||
if camera_modulation_cache is not None:
|
||||
camera_modulation = camera_modulation_cache.entries.get(index)
|
||||
if camera_modulation is None:
|
||||
camera_modulation = block.camera_modulation(camera_hidden_states)
|
||||
camera_modulation_cache.entries[index] = camera_modulation
|
||||
hidden_states, cross_cache = block(
|
||||
hidden_states,
|
||||
projected_text if cache.cross_attention[index] is None else None,
|
||||
timestep_projection,
|
||||
camera_hidden_states,
|
||||
camera_modulation=camera_modulation,
|
||||
self_cache=cache.self_attention[index],
|
||||
cross_cache=cache.cross_attention[index],
|
||||
current_start=current_start,
|
||||
|
||||
Reference in new issue
Block a user