Swift/MLX implementation of GLiNER2 - a unified schema-based information extraction framework. (Article)
name::dtype::[a|b]::desc field-spec form)maxLen truncationAdd to your Package.swift:
dependencies: [
.package(url: "https://github.com/MacPaw/Gliner2Swift", branch: "main"),
]
Or in Xcode: File → Add Package Dependencies → Enter the repository URL.
import GLiNER2Swift
// Load model (downloads automatically from HuggingFace, or pass a local directory)
let model = try await GLiNER2.fromPretrained("fastino/gliner2-base-v1")
// Extract entities — results come back as a [String: Any] dictionary
let text = "Tim Cook is CEO of Apple in Cupertino."
let result = model.extractEntities(
text: text,
entityTypes: ["person", "company", "location"],
includeSpans: true
)
if let entities = result["entities"] as? [String: [Any]] {
for (label, spans) in entities {
for case let span as [String: Any] in spans {
print("\(label): \(span["text"]!) [\(span["start"]!)-\(span["end"]!)]")
}
}
}
// person: Tim Cook [0-8]
// company: Apple [23-28]
// location: Cupertino [32-41]
| Model | Parameters | HuggingFace ID |
|---|---|---|
| Base | 205M | fastino/gliner2-base-v1 |
The extraction methods are synchronous (not throws) and return a [String: Any] result
dictionary; only fromPretrained is async throws.
let result = model.extractEntities(
text: "Your text here",
entityTypes: ["person", "organization", "location"]
)
// result["entities"] is [String: [Any]] — label → list of matches
let result = model.classifyText(
text: "Great product, highly recommend!",
task: "sentiment",
labels: ["positive", "negative", "neutral"]
)
// result["sentiment"] == "positive"
let schema = model.createSchema()
.entities(["person", "company"])
.classification(task: "sentiment", labels: ["positive", "negative"])
let result = model.extract(text: text, schema: schema)
// Splits into overlapping word-chunks, remaps spans back to the original text, and merges.
let result = model.extractEntitiesLong(text: veryLongText, entityTypes: ["person", "company"])
GLiNER2Swift supports loading LoRA (Low-Rank Adaptation) adapters trained with the Python GLiNER2 framework. Adapters are merged into the base weights at load time, giving identical results to Python with zero runtime overhead.
// One-step: load base model + adapter together
let model = try await GLiNER2.fromPretrained(
"fastino/gliner2-base-v1",
adapterPath: "/path/to/adapter"
)
// Two-step: load base model first, then attach adapter
let model = try await GLiNER2.fromPretrained("fastino/gliner2-base-v1")
try model.loadAdapter(from: "/path/to/adapter")
The adapter directory must contain:
adapter_config.json - LoRA configuration (rank, alpha, target modules)adapter_weights.safetensors - LoRA weight matricesAll parameters (rank, alpha, dropout, target modules) are read from adapter_config.json - any valid LoRA configuration is supported.
Instead of maintaining separate LoRA modules at runtime, weights are merged at load time:
W_merged = W_base + (lora_B @ lora_A) * (alpha / r)
This produces numerically identical results to Python's model.load_adapter() + model.merge_lora() pipeline.
GLiNER2Swift is a direct port of the Python GLiNER2 implementation, achieving numerical parity with the reference implementation:
On Apple Silicon (M3 Pro, fp16):
Opt-in int8 encoder quantization roughly halves steady-state memory (~415 MB → ~253 MB)
for a small accuracy trade-off; pass quantization: .int8 to fromPretrained.
This is an active port of the Python GLiNER2 implementation. Inference is at full prediction parity with the reference. The following are not yet implemented:
.bin checkpoints - Only safetensors weights are loadable (MLX cannot read pickle)deberta-v3-base is supported; other model variants are not yet availableContributions and PRs are welcome!
See CONTRIBUTING.md for guidelines on submitting PRs, branch naming conventions, and parity testing requirements.
Swift
86.1%
Python
13.9%
Swift/MLX implementation of GLiNER2 - a unified schema-based information extraction framework. (Article)
name::dtype::[a|b]::desc field-spec form)maxLen truncationAdd to your Package.swift:
dependencies: [
.package(url: "https://github.com/MacPaw/Gliner2Swift", branch: "main"),
]
Or in Xcode: File → Add Package Dependencies → Enter the repository URL.
import GLiNER2Swift
// Load model (downloads automatically from HuggingFace, or pass a local directory)
let model = try await GLiNER2.fromPretrained("fastino/gliner2-base-v1")
// Extract entities — results come back as a [String: Any] dictionary
let text = "Tim Cook is CEO of Apple in Cupertino."
let result = model.extractEntities(
text: text,
entityTypes: ["person", "company", "location"],
includeSpans: true
)
if let entities = result["entities"] as? [String: [Any]] {
for (label, spans) in entities {
for case let span as [String: Any] in spans {
print("\(label): \(span["text"]!) [\(span["start"]!)-\(span["end"]!)]")
}
}
}
// person: Tim Cook [0-8]
// company: Apple [23-28]
// location: Cupertino [32-41]
| Model | Parameters | HuggingFace ID |
|---|---|---|
| Base | 205M | fastino/gliner2-base-v1 |
The extraction methods are synchronous (not throws) and return a [String: Any] result
dictionary; only fromPretrained is async throws.
let result = model.extractEntities(
text: "Your text here",
entityTypes: ["person", "organization", "location"]
)
// result["entities"] is [String: [Any]] — label → list of matches
let result = model.classifyText(
text: "Great product, highly recommend!",
task: "sentiment",
labels: ["positive", "negative", "neutral"]
)
// result["sentiment"] == "positive"
let schema = model.createSchema()
.entities(["person", "company"])
.classification(task: "sentiment", labels: ["positive", "negative"])
let result = model.extract(text: text, schema: schema)
// Splits into overlapping word-chunks, remaps spans back to the original text, and merges.
let result = model.extractEntitiesLong(text: veryLongText, entityTypes: ["person", "company"])
GLiNER2Swift supports loading LoRA (Low-Rank Adaptation) adapters trained with the Python GLiNER2 framework. Adapters are merged into the base weights at load time, giving identical results to Python with zero runtime overhead.
// One-step: load base model + adapter together
let model = try await GLiNER2.fromPretrained(
"fastino/gliner2-base-v1",
adapterPath: "/path/to/adapter"
)
// Two-step: load base model first, then attach adapter
let model = try await GLiNER2.fromPretrained("fastino/gliner2-base-v1")
try model.loadAdapter(from: "/path/to/adapter")
The adapter directory must contain:
adapter_config.json - LoRA configuration (rank, alpha, target modules)adapter_weights.safetensors - LoRA weight matricesAll parameters (rank, alpha, dropout, target modules) are read from adapter_config.json - any valid LoRA configuration is supported.
Instead of maintaining separate LoRA modules at runtime, weights are merged at load time:
W_merged = W_base + (lora_B @ lora_A) * (alpha / r)
This produces numerically identical results to Python's model.load_adapter() + model.merge_lora() pipeline.
GLiNER2Swift is a direct port of the Python GLiNER2 implementation, achieving numerical parity with the reference implementation:
On Apple Silicon (M3 Pro, fp16):
Opt-in int8 encoder quantization roughly halves steady-state memory (~415 MB → ~253 MB)
for a small accuracy trade-off; pass quantization: .int8 to fromPretrained.
This is an active port of the Python GLiNER2 implementation. Inference is at full prediction parity with the reference. The following are not yet implemented:
.bin checkpoints - Only safetensors weights are loadable (MLX cannot read pickle)deberta-v3-base is supported; other model variants are not yet availableContributions and PRs are welcome!
See CONTRIBUTING.md for guidelines on submitting PRs, branch naming conventions, and parity testing requirements.
Swift
86.1%
Python
13.9%