diff --git a/docs/models/supported_models.md b/docs/models/supported_models.md
index 31f3ac35d..58bca4809 100644
--- a/docs/models/supported_models.md
+++ b/docs/models/supported_models.md
@@ -39,6 +39,7 @@ th {
| `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` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
+| `SanaWmPipeline` | SANA-WM | `BBBBruce/SANA-WM_bidirectional-stage1-diffusers` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/NVIDIA/SANA-WM.md) |
| `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` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
| `Wan22VACEPipeline` | Wan2.2-VACE | `Pyros13/Wan2.2-VACE-Fun-A14B-Diffusers` | ✅︎ | | | | — |
diff --git a/docs/user_guide/diffusion_features.md b/docs/user_guide/diffusion_features.md
index 5844d1114..34b642b9e 100644
--- a/docs/user_guide/diffusion_features.md
+++ b/docs/user_guide/diffusion_features.md
@@ -168,6 +168,14 @@ The following tables show which models support each feature:
| **Cosmos3** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (encode/decode) | ✅ | ❌ |
| **LongCat-Video-Avatar-1.5** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **MiniMax-H3** | ✅ (FL2VA) | ✅ | ✅ | ❌ | ✅ (DiT/TE) | ❌ | ✅ | ✅ | ✅ (tile) | ✅ (DiT) | ❌ |
+| **SANA-WM** | ❌ | ❌ | ❌5 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
+
+> Notes:
+> 5. SANA-WM cannot support sequence parallelism: its bidirectional gated delta
+> recurrence carries state across frames, so a rank cannot denoise a slice of
+> the token sequence in isolation. Doing so would need a distributed scan or
+> an all-gather before every GDN block. The remaining ❌ columns are simply
+> unvalidated on this model, not known-broken.
> **Step execution note:** Helios supports single-request step execution only;
> use `max_num_seqs=1`.
@@ -175,7 +183,7 @@ The following tables show which models support each feature:
**Frame Interpolation Support**
- **Supported**: Wan2.2 text-to-video, image-to-video, and TI2V pipelines
-- **Not supported**: Wan2.1-VACE, LTX-2, LTX-2.3, LTX-2.5, Helios, HunyuanVideo-1.5, DreamID-Omni
+- **Not supported**: Wan2.1-VACE, LTX-2, LTX-2.3, LTX-2.5, Helios, HunyuanVideo-1.5, DreamID-Omni, SANA-WM
### AudioGen
diff --git a/recipes/NVIDIA/SANA-WM.md b/recipes/NVIDIA/SANA-WM.md
new file mode 100644
index 000000000..2926cc46f
--- /dev/null
+++ b/recipes/NVIDIA/SANA-WM.md
@@ -0,0 +1,193 @@
+# SANA-WM
+
+> Camera-controllable first-frame image-to-video world model.
+
+## Summary
+
+- Vendor: Efficient-Large-Model / NVlabs SANA
+- Model: `BBBBruce/SANA-WM_bidirectional-stage1-diffusers` (standard diffusers layout, converted offline from the NVlabs release; Stage-1 transformer + VAE only)
+- Task: First-frame image-to-video generation with camera control
+- Mode: Online serving with the OpenAI-compatible video API
+- Model weights: about 13 GB for the Stage-1 transformer (10 GB) and VAE (2.3 GB)
+- Local disk: reserve about 40 GB for the Hugging Face cache and runtime artifacts
+- Recommended GPU: 24 GB or larger CUDA GPU
+- Maintainer: Community
+
+## When to use this recipe
+
+Use this recipe when you want to serve SANA-WM through `/v1/videos` or
+`/v1/videos/sync`. The model takes a text prompt, a first-frame image, and
+either an action DSL string or explicit camera poses. vLLM-Omni serves the
+SANA-WM Stage-1 DiT, decoded through the SANA VAE. The optional LTX-2 refiner
+stage is not supported by this integration; it is a planned follow-up.
+
+## References
+
+- Upstream model card:
+- Video API: [`docs/serving/videos_api.md`](../../docs/serving/videos_api.md)
+
+## Hardware Support
+
+## GPU
+
+### 1x NVIDIA RTX PRO 6000 Blackwell 96GB
+
+#### Capacity
+
+- Model storage: the Stage-1 transformer is about 10 GB and the VAE about
+ 2.3 GB. The Gemma text encoder is a separate 4.9 GB repo. The engine prefetches
+ the whole model repo at startup (`allow_patterns=["*"]`), which is why the
+ Stage-1 weights live in their own repo — the two-stage one carries an
+ additional 84 GB `refiner/` that this path never loads.
+- Text encoder: the pipeline tries `google/gemma-2-2b-it` first, then falls back
+ to the ungated mirror `Efficient-Large-Model/gemma-2-2b-it`. The first repo is
+ gated, so without an accepted licence and `hf auth login` you will see one
+ failed load before the fallback succeeds — that is expected, not an error to
+ chase. Set `VLLM_OMNI_SANA_WM_STAGE1_TEXT_ENCODER` to pin a specific repo or a
+ local path and skip both.
+- Disk sizing: provision about 40 GB of local disk or Hugging Face cache volume
+ so the model, temporary downloads, and generated artifacts fit without cache
+ eviction.
+- GPU sizing: the default 1280x704, 161-frame, 60-step serving profile peaks at
+ 22.6 GB of device memory and takes about 133 s to generate on one RTX PRO
+ 6000 Blackwell. The peak lands in the VAE decode, which is why the pipeline
+ forces VAE tiling on regardless of `vae_use_tiling` — without it the same
+ request costs about 9 GB more, and a 321-frame one OOMs outright. On smaller
+ GPUs, lower `width`, `height`, or `num_frames` before serving production
+ requests.
+
+#### Environment
+
+- OS: Linux
+- Python: 3.10+
+- Driver / runtime: NVIDIA driver with CUDA runtime supported by your PyTorch
+ build
+- Recommended operator library: Triton, installed through the vLLM/vLLM-Omni
+ Python environment
+- vLLM version: Match the repository requirements for your checkout
+- vLLM-Omni version or commit: Use the commit you are deploying from
+
+#### Command
+
+The repo ships the standard Diffusers layout (`model_index.json` +
+`transformer/`, `vae/`), and its `model_index.json` names `SanaWmPipeline`, so
+the pipeline class resolves on its own.
+
+```bash
+CUDA_VISIBLE_DEVICES=0 \
+vllm serve BBBBruce/SANA-WM_bidirectional-stage1-diffusers \
+ --omni \
+ --host 0.0.0.0 \
+ --port 8091
+```
+
+No deploy config: single-stage diffusion models are deliberately absent from
+`OMNI_PIPELINES` (`vllm_omni/config/pipeline_registry.py`), so stage resolution
+falls back to the default stage config and a YAML's stage settings — including
+`default_sampling_params` — would not be applied. The production generation
+settings therefore live in the model (`num_inference_steps=60`,
+`guidance_scale=5.0`); a request that omits a field gets them. The examples
+below still pass every field explicitly so the numbers are visible.
+
+If you point this at the older two-stage repo
+(`BBBBruce/SANA-WM_bidirectional-diffusers`), startup fails with `Model class
+SanaWmTwoStagesPipeline not found in diffusion model registry`, because that
+repo's `model_index.json` names a class this build does not register. Add
+`--model-class-name SanaWmPipeline` to override it.
+
+#### Verification
+
+Use a short smoke request first:
+
+```bash
+curl -sS -X POST http://localhost:8091/v1/videos/sync \
+ -H "Accept: video/mp4" \
+ -F "prompt=A slow forward camera move through a quiet city street." \
+ -F "negative_prompt=blurry, low quality, distorted, watermark" \
+ -F "input_reference=@/path/to/first_frame.png;type=image/png" \
+ -F "width=1280" \
+ -F "height=704" \
+ -F "num_frames=9" \
+ -F "fps=16" \
+ -F "num_inference_steps=2" \
+ -F "guidance_scale=5.0" \
+ -F "seed=42" \
+ --form-string 'extra_params={"sana_wm":{"action":"w-8","translation_speed":0.055,"rotation_speed_deg":1.2,"intrinsics":{"fx":640,"fy":640,"cx":640,"cy":352}}}' \
+ -o sana_wm_smoke.mp4
+```
+
+For a production-length request, note that the action durations must sum to
+`num_frames - 1` — the rollout includes the identity start pose — and a
+mismatch is rejected rather than padded or truncated:
+
+```bash
+curl -sS -X POST http://localhost:8091/v1/videos/sync \
+ -H "Accept: video/mp4" \
+ -F "prompt=A slow forward camera move through a quiet city street." \
+ -F "negative_prompt=blurry, low quality, distorted, watermark" \
+ -F "input_reference=@/path/to/first_frame.png;type=image/png" \
+ -F "width=1280" \
+ -F "height=704" \
+ -F "num_frames=161" \
+ -F "fps=16" \
+ -F "num_inference_steps=60" \
+ -F "guidance_scale=5.0" \
+ -F "seed=42" \
+ --form-string 'extra_params={"sana_wm":{"action":"w-160","translation_speed":0.055,"rotation_speed_deg":1.2,"intrinsics":{"fx":640,"fy":640,"cx":640,"cy":352}}}' \
+ -o sana_wm_output.mp4
+```
+
+Use `POST /v1/videos` instead when you want job storage and polling rather than
+inline MP4 bytes. It accepts the same form fields as `/v1/videos/sync`.
+
+```bash
+create_response=$(curl -sS -X POST http://localhost:8091/v1/videos \
+ -H "Accept: application/json" \
+ -F "prompt=A slow forward camera move through a quiet city street." \
+ -F "negative_prompt=blurry, low quality, distorted, watermark" \
+ -F "input_reference=@/path/to/first_frame.png;type=image/png" \
+ -F "width=1280" \
+ -F "height=704" \
+ -F "num_frames=161" \
+ -F "fps=16" \
+ -F "num_inference_steps=60" \
+ -F "guidance_scale=5.0" \
+ -F "seed=42" \
+ --form-string 'extra_params={"sana_wm":{"action":"w-160","translation_speed":0.055,"rotation_speed_deg":1.2,"intrinsics":{"fx":640,"fy":640,"cx":640,"cy":352}}}')
+
+video_id=$(echo "$create_response" | jq -r '.id')
+curl -sS "http://localhost:8091/v1/videos/${video_id}" | jq .
+curl -L "http://localhost:8091/v1/videos/${video_id}/content" -o sana_wm_output.mp4
+```
+
+#### Notes
+
+- Sequence parallelism is not supported. The bidirectional gated delta
+ recurrence carries state across frames, so a rank cannot denoise a slice
+ of the token sequence in isolation; supporting it needs a distributed scan
+ or an all-gather before the GDN blocks.
+
+- `input_reference` is required for the first frame. Use `image_reference` only
+ when you need a JSON-safe image URL or data URL instead of a multipart upload.
+- `sana_wm` must provide exactly one of `action` or `camera`.
+- Action strings use comma-separated `-` segments. Supported
+ keys are `w`, `a`, `s`, `d` for translation and `i`, `j`, `k`, `l` for
+ pitch/yaw rotation. The durations must sum to `num_frames - 1`.
+- Explicit camera control (alternative to `action`): pass
+ `"camera": {"poses": [...]}` where `poses` is a list of `num_frames`
+ camera-to-world 4x4 matrices (row-major, OpenCV `+X right, +Y down, +Z forward`
+ convention), e.g.
+ `extra_params={"sana_wm":{"camera":{"poses":[[[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]], ...]},"intrinsics":{...}}}`.
+ Most callers should prefer `action`; explicit poses exist for callers that
+ already have a per-frame trajectory.
+- Explicit `intrinsics` are recommended and take the mapping form
+ `{"fx":640,"fy":640,"cx":640,"cy":352}` (for 1280x704). This `{fx,fy,cx,cy}`
+ mapping is the only accepted intrinsics form; omit `intrinsics` to derive them
+ from the output resolution. All four values must be finite, and `fx`/`fy`
+ must be positive — the ray map divides by them.
+- The video API returns decoded MP4 bytes and has no `output_type` field, so
+ raw Stage-1 latents are reachable only from the offline API
+ (`OmniDiffusionSamplingParams(output_type="latent")`); see
+ [`tests/e2e/offline_inference/test_sana_wm.py`](../../tests/e2e/offline_inference/test_sana_wm.py).
+ Putting `output_type` in `extra_params` does not work: it lands in
+ `sampling_params.extra_args`, while the pipeline reads the top-level field.
diff --git a/recipes/README.md b/recipes/README.md
index 32c68e8c1..8b93124d0 100644
--- a/recipes/README.md
+++ b/recipes/README.md
@@ -70,6 +70,7 @@ recipes/
| [`OpenMOSS/MOSS-TTS.md`](./OpenMOSS/MOSS-TTS.md) | Online + offline multilingual TTS (MOSS-TTS family, 8B) | 1x H100 80GB |
| [`NVIDIA/Nemotron-Labs-Audex.md`](./NVIDIA/Nemotron-Labs-Audex.md) | TTS / text-to-audio / audio understanding / cascaded S2S (Audex 2B + 30B-A3B) | 1x H100 80GB |
| [`NVIDIA/NemotronLabs-VoiceChat.md`](./NVIDIA/NemotronLabs-VoiceChat.md) | Offline speech-to-speech voice chat (11B, frame-locked 12.5 Hz, 3-stage thinker/talker/code2wav) | 1x H100 80GB |
+| [`NVIDIA/SANA-WM.md`](./NVIDIA/SANA-WM.md) | First-frame image-to-video serving with camera control | 1x 24GB+ CUDA GPU; ~40GB disk |
| [`Qwen/Qwen-Image.md`](./Qwen/Qwen-Image.md) | Text-to-image serving with step-wise continuous batching replay and ModelOpt mixed FP8/NVFP4 | 1x A100 80GB / 2x B200 |
| [`Qwen/Qwen-Image-2512.md`](./Qwen/Qwen-Image-2512.md) | Text-to-image serving with step-wise continuous batching replay and ModelOpt FP8 / mixed FP8/NVFP4 | 1x A800 80GB / 2x B200 |
| [`Qwen/Qwen-Image-Edit.md`](./Qwen/Qwen-Image-Edit.md) | Text-guided single-image editing | 1x or 2x H200 141GB |
diff --git a/tests/diffusion/models/sana_wm/__init__.py b/tests/diffusion/models/sana_wm/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/diffusion/models/sana_wm/test_cam_reduce_scatter.py b/tests/diffusion/models/sana_wm/test_cam_reduce_scatter.py
new file mode 100644
index 000000000..cea8aeb76
--- /dev/null
+++ b/tests/diffusion/models/sana_wm/test_cam_reduce_scatter.py
@@ -0,0 +1,129 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
+
+"""Equivalence tests for SANA-WM's camera-branch reduce-scatter.
+
+``out_proj_cam`` is row-parallel, so every rank produces a partial sum over the
+full hidden width, while the GDN stream it is added to is TP-local. Instead of
+all-reducing and then discarding all but one slice, the branch reduce-scatters
+straight to the local shard. These tests pin the shard each rank ends up with,
+including the bias slice, on both code paths:
+
+* the process-group path, which drives ``torch.distributed.reduce_scatter_tensor``
+ directly because vLLM-Omni's diffusion ``GroupCoordinator`` does not implement
+ ``reduce_scatter``;
+* the all-reduce-and-slice fallback taken when the coordinator exposes no
+ ``device_group``.
+
+CPU-only: the collectives and the rank/world-size lookups are mocked, so a
+single process can simulate an arbitrary TP degree.
+"""
+
+from types import SimpleNamespace
+from unittest.mock import patch
+
+import pytest
+import torch
+
+from vllm_omni.diffusion.models.sana_wm.sana_wm_transformer import SanaWmSelfAttention
+
+pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
+
+_MODULE = "vllm_omni.diffusion.models.sana_wm.sana_wm_transformer"
+
+HIDDEN = 24
+
+
+def _host(bias: torch.Tensor | None) -> SimpleNamespace:
+ """A stand-in ``self`` exposing only what the method reads."""
+ return SimpleNamespace(out_proj_cam=SimpleNamespace(bias=bias))
+
+
+def _fake_reduce_scatter_tensor(rank: int, tp_size: int):
+ """Model ``reduce_scatter_tensor``: split dim 0 and keep chunk ``rank``.
+
+ The test drives a single process, so the other ranks' partials are already
+ folded into the input; scattering is all this has to reproduce.
+ """
+
+ def _impl(output: torch.Tensor, input_: torch.Tensor, group=None) -> None:
+ output.copy_(input_.chunk(tp_size, dim=0)[rank])
+
+ return _impl
+
+
+def _tp_group(with_device_group: bool):
+ return SimpleNamespace(device_group=object() if with_device_group else None)
+
+
+@pytest.mark.parametrize("tp_size", [2, 4])
+@pytest.mark.parametrize("rank", [0, 1])
+def test_process_group_path_matches_all_reduce_then_slice(tp_size: int, rank: int) -> None:
+ """The scattered shard equals the slice the all-reduce path would take."""
+ local_width = HIDDEN // tp_size
+ torch.manual_seed(0)
+ contrib = torch.randn(2, 3, HIDDEN)
+ bias = torch.randn(HIDDEN)
+ reference = torch.zeros(2, 3, local_width)
+
+ with (
+ patch(f"{_MODULE}.get_tensor_model_parallel_world_size", return_value=tp_size),
+ patch(f"{_MODULE}.get_tensor_model_parallel_rank", return_value=rank),
+ patch(f"{_MODULE}.get_tp_group", return_value=_tp_group(True)),
+ patch("torch.distributed.reduce_scatter_tensor", side_effect=_fake_reduce_scatter_tensor(rank, tp_size)),
+ ):
+ out = SanaWmSelfAttention._reduce_scatter_cam_contrib(_host(bias), contrib, reference)
+
+ start = rank * local_width
+ expected = (contrib + bias)[..., start : start + local_width]
+ assert out.shape == (2, 3, local_width)
+ torch.testing.assert_close(out, expected)
+
+
+@pytest.mark.parametrize("rank", [0, 1])
+def test_fallback_path_matches_the_process_group_path(rank: int) -> None:
+ """Without a device_group the method all-reduces and slices, same result."""
+ tp_size = 2
+ local_width = HIDDEN // tp_size
+ torch.manual_seed(1)
+ contrib = torch.randn(2, 3, HIDDEN)
+ bias = torch.randn(HIDDEN)
+ reference = torch.zeros(2, 3, local_width)
+
+ with (
+ patch(f"{_MODULE}.get_tensor_model_parallel_world_size", return_value=tp_size),
+ patch(f"{_MODULE}.get_tensor_model_parallel_rank", return_value=rank),
+ patch(f"{_MODULE}.get_tp_group", return_value=_tp_group(False)),
+ patch(f"{_MODULE}.tensor_model_parallel_all_reduce", side_effect=lambda t: t),
+ ):
+ out = SanaWmSelfAttention._reduce_scatter_cam_contrib(_host(bias), contrib, reference)
+
+ start = rank * local_width
+ torch.testing.assert_close(out, (contrib + bias)[..., start : start + local_width])
+
+
+def test_tp1_adds_bias_without_a_collective() -> None:
+ contrib = torch.randn(2, 3, HIDDEN)
+ bias = torch.randn(HIDDEN)
+
+ with (
+ patch(f"{_MODULE}.get_tensor_model_parallel_world_size", return_value=1),
+ patch("torch.distributed.reduce_scatter_tensor") as collective,
+ ):
+ out = SanaWmSelfAttention._reduce_scatter_cam_contrib(_host(bias), contrib, contrib)
+
+ assert collective.call_count == 0
+ torch.testing.assert_close(out, contrib + bias)
+
+
+def test_rejects_a_width_that_does_not_shard() -> None:
+ """A local width that is not 1/tp of the contribution is a wiring bug."""
+ contrib = torch.randn(1, 2, HIDDEN)
+ reference = torch.zeros(1, 2, HIDDEN // 3) # tp=2 would need HIDDEN // 2
+
+ with (
+ patch(f"{_MODULE}.get_tensor_model_parallel_world_size", return_value=2),
+ patch("torch.distributed.reduce_scatter_tensor"),
+ pytest.raises(ValueError, match="camera contribution width mismatch"),
+ ):
+ SanaWmSelfAttention._reduce_scatter_cam_contrib(_host(None), contrib, reference)
diff --git a/tests/diffusion/models/sana_wm/test_fused_qk_rms_norm.py b/tests/diffusion/models/sana_wm/test_fused_qk_rms_norm.py
new file mode 100644
index 000000000..b01ef02ce
--- /dev/null
+++ b/tests/diffusion/models/sana_wm/test_fused_qk_rms_norm.py
@@ -0,0 +1,111 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
+
+"""Equivalence tests for SANA-WM's TensorParallelRMSNorm q/k fusion.
+
+`fused_qk_rms_norm` packs the q and k per-token sum-of-squares into one tensor
+and issues a single tensor-parallel all-reduce instead of two. These tests lock
+in that it is numerically identical to applying `norm_q`/`norm_k` separately,
+and that it really issues exactly one (fused) all-reduce.
+
+CPU-only: the collective is mocked, so a single process simulates one rank
+(all-reduce is identity). This exercises the cat/split logic a real multi-rank
+run depends on.
+"""
+
+from unittest.mock import patch
+
+import pytest
+import torch
+from torch import nn
+
+from vllm_omni.diffusion.layers.norm import RMSNorm
+from vllm_omni.diffusion.models.sana_wm.sana_wm_transformer import (
+ TensorParallelRMSNorm,
+ fused_qk_rms_norm,
+)
+
+pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
+
+_MODULE = "vllm_omni.diffusion.models.sana_wm.sana_wm_transformer"
+
+
+def _make_norms(dim: int, tp_size: int) -> tuple[TensorParallelRMSNorm, TensorParallelRMSNorm]:
+ """Two norms with distinct random weights so a swapped q/k slice is caught."""
+ torch.manual_seed(0)
+ norm_q = TensorParallelRMSNorm(dim, tp_size=tp_size)
+ norm_k = TensorParallelRMSNorm(dim, tp_size=tp_size)
+ norm_q.weight.data.normal_()
+ norm_k.weight.data.normal_()
+ return norm_q, norm_k
+
+
+def _identity_all_reduce(tensor: torch.Tensor) -> torch.Tensor:
+ """Single-rank all-reduce: the reduction over one rank is the identity."""
+ return tensor
+
+
+def test_fused_matches_separate() -> None:
+ dim = 64
+ norm_q, norm_k = _make_norms(dim, tp_size=2)
+ q = torch.randn(2, 5, dim)
+ k = torch.randn(2, 5, dim)
+
+ with patch(f"{_MODULE}.tensor_model_parallel_all_reduce", side_effect=_identity_all_reduce):
+ ref_q, ref_k = norm_q(q), norm_k(k)
+ fused_q, fused_k = fused_qk_rms_norm(norm_q, norm_k, q, k)
+
+ torch.testing.assert_close(fused_q, ref_q, rtol=0, atol=0)
+ torch.testing.assert_close(fused_k, ref_k, rtol=0, atol=0)
+
+
+def test_fused_issues_one_all_reduce() -> None:
+ dim = 32
+ norm_q, norm_k = _make_norms(dim, tp_size=4)
+ q = torch.randn(1, 3, dim)
+ k = torch.randn(1, 3, dim)
+
+ with patch(f"{_MODULE}.tensor_model_parallel_all_reduce", side_effect=_identity_all_reduce) as collective:
+ fused_qk_rms_norm(norm_q, norm_k, q, k)
+ assert collective.call_count == 1
+ # Both sums travel in one tensor: last dim is 2, not 1.
+ assert collective.call_args.args[0].shape[-1] == 2
+
+ collective.reset_mock()
+ norm_q(q), norm_k(k)
+ assert collective.call_count == 2
+
+
+@pytest.mark.parametrize(
+ "norms",
+ [
+ (RMSNorm(16), RMSNorm(16)),
+ (nn.Identity(), nn.Identity()),
+ ],
+ ids=["plain_rms_norm", "identity"],
+)
+def test_falls_back_for_non_tp_norms(norms: tuple[nn.Module, nn.Module]) -> None:
+ """qk_norm=False and TP=1 both hand in norms the fused path cannot use."""
+ norm_q, norm_k = norms
+ q = torch.randn(2, 4, 16)
+ k = torch.randn(2, 4, 16)
+
+ with patch(f"{_MODULE}.tensor_model_parallel_all_reduce", side_effect=_identity_all_reduce) as collective:
+ fused_q, fused_k = fused_qk_rms_norm(norm_q, norm_k, q, k)
+ assert collective.call_count == 0
+
+ torch.testing.assert_close(fused_q, norm_q(q), rtol=0, atol=0)
+ torch.testing.assert_close(fused_k, norm_k(k), rtol=0, atol=0)
+
+
+def test_tp1_skips_the_collective() -> None:
+ norm_q, norm_k = _make_norms(16, tp_size=1)
+ q = torch.randn(2, 4, 16)
+ k = torch.randn(2, 4, 16)
+
+ with patch(f"{_MODULE}.tensor_model_parallel_all_reduce", side_effect=_identity_all_reduce) as collective:
+ fused_q, fused_k = fused_qk_rms_norm(norm_q, norm_k, q, k)
+ assert collective.call_count == 0
+
+ torch.testing.assert_close(fused_q, norm_q(q), rtol=0, atol=0)
+ torch.testing.assert_close(fused_k, norm_k(k), rtol=0, atol=0)
diff --git a/tests/diffusion/models/sana_wm/test_request.py b/tests/diffusion/models/sana_wm/test_request.py
new file mode 100644
index 000000000..7e40eedaf
--- /dev/null
+++ b/tests/diffusion/models/sana_wm/test_request.py
@@ -0,0 +1,142 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
+"""Unit tests for SANA-WM request normalization."""
+
+import pytest
+
+from vllm_omni.diffusion.models.sana_wm.request import (
+ SANA_WM_DEFAULT_HEIGHT,
+ SANA_WM_DEFAULT_NUM_FRAMES,
+ SANA_WM_DEFAULT_WIDTH,
+ normalize_sana_wm_payload,
+)
+
+pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
+
+
+def _prompt(**sana_wm):
+ """A valid request, with the action rollout kept in step with num_frames."""
+ block = {"num_frames": SANA_WM_DEFAULT_NUM_FRAMES, **sana_wm}
+ block.setdefault("action", f"w-{block['num_frames'] - 1}")
+ return {
+ "prompt": "a quiet city street",
+ "multi_modal_data": {"image": object()},
+ "sana_wm": block,
+ }
+
+
+def _identity_poses(count):
+ return [[[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]] for _ in range(count)]
+
+
+def _camera_prompt(**camera):
+ prompt = _prompt(num_frames=9)
+ prompt["sana_wm"].pop("action")
+ prompt["sana_wm"]["camera"] = {"poses": _identity_poses(9), **camera}
+ return prompt
+
+
+def test_defaults_are_latent_aligned():
+ payload = normalize_sana_wm_payload(_prompt())["additional_information"]["sana_wm"]
+ assert payload["num_frames"] == SANA_WM_DEFAULT_NUM_FRAMES
+ assert payload["height"] == SANA_WM_DEFAULT_HEIGHT
+ assert payload["width"] == SANA_WM_DEFAULT_WIDTH
+
+
+@pytest.mark.parametrize("num_frames", [9, 33, 161])
+def test_accepts_aligned_geometry(num_frames):
+ payload = normalize_sana_wm_payload(_prompt(num_frames=num_frames))["additional_information"]["sana_wm"]
+ assert payload["num_frames"] == num_frames
+
+
+@pytest.mark.parametrize("num_frames", [8, 10, 160])
+def test_rejects_unaligned_num_frames(num_frames):
+ # The VAE compresses 8x temporally; an unaligned count would be floored.
+ with pytest.raises(ValueError, match="num_frames"):
+ normalize_sana_wm_payload(_prompt(num_frames=num_frames))
+
+
+@pytest.mark.parametrize(("height", "width"), [(700, 1280), (704, 1270), (703, 1279)])
+def test_rejects_unaligned_resolution(height, width):
+ with pytest.raises(ValueError, match="divisible by 32"):
+ normalize_sana_wm_payload(_prompt(height=height, width=width))
+
+
+def test_normalization_is_idempotent():
+ once = normalize_sana_wm_payload(_prompt(num_frames=33))
+ twice = normalize_sana_wm_payload(once)
+ assert once["additional_information"]["sana_wm"] == twice["additional_information"]["sana_wm"]
+
+
+def test_rejects_action_shorter_than_num_frames():
+ # Used to shrink the camera tensor silently, then fail deep in the
+ # transformer on a camera/latent frame mismatch.
+ with pytest.raises(ValueError, match="rolls out 33 frames but num_frames is 161"):
+ normalize_sana_wm_payload(_prompt(action="w-32"))
+
+
+def test_rejects_action_longer_than_num_frames():
+ with pytest.raises(ValueError, match="rolls out 161 frames but num_frames is 9"):
+ normalize_sana_wm_payload(_prompt(num_frames=9, action="w-160"))
+
+
+def test_rejects_oversized_action_without_expanding_it():
+ # A 12-byte request body: expanding this rollout before checking the frame
+ # count allocated ~16 GB. Rejection has to be arithmetic, so this test also
+ # fails (by hanging/OOM) if the duration is ever expanded again.
+ with pytest.raises(ValueError, match="rolls out 1000000001 frames"):
+ normalize_sana_wm_payload(_prompt(action="w-1000000000"))
+
+
+@pytest.mark.parametrize("action", ["", " ", "w-0", "w-abc", "w", "x-4"])
+def test_rejects_malformed_action(action):
+ with pytest.raises(ValueError):
+ normalize_sana_wm_payload(_prompt(action=action))
+
+
+@pytest.mark.parametrize("value", [0, -1, float("nan"), float("inf")])
+def test_rejects_unusable_focal_length(value):
+ # compute_raymap divides by fx/fy; a zero or non-finite focal length turns
+ # the whole Plucker map into NaN and the response into a NaN video.
+ intrinsics = {"fx": value, "fy": 640, "cx": 640, "cy": 352}
+ with pytest.raises(ValueError, match="fx"):
+ normalize_sana_wm_payload(_prompt(intrinsics=intrinsics))
+
+
+@pytest.mark.parametrize("value", [float("nan"), float("inf")])
+def test_rejects_non_finite_principal_point(value):
+ intrinsics = {"fx": 640, "fy": 640, "cx": value, "cy": 352}
+ with pytest.raises(ValueError, match="must be finite"):
+ normalize_sana_wm_payload(_prompt(intrinsics=intrinsics))
+
+
+@pytest.mark.parametrize("value", [float("nan"), float("inf")])
+def test_rejects_non_finite_speeds(value):
+ # `parsed <= 0` is False for NaN, so the positivity check alone let it pass.
+ with pytest.raises(ValueError, match="must be finite"):
+ normalize_sana_wm_payload(_prompt(translation_speed=value))
+
+
+def test_rejects_non_finite_pose_entry():
+ prompt = _camera_prompt()
+ prompt["sana_wm"]["camera"]["poses"][3][2][1] = float("nan")
+ with pytest.raises(ValueError, match="must be finite"):
+ normalize_sana_wm_payload(prompt)
+
+
+def test_accepts_default_camera_metadata():
+ payload = normalize_sana_wm_payload(_camera_prompt())["additional_information"]["sana_wm"]
+ assert payload["camera"]["poses"]
+ # The unread fields are not carried into the canonical payload.
+ assert set(payload["camera"]) == {"poses"}
+
+
+@pytest.mark.parametrize(
+ ("field", "value"),
+ [("format", "w2c_4x4"), ("coordinate_system", "opengl")],
+)
+def test_rejects_unsupported_camera_convention(field, value):
+ # Nothing downstream reads these, so accepting one would mean interpreting
+ # the poses under the wrong convention.
+ with pytest.raises(ValueError, match=field.split("_")[0]):
+ normalize_sana_wm_payload(_camera_prompt(**{field: value}))
diff --git a/tests/e2e/offline_inference/test_sana_wm.py b/tests/e2e/offline_inference/test_sana_wm.py
new file mode 100644
index 000000000..8b1c460c2
--- /dev/null
+++ b/tests/e2e/offline_inference/test_sana_wm.py
@@ -0,0 +1,159 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
+
+from __future__ import annotations
+
+import os
+from typing import Any
+
+import numpy as np
+import pytest
+
+pytestmark = [
+ pytest.mark.advanced_model,
+ pytest.mark.diffusion,
+ pytest.mark.gpu,
+]
+
+
+def _make_sana_wm_e2e_image() -> Any:
+ from PIL import Image
+
+ from vllm_omni.diffusion.models.sana_wm import SANA_WM_OUTPUT_HEIGHT, SANA_WM_OUTPUT_WIDTH
+
+ return Image.new("RGB", (SANA_WM_OUTPUT_WIDTH, SANA_WM_OUTPUT_HEIGHT), (96, 128, 160))
+
+
+def _sana_wm_e2e_tensor_parallel_size() -> int:
+ return int(
+ os.environ.get(
+ "SANA_WM_E2E_TENSOR_PARALLEL_SIZE",
+ os.environ.get("SANA_WM_E2E_TP", "1"),
+ )
+ )
+
+
+def _coerce_video_array(video: Any) -> np.ndarray:
+ import torch
+
+ if isinstance(video, list):
+ assert video, "SANA-WM e2e produced an empty video list."
+ video = video[0]
+ if isinstance(video, torch.Tensor):
+ video = video.detach().cpu().float().numpy()
+ video_array = np.asarray(video)
+ if video_array.ndim == 5:
+ assert video_array.shape[0] == 1
+ video_array = video_array[0]
+ assert video_array.ndim == 4
+ return video_array
+
+
+def _assert_sana_wm_e2e_shape(
+ video: np.ndarray,
+ *,
+ output_type: str,
+ num_frames: int,
+) -> None:
+ if output_type == "latent":
+ # Latent output is channel-first: (C, T, H, W) after removing the
+ # batch dimension. Stage-1 emits LTX-2 latents with 128 channels.
+ assert video.shape[0] == 128
+ assert 0 < video.shape[1] <= num_frames
+ return
+ assert 0 < video.shape[0] <= num_frames
+
+
+def _run_sana_wm_e2e(*, output_type: str) -> np.ndarray:
+ import torch
+
+ from vllm_omni.diffusion.models.sana_wm import (
+ SANA_WM_MODEL_ID,
+ SANA_WM_OUTPUT_HEIGHT,
+ SANA_WM_OUTPUT_WIDTH,
+ )
+ from vllm_omni.entrypoints.omni import Omni
+ from vllm_omni.inputs.data import OmniDiffusionSamplingParams
+ from vllm_omni.outputs import OmniRequestOutput
+
+ if not torch.cuda.is_available():
+ pytest.skip("Sana-WM e2e requires CUDA.")
+
+ model = os.environ.get("SANA_WM_E2E_MODEL", SANA_WM_MODEL_ID)
+ model_class_name = os.environ.get("SANA_WM_E2E_MODEL_CLASS", "SanaWmPipeline")
+ num_frames = int(os.environ.get("SANA_WM_E2E_NUM_FRAMES", "9"))
+ stage1_steps = int(
+ os.environ.get(
+ "SANA_WM_E2E_STAGE1_STEPS",
+ os.environ.get("SANA_WM_E2E_NUM_INFERENCE_STEPS", "1"),
+ )
+ )
+ action = os.environ.get("SANA_WM_E2E_ACTION", f"w-{max(num_frames - 1, 1)}")
+ tensor_parallel_size = _sana_wm_e2e_tensor_parallel_size()
+ image = _make_sana_wm_e2e_image()
+ omni = Omni(
+ model=model,
+ model_class_name=model_class_name,
+ enforce_eager=True,
+ tensor_parallel_size=tensor_parallel_size,
+ )
+ extra_args: dict[str, Any] = {}
+ # The native Stage-1 path is latent-token capped; raise it for full-size
+ # e2e runs (704x1280 exceeds the default 4096-token cap).
+ native_max_tokens = os.environ.get("SANA_WM_E2E_NATIVE_MAX_TOKENS", "").strip()
+ if native_max_tokens:
+ extra_args["sana_wm_native_max_tokens"] = int(native_max_tokens)
+ # Return the requested output type so the shape assertion matches the
+ # pipeline output instead of the raw Stage-1 latent default.
+
+ output = omni.generate(
+ {
+ "prompt": "A slow forward camera move through a quiet city street.",
+ "multi_modal_data": {"image": image},
+ "sana_wm": {
+ "action": action,
+ "num_frames": num_frames,
+ "translation_speed": 0.055,
+ "rotation_speed_deg": 1.2,
+ # Avoid the optional Pi3X dependency by passing deterministic
+ # camera intrinsics directly.
+ "intrinsics": {
+ "fx": SANA_WM_OUTPUT_WIDTH / 2,
+ "fy": SANA_WM_OUTPUT_WIDTH / 2,
+ "cx": SANA_WM_OUTPUT_WIDTH / 2,
+ "cy": SANA_WM_OUTPUT_HEIGHT / 2,
+ },
+ },
+ },
+ OmniDiffusionSamplingParams(
+ height=SANA_WM_OUTPUT_HEIGHT,
+ width=SANA_WM_OUTPUT_WIDTH,
+ num_frames=num_frames,
+ seed=0,
+ fps=16,
+ num_inference_steps=stage1_steps,
+ guidance_scale=1.0,
+ guidance_scale_provided=True,
+ output_type=output_type,
+ extra_args=extra_args,
+ ),
+ )
+
+ request_output = output[0] if isinstance(output, list) else output
+ assert isinstance(request_output, OmniRequestOutput)
+ assert request_output.error is None
+ assert request_output.images
+
+ frames = request_output.images[0]
+ video = _coerce_video_array(frames)
+ _assert_sana_wm_e2e_shape(video, output_type=output_type, num_frames=num_frames)
+ return video
+
+
+def test_sana_wm_native_generates_video() -> None:
+ output_type = os.environ.get("SANA_WM_E2E_OUTPUT_TYPE", "np")
+ video = _run_sana_wm_e2e(output_type=output_type)
+ if output_type == "latent":
+ assert video.ndim == 4
+ else:
+ assert video.shape[-1] in (3, 4)
diff --git a/tests/e2e/online_serving/test_sana_wm.py b/tests/e2e/online_serving/test_sana_wm.py
new file mode 100644
index 000000000..883e4c367
--- /dev/null
+++ b/tests/e2e/online_serving/test_sana_wm.py
@@ -0,0 +1,106 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
+
+"""
+Online serving smoke for SANA-WM image-to-video (first-frame I2V via ``/v1/videos``).
+
+Boots the server straight from the model id — the Stage-1 repo's
+``model_index.json`` names ``SanaWmPipeline``, so the class resolves on its own
+— submits one async ``/v1/videos`` job with a first-frame reference image, and
+asserts the job completes and returns video bytes.
+
+From ``tests/``::
+
+ pytest -s -v e2e/online_serving/test_sana_wm.py -m "advanced_model and diffusion" --run-level=advanced_model
+"""
+
+import base64
+import json
+import os
+from io import BytesIO
+
+import pytest
+
+from tests.helpers.mark import hardware_marks
+from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler
+from vllm_omni.diffusion.models.sana_wm import (
+ SANA_WM_MODEL_ID,
+ SANA_WM_OUTPUT_HEIGHT,
+ SANA_WM_OUTPUT_WIDTH,
+)
+
+os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
+
+MODEL = SANA_WM_MODEL_ID
+PROMPT = "A slow forward camera move through a quiet city street."
+NEGATIVE_PROMPT = "blurry, low quality, distorted, watermark"
+
+# Smoke knobs: a handful of frames / steps so the job finishes quickly.
+SMOKE_NUM_FRAMES = 9
+SMOKE_NUM_INFERENCE_STEPS = 2
+# First-frame I2V needs a camera trajectory; "w-" is the forward-move action DSL.
+# Passed via the ``sana_wm`` form field (a JSON string parsed server-side); explicit
+# intrinsics avoid the optional Pi3X camera-calibration dependency.
+SANA_WM_PARAMS = {
+ "action": f"w-{SMOKE_NUM_FRAMES - 1}",
+ "translation_speed": 0.055,
+ "rotation_speed_deg": 1.2,
+ "intrinsics": {
+ "fx": SANA_WM_OUTPUT_WIDTH / 2,
+ "fy": SANA_WM_OUTPUT_WIDTH / 2,
+ "cx": SANA_WM_OUTPUT_WIDTH / 2,
+ "cy": SANA_WM_OUTPUT_HEIGHT / 2,
+ },
+}
+
+SINGLE_CARD_FEATURE_MARKS = hardware_marks(res={"cuda": "H100"})
+
+
+def _first_frame_data_url() -> str:
+ """A solid-color first frame encoded as a PNG ``data:`` URL for I2V."""
+ from PIL import Image
+
+ image = Image.new("RGB", (SANA_WM_OUTPUT_WIDTH, SANA_WM_OUTPUT_HEIGHT), (96, 128, 160))
+ buffer = BytesIO()
+ image.save(buffer, format="PNG")
+ encoded = base64.b64encode(buffer.getvalue()).decode("ascii")
+ return f"data:image/png;base64,{encoded}"
+
+
+def _get_diffusion_feature_cases(model: str):
+ """Single default server row."""
+ return [
+ pytest.param(
+ OmniServerParams(model=model),
+ id="default",
+ marks=SINGLE_CARD_FEATURE_MARKS,
+ ),
+ ]
+
+
+@pytest.mark.advanced_model
+@pytest.mark.diffusion
+@pytest.mark.parametrize("omni_server", _get_diffusion_feature_cases(MODEL), indirect=True)
+def test_image_to_video_001(omni_server: OmniServer, openai_client: OpenAIClientHandler) -> None:
+ """Default SANA-WM I2V smoke: async ``/v1/videos`` job completes and returns video bytes."""
+ request_config = {
+ "model": omni_server.model,
+ "image_reference": _first_frame_data_url(),
+ "form_data": {
+ "prompt": PROMPT,
+ "negative_prompt": NEGATIVE_PROMPT,
+ # The camera payload rides inside extra_params; there is no
+ # top-level sana_wm form field on the video endpoints, so sending
+ # one is silently dropped and the request fails in the
+ # preprocessor for missing camera control.
+ "extra_params": json.dumps({"sana_wm": SANA_WM_PARAMS}),
+ "height": SANA_WM_OUTPUT_HEIGHT,
+ "width": SANA_WM_OUTPUT_WIDTH,
+ "num_frames": SMOKE_NUM_FRAMES,
+ "fps": 16,
+ "num_inference_steps": SMOKE_NUM_INFERENCE_STEPS,
+ "guidance_scale": 5.0,
+ "seed": 42,
+ },
+ }
+ openai_client.send_video_diffusion_request(request_config)
diff --git a/tests/entrypoints/openai_api/test_video_server.py b/tests/entrypoints/openai_api/test_video_server.py
index b9b7df4df..a83edfcb9 100644
--- a/tests/entrypoints/openai_api/test_video_server.py
+++ b/tests/entrypoints/openai_api/test_video_server.py
@@ -2168,6 +2168,31 @@ def test_sync_sampling_params_pass_through(test_client, mocker: MockerFixture):
assert captured.quality == "high"
+def test_sync_sana_wm_extra_params_payload_passes_to_engine_prompt(test_client, mocker: MockerFixture):
+ _mock_encode_video_bytes(mocker)
+ response = test_client.post(
+ "/v1/videos/sync",
+ data={
+ "prompt": "drive forward",
+ "extra_params": json.dumps(
+ {
+ "sana_wm": {"action": "d-4", "rotation_speed_deg": 1.5},
+ "sana_wm_native_max_tokens": 30000,
+ }
+ ),
+ },
+ )
+
+ assert response.status_code == 200
+ engine = test_client.app.state.openai_serving_video._engine_client
+ captured_params = engine.captured_sampling_params_list[0]
+ # The camera block reaches the model through extra_args; the Sana preprocess
+ # hook is what lifts it onto the prompt.
+ assert captured_params.extra_args["sana_wm"]["action"] == "d-4"
+ assert captured_params.extra_args["sana_wm"]["rotation_speed_deg"] == 1.5
+ assert captured_params.extra_args["sana_wm_native_max_tokens"] == 30000
+
+
def test_sync_frame_interpolation_params_pass_to_sampling_params(test_client, mocker: MockerFixture):
"""Frame interpolation parameters should be forwarded on the sync path."""
encode_mock = _mock_encode_video_bytes(mocker)
diff --git a/tests/model_tests/diffusion/test_alignment.py b/tests/model_tests/diffusion/test_alignment.py
index 7faf175f8..737eba753 100644
--- a/tests/model_tests/diffusion/test_alignment.py
+++ b/tests/model_tests/diffusion/test_alignment.py
@@ -80,6 +80,7 @@ EXCLUDED_MODELS = [
"Pi0Pipeline",
"SoulXSingerPipeline",
"SoulXSingerSVCPipeline",
+ "SanaWmPipeline",
]
diff --git a/vllm_omni/diffusion/model_metadata.py b/vllm_omni/diffusion/model_metadata.py
index ecc53cdc6..e11a7541e 100644
--- a/vllm_omni/diffusion/model_metadata.py
+++ b/vllm_omni/diffusion/model_metadata.py
@@ -56,6 +56,10 @@ _DIFFUSION_MODEL_METADATA: dict[str, DiffusionModelMetadata] = {
"WanImageToVideoPipeline": DiffusionModelMetadata(attention_mask_free=True),
"WanVACEPipeline": DiffusionModelMetadata(attention_mask_free=True),
"WanS2VPipeline": DiffusionModelMetadata(attention_mask_free=True),
+ "SanaWmPipeline": DiffusionModelMetadata(
+ supports_multimodal_inputs=True,
+ max_multimodal_image_inputs=1,
+ ),
}
diff --git a/vllm_omni/diffusion/models/sana_wm/__init__.py b/vllm_omni/diffusion/models/sana_wm/__init__.py
new file mode 100644
index 000000000..f6f776194
--- /dev/null
+++ b/vllm_omni/diffusion/models/sana_wm/__init__.py
@@ -0,0 +1,25 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
+"""SANA-WM diffusion model integration."""
+
+from vllm_omni.diffusion.models.sana_wm.config import SanaWmConfig
+from vllm_omni.diffusion.models.sana_wm.pipeline_sana_wm import (
+ SANA_WM_MODEL_ID,
+ SANA_WM_OUTPUT_HEIGHT,
+ SANA_WM_OUTPUT_WIDTH,
+ SanaWmPipeline,
+ get_sana_wm_pre_process_func,
+)
+from vllm_omni.diffusion.models.sana_wm.request import normalize_sana_wm_payload
+from vllm_omni.diffusion.models.sana_wm.sana_wm_transformer import SanaWmTransformer3DModel
+
+__all__ = [
+ "SANA_WM_MODEL_ID",
+ "SANA_WM_OUTPUT_HEIGHT",
+ "SANA_WM_OUTPUT_WIDTH",
+ "SanaWmConfig",
+ "SanaWmPipeline",
+ "SanaWmTransformer3DModel",
+ "get_sana_wm_pre_process_func",
+ "normalize_sana_wm_payload",
+]
diff --git a/vllm_omni/diffusion/models/sana_wm/camera_control.py b/vllm_omni/diffusion/models/sana_wm/camera_control.py
new file mode 100644
index 000000000..914fbc0d3
--- /dev/null
+++ b/vllm_omni/diffusion/models/sana_wm/camera_control.py
@@ -0,0 +1,356 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
+"""Camera-control helpers for SANA-WM.
+
+This module mirrors the public NVlabs/Sana SANA-WM camera preparation path:
+WASD/IJKL action rollout or explicit camera-to-world poses are converted into
+relative poses, per-latent-frame ray metadata, and per-VAE-chunk Plucker maps.
+Model-local projection layers consume these tensors in the Stage-1 DiT.
+"""
+
+from __future__ import annotations
+
+import math
+from dataclasses import dataclass
+from typing import Any
+
+import numpy as np
+import torch
+
+SANA_WM_DEFAULT_TRANSLATION_SPEED = 0.05
+SANA_WM_DEFAULT_ROTATION_SPEED_DEG = 1.2
+SANA_WM_DEFAULT_PITCH_LIMIT_DEG = 85.0
+SANA_WM_ALLOWED_ACTION_KEYS = frozenset("wasdijkl")
+SANA_WM_DEFAULT_VAE_STRIDE = (8, 32, 32)
+
+
+@dataclass(frozen=True)
+class SanaWmCameraCondition:
+ poses: Any | None = None
+ intrinsics: Any | None = None
+ action: str | None = None
+ num_frames: int | None = None
+ height: int = 704
+ width: int = 1280
+ translation_speed: float = SANA_WM_DEFAULT_TRANSLATION_SPEED
+ rotation_speed_deg: float = SANA_WM_DEFAULT_ROTATION_SPEED_DEG
+ pitch_limit_deg: float = SANA_WM_DEFAULT_PITCH_LIMIT_DEG
+
+
+def _rot_x(angle_rad: float) -> np.ndarray:
+ c, s = np.cos(angle_rad), np.sin(angle_rad)
+ return np.array([[1.0, 0.0, 0.0], [0.0, c, -s], [0.0, s, c]], dtype=np.float64)
+
+
+def _rot_y(angle_rad: float) -> np.ndarray:
+ c, s = np.cos(angle_rad), np.sin(angle_rad)
+ return np.array([[c, 0.0, s], [0.0, 1.0, 0.0], [-s, 0.0, c]], dtype=np.float64)
+
+
+def _parse_action_segments(action: str) -> list[tuple[list[str], int]]:
+ """Parse an action string into ``(keys, duration)`` pairs without expanding it.
+
+ A single segment names an unbounded rollout (``"w-1000000000"`` is valid
+ syntax), so expanding here would allocate one list entry per frame before
+ anything checks the frame count. Callers total the durations and validate
+ against ``num_frames`` first; only :func:`_parse_action_string` expands.
+ """
+ cleaned = "".join(action.replace(",", ",").split())
+ if not cleaned:
+ raise ValueError("Sana-WM action string is empty.")
+
+ segments: list[tuple[list[str], int]] = []
+ for segment in cleaned.split(","):
+ if not segment or "-" not in segment:
+ raise ValueError(f"Invalid Sana-WM action segment {segment!r}: expected '-'.")
+ keys_part, duration = segment.rsplit("-", 1)
+ if not duration.isdigit() or int(duration) <= 0:
+ raise ValueError(f"Sana-WM action segment {segment!r} has a non-positive duration {duration!r}.")
+ keys_lower = keys_part.lower()
+ if keys_lower == "none":
+ keys: list[str] = []
+ else:
+ bad = sorted({key for key in keys_lower if key not in SANA_WM_ALLOWED_ACTION_KEYS})
+ if bad:
+ allowed = "".join(sorted(SANA_WM_ALLOWED_ACTION_KEYS))
+ raise ValueError(f"Sana-WM action segment {segment!r} contains unknown keys {bad}; allowed: {allowed}.")
+ keys = sorted(set(keys_lower))
+ segments.append((keys, int(duration)))
+ return segments
+
+
+def action_rollout_num_frames(action: str) -> int:
+ """Frames ``action`` rolls out: the identity start pose plus one per step.
+
+ Totals the durations arithmetically, so an oversized request is rejected
+ before anything allocates per-frame state.
+ """
+ return sum(duration for _, duration in _parse_action_segments(action)) + 1
+
+
+def _parse_action_string(action: str) -> list[list[str]]:
+ per_frame: list[list[str]] = []
+ for keys, duration in _parse_action_segments(action):
+ per_frame.extend([keys] * duration)
+ return per_frame
+
+
+def action_string_to_c2w(
+ action: str,
+ *,
+ translation_speed: float = SANA_WM_DEFAULT_TRANSLATION_SPEED,
+ rotation_speed_deg: float = SANA_WM_DEFAULT_ROTATION_SPEED_DEG,
+ pitch_limit_deg: float = SANA_WM_DEFAULT_PITCH_LIMIT_DEG,
+) -> np.ndarray:
+ """Roll out an OpenCV-convention ``(N + 1, 4, 4)`` c2w trajectory."""
+
+ per_frame = _parse_action_string(action)
+ rotate_rad = math.radians(rotation_speed_deg)
+ pitch_limit_rad = math.radians(pitch_limit_deg)
+ current = np.eye(4, dtype=np.float64)
+ current_pitch = 0.0
+ poses = [current.copy()]
+
+ for keys in per_frame:
+ held = set(keys)
+ rotation = current[:3, :3]
+ translation = current[:3, 3]
+
+ pitch_delta = (rotate_rad if "i" in held else 0.0) - (rotate_rad if "k" in held else 0.0)
+ new_pitch = current_pitch + pitch_delta
+ if -pitch_limit_rad <= new_pitch <= pitch_limit_rad:
+ current_pitch = new_pitch
+ else:
+ pitch_delta = 0.0
+
+ yaw_delta = (rotate_rad if "l" in held else 0.0) - (rotate_rad if "j" in held else 0.0)
+ new_rotation = _rot_y(yaw_delta) @ rotation @ _rot_x(pitch_delta)
+
+ forward = new_rotation[:, 2].copy()
+ forward[1] = 0.0
+ right = new_rotation[:, 0].copy()
+ right[1] = 0.0
+ forward_norm = float(np.linalg.norm(forward))
+ right_norm = float(np.linalg.norm(right))
+ if forward_norm > 0:
+ forward /= forward_norm + 1e-6
+ if right_norm > 0:
+ right /= right_norm + 1e-6
+
+ move = np.zeros(3, dtype=np.float64)
+ if "w" in held:
+ move += forward * translation_speed
+ if "s" in held:
+ move -= forward * translation_speed
+ if "d" in held:
+ move += right * translation_speed
+ if "a" in held:
+ move -= right * translation_speed
+
+ current = np.eye(4, dtype=np.float64)
+ current[:3, :3] = new_rotation
+ current[:3, 3] = translation + move
+ poses.append(current.copy())
+
+ return np.stack(poses, axis=0).astype(np.float32)
+
+
+def get_pose_inverse(transform: torch.Tensor) -> torch.Tensor:
+ """Invert homogeneous rigid transforms with shape ``(..., 4, 4)``."""
+
+ rotation = transform[..., :3, :3]
+ translation = transform[..., :3, 3]
+ rotation_inv = rotation.transpose(-1, -2)
+ translation_inv = -torch.matmul(rotation_inv, translation.unsqueeze(-1)).squeeze(-1)
+ output = torch.eye(4, dtype=transform.dtype, device=transform.device).repeat(transform.shape[:-2] + (1, 1))
+ output[..., :3, :3] = rotation_inv
+ output[..., :3, 3] = translation_inv
+ return output
+
+
+def compute_raymap(
+ intrinsics: torch.Tensor,
+ poses: torch.Tensor,
+ height: int,
+ width: int,
+ *,
+ use_plucker: bool = True,
+) -> torch.Tensor:
+ """Compute SANA-WM geometry ray maps.
+
+ Args:
+ intrinsics: ``(T, 4)`` tensor in ``[fx, fy, cx, cy]`` order.
+ poses: ``(T, 4, 4)`` camera-to-world matrices.
+ height: raymap height in latent pixels.
+ width: raymap width in latent pixels.
+ use_plucker: return ``[direction, moment]`` if true, otherwise
+ ``[origin, direction]``.
+ """
+
+ if intrinsics.ndim != 2 or intrinsics.shape[-1] != 4:
+ raise ValueError(f"Sana-WM intrinsics must have shape (T, 4), got {tuple(intrinsics.shape)}.")
+ if poses.ndim != 3 or poses.shape[-2:] != (4, 4):
+ raise ValueError(f"Sana-WM poses must have shape (T, 4, 4), got {tuple(poses.shape)}.")
+ if intrinsics.shape[0] != poses.shape[0]:
+ raise ValueError("Sana-WM intrinsics and poses must have the same temporal length.")
+
+ frames = intrinsics.shape[0]
+ device = intrinsics.device
+ dtype = intrinsics.dtype
+ y_grid, x_grid = torch.meshgrid(
+ torch.arange(height, device=device, dtype=dtype),
+ torch.arange(width, device=device, dtype=dtype),
+ indexing="ij",
+ )
+ x_grid = x_grid[None].expand(frames, -1, -1)
+ y_grid = y_grid[None].expand(frames, -1, -1)
+
+ fx = intrinsics[:, 0].view(frames, 1, 1)
+ fy = intrinsics[:, 1].view(frames, 1, 1)
+ cx = intrinsics[:, 2].view(frames, 1, 1)
+ cy = intrinsics[:, 3].view(frames, 1, 1)
+ dirs_cam = torch.stack([(x_grid - cx) / fx, (y_grid - cy) / fy, torch.ones_like(x_grid)], dim=-1)
+
+ rotation = poses[:, :3, :3]
+ translation = poses[:, :3, 3]
+ dirs_world = torch.einsum("tij,thwj->thwi", rotation, dirs_cam)
+ dirs_world = dirs_world / torch.norm(dirs_world, dim=-1, keepdim=True).clamp_min(1e-12)
+ origins = translation.view(frames, 1, 1, 3).expand_as(dirs_world)
+
+ if use_plucker:
+ moments = torch.cross(origins, dirs_world, dim=-1)
+ return torch.cat([dirs_world, moments], dim=-1)
+ return torch.cat([origins, dirs_world], dim=-1)
+
+
+def intrinsics_to_vec4_array(intrinsics: Any, *, num_frames: int, height: int, width: int) -> np.ndarray:
+ """Normalize the intrinsics payload to an ``(F, 4)`` ``[fx, fy, cx, cy]`` array.
+
+ Accepts either ``None`` (derive from the output resolution) or the
+ ``{fx, fy, cx, cy}`` mapping — the only form the request contract exposes.
+ """
+
+ if intrinsics is None:
+ focal = float(max(height, width))
+ vec = np.array([focal, focal, width / 2.0, height / 2.0], dtype=np.float32)
+ return np.broadcast_to(vec, (num_frames, 4)).copy()
+
+ if isinstance(intrinsics, dict):
+ vec = np.array(
+ [intrinsics["fx"], intrinsics["fy"], intrinsics["cx"], intrinsics["cy"]],
+ dtype=np.float32,
+ )
+ return np.broadcast_to(vec, (num_frames, 4)).copy()
+
+ raise ValueError("Sana-WM intrinsics must be a {fx, fy, cx, cy} mapping.")
+
+
+def _pack_camera_conditions(
+ poses: torch.Tensor,
+ intrinsics_latent: torch.Tensor,
+ *,
+ num_frames: int,
+ latent_height: int,
+ latent_width: int,
+ vae_time_stride: int,
+) -> dict[str, torch.Tensor]:
+ # This yields exactly the latent frame count the VAE produces:
+ # ``len(arange(0, n, s)) == ceil(n / s) == (n - 1) // s + 1``.
+ time_indices = torch.arange(0, num_frames, vae_time_stride)
+
+ raymap = torch.cat([poses[time_indices].reshape(len(time_indices), -1), intrinsics_latent[time_indices]], dim=-1)
+
+ chunks: list[torch.Tensor] = []
+ spatial_raymap_frames: list[torch.Tensor] = []
+ chunk_starts = time_indices - (vae_time_stride - 1)
+ for start, t_idx in zip(chunk_starts, time_indices):
+ start_idx = max(0, int(start))
+ end_idx = start_idx + vae_time_stride
+ chunk_poses = poses[start_idx:end_idx]
+ chunk_intrinsics = intrinsics_latent[start_idx:end_idx]
+ if chunk_poses.shape[0] < vae_time_stride:
+ pad = vae_time_stride - chunk_poses.shape[0]
+ chunk_poses = torch.cat([chunk_poses, chunk_poses[-1:].repeat(pad, 1, 1)], dim=0)
+ chunk_intrinsics = torch.cat([chunk_intrinsics, chunk_intrinsics[-1:].repeat(pad, 1)], dim=0)
+ plucker = compute_raymap(chunk_intrinsics, chunk_poses, latent_height, latent_width, use_plucker=True)
+ chunks.append(plucker.permute(0, 3, 1, 2).reshape(-1, latent_height, latent_width))
+
+ # Per-frame spatial ray-direction map for raymap_embedder: take the
+ # representative frame in each chunk (the anchor at t_idx) and use only
+ # the direction channels (last 3 of 6 in the origin+direction layout).
+ t = int(t_idx)
+ od = compute_raymap(
+ intrinsics_latent[t : t + 1],
+ poses[t : t + 1],
+ latent_height,
+ latent_width,
+ use_plucker=False,
+ ) # [1, H, W, 6]
+ spatial_raymap_frames.append(od[0, :, :, 3:]) # [H, W, 3] — direction only
+
+ chunk_plucker = torch.stack(chunks).permute(1, 0, 2, 3)
+ # spatial_raymap: [3, F_latent, H, W] — consumed by raymap_embedder
+ spatial_raymap = torch.stack(spatial_raymap_frames).permute(3, 0, 1, 2)
+ return {"raymap": raymap, "chunk_plucker": chunk_plucker, "spatial_raymap": spatial_raymap}
+
+
+def build_plucker_condition(
+ condition: SanaWmCameraCondition,
+ *,
+ vae_stride: tuple[int, int, int] | list[int] = SANA_WM_DEFAULT_VAE_STRIDE,
+) -> dict[str, torch.Tensor]:
+ """Build native Stage-1 camera tensors from normalized request metadata."""
+
+ if bool(condition.action) == (condition.poses is not None):
+ raise ValueError("Sana-WM camera condition requires exactly one of action or poses.")
+
+ if condition.action:
+ poses_c2w = action_string_to_c2w(
+ condition.action,
+ translation_speed=condition.translation_speed,
+ rotation_speed_deg=condition.rotation_speed_deg,
+ pitch_limit_deg=condition.pitch_limit_deg,
+ )
+ else:
+ poses_c2w = np.asarray(condition.poses, dtype=np.float32)
+
+ if poses_c2w.ndim != 3 or poses_c2w.shape[-2:] != (4, 4):
+ raise ValueError(f"Sana-WM c2w poses must have shape (F, 4, 4), got {poses_c2w.shape}.")
+
+ num_frames = condition.num_frames or int(poses_c2w.shape[0])
+ if poses_c2w.shape[0] < num_frames:
+ if condition.action:
+ num_frames = int(poses_c2w.shape[0])
+ else:
+ raise ValueError(f"Sana-WM c2w poses length {poses_c2w.shape[0]} is shorter than num_frames {num_frames}.")
+ poses_c2w = poses_c2w[:num_frames]
+
+ intrinsics = intrinsics_to_vec4_array(
+ condition.intrinsics,
+ num_frames=num_frames,
+ height=condition.height,
+ width=condition.width,
+ )
+
+ vae_time_stride = int(vae_stride[0])
+ vae_spatial_stride = int(vae_stride[-1])
+ latent_height = condition.height // vae_spatial_stride
+ latent_width = condition.width // vae_spatial_stride
+
+ poses = torch.from_numpy(poses_c2w).float()
+ first_inv = get_pose_inverse(poses[0:1]).squeeze(0)
+ poses_rel = torch.matmul(first_inv, poses[1:])
+ poses = torch.cat([torch.eye(4, dtype=poses.dtype).unsqueeze(0), poses_rel], dim=0)
+
+ intrinsics_latent = torch.from_numpy(intrinsics).float()
+ intrinsics_latent[:, [0, 2]] *= latent_width / float(condition.width)
+ intrinsics_latent[:, [1, 3]] *= latent_height / float(condition.height)
+
+ return _pack_camera_conditions(
+ poses,
+ intrinsics_latent,
+ num_frames=num_frames,
+ latent_height=latent_height,
+ latent_width=latent_width,
+ vae_time_stride=vae_time_stride,
+ )
diff --git a/vllm_omni/diffusion/models/sana_wm/config.py b/vllm_omni/diffusion/models/sana_wm/config.py
new file mode 100644
index 000000000..bf4eb428c
--- /dev/null
+++ b/vllm_omni/diffusion/models/sana_wm/config.py
@@ -0,0 +1,93 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
+"""Config for the SANA-WM Stage-1 transformer.
+
+Loaded from the standard diffusers ``transformer/config.json`` (a flat dict
+keyed by the dataclass field names). The bespoke ``config.yaml`` release format
+is handled once, offline, at checkpoint-conversion time — the runtime only ever
+sees the converted, diffusers-standard config.
+"""
+
+from __future__ import annotations
+
+import json
+from collections.abc import Mapping
+from dataclasses import dataclass, field, fields
+from pathlib import Path
+from typing import Any
+
+
+def _as_tuple3(value: Any, default: tuple[int, int, int]) -> tuple[int, int, int]:
+ if value is None:
+ return default
+ if isinstance(value, int):
+ return (value, value, value)
+ if isinstance(value, (list, tuple)):
+ if len(value) == 2:
+ return (1, int(value[0]), int(value[1]))
+ if len(value) == 3:
+ return (int(value[0]), int(value[1]), int(value[2]))
+ return default
+
+
+# LTX-2 VAE compression ratios (SANA-WM ships ``AutoencoderKLLTX2Video``).
+# They live here rather than in the pipeline so the request-normalisation module
+# can validate latent geometry without importing the pipeline.
+SANA_WM_VAE_SPATIAL_COMPRESSION = 32
+SANA_WM_VAE_TEMPORAL_COMPRESSION = 8
+
+
+@dataclass(frozen=True)
+class SanaWmConfig:
+ """SANA-WM Stage-1 architecture and runtime config.
+
+ Defaults mirror the first public HF release. ``from_json`` (reading
+ ``transformer/config.json``) is the normal construction path.
+ """
+
+ architecture_name: str | None = "SanaMSVideoCamCtrl_1600M_P1_D20"
+ num_blocks: int = 20
+ hidden_size: int = 2240
+ mlp_ratio: float = 3.0
+ attn_type: str = "BidirectionalGDNTriton"
+ softmax_every_n: int = 4
+ linear_head_dim: int = 112
+ conv_kernel_size: int = 4
+ t_kernel_size: int = 3
+ k_conv_only: bool = True
+ ffn_type: str = "GLUMBConvTemp"
+ pos_embed_type: str = "wan_rope"
+ patch_size: tuple[int, int, int] = (1, 1, 1)
+ qk_norm: bool = True
+ cross_norm: bool = True
+ mixed_precision: str = "bf16"
+ fp32_attention: bool = True
+ image_size: int = 720
+ cam_attn_compress: int = 1
+ use_chunk_plucker_post_attn: bool = True
+ chunk_plucker_channels: int = 48
+ chunk_plucker_post_attn_blocks: int = 20
+ inference_flow_shift: float = 9.8
+ scheduler_type: str = "flow_dpm-solver"
+ chi_prompt: list[str] = field(default_factory=list)
+ model_max_length: int = 300
+
+ @classmethod
+ def from_dict(cls, data: Mapping[str, Any]) -> SanaWmConfig:
+ """Build from a flat mapping of field name -> value.
+
+ Unknown keys (e.g. the diffusers ``_class_name`` marker or the
+ ``latent_channels`` / ``prompt_channels`` constructor kwargs) are
+ ignored. ``patch_size`` is coerced from a JSON list back to a tuple.
+ """
+ field_names = {f.name for f in fields(cls)}
+ known = {k: v for k, v in data.items() if k in field_names}
+ if "patch_size" in known:
+ known["patch_size"] = _as_tuple3(known["patch_size"], cls.patch_size)
+ return cls(**known)
+
+ @classmethod
+ def from_json(cls, path: str | Path) -> SanaWmConfig:
+ with open(path, encoding="utf-8") as f:
+ data = json.load(f)
+ return cls.from_dict(data if isinstance(data, Mapping) else {})
diff --git a/vllm_omni/diffusion/models/sana_wm/pipeline_sana_wm.py b/vllm_omni/diffusion/models/sana_wm/pipeline_sana_wm.py
new file mode 100644
index 000000000..9c2843040
--- /dev/null
+++ b/vllm_omni/diffusion/models/sana_wm/pipeline_sana_wm.py
@@ -0,0 +1,942 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
+"""Sana-WM pipeline integration.
+
+This module wires the registry-visible surface, release-layout validation, and
+the Stage-1 sampling loop. The DiT, Gated-DeltaNet and camera stack are
+vLLM-Omni-native layers (``sana_wm_transformer.py`` / ``ucpe.py``); nothing here
+calls into NVlabs code at runtime.
+"""
+
+from __future__ import annotations
+
+import os
+from collections.abc import Iterable, Mapping
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any, ClassVar
+
+import torch
+import torch.nn.functional as F
+from torch import nn
+from vllm.logger import init_logger
+from vllm.model_executor.models.utils import AutoWeightsLoader
+
+from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
+from vllm_omni.diffusion.distributed.cfg_parallel import CFGParallelMixin
+from vllm_omni.diffusion.distributed.utils import get_local_device
+from vllm_omni.diffusion.model_loader.diffusers_loader import DiffusersPipelineLoader
+from vllm_omni.diffusion.models.interface import SupportImageInput, SupportsComponentDiscovery
+from vllm_omni.diffusion.models.ltx2.ltx2_latents import (
+ denormalize_latents,
+ normalize_latents,
+ pack_latents,
+ resolve_video_latent_shape,
+ unpack_latents,
+)
+from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin
+from vllm_omni.diffusion.models.sana_wm.camera_control import (
+ SanaWmCameraCondition,
+ build_plucker_condition,
+)
+from vllm_omni.diffusion.models.sana_wm.config import (
+ SANA_WM_VAE_SPATIAL_COMPRESSION,
+ SANA_WM_VAE_TEMPORAL_COMPRESSION,
+ SanaWmConfig,
+)
+from vllm_omni.diffusion.models.sana_wm.request import normalize_sana_wm_payload
+from vllm_omni.diffusion.models.sana_wm.sana_wm_transformer import (
+ SANA_WM_STAGE1_PROMPT_CHANNELS,
+ SanaWmTransformer3DModel,
+)
+from vllm_omni.diffusion.models.schedulers import FlowMatchEulerDiscreteScheduler
+from vllm_omni.diffusion.offloader.module_collector import ModuleDiscovery
+from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import (
+ DiffusionPipelineProfilerMixin,
+)
+from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
+from vllm_omni.model_executor.model_loader.weight_utils import download_weights_from_hf_specific
+
+logger = init_logger(__name__)
+
+# The loader expects the standard diffusers layout (transformer/config.json +
+# transformer/diffusion_pytorch_model.safetensors + model_index.json), so this
+# repo is an offline conversion of the NVlabs release into that layout. It holds
+# Stage-1 only — transformer/ + vae/, with model_index.json naming
+# SanaWmPipeline — because startup prefetches the whole model repo
+# (allow_patterns=["*"]); a repo that also carried the refiner would pull tens of
+# GB this path never loads.
+SANA_WM_MODEL_ID = "BBBBruce/SANA-WM_bidirectional-stage1-diffusers"
+
+SANA_WM_STAGE1_DIT_FILE = "transformer/diffusion_pytorch_model.safetensors"
+SANA_WM_CONFIG_FILE = "transformer/config.json"
+SANA_WM_VAE_CONFIG_FILE = "vae/config.json"
+SANA_WM_VAE_WEIGHT_FILE = "vae/diffusion_pytorch_model.safetensors"
+SANA_WM_STAGE1_TEXT_ENCODER_ID = "google/gemma-2-2b-it"
+SANA_WM_STAGE1_TEXT_ENCODER_FALLBACK_ID = "Efficient-Large-Model/gemma-2-2b-it"
+SANA_WM_STAGE1_TEXT_ENCODER_ENV = "VLLM_OMNI_SANA_WM_STAGE1_TEXT_ENCODER"
+SANA_WM_OUTPUT_HEIGHT = 704
+SANA_WM_OUTPUT_WIDTH = 1280
+SANA_WM_NUM_TRAIN_TIMESTEPS = 1000
+# Maximum supported clip length (20s @16fps) — the native envelope, not a
+# request default. Request-side defaults live in
+# ``models/sana_wm/request.py``.
+SANA_WM_NATIVE_NUM_FRAMES = 321
+# Fail-fast latent-token cap for the native Stage-1 path, sized to the native
+# 704x1280 output at the maximum clip length so it only rejects genuinely
+# oversized requests. Overridable per-request via ``sana_wm_native_max_tokens``.
+SANA_WM_NATIVE_MAX_TOKENS = (
+ ((SANA_WM_NATIVE_NUM_FRAMES - 1) // SANA_WM_VAE_TEMPORAL_COMPRESSION + 1)
+ * (SANA_WM_OUTPUT_HEIGHT // SANA_WM_VAE_SPATIAL_COMPRESSION)
+ * (SANA_WM_OUTPUT_WIDTH // SANA_WM_VAE_SPATIAL_COMPRESSION)
+)
+# The NVlabs production inference settings for this release. They belong in the
+# model rather than in a deploy YAML: single-stage diffusion models resolve
+# through ``create_default_diffusion``, which never reads a YAML's
+# ``default_sampling_params``, so a caller that omits either field lands here.
+SANA_WM_DEFAULT_NUM_INFERENCE_STEPS = 60
+SANA_WM_DEFAULT_GUIDANCE_SCALE = 5.0
+
+SANA_WM_STAGE1_PATTERNS = (
+ SANA_WM_CONFIG_FILE,
+ SANA_WM_STAGE1_DIT_FILE,
+ SANA_WM_VAE_CONFIG_FILE,
+ SANA_WM_VAE_WEIGHT_FILE,
+)
+SANA_WM_STAGE1_DIT_SUBFOLDER = "transformer"
+SANA_WM_STAGE1_DIT_BASENAME = Path(SANA_WM_STAGE1_DIT_FILE).name
+
+
+@dataclass(frozen=True)
+class SanaWmLocalPaths:
+ """Resolved local file paths for a SANA-WM snapshot."""
+
+ root: Path
+ config: Path
+ stage1_dit: Path
+ vae_config: Path
+ vae_weights: Path
+
+
+@dataclass(frozen=True)
+class SanaWmNativeParams:
+ """Resolved generation settings for one Stage-1 request.
+
+ Built by ``_native_params`` from the request payload and sampling params,
+ and consumed by ``_run_native_backend`` — this is the production path.
+ """
+
+ height: int
+ width: int
+ num_frames: int
+ num_inference_steps: int
+ seed: int
+ # Defaults to the NVlabs production scale, so a caller that never mentions
+ # guidance still gets the two-branch CFG forward the model was tuned for.
+ # An explicit ``guidance_scale <= 1.0`` still selects the single-branch path.
+ cfg_scale: float = SANA_WM_DEFAULT_GUIDANCE_SCALE
+
+
+def build_sana_wm_download_patterns() -> tuple[str, ...]:
+ """Return the minimal HF allow-patterns needed for SANA-WM."""
+
+ return tuple(SANA_WM_STAGE1_PATTERNS)
+
+
+def resolve_sana_wm_local_paths(snapshot_dir: str | Path) -> SanaWmLocalPaths:
+ root = Path(snapshot_dir)
+ return SanaWmLocalPaths(
+ root=root,
+ config=root / SANA_WM_CONFIG_FILE,
+ stage1_dit=root / SANA_WM_STAGE1_DIT_FILE,
+ vae_config=root / SANA_WM_VAE_CONFIG_FILE,
+ vae_weights=root / SANA_WM_VAE_WEIGHT_FILE,
+ )
+
+
+def validate_sana_wm_local_paths(paths: SanaWmLocalPaths) -> None:
+ required = [
+ paths.config,
+ paths.stage1_dit,
+ paths.vae_config,
+ paths.vae_weights,
+ ]
+ missing = [path for path in required if not path.exists()]
+ if missing:
+ joined = ", ".join(str(path) for path in missing)
+ raise FileNotFoundError(f"Missing SANA-WM checkpoint files: {joined}")
+
+
+def resolve_or_download_sana_wm_checkpoint(
+ model: str = SANA_WM_MODEL_ID,
+ *,
+ revision: str | None = None,
+ cache_dir: str | None = None,
+) -> SanaWmLocalPaths:
+ """Resolve a local SANA-WM tree or download the required HF files."""
+
+ root = Path(model)
+ if root.is_dir():
+ snapshot_dir = root
+ else:
+ snapshot_dir = Path(
+ download_weights_from_hf_specific(
+ model,
+ cache_dir,
+ list(build_sana_wm_download_patterns()),
+ revision=revision,
+ require_all=True,
+ )
+ )
+ paths = resolve_sana_wm_local_paths(snapshot_dir)
+ validate_sana_wm_local_paths(paths)
+ return paths
+
+
+def build_sana_wm_output_envelope(
+ *,
+ output: Any,
+ output_type: str,
+ metadata: dict[str, Any],
+) -> dict[str, Any]:
+ """Wrap a Stage-1 result in the canonical output envelope.
+
+ ``normalize_diffusion_postprocess_output`` splits ``{"payload": ...,
+ "metadata": ...}`` into the API-facing payload and the metadata groups, so
+ the model-specific diagnostics ride along under a ``sana_wm`` group instead
+ of the removed ``DiffusionOutput.custom_output`` field.
+ """
+ payload_key = "latents" if output_type == "latent" else "video"
+ return {"payload": {payload_key: output}, "metadata": {"sana_wm": metadata}}
+
+
+def get_sana_wm_pre_process_func(od_config: OmniDiffusionConfig):
+ del od_config
+
+ def pre_process_func(request: Any) -> Any:
+ from vllm_omni.inputs.data import OmniTextPrompt
+
+ sampling_params = getattr(request, "sampling_params", None)
+ prompt = request.prompt
+ prompt_mapping = OmniTextPrompt(prompt=prompt) if isinstance(prompt, str) else prompt
+ prompt_mapping = dict(prompt_mapping)
+
+ # The camera block travels as extra_params["sana_wm"], which serving
+ # merges into extra_args. Offline callers may also put it straight on
+ # the prompt.
+ if sampling_params is not None:
+ extra_args = getattr(sampling_params, "extra_args", None)
+ if isinstance(extra_args, Mapping):
+ block = extra_args.get("sana_wm")
+ if isinstance(block, Mapping):
+ merged = dict(block)
+ merged.update(prompt_mapping.get("sana_wm") or {})
+ prompt_mapping["sana_wm"] = merged
+ # Geometry always comes from the standard sampling fields.
+ for field in ("num_frames", "height", "width"):
+ value = getattr(sampling_params, field, None)
+ if value is not None and not (field == "num_frames" and value == 1):
+ prompt_mapping[field] = value
+
+ request.prompt = normalize_sana_wm_payload(prompt_mapping)
+
+ # Preprocess is the only place the request is normalised, so publish the
+ # canonical geometry back onto sampling_params for the rest of the stack.
+ if sampling_params is not None:
+ payload = request.prompt["additional_information"]["sana_wm"]
+ sampling_params.num_frames = payload["num_frames"]
+ sampling_params.height = payload["height"]
+ sampling_params.width = payload["width"]
+ return request
+
+ return pre_process_func
+
+
+class SanaWmPipeline(
+ nn.Module,
+ CFGParallelMixin,
+ SupportImageInput,
+ SupportsComponentDiscovery,
+ ProgressBarMixin,
+ DiffusionPipelineProfilerMixin,
+):
+ """Stage-1 SANA-WM image-to-video pipeline."""
+
+ support_image_input: ClassVar[bool] = True
+ color_format: ClassVar[str] = "RGB"
+ # Skip the startup warmup: the generic dummy request carries a blank image
+ # with no camera poses and no action string, which SANA-WM's request
+ # validation always rejects. The first real request does the warming.
+ dummy_run_num_frames: ClassVar[int] = 0
+ _dit_modules: ClassVar[list[str]] = ["transformer"]
+ _encoder_modules: ClassVar[list[str]] = ["text_encoder"]
+ _vae_modules: ClassVar[list[str]] = ["vae"]
+ _resident_modules: ClassVar[list[str]] = []
+
+ def __init__(self, *, od_config: OmniDiffusionConfig | None = None, prefix: str = "") -> None:
+ super().__init__()
+ self.od_config = od_config
+ self.prefix = prefix
+
+ self.sana_wm_config = SanaWmConfig()
+ self.quant_config = getattr(od_config, "quantization_config", None) if od_config is not None else None
+
+ self.tokenizer: Any | None = None
+ self.text_encoder: nn.Module | None = None
+ self.vae: nn.Module | None = None
+ self.release_paths: SanaWmLocalPaths | None = None
+ self.weights_sources = []
+ if od_config is not None and od_config.model is not None:
+ # Resolve the checkpoint once at construction (the standard
+ # startup-time flow, like every other diffusion pipeline) so the
+ # transformer below is built with the transformer/config.json shape
+ # rather than defaults.
+ self.resolve_checkpoint()
+ self.weights_sources = [
+ DiffusersPipelineLoader.ComponentSource(
+ model_or_path=od_config.model,
+ subfolder=SANA_WM_STAGE1_DIT_SUBFOLDER,
+ revision=od_config.revision,
+ # The subfolder file carries module-local keys (standard
+ # diffusers layout); prefix them into the pipeline
+ # namespace for the loader's strict-coverage check.
+ prefix="transformer.",
+ fall_back_to_pt=False,
+ allow_patterns_overrides=[SANA_WM_STAGE1_DIT_BASENAME],
+ )
+ ]
+ # Built eagerly: the loader constructs the pipeline under the target
+ # device / default-dtype context, and the weight loader then streams
+ # checkpoint tensors directly into these modules.
+ self.transformer = SanaWmTransformer3DModel(
+ config=self.sana_wm_config,
+ quant_config=self.quant_config,
+ prefix=f"{prefix}.transformer" if prefix else "transformer",
+ )
+ self.device = get_local_device()
+ if od_config is not None and od_config.model is not None:
+ self._build_aux_components()
+
+ def resolve_checkpoint(self) -> SanaWmLocalPaths:
+ if self.release_paths is not None:
+ return self.release_paths
+ if self.od_config is None or self.od_config.model is None:
+ raise ValueError("Sana-WM checkpoint resolution requires od_config.model.")
+ self.release_paths = resolve_or_download_sana_wm_checkpoint(
+ self.od_config.model,
+ revision=self.od_config.revision,
+ )
+ self.sana_wm_config = SanaWmConfig.from_json(self.release_paths.config)
+ return self.release_paths
+
+ @staticmethod
+ def _extra_args(sampling_params: Any | None) -> dict[str, Any]:
+ extra_args = getattr(sampling_params, "extra_args", None) if sampling_params is not None else None
+ return dict(extra_args or {})
+
+ def _native_params(self, payload: dict[str, Any], sampling_params: Any | None) -> SanaWmNativeParams:
+ extra_args = self._extra_args(sampling_params)
+ height = int(getattr(sampling_params, "height", None) or payload["height"])
+ width = int(getattr(sampling_params, "width", None) or payload["width"])
+ num_frames = int(getattr(sampling_params, "num_frames", None) or payload["num_frames"])
+ steps = int(
+ getattr(sampling_params, "num_inference_steps", None)
+ or extra_args.get("num_inference_steps")
+ or SANA_WM_DEFAULT_NUM_INFERENCE_STEPS
+ )
+ seed = int(getattr(sampling_params, "seed", None) or extra_args.get("seed", 0))
+ if min(height, width, num_frames, steps) <= 0:
+ raise ValueError("Sana-WM native height, width, num_frames, and steps must be positive.")
+ cfg_scale = SANA_WM_DEFAULT_GUIDANCE_SCALE
+ if sampling_params is not None and getattr(sampling_params, "guidance_scale_provided", False):
+ cfg_scale = float(getattr(sampling_params, "guidance_scale", 1.0) or 1.0)
+ return SanaWmNativeParams(
+ height=height,
+ width=width,
+ num_frames=num_frames,
+ num_inference_steps=steps,
+ seed=seed,
+ cfg_scale=cfg_scale,
+ )
+
+ def _native_dtype(self, device: torch.device) -> torch.dtype:
+ dtype = getattr(self.od_config, "dtype", None) if self.od_config is not None else None
+ if isinstance(dtype, torch.dtype):
+ return dtype
+ return torch.float32 if device.type == "cpu" else torch.bfloat16
+
+ def _runtime_device_dtype(self) -> tuple[torch.device, torch.dtype]:
+ return self.device, self._native_dtype(self.device)
+
+ def _build_aux_components(self) -> None:
+ """Construct the text encoder and VAE at startup, on CPU.
+
+ ``ModuleDiscovery`` skips attributes that are ``None``, so components
+ created lazily on the first request are invisible to sequential
+ offload, HSDP auxiliary placement, and the shared device/memory
+ lifecycle. Building them here and deferring placement to the common
+ logic mirrors LTX-2's ``initialize_pipeline_components``.
+ """
+ dtype = self._native_dtype(self.device)
+ self._load_stage1_text_encoder(dtype=dtype)
+ self._load_vae(dtype=dtype)
+ self._place_aux_components()
+
+ def _place_aux_components(self) -> None:
+ """Move the discovered auxiliary components onto the local device.
+
+ Skipped when offload or HSDP owns placement — those paths expect the
+ components to still be on CPU when they take over.
+ """
+ parallel_config = getattr(self.od_config, "parallel_config", None)
+ if (
+ getattr(self.od_config, "enable_cpu_offload", False)
+ or getattr(self.od_config, "enable_layerwise_offload", False)
+ or getattr(parallel_config, "use_hsdp", False)
+ ):
+ return
+ modules = ModuleDiscovery.discover(self)
+ for module in (*modules.encoders, *modules.vaes, *modules.resident_modules):
+ module.to(self.device)
+
+ @staticmethod
+ def _component_local_files_only(model_id: Any) -> bool:
+ """Decide download policy per component, from its own model id.
+
+ The Stage-1 release directory is local once ``resolve_checkpoint`` has
+ run, but the Gemma text encoder lives in a separate Hub repo. Deriving
+ locality from the release paths forced ``local_files_only=True`` onto
+ Gemma as well, which forbids the download on a box that has not cached
+ it yet.
+ """
+ return Path(str(model_id)).expanduser().exists()
+
+ def _load_stage1_text_encoder(self, *, dtype: torch.dtype) -> None:
+ from transformers import AutoModel, AutoTokenizer
+
+ model_ids = [os.environ.get(SANA_WM_STAGE1_TEXT_ENCODER_ENV, "").strip()]
+ model_ids.extend([SANA_WM_STAGE1_TEXT_ENCODER_ID, SANA_WM_STAGE1_TEXT_ENCODER_FALLBACK_ID])
+ errors: list[str] = []
+ for model_id in dict.fromkeys(model_id for model_id in model_ids if model_id):
+ try:
+ self.tokenizer = AutoTokenizer.from_pretrained(
+ model_id,
+ local_files_only=self._component_local_files_only(model_id),
+ )
+ text_encoder_model_id = model_id
+ break
+ except OSError as exc:
+ errors.append(f"{model_id}: {exc}")
+ else:
+ raise OSError("Could not load SANA-WM Stage-1 text encoder. Tried: " + "; ".join(errors))
+ if getattr(self.tokenizer, "pad_token", None) is None:
+ self.tokenizer.pad_token = self.tokenizer.eos_token
+ self.tokenizer.padding_side = "right"
+ with torch.device("cpu"):
+ # AutoModel, not AutoModelForCausalLM: only the final hidden state is
+ # used, so the LM head would compute a [seq, 256k] logits tensor
+ # that is thrown away on every prompt encode.
+ self.text_encoder = AutoModel.from_pretrained(
+ text_encoder_model_id,
+ torch_dtype=dtype,
+ local_files_only=self._component_local_files_only(text_encoder_model_id),
+ )
+
+ def _ensure_stage1_text_encoder(self, *, device: torch.device, dtype: torch.dtype) -> None:
+ if self.text_encoder is None:
+ self._load_stage1_text_encoder(dtype=dtype)
+ self.text_encoder.to(device=device, dtype=dtype)
+
+ def _load_vae(self, *, dtype: torch.dtype) -> None:
+ if self.release_paths is None and self.od_config is not None and self.od_config.model is not None:
+ self.resolve_checkpoint()
+ if self.release_paths is None:
+ raise ValueError("Sana-WM VAE loading requires a resolved checkpoint.")
+
+ from diffusers import AutoencoderKLLTX2Video
+
+ with torch.device("cpu"):
+ self.vae = AutoencoderKLLTX2Video.from_pretrained(
+ str(self.release_paths.root),
+ subfolder="vae",
+ torch_dtype=dtype,
+ local_files_only=self._component_local_files_only(self.release_paths.root),
+ )
+ self.vae.use_framewise_encoding = True
+ self.vae.use_framewise_decoding = True
+ # NVlabs reads these from its YAML VAE config with 64/96 defaults; the
+ # diffusers VAE config.json only ships scaling_factor, so the defaults
+ # are load-bearing (the keys are genuinely absent here).
+ self.vae.tile_sample_stride_num_frames = int(getattr(self.vae.config, "tile_sample_stride_num_frames", 64))
+ self.vae.tile_sample_min_num_frames = int(getattr(self.vae.config, "tile_sample_min_num_frames", 96))
+
+ def _ensure_vae(self, *, device: torch.device, dtype: torch.dtype) -> None:
+ if self.vae is None:
+ self._load_vae(dtype=dtype)
+ # Tiling is re-asserted here, not in _load_vae: registry.py assigns
+ # ``model.vae.use_tiling = od_config.vae_use_tiling`` after the pipeline
+ # is constructed, and that flag defaults to False. Matching NVlabs'
+ # inference_video_scripts/inference_sana_wm.py, the SANA VAE has to
+ # tile — a 161-frame decode costs about 9 GiB more without it, and a
+ # 321-frame request (41 latent frames) decodes as one 3D volume and
+ # OOMs. Overriding is deliberate, but say so rather than dropping the
+ # caller's setting silently.
+ if not self.vae.use_tiling:
+ logger.warning("Sana-WM forces VAE tiling on; ignoring vae_use_tiling=False (the VAE OOMs without it).")
+ self.vae.use_tiling = True
+ self.vae.to(device=device, dtype=dtype)
+
+ @staticmethod
+ def _preprocess_first_frame(
+ image: Any,
+ *,
+ height: int,
+ width: int,
+ device: torch.device,
+ dtype: torch.dtype,
+ ) -> torch.Tensor:
+ """Return the first-frame image as ``[1, 3, 1, H, W]`` in ``[-1, 1]``.
+
+ ``VideoProcessor.preprocess`` covers the PIL / ndarray / tensor cases.
+ ``resample="bicubic"`` keeps the filter PIL's ``Image.resize`` defaulted
+ to; the processor's own default is lanczos, which would move the
+ conditioning latent.
+ """
+ import numpy as np
+ from diffusers.video_processor import VideoProcessor
+
+ # The processor normalises to [-1, 1] assuming float inputs in [0, 1].
+ if isinstance(image, np.ndarray) and image.dtype == np.uint8:
+ image = image.astype(np.float32) / 255.0
+
+ processor = VideoProcessor(vae_scale_factor=SANA_WM_VAE_SPATIAL_COMPRESSION, resample="bicubic")
+ frame = processor.preprocess(image, height=height, width=width)
+ return frame.unsqueeze(2).to(device=device, dtype=dtype)
+
+ def _vae_normalize_latent(self, latent: torch.Tensor) -> torch.Tensor:
+ """LTX-2 VAE per-channel latent normalisation matching NVlabs.
+
+ ``z_norm = (z_raw - latents_mean) * scaling_factor / latents_std``
+
+ The LTX-2 VAE (``AutoencoderKLLTX2Video`` in diffusers) ships with
+ per-channel ``latents_mean`` and ``latents_std`` tensors that must be
+ applied alongside ``scaling_factor`` for the round-trip to be
+ identity. Applying `* scaling_factor` alone gives decoded videos a
+ systematic G/B colour shift (pred RGB means
+ ~(110, 220, 208) vs ref ~(94, 114, 138)).
+
+ Returns the normalised latent in the same dtype as the input.
+ """
+ if self.vae is None:
+ raise RuntimeError("Sana-WM VAE did not initialize.")
+ return normalize_latents(
+ latent,
+ self.vae.latents_mean,
+ self.vae.latents_std,
+ float(self.vae.config.scaling_factor),
+ )
+
+ def _vae_denormalize_latent(self, latent: torch.Tensor) -> torch.Tensor:
+ """Inverse of :meth:`_vae_normalize_latent` — match NVlabs decode."""
+ if self.vae is None:
+ raise RuntimeError("Sana-WM VAE did not initialize.")
+ return denormalize_latents(
+ latent,
+ self.vae.latents_mean,
+ self.vae.latents_std,
+ float(self.vae.config.scaling_factor),
+ )
+
+ def _vae_encode_first_frame(
+ self,
+ image: Any,
+ *,
+ height: int,
+ width: int,
+ latent_height: int,
+ latent_width: int,
+ device: torch.device,
+ dtype: torch.dtype,
+ ) -> torch.Tensor:
+ """Encode the first-frame image to latent space via the LTX-2 VAE.
+
+ Returns shape ``[1, 128, 1, latent_height, latent_width]``.
+ """
+ self._ensure_vae(device=device, dtype=dtype)
+ if self.vae is None:
+ raise RuntimeError("Sana-WM VAE did not initialize.")
+ frame = self._preprocess_first_frame(image, height=height, width=width, device=device, dtype=dtype)
+ with torch.inference_mode():
+ encoded = self.vae.encode(frame)
+ first_latent = encoded.latent_dist.mean
+ first_latent = self._vae_normalize_latent(first_latent.to(dtype=dtype))
+ # The encoder's own rounding can land a pixel off the latent grid the
+ # noise tensor was built on, and the two are concatenated below.
+ if first_latent.shape[-2:] != (latent_height, latent_width):
+ first_latent = F.interpolate(
+ first_latent.squeeze(2),
+ size=(latent_height, latent_width),
+ mode="bilinear",
+ align_corners=False,
+ ).unsqueeze(2)
+ return first_latent # [1, 128, 1, lh, lw]
+
+ def _stage1_prompt_text(self, prompt: dict[str, Any]) -> str:
+ user_prompt = str(prompt.get("prompt") or "")
+ chi_prompt = "\n".join(part for part in self.sana_wm_config.chi_prompt if part)
+ if chi_prompt:
+ return chi_prompt + user_prompt
+ return user_prompt
+
+ def _native_prompt_embeds(
+ self,
+ prompt: dict[str, Any],
+ *,
+ device: torch.device,
+ dtype: torch.dtype,
+ ) -> torch.Tensor:
+ prompt_text = self._stage1_prompt_text(prompt)
+ self._ensure_stage1_text_encoder(device=device, dtype=dtype)
+ tokenizer = self.tokenizer
+ if tokenizer is None or self.text_encoder is None:
+ raise RuntimeError("Sana-WM Stage-1 text encoder did not initialize.")
+ chi_prompt = "\n".join(part for part in self.sana_wm_config.chi_prompt if part)
+ model_max_length = int(self.sana_wm_config.model_max_length)
+ max_length_all = model_max_length
+ if chi_prompt:
+ # NVlabs encodes the chi-prompt-prefixed text with extra room for
+ # the chi tokens, then keeps BOS plus the final model window.
+ max_length_all = len(tokenizer.encode(chi_prompt)) + model_max_length - 2
+ encoded = tokenizer(
+ [prompt_text],
+ padding="max_length",
+ max_length=max_length_all,
+ truncation=True,
+ add_special_tokens=True,
+ return_tensors="pt",
+ ).to(device)
+ # last_hidden_state is the same tensor the CausalLM wrapper exposed as
+ # hidden_states[-1] (both are post-final-norm), without retaining every
+ # intermediate layer.
+ hidden_states = self.text_encoder(**encoded).last_hidden_state
+ attention_mask = encoded.attention_mask
+ if chi_prompt:
+ select_index = [0] + list(range(-model_max_length + 1, 0))
+ hidden_states = hidden_states[:, select_index]
+ attention_mask = attention_mask[:, select_index]
+ if hidden_states.shape[-1] != SANA_WM_STAGE1_PROMPT_CHANNELS:
+ raise ValueError(
+ "Sana-WM Stage-1 Gemma hidden size mismatch: expected "
+ f"{SANA_WM_STAGE1_PROMPT_CHANNELS}, got {hidden_states.shape[-1]}."
+ )
+ # Return RAW Gemma hidden states: the transformer's ``attention_y_norm``
+ # RMSNorm does the normalisation, so normalising here double-normalises.
+ prompt_attention_mask = attention_mask.to(device=device, dtype=torch.float32)
+ # An all-ones mask carries no information. Keep it ``None`` so
+ # cross-attention takes the mask-free attention path instead of handing
+ # every block, on every step, a mask the backend must reshape to no
+ # effect. Evaluated once per prompt encode, so the device sync is paid
+ # twice per request (cond + uncond), not once per denoising step.
+ if bool(prompt_attention_mask.all()):
+ prompt_attention_mask = None
+ self._last_prompt_attention_mask = prompt_attention_mask
+ return hidden_states.to(device=device, dtype=dtype)
+
+ def predict_noise(self, **kwargs: Any) -> torch.Tensor:
+ """Single transformer forward.
+
+ Overrides the mixin default, which assumes a tuple-returning transformer
+ and takes ``result[0]``; SANA-WM's returns the noise prediction directly.
+ """
+ return self.transformer(**kwargs)
+
+ def _decode_native_latents(
+ self,
+ latents: torch.Tensor,
+ *,
+ output_type: str,
+ device: torch.device,
+ dtype: torch.dtype,
+ ) -> Any:
+ if output_type == "latent":
+ return latents
+ self._ensure_vae(device=device, dtype=dtype)
+ if self.vae is None:
+ raise RuntimeError("Sana-WM VAE did not initialize.")
+ # NVlabs always passes ``temb=None`` to ``vae.decode`` for this VAE
+ # (``builder.py::vae_decode -> LTX2VAE_diffusers``); match that.
+ denorm = self._vae_denormalize_latent(latents.to(getattr(self.vae, "dtype", dtype)))
+ video = self.vae.decode(denorm, temb=None, return_dict=False)[0]
+ # Post-process to the requested output_type (np/pil/pt). Let failures
+ # surface: silently returning the raw decoder tensor would hand the
+ # caller a wrong-format/range video with no error.
+ from diffusers.video_processor import VideoProcessor
+
+ processor = VideoProcessor(vae_scale_factor=getattr(self.vae, "spatial_compression_ratio", 32))
+ return processor.postprocess_video(video, output_type=output_type)
+
+ def _run_native_backend(
+ self,
+ *,
+ prompt: dict[str, Any],
+ payload: dict[str, Any],
+ sampling_params: Any | None,
+ ) -> DiffusionOutput:
+ params = self._native_params(payload, sampling_params)
+ latent_frames, latent_height, latent_width = resolve_video_latent_shape(
+ params.height,
+ params.width,
+ params.num_frames,
+ vae_spatial_compression_ratio=SANA_WM_VAE_SPATIAL_COMPRESSION,
+ vae_temporal_compression_ratio=SANA_WM_VAE_TEMPORAL_COMPRESSION,
+ )
+ token_count = latent_frames * latent_height * latent_width
+ extra_args = self._extra_args(sampling_params)
+ max_tokens = int(extra_args.get("sana_wm_native_max_tokens", SANA_WM_NATIVE_MAX_TOKENS))
+ if token_count > max_tokens:
+ raise ValueError(
+ "Sana-WM native latent token count exceeds the configured cap. "
+ f"Requested latent tokens={token_count}, max={max_tokens}. Request a smaller "
+ "`height`/`width`/`num_frames`, or raise the cap via "
+ "`sana_wm_native_max_tokens`."
+ )
+
+ device, dtype = self._runtime_device_dtype()
+ generator = torch.Generator(device=device)
+ generator.manual_seed(params.seed)
+ noise = torch.randn(
+ (1, 128, latent_frames, latent_height, latent_width),
+ device=device,
+ dtype=dtype,
+ generator=generator,
+ )
+
+ # Shared flow-matching Euler scheduler. NVlabs ``LTXFlowEuler`` drives
+ # diffusers' ``FlowMatchEulerDiscreteScheduler``; this is vLLM-Omni's
+ # vendored copy of it, including the ``per_token_timesteps`` step branch.
+ # The schedule applies the shift twice (once via the shifted ``sigma_min``
+ # persisted by ``__init__``, once in ``set_timesteps``), which is what
+ # reproduces the NVlabs timestep table (e.g. shift=9.8, N=3 ->
+ # [1000, 909.0, 87.7]).
+ scheduler = FlowMatchEulerDiscreteScheduler(
+ num_train_timesteps=SANA_WM_NUM_TRAIN_TIMESTEPS,
+ shift=self.sana_wm_config.inference_flow_shift,
+ )
+ scheduler.set_timesteps(params.num_inference_steps, device=device)
+ timesteps = scheduler.timesteps
+
+ # ``normalize_sana_wm_payload`` guarantees the key is present but says
+ # nothing about its type. Falling back to pure noise on an unusable
+ # image would silently drop the conditioning and produce a
+ # plausible-looking but wrong video, so this raises instead.
+ import numpy as _np
+
+ first_frame_image = (prompt.get("multi_modal_data") or {}).get("image")
+ if not (hasattr(first_frame_image, "convert") or isinstance(first_frame_image, (_np.ndarray, torch.Tensor))):
+ raise TypeError(
+ "Sana-WM first-frame image must be a PIL Image, numpy ndarray, or "
+ f"torch.Tensor; got {type(first_frame_image).__name__}."
+ )
+ # Per-frame timestep contract matching NVlabs ``LTXFlowEuler.sample``:
+ # frame 0 is the clean VAE-encoded conditioning latent and its timestep
+ # is held at 0 while the sampling loop drives the other frames from
+ # noise.
+ first_latent = self._vae_encode_first_frame(
+ first_frame_image,
+ height=params.height,
+ width=params.width,
+ latent_height=latent_height,
+ latent_width=latent_width,
+ device=device,
+ dtype=dtype,
+ )
+ # Place the CLEAN VAE-encoded first frame at frame 0 and rely on
+ # per-frame timesteps + the `condition_mask` torch.where restore (below)
+ # to keep it invariant across denoising steps.
+ latents = torch.cat([first_latent, noise[:, :, 1:]], dim=2)
+
+ prompt_embeds = self._native_prompt_embeds(
+ prompt,
+ device=device,
+ dtype=dtype,
+ )
+ prompt_attention_mask = self._last_prompt_attention_mask
+
+ # Under CFG, encode the negative prompt up front; each denoise step
+ # then runs a two-branch transformer forward. The negative prompt comes
+ # from the request (``prompt["negative_prompt"]``, set by
+ # ``serving_video``); absent, it defaults to the empty string matching
+ # NVlabs ``GenerationParams.negative_prompt = ""``.
+ do_cfg = params.cfg_scale > 1.0
+ if do_cfg:
+ negative_prompt_obj = {"prompt": str(prompt.get("negative_prompt") or "")}
+ negative_prompt_embeds = self._native_prompt_embeds(
+ negative_prompt_obj,
+ device=device,
+ dtype=dtype,
+ )
+ negative_prompt_attention_mask = self._last_prompt_attention_mask
+ # Restore the cond mask so later readers of
+ # `_last_prompt_attention_mask` see the positive branch.
+ self._last_prompt_attention_mask = prompt_attention_mask
+ else:
+ negative_prompt_embeds = None
+ negative_prompt_attention_mask = None
+
+ camera = payload.get("camera") or {}
+ condition = SanaWmCameraCondition(
+ poses=camera.get("poses") if isinstance(camera, dict) else None,
+ intrinsics=payload.get("intrinsics"),
+ action=payload.get("action"),
+ num_frames=params.num_frames,
+ height=params.height,
+ width=params.width,
+ translation_speed=float(payload.get("translation_speed", 0.05)),
+ rotation_speed_deg=float(payload.get("rotation_speed_deg", 1.2)),
+ )
+ camera_tensors = build_plucker_condition(condition)
+ plucker = camera_tensors["chunk_plucker"].to(device=device, dtype=dtype)
+ raymap = camera_tensors["raymap"].to(device=device, dtype=dtype)
+ spatial_raymap = camera_tensors.get("spatial_raymap")
+ if spatial_raymap is not None:
+ spatial_raymap = spatial_raymap.to(device=device, dtype=dtype)
+
+ # Build a per-frame condition mask matching NVlabs ``LTXFlowEuler``:
+ # frame 0 is the conditioning frame at sigma=0 (preserved after
+ # each ``scheduler.step``), all other frames carry the current
+ # sampling sigma. We omit the legacy `add_noise_to_image_conditioning_latents`
+ # motion-continuity term because the public SANA-WM config sets
+ # ``condition_frame_info={0: 0.0}`` (image_cond_noise_scale=0).
+ cam_batch = latents.shape[0]
+ cam_frames = latents.shape[2]
+ condition_mask = torch.zeros(cam_batch, 1, cam_frames, 1, 1, device=latents.device, dtype=latents.dtype)
+ condition_mask[:, :, 0] = 1.0
+
+ for timestep in timesteps:
+ # (B, 1, F) per-frame timestep, frame 0 forced to 0. Stays fp32:
+ # casting through the bf16 latent dtype would quantise it before
+ # ``SanaWmTimestepEmbedder``'s sinusoidal embed.
+ model_timestep = timestep.float().expand(cam_batch, 1, cam_frames).clone()
+ model_timestep[:, :, 0] = 0.0
+ positive_kwargs = {
+ "hidden_states": latents,
+ "timestep": model_timestep,
+ "encoder_hidden_states": prompt_embeds,
+ "encoder_attention_mask": prompt_attention_mask,
+ "plucker": plucker,
+ "raymap": raymap,
+ "spatial_raymap": spatial_raymap,
+ }
+ negative_kwargs = (
+ {
+ **positive_kwargs,
+ "encoder_hidden_states": negative_prompt_embeds,
+ "encoder_attention_mask": negative_prompt_attention_mask,
+ }
+ if do_cfg
+ else None
+ )
+ # ``cfg_normalize=False`` keeps the plain NVlabs combine,
+ # ``uncond + scale * (cond - uncond)``. Under cfg_parallel_size > 1
+ # the mixin runs one branch per rank and all-gathers instead of
+ # evaluating both branches serially.
+ noise_pred = self.predict_noise_maybe_with_cfg(
+ do_cfg,
+ params.cfg_scale,
+ positive_kwargs,
+ negative_kwargs,
+ cfg_normalize=False,
+ )
+
+ # The per-frame contract pairs with a per-token flow-matching Euler
+ # step. Both step branches end at ``prev = sample + dt *
+ # model_output``, but the scheduler defines dt as
+ # ``sigma - sigma_next`` per-token against ``sigma_next - sigma``
+ # standard, so the per-token call takes the sign-flipped prediction
+ # (matching NVlabs ``LTXFlowEuler.sample``); passing the standard
+ # sign here would integrate towards more noise.
+ #
+ # Broadcast the (B, 1, F) timestep to (B, 1, F, H, W) and flatten
+ # F*H*W; frame-0 conditioning tokens are already 0. The flatten
+ # order (F, H, W) matches ``pack_latents``.
+ pt_t = (
+ model_timestep.unsqueeze(-1)
+ .unsqueeze(-1)
+ .expand(cam_batch, 1, cam_frames, latents.shape[3], latents.shape[4])
+ .reshape(cam_batch, -1)
+ )
+ # The per-token branch consumes/returns the packed (B, N, C) layout
+ # and keeps its result in fp32; cast back so the latent dtype stays
+ # stable across steps.
+ stepped_packed = scheduler.step(
+ -pack_latents(noise_pred),
+ timestep,
+ pack_latents(latents),
+ per_token_timesteps=pt_t,
+ return_dict=False,
+ )[0]
+ stepped = unpack_latents(
+ stepped_packed.to(latents.dtype),
+ cam_frames,
+ latents.shape[3],
+ latents.shape[4],
+ )
+
+ # Match NVlabs LTXFlowEuler exactly. This is stricter than a plain
+ # condition-frame restore: with bf16 latents, the `t=1000`
+ # comparison rounds `1 - 1e-6` to `1`, so the first full-noise step
+ # is discarded for generated tokens as well.
+ tokens_to_denoise_mask = timestep / float(SANA_WM_NUM_TRAIN_TIMESTEPS) - 1e-6 < (1.0 - condition_mask)
+ latents = torch.where(tokens_to_denoise_mask, stepped, latents)
+
+ output_type = getattr(sampling_params, "output_type", None) or "np"
+ output = self._decode_native_latents(latents, output_type=output_type, device=device, dtype=dtype)
+ return DiffusionOutput(
+ output=build_sana_wm_output_envelope(
+ output=output,
+ output_type=output_type,
+ metadata={
+ "backend": "native_gdn",
+ "output_space": output_type,
+ "chi_prompt_applied": bool(self.sana_wm_config.chi_prompt),
+ "num_frames": params.num_frames,
+ "height": params.height,
+ "width": params.width,
+ "latent_tokens": token_count,
+ "sampling_steps": params.num_inference_steps,
+ },
+ ),
+ )
+
+ def forward(self, req: DiffusionRequestBatch, *args: Any, **kwargs: Any) -> DiffusionOutput:
+ del args, kwargs
+ if len(req.prompts) != 1:
+ raise ValueError("Sana-WM native backend currently supports exactly one prompt per request.")
+
+ prompt = req.prompts[0]
+ if isinstance(prompt, str):
+ raise ValueError("Sana-WM requires a mapping prompt with first-frame image and camera/action metadata.")
+ # get_sana_wm_pre_process_func is the single canonical entry point: it
+ # normalises the request and republishes the geometry onto
+ # sampling_params. normalize_sana_wm_payload is idempotent, so this only
+ # covers offline callers that bypass the preprocess hook.
+ prompt = normalize_sana_wm_payload(prompt)
+ payload = prompt["additional_information"]["sana_wm"]
+
+ # The checkpoint is resolved once in __init__; this is a cached no-op
+ # kept as a guard for pipelines constructed without od_config.model.
+ if self.od_config is not None and self.od_config.model is not None:
+ self.resolve_checkpoint()
+
+ return self._run_native_backend(
+ prompt=prompt,
+ payload=payload,
+ sampling_params=req.sampling_params,
+ )
+
+ def load_weights(self, weights: Iterable[tuple[str, Any]]) -> set[str]:
+ # Keys arrive pipeline-prefixed (``transformer.*``, per the component
+ # source). AutoWeightsLoader streams them to the submodule and returns
+ # the prefixed names the loader's strict-coverage check compares.
+ loader = AutoWeightsLoader(self)
+ return loader.load_weights(weights)
diff --git a/vllm_omni/diffusion/models/sana_wm/request.py b/vllm_omni/diffusion/models/sana_wm/request.py
new file mode 100644
index 000000000..010675045
--- /dev/null
+++ b/vllm_omni/diffusion/models/sana_wm/request.py
@@ -0,0 +1,269 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
+"""Request normalization for Sana-WM image-to-video.
+
+The released SANA-WM checkpoint is first-frame image-to-video. This module keeps
+request-side normalization lightweight: it validates the image/camera/action
+contract and stores a canonical payload under
+``additional_information["sana_wm"]``. Model-internal raymap / Plucker
+projection lives with the transformer.
+
+This lives beside the model rather than under ``stage_input_processors``
+because nothing loads it as one: the pipeline's ``pre_process_func`` is
+registered from ``pipeline_sana_wm``, the same as every other diffusion model,
+and no deploy config wires a ``custom_process_input_func`` here. Keeping it in
+the stage package forced the pipeline to import *upwards* — the only such
+import in ``diffusion/models/`` — which in turn made the VAE compression
+constants impossible to share and so duplicated.
+"""
+
+from __future__ import annotations
+
+import math
+from collections.abc import Mapping, Sequence
+from typing import Any
+
+from vllm_omni.diffusion.models.sana_wm.camera_control import action_rollout_num_frames
+from vllm_omni.diffusion.models.sana_wm.config import (
+ SANA_WM_VAE_SPATIAL_COMPRESSION,
+ SANA_WM_VAE_TEMPORAL_COMPRESSION,
+)
+
+SANA_WM_CANONICAL_KEY = "sana_wm"
+# Request fallbacks used whenever the field is absent, mirroring the deploy YAML
+# (161 frames) and the model's native resolution. Both are genuinely reachable:
+# a served request that omits num_frames never gets one injected, and callers
+# may ask for a different resolution.
+SANA_WM_DEFAULT_NUM_FRAMES = 161
+SANA_WM_DEFAULT_HEIGHT = 704
+SANA_WM_DEFAULT_WIDTH = 1280
+SANA_WM_DEFAULT_CAMERA_FORMAT = "c2w_4x4"
+SANA_WM_DEFAULT_COORDINATE_SYSTEM = "official"
+
+
+def _unwrap_single(value: Any) -> Any:
+ if isinstance(value, list):
+ return value[0] if value else None
+ return value
+
+
+def _as_dict(value: Any, *, name: str) -> dict[str, Any]:
+ if value is None:
+ return {}
+ if not isinstance(value, Mapping):
+ raise ValueError(f"Sana-WM {name} must be a mapping, got {type(value).__name__}.")
+ return dict(value)
+
+
+def _is_sequence(value: Any) -> bool:
+ return isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray))
+
+
+def _as_finite_float(value: Any, *, name: str) -> float:
+ """Parse a float and reject NaN/inf.
+
+ Non-finite geometry does not fail loudly downstream: it propagates through
+ the raymap into the Plucker maps and comes back as an all-NaN video with a
+ 200 response.
+ """
+ try:
+ parsed = float(value)
+ except (TypeError, ValueError) as exc:
+ raise ValueError(f"Sana-WM {name} must be a number, got {value!r}.") from exc
+ if not math.isfinite(parsed):
+ raise ValueError(f"Sana-WM {name} must be finite, got {parsed!r}.")
+ return parsed
+
+
+def _validate_numeric_matrix4x4(matrix: Any, *, name: str) -> None:
+ if not _is_sequence(matrix) or len(matrix) != 4:
+ raise ValueError(f"Sana-WM {name} must be a 4x4 matrix.")
+ for row_idx, row in enumerate(matrix):
+ if not _is_sequence(row) or len(row) != 4:
+ raise ValueError(f"Sana-WM {name}[{row_idx}] must contain 4 values.")
+ for col_idx, value in enumerate(row):
+ _as_finite_float(value, name=f"{name}[{row_idx}][{col_idx}]")
+
+
+def _validate_camera_poses(poses: Any, *, num_frames: int | None = None) -> None:
+ if not _is_sequence(poses) or len(poses) == 0:
+ raise ValueError("Sana-WM camera poses must be a non-empty sequence of 4x4 matrices.")
+ if num_frames is not None and len(poses) != num_frames:
+ raise ValueError(f"Sana-WM camera poses length {len(poses)} must equal num_frames {num_frames}.")
+ for idx, pose in enumerate(poses):
+ _validate_numeric_matrix4x4(pose, name=f"camera.poses[{idx}]")
+
+
+def _validate_intrinsics(intrinsics: Any) -> None:
+ # Only the ``{fx, fy, cx, cy}`` mapping is accepted (the one form the recipe
+ # and examples use). Camera intrinsics are optional; when omitted the model
+ # derives them from the output resolution.
+ if intrinsics is None:
+ return
+ if not isinstance(intrinsics, Mapping):
+ raise ValueError("Sana-WM intrinsics must be a {fx, fy, cx, cy} mapping.")
+ required = {"fx", "fy", "cx", "cy"}
+ missing = sorted(required - set(intrinsics))
+ if missing:
+ raise ValueError(f"Sana-WM intrinsics mapping is missing keys: {missing}.")
+ for key in required:
+ value = _as_finite_float(intrinsics[key], name=f"intrinsics[{key!r}]")
+ # ``compute_raymap`` divides by fx/fy. cx/cy stay unconstrained beyond
+ # finiteness — a principal point outside the frame is unusual, not wrong.
+ if key in ("fx", "fy") and value <= 0:
+ raise ValueError(f"Sana-WM intrinsics[{key!r}] must be positive, got {value}.")
+
+
+def _as_positive_int(value: Any, *, name: str) -> int:
+ try:
+ parsed = int(value)
+ except (TypeError, ValueError) as exc:
+ raise ValueError(f"Sana-WM {name} must be an integer, got {value!r}.") from exc
+ if parsed <= 0:
+ raise ValueError(f"Sana-WM {name} must be positive, got {parsed}.")
+ return parsed
+
+
+def _as_positive_float(value: Any, *, name: str) -> float:
+ # Finiteness first: every comparison against NaN is False, so a bare
+ # ``parsed <= 0`` would accept it.
+ parsed = _as_finite_float(value, name=name)
+ if parsed <= 0:
+ raise ValueError(f"Sana-WM {name} must be positive, got {parsed}.")
+ return parsed
+
+
+def _extract_image(mm_data: Mapping[str, Any]) -> Any:
+ return _unwrap_single(mm_data.get("image"))
+
+
+def _first_present(*values: Any) -> Any:
+ for value in values:
+ if value is not None:
+ return value
+ return None
+
+
+def _validate_latent_geometry(*, num_frames: int, height: int, width: int) -> None:
+ """Reject geometry the VAE would silently floor.
+
+ The SANA-WM VAE compresses 32x spatially and 8x temporally, so a request
+ that is not aligned loses pixels/frames instead of failing.
+ """
+ if height % SANA_WM_VAE_SPATIAL_COMPRESSION or width % SANA_WM_VAE_SPATIAL_COMPRESSION:
+ raise ValueError(
+ f"Sana-WM height and width must be divisible by {SANA_WM_VAE_SPATIAL_COMPRESSION}, "
+ f"got {height} and {width}."
+ )
+ if (num_frames - 1) % SANA_WM_VAE_TEMPORAL_COMPRESSION:
+ raise ValueError(
+ f"Sana-WM num_frames must satisfy (num_frames - 1) % {SANA_WM_VAE_TEMPORAL_COMPRESSION} == 0, "
+ f"got {num_frames}."
+ )
+
+
+def normalize_sana_wm_payload(prompt: Mapping[str, Any]) -> dict[str, Any]:
+ """Return a prompt copy with canonical Sana-WM request metadata.
+
+ The ``sana_wm`` block is read from the top-level ``sana_wm`` key, falling
+ back to ``additional_information["sana_wm"]`` so the function is idempotent
+ when called again on its own output. The first-frame image is read from
+ ``multi_modal_data["image"]``.
+ """
+
+ if not isinstance(prompt, Mapping):
+ raise ValueError(f"Sana-WM prompt must be a mapping, got {type(prompt).__name__}.")
+
+ result = dict(prompt)
+ mm_data = _as_dict(result.get("multi_modal_data"), name="multi_modal_data")
+ image = _extract_image(mm_data)
+ if image is None:
+ raise ValueError("Sana-WM requires a first-frame image in `multi_modal_data['image']`.")
+
+ additional = _as_dict(result.get("additional_information"), name="additional_information")
+ raw = _first_present(
+ result.get(SANA_WM_CANONICAL_KEY),
+ additional.get(SANA_WM_CANONICAL_KEY),
+ {},
+ )
+ raw = _as_dict(raw, name=SANA_WM_CANONICAL_KEY)
+
+ num_frames = _as_positive_int(
+ _first_present(raw.get("num_frames"), result.get("num_frames"), SANA_WM_DEFAULT_NUM_FRAMES),
+ name="num_frames",
+ )
+ height = _as_positive_int(
+ _first_present(raw.get("height"), result.get("height"), SANA_WM_DEFAULT_HEIGHT),
+ name="height",
+ )
+ width = _as_positive_int(_first_present(raw.get("width"), result.get("width"), SANA_WM_DEFAULT_WIDTH), name="width")
+ _validate_latent_geometry(num_frames=num_frames, height=height, width=width)
+
+ camera = raw.get("camera")
+ camera = _as_dict(camera, name="camera") if camera is not None else None
+ action = raw.get("action")
+ if action is not None:
+ if not isinstance(action, str) or not action.strip():
+ raise ValueError("Sana-WM action must be a non-empty string when provided.")
+ # Rejected here rather than in the model: a mismatch used to shorten the
+ # camera tensor silently and then fail deep in the transformer on a
+ # camera/latent frame mismatch, and an unbounded duration used to be
+ # expanded one entry per frame before any of that.
+ rollout_frames = action_rollout_num_frames(action)
+ if rollout_frames != num_frames:
+ raise ValueError(
+ f"Sana-WM action rolls out {rollout_frames} frames but num_frames is {num_frames}. "
+ "Action segment durations must sum to num_frames - 1."
+ )
+ if camera is not None and action is not None:
+ raise ValueError("Sana-WM accepts exactly one of `camera` or `action`, not both.")
+ if camera is None and action is None:
+ raise ValueError("Sana-WM requires either explicit camera poses or an action DSL string.")
+
+ canonical_camera = None
+ if camera is not None:
+ poses = camera.get("poses")
+ if poses is None:
+ raise ValueError("Sana-WM camera payload requires `poses`.")
+ _validate_camera_poses(poses, num_frames=num_frames)
+ # Only the defaults are accepted: nothing downstream reads these two
+ # fields, so honouring another value would mean interpreting w2c poses
+ # as c2w and silently generating the wrong trajectory.
+ camera_format = camera.get("format", SANA_WM_DEFAULT_CAMERA_FORMAT)
+ if camera_format != SANA_WM_DEFAULT_CAMERA_FORMAT:
+ raise ValueError(
+ f"Sana-WM only accepts camera format {SANA_WM_DEFAULT_CAMERA_FORMAT!r}; "
+ f"convert the poses before sending, got {camera_format!r}."
+ )
+ coordinate_system = camera.get("coordinate_system", SANA_WM_DEFAULT_COORDINATE_SYSTEM)
+ if coordinate_system != SANA_WM_DEFAULT_COORDINATE_SYSTEM:
+ raise ValueError(
+ f"Sana-WM only accepts coordinate system {SANA_WM_DEFAULT_COORDINATE_SYSTEM!r}, "
+ f"got {coordinate_system!r}."
+ )
+ canonical_camera = {"poses": poses}
+
+ intrinsics = raw.get("intrinsics")
+ _validate_intrinsics(intrinsics)
+ translation_speed = raw.get("translation_speed")
+ rotation_speed_deg = raw.get("rotation_speed_deg")
+
+ canonical = {
+ "num_frames": num_frames,
+ "height": height,
+ "width": width,
+ "camera": canonical_camera,
+ "action": action,
+ "intrinsics": intrinsics,
+ }
+ if translation_speed is not None:
+ canonical["translation_speed"] = _as_positive_float(translation_speed, name="translation_speed")
+ if rotation_speed_deg is not None:
+ canonical["rotation_speed_deg"] = _as_positive_float(rotation_speed_deg, name="rotation_speed_deg")
+
+ mm_data["image"] = image
+ result["multi_modal_data"] = mm_data
+ additional[SANA_WM_CANONICAL_KEY] = canonical
+ result["additional_information"] = additional
+ result.pop(SANA_WM_CANONICAL_KEY, None)
+ return result
diff --git a/vllm_omni/diffusion/models/sana_wm/sana_wm_transformer.py b/vllm_omni/diffusion/models/sana_wm/sana_wm_transformer.py
new file mode 100644
index 000000000..07537ec09
--- /dev/null
+++ b/vllm_omni/diffusion/models/sana_wm/sana_wm_transformer.py
@@ -0,0 +1,2065 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
+"""SANA-WM Stage-1 transformer.
+
+Native vLLM-Omni port of the NVlabs SANA-WM DiT. Modules are built eagerly at
+construction (under the loader's target-device context) and checkpoint tensors
+are streamed in via ``load_weights``. The Bidirectional Gated DeltaNet
+recurrence is implemented in pure PyTorch in this module.
+"""
+
+from __future__ import annotations
+
+import math
+from collections.abc import Iterable
+from typing import Any, ClassVar
+
+import torch
+import torch.nn.functional as F
+from diffusers.models.embeddings import Timesteps
+from torch import nn
+from vllm.distributed import (
+ get_tensor_model_parallel_rank,
+ get_tensor_model_parallel_world_size,
+ tensor_model_parallel_all_reduce,
+)
+from vllm.distributed.parallel_state import get_tp_group
+from vllm.model_executor.layers.linear import (
+ ColumnParallelLinear,
+ MergedColumnParallelLinear,
+ QKVParallelLinear,
+ RowParallelLinear,
+)
+from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
+from vllm.model_executor.model_loader.weight_utils import sharded_weight_loader
+
+from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
+from vllm_omni.diffusion.attention.layer import Attention
+from vllm_omni.diffusion.layers.adalayernorm import AdaLayerNorm
+from vllm_omni.diffusion.layers.norm import RMSNorm
+from vllm_omni.diffusion.models.sana_wm.config import SanaWmConfig
+from vllm_omni.diffusion.models.sana_wm.ucpe import (
+ SanaWmCamGeometry,
+ cam_prep_func,
+ prepare_cam_geometry,
+)
+
+SANA_WM_STAGE1_LATENT_CHANNELS = 128
+SANA_WM_STAGE1_PROMPT_CHANNELS = 2304
+SANA_WM_STAGE1_TIMESTEP_CHANNELS = 256
+
+
+def _shard_param_across_tp(param: torch.Tensor | None, dim: int = 0) -> None:
+ """Attach vLLM's TP shard loader to a plain (non-parallel-layer) parameter.
+
+ vLLM parallel layers narrow full checkpoint tensors to the local shard via
+ their own ``weight_loader``. SANA-WM also has plain parameters that become
+ TP-local - the GDN vectors (``A_log``, ``dt_bias``), the depthwise temporal
+ convs and the q/k norms - so they need the same behavior at load time.
+ Attached only under TP > 1: at TP = 1 the checkpoint tensor already matches
+ the parameter and ``sharded_weight_loader`` would require an initialized TP
+ group that standalone/unit-test builds do not have.
+ """
+ if param is None or get_tensor_model_parallel_world_size() <= 1:
+ return
+ param.weight_loader = sharded_weight_loader(dim)
+
+
+def _is_sana_wm_transformer_block(name: str, module: Any) -> bool:
+ del module
+ parts = name.split(".")
+ return len(parts) == 2 and parts[0] == "blocks" and parts[1].isdigit()
+
+
+def _to_3tuple(value: int | tuple[int, int] | tuple[int, int, int]) -> tuple[int, int, int]:
+ if isinstance(value, int):
+ return (value, value, value)
+ if len(value) == 2:
+ return (1, int(value[0]), int(value[1]))
+ return (int(value[0]), int(value[1]), int(value[2]))
+
+
+# ---------------------------------------------------------------------------
+# Bidirectional Gated DeltaNet recurrence (pure PyTorch)
+#
+# Ported from the Apache-2.0 NVlabs/Sana reference. SANA-WM's bidirectional
+# video-latent recurrence is a different contract than vLLM's autoregressive
+# Qwen3-Next GDN cache path, so it stays model-local.
+# ---------------------------------------------------------------------------
+
+
+def _validate_gdn_inputs(
+ query: torch.Tensor,
+ key: torch.Tensor,
+ value: torch.Tensor,
+ beta: torch.Tensor,
+ decay: torch.Tensor,
+ spatial_tokens: int,
+) -> tuple[int, int, int, int, int]:
+ if query.ndim != 4 or key.ndim != 4 or value.ndim != 4:
+ raise ValueError("Sana-WM GDN query/key/value must be shaped [B, H, D, N].")
+ if query.shape != key.shape or query.shape != value.shape:
+ raise ValueError(
+ "Sana-WM GDN query/key/value shapes must match, got "
+ f"{tuple(query.shape)}, {tuple(key.shape)}, {tuple(value.shape)}."
+ )
+ if beta.ndim != 4:
+ raise ValueError("Sana-WM GDN beta must be shaped [B, H, T, S].")
+ if decay.ndim != 3:
+ raise ValueError("Sana-WM GDN decay must be shaped [B, H, T].")
+ if spatial_tokens <= 0:
+ raise ValueError("Sana-WM GDN spatial_tokens must be positive.")
+
+ batch_size, num_heads, head_dim, token_count = query.shape
+ if token_count % spatial_tokens != 0:
+ raise ValueError(f"Sana-WM GDN token count {token_count} is not divisible by spatial_tokens={spatial_tokens}.")
+ frames = token_count // spatial_tokens
+ if beta.shape != (batch_size, num_heads, frames, spatial_tokens):
+ raise ValueError(
+ "Sana-WM GDN beta shape mismatch: expected "
+ f"{(batch_size, num_heads, frames, spatial_tokens)}, got {tuple(beta.shape)}."
+ )
+ if decay.shape != (batch_size, num_heads, frames):
+ raise ValueError(
+ f"Sana-WM GDN decay shape mismatch: expected {(batch_size, num_heads, frames)}, got {tuple(decay.shape)}."
+ )
+ return batch_size, num_heads, head_dim, token_count, frames
+
+
+def _delta_scan(
+ query_rot: torch.Tensor,
+ key_rot: torch.Tensor,
+ value: torch.Tensor,
+ beta: torch.Tensor,
+ decay: torch.Tensor,
+ *,
+ spatial_tokens: int,
+ query: torch.Tensor | None = None,
+ key: torch.Tensor | None = None,
+ skip_z: bool = False,
+ flip_output: bool = False,
+) -> tuple[torch.Tensor, torch.Tensor | None]:
+ """One-directional gated delta-rule recurrence over frames.
+
+ The KV stream consumes only the rotary-embedded Q/K; the Z (denominator)
+ stream consumes only the non-rotary Q/K. ``skip_z=True`` drops the Z stream
+ entirely and returns ``(numerator, None)`` — the numerator-only variant the
+ camera branch needs (NVlabs ``torch_recurrent_cam_single_path_delta_rule``),
+ so ``query``/``key`` may be omitted there.
+
+ ``flip_output=True`` emits the frames in reverse of the order they were
+ computed, which is what a caller running on flipped inputs wants; it costs a
+ list reversal instead of a flip over the assembled result.
+ """
+ if not skip_z and (query is None or key is None):
+ raise ValueError("Sana-WM delta scan needs non-rotary query/key unless skip_z is set.")
+
+ batch_size, num_heads, head_dim, token_count = query_rot.shape
+ frames = beta.shape[2]
+ if token_count != frames * spatial_tokens:
+ raise ValueError(f"Sana-WM delta scan token_count={token_count} != frames*S={frames * spatial_tokens}.")
+
+ def to_frames(tensor: torch.Tensor) -> torch.Tensor:
+ return tensor.view(batch_size, num_heads, head_dim, frames, spatial_tokens).permute(0, 1, 3, 2, 4)
+
+ query_rot_f = to_frames(query_rot)
+ key_rot_f = to_frames(key_rot)
+ value_f = to_frames(value)
+ state_kv = torch.zeros(batch_size, num_heads, head_dim, head_dim, device=query_rot.device, dtype=query_rot.dtype)
+ numerators: list[torch.Tensor] = []
+
+ if skip_z:
+ query_f = key_f = None
+ state_z = None
+ denominators = None
+ else:
+ query_f = to_frames(query)
+ key_f = to_frames(key)
+ state_z = torch.zeros(batch_size, num_heads, head_dim, 1, device=query_rot.device, dtype=query_rot.dtype)
+ denominators = []
+
+ for frame_idx in range(frames):
+ query_rot_t = query_rot_f[:, :, frame_idx]
+ key_rot_t = key_rot_f[:, :, frame_idx]
+ value_t = value_f[:, :, frame_idx]
+ beta_t = beta[:, :, frame_idx].unsqueeze(2)
+ decay_t = decay[:, :, frame_idx].view(batch_size, num_heads, 1, 1)
+
+ state_kv = state_kv * decay_t
+ value_pred = torch.matmul(state_kv, key_rot_t)
+ delta_value = (value_t - value_pred) * beta_t
+ state_kv = state_kv + torch.matmul(delta_value, key_rot_t.transpose(-1, -2))
+ numerators.append(torch.matmul(state_kv, query_rot_t))
+
+ if skip_z:
+ continue
+ query_t = query_f[:, :, frame_idx]
+ key_t = key_f[:, :, frame_idx]
+ state_z = state_z * decay_t
+ z_pred = torch.matmul(state_z.transpose(-1, -2), key_t)
+ delta_z = (1.0 - z_pred) * beta_t
+ state_z = state_z + torch.matmul(key_t, delta_z.transpose(-1, -2))
+ denominators.append(torch.matmul(state_z.transpose(-1, -2), query_t))
+
+ def restore(tensors: list[torch.Tensor], dim: int) -> torch.Tensor:
+ stacked = torch.stack(tensors[::-1] if flip_output else tensors, dim=2)
+ return stacked.permute(0, 1, 3, 2, 4).reshape(batch_size, num_heads, dim, token_count)
+
+ return restore(numerators, head_dim), (None if skip_z else restore(denominators, 1))
+
+
+def _bidirectional_delta_scan(
+ query_rot: torch.Tensor,
+ key_rot: torch.Tensor,
+ value: torch.Tensor,
+ beta: torch.Tensor,
+ decay: torch.Tensor,
+ *,
+ spatial_tokens: int,
+ query: torch.Tensor | None = None,
+ key: torch.Tensor | None = None,
+ skip_z: bool = False,
+) -> tuple[torch.Tensor, torch.Tensor | None]:
+ """Forward + backward delta scan, summed.
+
+ The backward direction shifts K/V/beta by one frame (zero pad) and the decay
+ by one frame (neutral 1.0 pad), matching the NVlabs ``flip_and_shift``
+ convention. The backward scan also emits its frames in forward order, so the
+ two directions add directly instead of flipping the assembled result back.
+ """
+ frames = beta.shape[2]
+
+ def reverse(tensor: torch.Tensor, *, shift_value: float | None = None) -> torch.Tensor:
+ return _reverse_frames(tensor, frames=frames, spatial_tokens=spatial_tokens, shift_value=shift_value)
+
+ num_fwd, den_fwd = _delta_scan(
+ query_rot,
+ key_rot,
+ value,
+ beta,
+ decay,
+ spatial_tokens=spatial_tokens,
+ query=query,
+ key=key,
+ skip_z=skip_z,
+ )
+
+ bwd_kwargs: dict[str, torch.Tensor] = {}
+ if not skip_z:
+ bwd_kwargs["query"] = reverse(query)
+ bwd_kwargs["key"] = reverse(key, shift_value=0.0)
+
+ num_bwd, den_bwd = _delta_scan(
+ reverse(query_rot),
+ reverse(key_rot, shift_value=0.0),
+ reverse(value, shift_value=0.0),
+ _flip_and_shift(beta, dim=2, shift_value=0.0),
+ _flip_and_shift(decay, dim=2, shift_value=1.0),
+ spatial_tokens=spatial_tokens,
+ skip_z=skip_z,
+ flip_output=True,
+ **bwd_kwargs,
+ )
+
+ return num_fwd + num_bwd, (None if skip_z else den_fwd + den_bwd)
+
+
+def _reverse_frames(
+ tensor: torch.Tensor,
+ *,
+ frames: int,
+ spatial_tokens: int,
+ shift_value: float | None = None,
+) -> torch.Tensor:
+ """Reverse frame order on a ``[..., T*S]`` tensor, optionally shifting by one frame.
+
+ The frame axis is the outer half of the flat token axis, so unfolding it in
+ place keeps the tensor's own layout: the flip writes a contiguous result and
+ the reshape back is a view. Routing this through a ``[..., T, D, S]``
+ permutation instead would cost a second full materialization.
+ """
+ lead = tensor.shape[:-1]
+ reversed_ = torch.flip(tensor.reshape(*lead, frames, spatial_tokens), dims=[-2])
+ if shift_value is not None:
+ padding = torch.full((*lead, 1, spatial_tokens), shift_value, device=tensor.device, dtype=tensor.dtype)
+ reversed_ = torch.cat([padding, reversed_.narrow(-2, 0, frames - 1)], dim=-2)
+ return reversed_.reshape(*lead, frames * spatial_tokens)
+
+
+def _flip_and_shift(tensor: torch.Tensor, *, dim: int, shift_value: float) -> torch.Tensor:
+ flipped = torch.flip(tensor, dims=[dim])
+ shifted = flipped.narrow(dim, 0, tensor.shape[dim] - 1)
+ pad_shape = list(tensor.shape)
+ pad_shape[dim] = 1
+ padding = torch.full(pad_shape, shift_value, device=tensor.device, dtype=tensor.dtype)
+ return torch.cat([padding, shifted], dim=dim)
+
+
+def reference_bidirectional_gated_delta_net(
+ query: torch.Tensor,
+ key: torch.Tensor,
+ value: torch.Tensor,
+ *,
+ beta: torch.Tensor,
+ decay: torch.Tensor,
+ spatial_tokens: int,
+ query_rot: torch.Tensor | None = None,
+ key_rot: torch.Tensor | None = None,
+ eps: float = 1e-8,
+) -> torch.Tensor:
+ """Run the SANA-WM bidirectional gated delta recurrence in PyTorch.
+
+ Inputs follow the layout used by the official Stage-1 operator:
+ ``query/key/value/query_rot/key_rot`` are ``[B, H, D, T*S]``, ``beta`` is
+ ``[B, H, T, S]``, and ``decay`` is ``[B, H, T]``.
+ """
+
+ _validate_gdn_inputs(query, key, value, beta, decay, spatial_tokens)
+ if query_rot is None:
+ query_rot = query
+ if key_rot is None:
+ key_rot = key
+ if query_rot.shape != query.shape or key_rot.shape != key.shape:
+ raise ValueError("Sana-WM GDN rotary query/key shapes must match query/key.")
+
+ dtype_orig = query.dtype
+ numerator, denominator = _bidirectional_delta_scan(
+ query_rot.float(),
+ key_rot.float(),
+ value.float(),
+ beta.float(),
+ decay.float(),
+ spatial_tokens=spatial_tokens,
+ query=query.float(),
+ key=key.float(),
+ )
+ return (numerator / (denominator + eps)).to(dtype_orig)
+
+
+class TensorParallelRMSNorm(nn.Module):
+ """RMSNorm whose statistics span a tensor-parallel sharded last dimension.
+
+ Models that normalize q/k across *all* heads (``qk_norm="rms_norm_across_heads"``)
+ cannot use a plain RMSNorm once the q/k projections are column-parallel: each
+ rank would then compute the RMS over its local head shard only. This
+ all-reduces the squared sum so the denominator matches the global width,
+ while the affine weight stays local.
+
+ Model-local on purpose: LTX-2 and DreamZero carry their own near-identical
+ copies, and converging all three belongs in its own change rather than in
+ the SANA-WM port.
+ """
+
+ def __init__(
+ self,
+ hidden_size: int,
+ eps: float = 1e-6,
+ elementwise_affine: bool = True,
+ tp_size: int = 1,
+ ) -> None:
+ super().__init__()
+ self.hidden_size = hidden_size
+ self.tp_size = max(int(tp_size), 1)
+ self.global_hidden_size = hidden_size * self.tp_size
+ self.eps = eps
+ if elementwise_affine:
+ self.weight = nn.Parameter(torch.ones(hidden_size))
+ else:
+ self.register_parameter("weight", None)
+
+ def _local_sum_sq(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
+ x_float = x.float()
+ return x_float, x_float.pow(2).sum(dim=-1, keepdim=True)
+
+ def _scale(self, x_float: torch.Tensor, global_sum_sq: torch.Tensor, input_dtype: torch.dtype) -> torch.Tensor:
+ out = x_float * torch.rsqrt(global_sum_sq / self.global_hidden_size + self.eps)
+ if self.weight is not None:
+ out = out * self.weight.float()
+ return out.to(dtype=input_dtype)
+
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
+ x_float, sum_sq = self._local_sum_sq(x)
+ if self.tp_size > 1:
+ sum_sq = tensor_model_parallel_all_reduce(sum_sq)
+ return self._scale(x_float, sum_sq, x.dtype)
+
+
+def fused_qk_rms_norm(
+ norm_q: nn.Module,
+ norm_k: nn.Module,
+ q: torch.Tensor,
+ k: torch.Tensor,
+) -> tuple[torch.Tensor, torch.Tensor]:
+ """Apply q/k :class:`TensorParallelRMSNorm` with a SINGLE fused all-reduce.
+
+ Self-attention normalizes q and k every step, and under TP each norm issues
+ its own tiny all-reduce of the per-token sum-of-squares. These collectives
+ are latency-bound, so packing both sums into one tensor halves the count
+ (2 collectives -> 1) for free.
+
+ Numerically identical to ``norm_q(q), norm_k(k)``: all-reduce is
+ elementwise, so packing along the last dim reduces each slice independently
+ with the same fp32 accumulation. Falls back to independent application when
+ either norm is not a :class:`TensorParallelRMSNorm` (e.g. ``nn.Identity``
+ when qk_norm is off, or plain ``RMSNorm`` at TP=1).
+ """
+ if not (isinstance(norm_q, TensorParallelRMSNorm) and isinstance(norm_k, TensorParallelRMSNorm)):
+ return norm_q(q), norm_k(k)
+ if norm_q.tp_size <= 1:
+ return norm_q(q), norm_k(k)
+
+ q_float, q_sum_sq = norm_q._local_sum_sq(q)
+ k_float, k_sum_sq = norm_k._local_sum_sq(k)
+ packed = tensor_model_parallel_all_reduce(torch.cat([q_sum_sq, k_sum_sq], dim=-1))
+ q_sum_sq, k_sum_sq = packed[..., 0:1], packed[..., 1:2]
+ return norm_q._scale(q_float, q_sum_sq, q.dtype), norm_k._scale(k_float, k_sum_sq, k.dtype)
+
+
+def _make_sharded_qk_rms_norm(hidden_size: int, eps: float = 1e-6) -> nn.Module:
+ """Build the q/k norm, TP-aware.
+
+ SANA-WM normalizes q/k across all heads, so once the projections are
+ column-parallel the RMS denominator has to span the TP ranks.
+ """
+ tp_size = get_tensor_model_parallel_world_size()
+ if tp_size > 1:
+ return TensorParallelRMSNorm(hidden_size, eps=eps, tp_size=tp_size)
+ return RMSNorm(hidden_size, eps=eps)
+
+
+class SanaWmTextProjection(nn.Module):
+ def __init__(
+ self,
+ prompt_channels: int,
+ hidden_size: int,
+ *,
+ quant_config: QuantizationConfig | None = None,
+ prefix: str = "",
+ ) -> None:
+ super().__init__()
+ self.fc1 = ColumnParallelLinear(
+ prompt_channels,
+ hidden_size,
+ bias=True,
+ gather_output=False,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.fc1" if prefix else "fc1",
+ )
+ self.fc2 = RowParallelLinear(
+ hidden_size,
+ hidden_size,
+ bias=True,
+ input_is_parallel=True,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.fc2" if prefix else "fc2",
+ )
+ self.act = nn.SiLU()
+
+ def forward(self, prompt_embeds: torch.Tensor) -> torch.Tensor:
+ return self.fc2(self.act(self.fc1(prompt_embeds)))
+
+
+class SanaWmTextEmbedder(nn.Module):
+ def __init__(
+ self,
+ prompt_channels: int,
+ hidden_size: int,
+ max_length: int,
+ *,
+ quant_config: QuantizationConfig | None = None,
+ prefix: str = "",
+ ) -> None:
+ super().__init__()
+ self.y_embedding = nn.Parameter(torch.zeros(max_length, prompt_channels))
+ self.y_proj = SanaWmTextProjection(
+ prompt_channels,
+ hidden_size,
+ quant_config=quant_config,
+ prefix=f"{prefix}.y_proj" if prefix else "y_proj",
+ )
+
+ def forward(
+ self,
+ prompt_embeds: torch.Tensor | None,
+ *,
+ batch_size: int,
+ dtype: torch.dtype,
+ ) -> torch.Tensor:
+ if prompt_embeds is None:
+ prompt_embeds = self.y_embedding.unsqueeze(0).expand(batch_size, -1, -1)
+ return self.y_proj(prompt_embeds.to(dtype=dtype))
+
+
+class SanaWmTimestepEmbedder(nn.Module):
+ def __init__(
+ self,
+ in_features: int,
+ hidden_size: int,
+ *,
+ quant_config: QuantizationConfig | None = None,
+ prefix: str = "",
+ ) -> None:
+ super().__init__()
+ linear_1 = ColumnParallelLinear(
+ in_features,
+ hidden_size,
+ bias=True,
+ gather_output=False,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.mlp.0" if prefix else "mlp.0",
+ )
+ linear_2 = RowParallelLinear(
+ hidden_size,
+ hidden_size,
+ bias=True,
+ input_is_parallel=True,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.mlp.2" if prefix else "mlp.2",
+ )
+ self.act = nn.SiLU()
+ self.mlp = nn.ModuleList([linear_1, self.act, linear_2])
+ # diffusers' Timesteps reproduces this model's sinusoid exactly:
+ # downscale_freq_shift=0 makes the exponent denominator half_dim, and
+ # flip_sin_to_cos=True emits [cos, sin] in that order. The MLP stays
+ # model-local because its checkpoint keys are mlp.0/mlp.2 and it is
+ # tensor-parallel.
+ self.timesteps_proj = Timesteps(num_channels=in_features, flip_sin_to_cos=True, downscale_freq_shift=0)
+
+ def forward(self, timestep: torch.Tensor) -> torch.Tensor:
+ emb = self.timesteps_proj(timestep.flatten())
+ hidden_states = self.mlp[0](emb.to(dtype=self.mlp[0].weight.dtype))
+ hidden_states = self.act(hidden_states)
+ return self.mlp[2](hidden_states)
+
+
+class SanaWmPatchEmbedMS3D(nn.Module):
+ """Official-style 3D patch embedder used by SANA-WM Stage-1."""
+
+ def __init__(
+ self,
+ patch_size: tuple[int, int, int],
+ in_channels: int,
+ hidden_size: int,
+ *,
+ kernel_size: tuple[int, int, int] | None = None,
+ bias: bool = True,
+ ) -> None:
+ super().__init__()
+ self.patch_size = _to_3tuple(patch_size)
+ self.kernel_size = _to_3tuple(kernel_size or patch_size)
+ self.proj = nn.Conv3d(
+ in_channels,
+ hidden_size,
+ kernel_size=self.kernel_size,
+ stride=self.patch_size,
+ bias=bias,
+ )
+ self.norm = nn.Identity()
+
+ def project_with_shape(self, latents: torch.Tensor) -> tuple[torch.Tensor, tuple[int, int, int]]:
+ hidden_states = self.norm(self.proj(latents))
+ _, _, frames, height, width = hidden_states.shape
+ return hidden_states.flatten(2).transpose(1, 2), (frames, height, width)
+
+ def forward(self, latents: torch.Tensor) -> torch.Tensor:
+ return self.project_with_shape(latents)[0]
+
+
+class SanaWmWanRotaryPosEmbed(nn.Module):
+ """Wan-style 3D RoPE table used by the official SANA-WM GDN blocks."""
+
+ def __init__(
+ self,
+ attention_head_dim: int,
+ *,
+ max_seq_len: int = 1024,
+ theta: float = 10000.0,
+ ) -> None:
+ super().__init__()
+ self.theta = theta
+ h_dim = w_dim = 2 * (attention_head_dim // 6)
+ t_dim = attention_head_dim - h_dim - w_dim
+ self._split_sizes = (
+ t_dim // 2,
+ h_dim // 2,
+ w_dim // 2,
+ )
+ self.freqs = self._build_freqs(max_seq_len)
+
+ def _build_1d_freq(self, dim: int, positions: torch.Tensor) -> torch.Tensor:
+ if dim <= 0:
+ return torch.empty(positions.shape[0], 0, dtype=torch.complex128)
+ freqs = 1.0 / (
+ self.theta ** (torch.arange(0, dim, 2, dtype=torch.float64, device=positions.device)[: dim // 2] / dim)
+ )
+ phase = torch.outer(positions.to(torch.float64), freqs)
+ return torch.polar(torch.ones_like(phase), phase)
+
+ def _build_freqs(self, max_seq_len: int) -> torch.Tensor:
+ positions = torch.arange(max_seq_len, dtype=torch.float64)
+ dims = (
+ self._split_sizes[0] * 2,
+ self._split_sizes[1] * 2,
+ self._split_sizes[2] * 2,
+ )
+ return torch.cat([self._build_1d_freq(dim, positions) for dim in dims], dim=1)
+
+ def forward(self, spatial_shape: tuple[int, int, int], device: torch.device) -> torch.Tensor:
+ frames, height, width = spatial_shape
+ if max(spatial_shape) > self.freqs.shape[0]:
+ self.freqs = self._build_freqs(max(spatial_shape)).to(self.freqs.device)
+ freqs = self.freqs.to(device=device)
+ freqs_f, freqs_h, freqs_w = freqs.split(self._split_sizes, dim=1)
+ f_dim, h_dim, w_dim = self._split_sizes
+ parts = [
+ freqs_f[:frames].view(frames, 1, 1, f_dim).expand(frames, height, width, f_dim),
+ freqs_h[:height].view(1, height, 1, h_dim).expand(frames, height, width, h_dim),
+ freqs_w[:width].view(1, 1, width, w_dim).expand(frames, height, width, w_dim),
+ ]
+ return torch.cat(parts, dim=-1).reshape(1, 1, frames * height * width, -1)
+
+
+class SanaWmSelfAttention(nn.Module):
+ def __init__(
+ self,
+ config: SanaWmConfig,
+ *,
+ use_gdn: bool = True,
+ quant_config: QuantizationConfig | None = None,
+ prefix: str = "",
+ ) -> None:
+ super().__init__()
+ hidden_size = config.hidden_size
+ self.total_num_heads = max(hidden_size // max(config.linear_head_dim, 1), 1)
+ self.head_dim = hidden_size // self.total_num_heads
+ self.num_heads = self.total_num_heads
+ self.num_kv_heads = self.total_num_heads
+ self.eps = 1e-8
+ self.use_gdn = use_gdn and "GDN" in config.attn_type
+ self.patch_size = _to_3tuple(config.patch_size)
+ # Total camera branch width from the checkpoint. TP ColumnParallel
+ # layers expose a per-rank local slice at runtime; keep both sizes so
+ # layer construction uses the global contract while local conv/norm
+ # tensors match q/k/v outputs.
+ self.total_cam_dim = hidden_size // max(config.cam_attn_compress, 1)
+ self.cam_dim = self.total_cam_dim
+ self.qkv = QKVParallelLinear(
+ hidden_size=hidden_size,
+ head_size=self.head_dim,
+ total_num_heads=self.total_num_heads,
+ bias=False,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.qkv" if prefix else "qkv",
+ )
+ self.num_heads = self.qkv.num_heads
+ self.num_kv_heads = self.qkv.num_kv_heads
+ self.proj = RowParallelLinear(
+ self.total_num_heads * self.head_dim,
+ hidden_size,
+ bias=True,
+ input_is_parallel=True,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.proj" if prefix else "proj",
+ )
+ self.beta_proj = ColumnParallelLinear(
+ hidden_size,
+ self.total_num_heads,
+ bias=True,
+ gather_output=False,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.beta_proj" if prefix else "beta_proj",
+ )
+ self.gate_proj = ColumnParallelLinear(
+ hidden_size,
+ self.total_num_heads,
+ bias=True,
+ gather_output=False,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.gate_proj" if prefix else "gate_proj",
+ )
+ self.output_gate = ColumnParallelLinear(
+ hidden_size,
+ self.total_num_heads * self.head_dim,
+ bias=True,
+ gather_output=False,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.output_gate" if prefix else "output_gate",
+ )
+ self.q_proj_cam = ColumnParallelLinear(
+ hidden_size,
+ self.total_cam_dim,
+ bias=True,
+ gather_output=False,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.q_proj_cam" if prefix else "q_proj_cam",
+ )
+ self.k_proj_cam = ColumnParallelLinear(
+ hidden_size,
+ self.total_cam_dim,
+ bias=True,
+ gather_output=False,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.k_proj_cam" if prefix else "k_proj_cam",
+ )
+ self.v_proj_cam = ColumnParallelLinear(
+ hidden_size,
+ self.total_cam_dim,
+ bias=True,
+ gather_output=False,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.v_proj_cam" if prefix else "v_proj_cam",
+ )
+ self.out_proj_cam = RowParallelLinear(
+ self.total_cam_dim,
+ hidden_size,
+ bias=True,
+ input_is_parallel=True,
+ # The camera contribution is summed into the TP-local GDN stream, so
+ # the cross-rank sum and the slice down to the local width collapse
+ # into a single reduce-scatter (see _reduce_scatter_cam_contrib).
+ # RowParallelLinear must therefore not all-reduce, and its bias is
+ # applied after the scatter rather than inside the matmul.
+ reduce_results=False,
+ skip_bias_add=True,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.out_proj_cam" if prefix else "out_proj_cam",
+ )
+ local_inner_dim = self.num_heads * self.head_dim
+ norm_cls = _make_sharded_qk_rms_norm if config.qk_norm else (lambda *_args, **_kwargs: nn.Identity())
+ self.q_norm = norm_cls(local_inner_dim)
+ self.k_norm = norm_cls(local_inner_dim)
+
+ # These names mirror the official GDN checkpoint.
+ self.A_log = nn.Parameter(torch.zeros(self.num_heads))
+ self.dt_bias = nn.Parameter(torch.zeros(self.num_heads))
+ self.register_buffer("recall_gate", torch.zeros(1))
+ self.conv_k = (
+ nn.Conv1d(
+ local_inner_dim,
+ local_inner_dim,
+ kernel_size=config.conv_kernel_size,
+ groups=local_inner_dim,
+ bias=False,
+ )
+ if self.use_gdn and config.conv_kernel_size > 0
+ else None
+ )
+ self.conv_q = None
+ self.conv_v = None
+
+ self.cam_dim = int(getattr(self.q_proj_cam, "output_size_per_partition", self.total_cam_dim))
+
+ cam_compress = max(config.cam_attn_compress, 1)
+ self.cam_heads = max(self.num_heads // cam_compress, 1)
+ if self.cam_dim % self.cam_heads != 0:
+ raise ValueError(f"Sana-WM local cam_dim={self.cam_dim} must be divisible by cam_heads={self.cam_heads}.")
+ self.cam_head_dim = self.cam_dim // self.cam_heads
+ # Under TP, cam_head_dim must equal main head_dim so the WAN RoPE
+ # (built for main head_dim) can be correctly sliced for the cam branch.
+ # A mismatch indicates the cam ColumnParallelLinear layers were not
+ # partitioned consistently with the main QKVParallelLinear.
+ if self.cam_head_dim != self.head_dim:
+ raise ValueError(
+ f"Sana-WM cam_head_dim={self.cam_head_dim} != head_dim={self.head_dim}. "
+ f"Under TP the cam layers must be partitioned by the same TP degree as the "
+ f"main QKV (cam_dim={self.cam_dim}, cam_heads={self.cam_heads}, "
+ f"total_cam_dim={self.total_cam_dim})."
+ )
+ self.q_norm_cam = norm_cls(self.cam_dim)
+ self.k_norm_cam = norm_cls(self.cam_dim)
+ self.conv_k_cam = (
+ nn.Conv1d(
+ self.cam_dim,
+ self.cam_dim,
+ kernel_size=config.conv_kernel_size,
+ groups=self.cam_dim,
+ bias=False,
+ )
+ if self.use_gdn and config.conv_kernel_size > 0
+ else None
+ )
+ self.conv_q_cam = None
+ self.conv_v_cam = None
+ # Softmax hybrid blocks (every ``softmax_every_n``-th) and the camera
+ # branch both run plain non-causal self-attention, so both go through
+ # the shared Attention for platform backend selection and the fp32
+ # fallback. The camera branch gets its own module because
+ # ``cam_attn_compress`` may give it fewer heads.
+ self.softmax_attn = Attention(
+ num_heads=self.num_heads,
+ head_size=self.head_dim,
+ num_kv_heads=self.num_kv_heads,
+ softmax_scale=self.head_dim**-0.5,
+ causal=False,
+ role="self",
+ qkv_layout="BSND",
+ skip_sequence_parallel=True,
+ prefix=prefix,
+ )
+ self.softmax_attn_cam = Attention(
+ num_heads=self.cam_heads,
+ head_size=self.cam_head_dim,
+ num_kv_heads=self.cam_heads,
+ softmax_scale=self.cam_head_dim**-0.5,
+ causal=False,
+ role="self",
+ qkv_layout="BSND",
+ skip_sequence_parallel=True,
+ prefix=prefix,
+ )
+ self._init_short_convs()
+ self._mark_tp_sharded_params()
+
+ def _mark_tp_sharded_params(self) -> None:
+ """Declare which plain parameters are TP-local (see ``_shard_param_across_tp``)."""
+ # Per-head GDN vectors: one entry per local head.
+ _shard_param_across_tp(self.A_log)
+ _shard_param_across_tp(self.dt_bias)
+ # Depthwise temporal convs and q/k norms: one entry per local channel.
+ for module in (self.conv_q, self.conv_k, self.conv_v, self.conv_q_cam, self.conv_k_cam, self.conv_v_cam):
+ if module is not None:
+ _shard_param_across_tp(module.weight)
+ for norm in (self.q_norm, self.k_norm, self.q_norm_cam, self.k_norm_cam):
+ _shard_param_across_tp(getattr(norm, "weight", None))
+
+ @staticmethod
+ def _init_short_conv(conv: nn.Conv1d | None) -> None:
+ if conv is None:
+ return
+ with torch.no_grad():
+ conv.weight.zero_()
+ conv.weight[:, 0, -1] = 1.0
+
+ def _init_short_convs(self) -> None:
+ for conv in (self.conv_q, self.conv_k, self.conv_v, self.conv_q_cam, self.conv_k_cam, self.conv_v_cam):
+ self._init_short_conv(conv)
+
+ @staticmethod
+ def _apply_rotary_emb(hidden_states: torch.Tensor, freqs: torch.Tensor) -> torch.Tensor:
+ rotated = torch.view_as_complex(hidden_states.permute(0, 1, 3, 2).to(torch.float64).unflatten(3, (-1, 2)))
+ output = torch.view_as_real(rotated * freqs).flatten(3, 4).permute(0, 1, 3, 2)
+ return output.type_as(hidden_states)
+
+ @staticmethod
+ def _reshape_to_temporal(
+ hidden_states: torch.Tensor,
+ spatial_shape: tuple[int, int, int],
+ ) -> tuple[torch.Tensor, int, int, int]:
+ batch_size, token_count, hidden_size = hidden_states.shape
+ frames, height, width = spatial_shape
+ spatial_tokens = height * width
+ if token_count != frames * spatial_tokens:
+ raise ValueError(f"Sana-WM temporal conv expects N=T*H*W, got N={token_count}, THW={spatial_shape}.")
+ hidden_states = hidden_states.reshape(batch_size, frames, spatial_tokens, hidden_size)
+ hidden_states = hidden_states.permute(0, 2, 1, 3).reshape(batch_size * spatial_tokens, frames, hidden_size)
+ return hidden_states, batch_size, spatial_tokens, frames
+
+ @staticmethod
+ def _reshape_from_temporal(
+ hidden_states: torch.Tensor,
+ batch_size: int,
+ spatial_tokens: int,
+ frames: int,
+ ) -> torch.Tensor:
+ hidden_size = hidden_states.shape[-1]
+ return (
+ hidden_states.reshape(batch_size, spatial_tokens, frames, hidden_size)
+ .permute(0, 2, 1, 3)
+ .reshape(batch_size, frames * spatial_tokens, hidden_size)
+ )
+
+ @staticmethod
+ def _causal_conv_1d(hidden_states: torch.Tensor, conv: nn.Conv1d) -> torch.Tensor:
+ dtype = hidden_states.dtype
+ conv_input = hidden_states.transpose(1, 2).to(conv.weight.dtype)
+ conv_input = F.pad(conv_input, (conv.kernel_size[0] - 1, 0))
+ output = conv(conv_input).transpose(1, 2)
+ return output.to(dtype)
+
+ def _bidirectional_temporal_short_conv(
+ self,
+ hidden_states: torch.Tensor,
+ conv: nn.Conv1d,
+ spatial_shape: tuple[int, int, int],
+ ) -> torch.Tensor:
+ hidden_states, batch_size, spatial_tokens, frames = self._reshape_to_temporal(hidden_states, spatial_shape)
+ forward_states = self._causal_conv_1d(hidden_states, conv)
+ backward_states = self._causal_conv_1d(hidden_states.flip(1), conv).flip(1)
+ center_weight = conv.weight[:, 0, -1].to(hidden_states.dtype)
+ center_states = hidden_states * center_weight.view(1, 1, -1)
+ output = forward_states + backward_states - center_states
+ return self._reshape_from_temporal(output, batch_size, spatial_tokens, frames)
+
+ def _compute_frame_gates(
+ self,
+ hidden_states: torch.Tensor,
+ spatial_shape: tuple[int, int, int],
+ ) -> tuple[torch.Tensor, torch.Tensor]:
+ batch_size, token_count, hidden_size = hidden_states.shape
+ frames, height, width = spatial_shape
+ spatial_tokens = height * width
+ if token_count != frames * spatial_tokens:
+ raise ValueError(f"Sana-WM GDN token layout mismatch: N={token_count}, expected {frames * spatial_tokens}.")
+ beta = torch.sigmoid(self.beta_proj(hidden_states))
+ beta = beta.reshape(batch_size, frames, spatial_tokens, self.num_heads).permute(0, 3, 1, 2)
+ frame_states = hidden_states.reshape(batch_size, frames, spatial_tokens, hidden_size).mean(dim=2)
+ gate = self.gate_proj(frame_states).float()
+ decay = torch.exp(
+ -self.A_log.float().exp().view(1, 1, -1) * F.softplus(gate + self.dt_bias.float().view(1, 1, -1))
+ )
+ return beta, decay.transpose(1, 2)
+
+ def _forward_gdn_raw(
+ self,
+ hidden_states: torch.Tensor,
+ spatial_shape: tuple[int, int, int],
+ rotary_emb: torch.Tensor | None,
+ *,
+ precomputed_gates: tuple[torch.Tensor, torch.Tensor] | None = None,
+ ) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]:
+ """Run main-branch bidirectional GDN and return the (B, N, q_size)
+ raw output BEFORE ``output_gate`` and ``proj`` are applied.
+
+ Returns ``(raw_output, (beta, decay))`` so the caller can share the
+ beta/decay gates with the camera branch (matches NVlabs' shared
+ ``precomputed_gates`` plumbing).
+ """
+ batch_size, token_count, _hidden_size = hidden_states.shape
+ frames, height, width = spatial_shape
+ spatial_tokens = height * width
+ if token_count != frames * spatial_tokens:
+ raise ValueError(f"Sana-WM GDN expects N=T*H*W, got N={token_count}, THW={spatial_shape}.")
+
+ qkv = self.qkv(hidden_states)
+ q_size = self.num_heads * self.head_dim
+ kv_size = self.num_kv_heads * self.head_dim
+ query, key, value = qkv.split([q_size, kv_size, kv_size], dim=-1)
+ if self.conv_k is not None:
+ key = self._bidirectional_temporal_short_conv(key, self.conv_k, spatial_shape)
+ if precomputed_gates is None:
+ beta, decay = self._compute_frame_gates(hidden_states, spatial_shape)
+ else:
+ beta, decay = precomputed_gates
+
+ query, key = fused_qk_rms_norm(self.q_norm, self.k_norm, query, key)
+ query = query.reshape(batch_size, token_count, self.num_heads, self.head_dim)
+ key = key.reshape(batch_size, token_count, self.num_heads, self.head_dim)
+ value = value.reshape(batch_size, token_count, self.num_heads, self.head_dim)
+
+ query = F.relu(query).permute(0, 2, 3, 1)
+ key = F.relu(key).permute(0, 2, 3, 1)
+ value = value.permute(0, 2, 3, 1)
+ key = key * ((self.head_dim**-0.5) * (spatial_tokens**-0.5))
+ if rotary_emb is not None:
+ query_rot = self._apply_rotary_emb(query, rotary_emb)
+ key_rot = self._apply_rotary_emb(key, rotary_emb)
+ else:
+ query_rot = query
+ key_rot = key
+
+ output = reference_bidirectional_gated_delta_net(
+ query,
+ key,
+ value,
+ beta=beta,
+ decay=decay,
+ spatial_tokens=spatial_tokens,
+ query_rot=query_rot,
+ key_rot=key_rot,
+ eps=self.eps,
+ )
+ output = output.permute(0, 3, 1, 2).reshape(batch_size, token_count, q_size)
+ return output, (beta, decay)
+
+ def _apply_output_gate_and_proj(
+ self,
+ combined: torch.Tensor,
+ hidden_states: torch.Tensor,
+ ) -> torch.Tensor:
+ """Apply SiLU output gate (driven by hidden_states) + final projection.
+
+ The gate reads ``hidden_states``, not ``combined``, and NVlabs applies
+ it once to the main+camera sum rather than per branch.
+ """
+ gate = F.silu(self.output_gate(hidden_states).float())
+ gated = combined * gate
+ return self.proj(gated.to(self.proj.weight.dtype))
+
+ def _reduce_scatter_cam_contrib(self, contrib: torch.Tensor, reference: torch.Tensor) -> torch.Tensor:
+ """Reduce the partial camera projection straight down to the local shard.
+
+ ``out_proj_cam`` is row-parallel over the camera features, so each rank
+ produces a partial sum spanning the full hidden width. The GDN stream it
+ is added to is TP-local (``output_gate`` and ``proj`` both expect the
+ local width), so an all-reduce would hand every rank the full result
+ only for all but one slice of it to be discarded. Reduce-scatter does
+ both steps in one collective and moves 1/tp of the bytes.
+
+ ``skip_bias_add`` keeps the bias out of the matmul so it can be applied
+ to the scattered shard; that slice is the only remaining use of the TP
+ rank here, and it is a 1-D view rather than a copy of the hidden tensor.
+
+ ``tensor_model_parallel_reduce_scatter`` is not usable here: it calls
+ ``get_tp_group().reduce_scatter()``, and vLLM-Omni's diffusion
+ ``GroupCoordinator`` deliberately does not implement that method (see
+ the note in ``diffusion/distributed/parallel_state.py``). Drive
+ ``torch.distributed`` on the group's process group instead, and fall
+ back to all-reduce plus a slice if the group is not exposed.
+ """
+ bias = self.out_proj_cam.bias
+ tp_size = get_tensor_model_parallel_world_size()
+ if tp_size == 1:
+ return contrib if bias is None else contrib + bias
+
+ local_width = reference.shape[-1]
+ if local_width <= 0 or contrib.shape[-1] != local_width * tp_size:
+ raise ValueError(
+ "Sana-WM TP camera contribution width mismatch: "
+ f"contrib={contrib.shape[-1]} expected={local_width * tp_size} (local={local_width}, tp={tp_size})."
+ )
+
+ device_group = getattr(get_tp_group(), "device_group", None)
+ if device_group is None:
+ out = tensor_model_parallel_all_reduce(contrib.contiguous())
+ start = get_tensor_model_parallel_rank() * local_width
+ out = out[..., start : start + local_width].contiguous()
+ else:
+ # reduce_scatter_tensor splits along dim 0, so move the rank axis to
+ # the front first. Rank r then receives the summed columns
+ # [r * local_width, (r + 1) * local_width), matching what the
+ # all-reduce-and-slice fallback produces.
+ batch, tokens, _ = contrib.shape
+ source = (
+ contrib.reshape(batch, tokens, tp_size, local_width)
+ .permute(2, 0, 1, 3)
+ .contiguous()
+ .reshape(tp_size * batch, tokens, local_width)
+ )
+ out = torch.empty((batch, tokens, local_width), device=contrib.device, dtype=contrib.dtype)
+ torch.distributed.reduce_scatter_tensor(out, source, group=device_group)
+
+ if bias is not None:
+ start = get_tensor_model_parallel_rank() * local_width
+ out = out + bias[start : start + local_width]
+ return out
+
+ def _forward_gdn(
+ self,
+ hidden_states: torch.Tensor,
+ spatial_shape: tuple[int, int, int],
+ rotary_emb: torch.Tensor | None,
+ ) -> torch.Tensor:
+ """Main-branch only GDN forward with output_gate + proj applied."""
+ raw, _ = self._forward_gdn_raw(hidden_states, spatial_shape, rotary_emb)
+ return self._apply_output_gate_and_proj(raw, hidden_states)
+
+ @staticmethod
+ def _downscale_to_reference_rms(
+ ref: torch.Tensor,
+ transformed: torch.Tensor,
+ eps: float = 1e-6,
+ ) -> torch.Tensor:
+ """Downscale ``transformed`` so its per-token channel RMS does not
+ exceed ``ref``'s. Mirrors NVlabs ``_downscale_to_reference_rms``.
+
+ Inputs are ``(B, H, N, D)`` (channel dim last). The RMS-scale is
+ clamped at 1.0 — the function only ever shrinks, never amplifies.
+ """
+ ref_rms = ref.square().mean(dim=-1, keepdim=True).add(eps).sqrt()
+ tr_rms = transformed.square().mean(dim=-1, keepdim=True).add(eps).sqrt()
+ scale = (ref_rms / tr_rms.clamp_min(eps)).clamp(max=1.0)
+ return transformed * scale
+
+ @staticmethod
+ def _ucpe_rotary_freqs(rotary_emb: torch.Tensor | None) -> torch.Tensor | None:
+ # SanaWmRope emits (1, 1, N, D//2) complex; the cam path wants (N, D//2).
+ if rotary_emb is None:
+ return None
+ return rotary_emb.squeeze(0).squeeze(0)
+
+ def _forward_softmax_raw(
+ self,
+ hidden_states: torch.Tensor,
+ spatial_shape: tuple[int, int, int],
+ rotary_emb: torch.Tensor | None,
+ ) -> torch.Tensor:
+ """Softmax self-attention raw output before output_gate/proj.
+
+ Mirrors NVlabs ``_forward_softmax_attn(..., apply_output_gate=False)``
+ for every-N-th hybrid blocks. GDN-specific gates are intentionally not
+ used here.
+ """
+ batch_size, token_count, hidden_size = hidden_states.shape
+ frames, height, width = spatial_shape
+ if token_count != frames * height * width:
+ raise ValueError(f"Sana-WM softmax attention expects N=T*H*W, got N={token_count}, THW={spatial_shape}.")
+
+ qkv = self.qkv(hidden_states)
+ q_size = self.num_heads * self.head_dim
+ kv_size = self.num_kv_heads * self.head_dim
+ query, key, value = qkv.split([q_size, kv_size, kv_size], dim=-1)
+
+ query, key = fused_qk_rms_norm(self.q_norm, self.k_norm, query, key)
+ query = query.reshape(batch_size, token_count, self.num_heads, self.head_dim)
+ key = key.reshape(batch_size, token_count, self.num_kv_heads, self.head_dim)
+ value = value.reshape(batch_size, token_count, self.num_kv_heads, self.head_dim)
+
+ if rotary_emb is not None:
+ query = self._apply_rotary_emb(query.permute(0, 2, 3, 1), rotary_emb).permute(0, 3, 1, 2)
+ key = self._apply_rotary_emb(key.permute(0, 2, 3, 1), rotary_emb).permute(0, 3, 1, 2)
+
+ # NVlabs runs the attention in bf16 even under ``fp32_attention=True``,
+ # since FlashAttention only supports bf16/fp16 and fp32 falls back to
+ # the math backend. Keep Q/K/V at the module dtype so an upstream step
+ # that promoted to fp32 does not silently change the kernel. See
+ # ``sana_gdn_camctrl_blocks.py::_forward_softmax_attn_sdpa``.
+ dtype_orig = hidden_states.dtype
+ attn = self.softmax_attn(query.to(dtype_orig), key.to(dtype_orig), value.to(dtype_orig))
+ return attn.reshape(batch_size, token_count, q_size)
+
+ def _forward_softmax_cam_branch(
+ self,
+ hidden_states: torch.Tensor,
+ spatial_shape: tuple[int, int, int],
+ cam_geometry: SanaWmCamGeometry,
+ rotary_emb: torch.Tensor | None,
+ ) -> torch.Tensor:
+ """UCPE camera branch for softmax hybrid blocks.
+
+ Matches NVlabs ``_forward_cam_branch_softmax``: camera Q/K/V are
+ projected from the same hidden stream, transformed by UCPE, run through
+ SDPA, inverse-transformed, then returned as raw ``cam_dim`` features.
+ """
+ batch_size, token_count, _ = hidden_states.shape
+ frames, height, width = spatial_shape
+ if token_count != frames * height * width:
+ raise ValueError(f"Sana-WM softmax cam branch expects N=T*H*W, got N={token_count}, THW={spatial_shape}.")
+
+ q_cam = self.q_proj_cam(hidden_states)
+ k_cam = self.k_proj_cam(hidden_states)
+ v_cam = self.v_proj_cam(hidden_states)
+
+ if self.conv_q_cam is not None:
+ q_cam = self._bidirectional_temporal_short_conv(q_cam, self.conv_q_cam, spatial_shape)
+ if self.conv_k_cam is not None:
+ k_cam = self._bidirectional_temporal_short_conv(k_cam, self.conv_k_cam, spatial_shape)
+ if self.conv_v_cam is not None:
+ v_cam = self._bidirectional_temporal_short_conv(v_cam, self.conv_v_cam, spatial_shape)
+
+ q_cam, k_cam = fused_qk_rms_norm(self.q_norm_cam, self.k_norm_cam, q_cam, k_cam)
+ q_cam = q_cam.reshape(batch_size, token_count, self.cam_heads, self.cam_head_dim)
+ k_cam = k_cam.reshape(batch_size, token_count, self.cam_heads, self.cam_head_dim)
+ v_cam = v_cam.reshape(batch_size, token_count, self.cam_heads, self.cam_head_dim)
+
+ q_cam = q_cam.transpose(1, 2).contiguous() # (B, H_cam, N, D)
+ k_cam = k_cam.transpose(1, 2).contiguous()
+ v_cam = v_cam.transpose(1, 2).contiguous()
+
+ q_cam_trans = cam_geometry.apply_q(q_cam)
+ kv_cam_trans = cam_geometry.apply_kv(torch.cat([k_cam, v_cam], dim=1))
+ k_cam_trans, v_cam_trans = torch.chunk(kv_cam_trans, chunks=2, dim=1)
+
+ q_cam_trans = self._downscale_to_reference_rms(q_cam, q_cam_trans)
+ k_cam_trans = self._downscale_to_reference_rms(k_cam, k_cam_trans)
+ v_cam_trans = self._downscale_to_reference_rms(v_cam, v_cam_trans)
+
+ # The shared Attention takes BSND and selects a backend that supports
+ # ``cam_head_dim`` directly, so odd head dims need no padding up to an
+ # SDPA-friendly size. Q/K/V stay at the module dtype for the same
+ # reason as the main softmax path above.
+ dtype_orig = hidden_states.dtype
+ out = self.softmax_attn_cam(
+ q_cam_trans.transpose(1, 2).to(dtype_orig),
+ k_cam_trans.transpose(1, 2).to(dtype_orig),
+ v_cam_trans.transpose(1, 2).to(dtype_orig),
+ )
+ out = cam_geometry.apply_output(out.transpose(1, 2))
+ return out.transpose(1, 2).reshape(batch_size, token_count, self.cam_dim)
+
+ def _forward_cam_branch(
+ self,
+ hidden_states: torch.Tensor,
+ spatial_shape: tuple[int, int, int],
+ cam_geometry: SanaWmCamGeometry,
+ rotary_emb: torch.Tensor | None,
+ *,
+ precomputed_gates: tuple[torch.Tensor, torch.Tensor],
+ ) -> torch.Tensor:
+ """UCPE camera branch — matches the NVlabs production
+ ``BidirectionalGDNUCPESinglePathLiteLABothTriton`` variant declared
+ in the SANA-WM 1600M release config.
+
+ Two things differ from the main GDN branch and are easy to get wrong:
+ Q/K/V all come from ``hidden_states`` (UCPE is not cross-attention —
+ camera information enters through the per-pixel projection matrices),
+ and the recurrence is single-path, i.e. numerator only, with no Z
+ denominator and no final divide.
+
+ Returns ``(B, N, cam_dim)`` raw — caller applies ``out_proj_cam`` and
+ the shared ``output_gate`` + ``proj``.
+ """
+ batch_size, token_count, _ = hidden_states.shape
+ frames, height, width = spatial_shape
+ spatial_tokens = height * width
+ if token_count != frames * spatial_tokens:
+ raise ValueError(f"Sana-WM cam branch expects N=T*H*W, got N={token_count}, THW={spatial_shape}.")
+
+ q_cam = self.q_proj_cam(hidden_states)
+ k_cam = self.k_proj_cam(hidden_states)
+ v_cam = self.v_proj_cam(hidden_states)
+
+ if self.conv_k_cam is not None:
+ k_cam = self._bidirectional_temporal_short_conv(k_cam, self.conv_k_cam, spatial_shape)
+ if self.conv_q_cam is not None:
+ q_cam = self._bidirectional_temporal_short_conv(q_cam, self.conv_q_cam, spatial_shape)
+ if self.conv_v_cam is not None:
+ v_cam = self._bidirectional_temporal_short_conv(v_cam, self.conv_v_cam, spatial_shape)
+
+ # Normalise through the model's own norm modules: under TP these are
+ # TensorParallelRMSNorm, which all-reduces the squared sum so the
+ # denominator spans the global channel width. Doing the RMS inside
+ # cam_prep_func would normalise over this rank's head shard only. q and
+ # k share a shape here, so the two reductions fuse into one.
+ q_cam, k_cam = fused_qk_rms_norm(self.q_norm_cam, self.k_norm_cam, q_cam, k_cam)
+ q_normed = q_cam.reshape(batch_size, token_count, self.cam_heads, self.cam_head_dim)
+ k_normed = k_cam.reshape(batch_size, token_count, self.cam_heads, self.cam_head_dim)
+
+ # cam_prep_func returns NVlabs' ``(B, H, D, N)`` scan layout directly,
+ # so nothing needs permuting before the recurrence below.
+ v_raw = v_cam.reshape(batch_size, token_count, self.cam_heads, self.cam_head_dim).contiguous()
+ k_scale = (self.cam_head_dim**-0.5) * (spatial_tokens**-0.5)
+ q_rot_bhdn, k_rot_bhdn, v_bhdn, inflation_sq = cam_prep_func(
+ q_normed.contiguous(),
+ k_normed.contiguous(),
+ v_raw,
+ proj_q=cam_geometry.proj_q,
+ proj_kv=cam_geometry.proj_kv,
+ rope_cos=cam_geometry.rope_cos,
+ rope_sin=cam_geometry.rope_sin,
+ k_scale=k_scale,
+ )
+
+ # Discount beta by the raw K inflation instead of shrinking Q/K/V first:
+ # the released NVlabs ``BothTriton`` cam path skips the Python
+ # ``PostUCPERenorm`` step and feeds the scan the raw UCPE-transformed
+ # tensors.
+ beta, decay = precomputed_gates
+ inflation_per_token = inflation_sq.reshape(batch_size, self.cam_heads, frames, spatial_tokens)
+ frame_inflation_sq = inflation_per_token.mean(dim=-1) # (B, H_cam, T)
+ # β/decay come from the main branch, so the cam branch can only reuse
+ # them head-for-head. cam_attn_compress > 1 would give the cam branch
+ # fewer heads and needs a grouping rule this port does not implement;
+ # __init__ already rejects it via the cam_head_dim check.
+ if beta.shape[1] != self.cam_heads:
+ raise ValueError(
+ "Sana-WM cam branch requires cam_heads == main heads (cam_attn_compress=1),"
+ f" got cam_heads={self.cam_heads} main_heads={beta.shape[1]}."
+ )
+ beta_cam = beta / frame_inflation_sq.unsqueeze(-1).clamp_min(1.0)
+ decay_cam = decay
+
+ cam_out_bhdn, _ = _bidirectional_delta_scan(
+ q_rot_bhdn.float(),
+ k_rot_bhdn.float(),
+ v_bhdn.float(),
+ beta_cam.float(),
+ decay_cam.float(),
+ spatial_tokens=spatial_tokens,
+ skip_z=True,
+ )
+ cam_out_bhdn = cam_out_bhdn.to(q_rot_bhdn.dtype)
+
+ # ``apply_output`` expects (B, H, N, D); the scan emits (B, H, D, N).
+ cam_out_bhnd = cam_out_bhdn.transpose(-1, -2).contiguous()
+ cam_out_bhnd = cam_geometry.apply_output(cam_out_bhnd)
+ cam_out_bhdn = cam_out_bhnd.transpose(-1, -2).contiguous()
+
+ # (B, H_cam, D_cam, N) → (B, N, cam_dim)
+ return cam_out_bhdn.permute(0, 3, 1, 2).reshape(batch_size, token_count, self.cam_dim)
+
+ # The GDN/UCPE branch runs a sequential recurrence whose trip count is the
+ # latent frame count, i.e. data-dependent at trace time. Run the whole
+ # attention eagerly (graph break) rather than letting torch.compile unroll
+ # it per shape; the rest of the block (MLP / norms / cross-attention) still
+ # compiles.
+ @torch.compiler.disable
+ def forward(
+ self,
+ hidden_states: torch.Tensor,
+ spatial_shape: tuple[int, int, int],
+ rotary_emb: torch.Tensor | None = None,
+ cam_geometry: SanaWmCamGeometry | None = None,
+ ) -> torch.Tensor:
+ """Forward with dual-branch GDN+UCPE when ``cam_geometry`` is
+ provided, otherwise main-branch-only GDN or softmax fallback.
+
+ ``cam_geometry`` holds the per-pixel 4x4 transforms built once per
+ transformer forward from the raw ``(B, T_latent, 20)`` raymap. The cam
+ branch consumes ``hidden_states`` as its sole Q/K/V source and applies
+ those transforms to the Q/K/V channels via UCPE.
+ """
+ if self.use_gdn:
+ if cam_geometry is None:
+ # No camera conditioning on this request — main branch only.
+ return self._forward_gdn(hidden_states, spatial_shape, rotary_emb)
+ # β/decay are computed here rather than inside each branch: NVlabs
+ # drives both recurrences from one set of gates, and the camera
+ # branch then discounts beta by its own K inflation.
+ beta, decay = self._compute_frame_gates(hidden_states, spatial_shape)
+ main_raw, _ = self._forward_gdn_raw(
+ hidden_states,
+ spatial_shape,
+ rotary_emb,
+ precomputed_gates=(beta, decay),
+ )
+ cam_raw = self._forward_cam_branch(
+ hidden_states,
+ spatial_shape,
+ cam_geometry,
+ rotary_emb,
+ precomputed_gates=(beta, decay),
+ )
+ cam_contrib = self.out_proj_cam(cam_raw)
+ cam_contrib = self._reduce_scatter_cam_contrib(cam_contrib, main_raw)
+ combined = main_raw + cam_contrib.to(main_raw.dtype)
+ attn_out = self._apply_output_gate_and_proj(combined, hidden_states)
+ return attn_out
+
+ main_raw = self._forward_softmax_raw(hidden_states, spatial_shape, rotary_emb)
+ if cam_geometry is not None:
+ cam_raw = self._forward_softmax_cam_branch(
+ hidden_states,
+ spatial_shape,
+ cam_geometry,
+ rotary_emb,
+ )
+ cam_contrib = self.out_proj_cam(cam_raw)
+ cam_contrib = self._reduce_scatter_cam_contrib(cam_contrib, main_raw)
+ main_raw = main_raw + cam_contrib.to(main_raw.dtype)
+ return self._apply_output_gate_and_proj(main_raw, hidden_states)
+
+
+class SanaWmCrossAttention(nn.Module):
+ def __init__(
+ self,
+ config: SanaWmConfig,
+ *,
+ quant_config: QuantizationConfig | None = None,
+ prefix: str = "",
+ ) -> None:
+ super().__init__()
+ hidden_size = config.hidden_size
+ self.total_num_heads = max(hidden_size // max(config.linear_head_dim, 1), 1)
+ self.head_dim = hidden_size // self.total_num_heads
+ self.num_heads = self.total_num_heads
+ inner = self.total_num_heads * self.head_dim
+ self.q_linear = ColumnParallelLinear(
+ hidden_size,
+ inner,
+ bias=True,
+ gather_output=False,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.q_linear" if prefix else "q_linear",
+ )
+ # The checkpoint stores K and V fused as one (2 * inner, hidden)
+ # tensor. A plain ColumnParallelLinear would shard that by the
+ # concatenated output dim, handing all of K to one rank and all of V
+ # to another; MergedColumnParallelLinear knows the block structure
+ # and shards K and V by heads independently, so `chunk(2)` below
+ # yields this rank's K and V directly - no gather, no manual slice.
+ self.kv_linear = MergedColumnParallelLinear(
+ hidden_size,
+ [inner, inner],
+ bias=True,
+ gather_output=False,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.kv_linear" if prefix else "kv_linear",
+ )
+ self.num_heads = self.q_linear.output_size_per_partition // self.head_dim
+ self.proj = RowParallelLinear(
+ inner,
+ hidden_size,
+ bias=True,
+ input_is_parallel=True,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.proj" if prefix else "proj",
+ )
+ local_inner = self.num_heads * self.head_dim
+ norm_cls = _make_sharded_qk_rms_norm if config.cross_norm else (lambda *_args, **_kwargs: nn.Identity())
+ self.q_norm = norm_cls(local_inner)
+ self.k_norm = norm_cls(local_inner)
+ # q is column-parallel and k is sliced to the local width before k_norm,
+ # so both norms hold one weight entry per local channel.
+ _shard_param_across_tp(getattr(self.q_norm, "weight", None))
+ _shard_param_across_tp(getattr(self.k_norm, "weight", None))
+ self.softmax_attn = Attention(
+ num_heads=self.num_heads,
+ head_size=self.head_dim,
+ num_kv_heads=self.num_heads,
+ softmax_scale=1.0 / (self.head_dim**0.5),
+ causal=False,
+ role="cross",
+ qkv_layout="BSND",
+ skip_sequence_parallel=True,
+ disable_kv_quant=True,
+ prefix=prefix,
+ )
+
+ def _reshape_to_seq_heads(self, tensor: torch.Tensor) -> torch.Tensor:
+ batch, seq_len, hidden_size = tensor.shape
+ return tensor.reshape(batch, seq_len, self.num_heads, hidden_size // self.num_heads)
+
+ def forward(
+ self,
+ hidden_states: torch.Tensor,
+ encoder_hidden_states: torch.Tensor,
+ attention_mask: torch.Tensor | None = None,
+ ) -> torch.Tensor:
+ query = self.q_norm(self.q_linear(hidden_states))
+ # MergedColumnParallelLinear emits [K_local; V_local], so the chunk is
+ # already this rank's head shard.
+ key, value = self.kv_linear(encoder_hidden_states).chunk(2, dim=-1)
+ key = self.k_norm(key)
+ if attention_mask is not None and attention_mask.ndim != 2:
+ raise ValueError(
+ f"Sana-WM cross-attention mask must be shaped [B, text_len], got {tuple(attention_mask.shape)}."
+ )
+ query = self._reshape_to_seq_heads(query)
+ key = self._reshape_to_seq_heads(key)
+ value = self._reshape_to_seq_heads(value)
+ # The prompt padding mask goes to the shared Attention through
+ # ``AttentionMetadata``: backends take the [B, text_len] form directly
+ # and convert/reshape it themselves (``_maybe_reshape_attn_mask``), so
+ # cross-attention keeps backend selection. ``None`` keeps the mask-free
+ # fast path - callers normalise an all-ones mask to ``None`` rather than
+ # paying for a no-op mask.
+ attn_metadata = AttentionMetadata(attn_mask=attention_mask) if attention_mask is not None else None
+ attn = self.softmax_attn(query, key, value, attn_metadata)
+ return self.proj(attn.flatten(2, 3))
+
+
+class _ConvWrapper(nn.Module):
+ def __init__(self, conv: nn.Module) -> None:
+ super().__init__()
+ self.conv = conv
+
+
+class SanaWmMbConvFfn(nn.Module):
+ def __init__(self, config: SanaWmConfig) -> None:
+ super().__init__()
+ hidden_size = config.hidden_size
+ expanded = int(hidden_size * config.mlp_ratio) * 2
+ t_padding = config.t_kernel_size // 2
+ self.glu_act = nn.SiLU()
+ self.inverted_conv = _ConvWrapper(nn.Conv2d(hidden_size, expanded, kernel_size=1))
+ self.depth_conv = _ConvWrapper(nn.Conv2d(expanded, expanded, kernel_size=3, padding=1, groups=expanded))
+ self.point_conv = _ConvWrapper(nn.Conv2d(expanded // 2, hidden_size, kernel_size=1, bias=False))
+ self.t_conv = nn.Conv2d(
+ hidden_size,
+ hidden_size,
+ kernel_size=(config.t_kernel_size, 1),
+ padding=(t_padding, 0),
+ bias=False,
+ )
+
+ def forward(self, hidden_states: torch.Tensor, spatial_shape: tuple[int, int, int]) -> torch.Tensor:
+ """Match NVlabs ``GLUMBConvTemp.forward`` exactly:
+
+ 1. Reshape ``(B, N=F*H*W, C) → (B*F, H, W, C) → (B*F, C, H, W)`` so the
+ spatial MBConv runs PER FRAME with proper 2D (H, W) geometry — the
+ 3×3 ``depth_conv`` must not span an (F, H*W) plane.
+ 2. Apply expand → depthwise 3×3 → GLU → contract on per-frame
+ (B*F, C, H, W).
+ 3. Temporal aggregation: reshape back to ``(B, C, F, H*W)``
+ and add ``t_conv`` (kernel ``(3, 1)``) along the F axis.
+ 4. Reshape to ``(B, N, C)``.
+ """
+ batch, _, hidden_size = hidden_states.shape
+ frames, height, width = spatial_shape
+ spatial_tokens = height * width
+ # (B, F*H*W, C) → (B*F, H, W, C) → (B*F, C, H, W)
+ x = (
+ hidden_states.reshape(batch * frames, spatial_tokens, hidden_size)
+ .reshape(batch * frames, height, width, hidden_size)
+ .permute(0, 3, 1, 2)
+ )
+ # Keep this NHWC-derived NCHW view non-contiguous like NVlabs: on bf16
+ # CUDA convs the stride selects a different kernel, and a contiguous
+ # copy breaks late-step parity.
+ # NVlabs ``ConvLayer`` is `Conv → norm → act` with
+ # `act=("silu", "silu", None)`, so inverted_conv activates and the other
+ # two do not. ``_ConvWrapper`` stores only the raw Conv2d (to match the
+ # `inverted_conv.conv.weight` checkpoint key), hence the explicit SiLU.
+ x_inv = self.glu_act(self.inverted_conv.conv(x))
+ x_dep = self.depth_conv.conv(x_inv)
+ value, gate = x_dep.chunk(2, dim=1)
+ x_glu = value * self.glu_act(gate)
+ x_pt = self.point_conv.conv(x_glu) # (B*F, hidden, H, W)
+ x = x_pt
+ # Temporal aggregation: (B*F, C, H, W) → (B, F, C, H*W) → (B, C, F, H*W)
+ x_temporal = x.reshape(batch, frames, hidden_size, spatial_tokens).permute(0, 2, 1, 3)
+ t_conv_out = self.t_conv(x_temporal)
+ x_temporal = x_temporal + t_conv_out
+ # → (B, F, H*W, C) → (B, N, C)
+ final = x_temporal.permute(0, 2, 3, 1).reshape(batch, frames * spatial_tokens, hidden_size).contiguous()
+ return final
+
+
+class SanaWmBlock(nn.Module):
+ def __init__(
+ self,
+ config: SanaWmConfig,
+ *,
+ block_idx: int = 0,
+ quant_config: QuantizationConfig | None = None,
+ prefix: str = "",
+ ) -> None:
+ super().__init__()
+ hidden_size = config.hidden_size
+ use_gdn = config.softmax_every_n <= 0 or (block_idx + 1) % config.softmax_every_n != 0
+ use_plucker_proj = config.use_chunk_plucker_post_attn and (
+ config.chunk_plucker_post_attn_blocks < 0 or block_idx < config.chunk_plucker_post_attn_blocks
+ )
+ self.norm1 = AdaLayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
+ self.attn = SanaWmSelfAttention(
+ config,
+ use_gdn=use_gdn,
+ quant_config=quant_config,
+ prefix=f"{prefix}.attn" if prefix else "attn",
+ )
+ self.cross_attn = SanaWmCrossAttention(
+ config,
+ quant_config=quant_config,
+ prefix=f"{prefix}.cross_attn" if prefix else "cross_attn",
+ )
+ self.norm2 = AdaLayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
+ self.mlp = SanaWmMbConvFfn(config)
+ if use_plucker_proj:
+ self.plucker_proj: nn.Module | None = ColumnParallelLinear(
+ hidden_size,
+ hidden_size,
+ bias=True,
+ gather_output=True,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.plucker_proj" if prefix else "plucker_proj",
+ )
+ else:
+ self.plucker_proj = None
+ self.scale_shift_table = nn.Parameter(torch.zeros(6, hidden_size))
+
+ def forward(
+ self,
+ hidden_states: torch.Tensor,
+ encoder_hidden_states: torch.Tensor,
+ timestep_modulation: torch.Tensor,
+ spatial_shape: tuple[int, int, int],
+ rotary_emb: torch.Tensor | None = None,
+ camera_hidden_states: torch.Tensor | None = None,
+ cam_geometry: SanaWmCamGeometry | None = None,
+ encoder_attention_mask: torch.Tensor | None = None,
+ ) -> torch.Tensor:
+ # Per-frame timestep contract (see SanaWmTransformer3DModel.forward).
+ if timestep_modulation.ndim > 2:
+ return self._forward_frame_aware(
+ hidden_states,
+ encoder_hidden_states,
+ timestep_modulation,
+ spatial_shape,
+ rotary_emb,
+ camera_hidden_states,
+ cam_geometry,
+ encoder_attention_mask,
+ )
+ batch_size = hidden_states.shape[0]
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
+ self.scale_shift_table[None] + timestep_modulation.reshape(batch_size, 6, -1)
+ ).chunk(6, dim=1)
+ attn_input = self.norm1(hidden_states, scale_msa, shift_msa)
+ attn_output = self.attn(attn_input, spatial_shape, rotary_emb, cam_geometry)
+ if camera_hidden_states is not None and self.plucker_proj is not None:
+ attn_output = attn_output + self.plucker_proj(camera_hidden_states)
+ hidden_states = hidden_states + gate_msa * attn_output
+ hidden_states = hidden_states + self.cross_attn(
+ hidden_states,
+ encoder_hidden_states,
+ encoder_attention_mask,
+ )
+ mlp_input = self.norm2(hidden_states, scale_mlp, shift_mlp)
+ hidden_states = hidden_states + gate_mlp * self.mlp(mlp_input, spatial_shape)
+ return hidden_states
+
+ def _forward_frame_aware(
+ self,
+ hidden_states: torch.Tensor,
+ encoder_hidden_states: torch.Tensor,
+ timestep_modulation: torch.Tensor,
+ spatial_shape: tuple[int, int, int],
+ rotary_emb: torch.Tensor | None,
+ camera_hidden_states: torch.Tensor | None,
+ cam_geometry: SanaWmCamGeometry | None,
+ encoder_attention_mask: torch.Tensor | None = None,
+ ) -> torch.Tensor:
+ """Per-frame timestep modulation path matching NVlabs
+ ``SanaVideoMSCamCtrlBlock.forward_frame_aware``.
+
+ ``timestep_modulation`` is ``(B, 1, F, 6*D)``. We split into the
+ six per-frame ``(B, F, 1, D)`` chunks via ``scale_shift_table``
+ and apply ``shift/scale/gate`` per frame, broadcasting over the
+ spatial tokens within each frame.
+ """
+ batch_size, token_count, hidden_size = hidden_states.shape
+ frames, height, width = spatial_shape
+ spatial_tokens = height * width
+ if token_count != frames * spatial_tokens:
+ raise ValueError(f"Sana-WM frame-aware block expects N=T*H*W, got N={token_count}, THW={spatial_shape}.")
+ if timestep_modulation.shape[2] != frames:
+ raise ValueError(
+ "Sana-WM frame-aware block: timestep frame axis must match spatial frames, "
+ f"got modulation frames={timestep_modulation.shape[2]} vs spatial frames={frames}."
+ )
+
+ # (B, 1, F, 6*D) -> (B, F, 6, D), then add the (1, 1, 6, D) table.
+ t_per_frame = timestep_modulation.reshape(batch_size, frames, 6, -1)
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
+ self.scale_shift_table[None, None, :, :] + t_per_frame
+ ).chunk(6, dim=-2) # each: (B, F, 1, D)
+
+ # Apply per-frame modulation by reshaping x to (B, F, S, D), broadcasting
+ # scale/shift over the spatial axis, then flattening back. The reshape
+ # comes first because the norm only reduces over the last axis, so it is
+ # unaffected, and the modulation then broadcasts as written.
+ x_4d = hidden_states.reshape(batch_size, frames, spatial_tokens, hidden_size)
+ x_msa_in = self.norm1(x_4d, scale_msa, shift_msa).reshape(batch_size, token_count, hidden_size)
+ attn_output = self.attn(x_msa_in, spatial_shape, rotary_emb, cam_geometry)
+ if camera_hidden_states is not None and self.plucker_proj is not None:
+ attn_output = attn_output + self.plucker_proj(camera_hidden_states)
+ attn_output_4d = attn_output.reshape(batch_size, frames, spatial_tokens, hidden_size)
+ hidden_states = hidden_states + (gate_msa * attn_output_4d).reshape(batch_size, token_count, hidden_size)
+
+ hidden_states = hidden_states + self.cross_attn(
+ hidden_states,
+ encoder_hidden_states,
+ encoder_attention_mask,
+ )
+
+ x_4d = hidden_states.reshape(batch_size, frames, spatial_tokens, hidden_size)
+ x_mlp_in = self.norm2(x_4d, scale_mlp, shift_mlp).reshape(batch_size, token_count, hidden_size)
+ mlp_out = self.mlp(x_mlp_in, spatial_shape)
+ mlp_out_4d = mlp_out.reshape(batch_size, frames, spatial_tokens, hidden_size)
+ hidden_states = hidden_states + (gate_mlp * mlp_out_4d).reshape(batch_size, token_count, hidden_size)
+ return hidden_states
+
+
+class SanaWmFinalLayer(nn.Module):
+ def __init__(
+ self,
+ hidden_size: int,
+ patch_size: tuple[int, int, int],
+ out_channels: int,
+ *,
+ quant_config: Any = None,
+ prefix: str = "",
+ ) -> None:
+ super().__init__()
+ self.norm_final = AdaLayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
+ self.scale_shift_table = nn.Parameter(torch.zeros(2, hidden_size))
+ self.out_channels = out_channels
+ out_features = math.prod(patch_size) * out_channels
+ self.linear: nn.Module = ColumnParallelLinear(
+ hidden_size,
+ out_features,
+ bias=True,
+ gather_output=True,
+ return_bias=False,
+ quant_config=quant_config,
+ prefix=f"{prefix}.linear" if prefix else "linear",
+ )
+
+ def forward(
+ self,
+ hidden_states: torch.Tensor,
+ timestep_embed: torch.Tensor,
+ spatial_shape: tuple[int, int, int] | None = None,
+ ) -> torch.Tensor:
+ # Per-frame timestep contract (see SanaWmTransformer3DModel.forward).
+ if timestep_embed.ndim > 2:
+ return self._forward_frame_aware(hidden_states, timestep_embed, spatial_shape)
+ shift, scale = (self.scale_shift_table[None] + timestep_embed[:, None]).chunk(2, dim=1)
+ return self.linear(self.norm_final(hidden_states, scale, shift))
+
+ def _forward_frame_aware(
+ self,
+ hidden_states: torch.Tensor,
+ timestep_embed: torch.Tensor,
+ spatial_shape: tuple[int, int, int] | None,
+ ) -> torch.Tensor:
+ """Per-frame final-layer modulation matching NVlabs
+ ``T2IFinalLayer.forward_frame_aware``.
+
+ ``timestep_embed`` is ``(B, 1, F, D)``. We transpose to
+ ``(B, F, 1, D)`` so it adds correctly to the ``(1, 1, 2, D)``
+ ``scale_shift_table`` and produces per-frame ``(B, F, 1, D)``
+ ``shift`` / ``scale`` that broadcast over the spatial tokens
+ within each frame.
+ """
+ batch_size, token_count, hidden_size = hidden_states.shape
+ frames = timestep_embed.shape[2]
+ if spatial_shape is not None:
+ spatial_tokens = spatial_shape[1] * spatial_shape[2]
+ else:
+ spatial_tokens = token_count // frames
+ if frames * spatial_tokens != token_count:
+ raise ValueError(
+ "Sana-WM frame-aware final layer: token count mismatch "
+ f"(N={token_count}, F={frames}, S={spatial_tokens})."
+ )
+ # (B, 1, F, D) -> (B, F, 1, D); add (1, 1, 2, D); chunk into shift/scale: each (B, F, 1, D).
+ t_per_frame = timestep_embed.transpose(1, 2)
+ shift, scale = (self.scale_shift_table[None, None, :, :] + t_per_frame).chunk(2, dim=-2)
+ x_4d = hidden_states.reshape(batch_size, frames, spatial_tokens, hidden_size)
+ x_mod = self.norm_final(x_4d, scale, shift).reshape(batch_size, token_count, hidden_size)
+ return self.linear(x_mod)
+
+
+class SanaWmTransformer3DModel(nn.Module):
+ """SANA-WM Stage-1 DiT: bidirectional Gated DeltaNet blocks with a softmax
+ attention block every ``softmax_every_n``."""
+
+ _repeated_blocks: ClassVar[list[str]] = ["SanaWmBlock"]
+ _layerwise_offload_blocks_attrs: ClassVar[list[str]] = ["blocks"]
+ _hsdp_shard_conditions: ClassVar[list[Any]] = [_is_sana_wm_transformer_block]
+
+ def __init__(
+ self,
+ config: SanaWmConfig | None = None,
+ *,
+ device: torch.device | str | None = None,
+ dtype: torch.dtype | None = None,
+ latent_channels: int = SANA_WM_STAGE1_LATENT_CHANNELS,
+ prompt_channels: int = SANA_WM_STAGE1_PROMPT_CHANNELS,
+ quant_config: QuantizationConfig | None = None,
+ prefix: str = "",
+ ) -> None:
+ super().__init__()
+ self.config = config or SanaWmConfig()
+ self.quant_config = quant_config
+ self.prefix = prefix
+ self._latent_channels = latent_channels
+ self._prompt_channels = prompt_channels
+ self.patch_size = _to_3tuple(self.config.patch_size)
+ self.x_embedder = SanaWmPatchEmbedMS3D(self.patch_size, self._latent_channels, self.config.hidden_size)
+ self.y_embedder = SanaWmTextEmbedder(
+ self._prompt_channels,
+ self.config.hidden_size,
+ self.config.model_max_length,
+ quant_config=self.quant_config,
+ prefix=f"{self.prefix}.y_embedder" if self.prefix else "y_embedder",
+ )
+ self.t_embedder = SanaWmTimestepEmbedder(
+ SANA_WM_STAGE1_TIMESTEP_CHANNELS,
+ self.config.hidden_size,
+ quant_config=self.quant_config,
+ prefix=f"{self.prefix}.t_embedder" if self.prefix else "t_embedder",
+ )
+ # t_block: SiLU → Linear(hidden, 6*hidden). Weight key is t_block.1.{weight,bias}
+ # to match the Stage-1 checkpoint layout (nn.Sequential index 1).
+ _t_block_linear: nn.Module = ColumnParallelLinear(
+ self.config.hidden_size,
+ 6 * self.config.hidden_size,
+ bias=True,
+ gather_output=True,
+ return_bias=False,
+ quant_config=self.quant_config,
+ prefix=f"{self.prefix}.t_block.1" if self.prefix else "t_block.1",
+ )
+ self.t_block = nn.Sequential(nn.SiLU(), _t_block_linear)
+ self.plucker_embedder = SanaWmPatchEmbedMS3D(
+ self.patch_size,
+ self.config.chunk_plucker_channels,
+ self.config.hidden_size,
+ )
+ self.raymap_embedder = SanaWmPatchEmbedMS3D(self.patch_size, 3, self.config.hidden_size)
+ self.blocks = nn.ModuleList(
+ [
+ SanaWmBlock(
+ self.config,
+ block_idx=i,
+ quant_config=self.quant_config,
+ prefix=f"{self.prefix}.blocks.{i}" if self.prefix else f"blocks.{i}",
+ )
+ for i in range(self.config.num_blocks)
+ ]
+ )
+ self.final_layer = SanaWmFinalLayer(
+ self.config.hidden_size,
+ self.patch_size,
+ self._latent_channels,
+ quant_config=self.quant_config,
+ prefix=f"{self.prefix}.final_layer" if self.prefix else "final_layer",
+ )
+ self.pos_embed = nn.Parameter(torch.zeros(1, 484, self.config.hidden_size))
+ self.rope = SanaWmWanRotaryPosEmbed(self.config.linear_head_dim)
+ self.attention_y_norm = RMSNorm(self.config.hidden_size)
+ if device is not None or dtype is not None:
+ self.to(device=device, dtype=dtype)
+
+ def _positional_embedding(self, token_count: int, dtype: torch.dtype, device: torch.device) -> torch.Tensor:
+ pos_embed = self.pos_embed.to(device=device, dtype=dtype)
+ if pos_embed.shape[1] == token_count:
+ return pos_embed
+ pos_embed = pos_embed.transpose(1, 2)
+ pos_embed = F.interpolate(pos_embed, size=token_count, mode="linear", align_corners=False)
+ return pos_embed.transpose(1, 2)
+
+ @staticmethod
+ def _match_tokens(hidden_states: torch.Tensor, expected_tokens: int) -> torch.Tensor:
+ if hidden_states.shape[1] == expected_tokens:
+ return hidden_states
+ if hidden_states.shape[1] > expected_tokens:
+ return hidden_states[:, :expected_tokens]
+ pad = hidden_states[:, -1:].expand(-1, expected_tokens - hidden_states.shape[1], -1)
+ return torch.cat([hidden_states, pad], dim=1)
+
+ def _camera_hidden_states_from_conditions(
+ self,
+ *,
+ plucker: torch.Tensor | None,
+ spatial_raymap: torch.Tensor | None,
+ spatial_shape: tuple[int, int, int],
+ batch_size: int,
+ dtype: torch.dtype,
+ device: torch.device,
+ ) -> torch.Tensor | None:
+ if plucker is None:
+ return None
+ if plucker.ndim == 4:
+ plucker = plucker.unsqueeze(0)
+ plucker = plucker.to(device=device, dtype=dtype)
+ if plucker.shape[0] == 1 and batch_size > 1:
+ plucker = plucker.expand(batch_size, -1, -1, -1, -1)
+ camera_hidden_states = self.plucker_embedder(plucker)
+ expected_tokens = spatial_shape[0] * spatial_shape[1] * spatial_shape[2]
+ camera_hidden_states = self._match_tokens(camera_hidden_states, expected_tokens)
+
+ # Fuse per-pixel ray-direction map via raymap_embedder only on the
+ # non-plucker path. NVlabs skips the absmap/raymap embedder whenever
+ # chunk-plucker post-attention injection is enabled.
+ if spatial_raymap is not None and not self.config.use_chunk_plucker_post_attn:
+ if spatial_raymap.ndim == 4: # [C, F, H, W] → [1, C, F, H, W]
+ spatial_raymap = spatial_raymap.unsqueeze(0)
+ spatial_raymap = spatial_raymap.to(device=device, dtype=dtype)
+ if spatial_raymap.shape[0] == 1 and batch_size > 1:
+ spatial_raymap = spatial_raymap.expand(batch_size, -1, -1, -1, -1)
+ ray_hidden = self._match_tokens(self.raymap_embedder(spatial_raymap), expected_tokens)
+ camera_hidden_states = camera_hidden_states + ray_hidden
+
+ return camera_hidden_states
+
+ def _unpatchify(self, hidden_states: torch.Tensor, spatial_shape: tuple[int, int, int]) -> torch.Tensor:
+ batch_size = hidden_states.shape[0]
+ frames, height, width = spatial_shape
+ patch_frames, patch_height, patch_width = self.patch_size
+ hidden_states = hidden_states.reshape(
+ batch_size,
+ frames,
+ height,
+ width,
+ patch_frames,
+ patch_height,
+ patch_width,
+ self._latent_channels,
+ )
+ hidden_states = torch.einsum("nfhwopqc->ncfohpwq", hidden_states)
+ return hidden_states.reshape(
+ batch_size,
+ self._latent_channels,
+ frames * patch_frames,
+ height * patch_height,
+ width * patch_width,
+ )
+
+ def forward(
+ self,
+ hidden_states: torch.Tensor,
+ timestep: torch.Tensor | float | int,
+ *,
+ encoder_hidden_states: torch.Tensor | None = None,
+ encoder_attention_mask: torch.Tensor | None = None,
+ camera_hidden_states: torch.Tensor | None = None,
+ plucker: torch.Tensor | None = None,
+ raymap: torch.Tensor | None = None,
+ spatial_raymap: torch.Tensor | None = None,
+ ) -> torch.Tensor:
+ if hidden_states.ndim != 5:
+ raise ValueError("Sana-WM transformer expects latent input shaped [B, C, F, H, W].")
+ batch_size = hidden_states.shape[0]
+ latent_shape = hidden_states.shape[2:]
+ hidden_states, spatial_shape = self.x_embedder.project_with_shape(hidden_states)
+ rotary_emb = None
+ if self.config.pos_embed_type == "wan_rope":
+ rotary_emb = self.rope(spatial_shape, hidden_states.device)
+ else:
+ hidden_states = hidden_states + self._positional_embedding(
+ hidden_states.shape[1], hidden_states.dtype, hidden_states.device
+ )
+
+ encoder_hidden_states = self.y_embedder(
+ encoder_hidden_states.to(device=hidden_states.device) if encoder_hidden_states is not None else None,
+ batch_size=batch_size,
+ dtype=hidden_states.dtype,
+ )
+ encoder_hidden_states = self.attention_y_norm(encoder_hidden_states)
+ # No mask means "attend to every prompt token"; keep it ``None`` so
+ # cross-attention takes the mask-free attention path rather than
+ # building an all-ones mask the backend would have to reshape per block.
+ if encoder_attention_mask is not None:
+ encoder_attention_mask = encoder_attention_mask.to(device=hidden_states.device)
+ if encoder_attention_mask.shape != encoder_hidden_states.shape[:2]:
+ raise ValueError(
+ "Sana-WM encoder_attention_mask must match text tokens "
+ f"{tuple(encoder_hidden_states.shape[:2])}, got {tuple(encoder_attention_mask.shape)}."
+ )
+
+ if not torch.is_tensor(timestep):
+ timestep = torch.tensor([timestep], device=hidden_states.device, dtype=torch.float32)
+ timestep = timestep.to(device=hidden_states.device)
+ if timestep.ndim == 0:
+ timestep = timestep.expand(batch_size)
+ elif timestep.ndim == 1 and timestep.shape[0] == 1 and batch_size > 1:
+ timestep = timestep.expand(batch_size)
+ # Per-frame timestep contract: a rank > 1 ``timestep`` (``(B, 1, F)``
+ # from the LTX flow-matching sampler) embeds on the flattened token axis
+ # and unflattens back, giving ``(B, 1, F, D)`` / ``(B, 1, F, 6*D)``.
+ # SanaWmBlock and SanaWmFinalLayer dispatch on ``ndim > 2`` to the
+ # frame-aware paths that broadcast modulation over spatial tokens.
+ timestep_shape = tuple(timestep.shape)
+ time_embed = self.t_embedder(timestep) # (numel, D)
+ _t_silu = self.t_block[0](time_embed)
+ timestep_modulation = self.t_block[1](_t_silu).to(hidden_states.dtype)
+ if len(timestep_shape) > 1:
+ time_embed = time_embed.unflatten(0, timestep_shape)
+ timestep_modulation = timestep_modulation.unflatten(0, timestep_shape)
+
+ if camera_hidden_states is None:
+ camera_hidden_states = self._camera_hidden_states_from_conditions(
+ plucker=plucker,
+ spatial_raymap=spatial_raymap,
+ spatial_shape=spatial_shape,
+ batch_size=batch_size,
+ dtype=hidden_states.dtype,
+ device=hidden_states.device,
+ )
+
+ # Build (B, T_latent, 20) camera_conditions from the raw raymap, then
+ # derive the UCPE geometry ONCE for the whole forward. The ray grid,
+ # its SE(3) inverse and the camera RoPE tables are identical in every
+ # block, so building them per block cost ~2400 rebuilds per request
+ # (20 blocks x 60 steps x 2 CFG branches). When the caller passes
+ # pre-embedded camera_hidden_states without raymap, the cam branch
+ # becomes a no-op (main-branch only).
+ cam_geometry: SanaWmCamGeometry | None = None
+ if raymap is not None:
+ # ``_pack_camera_conditions`` emits an unbatched (T_latent, 20).
+ camera_conditions = raymap.unsqueeze(0)
+ if batch_size > 1:
+ camera_conditions = camera_conditions.expand(batch_size, -1, -1)
+ camera_conditions = camera_conditions.to(device=hidden_states.device, dtype=hidden_states.dtype)
+ # cam_head_dim comes from the attention module rather than being
+ # re-derived here: __init__ already asserts it equals the main
+ # head_dim and is consistent across blocks.
+ attn = self.blocks[0].attn
+ cam_geometry = prepare_cam_geometry(
+ camera_conditions=camera_conditions,
+ spatial_shape=spatial_shape,
+ patch_size=self.patch_size,
+ head_dim=attn.cam_head_dim,
+ rotary_emb=attn._ucpe_rotary_freqs(rotary_emb),
+ )
+
+ for block in self.blocks:
+ hidden_states = block(
+ hidden_states,
+ encoder_hidden_states,
+ timestep_modulation,
+ spatial_shape,
+ rotary_emb,
+ camera_hidden_states,
+ cam_geometry,
+ encoder_attention_mask,
+ )
+
+ hidden_states = self.final_layer(hidden_states, time_embed.to(hidden_states.dtype), spatial_shape)
+ hidden_states = self._unpatchify(hidden_states, spatial_shape)
+ if hidden_states.shape[2:] != latent_shape:
+ hidden_states = F.interpolate(hidden_states, size=latent_shape, mode="trilinear", align_corners=False)
+ return hidden_states
+
+ def load_weights(self, weights: Iterable[tuple[str, Any]]) -> set[str]:
+ """Stream checkpoint tensors into the eagerly-built modules.
+
+ Follows the wan2_2 idiom: one ``named_parameters`` lookup table plus the
+ per-tensor ``weight_loader``, which narrows full checkpoint tensors to
+ the TP-local shard at copy time for both vLLM parallel layers and the
+ plain parameters marked by ``_shard_param_across_tp``. The source tensor
+ is dropped each iteration — no copy of the checkpoint is retained.
+ """
+ params_dict = dict(self.named_parameters())
+ buffers_dict = dict(self.named_buffers())
+ loaded: set[str] = set()
+ unmapped: list[str] = []
+ duplicates: list[str] = []
+
+ for source_name, tensor in weights:
+ # Weights ship in the standard diffusers ``transformer/`` layout, so
+ # the checkpoint key is already the module-local parameter name.
+ if source_name in loaded:
+ duplicates.append(source_name)
+ continue
+ if not isinstance(tensor, torch.Tensor):
+ raise TypeError(f"Sana-WM weight {source_name!r} must be a torch.Tensor, got {type(tensor).__name__}.")
+ target = params_dict.get(source_name)
+ if target is None:
+ target = buffers_dict.get(source_name)
+ if target is None:
+ unmapped.append(source_name)
+ continue
+ weight_loader = getattr(target, "weight_loader", None)
+ if callable(weight_loader):
+ # vLLM parallel layers and the plain params marked by
+ # `_shard_param_across_tp` narrow to the local shard here.
+ weight_loader(target, tensor)
+ else:
+ if tuple(target.shape) != tuple(tensor.shape):
+ raise ValueError(
+ f"Sana-WM weight shape mismatch for {source_name}: "
+ f"expected {tuple(target.shape)}, got {tuple(tensor.shape)}."
+ )
+ with torch.no_grad():
+ target.copy_(tensor.to(device=target.device, dtype=target.dtype))
+ loaded.add(source_name)
+
+ if unmapped or duplicates:
+ details = []
+ if unmapped:
+ details.append(f"unmapped={unmapped[:10]}")
+ if duplicates:
+ details.append(f"duplicates={duplicates[:10]}")
+ raise ValueError("Invalid SANA-WM Stage-1 checkpoint keys: " + "; ".join(details))
+ return loaded
diff --git a/vllm_omni/diffusion/models/sana_wm/ucpe.py b/vllm_omni/diffusion/models/sana_wm/ucpe.py
new file mode 100644
index 000000000..88a3a115d
--- /dev/null
+++ b/vllm_omni/diffusion/models/sana_wm/ucpe.py
@@ -0,0 +1,536 @@
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
+"""SANA-WM UCPE (Unified Camera Pose Embedding) per-block attention transforms.
+
+Ported from NVlabs/Sana ``sana_camctrl_blocks.py`` for the inference-only
+SANA-WM bidirectional 1600M release. Scope is intentionally narrow:
+
+* Pinhole camera only (xi=0); the UCM ``xi`` parameter is fixed.
+* Inference path only — no training-time camera-branch dropout.
+* Online computation only — the precomputed ``cam_pos_embeds`` shortcut
+ used by NVlabs' fused kernels is omitted because vLLM-Omni builds the
+ matrices once per transformer forward and shares them across all blocks.
+* ``apply_vo=True`` only — the SANA-WM checkpoint always uses the
+ inverse-output transform.
+
+Public surface:
+ ``prepare_cam_geometry(...)`` -> :class:`SanaWmCamGeometry`, built once per
+ transformer forward and threaded through every block. It carries both the
+ ``(apply_q, apply_kv, apply_output)`` closures used by the softmax camera
+ branch and the projection/RoPE tensors used by ``cam_prep_func(...)``, the
+ NVlabs fused camera-prep contract on the GDN camera branch.
+
+Each closure transforms a tensor of shape ``(B, num_heads, N, D)`` where
+``N = T * H * W`` (matching the GDN token layout) and ``D == head_dim``,
+returning a tensor of the same shape.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable
+from dataclasses import dataclass
+from functools import partial
+
+import torch
+import torch.nn.functional as F
+
+# ---------------------------------------------------------------------------
+# Pinhole ray-grid construction
+# ---------------------------------------------------------------------------
+
+
+def _make_pixel_grid(
+ height: int,
+ width: int,
+ device: torch.device,
+ dtype: torch.dtype,
+) -> torch.Tensor:
+ """Return ``(H, W, 3)`` grid ``(x, y, 1)`` of pixel coordinates."""
+ xs = torch.arange(width, device=device, dtype=dtype)
+ ys = torch.arange(height, device=device, dtype=dtype)
+ y_grid, x_grid = torch.meshgrid(ys, xs, indexing="ij")
+ return torch.stack((x_grid, y_grid, torch.ones_like(x_grid)), dim=-1)
+
+
+def _unproject_pinhole(
+ fx: torch.Tensor,
+ fy: torch.Tensor,
+ cx: torch.Tensor,
+ cy: torch.Tensor,
+ height: int,
+ width: int,
+) -> torch.Tensor:
+ """Unproject a pixel grid into camera-frame direction vectors.
+
+ Args:
+ fx, fy, cx, cy: shape ``(B, T)`` intrinsics in token-grid units — the
+ caller divides all four by patch size before calling.
+ height, width: token-grid size.
+
+ Returns:
+ ``(B, T, H, W, 3)`` direction tensor (NOT normalised).
+ """
+ B, T = fx.shape
+ device = fx.device
+ dtype = fx.dtype
+ grid = _make_pixel_grid(height, width, device, dtype) # (H, W, 3)
+ u = grid[..., 0] # (H, W)
+ v = grid[..., 1]
+ fx_e = fx.view(B, T, 1, 1)
+ fy_e = fy.view(B, T, 1, 1)
+ cx_e = cx.view(B, T, 1, 1)
+ cy_e = cy.view(B, T, 1, 1)
+ x = (u - cx_e) / fx_e
+ y = (v - cy_e) / fy_e
+ z = torch.ones_like(x)
+ # Match NVlabs UCM normalisation at xi=0: alpha = 1, gamma = 1/(1+r^2),
+ # X = gamma*x, Y = gamma*y, Z = gamma. The resulting vector has unit
+ # norm. Computed compactly via direct stack + normalise.
+ d_cam = torch.stack((x, y, z), dim=-1) # (B, T, H, W, 3)
+ d_cam = d_cam / d_cam.norm(dim=-1, keepdim=True).clamp_min(1e-12)
+ return d_cam
+
+
+def _world_to_ray_mats(
+ d_cam: torch.Tensor, # (B, T, H, W, 3) unit rays in camera frame
+ c2w: torch.Tensor, # (B, T, 4, 4) camera-to-world SE(3)
+) -> torch.Tensor:
+ """Build per-pixel ``ray <- world`` SE(3) transforms ``(B, T, H, W, 4, 4)``.
+
+ The ray frame is anchored at the camera origin with the +Z axis along
+ the per-pixel viewing direction and a +Y axis defined by the camera-Y
+ cross product. This matches NVlabs ``world_to_ray_mats``.
+ """
+ B, T, H, W, _ = d_cam.shape
+ device = d_cam.device
+ dtype = d_cam.dtype
+ rot_cam = c2w[..., :3, :3]
+ trans_cam = c2w[..., :3, 3]
+ d_world = torch.einsum("btij,bthwj->bthwi", rot_cam, d_cam)
+ cam_y = rot_cam[..., :, 1].unsqueeze(2).unsqueeze(3).expand(B, T, H, W, 3)
+ z_ray = F.normalize(d_world, dim=-1, eps=1e-6)
+ x_ray = F.normalize(torch.cross(cam_y, z_ray, dim=-1), dim=-1, eps=1e-6)
+ y_ray = F.normalize(torch.cross(z_ray, x_ray, dim=-1), dim=-1, eps=1e-6)
+ rot_local_to_world = torch.stack((x_ray, y_ray, z_ray), dim=-1) # (B,T,H,W,3,3)
+ rot_world_to_local = rot_local_to_world.transpose(-1, -2)
+ t_world = trans_cam.unsqueeze(2).unsqueeze(3).expand(B, T, H, W, 3)
+ t_w2l = -torch.einsum("bthwij,bthwj->bthwi", rot_world_to_local, t_world)
+ raymats = torch.zeros(B, T, H, W, 4, 4, device=device, dtype=dtype)
+ raymats[..., :3, :3] = rot_world_to_local
+ raymats[..., :3, 3] = t_w2l
+ raymats[..., 3, 3] = 1.0
+ # Guard NaNs from degenerate pixels (e.g. nan direction at infinity).
+ nan_mask = torch.isnan(d_world).any(-1)
+ if nan_mask.any():
+ eye = torch.eye(4, device=device, dtype=dtype)
+ raymats[nan_mask] = eye
+ return raymats
+
+
+def _process_camera_conditions(
+ camera_conditions: torch.Tensor, # (B, T, 20)
+ hw: tuple[int, int, int],
+ patch_size: tuple[int, int, int] = (1, 1, 1),
+) -> torch.Tensor:
+ """Convert ``(B, T, 20)`` SANA-WM camera conditions to ``(B, T, H, W, 4, 4)``
+ ``ray <- world`` transforms.
+
+ Layout of the last axis is ``[c2w_flat (16), fx, fy, cx, cy]``.
+ Intrinsics follow the NVlabs UCPE contract: the packed camera payload is
+ expressed on the pre-patch latent image grid, while ``HW`` is the
+ transformer token grid. ``patch_size`` maps those units to the token grid
+ before unprojection. The released SANA-WM P1 config uses ``(1, 1, 1)``.
+
+ Args:
+ camera_conditions: ``(B, T, 20)`` from ``_pack_camera_conditions``.
+ hw: ``(T, H, W)`` token-grid shape. Equal to the latent grid only
+ because the released config patches by ``(1, 1, 1)``.
+ """
+ B, T_cond, last = camera_conditions.shape
+ if last != 20:
+ raise ValueError(f"SANA-WM camera_conditions last dim must be 20, got {last}.")
+ T_latent, H_latent, W_latent = hw
+ if T_cond != T_latent:
+ raise ValueError(f"SANA-WM camera_conditions frames {T_cond} != latent frames {T_latent}.")
+ c2w = camera_conditions[..., :16].reshape(B, T_latent, 4, 4)
+ patch_t, patch_h, patch_w = patch_size
+ del patch_t
+ fx = camera_conditions[..., 16] / float(patch_w)
+ fy = camera_conditions[..., 17] / float(patch_h)
+ cx = camera_conditions[..., 18] / float(patch_w)
+ cy = camera_conditions[..., 19] / float(patch_h)
+ d_cam = _unproject_pinhole(fx, fy, cx, cy, H_latent, W_latent)
+ raymats = _world_to_ray_mats(d_cam, c2w)
+ return raymats
+
+
+def _invert_se3(transforms: torch.Tensor) -> torch.Tensor:
+ """Closed-form inverse of a stack of 4x4 SE(3) matrices."""
+ if transforms.shape[-2:] != (4, 4):
+ raise ValueError(f"SE3 inverse expects (..., 4, 4), got {tuple(transforms.shape)}.")
+ rot_inv = transforms[..., :3, :3].transpose(-1, -2)
+ out = torch.zeros_like(transforms)
+ out[..., :3, :3] = rot_inv
+ out[..., :3, 3] = -torch.einsum("...ij,...j->...i", rot_inv, transforms[..., :3, 3])
+ out[..., 3, 3] = 1.0
+ return out
+
+
+# ---------------------------------------------------------------------------
+# Per-token transform primitives
+# ---------------------------------------------------------------------------
+
+
+def _apply_ray_projmat(
+ feats: torch.Tensor, # (B, num_heads, N, D)
+ matrix: torch.Tensor, # (B, N, 4, 4)
+) -> torch.Tensor:
+ """Apply a per-token 4x4 projection to features grouped by 4 channels.
+
+ ``D`` must be a multiple of 4. Out-of-place; preserves ``feats.shape``.
+ """
+ B, num_heads, N, D = feats.shape
+ if D % 4 != 0:
+ raise ValueError(f"UCPE projmat expects D divisible by 4, got D={D}.")
+ feats_grouped = feats.reshape(B, num_heads, N, D // 4, 4)
+ # matrix: (B, N, 4, 4); feats_grouped: (B, num_heads, N, K, 4)
+ out = torch.einsum("bnij,bhnkj->bhnki", matrix, feats_grouped)
+ return out.reshape(B, num_heads, N, D)
+
+
+def _apply_complex_rope(
+ hidden_states: torch.Tensor,
+ freqs: torch.Tensor,
+ *,
+ inverse: bool = False,
+) -> torch.Tensor:
+ """Apply complex RoPE: treat last-dim pairs as ``(real, imag)`` and multiply
+ by ``freqs``. Computation runs in fp64 to match the NVlabs reference.
+
+ Args:
+ hidden_states: ``(..., D)`` with ``D % 2 == 0``.
+ freqs: complex tensor broadcastable to ``(..., D // 2)``.
+ inverse: when ``True`` apply ``freqs.conj()`` (used for ``apply_o``).
+ """
+ x_real = hidden_states.to(torch.float64).contiguous()
+ x_complex = torch.view_as_complex(x_real.unflatten(-1, (-1, 2)))
+ if inverse:
+ freqs = freqs.conj()
+ x_out = torch.view_as_real(x_complex * freqs).flatten(-2, -1)
+ return x_out.to(hidden_states.dtype)
+
+
+def _apply_block_diagonal(
+ feats: torch.Tensor,
+ func_size_pairs: list[tuple[Callable[[torch.Tensor], torch.Tensor], int]],
+) -> torch.Tensor:
+ """Split ``feats`` along the last axis by ``block_sizes`` and apply one
+ callable per block, then concatenate. Preserves ``feats.shape``.
+ """
+ funcs, block_sizes = zip(*func_size_pairs)
+ if feats.shape[-1] != sum(block_sizes):
+ raise ValueError(f"UCPE block-diagonal block sum {sum(block_sizes)} != feats D {feats.shape[-1]}.")
+ blocks = torch.split(feats, list(block_sizes), dim=-1)
+ return torch.cat([func(block) for func, block in zip(funcs, blocks)], dim=-1)
+
+
+def _slice_rope_for_cam(
+ rotary_emb: torch.Tensor | None,
+ rope_dim: int,
+) -> torch.Tensor | None:
+ """Re-slice WAN-style RoPE frequencies for a smaller ``rope_dim`` using the
+ same ``(T, H, W)`` split as the main branch. Mirrors NVlabs slicing logic.
+
+ The T/H/W band layout is read off the input shape
+ (``rotary_emb.shape[-1] * 2``) rather than a separately supplied head_dim,
+ so the split stays correct without the caller passing it. ``cam_head_dim``
+ still has to equal the main ``head_dim`` — ``SanaWmSelfAttention.__init__``
+ rejects anything else.
+ """
+ if rotary_emb is None:
+ return None
+ # Derive WAN RoPE T/H/W band offsets from the actual input tensor size,
+ # not from a head_dim argument. This is correct regardless of whether
+ # cam_head_dim == main_head_dim.
+ in_head_dim = rotary_emb.shape[-1] * 2
+ orig_t = in_head_dim // 2 - 2 * (in_head_dim // 6)
+ orig_h = in_head_dim // 6
+ new_t = rope_dim // 2 - 2 * (rope_dim // 6)
+ new_h = rope_dim // 6
+ new_w = rope_dim // 6
+ t_part = rotary_emb[..., :new_t]
+ h_part = rotary_emb[..., orig_t : orig_t + new_h]
+ w_part = rotary_emb[..., orig_t + orig_h : orig_t + orig_h + new_w]
+ return torch.cat([t_part, h_part, w_part], dim=-1)
+
+
+# ---------------------------------------------------------------------------
+# Apply-fn preparation
+# ---------------------------------------------------------------------------
+
+
+def _identity(x: torch.Tensor) -> torch.Tensor:
+ return x
+
+
+def _prepare_ray_apply_fns(
+ head_dim: int,
+ proj: torch.Tensor, # (B, N, 4, 4) ray <- world
+ proj_t: torch.Tensor, # (B, N, 4, 4) ray <- world transpose
+ proj_inv: torch.Tensor, # (B, N, 4, 4) world <- ray
+ rotary_emb: torch.Tensor | None = None,
+) -> tuple[Callable, Callable, Callable]:
+ """Build ``(apply_q, apply_kv, apply_o)`` block-diagonal callables.
+
+ Each callable transforms a ``(B, num_heads, N, head_dim)`` tensor:
+ the first ``head_dim // 2`` channels go through the per-token 4x4
+ projection, the remaining ``head_dim // 2`` channels go through complex
+ RoPE (or identity when ``rotary_emb`` is None).
+ """
+ if rotary_emb is not None:
+ rope_fwd = partial(_apply_complex_rope, freqs=rotary_emb, inverse=False)
+ rope_inv = partial(_apply_complex_rope, freqs=rotary_emb, inverse=True)
+ else:
+ rope_fwd = _identity
+ rope_inv = _identity
+
+ half = head_dim // 2
+
+ transforms_q = [
+ (partial(_apply_ray_projmat, matrix=proj_t), half),
+ (rope_fwd, half),
+ ]
+ transforms_kv = [
+ (partial(_apply_ray_projmat, matrix=proj_inv), half),
+ (rope_fwd, half),
+ ]
+ transforms_o = [
+ (partial(_apply_ray_projmat, matrix=proj), half),
+ (rope_inv, half),
+ ]
+
+ apply_q = partial(_apply_block_diagonal, func_size_pairs=transforms_q)
+ apply_kv = partial(_apply_block_diagonal, func_size_pairs=transforms_kv)
+ apply_o = partial(_apply_block_diagonal, func_size_pairs=transforms_o)
+ return apply_q, apply_kv, apply_o
+
+
+@dataclass(frozen=True)
+class SanaWmCamGeometry:
+ """UCPE geometry shared by every block of one transformer forward.
+
+ The ray grid, its SE(3) inverse and the camera RoPE tables depend only on
+ the camera payload, the latent grid, the patch size and the head dim — all
+ fixed for the whole forward — yet each block's camera branch used to
+ rebuild them. At 20 blocks x 60 steps x 2 CFG branches that is ~2400
+ rebuilds of an 18480-token ray grid and its 4x4 inverses per request.
+
+ ``apply_q``/``apply_kv``/``apply_output`` serve the softmax camera branch;
+ ``proj_q``/``proj_kv``/``rope_cos``/``rope_sin`` serve the fused
+ ``cam_prep_func`` contract on the GDN branch. Both are derived from the
+ same projection matrices, so they are built together.
+ """
+
+ proj_q: torch.Tensor
+ proj_kv: torch.Tensor
+ rope_cos: torch.Tensor
+ rope_sin: torch.Tensor
+ apply_q: Callable[[torch.Tensor], torch.Tensor]
+ apply_kv: Callable[[torch.Tensor], torch.Tensor]
+ apply_output: Callable[[torch.Tensor], torch.Tensor]
+
+
+def _prepare_ucpe_rope_tables(
+ rotary_emb_cam: torch.Tensor,
+ token_count: int,
+ half_dim: int,
+) -> tuple[torch.Tensor, torch.Tensor]:
+ """Convert complex RoPE freqs to NVlabs' interleaved cos/sin tables.
+
+ The output convention is ``out[d] = x[d] * cos[d] + x[d ^ 1] * sin[d]``.
+ For every complex pair, ``sin[2i] = -imag(freq[i])`` and
+ ``sin[2i + 1] = imag(freq[i])``.
+ """
+ freqs = rotary_emb_cam
+ while freqs.ndim > 2 and freqs.shape[0] == 1:
+ freqs = freqs.squeeze(0)
+ if freqs.ndim != 2:
+ raise ValueError(f"Sana-WM cam RoPE freqs must be 2D after squeeze, got {tuple(freqs.shape)}.")
+ if freqs.shape[0] != token_count:
+ raise ValueError(f"Sana-WM cam RoPE token count {freqs.shape[0]} != {token_count}.")
+ if freqs.shape[1] * 2 != half_dim:
+ raise ValueError(f"Sana-WM cam RoPE dim {freqs.shape[1] * 2} != half_dim {half_dim}.")
+ cos_half = freqs.real.float()
+ sin_half = freqs.imag.float()
+ rope_cos = cos_half.repeat_interleave(2, dim=-1).contiguous()
+ rope_sin = torch.stack((-sin_half, sin_half), dim=-1).reshape(token_count, half_dim).contiguous()
+ return rope_cos, rope_sin
+
+
+def prepare_cam_geometry(
+ *,
+ camera_conditions: torch.Tensor,
+ spatial_shape: tuple[int, int, int],
+ patch_size: tuple[int, int, int],
+ head_dim: int,
+ rotary_emb: torch.Tensor | None,
+) -> SanaWmCamGeometry:
+ """Build the per-forward UCPE geometry: ray projections, RoPE tables, apply fns.
+
+ Call once per transformer forward and share the result across every block —
+ see :class:`SanaWmCamGeometry` for why.
+ """
+ if head_dim % 4 != 0:
+ raise ValueError(f"UCPE head_dim must be divisible by 4, got {head_dim}.")
+ if head_dim % 2 != 0 or (head_dim // 2) % 4 != 0:
+ raise ValueError(f"Sana-WM cam prep head_dim={head_dim} must satisfy D % 2 == 0 and (D/2) % 4 == 0.")
+
+ batch = camera_conditions.shape[0]
+ frames, height, width = spatial_shape
+ token_count = frames * height * width
+ half_dim = head_dim // 2
+
+ raymats = _process_camera_conditions(camera_conditions, spatial_shape, patch_size=patch_size)
+ proj = raymats.reshape(batch, token_count, 4, 4).contiguous()
+ proj_q = proj.transpose(-1, -2).contiguous()
+ proj_kv = _invert_se3(proj).contiguous()
+
+ rotary_emb_cam = _slice_rope_for_cam(rotary_emb, half_dim)
+ if rotary_emb_cam is None:
+ rope_cos = torch.ones(token_count, half_dim, device=camera_conditions.device, dtype=torch.float32)
+ rope_sin = torch.zeros(token_count, half_dim, device=camera_conditions.device, dtype=torch.float32)
+ else:
+ rope_cos, rope_sin = _prepare_ucpe_rope_tables(rotary_emb_cam, token_count, half_dim)
+
+ apply_q, apply_kv, apply_output = _prepare_ray_apply_fns(head_dim, proj, proj_q, proj_kv, rotary_emb=rotary_emb_cam)
+ return SanaWmCamGeometry(
+ proj_q=proj_q,
+ proj_kv=proj_kv,
+ rope_cos=rope_cos,
+ rope_sin=rope_sin,
+ apply_q=apply_q,
+ apply_kv=apply_kv,
+ apply_output=apply_output,
+ )
+
+
+def _validate_cam_prep_inputs(
+ q_raw: torch.Tensor,
+ k_raw: torch.Tensor,
+ v_raw: torch.Tensor,
+ proj_q: torch.Tensor,
+ proj_kv: torch.Tensor,
+ rope_cos: torch.Tensor,
+ rope_sin: torch.Tensor,
+) -> int:
+ """Validate the fused cam-prep contract and return ``head_dim // 2``."""
+ if q_raw.shape != k_raw.shape or q_raw.shape != v_raw.shape:
+ raise ValueError(f"Sana-WM cam prep q/k/v shapes must match, got {q_raw.shape}, {k_raw.shape}, {v_raw.shape}.")
+ if q_raw.ndim != 4:
+ raise ValueError(f"Sana-WM cam prep q/k/v must be (B,N,H,D), got {tuple(q_raw.shape)}.")
+ batch, token_count, _num_heads, head_dim = q_raw.shape
+ if head_dim % 2 != 0 or (head_dim // 2) % 4 != 0:
+ raise ValueError(f"Sana-WM cam prep head_dim={head_dim} must satisfy D % 2 == 0 and (D/2) % 4 == 0.")
+ half_dim = head_dim // 2
+ if proj_q.shape != (batch, token_count, 4, 4):
+ raise ValueError(f"Sana-WM cam prep proj_q shape {tuple(proj_q.shape)} != {(batch, token_count, 4, 4)}.")
+ if proj_kv.shape != (batch, token_count, 4, 4):
+ raise ValueError(f"Sana-WM cam prep proj_kv shape {tuple(proj_kv.shape)} != {(batch, token_count, 4, 4)}.")
+ if rope_cos.shape != (token_count, half_dim) or rope_sin.shape != (token_count, half_dim):
+ raise ValueError(
+ "Sana-WM cam prep rope table shapes must be "
+ f"{(token_count, half_dim)}, got {tuple(rope_cos.shape)} and {tuple(rope_sin.shape)}."
+ )
+ return half_dim
+
+
+def _apply_ray_projection(feats: torch.Tensor, matrix: torch.Tensor) -> torch.Tensor:
+ batch, token_count, num_heads, half_dim = feats.shape
+ grouped = feats.reshape(batch, token_count, num_heads, half_dim // 4, 4)
+ out = torch.einsum("bnij,bnhgj->bnhgi", matrix.float(), grouped)
+ return out.reshape(batch, token_count, num_heads, half_dim)
+
+
+def _apply_interleaved_rope(feats: torch.Tensor, rope_cos: torch.Tensor, rope_sin: torch.Tensor) -> torch.Tensor:
+ half_dim = feats.shape[-1]
+ pair_index = torch.arange(half_dim, device=feats.device) ^ 1
+ paired = feats.index_select(-1, pair_index)
+ return feats * rope_cos.reshape(1, rope_cos.shape[0], 1, half_dim) + paired * rope_sin.reshape(
+ 1, rope_sin.shape[0], 1, half_dim
+ )
+
+
+def cam_prep_func(
+ q_normed: torch.Tensor,
+ k_normed: torch.Tensor,
+ v_raw: torch.Tensor,
+ *,
+ proj_q: torch.Tensor,
+ proj_kv: torch.Tensor,
+ rope_cos: torch.Tensor,
+ rope_sin: torch.Tensor,
+ k_scale: float,
+) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
+ """Native equivalent of NVlabs ``cam_prep_func``.
+
+ The caller is responsible for the q/k RMSNorm, which must run through the
+ model's norm modules so a tensor-parallel shard still normalises over the
+ global channel width. This function only applies ReLU, the K scale, the
+ UCPE ray projection and RoPE.
+
+ Args:
+ q_normed: ``(B, N, H, D)`` camera Q, already RMSNorm'd.
+ k_normed: ``(B, N, H, D)`` camera K, already RMSNorm'd. K must also
+ include the temporal short convolution.
+ v_raw: ``(B, N, H, D)`` raw camera V.
+ proj_q: ``(B, N, 4, 4)`` Q ray projection matrices.
+ proj_kv: ``(B, N, 4, 4)`` K/V ray projection matrices.
+ rope_cos: ``(N, D//2)`` interleaved-pair RoPE cosine table.
+ rope_sin: ``(N, D//2)`` interleaved-pair RoPE sine table.
+ k_scale: ``D^-0.5 * spatial_tokens^-0.5``.
+
+ Returns:
+ ``q_trans``, ``k_trans``, ``v_trans`` in ``(B, H, D, N)`` layout, and
+ ``inflation_sq`` in ``(B, H, N)``.
+ """
+ half_dim = _validate_cam_prep_inputs(
+ q_normed,
+ k_normed,
+ v_raw,
+ proj_q,
+ proj_kv,
+ rope_cos,
+ rope_sin,
+ )
+
+ q_norm = torch.relu(q_normed.float())
+ k_norm = torch.relu(k_normed.float()) * float(k_scale)
+ value = v_raw.float()
+
+ q_half = _apply_ray_projection(q_norm[..., :half_dim], proj_q)
+ k_half = _apply_ray_projection(k_norm[..., :half_dim], proj_kv)
+ v_half = _apply_ray_projection(value[..., :half_dim], proj_kv)
+
+ q_rope = _apply_interleaved_rope(q_norm[..., half_dim:], rope_cos.float(), rope_sin.float())
+ k_rope = _apply_interleaved_rope(k_norm[..., half_dim:], rope_cos.float(), rope_sin.float())
+ v_rope = _apply_interleaved_rope(value[..., half_dim:], rope_cos.float(), rope_sin.float())
+
+ k_pre_sq = k_norm.square().sum(dim=-1).permute(0, 2, 1).contiguous()
+ k_post = torch.cat((k_half, k_rope), dim=-1)
+ k_post_sq = k_post.square().sum(dim=-1).permute(0, 2, 1).contiguous()
+ inflation_sq = k_post_sq.clamp_min(1e-12) / k_pre_sq.clamp_min(1e-12)
+
+ def to_bhdn(tensor: torch.Tensor) -> torch.Tensor:
+ return tensor.permute(0, 2, 3, 1).contiguous().to(v_raw.dtype)
+
+ q_out = to_bhdn(torch.cat((q_half, q_rope), dim=-1))
+ k_out = to_bhdn(k_post)
+ v_out = to_bhdn(torch.cat((v_half, v_rope), dim=-1))
+ return q_out, k_out, v_out, inflation_sq
+
+
+__all__ = [
+ "SanaWmCamGeometry",
+ "cam_prep_func",
+ "prepare_cam_geometry",
+]
diff --git a/vllm_omni/diffusion/registry.py b/vllm_omni/diffusion/registry.py
index 47063bc4a..b248e6690 100644
--- a/vllm_omni/diffusion/registry.py
+++ b/vllm_omni/diffusion/registry.py
@@ -171,6 +171,11 @@ _DIFFUSION_MODELS = {
"pipeline_ming_imagegen",
"MingImagePipeline",
),
+ "SanaWmPipeline": (
+ "sana_wm",
+ "pipeline_sana_wm",
+ "SanaWmPipeline",
+ ),
"InternVLAA1Pipeline": (
"internvla_a1",
"pipeline_internvla_a1",
@@ -633,6 +638,7 @@ _DIFFUSION_PRE_PROCESS_FUNCS = {
"LingBotVideoPipeline": "get_lingbot_video_pre_process_func",
"HunyuanImage3ForCausalMM": "get_hunyuan_image_3_pre_process_func",
"MagiHumanPipeline": "get_magi_human_pre_process_func",
+ "SanaWmPipeline": "get_sana_wm_pre_process_func",
"Cosmos3OmniDiffusersPipeline": "get_cosmos3_pre_process_func",
"Cosmos3OmniPipeline": "get_cosmos3_pre_process_func",
"SoulXSingerPipeline": "get_soulxsinger_pre_process_func",