#[cfg(feature = "mkl")]
extern crate intel_mkl_src;
#[cfg(feature = "accelerate")]
extern crate accelerate_src;
use std::fmt::Display;
use std::path::PathBuf;
use anyhow::{Error as E, Result, bail};
use clap::{ArgGroup, Parser, ValueEnum};
use hf_hub::{Repo, RepoType, api::sync::Api};
use tokenizers::Tokenizer;
use fuel::lazy_debertav2::{
DebertaV2Config, DebertaV2NERModel, DebertaV2NERWeights, DebertaV2SeqClassificationModel,
DebertaV2SeqClassificationWeights, Id2Label, NERItem, TextClassificationItem,
};
#[derive(Parser, Debug, Clone, ValueEnum)]
enum ArgsTask {
Ner,
TextClassification,
FillMask,
}
impl Display for ArgsTask {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ArgsTask::Ner => write!(f, "ner"),
ArgsTask::TextClassification => write!(f, "text-classification"),
ArgsTask::FillMask => write!(f, "fill-mask"),
}
}
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
#[command(group(ArgGroup::new("model_source")
.required(true)
.args(&["model_id", "model_path"])))]
struct Args {
#[arg(long)]
cpu: bool,
#[arg(long, default_value_t = ArgsTask::Ner)]
task: ArgsTask,
#[arg(long)]
model_id: Option<String>,
#[arg(long, default_value = "main")]
revision: String,
#[arg(long)]
model_path: Option<PathBuf>,
#[arg(long = "sentence", name = "sentences", num_args = 1..)]
sentences: Vec<String>,
#[arg(long)]
id2label: Option<String>,
}
fn resolve_files(args: &Args) -> Result<(PathBuf, PathBuf, PathBuf)> {
match &args.model_path {
Some(base) => {
if !base.is_dir() {
bail!("Model path {} is not a directory.", base.display());
}
let config = base.join("config.json");
let tokenizer = base.join("tokenizer.json");
let weights = base.join("model.safetensors");
Ok((config, tokenizer, weights))
}
None => {
let model_id = args
.model_id
.as_ref()
.expect("clap group enforces model_id when model_path is unset")
.clone();
let repo = Repo::with_revision(model_id, RepoType::Model, args.revision.clone());
let api = Api::new()?;
let api = api.repo(repo);
let config = api.get("config.json")?;
let tokenizer = api.get("tokenizer.json")?;
let weights = api.get("model.safetensors")?;
Ok((config, tokenizer, weights))
}
}
}
fn tokenize(tokenizer: &Tokenizer, sentence: &str) -> Result<(Vec<u32>, Vec<String>, Vec<u32>)> {
let mut tk = tokenizer.clone();
let tk = tk
.with_padding(None)
.with_truncation(None)
.map_err(E::msg)?;
let enc = tk.encode(sentence, true).map_err(E::msg)?;
let ids = enc.get_ids().to_vec();
let tokens = enc.get_tokens().to_vec();
let special_mask = enc.get_special_tokens_mask().to_vec();
Ok((ids, tokens, special_mask))
}
fn ner_per_token_pred(logits_flat: &[f32], seq: usize, num_labels: usize) -> Vec<(usize, f32)> {
let mut out = Vec::with_capacity(seq);
for t in 0..seq {
let row = &logits_flat[t * num_labels..(t + 1) * num_labels];
let mut max_logit = f32::NEG_INFINITY;
let mut max_idx = 0_usize;
for (i, &v) in row.iter().enumerate() {
if v > max_logit {
max_logit = v;
max_idx = i;
}
}
let mut denom = 0.0_f32;
for &v in row {
denom += (v - max_logit).exp();
}
let prob = 1.0_f32 / denom.max(1e-30);
out.push((max_idx, prob));
}
out
}
fn softmax_row(logits_flat: &[f32]) -> Vec<f32> {
let mut max_logit = f32::NEG_INFINITY;
for &v in logits_flat {
if v > max_logit {
max_logit = v;
}
}
let mut exps: Vec<f32> = logits_flat.iter().map(|&v| (v - max_logit).exp()).collect();
let denom: f32 = exps.iter().sum::<f32>().max(1e-30);
for v in &mut exps {
*v /= denom;
}
exps
}
fn resolve_id2label(args: &Args, cfg: &DebertaV2Config) -> Result<Id2Label> {
if let Some(s) = &args.id2label {
let parsed: std::collections::HashMap<String, String> =
serde_json::from_str(s).map_err(|e| E::msg(format!("parsing --id2label: {e}")))?;
let mut out: Id2Label = std::collections::HashMap::new();
for (k, v) in parsed {
let id: u32 = k
.parse()
.map_err(|e| E::msg(format!("--id2label key {k:?} is not a u32: {e}")))?;
out.insert(id, v);
}
Ok(out)
} else if let Some(map) = &cfg.id2label {
Ok(map.clone())
} else {
bail!("`id2label` not found in the model config and not passed via --id2label")
}
}
fn run_ner(
cfg: &DebertaV2Config,
weights_path: &std::path::Path,
tokenizer: &Tokenizer,
sentences: &[String],
id2label: &Id2Label,
) -> Result<Vec<Vec<NERItem>>> {
let num_labels = id2label.len();
let st = unsafe { fuel::safetensors::MmapedSafetensors::multi(&[weights_path]) }
.map_err(|e| E::msg(format!("mmap safetensors: {e}")))?;
let weights = DebertaV2NERWeights::load_from_mmapped(&st, cfg, num_labels)
.map_err(|e| E::msg(format!("load NER weights: {e}")))?;
let model = DebertaV2NERModel::new(cfg.clone(), weights, num_labels);
let mut results: Vec<Vec<NERItem>> = Vec::with_capacity(sentences.len());
for sentence in sentences {
let (ids, tokens, special_mask) = tokenize(tokenizer, sentence)?;
let seq = ids.len();
let logits = model
.forward(&ids, None, None)
.map_err(|e| E::msg(format!("NER forward: {e}")))?;
let flat = logits.realize_f32();
let preds = ner_per_token_pred(&flat, seq, num_labels);
let mut row: Vec<NERItem> = Vec::new();
let mut t = 0;
while t < seq {
if special_mask[t] == 1 {
t += 1;
continue;
}
let (label_idx, score) = preds[t];
let label = match id2label.get(&(label_idx as u32)) {
Some(s) => s.clone(),
None => {
t += 1;
continue;
}
};
if label == "O" {
t += 1;
continue;
}
let start_t = t;
let mut word = tokens[t].clone();
let mut end = t + 1;
while end < seq {
if special_mask[end] == 1 {
break;
}
let (next_idx, _) = preds[end];
if next_idx != label_idx {
break;
}
word.push_str(&tokens[end]);
end += 1;
}
row.push(NERItem {
entity: label,
score,
word,
index: start_t,
});
t = end;
}
results.push(row);
}
Ok(results)
}
fn run_text_classification(
cfg: &DebertaV2Config,
weights_path: &std::path::Path,
tokenizer: &Tokenizer,
sentences: &[String],
id2label: &Id2Label,
) -> Result<Vec<TextClassificationItem>> {
let num_labels = id2label.len();
let st = unsafe { fuel::safetensors::MmapedSafetensors::multi(&[weights_path]) }
.map_err(|e| E::msg(format!("mmap safetensors: {e}")))?;
let weights = DebertaV2SeqClassificationWeights::load_from_mmapped(&st, cfg, num_labels)
.map_err(|e| E::msg(format!("load seq-classification weights: {e}")))?;
let model = DebertaV2SeqClassificationModel::new(cfg.clone(), weights, num_labels);
let mut results: Vec<TextClassificationItem> = Vec::with_capacity(sentences.len());
for sentence in sentences {
let (ids, _tokens, _special_mask) = tokenize(tokenizer, sentence)?;
let logits = model
.forward(&ids, None, None)
.map_err(|e| E::msg(format!("seq-classification forward: {e}")))?;
let flat = logits.realize_f32();
let probs = softmax_row(&flat);
let (best_idx, &best_prob) = probs
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.total_cmp(b))
.expect("non-empty num_labels");
let label = id2label
.get(&(best_idx as u32))
.cloned()
.unwrap_or_else(|| format!("LABEL_{best_idx}"));
results.push(TextClassificationItem {
label,
score: best_prob,
});
}
Ok(results)
}
fn main() -> Result<()> {
let args = Args::parse();
let _device = fuel_examples::device(args.cpu)?;
if args.sentences.is_empty() {
bail!("At least one --sentence is required");
}
let load_t0 = std::time::Instant::now();
let (config_path, tokenizer_path, weights_path) = resolve_files(&args)?;
let config_json = std::fs::read_to_string(&config_path)?;
let cfg = DebertaV2Config::from_hf_json_str(&config_json)
.map_err(|e| E::msg(format!("parsing config.json: {e}")))?;
let tokenizer = Tokenizer::from_file(&tokenizer_path).map_err(E::msg)?;
println!("Loaded config + tokenizer in {:?}", load_t0.elapsed());
match args.task {
ArgsTask::FillMask => {
bail!(
"`--task fill-mask` is not yet wired in the lazy DeBERTa-v2 port \
(no DebertaV2ForMaskedLM head). Pick `--task ner` or \
`--task text-classification`, or open a follow-up to ship the \
MLM head in `fuel::lazy_debertav2`."
);
}
ArgsTask::Ner => {
let id2label = resolve_id2label(&args, &cfg)?;
let infer_t0 = std::time::Instant::now();
let results = run_ner(&cfg, &weights_path, &tokenizer, &args.sentences, &id2label)?;
println!("Inferenced inputs in {:?}", infer_t0.elapsed());
println!("\n{results:?}");
}
ArgsTask::TextClassification => {
let id2label = resolve_id2label(&args, &cfg)?;
let infer_t0 = std::time::Instant::now();
let results = run_text_classification(
&cfg,
&weights_path,
&tokenizer,
&args.sentences,
&id2label,
)?;
println!("Inferenced inputs in {:?}", infer_t0.elapsed());
println!("\n{results:?}");
}
}
Ok(())
}