193 lines
8.7 KiB
Markdown
193 lines
8.7 KiB
Markdown
# 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: <https://huggingface.co/Efficient-Large-Model/SANA-WM_bidirectional>
|
||
|
|
- 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 `<keys>-<duration>` 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.
|