alexwengg commited on
Commit
3bfcc7d
·
verified ·
1 Parent(s): 85a9b84

Add validated GLiNER2 extraction Core ML stages

Browse files

FP32 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
Files changed (50) hide show
  1. README.md +27 -23
  2. benchmark-extraction.py +76 -0
  3. config.json +99 -0
  4. convert-explicit-coreml.py +170 -0
  5. convert-extraction-coreml.py +217 -0
  6. convert-record-coreml.py +222 -0
  7. convert-relation-coreml.py +165 -0
  8. convert_extraction_names.py +31 -0
  9. extraction-assets.lock.json +275 -0
  10. extraction_export.py +408 -0
  11. extraction_pool.py +117 -0
  12. extraction_runtime.py +525 -0
  13. gliner2_base_explicit_fp16_W64_Q8_S64.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  14. gliner2_base_explicit_fp16_W64_Q8_S64.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  15. gliner2_base_explicit_fp16_W64_Q8_S64.mlpackage/Manifest.json +18 -0
  16. gliner2_base_explicit_fp32_W64_Q8_S64.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  17. gliner2_base_explicit_fp32_W64_Q8_S64.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  18. gliner2_base_explicit_fp32_W64_Q8_S64.mlpackage/Manifest.json +18 -0
  19. gliner2_base_extraction_features_fp16_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  20. gliner2_base_extraction_features_fp16_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  21. gliner2_base_extraction_features_fp16_L128_W64_Q8.mlpackage/Manifest.json +18 -0
  22. gliner2_base_extraction_features_fp32_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  23. gliner2_base_extraction_features_fp32_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  24. gliner2_base_extraction_features_fp32_L128_W64_Q8.mlpackage/Manifest.json +18 -0
  25. gliner2_base_extraction_scorer_fp16_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  26. gliner2_base_extraction_scorer_fp16_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  27. gliner2_base_extraction_scorer_fp16_L128_W64_Q8.mlpackage/Manifest.json +18 -0
  28. gliner2_base_extraction_scorer_fp32_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  29. gliner2_base_extraction_scorer_fp32_L128_W64_Q8.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  30. gliner2_base_extraction_scorer_fp32_L128_W64_Q8.mlpackage/Manifest.json +18 -0
  31. gliner2_base_record_anchorless_fp16_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  32. gliner2_base_record_anchorless_fp16_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  33. gliner2_base_record_anchorless_fp16_F8_C192_I1536.mlpackage/Manifest.json +18 -0
  34. gliner2_base_record_anchorless_fp32_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  35. gliner2_base_record_anchorless_fp32_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  36. gliner2_base_record_anchorless_fp32_F8_C192_I1536.mlpackage/Manifest.json +18 -0
  37. gliner2_base_record_assignment_fp16_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  38. gliner2_base_record_assignment_fp16_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  39. gliner2_base_record_assignment_fp16_F8_C192_I1536.mlpackage/Manifest.json +18 -0
  40. gliner2_base_record_assignment_fp32_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  41. gliner2_base_record_assignment_fp32_F8_C192_I1536.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  42. gliner2_base_record_assignment_fp32_F8_C192_I1536.mlpackage/Manifest.json +18 -0
  43. gliner2_base_relation_fp16_W64_R4_P256.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  44. gliner2_base_relation_fp16_W64_R4_P256.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  45. gliner2_base_relation_fp16_W64_R4_P256.mlpackage/Manifest.json +18 -0
  46. gliner2_base_relation_fp32_W64_R4_P256.mlpackage/Data/com.apple.CoreML/model.mlmodel +3 -0
  47. gliner2_base_relation_fp32_W64_R4_P256.mlpackage/Data/com.apple.CoreML/weights/weight.bin +3 -0
  48. gliner2_base_relation_fp32_W64_R4_P256.mlpackage/Manifest.json +18 -0
  49. preprocessing.py +46 -0
  50. pyproject.toml +3 -1
README.md CHANGED
@@ -1,40 +1,44 @@
1
  ---
2
  license: apache-2.0
3
  library_name: coremltools
4
- pipeline_tag: text-classification
5
  tags:
6
  - coreml
7
  - gliner2
8
  - apple-silicon
9
  ---
10
 
11
- # GLiNER2.5 base Core ML classification
12
 
13
- Core ML FP16 conversion of the classification decision path in
14
- [Fastino GLiNER2.5 base](https://huggingface.co/fastino/gliner2.5-base-v1),
15
- revision `1a8bc24e00dc7300b9017c81d63e3dcdabb26596`.
16
- The original has 193,581,591 parameters (Apache-2.0). This package includes
17
- its trained encoder and classification head (184,945,921 parameters); it does
18
- not export entity, relation, record or span extraction. Use the original
19
- checkpoint for those tasks.
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 runtime.py --model-dir . --text "The rocket launched successfully." \
28
- --task topic --labels '["science","sports","politics"]'
 
 
 
 
 
 
29
  ```
30
 
31
- On an M5 Pro (macOS 27.0), this FP16 artifact matched the native chosen label
32
- on 100 of 100 eligible requests from a fixed application suite; the largest
33
- chosen-label confidence difference was 0.002999. Median Python Core ML call
34
- time was 8.82 ms. The selected sample skipped 300 rows with more than eight
35
- options and seven over-length rows before collecting 100 eligible cases.
36
- This is a smoke parity check, not a full Decision Index score or an ANE-only
37
- latency measure. See `verify-application100.json` for details.
 
 
38
 
39
- The conversion scripts, pinned dependencies and asset hashes are included.
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 small decision classifier Core ML export"
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"]