victor HF Staff commited on
Commit
eb9fca3
·
verified ·
1 Parent(s): 8364cb3

init: LingBot-Video MoE 30B-A3B ZeroGPU demo

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ 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_1.png filter=lfs diff=lfs merge=lfs -text
37
+ examples/ti2v_5.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,32 @@
1
  ---
2
- title: Lingbot Video
3
- emoji: 🐠
4
- colorFrom: yellow
5
- colorTo: green
6
  sdk: gradio
7
- sdk_version: 6.20.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: LingBot-Video MoE 30B-A3B
3
+ emoji: 🎬
4
+ colorFrom: indigo
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 6.10.0
 
8
  app_file: app.py
9
+ license: apache-2.0
10
+ short_description: Embodied MoE video generation — T2V, I2V, T2I
11
+ models:
12
+ - robbyant/lingbot-video-moe-30b-a3b
13
+ startup_duration_timeout: 1h
14
  ---
15
 
16
+ # LingBot-Video MoE 30B-A3B — ZeroGPU demo
17
+
18
+ Text-to-video, image-to-video and text-to-image with
19
+ [LingBot-Video MoE 30B-A3B](https://huggingface.co/robbyant/lingbot-video-moe-30b-a3b),
20
+ the first open-source large-scale MoE video generation model dedicated to embodied intelligence
21
+ (30B parameters, ~3B active per token).
22
+
23
+ This demo runs the base MoE DiT (the refiner stage is skipped to fit ZeroGPU) with:
24
+
25
+ - bf16 transformer + text encoder (Qwen3-VL), fp32 Wan VAE
26
+ - 480p resolution buckets, up to 5 s at 24 fps
27
+ - sequential CFG with SDPA flash attention
28
+ - `torch._grouped_mm` MoE expert dispatch
29
+
30
+ The model was trained on detailed structured captions — long, specific prompts (or JSON captions
31
+ from the [LingBot rewriter](https://huggingface.co/robbyant/lingbot-video-rewriter-lora)) work
32
+ much better than short casual ones.
app.py ADDED
@@ -0,0 +1,456 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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("LINGBOT_MOE_EXPERT_BACKEND", "grouped_mm")
8
+ os.environ.setdefault("LINGBOT_MOE_PAD_BACKEND", "vectorized")
9
+ os.environ.setdefault("DIFFUSERS_ATTN_BACKEND", "_native_flash")
10
+
11
+ import spaces # noqa: E402 — must be imported before torch
12
+
13
+ import random # noqa: E402
14
+ import time # noqa: E402
15
+ from pathlib import Path # noqa: E402
16
+
17
+ import gradio as gr # noqa: E402
18
+ import numpy as np # noqa: E402
19
+ import torch # noqa: E402
20
+ from PIL import Image # noqa: E402
21
+ from diffusers import AutoencoderKLWan # noqa: E402
22
+ from diffusers.utils import export_to_video # noqa: E402
23
+ from huggingface_hub import snapshot_download # noqa: E402
24
+ from transformers import AutoProcessor, Qwen3VLForConditionalGeneration # noqa: E402
25
+
26
+ from lingbot_video import ( # noqa: E402
27
+ FlowUniPCMultistepScheduler,
28
+ LingBotVideoImageToVideoPipeline,
29
+ LingBotVideoPipeline,
30
+ LingBotVideoTransformer3DModel,
31
+ )
32
+ from lingbot_video.pipeline_lingbot_video import ( # noqa: E402
33
+ DEFAULT_NEGATIVE_PROMPT,
34
+ DEFAULT_NEGATIVE_PROMPT_IMAGE,
35
+ )
36
+ from lingbot_video.utils import num_frames_from_duration # noqa: E402
37
+
38
+ MODEL_ID = "robbyant/lingbot-video-moe-30b-a3b"
39
+ FPS = 24
40
+ MAX_SEED = 2**31 - 1
41
+ MAX_GPU_SECONDS = 300
42
+
43
+ # (height, width), multiples of 16 — official 480p buckets.
44
+ VIDEO_SIZES = {
45
+ "832 × 480 (16:9)": (480, 832),
46
+ "480 × 832 (9:16)": (832, 480),
47
+ "640 × 480 (4:3)": (480, 640),
48
+ "480 × 480 (1:1)": (480, 480),
49
+ }
50
+ IMAGE_SIZES = {
51
+ "832 × 480 (16:9)": (480, 832),
52
+ "480 × 832 (9:16)": (832, 480),
53
+ "480 × 480 (1:1)": (480, 480),
54
+ "1280 × 736 (16:9, 720p)": (736, 1280),
55
+ "736 × 1280 (9:16, 720p)": (1280, 736),
56
+ "1088 × 1088 (1:1, 1080p)": (1088, 1088),
57
+ }
58
+
59
+ EXAMPLES_DIR = Path(__file__).parent / "examples"
60
+
61
+
62
+ def _read_example(name: str) -> str:
63
+ return (EXAMPLES_DIR / name).read_text(encoding="utf-8").strip()
64
+
65
+
66
+ print(f"[startup] downloading {MODEL_ID} (refiner excluded) ...", flush=True)
67
+ t0 = time.perf_counter()
68
+ model_dir = snapshot_download(
69
+ MODEL_ID,
70
+ ignore_patterns=["refiner/*", "*.bak_lingbot_video_diffusers"],
71
+ )
72
+ print(f"[startup] snapshot ready in {time.perf_counter() - t0:.1f}s", flush=True)
73
+
74
+ print(f"[startup] torch {torch.__version__}, grouped_mm available: {hasattr(torch, '_grouped_mm')}", flush=True)
75
+
76
+ t0 = time.perf_counter()
77
+ transformer = LingBotVideoTransformer3DModel.from_pretrained(
78
+ model_dir, subfolder="transformer", torch_dtype=torch.bfloat16
79
+ )
80
+ text_encoder = Qwen3VLForConditionalGeneration.from_pretrained(
81
+ model_dir, subfolder="text_encoder", dtype=torch.bfloat16, attn_implementation="sdpa"
82
+ )
83
+ processor = AutoProcessor.from_pretrained(model_dir, subfolder="processor")
84
+ vae = AutoencoderKLWan.from_pretrained(model_dir, subfolder="vae", torch_dtype=torch.float32)
85
+ print(f"[startup] components loaded in {time.perf_counter() - t0:.1f}s", flush=True)
86
+
87
+ t0 = time.perf_counter()
88
+ pipe = LingBotVideoPipeline(
89
+ transformer=transformer,
90
+ vae=vae,
91
+ text_encoder=text_encoder,
92
+ processor=processor,
93
+ scheduler=FlowUniPCMultistepScheduler.from_pretrained(model_dir, subfolder="scheduler"),
94
+ ).to("cuda")
95
+ pipe_i2v = LingBotVideoImageToVideoPipeline(
96
+ transformer=transformer,
97
+ vae=vae,
98
+ text_encoder=text_encoder,
99
+ processor=processor,
100
+ scheduler=FlowUniPCMultistepScheduler.from_pretrained(model_dir, subfolder="scheduler"),
101
+ )
102
+ print(f"[startup] pipelines on cuda in {time.perf_counter() - t0:.1f}s", flush=True)
103
+
104
+
105
+ def _resolve_seed(seed: float, randomize: bool) -> int:
106
+ if randomize:
107
+ return random.randint(0, MAX_SEED)
108
+ return int(seed)
109
+
110
+
111
+ def _latent_tokens(height: int, width: int, num_frames: int) -> int:
112
+ latent_frames = (num_frames - 1) // 4 + 1
113
+ return latent_frames * (height // 16) * (width // 16)
114
+
115
+
116
+ def _step_seconds(height: int, width: int, num_frames: int) -> float:
117
+ # Empirical per-step cost model (CFG included): quadratic attention term +
118
+ # linear MoE/MLP term, normalized to the 81-frame 480x832 token count.
119
+ r = _latent_tokens(height, width, num_frames) / 32760.0
120
+ return 9.0 * r * r + 6.0 * r
121
+
122
+
123
+ def _estimate_video_seconds(size_label: str, duration_s: float, steps: int, sizes=VIDEO_SIZES) -> int:
124
+ height, width = sizes[size_label]
125
+ num_frames = num_frames_from_duration(duration_s, FPS)
126
+ return int(40.0 + steps * _step_seconds(height, width, num_frames))
127
+
128
+
129
+ def _estimate_image_seconds(size_label: str, steps: int) -> int:
130
+ height, width = IMAGE_SIZES[size_label]
131
+ return int(30.0 + steps * max(_step_seconds(height, width, 1), 0.35))
132
+
133
+
134
+ def _check_budget(estimate: int) -> None:
135
+ if estimate > MAX_GPU_SECONDS:
136
+ raise gr.Error(
137
+ f"These settings need ~{estimate}s of GPU time (max {MAX_GPU_SECONDS}s). "
138
+ "Reduce the video duration, steps, or resolution."
139
+ )
140
+
141
+
142
+ def _export_video(frames: np.ndarray) -> str:
143
+ path = f"/tmp/lingbot_{int(time.time() * 1000)}.mp4"
144
+ export_to_video(frames, path, fps=FPS)
145
+ return path
146
+
147
+
148
+ def _t2v_duration(prompt, size_label, duration_s, steps, *args, **kwargs) -> int:
149
+ return min(MAX_GPU_SECONDS, _estimate_video_seconds(size_label, duration_s, int(steps)) + 20)
150
+
151
+
152
+ @spaces.GPU(duration=_t2v_duration, size="xlarge")
153
+ def generate_t2v(
154
+ prompt,
155
+ size_label,
156
+ duration_s,
157
+ steps,
158
+ guidance_scale,
159
+ shift,
160
+ negative_prompt,
161
+ seed,
162
+ randomize_seed,
163
+ progress=gr.Progress(track_tqdm=True),
164
+ ):
165
+ if not prompt or not prompt.strip():
166
+ raise gr.Error("Please enter a prompt.")
167
+ estimate = _estimate_video_seconds(size_label, duration_s, int(steps))
168
+ _check_budget(estimate)
169
+ height, width = VIDEO_SIZES[size_label]
170
+ num_frames = num_frames_from_duration(duration_s, FPS)
171
+ seed = _resolve_seed(seed, randomize_seed)
172
+ t0 = time.perf_counter()
173
+ output = pipe(
174
+ prompt=prompt.strip(),
175
+ negative_prompt=negative_prompt.strip() or DEFAULT_NEGATIVE_PROMPT,
176
+ height=height,
177
+ width=width,
178
+ num_frames=num_frames,
179
+ num_inference_steps=int(steps),
180
+ guidance_scale=float(guidance_scale),
181
+ shift=float(shift),
182
+ generator=torch.Generator().manual_seed(seed),
183
+ )
184
+ torch.cuda.synchronize()
185
+ elapsed = time.perf_counter() - t0
186
+ print(
187
+ f"[timing] t2v {width}x{height} f={num_frames} steps={int(steps)} "
188
+ f"tokens={_latent_tokens(height, width, num_frames)} took {elapsed:.1f}s "
189
+ f"(estimated {estimate}s)",
190
+ flush=True,
191
+ )
192
+ return _export_video(output.frames[0]), seed
193
+
194
+
195
+ def _i2v_duration(image, prompt, size_label, duration_s, steps, *args, **kwargs) -> int:
196
+ return min(MAX_GPU_SECONDS, _estimate_video_seconds(size_label, duration_s, int(steps)) + 35)
197
+
198
+
199
+ @spaces.GPU(duration=_i2v_duration, size="xlarge")
200
+ def generate_i2v(
201
+ image,
202
+ prompt,
203
+ size_label,
204
+ duration_s,
205
+ steps,
206
+ guidance_scale,
207
+ shift,
208
+ negative_prompt,
209
+ seed,
210
+ randomize_seed,
211
+ progress=gr.Progress(track_tqdm=True),
212
+ ):
213
+ if image is None:
214
+ raise gr.Error("Please upload a first-frame image.")
215
+ if not prompt or not prompt.strip():
216
+ raise gr.Error("Please enter a prompt.")
217
+ estimate = _estimate_video_seconds(size_label, duration_s, int(steps)) + 15
218
+ _check_budget(estimate)
219
+ height, width = VIDEO_SIZES[size_label]
220
+ num_frames = num_frames_from_duration(duration_s, FPS)
221
+ seed = _resolve_seed(seed, randomize_seed)
222
+ t0 = time.perf_counter()
223
+ output = pipe_i2v(
224
+ prompt=prompt.strip(),
225
+ image=image,
226
+ negative_prompt=negative_prompt.strip() or DEFAULT_NEGATIVE_PROMPT,
227
+ height=height,
228
+ width=width,
229
+ num_frames=num_frames,
230
+ num_inference_steps=int(steps),
231
+ guidance_scale=float(guidance_scale),
232
+ shift=float(shift),
233
+ generator=torch.Generator().manual_seed(seed),
234
+ )
235
+ torch.cuda.synchronize()
236
+ elapsed = time.perf_counter() - t0
237
+ print(
238
+ f"[timing] i2v {width}x{height} f={num_frames} steps={int(steps)} took {elapsed:.1f}s "
239
+ f"(estimated {estimate}s)",
240
+ flush=True,
241
+ )
242
+ return _export_video(output.frames[0]), seed
243
+
244
+
245
+ def _t2i_duration(prompt, size_label, steps, *args, **kwargs) -> int:
246
+ return min(MAX_GPU_SECONDS, _estimate_image_seconds(size_label, int(steps)) + 20)
247
+
248
+
249
+ @spaces.GPU(duration=_t2i_duration, size="xlarge")
250
+ def generate_t2i(
251
+ prompt,
252
+ size_label,
253
+ steps,
254
+ guidance_scale,
255
+ shift,
256
+ negative_prompt,
257
+ seed,
258
+ randomize_seed,
259
+ progress=gr.Progress(track_tqdm=True),
260
+ ):
261
+ if not prompt or not prompt.strip():
262
+ raise gr.Error("Please enter a prompt.")
263
+ height, width = IMAGE_SIZES[size_label]
264
+ seed = _resolve_seed(seed, randomize_seed)
265
+ t0 = time.perf_counter()
266
+ output = pipe(
267
+ prompt=prompt.strip(),
268
+ negative_prompt=negative_prompt.strip() or DEFAULT_NEGATIVE_PROMPT_IMAGE,
269
+ height=height,
270
+ width=width,
271
+ num_frames=1,
272
+ num_inference_steps=int(steps),
273
+ guidance_scale=float(guidance_scale),
274
+ shift=float(shift),
275
+ generator=torch.Generator().manual_seed(seed),
276
+ )
277
+ torch.cuda.synchronize()
278
+ elapsed = time.perf_counter() - t0
279
+ print(f"[timing] t2i {width}x{height} steps={int(steps)} took {elapsed:.1f}s", flush=True)
280
+ frame = output.frames[0][0]
281
+ image = Image.fromarray((np.clip(frame, 0, 1) * 255).astype(np.uint8))
282
+ return image, seed
283
+
284
+
285
+ DESCRIPTION = """
286
+ # LingBot-Video MoE 30B-A3B
287
+
288
+ First open-source large-scale MoE video generation model dedicated to **embodied intelligence** — 30B parameters,
289
+ 3B active per token. Text-to-video, image-to-video and text-to-image with a single model.
290
+
291
+ [Model card](https://huggingface.co/robbyant/lingbot-video-moe-30b-a3b) ·
292
+ [GitHub](https://github.com/Robbyant/lingbot-video) ·
293
+ [Project page](https://technology.robbyant.com/lingbot-video)
294
+
295
+ > 💡 The model was trained on **detailed structured captions**: long, specific prompts (or the JSON captions produced
296
+ > by the [LingBot rewriter](https://huggingface.co/robbyant/lingbot-video-rewriter-lora)) work much better than short ones.
297
+ > This demo runs the base MoE model (no refiner) at 480p on ZeroGPU.
298
+ """
299
+
300
+
301
+ def _seed_row():
302
+ with gr.Row():
303
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=42)
304
+ randomize = gr.Checkbox(label="Randomize seed", value=True)
305
+ return seed, randomize
306
+
307
+
308
+ def _advanced(negative_default: str, steps_value: int):
309
+ with gr.Accordion("Advanced settings", open=False):
310
+ steps = gr.Slider(label="Inference steps", minimum=4, maximum=40, step=1, value=steps_value)
311
+ guidance = gr.Slider(label="Guidance scale", minimum=1.0, maximum=10.0, step=0.5, value=3.0)
312
+ shift = gr.Slider(label="Flow shift", minimum=1.0, maximum=9.0, step=0.5, value=3.0)
313
+ negative = gr.Textbox(label="Negative prompt", value=negative_default, lines=3)
314
+ seed, randomize = _seed_row()
315
+ return steps, guidance, shift, negative, seed, randomize
316
+
317
+
318
+ with gr.Blocks(title="LingBot-Video MoE 30B-A3B") as demo:
319
+ gr.Markdown(DESCRIPTION)
320
+
321
+ with gr.Tab("Text → Video"):
322
+ with gr.Row():
323
+ with gr.Column():
324
+ t2v_prompt = gr.Textbox(
325
+ label="Prompt",
326
+ placeholder="Describe the scene in detail (plain text or a LingBot JSON caption)...",
327
+ lines=5,
328
+ max_lines=12,
329
+ )
330
+ with gr.Row():
331
+ t2v_size = gr.Dropdown(
332
+ label="Resolution", choices=list(VIDEO_SIZES), value="832 × 480 (16:9)"
333
+ )
334
+ t2v_dur = gr.Slider(
335
+ label="Video duration (s)", minimum=1.0, maximum=5.0, step=0.5, value=2.0
336
+ )
337
+ t2v_steps, t2v_guidance, t2v_shift, t2v_negative, t2v_seed, t2v_rand = _advanced(
338
+ DEFAULT_NEGATIVE_PROMPT, 24
339
+ )
340
+ t2v_btn = gr.Button("Generate video", variant="primary")
341
+ with gr.Column():
342
+ t2v_out = gr.Video(label="Generated video", autoplay=True)
343
+ t2v_seed_out = gr.Number(label="Seed used", interactive=False)
344
+
345
+ t2v_inputs = [
346
+ t2v_prompt, t2v_size, t2v_dur, t2v_steps, t2v_guidance, t2v_shift,
347
+ t2v_negative, t2v_seed, t2v_rand,
348
+ ]
349
+ t2v_btn.click(generate_t2v, inputs=t2v_inputs, outputs=[t2v_out, t2v_seed_out],
350
+ concurrency_id="gpu", concurrency_limit=1)
351
+ gr.Examples(
352
+ examples=[
353
+ [_read_example("t2v_bubbles.txt"), "832 × 480 (16:9)", 2.0, 24, 3.0, 3.0,
354
+ DEFAULT_NEGATIVE_PROMPT, 42, False],
355
+ [_read_example("t2v_robot_desk.txt"), "832 × 480 (16:9)", 2.0, 24, 3.0, 3.0,
356
+ DEFAULT_NEGATIVE_PROMPT, 42, False],
357
+ ["A corgi puppy runs through shallow water on a beach at golden hour. Slow motion, "
358
+ "splashes glistening in warm sunlight, shallow depth of field, cinematic color grading. "
359
+ "The camera tracks alongside the puppy at a low angle close to the water surface.",
360
+ "832 × 480 (16:9)", 2.0, 24, 3.0, 3.0, DEFAULT_NEGATIVE_PROMPT, 42, False],
361
+ ],
362
+ fn=generate_t2v,
363
+ inputs=t2v_inputs,
364
+ outputs=[t2v_out, t2v_seed_out],
365
+ cache_examples=True,
366
+ cache_mode="lazy",
367
+ )
368
+
369
+ with gr.Tab("Image → Video"):
370
+ with gr.Row():
371
+ with gr.Column():
372
+ i2v_image = gr.Image(label="First frame", type="pil")
373
+ i2v_prompt = gr.Textbox(
374
+ label="Prompt",
375
+ placeholder="Describe how the scene should evolve...",
376
+ lines=4,
377
+ max_lines=12,
378
+ )
379
+ with gr.Row():
380
+ i2v_size = gr.Dropdown(
381
+ label="Resolution", choices=list(VIDEO_SIZES), value="832 × 480 (16:9)"
382
+ )
383
+ i2v_dur = gr.Slider(
384
+ label="Video duration (s)", minimum=1.0, maximum=5.0, step=0.5, value=2.0
385
+ )
386
+ i2v_steps, i2v_guidance, i2v_shift, i2v_negative, i2v_seed, i2v_rand = _advanced(
387
+ DEFAULT_NEGATIVE_PROMPT, 24
388
+ )
389
+ i2v_btn = gr.Button("Generate video", variant="primary")
390
+ with gr.Column():
391
+ i2v_out = gr.Video(label="Generated video", autoplay=True)
392
+ i2v_seed_out = gr.Number(label="Seed used", interactive=False)
393
+
394
+ i2v_inputs = [
395
+ i2v_image, i2v_prompt, i2v_size, i2v_dur, i2v_steps, i2v_guidance, i2v_shift,
396
+ i2v_negative, i2v_seed, i2v_rand,
397
+ ]
398
+ i2v_btn.click(generate_i2v, inputs=i2v_inputs, outputs=[i2v_out, i2v_seed_out],
399
+ concurrency_id="gpu", concurrency_limit=1)
400
+ gr.Examples(
401
+ examples=[
402
+ [str(EXAMPLES_DIR / "ti2v_1.png"), _read_example("ti2v_1.txt"),
403
+ "832 × 480 (16:9)", 2.0, 24, 3.0, 3.0, DEFAULT_NEGATIVE_PROMPT, 42, False],
404
+ [str(EXAMPLES_DIR / "ti2v_5.png"), _read_example("ti2v_5.txt"),
405
+ "832 × 480 (16:9)", 2.0, 24, 3.0, 3.0, DEFAULT_NEGATIVE_PROMPT, 42, False],
406
+ ],
407
+ fn=generate_i2v,
408
+ inputs=i2v_inputs,
409
+ outputs=[i2v_out, i2v_seed_out],
410
+ cache_examples=True,
411
+ cache_mode="lazy",
412
+ )
413
+
414
+ with gr.Tab("Text → Image"):
415
+ with gr.Row():
416
+ with gr.Column():
417
+ t2i_prompt = gr.Textbox(
418
+ label="Prompt",
419
+ placeholder="Describe the image in detail...",
420
+ lines=5,
421
+ max_lines=12,
422
+ )
423
+ t2i_size = gr.Dropdown(
424
+ label="Resolution", choices=list(IMAGE_SIZES), value="1280 × 736 (16:9, 720p)"
425
+ )
426
+ t2i_steps, t2i_guidance, t2i_shift, t2i_negative, t2i_seed, t2i_rand = _advanced(
427
+ DEFAULT_NEGATIVE_PROMPT_IMAGE, 30
428
+ )
429
+ t2i_btn = gr.Button("Generate image", variant="primary")
430
+ with gr.Column():
431
+ t2i_out = gr.Image(label="Generated image")
432
+ t2i_seed_out = gr.Number(label="Seed used", interactive=False)
433
+
434
+ t2i_inputs = [
435
+ t2i_prompt, t2i_size, t2i_steps, t2i_guidance, t2i_shift,
436
+ t2i_negative, t2i_seed, t2i_rand,
437
+ ]
438
+ t2i_btn.click(generate_t2i, inputs=t2i_inputs, outputs=[t2i_out, t2i_seed_out],
439
+ concurrency_id="gpu", concurrency_limit=1)
440
+ gr.Examples(
441
+ examples=[
442
+ [_read_example("t2i_bottle.txt"), "1280 × 736 (16:9, 720p)", 30, 3.0, 3.0,
443
+ DEFAULT_NEGATIVE_PROMPT_IMAGE, 42, False],
444
+ ["A humanoid robot chef flipping a pancake in a bright modern kitchen, "
445
+ "photorealistic, dramatic side lighting, steam rising from the pan, "
446
+ "shallow depth of field, 85mm lens.",
447
+ "1280 × 736 (16:9, 720p)", 30, 3.0, 3.0, DEFAULT_NEGATIVE_PROMPT_IMAGE, 42, False],
448
+ ],
449
+ fn=generate_t2i,
450
+ inputs=t2i_inputs,
451
+ outputs=[t2i_out, t2i_seed_out],
452
+ cache_examples=True,
453
+ cache_mode="lazy",
454
+ )
455
+
456
+ demo.queue(max_size=30).launch()
examples/t2i_bottle.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ {"comprehensive_description":"A clear glass bottle filled with water sits on a light-colored wooden table, acting as a lens to focus sunlight. The bottle is positioned slightly to the left of the center, with its base resting on the table. Bright, direct sunlight from the upper right creates a brilliant, starburst-like lens flare on the table's surface, casting a warm, golden glow. The background is a soft, out-of-focus blur of green foliage and bright light, suggesting an outdoor garden or patio setting. The overall atmosphere is warm, serene, and summery, emphasizing the interplay between light and water.","camera_info":{"color":"Warm","frame_size":"Extreme Close Up","shot_type_angle":"Low angle","lens_size":"Long Lens","composition":"Left heavy","lighting":"Hard light","lighting_type":"Daylight"},"world_knowledge":[],"prominent_elements":[{"name":"glass bottle","description":"A clear, cylindrical glass bottle with a narrow neck and a flat base, partially filled with water.","location":"center-left","relative_size":"medium","shape_and_color":"Cylindrical with a tapered neck; transparent and clear","texture":"smooth, glossy","appearance_details":"The bottle contains clear water that reflects the surrounding light. The glass surface shows subtle highlights and reflections from the sun.","relationship":"Sits on the wooden table and acts as the focal point for the sunlight, creating a lens flare.","orientation":"upright","pose":"","expression":"","clothing":"","gender":"","skin_tone_and_texture":""},{"name":"wooden table","description":"A flat surface made of light-colored wood with visible grain and horizontal planks.","location":"spanning the bottom half of the frame","relative_size":"large","shape_and_color":"Rectangular planks; light tan and beige","texture":"matte, grainy","appearance_details":"The surface is illuminated by a bright, star-shaped lens flare caused by the bottle focusing the sunlight.","relationship":"Provides the base upon which the bottle rests.","orientation":"horizontal","pose":"","expression":"","clothing":"","gender":"","skin_tone_and_texture":""},{"name":"lens flare","description":"A bright, star-shaped burst of light created by sunlight passing through the water in the bottle.","location":"center, on the table surface","relative_size":"medium","shape_and_color":"Star-shaped; bright white and golden yellow","texture":"ethereal, glowing","appearance_details":"The flare has multiple sharp rays extending outwards from the point where the bottle meets the table.","relationship":"Originates from the interaction of sunlight and the water inside the glass bottle.","orientation":"radiating","pose":"","expression":"","clothing":"","gender":"","skin_tone_and_texture":""},{"name":"blurred foliage","description":"A soft-focus background consisting of green leaves and bright light spots.","location":"top half of the frame","relative_size":"large","shape_and_color":"Amorphous shapes; various shades of green and bright white","texture":"soft, blurry","appearance_details":"The background is heavily out of focus, creating a bokeh effect that emphasizes the sharpness of the bottle in the foreground.","relationship":"Provides a natural, outdoor context for the scene.","orientation":"upright","pose":"","expression":"","clothing":"","gender":"","skin_tone_and_texture":"","is_cluster":true,"number_of_objects":"numerous"}]}
examples/t2v_bubbles.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ {"comprehensive_description":{"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.","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."},"camera_info":{"color":"Saturated","frame_size":"Medium Close Up","shot_type_angle":"Low angle","lens_size":"Long Lens","composition":"Center","lighting":"Hard light","lighting_type":"Daylight"},"world_knowledge":[],"prominent_elements":[{"name":"young child","description":"A young child with short brown hair and a joyful expression, focused on playing with bubbles.","actions":[{"timestamp":"[0.0s - 1.5s]","action":"Dips the bubble wand into the solution bottle and lifts it out."},{"timestamp":"[1.5s - 2.5s]","action":"Brings the wand to their lips and blows to create bubbles."},{"timestamp":"[2.5s - 5.0s]","action":"Watches the bubbles float away, smiling slightly."}],"location":"Center of the frame","relative_size":"dominant","shape_and_color":"Human form; wearing a multi-colored striped shirt (red, yellow, green, blue).","texture":"Soft skin, fine hair, fabric texture of the shirt.","appearance_details":"Short brown hair, bright eyes, colorful horizontal stripes on the shirt.","relationship":"Holding the bubble wand and bottle, interacting with the bubbles.","orientation":"Facing forward and slightly to the right.","pose":"Standing or sitting upright, arms raised to hold the bubble wand.","expression":"Focused and happy.","clothing":"A short-sleeved shirt with horizontal stripes in red, yellow, green, and blue.","gender":"Male","skin_tone_and_texture":"Fair skin with a smooth texture."},{"name":"bubble wand and bottle","description":"A small plastic bottle containing bubble solution and a wand with a circular loop.","actions":[{"timestamp":"[0.0s - 1.5s]","action":"The wand is dipped into the bottle and then lifted out."},{"timestamp":"[1.5s - 2.5s]","action":"The wand is held at the child's mouth as bubbles are blown."}],"location":"Lower center of the frame, held by the child.","relative_size":"small","shape_and_color":"Cylindrical bottle with a blue cap; circular wand loop.","texture":"Smooth plastic.","appearance_details":"The bottle is partially filled with clear liquid; the wand has a thin handle.","relationship":"Held by the child's hands.","orientation":"Vertical bottle, horizontal wand loop.","pose":"","expression":"","clothing":"","gender":"","skin_tone_and_texture":""},{"name":"bubbles","description":"A cluster of small, iridescent, translucent bubbles floating in the air.","actions":[{"timestamp":"[1.5s - 5.0s]","action":"The bubbles are blown from the wand and float upwards and towards the right side of the frame."}],"location":"Upper center and right side of the frame.","relative_size":"medium","shape_and_color":"Spherical and iridescent with rainbow-like reflections.","texture":"Glossy and translucent.","appearance_details":"Shimmering surfaces that catch the sunlight.","relationship":"Created by the child's blowing action.","orientation":"Floating in various directions.","pose":"","expression":"","clothing":"","gender":"","skin_tone_and_texture":"","is_cluster":true,"number_of_objects":"many"}]}
examples/t2v_robot_desk.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ {"comprehensive_description":{"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.","camera_movement_description":""},"camera_info":{"color":"Cyan","frame_size":"Wide","shot_type_angle":"High angle","lens_size":"Ultra Wide / Fisheye","composition":"Balanced","lighting":"Soft light","lighting_type":"Artificial light"},"world_knowledge":[],"prominent_elements":[{"name":"right robotic arm","description":"A mechanical arm with a black and silver body, equipped with a two-fingered gripper and visible wiring.","actions":[{"timestamp":"[0.0s - 1.5s]","action":"Moves forward and to the left towards the game controller."},{"timestamp":"[1.5s - 2.5s]","action":"Grasps the game controller and lifts it upward."},{"timestamp":"[2.5s - 4.0s]","action":"Moves to the right, carrying the game controller over the open box."},{"timestamp":"[4.0s - 5.0s]","action":"Lowers the game controller into the box and releases it."}],"location":"Originates from the bottom right, moves to the center, then to the right.","relative_size":"large","shape_and_color":"Cylindrical and angular segments, black and silver.","texture":"Metallic and matte plastic.","appearance_details":"Visible joints, wiring, and a two-fingered gripper mechanism.","relationship":"Interacts directly with the game controller.","orientation":"Extends forward and slightly upward from the bottom right.","pose":"","expression":"","clothing":"","is_cluster":false,"number_of_objects":""},{"name":"left robotic arm","description":"A mechanical arm with a black and silver body, equipped with a two-fingered gripper.","actions":[{"timestamp":"[0.0s - 5.0s]","action":"Remains stationary."}],"location":"Bottom left corner of the frame.","relative_size":"large","shape_and_color":"Cylindrical and angular segments, black and silver.","texture":"Metallic and matte plastic.","appearance_details":"Visible joints and a two-fingered gripper mechanism.","relationship":"Positioned opposite the right robotic arm, not interacting with other objects.","orientation":"Extends forward and slightly upward from the bottom left.","pose":"","expression":"","clothing":"","is_cluster":false,"number_of_objects":""},{"name":"game controller","description":"A standard video game controller with joysticks, buttons, and a glowing light bar.","actions":[{"timestamp":"[0.0s - 1.5s]","action":"Rests stationary on the desk."},{"timestamp":"[1.5s - 2.5s]","action":"Is grasped and lifted upward by the right robotic arm."},{"timestamp":"[2.5s - 4.0s]","action":"Is moved to the right by the right robotic arm."},{"timestamp":"[4.0s - 5.0s]","action":"Is lowered into the open box and released."}],"location":"Initially on the left side of the desk, moved to the right side inside the box.","relative_size":"medium","shape_and_color":"Contoured shape, black with a blue light bar.","texture":"Matte plastic.","appearance_details":"Two joysticks, directional pad, action buttons, and a glowing blue light bar in the center.","relationship":"Initially on the desk, then grasped and moved by the right robotic arm, finally placed in the open box.","orientation":"Horizontal on the desk, then tilted while being carried.","pose":"","expression":"","clothing":"","is_cluster":false,"number_of_objects":""},{"name":"headphones","description":"A pair of over-ear headphones with a headband and ear cups.","actions":[{"timestamp":"[0.0s - 5.0s]","action":"Remains stationary on the desk."}],"location":"Center of the desk.","relative_size":"medium","shape_and_color":"Curved headband with circular ear cups, black.","texture":"Matte plastic and soft ear cushions.","appearance_details":"Visible ear cushions and a headband.","relationship":"Rests on the desk, untouched by the robotic arms.","orientation":"Lying flat on the desk.","pose":"","expression":"","clothing":"","is_cluster":false,"number_of_objects":""},{"name":"open box","description":"A rectangular box with the lid open, revealing a white interior.","actions":[{"timestamp":"[0.0s - 5.0s]","action":"Remains stationary on the desk."}],"location":"Right side of the desk.","relative_size":"medium","shape_and_color":"Rectangular, black exterior with a white interior.","texture":"Smooth cardboard or plastic.","appearance_details":"Open lid, empty interior.","relationship":"Serves as the receptacle for the game controller.","orientation":"Horizontal on the desk.","pose":"","expression":"","clothing":"","is_cluster":false,"number_of_objects":""}]}
examples/ti2v_1.png ADDED

Git LFS Details

  • SHA256: 6a318da1396f410fb1527e9d673e83db13987af88856bfc4e8a18323feee7063
  • Pointer size: 132 Bytes
  • Size of remote file: 1.9 MB
examples/ti2v_1.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ {"comprehensive_description":{"scene_content_description":"A high-end, cinematic still-life advertisement featuring a thick-bottomed, transparent whiskey glass resting on a dark wooden table. The glass contains amber-colored liquid, and a single transparent ice cube is suspended in mid-air directly above the rim, poised to fall. The background is dark and out of focus, creating a shallow depth of field that isolates the subject. The atmosphere is sophisticated, moody, and luxurious, emphasizing the clarity of the glass and the rich color of the whiskey.","camera_movement_description":"The camera remains in a stable close-up perspective throughout the duration. There is no significant panning, tilting, or zooming. Instead, the camera exhibits a very subtle, smooth, slow-motion breathing effect, maintaining a steady focus on the glass and the falling ice."},"camera_info":{"color":"Warm","frame_size":"Close Up","shot_type_angle":"Eye level","lens_size":"Macro","composition":"Center","lighting":"Soft light","lighting_type":"Studio lighting"},"world_knowledge":[],"prominent_elements":[{"name":"transparent ice cube","description":"A single, clear, roughly cubic block of ice","actions":[{"timestamp":"[0.0s - 0.8s]","action":"falls from above the rim, rotating slightly as it descends"},{"timestamp":"[0.8s - 1.6s]","action":"strikes the surface of the liquid, creating a circular depression and initial splash"},{"timestamp":"[1.6s - 2.8s]","action":"partially submerges, generating splashes and bubbles"},{"timestamp":"[2.8s - 4.0s]","action":"bobs slightly up and down and rotates within the liquid"},{"timestamp":"[4.0s - 5.0s]","action":"floats gently in the liquid, slowly coming to a stop"}],"location":"initially in the upper center of the frame, then moves into the center of the glass","relative_size":"medium","shape_and_color":"geometric cube, transparent with internal refractions","texture":"crystalline and solid","appearance_details":"visible internal air bubbles, cracks, and sharp edges that catch the light","relationship":"the dynamic object interacting with the liquid","orientation":"slightly tilted","pose":"","expression":"","clothing":"","gender":"","skin_tone_and_texture":""},{"name":"whiskey glass","description":"A heavy, short tumbler with a thick, textured base","actions":[{"timestamp":"[0.0s - 5.0s]","action":""}],"location":"center of the frame, resting on the table","relative_size":"large","shape_and_color":"cylindrical glass, transparent","texture":"smooth and reflective","appearance_details":"thick, grid-patterned base and clear walls that refract the amber liquid inside","relationship":"the container for the liquid and ice","orientation":"upright","pose":"","expression":"","clothing":"","gender":"","skin_tone_and_texture":""},{"name":"amber liquid","description":"A rich, golden-brown fluid filling the lower half of the glass","actions":[{"timestamp":"[0.0s - 0.8s]","action":"remains relatively still with a flat surface"},{"timestamp":"[0.8s - 1.6s]","action":"surface is pushed down by the ice, forming a circular wave"},{"timestamp":"[1.6s - 2.8s]","action":"creates splashes and fine droplets as the ice enters"},{"timestamp":"[2.8s - 4.0s]","action":"exhibits ripples that bounce off the glass walls and cross-ripples"},{"timestamp":"[4.0s - 5.0s]","action":"surface gradually calms down, becoming smooth again"}],"location":"inside the glass","relative_size":"large","shape_and_color":"fluid volume, deep amber","texture":"viscous and glossy","appearance_details":"surface tension and the way light passes through it","relationship":"contained within the glass","orientation":"horizontal","pose":"","expression":"","clothing":"","gender":"","skin_tone_and_texture":""},{"name":"dark wooden table","description":"A polished, dark wood surface","actions":[{"timestamp":"[0.0s - 5.0s]","action":""}],"location":"foreground and bottom of the frame","relative_size":"large","shape_and_color":"flat surface, dark brown/black","texture":"smooth with a slight sheen","appearance_details":"wood grain and reflections of the glass and light","relationship":"the supporting surface for the glass","orientation":"horizontal","pose":"","expression":"","clothing":"","gender":"","skin_tone_and_texture":""}]}
examples/ti2v_5.png ADDED

Git LFS Details

  • SHA256: dcae94869f4bf6faf31a6b6e72a9d1f84d23b41ebef6ca292aaeb08d583ae7b2
  • Pointer size: 132 Bytes
  • Size of remote file: 1.8 MB
examples/ti2v_5.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ {"comprehensive_description":{"scene_content_description":"A sleek, vibrant red sports car is captured in a high-speed motion sequence. The car begins by emerging from a dark, shadowed cave entrance, its glossy surface reflecting the intense light of the desert environment. As it accelerates, it kicks up a significant amount of dust and gravel from the sandy ground. The car then bursts into the bright, sun-drenched exterior, where the background reveals a vast, arid landscape with distant mountains under a clear sky. The lighting is dramatic, with high-contrast shadows inside the cave and brilliant, almost blinding sunlight outside, creating a sense of power and speed.","camera_movement_description":"The camera performs a dynamic tracking shot, moving parallel to the car's side. It starts with a close-up side profile and gradually pulls back and pans to the right, following the car's trajectory. As the car exits the cave, the camera angle shifts slightly upward and pulls back further to capture the car's front and side as it speeds into the open desert. The movement is smooth and fluid, maintaining a consistent distance from the subject."},"camera_info":{"color":"Warm","frame_size":"Wide","shot_type_angle":"Low angle","lens_size":"Wide","composition":"Balanced","lighting":"Hard light","lighting_type":"Daylight"},"world_knowledge":[],"prominent_elements":[{"name":"red sports car","description":"A modern, high-performance coupe with a glossy red finish and black accents","actions":[{"timestamp":"[0.0s - 4.0s]","action":"moves right while emerging from a dark cave"},{"timestamp":"[4.0s - 6.0s]","action":"accelerates and exits the cave into bright sunlight"},{"timestamp":"[6.0s - 8.0s]","action":"speeds right across the desert landscape"},{"timestamp":"[8.0s - 10.0s]","action":"continues moving right with the camera tracking its side"}],"location":"center of the frame, moving from left to right","relative_size":"dominant","shape_and_color":"aerodynamic coupe shape, vibrant red with black wheels and trim","texture":"glossy and metallic","appearance_details":"features a low-slung profile, large black multi-spoke wheels, and a visible driver inside","relationship":"the main subject of the video, interacting with the cave and desert environment","orientation":"facing right","pose":"","expression":"","clothing":"","gender":"","skin_tone_and_texture":""},{"name":"cave entrance","description":"A dark, rocky opening in a cliff face","actions":[{"timestamp":"[0.0s - 4.0s]","action":""}],"location":"left side of the frame","relative_size":"large","shape_and_color":"irregular rocky shape, dark brown and black","texture":"rough and rocky","appearance_details":"deep shadows inside the opening, contrasting with the bright exterior","relationship":"the starting point for the car's movement","orientation":"upright","pose":"","expression":"","clothing":"","gender":"","skin_tone_and_texture":""},{"name":"desert landscape","description":"A vast, arid environment with sandy ground and distant mountains","actions":[{"timestamp":"[0.0s - 10.0s]","action":""}],"location":"background and foreground","relative_size":"large","shape_and_color":"sandy beige ground, light blue sky, and brown mountains","texture":"sandy and dusty","appearance_details":"bright sunlight creates a hazy, high-contrast atmosphere","relationship":"the setting for the car's high-speed run","orientation":"horizontal","pose":"","expression":"","clothing":"","gender":"","skin_tone_and_texture":""}]}
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/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/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/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
+ spaces>=0.41
2
+ diffusers==0.39.0
3
+ transformers==5.8.1
4
+ accelerate>=1.10
5
+ safetensors>=0.4.5
6
+ torchvision
7
+ imageio>=2.35.0
8
+ imageio-ffmpeg>=0.5.1
9
+ numpy>=1.26
10
+ pillow>=10.4.0
11
+ scipy>=1.11