use anyhow::{Context, Result};
use std::path::Path;
use rayon::prelude::*;
use crate::EmbedEngine;
use crate::encoder_weights::EncBatch;
use crate::gliner::GlinerDevice;
use crate::weights::LazySt;
#[derive(Default)]
struct Linear {
w: Vec<f32>,
b: Vec<f32>,
n: usize,
k: usize,
packed: std::sync::OnceLock<crate::cpu_gemm::PackedWeight>,
gpu: std::sync::OnceLock<(wgpu::Buffer, wgpu::Buffer)>,
}
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(),
gpu: std::sync::OnceLock::new(),
})
}
fn gpu_weights(&self, ctx: &crate::GpuCtx) -> (&wgpu::Buffer, &wgpu::Buffer) {
let (w, b) = self
.gpu
.get_or_init(|| (ctx.storage(&self.w), ctx.storage(&self.b)));
(w, b)
}
fn forward_gpu(
&self,
gpu: Option<(&crate::GpuCtx, &crate::encoder::EncKernels)>,
x: &[f32],
) -> Option<Vec<f32>> {
let (ctx, kern) = gpu?;
let (wb, bb) = self
.gpu
.get_or_init(|| (ctx.storage(&self.w), ctx.storage(&self.b)));
let m = x.len() / self.k;
kern.gemm_resident(ctx, x, wb, bb, m, self.n, self.k, None)
.ok()
}
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");
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, down_idx: u32) -> Result<Self> {
Ok(Self {
up: Linear::load(st, &format!("{prefix}.0"))?,
down: Linear::load(st, &format!("{prefix}.{down_idx}"))?,
})
}
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)
}
fn forward_on(
&self,
gpu: Option<(&crate::GpuCtx, &crate::encoder::EncKernels)>,
x: &[f32],
) -> Vec<f32> {
let mut h = self
.up
.forward_gpu(gpu, x)
.unwrap_or_else(|| self.up.forward(x));
for v in h.iter_mut() {
*v = v.max(0.0);
}
self.down
.forward_gpu(gpu, &h)
.unwrap_or_else(|| self.down.forward(&h))
}
}
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()
},
})
}
}
struct Gru {
w_ih: Vec<f32>, w_hh: Vec<f32>, b_ih: Vec<f32>, b_hh: Vec<f32>, pos: Vec<f32>, projector: Mlp, h: usize,
max_count: usize,
}
impl Gru {
fn load(st: &LazySt, prefix: &str) -> Result<Self> {
let w_ih = st.tensor_f32(&format!("{prefix}.gru.weight_ih_l0"))?;
let w_hh = st.tensor_f32(&format!("{prefix}.gru.weight_hh_l0"))?;
let b_ih = st.tensor_f32(&format!("{prefix}.gru.bias_ih_l0"))?;
let b_hh = st.tensor_f32(&format!("{prefix}.gru.bias_hh_l0"))?;
let pos = st.tensor_f32(&format!("{prefix}.pos_embedding.weight"))?;
let h = b_ih.len() / 3;
let max_count = pos.len() / h;
anyhow::ensure!(
w_ih.len() == 3 * h * h && w_hh.len() == 3 * h * h,
"GRU weight geometry"
);
Ok(Self {
w_ih,
w_hh,
b_ih,
b_hh,
pos,
projector: Mlp::load(st, &format!("{prefix}.projector"), 2)?,
h,
max_count,
})
}
fn forward(&self, fields: &[f32], count: usize) -> Vec<f32> {
let h = self.h;
let m = fields.len() / h;
let count = count.min(self.max_count);
let mut ih = vec![0f32; count * 3 * h];
for t in 0..count {
let dst = &mut ih[t * 3 * h..(t + 1) * 3 * h];
dst.copy_from_slice(&self.b_ih);
crate::simd::gemv_acc(dst, &self.w_ih, &self.pos[t * h..(t + 1) * h]);
}
let mut out = vec![0f32; count * m * h];
let mut hh = vec![0f32; 3 * h];
for f in 0..m {
let field = &fields[f * h..(f + 1) * h];
let mut hs = field.to_vec();
for t in 0..count {
let ihs = &ih[t * 3 * h..(t + 1) * 3 * h];
hh.copy_from_slice(&self.b_hh);
crate::simd::gemv_acc(&mut hh, &self.w_hh, &hs);
for j in 0..h {
let r = sigmoid(ihs[j] + hh[j]);
let z = sigmoid(ihs[h + j] + hh[h + j]);
let n = (ihs[2 * h + j] + r * hh[2 * h + j]).tanh();
hs[j] = (1.0 - z) * n + z * hs[j];
}
let mut cat = Vec::with_capacity(2 * h);
cat.extend_from_slice(&hs);
cat.extend_from_slice(field);
let proj = self.projector.forward(&cat);
out[(t * m + f) * h..(t * m + f + 1) * h].copy_from_slice(&proj);
}
}
out
}
}
fn sigmoid(v: f32) -> f32 {
if v >= 0.0 {
1.0 / (1.0 + (-v).exp())
} else {
let e = v.exp();
e / (1.0 + e)
}
}
fn softmax(logits: &[f32]) -> Vec<f32> {
let max = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let exps: Vec<f32> = logits.iter().map(|&v| (v - max).exp()).collect();
let sum: f32 = exps.iter().sum();
exps.iter().map(|&e| e / sum).collect()
}
#[derive(Debug, Clone, PartialEq)]
pub struct RelEnd {
pub text: String,
pub start: usize,
pub end: usize,
pub confidence: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Relation {
pub label: String,
pub head: RelEnd,
pub tail: RelEnd,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Ner2Entity {
pub text: String,
pub label: String,
pub start: usize,
pub end: usize,
pub confidence: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ClsLabelScore {
pub label: String,
pub score: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ClsResult {
pub task: String,
pub scores: Vec<ClsLabelScore>,
pub chosen: Vec<ClsLabelScore>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ClsActivation {
#[default]
Auto,
Sigmoid,
Softmax,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ClsTask {
pub name: String,
pub labels: Vec<String>,
pub multi_label: bool,
pub cls_threshold: f32,
pub class_act: ClsActivation,
pub prompt: Option<String>,
pub label_descriptions: Vec<(String, String)>,
pub examples: Vec<(String, String)>,
}
impl ClsTask {
pub fn new(name: impl Into<String>, labels: Vec<String>) -> Self {
Self {
name: name.into(),
labels,
multi_label: false,
cls_threshold: 0.5,
class_act: ClsActivation::Auto,
prompt: None,
label_descriptions: Vec::new(),
examples: Vec::new(),
}
}
}
pub struct ClsIntermediates {
pub input_ids: Vec<u32>,
pub last_hidden_state: Vec<f32>, pub schema_block0: Vec<f32>, pub logits0: Vec<f32>, pub probs0: Vec<f32>, }
pub struct RelIntermediates {
pub input_ids: Vec<u32>,
pub last_hidden_state: Vec<f32>, pub token_embs: Vec<f32>, pub schema_block0: Vec<f32>, pub count_logits: Vec<f32>, pub pred_count: usize, pub struct_proj: Vec<f32>, pub span_rep: Vec<f32>, pub span_scores: Vec<f32>, pub relations: Vec<Relation>,
pub n_words: usize,
pub t: usize,
}
struct Block {
label: String,
p_pos: usize,
field_pos: [usize; 2],
}
struct Word {
first_tok: usize,
char_start: usize,
char_end: usize,
}
#[cfg(feature = "cli")]
struct WordTok {
ids: Vec<u32>,
char_start: usize,
char_end: usize,
}
struct ClsBlock {
p_pos: usize,
l_pos: Vec<usize>,
}
struct Block0 {
schema: Vec<f32>,
count_logits: Vec<f32>,
pred_count: usize,
struct_proj: Vec<f32>,
span_scores: Vec<f32>,
}
const FILL_WGSL: &str = r#"
@group(0) @binding(0) var<storage, read> a: array<f32>;
@group(0) @binding(1) var<storage, read> b: array<f32>;
@group(0) @binding(2) var<storage, read> idx: array<u32>;
@group(0) @binding(3) var<storage, read_write> out: array<f32>;
@group(0) @binding(4) var<uniform> dims: vec4<u32>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) g: vec3<u32>) {
let rows = dims.x;
let w = dims.y;
let j = g.x;
let row = g.y;
if (j >= w || row >= rows) { return; }
let li = idx[row * 2u];
let lj = idx[row * 2u + 1u];
out[row * w + j] = max(a[li * w + j] + b[lj * w + j], 0.0);
}
"#;
pub struct Gliner2 {
fill_pl: std::sync::OnceLock<wgpu::ComputePipeline>,
backbone: EmbedEngine,
project_start: Mlp, project_end: Mlp,
span_up: SpanUp, span_down: Linear, count_pred: Mlp, count_embed: Option<Gru>,
counting_layer: String,
classifier: Mlp, max_width: usize,
hidden: usize,
#[cfg(feature = "cli")]
tokenizer: tokenizers::Tokenizer,
#[cfg(feature = "cli")]
splitter: regex::Regex,
}
impl Gliner2 {
pub fn load(dir: &Path) -> Result<Self> {
Self::load_on(dir, GlinerDevice::Auto)
}
pub fn load_on(dir: &Path, 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 head_cfg: serde_json::Value = std::fs::read(dir.join("gliner2_head.json"))
.ok()
.and_then(|b| serde_json::from_slice(&b).ok())
.unwrap_or(serde_json::Value::Null);
let hidden = head_cfg
.get("hidden_size")
.and_then(|x| x.as_u64())
.unwrap_or(768) as usize;
let max_width = head_cfg
.get("max_width")
.and_then(|x| x.as_u64())
.unwrap_or(8) as usize;
let counting_layer = head_cfg
.get("counting_layer")
.and_then(|x| x.as_str())
.unwrap_or("count_lstm")
.to_string();
let count_embed = if counting_layer == "count_lstm" {
Some(Gru::load(&st, "count_embed")?)
} else {
None
};
let sp = "span_rep.span_rep_layer";
Ok(Self {
fill_pl: std::sync::OnceLock::new(),
project_start: Mlp::load(&st, &format!("{sp}.project_start"), 3)?,
project_end: Mlp::load(&st, &format!("{sp}.project_end"), 3)?,
span_up: SpanUp::load(&st, &format!("{sp}.out_project.0"))?,
span_down: Linear::load(&st, &format!("{sp}.out_project.3"))?,
count_pred: Mlp::load(&st, "count_pred", 2)?,
count_embed,
counting_layer,
classifier: Mlp::load(&st, "classifier", 2)?,
max_width,
hidden,
backbone,
#[cfg(feature = "cli")]
tokenizer: tokenizers::Tokenizer::from_file(dir.join("tokenizer.json"))
.map_err(|e| anyhow::anyhow!("gliner2 tokenizer: {e}"))?,
#[cfg(feature = "cli")]
splitter: regex::Regex::new(
r"(?ix)(?:https?://[^\s]+|www\.[^\s]+)|[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}|@[a-z0-9_]+|\w+(?:[-_]\w+)*|\S",
)?,
})
}
pub fn device(&self) -> String {
self.backbone.device()
}
#[cfg(feature = "cli")]
pub fn predict_relations(
&mut self,
text: &str,
labels: &[impl AsRef<str>],
threshold: f32,
) -> Result<Vec<Relation>> {
Ok(self.relations_debug(text, labels, threshold)?.0)
}
#[cfg(feature = "cli")]
pub fn relations_debug(
&mut self,
text: &str,
labels: &[impl AsRef<str>],
threshold: f32,
) -> Result<(Vec<Relation>, RelIntermediates)> {
anyhow::ensure!(!labels.is_empty(), "no relation labels given");
let Some(count_embed) = &self.count_embed else {
anyhow::bail!(
"counting layer {:?} is not ported — this checkpoint serves classification only \
(classify_text)",
self.counting_layer
);
};
let text = normalize_text(text);
let text = text.as_str();
let (input_ids, blocks, words) = self.tokenize(text, labels);
let orig: Vec<char> = text.chars().collect();
let states = self
.backbone
.forward_hidden(&EncBatch::from_seqs([input_ids.clone()]))
.context("gliner2 backbone")?;
let h = self.hidden;
let t = states.len() / h;
let l = words.len();
let mut token_embs = vec![0f32; l * h];
for (wi, w) in words.iter().enumerate() {
token_embs[wi * h..(wi + 1) * h]
.copy_from_slice(&states[w.first_tok * h..(w.first_tok + 1) * h]);
}
let span_rep = self.span_rep(&token_embs, l);
let mut relations = Vec::new();
let mut dbg_block0: Option<Block0> = None;
for blk in &blocks {
let mut schema = vec![0f32; 3 * h];
schema[0..h].copy_from_slice(&states[blk.p_pos * h..(blk.p_pos + 1) * h]);
schema[h..2 * h]
.copy_from_slice(&states[blk.field_pos[0] * h..(blk.field_pos[0] + 1) * h]);
schema[2 * h..3 * h]
.copy_from_slice(&states[blk.field_pos[1] * h..(blk.field_pos[1] + 1) * h]);
let count_logits = self.count_pred.forward(&schema[0..h]);
let pred_count = argmax(&count_logits);
let (struct_proj, span_scores) = if pred_count == 0 {
(Vec::new(), Vec::new())
} else {
let struct_proj = count_embed.forward(&schema[h..3 * h], pred_count);
let bp = struct_proj.len() / h; let scorer = Linear {
w: struct_proj.clone(),
b: vec![0.0; bp],
n: bp,
k: h,
..Default::default()
};
let t_score = std::time::Instant::now();
let mut flat = scorer.forward(&span_rep); if std::env::var("OSFKB_GLINER2_TRACE").is_ok_and(|v| v != "0") {
eprintln!(
" scorer: {:.3}s (rows {}, k {}, n {})",
t_score.elapsed().as_secs_f64(),
span_rep.len() / h,
h,
bp,
);
}
for v in flat.iter_mut() {
*v = sigmoid(*v);
}
let count = pred_count.min(count_embed.max_count);
let m = bp / count;
let kmax = self.max_width;
let mut ss = vec![0f32; count * m * l * kmax];
for li in 0..l {
for ki in 0..kmax {
let srow = (li * kmax + ki) * bp;
for b in 0..count {
for p in 0..m {
ss[((b * m + p) * l + li) * kmax + ki] = flat[srow + b * m + p];
}
}
}
}
(struct_proj, ss)
};
if !span_scores.is_empty() {
let count = pred_count.min(count_embed.max_count);
let m = 2usize;
let kmax = self.max_width;
for b in 0..count {
let head = find_span(&span_scores, b, 0, m, l, kmax, threshold, &words, &orig);
let tail = find_span(&span_scores, b, 1, m, l, kmax, threshold, &words, &orig);
if let (Some(head), Some(tail)) = (head, tail) {
relations.push(Relation {
label: blk.label.clone(),
head,
tail,
});
}
}
}
if dbg_block0.is_none() {
dbg_block0 = Some(Block0 {
schema,
count_logits,
pred_count,
struct_proj,
span_scores,
});
}
}
let b0 = dbg_block0.unwrap_or(Block0 {
schema: vec![],
count_logits: vec![],
pred_count: 0,
struct_proj: vec![],
span_scores: vec![],
});
let inter = RelIntermediates {
input_ids,
last_hidden_state: states,
token_embs,
schema_block0: b0.schema,
count_logits: b0.count_logits,
pred_count: b0.pred_count,
struct_proj: b0.struct_proj,
span_rep,
span_scores: b0.span_scores,
relations: relations.clone(),
n_words: l,
t,
};
Ok((relations, inter))
}
#[allow(clippy::too_many_arguments)]
fn span_rep_chained(
&self,
ctx: &crate::GpuCtx,
kern: &crate::encoder::EncKernels,
token_embs: &[f32],
l: usize,
valid: &[(usize, usize)],
) -> Option<Vec<f32>> {
use crate::encoder_weights::Act;
use crate::forward::{make_bg_pub, pipeline_pub, uni_pub};
let h = self.hidden;
let up_w = self.span_up.left.n;
let rows = valid.len();
if rows == 0 || l == 0 {
return None;
}
let idx: Vec<u32> = valid
.iter()
.flat_map(|&(li, ki)| [li as u32, (li + ki) as u32])
.collect();
let xb = ctx.storage(token_embs);
let mid = ctx.storage(&vec![0f32; l * self.project_start.up.n]);
let startb = ctx.storage(&vec![0f32; l * h]);
let endb = ctx.storage(&vec![0f32; l * h]);
let ab = ctx.storage(&vec![0f32; l * up_w]);
let bbuf = ctx.storage(&vec![0f32; l * up_w]);
let hbuf = ctx.storage(&vec![0f32; rows * up_w]);
let outb = ctx.storage(&vec![0f32; rows * h]);
let idxb = ctx.storage_bytes(bytemuck::cast_slice(&idx));
let (w, b) = &self.project_start.up.gpu_weights(ctx);
kern.gemm_resident_into(
ctx,
&xb,
w,
b,
&mid,
l,
self.project_start.up.n,
self.project_start.up.k,
Some(Act::Relu),
)
.ok()?;
let (w, b) = &self.project_start.down.gpu_weights(ctx);
kern.gemm_resident_into(
ctx,
&mid,
w,
b,
&startb,
l,
h,
self.project_start.down.k,
Some(Act::Relu),
)
.ok()?;
let (w, b) = &self.project_end.up.gpu_weights(ctx);
kern.gemm_resident_into(
ctx,
&xb,
w,
b,
&mid,
l,
self.project_end.up.n,
self.project_end.up.k,
Some(Act::Relu),
)
.ok()?;
let (w, b) = &self.project_end.down.gpu_weights(ctx);
kern.gemm_resident_into(
ctx,
&mid,
w,
b,
&endb,
l,
h,
self.project_end.down.k,
Some(Act::Relu),
)
.ok()?;
let (w, b) = &self.span_up.left.gpu_weights(ctx);
kern.gemm_resident_into(ctx, &startb, w, b, &ab, l, up_w, self.span_up.left.k, None)
.ok()?;
let (w, b) = &self.span_up.right.gpu_weights(ctx);
kern.gemm_resident_into(ctx, &endb, w, b, &bbuf, l, up_w, self.span_up.right.k, None)
.ok()?;
let pl = self
.fill_pl
.get_or_init(|| pipeline_pub(ctx, "gliner2_span_fill", FILL_WGSL));
let meta = uni_pub(
ctx,
bytemuck::cast_slice(&[rows as u32, up_w as u32, 0u32, 0u32]),
);
let bg = make_bg_pub(ctx, pl, &[&ab, &bbuf, &idxb, &hbuf], &meta);
crate::encoder::dispatch(ctx, pl, &bg, (up_w as u32).div_ceil(64), rows as u32);
let (w, b) = &self.span_down.gpu_weights(ctx);
kern.gemm_resident_into(ctx, &hbuf, w, b, &outb, rows, h, self.span_down.k, None)
.ok()?;
ctx.read(&outb, rows * h).ok()
}
fn densify(
flat: &[f32],
valid: &[(usize, usize)],
l: usize,
kmax: usize,
h: usize,
) -> Vec<f32> {
let zero = flat[0..h].to_vec(); let mut dense = vec![0f32; l * kmax * h];
for cell in dense.chunks_exact_mut(h) {
cell.copy_from_slice(&zero);
}
for (row, &(li, ki)) in valid.iter().enumerate() {
dense[(li * kmax + ki) * h..(li * kmax + ki + 1) * h]
.copy_from_slice(&flat[row * h..(row + 1) * h]);
}
dense
}
fn span_rep(&self, token_embs: &[f32], l: usize) -> Vec<f32> {
let tr = std::env::var("OSFKB_GLINER2_TRACE").is_ok_and(|v| v != "0");
let t0 = std::time::Instant::now();
let h = self.hidden;
let kmax = self.max_width;
if !std::env::var("OSFKB_GLINER2_CHAIN").is_ok_and(|v| v == "0") {
if let Some((ctx, enc)) = self.backbone.gpu_parts() {
let valid: Vec<(usize, usize)> = (0..l)
.flat_map(|li| (0..kmax).map(move |ki| (li, ki)))
.filter(|&(li, ki)| li + ki < l)
.collect();
if let Some(flat) = self.span_rep_chained(ctx, enc.kernels(), token_embs, l, &valid)
{
if tr {
eprintln!(
" span_rep {:.3}s (device chain, spans {})",
t0.elapsed().as_secs_f64(),
valid.len()
);
}
return Self::densify(&flat, &valid, l, kmax, h);
}
}
}
let gpu = (!std::env::var("OSFKB_GLINER2_HEAD_GPU").is_ok_and(|v| v == "0"))
.then(|| self.backbone.gpu_parts())
.flatten()
.map(|(ctx, enc)| (ctx, enc.kernels()));
let lin = |l: &Linear, x: &[f32]| -> Vec<f32> {
l.forward_gpu(gpu, x).unwrap_or_else(|| l.forward(x))
};
let start = self.project_start.forward_on(gpu, token_embs); let end = self.project_end.forward_on(gpu, token_embs); let relu = |v: &[f32]| -> Vec<f32> { v.iter().map(|x| x.max(0.0)).collect() };
let a = lin(&self.span_up.left, &relu(&start)); let b = lin(&self.span_up.right, &relu(&end)); let up_w = self.span_up.left.n;
let t_proj = t0.elapsed();
let valid: Vec<(usize, usize)> = (0..l)
.flat_map(|li| (0..kmax).map(move |ki| (li, ki)))
.filter(|&(li, ki)| li + ki < l)
.collect();
let t1 = std::time::Instant::now();
let mut hbuf = vec![0f32; valid.len() * up_w];
hbuf.par_chunks_mut(up_w)
.zip(valid.par_iter())
.for_each(|(row, &(li, ki))| {
let (ar, br) = (&a[li * up_w..], &b[(li + ki) * up_w..]);
for j in 0..up_w {
row[j] = (ar[j] + br[j]).max(0.0);
}
});
let t_fill = t1.elapsed();
let t_down = std::time::Instant::now();
let span_reps_flat = lin(&self.span_down, &hbuf); let t_dn = t_down.elapsed();
let t2 = std::time::Instant::now();
let dense = Self::densify(&span_reps_flat, &valid, l, kmax, h);
if tr {
eprintln!(
" span_rep {:.3}s = proj {:.3} + fill {:.3} + down {:.3} + dense {:.3} \
(L {}, spans {}, up_w {})",
t0.elapsed().as_secs_f64(),
t_proj.as_secs_f64(),
t_fill.as_secs_f64(),
t_dn.as_secs_f64(),
t2.elapsed().as_secs_f64(),
l,
valid.len(),
up_w,
);
}
dense
}
#[cfg(feature = "cli")]
fn tokenize(
&self,
text: &str,
labels: &[impl AsRef<str>],
) -> (Vec<u32>, Vec<Block>, Vec<Word>) {
let mut ids: Vec<u32> = Vec::new();
let mut blocks: Vec<Block> = Vec::new();
let push = |ids: &mut Vec<u32>, s: &str| -> (usize, usize) {
let start = ids.len();
let enc = self
.tokenizer
.encode(s, false)
.expect("gliner2 tokenize element");
ids.extend_from_slice(enc.get_ids());
(start, ids.len())
};
for (i, lab) in labels.iter().enumerate() {
if i > 0 {
push(&mut ids, "[SEP_STRUCT]");
}
push(&mut ids, "(");
let (ps, _) = push(&mut ids, "[P]");
push(&mut ids, lab.as_ref());
push(&mut ids, "(");
let (r1, _) = push(&mut ids, "[R]");
push(&mut ids, "head");
let (r2, _) = push(&mut ids, "[R]");
push(&mut ids, "tail");
push(&mut ids, ")");
push(&mut ids, ")");
blocks.push(Block {
label: lab.as_ref().to_string(),
p_pos: ps,
field_pos: [r1, r2],
});
}
push(&mut ids, "[SEP_TEXT]");
let low = text.to_lowercase();
let mut words: Vec<Word> = Vec::new();
for m in self.splitter.find_iter(&low) {
let char_start = low[..m.start()].chars().count();
let char_end = char_start + m.as_str().chars().count();
let (first_tok, _) = push(&mut ids, m.as_str());
words.push(Word {
first_tok,
char_start,
char_end,
});
}
(ids, blocks, words)
}
#[cfg(feature = "cli")]
fn entity_schema_ids(&self, labels: &[impl AsRef<str>]) -> (Vec<u32>, usize, Vec<usize>) {
let mut ids: Vec<u32> = Vec::new();
let push = |ids: &mut Vec<u32>, s: &str| -> usize {
let start = ids.len();
let enc = self
.tokenizer
.encode(s, false)
.expect("gliner2 tokenize element");
ids.extend_from_slice(enc.get_ids());
start
};
push(&mut ids, "(");
let p_pos = push(&mut ids, "[P]");
push(&mut ids, "entities");
push(&mut ids, "(");
let mut e_pos = Vec::with_capacity(labels.len());
for lab in labels {
e_pos.push(push(&mut ids, "[E]"));
push(&mut ids, lab.as_ref());
}
push(&mut ids, ")");
push(&mut ids, ")");
push(&mut ids, "[SEP_TEXT]");
(ids, p_pos, e_pos)
}
#[cfg(feature = "cli")]
fn word_toks(&self, text: &str) -> Vec<WordTok> {
let low = text.to_lowercase();
let mut out = Vec::new();
for m in self.splitter.find_iter(&low) {
let char_start = low[..m.start()].chars().count();
let char_end = char_start + m.as_str().chars().count();
let enc = self
.tokenizer
.encode(m.as_str(), false)
.expect("gliner2 tokenize word");
out.push(WordTok {
ids: enc.get_ids().to_vec(),
char_start,
char_end,
});
}
out
}
#[cfg(feature = "cli")]
pub fn predict_entities(
&mut self,
text: &str,
labels: &[impl AsRef<str>],
threshold: f32,
) -> Result<Vec<Ner2Entity>> {
self.predict_entities_windowed(text, labels, threshold, true)
}
#[cfg(feature = "cli")]
pub fn predict_entities_windowed(
&mut self,
text: &str,
labels: &[impl AsRef<str>],
threshold: f32,
batched: bool,
) -> Result<Vec<Ner2Entity>> {
anyhow::ensure!(!labels.is_empty(), "no entity labels given");
anyhow::ensure!(
self.count_embed.is_some(),
"counting layer {:?} is not ported — this checkpoint serves classification only \
(classify_text)",
self.counting_layer
);
let label_strs: Vec<String> = labels.iter().map(|l| l.as_ref().to_string()).collect();
let (schema_ids, p_pos, e_pos) = self.entity_schema_ids(labels);
let (orig, win_ids, win_words) = self.entity_windows(text, &schema_ids);
let t_bb = std::time::Instant::now();
let states_all: Vec<f32> = if batched {
self.backbone
.forward_hidden(&EncBatch::from_seqs(win_ids.iter().cloned()))
.context("gliner2 backbone (batched windows)")?
} else {
let mut v = Vec::new();
for ids in &win_ids {
v.extend(
self.backbone
.forward_hidden(&EncBatch::from_seqs([ids.clone()]))
.context("gliner2 backbone (per-window)")?,
);
}
v
};
let t_backbone = t_bb.elapsed();
let trace = std::env::var("OSFKB_GLINER2_TRACE").is_ok_and(|v| v != "0");
let t_heads = std::time::Instant::now();
let h = self.hidden;
let mut all: Vec<Ner2Entity> = Vec::new();
let mut tok_off = 0usize;
for (ids, wds) in win_ids.iter().zip(&win_words) {
let states = &states_all[tok_off * h..(tok_off + ids.len()) * h];
tok_off += ids.len();
let ents =
self.entities_window(states, p_pos, &e_pos, wds, &orig, &label_strs, threshold)?;
all.extend(ents);
}
if trace {
eprintln!(
"gliner2 trace: backbone {:.3}s | heads {:.3}s | windows {}",
t_backbone.as_secs_f64(),
t_heads.elapsed().as_secs_f64(),
win_ids.len(),
);
}
Ok(Self::merge_windows(all, &label_strs))
}
#[cfg(feature = "cli")]
pub fn predict_entities_batch(
&mut self,
texts: &[impl AsRef<str>],
labels: &[impl AsRef<str>],
threshold: f32,
) -> Result<Vec<Vec<Ner2Entity>>> {
anyhow::ensure!(!labels.is_empty(), "no entity labels given");
anyhow::ensure!(
self.count_embed.is_some(),
"counting layer {:?} is not ported — this checkpoint serves classification only \
(classify_text)",
self.counting_layer
);
if texts.is_empty() {
return Ok(Vec::new());
}
let label_strs: Vec<String> = labels.iter().map(|l| l.as_ref().to_string()).collect();
let (schema_ids, p_pos, e_pos) = self.entity_schema_ids(labels);
let mut origs: Vec<Vec<char>> = Vec::with_capacity(texts.len());
let mut spans: Vec<(usize, usize)> = Vec::with_capacity(texts.len()); let mut all_ids: Vec<Vec<u32>> = Vec::new();
let mut all_words: Vec<Vec<Word>> = Vec::new();
for text in texts {
let (orig, ids, wds) = self.entity_windows(text.as_ref(), &schema_ids);
let start = all_ids.len();
all_ids.extend(ids);
all_words.extend(wds);
spans.push((start, all_ids.len()));
origs.push(orig);
}
let states_all = self
.backbone
.forward_hidden(&EncBatch::from_seqs(all_ids.iter().cloned()))
.context("gliner2 backbone (batched texts)")?;
let h = self.hidden;
let mut offsets: Vec<usize> = Vec::with_capacity(all_ids.len() + 1);
let mut run = 0usize;
for ids in &all_ids {
offsets.push(run);
run += ids.len();
}
let mut out: Vec<Vec<Ner2Entity>> = Vec::with_capacity(texts.len());
for (ti, (wa, wb)) in spans.into_iter().enumerate() {
let mut found: Vec<Ner2Entity> = Vec::new();
for wi in wa..wb {
let off = offsets[wi];
let states = &states_all[off * h..(off + all_ids[wi].len()) * h];
let ents = self.entities_window(
states,
p_pos,
&e_pos,
&all_words[wi],
&origs[ti],
&label_strs,
threshold,
)?;
found.extend(ents);
}
out.push(Self::merge_windows(found, &label_strs));
}
Ok(out)
}
#[cfg(feature = "cli")]
fn entity_windows(
&mut self,
text: &str,
schema_ids: &[u32],
) -> (Vec<char>, Vec<Vec<u32>>, Vec<Vec<Word>>) {
let text = normalize_text(text);
let orig: Vec<char> = text.chars().collect();
let words = self.word_toks(&text);
let budget = self
.backbone
.config()
.max_pos
.saturating_sub(schema_ids.len())
.saturating_sub(4)
.max(1);
let overlap = self.max_width.max(1);
let mut windows: Vec<(usize, usize)> = Vec::new();
let mut a = 0usize;
while a < words.len() {
let mut j = a;
let mut toks = 0usize;
while j < words.len() && (j == a || toks + words[j].ids.len() <= budget) {
toks += words[j].ids.len();
j += 1;
}
windows.push((a, j));
if j >= words.len() {
break;
}
a = j.saturating_sub(overlap).max(a + 1);
}
if windows.is_empty() {
windows.push((0, 0)); }
let mut win_ids: Vec<Vec<u32>> = Vec::with_capacity(windows.len());
let mut win_words: Vec<Vec<Word>> = Vec::with_capacity(windows.len());
for (wa, wb) in windows {
let mut ids = schema_ids.to_vec();
let mut wds: Vec<Word> = Vec::with_capacity(wb - wa);
for w in &words[wa..wb] {
let first_tok = ids.len();
ids.extend_from_slice(&w.ids);
wds.push(Word {
first_tok,
char_start: w.char_start,
char_end: w.char_end,
});
}
win_ids.push(ids);
win_words.push(wds);
}
(orig, win_ids, win_words)
}
#[cfg(feature = "cli")]
fn merge_windows(all: Vec<Ner2Entity>, label_strs: &[String]) -> Vec<Ner2Entity> {
let mut out: Vec<Ner2Entity> = Vec::new();
for lab in label_strs {
let mut spans: Vec<Ner2Entity> =
all.iter().filter(|e| &e.label == lab).cloned().collect();
spans.sort_by(|a, b| {
b.confidence
.partial_cmp(&a.confidence)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut selected: Vec<Ner2Entity> = Vec::new();
for sp in spans {
let ov = selected
.iter()
.any(|s| !(sp.end <= s.start || sp.start >= s.end));
if !ov {
selected.push(sp);
}
}
out.extend(selected);
}
out
}
#[cfg(feature = "cli")]
#[allow(clippy::too_many_arguments)]
fn entities_window(
&mut self,
states: &[f32],
p_pos: usize,
e_pos: &[usize],
words: &[Word],
orig: &[char],
labels: &[String],
threshold: f32,
) -> Result<Vec<Ner2Entity>> {
let l = words.len();
if l == 0 {
return Ok(Vec::new());
}
let count_embed = self
.count_embed
.as_ref()
.expect("count_embed presence checked by predict_entities");
let h = self.hidden;
let mut token_embs = vec![0f32; l * h];
for (wi, w) in words.iter().enumerate() {
token_embs[wi * h..(wi + 1) * h]
.copy_from_slice(&states[w.first_tok * h..(w.first_tok + 1) * h]);
}
let span_rep = self.span_rep(&token_embs, l);
let count_logits = self.count_pred.forward(&states[p_pos * h..(p_pos + 1) * h]);
let pred_count = argmax(&count_logits);
if pred_count == 0 {
return Ok(Vec::new());
}
let n = e_pos.len();
let mut fields = vec![0f32; n * h];
for (i, &ep) in e_pos.iter().enumerate() {
fields[i * h..(i + 1) * h].copy_from_slice(&states[ep * h..(ep + 1) * h]);
}
let struct_proj = count_embed.forward(&fields, pred_count);
let bp = struct_proj.len() / h; let scorer = Linear {
w: struct_proj,
b: vec![0.0; bp],
n: bp,
k: h,
..Default::default()
};
let mut flat = scorer.forward(&span_rep); for v in flat.iter_mut() {
*v = sigmoid(*v);
}
let kmax = self.max_width;
let mut out = Vec::new();
for (ty, lab) in labels.iter().enumerate() {
let mut spans: Vec<(String, f32, usize, usize)> = Vec::new();
for li in 0..l {
for ki in 0..kmax {
if li + ki >= l {
break;
}
let s = flat[(li * kmax + ki) * bp + ty]; if s >= threshold {
let cs = words[li].char_start;
let ce = words[li + ki].char_end;
let txt: String = orig
.get(cs..ce)
.map(|c| c.iter().collect::<String>())
.unwrap_or_default();
spans.push((txt.trim().to_string(), s, cs, ce));
}
}
}
spans.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let mut selected: Vec<(String, f32, usize, usize)> = Vec::new();
for sp in spans {
let overlap = selected.iter().any(|s| !(sp.3 <= s.2 || sp.2 >= s.3));
if !overlap {
selected.push(sp);
}
}
for (txt, conf, cs, ce) in selected {
out.push(Ner2Entity {
text: txt,
label: lab.clone(),
start: cs,
end: ce,
confidence: conf,
});
}
}
Ok(out)
}
#[cfg(feature = "cli")]
fn tokenize_classification(&self, text: &str, tasks: &[ClsTask]) -> (Vec<u32>, Vec<ClsBlock>) {
let (mut ids, blocks) = self.classify_schema_ids(tasks);
let low = text.to_lowercase();
let push = |ids: &mut Vec<u32>, s: &str| {
let enc = self
.tokenizer
.encode(s, false)
.expect("gliner2 tokenize element");
ids.extend_from_slice(enc.get_ids());
};
for m in self.splitter.find_iter(&low) {
push(&mut ids, m.as_str());
}
(ids, blocks)
}
#[cfg(feature = "cli")]
fn classify_schema_ids(&self, tasks: &[ClsTask]) -> (Vec<u32>, Vec<ClsBlock>) {
let mut ids: Vec<u32> = Vec::new();
let mut blocks: Vec<ClsBlock> = Vec::new();
let push = |ids: &mut Vec<u32>, s: &str| -> usize {
let start = ids.len();
let enc = self
.tokenizer
.encode(s, false)
.expect("gliner2 tokenize element");
ids.extend_from_slice(enc.get_ids());
start
};
for (i, task) in tasks.iter().enumerate() {
if i > 0 {
push(&mut ids, "[SEP_STRUCT]");
}
let mut prompt_str = match &task.prompt {
Some(p) => format!("{}: {p}", task.name),
None => task.name.clone(),
};
for (label, desc) in &task.label_descriptions {
if task.labels.iter().any(|l| l == label) {
prompt_str.push_str(&format!(" [DESCRIPTION] {label}: {desc}"));
}
}
for (inp, out) in &task.examples {
if task.labels.iter().any(|l| l == out) {
prompt_str.push_str(&format!(" [EXAMPLE] {inp} [OUTPUT] {out}"));
}
}
push(&mut ids, "(");
let p_pos = push(&mut ids, "[P]");
push(&mut ids, &prompt_str);
push(&mut ids, "(");
let mut l_pos = Vec::with_capacity(task.labels.len());
for label in &task.labels {
l_pos.push(push(&mut ids, "[L]"));
push(&mut ids, label);
}
push(&mut ids, ")");
push(&mut ids, ")");
blocks.push(ClsBlock { p_pos, l_pos });
}
push(&mut ids, "[SEP_TEXT]");
(ids, blocks)
}
#[cfg(feature = "cli")]
pub fn classify_text(&mut self, text: &str, tasks: &[ClsTask]) -> Result<Vec<ClsResult>> {
anyhow::ensure!(!tasks.is_empty(), "no classification tasks given");
for task in tasks {
anyhow::ensure!(
!task.labels.is_empty(),
"task {:?} has no labels",
task.name
);
}
let text = normalize_text(text);
let (schema_ids, blocks) = self.classify_schema_ids(tasks);
let words = self.word_toks(&text);
let max_pos = self.backbone.config().max_pos;
let fit = max_pos
.saturating_sub(schema_ids.len())
.saturating_sub(4)
.max(1);
let budget = fit.min(128);
let overlap = (budget / 2).max(1);
let mut windows: Vec<(usize, usize)> = Vec::new();
let mut a = 0usize;
while a < words.len() {
let mut j = a;
let mut toks = 0usize;
while j < words.len() && (j == a || toks + words[j].ids.len() <= budget) {
toks += words[j].ids.len();
j += 1;
}
windows.push((a, j));
if j >= words.len() {
break;
}
a = j.saturating_sub(overlap).max(a + 1);
}
if windows.is_empty() {
windows.push((0, 0));
}
let mut max_scores: Vec<Vec<f32>> =
tasks.iter().map(|t| vec![0f32; t.labels.len()]).collect();
let mut chosen_any: Vec<Vec<bool>> =
tasks.iter().map(|t| vec![false; t.labels.len()]).collect();
for (wa, wb) in windows {
let mut ids = schema_ids.clone();
for w in &words[wa..wb] {
ids.extend_from_slice(&w.ids);
}
let per_task = self.classify_window(&ids, &blocks, tasks)?;
for (ti, task) in tasks.iter().enumerate() {
let probs = &per_task[ti];
for (li, &p) in probs.iter().enumerate() {
max_scores[ti][li] = max_scores[ti][li].max(p);
}
if task.multi_label {
let mut any = false;
for (li, &p) in probs.iter().enumerate() {
if p >= task.cls_threshold {
chosen_any[ti][li] = true;
any = true;
}
}
if !any {
chosen_any[ti][argmax(probs)] = true;
}
} else {
chosen_any[ti][argmax(probs)] = true;
}
}
}
let mut results = Vec::with_capacity(tasks.len());
for (ti, task) in tasks.iter().enumerate() {
let scores: Vec<ClsLabelScore> = task
.labels
.iter()
.enumerate()
.map(|(li, l)| ClsLabelScore {
label: l.clone(),
score: max_scores[ti][li],
})
.collect();
let chosen: Vec<ClsLabelScore> = (0..task.labels.len())
.filter(|&li| chosen_any[ti][li])
.map(|li| scores[li].clone())
.collect();
results.push(ClsResult {
task: task.name.clone(),
scores,
chosen,
});
}
Ok(results)
}
#[cfg(feature = "cli")]
fn classify_window(
&mut self,
input_ids: &[u32],
blocks: &[ClsBlock],
tasks: &[ClsTask],
) -> Result<Vec<Vec<f32>>> {
let states = self
.backbone
.forward_hidden(&EncBatch::from_seqs([input_ids.to_vec()]))
.context("gliner2 backbone")?;
let h = self.hidden;
let mut out = Vec::with_capacity(tasks.len());
for (task, blk) in tasks.iter().zip(blocks) {
let n = blk.l_pos.len();
let mut cls_embeds = vec![0f32; n * h];
for (j, &lp) in blk.l_pos.iter().enumerate() {
cls_embeds[j * h..(j + 1) * h].copy_from_slice(&states[lp * h..(lp + 1) * h]);
}
let logits = self.classifier.forward(&cls_embeds);
let sigmoid_act = match task.class_act {
ClsActivation::Sigmoid => true,
ClsActivation::Softmax => false,
ClsActivation::Auto => task.multi_label,
};
let probs: Vec<f32> = if sigmoid_act {
logits.iter().map(|&v| sigmoid(v)).collect()
} else {
softmax(&logits)
};
out.push(probs);
}
Ok(out)
}
#[cfg(feature = "cli")]
pub fn classify_debug(
&mut self,
text: &str,
tasks: &[ClsTask],
) -> Result<(Vec<ClsResult>, ClsIntermediates)> {
anyhow::ensure!(!tasks.is_empty(), "no classification tasks given");
for task in tasks {
anyhow::ensure!(
!task.labels.is_empty(),
"task {:?} has no labels",
task.name
);
}
let text = normalize_text(text);
let (input_ids, blocks) = self.tokenize_classification(text.as_str(), tasks);
let states = self
.backbone
.forward_hidden(&EncBatch::from_seqs([input_ids.clone()]))
.context("gliner2 backbone")?;
let h = self.hidden;
let mut results = Vec::with_capacity(tasks.len());
let mut dbg: Option<(Vec<f32>, Vec<f32>, Vec<f32>)> = None;
for (task, blk) in tasks.iter().zip(&blocks) {
let n = blk.l_pos.len();
let mut cls_embeds = vec![0f32; n * h];
for (j, &lp) in blk.l_pos.iter().enumerate() {
cls_embeds[j * h..(j + 1) * h].copy_from_slice(&states[lp * h..(lp + 1) * h]);
}
let logits = self.classifier.forward(&cls_embeds); let sigmoid_act = match task.class_act {
ClsActivation::Sigmoid => true,
ClsActivation::Softmax => false,
ClsActivation::Auto => task.multi_label,
};
let probs: Vec<f32> = if sigmoid_act {
logits.iter().map(|&v| sigmoid(v)).collect()
} else {
softmax(&logits)
};
let scores: Vec<ClsLabelScore> = task
.labels
.iter()
.zip(&probs)
.map(|(l, &p)| ClsLabelScore {
label: l.clone(),
score: p,
})
.collect();
let chosen: Vec<ClsLabelScore> = if task.multi_label {
let picked: Vec<ClsLabelScore> = scores
.iter()
.filter(|s| s.score >= task.cls_threshold)
.cloned()
.collect();
if picked.is_empty() {
vec![scores[argmax(&probs)].clone()]
} else {
picked
}
} else {
vec![scores[argmax(&probs)].clone()]
};
if dbg.is_none() {
let mut block0 = vec![0f32; (1 + n) * h];
block0[0..h].copy_from_slice(&states[blk.p_pos * h..(blk.p_pos + 1) * h]);
block0[h..].copy_from_slice(&cls_embeds);
dbg = Some((block0, logits.clone(), probs.clone()));
}
results.push(ClsResult {
task: task.name.clone(),
scores,
chosen,
});
}
let (schema_block0, logits0, probs0) = dbg.unwrap_or_default();
let inter = ClsIntermediates {
input_ids,
last_hidden_state: states,
schema_block0,
logits0,
probs0,
};
Ok((results, inter))
}
}
fn normalize_text(text: &str) -> String {
if text.is_empty() {
return ".".to_string();
}
if text.ends_with(['.', '!', '?']) {
text.to_string()
} else {
format!("{text}.")
}
}
fn argmax(v: &[f32]) -> usize {
let mut best = 0usize;
let mut bv = f32::NEG_INFINITY;
for (i, &x) in v.iter().enumerate() {
if x > bv {
bv = x;
best = i;
}
}
best
}
#[allow(clippy::too_many_arguments)]
fn find_span(
scores: &[f32],
b: usize,
p: usize,
m: usize,
l: usize,
kmax: usize,
threshold: f32,
words: &[Word],
orig: &[char],
) -> Option<RelEnd> {
for li in 0..l {
for ki in 0..kmax {
if li + ki >= l {
break; }
let s = scores[((b * m + p) * l + li) * kmax + ki];
if s >= threshold {
let cs = words[li].char_start;
let ce = words[li + ki].char_end;
let text: String = orig
.get(cs..ce)
.map(|c| c.iter().collect())
.unwrap_or_default();
return Some(RelEnd {
text: text.trim().to_string(),
start: cs,
end: ce,
confidence: s,
});
}
}
}
None
}