Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- .gitattributes +1 -0
- README.md +22 -6
- app.py +771 -0
- examples/t2v_bubbles.json +101 -0
- examples/t2v_robot.json +155 -0
- examples/t2v_woman.json +77 -0
- examples/ti2v_frame.png +3 -0
- examples/ti2v_robot.json +131 -0
- lingbot_video/__init__.py +36 -0
- lingbot_video/default_negative_prompt.json +57 -0
- lingbot_video/default_negative_prompt_image.json +41 -0
- lingbot_video/fsdp_inference.py +113 -0
- lingbot_video/inference_backend.py +114 -0
- lingbot_video/model_paths.py +28 -0
- lingbot_video/moe_pack_kernels.py +142 -0
- lingbot_video/moe_restore_kernels.py +92 -0
- lingbot_video/native_backend.py +347 -0
- lingbot_video/pipeline_lingbot_video.py +591 -0
- lingbot_video/pipeline_lingbot_video_i2v.py +352 -0
- lingbot_video/runner.py +1401 -0
- lingbot_video/scheduling_flow_unipc.py +796 -0
- lingbot_video/sglang_moe_shim.py +297 -0
- lingbot_video/transformer_lingbot_video.py +1312 -0
- lingbot_video/utils.py +287 -0
- requirements.txt +11 -0
- rewriter_prompts.py +293 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
examples/ti2v_frame.png filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,13 +1,29 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.20.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: LingBot-Video Dense 1.3B
|
| 3 |
+
emoji: 🎬
|
| 4 |
+
colorFrom: red
|
| 5 |
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.20.0
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
+
license: apache-2.0
|
| 10 |
+
short_description: Dense 1.3B embodied video generation — T2V, I2V, T2I
|
| 11 |
+
models:
|
| 12 |
+
- robbyant/lingbot-video-dense-1.3b
|
| 13 |
+
python_version: "3.12"
|
| 14 |
+
startup_duration_timeout: 1h
|
| 15 |
---
|
| 16 |
|
| 17 |
+
# LingBot-Video Dense 1.3B — ZeroGPU demo
|
| 18 |
+
|
| 19 |
+
Text-to-video, image-to-video and text-to-image with
|
| 20 |
+
[LingBot-Video Dense 1.3B](https://huggingface.co/robbyant/lingbot-video-dense-1.3b),
|
| 21 |
+
a lightweight dense video generation model for embodied intelligence (1.3B parameters).
|
| 22 |
+
|
| 23 |
+
This demo runs the dense DiT with:
|
| 24 |
+
|
| 25 |
+
- bf16 transformer + text encoder (Qwen3-VL), fp32 Wan VAE
|
| 26 |
+
- 480p resolution buckets for video, up to 5 s at 24 fps (plus 720p/1080p text-to-image)
|
| 27 |
+
- batched CFG with SDPA attention, TF32
|
| 28 |
+
- automatic prompt enhancement: the official two-step rewriter recipe (expand → JSON map) runs
|
| 29 |
+
on the official rewriter base model (Qwen3.6-27B) via HF Inference Providers
|
app.py
ADDED
|
@@ -0,0 +1,771 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
os.environ.setdefault("HF_HOME", "/tmp/hf_home")
|
| 4 |
+
os.environ.setdefault("HF_MODULES_CACHE", "/tmp/hf_modules")
|
| 5 |
+
os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")
|
| 6 |
+
# LingBot-Video runtime knobs (read at import / first forward).
|
| 7 |
+
os.environ.setdefault("DIFFUSERS_ATTN_BACKEND", "_native_flash")
|
| 8 |
+
|
| 9 |
+
import spaces # noqa: E402 — must be imported before torch
|
| 10 |
+
|
| 11 |
+
import base64
|
| 12 |
+
import io
|
| 13 |
+
import json
|
| 14 |
+
import random
|
| 15 |
+
import re
|
| 16 |
+
import time
|
| 17 |
+
import urllib.request
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
import gradio as gr
|
| 21 |
+
import numpy as np
|
| 22 |
+
import torch
|
| 23 |
+
from PIL import Image
|
| 24 |
+
from diffusers import AutoencoderKLWan
|
| 25 |
+
from diffusers.utils import export_to_video
|
| 26 |
+
from huggingface_hub import snapshot_download
|
| 27 |
+
from transformers import AutoProcessor, Qwen3VLForConditionalGeneration
|
| 28 |
+
|
| 29 |
+
from lingbot_video import (
|
| 30 |
+
FlowUniPCMultistepScheduler,
|
| 31 |
+
LingBotVideoImageToVideoPipeline,
|
| 32 |
+
LingBotVideoPipeline,
|
| 33 |
+
LingBotVideoTransformer3DModel,
|
| 34 |
+
)
|
| 35 |
+
from lingbot_video.pipeline_lingbot_video import (
|
| 36 |
+
DEFAULT_NEGATIVE_PROMPT,
|
| 37 |
+
DEFAULT_NEGATIVE_PROMPT_IMAGE,
|
| 38 |
+
)
|
| 39 |
+
from lingbot_video.utils import num_frames_from_duration
|
| 40 |
+
|
| 41 |
+
from rewriter_prompts import (
|
| 42 |
+
IMAGE_STEP1_EXPAND,
|
| 43 |
+
IMAGE_STEP2_MAP,
|
| 44 |
+
VIDEO_STEP1_EXPAND,
|
| 45 |
+
VIDEO_STEP2_MAP,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 49 |
+
torch.set_float32_matmul_precision("high")
|
| 50 |
+
|
| 51 |
+
MODEL_ID = "robbyant/lingbot-video-dense-1.3b"
|
| 52 |
+
FPS = 24
|
| 53 |
+
MAX_SEED = 2**31 - 1
|
| 54 |
+
MAX_GPU_SECONDS = 180
|
| 55 |
+
REWRITER_MODEL = os.environ.get("REWRITER_MODEL", "Qwen/Qwen3.6-27B:deepinfra")
|
| 56 |
+
|
| 57 |
+
# (height, width), multiples of 16 — official 480p buckets.
|
| 58 |
+
VIDEO_SIZES = {
|
| 59 |
+
"832 × 480 (16:9)": (480, 832),
|
| 60 |
+
"480 × 832 (9:16)": (832, 480),
|
| 61 |
+
"640 × 480 (4:3)": (480, 640),
|
| 62 |
+
"480 × 480 (1:1)": (480, 480),
|
| 63 |
+
}
|
| 64 |
+
IMAGE_SIZES = {
|
| 65 |
+
"832 × 480 (16:9)": (480, 832),
|
| 66 |
+
"480 × 832 (9:16)": (832, 480),
|
| 67 |
+
"480 × 480 (1:1)": (480, 480),
|
| 68 |
+
"1280 × 736 (16:9, 720p)": (736, 1280),
|
| 69 |
+
"736 × 1280 (9:16, 720p)": (1280, 736),
|
| 70 |
+
"1088 × 1088 (1:1, 1080p)": (1088, 1088),
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
EXAMPLES_DIR = Path(__file__).parent / "examples"
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _read_example(name: str) -> str:
|
| 77 |
+
return (EXAMPLES_DIR / name).read_text(encoding="utf-8").strip()
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
print(f"[startup] downloading {MODEL_ID} ...", flush=True)
|
| 81 |
+
t0 = time.perf_counter()
|
| 82 |
+
model_dir = snapshot_download(
|
| 83 |
+
MODEL_ID,
|
| 84 |
+
ignore_patterns=["*.bak_lingbot_video_diffusers"],
|
| 85 |
+
)
|
| 86 |
+
print(f"[startup] snapshot ready in {time.perf_counter() - t0:.1f}s", flush=True)
|
| 87 |
+
|
| 88 |
+
t0 = time.perf_counter()
|
| 89 |
+
transformer = LingBotVideoTransformer3DModel.from_pretrained(
|
| 90 |
+
model_dir, subfolder="transformer", torch_dtype=torch.bfloat16
|
| 91 |
+
)
|
| 92 |
+
text_encoder = Qwen3VLForConditionalGeneration.from_pretrained(
|
| 93 |
+
model_dir, subfolder="text_encoder", dtype=torch.bfloat16, attn_implementation="sdpa"
|
| 94 |
+
)
|
| 95 |
+
processor = AutoProcessor.from_pretrained(model_dir, subfolder="processor")
|
| 96 |
+
vae = AutoencoderKLWan.from_pretrained(model_dir, subfolder="vae", torch_dtype=torch.float32)
|
| 97 |
+
print(f"[startup] components loaded in {time.perf_counter() - t0:.1f}s", flush=True)
|
| 98 |
+
|
| 99 |
+
t0 = time.perf_counter()
|
| 100 |
+
pipe = LingBotVideoPipeline(
|
| 101 |
+
transformer=transformer,
|
| 102 |
+
vae=vae,
|
| 103 |
+
text_encoder=text_encoder,
|
| 104 |
+
processor=processor,
|
| 105 |
+
scheduler=FlowUniPCMultistepScheduler.from_pretrained(model_dir, subfolder="scheduler"),
|
| 106 |
+
).to("cuda")
|
| 107 |
+
pipe_i2v = LingBotVideoImageToVideoPipeline(
|
| 108 |
+
transformer=transformer,
|
| 109 |
+
vae=vae,
|
| 110 |
+
text_encoder=text_encoder,
|
| 111 |
+
processor=processor,
|
| 112 |
+
scheduler=FlowUniPCMultistepScheduler.from_pretrained(model_dir, subfolder="scheduler"),
|
| 113 |
+
)
|
| 114 |
+
print(f"[startup] pipelines on cuda in {time.perf_counter() - t0:.1f}s", flush=True)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def _resolve_seed(seed: float, randomize: bool) -> int:
|
| 118 |
+
if randomize:
|
| 119 |
+
return random.randint(0, MAX_SEED)
|
| 120 |
+
return int(seed)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
# ---------------------------------------------------------------------------
|
| 124 |
+
# Prompt enhancement — the official LingBot rewriter recipe (expand -> JSON map)
|
| 125 |
+
# run on the official rewriter base model (Qwen3.6-27B) via HF Inference
|
| 126 |
+
# Providers. The DiT was trained on these structured JSON captions; plain
|
| 127 |
+
# prompts are far out of distribution and produce severe artifacts.
|
| 128 |
+
# ---------------------------------------------------------------------------
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def _router_chat(text: str, max_tokens: int, image: Image.Image | None = None) -> str:
|
| 132 |
+
token = os.environ.get("HF_TOKEN")
|
| 133 |
+
if not token:
|
| 134 |
+
raise RuntimeError("HF_TOKEN is not configured for prompt enhancement.")
|
| 135 |
+
if image is not None:
|
| 136 |
+
image = image.convert("RGB")
|
| 137 |
+
image.thumbnail((768, 768))
|
| 138 |
+
buf = io.BytesIO()
|
| 139 |
+
image.save(buf, format="JPEG", quality=90)
|
| 140 |
+
data_uri = "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode()
|
| 141 |
+
content = [
|
| 142 |
+
{"type": "image_url", "image_url": {"url": data_uri}},
|
| 143 |
+
{"type": "text", "text": text},
|
| 144 |
+
]
|
| 145 |
+
else:
|
| 146 |
+
content = text
|
| 147 |
+
body = {
|
| 148 |
+
"model": REWRITER_MODEL,
|
| 149 |
+
"messages": [{"role": "user", "content": content}],
|
| 150 |
+
"max_tokens": max_tokens,
|
| 151 |
+
"temperature": 0.0,
|
| 152 |
+
"chat_template_kwargs": {"enable_thinking": False},
|
| 153 |
+
}
|
| 154 |
+
last_error = None
|
| 155 |
+
for attempt in range(2):
|
| 156 |
+
try:
|
| 157 |
+
req = urllib.request.Request(
|
| 158 |
+
"https://router.huggingface.co/v1/chat/completions",
|
| 159 |
+
data=json.dumps(body).encode(),
|
| 160 |
+
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
|
| 161 |
+
)
|
| 162 |
+
with urllib.request.urlopen(req, timeout=180) as resp:
|
| 163 |
+
data = json.load(resp)
|
| 164 |
+
out = (data["choices"][0]["message"].get("content") or "").strip()
|
| 165 |
+
if out:
|
| 166 |
+
return out
|
| 167 |
+
last_error = RuntimeError("empty rewriter response")
|
| 168 |
+
except Exception as exc: # noqa: BLE001
|
| 169 |
+
last_error = exc
|
| 170 |
+
if image is not None:
|
| 171 |
+
# provider may not accept image input — retry text-only
|
| 172 |
+
body["messages"][0]["content"] = text
|
| 173 |
+
image = None
|
| 174 |
+
raise RuntimeError(f"prompt enhancement failed: {last_error}")
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def _extract_caption_json(raw: str) -> dict:
|
| 178 |
+
match = re.search(r"```(?:json)?\s*(\{.*\})\s*```", raw, re.DOTALL)
|
| 179 |
+
s = match.group(1) if match else raw
|
| 180 |
+
start = s.find("{")
|
| 181 |
+
if start < 0:
|
| 182 |
+
raise ValueError("no JSON object in rewriter output")
|
| 183 |
+
s = s[start:]
|
| 184 |
+
try:
|
| 185 |
+
return json.loads(s)
|
| 186 |
+
except json.JSONDecodeError:
|
| 187 |
+
from json_repair import repair_json
|
| 188 |
+
|
| 189 |
+
obj = repair_json(s, return_objects=True)
|
| 190 |
+
if not isinstance(obj, dict):
|
| 191 |
+
raise ValueError("rewriter output is not a JSON object")
|
| 192 |
+
return obj
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
def enhance_prompt(prompt: str, mode: str, duration_s: float | None, image: Image.Image | None = None) -> str:
|
| 196 |
+
"""Two-stage official rewrite: plain prompt -> detailed prose -> JSON caption string."""
|
| 197 |
+
if mode == "t2i":
|
| 198 |
+
step1 = IMAGE_STEP1_EXPAND + "\n\nUser image prompt:\n" + prompt
|
| 199 |
+
else:
|
| 200 |
+
step1 = VIDEO_STEP1_EXPAND + "\n\n" + prompt + f"\n\nVideo Duration: {duration_s:g} seconds"
|
| 201 |
+
prose = _router_chat(step1, 1200, image=image)
|
| 202 |
+
if mode == "t2i":
|
| 203 |
+
step2 = IMAGE_STEP2_MAP + "\n\nDETAILED CAPTION:\n" + prose
|
| 204 |
+
else:
|
| 205 |
+
step2 = (
|
| 206 |
+
VIDEO_STEP2_MAP
|
| 207 |
+
+ f"\n\nVideo Duration: {duration_s:g} seconds\n\nDETAILED CAPTION:\n"
|
| 208 |
+
+ prose
|
| 209 |
+
+ "\n\nOutput the JSON now."
|
| 210 |
+
)
|
| 211 |
+
caption = _extract_caption_json(_router_chat(step2, 6000, image=image))
|
| 212 |
+
return json.dumps(caption, ensure_ascii=False, separators=(",", ":"))
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def _prepare_caption(prompt: str, mode: str, duration_s: float | None, enhance: bool,
|
| 216 |
+
image: Image.Image | None = None) -> str:
|
| 217 |
+
prompt = (prompt or "").strip()
|
| 218 |
+
if not prompt:
|
| 219 |
+
raise gr.Error("Please enter a prompt.")
|
| 220 |
+
if prompt.startswith("{") or not enhance:
|
| 221 |
+
return prompt # already a structured JSON caption, or enhancement disabled
|
| 222 |
+
t0 = time.perf_counter()
|
| 223 |
+
try:
|
| 224 |
+
caption = enhance_prompt(prompt, mode, duration_s, image=image)
|
| 225 |
+
except Exception as exc: # noqa: BLE001
|
| 226 |
+
print(f"[rewriter] failed: {exc}", flush=True)
|
| 227 |
+
raise gr.Error(
|
| 228 |
+
"Prompt enhancement failed (the model needs structured captions to work well). "
|
| 229 |
+
"Please try again, or paste a LingBot JSON caption and disable enhancement."
|
| 230 |
+
)
|
| 231 |
+
print(f"[rewriter] {mode} enhanced in {time.perf_counter() - t0:.1f}s ({len(caption)} chars)", flush=True)
|
| 232 |
+
return caption
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def _latent_tokens(height: int, width: int, num_frames: int) -> int:
|
| 236 |
+
latent_frames = (num_frames - 1) // 4 + 1
|
| 237 |
+
return latent_frames * (height // 16) * (width // 16)
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def _step_seconds(height: int, width: int, num_frames: int) -> float:
|
| 241 |
+
# Per-step cost (CFG included), calibrated relative to 81f @ 480x832.
|
| 242 |
+
# Dense 1.3B is ~3x faster than MoE 30B-A3B per the paper.
|
| 243 |
+
r = _latent_tokens(height, width, num_frames) / 32760.0
|
| 244 |
+
return 2.3 * r * r + 0.45 * r + 0.15
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def _estimate_video_seconds(size_label: str, duration_s: float, steps: int, sizes=VIDEO_SIZES) -> int:
|
| 248 |
+
height, width = sizes[size_label]
|
| 249 |
+
num_frames = num_frames_from_duration(duration_s, FPS)
|
| 250 |
+
return int(20.0 + steps * _step_seconds(height, width, num_frames))
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
def _estimate_image_seconds(size_label: str, steps: int) -> int:
|
| 254 |
+
height, width = IMAGE_SIZES[size_label]
|
| 255 |
+
return int(10.0 + steps * _step_seconds(height, width, 1))
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
def _check_budget(estimate: int) -> None:
|
| 259 |
+
if estimate > MAX_GPU_SECONDS:
|
| 260 |
+
raise gr.Error(
|
| 261 |
+
f"These settings need ~{estimate}s of GPU time (max {MAX_GPU_SECONDS}s). "
|
| 262 |
+
"Reduce the video duration, steps, or resolution."
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def _export_video(frames: np.ndarray) -> str:
|
| 267 |
+
path = f"/tmp/lingbot_{int(time.time() * 1000)}.mp4"
|
| 268 |
+
export_to_video(frames, path, fps=FPS)
|
| 269 |
+
return path
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def _video_gpu_duration(caption, height, width, num_frames, steps, *args, **kwargs) -> int:
|
| 273 |
+
return min(MAX_GPU_SECONDS, int(20.0 + int(steps) * _step_seconds(height, width, num_frames)) + 20)
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
@spaces.GPU(duration=_video_gpu_duration)
|
| 277 |
+
def _gpu_generate_video(caption, height, width, num_frames, steps, guidance_scale, shift,
|
| 278 |
+
negative_prompt, seed, image=None):
|
| 279 |
+
target = pipe_i2v if image is not None else pipe
|
| 280 |
+
kwargs = {"image": image} if image is not None else {}
|
| 281 |
+
t0 = time.perf_counter()
|
| 282 |
+
output = target(
|
| 283 |
+
prompt=caption,
|
| 284 |
+
negative_prompt=negative_prompt.strip() or DEFAULT_NEGATIVE_PROMPT,
|
| 285 |
+
height=height,
|
| 286 |
+
width=width,
|
| 287 |
+
num_frames=num_frames,
|
| 288 |
+
num_inference_steps=int(steps),
|
| 289 |
+
guidance_scale=float(guidance_scale),
|
| 290 |
+
shift=float(shift),
|
| 291 |
+
batch_cfg=True,
|
| 292 |
+
generator=torch.Generator().manual_seed(seed),
|
| 293 |
+
**kwargs,
|
| 294 |
+
)
|
| 295 |
+
torch.cuda.synchronize()
|
| 296 |
+
print(
|
| 297 |
+
f"[timing] {'i2v' if image is not None else 't2v'} {width}x{height} f={num_frames} "
|
| 298 |
+
f"steps={int(steps)} tokens={_latent_tokens(height, width, num_frames)} "
|
| 299 |
+
f"took {time.perf_counter() - t0:.1f}s",
|
| 300 |
+
flush=True,
|
| 301 |
+
)
|
| 302 |
+
return _export_video(output.frames[0])
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
def _image_gpu_duration(caption, height, width, steps, *args, **kwargs) -> int:
|
| 306 |
+
return min(MAX_GPU_SECONDS, int(10.0 + int(steps) * _step_seconds(height, width, 1)) + 15)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
@spaces.GPU(duration=_image_gpu_duration)
|
| 310 |
+
def _gpu_generate_image(caption, height, width, steps, guidance_scale, shift, negative_prompt, seed):
|
| 311 |
+
t0 = time.perf_counter()
|
| 312 |
+
output = pipe(
|
| 313 |
+
prompt=caption,
|
| 314 |
+
negative_prompt=negative_prompt.strip() or DEFAULT_NEGATIVE_PROMPT_IMAGE,
|
| 315 |
+
height=height,
|
| 316 |
+
width=width,
|
| 317 |
+
num_frames=1,
|
| 318 |
+
num_inference_steps=int(steps),
|
| 319 |
+
guidance_scale=float(guidance_scale),
|
| 320 |
+
shift=float(shift),
|
| 321 |
+
batch_cfg=True,
|
| 322 |
+
generator=torch.Generator().manual_seed(seed),
|
| 323 |
+
)
|
| 324 |
+
torch.cuda.synchronize()
|
| 325 |
+
print(f"[timing] t2i {width}x{height} steps={int(steps)} "
|
| 326 |
+
f"took {time.perf_counter() - t0:.1f}s", flush=True)
|
| 327 |
+
frame = output.frames[0][0]
|
| 328 |
+
return Image.fromarray((np.clip(frame, 0, 1) * 255).astype(np.uint8))
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
def generate_t2v(
|
| 332 |
+
prompt: str,
|
| 333 |
+
size_label: str = "832 × 480 (16:9)",
|
| 334 |
+
duration_s: float = 2.0,
|
| 335 |
+
steps: int = 30,
|
| 336 |
+
guidance_scale: float = 3.0,
|
| 337 |
+
shift: float = 3.0,
|
| 338 |
+
negative_prompt: str = "",
|
| 339 |
+
seed: int = 42,
|
| 340 |
+
randomize_seed: bool = True,
|
| 341 |
+
enhance: bool = True,
|
| 342 |
+
progress=gr.Progress(track_tqdm=True),
|
| 343 |
+
):
|
| 344 |
+
"""Generate a short video from a text prompt with LingBot-Video Dense 1.3B.
|
| 345 |
+
|
| 346 |
+
Args:
|
| 347 |
+
prompt: Scene description in natural language (it is auto-expanded into the
|
| 348 |
+
structured caption the model expects), or a raw LingBot JSON caption.
|
| 349 |
+
size_label: Resolution/aspect preset, e.g. "832 × 480 (16:9)", "480 × 832 (9:16)".
|
| 350 |
+
duration_s: Video length in seconds (1.0-5.0) at 24 fps.
|
| 351 |
+
steps: Number of denoising steps (more = more detail, slower).
|
| 352 |
+
guidance_scale: Classifier-free guidance strength.
|
| 353 |
+
shift: Flow-matching timestep shift.
|
| 354 |
+
negative_prompt: What to avoid; empty uses the model default.
|
| 355 |
+
seed: Random seed for reproducibility.
|
| 356 |
+
randomize_seed: If true, ignore seed and use a random one.
|
| 357 |
+
enhance: If true, expand a plain prompt into a structured caption before generation.
|
| 358 |
+
|
| 359 |
+
Returns:
|
| 360 |
+
The generated MP4 video, the seed used, and the structured caption fed to the model.
|
| 361 |
+
"""
|
| 362 |
+
estimate = _estimate_video_seconds(size_label, duration_s, int(steps))
|
| 363 |
+
_check_budget(estimate)
|
| 364 |
+
height, width = VIDEO_SIZES[size_label]
|
| 365 |
+
num_frames = num_frames_from_duration(duration_s, FPS)
|
| 366 |
+
seed = _resolve_seed(seed, randomize_seed)
|
| 367 |
+
caption = _prepare_caption(prompt, "t2v", duration_s, enhance)
|
| 368 |
+
video = _gpu_generate_video(
|
| 369 |
+
caption, height, width, num_frames, steps, guidance_scale, shift, negative_prompt, seed,
|
| 370 |
+
)
|
| 371 |
+
return video, seed, caption
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
def generate_i2v(
|
| 375 |
+
image,
|
| 376 |
+
prompt: str,
|
| 377 |
+
size_label: str = "832 × 480 (16:9)",
|
| 378 |
+
duration_s: float = 2.0,
|
| 379 |
+
steps: int = 30,
|
| 380 |
+
guidance_scale: float = 3.0,
|
| 381 |
+
shift: float = 3.0,
|
| 382 |
+
negative_prompt: str = "",
|
| 383 |
+
seed: int = 42,
|
| 384 |
+
randomize_seed: bool = True,
|
| 385 |
+
enhance: bool = True,
|
| 386 |
+
progress=gr.Progress(track_tqdm=True),
|
| 387 |
+
):
|
| 388 |
+
"""Animate a first-frame image into a short video with LingBot-Video Dense 1.3B.
|
| 389 |
+
|
| 390 |
+
Args:
|
| 391 |
+
image: The first frame to animate (filepath or PIL image).
|
| 392 |
+
prompt: How the scene should evolve, in natural language (auto-expanded), or a raw LingBot JSON caption.
|
| 393 |
+
size_label: Resolution/aspect preset, e.g. "832 × 480 (16:9)".
|
| 394 |
+
duration_s: Video length in seconds (1.0-5.0) at 24 fps.
|
| 395 |
+
steps: Number of denoising steps.
|
| 396 |
+
guidance_scale: Classifier-free guidance strength.
|
| 397 |
+
shift: Flow-matching timestep shift.
|
| 398 |
+
negative_prompt: What to avoid; empty uses the model default.
|
| 399 |
+
seed: Random seed for reproducibility.
|
| 400 |
+
randomize_seed: If true, ignore seed and use a random one.
|
| 401 |
+
enhance: If true, expand a plain prompt into a structured caption before generation.
|
| 402 |
+
|
| 403 |
+
Returns:
|
| 404 |
+
The generated MP4 video, the seed used, and the structured caption fed to the model.
|
| 405 |
+
"""
|
| 406 |
+
if image is None:
|
| 407 |
+
raise gr.Error("Please upload a first-frame image.")
|
| 408 |
+
estimate = _estimate_video_seconds(size_label, duration_s, int(steps)) + 15
|
| 409 |
+
_check_budget(estimate)
|
| 410 |
+
height, width = VIDEO_SIZES[size_label]
|
| 411 |
+
num_frames = num_frames_from_duration(duration_s, FPS)
|
| 412 |
+
seed = _resolve_seed(seed, randomize_seed)
|
| 413 |
+
caption = _prepare_caption(prompt, "ti2v", duration_s, enhance, image=image)
|
| 414 |
+
video = _gpu_generate_video(
|
| 415 |
+
caption, height, width, num_frames, steps, guidance_scale, shift, negative_prompt, seed,
|
| 416 |
+
image=image,
|
| 417 |
+
)
|
| 418 |
+
return video, seed, caption
|
| 419 |
+
|
| 420 |
+
|
| 421 |
+
def generate_t2i(
|
| 422 |
+
prompt: str,
|
| 423 |
+
size_label: str = "832 × 480 (16:9)",
|
| 424 |
+
steps: int = 30,
|
| 425 |
+
guidance_scale: float = 3.0,
|
| 426 |
+
shift: float = 3.0,
|
| 427 |
+
negative_prompt: str = "",
|
| 428 |
+
seed: int = 42,
|
| 429 |
+
randomize_seed: bool = True,
|
| 430 |
+
enhance: bool = True,
|
| 431 |
+
progress=gr.Progress(track_tqdm=True),
|
| 432 |
+
):
|
| 433 |
+
"""Generate an image from a text prompt with LingBot-Video Dense 1.3B.
|
| 434 |
+
|
| 435 |
+
Args:
|
| 436 |
+
prompt: Image description in natural language (auto-expanded), or a raw LingBot JSON caption.
|
| 437 |
+
size_label: Resolution/aspect preset, e.g. "832 × 480 (16:9)", "1088 × 1088 (1:1, 1080p)".
|
| 438 |
+
steps: Number of denoising steps.
|
| 439 |
+
guidance_scale: Classifier-free guidance strength.
|
| 440 |
+
shift: Flow-matching timestep shift.
|
| 441 |
+
negative_prompt: What to avoid; empty uses the model default.
|
| 442 |
+
seed: Random seed for reproducibility.
|
| 443 |
+
randomize_seed: If true, ignore seed and use a random one.
|
| 444 |
+
enhance: If true, expand a plain prompt into a structured caption before generation.
|
| 445 |
+
|
| 446 |
+
Returns:
|
| 447 |
+
The generated image, the seed used, and the structured caption fed to the model.
|
| 448 |
+
"""
|
| 449 |
+
height, width = IMAGE_SIZES[size_label]
|
| 450 |
+
seed = _resolve_seed(seed, randomize_seed)
|
| 451 |
+
caption = _prepare_caption(prompt, "t2i", None, enhance)
|
| 452 |
+
image = _gpu_generate_image(
|
| 453 |
+
caption, height, width, steps, guidance_scale, shift, negative_prompt, seed,
|
| 454 |
+
)
|
| 455 |
+
return image, seed, caption
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
HEADER_HTML = """
|
| 459 |
+
<div id="lb-header">
|
| 460 |
+
<div class="lb-top">
|
| 461 |
+
<div class="lb-title">
|
| 462 |
+
LingBot-Video
|
| 463 |
+
<span class="lb-badge">Dense 1.3B</span>
|
| 464 |
+
</div>
|
| 465 |
+
<nav class="lb-links">
|
| 466 |
+
<a href="https://huggingface.co/robbyant/lingbot-video-dense-1.3b" target="_blank" rel="noopener">Model</a>
|
| 467 |
+
<a href="https://github.com/Robbyant/lingbot-video" target="_blank" rel="noopener">GitHub</a>
|
| 468 |
+
<a href="https://technology.robbyant.com/lingbot-video" target="_blank" rel="noopener">Project</a>
|
| 469 |
+
</nav>
|
| 470 |
+
</div>
|
| 471 |
+
<p class="lb-sub">
|
| 472 |
+
Embodied-intelligence video generation from a lightweight dense model — text-to-video, image-to-video & text-to-image.
|
| 473 |
+
Prompts are auto-expanded into structured JSON captions
|
| 474 |
+
(<a href="https://huggingface.co/Qwen/Qwen3.6-27B" target="_blank" rel="noopener">Qwen3.6-27B</a>);
|
| 475 |
+
paste a raw LingBot caption to skip that. 480p video, ZeroGPU.
|
| 476 |
+
</p>
|
| 477 |
+
</div>
|
| 478 |
+
"""
|
| 479 |
+
|
| 480 |
+
CSS = """
|
| 481 |
+
#col-container { max-width: 1100px; margin: 0 auto; }
|
| 482 |
+
.dark .gradio-container { color: var(--body-text-color); }
|
| 483 |
+
/* Zero out Gradio's wrapper padding around the header HTML block. */
|
| 484 |
+
#lb-header-wrap, #lb-header-wrap .html-container {
|
| 485 |
+
padding: 0 !important;
|
| 486 |
+
border: none !important;
|
| 487 |
+
background: transparent !important;
|
| 488 |
+
}
|
| 489 |
+
/* Borderless header, flush with the app container. */
|
| 490 |
+
#lb-header {
|
| 491 |
+
margin: 0;
|
| 492 |
+
padding: 2px 0 8px 0;
|
| 493 |
+
}
|
| 494 |
+
#lb-header .lb-top {
|
| 495 |
+
display: flex;
|
| 496 |
+
align-items: center;
|
| 497 |
+
justify-content: flex-start;
|
| 498 |
+
flex-wrap: wrap;
|
| 499 |
+
gap: 6px 18px;
|
| 500 |
+
}
|
| 501 |
+
#lb-header .lb-title {
|
| 502 |
+
font-size: 1.35rem;
|
| 503 |
+
font-weight: 700;
|
| 504 |
+
line-height: 1.2;
|
| 505 |
+
color: var(--body-text-color);
|
| 506 |
+
display: flex;
|
| 507 |
+
align-items: center;
|
| 508 |
+
gap: 10px;
|
| 509 |
+
}
|
| 510 |
+
#lb-header .lb-badge {
|
| 511 |
+
font-size: 0.66rem;
|
| 512 |
+
font-weight: 600;
|
| 513 |
+
letter-spacing: 0.02em;
|
| 514 |
+
text-transform: uppercase;
|
| 515 |
+
color: var(--body-text-color-subdued);
|
| 516 |
+
background: var(--background-fill-primary);
|
| 517 |
+
border: 1px solid var(--border-color-primary);
|
| 518 |
+
border-radius: 999px;
|
| 519 |
+
padding: 2px 9px;
|
| 520 |
+
white-space: nowrap;
|
| 521 |
+
}
|
| 522 |
+
#lb-header .lb-links {
|
| 523 |
+
display: flex;
|
| 524 |
+
gap: 14px;
|
| 525 |
+
font-size: 0.85rem;
|
| 526 |
+
padding-left: 4px;
|
| 527 |
+
}
|
| 528 |
+
#lb-header .lb-links a {
|
| 529 |
+
color: var(--body-text-color);
|
| 530 |
+
opacity: 0.85;
|
| 531 |
+
text-decoration: none;
|
| 532 |
+
border-bottom: 1px solid transparent;
|
| 533 |
+
}
|
| 534 |
+
#lb-header .lb-links a:hover {
|
| 535 |
+
color: var(--link-text-color);
|
| 536 |
+
border-bottom-color: currentColor;
|
| 537 |
+
}
|
| 538 |
+
#lb-header .lb-sub {
|
| 539 |
+
margin: 7px 0 0 0;
|
| 540 |
+
font-size: 0.82rem;
|
| 541 |
+
line-height: 1.45;
|
| 542 |
+
color: var(--body-text-color);
|
| 543 |
+
opacity: 0.78;
|
| 544 |
+
}
|
| 545 |
+
#lb-header .lb-sub a { color: var(--link-text-color); text-decoration: none; }
|
| 546 |
+
#lb-header .lb-sub a:hover { text-decoration: underline; }
|
| 547 |
+
/* Gradio's theme pads <a> with 0 8px, which shows up as fake space inside the
|
| 548 |
+
"(Qwen3.6-27B)" parentheses — strip it on all header links. */
|
| 549 |
+
#lb-header a { padding: 0 !important; }
|
| 550 |
+
|
| 551 |
+
/* Compact, tidy examples: no wide horizontal overflow, subtle scrollbar */
|
| 552 |
+
.lb-examples .gr-samples-table td, .lb-examples table td { white-space: normal; }
|
| 553 |
+
.lb-examples { scrollbar-width: thin; }
|
| 554 |
+
.lb-examples ::-webkit-scrollbar { height: 8px; width: 8px; }
|
| 555 |
+
.lb-examples ::-webkit-scrollbar-thumb {
|
| 556 |
+
background: var(--border-color-primary);
|
| 557 |
+
border-radius: 999px;
|
| 558 |
+
}
|
| 559 |
+
.lb-examples ::-webkit-scrollbar-track { background: transparent; }
|
| 560 |
+
"""
|
| 561 |
+
|
| 562 |
+
|
| 563 |
+
def _seed_row():
|
| 564 |
+
with gr.Row():
|
| 565 |
+
seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=42)
|
| 566 |
+
randomize = gr.Checkbox(label="Randomize seed", value=True)
|
| 567 |
+
return seed, randomize
|
| 568 |
+
|
| 569 |
+
|
| 570 |
+
def _advanced(negative_default: str, steps_value: int):
|
| 571 |
+
with gr.Accordion("Advanced settings", open=False):
|
| 572 |
+
steps = gr.Slider(label="Inference steps", minimum=4, maximum=50, step=1, value=steps_value)
|
| 573 |
+
guidance = gr.Slider(label="Guidance scale", minimum=1.0, maximum=10.0, step=0.5, value=3.0)
|
| 574 |
+
shift = gr.Slider(label="Flow shift", minimum=1.0, maximum=9.0, step=0.5, value=3.0)
|
| 575 |
+
negative = gr.Textbox(label="Negative prompt", value=negative_default, lines=3)
|
| 576 |
+
seed, randomize = _seed_row()
|
| 577 |
+
return steps, guidance, shift, negative, seed, randomize
|
| 578 |
+
|
| 579 |
+
|
| 580 |
+
# Example runners: examples carry only the natural-language prompt (and first
|
| 581 |
+
# frame for I2V) — enhancement is on and every other control keeps its default,
|
| 582 |
+
# so the examples table stays to one or two clean columns instead of ten.
|
| 583 |
+
def _example_t2v(prompt):
|
| 584 |
+
return generate_t2v(prompt, "832 × 480 (16:9)", 2.0, 30, 3.0, 3.0,
|
| 585 |
+
DEFAULT_NEGATIVE_PROMPT, 42, False, True)
|
| 586 |
+
|
| 587 |
+
|
| 588 |
+
def _example_i2v(image, prompt):
|
| 589 |
+
return generate_i2v(image, prompt, "832 × 480 (16:9)", 2.0, 30, 3.0, 3.0,
|
| 590 |
+
DEFAULT_NEGATIVE_PROMPT, 42, False, True)
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
def _example_t2i(prompt):
|
| 594 |
+
return generate_t2i(prompt, "832 × 480 (16:9)", 30, 3.0, 3.0,
|
| 595 |
+
DEFAULT_NEGATIVE_PROMPT_IMAGE, 42, False, True)
|
| 596 |
+
|
| 597 |
+
|
| 598 |
+
# Pre-built example prompts derived from the official LingBot-Video example cases.
|
| 599 |
+
# For t2v/t2i, we use a short natural-language prompt that the rewriter expands.
|
| 600 |
+
# For i2v, we pass the first-frame image and a natural-language motion description.
|
| 601 |
+
T2V_EXAMPLES = [
|
| 602 |
+
"A young woman with long, wavy brown hair stands in a bright modern apartment living room, "
|
| 603 |
+
"wearing an oversized cream knit cardigan over a white tank top with beige trousers. She "
|
| 604 |
+
"smiles at the camera, shifts her weight, and adjusts her collar, showcasing her outfit.",
|
| 605 |
+
"A young child with short brown hair plays outdoors on a sunny day, blowing shimmering soap "
|
| 606 |
+
"bubbles with a wand. The bubbles float upwards, catching the warm afternoon light against a "
|
| 607 |
+
"soft-focus green background.",
|
| 608 |
+
"A robotic arm on a workbench reaches forward, grasps a black game controller, lifts it, "
|
| 609 |
+
"moves it to the right, and lowers it into an open box. Another robotic arm and headphones "
|
| 610 |
+
"remain stationary on the desk, top-down view.",
|
| 611 |
+
]
|
| 612 |
+
T2V_LABELS = ["👗 Woman in apartment", "🫧 Child blowing bubbles", "🤖 Robot arm sorting"]
|
| 613 |
+
|
| 614 |
+
I2V_EXAMPLES = [
|
| 615 |
+
[str(EXAMPLES_DIR / "ti2v_frame.png"),
|
| 616 |
+
"A fit young man and a sleek white humanoid robot run side by side along a cherry blossom "
|
| 617 |
+
"lined promenade toward the camera, which tracks backward. Energetic, futuristic, daylight."],
|
| 618 |
+
]
|
| 619 |
+
I2V_LABELS = ["🏃 Man and robot running"]
|
| 620 |
+
|
| 621 |
+
T2I_EXAMPLES = [
|
| 622 |
+
"A clear glass bottle of water on a sunlit wooden table acts as a lens, refracting bright "
|
| 623 |
+
"sunlight into a warm glow, extreme close-up, photorealistic.",
|
| 624 |
+
"A humanoid robot chef flipping a pancake in a bright modern kitchen, dramatic side lighting, "
|
| 625 |
+
"steam rising from the pan, shallow depth of field, 85mm lens.",
|
| 626 |
+
]
|
| 627 |
+
T2I_LABELS = ["🔆 Bottle as a lens", "🤖 Robot chef"]
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
with gr.Blocks(title="LingBot-Video Dense 1.3B", theme=gr.themes.Citrus(), css=CSS) as demo:
|
| 631 |
+
gr.HTML(HEADER_HTML, elem_id="lb-header-wrap")
|
| 632 |
+
|
| 633 |
+
with gr.Tab("Text → Video"):
|
| 634 |
+
with gr.Row():
|
| 635 |
+
with gr.Column():
|
| 636 |
+
t2v_prompt = gr.Textbox(
|
| 637 |
+
label="Prompt",
|
| 638 |
+
placeholder="Describe the scene in detail (plain text or a LingBot JSON caption)...",
|
| 639 |
+
lines=5,
|
| 640 |
+
max_lines=12,
|
| 641 |
+
)
|
| 642 |
+
with gr.Row():
|
| 643 |
+
t2v_size = gr.Dropdown(
|
| 644 |
+
label="Resolution", choices=list(VIDEO_SIZES), value="832 × 480 (16:9)"
|
| 645 |
+
)
|
| 646 |
+
t2v_dur = gr.Slider(
|
| 647 |
+
label="Video duration (s)", minimum=1.0, maximum=5.0, step=0.5, value=2.0
|
| 648 |
+
)
|
| 649 |
+
t2v_enhance = gr.Checkbox(
|
| 650 |
+
label="Enhance prompt (official rewriter — required for plain prompts)", value=True
|
| 651 |
+
)
|
| 652 |
+
t2v_steps, t2v_guidance, t2v_shift, t2v_negative, t2v_seed, t2v_rand = _advanced(
|
| 653 |
+
DEFAULT_NEGATIVE_PROMPT, 30
|
| 654 |
+
)
|
| 655 |
+
t2v_btn = gr.Button("Generate video", variant="primary")
|
| 656 |
+
with gr.Column():
|
| 657 |
+
t2v_out = gr.Video(label="Generated video", autoplay=True)
|
| 658 |
+
t2v_seed_out = gr.Number(label="Seed used", interactive=False)
|
| 659 |
+
with gr.Accordion("Structured caption used", open=False):
|
| 660 |
+
t2v_caption_out = gr.Textbox(label="Caption", lines=4)
|
| 661 |
+
|
| 662 |
+
t2v_inputs = [
|
| 663 |
+
t2v_prompt, t2v_size, t2v_dur, t2v_steps, t2v_guidance, t2v_shift,
|
| 664 |
+
t2v_negative, t2v_seed, t2v_rand, t2v_enhance,
|
| 665 |
+
]
|
| 666 |
+
t2v_outputs = [t2v_out, t2v_seed_out, t2v_caption_out]
|
| 667 |
+
t2v_btn.click(generate_t2v, inputs=t2v_inputs, outputs=t2v_outputs,
|
| 668 |
+
concurrency_id="gpu", concurrency_limit=1)
|
| 669 |
+
with gr.Column(elem_classes="lb-examples"):
|
| 670 |
+
gr.Examples(
|
| 671 |
+
examples=T2V_EXAMPLES,
|
| 672 |
+
example_labels=T2V_LABELS,
|
| 673 |
+
fn=_example_t2v,
|
| 674 |
+
inputs=[t2v_prompt],
|
| 675 |
+
outputs=t2v_outputs,
|
| 676 |
+
cache_examples=True,
|
| 677 |
+
cache_mode="lazy",
|
| 678 |
+
)
|
| 679 |
+
|
| 680 |
+
with gr.Tab("Image → Video"):
|
| 681 |
+
with gr.Row():
|
| 682 |
+
with gr.Column():
|
| 683 |
+
i2v_image = gr.Image(label="First frame", type="pil")
|
| 684 |
+
i2v_prompt = gr.Textbox(
|
| 685 |
+
label="Prompt",
|
| 686 |
+
placeholder="Describe how the scene should evolve...",
|
| 687 |
+
lines=4,
|
| 688 |
+
max_lines=12,
|
| 689 |
+
)
|
| 690 |
+
with gr.Row():
|
| 691 |
+
i2v_size = gr.Dropdown(
|
| 692 |
+
label="Resolution", choices=list(VIDEO_SIZES), value="832 × 480 (16:9)"
|
| 693 |
+
)
|
| 694 |
+
i2v_dur = gr.Slider(
|
| 695 |
+
label="Video duration (s)", minimum=1.0, maximum=5.0, step=0.5, value=2.0
|
| 696 |
+
)
|
| 697 |
+
i2v_enhance = gr.Checkbox(
|
| 698 |
+
label="Enhance prompt (official rewriter — required for plain prompts)", value=True
|
| 699 |
+
)
|
| 700 |
+
i2v_steps, i2v_guidance, i2v_shift, i2v_negative, i2v_seed, i2v_rand = _advanced(
|
| 701 |
+
DEFAULT_NEGATIVE_PROMPT, 30
|
| 702 |
+
)
|
| 703 |
+
i2v_btn = gr.Button("Generate video", variant="primary")
|
| 704 |
+
with gr.Column():
|
| 705 |
+
i2v_out = gr.Video(label="Generated video", autoplay=True)
|
| 706 |
+
i2v_seed_out = gr.Number(label="Seed used", interactive=False)
|
| 707 |
+
with gr.Accordion("Structured caption used", open=False):
|
| 708 |
+
i2v_caption_out = gr.Textbox(label="Caption", lines=4)
|
| 709 |
+
|
| 710 |
+
i2v_inputs = [
|
| 711 |
+
i2v_image, i2v_prompt, i2v_size, i2v_dur, i2v_steps, i2v_guidance, i2v_shift,
|
| 712 |
+
i2v_negative, i2v_seed, i2v_rand, i2v_enhance,
|
| 713 |
+
]
|
| 714 |
+
i2v_outputs = [i2v_out, i2v_seed_out, i2v_caption_out]
|
| 715 |
+
i2v_btn.click(generate_i2v, inputs=i2v_inputs, outputs=i2v_outputs,
|
| 716 |
+
concurrency_id="gpu", concurrency_limit=1)
|
| 717 |
+
with gr.Column(elem_classes="lb-examples"):
|
| 718 |
+
gr.Examples(
|
| 719 |
+
examples=I2V_EXAMPLES,
|
| 720 |
+
example_labels=I2V_LABELS,
|
| 721 |
+
fn=_example_i2v,
|
| 722 |
+
inputs=[i2v_image, i2v_prompt],
|
| 723 |
+
outputs=i2v_outputs,
|
| 724 |
+
cache_examples=True,
|
| 725 |
+
cache_mode="lazy",
|
| 726 |
+
)
|
| 727 |
+
|
| 728 |
+
with gr.Tab("Text → Image"):
|
| 729 |
+
with gr.Row():
|
| 730 |
+
with gr.Column():
|
| 731 |
+
t2i_prompt = gr.Textbox(
|
| 732 |
+
label="Prompt",
|
| 733 |
+
placeholder="Describe the image in detail...",
|
| 734 |
+
lines=5,
|
| 735 |
+
max_lines=12,
|
| 736 |
+
)
|
| 737 |
+
t2i_size = gr.Dropdown(
|
| 738 |
+
label="Resolution", choices=list(IMAGE_SIZES), value="832 × 480 (16:9)"
|
| 739 |
+
)
|
| 740 |
+
t2i_enhance = gr.Checkbox(
|
| 741 |
+
label="Enhance prompt (official rewriter — required for plain prompts)", value=True
|
| 742 |
+
)
|
| 743 |
+
t2i_steps, t2i_guidance, t2i_shift, t2i_negative, t2i_seed, t2i_rand = _advanced(
|
| 744 |
+
DEFAULT_NEGATIVE_PROMPT_IMAGE, 30
|
| 745 |
+
)
|
| 746 |
+
t2i_btn = gr.Button("Generate image", variant="primary")
|
| 747 |
+
with gr.Column():
|
| 748 |
+
t2i_out = gr.Image(label="Generated image")
|
| 749 |
+
t2i_seed_out = gr.Number(label="Seed used", interactive=False)
|
| 750 |
+
with gr.Accordion("Structured caption used", open=False):
|
| 751 |
+
t2i_caption_out = gr.Textbox(label="Caption", lines=4)
|
| 752 |
+
|
| 753 |
+
t2i_inputs = [
|
| 754 |
+
t2i_prompt, t2i_size, t2i_steps, t2i_guidance, t2i_shift,
|
| 755 |
+
t2i_negative, t2i_seed, t2i_rand, t2i_enhance,
|
| 756 |
+
]
|
| 757 |
+
t2i_outputs = [t2i_out, t2i_seed_out, t2i_caption_out]
|
| 758 |
+
t2i_btn.click(generate_t2i, inputs=t2i_inputs, outputs=t2i_outputs,
|
| 759 |
+
concurrency_id="gpu", concurrency_limit=1)
|
| 760 |
+
with gr.Column(elem_classes="lb-examples"):
|
| 761 |
+
gr.Examples(
|
| 762 |
+
examples=T2I_EXAMPLES,
|
| 763 |
+
example_labels=T2I_LABELS,
|
| 764 |
+
fn=_example_t2i,
|
| 765 |
+
inputs=[t2i_prompt],
|
| 766 |
+
outputs=t2i_outputs,
|
| 767 |
+
cache_examples=True,
|
| 768 |
+
cache_mode="lazy",
|
| 769 |
+
)
|
| 770 |
+
|
| 771 |
+
demo.queue(max_size=30).launch(css=CSS, mcp_server=True)
|
examples/t2v_bubbles.json
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"caption": {
|
| 3 |
+
"comprehensive_description": {
|
| 4 |
+
"scene_content_description": "A young child with short brown hair is outdoors on a bright, sunny day, playing with bubbles. The child is wearing a colorful striped shirt with horizontal bands of red, yellow, green, and blue. They are holding a small bottle of bubble solution and a wand, dipping the wand into the bottle and then blowing to create a stream of shimmering, iridescent bubbles. The background is a soft-focus outdoor setting with greenery and a hint of a building, creating a warm and joyful atmosphere. The lighting is bright and natural, highlighting the child's focused expression and the delicate, translucent nature of the bubbles.",
|
| 5 |
+
"camera_movement_description": "The camera is essentially stationary throughout the video, maintaining a medium close-up shot of the child from an eye-level angle. There is a very slight handheld tremor, but no intentional panning, tilting, or zooming occurs."
|
| 6 |
+
},
|
| 7 |
+
"camera_info": {
|
| 8 |
+
"color": "Saturated",
|
| 9 |
+
"frame_size": "Medium Close Up",
|
| 10 |
+
"shot_type_angle": "Low angle",
|
| 11 |
+
"lens_size": "Long Lens",
|
| 12 |
+
"composition": "Center",
|
| 13 |
+
"lighting": "Hard light",
|
| 14 |
+
"lighting_type": "Daylight"
|
| 15 |
+
},
|
| 16 |
+
"world_knowledge": [],
|
| 17 |
+
"prominent_elements": [
|
| 18 |
+
{
|
| 19 |
+
"name": "young child",
|
| 20 |
+
"description": "A young child with short brown hair and a joyful expression, focused on playing with bubbles.",
|
| 21 |
+
"actions": [
|
| 22 |
+
{
|
| 23 |
+
"timestamp": "[0.0s - 1.5s]",
|
| 24 |
+
"action": "Dips the bubble wand into the solution bottle and lifts it out."
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"timestamp": "[1.5s - 2.5s]",
|
| 28 |
+
"action": "Brings the wand to their lips and blows to create bubbles."
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"timestamp": "[2.5s - 5.0s]",
|
| 32 |
+
"action": "Watches the bubbles float away, smiling slightly."
|
| 33 |
+
}
|
| 34 |
+
],
|
| 35 |
+
"location": "Center of the frame",
|
| 36 |
+
"relative_size": "dominant",
|
| 37 |
+
"shape_and_color": "Human form; wearing a multi-colored striped shirt (red, yellow, green, blue).",
|
| 38 |
+
"texture": "Soft skin, fine hair, fabric texture of the shirt.",
|
| 39 |
+
"appearance_details": "Short brown hair, bright eyes, colorful horizontal stripes on the shirt.",
|
| 40 |
+
"relationship": "Holding the bubble wand and bottle, interacting with the bubbles.",
|
| 41 |
+
"orientation": "Facing forward and slightly to the right.",
|
| 42 |
+
"pose": "Standing or sitting upright, arms raised to hold the bubble wand.",
|
| 43 |
+
"expression": "Focused and happy.",
|
| 44 |
+
"clothing": "A short-sleeved shirt with horizontal stripes in red, yellow, green, and blue.",
|
| 45 |
+
"gender": "Male",
|
| 46 |
+
"skin_tone_and_texture": "Fair skin with a smooth texture."
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"name": "bubble wand and bottle",
|
| 50 |
+
"description": "A small plastic bottle containing bubble solution and a wand with a circular loop.",
|
| 51 |
+
"actions": [
|
| 52 |
+
{
|
| 53 |
+
"timestamp": "[0.0s - 1.5s]",
|
| 54 |
+
"action": "The wand is dipped into the bottle and then lifted out."
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"timestamp": "[1.5s - 2.5s]",
|
| 58 |
+
"action": "The wand is held at the child's mouth as bubbles are blown."
|
| 59 |
+
}
|
| 60 |
+
],
|
| 61 |
+
"location": "Lower center of the frame, held by the child.",
|
| 62 |
+
"relative_size": "small",
|
| 63 |
+
"shape_and_color": "Cylindrical bottle with a blue cap; circular wand loop.",
|
| 64 |
+
"texture": "Smooth plastic.",
|
| 65 |
+
"appearance_details": "The bottle is partially filled with clear liquid; the wand has a thin handle.",
|
| 66 |
+
"relationship": "Held by the child's hands.",
|
| 67 |
+
"orientation": "Vertical bottle, horizontal wand loop.",
|
| 68 |
+
"pose": "",
|
| 69 |
+
"expression": "",
|
| 70 |
+
"clothing": "",
|
| 71 |
+
"gender": "",
|
| 72 |
+
"skin_tone_and_texture": ""
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"name": "bubbles",
|
| 76 |
+
"description": "A cluster of small, iridescent, translucent bubbles floating in the air.",
|
| 77 |
+
"actions": [
|
| 78 |
+
{
|
| 79 |
+
"timestamp": "[1.5s - 5.0s]",
|
| 80 |
+
"action": "The bubbles are blown from the wand and float upwards and towards the right side of the frame."
|
| 81 |
+
}
|
| 82 |
+
],
|
| 83 |
+
"location": "Upper center and right side of the frame.",
|
| 84 |
+
"relative_size": "medium",
|
| 85 |
+
"shape_and_color": "Spherical and iridescent with rainbow-like reflections.",
|
| 86 |
+
"texture": "Glossy and translucent.",
|
| 87 |
+
"appearance_details": "Shimmering surfaces that catch the sunlight.",
|
| 88 |
+
"relationship": "Created by the child's blowing action.",
|
| 89 |
+
"orientation": "Floating in various directions.",
|
| 90 |
+
"pose": "",
|
| 91 |
+
"expression": "",
|
| 92 |
+
"clothing": "",
|
| 93 |
+
"gender": "",
|
| 94 |
+
"skin_tone_and_texture": "",
|
| 95 |
+
"is_cluster": true,
|
| 96 |
+
"number_of_objects": "many"
|
| 97 |
+
}
|
| 98 |
+
]
|
| 99 |
+
},
|
| 100 |
+
"duration": 5
|
| 101 |
+
}
|
examples/t2v_robot.json
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"caption": {
|
| 3 |
+
"comprehensive_description": {
|
| 4 |
+
"scene_content_description": "The video presents a first-person perspective of a workspace, likely a desk, viewed from a fixed, slightly elevated angle. The environment is well-lit with neutral, even lighting, creating a clear and focused atmosphere. The desk surface is a light grey color. On the left side of the desk, there is a black game controller with a glowing blue light bar, resting on a black mousepad. In the center of the desk lies a pair of black over-ear headphones. To the right, there is an open, empty black box with a white interior. In the foreground, two robotic arms are visible. The left robotic arm, featuring a black and silver body with a two-fingered gripper, remains completely stationary throughout the video. The right robotic arm, similar in design, is the active subject. It begins by moving forward and to the left, positioning its gripper over the game controller. It then grasps the controller, lifts it off the desk, and transports it to the right, moving it over the open box. Finally, the right robotic arm lowers the controller into the box and releases it. The headphones and the left robotic arm remain undisturbed during this entire sequence.",
|
| 5 |
+
"camera_movement_description": ""
|
| 6 |
+
},
|
| 7 |
+
"camera_info": {
|
| 8 |
+
"color": "Cyan",
|
| 9 |
+
"frame_size": "Wide",
|
| 10 |
+
"shot_type_angle": "High angle",
|
| 11 |
+
"lens_size": "Ultra Wide / Fisheye",
|
| 12 |
+
"composition": "Balanced",
|
| 13 |
+
"lighting": "Soft light",
|
| 14 |
+
"lighting_type": "Artificial light"
|
| 15 |
+
},
|
| 16 |
+
"world_knowledge": [],
|
| 17 |
+
"prominent_elements": [
|
| 18 |
+
{
|
| 19 |
+
"name": "right robotic arm",
|
| 20 |
+
"description": "A mechanical arm with a black and silver body, equipped with a two-fingered gripper and visible wiring.",
|
| 21 |
+
"actions": [
|
| 22 |
+
{
|
| 23 |
+
"timestamp": "[0.0s - 1.5s]",
|
| 24 |
+
"action": "Moves forward and to the left towards the game controller."
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"timestamp": "[1.5s - 2.5s]",
|
| 28 |
+
"action": "Grasps the game controller and lifts it upward."
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"timestamp": "[2.5s - 4.0s]",
|
| 32 |
+
"action": "Moves to the right, carrying the game controller over the open box."
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"timestamp": "[4.0s - 5.0s]",
|
| 36 |
+
"action": "Lowers the game controller into the box and releases it."
|
| 37 |
+
}
|
| 38 |
+
],
|
| 39 |
+
"location": "Originates from the bottom right, moves to the center, then to the right.",
|
| 40 |
+
"relative_size": "large",
|
| 41 |
+
"shape_and_color": "Cylindrical and angular segments, black and silver.",
|
| 42 |
+
"texture": "Metallic and matte plastic.",
|
| 43 |
+
"appearance_details": "Visible joints, wiring, and a two-fingered gripper mechanism.",
|
| 44 |
+
"relationship": "Interacts directly with the game controller.",
|
| 45 |
+
"orientation": "Extends forward and slightly upward from the bottom right.",
|
| 46 |
+
"pose": "",
|
| 47 |
+
"expression": "",
|
| 48 |
+
"clothing": "",
|
| 49 |
+
"is_cluster": false,
|
| 50 |
+
"number_of_objects": ""
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"name": "left robotic arm",
|
| 54 |
+
"description": "A mechanical arm with a black and silver body, equipped with a two-fingered gripper.",
|
| 55 |
+
"actions": [
|
| 56 |
+
{
|
| 57 |
+
"timestamp": "[0.0s - 5.0s]",
|
| 58 |
+
"action": "Remains stationary."
|
| 59 |
+
}
|
| 60 |
+
],
|
| 61 |
+
"location": "Bottom left corner of the frame.",
|
| 62 |
+
"relative_size": "large",
|
| 63 |
+
"shape_and_color": "Cylindrical and angular segments, black and silver.",
|
| 64 |
+
"texture": "Metallic and matte plastic.",
|
| 65 |
+
"appearance_details": "Visible joints and a two-fingered gripper mechanism.",
|
| 66 |
+
"relationship": "Positioned opposite the right robotic arm, not interacting with other objects.",
|
| 67 |
+
"orientation": "Extends forward and slightly upward from the bottom left.",
|
| 68 |
+
"pose": "",
|
| 69 |
+
"expression": "",
|
| 70 |
+
"clothing": "",
|
| 71 |
+
"is_cluster": false,
|
| 72 |
+
"number_of_objects": ""
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"name": "game controller",
|
| 76 |
+
"description": "A standard video game controller with joysticks, buttons, and a glowing light bar.",
|
| 77 |
+
"actions": [
|
| 78 |
+
{
|
| 79 |
+
"timestamp": "[0.0s - 1.5s]",
|
| 80 |
+
"action": "Rests stationary on the desk."
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"timestamp": "[1.5s - 2.5s]",
|
| 84 |
+
"action": "Is grasped and lifted upward by the right robotic arm."
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"timestamp": "[2.5s - 4.0s]",
|
| 88 |
+
"action": "Is moved to the right by the right robotic arm."
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"timestamp": "[4.0s - 5.0s]",
|
| 92 |
+
"action": "Is lowered into the open box and released."
|
| 93 |
+
}
|
| 94 |
+
],
|
| 95 |
+
"location": "Initially on the left side of the desk, moved to the right side inside the box.",
|
| 96 |
+
"relative_size": "medium",
|
| 97 |
+
"shape_and_color": "Contoured shape, black with a blue light bar.",
|
| 98 |
+
"texture": "Matte plastic.",
|
| 99 |
+
"appearance_details": "Two joysticks, directional pad, action buttons, and a glowing blue light bar in the center.",
|
| 100 |
+
"relationship": "Initially on the desk, then grasped and moved by the right robotic arm, finally placed in the open box.",
|
| 101 |
+
"orientation": "Horizontal on the desk, then tilted while being carried.",
|
| 102 |
+
"pose": "",
|
| 103 |
+
"expression": "",
|
| 104 |
+
"clothing": "",
|
| 105 |
+
"is_cluster": false,
|
| 106 |
+
"number_of_objects": ""
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"name": "headphones",
|
| 110 |
+
"description": "A pair of over-ear headphones with a headband and ear cups.",
|
| 111 |
+
"actions": [
|
| 112 |
+
{
|
| 113 |
+
"timestamp": "[0.0s - 5.0s]",
|
| 114 |
+
"action": "Remains stationary on the desk."
|
| 115 |
+
}
|
| 116 |
+
],
|
| 117 |
+
"location": "Center of the desk.",
|
| 118 |
+
"relative_size": "medium",
|
| 119 |
+
"shape_and_color": "Curved headband with circular ear cups, black.",
|
| 120 |
+
"texture": "Matte plastic and soft ear cushions.",
|
| 121 |
+
"appearance_details": "Visible ear cushions and a headband.",
|
| 122 |
+
"relationship": "Rests on the desk, untouched by the robotic arms.",
|
| 123 |
+
"orientation": "Lying flat on the desk.",
|
| 124 |
+
"pose": "",
|
| 125 |
+
"expression": "",
|
| 126 |
+
"clothing": "",
|
| 127 |
+
"is_cluster": false,
|
| 128 |
+
"number_of_objects": ""
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"name": "open box",
|
| 132 |
+
"description": "A rectangular box with the lid open, revealing a white interior.",
|
| 133 |
+
"actions": [
|
| 134 |
+
{
|
| 135 |
+
"timestamp": "[0.0s - 5.0s]",
|
| 136 |
+
"action": "Remains stationary on the desk."
|
| 137 |
+
}
|
| 138 |
+
],
|
| 139 |
+
"location": "Right side of the desk.",
|
| 140 |
+
"relative_size": "medium",
|
| 141 |
+
"shape_and_color": "Rectangular, black exterior with a white interior.",
|
| 142 |
+
"texture": "Smooth cardboard or plastic.",
|
| 143 |
+
"appearance_details": "Open lid, empty interior.",
|
| 144 |
+
"relationship": "Serves as the receptacle for the game controller.",
|
| 145 |
+
"orientation": "Horizontal on the desk.",
|
| 146 |
+
"pose": "",
|
| 147 |
+
"expression": "",
|
| 148 |
+
"clothing": "",
|
| 149 |
+
"is_cluster": false,
|
| 150 |
+
"number_of_objects": ""
|
| 151 |
+
}
|
| 152 |
+
]
|
| 153 |
+
},
|
| 154 |
+
"duration": 5
|
| 155 |
+
}
|
examples/t2v_woman.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"caption": {
|
| 3 |
+
"comprehensive_description": {
|
| 4 |
+
"scene_content_description": "A young woman with long, wavy brown hair is standing in a bright, modern apartment living room. She is wearing a stylish, oversized cream-colored knit cardigan over a white tank top, paired with high-waisted, wide-leg beige trousers. She holds a small, structured tan leather handbag in her left hand. The background features a neutral-toned interior with a beige sofa, a potted plant, and large windows that let in soft, natural light, creating a warm and inviting atmosphere. The woman is smiling and looking directly at the camera, showcasing her outfit with a confident and friendly demeanor.",
|
| 5 |
+
"camera_movement_description": "The camera is positioned at eye level and remains essentially stationary throughout the video, maintaining a medium shot that captures the subject from the waist up. There is a very shallow depth of field, keeping the woman in sharp focus while the background remains softly blurred."
|
| 6 |
+
},
|
| 7 |
+
"camera_info": {
|
| 8 |
+
"color": "Warm",
|
| 9 |
+
"frame_size": "Medium",
|
| 10 |
+
"shot_type_angle": "Eye level",
|
| 11 |
+
"lens_size": "Medium",
|
| 12 |
+
"composition": "Center",
|
| 13 |
+
"lighting": "Soft light",
|
| 14 |
+
"lighting_type": "Daylight"
|
| 15 |
+
},
|
| 16 |
+
"world_knowledge": [],
|
| 17 |
+
"prominent_elements": [
|
| 18 |
+
{
|
| 19 |
+
"name": "young woman",
|
| 20 |
+
"description": "A woman with long, wavy brown hair and a friendly expression, modeling a fashion outfit.",
|
| 21 |
+
"actions": [
|
| 22 |
+
{
|
| 23 |
+
"timestamp": "[0.0s - 0.5s]",
|
| 24 |
+
"action": "stands still, smiling at the camera"
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"timestamp": "[0.5s - 2.0s]",
|
| 28 |
+
"action": "shifts her weight and turns her body slightly to the right"
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"timestamp": "[2.0s - 3.5s]",
|
| 32 |
+
"action": "adjusts the collar of her cardigan with her right hand"
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"timestamp": "[3.5s - 5.0s]",
|
| 36 |
+
"action": "returns to a neutral pose, smiling at the camera"
|
| 37 |
+
}
|
| 38 |
+
],
|
| 39 |
+
"location": "center of the frame",
|
| 40 |
+
"relative_size": "dominant",
|
| 41 |
+
"shape_and_color": "slender build; wearing cream, white, and beige",
|
| 42 |
+
"texture": "soft knit cardigan, smooth fabric trousers",
|
| 43 |
+
"appearance_details": "long wavy brown hair, gold hoop earrings, tan leather handbag",
|
| 44 |
+
"relationship": "the main subject of the video, standing in front of a blurred apartment background",
|
| 45 |
+
"orientation": "upright, facing the camera",
|
| 46 |
+
"pose": "standing, shifting weight, and adjusting clothing",
|
| 47 |
+
"expression": "smiling and confident",
|
| 48 |
+
"clothing": "oversized cream knit cardigan, white tank top, high-waisted wide-leg beige trousers",
|
| 49 |
+
"gender": "female",
|
| 50 |
+
"skin_tone_and_texture": "fair skin with a smooth texture"
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"name": "tan handbag",
|
| 54 |
+
"description": "A small, structured leather handbag with a top handle.",
|
| 55 |
+
"actions": [
|
| 56 |
+
{
|
| 57 |
+
"timestamp": "[0.0s - 5.0s]",
|
| 58 |
+
"action": "held steady in the woman's left hand"
|
| 59 |
+
}
|
| 60 |
+
],
|
| 61 |
+
"location": "held in the woman's left hand, lower center of the frame",
|
| 62 |
+
"relative_size": "small",
|
| 63 |
+
"shape_and_color": "rectangular, tan or light brown",
|
| 64 |
+
"texture": "smooth leather",
|
| 65 |
+
"appearance_details": "structured shape with a top handle",
|
| 66 |
+
"relationship": "held by the woman as an accessory",
|
| 67 |
+
"orientation": "upright",
|
| 68 |
+
"pose": "",
|
| 69 |
+
"expression": "",
|
| 70 |
+
"clothing": "",
|
| 71 |
+
"gender": "",
|
| 72 |
+
"skin_tone_and_texture": ""
|
| 73 |
+
}
|
| 74 |
+
]
|
| 75 |
+
},
|
| 76 |
+
"duration": 5
|
| 77 |
+
}
|
examples/ti2v_frame.png
ADDED
|
Git LFS Details
|
examples/ti2v_robot.json
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"caption": {
|
| 3 |
+
"comprehensive_description": {
|
| 4 |
+
"scene_content_description": "A dynamic, high-fidelity sequence of a fit young man and a sleek white humanoid robot running side-by-side along a paved promenade lined with blooming cherry blossom trees. The man is dressed in black athletic wear, while the robot features a glossy white chassis with black mechanical joints. They are running towards the camera, which tracks backward to maintain their position in the frame. The background features a blurred cityscape and a bridge over a river, creating a sense of depth and urban vitality. The atmosphere is energetic and futuristic, blending organic nature with advanced technology.",
|
| 5 |
+
"camera_movement_description": "The camera executes a smooth, continuous backward tracking shot, moving parallel to the subjects' forward motion to keep them centered in the frame. There is a subtle, rhythmic vertical bobbing motion synchronized with the cadence of the runners' footsteps, adding a visceral sense of speed and physical exertion. The camera maintains a steady focus on the two runners, allowing the background elements to exhibit motion blur."
|
| 6 |
+
},
|
| 7 |
+
"camera_info": {
|
| 8 |
+
"color": "Natural",
|
| 9 |
+
"frame_size": "Wide",
|
| 10 |
+
"shot_type_angle": "Eye level",
|
| 11 |
+
"lens_size": "Telephoto",
|
| 12 |
+
"composition": "Symmetrical",
|
| 13 |
+
"lighting": "Bright sunlight",
|
| 14 |
+
"lighting_type": "Daylight"
|
| 15 |
+
},
|
| 16 |
+
"world_knowledge": [],
|
| 17 |
+
"prominent_elements": [
|
| 18 |
+
{
|
| 19 |
+
"name": "fit young man",
|
| 20 |
+
"description": "A muscular male with short dark hair, wearing a tight black t-shirt and black running shorts.",
|
| 21 |
+
"actions": [
|
| 22 |
+
{
|
| 23 |
+
"timestamp": "[0.0s - 5.0s]",
|
| 24 |
+
"action": "He runs steadily towards the camera, his arms pumping rhythmically at his sides and his legs cycling with powerful strides, maintaining a consistent pace."
|
| 25 |
+
}
|
| 26 |
+
],
|
| 27 |
+
"location": "Left side of the frame (viewer's perspective)",
|
| 28 |
+
"relative_size": "large",
|
| 29 |
+
"shape_and_color": "Athletic build, dark clothing contrasting with the bright surroundings.",
|
| 30 |
+
"texture": "Realistic skin and fabric",
|
| 31 |
+
"appearance_details": "White running shoes, focused and determined facial expression.",
|
| 32 |
+
"relationship": "The primary human subject",
|
| 33 |
+
"orientation": "Facing forward",
|
| 34 |
+
"pose": "Running posture, arms bent at the elbows",
|
| 35 |
+
"expression": "Focused and determined",
|
| 36 |
+
"clothing": "Black t-shirt, black shorts, white sneakers",
|
| 37 |
+
"gender": "Male",
|
| 38 |
+
"skin_tone_and_texture": "Tanned, muscular"
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"name": "humanoid robot",
|
| 42 |
+
"description": "A sleek, anthropomorphic machine with a glossy white exterior and exposed black mechanical joints at the shoulders, elbows, knees, and ankles.",
|
| 43 |
+
"actions": [
|
| 44 |
+
{
|
| 45 |
+
"timestamp": "[0.0s - 5.0s]",
|
| 46 |
+
"action": "It runs with mechanical precision, its limbs moving in a fluid, lifelike gait that perfectly matches the man's stride, its arms swinging in coordination with its legs."
|
| 47 |
+
}
|
| 48 |
+
],
|
| 49 |
+
"location": "Right side of the frame (viewer's perspective)",
|
| 50 |
+
"relative_size": "large",
|
| 51 |
+
"shape_and_color": "Angular and futuristic, white with black accents.",
|
| 52 |
+
"texture": "Smooth, reflective metal and matte composite materials",
|
| 53 |
+
"appearance_details": "A smooth, featureless black visor for a face; the label '07' is visible on its chest plate.",
|
| 54 |
+
"relationship": "The companion subject running alongside the man",
|
| 55 |
+
"orientation": "Facing forward",
|
| 56 |
+
"pose": "Running posture, mirroring the human",
|
| 57 |
+
"expression": "",
|
| 58 |
+
"clothing": "",
|
| 59 |
+
"gender": "",
|
| 60 |
+
"skin_tone_and_texture": ""
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"name": "cherry blossom trees",
|
| 64 |
+
"description": "A dense row of mature trees with dark trunks and canopies full of vibrant pink flowers.",
|
| 65 |
+
"actions": [
|
| 66 |
+
{
|
| 67 |
+
"timestamp": "[0.0s - 5.0s]",
|
| 68 |
+
"action": "The trees appear to rush past the camera due to the backward tracking motion, with the pink blossoms blurring slightly to emphasize speed."
|
| 69 |
+
}
|
| 70 |
+
],
|
| 71 |
+
"location": "Background, lining both sides of the path",
|
| 72 |
+
"relative_size": "large",
|
| 73 |
+
"shape_and_color": "Organic and sprawling, with dark brown bark and bright pink blossoms.",
|
| 74 |
+
"texture": "Rough bark and delicate petals",
|
| 75 |
+
"appearance_details": "Hanging branches that frame the top of the shot.",
|
| 76 |
+
"relationship": "The environmental setting",
|
| 77 |
+
"orientation": "Receding into the distance",
|
| 78 |
+
"pose": "",
|
| 79 |
+
"expression": "",
|
| 80 |
+
"clothing": "",
|
| 81 |
+
"gender": "",
|
| 82 |
+
"skin_tone_and_texture": ""
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"name": "paved promenade",
|
| 86 |
+
"description": "A wide, brick-paved walkway stretching from the foreground into the distance.",
|
| 87 |
+
"actions": [
|
| 88 |
+
{
|
| 89 |
+
"timestamp": "[0.0s - 5.0s]",
|
| 90 |
+
"action": "The ground rushes towards the bottom of the frame, providing a strong sense of forward momentum."
|
| 91 |
+
}
|
| 92 |
+
],
|
| 93 |
+
"location": "Lower center of the frame",
|
| 94 |
+
"relative_size": "large",
|
| 95 |
+
"shape_and_color": "Rectangular and linear, composed of reddish-brown bricks.",
|
| 96 |
+
"texture": "Rough and uneven",
|
| 97 |
+
"appearance_details": "Distinct pattern of the paving stones.",
|
| 98 |
+
"relationship": "The ground surface for the runners",
|
| 99 |
+
"orientation": "Leading the eye towards the horizon",
|
| 100 |
+
"pose": "",
|
| 101 |
+
"expression": "",
|
| 102 |
+
"clothing": "",
|
| 103 |
+
"gender": "",
|
| 104 |
+
"skin_tone_and_texture": ""
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"name": "urban background",
|
| 108 |
+
"description": "A blurred cityscape featuring tall buildings and a steel bridge spanning a body of water.",
|
| 109 |
+
"actions": [
|
| 110 |
+
{
|
| 111 |
+
"timestamp": "[0.0s - 5.0s]",
|
| 112 |
+
"action": "The background remains relatively static but shifts slightly due to the camera's lateral movement, remaining out of focus to keep attention on the runners."
|
| 113 |
+
}
|
| 114 |
+
],
|
| 115 |
+
"location": "Far background, visible through the gaps in the trees and to the right",
|
| 116 |
+
"relative_size": "medium",
|
| 117 |
+
"shape_and_color": "Geometric and muted, with grey, blue, and beige tones.",
|
| 118 |
+
"texture": "Indistinct due to depth of field",
|
| 119 |
+
"appearance_details": "Silhouette of skyscrapers and the truss structure of the bridge.",
|
| 120 |
+
"relationship": "The distant context",
|
| 121 |
+
"orientation": "Static in the distance",
|
| 122 |
+
"pose": "",
|
| 123 |
+
"expression": "",
|
| 124 |
+
"clothing": "",
|
| 125 |
+
"gender": "",
|
| 126 |
+
"skin_tone_and_texture": ""
|
| 127 |
+
}
|
| 128 |
+
]
|
| 129 |
+
},
|
| 130 |
+
"duration": 5
|
| 131 |
+
}
|
lingbot_video/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import importlib
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
_EXPORTS = {
|
| 8 |
+
"FlowUniPCMultistepScheduler": (
|
| 9 |
+
"lingbot_video.scheduling_flow_unipc",
|
| 10 |
+
"FlowUniPCMultistepScheduler",
|
| 11 |
+
),
|
| 12 |
+
"LingBotVideoImageToVideoPipeline": (
|
| 13 |
+
"lingbot_video.pipeline_lingbot_video_i2v",
|
| 14 |
+
"LingBotVideoImageToVideoPipeline",
|
| 15 |
+
),
|
| 16 |
+
"LingBotVideoPipeline": (
|
| 17 |
+
"lingbot_video.pipeline_lingbot_video",
|
| 18 |
+
"LingBotVideoPipeline",
|
| 19 |
+
),
|
| 20 |
+
"LingBotVideoTransformer3DModel": (
|
| 21 |
+
"lingbot_video.transformer_lingbot_video",
|
| 22 |
+
"LingBotVideoTransformer3DModel",
|
| 23 |
+
),
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
__all__ = sorted(_EXPORTS)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def __getattr__(name: str) -> Any:
|
| 30 |
+
try:
|
| 31 |
+
module_name, attr_name = _EXPORTS[name]
|
| 32 |
+
except KeyError as exc:
|
| 33 |
+
raise AttributeError(name) from exc
|
| 34 |
+
value = getattr(importlib.import_module(module_name), attr_name)
|
| 35 |
+
globals()[name] = value
|
| 36 |
+
return value
|
lingbot_video/default_negative_prompt.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"universal_negative": {
|
| 3 |
+
"visual_quality": [
|
| 4 |
+
"low quality",
|
| 5 |
+
"worst quality",
|
| 6 |
+
"blurry",
|
| 7 |
+
"pixelated",
|
| 8 |
+
"jpeg artifacts",
|
| 9 |
+
"low resolution",
|
| 10 |
+
"unstable color",
|
| 11 |
+
"color flicker",
|
| 12 |
+
"underexposed",
|
| 13 |
+
"overexposed",
|
| 14 |
+
"invisible subject",
|
| 15 |
+
"subject hidden in darkness"
|
| 16 |
+
],
|
| 17 |
+
"artistic_style": [
|
| 18 |
+
"painting",
|
| 19 |
+
"illustration",
|
| 20 |
+
"drawing",
|
| 21 |
+
"cartoon",
|
| 22 |
+
"3d render",
|
| 23 |
+
"cgi",
|
| 24 |
+
"sketch",
|
| 25 |
+
"digital art"
|
| 26 |
+
],
|
| 27 |
+
"composition_and_content": [
|
| 28 |
+
"text",
|
| 29 |
+
"watermark",
|
| 30 |
+
"signature",
|
| 31 |
+
"logo",
|
| 32 |
+
"subtitles",
|
| 33 |
+
"pillarboxed",
|
| 34 |
+
"side bars",
|
| 35 |
+
"portrait image in landscape frame"
|
| 36 |
+
],
|
| 37 |
+
"temporal_and_motion_stability": [
|
| 38 |
+
"flickering",
|
| 39 |
+
"jittery",
|
| 40 |
+
"motion blur",
|
| 41 |
+
"temporal inconsistency",
|
| 42 |
+
"warping",
|
| 43 |
+
"morphing",
|
| 44 |
+
"incoherent motion",
|
| 45 |
+
"unnatural movement",
|
| 46 |
+
"static object with sudden jump",
|
| 47 |
+
"frame-to-frame inconsistency"
|
| 48 |
+
],
|
| 49 |
+
"material_and_structure": [
|
| 50 |
+
"plastic-like glass",
|
| 51 |
+
"unrealistic texture",
|
| 52 |
+
"deformed bottle",
|
| 53 |
+
"liquid freezing improperly",
|
| 54 |
+
"distorted reflections"
|
| 55 |
+
]
|
| 56 |
+
}
|
| 57 |
+
}
|
lingbot_video/default_negative_prompt_image.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"universal_negative": {
|
| 3 |
+
"visual_quality": [
|
| 4 |
+
"low quality",
|
| 5 |
+
"worst quality",
|
| 6 |
+
"blurry",
|
| 7 |
+
"pixelated",
|
| 8 |
+
"jpeg artifacts",
|
| 9 |
+
"low resolution",
|
| 10 |
+
"underexposed",
|
| 11 |
+
"overexposed",
|
| 12 |
+
"invisible subject",
|
| 13 |
+
"subject hidden in darkness"
|
| 14 |
+
],
|
| 15 |
+
"artistic_style": [
|
| 16 |
+
"painting",
|
| 17 |
+
"illustration",
|
| 18 |
+
"drawing",
|
| 19 |
+
"cartoon",
|
| 20 |
+
"3d render",
|
| 21 |
+
"cgi",
|
| 22 |
+
"sketch",
|
| 23 |
+
"digital art"
|
| 24 |
+
],
|
| 25 |
+
"composition_and_content": [
|
| 26 |
+
"text",
|
| 27 |
+
"watermark",
|
| 28 |
+
"signature",
|
| 29 |
+
"logo",
|
| 30 |
+
"pillarboxed",
|
| 31 |
+
"side bars",
|
| 32 |
+
"portrait image in landscape frame"
|
| 33 |
+
],
|
| 34 |
+
"material_and_structure": [
|
| 35 |
+
"plastic-like glass",
|
| 36 |
+
"unrealistic texture",
|
| 37 |
+
"deformed bottle",
|
| 38 |
+
"distorted reflections"
|
| 39 |
+
]
|
| 40 |
+
}
|
| 41 |
+
}
|
lingbot_video/fsdp_inference.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import gc
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
import torch.distributed as dist
|
| 9 |
+
from torch.distributed.device_mesh import DeviceMesh, init_device_mesh
|
| 10 |
+
|
| 11 |
+
try:
|
| 12 |
+
from torch.distributed._composable.fsdp import fully_shard
|
| 13 |
+
except Exception as exc: # pragma: no cover - depends on the installed torch build
|
| 14 |
+
fully_shard = None
|
| 15 |
+
FSDP_IMPORT_ERROR = exc
|
| 16 |
+
else:
|
| 17 |
+
FSDP_IMPORT_ERROR = None
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass(frozen=True)
|
| 21 |
+
class FSDPInferenceInfo:
|
| 22 |
+
enabled: bool
|
| 23 |
+
world_size: int
|
| 24 |
+
wrapped_blocks: int
|
| 25 |
+
ignored_params: int
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def init_fsdp_inference_mesh() -> DeviceMesh | None:
|
| 29 |
+
if not dist.is_available() or not dist.is_initialized():
|
| 30 |
+
return None
|
| 31 |
+
world_size = dist.get_world_size()
|
| 32 |
+
if world_size <= 1:
|
| 33 |
+
return None
|
| 34 |
+
return init_device_mesh("cuda", (world_size,), mesh_dim_names=("fsdp",))
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _move_buffers_to_device(module: torch.nn.Module, device: torch.device) -> None:
|
| 38 |
+
for submodule in module.modules():
|
| 39 |
+
for name, buffer in tuple(submodule.named_buffers(recurse=False)):
|
| 40 |
+
if buffer is not None and buffer.device != device:
|
| 41 |
+
submodule._buffers[name] = buffer.to(device=device)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def _move_parameters_to_device(
|
| 45 |
+
parameters: set[torch.nn.Parameter],
|
| 46 |
+
device: torch.device,
|
| 47 |
+
) -> None:
|
| 48 |
+
for param in parameters:
|
| 49 |
+
if param.device != device:
|
| 50 |
+
param.data = param.data.to(device=device)
|
| 51 |
+
if param.grad is not None and param.grad.device != device:
|
| 52 |
+
param.grad.data = param.grad.data.to(device=device)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _current_cuda_device() -> torch.device | None:
|
| 56 |
+
if not torch.cuda.is_available():
|
| 57 |
+
return None
|
| 58 |
+
return torch.device("cuda", torch.cuda.current_device())
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def apply_fsdp_inference(
|
| 62 |
+
transformer: torch.nn.Module,
|
| 63 |
+
mesh: DeviceMesh | None,
|
| 64 |
+
) -> FSDPInferenceInfo:
|
| 65 |
+
if mesh is None:
|
| 66 |
+
return FSDPInferenceInfo(enabled=False, world_size=1, wrapped_blocks=0, ignored_params=0)
|
| 67 |
+
if bool(getattr(transformer, "_lingbot_fsdp_inference_enabled", False)):
|
| 68 |
+
blocks = getattr(transformer, "blocks", ())
|
| 69 |
+
return FSDPInferenceInfo(
|
| 70 |
+
enabled=True,
|
| 71 |
+
world_size=int(mesh.size()),
|
| 72 |
+
wrapped_blocks=len(blocks),
|
| 73 |
+
ignored_params=int(getattr(transformer, "_lingbot_fsdp_inference_ignored_params", 0)),
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
if fully_shard is None:
|
| 77 |
+
raise RuntimeError("PyTorch composable FSDP is not importable.") from FSDP_IMPORT_ERROR
|
| 78 |
+
|
| 79 |
+
cuda_device = _current_cuda_device()
|
| 80 |
+
if cuda_device is not None:
|
| 81 |
+
_move_buffers_to_device(transformer, cuda_device)
|
| 82 |
+
|
| 83 |
+
dtype_counts: dict[torch.dtype, int] = {}
|
| 84 |
+
for param in transformer.parameters():
|
| 85 |
+
dtype_counts[param.dtype] = dtype_counts.get(param.dtype, 0) + param.numel()
|
| 86 |
+
if dtype_counts:
|
| 87 |
+
primary_dtype = max(dtype_counts.items(), key=lambda item: item[1])[0]
|
| 88 |
+
ignored_params = {param for param in transformer.parameters() if param.dtype != primary_dtype}
|
| 89 |
+
else:
|
| 90 |
+
ignored_params = set()
|
| 91 |
+
if cuda_device is not None and ignored_params:
|
| 92 |
+
_move_parameters_to_device(ignored_params, cuda_device)
|
| 93 |
+
|
| 94 |
+
blocks: Any = getattr(transformer, "blocks", ())
|
| 95 |
+
wrapped_blocks = 0
|
| 96 |
+
for block in blocks:
|
| 97 |
+
block_ignored_params = {
|
| 98 |
+
param for param in block.parameters() if param in ignored_params
|
| 99 |
+
}
|
| 100 |
+
fully_shard(block, mesh=mesh, ignored_params=block_ignored_params)
|
| 101 |
+
wrapped_blocks += 1
|
| 102 |
+
fully_shard(transformer, mesh=mesh, ignored_params=ignored_params)
|
| 103 |
+
gc.collect()
|
| 104 |
+
if cuda_device is not None:
|
| 105 |
+
torch.cuda.empty_cache()
|
| 106 |
+
transformer._lingbot_fsdp_inference_enabled = True
|
| 107 |
+
transformer._lingbot_fsdp_inference_ignored_params = len(ignored_params)
|
| 108 |
+
return FSDPInferenceInfo(
|
| 109 |
+
enabled=True,
|
| 110 |
+
world_size=int(mesh.size()),
|
| 111 |
+
wrapped_blocks=wrapped_blocks,
|
| 112 |
+
ignored_params=len(ignored_params),
|
| 113 |
+
)
|
lingbot_video/inference_backend.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import importlib.util
|
| 4 |
+
import json
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Sequence, TextIO
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
BACKEND_TO_ENGINE = {
|
| 11 |
+
"diffusers": "diffusers",
|
| 12 |
+
"sglang": "sglang-native",
|
| 13 |
+
}
|
| 14 |
+
ENGINE_CHOICES = frozenset(BACKEND_TO_ENGINE.values())
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _option_present(argv: Sequence[str], option: str) -> bool:
|
| 18 |
+
return any(arg == option or arg.startswith(f"{option}=") for arg in argv)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def default_backend_argv(argv: Sequence[str]) -> list[str]:
|
| 22 |
+
"""Default the public CLI to direct diffusers when no backend is requested."""
|
| 23 |
+
|
| 24 |
+
normalized = list(argv)
|
| 25 |
+
if _option_present(normalized, "--backend") or _option_present(normalized, "--engine"):
|
| 26 |
+
return normalized
|
| 27 |
+
return ["--backend", "diffusers", *normalized]
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _module_available(name: str) -> bool:
|
| 31 |
+
if name in sys.modules:
|
| 32 |
+
return True
|
| 33 |
+
try:
|
| 34 |
+
return importlib.util.find_spec(name) is not None
|
| 35 |
+
except (ImportError, ModuleNotFoundError, KeyError, ValueError):
|
| 36 |
+
return False
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def sglang_native_available() -> bool:
|
| 40 |
+
"""Return whether the local SGLang-native adapter can be imported."""
|
| 41 |
+
|
| 42 |
+
if not _module_available("sglang"):
|
| 43 |
+
return False
|
| 44 |
+
try:
|
| 45 |
+
from lingbot_video.native_backend import LingBotVideoNativePipeline
|
| 46 |
+
except Exception:
|
| 47 |
+
return False
|
| 48 |
+
return LingBotVideoNativePipeline is not None
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def resolve_backend_engine(
|
| 52 |
+
*,
|
| 53 |
+
engine: str | None,
|
| 54 |
+
backend: str | None,
|
| 55 |
+
sglang_available: bool | None = None,
|
| 56 |
+
stderr: TextIO | None = None,
|
| 57 |
+
) -> str:
|
| 58 |
+
"""Resolve public ``--backend`` and internal ``--engine`` into a runner engine."""
|
| 59 |
+
|
| 60 |
+
explicit_engine = backend is None and engine is not None
|
| 61 |
+
if backend is not None:
|
| 62 |
+
if backend not in BACKEND_TO_ENGINE:
|
| 63 |
+
choices = ", ".join(sorted(BACKEND_TO_ENGINE))
|
| 64 |
+
raise ValueError(f"unsupported backend: {backend!r}; choices: {choices}")
|
| 65 |
+
resolved = BACKEND_TO_ENGINE[backend]
|
| 66 |
+
elif engine is not None:
|
| 67 |
+
if engine not in ENGINE_CHOICES:
|
| 68 |
+
choices = ", ".join(sorted(ENGINE_CHOICES))
|
| 69 |
+
raise ValueError(f"unsupported engine: {engine!r}; choices: {choices}")
|
| 70 |
+
resolved = engine
|
| 71 |
+
else:
|
| 72 |
+
resolved = "sglang-native"
|
| 73 |
+
|
| 74 |
+
if resolved != "sglang-native":
|
| 75 |
+
return resolved
|
| 76 |
+
if explicit_engine:
|
| 77 |
+
return resolved
|
| 78 |
+
|
| 79 |
+
available = sglang_available
|
| 80 |
+
if available is None:
|
| 81 |
+
available = sglang_native_available()
|
| 82 |
+
if available:
|
| 83 |
+
return resolved
|
| 84 |
+
|
| 85 |
+
stream = stderr if stderr is not None else sys.stderr
|
| 86 |
+
print(
|
| 87 |
+
"WARNING: SGLang backend requested but SGLang is not installed or its "
|
| 88 |
+
"native diffusion API is unavailable; falling back to diffusers. "
|
| 89 |
+
"Install requirements-sglang.txt to enable SGLang Diffusion.",
|
| 90 |
+
file=stream,
|
| 91 |
+
flush=True,
|
| 92 |
+
)
|
| 93 |
+
return "diffusers"
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def load_negative_prompt_json(path: str | Path) -> str:
|
| 97 |
+
"""Load an auto-negative JSON file and serialize it as the prompt string."""
|
| 98 |
+
|
| 99 |
+
with Path(path).open(encoding="utf-8") as f:
|
| 100 |
+
payload = json.load(f)
|
| 101 |
+
return json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def resolve_negative_prompt_arg(
|
| 105 |
+
negative_prompt: str | None,
|
| 106 |
+
negative_prompt_json: str | None,
|
| 107 |
+
) -> str | None:
|
| 108 |
+
"""Resolve manual and auto-negative prompt inputs for the runner."""
|
| 109 |
+
|
| 110 |
+
if negative_prompt and negative_prompt_json:
|
| 111 |
+
raise ValueError("Use either --negative_prompt or --negative_prompt_json, not both.")
|
| 112 |
+
if negative_prompt_json:
|
| 113 |
+
return load_negative_prompt_json(negative_prompt_json)
|
| 114 |
+
return negative_prompt
|
lingbot_video/model_paths.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def model_component_dir(model_dir: str | Path, component: str) -> Path:
|
| 8 |
+
"""Return a named component directory under a public model root."""
|
| 9 |
+
|
| 10 |
+
path = Path(model_dir) / component
|
| 11 |
+
if not path.is_dir():
|
| 12 |
+
raise FileNotFoundError(f"missing model component `{component}`: {path}")
|
| 13 |
+
return path
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def effective_refiner_model_dir(args: Any) -> Path | None:
|
| 17 |
+
"""Resolve the model root to use for refiner loading.
|
| 18 |
+
|
| 19 |
+
Passing --run_refiner means the public runner should load `refiner/` from
|
| 20 |
+
the same model root by default. --refiner_model_dir remains an override for
|
| 21 |
+
nonstandard package layouts.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
refiner_model_dir = getattr(args, "refiner_model_dir", None)
|
| 25 |
+
requested = bool(getattr(args, "run_refiner", False) or refiner_model_dir)
|
| 26 |
+
if not requested:
|
| 27 |
+
return None
|
| 28 |
+
return Path(refiner_model_dir or getattr(args, "model_dir"))
|
lingbot_video/moe_pack_kernels.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
try:
|
| 6 |
+
import triton
|
| 7 |
+
import triton.language as tl
|
| 8 |
+
except Exception: # pragma: no cover - Triton is optional outside GPU deployments.
|
| 9 |
+
triton = None
|
| 10 |
+
tl = None
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
if triton is not None and tl is not None:
|
| 14 |
+
@triton.jit
|
| 15 |
+
def _moe_route_count_slots_kernel(
|
| 16 |
+
flat_indices,
|
| 17 |
+
counts,
|
| 18 |
+
route_slots,
|
| 19 |
+
num_routes: tl.constexpr,
|
| 20 |
+
block_m: tl.constexpr,
|
| 21 |
+
):
|
| 22 |
+
offsets = tl.program_id(0) * block_m + tl.arange(0, block_m)
|
| 23 |
+
mask = offsets < num_routes
|
| 24 |
+
experts = tl.load(flat_indices + offsets, mask=mask, other=0)
|
| 25 |
+
slots = tl.atomic_add(counts + experts, 1, sem="relaxed", mask=mask)
|
| 26 |
+
tl.store(route_slots + offsets, slots, mask=mask)
|
| 27 |
+
|
| 28 |
+
@triton.jit
|
| 29 |
+
def _moe_pack_tokens_kernel(
|
| 30 |
+
tokens,
|
| 31 |
+
flat_scores,
|
| 32 |
+
flat_indices,
|
| 33 |
+
offsets,
|
| 34 |
+
route_slots,
|
| 35 |
+
permuted_tokens,
|
| 36 |
+
sorted_positions,
|
| 37 |
+
sorted_scores,
|
| 38 |
+
hidden_size: tl.constexpr,
|
| 39 |
+
top_k: tl.constexpr,
|
| 40 |
+
block_h: tl.constexpr,
|
| 41 |
+
):
|
| 42 |
+
route_idx = tl.program_id(0)
|
| 43 |
+
hidden_block = tl.program_id(1)
|
| 44 |
+
offsets_h = hidden_block * block_h + tl.arange(0, block_h)
|
| 45 |
+
hidden_mask = offsets_h < hidden_size
|
| 46 |
+
|
| 47 |
+
expert_idx = tl.load(flat_indices + route_idx)
|
| 48 |
+
slot = tl.load(route_slots + route_idx)
|
| 49 |
+
expert_offset = tl.load(offsets + expert_idx)
|
| 50 |
+
dest_idx = expert_offset + slot
|
| 51 |
+
token_idx = route_idx // top_k
|
| 52 |
+
|
| 53 |
+
values = tl.load(
|
| 54 |
+
tokens + token_idx * hidden_size + offsets_h,
|
| 55 |
+
mask=hidden_mask,
|
| 56 |
+
other=0.0,
|
| 57 |
+
)
|
| 58 |
+
tl.store(
|
| 59 |
+
permuted_tokens + dest_idx * hidden_size + offsets_h,
|
| 60 |
+
values,
|
| 61 |
+
mask=hidden_mask,
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
first_hidden_block = hidden_block == 0
|
| 65 |
+
tl.store(sorted_positions + dest_idx, route_idx, mask=first_hidden_block)
|
| 66 |
+
score = tl.load(flat_scores + route_idx, mask=first_hidden_block, other=0.0)
|
| 67 |
+
tl.store(sorted_scores + dest_idx, score, mask=first_hidden_block)
|
| 68 |
+
else:
|
| 69 |
+
_moe_route_count_slots_kernel = None
|
| 70 |
+
_moe_pack_tokens_kernel = None
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def reorder_tokens_triton_pack(
|
| 74 |
+
tokens: torch.Tensor,
|
| 75 |
+
top_scores: torch.Tensor,
|
| 76 |
+
top_indices: torch.Tensor,
|
| 77 |
+
num_experts: int,
|
| 78 |
+
block_h: int = 64,
|
| 79 |
+
block_m: int = 256,
|
| 80 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, int, int]:
|
| 81 |
+
if _moe_route_count_slots_kernel is None or _moe_pack_tokens_kernel is None:
|
| 82 |
+
raise RuntimeError("LINGBOT_MOE_REORDER_BACKEND=triton_pack requires Triton")
|
| 83 |
+
if tokens.device.type != "cuda":
|
| 84 |
+
raise RuntimeError("LINGBOT_MOE_REORDER_BACKEND=triton_pack requires CUDA tensors")
|
| 85 |
+
if tokens.ndim != 2:
|
| 86 |
+
raise ValueError(f"Expected 2D tokens, got {tokens.ndim}D")
|
| 87 |
+
if top_scores.shape != top_indices.shape:
|
| 88 |
+
raise ValueError("top_scores and top_indices must have the same shape")
|
| 89 |
+
if top_indices.ndim != 2:
|
| 90 |
+
raise ValueError(f"Expected 2D top_indices, got {top_indices.ndim}D")
|
| 91 |
+
|
| 92 |
+
num_tokens = tokens.shape[0]
|
| 93 |
+
hidden_size = tokens.shape[1]
|
| 94 |
+
top_k = top_indices.shape[1]
|
| 95 |
+
num_routes = top_indices.numel()
|
| 96 |
+
counts = torch.zeros(num_experts, dtype=torch.int32, device=tokens.device)
|
| 97 |
+
|
| 98 |
+
if num_routes == 0:
|
| 99 |
+
return (
|
| 100 |
+
tokens.new_empty((0, hidden_size)),
|
| 101 |
+
counts,
|
| 102 |
+
torch.empty(0, dtype=torch.int64, device=tokens.device),
|
| 103 |
+
top_scores.new_empty((0,)),
|
| 104 |
+
num_tokens,
|
| 105 |
+
top_k,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
tokens = tokens.contiguous()
|
| 109 |
+
flat_scores = top_scores.contiguous().reshape(-1)
|
| 110 |
+
flat_indices = top_indices.contiguous().to(torch.int32).reshape(-1)
|
| 111 |
+
route_slots = torch.empty(num_routes, dtype=torch.int32, device=tokens.device)
|
| 112 |
+
|
| 113 |
+
_moe_route_count_slots_kernel[(triton.cdiv(num_routes, block_m),)](
|
| 114 |
+
flat_indices,
|
| 115 |
+
counts,
|
| 116 |
+
route_slots,
|
| 117 |
+
num_routes,
|
| 118 |
+
block_m,
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
counts_i64 = counts.to(torch.int64)
|
| 122 |
+
offsets = torch.empty(num_experts + 1, dtype=torch.int64, device=tokens.device)
|
| 123 |
+
offsets[0] = 0
|
| 124 |
+
offsets[1:] = torch.cumsum(counts_i64, dim=0)
|
| 125 |
+
|
| 126 |
+
permuted_tokens = torch.empty((num_routes, hidden_size), dtype=tokens.dtype, device=tokens.device)
|
| 127 |
+
sorted_positions = torch.empty(num_routes, dtype=torch.int64, device=tokens.device)
|
| 128 |
+
sorted_scores = torch.empty(num_routes, dtype=top_scores.dtype, device=tokens.device)
|
| 129 |
+
_moe_pack_tokens_kernel[(num_routes, triton.cdiv(hidden_size, block_h))](
|
| 130 |
+
tokens,
|
| 131 |
+
flat_scores,
|
| 132 |
+
flat_indices,
|
| 133 |
+
offsets,
|
| 134 |
+
route_slots,
|
| 135 |
+
permuted_tokens,
|
| 136 |
+
sorted_positions,
|
| 137 |
+
sorted_scores,
|
| 138 |
+
hidden_size,
|
| 139 |
+
top_k,
|
| 140 |
+
block_h,
|
| 141 |
+
)
|
| 142 |
+
return permuted_tokens, counts, sorted_positions, sorted_scores, num_tokens, top_k
|
lingbot_video/moe_restore_kernels.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
try:
|
| 6 |
+
import triton
|
| 7 |
+
import triton.language as tl
|
| 8 |
+
except Exception: # pragma: no cover - Triton is optional outside GPU deployments.
|
| 9 |
+
triton = None
|
| 10 |
+
tl = None
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
if triton is not None and tl is not None:
|
| 14 |
+
@triton.jit
|
| 15 |
+
def _moe_restore_weighted_sum_kernel(
|
| 16 |
+
expert_output,
|
| 17 |
+
sorted_scores,
|
| 18 |
+
route_for_position,
|
| 19 |
+
output,
|
| 20 |
+
hidden_size: tl.constexpr,
|
| 21 |
+
top_k: tl.constexpr,
|
| 22 |
+
block_h: tl.constexpr,
|
| 23 |
+
):
|
| 24 |
+
token_idx = tl.program_id(0)
|
| 25 |
+
hidden_block = tl.program_id(1)
|
| 26 |
+
offsets_h = hidden_block * block_h + tl.arange(0, block_h)
|
| 27 |
+
hidden_mask = offsets_h < hidden_size
|
| 28 |
+
acc = tl.zeros((block_h,), dtype=tl.float32)
|
| 29 |
+
|
| 30 |
+
for route_slot in tl.range(0, top_k):
|
| 31 |
+
route_pos = token_idx * top_k + route_slot
|
| 32 |
+
route_idx = tl.load(route_for_position + route_pos)
|
| 33 |
+
active = route_idx >= 0
|
| 34 |
+
safe_route_idx = tl.maximum(route_idx, 0)
|
| 35 |
+
values = tl.load(
|
| 36 |
+
expert_output + safe_route_idx * hidden_size + offsets_h,
|
| 37 |
+
mask=active & hidden_mask,
|
| 38 |
+
other=0.0,
|
| 39 |
+
).to(tl.float32)
|
| 40 |
+
score = tl.load(sorted_scores + safe_route_idx, mask=active, other=0.0).to(tl.float32)
|
| 41 |
+
acc += values * score
|
| 42 |
+
|
| 43 |
+
tl.store(output + token_idx * hidden_size + offsets_h, acc, mask=hidden_mask)
|
| 44 |
+
else:
|
| 45 |
+
_moe_restore_weighted_sum_kernel = None
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def restore_tokens_triton(
|
| 49 |
+
expert_output: torch.Tensor,
|
| 50 |
+
sorted_positions: torch.Tensor,
|
| 51 |
+
sorted_scores: torch.Tensor,
|
| 52 |
+
num_tokens: int,
|
| 53 |
+
top_k: int,
|
| 54 |
+
block_h: int = 64,
|
| 55 |
+
) -> torch.Tensor:
|
| 56 |
+
if _moe_restore_weighted_sum_kernel is None:
|
| 57 |
+
raise RuntimeError("LINGBOT_MOE_RESTORE_BACKEND=triton requires Triton")
|
| 58 |
+
if expert_output.ndim != 2:
|
| 59 |
+
raise ValueError(f"Expected 2D expert_output, got {expert_output.ndim}D")
|
| 60 |
+
if sorted_positions.numel() != sorted_scores.numel():
|
| 61 |
+
raise ValueError("sorted_positions and sorted_scores must have the same length")
|
| 62 |
+
if sorted_positions.numel() == 0:
|
| 63 |
+
return expert_output.new_zeros((num_tokens, expert_output.shape[-1]))
|
| 64 |
+
|
| 65 |
+
hidden_size = expert_output.shape[-1]
|
| 66 |
+
route_for_position = torch.full(
|
| 67 |
+
(num_tokens * top_k,),
|
| 68 |
+
-1,
|
| 69 |
+
dtype=torch.int32,
|
| 70 |
+
device=expert_output.device,
|
| 71 |
+
)
|
| 72 |
+
route_for_position[sorted_positions] = torch.arange(
|
| 73 |
+
sorted_positions.numel(),
|
| 74 |
+
dtype=torch.int32,
|
| 75 |
+
device=expert_output.device,
|
| 76 |
+
)
|
| 77 |
+
output = torch.empty(
|
| 78 |
+
(num_tokens, hidden_size),
|
| 79 |
+
dtype=expert_output.dtype,
|
| 80 |
+
device=expert_output.device,
|
| 81 |
+
)
|
| 82 |
+
grid = (num_tokens, triton.cdiv(hidden_size, block_h))
|
| 83 |
+
_moe_restore_weighted_sum_kernel[grid](
|
| 84 |
+
expert_output.contiguous(),
|
| 85 |
+
sorted_scores.contiguous(),
|
| 86 |
+
route_for_position,
|
| 87 |
+
output,
|
| 88 |
+
hidden_size,
|
| 89 |
+
top_k,
|
| 90 |
+
block_h,
|
| 91 |
+
)
|
| 92 |
+
return output
|
lingbot_video/native_backend.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from contextlib import contextmanager
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from diffusers import DiffusionPipeline
|
| 11 |
+
|
| 12 |
+
from lingbot_video.model_paths import model_component_dir
|
| 13 |
+
from lingbot_video.pipeline_lingbot_video import (
|
| 14 |
+
DEFAULT_NEGATIVE_PROMPT,
|
| 15 |
+
LingBotVideoPipeline,
|
| 16 |
+
LingBotVideoPipelineOutput,
|
| 17 |
+
)
|
| 18 |
+
from lingbot_video.pipeline_lingbot_video_i2v import LingBotVideoImageToVideoPipeline
|
| 19 |
+
from lingbot_video.transformer_lingbot_video import LingBotVideoTransformer3DModel
|
| 20 |
+
|
| 21 |
+
try:
|
| 22 |
+
from sglang.multimodal_gen import registry as sglang_registry
|
| 23 |
+
except Exception: # pragma: no cover - SGLang is an optional deployment dep
|
| 24 |
+
sglang_registry = None
|
| 25 |
+
|
| 26 |
+
try:
|
| 27 |
+
from sglang.multimodal_gen.runtime.server_args import (
|
| 28 |
+
Backend,
|
| 29 |
+
ServerArgs,
|
| 30 |
+
set_global_server_args,
|
| 31 |
+
)
|
| 32 |
+
except Exception: # pragma: no cover - SGLang is an optional deployment dep
|
| 33 |
+
Backend = None
|
| 34 |
+
ServerArgs = None
|
| 35 |
+
set_global_server_args = None
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@dataclass
|
| 39 |
+
class LingBotVideoNativePipelineConfig:
|
| 40 |
+
"""Minimal config object for the LingBotVideo native adapter."""
|
| 41 |
+
|
| 42 |
+
flow_shift: float = 3.0
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
@dataclass
|
| 46 |
+
class LingBotVideoNativeSamplingParams:
|
| 47 |
+
"""Default sampling parameters for the native pipeline."""
|
| 48 |
+
|
| 49 |
+
prompt: str | None = None
|
| 50 |
+
negative_prompt: str = DEFAULT_NEGATIVE_PROMPT
|
| 51 |
+
height: int = 480
|
| 52 |
+
width: int = 832
|
| 53 |
+
num_frames: int = 81
|
| 54 |
+
fps: int = 24
|
| 55 |
+
num_inference_steps: int = 40
|
| 56 |
+
guidance_scale: float = 3.0
|
| 57 |
+
seed: int = 42
|
| 58 |
+
shift: float = 3.0
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@contextmanager
|
| 62 |
+
def _patch_qwen3vl_from_pretrained():
|
| 63 |
+
try:
|
| 64 |
+
from transformers import Qwen3VLForConditionalGeneration
|
| 65 |
+
except Exception:
|
| 66 |
+
yield
|
| 67 |
+
return
|
| 68 |
+
|
| 69 |
+
original_from_pretrained = Qwen3VLForConditionalGeneration.from_pretrained
|
| 70 |
+
attn_implementation = os.environ.get("LINGBOT_QWEN_ATTN_IMPLEMENTATION", "flash_attention_3")
|
| 71 |
+
|
| 72 |
+
@classmethod
|
| 73 |
+
def patched_from_pretrained(cls, pretrained_model_name_or_path, *args, **kwargs):
|
| 74 |
+
if attn_implementation:
|
| 75 |
+
kwargs.setdefault("attn_implementation", attn_implementation)
|
| 76 |
+
if "torch_dtype" in kwargs and "dtype" not in kwargs:
|
| 77 |
+
kwargs["dtype"] = kwargs.pop("torch_dtype")
|
| 78 |
+
return original_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
|
| 79 |
+
|
| 80 |
+
Qwen3VLForConditionalGeneration.from_pretrained = patched_from_pretrained
|
| 81 |
+
try:
|
| 82 |
+
yield
|
| 83 |
+
finally:
|
| 84 |
+
Qwen3VLForConditionalGeneration.from_pretrained = original_from_pretrained
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _module_dtype_name(module: Any) -> str | None:
|
| 88 |
+
if module is None:
|
| 89 |
+
return None
|
| 90 |
+
dtype = getattr(module, "dtype", None)
|
| 91 |
+
if isinstance(dtype, torch.dtype):
|
| 92 |
+
return str(dtype).replace("torch.", "")
|
| 93 |
+
if isinstance(module, torch.nn.Module):
|
| 94 |
+
try:
|
| 95 |
+
return str(next(module.parameters()).dtype).replace("torch.", "")
|
| 96 |
+
except StopIteration:
|
| 97 |
+
return None
|
| 98 |
+
return None
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def _default_device() -> torch.device:
|
| 102 |
+
if not torch.cuda.is_available():
|
| 103 |
+
return torch.device("cpu")
|
| 104 |
+
return torch.device("cuda", torch.cuda.current_device())
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _pipeline_class_for_mode(mode: str) -> type[DiffusionPipeline]:
|
| 108 |
+
if mode == "ti2v":
|
| 109 |
+
return LingBotVideoImageToVideoPipeline
|
| 110 |
+
return LingBotVideoPipeline
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _load_lingbot_diffusers_pipe(
|
| 114 |
+
model_path: str | Path,
|
| 115 |
+
dtype_map: dict[str, torch.dtype] | torch.dtype | None,
|
| 116 |
+
mode: str,
|
| 117 |
+
transformer_subfolder: str,
|
| 118 |
+
) -> DiffusionPipeline:
|
| 119 |
+
model_root = Path(model_path)
|
| 120 |
+
model_component_dir(model_root, transformer_subfolder)
|
| 121 |
+
transformer_dtype = (
|
| 122 |
+
dtype_map.get("transformer", dtype_map.get("default", torch.float32))
|
| 123 |
+
if isinstance(dtype_map, dict)
|
| 124 |
+
else dtype_map
|
| 125 |
+
)
|
| 126 |
+
transformer = LingBotVideoTransformer3DModel.from_pretrained(
|
| 127 |
+
str(model_root),
|
| 128 |
+
subfolder=transformer_subfolder,
|
| 129 |
+
torch_dtype=transformer_dtype,
|
| 130 |
+
)
|
| 131 |
+
load_kwargs: dict[str, Any] = {
|
| 132 |
+
"transformer": transformer,
|
| 133 |
+
"trust_remote_code": True,
|
| 134 |
+
}
|
| 135 |
+
if dtype_map is not None:
|
| 136 |
+
load_kwargs["torch_dtype"] = dtype_map
|
| 137 |
+
with _patch_qwen3vl_from_pretrained():
|
| 138 |
+
pipe = _pipeline_class_for_mode(mode).from_pretrained(
|
| 139 |
+
str(model_root),
|
| 140 |
+
**load_kwargs,
|
| 141 |
+
)
|
| 142 |
+
return pipe.to(_default_device())
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
class LingBotVideoNativeExecutionStage:
|
| 146 |
+
"""Single-stage LingBotVideo execution."""
|
| 147 |
+
|
| 148 |
+
def __init__(self, diffusers_pipe: DiffusionPipeline):
|
| 149 |
+
self.diffusers_pipe = diffusers_pipe
|
| 150 |
+
|
| 151 |
+
def __call__(self, **kwargs: Any) -> LingBotVideoPipelineOutput:
|
| 152 |
+
return self.forward(**kwargs)
|
| 153 |
+
|
| 154 |
+
@torch.no_grad()
|
| 155 |
+
def forward(self, **kwargs: Any) -> LingBotVideoPipelineOutput:
|
| 156 |
+
output = self.diffusers_pipe(**kwargs)
|
| 157 |
+
frames = output.frames if hasattr(output, "frames") else output[0]
|
| 158 |
+
return LingBotVideoPipelineOutput(frames=frames)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
class LingBotVideoNativePipeline:
|
| 162 |
+
"""SGLang-native LingBotVideo pipeline adapter."""
|
| 163 |
+
|
| 164 |
+
pipeline_name = "LingBotVideoNativePipeline"
|
| 165 |
+
pipeline_config_cls = LingBotVideoNativePipelineConfig
|
| 166 |
+
sampling_params_cls = LingBotVideoNativeSamplingParams
|
| 167 |
+
is_video_pipeline = True
|
| 168 |
+
|
| 169 |
+
def __init__(
|
| 170 |
+
self,
|
| 171 |
+
diffusers_pipe: DiffusionPipeline | str | Path,
|
| 172 |
+
*,
|
| 173 |
+
model_path: str | Path | None = None,
|
| 174 |
+
server_args: Any | None = None,
|
| 175 |
+
dtype_map: dict[str, torch.dtype] | torch.dtype | None = None,
|
| 176 |
+
mode: str = "t2v",
|
| 177 |
+
transformer_subfolder: str = "transformer",
|
| 178 |
+
**_: Any,
|
| 179 |
+
):
|
| 180 |
+
if isinstance(diffusers_pipe, (str, Path)):
|
| 181 |
+
model_path = diffusers_pipe if model_path is None else model_path
|
| 182 |
+
diffusers_pipe = _load_lingbot_diffusers_pipe(
|
| 183 |
+
model_path,
|
| 184 |
+
dtype_map,
|
| 185 |
+
mode=mode,
|
| 186 |
+
transformer_subfolder=transformer_subfolder,
|
| 187 |
+
)
|
| 188 |
+
if model_path is None:
|
| 189 |
+
raise ValueError("model_path is required for LingBotVideoNativePipeline")
|
| 190 |
+
self.diffusers_pipe = diffusers_pipe
|
| 191 |
+
self.model_path = str(model_path)
|
| 192 |
+
self.server_args = server_args
|
| 193 |
+
self.execution_stage = LingBotVideoNativeExecutionStage(diffusers_pipe)
|
| 194 |
+
self.modules = {"lingbot_video_pipeline": diffusers_pipe}
|
| 195 |
+
self.memory_usages: dict[str, float] = {}
|
| 196 |
+
|
| 197 |
+
@property
|
| 198 |
+
def transformer(self) -> Any:
|
| 199 |
+
return getattr(self.diffusers_pipe, "transformer", None)
|
| 200 |
+
|
| 201 |
+
@property
|
| 202 |
+
def text_encoder(self) -> Any:
|
| 203 |
+
return getattr(self.diffusers_pipe, "text_encoder", None)
|
| 204 |
+
|
| 205 |
+
@property
|
| 206 |
+
def vae(self) -> Any:
|
| 207 |
+
return getattr(self.diffusers_pipe, "vae", None)
|
| 208 |
+
|
| 209 |
+
@property
|
| 210 |
+
def scheduler(self) -> Any:
|
| 211 |
+
return getattr(self.diffusers_pipe, "scheduler", None)
|
| 212 |
+
|
| 213 |
+
def encode_prompt(self, *args: Any, **kwargs: Any) -> Any:
|
| 214 |
+
return self.diffusers_pipe.encode_prompt(*args, **kwargs)
|
| 215 |
+
|
| 216 |
+
def encode_video_latent(self, *args: Any, **kwargs: Any) -> Any:
|
| 217 |
+
return self.diffusers_pipe.encode_video_latent(*args, **kwargs)
|
| 218 |
+
|
| 219 |
+
@classmethod
|
| 220 |
+
def from_diffusers_pipe(
|
| 221 |
+
cls,
|
| 222 |
+
diffusers_pipe: DiffusionPipeline,
|
| 223 |
+
*,
|
| 224 |
+
model_path: str | Path,
|
| 225 |
+
server_args: Any | None = None,
|
| 226 |
+
) -> "LingBotVideoNativePipeline":
|
| 227 |
+
return cls(diffusers_pipe, model_path=model_path, server_args=server_args)
|
| 228 |
+
|
| 229 |
+
@classmethod
|
| 230 |
+
def from_pretrained(
|
| 231 |
+
cls,
|
| 232 |
+
model_path: str | Path,
|
| 233 |
+
*,
|
| 234 |
+
torch_dtype: dict[str, torch.dtype] | torch.dtype | None = None,
|
| 235 |
+
server_args: Any | None = None,
|
| 236 |
+
mode: str = "t2v",
|
| 237 |
+
transformer_subfolder: str = "transformer",
|
| 238 |
+
**kwargs: Any,
|
| 239 |
+
) -> "LingBotVideoNativePipeline":
|
| 240 |
+
return cls(
|
| 241 |
+
model_path,
|
| 242 |
+
model_path=model_path,
|
| 243 |
+
server_args=server_args,
|
| 244 |
+
dtype_map=torch_dtype,
|
| 245 |
+
mode=mode,
|
| 246 |
+
transformer_subfolder=transformer_subfolder,
|
| 247 |
+
**kwargs,
|
| 248 |
+
)
|
| 249 |
+
|
| 250 |
+
@torch.no_grad()
|
| 251 |
+
def __call__(
|
| 252 |
+
self,
|
| 253 |
+
*,
|
| 254 |
+
prompt: str,
|
| 255 |
+
negative_prompt: str = DEFAULT_NEGATIVE_PROMPT,
|
| 256 |
+
height: int = 480,
|
| 257 |
+
width: int = 832,
|
| 258 |
+
num_frames: int = 81,
|
| 259 |
+
num_inference_steps: int = 40,
|
| 260 |
+
guidance_scale: float = 3.0,
|
| 261 |
+
shift: float = 3.0,
|
| 262 |
+
generator: torch.Generator | None = None,
|
| 263 |
+
image: Any | None = None,
|
| 264 |
+
output_type: str = "np",
|
| 265 |
+
**kwargs: Any,
|
| 266 |
+
) -> LingBotVideoPipelineOutput:
|
| 267 |
+
call_kwargs = dict(
|
| 268 |
+
prompt=prompt,
|
| 269 |
+
negative_prompt=negative_prompt,
|
| 270 |
+
height=height,
|
| 271 |
+
width=width,
|
| 272 |
+
num_frames=num_frames,
|
| 273 |
+
num_inference_steps=num_inference_steps,
|
| 274 |
+
guidance_scale=guidance_scale,
|
| 275 |
+
shift=shift,
|
| 276 |
+
generator=generator,
|
| 277 |
+
output_type=output_type,
|
| 278 |
+
)
|
| 279 |
+
if image is not None:
|
| 280 |
+
call_kwargs["image"] = image
|
| 281 |
+
call_kwargs.update(kwargs)
|
| 282 |
+
return self.execution_stage(**call_kwargs)
|
| 283 |
+
|
| 284 |
+
def component_dtypes(self) -> dict[str, str | None]:
|
| 285 |
+
return {
|
| 286 |
+
"transformer": _module_dtype_name(self.transformer),
|
| 287 |
+
"text_encoder": _module_dtype_name(self.text_encoder),
|
| 288 |
+
"vae": _module_dtype_name(self.vae),
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
def register_lingbot_native_pipeline() -> bool:
|
| 293 |
+
"""Register the adapter in SGLang's native registry when SGLang is present."""
|
| 294 |
+
|
| 295 |
+
if sglang_registry is None:
|
| 296 |
+
return False
|
| 297 |
+
|
| 298 |
+
try:
|
| 299 |
+
sglang_registry._PIPELINE_REGISTRY[LingBotVideoNativePipeline.pipeline_name] = ( # type: ignore[attr-defined]
|
| 300 |
+
LingBotVideoNativePipeline
|
| 301 |
+
)
|
| 302 |
+
sglang_registry._PIPELINE_CONFIG_REGISTRY[LingBotVideoNativePipeline.pipeline_name] = ( # type: ignore[attr-defined]
|
| 303 |
+
LingBotVideoNativePipelineConfig,
|
| 304 |
+
LingBotVideoNativeSamplingParams,
|
| 305 |
+
)
|
| 306 |
+
except Exception:
|
| 307 |
+
return False
|
| 308 |
+
return True
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def load_lingbot_native_pipeline(
|
| 312 |
+
model_dir: Path,
|
| 313 |
+
dtype_map: dict[str, torch.dtype],
|
| 314 |
+
mode: str = "t2v",
|
| 315 |
+
transformer_subfolder: str = "transformer",
|
| 316 |
+
) -> LingBotVideoNativePipeline:
|
| 317 |
+
"""Load a LingBotVideo native adapter from a diffusers-format model dir."""
|
| 318 |
+
|
| 319 |
+
server_args = None
|
| 320 |
+
if Backend is not None and ServerArgs is not None and set_global_server_args is not None:
|
| 321 |
+
try:
|
| 322 |
+
server_args = ServerArgs(
|
| 323 |
+
model_path=str(model_dir),
|
| 324 |
+
backend=Backend.SGLANG,
|
| 325 |
+
trust_remote_code=True,
|
| 326 |
+
pipeline_class_name=LingBotVideoNativePipeline.pipeline_name,
|
| 327 |
+
pipeline_config=LingBotVideoNativePipelineConfig(),
|
| 328 |
+
dit_cpu_offload=False,
|
| 329 |
+
dit_layerwise_offload=False,
|
| 330 |
+
layerwise_offload_components=[],
|
| 331 |
+
text_encoder_cpu_offload=False,
|
| 332 |
+
image_encoder_cpu_offload=False,
|
| 333 |
+
vae_cpu_offload=False,
|
| 334 |
+
)
|
| 335 |
+
set_global_server_args(server_args)
|
| 336 |
+
except Exception:
|
| 337 |
+
server_args = None
|
| 338 |
+
|
| 339 |
+
register_lingbot_native_pipeline()
|
| 340 |
+
|
| 341 |
+
return LingBotVideoNativePipeline.from_pretrained(
|
| 342 |
+
model_path=model_dir,
|
| 343 |
+
torch_dtype=dtype_map,
|
| 344 |
+
server_args=server_args,
|
| 345 |
+
mode=mode,
|
| 346 |
+
transformer_subfolder=transformer_subfolder,
|
| 347 |
+
)
|
lingbot_video/pipeline_lingbot_video.py
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from contextlib import nullcontext
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
import torch.distributed as dist
|
| 10 |
+
from torchvision.transforms.functional import normalize as normalize_image_tensor
|
| 11 |
+
|
| 12 |
+
from diffusers import DiffusionPipeline
|
| 13 |
+
from diffusers.utils import BaseOutput
|
| 14 |
+
from diffusers.utils.torch_utils import randn_tensor
|
| 15 |
+
|
| 16 |
+
from .utils import (
|
| 17 |
+
LOW_NOISE_TAIL_V1_DEFAULT_STEPS,
|
| 18 |
+
batch_cfg_prompt_inputs,
|
| 19 |
+
compute_refiner_sigmas,
|
| 20 |
+
)
|
| 21 |
+
from .scheduling_flow_unipc import FlowUniPCMultistepScheduler
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
TOKEN_LENGTH = 37698
|
| 25 |
+
HIDDEN_STATE_SKIP_LAYER = 0
|
| 26 |
+
|
| 27 |
+
PROMPT_TEMPLATE = (
|
| 28 |
+
"<|im_start|>system\nGiven a user input that may include a text prompt alone, "
|
| 29 |
+
"a text prompt with an image reference, or a text prompt with a video reference "
|
| 30 |
+
"or a video reference alone, generate an \"Enhanced prompt\" that provides detailed "
|
| 31 |
+
"visual descriptions suitable for video generation. Evaluate the level of detail "
|
| 32 |
+
"in the user's input: if it is simple, enrich it by adding specifics about colors, "
|
| 33 |
+
"shapes, sizes, textures, lighting, motion dynamics, camera movement, temporal "
|
| 34 |
+
"progression, and spatial relationships to create vivid, concrete, and temporally "
|
| 35 |
+
"coherent scenes to create vivid and concrete scenes. Please generate only the "
|
| 36 |
+
"enhanced description for the prompt below and avoid including any additional "
|
| 37 |
+
"commentary or evaluations:<|im_end|>\n<|im_start|>user\n{}<|im_end|>\n"
|
| 38 |
+
"<|im_start|>assistant\n"
|
| 39 |
+
)
|
| 40 |
+
IMG_PROMPT_TEMPLATE = "<|vision_start|><|image_pad|><|vision_end|>"
|
| 41 |
+
VIDEO_PROMPT_TEMPLATE = "<|vision_start|><|video_pad|><|vision_end|>"
|
| 42 |
+
|
| 43 |
+
DEFAULT_NEGATIVE_PROMPT = (
|
| 44 |
+
'{"universal_negative": {"visual_quality": ["low quality", "worst quality", "blurry", "pixelated", "jpeg artifacts", "low resolution", "unstable color", "color flicker", "underexposed", "overexposed", "invisible subject", "subject hidden in darkness"], "artistic_style": ["painting", "illustration", "drawing", "cartoon", "3d render", "cgi", "sketch", "digital art"], "composition_and_content": ["text", "watermark", "signature", "logo", "subtitles", "pillarboxed", "side bars", "portrait image in landscape frame"], "temporal_and_motion_stability": ["flickering", "jittery", "motion blur", "temporal inconsistency", "warping", "morphing", "incoherent motion", "unnatural movement", "static object with sudden jump", "frame-to-frame inconsistency"], "material_and_structure": ["plastic-like glass", "unrealistic texture", "deformed bottle", "liquid freezing improperly", "distorted reflections"]}}'
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
# Still-image default (t2i): drops the whole temporal/motion block and the video-only
|
| 48 |
+
# codec/temporal terms that cannot apply to a single frame.
|
| 49 |
+
DEFAULT_NEGATIVE_PROMPT_IMAGE = (
|
| 50 |
+
'{"universal_negative": {"visual_quality": ["low quality", "worst quality", "blurry", "pixelated", "jpeg artifacts", "low resolution", "underexposed", "overexposed", "invisible subject", "subject hidden in darkness"], "artistic_style": ["painting", "illustration", "drawing", "cartoon", "3d render", "cgi", "sketch", "digital art"], "composition_and_content": ["text", "watermark", "signature", "logo", "pillarboxed", "side bars", "portrait image in landscape frame"], "material_and_structure": ["plastic-like glass", "unrealistic texture", "deformed bottle", "distorted reflections"]}}'
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
@dataclass
|
| 55 |
+
class LingBotVideoPipelineOutput(BaseOutput):
|
| 56 |
+
frames: Union[List[np.ndarray], torch.Tensor]
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _module_dtype(module: torch.nn.Module) -> torch.dtype:
|
| 60 |
+
try:
|
| 61 |
+
return next(module.parameters()).dtype
|
| 62 |
+
except StopIteration:
|
| 63 |
+
return torch.float32
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _transformer_timestep(timestep: torch.Tensor, transformer_dtype: torch.dtype) -> torch.Tensor:
|
| 67 |
+
sigma = timestep.float() / 1000.0
|
| 68 |
+
if transformer_dtype in {torch.bfloat16, torch.float16}:
|
| 69 |
+
sigma = sigma.to(transformer_dtype)
|
| 70 |
+
return (sigma * 1000.0).float()
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _transformer_autocast(device: torch.device, transformer_dtype: torch.dtype):
|
| 74 |
+
if device.type != "cuda" or transformer_dtype not in {torch.bfloat16, torch.float16}:
|
| 75 |
+
return nullcontext()
|
| 76 |
+
return torch.autocast(device_type="cuda", dtype=transformer_dtype)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _module_device(module: torch.nn.Module) -> torch.device:
|
| 80 |
+
try:
|
| 81 |
+
return next(module.parameters()).device
|
| 82 |
+
except StopIteration:
|
| 83 |
+
return torch.device("cpu")
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _group_global_rank(group: Optional[Any], group_rank: int) -> int:
|
| 87 |
+
if group is None:
|
| 88 |
+
return group_rank
|
| 89 |
+
get_global_rank = getattr(dist, "get_global_rank", None)
|
| 90 |
+
if get_global_rank is None:
|
| 91 |
+
return group_rank
|
| 92 |
+
return int(get_global_rank(group, group_rank))
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class LingBotVideoPipeline(DiffusionPipeline):
|
| 96 |
+
"""Minimal LingBotVideo t2v/t2i pipeline.
|
| 97 |
+
|
| 98 |
+
Standard CFG runs as two independent transformer forwards unless batched CFG
|
| 99 |
+
or CFG parallelism is explicitly requested.
|
| 100 |
+
"""
|
| 101 |
+
|
| 102 |
+
model_cpu_offload_seq = "text_encoder->transformer->vae"
|
| 103 |
+
|
| 104 |
+
def __init__(self, transformer, vae, text_encoder, processor, scheduler):
|
| 105 |
+
super().__init__()
|
| 106 |
+
if (
|
| 107 |
+
scheduler is not None
|
| 108 |
+
and scheduler.__class__.__name__ != FlowUniPCMultistepScheduler.__name__
|
| 109 |
+
):
|
| 110 |
+
raise TypeError(
|
| 111 |
+
"LingBotVideoPipeline requires vendored FlowUniPCMultistepScheduler; "
|
| 112 |
+
f"got {scheduler.__class__.__name__}."
|
| 113 |
+
)
|
| 114 |
+
self.register_modules(
|
| 115 |
+
transformer=transformer,
|
| 116 |
+
vae=vae,
|
| 117 |
+
text_encoder=text_encoder,
|
| 118 |
+
processor=processor,
|
| 119 |
+
scheduler=scheduler,
|
| 120 |
+
)
|
| 121 |
+
self.vae_scale_factor_temporal = 4
|
| 122 |
+
self.vae_scale_factor_spatial = 8
|
| 123 |
+
self.token_length = TOKEN_LENGTH
|
| 124 |
+
self.hidden_state_skip_layer = HIDDEN_STATE_SKIP_LAYER
|
| 125 |
+
self.prompt_template = PROMPT_TEMPLATE
|
| 126 |
+
self.img_prompt_template = IMG_PROMPT_TEMPLATE
|
| 127 |
+
self.video_prompt_template = VIDEO_PROMPT_TEMPLATE
|
| 128 |
+
self._crop_start: Optional[int] = None
|
| 129 |
+
|
| 130 |
+
@staticmethod
|
| 131 |
+
def check_inputs(height: int, width: int, num_frames: int) -> None:
|
| 132 |
+
if num_frames != 1 and (num_frames - 1) % 4 != 0:
|
| 133 |
+
raise ValueError(f"`num_frames` must be 1 or 4n+1, got {num_frames}.")
|
| 134 |
+
if height % 16 != 0 or width % 16 != 0:
|
| 135 |
+
raise ValueError(f"`height` and `width` must be multiples of 16, got {height}x{width}.")
|
| 136 |
+
|
| 137 |
+
@staticmethod
|
| 138 |
+
def _apply_inpainting(latents: torch.Tensor, cond_latent: torch.Tensor) -> torch.Tensor:
|
| 139 |
+
cond_t = cond_latent.shape[2]
|
| 140 |
+
latents[:, :, :cond_t] = cond_latent.float()
|
| 141 |
+
return latents
|
| 142 |
+
|
| 143 |
+
@staticmethod
|
| 144 |
+
def apply_text_to_template(text: str, template: str = PROMPT_TEMPLATE) -> str:
|
| 145 |
+
return template.format(text)
|
| 146 |
+
|
| 147 |
+
def _compute_crop_start(self) -> int:
|
| 148 |
+
if self._crop_start is None:
|
| 149 |
+
marker = "<|USER_INPUT_MARKER|>"
|
| 150 |
+
marked = self.prompt_template.format(marker)
|
| 151 |
+
marker_pos = marked.find(marker)
|
| 152 |
+
if marker_pos < 0:
|
| 153 |
+
self._crop_start = 0
|
| 154 |
+
else:
|
| 155 |
+
prefix = self.processor(
|
| 156 |
+
text=marked[:marker_pos],
|
| 157 |
+
images=None,
|
| 158 |
+
videos=None,
|
| 159 |
+
return_tensors="pt",
|
| 160 |
+
)
|
| 161 |
+
self._crop_start = int(prefix["input_ids"].shape[1])
|
| 162 |
+
return self._crop_start
|
| 163 |
+
|
| 164 |
+
def _build_prompt_inputs(
|
| 165 |
+
self,
|
| 166 |
+
prompt: Union[str, List[str]],
|
| 167 |
+
images: Optional[Any] = None,
|
| 168 |
+
videos: Optional[Any] = None,
|
| 169 |
+
video_metadata: Optional[Any] = None,
|
| 170 |
+
video_kwargs: Optional[Dict[str, Any]] = None,
|
| 171 |
+
):
|
| 172 |
+
if isinstance(prompt, str):
|
| 173 |
+
prompts = [prompt]
|
| 174 |
+
else:
|
| 175 |
+
prompts = list(prompt)
|
| 176 |
+
|
| 177 |
+
visual_template = ""
|
| 178 |
+
if images is not None:
|
| 179 |
+
visual_template = self.img_prompt_template
|
| 180 |
+
elif videos is not None:
|
| 181 |
+
visual_template = self.video_prompt_template
|
| 182 |
+
|
| 183 |
+
texts = [
|
| 184 |
+
self.apply_text_to_template(visual_template + text, self.prompt_template)
|
| 185 |
+
for text in prompts
|
| 186 |
+
]
|
| 187 |
+
kwargs = dict(video_kwargs or {})
|
| 188 |
+
return self.processor(
|
| 189 |
+
text=texts,
|
| 190 |
+
images=images,
|
| 191 |
+
videos=videos,
|
| 192 |
+
video_metadata=video_metadata,
|
| 193 |
+
do_resize=False,
|
| 194 |
+
truncation=True,
|
| 195 |
+
max_length=self.token_length,
|
| 196 |
+
padding="longest",
|
| 197 |
+
return_tensors="pt",
|
| 198 |
+
**kwargs,
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
@torch.no_grad()
|
| 202 |
+
def encode_prompt(
|
| 203 |
+
self,
|
| 204 |
+
prompt: Union[str, List[str]],
|
| 205 |
+
*,
|
| 206 |
+
images: Optional[Any] = None,
|
| 207 |
+
videos: Optional[Any] = None,
|
| 208 |
+
video_metadata: Optional[Any] = None,
|
| 209 |
+
video_kwargs: Optional[Dict[str, Any]] = None,
|
| 210 |
+
device: Optional[Union[str, torch.device]] = None,
|
| 211 |
+
return_inputs: bool = False,
|
| 212 |
+
):
|
| 213 |
+
if self.text_encoder is None or self.processor is None:
|
| 214 |
+
raise ValueError("`text_encoder` and `processor` are required for encode_prompt().")
|
| 215 |
+
|
| 216 |
+
device = torch.device(device) if device is not None else self._execution_device
|
| 217 |
+
inputs = self._build_prompt_inputs(
|
| 218 |
+
prompt,
|
| 219 |
+
images=images,
|
| 220 |
+
videos=videos,
|
| 221 |
+
video_metadata=video_metadata,
|
| 222 |
+
video_kwargs=video_kwargs,
|
| 223 |
+
)
|
| 224 |
+
inputs = inputs.to(device)
|
| 225 |
+
outputs = self.text_encoder(
|
| 226 |
+
**inputs,
|
| 227 |
+
output_hidden_states=self.hidden_state_skip_layer is not None,
|
| 228 |
+
)
|
| 229 |
+
if self.hidden_state_skip_layer is not None:
|
| 230 |
+
prompt_embeds = outputs.hidden_states[-(self.hidden_state_skip_layer + 1)]
|
| 231 |
+
else:
|
| 232 |
+
prompt_embeds = outputs.last_hidden_state
|
| 233 |
+
|
| 234 |
+
prompt_mask = inputs["attention_mask"]
|
| 235 |
+
crop_start = self._compute_crop_start()
|
| 236 |
+
if crop_start > 0:
|
| 237 |
+
prompt_embeds = prompt_embeds[:, crop_start:]
|
| 238 |
+
prompt_mask = prompt_mask[:, crop_start:]
|
| 239 |
+
|
| 240 |
+
# Batch=1 can drop right padding before DiT inference.
|
| 241 |
+
if prompt_embeds.shape[0] == 1:
|
| 242 |
+
true_len = int(prompt_mask[0].sum().item())
|
| 243 |
+
prompt_embeds = prompt_embeds[:, :true_len]
|
| 244 |
+
prompt_mask = prompt_mask[:, :true_len]
|
| 245 |
+
|
| 246 |
+
if return_inputs:
|
| 247 |
+
return prompt_embeds, prompt_mask, inputs
|
| 248 |
+
return prompt_embeds, prompt_mask
|
| 249 |
+
|
| 250 |
+
def prepare_latents(
|
| 251 |
+
self,
|
| 252 |
+
num_frames: int,
|
| 253 |
+
height: int,
|
| 254 |
+
width: int,
|
| 255 |
+
generator: Optional[torch.Generator],
|
| 256 |
+
latents: Optional[torch.Tensor],
|
| 257 |
+
device: torch.device,
|
| 258 |
+
) -> torch.Tensor:
|
| 259 |
+
latent_frames = (num_frames - 1) // self.vae_scale_factor_temporal + 1
|
| 260 |
+
latent_height = height // self.vae_scale_factor_spatial
|
| 261 |
+
latent_width = width // self.vae_scale_factor_spatial
|
| 262 |
+
shape = (
|
| 263 |
+
1,
|
| 264 |
+
self.transformer.config.in_channels,
|
| 265 |
+
latent_frames,
|
| 266 |
+
latent_height,
|
| 267 |
+
latent_width,
|
| 268 |
+
)
|
| 269 |
+
if latents is None:
|
| 270 |
+
return randn_tensor(shape, generator=generator, device=device, dtype=torch.float32)
|
| 271 |
+
if tuple(latents.shape) != shape:
|
| 272 |
+
raise ValueError(f"`latents` shape must be {shape}, got {tuple(latents.shape)}.")
|
| 273 |
+
return latents.to(device=device, dtype=torch.float32)
|
| 274 |
+
|
| 275 |
+
def _dit_latent_to_vae(self, latents: torch.Tensor) -> torch.Tensor:
|
| 276 |
+
mean = torch.tensor(self.vae.config.latents_mean, device=latents.device, dtype=torch.float32)
|
| 277 |
+
std_inv = 1.0 / torch.tensor(
|
| 278 |
+
self.vae.config.latents_std, device=latents.device, dtype=torch.float32
|
| 279 |
+
)
|
| 280 |
+
mean = mean.view(1, -1, 1, 1, 1)
|
| 281 |
+
std_inv = std_inv.view(1, -1, 1, 1, 1)
|
| 282 |
+
return latents.float() / std_inv + mean
|
| 283 |
+
|
| 284 |
+
def _vae_latent_to_dit(self, latents: torch.Tensor) -> torch.Tensor:
|
| 285 |
+
mean = torch.tensor(self.vae.config.latents_mean, device=latents.device, dtype=torch.float32)
|
| 286 |
+
std_inv = 1.0 / torch.tensor(
|
| 287 |
+
self.vae.config.latents_std, device=latents.device, dtype=torch.float32
|
| 288 |
+
)
|
| 289 |
+
mean = mean.view(1, -1, 1, 1, 1)
|
| 290 |
+
std_inv = std_inv.view(1, -1, 1, 1, 1)
|
| 291 |
+
return (latents.float() - mean) * std_inv
|
| 292 |
+
|
| 293 |
+
@torch.no_grad()
|
| 294 |
+
def encode_video_latent(
|
| 295 |
+
self,
|
| 296 |
+
video: torch.Tensor,
|
| 297 |
+
generator: Optional[torch.Generator] = None,
|
| 298 |
+
) -> torch.Tensor:
|
| 299 |
+
if self.vae is None:
|
| 300 |
+
raise ValueError("`vae` is required to encode video latents.")
|
| 301 |
+
vae_device = _module_device(self.vae)
|
| 302 |
+
video = video.to(device=vae_device, dtype=torch.float32)
|
| 303 |
+
bsz, channels, frames, height, width = video.shape
|
| 304 |
+
flat_video = video.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channels, height, width)
|
| 305 |
+
norm_flat_video = normalize_image_tensor(
|
| 306 |
+
flat_video,
|
| 307 |
+
[0.5, 0.5, 0.5],
|
| 308 |
+
[0.5, 0.5, 0.5],
|
| 309 |
+
inplace=False,
|
| 310 |
+
)
|
| 311 |
+
norm_video = (
|
| 312 |
+
norm_flat_video.reshape(bsz, frames, channels, height, width)
|
| 313 |
+
.permute(0, 2, 1, 3, 4)
|
| 314 |
+
.contiguous()
|
| 315 |
+
)
|
| 316 |
+
with torch.autocast(
|
| 317 |
+
"cuda",
|
| 318 |
+
dtype=torch.bfloat16,
|
| 319 |
+
enabled=vae_device.type == "cuda",
|
| 320 |
+
):
|
| 321 |
+
encoded = self.vae.encode(norm_video)
|
| 322 |
+
if hasattr(encoded, "latent_dist"):
|
| 323 |
+
latents = encoded.latent_dist.sample(generator)
|
| 324 |
+
else:
|
| 325 |
+
latents = encoded[0] if isinstance(encoded, tuple) else encoded
|
| 326 |
+
return self._vae_latent_to_dit(latents).to(latents)
|
| 327 |
+
|
| 328 |
+
@torch.no_grad()
|
| 329 |
+
def _decode_latents(
|
| 330 |
+
self,
|
| 331 |
+
latents: torch.Tensor,
|
| 332 |
+
) -> List[np.ndarray]:
|
| 333 |
+
vae_device = _module_device(self.vae)
|
| 334 |
+
vae_dtype = _module_dtype(self.vae)
|
| 335 |
+
vae_latents = self._dit_latent_to_vae(latents).to(device=vae_device, dtype=torch.float32)
|
| 336 |
+
if vae_latents.ndim == 5:
|
| 337 |
+
vae_latents = vae_latents.contiguous(memory_format=torch.channels_last_3d)
|
| 338 |
+
autocast_dtype = (
|
| 339 |
+
vae_dtype
|
| 340 |
+
if vae_device.type == "cuda" and vae_dtype in {torch.bfloat16, torch.float16}
|
| 341 |
+
else None
|
| 342 |
+
)
|
| 343 |
+
with torch.autocast(
|
| 344 |
+
"cuda",
|
| 345 |
+
dtype=autocast_dtype or torch.bfloat16,
|
| 346 |
+
enabled=autocast_dtype is not None,
|
| 347 |
+
):
|
| 348 |
+
decoded = self.vae.decode(vae_latents)
|
| 349 |
+
frames = decoded[0] if isinstance(decoded, tuple) else decoded.sample
|
| 350 |
+
frames = frames.float().clamp_(-1, 1)
|
| 351 |
+
frames = (frames + 1.0) / 2.0
|
| 352 |
+
frames = frames.permute(0, 2, 3, 4, 1).cpu().numpy()
|
| 353 |
+
return [video for video in frames]
|
| 354 |
+
|
| 355 |
+
@torch.no_grad()
|
| 356 |
+
def __call__(
|
| 357 |
+
self,
|
| 358 |
+
prompt: str,
|
| 359 |
+
negative_prompt: str = DEFAULT_NEGATIVE_PROMPT,
|
| 360 |
+
height: int = 480,
|
| 361 |
+
width: int = 480,
|
| 362 |
+
num_frames: int = 81,
|
| 363 |
+
num_inference_steps: int = 40,
|
| 364 |
+
guidance_scale: float = 6.0,
|
| 365 |
+
shift: float = 3.0,
|
| 366 |
+
generator: Optional[torch.Generator] = None,
|
| 367 |
+
latents: Optional[torch.Tensor] = None,
|
| 368 |
+
cond_latent: Optional[torch.Tensor] = None,
|
| 369 |
+
prompt_embeds: Optional[torch.Tensor] = None,
|
| 370 |
+
prompt_mask: Optional[torch.Tensor] = None,
|
| 371 |
+
negative_prompt_embeds: Optional[torch.Tensor] = None,
|
| 372 |
+
negative_prompt_mask: Optional[torch.Tensor] = None,
|
| 373 |
+
output_type: str = "np",
|
| 374 |
+
cfg_parallel_group: Optional[Any] = None,
|
| 375 |
+
batch_cfg: bool = False,
|
| 376 |
+
null_cond_clone_zero: bool = False,
|
| 377 |
+
t_thresh: Optional[float] = None,
|
| 378 |
+
refiner_sigma_tail_steps: int = LOW_NOISE_TAIL_V1_DEFAULT_STEPS,
|
| 379 |
+
offload_vae_during_denoise: bool = False,
|
| 380 |
+
return_dict: bool = True,
|
| 381 |
+
) -> Union[LingBotVideoPipelineOutput, Tuple[Union[List[np.ndarray], torch.Tensor]]]:
|
| 382 |
+
self.check_inputs(height, width, num_frames)
|
| 383 |
+
if self.transformer is None or self.scheduler is None:
|
| 384 |
+
raise ValueError("`transformer` and `scheduler` are required for generation.")
|
| 385 |
+
|
| 386 |
+
device = self._execution_device
|
| 387 |
+
do_cfg = guidance_scale > 1.0
|
| 388 |
+
requested_batch_cfg = bool(batch_cfg)
|
| 389 |
+
effective_batch_cfg = requested_batch_cfg
|
| 390 |
+
batch_cfg_fallback_reason = None
|
| 391 |
+
self._last_batch_cfg_requested = requested_batch_cfg
|
| 392 |
+
self._last_effective_batch_cfg = bool(effective_batch_cfg and do_cfg)
|
| 393 |
+
self._last_batch_cfg_fallback_reason = batch_cfg_fallback_reason
|
| 394 |
+
|
| 395 |
+
cfg_parallel = cfg_parallel_group is not None
|
| 396 |
+
if cfg_parallel and effective_batch_cfg:
|
| 397 |
+
raise ValueError("`cfg_parallel_group` and `batch_cfg` are mutually exclusive.")
|
| 398 |
+
cfg_parallel_rank = 0
|
| 399 |
+
cfg_parallel_world_size = 1
|
| 400 |
+
if cfg_parallel:
|
| 401 |
+
if not dist.is_available() or not dist.is_initialized():
|
| 402 |
+
raise ValueError("`cfg_parallel_group` requires an initialized process group.")
|
| 403 |
+
if not do_cfg:
|
| 404 |
+
raise ValueError("CFG parallel requires `guidance_scale > 1.0`.")
|
| 405 |
+
cfg_parallel_rank = dist.get_rank(cfg_parallel_group)
|
| 406 |
+
cfg_parallel_world_size = dist.get_world_size(cfg_parallel_group)
|
| 407 |
+
if cfg_parallel_world_size != 2:
|
| 408 |
+
raise ValueError(
|
| 409 |
+
f"CFG parallel currently requires exactly 2 ranks, got {cfg_parallel_world_size}."
|
| 410 |
+
)
|
| 411 |
+
|
| 412 |
+
if prompt_embeds is not None:
|
| 413 |
+
if prompt_mask is None:
|
| 414 |
+
raise ValueError("`prompt_mask` is required when `prompt_embeds` is provided.")
|
| 415 |
+
prompt_embeds = prompt_embeds.to(device=device)
|
| 416 |
+
prompt_mask = prompt_mask.to(device=device)
|
| 417 |
+
if negative_prompt_embeds is not None:
|
| 418 |
+
if negative_prompt_mask is None:
|
| 419 |
+
raise ValueError(
|
| 420 |
+
"`negative_prompt_mask` is required when `negative_prompt_embeds` is provided."
|
| 421 |
+
)
|
| 422 |
+
negative_prompt_embeds = negative_prompt_embeds.to(device=device)
|
| 423 |
+
negative_prompt_mask = negative_prompt_mask.to(device=device)
|
| 424 |
+
|
| 425 |
+
if cfg_parallel and cfg_parallel_rank == 1:
|
| 426 |
+
if negative_prompt_embeds is not None:
|
| 427 |
+
negative_embeds, negative_mask = negative_prompt_embeds, negative_prompt_mask
|
| 428 |
+
else:
|
| 429 |
+
negative_embeds, negative_mask = self.encode_prompt(negative_prompt, device=device)
|
| 430 |
+
prompt_embeds = prompt_mask = None
|
| 431 |
+
else:
|
| 432 |
+
if prompt_embeds is None:
|
| 433 |
+
prompt_embeds, prompt_mask = self.encode_prompt(prompt, device=device)
|
| 434 |
+
if do_cfg and not cfg_parallel:
|
| 435 |
+
if null_cond_clone_zero:
|
| 436 |
+
negative_embeds = torch.zeros_like(prompt_embeds)
|
| 437 |
+
negative_mask = prompt_mask.clone()
|
| 438 |
+
elif negative_prompt_embeds is not None:
|
| 439 |
+
negative_embeds, negative_mask = negative_prompt_embeds, negative_prompt_mask
|
| 440 |
+
else:
|
| 441 |
+
negative_embeds, negative_mask = self.encode_prompt(negative_prompt, device=device)
|
| 442 |
+
|
| 443 |
+
latents = self.prepare_latents(num_frames, height, width, generator, latents, device)
|
| 444 |
+
# Clean temporal-prefix condition (e.g. the ti2v refiner's first-frame
|
| 445 |
+
# latent): written into the latent before sampling and after every
|
| 446 |
+
# scheduler step, so the fixed frames stay clean while the rest denoise
|
| 447 |
+
# against them through attention.
|
| 448 |
+
if cond_latent is not None:
|
| 449 |
+
cond_latent = cond_latent.to(device=device, dtype=torch.float32)
|
| 450 |
+
latents = self._apply_inpainting(latents, cond_latent)
|
| 451 |
+
sigmas = compute_refiner_sigmas(
|
| 452 |
+
sigma_max=float(self.scheduler.sigma_max),
|
| 453 |
+
sigma_min=float(self.scheduler.sigma_min),
|
| 454 |
+
num_inference_steps=num_inference_steps,
|
| 455 |
+
shift=shift,
|
| 456 |
+
t_thresh=t_thresh,
|
| 457 |
+
tail_steps=refiner_sigma_tail_steps,
|
| 458 |
+
)
|
| 459 |
+
if sigmas is None:
|
| 460 |
+
self.scheduler.set_timesteps(num_inference_steps, device=device, shift=shift)
|
| 461 |
+
else:
|
| 462 |
+
self.scheduler.set_timesteps(
|
| 463 |
+
int(sigmas.shape[0]),
|
| 464 |
+
device=device,
|
| 465 |
+
sigmas=sigmas,
|
| 466 |
+
shift=1.0,
|
| 467 |
+
)
|
| 468 |
+
transformer_dtype = _module_dtype(self.transformer)
|
| 469 |
+
vae_restore_device: Optional[torch.device] = None
|
| 470 |
+
vae_offloaded = False
|
| 471 |
+
if offload_vae_during_denoise and output_type == "np" and self.vae is not None:
|
| 472 |
+
vae_device = _module_device(self.vae)
|
| 473 |
+
if vae_device.type == "cuda":
|
| 474 |
+
self.vae.to("cpu")
|
| 475 |
+
torch.cuda.empty_cache()
|
| 476 |
+
vae_restore_device = vae_device
|
| 477 |
+
vae_offloaded = True
|
| 478 |
+
cfg_latent_src = _group_global_rank(cfg_parallel_group, 0)
|
| 479 |
+
cfg_uncond_src = _group_global_rank(cfg_parallel_group, 1)
|
| 480 |
+
for i, timestep in enumerate(self.progress_bar(self.scheduler.timesteps)):
|
| 481 |
+
if cfg_parallel:
|
| 482 |
+
dist.broadcast(latents, src=cfg_latent_src, group=cfg_parallel_group)
|
| 483 |
+
timestep_batch = _transformer_timestep(timestep, transformer_dtype).expand(1).to(device)
|
| 484 |
+
latent_model_input = latents
|
| 485 |
+
if cfg_parallel:
|
| 486 |
+
if cfg_parallel_rank == 0:
|
| 487 |
+
branch_embeds = prompt_embeds
|
| 488 |
+
branch_mask = prompt_mask
|
| 489 |
+
branch_name = "transformer.cond"
|
| 490 |
+
else:
|
| 491 |
+
branch_embeds = negative_embeds
|
| 492 |
+
branch_mask = negative_mask
|
| 493 |
+
branch_name = "transformer.uncond"
|
| 494 |
+
branch_model_input = branch_embeds.to(transformer_dtype)
|
| 495 |
+
with _transformer_autocast(device, transformer_dtype):
|
| 496 |
+
branch_noise_pred = self.transformer(
|
| 497 |
+
latent_model_input,
|
| 498 |
+
timestep_batch,
|
| 499 |
+
branch_model_input,
|
| 500 |
+
encoder_attention_mask=branch_mask,
|
| 501 |
+
return_dict=False,
|
| 502 |
+
)[0].float()
|
| 503 |
+
if cfg_parallel_rank == 0:
|
| 504 |
+
noise_pred = branch_noise_pred
|
| 505 |
+
noise_pred_uncond = torch.empty_like(noise_pred)
|
| 506 |
+
else:
|
| 507 |
+
noise_pred_uncond = branch_noise_pred
|
| 508 |
+
dist.broadcast(noise_pred_uncond, src=cfg_uncond_src, group=cfg_parallel_group)
|
| 509 |
+
if cfg_parallel_rank != 0:
|
| 510 |
+
continue
|
| 511 |
+
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred - noise_pred_uncond)
|
| 512 |
+
else:
|
| 513 |
+
prompt_model_input = prompt_embeds.to(transformer_dtype)
|
| 514 |
+
if do_cfg and effective_batch_cfg:
|
| 515 |
+
negative_model_input = negative_embeds.to(transformer_dtype)
|
| 516 |
+
cfg_embeds, cfg_mask = batch_cfg_prompt_inputs(
|
| 517 |
+
prompt_model_input,
|
| 518 |
+
prompt_mask,
|
| 519 |
+
negative_model_input,
|
| 520 |
+
negative_mask,
|
| 521 |
+
null_cond_clone_zero=False,
|
| 522 |
+
)
|
| 523 |
+
cfg_latents = torch.cat([latent_model_input, latent_model_input], dim=0)
|
| 524 |
+
cfg_timesteps = torch.cat([timestep_batch, timestep_batch], dim=0)
|
| 525 |
+
with _transformer_autocast(device, transformer_dtype):
|
| 526 |
+
noise_batched = self.transformer(
|
| 527 |
+
cfg_latents,
|
| 528 |
+
cfg_timesteps,
|
| 529 |
+
cfg_embeds,
|
| 530 |
+
encoder_attention_mask=cfg_mask,
|
| 531 |
+
return_dict=False,
|
| 532 |
+
)[0].float()
|
| 533 |
+
noise_pred, noise_pred_uncond = noise_batched.chunk(2, dim=0)
|
| 534 |
+
noise_pred = noise_pred_uncond + guidance_scale * (
|
| 535 |
+
noise_pred - noise_pred_uncond
|
| 536 |
+
)
|
| 537 |
+
else:
|
| 538 |
+
with _transformer_autocast(device, transformer_dtype):
|
| 539 |
+
noise_pred = self.transformer(
|
| 540 |
+
latent_model_input,
|
| 541 |
+
timestep_batch,
|
| 542 |
+
prompt_model_input,
|
| 543 |
+
encoder_attention_mask=prompt_mask,
|
| 544 |
+
return_dict=False,
|
| 545 |
+
)[0].float()
|
| 546 |
+
|
| 547 |
+
if do_cfg and not effective_batch_cfg:
|
| 548 |
+
negative_model_input = negative_embeds.to(transformer_dtype)
|
| 549 |
+
with _transformer_autocast(device, transformer_dtype):
|
| 550 |
+
noise_pred_uncond = self.transformer(
|
| 551 |
+
latent_model_input,
|
| 552 |
+
timestep_batch,
|
| 553 |
+
negative_model_input,
|
| 554 |
+
encoder_attention_mask=negative_mask,
|
| 555 |
+
return_dict=False,
|
| 556 |
+
)[0].float()
|
| 557 |
+
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred - noise_pred_uncond)
|
| 558 |
+
|
| 559 |
+
latents = self.scheduler.step(
|
| 560 |
+
noise_pred,
|
| 561 |
+
timestep,
|
| 562 |
+
latents,
|
| 563 |
+
return_dict=False,
|
| 564 |
+
generator=generator,
|
| 565 |
+
)[0]
|
| 566 |
+
if cond_latent is not None:
|
| 567 |
+
latents = self._apply_inpainting(latents, cond_latent)
|
| 568 |
+
|
| 569 |
+
if cfg_parallel:
|
| 570 |
+
dist.barrier(group=cfg_parallel_group)
|
| 571 |
+
if cfg_parallel_rank != 0:
|
| 572 |
+
frames = latents if output_type == "latent" else []
|
| 573 |
+
self.maybe_free_model_hooks()
|
| 574 |
+
if not return_dict:
|
| 575 |
+
return (frames,)
|
| 576 |
+
return LingBotVideoPipelineOutput(frames=frames)
|
| 577 |
+
|
| 578 |
+
if output_type == "latent":
|
| 579 |
+
frames = latents
|
| 580 |
+
elif output_type == "np":
|
| 581 |
+
if vae_offloaded and vae_restore_device is not None:
|
| 582 |
+
self.vae.to(device=vae_restore_device)
|
| 583 |
+
torch.cuda.empty_cache()
|
| 584 |
+
frames = self._decode_latents(latents)
|
| 585 |
+
else:
|
| 586 |
+
raise ValueError(f"Unsupported output_type: {output_type}")
|
| 587 |
+
|
| 588 |
+
self.maybe_free_model_hooks()
|
| 589 |
+
if not return_dict:
|
| 590 |
+
return (frames,)
|
| 591 |
+
return LingBotVideoPipelineOutput(frames=frames)
|
lingbot_video/pipeline_lingbot_video_i2v.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
from typing import Any, Optional, Tuple, Union
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
import torch.distributed as dist
|
| 9 |
+
import torch.nn.functional as F
|
| 10 |
+
from PIL import Image
|
| 11 |
+
from diffusers.utils.torch_utils import randn_tensor
|
| 12 |
+
|
| 13 |
+
from .pipeline_lingbot_video import (
|
| 14 |
+
DEFAULT_NEGATIVE_PROMPT,
|
| 15 |
+
LingBotVideoPipeline,
|
| 16 |
+
LingBotVideoPipelineOutput,
|
| 17 |
+
_group_global_rank,
|
| 18 |
+
_module_device,
|
| 19 |
+
_module_dtype,
|
| 20 |
+
_transformer_autocast,
|
| 21 |
+
_transformer_timestep,
|
| 22 |
+
)
|
| 23 |
+
from .utils import batch_cfg_prompt_inputs
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
IMAGE_MIN_TOKEN_NUM = 4
|
| 27 |
+
IMAGE_MAX_TOKEN_NUM = 16384
|
| 28 |
+
MAX_RATIO = 200
|
| 29 |
+
SPATIAL_MERGE_SIZE = 2
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _round_by_factor(number: float, factor: int) -> int:
|
| 33 |
+
return round(number / factor) * factor
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def _ceil_by_factor(number: float, factor: int) -> int:
|
| 37 |
+
return math.ceil(number / factor) * factor
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _floor_by_factor(number: float, factor: int) -> int:
|
| 41 |
+
return math.floor(number / factor) * factor
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def smart_resize(
|
| 45 |
+
height: int,
|
| 46 |
+
width: int,
|
| 47 |
+
factor: int,
|
| 48 |
+
min_pixels: Optional[int] = None,
|
| 49 |
+
max_pixels: Optional[int] = None,
|
| 50 |
+
) -> Tuple[int, int]:
|
| 51 |
+
max_pixels = max_pixels if max_pixels is not None else IMAGE_MAX_TOKEN_NUM * factor**2
|
| 52 |
+
min_pixels = min_pixels if min_pixels is not None else IMAGE_MIN_TOKEN_NUM * factor**2
|
| 53 |
+
if max_pixels < min_pixels:
|
| 54 |
+
raise ValueError("max_pixels must be greater than or equal to min_pixels.")
|
| 55 |
+
if max(height, width) / min(height, width) > MAX_RATIO:
|
| 56 |
+
raise ValueError(f"absolute aspect ratio must be smaller than {MAX_RATIO}.")
|
| 57 |
+
|
| 58 |
+
resized_height = max(factor, _round_by_factor(height, factor))
|
| 59 |
+
resized_width = max(factor, _round_by_factor(width, factor))
|
| 60 |
+
if resized_height * resized_width > max_pixels:
|
| 61 |
+
beta = math.sqrt((height * width) / max_pixels)
|
| 62 |
+
resized_height = _floor_by_factor(height / beta, factor)
|
| 63 |
+
resized_width = _floor_by_factor(width / beta, factor)
|
| 64 |
+
elif resized_height * resized_width < min_pixels:
|
| 65 |
+
beta = math.sqrt(min_pixels / (height * width))
|
| 66 |
+
resized_height = _ceil_by_factor(height * beta, factor)
|
| 67 |
+
resized_width = _ceil_by_factor(width * beta, factor)
|
| 68 |
+
return resized_height, resized_width
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _pixel_tensor_to_pil(pixel: torch.Tensor) -> Image.Image:
|
| 72 |
+
"""Match torchvision.transforms.ToPILImage for a float CHW image in [0, 1]."""
|
| 73 |
+
frame = pixel[0, :, 0].detach().cpu().clamp(0, 1)
|
| 74 |
+
array = frame.permute(1, 2, 0).mul(255).byte().numpy()
|
| 75 |
+
return Image.fromarray(array, mode="RGB")
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
class LingBotVideoImageToVideoPipeline(LingBotVideoPipeline):
|
| 79 |
+
"""LingBotVideo ti2v pipeline.
|
| 80 |
+
|
| 81 |
+
The condition frame is used twice: as visual input for Qwen3-VL and as a
|
| 82 |
+
clean latent that is written into the beginning of the diffusion latent
|
| 83 |
+
before sampling and after every scheduler step.
|
| 84 |
+
"""
|
| 85 |
+
|
| 86 |
+
def preprocess_image(self, image: Image.Image, height: int, width: int) -> torch.Tensor:
|
| 87 |
+
if image is None:
|
| 88 |
+
raise ValueError("`image` is required when `image_tensor` is not provided.")
|
| 89 |
+
raw = torch.from_numpy(np.array(image.convert("RGB"))).permute(2, 0, 1).unsqueeze(0).contiguous()
|
| 90 |
+
old_h, old_w = raw.shape[-2:]
|
| 91 |
+
scale = max(height / old_h, width / old_w)
|
| 92 |
+
new_h = max(math.ceil(old_h * scale), height)
|
| 93 |
+
new_w = max(math.ceil(old_w * scale), width)
|
| 94 |
+
resized = F.interpolate(raw, size=(new_h, new_w), mode="bilinear", align_corners=False)
|
| 95 |
+
top = int(round((new_h - height) / 2.0))
|
| 96 |
+
left = int(round((new_w - width) / 2.0))
|
| 97 |
+
cropped = resized[:, :, top : top + height, left : left + width].float() / 255.0
|
| 98 |
+
return cropped.unsqueeze(2)
|
| 99 |
+
|
| 100 |
+
def _vision_patch_size(self) -> int:
|
| 101 |
+
for obj in (
|
| 102 |
+
getattr(getattr(self.text_encoder, "config", None), "vision_config", None),
|
| 103 |
+
getattr(getattr(self.processor, "image_processor", None), "config", None),
|
| 104 |
+
getattr(self.processor, "image_processor", None),
|
| 105 |
+
):
|
| 106 |
+
patch = getattr(obj, "patch_size", None)
|
| 107 |
+
if patch is not None:
|
| 108 |
+
return int(patch)
|
| 109 |
+
return 16
|
| 110 |
+
|
| 111 |
+
def _vlm_image(self, pixel: torch.Tensor) -> Image.Image:
|
| 112 |
+
image = _pixel_tensor_to_pil(pixel)
|
| 113 |
+
patch_factor = self._vision_patch_size() * SPATIAL_MERGE_SIZE
|
| 114 |
+
width, height = image.size
|
| 115 |
+
resized_height, resized_width = smart_resize(height, width, factor=patch_factor)
|
| 116 |
+
return image.resize((resized_width, resized_height))
|
| 117 |
+
|
| 118 |
+
@torch.no_grad()
|
| 119 |
+
def encode_image_latent(
|
| 120 |
+
self,
|
| 121 |
+
pixel: torch.Tensor,
|
| 122 |
+
generator: Optional[torch.Generator] = None,
|
| 123 |
+
) -> torch.Tensor:
|
| 124 |
+
if self.vae is None:
|
| 125 |
+
raise ValueError("`vae` is required to encode image latents.")
|
| 126 |
+
device = _module_device(self.vae)
|
| 127 |
+
pixel = pixel.to(device=device, dtype=torch.float32)
|
| 128 |
+
norm_pixel = (pixel - 0.5) / 0.5
|
| 129 |
+
with torch.autocast("cuda", dtype=torch.bfloat16, enabled=device.type == "cuda"):
|
| 130 |
+
latents = self.vae.encode(norm_pixel).latent_dist.sample(generator)
|
| 131 |
+
|
| 132 |
+
mean = torch.tensor(self.vae.config.latents_mean, device=latents.device, dtype=torch.float32)
|
| 133 |
+
std_inv = 1.0 / torch.tensor(
|
| 134 |
+
self.vae.config.latents_std, device=latents.device, dtype=torch.float32
|
| 135 |
+
)
|
| 136 |
+
mean = mean.view(1, -1, 1, 1, 1)
|
| 137 |
+
std_inv = std_inv.view(1, -1, 1, 1, 1)
|
| 138 |
+
return (latents.float() - mean) * std_inv
|
| 139 |
+
|
| 140 |
+
@torch.no_grad()
|
| 141 |
+
def __call__(
|
| 142 |
+
self,
|
| 143 |
+
prompt: str,
|
| 144 |
+
image: Optional[Image.Image] = None,
|
| 145 |
+
image_tensor: Optional[torch.Tensor] = None,
|
| 146 |
+
cond_latent: Optional[torch.Tensor] = None,
|
| 147 |
+
negative_prompt: str = DEFAULT_NEGATIVE_PROMPT,
|
| 148 |
+
height: int = 480,
|
| 149 |
+
width: int = 480,
|
| 150 |
+
num_frames: int = 81,
|
| 151 |
+
num_inference_steps: int = 40,
|
| 152 |
+
guidance_scale: float = 6.0,
|
| 153 |
+
shift: float = 3.0,
|
| 154 |
+
generator: Optional[torch.Generator] = None,
|
| 155 |
+
latents: Optional[torch.Tensor] = None,
|
| 156 |
+
prompt_embeds: Optional[torch.Tensor] = None,
|
| 157 |
+
prompt_mask: Optional[torch.Tensor] = None,
|
| 158 |
+
negative_prompt_embeds: Optional[torch.Tensor] = None,
|
| 159 |
+
negative_prompt_mask: Optional[torch.Tensor] = None,
|
| 160 |
+
output_type: str = "np",
|
| 161 |
+
cfg_parallel_group: Optional[Any] = None,
|
| 162 |
+
batch_cfg: bool = False,
|
| 163 |
+
null_cond_clone_zero: bool = False,
|
| 164 |
+
return_dict: bool = True,
|
| 165 |
+
) -> Union[LingBotVideoPipelineOutput, Tuple[Union[list, torch.Tensor]]]:
|
| 166 |
+
self.check_inputs(height, width, num_frames)
|
| 167 |
+
if self.transformer is None or self.scheduler is None:
|
| 168 |
+
raise ValueError("`transformer` and `scheduler` are required for generation.")
|
| 169 |
+
|
| 170 |
+
device = self._execution_device
|
| 171 |
+
do_cfg = guidance_scale > 1.0
|
| 172 |
+
requested_batch_cfg = bool(batch_cfg)
|
| 173 |
+
effective_batch_cfg = requested_batch_cfg and do_cfg
|
| 174 |
+
self._last_batch_cfg_requested = requested_batch_cfg
|
| 175 |
+
self._last_effective_batch_cfg = effective_batch_cfg
|
| 176 |
+
self._last_batch_cfg_fallback_reason = None
|
| 177 |
+
|
| 178 |
+
cfg_parallel = cfg_parallel_group is not None
|
| 179 |
+
if cfg_parallel and effective_batch_cfg:
|
| 180 |
+
raise ValueError("`cfg_parallel_group` and `batch_cfg` are mutually exclusive.")
|
| 181 |
+
cfg_parallel_rank = 0
|
| 182 |
+
cfg_parallel_world_size = 1
|
| 183 |
+
if cfg_parallel:
|
| 184 |
+
if not dist.is_available() or not dist.is_initialized():
|
| 185 |
+
raise ValueError("`cfg_parallel_group` requires an initialized process group.")
|
| 186 |
+
if not do_cfg:
|
| 187 |
+
raise ValueError("CFG parallel requires `guidance_scale > 1.0`.")
|
| 188 |
+
cfg_parallel_rank = dist.get_rank(cfg_parallel_group)
|
| 189 |
+
cfg_parallel_world_size = dist.get_world_size(cfg_parallel_group)
|
| 190 |
+
if cfg_parallel_world_size != 2:
|
| 191 |
+
raise ValueError(
|
| 192 |
+
f"CFG parallel currently requires exactly 2 ranks, got {cfg_parallel_world_size}."
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
pixel = image_tensor if image_tensor is not None else self.preprocess_image(image, height, width)
|
| 196 |
+
pixel = pixel.to(device=device, dtype=torch.float32)
|
| 197 |
+
vlm_image = self._vlm_image(pixel)
|
| 198 |
+
|
| 199 |
+
if prompt_embeds is not None:
|
| 200 |
+
if prompt_mask is None:
|
| 201 |
+
raise ValueError("`prompt_mask` is required when `prompt_embeds` is provided.")
|
| 202 |
+
prompt_embeds = prompt_embeds.to(device=device)
|
| 203 |
+
prompt_mask = prompt_mask.to(device=device)
|
| 204 |
+
if negative_prompt_embeds is not None:
|
| 205 |
+
if negative_prompt_mask is None:
|
| 206 |
+
raise ValueError(
|
| 207 |
+
"`negative_prompt_mask` is required when `negative_prompt_embeds` is provided."
|
| 208 |
+
)
|
| 209 |
+
negative_prompt_embeds = negative_prompt_embeds.to(device=device)
|
| 210 |
+
negative_prompt_mask = negative_prompt_mask.to(device=device)
|
| 211 |
+
|
| 212 |
+
if cfg_parallel and cfg_parallel_rank == 1:
|
| 213 |
+
if negative_prompt_embeds is not None:
|
| 214 |
+
negative_embeds, negative_mask = negative_prompt_embeds, negative_prompt_mask
|
| 215 |
+
else:
|
| 216 |
+
negative_embeds, negative_mask = self.encode_prompt(
|
| 217 |
+
negative_prompt, images=[vlm_image], device=device
|
| 218 |
+
)
|
| 219 |
+
prompt_embeds = prompt_mask = None
|
| 220 |
+
else:
|
| 221 |
+
if prompt_embeds is None:
|
| 222 |
+
prompt_embeds, prompt_mask = self.encode_prompt(prompt, images=[vlm_image], device=device)
|
| 223 |
+
if do_cfg and not cfg_parallel:
|
| 224 |
+
if null_cond_clone_zero:
|
| 225 |
+
negative_embeds = torch.zeros_like(prompt_embeds)
|
| 226 |
+
negative_mask = prompt_mask.clone()
|
| 227 |
+
elif negative_prompt_embeds is not None:
|
| 228 |
+
negative_embeds, negative_mask = negative_prompt_embeds, negative_prompt_mask
|
| 229 |
+
else:
|
| 230 |
+
negative_embeds, negative_mask = self.encode_prompt(
|
| 231 |
+
negative_prompt, images=[vlm_image], device=device
|
| 232 |
+
)
|
| 233 |
+
|
| 234 |
+
if cond_latent is None:
|
| 235 |
+
cond_latent = self.encode_image_latent(pixel, generator=generator)
|
| 236 |
+
cond_latent = cond_latent.to(device=device, dtype=torch.float32)
|
| 237 |
+
|
| 238 |
+
latents = self.prepare_latents(num_frames, height, width, generator, latents, device)
|
| 239 |
+
latents = self._apply_inpainting(latents, cond_latent)
|
| 240 |
+
self.scheduler.set_timesteps(num_inference_steps, device=device, shift=shift)
|
| 241 |
+
transformer_dtype = _module_dtype(self.transformer)
|
| 242 |
+
cfg_latent_src = _group_global_rank(cfg_parallel_group, 0)
|
| 243 |
+
cfg_uncond_src = _group_global_rank(cfg_parallel_group, 1)
|
| 244 |
+
|
| 245 |
+
for i, timestep in enumerate(self.progress_bar(self.scheduler.timesteps)):
|
| 246 |
+
if cfg_parallel:
|
| 247 |
+
dist.broadcast(latents, src=cfg_latent_src, group=cfg_parallel_group)
|
| 248 |
+
timestep_batch = _transformer_timestep(timestep, transformer_dtype).expand(1).to(device)
|
| 249 |
+
latent_model_input = latents
|
| 250 |
+
if cfg_parallel:
|
| 251 |
+
if cfg_parallel_rank == 0:
|
| 252 |
+
branch_embeds = prompt_embeds
|
| 253 |
+
branch_mask = prompt_mask
|
| 254 |
+
else:
|
| 255 |
+
branch_embeds = negative_embeds
|
| 256 |
+
branch_mask = negative_mask
|
| 257 |
+
branch_model_input = branch_embeds.to(transformer_dtype)
|
| 258 |
+
with _transformer_autocast(device, transformer_dtype):
|
| 259 |
+
branch_noise_pred = self.transformer(
|
| 260 |
+
latent_model_input,
|
| 261 |
+
timestep_batch,
|
| 262 |
+
branch_model_input,
|
| 263 |
+
encoder_attention_mask=branch_mask,
|
| 264 |
+
return_dict=False,
|
| 265 |
+
)[0].float()
|
| 266 |
+
if cfg_parallel_rank == 0:
|
| 267 |
+
noise_pred = branch_noise_pred
|
| 268 |
+
noise_pred_uncond = torch.empty_like(noise_pred)
|
| 269 |
+
else:
|
| 270 |
+
noise_pred_uncond = branch_noise_pred
|
| 271 |
+
dist.broadcast(noise_pred_uncond, src=cfg_uncond_src, group=cfg_parallel_group)
|
| 272 |
+
if cfg_parallel_rank != 0:
|
| 273 |
+
continue
|
| 274 |
+
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred - noise_pred_uncond)
|
| 275 |
+
else:
|
| 276 |
+
prompt_model_input = prompt_embeds.to(transformer_dtype)
|
| 277 |
+
if do_cfg and effective_batch_cfg:
|
| 278 |
+
negative_model_input = negative_embeds.to(transformer_dtype)
|
| 279 |
+
cfg_embeds, cfg_mask = batch_cfg_prompt_inputs(
|
| 280 |
+
prompt_model_input,
|
| 281 |
+
prompt_mask,
|
| 282 |
+
negative_model_input,
|
| 283 |
+
negative_mask,
|
| 284 |
+
null_cond_clone_zero=null_cond_clone_zero,
|
| 285 |
+
)
|
| 286 |
+
cfg_latents = torch.cat([latent_model_input, latent_model_input], dim=0)
|
| 287 |
+
cfg_timesteps = torch.cat([timestep_batch, timestep_batch], dim=0)
|
| 288 |
+
with _transformer_autocast(device, transformer_dtype):
|
| 289 |
+
noise_batched = self.transformer(
|
| 290 |
+
cfg_latents,
|
| 291 |
+
cfg_timesteps,
|
| 292 |
+
cfg_embeds,
|
| 293 |
+
encoder_attention_mask=cfg_mask,
|
| 294 |
+
return_dict=False,
|
| 295 |
+
)[0].float()
|
| 296 |
+
noise_pred, noise_pred_uncond = noise_batched.chunk(2, dim=0)
|
| 297 |
+
noise_pred = noise_pred_uncond + guidance_scale * (
|
| 298 |
+
noise_pred - noise_pred_uncond
|
| 299 |
+
)
|
| 300 |
+
else:
|
| 301 |
+
with _transformer_autocast(device, transformer_dtype):
|
| 302 |
+
noise_pred = self.transformer(
|
| 303 |
+
latent_model_input,
|
| 304 |
+
timestep_batch,
|
| 305 |
+
prompt_model_input,
|
| 306 |
+
encoder_attention_mask=prompt_mask,
|
| 307 |
+
return_dict=False,
|
| 308 |
+
)[0].float()
|
| 309 |
+
|
| 310 |
+
if do_cfg:
|
| 311 |
+
negative_model_input = negative_embeds.to(transformer_dtype)
|
| 312 |
+
with _transformer_autocast(device, transformer_dtype):
|
| 313 |
+
noise_pred_uncond = self.transformer(
|
| 314 |
+
latent_model_input,
|
| 315 |
+
timestep_batch,
|
| 316 |
+
negative_model_input,
|
| 317 |
+
encoder_attention_mask=negative_mask,
|
| 318 |
+
return_dict=False,
|
| 319 |
+
)[0].float()
|
| 320 |
+
noise_pred = noise_pred_uncond + guidance_scale * (
|
| 321 |
+
noise_pred - noise_pred_uncond
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
latents = self.scheduler.step(
|
| 325 |
+
noise_pred,
|
| 326 |
+
timestep,
|
| 327 |
+
latents,
|
| 328 |
+
return_dict=False,
|
| 329 |
+
generator=generator,
|
| 330 |
+
)[0]
|
| 331 |
+
latents = self._apply_inpainting(latents, cond_latent)
|
| 332 |
+
|
| 333 |
+
if cfg_parallel:
|
| 334 |
+
dist.barrier(group=cfg_parallel_group)
|
| 335 |
+
if cfg_parallel_rank != 0:
|
| 336 |
+
frames = latents if output_type == "latent" else []
|
| 337 |
+
self.maybe_free_model_hooks()
|
| 338 |
+
if not return_dict:
|
| 339 |
+
return (frames,)
|
| 340 |
+
return LingBotVideoPipelineOutput(frames=frames)
|
| 341 |
+
|
| 342 |
+
if output_type == "latent":
|
| 343 |
+
frames = latents
|
| 344 |
+
elif output_type == "np":
|
| 345 |
+
frames = self._decode_latents(latents)
|
| 346 |
+
else:
|
| 347 |
+
raise ValueError(f"Unsupported output_type: {output_type}")
|
| 348 |
+
|
| 349 |
+
self.maybe_free_model_hooks()
|
| 350 |
+
if not return_dict:
|
| 351 |
+
return (frames,)
|
| 352 |
+
return LingBotVideoPipelineOutput(frames=frames)
|
lingbot_video/runner.py
ADDED
|
@@ -0,0 +1,1401 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import gc
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import warnings
|
| 10 |
+
from contextlib import contextmanager
|
| 11 |
+
from datetime import timedelta
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from typing import Any
|
| 14 |
+
|
| 15 |
+
import numpy as np
|
| 16 |
+
import torch
|
| 17 |
+
import torch.distributed as dist
|
| 18 |
+
from torch.distributed.device_mesh import DeviceMesh
|
| 19 |
+
from PIL import Image
|
| 20 |
+
|
| 21 |
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
| 22 |
+
sys.path.insert(0, str(REPO_ROOT))
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _env_flag(name: str, default: bool = False) -> bool:
|
| 26 |
+
raw = os.environ.get(name)
|
| 27 |
+
if raw is None:
|
| 28 |
+
return default
|
| 29 |
+
return raw.lower() in {"1", "true", "yes", "on"}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _configure_concise_import_logs() -> None:
|
| 33 |
+
if _env_flag("LINGBOT_VERBOSE_LOGS"):
|
| 34 |
+
return
|
| 35 |
+
os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
|
| 36 |
+
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
|
| 37 |
+
os.environ.setdefault("DIFFUSERS_VERBOSITY", "error")
|
| 38 |
+
os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
|
| 39 |
+
warnings.filterwarnings(
|
| 40 |
+
"ignore",
|
| 41 |
+
message=r"Unable to import `torchao` Tensor objects.*",
|
| 42 |
+
)
|
| 43 |
+
warnings.filterwarnings(
|
| 44 |
+
"ignore",
|
| 45 |
+
message=r"`enable_parallelism` is an experimental feature.*",
|
| 46 |
+
)
|
| 47 |
+
warnings.filterwarnings(
|
| 48 |
+
"ignore",
|
| 49 |
+
message=r"barrier\(\): using the device under current context.*",
|
| 50 |
+
)
|
| 51 |
+
logging.getLogger(
|
| 52 |
+
"sglang.srt.layers.moe.moe_runner.triton_utils.fused_moe_triton_config"
|
| 53 |
+
).setLevel(logging.ERROR)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
_configure_concise_import_logs()
|
| 57 |
+
|
| 58 |
+
from lingbot_video.inference_backend import ( # noqa: E402
|
| 59 |
+
resolve_backend_engine,
|
| 60 |
+
resolve_negative_prompt_arg,
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
try:
|
| 64 |
+
import transformers as _transformers
|
| 65 |
+
import transformers.cache_utils as _transformers_cache_utils
|
| 66 |
+
except Exception as exc: # pragma: no cover - deployment dependency guard
|
| 67 |
+
_transformers = None
|
| 68 |
+
_transformers_cache_utils = None
|
| 69 |
+
_TRANSFORMERS_IMPORT_ERROR = exc
|
| 70 |
+
else:
|
| 71 |
+
_TRANSFORMERS_IMPORT_ERROR = None
|
| 72 |
+
|
| 73 |
+
try:
|
| 74 |
+
import diffusers.utils.import_utils as _diffusers_import_utils
|
| 75 |
+
except Exception as exc: # pragma: no cover - deployment dependency guard
|
| 76 |
+
_diffusers_import_utils = None
|
| 77 |
+
_DIFFUSERS_IMPORT_UTILS_ERROR = exc
|
| 78 |
+
else:
|
| 79 |
+
_DIFFUSERS_IMPORT_UTILS_ERROR = None
|
| 80 |
+
|
| 81 |
+
try:
|
| 82 |
+
from diffusers.utils import logging as _diffusers_logging
|
| 83 |
+
except Exception:
|
| 84 |
+
_diffusers_logging = None
|
| 85 |
+
|
| 86 |
+
try:
|
| 87 |
+
from transformers.utils import logging as _transformers_logging
|
| 88 |
+
except Exception:
|
| 89 |
+
_transformers_logging = None
|
| 90 |
+
|
| 91 |
+
try:
|
| 92 |
+
import huggingface_hub.utils as _hf_hub_utils
|
| 93 |
+
except Exception:
|
| 94 |
+
_hf_hub_utils = None
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _install_sglang_import_shims() -> None:
|
| 98 |
+
if _transformers is not None and _transformers_cache_utils is not None:
|
| 99 |
+
hybrid_cache = getattr(_transformers_cache_utils, "HybridCache", None)
|
| 100 |
+
if hybrid_cache is None:
|
| 101 |
+
hybrid_cache = getattr(_transformers_cache_utils, "DynamicCache", object)
|
| 102 |
+
_transformers_cache_utils.HybridCache = hybrid_cache
|
| 103 |
+
import_structure = getattr(_transformers, "_import_structure", {})
|
| 104 |
+
cache_exports = import_structure.setdefault("cache_utils", [])
|
| 105 |
+
if "HybridCache" not in cache_exports:
|
| 106 |
+
cache_exports.append("HybridCache")
|
| 107 |
+
_transformers.HybridCache = hybrid_cache
|
| 108 |
+
if _diffusers_import_utils is not None:
|
| 109 |
+
_diffusers_import_utils._peft_available = False
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
_install_sglang_import_shims()
|
| 113 |
+
|
| 114 |
+
try:
|
| 115 |
+
from lingbot_video.fsdp_inference import (
|
| 116 |
+
apply_fsdp_inference,
|
| 117 |
+
init_fsdp_inference_mesh,
|
| 118 |
+
)
|
| 119 |
+
from lingbot_video.model_paths import (
|
| 120 |
+
effective_refiner_model_dir,
|
| 121 |
+
model_component_dir,
|
| 122 |
+
)
|
| 123 |
+
from lingbot_video.pipeline_lingbot_video import (
|
| 124 |
+
DEFAULT_NEGATIVE_PROMPT,
|
| 125 |
+
DEFAULT_NEGATIVE_PROMPT_IMAGE,
|
| 126 |
+
LingBotVideoPipeline,
|
| 127 |
+
)
|
| 128 |
+
from lingbot_video.pipeline_lingbot_video_i2v import (
|
| 129 |
+
LingBotVideoImageToVideoPipeline,
|
| 130 |
+
)
|
| 131 |
+
from lingbot_video.utils import (
|
| 132 |
+
caption_from_sample,
|
| 133 |
+
load_first_frame_condition_tensor,
|
| 134 |
+
load_refiner_video_tensor,
|
| 135 |
+
num_frames_from_duration,
|
| 136 |
+
prepare_refiner_latent,
|
| 137 |
+
)
|
| 138 |
+
except Exception as exc: # pragma: no cover - reported when generation is attempted
|
| 139 |
+
DEFAULT_NEGATIVE_PROMPT = ""
|
| 140 |
+
DEFAULT_NEGATIVE_PROMPT_IMAGE = ""
|
| 141 |
+
LingBotVideoPipeline = None
|
| 142 |
+
LingBotVideoImageToVideoPipeline = None
|
| 143 |
+
apply_fsdp_inference = None
|
| 144 |
+
init_fsdp_inference_mesh = None
|
| 145 |
+
effective_refiner_model_dir = None
|
| 146 |
+
model_component_dir = None
|
| 147 |
+
caption_from_sample = None
|
| 148 |
+
load_first_frame_condition_tensor = None
|
| 149 |
+
load_refiner_video_tensor = None
|
| 150 |
+
num_frames_from_duration = None
|
| 151 |
+
prepare_refiner_latent = None
|
| 152 |
+
_LINGBOT_PIPELINE_IMPORT_ERROR = exc
|
| 153 |
+
else:
|
| 154 |
+
_LINGBOT_PIPELINE_IMPORT_ERROR = None
|
| 155 |
+
|
| 156 |
+
try:
|
| 157 |
+
from lingbot_video.transformer_lingbot_video import (
|
| 158 |
+
LingBotVideoTransformer3DModel,
|
| 159 |
+
)
|
| 160 |
+
except Exception as exc: # pragma: no cover - reported when generation is attempted
|
| 161 |
+
LingBotVideoTransformer3DModel = None
|
| 162 |
+
_TRANSFORMER_IMPORT_ERROR = exc
|
| 163 |
+
else:
|
| 164 |
+
_TRANSFORMER_IMPORT_ERROR = None
|
| 165 |
+
|
| 166 |
+
try:
|
| 167 |
+
from diffusers import ContextParallelConfig
|
| 168 |
+
from diffusers.hooks.context_parallel import apply_context_parallel
|
| 169 |
+
from diffusers.models._modeling_parallel import ParallelConfig
|
| 170 |
+
from diffusers.models.attention import AttentionModuleMixin
|
| 171 |
+
from diffusers.models.attention_processor import Attention, MochiAttention
|
| 172 |
+
from diffusers.utils import export_to_video
|
| 173 |
+
except Exception as exc: # pragma: no cover - reported by the selected engine
|
| 174 |
+
ContextParallelConfig = None
|
| 175 |
+
ParallelConfig = None
|
| 176 |
+
Attention = None
|
| 177 |
+
AttentionModuleMixin = None
|
| 178 |
+
MochiAttention = None
|
| 179 |
+
apply_context_parallel = None
|
| 180 |
+
export_to_video = None
|
| 181 |
+
_DIFFUSERS_IMPORT_ERROR = exc
|
| 182 |
+
else:
|
| 183 |
+
_DIFFUSERS_IMPORT_ERROR = None
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def _cuda_sdp_state() -> dict[str, bool | None]:
|
| 187 |
+
state: dict[str, bool | None] = {}
|
| 188 |
+
cuda_backends = getattr(torch.backends, "cuda", None)
|
| 189 |
+
if cuda_backends is None:
|
| 190 |
+
return state
|
| 191 |
+
getters = {
|
| 192 |
+
"flash": "flash_sdp_enabled",
|
| 193 |
+
"mem_efficient": "mem_efficient_sdp_enabled",
|
| 194 |
+
"math": "math_sdp_enabled",
|
| 195 |
+
"cudnn": "cudnn_sdp_enabled",
|
| 196 |
+
}
|
| 197 |
+
for key, getter in getters.items():
|
| 198 |
+
fn = getattr(cuda_backends, getter, None)
|
| 199 |
+
if fn is None:
|
| 200 |
+
state[key] = None
|
| 201 |
+
continue
|
| 202 |
+
try:
|
| 203 |
+
state[key] = bool(fn())
|
| 204 |
+
except Exception:
|
| 205 |
+
state[key] = None
|
| 206 |
+
return state
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def _restore_cuda_sdp_state(state: dict[str, bool | None]) -> None:
|
| 210 |
+
cuda_backends = getattr(torch.backends, "cuda", None)
|
| 211 |
+
if cuda_backends is None:
|
| 212 |
+
return
|
| 213 |
+
setters = {
|
| 214 |
+
"flash": "enable_flash_sdp",
|
| 215 |
+
"mem_efficient": "enable_mem_efficient_sdp",
|
| 216 |
+
"math": "enable_math_sdp",
|
| 217 |
+
"cudnn": "enable_cudnn_sdp",
|
| 218 |
+
}
|
| 219 |
+
for key, setter in setters.items():
|
| 220 |
+
value = state.get(key)
|
| 221 |
+
if value is None:
|
| 222 |
+
continue
|
| 223 |
+
fn = getattr(cuda_backends, setter, None)
|
| 224 |
+
if fn is None:
|
| 225 |
+
continue
|
| 226 |
+
try:
|
| 227 |
+
fn(value)
|
| 228 |
+
except Exception:
|
| 229 |
+
pass
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
_BASELINE_CUDA_SDP_STATE = _cuda_sdp_state()
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
RESOLUTION_BUCKETS: dict[str, dict[str, tuple[int, int]]] = {
|
| 236 |
+
"192p": {
|
| 237 |
+
"1:1": (192, 192),
|
| 238 |
+
"9:16": (192, 320),
|
| 239 |
+
"16:9": (320, 192),
|
| 240 |
+
"3:4": (192, 256),
|
| 241 |
+
"4:3": (256, 192),
|
| 242 |
+
},
|
| 243 |
+
"480p": {
|
| 244 |
+
"1:1": (480, 480),
|
| 245 |
+
"9:16": (480, 832),
|
| 246 |
+
"16:9": (832, 480),
|
| 247 |
+
"3:4": (480, 640),
|
| 248 |
+
"4:3": (640, 480),
|
| 249 |
+
},
|
| 250 |
+
"720p": {
|
| 251 |
+
"1:1": (736, 736),
|
| 252 |
+
"9:16": (736, 1280),
|
| 253 |
+
"16:9": (1280, 736),
|
| 254 |
+
"3:4": (736, 960),
|
| 255 |
+
"4:3": (960, 736),
|
| 256 |
+
},
|
| 257 |
+
"1080p": {
|
| 258 |
+
"1:1": (1088, 1088),
|
| 259 |
+
"9:16": (1088, 1920),
|
| 260 |
+
"16:9": (1920, 1088),
|
| 261 |
+
"3:4": (1088, 1440),
|
| 262 |
+
"4:3": (1440, 1088),
|
| 263 |
+
},
|
| 264 |
+
"2k": {
|
| 265 |
+
"1:1": (1440, 1440),
|
| 266 |
+
"9:16": (1440, 2560),
|
| 267 |
+
"16:9": (2560, 1440),
|
| 268 |
+
"3:4": (1440, 1920),
|
| 269 |
+
"4:3": (1920, 1440),
|
| 270 |
+
},
|
| 271 |
+
"4k": {
|
| 272 |
+
"1:1": (2176, 2176),
|
| 273 |
+
"9:16": (2176, 3840),
|
| 274 |
+
"16:9": (3840, 2176),
|
| 275 |
+
"3:4": (2176, 2880),
|
| 276 |
+
"4:3": (2880, 2176),
|
| 277 |
+
},
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def _distributed_env() -> tuple[int, int, int]:
|
| 282 |
+
return (
|
| 283 |
+
int(os.environ.get("RANK", "0")),
|
| 284 |
+
int(os.environ.get("LOCAL_RANK", "0")),
|
| 285 |
+
int(os.environ.get("WORLD_SIZE", "1")),
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
def _default_device() -> torch.device:
|
| 290 |
+
if not torch.cuda.is_available():
|
| 291 |
+
return torch.device("cpu")
|
| 292 |
+
return torch.device("cuda", torch.cuda.current_device())
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def _init_parallel(
|
| 296 |
+
cfg_degree: int,
|
| 297 |
+
context_degree: int,
|
| 298 |
+
enable_fsdp_inference: bool,
|
| 299 |
+
) -> tuple[int, int, int, Any | None, DeviceMesh | None, int, int]:
|
| 300 |
+
rank, local_rank, world_size = _distributed_env()
|
| 301 |
+
if cfg_degree < 1 or context_degree < 1:
|
| 302 |
+
raise ValueError("Parallel degrees must be >= 1.")
|
| 303 |
+
expected_world_size = cfg_degree * context_degree
|
| 304 |
+
if expected_world_size <= 1:
|
| 305 |
+
if enable_fsdp_inference and world_size > 1:
|
| 306 |
+
if not torch.cuda.is_available():
|
| 307 |
+
raise RuntimeError("FSDP inference requires CUDA devices.")
|
| 308 |
+
torch.cuda.set_device(local_rank)
|
| 309 |
+
if not dist.is_initialized():
|
| 310 |
+
dist.init_process_group("nccl", timeout=timedelta(minutes=30))
|
| 311 |
+
return rank, local_rank, world_size, None, None, 0, 0
|
| 312 |
+
if torch.cuda.is_available():
|
| 313 |
+
torch.cuda.set_device(0)
|
| 314 |
+
return rank, local_rank, world_size, None, None, 0, 0
|
| 315 |
+
if world_size != expected_world_size:
|
| 316 |
+
raise ValueError(
|
| 317 |
+
f"Parallel topology cfg={cfg_degree}, context={context_degree} requires "
|
| 318 |
+
f"WORLD_SIZE={expected_world_size}, got {world_size}."
|
| 319 |
+
)
|
| 320 |
+
if not torch.cuda.is_available():
|
| 321 |
+
raise RuntimeError("Distributed parallel inference requires CUDA devices.")
|
| 322 |
+
torch.cuda.set_device(local_rank)
|
| 323 |
+
if not dist.is_initialized():
|
| 324 |
+
dist.init_process_group("nccl", timeout=timedelta(minutes=30))
|
| 325 |
+
|
| 326 |
+
cfg_branch_rank = rank // context_degree
|
| 327 |
+
context_rank = rank % context_degree
|
| 328 |
+
cfg_parallel_group = None
|
| 329 |
+
if cfg_degree > 1:
|
| 330 |
+
lane_groups = [
|
| 331 |
+
dist.new_group(
|
| 332 |
+
ranks=[cfg_rank * context_degree + context_rank for cfg_rank in range(cfg_degree)]
|
| 333 |
+
)
|
| 334 |
+
for context_rank in range(context_degree)
|
| 335 |
+
]
|
| 336 |
+
cfg_parallel_group = lane_groups[context_rank]
|
| 337 |
+
|
| 338 |
+
context_mesh = None
|
| 339 |
+
if context_degree > 1 and cfg_degree > 1:
|
| 340 |
+
mesh_ranks = [
|
| 341 |
+
[
|
| 342 |
+
cfg_rank * context_degree + context_rank
|
| 343 |
+
for context_rank in range(context_degree)
|
| 344 |
+
]
|
| 345 |
+
for cfg_rank in range(cfg_degree)
|
| 346 |
+
]
|
| 347 |
+
context_mesh = DeviceMesh(
|
| 348 |
+
"cuda",
|
| 349 |
+
mesh_ranks,
|
| 350 |
+
mesh_dim_names=("ring", "ulysses"),
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
return rank, local_rank, world_size, cfg_parallel_group, context_mesh, cfg_branch_rank, context_rank
|
| 354 |
+
|
| 355 |
+
try:
|
| 356 |
+
from lingbot_video.native_backend import (
|
| 357 |
+
LingBotVideoNativePipeline,
|
| 358 |
+
register_lingbot_native_pipeline,
|
| 359 |
+
)
|
| 360 |
+
except Exception as exc: # pragma: no cover - only needed for --engine sglang-native
|
| 361 |
+
LingBotVideoNativePipeline = None
|
| 362 |
+
register_lingbot_native_pipeline = None
|
| 363 |
+
_NATIVE_BACKEND_IMPORT_ERROR = exc
|
| 364 |
+
else:
|
| 365 |
+
_NATIVE_BACKEND_IMPORT_ERROR = None
|
| 366 |
+
finally:
|
| 367 |
+
_restore_cuda_sdp_state(_BASELINE_CUDA_SDP_STATE)
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def _parse_dtype(name: str) -> torch.dtype:
|
| 371 |
+
normalized = name.lower()
|
| 372 |
+
if normalized in {"bf16", "bfloat16"}:
|
| 373 |
+
return torch.bfloat16
|
| 374 |
+
if normalized in {"fp16", "float16"}:
|
| 375 |
+
return torch.float16
|
| 376 |
+
if normalized in {"fp32", "float32"}:
|
| 377 |
+
return torch.float32
|
| 378 |
+
raise ValueError(f"unsupported dtype: {name}")
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def _dtype_name(dtype: torch.dtype) -> str:
|
| 382 |
+
return str(dtype).replace("torch.", "")
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
def _make_dtype_map(args: argparse.Namespace) -> dict[str, torch.dtype]:
|
| 386 |
+
default_dtype = _parse_dtype(args.default_dtype)
|
| 387 |
+
return {
|
| 388 |
+
"default": default_dtype,
|
| 389 |
+
"transformer": _parse_dtype(args.transformer_dtype),
|
| 390 |
+
"text_encoder": _parse_dtype(args.text_encoder_dtype),
|
| 391 |
+
"vae": _parse_dtype(args.vae_dtype),
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
def _make_default_image(height: int, width: int) -> Image.Image:
|
| 396 |
+
yy, xx = np.mgrid[0:height, 0:width]
|
| 397 |
+
red = (xx / max(width - 1, 1) * 255).astype(np.uint8)
|
| 398 |
+
green = (yy / max(height - 1, 1) * 255).astype(np.uint8)
|
| 399 |
+
blue = (((xx // 24 + yy // 24) % 2) * 180 + 40).astype(np.uint8)
|
| 400 |
+
return Image.fromarray(np.stack([red, green, blue], axis=-1), mode="RGB")
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
def _load_prompt_sample(path: Path) -> dict[str, Any]:
|
| 404 |
+
data = json.loads(path.read_text(encoding="utf-8"))
|
| 405 |
+
if isinstance(data, list):
|
| 406 |
+
if not data:
|
| 407 |
+
raise ValueError(f"`--prompt_json` is empty: {path}")
|
| 408 |
+
data = data[0]
|
| 409 |
+
if not isinstance(data, dict):
|
| 410 |
+
raise ValueError(f"`--prompt_json` must contain a dict or a non-empty list of dicts: {path}")
|
| 411 |
+
return data
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
def _caption_from_sample(sample: dict[str, Any]) -> str:
|
| 415 |
+
if caption_from_sample is not None:
|
| 416 |
+
return caption_from_sample(sample)
|
| 417 |
+
if "caption" in sample:
|
| 418 |
+
caption = sample["caption"]
|
| 419 |
+
else:
|
| 420 |
+
runtime_keys = {
|
| 421 |
+
"duration",
|
| 422 |
+
"fps",
|
| 423 |
+
"height",
|
| 424 |
+
"width",
|
| 425 |
+
"num_frames",
|
| 426 |
+
"resolution",
|
| 427 |
+
"ratio",
|
| 428 |
+
}
|
| 429 |
+
caption = {key: value for key, value in sample.items() if key not in runtime_keys}
|
| 430 |
+
if isinstance(caption, dict):
|
| 431 |
+
return str(caption.get("qwen_long_caption") or caption.get("comprehensive_description") or caption)
|
| 432 |
+
return str(caption)
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def _height_width_from_bucket(resolution: str, ratio: str) -> tuple[int, int]:
|
| 436 |
+
if resolution not in RESOLUTION_BUCKETS:
|
| 437 |
+
choices = ", ".join(sorted(RESOLUTION_BUCKETS))
|
| 438 |
+
raise ValueError(f"unsupported resolution {resolution!r}; choices: {choices}")
|
| 439 |
+
ratios = RESOLUTION_BUCKETS[resolution]
|
| 440 |
+
if ratio not in ratios:
|
| 441 |
+
choices = ", ".join(sorted(ratios))
|
| 442 |
+
raise ValueError(f"unsupported ratio {ratio!r} for {resolution}; choices: {choices}")
|
| 443 |
+
return ratios[ratio]
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
def _module_dtype(module: Any) -> str | None:
|
| 447 |
+
if module is None:
|
| 448 |
+
return None
|
| 449 |
+
if hasattr(module, "dtype"):
|
| 450 |
+
dtype = getattr(module, "dtype")
|
| 451 |
+
if isinstance(dtype, torch.dtype):
|
| 452 |
+
return _dtype_name(dtype)
|
| 453 |
+
if isinstance(module, torch.nn.Module):
|
| 454 |
+
try:
|
| 455 |
+
return _dtype_name(next(module.parameters()).dtype)
|
| 456 |
+
except StopIteration:
|
| 457 |
+
return None
|
| 458 |
+
return None
|
| 459 |
+
|
| 460 |
+
|
| 461 |
+
def _component_dtypes(pipe: Any) -> dict[str, str | None]:
|
| 462 |
+
return {
|
| 463 |
+
"transformer": _module_dtype(getattr(pipe, "transformer", None)),
|
| 464 |
+
"text_encoder": _module_dtype(getattr(pipe, "text_encoder", None)),
|
| 465 |
+
"vae": _module_dtype(getattr(pipe, "vae", None)),
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
def _disable_external_progress_bars() -> None:
|
| 470 |
+
for logging_module in (_diffusers_logging, _transformers_logging):
|
| 471 |
+
fn = getattr(logging_module, "disable_progress_bar", None)
|
| 472 |
+
if callable(fn):
|
| 473 |
+
fn()
|
| 474 |
+
hf_fn = getattr(_hf_hub_utils, "disable_progress_bars", None)
|
| 475 |
+
if callable(hf_fn):
|
| 476 |
+
hf_fn()
|
| 477 |
+
|
| 478 |
+
|
| 479 |
+
def _configure_pipeline_logs(pipe: Any) -> None:
|
| 480 |
+
inner_pipe = _inner_diffusers_pipe(pipe)
|
| 481 |
+
set_progress_bar_config = getattr(inner_pipe, "set_progress_bar_config", None)
|
| 482 |
+
if callable(set_progress_bar_config):
|
| 483 |
+
set_progress_bar_config(
|
| 484 |
+
disable=not _show_progress(),
|
| 485 |
+
desc="denoising",
|
| 486 |
+
dynamic_ncols=True,
|
| 487 |
+
)
|
| 488 |
+
|
| 489 |
+
|
| 490 |
+
@contextmanager
|
| 491 |
+
def _patch_qwen3vl_from_pretrained():
|
| 492 |
+
try:
|
| 493 |
+
from transformers import Qwen3VLForConditionalGeneration
|
| 494 |
+
except Exception:
|
| 495 |
+
yield
|
| 496 |
+
return
|
| 497 |
+
|
| 498 |
+
original_from_pretrained = Qwen3VLForConditionalGeneration.from_pretrained
|
| 499 |
+
attn_implementation = os.environ.get("LINGBOT_QWEN_ATTN_IMPLEMENTATION", "flash_attention_3")
|
| 500 |
+
|
| 501 |
+
@classmethod
|
| 502 |
+
def patched_from_pretrained(cls, pretrained_model_name_or_path, *args, **kwargs):
|
| 503 |
+
if attn_implementation:
|
| 504 |
+
kwargs.setdefault("attn_implementation", attn_implementation)
|
| 505 |
+
if "torch_dtype" in kwargs and "dtype" not in kwargs:
|
| 506 |
+
kwargs["dtype"] = kwargs.pop("torch_dtype")
|
| 507 |
+
return original_from_pretrained(pretrained_model_name_or_path, *args, **kwargs)
|
| 508 |
+
|
| 509 |
+
Qwen3VLForConditionalGeneration.from_pretrained = patched_from_pretrained
|
| 510 |
+
try:
|
| 511 |
+
yield
|
| 512 |
+
finally:
|
| 513 |
+
Qwen3VLForConditionalGeneration.from_pretrained = original_from_pretrained
|
| 514 |
+
|
| 515 |
+
|
| 516 |
+
def _destroy_parallel_if_needed() -> None:
|
| 517 |
+
if dist.is_available() and dist.is_initialized():
|
| 518 |
+
dist.destroy_process_group()
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
def _sync_parallel_if_needed() -> None:
|
| 522 |
+
if dist.is_available() and dist.is_initialized():
|
| 523 |
+
dist.barrier()
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
def _current_rank() -> int:
|
| 527 |
+
if dist.is_available() and dist.is_initialized():
|
| 528 |
+
return dist.get_rank()
|
| 529 |
+
return 0
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
def _is_main_process() -> bool:
|
| 533 |
+
return _current_rank() == 0
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
def _show_progress() -> bool:
|
| 537 |
+
return _is_main_process() and not _env_flag("LINGBOT_QUIET_PROGRESS")
|
| 538 |
+
|
| 539 |
+
|
| 540 |
+
def _log_progress(message: str) -> None:
|
| 541 |
+
if _show_progress():
|
| 542 |
+
print(message, flush=True)
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
def _apply_fsdp_inference_if_requested(
|
| 546 |
+
pipe: Any,
|
| 547 |
+
enabled: bool,
|
| 548 |
+
mesh: DeviceMesh | None,
|
| 549 |
+
) -> Any | None:
|
| 550 |
+
if not enabled:
|
| 551 |
+
return None
|
| 552 |
+
if apply_fsdp_inference is None:
|
| 553 |
+
raise RuntimeError("FSDP inference helpers are not importable.") from _LINGBOT_PIPELINE_IMPORT_ERROR
|
| 554 |
+
inner_pipe = _inner_diffusers_pipe(pipe)
|
| 555 |
+
transformer = getattr(inner_pipe, "transformer", None)
|
| 556 |
+
if not isinstance(transformer, torch.nn.Module):
|
| 557 |
+
raise ValueError(
|
| 558 |
+
"FSDP inference requires a pipeline with a torch.nn.Module transformer."
|
| 559 |
+
)
|
| 560 |
+
_log_progress("applying FSDP inference sharding")
|
| 561 |
+
info = apply_fsdp_inference(transformer, mesh)
|
| 562 |
+
_log_progress(f"applied FSDP inference sharding: {info}")
|
| 563 |
+
return info
|
| 564 |
+
|
| 565 |
+
|
| 566 |
+
def _cache_prompt_conditions(
|
| 567 |
+
pipe: Any,
|
| 568 |
+
prompt: str,
|
| 569 |
+
negative_prompt: str,
|
| 570 |
+
*,
|
| 571 |
+
device: torch.device,
|
| 572 |
+
null_cond_clone_zero: bool,
|
| 573 |
+
images: list[Any] | None = None,
|
| 574 |
+
) -> dict[str, torch.Tensor]:
|
| 575 |
+
encode_kwargs: dict[str, Any] = {"device": device}
|
| 576 |
+
if images is not None:
|
| 577 |
+
encode_kwargs["images"] = images
|
| 578 |
+
prompt_embeds, prompt_mask = pipe.encode_prompt(prompt, **encode_kwargs)
|
| 579 |
+
if null_cond_clone_zero:
|
| 580 |
+
negative_embeds = torch.zeros_like(prompt_embeds)
|
| 581 |
+
negative_mask = prompt_mask.clone()
|
| 582 |
+
else:
|
| 583 |
+
negative_embeds, negative_mask = pipe.encode_prompt(negative_prompt, **encode_kwargs)
|
| 584 |
+
return {
|
| 585 |
+
"prompt_embeds": prompt_embeds.detach().cpu(),
|
| 586 |
+
"prompt_mask": prompt_mask.detach().cpu(),
|
| 587 |
+
"negative_prompt_embeds": negative_embeds.detach().cpu(),
|
| 588 |
+
"negative_prompt_mask": negative_mask.detach().cpu(),
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
|
| 592 |
+
def _inner_diffusers_pipe(pipe: Any) -> Any:
|
| 593 |
+
return getattr(pipe, "diffusers_pipe", pipe)
|
| 594 |
+
|
| 595 |
+
|
| 596 |
+
def _cache_ti2v_prompt_conditions(
|
| 597 |
+
pipe: Any,
|
| 598 |
+
prompt: str,
|
| 599 |
+
negative_prompt: str,
|
| 600 |
+
image: Image.Image,
|
| 601 |
+
*,
|
| 602 |
+
height: int,
|
| 603 |
+
width: int,
|
| 604 |
+
device: torch.device,
|
| 605 |
+
null_cond_clone_zero: bool,
|
| 606 |
+
) -> tuple[dict[str, torch.Tensor], torch.Tensor]:
|
| 607 |
+
condition_pipe = _inner_diffusers_pipe(pipe)
|
| 608 |
+
if not hasattr(condition_pipe, "preprocess_image") or not hasattr(condition_pipe, "_vlm_image"):
|
| 609 |
+
raise ValueError("TI2V condition reuse requires a LingBotVideo image-to-video pipeline.")
|
| 610 |
+
pixel = condition_pipe.preprocess_image(image, height, width).to(
|
| 611 |
+
device=device,
|
| 612 |
+
dtype=torch.float32,
|
| 613 |
+
)
|
| 614 |
+
vlm_image = condition_pipe._vlm_image(pixel)
|
| 615 |
+
return (
|
| 616 |
+
_cache_prompt_conditions(
|
| 617 |
+
condition_pipe,
|
| 618 |
+
prompt,
|
| 619 |
+
negative_prompt,
|
| 620 |
+
device=device,
|
| 621 |
+
null_cond_clone_zero=null_cond_clone_zero,
|
| 622 |
+
images=[vlm_image],
|
| 623 |
+
),
|
| 624 |
+
pixel.detach().cpu(),
|
| 625 |
+
)
|
| 626 |
+
|
| 627 |
+
|
| 628 |
+
def _condition_call_kwargs(
|
| 629 |
+
cache: dict[str, torch.Tensor] | None,
|
| 630 |
+
device: torch.device,
|
| 631 |
+
) -> dict[str, torch.Tensor]:
|
| 632 |
+
if not cache:
|
| 633 |
+
return {}
|
| 634 |
+
return {key: value.to(device=device) for key, value in cache.items()}
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
def _pipeline_class_for_mode(mode: str) -> Any:
|
| 638 |
+
if mode == "ti2v":
|
| 639 |
+
if LingBotVideoImageToVideoPipeline is None:
|
| 640 |
+
raise RuntimeError(
|
| 641 |
+
"LingBotVideoImageToVideoPipeline is not importable."
|
| 642 |
+
) from _LINGBOT_PIPELINE_IMPORT_ERROR
|
| 643 |
+
return LingBotVideoImageToVideoPipeline
|
| 644 |
+
if LingBotVideoPipeline is None:
|
| 645 |
+
raise RuntimeError("LingBotVideoPipeline is not importable.") from _LINGBOT_PIPELINE_IMPORT_ERROR
|
| 646 |
+
return LingBotVideoPipeline
|
| 647 |
+
|
| 648 |
+
|
| 649 |
+
def _load_transformer_component(
|
| 650 |
+
model_dir: Path,
|
| 651 |
+
transformer_subfolder: str,
|
| 652 |
+
dtype_map: dict[str, torch.dtype],
|
| 653 |
+
) -> Any:
|
| 654 |
+
if LingBotVideoTransformer3DModel is None:
|
| 655 |
+
raise RuntimeError(
|
| 656 |
+
"LingBotVideoTransformer3DModel is not importable."
|
| 657 |
+
) from _TRANSFORMER_IMPORT_ERROR
|
| 658 |
+
if model_component_dir is None:
|
| 659 |
+
raise RuntimeError("model path helpers are not importable.") from _LINGBOT_PIPELINE_IMPORT_ERROR
|
| 660 |
+
model_component_dir(model_dir, transformer_subfolder)
|
| 661 |
+
transformer_dtype = dtype_map.get(
|
| 662 |
+
"transformer",
|
| 663 |
+
dtype_map.get("default", torch.float32),
|
| 664 |
+
)
|
| 665 |
+
_disable_external_progress_bars()
|
| 666 |
+
_log_progress(
|
| 667 |
+
f"loading transformer subfolder={transformer_subfolder} "
|
| 668 |
+
f"dtype={_dtype_name(transformer_dtype)} model_dir={model_dir}"
|
| 669 |
+
)
|
| 670 |
+
transformer = LingBotVideoTransformer3DModel.from_pretrained(
|
| 671 |
+
str(model_dir),
|
| 672 |
+
subfolder=transformer_subfolder,
|
| 673 |
+
torch_dtype=transformer_dtype,
|
| 674 |
+
)
|
| 675 |
+
_log_progress(f"loaded transformer subfolder={transformer_subfolder}")
|
| 676 |
+
return transformer
|
| 677 |
+
|
| 678 |
+
|
| 679 |
+
def _move_pipeline_aux_modules_to_device(pipe: Any, device: torch.device) -> Any:
|
| 680 |
+
inner_pipe = _inner_diffusers_pipe(pipe)
|
| 681 |
+
for name in ("text_encoder", "vae"):
|
| 682 |
+
module = getattr(inner_pipe, name, None)
|
| 683 |
+
if isinstance(module, torch.nn.Module):
|
| 684 |
+
_log_progress(f"moving {name} to {device}")
|
| 685 |
+
module.to(device)
|
| 686 |
+
return pipe
|
| 687 |
+
|
| 688 |
+
|
| 689 |
+
def _load_diffusers_pipe(
|
| 690 |
+
model_dir: Path,
|
| 691 |
+
dtype_map: dict[str, torch.dtype],
|
| 692 |
+
mode: str,
|
| 693 |
+
transformer_subfolder: str,
|
| 694 |
+
defer_transformer_to_device: bool = False,
|
| 695 |
+
) -> Any:
|
| 696 |
+
pipeline_class = _pipeline_class_for_mode(mode)
|
| 697 |
+
transformer = _load_transformer_component(model_dir, transformer_subfolder, dtype_map)
|
| 698 |
+
|
| 699 |
+
_disable_external_progress_bars()
|
| 700 |
+
_log_progress(f"loading pipeline mode={mode} model_dir={model_dir}")
|
| 701 |
+
with _patch_qwen3vl_from_pretrained():
|
| 702 |
+
pipe = pipeline_class.from_pretrained(
|
| 703 |
+
str(model_dir),
|
| 704 |
+
transformer=transformer,
|
| 705 |
+
trust_remote_code=True,
|
| 706 |
+
torch_dtype=dtype_map,
|
| 707 |
+
)
|
| 708 |
+
_log_progress(f"loaded pipeline mode={mode}")
|
| 709 |
+
device = _default_device()
|
| 710 |
+
if defer_transformer_to_device:
|
| 711 |
+
return _move_pipeline_aux_modules_to_device(pipe, device)
|
| 712 |
+
_log_progress(f"moving pipeline to {device}")
|
| 713 |
+
return pipe.to(device)
|
| 714 |
+
|
| 715 |
+
|
| 716 |
+
def _load_sglang_native_pipe(
|
| 717 |
+
model_dir: Path,
|
| 718 |
+
dtype_map: dict[str, torch.dtype],
|
| 719 |
+
mode: str,
|
| 720 |
+
transformer_subfolder: str,
|
| 721 |
+
defer_transformer_to_device: bool = False,
|
| 722 |
+
) -> Any:
|
| 723 |
+
if LingBotVideoNativePipeline is None or register_lingbot_native_pipeline is None:
|
| 724 |
+
raise RuntimeError(
|
| 725 |
+
"LingBotVideo SGLang native backend is not importable."
|
| 726 |
+
) from _NATIVE_BACKEND_IMPORT_ERROR
|
| 727 |
+
_restore_cuda_sdp_state(_BASELINE_CUDA_SDP_STATE)
|
| 728 |
+
try:
|
| 729 |
+
register_lingbot_native_pipeline()
|
| 730 |
+
diffusers_pipe = _load_diffusers_pipe(
|
| 731 |
+
model_dir,
|
| 732 |
+
dtype_map,
|
| 733 |
+
mode=mode,
|
| 734 |
+
transformer_subfolder=transformer_subfolder,
|
| 735 |
+
defer_transformer_to_device=defer_transformer_to_device,
|
| 736 |
+
)
|
| 737 |
+
return LingBotVideoNativePipeline.from_diffusers_pipe(
|
| 738 |
+
diffusers_pipe,
|
| 739 |
+
model_path=model_dir,
|
| 740 |
+
)
|
| 741 |
+
finally:
|
| 742 |
+
_restore_cuda_sdp_state(_BASELINE_CUDA_SDP_STATE)
|
| 743 |
+
|
| 744 |
+
|
| 745 |
+
def _load_pipe(
|
| 746 |
+
args: argparse.Namespace,
|
| 747 |
+
dtype_map: dict[str, torch.dtype],
|
| 748 |
+
*,
|
| 749 |
+
defer_transformer_to_device: bool = False,
|
| 750 |
+
):
|
| 751 |
+
model_dir = Path(args.model_dir).resolve()
|
| 752 |
+
if args.engine == "diffusers":
|
| 753 |
+
return (
|
| 754 |
+
_load_diffusers_pipe(
|
| 755 |
+
model_dir,
|
| 756 |
+
dtype_map,
|
| 757 |
+
mode=args.mode,
|
| 758 |
+
transformer_subfolder=args.transformer_subfolder,
|
| 759 |
+
defer_transformer_to_device=defer_transformer_to_device,
|
| 760 |
+
),
|
| 761 |
+
"diffusers-reference",
|
| 762 |
+
)
|
| 763 |
+
if args.engine == "sglang-native":
|
| 764 |
+
return (
|
| 765 |
+
_load_sglang_native_pipe(
|
| 766 |
+
model_dir,
|
| 767 |
+
dtype_map,
|
| 768 |
+
mode=args.mode,
|
| 769 |
+
transformer_subfolder=args.transformer_subfolder,
|
| 770 |
+
defer_transformer_to_device=defer_transformer_to_device,
|
| 771 |
+
),
|
| 772 |
+
"sglang-native",
|
| 773 |
+
)
|
| 774 |
+
raise ValueError(f"unsupported engine: {args.engine}")
|
| 775 |
+
|
| 776 |
+
|
| 777 |
+
def _refiner_model_available(
|
| 778 |
+
model_dir: str | None,
|
| 779 |
+
transformer_subfolder: str = "refiner",
|
| 780 |
+
) -> bool:
|
| 781 |
+
if not model_dir:
|
| 782 |
+
return False
|
| 783 |
+
root = Path(model_dir)
|
| 784 |
+
if not root.is_dir():
|
| 785 |
+
return False
|
| 786 |
+
required = ("model_index.json", "scheduler", "text_encoder", "vae", transformer_subfolder)
|
| 787 |
+
return all((root / name).exists() for name in required)
|
| 788 |
+
|
| 789 |
+
|
| 790 |
+
def _refiner_skip_reason(
|
| 791 |
+
requested: bool,
|
| 792 |
+
model_dir: str | None,
|
| 793 |
+
transformer_subfolder: str = "refiner",
|
| 794 |
+
) -> str | None:
|
| 795 |
+
if not requested:
|
| 796 |
+
return None
|
| 797 |
+
if not model_dir:
|
| 798 |
+
return "missing_refiner_model_dir"
|
| 799 |
+
if not _refiner_model_available(model_dir, transformer_subfolder):
|
| 800 |
+
return f"missing_refiner_component:{transformer_subfolder}"
|
| 801 |
+
return None
|
| 802 |
+
|
| 803 |
+
|
| 804 |
+
def _maybe_preload_refiner(
|
| 805 |
+
args: argparse.Namespace,
|
| 806 |
+
dtype_map: dict[str, torch.dtype],
|
| 807 |
+
device: torch.device,
|
| 808 |
+
context_parallel_rank: int,
|
| 809 |
+
context_parallel_mesh: DeviceMesh | None,
|
| 810 |
+
defer_transformer_to_device: bool,
|
| 811 |
+
) -> dict[str, Any]:
|
| 812 |
+
requested = bool(args.run_refiner)
|
| 813 |
+
available = (
|
| 814 |
+
_refiner_model_available(args.refiner_model_dir, args.refiner_transformer_subfolder)
|
| 815 |
+
if requested
|
| 816 |
+
else False
|
| 817 |
+
)
|
| 818 |
+
state: dict[str, Any] = {
|
| 819 |
+
"refiner_requested": requested,
|
| 820 |
+
"refiner_available": available,
|
| 821 |
+
"refiner_skipped_reason": _refiner_skip_reason(
|
| 822 |
+
requested,
|
| 823 |
+
args.refiner_model_dir,
|
| 824 |
+
args.refiner_transformer_subfolder,
|
| 825 |
+
),
|
| 826 |
+
"pipe": None,
|
| 827 |
+
"engine_name": None,
|
| 828 |
+
"component_dtypes": None,
|
| 829 |
+
}
|
| 830 |
+
if not requested or not available:
|
| 831 |
+
return state
|
| 832 |
+
|
| 833 |
+
refiner_args = argparse.Namespace(**vars(args))
|
| 834 |
+
refiner_args.model_dir = args.refiner_model_dir
|
| 835 |
+
refiner_args.transformer_subfolder = args.refiner_transformer_subfolder
|
| 836 |
+
refiner_args.vae_dtype = args.refiner_vae_dtype
|
| 837 |
+
# The refiner always samples through the text-only t2v pipeline; ti2v
|
| 838 |
+
# first-frame conditioning is injected as a clean frame-0 latent
|
| 839 |
+
# (`cond_latent`), not through the image-to-video pipeline.
|
| 840 |
+
refiner_args.mode = "t2v"
|
| 841 |
+
refiner_dtype_map = dict(dtype_map)
|
| 842 |
+
refiner_dtype_map["vae"] = _parse_dtype(args.refiner_vae_dtype)
|
| 843 |
+
refiner_pipe, refiner_engine_name = _load_pipe(
|
| 844 |
+
refiner_args,
|
| 845 |
+
refiner_dtype_map,
|
| 846 |
+
defer_transformer_to_device=defer_transformer_to_device,
|
| 847 |
+
)
|
| 848 |
+
refiner_component_dtypes = _component_dtypes(refiner_pipe)
|
| 849 |
+
expected_refiner_vae_dtype = _dtype_name(refiner_dtype_map["vae"])
|
| 850 |
+
if refiner_component_dtypes.get("vae") != expected_refiner_vae_dtype:
|
| 851 |
+
raise AssertionError(
|
| 852 |
+
f"Refiner VAE dtype mismatch: requested {expected_refiner_vae_dtype}, "
|
| 853 |
+
f"got {refiner_component_dtypes}"
|
| 854 |
+
)
|
| 855 |
+
if args.context_parallel_degree > 1:
|
| 856 |
+
_enable_context_parallel(
|
| 857 |
+
refiner_pipe.transformer,
|
| 858 |
+
args.context_parallel_degree,
|
| 859 |
+
args.context_parallel_ulysses_anything,
|
| 860 |
+
context_parallel_rank,
|
| 861 |
+
device,
|
| 862 |
+
context_parallel_mesh,
|
| 863 |
+
args.cfg_parallel_degree == 1,
|
| 864 |
+
)
|
| 865 |
+
state.update(
|
| 866 |
+
{
|
| 867 |
+
"pipe": refiner_pipe,
|
| 868 |
+
"engine_name": refiner_engine_name,
|
| 869 |
+
"component_dtypes": refiner_component_dtypes,
|
| 870 |
+
}
|
| 871 |
+
)
|
| 872 |
+
return state
|
| 873 |
+
|
| 874 |
+
|
| 875 |
+
def _extract_frames(result: Any) -> np.ndarray:
|
| 876 |
+
frames = result.frames if hasattr(result, "frames") else result[0]
|
| 877 |
+
if isinstance(frames, torch.Tensor):
|
| 878 |
+
tensor = frames.detach().cpu().float()
|
| 879 |
+
if tensor.ndim == 5:
|
| 880 |
+
# Accept B,T,C,H,W or B,C,T,H,W.
|
| 881 |
+
if tensor.shape[2] in (1, 3, 4):
|
| 882 |
+
tensor = tensor[0].permute(0, 2, 3, 1)
|
| 883 |
+
else:
|
| 884 |
+
tensor = tensor[0].permute(1, 2, 3, 0)
|
| 885 |
+
elif tensor.ndim == 4:
|
| 886 |
+
if tensor.shape[1] in (1, 3, 4):
|
| 887 |
+
tensor = tensor.permute(0, 2, 3, 1)
|
| 888 |
+
return tensor.numpy()
|
| 889 |
+
if isinstance(frames, list):
|
| 890 |
+
return np.asarray(frames[0])
|
| 891 |
+
return np.asarray(frames)
|
| 892 |
+
|
| 893 |
+
|
| 894 |
+
def _save_frames(
|
| 895 |
+
frames: np.ndarray,
|
| 896 |
+
mode: str,
|
| 897 |
+
output: Path,
|
| 898 |
+
fps: int,
|
| 899 |
+
) -> None:
|
| 900 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 901 |
+
|
| 902 |
+
if mode == "t2i":
|
| 903 |
+
Image.fromarray((frames[0] * 255).clip(0, 255).astype(np.uint8)).save(output)
|
| 904 |
+
else:
|
| 905 |
+
if export_to_video is None:
|
| 906 |
+
raise RuntimeError("diffusers.utils.export_to_video is not importable.") from _DIFFUSERS_IMPORT_ERROR
|
| 907 |
+
export_to_video(frames, str(output), fps=fps)
|
| 908 |
+
|
| 909 |
+
print(f"saved {output} shape={tuple(frames.shape)}", flush=True)
|
| 910 |
+
|
| 911 |
+
|
| 912 |
+
def _enable_context_parallel(
|
| 913 |
+
transformer: torch.nn.Module,
|
| 914 |
+
degree: int,
|
| 915 |
+
ulysses_anything: bool,
|
| 916 |
+
rank: int,
|
| 917 |
+
device: torch.device,
|
| 918 |
+
mesh: DeviceMesh | None,
|
| 919 |
+
use_native_mesh: bool,
|
| 920 |
+
) -> None:
|
| 921 |
+
if degree <= 1:
|
| 922 |
+
return
|
| 923 |
+
if (
|
| 924 |
+
ContextParallelConfig is None
|
| 925 |
+
or ParallelConfig is None
|
| 926 |
+
or apply_context_parallel is None
|
| 927 |
+
or Attention is None
|
| 928 |
+
or MochiAttention is None
|
| 929 |
+
or AttentionModuleMixin is None
|
| 930 |
+
):
|
| 931 |
+
raise RuntimeError("diffusers context-parallel helpers are unavailable.") from _DIFFUSERS_IMPORT_ERROR
|
| 932 |
+
cp_config = ContextParallelConfig(
|
| 933 |
+
ulysses_degree=degree,
|
| 934 |
+
ulysses_anything=ulysses_anything,
|
| 935 |
+
)
|
| 936 |
+
if use_native_mesh:
|
| 937 |
+
transformer.enable_parallelism(config=cp_config)
|
| 938 |
+
return
|
| 939 |
+
|
| 940 |
+
if mesh is None:
|
| 941 |
+
raise ValueError("A context-parallel mesh is required when context_parallel_degree > 1.")
|
| 942 |
+
|
| 943 |
+
config = ParallelConfig(context_parallel_config=cp_config)
|
| 944 |
+
config.setup(rank, degree, device, mesh=mesh)
|
| 945 |
+
if cp_config.ring_degree == 1 and int(mesh.mesh.numel()) != degree:
|
| 946 |
+
cp_config._flattened_mesh = cp_config._ulysses_mesh._flatten()
|
| 947 |
+
transformer._parallel_config = config
|
| 948 |
+
|
| 949 |
+
attention_classes = (Attention, MochiAttention, AttentionModuleMixin)
|
| 950 |
+
for module in transformer.modules():
|
| 951 |
+
if not isinstance(module, attention_classes):
|
| 952 |
+
continue
|
| 953 |
+
processor = module.processor
|
| 954 |
+
if processor is not None and hasattr(processor, "_parallel_config"):
|
| 955 |
+
processor._parallel_config = config
|
| 956 |
+
|
| 957 |
+
cp_plan = getattr(transformer, "_cp_plan", None)
|
| 958 |
+
if cp_plan is None:
|
| 959 |
+
raise ValueError("Transformer does not define a context-parallel plan.")
|
| 960 |
+
apply_context_parallel(transformer, cp_config, cp_plan)
|
| 961 |
+
|
| 962 |
+
|
| 963 |
+
def main() -> None:
|
| 964 |
+
parser = argparse.ArgumentParser()
|
| 965 |
+
parser.add_argument("--model_dir", required=True, help="LingBot-Video model root directory.")
|
| 966 |
+
parser.add_argument(
|
| 967 |
+
"--backend",
|
| 968 |
+
choices=["diffusers", "sglang"],
|
| 969 |
+
default=None,
|
| 970 |
+
help=(
|
| 971 |
+
"public backend selector; `sglang` uses SGLang Diffusion when "
|
| 972 |
+
"available and falls back to diffusers otherwise"
|
| 973 |
+
),
|
| 974 |
+
)
|
| 975 |
+
parser.add_argument(
|
| 976 |
+
"--engine",
|
| 977 |
+
choices=["sglang-native", "diffusers"],
|
| 978 |
+
default=None,
|
| 979 |
+
help=argparse.SUPPRESS,
|
| 980 |
+
)
|
| 981 |
+
parser.add_argument("--mode", choices=["t2i", "t2v", "ti2v"], required=True)
|
| 982 |
+
parser.add_argument("--prompt", default=None)
|
| 983 |
+
parser.add_argument("--prompt_json", default=None)
|
| 984 |
+
parser.add_argument("--negative_prompt", default=None,
|
| 985 |
+
help="negative prompt; if unset, uses the mode's default "
|
| 986 |
+
"(image default for t2i, video default for t2v/ti2v)")
|
| 987 |
+
parser.add_argument(
|
| 988 |
+
"--negative_prompt_json",
|
| 989 |
+
default=None,
|
| 990 |
+
help="path to the JSON file produced by rewriter/auto_negative.py",
|
| 991 |
+
)
|
| 992 |
+
parser.add_argument("--image", default=None)
|
| 993 |
+
parser.add_argument("--output", required=True)
|
| 994 |
+
parser.add_argument("--resolution", default=None)
|
| 995 |
+
parser.add_argument("--ratio", default=None)
|
| 996 |
+
parser.add_argument("--duration", type=float, default=None)
|
| 997 |
+
parser.add_argument("--height", type=int, default=192)
|
| 998 |
+
parser.add_argument("--width", type=int, default=320)
|
| 999 |
+
parser.add_argument("--num_frames", type=int, default=9)
|
| 1000 |
+
parser.add_argument("--steps", type=int, default=40)
|
| 1001 |
+
parser.add_argument("--guidance_scale", type=float, default=3.0)
|
| 1002 |
+
parser.add_argument("--shift", type=float, default=3.0)
|
| 1003 |
+
parser.add_argument("--seed", type=int, default=42)
|
| 1004 |
+
parser.add_argument("--fps", type=int, default=24)
|
| 1005 |
+
parser.add_argument("--default_dtype", default="bf16")
|
| 1006 |
+
parser.add_argument("--transformer_dtype", default="bf16")
|
| 1007 |
+
parser.add_argument("--transformer_subfolder", default="transformer")
|
| 1008 |
+
parser.add_argument("--text_encoder_dtype", default="bf16")
|
| 1009 |
+
parser.add_argument("--vae_dtype", default="fp32")
|
| 1010 |
+
parser.add_argument("--diffusers_attn_backend", default=os.environ.get("DIFFUSERS_ATTN_BACKEND", ""))
|
| 1011 |
+
parser.add_argument("--allow_tf32", action=argparse.BooleanOptionalAction, default=True)
|
| 1012 |
+
parser.add_argument(
|
| 1013 |
+
"--quiet_progress",
|
| 1014 |
+
action="store_true",
|
| 1015 |
+
help="Disable model-loading logs and denoising progress bars.",
|
| 1016 |
+
)
|
| 1017 |
+
parser.add_argument("--cfg_parallel_degree", type=int, default=1)
|
| 1018 |
+
parser.add_argument("--context_parallel_degree", type=int, default=1)
|
| 1019 |
+
parser.add_argument("--context_parallel_ulysses_anything", action="store_true")
|
| 1020 |
+
parser.add_argument(
|
| 1021 |
+
"--enable_fsdp_inference",
|
| 1022 |
+
action="store_true",
|
| 1023 |
+
help="Shard the base/refiner DiT transformers with PyTorch composable FSDP2.",
|
| 1024 |
+
)
|
| 1025 |
+
parser.add_argument("--batch_cfg", action="store_true")
|
| 1026 |
+
parser.add_argument("--null_cond_clone_zero", action="store_true")
|
| 1027 |
+
parser.add_argument("--reuse_condition_features", action="store_true")
|
| 1028 |
+
parser.add_argument("--run_refiner", action="store_true")
|
| 1029 |
+
parser.add_argument("--refiner_model_dir", default=None)
|
| 1030 |
+
parser.add_argument("--refiner_transformer_subfolder", default="refiner")
|
| 1031 |
+
parser.add_argument("--refiner_output", default=None)
|
| 1032 |
+
parser.add_argument("--refiner_height", type=int, default=1088)
|
| 1033 |
+
parser.add_argument("--refiner_width", type=int, default=1920)
|
| 1034 |
+
parser.add_argument("--refiner_steps", type=int, default=8)
|
| 1035 |
+
parser.add_argument("--refiner_guidance_scale", type=float, default=3.0)
|
| 1036 |
+
parser.add_argument("--refiner_shift", type=float, default=3.0)
|
| 1037 |
+
parser.add_argument("--refiner_t_thresh", type=float, default=0.85)
|
| 1038 |
+
parser.add_argument("--refiner_sigma_tail_steps", type=int, default=2)
|
| 1039 |
+
parser.add_argument("--refiner_fps", type=int, default=24)
|
| 1040 |
+
parser.add_argument("--refiner_sample_fps", type=int, default=24)
|
| 1041 |
+
parser.add_argument("--refiner_max_video_frames", type=int, default=None)
|
| 1042 |
+
parser.add_argument("--refiner_vae_dtype", default="fp32")
|
| 1043 |
+
parser.add_argument("--refiner_batch_cfg", action="store_true")
|
| 1044 |
+
parser.add_argument("--refiner_no_null_cond_clone_zero", action="store_true")
|
| 1045 |
+
parser.add_argument("--refiner_offload_vae_during_denoise", action="store_true")
|
| 1046 |
+
args = parser.parse_args()
|
| 1047 |
+
if args.quiet_progress:
|
| 1048 |
+
os.environ["LINGBOT_QUIET_PROGRESS"] = "1"
|
| 1049 |
+
args.engine = resolve_backend_engine(
|
| 1050 |
+
engine=args.engine,
|
| 1051 |
+
backend=args.backend,
|
| 1052 |
+
stderr=sys.stderr,
|
| 1053 |
+
)
|
| 1054 |
+
args.negative_prompt = resolve_negative_prompt_arg(
|
| 1055 |
+
args.negative_prompt,
|
| 1056 |
+
args.negative_prompt_json,
|
| 1057 |
+
)
|
| 1058 |
+
|
| 1059 |
+
prompt_sample = None
|
| 1060 |
+
if args.prompt_json:
|
| 1061 |
+
prompt_sample = _load_prompt_sample(Path(args.prompt_json))
|
| 1062 |
+
args.prompt = _caption_from_sample(prompt_sample)
|
| 1063 |
+
if args.duration is None and "duration" in prompt_sample:
|
| 1064 |
+
args.duration = float(prompt_sample["duration"])
|
| 1065 |
+
if args.prompt is None:
|
| 1066 |
+
raise ValueError(
|
| 1067 |
+
"Provide `--prompt_json <path_to_structured_prompt.json>` or an explicit `--prompt`."
|
| 1068 |
+
)
|
| 1069 |
+
if args.resolution or args.ratio:
|
| 1070 |
+
if not args.resolution or not args.ratio:
|
| 1071 |
+
raise ValueError("`--resolution` and `--ratio` must be provided together.")
|
| 1072 |
+
args.height, args.width = _height_width_from_bucket(args.resolution, args.ratio)
|
| 1073 |
+
if args.negative_prompt is None:
|
| 1074 |
+
args.negative_prompt = (
|
| 1075 |
+
DEFAULT_NEGATIVE_PROMPT_IMAGE if args.mode == "t2i" else DEFAULT_NEGATIVE_PROMPT
|
| 1076 |
+
)
|
| 1077 |
+
if args.mode != "t2i" and args.duration is not None:
|
| 1078 |
+
if num_frames_from_duration is None:
|
| 1079 |
+
raise RuntimeError("num_frames_from_duration is not importable.") from _LINGBOT_PIPELINE_IMPORT_ERROR
|
| 1080 |
+
args.num_frames = num_frames_from_duration(args.duration, args.fps)
|
| 1081 |
+
args.run_refiner = bool(args.run_refiner or args.refiner_model_dir)
|
| 1082 |
+
if args.run_refiner:
|
| 1083 |
+
if effective_refiner_model_dir is None:
|
| 1084 |
+
raise RuntimeError("model path helpers are not importable.") from _LINGBOT_PIPELINE_IMPORT_ERROR
|
| 1085 |
+
args.refiner_model_dir = str(effective_refiner_model_dir(args))
|
| 1086 |
+
|
| 1087 |
+
(
|
| 1088 |
+
rank,
|
| 1089 |
+
local_rank,
|
| 1090 |
+
world_size,
|
| 1091 |
+
cfg_parallel_group,
|
| 1092 |
+
context_parallel_mesh,
|
| 1093 |
+
cfg_branch_rank,
|
| 1094 |
+
context_parallel_rank,
|
| 1095 |
+
) = _init_parallel(
|
| 1096 |
+
args.cfg_parallel_degree,
|
| 1097 |
+
args.context_parallel_degree,
|
| 1098 |
+
args.enable_fsdp_inference,
|
| 1099 |
+
)
|
| 1100 |
+
if args.cfg_parallel_degree > 1 and args.batch_cfg:
|
| 1101 |
+
raise ValueError("`--cfg_parallel_degree > 1` and `--batch_cfg` are mutually exclusive.")
|
| 1102 |
+
if args.refiner_model_dir and args.cfg_parallel_degree > 1 and args.refiner_batch_cfg:
|
| 1103 |
+
raise ValueError(
|
| 1104 |
+
"`--cfg_parallel_degree > 1` and `--refiner_batch_cfg` are mutually exclusive."
|
| 1105 |
+
)
|
| 1106 |
+
if args.run_refiner and args.mode == "t2i":
|
| 1107 |
+
raise ValueError("Refiner is only supported for video modes.")
|
| 1108 |
+
if args.run_refiner and args.mode == "ti2v" and not args.image:
|
| 1109 |
+
raise ValueError(
|
| 1110 |
+
"The ti2v refiner conditions on the clean first frame; `--image` is required."
|
| 1111 |
+
)
|
| 1112 |
+
if args.diffusers_attn_backend:
|
| 1113 |
+
os.environ["DIFFUSERS_ATTN_BACKEND"] = args.diffusers_attn_backend
|
| 1114 |
+
if args.allow_tf32:
|
| 1115 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 1116 |
+
torch.set_float32_matmul_precision("high")
|
| 1117 |
+
if args.mode == "t2i":
|
| 1118 |
+
args.num_frames = 1
|
| 1119 |
+
|
| 1120 |
+
fsdp_mesh = None
|
| 1121 |
+
if args.enable_fsdp_inference:
|
| 1122 |
+
if init_fsdp_inference_mesh is None:
|
| 1123 |
+
raise RuntimeError("FSDP inference helpers are not importable.") from _LINGBOT_PIPELINE_IMPORT_ERROR
|
| 1124 |
+
fsdp_mesh = init_fsdp_inference_mesh()
|
| 1125 |
+
|
| 1126 |
+
dtype_map = _make_dtype_map(args)
|
| 1127 |
+
defer_transformer_to_device = fsdp_mesh is not None
|
| 1128 |
+
pipe, engine_name = _load_pipe(
|
| 1129 |
+
args,
|
| 1130 |
+
dtype_map,
|
| 1131 |
+
defer_transformer_to_device=defer_transformer_to_device,
|
| 1132 |
+
)
|
| 1133 |
+
_configure_pipeline_logs(pipe)
|
| 1134 |
+
component_dtypes = _component_dtypes(pipe)
|
| 1135 |
+
expected_vae_dtype = _dtype_name(dtype_map["vae"])
|
| 1136 |
+
if component_dtypes.get("vae") != expected_vae_dtype:
|
| 1137 |
+
raise AssertionError(
|
| 1138 |
+
f"VAE dtype mismatch: requested {expected_vae_dtype}, got {component_dtypes}"
|
| 1139 |
+
)
|
| 1140 |
+
device = _default_device()
|
| 1141 |
+
if args.context_parallel_degree > 1:
|
| 1142 |
+
_enable_context_parallel(
|
| 1143 |
+
pipe.transformer,
|
| 1144 |
+
args.context_parallel_degree,
|
| 1145 |
+
args.context_parallel_ulysses_anything,
|
| 1146 |
+
context_parallel_rank,
|
| 1147 |
+
device,
|
| 1148 |
+
context_parallel_mesh,
|
| 1149 |
+
args.cfg_parallel_degree == 1,
|
| 1150 |
+
)
|
| 1151 |
+
base_fsdp_info = _apply_fsdp_inference_if_requested(pipe, args.enable_fsdp_inference, fsdp_mesh)
|
| 1152 |
+
|
| 1153 |
+
refiner_state = _maybe_preload_refiner(
|
| 1154 |
+
args,
|
| 1155 |
+
dtype_map,
|
| 1156 |
+
device,
|
| 1157 |
+
context_parallel_rank,
|
| 1158 |
+
context_parallel_mesh,
|
| 1159 |
+
defer_transformer_to_device,
|
| 1160 |
+
)
|
| 1161 |
+
if refiner_state["pipe"] is not None:
|
| 1162 |
+
_configure_pipeline_logs(refiner_state["pipe"])
|
| 1163 |
+
refiner_fsdp_info = None
|
| 1164 |
+
if refiner_state["pipe"] is not None:
|
| 1165 |
+
refiner_fsdp_info = _apply_fsdp_inference_if_requested(
|
| 1166 |
+
refiner_state["pipe"],
|
| 1167 |
+
args.enable_fsdp_inference,
|
| 1168 |
+
fsdp_mesh,
|
| 1169 |
+
)
|
| 1170 |
+
if rank == 0 and refiner_state["refiner_requested"] and not refiner_state["refiner_available"]:
|
| 1171 |
+
print(
|
| 1172 |
+
"WARNING: refiner requested but unavailable; "
|
| 1173 |
+
f"reason={refiner_state['refiner_skipped_reason']} "
|
| 1174 |
+
f"refiner_model_dir={args.refiner_model_dir}",
|
| 1175 |
+
flush=True,
|
| 1176 |
+
)
|
| 1177 |
+
|
| 1178 |
+
generator = torch.Generator(device=device).manual_seed(args.seed)
|
| 1179 |
+
condition_cache = None
|
| 1180 |
+
input_image = None
|
| 1181 |
+
ti2v_image_tensor_cpu = None
|
| 1182 |
+
should_cache_conditions = (
|
| 1183 |
+
args.reuse_condition_features
|
| 1184 |
+
or bool(refiner_state["refiner_available"])
|
| 1185 |
+
or args.batch_cfg
|
| 1186 |
+
or args.null_cond_clone_zero
|
| 1187 |
+
)
|
| 1188 |
+
if args.mode == "ti2v":
|
| 1189 |
+
input_image = (
|
| 1190 |
+
Image.open(args.image).convert("RGB")
|
| 1191 |
+
if args.image
|
| 1192 |
+
else _make_default_image(args.height, args.width)
|
| 1193 |
+
)
|
| 1194 |
+
if should_cache_conditions:
|
| 1195 |
+
condition_cache, ti2v_image_tensor_cpu = _cache_ti2v_prompt_conditions(
|
| 1196 |
+
pipe,
|
| 1197 |
+
args.prompt,
|
| 1198 |
+
args.negative_prompt,
|
| 1199 |
+
input_image,
|
| 1200 |
+
height=args.height,
|
| 1201 |
+
width=args.width,
|
| 1202 |
+
device=device,
|
| 1203 |
+
null_cond_clone_zero=args.null_cond_clone_zero,
|
| 1204 |
+
)
|
| 1205 |
+
elif should_cache_conditions:
|
| 1206 |
+
condition_cache = _cache_prompt_conditions(
|
| 1207 |
+
pipe,
|
| 1208 |
+
args.prompt,
|
| 1209 |
+
args.negative_prompt,
|
| 1210 |
+
device=device,
|
| 1211 |
+
null_cond_clone_zero=args.null_cond_clone_zero,
|
| 1212 |
+
)
|
| 1213 |
+
call_kwargs = dict(
|
| 1214 |
+
prompt=args.prompt,
|
| 1215 |
+
negative_prompt=args.negative_prompt,
|
| 1216 |
+
height=args.height,
|
| 1217 |
+
width=args.width,
|
| 1218 |
+
num_frames=args.num_frames,
|
| 1219 |
+
num_inference_steps=args.steps,
|
| 1220 |
+
guidance_scale=args.guidance_scale,
|
| 1221 |
+
shift=args.shift,
|
| 1222 |
+
generator=generator,
|
| 1223 |
+
output_type="np",
|
| 1224 |
+
**_condition_call_kwargs(condition_cache, device),
|
| 1225 |
+
)
|
| 1226 |
+
call_kwargs["batch_cfg"] = args.batch_cfg
|
| 1227 |
+
if args.mode != "ti2v":
|
| 1228 |
+
call_kwargs["null_cond_clone_zero"] = args.null_cond_clone_zero
|
| 1229 |
+
if args.mode == "ti2v":
|
| 1230 |
+
call_kwargs["image"] = input_image
|
| 1231 |
+
if ti2v_image_tensor_cpu is not None:
|
| 1232 |
+
call_kwargs["image_tensor"] = ti2v_image_tensor_cpu.to(device=device)
|
| 1233 |
+
if args.cfg_parallel_degree > 1:
|
| 1234 |
+
call_kwargs["cfg_parallel_group"] = cfg_parallel_group
|
| 1235 |
+
|
| 1236 |
+
if rank == 0:
|
| 1237 |
+
print(
|
| 1238 |
+
"runtime "
|
| 1239 |
+
f"engine={engine_name} model_dir={args.model_dir} mode={args.mode} device={device} "
|
| 1240 |
+
f"rank={rank}/{world_size} local_rank={local_rank} "
|
| 1241 |
+
f"cfg_branch_rank={cfg_branch_rank} context_parallel_rank={context_parallel_rank} "
|
| 1242 |
+
f"cfg_parallel_degree={args.cfg_parallel_degree} "
|
| 1243 |
+
f"context_parallel_degree={args.context_parallel_degree} "
|
| 1244 |
+
f"height={args.height} width={args.width} frames={args.num_frames} steps={args.steps} "
|
| 1245 |
+
f"guidance={args.guidance_scale} shift={args.shift} seed={args.seed} "
|
| 1246 |
+
f"attn_backend={os.environ.get('DIFFUSERS_ATTN_BACKEND')} "
|
| 1247 |
+
f"allow_tf32={torch.backends.cuda.matmul.allow_tf32} "
|
| 1248 |
+
f"fsdp_inference={base_fsdp_info} "
|
| 1249 |
+
f"component_dtypes={component_dtypes}",
|
| 1250 |
+
flush=True,
|
| 1251 |
+
)
|
| 1252 |
+
|
| 1253 |
+
with torch.no_grad():
|
| 1254 |
+
result = pipe(**call_kwargs)
|
| 1255 |
+
frames = _extract_frames(result) if rank == 0 else None
|
| 1256 |
+
if rank == 0:
|
| 1257 |
+
_save_frames(
|
| 1258 |
+
frames,
|
| 1259 |
+
args.mode,
|
| 1260 |
+
Path(args.output),
|
| 1261 |
+
args.fps,
|
| 1262 |
+
)
|
| 1263 |
+
|
| 1264 |
+
if not refiner_state["refiner_requested"] or not refiner_state["refiner_available"]:
|
| 1265 |
+
_sync_parallel_if_needed()
|
| 1266 |
+
_destroy_parallel_if_needed()
|
| 1267 |
+
return
|
| 1268 |
+
|
| 1269 |
+
_sync_parallel_if_needed()
|
| 1270 |
+
del result, frames, call_kwargs
|
| 1271 |
+
gc.collect()
|
| 1272 |
+
if torch.cuda.is_available():
|
| 1273 |
+
torch.cuda.empty_cache()
|
| 1274 |
+
|
| 1275 |
+
if load_refiner_video_tensor is None or prepare_refiner_latent is None:
|
| 1276 |
+
raise RuntimeError("Refiner helpers are not importable.") from _LINGBOT_PIPELINE_IMPORT_ERROR
|
| 1277 |
+
refiner_pipe = refiner_state["pipe"]
|
| 1278 |
+
if refiner_pipe is None:
|
| 1279 |
+
raise RuntimeError("Refiner was marked available but was not preloaded.")
|
| 1280 |
+
refiner_engine_name = str(refiner_state["engine_name"])
|
| 1281 |
+
refiner_component_dtypes = refiner_state["component_dtypes"]
|
| 1282 |
+
|
| 1283 |
+
refiner_generator = torch.Generator(device=device).manual_seed(args.seed)
|
| 1284 |
+
lowres_video, lowres_meta = load_refiner_video_tensor(
|
| 1285 |
+
args.output,
|
| 1286 |
+
args.refiner_height,
|
| 1287 |
+
args.refiner_width,
|
| 1288 |
+
sample_fps=args.refiner_sample_fps,
|
| 1289 |
+
vae_tc=getattr(refiner_pipe, "vae_scale_factor_temporal", 4),
|
| 1290 |
+
max_frames=args.refiner_max_video_frames,
|
| 1291 |
+
)
|
| 1292 |
+
|
| 1293 |
+
# TI2V always refines with the clean input first frame as a fixed frame-0
|
| 1294 |
+
# latent: injected into the initial latent here and re-clamped after every
|
| 1295 |
+
# scheduler step via `cond_latent`.
|
| 1296 |
+
first_frame_condition_enabled = args.mode == "ti2v"
|
| 1297 |
+
refiner_cond_latent = None
|
| 1298 |
+
with torch.no_grad():
|
| 1299 |
+
x_up = refiner_pipe.encode_video_latent(lowres_video, generator=refiner_generator)
|
| 1300 |
+
if first_frame_condition_enabled:
|
| 1301 |
+
condition_pixels = load_first_frame_condition_tensor(
|
| 1302 |
+
args.image,
|
| 1303 |
+
args.refiner_height,
|
| 1304 |
+
args.refiner_width,
|
| 1305 |
+
geometry_height=args.height,
|
| 1306 |
+
geometry_width=args.width,
|
| 1307 |
+
)
|
| 1308 |
+
clean_x0 = refiner_pipe.encode_video_latent(
|
| 1309 |
+
condition_pixels,
|
| 1310 |
+
generator=refiner_generator,
|
| 1311 |
+
)
|
| 1312 |
+
refiner_cond_latent = clean_x0[:, :, 0:1].contiguous()
|
| 1313 |
+
x_up[:, :, 0:1] = refiner_cond_latent.to(dtype=x_up.dtype)
|
| 1314 |
+
del condition_pixels, clean_x0
|
| 1315 |
+
noise = torch.randn(
|
| 1316 |
+
x_up.shape,
|
| 1317 |
+
device=device,
|
| 1318 |
+
dtype=x_up.dtype,
|
| 1319 |
+
generator=refiner_generator,
|
| 1320 |
+
)
|
| 1321 |
+
initial_latent = prepare_refiner_latent(x_up, noise, args.refiner_t_thresh)
|
| 1322 |
+
del lowres_video, x_up, noise
|
| 1323 |
+
refiner_null_clone_zero = not args.refiner_no_null_cond_clone_zero
|
| 1324 |
+
# The ti2v condition cache carries image tokens; the refiner conditions on
|
| 1325 |
+
# text only, so it rebuilds t2v-style embeddings instead of reusing it.
|
| 1326 |
+
refiner_condition_cache = None if args.mode == "ti2v" else condition_cache
|
| 1327 |
+
should_cache_refiner_conditions = not (
|
| 1328 |
+
args.cfg_parallel_degree > 1 and cfg_branch_rank == 1 and not refiner_null_clone_zero
|
| 1329 |
+
)
|
| 1330 |
+
if refiner_condition_cache is None and should_cache_refiner_conditions:
|
| 1331 |
+
refiner_condition_cache = _cache_prompt_conditions(
|
| 1332 |
+
refiner_pipe,
|
| 1333 |
+
args.prompt,
|
| 1334 |
+
args.negative_prompt,
|
| 1335 |
+
device=device,
|
| 1336 |
+
null_cond_clone_zero=refiner_null_clone_zero,
|
| 1337 |
+
)
|
| 1338 |
+
elif refiner_null_clone_zero:
|
| 1339 |
+
refiner_condition_cache = dict(refiner_condition_cache)
|
| 1340 |
+
refiner_condition_cache["negative_prompt_embeds"] = torch.zeros_like(
|
| 1341 |
+
refiner_condition_cache["prompt_embeds"]
|
| 1342 |
+
)
|
| 1343 |
+
refiner_condition_cache["negative_prompt_mask"] = refiner_condition_cache["prompt_mask"].clone()
|
| 1344 |
+
|
| 1345 |
+
if rank == 0:
|
| 1346 |
+
print(
|
| 1347 |
+
"runtime "
|
| 1348 |
+
f"engine={refiner_engine_name} model_dir={args.refiner_model_dir} mode=refiner "
|
| 1349 |
+
f"height={args.refiner_height} width={args.refiner_width} "
|
| 1350 |
+
f"frames={lowres_meta['sample_frame']} steps={args.refiner_steps} "
|
| 1351 |
+
f"guidance={args.refiner_guidance_scale} shift={args.refiner_shift} "
|
| 1352 |
+
f"t_thresh={args.refiner_t_thresh} tail_steps={args.refiner_sigma_tail_steps} "
|
| 1353 |
+
f"seed={args.seed} "
|
| 1354 |
+
f"batch_cfg={args.refiner_batch_cfg} "
|
| 1355 |
+
f"first_frame_condition={first_frame_condition_enabled} "
|
| 1356 |
+
f"fsdp_inference={refiner_fsdp_info} "
|
| 1357 |
+
f"component_dtypes={refiner_component_dtypes}",
|
| 1358 |
+
flush=True,
|
| 1359 |
+
)
|
| 1360 |
+
|
| 1361 |
+
refiner_call_kwargs = dict(
|
| 1362 |
+
prompt=args.prompt,
|
| 1363 |
+
negative_prompt=args.negative_prompt,
|
| 1364 |
+
height=args.refiner_height,
|
| 1365 |
+
width=args.refiner_width,
|
| 1366 |
+
num_frames=int(lowres_meta["sample_frame"]),
|
| 1367 |
+
num_inference_steps=args.refiner_steps,
|
| 1368 |
+
guidance_scale=args.refiner_guidance_scale,
|
| 1369 |
+
shift=args.refiner_shift,
|
| 1370 |
+
t_thresh=args.refiner_t_thresh,
|
| 1371 |
+
refiner_sigma_tail_steps=args.refiner_sigma_tail_steps,
|
| 1372 |
+
generator=refiner_generator,
|
| 1373 |
+
latents=initial_latent,
|
| 1374 |
+
output_type="np",
|
| 1375 |
+
batch_cfg=args.refiner_batch_cfg,
|
| 1376 |
+
null_cond_clone_zero=refiner_null_clone_zero,
|
| 1377 |
+
offload_vae_during_denoise=args.refiner_offload_vae_during_denoise,
|
| 1378 |
+
**_condition_call_kwargs(refiner_condition_cache, device),
|
| 1379 |
+
)
|
| 1380 |
+
if refiner_cond_latent is not None:
|
| 1381 |
+
refiner_call_kwargs["cond_latent"] = refiner_cond_latent
|
| 1382 |
+
if args.cfg_parallel_degree > 1:
|
| 1383 |
+
refiner_call_kwargs["cfg_parallel_group"] = cfg_parallel_group
|
| 1384 |
+
|
| 1385 |
+
with torch.no_grad():
|
| 1386 |
+
refiner_result = refiner_pipe(**refiner_call_kwargs)
|
| 1387 |
+
if rank == 0:
|
| 1388 |
+
refiner_frames = _extract_frames(refiner_result)
|
| 1389 |
+
_save_frames(
|
| 1390 |
+
refiner_frames,
|
| 1391 |
+
args.mode,
|
| 1392 |
+
Path(args.refiner_output or str(Path(args.output).with_name(Path(args.output).stem + "_refined.mp4"))),
|
| 1393 |
+
args.refiner_fps,
|
| 1394 |
+
)
|
| 1395 |
+
|
| 1396 |
+
_sync_parallel_if_needed()
|
| 1397 |
+
_destroy_parallel_if_needed()
|
| 1398 |
+
|
| 1399 |
+
|
| 1400 |
+
if __name__ == "__main__":
|
| 1401 |
+
main()
|
lingbot_video/scheduling_flow_unipc.py
ADDED
|
@@ -0,0 +1,796 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from typing import List, Optional, Tuple, Union
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
import torch
|
| 6 |
+
from diffusers.configuration_utils import ConfigMixin, register_to_config
|
| 7 |
+
from diffusers.schedulers.scheduling_utils import (KarrasDiffusionSchedulers,
|
| 8 |
+
SchedulerMixin,
|
| 9 |
+
SchedulerOutput)
|
| 10 |
+
from diffusers.utils import deprecate, is_scipy_available
|
| 11 |
+
|
| 12 |
+
if is_scipy_available():
|
| 13 |
+
import scipy.stats
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class FlowUniPCMultistepScheduler(SchedulerMixin, ConfigMixin):
|
| 17 |
+
"""
|
| 18 |
+
`UniPCMultistepScheduler` is a training-free framework designed for the fast sampling of diffusion models.
|
| 19 |
+
|
| 20 |
+
This model inherits from [`SchedulerMixin`] and [`ConfigMixin`]. Check the superclass documentation for the generic
|
| 21 |
+
methods the library implements for all schedulers such as loading and saving.
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
num_train_timesteps (`int`, defaults to 1000):
|
| 25 |
+
The number of diffusion steps to train the model.
|
| 26 |
+
solver_order (`int`, default `2`):
|
| 27 |
+
The UniPC order which can be any positive integer. The effective order of accuracy is `solver_order + 1`
|
| 28 |
+
due to the UniC. It is recommended to use `solver_order=2` for guided sampling, and `solver_order=3` for
|
| 29 |
+
unconditional sampling.
|
| 30 |
+
prediction_type (`str`, defaults to "flow_prediction"):
|
| 31 |
+
Prediction type of the scheduler function; must be `flow_prediction` for this scheduler, which predicts
|
| 32 |
+
the flow of the diffusion process.
|
| 33 |
+
thresholding (`bool`, defaults to `False`):
|
| 34 |
+
Whether to use the "dynamic thresholding" method. This is unsuitable for latent-space diffusion models such
|
| 35 |
+
as Stable Diffusion.
|
| 36 |
+
dynamic_thresholding_ratio (`float`, defaults to 0.995):
|
| 37 |
+
The ratio for the dynamic thresholding method. Valid only when `thresholding=True`.
|
| 38 |
+
sample_max_value (`float`, defaults to 1.0):
|
| 39 |
+
The threshold value for dynamic thresholding. Valid only when `thresholding=True` and `predict_x0=True`.
|
| 40 |
+
predict_x0 (`bool`, defaults to `True`):
|
| 41 |
+
Whether to use the updating algorithm on the predicted x0.
|
| 42 |
+
solver_type (`str`, default `bh2`):
|
| 43 |
+
Solver type for UniPC. It is recommended to use `bh1` for unconditional sampling when steps < 10, and `bh2`
|
| 44 |
+
otherwise.
|
| 45 |
+
lower_order_final (`bool`, default `True`):
|
| 46 |
+
Whether to use lower-order solvers in the final steps. Only valid for < 15 inference steps. This can
|
| 47 |
+
stabilize the sampling of DPMSolver for steps < 15, especially for steps <= 10.
|
| 48 |
+
disable_corrector (`list`, default `[]`):
|
| 49 |
+
Decides which step to disable the corrector to mitigate the misalignment between `epsilon_theta(x_t, c)`
|
| 50 |
+
and `epsilon_theta(x_t^c, c)` which can influence convergence for a large guidance scale. Corrector is
|
| 51 |
+
usually disabled during the first few steps.
|
| 52 |
+
solver_p (`SchedulerMixin`, default `None`):
|
| 53 |
+
Any other scheduler that if specified, the algorithm becomes `solver_p + UniC`.
|
| 54 |
+
use_karras_sigmas (`bool`, *optional*, defaults to `False`):
|
| 55 |
+
Whether to use Karras sigmas for step sizes in the noise schedule during the sampling process. If `True`,
|
| 56 |
+
the sigmas are determined according to a sequence of noise levels {σi}.
|
| 57 |
+
use_exponential_sigmas (`bool`, *optional*, defaults to `False`):
|
| 58 |
+
Whether to use exponential sigmas for step sizes in the noise schedule during the sampling process.
|
| 59 |
+
timestep_spacing (`str`, defaults to `"linspace"`):
|
| 60 |
+
The way the timesteps should be scaled. Refer to Table 2 of the [Common Diffusion Noise Schedules and
|
| 61 |
+
Sample Steps are Flawed](https://huggingface.co/papers/2305.08891) for more information.
|
| 62 |
+
steps_offset (`int`, defaults to 0):
|
| 63 |
+
An offset added to the inference steps, as required by some model families.
|
| 64 |
+
final_sigmas_type (`str`, defaults to `"zero"`):
|
| 65 |
+
The final `sigma` value for the noise schedule during the sampling process. If `"sigma_min"`, the final
|
| 66 |
+
sigma is the same as the last sigma in the training schedule. If `zero`, the final sigma is set to 0.
|
| 67 |
+
"""
|
| 68 |
+
|
| 69 |
+
_compatibles = [e.name for e in KarrasDiffusionSchedulers]
|
| 70 |
+
order = 1
|
| 71 |
+
|
| 72 |
+
@register_to_config
|
| 73 |
+
def __init__(
|
| 74 |
+
self,
|
| 75 |
+
num_train_timesteps: int = 1000,
|
| 76 |
+
solver_order: int = 2,
|
| 77 |
+
prediction_type: str = "flow_prediction",
|
| 78 |
+
shift: Optional[float] = 1.0,
|
| 79 |
+
use_dynamic_shifting=False,
|
| 80 |
+
thresholding: bool = False,
|
| 81 |
+
dynamic_thresholding_ratio: float = 0.995,
|
| 82 |
+
sample_max_value: float = 1.0,
|
| 83 |
+
predict_x0: bool = True,
|
| 84 |
+
solver_type: str = "bh2",
|
| 85 |
+
lower_order_final: bool = True,
|
| 86 |
+
disable_corrector: List[int] = [],
|
| 87 |
+
solver_p: SchedulerMixin = None,
|
| 88 |
+
timestep_spacing: str = "linspace",
|
| 89 |
+
steps_offset: int = 0,
|
| 90 |
+
final_sigmas_type: Optional[str] = "zero", # "zero", "sigma_min"
|
| 91 |
+
):
|
| 92 |
+
|
| 93 |
+
if solver_type not in ["bh1", "bh2"]:
|
| 94 |
+
if solver_type in ["midpoint", "heun", "logrho"]:
|
| 95 |
+
self.register_to_config(solver_type="bh2")
|
| 96 |
+
else:
|
| 97 |
+
raise NotImplementedError(
|
| 98 |
+
f"{solver_type} is not implemented for {self.__class__}")
|
| 99 |
+
|
| 100 |
+
self.predict_x0 = predict_x0
|
| 101 |
+
# setable values
|
| 102 |
+
self.num_inference_steps = None
|
| 103 |
+
alphas = np.linspace(1, 1 / num_train_timesteps,
|
| 104 |
+
num_train_timesteps)[::-1].copy()
|
| 105 |
+
sigmas = 1.0 - alphas
|
| 106 |
+
sigmas = torch.from_numpy(sigmas).to(dtype=torch.float32)
|
| 107 |
+
|
| 108 |
+
if not use_dynamic_shifting:
|
| 109 |
+
# when use_dynamic_shifting is True, we apply the timestep shifting on the fly based on the image resolution
|
| 110 |
+
sigmas = shift * sigmas / (1 +
|
| 111 |
+
(shift - 1) * sigmas) # pyright: ignore
|
| 112 |
+
|
| 113 |
+
self.sigmas = sigmas
|
| 114 |
+
self.timesteps = sigmas * num_train_timesteps
|
| 115 |
+
|
| 116 |
+
self.model_outputs = [None] * solver_order
|
| 117 |
+
self.timestep_list = [None] * solver_order
|
| 118 |
+
self.lower_order_nums = 0
|
| 119 |
+
self.disable_corrector = disable_corrector
|
| 120 |
+
self.solver_p = solver_p
|
| 121 |
+
self.last_sample = None
|
| 122 |
+
self._step_index = None
|
| 123 |
+
self._begin_index = None
|
| 124 |
+
|
| 125 |
+
self.sigmas = self.sigmas.to(
|
| 126 |
+
"cpu") # to avoid too much CPU/GPU communication
|
| 127 |
+
self.sigma_min = self.sigmas[-1].item()
|
| 128 |
+
self.sigma_max = self.sigmas[0].item()
|
| 129 |
+
|
| 130 |
+
@property
|
| 131 |
+
def step_index(self):
|
| 132 |
+
"""
|
| 133 |
+
The index counter for current timestep. It will increase 1 after each scheduler step.
|
| 134 |
+
"""
|
| 135 |
+
return self._step_index
|
| 136 |
+
|
| 137 |
+
@property
|
| 138 |
+
def begin_index(self):
|
| 139 |
+
"""
|
| 140 |
+
The index for the first timestep. It should be set from pipeline with `set_begin_index` method.
|
| 141 |
+
"""
|
| 142 |
+
return self._begin_index
|
| 143 |
+
|
| 144 |
+
# Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler.set_begin_index
|
| 145 |
+
def set_begin_index(self, begin_index: int = 0):
|
| 146 |
+
"""
|
| 147 |
+
Sets the begin index for the scheduler. This function should be run from pipeline before the inference.
|
| 148 |
+
|
| 149 |
+
Args:
|
| 150 |
+
begin_index (`int`):
|
| 151 |
+
The begin index for the scheduler.
|
| 152 |
+
"""
|
| 153 |
+
self._begin_index = begin_index
|
| 154 |
+
|
| 155 |
+
# Modified from diffusers.schedulers.scheduling_flow_match_euler_discrete.FlowMatchEulerDiscreteScheduler.set_timesteps
|
| 156 |
+
def set_timesteps(
|
| 157 |
+
self,
|
| 158 |
+
num_inference_steps: Union[int, None] = None,
|
| 159 |
+
device: Union[str, torch.device] = None,
|
| 160 |
+
sigmas: Optional[List[float]] = None,
|
| 161 |
+
mu: Optional[Union[float, None]] = None,
|
| 162 |
+
shift: Optional[Union[float, None]] = None,
|
| 163 |
+
):
|
| 164 |
+
"""
|
| 165 |
+
Sets the discrete timesteps used for the diffusion chain (to be run before inference).
|
| 166 |
+
Args:
|
| 167 |
+
num_inference_steps (`int`):
|
| 168 |
+
Total number of the spacing of the time steps.
|
| 169 |
+
device (`str` or `torch.device`, *optional*):
|
| 170 |
+
The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
|
| 171 |
+
"""
|
| 172 |
+
|
| 173 |
+
if self.config.use_dynamic_shifting and mu is None:
|
| 174 |
+
raise ValueError(
|
| 175 |
+
" you have to pass a value for `mu` when `use_dynamic_shifting` is set to be `True`"
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
if sigmas is None:
|
| 179 |
+
sigmas = np.linspace(self.sigma_max, self.sigma_min,
|
| 180 |
+
num_inference_steps +
|
| 181 |
+
1).copy()[:-1] # pyright: ignore
|
| 182 |
+
|
| 183 |
+
if self.config.use_dynamic_shifting:
|
| 184 |
+
sigmas = self.time_shift(mu, 1.0, sigmas) # pyright: ignore
|
| 185 |
+
else:
|
| 186 |
+
if shift is None:
|
| 187 |
+
shift = self.config.shift
|
| 188 |
+
sigmas = shift * sigmas / (1 +
|
| 189 |
+
(shift - 1) * sigmas) # pyright: ignore
|
| 190 |
+
|
| 191 |
+
if self.config.final_sigmas_type == "sigma_min":
|
| 192 |
+
sigma_last = ((1 - self.alphas_cumprod[0]) /
|
| 193 |
+
self.alphas_cumprod[0])**0.5
|
| 194 |
+
elif self.config.final_sigmas_type == "zero":
|
| 195 |
+
sigma_last = 0
|
| 196 |
+
else:
|
| 197 |
+
raise ValueError(
|
| 198 |
+
f"`final_sigmas_type` must be one of 'zero', or 'sigma_min', but got {self.config.final_sigmas_type}"
|
| 199 |
+
)
|
| 200 |
+
|
| 201 |
+
timesteps = sigmas * self.config.num_train_timesteps
|
| 202 |
+
sigmas = np.concatenate([sigmas, [sigma_last]
|
| 203 |
+
]).astype(np.float32) # pyright: ignore
|
| 204 |
+
|
| 205 |
+
self.sigmas = torch.from_numpy(sigmas)
|
| 206 |
+
self.timesteps = torch.from_numpy(timesteps).to(
|
| 207 |
+
device=device, dtype=torch.int64)
|
| 208 |
+
|
| 209 |
+
self.num_inference_steps = len(timesteps)
|
| 210 |
+
|
| 211 |
+
self.model_outputs = [
|
| 212 |
+
None,
|
| 213 |
+
] * self.config.solver_order
|
| 214 |
+
self.lower_order_nums = 0
|
| 215 |
+
self.last_sample = None
|
| 216 |
+
if self.solver_p:
|
| 217 |
+
self.solver_p.set_timesteps(self.num_inference_steps, device=device)
|
| 218 |
+
|
| 219 |
+
# add an index counter for schedulers that allow duplicated timesteps
|
| 220 |
+
self._step_index = None
|
| 221 |
+
self._begin_index = None
|
| 222 |
+
self.sigmas = self.sigmas.to(
|
| 223 |
+
"cpu") # to avoid too much CPU/GPU communication
|
| 224 |
+
|
| 225 |
+
# Copied from diffusers.schedulers.scheduling_ddpm.DDPMScheduler._threshold_sample
|
| 226 |
+
def _threshold_sample(self, sample: torch.Tensor) -> torch.Tensor:
|
| 227 |
+
"""
|
| 228 |
+
"Dynamic thresholding: At each sampling step we set s to a certain percentile absolute pixel value in xt0 (the
|
| 229 |
+
prediction of x_0 at timestep t), and if s > 1, then we threshold xt0 to the range [-s, s] and then divide by
|
| 230 |
+
s. Dynamic thresholding pushes saturated pixels (those near -1 and 1) inwards, thereby actively preventing
|
| 231 |
+
pixels from saturation at each step. We find that dynamic thresholding results in significantly better
|
| 232 |
+
photorealism as well as better image-text alignment, especially when using very large guidance weights."
|
| 233 |
+
|
| 234 |
+
https://arxiv.org/abs/2205.11487
|
| 235 |
+
"""
|
| 236 |
+
dtype = sample.dtype
|
| 237 |
+
batch_size, channels, *remaining_dims = sample.shape
|
| 238 |
+
|
| 239 |
+
if dtype not in (torch.float32, torch.float64):
|
| 240 |
+
sample = sample.float(
|
| 241 |
+
) # upcast for quantile calculation, and clamp not implemented for cpu half
|
| 242 |
+
|
| 243 |
+
# Flatten sample for doing quantile calculation along each image
|
| 244 |
+
sample = sample.reshape(batch_size, channels * np.prod(remaining_dims))
|
| 245 |
+
|
| 246 |
+
abs_sample = sample.abs() # "a certain percentile absolute pixel value"
|
| 247 |
+
|
| 248 |
+
s = torch.quantile(
|
| 249 |
+
abs_sample, self.config.dynamic_thresholding_ratio, dim=1)
|
| 250 |
+
s = torch.clamp(
|
| 251 |
+
s, min=1, max=self.config.sample_max_value
|
| 252 |
+
) # When clamped to min=1, equivalent to standard clipping to [-1, 1]
|
| 253 |
+
s = s.unsqueeze(
|
| 254 |
+
1) # (batch_size, 1) because clamp will broadcast along dim=0
|
| 255 |
+
sample = torch.clamp(
|
| 256 |
+
sample, -s, s
|
| 257 |
+
) / s # "we threshold xt0 to the range [-s, s] and then divide by s"
|
| 258 |
+
|
| 259 |
+
sample = sample.reshape(batch_size, channels, *remaining_dims)
|
| 260 |
+
sample = sample.to(dtype)
|
| 261 |
+
|
| 262 |
+
return sample
|
| 263 |
+
|
| 264 |
+
# Copied from diffusers.schedulers.scheduling_flow_match_euler_discrete.FlowMatchEulerDiscreteScheduler._sigma_to_t
|
| 265 |
+
def _sigma_to_t(self, sigma):
|
| 266 |
+
return sigma * self.config.num_train_timesteps
|
| 267 |
+
|
| 268 |
+
def _sigma_to_alpha_sigma_t(self, sigma):
|
| 269 |
+
return 1 - sigma, sigma
|
| 270 |
+
|
| 271 |
+
# Copied from diffusers.schedulers.scheduling_flow_match_euler_discrete.set_timesteps
|
| 272 |
+
def time_shift(self, mu: float, sigma: float, t: torch.Tensor):
|
| 273 |
+
return math.exp(mu) / (math.exp(mu) + (1 / t - 1)**sigma)
|
| 274 |
+
|
| 275 |
+
def convert_model_output(
|
| 276 |
+
self,
|
| 277 |
+
model_output: torch.Tensor,
|
| 278 |
+
*args,
|
| 279 |
+
sample: torch.Tensor = None,
|
| 280 |
+
**kwargs,
|
| 281 |
+
) -> torch.Tensor:
|
| 282 |
+
r"""
|
| 283 |
+
Convert the model output to the corresponding type the UniPC algorithm needs.
|
| 284 |
+
|
| 285 |
+
Args:
|
| 286 |
+
model_output (`torch.Tensor`):
|
| 287 |
+
The direct output from the learned diffusion model.
|
| 288 |
+
timestep (`int`):
|
| 289 |
+
The current discrete timestep in the diffusion chain.
|
| 290 |
+
sample (`torch.Tensor`):
|
| 291 |
+
A current instance of a sample created by the diffusion process.
|
| 292 |
+
|
| 293 |
+
Returns:
|
| 294 |
+
`torch.Tensor`:
|
| 295 |
+
The converted model output.
|
| 296 |
+
"""
|
| 297 |
+
timestep = args[0] if len(args) > 0 else kwargs.pop("timestep", None)
|
| 298 |
+
if sample is None:
|
| 299 |
+
if len(args) > 1:
|
| 300 |
+
sample = args[1]
|
| 301 |
+
else:
|
| 302 |
+
raise ValueError(
|
| 303 |
+
"missing `sample` as a required keyward argument")
|
| 304 |
+
if timestep is not None:
|
| 305 |
+
deprecate(
|
| 306 |
+
"timesteps",
|
| 307 |
+
"1.0.0",
|
| 308 |
+
"Passing `timesteps` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`",
|
| 309 |
+
)
|
| 310 |
+
|
| 311 |
+
sigma = self.sigmas[self.step_index]
|
| 312 |
+
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma)
|
| 313 |
+
|
| 314 |
+
if self.predict_x0:
|
| 315 |
+
if self.config.prediction_type == "flow_prediction":
|
| 316 |
+
sigma_t = self.sigmas[self.step_index]
|
| 317 |
+
x0_pred = sample - sigma_t * model_output
|
| 318 |
+
else:
|
| 319 |
+
raise ValueError(
|
| 320 |
+
f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`,"
|
| 321 |
+
" `v_prediction` or `flow_prediction` for the UniPCMultistepScheduler."
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
if self.config.thresholding:
|
| 325 |
+
x0_pred = self._threshold_sample(x0_pred)
|
| 326 |
+
|
| 327 |
+
return x0_pred
|
| 328 |
+
else:
|
| 329 |
+
if self.config.prediction_type == "flow_prediction":
|
| 330 |
+
sigma_t = self.sigmas[self.step_index]
|
| 331 |
+
epsilon = sample - (1 - sigma_t) * model_output
|
| 332 |
+
else:
|
| 333 |
+
raise ValueError(
|
| 334 |
+
f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`,"
|
| 335 |
+
" `v_prediction` or `flow_prediction` for the UniPCMultistepScheduler."
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
if self.config.thresholding:
|
| 339 |
+
sigma_t = self.sigmas[self.step_index]
|
| 340 |
+
x0_pred = sample - sigma_t * model_output
|
| 341 |
+
x0_pred = self._threshold_sample(x0_pred)
|
| 342 |
+
epsilon = model_output + x0_pred
|
| 343 |
+
|
| 344 |
+
return epsilon
|
| 345 |
+
|
| 346 |
+
def multistep_uni_p_bh_update(
|
| 347 |
+
self,
|
| 348 |
+
model_output: torch.Tensor,
|
| 349 |
+
*args,
|
| 350 |
+
sample: torch.Tensor = None,
|
| 351 |
+
order: int = None, # pyright: ignore
|
| 352 |
+
**kwargs,
|
| 353 |
+
) -> torch.Tensor:
|
| 354 |
+
"""
|
| 355 |
+
One step for the UniP (B(h) version). Alternatively, `self.solver_p` is used if is specified.
|
| 356 |
+
|
| 357 |
+
Args:
|
| 358 |
+
model_output (`torch.Tensor`):
|
| 359 |
+
The direct output from the learned diffusion model at the current timestep.
|
| 360 |
+
prev_timestep (`int`):
|
| 361 |
+
The previous discrete timestep in the diffusion chain.
|
| 362 |
+
sample (`torch.Tensor`):
|
| 363 |
+
A current instance of a sample created by the diffusion process.
|
| 364 |
+
order (`int`):
|
| 365 |
+
The order of UniP at this timestep (corresponds to the *p* in UniPC-p).
|
| 366 |
+
|
| 367 |
+
Returns:
|
| 368 |
+
`torch.Tensor`:
|
| 369 |
+
The sample tensor at the previous timestep.
|
| 370 |
+
"""
|
| 371 |
+
prev_timestep = args[0] if len(args) > 0 else kwargs.pop(
|
| 372 |
+
"prev_timestep", None)
|
| 373 |
+
if sample is None:
|
| 374 |
+
if len(args) > 1:
|
| 375 |
+
sample = args[1]
|
| 376 |
+
else:
|
| 377 |
+
raise ValueError(
|
| 378 |
+
" missing `sample` as a required keyward argument")
|
| 379 |
+
if order is None:
|
| 380 |
+
if len(args) > 2:
|
| 381 |
+
order = args[2]
|
| 382 |
+
else:
|
| 383 |
+
raise ValueError(
|
| 384 |
+
" missing `order` as a required keyward argument")
|
| 385 |
+
if prev_timestep is not None:
|
| 386 |
+
deprecate(
|
| 387 |
+
"prev_timestep",
|
| 388 |
+
"1.0.0",
|
| 389 |
+
"Passing `prev_timestep` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`",
|
| 390 |
+
)
|
| 391 |
+
model_output_list = self.model_outputs
|
| 392 |
+
|
| 393 |
+
s0 = self.timestep_list[-1]
|
| 394 |
+
m0 = model_output_list[-1]
|
| 395 |
+
x = sample
|
| 396 |
+
|
| 397 |
+
if self.solver_p:
|
| 398 |
+
x_t = self.solver_p.step(model_output, s0, x).prev_sample
|
| 399 |
+
return x_t
|
| 400 |
+
|
| 401 |
+
sigma_t, sigma_s0 = self.sigmas[self.step_index + 1], self.sigmas[
|
| 402 |
+
self.step_index] # pyright: ignore
|
| 403 |
+
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma_t)
|
| 404 |
+
alpha_s0, sigma_s0 = self._sigma_to_alpha_sigma_t(sigma_s0)
|
| 405 |
+
|
| 406 |
+
lambda_t = torch.log(alpha_t) - torch.log(sigma_t)
|
| 407 |
+
lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0)
|
| 408 |
+
|
| 409 |
+
h = lambda_t - lambda_s0
|
| 410 |
+
device = sample.device
|
| 411 |
+
|
| 412 |
+
rks = []
|
| 413 |
+
D1s = []
|
| 414 |
+
for i in range(1, order):
|
| 415 |
+
si = self.step_index - i # pyright: ignore
|
| 416 |
+
mi = model_output_list[-(i + 1)]
|
| 417 |
+
alpha_si, sigma_si = self._sigma_to_alpha_sigma_t(self.sigmas[si])
|
| 418 |
+
lambda_si = torch.log(alpha_si) - torch.log(sigma_si)
|
| 419 |
+
rk = (lambda_si - lambda_s0) / h
|
| 420 |
+
rks.append(rk)
|
| 421 |
+
D1s.append((mi - m0) / rk) # pyright: ignore
|
| 422 |
+
|
| 423 |
+
rks.append(1.0)
|
| 424 |
+
rks = torch.tensor(rks, device=device)
|
| 425 |
+
|
| 426 |
+
R = []
|
| 427 |
+
b = []
|
| 428 |
+
|
| 429 |
+
hh = -h if self.predict_x0 else h
|
| 430 |
+
h_phi_1 = torch.expm1(hh) # h\phi_1(h) = e^h - 1
|
| 431 |
+
h_phi_k = h_phi_1 / hh - 1
|
| 432 |
+
|
| 433 |
+
factorial_i = 1
|
| 434 |
+
|
| 435 |
+
if self.config.solver_type == "bh1":
|
| 436 |
+
B_h = hh
|
| 437 |
+
elif self.config.solver_type == "bh2":
|
| 438 |
+
B_h = torch.expm1(hh)
|
| 439 |
+
else:
|
| 440 |
+
raise NotImplementedError()
|
| 441 |
+
|
| 442 |
+
for i in range(1, order + 1):
|
| 443 |
+
R.append(torch.pow(rks, i - 1))
|
| 444 |
+
b.append(h_phi_k * factorial_i / B_h)
|
| 445 |
+
factorial_i *= i + 1
|
| 446 |
+
h_phi_k = h_phi_k / hh - 1 / factorial_i
|
| 447 |
+
|
| 448 |
+
R = torch.stack(R)
|
| 449 |
+
b = torch.tensor(b, device=device)
|
| 450 |
+
|
| 451 |
+
if len(D1s) > 0:
|
| 452 |
+
D1s = torch.stack(D1s, dim=1) # (B, K)
|
| 453 |
+
# for order 2, we use a simplified version
|
| 454 |
+
if order == 2:
|
| 455 |
+
rhos_p = torch.tensor([0.5], dtype=x.dtype, device=device)
|
| 456 |
+
else:
|
| 457 |
+
rhos_p = torch.linalg.solve(R[:-1, :-1],
|
| 458 |
+
b[:-1]).to(device).to(x.dtype)
|
| 459 |
+
else:
|
| 460 |
+
D1s = None
|
| 461 |
+
|
| 462 |
+
if self.predict_x0:
|
| 463 |
+
x_t_ = sigma_t / sigma_s0 * x - alpha_t * h_phi_1 * m0
|
| 464 |
+
if D1s is not None:
|
| 465 |
+
pred_res = torch.einsum("k,bkc...->bc...", rhos_p,
|
| 466 |
+
D1s) # pyright: ignore
|
| 467 |
+
else:
|
| 468 |
+
pred_res = 0
|
| 469 |
+
x_t = x_t_ - alpha_t * B_h * pred_res
|
| 470 |
+
else:
|
| 471 |
+
x_t_ = alpha_t / alpha_s0 * x - sigma_t * h_phi_1 * m0
|
| 472 |
+
if D1s is not None:
|
| 473 |
+
pred_res = torch.einsum("k,bkc...->bc...", rhos_p,
|
| 474 |
+
D1s) # pyright: ignore
|
| 475 |
+
else:
|
| 476 |
+
pred_res = 0
|
| 477 |
+
x_t = x_t_ - sigma_t * B_h * pred_res
|
| 478 |
+
|
| 479 |
+
x_t = x_t.to(x.dtype)
|
| 480 |
+
return x_t
|
| 481 |
+
|
| 482 |
+
def multistep_uni_c_bh_update(
|
| 483 |
+
self,
|
| 484 |
+
this_model_output: torch.Tensor,
|
| 485 |
+
*args,
|
| 486 |
+
last_sample: torch.Tensor = None,
|
| 487 |
+
this_sample: torch.Tensor = None,
|
| 488 |
+
order: int = None, # pyright: ignore
|
| 489 |
+
**kwargs,
|
| 490 |
+
) -> torch.Tensor:
|
| 491 |
+
"""
|
| 492 |
+
One step for the UniC (B(h) version).
|
| 493 |
+
|
| 494 |
+
Args:
|
| 495 |
+
this_model_output (`torch.Tensor`):
|
| 496 |
+
The model outputs at `x_t`.
|
| 497 |
+
this_timestep (`int`):
|
| 498 |
+
The current timestep `t`.
|
| 499 |
+
last_sample (`torch.Tensor`):
|
| 500 |
+
The generated sample before the last predictor `x_{t-1}`.
|
| 501 |
+
this_sample (`torch.Tensor`):
|
| 502 |
+
The generated sample after the last predictor `x_{t}`.
|
| 503 |
+
order (`int`):
|
| 504 |
+
The `p` of UniC-p at this step. The effective order of accuracy should be `order + 1`.
|
| 505 |
+
|
| 506 |
+
Returns:
|
| 507 |
+
`torch.Tensor`:
|
| 508 |
+
The corrected sample tensor at the current timestep.
|
| 509 |
+
"""
|
| 510 |
+
this_timestep = args[0] if len(args) > 0 else kwargs.pop(
|
| 511 |
+
"this_timestep", None)
|
| 512 |
+
if last_sample is None:
|
| 513 |
+
if len(args) > 1:
|
| 514 |
+
last_sample = args[1]
|
| 515 |
+
else:
|
| 516 |
+
raise ValueError(
|
| 517 |
+
" missing`last_sample` as a required keyward argument")
|
| 518 |
+
if this_sample is None:
|
| 519 |
+
if len(args) > 2:
|
| 520 |
+
this_sample = args[2]
|
| 521 |
+
else:
|
| 522 |
+
raise ValueError(
|
| 523 |
+
" missing`this_sample` as a required keyward argument")
|
| 524 |
+
if order is None:
|
| 525 |
+
if len(args) > 3:
|
| 526 |
+
order = args[3]
|
| 527 |
+
else:
|
| 528 |
+
raise ValueError(
|
| 529 |
+
" missing`order` as a required keyward argument")
|
| 530 |
+
if this_timestep is not None:
|
| 531 |
+
deprecate(
|
| 532 |
+
"this_timestep",
|
| 533 |
+
"1.0.0",
|
| 534 |
+
"Passing `this_timestep` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`",
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
model_output_list = self.model_outputs
|
| 538 |
+
|
| 539 |
+
m0 = model_output_list[-1]
|
| 540 |
+
x = last_sample
|
| 541 |
+
x_t = this_sample
|
| 542 |
+
model_t = this_model_output
|
| 543 |
+
|
| 544 |
+
sigma_t, sigma_s0 = self.sigmas[self.step_index], self.sigmas[
|
| 545 |
+
self.step_index - 1] # pyright: ignore
|
| 546 |
+
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma_t)
|
| 547 |
+
alpha_s0, sigma_s0 = self._sigma_to_alpha_sigma_t(sigma_s0)
|
| 548 |
+
|
| 549 |
+
lambda_t = torch.log(alpha_t) - torch.log(sigma_t)
|
| 550 |
+
lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0)
|
| 551 |
+
|
| 552 |
+
h = lambda_t - lambda_s0
|
| 553 |
+
device = this_sample.device
|
| 554 |
+
|
| 555 |
+
rks = []
|
| 556 |
+
D1s = []
|
| 557 |
+
for i in range(1, order):
|
| 558 |
+
si = self.step_index - (i + 1) # pyright: ignore
|
| 559 |
+
mi = model_output_list[-(i + 1)]
|
| 560 |
+
alpha_si, sigma_si = self._sigma_to_alpha_sigma_t(self.sigmas[si])
|
| 561 |
+
lambda_si = torch.log(alpha_si) - torch.log(sigma_si)
|
| 562 |
+
rk = (lambda_si - lambda_s0) / h
|
| 563 |
+
rks.append(rk)
|
| 564 |
+
D1s.append((mi - m0) / rk) # pyright: ignore
|
| 565 |
+
|
| 566 |
+
rks.append(1.0)
|
| 567 |
+
rks = torch.tensor(rks, device=device)
|
| 568 |
+
|
| 569 |
+
R = []
|
| 570 |
+
b = []
|
| 571 |
+
|
| 572 |
+
hh = -h if self.predict_x0 else h
|
| 573 |
+
h_phi_1 = torch.expm1(hh) # h\phi_1(h) = e^h - 1
|
| 574 |
+
h_phi_k = h_phi_1 / hh - 1
|
| 575 |
+
|
| 576 |
+
factorial_i = 1
|
| 577 |
+
|
| 578 |
+
if self.config.solver_type == "bh1":
|
| 579 |
+
B_h = hh
|
| 580 |
+
elif self.config.solver_type == "bh2":
|
| 581 |
+
B_h = torch.expm1(hh)
|
| 582 |
+
else:
|
| 583 |
+
raise NotImplementedError()
|
| 584 |
+
|
| 585 |
+
for i in range(1, order + 1):
|
| 586 |
+
R.append(torch.pow(rks, i - 1))
|
| 587 |
+
b.append(h_phi_k * factorial_i / B_h)
|
| 588 |
+
factorial_i *= i + 1
|
| 589 |
+
h_phi_k = h_phi_k / hh - 1 / factorial_i
|
| 590 |
+
|
| 591 |
+
R = torch.stack(R)
|
| 592 |
+
b = torch.tensor(b, device=device)
|
| 593 |
+
|
| 594 |
+
if len(D1s) > 0:
|
| 595 |
+
D1s = torch.stack(D1s, dim=1)
|
| 596 |
+
else:
|
| 597 |
+
D1s = None
|
| 598 |
+
|
| 599 |
+
# for order 1, we use a simplified version
|
| 600 |
+
if order == 1:
|
| 601 |
+
rhos_c = torch.tensor([0.5], dtype=x.dtype, device=device)
|
| 602 |
+
else:
|
| 603 |
+
rhos_c = torch.linalg.solve(R, b).to(device).to(x.dtype)
|
| 604 |
+
|
| 605 |
+
if self.predict_x0:
|
| 606 |
+
x_t_ = sigma_t / sigma_s0 * x - alpha_t * h_phi_1 * m0
|
| 607 |
+
if D1s is not None:
|
| 608 |
+
corr_res = torch.einsum("k,bkc...->bc...", rhos_c[:-1], D1s)
|
| 609 |
+
else:
|
| 610 |
+
corr_res = 0
|
| 611 |
+
D1_t = model_t - m0
|
| 612 |
+
x_t = x_t_ - alpha_t * B_h * (corr_res + rhos_c[-1] * D1_t)
|
| 613 |
+
else:
|
| 614 |
+
x_t_ = alpha_t / alpha_s0 * x - sigma_t * h_phi_1 * m0
|
| 615 |
+
if D1s is not None:
|
| 616 |
+
corr_res = torch.einsum("k,bkc...->bc...", rhos_c[:-1], D1s)
|
| 617 |
+
else:
|
| 618 |
+
corr_res = 0
|
| 619 |
+
D1_t = model_t - m0
|
| 620 |
+
x_t = x_t_ - sigma_t * B_h * (corr_res + rhos_c[-1] * D1_t)
|
| 621 |
+
x_t = x_t.to(x.dtype)
|
| 622 |
+
return x_t
|
| 623 |
+
|
| 624 |
+
def index_for_timestep(self, timestep, schedule_timesteps=None):
|
| 625 |
+
if schedule_timesteps is None:
|
| 626 |
+
schedule_timesteps = self.timesteps
|
| 627 |
+
|
| 628 |
+
indices = (schedule_timesteps == timestep).nonzero()
|
| 629 |
+
|
| 630 |
+
# The sigma index that is taken for the **very** first `step`
|
| 631 |
+
# is always the second index (or the last index if there is only 1)
|
| 632 |
+
# This way we can ensure we don't accidentally skip a sigma in
|
| 633 |
+
# case we start in the middle of the denoising schedule (e.g. for image-to-image)
|
| 634 |
+
pos = 1 if len(indices) > 1 else 0
|
| 635 |
+
|
| 636 |
+
return indices[pos].item()
|
| 637 |
+
|
| 638 |
+
# Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler._init_step_index
|
| 639 |
+
def _init_step_index(self, timestep):
|
| 640 |
+
"""
|
| 641 |
+
Initialize the step_index counter for the scheduler.
|
| 642 |
+
"""
|
| 643 |
+
|
| 644 |
+
if self.begin_index is None:
|
| 645 |
+
if isinstance(timestep, torch.Tensor):
|
| 646 |
+
timestep = timestep.to(self.timesteps.device)
|
| 647 |
+
self._step_index = self.index_for_timestep(timestep)
|
| 648 |
+
else:
|
| 649 |
+
self._step_index = self._begin_index
|
| 650 |
+
|
| 651 |
+
def step(self,
|
| 652 |
+
model_output: torch.Tensor,
|
| 653 |
+
timestep: Union[int, torch.Tensor],
|
| 654 |
+
sample: torch.Tensor,
|
| 655 |
+
return_dict: bool = True,
|
| 656 |
+
generator=None) -> Union[SchedulerOutput, Tuple]:
|
| 657 |
+
"""
|
| 658 |
+
Predict the sample from the previous timestep by reversing the SDE. This function propagates the sample with
|
| 659 |
+
the multistep UniPC.
|
| 660 |
+
|
| 661 |
+
Args:
|
| 662 |
+
model_output (`torch.Tensor`):
|
| 663 |
+
The direct output from learned diffusion model.
|
| 664 |
+
timestep (`int`):
|
| 665 |
+
The current discrete timestep in the diffusion chain.
|
| 666 |
+
sample (`torch.Tensor`):
|
| 667 |
+
A current instance of a sample created by the diffusion process.
|
| 668 |
+
return_dict (`bool`):
|
| 669 |
+
Whether or not to return a [`~schedulers.scheduling_utils.SchedulerOutput`] or `tuple`.
|
| 670 |
+
|
| 671 |
+
Returns:
|
| 672 |
+
[`~schedulers.scheduling_utils.SchedulerOutput`] or `tuple`:
|
| 673 |
+
If return_dict is `True`, [`~schedulers.scheduling_utils.SchedulerOutput`] is returned, otherwise a
|
| 674 |
+
tuple is returned where the first element is the sample tensor.
|
| 675 |
+
|
| 676 |
+
"""
|
| 677 |
+
if self.num_inference_steps is None:
|
| 678 |
+
raise ValueError(
|
| 679 |
+
"Number of inference steps is 'None', you need to run 'set_timesteps' after creating the scheduler"
|
| 680 |
+
)
|
| 681 |
+
|
| 682 |
+
if self.step_index is None:
|
| 683 |
+
self._init_step_index(timestep)
|
| 684 |
+
|
| 685 |
+
use_corrector = (
|
| 686 |
+
self.step_index > 0 and
|
| 687 |
+
self.step_index - 1 not in self.disable_corrector and
|
| 688 |
+
self.last_sample is not None # pyright: ignore
|
| 689 |
+
)
|
| 690 |
+
|
| 691 |
+
model_output_convert = self.convert_model_output(
|
| 692 |
+
model_output, sample=sample)
|
| 693 |
+
if use_corrector:
|
| 694 |
+
sample = self.multistep_uni_c_bh_update(
|
| 695 |
+
this_model_output=model_output_convert,
|
| 696 |
+
last_sample=self.last_sample,
|
| 697 |
+
this_sample=sample,
|
| 698 |
+
order=self.this_order,
|
| 699 |
+
)
|
| 700 |
+
|
| 701 |
+
for i in range(self.config.solver_order - 1):
|
| 702 |
+
self.model_outputs[i] = self.model_outputs[i + 1]
|
| 703 |
+
self.timestep_list[i] = self.timestep_list[i + 1]
|
| 704 |
+
|
| 705 |
+
self.model_outputs[-1] = model_output_convert
|
| 706 |
+
self.timestep_list[-1] = timestep # pyright: ignore
|
| 707 |
+
|
| 708 |
+
if self.config.lower_order_final:
|
| 709 |
+
this_order = min(self.config.solver_order,
|
| 710 |
+
len(self.timesteps) -
|
| 711 |
+
self.step_index) # pyright: ignore
|
| 712 |
+
else:
|
| 713 |
+
this_order = self.config.solver_order
|
| 714 |
+
|
| 715 |
+
self.this_order = min(this_order,
|
| 716 |
+
self.lower_order_nums + 1) # warmup for multistep
|
| 717 |
+
assert self.this_order > 0
|
| 718 |
+
|
| 719 |
+
self.last_sample = sample
|
| 720 |
+
prev_sample = self.multistep_uni_p_bh_update(
|
| 721 |
+
model_output=model_output, # pass the original non-converted model output, in case solver-p is used
|
| 722 |
+
sample=sample,
|
| 723 |
+
order=self.this_order,
|
| 724 |
+
)
|
| 725 |
+
|
| 726 |
+
if self.lower_order_nums < self.config.solver_order:
|
| 727 |
+
self.lower_order_nums += 1
|
| 728 |
+
|
| 729 |
+
# upon completion increase step index by one
|
| 730 |
+
self._step_index += 1 # pyright: ignore
|
| 731 |
+
|
| 732 |
+
if not return_dict:
|
| 733 |
+
return (prev_sample,)
|
| 734 |
+
|
| 735 |
+
return SchedulerOutput(prev_sample=prev_sample)
|
| 736 |
+
|
| 737 |
+
def scale_model_input(self, sample: torch.Tensor, *args,
|
| 738 |
+
**kwargs) -> torch.Tensor:
|
| 739 |
+
"""
|
| 740 |
+
Ensures interchangeability with schedulers that need to scale the denoising model input depending on the
|
| 741 |
+
current timestep.
|
| 742 |
+
|
| 743 |
+
Args:
|
| 744 |
+
sample (`torch.Tensor`):
|
| 745 |
+
The input sample.
|
| 746 |
+
|
| 747 |
+
Returns:
|
| 748 |
+
`torch.Tensor`:
|
| 749 |
+
A scaled input sample.
|
| 750 |
+
"""
|
| 751 |
+
return sample
|
| 752 |
+
|
| 753 |
+
# Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler.add_noise
|
| 754 |
+
def add_noise(
|
| 755 |
+
self,
|
| 756 |
+
original_samples: torch.Tensor,
|
| 757 |
+
noise: torch.Tensor,
|
| 758 |
+
timesteps: torch.IntTensor,
|
| 759 |
+
) -> torch.Tensor:
|
| 760 |
+
# Make sure sigmas and timesteps have the same device and dtype as original_samples
|
| 761 |
+
sigmas = self.sigmas.to(
|
| 762 |
+
device=original_samples.device, dtype=original_samples.dtype)
|
| 763 |
+
if original_samples.device.type == "mps" and torch.is_floating_point(
|
| 764 |
+
timesteps):
|
| 765 |
+
# mps does not support float64
|
| 766 |
+
schedule_timesteps = self.timesteps.to(
|
| 767 |
+
original_samples.device, dtype=torch.float32)
|
| 768 |
+
timesteps = timesteps.to(
|
| 769 |
+
original_samples.device, dtype=torch.float32)
|
| 770 |
+
else:
|
| 771 |
+
schedule_timesteps = self.timesteps.to(original_samples.device)
|
| 772 |
+
timesteps = timesteps.to(original_samples.device)
|
| 773 |
+
|
| 774 |
+
# begin_index is None when the scheduler is used for training or pipeline does not implement set_begin_index
|
| 775 |
+
if self.begin_index is None:
|
| 776 |
+
step_indices = [
|
| 777 |
+
self.index_for_timestep(t, schedule_timesteps)
|
| 778 |
+
for t in timesteps
|
| 779 |
+
]
|
| 780 |
+
elif self.step_index is not None:
|
| 781 |
+
# add_noise is called after first denoising step (for inpainting)
|
| 782 |
+
step_indices = [self.step_index] * timesteps.shape[0]
|
| 783 |
+
else:
|
| 784 |
+
# add noise is called before first denoising step to create initial latent(img2img)
|
| 785 |
+
step_indices = [self.begin_index] * timesteps.shape[0]
|
| 786 |
+
|
| 787 |
+
sigma = sigmas[step_indices].flatten()
|
| 788 |
+
while len(sigma.shape) < len(original_samples.shape):
|
| 789 |
+
sigma = sigma.unsqueeze(-1)
|
| 790 |
+
|
| 791 |
+
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma)
|
| 792 |
+
noisy_samples = alpha_t * original_samples + sigma_t * noise
|
| 793 |
+
return noisy_samples
|
| 794 |
+
|
| 795 |
+
def __len__(self):
|
| 796 |
+
return self.config.num_train_timesteps
|
lingbot_video/sglang_moe_shim.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import importlib.util
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
from contextlib import contextmanager
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from types import ModuleType, SimpleNamespace
|
| 9 |
+
from typing import NamedTuple, Optional
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
import torch.nn.functional as F
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@dataclass
|
| 16 |
+
class LightSglangMoeRunnerConfig:
|
| 17 |
+
num_experts: Optional[int] = None
|
| 18 |
+
num_local_experts: Optional[int] = None
|
| 19 |
+
hidden_size: Optional[int] = None
|
| 20 |
+
intermediate_size_per_partition: Optional[int] = None
|
| 21 |
+
layer_id: Optional[int] = None
|
| 22 |
+
top_k: Optional[int] = None
|
| 23 |
+
num_fused_shared_experts: Optional[int] = None
|
| 24 |
+
params_dtype: Optional[torch.dtype] = None
|
| 25 |
+
routing_method_type: Optional[object] = None
|
| 26 |
+
activation: str = "silu"
|
| 27 |
+
is_gated: bool = True
|
| 28 |
+
apply_router_weight_on_input: bool = False
|
| 29 |
+
inplace: bool = False
|
| 30 |
+
no_combine: bool = False
|
| 31 |
+
routed_scaling_factor: Optional[float] = None
|
| 32 |
+
gemm1_alpha: Optional[float] = None
|
| 33 |
+
gemm1_clamp_limit: Optional[float] = None
|
| 34 |
+
swiglu_limit: Optional[float] = None
|
| 35 |
+
gate_up_interleaved: bool = False
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class LightSglangStandardTopKOutput(NamedTuple):
|
| 39 |
+
topk_weights: torch.Tensor
|
| 40 |
+
topk_ids: torch.Tensor
|
| 41 |
+
router_logits: torch.Tensor
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
SGLANG_MOE_SERVER_ARGS = SimpleNamespace(
|
| 45 |
+
enable_deterministic_inference=False,
|
| 46 |
+
enable_fused_moe_sum_all_reduce=False,
|
| 47 |
+
)
|
| 48 |
+
FP8_E4M3_MAX = 448.0
|
| 49 |
+
_SERVER_ARGS_READY = False
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def sglang_env_flag(name: str, default: bool = False) -> bool:
|
| 53 |
+
raw = os.environ.get(name)
|
| 54 |
+
if raw is None:
|
| 55 |
+
return default
|
| 56 |
+
return raw.lower() in {"1", "true", "yes", "on"}
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class SglangEnvFlag:
|
| 60 |
+
def __init__(self, name: str, default: bool = False):
|
| 61 |
+
self.name = name
|
| 62 |
+
self.default = default
|
| 63 |
+
|
| 64 |
+
def get(self) -> bool:
|
| 65 |
+
return sglang_env_flag(self.name, self.default)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def ensure_module(name: str, package_path: str | None = None) -> ModuleType:
|
| 69 |
+
module = sys.modules.get(name)
|
| 70 |
+
if module is None:
|
| 71 |
+
module = ModuleType(name)
|
| 72 |
+
sys.modules[name] = module
|
| 73 |
+
if package_path is not None:
|
| 74 |
+
module.__path__ = [package_path]
|
| 75 |
+
return module
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def fp8_scale_from_amax(amax: torch.Tensor) -> torch.Tensor:
|
| 79 |
+
return torch.clamp(amax.float() / FP8_E4M3_MAX, min=1e-12)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def quantize_to_fp8_e4m3fn(input: torch.Tensor, scale: torch.Tensor) -> torch.Tensor:
|
| 83 |
+
return torch.clamp(input.float() / scale, -FP8_E4M3_MAX, FP8_E4M3_MAX).to(
|
| 84 |
+
torch.float8_e4m3fn
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def raise_unsupported_sglang_quantization(*args, **kwargs):
|
| 89 |
+
raise RuntimeError("This LingBotVideo SGLang MoE shim only enables FP8 W8A8 quantization")
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def sglang_scaled_fp8_quant(
|
| 93 |
+
input: torch.Tensor,
|
| 94 |
+
scale: Optional[torch.Tensor] = None,
|
| 95 |
+
num_token_padding: Optional[int] = None,
|
| 96 |
+
use_per_token_if_dynamic: bool = False,
|
| 97 |
+
):
|
| 98 |
+
if input.ndim != 2:
|
| 99 |
+
raise ValueError(f"Expected 2D input tensor, got {input.ndim}D")
|
| 100 |
+
rows = input.shape[0]
|
| 101 |
+
output_rows = max(int(num_token_padding or 0), rows)
|
| 102 |
+
output = torch.empty((output_rows, input.shape[1]), device=input.device, dtype=torch.float8_e4m3fn)
|
| 103 |
+
input_contiguous = input.contiguous()
|
| 104 |
+
if scale is None:
|
| 105 |
+
if use_per_token_if_dynamic:
|
| 106 |
+
scale = fp8_scale_from_amax(input_contiguous.float().abs().amax(dim=1, keepdim=True))
|
| 107 |
+
if output_rows > rows:
|
| 108 |
+
padded_scale = torch.ones((output_rows, 1), device=input.device, dtype=torch.float32)
|
| 109 |
+
padded_scale[:rows] = scale
|
| 110 |
+
scale = padded_scale
|
| 111 |
+
else:
|
| 112 |
+
scale = fp8_scale_from_amax(input_contiguous.float().abs().amax()).reshape(1)
|
| 113 |
+
quant_scale = scale[:rows] if scale.ndim == 2 else scale
|
| 114 |
+
output[:rows] = quantize_to_fp8_e4m3fn(input_contiguous, quant_scale)
|
| 115 |
+
if output_rows > rows:
|
| 116 |
+
output[rows:] = torch.zeros(
|
| 117 |
+
(output_rows - rows, input.shape[1]), device=input.device, dtype=torch.float8_e4m3fn
|
| 118 |
+
)
|
| 119 |
+
return output, scale.contiguous()
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def sglang_per_token_group_quant_fp8(
|
| 123 |
+
x: torch.Tensor,
|
| 124 |
+
group_size: int,
|
| 125 |
+
eps: float = 1e-10,
|
| 126 |
+
column_major_scales: bool = False,
|
| 127 |
+
scale_tma_aligned: bool = False,
|
| 128 |
+
scale_ue8m0: bool = False,
|
| 129 |
+
fuse_silu_and_mul: bool = False,
|
| 130 |
+
masked_m: Optional[torch.Tensor] = None,
|
| 131 |
+
enable_v2: Optional[bool] = None,
|
| 132 |
+
):
|
| 133 |
+
if fuse_silu_and_mul:
|
| 134 |
+
half = x.shape[-1] // 2
|
| 135 |
+
x = (F.silu(x[..., :half]) * x[..., half:]).contiguous()
|
| 136 |
+
if x.shape[-1] % group_size != 0:
|
| 137 |
+
raise ValueError("The last dimension must be divisible by group_size")
|
| 138 |
+
x_view = x.contiguous().view(*x.shape[:-1], x.shape[-1] // group_size, group_size)
|
| 139 |
+
scales = fp8_scale_from_amax(x_view.float().abs().amax(dim=-1)).clamp_min(eps)
|
| 140 |
+
x_q = quantize_to_fp8_e4m3fn(x_view, scales.unsqueeze(-1)).view(x.shape)
|
| 141 |
+
if column_major_scales:
|
| 142 |
+
scales = scales.transpose(0, 1).contiguous()
|
| 143 |
+
return x_q.contiguous(), scales.contiguous()
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def sglang_silu_and_mul(x: torch.Tensor, out: torch.Tensor | None = None, *args, **kwargs):
|
| 147 |
+
half = x.shape[-1] // 2
|
| 148 |
+
result = F.silu(x[..., :half]) * x[..., half:]
|
| 149 |
+
if out is not None:
|
| 150 |
+
out.copy_(result)
|
| 151 |
+
return out
|
| 152 |
+
return result
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def sglang_gelu_and_mul(x: torch.Tensor, out: torch.Tensor | None = None, *args, **kwargs):
|
| 156 |
+
half = x.shape[-1] // 2
|
| 157 |
+
result = F.gelu(x[..., :half]) * x[..., half:]
|
| 158 |
+
if out is not None:
|
| 159 |
+
out.copy_(result)
|
| 160 |
+
return out
|
| 161 |
+
return result
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
@contextmanager
|
| 165 |
+
def null_sglang_config_override(config):
|
| 166 |
+
yield
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def install_sglang_moe_import_shims() -> None:
|
| 170 |
+
try:
|
| 171 |
+
spec = importlib.util.find_spec("sglang")
|
| 172 |
+
except Exception:
|
| 173 |
+
spec = None
|
| 174 |
+
if spec is None or not spec.submodule_search_locations:
|
| 175 |
+
return
|
| 176 |
+
|
| 177 |
+
package_dir = next(iter(spec.submodule_search_locations))
|
| 178 |
+
srt_dir = os.path.join(package_dir, "srt")
|
| 179 |
+
layers_dir = os.path.join(srt_dir, "layers")
|
| 180 |
+
moe_dir = os.path.join(layers_dir, "moe")
|
| 181 |
+
moe_runner_dir = os.path.join(moe_dir, "moe_runner")
|
| 182 |
+
triton_utils_dir = os.path.join(moe_runner_dir, "triton_utils")
|
| 183 |
+
jit_kernel_dir = os.path.join(package_dir, "jit_kernel")
|
| 184 |
+
|
| 185 |
+
ensure_module("sglang", package_dir)
|
| 186 |
+
ensure_module("sglang.srt", srt_dir)
|
| 187 |
+
ensure_module("sglang.srt.layers", layers_dir)
|
| 188 |
+
ensure_module("sglang.srt.layers.moe", moe_dir)
|
| 189 |
+
moe_runner_module = ensure_module("sglang.srt.layers.moe.moe_runner", moe_runner_dir)
|
| 190 |
+
moe_runner_module.MoeRunnerConfig = LightSglangMoeRunnerConfig
|
| 191 |
+
triton_utils_module = ensure_module(
|
| 192 |
+
"sglang.srt.layers.moe.moe_runner.triton_utils", triton_utils_dir
|
| 193 |
+
)
|
| 194 |
+
triton_utils_module.get_config = lambda: None
|
| 195 |
+
triton_utils_module.override_config = null_sglang_config_override
|
| 196 |
+
ensure_module("sglang.jit_kernel", jit_kernel_dir)
|
| 197 |
+
activation_module = ensure_module("sglang.jit_kernel.activation")
|
| 198 |
+
activation_module.silu_and_mul = sglang_silu_and_mul
|
| 199 |
+
activation_module.gelu_and_mul = sglang_gelu_and_mul
|
| 200 |
+
|
| 201 |
+
server_args_module = ensure_module("sglang.srt.server_args")
|
| 202 |
+
server_args_module.get_global_server_args = lambda: SGLANG_MOE_SERVER_ARGS
|
| 203 |
+
server_args_module.set_global_server_args_for_scheduler = lambda args: None
|
| 204 |
+
|
| 205 |
+
batch_module = ensure_module("sglang.srt.batch_invariant_ops")
|
| 206 |
+
batch_module.is_batch_invariant_mode_enabled = lambda: False
|
| 207 |
+
|
| 208 |
+
environ_module = ensure_module("sglang.srt.environ")
|
| 209 |
+
environ_module.envs = SimpleNamespace(
|
| 210 |
+
SGLANG_OPT_SWIGLU_CLAMP_FUSION=SglangEnvFlag("SGLANG_OPT_SWIGLU_CLAMP_FUSION"),
|
| 211 |
+
SGLANG_EXPERIMENTAL_LORA_OPTI=SglangEnvFlag("SGLANG_EXPERIMENTAL_LORA_OPTI"),
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
utils_module = ensure_module("sglang.srt.utils")
|
| 215 |
+
utils_module.cpu_has_amx_support = lambda: False
|
| 216 |
+
utils_module.get_bool_env_var = sglang_env_flag
|
| 217 |
+
utils_module.is_cpu = lambda: not torch.cuda.is_available()
|
| 218 |
+
utils_module.is_cuda = torch.cuda.is_available
|
| 219 |
+
utils_module.is_hip = lambda: False
|
| 220 |
+
utils_module.is_musa = lambda: False
|
| 221 |
+
utils_module.is_xpu = lambda: False
|
| 222 |
+
utils_module.use_intel_xpu_backend = lambda: False
|
| 223 |
+
utils_module.get_device_name = lambda: torch.cuda.get_device_name() if torch.cuda.is_available() else "cpu"
|
| 224 |
+
utils_module.is_sm90_supported = (
|
| 225 |
+
lambda: torch.cuda.is_available() and torch.cuda.get_device_capability()[0] >= 9
|
| 226 |
+
)
|
| 227 |
+
|
| 228 |
+
custom_op_module = ensure_module("sglang.srt.utils.custom_op")
|
| 229 |
+
custom_op_module.register_custom_op = lambda *args, **kwargs: (lambda fn: fn)
|
| 230 |
+
|
| 231 |
+
moe_utils_module = ensure_module("sglang.srt.layers.moe.utils")
|
| 232 |
+
moe_utils_module.get_moe_padding_size = lambda is_aiter_moe: 0
|
| 233 |
+
|
| 234 |
+
fp8_module = ensure_module("sglang.srt.layers.quantization.fp8_kernel")
|
| 235 |
+
fp8_module.per_token_group_quant_fp8 = sglang_per_token_group_quant_fp8
|
| 236 |
+
fp8_module.scaled_fp8_quant = sglang_scaled_fp8_quant
|
| 237 |
+
fp8_module.sglang_per_token_group_quant_fp8 = sglang_per_token_group_quant_fp8
|
| 238 |
+
|
| 239 |
+
int8_module = ensure_module("sglang.srt.layers.quantization.int8_kernel")
|
| 240 |
+
int8_module.per_token_group_quant_int8 = raise_unsupported_sglang_quantization
|
| 241 |
+
int8_module.per_token_quant_int8 = raise_unsupported_sglang_quantization
|
| 242 |
+
int8_module.sglang_per_token_group_quant_int8 = raise_unsupported_sglang_quantization
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
try:
|
| 246 |
+
from sglang.srt.layers.moe.moe_runner.triton_utils.fused_moe import (
|
| 247 |
+
fused_experts as sglang_fused_experts,
|
| 248 |
+
)
|
| 249 |
+
except Exception as direct_exc: # pragma: no cover - optional deployment dependency
|
| 250 |
+
install_sglang_moe_import_shims()
|
| 251 |
+
try:
|
| 252 |
+
from sglang.srt.layers.moe.moe_runner.triton_utils.fused_moe import (
|
| 253 |
+
fused_experts as sglang_fused_experts,
|
| 254 |
+
)
|
| 255 |
+
except Exception as shim_exc: # pragma: no cover - optional deployment dependency
|
| 256 |
+
sglang_fused_experts = None
|
| 257 |
+
SGLANG_MOE_IMPORT_ERROR = shim_exc
|
| 258 |
+
else:
|
| 259 |
+
SGLANG_MOE_IMPORT_ERROR = None
|
| 260 |
+
else:
|
| 261 |
+
SGLANG_MOE_IMPORT_ERROR = None
|
| 262 |
+
|
| 263 |
+
try:
|
| 264 |
+
from sglang.srt.server_args import (
|
| 265 |
+
get_global_server_args as sglang_get_global_server_args,
|
| 266 |
+
set_global_server_args_for_scheduler as sglang_set_global_server_args_for_scheduler,
|
| 267 |
+
)
|
| 268 |
+
except Exception: # pragma: no cover - optional deployment dependency
|
| 269 |
+
sglang_get_global_server_args = None
|
| 270 |
+
sglang_set_global_server_args_for_scheduler = None
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def _ensure_moe_server_args_attrs(args) -> None:
|
| 274 |
+
if not hasattr(args, "enable_deterministic_inference"):
|
| 275 |
+
args.enable_deterministic_inference = False
|
| 276 |
+
if not hasattr(args, "enable_fused_moe_sum_all_reduce"):
|
| 277 |
+
args.enable_fused_moe_sum_all_reduce = False
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def ensure_sglang_moe_ready() -> None:
|
| 281 |
+
global _SERVER_ARGS_READY
|
| 282 |
+
if sglang_fused_experts is None:
|
| 283 |
+
raise RuntimeError(
|
| 284 |
+
"LINGBOT_MOE_EXPERT_BACKEND=sglang_triton requires SGLang MoE runtime"
|
| 285 |
+
) from SGLANG_MOE_IMPORT_ERROR
|
| 286 |
+
if _SERVER_ARGS_READY:
|
| 287 |
+
return
|
| 288 |
+
_ensure_moe_server_args_attrs(SGLANG_MOE_SERVER_ARGS)
|
| 289 |
+
if sglang_get_global_server_args is not None:
|
| 290 |
+
try:
|
| 291 |
+
server_args = sglang_get_global_server_args()
|
| 292 |
+
except Exception:
|
| 293 |
+
server_args = SGLANG_MOE_SERVER_ARGS
|
| 294 |
+
if sglang_set_global_server_args_for_scheduler is not None:
|
| 295 |
+
sglang_set_global_server_args_for_scheduler(server_args)
|
| 296 |
+
_ensure_moe_server_args_attrs(server_args)
|
| 297 |
+
_SERVER_ARGS_READY = True
|
lingbot_video/transformer_lingbot_video.py
ADDED
|
@@ -0,0 +1,1312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import os
|
| 3 |
+
from typing import Optional, Tuple
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
import torch.nn.functional as F
|
| 8 |
+
import torch.distributed as dist
|
| 9 |
+
|
| 10 |
+
from diffusers.configuration_utils import ConfigMixin, register_to_config
|
| 11 |
+
from diffusers.models.attention_dispatch import dispatch_attention_fn
|
| 12 |
+
from diffusers.models._modeling_parallel import ContextParallelInput, ContextParallelOutput
|
| 13 |
+
from diffusers.models.embeddings import TimestepEmbedding, Timesteps
|
| 14 |
+
from diffusers.models.modeling_outputs import Transformer2DModelOutput
|
| 15 |
+
from diffusers.models.modeling_utils import ModelMixin
|
| 16 |
+
|
| 17 |
+
try:
|
| 18 |
+
from flash_attn_interface import flash_attn_varlen_func as flash_attn_varlen_func_v3
|
| 19 |
+
except Exception: # pragma: no cover - optional CUDA kernel.
|
| 20 |
+
flash_attn_varlen_func_v3 = None
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
from .moe_pack_kernels import reorder_tokens_triton_pack
|
| 24 |
+
from .moe_restore_kernels import restore_tokens_triton
|
| 25 |
+
from .sglang_moe_shim import (
|
| 26 |
+
LightSglangMoeRunnerConfig,
|
| 27 |
+
LightSglangStandardTopKOutput,
|
| 28 |
+
ensure_sglang_moe_ready,
|
| 29 |
+
fp8_scale_from_amax,
|
| 30 |
+
quantize_to_fp8_e4m3fn,
|
| 31 |
+
sglang_fused_experts,
|
| 32 |
+
)
|
| 33 |
+
except ImportError: # pragma: no cover - allows direct file loading in diagnostics.
|
| 34 |
+
from moe_pack_kernels import reorder_tokens_triton_pack
|
| 35 |
+
from moe_restore_kernels import restore_tokens_triton
|
| 36 |
+
from sglang_moe_shim import (
|
| 37 |
+
LightSglangMoeRunnerConfig,
|
| 38 |
+
LightSglangStandardTopKOutput,
|
| 39 |
+
ensure_sglang_moe_ready,
|
| 40 |
+
fp8_scale_from_amax,
|
| 41 |
+
quantize_to_fp8_e4m3fn,
|
| 42 |
+
sglang_fused_experts,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
LINGBOT_VIDEO_FP32_MODULES = (
|
| 47 |
+
"time_embedder",
|
| 48 |
+
"time_modulation",
|
| 49 |
+
"scale_shift_table",
|
| 50 |
+
"norm",
|
| 51 |
+
"norm1",
|
| 52 |
+
"norm2",
|
| 53 |
+
"norm_q",
|
| 54 |
+
"norm_k",
|
| 55 |
+
"norm_post_attn",
|
| 56 |
+
"norm_post_ffn",
|
| 57 |
+
"norm_out",
|
| 58 |
+
"norm_out_modulation",
|
| 59 |
+
"router",
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def should_keep_in_fp32(name: str) -> bool:
|
| 64 |
+
return any(module_name in name.split(".") for module_name in LINGBOT_VIDEO_FP32_MODULES)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _moe_expert_backend() -> str:
|
| 68 |
+
return os.environ.get("LINGBOT_MOE_EXPERT_BACKEND", "grouped_mm").lower().strip()
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _moe_pad_backend() -> str:
|
| 72 |
+
return os.environ.get("LINGBOT_MOE_PAD_BACKEND", "loop").lower().strip()
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _moe_reorder_backend() -> str:
|
| 76 |
+
return os.environ.get("LINGBOT_MOE_REORDER_BACKEND", "sort").lower().strip()
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _moe_restore_backend() -> str:
|
| 80 |
+
return os.environ.get("LINGBOT_MOE_RESTORE_BACKEND", "scatter").lower().strip()
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _all_to_all_split_cat(
|
| 84 |
+
local_input: torch.Tensor,
|
| 85 |
+
scatter_dim: int,
|
| 86 |
+
gather_dim: int,
|
| 87 |
+
group: dist.ProcessGroup,
|
| 88 |
+
) -> torch.Tensor:
|
| 89 |
+
world_size = dist.get_world_size(group)
|
| 90 |
+
input_list = [
|
| 91 |
+
tensor.contiguous()
|
| 92 |
+
for tensor in torch.tensor_split(local_input, world_size, scatter_dim)
|
| 93 |
+
]
|
| 94 |
+
output_list = [torch.empty_like(input_list[0]) for _ in range(world_size)]
|
| 95 |
+
dist.all_to_all(output_list, input_list, group=group)
|
| 96 |
+
return torch.cat(output_list, dim=gather_dim).contiguous()
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class LingBotVideoRMSNorm(nn.Module):
|
| 100 |
+
"""RMSNorm with fp32 accumulation."""
|
| 101 |
+
|
| 102 |
+
def __init__(self, dim: int, eps: float = 1e-6):
|
| 103 |
+
super().__init__()
|
| 104 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 105 |
+
self.variance_epsilon = eps
|
| 106 |
+
|
| 107 |
+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 108 |
+
input_dtype = hidden_states.dtype
|
| 109 |
+
hidden_states = hidden_states.to(torch.float32)
|
| 110 |
+
variance = hidden_states.pow(2).mean(-1, keepdim=True)
|
| 111 |
+
hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
|
| 112 |
+
return (self.weight * hidden_states).to(input_dtype)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def apply_rotary_emb(x: torch.Tensor, freqs_cis: torch.Tensor) -> torch.Tensor:
|
| 116 |
+
"""Apply complex RoPE to `(B, S, H, D)` attention tensors."""
|
| 117 |
+
with torch.amp.autocast("cuda", enabled=False):
|
| 118 |
+
x_c = torch.view_as_complex(x.float().reshape(*x.shape[:-1], -1, 2))
|
| 119 |
+
out = torch.view_as_real(x_c * freqs_cis.unsqueeze(2)).flatten(3)
|
| 120 |
+
return out.type_as(x)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
class LingBotVideoRotaryEmbedding(nn.Module):
|
| 124 |
+
"""Complex64 RoPE table indexed by position ids."""
|
| 125 |
+
|
| 126 |
+
def __init__(self, axes_dims: Tuple[int, ...], axes_lens: Tuple[int, ...], theta: float):
|
| 127 |
+
super().__init__()
|
| 128 |
+
self.axes_dims = tuple(axes_dims)
|
| 129 |
+
self.axes_lens = list(axes_lens)
|
| 130 |
+
self.theta = theta
|
| 131 |
+
self.freqs_cis = None
|
| 132 |
+
|
| 133 |
+
@staticmethod
|
| 134 |
+
def precompute_freqs_cis(dim: Tuple[int, ...], end: Tuple[int, ...], theta: float):
|
| 135 |
+
freqs_cis = []
|
| 136 |
+
for d, e in zip(dim, end):
|
| 137 |
+
freqs = 1.0 / (
|
| 138 |
+
theta ** (torch.arange(0, d, 2, dtype=torch.float64, device="cpu") / d)
|
| 139 |
+
)
|
| 140 |
+
timestep = torch.arange(e, device=freqs.device, dtype=torch.float64)
|
| 141 |
+
freqs = torch.outer(timestep, freqs).float()
|
| 142 |
+
freqs_cis.append(torch.polar(torch.ones_like(freqs), freqs).to(torch.complex64))
|
| 143 |
+
return freqs_cis
|
| 144 |
+
|
| 145 |
+
def forward(self, position_ids: torch.Tensor) -> torch.Tensor:
|
| 146 |
+
# position_ids: (S, 3) int → (S, head_dim/2) complex64
|
| 147 |
+
device = position_ids.device
|
| 148 |
+
max_vals = position_ids.max(dim=0).values.tolist()
|
| 149 |
+
needs_rebuild = self.freqs_cis is None or any(m >= l for m, l in zip(max_vals, self.axes_lens))
|
| 150 |
+
if needs_rebuild:
|
| 151 |
+
for i in range(len(self.axes_lens)):
|
| 152 |
+
if max_vals[i] >= self.axes_lens[i]:
|
| 153 |
+
self.axes_lens[i] = int(max_vals[i] * 1.5) + 1
|
| 154 |
+
self.freqs_cis = self.precompute_freqs_cis(
|
| 155 |
+
self.axes_dims, tuple(self.axes_lens), theta=self.theta
|
| 156 |
+
)
|
| 157 |
+
self.freqs_cis = [freqs_cis.to(device) for freqs_cis in self.freqs_cis]
|
| 158 |
+
elif self.freqs_cis[0].device != device:
|
| 159 |
+
self.freqs_cis = [freqs_cis.to(device) for freqs_cis in self.freqs_cis]
|
| 160 |
+
|
| 161 |
+
return torch.cat([self.freqs_cis[i][position_ids[:, i]] for i in range(len(self.axes_dims))], dim=-1)
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def make_joint_position_ids(
|
| 165 |
+
text_len: int, grid_t: int, grid_h: int, grid_w: int, device: torch.device
|
| 166 |
+
) -> torch.Tensor:
|
| 167 |
+
"""3D positions in [video; text] order. Text t-axis is 1..text_len; video t-axis starts at text_len+1.
|
| 168 |
+
|
| 169 |
+
Matches patchify_and_embed: cap start (1,0,0); vision start (cap_len+1,0,0);
|
| 170 |
+
freqs ordered with x first and cap second (same order as cat_interleave).
|
| 171 |
+
"""
|
| 172 |
+
tt = torch.arange(grid_t, device=device, dtype=torch.int32) + (text_len + 1)
|
| 173 |
+
hh = torch.arange(grid_h, device=device, dtype=torch.int32)
|
| 174 |
+
ww = torch.arange(grid_w, device=device, dtype=torch.int32)
|
| 175 |
+
grid = torch.stack(torch.meshgrid(tt, hh, ww, indexing="ij"), dim=-1).flatten(0, 2)
|
| 176 |
+
text_t = torch.arange(text_len, device=device, dtype=torch.int32) + 1
|
| 177 |
+
text_pos = torch.stack(
|
| 178 |
+
[text_t, torch.zeros_like(text_t), torch.zeros_like(text_t)], dim=-1
|
| 179 |
+
)
|
| 180 |
+
return torch.cat([grid, text_pos], dim=0) # (Nx + L, 3)
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def _cat_interleave(
|
| 184 |
+
a: torch.Tensor,
|
| 185 |
+
len_a: list[int],
|
| 186 |
+
b: torch.Tensor,
|
| 187 |
+
len_b: list[int],
|
| 188 |
+
) -> torch.Tensor:
|
| 189 |
+
a_split = torch.split(a, len_a, dim=1)
|
| 190 |
+
b_split = torch.split(b, len_b, dim=1)
|
| 191 |
+
blocks: list[torch.Tensor] = []
|
| 192 |
+
for x_part, text_part in zip(a_split, b_split):
|
| 193 |
+
blocks.extend([x_part, text_part])
|
| 194 |
+
return torch.cat(blocks, dim=1)
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
class LingBotVideoTextEmbedder(nn.Module):
|
| 198 |
+
"""Matches CondProjection: RMSNorm(text_dim, eps=1e-6 fixed) -> Linear-SiLU-Linear."""
|
| 199 |
+
|
| 200 |
+
def __init__(self, text_dim: int, hidden_size: int):
|
| 201 |
+
super().__init__()
|
| 202 |
+
self.norm = LingBotVideoRMSNorm(text_dim, eps=1e-6)
|
| 203 |
+
self.linear_1 = nn.Linear(text_dim, hidden_size, bias=True)
|
| 204 |
+
self.linear_2 = nn.Linear(hidden_size, hidden_size, bias=True)
|
| 205 |
+
|
| 206 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 207 |
+
x = self.norm(x)
|
| 208 |
+
return self.linear_2(F.silu(self.linear_1(x)))
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
class LingBotVideoAttention(nn.Module):
|
| 212 |
+
def __init__(self, hidden_size, num_heads, norm_eps, qkv_bias, out_bias):
|
| 213 |
+
super().__init__()
|
| 214 |
+
self.num_heads = num_heads
|
| 215 |
+
self.head_dim = hidden_size // num_heads
|
| 216 |
+
self.to_q = nn.Linear(hidden_size, hidden_size, bias=qkv_bias)
|
| 217 |
+
self.to_k = nn.Linear(hidden_size, hidden_size, bias=qkv_bias)
|
| 218 |
+
self.to_v = nn.Linear(hidden_size, hidden_size, bias=qkv_bias)
|
| 219 |
+
self.norm_q = LingBotVideoRMSNorm(self.head_dim, norm_eps)
|
| 220 |
+
self.norm_k = LingBotVideoRMSNorm(self.head_dim, norm_eps)
|
| 221 |
+
self.to_out = nn.Linear(hidden_size, hidden_size, bias=out_bias)
|
| 222 |
+
|
| 223 |
+
def forward(
|
| 224 |
+
self,
|
| 225 |
+
x,
|
| 226 |
+
rotary_emb,
|
| 227 |
+
attention_mask=None,
|
| 228 |
+
packed_indices: Optional[dict[str, torch.Tensor]] = None,
|
| 229 |
+
parallel_config=None,
|
| 230 |
+
):
|
| 231 |
+
B, S, _ = x.shape
|
| 232 |
+
if os.environ.get("LINGBOT_FUSED_QKV_LINEAR") == "1":
|
| 233 |
+
weight = torch.cat(
|
| 234 |
+
(self.to_q.weight, self.to_k.weight, self.to_v.weight),
|
| 235 |
+
dim=0,
|
| 236 |
+
)
|
| 237 |
+
bias = None
|
| 238 |
+
if self.to_q.bias is not None:
|
| 239 |
+
bias = torch.cat(
|
| 240 |
+
(self.to_q.bias, self.to_k.bias, self.to_v.bias),
|
| 241 |
+
dim=0,
|
| 242 |
+
)
|
| 243 |
+
qkv = F.linear(x, weight, bias)
|
| 244 |
+
q, k, v = qkv.view(B, S, 3, self.num_heads, self.head_dim).unbind(2)
|
| 245 |
+
else:
|
| 246 |
+
q = self.to_q(x).unflatten(2, (self.num_heads, self.head_dim))
|
| 247 |
+
k = self.to_k(x).unflatten(2, (self.num_heads, self.head_dim))
|
| 248 |
+
v = self.to_v(x).unflatten(2, (self.num_heads, self.head_dim))
|
| 249 |
+
q = apply_rotary_emb(self.norm_q(q), rotary_emb)
|
| 250 |
+
k = apply_rotary_emb(self.norm_k(k), rotary_emb)
|
| 251 |
+
# dispatch_attention_fn expects (B, S, H, D) in and out (same as the diffusers Wan processor)
|
| 252 |
+
if packed_indices is None:
|
| 253 |
+
out = dispatch_attention_fn(
|
| 254 |
+
q,
|
| 255 |
+
k,
|
| 256 |
+
v,
|
| 257 |
+
attn_mask=attention_mask,
|
| 258 |
+
parallel_config=parallel_config,
|
| 259 |
+
)
|
| 260 |
+
else:
|
| 261 |
+
if flash_attn_varlen_func_v3 is None:
|
| 262 |
+
raise RuntimeError("flash_attn_interface.flash_attn_varlen_func is required.")
|
| 263 |
+
if parallel_config is None:
|
| 264 |
+
result = flash_attn_varlen_func_v3(
|
| 265 |
+
q=q.reshape(-1, self.num_heads, self.head_dim),
|
| 266 |
+
k=k.reshape(-1, self.num_heads, self.head_dim),
|
| 267 |
+
v=v.reshape(-1, self.num_heads, self.head_dim),
|
| 268 |
+
cu_seqlens_q=packed_indices["cu_seqlens_kv"],
|
| 269 |
+
cu_seqlens_k=packed_indices["cu_seqlens_kv"],
|
| 270 |
+
max_seqlen_q=packed_indices["max_seqlen_in_batch_kv"],
|
| 271 |
+
max_seqlen_k=packed_indices["max_seqlen_in_batch_kv"],
|
| 272 |
+
causal=False,
|
| 273 |
+
)
|
| 274 |
+
out = result[0] if isinstance(result, tuple) else result
|
| 275 |
+
out = out.reshape(B, S, self.num_heads, self.head_dim)
|
| 276 |
+
else:
|
| 277 |
+
group = parallel_config.context_parallel_config._ulysses_mesh.get_group()
|
| 278 |
+
world_size = dist.get_world_size(group)
|
| 279 |
+
local_heads = self.num_heads // world_size
|
| 280 |
+
q_global = _all_to_all_split_cat(
|
| 281 |
+
q.reshape(B, S, self.num_heads * self.head_dim),
|
| 282 |
+
scatter_dim=2,
|
| 283 |
+
gather_dim=1,
|
| 284 |
+
group=group,
|
| 285 |
+
).view(B, S * world_size, local_heads, self.head_dim)
|
| 286 |
+
k_global = _all_to_all_split_cat(
|
| 287 |
+
k.reshape(B, S, self.num_heads * self.head_dim),
|
| 288 |
+
scatter_dim=2,
|
| 289 |
+
gather_dim=1,
|
| 290 |
+
group=group,
|
| 291 |
+
).view(B, S * world_size, local_heads, self.head_dim)
|
| 292 |
+
v_global = _all_to_all_split_cat(
|
| 293 |
+
v.reshape(B, S, self.num_heads * self.head_dim),
|
| 294 |
+
scatter_dim=2,
|
| 295 |
+
gather_dim=1,
|
| 296 |
+
group=group,
|
| 297 |
+
).view(B, S * world_size, local_heads, self.head_dim)
|
| 298 |
+
q_flat = q_global.reshape(-1, local_heads, self.head_dim)
|
| 299 |
+
k_flat = k_global.reshape(-1, local_heads, self.head_dim)
|
| 300 |
+
v_flat = v_global.reshape(-1, local_heads, self.head_dim)
|
| 301 |
+
result = flash_attn_varlen_func_v3(
|
| 302 |
+
q=q_flat,
|
| 303 |
+
k=k_flat,
|
| 304 |
+
v=v_flat,
|
| 305 |
+
cu_seqlens_q=packed_indices["cu_seqlens_kv"],
|
| 306 |
+
cu_seqlens_k=packed_indices["cu_seqlens_kv"],
|
| 307 |
+
max_seqlen_q=packed_indices["max_seqlen_in_batch_kv"],
|
| 308 |
+
max_seqlen_k=packed_indices["max_seqlen_in_batch_kv"],
|
| 309 |
+
causal=False,
|
| 310 |
+
)
|
| 311 |
+
out_global = result[0] if isinstance(result, tuple) else result
|
| 312 |
+
out_global = out_global.reshape(B, S * world_size, local_heads * self.head_dim)
|
| 313 |
+
out = _all_to_all_split_cat(
|
| 314 |
+
out_global,
|
| 315 |
+
scatter_dim=1,
|
| 316 |
+
gather_dim=2,
|
| 317 |
+
group=group,
|
| 318 |
+
).view(B, S, self.num_heads, self.head_dim)
|
| 319 |
+
return self.to_out(out.flatten(2, 3).type_as(x))
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
class LingBotVideoMLP(nn.Module):
|
| 323 |
+
def __init__(self, hidden_size, intermediate_size):
|
| 324 |
+
super().__init__()
|
| 325 |
+
self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
|
| 326 |
+
self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False)
|
| 327 |
+
self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=False)
|
| 328 |
+
|
| 329 |
+
def forward(self, x):
|
| 330 |
+
return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
class LingBotVideoRouter(nn.Module):
|
| 334 |
+
"""Matches the TokenChoiceTopKRouter inference path (no capacity/jitter/load stats).
|
| 335 |
+
|
| 336 |
+
The asymmetry must be preserved: selection uses the bias-added score, while gating
|
| 337 |
+
weights gather the bias-free score.
|
| 338 |
+
"""
|
| 339 |
+
|
| 340 |
+
def __init__(self, hidden_size, num_experts, top_k, score_func, norm_topk_prob,
|
| 341 |
+
n_group, topk_group, route_scale):
|
| 342 |
+
super().__init__()
|
| 343 |
+
self.num_experts = num_experts
|
| 344 |
+
self.top_k = top_k
|
| 345 |
+
self.score_func = score_func
|
| 346 |
+
self.norm_topk_prob = norm_topk_prob
|
| 347 |
+
self.n_group = n_group
|
| 348 |
+
self.topk_group = topk_group
|
| 349 |
+
self.route_scale = route_scale
|
| 350 |
+
self.weight = nn.Parameter(torch.empty(num_experts, hidden_size))
|
| 351 |
+
self.register_buffer("e_score_correction_bias", torch.zeros(num_experts), persistent=True)
|
| 352 |
+
|
| 353 |
+
def _group_limited_topk(self, scores_for_choice):
|
| 354 |
+
seq_len = scores_for_choice.shape[0]
|
| 355 |
+
experts_per_group = self.num_experts // self.n_group
|
| 356 |
+
grouped = scores_for_choice.view(seq_len, self.n_group, experts_per_group)
|
| 357 |
+
group_scores = grouped.topk(2, dim=-1)[0].sum(dim=-1)
|
| 358 |
+
group_idx = torch.topk(group_scores, k=self.topk_group, dim=-1, sorted=False)[1]
|
| 359 |
+
group_mask = torch.zeros_like(group_scores)
|
| 360 |
+
group_mask.scatter_(1, group_idx, 1)
|
| 361 |
+
score_mask = (
|
| 362 |
+
group_mask.unsqueeze(-1)
|
| 363 |
+
.expand(seq_len, self.n_group, experts_per_group)
|
| 364 |
+
.reshape(seq_len, -1)
|
| 365 |
+
)
|
| 366 |
+
masked = scores_for_choice.masked_fill(~score_mask.bool(), float("-inf"))
|
| 367 |
+
return torch.topk(masked, k=self.top_k, dim=-1, sorted=False)[1]
|
| 368 |
+
|
| 369 |
+
def forward(self, tokens: torch.Tensor):
|
| 370 |
+
with torch.amp.autocast(tokens.device.type, enabled=False):
|
| 371 |
+
logits = F.linear(tokens.float(), self.weight.float())
|
| 372 |
+
if self.score_func == "softmax":
|
| 373 |
+
scores = F.softmax(logits, dim=-1)
|
| 374 |
+
else:
|
| 375 |
+
scores = logits.sigmoid()
|
| 376 |
+
scores_for_choice = scores + self.e_score_correction_bias.unsqueeze(0)
|
| 377 |
+
if self.n_group is not None and self.n_group > 1:
|
| 378 |
+
top_indices = self._group_limited_topk(scores_for_choice)
|
| 379 |
+
else:
|
| 380 |
+
top_indices = torch.topk(scores_for_choice, k=self.top_k, dim=-1, sorted=False)[1]
|
| 381 |
+
top_scores = scores.gather(1, top_indices)
|
| 382 |
+
if self.top_k > 1 and self.norm_topk_prob:
|
| 383 |
+
top_scores = top_scores / (top_scores.sum(dim=-1, keepdim=True) + 1e-20)
|
| 384 |
+
top_scores = top_scores * self.route_scale
|
| 385 |
+
return top_indices, top_scores.to(tokens.dtype), logits, scores, scores_for_choice
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
class LingBotVideoGroupedExperts(nn.Module):
|
| 389 |
+
"""Weight layout matches GroupedExperts: w1 [E,I,H], w2 [E,H,I], w3 [E,I,H]. Eager per-expert compute."""
|
| 390 |
+
|
| 391 |
+
def __init__(self, num_experts, hidden_size, intermediate_size):
|
| 392 |
+
super().__init__()
|
| 393 |
+
self.num_experts = num_experts
|
| 394 |
+
self.w1 = nn.Parameter(torch.empty(num_experts, intermediate_size, hidden_size))
|
| 395 |
+
self.w2 = nn.Parameter(torch.empty(num_experts, hidden_size, intermediate_size))
|
| 396 |
+
self.w3 = nn.Parameter(torch.empty(num_experts, intermediate_size, hidden_size))
|
| 397 |
+
|
| 398 |
+
|
| 399 |
+
def _round_up_to_multiple(value: int, multiple: int) -> int:
|
| 400 |
+
return ((value + multiple - 1) // multiple) * multiple
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
class LingBotVideoSparseMoeBlock(nn.Module):
|
| 404 |
+
def __init__(self, hidden_size, intermediate_size, num_experts, top_k,
|
| 405 |
+
moe_intermediate_size, score_func, norm_topk_prob, n_group, topk_group,
|
| 406 |
+
routed_scaling_factor, n_shared_experts):
|
| 407 |
+
super().__init__()
|
| 408 |
+
self.hidden_size = hidden_size
|
| 409 |
+
self.num_experts = num_experts
|
| 410 |
+
self.router = LingBotVideoRouter(
|
| 411 |
+
hidden_size, num_experts, top_k, score_func, norm_topk_prob,
|
| 412 |
+
n_group, topk_group, routed_scaling_factor,
|
| 413 |
+
)
|
| 414 |
+
self.experts = LingBotVideoGroupedExperts(num_experts, hidden_size, moe_intermediate_size)
|
| 415 |
+
self._sglang_w13_cache: Optional[torch.Tensor] = None
|
| 416 |
+
self._sglang_w13_cache_key = None
|
| 417 |
+
self._sglang_fp8_cache = None
|
| 418 |
+
self._sglang_fp8_cache_key = None
|
| 419 |
+
self.shared_experts = None
|
| 420 |
+
if n_shared_experts is not None and n_shared_experts > 0:
|
| 421 |
+
self.shared_experts = LingBotVideoMLP(
|
| 422 |
+
hidden_size, moe_intermediate_size * n_shared_experts
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
@staticmethod
|
| 426 |
+
def _reorder_tokens(tokens: torch.Tensor, top_scores: torch.Tensor, top_indices: torch.Tensor, num_experts: int):
|
| 427 |
+
backend = _moe_reorder_backend()
|
| 428 |
+
if backend in {"triton_pack", "pack", "triton"}:
|
| 429 |
+
return reorder_tokens_triton_pack(tokens, top_scores, top_indices, num_experts)
|
| 430 |
+
if backend not in {"sort", "argsort", "default"}:
|
| 431 |
+
raise ValueError(
|
| 432 |
+
f"Unsupported LINGBOT_MOE_REORDER_BACKEND={backend!r}; "
|
| 433 |
+
"expected sort or triton_pack"
|
| 434 |
+
)
|
| 435 |
+
num_tokens = tokens.shape[0]
|
| 436 |
+
top_k = top_indices.shape[1]
|
| 437 |
+
flat_scores = top_scores.reshape(-1)
|
| 438 |
+
flat_indices = top_indices.reshape(-1)
|
| 439 |
+
active_positions = torch.where(flat_scores != 0)[0]
|
| 440 |
+
active_experts = flat_indices[active_positions]
|
| 441 |
+
|
| 442 |
+
counts = torch.zeros(num_experts, device=tokens.device, dtype=torch.int64)
|
| 443 |
+
counts.scatter_add_(0, active_experts, torch.ones_like(active_experts, dtype=torch.int64))
|
| 444 |
+
|
| 445 |
+
sort_order = torch.argsort(active_experts, stable=True)
|
| 446 |
+
sorted_positions = active_positions[sort_order]
|
| 447 |
+
sorted_scores = flat_scores[sorted_positions]
|
| 448 |
+
original_token_idx = sorted_positions // top_k
|
| 449 |
+
permuted_tokens = tokens[original_token_idx]
|
| 450 |
+
return permuted_tokens, counts, sorted_positions, sorted_scores, num_tokens, top_k
|
| 451 |
+
|
| 452 |
+
@staticmethod
|
| 453 |
+
def _pad_grouped_tokens_loop(tokens: torch.Tensor, counts: torch.Tensor, align: int = 8):
|
| 454 |
+
num_tokens = tokens.shape[0]
|
| 455 |
+
num_experts = int(counts.shape[0])
|
| 456 |
+
max_len = _round_up_to_multiple(num_tokens + num_experts * align, align)
|
| 457 |
+
counts_i64 = counts.to(torch.int64)
|
| 458 |
+
total_per_expert = torch.clamp_min(counts_i64, align)
|
| 459 |
+
aligned_counts = (
|
| 460 |
+
(total_per_expert + align - 1) // align * align
|
| 461 |
+
).to(torch.int32)
|
| 462 |
+
write_offsets = torch.cumsum(aligned_counts, dim=0) - aligned_counts
|
| 463 |
+
start_indices = torch.cumsum(counts_i64, dim=0) - counts_i64
|
| 464 |
+
|
| 465 |
+
fill_value = num_tokens
|
| 466 |
+
permuted_indices = torch.full(
|
| 467 |
+
(max_len,), fill_value, dtype=torch.int64, device=tokens.device
|
| 468 |
+
)
|
| 469 |
+
for expert_idx in range(num_experts):
|
| 470 |
+
length = int(counts_i64[expert_idx].item())
|
| 471 |
+
if length == 0:
|
| 472 |
+
continue
|
| 473 |
+
write_start = int(write_offsets[expert_idx].item())
|
| 474 |
+
start = int(start_indices[expert_idx].item())
|
| 475 |
+
permuted_indices[write_start:write_start + length] = torch.arange(
|
| 476 |
+
start, start + length, device=tokens.device, dtype=torch.int64
|
| 477 |
+
)
|
| 478 |
+
|
| 479 |
+
tokens_with_pad = torch.vstack((tokens, tokens.new_zeros((tokens.shape[-1],))))
|
| 480 |
+
input_shape = tokens_with_pad.shape
|
| 481 |
+
return input_shape, tokens_with_pad[permuted_indices], permuted_indices, aligned_counts
|
| 482 |
+
|
| 483 |
+
@staticmethod
|
| 484 |
+
def _pad_grouped_tokens_vectorized(tokens: torch.Tensor, counts: torch.Tensor, align: int = 8):
|
| 485 |
+
num_tokens = tokens.shape[0]
|
| 486 |
+
num_experts = int(counts.shape[0])
|
| 487 |
+
max_len = _round_up_to_multiple(num_tokens + num_experts * align, align)
|
| 488 |
+
counts_i64 = counts.to(torch.int64)
|
| 489 |
+
total_per_expert = torch.clamp_min(counts_i64, align)
|
| 490 |
+
aligned_counts_i64 = (total_per_expert + align - 1) // align * align
|
| 491 |
+
write_offsets = torch.cumsum(aligned_counts_i64, dim=0) - aligned_counts_i64
|
| 492 |
+
end_offsets = torch.cumsum(aligned_counts_i64, dim=0)
|
| 493 |
+
start_indices = torch.cumsum(counts_i64, dim=0) - counts_i64
|
| 494 |
+
|
| 495 |
+
slots = torch.arange(max_len, dtype=torch.int64, device=tokens.device)
|
| 496 |
+
expert_idx = torch.bucketize(slots, end_offsets, right=True)
|
| 497 |
+
valid_expert = expert_idx < num_experts
|
| 498 |
+
safe_expert_idx = expert_idx.clamp(max=num_experts - 1)
|
| 499 |
+
local_idx = slots - write_offsets[safe_expert_idx]
|
| 500 |
+
source_idx = start_indices[safe_expert_idx] + local_idx
|
| 501 |
+
valid = valid_expert & (local_idx < counts_i64[safe_expert_idx])
|
| 502 |
+
fill = torch.full_like(source_idx, num_tokens)
|
| 503 |
+
permuted_indices = torch.where(valid, source_idx, fill)
|
| 504 |
+
|
| 505 |
+
tokens_with_pad = torch.vstack((tokens, tokens.new_zeros((tokens.shape[-1],))))
|
| 506 |
+
input_shape = tokens_with_pad.shape
|
| 507 |
+
return (
|
| 508 |
+
input_shape,
|
| 509 |
+
tokens_with_pad[permuted_indices],
|
| 510 |
+
permuted_indices,
|
| 511 |
+
aligned_counts_i64.to(torch.int32),
|
| 512 |
+
)
|
| 513 |
+
|
| 514 |
+
@staticmethod
|
| 515 |
+
def _pad_grouped_tokens(tokens: torch.Tensor, counts: torch.Tensor, align: int = 8):
|
| 516 |
+
backend = _moe_pad_backend()
|
| 517 |
+
if backend in {"loop", "default"}:
|
| 518 |
+
return LingBotVideoSparseMoeBlock._pad_grouped_tokens_loop(tokens, counts, align)
|
| 519 |
+
if backend in {"vectorized", "torch"}:
|
| 520 |
+
return LingBotVideoSparseMoeBlock._pad_grouped_tokens_vectorized(tokens, counts, align)
|
| 521 |
+
raise ValueError(
|
| 522 |
+
f"Unsupported LINGBOT_MOE_PAD_BACKEND={backend!r}; expected loop or vectorized"
|
| 523 |
+
)
|
| 524 |
+
|
| 525 |
+
@staticmethod
|
| 526 |
+
def _unpad_grouped_tokens(output: torch.Tensor, input_shape: torch.Size, permuted_indices: torch.Tensor):
|
| 527 |
+
unpermuted = output.new_empty(input_shape)
|
| 528 |
+
unpermuted[permuted_indices, :] = output
|
| 529 |
+
return unpermuted[:-1]
|
| 530 |
+
|
| 531 |
+
def _run_grouped_experts(self, tokens: torch.Tensor, counts: torch.Tensor) -> torch.Tensor:
|
| 532 |
+
if not hasattr(torch, "_grouped_mm"):
|
| 533 |
+
return self._run_experts_for_loop(tokens, counts)
|
| 534 |
+
input_shape, padded_tokens, permuted_indices, aligned_counts = self._pad_grouped_tokens(tokens, counts)
|
| 535 |
+
offsets = torch.cumsum(aligned_counts, dim=0, dtype=torch.int32)
|
| 536 |
+
h = F.silu(
|
| 537 |
+
torch._grouped_mm(
|
| 538 |
+
padded_tokens.bfloat16(),
|
| 539 |
+
self.experts.w1.bfloat16().transpose(-2, -1),
|
| 540 |
+
offs=offsets,
|
| 541 |
+
)
|
| 542 |
+
)
|
| 543 |
+
h = h * torch._grouped_mm(
|
| 544 |
+
padded_tokens.bfloat16(),
|
| 545 |
+
self.experts.w3.bfloat16().transpose(-2, -1),
|
| 546 |
+
offs=offsets,
|
| 547 |
+
)
|
| 548 |
+
out = torch._grouped_mm(
|
| 549 |
+
h,
|
| 550 |
+
self.experts.w2.bfloat16().transpose(-2, -1),
|
| 551 |
+
offs=offsets,
|
| 552 |
+
).type_as(padded_tokens)
|
| 553 |
+
return self._unpad_grouped_tokens(out, input_shape, permuted_indices)
|
| 554 |
+
|
| 555 |
+
def _run_experts_for_loop(self, tokens: torch.Tensor, counts: torch.Tensor) -> torch.Tensor:
|
| 556 |
+
count_list = counts.tolist()
|
| 557 |
+
splits = torch.split(tokens, count_list, dim=0)
|
| 558 |
+
outputs = []
|
| 559 |
+
for expert_idx, expert_tokens in enumerate(splits):
|
| 560 |
+
if expert_tokens.numel() == 0:
|
| 561 |
+
continue
|
| 562 |
+
h = F.silu(expert_tokens @ self.experts.w1[expert_idx].transpose(-2, -1))
|
| 563 |
+
h = h * (expert_tokens @ self.experts.w3[expert_idx].transpose(-2, -1))
|
| 564 |
+
h = h @ self.experts.w2[expert_idx].transpose(-2, -1)
|
| 565 |
+
outputs.append(h)
|
| 566 |
+
if not outputs:
|
| 567 |
+
return tokens.new_zeros(tokens.shape)
|
| 568 |
+
return torch.cat(outputs, dim=0)
|
| 569 |
+
|
| 570 |
+
def _get_sglang_w13(self) -> torch.Tensor:
|
| 571 |
+
key = (
|
| 572 |
+
self.experts.w1.data_ptr(),
|
| 573 |
+
self.experts.w3.data_ptr(),
|
| 574 |
+
self.experts.w1.device,
|
| 575 |
+
self.experts.w3.device,
|
| 576 |
+
self.experts.w1.dtype,
|
| 577 |
+
self.experts.w3.dtype,
|
| 578 |
+
)
|
| 579 |
+
if self._sglang_w13_cache is None or self._sglang_w13_cache_key != key:
|
| 580 |
+
self._sglang_w13_cache = torch.cat(
|
| 581 |
+
(self.experts.w1.bfloat16(), self.experts.w3.bfloat16()), dim=1
|
| 582 |
+
).contiguous()
|
| 583 |
+
self._sglang_w13_cache_key = key
|
| 584 |
+
return self._sglang_w13_cache
|
| 585 |
+
|
| 586 |
+
@staticmethod
|
| 587 |
+
def _quantize_fp8_weight_per_expert(weight: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 588 |
+
weight_float = weight.float()
|
| 589 |
+
scale = fp8_scale_from_amax(weight_float.abs().amax(dim=(1, 2)))
|
| 590 |
+
quantized = quantize_to_fp8_e4m3fn(weight_float, scale[:, None, None]).contiguous()
|
| 591 |
+
return quantized, scale.contiguous()
|
| 592 |
+
|
| 593 |
+
def _get_sglang_fp8_weights(self) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 594 |
+
key = (
|
| 595 |
+
self.experts.w1.data_ptr(),
|
| 596 |
+
self.experts.w2.data_ptr(),
|
| 597 |
+
self.experts.w3.data_ptr(),
|
| 598 |
+
self.experts.w1.device,
|
| 599 |
+
self.experts.w2.device,
|
| 600 |
+
self.experts.w3.device,
|
| 601 |
+
self.experts.w1.dtype,
|
| 602 |
+
self.experts.w2.dtype,
|
| 603 |
+
self.experts.w3.dtype,
|
| 604 |
+
)
|
| 605 |
+
if self._sglang_fp8_cache is None or self._sglang_fp8_cache_key != key:
|
| 606 |
+
w13 = torch.cat((self.experts.w1.float(), self.experts.w3.float()), dim=1).contiguous()
|
| 607 |
+
w13_fp8, w13_scale = self._quantize_fp8_weight_per_expert(w13)
|
| 608 |
+
w2_fp8, w2_scale = self._quantize_fp8_weight_per_expert(self.experts.w2)
|
| 609 |
+
self._sglang_fp8_cache = (w13_fp8, w2_fp8, w13_scale, w2_scale)
|
| 610 |
+
self._sglang_fp8_cache_key = key
|
| 611 |
+
return self._sglang_fp8_cache
|
| 612 |
+
|
| 613 |
+
def _run_sglang_triton_experts(
|
| 614 |
+
self,
|
| 615 |
+
tokens: torch.Tensor,
|
| 616 |
+
top_scores: torch.Tensor,
|
| 617 |
+
top_indices: torch.Tensor,
|
| 618 |
+
) -> torch.Tensor:
|
| 619 |
+
ensure_sglang_moe_ready()
|
| 620 |
+
topk_output = LightSglangStandardTopKOutput(
|
| 621 |
+
top_scores.float(),
|
| 622 |
+
top_indices.to(torch.int32),
|
| 623 |
+
torch.empty(0, device=tokens.device),
|
| 624 |
+
)
|
| 625 |
+
runner_config = LightSglangMoeRunnerConfig(
|
| 626 |
+
num_experts=self.num_experts,
|
| 627 |
+
num_local_experts=self.num_experts,
|
| 628 |
+
activation="silu",
|
| 629 |
+
is_gated=True,
|
| 630 |
+
inplace=False,
|
| 631 |
+
)
|
| 632 |
+
return sglang_fused_experts(
|
| 633 |
+
tokens.contiguous().bfloat16(),
|
| 634 |
+
self._get_sglang_w13(),
|
| 635 |
+
self.experts.w2.bfloat16().contiguous(),
|
| 636 |
+
topk_output,
|
| 637 |
+
runner_config,
|
| 638 |
+
).type_as(tokens)
|
| 639 |
+
|
| 640 |
+
def _run_sglang_triton_fp8_experts(
|
| 641 |
+
self,
|
| 642 |
+
tokens: torch.Tensor,
|
| 643 |
+
top_scores: torch.Tensor,
|
| 644 |
+
top_indices: torch.Tensor,
|
| 645 |
+
) -> torch.Tensor:
|
| 646 |
+
ensure_sglang_moe_ready()
|
| 647 |
+
topk_output = LightSglangStandardTopKOutput(
|
| 648 |
+
top_scores.float(),
|
| 649 |
+
top_indices.to(torch.int32),
|
| 650 |
+
torch.empty(0, device=tokens.device),
|
| 651 |
+
)
|
| 652 |
+
runner_config = LightSglangMoeRunnerConfig(
|
| 653 |
+
num_experts=self.num_experts,
|
| 654 |
+
num_local_experts=self.num_experts,
|
| 655 |
+
activation="silu",
|
| 656 |
+
is_gated=True,
|
| 657 |
+
inplace=False,
|
| 658 |
+
)
|
| 659 |
+
w13_fp8, w2_fp8, w13_scale, w2_scale = self._get_sglang_fp8_weights()
|
| 660 |
+
return sglang_fused_experts(
|
| 661 |
+
tokens.contiguous().bfloat16(),
|
| 662 |
+
w13_fp8,
|
| 663 |
+
w2_fp8,
|
| 664 |
+
topk_output,
|
| 665 |
+
runner_config,
|
| 666 |
+
use_fp8_w8a8=True,
|
| 667 |
+
w1_scale=w13_scale,
|
| 668 |
+
w2_scale=w2_scale,
|
| 669 |
+
).type_as(tokens)
|
| 670 |
+
|
| 671 |
+
def _run_selected_experts(
|
| 672 |
+
self,
|
| 673 |
+
tokens: torch.Tensor,
|
| 674 |
+
top_scores: torch.Tensor,
|
| 675 |
+
top_indices: torch.Tensor,
|
| 676 |
+
) -> torch.Tensor:
|
| 677 |
+
backend = _moe_expert_backend()
|
| 678 |
+
if backend in {"grouped_mm", "torch_grouped_mm", "default"}:
|
| 679 |
+
(
|
| 680 |
+
permuted_tokens,
|
| 681 |
+
counts,
|
| 682 |
+
sorted_positions,
|
| 683 |
+
sorted_scores,
|
| 684 |
+
num_tokens,
|
| 685 |
+
top_k,
|
| 686 |
+
) = self._reorder_tokens(tokens, top_scores, top_indices, self.router.num_experts)
|
| 687 |
+
expert_output = self._run_grouped_experts(permuted_tokens, counts)
|
| 688 |
+
return self._restore_tokens(
|
| 689 |
+
expert_output,
|
| 690 |
+
sorted_positions,
|
| 691 |
+
sorted_scores,
|
| 692 |
+
num_tokens,
|
| 693 |
+
top_k,
|
| 694 |
+
)
|
| 695 |
+
if backend in {"sglang_triton", "triton", "sglang"}:
|
| 696 |
+
return self._run_sglang_triton_experts(tokens, top_scores, top_indices)
|
| 697 |
+
if backend in {"sglang_triton_fp8", "triton_fp8", "sglang_fp8"}:
|
| 698 |
+
return self._run_sglang_triton_fp8_experts(tokens, top_scores, top_indices)
|
| 699 |
+
raise ValueError(
|
| 700 |
+
f"Unsupported LINGBOT_MOE_EXPERT_BACKEND={backend!r}; "
|
| 701 |
+
"expected grouped_mm, sglang_triton, or sglang_triton_fp8"
|
| 702 |
+
)
|
| 703 |
+
|
| 704 |
+
@staticmethod
|
| 705 |
+
def _restore_tokens(
|
| 706 |
+
expert_output: torch.Tensor,
|
| 707 |
+
sorted_positions: torch.Tensor,
|
| 708 |
+
sorted_scores: torch.Tensor,
|
| 709 |
+
num_tokens: int,
|
| 710 |
+
top_k: int,
|
| 711 |
+
) -> torch.Tensor:
|
| 712 |
+
backend = _moe_restore_backend()
|
| 713 |
+
if backend in {"triton", "triton_fused", "fused"}:
|
| 714 |
+
return LingBotVideoSparseMoeBlock._restore_tokens_triton(
|
| 715 |
+
expert_output,
|
| 716 |
+
sorted_positions,
|
| 717 |
+
sorted_scores,
|
| 718 |
+
num_tokens,
|
| 719 |
+
top_k,
|
| 720 |
+
)
|
| 721 |
+
if backend in {"index_add", "index_add_", "scatter_add"}:
|
| 722 |
+
return LingBotVideoSparseMoeBlock._restore_tokens_index_add(
|
| 723 |
+
expert_output,
|
| 724 |
+
sorted_positions,
|
| 725 |
+
sorted_scores,
|
| 726 |
+
num_tokens,
|
| 727 |
+
top_k,
|
| 728 |
+
)
|
| 729 |
+
if backend in {"weighted_scatter", "weighted", "fast_scatter"}:
|
| 730 |
+
return LingBotVideoSparseMoeBlock._restore_tokens_weighted_scatter(
|
| 731 |
+
expert_output,
|
| 732 |
+
sorted_positions,
|
| 733 |
+
sorted_scores,
|
| 734 |
+
num_tokens,
|
| 735 |
+
top_k,
|
| 736 |
+
)
|
| 737 |
+
if backend in {"chunked", "chunked_scatter", "scatter_chunked"}:
|
| 738 |
+
return LingBotVideoSparseMoeBlock._restore_tokens_chunked_scatter(
|
| 739 |
+
expert_output,
|
| 740 |
+
sorted_positions,
|
| 741 |
+
sorted_scores,
|
| 742 |
+
num_tokens,
|
| 743 |
+
top_k,
|
| 744 |
+
)
|
| 745 |
+
if backend not in {"scatter", "default"}:
|
| 746 |
+
raise ValueError(
|
| 747 |
+
f"Unsupported LINGBOT_MOE_RESTORE_BACKEND={backend!r}; "
|
| 748 |
+
"expected scatter, chunked_scatter, weighted_scatter, index_add, or triton"
|
| 749 |
+
)
|
| 750 |
+
dim = expert_output.shape[-1]
|
| 751 |
+
unsorted = torch.zeros(
|
| 752 |
+
(num_tokens * top_k, dim),
|
| 753 |
+
dtype=expert_output.dtype,
|
| 754 |
+
device=expert_output.device,
|
| 755 |
+
)
|
| 756 |
+
unsorted[sorted_positions] = expert_output
|
| 757 |
+
unsorted = unsorted.reshape(num_tokens, top_k, dim)
|
| 758 |
+
|
| 759 |
+
scores_unsorted = torch.zeros(
|
| 760 |
+
num_tokens * top_k,
|
| 761 |
+
dtype=sorted_scores.dtype,
|
| 762 |
+
device=sorted_scores.device,
|
| 763 |
+
)
|
| 764 |
+
scores_unsorted[sorted_positions] = sorted_scores
|
| 765 |
+
scores_unsorted = scores_unsorted.reshape(num_tokens, top_k, 1)
|
| 766 |
+
return (unsorted.float() * scores_unsorted).sum(dim=1).to(expert_output.dtype)
|
| 767 |
+
|
| 768 |
+
@staticmethod
|
| 769 |
+
def _restore_tokens_chunked_scatter(
|
| 770 |
+
expert_output: torch.Tensor,
|
| 771 |
+
sorted_positions: torch.Tensor,
|
| 772 |
+
sorted_scores: torch.Tensor,
|
| 773 |
+
num_tokens: int,
|
| 774 |
+
top_k: int,
|
| 775 |
+
) -> torch.Tensor:
|
| 776 |
+
dim = expert_output.shape[-1]
|
| 777 |
+
chunk_size = int(os.environ.get("LINGBOT_MOE_RESTORE_CHUNK_SIZE", "128"))
|
| 778 |
+
if chunk_size <= 0:
|
| 779 |
+
raise ValueError("LINGBOT_MOE_RESTORE_CHUNK_SIZE must be positive")
|
| 780 |
+
|
| 781 |
+
scores_unsorted = torch.zeros(
|
| 782 |
+
num_tokens * top_k,
|
| 783 |
+
dtype=sorted_scores.dtype,
|
| 784 |
+
device=sorted_scores.device,
|
| 785 |
+
)
|
| 786 |
+
scores_unsorted[sorted_positions] = sorted_scores
|
| 787 |
+
scores_unsorted = scores_unsorted.reshape(num_tokens, top_k, 1)
|
| 788 |
+
output = expert_output.new_empty((num_tokens, dim))
|
| 789 |
+
for start in range(0, dim, chunk_size):
|
| 790 |
+
end = min(start + chunk_size, dim)
|
| 791 |
+
unsorted = torch.zeros(
|
| 792 |
+
(num_tokens * top_k, end - start),
|
| 793 |
+
dtype=expert_output.dtype,
|
| 794 |
+
device=expert_output.device,
|
| 795 |
+
)
|
| 796 |
+
unsorted[sorted_positions] = expert_output[:, start:end]
|
| 797 |
+
unsorted = unsorted.reshape(num_tokens, top_k, end - start)
|
| 798 |
+
output[:, start:end] = (unsorted.float() * scores_unsorted).sum(dim=1).to(
|
| 799 |
+
expert_output.dtype
|
| 800 |
+
)
|
| 801 |
+
return output
|
| 802 |
+
|
| 803 |
+
@staticmethod
|
| 804 |
+
def _restore_tokens_triton(
|
| 805 |
+
expert_output: torch.Tensor,
|
| 806 |
+
sorted_positions: torch.Tensor,
|
| 807 |
+
sorted_scores: torch.Tensor,
|
| 808 |
+
num_tokens: int,
|
| 809 |
+
top_k: int,
|
| 810 |
+
) -> torch.Tensor:
|
| 811 |
+
return restore_tokens_triton(
|
| 812 |
+
expert_output,
|
| 813 |
+
sorted_positions,
|
| 814 |
+
sorted_scores,
|
| 815 |
+
num_tokens,
|
| 816 |
+
top_k,
|
| 817 |
+
)
|
| 818 |
+
|
| 819 |
+
@staticmethod
|
| 820 |
+
def _restore_tokens_weighted_scatter(
|
| 821 |
+
expert_output: torch.Tensor,
|
| 822 |
+
sorted_positions: torch.Tensor,
|
| 823 |
+
sorted_scores: torch.Tensor,
|
| 824 |
+
num_tokens: int,
|
| 825 |
+
top_k: int,
|
| 826 |
+
) -> torch.Tensor:
|
| 827 |
+
weighted = (expert_output * sorted_scores[:, None].to(expert_output.dtype)).to(expert_output.dtype)
|
| 828 |
+
unsorted = torch.zeros(
|
| 829 |
+
(num_tokens * top_k, expert_output.shape[-1]),
|
| 830 |
+
dtype=expert_output.dtype,
|
| 831 |
+
device=expert_output.device,
|
| 832 |
+
)
|
| 833 |
+
unsorted[sorted_positions] = weighted
|
| 834 |
+
return unsorted.reshape(num_tokens, top_k, expert_output.shape[-1]).sum(dim=1)
|
| 835 |
+
|
| 836 |
+
@staticmethod
|
| 837 |
+
def _restore_tokens_index_add(
|
| 838 |
+
expert_output: torch.Tensor,
|
| 839 |
+
sorted_positions: torch.Tensor,
|
| 840 |
+
sorted_scores: torch.Tensor,
|
| 841 |
+
num_tokens: int,
|
| 842 |
+
top_k: int,
|
| 843 |
+
) -> torch.Tensor:
|
| 844 |
+
token_indices = torch.div(sorted_positions, top_k, rounding_mode="floor")
|
| 845 |
+
weighted = expert_output.float() * sorted_scores[:, None].float()
|
| 846 |
+
out = torch.zeros(
|
| 847 |
+
(num_tokens, expert_output.shape[-1]),
|
| 848 |
+
dtype=torch.float32,
|
| 849 |
+
device=expert_output.device,
|
| 850 |
+
)
|
| 851 |
+
out.index_add_(0, token_indices, weighted)
|
| 852 |
+
return out.to(expert_output.dtype)
|
| 853 |
+
|
| 854 |
+
def forward(self, hidden_states: torch.Tensor, padding_mask: Optional[torch.Tensor] = None):
|
| 855 |
+
# hidden_states: (B, S, H); padding_mask: (B*S,) with 1=valid (only needed when B>1)
|
| 856 |
+
B = hidden_states.shape[0]
|
| 857 |
+
tokens = hidden_states.view(-1, self.hidden_size)
|
| 858 |
+
top_indices, top_scores, logits, scores, scores_for_choice = self.router(tokens)
|
| 859 |
+
del logits, scores, scores_for_choice
|
| 860 |
+
if padding_mask is not None:
|
| 861 |
+
pm = padding_mask.unsqueeze(-1).to(top_scores.dtype)
|
| 862 |
+
top_scores = top_scores * pm
|
| 863 |
+
top_scores = top_scores / (top_scores.sum(dim=-1, keepdim=True) + 1e-9)
|
| 864 |
+
top_scores = top_scores * self.router.route_scale
|
| 865 |
+
|
| 866 |
+
out = self._run_selected_experts(tokens, top_scores, top_indices)
|
| 867 |
+
|
| 868 |
+
out = out.view(B, -1, self.hidden_size)
|
| 869 |
+
if self.shared_experts is not None:
|
| 870 |
+
shared_output = self.shared_experts(hidden_states)
|
| 871 |
+
out = out + shared_output
|
| 872 |
+
return out
|
| 873 |
+
|
| 874 |
+
|
| 875 |
+
class LingBotVideoBlock(nn.Module):
|
| 876 |
+
def __init__(
|
| 877 |
+
self,
|
| 878 |
+
hidden_size,
|
| 879 |
+
num_attention_heads,
|
| 880 |
+
intermediate_size,
|
| 881 |
+
norm_eps,
|
| 882 |
+
qkv_bias,
|
| 883 |
+
out_bias,
|
| 884 |
+
num_experts,
|
| 885 |
+
num_experts_per_tok,
|
| 886 |
+
moe_intermediate_size,
|
| 887 |
+
decoder_sparse_step,
|
| 888 |
+
mlp_only_layers,
|
| 889 |
+
n_shared_experts,
|
| 890 |
+
score_func,
|
| 891 |
+
norm_topk_prob,
|
| 892 |
+
n_group,
|
| 893 |
+
topk_group,
|
| 894 |
+
routed_scaling_factor,
|
| 895 |
+
layer_idx: int,
|
| 896 |
+
):
|
| 897 |
+
super().__init__()
|
| 898 |
+
self.layer_idx = layer_idx
|
| 899 |
+
h = hidden_size
|
| 900 |
+
self.scale_shift_table = nn.Parameter(torch.zeros(1, 6 * h))
|
| 901 |
+
self.norm1 = LingBotVideoRMSNorm(h, norm_eps)
|
| 902 |
+
self.attn = LingBotVideoAttention(
|
| 903 |
+
h, num_attention_heads, norm_eps, qkv_bias, out_bias
|
| 904 |
+
)
|
| 905 |
+
self.norm_post_attn = LingBotVideoRMSNorm(h, norm_eps)
|
| 906 |
+
self.norm2 = LingBotVideoRMSNorm(h, norm_eps)
|
| 907 |
+
# Sparsity decision matches MoEBlock: mlp_only_layers + decoder_sparse_step + num_experts
|
| 908 |
+
if layer_idx not in mlp_only_layers and (
|
| 909 |
+
num_experts > 0 and (layer_idx + 1) % decoder_sparse_step == 0
|
| 910 |
+
):
|
| 911 |
+
self.ffn = LingBotVideoSparseMoeBlock(
|
| 912 |
+
h, intermediate_size, num_experts, num_experts_per_tok,
|
| 913 |
+
moe_intermediate_size, score_func, norm_topk_prob,
|
| 914 |
+
n_group, topk_group, routed_scaling_factor,
|
| 915 |
+
n_shared_experts,
|
| 916 |
+
)
|
| 917 |
+
else:
|
| 918 |
+
self.ffn = LingBotVideoMLP(h, intermediate_size)
|
| 919 |
+
self.norm_post_ffn = LingBotVideoRMSNorm(h, norm_eps)
|
| 920 |
+
|
| 921 |
+
def forward(
|
| 922 |
+
self,
|
| 923 |
+
x,
|
| 924 |
+
temb6,
|
| 925 |
+
rotary_emb,
|
| 926 |
+
attention_mask=None,
|
| 927 |
+
moe_padding_mask=None,
|
| 928 |
+
packed_indices: Optional[dict[str, torch.Tensor]] = None,
|
| 929 |
+
parallel_config=None,
|
| 930 |
+
):
|
| 931 |
+
expected_tokens = x.shape[0] * x.shape[1]
|
| 932 |
+
if temb6.ndim != 2 or temb6.shape[0] != expected_tokens:
|
| 933 |
+
raise ValueError(
|
| 934 |
+
"LingBotVideoBlock expects token-level temb6 with shape "
|
| 935 |
+
f"(B*S, 6D); got {tuple(temb6.shape)} for hidden states {tuple(x.shape)}."
|
| 936 |
+
)
|
| 937 |
+
# AdaLN mod: dense and MoE both keep scale_shift_table fp32 (master
|
| 938 |
+
# moe/models.py:80 dropped the accidental `.to(dtype=c.dtype)` cast).
|
| 939 |
+
mod = temb6.view(x.shape[0], x.shape[1], -1) + self.scale_shift_table.unsqueeze(0)
|
| 940 |
+
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = mod.chunk(6, dim=-1)
|
| 941 |
+
gate_msa, gate_mlp = gate_msa.tanh(), gate_mlp.tanh()
|
| 942 |
+
scale_msa, scale_mlp = 1.0 + scale_msa, 1.0 + scale_mlp
|
| 943 |
+
|
| 944 |
+
# AdaLN modulation / norms run in fp32 (sensitive path); cast to the bulk
|
| 945 |
+
# compute dtype only at the bf16 Linear boundary. This replaces the old
|
| 946 |
+
# ambient autocast, which rounded Linear inputs to bf16 at the same point.
|
| 947 |
+
bulk_dtype = self.attn.to_q.weight.dtype
|
| 948 |
+
attn_in = (self.norm1(x) * scale_msa + shift_msa).to(bulk_dtype)
|
| 949 |
+
attn_out = self.attn(
|
| 950 |
+
attn_in,
|
| 951 |
+
rotary_emb,
|
| 952 |
+
attention_mask,
|
| 953 |
+
packed_indices=packed_indices,
|
| 954 |
+
parallel_config=parallel_config,
|
| 955 |
+
)
|
| 956 |
+
x = x + (gate_msa * self.norm_post_attn(attn_out)).to(x.dtype)
|
| 957 |
+
|
| 958 |
+
ffn_in = (self.norm2(x) * scale_mlp + shift_mlp).to(bulk_dtype)
|
| 959 |
+
if isinstance(self.ffn, LingBotVideoSparseMoeBlock):
|
| 960 |
+
ffn_out = self.ffn(ffn_in, padding_mask=moe_padding_mask)
|
| 961 |
+
else:
|
| 962 |
+
ffn_out = self.ffn(ffn_in)
|
| 963 |
+
ffn_normed = self.norm_post_ffn(ffn_out)
|
| 964 |
+
x = x + (gate_mlp * ffn_normed).to(x.dtype)
|
| 965 |
+
return x
|
| 966 |
+
|
| 967 |
+
|
| 968 |
+
class LingBotVideoTransformer3DModel(ModelMixin, ConfigMixin):
|
| 969 |
+
_supports_gradient_checkpointing = False
|
| 970 |
+
_no_split_modules = ["LingBotVideoBlock"]
|
| 971 |
+
_keep_in_fp32_modules = list(LINGBOT_VIDEO_FP32_MODULES)
|
| 972 |
+
|
| 973 |
+
def to(self, *args, **kwargs):
|
| 974 |
+
device, dtype, non_blocking, _ = torch._C._nn._parse_to(*args, **kwargs)
|
| 975 |
+
if dtype is None or dtype == torch.float32:
|
| 976 |
+
return super().to(*args, **kwargs)
|
| 977 |
+
|
| 978 |
+
dtype_is_floating = torch.is_floating_point(torch.empty((), dtype=dtype))
|
| 979 |
+
if not dtype_is_floating:
|
| 980 |
+
return super().to(*args, **kwargs)
|
| 981 |
+
|
| 982 |
+
if device is not None:
|
| 983 |
+
super().to(device=device, non_blocking=non_blocking)
|
| 984 |
+
|
| 985 |
+
for name, param in self.named_parameters():
|
| 986 |
+
if not torch.is_floating_point(param):
|
| 987 |
+
continue
|
| 988 |
+
target_dtype = torch.float32 if should_keep_in_fp32(name) else dtype
|
| 989 |
+
param.data = param.data.to(dtype=target_dtype, non_blocking=non_blocking)
|
| 990 |
+
if param.grad is not None:
|
| 991 |
+
param.grad.data = param.grad.data.to(dtype=target_dtype, non_blocking=non_blocking)
|
| 992 |
+
|
| 993 |
+
for name, buffer in self.named_buffers():
|
| 994 |
+
if not torch.is_floating_point(buffer):
|
| 995 |
+
continue
|
| 996 |
+
target_dtype = torch.float32 if should_keep_in_fp32(name) else dtype
|
| 997 |
+
buffer.data = buffer.data.to(dtype=target_dtype, non_blocking=non_blocking)
|
| 998 |
+
|
| 999 |
+
return self
|
| 1000 |
+
|
| 1001 |
+
@register_to_config
|
| 1002 |
+
def __init__(
|
| 1003 |
+
self,
|
| 1004 |
+
patch_size: Tuple[int, int, int] = (1, 2, 2),
|
| 1005 |
+
in_channels: int = 16,
|
| 1006 |
+
out_channels: int = 16,
|
| 1007 |
+
hidden_size: int = 2048,
|
| 1008 |
+
num_attention_heads: int = 16,
|
| 1009 |
+
depth: int = 24,
|
| 1010 |
+
intermediate_size: int = 6144,
|
| 1011 |
+
text_dim: int = 2560,
|
| 1012 |
+
freq_dim: int = 256,
|
| 1013 |
+
norm_eps: float = 1e-6,
|
| 1014 |
+
rope_theta: float = 256.0,
|
| 1015 |
+
axes_dims: Tuple[int, int, int] = (32, 48, 48),
|
| 1016 |
+
axes_lens: Tuple[int, int, int] = (8192, 1024, 1024),
|
| 1017 |
+
qkv_bias: bool = False,
|
| 1018 |
+
out_bias: bool = True,
|
| 1019 |
+
patch_embed_bias: bool = True,
|
| 1020 |
+
timestep_mlp_bias: bool = True,
|
| 1021 |
+
num_experts: int = 0,
|
| 1022 |
+
num_experts_per_tok: int = 8,
|
| 1023 |
+
moe_intermediate_size: int = 512,
|
| 1024 |
+
decoder_sparse_step: int = 1,
|
| 1025 |
+
mlp_only_layers: Tuple[int, ...] = (),
|
| 1026 |
+
n_shared_experts: Optional[int] = None,
|
| 1027 |
+
score_func: str = "sigmoid",
|
| 1028 |
+
norm_topk_prob: bool = True,
|
| 1029 |
+
n_group: Optional[int] = None,
|
| 1030 |
+
topk_group: Optional[int] = None,
|
| 1031 |
+
routed_scaling_factor: float = 1.0,
|
| 1032 |
+
):
|
| 1033 |
+
super().__init__()
|
| 1034 |
+
head_dim = hidden_size // num_attention_heads
|
| 1035 |
+
assert head_dim == sum(axes_dims), f"head_dim {head_dim} != sum(axes_dims) {sum(axes_dims)}"
|
| 1036 |
+
mlp_only_layers = tuple(mlp_only_layers)
|
| 1037 |
+
|
| 1038 |
+
self.patch_embedder = nn.Linear(
|
| 1039 |
+
in_channels * math.prod(patch_size), hidden_size, bias=patch_embed_bias
|
| 1040 |
+
)
|
| 1041 |
+
self.time_proj = Timesteps(freq_dim, flip_sin_to_cos=True, downscale_freq_shift=0)
|
| 1042 |
+
self.time_embedder = TimestepEmbedding(
|
| 1043 |
+
freq_dim, hidden_size, act_fn="silu", sample_proj_bias=timestep_mlp_bias
|
| 1044 |
+
)
|
| 1045 |
+
self.time_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 6 * hidden_size))
|
| 1046 |
+
self.text_embedder = LingBotVideoTextEmbedder(text_dim, hidden_size)
|
| 1047 |
+
self.rope = LingBotVideoRotaryEmbedding(axes_dims, axes_lens, rope_theta)
|
| 1048 |
+
self.blocks = nn.ModuleList(
|
| 1049 |
+
[
|
| 1050 |
+
LingBotVideoBlock(
|
| 1051 |
+
hidden_size=hidden_size,
|
| 1052 |
+
num_attention_heads=num_attention_heads,
|
| 1053 |
+
intermediate_size=intermediate_size,
|
| 1054 |
+
norm_eps=norm_eps,
|
| 1055 |
+
qkv_bias=qkv_bias,
|
| 1056 |
+
out_bias=out_bias,
|
| 1057 |
+
num_experts=num_experts,
|
| 1058 |
+
num_experts_per_tok=num_experts_per_tok,
|
| 1059 |
+
moe_intermediate_size=moe_intermediate_size,
|
| 1060 |
+
decoder_sparse_step=decoder_sparse_step,
|
| 1061 |
+
mlp_only_layers=mlp_only_layers,
|
| 1062 |
+
n_shared_experts=n_shared_experts,
|
| 1063 |
+
score_func=score_func,
|
| 1064 |
+
norm_topk_prob=norm_topk_prob,
|
| 1065 |
+
n_group=n_group,
|
| 1066 |
+
topk_group=topk_group,
|
| 1067 |
+
routed_scaling_factor=routed_scaling_factor,
|
| 1068 |
+
layer_idx=i,
|
| 1069 |
+
)
|
| 1070 |
+
for i in range(depth)
|
| 1071 |
+
]
|
| 1072 |
+
)
|
| 1073 |
+
self.norm_out = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=norm_eps)
|
| 1074 |
+
self.norm_out_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 2 * hidden_size))
|
| 1075 |
+
self.proj_out = nn.Linear(hidden_size, math.prod(patch_size) * out_channels)
|
| 1076 |
+
self.cp_joint = nn.Identity()
|
| 1077 |
+
self.cp_rotary = nn.Identity()
|
| 1078 |
+
self.cp_temb_input = nn.Identity()
|
| 1079 |
+
self.cp_temb6 = nn.Identity()
|
| 1080 |
+
self.cp_out = nn.Identity()
|
| 1081 |
+
self._cp_plan = {
|
| 1082 |
+
"cp_joint": {
|
| 1083 |
+
"input": ContextParallelInput(split_dim=1, expected_dims=3),
|
| 1084 |
+
},
|
| 1085 |
+
"cp_rotary": {
|
| 1086 |
+
"input": ContextParallelInput(split_dim=1, expected_dims=3),
|
| 1087 |
+
},
|
| 1088 |
+
"cp_temb_input": {
|
| 1089 |
+
"input": ContextParallelInput(split_dim=1, expected_dims=3),
|
| 1090 |
+
},
|
| 1091 |
+
"cp_temb6": {
|
| 1092 |
+
"input": ContextParallelInput(split_dim=1, expected_dims=3),
|
| 1093 |
+
},
|
| 1094 |
+
"cp_out": ContextParallelOutput(gather_dim=1, expected_dims=3),
|
| 1095 |
+
}
|
| 1096 |
+
|
| 1097 |
+
def forward(
|
| 1098 |
+
self,
|
| 1099 |
+
hidden_states: torch.Tensor, # (B, C, T, H, W)
|
| 1100 |
+
timestep: torch.Tensor, # (B,) ∈ [0, 1000](= sigma*1000)
|
| 1101 |
+
encoder_hidden_states: torch.Tensor, # (B, L, text_dim)
|
| 1102 |
+
encoder_attention_mask: Optional[torch.Tensor] = None, # (B, L) 1=valid
|
| 1103 |
+
return_dict: bool = True,
|
| 1104 |
+
):
|
| 1105 |
+
B, C, T, H, W = hidden_states.shape
|
| 1106 |
+
pF, pH, pW = self.config.patch_size
|
| 1107 |
+
gt, gh, gw = T // pF, H // pH, W // pW
|
| 1108 |
+
n_video = gt * gh * gw
|
| 1109 |
+
L = encoder_hidden_states.shape[1]
|
| 1110 |
+
device = hidden_states.device
|
| 1111 |
+
if encoder_attention_mask is not None:
|
| 1112 |
+
text_lens = encoder_attention_mask.sum(dim=-1).long()
|
| 1113 |
+
else:
|
| 1114 |
+
text_lens = torch.full((B,), L, dtype=torch.long, device=device)
|
| 1115 |
+
text_lens_list = [int(v) for v in text_lens.detach().cpu().tolist()]
|
| 1116 |
+
packed_batch = B > 1
|
| 1117 |
+
|
| 1118 |
+
# patchify: token order (f h w), feature order (pf ph pw c) -- matches patchify_and_embed
|
| 1119 |
+
patch_tokens = hidden_states.reshape(B, C, gt, pF, gh, pH, gw, pW)
|
| 1120 |
+
patch_tokens = patch_tokens.permute(0, 2, 4, 6, 3, 5, 7, 1).reshape(
|
| 1121 |
+
B,
|
| 1122 |
+
n_video,
|
| 1123 |
+
pF * pH * pW * C,
|
| 1124 |
+
)
|
| 1125 |
+
if packed_batch:
|
| 1126 |
+
packed_patch_tokens = patch_tokens.reshape(1, B * n_video, -1)
|
| 1127 |
+
x = torch.cat(
|
| 1128 |
+
[self.patch_embedder(patch_tokens[i : i + 1]) for i in range(B)],
|
| 1129 |
+
dim=1,
|
| 1130 |
+
)
|
| 1131 |
+
else:
|
| 1132 |
+
x = self.patch_embedder(patch_tokens)
|
| 1133 |
+
|
| 1134 |
+
if packed_batch:
|
| 1135 |
+
text_parts = [
|
| 1136 |
+
self.text_embedder(encoder_hidden_states[i : i + 1, : text_lens_list[i], :])
|
| 1137 |
+
for i in range(B)
|
| 1138 |
+
]
|
| 1139 |
+
text = torch.cat(text_parts, dim=1)
|
| 1140 |
+
joint = _cat_interleave(
|
| 1141 |
+
x,
|
| 1142 |
+
[n_video] * B,
|
| 1143 |
+
text,
|
| 1144 |
+
text_lens_list,
|
| 1145 |
+
)
|
| 1146 |
+
else:
|
| 1147 |
+
text = self.text_embedder(encoder_hidden_states)
|
| 1148 |
+
joint = torch.cat([x, text], dim=1) # [video; text]
|
| 1149 |
+
joint_seq_len = joint.shape[1]
|
| 1150 |
+
|
| 1151 |
+
# Per-sample RoPE: video t-axis start = real text length of this sample + 1
|
| 1152 |
+
rotary_parts = [
|
| 1153 |
+
self.rope(make_joint_position_ids(text_lens_list[i], gt, gh, gw, device))
|
| 1154 |
+
for i in range(B)
|
| 1155 |
+
]
|
| 1156 |
+
if packed_batch:
|
| 1157 |
+
rotary = torch.cat(rotary_parts, dim=0).unsqueeze(0)
|
| 1158 |
+
else:
|
| 1159 |
+
rotary = torch.stack(rotary_parts, dim=0) # (B, S, head_dim/2) complex64
|
| 1160 |
+
|
| 1161 |
+
parallel_config = getattr(self, "_parallel_config", None)
|
| 1162 |
+
use_packed_attention = parallel_config is not None
|
| 1163 |
+
|
| 1164 |
+
attention_mask = None
|
| 1165 |
+
moe_padding_mask = None
|
| 1166 |
+
packed_indices = None
|
| 1167 |
+
has_padding = encoder_attention_mask is not None and bool((text_lens < L).any())
|
| 1168 |
+
if packed_batch or use_packed_attention:
|
| 1169 |
+
sample_seq_lens = [n_video + text_len for text_len in text_lens_list]
|
| 1170 |
+
cu_seqlens = torch.zeros(B + 1, device=device, dtype=torch.int32)
|
| 1171 |
+
cu_seqlens[1:] = torch.cumsum(
|
| 1172 |
+
torch.tensor(sample_seq_lens, device=device, dtype=torch.int32),
|
| 1173 |
+
dim=0,
|
| 1174 |
+
)
|
| 1175 |
+
packed_indices = {
|
| 1176 |
+
"cu_seqlens_kv": cu_seqlens,
|
| 1177 |
+
"max_seqlen_in_batch_kv": max(sample_seq_lens),
|
| 1178 |
+
}
|
| 1179 |
+
has_padding = False
|
| 1180 |
+
if has_padding:
|
| 1181 |
+
key_mask = torch.cat(
|
| 1182 |
+
[torch.ones(B, n_video, dtype=torch.bool, device=device),
|
| 1183 |
+
encoder_attention_mask.bool()],
|
| 1184 |
+
dim=1,
|
| 1185 |
+
)
|
| 1186 |
+
attention_mask = key_mask[:, None, None, :] # (B,1,1,S) → SDPA broadcast
|
| 1187 |
+
moe_padding_mask = key_mask.reshape(-1).float() # (B*S,)
|
| 1188 |
+
packed_cp = packed_indices is not None and parallel_config is not None
|
| 1189 |
+
padding_size = 0
|
| 1190 |
+
if packed_cp:
|
| 1191 |
+
cp_config = parallel_config.context_parallel_config
|
| 1192 |
+
cp_world_size = int(getattr(cp_config, "ulysses_degree", getattr(cp_config, "_world_size", 1)))
|
| 1193 |
+
padding_size = (cp_world_size - (joint_seq_len % cp_world_size)) % cp_world_size
|
| 1194 |
+
if padding_size:
|
| 1195 |
+
joint = torch.cat(
|
| 1196 |
+
[
|
| 1197 |
+
joint,
|
| 1198 |
+
torch.zeros(
|
| 1199 |
+
joint.shape[0],
|
| 1200 |
+
padding_size,
|
| 1201 |
+
joint.shape[2],
|
| 1202 |
+
device=joint.device,
|
| 1203 |
+
dtype=joint.dtype,
|
| 1204 |
+
),
|
| 1205 |
+
],
|
| 1206 |
+
dim=1,
|
| 1207 |
+
)
|
| 1208 |
+
rotary = torch.cat(
|
| 1209 |
+
[
|
| 1210 |
+
rotary,
|
| 1211 |
+
torch.zeros(
|
| 1212 |
+
rotary.shape[0],
|
| 1213 |
+
padding_size,
|
| 1214 |
+
rotary.shape[2],
|
| 1215 |
+
device=rotary.device,
|
| 1216 |
+
dtype=rotary.dtype,
|
| 1217 |
+
),
|
| 1218 |
+
],
|
| 1219 |
+
dim=1,
|
| 1220 |
+
)
|
| 1221 |
+
if packed_indices is None:
|
| 1222 |
+
raise RuntimeError("packed_indices must be initialized for packed context parallel.")
|
| 1223 |
+
packed_indices["cu_seqlens_kv"] = torch.cat(
|
| 1224 |
+
[
|
| 1225 |
+
packed_indices["cu_seqlens_kv"],
|
| 1226 |
+
packed_indices["cu_seqlens_kv"][-1:] + padding_size,
|
| 1227 |
+
],
|
| 1228 |
+
dim=0,
|
| 1229 |
+
)
|
| 1230 |
+
packed_indices["max_seqlen_in_batch_kv"] = max(
|
| 1231 |
+
int(packed_indices["max_seqlen_in_batch_kv"]),
|
| 1232 |
+
int(padding_size),
|
| 1233 |
+
)
|
| 1234 |
+
joint_seq_len = joint.shape[1]
|
| 1235 |
+
|
| 1236 |
+
timestep_for_embed = timestep.float()
|
| 1237 |
+
timestep_proj = self.time_proj(timestep_for_embed)
|
| 1238 |
+
t_emb = self.time_embedder(timestep_proj) # (B, D)
|
| 1239 |
+
if packed_batch:
|
| 1240 |
+
temb_input = torch.cat(
|
| 1241 |
+
[
|
| 1242 |
+
t_emb[i : i + 1].unsqueeze(1).expand(1, n_video + text_lens_list[i], -1)
|
| 1243 |
+
for i in range(B)
|
| 1244 |
+
],
|
| 1245 |
+
dim=1,
|
| 1246 |
+
)
|
| 1247 |
+
if padding_size:
|
| 1248 |
+
temb_input = torch.cat(
|
| 1249 |
+
[
|
| 1250 |
+
temb_input,
|
| 1251 |
+
torch.zeros(
|
| 1252 |
+
temb_input.shape[0],
|
| 1253 |
+
padding_size,
|
| 1254 |
+
temb_input.shape[2],
|
| 1255 |
+
device=temb_input.device,
|
| 1256 |
+
dtype=temb_input.dtype,
|
| 1257 |
+
),
|
| 1258 |
+
],
|
| 1259 |
+
dim=1,
|
| 1260 |
+
)
|
| 1261 |
+
temb6 = self.time_modulation(temb_input.reshape(joint_seq_len, -1))
|
| 1262 |
+
temb6 = temb6.reshape(1, joint_seq_len, -1)
|
| 1263 |
+
else:
|
| 1264 |
+
temb_input = t_emb.unsqueeze(1).expand(B, joint_seq_len, -1) # (B, S, D)
|
| 1265 |
+
temb6 = self.time_modulation(temb_input.reshape(B * joint_seq_len, -1))
|
| 1266 |
+
temb6 = temb6.reshape(B, joint_seq_len, -1) # (B, S, 6D)
|
| 1267 |
+
|
| 1268 |
+
joint = self.cp_joint(joint)
|
| 1269 |
+
rotary = self.cp_rotary(rotary)
|
| 1270 |
+
if packed_cp:
|
| 1271 |
+
temb_input = self.cp_temb_input(temb_input)
|
| 1272 |
+
temb6 = self.cp_temb6(temb6)
|
| 1273 |
+
temb6 = temb6.reshape(temb6.shape[0] * temb6.shape[1], -1)
|
| 1274 |
+
|
| 1275 |
+
for block in self.blocks:
|
| 1276 |
+
joint = block(
|
| 1277 |
+
joint,
|
| 1278 |
+
temb6,
|
| 1279 |
+
rotary,
|
| 1280 |
+
attention_mask,
|
| 1281 |
+
moe_padding_mask,
|
| 1282 |
+
packed_indices=packed_indices,
|
| 1283 |
+
parallel_config=parallel_config,
|
| 1284 |
+
)
|
| 1285 |
+
if not packed_cp:
|
| 1286 |
+
joint = self.cp_out(joint)
|
| 1287 |
+
|
| 1288 |
+
final_mod = self.norm_out_modulation(temb_input.reshape(joint.shape[0] * joint.shape[1], -1))
|
| 1289 |
+
shift, scale = final_mod.reshape(joint.shape[0], joint.shape[1], -1).chunk(2, dim=-1)
|
| 1290 |
+
final_hidden = self.norm_out(joint) * (1.0 + scale) + shift
|
| 1291 |
+
projected = self.proj_out(final_hidden.to(self.proj_out.weight.dtype))
|
| 1292 |
+
if packed_cp:
|
| 1293 |
+
projected = self.cp_out(projected)
|
| 1294 |
+
if padding_size:
|
| 1295 |
+
projected = projected[:, :-padding_size, :]
|
| 1296 |
+
if packed_batch:
|
| 1297 |
+
split_lengths: list[int] = []
|
| 1298 |
+
for text_len in text_lens_list:
|
| 1299 |
+
split_lengths.extend([n_video, text_len])
|
| 1300 |
+
parts = torch.split(projected, split_lengths, dim=1)
|
| 1301 |
+
x = torch.cat(parts[::2], dim=1).reshape(B, n_video, -1)
|
| 1302 |
+
else:
|
| 1303 |
+
x = projected[:, :n_video]
|
| 1304 |
+
|
| 1305 |
+
# unpatchify (matches the rearrange in postprocess)
|
| 1306 |
+
Cout = self.config.out_channels
|
| 1307 |
+
x = x.reshape(B, gt, gh, gw, pF, pH, pW, Cout)
|
| 1308 |
+
x = x.permute(0, 7, 1, 4, 2, 5, 3, 6).reshape(B, Cout, T, H, W)
|
| 1309 |
+
|
| 1310 |
+
if not return_dict:
|
| 1311 |
+
return (x,)
|
| 1312 |
+
return Transformer2DModelOutput(sample=x)
|
lingbot_video/utils.py
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Any, Sequence
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn.functional as F
|
| 10 |
+
from PIL import Image
|
| 11 |
+
|
| 12 |
+
LOW_NOISE_TAIL_V1_NAME = "low_noise_tail_v1"
|
| 13 |
+
LOW_NOISE_TAIL_V1_DEFAULT_STEPS = 2
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def num_frames_from_duration(duration: float, fps: int) -> int:
|
| 17 |
+
frame_count = int(float(duration) * int(fps))
|
| 18 |
+
return ((frame_count - 1) // 4 + 1) * 4 + 1
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def validate_refiner_sigmas(
|
| 22 |
+
sigmas: Sequence[float] | np.ndarray,
|
| 23 |
+
t_thresh: float | None = None,
|
| 24 |
+
) -> np.ndarray:
|
| 25 |
+
arr = np.asarray(list(sigmas), dtype=np.float64)
|
| 26 |
+
if arr.ndim != 1 or arr.size == 0:
|
| 27 |
+
raise ValueError("refiner sigma schedule must be a non-empty 1D list")
|
| 28 |
+
if not np.all(np.isfinite(arr)):
|
| 29 |
+
raise ValueError("refiner sigma schedule contains non-finite values")
|
| 30 |
+
if np.any(arr < 0.0) or np.any(arr > 1.0):
|
| 31 |
+
raise ValueError(f"refiner sigma schedule values must be in [0, 1], got {arr.tolist()}")
|
| 32 |
+
if arr.size > 1 and not np.all(np.diff(arr) < 0.0):
|
| 33 |
+
raise ValueError(f"refiner sigma schedule must be strictly descending, got {arr.tolist()}")
|
| 34 |
+
if t_thresh is not None and abs(float(arr[0]) - float(t_thresh)) > 1e-6:
|
| 35 |
+
raise ValueError(f"refiner sigma schedule must start at t_thresh={float(t_thresh)}, got {float(arr[0])}")
|
| 36 |
+
return arr
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def compute_refiner_sigmas(
|
| 40 |
+
*,
|
| 41 |
+
sigma_max: float,
|
| 42 |
+
sigma_min: float,
|
| 43 |
+
num_inference_steps: int,
|
| 44 |
+
shift: float,
|
| 45 |
+
t_thresh: float | None,
|
| 46 |
+
tail_steps: int = 0,
|
| 47 |
+
) -> np.ndarray | None:
|
| 48 |
+
if t_thresh is None:
|
| 49 |
+
return None
|
| 50 |
+
t_value = float(t_thresh)
|
| 51 |
+
if not (0.0 < t_value <= 1.0):
|
| 52 |
+
raise ValueError(f"refiner t_thresh must lie in (0, 1], got {t_value}")
|
| 53 |
+
steps = int(num_inference_steps)
|
| 54 |
+
if steps < 1:
|
| 55 |
+
raise ValueError(f"num_inference_steps must be >= 1, got {steps}")
|
| 56 |
+
tail = int(tail_steps or 0)
|
| 57 |
+
if tail < 0:
|
| 58 |
+
raise ValueError(f"refiner_sigma_tail_steps must be >= 0, got {tail}")
|
| 59 |
+
|
| 60 |
+
base = np.linspace(float(sigma_max), float(sigma_min), steps + 1).copy()[:-1]
|
| 61 |
+
shift_value = float(shift)
|
| 62 |
+
shifted = shift_value * base / (1.0 + (shift_value - 1.0) * base)
|
| 63 |
+
eps = 1e-6
|
| 64 |
+
sigmas = shifted[shifted <= t_value + eps]
|
| 65 |
+
if sigmas.size == 0 or abs(float(sigmas[0]) - t_value) > eps:
|
| 66 |
+
sigmas = np.concatenate([[t_value], sigmas])
|
| 67 |
+
if tail > 0:
|
| 68 |
+
start = float(sigmas[-1])
|
| 69 |
+
stop = min(float(sigma_min), start)
|
| 70 |
+
extra = np.linspace(start, stop, tail + 2, dtype=np.float64)[1:-1]
|
| 71 |
+
sigmas = np.concatenate([sigmas, extra])
|
| 72 |
+
return validate_refiner_sigmas(sigmas, t_value).astype(np.float32)
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def prepare_refiner_latent(
|
| 76 |
+
x_up: torch.Tensor,
|
| 77 |
+
noise: torch.Tensor,
|
| 78 |
+
t_thresh: float | torch.Tensor,
|
| 79 |
+
) -> torch.Tensor:
|
| 80 |
+
if not torch.is_tensor(t_thresh):
|
| 81 |
+
t_thresh = torch.tensor(float(t_thresh), device=x_up.device, dtype=x_up.dtype)
|
| 82 |
+
while t_thresh.ndim < x_up.ndim:
|
| 83 |
+
t_thresh = t_thresh.view(*t_thresh.shape, *([1] * (x_up.ndim - t_thresh.ndim)))
|
| 84 |
+
return (1.0 - t_thresh) * x_up + t_thresh * noise
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _pad_prompt_embeds(
|
| 88 |
+
embeds: torch.Tensor,
|
| 89 |
+
mask: torch.Tensor,
|
| 90 |
+
target_length: int,
|
| 91 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 92 |
+
if embeds.ndim != 3:
|
| 93 |
+
raise ValueError(f"prompt embeds must be rank-3, got {tuple(embeds.shape)}")
|
| 94 |
+
if mask.ndim != 2:
|
| 95 |
+
raise ValueError(f"prompt mask must be rank-2, got {tuple(mask.shape)}")
|
| 96 |
+
if embeds.shape[:2] != mask.shape:
|
| 97 |
+
raise ValueError(
|
| 98 |
+
f"prompt embeds/mask shape mismatch: {tuple(embeds.shape)} vs {tuple(mask.shape)}"
|
| 99 |
+
)
|
| 100 |
+
if embeds.shape[0] != 1:
|
| 101 |
+
raise ValueError(f"batched CFG helper expects batch=1 inputs, got {embeds.shape[0]}")
|
| 102 |
+
if embeds.shape[1] > target_length:
|
| 103 |
+
raise ValueError(f"cannot pad length {embeds.shape[1]} down to {target_length}")
|
| 104 |
+
pad_len = target_length - embeds.shape[1]
|
| 105 |
+
if pad_len == 0:
|
| 106 |
+
return embeds, mask
|
| 107 |
+
embed_pad = torch.zeros(
|
| 108 |
+
embeds.shape[0],
|
| 109 |
+
pad_len,
|
| 110 |
+
embeds.shape[2],
|
| 111 |
+
dtype=embeds.dtype,
|
| 112 |
+
device=embeds.device,
|
| 113 |
+
)
|
| 114 |
+
mask_pad = torch.zeros(mask.shape[0], pad_len, dtype=mask.dtype, device=mask.device)
|
| 115 |
+
return torch.cat([embeds, embed_pad], dim=1), torch.cat([mask, mask_pad], dim=1)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def batch_cfg_prompt_inputs(
|
| 119 |
+
prompt_embeds: torch.Tensor,
|
| 120 |
+
prompt_mask: torch.Tensor,
|
| 121 |
+
negative_embeds: torch.Tensor,
|
| 122 |
+
negative_mask: torch.Tensor,
|
| 123 |
+
*,
|
| 124 |
+
null_cond_clone_zero: bool = False,
|
| 125 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 126 |
+
if null_cond_clone_zero:
|
| 127 |
+
zero_negative = torch.zeros_like(prompt_embeds)
|
| 128 |
+
return (
|
| 129 |
+
torch.cat([prompt_embeds, zero_negative], dim=0),
|
| 130 |
+
torch.cat([prompt_mask, prompt_mask.clone()], dim=0),
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
target_length = max(int(prompt_embeds.shape[1]), int(negative_embeds.shape[1]))
|
| 134 |
+
prompt_padded, prompt_mask_padded = _pad_prompt_embeds(
|
| 135 |
+
prompt_embeds, prompt_mask, target_length
|
| 136 |
+
)
|
| 137 |
+
negative_padded, negative_mask_padded = _pad_prompt_embeds(
|
| 138 |
+
negative_embeds, negative_mask, target_length
|
| 139 |
+
)
|
| 140 |
+
return (
|
| 141 |
+
torch.cat([prompt_padded, negative_padded], dim=0),
|
| 142 |
+
torch.cat([prompt_mask_padded, negative_mask_padded], dim=0),
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def caption_from_sample(sample: dict[str, Any]) -> str:
|
| 147 |
+
if "caption" in sample:
|
| 148 |
+
caption = sample["caption"]
|
| 149 |
+
else:
|
| 150 |
+
runtime_keys = {
|
| 151 |
+
"duration",
|
| 152 |
+
"fps",
|
| 153 |
+
"height",
|
| 154 |
+
"width",
|
| 155 |
+
"num_frames",
|
| 156 |
+
"resolution",
|
| 157 |
+
"ratio",
|
| 158 |
+
}
|
| 159 |
+
caption = {key: value for key, value in sample.items() if key not in runtime_keys}
|
| 160 |
+
if isinstance(caption, (dict, list)):
|
| 161 |
+
return json.dumps(caption, ensure_ascii=False, separators=(",", ":"))
|
| 162 |
+
return str(caption)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def compute_training_frame_budget(
|
| 166 |
+
num_source_frames: int,
|
| 167 |
+
source_fps: float,
|
| 168 |
+
sample_fps: int = 24,
|
| 169 |
+
vae_tc: int = 4,
|
| 170 |
+
) -> tuple[int, float, int]:
|
| 171 |
+
if num_source_frames <= 0:
|
| 172 |
+
return 1, 0.0, 1
|
| 173 |
+
if source_fps > sample_fps:
|
| 174 |
+
raw_val = int(num_source_frames / source_fps * sample_fps)
|
| 175 |
+
else:
|
| 176 |
+
raw_val = int(num_source_frames)
|
| 177 |
+
sample_frame = ((raw_val - 1) // vae_tc) * vae_tc + 1
|
| 178 |
+
sample_frame = max(sample_frame, 1)
|
| 179 |
+
vae_fps = sample_frame / num_source_frames * float(source_fps)
|
| 180 |
+
t_vae = (sample_frame - 1) // vae_tc + 1
|
| 181 |
+
return int(sample_frame), float(vae_fps), int(t_vae)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def compute_training_aligned_indices(
|
| 185 |
+
num_source_frames: int,
|
| 186 |
+
sample_frame: int,
|
| 187 |
+
) -> np.ndarray:
|
| 188 |
+
if sample_frame <= 0:
|
| 189 |
+
return np.zeros(0, dtype=int)
|
| 190 |
+
if num_source_frames <= 0:
|
| 191 |
+
return np.zeros(sample_frame, dtype=int)
|
| 192 |
+
if num_source_frames >= sample_frame:
|
| 193 |
+
return np.linspace(0, num_source_frames - 1, sample_frame, dtype=int)
|
| 194 |
+
head = np.arange(num_source_frames, dtype=int)
|
| 195 |
+
pad = np.full(sample_frame - num_source_frames, num_source_frames - 1, dtype=int)
|
| 196 |
+
return np.concatenate([head, pad])
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
def resize_video_tensor(video: torch.Tensor, height: int, width: int) -> torch.Tensor:
|
| 200 |
+
if video.ndim != 5:
|
| 201 |
+
raise ValueError(f"video tensor must have shape [B,C,T,H,W], got {tuple(video.shape)}")
|
| 202 |
+
bsz, channels, frames, _height, _width = video.shape
|
| 203 |
+
flat = video.permute(0, 2, 1, 3, 4).reshape(bsz * frames, channels, _height, _width)
|
| 204 |
+
resized = F.interpolate(flat, size=(height, width), mode="bicubic", align_corners=False)
|
| 205 |
+
resized = resized.clamp(0.0, 1.0)
|
| 206 |
+
return resized.reshape(bsz, frames, channels, height, width).permute(0, 2, 1, 3, 4).contiguous()
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def load_refiner_video_tensor(
|
| 210 |
+
path: str | Path,
|
| 211 |
+
height: int,
|
| 212 |
+
width: int,
|
| 213 |
+
*,
|
| 214 |
+
sample_fps: int = 24,
|
| 215 |
+
vae_tc: int = 4,
|
| 216 |
+
max_frames: int | None = None,
|
| 217 |
+
) -> tuple[torch.Tensor, dict[str, Any]]:
|
| 218 |
+
from decord import VideoReader, cpu
|
| 219 |
+
|
| 220 |
+
vr = VideoReader(str(path), ctx=cpu(0))
|
| 221 |
+
total = len(vr)
|
| 222 |
+
if total <= 0:
|
| 223 |
+
raise ValueError(f"Video has no frames: {path}")
|
| 224 |
+
src_fps = float(vr.get_avg_fps())
|
| 225 |
+
sample_frame, vae_fps, t_vae = compute_training_frame_budget(
|
| 226 |
+
total,
|
| 227 |
+
src_fps,
|
| 228 |
+
sample_fps=sample_fps,
|
| 229 |
+
vae_tc=vae_tc,
|
| 230 |
+
)
|
| 231 |
+
sample_frame_uncapped = int(sample_frame)
|
| 232 |
+
truncated = False
|
| 233 |
+
if max_frames is not None and sample_frame > int(max_frames):
|
| 234 |
+
sample_frame = int(max_frames)
|
| 235 |
+
truncated = True
|
| 236 |
+
vae_fps = float(sample_frame) / max(total, 1) * src_fps
|
| 237 |
+
t_vae = (sample_frame - 1) // vae_tc + 1
|
| 238 |
+
indices = compute_training_aligned_indices(total, sample_frame)
|
| 239 |
+
frames = torch.from_numpy(vr.get_batch(indices).asnumpy()).permute(0, 3, 1, 2).float()
|
| 240 |
+
frames = frames / 255.0
|
| 241 |
+
video = frames.permute(1, 0, 2, 3).unsqueeze(0).contiguous()
|
| 242 |
+
video = resize_video_tensor(video, height=height, width=width)
|
| 243 |
+
meta = {
|
| 244 |
+
"src_fps": float(src_fps),
|
| 245 |
+
"sample_frame": int(sample_frame),
|
| 246 |
+
"sample_frame_uncapped": int(sample_frame_uncapped),
|
| 247 |
+
"max_frames": None if max_frames is None else int(max_frames),
|
| 248 |
+
"truncated_by_max_frames": bool(truncated),
|
| 249 |
+
"vae_fps": float(vae_fps),
|
| 250 |
+
"t_vae": int(t_vae),
|
| 251 |
+
"num_source_frames": int(total),
|
| 252 |
+
"align_to_training": True,
|
| 253 |
+
}
|
| 254 |
+
return video, meta
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def load_first_frame_condition_tensor(
|
| 258 |
+
path: str | Path,
|
| 259 |
+
target_height: int,
|
| 260 |
+
target_width: int,
|
| 261 |
+
geometry_height: int,
|
| 262 |
+
geometry_width: int,
|
| 263 |
+
) -> torch.Tensor:
|
| 264 |
+
"""Load the clean first frame center-cropped to the lowres video's aspect.
|
| 265 |
+
|
| 266 |
+
The refiner latent grid is encoded from the lowres video, so the injected
|
| 267 |
+
frame-0 condition must share that video's geometry, not the raw image's.
|
| 268 |
+
"""
|
| 269 |
+
image = Image.open(path).convert("RGB")
|
| 270 |
+
image_width, image_height = image.size
|
| 271 |
+
geometry_aspect = float(geometry_width) / float(geometry_height)
|
| 272 |
+
image_aspect = float(image_width) / float(image_height)
|
| 273 |
+
if image_aspect > geometry_aspect:
|
| 274 |
+
crop_height = image_height
|
| 275 |
+
crop_width = max(1, int(round(crop_height * geometry_aspect)))
|
| 276 |
+
left = int(round((image_width - crop_width) / 2.0))
|
| 277 |
+
top = 0
|
| 278 |
+
else:
|
| 279 |
+
crop_width = image_width
|
| 280 |
+
crop_height = max(1, int(round(crop_width / geometry_aspect)))
|
| 281 |
+
left = 0
|
| 282 |
+
top = int(round((image_height - crop_height) / 2.0))
|
| 283 |
+
crop = image.crop((left, top, left + crop_width, top + crop_height))
|
| 284 |
+
crop = crop.resize((target_width, target_height), resample=Image.BICUBIC)
|
| 285 |
+
arr = np.asarray(crop, dtype=np.float32) / 255.0
|
| 286 |
+
frame = torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0).contiguous()
|
| 287 |
+
return frame.permute(1, 0, 2, 3).unsqueeze(0)
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
diffusers==0.39.0
|
| 2 |
+
transformers==5.8.1
|
| 3 |
+
accelerate>=1.10
|
| 4 |
+
safetensors>=0.4.5
|
| 5 |
+
torchvision
|
| 6 |
+
imageio>=2.35.0
|
| 7 |
+
imageio-ffmpeg>=0.5.1
|
| 8 |
+
numpy>=1.26
|
| 9 |
+
pillow>=10.4.0
|
| 10 |
+
scipy>=1.11
|
| 11 |
+
json_repair>=0.30
|
rewriter_prompts.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
VIDEO_STEP1_EXPAND = (
|
| 2 |
+
'You write ONE short, natural, standalone video caption in English from a brief user video\n'
|
| 3 |
+
'prompt — as if briefly recounting what happens in a real clip of that idea.\n'
|
| 4 |
+
'\n'
|
| 5 |
+
"INPUT: a short user prompt (and, when provided, the video's first frame as a visual anchor).\n"
|
| 6 |
+
'\n'
|
| 7 |
+
'HARD LENGTH LIMIT: the output MUST be UNDER 1000 characters. Aim for roughly 400-800\n'
|
| 8 |
+
'characters. Concise is a top priority.\n'
|
| 9 |
+
'\n'
|
| 10 |
+
'STYLE — a flowing story, NOT a checklist:\n'
|
| 11 |
+
'- Connected, natural English prose. NO headings, NO bullets, NO field labels.\n'
|
| 12 |
+
'- The MAIN THREAD is what HAPPENS — lead with the action. Do NOT march subject-by-subject\n'
|
| 13 |
+
' listing attributes; weave the few details you keep into the action naturally.\n'
|
| 14 |
+
'- One cohesive paragraph; never pad to add length.\n'
|
| 15 |
+
'\n'
|
| 16 |
+
'WHAT TO COVER (only these):\n'
|
| 17 |
+
'- The scene in a brief phrase.\n'
|
| 18 |
+
'- Each main subject by name/what-it-is plus ONE most defining visual trait — not a full\n'
|
| 19 |
+
' appearance list.\n'
|
| 20 |
+
'- What happens over the clip, in correct chronological order as the backbone, with an EXPLICIT\n'
|
| 21 |
+
' timestamp in seconds on each action, distributed within the given video duration — e.g.\n'
|
| 22 |
+
' "at 0.0s", "from 1.2s to 2.7s", "around the 3.4s mark", "finally from 4.0s to 5.0s". Span the\n'
|
| 23 |
+
' actions from 0s up to (but never beyond) the stated duration; the last one ends at or before it.\n'
|
| 24 |
+
' Attach a timestamp ONLY to a real action or change; mention a static/unchanging element once\n'
|
| 25 |
+
' with NO timestamp — never put a whole-clip span on something that merely exists.\n'
|
| 26 |
+
'- A one-word shot type only if it matters; otherwise skip the camera.\n'
|
| 27 |
+
"- Named entities only if clearly implied — don't invent identities.\n"
|
| 28 |
+
'\n'
|
| 29 |
+
'FAITHFUL EXPANSION:\n'
|
| 30 |
+
'- Stay consistent with the prompt; elaborate plausibly but never contradict it. If a first\n'
|
| 31 |
+
' frame is given, ground the scene and subject in it.\n'
|
| 32 |
+
'- Keep actions in their natural, real-world direction — never reverse them. Commit to one\n'
|
| 33 |
+
" coherent interpretation; don't hedge with alternatives.\n"
|
| 34 |
+
'- Keep subject identities and counts consistent.\n'
|
| 35 |
+
'\n'
|
| 36 |
+
'DOMAIN NOTES (apply whichever fits):\n'
|
| 37 |
+
'- Multiple events / sequence: lay out consecutive actions in STRICT chronological order as\n'
|
| 38 |
+
' distinct, separated steps with approximate timing — never blur them into one vague action.\n'
|
| 39 |
+
'- Robot manipulation (VLA): subjects are robotic arm(s), gripper(s) and workspace objects,\n'
|
| 40 |
+
' NOT people — no clothing/skin/gender/expression. When two arms are present, ALWAYS keep the\n'
|
| 41 |
+
' LEFT and RIGHT arm distinct and state which arm/gripper does each motion; never swap, merge,\n'
|
| 42 |
+
' or leave it ambiguous. Manipulation is STRICTLY NOT reversible.\n'
|
| 43 |
+
"- First-person (EGO): a head-mounted first-person view; camera motion IS the wearer's head\n"
|
| 44 |
+
' movement. The agent is the camera-wearer, a PERSON shown through their own hands/arms and\n'
|
| 45 |
+
' viewpoint; never a robot or external third-person subject. Stay strictly first-person.\n'
|
| 46 |
+
'\n'
|
| 47 |
+
'Output ONLY the caption text — no preamble, no headings, no explanation.\n'
|
| 48 |
+
'\n'
|
| 49 |
+
"## EXAMPLES — one representative example per domain. When you write your own caption, match these examples' style, format, length, and timestamp convention (each action gets a numeric timestamp in seconds within the video's duration).\n"
|
| 50 |
+
'\n'
|
| 51 |
+
'### Example 1 — general\n'
|
| 52 |
+
'USER PROMPT:\n'
|
| 53 |
+
'低角度广角镜头,镜头静止。中央大型条纹热气球缓慢上升并飘移(气球呈红橙黄绿蓝条纹,底部深色),背景中多个热气球在天空漂移。地面草地上散布着正在充气的热气球,一辆红色皮卡停放在旁。随后一辆蓝色拖车进入画面并横穿前景(覆盖蓝色防水布)。饱和色彩,硬光,日光,电影质感。\n'
|
| 54 |
+
'\n'
|
| 55 |
+
'DETAILED CAPTION:\n'
|
| 56 |
+
'At a vibrant hot air balloon festival on a sunny day, a wide shot shows a grassy field under a blue sky. A large, multi-colored striped balloon dominates the center, and from the start at 0.0 seconds until 5.3 seconds, it slowly rises and drifts slightly upward and to the right. Simultaneously during the 0.0s to 5.3s period, several small background balloons drift slowly across the sky in various directions above a cluster of grounded balloons. A red pickup truck and white van sit parked near the left side, while finally, around the 4.3s mark until 5.3s, a blue trailer enters the frame from the right and moves left across the foreground.\n'
|
| 57 |
+
'\n'
|
| 58 |
+
'### Example 2 — multi-event\n'
|
| 59 |
+
'USER PROMPT:\n'
|
| 60 |
+
'Subtitle: Outdoor High-Intensity Workout — All live-action, hyper-realistic, fixed shot with slight handheld unsteadiness.\n'
|
| 61 |
+
'\n'
|
| 62 |
+
'Style: Hard daylight, high-angle wide shot, center composition, sharp shadows, athletic tension.\n'
|
| 63 |
+
'\n'
|
| 64 |
+
'1. Muscular shirtless man, black headwrap, black boxing gloves, blue digital camo pants, black combat boots. Standing on concrete surface, background includes beige wall, chain-link fence, and trash bins. Stands in a boxing stance with hands up.\n'
|
| 65 |
+
'\n'
|
| 66 |
+
'2. Drops into a deep squat, then jumps upward explosively.\n'
|
| 67 |
+
'\n'
|
| 68 |
+
'3. Lands and drops into a deep squat, repeating this explosive jump and squat landing cycle.\n'
|
| 69 |
+
'\n'
|
| 70 |
+
'4. After the final landing and squat, begins to stand up.\n'
|
| 71 |
+
'\n'
|
| 72 |
+
'Overall movements coherent and natural, high contrast lighting, focused and athletic atmosphere.\n'
|
| 73 |
+
'\n'
|
| 74 |
+
'DETAILED CAPTION:\n'
|
| 75 |
+
'In an outdoor urban setting against a beige wall, a shirtless man wearing boxing gloves performs a workout on concrete. From 0.0s to 0.6s he stands in a boxing stance with hands up. Then from 0.6s to 1.2s he drops into a deep squat, followed by the period from 1.2s to 1.8s where he jumps upward explosively. From 1.8s to 2.4s he lands and drops into a deep squat, then from 2.4s to 3.0s jumps upward explosively again. Around the 3.0s to 3.6s mark he lands and drops into a deep squat, proceeding to jump upward explosively from 3.6s to 4.2s. From 4.2s to 4.8s he lands and drops into a deep squat once more, and finally from 4.8s to 5.0s he begins to stand up.\n'
|
| 76 |
+
'\n'
|
| 77 |
+
'### Example 3 — VLA\n'
|
| 78 |
+
'USER PROMPT:\n'
|
| 79 |
+
'环境:自动化超市补货工作站,俯视视角,明亮均匀人工光,清晰功能化氛围。物体:左侧打开的棕色纸箱内含整齐堆叠的红色香肠包装,右侧白色矩形料箱内含红黄绿混合食品包装及带黑色把手的透明塑料隔板。机器人:双臂系统。左臂黑色机身银色底座黑色夹爪(全程静止悬停),右臂白色机身黑色夹爪腕部蓝色指示灯(活动主体)。动作:右臂向下向左移动伸入白色料箱,抓取隔板黑色把手,向右滑动隔板,释放把手,向上向右收回复位。左臂悬停于纸箱上方保持静止。相机:固定高角度俯视视角,全程静止镜头,宽画幅,超广角镜头,柔和人工光,极致细节。\n'
|
| 80 |
+
'\n'
|
| 81 |
+
'DETAILED CAPTION:\n'
|
| 82 |
+
'A top-down wide shot shows an automated workspace with a cardboard box of sausage packages on the left and a white bin on the right. A stationary black and silver left robotic arm hovers over the box, while a white and black right robotic arm operates above the bin containing a black handle. From 0.0s to 2.0s, the right arm moves downwards and to the left, reaching into the white bin as the handle remains still. Then from 2.0s to 6.0s, the right arm grasps the black handle and moves to the right, pulling the handle and sliding the divider across the bin. Finally from 6.0s to 9.2s, the right arm releases the handle and moves upwards and to the right, returning to a resting position while the handle stays stationary. The box and bin remain fixed throughout.\n'
|
| 83 |
+
'\n'
|
| 84 |
+
'### Example 4 — EGO\n'
|
| 85 |
+
'USER PROMPT:\n'
|
| 86 |
+
'First-person POV, brightly lit grocery store produce section, soft artificial lighting. Two rectangular bins side-by-side in front; left filled with red/yellow apples, right piled with bright orange oranges. Background shows aisles and shoppers, one in red shirt with basket. Right hand enters from bottom, reaches into right orange pile, grasps one orange, lifts it upwards and slightly left. Camera moves forward and slightly down approaching bins, then remains stable with minor panning/tilting following hand movement.\n'
|
| 87 |
+
'\n'
|
| 88 |
+
'DETAILED CAPTION:\n'
|
| 89 |
+
"From a first-person perspective in a grocery store produce section, the camera approaches display bins containing apples on the left and bright oranges on the right. In the background aisles, a shopper wearing a red shirt walks from the left side of the frame towards the right between 0.0s and 2.0s, while another shopper in a striped shirt walks away from the camera down the aisle from 0.0s to 5.0s. The operator's right hand enters the frame from the bottom from 0.0s to 2.5s, then reaches into the pile of oranges and grasps one from 2.5s to 3.5s. Finally, the hand lifts the grasped orange upwards and to the left from 3.5s to 5.0s, selecting it from the cluster."
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
VIDEO_STEP2_MAP = (
|
| 93 |
+
'You are a structuring engine. You convert a DETAILED natural-language video caption (which concisely states the scene and the full motion/timing plan) into a STRUCTURED JSON caption. Your job is FAITHFUL structural extraction ONLY: do NOT drop, alter, reorder, or re-time anything the prose states (keep every subject, every action, and every timestamp exactly), and do NOT re-plan motion; BUT the prose is brief and intentionally omits fine visual attributes and camera settings — you MUST fill those omitted fields (texture, skin tone, precise colors, relative size, pose/orientation, clothing, and all camera_info) with plausible values that stay consistent with and never contradict the prose — just map what the prose already states into the schema.\n'
|
| 94 |
+
'\n'
|
| 95 |
+
'## OUTPUT FORMAT (JSON)\n'
|
| 96 |
+
'\n'
|
| 97 |
+
'```json\n'
|
| 98 |
+
'{\n'
|
| 99 |
+
' "comprehensive_description": {\n'
|
| 100 |
+
' "scene_content_description": "(String) A detailed description focusing on scene content, subject appearance, lighting, atmosphere, narrative, and interactions between elements. Text physically printed on visual objects should be mentioned alongside the object and detailed further in prominent_elements. For standalone OCR/text elements, briefly describe their role, relative scale, font style, color, and orientation; categorize them as \'static overlays\' (like watermarks) or \'integrated scene text\' (like subtitles/scrolling text); and describe their temporal behavior if they appear or disappear. Provide an exact transcription for prominent text (preserving spelling, punctuation, and capitalization), but summarize long or dense text blocks instead of transcribing them fully. **Do not describe camera movement here**. Maximum 800 words.",\n'
|
| 101 |
+
' "camera_movement_description": "(String) A detailed description focusing on camera behavior. Include camera movement types (Pan, Tilt, Zoom, Dolly, Truck, Roll), shooting angles (high/low/eye-level), shot size changes, and stability. Maximum 100 words. If the camera is essentially stationary, set to \'\'."\n'
|
| 102 |
+
' },\n'
|
| 103 |
+
'\n'
|
| 104 |
+
' "prominent_elements": [\n'
|
| 105 |
+
' {\n'
|
| 106 |
+
' "name": "(String) Short label for the object (e.g., \'red sports car\', \'elderly man\')",\n'
|
| 107 |
+
' "description": "(String) Detailed visual description of this specific element",\n'
|
| 108 |
+
' "actions": [\n'
|
| 109 |
+
' {\n'
|
| 110 |
+
' "timestamp": "(String) Time range when this action occurs, e.g., \'[0.0s - 3.0s]\'",\n'
|
| 111 |
+
' "action": "(String) Specific action description during this time period. **Direction must be described from the observer\'s perspective**. If the element has no action throughout, the entire actions array contains only one element with action set to \'\'."\n'
|
| 112 |
+
' }\n'
|
| 113 |
+
' ],\n'
|
| 114 |
+
' "location": "(String) Precise position in the frame or main area of activity (from observer\'s perspective)",\n'
|
| 115 |
+
' "relative_size": "(String) small / medium / large / dominant",\n'
|
| 116 |
+
' "shape_and_color": "(String) Basic geometric shape and dominant colors",\n'
|
| 117 |
+
' "texture": "(String) e.g., smooth, rough, metallic, furry, glossy, matte",\n'
|
| 118 |
+
' "appearance_details": "(String) Specific details such as patterns, text physically printed on the object, wear marks, or distinctive markings",\n'
|
| 119 |
+
' "relationship": "(String) This object\'s spatial or contextual relationship with other elements in the scene. If this object obscures text, specify exactly what it covers (e.g., \'blocking the letter O in COW). If it is obscured by floating text, describe what part of the object is covered.",\n'
|
| 120 |
+
' "orientation": "(String) e.g., upright, tilted, horizontal, facing away, diagonal",\n'
|
| 121 |
+
' \n'
|
| 122 |
+
' "pose": "(String) Body posture and its changes (human/humanoid only, otherwise empty)",\n'
|
| 123 |
+
' "expression": "(String) Facial expression and emotional changes (human/humanoid only, otherwise empty)",\n'
|
| 124 |
+
' "clothing": "(String) Clothing description, including colors and styles (human/humanoid only, otherwise empty)",\n'
|
| 125 |
+
' "gender": "(String) Apparent gender (human/humanoid only, otherwise empty)",\n'
|
| 126 |
+
' "skin_tone_and_texture": "(String) Skin appearance (human/humanoid only, otherwise empty)",\n'
|
| 127 |
+
' \n'
|
| 128 |
+
' "is_cluster": "true (only for cluster objects, omit otherwise)",\n'
|
| 129 |
+
' "number_of_objects": "(String) Exact number if countable, otherwise \'several\' (3-6), \'many\' (7-20), or \'numerous\' (20+)"\n'
|
| 130 |
+
' }\n'
|
| 131 |
+
' ],\n'
|
| 132 |
+
'\n'
|
| 133 |
+
' "camera_info": {\n'
|
| 134 |
+
' "color": "(String) Warm, Cool, Mixed, Saturated, Desaturated, Black and White, Red, Orange, Yellow, Green, Cyan, Blue, Magenta, or Pink",\n'
|
| 135 |
+
' "frame_size": "(String) Extreme Wide, Wide, Medium Wide, Medium, Medium Close Up, Close Up, or Extreme Close Up",\n'
|
| 136 |
+
' "shot_type_angle": "(String) High angle, Low angle, Dutch angle, Overhead, Aerial, or Eye level",\n'
|
| 137 |
+
' "lens_size": "(String) Ultra Wide / Fisheye, Wide, Medium, Long Lens, or Telephoto",\n'
|
| 138 |
+
' "composition": "(String) Center, Balanced, Symmetrical, Left heavy, Right heavy, or Short side",\n'
|
| 139 |
+
' "lighting": "(String) Hard light, Soft light, High contrast, Low contrast, Side light, Top light, Underlight, Backlight, Edge light, or Silhouette",\n'
|
| 140 |
+
' "lighting_type": "(String) Daylight, Sunny, Overcast, Moonlight, Artificial light, Practical light, Tungsten, Fluorescent, Firelight, or Mixed light"\n'
|
| 141 |
+
' }\n'
|
| 142 |
+
'}\n'
|
| 143 |
+
'```\n'
|
| 144 |
+
'\n'
|
| 145 |
+
'## IMPORTANT RULES\n'
|
| 146 |
+
'\n'
|
| 147 |
+
'1. **Think First:** Before generating the JSON, perform an internal "Let\'s think step by step" synthesis.\n'
|
| 148 |
+
'2. **Output Only JSON:** Do not output any thinking process or Markdown text outside of the JSON code block.\n'
|
| 149 |
+
'3. **Strict Fidelity (No Modification)**:\n'
|
| 150 |
+
'- Identity: Never alter the identity of subjects (e.g., a "real tiger" remains "real tiger").\n'
|
| 151 |
+
'- Actions: Verbs must be preserved exactly (e.g., "walking" cannot be changed to "running"). Sequence Integrity: You must preserve the complete action chain defined in the prompt. Do not summarize, skip, or merge distinct sequential steps into a single state. No Collective Summarization: Do not use summary phrases (e.g., "a sequence of...", "various movements") to cover multiple steps. If the prompt defines A -> B -> C, every step must be represented as a separate action segment. Any skipped step or summarized sequence is a CRITICAL ERROR.\n'
|
| 152 |
+
'- Quantity: If a number is specified, it must be exact. Quantifiers must be strictly preserved. "All" means all, "every" means every, "a single" means exactly 1. Do not paraphrase quantifiers into vague terms.\n'
|
| 153 |
+
'- Spatial Integrity: Relative positions (e.g., "A on the left of B") must be fixed as stated.\n'
|
| 154 |
+
'- Color & Text: Colors must be accurate; OCR text must be transcribed character-for-character (case-sensitive).\n'
|
| 155 |
+
'4. **Creative Expansion Constraints:** When expanding a simple prompt, ensure added details (e.g., "sun-drenched oak windowsill") never contradict, replace, or occlude the user\'s explicit subject requirements.\n'
|
| 156 |
+
'5. **Text Placement Logic:** \n'
|
| 157 |
+
'- Surface-Bound Text (Printed on objects): Must be transcribed exactly within the appearance_details field of its respective object in prominent_elements.\n'
|
| 158 |
+
'- Standalone Graphic Text (Floating/Poster text): Must NOT be an entry in prominent_elements. Describe its role, style, and exact transcription exclusively within the comprehensive_description.\n'
|
| 159 |
+
"- Dynamic Text: If the scene contains moving or updating text (e.g., a scrolling ticker or a subtitle), you must explicitly describe the text's movement path and content change frequency within the actions array of the respective prominent_elements or the comprehensive_description.\n"
|
| 160 |
+
'6. **Occlusion & Relational Mapping:** Explicitly document overlaps. If text occludes an object, or an object occludes text, specify which character(s) or object parts are affected in both relationship and comprehensive_description.\n'
|
| 161 |
+
'7. **Handle Missing Data:** If an attribute is not applicable to an element (e.g., pose for a mountain), set its value to "" (an empty string). Never use "N/A", "unknown", "not applicable", etc.\n'
|
| 162 |
+
'8. **Counting:** For number_of_objects, provide an exact number if specified by the user. Otherwise, use: "several" (3-6), "many" (7-20), or "numerous" (20+).\n'
|
| 163 |
+
'9. **Perspective:** Always describe positions (location) and directions (orientation) as "left" and "right" from the viewer\'s perspective.\n'
|
| 164 |
+
'10. **Visual Descriptive Realism:** Avoid abstract or emotional adjectives (e.g., "beautiful," "sad"). Instead, translate them into visually observable details (e.g., "soft golden-hour rim lighting," "desaturated cool blue tones with falling rain droplets").\n'
|
| 165 |
+
'11. **Clusters:** When describing a cluster (e.g., "a forest"), describe the collective appearance of the group rather than listing every individual tree.\n'
|
| 166 |
+
'12. **Attribute Consistency:** Ensure that the comprehensive_description and the individual prominent_elements are perfectly synchronized. Every element mentioned in the elements list must exist in the paragraph description and vice versa.\n'
|
| 167 |
+
'13. **Negative Constraints:** Any explicit prohibitions (e.g., "no text," "no background") must be strictly upheld. Never add elements that the user has explicitly requested to exclude.\n'
|
| 168 |
+
'14. **Subject Priority:** The primary subject specified by the user must be the anchor of the scene. Its description and prominence in the JSON must reflect its status as the most important element.\n'
|
| 169 |
+
'15. **Integrity:** Before generating JSON, you must cross-verify all keywords, actions, and states against the User Prompt; if any item is missing or any state intensity deviates from the original, you MUST regenerate the content to ensure 100% fidelity. \n'
|
| 170 |
+
'16. **Non-OCR Classification:** Do not force objects into "text" status. If a word appears in the prompt without context of "sign," "label," or "document," describe it as a physical attribute or brand marking on the object, not as OCR/Typography.\n'
|
| 171 |
+
"17. **Camera Fidelity:** The camera_info must be logically consistent with the visual scene. For example, if the scene is a vast landscape, the camera should reflect a 'Wide' or 'Ultra Wide' frame size and appropriate lens settings. Avoid contradictory pairings (e.g., 'Extreme Close Up' with a 'Wide' lens).\n"
|
| 172 |
+
'18. **Temporal Continuity:** Ensure temporal continuity for all subjects and environment settings. If an object is introduced, it must not disappear or reappear without a logical reason (e.g., leaving the frame or being occluded). Texture, color, and lighting must remain consistent across all action segments.\n'
|
| 173 |
+
"19. **Action-Camera Sync:** The camera movement must logically justify the subject's displacement. If the camera follows a subject (tracking shot), the subject's position in the frame should remain relatively stable. If the camera is static, the subject must show movement within the frame.\n"
|
| 174 |
+
"20. **Physical Logic:** All movements must respect physical laws unless the prompt specifies a surreal or fantasy context. Avoid 'gliding' motions; ensure footsteps or object interactions (e.g., picking up an item) align with the timestamped actions.\n"
|
| 175 |
+
'21. **Motion and Velocity:** Verbs must describe the process of an action, not just the result (e.g., use \'rising from the chair\' instead of just \'standing\'). Every action in the actions array must include a velocity modifier (e.g., "slowly walking," "abruptly turning," "smoothly panning," "rapidly accelerating"). Avoid generic verbs without speed or force descriptors.\n'
|
| 176 |
+
'22. **Language:** Output all JSON content in professional English, except for OCR text, which must be transcribed exactly as provided to maintain absolute fidelity.\n'
|
| 177 |
+
'23. **Audio Neglect:** The User Prompt may contain audio, music, or voiceover instructions. Strictly ignore all audio-related instructions. Do not attempt to rewrite, describe, or represent these as part of the JSON output. Focus exclusively on the visual, temporal, and narrative elements and OCR texts.\n'
|
| 178 |
+
'24. **Duration Adherence:** A target video duration is provided in the input. Every timestamp in the `actions` array MUST fall within `[0.0s, the given duration]`, and the action timeline should span up to (but never beyond) this duration. Distribute the action segments proportionally to the given length; the final segment must end at or before the given duration.\n'
|
| 179 |
+
'\n'
|
| 180 |
+
"## THIS STEP'S INPUT/OUTPUT\n"
|
| 181 |
+
'- Input: the detailed prose caption + the target video duration.\n'
|
| 182 |
+
'- The expansion/thinking is already done in the prose — preserve the stated core EXACTLY (scene, subjects, every action + timestamp, named entities); ONLY the fine visual attributes and camera fields that the brief prose omits may be completed — plausibly, consistently, never contradicting the prose.\n'
|
| 183 |
+
'- Output ONLY the JSON object (valid and parseable), following the schema and rules above.'
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
IMAGE_STEP1_EXPAND = (
|
| 187 |
+
'You write ONE short, natural, standalone IMAGE caption in English from a brief user image\n'
|
| 188 |
+
'prompt — as if briefly describing a real photo of that idea.\n'
|
| 189 |
+
'\n'
|
| 190 |
+
'This is a SINGLE STILL IMAGE: there is NO motion, NO time, NO timestamps, NO camera movement.\n'
|
| 191 |
+
'Describe only what the photo shows; never invent action, sequence, or a moving camera.\n'
|
| 192 |
+
'\n'
|
| 193 |
+
'HARD LENGTH LIMIT: the output MUST be UNDER 1000 characters. Aim for roughly 400-800\n'
|
| 194 |
+
'characters. Concise is a top priority.\n'
|
| 195 |
+
'\n'
|
| 196 |
+
'STYLE — a flowing description, NOT a checklist:\n'
|
| 197 |
+
'- Connected, natural English prose. NO headings, NO bullets, NO field labels.\n'
|
| 198 |
+
'- The MAIN THREAD is the main subject and the scene — lead with WHAT THE IMAGE SHOWS. Do NOT\n'
|
| 199 |
+
' march subject-by-subject listing attributes; weave the few details you keep into the\n'
|
| 200 |
+
' description naturally. One cohesive paragraph; never pad to add length.\n'
|
| 201 |
+
'\n'
|
| 202 |
+
'WHAT TO COVER (only these):\n'
|
| 203 |
+
'- The scene in a brief phrase (where it is / overall vibe).\n'
|
| 204 |
+
'- Each main subject by name/what-it-is plus ONE most defining visual trait — not a full\n'
|
| 205 |
+
' appearance list. The key spatial arrangement only if it defines the image.\n'
|
| 206 |
+
'- A one-word shot type only if it matters (e.g. close-up, wide shot, aerial); otherwise skip the camera.\n'
|
| 207 |
+
"- Named entities (real people / places / landmarks / brands) only if clearly implied — don't invent identities.\n"
|
| 208 |
+
'\n'
|
| 209 |
+
'FAITHFUL EXPANSION:\n'
|
| 210 |
+
'- Stay consistent with the prompt; elaborate plausibly but never contradict it. Commit to one\n'
|
| 211 |
+
" coherent interpretation; don't hedge with alternatives.\n"
|
| 212 |
+
'- Keep subject identities and counts consistent.\n'
|
| 213 |
+
'\n'
|
| 214 |
+
'Output ONLY the caption text — no preamble, no headings, no explanation.\n'
|
| 215 |
+
'\n'
|
| 216 |
+
'\n'
|
| 217 |
+
"## EXAMPLES — one representative example per type. When you write your own caption, match these examples' style, format, and length (one flowing paragraph, no timestamps, no camera movement — it is a single still image).\n"
|
| 218 |
+
'\n'
|
| 219 |
+
'### Example 1 — animal / wildlife\n'
|
| 220 |
+
'USER PROMPT:\n'
|
| 221 |
+
'A leopard tortoise dominates the center of the frame, captured from a high angle with its head and front legs extended toward the left. Its domed shell displays a striking mosaic of black and tan geometric patterns, contrasting with the rough, yellowish-tan scales of its skin and sharp claws. The creature is positioned on reddish-brown sandy soil scattered with dry twigs and pebbles, illuminated by warm daylight that creates a soft shadow beneath it against a backdrop of green grass on the right.\n'
|
| 222 |
+
'\n'
|
| 223 |
+
'DETAILED CAPTION:\n'
|
| 224 |
+
'A leopard tortoise is positioned centrally in a natural outdoor setting, resting on reddish-brown sandy soil scattered with small pebbles and dry twigs. Facing toward the left with its head and front legs extended, the animal displays a domed shell marked by intricate black and tan geometric patterns reminiscent of leopard spots. Its yellowish-tan skin contrasts with the earthy ground beneath its clawed feet, while a patch of green grass and low-lying vegetation rises in the background to the right. The distinct markings of the shell stand out against the textured ground and sparse greenery, capturing the tortoise within its earthy environment.\n'
|
| 225 |
+
'\n'
|
| 226 |
+
'### Example 2 — architecture / landmark\n'
|
| 227 |
+
'USER PROMPT:\n'
|
| 228 |
+
'极度广角镜头,高角度拍摄。明亮日光,硬光,暖色调,清晰阴影。宏伟的古典风格宫殿庭院,浅色石材。中央巨大的多层拱形入口,饰有金色和蓝色装饰带,深色凹陷内部可见小金门。宽阔石阶通向圆形分层平台。两侧对称亭阁,顶部为反射金色圆顶,蓝色瓷砖拱门,装饰攀爬绿藤和粉色花朵。露台布满茂盛绿植和小花。若干穿着传统长袍(白、灰、棕、红)的小人物散布庭院以示比例。背景岩石山坡,远处建筑,晴朗明亮天空。写实建筑摄影。\n'
|
| 229 |
+
'\n'
|
| 230 |
+
'DETAILED CAPTION:\n'
|
| 231 |
+
'A grand, sun-drenched courtyard of a classical-style palace complex is dominated by a massive central building featuring a large, multi-tiered arched entrance. Wide stone stairs lead up to the structure, flanked on either side by identical pavilions topped with brilliant, reflective golden domes and blue-tiled arches. The light-colored stone complex includes various terraces filled with lush greenery, while several small figures dressed in traditional robes are scattered throughout the courtyard, providing a sense of scale against the monumental architecture. In the background, a rocky hillside rises under a clear bright sky, dotted with additional buildings and vegetation surrounding the temple-like grounds.\n'
|
| 232 |
+
'\n'
|
| 233 |
+
'### Example 3 — landscape / nature\n'
|
| 234 |
+
'USER PROMPT:\n'
|
| 235 |
+
"Make me an image of a serene coastal scene featuring a classic white lighthouse attached to a keeper's house, with a vintage red pickup truck and a small boat on a trailer parked on a dirt path nearby, set against a lush green lawn, wooden fence, and bright blue sky over calm waters.\n"
|
| 236 |
+
'\n'
|
| 237 |
+
'DETAILED CAPTION:\n'
|
| 238 |
+
"A wide aerial view captures a serene coastal scene dominated by a classic white lighthouse and its attached keeper's house. The tall cylindrical tower features a black lantern room and rises from a white residence with a brown shingled roof and green shutters. To the left, a classic red pickup truck is parked on a dirt path with a small white and red boat resting on a trailer behind it. A lush green lawn slopes down toward a rocky shoreline where a weathered wooden fence runs along the edge of the deep blue water. In the background, a calm sea stretches to the horizon, meeting a distant tree-covered coastline under a bright blue sky filled with soft clouds.\n"
|
| 239 |
+
'\n'
|
| 240 |
+
'### Example 4 — person / portrait\n'
|
| 241 |
+
'USER PROMPT:\n'
|
| 242 |
+
'A stylish woman with long wavy blonde hair and bold red lipstick stands confidently on a city sidewalk, wearing a black long-sleeved dress with sheer patterned sleeves and a ruffled waistline. She carries a small black crossbody bag with a gold Saint Laurent logo slung over her shoulder, her left arm slightly extended and right arm by her side in a high-angle medium wide shot. The background features brick buildings and a blurred crosswalk with distant pedestrians under soft daylight, creating a sophisticated urban atmosphere with a left-heavy composition.\n'
|
| 243 |
+
'\n'
|
| 244 |
+
'DETAILED CAPTION:\n'
|
| 245 |
+
'A stylish woman with long blonde hair and bold red lipstick stands on a city sidewalk, gazing directly forward in a black long-sleeved dress with sheer patterned sleeves and a ruffled waistline. A small black crossbody bag featuring a gold Saint Laurent logo hangs at her side, complementing her sophisticated look. The setting is a classic urban street scene characterized by brick buildings and a black wall-mounted lamp visible behind her. In the distance, a blurred crosswalk shows several pedestrians, adding depth to the modern atmosphere surrounding the central figure.'
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
IMAGE_STEP2_MAP = (
|
| 249 |
+
'You convert a SHORT natural-language STILL-IMAGE caption (the DETAILED CAPTION) into ONE\n'
|
| 250 |
+
'structured JSON caption describing the image. Output ONLY the JSON object — no prose, no code fence.\n'
|
| 251 |
+
'\n'
|
| 252 |
+
'## TASK SEMANTICS (read carefully)\n'
|
| 253 |
+
'The detailed caption is SHORT and LOSSY: it states the scene, the main subjects, and each\n'
|
| 254 |
+
"subject's single most defining trait, but it deliberately OMITS fine attributes (exact colors,\n"
|
| 255 |
+
'texture, relative size, orientation, clothing, skin tone, pose, expression) and the camera\n'
|
| 256 |
+
'metadata. Your job:\n'
|
| 257 |
+
'- KEEP faithfully everything the prose states (scene, every named subject, named entities) — do\n'
|
| 258 |
+
' not drop, rename, merge, or split subjects, and do not contradict the prose.\n'
|
| 259 |
+
'- REASONABLY COMPLETE the fields the prose omits (fine attributes + camera_info) with plausible,\n'
|
| 260 |
+
' self-consistent values that do NOT contradict the prose. This is expected: the target JSON is a\n'
|
| 261 |
+
' FULL caption, so every schema field must be filled even when the prose did not mention it.\n'
|
| 262 |
+
'- This is a STILL IMAGE: no actions, no timestamps, no camera movement anywhere.\n'
|
| 263 |
+
'\n'
|
| 264 |
+
'## OUTPUT SCHEMA (exact keys)\n'
|
| 265 |
+
'A single JSON object with EXACTLY these top-level keys:\n'
|
| 266 |
+
'- "comprehensive_description": string — one flowing prose paragraph describing the whole image\n'
|
| 267 |
+
' (scene, subjects, their arrangement and look). No field labels inside it.\n'
|
| 268 |
+
'- "camera_info": object with EXACTLY these 7 string keys, each set to ONE value from its list:\n'
|
| 269 |
+
' - "color": Warm | Cool | Mixed | Saturated | Desaturated | White | Green | Blue | Red | Cyan\n'
|
| 270 |
+
' - "frame_size": Extreme Close Up | Close Up | Medium Close Up | Medium | Medium Wide | Wide | Extreme Wide\n'
|
| 271 |
+
' - "shot_type_angle": Eye level | Low angle | High angle | Overhead | Aerial\n'
|
| 272 |
+
' - "lens_size": Ultra Wide / Fisheye | Wide | Medium | Long Lens | Telephoto\n'
|
| 273 |
+
' - "composition": Center | Balanced | Symmetrical | Left heavy | Right heavy\n'
|
| 274 |
+
' - "lighting": Soft light | Hard light | Top light | Underlight | Backlight\n'
|
| 275 |
+
' - "lighting_type": Daylight | Artificial light\n'
|
| 276 |
+
'- "world_knowledge": array of strings — named entities / real-world facts (people, places,\n'
|
| 277 |
+
' landmarks, brands) stated or clearly implied by the prose; [] if none. NEVER invent identities.\n'
|
| 278 |
+
'- "prominent_elements": array of objects, one per notable subject/object. Each element has:\n'
|
| 279 |
+
' - always: "name", "description", "location", "relative_size" (one of: dominant | large | medium | small),\n'
|
| 280 |
+
' "shape_and_color", "texture", "appearance_details", "relationship", "orientation"\n'
|
| 281 |
+
' - for a PERSON, additionally: "pose", "expression", "clothing", "gender" (male | female),\n'
|
| 282 |
+
' "skin_tone_and_texture"\n'
|
| 283 |
+
' - for a GROUP/crowd of like items, additionally: "is_cluster": true and "number_of_objects": "<count or range as string>"\n'
|
| 284 |
+
'\n'
|
| 285 |
+
'## RULES\n'
|
| 286 |
+
'- Output STRICT valid JSON only (double quotes, no trailing commas, no comments, no code fence).\n'
|
| 287 |
+
'- Fill EVERY field; never leave a required field empty. Values you complete must be consistent\n'
|
| 288 |
+
' with the prose and with each other.\n'
|
| 289 |
+
'- Keep the number and identity of prominent_elements aligned with the subjects the prose names\n'
|
| 290 |
+
' (you may add a clearly-implied background element, but do not invent unrelated subjects).\n'
|
| 291 |
+
'- Copy named entities verbatim into world_knowledge.\n'
|
| 292 |
+
'- No actions, no timestamps, no camera-movement fields — this is a still image.'
|
| 293 |
+
)
|