umer1995 commited on
Commit
732d269
·
verified ·
1 Parent(s): 957aeac

Deploy NFA Track R FLUX.2 Fun CN ZeroGPU (real depth CN)

Browse files
Files changed (1) hide show
  1. app.py +118 -163
app.py CHANGED
@@ -4,12 +4,16 @@ REAL Fun ControlNet Union depth — NOT soft Flux2 image=depth (banned forever).
4
 
5
  VRAM choice (documented):
6
  size=\"large\" (48GB, 1× Pro) + VideoX-Fun model_cpu_offload_and_qfloat8.
 
 
7
  Escalation if OOM: size=\"xlarge\" + model_cpu_offload (2× quota).
8
  """
9
 
10
  from __future__ import annotations
11
 
12
  import os
 
 
13
  import sys
14
  import traceback
15
  from pathlib import Path
@@ -23,12 +27,11 @@ from omegaconf import OmegaConf
23
  from PIL import Image
24
 
25
  APP_DIR = Path(__file__).resolve().parent
26
- # Prefer vendored tree; else clone VideoX-Fun on Space (pip package omits submodules).
27
  _VX = APP_DIR / "vendor" / "VideoX-Fun"
28
  _VX_CACHE = Path.home() / "VideoX-Fun"
29
 
30
 
31
- def _patch_videox_models_init(root: Path) -> None:
32
  models_init = root / "videox_fun" / "models" / "__init__.py"
33
  if models_init.is_file():
34
  text = models_init.read_text(encoding="utf-8", errors="replace")
@@ -58,7 +61,7 @@ def _patch_videox_models_init(root: Path) -> None:
58
  "]\n",
59
  encoding="utf-8",
60
  )
61
- print("[nfa-fun-cn] patched videox_fun.models.__init__ (Flux2-only)", flush=True)
62
 
63
  pipe_init = root / "videox_fun" / "pipeline" / "__init__.py"
64
  if pipe_init.is_file():
@@ -69,24 +72,19 @@ def _patch_videox_models_init(root: Path) -> None:
69
  "__all__ = ['Flux2ControlPipeline']\n",
70
  encoding="utf-8",
71
  )
72
- print("[nfa-fun-cn] patched videox_fun.pipeline.__init__ (Flux2-only)", flush=True)
73
 
74
 
75
  def _ensure_videox_on_path() -> None:
76
  for candidate in (_VX, _VX_CACHE):
77
  if (candidate / "videox_fun" / "models").is_dir():
78
- _patch_videox_models_init(candidate)
79
  p = str(candidate)
80
  if p not in sys.path:
81
  sys.path.insert(0, p)
82
  return
83
- import subprocess
84
-
85
  print("[nfa-fun-cn] cloning VideoX-Fun for Fun CN runtime…", flush=True)
86
- _VX_CACHE.parent.mkdir(parents=True, exist_ok=True)
87
  if _VX_CACHE.exists():
88
- import shutil
89
-
90
  shutil.rmtree(_VX_CACHE, ignore_errors=True)
91
  subprocess.check_call(
92
  [
@@ -98,16 +96,14 @@ def _ensure_videox_on_path() -> None:
98
  str(_VX_CACHE),
99
  ]
100
  )
101
- _patch_videox_models_init(_VX_CACHE)
102
  sys.path.insert(0, str(_VX_CACHE))
103
 
104
 
105
  _ensure_videox_on_path()
106
 
107
  HF_TOKEN = (
108
- os.environ.get("HF_TOKEN")
109
- or os.environ.get("HUGGINGFACE_HUB_TOKEN")
110
- or ""
111
  ).strip()
112
  if HF_TOKEN:
113
  try:
@@ -124,13 +120,11 @@ CN_REPO = os.environ.get(
124
  CN_FILE = os.environ.get(
125
  "NFA_FUN_CN_FILE", "FLUX.2-dev-Fun-Controlnet-Union-2602.safetensors"
126
  ).strip()
127
- # large=48GB 1×; set NFA_FUN_CN_GPU_SIZE=xlarge only after OOM on large
128
  GPU_SIZE = (os.environ.get("NFA_FUN_CN_GPU_SIZE") or "large").strip().lower()
129
  if GPU_SIZE not in ("large", "xlarge"):
130
  GPU_SIZE = "large"
131
  GPU_DURATION = int(os.environ.get("NFA_FUN_CN_GPU_DURATION") or "300")
132
  WEIGHT_DTYPE = torch.bfloat16
133
- # VideoX-Fun official low-VRAM Fun CN mode for 48GB; offload-only on xlarge
134
  MEM_MODE = (
135
  os.environ.get("NFA_FUN_CN_MEM_MODE")
136
  or (
@@ -141,63 +135,47 @@ MEM_MODE = (
141
  ).strip()
142
 
143
  CONFIG_PATH = APP_DIR / "config" / "flux2_control.yaml"
144
- # Prefer HF Mount volumes (no 178GB ephemeral download). Fallback: cache download.
145
- MODEL_DIR = Path(
146
- os.environ.get("NFA_FLUX2_MOUNT") or "/data/FLUX.2-dev"
147
- )
148
  CN_MOUNT_DIR = Path(os.environ.get("NFA_FUN_CN_MOUNT") or "/data/Fun-CN")
149
  CACHE_ROOT = Path(
150
- os.environ.get("NFA_FUN_CN_CACHE")
151
- or (Path.home() / ".cache" / "nfa_fun_cn")
152
  )
153
 
154
  _PIPE = None
 
155
  _CN_FILE_PATH: Path | None = None
156
- _WEIGHTS_READY = False
157
 
158
 
159
  def _resolve_cn_path() -> Path:
160
  global _CN_FILE_PATH
161
  if _CN_FILE_PATH is not None and _CN_FILE_PATH.is_file():
162
  return _CN_FILE_PATH
163
- candidates = [
164
- CN_MOUNT_DIR / CN_FILE,
165
- CACHE_ROOT / CN_FILE,
166
- ]
167
- candidates.extend(CN_MOUNT_DIR.rglob(CN_FILE) if CN_MOUNT_DIR.is_dir() else [])
168
- candidates.extend(CACHE_ROOT.rglob(CN_FILE) if CACHE_ROOT.is_dir() else [])
169
  for c in candidates:
170
  if c.is_file():
171
  _CN_FILE_PATH = c
172
  return c
173
  raise FileNotFoundError(
174
  f"Fun CN weights missing: {CN_FILE}. "
175
- "Mount alibaba-pai/FLUX.2-dev-Fun-Controlnet-Union at /data/Fun-CN "
176
- "or download into cache."
177
  )
178
 
179
 
180
  def _ensure_weights() -> None:
181
- """Resolve mounted Hub volumes, or (last resort) selective download."""
182
- global _WEIGHTS_READY, MODEL_DIR, _CN_FILE_PATH
183
  if MODEL_DIR.is_dir() and (MODEL_DIR / "model_index.json").is_file():
184
  try:
185
- cn_path = _resolve_cn_path()
186
- _WEIGHTS_READY = True
187
- print(
188
- f"[nfa-fun-cn] using mounts model={MODEL_DIR} cn={cn_path}",
189
- flush=True,
190
- )
191
  return
192
  except FileNotFoundError:
193
  pass
194
-
195
- # Fallback: selective download (may OOM disk on ZeroGPU — mounts preferred)
196
- print(
197
- "[nfa-fun-cn] WARN mounts missing; selective download fallback "
198
- "(transformer+vae+tokenizer+text_encoder+scheduler only)",
199
- flush=True,
200
- )
201
  cache_model = CACHE_ROOT / "FLUX.2-dev"
202
  CACHE_ROOT.mkdir(parents=True, exist_ok=True)
203
  token = HF_TOKEN or None
@@ -215,15 +193,10 @@ def _ensure_weights() -> None:
215
  ],
216
  )
217
  path = hf_hub_download(
218
- repo_id=CN_REPO,
219
- filename=CN_FILE,
220
- local_dir=str(CACHE_ROOT),
221
- token=token,
222
  )
223
  MODEL_DIR = cache_model
224
  _CN_FILE_PATH = Path(path)
225
- _WEIGHTS_READY = True
226
- print(f"[nfa-fun-cn] weights ready model={MODEL_DIR} cn={_CN_FILE_PATH}", flush=True)
227
 
228
 
229
  def _prep_depth(depth_image: Image.Image, width: int, height: int) -> Image.Image:
@@ -234,103 +207,95 @@ def _prep_depth(depth_image: Image.Image, width: int, height: int) -> Image.Imag
234
 
235
 
236
  def _compose_prompt(positive: str, negative: str) -> tuple[str, str]:
237
- pos = (positive or "").strip()
238
- neg = (negative or "").strip() or " "
239
- return pos, neg
240
-
241
-
242
- def get_pipe():
243
- """Build VideoX-Fun Flux2ControlPipeline once per warm process."""
244
- global _PIPE
245
- if _PIPE is not None:
246
- return _PIPE
247
-
248
- _ensure_weights()
249
- _ensure_videox_on_path()
250
- from diffusers import FlowMatchEulerDiscreteScheduler
251
- from safetensors.torch import load_file
252
- from transformers import Mistral3ForConditionalGeneration, PixtralProcessor
253
-
254
- # Direct imports — avoid videox_fun.models.__init__ (pulls librosa/audio stacks).
255
- from videox_fun.models.flux2_transformer2d_control import (
256
- Flux2ControlTransformer2DModel,
257
- )
258
- from videox_fun.models.flux2_vae import AutoencoderKLFlux2
259
- from videox_fun.pipeline.pipeline_flux2_control import Flux2ControlPipeline
260
- from videox_fun.utils.fp8_optimization import (
261
- convert_model_weight_to_float8,
262
- convert_weight_dtype_wrapper,
263
- )
264
- from videox_fun.utils.utils import get_image_latent
265
 
266
- # stash for generate
267
- get_pipe._get_image_latent = get_image_latent # type: ignore[attr-defined]
268
 
269
- model_name = str(MODEL_DIR)
270
- cn_file = str(_resolve_cn_path())
271
- config = OmegaConf.load(str(CONFIG_PATH))
272
- print(
273
- f"[nfa-fun-cn] load Flux2ControlTransformer + Fun CN "
274
- f"mem={MEM_MODE} size={GPU_SIZE}",
275
- flush=True,
276
- )
277
- transformer = Flux2ControlTransformer2DModel.from_pretrained(
278
- model_name,
279
- subfolder="transformer",
280
- low_cpu_mem_usage=True,
281
- torch_dtype=WEIGHT_DTYPE,
282
- transformer_additional_kwargs=OmegaConf.to_container(
283
- config["transformer_additional_kwargs"]
284
- ),
285
- ).to(WEIGHT_DTYPE)
286
-
287
- state_dict = load_file(cn_file)
288
- state_dict = state_dict["state_dict"] if "state_dict" in state_dict else state_dict
289
- missing, unexpected = transformer.load_state_dict(state_dict, strict=False)
290
- print(
291
- f"[nfa-fun-cn] Fun CN loaded missing={len(missing)} unexpected={len(unexpected)}",
292
- flush=True,
293
- )
294
-
295
- vae = AutoencoderKLFlux2.from_pretrained(model_name, subfolder="vae").to(
296
- WEIGHT_DTYPE
297
- )
298
- tokenizer = PixtralProcessor.from_pretrained(model_name, subfolder="tokenizer")
299
- text_encoder = Mistral3ForConditionalGeneration.from_pretrained(
300
- model_name,
301
- subfolder="text_encoder",
302
- torch_dtype=WEIGHT_DTYPE,
303
- low_cpu_mem_usage=True,
304
- )
305
- scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
306
- model_name, subfolder="scheduler"
307
- )
308
- pipeline = Flux2ControlPipeline(
309
- vae=vae,
310
- tokenizer=tokenizer,
311
- text_encoder=text_encoder,
312
- transformer=transformer,
313
- scheduler=scheduler,
314
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
 
316
- device = "cuda" if torch.cuda.is_available() else "cpu"
317
- if MEM_MODE == "model_cpu_offload_and_qfloat8":
318
- convert_model_weight_to_float8(
319
- transformer,
320
- exclude_module_name=["img_in", "txt_in", "timestep"],
321
- device=device,
322
  )
323
- convert_weight_dtype_wrapper(transformer, WEIGHT_DTYPE)
324
- pipeline.enable_model_cpu_offload(device=device)
325
- elif MEM_MODE == "sequential_cpu_offload":
326
- pipeline.enable_sequential_cpu_offload(device=device)
327
- elif MEM_MODE == "model_cpu_offload":
328
- pipeline.enable_model_cpu_offload(device=device)
329
- else:
330
- pipeline.to(device=device)
331
-
332
- _PIPE = pipeline
333
- print("[nfa-fun-cn] Flux2ControlPipeline ready (REAL Fun CN)", flush=True)
 
 
 
 
 
 
 
 
334
  return _PIPE
335
 
336
 
@@ -346,15 +311,13 @@ def _generate_still_gpu(
346
  guidance: float,
347
  cn_strength: float,
348
  ) -> Image.Image:
349
- """GPU-billed Fun CN infer only — weights must already be on disk."""
350
  if torch.cuda.is_available():
351
  free, total = torch.cuda.mem_get_info()
352
  print(
353
  f"[nfa-fun-cn] cuda free={free/1e9:.1f}G total={total/1e9:.1f}G "
354
- f"duration={GPU_DURATION} size={GPU_SIZE} mem={MEM_MODE}",
355
  flush=True,
356
  )
357
-
358
  w = int(width) if width else 1216
359
  h = int(height) if height else 832
360
  w -= w % 16
@@ -362,22 +325,13 @@ def _generate_still_gpu(
362
  prompt, neg = _compose_prompt(positive, negative)
363
  if not prompt:
364
  raise gr.Error("positive prompt is required")
365
-
366
  depth = _prep_depth(depth_image, w, h)
367
- pipe = get_pipe()
368
- get_image_latent = get_pipe._get_image_latent # type: ignore[attr-defined]
369
-
370
- # VideoX-Fun control latents (NOT Flux2Pipeline soft image=)
371
- control_latent = get_image_latent(depth, sample_size=[h, w])[:, :, 0]
372
  inpaint_image = torch.zeros([1, 3, h, w])
373
  mask_image = torch.ones([1, 1, h, w]) * 255
374
-
375
- strength = float(cn_strength)
376
- if strength <= 0:
377
- strength = 0.75
378
- # ALIMAMA recommended band 0.65–0.80; allow Track R packet values
379
  strength = max(0.05, min(1.5, strength))
380
-
381
  device = "cuda" if torch.cuda.is_available() else "cpu"
382
  generator = torch.Generator(device=device).manual_seed(int(seed))
383
  print(
@@ -414,13 +368,14 @@ def generate_still(
414
  guidance: float = 4.0,
415
  cn_strength: float = 0.75,
416
  ) -> Image.Image:
417
- """CPU download + GPU Fun CN. Soft image=depth is never used."""
418
  try:
419
  if depth_image is None:
420
  raise gr.Error(
421
  "FUN_CN_REQUIRES_DEPTH: depth_image is required for real Fun ControlNet."
422
  )
423
  _ensure_weights()
 
 
424
  return _generate_still_gpu(
425
  positive,
426
  negative or "",
@@ -444,10 +399,10 @@ with gr.Blocks(title="NFA Track R FLUX.2 Fun CN ZeroGPU") as demo:
444
  gr.Markdown(
445
  "## NFA Track R — **Real Fun depth ControlNet** (ZeroGPU)\n"
446
  f"- Stack: VideoX-Fun `Flux2ControlPipeline` + `{CN_FILE}`\n"
447
- f"- Base: `{BASE_MODEL}`\n"
448
  f"- GPU: `size={GPU_SIZE}` duration={GPU_DURATION}s mem=`{MEM_MODE}`\n"
449
  "- Soft `image=depth` is **banned** on this Space.\n"
450
- "- First GPU call loads Fun CN (slow once)."
451
  )
452
  with gr.Row():
453
  with gr.Column():
 
4
 
5
  VRAM choice (documented):
6
  size=\"large\" (48GB, 1× Pro) + VideoX-Fun model_cpu_offload_and_qfloat8.
7
+ Weights via HF Mount volumes (not 178GB ephemeral download).
8
+ CPU-preload outside @spaces.GPU so ZeroGPU minutes are not burned on load.
9
  Escalation if OOM: size=\"xlarge\" + model_cpu_offload (2× quota).
10
  """
11
 
12
  from __future__ import annotations
13
 
14
  import os
15
+ import shutil
16
+ import subprocess
17
  import sys
18
  import traceback
19
  from pathlib import Path
 
27
  from PIL import Image
28
 
29
  APP_DIR = Path(__file__).resolve().parent
 
30
  _VX = APP_DIR / "vendor" / "VideoX-Fun"
31
  _VX_CACHE = Path.home() / "VideoX-Fun"
32
 
33
 
34
+ def _patch_videox_inits(root: Path) -> None:
35
  models_init = root / "videox_fun" / "models" / "__init__.py"
36
  if models_init.is_file():
37
  text = models_init.read_text(encoding="utf-8", errors="replace")
 
61
  "]\n",
62
  encoding="utf-8",
63
  )
64
+ print("[nfa-fun-cn] patched videox_fun.models.__init__", flush=True)
65
 
66
  pipe_init = root / "videox_fun" / "pipeline" / "__init__.py"
67
  if pipe_init.is_file():
 
72
  "__all__ = ['Flux2ControlPipeline']\n",
73
  encoding="utf-8",
74
  )
75
+ print("[nfa-fun-cn] patched videox_fun.pipeline.__init__", flush=True)
76
 
77
 
78
  def _ensure_videox_on_path() -> None:
79
  for candidate in (_VX, _VX_CACHE):
80
  if (candidate / "videox_fun" / "models").is_dir():
81
+ _patch_videox_inits(candidate)
82
  p = str(candidate)
83
  if p not in sys.path:
84
  sys.path.insert(0, p)
85
  return
 
 
86
  print("[nfa-fun-cn] cloning VideoX-Fun for Fun CN runtime…", flush=True)
 
87
  if _VX_CACHE.exists():
 
 
88
  shutil.rmtree(_VX_CACHE, ignore_errors=True)
89
  subprocess.check_call(
90
  [
 
96
  str(_VX_CACHE),
97
  ]
98
  )
99
+ _patch_videox_inits(_VX_CACHE)
100
  sys.path.insert(0, str(_VX_CACHE))
101
 
102
 
103
  _ensure_videox_on_path()
104
 
105
  HF_TOKEN = (
106
+ os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACE_HUB_TOKEN") or ""
 
 
107
  ).strip()
108
  if HF_TOKEN:
109
  try:
 
120
  CN_FILE = os.environ.get(
121
  "NFA_FUN_CN_FILE", "FLUX.2-dev-Fun-Controlnet-Union-2602.safetensors"
122
  ).strip()
 
123
  GPU_SIZE = (os.environ.get("NFA_FUN_CN_GPU_SIZE") or "large").strip().lower()
124
  if GPU_SIZE not in ("large", "xlarge"):
125
  GPU_SIZE = "large"
126
  GPU_DURATION = int(os.environ.get("NFA_FUN_CN_GPU_DURATION") or "300")
127
  WEIGHT_DTYPE = torch.bfloat16
 
128
  MEM_MODE = (
129
  os.environ.get("NFA_FUN_CN_MEM_MODE")
130
  or (
 
135
  ).strip()
136
 
137
  CONFIG_PATH = APP_DIR / "config" / "flux2_control.yaml"
138
+ MODEL_DIR = Path(os.environ.get("NFA_FLUX2_MOUNT") or "/data/FLUX.2-dev")
 
 
 
139
  CN_MOUNT_DIR = Path(os.environ.get("NFA_FUN_CN_MOUNT") or "/data/Fun-CN")
140
  CACHE_ROOT = Path(
141
+ os.environ.get("NFA_FUN_CN_CACHE") or (Path.home() / ".cache" / "nfa_fun_cn")
 
142
  )
143
 
144
  _PIPE = None
145
+ _PIPE_OFFLOAD_READY = False
146
  _CN_FILE_PATH: Path | None = None
147
+ _GET_IMAGE_LATENT = None
148
 
149
 
150
  def _resolve_cn_path() -> Path:
151
  global _CN_FILE_PATH
152
  if _CN_FILE_PATH is not None and _CN_FILE_PATH.is_file():
153
  return _CN_FILE_PATH
154
+ candidates = [CN_MOUNT_DIR / CN_FILE, CACHE_ROOT / CN_FILE]
155
+ if CN_MOUNT_DIR.is_dir():
156
+ candidates.extend(CN_MOUNT_DIR.rglob(CN_FILE))
157
+ if CACHE_ROOT.is_dir():
158
+ candidates.extend(CACHE_ROOT.rglob(CN_FILE))
 
159
  for c in candidates:
160
  if c.is_file():
161
  _CN_FILE_PATH = c
162
  return c
163
  raise FileNotFoundError(
164
  f"Fun CN weights missing: {CN_FILE}. "
165
+ "Mount alibaba-pai/FLUX.2-dev-Fun-Controlnet-Union at /data/Fun-CN."
 
166
  )
167
 
168
 
169
  def _ensure_weights() -> None:
170
+ global MODEL_DIR, _CN_FILE_PATH
 
171
  if MODEL_DIR.is_dir() and (MODEL_DIR / "model_index.json").is_file():
172
  try:
173
+ cn = _resolve_cn_path()
174
+ print(f"[nfa-fun-cn] using mounts model={MODEL_DIR} cn={cn}", flush=True)
 
 
 
 
175
  return
176
  except FileNotFoundError:
177
  pass
178
+ print("[nfa-fun-cn] WARN mounts missing; selective download fallback", flush=True)
 
 
 
 
 
 
179
  cache_model = CACHE_ROOT / "FLUX.2-dev"
180
  CACHE_ROOT.mkdir(parents=True, exist_ok=True)
181
  token = HF_TOKEN or None
 
193
  ],
194
  )
195
  path = hf_hub_download(
196
+ repo_id=CN_REPO, filename=CN_FILE, local_dir=str(CACHE_ROOT), token=token
 
 
 
197
  )
198
  MODEL_DIR = cache_model
199
  _CN_FILE_PATH = Path(path)
 
 
200
 
201
 
202
  def _prep_depth(depth_image: Image.Image, width: int, height: int) -> Image.Image:
 
207
 
208
 
209
  def _compose_prompt(positive: str, negative: str) -> tuple[str, str]:
210
+ return (positive or "").strip(), ((negative or "").strip() or " ")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
 
 
212
 
213
+ def get_pipe(*, prepare_gpu_offload: bool = False):
214
+ """CPU-load Fun CN pipeline; arm GPU offload only inside @spaces.GPU."""
215
+ global _PIPE, _PIPE_OFFLOAD_READY, _GET_IMAGE_LATENT
216
+ if _PIPE is None:
217
+ _ensure_weights()
218
+ _ensure_videox_on_path()
219
+ from diffusers import FlowMatchEulerDiscreteScheduler
220
+ from safetensors.torch import load_file
221
+ from transformers import Mistral3ForConditionalGeneration, PixtralProcessor
222
+ from videox_fun.models.flux2_transformer2d_control import (
223
+ Flux2ControlTransformer2DModel,
224
+ )
225
+ from videox_fun.models.flux2_vae import AutoencoderKLFlux2
226
+ from videox_fun.pipeline.pipeline_flux2_control import Flux2ControlPipeline
227
+ from videox_fun.utils.utils import get_image_latent
228
+
229
+ _GET_IMAGE_LATENT = get_image_latent
230
+ model_name = str(MODEL_DIR)
231
+ cn_file = str(_resolve_cn_path())
232
+ config = OmegaConf.load(str(CONFIG_PATH))
233
+ print(
234
+ f"[nfa-fun-cn] CPU-load Flux2Control + Fun CN mem={MEM_MODE}",
235
+ flush=True,
236
+ )
237
+ transformer = Flux2ControlTransformer2DModel.from_pretrained(
238
+ model_name,
239
+ subfolder="transformer",
240
+ low_cpu_mem_usage=True,
241
+ torch_dtype=WEIGHT_DTYPE,
242
+ transformer_additional_kwargs=OmegaConf.to_container(
243
+ config["transformer_additional_kwargs"]
244
+ ),
245
+ ).to(WEIGHT_DTYPE)
246
+ state_dict = load_file(cn_file)
247
+ state_dict = state_dict["state_dict"] if "state_dict" in state_dict else state_dict
248
+ missing, unexpected = transformer.load_state_dict(state_dict, strict=False)
249
+ print(
250
+ f"[nfa-fun-cn] Fun CN loaded missing={len(missing)} unexpected={len(unexpected)}",
251
+ flush=True,
252
+ )
253
+ vae = AutoencoderKLFlux2.from_pretrained(model_name, subfolder="vae").to(
254
+ WEIGHT_DTYPE
255
+ )
256
+ tokenizer = PixtralProcessor.from_pretrained(model_name, subfolder="tokenizer")
257
+ text_encoder = Mistral3ForConditionalGeneration.from_pretrained(
258
+ model_name,
259
+ subfolder="text_encoder",
260
+ torch_dtype=WEIGHT_DTYPE,
261
+ low_cpu_mem_usage=True,
262
+ )
263
+ scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
264
+ model_name, subfolder="scheduler"
265
+ )
266
+ _PIPE = Flux2ControlPipeline(
267
+ vae=vae,
268
+ tokenizer=tokenizer,
269
+ text_encoder=text_encoder,
270
+ transformer=transformer,
271
+ scheduler=scheduler,
272
+ )
273
+ print("[nfa-fun-cn] Flux2ControlPipeline CPU-ready (REAL Fun CN)", flush=True)
274
 
275
+ if prepare_gpu_offload and not _PIPE_OFFLOAD_READY and torch.cuda.is_available():
276
+ from videox_fun.utils.fp8_optimization import (
277
+ convert_model_weight_to_float8,
278
+ convert_weight_dtype_wrapper,
 
 
279
  )
280
+
281
+ device = "cuda"
282
+ transformer = _PIPE.transformer
283
+ if MEM_MODE == "model_cpu_offload_and_qfloat8":
284
+ convert_model_weight_to_float8(
285
+ transformer,
286
+ exclude_module_name=["img_in", "txt_in", "timestep"],
287
+ device=device,
288
+ )
289
+ convert_weight_dtype_wrapper(transformer, WEIGHT_DTYPE)
290
+ _PIPE.enable_model_cpu_offload(device=device)
291
+ elif MEM_MODE == "sequential_cpu_offload":
292
+ _PIPE.enable_sequential_cpu_offload(device=device)
293
+ elif MEM_MODE == "model_cpu_offload":
294
+ _PIPE.enable_model_cpu_offload(device=device)
295
+ else:
296
+ _PIPE.to(device=device)
297
+ _PIPE_OFFLOAD_READY = True
298
+ print(f"[nfa-fun-cn] GPU offload armed mem={MEM_MODE}", flush=True)
299
  return _PIPE
300
 
301
 
 
311
  guidance: float,
312
  cn_strength: float,
313
  ) -> Image.Image:
 
314
  if torch.cuda.is_available():
315
  free, total = torch.cuda.mem_get_info()
316
  print(
317
  f"[nfa-fun-cn] cuda free={free/1e9:.1f}G total={total/1e9:.1f}G "
318
+ f"duration={GPU_DURATION} size={GPU_SIZE}",
319
  flush=True,
320
  )
 
321
  w = int(width) if width else 1216
322
  h = int(height) if height else 832
323
  w -= w % 16
 
325
  prompt, neg = _compose_prompt(positive, negative)
326
  if not prompt:
327
  raise gr.Error("positive prompt is required")
 
328
  depth = _prep_depth(depth_image, w, h)
329
+ pipe = get_pipe(prepare_gpu_offload=True)
330
+ control_latent = _GET_IMAGE_LATENT(depth, sample_size=[h, w])[:, :, 0]
 
 
 
331
  inpaint_image = torch.zeros([1, 3, h, w])
332
  mask_image = torch.ones([1, 1, h, w]) * 255
333
+ strength = float(cn_strength) if float(cn_strength) > 0 else 0.75
 
 
 
 
334
  strength = max(0.05, min(1.5, strength))
 
335
  device = "cuda" if torch.cuda.is_available() else "cpu"
336
  generator = torch.Generator(device=device).manual_seed(int(seed))
337
  print(
 
368
  guidance: float = 4.0,
369
  cn_strength: float = 0.75,
370
  ) -> Image.Image:
 
371
  try:
372
  if depth_image is None:
373
  raise gr.Error(
374
  "FUN_CN_REQUIRES_DEPTH: depth_image is required for real Fun ControlNet."
375
  )
376
  _ensure_weights()
377
+ # Wall-clock CPU load — does not burn ZeroGPU minutes.
378
+ get_pipe(prepare_gpu_offload=False)
379
  return _generate_still_gpu(
380
  positive,
381
  negative or "",
 
399
  gr.Markdown(
400
  "## NFA Track R — **Real Fun depth ControlNet** (ZeroGPU)\n"
401
  f"- Stack: VideoX-Fun `Flux2ControlPipeline` + `{CN_FILE}`\n"
402
+ f"- Base: `{BASE_MODEL}` (HF Mount `/data/FLUX.2-dev`)\n"
403
  f"- GPU: `size={GPU_SIZE}` duration={GPU_DURATION}s mem=`{MEM_MODE}`\n"
404
  "- Soft `image=depth` is **banned** on this Space.\n"
405
+ "- First call CPU-loads weights (slow once), then GPU Fun CN infer."
406
  )
407
  with gr.Row():
408
  with gr.Column():