use anyhow::{Context, Result};
use std::path::Path;
use rayon::prelude::*;
use crate::EmbedEngine;
use crate::encoder_weights::EncBatch;
use crate::gliner_gpu::GlinerGpuHead;
use crate::weights::LazySt;
const MAX_WORDS: usize = 512;
const MAX_TYPES: usize = 32;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GlinerDevice {
Auto,
Cpu,
}
#[derive(Default)]
struct Linear {
w: Vec<f32>,
b: Vec<f32>,
n: usize,
k: usize,
packed: std::sync::OnceLock<crate::cpu_gemm::PackedWeight>,
}
impl Linear {
fn load(st: &LazySt, prefix: &str) -> Result<Self> {
let w = st.tensor_f32(&format!("{prefix}.weight"))?;
let b = st.tensor_f32(&format!("{prefix}.bias"))?;
let n = b.len();
let k = w.len() / n;
Ok(Self {
w,
b,
n,
k,
packed: std::sync::OnceLock::new(),
})
}
fn forward(&self, x: &[f32]) -> Vec<f32> {
let (n, k) = (self.n, self.k);
let m = x.len() / k;
assert_eq!(x.len(), m * k, "lhs shape");
assert_eq!(self.w.len(), n * k, "weight shape");
let mut out = vec![0f32; m * n];
let packed = self
.packed
.get_or_init(|| crate::cpu_gemm::PackedWeight::new(&self.w, n, k));
crate::cpu_gemm::gemm_packed(&mut out, x, packed, m, Some(&self.b));
out
}
}
struct Mlp {
up: Linear,
down: Linear,
}
impl Mlp {
fn load(st: &LazySt, prefix: &str) -> Result<Self> {
Ok(Self {
up: Linear::load(st, &format!("{prefix}.0"))?,
down: Linear::load(st, &format!("{prefix}.3"))?,
})
}
fn forward(&self, x: &[f32]) -> Vec<f32> {
let mut h = self.up.forward(x);
for v in h.iter_mut() {
*v = v.max(0.0); }
self.down.forward(&h)
}
}
struct BiLstm {
fwd: LstmDir,
rev: LstmDir,
hidden: usize,
}
struct LstmDir {
in_proj: Linear,
w_hh: Vec<f32>,
b_hh: Vec<f32>,
input: usize,
hidden: usize,
}
impl LstmDir {
fn load(st: &LazySt, prefix: &str, suffix: &str) -> Result<Self> {
let w_ih = st.tensor_f32(&format!("{prefix}.weight_ih_l0{suffix}"))?;
let w_hh = st.tensor_f32(&format!("{prefix}.weight_hh_l0{suffix}"))?;
let b_ih = st.tensor_f32(&format!("{prefix}.bias_ih_l0{suffix}"))?;
let b_hh = st.tensor_f32(&format!("{prefix}.bias_hh_l0{suffix}"))?;
let hidden = b_ih.len() / 4;
let input = w_ih.len() / (4 * hidden);
Ok(Self {
in_proj: Linear {
n: 4 * hidden,
k: input,
w: w_ih,
b: b_ih,
..Default::default()
},
w_hh,
b_hh,
input,
hidden,
})
}
fn run(&self, x: &[f32], order: impl Iterator<Item = usize>) -> Vec<f32> {
let (h_n, i_n) = (self.hidden, self.input);
let t = x.len() / i_n;
let xg = self.in_proj.forward(x); let mut out = vec![0f32; t * h_n];
let mut h = vec![0f32; h_n];
let mut c = vec![0f32; h_n];
let mut gates = vec![0f32; 4 * h_n];
for step in order {
let xr = &xg[step * 4 * h_n..(step + 1) * 4 * h_n];
for (g, gate) in gates.iter_mut().enumerate() {
*gate = xr[g] + self.b_hh[g];
}
gemv_acc(&mut gates, &self.w_hh, &h, h_n);
for j in 0..h_n {
let i_g = sigmoid(gates[j]);
let f_g = sigmoid(gates[h_n + j]);
let g_g = gates[2 * h_n + j].tanh();
let o_g = sigmoid(gates[3 * h_n + j]);
c[j] = f_g * c[j] + i_g * g_g;
h[j] = o_g * c[j].tanh();
}
out[step * h_n..(step + 1) * h_n].copy_from_slice(&h);
}
out
}
}
impl BiLstm {
fn load(st: &LazySt, prefix: &str) -> Result<Self> {
let fwd = LstmDir::load(st, prefix, "")?;
let rev = LstmDir::load(st, prefix, "_reverse")?;
anyhow::ensure!(
fwd.hidden == rev.hidden && fwd.input == rev.input,
"BiLSTM directions disagree on geometry"
);
let hidden = fwd.hidden;
Ok(Self { fwd, rev, hidden })
}
fn forward(&self, x: &[f32]) -> Vec<f32> {
let t = x.len() / self.fwd.input;
let (f, r) = rayon::join(|| self.fwd.run(x, 0..t), || self.rev.run(x, (0..t).rev()));
let h = self.hidden;
let mut out = vec![0f32; t * 2 * h];
for step in 0..t {
out[step * 2 * h..step * 2 * h + h].copy_from_slice(&f[step * h..(step + 1) * h]);
out[step * 2 * h + h..(step + 1) * 2 * h].copy_from_slice(&r[step * h..(step + 1) * h]);
}
out
}
}
#[inline]
fn gemv_acc(dst: &mut [f32], w: &[f32], x: &[f32], k: usize) {
debug_assert_eq!(x.len(), k);
crate::simd::gemv_acc(dst, w, x);
}
fn sigmoid(v: f32) -> f32 {
if v >= 0.0 {
1.0 / (1.0 + (-v).exp())
} else {
let e = v.exp();
e / (1.0 + e)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Entity {
pub start: usize,
pub end: usize,
pub label: usize,
pub score: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TextEntity {
pub start: usize,
pub end: usize,
pub text: String,
pub label: String,
pub score: f32,
}
fn split_words(text: &str) -> Vec<(usize, usize)> {
fn is_word(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
let chars: Vec<(usize, char)> = text.char_indices().collect();
let end_of =
|i: usize| -> usize { chars.get(i).map(|(o, _)| *o).unwrap_or_else(|| text.len()) };
let mut out = Vec::new();
let mut i = 0;
while i < chars.len() {
let (off, c) = chars[i];
if c.is_whitespace() {
i += 1;
} else if is_word(c) {
let mut j = i;
while j < chars.len() && is_word(chars[j].1) {
j += 1;
}
while j + 1 < chars.len()
&& (chars[j].1 == '-' || chars[j].1 == '_')
&& is_word(chars[j + 1].1)
{
j += 1;
while j < chars.len() && is_word(chars[j].1) {
j += 1;
}
}
out.push((off, end_of(j)));
i = j;
} else {
out.push((off, off + c.len_utf8()));
i += 1;
}
}
out
}
struct SpanUp {
left: Linear,
right: Linear,
}
impl SpanUp {
fn load(st: &LazySt, prefix: &str) -> Result<Self> {
let up = Linear::load(st, prefix)?;
let (n, k) = (up.n, up.k); anyhow::ensure!(
k % 2 == 0,
"out_project input {k} is not a concat of two halves"
);
let half = k / 2;
let mut left = Vec::with_capacity(n * half);
let mut right = Vec::with_capacity(n * half);
for row in up.w.chunks_exact(k) {
left.extend_from_slice(&row[..half]);
right.extend_from_slice(&row[half..]);
}
Ok(Self {
left: Linear {
w: left,
b: vec![0.0; n], n,
k: half,
..Default::default()
},
right: Linear {
w: right,
b: up.b,
n,
k: half,
..Default::default()
},
})
}
}
pub struct Gliner {
backbone: EmbedEngine,
projection: Linear,
rnn: BiLstm,
project_start: Mlp,
project_end: Mlp,
span_up: SpanUp,
span_down: Linear,
prompt_rep: Mlp,
gpu_head: Option<GlinerGpuHead>,
max_width: usize,
hidden: usize,
ent_token_id: u32,
#[cfg(feature = "cli")]
tokenizer: Option<tokenizers::Tokenizer>,
#[cfg(feature = "cli")]
ent_token: String,
#[cfg(feature = "cli")]
sep_token: String,
}
impl Gliner {
pub fn load(dir: &Path, ent_token_id: u32) -> Result<Self> {
Self::load_on(dir, ent_token_id, GlinerDevice::Auto)
}
pub fn load_on(dir: &Path, ent_token_id: u32, device: GlinerDevice) -> Result<Self> {
let backbone = match device {
GlinerDevice::Auto => EmbedEngine::auto(dir, 8192)?,
GlinerDevice::Cpu => EmbedEngine::cpu(dir)?,
};
let st = LazySt::open(dir)?;
let p = "span_rep_layer.span_rep_layer";
let projection = Linear::load(&st, "token_rep_layer.projection")?;
let rnn = BiLstm::load(&st, "rnn.lstm")?;
let head_cfg: serde_json::Value = std::fs::read(dir.join("gliner_head.json"))
.ok()
.and_then(|b| serde_json::from_slice(&b).ok())
.unwrap_or(serde_json::Value::Null);
#[cfg(feature = "cli")]
let s = |k: &str, d: &str| -> String {
head_cfg
.get(k)
.and_then(|x| x.as_str())
.unwrap_or(d)
.to_string()
};
#[cfg(feature = "cli")]
let tokenizer = tokenizers::Tokenizer::from_file(dir.join("tokenizer.json")).ok();
let head = Self {
projection,
hidden: rnn.hidden * 2,
rnn,
project_start: Mlp::load(&st, &format!("{p}.project_start"))?,
project_end: Mlp::load(&st, &format!("{p}.project_end"))?,
span_up: SpanUp::load(&st, &format!("{p}.out_project.0"))?,
span_down: Linear::load(&st, &format!("{p}.out_project.3"))?,
prompt_rep: Mlp::load(&st, "prompt_rep_layer")?,
max_width: head_cfg
.get("max_width")
.and_then(|x| x.as_u64())
.unwrap_or(12) as usize,
backbone,
ent_token_id,
#[cfg(feature = "cli")]
tokenizer,
#[cfg(feature = "cli")]
ent_token: s("ent_token", "<<ENT>>"),
#[cfg(feature = "cli")]
sep_token: s("sep_token", "<<SEP>>"),
gpu_head: None,
};
anyhow::ensure!(
head.projection.n == head.hidden,
"projection outputs {} but the BiLSTM is {}-wide",
head.projection.n,
head.hidden
);
let mut head = head;
if let Some(ctx) = head.backbone.gpu_ctx() {
let (d, up_w) = (head.hidden, head.span_up.left.n);
head.gpu_head = Some(GlinerGpuHead::new(
ctx,
(
&head.project_start.up.w,
&head.project_start.up.b,
&head.project_start.down.w,
&head.project_start.down.b,
),
(
&head.project_end.up.w,
&head.project_end.up.b,
&head.project_end.down.w,
&head.project_end.down.b,
),
(&head.span_up.left.w, &head.span_up.left.b),
(&head.span_up.right.w, &head.span_up.right.b),
(&head.span_down.w, &head.span_down.b),
d,
up_w,
head.max_width,
MAX_WORDS,
MAX_TYPES,
)?);
}
Ok(head)
}
pub fn with_max_width(mut self, max_width: usize) -> Self {
self.max_width = max_width;
self
}
#[cfg(feature = "cli")]
pub fn predict_text(
&mut self,
text: &str,
labels: &[impl AsRef<str>],
threshold: f32,
) -> Result<Vec<TextEntity>> {
let tokenizer = self
.tokenizer
.as_ref()
.context("checkpoint has no tokenizer.json — use `predict` with your own token ids")?;
anyhow::ensure!(!labels.is_empty(), "no entity types given");
let spans = split_words(text);
let mut seq: Vec<String> = Vec::with_capacity(2 * labels.len() + 1 + spans.len());
for l in labels {
seq.push(self.ent_token.clone());
seq.push(l.as_ref().to_string());
}
seq.push(self.sep_token.clone());
let prompt_len = seq.len();
seq.extend(spans.iter().map(|&(a, b)| text[a..b].to_string()));
let enc = tokenizer
.encode(tokenizers::InputSequence::from(seq), true)
.map_err(|e| anyhow::anyhow!("gliner tokenize: {e}"))?;
let ids = enc.get_ids().to_vec();
let mut words_mask = vec![0u32; ids.len()];
let mut prev: Option<u32> = None;
for (i, wid) in enc.get_word_ids().iter().enumerate() {
if let Some(w) = wid {
if prev != Some(*w) && (*w as usize) >= prompt_len {
words_mask[i] = *w + 1 - prompt_len as u32;
}
prev = Some(*w);
}
}
let hits = self.predict(&ids, &words_mask, threshold)?;
Ok(hits
.into_iter()
.map(|e| {
let (start, _) = spans[e.start];
let (_, end) = spans[e.end];
TextEntity {
start,
end,
text: text[start..end].to_string(),
label: labels[e.label].as_ref().to_string(),
score: e.score,
}
})
.collect())
}
pub fn device(&self) -> String {
self.backbone.device()
}
pub fn hidden(&self) -> usize {
self.hidden
}
pub fn scores(&mut self, input_ids: &[u32], words_mask: &[u32]) -> Result<Vec<Vec<Vec<f32>>>> {
anyhow::ensure!(
input_ids.len() == words_mask.len(),
"words_mask has {} entries for {} tokens",
words_mask.len(),
input_ids.len()
);
let d = self.hidden;
let prof = std::env::var("GLINER_PROFILE").is_ok();
let mut t = std::time::Instant::now();
let mut lap = |name: &str| {
if prof {
eprintln!(
" [profile] {name:<12} {:6.1} ms",
t.elapsed().as_secs_f64() * 1e3
);
}
t = std::time::Instant::now();
};
let states = self
.backbone
.forward_hidden(&EncBatch::from_seqs([input_ids.to_vec()]))
.context("gliner backbone")?;
lap("backbone");
let tokens = self.projection.forward(&states);
lap("projection");
let mut prompts: Vec<f32> = Vec::new();
for (i, &id) in input_ids.iter().enumerate() {
if id == self.ent_token_id {
prompts.extend_from_slice(&tokens[i * d..(i + 1) * d]);
}
}
let n_types = prompts.len() / d;
anyhow::ensure!(n_types > 0, "no <<ENT>> markers in input_ids");
let n_words = *words_mask.iter().max().unwrap_or(&0) as usize;
anyhow::ensure!(n_words > 0, "words_mask marks no words");
let mut words = vec![0f32; n_words * d];
for (i, &w) in words_mask.iter().enumerate() {
if w > 0 {
let dst = (w as usize - 1) * d;
words[dst..dst + d].copy_from_slice(&tokens[i * d..(i + 1) * d]);
}
}
let words = self.rnn.forward(&words);
lap("bilstm");
let prompt = self.prompt_rep.forward(&prompts);
if let (Some(gh), Some(ctx)) = (&self.gpu_head, self.backbone.gpu_ctx())
&& n_words <= MAX_WORDS
&& n_types <= MAX_TYPES
{
let flat = gh.scores(ctx, &words, &prompt, n_words, n_types)?;
lap("head(gpu)");
let mut out = vec![vec![vec![0f32; n_types]; self.max_width]; n_words];
for (l, per_width) in out.iter_mut().enumerate() {
for (k, per_type) in per_width.iter_mut().enumerate() {
if l + k >= n_words {
continue; }
let base = (l * self.max_width + k) * n_types;
per_type.copy_from_slice(&flat[base..base + n_types]);
}
}
return Ok(out);
}
let start = self.project_start.forward(&words);
let end = self.project_end.forward(&words);
let valid: Vec<(usize, usize)> = (0..n_words)
.flat_map(|l| (0..self.max_width).map(move |k| (l, k)))
.filter(|&(l, k)| l + k < n_words)
.collect();
let relu = |v: &[f32]| -> Vec<f32> { v.iter().map(|x| x.max(0.0)).collect() };
let a = self.span_up.left.forward(&relu(&start)); let b = self.span_up.right.forward(&relu(&end)); let up_w = self.span_up.left.n;
let mut h = vec![0f32; valid.len() * up_w];
h.par_chunks_exact_mut(up_w)
.zip(valid.par_iter())
.for_each(|(dst, &(l, k))| {
let (ar, br) = (&a[l * up_w..], &b[(l + k) * up_w..]);
for j in 0..up_w {
dst[j] = (ar[j] + br[j]).max(0.0);
}
});
let span_reps = self.span_down.forward(&h);
lap("head(cpu)");
let scorer = Linear {
w: prompt,
b: vec![0.0; n_types],
n: n_types,
k: d,
..Default::default()
};
let flat = scorer.forward(&span_reps);
let mut out = vec![vec![vec![0f32; n_types]; self.max_width]; n_words];
for (row, &(l, k)) in valid.iter().enumerate() {
out[l][k].copy_from_slice(&flat[row * n_types..(row + 1) * n_types]);
}
Ok(out)
}
pub fn predict(
&mut self,
input_ids: &[u32],
words_mask: &[u32],
threshold: f32,
) -> Result<Vec<Entity>> {
let scores = self.scores(input_ids, words_mask)?;
let n_words = scores.len();
let mut hits: Vec<Entity> = Vec::new();
for (l, per_width) in scores.iter().enumerate() {
for (k, per_type) in per_width.iter().enumerate() {
if l + k >= n_words {
continue;
}
for (c, &raw) in per_type.iter().enumerate() {
let p = sigmoid(raw);
if p > threshold {
hits.push(Entity {
start: l,
end: l + k,
label: c,
score: p,
});
}
}
}
}
hits.sort_by(|a, b| {
b.score
.partial_cmp(&a.score)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.start.cmp(&b.start))
});
let mut kept: Vec<Entity> = Vec::new();
for h in hits {
if kept.iter().any(|k| h.start <= k.end && k.start <= h.end) {
continue;
}
kept.push(h);
}
kept.sort_by_key(|e| (e.start, e.end));
Ok(kept)
}
}
#[cfg(test)]
mod lstm_probe {
use super::*;
#[test]
#[ignore = "perf probe: cargo test --release lstm_probe -- --ignored --nocapture"]
fn split_the_recurrent_step() {
const H: usize = 256; const T: usize = 176; let w: Vec<f32> = (0..4 * H * H)
.map(|i| ((i % 19) as f32 - 9.0) * 0.01)
.collect();
let x: Vec<f32> = (0..H).map(|i| ((i % 23) as f32 - 11.0) * 0.01).collect();
let mut gates = vec![0.1f32; 4 * H];
let (mut c, mut h) = (vec![0.05f32; H], vec![0f32; H]);
let (mut gemv, mut epi) = (f64::MAX, f64::MAX);
for _ in 0..20 {
let t0 = std::time::Instant::now();
for _ in 0..T {
gemv_acc(&mut gates, &w, &x, H);
}
gemv = gemv.min(t0.elapsed().as_secs_f64());
let t0 = std::time::Instant::now();
for _ in 0..T {
for j in 0..H {
let i_g = sigmoid(gates[j]);
let f_g = sigmoid(gates[H + j]);
let g_g = gates[2 * H + j].tanh();
let o_g = sigmoid(gates[3 * H + j]);
c[j] = f_g * c[j] + i_g * g_g;
h[j] = o_g * c[j].tanh();
}
}
epi = epi.min(t0.elapsed().as_secs_f64());
}
let mut read = f64::MAX;
let mut sink = 0f32;
for _ in 0..20 {
let t0 = std::time::Instant::now();
for _ in 0..T {
let mut acc = [0f32; 4];
for ch in w.chunks_exact(4) {
for l in 0..4 {
acc[l] += ch[l];
}
}
sink += acc[0] + acc[1] + acc[2] + acc[3];
}
read = read.min(t0.elapsed().as_secs_f64());
}
let read_gbs = (T * 4 * H * H * 4) as f64 / read / 1e9;
eprintln!(
" pure-read ceiling: {:6.2} ms ({read_gbs:5.1} GB/s) [sink {sink:.1}]",
read * 1e3
);
let gf = 2.0 * (T * 4 * H * H) as f64 / gemv / 1e9;
let gbs = (T * 4 * H * H * 4) as f64 / gemv / 1e9;
eprintln!(
"\n one direction, T={T}: gemv {:6.2} ms ({gf:5.1} GF/s, {gbs:5.1} GB/s) | epilogue {:6.2} ms\n",
gemv * 1e3,
epi * 1e3,
);
assert!(h.iter().any(|v| *v != 0.0));
}
}