[Core] Add realtime AR-Diffusion tick sessions for LingBot World 2.0 (#5491)

Signed-off-by: zhangbin.327 <zhangbin.327@bytedance.com>
Signed-off-by: Jack47 <scsvip@gmail.com>
Co-authored-by: zhangbin.327 <zhangbin.327@bytedance.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
authored and GitHub committed 2026-08-16 09:11:20 +08:00
1 parent 9798bc3a8d
commit 3875f8ded4
53 files changed
+10754 -129

No files matched your search

+1
View File
@@ -116,6 +116,7 @@ nav:
- Async Diffusion Output: design/feature/async_diffusion_output.md
- Async Omni Output Materialization: design/feature/omni_async_output_materialization.md
- Automatic Prefix Caching: design/feature/prefix_caching.md
- Realtime AR-Diffusion: design/feature/realtime_ar_diffusion.md
- Communication:
- OmniConnector Implementations:
- Mooncake Store Connector: design/feature/omni_connectors/mooncake_store_connector.md
@@ -4,6 +4,9 @@
> `experimental/ar_diffusion` contract. It follows the ownership and lifecycle
> direction in [#5137](https://github.com/vllm-project/vllm-omni/issues/5137),
> but it is not yet part of the stable module-design hierarchy.
>
> The end-to-end request, identity, failure, and memory contracts are documented
> in [Realtime AR-Diffusion sessions](feature/realtime_ar_diffusion.md).
`ARDiffusionEngine` is an opt-in diffusion backend for pipelines that keep
autoregressive attention KV across requests. The engine only selects
@@ -51,14 +54,13 @@ time, and its inherited batch/step entry points also fail explicitly. Supporting
those paths requires a batch-aware state-binding contract rather than silently
bypassing `bind_ar_diffusion_state()`.
The current implementation retains one session per model instance. A request
for a different `session_id` closes the resident session through the same LRU
lifecycle used by explicit close and failure cleanup before allocating the new
one. Self-attention and cross-attention memory are therefore sized for one
resident session even if the pipeline declares a larger `session_capacity`.
This prioritizes rollout throughput and prevents block-pool exhaustion before a
count-based eviction can run. Multi-session residency can be added later with a
memory-backed capacity calculation or allocation-pressure eviction.
The implementation may retain multiple sessions per model instance. It selects
the largest feasible capacity up to the pipeline's `session_capacity`, based on
self-attention pages, cross-attention allocations, scratch, and declared
model-owned state. One session is admitted whenever it fits actual free device
memory; `gpu_memory_fraction` controls additional residency. Requests beyond
that capacity evict the least-recently-used session through the same lifecycle
hook used by explicit close and failure cleanup.
Scratch storage is also capability-driven. `frames_per_block` reserves space for
an uncommitted current video block, while `max_scratch_tokens_per_branch`
@@ -0,0 +1,213 @@
# Realtime AR-Diffusion sessions
> **Status:** experimental. This document describes the internal execution
> contract implemented under `vllm_omni.experimental.ar_diffusion`. It does not
> define a public HTTP or WebSocket schema.
## Scope
Realtime AR-Diffusion serves world models that generate one autoregressive
latent block per request while retaining model state across requests. The
generic runtime owns identity, event ordering, session lifecycle, routing, and
KV allocation. A model adapter owns model-specific controls and conditioning.
LingBot World v2 is the first single-KV-branch integration. DreamZero uses the
same runner capability with two logical CFG branches.
## Identities
Four identifiers have distinct lifetimes:
| Identifier | Lifetime | Owner | Contract |
| --- | --- | --- | --- |
| `session_id` | One persistent world | Session manager | Selects runner KV and model-owned state. |
| `event_id` | One control/prompt update | Transport adapter | Unique and monotonically increasing within a session, including across reset. |
| `chunk_index` | One committed AR block position | Session | Contiguous from zero; reset starts again at zero. |
| `request_id` | One chunk execution | Session | Correlates the tick with its returned metadata. |
`AsyncOmni` keeps its normal UUID-suffixed engine routing ID. The tick
`request_id` remains inside the immutable AR-Diffusion snapshot and the
standard output metadata, while caller-visible `OmniRequestOutput.request_id`
continues to use AsyncOmni's existing external-ID contract. Protocol identity
therefore does not require a private entrypoint override.
## Request lifecycle
1. `ARDiffusionSession.accept_event()` validates and queues an immutable
`ARDiffusionSessionEvent`. Prompt and opaque control tracks may be updated in
the same event.
2. `ARDiffusionSession.next_chunk()` serializes chunk execution and calls
`_build_tick_locked()`. The resulting `ARDiffusionTickRequest` is a snapshot
of the current prompt, reduced controls, ordered event IDs, and chunk index.
3. `ARDiffusionOmniTickConsumer.execute_tick()` copies the stage sampling
parameters, stores the typed tick in
`sampling_params.extra_args["ar_diffusion_tick"]`, constructs the normal Omni
prompt through its `prompt_provider`, and invokes `AsyncOmni.generate()`.
4. `AsyncOmni` allocates its normal unique engine routing ID and submits one
`OmniDiffusionRequest` through the orchestrator.
5. `ARDiffusionModelRunner._request_session()` parses the typed tick.
`_get_or_create_session()` obtains runner-owned paged KV, and
`bind_ar_diffusion_state()` exposes it to the pipeline only for the duration
of `execute_model()`.
6. The model pipeline validates the tick and generates exactly one latent
block. LingBot interprets camera controls in
`LingBotCameraControlReducer` and
`LingBotWorldCausalDMDPipeline.forward()`; the generic runtime does not parse
camera or action schemas.
7. The pipeline returns one `DiffusionOutput` with the generated payload and an
`ARDiffusionChunkMetadata` mapping under `metadata["ar_diffusion"]`.
`AsyncOmni` exposes it as
`OmniRequestOutput.multimodal_output["metadata"]["ar_diffusion"]`.
8. The consumer parses the standard envelope. The session commits its reducer
state, prompt, controls, event IDs, and next chunk index only when returned
metadata exactly equals the submitted tick snapshot.
The metadata envelope is:
```json
{
"metadata": {
"ar_diffusion": {
"session_id": "world-7",
"request_id": "ar-world-7-3-...",
"chunk_index": 3,
"applied_event_ids": [10, 11]
}
}
}
```
## Transaction and failure semantics
Accepted events are not acknowledged as applied until the model output and
metadata are validated. Reducer `prepare()` is speculative; reducer `commit()`
and the session snapshot form one logical commit. A model, metadata, or reducer
failure leaves the chunk index and queued events uncommitted and transitions the
session to `FAILED`.
After a tick failure, worker state is closed because a forward may have
partially modified or already released KV. The same chunk is not retried in
place. A caller must explicitly reset the session, starting again at chunk
zero, or close it.
Explicit close follows this state machine:
```text
ACTIVE or FAILED
|
v
CLOSING ---- worker cleanup succeeds ----> CLOSED
|
+------- worker cleanup fails --------> CLEANUP_FAILED
|
+-- close retry --> CLOSING
```
`CLEANUP_FAILED` is a local tombstone. The session manager retains it, rejects
creation of the same `session_id`, and rejects reset. Only an idempotent close
retry that succeeds removes the session. Transport disconnect uses the same
path.
## KV ownership and capacity
`ARDiffusionKVCacheSpec` is the pipeline-owned immutable geometry contract. It
declares:
- TP-local self-attention geometry, AR block size, sink, and recent window;
- logical KV branches and their worker-local slots;
- fixed-length cross-attention caches;
- scratch tokens required by an uncommitted forward;
- persistent model-owned CUDA bytes per resident session; and
- the requested resident-session capacity.
The runner computes:
```text
required(capacity) =
scratch
+ managed_self_attention_pages(capacity)
+ capacity * cross_attention_bytes_per_session
+ capacity * model_owned_state_bytes_per_session
```
`gpu_memory_fraction` is a soft expansion budget:
```text
configured_budget = available_device_bytes * gpu_memory_fraction
effective_budget = max(configured_budget, required(1))
```
If `required(1)` exceeds actual available device memory, initialization fails.
Otherwise the runner always admits one viable session and uses the configured
fraction to select additional resident sessions up to the model-declared cap.
All model-owned reservations are deducted before the paged self-attention pool
is allocated.
LingBot reports its persistent full-horizon image-condition tensor. DreamZero
reports the measured 603 MiB per-session Wan VAE causal-convolution state.
## Concurrency and routing limits
The current runner executes one request at a time:
- `max_num_seqs` must be one;
- request batching and diffusion step execution are rejected;
- one AR block is generated per engine request; and
- an AR-Diffusion stage must have exactly one replica.
The single-replica restriction is intentional. Multi-replica support requires
session-affine routing so every tick, reset, and close reaches the worker that
owns the session. Random or round-robin routing is not correct.
Multiple resident sessions may share one runner. Capacity selection accounts
for all persistent pools, and the runner performs LRU eviction through the same
model lifecycle hook used by explicit close.
## Model boundary
The generic runtime transports controls as opaque
`ARDiffusionControlInput(track, schema, data)` values. Model adapters validate
and reduce those values. This keeps target/velocity, key state, pose,
intrinsics, and other model-specific conventions out of the session protocol.
For LingBot World v2:
- `lingbot_world/actions.py` parses key-state and trajectory controls;
- `lingbot_world/camera.py` loads or constructs camera trajectories and Plücker embeddings;
- `lingbot_world/transformer.py` implements checkpoint-compatible causal attention; and
- `lingbot_world/pipeline.py` constructs conditioning, owns small non-KV session state, and
produces one block plus the standard metadata envelope.
## Non-goals
This contract does not currently provide:
- a public HTTP or WebSocket request schema (the structured-interaction
frontend is tracked separately in
[#5527](https://github.com/vllm-project/vllm-omni/pull/5527));
- camera/action semantics shared by every world model;
- session migration or replication across workers;
- retry of an ambiguous partially executed chunk;
- cross-stage KV transfer; or
- stateful streaming VAE decode.
Those features can be layered on top without adding LingBot-specific identity
or lifecycle fields to the generic runtime.
## Required regression coverage
CPU contract tests cover:
- separation of tick correlation IDs from internal engine routing IDs;
- event ordering, metadata equality, and reducer fail-closed commit;
- close/disconnect failure tombstones and cleanup retry;
- capacity one under the default fraction for shipped LingBot geometry;
- capacity expansion under a tuned fraction;
- DreamZero model-owned state accounting;
- runner session reuse, reset, close, LRU eviction, and forward cleanup; and
- LingBot model registration, imports, camera/action reduction, and one-block
output metadata.
GPU validation must additionally exercise real model loading, action input,
prompt switching, rolling-window replacement, reset, close, finite output
latents, and exact metadata/request identity.
+1
View File
@@ -22,6 +22,7 @@ implementation contract; it is not, by itself, a general support claim.
- [Async Diffusion Output](feature/async_diffusion_output.md)
- [Async Omni Output Materialization](feature/omni_async_output_materialization.md)
- [Automatic Prefix Caching in Omni Models](feature/prefix_caching.md)
- [Realtime AR-Diffusion Sessions](feature/realtime_ar_diffusion.md)
### Communication
+5
View File
@@ -37,6 +37,7 @@ th {
| `Krea2Pipeline` | Krea 2 (Raw + Turbo) | `krea/Krea-2-Raw`, `krea/Krea-2-Turbo` | ✅︎ | | | | — |
| `WanPipeline` | Wan2.1-T2V, Wan2.2-T2V, Wan2.2-TI2V | `Wan-AI/Wan2.1-T2V-1.3B-Diffusers`, `Wan-AI/Wan2.1-T2V-14B-Diffusers`, `Wan-AI/Wan2.2-T2V-A14B-Diffusers`, `Wan-AI/Wan2.2-TI2V-5B-Diffusers` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
| `WanImageToVideoPipeline` | Wan2.2-I2V | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
| `LingBotWorldCausalDMDPipeline` | LingBot-World 2.0 v1 (experimental) | `robbyant/lingbot-world-v2-14b-causal-fast-diffusers` | Experimental | | | | — |
| `Cosmos3OmniDiffusersPipeline` | Cosmos3 T2I, T2V, I2V, V2V, T2V with sound, action policy | `nvidia/Cosmos3-Nano`, `nvidia/Cosmos3-Super` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
| `Wan22S2VPipeline` | Wan2.2-S2V | `Wan-AI/Wan2.2-S2V-14B` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
| `Wan22VACEPipeline` | Wan2.1-VACE | `Wan-AI/Wan2.1-VACE-1.3B-diffusers`, `Wan-AI/Wan2.1-VACE-14B-diffusers` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
@@ -117,6 +118,10 @@ support metadata until their recipe is audited.
✅︎ indicates the model is supported on that backend. Empty cells mean not listed as supported on that backend.
See the [LingBot-World 2.0 recipe](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Robbyant/LingBot-World-2.0.md)
for offline and experimental realtime usage, supported controls, validation,
and current limitations.
<sup>H3</sup> MiniMax H3 AMD GPU support is validated on gfx942 (MI300X) and
gfx950 (MI350) in BF16, using the AITER `FLASH_ATTN` diffusion attention backend.
Other AMD SKUs (e.g. MI325X) are not listed until their
@@ -0,0 +1,288 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Generate a LingBot-World v2 video from an image and camera trajectory.
The official checkpoint is licensed separately under CC BY-NC-SA and is
restricted to non-commercial use. This vLLM-Omni integration remains
Apache-2.0 licensed.
Example:
python examples/offline_inference/diffusion/lingbot_world_v2.py \
--prompt "The camera moves slowly forward through the scene." \
--image /path/to/first_frame.png \
--action-dir /path/to/actions/forward \
--output lingbot_world_v2.mp4
``--action-dir`` must contain ``poses.npy`` and ``intrinsics.npy``. The
example configures its parent as the trusted action root and sends the
contained directory through ``extra_args["action_path"]``.
"""
from __future__ import annotations
import argparse
import math
from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import numpy as np
_MODEL = "robbyant/lingbot-world-v2-14b-causal-fast-diffusers"
_NUM_INFERENCE_STEPS = 4
_MAX_SEQUENCE_LENGTH = 512
_MAX_PIXEL_AREA = 480 * 832
_MAX_RAW_FRAMES = 117
_MAX_SOURCE_CAMERA_FRAMES = 4096
_TEMPORAL_COMPRESSION = 4
_LATENT_FRAMES_PER_BLOCK = 3
_SPATIAL_ALIGNMENT = 16
@dataclass(frozen=True)
class LingBotPaths:
image: Path
action_dir: Path
action_root: Path
action_relative: Path
camera_frames: int
output: Path
def _validate_camera_arrays(action_dir: Path) -> int:
"""Validate the bounded camera contract before starting the engine."""
try:
poses = np.load(action_dir / "poses.npy", mmap_mode="r", allow_pickle=False)
intrinsics = np.load(action_dir / "intrinsics.npy", mmap_mode="r", allow_pickle=False)
except (OSError, TypeError, ValueError):
raise ValueError("Camera files must be valid numeric poses.npy and intrinsics.npy arrays.") from None
if poses.ndim != 3 or poses.shape[1:] != (4, 4):
raise ValueError("poses.npy must have shape [frames, 4, 4].")
if intrinsics.ndim != 2 or intrinsics.shape[1:] != (4,):
raise ValueError("intrinsics.npy must have shape [frames, 4].")
if poses.shape[0] != intrinsics.shape[0]:
raise ValueError("poses.npy and intrinsics.npy must contain the same frame count.")
if not 1 <= poses.shape[0] <= _MAX_SOURCE_CAMERA_FRAMES:
raise ValueError("Camera arrays must contain between 1 and 4096 source frames.")
if poses.dtype.kind not in "fiu" or intrinsics.dtype.kind not in "fiu":
raise ValueError("Camera arrays must contain real numeric values.")
if not np.isfinite(poses).all() or not np.isfinite(intrinsics).all():
raise ValueError("Camera arrays must contain only finite values.")
return int(poses.shape[0])
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Generate a LingBot-World v2 video from a first-frame image and camera trajectory."
)
parser.add_argument("--model", default=_MODEL, help="Official Hugging Face model ID or local checkpoint path.")
parser.add_argument("--prompt", required=True, help="Text description of the scene and desired motion.")
parser.add_argument("--image", required=True, help="Path to the first-frame image.")
parser.add_argument(
"--action-dir",
required=True,
help="Camera action directory containing poses.npy and intrinsics.npy.",
)
parser.add_argument("--height", type=int, default=480, help="Output height; must be divisible by 16.")
parser.add_argument("--width", type=int, default=832, help="Output width; must be divisible by 16.")
parser.add_argument(
"--num-frames",
type=int,
default=9,
help="Raw output frame count. Nine frames are one three-latent-frame causal block.",
)
parser.add_argument("--seed", type=int, default=42, help="Random seed.")
parser.add_argument(
"--tensor-parallel-size",
type=int,
default=1,
help="Number of GPUs used for tensor parallelism inside the DiT.",
)
parser.add_argument("--flow-shift", type=float, default=5.0, help="Positive FlowUniPC scheduler shift.")
parser.add_argument("--fps", type=int, default=16, help="Frames per second in the exported MP4.")
parser.add_argument("--output", default="lingbot_world_v2.mp4", help="Output MP4 path.")
parser.add_argument(
"--enforce-eager",
action="store_true",
help="Disable compilation and force eager execution.",
)
return parser.parse_args(argv)
def resolve_cli_paths(args: argparse.Namespace) -> LingBotPaths:
"""Canonicalize local inputs and construct a safe trusted-root mapping."""
image = Path(args.image).expanduser().resolve()
if not image.is_file():
raise ValueError("--image must point to an existing file.")
action_dir = Path(args.action_dir).expanduser().resolve()
if not action_dir.is_dir():
raise ValueError("--action-dir must point to an existing directory.")
if not all((action_dir / name).is_file() for name in ("poses.npy", "intrinsics.npy")):
raise ValueError("--action-dir must contain poses.npy and intrinsics.npy.")
camera_frames = _validate_camera_arrays(action_dir)
action_root = action_dir.parent.resolve()
try:
action_relative = action_dir.relative_to(action_root)
except ValueError:
raise ValueError("--action-dir must be contained by its canonical trusted action root.") from None
if not action_relative.parts:
raise ValueError("--action-dir must name a directory contained by a trusted action root.")
output = Path(args.output).expanduser().resolve()
return LingBotPaths(
image=image,
action_dir=action_dir,
action_root=action_root,
action_relative=action_relative,
camera_frames=camera_frames,
output=output,
)
def _positive_finite(value: float, flag: str) -> float:
result = float(value)
if not math.isfinite(result) or result <= 0:
raise ValueError(f"{flag} must be a positive finite number.")
return result
def build_omni_kwargs(
args: argparse.Namespace,
paths: LingBotPaths,
*,
parallel_config: Any,
) -> dict[str, Any]:
"""Build Omni configuration without overriding checkpoint class discovery."""
if args.tensor_parallel_size <= 0:
raise ValueError("--tensor-parallel-size must be a positive integer.")
flow_shift = _positive_finite(args.flow_shift, "--flow-shift")
model_path = Path(args.model).expanduser()
model = str(model_path.resolve()) if model_path.exists() else args.model
return {
"model": model,
"flow_shift": flow_shift,
"parallel_config": parallel_config,
"enforce_eager": args.enforce_eager,
"model_config": {"lingbot_action_root": str(paths.action_root)},
}
def build_request(
args: argparse.Namespace,
paths: LingBotPaths,
) -> tuple[dict[str, Any], dict[str, Any]]:
"""Build the prompt and fixed v1 causal-DMD sampling contract."""
prompt_text = args.prompt.strip()
if not prompt_text:
raise ValueError("--prompt must contain non-whitespace text.")
if args.height <= 0 or args.width <= 0:
raise ValueError("--height and --width must be positive integers.")
if args.height % _SPATIAL_ALIGNMENT or args.width % _SPATIAL_ALIGNMENT:
raise ValueError("--height and --width must each be divisible by 16.")
if args.height * args.width > _MAX_PIXEL_AREA:
raise ValueError("--height * --width must not exceed 480 * 832 pixels.")
if args.num_frames <= 0:
raise ValueError("--num-frames must be a positive integer.")
if args.num_frames > _MAX_RAW_FRAMES:
raise ValueError("--num-frames must not exceed 117 raw frames.")
if args.num_frames > paths.camera_frames:
raise ValueError("Camera arrays must contain at least --num-frames frames.")
if (args.num_frames - 1) % _TEMPORAL_COMPRESSION:
raise ValueError("--num-frames must satisfy (num_frames - 1) divisible by 4.")
latent_frames = (args.num_frames - 1) // _TEMPORAL_COMPRESSION + 1
if latent_frames % _LATENT_FRAMES_PER_BLOCK:
raise ValueError("--num-frames must map to whole three-frame latent blocks.")
if args.fps <= 0:
raise ValueError("--fps must be a positive integer.")
flow_shift = _positive_finite(args.flow_shift, "--flow-shift")
prompt = {
"prompt": prompt_text,
"multi_modal_data": {"image": str(paths.image)},
}
sampling_kwargs = {
"height": args.height,
"width": args.width,
"num_frames": args.num_frames,
"num_inference_steps": _NUM_INFERENCE_STEPS,
"max_sequence_length": _MAX_SEQUENCE_LENGTH,
"seed": args.seed,
"fps": args.fps,
"extra_args": {
"action_path": paths.action_relative.as_posix(),
"flow_shift": flow_shift,
},
}
return prompt, sampling_kwargs
def extract_video_array(outputs: Any) -> np.ndarray:
"""Unwrap one standard Omni diffusion video into ``[frames, H, W, C]``."""
if not isinstance(outputs, list) or len(outputs) != 1:
raise ValueError("Expected exactly one Omni output for the single LingBot request.")
output = outputs[0]
request_output = getattr(output, "request_output", None)
if request_output is not None and getattr(request_output, "images", None):
output = request_output
images = getattr(output, "images", None)
if not images:
multimodal_output = getattr(output, "multimodal_output", None) or {}
images = multimodal_output.get("video")
if images is None:
raise ValueError("No video frames found in Omni output.")
if isinstance(images, list) and len(images) == 1:
images = images[0]
video: np.ndarray = np.asarray(images)
if video.ndim == 5:
if video.shape[0] != 1:
raise ValueError(f"Expected one generated video, got batch shape {video.shape}.")
video = video[0]
if video.ndim != 4 or video.shape[-1] not in (3, 4):
raise ValueError(f"Expected video shape [frames, height, width, channels], got {video.shape}.")
return video
def main(argv: Sequence[str] | None = None) -> Path:
args = parse_args(argv)
paths = resolve_cli_paths(args)
prompt, sampling_kwargs = build_request(args, paths)
paths.output.parent.mkdir(parents=True, exist_ok=True)
# Keep vLLM imports below pure CLI/path validation so ``--help`` and the
# helper tests work on machines without the native vLLM runtime.
from diffusers.utils import export_to_video
from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.entrypoints.omni import Omni
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
parallel_config = DiffusionParallelConfig(tensor_parallel_size=args.tensor_parallel_size)
omni_kwargs = build_omni_kwargs(args, paths, parallel_config=parallel_config)
omni = Omni(**omni_kwargs)
try:
outputs = omni.generate(
prompt,
OmniDiffusionSamplingParams(**sampling_kwargs),
use_tqdm=False,
)
finally:
omni.close()
frames = extract_video_array(outputs)
export_to_video(frames, str(paths.output), fps=args.fps)
print(f"Saved {frames.shape[0]} frames to {paths.output}")
return paths.output
if __name__ == "__main__":
main()
@@ -0,0 +1,239 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Run LingBot-World 2.0 as an in-process realtime AR-Diffusion session.
This example exercises the internal session API. It is not a public HTTP or
WebSocket protocol. Each JSONL event advances the world by one three-latent
frame AR block and writes the latent plus its identity metadata. The current
scoped implementation supports at most ten ticks per generation epoch because
its image condition is bounded to 117 pixel frames; reset or create a session
to start a new world.
"""
from __future__ import annotations
import argparse
import asyncio
import json
import math
import time
from collections.abc import Sequence
from pathlib import Path
from typing import Any
_MODEL = "robbyant/lingbot-world-v2-14b-causal-fast-diffusers"
_CAMERA_ACTION_SCHEMA = "lingbot.camera_actions.v1"
_FRAMES_PER_BLOCK = 3
# ((117 pixel frames - 1) / VAE temporal factor 4 + 1) / 3 latent frames.
_MAX_REALTIME_TICKS = 10
def _camera_event_data(frames: list[list[str]]) -> dict[str, Any]:
"""Build the event-side script consumed by LingBotCameraControlReducer."""
return {"mode": "script", "frames": frames}
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run an in-process realtime LingBot-World 2.0 session.")
parser.add_argument("--model", default=_MODEL, help="Hugging Face model ID or local checkpoint path.")
parser.add_argument("--image", required=True, help="Initial RGB image.")
parser.add_argument("--prompt", required=True, help="Initial scene prompt.")
parser.add_argument(
"--events",
required=True,
help="JSONL file with one prompt/action event per AR block; at most 10 events.",
)
parser.add_argument("--output-dir", required=True, help="Directory for chunk latents and metadata.")
parser.add_argument("--session-id", default="lingbot-world", help="Persistent world session identifier.")
parser.add_argument("--height", type=int, default=480)
parser.add_argument("--width", type=int, default=832)
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--gpu-memory-fraction", type=float, default=0.1)
parser.add_argument("--tensor-parallel-size", type=int, default=1)
parser.add_argument("--enforce-eager", action="store_true")
return parser.parse_args(argv)
def _load_events(path: Path) -> list[dict[str, Any]]:
events: list[dict[str, Any]] = []
for line_number, raw_line in enumerate(path.read_text().splitlines(), start=1):
line = raw_line.strip()
if not line:
continue
try:
value = json.loads(line)
except json.JSONDecodeError as exc:
raise ValueError(f"events line {line_number} is not valid JSON: {exc.msg}.") from None
if not isinstance(value, dict):
raise ValueError(f"events line {line_number} must be a JSON object.")
event_id = value.get("event_id")
if isinstance(event_id, bool) or not isinstance(event_id, int) or event_id < 0:
raise ValueError(f"events line {line_number} requires a non-negative integer event_id.")
prompt = value.get("prompt")
if prompt is not None and (not isinstance(prompt, str) or not prompt.strip()):
raise ValueError(f"events line {line_number} prompt must be a non-empty string.")
frames = value.get("frames")
if frames is not None:
if not isinstance(frames, list) or len(frames) != _FRAMES_PER_BLOCK:
raise ValueError(f"events line {line_number} frames must contain exactly three lists.")
for frame in frames:
if not isinstance(frame, list) or any(not isinstance(action, str) for action in frame):
raise ValueError(f"events line {line_number} frames must contain only action strings.")
if prompt is None and frames is None:
raise ValueError(f"events line {line_number} must update prompt and/or frames.")
events.append({"event_id": event_id, "prompt": prompt, "frames": frames})
if not events:
raise ValueError("events file must contain at least one event.")
if len(events) > _MAX_REALTIME_TICKS:
raise ValueError(
"events file must contain at most "
f"{_MAX_REALTIME_TICKS} events because the current LingBot realtime "
"image-condition horizon is 117 pixel frames."
)
event_ids = [event["event_id"] for event in events]
if event_ids != sorted(set(event_ids)):
raise ValueError("event_id values must be unique and strictly increasing.")
return events
def _validate_args(args: argparse.Namespace) -> tuple[Path, Path, Path]:
image = Path(args.image).expanduser().resolve()
events = Path(args.events).expanduser().resolve()
output_dir = Path(args.output_dir).expanduser().resolve()
if not image.is_file():
raise ValueError("--image must point to an existing file.")
if not events.is_file():
raise ValueError("--events must point to an existing JSONL file.")
if not args.prompt.strip():
raise ValueError("--prompt must contain non-whitespace text.")
if args.height <= 0 or args.width <= 0 or args.height % 16 or args.width % 16:
raise ValueError("--height and --width must be positive multiples of 16.")
if args.tensor_parallel_size <= 0:
raise ValueError("--tensor-parallel-size must be positive.")
if not math.isfinite(args.gpu_memory_fraction) or not 0 < args.gpu_memory_fraction <= 1:
raise ValueError("--gpu-memory-fraction must be in (0, 1].")
return image, events, output_dir
async def run(argv: Sequence[str] | None = None) -> Path:
args = parse_args(argv)
image, events_path, output_dir = _validate_args(args)
events = _load_events(events_path)
output_dir.mkdir(parents=True, exist_ok=True)
# Imports stay below pure input validation so --help and helper tests do not
# require a CUDA-enabled vLLM installation.
import torch
from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.diffusion.models.lingbot_world.actions import LingBotCameraControlReducer
from vllm_omni.entrypoints.async_omni import AsyncOmni
from vllm_omni.experimental.ar_diffusion.consumer import ARDiffusionOmniTickConsumer
from vllm_omni.experimental.ar_diffusion.session import (
ARDiffusionSessionEvent,
ARDiffusionSessionManager,
ARDiffusionWorkerLifecycle,
)
from vllm_omni.experimental.ar_diffusion.tick_protocol import ARDiffusionControlInput
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
engine = AsyncOmni(
model=args.model,
engine_backend="vllm_omni.experimental.ar_diffusion.engine.ARDiffusionEngine",
enforce_eager=args.enforce_eager,
parallel_config=DiffusionParallelConfig(tensor_parallel_size=args.tensor_parallel_size),
max_num_seqs=1,
model_config={
"ar_diffusion_height": args.height,
"ar_diffusion_width": args.width,
"ar_diffusion_kv_config": {
"gpu_memory_fraction": args.gpu_memory_fraction,
"warmup_cudagraph": True,
},
},
)
sampling = OmniDiffusionSamplingParams(
height=args.height,
width=args.width,
num_frames=9,
num_inference_steps=4,
max_sequence_length=512,
seed=args.seed,
output_type="latent",
extra_args={"flow_shift": 5.0},
)
consumer = ARDiffusionOmniTickConsumer(
engine,
prompt_provider=lambda tick: {
"prompt": tick.prompt,
"multi_modal_data": {"image": str(image)},
},
sampling_params_list=[sampling],
diffusion_stage_id=0,
)
manager = ARDiffusionSessionManager(
tick_consumer=consumer,
lifecycle=ARDiffusionWorkerLifecycle(engine, stage_ids=[0], timeout=180.0),
max_pending_events=32,
control_reducer_factory=LingBotCameraControlReducer,
)
session = await manager.create_session(args.session_id)
measurements: list[dict[str, Any]] = []
try:
for chunk_index, event in enumerate(events):
controls = ()
if event["frames"] is not None:
controls = (
ARDiffusionControlInput(
track="camera",
schema=_CAMERA_ACTION_SCHEMA,
data=_camera_event_data(event["frames"]),
),
)
await session.accept_event(
ARDiffusionSessionEvent(
event_id=event["event_id"],
prompt=event["prompt"]
if event["prompt"] is not None
else (args.prompt if chunk_index == 0 else None),
controls=controls,
)
)
started = time.perf_counter()
output = await session.next_chunk()
elapsed = time.perf_counter() - started
if len(output.images) != 1 or not isinstance(output.images[0], torch.Tensor):
raise RuntimeError("Expected one latent tensor from each realtime LingBot chunk.")
latent = output.images[0].detach().float().cpu()
metadata = consumer.chunk_metadata(output).to_dict()
torch.save(latent, output_dir / f"chunk_{chunk_index:03d}.pt")
(output_dir / f"chunk_{chunk_index:03d}.json").write_text(
json.dumps(metadata, indent=2, sort_keys=True) + "\n"
)
measurements.append(
{
"chunk_index": chunk_index,
"latency_seconds": elapsed,
"shape": list(latent.shape),
"finite": bool(torch.isfinite(latent).all()),
"metadata": metadata,
}
)
print(json.dumps(measurements[-1], sort_keys=True), flush=True)
finally:
try:
await manager.close_session(args.session_id)
finally:
engine.shutdown()
summary_path = output_dir / "summary.json"
summary_path.write_text(json.dumps({"chunks": measurements}, indent=2, sort_keys=True) + "\n")
return summary_path
def main(argv: Sequence[str] | None = None) -> Path:
return asyncio.run(run(argv))
if __name__ == "__main__":
main()
+1
View File
@@ -60,6 +60,7 @@ recipes/
| [`meituan-longcat/LongCat-Video-Avatar-1.5.md`](./meituan-longcat/LongCat-Video-Avatar-1.5.md) | Audio-driven avatar video generation (AT2V / AI2V, single- and multi-speaker, AVC continuation) | 1x H100 80GB |
| [`mistralai/Voxtral-TTS.md`](./mistralai/Voxtral-TTS.md) | Online serving for TTS | 1x RTX 4090 24GB |
| [`Robbyant/LingBot-Video.md`](./Robbyant/LingBot-Video.md) | Native dense and MoE text-to-video serving | Dense: 1x L20X; MoE: 1x L20X (~67.7 GiB peak) |
| [`Robbyant/LingBot-World-2.0.md`](./Robbyant/LingBot-World-2.0.md) | Offline and experimental realtime interactive world generation | 1x H200/B200 or 2x B200 for TP=2 |
| [`cosmos3/Cosmos3-Nano.md`](./cosmos3/Cosmos3-Nano.md) | Text-to-image, text-to-video, image-to-video, video-to-video generation, text to video with sound, action policy | 1x H200 141GB / B300 |
| [`cosmos3/Cosmos3-Edge.md`](./cosmos3/Cosmos3-Edge.md) | T2I / T2V / I2V + action (Physical-AI) generation on the Nemotron-based Edge transformer | 1x GPU (~8 GiB) |
| [`cosmos3/Cosmos3-Super.md`](./cosmos3/Cosmos3-Super.md) | 64B T2I / T2V / I2V / V2V generation (+ optional audio) / Action policy | 8x H200/H100/A100 / 2x H200 / B300 |
+122
View File
@@ -0,0 +1,122 @@
# LingBot-World 2.0
> Offline and experimental realtime interactive world generation
## Summary
- Vendor: Robbyant
- Model: `robbyant/lingbot-world-v2-14b-causal-fast-diffusers`
- Task: image-conditioned interactive world generation
- Modes: offline trajectory replay and in-process realtime AR-Diffusion ticks
- Hardware validated: NVIDIA H200 and B200
- Maintainer: Community
The checkpoint is separately licensed under CC BY-NC-SA and restricted to
non-commercial use. The vLLM-Omni integration code remains Apache-2.0.
## Offline generation
The offline path consumes one source image and an action directory containing
`poses.npy` with shape `[frames, 4, 4]` and `intrinsics.npy` with shape
`[frames, 4]`.
```bash
python examples/offline_inference/diffusion/lingbot_world_v2.py \
--prompt "The camera moves slowly forward through the scene." \
--image /path/to/first_frame.png \
--action-dir /path/to/actions/forward \
--num-frames 81 \
--output lingbot_world_v2.mp4
```
The current causal-fast checkpoint uses four DMD sampling steps. Raw frame
counts must be `9 + 12k`, up to 117 frames. Camera files may contain a
longer bounded source trajectory (the official example contains 269 frames);
the runtime consumes only the prefix needed by the request.
## Realtime in-process generation
The realtime example keeps AR-Diffusion KV and model-owned state across
requests. Each JSONL line describes the prompt and/or three latent-frame camera
actions applied at the next chunk boundary:
```json
{"event_id":1,"prompt":"A road through a forest","frames":[["j"],[],[]]}
{"event_id":2,"frames":[["w"],["w"],["w"]]}
{"event_id":3,"prompt":"The road enters a snowy valley","frames":[[],[],[]]}
```
Run:
```bash
python examples/offline_inference/diffusion/lingbot_world_v2_realtime.py \
--image /path/to/first_frame.png \
--events /path/to/events.jsonl \
--output-dir /tmp/lingbot-realtime \
--gpu-memory-fraction 0.6
```
The example writes one latent tensor and one metadata JSON file per chunk. It
exercises the same `ARDiffusionSessionManager -> ARDiffusionOmniTickConsumer
-> AsyncOmni -> ARDiffusionEngine` path used by a future HTTP or WebSocket
transport.
This PR intentionally does not define a public realtime HTTP/WebSocket schema.
An online serving client should be added together with the public transport
API rather than exposing LingBot-specific event fields from the generic model
runtime. The structured camera-interaction frontend and transport work is
tracked separately in
[vllm-project/vllm-omni#5527](https://github.com/vllm-project/vllm-omni/pull/5527).
## Realtime identity and controls
- `session_id` identifies the persistent world and its worker-owned state.
- `event_id` identifies a prompt/control update and remains monotonic across reset.
- `chunk_index` is contiguous from zero and restarts from zero after reset.
- `request_id` correlates one chunk snapshot with its output metadata.
- AsyncOmni uses a separate UUID-suffixed internal engine routing ID.
The generic runtime transports controls as opaque snapshots. LingBot's adapter
accepts:
- `lingbot.camera_actions.v1` for per-latent-frame key states such as `w`, `a`,
`s`, `d`, `i`, `j`, `k`, and `l`;
- `lingbot.camera_trajectory.v1` for explicit pose/intrinsics trajectories.
## Validation
Real-checkpoint validation uses 480x832 output, four DMD steps, and seed 42.
The exercised matrix includes:
- TP=1 and TP=2 execution;
- two interleaved resident sessions;
- action input and prompt switching;
- seven contiguous chunks crossing the sink plus recent rolling window;
- direct versus paged replay;
- CUDA Graph execution;
- reset, close, failure cleanup, and exact metadata matching; and
- nine-frame VAE decode.
Official `generate.py` accuracy and performance numbers, together with
generation artifacts, are recorded in the PR validation section for the exact
tested commit.
## Current limitations
- Only the 14B causal-fast checkpoint is supported.
- The realtime control plane is internal; there is no public server transport yet.
- AR-Diffusion stages currently require one replica because session-affine
routing across replicas is not implemented.
- One AR block is generated per request and `max_num_seqs` must be one.
- Stateful streaming VAE decode is not implemented; the realtime example emits
latent chunks.
- SP/USP, pipeline/CFG parallelism, HSDP, VAE parallelism, quantization,
Cache-DiT, TeaCache, causal-pretrain, and the 1.3B checkpoint are not claimed.
- No AMD GPU, Ascend NPU, or Intel GPU support is claimed.
## References
- Checkpoint: <https://huggingface.co/robbyant/lingbot-world-v2-14b-causal-fast-diffusers>
- Official implementation: <https://github.com/robbyant/lingbot-world-v2>
- Offline example: [`examples/offline_inference/diffusion/lingbot_world_v2.py`](../../examples/offline_inference/diffusion/lingbot_world_v2.py)
- Realtime design: [`docs/design/feature/realtime_ar_diffusion.md`](../../docs/design/feature/realtime_ar_diffusion.md)
@@ -18,6 +18,7 @@ from vllm_omni.experimental.ar_diffusion.capability import (
)
from vllm_omni.experimental.ar_diffusion.kv_cache import ARDiffusionKVConfig
from vllm_omni.experimental.ar_diffusion.runner import ARDiffusionModelRunner
from vllm_omni.experimental.ar_diffusion.tick_protocol import ARDiffusionTickRequest
BLOCK = 16
POS = "positive"
@@ -56,6 +57,21 @@ def dreamzero_like_spec(*, capacity: int = 2) -> ARDiffusionKVCacheSpec:
)
def tiny_spec(*, capacity: int = 2) -> ARDiffusionKVCacheSpec:
return ARDiffusionKVCacheSpec(
num_layers=1,
num_kv_heads=1,
head_size=1,
tokens_per_frame=1,
frames_per_block=3,
window_frames=3,
sink_frames=3,
kv_branches=(ARDiffusionKVBranchSpec("main", 0),),
session_capacity=capacity,
cross_attention=(ARDiffusionCrossAttentionKVSpec("text", 2),),
)
class CapablePipeline:
def __init__(self, spec: ARDiffusionKVCacheSpec) -> None:
self.spec = spec
@@ -103,6 +119,7 @@ def make_runner(
*,
available_bytes: int = 1 << 28,
step_execution: bool = False,
gpu_memory_fraction: float = 0.1,
) -> ARDiffusionModelRunner:
runner = object.__new__(ARDiffusionModelRunner)
runner.od_config = SimpleNamespace(
@@ -113,7 +130,10 @@ def make_runner(
)
runner.device = torch.device("cpu")
runner.pipeline = pipeline
runner.ar_diffusion_kv_config = ARDiffusionKVConfig(enable=True)
runner.ar_diffusion_kv_config = ARDiffusionKVConfig(
enable=True,
gpu_memory_fraction=gpu_memory_fraction,
)
runner.kv_cache = None
runner._ar_diffusion_capability = None
runner._ar_diffusion_kv_cache_spec = None
@@ -132,6 +152,16 @@ def commit_one_frame(runner: ARDiffusionModelRunner, session_id: str, kv_branch:
return state
def commit_one_block(runner: ARDiffusionModelRunner, session_id: str, kv_branch: str):
state = runner._get_or_create_session(session_id)
assert runner.kv_cache is not None
seq_len = BLOCK * runner.kv_cache.frames_per_block
ctx = state.get_kv_caches(kv_branch, seq_len=seq_len, commit_current=True)[0].forward_ctx
ctx.ensure_video_slots(torch.device("cpu"))
state.commit_paged_context(kv_branch)
return state
def test_ar_runner_rejects_pipeline_without_capability():
runner = object.__new__(ARDiffusionModelRunner)
runner.od_config = SimpleNamespace(max_num_seqs=1)
@@ -140,6 +170,43 @@ def test_ar_runner_rejects_pipeline_without_capability():
runner._preallocate_kv_cache(available_bytes=1 << 20)
def test_runner_uses_typed_tick_as_authoritative_session_contract():
tick = ARDiffusionTickRequest(
session_id="world-7",
request_id="request-3",
chunk_index=3,
reset=True,
)
req = SimpleNamespace(
request_id="request-3",
sampling_params=SimpleNamespace(extra_args=tick.to_extra_args()),
)
session_id, extra_args, parsed = ARDiffusionModelRunner._request_session(req)
assert session_id == "world-7"
assert extra_args == tick.to_extra_args()
assert parsed == tick
def test_runner_keeps_engine_request_id_separate_from_tick_correlation_id():
tick = ARDiffusionTickRequest(
session_id="world-7",
request_id="client-request-3",
chunk_index=3,
)
req = SimpleNamespace(
request_id="engine-request-uuid",
sampling_params=SimpleNamespace(extra_args=tick.to_extra_args()),
)
session_id, _, parsed = ARDiffusionModelRunner._request_session(req)
assert session_id == "world-7"
assert req.request_id == "engine-request-uuid"
assert parsed.request_id == "client-request-3"
def test_lingbot_like_single_branch_session_reuse_reset_and_close():
pipeline = CapablePipeline(lingbot_like_spec())
runner = make_runner(pipeline)
@@ -170,6 +237,31 @@ def test_lingbot_like_single_branch_session_reuse_reset_and_close():
assert pipeline.closes == ["s1"]
def test_lingbot_like_interleaved_sessions_keep_independent_kv_partitions():
runner = make_runner(CapablePipeline(lingbot_like_spec(capacity=2)))
kv = runner.kv_cache
assert kv is not None
session_a = commit_one_block(runner, "world-a", "main")
a0_blocks = kv.window_block_ids(session_a.adapter("main"))
session_b = commit_one_block(runner, "world-b", "main")
b0_blocks = kv.window_block_ids(session_b.adapter("main"))
session_a_again = commit_one_block(runner, "world-a", "main")
a1_blocks = kv.window_block_ids(session_a_again.adapter("main"))
assert session_a_again is session_a
assert session_a.adapter("main").request_id == "ar::world-a::main"
assert session_b.adapter("main").request_id == "ar::world-b::main"
assert session_a.adapter("main").completed_chunks == 6
assert session_b.adapter("main").completed_chunks == 3
assert len(a0_blocks) == 3
assert len(b0_blocks) == 3
assert len(a1_blocks) == 6
assert set(a0_blocks) <= set(a1_blocks)
assert set(a1_blocks).isdisjoint(b0_blocks)
assert tuple(runner._sessions) == ("world-b", "world-a")
def test_lingbot_like_sink_survives_sliding_window_eviction():
runner = make_runner(CapablePipeline(lingbot_like_spec()))
kv = runner.kv_cache
@@ -198,12 +290,12 @@ def test_dreamzero_like_two_branches_are_independent():
def test_lru_eviction_releases_blocks_and_notifies_pipeline():
pipeline = CapablePipeline(lingbot_like_spec(capacity=8))
pipeline = CapablePipeline(lingbot_like_spec(capacity=2))
runner = make_runner(pipeline)
kv = runner.kv_cache
assert kv is not None
assert runner._session_capacity == 1
assert kv.session_capacity == 1
assert runner._session_capacity == 2
assert kv.session_capacity == 2
free_total = kv.manager.block_pool.get_num_free_blocks()
old = commit_one_frame(runner, "old", "main")
k = torch.randn(1, 8, 4, 64)
@@ -211,13 +303,56 @@ def test_lru_eviction_releases_blocks_and_notifies_pipeline():
assert kv.manager.block_pool.get_num_free_blocks() < free_total
runner._get_or_create_session("new")
assert tuple(runner._sessions) == ("old", "new")
runner._get_or_create_session("newest")
assert tuple(runner._sessions) == ("new",)
assert tuple(runner._sessions) == ("new", "newest")
assert pipeline.closes == ["old"]
assert "old" not in kv._cross_sessions
assert kv.manager.block_pool.get_num_free_blocks() == free_total
def test_budget_reduced_capacity_drives_runner_lru():
pipeline = CapablePipeline(tiny_spec(capacity=2))
# One tiny LingBot-like session requires 128 bytes; two require 192.
runner = make_runner(
pipeline,
available_bytes=128,
gpu_memory_fraction=1.0,
)
kv = runner.kv_cache
assert kv is not None
assert kv.requested_session_capacity == 2
assert kv.session_capacity == 1
assert runner._session_capacity == 1
runner._get_or_create_session("old")
runner._get_or_create_session("new")
assert tuple(runner._sessions) == ("new",)
assert pipeline.closes == ["old"]
def test_dreamzero_like_requested_capacity_is_capped_by_budget():
spec = dreamzero_like_spec(capacity=64)
# Per all-layer self-KV page: 65,536 bytes. Two resident sessions need:
# managed=(2 * (2 * 6 + 4) + 2)=34 pages, scratch=8 pages,
# cross-attention=1 page/session, for 44 pages total.
page_bytes = 65_536
pipeline = CapablePipeline(spec)
runner = make_runner(
pipeline,
available_bytes=44 * page_bytes,
gpu_memory_fraction=1.0,
)
kv = runner.kv_cache
assert kv is not None
assert kv.requested_session_capacity == 64
assert kv.session_capacity == 2
assert runner._session_capacity == 2
assert kv.cross_attention_reserved_bytes == 2 * page_bytes
def test_forward_exception_releases_pending_allocation_and_model_state(monkeypatch):
pipeline = CapablePipeline(lingbot_like_spec())
runner = make_runner(pipeline)
@@ -232,7 +367,10 @@ def test_forward_exception_releases_pending_allocation_and_model_state(monkeypat
raise RuntimeError("layer exploded")
monkeypatch.setattr(DiffusionModelRunner, "execute_model", boom)
request = SimpleNamespace(sampling_params=SimpleNamespace(extra_args={"session_id": "broken"}))
request = SimpleNamespace(
request_id="broken-request",
sampling_params=SimpleNamespace(extra_args={"session_id": "broken"}),
)
with pytest.raises(RuntimeError, match="layer exploded"):
runner.execute_model(request)
@@ -263,7 +401,10 @@ def test_synchronize_exception_uses_forward_cleanup_path(monkeypatch):
monkeypatch.setattr(DiffusionModelRunner, "execute_model", return_after_allocation)
monkeypatch.setattr(torch.accelerator, "synchronize", synchronize_boom)
runner.device = torch.device("cuda")
request = SimpleNamespace(sampling_params=SimpleNamespace(extra_args={"session_id": "broken"}))
request = SimpleNamespace(
request_id="broken-request",
sampling_params=SimpleNamespace(extra_args={"session_id": "broken"}),
)
with pytest.raises(RuntimeError, match="asynchronous kernel failed"):
runner.execute_model(request)
@@ -0,0 +1,231 @@
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
from collections.abc import AsyncIterator, Sequence
from typing import Any
import pytest
from vllm_omni.experimental.ar_diffusion.consumer import ARDiffusionOmniTickConsumer
from vllm_omni.experimental.ar_diffusion.session import (
ARDiffusionSession,
ARDiffusionSessionEvent,
)
from vllm_omni.experimental.ar_diffusion.tick_protocol import (
ARDiffusionChunkMetadata,
ARDiffusionControlInput,
ARDiffusionTickRequest,
)
from vllm_omni.inputs.data import (
OmniDiffusionSamplingParams,
OmniPromptType,
OmniSamplingParams,
)
from vllm_omni.outputs import OmniRequestOutput
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class FakeStagePool:
def __init__(self, num_replicas: int = 1) -> None:
self.num_replicas = num_replicas
class FakeEngine:
def __init__(self, num_replicas: int = 1) -> None:
self.stage_pools = [FakeStagePool(num_replicas)]
class FakeGenerateClient:
def __init__(
self,
*,
include_metadata: bool = True,
applied_event_ids: list[int] | None = None,
output_error: str | None = None,
num_replicas: int = 1,
) -> None:
self.engine = FakeEngine(num_replicas)
self.include_metadata = include_metadata
self.applied_event_ids = applied_event_ids
self.output_error = output_error
self.calls: list[dict[str, Any]] = []
async def generate(
self,
prompt: OmniPromptType,
*,
request_id: str,
sampling_params_list: Sequence[OmniSamplingParams],
output_modalities: list[str] | None = None,
) -> AsyncIterator[OmniRequestOutput]:
self.calls.append(
{
"prompt": prompt,
"request_id": request_id,
"sampling_params_list": sampling_params_list,
"output_modalities": output_modalities,
}
)
params = sampling_params_list[0]
assert isinstance(params, OmniDiffusionSamplingParams)
tick = ARDiffusionTickRequest.from_extra_args(params.extra_args)
assert tick is not None
chunk_metadata = ARDiffusionChunkMetadata.from_tick(tick).to_dict()
if self.applied_event_ids is not None:
chunk_metadata["applied_event_ids"] = self.applied_event_ids
metadata = {"metadata": {"ar_diffusion": chunk_metadata}} if self.include_metadata else {}
yield OmniRequestOutput(
request_id=request_id,
stage_id=0,
final_output_type="video",
images=["chunk"], # type: ignore[list-item]
_multimodal_output=metadata,
finished=True,
error=self.output_error,
)
class FakeLifecycle:
async def reset_session(self, session_id: str) -> None:
pass
async def close_session(self, session_id: str) -> None:
pass
def make_tick() -> ARDiffusionTickRequest:
return ARDiffusionTickRequest(
session_id="world-1",
request_id="world-1-chunk-3",
chunk_index=3,
applied_event_ids=(7, 8),
prompt="turn left",
controls=(
ARDiffusionControlInput(
track="camera",
schema="example.camera.v1",
data={"samples": [{"x": 1.0}]},
),
),
)
def test_consumer_rejects_replicated_ar_diffusion_stage() -> None:
client = FakeGenerateClient(num_replicas=2)
with pytest.raises(
ValueError,
match=r"require exactly one replica.*num_replicas=2",
):
ARDiffusionOmniTickConsumer(
client,
prompt_provider=lambda tick: tick.prompt or "",
sampling_params_list=[OmniDiffusionSamplingParams()],
diffusion_stage_id=0,
)
assert client.calls == []
@pytest.mark.asyncio
async def test_consumer_submits_typed_tick_and_returns_standard_output() -> None:
client = FakeGenerateClient()
template = OmniDiffusionSamplingParams(
output_type="latent",
extra_args={"deployment_option": "kept"},
)
consumer = ARDiffusionOmniTickConsumer(
client,
prompt_provider=lambda tick: {
"prompt": tick.prompt,
"multi_modal_data": {"image": "initial-image"},
},
sampling_params_list=[template],
diffusion_stage_id=0,
output_modalities=["video"],
)
tick = make_tick()
output = await consumer.execute_tick(tick)
assert output.images == ["chunk"]
assert consumer.chunk_metadata(output) == ARDiffusionChunkMetadata.from_tick(tick)
assert len(client.calls) == 1
call = client.calls[0]
assert call["request_id"] == tick.request_id
assert call["output_modalities"] == ["video"]
assert call["prompt"] == {
"prompt": "turn left",
"multi_modal_data": {"image": "initial-image"},
}
submitted_params = call["sampling_params_list"][0]
assert submitted_params.extra_args["deployment_option"] == "kept"
assert ARDiffusionTickRequest.from_extra_args(submitted_params.extra_args) == tick
assert "ar_diffusion_tick" not in template.extra_args
@pytest.mark.asyncio
async def test_consumer_rejects_missing_chunk_metadata() -> None:
consumer = ARDiffusionOmniTickConsumer(
FakeGenerateClient(include_metadata=False),
prompt_provider=lambda tick: tick.prompt or "",
sampling_params_list=[OmniDiffusionSamplingParams()],
diffusion_stage_id=0,
)
with pytest.raises(ValueError, match="multimodal_output.metadata"):
await consumer.execute_tick(make_tick())
@pytest.mark.asyncio
async def test_consumer_rejects_non_monotonic_applied_event_metadata() -> None:
consumer = ARDiffusionOmniTickConsumer(
FakeGenerateClient(applied_event_ids=[8, 7]),
prompt_provider=lambda tick: tick.prompt or "",
sampling_params_list=[OmniDiffusionSamplingParams()],
diffusion_stage_id=0,
)
with pytest.raises(ValueError, match="unique and strictly increasing"):
await consumer.execute_tick(make_tick())
@pytest.mark.asyncio
async def test_consumer_preserves_engine_failure_before_metadata_validation() -> None:
consumer = ARDiffusionOmniTickConsumer(
FakeGenerateClient(include_metadata=False, output_error="model failed"),
prompt_provider=lambda tick: tick.prompt or "",
sampling_params_list=[OmniDiffusionSamplingParams()],
diffusion_stage_id=0,
)
with pytest.raises(RuntimeError, match="model failed"):
await consumer.execute_tick(make_tick())
@pytest.mark.asyncio
async def test_session_commits_only_after_standard_engine_output_metadata() -> None:
client = FakeGenerateClient()
consumer = ARDiffusionOmniTickConsumer(
client,
prompt_provider=lambda tick: tick.prompt or "",
sampling_params_list=[OmniDiffusionSamplingParams()],
diffusion_stage_id=0,
)
session = ARDiffusionSession(
"world-1",
tick_consumer=consumer,
lifecycle=FakeLifecycle(),
max_pending_events=4,
request_id_factory=lambda session_id, chunk_index: f"{session_id}-chunk-{chunk_index}",
)
await session.accept_event(ARDiffusionSessionEvent(event_id=7, prompt="turn left"))
output = await session.next_chunk()
assert output.request_id == "world-1-chunk-0"
assert session.chunk_index == 1
assert session.pending_event_count == 0
assert client.calls[0]["request_id"] == "world-1-chunk-0"
+153 -6
View File
@@ -30,6 +30,8 @@ pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
BLOCK = 16
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def make_spec(*, chunk_size=BLOCK, window_chunks=2, sink_chunks=0, reset_at_boundary=False):
return ChunkWindowSpec(
@@ -95,7 +97,6 @@ def test_sliding_replace_snaps_to_chunk_boundary():
def test_sink_chunks_protected():
# sink = 1 chunk (16 tokens) is never skipped.
skip = chunk_window_skipped_tokens(80, chunk_size=16, sliding_window=32, sink_chunks=1, reset_at_boundary=False)
# raw overflow = 80 - 32 - 16 = 32 -> snapped 32
assert skip == 32
@@ -201,12 +202,149 @@ def test_cross_attn_pool_deducted_from_self_attn_budget():
kv_branches=(ARDiffusionKVBranchSpec("positive", 0), ARDiffusionKVBranchSpec("negative", 0)),
session_capacity=1,
cross_attention_lengths={"text": L},
device=torch.device("cpu"),
)
cross_bytes = 2 * 2 * L * 4 * 64 * torch.float16.itemsize * 2 # K+V, pos+neg, layers
expected = compute_num_blocks(avail - cross_bytes, 0.5, kv.spec.page_size_bytes * 2)
page_bytes = kv.spec.page_size_bytes * 2
scratch_bytes = kv.scratch_num_blocks * page_bytes
expected = (int(avail * 0.5) - cross_bytes - scratch_bytes) // page_bytes
assert kv.num_blocks == expected
assert expected > 1 * (2 + 1) + 2 # above the local-slot floor, so the cross deduction is what's tested
assert kv.cross_attention_reserved_bytes == cross_bytes
assert (kv.num_blocks_total * page_bytes) + cross_bytes <= int(avail * 0.5)
assert expected > 1 * (2 + 1) + 2 # above the local-slot minimum, so the cross deduction is what's tested
def _make_tiny_capacity_kv(
*,
requested_capacity: int,
available_bytes: int,
gpu_memory_fraction: float = 1.0,
model_owned_state_bytes_per_session: int = 0,
) -> ARDiffusionKVCache:
return ARDiffusionKVCache(
ARDiffusionKVConfig(
enable=True,
chunk_size=1,
window_chunks=3,
sink_chunks=3,
gpu_memory_fraction=gpu_memory_fraction,
),
num_layers=1,
num_kv_heads=1,
head_size=1,
dtype=torch.float32,
block_size=1,
max_model_len=128,
available_bytes=available_bytes,
kv_branches=(ARDiffusionKVBranchSpec("main", 0),),
session_capacity=requested_capacity,
cross_attention_lengths={"text": 2},
frames_per_block=3,
model_owned_state_bytes_per_session=model_owned_state_bytes_per_session,
device=torch.device("cpu"),
)
def test_capacity_two_retains_both_windows_and_allocates_next_block():
# LingBot-like single-branch geometry: page=8 bytes; capacity=2 requires
# 17 managed + 3 scratch blocks, plus two 16-byte cross-attention
# reservations: 192 bytes exactly.
kv = _make_tiny_capacity_kv(requested_capacity=2, available_bytes=192)
assert kv.requested_session_capacity == 2
assert kv.session_capacity == 2
assert kv.managed_num_blocks == 17
adapters = [kv.begin_request(session_id) for session_id in ("first", "second")]
for adapter in adapters:
for _ in range(6): # three sink + three window blocks
kv.allocate_token_slots(adapter, 1)
kv.commit_chunk(adapter)
assert len(kv.window_block_ids(adapter)) == 6
# Only one request is in flight, so its three-frame block is counted once.
kv.allocate_token_slots(adapters[0], 3)
def test_requested_capacity_is_capped_and_cross_reservation_uses_effective_capacity():
kv = _make_tiny_capacity_kv(requested_capacity=64, available_bytes=192)
assert kv.requested_session_capacity == 64
assert kv.session_capacity == 2
assert kv.cross_attention_bytes_per_session == 16
assert kv.cross_attention_reserved_bytes == 32
assert kv.num_blocks_total * 8 + kv.cross_attention_reserved_bytes == kv.memory_budget_bytes
def test_capacity_rejects_budget_that_cannot_fit_one_session():
# One session needs 11 managed + 3 scratch blocks and 16 cross bytes:
# 14 * 8 + 16 = 128 bytes.
with pytest.raises(ValueError, match="cannot fit one session.*available=127 bytes.*required=128 bytes"):
_make_tiny_capacity_kv(requested_capacity=1, available_bytes=127)
def test_fraction_is_soft_floor_when_one_session_fits_actual_memory():
kv = _make_tiny_capacity_kv(
requested_capacity=2,
available_bytes=1280,
gpu_memory_fraction=0.05,
)
assert kv.configured_memory_budget_bytes == 64
assert kv.memory_budget_bytes == 128
assert kv.session_capacity == 1
def test_model_owned_state_reduces_effective_session_capacity():
kv = _make_tiny_capacity_kv(
requested_capacity=2,
available_bytes=200,
model_owned_state_bytes_per_session=16,
)
assert kv.session_capacity == 1
assert kv.model_owned_state_reserved_bytes == 16
assert (
kv.num_blocks_total * 8 + kv.cross_attention_reserved_bytes + kv.model_owned_state_reserved_bytes
<= kv.memory_budget_bytes
)
def _make_shipped_lingbot_geometry(*, gpu_memory_fraction: float) -> ARDiffusionKVCache:
return ARDiffusionKVCache(
ARDiffusionKVConfig(
enable=True,
chunk_size=1560,
window_chunks=9,
sink_chunks=9,
gpu_memory_fraction=gpu_memory_fraction,
),
num_layers=40,
num_kv_heads=40,
head_size=128,
dtype=torch.bfloat16,
block_size=1560,
max_model_len=1 << 20,
available_bytes=100 * (1 << 30),
kv_branches=(ARDiffusionKVBranchSpec("main", 0),),
session_capacity=2,
cross_attention_lengths={"text": 512},
frames_per_block=3,
model_owned_state_bytes_per_session=7_488_000,
)
def test_shipped_lingbot_geometry_default_fraction_admits_one_session():
kv = _make_shipped_lingbot_geometry(gpu_memory_fraction=0.1)
assert kv.configured_memory_budget_bytes == 10 * (1 << 30)
assert kv.memory_budget_bytes == 33_653_670_400
assert kv.session_capacity == 1
def test_shipped_lingbot_geometry_tuned_fraction_admits_two_sessions():
kv = _make_shipped_lingbot_geometry(gpu_memory_fraction=0.6)
assert kv.memory_budget_bytes == 60 * (1 << 30)
assert kv.session_capacity == 2
def _make_kv(
@@ -221,15 +359,24 @@ def _make_kv(
if local_branches == 1
else (ARDiffusionKVBranchSpec("positive", 0), ARDiffusionKVBranchSpec("negative", 1))
)
page_bytes = 2 * BLOCK * 4 * 64 * torch.float32.itemsize
declared_scratch_blocks = (max_scratch_tokens_per_branch + BLOCK - 1) // BLOCK
scratch_blocks = local_branches * (num_frame_per_block + declared_scratch_blocks)
managed_blocks = local_branches * (window_chunks + num_frame_per_block) + 2
return ARDiffusionKVCache(
ARDiffusionKVConfig(enable=True, chunk_size=BLOCK, window_chunks=window_chunks),
ARDiffusionKVConfig(
enable=True,
chunk_size=BLOCK,
window_chunks=window_chunks,
gpu_memory_fraction=1.0,
),
num_layers=1,
num_kv_heads=4,
head_size=64,
dtype=torch.float32,
block_size=BLOCK,
max_model_len=4096,
available_bytes=1 << 16, # tiny -> the floor binds
available_bytes=(managed_blocks + scratch_blocks) * page_bytes,
device=torch.device("cpu"),
kv_branches=kv_branches,
session_capacity=1,
@@ -54,13 +54,15 @@ def test_full_request_lifecycle_and_eviction():
used = {int(s) // kv.block_size for s in slots}
assert kv.null_block_id not in used
assert used <= set(block_table)
# Resident window stays bounded by window + the in-flight chunk.
# Allocation may transiently add one in-flight chunk.
assert len(kv.window_block_ids(adapter)) <= kv.spec.window_chunks + 1
free_after.append(kv.manager.block_pool.get_num_free_blocks())
kv.commit_chunk(adapter)
# A successful commit immediately prunes physical ownership.
assert len(kv.window_block_ids(adapter)) <= kv.spec.window_chunks
free_after.append(kv.manager.block_pool.get_num_free_blocks())
# Pool memory plateaus once the window is full (eviction recycles blocks).
assert free_after[-1] == free_after[kv.spec.window_chunks]
assert free_after[-1] == free_after[kv.spec.window_chunks - 1]
kv.end_request(adapter)
assert kv.manager.block_pool.get_num_free_blocks() == free_total
@@ -332,7 +332,7 @@ def test_block_table_padded_to_fixed_width():
def test_prepare_is_idempotent_and_layers_share_metadata():
device = torch.device("cpu")
_, st = make_state(num_layers=2)
kv, st = make_state(num_layers=2)
contexts = st.get_kv_caches(POS, seq_len=BLOCK, commit_current=False)
fctx = contexts[0].forward_ctx
fctx.prepare(device=device, action_len=0, query_len=BLOCK)
@@ -349,6 +349,10 @@ def test_prepare_is_idempotent_and_layers_share_metadata():
assert i0.block_table is i1.block_table
assert i0.seq_lens is i1.seq_lens
assert i0.video_slots is i1.video_slots
assert i0.key_pool is kv._k_pools[0]
assert i0.value_pool is kv._v_pools[0]
assert i1.key_pool is kv._k_pools[1]
assert i1.value_pool is kv._v_pools[1]
def test_layer_inputs_before_prepare_raises():
@@ -174,6 +174,19 @@ def test_window_reset_can_keep_named_cross_attention_cache():
assert not st.is_cross_attention_populated(POS, "text")
def test_clear_cross_attention_preserves_self_attention_history():
kv, st = make_state(num_layers=1, cross_attn_length=8)
_prepare_and_commit(st, POS, 2)
k = torch.randn(1, 8, N_HEADS, HEAD_DIM)
st.populate_cross_attention(POS, "text", [(k, k)])
resident_before = kv.window_block_ids(st.adapter(POS))
st.clear_cross_attention()
assert kv.window_block_ids(st.adapter(POS)) == resident_before
assert not st.is_cross_attention_populated(POS, "text")
def test_cross_attention_uses_logical_branch_when_local_slot_is_shared():
_, st = make_state(num_layers=2, cross_attn_length=8, shared_local_index=True)
assert st.kv_cache.num_local_kv_branches == 1
@@ -0,0 +1,505 @@
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import asyncio
from typing import Any
import pytest
from vllm_omni.experimental.ar_diffusion.session import (
ARDiffusionChunkMetadataError,
ARDiffusionEventAcceptanceStatus,
ARDiffusionEventQueueFullError,
ARDiffusionPreparedControls,
ARDiffusionSession,
ARDiffusionSessionError,
ARDiffusionSessionEvent,
ARDiffusionSessionManager,
ARDiffusionSessionStateError,
ARDiffusionSessionStatus,
ARDiffusionStaleEventError,
ARDiffusionWorkerLifecycle,
)
from vllm_omni.experimental.ar_diffusion.tick_protocol import (
ARDiffusionChunkMetadata,
ARDiffusionControlInput,
ARDiffusionTickRequest,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class FakeTickConsumer:
def __init__(self) -> None:
self.ticks: list[ARDiffusionTickRequest] = []
self.failures_remaining = 0
self.started: asyncio.Event | None = None
self.release: asyncio.Event | None = None
self.metadata_override: ARDiffusionChunkMetadata | None = None
async def execute_tick(self, tick: ARDiffusionTickRequest) -> ARDiffusionChunkMetadata:
self.ticks.append(tick)
if self.started is not None:
self.started.set()
if self.release is not None:
await self.release.wait()
if self.failures_remaining:
self.failures_remaining -= 1
raise RuntimeError("fake tick failed")
return self.metadata_override or ARDiffusionChunkMetadata.from_tick(tick)
def chunk_metadata(self, output: ARDiffusionChunkMetadata) -> ARDiffusionChunkMetadata:
return output
class FakeLifecycle:
def __init__(self) -> None:
self.resets: list[str] = []
self.closes: list[str] = []
self.close_failures_remaining = 0
async def reset_session(self, session_id: str) -> None:
self.resets.append(session_id)
async def close_session(self, session_id: str) -> None:
self.closes.append(session_id)
if self.close_failures_remaining:
self.close_failures_remaining -= 1
raise RuntimeError("fake close failed")
class FailingCommitReducer:
def __init__(self) -> None:
self.commit_calls = 0
self.reset_calls = 0
def prepare(
self,
*,
current_controls: dict[str, ARDiffusionControlInput],
events: tuple[ARDiffusionSessionEvent, ...],
chunk_index: int,
) -> ARDiffusionPreparedControls:
del current_controls, events, chunk_index
return ARDiffusionPreparedControls(controls=())
def commit(self, prepared: ARDiffusionPreparedControls) -> None:
del prepared
self.commit_calls += 1
raise RuntimeError("fake reducer commit failed")
def reset(self) -> None:
self.reset_calls += 1
def make_session(
*,
max_pending_events: int = 8,
consumer: FakeTickConsumer | None = None,
lifecycle: FakeLifecycle | None = None,
) -> tuple[ARDiffusionSession, FakeTickConsumer, FakeLifecycle]:
resolved_consumer = consumer or FakeTickConsumer()
resolved_lifecycle = lifecycle or FakeLifecycle()
session = ARDiffusionSession(
"world-1",
tick_consumer=resolved_consumer,
lifecycle=resolved_lifecycle,
max_pending_events=max_pending_events,
request_id_factory=lambda session_id, chunk_index: f"{session_id}-request-{chunk_index}",
)
return session, resolved_consumer, resolved_lifecycle
@pytest.mark.asyncio
async def test_events_are_accepted_then_sorted_and_applied_at_chunk_boundary() -> None:
session, consumer, _ = make_session()
event_two = ARDiffusionSessionEvent(event_id=2, prompt="turn")
event_one = ARDiffusionSessionEvent(
event_id=1,
controls=(
ARDiffusionControlInput(
track="camera",
schema="example.camera.v1",
data={"samples": [{"x": 1.0}]},
),
),
)
accepted_two = await session.accept_event(event_two)
accepted_one = await session.accept_event(event_one)
duplicate = await session.accept_event(event_two)
assert accepted_two.status is ARDiffusionEventAcceptanceStatus.ACCEPTED
assert accepted_one.status is ARDiffusionEventAcceptanceStatus.ACCEPTED
assert duplicate.status is ARDiffusionEventAcceptanceStatus.DUPLICATE
assert consumer.ticks == []
metadata = await session.next_chunk()
assert metadata.applied_event_ids == (1, 2)
assert consumer.ticks[0] == ARDiffusionTickRequest(
session_id="world-1",
request_id="world-1-request-0",
chunk_index=0,
applied_event_ids=(1, 2),
prompt="turn",
controls=(
ARDiffusionControlInput(
track="camera",
schema="example.camera.v1",
data={"samples": [{"x": 1.0}]},
),
),
)
assert session.pending_event_count == 0
applied_duplicate = await session.accept_event(event_two)
assert applied_duplicate.status is ARDiffusionEventAcceptanceStatus.DUPLICATE
next_metadata = await session.next_chunk()
assert next_metadata.chunk_index == 1
assert next_metadata.applied_event_ids == ()
assert consumer.ticks[1].prompt == "turn"
assert consumer.ticks[1].controls == consumer.ticks[0].controls
@pytest.mark.asyncio
async def test_composite_event_is_isolated_and_queue_admission_is_atomic() -> None:
session, consumer, _ = make_session(max_pending_events=1)
payload = {"samples": [{"x": 1.0}]}
composite = ARDiffusionSessionEvent(
event_id=5,
prompt="new prompt",
controls=(
ARDiffusionControlInput(
track="camera",
schema="example.camera.v1",
data=payload,
),
ARDiffusionControlInput(
track="action",
schema="example.action.v1",
data={"keys": ["left"]},
),
),
)
await session.accept_event(composite)
payload["samples"][0]["x"] = 99.0
duplicate = await session.accept_event(composite)
assert duplicate.status is ARDiffusionEventAcceptanceStatus.DUPLICATE
with pytest.raises(ARDiffusionEventQueueFullError, match="queue is full"):
await session.accept_event(ARDiffusionSessionEvent(event_id=6, prompt="rejected"))
await session.next_chunk()
assert consumer.ticks[0].applied_event_ids == (5,)
assert consumer.ticks[0].prompt == "new prompt"
assert consumer.ticks[0].controls[0].data["keys"] == ("left",)
assert consumer.ticks[0].controls[1].data["samples"] == ({"x": 1.0},)
assert consumer.ticks[0].controls[0].to_dict()["data"] == {"keys": ["left"]}
assert consumer.ticks[0].controls[1].to_dict()["data"] == {"samples": [{"x": 1.0}]}
def test_event_rejects_duplicate_tracks_and_non_transport_payloads() -> None:
with pytest.raises(ValueError, match="at most once"):
ARDiffusionSessionEvent(
event_id=1,
controls=(
ARDiffusionControlInput(track="camera", schema="one", data={}),
ARDiffusionControlInput(track="camera", schema="two", data={}),
),
)
with pytest.raises(ValueError, match="transport-safe"):
ARDiffusionControlInput(
track="camera",
schema="example.camera.v1",
data={"object": object()},
)
@pytest.mark.asyncio
async def test_failed_tick_closes_worker_and_requires_reset() -> None:
consumer = FakeTickConsumer()
consumer.failures_remaining = 1
lifecycle = FakeLifecycle()
session, _, _ = make_session(consumer=consumer, lifecycle=lifecycle)
await session.accept_event(ARDiffusionSessionEvent(event_id=3, prompt="retry me"))
with pytest.raises(RuntimeError, match="fake tick failed"):
await session.next_chunk()
assert session.status is ARDiffusionSessionStatus.FAILED
assert session.chunk_index == 0
assert session.pending_event_count == 1
assert lifecycle.closes == ["world-1"]
with pytest.raises(ARDiffusionSessionStateError, match="failed"):
await session.next_chunk()
await session.reset()
assert session.status is ARDiffusionSessionStatus.ACTIVE
assert session.pending_event_count == 0
assert lifecycle.resets == ["world-1"]
await session.accept_event(ARDiffusionSessionEvent(event_id=4, prompt="after reset"))
metadata = await session.next_chunk()
assert metadata.chunk_index == 0
assert metadata.applied_event_ids == (4,)
assert [tick.applied_event_ids for tick in consumer.ticks] == [(3,), (4,)]
@pytest.mark.asyncio
async def test_inflight_snapshot_is_immutable_and_new_event_waits_for_next_chunk() -> None:
consumer = FakeTickConsumer()
consumer.started = asyncio.Event()
consumer.release = asyncio.Event()
session, _, _ = make_session(consumer=consumer)
await session.accept_event(ARDiffusionSessionEvent(event_id=10, prompt="first"))
first_chunk = asyncio.create_task(session.next_chunk())
await consumer.started.wait()
accepted = await session.accept_event(ARDiffusionSessionEvent(event_id=11, prompt="second"))
assert accepted.status is ARDiffusionEventAcceptanceStatus.ACCEPTED
assert consumer.ticks[0].prompt == "first"
assert consumer.ticks[0].applied_event_ids == (10,)
with pytest.raises(ARDiffusionStaleEventError, match="event floor is 10"):
await session.accept_event(ARDiffusionSessionEvent(event_id=9, prompt="stale"))
consumer.release.set()
first_metadata = await first_chunk
second_metadata = await session.next_chunk()
assert first_metadata.applied_event_ids == (10,)
assert second_metadata.applied_event_ids == (11,)
assert consumer.ticks[1].prompt == "second"
@pytest.mark.asyncio
async def test_metadata_mismatch_does_not_commit_snapshot() -> None:
consumer = FakeTickConsumer()
consumer.metadata_override = ARDiffusionChunkMetadata(
session_id="another-world",
request_id="wrong",
chunk_index=0,
)
lifecycle = FakeLifecycle()
session, _, _ = make_session(consumer=consumer, lifecycle=lifecycle)
await session.accept_event(ARDiffusionSessionEvent(event_id=1, prompt="pending"))
with pytest.raises(ARDiffusionChunkMetadataError, match="does not match"):
await session.next_chunk()
assert session.chunk_index == 0
assert session.pending_event_count == 1
assert session.status is ARDiffusionSessionStatus.FAILED
assert lifecycle.closes == ["world-1"]
@pytest.mark.asyncio
async def test_reducer_commit_failure_is_fail_closed_without_partial_session_commit() -> None:
consumer = FakeTickConsumer()
lifecycle = FakeLifecycle()
reducer = FailingCommitReducer()
session = ARDiffusionSession(
"world-1",
tick_consumer=consumer,
lifecycle=lifecycle,
max_pending_events=4,
request_id_factory=lambda session_id, chunk_index: f"{session_id}-request-{chunk_index}",
control_reducer=reducer,
)
await session.accept_event(ARDiffusionSessionEvent(event_id=1, prompt="pending"))
with pytest.raises(RuntimeError, match="fake reducer commit failed"):
await session.next_chunk()
assert reducer.commit_calls == 1
assert session.status is ARDiffusionSessionStatus.FAILED
assert session.chunk_index == 0
assert session.pending_event_count == 1
assert lifecycle.closes == ["world-1"]
with pytest.raises(ARDiffusionSessionStateError, match="failed"):
await session.next_chunk()
await session.reset()
assert reducer.reset_calls == 1
assert session.status is ARDiffusionSessionStatus.ACTIVE
assert session.chunk_index == 0
assert session.pending_event_count == 0
@pytest.mark.asyncio
async def test_reset_close_and_disconnect_reach_lifecycle_boundary() -> None:
consumer = FakeTickConsumer()
lifecycle = FakeLifecycle()
manager = ARDiffusionSessionManager(
tick_consumer=consumer,
lifecycle=lifecycle,
max_pending_events=4,
request_id_factory=lambda session_id, chunk_index: f"{session_id}-{chunk_index}",
)
session = await manager.create_session("world-reset")
await session.accept_event(ARDiffusionSessionEvent(event_id=4, prompt="before reset"))
await session.next_chunk()
await manager.reset_session("world-reset")
assert lifecycle.resets == ["world-reset"]
assert session.status is ARDiffusionSessionStatus.ACTIVE
assert session.chunk_index == 0
duplicate = await session.accept_event(ARDiffusionSessionEvent(event_id=4, prompt="duplicate"))
assert duplicate.status is ARDiffusionEventAcceptanceStatus.DUPLICATE
reset_tick = await session.next_chunk()
assert reset_tick.chunk_index == 0
assert consumer.ticks[-1].prompt is None
assert consumer.ticks[-1].controls == ()
await manager.close_session("world-reset")
assert lifecycle.closes == ["world-reset"]
assert session.status is ARDiffusionSessionStatus.CLOSED
with pytest.raises(ARDiffusionSessionStateError, match="does not exist"):
await manager.get_session("world-reset")
disconnected = await manager.create_session("world-disconnect")
await manager.disconnect("world-disconnect")
assert disconnected.status is ARDiffusionSessionStatus.CLOSED
assert lifecycle.closes[-1] == "world-disconnect"
@pytest.mark.asyncio
async def test_manager_keeps_interleaved_sessions_independent() -> None:
consumer = FakeTickConsumer()
manager = ARDiffusionSessionManager(
tick_consumer=consumer,
lifecycle=FakeLifecycle(),
max_pending_events=4,
request_id_factory=lambda session_id, chunk_index: f"{session_id}-request-{chunk_index}",
)
session_a = await manager.create_session("world-a")
session_b = await manager.create_session("world-b")
await session_a.accept_event(ARDiffusionSessionEvent(event_id=10, prompt="a0"))
await session_a.next_chunk()
await session_b.accept_event(ARDiffusionSessionEvent(event_id=1, prompt="b0"))
await session_b.next_chunk()
await session_a.accept_event(ARDiffusionSessionEvent(event_id=11, prompt="a1"))
await session_a.next_chunk()
assert [
(
tick.session_id,
tick.request_id,
tick.chunk_index,
tick.applied_event_ids,
)
for tick in consumer.ticks
] == [
("world-a", "world-a-request-0", 0, (10,)),
("world-b", "world-b-request-0", 0, (1,)),
("world-a", "world-a-request-1", 1, (11,)),
]
assert session_a.chunk_index == 2
assert session_b.chunk_index == 1
with pytest.raises(ARDiffusionStaleEventError, match="event floor is 11"):
await session_a.accept_event(ARDiffusionSessionEvent(event_id=2, prompt="stale only for a"))
assert (
await session_b.accept_event(ARDiffusionSessionEvent(event_id=2, prompt="b1"))
).status is ARDiffusionEventAcceptanceStatus.ACCEPTED
@pytest.mark.asyncio
async def test_failed_close_retains_tombstone_until_cleanup_retry_succeeds() -> None:
consumer = FakeTickConsumer()
lifecycle = FakeLifecycle()
lifecycle.close_failures_remaining = 1
manager = ARDiffusionSessionManager(
tick_consumer=consumer,
lifecycle=lifecycle,
max_pending_events=4,
)
session = await manager.create_session("world-close-retry")
with pytest.raises(RuntimeError, match="fake close failed"):
await manager.close_session("world-close-retry")
assert session.status is ARDiffusionSessionStatus.CLEANUP_FAILED
assert await manager.get_session("world-close-retry") is session
with pytest.raises(ARDiffusionSessionStateError, match="already exists"):
await manager.create_session("world-close-retry")
with pytest.raises(ARDiffusionSessionStateError, match="cleanup_failed"):
await manager.reset_session("world-close-retry")
await manager.close_session("world-close-retry")
assert lifecycle.closes == ["world-close-retry", "world-close-retry"]
assert session.status is ARDiffusionSessionStatus.CLOSED
with pytest.raises(ARDiffusionSessionStateError, match="does not exist"):
await manager.get_session("world-close-retry")
recreated = await manager.create_session("world-close-retry")
assert recreated is not session
@pytest.mark.asyncio
async def test_failed_disconnect_retains_session_for_explicit_cleanup_retry() -> None:
lifecycle = FakeLifecycle()
lifecycle.close_failures_remaining = 1
manager = ARDiffusionSessionManager(
tick_consumer=FakeTickConsumer(),
lifecycle=lifecycle,
max_pending_events=4,
)
session = await manager.create_session("world-disconnect-retry")
with pytest.raises(RuntimeError, match="fake close failed"):
await manager.disconnect("world-disconnect-retry")
assert session.status is ARDiffusionSessionStatus.CLEANUP_FAILED
assert await manager.get_session("world-disconnect-retry") is session
await manager.close_session("world-disconnect-retry")
assert session.status is ARDiffusionSessionStatus.CLOSED
class FakeRPCClient:
def __init__(self, result: list[Any]) -> None:
self.result = result
self.calls: list[dict[str, Any]] = []
async def collective_rpc(self, **kwargs: Any) -> list[Any]:
self.calls.append(kwargs)
return self.result
@pytest.mark.asyncio
async def test_worker_lifecycle_routes_to_selected_stage_rpc() -> None:
client = FakeRPCClient([[True]])
lifecycle = ARDiffusionWorkerLifecycle(client, stage_ids=[2], timeout=3.0)
await lifecycle.reset_session("world-1")
await lifecycle.close_session("world-1")
assert client.calls == [
{
"method": "reset_ar_diffusion_session",
"timeout": 3.0,
"args": ("world-1",),
"stage_ids": [2],
},
{
"method": "close_ar_diffusion_session",
"timeout": 3.0,
"args": ("world-1",),
"stage_ids": [2],
},
]
@pytest.mark.asyncio
async def test_worker_lifecycle_rejects_unsupported_stage_result() -> None:
lifecycle = ARDiffusionWorkerLifecycle(
FakeRPCClient([{"supported": False, "error": "not an AR stage"}]),
stage_ids=[1],
)
with pytest.raises(ARDiffusionSessionError, match="not an AR stage"):
await lifecycle.close_session("world-1")
@@ -0,0 +1,108 @@
# SPDX-License-Identifier: Apache-2.0
import pytest
from vllm_omni.experimental.ar_diffusion.tick_protocol import (
ARDiffusionChunkMetadata,
ARDiffusionControlInput,
ARDiffusionTickRequest,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_tick_round_trip_preserves_identity_and_control_snapshot() -> None:
tick = ARDiffusionTickRequest(
session_id="world-1",
request_id="chunk-request-7",
chunk_index=7,
applied_event_ids=(10, 11),
prompt="turn left",
controls=(
ARDiffusionControlInput(
track="camera",
schema="lingbot.camera_trajectory.v1",
data={"poses": [[1.0, 0.0]], "intrinsics": [[2.0]]},
),
),
)
restored = ARDiffusionTickRequest.from_extra_args(tick.to_extra_args())
assert restored == tick
assert ARDiffusionChunkMetadata.from_tick(tick).to_dict() == {
"session_id": "world-1",
"request_id": "chunk-request-7",
"chunk_index": 7,
"applied_event_ids": [10, 11],
}
@pytest.mark.parametrize(
("field", "value", "message"),
[
("session_id", "", "session_id"),
("request_id", "", "request_id"),
("chunk_index", -1, "chunk_index"),
("applied_event_ids", (2, 1), "strictly increasing"),
("applied_event_ids", (1, 1), "strictly increasing"),
],
)
def test_tick_rejects_invalid_identity(
field: str,
value: object,
message: str,
) -> None:
values = {
"session_id": "world-1",
"request_id": "request-1",
"chunk_index": 0,
"applied_event_ids": (),
}
values[field] = value
with pytest.raises(ValueError, match=message):
ARDiffusionTickRequest(**values)
def test_tick_snapshot_is_detached_and_deeply_immutable() -> None:
payload = {"samples": [{"keys": ["w"]}]}
event_ids = [1, 2]
controls = [
ARDiffusionControlInput(
track="camera",
schema="example.camera.v1",
data=payload,
)
]
tick = ARDiffusionTickRequest(
session_id="world-1",
request_id="request-a",
chunk_index=0,
applied_event_ids=event_ids, # type: ignore[arg-type]
controls=controls, # type: ignore[arg-type]
)
serialized = tick.to_extra_args()
payload["samples"][0]["keys"].append("j")
event_ids.append(3)
controls.clear()
serialized["ar_diffusion_tick"]["controls"][0]["data"]["samples"][0]["keys"].append("k")
assert tick.applied_event_ids == (1, 2)
assert isinstance(tick.controls, tuple)
assert tick.to_extra_args()["ar_diffusion_tick"]["controls"] == [
{
"track": "camera",
"schema": "example.camera.v1",
"data": {"samples": [{"keys": ["w"]}]},
}
]
with pytest.raises(TypeError):
tick.controls[0].data["samples"] = () # type: ignore[index]
with pytest.raises(AttributeError):
tick.controls[0].data["samples"][0]["keys"].append("j")
def test_legacy_request_has_no_typed_tick() -> None:
assert ARDiffusionTickRequest.from_extra_args({"session_id": "legacy"}) is None
@@ -0,0 +1,53 @@
# SPDX-License-Identifier: Apache-2.0
import pytest
from vllm_omni.diffusion.worker.diffusion_worker import DiffusionWorker
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class FakeARDiffusionRunner:
def __init__(self) -> None:
self.resets: list[str] = []
self.closes: list[str] = []
def reset_session(self, session_id: str) -> None:
self.resets.append(session_id)
def close_session(self, session_id: str) -> None:
self.closes.append(session_id)
class FakeRegularRunner:
pass
def make_worker(runner: object) -> DiffusionWorker:
worker = object.__new__(DiffusionWorker)
worker.model_runner = runner
return worker
def test_worker_delegates_ar_session_lifecycle_to_runner() -> None:
runner = FakeARDiffusionRunner()
worker = make_worker(runner)
assert worker.reset_ar_diffusion_session("world-1") is True
assert worker.close_ar_diffusion_session("world-1") is True
assert runner.resets == ["world-1"]
assert runner.closes == ["world-1"]
def test_worker_reports_unsupported_runner_without_model_assumptions() -> None:
worker = make_worker(FakeRegularRunner())
assert worker.reset_ar_diffusion_session("world-1") is False
assert worker.close_ar_diffusion_session("world-1") is False
def test_worker_rejects_empty_session_id() -> None:
worker = make_worker(FakeARDiffusionRunner())
with pytest.raises(ValueError, match="session_id"):
worker.close_ar_diffusion_session("")
@@ -6,7 +6,10 @@ from types import SimpleNamespace
import pytest
from vllm_omni.diffusion.models.dreamzero.pipeline_dreamzero import DreamZeroPipeline
from vllm_omni.diffusion.models.dreamzero.pipeline_dreamzero import (
DREAMZERO_MODEL_OWNED_STATE_BYTES_PER_SESSION,
DreamZeroPipeline,
)
from vllm_omni.diffusion.models.dreamzero.state_dreamzero import DreamZeroState
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -32,6 +35,37 @@ def test_dreamzero_pipeline_state_is_session_keyed() -> None:
assert session_b.call_count == 3
def test_dreamzero_kv_spec_accounts_for_model_owned_cuda_state(monkeypatch) -> None:
pipeline = DreamZeroPipeline.__new__(DreamZeroPipeline)
pipeline.transformer = SimpleNamespace(
frame_seqlen=16,
blocks=[
SimpleNamespace(
self_attn=SimpleNamespace(
max_attention_size=96,
tp_num_heads=4,
)
)
],
text_len=8,
model_type="t2v",
num_layers=2,
num_heads=4,
dim=256,
num_frame_per_block=4,
num_action_per_block=2,
num_state_per_block=1,
)
monkeypatch.setattr(
"vllm_omni.diffusion.models.dreamzero.pipeline_dreamzero.get_classifier_free_guidance_world_size",
lambda: 1,
)
spec = pipeline.ar_diffusion_kv_cache_spec()
assert spec.model_owned_state_bytes_per_session == DREAMZERO_MODEL_OWNED_STATE_BYTES_PER_SESSION
def test_dreamzero_pipeline_state_follows_runner_lifecycle_notifications() -> None:
pipeline = _empty_pipeline()
@@ -0,0 +1 @@
# SPDX-License-Identifier: Apache-2.0
@@ -0,0 +1,13 @@
{
"_class_name": "LingBotWorldCausalDMDPipeline",
"_diffusers_version": "0.35.0.dev0",
"boundary_ratio": 0.9,
"image_encoder": [null, null],
"image_processor": [null, null],
"scheduler": ["diffusers", "UniPCMultistepScheduler"],
"text_encoder": ["transformers", "UMT5EncoderModel"],
"tokenizer": ["transformers", "T5TokenizerFast"],
"transformer": ["diffusers", "CausalLingBotWorldTransformer3DModel"],
"transformer_2": [null, null],
"vae": ["diffusers", "AutoencoderKLWan"]
}
@@ -0,0 +1,42 @@
{
"sources": {
"config": "https://huggingface.co/robbyant/lingbot-world-v2-14b-causal-fast-diffusers/resolve/main/transformer/config.json",
"shard_1_header": "https://huggingface.co/robbyant/lingbot-world-v2-14b-causal-fast-diffusers/resolve/main/transformer/diffusion_pytorch_model-00001-of-00008.safetensors",
"shard_8_header": "https://huggingface.co/robbyant/lingbot-world-v2-14b-causal-fast-diffusers/resolve/main/transformer/diffusion_pytorch_model-00008-of-00008.safetensors"
},
"dtype": "F32",
"shard_representatives": {
"diffusion_pytorch_model-00001-of-00008.safetensors": "blocks.0.cam_injector_layer1.bias",
"diffusion_pytorch_model-00002-of-00008.safetensors": "blocks.4.cam_injector_layer1.bias",
"diffusion_pytorch_model-00003-of-00008.safetensors": "blocks.10.cam_injector_layer1.bias",
"diffusion_pytorch_model-00004-of-00008.safetensors": "blocks.14.cam_scale_layer.bias",
"diffusion_pytorch_model-00005-of-00008.safetensors": "blocks.20.cam_injector_layer1.bias",
"diffusion_pytorch_model-00006-of-00008.safetensors": "blocks.25.cam_injector_layer1.bias",
"diffusion_pytorch_model-00007-of-00008.safetensors": "blocks.30.cam_injector_layer1.bias",
"diffusion_pytorch_model-00008-of-00008.safetensors": "blocks.35.cam_injector_layer1.bias"
},
"parameter_shapes": {
"blocks.0.cam_injector_layer1.bias": [5120],
"blocks.4.cam_injector_layer1.bias": [5120],
"blocks.10.cam_injector_layer1.bias": [5120],
"blocks.14.cam_scale_layer.bias": [5120],
"blocks.20.cam_injector_layer1.bias": [5120],
"blocks.25.cam_injector_layer1.bias": [5120],
"blocks.30.cam_injector_layer1.bias": [5120],
"blocks.35.cam_injector_layer1.bias": [5120],
"patch_embedding.weight": [5120, 36, 1, 2, 2],
"patch_embedding_wancamctrl.weight": [5120, 1536],
"c2ws_hidden_states_layer1.weight": [5120, 5120],
"c2ws_hidden_states_layer2.weight": [5120, 5120],
"blocks.0.modulation": [1, 6, 5120],
"blocks.0.self_attn.q.weight": [5120, 5120],
"blocks.0.ffn.0.weight": [13824, 5120],
"blocks.0.ffn.2.weight": [5120, 13824],
"blocks.0.cam_injector_layer1.weight": [5120, 5120],
"blocks.0.cam_injector_layer2.weight": [5120, 5120],
"blocks.0.cam_scale_layer.weight": [5120, 5120],
"blocks.0.cam_shift_layer.weight": [5120, 5120],
"head.modulation": [1, 2, 5120],
"head.head.weight": [64, 5120]
}
}
@@ -0,0 +1,30 @@
{
"_class_name": "UniPCMultistepScheduler",
"_diffusers_version": "0.35.0.dev0",
"beta_end": 0.02,
"beta_schedule": "linear",
"beta_start": 0.0001,
"disable_corrector": [],
"dynamic_thresholding_ratio": 0.995,
"final_sigmas_type": "zero",
"flow_shift": 5.0,
"lower_order_final": true,
"num_train_timesteps": 1000,
"predict_x0": true,
"prediction_type": "flow_prediction",
"rescale_betas_zero_snr": false,
"sample_max_value": 1.0,
"solver_order": 2,
"solver_p": null,
"solver_type": "bh2",
"steps_offset": 0,
"thresholding": false,
"time_shift_type": "exponential",
"timestep_spacing": "linspace",
"trained_betas": null,
"use_beta_sigmas": false,
"use_dynamic_shifting": false,
"use_exponential_sigmas": false,
"use_flow_sigmas": true,
"use_karras_sigmas": false
}
@@ -0,0 +1,112 @@
{
"source": "https://huggingface.co/robbyant/lingbot-world-v2-14b-causal-fast-diffusers/resolve/main/transformer/diffusion_pytorch_model.safetensors.index.json",
"index_parameter_count": 1421,
"canonical_name_set_sha256": "5d31b7f64fd5e5e271b789b2dbe50fb7bbf146bd8b750646bdc9abb4e5b8139d",
"canonical_name_to_shard_sha256": "e1addc27d2b1fad6f10226a23a265ee3e3c61e74dce4d5859727bdf180a67992",
"canonical_serialization": "sorted lines formatted as parameter_name=shard_filename with LF separators and no trailing LF",
"shards": {
"diffusion_pytorch_model-00001-of-00008.safetensors": {
"parameter_count": 180,
"parameter_names_sha256": "81a3d4778de604e75609aae7dac3d8dcbc081811e7748267fe953bbeb375e79d"
},
"diffusion_pytorch_model-00002-of-00008.safetensors": {
"parameter_count": 180,
"parameter_names_sha256": "4e36e2596a6468f29179fa5795396f2e9a7f7d8debc912b34a6f6c2c7c7c610d"
},
"diffusion_pytorch_model-00003-of-00008.safetensors": {
"parameter_count": 180,
"parameter_names_sha256": "6f74a7b2cf7444c2daa2b3fc32bd3787589b35543c230c5c13b89f0f0f2a0998"
},
"diffusion_pytorch_model-00004-of-00008.safetensors": {
"parameter_count": 180,
"parameter_names_sha256": "ec5c967ed5271ba7834337c4ee93a73f8ed0eead4492d4920624251c65c058ac"
},
"diffusion_pytorch_model-00005-of-00008.safetensors": {
"parameter_count": 180,
"parameter_names_sha256": "ad1fdab5830dd9ce43ea007f112b4be223327b7c8197c766d9cf9219e74d619b"
},
"diffusion_pytorch_model-00006-of-00008.safetensors": {
"parameter_count": 180,
"parameter_names_sha256": "fd58f0d86be42d8afcf55d3e07d8a22f7f6ef5064da931a600c890b71b6dfbae"
},
"diffusion_pytorch_model-00007-of-00008.safetensors": {
"parameter_count": 180,
"parameter_names_sha256": "305a1714b5f4fb54d5877c593b451273ab1490d90fd0896a299d96e140295047"
},
"diffusion_pytorch_model-00008-of-00008.safetensors": {
"parameter_count": 161,
"parameter_names_sha256": "4c18d94d96168682eb212f93e3cdf7313928776d8d9e6e4de0255fe899decb25"
}
},
"top_level_modules": [
"blocks",
"c2ws_hidden_states_layer1",
"c2ws_hidden_states_layer2",
"head",
"patch_embedding",
"patch_embedding_wancamctrl",
"text_embedding",
"time_embedding",
"time_projection"
],
"non_block_parameters": [
"c2ws_hidden_states_layer1.bias",
"c2ws_hidden_states_layer1.weight",
"c2ws_hidden_states_layer2.bias",
"c2ws_hidden_states_layer2.weight",
"head.head.bias",
"head.head.weight",
"head.modulation",
"patch_embedding.bias",
"patch_embedding.weight",
"patch_embedding_wancamctrl.bias",
"patch_embedding_wancamctrl.weight",
"text_embedding.0.bias",
"text_embedding.0.weight",
"text_embedding.2.bias",
"text_embedding.2.weight",
"time_embedding.0.bias",
"time_embedding.0.weight",
"time_embedding.2.bias",
"time_embedding.2.weight",
"time_projection.1.bias",
"time_projection.1.weight"
],
"block_parameter_suffixes": [
"cam_injector_layer1.bias",
"cam_injector_layer1.weight",
"cam_injector_layer2.bias",
"cam_injector_layer2.weight",
"cam_scale_layer.bias",
"cam_scale_layer.weight",
"cam_shift_layer.bias",
"cam_shift_layer.weight",
"cross_attn.k.bias",
"cross_attn.k.weight",
"cross_attn.norm_k.weight",
"cross_attn.norm_q.weight",
"cross_attn.o.bias",
"cross_attn.o.weight",
"cross_attn.q.bias",
"cross_attn.q.weight",
"cross_attn.v.bias",
"cross_attn.v.weight",
"ffn.0.bias",
"ffn.0.weight",
"ffn.2.bias",
"ffn.2.weight",
"modulation",
"norm3.bias",
"norm3.weight",
"self_attn.k.bias",
"self_attn.k.weight",
"self_attn.norm_k.weight",
"self_attn.norm_q.weight",
"self_attn.o.bias",
"self_attn.o.weight",
"self_attn.q.bias",
"self_attn.q.weight",
"self_attn.v.bias",
"self_attn.v.weight"
]
}
@@ -0,0 +1,294 @@
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import asyncio
import pytest
import torch
from vllm_omni.diffusion.models.lingbot_world.actions import (
LINGBOT_CAMERA_ACTION_SCHEMA,
LINGBOT_CAMERA_TRAJECTORY_SCHEMA,
LingBotCameraControlReducer,
integrate_lingbot_camera_actions,
parse_lingbot_camera_action_frames,
)
from vllm_omni.experimental.ar_diffusion.session import (
ARDiffusionSession,
ARDiffusionSessionEvent,
)
from vllm_omni.experimental.ar_diffusion.tick_protocol import (
ARDiffusionChunkMetadata,
ARDiffusionControlInput,
ARDiffusionTickRequest,
)
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
def _action_control(mode: str, **data) -> ARDiffusionControlInput:
return ARDiffusionControlInput(
track="camera",
schema=LINGBOT_CAMERA_ACTION_SCHEMA,
data={"mode": mode, **data},
)
def _prepared_frames(prepared) -> tuple[tuple[str, ...], ...]:
assert len(prepared.controls) == 1
return prepared.controls[0].data["frames"]
def test_state_controls_hold_release_and_preserve_short_pulse() -> None:
reducer = LingBotCameraControlReducer()
pressed = reducer.prepare(
current_controls={},
events=(
ARDiffusionSessionEvent(
event_id=1,
controls=(
_action_control(
"state",
transitions=[{"actions": ["W"], "client_ts_ms": 10}],
),
),
),
),
chunk_index=0,
)
assert _prepared_frames(pressed) == (("w",), ("w",), ("w",))
assert pressed.controls[0].to_dict()["data"]["frames"] == [["w"], ["w"], ["w"]]
reducer.commit(pressed)
held = reducer.prepare(
current_controls={"camera": pressed.controls[0]},
events=(),
chunk_index=1,
)
assert _prepared_frames(held) == (("w",), ("w",), ("w",))
reducer.commit(held)
pulse = reducer.prepare(
current_controls={"camera": held.controls[0]},
events=(
ARDiffusionSessionEvent(
event_id=2,
controls=(
_action_control(
"state",
transitions=[
{"actions": ["j"], "client_ts_ms": 20},
{"actions": [], "client_ts_ms": 21},
],
),
),
),
),
chunk_index=2,
)
assert _prepared_frames(pulse) == (("j",), (), ())
reducer.commit(pulse)
released = reducer.prepare(
current_controls={"camera": pulse.controls[0]},
events=(),
chunk_index=3,
)
assert _prepared_frames(released) == ((), (), ())
def test_script_is_transactional_consumed_once_and_neutral_padded() -> None:
reducer = LingBotCameraControlReducer()
event = ARDiffusionSessionEvent(
event_id=1,
controls=(
_action_control(
"script",
frames=[["w"], ["d"], ["l"], ["i"]],
),
),
)
first_attempt = reducer.prepare(
current_controls={},
events=(event,),
chunk_index=0,
)
assert _prepared_frames(first_attempt) == (("w",), ("d",), ("l",))
# An uncommitted failed attempt must not consume the reducer's script.
second_attempt = reducer.prepare(
current_controls={},
events=(event,),
chunk_index=0,
)
assert _prepared_frames(second_attempt) == (("w",), ("d",), ("l",))
reducer.commit(second_attempt)
tail = reducer.prepare(
current_controls={"camera": second_attempt.controls[0]},
events=(),
chunk_index=1,
)
assert _prepared_frames(tail) == (("i",), (), ())
reducer.commit(tail)
exhausted = reducer.prepare(
current_controls={"camera": tail.controls[0]},
events=(),
chunk_index=2,
)
assert _prepared_frames(exhausted) == ((), (), ())
def test_trajectory_control_remains_an_expert_override() -> None:
reducer = LingBotCameraControlReducer()
trajectory = ARDiffusionControlInput(
track="camera",
schema=LINGBOT_CAMERA_TRAJECTORY_SCHEMA,
data={"poses": [[[1.0]]], "intrinsics": [[1.0]]},
)
prepared = reducer.prepare(
current_controls={},
events=(
ARDiffusionSessionEvent(
event_id=1,
controls=(trajectory,),
),
),
chunk_index=0,
)
assert prepared.controls == (trajectory,)
def test_action_integrator_matches_lingbot_motion_and_calibration() -> None:
trajectory, pitch = integrate_lingbot_camera_actions(
[["w"], ["d"], ["l"]],
width=832,
height=480,
)
assert pitch == 0.0
torch.testing.assert_close(
trajectory.poses[0, :3, 3],
torch.tensor([0.0, 0.0, 0.05], dtype=torch.float64),
atol=1e-6,
rtol=0,
)
torch.testing.assert_close(
trajectory.poses[1, :3, 3],
torch.tensor([0.05, 0.0, 0.05], dtype=torch.float64),
atol=1e-6,
rtol=0,
)
expected_yaw = torch.tensor(
[
[0.9945219, 0.0, 0.10452846],
[0.0, 1.0, 0.0],
[-0.10452846, 0.0, 0.9945219],
],
dtype=torch.float64,
)
torch.testing.assert_close(
trajectory.poses[2, :3, :3],
expected_yaw,
atol=1e-6,
rtol=0,
)
torch.testing.assert_close(
trajectory.intrinsics[0],
torch.tensor([500.0, 500.0, 416.0, 240.0]),
)
resized, _ = integrate_lingbot_camera_actions(
[[]],
width=416,
height=240,
)
scaled = resized.intrinsics[0] * torch.tensor([416 / 832, 240 / 480, 416 / 832, 240 / 480])
torch.testing.assert_close(
scaled,
torch.tensor([500.0, 500.0, 208.0, 120.0]),
)
@pytest.mark.parametrize(
("data", "message"),
[
({"mode": "frames", "frames": [["x"], [], []]}, "W/A/S/D/I/J/K/L"),
({"mode": "frames", "frames": [["w"]]}, "exactly one action list"),
({"mode": "state", "frames": [[], [], []]}, "mode='frames'"),
],
)
def test_model_tick_action_payload_validation(data, message: str) -> None:
with pytest.raises(ValueError, match=message):
parse_lingbot_camera_action_frames(data, expected_frames=3)
class _TickConsumer:
def __init__(self) -> None:
self.ticks: list[ARDiffusionTickRequest] = []
async def execute_tick(self, tick: ARDiffusionTickRequest) -> ARDiffusionChunkMetadata:
self.ticks.append(tick)
return ARDiffusionChunkMetadata.from_tick(tick)
def chunk_metadata(self, output: ARDiffusionChunkMetadata) -> ARDiffusionChunkMetadata:
return output
class _Lifecycle:
async def reset_session(self, session_id: str) -> None:
del session_id
async def close_session(self, session_id: str) -> None:
del session_id
def test_session_reducer_keeps_prompt_and_actions_atomic_and_resets() -> None:
async def exercise() -> None:
consumer = _TickConsumer()
reducer = LingBotCameraControlReducer()
session = ARDiffusionSession(
"world-actions",
tick_consumer=consumer,
lifecycle=_Lifecycle(),
max_pending_events=8,
request_id_factory=lambda session_id, chunk_index: f"{session_id}-{chunk_index}",
control_reducer=reducer,
)
await session.accept_event(
ARDiffusionSessionEvent(
event_id=1,
prompt="turn toward the window",
controls=(
_action_control(
"state",
transitions=[{"actions": ["d"]}],
),
),
)
)
await session.next_chunk()
tick = consumer.ticks[0]
assert tick.prompt == "turn toward the window"
assert tick.applied_event_ids == (1,)
assert tick.controls[0].data["frames"] == (("d",), ("d",), ("d",))
await session.reset()
await session.accept_event(
ARDiffusionSessionEvent(
event_id=2,
controls=(
_action_control(
"state",
transitions=[{"actions": []}],
),
),
)
)
await session.next_chunk()
assert consumer.ticks[1].chunk_index == 0
assert consumer.ticks[1].controls[0].data["frames"] == ((), (), ())
asyncio.run(exercise())
@@ -0,0 +1,543 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
import importlib.util
import math
import sys
from pathlib import Path
from types import ModuleType
import pytest
import torch
import torch.nn.functional as F
from torch import nn
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
_MODULE_PATH = Path(__file__).parents[4] / "vllm_omni/diffusion/models/lingbot_world/transformer.py"
_MODULE_NAME = "_lingbot_world_attention_under_test"
_MISSING_MODULE = object()
_STUBBED_MODULE_NAMES = (
"vllm",
"vllm.distributed",
"vllm.model_executor",
"vllm.model_executor.layers",
"vllm.model_executor.layers.conv",
"vllm.model_executor.layers.linear",
"vllm.model_executor.model_loader",
"vllm.model_executor.model_loader.weight_utils",
"vllm.model_executor.utils",
"vllm_omni",
"vllm_omni.diffusion",
"vllm_omni.diffusion.attention",
"vllm_omni.diffusion.attention.layer",
"vllm_omni.diffusion.layers",
"vllm_omni.diffusion.layers.norm",
"vllm_omni.diffusion.layers.rope",
"vllm_omni.experimental",
"vllm_omni.experimental.ar_diffusion",
"vllm_omni.experimental.ar_diffusion.kv_cache",
"vllm_omni.experimental.ar_diffusion.kv_cache.paged_attention",
)
@pytest.fixture(autouse=True)
def _inference_context():
with torch.inference_mode():
yield
def _install_vllm_stubs() -> None:
for name in _STUBBED_MODULE_NAMES:
sys.modules[name] = ModuleType(name)
distributed = sys.modules["vllm.distributed"]
distributed.get_tensor_model_parallel_rank = lambda: 0
distributed.get_tensor_model_parallel_world_size = lambda: 1
distributed.tensor_model_parallel_all_reduce = lambda value: value
def set_weight_attrs(weight: torch.Tensor, attrs: dict) -> None:
for name, value in attrs.items():
setattr(weight, name, value)
sys.modules["vllm.model_executor.utils"].set_weight_attrs = set_weight_attrs
sys.modules["vllm.model_executor.model_loader.weight_utils"].default_weight_loader = lambda param, loaded_weight: (
param.data.copy_(loaded_weight)
)
class _Linear(nn.Module):
def __init__(
self,
input_size: int,
output_size: int,
*,
bias: bool = True,
return_bias: bool = False,
**kwargs,
) -> None:
super().__init__()
del kwargs
self.return_bias = return_bias
self.weight = nn.Parameter(torch.empty(output_size, input_size))
self.bias = nn.Parameter(torch.empty(output_size)) if bias else None
nn.init.kaiming_uniform_(self.weight, a=math.sqrt(5))
if self.bias is not None:
nn.init.zeros_(self.bias)
self.calls = 0
def forward(self, value: torch.Tensor):
self.calls += 1
output = F.linear(value, self.weight, self.bias)
return (output, self.bias) if self.return_bias else output
linear = sys.modules["vllm.model_executor.layers.linear"]
linear.ColumnParallelLinear = _Linear
linear.RowParallelLinear = _Linear
class _QKVLinear(nn.Module):
def __init__(
self,
hidden_size: int,
head_size: int,
total_num_heads: int,
total_num_kv_heads: int | None = None,
*,
bias: bool = True,
**kwargs,
) -> None:
super().__init__()
del kwargs
self.num_heads = total_num_heads
self.num_kv_heads = total_num_kv_heads or total_num_heads
output_size = (self.num_heads + 2 * self.num_kv_heads) * head_size
self.weight = nn.Parameter(torch.empty(output_size, hidden_size))
self.bias = nn.Parameter(torch.empty(output_size)) if bias else None
nn.init.kaiming_uniform_(self.weight, a=math.sqrt(5))
if self.bias is not None:
nn.init.zeros_(self.bias)
self.calls = 0
def load_shard(param: torch.Tensor, loaded_weight: torch.Tensor, shard_id: str) -> None:
shard_index = {"q": 0, "k": 1, "v": 2}[shard_id]
shard_size = param.shape[0] // 3
param.data.narrow(0, shard_index * shard_size, shard_size).copy_(loaded_weight)
self.weight.weight_loader = load_shard
if self.bias is not None:
self.bias.weight_loader = load_shard
def forward(self, value: torch.Tensor):
self.calls += 1
return F.linear(value, self.weight, self.bias), None
linear.QKVParallelLinear = _QKVLinear
class _Conv3dLayer(nn.Conv3d):
def __init__(self, in_channels: int, out_channels: int, kernel_size, stride, **kwargs) -> None:
super().__init__(in_channels, out_channels, kernel_size=kernel_size, stride=stride, **kwargs)
sys.modules["vllm.model_executor.layers.conv"].Conv3dLayer = _Conv3dLayer
class _LayerNorm(nn.LayerNorm):
def __init__(self, dim: int, eps: float = 1e-6, elementwise_affine: bool = True) -> None:
super().__init__(dim, eps=eps, elementwise_affine=elementwise_affine)
sys.modules["vllm_omni.diffusion.layers.norm"].LayerNorm = _LayerNorm
class _Attention(nn.Module):
def __init__(self, *args, softmax_scale: float, **kwargs) -> None:
super().__init__()
del args
self.softmax_scale = softmax_scale
self.causal = kwargs["causal"]
self.calls: list[tuple[torch.Tensor, torch.Tensor, torch.Tensor]] = []
def forward(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata=None,
) -> torch.Tensor:
del attn_metadata
self.calls.append((query.detach().clone(), key.detach().clone(), value.detach().clone()))
scores = torch.einsum("bqhd,bkhd->bhqk", query, key) * self.softmax_scale
weights = scores.softmax(dim=-1)
return torch.einsum("bhqk,bkhd->bqhd", weights, value)
sys.modules["vllm_omni.diffusion.attention.layer"].Attention = _Attention
class _RotaryEmbeddingWan(nn.Module):
def __init__(self, *args, **kwargs) -> None:
super().__init__()
del args, kwargs
self.calls: list[tuple[torch.Tensor, torch.Tensor]] = []
def forward(self, value: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
self.calls.append((cos.detach().clone(), sin.detach().clone()))
return value
sys.modules["vllm_omni.diffusion.layers.rope"].RotaryEmbeddingWan = _RotaryEmbeddingWan
paged_attention = sys.modules["vllm_omni.experimental.ar_diffusion.kv_cache.paged_attention"]
class _PagedLayerContext:
pass
class _PagedLayerInputs:
pass
paged_attention.ARDiffusionPagedLayerContext = _PagedLayerContext
paged_attention.ARDiffusionPagedLayerInputs = _PagedLayerInputs
paged_attention.ar_diffusion_paged_attention = lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("CUDA paged attention was not expected in this CPU test")
)
paged_attention.paged_write_attn = lambda *args, **kwargs: (_ for _ in ()).throw(
AssertionError("paged path was not expected in this direct-cache test")
)
def _load_module():
assert _MODULE_PATH.exists(), "LingBot attention module has not been implemented"
module_names = (*_STUBBED_MODULE_NAMES, _MODULE_NAME)
previous = {name: sys.modules.get(name, _MISSING_MODULE) for name in module_names}
try:
_install_vllm_stubs()
spec = importlib.util.spec_from_file_location(_MODULE_NAME, _MODULE_PATH)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
finally:
for name, original in previous.items():
if original is _MISSING_MODULE:
sys.modules.pop(name, None)
else:
sys.modules[name] = original
def test_load_module_restores_process_module_state() -> None:
module_names = (*_STUBBED_MODULE_NAMES, _MODULE_NAME)
previous = {name: sys.modules.get(name, _MISSING_MODULE) for name in module_names}
_load_module()
for name, original in previous.items():
assert sys.modules.get(name, _MISSING_MODULE) is original
def test_allocate_lingbot_cache_creates_request_local_layer_storage() -> None:
module = _load_module()
cache = module.allocate_lingbot_cache(
batch_size=2,
num_layers=3,
max_tokens=7,
num_local_heads=2,
head_dim=4,
device=torch.device("cpu"),
dtype=torch.float32,
)
assert len(cache.self_attention) == 3
assert len(cache.cross_attention) == 3
assert cache.cross_attention == [None, None, None]
assert cache.self_attention[0].key.shape == (2, 7, 2, 4)
assert cache.self_attention[0].value.shape == (2, 7, 2, 4)
assert cache.self_attention[0].key.dtype == torch.float32
assert cache.self_attention[0].end == 0
assert cache.self_attention[0].key.data_ptr() != cache.self_attention[1].key.data_ptr()
def _allocate_single_layer(module, *, max_tokens: int):
return module.allocate_lingbot_cache(
batch_size=1,
num_layers=1,
max_tokens=max_tokens,
num_local_heads=1,
head_dim=2,
device=torch.device("cpu"),
dtype=torch.float32,
)
def _set_identity_attention(attention: nn.Module) -> None:
qkv = getattr(attention, "qkv", None)
if qkv is not None:
projections = (qkv, attention.o)
else:
projections = (attention.q, attention.k, attention.v, attention.o)
for projection in projections:
with torch.no_grad():
identity = torch.eye(2)
projection.weight.copy_(torch.cat((identity, identity, identity)) if projection is qkv else identity)
if projection.bias is not None:
projection.bias.zero_()
attention.norm_q = nn.Identity()
attention.norm_k = nn.Identity()
def _tokens(*values: float) -> torch.Tensor:
return torch.tensor([[[value, 0.0] for value in values]])
def _record_inputs(module: nn.Module) -> list[tuple]:
records: list[tuple] = []
module.register_forward_pre_hook(lambda _module, args: records.append(args))
return records
def _record_outputs(module: nn.Module) -> list[torch.Tensor]:
records: list[torch.Tensor] = []
def record(_module, _args, output) -> None:
records.append(output[0] if isinstance(output, tuple) else output)
module.register_forward_hook(record)
return records
def _cache_snapshot(cache) -> tuple[torch.Tensor, torch.Tensor, tuple[int, int, int | None, int]]:
return (
cache.key.detach().view(torch.uint8).clone(),
cache.value.detach().view(torch.uint8).clone(),
(cache.end, cache.absolute_end, cache.last_start, cache.sink_end),
)
def _assert_cache_unchanged(cache, snapshot) -> None:
key, value, metadata = snapshot
assert torch.equal(cache.key.detach().view(torch.uint8), key)
assert torch.equal(cache.value.detach().view(torch.uint8), value)
assert (cache.end, cache.absolute_end, cache.last_start, cache.sink_end) == metadata
def test_self_attention_repeated_offset_overwrites_then_later_offset_appends() -> None:
module = _load_module()
attention = module.LingBotSelfAttention(dim=2, num_heads=1)
_set_identity_attention(attention)
cache = _allocate_single_layer(module, max_tokens=6).self_attention[0]
attention(_tokens(1, 2), cache=cache, current_start=0, sink_tokens=0)
attention(_tokens(10, 20), cache=cache, current_start=0, sink_tokens=0)
assert cache.end == 2
assert cache.absolute_end == 2
torch.testing.assert_close(cache.key[0, : cache.end, 0, 0], torch.tensor([10.0, 20.0]))
attention(_tokens(3, 4), cache=cache, current_start=2, sink_tokens=0)
assert cache.end == 4
assert cache.absolute_end == 4
torch.testing.assert_close(cache.key[0, : cache.end, 0, 0], torch.tensor([10.0, 20.0, 3.0, 4.0]))
def test_self_attention_uses_one_fused_qkv_projection() -> None:
module = _load_module()
attention = module.LingBotSelfAttention(dim=2, num_heads=1)
cache = _allocate_single_layer(module, max_tokens=2).self_attention[0]
attention(_tokens(1, 2), cache=cache, current_start=0, sink_tokens=0)
assert attention.qkv.calls == 1
def test_self_attention_routes_paged_inputs_through_engine_owned_cache() -> None:
module = _load_module()
attention = module.LingBotSelfAttention(dim=2, num_heads=1)
_set_identity_attention(attention)
paged_inputs = module.ARDiffusionPagedLayerInputs()
calls = []
def paged_write(inputs, query, key, value, k_act, v_act, scale):
calls.append((inputs, query.clone(), key.clone(), value.clone(), k_act, v_act, scale))
return query
module.paged_write_attn = paged_write
output = attention(
_tokens(1, 2),
cache=paged_inputs,
current_start=0,
sink_tokens=0,
update_cache=False,
)
assert output.shape == (1, 2, 2)
assert len(calls) == 1
assert calls[0][0] is paged_inputs
assert calls[0][4:6] == (None, None)
assert attention.attn.calls == []
assert not any(hasattr(attention, name) for name in ("q", "k", "v"))
def test_self_attention_is_chunk_causal_without_masking_inside_current_chunk() -> None:
module = _load_module()
attention = module.LingBotSelfAttention(dim=2, num_heads=1)
_set_identity_attention(attention)
cache = _allocate_single_layer(module, max_tokens=8).self_attention[0]
attention_inputs = _record_inputs(attention.attn)
attention(_tokens(1, 2), cache=cache, current_start=0, sink_tokens=0)
first_keys = attention_inputs[-1][1].detach().clone()
attention(_tokens(3, 4), cache=cache, current_start=2, sink_tokens=0)
second_keys = attention_inputs[-1][1].detach().clone()
assert attention.attn.causal is False
torch.testing.assert_close(first_keys[0, :, 0, 0], torch.tensor([1.0, 2.0]))
torch.testing.assert_close(second_keys[0, :, 0, 0], torch.tensor([1.0, 2.0, 3.0, 4.0]))
def test_self_attention_retains_sink_and_latest_local_history_after_eviction() -> None:
module = _load_module()
attention = module.LingBotSelfAttention(dim=2, num_heads=1)
_set_identity_attention(attention)
cache = _allocate_single_layer(module, max_tokens=4).self_attention[0]
attention_inputs = _record_inputs(attention.attn)
attention(_tokens(1, 2), cache=cache, current_start=0, sink_tokens=1)
attention(_tokens(3, 4), cache=cache, current_start=2, sink_tokens=1)
attention(_tokens(5, 6), cache=cache, current_start=4, sink_tokens=1)
visible_keys = attention_inputs[-1][1].detach().clone()
assert cache.end == 4
assert cache.absolute_end == 6
torch.testing.assert_close(visible_keys[0, :, 0, 0], torch.tensor([1.0, 4.0, 5.0, 6.0]))
torch.testing.assert_close(cache.key[0, : cache.end, 0, 0], torch.tensor([1.0, 4.0, 5.0, 6.0]))
def test_cross_attention_projects_encoder_kv_once_per_request() -> None:
module = _load_module()
attention = module.LingBotCrossAttention(dim=2, num_heads=1)
_set_identity_attention(attention)
key_outputs = _record_outputs(attention.k)
value_outputs = _record_outputs(attention.v)
attention_inputs = _record_inputs(attention.attn)
output, cache = attention(_tokens(1), _tokens(2, 3), cache=None)
second_output, reused_cache = attention(_tokens(4), _tokens(20, 30), cache=cache)
assert output.shape == second_output.shape == (1, 1, 2)
assert reused_cache is cache
assert len(key_outputs) == 1
assert len(value_outputs) == 1
assert cache.end == 2
torch.testing.assert_close(cache.key[0, :, 0, 0], torch.tensor([2.0, 3.0]))
torch.testing.assert_close(attention_inputs[-1][1], cache.key)
def test_self_attention_cache_is_isolated_between_requests() -> None:
module = _load_module()
attention = module.LingBotSelfAttention(dim=2, num_heads=1)
_set_identity_attention(attention)
first = _allocate_single_layer(module, max_tokens=4).self_attention[0]
second = _allocate_single_layer(module, max_tokens=4).self_attention[0]
attention(_tokens(1, 2), cache=first, current_start=0, sink_tokens=0)
assert first.end == 2
assert second.end == 0
assert torch.count_nonzero(second.key) == 0
attention(_tokens(8), cache=second, current_start=0, sink_tokens=0)
torch.testing.assert_close(first.key[0, : first.end, 0, 0], torch.tensor([1.0, 2.0]))
torch.testing.assert_close(second.key[0, : second.end, 0, 0], torch.tensor([8.0]))
def test_cross_attention_keeps_checkpoint_qkvo_and_norm_parameter_names() -> None:
module = _load_module()
attention = module.LingBotCrossAttention(dim=4, num_heads=2)
names = set(attention.state_dict())
assert {
"q.weight",
"k.weight",
"v.weight",
"o.weight",
"norm_q.weight",
"norm_k.weight",
} <= names
assert callable(attention.norm_q.weight.weight_loader)
def test_tp_world_size_one_attention_output_shapes() -> None:
module = _load_module()
self_attention = module.LingBotSelfAttention(dim=4, num_heads=2)
cross_attention = module.LingBotCrossAttention(dim=4, num_heads=2)
cache = module.allocate_lingbot_cache(
batch_size=2,
num_layers=1,
max_tokens=5,
num_local_heads=2,
head_dim=2,
device=torch.device("cpu"),
dtype=torch.float32,
)
hidden_states = torch.randn(2, 3, 4)
encoder_hidden_states = torch.randn(2, 5, 4)
self_output = self_attention(hidden_states, cache=cache.self_attention[0], current_start=0, sink_tokens=0)
cross_output, cache.cross_attention[0] = cross_attention(
hidden_states,
encoder_hidden_states,
cache=cache.cross_attention[0],
)
assert self_output.shape == hidden_states.shape
assert cross_output.shape == hidden_states.shape
assert cache.cross_attention[0] is not None
def test_self_attention_applies_rotary_embedding_to_current_query_and_key() -> None:
module = _load_module()
attention = module.LingBotSelfAttention(dim=2, num_heads=1)
cache = _allocate_single_layer(module, max_tokens=2).self_attention[0]
cos = torch.ones(2, 1)
sin = torch.zeros(2, 1)
rotary_inputs = _record_inputs(attention.rotary_embedding)
attention(_tokens(1, 2), cache=cache, current_start=0, rotary_emb=(cos, sin), sink_tokens=0)
assert len(rotary_inputs) == 2
torch.testing.assert_close(rotary_inputs[0][1], cos)
torch.testing.assert_close(rotary_inputs[1][2], sin)
def test_attention_block_owns_checkpoint_parent_namespaces() -> None:
module = _load_module()
block = module.LingBotAttentionBlock(dim=4, num_heads=2, prefix="blocks.0")
names = set(block.state_dict())
expected = {
*(f"self_attn.{child}.weight" for child in ("qkv", "o", "norm_q", "norm_k")),
*(f"cross_attn.{child}.weight" for child in ("q", "k", "v", "o", "norm_q", "norm_k")),
}
assert expected <= names
def test_cross_attention_caches_are_isolated_between_requests() -> None:
module = _load_module()
attention = module.LingBotCrossAttention(dim=2, num_heads=1)
_, first = attention(_tokens(1), _tokens(2, 3), cache=None)
_, second = attention(_tokens(1), _tokens(8, 9), cache=None)
assert first.key.untyped_storage().data_ptr() != second.key.untyped_storage().data_ptr()
assert first.value.untyped_storage().data_ptr() != second.value.untyped_storage().data_ptr()
second_key = second.key.clone()
first.key.zero_()
torch.testing.assert_close(second.key, second_key)
def test_tp_rmsnorm_weight_loader_selects_rank_shard(monkeypatch: pytest.MonkeyPatch) -> None:
module = _load_module()
monkeypatch.setattr(module, "get_tensor_model_parallel_world_size", lambda: 2)
monkeypatch.setattr(module, "get_tensor_model_parallel_rank", lambda: 1)
norm = module._LingBotRMSNorm(2, eps=1e-6)
norm.weight.weight_loader(norm.weight, torch.tensor([10.0, 20.0, 30.0, 40.0]))
torch.testing.assert_close(norm.weight, torch.tensor([30.0, 40.0]))
@@ -0,0 +1,525 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
import importlib.util
import math
import subprocess
import sys
from pathlib import Path
import numpy as np
import pytest
import torch
_MODULE_PATH = Path(__file__).parents[4] / "vllm_omni/diffusion/models/lingbot_world/camera.py"
_SPEC = importlib.util.spec_from_file_location("_lingbot_world_camera_under_test", _MODULE_PATH)
assert _SPEC is not None and _SPEC.loader is not None
_CAMERA_MODULE = importlib.util.module_from_spec(_SPEC)
sys.modules[_SPEC.name] = _CAMERA_MODULE
_SPEC.loader.exec_module(_CAMERA_MODULE)
CameraTrajectory = _CAMERA_MODULE.CameraTrajectory
build_plucker_embedding = _CAMERA_MODULE.build_plucker_embedding
interpolate_camera_trajectory = _CAMERA_MODULE.interpolate_camera_trajectory
load_camera_trajectory = _CAMERA_MODULE.load_camera_trajectory
resolve_trusted_action_directory = _CAMERA_MODULE.resolve_trusted_action_directory
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
def _identity_poses(num_frames: int) -> np.ndarray:
return np.repeat(np.eye(4, dtype=np.float64)[None], num_frames, axis=0)
def _write_trajectory(
directory: Path,
poses: np.ndarray,
intrinsics: np.ndarray,
) -> None:
np.save(directory / "poses.npy", poses)
np.save(directory / "intrinsics.npy", intrinsics)
def _trusted(directory: Path):
return resolve_trusted_action_directory(".", directory)
def _trajectory(
poses: torch.Tensor,
intrinsics: torch.Tensor | None = None,
) -> CameraTrajectory:
if intrinsics is None:
intrinsics = torch.tensor([[832.0 / 3.0, 160.0, 416.0, 240.0]]).repeat(poses.shape[0], 1)
return CameraTrajectory(poses=poses, intrinsics=intrinsics)
@pytest.mark.parametrize("present_file", [None, "poses.npy"])
def test_load_camera_trajectory_rejects_missing_files(tmp_path: Path, present_file: str | None) -> None:
if present_file == "poses.npy":
np.save(tmp_path / present_file, _identity_poses(1))
missing_file = "poses.npy" if present_file is None else "intrinsics.npy"
with pytest.raises(FileNotFoundError, match=missing_file):
load_camera_trajectory(_trusted(tmp_path))
@pytest.mark.parametrize(
("poses", "intrinsics", "message"),
[
(np.zeros((2, 3, 4)), np.zeros((2, 4)), "poses.npy"),
(_identity_poses(2), np.zeros((2, 3)), "intrinsics.npy"),
(_identity_poses(0), np.zeros((0, 4)), "1 and 4096 source frames"),
(_identity_poses(2), np.zeros((1, 4)), "same number of frames"),
],
)
def test_load_camera_trajectory_rejects_invalid_shapes(
tmp_path: Path,
poses: np.ndarray,
intrinsics: np.ndarray,
message: str,
) -> None:
_write_trajectory(tmp_path, poses, intrinsics)
with pytest.raises(ValueError, match=message):
load_camera_trajectory(_trusted(tmp_path))
@pytest.mark.parametrize("array_name", ["poses", "intrinsics"])
def test_load_camera_trajectory_rejects_non_finite_values(tmp_path: Path, array_name: str) -> None:
poses = _identity_poses(1)
intrinsics = np.ones((1, 4), dtype=np.float64)
if array_name == "poses":
poses[0, 0, 0] = np.nan
else:
intrinsics[0, 0] = np.inf
_write_trajectory(tmp_path, poses, intrinsics)
with pytest.raises(ValueError, match="finite"):
load_camera_trajectory(_trusted(tmp_path))
def test_load_camera_trajectory_preserves_raw_c2w_poses(tmp_path: Path) -> None:
first_c2w = np.array(
[
[0.0, -1.0, 0.0, 2.0],
[1.0, 0.0, 0.0, 3.0],
[0.0, 0.0, 1.0, 4.0],
[0.0, 0.0, 0.0, 1.0],
]
)
second_c2w = np.array(
[
[0.0, 0.0, 1.0, 5.0],
[0.0, 1.0, 0.0, 4.0],
[-1.0, 0.0, 0.0, 6.0],
[0.0, 0.0, 0.0, 1.0],
]
)
poses = np.stack((first_c2w, second_c2w))
intrinsics = np.array([[100.0, 110.0, 120.0, 130.0]] * 2)
_write_trajectory(tmp_path, poses, intrinsics)
trajectory = load_camera_trajectory(_trusted(tmp_path))
assert trajectory.poses.dtype == torch.float32
assert trajectory.intrinsics.dtype == torch.float32
torch.testing.assert_close(trajectory.poses, torch.from_numpy(poses).float())
torch.testing.assert_close(trajectory.intrinsics, torch.from_numpy(intrinsics).float())
def test_load_camera_trajectory_rejects_object_dtype_before_materializing(tmp_path: Path) -> None:
poses = _identity_poses(1).astype(object)
intrinsics = np.ones((1, 4), dtype=np.float32)
_write_trajectory(tmp_path, poses, intrinsics)
with pytest.raises(ValueError, match="poses.npy.*real numeric"):
load_camera_trajectory(_trusted(tmp_path))
def test_load_camera_trajectory_accepts_official_length_and_materializes_request_prefix(tmp_path: Path) -> None:
_write_trajectory(
tmp_path,
_identity_poses(269),
np.arange(269 * 4, dtype=np.float32).reshape(269, 4),
)
trajectory = load_camera_trajectory(_trusted(tmp_path))
assert trajectory.poses.shape == (117, 4, 4)
assert trajectory.intrinsics.shape == (117, 4)
np.testing.assert_array_equal(
trajectory.intrinsics.numpy(),
np.arange(269 * 4, dtype=np.float32).reshape(269, 4)[:117],
)
def test_load_camera_trajectory_rejects_unbounded_source_frame_count(tmp_path: Path) -> None:
_write_trajectory(
tmp_path,
_identity_poses(4097),
np.ones((4097, 4), dtype=np.float32),
)
with pytest.raises(ValueError, match="poses.npy.*bounded camera-array limit|4096 source frames"):
load_camera_trajectory(_trusted(tmp_path))
def test_load_camera_trajectory_rejects_truncated_payload_before_loading(tmp_path: Path) -> None:
_write_trajectory(tmp_path, _identity_poses(1), np.ones((1, 4), dtype=np.float32))
poses_path = tmp_path / "poses.npy"
poses_path.write_bytes(poses_path.read_bytes()[:-1])
with pytest.raises(ValueError, match="poses.npy byte size"):
load_camera_trajectory(_trusted(tmp_path))
def test_load_camera_trajectory_rejects_oversized_npy_header(tmp_path: Path) -> None:
header_size = 20_000
(tmp_path / "poses.npy").write_bytes(
np.lib.format.magic(2, 0) + header_size.to_bytes(4, "little") + b" " * (header_size - 1) + b"\n"
)
np.save(tmp_path / "intrinsics.npy", np.ones((1, 4), dtype=np.float32))
with pytest.raises(ValueError, match="poses.npy.*oversized NPY header"):
load_camera_trajectory(_trusted(tmp_path))
def test_load_camera_trajectory_rejects_symlink_swap_after_resolution(tmp_path: Path) -> None:
trusted_root = tmp_path / "trusted"
action_directory = trusted_root / "action"
outside = tmp_path / "outside"
action_directory.mkdir(parents=True)
outside.mkdir()
_write_trajectory(action_directory, _identity_poses(1), np.ones((1, 4), dtype=np.float32))
_write_trajectory(outside, _identity_poses(1), np.ones((1, 4), dtype=np.float32))
resolved = resolve_trusted_action_directory("action", trusted_root)
action_directory.rename(trusted_root / "original-action")
action_directory.symlink_to(outside, target_is_directory=True)
with pytest.raises(ValueError, match="securely open.*action directory"):
load_camera_trajectory(resolved)
def test_load_camera_trajectory_rejects_symlinked_camera_file(tmp_path: Path) -> None:
trusted_root = tmp_path / "trusted"
action_directory = trusted_root / "action"
outside = tmp_path / "outside"
action_directory.mkdir(parents=True)
outside.mkdir()
_write_trajectory(action_directory, _identity_poses(1), np.ones((1, 4), dtype=np.float32))
np.save(outside / "poses.npy", _identity_poses(1))
resolved = resolve_trusted_action_directory("action", trusted_root)
(action_directory / "poses.npy").unlink()
(action_directory / "poses.npy").symlink_to(outside / "poses.npy")
with pytest.raises(ValueError, match="poses.npy.*opened securely"):
load_camera_trajectory(resolved)
@pytest.mark.skipif(not hasattr(__import__("os"), "mkfifo"), reason="requires POSIX FIFO support")
def test_load_camera_trajectory_rejects_fifo_without_blocking(tmp_path: Path) -> None:
import os
os.mkfifo(tmp_path / "poses.npy")
np.save(tmp_path / "intrinsics.npy", np.ones((1, 4), dtype=np.float32))
script = """
import importlib.util
import sys
spec = importlib.util.spec_from_file_location("_lingbot_camera_fifo_test", sys.argv[1])
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
action = module.resolve_trusted_action_directory(".", sys.argv[2])
try:
module.load_camera_trajectory(action)
except ValueError as error:
print(error)
else:
raise AssertionError("FIFO camera input was accepted")
"""
completed = subprocess.run(
[sys.executable, "-c", script, str(_MODULE_PATH), str(tmp_path)],
capture_output=True,
text=True,
timeout=3,
check=False,
)
assert completed.returncode == 0, completed.stderr
assert "poses.npy must be a regular NPY file" in completed.stdout
def test_load_camera_trajectory_decodes_the_validated_file_snapshot(tmp_path: Path, monkeypatch) -> None:
original_poses = _identity_poses(1)
replacement_poses = original_poses.copy()
replacement_poses[0, 0, 3] = 123.0
_write_trajectory(tmp_path, original_poses, np.ones((1, 4), dtype=np.float32))
poses_path = tmp_path / "poses.npy"
original_load = _CAMERA_MODULE.np.load
mutation_count = 0
def mutate_source_then_decode(source, *args, **kwargs):
nonlocal mutation_count
if mutation_count == 0:
mutation_count += 1
np.save(poses_path, replacement_poses)
return original_load(source, *args, **kwargs)
monkeypatch.setattr(_CAMERA_MODULE.np, "load", mutate_source_then_decode)
trajectory = load_camera_trajectory(_trusted(tmp_path))
assert mutation_count == 1
torch.testing.assert_close(trajectory.poses, torch.from_numpy(original_poses).float())
def test_load_camera_trajectory_ignores_non_camera_event_files(tmp_path: Path) -> None:
poses = _identity_poses(1)
intrinsics = np.ones((1, 4), dtype=np.float32)
_write_trajectory(tmp_path, poses, intrinsics)
np.save(tmp_path / "wasd_events.npy", np.array([{"key": "w"}], dtype=object))
trajectory = load_camera_trajectory(_trusted(tmp_path))
torch.testing.assert_close(trajectory.poses, torch.from_numpy(poses).float())
def test_interpolate_camera_trajectory_uses_linear_and_spherical_interpolation() -> None:
poses = torch.eye(4).repeat(2, 1, 1)
poses[1, :3, :3] = torch.tensor(
[
[0.0, -1.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 0.0, 1.0],
]
)
poses[1, :3, 3] = torch.tensor([2.0, 4.0, 6.0])
intrinsics = torch.tensor([[10.0, 20.0, 30.0, 40.0], [20.0, 40.0, 60.0, 80.0]])
trajectory = CameraTrajectory(poses=poses, intrinsics=intrinsics)
result = interpolate_camera_trajectory(trajectory, 3)
torch.testing.assert_close(result.poses[0], poses[0])
torch.testing.assert_close(result.poses[-1], poses[-1])
torch.testing.assert_close(result.poses[1, :3, 3], torch.tensor([1.0, 2.0, 3.0]))
expected_mid_rotation = torch.tensor(
[
[math.sqrt(0.5), -math.sqrt(0.5), 0.0],
[math.sqrt(0.5), math.sqrt(0.5), 0.0],
[0.0, 0.0, 1.0],
]
)
torch.testing.assert_close(result.poses[1, :3, :3], expected_mid_rotation)
torch.testing.assert_close(result.intrinsics[1], torch.tensor([15.0, 30.0, 45.0, 60.0]))
def test_build_plucker_embedding_has_forward_center_ray() -> None:
trajectory = _trajectory(torch.eye(4).unsqueeze(0))
result = build_plucker_embedding(
trajectory,
height=3,
width=3,
target_height=3,
target_width=3,
device=torch.device("cpu"),
dtype=torch.float32,
)
torch.testing.assert_close(result[0, :3, 1, 1], torch.zeros(3))
torch.testing.assert_close(result[0, 3:, 1, 1], torch.tensor([0.0, 0.0, 1.0]))
def test_build_plucker_embedding_scales_reference_intrinsics() -> None:
trajectory = _trajectory(
torch.eye(4).unsqueeze(0),
torch.tensor([[416.0, 240.0, 416.0, 240.0]]),
)
result = build_plucker_embedding(
trajectory,
height=4,
width=8,
target_height=4,
target_width=8,
device=torch.device("cpu"),
dtype=torch.float32,
)
expected_corner_direction = torch.tensor([-0.875, -0.75, 1.0])
expected_corner_direction /= torch.linalg.vector_norm(expected_corner_direction)
torch.testing.assert_close(result[0, 3:, 0, 0], expected_corner_direction)
def test_build_plucker_embedding_samples_target_pixel_centers() -> None:
trajectory = _trajectory(
torch.eye(4).unsqueeze(0),
torch.tensor([[416.0, 240.0, 416.0, 240.0]]),
)
result = build_plucker_embedding(
trajectory,
height=6,
width=10,
target_height=2,
target_width=5,
device=torch.device("cpu"),
dtype=torch.float32,
)
# Target centers map through the source scale: (0.5 * 2, 0.5 * 3).
expected_direction = torch.tensor([-0.8, -0.5, 1.0])
expected_direction /= torch.linalg.vector_norm(expected_direction)
torch.testing.assert_close(result[0, 3:, 0, 0], expected_direction)
def test_build_plucker_embedding_uses_framewise_deltas_for_rotations_and_translations() -> None:
poses = torch.eye(4).repeat(3, 1, 1)
poses[0, :3, 3] = torch.tensor([5.0, 7.0, 11.0])
poses[1, :3, :3] = torch.tensor(
[
[0.0, -1.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 0.0, 1.0],
]
)
poses[1, :3, 3] = torch.tensor([5.0, 9.0, 11.0])
poses[2, :3, :3] = torch.tensor(
[
[-1.0, 0.0, 0.0],
[0.0, -1.0, 0.0],
[0.0, 0.0, 1.0],
]
)
poses[2, :3, 3] = torch.tensor([3.0, 9.0, 11.0])
trajectory = _trajectory(
poses,
torch.tensor([[416.0, 480.0, 208.0, 240.0]]).repeat(3, 1),
)
result = build_plucker_embedding(
trajectory,
height=1,
width=2,
target_height=1,
target_width=2,
device=torch.device("cpu"),
dtype=torch.float32,
)
expected_direction = torch.tensor([0.0, 1.0, 1.0]) / math.sqrt(2.0)
torch.testing.assert_close(result[0, :3, 0, 1], torch.zeros(3))
torch.testing.assert_close(result[1, :3, 0, 1], torch.tensor([0.0, 1.0, 0.0]))
torch.testing.assert_close(result[2, :3, 0, 1], torch.tensor([0.0, 1.0, 0.0]))
torch.testing.assert_close(result[1, 3:, 0, 1], expected_direction)
torch.testing.assert_close(result[2, 3:, 0, 1], expected_direction)
def test_build_plucker_embedding_normalizes_framewise_translation_by_max_norm() -> None:
poses = torch.eye(4).repeat(3, 1, 1)
poses[:, :3, 3] = torch.tensor(
[
[10.0, -2.0, 5.0],
[13.0, 2.0, 5.0],
[19.0, 10.0, 5.0],
]
)
result = build_plucker_embedding(
_trajectory(poses),
height=1,
width=1,
target_height=1,
target_width=1,
device=torch.device("cpu"),
dtype=torch.float32,
)
expected_origins = torch.tensor([[0.0, 0.0, 0.0], [0.3, 0.4, 0.0], [0.6, 0.8, 0.0]])
torch.testing.assert_close(result[:, :3, 0, 0], expected_origins)
def test_build_plucker_embedding_interpolates_raw_poses_before_framewise_conversion() -> None:
poses = torch.eye(4).repeat(2, 1, 1)
poses[0, :3, 3] = torch.tensor([20.0, 4.0, -2.0])
poses[1, :3, 3] = torch.tensor([26.0, 12.0, -2.0])
raw = _trajectory(poses)
interpolated = interpolate_camera_trajectory(raw, 3)
result = build_plucker_embedding(
interpolated,
height=1,
width=1,
target_height=1,
target_width=1,
device=torch.device("cpu"),
dtype=torch.float32,
)
expected_delta = torch.tensor([0.6, 0.8, 0.0])
torch.testing.assert_close(result[0, :3, 0, 0], torch.zeros(3))
torch.testing.assert_close(result[1, :3, 0, 0], expected_delta)
torch.testing.assert_close(result[2, :3, 0, 0], expected_delta)
def test_build_plucker_embedding_uses_half_pixel_centers_and_origin_direction_channel_order() -> None:
poses = torch.eye(4).repeat(2, 1, 1)
poses[1, :3, 3] = torch.tensor([2.0, 0.0, 0.0])
trajectory = _trajectory(
poses,
torch.tensor([[832.0, 480.0, 0.0, 0.0]]).repeat(2, 1),
)
result = build_plucker_embedding(
trajectory,
height=1,
width=2,
target_height=1,
target_width=2,
device=torch.device("cpu"),
dtype=torch.float32,
)
expected_direction = torch.tensor([0.25, 0.5, 1.0])
expected_direction /= torch.linalg.vector_norm(expected_direction)
torch.testing.assert_close(result[1, :3, 0, 0], torch.tensor([1.0, 0.0, 0.0]))
torch.testing.assert_close(result[1, 3:, 0, 0], expected_direction)
def test_build_plucker_embedding_honors_shape_dtype_and_device() -> None:
trajectory = _trajectory(torch.eye(4).repeat(2, 1, 1))
result = build_plucker_embedding(
trajectory,
height=12,
width=20,
target_height=3,
target_width=5,
device=torch.device("cpu"),
dtype=torch.float64,
)
assert result.shape == (2, 6, 3, 5)
assert result.dtype == torch.float64
assert result.device == torch.device("cpu")
def test_build_plucker_embedding_is_deterministic() -> None:
trajectory = _trajectory(torch.eye(4).repeat(2, 1, 1))
kwargs = {
"height": 12,
"width": 20,
"target_height": 3,
"target_width": 5,
"device": torch.device("cpu"),
"dtype": torch.float32,
}
first = build_plucker_embedding(trajectory, **kwargs)
second = build_plucker_embedding(trajectory, **kwargs)
torch.testing.assert_close(first, second, rtol=0.0, atol=0.0)
@@ -0,0 +1,685 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
import hashlib
import inspect
import json
import math
from pathlib import Path
import pytest
import torch
from tests.diffusion.models.lingbot_world import test_lingbot_world_attention as attention_tests
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
_FIXTURE_PATH = Path(__file__).with_name("fixtures") / "lingbot_world_weight_index_names.json.fixture"
_SHAPE_FIXTURE_PATH = Path(__file__).with_name("fixtures") / "lingbot_world_official_shapes.json.fixture"
@pytest.fixture(autouse=True)
def _inference_context():
with torch.inference_mode():
yield
def _tiny_model(
module,
*,
num_layers: int = 2,
num_frames_per_block: int = 1,
sliding_window_num_frames: int = 3,
):
return module.CausalLingBotWorldTransformer3DModel(
patch_size=(1, 2, 2),
num_attention_heads=2,
attention_head_dim=2,
in_channels=36,
out_channels=2,
text_dim=6,
freq_dim=4,
ffn_dim=8,
num_layers=num_layers,
cross_attn_norm=True,
eps=1e-6,
rope_max_seq_len=16,
sink_size=1,
num_frames_per_block=num_frames_per_block,
sliding_window_num_frames=sliding_window_num_frames,
local_attn_size=-1,
)
def _cache(module, model, *, max_tokens: int | None = None):
if max_tokens is None:
window_frames = (
model.config.local_attn_size
if model.config.local_attn_size != -1
else model.config.sliding_window_num_frames
)
max_tokens = window_frames * 2 * 2
return module.allocate_lingbot_cache(
batch_size=1,
num_layers=len(model.blocks),
max_tokens=max_tokens,
num_local_heads=2,
head_dim=2,
device=torch.device("cpu"),
dtype=torch.float32,
)
def _checkpoint_parameter_specs(model) -> dict[str, tuple[torch.Size, torch.dtype]]:
specs: dict[str, tuple[torch.Size, torch.dtype]] = {}
for name, parameter in model.named_parameters():
marker = ".self_attn.qkv."
if marker not in name:
specs[name] = (parameter.shape, parameter.dtype)
continue
shard_shape = torch.Size((parameter.shape[0] // 3, *parameter.shape[1:]))
for projection in ("q", "k", "v"):
specs[name.replace(marker, f".self_attn.{projection}.")] = (shard_shape, parameter.dtype)
return specs
def _checkpoint_weights_for_model(model) -> tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]:
checkpoint_weights: dict[str, torch.Tensor] = {}
expected_parameters: dict[str, torch.Tensor] = {}
next_value = 1
for name, parameter in model.named_parameters():
marker = ".self_attn.qkv."
if marker not in name:
value = torch.full_like(parameter, next_value)
checkpoint_weights[name] = value
expected_parameters[name] = value
next_value += 1
continue
shards = []
shard_shape = (parameter.shape[0] // 3, *parameter.shape[1:])
for projection in ("q", "k", "v"):
value = torch.full(shard_shape, next_value, dtype=parameter.dtype, device=parameter.device)
checkpoint_weights[name.replace(marker, f".self_attn.{projection}.")] = value
shards.append(value)
next_value += 1
expected_parameters[name] = torch.cat(shards)
return checkpoint_weights, expected_parameters
def _self_cache_snapshot(cache) -> list[tuple[torch.Tensor, torch.Tensor, tuple[int, int, int | None, int]]]:
return [attention_tests._cache_snapshot(layer_cache) for layer_cache in cache.self_attention]
def _assert_self_cache_unchanged(cache, snapshot) -> None:
for layer_cache, layer_snapshot in zip(cache.self_attention, snapshot, strict=True):
attention_tests._assert_cache_unchanged(layer_cache, layer_snapshot)
def test_tiny_transformer_runs_four_chunks_with_explicit_cache_commit_and_camera_path() -> None:
torch.manual_seed(7)
module = attention_tests._load_module()
model = _tiny_model(module).eval()
cache = _cache(module, model)
encoder_hidden_states = torch.randn(1, 3, 6)
camera_hidden_states = torch.randn(1, 6 * 8 * 8, 1, 4, 4)
cross_key_outputs = [attention_tests._record_outputs(block.cross_attn.k) for block in model.blocks]
text_projection_outputs = attention_tests._record_outputs(model.text_embedding)
outputs = []
assert model.patch_embedding.in_channels == 36
assert model.patch_embedding_wancamctrl.in_features == 6 * 8 * 8 * 1 * 2 * 2
for start_frame in range(4):
hidden_states = torch.randn(1, 36, 1, 4, 4)
timestep = torch.tensor([float(start_frame + 1)])
snapshot = _self_cache_snapshot(cache)
transient_output = model(
hidden_states,
timestep,
encoder_hidden_states,
camera_hidden_states,
cache=cache,
start_frame=start_frame,
update_cache=False,
)
assert transient_output.shape == (1, 2, 1, 4, 4)
_assert_self_cache_unchanged(cache, snapshot)
assert all(layer_cache is not None for layer_cache in cache.cross_attention)
committed_output = model(
hidden_states,
timestep,
encoder_hidden_states,
camera_hidden_states,
cache=cache,
start_frame=start_frame,
update_cache=True,
)
torch.testing.assert_close(committed_output, transient_output)
outputs.append(committed_output)
expected_token_end = (start_frame + 1) * 2 * 2
assert all(layer_cache.absolute_end == expected_token_end for layer_cache in cache.self_attention)
assert len(outputs) == 4
assert all(len(outputs) == 1 for outputs in cross_key_outputs)
assert len(text_projection_outputs) == 1
alternate_cache = _cache(module, model)
alternate_output = model(
torch.zeros(1, 36, 1, 4, 4),
torch.tensor([1.0]),
encoder_hidden_states,
torch.zeros_like(camera_hidden_states),
cache=alternate_cache,
start_frame=0,
update_cache=False,
)
camera_output = model(
torch.zeros(1, 36, 1, 4, 4),
torch.tensor([1.0]),
encoder_hidden_states,
torch.ones_like(camera_hidden_states),
cache=_cache(module, model),
start_frame=0,
update_cache=False,
)
assert not torch.equal(camera_output, alternate_output)
def test_partial_cross_attention_cache_projects_text_only_for_missing_layers() -> None:
module = attention_tests._load_module()
model = _tiny_model(module, num_layers=2).eval()
cache = _cache(module, model)
hidden_states = torch.randn(1, 36, 1, 4, 4)
timestep = torch.tensor([1.0])
encoder_hidden_states = torch.randn(1, 3, 6)
camera_hidden_states = torch.randn(1, 6 * 8 * 8, 1, 4, 4)
model(
hidden_states,
timestep,
encoder_hidden_states,
camera_hidden_states,
cache=cache,
start_frame=0,
update_cache=False,
)
cache.cross_attention[1] = None
text_projection_outputs = attention_tests._record_outputs(model.text_embedding)
cross_key_outputs = [attention_tests._record_outputs(block.cross_attn.k) for block in model.blocks]
model(
hidden_states,
timestep,
encoder_hidden_states,
camera_hidden_states,
cache=cache,
start_frame=0,
update_cache=False,
)
assert len(text_projection_outputs) == 1
assert [len(outputs) for outputs in cross_key_outputs] == [0, 1]
assert all(layer_cache is not None for layer_cache in cache.cross_attention)
@pytest.mark.parametrize("frames", [1, 6], ids=("partial", "multiple_blocks"))
def test_forward_rejects_chunks_that_do_not_equal_configured_block_size(frames: int) -> None:
module = attention_tests._load_module()
model = _tiny_model(
module,
num_layers=1,
num_frames_per_block=3,
sliding_window_num_frames=6,
)
cache = _cache(module, model)
snapshot = _self_cache_snapshot(cache)
patch_inputs = attention_tests._record_inputs(model.patch_embedding)
with pytest.raises(ValueError, match="exactly 3 post-patch frames"):
model(
torch.randn(1, 36, frames, 4, 4),
torch.tensor([1.0]),
torch.randn(1, 3, 6),
torch.randn(1, 6 * 8 * 8, frames, 4, 4),
cache=cache,
start_frame=0,
update_cache=True,
)
assert patch_inputs == []
_assert_self_cache_unchanged(cache, snapshot)
assert cache.cross_attention == [None]
def test_forward_accepts_exactly_one_configured_frame_block() -> None:
module = attention_tests._load_module()
model = _tiny_model(
module,
num_layers=1,
num_frames_per_block=3,
sliding_window_num_frames=6,
)
output = model(
torch.randn(1, 36, 3, 4, 4),
torch.tensor([1.0]),
torch.randn(1, 3, 6),
torch.randn(1, 6 * 8 * 8, 3, 4, 4),
cache=_cache(module, model),
start_frame=0,
update_cache=False,
)
assert output.shape == (1, 2, 3, 4, 4)
def test_transformer_allocates_request_cache_from_its_configured_geometry() -> None:
module = attention_tests._load_module()
model = _tiny_model(
module,
num_layers=2,
num_frames_per_block=3,
sliding_window_num_frames=6,
)
cache = model.allocate_cache(
batch_size=2,
latent_height=8,
latent_width=12,
device=torch.device("cpu"),
dtype=torch.float32,
)
assert len(cache.self_attention) == 2
assert len(cache.cross_attention) == 2
assert cache.cross_attention == [None, None]
assert all(layer.key.shape == (2, 6 * 4 * 6, 2, 2) for layer in cache.self_attention)
assert all(layer.value.shape == (2, 6 * 4 * 6, 2, 2) for layer in cache.self_attention)
def test_video_patch_embedding_uses_temporal_height_width_token_order() -> None:
module = attention_tests._load_module()
model = _tiny_model(module, num_layers=1, num_frames_per_block=2)
hidden_states = torch.zeros(1, 36, 2, 2, 4)
hidden_states[0, 0] = torch.tensor(
[
[[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]],
[[9.0, 10.0, 11.0, 12.0], [13.0, 14.0, 15.0, 16.0]],
]
)
with torch.no_grad():
model.patch_embedding.weight.zero_()
model.patch_embedding.bias.zero_()
model.patch_embedding.weight[0, 0, 0, 0, 0] = 1
tokens = model.patch_embedding(hidden_states).flatten(2).transpose(1, 2)
torch.testing.assert_close(tokens[0, :, 0], torch.tensor([1.0, 3.0, 9.0, 11.0]))
def test_unpatchify_restores_two_frame_channel_and_spatial_order() -> None:
module = attention_tests._load_module()
model = _tiny_model(module, num_layers=1, num_frames_per_block=2)
hidden_states = (
torch.stack(
(
torch.arange(0, 8),
torch.arange(10, 18),
torch.arange(20, 28),
torch.arange(30, 38),
)
)
.unsqueeze(0)
.float()
)
expected = torch.tensor(
[
[
[[0, 2, 10, 12], [4, 6, 14, 16]],
[[20, 22, 30, 32], [24, 26, 34, 36]],
],
[
[[1, 3, 11, 13], [5, 7, 15, 17]],
[[21, 23, 31, 33], [25, 27, 35, 37]],
],
],
dtype=torch.float32,
).unsqueeze(0)
output = model._unpatchify(
hidden_states,
batch_size=1,
frames=2,
height=1,
width=2,
)
torch.testing.assert_close(output, expected)
def test_head_modulation_broadcasts_distinct_condition_per_frame() -> None:
module = attention_tests._load_module()
model = _tiny_model(module, num_layers=1, num_frames_per_block=2)
model.head.norm = torch.nn.Identity()
with torch.no_grad():
model.head.modulation.zero_()
model.head.head.weight.zero_()
model.head.head.bias.zero_()
model.head.head.weight[:, 0] = 1
hidden_states = torch.ones(1, 4, model.dim)
timestep_embedding = torch.tensor([[[10.0, 0.0, 0.0, 0.0], [20.0, 0.0, 0.0, 0.0]]])
expected = torch.tensor([21.0, 21.0, 41.0, 41.0]).view(1, 4, 1).expand(1, 4, 8)
output = model.head(hidden_states, timestep_embedding)
torch.testing.assert_close(output, expected)
def test_constructor_defaults_match_official_checkpoint_config() -> None:
module = attention_tests._load_module()
parameters = inspect.signature(module.CausalLingBotWorldTransformer3DModel.__init__).parameters
expected = {
"in_channels": 36,
"out_channels": 16,
"num_layers": 40,
"num_attention_heads": 40,
"attention_head_dim": 128,
"patch_size": (1, 2, 2),
"text_dim": 4096,
"ffn_dim": 13824,
"freq_dim": 256,
"rope_max_seq_len": 1024,
"eps": 1e-6,
"cross_attn_norm": True,
"sink_size": 9,
"num_frames_per_block": 3,
"sliding_window_num_frames": 18,
"local_attn_size": -1,
}
assert {name: parameters[name].default for name in expected} == expected
def test_transformer_exposes_parameter_dtype_for_pipeline_runtime() -> None:
module = attention_tests._load_module()
model = _tiny_model(module)
assert model.dtype == next(model.parameters()).dtype
def test_transformer_declares_regional_compile_block() -> None:
module = attention_tests._load_module()
assert module.CausalLingBotWorldTransformer3DModel._repeated_blocks == ["LingBotAttentionBlock"]
def test_lingbot_rms_norm_uses_global_tp_square_mean(monkeypatch) -> None:
module = attention_tests._load_module()
reduced_values: list[torch.Tensor] = []
monkeypatch.setattr(module, "get_tensor_model_parallel_world_size", lambda: 2)
def all_reduce(value: torch.Tensor) -> torch.Tensor:
reduced_values.append(value.detach().clone())
return value * 2
monkeypatch.setattr(module, "tensor_model_parallel_all_reduce", all_reduce)
norm = module._LingBotRMSNorm(hidden_size=2, eps=0.0)
value = torch.tensor([[3.0, 4.0]])
output = norm(value)
torch.testing.assert_close(output, value / math.sqrt(12.5))
assert len(reduced_values) == 1
torch.testing.assert_close(reduced_values[0], torch.tensor([[25.0]]))
def test_attention_rejects_heads_not_divisible_by_tp_size(monkeypatch) -> None:
module = attention_tests._load_module()
monkeypatch.setattr(module, "get_tensor_model_parallel_world_size", lambda: 3)
with pytest.raises(ValueError, match="num_heads=2.*tp_size=3"):
module.LingBotSelfAttention(dim=4, num_heads=2)
def test_constructor_rejects_unsupported_qk_norm_with_config_error() -> None:
module = attention_tests._load_module()
with pytest.raises(ValueError, match="qk_norm.*rms_norm_across_heads"):
module.CausalLingBotWorldTransformer3DModel(qk_norm="layer_norm")
@pytest.mark.parametrize("field", ["image_dim", "added_kv_proj_dim", "pos_embed_seq_len"])
def test_constructor_rejects_non_null_image_embedding_fields(field: str) -> None:
module = attention_tests._load_module()
with pytest.raises(ValueError, match=field):
module.CausalLingBotWorldTransformer3DModel(**{field: 4})
def test_constructor_rejects_unsupported_quantization_with_runtime_error() -> None:
module = attention_tests._load_module()
with pytest.raises(RuntimeError, match="quant_config.*not supported"):
module.CausalLingBotWorldTransformer3DModel(quant_config=object())
def test_from_config_accepts_diffusers_metadata_and_normalizes_patch_size() -> None:
module = attention_tests._load_module()
class ConfigProbe(module.CausalLingBotWorldTransformer3DModel):
def __init__(self, **kwargs) -> None:
self.received_kwargs = kwargs
config = {
"_class_name": "CausalLingBotWorldTransformer3DModel",
"_diffusers_version": "0.35.0.dev0",
"patch_size": [1, 2, 2],
"num_attention_heads": 40,
"attention_head_dim": 128,
"in_channels": 36,
"out_channels": 16,
"text_dim": 4096,
"freq_dim": 256,
"ffn_dim": 13824,
"num_layers": 40,
"cross_attn_norm": True,
"eps": 1e-6,
"image_dim": None,
"added_kv_proj_dim": None,
"rope_max_seq_len": 1024,
"pos_embed_seq_len": None,
"qk_norm": "rms_norm_across_heads",
"sink_size": 9,
"num_frames_per_block": 3,
"sliding_window_num_frames": 18,
"local_attn_size": -1,
}
model = ConfigProbe.from_config(config, prefix="transformer")
assert model.received_kwargs["patch_size"] == (1, 2, 2)
assert model.received_kwargs["num_layers"] == 40
assert model.received_kwargs["prefix"] == "transformer"
@pytest.mark.parametrize(
("field", "value"),
[
("in_channels", 35),
("num_layers", 39),
("num_attention_heads", 32),
("attention_head_dim", 64),
("ffn_dim", 10240),
("num_frames_per_block", 4),
("sliding_window_num_frames", 24),
("sink_size", 0),
],
)
def test_from_config_rejects_checkpoint_topology_drift(field: str, value: object) -> None:
module = attention_tests._load_module()
config = {
"_class_name": "CausalLingBotWorldTransformer3DModel",
"_diffusers_version": "0.35.0.dev0",
"patch_size": [1, 2, 2],
"num_attention_heads": 40,
"attention_head_dim": 128,
"in_channels": 36,
"out_channels": 16,
"text_dim": 4096,
"freq_dim": 256,
"ffn_dim": 13824,
"num_layers": 40,
"cross_attn_norm": True,
"eps": 1e-6,
"image_dim": None,
"added_kv_proj_dim": None,
"rope_max_seq_len": 1024,
"pos_embed_seq_len": None,
"qk_norm": "rms_norm_across_heads",
"sink_size": 9,
"num_frames_per_block": 3,
"sliding_window_num_frames": 18,
"local_attn_size": -1,
}
config[field] = value
with pytest.raises(ValueError, match=field):
module.CausalLingBotWorldTransformer3DModel.from_config(config)
def test_from_config_ignores_non_semantic_checkpoint_metadata() -> None:
module = attention_tests._load_module()
config = {
"_class_name": "CausalLingBotWorldTransformer3DModel",
"architectures": ["CausalLingBotWorldTransformer3DModel"],
"future_diffusers_metadata": {"version": 1},
}
with torch.device("meta"):
model = module.CausalLingBotWorldTransformer3DModel.from_config(config)
assert model.config.in_channels == 36
assert model.config.num_layers == 40
def test_load_weights_uses_parameter_loaders_and_rejects_unknown_model_keys() -> None:
module = attention_tests._load_module()
model = _tiny_model(module, num_layers=1)
checkpoint_weights, expected_parameters = _checkpoint_weights_for_model(model)
loader_calls: list[str] = []
qkv_weight = model.blocks[0].self_attn.qkv.weight
def record_loader(param: torch.Tensor, loaded_weight: torch.Tensor, shard_id: str) -> None:
loader_calls.append(shard_id)
shard_index = {"q": 0, "k": 1, "v": 2}[shard_id]
shard_size = param.shape[0] // 3
param.data.narrow(0, shard_index * shard_size, shard_size).copy_(loaded_weight)
qkv_weight.weight_loader = record_loader
loaded = model.load_weights(iter(checkpoint_weights.items()))
assert set(checkpoint_weights) <= loaded
assert set(dict(model.named_parameters())) <= loaded
assert loader_calls == ["q", "k", "v"]
for name, param in model.named_parameters():
torch.testing.assert_close(param, expected_parameters[name])
first_name = next(iter(checkpoint_weights))
partial = _tiny_model(module, num_layers=1)
partial_loaded = partial.load_weights([(first_name, checkpoint_weights[first_name])])
expected_partial = {first_name}
for projection in ("q", "k", "v"):
marker = f".self_attn.{projection}."
if marker in first_name:
expected_partial.add(first_name.replace(marker, ".self_attn.qkv."))
break
assert partial_loaded == expected_partial
with pytest.raises(KeyError, match="unexpected_model.weight"):
model.load_weights([("unexpected_model.weight", torch.ones(1))])
def test_load_weights_consumes_checkpoint_iterator_incrementally() -> None:
module = attention_tests._load_module()
model = _tiny_model(module, num_layers=1)
first_name, first_param = next(iter(model.named_parameters()))
loaded_weight = torch.full_like(first_param, 17)
def weights():
yield first_name, loaded_weight
torch.testing.assert_close(first_param, loaded_weight)
assert model.load_weights(weights()) == {first_name}
def test_real_auto_weights_loader_covers_fused_model_parameter_namespace() -> None:
utils = pytest.importorskip("vllm.model_executor.models.utils")
module = attention_tests._load_module()
transformer = _tiny_model(module, num_layers=1)
checkpoint_weights, _ = _checkpoint_weights_for_model(transformer)
class Pipeline(torch.nn.Module):
def __init__(self) -> None:
super().__init__()
self.transformer = transformer
pipeline = Pipeline()
loaded = utils.AutoWeightsLoader(pipeline).load_weights(
(f"transformer.{name}", value) for name, value in checkpoint_weights.items()
)
assert set(dict(pipeline.named_parameters())) <= loaded
def test_checkpoint_weight_index_fixture_matches_model_namespaces() -> None:
module = attention_tests._load_module()
fixture = json.loads(_FIXTURE_PATH.read_text())
model = _tiny_model(module, num_layers=1)
parameter_names = set(_checkpoint_parameter_specs(model))
assert {name.split(".", 1)[0] for name in parameter_names} == set(fixture["top_level_modules"])
assert {name for name in parameter_names if not name.startswith("blocks.")} == set(fixture["non_block_parameters"])
assert {name.removeprefix("blocks.0.") for name in parameter_names if name.startswith("blocks.0.")} == set(
fixture["block_parameter_suffixes"]
)
expanded_names = set(fixture["non_block_parameters"])
expanded_names.update(
f"blocks.{layer}.{suffix}" for layer in range(40) for suffix in fixture["block_parameter_suffixes"]
)
canonical_names = "\n".join(sorted(expanded_names))
assert len(expanded_names) == fixture["index_parameter_count"] == 1421
assert hashlib.sha256(canonical_names.encode()).hexdigest() == fixture["canonical_name_set_sha256"]
assert fixture["canonical_name_to_shard_sha256"] == (
"e1addc27d2b1fad6f10226a23a265ee3e3c61e74dce4d5859727bdf180a67992"
)
assert list(fixture["shards"]) == [
f"diffusion_pytorch_model-{index:05}-of-00008.safetensors" for index in range(1, 9)
]
assert sum(shard["parameter_count"] for shard in fixture["shards"].values()) == 1421
assert all(len(shard["parameter_names_sha256"]) == 64 for shard in fixture["shards"].values())
def test_official_default_shapes_match_public_safetensors_header_fixture() -> None:
module = attention_tests._load_module()
fixture = json.loads(_SHAPE_FIXTURE_PATH.read_text())
with torch.device("meta"):
model = module.CausalLingBotWorldTransformer3DModel()
parameter_specs = _checkpoint_parameter_specs(model)
expected_dtype = {"F32": torch.float32}[fixture["dtype"]]
assert len(fixture["shard_representatives"]) == 8
assert set(fixture["shard_representatives"].values()) <= set(fixture["parameter_shapes"])
for name, expected_shape in fixture["parameter_shapes"].items():
shape, dtype = parameter_specs[name]
assert tuple(shape) == tuple(expected_shape), name
assert dtype == expected_dtype
@@ -0,0 +1,39 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from __future__ import annotations
import ast
from pathlib import Path
import pytest
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
_ROOT = Path(__file__).parents[4]
_PRODUCTION_FILES = (
_ROOT / "vllm_omni/diffusion/models/lingbot_world/camera.py",
_ROOT / "vllm_omni/diffusion/models/lingbot_world/transformer.py",
_ROOT / "vllm_omni/diffusion/models/lingbot_world/pipeline.py",
)
def test_all_lingbot_production_functions_have_complete_annotations() -> None:
missing: list[str] = []
for path in _PRODUCTION_FILES:
tree = ast.parse(path.read_text())
for node in ast.walk(tree):
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
arguments = (*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs)
for argument in arguments:
if argument.arg not in {"self", "cls"} and argument.annotation is None:
missing.append(f"{path.name}:{node.lineno}:{node.name}:{argument.arg}")
if node.args.vararg is not None and node.args.vararg.annotation is None:
missing.append(f"{path.name}:{node.lineno}:{node.name}:*{node.args.vararg.arg}")
if node.args.kwarg is not None and node.args.kwarg.annotation is None:
missing.append(f"{path.name}:{node.lineno}:{node.name}:**{node.args.kwarg.arg}")
if node.returns is None:
missing.append(f"{path.name}:{node.lineno}:{node.name}:return")
assert missing == []
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,84 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""End-to-end smoke test for LingBot-World v2 generation."""
from __future__ import annotations
import os
from pathlib import Path
import numpy as np
import pytest
from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniRunnerHandler
from vllm_omni.diffusion.data import DiffusionParallelConfig
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
MODEL = os.environ.get(
"VLLM_OMNI_LINGBOT_WORLD_V2_CHECKPOINT_PATH",
"robbyant/lingbot-world-v2-14b-causal-fast-diffusers",
)
_IMAGE_PATH = os.environ.get("VLLM_OMNI_LINGBOT_WORLD_V2_IMAGE_PATH")
_ACTION_DIR_VALUE = os.environ.get("VLLM_OMNI_LINGBOT_WORLD_V2_ACTION_DIR")
_ACTION_DIR = Path(_ACTION_DIR_VALUE).expanduser().resolve() if _ACTION_DIR_VALUE else None
_HAS_INPUT_ASSETS = _IMAGE_PATH is not None and _ACTION_DIR is not None
_OMNI_RUNNER_PARAM = (
MODEL,
None,
{
"flow_shift": 5.0,
"parallel_config": DiffusionParallelConfig(tensor_parallel_size=1),
"model_config": {
"lingbot_action_root": str(_ACTION_DIR.parent) if _ACTION_DIR is not None else "",
},
},
)
pytestmark = [
pytest.mark.slow,
pytest.mark.diffusion,
pytest.mark.skipif(
not _HAS_INPUT_ASSETS,
reason=("VLLM_OMNI_LINGBOT_WORLD_V2_IMAGE_PATH and VLLM_OMNI_LINGBOT_WORLD_V2_ACTION_DIR are required"),
),
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
]
@hardware_test(res={"cuda": "H100"}, num_cards=1)
def test_lingbot_world_v2_tp1_one_block(omni_runner_handler: OmniRunnerHandler) -> None:
"""Load the real checkpoint and generate one causal block on one GPU."""
assert _IMAGE_PATH is not None
assert _ACTION_DIR is not None
response = omni_runner_handler.send_diffusion_request(
{
"model": omni_runner_handler.runner.model_name,
"prompt": "The camera moves slowly forward through the scene.",
"multi_modal_data": {"image": str(Path(_IMAGE_PATH).expanduser().resolve())},
"sampling_params": OmniDiffusionSamplingParams(
height=480,
width=832,
num_frames=9,
num_inference_steps=4,
max_sequence_length=512,
seed=42,
fps=16,
extra_args={
"action_path": _ACTION_DIR.name,
"flow_shift": 5.0,
},
),
}
)
assert response.success
assert response.images is not None
assert len(response.images) == 1
video = np.asarray(response.images[0])
assert video.shape == (1, 9, 480, 832, 3)
assert np.isfinite(video).all()
assert float(np.ptp(video)) > 1e-6
assert float(np.mean(np.abs(np.diff(video, axis=1)))) > 1e-6
+4 -2
View File
@@ -417,8 +417,9 @@ class TestTryResolveOmniModelType:
class TestLoadAndResolveStageConfigs:
def test_load_and_resolve_with_kwargs(self, mocker: MockerFixture):
"""Ensure that dtype survives default stage creation."""
kwargs = {"dtype": torch.float32}
"""Ensure that kwargs survive default stage creation."""
engine_backend = "vllm_omni.experimental.ar_diffusion.engine.ARDiffusionEngine"
kwargs = {"dtype": torch.float32, "engine_backend": engine_backend}
mocker.patch("vllm_omni.entrypoints.utils.resolve_model_config_path", return_value=None)
mocker.patch("vllm_omni.entrypoints.utils.load_stage_configs_from_model", return_value=([], None))
@@ -431,6 +432,7 @@ class TestLoadAndResolveStageConfigs:
assert config_path is None
assert len(stage_configs) == 1
assert "dtype" in stage_configs[0]["engine_args"]
assert stage_configs[0]["engine_args"]["engine_backend"] == engine_backend
def test_deploy_config_preserves_cli_overrides_and_replicas(self, tmp_path, mocker: MockerFixture):
deploy_path = tmp_path / "qwen3_multi.yaml"
@@ -64,6 +64,9 @@ EXCLUDED_MODELS = [
"HunyuanVideo15Pipeline",
"HunyuanVideo15ImageToVideoPipeline",
"LingBotVideoPipeline",
# Requires camera/action assets and persistent AR state; covered by its
# dedicated contract tests and real-checkpoint E2E smoke instead.
"LingBotWorldCausalDMDPipeline",
"MagiHumanPipeline",
"OmniVoicePipeline",
"OmniVoice",
@@ -70,6 +70,9 @@ from vllm_omni.inputs.data import OmniDiffusionSamplingParams
logger = logging.getLogger(__name__)
MAX_DREAMZERO_SESSIONS = 64
# Shipped DreamZero geometry retains 24 Wan VAE causal-convolution cache
# entries. This is the measured persistent CUDA upper bound per live session.
DREAMZERO_MODEL_OWNED_STATE_BYTES_PER_SESSION = 603 * 1024 * 1024
# The pipeline's per-session state is a bespoke ``DreamZeroState`` by default, or
# a ``DreamZeroStateAdapter`` view when the opt-in session manager is enabled.
@@ -149,6 +152,7 @@ class DreamZeroPipeline(nn.Module, CFGParallelMixin):
session_capacity=MAX_DREAMZERO_SESSIONS,
cross_attention=tuple(cross_attention),
max_scratch_tokens_per_branch=int(transformer.num_action_per_block + transformer.num_state_per_block),
model_owned_state_bytes_per_session=DREAMZERO_MODEL_OWNED_STATE_BYTES_PER_SESSION,
)
@contextmanager
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from .actions import LingBotCameraControlReducer
from .pipeline import (
LingBotWorldCausalDMDPipeline,
get_lingbot_world_post_process_func,
get_lingbot_world_pre_process_func,
)
from .transformer import CausalLingBotWorldTransformer3DModel
__all__ = [
"CausalLingBotWorldTransformer3DModel",
"LingBotCameraControlReducer",
"LingBotWorldCausalDMDPipeline",
"get_lingbot_world_post_process_func",
"get_lingbot_world_pre_process_func",
]
@@ -0,0 +1,302 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Realtime keyboard controls for LingBot World 2.0."""
from __future__ import annotations
import math
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import Any
import numpy as np
import torch
from vllm_omni.diffusion.models.lingbot_world.camera import (
CameraTrajectory,
)
from vllm_omni.experimental.ar_diffusion.session import (
ARDiffusionPreparedControls,
ARDiffusionSessionEvent,
)
from vllm_omni.experimental.ar_diffusion.tick_protocol import (
ARDiffusionControlInput,
)
LINGBOT_CAMERA_ACTION_SCHEMA = "lingbot.camera_actions.v1"
LINGBOT_CAMERA_TRAJECTORY_SCHEMA = "lingbot.camera_trajectory.v1"
_ACTION_ORDER = ("w", "a", "s", "d", "i", "j", "k", "l")
_VALID_ACTIONS = frozenset(_ACTION_ORDER)
_REFERENCE_HEIGHT = 480
_REFERENCE_WIDTH = 832
def _normalize_actions(value: object, *, field: str) -> tuple[str, ...]:
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
raise ValueError(f"{field} must be a sequence of LingBot action keys.")
actions: set[str] = set()
for item in value:
if not isinstance(item, str) or item.lower() not in _VALID_ACTIONS:
raise ValueError(f"{field} supports only W/A/S/D/I/J/K/L action keys.")
actions.add(item.lower())
return tuple(action for action in _ACTION_ORDER if action in actions)
def _normalize_frames(value: object, *, field: str) -> tuple[tuple[str, ...], ...]:
if not isinstance(value, Sequence) or isinstance(value, (str, bytes)):
raise ValueError(f"{field} must be a sequence of per-frame action lists.")
return tuple(_normalize_actions(actions, field=f"{field}[{index}]") for index, actions in enumerate(value))
def parse_lingbot_camera_action_frames(
data: Mapping[str, Any],
*,
expected_frames: int,
) -> tuple[tuple[str, ...], ...]:
"""Validate the chunk-sized payload emitted by the session reducer."""
if data.get("mode") != "frames":
raise ValueError("lingbot.camera_actions.v1 model ticks require mode='frames'.")
frames = _normalize_frames(data.get("frames"), field="camera action frames")
if len(frames) != expected_frames:
raise ValueError(
"lingbot.camera_actions.v1 must contain exactly one action list per "
f"latent frame; expected {expected_frames}, got {len(frames)}."
)
return frames
@dataclass(frozen=True)
class _LingBotCameraReducerState:
mode: str | None = None
held_actions: tuple[str, ...] = ()
script_frames: tuple[tuple[str, ...], ...] = ()
class LingBotCameraControlReducer:
"""Sample live state/script controls into one LingBot AR block.
State mode preserves held keys across chunks and guarantees a one-frame
pulse when a press and release both arrive before the next chunk. Script
mode is a finite FIFO padded with neutral frames after exhaustion.
"""
def __init__(self, *, frames_per_block: int = 3) -> None:
if isinstance(frames_per_block, bool) or not isinstance(frames_per_block, int) or frames_per_block <= 0:
raise ValueError("frames_per_block must be a positive integer.")
self._frames_per_block = frames_per_block
self._state = _LingBotCameraReducerState()
@staticmethod
def _state_transitions(data: Mapping[str, Any]) -> tuple[tuple[str, ...], ...]:
transitions = data.get("transitions")
if not isinstance(transitions, Sequence) or isinstance(transitions, (str, bytes)) or not transitions:
raise ValueError("LingBot state-mode camera actions require non-empty transitions.")
normalized: list[tuple[str, ...]] = []
previous_timestamp: int | None = None
for index, transition in enumerate(transitions):
if not isinstance(transition, Mapping):
raise ValueError("Each LingBot camera state transition must be a mapping.")
timestamp = transition.get("client_ts_ms")
if timestamp is not None:
if isinstance(timestamp, bool) or not isinstance(timestamp, int) or timestamp < 0:
raise ValueError("client_ts_ms must be a non-negative integer when provided.")
if previous_timestamp is not None and timestamp < previous_timestamp:
raise ValueError("client_ts_ms values must be monotonic within one event.")
previous_timestamp = timestamp
normalized.append(
_normalize_actions(
transition.get("actions"),
field=f"camera state transitions[{index}].actions",
)
)
return tuple(normalized)
@staticmethod
def _script(data: Mapping[str, Any]) -> tuple[tuple[str, ...], ...]:
return _normalize_frames(data.get("frames"), field="camera action script frames")
def prepare(
self,
*,
current_controls: Mapping[str, ARDiffusionControlInput],
events: Sequence[ARDiffusionSessionEvent],
chunk_index: int,
) -> ARDiffusionPreparedControls:
del chunk_index
controls = {
track: control
for track, control in current_controls.items()
if not (track == "camera" and control.schema == LINGBOT_CAMERA_ACTION_SCHEMA)
}
state = self._state
pending_transitions: tuple[tuple[str, ...], ...] = ()
for event in events:
for control in event.controls:
if control.track != "camera" or control.schema != LINGBOT_CAMERA_ACTION_SCHEMA:
controls[control.track] = control
if control.track == "camera":
state = _LingBotCameraReducerState()
pending_transitions = ()
continue
mode = control.data.get("mode")
controls.pop("camera", None)
if mode == "script":
state = _LingBotCameraReducerState(
mode="script",
script_frames=self._script(control.data),
)
pending_transitions = ()
elif mode == "state":
transitions = self._state_transitions(control.data)
if state.mode != "state":
state = _LingBotCameraReducerState(mode="state")
else:
state = _LingBotCameraReducerState(
mode="state",
held_actions=state.held_actions,
)
pending_transitions += transitions
else:
raise ValueError("lingbot.camera_actions.v1 events require mode='state' or mode='script'.")
if state.mode == "script":
frames = state.script_frames[: self._frames_per_block]
frames += ((),) * (self._frames_per_block - len(frames))
state = _LingBotCameraReducerState(
mode="script",
script_frames=state.script_frames[self._frames_per_block :],
)
elif state.mode == "state":
final_actions = pending_transitions[-1] if pending_transitions else state.held_actions
pulse = next(
(actions for actions in reversed(pending_transitions) if actions),
None,
)
if pulse is not None and pulse != final_actions:
frames = (pulse,) + (final_actions,) * (self._frames_per_block - 1)
else:
frames = (final_actions,) * self._frames_per_block
state = _LingBotCameraReducerState(
mode="state",
held_actions=final_actions,
)
else:
frames = ()
if frames:
controls["camera"] = ARDiffusionControlInput(
track="camera",
schema=LINGBOT_CAMERA_ACTION_SCHEMA,
data={
"mode": "frames",
"frames": [list(actions) for actions in frames],
},
)
return ARDiffusionPreparedControls(
controls=tuple(controls[track] for track in sorted(controls)),
private_state=state,
)
def commit(self, prepared: ARDiffusionPreparedControls) -> None:
if not isinstance(prepared.private_state, _LingBotCameraReducerState):
raise TypeError("LingBot reducer prepared state has an unexpected type.")
self._state = prepared.private_state
def reset(self) -> None:
self._state = _LingBotCameraReducerState()
def _rotation_matrix(axis: str, angle: float) -> np.ndarray:
cosine = math.cos(angle)
sine = math.sin(angle)
if axis == "x":
return np.array(
((1.0, 0.0, 0.0), (0.0, cosine, -sine), (0.0, sine, cosine)),
dtype=np.float64,
)
return np.array(
((cosine, 0.0, sine), (0.0, 1.0, 0.0), (-sine, 0.0, cosine)),
dtype=np.float64,
)
def integrate_lingbot_camera_actions(
frames: Sequence[Sequence[str]],
*,
width: int,
height: int,
initial_pose: torch.Tensor | None = None,
initial_pitch: float = 0.0,
) -> tuple[CameraTrajectory, float]:
"""Convert latent-frame WASD/IJKL controls to cumulative C2W poses.
Calibration and motion constants intentionally match SGLang's LingBot
adapter: movement 0.05, pitch 4 degrees, yaw 6 degrees, pitch clamp 85
degrees, and target-resolution focal lengths of 500 pixels.
"""
normalized_frames = tuple(
_normalize_actions(actions, field=f"camera action frames[{index}]") for index, actions in enumerate(frames)
)
if not normalized_frames:
raise ValueError("camera action frames must not be empty.")
if width <= 0 or height <= 0:
raise ValueError("camera action resolution must be positive.")
if initial_pose is None:
current_pose = np.eye(4, dtype=np.float64)
else:
if initial_pose.shape != (4, 4) or not torch.isfinite(initial_pose).all():
raise ValueError("initial_pose must be one finite 4x4 camera-to-world matrix.")
current_pose = initial_pose.detach().cpu().double().numpy().copy()
current_pitch = float(initial_pitch)
pitch_limit = math.radians(85.0)
poses: list[np.ndarray] = []
for actions in normalized_frames:
rotation = current_pose[:3, :3]
translation = current_pose[:3, 3]
pitch_delta = math.radians(4.0) * (("i" in actions) - ("k" in actions))
if not -pitch_limit <= current_pitch + pitch_delta <= pitch_limit:
pitch_delta = 0.0
else:
current_pitch += pitch_delta
yaw_delta = math.radians(6.0) * (("l" in actions) - ("j" in actions))
new_rotation = _rotation_matrix("y", yaw_delta) @ rotation @ _rotation_matrix("x", pitch_delta)
forward = np.array((new_rotation[0, 2], 0.0, new_rotation[2, 2]))
right = np.array((new_rotation[0, 0], 0.0, new_rotation[2, 0]))
forward_norm = np.linalg.norm(forward)
right_norm = np.linalg.norm(right)
if forward_norm > 0:
forward /= forward_norm + 1e-6
if right_norm > 0:
right /= right_norm + 1e-6
movement = np.zeros(3, dtype=np.float64)
movement += forward * 0.05 * (("w" in actions) - ("s" in actions))
movement += right * 0.05 * (("d" in actions) - ("a" in actions))
current_pose = np.eye(4, dtype=np.float64)
current_pose[:3, :3] = new_rotation
current_pose[:3, 3] = translation + movement
poses.append(current_pose)
# build_plucker_embedding expects intrinsics in the 832x480 reference
# coordinate system. These values become SGLang's [500, 500, W/2, H/2]
# after that function scales them to the requested resolution.
reference_intrinsics = (
500.0 * _REFERENCE_WIDTH / width,
500.0 * _REFERENCE_HEIGHT / height,
_REFERENCE_WIDTH / 2,
_REFERENCE_HEIGHT / 2,
)
trajectory = CameraTrajectory(
# Keep the cumulative controller pose in FP64 between chunks, matching
# SGLang's NumPy integration and avoiding long-session drift.
poses=torch.from_numpy(np.stack(poses)),
intrinsics=torch.tensor(reference_intrinsics, dtype=torch.float32).repeat(len(poses), 1),
)
return trajectory, current_pitch
@@ -0,0 +1,481 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Camera trajectory loading and geometry for LingBot World v2 conditioning."""
from __future__ import annotations
import io
import math
import os
import stat
from collections.abc import Iterator
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import BinaryIO
import numpy as np
import torch
_REFERENCE_HEIGHT = 480
_REFERENCE_WIDTH = 832
_MAX_ACTION_FRAMES = 117
_MAX_SOURCE_ACTION_FRAMES = 4096
_MAX_NPY_HEADER_BYTES = 10_000
_DIRECTORY_OPEN_FLAGS = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
_FILE_OPEN_FLAGS = os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK
@dataclass(frozen=True)
class CameraTrajectory:
"""Camera-to-world poses and ``(fx, fy, cx, cy)`` intrinsics by frame."""
poses: torch.Tensor
intrinsics: torch.Tensor
@dataclass(frozen=True)
class TrustedActionDirectory:
"""Canonical action location tied to the trusted root's filesystem identity."""
root: Path
relative: Path
root_device: int
root_inode: int
def resolve_trusted_action_directory(
action_path: str | os.PathLike[str],
trusted_root: str | os.PathLike[str],
) -> TrustedActionDirectory:
"""Resolve an action directory beneath a trusted root without exposing paths."""
try:
root = Path(trusted_root).expanduser().resolve(strict=True)
except (OSError, RuntimeError):
raise ValueError("The configured LingBot trusted action root is unavailable.") from None
if not root.is_dir():
raise ValueError("The configured LingBot trusted action root must be a directory.")
try:
candidate = Path(action_path).expanduser()
if not candidate.is_absolute():
candidate = root / candidate
candidate = candidate.resolve(strict=True)
relative = candidate.relative_to(root)
except (OSError, RuntimeError, ValueError):
raise ValueError(
"sampling_params.extra_args.action_path must be contained by the trusted action root."
) from None
if not candidate.is_dir():
raise ValueError("sampling_params.extra_args.action_path must identify a directory in the trusted action root.")
try:
root_stat = root.stat()
except OSError:
raise ValueError("The configured LingBot trusted action root is unavailable.") from None
return TrustedActionDirectory(
root=root,
relative=relative,
root_device=root_stat.st_dev,
root_inode=root_stat.st_ino,
)
def _validate_trajectory(
trajectory: CameraTrajectory,
*,
file_suffix: str = "",
) -> None:
poses = trajectory.poses
intrinsics = trajectory.intrinsics
if poses.ndim != 3 or poses.shape[1:] != (4, 4):
raise ValueError(f"poses{file_suffix} must have shape [frames, 4, 4], got {tuple(poses.shape)}")
if intrinsics.ndim != 2 or intrinsics.shape[1:] != (4,):
raise ValueError(
f"intrinsics{file_suffix} must have shape [frames, 4] ordered as fx, fy, cx, cy, "
f"got {tuple(intrinsics.shape)}"
)
if poses.shape[0] == 0:
raise ValueError("camera trajectory must contain at least one frame")
if poses.shape[0] != intrinsics.shape[0]:
raise ValueError("poses and intrinsics must contain the same number of frames")
if not torch.isfinite(poses).all() or not torch.isfinite(intrinsics).all():
raise ValueError("camera trajectory values must all be finite")
@contextmanager
def _open_action_directory(action_directory: TrustedActionDirectory) -> Iterator[int]:
root_fd: int | None = None
action_fd: int | None = None
try:
root_fd = os.open(action_directory.root, _DIRECTORY_OPEN_FLAGS)
root_stat = os.fstat(root_fd)
if (root_stat.st_dev, root_stat.st_ino) != (
action_directory.root_device,
action_directory.root_inode,
):
raise ValueError("The configured LingBot trusted action root changed before camera files were opened.")
action_fd = os.dup(root_fd)
for component in action_directory.relative.parts:
next_fd = os.open(component, _DIRECTORY_OPEN_FLAGS, dir_fd=action_fd)
os.close(action_fd)
action_fd = next_fd
except OSError:
if action_fd is not None:
os.close(action_fd)
if root_fd is not None:
os.close(root_fd)
raise ValueError("Unable to securely open the LingBot action directory within the trusted root.") from None
except ValueError:
if action_fd is not None:
os.close(action_fd)
if root_fd is not None:
os.close(root_fd)
raise
assert action_fd is not None
assert root_fd is not None
try:
yield action_fd
finally:
os.close(action_fd)
os.close(root_fd)
@contextmanager
def _open_camera_file(action_fd: int, filename: str) -> Iterator[tuple[BinaryIO, os.stat_result]]:
file_fd = -1
try:
file_fd = os.open(filename, _FILE_OPEN_FLAGS, dir_fd=action_fd)
file_stat = os.fstat(file_fd)
if not stat.S_ISREG(file_stat.st_mode):
raise ValueError(f"{filename} must be a regular NPY file.")
with os.fdopen(file_fd, "rb", closefd=True) as file_handle:
file_fd = -1
yield file_handle, file_stat
except FileNotFoundError:
raise FileNotFoundError(f"camera trajectory file not found: {filename}") from None
except OSError:
raise ValueError(f"{filename} could not be opened securely from the trusted action directory.") from None
finally:
if file_fd >= 0:
os.close(file_fd)
def _load_bounded_npy(
action_fd: int,
filename: str,
*,
trailing_shape: tuple[int, ...],
) -> np.ndarray:
with _open_camera_file(action_fd, filename) as (file_handle, file_stat):
max_payload_bytes = _MAX_SOURCE_ACTION_FRAMES * math.prod(trailing_shape) * 8
max_file_bytes = _MAX_NPY_HEADER_BYTES + max_payload_bytes + 32
if file_stat.st_size > max_file_bytes:
raise ValueError(f"{filename} byte size exceeds the bounded camera-array limit.")
snapshot = file_handle.read(max_file_bytes + 1)
if len(snapshot) != file_stat.st_size:
raise ValueError(f"{filename} changed while its bounded contents were read.")
snapshot_file = io.BytesIO(snapshot)
try:
version = np.lib.format.read_magic(snapshot_file)
if version == (1, 0):
shape, _fortran_order, dtype = np.lib.format.read_array_header_1_0(snapshot_file)
elif version == (2, 0):
shape, _fortran_order, dtype = np.lib.format.read_array_header_2_0(snapshot_file)
else:
raise ValueError(f"{filename} must use NPY format version 1.0 or 2.0.")
except (EOFError, TypeError, ValueError):
raise ValueError(f"{filename} has an invalid or oversized NPY header.") from None
if len(shape) != len(trailing_shape) + 1 or tuple(shape[1:]) != trailing_shape:
expected = ", ".join(str(value) for value in trailing_shape)
raise ValueError(f"{filename} must have shape [frames, {expected}], got {shape}.")
frames = shape[0]
if isinstance(frames, bool) or not isinstance(frames, int) or not 1 <= frames <= _MAX_SOURCE_ACTION_FRAMES:
raise ValueError(f"{filename} must contain between 1 and {_MAX_SOURCE_ACTION_FRAMES} source frames.")
if dtype.kind not in "fiu" or dtype.hasobject or dtype.itemsize > 8:
raise ValueError(f"{filename} must contain real numeric values with at most 8 bytes per element.")
data_offset = snapshot_file.tell()
expected_bytes = math.prod(shape) * dtype.itemsize
if len(snapshot) != data_offset + expected_bytes:
raise ValueError(f"{filename} byte size does not match its NPY header.")
snapshot_file.seek(0)
try:
array = np.load(snapshot_file, allow_pickle=False)
except (EOFError, TypeError, ValueError):
raise ValueError(f"{filename} does not contain a valid numeric NPY array.") from None
# Official LingBot trajectories may be longer than one request. Only
# materialize the bounded prefix that any supported request can consume.
result: np.ndarray = np.asarray(array[:_MAX_ACTION_FRAMES], dtype=np.float32)
return result
def load_camera_trajectory(action_directory: TrustedActionDirectory) -> CameraTrajectory:
"""Load bounded numeric camera arrays through a trusted directory handle."""
if not isinstance(action_directory, TrustedActionDirectory):
raise TypeError("action_directory must be resolved against a trusted action root before loading.")
with _open_action_directory(action_directory) as action_fd:
poses_array = _load_bounded_npy(action_fd, "poses.npy", trailing_shape=(4, 4))
intrinsics_array = _load_bounded_npy(action_fd, "intrinsics.npy", trailing_shape=(4,))
trajectory = CameraTrajectory(
poses=torch.from_numpy(poses_array),
intrinsics=torch.from_numpy(intrinsics_array),
)
_validate_trajectory(trajectory, file_suffix=".npy")
return trajectory
def _linear_interpolate(values: torch.Tensor, target_times: np.ndarray) -> torch.Tensor:
source_times = np.linspace(0.0, 1.0, values.shape[0])
values_array = values.detach().cpu().double().numpy()
columns = [
np.interp(target_times, source_times, values_array[:, column]) for column in range(values_array.shape[1])
]
interpolated = torch.from_numpy(np.stack(columns, axis=1))
return interpolated.to(device=values.device, dtype=values.dtype)
def _rotation_matrices_to_quaternions(matrices: np.ndarray) -> np.ndarray:
"""Convert rotation matrices to normalized ``(w, x, y, z)`` quaternions."""
quaternions: np.ndarray = np.empty((matrices.shape[0], 4), dtype=np.float64)
for index, matrix in enumerate(matrices):
trace = float(np.trace(matrix))
if trace > 0.0:
scale = math.sqrt(trace + 1.0) * 2.0
quaternion = np.array(
[
0.25 * scale,
(matrix[2, 1] - matrix[1, 2]) / scale,
(matrix[0, 2] - matrix[2, 0]) / scale,
(matrix[1, 0] - matrix[0, 1]) / scale,
]
)
else:
axis = int(np.argmax(np.diag(matrix)))
if axis == 0:
scale = math.sqrt(max(0.0, 1.0 + matrix[0, 0] - matrix[1, 1] - matrix[2, 2])) * 2.0
quaternion = np.array(
[
(matrix[2, 1] - matrix[1, 2]) / scale,
0.25 * scale,
(matrix[0, 1] + matrix[1, 0]) / scale,
(matrix[0, 2] + matrix[2, 0]) / scale,
]
)
elif axis == 1:
scale = math.sqrt(max(0.0, 1.0 + matrix[1, 1] - matrix[0, 0] - matrix[2, 2])) * 2.0
quaternion = np.array(
[
(matrix[0, 2] - matrix[2, 0]) / scale,
(matrix[0, 1] + matrix[1, 0]) / scale,
0.25 * scale,
(matrix[1, 2] + matrix[2, 1]) / scale,
]
)
else:
scale = math.sqrt(max(0.0, 1.0 + matrix[2, 2] - matrix[0, 0] - matrix[1, 1])) * 2.0
quaternion = np.array(
[
(matrix[1, 0] - matrix[0, 1]) / scale,
(matrix[0, 2] + matrix[2, 0]) / scale,
(matrix[1, 2] + matrix[2, 1]) / scale,
0.25 * scale,
]
)
norm = np.linalg.norm(quaternion)
if not np.isfinite(norm) or norm <= np.finfo(np.float64).eps:
raise ValueError("camera rotation matrices must describe valid rotations")
quaternions[index] = quaternion / norm
return quaternions
def _quaternions_to_rotation_matrices(quaternions: np.ndarray) -> np.ndarray:
quaternions = quaternions / np.linalg.norm(quaternions, axis=1, keepdims=True)
w, x, y, z = quaternions.T
matrices: np.ndarray = np.stack(
(
1 - 2 * (y * y + z * z),
2 * (x * y - z * w),
2 * (x * z + y * w),
2 * (x * y + z * w),
1 - 2 * (x * x + z * z),
2 * (y * z - x * w),
2 * (x * z - y * w),
2 * (y * z + x * w),
1 - 2 * (x * x + y * y),
),
axis=1,
).reshape(-1, 3, 3)
return matrices
def _slerp_rotations(matrices: np.ndarray, target_times: np.ndarray) -> np.ndarray:
quaternions = _rotation_matrices_to_quaternions(matrices)
interpolated = np.empty((target_times.shape[0], 4), dtype=np.float64)
for target_index, target_time in enumerate(target_times):
scaled_time = float(target_time) * (matrices.shape[0] - 1)
left = min(max(math.floor(scaled_time), 0), matrices.shape[0] - 2)
fraction = scaled_time - left
first = quaternions[left]
second = quaternions[left + 1]
cosine = float(np.dot(first, second))
if cosine < 0.0:
second = -second
cosine = -cosine
cosine = min(cosine, 1.0)
if cosine > 0.9995:
quaternion = first + fraction * (second - first)
interpolated[target_index] = quaternion / np.linalg.norm(quaternion)
continue
angle = math.acos(cosine)
denominator = math.sin(angle)
interpolated[target_index] = (
math.sin((1.0 - fraction) * angle) / denominator * first + math.sin(fraction * angle) / denominator * second
)
return _quaternions_to_rotation_matrices(interpolated)
def interpolate_camera_trajectory(
trajectory: CameraTrajectory,
num_frames: int,
) -> CameraTrajectory:
"""Resample translations/intrinsics linearly and rotations with quaternion SLERP."""
_validate_trajectory(trajectory)
if num_frames <= 0:
raise ValueError(f"num_frames must be positive, got {num_frames}")
source_frames = trajectory.poses.shape[0]
if num_frames == source_frames:
return trajectory
if source_frames == 1:
return CameraTrajectory(
poses=trajectory.poses.repeat(num_frames, 1, 1),
intrinsics=trajectory.intrinsics.repeat(num_frames, 1),
)
# Rotations use quaternion SLERP; translations and intrinsics are linear.
target_times = np.linspace(0.0, 1.0, num_frames)
rotation_matrices = trajectory.poses[:, :3, :3].detach().cpu().double().numpy()
interpolated_rotations = _slerp_rotations(rotation_matrices, target_times)
translations = _linear_interpolate(trajectory.poses[:, :3, 3], target_times)
intrinsics = _linear_interpolate(trajectory.intrinsics, target_times)
poses = torch.eye(
4,
device=trajectory.poses.device,
dtype=trajectory.poses.dtype,
).repeat(num_frames, 1, 1)
poses[:, :3, :3] = torch.from_numpy(interpolated_rotations).to(
device=trajectory.poses.device,
dtype=trajectory.poses.dtype,
)
poses[:, :3, 3] = translations
return CameraTrajectory(poses=poses, intrinsics=intrinsics)
def _invert_camera_poses(poses: torch.Tensor) -> torch.Tensor:
"""Invert rigid transforms using R^-1 = R^T and t^-1 = -R^T t."""
rotations = poses[:, :3, :3]
translations = poses[:, :3, 3:]
inverse_rotations = rotations.transpose(-1, -2)
inverse = torch.eye(4, device=poses.device, dtype=poses.dtype).repeat(poses.shape[0], 1, 1)
inverse[:, :3, :3] = inverse_rotations
inverse[:, :3, 3:] = -torch.bmm(inverse_rotations, translations)
return inverse
def _prepare_framewise_poses(raw_poses: torch.Tensor) -> torch.Tensor:
"""Convert raw C2W poses to normalized first-relative framewise deltas."""
# Remove global placement, then convert the sequence to framewise deltas.
first_world_to_camera = _invert_camera_poses(raw_poses[:1])
relative_poses = torch.matmul(first_world_to_camera, raw_poses).clone()
relative_poses[0] = torch.eye(4, device=raw_poses.device, dtype=raw_poses.dtype)
if relative_poses.shape[0] > 1:
relative_poses[1:] = torch.bmm(
_invert_camera_poses(relative_poses[:-1]),
relative_poses[1:],
)
# Only relative direction/magnitude matters to the checkpoint; normalizing
# by the largest step removes the trajectory's arbitrary translation scale.
translations = relative_poses[:, :3, 3]
max_translation_norm = torch.linalg.vector_norm(translations, dim=-1).max()
if max_translation_norm > 0:
relative_poses[:, :3, 3] = translations / max_translation_norm
return relative_poses
def build_plucker_embedding(
trajectory: CameraTrajectory,
*,
height: int,
width: int,
target_height: int,
target_width: int,
device: torch.device,
dtype: torch.dtype,
) -> torch.Tensor:
"""Build checkpoint-ordered ``(ray origin, ray direction)`` channels."""
_validate_trajectory(trajectory)
dimensions = {
"height": height,
"width": width,
"target_height": target_height,
"target_width": target_width,
}
for name, value in dimensions.items():
if value <= 0:
raise ValueError(f"{name} must be positive, got {value}")
if dtype not in (torch.float16, torch.bfloat16, torch.float32, torch.float64):
raise ValueError(f"dtype must be floating point, got {dtype}")
# Compute geometry in at least FP32, then cast at the model boundary.
compute_dtype = torch.float32 if dtype in (torch.float16, torch.bfloat16) else dtype
poses = _prepare_framewise_poses(trajectory.poses.to(device=device, dtype=compute_dtype))
intrinsics = trajectory.intrinsics.to(device=device, dtype=compute_dtype).clone()
intrinsics[:, (0, 2)] *= width / _REFERENCE_WIDTH
intrinsics[:, (1, 3)] *= height / _REFERENCE_HEIGHT
if torch.any(intrinsics[:, :2] == 0):
raise ValueError("camera focal lengths fx and fy must be non-zero")
# Sample requested-pixel coordinates directly at the conditioning resolution.
x_coordinates = (torch.arange(target_width, device=device, dtype=compute_dtype) + 0.5) * (width / target_width)
y_coordinates = (torch.arange(target_height, device=device, dtype=compute_dtype) + 0.5) * (height / target_height)
grid_y, grid_x = torch.meshgrid(y_coordinates, x_coordinates, indexing="ij")
grid_x = grid_x.unsqueeze(0)
grid_y = grid_y.unsqueeze(0)
# Pinhole back-projection: pixel (x, y) becomes the camera-space direction
# ((x-cx)/fx, (y-cy)/fy, 1), which is normalized before rotation.
fx, fy, cx, cy = intrinsics.unbind(dim=1)
camera_x = (grid_x - cx[:, None, None]) / fx[:, None, None]
camera_y = (grid_y - cy[:, None, None]) / fy[:, None, None]
camera_directions = torch.stack(
(camera_x, camera_y, torch.ones_like(camera_x)),
dim=-1,
)
camera_directions = camera_directions / torch.linalg.vector_norm(camera_directions, dim=-1, keepdim=True)
# A ray is represented by its camera center (origin) and unit direction in
# the prepared relative coordinate frame. Channel order is checkpoint
# data, not an implementation preference: [origin_xyz, direction_xyz].
world_directions = torch.einsum(
"fij,fhwj->fhwi",
poses[:, :3, :3],
camera_directions,
)
world_directions = world_directions / torch.linalg.vector_norm(world_directions, dim=-1, keepdim=True)
ray_origins = poses[:, None, None, :3, 3].expand_as(world_directions)
embedding = torch.cat((ray_origins, world_directions), dim=-1)
return embedding.permute(0, 3, 1, 2).contiguous().to(dtype=dtype)
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+8
View File
@@ -136,6 +136,11 @@ _DIFFUSION_MODELS = {
"pipeline_wan2_2_i2v",
"WanI2VDMD2Pipeline",
),
"LingBotWorldCausalDMDPipeline": (
"lingbot_world",
"pipeline",
"LingBotWorldCausalDMDPipeline",
),
"LongCatImagePipeline": (
"longcat_image",
"pipeline_longcat_image",
@@ -353,6 +358,7 @@ _NO_CACHE_ACCELERATION = {
# Pipelines that do not support cache acceleration (cache_dit / tea_cache).
"NextStep11Pipeline",
"AudioXPipeline",
"LingBotWorldCausalDMDPipeline",
}
@@ -547,6 +553,7 @@ _DIFFUSION_POST_PROCESS_FUNCS = {
"WanS2VPipeline": "get_wan22_s2v_post_process_func",
"WanT2VDMD2Pipeline": "get_wan22_post_process_func",
"WanI2VDMD2Pipeline": "get_wan22_i2v_post_process_func",
"LingBotWorldCausalDMDPipeline": "get_lingbot_world_post_process_func",
"LongCatImagePipeline": "get_longcat_image_post_process_func",
"LongCatVideoAvatarPipeline": "get_longcat_video_avatar_post_process_func",
"BagelPipeline": "get_bagel_post_process_func",
@@ -607,6 +614,7 @@ _DIFFUSION_PRE_PROCESS_FUNCS = {
"WanS2VPipeline": "get_wan22_s2v_pre_process_func",
"WanT2VDMD2Pipeline": "get_wan22_pre_process_func",
"WanI2VDMD2Pipeline": "get_wan22_i2v_pre_process_func",
"LingBotWorldCausalDMDPipeline": "get_lingbot_world_pre_process_func",
"OmniGen2Pipeline": "get_omnigen2_pre_process_func",
"HeliosPipeline": "get_helios_pre_process_func",
"HeliosPyramidPipeline": "get_helios_pre_process_func",
@@ -444,6 +444,25 @@ class DiffusionWorker:
else:
profiler.stop()
def _run_ar_diffusion_session_lifecycle(self, method: str, session_id: str) -> bool:
"""Delegate an optional AR session lifecycle call to the model runner."""
if not isinstance(session_id, str) or not session_id.strip():
raise ValueError("session_id must be a non-empty string.")
assert self.model_runner is not None, "Model runner not initialized"
lifecycle_method = getattr(self.model_runner, method, None)
if not callable(lifecycle_method):
return False
lifecycle_method(session_id)
return True
def reset_ar_diffusion_session(self, session_id: str) -> bool:
"""Reset runner-owned AR state through the collective RPC boundary."""
return self._run_ar_diffusion_session_lifecycle("reset_session", session_id)
def close_ar_diffusion_session(self, session_id: str) -> bool:
"""Close runner-owned AR state through the collective RPC boundary."""
return self._run_ar_diffusion_session_lifecycle("close_session", session_id)
def execute_model(
self,
req: OmniDiffusionRequest | list[OmniDiffusionRequest],
+3
View File
@@ -982,6 +982,9 @@ class AsyncOmniEngine:
stage_engine_args = {
"max_num_seqs": kwargs.get("max_num_seqs") or 1,
"parallel_config": parallel_config,
# Default-stage construction bypasses the structured projection.
# Runner selection remains owned by the selected engine/platform.
"engine_backend": kwargs.get("engine_backend", "default"),
"model_class_name": kwargs.get("model_class_name", None),
"task_type": kwargs.get("task_type", None),
"model_config": kwargs.get("model_config", None),
@@ -13,12 +13,32 @@ from vllm_omni.experimental.ar_diffusion.capability import (
SupportsARDiffusionPipeline,
SupportsARDiffusionWarmup,
)
from vllm_omni.experimental.ar_diffusion.consumer import ARDiffusionOmniTickConsumer
from vllm_omni.experimental.ar_diffusion.engine import ARDiffusionEngine
from vllm_omni.experimental.ar_diffusion.session import (
ARDiffusionSession,
ARDiffusionSessionManager,
ARDiffusionWorkerLifecycle,
)
from vllm_omni.experimental.ar_diffusion.tick_protocol import (
AR_DIFFUSION_TICK_KEY,
ARDiffusionChunkMetadata,
ARDiffusionControlInput,
ARDiffusionTickRequest,
)
__all__ = [
"ARDiffusionKVBranchSpec",
"ARDiffusionCrossAttentionKVSpec",
"ARDiffusionEngine",
"ARDiffusionOmniTickConsumer",
"ARDiffusionSession",
"ARDiffusionSessionManager",
"ARDiffusionWorkerLifecycle",
"AR_DIFFUSION_TICK_KEY",
"ARDiffusionChunkMetadata",
"ARDiffusionControlInput",
"ARDiffusionTickRequest",
"ARDiffusionKVCacheSpec",
"SupportsARDiffusionPipeline",
"SupportsARDiffusionWarmup",
@@ -58,6 +58,9 @@ class ARDiffusionKVCacheSpec:
``max_scratch_tokens_per_branch`` is the maximum non-video KV (for example,
action/state registers) that must coexist with an uncommitted video block.
``model_owned_state_bytes_per_session`` is a conservative upper bound for
persistent per-session CUDA state outside runner-owned self/cross-attention
KV and scratch pools.
``session_capacity`` is the pipeline's upper bound; a runner may retain
fewer sessions when its current memory and execution policy is stricter.
@@ -79,6 +82,7 @@ class ARDiffusionKVCacheSpec:
cross_attention: tuple[ARDiffusionCrossAttentionKVSpec, ...] = ()
max_model_len: int = 1 << 20
max_scratch_tokens_per_branch: int = 0
model_owned_state_bytes_per_session: int = 0
def __post_init__(self) -> None:
positive_fields = {
@@ -101,6 +105,11 @@ class ARDiffusionKVCacheSpec:
"AR-Diffusion max_scratch_tokens_per_branch must be non-negative, "
f"got {self.max_scratch_tokens_per_branch}"
)
if self.model_owned_state_bytes_per_session < 0:
raise ValueError(
"AR-Diffusion model_owned_state_bytes_per_session must be non-negative, "
f"got {self.model_owned_state_bytes_per_session}"
)
if not self.kv_branches:
raise ValueError("AR-Diffusion requires at least one KV branch")
kv_branch_names = [kv_branch.name for kv_branch in self.kv_branches]
@@ -0,0 +1,168 @@
# SPDX-License-Identifier: Apache-2.0
"""AsyncOmni consumer for typed realtime AR-Diffusion ticks."""
from __future__ import annotations
import copy
from collections.abc import AsyncIterator, Callable, Mapping, Sequence
from typing import Any, Protocol
from vllm_omni.experimental.ar_diffusion.tick_protocol import (
ARDiffusionChunkMetadata,
ARDiffusionTickRequest,
)
from vllm_omni.inputs.data import (
OmniDiffusionSamplingParams,
OmniPromptType,
OmniSamplingParams,
)
from vllm_omni.outputs import OmniRequestOutput
AR_DIFFUSION_OUTPUT_METADATA_KEY = "ar_diffusion"
class ARDiffusionGenerateClient(Protocol):
"""Internal subset of AsyncOmni used to execute one tick."""
def generate(
self,
prompt: OmniPromptType,
*,
request_id: str,
sampling_params_list: Sequence[OmniSamplingParams],
output_modalities: list[str] | None = None,
) -> AsyncIterator[OmniRequestOutput]: ...
def _metadata_mapping(output: OmniRequestOutput) -> Mapping[str, Any]:
multimodal_output = output.multimodal_output
if not isinstance(multimodal_output, Mapping):
raise ValueError("AR-Diffusion output must contain a multimodal output mapping.")
metadata = multimodal_output.get("metadata")
if not isinstance(metadata, Mapping):
raise ValueError("AR-Diffusion output must contain multimodal_output.metadata.")
value = metadata.get(AR_DIFFUSION_OUTPUT_METADATA_KEY)
if not isinstance(value, Mapping):
raise ValueError(f"AR-Diffusion output metadata must contain {AR_DIFFUSION_OUTPUT_METADATA_KEY!r}.")
return value
def _parse_chunk_metadata(output: OmniRequestOutput) -> ARDiffusionChunkMetadata:
value = _metadata_mapping(output)
session_id = value.get("session_id")
request_id = value.get("request_id")
chunk_index = value.get("chunk_index")
applied_event_ids = value.get("applied_event_ids", ())
if not isinstance(session_id, str) or not session_id.strip():
raise ValueError("AR-Diffusion output metadata session_id must be a non-empty string.")
if not isinstance(request_id, str) or not request_id.strip():
raise ValueError("AR-Diffusion output metadata request_id must be a non-empty string.")
if isinstance(chunk_index, bool) or not isinstance(chunk_index, int) or chunk_index < 0:
raise ValueError("AR-Diffusion output metadata chunk_index must be a non-negative integer.")
if not isinstance(applied_event_ids, Sequence) or isinstance(applied_event_ids, (str, bytes, bytearray)):
raise ValueError("AR-Diffusion output metadata applied_event_ids must be a sequence.")
event_ids = tuple(applied_event_ids)
if any(isinstance(event_id, bool) or not isinstance(event_id, int) or event_id < 0 for event_id in event_ids):
raise ValueError("AR-Diffusion output metadata applied_event_ids must contain non-negative integers.")
if event_ids != tuple(sorted(set(event_ids))):
raise ValueError("AR-Diffusion output metadata applied_event_ids must be unique and strictly increasing.")
return ARDiffusionChunkMetadata(
session_id=session_id,
request_id=request_id,
chunk_index=chunk_index,
applied_event_ids=event_ids,
)
class ARDiffusionOmniTickConsumer:
"""Converts a typed tick into one AsyncOmni diffusion request.
``prompt_provider`` packages session initialization data such as an initial
image into the normal Omni prompt. Control ``track/schema/data`` remains in
the typed tick and is not interpreted by this class.
"""
def __init__(
self,
engine_client: ARDiffusionGenerateClient,
*,
prompt_provider: Callable[[ARDiffusionTickRequest], OmniPromptType],
sampling_params_list: Sequence[OmniSamplingParams],
diffusion_stage_id: int,
output_modalities: Sequence[str] | None = None,
) -> None:
templates = tuple(sampling_params_list)
if not templates:
raise ValueError("sampling_params_list must not be empty.")
if (
isinstance(diffusion_stage_id, bool)
or not isinstance(diffusion_stage_id, int)
or not 0 <= diffusion_stage_id < len(templates)
):
raise ValueError("diffusion_stage_id must index sampling_params_list.")
if not isinstance(templates[diffusion_stage_id], OmniDiffusionSamplingParams):
raise TypeError("The selected diffusion stage must use OmniDiffusionSamplingParams.")
stage_pools = getattr(getattr(engine_client, "engine", None), "stage_pools", None)
if stage_pools is None or not 0 <= diffusion_stage_id < len(stage_pools):
raise ValueError(
"AR-Diffusion realtime execution requires an initialized AsyncOmni "
"engine with a matching diffusion stage pool."
)
replica_count = getattr(stage_pools[diffusion_stage_id], "num_replicas", None)
if isinstance(replica_count, bool) or not isinstance(replica_count, int) or replica_count < 1:
raise ValueError("The selected AR-Diffusion stage must expose a positive num_replicas.")
if replica_count != 1:
raise ValueError(
"AR-Diffusion realtime sessions currently require exactly one "
"replica for the selected diffusion stage; session-affine "
f"routing is not implemented (got num_replicas={replica_count})."
)
self._engine_client = engine_client
self._prompt_provider = prompt_provider
self._sampling_params_templates = templates
self._diffusion_stage_id = diffusion_stage_id
self._output_modalities = list(output_modalities) if output_modalities is not None else None
def _sampling_params_for_tick(
self,
tick: ARDiffusionTickRequest,
) -> list[OmniSamplingParams]:
params = [copy.copy(template) for template in self._sampling_params_templates]
diffusion_params = params[self._diffusion_stage_id]
assert isinstance(diffusion_params, OmniDiffusionSamplingParams)
diffusion_params.extra_args = {
**(diffusion_params.extra_args or {}),
**tick.to_extra_args(),
}
return params
async def execute_tick(self, tick: ARDiffusionTickRequest) -> OmniRequestOutput:
prompt = self._prompt_provider(tick)
final_output: OmniRequestOutput | None = None
async for output in self._engine_client.generate(
prompt,
request_id=tick.request_id,
sampling_params_list=self._sampling_params_for_tick(tick),
output_modalities=self._output_modalities,
):
if output.stage_id != self._diffusion_stage_id:
continue
if output.finished:
final_output = output
if final_output is None:
raise RuntimeError(
f"AR-Diffusion tick {tick.request_id!r} completed without a final "
f"output from stage {self._diffusion_stage_id}."
)
if final_output.error:
raise RuntimeError(f"AR-Diffusion tick {tick.request_id!r} failed: {final_output.error}")
if final_output.request_id != tick.request_id:
raise ValueError("AR-Diffusion output request_id does not match the submitted tick request_id.")
# Parse here so malformed/missing standard metadata fails at the engine
# boundary, before the session attempts to commit accepted events.
self.chunk_metadata(final_output)
return final_output
def chunk_metadata(self, output: OmniRequestOutput) -> ARDiffusionChunkMetadata:
return _parse_chunk_metadata(output)
@@ -32,7 +32,6 @@ from vllm_omni.experimental.ar_diffusion.kv_cache.paged_attention import (
ARDiffusionPagedLayerInputs,
ar_diffusion_paged_attention,
paged_write_attn,
set_current_paged_kv_cache,
)
__all__ = [
@@ -53,5 +52,4 @@ __all__ = [
"paged_write_attn",
"pool_write_chunk",
"resident_block_ids",
"set_current_paged_kv_cache",
]
@@ -23,7 +23,8 @@ class ARDiffusionKVConfig:
sink_chunks: int = 0
# Boundary reset vs. sliding replacement.
reset_at_boundary: bool = False
# Fraction of free device memory budgeted for the AR-Diffusion KV pool.
# Fraction of free device memory used to admit additional resident
# sessions. One session is admitted whenever it fits actual free memory.
gpu_memory_fraction: float = 0.1
# When CUDA graph / torch.compile is on (not enforce_eager), pre-capture the
# DiT graphs for every window-fill shape at load time via a synthetic rollout,
@@ -174,6 +174,7 @@ class ARDiffusionKVCache:
device: torch.device | None = None,
frames_per_block: int = 1,
max_scratch_tokens_per_branch: int = 0,
model_owned_state_bytes_per_session: int = 0,
) -> None:
if not config.enable:
raise ValueError("ARDiffusionKVCache built with a disabled ARDiffusionKVConfig")
@@ -197,13 +198,17 @@ class ARDiffusionKVCache:
self.config = config
self.kv_branches = kv_branches
self.session_capacity = session_capacity
self.requested_session_capacity = session_capacity
self._kv_branch_local_indices = {kv_branch.name: kv_branch.local_index for kv_branch in kv_branches}
self.num_local_kv_branches = max(local_indices) + 1
if frames_per_block <= 0:
raise ValueError(f"frames_per_block must be positive, got {frames_per_block}")
if max_scratch_tokens_per_branch < 0:
raise ValueError(f"max_scratch_tokens_per_branch must be non-negative, got {max_scratch_tokens_per_branch}")
if model_owned_state_bytes_per_session < 0:
raise ValueError(
f"model_owned_state_bytes_per_session must be non-negative, got {model_owned_state_bytes_per_session}"
)
self.frames_per_block = int(frames_per_block)
self.block_size = block_size
self.num_layers = num_layers
@@ -217,28 +222,11 @@ class ARDiffusionKVCache:
self.device = device or torch.device("cpu")
self._allocate_tensors = device is not None
self._adapters: dict[str, ARDiffusionRequestAdapter] = {}
# Named cross-attention K/V is allocated lazily per session and released
# with that session. Reserve its worst-case capacity when sizing the
# paged self-attention pool so the two stores share one memory budget.
self._cross_sessions: dict[
str,
dict[str, dict[str, tuple[list[torch.Tensor], list[torch.Tensor]]]],
] = {}
def _cross_pool_bytes(length: int) -> int:
return int(2 * len(self.kv_branches) * length * num_kv_heads * head_size * dtype.itemsize * num_layers)
cross_bytes_per_session = sum(_cross_pool_bytes(length) for length in self.cross_attention_lengths.values())
cross_total_bytes = cross_bytes_per_session * session_capacity
if cross_total_bytes:
_log.info(
"AR-Diffusion cross-attn reservation: %.1f MiB/session × %d sessions = %.1f MiB",
cross_bytes_per_session / (1024 * 1024),
session_capacity,
cross_total_bytes / (1024 * 1024),
)
self.spec = ChunkWindowSpec(
block_size=block_size,
num_kv_heads=num_kv_heads,
@@ -250,41 +238,7 @@ class ARDiffusionKVCache:
sink_chunks=config.sink_chunks,
reset_at_boundary=config.reset_at_boundary,
)
# Each pool block spans all layers' K/V, so size against the per-layer
# page size times the layer count.
# Size the self-attn pool against memory reserved for the maximum number
# of lazily allocated cross-attention sessions.
num_blocks = compute_num_blocks(
max(0, available_bytes - cross_total_bytes),
config.gpu_memory_fraction,
self.spec.page_size_bytes * num_layers,
)
# Floor: one forward needs the resident window plus the in-flight chunk
# (frames_per_block frame-blocks) for every KV branch THIS rank runs,
# with a little eviction-transient headroom. The memory-fraction
# heuristic can under-size this once block_size grows — e.g. frame-granular
# paging at the true frame_seqlen makes each block larger and the pool
# fewer-blocks — so guarantee the minimum the rollout cannot run without,
# otherwise allocate_chunk hits an exhausted pool mid-forward.
resident_blocks = config.sink_chunks + config.window_chunks
min_blocks = self.num_local_kv_branches * (resident_blocks + self.frames_per_block) + 2
if num_blocks < min_blocks:
_log.warning(
"AR-Diffusion KV pool: memory-fraction sizing gave %d blocks; raising to the %d-block "
"floor (%d local KV branch(es) x (sink_chunks=%d + window_chunks=%d "
"+ frames_per_block=%d) + 2 headroom)",
num_blocks,
min_blocks,
self.num_local_kv_branches,
config.sink_chunks,
config.window_chunks,
self.frames_per_block,
)
num_blocks = min_blocks
layer_names = [f"ar_diffusion.layer.{i}" for i in range(num_layers)]
self.manager = build_kv_manager(self.spec, layer_names, num_blocks, max_model_len)
self.managed_num_blocks = num_blocks
self.num_blocks = num_blocks
# Scratch blocks are outside KVCacheManager ownership. A non-committing
# forward needs one block per current frame plus space for any
# model-declared action/state tokens that coexist with video KV.
@@ -297,6 +251,101 @@ class ARDiffusionKVCache:
scratch_per_kv_branch = max(minimum_scratch_blocks, override_blocks)
self.scratch_blocks_per_kv_branch = scratch_per_kv_branch
self.scratch_num_blocks = self.num_local_kv_branches * scratch_per_kv_branch
# The self-attention pool, scratch pool, and lazily materialized
# cross-attention caches share one hard memory budget. Select the largest
# feasible resident-session count rather than raising a block-count floor
# past that budget. All resident sessions need a complete sink + window;
# only one request can be in flight, so frames_per_block is counted once.
page_size_bytes = self.spec.page_size_bytes * num_layers
self.available_memory_bytes = available_bytes
self.configured_memory_budget_bytes = int(available_bytes * config.gpu_memory_fraction)
# Reuse the public helper's validation for the memory fraction/page size.
compute_num_blocks(available_bytes, config.gpu_memory_fraction, page_size_bytes)
self.scratch_reserved_bytes = self.scratch_num_blocks * page_size_bytes
self.model_owned_state_bytes_per_session = model_owned_state_bytes_per_session
def _cross_pool_bytes(length: int) -> int:
return int(2 * len(self.kv_branches) * length * num_kv_heads * head_size * dtype.itemsize * num_layers)
self.cross_attention_bytes_per_session = sum(
_cross_pool_bytes(length) for length in self.cross_attention_lengths.values()
)
def _required_managed_blocks(capacity: int) -> int:
resident_per_session = config.sink_chunks + config.window_chunks
return self.num_local_kv_branches * (capacity * resident_per_session + self.frames_per_block) + 2
def _required_bytes(capacity: int) -> int:
return (
self.scratch_reserved_bytes
+ capacity * self.cross_attention_bytes_per_session
+ capacity * self.model_owned_state_bytes_per_session
+ _required_managed_blocks(capacity) * page_size_bytes
)
one_session_bytes = _required_bytes(1)
if one_session_bytes > available_bytes:
raise ValueError(
"AR-Diffusion available device memory cannot fit one session: "
f"available={available_bytes} bytes, required={one_session_bytes} bytes "
"(managed self-attention + cross-attention + scratch + model-owned state)."
)
self.memory_budget_bytes = max(self.configured_memory_budget_bytes, one_session_bytes)
if self.memory_budget_bytes > self.configured_memory_budget_bytes:
_log.warning(
"AR-Diffusion raised the configured memory budget from %d to %d bytes "
"to admit one session that fits actual free device memory",
self.configured_memory_budget_bytes,
self.memory_budget_bytes,
)
effective_capacity = 0
required_managed_blocks = 0
for candidate in range(session_capacity, 0, -1):
candidate_managed_blocks = _required_managed_blocks(candidate)
if _required_bytes(candidate) <= self.memory_budget_bytes:
effective_capacity = candidate
required_managed_blocks = candidate_managed_blocks
break
assert effective_capacity > 0
self.session_capacity = effective_capacity
self.cross_attention_reserved_bytes = self.cross_attention_bytes_per_session * effective_capacity
self.model_owned_state_reserved_bytes = self.model_owned_state_bytes_per_session * effective_capacity
self_attn_budget_bytes = (
self.memory_budget_bytes
- self.scratch_reserved_bytes
- self.cross_attention_reserved_bytes
- self.model_owned_state_reserved_bytes
)
num_blocks = self_attn_budget_bytes // page_size_bytes
assert num_blocks >= required_managed_blocks
if effective_capacity < session_capacity:
_log.warning(
"AR-Diffusion resident session capacity reduced from %d to %d by the KV memory budget",
session_capacity,
effective_capacity,
)
if self.cross_attention_reserved_bytes:
_log.info(
"AR-Diffusion cross-attn reservation: %.1f MiB/session × %d sessions = %.1f MiB",
self.cross_attention_bytes_per_session / (1024 * 1024),
effective_capacity,
self.cross_attention_reserved_bytes / (1024 * 1024),
)
if self.model_owned_state_reserved_bytes:
_log.info(
"AR-Diffusion model-owned state reservation: %.1f MiB/session × %d sessions = %.1f MiB",
self.model_owned_state_bytes_per_session / (1024 * 1024),
effective_capacity,
self.model_owned_state_reserved_bytes / (1024 * 1024),
)
layer_names = [f"ar_diffusion.layer.{i}" for i in range(num_layers)]
self.manager = build_kv_manager(self.spec, layer_names, num_blocks, max_model_len)
self.managed_num_blocks = num_blocks
self.num_blocks = num_blocks
self.num_blocks_total = self.managed_num_blocks + self.scratch_num_blocks
self.null_block_id = self.manager.block_pool.null_block.block_id
@@ -561,6 +610,11 @@ class ARDiffusionKVCache:
"""
_log.debug("AR-Diffusion commit: req=%s before=%d", adapter.request_id, adapter.completed_chunks)
adapter.on_chunk_committed()
self.manager.remove_skipped_blocks(
adapter.request_id,
adapter.num_computed_tokens,
num_prompt_tokens=adapter.num_prompt_tokens,
)
_log.debug("AR-Diffusion commit: req=%s after=%d", adapter.request_id, adapter.completed_chunks)
# -- pool-backed K/V access --------------------------------------------
@@ -10,19 +10,6 @@ import torch
from vllm_omni.experimental.ar_diffusion.kv_cache.paged import compute_slot_mapping
# Set by ARDiffusionPagedForwardContext.prepare() before each KV branch forward and
# read by the fused write+attend custom op below. The pools are process-lifetime
# allocations owned by one ARDiffusionKVCache per worker process (CFG-parallel /
# TP ranks are separate processes), mirroring vLLM's forward-context pattern for
# keeping multi-GiB cache tensors out of the compiled graph's inputs.
_CURRENT_PAGED_KV_CACHE: Any = None
def set_current_paged_kv_cache(kv_cache: Any) -> None:
global _CURRENT_PAGED_KV_CACHE
_CURRENT_PAGED_KV_CACHE = kv_cache
_LAYER_IDX_TENSORS: dict[int, torch.Tensor] = {}
@@ -50,6 +37,9 @@ class ARDiffusionPagedLayerInputs(NamedTuple):
"""
layer_idx: torch.Tensor
key_pool: torch.Tensor
value_pool: torch.Tensor
block_size: int
seq_len: int
video_slots: torch.Tensor
action_slots: torch.Tensor
@@ -228,7 +218,6 @@ class ARDiffusionPagedForwardContext:
) = self.build_block_table(action_len=action_len, query_len=query_len, device=device)
if self.action_slot_mapping is None:
self.action_slot_mapping = torch.empty(0, dtype=torch.long, device=device)
set_current_paged_kv_cache(self.kv_cache)
self._prepared = True
def layer_inputs(self, layer_idx: int) -> ARDiffusionPagedLayerInputs:
@@ -236,6 +225,9 @@ class ARDiffusionPagedForwardContext:
raise RuntimeError("ARDiffusionPagedForwardContext.layer_inputs() before prepare()")
return ARDiffusionPagedLayerInputs(
layer_idx=_layer_idx_tensor(layer_idx),
key_pool=self.kv_cache._k_pools[layer_idx],
value_pool=self.kv_cache._v_pools[layer_idx],
block_size=int(self.kv_cache.block_size),
seq_len=int(self.seq_len),
video_slots=self.current_video_slot_mapping,
action_slots=self.action_slot_mapping,
@@ -476,17 +468,18 @@ def ar_diffusion_paged_attention(
# One opaque op per layer keeps the compiled DiT block fullgraph: dynamo treats
# it as a single graph node (no eager island, no graph breaks), and the K/V slot
# writes happen inside the op so write→read ordering with the block-table kernel
# is internal — no hidden-mutation ordering hazards against a separate reader op
# and no multi-GiB pool tensors as graph inputs (the flat and paged pool views
# alias the same storage, which AOTAutograd handles poorly as inputs). Pattern
# follows sage_attn3.py in-repo and vLLM's own unified attention ops.
# is internal. The flat pools are explicit mutable inputs: Inductor/CUDA Graph
# must track their storage lifetime instead of observing an undeclared mutation
# through a process-global registry.
def _paged_write_attn_impl(
query: torch.Tensor,
k_curr: torch.Tensor,
v_curr: torch.Tensor,
k_act: torch.Tensor | None,
v_act: torch.Tensor | None,
layer_idx: torch.Tensor,
key_pool: torch.Tensor,
value_pool: torch.Tensor,
block_size: int,
video_slots: torch.Tensor,
action_slots: torch.Tensor,
block_table: torch.Tensor,
@@ -496,21 +489,17 @@ def _paged_write_attn_impl(
max_seq_len: int,
softmax_scale: float,
) -> torch.Tensor:
kv = _CURRENT_PAGED_KV_CACHE
if kv is None:
raise RuntimeError("ar_diffusion_paged_write_attn called before prepare() set the KV pool registry")
layer_idx = int(layer_idx)
k_pool = kv._k_pools[layer_idx]
v_pool = kv._v_pools[layer_idx]
k_pool[video_slots] = k_curr.to(k_pool.dtype)
v_pool[video_slots] = v_curr.to(v_pool.dtype)
key_pool[video_slots] = k_curr.to(key_pool.dtype)
value_pool[video_slots] = v_curr.to(value_pool.dtype)
if k_act is not None and v_act is not None and k_act.shape[0] > 0:
k_pool[action_slots] = k_act.to(k_pool.dtype)
v_pool[action_slots] = v_act.to(v_pool.dtype)
key_pool[action_slots] = k_act.to(key_pool.dtype)
value_pool[action_slots] = v_act.to(value_pool.dtype)
key_cache = key_pool.unflatten(0, (-1, block_size))
value_cache = value_pool.unflatten(0, (-1, block_size))
return ar_diffusion_paged_attention(
query,
kv.key_cache(layer_idx),
kv.value_cache(layer_idx),
key_cache,
value_cache,
block_table=block_table,
query_start_loc=query_start_loc,
seq_lens=seq_lens,
@@ -525,14 +514,19 @@ def _paged_write_attn_impl(
# the module from sys.modules (same as sage_attn3.py).
if not hasattr(torch.ops.vllm_omni, "ar_diffusion_paged_write_attn"):
@torch.library.custom_op("vllm_omni::ar_diffusion_paged_write_attn", mutates_args=())
@torch.library.custom_op(
"vllm_omni::ar_diffusion_paged_write_attn",
mutates_args=("key_pool", "value_pool"),
)
def _paged_write_attn_op(
query: torch.Tensor,
k_curr: torch.Tensor,
v_curr: torch.Tensor,
k_act: torch.Tensor | None,
v_act: torch.Tensor | None,
layer_idx: torch.Tensor,
key_pool: torch.Tensor,
value_pool: torch.Tensor,
block_size: int,
video_slots: torch.Tensor,
action_slots: torch.Tensor,
block_table: torch.Tensor,
@@ -548,7 +542,9 @@ if not hasattr(torch.ops.vllm_omni, "ar_diffusion_paged_write_attn"):
v_curr,
k_act,
v_act,
layer_idx,
key_pool,
value_pool,
block_size,
video_slots,
action_slots,
block_table,
@@ -566,7 +562,9 @@ if not hasattr(torch.ops.vllm_omni, "ar_diffusion_paged_write_attn"):
v_curr,
k_act,
v_act,
layer_idx,
key_pool,
value_pool,
block_size,
video_slots,
action_slots,
block_table,
@@ -589,7 +587,9 @@ def paged_write_attn(
v_curr,
k_act,
v_act,
inputs.layer_idx,
inputs.key_pool,
inputs.value_pool,
inputs.block_size,
inputs.video_slots,
inputs.action_slots,
inputs.block_table,
@@ -130,7 +130,7 @@ class ARDiffusionKVState:
if ctx.commit_current and ctx._allocated_video:
n_chunks = ctx.seq_len // self.kv_cache.spec.chunk_size
for _ in range(n_chunks):
ctx.adapter.on_chunk_committed()
self.kv_cache.commit_chunk(ctx.adapter)
self._committed[kv_branch] += ctx.seq_len
_log.debug(
"AR-Diffusion COMMIT [%s] new_tokens=%d chunks=%d resident=%d/%d",
@@ -173,6 +173,17 @@ class ARDiffusionKVState:
for i in range(self.num_layers)
]
def clear_cross_attention(self) -> None:
"""Invalidate all named cross-attention caches without dropping self-KV.
Prompt changes may preserve the autoregressive world history while
requiring fresh text projections. This operation deliberately leaves
branch adapters and their resident self-attention blocks untouched.
"""
if self._closed:
raise RuntimeError(f"AR-Diffusion session {self.session_id!r} is closed")
self.kv_cache.release_cross_attention(self.session_id)
def close(self) -> None:
"""Release all self- and cross-attention storage owned by this session."""
if self._closed:
+16 -8
View File
@@ -21,6 +21,7 @@ from vllm_omni.experimental.ar_diffusion.capability import (
from vllm_omni.experimental.ar_diffusion.kv_cache.config import ARDiffusionKVConfig
from vllm_omni.experimental.ar_diffusion.kv_cache.manager import ARDiffusionKVCache
from vllm_omni.experimental.ar_diffusion.kv_cache.state import ARDiffusionKVState
from vllm_omni.experimental.ar_diffusion.tick_protocol import ARDiffusionTickRequest
logger = init_logger(__name__)
@@ -51,7 +52,6 @@ class ARDiffusionModelRunner(DiffusionModelRunner):
"""
_WARMUP_SID = "__ardiffusion_warmup__"
_MAX_RESIDENT_SESSIONS = 1
def __init__(self, vllm_config: object, od_config: OmniDiffusionConfig, device: torch.device) -> None:
super().__init__(vllm_config, od_config, device)
@@ -123,7 +123,6 @@ class ARDiffusionModelRunner(DiffusionModelRunner):
self.ar_diffusion_kv_config = config
self._ar_diffusion_capability = capability
self._ar_diffusion_kv_cache_spec = spec
self._session_capacity = min(spec.session_capacity, self._MAX_RESIDENT_SESSIONS)
self.kv_cache = ARDiffusionKVCache(
config,
num_layers=spec.num_layers,
@@ -134,12 +133,14 @@ class ARDiffusionModelRunner(DiffusionModelRunner):
max_model_len=spec.max_model_len,
available_bytes=self._available_memory_bytes() if available_bytes is None else available_bytes,
kv_branches=spec.kv_branches,
session_capacity=self._session_capacity,
session_capacity=spec.session_capacity,
cross_attention_lengths=spec.cross_attention_lengths,
frames_per_block=spec.frames_per_block,
max_scratch_tokens_per_branch=spec.max_scratch_tokens_per_branch,
model_owned_state_bytes_per_session=spec.model_owned_state_bytes_per_session,
device=self.device,
)
self._session_capacity = self.kv_cache.session_capacity
logger.info(
"AR-Diffusion KV cache: blocks=%d layers=%d local_kv_heads=%d head_size=%d "
"tokens/frame=%d frames/block=%d window=%d sink=%d kv_branches=%s cross=%s "
@@ -230,9 +231,14 @@ class ARDiffusionModelRunner(DiffusionModelRunner):
return state
@staticmethod
def _request_session(req: OmniDiffusionRequest) -> tuple[str, dict]:
def _request_session(
req: OmniDiffusionRequest,
) -> tuple[str, dict, ARDiffusionTickRequest | None]:
extra_args = req.sampling_params.extra_args or {}
return str(extra_args.get("session_id") or "default"), extra_args
tick = ARDiffusionTickRequest.from_extra_args(extra_args)
if tick is not None:
return tick.session_id, extra_args, tick
return str(extra_args.get("session_id") or "default"), extra_args, None
def execute_model(
self,
@@ -245,8 +251,10 @@ class ARDiffusionModelRunner(DiffusionModelRunner):
if capability is None:
raise RuntimeError("AR-Diffusion capability missing after KV cache initialization")
session_id, extra_args = self._request_session(req)
if extra_args.get("reset", False):
session_id, extra_args, tick = self._request_session(req)
reset = tick.reset if tick is not None else bool(extra_args.get("reset", False))
close_session = tick.close_session if tick is not None else bool(extra_args.get("close_session", False))
if reset:
self.reset_session(session_id)
state = self._get_or_create_session(session_id)
started = time.perf_counter()
@@ -268,7 +276,7 @@ class ARDiffusionModelRunner(DiffusionModelRunner):
)
raise
self._perf_e2e_times.append(time.perf_counter() - started)
if extra_args.get("close_session", False):
if close_session:
self.close_session(session_id)
return output
@@ -0,0 +1,578 @@
# SPDX-License-Identifier: Apache-2.0
"""Internal realtime session control plane for AR-Diffusion.
This module owns transport-level identity, event ordering, backpressure, and
chunk-boundary snapshots. Model-defined control payloads remain opaque here.
"""
from __future__ import annotations
import asyncio
import uuid
from collections import OrderedDict
from collections.abc import Callable, Mapping, Sequence
from dataclasses import dataclass
from enum import Enum
from typing import Any, Generic, Protocol, TypeVar
from vllm_omni.experimental.ar_diffusion.tick_protocol import (
ARDiffusionChunkMetadata,
ARDiffusionControlInput,
ARDiffusionTickRequest,
)
class ARDiffusionSessionError(RuntimeError):
"""Base error for realtime AR-Diffusion session operations."""
class ARDiffusionSessionStateError(ARDiffusionSessionError):
"""Raised when an operation is invalid for the current session state."""
class ARDiffusionEventQueueFullError(ARDiffusionSessionError):
"""Raised when accepting an event would exceed the bounded queue."""
class ARDiffusionStaleEventError(ARDiffusionSessionError):
"""Raised when an event is older than the current monotonic boundary."""
class ARDiffusionChunkMetadataError(ARDiffusionSessionError):
"""Raised when a tick consumer returns metadata for a different chunk."""
class ARDiffusionEventAcceptanceStatus(str, Enum):
ACCEPTED = "accepted"
DUPLICATE = "duplicate"
class ARDiffusionSessionStatus(str, Enum):
ACTIVE = "active"
RESETTING = "resetting"
FAILED = "failed"
CLOSING = "closing"
CLEANUP_FAILED = "cleanup_failed"
CLOSED = "closed"
def _copy_control(control: ARDiffusionControlInput) -> ARDiffusionControlInput:
return ARDiffusionControlInput(
track=control.track,
schema=control.schema,
data=control.data,
)
@dataclass(frozen=True)
class ARDiffusionSessionEvent:
"""One atomically accepted prompt/control update.
``prompt=None`` means that this event does not update the prompt. Controls
are keyed by ``track`` when the chunk snapshot is assembled; their schema
and data are otherwise opaque to this layer.
"""
event_id: int
prompt: str | None = None
controls: tuple[ARDiffusionControlInput, ...] = ()
def __post_init__(self) -> None:
if isinstance(self.event_id, bool) or not isinstance(self.event_id, int) or self.event_id < 0:
raise ValueError("event_id must be a non-negative integer.")
if self.prompt is not None and not isinstance(self.prompt, str):
raise ValueError("prompt must be a string or None.")
controls = tuple(self.controls)
if any(not isinstance(control, ARDiffusionControlInput) for control in controls):
raise ValueError("controls must contain ARDiffusionControlInput values.")
tracks = [control.track for control in controls]
if len(tracks) != len(set(tracks)):
raise ValueError("A composite event may update each control track at most once.")
if self.prompt is None and not controls:
raise ValueError("An event must update the prompt, one or more controls, or both.")
object.__setattr__(self, "controls", controls)
@dataclass(frozen=True)
class ARDiffusionEventAcceptance:
"""Immediate queue acceptance result; this does not mean applied."""
session_id: str
event_id: int
status: ARDiffusionEventAcceptanceStatus
pending_event_count: int
TickOutputT = TypeVar("TickOutputT")
class ARDiffusionTickConsumer(Protocol[TickOutputT]):
"""Consumes one immutable chunk-boundary tick."""
async def execute_tick(self, tick: ARDiffusionTickRequest) -> TickOutputT: ...
def chunk_metadata(self, output: TickOutputT) -> ARDiffusionChunkMetadata: ...
@dataclass(frozen=True)
class ARDiffusionPreparedControls:
"""One reducer-owned, transactional control snapshot.
``private_state`` is opaque to the session layer and becomes durable only
after the corresponding model tick succeeds.
"""
controls: tuple[ARDiffusionControlInput, ...]
private_state: Any = None
class ARDiffusionControlReducer(Protocol):
"""Optional model adapter for stateful controls sampled per AR block."""
def prepare(
self,
*,
current_controls: Mapping[str, ARDiffusionControlInput],
events: Sequence[ARDiffusionSessionEvent],
chunk_index: int,
) -> ARDiffusionPreparedControls: ...
def commit(self, prepared: ARDiffusionPreparedControls) -> None: ...
def reset(self) -> None: ...
class ARDiffusionSessionLifecycle(Protocol):
"""Worker-facing session lifecycle operations."""
async def reset_session(self, session_id: str) -> None: ...
async def close_session(self, session_id: str) -> None: ...
class ARDiffusionCollectiveRPCClient(Protocol):
"""Subset of the engine control RPC surface used by session lifecycle."""
async def collective_rpc(
self,
method: str,
timeout: float | None = None,
args: tuple[Any, ...] = (),
kwargs: dict[str, Any] | None = None,
stage_ids: list[int] | None = None,
) -> list[Any]: ...
def _collect_rpc_support_results(value: Any, *, errors: list[str], supported: list[bool]) -> None:
if isinstance(value, bool):
supported.append(value)
return
if isinstance(value, (list, tuple)):
for item in value:
_collect_rpc_support_results(item, errors=errors, supported=supported)
return
if isinstance(value, Mapping):
if value.get("supported") is False:
errors.append(str(value.get("error") or "target stage does not support this lifecycle RPC"))
return
if "result" in value:
_collect_rpc_support_results(value["result"], errors=errors, supported=supported)
class ARDiffusionWorkerLifecycle:
"""Routes reset/close through the existing orchestrator collective RPC."""
def __init__(
self,
rpc_client: ARDiffusionCollectiveRPCClient,
*,
stage_ids: Sequence[int],
timeout: float | None = None,
) -> None:
normalized_stage_ids = tuple(stage_ids)
if not normalized_stage_ids:
raise ValueError("stage_ids must select at least one AR-Diffusion stage.")
if any(isinstance(stage_id, bool) or not isinstance(stage_id, int) or stage_id < 0 for stage_id in stage_ids):
raise ValueError("stage_ids must contain non-negative integers.")
self._rpc_client = rpc_client
self._stage_ids = normalized_stage_ids
self._timeout = timeout
async def _invoke(self, method: str, session_id: str) -> None:
results = await self._rpc_client.collective_rpc(
method=method,
timeout=self._timeout,
args=(session_id,),
stage_ids=list(self._stage_ids),
)
errors: list[str] = []
supported: list[bool] = []
_collect_rpc_support_results(results, errors=errors, supported=supported)
if errors:
raise ARDiffusionSessionError(f"{method} failed: {'; '.join(errors)}")
if not supported or not all(supported):
raise ARDiffusionSessionError(f"{method} is not supported by every selected worker.")
async def reset_session(self, session_id: str) -> None:
await self._invoke("reset_ar_diffusion_session", session_id)
async def close_session(self, session_id: str) -> None:
await self._invoke("close_ar_diffusion_session", session_id)
def _default_request_id(session_id: str, chunk_index: int) -> str:
return f"ar-{session_id}-{chunk_index}-{uuid.uuid4().hex}"
class ARDiffusionSession(Generic[TickOutputT]):
"""Transactional event queue and chunk snapshot for one realtime world."""
def __init__(
self,
session_id: str,
*,
tick_consumer: ARDiffusionTickConsumer[TickOutputT],
lifecycle: ARDiffusionSessionLifecycle,
max_pending_events: int,
request_id_factory: Callable[[str, int], str] | None = None,
control_reducer: ARDiffusionControlReducer | None = None,
) -> None:
if not isinstance(session_id, str) or not session_id.strip():
raise ValueError("session_id must be a non-empty string.")
if isinstance(max_pending_events, bool) or not isinstance(max_pending_events, int) or max_pending_events <= 0:
raise ValueError("max_pending_events must be a positive integer.")
self.session_id = session_id
self._tick_consumer = tick_consumer
self._lifecycle = lifecycle
self._max_pending_events = max_pending_events
self._request_id_factory = request_id_factory or _default_request_id
self._control_reducer = control_reducer
self._status = ARDiffusionSessionStatus.ACTIVE
self._pending_events: dict[int, ARDiffusionSessionEvent] = {}
self._recent_event_ids: OrderedDict[int, None] = OrderedDict()
self._event_floor = -1
self._inflight_event_floor: int | None = None
self._chunk_index = 0
self._prompt: str | None = None
self._controls: dict[str, ARDiffusionControlInput] = {}
self._state_lock = asyncio.Lock()
self._tick_lock = asyncio.Lock()
@property
def status(self) -> ARDiffusionSessionStatus:
return self._status
@property
def chunk_index(self) -> int:
return self._chunk_index
@property
def pending_event_count(self) -> int:
return len(self._pending_events)
def _require_active(self) -> None:
if self._status is not ARDiffusionSessionStatus.ACTIVE:
raise ARDiffusionSessionStateError(f"AR-Diffusion session {self.session_id!r} is {self._status.value}.")
def _require_resettable(self) -> None:
if self._status not in {
ARDiffusionSessionStatus.ACTIVE,
ARDiffusionSessionStatus.FAILED,
}:
raise ARDiffusionSessionStateError(f"AR-Diffusion session {self.session_id!r} is {self._status.value}.")
async def accept_event(self, event: ARDiffusionSessionEvent) -> ARDiffusionEventAcceptance:
"""Atomically enqueue an event or return an idempotent duplicate result."""
if not isinstance(event, ARDiffusionSessionEvent):
raise TypeError("event must be an ARDiffusionSessionEvent.")
async with self._state_lock:
self._require_active()
if event.event_id in self._pending_events:
return ARDiffusionEventAcceptance(
session_id=self.session_id,
event_id=event.event_id,
status=ARDiffusionEventAcceptanceStatus.DUPLICATE,
pending_event_count=len(self._pending_events),
)
if event.event_id in self._recent_event_ids:
return ARDiffusionEventAcceptance(
session_id=self.session_id,
event_id=event.event_id,
status=ARDiffusionEventAcceptanceStatus.DUPLICATE,
pending_event_count=len(self._pending_events),
)
event_floor = self._event_floor
if self._inflight_event_floor is not None:
event_floor = max(event_floor, self._inflight_event_floor)
if event.event_id <= event_floor:
raise ARDiffusionStaleEventError(
f"event_id {event.event_id} is stale; the session event floor is {event_floor}."
)
if len(self._pending_events) >= self._max_pending_events:
raise ARDiffusionEventQueueFullError(
f"AR-Diffusion session {self.session_id!r} event queue is full "
f"({self._max_pending_events} pending)."
)
stored_event = ARDiffusionSessionEvent(
event_id=event.event_id,
prompt=event.prompt,
controls=tuple(_copy_control(control) for control in event.controls),
)
self._pending_events[event.event_id] = stored_event
self._recent_event_ids[event.event_id] = None
while len(self._recent_event_ids) > self._max_pending_events:
self._recent_event_ids.popitem(last=False)
return ARDiffusionEventAcceptance(
session_id=self.session_id,
event_id=event.event_id,
status=ARDiffusionEventAcceptanceStatus.ACCEPTED,
pending_event_count=len(self._pending_events),
)
def _build_tick_locked(
self,
) -> tuple[
ARDiffusionTickRequest,
str | None,
dict[str, ARDiffusionControlInput],
ARDiffusionPreparedControls | None,
]:
event_ids = tuple(sorted(self._pending_events))
events = tuple(self._pending_events[event_id] for event_id in event_ids)
prompt = self._prompt
for event in events:
if event.prompt is not None:
prompt = event.prompt
prepared_controls: ARDiffusionPreparedControls | None = None
if self._control_reducer is None:
controls = {track: _copy_control(control) for track, control in self._controls.items()}
for event in events:
for control in event.controls:
controls[control.track] = _copy_control(control)
else:
prepared_controls = self._control_reducer.prepare(
current_controls={track: _copy_control(control) for track, control in self._controls.items()},
events=events,
chunk_index=self._chunk_index,
)
if not isinstance(prepared_controls, ARDiffusionPreparedControls):
raise TypeError("control reducer prepare() must return ARDiffusionPreparedControls.")
controls = {}
for control in prepared_controls.controls:
if not isinstance(control, ARDiffusionControlInput):
raise TypeError("prepared controls must contain ARDiffusionControlInput values.")
if control.track in controls:
raise ValueError("prepared controls must contain at most one value per track.")
controls[control.track] = _copy_control(control)
chunk_index = self._chunk_index
request_id = self._request_id_factory(self.session_id, chunk_index)
tick = ARDiffusionTickRequest(
session_id=self.session_id,
request_id=request_id,
chunk_index=chunk_index,
applied_event_ids=event_ids,
prompt=prompt,
controls=tuple(_copy_control(controls[track]) for track in sorted(controls)),
)
self._inflight_event_floor = event_ids[-1] if event_ids else None
return tick, prompt, controls, prepared_controls
async def _commit_tick(
self,
tick: ARDiffusionTickRequest,
prompt: str | None,
controls: dict[str, ARDiffusionControlInput],
prepared_controls: ARDiffusionPreparedControls | None,
) -> None:
async with self._state_lock:
# Reducer state and the session snapshot form one logical commit.
# Run the only fallible operation before mutating session fields so
# a reducer failure cannot acknowledge events or advance the chunk.
if prepared_controls is not None:
assert self._control_reducer is not None
self._control_reducer.commit(prepared_controls)
for event_id in tick.applied_event_ids:
self._pending_events.pop(event_id, None)
if tick.applied_event_ids:
self._event_floor = tick.applied_event_ids[-1]
self._prompt = prompt
self._controls = controls
self._chunk_index += 1
self._inflight_event_floor = None
async def _fail_tick(self, tick_error: BaseException) -> None:
async with self._state_lock:
self._inflight_event_floor = None
self._status = ARDiffusionSessionStatus.FAILED
try:
# A model forward may already have released its state. This
# idempotent close also covers successful forwards whose metadata
# or local reducer commit could not be accepted.
await self._lifecycle.close_session(self.session_id)
except Exception as cleanup_error:
raise ARDiffusionSessionError(
"AR-Diffusion tick failed "
f"({type(tick_error).__name__}: {tick_error}) and worker "
f"session cleanup also failed: {cleanup_error}"
) from tick_error
async def next_chunk(self) -> TickOutputT:
"""Execute and atomically commit one chunk-boundary snapshot."""
async with self._tick_lock:
async with self._state_lock:
self._require_active()
tick, prompt, controls, prepared_controls = self._build_tick_locked()
expected_metadata = ARDiffusionChunkMetadata.from_tick(tick)
try:
output = await self._tick_consumer.execute_tick(tick)
metadata = self._tick_consumer.chunk_metadata(output)
if not isinstance(metadata, ARDiffusionChunkMetadata) or metadata != expected_metadata:
raise ARDiffusionChunkMetadataError(
"Tick consumer metadata does not match the submitted AR-Diffusion tick."
)
except BaseException as tick_error:
await self._fail_tick(tick_error)
raise
commit_task = asyncio.create_task(
self._commit_tick(
tick,
prompt,
controls,
prepared_controls,
)
)
try:
await asyncio.shield(commit_task)
except asyncio.CancelledError:
# Once a consumer reports success, the local chunk/event state
# must commit even if the transport task is being cancelled.
try:
await commit_task
except BaseException as commit_error:
await self._fail_tick(commit_error)
raise
raise
except BaseException as commit_error:
await self._fail_tick(commit_error)
raise
return output
async def reset(self) -> None:
"""Reset worker state and discard queued/current transport state."""
async with self._tick_lock:
async with self._state_lock:
self._require_resettable()
self._status = ARDiffusionSessionStatus.RESETTING
try:
await self._lifecycle.reset_session(self.session_id)
except BaseException:
async with self._state_lock:
self._status = ARDiffusionSessionStatus.FAILED
raise
async with self._state_lock:
if self._pending_events:
self._event_floor = max(self._event_floor, max(self._pending_events))
self._pending_events.clear()
self._prompt = None
self._controls.clear()
if self._control_reducer is not None:
self._control_reducer.reset()
self._inflight_event_floor = None
self._chunk_index = 0
self._status = ARDiffusionSessionStatus.ACTIVE
async def close(self) -> None:
"""Close worker state, retaining a tombstone until cleanup succeeds."""
async with self._tick_lock:
async with self._state_lock:
if self._status is ARDiffusionSessionStatus.CLOSED:
return
self._status = ARDiffusionSessionStatus.CLOSING
try:
await self._lifecycle.close_session(self.session_id)
except BaseException:
async with self._state_lock:
self._status = ARDiffusionSessionStatus.CLEANUP_FAILED
raise
else:
async with self._state_lock:
self._pending_events.clear()
self._prompt = None
self._controls.clear()
self._inflight_event_floor = None
self._status = ARDiffusionSessionStatus.CLOSED
async def disconnect(self) -> None:
"""Disconnect uses the same owning-worker cleanup as explicit close."""
await self.close()
class ARDiffusionSessionManager(Generic[TickOutputT]):
"""Creates and owns transport sessions sharing one tick/lifecycle backend."""
def __init__(
self,
*,
tick_consumer: ARDiffusionTickConsumer[TickOutputT],
lifecycle: ARDiffusionSessionLifecycle,
max_pending_events: int,
request_id_factory: Callable[[str, int], str] | None = None,
session_id_factory: Callable[[], str] | None = None,
control_reducer_factory: Callable[[], ARDiffusionControlReducer] | None = None,
) -> None:
self._tick_consumer = tick_consumer
self._lifecycle = lifecycle
self._max_pending_events = max_pending_events
self._request_id_factory = request_id_factory
self._session_id_factory = session_id_factory or (lambda: uuid.uuid4().hex)
self._control_reducer_factory = control_reducer_factory
self._sessions: dict[str, ARDiffusionSession[TickOutputT]] = {}
self._lock = asyncio.Lock()
async def create_session(self, session_id: str | None = None) -> ARDiffusionSession[TickOutputT]:
resolved_session_id = session_id or self._session_id_factory()
async with self._lock:
if resolved_session_id in self._sessions:
raise ARDiffusionSessionStateError(f"AR-Diffusion session {resolved_session_id!r} already exists.")
session = ARDiffusionSession(
resolved_session_id,
tick_consumer=self._tick_consumer,
lifecycle=self._lifecycle,
max_pending_events=self._max_pending_events,
request_id_factory=self._request_id_factory,
control_reducer=(
self._control_reducer_factory() if self._control_reducer_factory is not None else None
),
)
self._sessions[resolved_session_id] = session
return session
async def get_session(self, session_id: str) -> ARDiffusionSession[TickOutputT]:
async with self._lock:
try:
return self._sessions[session_id]
except KeyError as exc:
raise ARDiffusionSessionStateError(f"AR-Diffusion session {session_id!r} does not exist.") from exc
async def reset_session(self, session_id: str) -> None:
session = await self.get_session(session_id)
await session.reset()
async def close_session(self, session_id: str) -> None:
session = await self.get_session(session_id)
await session.close()
async with self._lock:
if self._sessions.get(session_id) is session:
self._sessions.pop(session_id)
async def disconnect(self, session_id: str) -> None:
"""Release the owning worker route when its transport disconnects."""
await self.close_session(session_id)
@@ -0,0 +1,202 @@
# SPDX-License-Identifier: Apache-2.0
"""Typed request boundary shared by AR-Diffusion models and serving APIs.
The transport layer owns event ordering and snapshots. Model adapters consume
the resulting immutable tick without learning about WebSocket messages.
"""
from __future__ import annotations
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from types import MappingProxyType
from typing import Any
AR_DIFFUSION_TICK_KEY = "ar_diffusion_tick"
def _non_empty_string(value: object, *, field: str) -> str:
if not isinstance(value, str) or not value.strip():
raise ValueError(f"{field} must be a non-empty string.")
return value
def _non_negative_int(value: object, *, field: str) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
raise ValueError(f"{field} must be a non-negative integer.")
return value
def _freeze_transport_value(value: Any, *, path: str) -> Any:
"""Copy JSON-like transport data into recursively immutable containers."""
if value is None or isinstance(value, (str, bool, int, float)):
return value
if isinstance(value, Mapping):
frozen: dict[str, Any] = {}
for key, item in value.items():
if not isinstance(key, str):
raise ValueError(f"{path} mapping keys must be strings.")
frozen[key] = _freeze_transport_value(item, path=f"{path}.{key}")
return MappingProxyType(frozen)
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
return tuple(_freeze_transport_value(item, path=f"{path}[]") for item in value)
raise ValueError(
f"{path} must contain only transport-safe mappings, sequences, and scalar values; got {type(value).__name__}."
)
def _thaw_transport_value(value: Any) -> Any:
"""Return a detached JSON-serializable copy of an immutable snapshot."""
if isinstance(value, Mapping):
return {key: _thaw_transport_value(item) for key, item in value.items()}
if isinstance(value, tuple):
return [_thaw_transport_value(item) for item in value]
return value
@dataclass(frozen=True)
class ARDiffusionControlInput:
"""One model-defined control track in a chunk-boundary snapshot."""
track: str
schema: str
data: Mapping[str, Any]
def __post_init__(self) -> None:
_non_empty_string(self.track, field="control.track")
_non_empty_string(self.schema, field="control.schema")
if not isinstance(self.data, Mapping):
raise ValueError("control.data must be a mapping.")
object.__setattr__(
self,
"data",
_freeze_transport_value(self.data, path=f"control[{self.track!r}].data"),
)
def to_dict(self) -> dict[str, Any]:
return {
"track": self.track,
"schema": self.schema,
"data": _thaw_transport_value(self.data),
}
@classmethod
def from_dict(cls, value: object) -> ARDiffusionControlInput:
if not isinstance(value, Mapping):
raise ValueError("Each control must be a mapping.")
return cls(
track=value.get("track"), # type: ignore[arg-type]
schema=value.get("schema"), # type: ignore[arg-type]
data=value.get("data"), # type: ignore[arg-type]
)
@dataclass(frozen=True)
class ARDiffusionTickRequest:
"""A single atomic AR-block computation.
``request_id`` identifies this computation, ``session_id`` identifies the
persistent rollout, and ``applied_event_ids`` records which accepted
interaction events are represented by this immutable snapshot.
"""
session_id: str
request_id: str
chunk_index: int
applied_event_ids: tuple[int, ...] = ()
prompt: str | None = None
controls: tuple[ARDiffusionControlInput, ...] = ()
reset: bool = False
close_session: bool = False
def __post_init__(self) -> None:
_non_empty_string(self.session_id, field="session_id")
_non_empty_string(self.request_id, field="request_id")
_non_negative_int(self.chunk_index, field="chunk_index")
if self.prompt is not None and not isinstance(self.prompt, str):
raise ValueError("prompt must be a string or None.")
if not isinstance(self.reset, bool):
raise ValueError("reset must be a boolean.")
if not isinstance(self.close_session, bool):
raise ValueError("close_session must be a boolean.")
event_ids = tuple(
_non_negative_int(event_id, field="applied_event_ids item") for event_id in self.applied_event_ids
)
if event_ids != tuple(sorted(set(event_ids))):
raise ValueError("applied_event_ids must be unique and strictly increasing.")
controls = tuple(self.controls)
if any(not isinstance(control, ARDiffusionControlInput) for control in controls):
raise ValueError("controls must contain ARDiffusionControlInput values.")
object.__setattr__(self, "applied_event_ids", event_ids)
object.__setattr__(self, "controls", controls)
def to_extra_args(self) -> dict[str, Any]:
"""Serialize the tick under one namespaced ``extra_args`` key."""
return {
AR_DIFFUSION_TICK_KEY: {
"session_id": self.session_id,
"request_id": self.request_id,
"chunk_index": self.chunk_index,
"applied_event_ids": list(self.applied_event_ids),
"prompt": self.prompt,
"controls": [control.to_dict() for control in self.controls],
"reset": self.reset,
"close_session": self.close_session,
}
}
@classmethod
def from_extra_args(
cls,
extra_args: Mapping[str, Any] | None,
) -> ARDiffusionTickRequest | None:
"""Parse a typed tick, returning ``None`` for legacy AR requests."""
if extra_args is None or AR_DIFFUSION_TICK_KEY not in extra_args:
return None
value = extra_args[AR_DIFFUSION_TICK_KEY]
if not isinstance(value, Mapping):
raise ValueError(f"{AR_DIFFUSION_TICK_KEY} must be a mapping.")
raw_event_ids = value.get("applied_event_ids", ())
raw_controls = value.get("controls", ())
if not isinstance(raw_event_ids, Sequence) or isinstance(raw_event_ids, (str, bytes)):
raise ValueError("applied_event_ids must be a sequence.")
if not isinstance(raw_controls, Sequence) or isinstance(raw_controls, (str, bytes)):
raise ValueError("controls must be a sequence.")
return cls(
session_id=value.get("session_id"), # type: ignore[arg-type]
request_id=value.get("request_id"), # type: ignore[arg-type]
chunk_index=value.get("chunk_index"), # type: ignore[arg-type]
applied_event_ids=tuple(raw_event_ids),
prompt=value.get("prompt"), # type: ignore[arg-type]
controls=tuple(ARDiffusionControlInput.from_dict(control) for control in raw_controls),
reset=value.get("reset", False), # type: ignore[arg-type]
close_session=value.get("close_session", False), # type: ignore[arg-type]
)
@dataclass(frozen=True)
class ARDiffusionChunkMetadata:
"""Identity metadata returned with one generated AR block."""
session_id: str
request_id: str
chunk_index: int
applied_event_ids: tuple[int, ...] = ()
@classmethod
def from_tick(cls, tick: ARDiffusionTickRequest) -> ARDiffusionChunkMetadata:
return cls(
session_id=tick.session_id,
request_id=tick.request_id,
chunk_index=tick.chunk_index,
applied_event_ids=tick.applied_event_ids,
)
def to_dict(self) -> dict[str, Any]:
return {
"session_id": self.session_id,
"request_id": self.request_id,
"chunk_index": self.chunk_index,
"applied_event_ids": list(self.applied_event_ids),
}