#!/usr/bin/env python3 """ Convert the TRELLIS.2 sparse-structure flow DiT checkpoint (ss_flow_img_dit_1_3B_64_bf16.safetensors) to a GGUF file for trellis2.cpp. This is the stage-1 generator: a ~1.3B-param DiT with adaLN-Zero modulation (share_mod), self-attention + cross-attention to the DINOv3 cond tokens, 3D RoPE, and QK-RMSNorm. See trellis2/models/sparse_structure_flow.py. Like the sam3.cpp converters this is a self-contained script (only safetensors + numpy + torch). It writes a standard GGUF v3 file — no `gguf` package needed — so the C++ side loads it with ggml's built-in gguf_init_from_file(): tensors are keyed by their original checkpoint names and hyperparameters travel as KV metadata under the `trellis2.ss_flow.*` namespace. Usage: python convert_ss_flow_to_gguf.py \ --model /path/to/ss_flow_img_dit_1_3B_64_bf16.safetensors \ --output ss_flow_dit.gguf --ftype 1 # --model/--config default to the microsoft/TRELLIS.2-4B HF cache snapshot. ftype: 0 = f32 (lossless upcast from the bf16 checkpoint), 1 = f16 (default; big 2-D weight matrices only, norms/gammas stay f32), 2 = bf16 (lossless, native checkpoint precision; needs bf16-capable ggml). """ import argparse import glob import json import os import struct import sys import numpy as np # ── GGUF / GGML constants (must match the bundled ggml) ────────────────────── GGUF_MAGIC = b"GGUF" GGUF_VERSION = 3 GGUF_ALIGNMENT = 32 # GGML tensor types GGML_TYPE_F32 = 0 GGML_TYPE_F16 = 1 GGML_TYPE_BF16 = 30 # GGUF metadata value types GGUF_VT_UINT32 = 4 GGUF_VT_INT32 = 5 GGUF_VT_FLOAT32 = 6 GGUF_VT_BOOL = 7 GGUF_VT_STRING = 8 ARCH = "trellis2-ss-flow" KV_PREFIX = "trellis2.ss_flow." DEFAULT_SNAPSHOT = os.path.expanduser( "~/.cache/huggingface/hub/models--microsoft--TRELLIS.2-4B/snapshots/*/ckpts" ) CKPT_STEM = "ss_flow_img_dit_1_3B_64_bf16" # ── GGUF writer (minimal, v3) ──────────────────────────────────────────────── def _gguf_str(s: str) -> bytes: b = s.encode("utf-8") return struct.pack(" bytes: return _gguf_str(key) + struct.pack(" int: return (n + a - 1) // a * a # ── ftype policy ───────────────────────────────────────────────────────────── def choose_type(name: str, shape, ftype: int) -> int: """Pick the on-disk ggml type for a tensor given the requested ftype.""" if ftype == 0: return GGML_TYPE_F32 if ftype == 2: return GGML_TYPE_BF16 # ftype == 1: f16 for the big 2-D weight matrices, f32 for everything that # is precision-sensitive (norm gammas, modulation, biases, all 1-D). keep_f32 = ("gamma" in name) or ("modulation" in name) or ("norm" in name) if len(shape) >= 2 and not keep_f32: return GGML_TYPE_F16 return GGML_TYPE_F32 def to_bytes(t, ggml_type: int) -> bytes: """torch tensor -> raw little-endian bytes in the chosen ggml type.""" import torch t = t.detach().cpu().contiguous() if ggml_type == GGML_TYPE_F32: return t.float().numpy().astype("> 16) & 1)) >> 16 return rounded.astype(" 0 else [1] # ggml ne[] order tensors.append((name, gtype, dims, raw)) counts[gtype] += 1 print(f"tensors: {len(tensors)} " f"(f32={counts[GGML_TYPE_F32]}, f16={counts[GGML_TYPE_F16]}, bf16={counts[GGML_TYPE_BF16]})") # ── assemble header + infos, compute aligned data offsets ──────────────── header = bytearray() header += GGUF_MAGIC header += struct.pack("