multimodalart HF Staff commited on
Commit
1b0a14f
·
verified ·
1 Parent(s): 15994e4

Qwen-Video-Edit ZeroGPU demo

Browse files
.gitattributes CHANGED
@@ -35,3 +35,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  example1.mp4 filter=lfs diff=lfs merge=lfs -text
37
  example2.mp4 filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  example1.mp4 filter=lfs diff=lfs merge=lfs -text
37
  example2.mp4 filter=lfs diff=lfs merge=lfs -text
38
+ examples/city_traffic_night.mp4 filter=lfs diff=lfs merge=lfs -text
39
+ examples/man_dancing.mp4 filter=lfs diff=lfs merge=lfs -text
40
+ examples/ocean_waves.mp4 filter=lfs diff=lfs merge=lfs -text
41
+ examples/pottery_wheel.mp4 filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -4,37 +4,61 @@ emoji: 🎬
4
  colorFrom: gray
5
  colorTo: green
6
  sdk: gradio
7
- sdk_version: 6.15.1
8
  app_file: app.py
9
- short_description: Instruction-based video editing via an image editing model
10
  python_version: "3.12"
11
  startup_duration_timeout: 1h
 
12
  ---
13
 
14
  # Qwen-Video-Edit
15
 
16
- Instruction-based **video editing by repurposing an image editing model**. Upload a source video and an editing instruction, and the model produces an edited video by applying the instruction to the video content.
 
17
 
18
- Built on [Qwen-Image-Edit](https://huggingface.co/Qwen/Qwen-Image-Edit)'s DiT, which directly edits Wan 2.1 video-VAE latents, bridged by two tiny trainable projections warm-started from the DiT's own input/output layers.
 
 
 
19
 
20
- **Paper**: [Qwen-Video-Edit: Instruction-Based Video Editing by Repurposing an Image Editing Model](https://arxiv.org/abs/2608.14790)
21
 
22
- ## How it works
 
 
 
 
 
23
 
24
- ```
25
- source video → (frozen Wan2.1 VAE) → video latents
26
-
27
- trainable in-projection
28
-
29
- prompt + frame-grid preview → Qwen-Image-Edit DiT (LoRA) → grid RoPE over latent frames
30
-
31
- trainable out-projection
32
-
33
- edited latents → (frozen Wan2.1 VAE) → edited video
34
- ```
35
 
36
- ## Credits
 
 
 
 
37
 
38
- - Model: [yunpeng1998/Qwen-Video-Edit](https://huggingface.co/yunpeng1998/Qwen-Video-Edit)
39
- - Code: [yunpeng1998/Qwen-Video-Edit](https://github.com/yunpeng1998/Qwen-Video-Edit)
40
- - Built on [DiffSynth-Studio](https://github.com/modelscope/DiffSynth-Studio), [Qwen-Image-Edit](https://huggingface.co/Qwen/Qwen-Image-Edit), and [Wan 2.1](https://huggingface.co/Wan-AI/Wan2.1-T2V-1.3B)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  colorFrom: gray
5
  colorTo: green
6
  sdk: gradio
7
+ sdk_version: 6.24.0
8
  app_file: app.py
9
+ short_description: Instruction-based video editing with Qwen-Image-Edit
10
  python_version: "3.12"
11
  startup_duration_timeout: 1h
12
+ license: mit
13
  ---
14
 
15
  # Qwen-Video-Edit
16
 
17
+ Demo of **[Qwen-Video-Edit: Instruction-Based Video Editing by Repurposing an Image
18
+ Editing Model](https://huggingface.co/papers/2608.14790)** (Bai, Gandelsman, Gharbi, Huang).
19
 
20
+ * Paper: https://huggingface.co/papers/2608.14790
21
+ * Project page: https://yunpeng1998.github.io/Qwen-Video-Edit-Page
22
+ * Code: https://github.com/yunpeng1998/Qwen-Video-Edit
23
+ * Weights: https://huggingface.co/yunpeng1998/Qwen-Video-Edit (`360P/step-30000.safetensors`)
24
 
25
+ ## What it does
26
 
27
+ An image-editing DiT (Qwen-Image-Edit, 20.4B) is repurposed to edit **video latents**
28
+ directly. The source clip is encoded by the frozen Wan 2.1 video VAE; two small trainable
29
+ projections bridge the 16-channel video latents into and out of the DiT's token space, and
30
+ the 12 latent frames are laid out as a 3×4 tile grid of one virtual image so the image
31
+ model's 2-D RoPE covers the whole clip. Decoding the edited tokens back through the Wan VAE
32
+ gives the edited video.
33
 
34
+ ## What this Space runs
 
 
 
 
 
 
 
 
 
 
35
 
36
+ This is **stage 1** of the authors' `infer.py`, matching the released 360P checkpoint's
37
+ training configuration exactly (`--num_frames 45 --video_max_pixels 245760
38
+ --latent_mode wan_compressed --pe_mode grid`), including the flow-matching schedule with
39
+ `dynamic_shift_len`, the Qwen2.5-VL prompt embedding over a 3×3 preview grid of the clip,
40
+ and true-CFG with the norm-preserving rescale.
41
 
42
+ Deliberate deviations, for ZeroGPU:
43
+
44
+ * **One 45-frame (≈2.8 s) chunk per run**, at 360p — the window the checkpoint was
45
+ trained on. The reference script can loop over multiple chunks.
46
+ * **The optional Wan 2.2 denoising-enhancement pass is not run** (it needs ~80 GB of extra
47
+ weights on top of the editing stack). This is equivalent to the reference's
48
+ `--skip_enhance`.
49
+ * **Quantization**: the DiT transformer blocks run in fp8 (dynamic activation) and the
50
+ Qwen2.5-VL text encoder in int8 weight-only, via `torchao`. Entry/exit layers and both
51
+ trainable projections stay in bf16.
52
+ * Uploads are temporally resampled to 16 fps (the training frame rate) before the
53
+ 45-frame window is taken, so higher-fps clips are not played back in slow motion.
54
+ * The default is 20 denoising steps (the reference default is 40) to keep a run inside a
55
+ reasonable ZeroGPU slice; raise it in **Advanced settings** for higher fidelity.
56
+
57
+ ## Example clips
58
+
59
+ The bundled example videos come from
60
+ [`linoyts/repo-to-space-example-videos`](https://huggingface.co/datasets/linoyts/repo-to-space-example-videos)
61
+ (CC0-1.0), trimmed to the model's 45-frame window. The editing instructions are taken
62
+ verbatim from the authors' own `examples/prompts.txt`.
63
+
64
+ Model weights are MIT-licensed; the vendored DiffSynth-Studio code is Apache-2.0.
app.py CHANGED
@@ -1,364 +1,416 @@
1
- """Qwen-Video-Edit: Instruction-based video editing by repurposing an image editing model.
2
 
3
- Gradio Space demo for yunpeng1998/Qwen-Video-Edit.
 
 
 
 
 
 
 
 
 
4
  """
5
 
6
  import os
7
 
8
- os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
9
  os.environ.setdefault("DIFFSYNTH_DOWNLOAD_SOURCE", "huggingface")
 
 
 
 
10
 
11
- import spaces # MUST come before torch / any CUDA-touching import
12
- import functools
13
- import io
14
- import time
15
- import tempfile
16
- import math
17
- import gc
18
 
19
- import torch
20
- import numpy as np
21
- from PIL import Image
22
- import gradio as gr
23
 
24
- print = functools.partial(print, flush=True)
 
 
 
 
 
 
25
 
26
- # Apply rope patch guard (verifies vendored diffsynth is the patched copy)
27
- import rope_patch
28
- rope_patch.apply()
29
 
30
- from diffsynth.core import ModelConfig
31
- from diffsynth.pipelines.qwen_image import QwenImagePipeline, QwenImageUnit_PromptEmbedder
32
- from safetensors.torch import load_file
33
 
34
- from dataset import (
35
- adaptive_dims, build_preview_grid, decode_video_frames,
36
- first_n_frames_padded, frames_to_tensor, open_maybe_remote,
37
- )
38
- from model import factorize_latent_grid, model_fn_video_tokens, num_token_groups
39
- from projections import (
40
- WanToQwenProjection, QwenToWanProjection,
41
  )
42
 
43
- # ---- Model configuration (must match checkpoint training config) ----
44
- CHECKPOINT_REPO = "yunpeng1998/Qwen-Video-Edit"
45
- CHECKPOINT_PATH = "360P/step-30000.safetensors"
46
- NUM_FRAMES = 13 # Minimal frames to fit in VRAM (still satisfies (n-1)%4==0)
47
- VIDEO_MAX_PIXELS = 86400 # ~270p for VRAM
 
 
 
 
 
 
 
 
48
  LATENT_MODE = "wan_compressed"
49
  PE_MODE = "grid"
50
- ZERO_COND_T = True
51
- NUM_INFERENCE_STEPS = 30
52
- CFG_SCALE = 4.0
53
- DEFAULT_SEED = 42
54
  FPS = 16
55
-
56
- DEVICE = "cuda"
57
- CPU = "cpu"
58
  DTYPE = torch.bfloat16
 
59
 
60
- latent_grid = factorize_latent_grid(num_token_groups(NUM_FRAMES, LATENT_MODE))
61
-
62
-
63
- def load_checkpoint_into(pipe, in_proj, out_proj, checkpoint_path):
64
- """Load the fine-tuned DiT weights + projections from a safetensors checkpoint."""
65
- state_dict = load_file(checkpoint_path)
66
- dit_sd = {k[len("pipe.dit."):]: v for k, v in state_dict.items() if k.startswith("pipe.dit.")}
67
- in_proj.load_state_dict({k[len("in_proj."):]: v for k, v in state_dict.items() if k.startswith("in_proj.")})
68
- out_proj.load_state_dict({k[len("out_proj."):]: v for k, v in state_dict.items() if k.startswith("out_proj.")})
69
- if any("lora" in k for k in dit_sd):
70
- pipe.load_lora(pipe.dit, state_dict=dit_sd, hotload=True)
71
- print(f"[app] Loaded LoRA DiT weights ({len(dit_sd)} tensors) + projections.")
72
- else:
73
- pipe.dit.load_state_dict(dit_sd, strict=False)
74
- print(f"[app] Loaded full DiT weights ({len(dit_sd)} tensors) + projections.")
75
-
76
-
77
- def tensor_to_uint8_frames(video):
78
- """(C,T,H,W) [-1,1] -> list of HWC uint8 arrays."""
79
- v = ((video.float().clamp(-1, 1) + 1) * 127.5).to(torch.uint8)
80
- return [v[:, t].permute(1, 2, 0).cpu().numpy() for t in range(v.shape[1])]
81
-
82
-
83
- def save_video(frames_uint8, path_base, fps=16):
84
- """Save frames as mp4."""
85
- import imageio.v3 as iio
86
- path = path_base + ".mp4"
87
- iio.imwrite(path, frames_uint8, fps=fps, codec="libx264")
88
- return path
89
-
90
-
91
- def free_cuda():
92
- """Empty cache and collect garbage."""
93
- torch.cuda.empty_cache()
94
- gc.collect()
95
 
 
 
 
 
96
 
97
- # ---- Load model at module scope ----
98
- print("[app] Loading Qwen-Video-Edit pipeline "
99
- "(downloading on first run, can take several minutes) ...")
100
- t0 = time.time()
101
 
102
- # Load DiT (Qwen-Image-Edit transformer) — goes to CUDA for ZeroGPU packing
103
- model_configs = [
104
- ModelConfig(
105
- model_id="Qwen/Qwen-Image-Edit",
106
- origin_file_pattern="transformer/diffusion_pytorch_model*.safetensors",
107
- ),
108
- ]
109
-
110
- # Load text encoder separately — keep on CPU to save VRAM
111
- text_encoder_config = ModelConfig(
112
- model_id="Qwen/Qwen-Image",
113
- origin_file_pattern="text_encoder/model*.safetensors",
114
- )
115
 
 
 
 
 
 
116
  pipe = QwenImagePipeline.from_pretrained(
117
- torch_dtype=DTYPE, device=DEVICE, model_configs=model_configs,
 
 
 
 
118
  tokenizer_config=ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="tokenizer/"),
119
  processor_config=ModelConfig(model_id="Qwen/Qwen-Image-Edit", origin_file_pattern="processor/"),
120
  )
121
 
122
- # Load text encoder on CPU (NOT packed by ZeroGPU saves ~17.5GB VRAM)
123
- text_encoder_configs = [text_encoder_config]
124
- text_encoder_pool = pipe.download_and_load_models(
125
- text_encoder_configs, None
126
  )
127
- pipe.text_encoder = text_encoder_pool.fetch_model("qwen_image_text_encoder")
128
- pipe.text_encoder.to(device=CPU) # Keep on CPU
129
-
130
- # Load Wan 2.1 video VAE on CPU (loaded to CUDA only during encode/decode)
131
- wan_id = "Wan-AI/Wan2.1-T2V-1.3B"
132
- wan_pattern = "Wan2.1_VAE.pth"
133
- vae_pool = pipe.download_and_load_models(
134
- [ModelConfig(model_id=wan_id, origin_file_pattern=wan_pattern)], None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  )
136
- wan_vae = vae_pool.fetch_model("wan_video_vae")
137
- wan_vae.to(device=CPU) # Keep on CPU
138
 
139
- # Create projections
140
- in_proj = WanToQwenProjection(16, pipe.dit.img_in.out_features)
141
- out_proj = QwenToWanProjection(16, pipe.dit.img_in.out_features)
 
142
 
143
- # Download and load checkpoint
144
- from huggingface_hub import hf_hub_download
145
- checkpoint_path = hf_hub_download(CHECKPOINT_REPO, CHECKPOINT_PATH)
146
- load_checkpoint_into(pipe, in_proj, out_proj, checkpoint_path)
147
 
148
- in_proj.to(device=DEVICE, dtype=DTYPE)
149
- out_proj.to(device=DEVICE, dtype=DTYPE)
 
 
 
 
 
150
 
151
- print(f"[app] Pipeline ready in {time.time() - t0:.0f}s.")
152
 
 
 
153
 
154
- @spaces.GPU(duration=300, size="xlarge")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  def edit_video(
156
- source_video_path: str,
157
- edit_instruction: str,
 
 
 
158
  seed: int = DEFAULT_SEED,
159
- num_inference_steps: int = NUM_INFERENCE_STEPS,
160
- cfg_scale: float = CFG_SCALE,
161
- negative_prompt: str = " ",
162
  progress=gr.Progress(track_tqdm=True),
163
  ):
164
- """Edit a source video based on a text instruction.
165
 
166
  Args:
167
- source_video_path: Path to the source video file (mp4).
168
- edit_instruction: Text instruction describing how to edit the video.
169
- seed: Random seed for reproducibility.
170
- num_inference_steps: Number of denoising steps (more = higher quality, slower).
171
- cfg_scale: Classifier-free guidance scale (1 = no CFG, 4 = default).
172
- negative_prompt: Negative prompt for CFG guidance.
 
 
 
 
173
 
174
  Returns:
175
- Path to the edited video file.
176
  """
177
- t_start = time.time()
178
-
179
- # VRAM debug
180
- if torch.cuda.is_available():
181
- vram_total = torch.cuda.get_device_properties(0).total_memory / 1e9
182
- vram_alloc = torch.cuda.memory_allocated(0) / 1e9
183
- print(f"[app] VRAM: {vram_alloc:.1f} / {vram_total:.1f} GB allocated")
184
-
185
- # Read and decode the source video
186
- with open(source_video_path, "rb") as f:
187
- all_frames = decode_video_frames(f.read())
188
-
189
- # Use only the first NUM_FRAMES frames
190
- chunk = first_n_frames_padded(all_frames[:NUM_FRAMES], NUM_FRAMES)
191
- prompt = edit_instruction
192
 
193
- # Determine dimensions from max pixels
 
194
  h0, w0 = chunk[0].shape[:2]
195
- width, height = adaptive_dims(w0, h0, VIDEO_MAX_PIXELS)
196
-
197
- # Prepare source video tensor
198
  source = frames_to_tensor(chunk, width, height)
199
  preview = build_preview_grid(chunk)
 
200
 
201
- # --- Phase 1: VAE encode (load VAE to CUDA, then free) ---
202
- wan_vae.to(device=DEVICE, dtype=DTYPE)
203
- tiled = height * width >= 700_000
204
- ref_latents = wan_vae.encode(
205
- [source.to(dtype=DTYPE)], device=DEVICE, tiled=tiled
206
- ).to(device=DEVICE, dtype=DTYPE)
207
- wan_vae.to(device=CPU)
208
- free_cuda()
209
-
210
- # --- Phase 2: Prompt embedding (load text encoder to CUDA, then free) ---
211
- pipe.text_encoder.to(device=DEVICE, dtype=DTYPE)
212
- emb = QwenImageUnit_PromptEmbedder().process(pipe, prompt=prompt, edit_image=preview)
213
- use_cfg = cfg_scale > 1.0
214
- if use_cfg:
215
- neg_emb = QwenImageUnit_PromptEmbedder().process(
216
- pipe, prompt=negative_prompt, edit_image=preview
217
  )
218
- pipe.text_encoder.to(device=CPU)
219
- free_cuda()
220
-
221
- # --- Phase 3: Denoising loop (only DiT + projections in VRAM) ---
222
- if torch.cuda.is_available():
223
- vram_alloc = torch.cuda.memory_allocated(0) / 1e9
224
- vram_total = torch.cuda.get_device_properties(0).total_memory / 1e9
225
- print(f"[app] Before denoising: VRAM {vram_alloc:.1f} / {vram_total:.1f} GB")
226
- group = getattr(in_proj, "group", 1)
227
- noise_seq_len = (
228
- (ref_latents.shape[2] // group)
229
- * (ref_latents.shape[3] // 2)
230
- * (ref_latents.shape[4] // 2)
231
- )
232
- pipe.scheduler.set_timesteps(
233
- num_inference_steps, dynamic_shift_len=noise_seq_len
234
- )
235
- gen = torch.Generator(device="cpu").manual_seed(seed)
236
- latents = torch.randn(ref_latents.shape, generator=gen).to(
237
- device=DEVICE, dtype=DTYPE
238
- )
239
 
240
- def forward(e, timestep):
241
- return model_fn_video_tokens(
242
- pipe.dit, in_proj, out_proj,
243
- latents=latents, ref_latents=ref_latents,
244
- prompt_emb=e["prompt_emb"], prompt_emb_mask=e["prompt_emb_mask"],
245
- timestep=timestep, latent_grid=latent_grid,
246
- zero_cond_t=ZERO_COND_T, pe_mode=PE_MODE,
247
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
 
249
- from tqdm import tqdm
250
- for pid, timestep in enumerate(tqdm(pipe.scheduler.timesteps, desc="denoising")):
251
- timestep = timestep.unsqueeze(0).to(dtype=DTYPE, device=DEVICE)
252
- try:
253
  noise_pred = forward(emb, timestep)
254
  if use_cfg:
 
 
255
  neg_pred = forward(neg_emb, timestep)
256
  comb = neg_pred + cfg_scale * (noise_pred - neg_pred)
257
- comb = comb * (
258
- torch.norm(noise_pred, dim=1, keepdim=True)
259
- / torch.norm(comb, dim=1, keepdim=True)
260
- )
261
  noise_pred = comb
262
- latents = pipe.scheduler.step(
263
- noise_pred, pipe.scheduler.timesteps[pid], latents
264
- )
265
- except RuntimeError as e:
266
- if "NVML_SUCCESS" in str(e) or "out of memory" in str(e).lower():
267
- return None, f"GPU out of memory. The Qwen-Video-Edit DiT (~42GB) is very large. Try reducing inference steps or using a smaller video."
268
- raise
269
-
270
- # --- Phase 4: VAE decode (load VAE back to CUDA) ---
271
- wan_vae.to(device=DEVICE, dtype=DTYPE)
272
- tiled_dec = (latents.shape[3] * 8) * (latents.shape[4] * 8) >= 700_000
273
- video = wan_vae.decode(latents, device=DEVICE, tiled=tiled_dec)[0].cpu()
274
- wan_vae.to(device=CPU)
275
- free_cuda()
276
-
277
- # Save to temporary file
278
- tmp_dir = tempfile.mkdtemp()
279
- out_path = os.path.join(tmp_dir, "edited_video")
280
- save_video(tensor_to_uint8_frames(video), out_path, fps=FPS)
281
-
282
- elapsed = time.time() - t_start
283
- print(f"[app] Video edit completed in {elapsed:.1f}s")
284
-
285
- return out_path + ".mp4", f"Edit completed in {elapsed:.1f}s"
286
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
 
288
- # ---- Gradio UI ----
289
  CSS = """
290
  #col-container { max-width: 1100px; margin: 0 auto; }
291
- .dark .gradio-container { color: var(--body-text-color); }
292
  """
293
 
294
  with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
295
  with gr.Column(elem_id="col-container"):
296
- gr.Markdown("# 🎬 Qwen-Video-Edit")
297
  gr.Markdown(
298
- "Instruction-based video editing by repurposing an image editing model. "
299
- "Upload a video, describe how to edit it, and get the edited result.\n\n"
300
- "[Paper](https://arxiv.org/abs/2608.14790) | "
301
- "[Model](https://huggingface.co/yunpeng1998/Qwen-Video-Edit) | "
302
- "[Code](https://github.com/yunpeng1998/Qwen-Video-Edit)"
303
- )
 
 
 
 
 
 
304
 
 
 
 
 
305
  with gr.Row():
306
- with gr.Column(scale=1):
307
- source_video = gr.Video(
308
- label="Source Video",
309
- sources=["upload"],
310
- )
311
- edit_instruction = gr.Textbox(
312
- label="Edit Instruction",
313
- placeholder="e.g., Replace the woman with robot",
314
  lines=2,
315
  )
316
- run_btn = gr.Button("Edit Video", variant="primary")
317
- with gr.Column(scale=1):
318
- output_video = gr.Video(label="Edited Video")
319
- status_text = gr.Textbox(label="Status", interactive=False)
320
-
321
- with gr.Accordion("Advanced Settings", open=False):
322
- seed = gr.Number(label="Seed", value=DEFAULT_SEED, precision=0)
323
- num_steps = gr.Slider(
324
- label="Inference Steps",
325
- minimum=10,
326
- maximum=80,
327
- value=NUM_INFERENCE_STEPS,
328
- step=1,
329
- )
330
- cfg = gr.Slider(
331
- label="CFG Scale",
332
- minimum=1.0,
333
- maximum=10.0,
334
- value=CFG_SCALE,
335
- step=0.5,
336
- )
337
- negative = gr.Textbox(
338
- label="Negative Prompt",
339
- value=" ",
340
- lines=1,
341
  )
342
 
343
  gr.Examples(
344
- examples=[
345
- ["example1.mp4", "Replace the woman with robot"],
346
- ["example2.mp4", "Replace the woman with an animated-style Captain Marvel"],
347
- ["example1.mp4", "Make it watercolor drawing style."],
348
- ["example2.mp4", "Convert to van Gogh style"],
349
- ],
350
- inputs=[source_video, edit_instruction],
351
- outputs=[output_video, status_text],
352
- fn=edit_video,
353
- cache_examples=False,
354
- run_on_click=True,
355
  )
356
 
357
- run_btn.click(
 
358
  fn=edit_video,
359
- inputs=[source_video, edit_instruction, seed, num_steps, cfg, negative],
360
- outputs=[output_video, status_text],
361
- api_name="edit_video",
362
  )
363
 
364
- demo.launch(mcp_server=True)
 
1
+ """Qwen-Video-Edit — instruction-based video editing on ZeroGPU.
2
 
3
+ Stage 1 of the official pipeline (`infer.py`), run on a single 45-frame chunk:
4
+
5
+ source video -> (frozen Wan 2.1 video VAE) -> wan latents
6
+ -> trainable in-projection
7
+ -> Qwen-Image-Edit DiT (full fine-tune, grid RoPE)
8
+ -> trainable out-projection
9
+ -> (frozen Wan 2.1 video VAE) -> edited video
10
+
11
+ The optional Wan2.2 denoising-enhancement stage (~80GB of extra weights) is
12
+ not run here — it does not fit alongside the editing stack on ZeroGPU.
13
  """
14
 
15
  import os
16
 
17
+ # DiffSynth's loader defaults to ModelScope; keep everything on the HF Hub.
18
  os.environ.setdefault("DIFFSYNTH_DOWNLOAD_SOURCE", "huggingface")
19
+ os.environ.setdefault(
20
+ "DIFFSYNTH_MODEL_BASE_PATH",
21
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), "models"),
22
+ )
23
 
24
+ import spaces # noqa: E402 -- must precede any torch / CUDA-touching import
 
 
 
 
 
 
25
 
26
+ import random # noqa: E402
27
+ import tempfile # noqa: E402
28
+ import time # noqa: E402
 
29
 
30
+ import gradio as gr # noqa: E402
31
+ import imageio.v3 as iio # noqa: E402
32
+ import numpy as np # noqa: E402
33
+ import torch # noqa: E402
34
+ from huggingface_hub import hf_hub_download # noqa: E402
35
+ from safetensors.torch import load_file # noqa: E402
36
+ from tqdm import tqdm # noqa: E402
37
 
38
+ import rope_patch # noqa: E402
 
 
39
 
40
+ rope_patch.apply()
 
 
41
 
42
+ from diffsynth.core import ModelConfig # noqa: E402
43
+ from diffsynth.core.vram.initialization import skip_model_initialization # noqa: E402
44
+ from diffsynth.models.qwen_image_dit import QwenImageDiT # noqa: E402
45
+ from diffsynth.pipelines.qwen_image import ( # noqa: E402
46
+ QwenImagePipeline,
47
+ QwenImageUnit_PromptEmbedder,
 
48
  )
49
 
50
+ from dataset import adaptive_dims, build_preview_grid, frames_to_tensor # noqa: E402
51
+ from model import factorize_latent_grid, model_fn_video_tokens, num_token_groups # noqa: E402
52
+ from projections import QwenToWanProjection, WanToQwenProjection # noqa: E402
53
+
54
+
55
+ # --------------------------------------------------------------------------
56
+ # Checkpoint-pinned configuration (must match how the 360P model was trained;
57
+ # see the repo README: --num_frames 45 --video_max_pixels 245760
58
+ # --latent_mode wan_compressed --pe_mode grid, no --zero_cond_t).
59
+ # --------------------------------------------------------------------------
60
+ CKPT_REPO = "yunpeng1998/Qwen-Video-Edit"
61
+ CKPT_FILE = "360P/step-30000.safetensors"
62
+ NUM_FRAMES = 45
63
  LATENT_MODE = "wan_compressed"
64
  PE_MODE = "grid"
65
+ ZERO_COND_T = False
 
 
 
66
  FPS = 16
 
 
 
67
  DTYPE = torch.bfloat16
68
+ MAX_SEED = np.iinfo(np.int32).max
69
 
70
+ DEFAULT_STEPS = 20
71
+ DEFAULT_CFG = 4.0
72
+ DEFAULT_NEG = " "
73
+ DEFAULT_SEED = 42
74
+ NATIVE_PIXELS = 245760 # 360p the resolution the checkpoint was trained at
75
+ DEFAULT_PIXELS = NATIVE_PIXELS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
+ # Calibrated against the live Space (see README): seconds of GPU time per DiT
78
+ # forward at the native 360p token count, plus fixed VAE / text-encoder cost.
79
+ SEC_PER_FORWARD = 3.0
80
+ FIXED_OVERHEAD = 45.0
81
 
82
+ LATENT_GRID = factorize_latent_grid(num_token_groups(NUM_FRAMES, LATENT_MODE))
 
 
 
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
+ # --------------------------------------------------------------------------
86
+ # Model loading (module scope, eagerly onto CUDA — ZeroGPU streams it in)
87
+ # --------------------------------------------------------------------------
88
+ print("[boot] Loading Qwen text encoder / tokenizer / processor ...", flush=True)
89
+ _t0 = time.time()
90
  pipe = QwenImagePipeline.from_pretrained(
91
+ torch_dtype=DTYPE,
92
+ device="cpu",
93
+ model_configs=[
94
+ ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="text_encoder/model*.safetensors"),
95
+ ],
96
  tokenizer_config=ModelConfig(model_id="Qwen/Qwen-Image", origin_file_pattern="tokenizer/"),
97
  processor_config=ModelConfig(model_id="Qwen/Qwen-Image-Edit", origin_file_pattern="processor/"),
98
  )
99
 
100
+ print("[boot] Loading the Wan 2.1 video VAE ...", flush=True)
101
+ _pool = pipe.download_and_load_models(
102
+ [ModelConfig(model_id="Wan-AI/Wan2.1-T2V-1.3B", origin_file_pattern="Wan2.1_VAE.pth")], None
 
103
  )
104
+ wan_vae = _pool.fetch_model("wan_video_vae")
105
+
106
+ print(f"[boot] Downloading the Qwen-Video-Edit DiT checkpoint (~41GB) ...", flush=True)
107
+ _ckpt_path = hf_hub_download(CKPT_REPO, CKPT_FILE)
108
+ print("[boot] Loading the fine-tuned DiT + projections ...", flush=True)
109
+ _state_dict = load_file(_ckpt_path, device="cpu")
110
+
111
+ in_proj = WanToQwenProjection(16, 3072)
112
+ out_proj = QwenToWanProjection(16, 3072)
113
+ in_proj.load_state_dict({k[len("in_proj.") :]: v for k, v in _state_dict.items() if k.startswith("in_proj.")})
114
+ out_proj.load_state_dict({k[len("out_proj.") :]: v for k, v in _state_dict.items() if k.startswith("out_proj.")})
115
+ in_proj = in_proj.to(dtype=DTYPE).eval()
116
+ out_proj = out_proj.to(dtype=DTYPE).eval()
117
+
118
+ _dit_sd = {k[len("pipe.dit.") :]: v for k, v in _state_dict.items() if k.startswith("pipe.dit.")}
119
+ with skip_model_initialization():
120
+ dit = QwenImageDiT()
121
+ _missing, _unexpected = dit.load_state_dict(_dit_sd, strict=False, assign=True)
122
+ _still_meta = [n for n, p in dit.named_parameters() if p.is_meta]
123
+ if _still_meta:
124
+ raise RuntimeError(f"DiT parameters missing from the checkpoint: {_still_meta[:8]} ({len(_still_meta)} total)")
125
+ print(
126
+ f"[boot] DiT loaded: {len(_dit_sd)} tensors, {sum(p.numel() for p in dit.parameters()) / 1e9:.2f}B params "
127
+ f"(missing={len(_missing)}, unexpected={len(_unexpected)})",
128
+ flush=True,
129
+ )
130
+ dit = dit.eval()
131
+ del _state_dict, _dit_sd
132
+
133
+ pipe.dit = dit
134
+ pipe.to("cuda")
135
+ wan_vae = wan_vae.to("cuda").eval()
136
+ in_proj = in_proj.to("cuda")
137
+ out_proj = out_proj.to("cuda")
138
+
139
+ # The bf16 stack is ~57GB (20.4B DiT + 8.3B Qwen2.5-VL text encoder), which does
140
+ # not fit a ZeroGPU slice. fp8 dynamic-activation quantization of the DiT blocks
141
+ # and int8 weight-only on the text encoder bring it to ~29GB and roughly halve
142
+ # the DiT matmul cost on Blackwell.
143
+ from torchao.quantization import ( # noqa: E402
144
+ Float8DynamicActivationFloat8WeightConfig,
145
+ Int8WeightOnlyConfig,
146
+ quantize_,
147
  )
 
 
148
 
149
+ print("[boot] Quantizing (fp8 DiT blocks / int8 text encoder) ...", flush=True)
150
+ quantize_(dit.transformer_blocks, Float8DynamicActivationFloat8WeightConfig())
151
+ quantize_(pipe.text_encoder, Int8WeightOnlyConfig())
152
+ print(f"[boot] Ready in {time.time() - _t0:.0f}s.", flush=True)
153
 
 
 
 
 
154
 
155
+ # --------------------------------------------------------------------------
156
+ # Helpers (ported 1:1 from the repo's infer.py / dataset.py)
157
+ # --------------------------------------------------------------------------
158
+ def tensor_to_uint8_frames(video):
159
+ """(C, T, H, W) in [-1, 1] -> list of HWC uint8 frames."""
160
+ v = ((video.float().clamp(-1, 1) + 1) * 127.5).to(torch.uint8)
161
+ return [v[:, t].permute(1, 2, 0).cpu().numpy() for t in range(v.shape[1])]
162
 
 
163
 
164
+ def read_chunk(video_path):
165
+ """Decode the source video and take the first NUM_FRAMES at ~16 fps.
166
 
167
+ The checkpoint was trained on 16 fps clips, so a higher-fps upload is
168
+ temporally subsampled (rather than blindly taking 45 consecutive frames,
169
+ which would play back in slow motion at the fixed 16 fps output rate).
170
+ """
171
+ try:
172
+ meta = iio.immeta(video_path, plugin="pyav")
173
+ src_fps = float(meta.get("fps") or FPS)
174
+ except Exception:
175
+ src_fps = float(FPS)
176
+ if not np.isfinite(src_fps) or src_fps <= 0:
177
+ src_fps = float(FPS)
178
+
179
+ frames = [np.asarray(f) for f in iio.imiter(video_path, plugin="pyav")]
180
+ if not frames:
181
+ raise gr.Error("Could not decode any frames from that video.")
182
+ frames = [f[..., :3] if f.ndim == 3 else np.stack([f] * 3, -1) for f in frames]
183
+
184
+ idx = np.round(np.arange(NUM_FRAMES) * src_fps / FPS).astype(int)
185
+ idx = np.minimum(idx, len(frames) - 1)
186
+ return [frames[i] for i in idx]
187
+
188
+
189
+ def _estimate_duration(*args, **kwargs):
190
+ def pick(name, pos, default):
191
+ if name in kwargs:
192
+ return kwargs[name]
193
+ return args[pos] if len(args) > pos else default
194
+
195
+ steps = int(pick("num_inference_steps", 2, DEFAULT_STEPS))
196
+ cfg = float(pick("cfg_scale", 3, DEFAULT_CFG))
197
+ pixels = float(pick("max_pixels", 7, DEFAULT_PIXELS))
198
+ forwards = steps * (2 if cfg > 1.0 else 1)
199
+ return int(FIXED_OVERHEAD + forwards * SEC_PER_FORWARD * (pixels / NATIVE_PIXELS))
200
+
201
+
202
+ # --------------------------------------------------------------------------
203
+ # Inference
204
+ # --------------------------------------------------------------------------
205
+ @spaces.GPU(duration=_estimate_duration)
206
  def edit_video(
207
+ video_path: str,
208
+ instruction: str,
209
+ num_inference_steps: int = DEFAULT_STEPS,
210
+ cfg_scale: float = DEFAULT_CFG,
211
+ negative_prompt: str = DEFAULT_NEG,
212
  seed: int = DEFAULT_SEED,
213
+ randomize_seed: bool = False,
214
+ max_pixels: int = DEFAULT_PIXELS,
 
215
  progress=gr.Progress(track_tqdm=True),
216
  ):
217
+ """Edit the first ~2.8 seconds of a video according to a text instruction.
218
 
219
  Args:
220
+ video_path: Path to the source video. Only the first 45 frames
221
+ (resampled to 16 fps) are edited.
222
+ instruction: Editing instruction, e.g. "Make it Ghibli style".
223
+ num_inference_steps: Flow-matching denoising steps.
224
+ cfg_scale: True-CFG scale. Values above 1 double the compute per step.
225
+ negative_prompt: Negative prompt used when cfg_scale > 1.
226
+ seed: Random seed for the initial noise.
227
+ randomize_seed: Draw a fresh random seed instead of using `seed`.
228
+ max_pixels: Pixel budget per frame (245760 = the native 360p training
229
+ resolution; lower is faster).
230
 
231
  Returns:
232
+ The edited video (mp4, 16 fps) and the seed that produced it.
233
  """
234
+ if not video_path:
235
+ raise gr.Error("Please upload a source video.")
236
+ if not instruction or not instruction.strip():
237
+ raise gr.Error("Please write an editing instruction.")
238
+
239
+ if randomize_seed:
240
+ seed = random.randint(0, MAX_SEED)
241
+ seed = int(seed)
242
+ num_inference_steps = int(num_inference_steps)
243
+ cfg_scale = float(cfg_scale)
244
+ max_pixels = int(max_pixels)
 
 
 
 
245
 
246
+ t_start = time.time()
247
+ chunk = read_chunk(video_path)
248
  h0, w0 = chunk[0].shape[:2]
249
+ width, height = adaptive_dims(w0, h0, max_pixels)
 
 
250
  source = frames_to_tensor(chunk, width, height)
251
  preview = build_preview_grid(chunk)
252
+ print(f"[run] {width}x{height}x{NUM_FRAMES} | steps={num_inference_steps} cfg={cfg_scale} seed={seed}", flush=True)
253
 
254
+ with torch.no_grad():
255
+ tiled = height * width >= 700_000
256
+ ref_latents = wan_vae.encode([source.to(dtype=DTYPE)], device="cuda", tiled=tiled).to(
257
+ device="cuda", dtype=DTYPE
 
 
 
 
 
 
 
 
 
 
 
 
258
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
+ embedder = QwenImageUnit_PromptEmbedder()
261
+ emb = embedder.process(pipe, prompt=instruction, edit_image=preview)
262
+ use_cfg = cfg_scale > 1.0
263
+ neg_emb = embedder.process(pipe, prompt=negative_prompt, edit_image=preview) if use_cfg else None
264
+
265
+ group = getattr(in_proj, "group", 1)
266
+ noise_seq_len = (ref_latents.shape[2] // group) * (ref_latents.shape[3] // 2) * (ref_latents.shape[4] // 2)
267
+ pipe.scheduler.set_timesteps(num_inference_steps, dynamic_shift_len=noise_seq_len)
268
+ gen = torch.Generator(device="cpu").manual_seed(seed)
269
+ latents = torch.randn(ref_latents.shape, generator=gen).to(device="cuda", dtype=DTYPE)
270
+
271
+ def forward(e, timestep):
272
+ return model_fn_video_tokens(
273
+ dit,
274
+ in_proj,
275
+ out_proj,
276
+ latents=latents,
277
+ ref_latents=ref_latents,
278
+ prompt_emb=e["prompt_emb"],
279
+ prompt_emb_mask=e["prompt_emb_mask"],
280
+ timestep=timestep,
281
+ latent_grid=LATENT_GRID,
282
+ zero_cond_t=ZERO_COND_T,
283
+ pe_mode=PE_MODE,
284
+ )
285
 
286
+ t_denoise = time.time()
287
+ for pid, timestep in enumerate(tqdm(pipe.scheduler.timesteps, desc="denoise")):
288
+ timestep = timestep.unsqueeze(0).to(dtype=DTYPE, device="cuda")
 
289
  noise_pred = forward(emb, timestep)
290
  if use_cfg:
291
+ # True CFG with a norm-preserving rescale over the latent
292
+ # channel dim (exactly as in the repo's infer.py).
293
  neg_pred = forward(neg_emb, timestep)
294
  comb = neg_pred + cfg_scale * (noise_pred - neg_pred)
295
+ comb = comb * (torch.norm(noise_pred, dim=1, keepdim=True) / torch.norm(comb, dim=1, keepdim=True))
 
 
 
296
  noise_pred = comb
297
+ latents = pipe.scheduler.step(noise_pred, pipe.scheduler.timesteps[pid], latents)
298
+ denoise_s = time.time() - t_denoise
299
+
300
+ tiled = (latents.shape[3] * 8) * (latents.shape[4] * 8) >= 700_000
301
+ video = wan_vae.decode(latents, device="cuda", tiled=tiled)[0].cpu()
302
+
303
+ out_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
304
+ iio.imwrite(out_path, np.stack(tensor_to_uint8_frames(video)), fps=FPS, codec="libx264")
305
+ total = time.time() - t_start
306
+ forwards = num_inference_steps * (2 if cfg_scale > 1.0 else 1)
307
+ print(
308
+ f"[run] done in {total:.1f}s (denoise {denoise_s:.1f}s over {forwards} forwards "
309
+ f"= {denoise_s / max(forwards, 1):.2f}s/forward)",
310
+ flush=True,
311
+ )
312
+ return out_path, seed
313
+
314
+
315
+ def run_example(video_path: str, instruction: str):
316
+ """Run an example row: edit a bundled clip with its instruction, all other
317
+ settings left at their defaults."""
318
+ return edit_video(video_path, instruction)
319
+
320
+
321
+ # --------------------------------------------------------------------------
322
+ # UI
323
+ # --------------------------------------------------------------------------
324
+ EXAMPLES = [
325
+ ["examples/man_dancing.mp4", "Turn the dancer into a glowing neon hologram"],
326
+ [
327
+ "examples/ocean_waves.mp4",
328
+ "Convert the video into a soft-focus, impressionist painting with brushstrokes "
329
+ "mimicking the movement of the waves and the warmth of the sunset.",
330
+ ],
331
+ ["examples/pottery_wheel.mp4", "Imitate the look of the Ghibli style."],
332
+ [
333
+ "examples/city_traffic_night.mp4",
334
+ "Convert the urban landscape into a retro-futuristic cityscape rendered in "
335
+ "neon-drenched 1980s synthwave style, with glowing rooftops, floating vehicles, "
336
+ "and chromatic halos around buildings.",
337
+ ],
338
+ ]
339
 
 
340
  CSS = """
341
  #col-container { max-width: 1100px; margin: 0 auto; }
 
342
  """
343
 
344
  with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
345
  with gr.Column(elem_id="col-container"):
 
346
  gr.Markdown(
347
+ """
348
+ # 🎬 Qwen-Video-Edit
349
+
350
+ **Instruction-based video editing by repurposing an image editing model.**
351
+ Qwen-Image-Edit's DiT edits Wan 2.1 video-VAE latents directly, bridged by two tiny
352
+ projections warm-started from the DiT's own input/output layers — no video-pretrained
353
+ transformer required.
354
+
355
+ [Paper](https://huggingface.co/papers/2608.14790) ·
356
+ [Project page](https://yunpeng1998.github.io/Qwen-Video-Edit-Page) ·
357
+ [Code](https://github.com/yunpeng1998/Qwen-Video-Edit) ·
358
+ [Weights](https://huggingface.co/yunpeng1998/Qwen-Video-Edit)
359
 
360
+ The model edits one **45-frame (≈2.8 s) chunk at 360p** per run — that is the window the
361
+ released checkpoint was trained on.
362
+ """
363
+ )
364
  with gr.Row():
365
+ with gr.Column():
366
+ video_in = gr.Video(label="Source video", height=340)
367
+ instruction = gr.Textbox(
368
+ label="Editing instruction",
369
+ placeholder="e.g. Make it Ghibli style.",
 
 
 
370
  lines=2,
371
  )
372
+ run_btn = gr.Button("Edit video", variant="primary")
373
+ with gr.Column():
374
+ video_out = gr.Video(label="Edited video", height=340, autoplay=True, loop=True)
375
+ seed_out = gr.Number(label="Seed used", interactive=False)
376
+
377
+ with gr.Accordion("Advanced settings", open=False):
378
+ with gr.Row():
379
+ steps = gr.Slider(
380
+ label="Denoising steps", minimum=8, maximum=40, step=1, value=DEFAULT_STEPS
381
+ )
382
+ cfg = gr.Slider(
383
+ label="True-CFG scale (>1 doubles the compute)",
384
+ minimum=1.0,
385
+ maximum=7.0,
386
+ step=0.1,
387
+ value=DEFAULT_CFG,
388
+ )
389
+ negative = gr.Textbox(label="Negative prompt", value=DEFAULT_NEG)
390
+ with gr.Row():
391
+ seed_in = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=DEFAULT_SEED)
392
+ randomize = gr.Checkbox(label="Randomize seed", value=False)
393
+ pixels = gr.Radio(
394
+ label="Resolution",
395
+ choices=[("360p — native training resolution", NATIVE_PIXELS), ("256p — faster", 122880)],
396
+ value=DEFAULT_PIXELS,
397
  )
398
 
399
  gr.Examples(
400
+ examples=EXAMPLES,
401
+ inputs=[video_in, instruction],
402
+ outputs=[video_out, seed_out],
403
+ fn=run_example,
404
+ cache_examples=True,
405
+ cache_mode="lazy",
406
+ label="Examples (instructions from the authors' prompt list)",
 
 
 
 
407
  )
408
 
409
+ gr.on(
410
+ triggers=[run_btn.click, instruction.submit],
411
  fn=edit_video,
412
+ inputs=[video_in, instruction, steps, cfg, negative, seed_in, randomize, pixels],
413
+ outputs=[video_out, seed_out],
 
414
  )
415
 
416
+ demo.launch(mcp_server=True)
diffsynth/core/loader/model.py CHANGED
@@ -20,7 +20,7 @@ def load_model(model_class, path, config=None, torch_dtype=torch.bfloat16, devic
20
  dtypes = [vram_config["offload_dtype"], vram_config["onload_dtype"], vram_config["preparing_dtype"], vram_config["computation_dtype"]]
21
  dtype = [d for d in dtypes if d != "disk"][0]
22
  if vram_config["offload_device"] != "disk":
23
- if state_dict is None: state_dict = DiskMap(path, "cpu", torch_dtype=dtype)
24
  if state_dict_converter is not None:
25
  state_dict = state_dict_converter(state_dict)
26
  else:
@@ -28,7 +28,7 @@ def load_model(model_class, path, config=None, torch_dtype=torch.bfloat16, devic
28
  model.load_state_dict(state_dict, assign=True)
29
  model = enable_vram_management(model, module_map, vram_config=vram_config, disk_map=None, vram_limit=vram_limit)
30
  else:
31
- disk_map = DiskMap(path, "cpu", state_dict_converter=state_dict_converter)
32
  model = enable_vram_management(model, module_map, vram_config=vram_config, disk_map=disk_map, vram_limit=vram_limit)
33
  else:
34
  # Why do we use `DiskMap`?
@@ -38,11 +38,9 @@ def load_model(model_class, path, config=None, torch_dtype=torch.bfloat16, devic
38
  if state_dict is not None:
39
  pass
40
  elif use_disk_map:
41
- # On ZeroGPU, always load to CPU first — the .to(device) below
42
- # is intercepted by the spaces hijack to pack weights to disk.
43
- state_dict = DiskMap(path, "cpu", torch_dtype=torch_dtype)
44
  else:
45
- state_dict = load_state_dict(path, torch_dtype, "cpu")
46
  # Why do we use `state_dict_converter`?
47
  # Some models are saved in complex formats,
48
  # and we need to convert the state dict into the appropriate format.
@@ -79,7 +77,7 @@ def load_model_with_disk_offload(model_class, path, config=None, torch_dtype=tor
79
  model = model_class(**config)
80
  if hasattr(model, "eval"):
81
  model = model.eval()
82
- disk_map = DiskMap(path, "cpu", state_dict_converter=state_dict_converter)
83
  vram_config = {
84
  "offload_dtype": "disk",
85
  "offload_device": "disk",
 
20
  dtypes = [vram_config["offload_dtype"], vram_config["onload_dtype"], vram_config["preparing_dtype"], vram_config["computation_dtype"]]
21
  dtype = [d for d in dtypes if d != "disk"][0]
22
  if vram_config["offload_device"] != "disk":
23
+ if state_dict is None: state_dict = DiskMap(path, device, torch_dtype=dtype)
24
  if state_dict_converter is not None:
25
  state_dict = state_dict_converter(state_dict)
26
  else:
 
28
  model.load_state_dict(state_dict, assign=True)
29
  model = enable_vram_management(model, module_map, vram_config=vram_config, disk_map=None, vram_limit=vram_limit)
30
  else:
31
+ disk_map = DiskMap(path, device, state_dict_converter=state_dict_converter)
32
  model = enable_vram_management(model, module_map, vram_config=vram_config, disk_map=disk_map, vram_limit=vram_limit)
33
  else:
34
  # Why do we use `DiskMap`?
 
38
  if state_dict is not None:
39
  pass
40
  elif use_disk_map:
41
+ state_dict = DiskMap(path, device, torch_dtype=torch_dtype)
 
 
42
  else:
43
+ state_dict = load_state_dict(path, torch_dtype, device)
44
  # Why do we use `state_dict_converter`?
45
  # Some models are saved in complex formats,
46
  # and we need to convert the state dict into the appropriate format.
 
77
  model = model_class(**config)
78
  if hasattr(model, "eval"):
79
  model = model.eval()
80
+ disk_map = DiskMap(path, device, state_dict_converter=state_dict_converter)
81
  vram_config = {
82
  "offload_dtype": "disk",
83
  "offload_device": "disk",
diffsynth/core/vram/disk_map.py CHANGED
@@ -47,13 +47,13 @@ class DiskMap:
47
  if len(self.files) == 0:
48
  for path in self.path:
49
  if path.endswith(".safetensors"):
50
- self.files.append(safe_open(path, framework="pt", device="cpu"))
51
  else:
52
- self.files.append(SafetensorsCompatibleBinaryLoader(path, device="cpu"))
53
  else:
54
  for i, path in enumerate(self.path):
55
  if path.endswith(".safetensors"):
56
- self.files[i] = safe_open(path, framework="pt", device="cpu")
57
  self.num_params = 0
58
 
59
  def __getitem__(self, name):
 
47
  if len(self.files) == 0:
48
  for path in self.path:
49
  if path.endswith(".safetensors"):
50
+ self.files.append(safe_open(path, framework="pt", device=str(self.device)))
51
  else:
52
+ self.files.append(SafetensorsCompatibleBinaryLoader(path, device=self.device))
53
  else:
54
  for i, path in enumerate(self.path):
55
  if path.endswith(".safetensors"):
56
+ self.files[i] = safe_open(path, framework="pt", device=str(self.device))
57
  self.num_params = 0
58
 
59
  def __getitem__(self, name):
diffsynth/models/qwen_image_dit.py CHANGED
@@ -22,12 +22,6 @@ try:
22
  except ModuleNotFoundError:
23
  FLASH_ATTN_3_AVAILABLE = False
24
 
25
- try:
26
- from flash_attn import flash_attn_func as flash_attn_2_func
27
- FLASH_ATTN_2_AVAILABLE = True
28
- except ModuleNotFoundError:
29
- FLASH_ATTN_2_AVAILABLE = False
30
-
31
 
32
  def qwen_image_flash_attention(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, num_heads: int, attention_mask = None, enable_fp8_attention: bool = False):
33
  if FLASH_ATTN_3_AVAILABLE and attention_mask is None:
@@ -51,14 +45,6 @@ def qwen_image_flash_attention(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor
51
  x = x[0]
52
  x = x.to(origin_dtype) * v_std
53
  x = rearrange(x, "b s n d -> b s (n d)", n=num_heads)
54
- elif FLASH_ATTN_2_AVAILABLE and attention_mask is None:
55
- q = rearrange(q, "b n s d -> b s n d", n=num_heads)
56
- k = rearrange(k, "b n s d -> b s n d", n=num_heads)
57
- v = rearrange(v, "b n s d -> b s n d", n=num_heads)
58
- x = flash_attn_2_func(q, k, v)
59
- if isinstance(x, tuple):
60
- x = x[0]
61
- x = rearrange(x, "b s n d -> b s (n d)", n=num_heads)
62
  else:
63
  x = torch.nn.functional.scaled_dot_product_attention(q, k, v, attn_mask=attention_mask)
64
  x = rearrange(x, "b n s d -> b s (n d)", n=num_heads)
 
22
  except ModuleNotFoundError:
23
  FLASH_ATTN_3_AVAILABLE = False
24
 
 
 
 
 
 
 
25
 
26
  def qwen_image_flash_attention(q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, num_heads: int, attention_mask = None, enable_fp8_attention: bool = False):
27
  if FLASH_ATTN_3_AVAILABLE and attention_mask is None:
 
45
  x = x[0]
46
  x = x.to(origin_dtype) * v_std
47
  x = rearrange(x, "b s n d -> b s (n d)", n=num_heads)
 
 
 
 
 
 
 
 
48
  else:
49
  x = torch.nn.functional.scaled_dot_product_attention(q, k, v, attn_mask=attention_mask)
50
  x = rearrange(x, "b n s d -> b s (n d)", n=num_heads)
examples/city_traffic_night.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ed5ce2a6c4c7b951ff34d5d9ae282080c2f19d3864b5efa4ccc79b1ce9092b2c
3
+ size 335551
examples/man_dancing.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6061764e56dd8cb6220894c254a639f0547869ea2609a9c971ee7dc9940fcc10
3
+ size 333527
examples/ocean_waves.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:477dfe9c68d1ba5525320c07e597963d657ed14471ece6343fc667f865c209a5
3
+ size 1366054
examples/pottery_wheel.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4fbb21011083c2a5e7f417dcce6aa92a54d124b2d4e2d926476875df6d4850fa
3
+ size 380123
requirements.txt CHANGED
@@ -1,7 +1,8 @@
1
- torch
2
  torchvision
 
3
  transformers
4
  accelerate
 
5
  safetensors
6
  einops
7
  imageio
@@ -9,9 +10,9 @@ imageio[ffmpeg]
9
  av
10
  numpy
11
  Pillow
12
- modelscope
13
  tqdm
 
 
 
14
  sentencepiece
15
- diffusers
16
- peft
17
- https://huggingface.co/datasets/multimodalart/zerogpu-blackwell-wheels/resolve/main/wheels/pt211-cu130-cp312/flash_attn-2.8.3-cp312-cp312-linux_x86_64.whl
 
 
1
  torchvision
2
+ torchao
3
  transformers
4
  accelerate
5
+ peft
6
  safetensors
7
  einops
8
  imageio
 
10
  av
11
  numpy
12
  Pillow
13
+ pandas
14
  tqdm
15
+ modelscope
16
+ ftfy
17
+ regex
18
  sentencepiece