This repository has been archived on 2026-08-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
2026-08-20 00:45:43 +02:00

647 lines
25 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
BC-250 gfx1010 Comprehensive Monkey-Patch v17
1. BF16 KILL: gfx1010 has NO native bf16 — force f16 everywhere
2. Softmax: manual impl for dim > threshold (VGPR overflow fix)
3. SDPA: manual impl for large sequences
4. GGUF: GGMLTensor.to() patched — quantized weights ALWAYS stay on CPU
CPU dequant → f16 → GPU transfer per layer (gfx1010 GPU can't dequant)
5. Mmap: pre-clones non-GGUF tensor data before GPU transfer
6. GPU: pre-warms context and caching allocator
7. CLIP: forces text encoder to CPU (memory constraint)
8. VAE: CPU f32 decode cached in RAM (faster than GPU on this APU)
9. Threads: all CPU cores for intra-op parallelism
10. Rope: force rope() to CPU — gfx1010 has NO native float64
11. Non-blocking disabled: gfx1010 without SDMA hangs on async copies
v14: rope() → CPU (gfx1010 has no float64 HW)
v15: GGUF dequant → CPU (gfx1010 GPU hangs on Q5_K bitwise ops)
v16: GGMLTensor.to() patched to keep quantized weights on CPU
v17: cast_to → direct .to(non_blocking=False), warmup removed
- non_blocking=True hangs on gfx1010 (no SDMA, async HIP copy broken)
- replaced empty_like+copy_ with direct .to() for CPU→GPU transfer
- device_supports_non_blocking → always False for this device
BC-250 APU / gfx1010: XNACK disabled, no GPU page fault handling.
Place in ComfyUI root and import as first line of main.py.
"""
import os
os.environ.setdefault("TORCHDYNAMO_DISABLE", "1")
import torch
import torch.nn.functional as F
import sys
import gc
import logging
import time as _time
logger = logging.getLogger(__name__)
# === THREAD CONFIGURATION ===
_NUM_THREADS = int(os.environ.get("BC250_NUM_THREADS", str(os.cpu_count() or 12)))
torch.set_num_threads(_NUM_THREADS)
logger.warning(f"[BC-250] Torch threads: intra-op={_NUM_THREADS}")
# Disable torch._dynamo — gfx1010 doesn't benefit, compilation overhead is massive
try:
torch._dynamo.config.suppress_errors = True
logger.warning("[BC-250] torch._dynamo: TORCHDYNAMO_DISABLE=1 + suppress_errors")
except Exception:
logger.warning("[BC-250] torch._dynamo: TORCHDYNAMO_DISABLE=1 (env only)")
SAFE_SOFTMAX_THRESHOLD = int(os.environ.get("BC250_SOFTMAX_THRESHOLD", "4096"))
_original_softmax = torch.nn.functional.softmax
_original_tensor_softmax = torch.Tensor.softmax
_original_sdpa = torch.nn.functional.scaled_dot_product_attention
# === MMAP PRE-CLONE + BF16 KILL PATCH ===
_original_module_apply = torch.nn.Module._apply
def _is_ggml_tensor(t):
"""Check if tensor is a GGMLTensor (has GGUF quantization metadata)."""
return hasattr(t, 'tensor_type')
# === GGML TENSOR CPU LOCK ===
# Patched later when GGUF module loads (_try_patch_gguf).
# GGMLTensor.to() is monkey-patched so quantized weights NEVER leave CPU.
# This prevents both: GPU dequant hang AND GPU→CPU transfer hang.
def _bc250_safe_apply(self, fn, recurse=True):
"""Pre-clone mmap'd CPU tensor data before GPU transfer (XNACK workaround).
Converts ALL bf16 → f16 (gfx1010 has no native bf16 — including GGML BF16).
BF16 GGML tensors are dequantized to f32→f16, becoming regular tensors.
Note: GGMLTensor.clone() returns self, so quantized GGUF weights are unaffected."""
for key, param in self._parameters.items():
if param is None:
continue
# Clone CPU data for XNACK workaround (skip GGML: clone() returns self)
if param.device.type == 'cpu' and not _is_ggml_tensor(param.data):
param.data = param.data.clone()
# gfx1010: no native bf16. Convert ALL bf16 → f16 (including GGML BF16)
if param.data.dtype == torch.bfloat16:
param.data = param.data.float().half()
for key, buf in self._buffers.items():
if buf is None:
continue
if buf.device.type == 'cpu' and not _is_ggml_tensor(buf):
buf = buf.clone()
if buf.dtype == torch.bfloat16:
buf = buf.float().half()
self._buffers[key] = buf
return _original_module_apply(self, fn, recurse)
# === SOFTMAX PATCH ===
def _safe_softmax_impl(input, dim=-1):
x_max = input.max(dim=dim, keepdim=True).values
exp_x = torch.exp(input - x_max)
return exp_x / exp_x.sum(dim=dim, keepdim=True)
def patched_softmax(input, dim=None, _stacklevel=3, dtype=None):
if dim is None:
dim = -1
if dtype is not None:
input = input.to(dtype)
if input.is_cuda and input.shape[dim] > SAFE_SOFTMAX_THRESHOLD:
if not getattr(patched_softmax, '_logged', False):
logger.warning(f"[BC-250] Manual F.softmax: shape={list(input.shape)}, dim={dim}")
patched_softmax._logged = True
return _safe_softmax_impl(input, dim)
return _original_softmax(input, dim=dim)
def patched_tensor_softmax(self, dim=-1, dtype=None):
if dtype is not None:
self = self.to(dtype)
if self.is_cuda and self.shape[dim] > SAFE_SOFTMAX_THRESHOLD:
if not getattr(patched_tensor_softmax, '_logged', False):
logger.warning(f"[BC-250] Manual softmax: shape={list(self.shape)}, dim={dim}")
patched_tensor_softmax._logged = True
return _safe_softmax_impl(self, dim)
return _original_tensor_softmax(self, dim=dim)
def _safe_sdpa(query, key, value, attn_mask=None, dropout_p=0.0, is_causal=False, scale=None):
L, S = query.size(-2), key.size(-2)
if scale is None:
scale = query.size(-1) ** -0.5
attn_weight = torch.matmul(query, key.transpose(-2, -1)) * scale
if is_causal:
causal_mask = torch.triu(torch.ones(L, S, dtype=torch.bool, device=query.device), diagonal=1)
attn_weight = attn_weight.masked_fill(causal_mask, float('-inf'))
if attn_mask is not None:
if attn_mask.dtype == torch.bool:
attn_weight = attn_weight.masked_fill(~attn_mask, float('-inf'))
else:
attn_weight = attn_weight + attn_mask
attn_weight = _safe_softmax_impl(attn_weight, dim=-1)
if dropout_p > 0.0:
attn_weight = torch.nn.functional.dropout(attn_weight, p=dropout_p)
return torch.matmul(attn_weight, value)
def patched_sdpa(query, key, value, attn_mask=None, dropout_p=0.0, is_causal=False, scale=None):
S = key.size(-2)
if query.is_cuda and S > SAFE_SOFTMAX_THRESHOLD:
if not getattr(patched_sdpa, '_logged', False):
logger.warning(f"[BC-250] Manual SDPA: Q={list(query.shape)}, S={S}")
patched_sdpa._logged = True
return _safe_sdpa(query, key, value, attn_mask=attn_mask,
dropout_p=dropout_p, is_causal=is_causal, scale=scale)
return _original_sdpa(query, key, value, attn_mask=attn_mask,
dropout_p=dropout_p, is_causal=is_causal, scale=scale)
# === GGUF CPU DEQUANT + GGMLTensor CPU LOCK ===
_gguf_patched = False
def _try_patch_gguf():
"""Patch GGUF for BC-250:
1. GGMLTensor.to() → keeps quantized weights on CPU (ignore device arg)
2. cast_bias_weight → CPU dequant, then transfer f16 result to GPU
gfx1010 GPU cannot dequantize Q5_K (bitwise ops hang).
And once weights are on GPU, transferring back to CPU also hangs.
Only safe path: weights stay CPU → dequant on CPU → f16 to GPU."""
global _gguf_patched
if _gguf_patched:
return True
ops_mod = None
dequant_mod = None
for name, mod in sys.modules.items():
if mod is None:
continue
if name.endswith('.ops') and 'GGUF' in name:
ops_mod = mod
if name.endswith('.dequant') and 'GGUF' in name:
dequant_mod = mod
if ops_mod is None or dequant_mod is None:
return False
GGMLLayer = getattr(ops_mod, 'GGMLLayer', None)
GGMLTensor = getattr(ops_mod, 'GGMLTensor', None)
is_quantized_fn = getattr(dequant_mod, 'is_quantized', None)
if GGMLLayer is None or GGMLTensor is None or is_quantized_fn is None:
return False
torch_compiler_disable = getattr(ops_mod, 'torch_compiler_disable', None)
# === PATCH 1: GGMLTensor.to() — keep quantized on CPU ===
_original_ggml_to = GGMLTensor.to
def _bc250_ggml_to(self, *args, **kwargs):
"""Intercept .to() calls: keep quantized weights on CPU.
Only allow dtype changes, block device changes to CUDA.
This prevents load_models_gpu from moving GGUF weights to GPU."""
# Check if this is a quantized tensor (has tensor_type metadata)
if hasattr(self, 'tensor_type') and self.tensor_type is not None:
# Parse the .to() call to extract device, dtype, non_blocking
# Common patterns from nn.Module._apply:
# t.to(device, dtype, non_blocking) — 3 positional
# t.to(device) — 1 positional
# t.to(dtype) — 1 positional (dtype)
# t.to(device=..., dtype=..., non_blocking=...) — kwargs
parsed_device = kwargs.get('device', None)
parsed_dtype = kwargs.get('dtype', None)
parsed_nb = kwargs.get('non_blocking', False)
parsed_mem_fmt = kwargs.get('memory_format', None)
for a in args:
if isinstance(a, torch.device):
parsed_device = a
elif isinstance(a, str):
try:
parsed_device = torch.device(a)
except Exception:
pass
elif isinstance(a, torch.dtype):
parsed_dtype = a
elif isinstance(a, bool):
parsed_nb = a
elif a is None:
# dtype=None from Module.to() convert function
pass
# Block CUDA transfer for quantized weights — stay on CPU
if parsed_device is not None and parsed_device.type == 'cuda':
# Reconstruct call without device, keeping dtype/non_blocking
remap_kwargs = {}
if parsed_dtype is not None:
remap_kwargs['dtype'] = parsed_dtype
if parsed_nb:
remap_kwargs['non_blocking'] = parsed_nb
if parsed_mem_fmt is not None:
remap_kwargs['memory_format'] = parsed_mem_fmt
if remap_kwargs:
return _original_ggml_to(self, **remap_kwargs)
return self # No-op: was just a device move
return _original_ggml_to(self, *args, **kwargs)
GGMLTensor.to = _bc250_ggml_to
logger.warning("[BC-250] GGMLTensor.to() patched: quantized weights locked to CPU")
# === PATCH 2: cast_bias_weight — CPU dequant + GPU transfer ===
_fwd_count = [0, 0.0]
def _bc250_cast_bias_weight(s, input=None, dtype=None, device=None, bias_dtype=None):
"""CPU dequant → GPU transfer. Weights are guaranteed CPU (GGMLTensor.to patched).
Dequant on CPU via get_weight(), transfer f16 to GPU with synchronous .to().
gfx1010 without SDMA cannot do async copies — non_blocking=False always."""
if _fwd_count[0] == 0:
_fwd_count[1] = _time.time()
_fwd_count[0] += 1
if input is not None:
if dtype is None:
dtype = getattr(input, "dtype", torch.float32)
if bias_dtype is None:
bias_dtype = dtype
if device is None:
device = input.device
# gfx1010: never dequant to bf16
if dtype == torch.bfloat16:
dtype = torch.float16
if bias_dtype == torch.bfloat16:
bias_dtype = torch.float16
is_first = _fwd_count[0] <= 3 or 499 <= _fwd_count[0] <= 505
if is_first:
logger.warning(f"[BC-250] CBW#{_fwd_count[0]}: device={device}, w_type={type(s.weight).__name__}, w_dev={s.weight.device}, has_tt={hasattr(s.weight, 'tensor_type')}")
bias = None
if s.bias is not None:
if is_first:
logger.warning(f"[BC-250] CBW#{_fwd_count[0]}: get_weight(bias)...")
bias = s.get_weight(s.bias, bias_dtype)
if is_first:
logger.warning(f"[BC-250] CBW#{_fwd_count[0]}: bias got, type={type(bias).__name__}, dev={bias.device}, dt={bias.dtype}")
if type(bias) is not torch.Tensor:
bias = bias.as_subclass(torch.Tensor)
if bias.device != device or bias.dtype != bias_dtype:
if is_first:
logger.warning(f"[BC-250] CBW#{_fwd_count[0]}: bias.to({device})...")
bias = bias.to(device=device, dtype=bias_dtype, non_blocking=False)
if is_first:
logger.warning(f"[BC-250] CBW#{_fwd_count[0]}: bias transferred")
if is_first:
logger.warning(f"[BC-250] CBW#{_fwd_count[0]}: get_weight(weight)...")
weight = s.get_weight(s.weight, dtype)
if is_first:
logger.warning(f"[BC-250] CBW#{_fwd_count[0]}: weight got, type={type(weight).__name__}, shape={list(weight.shape)}, dev={weight.device}, dt={weight.dtype}")
if type(weight) is not torch.Tensor:
weight = weight.as_subclass(torch.Tensor)
if weight.device != device or weight.dtype != dtype:
if is_first:
logger.warning(f"[BC-250] CBW#{_fwd_count[0]}: weight.to({device}, {dtype})...")
weight = weight.to(device=device, dtype=dtype, non_blocking=False)
if is_first:
logger.warning(f"[BC-250] CBW#{_fwd_count[0]}: weight transferred")
if _fwd_count[0] % 100 == 0:
elapsed = _time.time() - _fwd_count[1]
logger.warning(f"[BC-250] Layer {_fwd_count[0]}, elapsed {elapsed:.1f}s")
# Log all CUDA transfers to find the exact hang point
if device is not None and hasattr(device, 'type') and device.type == 'cuda':
logger.warning(f"[BC-250] CUDA#{_fwd_count[0]}: {list(weight.shape)} {weight.dtype} done")
return weight, bias
if torch_compiler_disable is not None:
_bc250_cast_bias_weight = torch_compiler_disable()(_bc250_cast_bias_weight)
GGMLLayer.cast_bias_weight = _bc250_cast_bias_weight
_gguf_patched = True
logger.warning("[BC-250] GGUF patched: CPU dequant + GGMLTensor CPU-locked")
return True
class _GGUFImportWatcher:
def __init__(self):
self.done = False
def find_module(self, fullname, path=None):
if self.done:
return None
if 'GGUF' in fullname and ('dequant' in fullname or 'ops' in fullname):
return self
return None
def load_module(self, fullname):
if self in sys.meta_path:
sys.meta_path.remove(self)
try:
import importlib
mod = importlib.import_module(fullname)
finally:
if self not in sys.meta_path:
sys.meta_path.insert(0, self)
if _try_patch_gguf():
self.done = True
return mod
# === MODEL MANAGEMENT PATCHES (text encoder CPU + bf16 kill + load hook) ===
_mm_patched = False
def _try_patch_model_management():
"""Patches comfy.model_management:
- Text encoder → CPU
- should_use_bf16 → always False
- unet_dtype → never returns bf16
- load_models_gpu → cleanup + post-load bf16→f16
"""
global _mm_patched
if _mm_patched:
return True
mm = sys.modules.get('comfy.model_management')
if mm is None:
return False
# Text encoder on CPU
mm.text_encoder_device = lambda: torch.device("cpu")
mm.text_encoder_offload_device = lambda: torch.device("cpu")
logger.warning("[BC-250] Text encoder forced to CPU")
# Kill bf16 globally — gfx1010 has no native bf16
mm.should_use_bf16 = lambda *a, **kw: False
logger.warning("[BC-250] should_use_bf16 → always False (gfx1010)")
# Force non_blocking=False — gfx1010 without SDMA hangs on async HIP copies
mm.device_supports_non_blocking = lambda *a, **kw: False
logger.warning("[BC-250] device_supports_non_blocking → always False (no SDMA)")
_original_unet_dtype = mm.unet_dtype
def _bc250_unet_dtype(*args, **kwargs):
return torch.float16 # gfx1010: always f16 (2× faster than f32, no bf16 HW)
mm.unet_dtype = _bc250_unet_dtype
logger.warning("[BC-250] unet_dtype patched: always f16")
# Ensure fp16 is recognized as available
mm.should_use_fp16 = lambda *a, **kw: True
logger.warning("[BC-250] should_use_fp16 → always True")
# Load hook: cleanup + post-load bf16 → f16 conversion
_original_load = getattr(mm, 'load_models_gpu', None)
if _original_load is not None:
def _bc250_load_models_gpu(models, *args, **kwargs):
gc.collect()
torch.cuda.empty_cache()
result = _original_load(models, *args, **kwargs)
# Post-load: convert ALL remaining bf16 params/buffers to f16
for m in models:
real_model = getattr(m, 'model', None)
if real_model is None:
continue
converted = 0
for p in real_model.parameters():
if p.dtype == torch.bfloat16:
p.data = p.data.float().half()
converted += 1
for name, buf in real_model.named_buffers():
if buf is not None and buf.dtype == torch.bfloat16:
parts = name.split('.')
obj = real_model
for part in parts[:-1]:
obj = getattr(obj, part)
setattr(obj, parts[-1], buf.float().half())
converted += 1
if converted > 0:
logger.warning(f"[BC-250] Post-load: converted {converted} bf16→f16 params/buffers")
return result
mm.load_models_gpu = _bc250_load_models_gpu
logger.warning("[BC-250] GPU load hook installed (cleanup + bf16 kill)")
_mm_patched = True
return True
class _ModelMgmtWatcher:
def __init__(self):
self.done = False
def find_module(self, fullname, path=None):
if self.done:
return None
if fullname == 'comfy.model_management':
return self
return None
def load_module(self, fullname):
if self in sys.meta_path:
sys.meta_path.remove(self)
try:
import importlib
mod = importlib.import_module(fullname)
finally:
if self not in sys.meta_path:
sys.meta_path.insert(0, self)
if _try_patch_model_management():
self.done = True
return mod
# === VAE CPU CACHED ===
_vae_patched = False
_vae_cached = False
def _try_patch_vae_gpu():
global _vae_patched
if _vae_patched:
return True
sd_mod = sys.modules.get('comfy.sd')
if sd_mod is None:
return False
VAE = getattr(sd_mod, 'VAE', None)
if VAE is None:
return False
def _ensure_vae_cached_cpu(self):
"""Keep VAE on CPU in RAM, eval mode. No GPU transfer needed.
On BC-250 APU: GPU VAE decode is slower than CPU (24 CUs, no SDMA).
CPU has 12 Zen2 threads and direct RAM access — faster for VAE convolutions."""
global _vae_cached
if not _vae_cached:
t0 = _time.time()
self.first_stage_model.to(device='cpu', dtype=torch.float32)
self.first_stage_model.eval()
_vae_cached = True
logger.warning(f"[BC-250] VAE cached on CPU (f32) in {_time.time()-t0:.1f}s")
self.disable_offload = True
def _bc250_vae_decode(self, samples_in, vae_options={}):
t0 = _time.time()
self.throw_exception_if_invalid()
if self.latent_dim == 2 and samples_in.ndim == 5:
samples_in = samples_in[:, :, 0]
_ensure_vae_cached_cpu(self)
pixel_samples = None
with torch.no_grad():
for x in range(samples_in.shape[0]):
sample = samples_in[x:x+1].float().cpu()
decoded = self.first_stage_model.decode(sample, **vae_options)
if decoded.ndim == 5:
decoded = decoded[:, :, 0]
out = self.process_output(decoded.float())
if pixel_samples is None:
pixel_samples = torch.empty(
(samples_in.shape[0],) + tuple(out.shape[1:]), device='cpu'
)
pixel_samples[x:x+1] = out
del decoded, sample
pixel_samples = pixel_samples.movedim(1, -1)
elapsed = _time.time() - t0
logger.warning(f"[BC-250] VAE decode (CPU f32): {elapsed:.1f}s")
return pixel_samples
def _bc250_vae_encode(self, pixel_samples):
t0 = _time.time()
self.throw_exception_if_invalid()
_ensure_vae_cached_cpu(self)
with torch.no_grad():
pixels_in = self.process_input(pixel_samples).float().cpu()
result = self.first_stage_model.encode(pixels_in).float()
logger.warning(f"[BC-250] VAE encode (CPU f32): {_time.time() - t0:.1f}s")
return result
VAE.decode = _bc250_vae_decode
VAE.encode = _bc250_vae_encode
_vae_patched = True
logger.warning("[BC-250] VAE patched: CPU f32 decode/encode (cached in RAM)")
return True
class _SDModuleWatcher:
def __init__(self):
self.done = False
def find_module(self, fullname, path=None):
if self.done:
return None
if fullname == 'comfy.sd':
return self
return None
def load_module(self, fullname):
if self in sys.meta_path:
sys.meta_path.remove(self)
try:
import importlib
mod = importlib.import_module(fullname)
finally:
if self not in sys.meta_path:
sys.meta_path.insert(0, self)
if _try_patch_vae_gpu():
self.done = True
return mod
# === ROPE CPU PATCH (gfx1010 has no float64 hardware) ===
_rope_patched = False
def _try_patch_rope():
"""Patch rope() in flux/math.py to always compute on CPU.
gfx1010 has no native float64 — GPU float64 ops are software-emulated and hang."""
global _rope_patched
if _rope_patched:
return True
flux_math = sys.modules.get('comfy.ldm.flux.math')
if flux_math is None:
return False
_original_rope = getattr(flux_math, 'rope', None)
if _original_rope is None:
return False
def _bc250_rope(pos, dim, theta):
"""Compute rope on CPU (float64 not supported on gfx1010), then move result to original device."""
assert dim % 2 == 0
target_device = pos.device
device = torch.device("cpu")
scale = torch.linspace(0, (dim - 2) / dim, steps=dim // 2, dtype=torch.float64, device=device)
omega = 1.0 / (theta ** scale)
out = torch.einsum("...n,d->...nd", pos.to(dtype=torch.float32, device=device), omega)
from einops import rearrange
out = torch.stack([torch.cos(out), -torch.sin(out), torch.sin(out), torch.cos(out)], dim=-1)
out = rearrange(out, "b n d (i j) -> b n d i j", i=2, j=2)
return out.to(dtype=torch.float32, device=target_device)
flux_math.rope = _bc250_rope
_rope_patched = True
logger.warning("[BC-250] rope() patched: CPU computation (no float64 on gfx1010)")
return True
class _FluxMathWatcher:
def __init__(self):
self.done = False
def find_module(self, fullname, path=None):
if self.done:
return None
if fullname == 'comfy.ldm.flux.math':
return self
return None
def load_module(self, fullname):
if self in sys.meta_path:
sys.meta_path.remove(self)
try:
import importlib
mod = importlib.import_module(fullname)
finally:
if self not in sys.meta_path:
sys.meta_path.insert(0, self)
if _try_patch_rope():
self.done = True
return mod
# === WARMUP REMOVED (v17) ===
# First prompt may be slower, subsequent prompts benefit from HIP kernel caches.
# === INSTALL ===
def _prewarm_gpu():
try:
if not torch.cuda.is_available():
return
# Warm GPU context + allocator
dummy = torch.zeros(1, device='cuda')
_ = dummy + 1
torch.cuda.synchronize()
# Warm CPU→GPU copy kernel (COMGR JIT on first transfer)
cpu_t = torch.randn(256, 256, dtype=torch.float16)
gpu_t = cpu_t.to('cuda')
_ = torch.matmul(gpu_t, gpu_t.T)
torch.cuda.synchronize()
del dummy, cpu_t, gpu_t, _
torch.cuda.empty_cache()
logger.warning("[BC-250] GPU pre-warmed (context + copy + matmul)")
except Exception as e:
logger.warning(f"[BC-250] GPU pre-warm failed: {e}")
def install():
# Mmap pre-clone + bf16 kill patch
torch.nn.Module._apply = _bc250_safe_apply
logger.warning("[BC-250] Mmap pre-clone + bf16→f16 patch installed")
# Softmax patches
torch.nn.functional.softmax = patched_softmax
torch.Tensor.softmax = patched_tensor_softmax
torch.nn.functional.scaled_dot_product_attention = patched_sdpa
logger.warning(f"[BC-250] Softmax patch installed (threshold={SAFE_SOFTMAX_THRESHOLD})")
# Deferred patches via import hooks
sys.meta_path.insert(0, _GGUFImportWatcher())
sys.meta_path.insert(0, _ModelMgmtWatcher())
sys.meta_path.insert(0, _SDModuleWatcher())
sys.meta_path.insert(0, _FluxMathWatcher())
# Try immediate patches
_try_patch_gguf()
_try_patch_model_management()
_try_patch_vae_gpu()
_try_patch_rope()
_prewarm_gpu()
logger.warning("[BC-250] v17 ready — no warmup, first prompt may be slow")
install()