tinygemma3-2m / convert_to_gguf.py
shibatch's picture
Upload folder using huggingface_hub
554dc63 verified
Raw
History Blame Contribute Delete
4.17 kB
#!/usr/bin/env python3
"""Convert this repository's Hugging Face checkpoint to Q4_K_M GGUF.
The tokenizer is a small custom ByteLevel BPE. Its behavior is GPT-2-style,
but its tokenizer probe hash is not yet present in llama.cpp's generated
pre-tokenizer lookup table. This wrapper validates the tokenizer structure
before supplying the corresponding ``gpt-2`` pre-tokenizer identifier.
"""
from __future__ import annotations
import argparse
import json
import runpy
import shutil
import subprocess
import sys
from pathlib import Path
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--llama-cpp",
type=Path,
required=True,
help="Path to a llama.cpp checkout containing convert_hf_to_gguf.py",
)
quantizer = shutil.which("llama-quantize")
parser.add_argument(
"--quantizer",
type=Path,
default=Path(quantizer) if quantizer else None,
help="Path to llama-quantize (default: resolve it from PATH)",
)
parser.add_argument(
"--outfile",
type=Path,
default=Path("gguf/tinygemma3-2m-Q4_K_M.gguf"),
)
return parser.parse_args()
def is_expected_bytelevel_bpe(model_dir: Path) -> bool:
tokenizer = json.loads((model_dir / "tokenizer.json").read_text())
return (
tokenizer.get("normalizer") is None
and tokenizer.get("model", {}).get("type") == "BPE"
and tokenizer.get("model", {}).get("byte_fallback") is False
and tokenizer.get("pre_tokenizer")
== {
"type": "ByteLevel",
"add_prefix_space": False,
"trim_offsets": False,
"use_regex": True,
}
)
def main() -> None:
args = parse_args()
repo_dir = Path(__file__).resolve().parent
model_dir = repo_dir / "hf"
converter = args.llama_cpp.resolve() / "convert_hf_to_gguf.py"
quantizer = args.quantizer.resolve() if args.quantizer else None
outfile = args.outfile if args.outfile.is_absolute() else repo_dir / args.outfile
intermediate = outfile.parent / ".tinygemma3-2m-f16.intermediate.gguf"
if not converter.is_file():
raise SystemExit(f"llama.cpp converter not found: {converter}")
if quantizer is None or not quantizer.is_file():
raise SystemExit("llama-quantize not found; pass it with --quantizer")
if not is_expected_bytelevel_bpe(model_dir):
raise SystemExit("Unexpected tokenizer structure; refusing to guess GGUF metadata")
sys.path.insert(0, str(args.llama_cpp.resolve()))
from conversion import TextModel # noqa: PLC0415
from conversion.gemma import Gemma3Model # noqa: PLC0415
original = TextModel.get_vocab_base_pre
def get_vocab_base_pre(self: TextModel, tokenizer: object) -> str:
if Path(self.dir_model).resolve() == model_dir.resolve():
return "gpt-2"
return original(self, tokenizer)
def modify_tensors(
self: Gemma3Model, data_torch: object, name: str, bid: int | None
) -> object:
# This checkpoint intentionally pads the embedding matrix and logits
# from 1,003 tokenizer entries to config.vocab_size=1,024. Keep those
# rows so the GGUF architecture and HF reference logits stay aligned.
f_shift = self.norm_shift(name)
if f_shift != 0.0:
data_torch = data_torch + f_shift
yield from super(Gemma3Model, self).modify_tensors(data_torch, name, bid)
TextModel.get_vocab_base_pre = get_vocab_base_pre
Gemma3Model.modify_tensors = modify_tensors
outfile.parent.mkdir(parents=True, exist_ok=True)
sys.argv = [
str(converter),
str(model_dir),
"--outfile",
str(intermediate),
"--outtype",
"f16",
"--model-name",
"tinygemma3-2m",
]
try:
runpy.run_path(str(converter), run_name="__main__")
subprocess.run(
[str(quantizer), str(intermediate), str(outfile), "Q4_K_M"],
check=True,
)
finally:
intermediate.unlink(missing_ok=True)
if __name__ == "__main__":
main()