ONNX WebGPU export of GLiNER 2.5 + no-dependencies inference
Python
1
33 commits
updated Aug 27, 2026
Live demo: https://gliner25-onnx-webgpu.pages.dev
Zero-shot named-entity recognition with GLiNER2.5 (boundary architecture), fully in the browser via ONNX Runtime Web — WebGPU when available, WASM fallback. No server, no keys, no build step.
To run locally instead, serve this directory and load index.html. Nothing is bundled:
cd gliner-2.5
python3 -m http.server 8080 # or: npx serve .
# → http://localhost:8080
Requires a browser with navigator.gpu (Chrome/Edge 113+, Safari 26+ tech preview, etc.) for the WebGPU path; otherwise the demo transparently falls back to WASM. The model (one of three sizes) downloads from Hugging Face on first use and is cached by the browser.
The gliner2.5-{small,base,multi}-v1-onnx exports are revision 5 boundary
graphs: DeBERTa encoder, sparse proposer, pair reranker, classification MLP,
cached text_states, candidate_states, plus tiny heads.onnx / records.onnx.
One encoder pass. Host packing and decode live here
(clean-room from fastino-ai/GLiNER2, Apache-2.0).
words ──▶ schema pack: ( [P] parent ( [E]|[C] label … ) ) [SEP_TEXT] words
──▶ ONNX
──▶ entities: sigmoid(pair_logits) → threshold → spans
──▶ classify: sigmoid(cls_logits) → argmax / threshold
Load via weightlift ModelManager + glinerModel()
in this package. Demo pins [email protected] on jsDelivr (same version as
package.json) because Cloudflare Pages direct-upload does not honor
.gitignore; do not put node_modules in this directory.
Still not in the graph: JointIE / extract_relations ([R]), record-mode
JSON, constrained implies/excludes (those last are JS when we add them),
span-attribute rescoring. text_states is exported so those heads do not need
a second encoder later.
index.html — demo (weightlift ModelManager + Fastino templates)src/gliner-boundary.mjs — packing, v2/v3 decode, classify, long-docsrc/api.mjs — AutoExtractor-shaped JS APIsrc/weightlift.mjs — glinerModel() adaptersrc/demo-presets.mjs — Fastino README/tutorial/blog examplesPROGRAM.md — remaining heads (relations, records)export_v3_cls.py / upload_v3_models.py| Model | Params | ONNX | Languages |
|---|---|---|---|
| gliner2.5-small-v1-onnx | 74M | 288 MB | English |
| gliner2.5-base-v1-onnx | 194M | 746 MB | English |
| gliner2.5-multi-v1-onnx | 287M | 1.12 GB | Multilingual |
import { GLINER_MODELS, hfFileUrl, GlinerBoundaryRuntime, downloadModel } from "./src/gliner-boundary.mjs";
import * as ort from "https://cdn.jsdelivr.net/npm/[email protected]/dist/ort.webgpu.bundle.min.mjs";
const repo = GLINER_MODELS.small.repo;
const bytes = await downloadModel(hfFileUrl(repo, "onnx/model.onnx"));
const session = await ort.InferenceSession.create(bytes.buffer, { executionProviders: ["webgpu"] });
const { AutoTokenizer, env } = await import("https://cdn.jsdelivr.net/npm/@huggingface/[email protected]");
env.allowLocalModels = false;
const tokenizer = await AutoTokenizer.from_pretrained(repo);
const tokenize = (t) => Array.from(tokenizer.encode(t, { add_special_tokens: false }).ids ?? []);
const rt = new GlinerBoundaryRuntime({ ort, session, tokenize });
const entities = await rt.extract(
"Apple CEO Tim Cook announced iPhone 15 in Cupertino yesterday.",
["company", "person", "product", "location"],
{ threshold: 0.5 },
);
AutoExtractor pipeline produces (4-decimal parity verified). The first revision exposed boundary marginals only and required a min(sigmoid(start), sigmoid(end)) proxy; that cost precision at low thresholds. With v2, the threshold curve is flat: 0.3–0.7 all within ~0.02 F1 of peak.classify_text is one pass (max_len 4096 words).
On this host, one-shot small finished 4096 words; base and multi
died between 3500 and 3600 words. Long docs: classify_text_long /
extract_entities_long (384/64 windows). A 4096-word file is 13 runs of
384 words, not one 4096-word encode.ONNX WebGPU export of GLiNER 2.5 + no-dependencies inference
Python
1
33 commits
updated Aug 27, 2026
Live demo: https://gliner25-onnx-webgpu.pages.dev
Zero-shot named-entity recognition with GLiNER2.5 (boundary architecture), fully in the browser via ONNX Runtime Web — WebGPU when available, WASM fallback. No server, no keys, no build step.
To run locally instead, serve this directory and load index.html. Nothing is bundled:
cd gliner-2.5
python3 -m http.server 8080 # or: npx serve .
# → http://localhost:8080
Requires a browser with navigator.gpu (Chrome/Edge 113+, Safari 26+ tech preview, etc.) for the WebGPU path; otherwise the demo transparently falls back to WASM. The model (one of three sizes) downloads from Hugging Face on first use and is cached by the browser.
The gliner2.5-{small,base,multi}-v1-onnx exports are revision 5 boundary
graphs: DeBERTa encoder, sparse proposer, pair reranker, classification MLP,
cached text_states, candidate_states, plus tiny heads.onnx / records.onnx.
One encoder pass. Host packing and decode live here
(clean-room from fastino-ai/GLiNER2, Apache-2.0).
words ──▶ schema pack: ( [P] parent ( [E]|[C] label … ) ) [SEP_TEXT] words
──▶ ONNX
──▶ entities: sigmoid(pair_logits) → threshold → spans
──▶ classify: sigmoid(cls_logits) → argmax / threshold
Load via weightlift ModelManager + glinerModel()
in this package. Demo pins [email protected] on jsDelivr (same version as
package.json) because Cloudflare Pages direct-upload does not honor
.gitignore; do not put node_modules in this directory.
Still not in the graph: JointIE / extract_relations ([R]), record-mode
JSON, constrained implies/excludes (those last are JS when we add them),
span-attribute rescoring. text_states is exported so those heads do not need
a second encoder later.
index.html — demo (weightlift ModelManager + Fastino templates)src/gliner-boundary.mjs — packing, v2/v3 decode, classify, long-docsrc/api.mjs — AutoExtractor-shaped JS APIsrc/weightlift.mjs — glinerModel() adaptersrc/demo-presets.mjs — Fastino README/tutorial/blog examplesPROGRAM.md — remaining heads (relations, records)export_v3_cls.py / upload_v3_models.py| Model | Params | ONNX | Languages |
|---|---|---|---|
| gliner2.5-small-v1-onnx | 74M | 288 MB | English |
| gliner2.5-base-v1-onnx | 194M | 746 MB | English |
| gliner2.5-multi-v1-onnx | 287M | 1.12 GB | Multilingual |
import { GLINER_MODELS, hfFileUrl, GlinerBoundaryRuntime, downloadModel } from "./src/gliner-boundary.mjs";
import * as ort from "https://cdn.jsdelivr.net/npm/[email protected]/dist/ort.webgpu.bundle.min.mjs";
const repo = GLINER_MODELS.small.repo;
const bytes = await downloadModel(hfFileUrl(repo, "onnx/model.onnx"));
const session = await ort.InferenceSession.create(bytes.buffer, { executionProviders: ["webgpu"] });
const { AutoTokenizer, env } = await import("https://cdn.jsdelivr.net/npm/@huggingface/[email protected]");
env.allowLocalModels = false;
const tokenizer = await AutoTokenizer.from_pretrained(repo);
const tokenize = (t) => Array.from(tokenizer.encode(t, { add_special_tokens: false }).ids ?? []);
const rt = new GlinerBoundaryRuntime({ ort, session, tokenize });
const entities = await rt.extract(
"Apple CEO Tim Cook announced iPhone 15 in Cupertino yesterday.",
["company", "person", "product", "location"],
{ threshold: 0.5 },
);
AutoExtractor pipeline produces (4-decimal parity verified). The first revision exposed boundary marginals only and required a min(sigmoid(start), sigmoid(end)) proxy; that cost precision at low thresholds. With v2, the threshold curve is flat: 0.3–0.7 all within ~0.02 F1 of peak.classify_text is one pass (max_len 4096 words).
On this host, one-shot small finished 4096 words; base and multi
died between 3500 and 3600 words. Long docs: classify_text_long /
extract_entities_long (384/64 windows). A 4096-word file is 13 runs of
384 words, not one 4096-word encode.