# SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 """Int8 ConvRot TE checkpoint, group-256 Hadamard; keys must match the strict load_state_dict the loader expects; lm_head omitted, pipelines read hidden states only""" from __future__ import annotations import argparse import hashlib import json import os import re import sys from pathlib import Path BACKEND = Path(__file__).resolve().parent.parent / "studio" / "backend" DECODER_LINEAR = re.compile( r"^model\.(language_model\.)?layers\.\d+\.(self_attn\.[qkvo]_proj|mlp\.(gate|up|down)_proj)\.weight$" ) # UMT5 / T5 encoder blocks (Wan's UMT5-XXL): self-attention q/k/v/o and the MLP, gated (wi_0 / wi_1) or not (wi), + wo. T5_ENCODER_LINEAR = re.compile( r"^encoder\.block\.\d+\.layer\.(0\.SelfAttention\.[qkvo]|1\.DenseReluDense\.(wi_0|wi_1|wi|wo))\.weight$" ) _T5_CLASSES = ("UMT5EncoderModel", "T5EncoderModel") # The encoder classes' _keep_in_fp32_modules: a projection kept dense is stored float32, as from_pretrained holds it. _T5_KEEP_FP32 = ("wo",) def _linear_pattern(te_class): return T5_ENCODER_LINEAR if te_class in _T5_CLASSES else DECODER_LINEAR _ST_DTYPES = {"float32": "F32", "bfloat16": "BF16", "float16": "F16", "int8": "I8", "uint8": "U8"} def _write_safetensors(path: Path, tensors: dict, metadata: dict) -> None: """safetensors with a canonical header (sorted keys) so a rebuild is byte-identical; ``save_file`` orders the metadata map differently per run. Widest dtype first, so every tensor starts at its own alignment.""" import torch order = sorted(tensors, key = lambda n: (-tensors[n].element_size(), n)) header: dict = {"__metadata__": dict(sorted(metadata.items()))} offset = 0 for name in order: t = tensors[name] size = t.numel() * t.element_size() header[name] = { "dtype": _ST_DTYPES[str(t.dtype).replace("torch.", "")], "shape": list(t.shape), "data_offsets": [offset, offset + size], } offset += size blob = json.dumps(header, sort_keys = True, separators = (",", ":")).encode("utf-8") blob += b" " * (-len(blob) % 8) with open(path, "wb") as fh: fh.write(len(blob).to_bytes(8, "little")) fh.write(blob) for name in order: fh.write(tensors[name].contiguous().view(torch.uint8).numpy().tobytes()) def _download_component(base: str, component: str, token) -> Path: """The component folder of ``base``, data files only: the build reads JSON and safetensors (safe_open), never anything executable, so nothing else is fetched.""" from huggingface_hub import snapshot_download root = snapshot_download( base, allow_patterns = [f"{component}/*.json", f"{component}/*.safetensors"], token = token, ) return Path(root) / component def main(argv = None) -> int: p = argparse.ArgumentParser() p.add_argument("--base", required = True, help = "diffusers base repo carrying text_encoder/") p.add_argument("--family", required = True) p.add_argument("--component", default = "text_encoder") p.add_argument("--src", default = None, help = "local dense encoder folder (skips the download)") p.add_argument("--out", required = True, help = "output .safetensors path") p.add_argument("--group", type = int, default = 256) p.add_argument( "--device", default = "cuda" if os.environ.get("CUDA_VISIBLE_DEVICES") != "" else "cpu" ) p.add_argument("--hf-token", default = None) p.add_argument( "--load", choices = ("shards", "transformers", "ideogram4"), default = "shards", help = "ideogram4: build the encoder with Unsloth's Ideogram 4 loader (vendor fp8 + weight_scale " "dequantized to a bf16 Qwen3VLModel), then quantize it like --load transformers", ) p.add_argument( "--te-class", default = None, help = "transformers class to load (--load transformers)" ) p.add_argument( "--base-model-id", default = None, help = "base_model_id to record (default: --base)" ) p.add_argument( "--keep-dense", default = "", help = "comma-separated Linear leaf names left dense (e.g. 'wo' for UMT5); fp32-kept modules stay float32", ) args = p.parse_args(argv) keep_dense = tuple(t for t in (x.strip() for x in args.keep_dense.split(",")) if t) if args.load in ("transformers", "ideogram4"): return _main_transformers(args) sys.path.insert(0, str(BACKEND)) import safetensors import torch import transformers from safetensors import safe_open from core.inference.diffusion_te_prequant import ( TE_PREQUANT_FORMAT_INT8_CONVROT, quantize_int8_convrot_weight, ) from core.inference.prequant_safetensors import UNSLOTH_FORMAT_KEY, UNSLOTH_METADATA_KEY src = ( Path(args.src) if args.src else _download_component(args.base, args.component, args.hf_token) ) config = json.loads((src / "config.json").read_text(encoding = "utf-8")) index_path = src / "model.safetensors.index.json" if index_path.is_file(): weight_map = json.loads(index_path.read_text(encoding = "utf-8"))["weight_map"] else: with safe_open(str(src / "model.safetensors"), framework = "pt") as handle: weight_map = {name: "model.safetensors" for name in handle.keys()} te_class = (config.get("architectures") or [None])[0] pattern = _linear_pattern(te_class) keep_fp32 = _T5_KEEP_FP32 if te_class in _T5_CLASSES else () device = torch.device(args.device if torch.cuda.is_available() else "cpu") quant = {"format": "int8_tensorwise", "convrot": True, "convrot_groupsize": args.group} blob = torch.tensor(list(json.dumps(quant).encode("utf-8")), dtype = torch.uint8) tensors: dict = {} quantized = 0 for shard in sorted(set(weight_map.values())): with safe_open(str(src / shard), framework = "pt", device = "cpu") as handle: for name in sorted(n for n, s in weight_map.items() if s == shard): if name == "lm_head.weight": continue tensor = handle.get_tensor(name) leaf = ( name[: -len(".weight")].rsplit(".", 1)[-1] if name.endswith(".weight") else "" ) if pattern.match(name) and leaf not in keep_dense: q, scale = quantize_int8_convrot_weight(tensor.to(device), args.group) prefix = name[: -len(".weight")] tensors[name] = q.cpu().contiguous() tensors[prefix + ".weight_scale"] = scale.cpu().contiguous() tensors[prefix + ".comfy_quant"] = blob.clone() quantized += 1 elif pattern.match(name) and leaf in keep_fp32: tensors[name] = tensor.to(torch.float32).contiguous() else: tensors[name] = tensor.to(torch.bfloat16).contiguous() if not quantized: raise SystemExit("no projections matched; is this a Qwen3 / Qwen3-VL or UMT5 text encoder?") metadata = { "base_model_id": args.base_model_id or args.base, "family": args.family, "scheme": "int8", "component": args.component, "te_class": te_class, "torch_dtype": "bfloat16", "quant": dict( quant, weight_only = True, scale = "per_output_channel_absmax_over_127", linears = quantized, pattern = pattern.pattern, kept_dense = list(keep_dense), ), "lm_head": "dropped" if te_class not in _T5_CLASSES else "absent", "torch_version": torch.__version__, "transformers_version": transformers.__version__, "safetensors_version": safetensors.__version__, } names = sorted(tensors) header = { UNSLOTH_FORMAT_KEY: TE_PREQUANT_FORMAT_INT8_CONVROT, UNSLOTH_METADATA_KEY: json.dumps(metadata, sort_keys = True), "tensor_names": json.dumps(names), } header.update({name: json.dumps({"_type": "Tensor"}) for name in names}) out = Path(args.out) out.parent.mkdir(parents = True, exist_ok = True) tmp = out.with_name(out.name + ".tmp") _write_safetensors(tmp, tensors, header) os.replace(tmp, out) digest = hashlib.sha256() with open(out, "rb") as fh: for chunk in iter(lambda: fh.read(1 << 24), b""): digest.update(chunk) print( json.dumps( { "out": str(out), "bytes": out.stat().st_size, "sha256": digest.hexdigest(), "quantized_linears": quantized, "tensors": len(names), } ) ) return 0 def _main_transformers(args) -> int: """``--load transformers``: quantize the runtime state dict of the encoder class the pipeline builds.""" sys.path.insert(0, str(BACKEND)) import safetensors import torch import transformers from core.inference.diffusion_te_prequant import ( TE_INT8_CONVROT_LINEAR, TE_PREQUANT_FORMAT_INT8_CONVROT, quantize_int8_convrot_weight, ) from core.inference.prequant_safetensors import UNSLOTH_FORMAT_KEY, UNSLOTH_METADATA_KEY src = args.src or args.base if args.load == "ideogram4": if args.component != "text_encoder": raise SystemExit("--load ideogram4 reads /text_encoder only") from core.inference.diffusion_ideogram4 import load_ideogram4_text_encoder # The exact module the runtime builds (from_pretrained cannot read the vendor fp8 layout). encoder = load_ideogram4_text_encoder(src, torch.bfloat16, hf_token = args.hf_token).eval() te_class = type(encoder).__name__ else: kwargs = {"token": args.hf_token, "torch_dtype": torch.bfloat16} if not args.src: kwargs["subfolder"] = args.component config = transformers.AutoConfig.from_pretrained( src, **{k: v for k, v in kwargs.items() if k != "torch_dtype"} ) te_class = args.te_class or (getattr(config, "architectures", None) or [None])[0] encoder = getattr(transformers, te_class).from_pretrained(src, **kwargs).eval() device = torch.device(args.device if torch.cuda.is_available() else "cpu") quant = {"format": "int8_tensorwise", "convrot": True, "convrot_groupsize": args.group} blob = torch.tensor(list(json.dumps(quant).encode("utf-8")), dtype = torch.uint8) keep_dense = tuple(t for t in (x.strip() for x in args.keep_dense.split(",")) if t) linears = { name for name, module in encoder.named_modules() if isinstance(module, torch.nn.Linear) and TE_INT8_CONVROT_LINEAR.search(name) and name.rsplit(".", 1)[-1] not in keep_dense } # The encoder's own fp32-kept modules (T5's ``wo``) are stored float32 when left dense, as the shard path does. keep_fp32 = tuple(str(m) for m in (getattr(encoder, "_keep_in_fp32_modules", None) or ())) tensors: dict = {} for name, tensor in encoder.state_dict().items(): if name == "lm_head.weight": continue prefix = name[: -len(".weight")] if name.endswith(".weight") else None if prefix in linears: q, scale = quantize_int8_convrot_weight(tensor.to(device), args.group) tensors[name] = q.cpu().contiguous() tensors[prefix + ".weight_scale"] = scale.cpu().contiguous() tensors[prefix + ".comfy_quant"] = blob.clone() else: if not tensor.is_floating_point(): dtype = tensor.dtype elif any(t in name.split(".") for t in keep_fp32): dtype = torch.float32 else: dtype = torch.bfloat16 tensors[name] = tensor.detach().to("cpu", dtype) tensors[name] = tensors[name].contiguous().clone() if not linears: raise SystemExit(f"no Linear in {te_class} matched TE_INT8_CONVROT_LINEAR") metadata = { "base_model_id": args.base_model_id or args.base, "family": args.family, "scheme": "int8", "component": args.component, "te_class": te_class, "torch_dtype": "bfloat16", "quant": dict( quant, weight_only = True, scale = "per_output_channel_absmax_over_127", linears = len(linears), ), "lm_head": "dropped", "torch_version": torch.__version__, "transformers_version": transformers.__version__, "safetensors_version": safetensors.__version__, } names = sorted(tensors) header = { UNSLOTH_FORMAT_KEY: TE_PREQUANT_FORMAT_INT8_CONVROT, UNSLOTH_METADATA_KEY: json.dumps(metadata, sort_keys = True), "tensor_names": json.dumps(names), } header.update({name: json.dumps({"_type": "Tensor"}) for name in names}) out = Path(args.out) out.parent.mkdir(parents = True, exist_ok = True) tmp = out.with_name(out.name + ".tmp") _write_safetensors(tmp, tensors, header) os.replace(tmp, out) digest = hashlib.sha256() with open(out, "rb") as fh: for chunk in iter(lambda: fh.read(1 << 24), b""): digest.update(chunk) print( json.dumps( { "out": str(out), "bytes": out.stat().st_size, "sha256": digest.hexdigest(), "te_class": te_class, "quantized_linears": len(linears), "tensors": len(names), } ) ) return 0 if __name__ == "__main__": raise SystemExit(main())