StreamingSpan¶
StreamingSpan is GLiNER’s architecture for named entity recognition over text that arrives incrementally. It uses a causal decoder as the text backbone, retains reusable state between chunks, and revises recent span predictions when new right context becomes available.
Use the
knowledgator/gliner-stream-pii-v1.0
checkpoint to get started with streaming PII detection. GLiNER.from_pretrained
reads model_type="gliner_streaming_span" from the checkpoint and selects
StreamingSpanGLiNER automatically.
from gliner import GLiNER
model = GLiNER.from_pretrained("knowledgator/gliner-stream-pii-v1.0")
model.eval()
Quick start¶
Pass one chunk at a time with a stable session ID. The chunks are concatenated exactly as supplied, so retain spaces and punctuation at chunk boundaries.
labels = ["person", "email address", "phone number"]
session_id = "support-call-42"
chunks = [
"Customer Alice Johnson ",
"can be reached at alice@example.com ",
"or +1 202-555-0147.",
]
try:
for chunk in chunks:
snapshot = model.inference(
[chunk],
labels,
session_id=[session_id],
threshold=0.5,
)[0]
print(snapshot)
finally:
model.clear_session(session_id)
Each snapshot is the complete set of entities currently active for the
accumulated session text. It is not a list of only the entities detected in the
latest chunk. An entity has the same shape as an ordinary GLiNER prediction:
{
"start": 9, # document-relative, inclusive character offset
"end": 22, # document-relative, exclusive character offset
"text": "Alice Johnson",
"label": "person",
"score": 0.93,
}
Scores and even the active boundaries may change between snapshots as more
context arrives. Consumers that need an event stream should diff consecutive
snapshots by (start, end, label).
Architecture¶
StreamingSpan uses the following cold and warm paths:
On the first append, labels and text are serialized as
label<<LABEL>>...<<SEP>>textand passed through the causal decoder.A compact label context encoder processes only the prompt through
<<SEP>>. Each<<LABEL>>state becomes an entity-type representation, which is cached.Text subtokens are pooled into word representations. The model constructs span representations and scores them against the label representations.
On later appends, only the new decoder tokens are evaluated. Cached KV, label, and word states are reused and extended.
Scores for new and recently revisited span boundaries are merged into the session’s score history, then the complete history is decoded.
The default markerV2 span layer combines the candidate’s start word, end
word, and latest visible word. Candidate width is bounded by max_width.
right_context_width determines how far behind the newest word an existing
span may be revisited; it defaults to max_width. Setting it to 0 keeps old
span scores fixed.
An optional span_encoder_config adds a dense-input DeBERTa-v2, ModernBERT, or
RNN encoder before span construction. A bidirectional span encoder can change
every historical word representation, so the model re-scores all historical
span candidates after each append. This still reuses causal decoder states;
recompute=True is the option that rebuilds the entire accumulated sequence.
For a component-level explanation, see GLiNER StreamingSpan in the architecture guide.
Choosing an inference mode¶
StreamingSpan provides four inference surfaces. They share prediction semantics but differ in who owns the cache and how work is batched.
Surface |
Cache ownership |
Scheduling |
Best fit |
|---|---|---|---|
|
None |
Ordinary GLiNER batching |
Complete, independent texts |
|
One cache per ID on the model |
Compatible sessions are batched per call |
Flexible synchronous session sets |
|
One persistent batched cache on the handle |
Fixed rows advance together |
Stable groups with aligned arrival cadence |
|
One cache per ID on the model |
Dynamic microbatching |
Concurrent, independently arriving streams |
Stateless inference¶
The architecture can process complete texts without retaining state. Omitting
session_id delegates to the ordinary GLiNER inference pipeline.
entities = model.predict_entities(
"Alice Johnson's email is alice@example.com.",
labels,
threshold=0.5,
)
batch = model.inference(
["Alice called.", "Bob emailed."],
labels,
batch_size=2,
)
Use stateless inference when the whole input is already available. It avoids session lifecycle and cache-memory concerns.
Flexible synchronous sessions¶
Supplying session_id turns each input into an append operation. Use one
stable, non-empty session ID per text; IDs must be unique within one call.
session_ids = ["call-a", "call-b"]
first = model.inference(
["Alice Johnson ", "Bob Smith "],
labels,
session_id=session_ids,
batch_size=8,
)
second = model.inference(
["shared her email.", "shared his number."],
labels,
session_id=session_ids,
batch_size=8,
)
model.clear_session(session_ids)
Cold sessions are batched together. Warm sessions with the same cached decoder
length are also batched together; different lengths are processed in separate
groups. batch_size limits how many append requests enter a group at once.
Session order may change between calls because state is addressed by ID.
A blank chunk returns [] and does not advance that session. Labels must stay
in the same order for the lifetime of a session. To use a different label set,
clear the session or append non-empty text with recompute=True.
Persistent fixed-order batches¶
When the same streams advance together, a persistent batch avoids repeatedly stacking and splitting their historical KV caches. The session-to-row mapping and labels are immutable for the handle’s lifetime.
with model.create_streaming_batch(
session_ids=["call-a", "call-b"],
labels=labels,
) as stream:
first = stream.append(["Alice Johnson ", "Bob Smith "])
second = stream.append(["shared her email.", "shared his number."])
# Keep call-a unchanged while call-b advances.
third = stream.append(["", " It is +1 202-555-0147."])
append returns one complete snapshot per row. An empty row retains its
existing snapshot. If every row is empty, no model forward is performed.
The handle offers two lifecycle operations:
reset()discards the complete batched cache but keeps the handle, row mapping, and labels usable.close()releases the cache and permanently closes the handle. The context manager calls it automatically.
Passing recompute=True to append rebuilds every row from its complete
accumulated text. It is a batch-wide operation.
Asynchronous dynamic microbatching¶
The asynchronous engine collects independently arriving appends for a short window and sends compatible sessions through batched forwards. Calls for the same session remain FIFO ordered, while different session IDs can be submitted concurrently.
import asyncio
async def consume(engine, session_id, chunks):
latest = []
async for latest in engine.stream(session_id, chunks, labels, threshold=0.5):
print(session_id, latest)
return latest
async def main():
async with model.create_async_streaming_engine(
max_batch_size=32,
batch_wait_timeout_ms=2,
queue_capacity=4096,
) as engine:
results = await asyncio.gather(
consume(engine, "call-a", ["Alice ", "shared her email."]),
consume(engine, "call-b", ["Bob ", "shared his number."]),
)
await engine.clear_session("call-a")
await engine.clear_session("call-b")
return results
snapshots = asyncio.run(main())
The engine runs model work outside the event-loop thread and uses one worker per
engine, avoiding competing cache mutations and CUDA launches.
max_batch_size caps a microbatch, batch_wait_timeout_ms trades a small amount
of latency for more batching opportunities, and queue_capacity applies
backpressure to producers. Leaving the async context drains queued work and
closes the scheduler.
Use await engine.clear_session(id) while an engine is active; it waits for
earlier work on that session before removing the cache. Blank appends return
[] without entering the queue or changing state.
Session and cache lifecycle¶
A session retains more than the decoder’s KV tensors:
Cached state |
Purpose |
|---|---|
Decoder KV and attention state |
Lets new tokens attend to the prompt and historical text without re-encoding it |
Pooled word states |
Supports spans that cross chunk boundaries and recent span rescoring |
Label representations |
Avoids re-encoding an unchanged entity-type prompt |
Text, tokens, and character offsets |
Produces document-relative entity offsets and text |
Span-score history |
Preserves old candidates and replaces scores for revised boundaries |
KV, word, and label tensors stay with the model device. Historical span scores are kept on CPU so the prediction history does not consume progressively more accelerator memory.
For sessions created through inference(..., session_id=...) or the async
engine, use:
model.clear_session("call-a") # one session
model.clear_session(["call-b", "call-c"]) # several sessions
model.clear_sessions() # every model-owned session
print(model.session_count)
Persistent StreamingBatch handles own their cache separately and are not
included in model.session_count; use the handle’s reset() or close().
Model-owned sessions have no automatic TTL or LRU eviction.
Context limits¶
The effective session limit is the smaller of max_cache_length, when set, and
the decoder backbone’s native positional limit. The serialized label prompt
also consumes decoder positions. Streaming sessions disable preprocessing
truncation, and the implementation does not silently evict old context. An
append that would exceed the limit raises ValueError; finish and clear the
session, reset its batch, or start a new session.
This differs from stateless StreamingSpan and other GLiNER inference, where
config.max_len is a text-only splitter-token limit and an overlong input is
reduced to a prefix. See Input limits and truncation.
A persistent batch stores the padded physical width of every append. Group streams with similar chunk sizes to reduce padded cache positions and avoid reaching the physical context limit earlier than necessary.
Prediction and revision controls¶
The streaming APIs accept the standard span-decoding controls:
Option |
Effect |
|---|---|
|
Minimum entity score; defaults to |
|
If |
|
Permit more than one label for a span |
|
Add per-class probabilities to each returned entity |
|
Rebuild state from all accumulated text instead of incrementally appending |
packing_config, input_spans, and external model-input tensors are not
supported when session_id is supplied. They remain available to the stateless
parent inference path where otherwise supported.
Two model configuration fields control incremental revisions:
max_widthis the maximum candidate span width in words.right_context_widthis the number of words behind the latest word whose span endings are eligible for rescoring.Noneis normalized tomax_width.
A larger right-context window can improve revisions at the cost of scoring more
candidates per append. recompute=True is useful as an occasional correctness
check or when changing labels, but it forfeits incremental decoder savings for
that call.
Chunking guidance¶
Prefer chunks that end at word, punctuation, or sentence boundaries. Splitting one logical word across calls makes the word splitter treat the pieces as separate streaming words.
Preserve boundary whitespace.
"Alice "followed by"joined"reconstructs"Alice joined";"Alice"followed by"joined"reconstructs"Alicejoined".Smaller chunks provide earlier updates but incur more Python, tokenization, decoding, and scheduling overhead. Larger chunks improve throughput but delay the first prediction.
Use stable, tenant-safe session IDs and always clear abandoned sessions.
Treat every response as replaceable state. Do not append snapshots directly to a result list as though they were deltas.
Configuration and training¶
StreamingSpan checkpoints use model_type: gliner_streaming_span. The main
architecture-specific fields are:
Field |
Description |
|---|---|
|
Causal decoder backbone and its saved configuration |
|
Marker placed after each label; defaults to |
|
Boundary between the entity prompt and text; defaults to |
|
DeBERTa-v2, ModernBERT, or RNN encoder for the compact prompt |
|
Span representation; StreamingSpan defaults to |
|
Optional DeBERTa-v2, ModernBERT, or RNN word context encoder |
|
|
|
Maximum entity span width in words |
|
Rolling span revision window |
|
Optional upper bound on cached decoder positions |
|
Stateless preprocessing limit; cached sessions use the decoder context limit instead |
Normally these values should come from the checkpoint rather than be overridden
at inference time. To train or fine-tune the architecture, adapt
configs/config_streaming_span.yaml and use the normal GLiNER training entry
point:
python train.py --config configs/config_streaming_span.yaml
The data format and general trainer options are the same as for other span-based GLiNER models; see Training and Configuration. The executable inference modes example demonstrates every serving surface, while the interactive streaming example shows how to diff and render live snapshots.