2026-08-25 · 15 min · information-extraction · ner · encoders · small-models · structured-output
There is a category of model that gets very little attention and does an enormous amount of work: the small encoder that turns text into structured records. Not a chat model asked nicely for JSON — a 200-million-parameter thing that runs on a CPU, takes a schema, and returns spans with character offsets you can check against the source. The GLiNER family has been the open flagship of that category since 2023, and Fastino's GLiNER2.5, released this week, is the first version that changes the part of the architecture everything else was built around.
The change is one sentence: the model stopped enumerating spans and started scoring boundaries. Almost everything in the release notes is downstream of that, including three capabilities that look unrelated to it.
| What it is | zero-shot information extraction: entities, relations, classification, JSON records, from a declared schema |
| The change | span enumeration → boundary prediction (start / end / inside scores, sparse pairing) |
| Weights | gliner2.5-small-v1 74M · gliner2.5-base-v1 0.2B · gliner2.5-multi-v1 0.3B — all Apache 2.0 |
| Backbone | mDeBERTa-v3-base for the multilingual checkpoint, ~594 MB at FP16 |
| Sequence length | 4,096 words natively, plus library-level chunking with global offset remapping |
| Max span width | gone — an entity may start at the first token and end at the last |
| Headline number | 16-benchmark macro-F1 56.17 (0.3B) vs 56.09 for GLiNER2 · +24.75 on XNLI |
| The number under it | drop XNLI and the 0.3B checkpoint is 1.57 points behind its predecessor; the 0.2B one is 1.27 ahead |
| Lineage | GLiNER2, arXiv:2507.18546 · Zaratiana, Pasternak, Boyd, Hurn-Maloney, Lewis |
The width axis
Here is how every GLiNER up to this one located an entity. The encoder reads the text and the schema's queries together. Then, for each query, the model enumerates candidate spans — start position 0 with width 1, start 0 with width 2, and so on up to some max_width — and scores each candidate against the query.
That is a two-dimensional grid, and the second dimension is the trouble. It is why the models carry a max_width at all: the grid is the computation, and a grid has to end somewhere. Anything longer than the ceiling is not scored poorly. It is not scored.
The old grid has two axes and the second one is the problem. Every allowed width is a row, so the cost of the model grows with the longest thing you are willing to find, and the row you did not pay for does not merely score badly — it is never scored at all. Raising max_width to catch an eleven-word address makes every query more expensive on every document, including the ones full of two-word names.
Boundary prediction deletes the axis. The model says where entities start and where they end, a sparse stage keeps the best few of each, and pairing them is k² regardless of how far apart the pair sits or how long the document is. Drag the length slider: the orange bar tracks the document and the green one does not move.
Boundary prediction removes the axis rather than raising it. For each query the model emits a start score and an end score over the token boundaries, plus an inside score over the tokens themselves. A sparse proposal stage keeps the best few starts and the best few ends and pairs them, with no constraint on how far apart a pair may sit; a reranking head then scores each proposed candidate using both the boundary evidence and the span's content. Relation candidates are drawn from that same pool rather than through a separate path — which matters later.
Two consequences fall out immediately. A forty-word indemnification clause costs exactly what a two-word name costs to locate. And the per-query candidate count stops depending on document length, because it is now k² in the proposal budget instead of L × W in the document.

The workarounds people used are worth naming, because they are the actual cost of the old design. You could raise max_width, which makes every query more expensive on every document including the ones full of two-word names. Or you could extract fragments and stitch them together afterwards, which is a second system with its own failure modes, sitting downstream of a model that has no idea it is being asked half a question.
Reading past page one
Removing the explicit span representations also cut enough memory to train at 4,096 words. That is a real number to have: most contracts, incident reports and meeting transcripts fit in one forward pass, and the ones that don't now get a first-class chunking path in the library rather than an exercise for the reader.
The chunking is the less glamorous half and probably the more useful one. Every extraction task has a long-document variant — entities, classification, JSON schemas, relations, generic extraction — that splits the document into overlapping word chunks, runs the schema over each, and remaps every returned span to a character offset in the original document, merging duplicates from the overlaps under a policy you choose.
The single-window row is the honest picture of running a 512-token encoder at a contract: it does not perform badly on page forty, it never reads page forty. Whatever recall number you quote is a recall number over the first two percent of the document.
Chunking fixes coverage and introduces two new problems, which is why having it in the library matters more than it sounds. Every span now needs remapping from a chunk-local index back to a character offset in the original file, or you cannot verify it against the source or redact at it. And the overlap that stops boundary-straddling mentions from vanishing is the same overlap that returns them twice — drag overlap to zero with a 40-word clause type in the schema and watch mentions disappear; raise it and watch the duplicate count climb instead. Both of those were previously yours to write.
It is worth sitting with the first mode in that control for a moment. A 512-token encoder pointed at a three-hundred-page contract does not do 3% as well as it would on a short document. It reads the first two pages and reports, with total confidence, on those. Any recall figure you quote is a recall figure over the prefix — and nothing in the output tells you that, which is what makes it a genuinely dangerous default rather than merely a limited one.
The second thing that control shows is why the overlap is not a tuning nicety. A span that straddles a chunk boundary is fully contained in no chunk, so with zero overlap it is missed by every pass — and the longer your entity types are, the more often that happens. Which means the width-ceiling problem and the chunking problem are the same problem wearing different clothes: both are about spans that the geometry of the computation cannot see.
A graph, not two lists
GLiNER2 could extract relations. What it returned was two independently thresholded lists — entities here, triples there — and the union of two independent argmaxes is very often not a graph.
Leave it on independent thresholds and drag τ down from 0.90. At 0.81 the edge Bob works_for Acme enters the output while Bob himself, at 0.28, is still nowhere near the bar — an edge naming a node that does not exist. Drag further and Alice acquires a second employer. Neither is a model error; both are what happens when two lists are produced by two independent argmaxes and stapled together afterwards.
Switch to joint decode and the same numbers behave. The 0.81 edge pulls Bob in with it, because a relation cannot be admitted without its endpoints. The 0.44 edge to Globex is refused, because by the time it comes up Alice already has the employer the rule allows. The consistency work did not disappear — it moved from your ingestion pipeline into the decoder, where it can be done while the scores are still available.
Play with the threshold under "independent thresholds" and the failure is not subtle. A relation whose confidence is 0.81 sails past any reasonable bar while the entity it names sits at 0.28 and is dropped, so the output contains an edge pointing at a node that isn't there. Push the bar lower to rescue the node and a different rule breaks: a person acquires two employers in a schema that permits one.
Neither of those is the model being wrong. Both are what happens when you produce two lists with two argmaxes and staple them together.

GLiNER2.5 scores all candidate entities and relations in the same forward pass and then assembles them, checking each candidate against the schema's declared rules as the solution is built rather than filtering afterwards. Invalid combinations are never admitted to the search, so the returned structure conforms by construction.
The claim worth being precise about: this does not make the extractions more accurate. It makes them well-formed. Those are different properties and only one of them was ever the user's job. A knowledge graph is as reliable as its least consistent edge, and every ingestion pipeline built on independent triples grows the same three pieces of code — reject dangling references, enforce cardinality, break cycles. Joint decoding moves that work to where the scores still exist, which is the only place it can be done well. Downstream, all you have is the survivors.
Two heads, one rule
The same argument, one level up. Consider a guardrail classifier that answers two questions at once: is this prompt safe, and if not, what kind of harm is it. Decoded independently, nothing stops the pair from being a contradiction.
Both heads are confident and both are individually reasonable. The safety head leans safe, the harm head is sure it is an injection attempt, and their two argmaxes describe a prompt that is simultaneously fine and an attack. Turn the rule off and watch the decoder walk straight into that corner: it is the highest-scoring cell in the whole grid, which is exactly why independent decoding keeps finding it.
With the rule on, that corner is not a low-scoring option — it is not an option. The decoder searches only the legal cells, and the best legal cell flips the safety verdict rather than keeping a verdict that contradicts the label sitting next to it. That is worth more than it sounds: a guardrail whose output can be self-contradictory needs a reconciliation layer behind it, and that layer is code you write, maintain, and get wrong.
The thing that control makes visible — and the reason it is a grid rather than two bars — is that the contradiction is not a low-probability accident that better calibration would fix. Turn the rule off and the forbidden corner is frequently the highest-scoring cell in the entire product space. Independent decoding does not stumble into it. It is drawn there.

With the rule declared, the invalid corner is not a low-scoring option; it is not an option. The decoder searches the legal cells only, and the best legal cell flips the safety verdict rather than keeping one that contradicts the label next to it. And when no valid assignment exists at all, GLiNER2.5 raises rather than returning an invalid classification — the right call, and one that a lot of structured-output tooling gets backwards by silently emitting the closest thing to valid.
Attributes, in the same pass
The fifth capability is the smallest and the one I'd reach for most often. Span attributes let the schema attach a small set of qualitative labels to extracted spans — sentiment on a product mention, negation status on a symptom, dosage form on a medication — decoded in the same forward pass as the entities.
from gliner2 import AutoExtractor
model = AutoExtractor.from_pretrained("fastino/gliner2.5-multi-v1")
schema = (
model.schema()
.entities({"symptom": "a reported symptom", "medication": "a drug name"})
.attributes("clinical", ["negated", "affirmed"], applies_to=["symptom"])
)
model.extract("Patient denies chest pain; continues 25 mg lisinopril daily.", schema)GLiNER2 could classify and extract in one pass, but its classifications applied to the input, not to each span, so entities came back flat. The distinction matters more in clinical and legal text than anywhere else: "chest pain" and "denies chest pain" are the same span with opposite meanings, and a pipeline that extracts the span and then re-classifies each one in a second pass is paying a second forward pass per mention to recover context the first pass already had.
What the benchmarks actually say
Fastino publishes per-dataset scores in the appendix "so regressions are visible alongside gains". That is unusually good practice and worth taking them up on, because the summary line and the appendix tell noticeably different stories.
The summary line for the 0.3B checkpoint is a 0.08-point win, which reads as parity, and parity is the correct claim — Fastino frames the whole benchmark section as evidence that the new architecture does not trade away quality, not that it raises it. What the per-dataset table adds is where that parity comes from: one dataset moved +24.75 and eleven of the other fifteen went down. Press “drop XNLI” and the 0.3B model is 1.57 points behind its predecessor.
Now switch to the 0.2B row and press it again. That checkpoint keeps a 1.27-point gain with XNLI removed — broad, unglamorous, spread across extraction. Two checkpoints from one release with genuinely different stories, and only the appendix tells you which is which.
Every number in that control is theirs; every average is recomputed here from the sixteen per-dataset figures rather than quoted, which is how you can check the transcription — the Overall row reproduces their published 56.17 / 56.09 / 54.87 / 53.34 exactly.
Three readings, in order of how much they change your decision.
The headline is parity, and parity is the claim. 56.17 against 56.09 at 0.3B is a tie, and Fastino frames the section correctly: the evidence is that the new architecture does not trade away the quality the family is known for while adding relational decoding and constrained classification heads. That is a real and sufficient result. A new candidate-generation mechanism that costs nothing in accuracy and removes a structural ceiling is a good trade even at exactly zero points.
At 0.3B, that parity is carried by one dataset. XNLI moved +24.75 and eleven of the other fifteen sets went down. Press "drop XNLI" and the multilingual checkpoint sits 1.57 points behind GLiNER2. CrossNER-politics alone gives back 7.21. If your workload looks like CrossNER — domain-specific entity types over short, clean text — the 0.3B upgrade is a small regression that buys you capabilities, not a quality improvement that also happens to add them.
At 0.2B, the gain is real and broad. The base checkpoint keeps +1.27 with XNLI removed, spread across extraction rather than concentrated: few_nerd +7.92, hipe2020 +10.11, ronec +5.46, german_ler +4.28. Two checkpoints, one release, opposite conclusions — and you only get that from the appendix.

One more thing worth flagging about that XNLI jump: it is not an extraction result. NLI is textual entailment — does sentence A entail, contradict, or stay neutral toward sentence B — and 37.55 is roughly the floor for a three-way task, so the old multilingual checkpoint was close to not doing it at all. Going from broken to functional on a task is a legitimate and useful fix. Averaging it in with fifteen tasks that were already working is what makes the mean move, and the mean is the number most people will read.
RONEC deserves the opposite note, in Fastino's favour. Romanian was not a target language in training, so the +5.46 at 0.2B is a genuine zero-shot transfer result — the kind of number that is easy to leave out and they put in the headline chart.
The schema is the API
Under all five capabilities is a design choice that predates this release and is the actual reason to reach for this family: the interface is a declared schema, not a prompt.
schema = (
model.schema()
.entities({"person": ..., "organization": ..., "location": ...})
.relations([("person", "works_for", "organization")])
.rule("works_for", cardinality=1)
.classify("sentiment", ["positive", "negative", "neutral"])
)That object is a thing your code can version, diff, and test. Its output has typed fields and character offsets, so every span is checkable against the source — text[start:end] is the extracted string, which sounds trivial until you have spent an afternoon reconciling an LLM's paraphrase of a name with the name. There is no parse step, no retry on malformed JSON, no temperature. On CPU, at 74M to 0.3B parameters, over documents you were previously chopping up by hand.
Where I'd actually use it
The honest scope: this is a layer, not a replacement for a reasoning model. It finds and types things that are present in the text. It does not infer things that aren't, summarise, or reason about what it found. Asked to pull "the party bearing termination risk" it will do something confident and wrong, because that is a judgment, not a span.
Within that scope the fit is very good, and the release changes the boundary in one specific direction: the tasks that used to require code around the model — chunking, offset remapping, dangling-edge filtering, cardinality enforcement, verdict reconciliation, a second pass to qualify each span — are now inside it. That is the upgrade. Not accuracy; surface area.
Three places I would put it today. PII discovery across whole documents rather than first windows, where the global character offset is what lets you redact at the source. Knowledge-graph ingestion, where joint decoding removes the validation layer that every such pipeline grows. And agent guardrails, where a self-contradictory verdict is worse than a wrong one, because a wrong one at least fails in a way your code can see.
The ledger
Genuinely new. Boundary prediction with no width ceiling, and a per-query candidate count that no longer tracks document length. Joint entity–relation decoding under declared structural rules. Constrained classification across tasks with a real error on unsatisfiable schemas. Span attributes in the same forward pass. Native 4,096-word context plus library chunking with offset remapping and duplicate merging. Three Apache-2.0 checkpoints from 74M to 0.3B.
Overstated. "Achieves higher overall average F1" is true of both checkpoints as arithmetic, and materially true of only one of them. At 0.3B the average is a tie carried by a single dataset that went from broken to working, sitting on top of eleven regressions.
Unmeasured. No latency or throughput figures anywhere in the release — for a family whose entire pitch is small and fast, and whose central architectural claim is about how computation scales with document length, that is the missing table. "Linear in sequence length for a fixed schema and candidate budget" is a complexity claim; it is not a milliseconds-per-page claim, and the second one is what you deploy against. The 74M checkpoint appears in the model list and in no benchmark at all. And there is no comparison to the obvious alternative — a small instruct model doing the same extraction with constrained decoding — which is the actual buy-or-build question anyone evaluating this is asking.
The interesting thing about this release is not the scoreboard. It is that a family which has spent three years being the small model that finds entities has quietly become a small model that returns validated structure, and that the change making it possible was subtraction: an axis of the computation, removed.