use std::path::{Path, PathBuf};
use crate::Device;
#[derive(Debug, Clone)]
pub struct SessionSnapshot {
pub kv: crate::kv_cache::LayerKvCache,
pub tokens: Vec<u32>,
}
pub trait LmRunner: Send {
fn family(&self) -> &'static str;
fn vocab_size(&self) -> usize;
fn predict_logits(&mut self, prompt_ids: &[u32]) -> anyhow::Result<Vec<f32>>;
fn generate(
&mut self,
prompt_ids: &[u32],
n_new: usize,
on_token: &mut dyn FnMut(u32) -> bool,
) -> anyhow::Result<Vec<u32>> {
let mut context: Vec<u32> = prompt_ids.to_vec();
let mut produced: Vec<u32> = Vec::with_capacity(n_new);
for _ in 0..n_new {
let logits = self.predict_logits(&context)?;
let next = argmax_u32(&logits);
produced.push(next);
let cont = on_token(next);
context.push(next);
if !cont {
break;
}
}
Ok(produced)
}
fn prefill_logits(&mut self, _prompt_ids: &[u32]) -> anyhow::Result<Vec<f32>> {
Err(anyhow::anyhow!(
"{}: host-driven logits decode (prefill_logits) unsupported — wrap \
this runner in `HostDriven` for a correct, slower version, or \
forward the family's own cached pair",
self.family()
))
}
fn decode_logits(&mut self, _token: u32) -> anyhow::Result<Vec<f32>> {
Err(anyhow::anyhow!(
"{}: host-driven logits decode (decode_logits) unsupported — wrap \
this runner in `HostDriven` for a correct, slower version, or \
forward the family's own cached pair",
self.family()
))
}
fn set_multimodal_phase_callback(
&mut self,
_cb: Option<std::sync::Arc<dyn Fn(&str) + Send + Sync>>,
) {
}
fn prefill_logits_reusing(
&mut self,
prompt: &[u32],
_snap: &SessionSnapshot,
_reuse_len: usize,
) -> anyhow::Result<Vec<f32>> {
self.prefill_logits(prompt)
}
fn export_session(&self) -> Option<SessionSnapshot> {
None
}
fn restore_session(&mut self, _snap: &SessionSnapshot) -> bool {
false
}
fn kv_store_stats(&self) -> Option<(usize, usize, usize)> {
None
}
fn supports_multimodal(&self) -> bool {
false
}
fn generate_multimodal(
&mut self,
_prompt: &str,
_rgb: &[u8],
_img_w: usize,
_img_h: usize,
_tokenizer: Option<&Path>,
_n_new: usize,
_on_token: &mut dyn FnMut(u32) -> bool,
) -> anyhow::Result<Vec<u32>> {
Err(anyhow::anyhow!(
"this LmRunner does not support multimodal generation"
))
}
}
impl LmRunner for Box<dyn LmRunner> {
fn family(&self) -> &'static str {
(**self).family()
}
fn vocab_size(&self) -> usize {
(**self).vocab_size()
}
fn predict_logits(&mut self, prompt_ids: &[u32]) -> anyhow::Result<Vec<f32>> {
(**self).predict_logits(prompt_ids)
}
fn generate(
&mut self,
prompt_ids: &[u32],
n_new: usize,
on_token: &mut dyn FnMut(u32) -> bool,
) -> anyhow::Result<Vec<u32>> {
(**self).generate(prompt_ids, n_new, on_token)
}
fn prefill_logits(&mut self, prompt_ids: &[u32]) -> anyhow::Result<Vec<f32>> {
(**self).prefill_logits(prompt_ids)
}
fn decode_logits(&mut self, token: u32) -> anyhow::Result<Vec<f32>> {
(**self).decode_logits(token)
}
fn set_multimodal_phase_callback(
&mut self,
cb: Option<std::sync::Arc<dyn Fn(&str) + Send + Sync>>,
) {
(**self).set_multimodal_phase_callback(cb);
}
fn prefill_logits_reusing(
&mut self,
prompt: &[u32],
snap: &SessionSnapshot,
reuse_len: usize,
) -> anyhow::Result<Vec<f32>> {
(**self).prefill_logits_reusing(prompt, snap, reuse_len)
}
fn export_session(&self) -> Option<SessionSnapshot> {
(**self).export_session()
}
fn restore_session(&mut self, snap: &SessionSnapshot) -> bool {
(**self).restore_session(snap)
}
fn kv_store_stats(&self) -> Option<(usize, usize, usize)> {
(**self).kv_store_stats()
}
fn supports_multimodal(&self) -> bool {
(**self).supports_multimodal()
}
fn generate_multimodal(
&mut self,
prompt: &str,
rgb: &[u8],
img_w: usize,
img_h: usize,
tokenizer: Option<&Path>,
n_new: usize,
on_token: &mut dyn FnMut(u32) -> bool,
) -> anyhow::Result<Vec<u32>> {
(**self).generate_multimodal(prompt, rgb, img_w, img_h, tokenizer, n_new, on_token)
}
}
pub struct HostDriven<R: LmRunner> {
inner: R,
context: Vec<u32>,
warned: bool,
}
impl<R: LmRunner> HostDriven<R> {
pub fn new(inner: R) -> Self {
Self {
inner,
context: Vec::new(),
warned: false,
}
}
pub fn inner(&self) -> &R {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut R {
&mut self.inner
}
pub fn into_inner(self) -> R {
self.inner
}
pub fn fell_back(&self) -> bool {
self.warned
}
fn note_fallback(&mut self) {
if !self.warned {
self.warned = true;
if rlx_ir::env::var("RLX_WARN_SLOW_DECODE").is_some() {
eprintln!(
"[lm] {}: no cached host-driven decode; re-prefilling the \
whole context each token (O(n^2) over a generation)",
self.inner.family()
);
}
}
}
}
impl<R: LmRunner> LmRunner for HostDriven<R> {
fn family(&self) -> &'static str {
self.inner.family()
}
fn vocab_size(&self) -> usize {
self.inner.vocab_size()
}
fn predict_logits(&mut self, prompt_ids: &[u32]) -> anyhow::Result<Vec<f32>> {
self.inner.predict_logits(prompt_ids)
}
fn generate(
&mut self,
prompt_ids: &[u32],
n_new: usize,
on_token: &mut dyn FnMut(u32) -> bool,
) -> anyhow::Result<Vec<u32>> {
self.inner.generate(prompt_ids, n_new, on_token)
}
fn prefill_logits(&mut self, prompt_ids: &[u32]) -> anyhow::Result<Vec<f32>> {
match self.inner.prefill_logits(prompt_ids) {
Ok(logits) => {
self.context.clear();
self.context.extend_from_slice(prompt_ids);
Ok(logits)
}
Err(_) => {
self.note_fallback();
self.context.clear();
self.context.extend_from_slice(prompt_ids);
self.inner.predict_logits(prompt_ids)
}
}
}
fn decode_logits(&mut self, token: u32) -> anyhow::Result<Vec<f32>> {
if self.context.is_empty() {
anyhow::bail!(
"{}: decode_logits before prefill_logits — there is no \
sequence to continue",
self.inner.family()
);
}
if let Ok(logits) = self.inner.decode_logits(token) {
self.context.push(token);
return Ok(logits);
}
self.note_fallback();
self.context.push(token);
let context = std::mem::take(&mut self.context);
let out = self.inner.predict_logits(&context);
self.context = context;
out
}
fn set_multimodal_phase_callback(
&mut self,
cb: Option<std::sync::Arc<dyn Fn(&str) + Send + Sync>>,
) {
self.inner.set_multimodal_phase_callback(cb);
}
fn prefill_logits_reusing(
&mut self,
prompt: &[u32],
snap: &SessionSnapshot,
reuse_len: usize,
) -> anyhow::Result<Vec<f32>> {
self.inner.prefill_logits_reusing(prompt, snap, reuse_len)
}
fn export_session(&self) -> Option<SessionSnapshot> {
self.inner.export_session()
}
fn restore_session(&mut self, snap: &SessionSnapshot) -> bool {
self.inner.restore_session(snap)
}
fn kv_store_stats(&self) -> Option<(usize, usize, usize)> {
self.inner.kv_store_stats()
}
fn supports_multimodal(&self) -> bool {
self.inner.supports_multimodal()
}
fn generate_multimodal(
&mut self,
prompt: &str,
rgb: &[u8],
img_w: usize,
img_h: usize,
tokenizer: Option<&Path>,
n_new: usize,
on_token: &mut dyn FnMut(u32) -> bool,
) -> anyhow::Result<Vec<u32>> {
self.inner
.generate_multimodal(prompt, rgb, img_w, img_h, tokenizer, n_new, on_token)
}
}
fn argmax_u32(logits: &[f32]) -> u32 {
let mut best = 0usize;
let mut best_v = f32::NEG_INFINITY;
for (i, &v) in logits.iter().enumerate() {
if v > best_v {
best_v = v;
best = i;
}
}
best as u32
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WeightFormat {
Safetensors,
Gguf,
Mlx,
Dduf,
}
impl WeightFormat {
pub fn from_path(path: &Path) -> anyhow::Result<Self> {
if path.is_dir() {
if path.join("config.json").is_file()
&& (path.join("model.safetensors").is_file()
|| path.join("model.safetensors.index.json").is_file()
|| std::fs::read_dir(path)?.any(|e| {
e.ok()
.and_then(|e| e.path().extension().map(|x| x == "safetensors"))
.unwrap_or(false)
}))
{
return Ok(Self::Mlx);
}
return Err(anyhow::anyhow!(
"directory {:?} is not an mlx-community weight dir (need config.json + safetensors)",
path
));
}
match path.extension().and_then(|s| s.to_str()) {
Some("safetensors") => Ok(Self::Safetensors),
Some("gguf") => Ok(Self::Gguf),
Some("npz") | Some("npy") => Ok(Self::Mlx),
Some("dduf") => Ok(Self::Dduf),
other => Err(anyhow::anyhow!(
"cannot autodetect weight format from extension {:?} on {:?}",
other,
path
)),
}
}
pub fn parse(s: &str) -> anyhow::Result<Self> {
match s {
"safetensors" => Ok(Self::Safetensors),
"gguf" => Ok(Self::Gguf),
"mlx" | "npz" => Ok(Self::Mlx),
"dduf" => Ok(Self::Dduf),
other => Err(anyhow::anyhow!(
"expected safetensors|gguf|mlx|dduf, got {other}"
)),
}
}
}
#[derive(Debug, Clone, Default)]
pub enum ConfigSource<T> {
#[default]
Embedded,
JsonFile(PathBuf),
Explicit(T),
}
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub enum MirostatMode {
#[default]
Off,
V1,
V2,
}
#[derive(Debug, Clone)]
pub struct SampleOpts {
pub temperature: f32,
pub top_p: f32,
pub top_k: Option<u32>,
pub repetition_penalty: f32,
pub dynamic_temp: Option<(f32, f32)>,
pub dynamic_temp_exponent: f32,
pub typical_p: f32,
pub top_n_sigma: f32,
pub min_p: f32,
pub xtc_threshold: f32,
pub xtc_prob: f32,
pub dry_multiplier: f32,
pub dry_base: f32,
pub dry_allowed_length: usize,
pub dry_max_ngram: usize,
pub dry_sequence_breakers: Vec<u32>,
pub mirostat: MirostatMode,
pub mirostat_tau: f32,
pub mirostat_eta: f32,
pub mirostat_m: usize,
pub frequency_penalty: f32,
pub presence_penalty: f32,
pub repetition_window: usize,
pub min_keep: usize,
}
impl Default for SampleOpts {
fn default() -> Self {
Self::greedy()
}
}
impl SampleOpts {
pub fn greedy() -> Self {
Self {
temperature: 0.0,
top_p: 1.0,
top_k: None,
repetition_penalty: 1.0,
dynamic_temp: None,
dynamic_temp_exponent: 1.0,
typical_p: 1.0,
top_n_sigma: 0.0,
min_p: 0.0,
xtc_threshold: 0.0,
xtc_prob: 0.0,
dry_multiplier: 0.0,
dry_base: 1.75,
dry_allowed_length: 2,
dry_max_ngram: 32,
dry_sequence_breakers: Vec::new(),
mirostat: MirostatMode::Off,
mirostat_tau: 5.0,
mirostat_eta: 0.1,
mirostat_m: 100,
frequency_penalty: 0.0,
presence_penalty: 0.0,
repetition_window: 64,
min_keep: 1,
}
}
pub fn nucleus(temperature: f32, top_p: f32) -> Self {
Self {
temperature,
top_p,
..Self::greedy()
}
}
pub fn is_greedy(&self) -> bool {
self.temperature <= 0.0 && self.mirostat == MirostatMode::Off
}
pub fn is_classic(&self) -> bool {
self.dynamic_temp.is_none()
&& self.typical_p >= 1.0
&& self.top_n_sigma <= 0.0
&& self.min_p <= 0.0
&& self.xtc_prob <= 0.0
&& self.dry_multiplier <= 0.0
&& self.mirostat == MirostatMode::Off
&& self.frequency_penalty == 0.0
&& self.presence_penalty == 0.0
&& (self.repetition_penalty - 1.0).abs() < f32::EPSILON
}
pub fn into_chain(&self) -> crate::samplers::SamplerChain {
use crate::samplers::*;
let mut b = SamplerChain::builder();
if (self.repetition_penalty - 1.0).abs() > f32::EPSILON
|| self.frequency_penalty != 0.0
|| self.presence_penalty != 0.0
{
b = b.push(RepetitionPenalty {
penalty: self.repetition_penalty,
frequency: self.frequency_penalty,
presence: self.presence_penalty,
last_n: self.repetition_window,
});
}
if self.dry_multiplier > 0.0 {
b = b.push(Dry {
multiplier: self.dry_multiplier,
base: self.dry_base,
allowed_length: self.dry_allowed_length,
max_ngram: self.dry_max_ngram,
sequence_breakers: self.dry_sequence_breakers.clone(),
});
}
if self.mirostat == MirostatMode::Off {
if let Some((mn, mx)) = self.dynamic_temp {
b = b.push(DynamicTemperature {
min: mn,
max: mx,
exponent: self.dynamic_temp_exponent,
});
} else if self.temperature > 0.0 && (self.temperature - 1.0).abs() > f32::EPSILON {
b = b.push(Temperature {
t: self.temperature,
});
} else if self.temperature <= 0.0 {
b = b.push(Temperature { t: 1e-6 });
}
}
if let Some(k) = self.top_k {
if k > 0 {
b = b.push(TopK { k: k as usize });
}
}
if self.typical_p < 1.0 && self.typical_p > 0.0 {
b = b.push(TypicalP {
p: self.typical_p,
min_keep: self.min_keep,
});
}
if self.top_p < 1.0 && self.top_p > 0.0 {
b = b.push(TopP {
p: self.top_p,
min_keep: self.min_keep,
});
}
if self.min_p > 0.0 {
b = b.push(MinP {
p: self.min_p,
min_keep: self.min_keep,
});
}
if self.top_n_sigma > 0.0 {
b = b.push(TopNSigma {
n: self.top_n_sigma,
});
}
if self.xtc_prob > 0.0 && self.xtc_threshold > 0.0 {
b = b.push(Xtc {
threshold: self.xtc_threshold,
prob: self.xtc_prob,
min_keep: self.min_keep,
});
}
match self.mirostat {
MirostatMode::Off => {}
MirostatMode::V1 => {
b = b.push(MirostatV1 {
tau: self.mirostat_tau,
eta: self.mirostat_eta,
m: self.mirostat_m,
});
}
MirostatMode::V2 => {
b = b.push(MirostatV2 {
tau: self.mirostat_tau,
eta: self.mirostat_eta,
});
}
}
b.build()
}
}
pub const PACKED_GGUF_AUTO_THRESHOLD_BYTES: u64 = 256 * 1024 * 1024;
#[derive(Debug, Clone)]
pub struct LmRunnerBuilder<Cfg> {
pub weights: Option<PathBuf>,
pub config: ConfigSource<Cfg>,
pub device: Device,
pub max_seq: usize,
pub max_memory_gb: Option<f32>,
pub stream: bool,
pub sample: SampleOpts,
pub format: Option<WeightFormat>,
pub packed_weights: Option<bool>,
pub prefer_gguf: Option<String>,
}
impl<Cfg> Default for LmRunnerBuilder<Cfg> {
fn default() -> Self {
Self {
weights: None,
config: ConfigSource::Embedded,
device: Device::Cpu,
max_seq: 128,
max_memory_gb: None,
stream: true,
sample: SampleOpts::greedy(),
format: None,
packed_weights: None,
prefer_gguf: None,
}
}
}
impl<Cfg> LmRunnerBuilder<Cfg> {
pub fn new() -> Self {
Self::default()
}
pub fn weights<P: Into<PathBuf>>(mut self, p: P) -> Self {
self.weights = Some(p.into());
self
}
pub fn config(mut self, src: ConfigSource<Cfg>) -> Self {
self.config = src;
self
}
pub fn config_value(self, cfg: Cfg) -> Self {
self.config(ConfigSource::Explicit(cfg))
}
pub fn device(mut self, d: Device) -> Self {
self.device = d;
self
}
pub fn max_seq(mut self, n: usize) -> Self {
self.max_seq = n;
self
}
pub fn max_memory_gb(mut self, gb: f32) -> Self {
self.max_memory_gb = Some(gb);
self
}
pub fn stream(mut self, on: bool) -> Self {
self.stream = on;
self
}
pub fn sample(mut self, s: SampleOpts) -> Self {
self.sample = s;
self
}
pub fn format(mut self, fmt: WeightFormat) -> Self {
self.format = Some(fmt);
self
}
pub fn packed_weights(mut self, on: bool) -> Self {
self.packed_weights = Some(on);
self
}
pub fn prefer_gguf<S: Into<String>>(mut self, q: S) -> Self {
self.prefer_gguf = Some(q.into());
self
}
pub fn resolved_format(&self) -> anyhow::Result<WeightFormat> {
match self.format {
Some(f) => Ok(f),
None => {
let p = self
.weights
.as_deref()
.ok_or_else(|| anyhow::anyhow!("weights path required"))?;
WeightFormat::from_path(p)
}
}
}
pub fn resolved_packed(&self, fmt: WeightFormat) -> bool {
match self.packed_weights {
Some(b) => b,
None => {
if !matches!(fmt, WeightFormat::Gguf) {
return false;
}
self.weights
.as_deref()
.and_then(|p| std::fs::metadata(p).ok())
.map(|m| m.len() >= PACKED_GGUF_AUTO_THRESHOLD_BYTES)
.unwrap_or(false)
}
}
}
}
pub struct ModelRegistration {
pub family: &'static str,
pub description: &'static str,
pub matches: fn(arch: &str, path: &Path) -> bool,
}
inventory::collect!(ModelRegistration);
pub extern crate inventory;
pub fn registered_models() -> impl Iterator<Item = &'static ModelRegistration> {
inventory::iter::<ModelRegistration>.into_iter()
}
pub fn auto_runner_name(arch: &str, path: &Path) -> Option<&'static str> {
let arch_lc = arch.to_ascii_lowercase();
registered_models()
.find(|m| (m.matches)(&arch_lc, path))
.map(|m| m.family)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_source_default_is_embedded() {
let s: ConfigSource<()> = ConfigSource::default();
assert!(matches!(s, ConfigSource::Embedded));
}
#[test]
fn builder_defaults_match_legacy_runners() {
let b: LmRunnerBuilder<()> = LmRunnerBuilder::new();
assert_eq!(b.device, Device::Cpu);
assert_eq!(b.max_seq, 128);
assert!(b.stream);
assert!(b.sample.is_greedy());
assert!(b.packed_weights.is_none());
}
#[test]
fn packed_auto_size_threshold() {
let mut b: LmRunnerBuilder<()> = LmRunnerBuilder::new();
b.weights = Some("/nonexistent/file.gguf".into());
assert!(!b.resolved_packed(WeightFormat::Gguf));
b.packed_weights = Some(true);
assert!(b.resolved_packed(WeightFormat::Gguf));
b.packed_weights = None;
assert!(!b.resolved_packed(WeightFormat::Safetensors));
}
}
#[cfg(test)]
mod host_driven_tests {
use super::*;
struct NoPair {
vocab: usize,
prefills: usize,
}
impl LmRunner for NoPair {
fn family(&self) -> &'static str {
"nopair"
}
fn vocab_size(&self) -> usize {
self.vocab
}
fn predict_logits(&mut self, prompt_ids: &[u32]) -> anyhow::Result<Vec<f32>> {
self.prefills += 1;
let pick = (prompt_ids.iter().map(|&t| t as usize).sum::<usize>() + prompt_ids.len())
% self.vocab;
let mut l = vec![0.0f32; self.vocab];
l[pick] = 1.0;
Ok(l)
}
}
struct HasPair {
vocab: usize,
context: Vec<u32>,
prefills: usize,
}
impl LmRunner for HasPair {
fn family(&self) -> &'static str {
"haspair"
}
fn vocab_size(&self) -> usize {
self.vocab
}
fn predict_logits(&mut self, prompt_ids: &[u32]) -> anyhow::Result<Vec<f32>> {
self.prefills += 1;
let pick = (prompt_ids.iter().map(|&t| t as usize).sum::<usize>() + prompt_ids.len())
% self.vocab;
let mut l = vec![0.0f32; self.vocab];
l[pick] = 1.0;
Ok(l)
}
fn prefill_logits(&mut self, prompt_ids: &[u32]) -> anyhow::Result<Vec<f32>> {
self.context = prompt_ids.to_vec();
self.predict_logits(prompt_ids)
}
fn decode_logits(&mut self, token: u32) -> anyhow::Result<Vec<f32>> {
self.context.push(token);
let c = std::mem::take(&mut self.context);
let out = self.predict_logits(&c);
self.context = c;
out
}
}
fn host_driven_tokens<R: LmRunner>(r: &mut R, prompt: &[u32], n: usize) -> Vec<u32> {
let mut logits = LmRunner::prefill_logits(r, prompt).expect("prefill");
let mut out = Vec::new();
for _ in 0..n {
let t = argmax_u32(&logits);
out.push(t);
logits = LmRunner::decode_logits(r, t).expect("decode");
}
out
}
#[test]
fn a_runner_without_the_pair_cannot_be_driven_by_a_server() {
let mut bare = NoPair {
vocab: 16,
prefills: 0,
};
let err = LmRunner::prefill_logits(&mut bare, &[1, 2])
.unwrap_err()
.to_string();
assert!(err.contains("unsupported"), "{err}");
assert!(
err.contains("HostDriven"),
"the error should name the fix: {err}"
);
}
#[test]
fn wrapping_it_gives_the_same_tokens_as_its_own_generate() {
let prompt = [3u32, 5, 7];
let mut own = NoPair {
vocab: 16,
prefills: 0,
};
let want = own
.generate(&prompt, 6, &mut |_| true)
.expect("the default greedy loop");
let mut wrapped = HostDriven::new(NoPair {
vocab: 16,
prefills: 0,
});
let got = host_driven_tokens(&mut wrapped, &prompt, 6);
assert_eq!(got, want);
assert!(
wrapped.fell_back(),
"NoPair has no cached pair, so this is the slow path"
);
}
#[test]
fn wrapping_a_runner_that_has_the_pair_uses_it() {
let mut wrapped = HostDriven::new(HasPair {
vocab: 16,
context: Vec::new(),
prefills: 0,
});
let got = host_driven_tokens(&mut wrapped, &[3, 5, 7], 6);
assert_eq!(got.len(), 6);
assert!(
!wrapped.fell_back(),
"the family's own pair should have been used"
);
}
#[test]
fn decoding_before_prefilling_says_so() {
let mut wrapped = HostDriven::new(NoPair {
vocab: 16,
prefills: 0,
});
let err = LmRunner::decode_logits(&mut wrapped, 4)
.unwrap_err()
.to_string();
assert!(err.contains("before prefill"), "{err}");
}
#[test]
fn the_context_grows_by_exactly_one_token_per_step() {
let mut wrapped = HostDriven::new(NoPair {
vocab: 1024,
prefills: 0,
});
let prompt = [2u32, 4];
let mut logits = LmRunner::prefill_logits(&mut wrapped, &prompt).unwrap();
let mut expect: Vec<u32> = prompt.to_vec();
for _ in 0..5 {
let t = argmax_u32(&logits);
expect.push(t);
logits = LmRunner::decode_logits(&mut wrapped, t).unwrap();
let want = (expect.iter().map(|&t| t as usize).sum::<usize>() + expect.len()) % 1024;
assert_eq!(argmax_u32(&logits) as usize, want, "context: {expect:?}");
}
}
}