Instructions to use FluidInference/gliner2-5-small-coreml with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- GLiNER2
How to use FluidInference/gliner2-5-small-coreml with GLiNER2:
from gliner2 import GLiNER2 model = GLiNER2.from_pretrained("FluidInference/gliner2-5-small-coreml") # Extract entities text = "Apple CEO Tim Cook announced iPhone 15 in Cupertino yesterday." result = extractor.extract_entities(text, ["company", "person", "product", "location"]) print(result) - Notebooks
- Google Colab
- Kaggle
Publish GLiNER2.5 small Core ML classification exports
Browse files- README.md +62 -0
- assets.lock.json +26 -0
- compress-coreml.py +46 -0
- conversion.json +37 -0
- convert-coreml.py +131 -0
- export_model.py +47 -0
- gliner2_small_classification_fp16_L128_K8.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- gliner2_small_classification_fp16_L128_K8.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- gliner2_small_classification_fp16_L128_K8.mlpackage/Manifest.json +18 -0
- gliner2_small_classification_lut8_L128_K8.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- gliner2_small_classification_lut8_L128_K8.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- gliner2_small_classification_lut8_L128_K8.mlpackage/Manifest.json +18 -0
- preprocessing.py +43 -0
- pyproject.toml +29 -0
- runtime.py +37 -0
- tokenizer/tokenizer.json +0 -0
- tokenizer/tokenizer_config.json +31 -0
- uv.lock +0 -0
- verify-application100.json +18 -0
- verify-lut8-application100.json +18 -0
- verify.py +83 -0
README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
library_name: coremltools
|
| 4 |
+
pipeline_tag: text-classification
|
| 5 |
+
tags:
|
| 6 |
+
- coreml
|
| 7 |
+
- gliner2
|
| 8 |
+
- apple-silicon
|
| 9 |
+
- neural-engine
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# GLiNER2.5 small Core ML classification
|
| 13 |
+
|
| 14 |
+
This is a Core ML conversion of the **classification decision path** of
|
| 15 |
+
[Fastino's GLiNER2.5 small](https://huggingface.co/fastino/gliner2.5-small-v1),
|
| 16 |
+
source revision `7e6f537f10337497069276892a5ef435028252ce`.
|
| 17 |
+
The original model has 73,881,879 parameters and is licensed Apache-2.0.
|
| 18 |
+
This export contains its encoder and trained classification head (70,944,385
|
| 19 |
+
parameters); entity, relation, record, count and span extraction heads are **not**
|
| 20 |
+
included. Use the original checkpoint for those tasks.
|
| 21 |
+
|
| 22 |
+
The FP16 L128/K8 package occupies 151,542,752 bytes. It accepts up to eight
|
| 23 |
+
classification labels, with native GLiNER2 schema rendering. The tokenizer
|
| 24 |
+
files are included in this repository. The Core ML deployment target is iOS 17
|
| 25 |
+
or macOS 14. `preprocessing.py` and `runtime.py` implement the request path
|
| 26 |
+
without loading the original model weights at inference time.
|
| 27 |
+
|
| 28 |
+
A Python example, from the directory containing this README:
|
| 29 |
+
|
| 30 |
+
```bash
|
| 31 |
+
uv sync
|
| 32 |
+
uv run python runtime.py --model-dir . \
|
| 33 |
+
--text "The rocket launched successfully." \
|
| 34 |
+
--task topic --labels '["science","sports","politics"]'
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
The model returns a label, confidence and probabilities. Inputs that exceed the
|
| 38 |
+
bucket capacity need a larger bucket; the runtime must not truncate them. The
|
| 39 |
+
runtime requires macOS to execute Core ML prediction.
|
| 40 |
+
|
| 41 |
+
## Validation
|
| 42 |
+
|
| 43 |
+
On an Apple M5 Pro running macOS 27.0, the FP16 package matched the native
|
| 44 |
+
classifier's chosen label on all 100 eligible requests selected in source order
|
| 45 |
+
from a fixed application suite. The largest chosen-label confidence difference
|
| 46 |
+
was 0.001723. Median Python `MLModel.predict` call time was 7.72 ms, including
|
| 47 |
+
Python/Core ML dispatch; this is not an ANE-only measure. There were 300 rows
|
| 48 |
+
with more than eight options and seven over-length rows before 100 eligible rows
|
| 49 |
+
were collected. The smoke run does not establish a Decision Index score or
|
| 50 |
+
broader application accuracy. `verify-application100.json` contains the exact
|
| 51 |
+
counts and results.
|
| 52 |
+
|
| 53 |
+
An experimental LUT8 per-tensor package occupies 76,211,068 bytes and matched
|
| 54 |
+
all 100 chosen labels, but its largest confidence difference was 0.07859.
|
| 55 |
+
It is not the recommended parity artifact. Grouped-channel LUT8 requires an
|
| 56 |
+
iOS 18 or later Core ML target and has not yet been evaluated.
|
| 57 |
+
|
| 58 |
+
Conversion source, pinned dependencies and verification scripts are included.
|
| 59 |
+
The exporter freezes two static DeBERTa attention expressions and uses a finite
|
| 60 |
+
FP16 mask sentinel; the real-model tests and native/Core ML checks guard this
|
| 61 |
+
rewrite. Upstream authors receive credit for the original model; Fluid
|
| 62 |
+
Inference performed this Core ML conversion.
|
assets.lock.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"source": "fastino/gliner2.5-small-v1",
|
| 3 |
+
"revision": "7e6f537f10337497069276892a5ef435028252ce",
|
| 4 |
+
"files": {
|
| 5 |
+
"config.json": {
|
| 6 |
+
"size": 3152,
|
| 7 |
+
"sha256": "0b7d9e1401ceeb83e992ec66d2f93bff7e5646428f1b4706ec527cf88f53578a"
|
| 8 |
+
},
|
| 9 |
+
"encoder_config/config.json": {
|
| 10 |
+
"size": 856,
|
| 11 |
+
"sha256": "db837d0dc587f5858687ef860c1f400de10f3c3e44f88daef8cbda80d74e4c9c"
|
| 12 |
+
},
|
| 13 |
+
"model.safetensors": {
|
| 14 |
+
"size": 295567700,
|
| 15 |
+
"sha256": "4ee982787ace270d4bf15dbcb28ced38e0aa201372347114ceedd6336055de2b"
|
| 16 |
+
},
|
| 17 |
+
"tokenizer.json": {
|
| 18 |
+
"size": 8341713,
|
| 19 |
+
"sha256": "cbc8ae6037812709c9c26f2a160f8dc48b0440bcb79c8141804259ae2d6adac3"
|
| 20 |
+
},
|
| 21 |
+
"tokenizer_config.json": {
|
| 22 |
+
"size": 645,
|
| 23 |
+
"sha256": "0bf3ea0873234bd9bfdd3853c440395009ac6365a925b91654daed5396d655e1"
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
}
|
compress-coreml.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Palettize a verified GLiNER2 classification Core ML model."""
|
| 2 |
+
import argparse
|
| 3 |
+
import json
|
| 4 |
+
import time
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import coremltools as ct
|
| 8 |
+
from coremltools.optimize.coreml import OpPalettizerConfig, OptimizationConfig, palettize_weights
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def package_bytes(path):
|
| 12 |
+
return sum(item.stat().st_size for item in path.rglob("*") if item.is_file())
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def main():
|
| 16 |
+
parser = argparse.ArgumentParser()
|
| 17 |
+
parser.add_argument("--source", required=True)
|
| 18 |
+
parser.add_argument("--output", required=True)
|
| 19 |
+
parser.add_argument("--bits", type=int, choices=[4, 6, 8], default=8)
|
| 20 |
+
parser.add_argument("--granularity", choices=["per_tensor", "per_grouped_channel"], default="per_tensor")
|
| 21 |
+
parser.add_argument("--group-size", type=int, default=32)
|
| 22 |
+
args = parser.parse_args()
|
| 23 |
+
start = time.perf_counter()
|
| 24 |
+
model = ct.models.MLModel(args.source, skip_model_load=True)
|
| 25 |
+
config = OptimizationConfig(global_config=OpPalettizerConfig(
|
| 26 |
+
mode="kmeans", nbits=args.bits, granularity=args.granularity,
|
| 27 |
+
group_size=args.group_size,
|
| 28 |
+
enable_per_channel_scale=args.granularity == "per_grouped_channel",
|
| 29 |
+
num_kmeans_workers=4,
|
| 30 |
+
))
|
| 31 |
+
compressed = palettize_weights(model, config=config)
|
| 32 |
+
compressed.short_description = f"{model.short_description}; LUT{args.bits} weights"
|
| 33 |
+
compressed.author = model.author
|
| 34 |
+
compressed.license = model.license
|
| 35 |
+
compressed.user_defined_metadata.update(model.user_defined_metadata)
|
| 36 |
+
compressed.user_defined_metadata["weight_compression"] = f"LUT{args.bits} kmeans"
|
| 37 |
+
compressed.save(args.output)
|
| 38 |
+
report = {"source": args.source, "output": args.output, "granularity": args.granularity,
|
| 39 |
+
"source_bytes": package_bytes(Path(args.source)),
|
| 40 |
+
"output_bytes": package_bytes(Path(args.output)), "compression_seconds": time.perf_counter() - start}
|
| 41 |
+
Path(args.output).with_suffix(".json").write_text(json.dumps(report, indent=2) + "\n")
|
| 42 |
+
print(json.dumps(report, indent=2))
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
if __name__ == "__main__":
|
| 46 |
+
main()
|
conversion.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"source_model": "fastino/gliner2.5-small-v1",
|
| 3 |
+
"source_revision": "7e6f537f10337497069276892a5ef435028252ce",
|
| 4 |
+
"package": "build/gliner2_small_classification_fp16_L128_K8.mlpackage",
|
| 5 |
+
"package_bytes": 151542752,
|
| 6 |
+
"native_total_parameters": 73881879,
|
| 7 |
+
"exported_parameters": 70944385,
|
| 8 |
+
"wrapper_max_logit_error": 4.76837158203125e-07,
|
| 9 |
+
"coremltools": "9.0",
|
| 10 |
+
"torch": "2.7.0",
|
| 11 |
+
"cases": [
|
| 12 |
+
{
|
| 13 |
+
"text": "The rocket launched successfully.",
|
| 14 |
+
"native_label": "science",
|
| 15 |
+
"coreml_label": "science",
|
| 16 |
+
"native_confidence": 0.9989821314811707,
|
| 17 |
+
"coreml_confidence": 0.9989795088768005,
|
| 18 |
+
"absolute_confidence_error": 2.6226043701171875e-06
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"text": "The team won the football championship.",
|
| 22 |
+
"native_label": "sports",
|
| 23 |
+
"coreml_label": "sports",
|
| 24 |
+
"native_confidence": 0.9999417066574097,
|
| 25 |
+
"coreml_confidence": 0.9999417066574097,
|
| 26 |
+
"absolute_confidence_error": 0.0
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"text": "The budget was approved by parliament.",
|
| 30 |
+
"native_label": "politics",
|
| 31 |
+
"coreml_label": "politics",
|
| 32 |
+
"native_confidence": 0.9999653100967407,
|
| 33 |
+
"coreml_confidence": 0.9999654293060303,
|
| 34 |
+
"absolute_confidence_error": 1.1920928955078125e-07
|
| 35 |
+
}
|
| 36 |
+
]
|
| 37 |
+
}
|
convert-coreml.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Convert and verify the pinned GLiNER2.5-small classification decision path."""
|
| 2 |
+
import argparse
|
| 3 |
+
import json
|
| 4 |
+
import math
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import coremltools as ct
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
from gliner2 import AutoExtractor
|
| 11 |
+
from huggingface_hub import snapshot_download
|
| 12 |
+
from transformers.models.deberta_v2 import modeling_deberta_v2
|
| 13 |
+
|
| 14 |
+
from export_model import GLiNER2ClassificationExport, coreml_safe_attention_forward
|
| 15 |
+
from preprocessing import native_batch, prepare_classification
|
| 16 |
+
|
| 17 |
+
MODEL_ID = "fastino/gliner2.5-small-v1"
|
| 18 |
+
MODEL_REVISION = "7e6f537f10337497069276892a5ef435028252ce"
|
| 19 |
+
EXAMPLES = [
|
| 20 |
+
("The rocket launched successfully.", "topic", ["science", "sports", "politics"]),
|
| 21 |
+
("The team won the football championship.", "topic", ["science", "sports", "politics"]),
|
| 22 |
+
("The budget was approved by parliament.", "topic", ["science", "sports", "politics"]),
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
def main():
|
| 26 |
+
parser = argparse.ArgumentParser()
|
| 27 |
+
parser.add_argument("--output-dir", default="build")
|
| 28 |
+
parser.add_argument("--length", type=int, default=128)
|
| 29 |
+
parser.add_argument("--max-options", type=int, default=8)
|
| 30 |
+
parser.add_argument("--precision", choices=["fp16", "fp32"], default="fp16")
|
| 31 |
+
args = parser.parse_args()
|
| 32 |
+
torch.set_num_threads(4)
|
| 33 |
+
source = snapshot_download(
|
| 34 |
+
MODEL_ID, revision=MODEL_REVISION,
|
| 35 |
+
allow_patterns=[
|
| 36 |
+
"config.json", "encoder_config/*", "model.safetensors", "tokenizer.json", "tokenizer_config.json"
|
| 37 |
+
],
|
| 38 |
+
)
|
| 39 |
+
native = AutoExtractor.from_pretrained(source, map_location="cpu").eval()
|
| 40 |
+
wrapper = GLiNER2ClassificationExport(native).eval()
|
| 41 |
+
text, task, labels = EXAMPLES[0]
|
| 42 |
+
arrays = prepare_classification(native, text, task, labels, args.length, args.max_options)
|
| 43 |
+
tensors = tuple(torch.from_numpy(value) for value in arrays.values())
|
| 44 |
+
with torch.no_grad():
|
| 45 |
+
batch = native_batch(native, text, task, labels, args.length)
|
| 46 |
+
core = native._encode_core(batch)
|
| 47 |
+
expected = native.classifier(core["cls_specs"][0][0]["choice_states"]).squeeze(-1)
|
| 48 |
+
actual = wrapper(*tensors)[0][0, : len(labels)]
|
| 49 |
+
wrapper_error = float((expected - actual).abs().max())
|
| 50 |
+
if wrapper_error > 1e-4:
|
| 51 |
+
raise RuntimeError(f"Wrapper/native logit mismatch: {wrapper_error}")
|
| 52 |
+
# The upstream scale is a constant for a fixed DeBERTa attention head width.
|
| 53 |
+
# Its traced int32 sqrt is rejected by Core ML; freeze the identical float32
|
| 54 |
+
# value while tracing, and restore the upstream implementation immediately.
|
| 55 |
+
original_scale = modeling_deberta_v2.scaled_size_sqrt
|
| 56 |
+
original_rpos = modeling_deberta_v2.build_rpos
|
| 57 |
+
original_attention = modeling_deberta_v2.DisentangledSelfAttention.forward
|
| 58 |
+
|
| 59 |
+
def static_scale(query_layer, scale_factor):
|
| 60 |
+
value = math.sqrt(float(query_layer.shape[-1] * scale_factor))
|
| 61 |
+
return torch.tensor(value, dtype=torch.float32, device=query_layer.device)
|
| 62 |
+
|
| 63 |
+
modeling_deberta_v2.scaled_size_sqrt = static_scale
|
| 64 |
+
# The encoder only uses self-attention: query and key sequence lengths are
|
| 65 |
+
# identical, so the scripted build_rpos returns relative_pos unchanged.
|
| 66 |
+
# Freeze that branch to avoid a Core ML conditional with mismatched ranks.
|
| 67 |
+
modeling_deberta_v2.build_rpos = lambda query, key, relative_pos, buckets, max_pos: relative_pos
|
| 68 |
+
modeling_deberta_v2.DisentangledSelfAttention.forward = coreml_safe_attention_forward
|
| 69 |
+
try:
|
| 70 |
+
with torch.no_grad():
|
| 71 |
+
frozen = wrapper(*tensors)[0][0, : len(labels)]
|
| 72 |
+
frozen_error = float((expected - frozen).abs().max())
|
| 73 |
+
if frozen_error > 1e-4:
|
| 74 |
+
raise RuntimeError(f"Frozen attention scale changed native logits: {frozen_error}")
|
| 75 |
+
traced = torch.jit.trace(wrapper, tensors, check_trace=False)
|
| 76 |
+
finally:
|
| 77 |
+
modeling_deberta_v2.scaled_size_sqrt = original_scale
|
| 78 |
+
modeling_deberta_v2.build_rpos = original_rpos
|
| 79 |
+
modeling_deberta_v2.DisentangledSelfAttention.forward = original_attention
|
| 80 |
+
converted = ct.convert(
|
| 81 |
+
traced, convert_to="mlprogram", minimum_deployment_target=ct.target.iOS17,
|
| 82 |
+
compute_precision=ct.precision.FLOAT16 if args.precision == "fp16" else ct.precision.FLOAT32,
|
| 83 |
+
compute_units=ct.ComputeUnit.CPU_ONLY,
|
| 84 |
+
inputs=[
|
| 85 |
+
ct.TensorType(name="input_ids", shape=(1, args.length), dtype=np.int32),
|
| 86 |
+
ct.TensorType(name="attention_mask", shape=(1, args.length), dtype=np.int32),
|
| 87 |
+
ct.TensorType(name="marker_indices", shape=(1, args.max_options), dtype=np.int32),
|
| 88 |
+
ct.TensorType(name="marker_mask", shape=(1, args.max_options), dtype=np.float32),
|
| 89 |
+
],
|
| 90 |
+
outputs=[ct.TensorType(name="logits", dtype=np.float32), ct.TensorType(name="probabilities", dtype=np.float32)],
|
| 91 |
+
)
|
| 92 |
+
converted.short_description = "GLiNER2.5-small native schema classification path"
|
| 93 |
+
converted.author = "Fastino (original); Fluid Inference (Core ML conversion)"
|
| 94 |
+
converted.license = "Apache-2.0"
|
| 95 |
+
converted.user_defined_metadata.update({
|
| 96 |
+
"source_model": MODEL_ID, "source_revision": MODEL_REVISION,
|
| 97 |
+
"scope": "classification only; entity/relation/record extraction heads not exported",
|
| 98 |
+
"length": str(args.length), "max_options": str(args.max_options),
|
| 99 |
+
})
|
| 100 |
+
out = Path(args.output_dir)
|
| 101 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 102 |
+
package = out / f"gliner2_small_classification_{args.precision}_L{args.length}_K{args.max_options}.mlpackage"
|
| 103 |
+
converted.save(str(package))
|
| 104 |
+
runtime = ct.models.MLModel(str(package), compute_units=ct.ComputeUnit.ALL)
|
| 105 |
+
cases = []
|
| 106 |
+
for text, task, labels in EXAMPLES:
|
| 107 |
+
arrays = prepare_classification(native, text, task, labels, args.length, args.max_options)
|
| 108 |
+
native_output = native.classify_text(text, {task: labels}, include_confidence=True, max_len=args.length)[task]
|
| 109 |
+
prediction = runtime.predict(arrays)
|
| 110 |
+
scores = np.asarray(prediction["probabilities"])[0, : len(labels)]
|
| 111 |
+
choice = labels[int(scores.argmax())]
|
| 112 |
+
if choice != native_output["label"]:
|
| 113 |
+
raise RuntimeError(f"Core ML/native choice mismatch: {choice} != {native_output['label']}")
|
| 114 |
+
cases.append({
|
| 115 |
+
"text": text, "native_label": native_output["label"], "coreml_label": choice,
|
| 116 |
+
"native_confidence": native_output["confidence"], "coreml_confidence": float(scores.max()),
|
| 117 |
+
"absolute_confidence_error": abs(float(scores.max()) - native_output["confidence"]),
|
| 118 |
+
})
|
| 119 |
+
report = {
|
| 120 |
+
"source_model": MODEL_ID, "source_revision": MODEL_REVISION, "package": str(package),
|
| 121 |
+
"package_bytes": sum(f.stat().st_size for f in package.rglob("*") if f.is_file()),
|
| 122 |
+
"native_total_parameters": sum(p.numel() for p in native.parameters()),
|
| 123 |
+
"exported_parameters": sum(p.numel() for p in wrapper.parameters()),
|
| 124 |
+
"wrapper_max_logit_error": wrapper_error, "coremltools": ct.__version__,
|
| 125 |
+
"torch": torch.__version__, "cases": cases,
|
| 126 |
+
}
|
| 127 |
+
(out / "conversion.json").write_text(json.dumps(report, indent=2) + "\n")
|
| 128 |
+
print(json.dumps(report, indent=2))
|
| 129 |
+
|
| 130 |
+
if __name__ == "__main__":
|
| 131 |
+
main()
|
export_model.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Native GLiNER2 classification path with explicit marker routing."""
|
| 2 |
+
import torch
|
| 3 |
+
from torch import nn
|
| 4 |
+
from transformers.models.deberta_v2 import modeling_deberta_v2
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def coreml_safe_attention_forward(
|
| 8 |
+
self, hidden_states, attention_mask, output_attentions=False,
|
| 9 |
+
query_states=None, relative_pos=None, rel_embeddings=None,
|
| 10 |
+
):
|
| 11 |
+
"""Native DeBERTa attention with a finite mask sentinel for FP16 Core ML."""
|
| 12 |
+
if query_states is None:
|
| 13 |
+
query_states = hidden_states
|
| 14 |
+
query = self.transpose_for_scores(self.query_proj(query_states), self.num_attention_heads)
|
| 15 |
+
key = self.transpose_for_scores(self.key_proj(hidden_states), self.num_attention_heads)
|
| 16 |
+
value = self.transpose_for_scores(self.value_proj(hidden_states), self.num_attention_heads)
|
| 17 |
+
factor = 1 + int("c2p" in self.pos_att_type) + int("p2c" in self.pos_att_type)
|
| 18 |
+
scale = modeling_deberta_v2.scaled_size_sqrt(query, factor)
|
| 19 |
+
scores = torch.bmm(query, key.transpose(-1, -2) / scale.to(dtype=query.dtype))
|
| 20 |
+
if self.relative_attention:
|
| 21 |
+
relative = self.disentangled_attention_bias(
|
| 22 |
+
query, key, relative_pos, self.pos_dropout(rel_embeddings), factor
|
| 23 |
+
)
|
| 24 |
+
scores = scores + relative
|
| 25 |
+
scores = scores.view(-1, self.num_attention_heads, scores.size(-2), scores.size(-1))
|
| 26 |
+
scores = scores.masked_fill(~attention_mask.bool(), -1e4)
|
| 27 |
+
probabilities = self.dropout(torch.softmax(scores, dim=-1))
|
| 28 |
+
context = torch.bmm(probabilities.view(-1, probabilities.size(-2), probabilities.size(-1)), value)
|
| 29 |
+
context = context.view(-1, self.num_attention_heads, context.size(-2), context.size(-1))
|
| 30 |
+
context = context.permute(0, 2, 1, 3).contiguous()
|
| 31 |
+
context = context.view(context.size()[:-2] + (-1,))
|
| 32 |
+
return (context, probabilities) if output_attentions else (context, None)
|
| 33 |
+
|
| 34 |
+
class GLiNER2ClassificationExport(nn.Module):
|
| 35 |
+
def __init__(self, native: nn.Module):
|
| 36 |
+
super().__init__()
|
| 37 |
+
self.encoder = native.encoder
|
| 38 |
+
self.classifier = native.classifier
|
| 39 |
+
self.temperature = float(native.boundary_settings.classification_temperature)
|
| 40 |
+
|
| 41 |
+
def forward(self, input_ids, attention_mask, marker_indices, marker_mask):
|
| 42 |
+
hidden = self.encoder(input_ids=input_ids.long(), attention_mask=attention_mask.long()).last_hidden_state
|
| 43 |
+
indices = marker_indices.long().unsqueeze(-1).expand(-1, -1, hidden.shape[-1])
|
| 44 |
+
states = hidden.gather(1, indices)
|
| 45 |
+
logits = self.classifier(states).squeeze(-1) / self.temperature
|
| 46 |
+
logits = torch.where(marker_mask > 0.5, logits, torch.full_like(logits, -1e4))
|
| 47 |
+
return logits, torch.softmax(logits, dim=-1)
|
gliner2_small_classification_fp16_L128_K8.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c544ebc230a3515195b40556f4e9f625c88dd29e0ca5f8f23a127219e2d26588
|
| 3 |
+
size 596663
|
gliner2_small_classification_fp16_L128_K8.mlpackage/Data/com.apple.CoreML/weights/weight.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:91200fe39fdcabfc341288e560de9786df102ba519bcbcd3271320017abb2cd6
|
| 3 |
+
size 150945472
|
gliner2_small_classification_fp16_L128_K8.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"0BDBE49B-B06F-49EC-8702-7009DEAF64A4": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Weights",
|
| 7 |
+
"name": "weights",
|
| 8 |
+
"path": "com.apple.CoreML/weights"
|
| 9 |
+
},
|
| 10 |
+
"FBBC8447-2EAA-4114-BB90-A1ED959442C0": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Specification",
|
| 13 |
+
"name": "model.mlmodel",
|
| 14 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "FBBC8447-2EAA-4114-BB90-A1ED959442C0"
|
| 18 |
+
}
|
gliner2_small_classification_lut8_L128_K8.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1db07dbf4e21947966373ec7df9f9b315cb711a02445c3f72e7ff38ff11b137b
|
| 3 |
+
size 611923
|
gliner2_small_classification_lut8_L128_K8.mlpackage/Data/com.apple.CoreML/weights/weight.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:361194f5c5f530fc0e6f20705f7340f5e1ad6c5a451b5d3fc4d46364a6abbe00
|
| 3 |
+
size 75598528
|
gliner2_small_classification_lut8_L128_K8.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"1F7B203F-B038-49C4-9982-5CEBB5449266": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Weights",
|
| 7 |
+
"name": "weights",
|
| 8 |
+
"path": "com.apple.CoreML/weights"
|
| 9 |
+
},
|
| 10 |
+
"4CB15B68-E62E-4344-86BA-955F68BADB51": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Specification",
|
| 13 |
+
"name": "model.mlmodel",
|
| 14 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "4CB15B68-E62E-4344-86BA-955F68BADB51"
|
| 18 |
+
}
|
preprocessing.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Native GLiNER2 schema preprocessing for a fixed Core ML bucket."""
|
| 2 |
+
import numpy as np
|
| 3 |
+
from gliner2 import Schema
|
| 4 |
+
from gliner2.models.base import load_extractor_tokenizer
|
| 5 |
+
from gliner2.processor import SchemaTransformer
|
| 6 |
+
from gliner2.training.trainer import ExtractorCollator
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def load_processor(tokenizer_dir: str):
|
| 10 |
+
"""Load only the tokenizer and schema formatter needed by the Core ML model."""
|
| 11 |
+
return SchemaTransformer(tokenizer=load_extractor_tokenizer(tokenizer_dir), token_pooling="first")
|
| 12 |
+
|
| 13 |
+
def native_batch(native, text: str, task: str, labels: list[str], length: int):
|
| 14 |
+
schema = Schema().classification(task, labels)
|
| 15 |
+
collator = ExtractorCollator(native.processor, is_training=False, max_len=length, architecture=native.architecture)
|
| 16 |
+
return collator([(text, schema.build())])
|
| 17 |
+
|
| 18 |
+
def prepare_classification(native, text: str, task: str, labels: list[str], length: int, max_options: int):
|
| 19 |
+
return prepare_with_processor(native.processor, text, task, labels, length, max_options)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def prepare_with_processor(processor, text: str, task: str, labels: list[str], length: int, max_options: int):
|
| 23 |
+
if not 1 <= len(labels) <= max_options:
|
| 24 |
+
raise ValueError(f"Expected 1..{max_options} labels, got {len(labels)}")
|
| 25 |
+
schema = Schema().classification(task, labels)
|
| 26 |
+
collator = ExtractorCollator(processor, is_training=False, max_len=length, architecture="boundary")
|
| 27 |
+
batch = collator([(text, schema.build())])
|
| 28 |
+
ids = batch.input_ids.numpy()
|
| 29 |
+
attention = batch.attention_mask.numpy()
|
| 30 |
+
indices = batch.cls_marker_indices.numpy()
|
| 31 |
+
mask = batch.cls_marker_mask.numpy()
|
| 32 |
+
if ids.shape[1] > length or indices.shape[1] != len(labels) or int(mask.sum()) != len(labels):
|
| 33 |
+
raise ValueError("Input exceeds bucket or classification markers were truncated")
|
| 34 |
+
ids = np.pad(ids, ((0, 0), (0, length - ids.shape[1])), constant_values=processor.tokenizer.pad_token_id)
|
| 35 |
+
attention = np.pad(attention, ((0, 0), (0, length - attention.shape[1])))
|
| 36 |
+
indices = np.pad(indices, ((0, 0), (0, max_options - indices.shape[1])))
|
| 37 |
+
mask = np.pad(mask, ((0, 0), (0, max_options - mask.shape[1])))
|
| 38 |
+
return {
|
| 39 |
+
"input_ids": ids.astype(np.int32),
|
| 40 |
+
"attention_mask": attention.astype(np.int32),
|
| 41 |
+
"marker_indices": indices.astype(np.int32),
|
| 42 |
+
"marker_mask": mask.astype(np.float32),
|
| 43 |
+
}
|
pyproject.toml
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "gliner2-small-coreml"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Pinned GLiNER 2.5 small decision classifier Core ML export"
|
| 5 |
+
requires-python = ">=3.12,<3.13"
|
| 6 |
+
dependencies = [
|
| 7 |
+
"coremltools==9.0",
|
| 8 |
+
"gliner2[local]==2.0.0",
|
| 9 |
+
"huggingface-hub>=0.34,<1",
|
| 10 |
+
"numpy<2.3",
|
| 11 |
+
"protobuf>=5,<7",
|
| 12 |
+
"sentencepiece>=0.2,<0.3",
|
| 13 |
+
"torch==2.7.0",
|
| 14 |
+
"transformers==4.57.6",
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
[dependency-groups]
|
| 18 |
+
dev = ["pytest>=8.4", "ruff>=0.13"]
|
| 19 |
+
|
| 20 |
+
[tool.pytest.ini_options]
|
| 21 |
+
testpaths = ["tests"]
|
| 22 |
+
pythonpath = ["."]
|
| 23 |
+
|
| 24 |
+
[tool.ruff]
|
| 25 |
+
line-length = 120
|
| 26 |
+
target-version = "py312"
|
| 27 |
+
|
| 28 |
+
[tool.ruff.lint]
|
| 29 |
+
select = ["E", "F", "I"]
|
runtime.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Run a GLiNER2 Core ML classifier without loading the original model weights."""
|
| 2 |
+
import argparse
|
| 3 |
+
import json
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
import coremltools as ct
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
from preprocessing import load_processor, prepare_with_processor
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def classify(model_dir: str, text: str, task: str, labels: list[str], length: int = 128, max_options: int = 8):
|
| 13 |
+
model_dir = Path(model_dir)
|
| 14 |
+
package = model_dir / f"gliner2_small_classification_fp16_L{length}_K{max_options}.mlpackage"
|
| 15 |
+
processor = load_processor(str(model_dir / "tokenizer"))
|
| 16 |
+
arrays = prepare_with_processor(processor, text, task, labels, length, max_options)
|
| 17 |
+
model = ct.models.MLModel(str(package), compute_units=ct.ComputeUnit.ALL)
|
| 18 |
+
scores = np.asarray(model.predict(arrays)["probabilities"])[0, : len(labels)]
|
| 19 |
+
return {"label": labels[int(scores.argmax())], "confidence": float(scores.max()),
|
| 20 |
+
"probabilities": {label: float(score) for label, score in zip(labels, scores)}}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def main():
|
| 24 |
+
parser = argparse.ArgumentParser()
|
| 25 |
+
parser.add_argument("--model-dir", required=True)
|
| 26 |
+
parser.add_argument("--text", required=True)
|
| 27 |
+
parser.add_argument("--task", default="decision")
|
| 28 |
+
parser.add_argument("--labels", required=True, help="JSON list of label strings")
|
| 29 |
+
parser.add_argument("--length", type=int, default=128)
|
| 30 |
+
parser.add_argument("--max-options", type=int, default=8)
|
| 31 |
+
args = parser.parse_args()
|
| 32 |
+
print(json.dumps(classify(args.model_dir, args.text, args.task, json.loads(args.labels),
|
| 33 |
+
args.length, args.max_options), indent=2))
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
if __name__ == "__main__":
|
| 37 |
+
main()
|
tokenizer/tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tokenizer/tokenizer_config.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_prefix_space": true,
|
| 3 |
+
"backend": "tokenizers",
|
| 4 |
+
"bos_token": "[CLS]",
|
| 5 |
+
"cls_token": "[CLS]",
|
| 6 |
+
"do_lower_case": false,
|
| 7 |
+
"eos_token": "[SEP]",
|
| 8 |
+
"extra_special_tokens": [
|
| 9 |
+
"[SEP_STRUCT]",
|
| 10 |
+
"[SEP_TEXT]",
|
| 11 |
+
"[P]",
|
| 12 |
+
"[C]",
|
| 13 |
+
"[E]",
|
| 14 |
+
"[R]",
|
| 15 |
+
"[L]",
|
| 16 |
+
"[EXAMPLE]",
|
| 17 |
+
"[OUTPUT]",
|
| 18 |
+
"[DESCRIPTION]"
|
| 19 |
+
],
|
| 20 |
+
"is_local": true,
|
| 21 |
+
"local_files_only": false,
|
| 22 |
+
"mask_token": "[MASK]",
|
| 23 |
+
"model_max_length": 1000000000000000019884624838656,
|
| 24 |
+
"pad_token": "[PAD]",
|
| 25 |
+
"sep_token": "[SEP]",
|
| 26 |
+
"split_by_punct": false,
|
| 27 |
+
"tokenizer_class": "DebertaV2Tokenizer",
|
| 28 |
+
"unk_id": 3,
|
| 29 |
+
"unk_token": "[UNK]",
|
| 30 |
+
"vocab_type": "spm"
|
| 31 |
+
}
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
verify-application100.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "fastino/gliner2.5-small-v1",
|
| 3 |
+
"revision": "7e6f537f10337497069276892a5ef435028252ce",
|
| 4 |
+
"package": "build/gliner2_small_classification_fp16_L128_K8.mlpackage",
|
| 5 |
+
"selected_manifest": "first eligible rows in source suite order; no gold labels used",
|
| 6 |
+
"counts": {
|
| 7 |
+
"checked": 100,
|
| 8 |
+
"too_many_options": 300,
|
| 9 |
+
"too_long": 7,
|
| 10 |
+
"duplicate_labels": 0,
|
| 11 |
+
"mismatches": 0
|
| 12 |
+
},
|
| 13 |
+
"maximum_confidence_error": 0.0017229318618774414,
|
| 14 |
+
"mean_confidence_error": 0.00020694971084594726,
|
| 15 |
+
"model_call_p50_ms": 7.718917040619999,
|
| 16 |
+
"model_call_p95_ms": 9.14695899700746,
|
| 17 |
+
"failures": []
|
| 18 |
+
}
|
verify-lut8-application100.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "fastino/gliner2.5-small-v1",
|
| 3 |
+
"revision": "7e6f537f10337497069276892a5ef435028252ce",
|
| 4 |
+
"package": "build/gliner2_small_classification_lut8_L128_K8.mlpackage",
|
| 5 |
+
"selected_manifest": "first eligible rows in source suite order; no gold labels used",
|
| 6 |
+
"counts": {
|
| 7 |
+
"checked": 100,
|
| 8 |
+
"too_many_options": 300,
|
| 9 |
+
"too_long": 7,
|
| 10 |
+
"duplicate_labels": 0,
|
| 11 |
+
"mismatches": 0
|
| 12 |
+
},
|
| 13 |
+
"maximum_confidence_error": 0.07858973741531372,
|
| 14 |
+
"mean_confidence_error": 0.004985119104385376,
|
| 15 |
+
"model_call_p50_ms": 7.664750039111823,
|
| 16 |
+
"model_call_p95_ms": 10.965458990540355,
|
| 17 |
+
"failures": []
|
| 18 |
+
}
|
verify.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compare the pinned GLiNER2 native classifier with its Core ML export."""
|
| 2 |
+
import argparse
|
| 3 |
+
import json
|
| 4 |
+
import statistics
|
| 5 |
+
import time
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import coremltools as ct
|
| 9 |
+
import numpy as np
|
| 10 |
+
import torch
|
| 11 |
+
from gliner2 import AutoExtractor
|
| 12 |
+
|
| 13 |
+
from preprocessing import prepare_classification
|
| 14 |
+
|
| 15 |
+
MODEL_ID = "fastino/gliner2.5-small-v1"
|
| 16 |
+
MODEL_REVISION = "7e6f537f10337497069276892a5ef435028252ce"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def main():
|
| 20 |
+
parser = argparse.ArgumentParser()
|
| 21 |
+
parser.add_argument("--package", default="build/gliner2_small_classification_fp16_L128_K8.mlpackage")
|
| 22 |
+
parser.add_argument("--suite", required=True)
|
| 23 |
+
parser.add_argument("--limit", type=int, default=100)
|
| 24 |
+
parser.add_argument("--length", type=int, default=128)
|
| 25 |
+
parser.add_argument("--max-options", type=int, default=8)
|
| 26 |
+
parser.add_argument("--out", default="build/verify.json")
|
| 27 |
+
args = parser.parse_args()
|
| 28 |
+
torch.set_num_threads(4)
|
| 29 |
+
source = Path.home() / ".cache/huggingface/hub/models--fastino--gliner2.5-small-v1/snapshots" / MODEL_REVISION
|
| 30 |
+
native = AutoExtractor.from_pretrained(str(source), map_location="cpu").eval()
|
| 31 |
+
model = ct.models.MLModel(args.package, compute_units=ct.ComputeUnit.ALL)
|
| 32 |
+
counts = {"checked": 0, "too_many_options": 0, "too_long": 0, "duplicate_labels": 0, "mismatches": 0}
|
| 33 |
+
errors = []
|
| 34 |
+
latencies = []
|
| 35 |
+
failures = []
|
| 36 |
+
for line in Path(args.suite).open():
|
| 37 |
+
row = json.loads(line)
|
| 38 |
+
options = row.get("options") or []
|
| 39 |
+
if len(options) > args.max_options or not options:
|
| 40 |
+
counts["too_many_options"] += 1
|
| 41 |
+
continue
|
| 42 |
+
labels = [(description or key).strip() for key, description in options]
|
| 43 |
+
if len(set(labels)) != len(labels):
|
| 44 |
+
counts["duplicate_labels"] += 1
|
| 45 |
+
continue
|
| 46 |
+
text = row["state"]
|
| 47 |
+
task = "decision"
|
| 48 |
+
try:
|
| 49 |
+
arrays = prepare_classification(native, text, task, labels, args.length, args.max_options)
|
| 50 |
+
except ValueError:
|
| 51 |
+
counts["too_long"] += 1
|
| 52 |
+
continue
|
| 53 |
+
expected = native.classify_text(text, {task: labels}, include_confidence=True, max_len=args.length)[task]
|
| 54 |
+
start = time.perf_counter()
|
| 55 |
+
prediction = model.predict(arrays)
|
| 56 |
+
latencies.append((time.perf_counter() - start) * 1000)
|
| 57 |
+
probabilities = np.asarray(prediction["probabilities"])[0, : len(labels)]
|
| 58 |
+
chosen = labels[int(probabilities.argmax())]
|
| 59 |
+
error = abs(float(probabilities.max()) - float(expected["confidence"]))
|
| 60 |
+
errors.append(error)
|
| 61 |
+
counts["checked"] += 1
|
| 62 |
+
if chosen != expected["label"]:
|
| 63 |
+
counts["mismatches"] += 1
|
| 64 |
+
failures.append({"suite": row["suite"], "index": row["index"], "native": expected, "coreml": chosen})
|
| 65 |
+
if counts["checked"] >= args.limit:
|
| 66 |
+
break
|
| 67 |
+
result = {
|
| 68 |
+
"model": MODEL_ID, "revision": MODEL_REVISION, "package": args.package,
|
| 69 |
+
"selected_manifest": "first eligible rows in source suite order; no gold labels used",
|
| 70 |
+
"counts": counts, "maximum_confidence_error": max(errors, default=None),
|
| 71 |
+
"mean_confidence_error": statistics.mean(errors) if errors else None,
|
| 72 |
+
"model_call_p50_ms": statistics.median(latencies[1:]) if len(latencies) > 1 else None,
|
| 73 |
+
"model_call_p95_ms": sorted(latencies[1:])[int(0.95 * (len(latencies) - 1))] if len(latencies) > 1 else None,
|
| 74 |
+
"failures": failures[:20],
|
| 75 |
+
}
|
| 76 |
+
Path(args.out).parent.mkdir(parents=True, exist_ok=True)
|
| 77 |
+
Path(args.out).write_text(json.dumps(result, indent=2) + "\n")
|
| 78 |
+
print(json.dumps(result, indent=2))
|
| 79 |
+
if counts["checked"] == 0 or counts["mismatches"]:
|
| 80 |
+
raise SystemExit(1)
|
| 81 |
+
|
| 82 |
+
if __name__ == "__main__":
|
| 83 |
+
main()
|