Zero-Shot Classification
Core ML
GLiNER
GLiNER2
English
coremltools
deberta-v3
apple-silicon
fp16
multifunction
Instructions to use augustoFranke/GLiNER2.5-Decide-CoreML-FP16-MultiFn-L64-512 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- GLiNER
How to use augustoFranke/GLiNER2.5-Decide-CoreML-FP16-MultiFn-L64-512 with GLiNER:
from gliner import GLiNER model = GLiNER.from_pretrained("augustoFranke/GLiNER2.5-Decide-CoreML-FP16-MultiFn-L64-512") text = "Cristiano Ronaldo dos Santos Aveiro was born on 5 February 1985 in Funchal, Madeira, Portugal." labels = ["person", "date", "location"] entities = model.predict_entities(text, labels) for entity in entities: print(entity["text"], "=>", entity["label"]) - GLiNER2
How to use augustoFranke/GLiNER2.5-Decide-CoreML-FP16-MultiFn-L64-512 with GLiNER2:
from gliner2 import AutoExtractor extractor = AutoExtractor.from_pretrained("augustoFranke/GLiNER2.5-Decide-CoreML-FP16-MultiFn-L64-512") # 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
File size: 9,649 Bytes
cb96101 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | """Convert and verify the pinned GLiNER2.5-Decide multi-head classification path.
Modified from Fluid Inference's gliner2-5-decide-coreml `convert-coreml.py` (Apache-2.0):
traces with an example that fits the requested length and verifies every example that fits,
so buckets shorter than the original examples (L64) can be built and checked.
"""
import argparse
import json
import math
from pathlib import Path
import coremltools as ct
import numpy as np
import torch
from gliner2 import AutoExtractor
from huggingface_hub import snapshot_download
from transformers.models.deberta_v2 import modeling_deberta_v2
from convert_names import MODEL_ID, MODEL_REVISION
from export_model import GLiNER2DecideExport, coreml_safe_attention_forward
from preprocessing import classification_schema, prepare_decision
from runtime import decode, package_name
EXAMPLES = [
("My subscription renewed on April 15 for ¥5,400 after the service was already down. "
"Can I get that charge refunded?",
{"intent": ["order_status", "refund_request", "cancel_subscription", "update_payment", "login_problem",
"shipping_delay", "bug_report", "speak_to_human"]}),
("Battery dies before lunch, but the keyboard and the screen are the best I have used on a laptop.",
{"sentiment": ["positive", "negative", "mixed", "neutral"],
"aspects": {"labels": ["battery", "keyboard", "screen", "camera", "price", "support"],
"multi_label": True, "cls_threshold": 0.4}}),
("From: compliance@group.example\nSubject: Protocol update — action required today\n\n"
"Please confirm the new retention rule is applied before Friday's audit.",
{"intent": ["fyi", "request", "approval", "complaint", "newsletter", "security_alert"],
"urgency": ["low", "normal", "high", "critical"],
"route": ["support", "billing", "legal", "security", "finance", "archive"]}),
("My subscription renewed on April 15 for 5,400 yen after the service was already down. "
"Can I get that charge refunded?",
{"intent": ["order_status", "refund_request", "cancel_subscription", "update_payment", "other"],
"urgency": ["low", "normal", "high"]}),
]
def fits(processor, text, tasks, bucket):
try:
prepare_decision(processor, text, tasks, *bucket)
return True
except ValueError:
return False
def native_logits(native, text, tasks):
"""Per-head native logits via the upstream span collator, encoder and shared classifier."""
from gliner2.training.trainer import ExtractorCollator
collator = ExtractorCollator(native.processor, is_training=False, max_len=None, architecture="span")
batch = collator([(text, classification_schema(tasks).build())])
_, schema_embs = native._encode_batch(batch)
return [native.classifier(torch.stack(embs[1:])).squeeze(-1) for embs in schema_embs[0]]
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--output-dir", default="build")
parser.add_argument("--length", type=int, default=128)
parser.add_argument("--max-heads", type=int, default=4)
parser.add_argument("--max-options", type=int, default=8)
parser.add_argument("--precision", choices=["fp16", "fp32"], default="fp16")
args = parser.parse_args()
torch.set_num_threads(4)
source = snapshot_download(
MODEL_ID, revision=MODEL_REVISION,
allow_patterns=[
"config.json", "encoder_config/*", "model.safetensors", "tokenizer.json", "tokenizer_config.json",
"special_tokens_map.json",
],
)
native = AutoExtractor.from_pretrained(source, map_location="cpu").eval()
wrapper = GLiNER2DecideExport(native).eval()
bucket = (args.length, args.max_heads, args.max_options)
fitting = [e for e in EXAMPLES if fits(native.processor, *e, bucket)]
if not fitting:
raise RuntimeError(f"no example fits bucket {bucket}")
print(f"examples fitting L{args.length}: {len(fitting)}/{len(EXAMPLES)}")
text, tasks = fitting[-1] if args.length < 128 else (EXAMPLES[2] if EXAMPLES[2] in fitting else fitting[-1])
arrays = prepare_decision(native.processor, text, tasks, *bucket)
tensors = tuple(torch.from_numpy(value) for value in arrays.values())
def wrapper_error():
with torch.no_grad():
expected = native_logits(native, text, tasks)
actual = wrapper(*tensors)[0][0]
return max(float((row - actual[h, : len(row)]).abs().max()) for h, row in enumerate(expected))
error = wrapper_error()
if error > 1e-4:
raise RuntimeError(f"Wrapper/native logit mismatch: {error}")
# The upstream scale is a constant for a fixed DeBERTa attention head width.
# Its traced int32 sqrt is rejected by Core ML; freeze the identical float32
# value while tracing, and restore the upstream implementation immediately.
original_scale = modeling_deberta_v2.scaled_size_sqrt
original_rpos = modeling_deberta_v2.build_rpos
original_attention = modeling_deberta_v2.DisentangledSelfAttention.forward
def static_scale(query_layer, scale_factor):
value = math.sqrt(float(query_layer.shape[-1] * scale_factor))
return torch.tensor(value, dtype=torch.float32, device=query_layer.device)
modeling_deberta_v2.scaled_size_sqrt = static_scale
# The encoder only uses self-attention: query and key sequence lengths are
# identical, so the scripted build_rpos returns relative_pos unchanged.
modeling_deberta_v2.build_rpos = lambda query, key, relative_pos, buckets, max_pos: relative_pos
modeling_deberta_v2.DisentangledSelfAttention.forward = coreml_safe_attention_forward
try:
frozen_error = wrapper_error()
if frozen_error > 1e-4:
raise RuntimeError(f"Frozen attention scale changed native logits: {frozen_error}")
with torch.no_grad():
traced = torch.jit.trace(wrapper, tensors, check_trace=False)
finally:
modeling_deberta_v2.scaled_size_sqrt = original_scale
modeling_deberta_v2.build_rpos = original_rpos
modeling_deberta_v2.DisentangledSelfAttention.forward = original_attention
grid = (1, args.max_heads, args.max_options)
converted = ct.convert(
traced, convert_to="mlprogram", minimum_deployment_target=ct.target.iOS17,
compute_precision=ct.precision.FLOAT16 if args.precision == "fp16" else ct.precision.FLOAT32,
compute_units=ct.ComputeUnit.CPU_ONLY,
inputs=[
ct.TensorType(name="input_ids", shape=(1, args.length), dtype=np.int32),
ct.TensorType(name="attention_mask", shape=(1, args.length), dtype=np.int32),
ct.TensorType(name="marker_indices", shape=grid, dtype=np.int32),
ct.TensorType(name="marker_mask", shape=grid, dtype=np.float32),
],
outputs=[ct.TensorType(name="logits", dtype=np.float32), ct.TensorType(name="probabilities", dtype=np.float32)],
)
converted.short_description = "GLiNER2.5-Decide multi-head schema classification path"
converted.author = "Fastino (original); Fluid Inference (Core ML conversion)"
converted.license = "Apache-2.0"
converted.user_defined_metadata.update({
"source_model": MODEL_ID, "source_revision": MODEL_REVISION,
"scope": "classification only; span and count heads not exported",
"length": str(args.length), "max_heads": str(args.max_heads), "max_options": str(args.max_options),
})
out = Path(args.output_dir)
out.mkdir(parents=True, exist_ok=True)
package = out / package_name(args.precision, *bucket)
converted.save(str(package))
runtime = ct.models.MLModel(str(package), compute_units=ct.ComputeUnit.ALL)
cases = []
for text, tasks in fitting:
native_output = native.classify_text(text, tasks, include_confidence=True)
logits = np.asarray(runtime.predict(prepare_decision(native.processor, text, tasks, *bucket))["logits"])[0]
coreml_output = decode(tasks, logits)
if json.dumps(labels_only(native_output)) != json.dumps(labels_only(coreml_output)):
raise RuntimeError(f"Core ML/native mismatch: {coreml_output} != {native_output}")
cases.append({"text": text, "native": native_output, "coreml": coreml_output,
"max_confidence_error": confidence_error(native_output, coreml_output)})
report = {
"source_model": MODEL_ID, "source_revision": MODEL_REVISION, "package": str(package),
"package_bytes": sum(f.stat().st_size for f in package.rglob("*") if f.is_file()),
"native_total_parameters": sum(p.numel() for p in native.parameters()),
"exported_parameters": sum(p.numel() for p in wrapper.parameters()),
"wrapper_max_logit_error": error, "coremltools": ct.__version__, "torch": torch.__version__,
"cases": cases,
}
(out / f"conversion-{package.stem}.json").write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n")
print(json.dumps(report, indent=2, ensure_ascii=False))
def _entries(value):
return value if isinstance(value, list) else [value]
def labels_only(result: dict) -> dict:
return {task: sorted(entry["label"] for entry in _entries(value)) for task, value in result.items()}
def confidence_error(native: dict, coreml: dict) -> float:
errors = []
for task, value in native.items():
predicted = {entry["label"]: entry["confidence"] for entry in _entries(coreml[task])}
errors += [abs(entry["confidence"] - predicted[entry["label"]]) for entry in _entries(value)]
return max(errors)
if __name__ == "__main__":
main()
|