Image21-INT4 / scripts /runtime.py
ixim's picture
Release verified Image21-INT4 conversion
9116984 verified
Raw
History Blame Contribute Delete
6.38 kB
"""Load Image21-INT4 or the BF16 source on CUDA, Apple Silicon, or CPU.
SDNQ stores UINT4 weights and dequantizes them with ordinary PyTorch operators
unless a CUDA/XPU matmul kernel is requested. This loader does not request that
kernel, so CUDA and MPS execute the same eager implementation. Bitsandbytes is
not used.
"""
import json
import os
from pathlib import Path
os.environ.setdefault('DIFFUSERS_SDNQ_TRANSFORMERS', '1')
import torch
from scripts.device import choose_runtime
def configure_caches():
root = Path(__file__).resolve().parents[1]
os.environ.setdefault('HF_HOME', str(root / '.cache' / 'huggingface'))
os.environ.setdefault('NUMBA_CACHE_DIR', str(root / '.cache' / 'numba'))
def detect_device(requested=None):
if requested:
device = torch.device(requested)
if device.type == 'cuda' and not torch.cuda.is_available():
raise RuntimeError('CUDA was requested but is not available')
if device.type == 'mps' and not torch.backends.mps.is_available():
raise RuntimeError('MPS was requested but is not available')
return device
if torch.cuda.is_available():
return torch.device('cuda')
if torch.backends.mps.is_available():
return torch.device('mps')
return torch.device('cpu')
def accelerator_bytes(device):
if device.type == 'cuda':
return torch.cuda.get_device_properties(device).total_memory
import psutil
return psutil.virtual_memory().total
def read_quantization(model):
path = Path(model) / 'transformer' / 'config.json'
if not path.is_file():
return {}
return json.loads(path.read_text(encoding='utf-8')).get('quantization_config') or {}
def quantization_dict(module):
config = getattr(module, 'config', None)
raw = getattr(config, 'quantization_config', None)
if raw is None:
return {}
if hasattr(raw, 'to_dict'):
return raw.to_dict()
if isinstance(raw, dict):
return raw
return dict(raw)
def _config_value(value):
value = getattr(value, 'value', value)
return str(value).split('.')[-1].lower()
def _require_int4(module, name):
quant = quantization_dict(module)
method = _config_value(quant.get('quant_method', ''))
dtype = _config_value(quant.get('weights_dtype', ''))
if method != 'sdnq' or dtype != 'uint4':
raise ValueError(f'{name} is not an SDNQ UINT4 checkpoint: {method} {dtype}')
if quant.get('use_quantized_matmul'):
raise ValueError(f'{name} enables quantized matmul, which is not part of the portable INT4 runtime')
def _dtype(name):
override = os.environ.get('IMAGE21_DTYPE')
return getattr(torch, override or name)
def _enable_group(module, device, offload_type):
from diffusers.hooks.group_offloading import apply_group_offloading
kwargs = dict(onload_device=device, offload_device=torch.device('cpu'),
offload_type=offload_type, use_stream=False, non_blocking=False)
if offload_type == 'block_level':
kwargs['num_blocks_per_group'] = 1
if hasattr(module, 'enable_group_offload'):
module.enable_group_offload(**kwargs)
else:
apply_group_offloading(module, **kwargs)
def apply_offload(pipe, spec, device):
mode = spec['offload']
if mode == 'resident':
pipe.to(device)
elif mode == 'model':
if device.type != 'cuda':
raise ValueError('Model CPU offload is the CUDA path; Apple Silicon uses resident MPS')
pipe.enable_model_cpu_offload(gpu_id=device.index or 0)
elif mode == 'group':
if device.type != 'cuda':
raise ValueError('Group offload in this loader targets CUDA; Apple Silicon uses resident MPS')
# The transformer is the activation-heavy component, so it moves one block at a time.
# The text encoder moves one linear or embedding at a time: a whole decoder block's
# dequantized MLP does not leave enough room under an 8GB cap.
_enable_group(pipe.transformer, device, 'block_level')
_enable_group(pipe.text_encoder, device, 'leaf_level')
# A 1024 decode keeps several GiB live beside the denoiser cache. On an 8GB
# board that peak does not fit, and tiling changes pixels. Decode on CPU.
pipe.vae.to('cpu')
decode = pipe.vae.decode
encode_image = pipe._encode_vae_image
def _decode_on_cpu(latents, *args, **kwargs):
if torch.is_tensor(latents):
latents = latents.detach().to('cpu')
return decode(latents, *args, **kwargs)
def _encode_on_cpu(image, generator):
device = image.device
encoded = encode_image(image.detach().to('cpu'), generator)
return encoded.to(device=device, dtype=image.dtype)
pipe.vae.decode = _decode_on_cpu
pipe._encode_vae_image = _encode_on_cpu
else:
raise ValueError(f'Unknown offload mode: {mode}')
pipe.image21_runtime = dict(spec, device=str(device))
return pipe
def load_pipeline(model, offload='auto', device=None, local_files_only=False):
"""Load a local directory or a Hub repository id."""
configure_caches()
quant = read_quantization(model) if Path(model).exists() else {'quant_method': 'sdnq', 'weights_dtype': 'uint4'}
method = str(quant.get('quant_method', '')).lower()
if method == 'sdnq':
import sdnq # noqa: F401 (registers the quantizer with diffusers and transformers)
elif quant:
raise ValueError(f'Unsupported quantization config: {quant}')
from diffusers import QwenImage21Pipeline
device = detect_device(device)
spec = choose_runtime(device.type, accelerator_bytes(device))
if offload != 'auto':
if offload not in ('model', 'group', 'resident'):
raise ValueError('offload must be auto, model, group, or resident')
spec = dict(spec, offload=offload)
dtype = _dtype(spec['dtype'])
pipe = QwenImage21Pipeline.from_pretrained(str(model), dtype=dtype, local_files_only=local_files_only)
if method == 'sdnq':
_require_int4(pipe.transformer, 'transformer')
_require_int4(pipe.text_encoder, 'text_encoder')
if quantization_dict(pipe.vae):
raise ValueError('VAE is expected to stay in floating point')
apply_offload(pipe, spec, device)
return pipe