use cera::kv_cache::KvCacheConfig;
use cera::{
BackendPreference, CeraEngine, EngineConfig, FinishReason, GenerateOpts, ModalitySink,
SessionConfig,
};
struct NullSink;
impl ModalitySink for NullSink {
fn on_done(&mut self, _reason: FinishReason) {}
}
fn dump_thread_affinity(label: &str) {
use std::collections::BTreeMap;
let mut counts: BTreeMap<(String, String), usize> = BTreeMap::new();
let Ok(entries) = std::fs::read_dir("/proc/self/task") else {
println!("{label}: /proc/self/task unavailable");
return;
};
for entry in entries.flatten() {
let dir = entry.path();
let comm = std::fs::read_to_string(dir.join("comm"))
.unwrap_or_default()
.trim()
.to_string();
let mask = std::fs::read_to_string(dir.join("status"))
.unwrap_or_default()
.lines()
.find(|l| l.starts_with("Cpus_allowed_list:"))
.and_then(|l| l.split_whitespace().nth(1).map(str::to_string))
.unwrap_or_default();
*counts.entry((comm, mask)).or_default() += 1;
}
println!("{label}:");
for ((comm, mask), n) in counts {
println!(" {n:>2} x {comm:<24} cpus_allowed={mask}");
}
}
const USAGE: &str = "usage: embedder_path <model.gguf> [prompt_tokens] [max_tokens] [runs]";
fn arg<T: std::str::FromStr>(v: Option<String>, name: &str, default: T) -> Result<T, String> {
match v {
None => Ok(default),
Some(s) => s
.parse()
.map_err(|_| format!("{name}: expected a number, got `{s}`\n{USAGE}")),
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let model = args.next().ok_or(USAGE)?;
let prompt_tokens: usize = arg(args.next(), "prompt_tokens", 512)?;
let max_tokens: u32 = arg(args.next(), "max_tokens", 16)?;
let runs: usize = arg(args.next(), "runs", 6)?;
if prompt_tokens == 0 || max_tokens == 0 || runs == 0 {
return Err(format!("prompt_tokens, max_tokens and runs must all be >= 1\n{USAGE}").into());
}
let configure = std::env::var("CERA_EMBEDDER_CONFIGURE").is_ok_and(|v| {
!matches!(
v.trim().to_ascii_lowercase().as_str(),
"" | "0" | "false" | "off"
)
});
if configure {
let n = cera::backend::cpu::configure_thread_pool();
println!("called configure_thread_pool() -> {n} threads");
} else {
println!("did NOT call configure_thread_pool() (embedder path)");
}
#[allow(clippy::needless_update)]
let engine_cfg = EngineConfig {
context_size: 4096,
backend: BackendPreference::Cpu,
..Default::default()
};
let engine = CeraEngine::from_path(&model, engine_cfg)?;
let cache_on = std::env::var("CERA_EMBEDDER_CACHE").is_ok_and(|v| {
!matches!(
v.trim().to_ascii_lowercase().as_str(),
"" | "0" | "false" | "off"
)
});
println!("prefix cache: {}", if cache_on { "default" } else { "off" });
if !cache_on {
engine.configure_cache(KvCacheConfig {
max_warm_entries: 0,
max_warm_bytes: 0,
max_cold_bytes: 0,
cache_dir: None,
..Default::default()
});
}
let bos = engine.tokenizer().bos_token();
let mut tokens: Vec<u32> = Vec::with_capacity(prompt_tokens);
if let Some(b) = bos {
tokens.push(b);
}
let filler = engine
.tokenizer()
.encode("the quick brown fox jumps over a lazy dog ");
if filler.is_empty() {
return Err("tokenizer encoded the filler string to nothing; cannot build a prompt".into());
}
while tokens.len() < prompt_tokens {
tokens.push(filler[tokens.len() % filler.len()]);
}
let opts = GenerateOpts {
max_tokens,
temperature: 0.0,
top_k: 1,
ignore_eos: true,
..Default::default()
};
for i in 0..runs {
let mut session = engine.new_session(SessionConfig::default())?;
session.append_tokens(&tokens)?;
let summary = session.generate(&opts, &mut NullSink)?;
let pf = if summary.prompt_eval_ms > 0 {
summary.prompt_eval_tokens as f64 * 1000.0 / summary.prompt_eval_ms as f64
} else {
f64::NAN
};
let dc = if summary.decode_ms > 0 {
summary.tokens_generated as f64 * 1000.0 / summary.decode_ms as f64
} else {
f64::NAN
};
println!(
"run {}/{}: prefill={pf:.0} decode={dc:.1} tok/s",
i + 1,
runs
);
if i == 0 {
dump_thread_affinity("thread affinity after first generate");
}
}
Ok(())
}