"""Build a GGUF artifact from a surya VLM checkpoint, suitable for stock llama.cpp. Three patches are applied to llama.cpp's convert_hf_to_gguf.py before running the HF→GGUF conversion. All fixes are baked into the output artifact, so the resulting GGUF runs on unpatched llama.cpp. Two checkpoint-side patches are also applied to a working copy: - config.json: architectures → ["Qwen3_5ForConditionalGeneration"] - tokenizer_config.json: tokenizer_class → "PreTrainedTokenizerFast", strip backend / extra_special_tokens / is_local The original checkpoint is never modified — patches land in a sibling working dir under --out-dir. Usage: python -m surya.scripts.build_gguf \\ --checkpoint datalab-to/surya-2.1.6 \\ --out-dir ./gguf-build """ import argparse import json import os import shutil import subprocess import sys from pathlib import Path LLAMA_CPP_REPO = "https://github.com/ggerganov/llama.cpp.git" # Pinned commit the converter patches were authored against. Bump when # the upstream converter drifts; re-validate the anchor strings below. LLAMA_CPP_REV = "bbeb89d76c41bc250f16e4a6fefcc9b530d6e3f3" # ---- llama.cpp converter patches ----------------------------------------- # Each patch is (sentinel, anchor, replacement). Idempotent: if `sentinel` # is already in the file, the patch is skipped. _PATCH_REGISTER_HASH_ANCHOR = """ if chkhsh == "862f827721df956049dff5ca81a57f29e575280bc622e290d3bf4e35eca29015": # ref: https://huggingface.co/codefuse-ai/F2LLM-v2-4B res = "f2llmv2" """ _PATCH_REGISTER_HASH_REPLACEMENT = ( _PATCH_REGISTER_HASH_ANCHOR + """ if chkhsh == "11865354be60ff9206694aed04242190f1807029bbd750bfbced09b4d26f1ad2": # surya-2.1.0 char-level WordLevel tokenizer (Split regex=".") res = "default" """ ) _PATCH_VOCAB_ANCHOR = """ def _set_vocab_gpt2(self) -> None: tokens, toktypes, tokpre = self.get_vocab_base() self.gguf_writer.add_tokenizer_model("gpt2") self.gguf_writer.add_tokenizer_pre(tokpre) self.gguf_writer.add_token_list(tokens) self.gguf_writer.add_token_types(toktypes) special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=True) special_vocab.add_to_gguf(self.gguf_writer) """ _PATCH_VOCAB_REPLACEMENT = ''' def _set_vocab_gpt2(self) -> None: tokens, toktypes, tokpre = self.get_vocab_base() # surya: char-level / WordLevel vocabs contain raw bytes (e.g. " ", # "\\n"). llama.cpp's gpt2 vocab decoder applies bytes_to_unicode # when emitting NORMAL tokens, so encode bytes here for round-trip. # Idempotent on already-encoded vocabs. tokens = self._maybe_encode_gpt2_bytes(tokens, toktypes) self.gguf_writer.add_tokenizer_model("gpt2") self.gguf_writer.add_tokenizer_pre(tokpre) self.gguf_writer.add_token_list(tokens) self.gguf_writer.add_token_types(toktypes) special_vocab = gguf.SpecialVocab(self.dir_model, load_merges=True) special_vocab.add_to_gguf(self.gguf_writer) # surya: char-level / WordLevel vocabs have no merges, but the gpt2 # vocab loader requires the field. Write a single dummy entry. if not special_vocab.merges: self.gguf_writer.add_token_merges(["a a"]) @staticmethod def _maybe_encode_gpt2_bytes(tokens: list[str], toktypes: list[int]) -> list[str]: """Apply GPT-2 bytes_to_unicode to NORMAL tokens iff any contain raw whitespace/control bytes. Idempotent on already-encoded vocabs.""" bs = (list(range(ord("!"), ord("~") + 1)) + list(range(ord("¡"), ord("¬") + 1)) + list(range(ord("®"), ord("ÿ") + 1))) cs = bs[:] n = 0 for b in range(256): if b not in bs: bs.append(b) cs.append(2 ** 8 + n) n += 1 byte_to_unicode = {b: chr(c) for b, c in zip(bs, cs)} printable = set(range(ord("!"), ord("~") + 1)) needs = False for tok, ttype in zip(tokens, toktypes): if ttype != gguf.TokenType.NORMAL: continue for ch in tok: cb = ord(ch) if cb < 0x80 and cb not in printable: needs = True break if needs: break if not needs: return tokens out: list[str] = [] for tok, ttype in zip(tokens, toktypes): if ttype == gguf.TokenType.NORMAL: out.append("".join(byte_to_unicode[b] for b in tok.encode("utf-8"))) else: out.append(tok) return out ''' CONVERTER_PATCHES = [ { "name": "register surya-2.1.0 pre-tokenizer hash", "sentinel": '"11865354be60ff9206694aed04242190f1807029bbd750bfbced09b4d26f1ad2"', "anchor": _PATCH_REGISTER_HASH_ANCHOR, "replacement": _PATCH_REGISTER_HASH_REPLACEMENT, }, { "name": "byte-encode NORMAL tokens + dummy merges in _set_vocab_gpt2", "sentinel": "_maybe_encode_gpt2_bytes", "anchor": _PATCH_VOCAB_ANCHOR, "replacement": _PATCH_VOCAB_REPLACEMENT, }, ] def patch_converter(convert_py: Path) -> None: text = convert_py.read_text() changed = False for p in CONVERTER_PATCHES: if p["sentinel"] in text: print(f" [skip] {p['name']} (already applied)") continue if p["anchor"] not in text: raise RuntimeError( f"converter patch {p['name']!r} could not find its anchor. " f"llama.cpp upstream may have drifted; re-pin LLAMA_CPP_REV " f"and re-validate anchors." ) text = text.replace(p["anchor"], p["replacement"], 1) print(f" [apply] {p['name']}") changed = True if changed: convert_py.write_text(text) # ---- checkpoint-side patches ---------------------------------------------- def patch_checkpoint(src: Path, dst: Path) -> None: """Symlink-clone src into dst, with config.json + tokenizer_config.json rewritten for stock transformers/llama.cpp compatibility.""" if dst.exists(): shutil.rmtree(dst) dst.mkdir(parents=True) overrides = {"config.json", "tokenizer_config.json"} for entry in src.iterdir(): if entry.name in overrides: continue os.symlink(entry.resolve(), dst / entry.name) cfg = json.loads((src / "config.json").read_text()) cfg["architectures"] = ["Qwen3_5ForConditionalGeneration"] (dst / "config.json").write_text(json.dumps(cfg, indent=2)) tk = json.loads((src / "tokenizer_config.json").read_text()) tk["tokenizer_class"] = "PreTrainedTokenizerFast" for k in ("backend", "extra_special_tokens", "is_local"): tk.pop(k, None) (dst / "tokenizer_config.json").write_text(json.dumps(tk, indent=2)) # ---- llama.cpp resolution ------------------------------------------------- def ensure_llama_cpp(repo_dir: Path, rev: str) -> Path: if not repo_dir.exists(): repo_dir.parent.mkdir(parents=True, exist_ok=True) print(f"[clone] {LLAMA_CPP_REPO} → {repo_dir}") subprocess.check_call(["git", "clone", LLAMA_CPP_REPO, str(repo_dir)]) head = subprocess.check_output( ["git", "-C", str(repo_dir), "rev-parse", "HEAD"], text=True ).strip() if head != rev: # Discard any prior patches so the checkout is clean. subprocess.check_call( ["git", "-C", str(repo_dir), "reset", "--hard", "--quiet", "HEAD"] ) subprocess.check_call( ["git", "-C", str(repo_dir), "fetch", "--quiet", "origin"] ) print(f"[checkout] llama.cpp @ {rev}") subprocess.check_call(["git", "-C", str(repo_dir), "checkout", "--quiet", rev]) return repo_dir # ---- checkpoint resolution ------------------------------------------------ def resolve_checkpoint(checkpoint: str) -> Path: p = Path(checkpoint) if p.exists(): return p.resolve() from huggingface_hub import snapshot_download print(f"[download] {checkpoint}") return Path(snapshot_download(checkpoint)) # ---- main ----------------------------------------------------------------- def main() -> int: from surya.settings import settings ap = argparse.ArgumentParser( description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter, ) ap.add_argument( "--checkpoint", default=settings.SURYA_MODEL_CHECKPOINT, help="HF repo id or local checkpoint dir", ) ap.add_argument("--out-dir", type=Path, default=Path("./gguf-build")) ap.add_argument( "--name", default="surya-2", help="Output basename. Produces .gguf and -mmproj.gguf", ) ap.add_argument( "--llama-cpp-dir", type=Path, default=Path.home() / ".cache" / "datalab" / "llama.cpp", ) ap.add_argument("--llama-cpp-rev", default=LLAMA_CPP_REV) ap.add_argument( "--outtype", default="f16", help="convert_hf_to_gguf --outtype (f16, bf16, q8_0, ...)", ) ap.add_argument( "--keep-work", action="store_true", help="Keep the patched-checkpoint working dir on success", ) args = ap.parse_args() args.out_dir.mkdir(parents=True, exist_ok=True) work = args.out_dir / "_patched_ckpt" src = resolve_checkpoint(args.checkpoint) print(f"[checkpoint] {src}") print("[patch] checkpoint config + tokenizer_config") patch_checkpoint(src, work) repo = ensure_llama_cpp(args.llama_cpp_dir, args.llama_cpp_rev) convert_py = repo / "convert_hf_to_gguf.py" print("[patch] llama.cpp convert_hf_to_gguf.py") patch_converter(convert_py) out_llm = (args.out_dir / f"{args.name}.gguf").resolve() out_mmproj = (args.out_dir / f"{args.name}-mmproj.gguf").resolve() print(f"[convert] LLM → {out_llm}") subprocess.check_call( [ sys.executable, str(convert_py), str(work), "--outfile", str(out_llm), "--outtype", args.outtype, ] ) print(f"[convert] mmproj → {out_mmproj}") subprocess.check_call( [ sys.executable, str(convert_py), str(work), "--mmproj", "--outfile", str(out_mmproj), "--outtype", args.outtype, ] ) if not args.keep_work: shutil.rmtree(work) print() print(f" LLM: {out_llm}") print(f" mmproj: {out_mmproj}") return 0 if __name__ == "__main__": sys.exit(main())