Instructions to use FluidInference/gliner2-5-multi-coreml with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- GLiNER2
How to use FluidInference/gliner2-5-multi-coreml with GLiNER2:
from gliner2 import GLiNER2 model = GLiNER2.from_pretrained("FluidInference/gliner2-5-multi-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 multilingual Core ML classification exports
Browse files- .gitattributes +1 -0
- README.md +45 -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_multi_classification_fp16_L128_K8.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- gliner2_multi_classification_fp16_L128_K8.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- gliner2_multi_classification_fp16_L128_K8.mlpackage/Manifest.json +18 -0
- gliner2_multi_classification_fp32_L128_K8.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- gliner2_multi_classification_fp32_L128_K8.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- gliner2_multi_classification_fp32_L128_K8.mlpackage/Manifest.json +18 -0
- preprocessing.py +43 -0
- pyproject.toml +29 -0
- runtime.py +39 -0
- tokenizer/tokenizer.json +3 -0
- tokenizer/tokenizer_config.json +31 -0
- uv.lock +0 -0
- verify-fp16-application300.json +28 -0
- verify-fp32-application100.json +18 -0
- verify-multilingual-fixtures.json +62 -0
- verify.py +83 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
tokenizer/tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
library_name: coremltools
|
| 4 |
+
pipeline_tag: text-classification
|
| 5 |
+
tags:
|
| 6 |
+
- coreml
|
| 7 |
+
- gliner2
|
| 8 |
+
- multilingual
|
| 9 |
+
- apple-silicon
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# GLiNER2.5 multilingual Core ML classification
|
| 13 |
+
|
| 14 |
+
Core ML conversion of the classification path of
|
| 15 |
+
[Fastino GLiNER2.5 multilingual](https://huggingface.co/fastino/gliner2.5-multi-v1),
|
| 16 |
+
revision `a221b77a8baf4a613b8f8652661d41fa10a5641e` (Apache-2.0).
|
| 17 |
+
The original checkpoint has 287,355,159 parameters. This export includes the
|
| 18 |
+
encoder and trained classification head (278,719,489 parameters); it does not
|
| 19 |
+
include entity, relation, record or span extraction. Use the original checkpoint
|
| 20 |
+
for those tasks.
|
| 21 |
+
|
| 22 |
+
The L128/K8 FP16 package is 576,528,829 bytes. It matched native choices on
|
| 23 |
+
299 of 300 eligible fixed application requests, with one near tie reversing
|
| 24 |
+
"sports" and "world news". Maximum chosen-confidence difference was 0.009249;
|
| 25 |
+
median Python Core ML call was 8.81 ms on an M5 Pro running macOS 27.0.
|
| 26 |
+
The FP32 reference package is 1,152,027,886 bytes; it matched all 100 sampled
|
| 27 |
+
native choices, with maximum confidence difference 0.00000805 and median call
|
| 28 |
+
11.24 ms. Four French, Spanish, Chinese and German smoke phrases gave the same
|
| 29 |
+
chosen labels in native, FP16 and FP32. See the report JSON files. These are
|
| 30 |
+
parity checks, not a complete Decision Index run or ANE-only timings.
|
| 31 |
+
|
| 32 |
+
The model uses the upstream GLiNER2 schema and tokenizer supplied here.
|
| 33 |
+
The Core ML minimum target is iOS 17/macOS 14. At most eight labels fit this
|
| 34 |
+
bucket; inputs over capacity require a larger model bucket rather than
|
| 35 |
+
truncation. The included runtime loads only Core ML and tokenizer files:
|
| 36 |
+
|
| 37 |
+
```bash
|
| 38 |
+
uv sync
|
| 39 |
+
uv run python runtime.py --model-dir . --text '科学家发现了一颗新的行星。' \
|
| 40 |
+
--task topic --labels '["science","sports","politics"]' --precision fp16
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
Use `--precision fp32` when exact native decisions around narrow margins matter.
|
| 44 |
+
The conversion scripts, pinned dependencies and asset hashes are included.
|
| 45 |
+
Fluid Inference converted the model; Fastino authored the original checkpoint.
|
assets.lock.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"source": "fastino/gliner2.5-multi-v1",
|
| 3 |
+
"revision": "a221b77a8baf4a613b8f8652661d41fa10a5641e",
|
| 4 |
+
"files": {
|
| 5 |
+
"config.json": {
|
| 6 |
+
"size": 3151,
|
| 7 |
+
"sha256": "8b59a0f426a65859c89cd1ea850c3529c09aa3be3a6fafd8eddfdd17b1bf0146"
|
| 8 |
+
},
|
| 9 |
+
"encoder_config/config.json": {
|
| 10 |
+
"size": 857,
|
| 11 |
+
"sha256": "fa4f9ef2903b5369ab172333aae4574e6a476511d7465845cf59f8360ee18716"
|
| 12 |
+
},
|
| 13 |
+
"model.safetensors": {
|
| 14 |
+
"size": 1149461028,
|
| 15 |
+
"sha256": "c1ff4ec0bc00031c15530b8f3c33d3677f27949e6a0cb52e1247a6224b6c5395"
|
| 16 |
+
},
|
| 17 |
+
"tokenizer.json": {
|
| 18 |
+
"size": 16035853,
|
| 19 |
+
"sha256": "c62446df87ae18ec98b133f8f84fc449a07cc89bbf8ef192a4cb5f9c53777a7a"
|
| 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-multi-v1",
|
| 3 |
+
"source_revision": "a221b77a8baf4a613b8f8652661d41fa10a5641e",
|
| 4 |
+
"package": "build/gliner2_multi_classification_fp32_L128_K8.mlpackage",
|
| 5 |
+
"package_bytes": 1152027886,
|
| 6 |
+
"native_total_parameters": 287355159,
|
| 7 |
+
"exported_parameters": 278719489,
|
| 8 |
+
"wrapper_max_logit_error": 0.0,
|
| 9 |
+
"coremltools": "9.0",
|
| 10 |
+
"torch": "2.7.0",
|
| 11 |
+
"cases": [
|
| 12 |
+
{
|
| 13 |
+
"text": "The rocket launched successfully.",
|
| 14 |
+
"native_label": "sports",
|
| 15 |
+
"coreml_label": "sports",
|
| 16 |
+
"native_confidence": 0.5060790777206421,
|
| 17 |
+
"coreml_confidence": 0.5060756206512451,
|
| 18 |
+
"absolute_confidence_error": 3.4570693969726562e-06
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"text": "The team won the football championship.",
|
| 22 |
+
"native_label": "sports",
|
| 23 |
+
"coreml_label": "sports",
|
| 24 |
+
"native_confidence": 0.9999996423721313,
|
| 25 |
+
"coreml_confidence": 0.9999996423721313,
|
| 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.9999977350234985,
|
| 33 |
+
"coreml_confidence": 0.9999977350234985,
|
| 34 |
+
"absolute_confidence_error": 0.0
|
| 35 |
+
}
|
| 36 |
+
]
|
| 37 |
+
}
|
convert-coreml.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Convert and verify the pinned GLiNER2.5-multi 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-multi-v1"
|
| 18 |
+
MODEL_REVISION = "a221b77a8baf4a613b8f8652661d41fa10a5641e"
|
| 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-multi 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_multi_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_multi_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:ddee410a764552de583f87c2fa383b327f962cd0a3a06129d25463e300df9ec4
|
| 3 |
+
size 990100
|
gliner2_multi_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:519b9689dce771f7434aa12ec68ea3ceb47d9675888f54825c0dbf84aea0ac41
|
| 3 |
+
size 575538112
|
gliner2_multi_classification_fp16_L128_K8.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"5189F522-6EF7-44F9-A58A-C2AE3A2607EE": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Specification",
|
| 7 |
+
"name": "model.mlmodel",
|
| 8 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 9 |
+
},
|
| 10 |
+
"53AB227A-708E-47B2-A73B-1C4A0787D02E": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Weights",
|
| 13 |
+
"name": "weights",
|
| 14 |
+
"path": "com.apple.CoreML/weights"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "5189F522-6EF7-44F9-A58A-C2AE3A2607EE"
|
| 18 |
+
}
|
gliner2_multi_classification_fp32_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:41d12153f9453d5bc2c0b31c2d9469639b8719e71243e1d9821ed8fe3a5ca987
|
| 3 |
+
size 965317
|
gliner2_multi_classification_fp32_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:273c343d1a26d66a0feca3325e335e9825dcac6f476f57f1f8e51d594bf4e860
|
| 3 |
+
size 1151061952
|
gliner2_multi_classification_fp32_L128_K8.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"6FD8F497-0DFB-47AC-A8DE-EFECB501243E": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Specification",
|
| 7 |
+
"name": "model.mlmodel",
|
| 8 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 9 |
+
},
|
| 10 |
+
"DAB0BCC1-3F7E-4D48-836A-E64F9EAD6142": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Weights",
|
| 13 |
+
"name": "weights",
|
| 14 |
+
"path": "com.apple.CoreML/weights"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "6FD8F497-0DFB-47AC-A8DE-EFECB501243E"
|
| 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-multi-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,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 13 |
+
max_options: int = 8, precision: str = "fp16"):
|
| 14 |
+
model_dir = Path(model_dir)
|
| 15 |
+
package = model_dir / f"gliner2_multi_classification_{precision}_L{length}_K{max_options}.mlpackage"
|
| 16 |
+
processor = load_processor(str(model_dir / "tokenizer"))
|
| 17 |
+
arrays = prepare_with_processor(processor, text, task, labels, length, max_options)
|
| 18 |
+
model = ct.models.MLModel(str(package), compute_units=ct.ComputeUnit.ALL)
|
| 19 |
+
scores = np.asarray(model.predict(arrays)["probabilities"])[0, : len(labels)]
|
| 20 |
+
return {"label": labels[int(scores.argmax())], "confidence": float(scores.max()),
|
| 21 |
+
"probabilities": {label: float(score) for label, score in zip(labels, scores)}}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def main():
|
| 25 |
+
parser = argparse.ArgumentParser()
|
| 26 |
+
parser.add_argument("--model-dir", required=True)
|
| 27 |
+
parser.add_argument("--text", required=True)
|
| 28 |
+
parser.add_argument("--task", default="decision")
|
| 29 |
+
parser.add_argument("--labels", required=True, help="JSON list of label strings")
|
| 30 |
+
parser.add_argument("--length", type=int, default=128)
|
| 31 |
+
parser.add_argument("--max-options", type=int, default=8)
|
| 32 |
+
parser.add_argument("--precision", choices=["fp16", "fp32"], default="fp16")
|
| 33 |
+
args = parser.parse_args()
|
| 34 |
+
print(json.dumps(classify(args.model_dir, args.text, args.task, json.loads(args.labels),
|
| 35 |
+
args.length, args.max_options, args.precision), indent=2))
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
if __name__ == "__main__":
|
| 39 |
+
main()
|
tokenizer/tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c62446df87ae18ec98b133f8f84fc449a07cc89bbf8ef192a4cb5f9c53777a7a
|
| 3 |
+
size 16035853
|
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-fp16-application300.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "fastino/gliner2.5-multi-v1",
|
| 3 |
+
"revision": "a221b77a8baf4a613b8f8652661d41fa10a5641e",
|
| 4 |
+
"package": "build/gliner2_multi_classification_fp16_L128_K8.mlpackage",
|
| 5 |
+
"selected_manifest": "first eligible rows in source suite order; no gold labels used",
|
| 6 |
+
"counts": {
|
| 7 |
+
"checked": 300,
|
| 8 |
+
"too_many_options": 300,
|
| 9 |
+
"too_long": 75,
|
| 10 |
+
"duplicate_labels": 0,
|
| 11 |
+
"mismatches": 1
|
| 12 |
+
},
|
| 13 |
+
"maximum_confidence_error": 0.00924915075302124,
|
| 14 |
+
"mean_confidence_error": 0.0009126420815785726,
|
| 15 |
+
"model_call_p50_ms": 8.808917016722262,
|
| 16 |
+
"model_call_p95_ms": 13.894708012230694,
|
| 17 |
+
"failures": [
|
| 18 |
+
{
|
| 19 |
+
"suite": "jev.ag_news",
|
| 20 |
+
"index": 100,
|
| 21 |
+
"native": {
|
| 22 |
+
"label": "sports",
|
| 23 |
+
"confidence": 0.4976130723953247
|
| 24 |
+
},
|
| 25 |
+
"coreml": "world news and international politics"
|
| 26 |
+
}
|
| 27 |
+
]
|
| 28 |
+
}
|
verify-fp32-application100.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "fastino/gliner2.5-multi-v1",
|
| 3 |
+
"revision": "a221b77a8baf4a613b8f8652661d41fa10a5641e",
|
| 4 |
+
"package": "build/gliner2_multi_classification_fp32_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": 31,
|
| 10 |
+
"duplicate_labels": 0,
|
| 11 |
+
"mismatches": 0
|
| 12 |
+
},
|
| 13 |
+
"maximum_confidence_error": 8.046627044677734e-06,
|
| 14 |
+
"mean_confidence_error": 7.531046867370605e-07,
|
| 15 |
+
"model_call_p50_ms": 11.235417041461915,
|
| 16 |
+
"model_call_p95_ms": 19.665875006467104,
|
| 17 |
+
"failures": []
|
| 18 |
+
}
|
verify-multilingual-fixtures.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"text": "La sélection a remporté le championnat de football.",
|
| 4 |
+
"native": {
|
| 5 |
+
"label": "sports",
|
| 6 |
+
"confidence": 0.9999890327453613
|
| 7 |
+
},
|
| 8 |
+
"fp16": {
|
| 9 |
+
"label": "sports",
|
| 10 |
+
"confidence": 0.9999892711639404
|
| 11 |
+
},
|
| 12 |
+
"fp32": {
|
| 13 |
+
"label": "sports",
|
| 14 |
+
"confidence": 0.9999890327453613
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"text": "El parlamento aprobó el presupuesto.",
|
| 19 |
+
"native": {
|
| 20 |
+
"label": "politics",
|
| 21 |
+
"confidence": 0.9999994039535522
|
| 22 |
+
},
|
| 23 |
+
"fp16": {
|
| 24 |
+
"label": "politics",
|
| 25 |
+
"confidence": 0.9999994039535522
|
| 26 |
+
},
|
| 27 |
+
"fp32": {
|
| 28 |
+
"label": "politics",
|
| 29 |
+
"confidence": 0.9999994039535522
|
| 30 |
+
}
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"text": "科学家发现了一颗新的行星。",
|
| 34 |
+
"native": {
|
| 35 |
+
"label": "science",
|
| 36 |
+
"confidence": 0.9999904632568359
|
| 37 |
+
},
|
| 38 |
+
"fp16": {
|
| 39 |
+
"label": "science",
|
| 40 |
+
"confidence": 0.9999902248382568
|
| 41 |
+
},
|
| 42 |
+
"fp32": {
|
| 43 |
+
"label": "science",
|
| 44 |
+
"confidence": 0.9999904632568359
|
| 45 |
+
}
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"text": "Das Team gewann die Meisterschaft.",
|
| 49 |
+
"native": {
|
| 50 |
+
"label": "sports",
|
| 51 |
+
"confidence": 0.9999986886978149
|
| 52 |
+
},
|
| 53 |
+
"fp16": {
|
| 54 |
+
"label": "sports",
|
| 55 |
+
"confidence": 0.9999986886978149
|
| 56 |
+
},
|
| 57 |
+
"fp32": {
|
| 58 |
+
"label": "sports",
|
| 59 |
+
"confidence": 0.9999986886978149
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
]
|
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-multi-v1"
|
| 16 |
+
MODEL_REVISION = "a221b77a8baf4a613b8f8652661d41fa10a5641e"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def main():
|
| 20 |
+
parser = argparse.ArgumentParser()
|
| 21 |
+
parser.add_argument("--package", default="build/gliner2_multi_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-multi-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()
|