umer1995 commited on
Commit
e8f050d
·
verified ·
1 Parent(s): bd99637

Fun CN: stream fp8 DiT on CUDA inside @spaces.GPU xlarge (no host bf16)

Browse files
Files changed (1) hide show
  1. app.py +123 -97
app.py CHANGED
@@ -135,7 +135,7 @@ CN_FILE = os.environ.get(
135
  GPU_SIZE = (os.environ.get("NFA_FUN_CN_GPU_SIZE") or "xlarge").strip().lower()
136
  if GPU_SIZE not in ("large", "xlarge"):
137
  GPU_SIZE = "xlarge"
138
- GPU_DURATION = int(os.environ.get("NFA_FUN_CN_GPU_DURATION") or "300")
139
  WEIGHT_DTYPE = torch.bfloat16
140
  MEM_MODE = (
141
  os.environ.get("NFA_FUN_CN_MEM_MODE") or "model_cpu_offload_and_qfloat8"
@@ -281,32 +281,38 @@ def _fp8_dtype_for_key(key: str) -> torch.dtype:
281
  return torch.float8_e4m3fn
282
 
283
 
284
- def _set_tensor(model: torch.nn.Module, key: str, tensor: torch.Tensor) -> None:
285
- """Materialize on CPU as bf16, then in-place cast to float8 when allowed.
286
-
287
- Direct float8 via set_module_tensor_to_device hung on ZeroGPU CPU; bf16
288
- assign + ``param.data = param.data.to(float8)`` is the stable path.
289
- """
 
 
290
  from accelerate.utils import set_module_tensor_to_device
291
 
 
 
292
  value = tensor.detach().to(dtype=WEIGHT_DTYPE)
293
- set_module_tensor_to_device(
294
- model, key, device="cpu", value=value, dtype=WEIGHT_DTYPE
295
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  del value
297
- if _fp8_dtype_for_key(key) != torch.float8_e4m3fn:
298
- return
299
- # Walk to the Parameter and cast storage in-place (frees bf16 bits).
300
- mod: torch.nn.Module = model
301
- parts = key.split(".")
302
- for p in parts[:-1]:
303
- mod = getattr(mod, p)
304
- leaf = parts[-1]
305
- param = getattr(mod, leaf)
306
- if isinstance(param, torch.nn.Parameter):
307
- param.data = param.data.to(torch.float8_e4m3fn)
308
- elif isinstance(param, torch.Tensor):
309
- setattr(mod, leaf, param.to(torch.float8_e4m3fn))
310
 
311
 
312
  def _stream_shards_into_model(
@@ -314,39 +320,46 @@ def _stream_shards_into_model(
314
  shard_paths: list[str],
315
  *,
316
  label: str,
 
317
  ) -> None:
318
- """Load one safetensors shard at a time fp8; never accumulate full bf16."""
319
- from safetensors.torch import load_file
320
 
321
- model_sd = model.state_dict()
 
322
  loaded = 0
323
  skipped = 0
324
  for i, path in enumerate(shard_paths):
325
  _check_load_deadline(f"{label}_shard_{i}")
326
  print(
327
  f"[nfa-fun-cn] {label} shard {i+1}/{len(shard_paths)} "
328
- f"rss_max≈{_rss_gb():.1f}GB path={Path(path).name}",
329
  flush=True,
330
  )
331
- sd = load_file(path, device="cpu")
332
- n_keys = len(sd)
333
- for j, (key, tensor) in enumerate(sd.items()):
334
- if key not in model_sd:
335
- skipped += 1
336
- continue
337
- if tuple(tensor.shape) != tuple(model_sd[key].shape):
338
- skipped += 1
339
- continue
340
- _set_tensor(model, key, tensor)
341
- loaded += 1
342
- if (j + 1) % 200 == 0 or (j + 1) == n_keys:
343
- print(
344
- f"[nfa-fun-cn] {label} shard {i+1} keys {j+1}/{n_keys} "
345
- f"loaded={loaded} rss_max≈{_rss_gb():.1f}GB",
346
- flush=True,
347
- )
348
- del sd
 
 
 
349
  gc.collect()
 
 
350
  print(
351
  f"[nfa-fun-cn] {label} stream done loaded={loaded} skipped={skipped} "
352
  f"rss_max≈{_rss_gb():.1f}GB",
@@ -354,24 +367,16 @@ def _stream_shards_into_model(
354
  )
355
 
356
 
357
- def _init_missing_control_params(model: torch.nn.Module) -> None:
358
  """Mirror VideoX missing-key init for control blocks (zeros / clones)."""
359
  from accelerate.utils import set_module_tensor_to_device
360
 
361
  sd = {k: v for k, v in model.named_parameters()}
362
- # named_parameters may still be meta; use state_dict meta shapes
363
  meta_sd = model.state_dict()
364
- missing = [
365
- k
366
- for k, v in meta_sd.items()
367
- if getattr(v, "is_meta", False) or (hasattr(v, "device") and v.device.type == "meta")
368
- ]
369
- if not missing:
370
- # Also detect uninitialized via device
371
- missing = []
372
- for name, param in model.named_parameters():
373
- if param.device.type == "meta":
374
- missing.append(name)
375
  if not missing:
376
  print("[nfa-fun-cn] no meta params left before Fun CN overlay", flush=True)
377
  return
@@ -381,65 +386,72 @@ def _init_missing_control_params(model: torch.nn.Module) -> None:
381
  for key in missing:
382
  shape = tuple(meta_sd[key].shape)
383
  dtype = _fp8_dtype_for_key(key)
384
- # Prefer clone from non-control twin when VideoX does
385
  twin = key.replace("control_", "")
386
  if "control" in key and twin in sd and sd[twin].device.type != "meta":
387
  value = sd[twin].detach().to(dtype=torch.bfloat16).to(dtype=dtype)
388
- elif "after_proj" in key or "before_proj" in key:
389
- value = torch.zeros(shape, dtype=dtype)
390
- elif "bias" in key:
391
  value = torch.zeros(shape, dtype=dtype)
392
  else:
393
  value = torch.zeros(shape, dtype=dtype)
394
- set_module_tensor_to_device(model, key, device="cpu", value=value, dtype=dtype)
 
 
395
 
396
 
397
- def _overlay_fun_cn(model: torch.nn.Module, cn_path: Path) -> tuple[int, int]:
 
 
398
  """Apply Fun CN Union weights without loading a second full DiT."""
399
  from safetensors import safe_open
400
 
401
- model_sd = model.state_dict()
402
  loaded = 0
403
  skipped = 0
404
  print(
405
- f"[nfa-fun-cn] Fun CN overlay {cn_path.name} "
406
  f"rss_max≈{_rss_gb():.1f}GB",
407
  flush=True,
408
  )
409
  with safe_open(str(cn_path), framework="pt", device="cpu") as f:
410
  keys = list(f.keys())
411
  if keys == ["state_dict"]:
412
- # rare wrapper — fall back to full load of inner dict only
413
  from safetensors.torch import load_file
414
 
415
  wrapped = load_file(str(cn_path))
416
  inner = wrapped.get("state_dict", wrapped)
417
  for key, tensor in inner.items():
418
- if key not in model_sd or tuple(tensor.shape) != tuple(model_sd[key].shape):
419
  skipped += 1
420
  continue
421
- _set_tensor(model, key, tensor)
422
  loaded += 1
423
  del wrapped, inner
424
  gc.collect()
425
  else:
426
- for key in keys:
427
- if key not in model_sd:
428
  skipped += 1
429
  continue
430
  tensor = f.get_tensor(key)
431
- if tuple(tensor.shape) != tuple(model_sd[key].shape):
432
  skipped += 1
 
433
  continue
434
- _set_tensor(model, key, tensor)
435
  loaded += 1
436
  del tensor
 
 
 
 
 
 
437
  gc.collect()
438
  return loaded, skipped
439
 
440
 
441
- def _load_control_transformer_fp8(model_name: str, cn_file: str):
442
- """Q8-class painter: empty meta → stream bf16 shards as float8 → Fun CN."""
443
  global _FUN_CN_LOADED
444
  import accelerate
445
  from videox_fun.models.flux2_transformer2d_control import (
@@ -456,8 +468,8 @@ def _load_control_transformer_fp8(model_name: str, cn_file: str):
456
  ])
457
 
458
  print(
459
- "[nfa-fun-cn] CPU-load Flux2Control as float8 stream "
460
- f"(NOT full bf16) mem={MEM_MODE} gpu_size={GPU_SIZE}",
461
  flush=True,
462
  )
463
  with accelerate.init_empty_weights():
@@ -467,15 +479,19 @@ def _load_control_transformer_fp8(model_name: str, cn_file: str):
467
  shards = sorted(glob.glob(os.path.join(shard_dir, "*.safetensors")))
468
  if not shards:
469
  raise FileNotFoundError(f"No transformer shards under {shard_dir}")
470
- _stream_shards_into_model(transformer, shards, label="DiT-fp8")
 
 
471
  _check_load_deadline("after_dit_stream")
472
- _init_missing_control_params(transformer)
473
  _check_load_deadline("after_control_init")
474
- loaded, skipped = _overlay_fun_cn(transformer, Path(cn_file))
 
 
475
  _FUN_CN_LOADED = True
476
  print(
477
  f"[nfa-fun-cn] Fun CN loaded overlay_ok={loaded} skipped={skipped} "
478
- f"rss_max≈{_rss_gb():.1f}GB (REAL Fun CN, fp8 painter)",
479
  flush=True,
480
  )
481
  return transformer
@@ -508,9 +524,17 @@ def _load_local_quantized_te(te_root: Path):
508
 
509
 
510
  def get_pipe(*, prepare_gpu_offload: bool = False):
511
- """CPU-load Fun CN pipeline (fp8 DiT stream); arm GPU offload inside @spaces.GPU."""
512
  global _PIPE, _PIPE_OFFLOAD_READY, _GET_IMAGE_LATENT, _LOAD_T0, _FUN_CN_LOADED
513
  if _PIPE is None:
 
 
 
 
 
 
 
 
514
  _LOAD_T0 = time.time()
515
  _FUN_CN_LOADED = False
516
  _ensure_weights()
@@ -525,14 +549,21 @@ def get_pipe(*, prepare_gpu_offload: bool = False):
525
  _GET_IMAGE_LATENT = get_image_latent
526
  model_name = str(MODEL_DIR)
527
  cn_file = str(_resolve_cn_path())
 
 
 
 
 
 
528
 
529
- transformer = _load_control_transformer_fp8(model_name, cn_file)
 
 
530
  _check_load_deadline("post_fun_cn")
531
 
532
  vae = AutoencoderKLFlux2.from_pretrained(model_name, subfolder="vae").to(
533
  WEIGHT_DTYPE
534
  )
535
- # Tokenizer from base FLUX.2 mount (same vocab as TE).
536
  tokenizer = PixtralProcessor.from_pretrained(model_name, subfolder="tokenizer")
537
  text_encoder = _load_local_quantized_te(te_root)
538
  scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
@@ -547,7 +578,7 @@ def get_pipe(*, prepare_gpu_offload: bool = False):
547
  )
548
  elapsed = time.time() - _LOAD_T0
549
  print(
550
- f"[nfa-fun-cn] Flux2ControlPipeline CPU-ready "
551
  f"(REAL Fun CN + fp8 DiT + local TE) in {elapsed:.0f}s "
552
  f"rss_max≈{_rss_gb():.1f}GB",
553
  flush=True,
@@ -558,12 +589,9 @@ def get_pipe(*, prepare_gpu_offload: bool = False):
558
 
559
  device = "cuda"
560
  transformer = _PIPE.transformer
561
- # Weights already float8 from stream load — only wrap compute dtype.
562
- if MEM_MODE in (
563
- "model_cpu_offload_and_qfloat8",
564
- "model_full_load_and_qfloat8",
565
- ):
566
- convert_weight_dtype_wrapper(transformer, WEIGHT_DTYPE)
567
  if MEM_MODE == "sequential_cpu_offload":
568
  _PIPE.enable_sequential_cpu_offload(device=device)
569
  elif MEM_MODE in ("model_cpu_offload", "model_cpu_offload_and_qfloat8"):
@@ -602,7 +630,10 @@ def _generate_still_gpu(
602
  if not prompt:
603
  raise gr.Error("positive prompt is required")
604
  depth = _prep_depth(depth_image, w, h)
 
605
  pipe = get_pipe(prepare_gpu_offload=True)
 
 
606
  control_latent = _GET_IMAGE_LATENT(depth, sample_size=[h, w])[:, :, 0]
607
  inpaint_image = torch.zeros([1, 3, h, w])
608
  mask_image = torch.ones([1, 1, h, w]) * 255
@@ -651,12 +682,7 @@ def generate_still(
651
  "FUN_CN_REQUIRES_DEPTH: depth_image is required for real Fun ControlNet."
652
  )
653
  _ensure_weights()
654
- # Wall-clock CPU load does not burn ZeroGPU minutes.
655
- get_pipe(prepare_gpu_offload=False)
656
- if not _FUN_CN_LOADED:
657
- raise RuntimeError(
658
- "FUN_CN_LOAD_ABORT: pipeline built but Fun CN flag false"
659
- )
660
  return _generate_still_gpu(
661
  positive,
662
  negative or "",
 
135
  GPU_SIZE = (os.environ.get("NFA_FUN_CN_GPU_SIZE") or "xlarge").strip().lower()
136
  if GPU_SIZE not in ("large", "xlarge"):
137
  GPU_SIZE = "xlarge"
138
+ GPU_DURATION = int(os.environ.get("NFA_FUN_CN_GPU_DURATION") or "600")
139
  WEIGHT_DTYPE = torch.bfloat16
140
  MEM_MODE = (
141
  os.environ.get("NFA_FUN_CN_MEM_MODE") or "model_cpu_offload_and_qfloat8"
 
281
  return torch.float8_e4m3fn
282
 
283
 
284
+ def _set_tensor(
285
+ model: torch.nn.Module,
286
+ key: str,
287
+ tensor: torch.Tensor,
288
+ *,
289
+ device: str,
290
+ ) -> None:
291
+ """Assign one weight; prefer CUDA float8 (CPU float8 hung on ZeroGPU host)."""
292
  from accelerate.utils import set_module_tensor_to_device
293
 
294
+ target = _fp8_dtype_for_key(key)
295
+ # Always stage through bf16 for numeric stability, then target dtype on device.
296
  value = tensor.detach().to(dtype=WEIGHT_DTYPE)
297
+ if target == torch.float8_e4m3fn and device.startswith("cuda"):
298
+ value = value.to(dtype=target)
299
+ set_module_tensor_to_device(
300
+ model, key, device=device, value=value, dtype=target
301
+ )
302
+ else:
303
+ set_module_tensor_to_device(
304
+ model, key, device=device, value=value, dtype=WEIGHT_DTYPE
305
+ )
306
+ if target == torch.float8_e4m3fn:
307
+ mod: torch.nn.Module = model
308
+ parts = key.split(".")
309
+ for p in parts[:-1]:
310
+ mod = getattr(mod, p)
311
+ leaf = parts[-1]
312
+ param = getattr(mod, leaf)
313
+ if isinstance(param, torch.nn.Parameter):
314
+ param.data = param.data.to(torch.float8_e4m3fn)
315
  del value
 
 
 
 
 
 
 
 
 
 
 
 
 
316
 
317
 
318
  def _stream_shards_into_model(
 
320
  shard_paths: list[str],
321
  *,
322
  label: str,
323
+ device: str,
324
  ) -> None:
325
+ """Stream shards key-by-key (no full-shard RAM spike) into float8 on device."""
326
+ from safetensors import safe_open
327
 
328
+ # Shapes only — avoid pulling meta tensors repeatedly.
329
+ shape_map = {k: tuple(v.shape) for k, v in model.state_dict().items()}
330
  loaded = 0
331
  skipped = 0
332
  for i, path in enumerate(shard_paths):
333
  _check_load_deadline(f"{label}_shard_{i}")
334
  print(
335
  f"[nfa-fun-cn] {label} shard {i+1}/{len(shard_paths)} "
336
+ f"device={device} rss_max≈{_rss_gb():.1f}GB path={Path(path).name}",
337
  flush=True,
338
  )
339
+ with safe_open(path, framework="pt", device="cpu") as f:
340
+ keys = list(f.keys())
341
+ n_keys = len(keys)
342
+ for j, key in enumerate(keys):
343
+ if key not in shape_map:
344
+ skipped += 1
345
+ continue
346
+ tensor = f.get_tensor(key)
347
+ if tuple(tensor.shape) != shape_map[key]:
348
+ skipped += 1
349
+ del tensor
350
+ continue
351
+ _set_tensor(model, key, tensor, device=device)
352
+ loaded += 1
353
+ del tensor
354
+ if (j + 1) % 50 == 0 or (j + 1) == n_keys:
355
+ print(
356
+ f"[nfa-fun-cn] {label} shard {i+1} keys {j+1}/{n_keys} "
357
+ f"loaded={loaded} rss_max≈{_rss_gb():.1f}GB",
358
+ flush=True,
359
+ )
360
  gc.collect()
361
+ if device.startswith("cuda"):
362
+ torch.cuda.empty_cache()
363
  print(
364
  f"[nfa-fun-cn] {label} stream done loaded={loaded} skipped={skipped} "
365
  f"rss_max≈{_rss_gb():.1f}GB",
 
367
  )
368
 
369
 
370
+ def _init_missing_control_params(model: torch.nn.Module, *, device: str) -> None:
371
  """Mirror VideoX missing-key init for control blocks (zeros / clones)."""
372
  from accelerate.utils import set_module_tensor_to_device
373
 
374
  sd = {k: v for k, v in model.named_parameters()}
 
375
  meta_sd = model.state_dict()
376
+ missing = []
377
+ for name, param in model.named_parameters():
378
+ if param.device.type == "meta":
379
+ missing.append(name)
 
 
 
 
 
 
 
380
  if not missing:
381
  print("[nfa-fun-cn] no meta params left before Fun CN overlay", flush=True)
382
  return
 
386
  for key in missing:
387
  shape = tuple(meta_sd[key].shape)
388
  dtype = _fp8_dtype_for_key(key)
 
389
  twin = key.replace("control_", "")
390
  if "control" in key and twin in sd and sd[twin].device.type != "meta":
391
  value = sd[twin].detach().to(dtype=torch.bfloat16).to(dtype=dtype)
392
+ elif "after_proj" in key or "before_proj" in key or "bias" in key:
 
 
393
  value = torch.zeros(shape, dtype=dtype)
394
  else:
395
  value = torch.zeros(shape, dtype=dtype)
396
+ set_module_tensor_to_device(
397
+ model, key, device=device, value=value, dtype=dtype
398
+ )
399
 
400
 
401
+ def _overlay_fun_cn(
402
+ model: torch.nn.Module, cn_path: Path, *, device: str
403
+ ) -> tuple[int, int]:
404
  """Apply Fun CN Union weights without loading a second full DiT."""
405
  from safetensors import safe_open
406
 
407
+ shape_map = {k: tuple(v.shape) for k, v in model.state_dict().items()}
408
  loaded = 0
409
  skipped = 0
410
  print(
411
+ f"[nfa-fun-cn] Fun CN overlay {cn_path.name} device={device} "
412
  f"rss_max≈{_rss_gb():.1f}GB",
413
  flush=True,
414
  )
415
  with safe_open(str(cn_path), framework="pt", device="cpu") as f:
416
  keys = list(f.keys())
417
  if keys == ["state_dict"]:
 
418
  from safetensors.torch import load_file
419
 
420
  wrapped = load_file(str(cn_path))
421
  inner = wrapped.get("state_dict", wrapped)
422
  for key, tensor in inner.items():
423
+ if key not in shape_map or tuple(tensor.shape) != shape_map[key]:
424
  skipped += 1
425
  continue
426
+ _set_tensor(model, key, tensor, device=device)
427
  loaded += 1
428
  del wrapped, inner
429
  gc.collect()
430
  else:
431
+ for j, key in enumerate(keys):
432
+ if key not in shape_map:
433
  skipped += 1
434
  continue
435
  tensor = f.get_tensor(key)
436
+ if tuple(tensor.shape) != shape_map[key]:
437
  skipped += 1
438
+ del tensor
439
  continue
440
+ _set_tensor(model, key, tensor, device=device)
441
  loaded += 1
442
  del tensor
443
+ if (j + 1) % 50 == 0:
444
+ print(
445
+ f"[nfa-fun-cn] Fun CN overlay keys {j+1}/{len(keys)} "
446
+ f"loaded={loaded}",
447
+ flush=True,
448
+ )
449
  gc.collect()
450
  return loaded, skipped
451
 
452
 
453
+ def _load_control_transformer_fp8(model_name: str, cn_file: str, *, device: str):
454
+ """Q8-class painter: empty meta → stream to device float8 → Fun CN."""
455
  global _FUN_CN_LOADED
456
  import accelerate
457
  from videox_fun.models.flux2_transformer2d_control import (
 
468
  ])
469
 
470
  print(
471
+ "[nfa-fun-cn] load Flux2Control float8 stream "
472
+ f"device={device} (NOT full bf16 host) mem={MEM_MODE} gpu_size={GPU_SIZE}",
473
  flush=True,
474
  )
475
  with accelerate.init_empty_weights():
 
479
  shards = sorted(glob.glob(os.path.join(shard_dir, "*.safetensors")))
480
  if not shards:
481
  raise FileNotFoundError(f"No transformer shards under {shard_dir}")
482
+ _stream_shards_into_model(
483
+ transformer, shards, label="DiT-fp8", device=device
484
+ )
485
  _check_load_deadline("after_dit_stream")
486
+ _init_missing_control_params(transformer, device=device)
487
  _check_load_deadline("after_control_init")
488
+ loaded, skipped = _overlay_fun_cn(
489
+ transformer, Path(cn_file), device=device
490
+ )
491
  _FUN_CN_LOADED = True
492
  print(
493
  f"[nfa-fun-cn] Fun CN loaded overlay_ok={loaded} skipped={skipped} "
494
+ f"rss_max≈{_rss_gb():.1f}GB (REAL Fun CN, fp8 painter, {device})",
495
  flush=True,
496
  )
497
  return transformer
 
524
 
525
 
526
  def get_pipe(*, prepare_gpu_offload: bool = False):
527
+ """Build Fun CN pipeline on CUDA when available (avoids host-RAM bf16 hang)."""
528
  global _PIPE, _PIPE_OFFLOAD_READY, _GET_IMAGE_LATENT, _LOAD_T0, _FUN_CN_LOADED
529
  if _PIPE is None:
530
+ if not prepare_gpu_offload:
531
+ # Do not CPU-preload DiT — that was the prior 35min hang.
532
+ print(
533
+ "[nfa-fun-cn] defer DiT+Fun CN load until @spaces.GPU "
534
+ f"(size={GPU_SIZE})",
535
+ flush=True,
536
+ )
537
+ return None
538
  _LOAD_T0 = time.time()
539
  _FUN_CN_LOADED = False
540
  _ensure_weights()
 
549
  _GET_IMAGE_LATENT = get_image_latent
550
  model_name = str(MODEL_DIR)
551
  cn_file = str(_resolve_cn_path())
552
+ device = "cuda" if torch.cuda.is_available() else "cpu"
553
+ if device != "cuda":
554
+ raise RuntimeError(
555
+ "FUN_CN_REQUIRES_CUDA: fp8 Fun CN load must run inside "
556
+ "@spaces.GPU (CPU path hangs / thrash)."
557
+ )
558
 
559
+ transformer = _load_control_transformer_fp8(
560
+ model_name, cn_file, device=device
561
+ )
562
  _check_load_deadline("post_fun_cn")
563
 
564
  vae = AutoencoderKLFlux2.from_pretrained(model_name, subfolder="vae").to(
565
  WEIGHT_DTYPE
566
  )
 
567
  tokenizer = PixtralProcessor.from_pretrained(model_name, subfolder="tokenizer")
568
  text_encoder = _load_local_quantized_te(te_root)
569
  scheduler = FlowMatchEulerDiscreteScheduler.from_pretrained(
 
578
  )
579
  elapsed = time.time() - _LOAD_T0
580
  print(
581
+ f"[nfa-fun-cn] Flux2ControlPipeline ready "
582
  f"(REAL Fun CN + fp8 DiT + local TE) in {elapsed:.0f}s "
583
  f"rss_max≈{_rss_gb():.1f}GB",
584
  flush=True,
 
589
 
590
  device = "cuda"
591
  transformer = _PIPE.transformer
592
+ convert_weight_dtype_wrapper(transformer, WEIGHT_DTYPE)
593
+ # DiT already resident on CUDA as fp8 — keep full GPU load (xlarge 96GB).
594
+ # TE stays CPU via pipeline hooks when using cpu_offload for VAE/TE only.
 
 
 
595
  if MEM_MODE == "sequential_cpu_offload":
596
  _PIPE.enable_sequential_cpu_offload(device=device)
597
  elif MEM_MODE in ("model_cpu_offload", "model_cpu_offload_and_qfloat8"):
 
630
  if not prompt:
631
  raise gr.Error("positive prompt is required")
632
  depth = _prep_depth(depth_image, w, h)
633
+ # Load DiT+Fun CN HERE (on GPU) — never full bf16 on host.
634
  pipe = get_pipe(prepare_gpu_offload=True)
635
+ if pipe is None or not _FUN_CN_LOADED:
636
+ raise RuntimeError("FUN_CN_LOAD_ABORT: Fun CN not loaded on GPU")
637
  control_latent = _GET_IMAGE_LATENT(depth, sample_size=[h, w])[:, :, 0]
638
  inpaint_image = torch.zeros([1, 3, h, w])
639
  mask_image = torch.ones([1, 1, h, w]) * 255
 
682
  "FUN_CN_REQUIRES_DEPTH: depth_image is required for real Fun ControlNet."
683
  )
684
  _ensure_weights()
685
+ # Mounts only on host; DiT+Fun CN load is inside @spaces.GPU.
 
 
 
 
 
686
  return _generate_still_gpu(
687
  positive,
688
  negative or "",