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 GLiNER2 model = GLiNER2.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 GLiNER2 extraction Core ML stages
Browse filesFP32 full extraction stages with selected native parity; base also includes FP16 with documented confidence drift. Classification artifacts remain available.
This view is limited to 50 files because it contains too many changes. See raw diff
- README.md +27 -23
- benchmark-extraction.py +76 -0
- config.json +99 -0
- convert-explicit-coreml.py +170 -0
- convert-extraction-coreml.py +217 -0
- convert-record-coreml.py +222 -0
- convert-relation-coreml.py +165 -0
- convert_extraction_names.py +31 -0
- extraction-assets.lock.json +275 -0
- extraction_export.py +408 -0
- extraction_pool.py +117 -0
- extraction_runtime.py +525 -0
- gliner2_base_explicit_fp16_W64_Q8_S64.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- gliner2_base_explicit_fp16_W64_Q8_S64.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- gliner2_base_explicit_fp16_W64_Q8_S64.mlpackage/Manifest.json +18 -0
- gliner2_base_explicit_fp32_W64_Q8_S64.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- gliner2_base_explicit_fp32_W64_Q8_S64.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- gliner2_base_explicit_fp32_W64_Q8_S64.mlpackage/Manifest.json +18 -0
- gliner2_base_extraction_features_fp16_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- gliner2_base_extraction_features_fp16_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- gliner2_base_extraction_features_fp16_L128_W64_Q8.mlpackage/Manifest.json +18 -0
- gliner2_base_extraction_features_fp32_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- gliner2_base_extraction_features_fp32_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- gliner2_base_extraction_features_fp32_L128_W64_Q8.mlpackage/Manifest.json +18 -0
- gliner2_base_extraction_scorer_fp16_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- gliner2_base_extraction_scorer_fp16_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- gliner2_base_extraction_scorer_fp16_L128_W64_Q8.mlpackage/Manifest.json +18 -0
- gliner2_base_extraction_scorer_fp32_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- gliner2_base_extraction_scorer_fp32_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- gliner2_base_extraction_scorer_fp32_L128_W64_Q8.mlpackage/Manifest.json +18 -0
- gliner2_base_record_anchorless_fp16_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- gliner2_base_record_anchorless_fp16_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- gliner2_base_record_anchorless_fp16_F8_C192_I1536.mlpackage/Manifest.json +18 -0
- gliner2_base_record_anchorless_fp32_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- gliner2_base_record_anchorless_fp32_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- gliner2_base_record_anchorless_fp32_F8_C192_I1536.mlpackage/Manifest.json +18 -0
- gliner2_base_record_assignment_fp16_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- gliner2_base_record_assignment_fp16_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- gliner2_base_record_assignment_fp16_F8_C192_I1536.mlpackage/Manifest.json +18 -0
- gliner2_base_record_assignment_fp32_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- gliner2_base_record_assignment_fp32_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- gliner2_base_record_assignment_fp32_F8_C192_I1536.mlpackage/Manifest.json +18 -0
- gliner2_base_relation_fp16_W64_R4_P256.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- gliner2_base_relation_fp16_W64_R4_P256.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- gliner2_base_relation_fp16_W64_R4_P256.mlpackage/Manifest.json +18 -0
- gliner2_base_relation_fp32_W64_R4_P256.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
- gliner2_base_relation_fp32_W64_R4_P256.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
- gliner2_base_relation_fp32_W64_R4_P256.mlpackage/Manifest.json +18 -0
- preprocessing.py +46 -0
- pyproject.toml +3 -1
README.md
CHANGED
|
@@ -1,40 +1,44 @@
|
|
| 1 |
---
|
| 2 |
license: apache-2.0
|
| 3 |
library_name: coremltools
|
| 4 |
-
pipeline_tag:
|
| 5 |
tags:
|
| 6 |
- coreml
|
| 7 |
- gliner2
|
| 8 |
- apple-silicon
|
| 9 |
---
|
| 10 |
|
| 11 |
-
# GLiNER2.5 base Core ML
|
| 12 |
|
| 13 |
-
Core ML
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
|
| 21 |
-
The L128/K8 package is 388,981,604 bytes, with up to eight labels. It targets
|
| 22 |
-
iOS 17/macOS 14. Included tokenizer and `preprocessing.py` reproduce native
|
| 23 |
-
schema rendering without loading the original PyTorch weights at runtime.
|
| 24 |
|
| 25 |
```bash
|
| 26 |
uv sync
|
| 27 |
-
uv run python
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
```
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
The
|
| 40 |
-
Fluid Inference converted the model; Fastino authored the original checkpoint.
|
|
|
|
| 1 |
---
|
| 2 |
license: apache-2.0
|
| 3 |
library_name: coremltools
|
| 4 |
+
pipeline_tag: token-classification
|
| 5 |
tags:
|
| 6 |
- coreml
|
| 7 |
- gliner2
|
| 8 |
- apple-silicon
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# GLiNER2.5 base for Core ML
|
| 12 |
|
| 13 |
+
This repository contains fixed-shape Core ML exports of the trained classification and extraction paths from [Fastino/gliner2.5-base-v1](https://huggingface.co/fastino/gliner2.5-base-v1) at revision `1a8bc24e00dc7300b9017c81d63e3dcdabb26596`. The original Apache-2.0 checkpoint has 193,581,591 parameters. Fastino authored the source model; Fluid Inference converted it. The model packages include the learned encoder, classification, boundary, relation, explicit-span, and record heads. The Python runtime keeps the source GLiNER2 schema, candidate selection, and decoder semantics.
|
| 14 |
+
|
| 15 |
+
## Extraction
|
| 16 |
+
|
| 17 |
+
The FP32 extraction stage packages support entities, relations, entity attributes, enum choices, natural/latent/anchorless records, and schemas mixed with classification. In a small fixed manifest of real text and schema fixtures, FP32 matched the native structured output on **11/11** cases. The largest FP32 confidence difference was 0.00000114. The multilingual manifest includes Spanish, French, Chinese, and German text. These checks are selected parity fixtures, not a Decision Index score or a full dataset evaluation.
|
| 18 |
+
|
| 19 |
+
FP16 result: 11/11 structures matched; the largest confidence difference was 0.1653 on a latent-record fixture. The base extraction FP16 packages are also included for speed sensitive applications; use FP32 when confidence values or latent-record decisions need closer native agreement.
|
| 20 |
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
```bash
|
| 23 |
uv sync
|
| 24 |
+
uv run python - <<'PYCODE'
|
| 25 |
+
from gliner2 import Schema
|
| 26 |
+
from extraction_runtime import CoreMLBoundaryExtractor
|
| 27 |
+
|
| 28 |
+
model = CoreMLBoundaryExtractor('.', precision='fp32')
|
| 29 |
+
schema = Schema().entities(['person', 'organization', 'location'])
|
| 30 |
+
print(model.extract('Alice founded Acme in Toronto.', schema, include_spans=True))
|
| 31 |
+
PYCODE
|
| 32 |
```
|
| 33 |
|
| 34 |
+
The extraction bucket holds up to 128 combined subword tokens, 64 text words, 8 extraction queries, and 8 classification choices. Candidate, explicit span, relation pair, and record capacities are fixed in the package names. Requests beyond the bucket raise `ValueError`; they require a larger exported bucket. The runtime uses the original tokenizer files and `gliner2==2.0.0`, but loads no PyTorch model weights. Run `verify-full-extraction.py` with the pinned native checkpoint for the selected parity check.
|
| 35 |
+
|
| 36 |
+
On an M5 Pro with macOS 27.0, FP32 end-to-end median 9.93 ms with All. FP16 median was 8.98 ms with All, 10.26 ms with CPU+Neural Engine, and 21.39 ms with CPU Only for a selected three-label entity request after 20 warmups and over 200 Python calls. Those are local end-to-end measurements for this shape, not ANE-only latency or a device-wide benchmark.
|
| 37 |
+
|
| 38 |
+
The feature graph compute plan assigned 53.13% of operations to ANE and 46.87% to CPU under CPU+Neural Engine on this machine. Per-tensor LUT8 compression reduced the FP16 feature package from 391 MB to 196 MB, but only 10/11 selected structures matched, so that compressed package is omitted.
|
| 39 |
+
|
| 40 |
+
## Classification
|
| 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.
|
|
|
benchmark-extraction.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Selected fixed-bucket end-to-end Core ML extraction latency on this Mac."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import platform
|
| 6 |
+
import statistics
|
| 7 |
+
import time
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
import coremltools as ct
|
| 11 |
+
import psutil
|
| 12 |
+
from gliner2 import Schema
|
| 13 |
+
|
| 14 |
+
from extraction_runtime import CoreMLBoundaryExtractor
|
| 15 |
+
|
| 16 |
+
UNITS = {
|
| 17 |
+
"cpu_only": ct.ComputeUnit.CPU_ONLY,
|
| 18 |
+
"cpu_and_gpu": ct.ComputeUnit.CPU_AND_GPU,
|
| 19 |
+
"cpu_and_neural_engine": ct.ComputeUnit.CPU_AND_NE,
|
| 20 |
+
"all": ct.ComputeUnit.ALL,
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def percentile(values, fraction):
|
| 25 |
+
ordered = sorted(values)
|
| 26 |
+
return ordered[min(round(fraction * (len(ordered) - 1)), len(ordered) - 1)]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def main():
|
| 30 |
+
parser = argparse.ArgumentParser()
|
| 31 |
+
parser.add_argument("--model-dir", required=True)
|
| 32 |
+
parser.add_argument("--precision", choices=["fp16", "fp32"], default="fp32")
|
| 33 |
+
parser.add_argument("--units", choices=list(UNITS), default="all")
|
| 34 |
+
parser.add_argument("--warmup", type=int, default=20)
|
| 35 |
+
parser.add_argument("--iterations", type=int, default=200)
|
| 36 |
+
args = parser.parse_args()
|
| 37 |
+
text = "Alice founded Acme in Toronto in 2020."
|
| 38 |
+
schema = Schema().entities(["person", "organization", "location"])
|
| 39 |
+
started = time.perf_counter()
|
| 40 |
+
runtime = CoreMLBoundaryExtractor(args.model_dir, precision=args.precision, compute_units=UNITS[args.units])
|
| 41 |
+
load_ms = (time.perf_counter() - started) * 1000
|
| 42 |
+
for _ in range(args.warmup):
|
| 43 |
+
runtime.extract(text, schema)
|
| 44 |
+
process = psutil.Process()
|
| 45 |
+
latencies = []
|
| 46 |
+
peak_rss = process.memory_info().rss
|
| 47 |
+
for _ in range(args.iterations):
|
| 48 |
+
start = time.perf_counter()
|
| 49 |
+
runtime.extract(text, schema)
|
| 50 |
+
latencies.append((time.perf_counter() - start) * 1000)
|
| 51 |
+
peak_rss = max(peak_rss, process.memory_info().rss)
|
| 52 |
+
report = {
|
| 53 |
+
"purpose": "selected end-to-end entity extraction latency, no benchmark scoring",
|
| 54 |
+
"fixture": text,
|
| 55 |
+
"shape": "L128/W64/Q8/C192",
|
| 56 |
+
"precision": args.precision,
|
| 57 |
+
"compute_units": args.units,
|
| 58 |
+
"warmup": args.warmup,
|
| 59 |
+
"iterations": args.iterations,
|
| 60 |
+
"load_ms": load_ms,
|
| 61 |
+
"p50_ms": statistics.median(latencies),
|
| 62 |
+
"p95_ms": percentile(latencies, 0.95),
|
| 63 |
+
"mean_ms": statistics.mean(latencies),
|
| 64 |
+
"peak_process_rss_bytes": peak_rss,
|
| 65 |
+
"macos": platform.mac_ver()[0],
|
| 66 |
+
"machine": platform.machine(),
|
| 67 |
+
"coremltools": ct.__version__,
|
| 68 |
+
}
|
| 69 |
+
folder = Path(args.model_dir)
|
| 70 |
+
path = folder / f"benchmark-{args.precision}-{args.units}.json"
|
| 71 |
+
path.write_text(json.dumps(report, indent=2) + "\n")
|
| 72 |
+
print(json.dumps(report, indent=2))
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
if __name__ == "__main__":
|
| 76 |
+
main()
|
config.json
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architecture": "boundary",
|
| 3 |
+
"architecture_version": 1,
|
| 4 |
+
"architectures": [
|
| 5 |
+
"BoundaryExtractor"
|
| 6 |
+
],
|
| 7 |
+
"attn_implementation": "sdpa",
|
| 8 |
+
"boundary_head": {
|
| 9 |
+
"abstention_loss_weight": 0.2,
|
| 10 |
+
"abstention_threshold": 0.5,
|
| 11 |
+
"adaptive_threshold": false,
|
| 12 |
+
"bidirectional_proposals": true,
|
| 13 |
+
"boundary_attention_heads": 4,
|
| 14 |
+
"boundary_attention_layers": 2,
|
| 15 |
+
"boundary_attention_window": 128,
|
| 16 |
+
"boundary_dim": 128,
|
| 17 |
+
"boundary_ffn_multiplier": 2.0,
|
| 18 |
+
"boundary_focal_clip": 0.05,
|
| 19 |
+
"boundary_focal_gamma_negative": 2.0,
|
| 20 |
+
"boundary_focal_gamma_positive": 0.0,
|
| 21 |
+
"boundary_marginal_loss": "asymmetric_focal",
|
| 22 |
+
"boundary_negative_weight": 0.5,
|
| 23 |
+
"boundary_refinement_layers": 1,
|
| 24 |
+
"boundary_top_k_alpha": 0.08,
|
| 25 |
+
"boundary_top_k_bucket": 8,
|
| 26 |
+
"boundary_top_k_max": 128,
|
| 27 |
+
"candidate_attention_heads": 4,
|
| 28 |
+
"candidate_attention_layers": 0,
|
| 29 |
+
"candidate_budget": 192,
|
| 30 |
+
"candidate_pool": "shared",
|
| 31 |
+
"classification_loss_weight": 1.0,
|
| 32 |
+
"classification_temperature": 1.0,
|
| 33 |
+
"consistency_loss_weight": 0.1,
|
| 34 |
+
"consistency_warmup_steps": 2000,
|
| 35 |
+
"content_dim": 64,
|
| 36 |
+
"content_soft_max_pool": false,
|
| 37 |
+
"count_loss_weight": 0.2,
|
| 38 |
+
"directional_relation_states": true,
|
| 39 |
+
"dropout": 0.1,
|
| 40 |
+
"enable_abstention": true,
|
| 41 |
+
"enable_count_head": true,
|
| 42 |
+
"enable_records": true,
|
| 43 |
+
"enable_relations": true,
|
| 44 |
+
"enable_rotary_endpoints": true,
|
| 45 |
+
"enable_span_content": true,
|
| 46 |
+
"end_block_size": 256,
|
| 47 |
+
"end_top_k": 24,
|
| 48 |
+
"endpoint_difference_features": true,
|
| 49 |
+
"ends_per_start": 12,
|
| 50 |
+
"export_mode": "auto",
|
| 51 |
+
"hard_negative_keep_all_when_absent": true,
|
| 52 |
+
"hard_negatives_per_positive": 20,
|
| 53 |
+
"loss_reduction": "sum",
|
| 54 |
+
"max_gold_per_query": 64,
|
| 55 |
+
"max_negative_queries_per_batch": 64,
|
| 56 |
+
"min_pool_per_query": 8,
|
| 57 |
+
"minimum_hard_negatives": 16,
|
| 58 |
+
"multihead_pair_compat_heads": 8,
|
| 59 |
+
"negative_query_ratio": 1.0,
|
| 60 |
+
"overlap_policy": "flat",
|
| 61 |
+
"pair_dim": 128,
|
| 62 |
+
"pair_temperature": 1.0,
|
| 63 |
+
"pool_boundary_top_k": 32,
|
| 64 |
+
"pool_size": 192,
|
| 65 |
+
"proposal_loss_weight": 0.3,
|
| 66 |
+
"query_attention_layers": 0,
|
| 67 |
+
"query_conditioned_inside_weight": true,
|
| 68 |
+
"record_anchor_proposal_threshold": 0.2,
|
| 69 |
+
"record_anchor_threshold": 0.5,
|
| 70 |
+
"record_dim": 128,
|
| 71 |
+
"record_field_threshold": 0.5,
|
| 72 |
+
"record_instance_queries": 32,
|
| 73 |
+
"record_loss_weight": 1.0,
|
| 74 |
+
"record_temperature": 1.0,
|
| 75 |
+
"relation_argument_proposal_threshold": 0.2,
|
| 76 |
+
"relation_biaffine_content": true,
|
| 77 |
+
"relation_heads_per_type": 32,
|
| 78 |
+
"relation_loss_weight": 1.0,
|
| 79 |
+
"relation_pair_cap": 64,
|
| 80 |
+
"relation_tails_per_type": 32,
|
| 81 |
+
"relation_temperature": 1.0,
|
| 82 |
+
"rerank_listwise_weight": 0.3,
|
| 83 |
+
"reranker_endpoint_compat": true,
|
| 84 |
+
"rotary_base": 10000.0,
|
| 85 |
+
"soft_iou_anneal_steps": 20000,
|
| 86 |
+
"soft_iou_aux_weight": 0.2,
|
| 87 |
+
"start_top_k": 24,
|
| 88 |
+
"starts_per_end": 12,
|
| 89 |
+
"training_candidate_budget": 192,
|
| 90 |
+
"use_inside_evidence": true,
|
| 91 |
+
"vectorized_pair_elements": 16777216
|
| 92 |
+
},
|
| 93 |
+
"config_version": 3,
|
| 94 |
+
"max_len": 4096,
|
| 95 |
+
"model_name": "microsoft/deberta-v3-base",
|
| 96 |
+
"model_type": "extractor",
|
| 97 |
+
"token_pooling": "first",
|
| 98 |
+
"transformers_version": "5.8.0"
|
| 99 |
+
}
|
convert-explicit-coreml.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Export GLiNER2.5 base trained explicit-span scorer for attributes and enums."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import coremltools as ct
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
from gliner2 import AutoExtractor, Schema
|
| 11 |
+
from huggingface_hub import snapshot_download
|
| 12 |
+
|
| 13 |
+
from extraction_export import ExtractionExplicitSpanExport, ExtractionFeaturesExport, coreml_trace_patches
|
| 14 |
+
from preprocessing import prepare_extraction
|
| 15 |
+
|
| 16 |
+
MODEL_ID = "fastino/gliner2.5-base-v1"
|
| 17 |
+
MODEL_REVISION = "1a8bc24e00dc7300b9017c81d63e3dcdabb26596"
|
| 18 |
+
INPUT_NAMES = (
|
| 19 |
+
"text_states",
|
| 20 |
+
"text_mask",
|
| 21 |
+
"query_states",
|
| 22 |
+
"query_mask",
|
| 23 |
+
"boundary_states",
|
| 24 |
+
"start_logits",
|
| 25 |
+
"end_logits",
|
| 26 |
+
"inside_prefix",
|
| 27 |
+
"inside_prefix_mean",
|
| 28 |
+
"span_indices",
|
| 29 |
+
"span_mask",
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def main():
|
| 34 |
+
parser = argparse.ArgumentParser()
|
| 35 |
+
parser.add_argument("--output-dir", default="build/extraction")
|
| 36 |
+
parser.add_argument("--precision", choices=["fp16", "fp32"], default="fp32")
|
| 37 |
+
parser.add_argument("--length", type=int, default=128)
|
| 38 |
+
parser.add_argument("--max-words", type=int, default=64)
|
| 39 |
+
parser.add_argument("--max-queries", type=int, default=8)
|
| 40 |
+
parser.add_argument("--max-spans", type=int, default=64)
|
| 41 |
+
args = parser.parse_args()
|
| 42 |
+
torch.set_num_threads(4)
|
| 43 |
+
source = snapshot_download(
|
| 44 |
+
MODEL_ID,
|
| 45 |
+
revision=MODEL_REVISION,
|
| 46 |
+
allow_patterns=[
|
| 47 |
+
"config.json",
|
| 48 |
+
"encoder_config/*",
|
| 49 |
+
"model.safetensors",
|
| 50 |
+
"tokenizer.json",
|
| 51 |
+
"tokenizer_config.json",
|
| 52 |
+
],
|
| 53 |
+
)
|
| 54 |
+
native = AutoExtractor.from_pretrained(source, map_location="cpu").eval()
|
| 55 |
+
text = "Alice founded Acme in Toronto in 2020."
|
| 56 |
+
schema = Schema().entities(["person", "organization", "location"])
|
| 57 |
+
arrays, batch = prepare_extraction(native.processor, text, schema, args.length, args.max_words, args.max_queries)
|
| 58 |
+
tensors = tuple(torch.from_numpy(value) for value in arrays.values())
|
| 59 |
+
with torch.no_grad(), coreml_trace_patches():
|
| 60 |
+
features = ExtractionFeaturesExport(native).eval()(*tensors)
|
| 61 |
+
with torch.no_grad():
|
| 62 |
+
core = native._encode_core(batch)
|
| 63 |
+
candidates = native.boundary_head(
|
| 64 |
+
core["text_states"], core["text_mask"], core["query_states"], core["query_mask"]
|
| 65 |
+
).candidates
|
| 66 |
+
query_count = core["query_states"].shape[1]
|
| 67 |
+
indices = torch.zeros(1, args.max_queries, args.max_spans, 2, dtype=torch.int32)
|
| 68 |
+
mask = torch.zeros(1, args.max_queries, args.max_spans, dtype=torch.float32)
|
| 69 |
+
count = min(args.max_spans, candidates.indices.shape[2])
|
| 70 |
+
indices[:, :query_count, :count] = candidates.indices[:, :query_count, :count].int()
|
| 71 |
+
mask[:, :query_count, :count] = candidates.valid_mask[:, :query_count, :count].float()
|
| 72 |
+
wrapper = ExtractionExplicitSpanExport(native).eval()
|
| 73 |
+
arguments = (
|
| 74 |
+
features[0],
|
| 75 |
+
tensors[3],
|
| 76 |
+
features[1],
|
| 77 |
+
tensors[5],
|
| 78 |
+
features[2],
|
| 79 |
+
features[4],
|
| 80 |
+
features[5],
|
| 81 |
+
features[6],
|
| 82 |
+
features[7],
|
| 83 |
+
indices,
|
| 84 |
+
mask,
|
| 85 |
+
)
|
| 86 |
+
with torch.no_grad():
|
| 87 |
+
reference = wrapper(*arguments)
|
| 88 |
+
native_reference = native.boundary_head.score_explicit_spans(
|
| 89 |
+
core["text_states"],
|
| 90 |
+
core["text_mask"],
|
| 91 |
+
core["query_states"],
|
| 92 |
+
core["query_mask"],
|
| 93 |
+
indices[:, :query_count].long(),
|
| 94 |
+
mask[:, :query_count].bool(),
|
| 95 |
+
)
|
| 96 |
+
wrapper_error = float(
|
| 97 |
+
(reference[:, :query_count][mask[:, :query_count].bool()] - native_reference[mask[:, :query_count].bool()])
|
| 98 |
+
.abs()
|
| 99 |
+
.max()
|
| 100 |
+
)
|
| 101 |
+
traced = torch.jit.trace(wrapper, arguments, check_trace=False)
|
| 102 |
+
if wrapper_error > 1e-4:
|
| 103 |
+
raise RuntimeError(f"Explicit span wrapper differs from native: {wrapper_error}")
|
| 104 |
+
precision = ct.precision.FLOAT16 if args.precision == "fp16" else ct.precision.FLOAT32
|
| 105 |
+
converted = ct.convert(
|
| 106 |
+
traced,
|
| 107 |
+
convert_to="mlprogram",
|
| 108 |
+
minimum_deployment_target=ct.target.iOS17,
|
| 109 |
+
compute_precision=precision,
|
| 110 |
+
compute_units=ct.ComputeUnit.CPU_ONLY,
|
| 111 |
+
inputs=[
|
| 112 |
+
ct.TensorType(name=name, shape=tuple(value.shape), dtype=np.int32 if name == "span_indices" else np.float32)
|
| 113 |
+
for name, value in zip(INPUT_NAMES, arguments)
|
| 114 |
+
],
|
| 115 |
+
outputs=[ct.TensorType(name="span_logits", dtype=np.float32)],
|
| 116 |
+
)
|
| 117 |
+
converted.short_description = "GLiNER2.5 base trained explicit-span extraction scorer"
|
| 118 |
+
converted.author = "Fastino (original); Fluid Inference (Core ML conversion)"
|
| 119 |
+
converted.license = "Apache-2.0"
|
| 120 |
+
converted.user_defined_metadata.update(
|
| 121 |
+
{
|
| 122 |
+
"source_model": MODEL_ID,
|
| 123 |
+
"source_revision": MODEL_REVISION,
|
| 124 |
+
"stage": "trained explicit-span proposal and reranker",
|
| 125 |
+
"word_capacity": str(args.max_words),
|
| 126 |
+
"query_capacity": str(args.max_queries),
|
| 127 |
+
"span_capacity": str(args.max_spans),
|
| 128 |
+
}
|
| 129 |
+
)
|
| 130 |
+
out = Path(args.output_dir)
|
| 131 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 132 |
+
suffix = f"{args.precision}_W{args.max_words}_Q{args.max_queries}_S{args.max_spans}"
|
| 133 |
+
package = out / f"gliner2_base_explicit_{suffix}.mlpackage"
|
| 134 |
+
converted.save(str(package))
|
| 135 |
+
runtime = ct.models.MLModel(str(package), compute_units=ct.ComputeUnit.CPU_ONLY)
|
| 136 |
+
predicted = runtime.predict(
|
| 137 |
+
{
|
| 138 |
+
name: value.detach().numpy().astype(np.int32 if name == "span_indices" else np.float32)
|
| 139 |
+
for name, value in zip(INPUT_NAMES, arguments)
|
| 140 |
+
}
|
| 141 |
+
)["span_logits"]
|
| 142 |
+
runtime_error = float(
|
| 143 |
+
np.max(
|
| 144 |
+
np.abs(
|
| 145 |
+
predicted[:, :query_count][mask[:, :query_count].bool().numpy()]
|
| 146 |
+
- reference.numpy()[:, :query_count][mask[:, :query_count].bool().numpy()]
|
| 147 |
+
)
|
| 148 |
+
)
|
| 149 |
+
)
|
| 150 |
+
if not np.isfinite(runtime_error):
|
| 151 |
+
raise RuntimeError("Explicit-span scorer produced non-finite logits")
|
| 152 |
+
report = {
|
| 153 |
+
"source_model": MODEL_ID,
|
| 154 |
+
"source_revision": MODEL_REVISION,
|
| 155 |
+
"precision": args.precision,
|
| 156 |
+
"fixture": text,
|
| 157 |
+
"valid_spans": int(mask.sum()),
|
| 158 |
+
"wrapper_max_absolute_error": wrapper_error,
|
| 159 |
+
"coreml_max_absolute_error": runtime_error,
|
| 160 |
+
"package": str(package),
|
| 161 |
+
"package_bytes": sum(file.stat().st_size for file in package.rglob("*") if file.is_file()),
|
| 162 |
+
"coremltools": ct.__version__,
|
| 163 |
+
"torch": torch.__version__,
|
| 164 |
+
}
|
| 165 |
+
(out / f"explicit-{suffix}.json").write_text(json.dumps(report, indent=2) + "\n")
|
| 166 |
+
print(json.dumps(report, indent=2))
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
if __name__ == "__main__":
|
| 170 |
+
main()
|
convert-extraction-coreml.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Export the trained GLiNER2.5 base boundary extraction stages to Core ML."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import shutil
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import coremltools as ct
|
| 9 |
+
import numpy as np
|
| 10 |
+
import torch
|
| 11 |
+
from gliner2 import AutoExtractor, Schema
|
| 12 |
+
from huggingface_hub import snapshot_download
|
| 13 |
+
|
| 14 |
+
from convert_extraction_names import FEATURE_NAMES, SCORE_INPUT_NAMES
|
| 15 |
+
from extraction_export import ExtractionFeaturesExport, ExtractionScoreExport, coreml_trace_patches
|
| 16 |
+
from extraction_pool import select_candidates
|
| 17 |
+
from preprocessing import prepare_extraction
|
| 18 |
+
|
| 19 |
+
MODEL_ID = "fastino/gliner2.5-base-v1"
|
| 20 |
+
MODEL_REVISION = "1a8bc24e00dc7300b9017c81d63e3dcdabb26596"
|
| 21 |
+
FIXTURE_TEXT = "Alice founded Acme in Toronto in 2020."
|
| 22 |
+
FIXTURE_SCHEMA = Schema().entities(["person", "organization", "location"])
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def package_bytes(path: Path) -> int:
|
| 26 |
+
return sum(file.stat().st_size for file in path.rglob("*") if file.is_file())
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def main():
|
| 30 |
+
parser = argparse.ArgumentParser()
|
| 31 |
+
parser.add_argument("--output-dir", default="build/extraction")
|
| 32 |
+
parser.add_argument("--length", type=int, default=128, help="Subword capacity")
|
| 33 |
+
parser.add_argument("--max-words", type=int, default=64)
|
| 34 |
+
parser.add_argument("--max-queries", type=int, default=8)
|
| 35 |
+
parser.add_argument("--precision", choices=["fp16", "fp32"], default="fp32")
|
| 36 |
+
args = parser.parse_args()
|
| 37 |
+
torch.set_num_threads(4)
|
| 38 |
+
source = snapshot_download(
|
| 39 |
+
MODEL_ID,
|
| 40 |
+
revision=MODEL_REVISION,
|
| 41 |
+
allow_patterns=[
|
| 42 |
+
"config.json",
|
| 43 |
+
"encoder_config/*",
|
| 44 |
+
"model.safetensors",
|
| 45 |
+
"tokenizer.json",
|
| 46 |
+
"tokenizer_config.json",
|
| 47 |
+
],
|
| 48 |
+
)
|
| 49 |
+
native = AutoExtractor.from_pretrained(source, map_location="cpu").eval()
|
| 50 |
+
arrays, batch = prepare_extraction(
|
| 51 |
+
native.processor, FIXTURE_TEXT, FIXTURE_SCHEMA, args.length, args.max_words, args.max_queries
|
| 52 |
+
)
|
| 53 |
+
tensors = tuple(torch.from_numpy(value) for value in arrays.values())
|
| 54 |
+
features_wrapper = ExtractionFeaturesExport(native).eval()
|
| 55 |
+
with torch.no_grad(), coreml_trace_patches():
|
| 56 |
+
features_reference = features_wrapper(*tensors)
|
| 57 |
+
traced_features = torch.jit.trace(features_wrapper, tensors, check_trace=False)
|
| 58 |
+
with torch.no_grad():
|
| 59 |
+
native_core = native._encode_core(batch)
|
| 60 |
+
valid_words = native_core["text_states"].shape[1]
|
| 61 |
+
valid_queries = native_core["query_states"].shape[1]
|
| 62 |
+
routing_error = max(
|
| 63 |
+
float((features_reference[0][:, :valid_words] - native_core["text_states"]).abs().max()),
|
| 64 |
+
float((features_reference[1][:, :valid_queries] - native_core["query_states"]).abs().max()),
|
| 65 |
+
)
|
| 66 |
+
if routing_error > 1e-4:
|
| 67 |
+
raise RuntimeError(f"Traced routing differs from native: {routing_error}")
|
| 68 |
+
|
| 69 |
+
precision = ct.precision.FLOAT16 if args.precision == "fp16" else ct.precision.FLOAT32
|
| 70 |
+
input_names = tuple(arrays)
|
| 71 |
+
features_model = ct.convert(
|
| 72 |
+
traced_features,
|
| 73 |
+
convert_to="mlprogram",
|
| 74 |
+
minimum_deployment_target=ct.target.iOS17,
|
| 75 |
+
compute_precision=precision,
|
| 76 |
+
compute_units=ct.ComputeUnit.CPU_ONLY,
|
| 77 |
+
inputs=[ct.TensorType(name=name, shape=arrays[name].shape, dtype=arrays[name].dtype) for name in input_names],
|
| 78 |
+
outputs=[ct.TensorType(name=name, dtype=np.float32) for name in FEATURE_NAMES],
|
| 79 |
+
)
|
| 80 |
+
features_model.short_description = "GLiNER2.5 base trained boundary extraction features"
|
| 81 |
+
features_model.author = "Fastino (original); Fluid Inference (Core ML conversion)"
|
| 82 |
+
features_model.license = "Apache-2.0"
|
| 83 |
+
features_model.user_defined_metadata.update(
|
| 84 |
+
{
|
| 85 |
+
"source_model": MODEL_ID,
|
| 86 |
+
"source_revision": MODEL_REVISION,
|
| 87 |
+
"stage": "extraction features and trained boundary heads",
|
| 88 |
+
"subword_capacity": str(args.length),
|
| 89 |
+
"word_capacity": str(args.max_words),
|
| 90 |
+
"query_capacity": str(args.max_queries),
|
| 91 |
+
}
|
| 92 |
+
)
|
| 93 |
+
out = Path(args.output_dir)
|
| 94 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 95 |
+
shutil.copy2(Path(source) / "config.json", out / "config.json")
|
| 96 |
+
tokenizer_dir = out / "tokenizer"
|
| 97 |
+
tokenizer_dir.mkdir(exist_ok=True)
|
| 98 |
+
for name in ("tokenizer.json", "tokenizer_config.json"):
|
| 99 |
+
shutil.copy2(Path(source) / name, tokenizer_dir / name)
|
| 100 |
+
suffix = f"{args.precision}_L{args.length}_W{args.max_words}_Q{args.max_queries}"
|
| 101 |
+
features_path = out / f"gliner2_base_extraction_features_{suffix}.mlpackage"
|
| 102 |
+
if features_path.exists():
|
| 103 |
+
shutil.rmtree(features_path)
|
| 104 |
+
features_model.save(str(features_path))
|
| 105 |
+
print(f"Saved {features_path}", flush=True)
|
| 106 |
+
|
| 107 |
+
head = native.boundary_head
|
| 108 |
+
pooled = select_candidates(
|
| 109 |
+
features_reference[8],
|
| 110 |
+
features_reference[9],
|
| 111 |
+
features_reference[3].bool(),
|
| 112 |
+
tensors[5].bool(),
|
| 113 |
+
features_reference[4],
|
| 114 |
+
features_reference[5],
|
| 115 |
+
boundary_top_k=head.shared_pool_builder.pool_boundary_top_k,
|
| 116 |
+
pool_size=head.shared_pool_builder.pool_size,
|
| 117 |
+
min_pool_per_query=head.shared_pool_builder.min_pool_per_query,
|
| 118 |
+
)
|
| 119 |
+
score_tensors = (
|
| 120 |
+
features_reference[0],
|
| 121 |
+
tensors[3],
|
| 122 |
+
features_reference[1],
|
| 123 |
+
tensors[5],
|
| 124 |
+
features_reference[2],
|
| 125 |
+
features_reference[4],
|
| 126 |
+
features_reference[5],
|
| 127 |
+
features_reference[6],
|
| 128 |
+
features_reference[7],
|
| 129 |
+
pooled.indices.int(),
|
| 130 |
+
pooled.mask.float(),
|
| 131 |
+
pooled.compat_logits,
|
| 132 |
+
)
|
| 133 |
+
scorer_wrapper = ExtractionScoreExport(native).eval()
|
| 134 |
+
with torch.no_grad():
|
| 135 |
+
scores_reference = scorer_wrapper(*score_tensors)
|
| 136 |
+
traced_scores = torch.jit.trace(scorer_wrapper, score_tensors, check_trace=False)
|
| 137 |
+
scorer_model = ct.convert(
|
| 138 |
+
traced_scores,
|
| 139 |
+
convert_to="mlprogram",
|
| 140 |
+
minimum_deployment_target=ct.target.iOS17,
|
| 141 |
+
compute_precision=precision,
|
| 142 |
+
compute_units=ct.ComputeUnit.CPU_ONLY,
|
| 143 |
+
inputs=[
|
| 144 |
+
ct.TensorType(
|
| 145 |
+
name=name, shape=tuple(value.shape), dtype=np.int32 if name == "candidate_indices" else np.float32
|
| 146 |
+
)
|
| 147 |
+
for name, value in zip(SCORE_INPUT_NAMES, score_tensors)
|
| 148 |
+
],
|
| 149 |
+
outputs=[
|
| 150 |
+
ct.TensorType(name="pair_logits", dtype=np.float32),
|
| 151 |
+
ct.TensorType(name="candidate_states", dtype=np.float32),
|
| 152 |
+
],
|
| 153 |
+
)
|
| 154 |
+
scorer_model.short_description = "GLiNER2.5 base trained shared-pool extraction scorer"
|
| 155 |
+
scorer_model.author = "Fastino (original); Fluid Inference (Core ML conversion)"
|
| 156 |
+
scorer_model.license = "Apache-2.0"
|
| 157 |
+
scorer_model.user_defined_metadata.update(
|
| 158 |
+
{
|
| 159 |
+
"source_model": MODEL_ID,
|
| 160 |
+
"source_revision": MODEL_REVISION,
|
| 161 |
+
"stage": "trained extraction candidate scorer",
|
| 162 |
+
"candidate_capacity": str(head.shared_pool_builder.pool_size),
|
| 163 |
+
}
|
| 164 |
+
)
|
| 165 |
+
scorer_path = out / f"gliner2_base_extraction_scorer_{suffix}.mlpackage"
|
| 166 |
+
if scorer_path.exists():
|
| 167 |
+
shutil.rmtree(scorer_path)
|
| 168 |
+
scorer_model.save(str(scorer_path))
|
| 169 |
+
print(f"Saved {scorer_path}", flush=True)
|
| 170 |
+
|
| 171 |
+
# The first runtime check uses the same selected real fixture as the trace.
|
| 172 |
+
runtime_features = ct.models.MLModel(str(features_path), compute_units=ct.ComputeUnit.CPU_ONLY)
|
| 173 |
+
predicted_features = runtime_features.predict(arrays)
|
| 174 |
+
errors = {
|
| 175 |
+
name: float(np.max(np.abs(np.asarray(predicted_features[name]) - reference.detach().numpy())))
|
| 176 |
+
for name, reference in zip(FEATURE_NAMES, features_reference)
|
| 177 |
+
}
|
| 178 |
+
if any(not np.isfinite(value) for value in errors.values()):
|
| 179 |
+
raise RuntimeError("Extraction features contain non-finite values")
|
| 180 |
+
runtime_scorer = ct.models.MLModel(str(scorer_path), compute_units=ct.ComputeUnit.CPU_ONLY)
|
| 181 |
+
score_arrays = {
|
| 182 |
+
name: value.detach().numpy().astype(np.int32 if name == "candidate_indices" else np.float32)
|
| 183 |
+
for name, value in zip(SCORE_INPUT_NAMES, score_tensors)
|
| 184 |
+
}
|
| 185 |
+
predicted_scores = runtime_scorer.predict(score_arrays)
|
| 186 |
+
errors["pair_logits"] = float(np.max(np.abs(predicted_scores["pair_logits"] - scores_reference[0].numpy())))
|
| 187 |
+
errors["candidate_states"] = float(
|
| 188 |
+
np.max(np.abs(predicted_scores["candidate_states"] - scores_reference[1].numpy()))
|
| 189 |
+
)
|
| 190 |
+
if any(not np.isfinite(value) for value in errors.values()):
|
| 191 |
+
raise RuntimeError("Extraction scorer contains non-finite values")
|
| 192 |
+
report = {
|
| 193 |
+
"source_model": MODEL_ID,
|
| 194 |
+
"source_revision": MODEL_REVISION,
|
| 195 |
+
"precision": args.precision,
|
| 196 |
+
"fixture": FIXTURE_TEXT,
|
| 197 |
+
"shape": {
|
| 198 |
+
"subwords": args.length,
|
| 199 |
+
"words": args.max_words,
|
| 200 |
+
"queries": args.max_queries,
|
| 201 |
+
"candidates": head.shared_pool_builder.pool_size,
|
| 202 |
+
},
|
| 203 |
+
"routing_max_absolute_error": routing_error,
|
| 204 |
+
"runtime_max_absolute_errors": errors,
|
| 205 |
+
"packages": {
|
| 206 |
+
"features": {"path": str(features_path), "bytes": package_bytes(features_path)},
|
| 207 |
+
"scorer": {"path": str(scorer_path), "bytes": package_bytes(scorer_path)},
|
| 208 |
+
},
|
| 209 |
+
"coremltools": ct.__version__,
|
| 210 |
+
"torch": torch.__version__,
|
| 211 |
+
}
|
| 212 |
+
(out / f"conversion-{suffix}.json").write_text(json.dumps(report, indent=2) + "\n")
|
| 213 |
+
print(json.dumps(report, indent=2))
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
if __name__ == "__main__":
|
| 217 |
+
main()
|
convert-record-coreml.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Export GLiNER2.5 base trained record assignment and anchorless heads."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import shutil
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import coremltools as ct
|
| 9 |
+
import numpy as np
|
| 10 |
+
import torch
|
| 11 |
+
from gliner2 import AutoExtractor, Schema
|
| 12 |
+
from gliner2.training.trainer import ExtractorCollator
|
| 13 |
+
from huggingface_hub import snapshot_download
|
| 14 |
+
|
| 15 |
+
from extraction_export import ExtractionRecordAnchorlessExport, ExtractionRecordAssignmentExport
|
| 16 |
+
|
| 17 |
+
MODEL_ID = "fastino/gliner2.5-base-v1"
|
| 18 |
+
MODEL_REVISION = "1a8bc24e00dc7300b9017c81d63e3dcdabb26596"
|
| 19 |
+
FIXTURE_TEXT = "Alice works at Acme. Bob works at Beta."
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def record_fixture(native, mode):
|
| 23 |
+
schema = Schema()
|
| 24 |
+
builder = schema.structure("employment", mode=mode, anchor="person" if mode == "natural" else None)
|
| 25 |
+
builder.field("person", dtype="str")
|
| 26 |
+
builder.field("company", dtype="str")
|
| 27 |
+
batch = ExtractorCollator(native.processor, is_training=False, max_len=None, architecture="boundary")(
|
| 28 |
+
[(FIXTURE_TEXT, schema.build())]
|
| 29 |
+
)
|
| 30 |
+
with torch.no_grad():
|
| 31 |
+
core = native._encode_core(batch)
|
| 32 |
+
candidates = native.boundary_head(
|
| 33 |
+
core["text_states"], core["text_mask"], core["query_states"], core["query_mask"]
|
| 34 |
+
).candidates
|
| 35 |
+
spec = next(iter(batch.record_specs[0].values()))
|
| 36 |
+
group = native.record_decoder.forward_group(spec, core["query_states"][0], candidates, 0)
|
| 37 |
+
field_states = [
|
| 38 |
+
candidates.candidate_states[0, query_id][candidates.valid_mask[0, query_id]]
|
| 39 |
+
for query_id in group.field_query_ids
|
| 40 |
+
]
|
| 41 |
+
queries = core["query_states"][0][group.field_query_ids]
|
| 42 |
+
return group, spec, field_states, queries
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def pad_first(value, size: int):
|
| 46 |
+
if value.shape[0] > size:
|
| 47 |
+
raise ValueError(f"Real record fixture exceeds bucket capacity {size}")
|
| 48 |
+
result = value.new_zeros((size, *value.shape[1:]))
|
| 49 |
+
result[: value.shape[0]] = value
|
| 50 |
+
return result
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def package_bytes(path):
|
| 54 |
+
return sum(file.stat().st_size for file in path.rglob("*") if file.is_file())
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def main():
|
| 58 |
+
parser = argparse.ArgumentParser()
|
| 59 |
+
parser.add_argument("--output-dir", default="build/extraction")
|
| 60 |
+
parser.add_argument("--precision", choices=["fp16", "fp32"], default="fp32")
|
| 61 |
+
parser.add_argument("--max-fields", type=int, default=8)
|
| 62 |
+
parser.add_argument("--max-candidates", type=int, default=192)
|
| 63 |
+
parser.add_argument("--max-instances", type=int, default=1536)
|
| 64 |
+
args = parser.parse_args()
|
| 65 |
+
torch.set_num_threads(4)
|
| 66 |
+
source = snapshot_download(
|
| 67 |
+
MODEL_ID,
|
| 68 |
+
revision=MODEL_REVISION,
|
| 69 |
+
allow_patterns=[
|
| 70 |
+
"config.json",
|
| 71 |
+
"encoder_config/*",
|
| 72 |
+
"model.safetensors",
|
| 73 |
+
"tokenizer.json",
|
| 74 |
+
"tokenizer_config.json",
|
| 75 |
+
],
|
| 76 |
+
)
|
| 77 |
+
native = AutoExtractor.from_pretrained(source, map_location="cpu").eval()
|
| 78 |
+
head = native.record_decoder
|
| 79 |
+
if args.max_instances < args.max_fields * args.max_candidates:
|
| 80 |
+
raise ValueError("Instance bucket must hold all latent field candidates")
|
| 81 |
+
|
| 82 |
+
group, spec, field_states, queries = record_fixture(native, "natural")
|
| 83 |
+
anchor_index = group.field_query_ids.index(spec.anchor_query_id)
|
| 84 |
+
instances = field_states[anchor_index]
|
| 85 |
+
hidden = instances.shape[-1]
|
| 86 |
+
field_candidates = torch.zeros(args.max_fields, args.max_candidates, hidden)
|
| 87 |
+
for field_index, states in enumerate(field_states):
|
| 88 |
+
if states.shape[0] > args.max_candidates:
|
| 89 |
+
raise ValueError("Record candidate count exceeds bucket")
|
| 90 |
+
field_candidates[field_index, : states.shape[0]] = states
|
| 91 |
+
assignment_args = (pad_first(instances, args.max_instances), pad_first(queries, args.max_fields), field_candidates)
|
| 92 |
+
assignment_wrapper = ExtractionRecordAssignmentExport(native).eval()
|
| 93 |
+
with torch.no_grad():
|
| 94 |
+
assignment_reference = assignment_wrapper(*assignment_args)
|
| 95 |
+
for field_index, expected in enumerate(group.assign_logits):
|
| 96 |
+
actual = assignment_reference[0][: instances.shape[0], field_index, : expected.shape[1]]
|
| 97 |
+
if not torch.allclose(actual, expected, atol=1e-4):
|
| 98 |
+
raise RuntimeError("Record assignment wrapper differs from native")
|
| 99 |
+
assignment_trace = torch.jit.trace(assignment_wrapper, assignment_args, check_trace=False)
|
| 100 |
+
|
| 101 |
+
precision = ct.precision.FLOAT16 if args.precision == "fp16" else ct.precision.FLOAT32
|
| 102 |
+
assignment_model = ct.convert(
|
| 103 |
+
assignment_trace,
|
| 104 |
+
convert_to="mlprogram",
|
| 105 |
+
minimum_deployment_target=ct.target.iOS17,
|
| 106 |
+
compute_precision=precision,
|
| 107 |
+
compute_units=ct.ComputeUnit.CPU_ONLY,
|
| 108 |
+
inputs=[
|
| 109 |
+
ct.TensorType(name=name, shape=tuple(value.shape), dtype=np.float32)
|
| 110 |
+
for name, value in zip(("instance_states", "field_queries", "field_candidate_states"), assignment_args)
|
| 111 |
+
],
|
| 112 |
+
outputs=[
|
| 113 |
+
ct.TensorType(name="assignment_logits", dtype=np.float32),
|
| 114 |
+
ct.TensorType(name="object_logits", dtype=np.float32),
|
| 115 |
+
ct.TensorType(name="latent_seed_logits", dtype=np.float32),
|
| 116 |
+
],
|
| 117 |
+
)
|
| 118 |
+
assignment_model.short_description = "GLiNER2.5 base trained record assignment and object heads"
|
| 119 |
+
assignment_model.author = "Fastino (original); Fluid Inference (Core ML conversion)"
|
| 120 |
+
assignment_model.license = "Apache-2.0"
|
| 121 |
+
assignment_model.user_defined_metadata.update(
|
| 122 |
+
{
|
| 123 |
+
"source_model": MODEL_ID,
|
| 124 |
+
"source_revision": MODEL_REVISION,
|
| 125 |
+
"stage": "trained record assignment, object and latent seed heads",
|
| 126 |
+
"field_capacity": str(args.max_fields),
|
| 127 |
+
"candidate_capacity": str(args.max_candidates),
|
| 128 |
+
"instance_capacity": str(args.max_instances),
|
| 129 |
+
}
|
| 130 |
+
)
|
| 131 |
+
out = Path(args.output_dir)
|
| 132 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 133 |
+
suffix = f"{args.precision}_F{args.max_fields}_C{args.max_candidates}_I{args.max_instances}"
|
| 134 |
+
assignment_path = out / f"gliner2_base_record_assignment_{suffix}.mlpackage"
|
| 135 |
+
if assignment_path.exists():
|
| 136 |
+
shutil.rmtree(assignment_path)
|
| 137 |
+
assignment_model.save(str(assignment_path))
|
| 138 |
+
assignment_runtime = ct.models.MLModel(str(assignment_path), compute_units=ct.ComputeUnit.CPU_ONLY)
|
| 139 |
+
assignment_prediction = assignment_runtime.predict(
|
| 140 |
+
{
|
| 141 |
+
name: value.numpy().astype(np.float32)
|
| 142 |
+
for name, value in zip(("instance_states", "field_queries", "field_candidate_states"), assignment_args)
|
| 143 |
+
}
|
| 144 |
+
)
|
| 145 |
+
assignment_errors = {
|
| 146 |
+
name: float(np.max(np.abs(assignment_prediction[name] - expected.numpy())))
|
| 147 |
+
for name, expected in zip(("assignment_logits", "object_logits", "latent_seed_logits"), assignment_reference)
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
_, _, anchorless_field_states, _ = record_fixture(native, "anchorless")
|
| 151 |
+
context = torch.cat(anchorless_field_states, 0)
|
| 152 |
+
context_size = args.max_fields * args.max_candidates
|
| 153 |
+
context_states = pad_first(context, context_size)
|
| 154 |
+
context_mask = torch.zeros(context_size, dtype=torch.float32)
|
| 155 |
+
context_mask[: context.shape[0]] = 1.0
|
| 156 |
+
anchorless_wrapper = ExtractionRecordAnchorlessExport(native).eval()
|
| 157 |
+
with torch.no_grad():
|
| 158 |
+
anchorless_reference = anchorless_wrapper(context_states, context_mask)
|
| 159 |
+
native_states = head._anchorless_states(anchorless_field_states)
|
| 160 |
+
wrapper_error = float((anchorless_reference - native_states).abs().max())
|
| 161 |
+
if wrapper_error > 1e-4:
|
| 162 |
+
raise RuntimeError(f"Anchorless wrapper differs from native: {wrapper_error}")
|
| 163 |
+
anchorless_trace = torch.jit.trace(anchorless_wrapper, (context_states, context_mask), check_trace=False)
|
| 164 |
+
anchorless_model = ct.convert(
|
| 165 |
+
anchorless_trace,
|
| 166 |
+
convert_to="mlprogram",
|
| 167 |
+
minimum_deployment_target=ct.target.iOS17,
|
| 168 |
+
compute_precision=precision,
|
| 169 |
+
compute_units=ct.ComputeUnit.CPU_ONLY,
|
| 170 |
+
inputs=[
|
| 171 |
+
ct.TensorType(name="context_states", shape=tuple(context_states.shape), dtype=np.float32),
|
| 172 |
+
ct.TensorType(name="context_mask", shape=tuple(context_mask.shape), dtype=np.float32),
|
| 173 |
+
],
|
| 174 |
+
outputs=[ct.TensorType(name="instance_states", dtype=np.float32)],
|
| 175 |
+
)
|
| 176 |
+
anchorless_model.short_description = "GLiNER2.5 base trained anchorless record instance head"
|
| 177 |
+
anchorless_model.author = "Fastino (original); Fluid Inference (Core ML conversion)"
|
| 178 |
+
anchorless_model.license = "Apache-2.0"
|
| 179 |
+
anchorless_model.user_defined_metadata.update(
|
| 180 |
+
{
|
| 181 |
+
"source_model": MODEL_ID,
|
| 182 |
+
"source_revision": MODEL_REVISION,
|
| 183 |
+
"stage": "trained anchorless record instance head",
|
| 184 |
+
"context_capacity": str(context_size),
|
| 185 |
+
}
|
| 186 |
+
)
|
| 187 |
+
anchorless_path = out / f"gliner2_base_record_anchorless_{suffix}.mlpackage"
|
| 188 |
+
if anchorless_path.exists():
|
| 189 |
+
shutil.rmtree(anchorless_path)
|
| 190 |
+
anchorless_model.save(str(anchorless_path))
|
| 191 |
+
anchorless_runtime = ct.models.MLModel(str(anchorless_path), compute_units=ct.ComputeUnit.CPU_ONLY)
|
| 192 |
+
anchorless_prediction = anchorless_runtime.predict(
|
| 193 |
+
{
|
| 194 |
+
"context_states": context_states.numpy().astype(np.float32),
|
| 195 |
+
"context_mask": context_mask.numpy().astype(np.float32),
|
| 196 |
+
}
|
| 197 |
+
)["instance_states"]
|
| 198 |
+
anchorless_error = float(np.max(np.abs(anchorless_prediction - anchorless_reference.numpy())))
|
| 199 |
+
if not all(np.isfinite(value) for value in (*assignment_errors.values(), anchorless_error)):
|
| 200 |
+
raise RuntimeError("Record head produced non-finite values")
|
| 201 |
+
report = {
|
| 202 |
+
"source_model": MODEL_ID,
|
| 203 |
+
"source_revision": MODEL_REVISION,
|
| 204 |
+
"precision": args.precision,
|
| 205 |
+
"fixture": FIXTURE_TEXT,
|
| 206 |
+
"shape": {"fields": args.max_fields, "candidates": args.max_candidates, "instances": args.max_instances},
|
| 207 |
+
"assignment_max_absolute_errors": assignment_errors,
|
| 208 |
+
"anchorless_wrapper_max_absolute_error": wrapper_error,
|
| 209 |
+
"anchorless_coreml_max_absolute_error": anchorless_error,
|
| 210 |
+
"packages": {
|
| 211 |
+
"assignment": {"path": str(assignment_path), "bytes": package_bytes(assignment_path)},
|
| 212 |
+
"anchorless": {"path": str(anchorless_path), "bytes": package_bytes(anchorless_path)},
|
| 213 |
+
},
|
| 214 |
+
"coremltools": ct.__version__,
|
| 215 |
+
"torch": torch.__version__,
|
| 216 |
+
}
|
| 217 |
+
(out / f"record-{suffix}.json").write_text(json.dumps(report, indent=2) + "\n")
|
| 218 |
+
print(json.dumps(report, indent=2))
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
if __name__ == "__main__":
|
| 222 |
+
main()
|
convert-relation-coreml.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Export the trained GLiNER2.5 base sparse relation scorer to Core ML."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import coremltools as ct
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
from gliner2 import AutoExtractor, Schema
|
| 11 |
+
from gliner2.models.base import QueryLayout
|
| 12 |
+
from gliner2.training.trainer import ExtractorCollator
|
| 13 |
+
from huggingface_hub import snapshot_download
|
| 14 |
+
|
| 15 |
+
from extraction_export import ExtractionRelationExport
|
| 16 |
+
|
| 17 |
+
MODEL_ID = "fastino/gliner2.5-base-v1"
|
| 18 |
+
MODEL_REVISION = "1a8bc24e00dc7300b9017c81d63e3dcdabb26596"
|
| 19 |
+
INPUT_NAMES = (
|
| 20 |
+
"text_states",
|
| 21 |
+
"text_length",
|
| 22 |
+
"relation_states",
|
| 23 |
+
"batch_index",
|
| 24 |
+
"relation_index",
|
| 25 |
+
"head_start",
|
| 26 |
+
"head_end",
|
| 27 |
+
"tail_start",
|
| 28 |
+
"tail_end",
|
| 29 |
+
"pair_mask",
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def pad(value, size: int, fill=0):
|
| 34 |
+
if value.shape[0] > size:
|
| 35 |
+
raise ValueError(f"Relation fixture exceeds capacity {size}")
|
| 36 |
+
output = value.new_full((size, *value.shape[1:]), fill)
|
| 37 |
+
output[: value.shape[0]] = value
|
| 38 |
+
return output
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def main():
|
| 42 |
+
parser = argparse.ArgumentParser()
|
| 43 |
+
parser.add_argument("--output-dir", default="build/extraction")
|
| 44 |
+
parser.add_argument("--precision", choices=["fp16", "fp32"], default="fp32")
|
| 45 |
+
parser.add_argument("--max-words", type=int, default=64)
|
| 46 |
+
parser.add_argument("--max-relations", type=int, default=4)
|
| 47 |
+
args = parser.parse_args()
|
| 48 |
+
torch.set_num_threads(4)
|
| 49 |
+
source = snapshot_download(
|
| 50 |
+
MODEL_ID,
|
| 51 |
+
revision=MODEL_REVISION,
|
| 52 |
+
allow_patterns=[
|
| 53 |
+
"config.json",
|
| 54 |
+
"encoder_config/*",
|
| 55 |
+
"model.safetensors",
|
| 56 |
+
"tokenizer.json",
|
| 57 |
+
"tokenizer_config.json",
|
| 58 |
+
],
|
| 59 |
+
)
|
| 60 |
+
native = AutoExtractor.from_pretrained(source, map_location="cpu").eval()
|
| 61 |
+
text = "Alice founded Acme in Toronto."
|
| 62 |
+
schema = Schema().relations(["founded"])
|
| 63 |
+
batch = ExtractorCollator(native.processor, is_training=False, max_len=None, architecture="boundary")(
|
| 64 |
+
[(text, schema.build())]
|
| 65 |
+
)
|
| 66 |
+
with torch.no_grad():
|
| 67 |
+
core = native._encode_core(batch)
|
| 68 |
+
output = native.boundary_head(core["text_states"], core["text_mask"], core["query_states"], core["query_mask"])
|
| 69 |
+
sample = native._single_sample_candidates(output.candidates, 0)
|
| 70 |
+
relation_specs = core["rel_specs"][0]
|
| 71 |
+
pairs = native.relation_pair_generator.generate_batched(
|
| 72 |
+
sample, [QueryLayout(queries=())], [[entry["spec"] for entry in relation_specs]], compact=False
|
| 73 |
+
)
|
| 74 |
+
relation_states = torch.stack([entry["query_state"] for entry in relation_specs]).unsqueeze(0)
|
| 75 |
+
native_scores = native.relation_scorer(core["text_states"], relation_states, sample, pairs)
|
| 76 |
+
pair_cap = args.max_relations * native.boundary_settings.relation_pair_cap
|
| 77 |
+
text_states = torch.zeros(1, args.max_words, core["text_states"].shape[-1])
|
| 78 |
+
text_states[:, : core["text_states"].shape[1]] = core["text_states"]
|
| 79 |
+
relation_padded = torch.zeros(1, args.max_relations, relation_states.shape[-1])
|
| 80 |
+
relation_padded[:, : relation_states.shape[1]] = relation_states
|
| 81 |
+
arguments = (
|
| 82 |
+
text_states,
|
| 83 |
+
torch.tensor([core["text_states"].shape[1]], dtype=torch.int32),
|
| 84 |
+
relation_padded,
|
| 85 |
+
pad(pairs.batch_index.int(), pair_cap),
|
| 86 |
+
pad(pairs.relation_index.int(), pair_cap),
|
| 87 |
+
pad(pairs.head_start.int(), pair_cap),
|
| 88 |
+
pad(pairs.head_end.int(), pair_cap),
|
| 89 |
+
pad(pairs.tail_start.int(), pair_cap),
|
| 90 |
+
pad(pairs.tail_end.int(), pair_cap),
|
| 91 |
+
pad(pairs.pair_mask.float(), pair_cap),
|
| 92 |
+
)
|
| 93 |
+
wrapper = ExtractionRelationExport(native).eval()
|
| 94 |
+
with torch.no_grad():
|
| 95 |
+
reference = wrapper(*arguments)
|
| 96 |
+
wrapper_error = float((reference[: len(pairs)] - native_scores).abs().max())
|
| 97 |
+
traced = torch.jit.trace(wrapper, arguments, check_trace=False)
|
| 98 |
+
if wrapper_error > 1e-4:
|
| 99 |
+
raise RuntimeError(f"Relation wrapper differs from native: {wrapper_error}")
|
| 100 |
+
precision = ct.precision.FLOAT16 if args.precision == "fp16" else ct.precision.FLOAT32
|
| 101 |
+
converted = ct.convert(
|
| 102 |
+
traced,
|
| 103 |
+
convert_to="mlprogram",
|
| 104 |
+
minimum_deployment_target=ct.target.iOS17,
|
| 105 |
+
compute_precision=precision,
|
| 106 |
+
compute_units=ct.ComputeUnit.CPU_ONLY,
|
| 107 |
+
inputs=[
|
| 108 |
+
ct.TensorType(
|
| 109 |
+
name=name,
|
| 110 |
+
shape=tuple(value.shape),
|
| 111 |
+
dtype=np.float32 if name in ("text_states", "relation_states", "pair_mask") else np.int32,
|
| 112 |
+
)
|
| 113 |
+
for name, value in zip(INPUT_NAMES, arguments)
|
| 114 |
+
],
|
| 115 |
+
outputs=[ct.TensorType(name="relation_logits", dtype=np.float32)],
|
| 116 |
+
)
|
| 117 |
+
converted.short_description = "GLiNER2.5 base trained sparse relation scoring head"
|
| 118 |
+
converted.author = "Fastino (original); Fluid Inference (Core ML conversion)"
|
| 119 |
+
converted.license = "Apache-2.0"
|
| 120 |
+
converted.user_defined_metadata.update(
|
| 121 |
+
{
|
| 122 |
+
"source_model": MODEL_ID,
|
| 123 |
+
"source_revision": MODEL_REVISION,
|
| 124 |
+
"stage": "trained relation scorer",
|
| 125 |
+
"word_capacity": str(args.max_words),
|
| 126 |
+
"relation_capacity": str(args.max_relations),
|
| 127 |
+
"pair_capacity": str(pair_cap),
|
| 128 |
+
}
|
| 129 |
+
)
|
| 130 |
+
out = Path(args.output_dir)
|
| 131 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 132 |
+
suffix = f"{args.precision}_W{args.max_words}_R{args.max_relations}_P{pair_cap}"
|
| 133 |
+
package = out / f"gliner2_base_relation_{suffix}.mlpackage"
|
| 134 |
+
converted.save(str(package))
|
| 135 |
+
model = ct.models.MLModel(str(package), compute_units=ct.ComputeUnit.CPU_ONLY)
|
| 136 |
+
prediction = model.predict(
|
| 137 |
+
{
|
| 138 |
+
name: value.numpy().astype(
|
| 139 |
+
np.float32 if name in ("text_states", "relation_states", "pair_mask") else np.int32
|
| 140 |
+
)
|
| 141 |
+
for name, value in zip(INPUT_NAMES, arguments)
|
| 142 |
+
}
|
| 143 |
+
)["relation_logits"]
|
| 144 |
+
runtime_error = float(np.max(np.abs(prediction[: len(pairs)] - reference.numpy()[: len(pairs)])))
|
| 145 |
+
if not np.isfinite(runtime_error):
|
| 146 |
+
raise RuntimeError("Relation scorer produced non-finite values")
|
| 147 |
+
report = {
|
| 148 |
+
"source_model": MODEL_ID,
|
| 149 |
+
"source_revision": MODEL_REVISION,
|
| 150 |
+
"precision": args.precision,
|
| 151 |
+
"fixture": text,
|
| 152 |
+
"valid_pairs": int(pairs.pair_mask.sum()),
|
| 153 |
+
"wrapper_max_absolute_error": wrapper_error,
|
| 154 |
+
"coreml_max_absolute_error": runtime_error,
|
| 155 |
+
"package": str(package),
|
| 156 |
+
"package_bytes": sum(file.stat().st_size for file in package.rglob("*") if file.is_file()),
|
| 157 |
+
"coremltools": ct.__version__,
|
| 158 |
+
"torch": torch.__version__,
|
| 159 |
+
}
|
| 160 |
+
(out / f"relation-{suffix}.json").write_text(json.dumps(report, indent=2) + "\n")
|
| 161 |
+
print(json.dumps(report, indent=2))
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
if __name__ == "__main__":
|
| 165 |
+
main()
|
convert_extraction_names.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ordered tensor contract shared by extraction conversion and verification."""
|
| 2 |
+
|
| 3 |
+
FEATURE_NAMES = (
|
| 4 |
+
"text_states",
|
| 5 |
+
"query_states",
|
| 6 |
+
"boundary_states",
|
| 7 |
+
"boundary_mask",
|
| 8 |
+
"start_logits",
|
| 9 |
+
"end_logits",
|
| 10 |
+
"inside_prefix",
|
| 11 |
+
"inside_prefix_mean",
|
| 12 |
+
"pool_start_projection",
|
| 13 |
+
"pool_end_projection",
|
| 14 |
+
"null_logits",
|
| 15 |
+
"count_log_rates",
|
| 16 |
+
"classification_logits",
|
| 17 |
+
)
|
| 18 |
+
SCORE_INPUT_NAMES = (
|
| 19 |
+
"text_states",
|
| 20 |
+
"text_mask",
|
| 21 |
+
"query_states",
|
| 22 |
+
"query_mask",
|
| 23 |
+
"boundary_states",
|
| 24 |
+
"start_logits",
|
| 25 |
+
"end_logits",
|
| 26 |
+
"inside_prefix",
|
| 27 |
+
"inside_prefix_mean",
|
| 28 |
+
"candidate_indices",
|
| 29 |
+
"candidate_mask",
|
| 30 |
+
"candidate_compatibility",
|
| 31 |
+
)
|
extraction-assets.lock.json
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"source_model": "fastino/gliner2.5-base-v1",
|
| 3 |
+
"source_revision": "1a8bc24e00dc7300b9017c81d63e3dcdabb26596",
|
| 4 |
+
"package_files": [
|
| 5 |
+
{
|
| 6 |
+
"package": "gliner2_base_explicit_fp16_W64_Q8_S64.mlpackage",
|
| 7 |
+
"bytes": 614010,
|
| 8 |
+
"files": [
|
| 9 |
+
{
|
| 10 |
+
"path": "gliner2_base_explicit_fp16_W64_Q8_S64.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
| 11 |
+
"bytes": 61009,
|
| 12 |
+
"sha256": "833cfc75c7b45d95f423b00770569ad00707aad4d7e993d9288aacd6392901be"
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"path": "gliner2_base_explicit_fp16_W64_Q8_S64.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
| 16 |
+
"bytes": 552384,
|
| 17 |
+
"sha256": "b8f472bf52874cf6fbe92e08333cb1976236fe179a186efeba049f1814ca2f6f"
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"path": "gliner2_base_explicit_fp16_W64_Q8_S64.mlpackage/Manifest.json",
|
| 21 |
+
"bytes": 617,
|
| 22 |
+
"sha256": "2f83e83160221b29ce2f9156518bfa6bdaefd94c7c1eabff8de06131374b41d6"
|
| 23 |
+
}
|
| 24 |
+
]
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"package": "gliner2_base_explicit_fp32_W64_Q8_S64.mlpackage",
|
| 28 |
+
"bytes": 1152468,
|
| 29 |
+
"files": [
|
| 30 |
+
{
|
| 31 |
+
"path": "gliner2_base_explicit_fp32_W64_Q8_S64.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
| 32 |
+
"bytes": 48811,
|
| 33 |
+
"sha256": "6226a1f7b91ceb1b2528d5eac17fa7b30db7c6e921ebc4a8b1b7ad4b8dca9096"
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"path": "gliner2_base_explicit_fp32_W64_Q8_S64.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
| 37 |
+
"bytes": 1103040,
|
| 38 |
+
"sha256": "7dded10597f6d93d30f9842d963e6a1ed503a7e1ed53d6f713ee22da38101e1f"
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"path": "gliner2_base_explicit_fp32_W64_Q8_S64.mlpackage/Manifest.json",
|
| 42 |
+
"bytes": 617,
|
| 43 |
+
"sha256": "18d42e32e177e558362500e951fdde1310afce5a11435e7a108264c184e42d54"
|
| 44 |
+
}
|
| 45 |
+
]
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"package": "gliner2_base_extraction_features_fp16_L128_W64_Q8.mlpackage",
|
| 49 |
+
"bytes": 390998448,
|
| 50 |
+
"files": [
|
| 51 |
+
{
|
| 52 |
+
"path": "gliner2_base_extraction_features_fp16_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
| 53 |
+
"bytes": 1046087,
|
| 54 |
+
"sha256": "81a989bd9c9791ca2cb2b10c9456795a3f98cb2a41132ad894ec6aae3bee8c58"
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"path": "gliner2_base_extraction_features_fp16_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
| 58 |
+
"bytes": 389951744,
|
| 59 |
+
"sha256": "f89a14b93e79bc0c5f03ae1d2afdecabc12d0ba0a6551107efb60d19e2fef913"
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"path": "gliner2_base_extraction_features_fp16_L128_W64_Q8.mlpackage/Manifest.json",
|
| 63 |
+
"bytes": 617,
|
| 64 |
+
"sha256": "5b86a3a9736fb9849f4cf6bcae5fd8038332c2ac4106f5ed5fc23a72c4b77ed4"
|
| 65 |
+
}
|
| 66 |
+
]
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"package": "gliner2_base_extraction_features_fp32_L128_W64_Q8.mlpackage",
|
| 70 |
+
"bytes": 780899033,
|
| 71 |
+
"files": [
|
| 72 |
+
{
|
| 73 |
+
"path": "gliner2_base_extraction_features_fp32_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
| 74 |
+
"bytes": 1012396,
|
| 75 |
+
"sha256": "952aba39fb31af06ae240e1c03ce0a9fe065c2e5997d4f455fb0df2055059103"
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"path": "gliner2_base_extraction_features_fp32_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
| 79 |
+
"bytes": 779886020,
|
| 80 |
+
"sha256": "3edc72e18c141125c4231b7a3b0d7d0105135271f7deff924818951e6f354ef0"
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"path": "gliner2_base_extraction_features_fp32_L128_W64_Q8.mlpackage/Manifest.json",
|
| 84 |
+
"bytes": 617,
|
| 85 |
+
"sha256": "5d9751b4961ccdb017b0e8a01e73920ced48c101a9845164a19741802dc43f8c"
|
| 86 |
+
}
|
| 87 |
+
]
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"package": "gliner2_base_extraction_scorer_fp16_L128_W64_Q8.mlpackage",
|
| 91 |
+
"bytes": 903154,
|
| 92 |
+
"files": [
|
| 93 |
+
{
|
| 94 |
+
"path": "gliner2_base_extraction_scorer_fp16_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
| 95 |
+
"bytes": 42953,
|
| 96 |
+
"sha256": "a28d35b1697ca5877e4c37be3fc73636a6795c7a7fb589aadee053ecbc06d496"
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"path": "gliner2_base_extraction_scorer_fp16_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
| 100 |
+
"bytes": 859584,
|
| 101 |
+
"sha256": "63e7a5f12ea123730faf9248e3d5245d8861cda7bdc678a29b6ed10c1c0450a3"
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"path": "gliner2_base_extraction_scorer_fp16_L128_W64_Q8.mlpackage/Manifest.json",
|
| 105 |
+
"bytes": 617,
|
| 106 |
+
"sha256": "25563e15f2ec7a511142b626c74007b9816de4fd3729395c2a40ca6da5e6a866"
|
| 107 |
+
}
|
| 108 |
+
]
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"package": "gliner2_base_extraction_scorer_fp32_L128_W64_Q8.mlpackage",
|
| 112 |
+
"bytes": 1750631,
|
| 113 |
+
"files": [
|
| 114 |
+
{
|
| 115 |
+
"path": "gliner2_base_extraction_scorer_fp32_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
| 116 |
+
"bytes": 32574,
|
| 117 |
+
"sha256": "dc02f86228a4c95872de1d6dab5d5a1ca2d89c464ba1df0eb1f4333cc4f517f9"
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"path": "gliner2_base_extraction_scorer_fp32_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
| 121 |
+
"bytes": 1717440,
|
| 122 |
+
"sha256": "ab96965841dd7802bb22d3a4d65ac2ef27e4df4daf4be1a49a65d8bc3c87f595"
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
"path": "gliner2_base_extraction_scorer_fp32_L128_W64_Q8.mlpackage/Manifest.json",
|
| 126 |
+
"bytes": 617,
|
| 127 |
+
"sha256": "753acbe0781d89b5b157da08ce5d75d21f61b0c4f86a9ceafa21d3274df54259"
|
| 128 |
+
}
|
| 129 |
+
]
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
"package": "gliner2_base_record_anchorless_fp16_F8_C192_I1536.mlpackage",
|
| 133 |
+
"bytes": 1443015,
|
| 134 |
+
"files": [
|
| 135 |
+
{
|
| 136 |
+
"path": "gliner2_base_record_anchorless_fp16_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
| 137 |
+
"bytes": 6558,
|
| 138 |
+
"sha256": "3ae7e7e6540577572bc1ae82d334d9294dd2bfb2d270cdf5166edcb9ce487495"
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
"path": "gliner2_base_record_anchorless_fp16_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
| 142 |
+
"bytes": 1435840,
|
| 143 |
+
"sha256": "73f5d6812653173c71dcd666e9477dc275fae42290bb0bfa5aef00e1c721a7f5"
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"path": "gliner2_base_record_anchorless_fp16_F8_C192_I1536.mlpackage/Manifest.json",
|
| 147 |
+
"bytes": 617,
|
| 148 |
+
"sha256": "4b6d679bf502518e30cdc5e5a65154a6c37552937301ba9146c4a2057d13f7dd"
|
| 149 |
+
}
|
| 150 |
+
]
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"package": "gliner2_base_record_anchorless_fp32_F8_C192_I1536.mlpackage",
|
| 154 |
+
"bytes": 2876843,
|
| 155 |
+
"files": [
|
| 156 |
+
{
|
| 157 |
+
"path": "gliner2_base_record_anchorless_fp32_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
| 158 |
+
"bytes": 4994,
|
| 159 |
+
"sha256": "fa573f67c4b36f80f02668d377abd244956d1afba07be7d55866080b8188b6ef"
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
"path": "gliner2_base_record_anchorless_fp32_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
| 163 |
+
"bytes": 2871232,
|
| 164 |
+
"sha256": "f48cb084768846394746b4a39d189a47be19392708075f854af3dfe8076db4cd"
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"path": "gliner2_base_record_anchorless_fp32_F8_C192_I1536.mlpackage/Manifest.json",
|
| 168 |
+
"bytes": 617,
|
| 169 |
+
"sha256": "f646f499b1c601271b51c1420dc21ffc68fc983e2a65d4377a2094a094992385"
|
| 170 |
+
}
|
| 171 |
+
]
|
| 172 |
+
},
|
| 173 |
+
{
|
| 174 |
+
"package": "gliner2_base_record_assignment_fp16_F8_C192_I1536.mlpackage",
|
| 175 |
+
"bytes": 605158,
|
| 176 |
+
"files": [
|
| 177 |
+
{
|
| 178 |
+
"path": "gliner2_base_record_assignment_fp16_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
| 179 |
+
"bytes": 9981,
|
| 180 |
+
"sha256": "5ff5970e8851aeae902aac01e4c4586d453b0f39797f342d9a9b2a2791d7e605"
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"path": "gliner2_base_record_assignment_fp16_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
| 184 |
+
"bytes": 594560,
|
| 185 |
+
"sha256": "d59545d5a9df722194e38a0a3ee03c6a5f95c58fa9608f4b2f3f6e3f56d0136e"
|
| 186 |
+
},
|
| 187 |
+
{
|
| 188 |
+
"path": "gliner2_base_record_assignment_fp16_F8_C192_I1536.mlpackage/Manifest.json",
|
| 189 |
+
"bytes": 617,
|
| 190 |
+
"sha256": "365d8c13d2a88c83072fa7ec640919f817ad87b245c10caf1445cfde6e0dd040"
|
| 191 |
+
}
|
| 192 |
+
]
|
| 193 |
+
},
|
| 194 |
+
{
|
| 195 |
+
"package": "gliner2_base_record_assignment_fp32_F8_C192_I1536.mlpackage",
|
| 196 |
+
"bytes": 1196559,
|
| 197 |
+
"files": [
|
| 198 |
+
{
|
| 199 |
+
"path": "gliner2_base_record_assignment_fp32_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
| 200 |
+
"bytes": 7462,
|
| 201 |
+
"sha256": "63cad042b4d8487a0e6f57eec23a6a69f8687c33720c5213b8d52c1dd2169572"
|
| 202 |
+
},
|
| 203 |
+
{
|
| 204 |
+
"path": "gliner2_base_record_assignment_fp32_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
| 205 |
+
"bytes": 1188480,
|
| 206 |
+
"sha256": "5f870796d2a8e0539b20964ebcd80cae65677ed11d4b36b12df9a21d158181c1"
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
"path": "gliner2_base_record_assignment_fp32_F8_C192_I1536.mlpackage/Manifest.json",
|
| 210 |
+
"bytes": 617,
|
| 211 |
+
"sha256": "991014787fc73007fc398cfe7c77537d241c730ca8363697f2e70478389b95f8"
|
| 212 |
+
}
|
| 213 |
+
]
|
| 214 |
+
},
|
| 215 |
+
{
|
| 216 |
+
"package": "gliner2_base_relation_fp16_W64_R4_P256.mlpackage",
|
| 217 |
+
"bytes": 11848707,
|
| 218 |
+
"files": [
|
| 219 |
+
{
|
| 220 |
+
"path": "gliner2_base_relation_fp16_W64_R4_P256.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
| 221 |
+
"bytes": 32410,
|
| 222 |
+
"sha256": "e6562f5850272ffe0b38363da5a85a70492e170e547f58b008021b6a8aa375ec"
|
| 223 |
+
},
|
| 224 |
+
{
|
| 225 |
+
"path": "gliner2_base_relation_fp16_W64_R4_P256.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
| 226 |
+
"bytes": 11815680,
|
| 227 |
+
"sha256": "1577561a8686c9657ec38df97ff701cbc7b588da3efae6404e33622ab1cc77e5"
|
| 228 |
+
},
|
| 229 |
+
{
|
| 230 |
+
"path": "gliner2_base_relation_fp16_W64_R4_P256.mlpackage/Manifest.json",
|
| 231 |
+
"bytes": 617,
|
| 232 |
+
"sha256": "a79be16e25f1d7b5a261a8a2f1a5cd7dea41fc2d18e755338bab5dabf024662c"
|
| 233 |
+
}
|
| 234 |
+
]
|
| 235 |
+
},
|
| 236 |
+
{
|
| 237 |
+
"package": "gliner2_base_relation_fp32_W64_R4_P256.mlpackage",
|
| 238 |
+
"bytes": 23661032,
|
| 239 |
+
"files": [
|
| 240 |
+
{
|
| 241 |
+
"path": "gliner2_base_relation_fp32_W64_R4_P256.mlpackage/Data/com.apple.CoreML/model.mlmodel",
|
| 242 |
+
"bytes": 29823,
|
| 243 |
+
"sha256": "5970ae822c86e3f11ef86341034824661fa10184730953ac4bcdb7086e87a6ea"
|
| 244 |
+
},
|
| 245 |
+
{
|
| 246 |
+
"path": "gliner2_base_relation_fp32_W64_R4_P256.mlpackage/Data/com.apple.CoreML/weights/weight.bin",
|
| 247 |
+
"bytes": 23630592,
|
| 248 |
+
"sha256": "1dd7c8e640688002c174080710cc5ba3fa32c5dc399b52ef0bf7f4c5ef8d01cb"
|
| 249 |
+
},
|
| 250 |
+
{
|
| 251 |
+
"path": "gliner2_base_relation_fp32_W64_R4_P256.mlpackage/Manifest.json",
|
| 252 |
+
"bytes": 617,
|
| 253 |
+
"sha256": "22d5d1c3db4225665d32ae55f8266c9c3ff6980bfe7c60db655529b8dfefbba9"
|
| 254 |
+
}
|
| 255 |
+
]
|
| 256 |
+
}
|
| 257 |
+
],
|
| 258 |
+
"runtime_files": [
|
| 259 |
+
{
|
| 260 |
+
"path": "config.json",
|
| 261 |
+
"bytes": 3150,
|
| 262 |
+
"sha256": "0eb92d00584d613aab32b2178f84a85176b62c87ae3689ce9084e83f6eba64d1"
|
| 263 |
+
},
|
| 264 |
+
{
|
| 265 |
+
"path": "tokenizer/tokenizer_config.json",
|
| 266 |
+
"bytes": 645,
|
| 267 |
+
"sha256": "0bf3ea0873234bd9bfdd3853c440395009ac6365a925b91654daed5396d655e1"
|
| 268 |
+
},
|
| 269 |
+
{
|
| 270 |
+
"path": "tokenizer/tokenizer.json",
|
| 271 |
+
"bytes": 8341713,
|
| 272 |
+
"sha256": "cbc8ae6037812709c9c26f2a160f8dc48b0440bcb79c8141804259ae2d6adac3"
|
| 273 |
+
}
|
| 274 |
+
]
|
| 275 |
+
}
|
extraction_export.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Core ML graph wrappers for GLiNER2.5's trained boundary extraction path."""
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
from contextlib import contextmanager
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
from coremltools.converters.mil import Builder as mb
|
| 8 |
+
from coremltools.converters.mil.frontend.torch.ops import _get_inputs
|
| 9 |
+
from coremltools.converters.mil.frontend.torch.torch_op_registry import register_torch_op
|
| 10 |
+
from coremltools.converters.mil.mil import types
|
| 11 |
+
from gliner2.models.boundary import encoding, heads
|
| 12 |
+
from gliner2.models.boundary.pool import PooledCandidates
|
| 13 |
+
from gliner2.models.boundary.proposal import BoundaryProposals
|
| 14 |
+
from transformers.models.deberta_v2 import modeling_deberta_v2
|
| 15 |
+
|
| 16 |
+
from export_model import coreml_safe_attention_forward
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@register_torch_op(override=True)
|
| 20 |
+
def clamp_min(context, node):
|
| 21 |
+
"""Preserve the tensor dtype when TorchScript supplied a Python scalar."""
|
| 22 |
+
x, y = _get_inputs(context, node, expected=2)
|
| 23 |
+
if x.dtype != y.dtype:
|
| 24 |
+
y = mb.cast(x=y, dtype=types.builtin_to_string(x.dtype))
|
| 25 |
+
context.add(mb.maximum(x=x, y=y, name=node.name))
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@register_torch_op(torch_alias=["clip"], override=True)
|
| 29 |
+
def clamp(context, node):
|
| 30 |
+
"""Avoid promoting integer span indices to float for an absent bound."""
|
| 31 |
+
inputs = _get_inputs(context, node, expected=[1, 2, 3])
|
| 32 |
+
x = inputs[0]
|
| 33 |
+
lower = inputs[1] if len(inputs) > 1 and inputs[1] is not None else None
|
| 34 |
+
upper = inputs[2] if len(inputs) > 2 and inputs[2] is not None else None
|
| 35 |
+
result = x
|
| 36 |
+
for bound, op in ((upper, mb.minimum), (lower, mb.maximum)):
|
| 37 |
+
if bound is None:
|
| 38 |
+
continue
|
| 39 |
+
if bound.dtype != x.dtype:
|
| 40 |
+
bound = mb.cast(x=bound, dtype=types.builtin_to_string(x.dtype))
|
| 41 |
+
result = op(x=result, y=bound)
|
| 42 |
+
context.add(mb.identity(x=result, name=node.name))
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def shift_left(text_states, bos_state):
|
| 46 |
+
"""Functional equivalent of the upstream in-place BOS placement."""
|
| 47 |
+
bos = bos_state.to(text_states.dtype).view(1, 1, -1)
|
| 48 |
+
return torch.cat((bos.expand(text_states.shape[0], 1, -1), text_states), 1)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def shift_right(text_states, text_lengths, eos_state):
|
| 52 |
+
"""Functional equivalent of the upstream in-place EOS placement."""
|
| 53 |
+
batch, length, hidden = text_states.shape
|
| 54 |
+
eos = eos_state.to(text_states.dtype).view(1, 1, hidden)
|
| 55 |
+
right = torch.cat((text_states, eos.expand(batch, 1, hidden)), 1)
|
| 56 |
+
positions = torch.arange(length + 1, device=text_states.device).view(1, length + 1, 1)
|
| 57 |
+
return torch.where(
|
| 58 |
+
positions == text_lengths.view(batch, 1, 1),
|
| 59 |
+
eos.expand(batch, length + 1, hidden),
|
| 60 |
+
right,
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def safe_boundary_attention(self, states, mask):
|
| 65 |
+
"""Explicit scaled attention with the same finite masked result as upstream."""
|
| 66 |
+
batch, length, dim = states.shape
|
| 67 |
+
qkv = self.qkv_projection(self.norm(states)).view(batch, length, 3, self.num_heads, self.head_dim)
|
| 68 |
+
query, key, value = qkv.permute(2, 0, 3, 1, 4)
|
| 69 |
+
allowed = mask.view(batch, 1, 1, length).expand(batch, 1, length, length)
|
| 70 |
+
if self.window > 0:
|
| 71 |
+
positions = torch.arange(length, device=states.device)
|
| 72 |
+
local = (positions.view(length, 1) - positions.view(1, length)).abs() <= self.window
|
| 73 |
+
allowed = allowed & local.view(1, 1, length, length)
|
| 74 |
+
diagonal = torch.eye(length, dtype=torch.bool, device=states.device).view(1, 1, length, length)
|
| 75 |
+
allowed = (allowed.float() + diagonal.float()) > 0.5
|
| 76 |
+
scores = torch.matmul(query, key.transpose(-1, -2)) / math.sqrt(self.head_dim)
|
| 77 |
+
scores = scores.masked_fill(~allowed, -1e4)
|
| 78 |
+
attended = torch.matmul(torch.softmax(scores, dim=-1), value)
|
| 79 |
+
attended = attended.transpose(1, 2).reshape(batch, length, dim)
|
| 80 |
+
return (states + self.dropout(self.output_projection(attended))) * mask.unsqueeze(-1).to(states.dtype)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def safe_query_head(self, boundary, boundary_mask, text, text_mask, query, query_mask):
|
| 84 |
+
"""Upstream marginals with a dtype-safe count clamp for Core ML."""
|
| 85 |
+
scale = 1.0 / math.sqrt(self.boundary_dim)
|
| 86 |
+
start = (
|
| 87 |
+
torch.einsum(
|
| 88 |
+
"bld,bqd->bql",
|
| 89 |
+
self.dropout(self.start_boundary_projection(boundary)),
|
| 90 |
+
self.start_query_projection(query),
|
| 91 |
+
)
|
| 92 |
+
* scale
|
| 93 |
+
)
|
| 94 |
+
end = (
|
| 95 |
+
torch.einsum(
|
| 96 |
+
"bld,bqd->bql",
|
| 97 |
+
self.dropout(self.end_boundary_projection(boundary)),
|
| 98 |
+
self.end_query_projection(query),
|
| 99 |
+
)
|
| 100 |
+
* scale
|
| 101 |
+
)
|
| 102 |
+
inside = (
|
| 103 |
+
torch.einsum(
|
| 104 |
+
"bld,bqd->bql",
|
| 105 |
+
self.dropout(self.inside_text_projection(text)),
|
| 106 |
+
self.inside_query_projection(query),
|
| 107 |
+
)
|
| 108 |
+
* scale
|
| 109 |
+
)
|
| 110 |
+
boundary_keep = boundary_mask.unsqueeze(1) & query_mask.unsqueeze(-1)
|
| 111 |
+
text_keep = text_mask.unsqueeze(1) & query_mask.unsqueeze(-1)
|
| 112 |
+
start = heads._masked_fill_min(start, boundary_keep)
|
| 113 |
+
end = heads._masked_fill_min(end, boundary_keep)
|
| 114 |
+
inside = heads._masked_fill_min(inside, text_keep)
|
| 115 |
+
inside_for_prefix = inside.masked_fill(~text_keep, 0.0).float()
|
| 116 |
+
count = torch.clamp(text_keep.sum(-1, keepdim=True).float(), min=1.0)
|
| 117 |
+
mean = (inside_for_prefix.sum(-1, keepdim=True) / count).detach()
|
| 118 |
+
centered = (inside_for_prefix - mean) * text_keep.to(inside_for_prefix.dtype)
|
| 119 |
+
zeros = torch.zeros(centered.shape[0], centered.shape[1], 1, dtype=torch.float32, device=text.device)
|
| 120 |
+
prefix = torch.cat((zeros, centered.cumsum(dim=-1)), dim=-1)
|
| 121 |
+
return heads.BoundaryMarginals(start, end, inside, prefix, mean)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
@contextmanager
|
| 125 |
+
def coreml_trace_patches():
|
| 126 |
+
"""Apply and restore mathematically equivalent trace-safe operations."""
|
| 127 |
+
saved = (
|
| 128 |
+
modeling_deberta_v2.scaled_size_sqrt,
|
| 129 |
+
modeling_deberta_v2.build_rpos,
|
| 130 |
+
modeling_deberta_v2.DisentangledSelfAttention.forward,
|
| 131 |
+
encoding.shift_left_with_bos,
|
| 132 |
+
encoding.shift_right_with_eos,
|
| 133 |
+
encoding.BoundaryAttentionBlock.forward,
|
| 134 |
+
heads.BoundaryQueryHead.forward,
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
def static_scale(query_layer, scale_factor):
|
| 138 |
+
value = math.sqrt(float(query_layer.shape[-1] * scale_factor))
|
| 139 |
+
return torch.tensor(value, dtype=torch.float32, device=query_layer.device)
|
| 140 |
+
|
| 141 |
+
modeling_deberta_v2.scaled_size_sqrt = static_scale
|
| 142 |
+
modeling_deberta_v2.build_rpos = lambda query, key, relative_pos, buckets, max_pos: relative_pos
|
| 143 |
+
modeling_deberta_v2.DisentangledSelfAttention.forward = coreml_safe_attention_forward
|
| 144 |
+
encoding.shift_left_with_bos = shift_left
|
| 145 |
+
encoding.shift_right_with_eos = shift_right
|
| 146 |
+
encoding.BoundaryAttentionBlock.forward = safe_boundary_attention
|
| 147 |
+
heads.BoundaryQueryHead.forward = safe_query_head
|
| 148 |
+
try:
|
| 149 |
+
yield
|
| 150 |
+
finally:
|
| 151 |
+
(
|
| 152 |
+
modeling_deberta_v2.scaled_size_sqrt,
|
| 153 |
+
modeling_deberta_v2.build_rpos,
|
| 154 |
+
modeling_deberta_v2.DisentangledSelfAttention.forward,
|
| 155 |
+
encoding.shift_left_with_bos,
|
| 156 |
+
encoding.shift_right_with_eos,
|
| 157 |
+
encoding.BoundaryAttentionBlock.forward,
|
| 158 |
+
heads.BoundaryQueryHead.forward,
|
| 159 |
+
) = saved
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
class ExtractionFeaturesExport(torch.nn.Module):
|
| 163 |
+
"""Trained encoder, boundary marginals, pool projections and null/count heads."""
|
| 164 |
+
|
| 165 |
+
def __init__(self, native):
|
| 166 |
+
super().__init__()
|
| 167 |
+
self.encoder = native.encoder
|
| 168 |
+
self.head = native.boundary_head
|
| 169 |
+
self.classifier = native.classifier
|
| 170 |
+
|
| 171 |
+
def forward(
|
| 172 |
+
self,
|
| 173 |
+
input_ids,
|
| 174 |
+
attention_mask,
|
| 175 |
+
text_indices,
|
| 176 |
+
text_mask,
|
| 177 |
+
query_indices,
|
| 178 |
+
query_mask,
|
| 179 |
+
cls_indices,
|
| 180 |
+
cls_mask,
|
| 181 |
+
):
|
| 182 |
+
hidden = self.encoder(input_ids=input_ids.long(), attention_mask=attention_mask.long()).last_hidden_state
|
| 183 |
+
text_idx = text_indices.long().unsqueeze(-1).expand(-1, -1, hidden.shape[-1])
|
| 184 |
+
query_idx = query_indices.long().unsqueeze(-1).expand(-1, -1, hidden.shape[-1])
|
| 185 |
+
text = hidden.gather(1, text_idx) * text_mask.unsqueeze(-1)
|
| 186 |
+
query = hidden.gather(1, query_idx) * query_mask.unsqueeze(-1)
|
| 187 |
+
cls_idx = cls_indices.long().unsqueeze(-1).expand(-1, -1, hidden.shape[-1])
|
| 188 |
+
classification_states = hidden.gather(1, cls_idx)
|
| 189 |
+
classification_logits = self.classifier(classification_states).squeeze(-1)
|
| 190 |
+
classification_logits = torch.where(
|
| 191 |
+
cls_mask > 0.5, classification_logits, torch.full_like(classification_logits, -1e4)
|
| 192 |
+
)
|
| 193 |
+
tm, qm = text_mask > 0.5, query_mask > 0.5
|
| 194 |
+
encoded = self.head.boundary_encoder(text, tm)
|
| 195 |
+
marginal = self.head.boundary_query_head(encoded.states, encoded.mask, text, tm, query, qm)
|
| 196 |
+
return (
|
| 197 |
+
text,
|
| 198 |
+
query,
|
| 199 |
+
encoded.states,
|
| 200 |
+
encoded.mask.float(),
|
| 201 |
+
marginal.start_logits,
|
| 202 |
+
marginal.end_logits,
|
| 203 |
+
marginal.inside_prefix,
|
| 204 |
+
marginal.inside_prefix_mean,
|
| 205 |
+
self.head.shared_pool_builder.start_projection(encoded.states),
|
| 206 |
+
self.head.shared_pool_builder.end_projection(encoded.states),
|
| 207 |
+
self.head.null_projection(query).squeeze(-1),
|
| 208 |
+
self.head.count_head(query).squeeze(-1),
|
| 209 |
+
classification_logits,
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
class ExtractionScoreExport(torch.nn.Module):
|
| 214 |
+
"""Trained shared-pool reranker and record candidate state projection."""
|
| 215 |
+
|
| 216 |
+
def __init__(self, native):
|
| 217 |
+
super().__init__()
|
| 218 |
+
self.scorer = native.boundary_head.shared_pool_scorer
|
| 219 |
+
self.candidate_encoder = native.boundary_head.candidate_encoder
|
| 220 |
+
|
| 221 |
+
def forward(
|
| 222 |
+
self,
|
| 223 |
+
text,
|
| 224 |
+
text_mask,
|
| 225 |
+
query,
|
| 226 |
+
query_mask,
|
| 227 |
+
boundary,
|
| 228 |
+
starts,
|
| 229 |
+
ends,
|
| 230 |
+
inside,
|
| 231 |
+
inside_mean,
|
| 232 |
+
indices,
|
| 233 |
+
pool_mask,
|
| 234 |
+
compatibility,
|
| 235 |
+
):
|
| 236 |
+
tm, qm = text_mask > 0.5, query_mask > 0.5
|
| 237 |
+
pooled = PooledCandidates(indices.long(), pool_mask > 0.5, None, None, compatibility)
|
| 238 |
+
score, _ = self.scorer(
|
| 239 |
+
boundary,
|
| 240 |
+
query,
|
| 241 |
+
qm,
|
| 242 |
+
pooled,
|
| 243 |
+
starts,
|
| 244 |
+
ends,
|
| 245 |
+
inside,
|
| 246 |
+
tm.sum(-1).long(),
|
| 247 |
+
text,
|
| 248 |
+
tm,
|
| 249 |
+
inside_prefix_mean=inside_mean,
|
| 250 |
+
)
|
| 251 |
+
index = indices.long()
|
| 252 |
+
start_states = boundary.gather(1, index[..., 0].unsqueeze(-1).expand(-1, -1, boundary.shape[-1]))
|
| 253 |
+
end_states = boundary.gather(1, index[..., 1].unsqueeze(-1).expand(-1, -1, boundary.shape[-1]))
|
| 254 |
+
candidate_states = self.candidate_encoder(torch.cat((start_states, end_states), -1))
|
| 255 |
+
candidate_states = candidate_states * pool_mask.unsqueeze(-1)
|
| 256 |
+
return score.transpose(1, 2), candidate_states
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
class ExtractionRelationExport(torch.nn.Module):
|
| 260 |
+
"""The trained sparse relation scorer with tensor-only pair routing."""
|
| 261 |
+
|
| 262 |
+
def __init__(self, native):
|
| 263 |
+
super().__init__()
|
| 264 |
+
self.scorer = native.relation_scorer
|
| 265 |
+
|
| 266 |
+
def forward(
|
| 267 |
+
self,
|
| 268 |
+
text,
|
| 269 |
+
text_length,
|
| 270 |
+
relation,
|
| 271 |
+
batch_index,
|
| 272 |
+
relation_index,
|
| 273 |
+
head_start,
|
| 274 |
+
head_end,
|
| 275 |
+
tail_start,
|
| 276 |
+
tail_end,
|
| 277 |
+
pair_mask,
|
| 278 |
+
):
|
| 279 |
+
scorer = self.scorer
|
| 280 |
+
length = text.shape[1]
|
| 281 |
+
batch_valid = (batch_index >= 0) & (batch_index < text.shape[0])
|
| 282 |
+
relation_valid = (relation_index >= 0) & (relation_index < relation.shape[1])
|
| 283 |
+
valid = batch_valid & relation_valid & (pair_mask > 0.5)
|
| 284 |
+
batch = batch_index.long().clamp(0, text.shape[0] - 1)
|
| 285 |
+
rel_index = relation_index.long().clamp(0, relation.shape[1] - 1)
|
| 286 |
+
|
| 287 |
+
def gather(position):
|
| 288 |
+
return text[batch, position.long().clamp(0, length - 1)]
|
| 289 |
+
|
| 290 |
+
h_start = gather(head_start)
|
| 291 |
+
h_end = gather(head_end - 1)
|
| 292 |
+
t_start = gather(tail_start)
|
| 293 |
+
t_end = gather(tail_end - 1)
|
| 294 |
+
rel = relation[batch, rel_index]
|
| 295 |
+
delta = (tail_start - head_start).to(text.dtype)
|
| 296 |
+
order = torch.sign(delta).unsqueeze(-1)
|
| 297 |
+
distance = (delta.abs() / text_length.float().clamp_min(1.0)).unsqueeze(-1)
|
| 298 |
+
features = torch.cat((h_start, h_end, t_start, t_end, rel, order, distance), -1)
|
| 299 |
+
score = scorer.mlp(features).squeeze(-1)
|
| 300 |
+
if scorer.use_biaffine_content:
|
| 301 |
+
prefix = torch.cat(
|
| 302 |
+
(text.new_zeros(text.shape[0], 1, scorer.hidden_size), text.float().cumsum(1).to(text.dtype)),
|
| 303 |
+
dim=1,
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
def pool(start, end):
|
| 307 |
+
total = prefix[batch, end.long().clamp(0, length)] - prefix[batch, start.long().clamp(0, length)]
|
| 308 |
+
width = (end - start).clamp_min(1).unsqueeze(-1).to(total.dtype)
|
| 309 |
+
return total / width
|
| 310 |
+
|
| 311 |
+
head_content = scorer.head_content_projection(pool(head_start, head_end))
|
| 312 |
+
tail_content = scorer.tail_content_projection(pool(tail_start, tail_end))
|
| 313 |
+
gate = torch.sigmoid(scorer.relation_content_gate(rel))
|
| 314 |
+
biaffine = (head_content * gate * tail_content).sum(-1) / (scorer.hidden_size**0.5)
|
| 315 |
+
linear = scorer.content_linear(torch.cat((head_content, tail_content, rel), -1)).squeeze(-1)
|
| 316 |
+
score = score + biaffine + linear
|
| 317 |
+
return score.masked_fill(~valid, 0.0)
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
class ExtractionRecordAssignmentExport(torch.nn.Module):
|
| 321 |
+
"""All trained natural/latent/anchorless object and field assignment layers."""
|
| 322 |
+
|
| 323 |
+
def __init__(self, native):
|
| 324 |
+
super().__init__()
|
| 325 |
+
self.head = native.record_decoder
|
| 326 |
+
|
| 327 |
+
def forward(self, instances, field_queries, field_candidates):
|
| 328 |
+
head = self.head
|
| 329 |
+
instance_projection = head.inst_proj(instances)
|
| 330 |
+
field_projection = head.field_proj(field_queries)
|
| 331 |
+
query = instance_projection.unsqueeze(1) + field_projection.unsqueeze(0)
|
| 332 |
+
null_scores = torch.einsum("ifd,d->if", query, head.null_embed)
|
| 333 |
+
candidate_scores = torch.einsum("ifd,fcd->ifc", query, head.cand_proj(field_candidates))
|
| 334 |
+
assignment = torch.cat((null_scores.unsqueeze(-1), candidate_scores), -1)
|
| 335 |
+
object_scores = head.object_head(instances).squeeze(-1)
|
| 336 |
+
latent_scores = head.latent_seed_head(instances).squeeze(-1)
|
| 337 |
+
return assignment, object_scores, latent_scores
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
class ExtractionRecordAnchorlessExport(torch.nn.Module):
|
| 341 |
+
"""Trained learned-instance and contextual attention path for records."""
|
| 342 |
+
|
| 343 |
+
def __init__(self, native):
|
| 344 |
+
super().__init__()
|
| 345 |
+
self.head = native.record_decoder
|
| 346 |
+
|
| 347 |
+
def forward(self, context_states, context_mask):
|
| 348 |
+
head = self.head
|
| 349 |
+
instances = head.instance_embed
|
| 350 |
+
query = head.q_proj(instances)
|
| 351 |
+
key = head.k_proj(context_states)
|
| 352 |
+
value = head.v_proj(context_states)
|
| 353 |
+
attention = torch.matmul(query, key.transpose(-1, -2)) / math.sqrt(head.record_dim)
|
| 354 |
+
attention = attention.masked_fill(context_mask.unsqueeze(0) < 0.5, -1e4)
|
| 355 |
+
pooled = torch.matmul(torch.softmax(attention, -1), value)
|
| 356 |
+
return instances + pooled * (context_mask.sum() > 0).to(pooled.dtype)
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
class ExtractionExplicitSpanExport(torch.nn.Module):
|
| 360 |
+
"""Trained proposal prior and reranker for forced attribute/enum spans."""
|
| 361 |
+
|
| 362 |
+
def __init__(self, native):
|
| 363 |
+
super().__init__()
|
| 364 |
+
self.proposer = native.boundary_head.boundary_proposer
|
| 365 |
+
self.scorer = native.boundary_head.pair_scorer
|
| 366 |
+
|
| 367 |
+
def forward(
|
| 368 |
+
self,
|
| 369 |
+
text,
|
| 370 |
+
text_mask,
|
| 371 |
+
query,
|
| 372 |
+
query_mask,
|
| 373 |
+
boundary,
|
| 374 |
+
starts,
|
| 375 |
+
ends,
|
| 376 |
+
inside,
|
| 377 |
+
inside_mean,
|
| 378 |
+
indices,
|
| 379 |
+
valid_mask,
|
| 380 |
+
):
|
| 381 |
+
tm, qm = text_mask > 0.5, query_mask > 0.5
|
| 382 |
+
idx = indices.long()
|
| 383 |
+
legal = (
|
| 384 |
+
(idx[..., 0] >= 0)
|
| 385 |
+
& (idx[..., 1] > idx[..., 0])
|
| 386 |
+
& (idx[..., 1] <= tm.sum(-1).view(-1, 1, 1))
|
| 387 |
+
& qm.unsqueeze(-1)
|
| 388 |
+
& (valid_mask > 0.5)
|
| 389 |
+
)
|
| 390 |
+
compatibility = self.proposer.score_explicit_pairs(boundary, query, idx, legal)
|
| 391 |
+
proposals = BoundaryProposals(
|
| 392 |
+
indices=idx,
|
| 393 |
+
logits=None,
|
| 394 |
+
valid_mask=legal,
|
| 395 |
+
compat_logits=compatibility,
|
| 396 |
+
)
|
| 397 |
+
return self.scorer(
|
| 398 |
+
boundary,
|
| 399 |
+
query,
|
| 400 |
+
proposals,
|
| 401 |
+
starts,
|
| 402 |
+
ends,
|
| 403 |
+
inside,
|
| 404 |
+
tm.sum(-1).long(),
|
| 405 |
+
text,
|
| 406 |
+
tm,
|
| 407 |
+
inside_prefix_mean=inside_mean,
|
| 408 |
+
)
|
extraction_pool.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Weight-free GLiNER2 boundary candidate selection between Core ML stages.
|
| 2 |
+
|
| 3 |
+
The learned start/end projections are outputs of the first Core ML stage. This
|
| 4 |
+
module preserves GLiNER2 2.0.0's stable ranking and deduplication on the host.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import math
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
from gliner2.models.boundary.constants import MASK_LOGIT
|
| 11 |
+
from gliner2.models.boundary.indexing import gather_rows
|
| 12 |
+
from gliner2.models.boundary.pool import PooledCandidates, _deduplicate_pool
|
| 13 |
+
from gliner2.models.boundary.proposal import select_top_boundaries
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def select_candidates(
|
| 17 |
+
start_projection: torch.Tensor,
|
| 18 |
+
end_projection: torch.Tensor,
|
| 19 |
+
boundary_mask: torch.Tensor,
|
| 20 |
+
query_mask: torch.Tensor,
|
| 21 |
+
start_logits: torch.Tensor,
|
| 22 |
+
end_logits: torch.Tensor,
|
| 23 |
+
*,
|
| 24 |
+
boundary_top_k: int,
|
| 25 |
+
pool_size: int,
|
| 26 |
+
min_pool_per_query: int,
|
| 27 |
+
) -> PooledCandidates:
|
| 28 |
+
"""Select the native shared pool using already projected Core ML states."""
|
| 29 |
+
batch, n_boundaries, dim = start_projection.shape
|
| 30 |
+
n_queries = query_mask.shape[1]
|
| 31 |
+
if end_projection.shape != start_projection.shape:
|
| 32 |
+
raise ValueError("Start and end projections must have the same shape")
|
| 33 |
+
if start_logits.shape != (batch, n_queries, n_boundaries):
|
| 34 |
+
raise ValueError("Start logits do not match boundary and query dimensions")
|
| 35 |
+
if end_logits.shape != start_logits.shape:
|
| 36 |
+
raise ValueError("End logits do not match start logits")
|
| 37 |
+
if boundary_mask.shape != (batch, n_boundaries) or query_mask.shape != (batch, n_queries):
|
| 38 |
+
raise ValueError("Boundary or query mask has an unexpected shape")
|
| 39 |
+
|
| 40 |
+
floor = torch.full_like(start_logits, MASK_LOGIT)
|
| 41 |
+
valid_boundary = boundary_mask.unsqueeze(1) & query_mask.unsqueeze(-1)
|
| 42 |
+
union_start = torch.where(valid_boundary, start_logits, floor).amax(1)
|
| 43 |
+
union_end = torch.where(valid_boundary, end_logits, floor).amax(1)
|
| 44 |
+
union_valid = boundary_mask & query_mask.any(-1, keepdim=True)
|
| 45 |
+
_, starts, starts_valid = select_top_boundaries(union_start.unsqueeze(1), union_valid.unsqueeze(1), boundary_top_k)
|
| 46 |
+
_, ends, ends_valid = select_top_boundaries(union_end.unsqueeze(1), union_valid.unsqueeze(1), boundary_top_k)
|
| 47 |
+
starts, ends = starts[:, 0], ends[:, 0]
|
| 48 |
+
starts_valid, ends_valid = starts_valid[:, 0], ends_valid[:, 0]
|
| 49 |
+
n_starts, n_ends = starts.shape[1], ends.shape[1]
|
| 50 |
+
pair_start = starts.unsqueeze(-1).expand(batch, n_starts, n_ends).reshape(batch, -1)
|
| 51 |
+
pair_end = ends.unsqueeze(1).expand(batch, n_starts, n_ends).reshape(batch, -1)
|
| 52 |
+
pair_valid = (
|
| 53 |
+
starts_valid.unsqueeze(-1) & ends_valid.unsqueeze(1) & (ends.unsqueeze(1) > starts.unsqueeze(-1))
|
| 54 |
+
).reshape(batch, -1)
|
| 55 |
+
|
| 56 |
+
selected_start = gather_rows(start_projection, pair_start)
|
| 57 |
+
selected_end = gather_rows(end_projection, pair_end)
|
| 58 |
+
compatibility = (selected_start * selected_end).sum(-1) / math.sqrt(dim)
|
| 59 |
+
union_pair_score = (
|
| 60 |
+
compatibility
|
| 61 |
+
+ union_start.gather(1, pair_start.clamp(0, n_boundaries - 1))
|
| 62 |
+
+ union_end.gather(1, pair_end.clamp(0, n_boundaries - 1))
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
quota = min(min_pool_per_query, pair_start.shape[-1])
|
| 66 |
+
quota_keys = pair_start.new_zeros((batch, 0))
|
| 67 |
+
quota_scores = union_pair_score.new_zeros((batch, 0))
|
| 68 |
+
quota_valid = pair_valid.new_zeros((batch, 0))
|
| 69 |
+
if quota:
|
| 70 |
+
start_idx = pair_start.clamp(0, start_logits.shape[2] - 1).unsqueeze(1).expand(batch, n_queries, -1)
|
| 71 |
+
end_idx = pair_end.clamp(0, end_logits.shape[2] - 1).unsqueeze(1).expand(batch, n_queries, -1)
|
| 72 |
+
per_query = start_logits.gather(2, start_idx) + end_logits.gather(2, end_idx) + compatibility.unsqueeze(1)
|
| 73 |
+
per_query_valid = pair_valid.unsqueeze(1) & query_mask.unsqueeze(-1)
|
| 74 |
+
ranked = torch.argsort(
|
| 75 |
+
per_query.masked_fill(~per_query_valid, MASK_LOGIT),
|
| 76 |
+
dim=-1,
|
| 77 |
+
descending=True,
|
| 78 |
+
stable=True,
|
| 79 |
+
)[..., :quota]
|
| 80 |
+
quota_start = start_idx.gather(-1, ranked)
|
| 81 |
+
quota_end = end_idx.gather(-1, ranked)
|
| 82 |
+
quota_valid = per_query_valid.gather(-1, ranked).reshape(batch, -1)
|
| 83 |
+
quota_keys = (quota_start * n_boundaries + quota_end).reshape(batch, -1)
|
| 84 |
+
rank_bonus = torch.arange(quota, 0, -1, device=start_projection.device, dtype=union_pair_score.dtype)
|
| 85 |
+
quota_scores = (
|
| 86 |
+
union_pair_score.new_full((batch, n_queries, quota), -MASK_LOGIT * 0.5) + rank_bonus.view(1, 1, quota)
|
| 87 |
+
).reshape(batch, -1)
|
| 88 |
+
|
| 89 |
+
global_keys = pair_start * n_boundaries + pair_end
|
| 90 |
+
all_keys = torch.cat((quota_keys, global_keys), -1)
|
| 91 |
+
all_scores = torch.cat((quota_scores, union_pair_score.detach()), -1)
|
| 92 |
+
all_valid = torch.cat((quota_valid, pair_valid), -1)
|
| 93 |
+
selected_keys, selected_valid = _deduplicate_pool(all_keys, all_scores, all_valid, pool_size, n_boundaries)
|
| 94 |
+
selected_keys = torch.where(selected_valid, selected_keys, torch.zeros_like(selected_keys))
|
| 95 |
+
selected_s = torch.div(selected_keys, n_boundaries, rounding_mode="floor")
|
| 96 |
+
selected_e = selected_keys - selected_s * n_boundaries
|
| 97 |
+
indices = torch.stack((selected_s, selected_e), -1)
|
| 98 |
+
indices = torch.where(selected_valid.unsqueeze(-1), indices, torch.zeros_like(indices))
|
| 99 |
+
|
| 100 |
+
gathered_start = gather_rows(start_projection, selected_s)
|
| 101 |
+
gathered_end = gather_rows(end_projection, selected_e)
|
| 102 |
+
selected_compat = (gathered_start * gathered_end).sum(-1) / math.sqrt(dim)
|
| 103 |
+
selected_score = (
|
| 104 |
+
selected_compat
|
| 105 |
+
+ union_start.gather(1, selected_s.clamp(0, n_boundaries - 1))
|
| 106 |
+
+ union_end.gather(1, selected_e.clamp(0, n_boundaries - 1))
|
| 107 |
+
)
|
| 108 |
+
selected_score = selected_score.masked_fill(~selected_valid, MASK_LOGIT)
|
| 109 |
+
selected_compat = torch.where(selected_valid, selected_compat, torch.zeros_like(selected_compat))
|
| 110 |
+
return PooledCandidates(
|
| 111 |
+
indices=indices,
|
| 112 |
+
mask=selected_valid,
|
| 113 |
+
proposal_logits=selected_score,
|
| 114 |
+
gold_mask=None,
|
| 115 |
+
compat_logits=selected_compat,
|
| 116 |
+
stats=None,
|
| 117 |
+
)
|
extraction_runtime.py
ADDED
|
@@ -0,0 +1,525 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""GLiNER2.5 base extraction runtime using only Core ML trained weights."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
from contextvars import ContextVar
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import coremltools as ct
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
from gliner2.configuration import BoundaryHeadSettings
|
| 11 |
+
from gliner2.models.boundary.engine import BoundaryExtractor
|
| 12 |
+
from gliner2.models.boundary.records import RecordGroupOutput
|
| 13 |
+
from gliner2.models.boundary.relations import (
|
| 14 |
+
RelationProposalSettings,
|
| 15 |
+
RelationTypeSpec,
|
| 16 |
+
TypedRelationPairGenerator,
|
| 17 |
+
)
|
| 18 |
+
from gliner2.models.outputs import CandidateTensorBatch, ExtractorOutput
|
| 19 |
+
|
| 20 |
+
from convert_extraction_names import FEATURE_NAMES, SCORE_INPUT_NAMES
|
| 21 |
+
from extraction_pool import select_candidates
|
| 22 |
+
from preprocessing import load_processor, prepare_extraction
|
| 23 |
+
|
| 24 |
+
MODEL_PREFIX = "gliner2_base"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def to_tensor(value):
|
| 28 |
+
return torch.from_numpy(np.asarray(value).copy())
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def padded(value, size):
|
| 32 |
+
if value.shape[0] > size:
|
| 33 |
+
raise ValueError(f"Request exceeds Core ML bucket capacity {size}")
|
| 34 |
+
result = value.new_zeros((size, *value.shape[1:]))
|
| 35 |
+
result[: value.shape[0]] = value
|
| 36 |
+
return result
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class CoreMLBoundaryHead(torch.nn.Module):
|
| 40 |
+
"""Provide native decoder tensors from the converted extraction stages."""
|
| 41 |
+
|
| 42 |
+
def __init__(self, context: ContextVar, explicit_model, max_queries: int, max_spans: int):
|
| 43 |
+
super().__init__()
|
| 44 |
+
self.context = context
|
| 45 |
+
self.explicit_model = explicit_model
|
| 46 |
+
self.max_queries = max_queries
|
| 47 |
+
self.max_spans = max_spans
|
| 48 |
+
|
| 49 |
+
def forward(self, text, text_mask, query, query_mask, return_candidates=True):
|
| 50 |
+
state = self.context.get()
|
| 51 |
+
features = state["features"]
|
| 52 |
+
full = state["candidates"]
|
| 53 |
+
count = query.shape[1]
|
| 54 |
+
selected = CandidateTensorBatch(
|
| 55 |
+
indices=full.indices[:, :count],
|
| 56 |
+
proposal_logits=full.proposal_logits[:, :count],
|
| 57 |
+
pair_logits=full.pair_logits[:, :count],
|
| 58 |
+
valid_mask=full.valid_mask[:, :count],
|
| 59 |
+
query_mask=full.query_mask[:, :count],
|
| 60 |
+
candidate_states=full.candidate_states[:, :count],
|
| 61 |
+
)
|
| 62 |
+
return ExtractorOutput(
|
| 63 |
+
candidates=selected if return_candidates else None,
|
| 64 |
+
start_logits=features["start_logits"][:, :count],
|
| 65 |
+
end_logits=features["end_logits"][:, :count],
|
| 66 |
+
null_logits=features["null_logits"][:, :count],
|
| 67 |
+
count_log_rates=features["count_log_rates"][:, :count],
|
| 68 |
+
batch_size=1,
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
def score_explicit_spans(self, text, text_mask, query, query_mask, indices, valid_mask=None):
|
| 72 |
+
state = self.context.get()
|
| 73 |
+
features = state["features"]
|
| 74 |
+
full_queries = features["query_states"][0]
|
| 75 |
+
active_queries = int(state["arrays"]["query_mask"].sum())
|
| 76 |
+
query_count = query.shape[1]
|
| 77 |
+
span_count = indices.shape[2]
|
| 78 |
+
if query_count > self.max_queries or span_count > self.max_spans:
|
| 79 |
+
raise ValueError("Explicit-span request exceeds Core ML bucket capacity")
|
| 80 |
+
selected = []
|
| 81 |
+
for row in query[0]:
|
| 82 |
+
equal = torch.isclose(full_queries[:active_queries], row, atol=1e-6, rtol=0).all(-1)
|
| 83 |
+
matches = equal.nonzero(as_tuple=False).flatten()
|
| 84 |
+
if matches.numel() != 1:
|
| 85 |
+
raise ValueError("Explicit-span query cannot be mapped to the encoded schema")
|
| 86 |
+
selected.append(int(matches[0]))
|
| 87 |
+
selection = torch.tensor(selected, dtype=torch.long)
|
| 88 |
+
query_states = padded(query[0], self.max_queries).unsqueeze(0)
|
| 89 |
+
query_mask_padded = padded(query_mask[0].float(), self.max_queries).unsqueeze(0)
|
| 90 |
+
|
| 91 |
+
def selected_feature(name):
|
| 92 |
+
source = features[name][0].index_select(0, selection)
|
| 93 |
+
return padded(source, self.max_queries).unsqueeze(0)
|
| 94 |
+
|
| 95 |
+
span_indices = torch.zeros(1, self.max_queries, self.max_spans, 2, dtype=torch.int32)
|
| 96 |
+
span_mask = torch.zeros(1, self.max_queries, self.max_spans, dtype=torch.float32)
|
| 97 |
+
span_indices[:, :query_count, :span_count] = indices.int()
|
| 98 |
+
span_mask[:, :query_count, :span_count] = valid_mask.float() if valid_mask is not None else 1.0
|
| 99 |
+
values = (
|
| 100 |
+
text,
|
| 101 |
+
text_mask.float(),
|
| 102 |
+
query_states,
|
| 103 |
+
query_mask_padded,
|
| 104 |
+
features["boundary_states"],
|
| 105 |
+
selected_feature("start_logits"),
|
| 106 |
+
selected_feature("end_logits"),
|
| 107 |
+
selected_feature("inside_prefix"),
|
| 108 |
+
selected_feature("inside_prefix_mean"),
|
| 109 |
+
span_indices,
|
| 110 |
+
span_mask,
|
| 111 |
+
)
|
| 112 |
+
names = (
|
| 113 |
+
"text_states",
|
| 114 |
+
"text_mask",
|
| 115 |
+
"query_states",
|
| 116 |
+
"query_mask",
|
| 117 |
+
"boundary_states",
|
| 118 |
+
"start_logits",
|
| 119 |
+
"end_logits",
|
| 120 |
+
"inside_prefix",
|
| 121 |
+
"inside_prefix_mean",
|
| 122 |
+
"span_indices",
|
| 123 |
+
"span_mask",
|
| 124 |
+
)
|
| 125 |
+
output = self.explicit_model.predict(
|
| 126 |
+
{
|
| 127 |
+
name: value.numpy().astype(np.int32 if name == "span_indices" else np.float32)
|
| 128 |
+
for name, value in zip(names, values)
|
| 129 |
+
}
|
| 130 |
+
)["span_logits"]
|
| 131 |
+
return to_tensor(output)[:, :query_count, :span_count]
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
class CoreMLRelationScorer(torch.nn.Module):
|
| 135 |
+
"""Call the trained Core ML relation graph after native pair selection."""
|
| 136 |
+
|
| 137 |
+
def __init__(self, context: ContextVar, model, max_words: int, max_relations: int, pair_cap: int):
|
| 138 |
+
super().__init__()
|
| 139 |
+
self.context = context
|
| 140 |
+
self.model = model
|
| 141 |
+
self.max_words = max_words
|
| 142 |
+
self.max_relations = max_relations
|
| 143 |
+
self.pair_cap = pair_cap
|
| 144 |
+
|
| 145 |
+
def forward(self, text, relation, candidates, pairs):
|
| 146 |
+
count = len(pairs)
|
| 147 |
+
if text.shape[1] != self.max_words or relation.shape[1] > self.max_relations or count > self.pair_cap:
|
| 148 |
+
raise ValueError("Relation request exceeds Core ML bucket capacity")
|
| 149 |
+
relation_states = torch.zeros(1, self.max_relations, relation.shape[-1], dtype=relation.dtype)
|
| 150 |
+
relation_states[:, : relation.shape[1]] = relation
|
| 151 |
+
state = self.context.get()
|
| 152 |
+
text_length = int(state["arrays"]["text_mask"].sum())
|
| 153 |
+
fields = (
|
| 154 |
+
text,
|
| 155 |
+
torch.tensor([text_length], dtype=torch.int32),
|
| 156 |
+
relation_states,
|
| 157 |
+
padded(pairs.batch_index.int(), self.pair_cap),
|
| 158 |
+
padded(pairs.relation_index.int(), self.pair_cap),
|
| 159 |
+
padded(pairs.head_start.int(), self.pair_cap),
|
| 160 |
+
padded(pairs.head_end.int(), self.pair_cap),
|
| 161 |
+
padded(pairs.tail_start.int(), self.pair_cap),
|
| 162 |
+
padded(pairs.tail_end.int(), self.pair_cap),
|
| 163 |
+
padded(pairs.pair_mask.float(), self.pair_cap),
|
| 164 |
+
)
|
| 165 |
+
names = (
|
| 166 |
+
"text_states",
|
| 167 |
+
"text_length",
|
| 168 |
+
"relation_states",
|
| 169 |
+
"batch_index",
|
| 170 |
+
"relation_index",
|
| 171 |
+
"head_start",
|
| 172 |
+
"head_end",
|
| 173 |
+
"tail_start",
|
| 174 |
+
"tail_end",
|
| 175 |
+
"pair_mask",
|
| 176 |
+
)
|
| 177 |
+
output = self.model.predict(
|
| 178 |
+
{
|
| 179 |
+
name: value.numpy().astype(
|
| 180 |
+
np.float32 if name in ("text_states", "relation_states", "pair_mask") else np.int32
|
| 181 |
+
)
|
| 182 |
+
for name, value in zip(names, fields)
|
| 183 |
+
}
|
| 184 |
+
)["relation_logits"]
|
| 185 |
+
return to_tensor(output)[:count]
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
class CoreMLRecordHead(torch.nn.Module):
|
| 189 |
+
"""Keep GLiNER2's instance ordering while running learned layers in Core ML."""
|
| 190 |
+
|
| 191 |
+
def __init__(self, assignment_model, anchorless_model, max_fields=8, max_candidates=192, max_instances=1536):
|
| 192 |
+
super().__init__()
|
| 193 |
+
self.assignment_model = assignment_model
|
| 194 |
+
self.anchorless_model = anchorless_model
|
| 195 |
+
self.max_fields = max_fields
|
| 196 |
+
self.max_candidates = max_candidates
|
| 197 |
+
self.max_instances = max_instances
|
| 198 |
+
|
| 199 |
+
def forward_group(self, spec, query_states, candidates, sample_index):
|
| 200 |
+
field_specs = list(spec.fields)
|
| 201 |
+
field_query_ids = [field.query_id for field in field_specs]
|
| 202 |
+
if len(field_specs) > self.max_fields:
|
| 203 |
+
raise ValueError("Record has more fields than the Core ML bucket")
|
| 204 |
+
query_count = query_states.shape[0]
|
| 205 |
+
if any(query_id < 0 or query_id >= query_count for query_id in field_query_ids):
|
| 206 |
+
raise ValueError("Record field query is outside the encoded schema")
|
| 207 |
+
field_states, field_spans, field_logits, field_masks = [], [], [], []
|
| 208 |
+
for query_id in field_query_ids:
|
| 209 |
+
mask = candidates.valid_mask[sample_index, query_id]
|
| 210 |
+
if int(mask.sum()) > self.max_candidates:
|
| 211 |
+
raise ValueError("Record candidate count exceeds Core ML bucket")
|
| 212 |
+
field_states.append(candidates.candidate_states[sample_index, query_id][mask])
|
| 213 |
+
field_spans.append(candidates.indices[sample_index, query_id][mask])
|
| 214 |
+
field_logits.append(candidates.pair_logits[sample_index, query_id][mask])
|
| 215 |
+
field_masks.append(torch.ones(int(mask.sum()), dtype=torch.bool))
|
| 216 |
+
|
| 217 |
+
instance_seed = []
|
| 218 |
+
instance_spans = []
|
| 219 |
+
if spec.mode == "natural":
|
| 220 |
+
anchor = field_query_ids.index(spec.anchor_query_id)
|
| 221 |
+
instances = field_states[anchor]
|
| 222 |
+
for index, span in enumerate(field_spans[anchor]):
|
| 223 |
+
instance_seed.append((anchor, index))
|
| 224 |
+
instance_spans.append((int(span[0]), int(span[1])))
|
| 225 |
+
elif spec.mode == "latent":
|
| 226 |
+
instances = (
|
| 227 |
+
torch.cat(field_states, 0) if field_states else query_states.new_zeros((0, query_states.shape[-1]))
|
| 228 |
+
)
|
| 229 |
+
for field_index, spans in enumerate(field_spans):
|
| 230 |
+
for index, span in enumerate(spans):
|
| 231 |
+
instance_seed.append((field_index, index))
|
| 232 |
+
instance_spans.append((int(span[0]), int(span[1])))
|
| 233 |
+
else:
|
| 234 |
+
context_states = (
|
| 235 |
+
torch.cat(field_states, 0) if field_states else query_states.new_zeros((0, query_states.shape[-1]))
|
| 236 |
+
)
|
| 237 |
+
context_capacity = self.max_fields * self.max_candidates
|
| 238 |
+
context_mask = torch.zeros(context_capacity, dtype=torch.float32)
|
| 239 |
+
context_mask[: context_states.shape[0]] = 1.0
|
| 240 |
+
output = self.anchorless_model.predict(
|
| 241 |
+
{
|
| 242 |
+
"context_states": padded(context_states, context_capacity).numpy().astype(np.float32),
|
| 243 |
+
"context_mask": context_mask.numpy(),
|
| 244 |
+
}
|
| 245 |
+
)["instance_states"]
|
| 246 |
+
instances = to_tensor(output)
|
| 247 |
+
instance_seed = [None] * instances.shape[0]
|
| 248 |
+
instance_spans = [None] * instances.shape[0]
|
| 249 |
+
|
| 250 |
+
count = instances.shape[0]
|
| 251 |
+
if count > self.max_instances:
|
| 252 |
+
raise ValueError("Record instance count exceeds Core ML bucket")
|
| 253 |
+
hidden = query_states.shape[-1]
|
| 254 |
+
candidate_states = torch.zeros(self.max_fields, self.max_candidates, hidden)
|
| 255 |
+
for index, states in enumerate(field_states):
|
| 256 |
+
candidate_states[index, : states.shape[0]] = states
|
| 257 |
+
fields = (
|
| 258 |
+
padded(instances, self.max_instances),
|
| 259 |
+
padded(query_states[field_query_ids], self.max_fields),
|
| 260 |
+
candidate_states,
|
| 261 |
+
)
|
| 262 |
+
predicted = self.assignment_model.predict(
|
| 263 |
+
{
|
| 264 |
+
name: value.numpy().astype(np.float32)
|
| 265 |
+
for name, value in zip(("instance_states", "field_queries", "field_candidate_states"), fields)
|
| 266 |
+
}
|
| 267 |
+
)
|
| 268 |
+
assignment = to_tensor(predicted["assignment_logits"])
|
| 269 |
+
assignment_by_field = [
|
| 270 |
+
assignment[:count, field_index, : 1 + states.shape[0]] for field_index, states in enumerate(field_states)
|
| 271 |
+
]
|
| 272 |
+
if spec.mode == "natural":
|
| 273 |
+
object_logits = field_logits[field_query_ids.index(spec.anchor_query_id)]
|
| 274 |
+
elif spec.mode == "latent":
|
| 275 |
+
object_logits = to_tensor(predicted["latent_seed_logits"])[:count]
|
| 276 |
+
else:
|
| 277 |
+
object_logits = to_tensor(predicted["object_logits"])[:count]
|
| 278 |
+
return RecordGroupOutput(
|
| 279 |
+
spec=spec,
|
| 280 |
+
object_logits=object_logits,
|
| 281 |
+
assign_logits=assignment_by_field,
|
| 282 |
+
field_query_ids=field_query_ids,
|
| 283 |
+
field_specs=field_specs,
|
| 284 |
+
field_spans=field_spans,
|
| 285 |
+
field_cand_mask=field_masks,
|
| 286 |
+
field_cand_logits=field_logits,
|
| 287 |
+
instance_seed=instance_seed,
|
| 288 |
+
instance_spans=instance_spans,
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
class CoreMLBoundaryExtractor(BoundaryExtractor):
|
| 293 |
+
"""Native GLiNER2 schema/decoder with all trained extraction heads in Core ML."""
|
| 294 |
+
|
| 295 |
+
def __init__(self, model_dir: str, precision: str = "fp32", compute_units=ct.ComputeUnit.CPU_ONLY):
|
| 296 |
+
if precision not in ("fp16", "fp32"):
|
| 297 |
+
raise ValueError("precision must be fp16 or fp32")
|
| 298 |
+
torch.nn.Module.__init__(self)
|
| 299 |
+
folder = Path(model_dir)
|
| 300 |
+
config = json.loads((folder / "config.json").read_text())
|
| 301 |
+
self.boundary_settings = BoundaryHeadSettings(**config["boundary_head"])
|
| 302 |
+
self.processor = load_processor(str(folder / "tokenizer"))
|
| 303 |
+
self.enable_records = self.boundary_settings.enable_records
|
| 304 |
+
self.enable_relations = self.boundary_settings.enable_relations
|
| 305 |
+
self.strict_extraction = True
|
| 306 |
+
self.length, self.max_words, self.max_queries = 128, 64, 8
|
| 307 |
+
self._context = ContextVar("gliner2_coreml_extraction_context")
|
| 308 |
+
suffix = f"{precision}_L128_W64_Q8"
|
| 309 |
+
self.features_model = ct.models.MLModel(
|
| 310 |
+
str(folder / f"{MODEL_PREFIX}_extraction_features_{suffix}.mlpackage"), compute_units=compute_units
|
| 311 |
+
)
|
| 312 |
+
self.scorer_model = ct.models.MLModel(
|
| 313 |
+
str(folder / f"{MODEL_PREFIX}_extraction_scorer_{suffix}.mlpackage"), compute_units=compute_units
|
| 314 |
+
)
|
| 315 |
+
explicit = ct.models.MLModel(
|
| 316 |
+
str(folder / f"{MODEL_PREFIX}_explicit_{precision}_W64_Q8_S64.mlpackage"), compute_units=compute_units
|
| 317 |
+
)
|
| 318 |
+
relation = ct.models.MLModel(
|
| 319 |
+
str(folder / f"{MODEL_PREFIX}_relation_{precision}_W64_R4_P256.mlpackage"), compute_units=compute_units
|
| 320 |
+
)
|
| 321 |
+
assignment = ct.models.MLModel(
|
| 322 |
+
str(folder / f"{MODEL_PREFIX}_record_assignment_{precision}_F8_C192_I1536.mlpackage"),
|
| 323 |
+
compute_units=compute_units,
|
| 324 |
+
)
|
| 325 |
+
anchorless = ct.models.MLModel(
|
| 326 |
+
str(folder / f"{MODEL_PREFIX}_record_anchorless_{precision}_F8_C192_I1536.mlpackage"),
|
| 327 |
+
compute_units=compute_units,
|
| 328 |
+
)
|
| 329 |
+
self.boundary_head = CoreMLBoundaryHead(self._context, explicit, self.max_queries, 64)
|
| 330 |
+
self.relation_scorer = CoreMLRelationScorer(self._context, relation, self.max_words, 4, 256)
|
| 331 |
+
self.record_decoder = CoreMLRecordHead(assignment, anchorless)
|
| 332 |
+
self.relation_pair_generator = TypedRelationPairGenerator(
|
| 333 |
+
RelationProposalSettings(
|
| 334 |
+
heads_per_relation=self.boundary_settings.relation_heads_per_type,
|
| 335 |
+
tails_per_relation=self.boundary_settings.relation_tails_per_type,
|
| 336 |
+
pair_cap=self.boundary_settings.relation_pair_cap,
|
| 337 |
+
argument_threshold=self.boundary_settings.relation_argument_proposal_threshold,
|
| 338 |
+
)
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
def _encode_core(self, batch):
|
| 342 |
+
features = self._context.get()["features"]
|
| 343 |
+
query_states = features["query_states"]
|
| 344 |
+
text_states = features["text_states"]
|
| 345 |
+
query_mask = to_tensor(self._context.get()["arrays"]["query_mask"]).bool()
|
| 346 |
+
query_count = int(query_mask.sum())
|
| 347 |
+
query_states = query_states[:, :query_count]
|
| 348 |
+
query_mask = query_mask[:, :query_count]
|
| 349 |
+
text_mask = to_tensor(self._context.get()["arrays"]["text_mask"]).bool()
|
| 350 |
+
ext_specs, cls_specs, rel_specs, word_offsets = [], [], [], []
|
| 351 |
+
for sample_index in range(len(batch)):
|
| 352 |
+
specs = [
|
| 353 |
+
{
|
| 354 |
+
"group_index": item.task_index,
|
| 355 |
+
"field_index": item.role_index,
|
| 356 |
+
"task_type": item.task_type,
|
| 357 |
+
"task_name": item.task_name,
|
| 358 |
+
"field_name": item.role_name,
|
| 359 |
+
}
|
| 360 |
+
for item in batch.query_layouts[sample_index].queries
|
| 361 |
+
]
|
| 362 |
+
ext_specs.append(specs)
|
| 363 |
+
classifications = []
|
| 364 |
+
choice_offset = 0
|
| 365 |
+
for group_index in range(batch.schema_counts[sample_index]):
|
| 366 |
+
if batch.task_types[sample_index][group_index] != "classifications":
|
| 367 |
+
continue
|
| 368 |
+
count = max(len(batch.schema_special_indices[sample_index][group_index]) - 1, 0)
|
| 369 |
+
schema_tokens = batch.schema_tokens_list[sample_index][group_index]
|
| 370 |
+
if count:
|
| 371 |
+
classifications.append(
|
| 372 |
+
{
|
| 373 |
+
"group_index": group_index,
|
| 374 |
+
"task_name": schema_tokens[2],
|
| 375 |
+
"schema_tokens": schema_tokens,
|
| 376 |
+
"group_embs": features["classification_logits"][
|
| 377 |
+
sample_index, choice_offset : choice_offset + count
|
| 378 |
+
],
|
| 379 |
+
}
|
| 380 |
+
)
|
| 381 |
+
choice_offset += count
|
| 382 |
+
cls_specs.append(classifications)
|
| 383 |
+
word_offsets.append(
|
| 384 |
+
max(int(batch.text_word_counts[sample_index]) - len(batch.start_mappings[sample_index]), 0)
|
| 385 |
+
)
|
| 386 |
+
groups = {}
|
| 387 |
+
for query_id, spec in enumerate(specs):
|
| 388 |
+
groups.setdefault(spec["group_index"], []).append(query_id)
|
| 389 |
+
relations = []
|
| 390 |
+
for group_index, role_ids in groups.items():
|
| 391 |
+
if batch.task_types[sample_index][group_index] != "relations" or len(role_ids) < 2:
|
| 392 |
+
continue
|
| 393 |
+
head_id, tail_id = role_ids[:2]
|
| 394 |
+
role_states = query_states[sample_index, [head_id, tail_id]]
|
| 395 |
+
state = (
|
| 396 |
+
torch.cat((role_states[0], role_states[1]), -1)
|
| 397 |
+
if self.boundary_settings.directional_relation_states
|
| 398 |
+
else role_states.mean(0)
|
| 399 |
+
)
|
| 400 |
+
relations.append(
|
| 401 |
+
{
|
| 402 |
+
"group_index": group_index,
|
| 403 |
+
"relation_type": specs[head_id]["task_name"],
|
| 404 |
+
"spec": RelationTypeSpec(
|
| 405 |
+
specs[head_id]["task_name"], head_query_ids=(head_id,), tail_query_ids=(tail_id,)
|
| 406 |
+
),
|
| 407 |
+
"query_state": state,
|
| 408 |
+
}
|
| 409 |
+
)
|
| 410 |
+
rel_specs.append(relations)
|
| 411 |
+
return {
|
| 412 |
+
"text_states": text_states,
|
| 413 |
+
"text_mask": text_mask,
|
| 414 |
+
"text_lengths": text_mask.sum(-1).long(),
|
| 415 |
+
"query_states": query_states,
|
| 416 |
+
"query_mask": query_mask,
|
| 417 |
+
"ext_specs": ext_specs,
|
| 418 |
+
"cls_specs": cls_specs,
|
| 419 |
+
"rel_specs": rel_specs,
|
| 420 |
+
"word_offsets": word_offsets,
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
def _extract_classification_result(self, results, schema_name, schema, embs, schema_tokens, temperature=1.0):
|
| 424 |
+
cls_config = self._resolve_classification_config(schema_tokens[2], schema.get("classifications", []))
|
| 425 |
+
if cls_config is None:
|
| 426 |
+
return
|
| 427 |
+
if temperature <= 0:
|
| 428 |
+
raise ValueError("Classification temperature must be positive")
|
| 429 |
+
logits = embs / temperature
|
| 430 |
+
activation = cls_config.get("class_act", "auto")
|
| 431 |
+
multi_label = cls_config.get("multi_label", False)
|
| 432 |
+
if activation == "sigmoid" or (activation != "softmax" and multi_label):
|
| 433 |
+
probabilities = torch.sigmoid(logits)
|
| 434 |
+
else:
|
| 435 |
+
probabilities = torch.softmax(logits, dim=-1)
|
| 436 |
+
labels = cls_config["labels"]
|
| 437 |
+
if multi_label:
|
| 438 |
+
threshold = cls_config.get("cls_threshold", 0.5)
|
| 439 |
+
chosen = [
|
| 440 |
+
(labels[index], float(probabilities[index]))
|
| 441 |
+
for index in range(len(labels))
|
| 442 |
+
if float(probabilities[index]) >= threshold
|
| 443 |
+
]
|
| 444 |
+
if not chosen:
|
| 445 |
+
best = int(probabilities.argmax())
|
| 446 |
+
chosen = [(labels[best], float(probabilities[best]))]
|
| 447 |
+
results[cls_config["task"]] = chosen
|
| 448 |
+
return
|
| 449 |
+
best = int(probabilities.argmax())
|
| 450 |
+
results[cls_config["task"]] = (labels[best], float(probabilities[best]))
|
| 451 |
+
|
| 452 |
+
def extract(
|
| 453 |
+
self,
|
| 454 |
+
text: str,
|
| 455 |
+
schema,
|
| 456 |
+
threshold: float = 0.5,
|
| 457 |
+
format_results: bool = True,
|
| 458 |
+
include_confidence: bool = False,
|
| 459 |
+
include_spans: bool = False,
|
| 460 |
+
overlap_policy=None,
|
| 461 |
+
):
|
| 462 |
+
schema_dicts, metadata_list = self._build_schema_dicts_and_metadata([schema])
|
| 463 |
+
if overlap_policy is not None:
|
| 464 |
+
metadata_list[0]["_overlap_policy"] = self._resolved_overlap_policy(overlap_policy)
|
| 465 |
+
arrays, batch = prepare_extraction(
|
| 466 |
+
self.processor, text, schema_dicts[0], self.length, self.max_words, self.max_queries
|
| 467 |
+
)
|
| 468 |
+
predicted = self.features_model.predict(arrays)
|
| 469 |
+
features = {name: to_tensor(predicted[name]) for name in FEATURE_NAMES}
|
| 470 |
+
query_mask = to_tensor(arrays["query_mask"]).bool()
|
| 471 |
+
candidates = None
|
| 472 |
+
if bool(query_mask.any()):
|
| 473 |
+
head = self.boundary_settings
|
| 474 |
+
pool = select_candidates(
|
| 475 |
+
features["pool_start_projection"],
|
| 476 |
+
features["pool_end_projection"],
|
| 477 |
+
features["boundary_mask"].bool(),
|
| 478 |
+
query_mask,
|
| 479 |
+
features["start_logits"],
|
| 480 |
+
features["end_logits"],
|
| 481 |
+
boundary_top_k=head.pool_boundary_top_k,
|
| 482 |
+
pool_size=head.pool_size,
|
| 483 |
+
min_pool_per_query=head.min_pool_per_query,
|
| 484 |
+
)
|
| 485 |
+
values = (
|
| 486 |
+
features["text_states"],
|
| 487 |
+
to_tensor(arrays["text_mask"]),
|
| 488 |
+
features["query_states"],
|
| 489 |
+
to_tensor(arrays["query_mask"]),
|
| 490 |
+
features["boundary_states"],
|
| 491 |
+
features["start_logits"],
|
| 492 |
+
features["end_logits"],
|
| 493 |
+
features["inside_prefix"],
|
| 494 |
+
features["inside_prefix_mean"],
|
| 495 |
+
pool.indices.int(),
|
| 496 |
+
pool.mask.float(),
|
| 497 |
+
pool.compat_logits,
|
| 498 |
+
)
|
| 499 |
+
scored = self.scorer_model.predict(
|
| 500 |
+
{
|
| 501 |
+
name: value.numpy().astype(np.int32 if name == "candidate_indices" else np.float32)
|
| 502 |
+
for name, value in zip(SCORE_INPUT_NAMES, values)
|
| 503 |
+
}
|
| 504 |
+
)
|
| 505 |
+
candidates = CandidateTensorBatch(
|
| 506 |
+
indices=pool.indices.unsqueeze(1).expand(1, self.max_queries, -1, 2),
|
| 507 |
+
proposal_logits=pool.proposal_logits.unsqueeze(1).expand(1, self.max_queries, -1),
|
| 508 |
+
pair_logits=to_tensor(scored["pair_logits"]),
|
| 509 |
+
valid_mask=pool.mask.unsqueeze(1).expand(1, self.max_queries, -1),
|
| 510 |
+
query_mask=query_mask,
|
| 511 |
+
candidate_states=to_tensor(scored["candidate_states"]).unsqueeze(1).expand(1, self.max_queries, -1, -1),
|
| 512 |
+
)
|
| 513 |
+
token = self._context.set({"arrays": arrays, "features": features, "candidates": candidates})
|
| 514 |
+
try:
|
| 515 |
+
raw = self._extract_from_batch(batch, threshold, metadata_list, include_confidence, include_spans)[0]
|
| 516 |
+
if format_results:
|
| 517 |
+
return self.format_results(
|
| 518 |
+
raw,
|
| 519 |
+
include_confidence,
|
| 520 |
+
metadata_list[0].get("relation_order", []),
|
| 521 |
+
metadata_list[0].get("classification_tasks", []),
|
| 522 |
+
)
|
| 523 |
+
return raw
|
| 524 |
+
finally:
|
| 525 |
+
self._context.reset(token)
|
gliner2_base_explicit_fp16_W64_Q8_S64.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:833cfc75c7b45d95f423b00770569ad00707aad4d7e993d9288aacd6392901be
|
| 3 |
+
size 61009
|
gliner2_base_explicit_fp16_W64_Q8_S64.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:b8f472bf52874cf6fbe92e08333cb1976236fe179a186efeba049f1814ca2f6f
|
| 3 |
+
size 552384
|
gliner2_base_explicit_fp16_W64_Q8_S64.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"D726DDD0-C257-44BB-B2AB-E6F00C6422DA": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Weights",
|
| 7 |
+
"name": "weights",
|
| 8 |
+
"path": "com.apple.CoreML/weights"
|
| 9 |
+
},
|
| 10 |
+
"EA5D631C-0A1C-45BC-BA64-A5F37269708E": {
|
| 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": "EA5D631C-0A1C-45BC-BA64-A5F37269708E"
|
| 18 |
+
}
|
gliner2_base_explicit_fp32_W64_Q8_S64.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6226a1f7b91ceb1b2528d5eac17fa7b30db7c6e921ebc4a8b1b7ad4b8dca9096
|
| 3 |
+
size 48811
|
gliner2_base_explicit_fp32_W64_Q8_S64.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:7dded10597f6d93d30f9842d963e6a1ed503a7e1ed53d6f713ee22da38101e1f
|
| 3 |
+
size 1103040
|
gliner2_base_explicit_fp32_W64_Q8_S64.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"7CEC5B51-AEEB-4F06-BB27-342E29DF9350": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Specification",
|
| 7 |
+
"name": "model.mlmodel",
|
| 8 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 9 |
+
},
|
| 10 |
+
"AD3570EB-1FD4-4674-9D29-2A3B6B2083D7": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Weights",
|
| 13 |
+
"name": "weights",
|
| 14 |
+
"path": "com.apple.CoreML/weights"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "7CEC5B51-AEEB-4F06-BB27-342E29DF9350"
|
| 18 |
+
}
|
gliner2_base_extraction_features_fp16_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:81a989bd9c9791ca2cb2b10c9456795a3f98cb2a41132ad894ec6aae3bee8c58
|
| 3 |
+
size 1046087
|
gliner2_base_extraction_features_fp16_L128_W64_Q8.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:f89a14b93e79bc0c5f03ae1d2afdecabc12d0ba0a6551107efb60d19e2fef913
|
| 3 |
+
size 389951744
|
gliner2_base_extraction_features_fp16_L128_W64_Q8.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"CE6FE6C9-1B05-44D1-86BE-D05BA3A90F48": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Weights",
|
| 7 |
+
"name": "weights",
|
| 8 |
+
"path": "com.apple.CoreML/weights"
|
| 9 |
+
},
|
| 10 |
+
"FABF84D1-9ACF-40E8-AD85-5BFDFA7D831E": {
|
| 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": "FABF84D1-9ACF-40E8-AD85-5BFDFA7D831E"
|
| 18 |
+
}
|
gliner2_base_extraction_features_fp32_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:952aba39fb31af06ae240e1c03ce0a9fe065c2e5997d4f455fb0df2055059103
|
| 3 |
+
size 1012396
|
gliner2_base_extraction_features_fp32_L128_W64_Q8.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:3edc72e18c141125c4231b7a3b0d7d0105135271f7deff924818951e6f354ef0
|
| 3 |
+
size 779886020
|
gliner2_base_extraction_features_fp32_L128_W64_Q8.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"27195BA1-5D5E-4583-8356-AEF31C964DF5": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Weights",
|
| 7 |
+
"name": "weights",
|
| 8 |
+
"path": "com.apple.CoreML/weights"
|
| 9 |
+
},
|
| 10 |
+
"B9ECF184-78AE-4CED-A667-63FF8B2342CE": {
|
| 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": "B9ECF184-78AE-4CED-A667-63FF8B2342CE"
|
| 18 |
+
}
|
gliner2_base_extraction_scorer_fp16_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a28d35b1697ca5877e4c37be3fc73636a6795c7a7fb589aadee053ecbc06d496
|
| 3 |
+
size 42953
|
gliner2_base_extraction_scorer_fp16_L128_W64_Q8.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:63e7a5f12ea123730faf9248e3d5245d8861cda7bdc678a29b6ed10c1c0450a3
|
| 3 |
+
size 859584
|
gliner2_base_extraction_scorer_fp16_L128_W64_Q8.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"3FDDF70C-07D4-4EC0-B0F5-C918F277B00E": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Specification",
|
| 7 |
+
"name": "model.mlmodel",
|
| 8 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 9 |
+
},
|
| 10 |
+
"44AFC159-B1AA-41A7-B5E7-7A63C4CADFB4": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Weights",
|
| 13 |
+
"name": "weights",
|
| 14 |
+
"path": "com.apple.CoreML/weights"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "3FDDF70C-07D4-4EC0-B0F5-C918F277B00E"
|
| 18 |
+
}
|
gliner2_base_extraction_scorer_fp32_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dc02f86228a4c95872de1d6dab5d5a1ca2d89c464ba1df0eb1f4333cc4f517f9
|
| 3 |
+
size 32574
|
gliner2_base_extraction_scorer_fp32_L128_W64_Q8.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:ab96965841dd7802bb22d3a4d65ac2ef27e4df4daf4be1a49a65d8bc3c87f595
|
| 3 |
+
size 1717440
|
gliner2_base_extraction_scorer_fp32_L128_W64_Q8.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"39A0473F-D89D-43AB-A591-4D885B89F208": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Weights",
|
| 7 |
+
"name": "weights",
|
| 8 |
+
"path": "com.apple.CoreML/weights"
|
| 9 |
+
},
|
| 10 |
+
"403A1040-2367-428B-A9F2-2E969F0B9FDC": {
|
| 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": "403A1040-2367-428B-A9F2-2E969F0B9FDC"
|
| 18 |
+
}
|
gliner2_base_record_anchorless_fp16_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3ae7e7e6540577572bc1ae82d334d9294dd2bfb2d270cdf5166edcb9ce487495
|
| 3 |
+
size 6558
|
gliner2_base_record_anchorless_fp16_F8_C192_I1536.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:73f5d6812653173c71dcd666e9477dc275fae42290bb0bfa5aef00e1c721a7f5
|
| 3 |
+
size 1435840
|
gliner2_base_record_anchorless_fp16_F8_C192_I1536.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"299C8B61-4089-4454-B109-2D7002032011": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Weights",
|
| 7 |
+
"name": "weights",
|
| 8 |
+
"path": "com.apple.CoreML/weights"
|
| 9 |
+
},
|
| 10 |
+
"63E77F60-B2F1-4E33-9505-B84C84607AC7": {
|
| 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": "63E77F60-B2F1-4E33-9505-B84C84607AC7"
|
| 18 |
+
}
|
gliner2_base_record_anchorless_fp32_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fa573f67c4b36f80f02668d377abd244956d1afba07be7d55866080b8188b6ef
|
| 3 |
+
size 4994
|
gliner2_base_record_anchorless_fp32_F8_C192_I1536.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:f48cb084768846394746b4a39d189a47be19392708075f854af3dfe8076db4cd
|
| 3 |
+
size 2871232
|
gliner2_base_record_anchorless_fp32_F8_C192_I1536.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"05BE39C5-5E8D-408A-802C-C14032032B68": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Weights",
|
| 7 |
+
"name": "weights",
|
| 8 |
+
"path": "com.apple.CoreML/weights"
|
| 9 |
+
},
|
| 10 |
+
"8059378D-AEA5-4997-8297-940F2F714C91": {
|
| 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": "8059378D-AEA5-4997-8297-940F2F714C91"
|
| 18 |
+
}
|
gliner2_base_record_assignment_fp16_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5ff5970e8851aeae902aac01e4c4586d453b0f39797f342d9a9b2a2791d7e605
|
| 3 |
+
size 9981
|
gliner2_base_record_assignment_fp16_F8_C192_I1536.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:d59545d5a9df722194e38a0a3ee03c6a5f95c58fa9608f4b2f3f6e3f56d0136e
|
| 3 |
+
size 594560
|
gliner2_base_record_assignment_fp16_F8_C192_I1536.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"68A464FA-A24A-46B9-9356-80E6888F5D9D": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Specification",
|
| 7 |
+
"name": "model.mlmodel",
|
| 8 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 9 |
+
},
|
| 10 |
+
"A795C3EE-066E-4321-A479-969549184B3F": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Weights",
|
| 13 |
+
"name": "weights",
|
| 14 |
+
"path": "com.apple.CoreML/weights"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "68A464FA-A24A-46B9-9356-80E6888F5D9D"
|
| 18 |
+
}
|
gliner2_base_record_assignment_fp32_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:63cad042b4d8487a0e6f57eec23a6a69f8687c33720c5213b8d52c1dd2169572
|
| 3 |
+
size 7462
|
gliner2_base_record_assignment_fp32_F8_C192_I1536.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:5f870796d2a8e0539b20964ebcd80cae65677ed11d4b36b12df9a21d158181c1
|
| 3 |
+
size 1188480
|
gliner2_base_record_assignment_fp32_F8_C192_I1536.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"832C4AAB-6488-45A5-B0EB-42E172A71A5B": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Weights",
|
| 7 |
+
"name": "weights",
|
| 8 |
+
"path": "com.apple.CoreML/weights"
|
| 9 |
+
},
|
| 10 |
+
"E0009128-6F7C-4DFF-8EE5-473660E36248": {
|
| 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": "E0009128-6F7C-4DFF-8EE5-473660E36248"
|
| 18 |
+
}
|
gliner2_base_relation_fp16_W64_R4_P256.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e6562f5850272ffe0b38363da5a85a70492e170e547f58b008021b6a8aa375ec
|
| 3 |
+
size 32410
|
gliner2_base_relation_fp16_W64_R4_P256.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:1577561a8686c9657ec38df97ff701cbc7b588da3efae6404e33622ab1cc77e5
|
| 3 |
+
size 11815680
|
gliner2_base_relation_fp16_W64_R4_P256.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"21433269-C580-4889-9FA2-579489ECCCB1": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Weights",
|
| 7 |
+
"name": "weights",
|
| 8 |
+
"path": "com.apple.CoreML/weights"
|
| 9 |
+
},
|
| 10 |
+
"6A4D1B00-0FCB-4AA0-B64F-43CF443BCA7C": {
|
| 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": "6A4D1B00-0FCB-4AA0-B64F-43CF443BCA7C"
|
| 18 |
+
}
|
gliner2_base_relation_fp32_W64_R4_P256.mlpackage/Data/com.apple.CoreML/model.mlmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5970ae822c86e3f11ef86341034824661fa10184730953ac4bcdb7086e87a6ea
|
| 3 |
+
size 29823
|
gliner2_base_relation_fp32_W64_R4_P256.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:1dd7c8e640688002c174080710cc5ba3fa32c5dc399b52ef0bf7f4c5ef8d01cb
|
| 3 |
+
size 23630592
|
gliner2_base_relation_fp32_W64_R4_P256.mlpackage/Manifest.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"fileFormatVersion": "1.0.0",
|
| 3 |
+
"itemInfoEntries": {
|
| 4 |
+
"1BDDDB06-D37B-4A40-BEAB-8A1B63278068": {
|
| 5 |
+
"author": "com.apple.CoreML",
|
| 6 |
+
"description": "CoreML Model Specification",
|
| 7 |
+
"name": "model.mlmodel",
|
| 8 |
+
"path": "com.apple.CoreML/model.mlmodel"
|
| 9 |
+
},
|
| 10 |
+
"ACF6B577-6E60-42EF-B900-AA17259DF0D8": {
|
| 11 |
+
"author": "com.apple.CoreML",
|
| 12 |
+
"description": "CoreML Model Weights",
|
| 13 |
+
"name": "weights",
|
| 14 |
+
"path": "com.apple.CoreML/weights"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"rootModelIdentifier": "1BDDDB06-D37B-4A40-BEAB-8A1B63278068"
|
| 18 |
+
}
|
preprocessing.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 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
|
|
@@ -10,11 +11,13 @@ 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 |
|
|
@@ -41,3 +44,46 @@ def prepare_with_processor(processor, text: str, task: str, labels: list[str], l
|
|
| 41 |
"marker_indices": indices.astype(np.int32),
|
| 42 |
"marker_mask": mask.astype(np.float32),
|
| 43 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""Native GLiNER2 schema preprocessing for a fixed Core ML bucket."""
|
| 2 |
+
|
| 3 |
import numpy as np
|
| 4 |
from gliner2 import Schema
|
| 5 |
from gliner2.models.base import load_extractor_tokenizer
|
|
|
|
| 11 |
"""Load only the tokenizer and schema formatter needed by the Core ML model."""
|
| 12 |
return SchemaTransformer(tokenizer=load_extractor_tokenizer(tokenizer_dir), token_pooling="first")
|
| 13 |
|
| 14 |
+
|
| 15 |
def native_batch(native, text: str, task: str, labels: list[str], length: int):
|
| 16 |
schema = Schema().classification(task, labels)
|
| 17 |
collator = ExtractorCollator(native.processor, is_training=False, max_len=length, architecture=native.architecture)
|
| 18 |
return collator([(text, schema.build())])
|
| 19 |
|
| 20 |
+
|
| 21 |
def prepare_classification(native, text: str, task: str, labels: list[str], length: int, max_options: int):
|
| 22 |
return prepare_with_processor(native.processor, text, task, labels, length, max_options)
|
| 23 |
|
|
|
|
| 44 |
"marker_indices": indices.astype(np.int32),
|
| 45 |
"marker_mask": mask.astype(np.float32),
|
| 46 |
}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def prepare_extraction(
|
| 50 |
+
processor,
|
| 51 |
+
text: str,
|
| 52 |
+
schema,
|
| 53 |
+
length: int,
|
| 54 |
+
max_words: int,
|
| 55 |
+
max_queries: int,
|
| 56 |
+
max_choices: int = 8,
|
| 57 |
+
):
|
| 58 |
+
"""Prepare an extractive schema without allowing upstream word truncation."""
|
| 59 |
+
if min(length, max_words, max_queries, max_choices) < 1:
|
| 60 |
+
raise ValueError("Extraction bucket dimensions must all be positive")
|
| 61 |
+
built_schema = schema.build() if hasattr(schema, "build") else schema
|
| 62 |
+
collator = ExtractorCollator(processor, is_training=False, max_len=None, architecture="boundary")
|
| 63 |
+
batch = collator([(text, built_schema)])
|
| 64 |
+
if batch.input_ids.shape[1] > length:
|
| 65 |
+
raise ValueError(f"Schema and text require {batch.input_ids.shape[1]} subwords; bucket holds {length}")
|
| 66 |
+
if batch.text_word_indices.shape[1] > max_words:
|
| 67 |
+
raise ValueError(f"Text requires {batch.text_word_indices.shape[1]} words; bucket holds {max_words}")
|
| 68 |
+
if batch.query_marker_indices.shape[1] > max_queries:
|
| 69 |
+
raise ValueError(f"Schema requires {batch.query_marker_indices.shape[1]} queries; bucket holds {max_queries}")
|
| 70 |
+
if batch.cls_marker_indices.shape[1] > max_choices:
|
| 71 |
+
raise ValueError(f"Schema requires {batch.cls_marker_indices.shape[1]} choices; bucket holds {max_choices}")
|
| 72 |
+
if batch.query_marker_indices.shape[1] == 0 and batch.cls_marker_indices.shape[1] == 0:
|
| 73 |
+
raise ValueError("Schema has no extraction or classification queries")
|
| 74 |
+
|
| 75 |
+
def padded(values, width, fill=0):
|
| 76 |
+
array = values.numpy()
|
| 77 |
+
return np.pad(array, ((0, 0), (0, width - array.shape[1])), constant_values=fill)
|
| 78 |
+
|
| 79 |
+
arrays = {
|
| 80 |
+
"input_ids": padded(batch.input_ids, length, processor.tokenizer.pad_token_id).astype(np.int32),
|
| 81 |
+
"attention_mask": padded(batch.attention_mask, length).astype(np.int32),
|
| 82 |
+
"text_indices": padded(batch.text_word_indices, max_words).astype(np.int32),
|
| 83 |
+
"text_mask": padded(batch.text_word_mask, max_words).astype(np.float32),
|
| 84 |
+
"query_indices": padded(batch.query_marker_indices, max_queries).astype(np.int32),
|
| 85 |
+
"query_mask": padded(batch.query_marker_mask, max_queries).astype(np.float32),
|
| 86 |
+
"cls_indices": padded(batch.cls_marker_indices, max_choices).astype(np.int32),
|
| 87 |
+
"cls_mask": padded(batch.cls_marker_mask, max_choices).astype(np.float32),
|
| 88 |
+
}
|
| 89 |
+
return arrays, batch
|
pyproject.toml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
[project]
|
| 2 |
name = "gliner2-base-coreml"
|
| 3 |
version = "0.1.0"
|
| 4 |
-
description = "Pinned GLiNER 2.5
|
| 5 |
requires-python = ">=3.12,<3.13"
|
| 6 |
dependencies = [
|
| 7 |
"coremltools==9.0",
|
|
@@ -9,6 +9,7 @@ dependencies = [
|
|
| 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",
|
|
@@ -16,6 +17,7 @@ dependencies = [
|
|
| 16 |
|
| 17 |
[dependency-groups]
|
| 18 |
dev = ["pytest>=8.4", "ruff>=0.13"]
|
|
|
|
| 19 |
|
| 20 |
[tool.pytest.ini_options]
|
| 21 |
testpaths = ["tests"]
|
|
|
|
| 1 |
[project]
|
| 2 |
name = "gliner2-base-coreml"
|
| 3 |
version = "0.1.0"
|
| 4 |
+
description = "Pinned GLiNER 2.5 base Core ML classification and extraction export"
|
| 5 |
requires-python = ">=3.12,<3.13"
|
| 6 |
dependencies = [
|
| 7 |
"coremltools==9.0",
|
|
|
|
| 9 |
"huggingface-hub>=0.34,<1",
|
| 10 |
"numpy<2.3",
|
| 11 |
"protobuf>=5,<7",
|
| 12 |
+
"psutil>=7,<8",
|
| 13 |
"sentencepiece>=0.2,<0.3",
|
| 14 |
"torch==2.7.0",
|
| 15 |
"transformers==4.57.6",
|
|
|
|
| 17 |
|
| 18 |
[dependency-groups]
|
| 19 |
dev = ["pytest>=8.4", "ruff>=0.13"]
|
| 20 |
+
compression = ["scikit-learn==1.5.1"]
|
| 21 |
|
| 22 |
[tool.pytest.ini_options]
|
| 23 |
testpaths = ["tests"]
|