#!/usr/bin/env python3 """ Convert the TRELLIS.2 sparse-structure DECODER checkpoint (ss_dec_conv3d_16l8_fp16.safetensors) to a GGUF file for trellis2.cpp. This is the stage-1 decoder D_S: a dense 3D-conv ResNet that turns the sparse-structure latent z_s ([8, 16, 16, 16]) into an occupancy logit grid at 64^3 ([1, 64, 64, 64]). It upsamples 16 -> 32 -> 64 with two pixel-shuffle blocks. Architecture (config ss_dec_conv3d_16l8_fp16.json): SparseStructureDecoder(out_channels=1, latent_channels=8, num_res_blocks=2, num_res_blocks_middle=2, channels=[512, 128, 32], norm_type="layer") Like convert_ss_flow_to_gguf.py this is self-contained (safetensors + numpy + torch) and writes a standard GGUF v3 file read back by ggml's gguf_init_from_file(). Hyperparameters travel as KV metadata under the `trellis2.ss_dec.*` namespace; tensors keep their original checkpoint names. Conv3d weights are 5-D in PyTorch ([OC, IC, kD, kH, kW]). ggml tensors are 4-D, and ggml_conv_3d_direct wants the kernel as ne = [kW, kH, kD, IC*OC] with the merged channel index packed oc*IC + ic. A C-contiguous [OC, IC, kD, kH, kW] array reshaped to [OC*IC, kD, kH, kW] is exactly that packing (and identical bytes), so we just reshape before writing — no permute, no data movement. Usage: python convert_ss_dec_to_gguf.py --output ss_dec.gguf --ftype 0 # --model/--config default to the microsoft/TRELLIS-image-large HF snapshot. ftype: 0 = f32 (lossless upcast from the fp16 checkpoint; use for validation), 1 = f16 (default; conv weight matrices f16, norms/biases f32). """ 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_TYPE_F32 = 0 GGML_TYPE_F16 = 1 GGUF_VT_UINT32 = 4 GGUF_VT_INT32 = 5 GGUF_VT_FLOAT32 = 6 GGUF_VT_BOOL = 7 GGUF_VT_STRING = 8 ARCH = "trellis2-ss-dec" KV_PREFIX = "trellis2.ss_dec." DEFAULT_SNAPSHOT = os.path.expanduser( "~/.cache/huggingface/hub/models--microsoft--TRELLIS-image-large/snapshots/*/ckpts" ) CKPT_STEM = "ss_dec_conv3d_16l8_fp16" # ── 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 def choose_type(shape, ftype: int) -> int: """f32 always for ftype 0; for ftype 1 the big conv weight matrices are f16 while everything 1-D (biases, norm weight/bias) stays f32.""" if ftype == 0: return GGML_TYPE_F32 return GGML_TYPE_F16 if len(shape) >= 2 else GGML_TYPE_F32 def to_bytes(arr_f32: np.ndarray, ggml_type: int) -> bytes: if ggml_type == GGML_TYPE_F32: return arr_f32.astype(" [OC*IC, kD, kH, kW] OC, IC, kD, kH, kW = shape arr = np.ascontiguousarray(arr).reshape(OC * IC, kD, kH, kW) shape = arr.shape gtype = choose_type(shape, args.ftype) raw = to_bytes(np.ascontiguousarray(arr), gtype) dims = list(reversed(shape)) if len(shape) > 0 else [1] # ggml ne[] order tensors.append((name, gtype, dims, raw)) counts[gtype] += 1 print(f"tensors: {len(tensors)} (f32={counts[GGML_TYPE_F32]}, f16={counts[GGML_TYPE_F16]})") # ── assemble header + infos, compute aligned data offsets ──────────────── header = bytearray() header += GGUF_MAGIC header += struct.pack("