Piko-9b / scripts /validate_inference.py
Dexy2's picture
Rewrite model card around verified evidence; correct misattributed benchmarks and config path leak
0810902 verified
Raw
History Blame Contribute Delete
14.5 kB
#!/usr/bin/env python3
"""Load Piko-9b once and exercise every capability the model card claims.
Each check is recorded independently, so a failure in one modality does not
hide the results of the others. Output is a JSON record suitable for pasting
into the audit report; nothing here is scored by hand.
Usage
-----
python scripts/validate_inference.py --model <path-or-repo-id> \
--output reports/inference_validation.json
"""
from __future__ import annotations
import argparse
import json
import platform
import sys
import time
import traceback
from collections.abc import Callable
from pathlib import Path
from typing import Any
def build_ocr_image(path: Path) -> None:
"""Render a deterministic synthetic receipt. No network, no licensing risk."""
from PIL import Image, ImageDraw
image = Image.new("RGB", (520, 300), "white")
draw = ImageDraw.Draw(image)
lines = [
"NORTHGATE HARDWARE",
"144 Mill Road",
"",
"Date: 2026-03-14",
"Invoice: 40817",
"",
"Hex bolts M6 12.40",
"Wood glue 6.25",
"Sandpaper pack 4.10",
"",
"TOTAL 22.75",
]
y = 18
for line in lines:
draw.text((24, y), line, fill="black")
y += 24
image.save(path)
def build_chart_image(path: Path) -> None:
"""Render a deterministic bar chart with labelled values."""
from PIL import Image, ImageDraw
image = Image.new("RGB", (460, 300), "white")
draw = ImageDraw.Draw(image)
bars = [("Q1", 40), ("Q2", 95), ("Q3", 60), ("Q4", 130)]
base_y = 250
for index, (label, value) in enumerate(bars):
x = 60 + index * 90
draw.rectangle([x, base_y - value, x + 50, base_y], fill="black")
draw.text((x + 12, base_y + 8), label, fill="black")
draw.text((x + 6, base_y - value - 16), str(value), fill="black")
draw.text((40, 12), "Units sold by quarter", fill="black")
image.save(path)
class Validator:
def __init__(
self, model_path: str, dtype: str, device_map: Any, quantization: str = "none"
) -> None:
self.model_path = model_path
self.dtype = dtype
self.device_map = device_map
self.quantization = quantization
self.results: list[dict[str, Any]] = []
self.model = None
self.processor = None
self.tokenizer = None
# -- harness ---------------------------------------------------------- #
def check(self, name: str, fn: Callable[[], Any]) -> Any:
started = time.perf_counter()
try:
detail = fn()
record = {
"check": name,
"status": "pass",
"seconds": round(time.perf_counter() - started, 2),
"detail": detail,
}
except Exception as exc: # noqa: BLE001 - every failure must be recorded
record = {
"check": name,
"status": "fail",
"seconds": round(time.perf_counter() - started, 2),
"error": f"{type(exc).__name__}: {exc}",
"traceback": traceback.format_exc(limit=4),
}
self.results.append(record)
marker = "PASS" if record["status"] == "pass" else "FAIL"
print(f"[{marker}] {name} ({record['seconds']}s)", flush=True)
if record["status"] == "fail":
print(f" {record['error']}", flush=True)
return record
# -- loading ---------------------------------------------------------- #
def load(self) -> dict[str, Any]:
import torch
from transformers import AutoConfig, AutoProcessor, AutoTokenizer
torch_dtype = {"bfloat16": torch.bfloat16, "float16": torch.float16}[self.dtype]
config = AutoConfig.from_pretrained(self.model_path)
extra: dict[str, Any] = {}
if self.quantization == "4bit":
from transformers import BitsAndBytesConfig
extra["quantization_config"] = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch_dtype,
bnb_4bit_use_double_quant=True,
)
elif self.quantization == "8bit":
from transformers import BitsAndBytesConfig
extra["quantization_config"] = BitsAndBytesConfig(load_in_8bit=True)
loaded_with = None
model = None
errors: dict[str, str] = {}
for class_name in ("AutoModelForMultimodalLM", "AutoModelForImageTextToText"):
try:
import transformers
cls = getattr(transformers, class_name)
except AttributeError:
errors[class_name] = "class not available in this transformers version"
continue
try:
model = cls.from_pretrained(
self.model_path,
dtype=torch_dtype,
device_map=self.device_map,
**extra,
)
loaded_with = class_name
break
except Exception as exc: # noqa: BLE001
errors[class_name] = f"{type(exc).__name__}: {exc}"
if model is None:
raise RuntimeError(f"No auto class could load the model: {errors}")
model.eval()
self.model = model
self.processor = AutoProcessor.from_pretrained(self.model_path)
self.tokenizer = AutoTokenizer.from_pretrained(self.model_path)
parameters = sum(p.numel() for p in model.parameters())
vision_parameters = 0
if hasattr(model, "model") and hasattr(model.model, "visual"):
vision_parameters = sum(p.numel() for p in model.model.visual.parameters())
return {
"loaded_with": loaded_with,
"auto_class_errors": errors,
"trust_remote_code_required": False,
"architectures": config.architectures,
"model_type": config.model_type,
"total_parameters": parameters,
"vision_parameters": vision_parameters,
"language_parameters": parameters - vision_parameters,
"device_map": str(getattr(model, "hf_device_map", self.device_map)),
"processor_class": type(self.processor).__name__,
"tokenizer_class": type(self.tokenizer).__name__,
}
# -- generation helpers ----------------------------------------------- #
def _generate(self, messages: list[dict[str, Any]], max_new_tokens: int) -> str:
import torch
inputs = self.processor.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
return_tensors="pt",
).to(self.model.device)
with torch.inference_mode():
output = self.model.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=False)
prompt_length = inputs["input_ids"].shape[1]
return self.processor.decode(output[0][prompt_length:], skip_special_tokens=True).strip()
def text_only(self, prompt: str, max_new_tokens: int = 96) -> dict[str, Any]:
messages = [{"role": "user", "content": [{"type": "text", "text": prompt}]}]
text = self._generate(messages, max_new_tokens)
return {"prompt": prompt, "response": text}
def with_image(
self, image_path: Path, prompt: str, max_new_tokens: int = 128
) -> dict[str, Any]:
messages = [
{
"role": "user",
"content": [
{"type": "image", "url": str(image_path)},
{"type": "text", "text": prompt},
],
}
]
text = self._generate(messages, max_new_tokens)
return {"image": image_path.name, "prompt": prompt, "response": text}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--model", required=True)
parser.add_argument("--dtype", default="bfloat16", choices=["bfloat16", "float16"])
parser.add_argument("--device-map", default="auto")
parser.add_argument(
"--quantization",
default="none",
choices=["none", "4bit", "8bit"],
help="CPU offload corrupts this architecture; use 4bit to stay resident on one GPU.",
)
parser.add_argument("--assets", type=Path, default=Path("evaluation/prompts/assets"))
parser.add_argument("--output", type=Path, default=Path("reports/inference_validation.json"))
args = parser.parse_args()
import torch
import transformers
args.assets.mkdir(parents=True, exist_ok=True)
ocr_image = args.assets / "synthetic_receipt.png"
chart_image = args.assets / "synthetic_chart.png"
build_ocr_image(ocr_image)
build_chart_image(chart_image)
device_map: Any = args.device_map
if args.quantization != "none" and device_map == "auto":
device_map = {"": 0} # keep every module on one device
validator = Validator(args.model, args.dtype, device_map, args.quantization)
environment = {
"python": platform.python_version(),
"platform": platform.platform(),
"torch": torch.__version__,
"transformers": transformers.__version__,
"cuda_available": torch.cuda.is_available(),
"gpu": torch.cuda.get_device_name(0) if torch.cuda.is_available() else None,
"vram_bytes": torch.cuda.get_device_properties(0).total_memory
if torch.cuda.is_available()
else None,
"dtype": args.dtype,
"device_map": str(device_map),
"quantization": args.quantization,
"model": args.model,
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
}
load_record = validator.check("load_model", validator.load)
if load_record["status"] == "fail":
_write(args.output, environment, validator.results)
sys.exit("Model failed to load; remaining checks skipped.")
validator.check(
"text_only_generation",
lambda: validator.text_only("Write a Python function that reverses a string."),
)
validator.check(
"text_only_identity",
lambda: validator.text_only("What model are you? Answer in one short sentence.", 48),
)
validator.check(
"text_only_reasoning",
lambda: validator.text_only(
"A shop sells pens at 3 for $2. How much do 12 pens cost? Answer with the number only.",
48,
),
)
validator.check(
"image_ocr",
lambda: validator.with_image(
ocr_image, "Read this receipt. Give the merchant name and the total."
),
)
validator.check(
"image_document_json",
lambda: validator.with_image(
ocr_image,
'Return only JSON: {"merchant": str, "date": "YYYY-MM-DD", "total": float}',
),
)
validator.check(
"image_chart",
lambda: validator.with_image(chart_image, "Which quarter is highest, and what value?"),
)
validator.check(
"image_caption",
lambda: validator.with_image(chart_image, "Describe this image in one sentence."),
)
def multi_turn() -> dict[str, Any]:
messages = [
{"role": "user", "content": [{"type": "text", "text": "My favourite number is 47."}]},
{"role": "assistant", "content": [{"type": "text", "text": "Noted."}]},
{
"role": "user",
"content": [{"type": "text", "text": "Double my favourite number. Number only."}],
},
]
return {"response": validator._generate(messages, 32)}
validator.check("multi_turn_conversation", multi_turn)
def determinism() -> dict[str, Any]:
first = validator.text_only("Name three primary colours.", 32)["response"]
second = validator.text_only("Name three primary colours.", 32)["response"]
return {"identical": first == second, "first": first, "second": second}
validator.check("greedy_determinism", determinism)
def batch() -> dict[str, Any]:
import torch
prompts = ["Capital of Japan?", "2 + 2 = ?"]
texts = [
validator.processor.apply_chat_template(
[{"role": "user", "content": [{"type": "text", "text": p}]}],
add_generation_prompt=True,
tokenize=False,
)
for p in prompts
]
inputs = validator.processor(text=texts, return_tensors="pt", padding=True).to(
validator.model.device
)
with torch.inference_mode():
output = validator.model.generate(**inputs, max_new_tokens=24, do_sample=False)
decoded = [
validator.processor.decode(
output[i][inputs["input_ids"].shape[1] :], skip_special_tokens=True
).strip()
for i in range(len(prompts))
]
return {"prompts": prompts, "responses": decoded}
validator.check("batch_inference", batch)
def long_context() -> dict[str, Any]:
needle = "The maintenance code for the north pump is QF-8812."
filler = "Routine log entry: all systems nominal. " * 900
prompt = f"{filler}\n{needle}\n{filler}\n\nWhat is the maintenance code for the north pump?"
tokens = len(validator.tokenizer(prompt)["input_ids"])
response = validator.text_only(prompt, 32)["response"]
return {
"prompt_tokens": tokens,
"response": response,
"contains_needle": "QF-8812" in response,
}
validator.check("long_context_retrieval", long_context)
_write(args.output, environment, validator.results)
passed = sum(1 for r in validator.results if r["status"] == "pass")
print(f"\n{passed}/{len(validator.results)} checks passed -> {args.output}")
def _write(output: Path, environment: dict[str, Any], results: list[dict[str, Any]]) -> None:
output.parent.mkdir(parents=True, exist_ok=True)
payload = {
"environment": environment,
"summary": {
"total": len(results),
"passed": sum(1 for r in results if r["status"] == "pass"),
"failed": sum(1 for r in results if r["status"] == "fail"),
},
"results": results,
}
output.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
if __name__ == "__main__":
main()