# SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 """Convert a published ``.pt`` pre-quant checkpoint (denoiser or text encoder) to ``.safetensors``. python scripts/convert_prequant_to_safetensors.py SRC.pt [SRC2.pt ...] --out-dir DIR \ [--repo owner/name --revision SHA] Writes ``DIR///.safetensors`` (``DIR/.safetensors`` without ``--repo``), the layout ``UNSLOTH_DIFFUSION_PREQUANT_MIRROR`` reads. Nothing is uploaded. Denoisers use the container Qwen-Image-2.1's published artifacts already use (``prequant_safetensors``: torchao's flattened tensors plus ``unsloth_format`` / ``unsloth_metadata``), so a released Studio that resolves ``-.safetensors`` reads it too. Two header keys are added: ``unsloth_quant_layout`` (how to rebuild every weight from its plain tensors without torchao, and for int8 that the weights came from torchao v1, so torchao <= 0.17 rebuilds the v1 class the ``.pt`` held) and ``unsloth_source`` (the file, its sha256 and the repo revision it was converted from). ``unsloth_metadata`` is copied unchanged. Text encoders are plain fp8 / bf16 tensors and are written in the plain layout ``load_plain_prequant_safetensors`` reads, with no torchao involved. Every weight is checked after writing: the file is read back through Studio's loader and each tensor compared bit for bit with the ``.pt`` as the same install reads it. """ from __future__ import annotations import argparse import hashlib import json import os import sys import time from pathlib import Path BACKEND = Path(__file__).resolve().parent.parent / "studio" / "backend" def sha256_of(path: str, chunk: int = 64 << 20) -> str: digest = hashlib.sha256() with open(path, "rb") as fh: while True: block = fh.read(chunk) if not block: break digest.update(block) return digest.hexdigest() def _record_v1_facts(facts: list): """Wrap the legacy decoder so each v1 weight's facts are recorded before it is rebuilt (torchao >= 0.18). Patches a module global for the duration of one read, so conversions must not run concurrently in one process (this is a single-threaded CLI).""" from core.inference import prequant_legacy_int8 as legacy original = legacy._rebuild_weight def recording(name, w, standins, api): facts.append(_v1_facts_of(w, standins)) return original(name, w, standins, api) legacy._rebuild_weight = recording return lambda: setattr(legacy, "_rebuild_weight", original) def _v1_facts_of(w, standins = None) -> dict: """The INT8_V1_FACTS view of one v1 weight (a real torchao object or the decoder's stand-in).""" from core.inference.prequant_legacy_int8 import _ACT_QUANT, _LAYOUT state = getattr(w, "__dict__", {}) aqt = state.get("original_weight_tensor") impl = getattr(aqt, "tensor_impl", None) act = state.get("input_quant_func") scale = getattr(impl, "scale", None) zero_point = getattr(impl, "zero_point", None) domain = getattr(aqt, "zero_point_domain", None) layout = getattr(impl, "_layout", None) if standins is not None: # torchao >= 0.18 read the pickle into stand-ins registered under the v1 names: report those names. if act is standins.get(_ACT_QUANT): act = _ACT_QUANT if isinstance(layout, standins.get(_LAYOUT, ())): layout = _LAYOUT return { "act_quant": (act if isinstance(act, str) else getattr(act, "__name__", str(act))).rsplit( ".", 1 )[-1], "quant_kwargs": dict(state.get("quant_kwargs") or {}), "zero_point": None if zero_point is None else "tensor", "zero_point_domain": getattr(domain, "name", str(domain)), "quant_min": getattr(aqt, "quant_min", "?"), "quant_max": getattr(aqt, "quant_max", "?"), "scale_shape": "out" if scale is not None and scale.dim() == 1 else "other", "layout": layout.rsplit(".", 1)[-1] if isinstance(layout, str) else type(layout).__name__, } def _to_int8_tensor_dict(state_dict: dict, facts: list) -> dict: """torchao <= 0.17 hands back v1 objects: record their facts and rebuild them as Int8Tensor to flatten.""" from core.inference.prequant_legacy_int8 import ( LEGACY_INT8_CLASS_NAMES, _ACT_QUANT, _int8_tensor_api, _rebuild_weight, _resolve, ) classes = {n: _resolve(n) for n in (*LEGACY_INT8_CLASS_NAMES, _ACT_QUANT)} laqt = classes[LEGACY_INT8_CLASS_NAMES[0]] if laqt is None: return state_dict api = _int8_tensor_api() out = {} for key, value in state_dict.items(): if isinstance(value, laqt): facts.append(_v1_facts_of(value)) out[key] = _rebuild_weight(key, value, classes, api) else: out[key] = value return out def _canonical(value): """``(class name, {attr: tensor or value})`` of a weight, recursing into torchao subclasses.""" import torch if type(value) is torch.Tensor: return ("Tensor", value) flatten = getattr(value, "__tensor_flatten__", None) if flatten is None: return (type(value).__name__, value) names, ctx = flatten() return (type(value).__name__, {n: _canonical(getattr(value, n)) for n in names}, repr(ctx)) def _same(a, b) -> bool: import torch if isinstance(a, torch.Tensor) and isinstance(b, torch.Tensor): if a.dtype != b.dtype or tuple(a.shape) != tuple(b.shape): return False # Raw bytes, not torch.equal: -0.0 == 0.0 and NaN != NaN would otherwise decide. Flattened first: # a 0-dim tensor (nvfp4's per-tensor scale) cannot be viewed as bytes of another element size. return a.numel() == 0 or torch.equal( a.detach().contiguous().cpu().reshape(-1).view(torch.uint8), b.detach().contiguous().cpu().reshape(-1).view(torch.uint8), ) if isinstance(a, tuple) and isinstance(b, tuple): return len(a) == len(b) and all(_same(x, y) for x, y in zip(a, b, strict = True)) if isinstance(a, dict) and isinstance(b, dict): return a.keys() == b.keys() and all(_same(a[k], b[k]) for k in a) return a == b def compare_state_dicts(reference: dict, candidate: dict) -> list: """Keys whose class, attributes or tensor bytes differ (empty = bit-identical).""" bad = sorted(set(reference) ^ set(candidate)) for key in reference: if key in candidate and not _same(_canonical(reference[key]), _canonical(candidate[key])): bad.append(key) return bad def _untie(flat: dict) -> dict: """safetensors refuses tensors that share storage (tied embeddings): write each one as its own copy. Studio's text-encoder loader re-ties after ``load_state_dict`` (``tie_weights``), exactly as it does for the published safetensors encoders, so the copy costs disk only.""" seen: set = set() out = {} for key, value in flat.items(): value = value.contiguous() ptr = value.untyped_storage().data_ptr() if value.numel() else None if ptr is not None and ptr in seen: value = value.clone() elif ptr is not None: seen.add(ptr) out[key] = value return out def tied_copies(state_dict: dict) -> dict: """``{alias: source}`` for keys that share storage, shape and stride with an earlier key (a tied embedding).""" seen: dict = {} aliases = {} for key, value in state_dict.items(): if not getattr(value, "numel", lambda: 0)(): continue ident = ( value.untyped_storage().data_ptr(), value.storage_offset(), tuple(value.shape), tuple(value.stride()), value.dtype, ) if ident in seen: aliases[key] = seen[ident] else: seen[ident] = key return aliases def write_plain_safetensors( path: str, *, fmt: str, state_dict: dict, metadata: dict, extra: dict ) -> None: """The torchao-free plain layout ``load_plain_prequant_safetensors`` reads (text encoders).""" import torch from safetensors.torch import save_file from core.inference.prequant_safetensors import ( UNSLOTH_FORMAT_KEY, UNSLOTH_METADATA_KEY, UNSLOTH_ROOT_KEYS_KEY, UNSLOTH_ROOT_PREFIX, ) names, roots, flat = [], [], {} for key, value in state_dict.items(): if type(value) is not torch.Tensor: raise ValueError(f"{key!r} is a {type(value).__name__}, not a plain tensor") if "." in key: names.append(key) flat[key] = value.contiguous() else: roots.append(key) flat[f"{UNSLOTH_ROOT_PREFIX}{key}"] = value.contiguous() header = {name: json.dumps({"_type": "Tensor"}) for name in names} header["tensor_names"] = json.dumps(names) header[UNSLOTH_FORMAT_KEY] = fmt header[UNSLOTH_METADATA_KEY] = json.dumps(metadata, default = str) if roots: header[UNSLOTH_ROOT_KEYS_KEY] = json.dumps(roots) header.update(extra) save_file(_untie(flat), path, metadata = header) def convert( src: str, out_dir: str, *, repo: str | None, revision: str | None, sha: str | None, drop_tied: bool = False, ) -> dict: import torch from core.inference import diffusion_prequant as dp from core.inference import prequant_safetensors as ps from core.inference.diffusion_te_prequant import TE_PREQUANT_FORMAT from core.inference.prequant_native import ( INT8_SOURCE_V1, INT8_V1_FACTS, QUANT_LAYOUT_KEY, SOURCE_KEY, quant_layout_header, ) t0 = time.time() name = os.path.basename(src) stem = os.path.splitext(name)[0] if name.endswith((".pt", ".pth")) else name dest_dir = os.path.join(out_dir, *repo.split("/")) if repo else out_dir os.makedirs(dest_dir, exist_ok = True) dst = os.path.join(dest_dir, stem + ".safetensors") source = {"file": name, "sha256": sha or sha256_of(src), "bytes": os.path.getsize(src)} if repo: source["repo"] = repo if revision: source["revision"] = revision try: import torchao source["converter_torchao"] = torchao.__version__ except Exception: # noqa: BLE001 pass source["converter_torch"] = torch.__version__ # Read as Studio does (allowlisted weights_only); on torchao >= 0.18 v1 int8 loads as Int8Tensor. facts: list = [] restore = _record_v1_facts(facts) try: if str(src).lower().endswith(".safetensors"): ckpt = dp._load_prequant_checkpoint(src, map_location = "cpu", mmap = True) else: # Bypasses Studio's nvfp4 pickle refusal: the only way a local nvfp4 .pt becomes .safetensors. ckpt = dp._torch_load_prequant(src, map_location = "cpu", mmap = True) finally: restore() fmt = ckpt.get("format") is_te = fmt == TE_PREQUANT_FORMAT metadata = dict(ckpt.get("metadata") or {}) state_dict = ckpt["state_dict"] extra = {SOURCE_KEY: json.dumps(source)} dropped: dict = {} if is_te: written = state_dict if drop_tied: # Store a tied embedding once (UMT5's encoder.embed_tokens.weight is shared.weight): the TE loader fills # the alias from the model's own tie (diffusion_te_prequant._fill_tied_aliases). Older readers refuse such # a file and load the dense encoder, so only use it for an encoder no older release was pointed at. # A tied lm_head stays: the loader never fills a head alias (whether a head is tied is per config). dropped = { alias: key for alias, key in tied_copies(state_dict).items() if not alias.endswith("lm_head.weight") } written = {k: v for k, v in state_dict.items() if k not in dropped} write_plain_safetensors(dst, fmt = fmt, state_dict = written, metadata = metadata, extra = extra) back = ps.load_plain_prequant_safetensors(dst) for alias, source_key in dropped.items(): back["state_dict"][alias] = back["state_dict"][source_key] else: if fmt not in dp.PREQUANT_FORMATS: raise ValueError(f"{src}: unrecognised format {fmt!r}") flat_sd = _to_int8_tensor_dict(state_dict, facts) int8_source = None if facts: int8 = sum(type(v).__name__ == "Int8Tensor" for v in flat_sd.values()) if len(facts) != int8: raise ValueError( f"{src}: {int8} int8 weights but {len(facts)} came from torchao v1" ) distinct = {json.dumps(f, sort_keys = True, default = str) for f in facts} if distinct != {json.dumps(INT8_V1_FACTS, sort_keys = True)}: raise ValueError( f"{src}: v1 int8 weights do not match the supported v1 layout: {sorted(distinct)[:3]}" ) int8_source = INT8_SOURCE_V1 layout = quant_layout_header(flat_sd, int8_source = int8_source) from safetensors.torch import save_file helpers = ps._torchao_helpers() if helpers is None: raise RuntimeError("torchao >= 0.16 is required to write the flattened layout") undotted = ps.unsupported_state_dict_keys(flat_sd) if undotted: raise ValueError( f"{src}: root-level quantized weights cannot be flattened: {undotted[:8]}" ) roots = ps._root_level_keys(flat_sd) quantizable = {k: v for k, v in flat_sd.items() if k not in set(roots)} flat, torchao_metadata = helpers[0](quantizable) header = dict(torchao_metadata or {}) header[ps.UNSLOTH_FORMAT_KEY] = str(fmt) header[ps.UNSLOTH_METADATA_KEY] = json.dumps(metadata, default = str) header[QUANT_LAYOUT_KEY] = json.dumps(layout) header.update(extra) if roots: flat = dict(flat) for key in roots: flat[f"{ps.UNSLOTH_ROOT_PREFIX}{key}"] = flat_sd[key].contiguous() header[ps.UNSLOTH_ROOT_KEYS_KEY] = json.dumps([str(k) for k in roots]) save_file(_untie(dict(flat)), dst, metadata = header) back = ps.load_prequant_safetensors(dst, mmap = True) # Bit-identity against the .pt as this install reads it. bad = compare_state_dicts(state_dict, back["state_dict"]) if bad: raise AssertionError(f"{dst}: {len(bad)} tensors differ from {src}, e.g. {bad[:3]}") if back["format"] != fmt or back["metadata"] != metadata: raise AssertionError(f"{dst}: format or metadata changed in the round trip") rec = { "src": src, "dst": dst, "kind": "text_encoder" if is_te else "transformer", "format": fmt, "scheme": metadata.get("scheme"), "tensors": len(state_dict), "quantized": sum(1 for v in state_dict.values() if type(v) is not torch.Tensor), "int8_source": None if is_te else layout.get("int8_source"), "reader": back.get("reader"), "src_bytes": source["bytes"], "src_sha256": source["sha256"], "dst_bytes": os.path.getsize(dst), "dst_sha256": sha256_of(dst), "seconds": round(time.time() - t0, 1), "verified_bit_identical": True, "tied_copies_dropped": dropped, } return rec def main(argv = None) -> int: p = argparse.ArgumentParser(description = __doc__.split("\n\n")[0]) p.add_argument("src", nargs = "+") p.add_argument("--out-dir", required = True) p.add_argument( "--repo", default = None, help = "owner/name the .pt came from (mirror layout + provenance)" ) p.add_argument("--revision", default = None) p.add_argument("--sha256", default = None, help = "known sha256 of a single SRC (skips hashing it)") p.add_argument("--report", default = None, help = "append one JSON line per file here") p.add_argument( "--drop-tied-copies", action = "store_true", help = "text encoders: store a tied tensor once (the loader re-fills the alias from the model's tie)", ) args = p.parse_args(argv) sys.path.insert(0, str(BACKEND)) for src in args.src: rec = convert( src, args.out_dir, repo = args.repo, revision = args.revision, sha = args.sha256 if len(args.src) == 1 else None, drop_tied = args.drop_tied_copies, ) line = json.dumps(rec) print(line, flush = True) if args.report: with open(args.report, "a") as fh: fh.write(line + "\n") return 0 if __name__ == "__main__": raise SystemExit(main())