Download src/models/v2_inference.py from thenukegun10x/PlantDetect-V2-Preview: direct link, hf CLI and curl.
- Browser
- Download file 4.34 kB
-
https://huggingface.co/thenukegun10x/PlantDetect-V2-Preview/resolve/main/src/models/v2_inference.py
- Command line
-
hf download hf://thenukegun10x/PlantDetect-V2-Preview/src/models/v2_inference.py
-
curl -L -o v2_inference.py https://huggingface.co/thenukegun10x/PlantDetect-V2-Preview/resolve/main/src/models/v2_inference.py
4.34 kB
| """Portable eager inference for the released V2 mean-pooling model. | |
| The CLI used to ship a pair of TorchScript graphs (``encoder.ts`` / ``classifier.ts``). | |
| TorchScript is a *code* container - it stores serialised Python plus pickle that runs on | |
| load - so third-party model scanners flag it as potentially executable no matter what is | |
| inside (Protect AI Guardian, ``PAIT-TCHST-301``). This module rebuilds the same two halves | |
| from the code-free ``model.safetensors`` and ``config.json`` instead: | |
| * **encoder** - the DINOv3 + MoE trunk cast to bfloat16, exactly the dtype and weights the | |
| former ``encoder.ts`` carried, so the embeddings are bit-identical (verified at | |
| ``0.000e+00`` max-abs against the old graphs); | |
| * **classifier** - the fp32 Sub-Centre ArcFace and taxonomic heads. | |
| The public surface mirrors the old graphs so callers only swap ``torch.jit.load`` for | |
| :func:`load_bundle`. | |
| Built with DINOv3. | |
| """ | |
| from __future__ import annotations | |
| import copy | |
| import json | |
| from pathlib import Path | |
| import torch | |
| from torch import nn | |
| from .plant_unified import PlantUnified | |
| class _Encoder(nn.Module): | |
| """The DINOv3 + MoE trunk and feature fusion, without the training-only aux losses. | |
| Mirrors the module the removed ``encoder.ts`` wrapped: it runs the stem, one MoE layer | |
| and the fusion, and skips the load-balance statistics the training forward computes | |
| (they do not affect the embedding). Keeping the op set identical to the old graph also | |
| keeps the cost identical rather than paying for eager-only bookkeeping. | |
| """ | |
| def __init__(self, model: PlantUnified): | |
| super().__init__() | |
| self.stem = model.stem | |
| self.norm = model.moe_norms[0] | |
| self.moe = model.moe_layers[0] | |
| self.fusion = model.fusion | |
| self.n_prefix = model.n_prefix | |
| def forward(self, image: torch.Tensor) -> torch.Tensor: | |
| maps = self.stem.forward_intermediates( | |
| image, indices=[7, 11], output_fmt="NLC", norm=True, | |
| return_prefix_tokens=True, intermediates_only=True) | |
| mid, deep = [torch.cat((prefix, spatial), dim=1) for spatial, prefix in maps] | |
| routed = self.moe(self.norm(deep), ctx=deep[:, 0])[0] | |
| deep = deep + routed | |
| return self.fusion(mid[:, self.n_prefix:], deep[:, self.n_prefix:]).float() | |
| class V2Bundle: | |
| """The encoder/classifier pair the CLI and harness scripts call. | |
| ``encoder`` takes the same bfloat16 image batch the graphs took and returns a float32 | |
| embedding; ``classifier`` takes a float32 embedding and returns the | |
| ``(species, genus, family)`` logits. Keeping the exact signatures means the call sites | |
| do not change. | |
| """ | |
| def __init__(self, model_fp32: PlantUnified, encoder_fp32: PlantUnified): | |
| self.model = model_fp32 | |
| # bf16 encoder: the encoder graph stored bf16 weights, so this reproduces it | |
| # bit-for-bit while the fp32 model keeps the precision-sensitive ArcFace head. | |
| self._encoder = _Encoder(encoder_fp32).to(torch.bfloat16).eval() | |
| def encoder(self, images: torch.Tensor) -> torch.Tensor: | |
| return self._encoder(images) | |
| def classifier(self, embedding: torch.Tensor): | |
| logits = self.model.classify_embeddings(embedding) | |
| return logits["species"], logits["genus"], logits["family"] | |
| def build_model(config: dict) -> PlantUnified: | |
| """Instantiate the architecture from a released ``config.json`` (no pretrained load).""" | |
| model_config = dict(config["model_config"]) | |
| model_config.pop("pretrained", None) | |
| return PlantUnified(pretrained=False, **model_config) | |
| def load_bundle(directory, device="cpu") -> V2Bundle: | |
| """Build the inference bundle from a release directory containing ``config.json``. | |
| ``model.safetensors`` is loaded with ``strict=True``; the shared ``moe_layer`` alias in | |
| the released file is resolved by ``safetensors.torch.load_model``. | |
| """ | |
| from safetensors.torch import load_model | |
| directory = Path(directory) | |
| config = json.loads((directory / "config.json").read_text(encoding="utf-8")) | |
| model = build_model(config) | |
| load_model(model, str(directory / "model.safetensors"), strict=True, device=str(device)) | |
| model = model.to(device).eval() | |
| return V2Bundle(model, copy.deepcopy(model)) | |