Download source/src/vimeml/deployment/bundle.py from Voltline/vimeml-tiny-ja-v2.1: direct link, hf CLI and curl.
- Browser
- Download file 7.98 kB
-
https://huggingface.co/Voltline/vimeml-tiny-ja-v2.1/resolve/main/source/src/vimeml/deployment/bundle.py
- Command line
-
hf download hf://Voltline/vimeml-tiny-ja-v2.1/source/src/vimeml/deployment/bundle.py
-
curl -L -o bundle.py https://huggingface.co/Voltline/vimeml-tiny-ja-v2.1/resolve/main/source/src/vimeml/deployment/bundle.py
7.98 kB
| """Inference-only bundles and portable provenance; never rewrite source artifacts.""" | |
| import hashlib | |
| import importlib.metadata | |
| import json | |
| import platform | |
| import shutil | |
| from pathlib import Path | |
| import sentencepiece as spm | |
| import torch | |
| from vimeml.training.data import file_sha | |
| from vimeml.training.infer import JapaneseLM, ROOT | |
| from vimeml.training.model import GPTConfig, TinyGPT | |
| FORMAT = "vimeml_inference_bundle_v1" | |
| CORE = ( | |
| "src/vimeml/training/model.py", "src/vimeml/training/data.py", | |
| "src/vimeml/training/train.py", "src/vimeml/training/infer.py", | |
| "src/vimeml/training/evaluate_ime.py", "src/vimeml/benchmarks/evaluate_ajimee.py", | |
| "src/vimeml/tools/phrase_demo.py", | |
| ) | |
| def write_json(path, value): | |
| Path(path).write_text(json.dumps(value, ensure_ascii=False, indent=2, allow_nan=False) + "\n", encoding="utf-8") | |
| def read_json(path): | |
| return json.loads(Path(path).read_text(encoding="utf-8-sig")) | |
| def fresh_directory(path): | |
| # Even empty preexisting directories are rejected; no overwrite switch. | |
| Path(path).mkdir(parents=True, exist_ok=False) | |
| def environment(): | |
| packages = {} | |
| for name in ("torch", "numpy", "sentencepiece", "coremltools"): | |
| try: | |
| packages[name] = importlib.metadata.version(name) | |
| except importlib.metadata.PackageNotFoundError: | |
| packages[name] = None | |
| return {"python": platform.python_version(), "platform": platform.platform(), | |
| "machine": platform.machine(), "packages": packages} | |
| def source_fingerprints(): | |
| paths = [*CORE, *(p.relative_to(ROOT).as_posix() for p in sorted((ROOT / "src/vimeml/deployment").glob("*.py")))] | |
| # Raw hashes preserve the original record; LF hashes allow Git CRLF/LF checkouts. | |
| return {name: {"sha256": file_sha(ROOT / name), "lf_sha256": hashlib.sha256( | |
| (ROOT / name).read_bytes().replace(b"\r\n", b"\n")).hexdigest()} for name in paths} | |
| def tree_inventory(path): | |
| path = Path(path) | |
| return {p.relative_to(path).as_posix(): {"bytes": p.stat().st_size, "sha256": file_sha(p)} | |
| for p in sorted(path.rglob("*")) if p.is_file()} | |
| def export_bundle(checkpoint, tokenizer, token_manifest, output): | |
| checkpoint, tokenizer, token_manifest, output = map(Path, (checkpoint, tokenizer, token_manifest, output)) | |
| if output.exists(): | |
| raise ValueError("Bundle output already exists; choose a new versioned directory.") | |
| saved = torch.load(checkpoint, map_location="cpu", weights_only=True) | |
| if saved.get("format") != "vimeml_tiny_gpt_v1": | |
| raise ValueError("Unsupported checkpoint.") | |
| if file_sha(token_manifest) != saved["signatures"]["tokens"]: | |
| raise ValueError("Token manifest is not bound to this checkpoint.") | |
| manifest = read_json(token_manifest) | |
| expected = [value for name, value in manifest["input_sha256"].items() | |
| if name.replace("\\", "/").rsplit("/", 1)[-1] == "tokenizer.model"] | |
| if expected != [file_sha(tokenizer)]: | |
| raise ValueError("Tokenizer does not match checkpoint.") | |
| fingerprints = source_fingerprints() | |
| for name, digest in saved["signatures"]["training_code"].items(): | |
| if file_sha(ROOT / "src/vimeml/training" / name) != digest: | |
| raise ValueError(f"Frozen training code fingerprint changed: {name}") | |
| processor = spm.SentencePieceProcessor(model_file=str(tokenizer)) | |
| special = {name: getattr(processor, f"{name}_id")() for name in ("pad", "unk", "bos", "eos")} | |
| config = GPTConfig(**saved["model_config"]) | |
| if special != manifest["special_ids"] or processor.vocab_size() != config.vocab_size: | |
| raise ValueError("Vocabulary/special IDs mismatch.") | |
| state = saved["model"] | |
| if not torch.equal(state["lm_head.weight"], state["token_embedding.weight"]): | |
| raise ValueError("Expected identical tied embedding and LM head.") | |
| model = TinyGPT(config).eval() | |
| model.load_state_dict(state, strict=True) | |
| if any(not torch.isfinite(p).all() for p in model.parameters()): | |
| raise ValueError("Nonfinite frozen weight.") | |
| # Keep only unique inference weights as FP32; TinyGPT re-establishes tying. | |
| weights = {name: tensor.detach().cpu().float().contiguous() for name, tensor in state.items() | |
| if name != "lm_head.weight"} | |
| fresh_directory(output) | |
| torch.save(weights, output / "weights.pt") | |
| write_json(output / "config.json", model.configuration()) | |
| shutil.copyfile(tokenizer, output / "tokenizer.model") | |
| shutil.copyfile(token_manifest, output / "token-manifest.json") | |
| result = {"format": FORMAT, "status": "complete", "parameter_count": model.parameter_count(), | |
| "precision": "fp32", "special_ids": special, "tied_lm_head": True, | |
| "checkpoint_sha256": file_sha(checkpoint), "checkpoint_step": saved["step"], | |
| "training_signatures": saved["signatures"], "source_fingerprints": fingerprints, | |
| "environment": environment(), "files": tree_inventory(output), | |
| "policy": "Weights only; no optimizer, RNG or training state. LM-only contextual suffix logP sum; no extra EOS; no KV cache."} | |
| write_json(output / "manifest.json", result) | |
| return result | |
| def verify_bundle(path, check_code=True): | |
| path = Path(path) | |
| manifest = read_json(path / "manifest.json") | |
| if manifest.get("format") != FORMAT or manifest.get("status") != "complete": | |
| raise ValueError("Unsupported/incomplete inference bundle.") | |
| if set(manifest["files"]) != {"weights.pt", "config.json", "tokenizer.model", "token-manifest.json"}: | |
| raise ValueError("Unexpected inference bundle file list.") | |
| for name, entry in manifest["files"].items(): | |
| if file_sha(path / name) != entry["sha256"] or (path / name).stat().st_size != entry["bytes"]: | |
| raise ValueError(f"Bundle checksum mismatch: {name}") | |
| if check_code: | |
| for name, entry in manifest["source_fingerprints"].items(): | |
| digest = hashlib.sha256((ROOT / name).read_bytes().replace(b"\r\n", b"\n")).hexdigest() | |
| if digest != entry["lf_sha256"]: | |
| raise ValueError(f"Bundle code changed: {name}; re-export as a new bundle.") | |
| return manifest | |
| class BundleLM(JapaneseLM): | |
| """Use frozen inference/scoring/search methods without a training checkpoint.""" | |
| def __init__(self, bundle): | |
| bundle = Path(bundle) | |
| manifest = verify_bundle(bundle) | |
| self.device = torch.device("cpu") | |
| self.processor = spm.SentencePieceProcessor(model_file=str(bundle / "tokenizer.model")) | |
| self.special = {name: getattr(self.processor, f"{name}_id")() for name in ("pad", "unk", "bos", "eos")} | |
| self.model = TinyGPT(GPTConfig(**read_json(bundle / "config.json"))).eval() | |
| if self.special != manifest["special_ids"] or self.processor.vocab_size() != self.model.config.vocab_size: | |
| raise ValueError("Bundle tokenizer contract mismatch.") | |
| state = torch.load(bundle / "weights.pt", map_location="cpu", weights_only=True) | |
| if "lm_head.weight" in state: | |
| raise ValueError("Bundle must store the shared embedding only once.") | |
| loaded = self.model.load_state_dict(state, strict=False) | |
| if loaded.missing_keys != ["lm_head.weight"] or loaded.unexpected_keys: | |
| raise ValueError(f"Unexpected inference state: {loaded}") | |
| if self.model.parameter_count() != manifest["parameter_count"]: | |
| raise ValueError("Parameter count mismatch.") | |
| self.forbidden = tuple(self.special[name] for name in ("pad", "unk", "bos")) | |
| self.metadata = {"checkpoint_sha256": manifest["checkpoint_sha256"], | |
| "tokenizer_sha256": manifest["files"]["tokenizer.model"]["sha256"], | |
| "bundle_manifest_sha256": file_sha(bundle / "manifest.json"), | |
| "model": self.model.configuration(), "precision": "fp32", "device": "cpu", | |
| "environment": environment()} | |