research/demo/01_download_base.py
Builder 5dfc4a9fad Lead with a real end-to-end model-capability licensing demo
Rebuild the repo so its spine is a real, reproducible demonstration of
licensing an actual model capability, not payload-agnostic crypto on
stand-in blobs. The clean-room Ed25519 + AES-256-GCM primitives stay as
the fast mechanism layer; the real thing is now the headline.

New demo/ walkthrough (steps 1-7), each a standalone script printing
machine-checked evidence:
  1 download Qwen2.5-0.5B-Instruct from Hugging Face (gitignored cache)
  2 base scores 0.000 on an invented tool-call protocol (capability C)
  3 train a PEFT LoRA on C, base frozen (SHA-256 byte-identical proof)
  4 base + LoRA scores 0.925 on a held-out set with unseen arguments
  5 seal the adapter as an AES-256-GCM unit under an Ed25519 leaf cert
  6 valid licence decrypts-at-load and runs C at 0.925
  7 access-gate then crypto-erase: original and exfiltrated copy both
    permanently undecryptable, base alone back to 0.000

Reference run on an RTX 4090 captured the observed numbers now in the
README. keystore.py gains export_state/load_state so the authority (and
crypto-erasure) persists across the separate demo commands. A single
run_demo.sh drives steps 1-7; run_all.sh + pytest remain the fast
crypto-only mechanism tests.

Ships code only: base weights, HF cache, trained adapter, wrapping keys
and every sealed unit are gitignored and never committed. README rewritten
to lead with the demo and the observed numbers, with honest bounds
(in-memory adapter during a live licence needs a hardware enclave) and a
capability-tree scale-up as future work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 03:20:12 +10:00

57 lines
2 KiB
Python

#!/usr/bin/env python3
"""Step 1: download the base model from Hugging Face into a gitignored cache.
Prints the exact model id, where the weights landed locally, the on-disk size,
and the parameter count. Nothing here is committed: the cache directory is
gitignored, and the reviewer pulls the weights themselves by running this step.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from demo.common import BASE_MODEL_ID, HF_CACHE, HF_LINK
def _dir_size_bytes(path: Path) -> int:
return sum(p.stat().st_size for p in path.rglob("*") if p.is_file())
def main() -> int:
print("=== Step 1: download base model ===\n")
print(f" model id: {BASE_MODEL_ID}")
print(f" hugging face: {HF_LINK}")
print(f" cache dir: {HF_CACHE} (gitignored)\n")
from huggingface_hub import snapshot_download
from transformers import AutoConfig
local_path = snapshot_download(repo_id=BASE_MODEL_ID)
config = AutoConfig.from_pretrained(BASE_MODEL_ID)
n_params = None
# Derive parameter count cheaply from config where possible, else load.
try:
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained(BASE_MODEL_ID)
n_params = sum(p.numel() for p in model.parameters())
del model
except Exception as exc: # pragma: no cover - informational only
print(f" (parameter count skipped: {exc})")
size_gib = _dir_size_bytes(Path(local_path)) / 2**30
print(f" downloaded to: {local_path}")
print(f" on-disk size: {size_gib:.2f} GiB")
if n_params is not None:
print(f" parameters: {n_params:,} ({n_params / 1e6:.0f}M)")
print(f" architecture: {config.architectures}")
print(f" hidden_size={config.hidden_size}, layers={config.num_hidden_layers}")
print("\nRESULT: PASS - base model present locally and ready to run.")
return 0
if __name__ == "__main__":
sys.exit(main())