Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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
|
| 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 |
-
# ✅ 第
|
| 26 |
# =========================
|
| 27 |
import os
|
| 28 |
|
| 29 |
-
|
| 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 |
-
|
| 43 |
-
import
|
| 44 |
-
|
| 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 |
-
|
| 64 |
-
|
| 65 |
-
|
|
|
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
# =========================
|
| 69 |
-
#
|
| 70 |
-
# diffnext 在【模块导入时】就用 @torch.compile 装饰了 RoPE 实现
|
| 71 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
# 🔧 第
|
|
|
|
| 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 |
-
# 🔧 第
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
|
| 207 |
|
| 208 |
# =========================
|
| 209 |
-
#
|
| 210 |
-
# 注意:URSA 的 logits 是用 nn.functional.linear(x, self.lm_head.weight) 算的,
|
| 211 |
-
# 直接吃 weight 张量,所以 lm_head 绝对不能换成 Linear4bit,否则会炸。
|
| 212 |
# =========================
|
| 213 |
-
|
| 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 |
-
|
| 276 |
-
|
|
|
|
|
|
|
|
|
|
| 277 |
|
| 278 |
-
|
| 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 |
-
|
| 325 |
-
|
| 326 |
-
|
| 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 |
-
|
| 353 |
-
|
| 354 |
-
|
| 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 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
print("[quant] transformer: int8 dynamic", flush=True)
|
| 398 |
except Exception as e:
|
| 399 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
args.
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
)
|
| 490 |
-
|
| 491 |
-
print(f"[done ] seed={used_seed} -> {os.path.abspath(args.out)}", flush=True)
|
| 492 |
-
raise SystemExit(0)
|
| 493 |
|
| 494 |
|
| 495 |
# =========================
|
| 496 |
-
# ✅ 第
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
# =========================
|
| 498 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|
|
|
|
|
|
|
| 507 |
"</div>"
|
| 508 |
)
|
| 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
|
| 515 |
-
"a close-up shot of a woman standing in a dimly lit room
|
| 516 |
-
"The camera slowly rotates around a massive stack of vintage televisions
|
| 517 |
]
|
| 518 |
-
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
negative_prompt = gr.Text(
|
| 528 |
-
label="Negative Prompt",
|
| 529 |
-
|
|
|
|
|
|
|
| 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=
|
| 535 |
|
| 536 |
-
options = gr.Accordion("Options", open=
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 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 |
-
|
| 545 |
-
|
|
|
|
| 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=
|
| 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=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 561 |
outputs=[image_prompt, seed],
|
| 562 |
)
|
| 563 |
gr.on(
|
| 564 |
-
triggers=[generate_video_btn.click],
|
| 565 |
fn=generate_video,
|
| 566 |
-
inputs=[
|
| 567 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
outputs=[result, seed],
|
| 569 |
)
|
|
|
|
| 570 |
app.__exit__(), app.queue(default_concurrency_limit=1).launch(
|
| 571 |
-
share=
|
| 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 |
)
|