securecoder-scripts / merge_securecoder.py
Taimwe's picture
Add merge+GGUF script
d9dfe3c verified
Raw History Blame
5.51 kB
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "unsloth",
# "transformers>=4.57",
# "huggingface_hub",
# ]
# ///
"""Merge the SecureCoder LoRA into the base Qwen3-Coder-30B-A3B-Instruct
checkpoint, upload a 16-bit safetensors repo, then quantise to Q4_K_M GGUF.
Default base: unsloth/Qwen3-Coder-30B-A3B-Instruct
Default adapter: Taimwe/securecoder-30b-pro
Run on HF Jobs (a100-large has the headroom to load 30B in 16-bit):
hf jobs run -d --flavor a100-large --timeout 90m --secrets HF_TOKEN \\
ghcr.io/astral-sh/uv:python3.12-bookworm \\
uv run --no-project https://huggingface.co/Taimwe/securecoder-scripts/resolve/main/merge_securecoder.py \\
-- --adapter Taimwe/securecoder-30b-pro \\
--output-repo Taimwe/securecoder-30b-pro-merged \\
--gguf-repo Taimwe/securecoder-30b-pro-GGUF
"""
from __future__ import annotations
import argparse
import logging
import os
import shutil
import sys
import time
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("merge")
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Merge + GGUF + push the SecureCoder LoRA")
p.add_argument("--base", default="unsloth/Qwen3-Coder-30B-A3B-Instruct")
p.add_argument("--adapter", default="Taimwe/securecoder-30b-pro")
p.add_argument("--output-repo", default="Taimwe/securecoder-30b-pro-merged")
p.add_argument("--gguf-repo", default=None)
p.add_argument("--gguf-quant", default="Q4_K_M")
p.add_argument("--private", action="store_true")
p.add_argument("--work-dir", default="/data/securecoder-merge")
p.add_argument("--max-shard-size", default="5GB")
return p.parse_args()
def main() -> int:
args = parse_args()
token = os.environ.get("HF_TOKEN")
if not token:
log.error("HF_TOKEN not set")
return 1
import torch
from huggingface_hub import HfApi
from unsloth import FastLanguageModel
if not torch.cuda.is_available():
log.error("no CUDA - merge needs a GPU")
return 1
log.info("GPU: %s", torch.cuda.get_device_name(0))
work = args.work_dir
if os.path.exists(work):
shutil.rmtree(work)
os.makedirs(work, exist_ok=True)
log.info("loading base %s in 16-bit ...", args.base)
started = time.time()
model, tokenizer = FastLanguageModel.from_pretrained(
model_name=args.base,
max_seq_length=2048,
dtype=torch.bfloat16,
load_in_4bit=False,
)
log.info("loading adapter %s ...", args.adapter)
from peft import PeftModel
model = PeftModel.from_pretrained(model, args.adapter, token=token)
log.info("merging ...")
model = model.merge_and_unload()
log.info("merge done in %.1f min", (time.time() - started) / 60)
out_dir = os.path.join(work, "merged")
model.save_pretrained(out_dir, safe_serialization=True, max_shard_size=args.max_shard_size)
tokenizer.save_pretrained(out_dir)
log.info("saved merged model to %s", out_dir)
api = HfApi(token=token)
api.create_repo(args.output_repo, repo_type="model", exist_ok=True, private=args.private)
log.info("uploading to %s ...", args.output_repo)
api.upload_folder(folder_path=out_dir, repo_id=args.output_repo, repo_type="model",
commit_message="Merge SecureCoder LoRA into base (16-bit)")
log.info("merged model live: https://huggingface.co/%s", args.output_repo)
if args.gguf_repo:
log.info("re-loading merged model for GGUF export ...")
from unsloth import FastLanguageModel as FLM
model, tokenizer = FLM.from_pretrained(
model_name=out_dir,
max_seq_length=2048,
dtype=torch.bfloat16,
load_in_4bit=False,
)
gguf_path = os.path.join(work, "gguf")
os.makedirs(gguf_path, exist_ok=True)
log.info("quantising to %s ...", args.gguf_quant)
try:
model.quantize_gguf_model(save_dir=gguf_path, quantization=args.gguf_quant)
except Exception as exc: # noqa: BLE001
log.warning("model.quantize_gguf_model failed (%s); falling back to llama-quantize CLI", exc)
from huggingface_hub import hf_hub_download
from pathlib import Path as _P
qbin = hf_hub_download(repo_id="unsloth/llama.cpp", filename="llama-quantize",
repo_type="model", token=token)
import subprocess
subprocess.run(["chmod", "+x", qbin], check=False)
src = next(_P(out_dir).glob("*.gguf"), None)
if src is None:
log.error("no GGUF produced by Unsloth quantise pass")
return 1
subprocess.run([qbin, str(src), str(_P(gguf_path) / src.name), args.gguf_quant], check=True)
api.create_repo(args.gguf_repo, repo_type="model", exist_ok=True, private=args.private)
api.upload_folder(folder_path=gguf_path, repo_id=args.gguf_repo, repo_type="model",
commit_message=f"Add {args.gguf_quant} GGUF export")
log.info("GGUF live: https://huggingface.co/%s", args.gguf_repo)
print("=" * 78)
print("MERGE COMPLETE")
print(f" merged: https://huggingface.co/{args.output_repo}")
if args.gguf_repo:
print(f" gguf : https://huggingface.co/{args.gguf_repo}")
print("=" * 78)
return 0
if __name__ == "__main__":
raise SystemExit(main())