use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{Error as E, Result, anyhow};
use clap::Parser;
use fuel::Shape;
use fuel::lazy::{Tensor, load_tensor_as_f32, load_transposed_matrix};
use fuel::lazy_bert::{BertConfig, BertModel, BertWeights};
use hf_hub::{Repo, RepoType, api::sync::Api};
use tokenizers::Tokenizer;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(long)]
cpu: bool,
#[arg(long)]
tracing: bool,
#[arg(long)]
model_id: Option<String>,
#[arg(long, default_value = "main")]
revision: String,
#[arg(long)]
tokenizer_file: Option<String>,
#[arg(long)]
weight_files: Option<String>,
#[arg(long)]
config_file: Option<String>,
#[arg(long)]
prompt: Option<String>,
}
struct MlmHead {
transform_dense_w: Arc<[f32]>, transform_dense_b: Arc<[f32]>, transform_ln_gain: Arc<[f32]>, transform_ln_bias: Arc<[f32]>, decoder_w: Arc<[f32]>, decoder_b: Arc<[f32]>, }
fn load_mlm_head(st: &fuel::safetensors::MmapedSafetensors, cfg: &BertConfig) -> Result<MlmHead> {
let h = cfg.hidden_size;
let v = cfg.vocab_size;
let dense_w = load_transposed_matrix(st, "cls.predictions.transform.dense.weight", h, h)
.map_err(|e| anyhow!("{e}"))?;
let dense_b = load_tensor_as_f32(st, "cls.predictions.transform.dense.bias")
.map_err(|e| anyhow!("{e}"))?;
let ln_gain = load_tensor_as_f32(st, "cls.predictions.transform.LayerNorm.weight")
.map_err(|e| anyhow!("{e}"))?;
let ln_bias = load_tensor_as_f32(st, "cls.predictions.transform.LayerNorm.bias")
.map_err(|e| anyhow!("{e}"))?;
let decoder_w = load_transposed_matrix(st, "cls.predictions.decoder.weight", v, h)
.map_err(|e| anyhow!("{e}"))?;
let decoder_b =
load_tensor_as_f32(st, "cls.predictions.decoder.bias").map_err(|e| anyhow!("{e}"))?;
Ok(MlmHead {
transform_dense_w: Arc::from(dense_w),
transform_dense_b: Arc::from(dense_b),
transform_ln_gain: Arc::from(ln_gain),
transform_ln_bias: Arc::from(ln_bias),
decoder_w: Arc::from(decoder_w),
decoder_b: Arc::from(decoder_b),
})
}
fn apply_mlm_head(hidden: &Tensor, mlm: &MlmHead, cfg: &BertConfig) -> Result<Tensor> {
let h = cfg.hidden_size;
let v = cfg.vocab_size;
let dense_t = hidden.const_f32_like(
Arc::clone(&mlm.transform_dense_w),
Shape::from_dims(&[h, h]),
)?;
let x = hidden.matmul(&dense_t)?;
let bias_t = hidden
.const_f32_like(Arc::clone(&mlm.transform_dense_b), Shape::from_dims(&[h]))?
.reshape(Shape::from_dims(&[1, 1, h]))?;
let x = x.broadcast_add(&bias_t)?;
let x = x.gelu();
let x = x.layer_norm_affine(
Arc::clone(&mlm.transform_ln_gain),
Arc::clone(&mlm.transform_ln_bias),
cfg.layer_norm_eps,
)?;
let dec_t = hidden.const_f32_like(Arc::clone(&mlm.decoder_w), Shape::from_dims(&[h, v]))?;
let logits = x.matmul(&dec_t)?;
let dec_b = hidden
.const_f32_like(Arc::clone(&mlm.decoder_b), Shape::from_dims(&[v]))?
.reshape(Shape::from_dims(&[1, 1, v]))?;
Ok(logits.broadcast_add(&dec_b)?)
}
fn main() -> Result<()> {
let args = Args::parse();
let api = Api::new()?;
let model_id = args
.model_id
.clone()
.unwrap_or_else(|| "prithivida/Splade_PP_en_v1".to_string());
let repo = api.repo(Repo::with_revision(
model_id,
RepoType::Model,
args.revision.clone(),
));
let tokenizer_filename = match args.tokenizer_file.clone() {
Some(file) => std::path::PathBuf::from(file),
None => repo.get("tokenizer.json")?,
};
let config_filename = match args.config_file.clone() {
Some(file) => std::path::PathBuf::from(file),
None => repo.get("config.json")?,
};
let weights_filename = match args.weight_files.clone() {
Some(files) => PathBuf::from(files),
None => repo.get("model.safetensors")?,
};
let config_str = std::fs::read_to_string(config_filename)?;
let config: BertConfig =
BertConfig::from_hf_json_str(&config_str).map_err(|e| anyhow!("{e}"))?;
let tokenizer = Tokenizer::from_file(tokenizer_filename).map_err(E::msg)?;
let _device = fuel_examples::device(args.cpu)?;
let st = unsafe { fuel::safetensors::MmapedSafetensors::multi(&[weights_filename]) }
.map_err(|e| anyhow!("mmap safetensors: {e}"))?;
let bert_weights = BertWeights::load_from_mmapped(&st, &config).map_err(|e| anyhow!("{e}"))?;
let mlm = load_mlm_head(&st, &config)?;
let model = BertModel::new(config.clone(), bert_weights);
let prompt = args
.prompt
.unwrap_or_else(|| "The cat sits outside, listening for the postman.".to_string());
let encoded = tokenizer.encode(prompt.as_str(), true).map_err(E::msg)?;
let tokens: Vec<u32> = encoded.get_ids().to_vec();
let hidden = model.forward(&tokens).map_err(|e| anyhow!("{e}"))?;
let logits = apply_mlm_head(&hidden, &mlm, &config)?;
let post_act = logits.relu().add_scalar(1.0).log();
let pooled = post_act.max_dim(1_usize)?;
let vec = pooled.realize_f32();
let mut idxs: Vec<usize> = (0..vec.len()).filter(|&i| vec[i] != 0.0).collect();
idxs.sort_unstable_by(|&a, &b| vec[b].partial_cmp(&vec[a]).unwrap());
let top_k = 20.min(idxs.len());
let top: Vec<u32> = idxs[..top_k].iter().map(|&i| i as u32).collect();
let decoded = tokenizer
.decode(top.as_slice(), true)
.unwrap_or_else(|_| "<decode failed>".into());
println!("top {top_k} SPLADE terms for: {prompt}");
println!(" {decoded}");
println!(
" values: {:?}",
idxs[..top_k].iter().map(|&i| vec[i]).collect::<Vec<_>>(),
);
Ok(())
}