Instructions to use FluidInference/gliner2-5-base-coreml with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- GLiNER2
How to use FluidInference/gliner2-5-base-coreml with GLiNER2:
from gliner2 import AutoExtractor extractor = AutoExtractor.from_pretrained("FluidInference/gliner2-5-base-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
Add validated embedding W8 classification option
Browse files- README.md +2 -0
- build/gliner2_base_classification_embedding_w8_L128_K8.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- build/gliner2_base_classification_embedding_w8_L128_K8.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- build/gliner2_base_classification_embedding_w8_L128_K8.mlpackage/Manifest.json +18 -0
- compress-coreml.py +51 -11
- reports/classification-embedding-w8.json +149 -0
- runtime.py +7 -3
- verify-compression.py +135 -0
README.md
CHANGED
|
@@ -41,4 +41,6 @@ The feature graph compute plan assigned 53.13% of operations to ANE and 46.87% t
|
|
| 41 |
|
| 42 |
The original L128/K8 classification packages remain available, with a separate `runtime.py` entry point. Up to eight labels fit that bucket. 100/100 selected choices matched in FP16. See the classification report JSONs for the exact selected samples and limits. The full checkpoint's task and dataset scores have not been reproduced here.
|
| 43 |
|
|
|
|
|
|
|
| 44 |
The Core ML deployment target is iOS 17/macOS 14. Conversion scripts, pinned dependencies, asset hashes, and selected verification reports are included. The source revision is a current pinned snapshot; identity with the historical Decision Index evaluation checkpoint has not been established.
|
|
|
|
| 41 |
|
| 42 |
The original L128/K8 classification packages remain available, with a separate `runtime.py` entry point. Up to eight labels fit that bucket. 100/100 selected choices matched in FP16. See the classification report JSONs for the exact selected samples and limits. The full checkpoint's task and dataset scores have not been reproduced here.
|
| 43 |
|
| 44 |
+
An optional **embedding-only W8** classifier keeps the encoder and heads FP16. It reduces the package from **388,981,604 to 291,053,994 bytes** (25.2%). On an M5 Pro plugged into AC, it agreed with FP16 on all 14 bucket-fitting requests in a fixed 20-request manifest (largest probability difference 0.00992), and matched the native classifier on 100/100 selected eligible application requests (largest confidence difference 0.01370). The 14-request paired full-request p50 was 3.662 ms FP16 versus 3.632 ms W8 with automatic device selection; this is a size option, not a demonstrated speedup. Forced CPU+ANE was slower (9.168 versus 9.666 ms). The W8 compute plan put 496/516 executable ops on ANE under forced CPU+ANE, with 20 CPU boundary/constant ops. Use `runtime.py --precision embedding_w8` for this classification package; extraction continues to use its separate FP32 or FP16 packages.
|
| 45 |
+
|
| 46 |
The Core ML deployment target is iOS 17/macOS 14. Conversion scripts, pinned dependencies, asset hashes, and selected verification reports are included. The source revision is a current pinned snapshot; identity with the historical Decision Index evaluation checkpoint has not been established.
|
build/gliner2_base_classification_embedding_w8_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:11c56d5d686afb3812181024125c8102756d616230606e849a0c97ebd9b34dd9
|
| 3 |
+
size 990593
|
build/gliner2_base_classification_embedding_w8_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:037f91cb176d31a38bc1795272d99c47531f3ac235b6f4533bab0df9ae4de327
|
| 3 |
+
size 290062784
|
build/gliner2_base_classification_embedding_w8_L128_K8.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"1D1E4BE6-7753-4709-9319-081055B47FD9": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Specification",
|
| 7 |
+
"name": "model.mlmodel",
|
| 8 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 9 |
+
},
|
| 10 |
+
"8F904B6A-73EB-47FB-B055-2D2B3240F1A0": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Weights",
|
| 13 |
+
"name": "weights",
|
| 14 |
+
"path": "com.apple.CoreML/weights"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "1D1E4BE6-7753-4709-9319-081055B47FD9"
|
| 18 |
+
}
|
compress-coreml.py
CHANGED
|
@@ -5,7 +5,14 @@ import time
|
|
| 5 |
from pathlib import Path
|
| 6 |
|
| 7 |
import coremltools as ct
|
| 8 |
-
from coremltools.optimize.coreml import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
|
| 11 |
def package_bytes(path):
|
|
@@ -17,25 +24,58 @@ def main():
|
|
| 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 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"] =
|
| 37 |
compressed.save(args.output)
|
| 38 |
-
report = {"source": args.source, "output": args.output, "
|
|
|
|
|
|
|
|
|
|
| 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")
|
|
|
|
| 5 |
from pathlib import Path
|
| 6 |
|
| 7 |
import coremltools as ct
|
| 8 |
+
from coremltools.optimize.coreml import (
|
| 9 |
+
OpLinearQuantizerConfig,
|
| 10 |
+
OpPalettizerConfig,
|
| 11 |
+
OptimizationConfig,
|
| 12 |
+
get_weights_metadata,
|
| 13 |
+
linear_quantize_weights,
|
| 14 |
+
palettize_weights,
|
| 15 |
+
)
|
| 16 |
|
| 17 |
|
| 18 |
def package_bytes(path):
|
|
|
|
| 24 |
parser.add_argument("--source", required=True)
|
| 25 |
parser.add_argument("--output", required=True)
|
| 26 |
parser.add_argument("--bits", type=int, choices=[4, 6, 8], default=8)
|
| 27 |
+
parser.add_argument("--method", choices=["lut", "linear", "embedding"], default="lut")
|
| 28 |
+
parser.add_argument("--linear-mode", choices=["linear_symmetric", "linear"], default="linear_symmetric")
|
| 29 |
parser.add_argument("--granularity", choices=["per_tensor", "per_grouped_channel"], default="per_tensor")
|
| 30 |
parser.add_argument("--group-size", type=int, default=32)
|
| 31 |
args = parser.parse_args()
|
| 32 |
start = time.perf_counter()
|
| 33 |
model = ct.models.MLModel(args.source, skip_model_load=True)
|
| 34 |
+
selected_weights = None
|
| 35 |
+
if args.method in ("linear", "embedding"):
|
| 36 |
+
if args.bits != 8:
|
| 37 |
+
parser.error("linear quantization here supports only 8-bit weights")
|
| 38 |
+
quantizer = OpLinearQuantizerConfig(
|
| 39 |
+
mode=args.linear_mode, dtype="int8", granularity="per_channel", weight_threshold=2048,
|
| 40 |
+
)
|
| 41 |
+
if args.method == "embedding":
|
| 42 |
+
metadata = get_weights_metadata(model, weight_threshold=2048)
|
| 43 |
+
names = [
|
| 44 |
+
name for name, weight in metadata.items()
|
| 45 |
+
if "encoder_embeddings_word_embeddings" in name
|
| 46 |
+
and len(weight.val.shape) == 2
|
| 47 |
+
and any(op.op_type == "gather" for op in weight.child_ops)
|
| 48 |
+
]
|
| 49 |
+
if len(names) != 1:
|
| 50 |
+
raise ValueError(f"expected one token embedding weight, found {names}")
|
| 51 |
+
config = OptimizationConfig(op_name_configs={names[0]: quantizer})
|
| 52 |
+
selected_weights = names
|
| 53 |
+
else:
|
| 54 |
+
config = OptimizationConfig(global_config=quantizer)
|
| 55 |
+
compressed = linear_quantize_weights(model, config=config)
|
| 56 |
+
variant = (
|
| 57 |
+
f"W8 {args.linear_mode} token embedding only"
|
| 58 |
+
if args.method == "embedding" else f"W8 {args.linear_mode} per-channel"
|
| 59 |
+
)
|
| 60 |
+
else:
|
| 61 |
+
config = OptimizationConfig(global_config=OpPalettizerConfig(
|
| 62 |
+
mode="kmeans", nbits=args.bits, granularity=args.granularity,
|
| 63 |
+
group_size=args.group_size,
|
| 64 |
+
enable_per_channel_scale=args.granularity == "per_grouped_channel",
|
| 65 |
+
num_kmeans_workers=4,
|
| 66 |
+
))
|
| 67 |
+
compressed = palettize_weights(model, config=config)
|
| 68 |
+
variant = f"LUT{args.bits} {args.granularity}"
|
| 69 |
+
compressed.short_description = f"{model.short_description}; {variant} weights"
|
| 70 |
compressed.author = model.author
|
| 71 |
compressed.license = model.license
|
| 72 |
compressed.user_defined_metadata.update(model.user_defined_metadata)
|
| 73 |
+
compressed.user_defined_metadata["weight_compression"] = variant
|
| 74 |
compressed.save(args.output)
|
| 75 |
+
report = {"source": args.source, "output": args.output, "method": args.method,
|
| 76 |
+
"linear_mode": args.linear_mode if args.method in ("linear", "embedding") else None,
|
| 77 |
+
"granularity": "per_channel" if args.method in ("linear", "embedding") else args.granularity,
|
| 78 |
+
"selected_weights": selected_weights,
|
| 79 |
"source_bytes": package_bytes(Path(args.source)),
|
| 80 |
"output_bytes": package_bytes(Path(args.output)), "compression_seconds": time.perf_counter() - start}
|
| 81 |
Path(args.output).with_suffix(".json").write_text(json.dumps(report, indent=2) + "\n")
|
reports/classification-embedding-w8.json
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"source_revision": "1a8bc24e00dc7300b9017c81d63e3dcdabb26596",
|
| 3 |
+
"shape": "L128/K8 classification",
|
| 4 |
+
"candidate_package": "gliner2_base_classification_embedding_w8_L128_K8.mlpackage",
|
| 5 |
+
"weight_sha256": "037f91cb176d31a38bc1795272d99c47531f3ac235b6f4533bab0df9ae4de327",
|
| 6 |
+
"compression": {
|
| 7 |
+
"source": "build/gliner2_base_classification_fp16_L128_K8.mlpackage",
|
| 8 |
+
"output": "build/gliner2_base_classification_embedding_w8_L128_K8.mlpackage",
|
| 9 |
+
"method": "embedding",
|
| 10 |
+
"granularity": "per_channel",
|
| 11 |
+
"selected_weights": [
|
| 12 |
+
"encoder_embeddings_word_embeddings_weight_to_fp16"
|
| 13 |
+
],
|
| 14 |
+
"source_bytes": 388981604,
|
| 15 |
+
"output_bytes": 291053994,
|
| 16 |
+
"compression_seconds": 5.819930917001329
|
| 17 |
+
},
|
| 18 |
+
"paired_first_manifest_all": {
|
| 19 |
+
"reference": "build/gliner2_base_classification_fp16_L128_K8.mlpackage",
|
| 20 |
+
"candidate": "build/gliner2_base_classification_embedding_w8_L128_K8.mlpackage",
|
| 21 |
+
"manifest": "../../gliner2-small/coreml/fixtures/optimization-selected20.jsonl",
|
| 22 |
+
"compute_units": "all",
|
| 23 |
+
"processor": "arm",
|
| 24 |
+
"machine": "arm64",
|
| 25 |
+
"macos": "27.0",
|
| 26 |
+
"power_audit_after_run": "Now drawing from 'AC Power'\n -InternalBattery-0 (id=35651683)\t50%; charging; 1:25 remaining present: true",
|
| 27 |
+
"reference_load_ms": 2044.181875011418,
|
| 28 |
+
"candidate_load_ms": 4582.357916980982,
|
| 29 |
+
"warmup_calls_per_model": 1,
|
| 30 |
+
"timed_order": "alternating reference-first/candidate-first by request",
|
| 31 |
+
"checked": 14,
|
| 32 |
+
"choice_agreement": 14,
|
| 33 |
+
"max_probability_error": 0.00992196798324585,
|
| 34 |
+
"probability_error_limit": 0.02,
|
| 35 |
+
"reference_p50_ms": 3.334374981932342,
|
| 36 |
+
"candidate_p50_ms": 3.261812496930361,
|
| 37 |
+
"reference_p95_ms": 9.934750036336482,
|
| 38 |
+
"candidate_p95_ms": 5.232541996520013,
|
| 39 |
+
"reference_total_p50_ms": 3.6611664691008627,
|
| 40 |
+
"candidate_total_p50_ms": 3.6319584760349244
|
| 41 |
+
},
|
| 42 |
+
"paired_first_manifest_cpu_ane": {
|
| 43 |
+
"reference": "build/gliner2_base_classification_fp16_L128_K8.mlpackage",
|
| 44 |
+
"candidate": "build/gliner2_base_classification_embedding_w8_L128_K8.mlpackage",
|
| 45 |
+
"manifest": "../../gliner2-small/coreml/fixtures/optimization-selected20.jsonl",
|
| 46 |
+
"compute_units": "cpu-ane",
|
| 47 |
+
"processor": "arm",
|
| 48 |
+
"machine": "arm64",
|
| 49 |
+
"macos": "27.0",
|
| 50 |
+
"power_audit_after_run": "Now drawing from 'AC Power'\n -InternalBattery-0 (id=35651683)\t51%; charging; 1:33 remaining present: true",
|
| 51 |
+
"reference_load_ms": 3068.8863749965094,
|
| 52 |
+
"candidate_load_ms": 5926.175208995119,
|
| 53 |
+
"warmup_calls_per_model": 1,
|
| 54 |
+
"timed_order": "alternating reference-first/candidate-first by request",
|
| 55 |
+
"checked": 14,
|
| 56 |
+
"choice_agreement": 14,
|
| 57 |
+
"max_probability_error": 0.0068359375,
|
| 58 |
+
"probability_error_limit": 0.02,
|
| 59 |
+
"reference_p50_ms": 7.578062970424071,
|
| 60 |
+
"candidate_p50_ms": 7.721708010649309,
|
| 61 |
+
"reference_p95_ms": 8.475541020743549,
|
| 62 |
+
"candidate_p95_ms": 9.883625025395304,
|
| 63 |
+
"reference_total_p50_ms": 9.168104501441121,
|
| 64 |
+
"candidate_total_p50_ms": 9.666354482760653
|
| 65 |
+
},
|
| 66 |
+
"native_selected100": {
|
| 67 |
+
"model": "fastino/gliner2.5-base-v1",
|
| 68 |
+
"revision": "1a8bc24e00dc7300b9017c81d63e3dcdabb26596",
|
| 69 |
+
"package": "build/gliner2_base_classification_embedding_w8_L128_K8.mlpackage",
|
| 70 |
+
"selected_manifest": "first eligible rows in source suite order; no gold labels used",
|
| 71 |
+
"counts": {
|
| 72 |
+
"checked": 100,
|
| 73 |
+
"too_many_options": 300,
|
| 74 |
+
"too_long": 7,
|
| 75 |
+
"duplicate_labels": 0,
|
| 76 |
+
"mismatches": 0
|
| 77 |
+
},
|
| 78 |
+
"maximum_confidence_error": 0.013702988624572754,
|
| 79 |
+
"mean_confidence_error": 0.001015886664390564,
|
| 80 |
+
"model_call_p50_ms": 10.512874985579401,
|
| 81 |
+
"model_call_p95_ms": 14.00475000264123,
|
| 82 |
+
"failures": []
|
| 83 |
+
},
|
| 84 |
+
"compute_plan_forced_cpu_ane": {
|
| 85 |
+
"total_ops": 516,
|
| 86 |
+
"ane_ops": 496,
|
| 87 |
+
"cpu_ops": 20,
|
| 88 |
+
"ane_percent": 96.1,
|
| 89 |
+
"reasons": [
|
| 90 |
+
{
|
| 91 |
+
"reason": "Unsupported tensor data type: int32",
|
| 92 |
+
"count": 17,
|
| 93 |
+
"estimated_cpu_runtime_ms": 2.8975,
|
| 94 |
+
"op_types": {
|
| 95 |
+
"ios17.expand_dims": 5,
|
| 96 |
+
"ios17.cast": 5,
|
| 97 |
+
"ios17.add": 2,
|
| 98 |
+
"select": 2,
|
| 99 |
+
"ios17.gather": 1,
|
| 100 |
+
"ios17.mul": 1,
|
| 101 |
+
"tile": 1
|
| 102 |
+
},
|
| 103 |
+
"ops": [
|
| 104 |
+
"add_0",
|
| 105 |
+
"select_0",
|
| 106 |
+
"add_0_1",
|
| 107 |
+
"select_0_1",
|
| 108 |
+
"inputs_embeds_cast_fp16",
|
| 109 |
+
"mask_3",
|
| 110 |
+
"mask_3_to_fp16",
|
| 111 |
+
"var_86",
|
| 112 |
+
"extended_attention_mask",
|
| 113 |
+
"extended_attention_mask_to_uint16",
|
| 114 |
+
"var_88_cast_int16_to_int32",
|
| 115 |
+
"var_89",
|
| 116 |
+
"attention_mask_1",
|
| 117 |
+
"var_272",
|
| 118 |
+
"var_2537",
|
| 119 |
+
"indices",
|
| 120 |
+
"indices_to_int16"
|
| 121 |
+
]
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"reason": "Unable to resolve operation input \"y\".",
|
| 125 |
+
"count": 2,
|
| 126 |
+
"estimated_cpu_runtime_ms": 0.002,
|
| 127 |
+
"op_types": {
|
| 128 |
+
"ios17.greater_equal": 2
|
| 129 |
+
},
|
| 130 |
+
"ops": [
|
| 131 |
+
"greater_equal_0",
|
| 132 |
+
"greater_equal_0_1"
|
| 133 |
+
]
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"reason": "ANE not available for this op",
|
| 137 |
+
"count": 1,
|
| 138 |
+
"estimated_cpu_runtime_ms": 0,
|
| 139 |
+
"op_types": {
|
| 140 |
+
"ios16.constexpr_affine_dequantize": 1
|
| 141 |
+
},
|
| 142 |
+
"ops": [
|
| 143 |
+
"encoder_embeddings_word_embeddings_weight_to_fp16_quantized"
|
| 144 |
+
]
|
| 145 |
+
}
|
| 146 |
+
]
|
| 147 |
+
},
|
| 148 |
+
"scope": "Selected real classification requests only; no Decision Index or 2048 score; extraction packages are separate."
|
| 149 |
+
}
|
runtime.py
CHANGED
|
@@ -9,9 +9,12 @@ import numpy as np
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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)
|
|
@@ -28,9 +31,10 @@ def main():
|
|
| 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__":
|
|
|
|
| 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 |
+
precision: str = "fp16"):
|
| 14 |
model_dir = Path(model_dir)
|
| 15 |
+
if precision not in ("fp16", "embedding_w8"):
|
| 16 |
+
raise ValueError(f"unsupported classification precision: {precision}")
|
| 17 |
+
package = model_dir / f"gliner2_base_classification_{precision}_L{length}_K{max_options}.mlpackage"
|
| 18 |
processor = load_processor(str(model_dir / "tokenizer"))
|
| 19 |
arrays = prepare_with_processor(processor, text, task, labels, length, max_options)
|
| 20 |
model = ct.models.MLModel(str(package), compute_units=ct.ComputeUnit.ALL)
|
|
|
|
| 31 |
parser.add_argument("--labels", required=True, help="JSON list of label strings")
|
| 32 |
parser.add_argument("--length", type=int, default=128)
|
| 33 |
parser.add_argument("--max-options", type=int, default=8)
|
| 34 |
+
parser.add_argument("--precision", choices=["fp16", "embedding_w8"], default="fp16")
|
| 35 |
args = parser.parse_args()
|
| 36 |
print(json.dumps(classify(args.model_dir, args.text, args.task, json.loads(args.labels),
|
| 37 |
+
args.length, args.max_options, args.precision), indent=2))
|
| 38 |
|
| 39 |
|
| 40 |
if __name__ == "__main__":
|
verify-compression.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compare two GLiNER2 Core ML packages on selected real classification requests."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import json
|
| 7 |
+
import math
|
| 8 |
+
import platform
|
| 9 |
+
import statistics
|
| 10 |
+
import subprocess
|
| 11 |
+
import time
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
import coremltools as ct
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
from preprocessing import load_processor, prepare_with_processor
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def percentile(values: list[float], p: float) -> float | None:
|
| 21 |
+
if not values:
|
| 22 |
+
return None
|
| 23 |
+
return sorted(values)[max(0, min(len(values) - 1, math.ceil(p * len(values)) - 1))]
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def median(cases: list[dict], field: str) -> float | None:
|
| 27 |
+
return statistics.median(row[field] for row in cases) if cases else None
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def run() -> None:
|
| 31 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 32 |
+
parser.add_argument("--reference", type=Path, required=True)
|
| 33 |
+
parser.add_argument("--candidate", type=Path, required=True)
|
| 34 |
+
parser.add_argument("--tokenizer", type=Path, required=True)
|
| 35 |
+
parser.add_argument("--manifest", type=Path, required=True)
|
| 36 |
+
parser.add_argument("--out", type=Path, required=True)
|
| 37 |
+
parser.add_argument("--compute-units", choices=["all", "cpu-ane"], default="all")
|
| 38 |
+
parser.add_argument("--length", type=int, default=128)
|
| 39 |
+
parser.add_argument("--max-options", type=int, default=8)
|
| 40 |
+
parser.add_argument("--limit", type=int, default=None, help="stop after this many eligible requests")
|
| 41 |
+
parser.add_argument("--max-probability-error", type=float, default=0.02)
|
| 42 |
+
args = parser.parse_args()
|
| 43 |
+
|
| 44 |
+
units = ct.ComputeUnit.ALL if args.compute_units == "all" else ct.ComputeUnit.CPU_AND_NE
|
| 45 |
+
processor = load_processor(str(args.tokenizer))
|
| 46 |
+
load_start = time.perf_counter()
|
| 47 |
+
reference = ct.models.MLModel(str(args.reference), compute_units=units)
|
| 48 |
+
reference_load_ms = (time.perf_counter() - load_start) * 1000
|
| 49 |
+
load_start = time.perf_counter()
|
| 50 |
+
candidate = ct.models.MLModel(str(args.candidate), compute_units=units)
|
| 51 |
+
candidate_load_ms = (time.perf_counter() - load_start) * 1000
|
| 52 |
+
rows = [json.loads(line) for line in args.manifest.read_text().splitlines() if line]
|
| 53 |
+
prepared = []
|
| 54 |
+
skipped = []
|
| 55 |
+
for row in rows:
|
| 56 |
+
if args.limit is not None and len(prepared) >= args.limit:
|
| 57 |
+
break
|
| 58 |
+
labels = [(description or key).strip() for key, description in row["options"]]
|
| 59 |
+
prepare_start = time.perf_counter()
|
| 60 |
+
try:
|
| 61 |
+
arrays = prepare_with_processor(processor, row["state"], "decision", labels, args.length, args.max_options)
|
| 62 |
+
except ValueError as error:
|
| 63 |
+
skipped.append({"suite": row["suite"], "index": row["index"], "reason": str(error)})
|
| 64 |
+
continue
|
| 65 |
+
prepare_ms = (time.perf_counter() - prepare_start) * 1000
|
| 66 |
+
|
| 67 |
+
prepared.append((row, labels, arrays, prepare_ms))
|
| 68 |
+
|
| 69 |
+
if prepared:
|
| 70 |
+
for model in (reference, candidate):
|
| 71 |
+
model.predict(prepared[0][2])
|
| 72 |
+
|
| 73 |
+
checked = []
|
| 74 |
+
for index, (row, labels, arrays, prepare_ms) in enumerate(prepared):
|
| 75 |
+
outputs = {}
|
| 76 |
+
order = (("reference", reference), ("candidate", candidate))
|
| 77 |
+
if index % 2:
|
| 78 |
+
order = tuple(reversed(order))
|
| 79 |
+
for name, model in order:
|
| 80 |
+
start = time.perf_counter()
|
| 81 |
+
probabilities = np.asarray(model.predict(arrays)["probabilities"])[0, : len(labels)]
|
| 82 |
+
outputs[name] = (probabilities, (time.perf_counter() - start) * 1000)
|
| 83 |
+
expected, reference_ms = outputs["reference"]
|
| 84 |
+
actual, candidate_ms = outputs["candidate"]
|
| 85 |
+
checked.append({
|
| 86 |
+
"suite": row["suite"],
|
| 87 |
+
"index": row["index"],
|
| 88 |
+
"choice_agrees": int(expected.argmax()) == int(actual.argmax()),
|
| 89 |
+
"max_probability_error": float(np.max(np.abs(expected - actual))),
|
| 90 |
+
"prepare_ms": prepare_ms,
|
| 91 |
+
"reference_ms": reference_ms,
|
| 92 |
+
"candidate_ms": candidate_ms,
|
| 93 |
+
"reference_total_ms": prepare_ms + reference_ms,
|
| 94 |
+
"candidate_total_ms": prepare_ms + candidate_ms,
|
| 95 |
+
})
|
| 96 |
+
|
| 97 |
+
report = {
|
| 98 |
+
"reference": str(args.reference),
|
| 99 |
+
"candidate": str(args.candidate),
|
| 100 |
+
"manifest": str(args.manifest),
|
| 101 |
+
"compute_units": args.compute_units,
|
| 102 |
+
"processor": platform.processor(),
|
| 103 |
+
"machine": platform.machine(),
|
| 104 |
+
"macos": platform.mac_ver()[0],
|
| 105 |
+
"power_audit_after_run": subprocess.run(["pmset", "-g", "batt"], capture_output=True, text=True).stdout.strip(),
|
| 106 |
+
"reference_load_ms": reference_load_ms,
|
| 107 |
+
"candidate_load_ms": candidate_load_ms,
|
| 108 |
+
"warmup_calls_per_model": 1 if prepared else 0,
|
| 109 |
+
"timed_order": "alternating reference-first/candidate-first by request",
|
| 110 |
+
"checked": len(checked),
|
| 111 |
+
"skipped": skipped,
|
| 112 |
+
"choice_agreement": sum(row["choice_agrees"] for row in checked),
|
| 113 |
+
"max_probability_error": max((row["max_probability_error"] for row in checked), default=None),
|
| 114 |
+
"probability_error_limit": args.max_probability_error,
|
| 115 |
+
"reference_p50_ms": median(checked, "reference_ms"),
|
| 116 |
+
"candidate_p50_ms": median(checked, "candidate_ms"),
|
| 117 |
+
"reference_p95_ms": percentile([row["reference_ms"] for row in checked], 0.95),
|
| 118 |
+
"candidate_p95_ms": percentile([row["candidate_ms"] for row in checked], 0.95),
|
| 119 |
+
"reference_total_p50_ms": median(checked, "reference_total_ms"),
|
| 120 |
+
"candidate_total_p50_ms": median(checked, "candidate_total_ms"),
|
| 121 |
+
"cases": checked,
|
| 122 |
+
}
|
| 123 |
+
args.out.parent.mkdir(parents=True, exist_ok=True)
|
| 124 |
+
args.out.write_text(json.dumps(report, indent=2) + "\n")
|
| 125 |
+
summary = {key: value for key, value in report.items() if key not in ("cases", "skipped")}
|
| 126 |
+
summary["skipped_count"] = len(skipped)
|
| 127 |
+
print(json.dumps(summary, indent=2))
|
| 128 |
+
if not checked or report["choice_agreement"] != len(checked):
|
| 129 |
+
raise SystemExit("candidate changed a checked decision")
|
| 130 |
+
if report["max_probability_error"] > args.max_probability_error:
|
| 131 |
+
raise SystemExit("candidate exceeded the selected-request probability error limit")
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
if __name__ == "__main__":
|
| 135 |
+
run()
|