cuio commited on
Commit
04e9f89
·
verified ·
1 Parent(s): adc74dd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +422 -397
app.py CHANGED
@@ -12,63 +12,50 @@
12
  # See the License for the specific language governing permissions and
13
  # limitations under the License.
14
  # ------------------------------------------------------------------------
15
- """URSA TI2V application —— 纯 CPU + 4bit (bitsandbytes NF4) 版本.
16
-
17
- 相对 ZeroGPU 版的改动:
18
- 1. 去掉 CUDA / ZeroGPU 依赖,全部在 CPU 上跑(spaces.GPU 变 no-op)。
19
- 2. 权重量化:transformer 里的 nn.Linear 换成 bitsandbytes Linear4bit (NF4)。
20
- 3. CPU 上没有 flex_attention,把 FlexAttentionCausal2D 改写成 SDPA 实现。
21
- 4. 默认分辨率 / 帧数 / 步数降到 CPU 能承受的档位。
22
- """
23
 
24
  # =========================
25
- # ✅ 第 0 步:环境变量(必须早于 import torch,线程数只能在这里设)
26
  # =========================
27
  import os
28
 
29
- _THREADS = str(os.cpu_count() or 4)
30
- os.environ.setdefault("OMP_NUM_THREADS", _THREADS)
31
- os.environ.setdefault("MKL_NUM_THREADS", _THREADS)
32
- # 纯 CPU 模式:想用 GPU 就自行 export CUDA_VISIBLE_DEVICES=0
33
- os.environ.setdefault("CUDA_VISIBLE_DEVICES", "")
34
- # torch.compile 在 CPU 上要走 inductor,URSA 只用它编译 RoPE / flex_attention,收益为负
35
- os.environ["TORCH_COMPILE_DISABLE"] = "1"
36
- os.environ["TOKENIZERS_PARALLELISM"] = "true"
37
  if os.environ.get("HF_TOKEN"):
38
  os.environ.setdefault("HUGGINGFACE_HUB_TOKEN", os.environ["HF_TOKEN"])
39
  os.environ["HF_HUB_ENABLE_HUGGINGFACE_TRANSFER"] = "1"
40
  os.environ["HF_HUB_DOWNLOAD_TIMEOUT"] = "300"
 
 
 
 
 
41
 
42
- import argparse # noqa: E402
43
- import shutil # noqa: E402
44
- import time # noqa: E402
45
-
46
- import numpy as np # noqa: E402
47
- import PIL.Image # noqa: E402
48
- import torch # noqa: E402
49
- import torch.nn.functional as F # noqa: E402
50
-
51
- torch.set_num_threads(int(os.environ.get("OMP_NUM_THREADS", _THREADS)))
52
- try:
53
- CPU_CAP = torch.backends.cpu.get_cpu_capability()
54
- except Exception: # 老版本 torch 没有这个 API
55
- CPU_CAP = "UNKNOWN"
56
-
57
-
58
- def cpu_supports_bf16() -> bool:
59
- """CPU 是否能比较体面地跑 bf16(AVX512-BF16 / AMX)。"""
60
- try: # bitsandbytes 的判断最准,优先用
61
- from bitsandbytes.functional import has_avx512bf16
62
 
63
- return bool(has_avx512bf16())
64
- except Exception:
65
- return CPU_CAP == "AVX512"
 
66
 
 
 
 
 
67
 
68
  # =========================
69
- # ✅ 第 1 步:把 torch.compile 变成恒等变换
70
- # diffnext 在【模块导入时】就用 @torch.compile 装饰了 RoPE 实现,
71
- # CPU 上 inductor 只会更慢(且首次编译会卡几十秒)。
 
 
 
 
 
 
 
72
  # =========================
73
  try:
74
  torch._dynamo.config.disable = True
@@ -78,7 +65,9 @@ except Exception:
78
 
79
  def _eager_compile(model=None, **kwargs):
80
  """@torch.compile(...) / torch.compile(model) 都直接返回原对象。"""
81
- return model if model is not None else (lambda fn: fn)
 
 
82
 
83
 
84
  torch.compile = _eager_compile
@@ -88,7 +77,8 @@ from diffnext.utils import export_to_image, export_to_video # noqa: E402
88
 
89
 
90
  # =========================
91
- # 🔧 第 2 步:兜底 —— 把被编译的 RoPE 方法换成纯 eager 实现
 
92
  # =========================
93
  def _patch_rope_eager():
94
  from diffnext.models import embeddings as emb
@@ -115,16 +105,23 @@ def _patch_rope_eager():
115
  return patched
116
 
117
 
 
 
 
 
 
 
 
 
 
118
  # =========================
119
- # 🔧 第 3 步:修复 Qwen3Config 缺失 rope_theta
120
  # =========================
121
  def _patch_diffnext_qwen3():
122
  try:
123
  from diffnext.models.text_encoders import qwen3
124
  except Exception:
125
  return
126
- if not hasattr(qwen3, "Qwen3RotaryEmbedding"):
127
- return
128
 
129
  orig_from_config = qwen3.Qwen3RotaryEmbedding.from_config
130
 
@@ -132,7 +129,10 @@ def _patch_diffnext_qwen3():
132
  def patched_from_config(cls, config):
133
  if not hasattr(config, "rope_theta"):
134
  rp = getattr(config, "rope_parameters", None)
135
- config.rope_theta = rp["rope_theta"] if isinstance(rp, dict) and "rope_theta" in rp else 1e6
 
 
 
136
  if not hasattr(config, "max_position_embeddings"):
137
  config.max_position_embeddings = int(getattr(config, "max_seq_len", 2048))
138
  return orig_from_config(config)
@@ -140,362 +140,356 @@ def _patch_diffnext_qwen3():
140
  qwen3.Qwen3RotaryEmbedding.from_config = patched_from_config
141
 
142
 
143
- # =========================
144
- # 🔧 第 4 步(CPU 关键):flex_attention 在 CPU 上不可用,换成 SDPA
145
- # Qwen3SdpaAttention 里是这么分支的:
146
- # if self.flex_attn and self.flex_attn.offsets: -> flex_attention
147
- # else: -> F.scaled_dot_product_attention
148
- # 而推理 pipeline 从不调用 set_offsets(),所以正常就走 SDPA 分支。
149
- # 这里仍然重写 forward,保证万一 offsets 被设置也不会掉进 CPU 不支持的
150
- # flex_attention(此时用一张物化出来的 bool mask 表达 2D 因果图)。
151
- # =========================
152
- _HAS_GQA = tuple(int(x) for x in torch.__version__.split("+")[0].split(".")[:2]) >= (2, 5)
153
-
154
-
155
- def _sdpa(q, k, v, attn_mask=None):
156
- if _HAS_GQA:
157
- return F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask, enable_gqa=True)
158
- n_rep = q.shape[1] // k.shape[1] # 老 torch:手动展开 GQA
159
- k, v = k.repeat_interleave(n_rep, dim=1), v.repeat_interleave(n_rep, dim=1)
160
- return F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask)
161
-
162
-
163
- def _patch_flex_attn_sdpa():
164
- from diffnext.models.flex_attention import FlexAttentionCausal2D
165
-
166
- def _build_mask(self, q):
167
- """把 2D 因果规则物化成 [1, 1, T, T] 的 bool mask(只在 offsets 非空时用)。"""
168
- offsets = torch.as_tensor(self.offsets, device=q.device)
169
- flags = list(self.flags or [])
170
- key = (tuple(self.offsets), tuple(flags), q.shape[-2], str(q.device))
171
- if getattr(self, "_mask_key", None) == key:
172
- return self._mask_cache
173
- counts = offsets[1:] - offsets[:-1]
174
- ids = torch.arange(len(counts), device=q.device).repeat_interleave(counts)
175
- flags = torch.as_tensor(flags + [-1] * (len(counts) - len(flags)), device=q.device)
176
- flags = flags.repeat_interleave(counts)
177
- idx = torch.arange(ids.numel(), device=q.device)
178
- # (qi >= ki) | (ids[qi] * flags[qi] == ids[ki])
179
- mask = (idx[:, None] >= idx[None, :]) | ((ids[:, None] * flags[:, None]) == ids[None, :])
180
- self._mask_key, self._mask_cache = key, mask[None, None]
181
- return self._mask_cache
182
-
183
- def forward(self, q, k, v):
184
- if not self.offsets: # 推理默认路径:全双向 SDPA
185
- return _sdpa(q, k, v)
186
- return _sdpa(q, k, v, attn_mask=_build_mask(self, q))
187
-
188
- FlexAttentionCausal2D.forward = forward
189
- return True
190
 
191
 
192
- try:
193
- _ROPE_PATCHED = _patch_rope_eager()
194
- except Exception as e:
195
- _ROPE_PATCHED = ["FAILED: %r" % (e,)]
196
- _patch_diffnext_qwen3()
197
- try:
198
- _FLEX_PATCHED = _patch_flex_attn_sdpa()
199
- except Exception as e:
200
- _FLEX_PATCHED = "FAILED: %r" % (e,)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
- print(f"[patch] torch.compile -> identity : {torch.compile is _eager_compile}", flush=True)
203
- print(f"[patch] rope eager : {_ROPE_PATCHED}", flush=True)
204
- print(f"[patch] flex_attention -> sdpa : {_FLEX_PATCHED}", flush=True)
205
- print(f"[env ] cpu={CPU_CAP} threads={torch.get_num_threads()} bf16_fast={cpu_supports_bf16()}", flush=True)
 
 
 
 
 
 
 
 
 
206
 
207
 
208
  # =========================
209
- # 🔧 第 5 步:4bit 量化(bitsandbytes NF4)
210
- # 注意:URSA 的 logits 是用 nn.functional.linear(x, self.lm_head.weight) 算的,
211
- # 直接吃 weight 张量,所以 lm_head 绝对不能换成 Linear4bit,否则会炸。
212
  # =========================
213
- def _linear_targets(root, skip_last_names=("lm_head",)):
214
- """收集需要量化的 nn.Linear(按名字最后一节跳过 lm_head 等)。"""
215
- out = []
216
- for name, mod in root.named_modules():
217
- if isinstance(mod, torch.nn.Linear) and name.split(".")[-1] not in skip_last_names:
218
- out.append((name, mod))
219
- return out
220
-
221
-
222
- def quantize_4bit(root, compute_dtype, quant_type="nf4", blocksize=64, skip_last_names=("lm_head",)):
223
- """把 root 下的 nn.Linear 就地换成 bnb Linear4bit,返回 (层数, 原始字节, 量化后字节)。"""
224
- import bitsandbytes as bnb
225
- from bitsandbytes.functional import quantize_4bit as _q4
226
- from bitsandbytes.nn import Linear4bit, Params4bit
227
-
228
- targets = _linear_targets(root, skip_last_names)
229
- n, raw_bytes, q_bytes = 0, 0, 0
230
- for name, mod in targets:
231
- if isinstance(mod, Linear4bit):
232
- continue
233
- parent = root.get_submodule(name.rsplit(".", 1)[0]) if "." in name else root
234
- leaf = name.rsplit(".", 1)[-1]
235
-
236
- w = mod.weight.detach()
237
- if w.dtype not in (torch.float32, torch.bfloat16, torch.float16):
238
- continue
239
- raw_bytes += w.numel() * 4
240
- qw, qs = _q4(
241
- w.to(torch.float32).contiguous(),
242
- blocksize=blocksize,
243
- compress_statistics=True,
244
- quant_type=quant_type,
245
- )
246
- new = Linear4bit(
247
- mod.in_features,
248
- mod.out_features,
249
- bias=mod.bias is not None,
250
- compute_dtype=compute_dtype,
251
- compress_statistics=True,
252
- quant_type=quant_type,
253
- device=qw.device,
254
- )
255
- new.weight = Params4bit(
256
- qw,
257
- requires_grad=False,
258
- quant_state=qs,
259
- blocksize=blocksize,
260
- compress_statistics=True,
261
- quant_type=quant_type,
262
- quant_storage=qw.dtype,
263
- module=new,
264
- bnb_quantized=True,
265
- )
266
- if mod.bias is not None:
267
- new.bias.data.copy_(mod.bias.detach())
268
- setattr(parent, leaf, new)
269
- q_bytes += qw.numel() * qw.element_size() + (qs.absmax.numel() * qs.absmax.element_size())
270
- n += 1
271
- del targets
272
- return n, raw_bytes, q_bytes
273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
- def quantize_int8_dynamic(root):
276
- """bnb 不可用时的退路:PyTorch 原生 int8 动态量化(CPU 上有 fbgemm/onednn kernel)。
 
 
 
277
 
278
- 注意:动态量化要求 fp32 模型,且 lm_head 会被 nn.functional.linear 直接用,
279
- 必须先从模块树里真正摘掉再装回。
280
- """
281
- import torch.ao.quantization as tq
282
-
283
- head = root._modules.pop("lm_head", None) # 真摘除(赋 None 不会从 _modules 移除)
284
- try:
285
- tq.quantize_dynamic(root, {torch.nn.Linear}, dtype=torch.qint8)
286
- finally:
287
- if head is not None:
288
- root.lm_head = head
289
-
290
-
291
- def self_test_4bit():
292
- """不开模型,先验证 CPU 上的 4bit 通路确实可用。"""
293
- import bitsandbytes as bnb
294
- from bitsandbytes.nn import Linear4bit
295
-
296
- print(f"[test ] bitsandbytes {bnb.__version__}", flush=True)
297
- torch.manual_seed(0)
298
- m, n, k = 512, 512, 64
299
- ref = torch.nn.Linear(k, n, bias=False).eval()
300
- q = Linear4bit(k, n, bias=False, compute_dtype=torch.float32, quant_type="nf4", device="cpu")
301
- from bitsandbytes.functional import quantize_4bit as _q4
302
-
303
- qw, qs = _q4(ref.weight.detach().contiguous(), compress_statistics=True, quant_type="nf4")
304
- from bitsandbytes.nn import Params4bit
305
-
306
- q.weight = Params4bit(qw, requires_grad=False, quant_state=qs, quant_type="nf4",
307
- quant_storage=qw.dtype, module=q, bnb_quantized=True)
308
- x = torch.randn(m, k)
309
- with torch.no_grad():
310
- t0, out = time.time(), q(x)
311
- t_q = time.time() - t0
312
- t0 = time.time()
313
- ref_out = ref(x)
314
- t_fp = time.time() - t0
315
- err = (out - ref_out).abs().mean().item()
316
- print(f"[test ] nf4 err(mean|Δ|)={err:.4f} 4bit {t_q * 1e3:.1f}ms vs fp32 {t_fp * 1e3:.1f}ms "
317
- f"({t_fp / max(t_q, 1e-9):.2f}x)", flush=True)
318
- return err
319
 
320
 
321
  # =========================
322
- # ✅ 第 6 步:参数与模型加载
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  # =========================
324
- def parse_args():
325
- p = argparse.ArgumentParser(description="URSA TI2V on CPU with 4-bit weights")
326
- p.add_argument("--model", default="BAAI/URSA-0.6B-FSQ320")
327
- p.add_argument("--quant", default="nf4", choices=["nf4", "fp4", "int8", "none"])
328
- p.add_argument("--load-dtype", default="auto", choices=["auto", "bfloat16", "float32"])
329
- p.add_argument("--compute-dtype", default="auto", choices=["auto", "bfloat16", "float32"])
330
- p.add_argument("--quant-vae", action="store_true", help="连 VAE 里的 Linear 一起量化(画质会掉)")
331
- p.add_argument("--threads", type=int, default=0)
332
- p.add_argument("--self-test", action="store_true", help="只验证 CPU 4bit 通路,不加载模型")
333
- # 生成参数(CLI 模式 / UI 默认值)
334
- p.add_argument("--width", type=int, default=256)
335
- p.add_argument("--height", type=int, default=160)
336
- p.add_argument("--num-frames", type=int, default=9)
337
- p.add_argument("--steps", type=int, default=10)
338
- p.add_argument("--guidance", type=float, default=7.0)
339
- p.add_argument("--motion", type=float, default=9.0)
340
- p.add_argument("--seed", type=int, default=0)
341
- p.add_argument("--no-randomize", action="store_true")
342
- p.add_argument("--prompt", default="")
343
- p.add_argument("--negative-prompt", default="worst quality, low quality, inconsistent motion, blurry, jittery")
344
- p.add_argument("--image", default="", help="可选:图生视频的首帧图片路径")
345
- p.add_argument("--out", default="ursa_out.mp4")
346
- p.add_argument("--cli", action="store_true", help="不启动 Gradio,直接生成一次并保存")
347
- p.add_argument("--share", action="store_true")
348
- p.add_argument("--server-port", type=int, default=7860)
349
- return p.parse_args()
350
 
351
 
352
- args = parse_args()
353
- if args.threads > 0:
354
- torch.set_num_threads(args.threads)
355
-
356
- if args.self_test:
357
- self_test_4bit()
358
- raise SystemExit(0)
359
-
360
- if args.load_dtype == "auto":
361
- args.load_dtype = "bfloat16" if cpu_supports_bf16() else "float32"
362
- if args.compute_dtype == "auto":
363
- args.compute_dtype = args.load_dtype
364
- load_dtype = getattr(torch, args.load_dtype)
365
- compute_dtype = getattr(torch, args.compute_dtype)
366
-
367
- DEVICE = torch.device("cpu")
368
- print(f"[load ] model={args.model} quant={args.quant} load_dtype={args.load_dtype} "
369
- f"compute_dtype={args.compute_dtype}", flush=True)
370
-
371
- pipe = URSAPipeline.from_pretrained(args.model, torch_dtype=load_dtype, trust_remote_code=True)
372
- pipe.to(DEVICE)
373
- # 推理路径不会设置 offsets,这里显式清一次,确保走 SDPA 而不是 flex_attention
374
- try:
375
- pipe.transformer.model.flex_attn.offsets = None
376
- except Exception:
377
- pass
378
-
379
- # 量化
380
- if args.quant in ("nf4", "fp4"):
381
- try:
382
- n, raw_b, q_b = quantize_4bit(pipe.transformer, compute_dtype, quant_type=args.quant)
383
- print(f"[quant] transformer: {n} Linear -> {args.quant.upper()} "
384
- f"({raw_b / 2**20:.0f}MiB -> {q_b / 2**20:.0f}MiB)", flush=True)
385
- if args.quant_vae and hasattr(pipe, "vae"):
386
- n2, _, _ = quantize_4bit(pipe.vae, compute_dtype, quant_type=args.quant)
387
- print(f"[quant] vae: {n2} Linear -> {args.quant.upper()}", flush=True)
388
- except Exception as e: # bnb 不可用就退回 int8 / 不量化
389
- print(f"[quant] bitsandbytes 4bit 失败({e!r}),回退 int8 动态量化", flush=True)
390
- args.quant = "int8"
391
- if args.quant == "int8":
392
  try:
393
- if args.load_dtype != "float32": # 动态量化只吃 fp32
394
- print("[quant] int8 动态量化需要 fp32,先把 transformer 转成 float32", flush=True)
395
- pipe.transformer.float()
396
- quantize_int8_dynamic(pipe.transformer)
397
- print("[quant] transformer: int8 dynamic", flush=True)
398
  except Exception as e:
399
- print(f"[quant] int8 也失败({e!r}),以 {args.load_dtype} 直接跑", flush=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
400
 
401
- n_param = sum(p.numel() for p in pipe.transformer.parameters())
402
- print(f"[load ] transformer params={n_param / 1e6:.1f}M temporal_stride={pipe.vae_temporal_stride} "
403
- f"spatial_stride={pipe.vae_spatial_stride}", flush=True)
404
 
 
405
 
406
- def latent_tokens(h, w, num_frames):
407
- return ((num_frames - 1) // pipe.vae_temporal_stride + 1) * (h // pipe.vae_spatial_stride) * (w // pipe.vae_spatial_stride)
408
 
 
 
 
409
 
410
- # =========================
411
- # ✅ 第 7 步:推理(无 @spaces.GPU,CPU 上就是普通函数)
412
- # =========================
413
- def crop_image(image, target_h, target_w):
414
- h, w = image.height, image.width
415
- if w / h > target_w / target_h:
416
- new_w = int(h * target_w / target_h)
417
- image = image.crop(((w - new_w) // 2, 0, (w - new_w) // 2 + new_w, h))
418
- else:
419
- new_h = int(w * target_h / target_w)
420
- image = image.crop((0, (h - new_h) // 2, w, (h - new_h) // 2 + new_h))
421
- return np.array(image.resize((target_w, target_h), PIL.Image.Resampling.BILINEAR))
422
 
 
 
 
 
 
 
 
 
 
 
 
 
423
 
424
- def generate_image(prompt, negative_prompt, seed, randomize_seed, guidance_scale,
425
- size="256x160", num_inference_steps=12):
426
- t0 = time.time()
427
- w, h = (int(x) for x in size.split("x"))
428
- seed = int(np.random.randint(2147483647)) if randomize_seed else int(seed)
429
- generator = torch.Generator(device=DEVICE).manual_seed(seed)
430
- with torch.no_grad():
431
- images = pipe(
432
- prompt=prompt,
433
- negative_prompt=negative_prompt,
434
- height=h,
435
- width=w,
436
- num_frames=1,
437
- num_inference_steps=int(num_inference_steps),
438
- guidance_scale=guidance_scale,
439
- generator=generator,
440
- output_type="np",
441
- ).frames
442
- print(f"[t2i ] {time.time() - t0:.1f}s tokens={latent_tokens(h, w, 1)}", flush=True)
443
- return [export_to_image(image, quality=95) for image in images] + [seed]
444
 
 
 
 
445
 
446
- def generate_video(prompt, negative_prompt, image, motion_score, seed, randomize_seed,
447
- guidance_scale, num_inference_steps, size="256x160", num_frames=9,
448
- output_type="np"):
449
- t0 = time.time()
450
- w, h = (int(x) for x in size.split("x"))
451
- num_frames, num_inference_steps = int(num_frames), int(num_inference_steps)
452
- seed = int(np.random.randint(2147483647)) if randomize_seed else int(seed)
453
- generator = torch.Generator(device=DEVICE).manual_seed(seed)
454
- with torch.no_grad():
455
- frames = pipe(
456
- prompt=f"motion={float(motion_score):.1f}, {prompt}",
457
- negative_prompt=negative_prompt,
458
- image=crop_image(image, h, w) if image is not None else None,
459
- height=h,
460
- width=w,
461
- num_frames=num_frames,
462
- num_inference_steps=num_inference_steps,
463
- guidance_scale=guidance_scale,
464
- generator=generator,
465
- output_type=output_type,
466
- ).frames[0]
467
- dt = time.time() - t0
468
- print(f"[ti2v ] {dt:.1f}s {w}x{h}x{num_frames} steps={num_inference_steps} "
469
- f"tokens/步≈{latent_tokens(h, w, num_frames) * (2 if guidance_scale > 1 else 1)}", flush=True)
470
- return export_to_video(frames, fps=12), seed
471
 
 
 
 
 
 
 
 
 
472
 
473
- # =========================
474
- # ✅ 第 8 步:CLI 模式
475
- # =========================
476
- if args.cli or args.prompt:
477
- img = PIL.Image.open(args.image) if args.image else None
478
- path, used_seed = generate_video(
479
- args.prompt or "a lone grizzly bear walks through a misty forest at dawn.",
480
- args.negative_prompt,
481
- img,
482
- args.motion,
483
- args.seed,
484
- not args.no_randomize,
485
- args.guidance,
486
- args.steps,
487
- size=f"{args.width}x{args.height}",
488
- num_frames=args.num_frames,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
  )
490
- shutil.copy(path, args.out)
491
- print(f"[done ] seed={used_seed} -> {os.path.abspath(args.out)}", flush=True)
492
- raise SystemExit(0)
493
 
494
 
495
  # =========================
496
- # ✅ 第 9 步:Gradio UI
 
 
 
 
497
  # =========================
498
- import gradio as gr # noqa: E402
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
 
500
  css = """#col-container {margin: 0 auto; max-width: 1366px}"""
501
  header = (
@@ -503,51 +497,67 @@ header = (
503
  "<h2>Uniform Discrete Diffusion with Metric Path for Video Generation</h2>"
504
  "<h3><a href='https://arxiv.org/abs/2510.24717' target='_blank' rel='noopener'>[paper]</a>"
505
  "<a href='https://github.com/baaivision/URSA' target='_blank' rel='noopener'>[code]</a></h3>"
506
- "<p>CPU + 4bit (NF4) 推理:建议 256x160 / 9 帧 / 10 步起步,出片约数分钟。</p>"
 
 
507
  "</div>"
508
  )
509
 
510
- SIZE_CHOICES = ["256x160", "320x192", "384x256", "512x320"]
 
 
 
 
511
  prompts = [
512
  "a lone grizzly bear walks through a misty forest at dawn, sunlight catching its fur.",
513
  "Many spotted jellyfish pulsating under water. Their bodies are transparent and glowing in deep ocean.",
514
- "An intense close-up of a soldier's face, covered in dirt and sweat, his eyes filled with determination.",
515
- "a close-up shot of a woman standing in a dimly lit room, wearing a traditional red and gold chinese outfit.",
516
- "The camera slowly rotates around a massive stack of vintage televisions in a large museum gallery.",
517
  ]
518
- examples = [[p, 9] for p in prompts]
 
 
519
 
520
  if __name__ == "__main__":
 
521
  app = gr.Blocks(css=css, theme="origin").__enter__()
522
  container = gr.Column(elem_id="col-container").__enter__()
523
  _, main_row = gr.Markdown(header), gr.Row().__enter__()
524
 
 
525
  input_col = gr.Column().__enter__()
526
- prompt = gr.Text(label="Prompt", lines=5, value=prompts[0])
 
 
 
 
 
527
  negative_prompt = gr.Text(
528
- label="Negative Prompt", lines=1,
529
- value="worst quality, low quality, inconsistent motion, static, still, blurry, jittery, ugly",
 
 
530
  )
531
  with gr.Row():
532
  generate_image_btn = gr.Button("Generate Image Prompt", variant="primary", size="lg")
533
  generate_video_btn = gr.Button("Generate Video", variant="primary", size="lg")
534
- image_prompt = gr.Image(label="Image Prompt", height=320, type="pil")
535
 
536
- options = gr.Accordion("Options", open=True).__enter__()
537
- with gr.Row():
538
- size = gr.Dropdown(label="Resolution", choices=SIZE_CHOICES, value="256x160")
539
- num_frames = gr.Dropdown(label="Frames", choices=[1, 5, 9, 17, 25, 49], value=9)
540
- with gr.Row():
541
- seed = gr.Slider(label="Seed", maximum=2147483647, step=1, value=0)
542
- randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
543
  with gr.Row():
544
- guidance_scale = gr.Slider(label="Guidance scale", minimum=1, maximum=10.0, step=0.1, value=7.0)
545
- num_inference_steps = gr.Slider(label="Inference steps", minimum=2, maximum=30, step=1, value=10)
 
546
  options.__exit__(), input_col.__exit__()
547
 
 
548
  result_col = gr.Column().__enter__()
549
  motion = gr.Slider(label="Motion Score", minimum=1, maximum=10, step=1, value=9)
550
- result = gr.Video(label="Result", height=320, show_label=False, autoplay=True)
551
  result_col.__exit__(), main_row.__exit__()
552
 
553
  with gr.Row():
@@ -555,18 +565,33 @@ if __name__ == "__main__":
555
 
556
  container.__exit__()
557
  gr.on(
558
- triggers=[generate_image_btn.click],
559
  fn=generate_image,
560
- inputs=[prompt, negative_prompt, seed, randomize_seed, guidance_scale, size, num_inference_steps],
 
 
 
 
 
 
561
  outputs=[image_prompt, seed],
562
  )
563
  gr.on(
564
- triggers=[generate_video_btn.click],
565
  fn=generate_video,
566
- inputs=[prompt, negative_prompt, image_prompt, motion, seed, randomize_seed,
567
- guidance_scale, num_inference_steps, size, num_frames],
 
 
 
 
 
 
 
 
568
  outputs=[result, seed],
569
  )
 
570
  app.__exit__(), app.queue(default_concurrency_limit=1).launch(
571
- share=args.share, ssr_mode=False, max_threads=1, server_port=args.server_port
572
  )
 
12
  # See the License for the specific language governing permissions and
13
  # limitations under the License.
14
  # ------------------------------------------------------------------------
15
+ """URSA TI2V application (torchao int4 / int8 weight-only quantization)."""
 
 
 
 
 
 
 
16
 
17
  # =========================
18
+ # ✅ 第 1 步:环境变量必须最先设置(早于 torch / spaces / diffnext)
19
  # =========================
20
  import os
21
 
22
+ # 只在 token 存在时设置,避免把 HF_TOKEN 污染成空字符串
 
 
 
 
 
 
 
23
  if os.environ.get("HF_TOKEN"):
24
  os.environ.setdefault("HUGGINGFACE_HUB_TOKEN", os.environ["HF_TOKEN"])
25
  os.environ["HF_HUB_ENABLE_HUGGINGFACE_TRANSFER"] = "1"
26
  os.environ["HF_HUB_DOWNLOAD_TIMEOUT"] = "300"
27
+ os.environ["TOKENIZERS_PARALLELISM"] = "true"
28
+ # 必须在 CUDA 初始化 / torch 导入前设置
29
+ os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "expandable_segments:True"
30
+ # 关掉 TorchDynamo(torch 在 import 时读取该环境变量)
31
+ os.environ["TORCH_COMPILE_DISABLE"] = "1"
32
 
33
+ # =========================
34
+ # ✅ 第 2 步:import spaces 必须早于 torch(ZeroGPU 硬性要求)
35
+ # =========================
36
+ import spaces
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
+ import argparse
39
+ import re
40
+ import sys
41
+ import time
42
 
43
+ import gradio as gr
44
+ import numpy as np
45
+ import PIL.Image
46
+ import torch
47
 
48
  # =========================
49
+ # 🔧 第 3 步:把 torch.compile 变成恒等变换
50
+ # diffnext 在【模块导入时】就用 @torch.compile 装饰了 RoPE 实现:
51
+ # - FlexRotaryEmbedding.PEFunc.interleaved_impl / partitioned_impl
52
+ # - RotaryEmbed3D.PEFunc.call_impl
53
+ # - FlexAttn.get_attn_func() 运行期调用 torch.compile(flex_attention)
54
+ # 该编译是惰性的,直到首次真实推理(进入 @spaces.GPU、真卡挂载后)才真正触发,
55
+ # 此时 TorchInductor 用 CUDA event 给 Triton kernel 做 autotune 微基准,
56
+ # 在 ZeroGPU 的 fork worker 里 event 计时失效 →
57
+ # RuntimeError: Both events must be completed before calculating elapsed time
58
+ # ZeroGPU 官方明确不支持 torch.compile(每任务新起进程,JIT 无法复用)。
59
  # =========================
60
  try:
61
  torch._dynamo.config.disable = True
 
65
 
66
  def _eager_compile(model=None, **kwargs):
67
  """@torch.compile(...) / torch.compile(model) 都直接返回原对象。"""
68
+ if model is not None:
69
+ return model
70
+ return lambda fn: fn
71
 
72
 
73
  torch.compile = _eager_compile
 
77
 
78
 
79
  # =========================
80
+ # 🔧 第 4 步:兜底 —— 直接把被编译的 RoPE 方法换成纯 eager 实现
81
+ # (即使第 3 步失效,这一步也能保证不会走 Inductor)
82
  # =========================
83
  def _patch_rope_eager():
84
  from diffnext.models import embeddings as emb
 
105
  return patched
106
 
107
 
108
+ try:
109
+ _ROPE_PATCHED = _patch_rope_eager()
110
+ except Exception as e: # 不因打补丁失败而阻塞启动
111
+ _ROPE_PATCHED = ["FAILED: %r" % (e,)]
112
+
113
+ print("[patch] torch.compile -> identity:", torch.compile is _eager_compile, flush=True)
114
+ print("[patch] rope eager:", _ROPE_PATCHED, flush=True)
115
+
116
+
117
  # =========================
118
+ # 🔧 第 5 步:修复 Qwen3Config 缺失 rope_theta
119
  # =========================
120
  def _patch_diffnext_qwen3():
121
  try:
122
  from diffnext.models.text_encoders import qwen3
123
  except Exception:
124
  return
 
 
125
 
126
  orig_from_config = qwen3.Qwen3RotaryEmbedding.from_config
127
 
 
129
  def patched_from_config(cls, config):
130
  if not hasattr(config, "rope_theta"):
131
  rp = getattr(config, "rope_parameters", None)
132
+ if isinstance(rp, dict) and "rope_theta" in rp:
133
+ config.rope_theta = rp["rope_theta"]
134
+ else:
135
+ config.rope_theta = float(1e6)
136
  if not hasattr(config, "max_position_embeddings"):
137
  config.max_position_embeddings = int(getattr(config, "max_seq_len", 2048))
138
  return orig_from_config(config)
 
140
  qwen3.Qwen3RotaryEmbedding.from_config = patched_from_config
141
 
142
 
143
+ _patch_diffnext_qwen3()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
 
146
+ def parse_args():
147
+ parser = argparse.ArgumentParser(description="Serve URSA TI2V application")
148
+ parser.add_argument("--model", default="BAAI/URSA-0.6B-FSQ320", help="model path")
149
+ parser.add_argument("--device", type=int, default=0, help="device index")
150
+ parser.add_argument("--precision", default="float16", help="compute precision")
151
+ # ---- torchao 量化开关 ----
152
+ parser.add_argument(
153
+ "--quant",
154
+ default="int4",
155
+ choices=["none", "int4", "int8"],
156
+ help="torchao weight-only 量化:int4=tinygemm 4bit / int8=A16W8",
157
+ )
158
+ parser.add_argument(
159
+ "--quant-group-size",
160
+ type=int,
161
+ default=128,
162
+ choices=[32, 64, 128, 256],
163
+ help="分组量化粒度,越小越精细、压缩率越低(32 精度最好但量化最慢)",
164
+ )
165
+ parser.add_argument(
166
+ "--quant-algo",
167
+ default="hqq",
168
+ choices=["hqq", "tinygemm"],
169
+ help="int4 的 qparams 算法:hqq 精度高但量化慢;tinygemm 快但需 mslk 依赖",
170
+ )
171
+ parser.add_argument(
172
+ "--quant-keep-layers",
173
+ type=int,
174
+ default=2,
175
+ help="首尾各 N 层保持原精度(0=全部量化),越大越稳、越费显存",
176
+ )
177
+ parser.add_argument("--quant-lm-head", action="store_true", help="连 codebook 输出头一起量化(不建议)")
178
+ parser.add_argument("--quant-vae", action="store_true", help="连 VAE 一起量化(强烈不建议,画质损失大)")
179
+ return parser.parse_args()
180
 
181
+
182
+ def crop_image(image, target_h, target_w):
183
+ h, w = image.height, image.width
184
+ aspect_ratio_target, aspect_ratio = target_w / target_h, w / h
185
+ if aspect_ratio > aspect_ratio_target:
186
+ new_w = int(h * aspect_ratio_target)
187
+ x_start = (w - new_w) // 2
188
+ image = image.crop((x_start, 0, x_start + new_w, h))
189
+ else:
190
+ new_h = int(w / aspect_ratio_target)
191
+ y_start = (h - new_h) // 2
192
+ image = image.crop((0, y_start, w, y_start + new_h))
193
+ return np.array(image.resize((target_w, target_h), PIL.Image.Resampling.BILINEAR))
194
 
195
 
196
  # =========================
197
+ # ✅ 第 6 步:模型加载放在【模块顶层】(ZeroGPU 硬性要求)
 
 
198
  # =========================
199
+ args = parse_args()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
+ # ---------------------------------------------------------------------------
202
+ # ⚠️ torchao int4 的 CUDA 内核(aten::_weight_int4pack_mm)硬性要求:
203
+ # 1) 权重与激活必须是 **bfloat16**(Int4TilePackedTo4dTensor 里直接 assert)
204
+ # 2) 计算能力 >= sm_80(Ampere+)。ZeroGPU 的 A100 / H100 / Blackwell 都满足
205
+ # 所以只要开量化,就强制把精度抬到 bfloat16,避免 fp16 下 dispatch 失败。
206
+ # ---------------------------------------------------------------------------
207
+ if args.quant != "none" and args.precision.lower() != "bfloat16":
208
+ print(
209
+ f"[quant] force precision {args.precision} -> bfloat16 "
210
+ "(torchao int4 tinygemm requires bf16)",
211
+ flush=True,
212
+ )
213
+ args.precision = "bfloat16"
214
 
215
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu", args.device)
216
+ model_args = {
217
+ "torch_dtype": getattr(torch, args.precision.lower()),
218
+ "trust_remote_code": True,
219
+ }
220
 
221
+ pipe = URSAPipeline.from_pretrained(args.model, **model_args).to(device)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
 
224
  # =========================
225
+ # ✅ 第 6.5 步:torchao 权重量化
226
+ #
227
+ # ⚠️ 为什么仍然不能挪到顶层 CPU 上做?
228
+ # int4 的 tile_packed_to_4d 布局是 **device-specific** 的:
229
+ # - 量化时会调 torch.cuda.get_device_capability() 做 assert;
230
+ # - 打包进 tinygemm 专用 4D 布局([N/8][K/128][32][4]);
231
+ # - transformers 文档也明确 "int4 model can only be loaded on the same
232
+ # device it was quantized on"。
233
+ # ZeroGPU 在模块加载阶段 cuda 不可见,所以量化必须【惰性】到 @spaces.GPU 内执行。
234
+ #
235
+ # ⚠️ packing format 选型(踩坑点):
236
+ # - tile_packed_to_4d + hqq → aten 原生 _weight_int4pack_mm,无额外依赖 ✅ 默认
237
+ # - plain (Int4Tensor) → 走 torch.ops.mslk.*,需要 MSLK 可选依赖,
238
+ # 没装 mslk 就会在 forward 时炸 ❌
239
+ # - plain_int32 → XPU / NPU 专用,CUDA 直接不支持 ❌
240
+ #
241
+ # ⚠️ 量化范围:
242
+ # - 只替换 nn.Linear:主干 attn(q/k/v/o) + mlp(gate/up/down) 被压缩;
243
+ # - nn.Embedding(vocab 215669 的 token 表)不动 → 它是显存大头且量化后
244
+ # forward 要整表反量化,只省存储不省峰值显存;
245
+ # - lm_head(hidden -> 64000 codebook logits)默认不动,它是采样 token 的最后
246
+ # 一道精度关口,量化后最容易掉画质;
247
+ # - VAE 默认不动(视频解码直接决定观感);
248
+ # - 首尾若干层默认保持 bf16,避免误差在网络两端被放大。
249
  # =========================
250
+ QUANT_MODE = args.quant
251
+ COMPUTE_DTYPE = getattr(torch, args.precision.lower())
252
+ _QUANT_STATE = {"log": "pending"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
 
254
 
255
+ def probe_torchao():
256
+ """探测 torchao 是否可用。构建期无 GPU 也能 import,这里只做可读诊断。"""
257
+ info = {"available": False, "version": None, "error": None}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  try:
259
+ import torchao
260
+
261
+ info["available"] = True
262
+ info["version"] = getattr(torchao, "__version__", "?")
 
263
  except Exception as e:
264
+ info["error"] = repr(e)
265
+ print(
266
+ "[torchao] available=%s version=%s torch=%s cuda=%s quant=%s%s"
267
+ % (
268
+ info["available"],
269
+ info["version"],
270
+ torch.__version__,
271
+ torch.version.cuda,
272
+ QUANT_MODE,
273
+ " error=%s" % info["error"] if info["error"] else "",
274
+ ),
275
+ flush=True,
276
+ )
277
+ return info
278
 
 
 
 
279
 
280
+ _TORCHAO_PROBE = probe_torchao() if QUANT_MODE != "none" else {"available": False}
281
 
 
 
282
 
283
+ def _is_torchao_weight(w) -> bool:
284
+ """量化后权重是 torchao 的 Tensor subclass(如 Int4TilePackedTo4dTensor)。"""
285
+ return isinstance(w, torch.Tensor) and type(w).__module__.startswith("torchao")
286
 
 
 
 
 
 
 
 
 
 
 
 
 
287
 
288
+ def is_quantized() -> bool:
289
+ """判断权重是否已量化(fork worker 里也能正确识别)。"""
290
+ if QUANT_MODE == "none":
291
+ return True
292
+ for m in pipe.transformer.modules():
293
+ if isinstance(m, torch.nn.Linear) and _is_torchao_weight(m.weight):
294
+ return True
295
+ if args.quant_vae:
296
+ for m in pipe.vae.modules():
297
+ if isinstance(m, torch.nn.Linear) and _is_torchao_weight(m.weight):
298
+ return True
299
+ return False
300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
+ def _layer_index(path: str):
303
+ m = re.search(r"model\.layers\.(\d+)\.", path)
304
+ return int(m.group(1)) if m else None
305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
+ def _should_quantize(path: str, n_layers: int) -> bool:
308
+ if "lm_head" in path and not args.quant_lm_head:
309
+ return False
310
+ idx = _layer_index(path)
311
+ if idx is not None and args.quant_keep_layers > 0 and n_layers > 2 * args.quant_keep_layers:
312
+ if idx < args.quant_keep_layers or idx >= n_layers - args.quant_keep_layers:
313
+ return False
314
+ return True
315
 
316
+
317
+ def build_quant_config():
318
+ """构造 torchao 的量化 config。"""
319
+ from torchao.quantization import Int4WeightOnlyConfig, Int8WeightOnlyConfig
320
+
321
+ if QUANT_MODE == "int8":
322
+ return Int8WeightOnlyConfig(group_size=args.quant_group_size, set_inductor_config=False)
323
+
324
+ # int4:CUDA 上唯一走 aten 原生 _weight_int4pack_mm 的组合
325
+ if args.quant_algo == "hqq":
326
+ return Int4WeightOnlyConfig(
327
+ group_size=args.quant_group_size,
328
+ int4_packing_format="tile_packed_to_4d",
329
+ int4_choose_qparams_algorithm="hqq",
330
+ set_inductor_config=False,
331
+ )
332
+ # tinygemm:量化快(非迭代),但 PLAIN 布局需要 mslk 可选依赖
333
+ return Int4WeightOnlyConfig(
334
+ group_size=args.quant_group_size,
335
+ int4_packing_format="plain",
336
+ int4_choose_qparams_algorithm="tinygemm",
337
+ set_inductor_config=False,
338
+ )
339
+
340
+
341
+ @torch.no_grad()
342
+ def apply_torchao_quant_() -> tuple:
343
+ """用 torchao 原地量化 transformer(可选 VAE)。必须在 CUDA 上执行。"""
344
+ from torchao.quantization import quantize_
345
+
346
+ config = build_quant_config()
347
+ dev = torch.device("cuda", args.device)
348
+ n_layers = len(pipe.transformer.model.layers)
349
+ seen = []
350
+
351
+ def filter_fn(module: torch.nn.Module, fqn: str) -> bool:
352
+ if not isinstance(module, torch.nn.Linear):
353
+ return False
354
+ ok = _should_quantize(fqn, n_layers)
355
+ seen.append((fqn, ok))
356
+ return ok
357
+
358
+ quantize_(pipe.transformer, config, filter_fn=filter_fn, device=dev)
359
+ quantize_(pipe.vae, config, filter_fn=lambda m, fqn: isinstance(m, torch.nn.Linear)) if (
360
+ args.quant_vae
361
+ ) else None
362
+
363
+ converted = sum(1 for _, ok in seen if ok)
364
+ skipped = [fqn for fqn, ok in seen if not ok]
365
+ return converted, skipped
366
+
367
+
368
+ def ensure_model_ready():
369
+ """把 pipeline 搬到真卡并完成一次性量化。必须在 @spaces.GPU 内部调用。"""
370
+ global QUANT_MODE
371
+ dev = torch.device("cuda", args.device)
372
+ if str(pipe.device) != str(dev):
373
+ pipe.to(dev)
374
+ if QUANT_MODE == "none" or is_quantized():
375
+ return
376
+ t0 = time.time()
377
+ try:
378
+ converted, skipped = apply_torchao_quant_()
379
+ except Exception as e:
380
+ # torchao 缺失 / 内核不匹配 / OOM → 优雅降级到原精度,不能让整个生成失败
381
+ QUANT_MODE = "none"
382
+ _QUANT_STATE["log"] = f"FALLBACK to {args.precision}: {e!r}"
383
+ print("[quant]", _QUANT_STATE["log"], flush=True)
384
+ return
385
+ if torch.cuda.is_available():
386
+ torch.cuda.empty_cache()
387
+ mem = torch.cuda.memory_allocated() / 2**30
388
+ else: # pragma: no cover
389
+ mem = -1
390
+ _QUANT_STATE["log"] = (
391
+ f"{args.quant}/{args.quant_algo}/g{args.quant_group_size}: {converted} linear converted, "
392
+ f"{len(skipped)} skipped ({', '.join(skipped[:6])}{' ...' if len(skipped) > 6 else ''}), "
393
+ f"{time.time() - t0:.1f}s, mem={mem:.2f}GiB"
394
  )
395
+ print("[quant]", _QUANT_STATE["log"], flush=True)
 
 
396
 
397
 
398
  # =========================
399
+ # ✅ 第 7 步:真正跑推理的函数加 @spaces.GPU
400
+ # ZeroGPU 对单次 duration 有两道独立检查:
401
+ # 1) 档位上限:超过 → 「illegal duration」(与剩余配额无关,直接拒绝)
402
+ # 2) 配额预扣:声明值 > 访客剩余配额 → 「quota exceeded」
403
+ # 两道都要求声明值越小越好,所以用【动态 duration】按实际步数估算。
404
  # =========================
405
+
406
+ # 非 PRO 档位的单次上限。若仍报 illegal duration,逐级下调到 60 / 30 再试。
407
+ MAX_DURATION = 120
408
+
409
+ # 实测基准(half Blackwell, 512x320):T2I 25 步约 4s,其中首步含预热约 1.3s
410
+ IMAGE_OVERHEAD, IMAGE_PER_STEP = 15, 0.5
411
+ VIDEO_OVERHEAD, VIDEO_PER_STEP = 20, 2.0
412
+
413
+ # 首次调用额外开销:CPU→GPU 搬运 + int4 打包(HQQ 是迭代算法,group_size 越小越慢)。
414
+ # ZeroGPU 每任务新起进程,量化状态可能不复用,所以按「是否已量化」动态加。
415
+ QUANT_EXTRA_IMAGE, QUANT_EXTRA_VIDEO = 25, 40
416
+
417
+
418
+ def _clamped(value):
419
+ return int(max(20, min(value, MAX_DURATION)))
420
+
421
+
422
+ def _quant_extra(kind: str) -> float:
423
+ if QUANT_MODE == "none" or is_quantized():
424
+ return 0.0
425
+ return QUANT_EXTRA_IMAGE if kind == "image" else QUANT_EXTRA_VIDEO
426
+
427
+
428
+ def estimate_image_duration(
429
+ prompt,
430
+ negative_prompt,
431
+ seed,
432
+ randomize_seed,
433
+ guidance_scale,
434
+ num_inference_steps=25,
435
+ ):
436
+ return _clamped(IMAGE_OVERHEAD + num_inference_steps * IMAGE_PER_STEP + _quant_extra("image"))
437
+
438
+
439
+ def estimate_video_duration(
440
+ prompt,
441
+ negative_prompt,
442
+ image,
443
+ motion_score,
444
+ seed,
445
+ randomize_seed,
446
+ guidance_scale,
447
+ num_inference_steps,
448
+ output_type="np",
449
+ ):
450
+ return _clamped(VIDEO_OVERHEAD + num_inference_steps * VIDEO_PER_STEP + _quant_extra("video"))
451
+
452
+
453
+ @spaces.GPU(duration=estimate_image_duration)
454
+ def generate_image(
455
+ prompt,
456
+ negative_prompt,
457
+ seed,
458
+ randomize_seed,
459
+ guidance_scale,
460
+ num_inference_steps=25,
461
+ ):
462
+ ensure_model_ready()
463
+ args = {**locals(), **video_presets["t2i"]}
464
+ seed = np.random.randint(2147483647) if randomize_seed else seed
465
+ device = getattr(pipe, "_offload_device", pipe.device)
466
+ generator = torch.Generator(device=device).manual_seed(seed)
467
+ images = pipe(generator=generator, **args).frames
468
+ return [export_to_image(image, quality=95) for image in images] + [seed]
469
+
470
+
471
+ @spaces.GPU(duration=estimate_video_duration)
472
+ def generate_video(
473
+ prompt,
474
+ negative_prompt,
475
+ image,
476
+ motion_score,
477
+ seed,
478
+ randomize_seed,
479
+ guidance_scale,
480
+ num_inference_steps,
481
+ output_type="np",
482
+ ):
483
+ ensure_model_ready()
484
+ args = {**locals(), **video_presets["ti2v"]}
485
+ args["prompt"] = f"motion={motion_score:.1f}, {prompt}"
486
+ args["image"] = crop_image(image, args["height"], args["width"]) if image else None
487
+ seed = np.random.randint(2147483647) if randomize_seed else seed
488
+ device = getattr(pipe, "_offload_device", pipe.device)
489
+ generator = torch.Generator(device=device).manual_seed(seed)
490
+ frames = pipe(generator=generator, **args).frames[0]
491
+ return export_to_video(frames, fps=12), seed
492
+
493
 
494
  css = """#col-container {margin: 0 auto; max-width: 1366px}"""
495
  header = (
 
497
  "<h2>Uniform Discrete Diffusion with Metric Path for Video Generation</h2>"
498
  "<h3><a href='https://arxiv.org/abs/2510.24717' target='_blank' rel='noopener'>[paper]</a>"
499
  "<a href='https://github.com/baaivision/URSA' target='_blank' rel='noopener'>[code]</a></h3>"
500
+ f"<p style='opacity:.7'>torchao weight-only quantization: <b>{QUANT_MODE}</b>"
501
+ f" ({args.quant_algo}, group_size={args.quant_group_size}, {args.precision}, "
502
+ f"keep {args.quant_keep_layers} head/tail layers)</p>"
503
  "</div>"
504
  )
505
 
506
+ video_presets = {
507
+ "t2i": {"width": 512, "height": 320, "num_frames": 1},
508
+ "ti2v": {"width": 512, "height": 320, "num_frames": 49},
509
+ }
510
+
511
  prompts = [
512
  "a lone grizzly bear walks through a misty forest at dawn, sunlight catching its fur.",
513
  "Many spotted jellyfish pulsating under water. Their bodies are transparent and glowing in deep ocean.",
514
+ "An intense close-up of a soldier’s face, covered in dirt and sweat, his eyes filled with determination as he surveys the battlefield.",
515
+ "a close-up shot of a woman standing in a dimly lit room. she is wearing a traditional chinese outfit, which includes a red and gold dress with intricate designs and a matching headpiece. the woman has her hair styled in an updo, adorned with a gold accessory. her makeup is done in a way that accentuates her features, with red lipstick and dark eyeshadow. she is looking directly at the camera with a neutral expression. the room has a rustic feel, with wooden beams and a stone wall visible in the background. the lighting in the room is soft and warm, creating a contrast with the woman's vibrant attire. there are no texts or other objects in the video. the style of the video is a portrait, focusing on the woman and her attire.",
516
+ "The camera slowly rotates around a massive stack of vintage televisions that are placed within a large New York museum gallery. Each of the televisions is showing a different program. There are 1950s sci-fi movies with their distinctive visuals, horror movies with their creepy scenes, news broadcasts with moving images and words, static on some screens, and a 1970s sitcom with its characteristic look. The televisions are of various sizes and designs, some with rounded edges and others with more angular shapes. The gallery is well-lit, with light falling on the stack of televisions and highlighting the different programs being shown. There are no people visible in the immediate vicinity, only the stack of televisions and the surrounding gallery space.",
517
  ]
518
+ motion_scores = [9, 9, 9, 9, 9]
519
+ examples = [list(x) for x in zip(prompts, motion_scores)]
520
+
521
 
522
  if __name__ == "__main__":
523
+ # Application
524
  app = gr.Blocks(css=css, theme="origin").__enter__()
525
  container = gr.Column(elem_id="col-container").__enter__()
526
  _, main_row = gr.Markdown(header), gr.Row().__enter__()
527
 
528
+ # Input
529
  input_col = gr.Column().__enter__()
530
+ prompt = gr.Text(
531
+ label="Prompt",
532
+ placeholder="Describe the video you want to generate",
533
+ value="A lone grizzly bear walks through a misty forest at dawn, sunlight catching its fur.",
534
+ lines=5,
535
+ )
536
  negative_prompt = gr.Text(
537
+ label="Negative Prompt",
538
+ placeholder="Describe what you don't want in the video",
539
+ value="worst quality, low quality, inconsistent motion, static, still, blurry, jittery, distorted, ugly",
540
+ lines=1,
541
  )
542
  with gr.Row():
543
  generate_image_btn = gr.Button("Generate Image Prompt", variant="primary", size="lg")
544
  generate_video_btn = gr.Button("Generate Video", variant="primary", size="lg")
545
+ image_prompt = gr.Image(label="Image Prompt", height=480, type="pil")
546
 
547
+ options = gr.Accordion("Options", open=False).__enter__()
548
+ seed = gr.Slider(label="Seed", maximum=2147483647, step=1, value=0)
549
+ randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
550
+ guidance_scale = gr.Slider(label="Guidance scale", minimum=1, maximum=10.0, step=0.1, value=7.0)
 
 
 
551
  with gr.Row():
552
+ # 上限从 100 收到 50:与 estimate_video_duration 的估算保持自洽,
553
+ # 避免访客拉满步数后必然撞上档位上限而被直接拒绝
554
+ num_inference_steps = gr.Slider(label="Inference steps", minimum=1, maximum=50, step=1, value=30)
555
  options.__exit__(), input_col.__exit__()
556
 
557
+ # Results
558
  result_col = gr.Column().__enter__()
559
  motion = gr.Slider(label="Motion Score", minimum=1, maximum=10, step=1, value=9)
560
+ result = gr.Video(label="Result", height=480, show_label=False, autoplay=True)
561
  result_col.__exit__(), main_row.__exit__()
562
 
563
  with gr.Row():
 
565
 
566
  container.__exit__()
567
  gr.on(
568
+ triggers=[generate_image_btn.click, prompt.submit, negative_prompt.submit],
569
  fn=generate_image,
570
+ inputs=[
571
+ prompt,
572
+ negative_prompt,
573
+ seed,
574
+ randomize_seed,
575
+ guidance_scale,
576
+ ],
577
  outputs=[image_prompt, seed],
578
  )
579
  gr.on(
580
+ triggers=[generate_video_btn.click, prompt.submit, negative_prompt.submit],
581
  fn=generate_video,
582
+ inputs=[
583
+ prompt,
584
+ negative_prompt,
585
+ image_prompt,
586
+ motion,
587
+ seed,
588
+ randomize_seed,
589
+ guidance_scale,
590
+ num_inference_steps,
591
+ ],
592
  outputs=[result, seed],
593
  )
594
+ # 限制并发,避免多个请求抢同一张卡
595
  app.__exit__(), app.queue(default_concurrency_limit=1).launch(
596
+ share=False, ssr_mode=False, max_threads=1
597
  )