Model management (v2.4.0): - single-active-per-family registry releases the previous model fully (RAM, VRAM, ComfyUI current_loaded_models) before a new one loads - file-identity cache keys (basename+mtime+size+attention+q4+dtype) prevent cross-file collisions; ASR request keys share the consumer namespace so identical re-runs never spuriously evict Quant-resident runtime (v2.5.0): - GGUF weights stay raw-block resident end-to-end: uint8 parameters, per-matmul dequant kernels for Q8_0/Q4_K/Q5_K/Q6_K pinned bitwise to the gguf-py oracle; F32/F16/BF16 pass through at native dtype via zero-copy views; load-time RAM spike (~2x float size) eliminated - quantized safetensors via *.comfy_quant metadata: rotated ConvRot INT8 residents through comfy-kitchen, plain rowwise int8 / fp8 e4m3+e5m2 / int8_blockwise dequant-at-load (per-row, scalar, and per-gs-block scale layouts); unsupported formats hard-fail with actionable errors; dense gate rejects unplanned quant storages; rotated non-Linear targets (embeddings) fail with re-export guidance - dtype casts filter quant-resident storage (_quant_resident markers, fp32 weight_scale protection); SageAttention wrapper resolves activation dtype per module kind, fixing uint8-weight crash Tests: 937 passed / 5 pre-existing failures / 4 skipped
133 lines
4.9 KiB
Python
133 lines
4.9 KiB
Python
"""Shared quantized-linear replacement and weight-plan validation.
|
|
|
|
Used by BOTH quant-resident families:
|
|
|
|
- GGUF raw-block residency (:mod:`modules.gguf_quant`) — ``nn.Linear`` targets
|
|
are swapped for :class:`~modules.gguf_quant.GGUFLinear` before any weights
|
|
are assigned.
|
|
- ConvRot INT8 checkpoints (:mod:`modules.convrot_quant`) — targets are swapped
|
|
for :class:`~modules.convrot_quant.ConvRotInt8Linear`.
|
|
|
|
Replacement must run BEFORE weight assignment (the swapped modules own
|
|
differently-shaped/dtyped parameters). All checks are strict: a target that is
|
|
missing, not an ``nn.Linear``, or shape-inconsistent raises immediately rather
|
|
than silently misloading weights.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
import torch
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class QuantTargetMismatch(RuntimeError):
|
|
"""Raised when a planned quantized-linear replacement cannot be applied."""
|
|
|
|
|
|
def resolve_module(model: torch.nn.Module, path: str) -> torch.nn.Module:
|
|
"""Resolve a dotted module path against ``model`` (KeyError if absent)."""
|
|
mod = model
|
|
for part in path.split("."):
|
|
if not hasattr(mod, part):
|
|
raise KeyError(path)
|
|
mod = getattr(mod, part)
|
|
return mod
|
|
|
|
|
|
def replace_linears_for_quant(model: torch.nn.Module, layer_plan: dict) -> list:
|
|
"""Swap every module named in ``layer_plan`` for a factory-built module.
|
|
|
|
Args:
|
|
model: The model tree to mutate.
|
|
layer_plan: mapping of dotted module path ->
|
|
callable ``(in_features, out_features, has_bias) -> nn.Module``.
|
|
|
|
Returns:
|
|
List of replaced module paths (order follows the plan).
|
|
|
|
Raises:
|
|
QuantTargetMismatch: On missing / non-Linear / shape-mismatched
|
|
targets. Nothing is replaced unless ALL targets validate first.
|
|
"""
|
|
modules = dict(model.named_modules())
|
|
built = []
|
|
for prefix, factory in layer_plan.items():
|
|
if prefix not in modules:
|
|
raise QuantTargetMismatch(
|
|
f"Quantized layer '{prefix}' not found in model tree"
|
|
)
|
|
target = modules[prefix]
|
|
if isinstance(target, torch.nn.Linear):
|
|
in_f = target.in_features
|
|
out_f = target.out_features
|
|
bias = target.bias is not None
|
|
else:
|
|
kind = type(target).__name__
|
|
raise QuantTargetMismatch(
|
|
f"Quantized layer '{prefix}' is {kind}, expected nn.Linear. "
|
|
f"Rotated INT8 quantization can only execute on Linear "
|
|
f"modules. If this checkpoint quantizes/rotates non-Linear "
|
|
f"modules (e.g. embeddings, norms), re-export it excluding "
|
|
f"those layers (e.g. the converter's skip-embeddings / "
|
|
f"--heur option)."
|
|
)
|
|
try:
|
|
new_mod = factory(in_f, out_f, bias)
|
|
except Exception as e:
|
|
raise QuantTargetMismatch(
|
|
f"Failed to build quantized replacement for '{prefix}': {e}"
|
|
) from e
|
|
built.append((prefix, new_mod))
|
|
|
|
parent_cache = {}
|
|
replaced = []
|
|
for prefix, new_mod in built:
|
|
parent_name, _, child_name = prefix.rpartition(".")
|
|
if parent_name not in parent_cache:
|
|
parent_cache[parent_name] = (
|
|
model if not parent_name else resolve_module(model, parent_name)
|
|
)
|
|
setattr(parent_cache[parent_name], child_name, new_mod)
|
|
replaced.append(prefix)
|
|
|
|
logger.info(f"Replaced {len(replaced)} nn.Linear(s) with quant-resident modules")
|
|
return replaced
|
|
|
|
|
|
def validate_weight_plan(
|
|
*,
|
|
is_gguf_file: bool,
|
|
convrot_quant_map: dict | None,
|
|
use_llm_4bit: bool,
|
|
attention_mode: str,
|
|
gguf_kquant_present: bool = False,
|
|
) -> None:
|
|
"""Single choke point for quantization-family exclusivity rules (D4).
|
|
|
|
Rules:
|
|
- GGUF file + ConvRot metadata present -> hard error (mutually exclusive).
|
|
- ConvRot checkpoint + bnb 4-bit requested -> hard error.
|
|
- SageAttention over GGUF K-quants -> allowed, warning only (sage patches
|
|
attention math only; K-quant linears are unaffected).
|
|
"""
|
|
if is_gguf_file and convrot_quant_map:
|
|
raise ValueError(
|
|
"Weight plan conflict: the file contains both GGUF tensors and "
|
|
"ConvRot (*.comfy_quant) metadata. These formats are mutually "
|
|
"exclusive; the checkpoint is likely corrupted or misconverted."
|
|
)
|
|
if convrot_quant_map and use_llm_4bit:
|
|
raise ValueError(
|
|
"Weight plan conflict: the checkpoint already carries ConvRot INT8 "
|
|
"quantized linears; 'quantize_llm_4bit' cannot be combined with it."
|
|
)
|
|
if attention_mode == "sage" and is_gguf_file and gguf_kquant_present:
|
|
logger.warning(
|
|
"SageAttention requested alongside GGUF K-quants: sage patches "
|
|
"attention computation only, quantized linear layers still run "
|
|
"through per-matmul dequantization."
|
|
)
|