#!/usr/bin/env python3 """Verify the published LoRA adapter loads and can generate (needs GPU for full test).""" from __future__ import annotations import argparse import json import os import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts")) from hf_auth import resolve_hf_token # noqa: E402 DEFAULT_ADAPTER = os.environ.get( "PMS_FINETUNED_MODEL", "GuusBouwensNL/plane-mode-nemotron-4b-study-coach", ) DEFAULT_BASE = os.environ.get( "PMS_FT_BASE_MODEL", "nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16", ) def verify_hub_files(adapter_repo: str, token: str | None) -> dict: from huggingface_hub import HfApi api = HfApi(token=token) info = api.model_info(adapter_repo) names = {s.rfilename for s in info.siblings} required = {"adapter_config.json", "adapter_model.safetensors"} missing = sorted(required - names) if missing: raise FileNotFoundError(f"Missing on Hub: {missing}") cfg = json.loads( Path( api.hf_hub_download(adapter_repo, "adapter_config.json", token=token) ).read_text(encoding="utf-8") ) base = cfg.get("base_model_name_or_path", DEFAULT_BASE) return { "adapter_repo": adapter_repo, "base_model": base, "peft_type": cfg.get("peft_type"), "target_modules": cfg.get("target_modules"), "files": sorted(names), } def smoke_generate(adapter_repo: str, base_model: str, token: str | None) -> str: import torch from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer if not torch.cuda.is_available(): raise RuntimeError("CUDA required for smoke generation test") tokenizer = AutoTokenizer.from_pretrained( adapter_repo, token=token, trust_remote_code=True ) base = AutoModelForCausalLM.from_pretrained( base_model, token=token, torch_dtype=torch.bfloat16, device_map="auto", trust_remote_code=True, ) model = PeftModel.from_pretrained(base, adapter_repo, token=token) model.eval() messages = [ { "role": "system", "content": "You are Plane Mode Scholar, a concise study coach.", }, {"role": "user", "content": "In one sentence, what is gradient descent?"}, ] text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True, enable_thinking=False ) inputs = tokenizer(text, return_tensors="pt").to(model.device) with torch.no_grad(): out = model.generate(**inputs, max_new_tokens=64, do_sample=False) reply = tokenizer.decode(out[0][inputs["input_ids"].shape[1] :], skip_special_tokens=True) return reply.strip() def main() -> int: parser = argparse.ArgumentParser(description="Verify fine-tuned Nemotron adapter") parser.add_argument("--adapter", default=DEFAULT_ADAPTER) parser.add_argument("--base", default=None, help="Override base model") parser.add_argument("--smoke", action="store_true", help="Run GPU generation test") args = parser.parse_args() token = resolve_hf_token() print(f"Adapter: {args.adapter}") meta = verify_hub_files(args.adapter, token) base = args.base or meta["base_model"] print(f"Base: {base}") print(f"PEFT: {meta['peft_type']} r={meta.get('target_modules')}") print("Hub files OK") if not args.smoke: print("Skipping generation (pass --smoke on a CUDA machine)") return 0 reply = smoke_generate(args.adapter, base, token) print(f"Sample reply: {reply[:300]}") return 0 if reply else 1 if __name__ == "__main__": raise SystemExit(main())