Full-parity architecture port of GLiNER2 to Rust/candle
Rust
2
86 commits
updated Aug 30, 2026
A high-performance, pure Rust implementation of GLiNER2
(span-enumeration) and GLiNER2.5
(boundary-prediction) information extraction models, built on HuggingFace's
candle ML framework — no PyTorch
runtime required. Designed for efficient CPU inference with real model weights
downloaded from the HuggingFace Hub.
The
-candlesuffix sets this crate apart from the existinggliner2/gliner2-rscrates on crates.io, which are built on the ONNX Runtime backend.gliner2-candleis the pure-Rust, candle-native implementation — no ONNX Runtime, no libtorch, justcargo build.
Both architectures run end-to-end with a documented numerical parity report against the Python reference across all four task types:
| Task type | Parity vs Python |
|---|---|
| Entities (2.5) | Full matrix parity — global = 0.0000, relevant = 0.0000 |
| Classifications | Exact output match (positive) |
| Relations | Exact format match (bare pairs, no flags) |
| Attributes (single-label) | Softmax logits matched to 8 decimals (0.9966161847) |
| Attributes (multi-label) | Sigmoid logits matched to 7 decimals (0.5735875) |
The GLiNER2 (span-enumeration) pipeline also produces entity/classification/ relation/structure outputs that match the Python reference.
The boundary-prediction pipeline is validated numerically against the Python reference:
fastino/gliner2.5-{small,base,multi}-v1debertav2)src/chunking.rs)use gliner2_candle::GLiNER2;
use gliner2_candle::schema::types::Schema;
fn main() -> gliner2_candle::Result<()> {
let engine = GLiNER2::from_pretrained("fastino/gliner2.5-small-v1")?;
// Entities + span attributes (hidden queries, no dedicated weights).
let mut groups = std::collections::HashMap::new();
groups.insert(
"sentiment".to_string(),
gliner2_candle::schema::types::AttributeGroup {
labels: vec!["positive".to_string(), "negative".to_string()],
multi_label: true,
threshold: 0.5,
applies_to: Some(vec!["person".to_string()]),
qualify_labels: false,
},
);
let schema = Schema::new()
.entities(vec![gliner2_candle::schema::types::EntityDef::new("person")])
.entity_attributes(groups)?;
// Long documents are chunked automatically.
let result = engine.extract(
"Apple CEO Tim Cook announced great results in Cupertino.",
&schema,
0.5,
true,
true,
None,
)?;
println!("{result:#}");
Ok(())
}
count_embed (GRU + Transformer) layersrc/constraints.rs, Kleene-3 logic)batch_size, parallel preprocessing)hf-hubtchGLiNER2: Text + Schema → Collator → DeBERTa V3 → Span Rep → Classifier → Output
GLiNER2.5: Text + Schema → Collator → DeBERTa V3 → boundary encoder → shared-pool/pair scoring → decode
(>384 words: split into overlapping chunks, extract per chunk, merge spans)
| Component | File | Purpose |
|---|---|---|
| DeBERTa V3 Encoder | src/model/deberta_v3.rs | Custom DeBERTa V3 (no token_type_embeddings, exact-erf GELU, rel-bias) |
| Boundary Encoder | src/model/boundary.rs | Boundary projection, attention, SwiGLU refinement, scorers |
| Candle Encoder Wrapper | src/model/candle_encoder.rs | BERT / DeBERTa V2 / V3 routing |
| Span Representation | src/model/span_rep.rs | markerV0 (GLiNER2 path) |
| Classifier / Count Pred | src/model/classifier.rs, count_pred.rs | |
| Collator | src/batch/collator.rs | Tokenization + schema encoding + batching |
| Inference Engine | src/inference/engine.rs | Main API + extraction |
| Boundary Decode | src/inference/boundary.rs | Query building, attribute attachment, span resolution |
| Chunking | src/chunking.rs | Long-document split/merge policies |
[dependencies]
gliner2-candle = { git = "https://github.com/mrorigo/gliner2-candle" }
candle-core, candle-nn, candle-transformers — HuggingFace's pure Rust ML frameworktokenizers — HuggingFace tokenizer libraryhf-hub — HuggingFace Hub downloadsserde / serde_json — JSON serializationregex — Regex validatorsuse gliner2_candle::{GLiNER2, ExtractorConfig, SchemaBuilder};
fn main() -> gliner2_candle::Result<()> {
let config = ExtractorConfig::builder()
.model_name("fastino/gliner2-base-v1")
.hidden_size(768)
.vocab_size(128011)
.num_hidden_layers(12)
.num_attention_heads(12)
.intermediate_size(3072)
.build()?;
// Tokenizer + weights download automatically from the Hub.
let engine = GLiNER2::new(&config)?;
let schema = SchemaBuilder::new()
.entities(vec!["person".to_string(), "organization".to_string()])
.build()?;
let result = engine.extract(
"Apple CEO Tim Cook visited Cupertino.",
&schema,
0.5, // threshold
true, // include_confidence
true, // include_spans
None, // max_len
)?;
println!("{result:#}");
Ok(())
}
let texts = vec![
"Apple CEO Tim Cook".to_string(),
"Google founder Larry Page".to_string(),
];
let results = engine.batch_extract_entities(
&texts,
&["person", "organization"],
2, // batch_size
None, // threshold
1, // num_workers
true, // include_confidence
true, // include_spans
None, // max_len
)?;
brainless/gliner2-candleBoth target GLiNER2 with candle. gliner2-candle is a fuller, production-oriented
stack covering entities and broader schema/task plumbing (classifications,
structures, relations, GLiNER2.5 boundary attributes), at the cost of more
complexity. brainless/gliner2-candle is a minimal, entity-focused
implementation (~1 KLOC) that is quick to audit and modify. Choose the former
for a complete task surface and long-term extensibility; the latter for the
smallest possible entity-extraction footprint.
cargo test --lib # 148 unit tests
cargo test --test real_inference_test # GLiNER2 real-hub inference
cargo test --release --test real_inference_test_25 -- --ignored # GLiNER2.5 checkpoints
cargo test --release --test gliner25_boundary_test full_matrix_parity -- --ignored
The test_task_output_parity test (in real_inference_test_25.rs) runs
classifications, relations, and attributes through both pipelines and asserts
the numeric matches documented above.
6.68e-6 vs Python; pair logits global / relevant = 0.0000Apache-2.0
86 commits
Rust
97.1%
Python
2.9%
Full-parity architecture port of GLiNER2 to Rust/candle
Rust
2
86 commits
updated Aug 30, 2026
A high-performance, pure Rust implementation of GLiNER2
(span-enumeration) and GLiNER2.5
(boundary-prediction) information extraction models, built on HuggingFace's
candle ML framework — no PyTorch
runtime required. Designed for efficient CPU inference with real model weights
downloaded from the HuggingFace Hub.
The
-candlesuffix sets this crate apart from the existinggliner2/gliner2-rscrates on crates.io, which are built on the ONNX Runtime backend.gliner2-candleis the pure-Rust, candle-native implementation — no ONNX Runtime, no libtorch, justcargo build.
Both architectures run end-to-end with a documented numerical parity report against the Python reference across all four task types:
| Task type | Parity vs Python |
|---|---|
| Entities (2.5) | Full matrix parity — global = 0.0000, relevant = 0.0000 |
| Classifications | Exact output match (positive) |
| Relations | Exact format match (bare pairs, no flags) |
| Attributes (single-label) | Softmax logits matched to 8 decimals (0.9966161847) |
| Attributes (multi-label) | Sigmoid logits matched to 7 decimals (0.5735875) |
The GLiNER2 (span-enumeration) pipeline also produces entity/classification/ relation/structure outputs that match the Python reference.
The boundary-prediction pipeline is validated numerically against the Python reference:
fastino/gliner2.5-{small,base,multi}-v1debertav2)src/chunking.rs)use gliner2_candle::GLiNER2;
use gliner2_candle::schema::types::Schema;
fn main() -> gliner2_candle::Result<()> {
let engine = GLiNER2::from_pretrained("fastino/gliner2.5-small-v1")?;
// Entities + span attributes (hidden queries, no dedicated weights).
let mut groups = std::collections::HashMap::new();
groups.insert(
"sentiment".to_string(),
gliner2_candle::schema::types::AttributeGroup {
labels: vec!["positive".to_string(), "negative".to_string()],
multi_label: true,
threshold: 0.5,
applies_to: Some(vec!["person".to_string()]),
qualify_labels: false,
},
);
let schema = Schema::new()
.entities(vec![gliner2_candle::schema::types::EntityDef::new("person")])
.entity_attributes(groups)?;
// Long documents are chunked automatically.
let result = engine.extract(
"Apple CEO Tim Cook announced great results in Cupertino.",
&schema,
0.5,
true,
true,
None,
)?;
println!("{result:#}");
Ok(())
}
count_embed (GRU + Transformer) layersrc/constraints.rs, Kleene-3 logic)batch_size, parallel preprocessing)hf-hubtchGLiNER2: Text + Schema → Collator → DeBERTa V3 → Span Rep → Classifier → Output
GLiNER2.5: Text + Schema → Collator → DeBERTa V3 → boundary encoder → shared-pool/pair scoring → decode
(>384 words: split into overlapping chunks, extract per chunk, merge spans)
| Component | File | Purpose |
|---|---|---|
| DeBERTa V3 Encoder | src/model/deberta_v3.rs | Custom DeBERTa V3 (no token_type_embeddings, exact-erf GELU, rel-bias) |
| Boundary Encoder | src/model/boundary.rs | Boundary projection, attention, SwiGLU refinement, scorers |
| Candle Encoder Wrapper | src/model/candle_encoder.rs | BERT / DeBERTa V2 / V3 routing |
| Span Representation | src/model/span_rep.rs | markerV0 (GLiNER2 path) |
| Classifier / Count Pred | src/model/classifier.rs, count_pred.rs | |
| Collator | src/batch/collator.rs | Tokenization + schema encoding + batching |
| Inference Engine | src/inference/engine.rs | Main API + extraction |
| Boundary Decode | src/inference/boundary.rs | Query building, attribute attachment, span resolution |
| Chunking | src/chunking.rs | Long-document split/merge policies |
[dependencies]
gliner2-candle = { git = "https://github.com/mrorigo/gliner2-candle" }
candle-core, candle-nn, candle-transformers — HuggingFace's pure Rust ML frameworktokenizers — HuggingFace tokenizer libraryhf-hub — HuggingFace Hub downloadsserde / serde_json — JSON serializationregex — Regex validatorsuse gliner2_candle::{GLiNER2, ExtractorConfig, SchemaBuilder};
fn main() -> gliner2_candle::Result<()> {
let config = ExtractorConfig::builder()
.model_name("fastino/gliner2-base-v1")
.hidden_size(768)
.vocab_size(128011)
.num_hidden_layers(12)
.num_attention_heads(12)
.intermediate_size(3072)
.build()?;
// Tokenizer + weights download automatically from the Hub.
let engine = GLiNER2::new(&config)?;
let schema = SchemaBuilder::new()
.entities(vec!["person".to_string(), "organization".to_string()])
.build()?;
let result = engine.extract(
"Apple CEO Tim Cook visited Cupertino.",
&schema,
0.5, // threshold
true, // include_confidence
true, // include_spans
None, // max_len
)?;
println!("{result:#}");
Ok(())
}
let texts = vec![
"Apple CEO Tim Cook".to_string(),
"Google founder Larry Page".to_string(),
];
let results = engine.batch_extract_entities(
&texts,
&["person", "organization"],
2, // batch_size
None, // threshold
1, // num_workers
true, // include_confidence
true, // include_spans
None, // max_len
)?;
brainless/gliner2-candleBoth target GLiNER2 with candle. gliner2-candle is a fuller, production-oriented
stack covering entities and broader schema/task plumbing (classifications,
structures, relations, GLiNER2.5 boundary attributes), at the cost of more
complexity. brainless/gliner2-candle is a minimal, entity-focused
implementation (~1 KLOC) that is quick to audit and modify. Choose the former
for a complete task surface and long-term extensibility; the latter for the
smallest possible entity-extraction footprint.
cargo test --lib # 148 unit tests
cargo test --test real_inference_test # GLiNER2 real-hub inference
cargo test --release --test real_inference_test_25 -- --ignored # GLiNER2.5 checkpoints
cargo test --release --test gliner25_boundary_test full_matrix_parity -- --ignored
The test_task_output_parity test (in real_inference_test_25.rs) runs
classifications, relations, and attributes through both pipelines and asserts
the numeric matches documented above.
6.68e-6 vs Python; pair logits global / relevant = 0.0000Apache-2.0
86 commits
Rust
97.1%
Python
2.9%