[diffusion] feat: add LingBot realtime prompt, KV window, and lazy VAE controls (#30040)

This commit is contained in:
HuangJi
2026-07-05 11:11:54 +08:00
committed by GitHub
parent fbe3110866
commit 5e6f49c986
13 changed files with 1286 additions and 57 deletions
@@ -11,6 +11,7 @@ from typing import Any
import numpy as np
import torch
from sglang.multimodal_gen import envs
from sglang.multimodal_gen.configs.models import DiTConfig
from sglang.multimodal_gen.configs.models.dits import LingBotWorldVideoConfig
from sglang.multimodal_gen.configs.pipeline_configs.wan import Wan2_2_I2V_A14B_Config
@@ -325,6 +326,44 @@ class LingBotWorldCausalDMDConfig(LingBotWorldI2VConfig):
warp_denoising_step: bool = True
realtime_causal_sink_size: int | None = None
realtime_causal_kv_cache_num_frames: int | None = None
interactive_kv_window_enable: bool = False
interactive_kv_still_window: int | None = 3
interactive_kv_moving_window: int | None = 12
interactive_kv_still_chunks: int = 2
lazy_vae_encode_black_frames: int = 0
def preprocess_vae_encode(self, image, vae):
image = super().preprocess_vae_encode(image, vae)
lazy_black_frames = envs.SGLANG_LINGBOT_LAZY_VAE_ENCODE_BLACK_FRAMES
if lazy_black_frames is None:
lazy_black_frames = self.lazy_vae_encode_black_frames
lazy_black_frames = max(0, int(lazy_black_frames or 0))
if lazy_black_frames <= 0 or image.ndim != 5:
return image
num_frames = int(image.shape[2])
if num_frames <= 1:
return image
temporal_ratio = int(self.vae_config.arch_config.temporal_compression_ratio)
encode_frames = min(num_frames, 1 + lazy_black_frames)
if (encode_frames - 1) % temporal_ratio != 0:
encode_frames = (encode_frames - 1) // temporal_ratio + 1
encode_frames = encode_frames * temporal_ratio + 1
encode_frames = min(num_frames, encode_frames)
if encode_frames >= num_frames:
return image
logger.info(
"LingBot lazy VAE encode: pixel_frames=%s encode_pixel_frames=%s "
"black_frames=%s temporal_ratio=%s",
num_frames,
encode_frames,
lazy_black_frames,
temporal_ratio,
)
return image[:, :, :encode_frames].contiguous()
def postprocess_image_latent(self, latent_condition, batch):
"""Build condition tensor aligned to chunk_size (num_frames_per_block).
@@ -342,9 +381,31 @@ class LingBotWorldCausalDMDConfig(LingBotWorldI2VConfig):
latent_width = batch.width // spatial_ratio
# Align num_latent_frames to chunk_size
num_latent_frames = latent_condition.shape[2]
num_latent_frames = num_latent_frames - (num_latent_frames % chunk_size)
latent_condition = latent_condition[:, :, :num_latent_frames, :, :]
target_latent_frames = (int(batch.num_frames) - 1) // temporal_ratio + 1
target_latent_frames = target_latent_frames - (
target_latent_frames % chunk_size
)
encoded_latent_frames = int(latent_condition.shape[2])
if encoded_latent_frames < target_latent_frames:
tail = latent_condition[:, :, -1:, :, :].repeat(
1,
1,
target_latent_frames - encoded_latent_frames,
1,
1,
)
latent_condition = torch.cat([latent_condition, tail], dim=2)
elif encoded_latent_frames > target_latent_frames:
latent_condition = latent_condition[:, :, :target_latent_frames, :, :]
num_latent_frames = int(latent_condition.shape[2])
if encoded_latent_frames != num_latent_frames:
logger.info(
"LingBot lazy VAE condition: encoded_latent_frames=%s "
"target_latent_frames=%s",
encoded_latent_frames,
num_latent_frames,
)
# Number of initial frames that have actual image content
# (latent_condition from VAE encode of [image, zeros...])
+8
View File
@@ -57,6 +57,8 @@ if TYPE_CHECKING:
SGLANG_CACHE_DIT_SECONDARY_TS_ORDER: int = 1
# model loading
SGLANG_USE_RUNAI_MODEL_STREAMER: bool = True
SGLANG_LINGBOT_ENABLE_INTERACTIVE_KV_WINDOW: bool = False
SGLANG_LINGBOT_LAZY_VAE_ENCODE_BLACK_FRAMES: int | None = None
SGLANG_DIFFUSION_FLASHINFER_FP4_GEMM_BACKEND: str | None = None
SGLANG_DIFFUSION_ENABLE_W8A8_FP8_GEMM: bool = False
SGLANG_DIFFUSION_VAE_CHANNELS_LAST_3D: str = "auto"
@@ -295,6 +297,12 @@ environment_variables: dict[str, Callable[[], Any]] = {
"SGLANG_USE_RUNAI_MODEL_STREAMER": _lazy_bool(
"SGLANG_USE_RUNAI_MODEL_STREAMER", "true"
),
"SGLANG_LINGBOT_ENABLE_INTERACTIVE_KV_WINDOW": _lazy_bool(
"SGLANG_LINGBOT_ENABLE_INTERACTIVE_KV_WINDOW"
),
"SGLANG_LINGBOT_LAZY_VAE_ENCODE_BLACK_FRAMES": _lazy_int(
"SGLANG_LINGBOT_LAZY_VAE_ENCODE_BLACK_FRAMES"
),
# FlashInfer FP4 GEMM backend override for diffusion NVFP4.
# When unset, diffusion ModelOpt NVFP4 defaults to flashinfer_trtllm.
# Supported values:
@@ -14,7 +14,15 @@ from sglang.multimodal_gen.runtime.entrypoints.openai.realtime.realtime_adapter
build_realtime_sampling_params,
save_realtime_first_frame,
)
from sglang.multimodal_gen.runtime.realtime.control_signals import ControlSignalQueue
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.lingbot_world.constants import (
LINGBOT_CAMERA_ACTIONS_CONDITION,
LINGBOT_PROMPT_UPDATED_CONDITION,
)
from sglang.multimodal_gen.runtime.realtime.control_signals import (
ControlSignalQueue,
ParsedControlEventPayload,
parse_control_event_payload,
)
from sglang.multimodal_gen.runtime.realtime.states import (
RealtimeCameraControlState,
)
@@ -29,6 +37,7 @@ if TYPE_CHECKING:
LINGBOT_REALTIME_DEFAULT_NUM_INFERENCE_STEPS = 4
LINGBOT_REALTIME_MIN_CONDITION_CHUNKS = 2
COMPOSITE_INPUT_EVENT_KIND = "composite_input"
class LingBotWorldRealtimeState(RealtimeCameraControlState):
@@ -47,16 +56,44 @@ class LingBotWorldRealtimeState(RealtimeCameraControlState):
def receive_prompt(self, prompt: str, *, event_id: int | None = None) -> None:
self.prompt_queue.push("prompt", prompt, event_id=event_id)
def parse_camera_control_event_payload(
self,
payload: Any,
*,
event_id: int | None,
) -> ParsedControlEventPayload:
return parse_control_event_payload(
payload,
event_id=event_id,
kind="camera_actions",
normalize_state_payload=self._normalize_state_actions,
validate_script_payload=LingBotWorldRealtimeAdapter._validate_camera_actions,
)
def receive_parsed_camera_control_event_payload(
self,
parsed: ParsedControlEventPayload,
*,
event_id: int | None,
) -> str:
if parsed.mode == "state":
transitions = parsed.payload
self.receive_camera_state_transitions(transitions)
return f"kind=camera_actions, mode=state, transitions={len(transitions)}"
camera_actions = parsed.payload
self.receive_camera_action_script(camera_actions, event_id=event_id)
return f"kind=camera_actions, mode=script, frames={len(camera_actions)}"
def receive_camera_control_event_payload(
self,
payload: Any,
*,
event_id: int | None,
) -> str:
return super().receive_camera_control_event_payload(
payload,
event_id=event_id,
validate_camera_actions=LingBotWorldRealtimeAdapter._validate_camera_actions,
parsed = self.parse_camera_control_event_payload(payload, event_id=event_id)
return self.receive_parsed_camera_control_event_payload(
parsed, event_id=event_id
)
def sample_prompt(self) -> str:
@@ -86,7 +123,7 @@ class LingBotWorldRealtimeAdapter(BaseRealtimeModelAdapter):
request: RealtimeVideoGenerationsRequest,
) -> None:
condition_inputs = request.condition_inputs or {}
camera_actions = condition_inputs.get("camera_actions")
camera_actions = condition_inputs.get(LINGBOT_CAMERA_ACTIONS_CONDITION)
if camera_actions is not None:
state = self._state(session)
state.receive_camera_action_script(
@@ -113,17 +150,119 @@ class LingBotWorldRealtimeAdapter(BaseRealtimeModelAdapter):
) -> str:
state = self._state(session)
if event.kind == "camera_actions":
return state.receive_camera_control_event_payload(
event.payload,
event_id=event.event_id,
)
return self._ingest_camera_actions(state, event.payload, event.event_id)
elif event.kind == "prompt":
if not isinstance(event.payload, str) or not event.payload:
raise ValueError("prompt event payload must be a non-empty string")
state.receive_prompt(event.payload, event_id=event.event_id)
return f"kind=prompt, prompt_len={len(event.payload)}"
return self._ingest_prompt(state, event.payload, event.event_id)
elif event.kind == COMPOSITE_INPUT_EVENT_KIND:
return self._ingest_composite_input(state, event.payload, event.event_id)
raise ValueError(f"unsupported event kind: {event.kind}")
def _ingest_camera_actions(
self,
state: LingBotWorldRealtimeState,
payload: Any,
event_id: int | None,
) -> str:
return state.receive_camera_control_event_payload(
payload,
event_id=event_id,
)
def _ingest_prompt(
self,
state: LingBotWorldRealtimeState,
payload: Any,
event_id: int | None,
) -> str:
prompt = self._validate_prompt_payload(payload)
state.receive_prompt(prompt, event_id=event_id)
return f"kind=prompt, prompt_len={len(prompt)}"
@staticmethod
def _validate_prompt_payload(payload: Any) -> str:
if not isinstance(payload, str) or not payload:
raise ValueError("prompt event payload must be a non-empty string")
return payload
def _ingest_composite_input(
self,
state: LingBotWorldRealtimeState,
payload: Any,
event_id: int | None,
) -> str:
if not isinstance(payload, dict):
raise ValueError("composite_input event payload must be a map")
input_types = payload.get("input_types")
if not isinstance(input_types, list) or not input_types:
raise ValueError(
"composite_input event payload requires non-empty input_types"
)
parsed_inputs = []
for input_type in input_types:
if not isinstance(input_type, str) or not input_type:
raise ValueError(
"composite_input input_types must contain non-empty strings"
)
if input_type not in payload:
raise ValueError(f"composite_input event payload requires {input_type}")
parsed_inputs.append(
(
input_type,
self._parse_composite_input_item(
state,
input_type,
payload[input_type],
event_id,
),
)
)
input_logs = []
for input_type, parsed_payload in parsed_inputs:
input_logs.append(
self._ingest_parsed_composite_input_item(
state,
input_type,
parsed_payload,
event_id,
)
)
return f"kind=composite_input, inputs={input_logs}"
def _parse_composite_input_item(
self,
state: LingBotWorldRealtimeState,
input_type: str,
payload: Any,
event_id: int | None,
) -> Any:
if input_type == "camera_actions":
return state.parse_camera_control_event_payload(
payload,
event_id=event_id,
)
if input_type == "prompt":
return self._validate_prompt_payload(payload)
raise ValueError(f"unsupported composite_input type: {input_type}")
def _ingest_parsed_composite_input_item(
self,
state: LingBotWorldRealtimeState,
input_type: str,
parsed_payload: Any,
event_id: int | None,
) -> str:
if input_type == "camera_actions":
return state.receive_parsed_camera_control_event_payload(
parsed_payload,
event_id=event_id,
)
if input_type == "prompt":
state.receive_prompt(parsed_payload, event_id=event_id)
return f"kind=prompt, prompt_len={len(parsed_payload)}"
raise ValueError(f"unsupported composite_input type: {input_type}")
def sample_chunk_inputs(
self,
session: GenerateSession,
@@ -137,18 +276,22 @@ class LingBotWorldRealtimeAdapter(BaseRealtimeModelAdapter):
if request is None:
raise ValueError("realtime request is not initialized")
prompt_updated = False
if chunk.index == 0:
prompt = request.prompt
elif state.has_prompt():
prompt = state.sample_prompt()
request.prompt = prompt
prompt_updated = True
else:
prompt = request.prompt
condition_inputs = {}
if prompt_updated:
condition_inputs[LINGBOT_PROMPT_UPDATED_CONDITION] = True
camera_actions = state.sample_camera_actions(chunk_size)
if camera_actions is not None:
condition_inputs["camera_actions"] = camera_actions
condition_inputs[LINGBOT_CAMERA_ACTIONS_CONDITION] = camera_actions
return RealtimeChunkInputs(prompt=prompt, condition_inputs=condition_inputs)
def build_sampling_params(
@@ -110,6 +110,7 @@ class CausalSelfAttentionKVCache:
value: torch.Tensor,
current_chunk_start: int,
cache_head_start: int | None = None,
recent_window_tokens: int | None = None,
debug_name: str = "causal KV cache",
) -> CausalAttentionKVView:
"""write fresh kv into the cache, returns the part of view visible to the current chunk
@@ -118,6 +119,11 @@ class CausalSelfAttentionKVCache:
current_chunk_start: the global position of the start of the chunk
cache_head_start: first cache head for key/value when they only
carry a local slice of the cache heads; other heads are left untouched
recent_window_tokens: recent-window attention size. ``None``
returns the full visible attention window. ``0`` keeps only sink
tokens plus the current chunk. A positive value keeps sink tokens,
up to that many tokens before the current chunk, and the current
chunk. Negative values are invalid.
"""
num_new_tokens = key.shape[1]
@@ -265,17 +271,22 @@ class CausalSelfAttentionKVCache:
if cache_head_slice is None:
self.k[:, local_start_index:local_end_index] = key
self.v[:, local_start_index:local_end_index] = value
visible_k = self.k[:, attn_start_index:updated_local_end]
visible_v = self.v[:, attn_start_index:updated_local_end]
visible_k, visible_v = self._visible_attention_kv(
local_start_index=local_start_index,
updated_local_end=updated_local_end,
attn_start_index=attn_start_index,
recent_window_tokens=recent_window_tokens,
)
else:
self.k[:, local_start_index:local_end_index, cache_head_slice, :] = key
self.v[:, local_start_index:local_end_index, cache_head_slice, :] = value
visible_k = self.k[
:, attn_start_index:updated_local_end, cache_head_slice, :
]
visible_v = self.v[
:, attn_start_index:updated_local_end, cache_head_slice, :
]
visible_k, visible_v = self._visible_attention_kv(
local_start_index=local_start_index,
updated_local_end=updated_local_end,
attn_start_index=attn_start_index,
recent_window_tokens=recent_window_tokens,
cache_head_slice=cache_head_slice,
)
self._write_indices(
global_end_index=updated_global_end,
@@ -290,6 +301,100 @@ class CausalSelfAttentionKVCache:
visible_global_end=updated_global_end,
)
def _visible_attention_kv(
self,
*,
local_start_index: int,
updated_local_end: int,
attn_start_index: int,
recent_window_tokens: int | None,
cache_head_slice: slice | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Return the visible KV slice for the current attention call.
When ``recent_window_tokens`` is ``None``, the returned token range is
the standard sliding window::
[attn_start_index, updated_local_end)
When recent-window selection is enabled, ``recent_window_tokens`` must be
non-negative and the returned token ranges are::
sink_end = min(self.sink_tokens, updated_local_end)
recent_start = max(sink_end, local_start_index - recent_window_tokens)
[0, sink_end) + [recent_start, updated_local_end)
Thus ``0`` keeps only sink tokens plus the current chunk.
``cache_head_slice`` applies the same token ranges to a subset of KV
heads.
"""
if recent_window_tokens is None:
if cache_head_slice is None:
return (
self.k[:, attn_start_index:updated_local_end],
self.v[:, attn_start_index:updated_local_end],
)
return (
self.k[:, attn_start_index:updated_local_end, cache_head_slice, :],
self.v[:, attn_start_index:updated_local_end, cache_head_slice, :],
)
if recent_window_tokens < 0:
raise ValueError("recent_window_tokens must be non-negative or None")
sink_end = min(self.sink_tokens, updated_local_end)
recent_start = max(sink_end, local_start_index - recent_window_tokens)
if recent_start <= sink_end:
if cache_head_slice is None:
return self.k[:, :updated_local_end], self.v[:, :updated_local_end]
return (
self.k[:, :updated_local_end, cache_head_slice, :],
self.v[:, :updated_local_end, cache_head_slice, :],
)
if sink_end <= 0:
if cache_head_slice is None:
return (
self.k[:, recent_start:updated_local_end],
self.v[:, recent_start:updated_local_end],
)
return (
self.k[:, recent_start:updated_local_end, cache_head_slice, :],
self.v[:, recent_start:updated_local_end, cache_head_slice, :],
)
if cache_head_slice is None:
return (
torch.cat(
[
self.k[:, :sink_end],
self.k[:, recent_start:updated_local_end],
],
dim=1,
),
torch.cat(
[
self.v[:, :sink_end],
self.v[:, recent_start:updated_local_end],
],
dim=1,
),
)
return (
torch.cat(
[
self.k[:, :sink_end, cache_head_slice, :],
self.k[:, recent_start:updated_local_end, cache_head_slice, :],
],
dim=1,
),
torch.cat(
[
self.v[:, :sink_end, cache_head_slice, :],
self.v[:, recent_start:updated_local_end, cache_head_slice, :],
],
dim=1,
),
)
@dataclass(slots=True)
class CrossAttentionKVCache:
@@ -79,6 +79,13 @@ from sglang.multimodal_gen.runtime.models.dits.wanvideo import (
WanTransformer3DModel,
)
from sglang.multimodal_gen.runtime.models.utils import _use_aiter
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.lingbot_world.constants import (
LINGBOT_C2WS_PLUCKER_EMB_CACHE,
LINGBOT_CAM_CONDITIONER_CACHE,
LINGBOT_ROPE_CACHE,
LINGBOT_SEQUENCE_SHARD_ROPE_CACHE,
LINGBOT_TIME_EMBEDDINGS_CACHE,
)
from sglang.multimodal_gen.runtime.platforms import (
AttentionBackendEnum,
current_platform,
@@ -299,6 +306,11 @@ class LingBotWorldCausalSelfAttention(CausalWanSelfAttention):
value=v,
current_chunk_start=current_start,
cache_head_start=cache_head_start,
recent_window_tokens=(
None
if update_cache_only
else getattr(forward_batch, "realtime_causal_kv_sample_tokens", None)
),
debug_name="LingBot KV cache",
)
if update_cache_only:
@@ -998,7 +1010,7 @@ class CausalLingBotWorldTransformerBlock(CausalWanTransformerBlock):
return self.cam_conditioner.compute_scale_shift(c2ws_plucker_emb)
cache = CausalLingBotWorldTransformer3DModel._get_request_cache(
forward_batch, "lingbot_cam_conditioner"
forward_batch, LINGBOT_CAM_CONDITIONER_CACHE
)
if cache is None:
return self.cam_conditioner.compute_scale_shift(c2ws_plucker_emb)
@@ -1211,7 +1223,7 @@ class CausalLingBotWorldTransformer3DModel(CausalWanTransformer3DModel):
) -> torch.Tensor | None:
if c2ws_plucker_emb is None:
return None
cache = self._get_request_cache(forward_batch, "lingbot_c2ws_plucker_emb")
cache = self._get_request_cache(forward_batch, LINGBOT_C2WS_PLUCKER_EMB_CACHE)
cache_key = (
c2ws_plucker_emb.data_ptr(),
tuple(c2ws_plucker_emb.shape),
@@ -1274,7 +1286,7 @@ class CausalLingBotWorldTransformer3DModel(CausalWanTransformer3DModel):
start_frame: int,
device: torch.device,
) -> tuple[torch.Tensor, ...]:
cache = self._get_request_cache(forward_batch, "lingbot_rope")
cache = self._get_request_cache(forward_batch, LINGBOT_ROPE_CACHE)
cache_key = (
post_patch_num_frames,
post_patch_height,
@@ -1327,7 +1339,9 @@ class CausalLingBotWorldTransformer3DModel(CausalWanTransformer3DModel):
post_patch_width: int,
device: torch.device,
) -> tuple[torch.Tensor, ...]:
cache = self._get_request_cache(forward_batch, "lingbot_sequence_shard_rope")
cache = self._get_request_cache(
forward_batch, LINGBOT_SEQUENCE_SHARD_ROPE_CACHE
)
cache_key = (
local_seq_len,
token_start,
@@ -1393,7 +1407,7 @@ class CausalLingBotWorldTransformer3DModel(CausalWanTransformer3DModel):
timestep: torch.LongTensor,
forward_batch,
) -> tuple[torch.Tensor, torch.Tensor]:
cache = self._get_request_cache(forward_batch, "lingbot_time_embeddings")
cache = self._get_request_cache(forward_batch, LINGBOT_TIME_EMBEDDINGS_CACHE)
current_timestep = get_forward_context().current_timestep
cache_key = (
current_timestep,
@@ -1426,7 +1440,7 @@ class CausalLingBotWorldTransformer3DModel(CausalWanTransformer3DModel):
if not self._should_cache_cam_conditioner(forward_batch):
return None
cache = self._get_request_cache(forward_batch, "lingbot_cam_conditioner")
cache = self._get_request_cache(forward_batch, LINGBOT_CAM_CONDITIONER_CACHE)
if cache is None:
return None
@@ -18,7 +18,7 @@ from sglang.multimodal_gen.runtime.pipelines_core.stages import (
DMDTimestepPreparationStage,
ImageEncodingStage,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.lingbot_world import (
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.lingbot_world.lingbot_world_causal_denoising import (
LingBotWorldCausalDMDDenoisingStage,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.realtime import (
@@ -215,6 +215,7 @@ class Req:
realtime_output_pacing: bool = False
realtime_causal_sink_size: int | None = None
realtime_causal_kv_cache_num_frames: int | None = None
realtime_causal_kv_sample_tokens: int | None = None
# return websocket-friendly raw RGB frame bytes instead of rwa tensors
return_raw_frames: bool = False
@@ -1094,6 +1094,7 @@ class CausalDMDDenoisingStage(DenoisingStage):
device,
*,
sequence_shard_enabled: bool = False,
kv_cache_size: int | None = None,
) -> None:
"""
Initialize (but not fill) a Per-GPU KV cache aligned with the model assumptions.
@@ -1102,9 +1103,10 @@ class CausalDMDDenoisingStage(DenoisingStage):
sequence_shard_enabled=sequence_shard_enabled
)
attention_head_dim = self.transformer.attention_head_dim
kv_cache_size = self._get_causal_kv_cache_size(
sequence_shard_enabled=sequence_shard_enabled
)
if kv_cache_size is None:
kv_cache_size = self._get_causal_kv_cache_size(
sequence_shard_enabled=sequence_shard_enabled
)
self.causal_kv_cache = self._allocate_causal_kv_cache(
batch_size=batch_size,
kv_cache_size=kv_cache_size,
@@ -1,9 +1,16 @@
# SPDX-License-Identifier: Apache-2.0
"""LingBot-World-specific pipeline stages."""
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.lingbot_world.lingbot_world_causal_denoising import (
LingBotWorldCausalDMDDenoisingStage,
)
def __getattr__(name: str):
if name == "LingBotWorldCausalDMDDenoisingStage":
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.lingbot_world.lingbot_world_causal_denoising import (
LingBotWorldCausalDMDDenoisingStage,
)
return LingBotWorldCausalDMDDenoisingStage
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
__all__ = [
"LingBotWorldCausalDMDDenoisingStage",
@@ -0,0 +1,11 @@
# SPDX-License-Identifier: Apache-2.0
LINGBOT_CAMERA_ACTIONS_CONDITION = "camera_actions"
LINGBOT_PROMPT_UPDATED_CONDITION = "lingbot_prompt_updated"
LINGBOT_INTERACTIVE_KV_WINDOW_CACHE = "lingbot_interactive_kv_window"
LINGBOT_C2WS_PLUCKER_EMB_CACHE = "lingbot_c2ws_plucker_emb"
LINGBOT_CAM_CONDITIONER_CACHE = "lingbot_cam_conditioner"
LINGBOT_ROPE_CACHE = "lingbot_rope"
LINGBOT_SEQUENCE_SHARD_ROPE_CACHE = "lingbot_sequence_shard_rope"
LINGBOT_TIME_EMBEDDINGS_CACHE = "lingbot_time_embeddings"
@@ -3,8 +3,11 @@
"""LingBot-World causal DMD denoising stage."""
from typing import Any
import torch
from sglang.multimodal_gen import envs
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_ring_parallel_world_size,
get_ulysses_parallel_world_size,
@@ -14,6 +17,15 @@ from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
from sglang.multimodal_gen.runtime.pipelines_core.stages.causal_denoising import (
CausalDMDCachePolicy,
CausalDMDDenoisingStage,
CausalDMDForwardContext,
CausalDMDRealtimeCacheContext,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.lingbot_world.constants import (
LINGBOT_C2WS_PLUCKER_EMB_CACHE,
LINGBOT_CAM_CONDITIONER_CACHE,
LINGBOT_CAMERA_ACTIONS_CONDITION,
LINGBOT_INTERACTIVE_KV_WINDOW_CACHE,
LINGBOT_PROMPT_UPDATED_CONDITION,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.validators import (
StageValidators as V,
@@ -23,6 +35,9 @@ from sglang.multimodal_gen.runtime.pipelines_core.stages.validators import (
)
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.multimodal_gen.runtime.server_args import ServerArgs
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class LingBotWorldCausalDMDDenoisingStage(CausalDMDDenoisingStage):
@@ -76,8 +91,11 @@ class LingBotWorldCausalDMDDenoisingStage(CausalDMDDenoisingStage):
def _causal_kv_cache_kwargs(
self,
policy: CausalDMDCachePolicy,
) -> dict[str, bool]:
return {"sequence_shard_enabled": policy.sequence_shard_enabled}
) -> dict[str, Any]:
return {
"sequence_shard_enabled": policy.sequence_shard_enabled,
"kv_cache_size": policy.expected_cache_tokens,
}
def _use_causal_cache_int_indices(
self,
@@ -86,6 +104,316 @@ class LingBotWorldCausalDMDDenoisingStage(CausalDMDDenoisingStage):
) -> bool:
return True
@staticmethod
def _chunk_has_camera_motion(actions) -> bool:
if not actions:
return False
for frame_actions in actions:
if frame_actions:
return True
return False
def _uses_interactive_kv_window(
self,
batch: Req,
server_args: ServerArgs,
) -> bool:
if not self._interactive_kv_window_enabled(server_args):
return False
condition_inputs = getattr(batch, "condition_inputs", None) or {}
return LINGBOT_CAMERA_ACTIONS_CONDITION in condition_inputs
@staticmethod
def _interactive_kv_window_enabled(server_args: ServerArgs) -> bool:
config_enabled = bool(
getattr(
server_args.pipeline_config,
"interactive_kv_window_enable",
False,
)
)
return config_enabled or envs.SGLANG_LINGBOT_ENABLE_INTERACTIVE_KV_WINDOW
def _apply_causal_cache_overrides(
self,
batch: Req,
server_args: ServerArgs,
) -> None:
self._reset_causal_cache_config_defaults()
super()._apply_causal_cache_overrides(batch, server_args)
self._sync_interactive_kv_cache_window(server_args)
def _reset_causal_cache_config_defaults(self) -> None:
arch_config = getattr(
getattr(self.transformer, "config", None), "arch_config", None
)
if arch_config is None:
return
if hasattr(arch_config, "sink_size"):
self.sink_size = int(arch_config.sink_size)
if hasattr(arch_config, "sliding_window_num_frames"):
self.sliding_window_num_frames = int(arch_config.sliding_window_num_frames)
def _sync_interactive_kv_cache_window(self, server_args: ServerArgs) -> None:
if not self._interactive_kv_window_enabled(server_args):
return
if self.local_attn_size != -1:
return
self.sliding_window_num_frames = (
self._effective_interactive_kv_cache_num_frames(server_args)
)
def _effective_interactive_kv_cache_num_frames(
self,
server_args: ServerArgs,
) -> int:
cache_window = int(self.sliding_window_num_frames)
if self.local_attn_size != -1:
return cache_window
moving_window = self._moving_kv_sample_num_frames(server_args) or 0
still_window = self._still_kv_sample_num_frames(server_args) or 0
return max(
cache_window,
int(self.sink_size)
+ max(moving_window, still_window)
+ int(self.num_frames_per_block),
)
def _build_realtime_causal_cache_policy(
self,
batch: Req,
server_args: ServerArgs,
) -> CausalDMDCachePolicy:
policy = super()._build_realtime_causal_cache_policy(batch, server_args)
if self._interactive_kv_window_enabled(server_args):
policy.expected_cache_tokens = (
self._effective_interactive_kv_cache_num_frames(server_args)
* self.num_token_per_frame
)
return policy
@staticmethod
def _should_reset_lingbot_crossattn_cache(batch: Req) -> bool:
condition_inputs = getattr(batch, "condition_inputs", None) or {}
return bool(condition_inputs.get(LINGBOT_PROMPT_UPDATED_CONDITION))
def _sync_lingbot_crossattn_cache(
self,
batch: Req,
cache_ctx: CausalDMDRealtimeCacheContext,
) -> None:
if self._should_reset_lingbot_crossattn_cache(batch):
self._reset_crossattn_cache(cache_ctx.crossattn_cache)
def _prepare_realtime_causal_caches(
self,
batch: Req,
server_args: ServerArgs,
ctx: CausalDMDForwardContext,
) -> CausalDMDRealtimeCacheContext:
cache_ctx = super()._prepare_realtime_causal_caches(batch, server_args, ctx)
self._sync_lingbot_crossattn_cache(batch, cache_ctx)
return cache_ctx
def _base_kv_sample_num_frames(self) -> int | None:
sample_frames = (
int(self.sliding_window_num_frames)
- int(self.sink_size)
- int(self.num_frames_per_block)
)
return sample_frames if sample_frames > 0 else None
@staticmethod
def _optional_non_negative_int(value: Any) -> int | None:
if value is None:
return None
return max(0, int(value))
def _moving_kv_sample_num_frames(
self,
server_args: ServerArgs,
) -> int | None:
return self._optional_non_negative_int(
getattr(
server_args.pipeline_config,
"interactive_kv_moving_window",
None,
)
)
def _still_kv_sample_num_frames(
self,
server_args: ServerArgs,
) -> int | None:
return self._optional_non_negative_int(
getattr(
server_args.pipeline_config,
"interactive_kv_still_window",
3,
)
)
def _get_interactive_kv_sample_num_frames(
self,
cache_state,
batch: Req,
server_args: ServerArgs,
) -> int | None:
pipeline_config = server_args.pipeline_config
if not self._interactive_kv_window_enabled(server_args):
return None
if not self._uses_interactive_kv_window(batch, server_args):
return self._base_kv_sample_num_frames()
dynamic_state = cache_state.runtime_cache.setdefault(
LINGBOT_INTERACTIVE_KV_WINDOW_CACHE,
{
"consecutive_still_chunks": 0,
"sample_num_frames": None,
},
)
if cache_state.chunk_idx == 0:
dynamic_state["consecutive_still_chunks"] = 0
dynamic_state["sample_num_frames"] = None
moving_window = self._moving_kv_sample_num_frames(server_args)
if moving_window is None:
return None
still_window = self._still_kv_sample_num_frames(server_args)
still_chunks_threshold = max(
1, int(getattr(pipeline_config, "interactive_kv_still_chunks", 2))
)
if dynamic_state["sample_num_frames"] is None:
dynamic_state["sample_num_frames"] = moving_window
condition_inputs = getattr(batch, "condition_inputs", None) or {}
if self._chunk_has_camera_motion(
condition_inputs.get(LINGBOT_CAMERA_ACTIONS_CONDITION)
):
dynamic_state["consecutive_still_chunks"] = 0
dynamic_state["sample_num_frames"] = moving_window
else:
dynamic_state["consecutive_still_chunks"] += 1
if (
still_window is not None
and dynamic_state["consecutive_still_chunks"] >= still_chunks_threshold
):
dynamic_state["sample_num_frames"] = still_window
return int(dynamic_state["sample_num_frames"])
def _log_lingbot_kv_window(
self,
cache_state,
batch: Req,
server_args: ServerArgs,
*,
sample_frames: int | None,
) -> None:
if not self._interactive_kv_window_enabled(server_args):
return
mode = "base"
still_chunks = None
if self._uses_interactive_kv_window(batch, server_args):
dynamic_state = cache_state.runtime_cache.get(
LINGBOT_INTERACTIVE_KV_WINDOW_CACHE, {}
)
still_chunks = dynamic_state.get("consecutive_still_chunks")
condition_inputs = getattr(batch, "condition_inputs", None) or {}
if self._chunk_has_camera_motion(
condition_inputs.get(LINGBOT_CAMERA_ACTIONS_CONDITION)
):
mode = "moving"
else:
still_window = self._still_kv_sample_num_frames(server_args)
still_chunks_threshold = max(
1,
int(
getattr(
server_args.pipeline_config,
"interactive_kv_still_chunks",
2,
)
),
)
if (
still_window is not None
and sample_frames == still_window
and still_chunks is not None
and still_chunks >= still_chunks_threshold
):
mode = "still"
else:
mode = "moving"
window_frames = (
int(self.sliding_window_num_frames)
if sample_frames is None
else int(self.sink_size)
+ int(sample_frames)
+ int(self.num_frames_per_block)
)
sample_tokens = (
None
if sample_frames is None
else int(sample_frames) * int(self.num_token_per_frame)
)
logger.info(
"LingBot interactive KV window: session_id=%s request_id=%s "
"chunk_idx=%s mode=%s window_frames=%s sample_frames=%s "
"cache_frames=%s sink_frames=%s current_frames=%s sample_tokens=%s "
"cache_tokens=%s still_chunks=%s",
getattr(batch, "realtime_session_id", None),
getattr(batch, "request_id", None),
getattr(batch, "block_idx", None),
mode,
window_frames,
sample_frames,
int(self.sliding_window_num_frames),
int(self.sink_size),
int(self.num_frames_per_block),
sample_tokens,
int(self.sliding_window_num_frames) * int(self.num_token_per_frame),
still_chunks,
)
def _set_lingbot_kv_sample_tokens(
self,
cache_state,
batch: Req,
server_args: ServerArgs,
) -> int | None:
self._sync_interactive_kv_cache_window(server_args)
sample_frames = self._get_interactive_kv_sample_num_frames(
cache_state,
batch,
server_args,
)
sample_tokens = (
None
if sample_frames is None
else int(sample_frames) * self.num_token_per_frame
)
self._log_lingbot_kv_window(
cache_state,
batch,
server_args,
sample_frames=sample_frames,
)
previous = getattr(batch, "realtime_causal_kv_sample_tokens", None)
batch.realtime_causal_kv_sample_tokens = sample_tokens
return previous
@staticmethod
def _clear_lingbot_dynamic_condition_cache(cache_state) -> None:
runtime_cache = getattr(cache_state, "runtime_cache", None)
if runtime_cache is None:
return
runtime_cache.pop(LINGBOT_C2WS_PLUCKER_EMB_CACHE, None)
runtime_cache.pop(LINGBOT_CAM_CONDITIONER_CACHE, None)
def verify_input(self, batch: Req, server_args: ServerArgs) -> VerificationResult:
result = VerificationResult()
result.add_check(
@@ -97,6 +425,35 @@ class LingBotWorldCausalDMDDenoisingStage(CausalDMDDenoisingStage):
result.add_check("prompt_embeds", batch.prompt_embeds, V.list_not_empty)
return result
def _denoise_realtime_causal_chunk(
self,
batch: Req,
server_args: ServerArgs,
*,
ctx,
cache_ctx,
chunk_latents: torch.Tensor,
prepare_model_input,
prepare_context_input,
) -> torch.Tensor:
previous_sample_tokens = self._set_lingbot_kv_sample_tokens(
cache_ctx.cache_state,
batch,
server_args,
)
try:
return super()._denoise_realtime_causal_chunk(
batch,
server_args,
ctx=ctx,
cache_ctx=cache_ctx,
chunk_latents=chunk_latents,
prepare_model_input=prepare_model_input,
prepare_context_input=prepare_context_input,
)
finally:
batch.realtime_causal_kv_sample_tokens = previous_sample_tokens
def _get_causal_dmd_latents(self, batch: Req) -> torch.Tensor:
latents = batch.latents
assert latents is not None, (
@@ -294,16 +651,18 @@ class LingBotWorldCausalDMDDenoisingStage(CausalDMDDenoisingStage):
device=ctx.device,
)
current_latents = self._denoise_realtime_causal_chunk(
batch,
server_args,
ctx=ctx,
cache_ctx=cache_ctx,
chunk_latents=current_latents,
prepare_model_input=prepare_model_input,
prepare_context_input=prepare_model_input,
)
cache_ctx.cache_state.runtime_cache.pop("lingbot_cam_conditioner", None)
try:
current_latents = self._denoise_realtime_causal_chunk(
batch,
server_args,
ctx=ctx,
cache_ctx=cache_ctx,
chunk_latents=current_latents,
prepare_model_input=prepare_model_input,
prepare_context_input=prepare_model_input,
)
finally:
self._clear_lingbot_dynamic_condition_cache(cache_ctx.cache_state)
# Advance cumulative frame position
self._advance_realtime_causal_cache(cache_ctx, num_frames=ctx.num_frames)
@@ -4,7 +4,11 @@ from types import SimpleNamespace
import torch
from sglang.multimodal_gen.configs.pipeline_configs.lingbot_world import (
LingBotWorldCausalDMDConfig,
)
from sglang.multimodal_gen.runtime.layers.kvcache.causal_attention_cache import (
CausalSelfAttentionKVCache,
CrossAttentionKVCache,
)
from sglang.multimodal_gen.runtime.models.dits import (
@@ -15,9 +19,21 @@ from sglang.multimodal_gen.runtime.models.dits.lingbot_world import (
CausalLingBotWorldTransformerBlock,
LingBotWorldCamConditioner,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.lingbot_world import (
from sglang.multimodal_gen.runtime.pipelines_core.stages.causal_denoising import (
CausalDMDCachePolicy,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.lingbot_world.constants import (
LINGBOT_C2WS_PLUCKER_EMB_CACHE,
LINGBOT_CAM_CONDITIONER_CACHE,
LINGBOT_PROMPT_UPDATED_CONDITION,
LINGBOT_ROPE_CACHE,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.lingbot_world.lingbot_world_causal_denoising import (
LingBotWorldCausalDMDDenoisingStage,
)
from sglang.multimodal_gen.runtime.realtime.states import RealtimeCausalDiTState
LINGBOT_INTERACTIVE_KV_WINDOW_ENV = "SGLANG_LINGBOT_ENABLE_INTERACTIVE_KV_WINDOW"
def test_lingbot_denoising_stage_does_not_own_realtime_cache_refs():
@@ -181,6 +197,408 @@ def test_lingbot_realtime_attention_cache_rolls_with_sink_window():
]
def test_lingbot_realtime_attention_cache_samples_sink_and_recent_window():
cache = CausalSelfAttentionKVCache(
k=torch.zeros(1, 8, 1, 1),
v=torch.zeros(1, 8, 1, 1),
global_end_index=torch.zeros(1, dtype=torch.long),
local_end_index=torch.zeros(1, dtype=torch.long),
global_end_index_int=0,
local_end_index_int=0,
cache_size=8,
sink_tokens=2,
attention_window_size=8,
)
cache.update_and_get_attention_kv(
key=torch.ones(1, 3, 1, 1),
value=torch.ones(1, 3, 1, 1),
current_chunk_start=0,
)
cache.update_and_get_attention_kv(
key=torch.full((1, 3, 1, 1), 2.0),
value=torch.full((1, 3, 1, 1), 2.0),
current_chunk_start=3,
)
sampled_view = cache.update_and_get_attention_kv(
key=torch.full((1, 3, 1, 1), 3.0),
value=torch.full((1, 3, 1, 1), 3.0),
current_chunk_start=6,
recent_window_tokens=1,
)
assert sampled_view.k.flatten().tolist() == [1.0, 1.0, 2.0, 3.0, 3.0, 3.0]
assert sampled_view.v.flatten().tolist() == [1.0, 1.0, 2.0, 3.0, 3.0, 3.0]
current_only_view = cache.update_and_get_attention_kv(
key=torch.full((1, 3, 1, 1), 4.0),
value=torch.full((1, 3, 1, 1), 4.0),
current_chunk_start=6,
recent_window_tokens=0,
)
assert current_only_view.k.flatten().tolist() == [1.0, 1.0, 4.0, 4.0, 4.0]
assert current_only_view.v.flatten().tolist() == [1.0, 1.0, 4.0, 4.0, 4.0]
def test_lingbot_interactive_kv_window_config_default_disabled():
field = LingBotWorldCausalDMDConfig.__dataclass_fields__[
"interactive_kv_window_enable"
]
assert field.default is False
def test_lingbot_lazy_vae_encode_black_frames_env(monkeypatch):
config = LingBotWorldCausalDMDConfig()
temporal_ratio = int(config.vae_config.arch_config.temporal_compression_ratio)
image = torch.zeros(1, 3, temporal_ratio * 2 + 1, 2, 2)
monkeypatch.delenv("SGLANG_LINGBOT_LAZY_VAE_ENCODE_BLACK_FRAMES", raising=False)
assert config.preprocess_vae_encode(image, vae=None) is image
monkeypatch.setenv("SGLANG_LINGBOT_LAZY_VAE_ENCODE_BLACK_FRAMES", "0")
assert config.preprocess_vae_encode(image, vae=None) is image
monkeypatch.setenv(
"SGLANG_LINGBOT_LAZY_VAE_ENCODE_BLACK_FRAMES", str(temporal_ratio)
)
encoded = config.preprocess_vae_encode(image, vae=None)
assert encoded.shape[2] == temporal_ratio + 1
def test_lingbot_interactive_kv_window_samples_base_moving_and_still(monkeypatch):
monkeypatch.delenv(LINGBOT_INTERACTIVE_KV_WINDOW_ENV, raising=False)
stage = LingBotWorldCausalDMDDenoisingStage.__new__(
LingBotWorldCausalDMDDenoisingStage
)
stage.local_attn_size = -1
stage.sink_size = 9
stage.num_token_per_frame = 10
stage.num_frames_per_block = 3
stage.sliding_window_num_frames = 18
stage.transformer = SimpleNamespace(num_attention_heads=1)
server_args = SimpleNamespace(
pipeline_config=SimpleNamespace(
interactive_kv_window_enable=True,
interactive_kv_moving_window=12,
interactive_kv_still_window=3,
interactive_kv_still_chunks=2,
)
)
cache_state = RealtimeCausalDiTState()
batch = SimpleNamespace(condition_inputs={})
previous = stage._set_lingbot_kv_sample_tokens(cache_state, batch, server_args)
assert previous is None
assert stage.sliding_window_num_frames == 24
assert batch.realtime_causal_kv_sample_tokens == 120
policy = stage._build_realtime_causal_cache_policy(batch, server_args)
assert policy.expected_cache_tokens == 240
batch.condition_inputs = {"camera_actions": [["w"], [], []]}
cache_state.chunk_idx = 0
stage._set_lingbot_kv_sample_tokens(cache_state, batch, server_args)
assert batch.realtime_causal_kv_sample_tokens == 120
batch.condition_inputs = {"camera_actions": [[], [], []]}
cache_state.chunk_idx = 1
stage._set_lingbot_kv_sample_tokens(cache_state, batch, server_args)
assert batch.realtime_causal_kv_sample_tokens == 120
cache_state.chunk_idx = 2
stage._set_lingbot_kv_sample_tokens(cache_state, batch, server_args)
assert batch.realtime_causal_kv_sample_tokens == 30
def test_lingbot_interactive_kv_window_none_disables_moving_window(monkeypatch):
monkeypatch.delenv(LINGBOT_INTERACTIVE_KV_WINDOW_ENV, raising=False)
stage = LingBotWorldCausalDMDDenoisingStage.__new__(
LingBotWorldCausalDMDDenoisingStage
)
stage.local_attn_size = -1
stage.sink_size = 9
stage.num_token_per_frame = 10
stage.num_frames_per_block = 3
stage.sliding_window_num_frames = 18
stage.transformer = SimpleNamespace(num_attention_heads=1)
server_args = SimpleNamespace(
pipeline_config=SimpleNamespace(
realtime_causal_sink_size=9,
realtime_causal_kv_cache_num_frames=18,
interactive_kv_window_enable=True,
interactive_kv_moving_window=None,
interactive_kv_still_window=3,
interactive_kv_still_chunks=2,
)
)
cache_state = RealtimeCausalDiTState()
batch = SimpleNamespace(condition_inputs={"camera_actions": [["w"], [], []]})
stage._set_lingbot_kv_sample_tokens(cache_state, batch, server_args)
assert batch.realtime_causal_kv_sample_tokens is None
policy = stage._build_realtime_causal_cache_policy(batch, server_args)
assert policy.expected_cache_tokens == 180
def test_lingbot_interactive_kv_window_zero_is_valid_moving_window(monkeypatch):
monkeypatch.delenv(LINGBOT_INTERACTIVE_KV_WINDOW_ENV, raising=False)
stage = LingBotWorldCausalDMDDenoisingStage.__new__(
LingBotWorldCausalDMDDenoisingStage
)
stage.local_attn_size = -1
stage.sink_size = 9
stage.num_token_per_frame = 10
stage.num_frames_per_block = 3
stage.sliding_window_num_frames = 18
stage.transformer = SimpleNamespace(num_attention_heads=1)
server_args = SimpleNamespace(
pipeline_config=SimpleNamespace(
realtime_causal_sink_size=9,
realtime_causal_kv_cache_num_frames=18,
interactive_kv_window_enable=True,
interactive_kv_moving_window=0,
interactive_kv_still_window=None,
interactive_kv_still_chunks=2,
)
)
cache_state = RealtimeCausalDiTState()
batch = SimpleNamespace(condition_inputs={"camera_actions": [["w"], [], []]})
stage._set_lingbot_kv_sample_tokens(cache_state, batch, server_args)
assert batch.realtime_causal_kv_sample_tokens == 0
policy = stage._build_realtime_causal_cache_policy(batch, server_args)
assert policy.expected_cache_tokens == 180
def test_lingbot_interactive_kv_window_updates_total_window_for_moving_default(
monkeypatch,
):
monkeypatch.delenv(LINGBOT_INTERACTIVE_KV_WINDOW_ENV, raising=False)
stage = LingBotWorldCausalDMDDenoisingStage.__new__(
LingBotWorldCausalDMDDenoisingStage
)
stage.local_attn_size = -1
stage.sink_size = 9
stage.num_token_per_frame = 10
stage.num_frames_per_block = 3
stage.sliding_window_num_frames = 18
stage.transformer = SimpleNamespace(num_attention_heads=1)
server_args = SimpleNamespace(
pipeline_config=SimpleNamespace(
realtime_causal_sink_size=9,
realtime_causal_kv_cache_num_frames=18,
interactive_kv_window_enable=True,
interactive_kv_moving_window=12,
interactive_kv_still_window=3,
interactive_kv_still_chunks=2,
)
)
batch = SimpleNamespace(condition_inputs={"camera_actions": [["w"], [], []]})
policy = stage._build_realtime_causal_cache_policy(batch, server_args)
assert stage.sliding_window_num_frames == 24
assert policy.expected_cache_tokens == 240
def test_lingbot_interactive_kv_window_resets_stage_window_between_requests(
monkeypatch,
):
monkeypatch.delenv(LINGBOT_INTERACTIVE_KV_WINDOW_ENV, raising=False)
stage = LingBotWorldCausalDMDDenoisingStage.__new__(
LingBotWorldCausalDMDDenoisingStage
)
stage.local_attn_size = -1
stage.sink_size = 9
stage.num_token_per_frame = 10
stage.num_frames_per_block = 3
stage.sliding_window_num_frames = 18
stage.transformer = SimpleNamespace(
num_attention_heads=1,
config=SimpleNamespace(
arch_config=SimpleNamespace(
sink_size=9,
sliding_window_num_frames=18,
)
),
)
dynamic_args = SimpleNamespace(
pipeline_config=SimpleNamespace(
realtime_causal_sink_size=9,
realtime_causal_kv_cache_num_frames=18,
interactive_kv_window_enable=True,
interactive_kv_moving_window=12,
interactive_kv_still_window=3,
interactive_kv_still_chunks=2,
)
)
disabled_args = SimpleNamespace(
pipeline_config=SimpleNamespace(
interactive_kv_window_enable=False,
interactive_kv_moving_window=12,
interactive_kv_still_window=3,
interactive_kv_still_chunks=2,
)
)
dynamic_batch = SimpleNamespace(condition_inputs={"camera_actions": [["w"]]})
dynamic_policy = stage._build_realtime_causal_cache_policy(
dynamic_batch, dynamic_args
)
assert stage.sliding_window_num_frames == 24
assert dynamic_policy.expected_cache_tokens == 240
disabled_batch = SimpleNamespace(condition_inputs={})
disabled_policy = stage._build_realtime_causal_cache_policy(
disabled_batch, disabled_args
)
assert stage.sliding_window_num_frames == 18
assert disabled_policy.expected_cache_tokens == 180
def test_lingbot_interactive_kv_window_default_disabled(monkeypatch):
monkeypatch.delenv(LINGBOT_INTERACTIVE_KV_WINDOW_ENV, raising=False)
stage = LingBotWorldCausalDMDDenoisingStage.__new__(
LingBotWorldCausalDMDDenoisingStage
)
stage.local_attn_size = -1
stage.sink_size = 9
stage.num_token_per_frame = 10
stage.num_frames_per_block = 3
stage.sliding_window_num_frames = 18
stage.transformer = SimpleNamespace(num_attention_heads=1)
server_args = SimpleNamespace(
pipeline_config=SimpleNamespace(
realtime_causal_sink_size=9,
realtime_causal_kv_cache_num_frames=18,
interactive_kv_window_enable=False,
interactive_kv_moving_window=12,
interactive_kv_still_window=3,
interactive_kv_still_chunks=2,
)
)
cache_state = RealtimeCausalDiTState()
batch = SimpleNamespace(condition_inputs={"camera_actions": [["w"], [], []]})
stage._set_lingbot_kv_sample_tokens(cache_state, batch, server_args)
policy = stage._build_realtime_causal_cache_policy(batch, server_args)
assert stage.sliding_window_num_frames == 18
assert batch.realtime_causal_kv_sample_tokens is None
assert policy.expected_cache_tokens == 180
def test_lingbot_interactive_kv_window_env_can_enable_default(monkeypatch):
monkeypatch.setenv(LINGBOT_INTERACTIVE_KV_WINDOW_ENV, "1")
stage = LingBotWorldCausalDMDDenoisingStage.__new__(
LingBotWorldCausalDMDDenoisingStage
)
stage.local_attn_size = -1
stage.sink_size = 9
stage.num_token_per_frame = 10
stage.num_frames_per_block = 3
stage.sliding_window_num_frames = 18
stage.transformer = SimpleNamespace(num_attention_heads=1)
server_args = SimpleNamespace(
pipeline_config=SimpleNamespace(
realtime_causal_sink_size=9,
realtime_causal_kv_cache_num_frames=18,
interactive_kv_window_enable=False,
interactive_kv_moving_window=12,
interactive_kv_still_window=3,
interactive_kv_still_chunks=2,
)
)
cache_state = RealtimeCausalDiTState()
batch = SimpleNamespace(condition_inputs={"camera_actions": [["w"], [], []]})
stage._set_lingbot_kv_sample_tokens(cache_state, batch, server_args)
policy = stage._build_realtime_causal_cache_policy(batch, server_args)
assert stage.sliding_window_num_frames == 24
assert batch.realtime_causal_kv_sample_tokens == 120
assert policy.expected_cache_tokens == 240
def test_lingbot_interactive_kv_window_allocates_expected_cache_size():
stage = LingBotWorldCausalDMDDenoisingStage.__new__(
LingBotWorldCausalDMDDenoisingStage
)
stage.num_transformer_blocks = 1
stage.local_attn_size = -1
stage.sink_size = 9
stage.num_token_per_frame = 10
stage.num_frames_per_block = 3
stage.sliding_window_num_frames = 18
stage.transformer = SimpleNamespace(num_attention_heads=1, attention_head_dim=1)
policy = CausalDMDCachePolicy(
sequence_shard_enabled=False,
num_attention_heads=1,
expected_cache_tokens=240,
expected_sink_tokens=90,
kv_cache_kwargs={},
)
stage._initialize_kv_cache(
batch_size=1,
dtype=torch.float32,
device=torch.device("cpu"),
**stage._causal_kv_cache_kwargs(policy),
)
assert stage.causal_kv_cache is not None
cache = stage.causal_kv_cache[0]
assert cache.cache_size == 240
assert cache.k.shape[1] == 240
assert cache.sink_tokens == 90
assert cache.attention_window_size == 240
def test_lingbot_dynamic_condition_cache_clear_removes_chunk_entries():
cache_state = SimpleNamespace(
runtime_cache={
LINGBOT_C2WS_PLUCKER_EMB_CACHE: object(),
LINGBOT_CAM_CONDITIONER_CACHE: object(),
LINGBOT_ROPE_CACHE: object(),
}
)
LingBotWorldCausalDMDDenoisingStage._clear_lingbot_dynamic_condition_cache(
cache_state
)
assert LINGBOT_C2WS_PLUCKER_EMB_CACHE not in cache_state.runtime_cache
assert LINGBOT_CAM_CONDITIONER_CACHE not in cache_state.runtime_cache
assert LINGBOT_ROPE_CACHE in cache_state.runtime_cache
def test_lingbot_crossattn_cache_resets_on_prompt_event():
stage = LingBotWorldCausalDMDDenoisingStage.__new__(
LingBotWorldCausalDMDDenoisingStage
)
crossattn_cache = [
CrossAttentionKVCache(
k=torch.empty(1),
v=torch.empty(1),
)
]
crossattn_cache[0].store(torch.ones(1), torch.ones(1))
cache_ctx = SimpleNamespace(
cache_state=RealtimeCausalDiTState(),
crossattn_cache=crossattn_cache,
)
batch = SimpleNamespace(condition_inputs={})
stage._sync_lingbot_crossattn_cache(batch, cache_ctx)
assert crossattn_cache[0].is_init
batch.condition_inputs = {LINGBOT_PROMPT_UPDATED_CONDITION: True}
stage._sync_lingbot_crossattn_cache(batch, cache_ctx)
assert not crossattn_cache[0].is_init
def test_lingbot_i2v_model_input_writer_reuses_buffer():
latents = torch.ones(1, 16, 3, 2, 2)
condition = torch.full((1, 20, 3, 2, 2), 2.0)
@@ -255,7 +673,7 @@ def test_lingbot_cam_conditioner_cache_reuses_source_tensor(monkeypatch):
assert first is second
assert third is not first
assert block.cam_conditioner.calls == 2
cache = forward_batch.extra["lingbot_cam_conditioner"]
cache = forward_batch.extra[LINGBOT_CAM_CONDITIONER_CACHE]
assert cache["source_key"][0] == next_source.data_ptr()
assert len(cache["entries"]) == 1
@@ -287,7 +705,7 @@ def test_lingbot_cam_conditioner_cache_skips_non_sequence_shard(monkeypatch):
assert first is not second
assert first[0] is not second[0]
assert block.cam_conditioner.calls == 2
assert "lingbot_cam_conditioner" not in forward_batch.extra
assert LINGBOT_CAM_CONDITIONER_CACHE not in forward_batch.extra
def test_lingbot_cam_conditioner_cache_skips_single_ulysses_world(monkeypatch):
@@ -319,7 +737,7 @@ def test_lingbot_cam_conditioner_cache_skips_single_ulysses_world(monkeypatch):
assert first is not second
assert block.cam_conditioner.calls == 2
assert "lingbot_cam_conditioner" not in forward_batch.extra
assert LINGBOT_CAM_CONDITIONER_CACHE not in forward_batch.extra
def test_lingbot_cam_conditioner_cache_reuses_context_update(monkeypatch):
@@ -351,7 +769,7 @@ def test_lingbot_cam_conditioner_cache_reuses_context_update(monkeypatch):
assert first is second
assert block.cam_conditioner.calls == 1
assert "lingbot_cam_conditioner" in forward_batch.extra
assert LINGBOT_CAM_CONDITIONER_CACHE in forward_batch.extra
def test_lingbot_model_prepares_cam_conditioner_scale_shifts(monkeypatch):
@@ -6,6 +6,7 @@ from types import SimpleNamespace
import msgspec.msgpack
import numpy as np
import pytest
import torch
from sglang.multimodal_gen.configs.pipeline_configs.lingbot_world import (
@@ -36,7 +37,7 @@ from sglang.multimodal_gen.runtime.entrypoints.openai.realtime.registry import (
get_realtime_model_adapter,
)
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.lingbot_world import (
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.lingbot_world.lingbot_world_causal_denoising import (
LingBotWorldCausalDMDDenoisingStage,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.realtime.base import (
@@ -296,6 +297,104 @@ def test_lingbot_realtime_adapter_ingests_generic_events():
assert state.latest_sampled_event_id == 8
def test_lingbot_realtime_adapter_ingests_composite_input_event():
adapter = lingbot_realtime.LingBotWorldRealtimeAdapter()
session = GenerateSession()
session.set_adapter(adapter)
session.set_request(
RealtimeVideoGenerationsRequest(
type="init",
prompt="walk forward",
)
)
composite_event = RealtimeEvent(
type="event",
kind="composite_input",
payload={
"input_types": ["prompt", "camera_actions"],
"prompt": "turn left",
"camera_actions": [["w"], ["d"]],
},
event_id=9,
)
event_log = adapter.ingest_event(session, composite_event)
assert "kind=composite_input" in event_log
chunk_inputs = adapter.sample_chunk_inputs(
session,
server_args=SimpleNamespace(),
chunk=SimpleNamespace(index=1),
chunk_size=3,
)
assert chunk_inputs.prompt == "turn left"
assert chunk_inputs.condition_inputs[
lingbot_realtime.LINGBOT_PROMPT_UPDATED_CONDITION
]
assert chunk_inputs.condition_inputs[
lingbot_realtime.LINGBOT_CAMERA_ACTIONS_CONDITION
] == [["w"], ["d"], []]
assert adapter.get_realtime_event_id(session) == 9
def test_lingbot_realtime_adapter_rejects_composite_input_atomically():
adapter = lingbot_realtime.LingBotWorldRealtimeAdapter()
session = GenerateSession()
session.set_adapter(adapter)
session.set_request(
RealtimeVideoGenerationsRequest(
type="init",
prompt="walk forward",
)
)
composite_event = RealtimeEvent(
type="event",
kind="composite_input",
payload={
"input_types": ["prompt", "camera_actions"],
"prompt": "turn left",
"camera_actions": ["w"],
},
event_id=10,
)
with pytest.raises(ValueError, match="camera_actions"):
adapter.ingest_event(session, composite_event)
state = adapter._state(session)
assert not state.has_prompt()
assert state.sample_camera_actions(3) is None
def test_lingbot_realtime_prompt_event_marks_crossattn_reset():
adapter = lingbot_realtime.LingBotWorldRealtimeAdapter()
session = GenerateSession()
session.set_adapter(adapter)
session.set_request(
RealtimeVideoGenerationsRequest(
type="init",
prompt="walk forward",
)
)
state = adapter._state(session)
state.receive_prompt("turn left", event_id=8)
chunk_inputs = adapter.sample_chunk_inputs(
session,
server_args=SimpleNamespace(),
chunk=SimpleNamespace(index=1),
chunk_size=3,
)
assert chunk_inputs.prompt == "turn left"
assert session.request.prompt == "turn left"
assert chunk_inputs.condition_inputs[
lingbot_realtime.LINGBOT_PROMPT_UPDATED_CONDITION
]
def test_lingbot_realtime_adapter_ingests_state_camera_events():
adapter = lingbot_realtime.LingBotWorldRealtimeAdapter()
session = GenerateSession()
@@ -672,6 +771,7 @@ def test_lingbot_realtime_condition_horizon_repeats_blank_tail_chunk():
latent_condition = torch.ones(1, latent_channels, latent_frames, 2, 2)
batch = SimpleNamespace(
height=2 * spatial_ratio,
num_frames=num_frames,
width=2 * spatial_ratio,
)
condition_full = config.postprocess_image_latent(latent_condition, batch)