Files
wehub-resource-sync 925e56bb5f
Unit tests / build (t4_gpu) (push) Has been cancelled
Unit tests / build (ubuntu-latest) (push) Has been cancelled
Unit tests / build (windows-latest) (push) Has been cancelled
Test CLI scripts / build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:56 +08:00

312 lines
11 KiB
Python

"""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 <name>.gguf and <name>-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())