Introduce MODEL_TYPE_MINIMAX_H3, keep-float/int4-sensitive presets, and automatic adapter detection; bump package version to 2.1.0 and update changelog. Add robust ConvRot group-size validation (_validate_convrot_group_size), stricter Hadamard shape checks, and improved error handling when loading/saving native INT8 ConvRot metadata. Add unit tests covering ConvRot metadata round-trips, invalid metadata rejection, save-time consistency checks, and MiniMax H3 detection.
2739 lines
115 KiB
Python
2739 lines
115 KiB
Python
import os
|
|
import logging
|
|
import json
|
|
import torch
|
|
from torch import Tensor, nn
|
|
import torch.nn.functional as F
|
|
import comfy.model_management
|
|
|
|
from .int4_compile_compat import native_int4_linear
|
|
from .quantization_policy import DEFAULT_INT4_MIXED_RATIO
|
|
from .quantization_policy import is_int4_compatible_linear
|
|
from .quantization_policy import normalize_int4_mixed_ratio
|
|
from .quantization_policy import normalize_layer_name
|
|
from .quantization_policy import select_int4_mixed_layers
|
|
|
|
try:
|
|
from comfy.quant_ops import QuantizedTensor, TensorCoreConvRotW4A4Layout
|
|
_NATIVE_INT4_AVAILABLE = True
|
|
except (ImportError, AttributeError):
|
|
QuantizedTensor = None
|
|
TensorCoreConvRotW4A4Layout = None
|
|
_NATIVE_INT4_AVAILABLE = False
|
|
|
|
_INT8_FORCE_DISABLE_TORCH_COMPILE = os.environ.get("INT8_FORCE_DISABLE_TORCH_COMPILE", "0") == "1"
|
|
_INT8_FILE_SLICE_LOAD_ENABLED = os.environ.get("INT8_FILE_SLICE_LOAD", "1") != "0"
|
|
_INT8_FILE_SLICE_LOAD_WARNED = False
|
|
|
|
try:
|
|
import comfy.memory_management as comfy_memory_management
|
|
except Exception:
|
|
comfy_memory_management = None
|
|
|
|
try:
|
|
import comfy_aimdo.host_buffer as comfy_aimdo_host_buffer
|
|
import comfy_aimdo.torch as comfy_aimdo_torch
|
|
_AIMDO_FILE_SLICE_LOAD_AVAILABLE = True
|
|
except Exception:
|
|
comfy_aimdo_host_buffer = None
|
|
comfy_aimdo_torch = None
|
|
_AIMDO_FILE_SLICE_LOAD_AVAILABLE = False
|
|
|
|
# Add this at the top of your file
|
|
try:
|
|
from .int8_fused_kernel import triton_int8_linear
|
|
from .int8_fused_kernel import triton_int8_linear_per_row
|
|
from .int8_fused_kernel import triton_quantize_rowwise
|
|
from .int8_fused_kernel import TRITON_ROWWISE_QUANT_MAX_COLS
|
|
_TRITON_AVAILABLE = True
|
|
except ImportError:
|
|
TRITON_ROWWISE_QUANT_MAX_COLS = 8192
|
|
_TRITON_AVAILABLE = False
|
|
logging.info("Quantization Toolkit INT8: Triton was not found; falling back to torch._int_mm.")
|
|
|
|
try:
|
|
from .quarot import build_hadamard as _quarot_build_hadamard
|
|
from .quarot import rotate_weight as _quarot_rotate_weight
|
|
_QUAROT_AVAILABLE = True
|
|
except Exception:
|
|
_QUAROT_AVAILABLE = False
|
|
|
|
try:
|
|
from .convrot import build_hadamard as _convrot_build_hadamard
|
|
from .convrot import rotate_weight as _convrot_rotate_weight
|
|
_CONVROT_AVAILABLE = True
|
|
except Exception:
|
|
_CONVROT_AVAILABLE = False
|
|
|
|
if _INT8_FORCE_DISABLE_TORCH_COMPILE:
|
|
try:
|
|
_disable_torch_compile = torch.compiler.disable
|
|
except Exception:
|
|
try:
|
|
import torch._dynamo as _torch_dynamo
|
|
_disable_torch_compile = _torch_dynamo.disable
|
|
except Exception:
|
|
def _disable_torch_compile(fn):
|
|
return fn
|
|
else:
|
|
def _disable_torch_compile(fn):
|
|
return fn
|
|
|
|
try:
|
|
_SMALL_BATCH_FALLBACK_MAX_ROWS = max(0, int(os.environ.get("INT8_SMALL_BATCH_FALLBACK_MAX_ROWS", "16")))
|
|
except ValueError:
|
|
_SMALL_BATCH_FALLBACK_MAX_ROWS = 16
|
|
|
|
try:
|
|
_SMALL_BATCH_FALLBACK_MIN_ROWS = max(1, int(os.environ.get("INT8_SMALL_BATCH_FALLBACK_MIN_ROWS", "2")))
|
|
except ValueError:
|
|
_SMALL_BATCH_FALLBACK_MIN_ROWS = 2
|
|
|
|
try:
|
|
_SMALL_LAYER_MAX_PARAMS = max(1, int(os.environ.get("INT8_SMALL_LAYER_MAX_PARAMS", "1000000")))
|
|
except ValueError:
|
|
_SMALL_LAYER_MAX_PARAMS = 1_000_000
|
|
|
|
_ADAPTIVE_SMALL_BATCH_FALLBACK = os.environ.get("INT8_SMALL_BATCH_FALLBACK_ADAPTIVE", "1") == "1"
|
|
_DYNAMIC_LORA_DEBUG = os.environ.get("INT8_DYNAMIC_LORA_DEBUG", "0") == "1"
|
|
_DYNAMIC_LORA_BATCH = os.environ.get("INT8_DYNAMIC_LORA_BATCH", "1") == "1"
|
|
_RUNTIME_STATS_ENABLED = os.environ.get("INT8_RUNTIME_STATS", "0") == "1"
|
|
SMALL_BATCH_FALLBACK_NEVER = "never"
|
|
SMALL_BATCH_FALLBACK_SMALL_LAYERS = "only_small_layers"
|
|
SMALL_BATCH_FALLBACK_ALWAYS = "always"
|
|
SMALL_BATCH_FALLBACK_CHOICES = [
|
|
SMALL_BATCH_FALLBACK_SMALL_LAYERS,
|
|
SMALL_BATCH_FALLBACK_ALWAYS,
|
|
SMALL_BATCH_FALLBACK_NEVER,
|
|
]
|
|
DEFAULT_SMALL_BATCH_FALLBACK = SMALL_BATCH_FALLBACK_SMALL_LAYERS
|
|
INT8_BACKEND_TRITON = "triton"
|
|
INT8_BACKEND_TRITON_LEGACY_UNSAFE = "triton_legacy_unsafe"
|
|
INT8_BACKEND_TORCH_INT_MM = "torch_int_mm"
|
|
INT8_BACKEND_CHOICES = [
|
|
INT8_BACKEND_TORCH_INT_MM,
|
|
INT8_BACKEND_TRITON,
|
|
INT8_BACKEND_TRITON_LEGACY_UNSAFE,
|
|
]
|
|
DEFAULT_INT8_BACKEND = INT8_BACKEND_TORCH_INT_MM
|
|
OUTLIER_METHOD_NONE = "none"
|
|
OUTLIER_METHOD_QUAROT = "quarot"
|
|
OUTLIER_METHOD_CONVROT = "convrot"
|
|
OUTLIER_METHOD_HADANORM = "hadanorm"
|
|
QUANTIZATION_MODE_INT8 = "int8"
|
|
QUANTIZATION_MODE_INT8_CONVROT = "int8_convrot"
|
|
QUANTIZATION_MODE_INT8_QUAROT = "int8_quarot"
|
|
QUANTIZATION_MODE_INT8_HADANORM = "int8_hadanorm"
|
|
QUANTIZATION_MODE_INT4_MIXED = "int4_mixed"
|
|
QUANTIZATION_MODE_INT4_FULL = "int4_full"
|
|
QUANTIZATION_MODE_CHOICES = [
|
|
QUANTIZATION_MODE_INT8,
|
|
QUANTIZATION_MODE_INT8_CONVROT,
|
|
QUANTIZATION_MODE_INT8_QUAROT,
|
|
QUANTIZATION_MODE_INT8_HADANORM,
|
|
QUANTIZATION_MODE_INT4_MIXED,
|
|
QUANTIZATION_MODE_INT4_FULL,
|
|
]
|
|
OUTLIER_METHOD_CHOICES = [
|
|
OUTLIER_METHOD_NONE,
|
|
OUTLIER_METHOD_CONVROT,
|
|
OUTLIER_METHOD_QUAROT,
|
|
OUTLIER_METHOD_HADANORM,
|
|
]
|
|
_QUAROT_GROUP_SIZE = 128
|
|
_CONVROT_GROUP_SIZE = 256
|
|
_QUAROT_OFFSET_WARNED: set[tuple[int, int, int] | str] = set()
|
|
_HADANORM_ALPHA = 0.5
|
|
_HADANORM_EPS = 1e-6
|
|
_INT4_QUANT_GROUP_SIZE = 64
|
|
|
|
try:
|
|
_DYNAMIC_LORA_BATCH_MAX_RANK = max(64, int(os.environ.get("INT8_DYNAMIC_LORA_BATCH_MAX_RANK", "4096")))
|
|
except ValueError:
|
|
_DYNAMIC_LORA_BATCH_MAX_RANK = 4096
|
|
|
|
_FLOAT8_DTYPES = tuple(
|
|
dtype for dtype in (
|
|
getattr(torch, "float8_e4m3fn", None),
|
|
getattr(torch, "float8_e4m3fnuz", None),
|
|
getattr(torch, "float8_e5m2", None),
|
|
getattr(torch, "float8_e5m2fnuz", None),
|
|
)
|
|
if dtype is not None
|
|
)
|
|
|
|
# --- Quantization Utils ---
|
|
|
|
def _get_int8_compute_device(fallback_device: torch.device | None = None) -> torch.device:
|
|
try:
|
|
return comfy.model_management.get_torch_device()
|
|
except Exception:
|
|
if fallback_device is not None:
|
|
return fallback_device
|
|
return torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
|
|
|
|
def tensor_to_int8_compute_device(tensor: Tensor, device: torch.device | None, non_blocking: bool = True) -> Tensor:
|
|
if device is None:
|
|
return tensor
|
|
|
|
target_device = torch.device(device)
|
|
if (
|
|
not _INT8_FILE_SLICE_LOAD_ENABLED
|
|
or not _AIMDO_FILE_SLICE_LOAD_AVAILABLE
|
|
or comfy_memory_management is None
|
|
or not hasattr(comfy_memory_management, "read_tensor_file_slice_into")
|
|
or tensor.device.type != "cpu"
|
|
or target_device.type != "cuda"
|
|
):
|
|
return tensor.to(target_device, non_blocking=non_blocking)
|
|
|
|
size = tensor.numel() * tensor.element_size()
|
|
if size <= 0:
|
|
return tensor.to(target_device, non_blocking=non_blocking)
|
|
|
|
global _INT8_FILE_SLICE_LOAD_WARNED
|
|
try:
|
|
host_buffer = comfy_aimdo_host_buffer.HostBuffer(size)
|
|
host_tensor = comfy_aimdo_torch.hostbuf_to_tensor(host_buffer)
|
|
host_view = host_tensor[:size].view(dtype=tensor.dtype).view(tensor.shape)
|
|
if comfy_memory_management.read_tensor_file_slice_into(tensor, host_view):
|
|
output = torch.empty_like(tensor, device=target_device)
|
|
output.copy_(host_view, non_blocking=False)
|
|
return output
|
|
except Exception as e:
|
|
if not _INT8_FILE_SLICE_LOAD_WARNED:
|
|
logging.warning(f"INT8 file-slice load disabled for this tensor; falling back to tensor.to() ({e}).")
|
|
_INT8_FILE_SLICE_LOAD_WARNED = True
|
|
|
|
return tensor.to(target_device, non_blocking=non_blocking)
|
|
|
|
def _is_float8_dtype(dtype: torch.dtype) -> bool:
|
|
return dtype in _FLOAT8_DTYPES
|
|
|
|
def _normalize_outlier_method(method) -> str:
|
|
if not isinstance(method, str):
|
|
return OUTLIER_METHOD_NONE
|
|
|
|
normalized = method.strip().lower()
|
|
if normalized in OUTLIER_METHOD_CHOICES:
|
|
return normalized
|
|
return OUTLIER_METHOD_NONE
|
|
|
|
def normalize_quantization_mode(mode) -> str:
|
|
if not isinstance(mode, str):
|
|
return QUANTIZATION_MODE_INT8
|
|
|
|
normalized = mode.strip().lower()
|
|
if normalized in QUANTIZATION_MODE_CHOICES:
|
|
return normalized
|
|
return QUANTIZATION_MODE_INT8
|
|
|
|
def quantization_mode_outlier_method(mode) -> str:
|
|
normalized = normalize_quantization_mode(mode)
|
|
return {
|
|
QUANTIZATION_MODE_INT8_CONVROT: OUTLIER_METHOD_CONVROT,
|
|
QUANTIZATION_MODE_INT8_QUAROT: OUTLIER_METHOD_QUAROT,
|
|
QUANTIZATION_MODE_INT8_HADANORM: OUTLIER_METHOD_HADANORM,
|
|
}.get(normalized, OUTLIER_METHOD_NONE)
|
|
|
|
def quantization_mode_is_int4(mode) -> bool:
|
|
return normalize_quantization_mode(mode) in (
|
|
QUANTIZATION_MODE_INT4_MIXED,
|
|
QUANTIZATION_MODE_INT4_FULL,
|
|
)
|
|
|
|
def native_int4_available() -> bool:
|
|
return _NATIVE_INT4_AVAILABLE
|
|
|
|
def _is_native_int4_tensor(value) -> bool:
|
|
return bool(
|
|
_NATIVE_INT4_AVAILABLE
|
|
and isinstance(value, QuantizedTensor)
|
|
and getattr(value, "_layout_cls", None) == "TensorCoreConvRotW4A4Layout"
|
|
)
|
|
|
|
def _make_native_int4_tensor(qdata, scale, orig_dtype, orig_shape, convrot_groupsize=256, linear_dtype="int4"):
|
|
if not _NATIVE_INT4_AVAILABLE:
|
|
raise RuntimeError("Native INT4 requires a recent ComfyUI and comfy-kitchen installation")
|
|
|
|
params = TensorCoreConvRotW4A4Layout.Params(
|
|
scale=scale,
|
|
orig_dtype=orig_dtype,
|
|
orig_shape=tuple(orig_shape),
|
|
convrot_groupsize=int(convrot_groupsize),
|
|
quant_group_size=_INT4_QUANT_GROUP_SIZE,
|
|
linear_dtype=str(linear_dtype),
|
|
)
|
|
return QuantizedTensor(qdata, "TensorCoreConvRotW4A4Layout", params)
|
|
|
|
def quantize_native_int4(weight, linear_dtype="int4", stochastic_rounding=0):
|
|
if not _NATIVE_INT4_AVAILABLE:
|
|
raise RuntimeError("Native INT4 requires a recent ComfyUI and comfy-kitchen installation")
|
|
if weight.ndim != 2:
|
|
raise ValueError("ConvRot W4A4 requires a two-dimensional weight")
|
|
if weight.shape[1] % _CONVROT_GROUP_SIZE != 0:
|
|
raise ValueError(
|
|
f"ConvRot W4A4 in_features={weight.shape[1]} is not divisible by {_CONVROT_GROUP_SIZE}"
|
|
)
|
|
|
|
return QuantizedTensor.from_float(
|
|
weight,
|
|
"TensorCoreConvRotW4A4Layout",
|
|
convrot_groupsize=_CONVROT_GROUP_SIZE,
|
|
quant_group_size=_INT4_QUANT_GROUP_SIZE,
|
|
stochastic_rounding=stochastic_rounding,
|
|
linear_dtype=linear_dtype,
|
|
)
|
|
|
|
def _get_module_outlier_method(module) -> str:
|
|
method = _normalize_outlier_method(getattr(module, "_outlier_method", None))
|
|
if method != OUTLIER_METHOD_NONE:
|
|
return method
|
|
if bool(getattr(module, "_use_quarot", False)):
|
|
return OUTLIER_METHOD_QUAROT
|
|
return OUTLIER_METHOD_NONE
|
|
|
|
def _outlier_method_uses_hadamard(method) -> bool:
|
|
normalized = _normalize_outlier_method(method)
|
|
return normalized in (OUTLIER_METHOD_QUAROT, OUTLIER_METHOD_CONVROT, OUTLIER_METHOD_HADANORM)
|
|
|
|
def _get_outlier_group_size(method) -> int:
|
|
normalized = _normalize_outlier_method(method)
|
|
if normalized == OUTLIER_METHOD_CONVROT:
|
|
return _CONVROT_GROUP_SIZE
|
|
return _QUAROT_GROUP_SIZE
|
|
|
|
def _build_outlier_hadamard(method, group_size: int, device=None, dtype=torch.float32):
|
|
normalized = _normalize_outlier_method(method)
|
|
if normalized == OUTLIER_METHOD_CONVROT:
|
|
if not _CONVROT_AVAILABLE:
|
|
return None
|
|
return _convrot_build_hadamard(group_size, device=device, dtype=dtype)
|
|
if not _QUAROT_AVAILABLE:
|
|
return None
|
|
return _quarot_build_hadamard(group_size, device=device, dtype=dtype)
|
|
|
|
def _validate_convrot_group_size(group_size, in_features: int, context: str) -> int:
|
|
try:
|
|
group_size = int(group_size)
|
|
except (TypeError, ValueError) as e:
|
|
raise ValueError(f"{context}: invalid ConvRot group size {group_size!r}") from e
|
|
|
|
if group_size <= 0:
|
|
raise ValueError(f"{context}: ConvRot group size must be positive, got {group_size}")
|
|
if group_size < 4 or (group_size & (group_size - 1)) != 0 or ((group_size.bit_length() - 1) % 2) != 0:
|
|
raise ValueError(f"{context}: ConvRot group size must be a power of 4, got {group_size}")
|
|
if in_features % group_size != 0:
|
|
raise ValueError(
|
|
f"{context}: in_features {in_features} is not divisible by ConvRot group size {group_size}"
|
|
)
|
|
return group_size
|
|
|
|
def _rotate_weight_for_outlier_method(weight: Tensor, h_matrix: Tensor, group_size: int, method) -> Tensor:
|
|
normalized = _normalize_outlier_method(method)
|
|
if normalized == OUTLIER_METHOD_CONVROT:
|
|
return _convrot_rotate_weight(weight, h_matrix, group_size=group_size)
|
|
return _quarot_rotate_weight(weight, h_matrix, group_size=group_size)
|
|
|
|
def _compute_hadanorm_sigma(weight: Tensor) -> Tensor:
|
|
weight_f = weight.float()
|
|
channel_max = weight_f.abs().amax(dim=0)
|
|
sigma = channel_max.clamp(min=_HADANORM_EPS).pow(-(1.0 - _HADANORM_ALPHA))
|
|
return sigma.clamp(min=_HADANORM_EPS)
|
|
|
|
def quantize_int8(x: Tensor, scale: float | Tensor) -> Tensor:
|
|
return x.float().mul(1.0 / scale).round_().clamp_(-128.0, 127.0).to(torch.int8)
|
|
|
|
def quantize_int8_tensorwise(x: Tensor) -> tuple[Tensor, Tensor]:
|
|
abs_max = x.abs().max()
|
|
scale = (abs_max.float() / 127.0).clamp(min=1e-30)
|
|
return quantize_int8(x, scale), scale
|
|
|
|
def quantize_int8_axiswise(x: Tensor, dim: int) -> tuple[Tensor, Tensor]:
|
|
abs_max = x.abs().amax(dim=dim, keepdim=True)
|
|
scale = (abs_max.float() / 127.0).clamp(min=1e-30)
|
|
return quantize_int8(x, scale), scale
|
|
|
|
def quantize_int8_rowwise(x: Tensor) -> tuple[Tensor, Tensor]:
|
|
"""
|
|
Per-row (out_features-wise) INT8 quantization for weight matrices.
|
|
Returns:
|
|
q_weight: INT8 tensor [rows, cols]
|
|
q_scale: FP32 scale tensor [rows, 1]
|
|
"""
|
|
x_for_quant = x
|
|
if _is_float8_dtype(x_for_quant.dtype):
|
|
# Convert FP8 weights to a quantization-friendly compute dtype first.
|
|
x_for_quant = x_for_quant.to(torch.float16)
|
|
|
|
if _TRITON_AVAILABLE and x_for_quant.is_cuda and x_for_quant.ndim == 2 and x_for_quant.shape[1] <= TRITON_ROWWISE_QUANT_MAX_COLS:
|
|
try:
|
|
return triton_quantize_rowwise(x_for_quant)
|
|
except Exception:
|
|
pass
|
|
|
|
return quantize_int8_axiswise(x_for_quant, dim=-1)
|
|
|
|
def dequantize(q: Tensor, scale: float | Tensor) -> Tensor:
|
|
if isinstance(scale, torch.Tensor) and scale.device != q.device:
|
|
scale = scale.to(q.device, non_blocking=True)
|
|
return q.float() * scale
|
|
|
|
def _normalize_runtime_backend(runtime_backend) -> str:
|
|
return runtime_backend if runtime_backend in INT8_BACKEND_CHOICES else DEFAULT_INT8_BACKEND
|
|
|
|
def _normalize_small_batch_fallback(small_batch_fallback) -> str:
|
|
return small_batch_fallback if small_batch_fallback in SMALL_BATCH_FALLBACK_CHOICES else DEFAULT_SMALL_BATCH_FALLBACK
|
|
|
|
def get_current_int8_runtime_config() -> dict:
|
|
return {
|
|
"runtime_backend": _normalize_runtime_backend(getattr(Int8TensorwiseOps, "runtime_backend", DEFAULT_INT8_BACKEND)),
|
|
"small_batch_fallback": _normalize_small_batch_fallback(getattr(Int8TensorwiseOps, "small_batch_fallback_mode", DEFAULT_SMALL_BATCH_FALLBACK)),
|
|
"prepack_int8_weights": bool(getattr(Int8TensorwiseOps, "prepack_int8_weights", False)),
|
|
}
|
|
|
|
def configure_int8_module_runtime(
|
|
module,
|
|
small_batch_fallback=None,
|
|
runtime_backend=None,
|
|
prepack_int8_weights=None,
|
|
):
|
|
runtime_backend = _normalize_runtime_backend(
|
|
runtime_backend if runtime_backend is not None else getattr(module, "_runtime_backend", DEFAULT_INT8_BACKEND)
|
|
)
|
|
small_batch_fallback = _normalize_small_batch_fallback(
|
|
small_batch_fallback if small_batch_fallback is not None else getattr(module, "_small_batch_fallback_mode", DEFAULT_SMALL_BATCH_FALLBACK)
|
|
)
|
|
prepack_int8_weights = (
|
|
bool(prepack_int8_weights)
|
|
if prepack_int8_weights is not None
|
|
else bool(getattr(module, "_prepack_int8_weights", False))
|
|
)
|
|
|
|
module._runtime_backend = runtime_backend
|
|
module._runtime_uses_triton = runtime_backend in (INT8_BACKEND_TRITON, INT8_BACKEND_TRITON_LEGACY_UNSAFE)
|
|
module._runtime_uses_legacy_triton = runtime_backend == INT8_BACKEND_TRITON_LEGACY_UNSAFE
|
|
module._small_batch_fallback_mode = small_batch_fallback
|
|
module._prepack_int8_weights = prepack_int8_weights
|
|
return module
|
|
|
|
def _prepack_int8_weight(weight: Tensor | None):
|
|
if not isinstance(weight, torch.Tensor) or weight.dtype != torch.int8 or weight.ndim != 2:
|
|
return None
|
|
return weight.detach().T.contiguous()
|
|
|
|
def _prepack_torch_int_mm_weight(weight: Tensor | None):
|
|
if not isinstance(weight, torch.Tensor) or weight.dtype != torch.int8 or weight.ndim != 2:
|
|
return None
|
|
|
|
out_features = int(weight.shape[0])
|
|
in_features = int(weight.shape[1])
|
|
padded_out_features = out_features
|
|
padded_in_features = in_features
|
|
|
|
if padded_out_features % 8 != 0:
|
|
padded_out_features += 8 - (padded_out_features % 8)
|
|
if padded_in_features % 8 != 0:
|
|
padded_in_features += 8 - (padded_in_features % 8)
|
|
|
|
if padded_out_features == out_features and padded_in_features == in_features:
|
|
return None
|
|
|
|
padded_weight = torch.zeros(
|
|
(padded_out_features, padded_in_features),
|
|
device=weight.device,
|
|
dtype=weight.dtype,
|
|
)
|
|
padded_weight[:out_features, :in_features].copy_(weight.detach())
|
|
return padded_weight.T.contiguous()
|
|
|
|
def _get_prepacked_weight(linear_module, device: torch.device):
|
|
packed = getattr(linear_module, "weight_packed", None)
|
|
if not isinstance(packed, torch.Tensor):
|
|
return None
|
|
if packed.device != device:
|
|
packed = packed.to(device, non_blocking=True)
|
|
return packed
|
|
|
|
def _get_torch_int_mm_weight(linear_module, weight: Tensor, device: torch.device):
|
|
packed = getattr(linear_module, "weight_int_mm", None)
|
|
if isinstance(packed, torch.Tensor):
|
|
return packed if packed.device == device else packed.to(device, non_blocking=True)
|
|
return weight.T
|
|
|
|
def _torch_int_mm_safe(a: Tensor, b: Tensor, output_columns: int | None = None) -> Tensor:
|
|
rows = int(a.shape[0])
|
|
columns = int(output_columns) if output_columns is not None else int(b.shape[1])
|
|
inner = int(a.shape[1])
|
|
b_inner = int(b.shape[0])
|
|
|
|
if a.is_cuda and a.shape[0] <= 16:
|
|
pad_rows = 17 - rows
|
|
if pad_rows > 0:
|
|
padding = torch.zeros((pad_rows, a.shape[1]), device=a.device, dtype=a.dtype)
|
|
a = torch.cat((a, padding), dim=0)
|
|
|
|
if a.is_cuda and inner > 0:
|
|
target_inner = max(inner, b_inner)
|
|
if target_inner % 8 != 0:
|
|
target_inner += 8 - (target_inner % 8)
|
|
if int(a.shape[1]) < target_inner:
|
|
pad_inner = target_inner - int(a.shape[1])
|
|
padding = torch.zeros((a.shape[0], pad_inner), device=a.device, dtype=a.dtype)
|
|
a = torch.cat((a, padding), dim=1)
|
|
if int(b.shape[0]) < target_inner:
|
|
pad_inner = target_inner - int(b.shape[0])
|
|
padding = torch.zeros((pad_inner, b.shape[1]), device=b.device, dtype=b.dtype)
|
|
b = torch.cat((b, padding), dim=0)
|
|
|
|
if b.is_cuda and (columns % 8) != 0:
|
|
if int(b.shape[1]) < columns + (8 - (columns % 8)):
|
|
pad_columns = columns + (8 - (columns % 8)) - int(b.shape[1])
|
|
padding = torch.zeros((b.shape[0], pad_columns), device=b.device, dtype=b.dtype)
|
|
b = torch.cat((b, padding), dim=1)
|
|
|
|
return torch._int_mm(a, b)[:rows, :columns]
|
|
|
|
def _torch_int_mm_dispatch(a: Tensor, b: Tensor, output_columns: int | None = None) -> Tensor:
|
|
"""Use the native fast path when CUDA's alignment constraints are already met."""
|
|
columns = int(output_columns) if output_columns is not None else int(b.shape[1])
|
|
if (
|
|
(not a.is_cuda or int(a.shape[0]) > 16)
|
|
and int(a.shape[1]) == int(b.shape[0])
|
|
and int(a.shape[1]) % 8 == 0
|
|
and columns % 8 == 0
|
|
):
|
|
return torch._int_mm(a, b)
|
|
return _torch_int_mm_safe(a, b, output_columns=columns)
|
|
|
|
def stochastic_round_int8_delta(x: Tensor, scale: float | Tensor, seed: int = 0) -> Tensor:
|
|
"""
|
|
Quantize a delta tensor to INT8 using stochastic rounding.
|
|
Used for LoRA deltas to minimize quantization error.
|
|
"""
|
|
generator = torch.Generator(device=x.device)
|
|
generator.manual_seed(seed)
|
|
|
|
# Scale to INT8 range
|
|
if isinstance(scale, torch.Tensor) and scale.device != x.device:
|
|
scale = scale.to(x.device, non_blocking=True)
|
|
x_scaled = x / scale
|
|
|
|
# Stochastic rounding
|
|
x_floor = torch.floor(x_scaled)
|
|
fraction = x_scaled - x_floor
|
|
del x_scaled
|
|
|
|
# Speed optimization: Create random values directly on the target device
|
|
random_vals = torch.rand(x_floor.shape, generator=generator, device=x.device, dtype=x_floor.dtype)
|
|
x_rounded = torch.where(random_vals < fraction, x_floor + 1, x_floor)
|
|
del random_vals
|
|
del fraction
|
|
del x_floor
|
|
|
|
return torch.clamp(x_rounded, -128, 127).to(torch.int8)
|
|
|
|
|
|
# --- LinearW8A8 Core ---
|
|
|
|
@torch.no_grad()
|
|
@_disable_torch_compile
|
|
def int8_forward_dynamic(
|
|
x: Tensor,
|
|
weight: Tensor,
|
|
weight_scale: float | Tensor,
|
|
bias: Tensor | None,
|
|
compute_dtype: torch.dtype,
|
|
use_triton: bool = True,
|
|
weight_packed: Tensor | None = None,
|
|
weight_int_mm: Tensor | None = None,
|
|
legacy_triton_unsafe: bool = False,
|
|
) -> Tensor:
|
|
"""Forward with dynamic per-token activation quantization."""
|
|
|
|
# --- FAST PATH: Triton Fused Kernel ---
|
|
if _TRITON_AVAILABLE and use_triton and x.is_cuda and x.shape[-1] <= TRITON_ROWWISE_QUANT_MAX_COLS:
|
|
if isinstance(weight_packed, torch.Tensor):
|
|
return triton_int8_linear(x, weight_packed, weight_scale, bias, compute_dtype, weight_is_prepacked=True, legacy_unsafe=legacy_triton_unsafe)
|
|
return triton_int8_linear(x, weight, weight_scale, bias, compute_dtype, legacy_unsafe=legacy_triton_unsafe)
|
|
|
|
# --- SLOW PATH: Standard PyTorch ---
|
|
# Quantize activations per row (dynamic)
|
|
x_8, x_scale = quantize_int8_axiswise(x, dim=-1)
|
|
if isinstance(weight_scale, torch.Tensor) and weight_scale.device != x.device:
|
|
weight_scale = weight_scale.to(x.device, non_blocking=True)
|
|
|
|
# INT8 Matmul (Outputs Int32)
|
|
matmul_weight = weight_int_mm if isinstance(weight_int_mm, torch.Tensor) else weight.T
|
|
res = _torch_int_mm_dispatch(x_8, matmul_weight, output_columns=weight.shape[0])
|
|
|
|
# Dequantize: (res * weight_scale * x_scale)
|
|
# Note: Creating intermediate Float tensors here is VRAM heavy
|
|
res_scaled = res.float().mul_(x_scale).mul_(weight_scale).to(compute_dtype)
|
|
|
|
if bias is not None:
|
|
res_scaled = res_scaled + bias.to(compute_dtype)
|
|
return res_scaled
|
|
|
|
@torch.no_grad()
|
|
@_disable_torch_compile
|
|
def int8_forward_dynamic_per_row(
|
|
x: Tensor,
|
|
weight: Tensor,
|
|
weight_scale: Tensor,
|
|
bias: Tensor | None,
|
|
compute_dtype: torch.dtype,
|
|
use_triton: bool = True,
|
|
weight_packed: Tensor | None = None,
|
|
weight_int_mm: Tensor | None = None,
|
|
legacy_triton_unsafe: bool = False,
|
|
) -> Tensor:
|
|
"""Forward with dynamic per-token activation quantization and per-row weight quantization."""
|
|
|
|
# --- FAST PATH: Triton Fused Kernel (per-row) ---
|
|
if _TRITON_AVAILABLE and use_triton and x.is_cuda and x.shape[-1] <= TRITON_ROWWISE_QUANT_MAX_COLS:
|
|
if isinstance(weight_packed, torch.Tensor):
|
|
return triton_int8_linear_per_row(x, weight_packed, weight_scale, bias, compute_dtype, weight_is_prepacked=True, legacy_unsafe=legacy_triton_unsafe)
|
|
return triton_int8_linear_per_row(x, weight, weight_scale, bias, compute_dtype, legacy_unsafe=legacy_triton_unsafe)
|
|
|
|
# --- SLOW PATH: Standard PyTorch ---
|
|
x_8, x_scale = quantize_int8_axiswise(x, dim=-1)
|
|
if weight_scale.device != x.device:
|
|
weight_scale = weight_scale.to(x.device, non_blocking=True)
|
|
|
|
# INT8 Matmul (Outputs Int32)
|
|
matmul_weight = weight_int_mm if isinstance(weight_int_mm, torch.Tensor) else weight.T
|
|
res = _torch_int_mm_dispatch(x_8, matmul_weight, output_columns=weight.shape[0])
|
|
|
|
# Dequantize with per-row weight scales
|
|
# res[i, j] = sum_k(x_8[i, k] * weight[j, k]) * x_scale[i] * weight_scale[j]
|
|
res_scaled = res.float().mul_(x_scale).mul_(weight_scale.T).to(compute_dtype)
|
|
|
|
if bias is not None:
|
|
res_scaled = res_scaled + bias.to(compute_dtype)
|
|
return res_scaled
|
|
|
|
def _native_int8_convrot_linear(
|
|
x: Tensor,
|
|
weight: Tensor,
|
|
weight_scale: Tensor,
|
|
bias: Tensor | None,
|
|
compute_dtype: torch.dtype,
|
|
group_size: int,
|
|
) -> Tensor:
|
|
dtype_code = {
|
|
torch.float32: 0,
|
|
torch.float16: 1,
|
|
torch.bfloat16: 2,
|
|
}[compute_dtype]
|
|
return torch.ops.comfy_kitchen.int8_linear(
|
|
x.contiguous(),
|
|
weight.contiguous(),
|
|
weight_scale,
|
|
bias,
|
|
dtype_code,
|
|
True,
|
|
group_size,
|
|
)
|
|
|
|
def _normalize_dynamic_offset(offset):
|
|
if offset is None:
|
|
return None
|
|
if not isinstance(offset, (tuple, list)) or len(offset) < 3:
|
|
return None
|
|
try:
|
|
return int(offset[0]), int(offset[1]), int(offset[2])
|
|
except Exception:
|
|
return None
|
|
|
|
def _resolve_dynamic_entry_tensors(entry, device: torch.device):
|
|
entry_A = entry.get("A")
|
|
entry_B = entry.get("B")
|
|
if entry_A is None or entry_B is None:
|
|
return None, None
|
|
|
|
lA = entry.get("_A_cached")
|
|
lB = entry.get("_B_cached")
|
|
|
|
if lA is None or lA.device != device:
|
|
lA = entry_A if entry_A.device == device else entry_A.to(device, non_blocking=True)
|
|
entry["_A_cached"] = lA
|
|
if lB is None or lB.device != device:
|
|
lB = entry_B if entry_B.device == device else entry_B.to(device, non_blocking=True)
|
|
entry["_B_cached"] = lB
|
|
|
|
return lA, lB
|
|
|
|
def _can_batch_dynamic_entries(prepared_entries, output_length: int | None):
|
|
if not _DYNAMIC_LORA_BATCH or len(prepared_entries) < 2:
|
|
return False
|
|
|
|
rank_total = 0
|
|
input_width = None
|
|
output_width = None
|
|
a_dtype = None
|
|
b_dtype = None
|
|
|
|
for _, lA, lB in prepared_entries:
|
|
if not isinstance(lA, torch.Tensor) or not isinstance(lB, torch.Tensor):
|
|
return False
|
|
if lA.ndim != 2 or lB.ndim != 2:
|
|
return False
|
|
if lB.shape[1] != lA.shape[0]:
|
|
return False
|
|
|
|
if input_width is None:
|
|
input_width = lA.shape[1]
|
|
elif input_width != lA.shape[1]:
|
|
return False
|
|
|
|
if output_width is None:
|
|
output_width = lB.shape[0]
|
|
elif output_width != lB.shape[0]:
|
|
return False
|
|
|
|
if a_dtype is None:
|
|
a_dtype = lA.dtype
|
|
elif a_dtype != lA.dtype:
|
|
return False
|
|
|
|
if b_dtype is None:
|
|
b_dtype = lB.dtype
|
|
elif b_dtype != lB.dtype:
|
|
return False
|
|
|
|
rank_total += int(lA.shape[0])
|
|
if rank_total > _DYNAMIC_LORA_BATCH_MAX_RANK:
|
|
return False
|
|
|
|
if output_length is not None and output_width is not None and output_width != output_length:
|
|
return False
|
|
|
|
return True
|
|
|
|
def _get_small_batch_fallback_threshold(linear_module) -> int:
|
|
mode = getattr(linear_module, "_small_batch_fallback_mode", None)
|
|
if mode is None:
|
|
mode = getattr(Int8TensorwiseOps, "small_batch_fallback_mode", SMALL_BATCH_FALLBACK_SMALL_LAYERS)
|
|
if mode not in SMALL_BATCH_FALLBACK_CHOICES:
|
|
mode = SMALL_BATCH_FALLBACK_SMALL_LAYERS
|
|
if mode == SMALL_BATCH_FALLBACK_NEVER:
|
|
return 0
|
|
|
|
base_rows = _SMALL_BATCH_FALLBACK_MAX_ROWS
|
|
if base_rows <= 0:
|
|
return 0
|
|
|
|
if not _ADAPTIVE_SMALL_BATCH_FALLBACK:
|
|
return base_rows
|
|
|
|
weight = getattr(linear_module, "weight", None)
|
|
if not isinstance(weight, torch.Tensor) or weight.ndim < 2:
|
|
return base_rows
|
|
|
|
out_features = int(weight.shape[0])
|
|
in_features = int(weight.shape[1])
|
|
matmul_size = out_features * in_features
|
|
if mode == SMALL_BATCH_FALLBACK_SMALL_LAYERS and matmul_size > _SMALL_LAYER_MAX_PARAMS:
|
|
return 0
|
|
|
|
rows = base_rows
|
|
if matmul_size >= 12_000_000:
|
|
rows = min(rows, 6)
|
|
elif matmul_size >= 8_000_000:
|
|
rows = min(rows, 8)
|
|
elif matmul_size >= 4_000_000:
|
|
rows = min(rows, 12)
|
|
elif matmul_size <= 1_000_000:
|
|
rows = min(32, max(rows, base_rows + 6))
|
|
|
|
if getattr(linear_module, "_is_per_row", False):
|
|
rows = max(_SMALL_BATCH_FALLBACK_MIN_ROWS, rows - 2)
|
|
|
|
dynamic_entries = getattr(linear_module, "dynamic_lora_entries", None)
|
|
dynamic_count = len(dynamic_entries) if dynamic_entries else 0
|
|
if dynamic_count >= 4:
|
|
rows = max(_SMALL_BATCH_FALLBACK_MIN_ROWS, rows - 4)
|
|
elif dynamic_count >= 2:
|
|
rows = max(_SMALL_BATCH_FALLBACK_MIN_ROWS, rows - 2)
|
|
|
|
return max(_SMALL_BATCH_FALLBACK_MIN_ROWS, rows)
|
|
|
|
@torch.no_grad()
|
|
@_disable_torch_compile
|
|
def apply_dynamic_lora_delta(
|
|
x_input: Tensor,
|
|
y: Tensor,
|
|
lora_A: Tensor | None,
|
|
lora_B: Tensor | None,
|
|
lora_alpha,
|
|
lora_entries,
|
|
device: torch.device,
|
|
correction_x: Tensor | None = None,
|
|
) -> Tensor:
|
|
if lora_entries:
|
|
grouped_entries = {}
|
|
for entry in lora_entries:
|
|
offset_key = _normalize_dynamic_offset(entry.get("offset"))
|
|
grouped_entries.setdefault(offset_key, []).append(entry)
|
|
|
|
for offset_key, entries in grouped_entries.items():
|
|
dim = None
|
|
start = None
|
|
length = None
|
|
x_src = x_input
|
|
correction_src = correction_x
|
|
|
|
if offset_key is not None:
|
|
dim, start, length = offset_key
|
|
if dim == 1:
|
|
if not (start >= 0 and length > 0 and (start + length) <= x_input.shape[-1]):
|
|
if _DYNAMIC_LORA_DEBUG:
|
|
logging.info(
|
|
f"Quantization Toolkit Dynamic LoRA: skipping invalid input offset={offset_key} "
|
|
f"for x shape={tuple(x_input.shape)}"
|
|
)
|
|
continue
|
|
x_src = x_input.narrow(-1, start, length)
|
|
if correction_x is not None:
|
|
correction_src = correction_x.narrow(-1, start, length)
|
|
elif dim == 0:
|
|
if not (start >= 0 and length > 0 and (start + length) <= y.shape[-1]):
|
|
if _DYNAMIC_LORA_DEBUG:
|
|
logging.info(
|
|
f"Quantization Toolkit Dynamic LoRA: skipping invalid output offset={offset_key} "
|
|
f"for y shape={tuple(y.shape)}"
|
|
)
|
|
continue
|
|
|
|
prepared_entries = []
|
|
for entry in entries:
|
|
lA, lB = _resolve_dynamic_entry_tensors(entry, device)
|
|
if lA is None or lB is None:
|
|
continue
|
|
prepared_entries.append((entry, lA, lB))
|
|
|
|
if not prepared_entries:
|
|
continue
|
|
|
|
output_length = length if dim == 0 else None
|
|
batched = _can_batch_dynamic_entries(prepared_entries, output_length)
|
|
if batched:
|
|
cat_A = torch.cat([lA for _, lA, _ in prepared_entries], dim=0)
|
|
cat_B = torch.cat([lB for _, _, lB in prepared_entries], dim=1)
|
|
lora_x = F.linear(x_src.to(cat_A.dtype), cat_A)
|
|
lora_y = F.linear(lora_x, cat_B).to(y.dtype)
|
|
correction_y = None
|
|
if correction_src is not None:
|
|
correction_proj = F.linear(correction_src.to(cat_A.dtype), cat_A)
|
|
correction_y = F.linear(correction_proj, cat_B).to(y.dtype)
|
|
|
|
if dim == 0:
|
|
y.narrow(-1, start, length).add_(lora_y)
|
|
if correction_y is not None:
|
|
y.narrow(-1, start, length).add_(correction_y)
|
|
else:
|
|
if lora_y.shape[-1] != y.shape[-1]:
|
|
if _DYNAMIC_LORA_DEBUG:
|
|
logging.info(
|
|
f"Quantization Toolkit Dynamic LoRA: skipping mismatched batched add "
|
|
f"lora_y={tuple(lora_y.shape)} y={tuple(y.shape)} offset={offset_key}"
|
|
)
|
|
continue
|
|
y.add_(lora_y)
|
|
if correction_y is not None:
|
|
y.add_(correction_y)
|
|
continue
|
|
|
|
for _, lA, lB in prepared_entries:
|
|
lora_x = F.linear(x_src.to(lA.dtype), lA)
|
|
lora_y = F.linear(lora_x, lB).to(y.dtype)
|
|
correction_y = None
|
|
if correction_src is not None:
|
|
correction_proj = F.linear(correction_src.to(lA.dtype), lA)
|
|
correction_y = F.linear(correction_proj, lB).to(y.dtype)
|
|
|
|
if dim == 0:
|
|
if lora_y.shape[-1] != length:
|
|
if _DYNAMIC_LORA_DEBUG:
|
|
logging.info(
|
|
f"Quantization Toolkit Dynamic LoRA: skipping mismatched output slice "
|
|
f"offset={offset_key} lora_y={tuple(lora_y.shape)} y={tuple(y.shape)}"
|
|
)
|
|
continue
|
|
y.narrow(-1, start, length).add_(lora_y)
|
|
if correction_y is not None:
|
|
y.narrow(-1, start, length).add_(correction_y)
|
|
continue
|
|
|
|
if lora_y.shape[-1] != y.shape[-1]:
|
|
if _DYNAMIC_LORA_DEBUG:
|
|
logging.info(
|
|
f"Quantization Toolkit Dynamic LoRA: skipping mismatched full add "
|
|
f"lora_y={tuple(lora_y.shape)} y={tuple(y.shape)} offset={offset_key}"
|
|
)
|
|
continue
|
|
|
|
y.add_(lora_y)
|
|
if correction_y is not None:
|
|
y.add_(correction_y)
|
|
|
|
return y
|
|
|
|
if lora_A is None or lora_B is None:
|
|
return y
|
|
|
|
lA = lora_A if lora_A.device == device else lora_A.to(device, non_blocking=True)
|
|
lB = lora_B if lora_B.device == device else lora_B.to(device, non_blocking=True)
|
|
|
|
lora_x = F.linear(x_input.to(lA.dtype), lA)
|
|
lora_y = F.linear(lora_x, lB)
|
|
|
|
if lora_alpha is not None:
|
|
lora_y = lora_y * lora_alpha
|
|
|
|
output = y + lora_y.to(y.dtype)
|
|
if correction_x is not None:
|
|
correction_proj = F.linear(correction_x.to(lA.dtype), lA)
|
|
correction_y = F.linear(correction_proj, lB)
|
|
if lora_alpha is not None:
|
|
correction_y = correction_y * lora_alpha
|
|
output = output + correction_y.to(y.dtype)
|
|
|
|
return output
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
# INT8 LoRA Adapter - High Precision, Low RAM Patching
|
|
# =============================================================================
|
|
|
|
def _unpack_lora_weights(weights):
|
|
if not isinstance(weights, (list, tuple)) or len(weights) < 2:
|
|
return None
|
|
|
|
mat1 = weights[0]
|
|
mat2 = weights[1]
|
|
alpha = weights[2] if len(weights) > 2 else None
|
|
mid = weights[3] if len(weights) > 3 else None
|
|
dora_scale = weights[4] if len(weights) > 4 else None
|
|
reshape = weights[5] if len(weights) > 5 else None
|
|
|
|
if not isinstance(mat1, torch.Tensor) or not isinstance(mat2, torch.Tensor):
|
|
return None
|
|
|
|
return mat1, mat2, alpha, mid, dora_scale, reshape
|
|
|
|
|
|
def _compute_lora_scale(weights, strength):
|
|
unpacked = _unpack_lora_weights(weights)
|
|
if unpacked is None:
|
|
return strength
|
|
|
|
_, mat2, alpha, _, _, _ = unpacked
|
|
rank = mat2.shape[0] if mat2.ndim >= 2 else 1
|
|
if alpha is None:
|
|
return strength
|
|
|
|
return (alpha / rank) * strength
|
|
|
|
|
|
def _compute_fast_lora_diff(weights, target_shape, device, intermediate_dtype):
|
|
unpacked = _unpack_lora_weights(weights)
|
|
if unpacked is None:
|
|
return None
|
|
|
|
mat1, mat2, _, mid, dora_scale, reshape = unpacked
|
|
if dora_scale is not None or reshape is not None:
|
|
return None
|
|
|
|
try:
|
|
mat1_f = mat1.to(device, dtype=intermediate_dtype)
|
|
mat2_f = mat2.to(device, dtype=intermediate_dtype)
|
|
|
|
if mid is not None:
|
|
mid_f = mid.to(device, dtype=intermediate_dtype)
|
|
final_shape = [mat2_f.shape[1], mat2_f.shape[0], mid_f.shape[2], mid_f.shape[3]]
|
|
mat2_f = (
|
|
torch.mm(
|
|
mat2_f.transpose(0, 1).flatten(start_dim=1),
|
|
mid_f.transpose(0, 1).flatten(start_dim=1),
|
|
)
|
|
.reshape(final_shape)
|
|
.transpose(0, 1)
|
|
)
|
|
|
|
return torch.mm(
|
|
mat1_f.flatten(start_dim=1),
|
|
mat2_f.flatten(start_dim=1),
|
|
).reshape(target_shape)
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _compute_dynamic_lora_factors(weights, strength):
|
|
unpacked = _unpack_lora_weights(weights)
|
|
if unpacked is None:
|
|
return None
|
|
|
|
mat1, mat2, _, mid, dora_scale, reshape = unpacked
|
|
if dora_scale is not None or reshape is not None:
|
|
return None
|
|
|
|
try:
|
|
mat2_eff = mat2
|
|
if mid is not None:
|
|
final_shape = [mat2.shape[1], mat2.shape[0], mid.shape[2], mid.shape[3]]
|
|
mat2_eff = (
|
|
torch.mm(
|
|
mat2.transpose(0, 1).flatten(start_dim=1),
|
|
mid.transpose(0, 1).flatten(start_dim=1),
|
|
)
|
|
.reshape(final_shape)
|
|
.transpose(0, 1)
|
|
)
|
|
|
|
scale = _compute_lora_scale(weights, strength)
|
|
return mat2_eff * scale, mat1
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _get_effective_weight_scale(weight_scale, row_count, offset=None):
|
|
if not isinstance(weight_scale, torch.Tensor) or weight_scale.numel() == 1:
|
|
return weight_scale
|
|
|
|
if weight_scale.shape[0] == row_count:
|
|
return weight_scale
|
|
|
|
if offset is not None and len(offset) >= 3:
|
|
try:
|
|
dim = int(offset[0])
|
|
start = int(offset[1])
|
|
size = int(offset[2])
|
|
if dim == 0 and size == row_count and start >= 0 and (start + size) <= weight_scale.shape[0]:
|
|
return weight_scale.narrow(0, start, size)
|
|
except Exception:
|
|
pass
|
|
|
|
# Unknown mapping between scale rows and weight rows.
|
|
# Fall back to scalar mean to avoid shape/device crashes.
|
|
return weight_scale.float().mean()
|
|
|
|
def _get_effective_hadanorm_sigma(hadanorm_sigma, input_width, offset=None):
|
|
if not isinstance(hadanorm_sigma, torch.Tensor):
|
|
return None
|
|
|
|
if hadanorm_sigma.numel() == input_width:
|
|
return hadanorm_sigma
|
|
|
|
normalized = _normalize_dynamic_offset(offset)
|
|
if normalized is None:
|
|
return None
|
|
|
|
dim, start, size = normalized
|
|
if dim != 1 or size != input_width:
|
|
return None
|
|
if start < 0 or (start + size) > hadanorm_sigma.numel():
|
|
return None
|
|
return hadanorm_sigma.narrow(0, start, size)
|
|
|
|
def _rotate_activation_runtime(x: Tensor, h_matrix: Tensor, group_size: int) -> Tensor:
|
|
original_shape = x.shape
|
|
feature_count = original_shape[-1]
|
|
group_count = feature_count // group_size
|
|
grouped_x = x.reshape(*original_shape[:-1], group_count, group_size)
|
|
return torch.matmul(grouped_x, h_matrix).reshape(original_shape)
|
|
|
|
def _hadamard_offset_supported(offset, input_width: int, group_size: int) -> bool:
|
|
normalized = _normalize_dynamic_offset(offset)
|
|
if normalized is None:
|
|
return True
|
|
|
|
dim, start, size = normalized
|
|
if dim == 0:
|
|
return True
|
|
if dim != 1:
|
|
return False
|
|
|
|
if size != input_width:
|
|
return False
|
|
if start < 0 or size <= 0:
|
|
return False
|
|
if (start % group_size) != 0 or (size % group_size) != 0:
|
|
return False
|
|
return True
|
|
|
|
def _transform_weight_like_for_outlier_method(
|
|
weight_like: Tensor,
|
|
outlier_method,
|
|
comp_device: torch.device,
|
|
hadanorm_sigma: Tensor | None = None,
|
|
offset=None,
|
|
) -> Tensor:
|
|
method = _normalize_outlier_method(outlier_method)
|
|
group_size = _get_outlier_group_size(method)
|
|
if method == OUTLIER_METHOD_NONE:
|
|
return weight_like
|
|
if weight_like.ndim < 2 or weight_like.shape[1] % group_size != 0:
|
|
return weight_like
|
|
if not _outlier_method_uses_hadamard(method):
|
|
return weight_like
|
|
if not _hadamard_offset_supported(offset, weight_like.shape[1], group_size):
|
|
normalized = _normalize_dynamic_offset(offset)
|
|
key = normalized if normalized is not None else "unsupported_offset"
|
|
if key not in _QUAROT_OFFSET_WARNED:
|
|
_QUAROT_OFFSET_WARNED.add(key)
|
|
logging.warning(
|
|
f"Quantization Toolkit INT8 {method}: skipping transform for unsupported offset={normalized} "
|
|
f"weight_shape={tuple(weight_like.shape)}"
|
|
)
|
|
return weight_like
|
|
|
|
try:
|
|
rotate_dtype = torch.float32 if weight_like.dtype in (torch.float16, torch.bfloat16) else weight_like.dtype
|
|
weight_work = weight_like.to(comp_device, dtype=rotate_dtype)
|
|
if method == OUTLIER_METHOD_HADANORM:
|
|
sigma = _get_effective_hadanorm_sigma(hadanorm_sigma, weight_like.shape[1], offset=offset)
|
|
if sigma is None:
|
|
logging.warning(
|
|
f"Quantization Toolkit INT8 HadaNorm: missing sigma for transformed weight_shape={tuple(weight_like.shape)} "
|
|
f"offset={_normalize_dynamic_offset(offset)}"
|
|
)
|
|
return weight_like
|
|
sigma = sigma.to(comp_device, dtype=rotate_dtype, non_blocking=True).view(1, -1)
|
|
weight_work = weight_work * sigma
|
|
h_matrix = _build_outlier_hadamard(method, group_size, device=comp_device, dtype=rotate_dtype)
|
|
if h_matrix is None:
|
|
return weight_like
|
|
rotated = _rotate_weight_for_outlier_method(weight_work, h_matrix, group_size=group_size, method=method)
|
|
return rotated.to(weight_like.dtype)
|
|
except Exception:
|
|
return weight_like
|
|
|
|
def _apply_outlier_activation_transform(
|
|
x: Tensor,
|
|
outlier_method,
|
|
hadamard: Tensor | None = None,
|
|
hadanorm_sigma: Tensor | None = None,
|
|
):
|
|
method = _normalize_outlier_method(outlier_method)
|
|
if method == OUTLIER_METHOD_NONE or not isinstance(hadamard, torch.Tensor):
|
|
return x, None
|
|
group_size = int(hadamard.shape[-1])
|
|
if group_size <= 0 or x.shape[-1] % group_size != 0:
|
|
return x, None
|
|
|
|
transform_dtype = torch.float32 if x.device.type == "cpu" and x.dtype in (torch.float16, torch.bfloat16) else x.dtype
|
|
x_work = x.to(transform_dtype) if x.dtype != transform_dtype else x
|
|
|
|
if method == OUTLIER_METHOD_HADANORM:
|
|
if not isinstance(hadanorm_sigma, torch.Tensor):
|
|
return x, None
|
|
sigma = hadanorm_sigma
|
|
if sigma.device != x_work.device or sigma.dtype != x_work.dtype:
|
|
sigma = sigma.to(x_work.device, dtype=x_work.dtype, non_blocking=True)
|
|
x_work = x_work / sigma.view(*([1] * (x_work.ndim - 1)), -1)
|
|
|
|
h_matrix = hadamard
|
|
if h_matrix.device != x_work.device or h_matrix.dtype != x_work.dtype:
|
|
h_matrix = h_matrix.to(x_work.device, dtype=x_work.dtype, non_blocking=True)
|
|
|
|
x_rotated = _rotate_activation_runtime(x_work, h_matrix, group_size)
|
|
if method == OUTLIER_METHOD_HADANORM:
|
|
correction_source = x_rotated.mean(dim=-2, keepdim=True)
|
|
return x_rotated - correction_source, correction_source
|
|
|
|
return x_rotated, None
|
|
|
|
def _apply_int8_delta_inplace(weight, delta_f, weight_scale, seed, offset=None):
|
|
comp_device = _get_int8_compute_device(weight.device)
|
|
delta_dev = delta_f.to(comp_device)
|
|
effective_scale = _get_effective_weight_scale(weight_scale, delta_dev.shape[0], offset)
|
|
delta_int8 = stochastic_round_int8_delta(delta_dev, effective_scale, seed)
|
|
del delta_dev
|
|
res = weight.to(comp_device, torch.int32) + delta_int8.to(comp_device, torch.int32)
|
|
del delta_int8
|
|
patched_weight = torch.clamp(res, -128, 127).to(torch.int8)
|
|
del res
|
|
if patched_weight.device != weight.device:
|
|
patched_weight = patched_weight.to(weight.device)
|
|
weight.copy_(patched_weight)
|
|
del patched_weight
|
|
return weight
|
|
|
|
def _decode_comfy_quant_config(value):
|
|
if not isinstance(value, torch.Tensor):
|
|
return None
|
|
try:
|
|
return json.loads(value.detach().cpu().numpy().tobytes())
|
|
except Exception:
|
|
return None
|
|
|
|
def _encode_comfy_quant_config(config: dict) -> Tensor:
|
|
return torch.tensor(list(json.dumps(config, separators=(",", ":")).encode("utf-8")), dtype=torch.uint8)
|
|
|
|
try:
|
|
from comfy.weight_adapter.lora import LoRAAdapter
|
|
_LORA_ADAPTER_AVAILABLE = True
|
|
except ImportError:
|
|
_LORA_ADAPTER_AVAILABLE = False
|
|
|
|
try:
|
|
from comfy.weight_adapter.base import WeightAdapterBase
|
|
_WEIGHT_ADAPTER_BASE_AVAILABLE = True
|
|
except ImportError:
|
|
_WEIGHT_ADAPTER_BASE_AVAILABLE = False
|
|
|
|
_ADAPTER_STATE_UNSET = object()
|
|
_INT8_LORA_PATCH_PAYLOAD = "quantization_toolkit_int8_lora_patch_v1"
|
|
_INT8_MERGED_LORA_PATCH_PAYLOAD = "quantization_toolkit_int8_merged_lora_patch_v1"
|
|
_INT8_WEIGHT_PATCH_PAYLOAD = "quantization_toolkit_int8_weight_patch_v1"
|
|
|
|
|
|
def _is_int8_adapter_payload(value, marker):
|
|
return isinstance(value, (tuple, list)) and len(value) >= 1 and value[0] == marker
|
|
|
|
if _LORA_ADAPTER_AVAILABLE:
|
|
class INT8LoRAPatchAdapter(LoRAAdapter):
|
|
"""
|
|
Specialized LoRA adapter that patches INT8 weights IN-PLACE in INT8 space.
|
|
"""
|
|
def __init__(self, loaded_keys, weights, weight_scale=_ADAPTER_STATE_UNSET, seed=0, use_quarot=False, outlier_method=None, hadanorm_sigma=None):
|
|
if _is_int8_adapter_payload(weights, _INT8_LORA_PATCH_PAYLOAD):
|
|
payload = weights
|
|
weights = payload[1]
|
|
if weight_scale is _ADAPTER_STATE_UNSET:
|
|
weight_scale = payload[2]
|
|
seed = payload[3]
|
|
outlier_method = payload[4]
|
|
hadanorm_sigma = payload[5]
|
|
elif weight_scale is _ADAPTER_STATE_UNSET:
|
|
weight_scale = 1.0
|
|
|
|
self.lora_weights = weights
|
|
self.weight_scale = weight_scale
|
|
self.seed = seed
|
|
resolved_method = _normalize_outlier_method(outlier_method)
|
|
if resolved_method == OUTLIER_METHOD_NONE and use_quarot:
|
|
resolved_method = OUTLIER_METHOD_QUAROT
|
|
self.outlier_method = resolved_method
|
|
self.hadanorm_sigma = hadanorm_sigma
|
|
payload = (
|
|
_INT8_LORA_PATCH_PAYLOAD,
|
|
self.lora_weights,
|
|
self.weight_scale,
|
|
self.seed,
|
|
self.outlier_method,
|
|
self.hadanorm_sigma,
|
|
)
|
|
super().__init__(loaded_keys, payload)
|
|
self.base_adapter = LoRAAdapter(loaded_keys, self.lora_weights)
|
|
|
|
def calculate_shape(self, key):
|
|
return self.base_adapter.calculate_shape(key)
|
|
|
|
def _calculate_weight_fallback(self, weight, key, strength, strength_model, offset, function, intermediate_dtype, original_weight):
|
|
if weight.dtype != torch.int8:
|
|
return self.base_adapter.calculate_weight(
|
|
weight,
|
|
key,
|
|
strength,
|
|
strength_model,
|
|
offset,
|
|
function,
|
|
intermediate_dtype,
|
|
original_weight,
|
|
)
|
|
|
|
device = weight.device
|
|
comp_device = _get_int8_compute_device(device)
|
|
effective_scale = _get_effective_weight_scale(self.weight_scale, weight.shape[0], offset)
|
|
base_weight_f = dequantize(weight.to(comp_device), effective_scale).to(intermediate_dtype)
|
|
original_base_f = base_weight_f.clone()
|
|
|
|
patched_weight_f = self.base_adapter.calculate_weight(
|
|
base_weight_f,
|
|
key,
|
|
strength,
|
|
strength_model,
|
|
offset,
|
|
function,
|
|
intermediate_dtype,
|
|
original_weight,
|
|
)
|
|
|
|
delta_f = patched_weight_f - original_base_f
|
|
del patched_weight_f
|
|
del original_base_f
|
|
del base_weight_f
|
|
delta_f = _transform_weight_like_for_outlier_method(
|
|
delta_f,
|
|
self.outlier_method,
|
|
comp_device,
|
|
hadanorm_sigma=self.hadanorm_sigma,
|
|
offset=offset,
|
|
)
|
|
final_weight = _apply_int8_delta_inplace(weight, delta_f, self.weight_scale, self.seed, offset)
|
|
del delta_f
|
|
return final_weight
|
|
|
|
def calculate_weight(self, weight, key, strength, strength_model, offset, function, intermediate_dtype=torch.float32, original_weight=None):
|
|
device = weight.device
|
|
comp_device = _get_int8_compute_device(device)
|
|
lora_diff = _compute_fast_lora_diff(self.lora_weights, weight.shape, comp_device, intermediate_dtype)
|
|
|
|
if lora_diff is None:
|
|
return self._calculate_weight_fallback(
|
|
weight,
|
|
key,
|
|
strength,
|
|
strength_model,
|
|
offset,
|
|
function,
|
|
intermediate_dtype,
|
|
original_weight,
|
|
)
|
|
|
|
scale = _compute_lora_scale(self.lora_weights, strength)
|
|
if weight.dtype == torch.int8:
|
|
delta_f = lora_diff * scale
|
|
del lora_diff
|
|
delta_f = _transform_weight_like_for_outlier_method(
|
|
delta_f,
|
|
self.outlier_method,
|
|
comp_device,
|
|
hadanorm_sigma=self.hadanorm_sigma,
|
|
offset=offset,
|
|
)
|
|
final_weight = _apply_int8_delta_inplace(weight, delta_f, self.weight_scale, self.seed, offset)
|
|
del delta_f
|
|
return final_weight
|
|
else:
|
|
final_weight = weight + (lora_diff * scale).to(weight.device, weight.dtype)
|
|
del lora_diff
|
|
return final_weight
|
|
|
|
class INT8MergedLoRAPatchAdapter(LoRAAdapter):
|
|
"""
|
|
Adapter that merges multiple LoRAs in float space BEFORE applying a single
|
|
stochastic rounding step. This is much more precise for LoRA stacks.
|
|
"""
|
|
def __init__(self, patches, weight_scale=_ADAPTER_STATE_UNSET, seed=0, use_quarot=False, outlier_method=None, hadanorm_sigma=None):
|
|
if _is_int8_adapter_payload(weight_scale, _INT8_MERGED_LORA_PATCH_PAYLOAD):
|
|
payload = weight_scale
|
|
patches = payload[1]
|
|
weight_scale = payload[2]
|
|
seed = payload[3]
|
|
outlier_method = payload[4]
|
|
hadanorm_sigma = payload[5]
|
|
elif weight_scale is _ADAPTER_STATE_UNSET:
|
|
weight_scale = 1.0
|
|
|
|
# We need to satisfy the base LoRAAdapter constructor.
|
|
# We use the first patch's keys/weights as a reference.
|
|
first_patch_adapter = patches[0][0]
|
|
|
|
# patches is a list of (adapter, strength)
|
|
self.patches = patches
|
|
self.weight_scale = weight_scale
|
|
self.seed = seed
|
|
resolved_method = _normalize_outlier_method(outlier_method)
|
|
if resolved_method == OUTLIER_METHOD_NONE and use_quarot:
|
|
resolved_method = OUTLIER_METHOD_QUAROT
|
|
self.outlier_method = resolved_method
|
|
self.hadanorm_sigma = hadanorm_sigma
|
|
payload = (
|
|
_INT8_MERGED_LORA_PATCH_PAYLOAD,
|
|
self.patches,
|
|
self.weight_scale,
|
|
self.seed,
|
|
self.outlier_method,
|
|
self.hadanorm_sigma,
|
|
)
|
|
super().__init__(first_patch_adapter.loaded_keys, payload)
|
|
|
|
def calculate_shape(self, key):
|
|
for adapter, _strength in self.patches:
|
|
calculate_shape = getattr(adapter, "calculate_shape", None)
|
|
if not callable(calculate_shape):
|
|
continue
|
|
shape = calculate_shape(key)
|
|
if shape is not None:
|
|
return shape
|
|
return None
|
|
|
|
def _calculate_weight_fallback(self, weight, key, strength_model, offset, function, intermediate_dtype, original_weight):
|
|
if weight.dtype == torch.int8:
|
|
device = weight.device
|
|
comp_device = _get_int8_compute_device(device)
|
|
effective_scale = _get_effective_weight_scale(self.weight_scale, weight.shape[0], offset)
|
|
base_weight_f = dequantize(weight.to(comp_device), effective_scale).to(intermediate_dtype)
|
|
original_base_f = base_weight_f.clone()
|
|
patched_weight_f = base_weight_f
|
|
else:
|
|
patched_weight_f = weight.to(dtype=intermediate_dtype).clone()
|
|
original_base_f = None
|
|
|
|
for adapter, lora_strength in self.patches:
|
|
patched_weight_f = adapter.calculate_weight(
|
|
patched_weight_f,
|
|
key,
|
|
lora_strength,
|
|
strength_model,
|
|
offset,
|
|
function,
|
|
intermediate_dtype,
|
|
original_weight,
|
|
)
|
|
|
|
if weight.dtype == torch.int8:
|
|
delta_f = patched_weight_f - original_base_f
|
|
del patched_weight_f
|
|
del original_base_f
|
|
delta_f = _transform_weight_like_for_outlier_method(
|
|
delta_f,
|
|
self.outlier_method,
|
|
comp_device,
|
|
hadanorm_sigma=self.hadanorm_sigma,
|
|
offset=offset,
|
|
)
|
|
final_weight = _apply_int8_delta_inplace(weight, delta_f, self.weight_scale, self.seed, offset)
|
|
del delta_f
|
|
return final_weight
|
|
|
|
final_weight = patched_weight_f.to(weight.device, weight.dtype)
|
|
del patched_weight_f
|
|
return final_weight
|
|
|
|
def calculate_weight(self, weight, key, strength, strength_model, offset, function, intermediate_dtype=torch.float32, original_weight=None):
|
|
# Note: 'strength' from ComfyUI is ignored here as we use internal lora_strengths
|
|
device = weight.device
|
|
comp_device = _get_int8_compute_device(device)
|
|
|
|
total_delta_f = None
|
|
|
|
for adapter, lora_strength in self.patches:
|
|
if not isinstance(adapter, LoRAAdapter):
|
|
return self._calculate_weight_fallback(
|
|
weight,
|
|
key,
|
|
strength_model,
|
|
offset,
|
|
function,
|
|
intermediate_dtype,
|
|
original_weight,
|
|
)
|
|
|
|
delta = _compute_fast_lora_diff(adapter.weights, weight.shape, comp_device, intermediate_dtype)
|
|
if delta is None:
|
|
return self._calculate_weight_fallback(
|
|
weight,
|
|
key,
|
|
strength_model,
|
|
offset,
|
|
function,
|
|
intermediate_dtype,
|
|
original_weight,
|
|
)
|
|
|
|
scale = _compute_lora_scale(adapter.weights, lora_strength)
|
|
|
|
if total_delta_f is None:
|
|
total_delta_f = delta * scale
|
|
else:
|
|
total_delta_f += delta * scale
|
|
del delta
|
|
|
|
if total_delta_f is None:
|
|
return weight
|
|
|
|
if weight.dtype == torch.int8:
|
|
# One single stochastic rounding step for all combined LoRAs
|
|
total_delta_f = _transform_weight_like_for_outlier_method(
|
|
total_delta_f,
|
|
self.outlier_method,
|
|
comp_device,
|
|
hadanorm_sigma=self.hadanorm_sigma,
|
|
offset=offset,
|
|
)
|
|
final_weight = _apply_int8_delta_inplace(weight, total_delta_f, self.weight_scale, self.seed, offset)
|
|
del total_delta_f
|
|
return final_weight
|
|
else:
|
|
final_weight = weight + total_delta_f.to(device, weight.dtype)
|
|
del total_delta_f
|
|
return final_weight
|
|
|
|
if _WEIGHT_ADAPTER_BASE_AVAILABLE:
|
|
class INT8WeightPatchAdapter(WeightAdapterBase):
|
|
name = "int8_weight_patch"
|
|
|
|
def __init__(self, base_adapter, weight_scale=_ADAPTER_STATE_UNSET, seed=0, use_quarot=False, outlier_method=None, hadanorm_sigma=None):
|
|
if _is_int8_adapter_payload(weight_scale, _INT8_WEIGHT_PATCH_PAYLOAD):
|
|
payload = weight_scale
|
|
base_adapter = payload[1]
|
|
weight_scale = payload[2]
|
|
seed = payload[3]
|
|
outlier_method = payload[4]
|
|
hadanorm_sigma = payload[5]
|
|
elif weight_scale is _ADAPTER_STATE_UNSET:
|
|
weight_scale = 1.0
|
|
|
|
self.base_adapter = base_adapter
|
|
self.weight_scale = weight_scale
|
|
self.seed = seed
|
|
resolved_method = _normalize_outlier_method(outlier_method)
|
|
if resolved_method == OUTLIER_METHOD_NONE and use_quarot:
|
|
resolved_method = OUTLIER_METHOD_QUAROT
|
|
self.outlier_method = resolved_method
|
|
self.hadanorm_sigma = hadanorm_sigma
|
|
self.loaded_keys = getattr(base_adapter, "loaded_keys", set())
|
|
self.weights = (
|
|
_INT8_WEIGHT_PATCH_PAYLOAD,
|
|
self.base_adapter,
|
|
self.weight_scale,
|
|
self.seed,
|
|
self.outlier_method,
|
|
self.hadanorm_sigma,
|
|
)
|
|
|
|
def calculate_shape(self, key):
|
|
calculate_shape = getattr(self.base_adapter, "calculate_shape", None)
|
|
return calculate_shape(key) if callable(calculate_shape) else None
|
|
|
|
def calculate_weight(self, weight, key, strength, strength_model, offset, function, intermediate_dtype=torch.float32, original_weight=None):
|
|
if weight.dtype != torch.int8:
|
|
return self.base_adapter.calculate_weight(
|
|
weight,
|
|
key,
|
|
strength,
|
|
strength_model,
|
|
offset,
|
|
function,
|
|
intermediate_dtype,
|
|
original_weight,
|
|
)
|
|
|
|
comp_device = _get_int8_compute_device(weight.device)
|
|
effective_scale = _get_effective_weight_scale(self.weight_scale, weight.shape[0], offset)
|
|
base_weight_f = dequantize(weight.to(comp_device), effective_scale).to(intermediate_dtype)
|
|
original_base_f = base_weight_f.clone()
|
|
|
|
patched_weight_f = self.base_adapter.calculate_weight(
|
|
base_weight_f,
|
|
key,
|
|
strength,
|
|
strength_model,
|
|
offset,
|
|
function,
|
|
intermediate_dtype,
|
|
original_weight,
|
|
)
|
|
if patched_weight_f is None:
|
|
return weight
|
|
|
|
delta_f = patched_weight_f - original_base_f
|
|
del patched_weight_f
|
|
del original_base_f
|
|
del base_weight_f
|
|
delta_f = _transform_weight_like_for_outlier_method(
|
|
delta_f,
|
|
self.outlier_method,
|
|
comp_device,
|
|
hadanorm_sigma=self.hadanorm_sigma,
|
|
offset=offset,
|
|
)
|
|
final_weight = _apply_int8_delta_inplace(weight, delta_f, self.weight_scale, self.seed, offset)
|
|
del delta_f
|
|
return final_weight
|
|
else:
|
|
INT8WeightPatchAdapter = None
|
|
|
|
|
|
# =============================================================================
|
|
# Dynamic LoRA Synchronization Hook
|
|
# =============================================================================
|
|
|
|
_NATIVE_DYNAMIC_LORA_HOOK_ATTRIBUTE = "_quantization_toolkit_dynamic_lora_hook"
|
|
_NATIVE_DYNAMIC_LORA_FORMATS = ("int8_tensorwise", "convrot_w4a4")
|
|
|
|
|
|
def _is_native_dynamic_lora_module(module):
|
|
return bool(
|
|
getattr(module, "quant_format", None) in _NATIVE_DYNAMIC_LORA_FORMATS
|
|
and not getattr(module, "_is_quantized", False)
|
|
)
|
|
|
|
|
|
def _native_dynamic_lora_forward_hook(module, input_args, input_kwargs, output):
|
|
dynamic_entries = getattr(module, "dynamic_lora_entries", None)
|
|
if not dynamic_entries:
|
|
return output
|
|
|
|
input_tensor = input_args[0] if input_args else input_kwargs.get("input", None)
|
|
if not isinstance(input_tensor, torch.Tensor) or not isinstance(output, torch.Tensor):
|
|
return output
|
|
|
|
return apply_dynamic_lora_delta(
|
|
x_input=input_tensor,
|
|
y=output,
|
|
lora_A=None,
|
|
lora_B=None,
|
|
lora_alpha=None,
|
|
lora_entries=dynamic_entries,
|
|
device=input_tensor.device,
|
|
)
|
|
|
|
|
|
def _ensure_native_dynamic_lora_runtime(module):
|
|
if not _is_native_dynamic_lora_module(module):
|
|
return False
|
|
|
|
if not hasattr(module, _NATIVE_DYNAMIC_LORA_HOOK_ATTRIBUTE):
|
|
hook_handle = module.register_forward_hook(
|
|
_native_dynamic_lora_forward_hook,
|
|
with_kwargs=True,
|
|
)
|
|
setattr(module, _NATIVE_DYNAMIC_LORA_HOOK_ATTRIBUTE, hook_handle)
|
|
|
|
if not hasattr(module, "dynamic_lora_entries"):
|
|
module.dynamic_lora_entries = None
|
|
if not hasattr(module, "lora_A"):
|
|
module.lora_A = None
|
|
if not hasattr(module, "lora_B"):
|
|
module.lora_B = None
|
|
if not hasattr(module, "lora_alpha"):
|
|
module.lora_alpha = None
|
|
|
|
return True
|
|
|
|
|
|
def _materialize_native_dynamic_lora_entries(entries, device):
|
|
grouped_entries = {}
|
|
for entry in entries:
|
|
offset = _normalize_dynamic_offset(entry.get("offset"))
|
|
grouped_entries.setdefault(offset, []).append(entry)
|
|
|
|
materialized_entries = []
|
|
for offset, offset_entries in grouped_entries.items():
|
|
prepared_entries = [
|
|
(entry, entry["A"], entry["B"])
|
|
for entry in offset_entries
|
|
]
|
|
output_length = offset[2] if offset is not None and offset[0] == 0 else None
|
|
source_devices = {
|
|
tensor.device
|
|
for _entry, lora_A, lora_B in prepared_entries
|
|
for tensor in (lora_A, lora_B)
|
|
}
|
|
if len(source_devices) == 1 and _can_batch_dynamic_entries(prepared_entries, output_length):
|
|
combined_A = torch.cat([lora_A for _entry, lora_A, _lora_B in prepared_entries], dim=0)
|
|
combined_B = torch.cat([lora_B for _entry, _lora_A, lora_B in prepared_entries], dim=1)
|
|
materialized_entries.append({
|
|
"A": combined_A.to(device),
|
|
"B": combined_B.to(device),
|
|
"offset": offset,
|
|
})
|
|
continue
|
|
|
|
materialized_entries.extend(
|
|
{
|
|
"A": lora_A.to(device),
|
|
"B": lora_B.to(device),
|
|
"offset": offset,
|
|
}
|
|
for _entry, lora_A, lora_B in prepared_entries
|
|
)
|
|
|
|
return materialized_entries
|
|
|
|
|
|
def _iter_dynamic_lora_modules(root_module):
|
|
"""Traverse the effective module tree, including hidden lazy-compile sources."""
|
|
visited_module_ids = set()
|
|
|
|
def walk(module_name, module):
|
|
# Quantized Lazy Torch Compile intentionally keeps its source module
|
|
# unregistered. Treat that source as occupying the proxy's original path
|
|
# so ComfyUI patch keys continue to match during the temporary swap.
|
|
source_module = getattr(module, "_source_module", None)
|
|
if isinstance(source_module, nn.Module) and source_module is not module:
|
|
yield from walk(module_name, source_module)
|
|
return
|
|
|
|
module_id = id(module)
|
|
if module_id in visited_module_ids:
|
|
return
|
|
visited_module_ids.add(module_id)
|
|
|
|
yield module_name, module
|
|
for child_name, child_module in module.named_children():
|
|
qualified_name = f"{module_name}.{child_name}" if module_name else child_name
|
|
yield from walk(qualified_name, child_module)
|
|
|
|
yield from walk("", root_module)
|
|
|
|
class DynamicLoRAHook:
|
|
"""
|
|
Hook registered on the diffusion_model to synchronize dynamic LoRA attributes
|
|
with the current ModelPatcher context at the start of each forward pass.
|
|
"""
|
|
def __init__(self):
|
|
self.current_lora_id = None
|
|
|
|
@staticmethod
|
|
def _compute_lora_id(dynamic_loras):
|
|
if not dynamic_loras:
|
|
return None
|
|
|
|
signature = []
|
|
# Prefer a loader-created UUID so cloned dicts keep a stable identity while
|
|
# newly loaded or changed LoRA stacks force recomposition.
|
|
for entry in dynamic_loras:
|
|
patch_uuid = entry.get("patch_uuid", None)
|
|
if patch_uuid is not None:
|
|
signature.append((str(patch_uuid), float(entry.get("strength", 0.0))))
|
|
continue
|
|
|
|
patches = entry.get("patches", {})
|
|
if isinstance(patches, dict):
|
|
patch_items = tuple(
|
|
sorted(
|
|
(
|
|
str(raw_key),
|
|
id(adapter),
|
|
)
|
|
for raw_key, adapter in patches.items()
|
|
)
|
|
)
|
|
else:
|
|
patch_items = ()
|
|
signature.append((
|
|
entry.get("name", ""),
|
|
float(entry.get("strength", 0.0)),
|
|
patch_items,
|
|
))
|
|
|
|
return hash(tuple(
|
|
signature
|
|
))
|
|
|
|
@classmethod
|
|
@_disable_torch_compile
|
|
def sync_from_transformer_options(cls, diffusion_model, transformer_options):
|
|
if transformer_options is None:
|
|
transformer_options = {}
|
|
|
|
dynamic_loras = transformer_options.get("dynamic_loras", [])
|
|
target_modules = []
|
|
|
|
if diffusion_model is not None:
|
|
target_modules.append(diffusion_model)
|
|
orig_mod = getattr(diffusion_model, "_orig_mod", None)
|
|
if orig_mod is not None:
|
|
target_modules.append(orig_mod)
|
|
|
|
for module in target_modules:
|
|
hook = cls.register(module)
|
|
lora_id = cls._compute_lora_id(dynamic_loras)
|
|
if lora_id == hook.current_lora_id:
|
|
continue
|
|
hook.apply_composition(module, dynamic_loras)
|
|
hook.current_lora_id = lora_id
|
|
|
|
@_disable_torch_compile
|
|
def pre_forward(self, module, input_args, input_kwargs):
|
|
# 1. Try to find transformer_options
|
|
transformer_options = input_kwargs.get("transformer_options", {})
|
|
if not transformer_options:
|
|
# Fallback for models that pass it in context
|
|
context = input_args[2] if len(input_args) > 2 else None
|
|
if isinstance(context, dict) and "transformer_options" in context:
|
|
transformer_options = context["transformer_options"]
|
|
|
|
dynamic_loras = transformer_options.get("dynamic_loras", [])
|
|
|
|
# 2. Generate a stable ID for this set of LoRAs
|
|
lora_id = self._compute_lora_id(dynamic_loras)
|
|
|
|
if lora_id == self.current_lora_id:
|
|
return None # Already synchronized
|
|
|
|
# 3. Synchronize all linear layers
|
|
self.apply_composition(module, dynamic_loras)
|
|
self.current_lora_id = lora_id
|
|
return None
|
|
|
|
@_disable_torch_compile
|
|
def apply_composition(self, diffusion_model, dynamic_loras):
|
|
def normalize_patch_key(raw_key):
|
|
key = raw_key[0] if isinstance(raw_key, tuple) else raw_key
|
|
if not isinstance(key, str):
|
|
return None
|
|
|
|
if key.endswith(".weight"):
|
|
key = key[:-7]
|
|
|
|
if key.startswith("diffusion_model."):
|
|
key = key[len("diffusion_model."):]
|
|
elif key.startswith("model.diffusion_model."):
|
|
key = key[len("model.diffusion_model."):]
|
|
elif key.startswith("model."):
|
|
key = key[len("model."):]
|
|
|
|
if key.startswith("_orig_mod.diffusion_model."):
|
|
key = key[len("_orig_mod.diffusion_model."):]
|
|
elif key.startswith("_orig_mod."):
|
|
key = key[len("_orig_mod."):]
|
|
|
|
return key
|
|
|
|
def normalize_module_name(module_name):
|
|
name = module_name
|
|
if name.startswith("_orig_mod.diffusion_model."):
|
|
name = name[len("_orig_mod.diffusion_model."):]
|
|
elif name.startswith("_orig_mod."):
|
|
name = name[len("_orig_mod."):]
|
|
elif name.startswith("diffusion_model."):
|
|
name = name[len("diffusion_model."):]
|
|
elif name.startswith("model.diffusion_model."):
|
|
name = name[len("model.diffusion_model."):]
|
|
elif name.startswith("model."):
|
|
name = name[len("model."):]
|
|
|
|
while name.startswith("_orig_mod."):
|
|
name = name[len("_orig_mod."):]
|
|
name = name.replace("._orig_mod.", ".")
|
|
return name
|
|
|
|
# Pre-group patches by layer
|
|
layer_patches = {}
|
|
if dynamic_loras:
|
|
for entry in dynamic_loras:
|
|
strength = entry["strength"]
|
|
for key, adapter in entry["patches"].items():
|
|
normalized_key = normalize_patch_key(key)
|
|
if normalized_key is None:
|
|
continue
|
|
offset = key[1] if isinstance(key, tuple) and len(key) > 1 else None
|
|
if normalized_key not in layer_patches:
|
|
layer_patches[normalized_key] = []
|
|
layer_patches[normalized_key].append((adapter, strength, offset))
|
|
|
|
# Update all modules
|
|
candidate_modules = 0
|
|
matched_modules = 0
|
|
native_runtime_modules = 0
|
|
for name, module in _iter_dynamic_lora_modules(diffusion_model):
|
|
uses_native_dynamic_runtime = _ensure_native_dynamic_lora_runtime(module)
|
|
if uses_native_dynamic_runtime:
|
|
native_runtime_modules += 1
|
|
|
|
if not hasattr(module, "lora_A"):
|
|
continue
|
|
candidate_modules += 1
|
|
|
|
normalized_name = normalize_module_name(name)
|
|
patches = layer_patches.get(normalized_name)
|
|
|
|
if not patches:
|
|
module.dynamic_lora_entries = None
|
|
module.lora_A = None
|
|
module.lora_B = None
|
|
module.lora_alpha = None
|
|
continue
|
|
|
|
# Compose
|
|
matched_modules += 1
|
|
entries = []
|
|
module_outlier_method = _get_module_outlier_method(module)
|
|
hadanorm_sigma = getattr(module, "hadanorm_sigma", None)
|
|
for adapter, strength, offset in patches:
|
|
if not _LORA_ADAPTER_AVAILABLE or not isinstance(adapter, LoRAAdapter):
|
|
if _DYNAMIC_LORA_DEBUG:
|
|
logging.warning(f"Quantization Toolkit Dynamic LoRA: skipping non-LoRA adapter for module={normalized_name}.")
|
|
continue
|
|
|
|
factors = _compute_dynamic_lora_factors(adapter.weights, strength)
|
|
if factors is None:
|
|
if _DYNAMIC_LORA_DEBUG:
|
|
logging.warning(
|
|
f"Quantization Toolkit Dynamic LoRA: skipping unsupported LoRA format for module={normalized_name} "
|
|
f"offset={offset}"
|
|
)
|
|
continue
|
|
|
|
curr_A, curr_B = factors
|
|
curr_A = _transform_weight_like_for_outlier_method(
|
|
curr_A,
|
|
module_outlier_method,
|
|
curr_A.device,
|
|
hadanorm_sigma=hadanorm_sigma,
|
|
offset=offset,
|
|
)
|
|
|
|
entries.append({
|
|
"A": curr_A,
|
|
"B": curr_B,
|
|
"offset": offset,
|
|
})
|
|
|
|
if entries:
|
|
module_weight = getattr(module, "weight", None)
|
|
device = module_weight.device if isinstance(module_weight, torch.Tensor) else torch.device("cpu")
|
|
if uses_native_dynamic_runtime:
|
|
module.dynamic_lora_entries = _materialize_native_dynamic_lora_entries(entries, device)
|
|
else:
|
|
module.dynamic_lora_entries = [
|
|
{
|
|
"A": entry["A"].to(device),
|
|
"B": entry["B"].to(device),
|
|
"offset": entry["offset"],
|
|
}
|
|
for entry in entries
|
|
]
|
|
else:
|
|
module.dynamic_lora_entries = None
|
|
|
|
# Keep legacy fields unset to force offset-aware entry path.
|
|
module.lora_A = None
|
|
module.lora_B = None
|
|
module.lora_alpha = None
|
|
|
|
if dynamic_loras and matched_modules == 0:
|
|
logging.warning(
|
|
"Quantization Toolkit Dynamic LoRA: no compatible quantized layers matched the loaded LoRA patches."
|
|
)
|
|
elif dynamic_loras:
|
|
logging.info(
|
|
f"Quantization Toolkit Dynamic LoRA: activated {len(dynamic_loras)} LoRA(s) across "
|
|
f"{matched_modules} quantized layer(s)."
|
|
)
|
|
|
|
if _DYNAMIC_LORA_DEBUG:
|
|
logging.info(
|
|
f"Quantization Toolkit Dynamic LoRA: candidate_modules={candidate_modules} "
|
|
f"patch_keys={len(layer_patches)} matched_modules={matched_modules} "
|
|
f"native_runtime_modules={native_runtime_modules}"
|
|
)
|
|
|
|
@classmethod
|
|
def register(cls, diffusion_model):
|
|
if not hasattr(diffusion_model, "_dynamic_lora_hook"):
|
|
hook = cls()
|
|
diffusion_model._dynamic_lora_hook = hook
|
|
diffusion_model.register_forward_pre_hook(hook.pre_forward, with_kwargs=True)
|
|
return diffusion_model._dynamic_lora_hook
|
|
|
|
|
|
# =============================================================================
|
|
# Int8TensorwiseOps - ComfyUI Custom Operations
|
|
# =============================================================================
|
|
|
|
try:
|
|
from comfy.ops import manual_cast, cast_bias_weight, uncast_bias_weight
|
|
_COMFY_OPS_AVAILABLE = True
|
|
except ImportError:
|
|
_COMFY_OPS_AVAILABLE = False
|
|
|
|
|
|
if _COMFY_OPS_AVAILABLE:
|
|
class Int8TensorwiseOps(manual_cast):
|
|
"""
|
|
Custom ComfyUI operations for INT8 tensorwise quantization.
|
|
"""
|
|
keep_float_names = []
|
|
int4_sensitive_names = []
|
|
int4_mixed_selected_names = set()
|
|
int4_mixed_ratio = DEFAULT_INT4_MIXED_RATIO
|
|
dynamic_quantize = False # Manual toggle for on-the-fly quantization
|
|
outlier_method = OUTLIER_METHOD_NONE
|
|
quantization_mode = QUANTIZATION_MODE_INT8
|
|
use_triton = True
|
|
runtime_backend = DEFAULT_INT8_BACKEND
|
|
runtime_uses_triton = DEFAULT_INT8_BACKEND in (INT8_BACKEND_TRITON, INT8_BACKEND_TRITON_LEGACY_UNSAFE)
|
|
runtime_uses_legacy_triton = False
|
|
prepack_int8_weights = False
|
|
small_batch_fallback_mode = SMALL_BATCH_FALLBACK_SMALL_LAYERS
|
|
_is_prequantized = None # Global flag for current load
|
|
_otf_progress_total = None
|
|
_otf_progress_processed = 0
|
|
_otf_progress_quantized = 0
|
|
_otf_progress_outlier = 0
|
|
_otf_progress_last_bucket = -1
|
|
_runtime_stats = {}
|
|
|
|
@classmethod
|
|
def reset_otf_progress(cls):
|
|
cls._otf_progress_total = None
|
|
cls._otf_progress_processed = 0
|
|
cls._otf_progress_quantized = 0
|
|
cls._otf_progress_outlier = 0
|
|
cls._otf_progress_last_bucket = -1
|
|
cls.int4_mixed_selected_names = set()
|
|
|
|
@classmethod
|
|
def _init_otf_progress(cls, state_dict, current_prefix=None, current_weight=None):
|
|
if cls._otf_progress_total is not None:
|
|
return
|
|
|
|
weight_entries = []
|
|
if isinstance(current_weight, torch.Tensor):
|
|
weight_entries.append((normalize_layer_name(current_prefix), current_weight))
|
|
for key, value in state_dict.items():
|
|
if not isinstance(value, torch.Tensor):
|
|
continue
|
|
if not key.endswith("weight"):
|
|
continue
|
|
if value.dtype in (torch.float16, torch.bfloat16, torch.float32) or _is_float8_dtype(value.dtype):
|
|
weight_entries.append((normalize_layer_name(key[:-len("weight")]), value))
|
|
|
|
total_estimate = len(weight_entries)
|
|
if normalize_quantization_mode(cls.quantization_mode) == QUANTIZATION_MODE_INT4_MIXED:
|
|
compatible_names = [
|
|
layer_name
|
|
for layer_name, weight in weight_entries
|
|
if not any(pattern in layer_name for pattern in cls.keep_float_names)
|
|
and is_int4_compatible_linear(weight)
|
|
]
|
|
selected_names = select_int4_mixed_layers(
|
|
compatible_names,
|
|
cls.int4_mixed_ratio,
|
|
cls.int4_sensitive_names,
|
|
)
|
|
cls.int4_mixed_selected_names = set(selected_names)
|
|
logging.info(
|
|
"Quantization Toolkit on-the-fly: INT4 mixed profile "
|
|
f"(compatible={len(compatible_names)}, "
|
|
f"kept_int8={len(selected_names)}, "
|
|
f"ratio={normalize_int4_mixed_ratio(cls.int4_mixed_ratio):.2f})"
|
|
)
|
|
|
|
cls._otf_progress_total = max(1, int(total_estimate))
|
|
logging.info(f"Quantization Toolkit on-the-fly: starting scan (estimated layers: {cls._otf_progress_total}).")
|
|
|
|
@classmethod
|
|
def _update_otf_progress(cls, *, quantized: bool, outlier_adjusted: bool):
|
|
total = cls._otf_progress_total if cls._otf_progress_total is not None else 1
|
|
cls._otf_progress_processed += 1
|
|
if quantized:
|
|
cls._otf_progress_quantized += 1
|
|
if outlier_adjusted:
|
|
cls._otf_progress_outlier += 1
|
|
|
|
percent = min(100, int((cls._otf_progress_processed * 100) / max(1, total)))
|
|
bucket = percent // 5
|
|
if bucket != cls._otf_progress_last_bucket:
|
|
cls._otf_progress_last_bucket = bucket
|
|
logging.info(
|
|
f"Quantization Toolkit on-the-fly: {percent:3d}% "
|
|
f"({cls._otf_progress_processed}/{total}) "
|
|
f"quantized={cls._otf_progress_quantized} "
|
|
f"outlier_adjusted={cls._otf_progress_outlier}"
|
|
)
|
|
|
|
@classmethod
|
|
def summarize_otf_progress(cls):
|
|
if cls._otf_progress_processed <= 0:
|
|
return
|
|
logging.info(
|
|
"Quantization Toolkit on-the-fly: complete "
|
|
f"(processed={cls._otf_progress_processed}, "
|
|
f"quantized={cls._otf_progress_quantized}, "
|
|
f"outlier_adjusted={cls._otf_progress_outlier})"
|
|
)
|
|
|
|
@classmethod
|
|
def reset_runtime_stats(cls):
|
|
cls._runtime_stats = {
|
|
"linear_calls": 0,
|
|
"comfy_kitchen_convrot": 0,
|
|
"triton": 0,
|
|
"triton_legacy_unsafe": 0,
|
|
"torch_int_mm": 0,
|
|
"small_batch_fallback": 0,
|
|
"prepacked": 0,
|
|
}
|
|
|
|
@classmethod
|
|
def _increment_runtime_stat(cls, key):
|
|
if not _RUNTIME_STATS_ENABLED:
|
|
return
|
|
try:
|
|
if torch.compiler.is_compiling():
|
|
return
|
|
except Exception:
|
|
pass
|
|
if not cls._runtime_stats:
|
|
cls.reset_runtime_stats()
|
|
cls._runtime_stats[key] = cls._runtime_stats.get(key, 0) + 1
|
|
|
|
@classmethod
|
|
def log_runtime_stats(cls, prefix="Quantization Toolkit INT8 runtime:"):
|
|
if not _RUNTIME_STATS_ENABLED:
|
|
return
|
|
logging.info(
|
|
f"{prefix} backend={cls.runtime_backend} "
|
|
f"small_batch_fallback={cls.small_batch_fallback_mode} "
|
|
f"prepack_int8_weights={cls.prepack_int8_weights} "
|
|
f"calls={cls._runtime_stats}"
|
|
)
|
|
|
|
class Linear(manual_cast.Linear):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.register_buffer("weight_scale", None)
|
|
self.register_buffer("weight_packed", None, persistent=False)
|
|
self.register_buffer("weight_int_mm", None, persistent=False)
|
|
self.register_buffer("quarot_hadamard", None)
|
|
self.register_buffer("hadanorm_sigma", None)
|
|
self._is_quantized = False
|
|
self._quant_format = None
|
|
self._convrot_groupsize = _CONVROT_GROUP_SIZE
|
|
self._quant_group_size = _INT4_QUANT_GROUP_SIZE
|
|
self._linear_dtype = "int4"
|
|
self._is_per_row = False
|
|
self._use_quarot = False
|
|
self._outlier_method = OUTLIER_METHOD_NONE
|
|
self.compute_dtype = torch.bfloat16
|
|
self.dynamic_lora_entries = None
|
|
self.lora_A = None
|
|
self.lora_B = None
|
|
self.lora_alpha = None
|
|
configure_int8_module_runtime(
|
|
self,
|
|
small_batch_fallback=Int8TensorwiseOps.small_batch_fallback_mode,
|
|
runtime_backend=Int8TensorwiseOps.runtime_backend,
|
|
prepack_int8_weights=Int8TensorwiseOps.prepack_int8_weights,
|
|
)
|
|
|
|
def reset_parameters(self):
|
|
return None
|
|
|
|
def _load_from_state_dict(self, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs):
|
|
weight_key = prefix + "weight"
|
|
scale_key = prefix + "weight_scale"
|
|
input_scale_key = prefix + "input_scale"
|
|
hadamard_key = prefix + "quarot_hadamard"
|
|
hadanorm_sigma_key = prefix + "hadanorm_sigma"
|
|
bias_key = prefix + "bias"
|
|
|
|
weight_scale = state_dict.pop(scale_key, None)
|
|
stored_hadamard = state_dict.pop(hadamard_key, None)
|
|
stored_hadanorm_sigma = state_dict.pop(hadanorm_sigma_key, None)
|
|
native_quant_config = _decode_comfy_quant_config(state_dict.pop(prefix + "comfy_quant", None))
|
|
weight_tensor = state_dict.pop(weight_key, None)
|
|
|
|
# Pop input_scale to clean state_dict, but ignore it
|
|
_ = state_dict.pop(input_scale_key, None)
|
|
|
|
if weight_tensor is not None:
|
|
native_quant_format = (
|
|
native_quant_config.get("format")
|
|
if isinstance(native_quant_config, dict)
|
|
else None
|
|
)
|
|
if native_quant_format == "convrot_w4a4" and weight_scale is not None:
|
|
self._is_quantized = True
|
|
self._quant_format = "convrot_w4a4"
|
|
self._use_quarot = False
|
|
self._outlier_method = OUTLIER_METHOD_NONE
|
|
self.quarot_hadamard = None
|
|
self.hadanorm_sigma = None
|
|
self.weight_packed = None
|
|
self.weight_int_mm = None
|
|
self._convrot_groupsize = _validate_convrot_group_size(
|
|
native_quant_config.get("convrot_groupsize", _CONVROT_GROUP_SIZE),
|
|
self.in_features,
|
|
f"Quantized layer {prefix.rstrip('.') or '<root>'}",
|
|
)
|
|
self._quant_group_size = _INT4_QUANT_GROUP_SIZE
|
|
self._linear_dtype = str(native_quant_config.get("linear_dtype", "int4"))
|
|
orig_dtype = getattr(self, "weight_comfy_model_dtype", None) or torch.bfloat16
|
|
scale = weight_scale.float() if isinstance(weight_scale, torch.Tensor) else torch.tensor(weight_scale, dtype=torch.float32)
|
|
self.weight_scale = scale.reshape(-1)
|
|
q_weight = _make_native_int4_tensor(
|
|
weight_tensor,
|
|
self.weight_scale,
|
|
orig_dtype,
|
|
(self.out_features, self.in_features),
|
|
convrot_groupsize=self._convrot_groupsize,
|
|
linear_dtype=self._linear_dtype,
|
|
)
|
|
self.weight = nn.Parameter(q_weight, requires_grad=False)
|
|
self._is_per_row = True
|
|
Int8TensorwiseOps._is_prequantized = True
|
|
elif weight_tensor.dtype == torch.int8 and weight_scale is not None:
|
|
# Load Quantized
|
|
self._is_quantized = True
|
|
self._quant_format = "int8_tensorwise"
|
|
self._use_quarot = False
|
|
self.quarot_hadamard = None
|
|
self.hadanorm_sigma = None
|
|
self._outlier_method = OUTLIER_METHOD_NONE
|
|
self.weight = nn.Parameter(weight_tensor, requires_grad=False)
|
|
self.weight_packed = _prepack_int8_weight(weight_tensor) if self._prepack_int8_weights else None
|
|
self.weight_int_mm = _prepack_torch_int_mm_weight(weight_tensor)
|
|
Int8TensorwiseOps._is_prequantized = True # Found a quantized layer
|
|
|
|
if isinstance(weight_scale, torch.Tensor):
|
|
if weight_scale.numel() == 1:
|
|
self.weight_scale = weight_scale.float().reshape(1)
|
|
self._is_per_row = False
|
|
elif weight_scale.dim() == 2 and weight_scale.shape[1] == 1:
|
|
self.weight_scale = weight_scale.float()
|
|
self._is_per_row = True
|
|
else:
|
|
self.weight_scale = weight_scale.float()
|
|
self._is_per_row = False
|
|
else:
|
|
self.weight_scale = torch.tensor([float(weight_scale)], dtype=torch.float32)
|
|
self._is_per_row = False
|
|
|
|
if isinstance(stored_hadanorm_sigma, torch.Tensor):
|
|
self.hadanorm_sigma = stored_hadanorm_sigma.float()
|
|
self._outlier_method = OUTLIER_METHOD_HADANORM
|
|
if isinstance(stored_hadamard, torch.Tensor):
|
|
self.quarot_hadamard = stored_hadamard.float()
|
|
if self._outlier_method == OUTLIER_METHOD_NONE:
|
|
self._use_quarot = True
|
|
self._outlier_method = OUTLIER_METHOD_QUAROT
|
|
if (
|
|
self._outlier_method == OUTLIER_METHOD_NONE
|
|
and isinstance(native_quant_config, dict)
|
|
and native_quant_config.get("format") == "int8_tensorwise"
|
|
and native_quant_config.get("convrot", False)
|
|
):
|
|
convrot_group_size = _validate_convrot_group_size(
|
|
native_quant_config.get("convrot_groupsize", _CONVROT_GROUP_SIZE),
|
|
self.in_features,
|
|
f"Quantized layer {prefix.rstrip('.') or '<root>'}",
|
|
)
|
|
try:
|
|
h_matrix = _build_outlier_hadamard(
|
|
OUTLIER_METHOD_CONVROT,
|
|
convrot_group_size,
|
|
device="cpu",
|
|
dtype=torch.float32,
|
|
)
|
|
if h_matrix is None:
|
|
raise RuntimeError("ConvRot Hadamard helper is unavailable")
|
|
self._convrot_groupsize = convrot_group_size
|
|
self.quarot_hadamard = h_matrix
|
|
self._outlier_method = OUTLIER_METHOD_CONVROT
|
|
except Exception as e:
|
|
raise ValueError(
|
|
f"Quantized layer {prefix.rstrip('.') or '<root>'}: invalid ConvRot metadata ({e})"
|
|
) from e
|
|
|
|
elif weight_tensor.dtype in (torch.float16, torch.bfloat16, torch.float32) or _is_float8_dtype(weight_tensor.dtype):
|
|
# Load High-Precision
|
|
# Detect if the model is pre-quantized if we don't know yet
|
|
if Int8TensorwiseOps._is_prequantized is None:
|
|
# Robust detection: scan keys and a sample of values
|
|
is_prequant = False
|
|
for k in state_dict.keys():
|
|
if "weight_scale" in k or "comfy_quant" in k:
|
|
is_prequant = True
|
|
break
|
|
|
|
if not is_prequant:
|
|
# Fallback: scan a sample of values for int8 tensors
|
|
for i, v in enumerate(state_dict.values()):
|
|
if i > 200: break # Safety limit
|
|
if getattr(v, "dtype", None) == torch.int8:
|
|
is_prequant = True
|
|
break
|
|
Int8TensorwiseOps._is_prequantized = is_prequant
|
|
|
|
track_progress = Int8TensorwiseOps.dynamic_quantize and not Int8TensorwiseOps._is_prequantized
|
|
if track_progress:
|
|
Int8TensorwiseOps._init_otf_progress(
|
|
state_dict,
|
|
current_prefix=prefix,
|
|
current_weight=weight_tensor,
|
|
)
|
|
|
|
quantization_mode = normalize_quantization_mode(Int8TensorwiseOps.quantization_mode)
|
|
int4_mode = quantization_mode_is_int4(quantization_mode)
|
|
keep_float = any(name in prefix for name in Int8TensorwiseOps.keep_float_names)
|
|
int4_sensitive = (
|
|
normalize_layer_name(prefix) in Int8TensorwiseOps.int4_mixed_selected_names
|
|
)
|
|
is_dim1 = self.in_features == 1 or self.out_features == 1 or weight_tensor.ndim == 1
|
|
quantized_now = False
|
|
outlier_adjusted_now = False
|
|
|
|
if keep_float or is_dim1 or Int8TensorwiseOps._is_prequantized or not Int8TensorwiseOps.dynamic_quantize:
|
|
self._is_quantized = False
|
|
self._quant_format = None
|
|
self._is_per_row = False
|
|
self._use_quarot = False
|
|
self.quarot_hadamard = None
|
|
self.hadanorm_sigma = None
|
|
self._outlier_method = OUTLIER_METHOD_NONE
|
|
self.weight = nn.Parameter(weight_tensor, requires_grad=False)
|
|
self.weight_packed = None
|
|
self.weight_int_mm = None
|
|
else:
|
|
# Quantize on the fly (per-row, including FP8 -> INT8).
|
|
device = _get_int8_compute_device(weight_tensor.device)
|
|
w_gpu = tensor_to_int8_compute_device(weight_tensor, device, non_blocking=True)
|
|
outlier_method = quantization_mode_outlier_method(quantization_mode)
|
|
if _is_float8_dtype(w_gpu.dtype):
|
|
w_gpu = w_gpu.to(torch.float16 if device.type == "cuda" else torch.float32)
|
|
elif outlier_method != OUTLIER_METHOD_NONE and w_gpu.dtype in (torch.float16, torch.bfloat16):
|
|
w_gpu = w_gpu.float()
|
|
self._use_quarot = False
|
|
self.quarot_hadamard = None
|
|
self.hadanorm_sigma = None
|
|
self._outlier_method = OUTLIER_METHOD_NONE
|
|
|
|
int4_compatible = (
|
|
int4_mode
|
|
and _NATIVE_INT4_AVAILABLE
|
|
and w_gpu.ndim == 2
|
|
and w_gpu.shape[1] % _CONVROT_GROUP_SIZE == 0
|
|
)
|
|
if int4_compatible and not (
|
|
quantization_mode == QUANTIZATION_MODE_INT4_MIXED and int4_sensitive
|
|
):
|
|
linear_dtype = "int4"
|
|
q_weight = quantize_native_int4(
|
|
w_gpu,
|
|
linear_dtype=linear_dtype,
|
|
).to("cpu")
|
|
self.weight = nn.Parameter(q_weight, requires_grad=False)
|
|
self.weight_scale = q_weight._params.scale
|
|
self.weight_packed = None
|
|
self.weight_int_mm = None
|
|
self._is_quantized = True
|
|
self._quant_format = "convrot_w4a4"
|
|
self._is_per_row = True
|
|
self._convrot_groupsize = q_weight._params.convrot_groupsize
|
|
self._quant_group_size = q_weight._params.quant_group_size
|
|
self._linear_dtype = q_weight._params.linear_dtype
|
|
quantized_now = True
|
|
outlier_adjusted_now = True
|
|
if track_progress:
|
|
Int8TensorwiseOps._update_otf_progress(
|
|
quantized=quantized_now,
|
|
outlier_adjusted=outlier_adjusted_now,
|
|
)
|
|
bias_tensor = state_dict.pop(bias_key, None)
|
|
if bias_tensor is not None:
|
|
self.bias = nn.Parameter(bias_tensor, requires_grad=False)
|
|
else:
|
|
self.bias = None
|
|
return
|
|
|
|
if int4_mode:
|
|
# Mixed-mode sensitive layers and W4-incompatible shapes use W8.
|
|
outlier_method = (
|
|
OUTLIER_METHOD_CONVROT
|
|
if w_gpu.shape[1] % _CONVROT_GROUP_SIZE == 0
|
|
else OUTLIER_METHOD_NONE
|
|
)
|
|
|
|
if (
|
|
outlier_method != OUTLIER_METHOD_NONE
|
|
and w_gpu.ndim == 2
|
|
and w_gpu.shape[1] % _get_outlier_group_size(outlier_method) == 0
|
|
):
|
|
try:
|
|
group_size = _get_outlier_group_size(outlier_method)
|
|
h_matrix = _build_outlier_hadamard(outlier_method, group_size, device=device, dtype=w_gpu.dtype)
|
|
if h_matrix is None:
|
|
raise RuntimeError(f"{outlier_method} Hadamard helper is unavailable")
|
|
if outlier_method == OUTLIER_METHOD_HADANORM:
|
|
hadanorm_sigma = _compute_hadanorm_sigma(w_gpu).to(device=device, dtype=w_gpu.dtype)
|
|
w_gpu = w_gpu * hadanorm_sigma.view(1, -1)
|
|
self.hadanorm_sigma = hadanorm_sigma.detach().cpu()
|
|
w_gpu = _rotate_weight_for_outlier_method(w_gpu, h_matrix, group_size=group_size, method=outlier_method)
|
|
self.quarot_hadamard = h_matrix.detach().cpu()
|
|
self._use_quarot = outlier_method == OUTLIER_METHOD_QUAROT
|
|
self._outlier_method = outlier_method
|
|
except Exception:
|
|
self._use_quarot = False
|
|
self.quarot_hadamard = None
|
|
self.hadanorm_sigma = None
|
|
self._outlier_method = OUTLIER_METHOD_NONE
|
|
|
|
q_weight, q_scale = quantize_int8_rowwise(w_gpu)
|
|
self.weight = nn.Parameter(q_weight.cpu(), requires_grad=False)
|
|
self.weight_packed = _prepack_int8_weight(self.weight) if self._prepack_int8_weights else None
|
|
self.weight_int_mm = _prepack_torch_int_mm_weight(self.weight)
|
|
self.weight_scale = (
|
|
q_scale.cpu()
|
|
if isinstance(q_scale, torch.Tensor)
|
|
else torch.tensor([float(q_scale)], dtype=torch.float32)
|
|
)
|
|
self._is_quantized = True
|
|
self._quant_format = "int8_tensorwise"
|
|
self._is_per_row = self.weight_scale.dim() == 2 and self.weight_scale.shape[1] == 1
|
|
quantized_now = True
|
|
outlier_adjusted_now = self._outlier_method != OUTLIER_METHOD_NONE
|
|
|
|
if track_progress:
|
|
Int8TensorwiseOps._update_otf_progress(
|
|
quantized=quantized_now,
|
|
outlier_adjusted=outlier_adjusted_now,
|
|
)
|
|
else:
|
|
self._is_quantized = False
|
|
self._is_per_row = False
|
|
self._use_quarot = False
|
|
self.quarot_hadamard = None
|
|
self.hadanorm_sigma = None
|
|
self._outlier_method = OUTLIER_METHOD_NONE
|
|
self.weight = nn.Parameter(weight_tensor, requires_grad=False)
|
|
self.weight_packed = None
|
|
self.weight_int_mm = None
|
|
else:
|
|
missing_keys.append(weight_key)
|
|
|
|
bias_tensor = state_dict.pop(bias_key, None)
|
|
if bias_tensor is not None:
|
|
self.bias = nn.Parameter(bias_tensor, requires_grad=False)
|
|
else:
|
|
self.bias = None
|
|
|
|
def state_dict(self, *args, destination=None, prefix="", keep_vars=False):
|
|
output = super().state_dict(*args, destination=destination, prefix=prefix, keep_vars=keep_vars)
|
|
if not getattr(self, "_is_quantized", False):
|
|
return output
|
|
|
|
if self._quant_format == "convrot_w4a4" and _is_native_int4_tensor(self.weight):
|
|
params = self.weight._params
|
|
convrot_group_size = _validate_convrot_group_size(
|
|
params.convrot_groupsize,
|
|
self.in_features,
|
|
f"Quantized layer {prefix.rstrip('.') or '<root>'}",
|
|
)
|
|
output[prefix + "weight"] = self.weight._qdata if keep_vars else self.weight._qdata.detach()
|
|
output[prefix + "weight_scale"] = params.scale if keep_vars else params.scale.detach()
|
|
quant_config = {
|
|
"format": "convrot_w4a4",
|
|
"convrot_groupsize": convrot_group_size,
|
|
}
|
|
if params.linear_dtype != "int4":
|
|
quant_config["linear_dtype"] = params.linear_dtype
|
|
output[prefix + "comfy_quant"] = _encode_comfy_quant_config(quant_config)
|
|
return output
|
|
|
|
outlier_method = _get_module_outlier_method(self)
|
|
if outlier_method not in (OUTLIER_METHOD_NONE, OUTLIER_METHOD_CONVROT):
|
|
return output
|
|
|
|
quant_config = {"format": "int8_tensorwise"}
|
|
if outlier_method == OUTLIER_METHOD_CONVROT:
|
|
hadamard = getattr(self, "quarot_hadamard", None)
|
|
convrot_group_size = _validate_convrot_group_size(
|
|
self._convrot_groupsize,
|
|
self.in_features,
|
|
f"Quantized layer {prefix.rstrip('.') or '<root>'}",
|
|
)
|
|
if not isinstance(hadamard, torch.Tensor) or tuple(hadamard.shape) != (convrot_group_size, convrot_group_size):
|
|
raise ValueError(
|
|
f"Quantized layer {prefix.rstrip('.') or '<root>'}: ConvRot Hadamard shape must be "
|
|
f"({convrot_group_size}, {convrot_group_size})"
|
|
)
|
|
quant_config["convrot"] = True
|
|
quant_config["convrot_groupsize"] = convrot_group_size
|
|
output.pop(prefix + "quarot_hadamard", None)
|
|
|
|
output[prefix + "comfy_quant"] = _encode_comfy_quant_config(quant_config)
|
|
return output
|
|
|
|
def _replace_weight(self, new_weight, inplace_update=False):
|
|
if inplace_update and not _is_native_int4_tensor(new_weight):
|
|
self.weight.data.copy_(new_weight)
|
|
else:
|
|
self.weight = nn.Parameter(new_weight, requires_grad=False)
|
|
if self._quant_format == "convrot_w4a4":
|
|
self.weight_scale = self.weight._params.scale
|
|
self.weight_packed = None
|
|
self.weight_int_mm = None
|
|
return
|
|
self.weight_packed = (
|
|
_prepack_int8_weight(self.weight)
|
|
if self._is_quantized and self._prepack_int8_weights
|
|
else None
|
|
)
|
|
self.weight_int_mm = _prepack_torch_int_mm_weight(self.weight) if self._is_quantized else None
|
|
|
|
def convert_weight(self, _weight, inplace=False):
|
|
if not self._is_quantized:
|
|
return _weight
|
|
if self._quant_format == "convrot_w4a4":
|
|
source_weight = _weight if _is_native_int4_tensor(_weight) else self.weight
|
|
return source_weight.dequantize()
|
|
target_device = _weight.device if isinstance(_weight, torch.Tensor) else self.weight.device
|
|
if self.weight.device == target_device:
|
|
return self.weight.clone()
|
|
return self.weight.to(target_device, non_blocking=True).clone()
|
|
|
|
def set_weight(self, out_weight, inplace_update=False, seed=0, return_weight=False, **kwargs):
|
|
if not self._is_quantized:
|
|
new_weight = out_weight.to(self.weight.dtype)
|
|
if return_weight:
|
|
return new_weight
|
|
|
|
if inplace_update:
|
|
self._replace_weight(new_weight, inplace_update=True)
|
|
else:
|
|
self._replace_weight(new_weight)
|
|
return
|
|
|
|
if self._quant_format == "convrot_w4a4":
|
|
if _is_native_int4_tensor(out_weight):
|
|
new_weight = out_weight
|
|
else:
|
|
new_weight = self.weight.requantize_from_float(
|
|
out_weight,
|
|
scale="recalculate",
|
|
stochastic_rounding=seed,
|
|
inplace_ops=True,
|
|
)
|
|
if return_weight:
|
|
return new_weight
|
|
self._replace_weight(new_weight)
|
|
return
|
|
|
|
if out_weight.dtype == torch.int8:
|
|
if return_weight:
|
|
return out_weight
|
|
|
|
if inplace_update:
|
|
self._replace_weight(out_weight, inplace_update=True)
|
|
else:
|
|
self._replace_weight(out_weight)
|
|
return
|
|
|
|
# Re-quantize if fallback occurred
|
|
new_weight = stochastic_round_int8_delta(out_weight, self.weight_scale, seed)
|
|
|
|
if return_weight:
|
|
return new_weight
|
|
|
|
if inplace_update:
|
|
self._replace_weight(new_weight, inplace_update=True)
|
|
else:
|
|
self._replace_weight(new_weight)
|
|
|
|
def set_bias(self, out_bias, inplace_update=False, seed=0, return_weight=False, **kwargs):
|
|
if out_bias is None: return None
|
|
|
|
new_bias = out_bias
|
|
if return_weight:
|
|
return new_bias
|
|
|
|
if inplace_update:
|
|
if self.bias is not None:
|
|
self.bias.data.copy_(new_bias)
|
|
else:
|
|
self.bias = nn.Parameter(new_bias, requires_grad=False)
|
|
|
|
@_disable_torch_compile
|
|
def forward(self, x: Tensor) -> Tensor:
|
|
"""Fast forward using torch._int_mm for quantized weights."""
|
|
|
|
if not self._is_quantized:
|
|
weight, bias, offload_stream = cast_bias_weight(self, x, offloadable=True)
|
|
out = F.linear(x, weight, bias)
|
|
uncast_bias_weight(self, weight, bias, offload_stream)
|
|
return out
|
|
|
|
if self._quant_format == "convrot_w4a4":
|
|
weight, bias, offload_stream = cast_bias_weight(
|
|
self,
|
|
input=None,
|
|
dtype=self.weight.dtype,
|
|
device=x.device,
|
|
bias_dtype=x.dtype,
|
|
offloadable=True,
|
|
compute_dtype=x.dtype,
|
|
want_requant=True,
|
|
)
|
|
out = native_int4_linear(x, weight, bias)
|
|
uncast_bias_weight(self, weight, bias, offload_stream)
|
|
return apply_dynamic_lora_delta(
|
|
x_input=x,
|
|
y=out,
|
|
lora_A=self.lora_A,
|
|
lora_B=self.lora_B,
|
|
lora_alpha=self.lora_alpha,
|
|
lora_entries=self.dynamic_lora_entries,
|
|
device=x.device,
|
|
)
|
|
|
|
# 1. Move weight/bias/scale to device (non_blocking)
|
|
weight = self.weight if self.weight.device == x.device else self.weight.to(x.device, non_blocking=True)
|
|
if self.bias is None:
|
|
bias = None
|
|
else:
|
|
bias = self.bias if self.bias.device == x.device else self.bias.to(x.device, non_blocking=True)
|
|
|
|
w_scale = self.weight_scale
|
|
if isinstance(w_scale, torch.Tensor):
|
|
if w_scale.device != x.device:
|
|
w_scale = w_scale.to(x.device, non_blocking=True)
|
|
|
|
compute_dtype = x.dtype if x.dtype in (torch.float16, torch.bfloat16) else torch.bfloat16
|
|
|
|
x_shape = x.shape
|
|
outlier_method = _get_module_outlier_method(self)
|
|
if outlier_method == OUTLIER_METHOD_CONVROT:
|
|
if _RUNTIME_STATS_ENABLED:
|
|
Int8TensorwiseOps._increment_runtime_stat("linear_calls")
|
|
Int8TensorwiseOps._increment_runtime_stat("comfy_kitchen_convrot")
|
|
y_view = _native_int8_convrot_linear(
|
|
x,
|
|
weight,
|
|
w_scale,
|
|
bias,
|
|
compute_dtype,
|
|
self._convrot_groupsize,
|
|
)
|
|
if self.dynamic_lora_entries or (
|
|
isinstance(self.lora_A, torch.Tensor)
|
|
and isinstance(self.lora_B, torch.Tensor)
|
|
):
|
|
x_transformed, correction_source = _apply_outlier_activation_transform(
|
|
x,
|
|
outlier_method,
|
|
hadamard=self.quarot_hadamard,
|
|
hadanorm_sigma=self.hadanorm_sigma,
|
|
)
|
|
else:
|
|
x_transformed = x
|
|
correction_source = None
|
|
return apply_dynamic_lora_delta(
|
|
x_input=x_transformed,
|
|
y=y_view,
|
|
lora_A=self.lora_A,
|
|
lora_B=self.lora_B,
|
|
lora_alpha=self.lora_alpha,
|
|
lora_entries=self.dynamic_lora_entries,
|
|
device=x.device,
|
|
correction_x=correction_source,
|
|
)
|
|
|
|
x_transformed, correction_source = _apply_outlier_activation_transform(
|
|
x,
|
|
outlier_method,
|
|
hadamard=self.quarot_hadamard,
|
|
hadanorm_sigma=self.hadanorm_sigma,
|
|
)
|
|
x_2d = x_transformed.reshape(-1, x_shape[-1])
|
|
|
|
use_triton = self._runtime_uses_triton
|
|
legacy_triton_unsafe = self._runtime_uses_legacy_triton
|
|
prepack_int8_weights = self._prepack_int8_weights
|
|
weight_packed = (
|
|
_get_prepacked_weight(self, x.device)
|
|
if use_triton and prepack_int8_weights
|
|
else None
|
|
)
|
|
weight_int_mm = (
|
|
_get_torch_int_mm_weight(self, weight, x.device)
|
|
if not use_triton
|
|
else None
|
|
)
|
|
|
|
small_batch_threshold = _get_small_batch_fallback_threshold(self)
|
|
use_small_batch_fallback = small_batch_threshold > 0 and x_2d.shape[0] <= small_batch_threshold
|
|
correction_2d = None
|
|
input_2d = x_2d
|
|
matmul_bias = bias
|
|
if correction_source is not None:
|
|
correction_2d = correction_source.reshape(-1, correction_source.shape[-1])
|
|
input_2d = torch.cat((x_2d, correction_2d), dim=0)
|
|
matmul_bias = None
|
|
|
|
if use_small_batch_fallback:
|
|
if _RUNTIME_STATS_ENABLED:
|
|
Int8TensorwiseOps._increment_runtime_stat("linear_calls")
|
|
Int8TensorwiseOps._increment_runtime_stat("small_batch_fallback")
|
|
# Small batch fallback
|
|
w_float = dequantize(weight, w_scale).to(input_2d.dtype)
|
|
bias_typed = matmul_bias.to(input_2d.dtype) if matmul_bias is not None else None
|
|
y = F.linear(input_2d, w_float, bias_typed)
|
|
else:
|
|
if _RUNTIME_STATS_ENABLED:
|
|
Int8TensorwiseOps._increment_runtime_stat("linear_calls")
|
|
if use_triton:
|
|
if legacy_triton_unsafe:
|
|
Int8TensorwiseOps._increment_runtime_stat("triton_legacy_unsafe")
|
|
else:
|
|
Int8TensorwiseOps._increment_runtime_stat("triton")
|
|
if isinstance(weight_packed, torch.Tensor):
|
|
Int8TensorwiseOps._increment_runtime_stat("prepacked")
|
|
else:
|
|
Int8TensorwiseOps._increment_runtime_stat("torch_int_mm")
|
|
|
|
if self._is_per_row:
|
|
y = int8_forward_dynamic_per_row(
|
|
input_2d,
|
|
weight,
|
|
w_scale,
|
|
matmul_bias,
|
|
compute_dtype,
|
|
use_triton=use_triton,
|
|
weight_packed=weight_packed,
|
|
weight_int_mm=weight_int_mm,
|
|
legacy_triton_unsafe=legacy_triton_unsafe,
|
|
)
|
|
else:
|
|
y = int8_forward_dynamic(
|
|
input_2d,
|
|
weight,
|
|
w_scale,
|
|
matmul_bias,
|
|
compute_dtype,
|
|
use_triton=use_triton,
|
|
weight_packed=weight_packed,
|
|
weight_int_mm=weight_int_mm,
|
|
legacy_triton_unsafe=legacy_triton_unsafe,
|
|
)
|
|
|
|
if correction_2d is not None:
|
|
y, correction = y.split((x_2d.shape[0], correction_2d.shape[0]), dim=0)
|
|
if bias is not None:
|
|
y = y + bias.to(y.dtype)
|
|
y_view = y.reshape(*x_shape[:-1], y.shape[-1])
|
|
correction = correction.reshape(*correction_source.shape[:-1], correction.shape[-1])
|
|
y_view = y_view + correction.to(y_view.dtype)
|
|
else:
|
|
y_view = y.reshape(*x_shape[:-1], y.shape[-1])
|
|
|
|
# Dynamic LoRA Path
|
|
y_view = apply_dynamic_lora_delta(
|
|
x_input=x_transformed,
|
|
y=y_view,
|
|
lora_A=self.lora_A,
|
|
lora_B=self.lora_B,
|
|
lora_alpha=self.lora_alpha,
|
|
lora_entries=self.dynamic_lora_entries,
|
|
device=x.device,
|
|
correction_x=correction_source,
|
|
)
|
|
|
|
return y_view.reshape(*x_shape[:-1], y_view.shape[-1])
|
|
|
|
# Pass-through for other layers
|
|
class GroupNorm(manual_cast.GroupNorm): pass
|
|
class LayerNorm(manual_cast.LayerNorm): pass
|
|
class Conv2d(manual_cast.Conv2d): pass
|
|
class Conv3d(manual_cast.Conv3d): pass
|
|
class ConvTranspose2d(manual_cast.ConvTranspose2d): pass
|
|
class Embedding(manual_cast.Embedding): pass
|
|
|
|
@classmethod
|
|
def conv_nd(cls, dims, *args, **kwargs):
|
|
if dims == 2: return cls.Conv2d(*args, **kwargs)
|
|
elif dims == 3: return cls.Conv3d(*args, **kwargs)
|
|
else: raise ValueError(f"unsupported dimensions: {dims}")
|