Uploaded sanitized BC250/ROCm Repository.
This commit is contained in:
@@ -0,0 +1,631 @@
|
||||
"""
|
||||
BC-250 gfx1010 Comprehensive Monkey-Patch v10
|
||||
1. Replaces torch.softmax with manual implementation (VGPR overflow fix)
|
||||
2. Replaces SDPA with manual implementation
|
||||
3. Patches GGUF cast_bias_weight to dequant on CPU (avoids GPU page-fault hangs)
|
||||
4. Pre-clones mmap'd tensor data before GPU transfer (XNACK workaround)
|
||||
5. Pre-warms GPU context and caching allocator
|
||||
6. Forces text encoder to CPU (memory constraint)
|
||||
7. VAE decode on CPU float32 (bypasses GPU managed memory issues)
|
||||
8. Sets torch threads to all CPU cores (faster CPU ops + VAE decode)
|
||||
9. Caches VAE model on CPU (avoids reload each generation)
|
||||
10. Startup preloading: submits warmup prompt to preload all models on boot
|
||||
|
||||
v10 changes:
|
||||
- Background warmup thread submits 64x64 @ 1 step prompt after server starts
|
||||
- All models (CLIP, UNET, VAE) preloaded before user interaction
|
||||
- Models configurable via BC250_PRELOAD_* env vars
|
||||
|
||||
BC-250 APU / gfx1010: XNACK disabled, no GPU page fault handling.
|
||||
GPU copy shader hangs on non-resident pages (mmap'd or swapped).
|
||||
Place in ComfyUI root and import as first line of main.py.
|
||||
"""
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import os
|
||||
import sys
|
||||
import gc
|
||||
import logging
|
||||
import threading
|
||||
import json
|
||||
import time as _time
|
||||
import threading
|
||||
import json
|
||||
import time as _time
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# === THREAD CONFIGURATION ===
|
||||
# BC-250 has 12 threads (6C/12T Zen2). Use all for CPU-heavy work (VAE, CLIP, dequant).
|
||||
_NUM_THREADS = int(os.environ.get("BC250_NUM_THREADS", str(os.cpu_count() or 12)))
|
||||
torch.set_num_threads(_NUM_THREADS)
|
||||
# Note: set_num_interop_threads must be called before any parallel op, skip to avoid deadlock
|
||||
logger.warning(f"[BC-250] Torch threads: intra-op={_NUM_THREADS}")
|
||||
|
||||
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 PATCH ===
|
||||
_original_module_apply = torch.nn.Module._apply
|
||||
|
||||
def _bc250_safe_apply(self, fn, recurse=True):
|
||||
"""Pre-clone mmap'd CPU tensor data before GPU transfer to avoid XNACK hangs."""
|
||||
for key, param in self._parameters.items():
|
||||
if param is not None and param.device.type == 'cpu':
|
||||
param.data = param.data.clone()
|
||||
for key, buf in self._buffers.items():
|
||||
if buf is not None and buf.device.type == 'cpu':
|
||||
self._buffers[key] = buf.clone()
|
||||
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 triggered: shape={list(input.shape)}, dim={dim}, threshold={SAFE_SOFTMAX_THRESHOLD}")
|
||||
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 triggered: shape={list(self.shape)}, dim={dim}, threshold={SAFE_SOFTMAX_THRESHOLD}")
|
||||
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 triggered: Q={list(query.shape)}, K={list(key.shape)}, S={S}, threshold={SAFE_SOFTMAX_THRESHOLD}")
|
||||
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 PATCH (cast_bias_weight override) ===
|
||||
_gguf_patched = False
|
||||
|
||||
def _try_patch_gguf():
|
||||
"""Patch GGMLLayer.cast_bias_weight to dequant on CPU, send floats 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)
|
||||
is_quantized_fn = getattr(dequant_mod, 'is_quantized', None)
|
||||
if GGMLLayer is None or is_quantized_fn is None:
|
||||
return False
|
||||
|
||||
def _bc250_cast_bias_weight(s, input=None, dtype=None, device=None, bias_dtype=None):
|
||||
"""Dequant on CPU, send float results to GPU.
|
||||
|
||||
Cannot use .to(device) on quantized GGUF tensors from mmap'd files
|
||||
(GPU copy shader hangs on non-resident pages, XNACK disabled).
|
||||
Dequant to float on CPU, then transfer dequantized float to GPU.
|
||||
"""
|
||||
import comfy.model_management
|
||||
import comfy.ops
|
||||
|
||||
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
|
||||
|
||||
non_blocking = comfy.model_management.device_supports_non_blocking(device)
|
||||
|
||||
bias = None
|
||||
if s.bias is not None:
|
||||
if is_quantized_fn(s.bias):
|
||||
bias = s.get_weight(s.bias, bias_dtype)
|
||||
else:
|
||||
bias = s.get_weight(s.bias.to(device), bias_dtype)
|
||||
bias = comfy.ops.cast_to(bias, bias_dtype, device, non_blocking=non_blocking, copy=False)
|
||||
|
||||
if is_quantized_fn(s.weight):
|
||||
weight = s.get_weight(s.weight, dtype)
|
||||
else:
|
||||
weight = s.get_weight(s.weight.to(device), dtype)
|
||||
weight = comfy.ops.cast_to(weight, dtype, device, non_blocking=non_blocking, copy=False)
|
||||
return weight, bias
|
||||
|
||||
GGMLLayer.cast_bias_weight = _bc250_cast_bias_weight
|
||||
|
||||
_gguf_patched = True
|
||||
logger.warning("[BC-250] GGUF cast_bias_weight patched (CPU dequant)")
|
||||
return True
|
||||
|
||||
# === IMPORT HOOK for deferred GGUF patching ===
|
||||
|
||||
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
|
||||
|
||||
# === TEXT ENCODER CPU PATCH ===
|
||||
_te_patched = False
|
||||
|
||||
def _try_patch_text_encoder_device():
|
||||
global _te_patched
|
||||
if _te_patched:
|
||||
return True
|
||||
mm = sys.modules.get('comfy.model_management')
|
||||
if mm is None:
|
||||
return False
|
||||
mm.text_encoder_device = lambda: torch.device("cpu")
|
||||
mm.text_encoder_offload_device = lambda: torch.device("cpu")
|
||||
|
||||
_te_patched = True
|
||||
logger.warning("[BC-250] Text encoder forced to CPU (memory constraint)")
|
||||
return True
|
||||
|
||||
# === VAE CPU FLOAT32 DECODE PATCH ===
|
||||
# Decode VAE on CPU using float32 (not fp16). fp16 on CPU is emulated (10x slower).
|
||||
# Cannot use GPU because UNet managed memory blocks new GPU allocations (XNACK disabled).
|
||||
# 320MB VAE at float32 = 640MB RAM. For 256x256: ~2-3 min on 12-thread CPU.
|
||||
|
||||
_vae_patched = False
|
||||
_vae_cached = False # Track whether VAE is already loaded to CPU float32
|
||||
|
||||
def _try_patch_vae_cpu():
|
||||
"""Patch comfy.sd.VAE to decode on CPU with float32, with persistent caching."""
|
||||
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
|
||||
|
||||
_original_vae_encode = getattr(VAE, 'encode', None)
|
||||
|
||||
def _ensure_vae_on_cpu_f32(self):
|
||||
"""Move VAE to CPU float32 once, then keep it cached."""
|
||||
global _vae_cached
|
||||
if not _vae_cached or next(self.first_stage_model.parameters()).dtype != torch.float32:
|
||||
logger.warning("[BC-250] Loading VAE to CPU float32 (will stay cached)")
|
||||
self.first_stage_model.to(torch.float32).to(torch.device("cpu"))
|
||||
self.first_stage_model.eval()
|
||||
_vae_cached = True
|
||||
# Prevent ComfyUI model_management from offloading the VAE
|
||||
self.disable_offload = True
|
||||
|
||||
def _bc250_vae_decode(self, samples_in, vae_options={}):
|
||||
"""CPU float32 VAE decode — bypasses GPU managed memory entirely.
|
||||
|
||||
The UNet (5032MB managed memory) blocks new GPU allocations
|
||||
when its pages are swapped by the OS (XNACK disabled on gfx1010).
|
||||
float32 on CPU is ~5x faster than fp16 (which requires emulation).
|
||||
VAE stays cached on CPU after first load — no re-conversion needed.
|
||||
"""
|
||||
import time
|
||||
t0 = time.time()
|
||||
logger.warning("[BC-250] VAE decode: CPU float32 (cached)")
|
||||
|
||||
self.throw_exception_if_invalid()
|
||||
|
||||
if self.latent_dim == 2 and samples_in.ndim == 5:
|
||||
samples_in = samples_in[:, :, 0]
|
||||
|
||||
cpu = torch.device("cpu")
|
||||
_ensure_vae_on_cpu_f32(self)
|
||||
|
||||
pixel_samples = None
|
||||
with torch.no_grad():
|
||||
for x in range(samples_in.shape[0]):
|
||||
sample = samples_in[x:x+1].to(torch.float32)
|
||||
decoded = self.first_stage_model.decode(sample, **vae_options)
|
||||
# Squeeze temporal dim for 3D video autoencoders (single image)
|
||||
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
|
||||
|
||||
# NCHW → NHWC (same as original ComfyUI VAE.decode line 977)
|
||||
pixel_samples = pixel_samples.movedim(1, -1)
|
||||
|
||||
elapsed = time.time() - t0
|
||||
logger.warning(f"[BC-250] VAE decode complete in {elapsed:.1f}s")
|
||||
return pixel_samples
|
||||
|
||||
VAE.decode = _bc250_vae_decode
|
||||
|
||||
if _original_vae_encode is not None:
|
||||
def _bc250_vae_encode(self, pixel_samples):
|
||||
"""CPU float32 VAE encode (cached)."""
|
||||
import time
|
||||
t0 = time.time()
|
||||
logger.warning("[BC-250] VAE encode: CPU float32 (cached)")
|
||||
self.throw_exception_if_invalid()
|
||||
|
||||
_ensure_vae_on_cpu_f32(self)
|
||||
|
||||
with torch.no_grad():
|
||||
pixels_in = self.process_input(pixel_samples).to(torch.float32)
|
||||
result = self.first_stage_model.encode(pixels_in).float()
|
||||
|
||||
elapsed = time.time() - t0
|
||||
logger.warning(f"[BC-250] VAE encode complete in {elapsed:.1f}s")
|
||||
return result
|
||||
|
||||
VAE.encode = _bc250_vae_encode
|
||||
|
||||
_vae_patched = True
|
||||
logger.warning("[BC-250] VAE patched: CPU float32 decode/encode with caching (bypass GPU managed memory)")
|
||||
return True
|
||||
|
||||
class _SDModuleWatcher:
|
||||
"""Patches comfy.sd.VAE after it's imported."""
|
||||
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_cpu():
|
||||
self.done = True
|
||||
return mod
|
||||
|
||||
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_text_encoder_device():
|
||||
self.done = True
|
||||
return mod
|
||||
|
||||
# === GPU MEMORY CLEANUP HOOK ===
|
||||
# Patch model_management.load_models_gpu to clean up before loading
|
||||
|
||||
_load_patched = False
|
||||
|
||||
def _try_patch_load_models():
|
||||
"""Add GPU memory cleanup before model loading."""
|
||||
global _load_patched
|
||||
if _load_patched:
|
||||
return True
|
||||
|
||||
mm = sys.modules.get('comfy.model_management')
|
||||
if mm is None:
|
||||
return False
|
||||
|
||||
_original_load = getattr(mm, 'load_models_gpu', None)
|
||||
if _original_load is None:
|
||||
return False
|
||||
|
||||
def _bc250_load_models_gpu(models, *args, **kwargs):
|
||||
"""Clean GPU cache before loading models to prevent memory pressure hangs."""
|
||||
gc.collect()
|
||||
torch.cuda.empty_cache()
|
||||
return _original_load(models, *args, **kwargs)
|
||||
|
||||
mm.load_models_gpu = _bc250_load_models_gpu
|
||||
_load_patched = True
|
||||
logger.warning("[BC-250] GPU memory cleanup hook installed (load_models_gpu)")
|
||||
return True
|
||||
|
||||
# === STARTUP PRELOAD ===
|
||||
|
||||
_PRELOAD_CLIP = os.environ.get("BC250_PRELOAD_CLIP", "Qwen_3_4b-Q8_0.gguf")
|
||||
_PRELOAD_UNET = os.environ.get("BC250_PRELOAD_UNET", "z_image_turbo-Q5_K_S.gguf")
|
||||
_PRELOAD_VAE = os.environ.get("BC250_PRELOAD_VAE", "ae.safetensors")
|
||||
_PRELOAD_PORT = int(os.environ.get("BC250_PRELOAD_PORT", "8188"))
|
||||
_PRELOAD_ENABLED = os.environ.get("BC250_PRELOAD", "1") == "1"
|
||||
|
||||
def _preload_models():
|
||||
"""Background thread: wait for ComfyUI server, then submit a warmup prompt."""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
url = f"http://127.0.0.1:{_PRELOAD_PORT}"
|
||||
|
||||
# Wait for server to be ready (max 120s)
|
||||
logger.warning("[BC-250] Preload: waiting for ComfyUI server...")
|
||||
for _ in range(240):
|
||||
try:
|
||||
urllib.request.urlopen(f"{url}/api/system_stats", timeout=2)
|
||||
break
|
||||
except (urllib.error.URLError, OSError, ConnectionRefusedError):
|
||||
_time.sleep(0.5)
|
||||
else:
|
||||
logger.warning("[BC-250] Preload: server not ready after 120s, skipping")
|
||||
return
|
||||
|
||||
logger.warning("[BC-250] Preload: server ready, submitting warmup prompt...")
|
||||
|
||||
warmup = {
|
||||
"1": {"class_type": "CLIPLoaderGGUF", "inputs": {"clip_name": _PRELOAD_CLIP, "type": "lumina2"}},
|
||||
"2": {"class_type": "CLIPTextEncode", "inputs": {"text": "warmup", "clip": ["1", 0]}},
|
||||
"3": {"class_type": "CLIPTextEncode", "inputs": {"text": "", "clip": ["1", 0]}},
|
||||
"4": {"class_type": "UnetLoaderGGUF", "inputs": {"unet_name": _PRELOAD_UNET}},
|
||||
"5": {"class_type": "EmptyLatentImage", "inputs": {"width": 64, "height": 64, "batch_size": 1}},
|
||||
"6": {"class_type": "KSampler", "inputs": {
|
||||
"seed": 1, "steps": 1, "cfg": 1.0, "sampler_name": "euler",
|
||||
"scheduler": "normal", "denoise": 1.0,
|
||||
"model": ["4", 0], "positive": ["2", 0], "negative": ["3", 0], "latent_image": ["5", 0]
|
||||
}},
|
||||
"7": {"class_type": "VAELoader", "inputs": {"vae_name": _PRELOAD_VAE}},
|
||||
"8": {"class_type": "VAEDecode", "inputs": {"samples": ["6", 0], "vae": ["7", 0]}},
|
||||
"9": {"class_type": "SaveImage", "inputs": {"filename_prefix": "_warmup", "images": ["8", 0]}}
|
||||
}
|
||||
|
||||
payload = json.dumps({"prompt": warmup}).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
f"{url}/api/prompt",
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST"
|
||||
)
|
||||
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
data = json.loads(resp.read())
|
||||
prompt_id = data.get("prompt_id", "unknown")
|
||||
logger.warning(f"[BC-250] Preload: warmup prompt queued (id={prompt_id})")
|
||||
|
||||
# Wait for completion (max 5min)
|
||||
for _ in range(300):
|
||||
_time.sleep(1)
|
||||
try:
|
||||
hist_resp = urllib.request.urlopen(f"{url}/api/history/{prompt_id}", timeout=5)
|
||||
hist = json.loads(hist_resp.read())
|
||||
if prompt_id in hist:
|
||||
logger.warning("[BC-250] Preload: all models loaded and cached. Ready for user prompts.")
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.warning("[BC-250] Preload: warmup timed out after 5min")
|
||||
except Exception as e:
|
||||
logger.warning(f"[BC-250] Preload: warmup failed: {e}")
|
||||
|
||||
|
||||
def _start_preload_thread():
|
||||
if not _PRELOAD_ENABLED:
|
||||
logger.warning("[BC-250] Preload: disabled (BC250_PRELOAD=0)")
|
||||
return
|
||||
t = threading.Thread(target=_preload_models, daemon=True, name="BC250-Preload")
|
||||
t.start()
|
||||
logger.warning("[BC-250] Preload: background warmup thread started")
|
||||
|
||||
# === STARTUP PRELOAD ===
|
||||
|
||||
_PRELOAD_CLIP = os.environ.get("BC250_PRELOAD_CLIP", "Qwen_3_4b-Q8_0.gguf")
|
||||
_PRELOAD_UNET = os.environ.get("BC250_PRELOAD_UNET", "z_image_turbo-Q5_K_S.gguf")
|
||||
_PRELOAD_VAE = os.environ.get("BC250_PRELOAD_VAE", "ae.safetensors")
|
||||
_PRELOAD_PORT = int(os.environ.get("BC250_PRELOAD_PORT", "8188"))
|
||||
_PRELOAD_ENABLED = os.environ.get("BC250_PRELOAD", "1") == "1"
|
||||
|
||||
def _preload_models():
|
||||
"""Background thread: wait for ComfyUI server, then submit a warmup prompt."""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
url = f"http://127.0.0.1:{_PRELOAD_PORT}"
|
||||
|
||||
# Wait for server to be ready (max 120s)
|
||||
logger.warning("[BC-250] Preload: waiting for ComfyUI server...")
|
||||
for _ in range(240):
|
||||
try:
|
||||
urllib.request.urlopen(f"{url}/api/system_stats", timeout=2)
|
||||
break
|
||||
except (urllib.error.URLError, OSError, ConnectionRefusedError):
|
||||
_time.sleep(0.5)
|
||||
else:
|
||||
logger.warning("[BC-250] Preload: server not ready after 120s, skipping")
|
||||
return
|
||||
|
||||
logger.warning("[BC-250] Preload: server ready, submitting warmup prompt...")
|
||||
|
||||
warmup = {
|
||||
"1": {"class_type": "CLIPLoaderGGUF", "inputs": {"clip_name": _PRELOAD_CLIP, "type": "lumina2"}},
|
||||
"2": {"class_type": "CLIPTextEncode", "inputs": {"text": "warmup", "clip": ["1", 0]}},
|
||||
"3": {"class_type": "CLIPTextEncode", "inputs": {"text": "", "clip": ["1", 0]}},
|
||||
"4": {"class_type": "UnetLoaderGGUF", "inputs": {"unet_name": _PRELOAD_UNET}},
|
||||
"5": {"class_type": "EmptyLatentImage", "inputs": {"width": 64, "height": 64, "batch_size": 1}},
|
||||
"6": {"class_type": "KSampler", "inputs": {
|
||||
"seed": 1, "steps": 1, "cfg": 1.0, "sampler_name": "euler",
|
||||
"scheduler": "normal", "denoise": 1.0,
|
||||
"model": ["4", 0], "positive": ["2", 0], "negative": ["3", 0], "latent_image": ["5", 0]
|
||||
}},
|
||||
"7": {"class_type": "VAELoader", "inputs": {"vae_name": _PRELOAD_VAE}},
|
||||
"8": {"class_type": "VAEDecode", "inputs": {"samples": ["6", 0], "vae": ["7", 0]}},
|
||||
"9": {"class_type": "SaveImage", "inputs": {"filename_prefix": "_warmup", "images": ["8", 0]}}
|
||||
}
|
||||
|
||||
payload = json.dumps({"prompt": warmup}).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
f"{url}/api/prompt",
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST"
|
||||
)
|
||||
|
||||
try:
|
||||
resp = urllib.request.urlopen(req, timeout=10)
|
||||
data = json.loads(resp.read())
|
||||
prompt_id = data.get("prompt_id", "unknown")
|
||||
logger.warning(f"[BC-250] Preload: warmup prompt queued (id={prompt_id})")
|
||||
|
||||
# Wait for completion (max 5min)
|
||||
for _ in range(300):
|
||||
_time.sleep(1)
|
||||
try:
|
||||
hist_resp = urllib.request.urlopen(f"{url}/api/history/{prompt_id}", timeout=5)
|
||||
hist = json.loads(hist_resp.read())
|
||||
if prompt_id in hist:
|
||||
logger.warning("[BC-250] Preload: all models loaded and cached. Ready for user prompts.")
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.warning("[BC-250] Preload: warmup timed out after 5min")
|
||||
except Exception as e:
|
||||
logger.warning(f"[BC-250] Preload: warmup failed: {e}")
|
||||
|
||||
|
||||
def _start_preload_thread():
|
||||
if not _PRELOAD_ENABLED:
|
||||
logger.warning("[BC-250] Preload: disabled (BC250_PRELOAD=0)")
|
||||
return
|
||||
t = threading.Thread(target=_preload_models, daemon=True, name="BC250-Preload")
|
||||
t.start()
|
||||
logger.warning("[BC-250] Preload: background warmup thread started")
|
||||
|
||||
# === INSTALL ===
|
||||
|
||||
def _prewarm_gpu():
|
||||
try:
|
||||
if not torch.cuda.is_available():
|
||||
return
|
||||
dummy = torch.zeros(1, device='cuda')
|
||||
_ = dummy + 1
|
||||
torch.cuda.synchronize()
|
||||
del dummy
|
||||
torch.cuda.empty_cache()
|
||||
logger.warning("[BC-250] GPU pre-warmed (context + allocator ready)")
|
||||
except Exception as e:
|
||||
logger.warning(f"[BC-250] GPU pre-warm failed: {e}")
|
||||
|
||||
|
||||
def install():
|
||||
# Mmap pre-clone patch
|
||||
torch.nn.Module._apply = _bc250_safe_apply
|
||||
logger.warning("[BC-250] Mmap pre-clone patch installed (XNACK workaround)")
|
||||
|
||||
# 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 monkey-patch installed (threshold={SAFE_SOFTMAX_THRESHOLD})")
|
||||
|
||||
# GGUF deferred cast_bias_weight patch
|
||||
sys.meta_path.insert(0, _GGUFImportWatcher())
|
||||
logger.warning("[BC-250] GGUF CPU-dequant hook registered (cast_bias_weight)")
|
||||
|
||||
# Text encoder CPU patch
|
||||
sys.meta_path.insert(0, _ModelMgmtWatcher())
|
||||
|
||||
# VAE CPU-only patch
|
||||
sys.meta_path.insert(0, _SDModuleWatcher())
|
||||
|
||||
# Try immediate patches if modules already loaded
|
||||
_try_patch_gguf()
|
||||
_try_patch_text_encoder_device()
|
||||
_try_patch_vae_cpu()
|
||||
_try_patch_load_models()
|
||||
|
||||
# Pre-warm GPU
|
||||
_prewarm_gpu()
|
||||
|
||||
# Start background preload thread
|
||||
_start_preload_thread()
|
||||
|
||||
install()
|
||||
Reference in New Issue
Block a user