1use std::path::{Path, PathBuf};
7use std::sync::Mutex;
8
9use anyhow::{Context, Result, bail};
10use rlx_core::validate_standard_device;
11use rlx_core::weight_loader::GgufLoader;
12use rlx_llama_base::LlamaBaseConfig;
13use rlx_llama32::{Llama32Generator, Llama32Runner, Llama32RunnerBuilder, MetalGgufPrefillMode};
14
15use crate::backbone::BackboneLoadOptions;
16use crate::device::lm_kv_decode_supported;
17use rlx_qwen3::{SampleOpts, apply_repetition_penalty, sample_token_at};
18use rlx_qwen35::encode_prompt_from_gguf;
19use rlx_runtime::Device;
20use rlx_runtime::{
21 llama_decode_bucket_compile_peak_bytes, llama_decode_oneshot_compile_peak_bytes,
22 memory_headroom_bytes, process_rss_bytes, soft_memory_budget_bytes, would_exceed_soft_budget,
23};
24
25use crate::runner::GenerationConfig;
26use crate::tokens::{
27 STOP_TOKEN_ID, custom_token_id_to_code, is_snac_slot_token, mask_logits_for_snac_slot,
28 use_snac_logit_mask_for,
29};
30
31pub const DEFAULT_N_CTX: u32 = 2048;
32pub const DEFAULT_COMPILE_SEQ_CAP: u32 = 256;
35
36pub const SYNTHESIS_COMPILE_SEQ_CAP: u32 = 128;
39
40enum DecodeStrategy {
41 Bucket { max_past: usize },
43 Dynamic { cache_capacity: usize },
45}
46
47fn shared_ram_mode() -> bool {
48 if matches!(
49 std::env::var("ORPHEUS_LOW_MEM").ok().as_deref(),
50 Some("1") | Some("true") | Some("TRUE")
51 ) {
52 return true;
53 }
54 soft_memory_budget_bytes()
56 .is_some_and(|budget| budget < llama_decode_bucket_compile_peak_bytes() / 2)
57}
58
59pub(crate) fn low_mem_mode() -> bool {
60 shared_ram_mode()
61 || memory_headroom_bytes().is_some_and(|h| h < llama_decode_oneshot_compile_peak_bytes())
62}
63
64fn log_soft_memory_budget() {
65 if let Some(budget) = soft_memory_budget_bytes() {
66 let rss = process_rss_bytes().unwrap_or(0);
67 let headroom = budget.saturating_sub(rss);
68 eprintln!(
69 "[orpheus/backbone] soft RAM budget {:.1} GiB (RSS {:.1} GiB, headroom {:.1} GiB)",
70 budget as f64 / 1024.0 / 1024.0 / 1024.0,
71 rss as f64 / 1024.0 / 1024.0 / 1024.0,
72 headroom as f64 / 1024.0 / 1024.0 / 1024.0,
73 );
74 }
75}
76
77fn synthesis_decode_plan(compile_cap: usize, opts: &BackboneLoadOptions) -> DecodeStrategy {
80 let max_past = decode_bucket_max_past(compile_cap);
81 let want_bucket = env_flag("ORPHEUS_BUCKET_DECODE") == Some(true)
82 || (!opts.memory_efficient && env_flag("ORPHEUS_BUCKET_DECODE") != Some(false));
83
84 if want_bucket {
85 if would_exceed_soft_budget(llama_decode_bucket_compile_peak_bytes()) {
86 eprintln!(
87 "[orpheus/backbone] bucket decode denied (headroom < {:.1} GiB) — dynamic decode",
88 llama_decode_bucket_compile_peak_bytes() as f64 / 1024.0 / 1024.0 / 1024.0,
89 );
90 } else {
91 eprintln!(
92 "[orpheus/backbone] bucket decode enabled (max_past={max_past}) — set ORPHEUS_BUCKET_DECODE=0 to save RAM"
93 );
94 return DecodeStrategy::Bucket { max_past };
95 }
96 } else if env_flag("ORPHEUS_BUCKET_DECODE") == Some(false) {
97 eprintln!("[orpheus/backbone] ORPHEUS_BUCKET_DECODE=0 — dynamic decode");
98 }
99
100 let cache_capacity = if low_mem_mode() || opts.memory_efficient {
101 1
102 } else {
103 2
104 };
105 DecodeStrategy::Dynamic { cache_capacity }
106}
107
108fn compile_seq_cap_for(n_ctx: u32, opts: &BackboneLoadOptions) -> usize {
109 let base = if env_flag("ORPHEUS_HIGH_MEM") == Some(true) {
110 DEFAULT_COMPILE_SEQ_CAP
111 } else if low_mem_mode() {
112 64
113 } else if opts.memory_efficient {
114 SYNTHESIS_COMPILE_SEQ_CAP
115 } else {
116 DEFAULT_COMPILE_SEQ_CAP
117 };
118 std::env::var("ORPHEUS_COMPILE_SEQ_CAP")
119 .ok()
120 .and_then(|s| s.parse::<u32>().ok())
121 .map(|c| c.min(n_ctx) as usize)
122 .unwrap_or((base.min(n_ctx)) as usize)
123}
124
125fn prefill_cache_capacity() -> usize {
126 std::env::var("ORPHEUS_PREFILL_CACHE")
127 .ok()
128 .and_then(|s| s.parse().ok())
129 .unwrap_or(if low_mem_mode() { 1 } else { 2 })
130}
131
132fn decode_bucket_max_past(compile_cap: usize) -> usize {
133 std::env::var("ORPHEUS_DECODE_CACHE_CAP")
134 .ok()
135 .and_then(|s| s.parse().ok())
136 .unwrap_or(compile_cap.min(64))
137}
138
139fn env_flag(name: &str) -> Option<bool> {
140 match std::env::var(name).ok().as_deref() {
141 Some("1") | Some("true") | Some("TRUE") => Some(true),
142 Some("0") | Some("false") | Some("FALSE") => Some(false),
143 _ => None,
144 }
145}
146
147fn sample_opts(cfg: &GenerationConfig) -> SampleOpts {
148 if cfg.greedy || std::env::var("ORPHEUS_GREEDY").ok().as_deref() == Some("1") {
149 return SampleOpts::greedy();
150 }
151 SampleOpts::temperature(cfg.temperature, cfg.seed)
152 .with_top_p(cfg.top_p)
153 .with_top_k(cfg.top_k)
154}
155
156fn use_compile_caches() -> bool {
159 env_flag("ORPHEUS_COMPILE_CACHE") == Some(true) && !low_mem_mode()
160}
161
162fn metal_f32_kv_enabled() -> bool {
163 env_flag("ORPHEUS_METAL_KV") == Some(true)
164}
165
166fn use_packed_lm(device: Device) -> bool {
168 if let Some(v) = env_flag("ORPHEUS_PACKED_LM") {
169 return v;
170 }
171 if env_flag("ORPHEUS_FAST_LM") == Some(true) {
172 return false;
173 }
174 if matches!(device, Device::Metal) && metal_f32_kv_enabled() {
175 return false;
176 }
177 matches!(device, Device::Cpu) && !lm_kv_decode_supported(device)
179}
180
181fn use_fast_kv(device: Device, opts: &BackboneLoadOptions) -> bool {
183 if let Some(v) = opts.use_fast_kv {
184 return v;
185 }
186 if env_flag("ORPHEUS_FAST_LM") == Some(false) {
187 return false;
188 }
189 if env_flag("ORPHEUS_METAL_KV") == Some(false) {
190 return false;
191 }
192 lm_kv_decode_supported(device) && !use_packed_lm(device)
193}
194
195enum LmEngine {
196 Packed(Box<Llama32Runner>),
197 Kv(Box<Llama32Generator>),
198}
199
200fn push_orpheus_stream_token(
201 tok: u32,
202 stream_index: &mut usize,
203 on_code: &mut impl FnMut(i32) -> Result<()>,
204) -> Result<bool> {
205 if tok == STOP_TOKEN_ID {
206 return Ok(true);
207 }
208 if is_snac_slot_token(tok, *stream_index) {
211 if let Some(code) = custom_token_id_to_code(tok, *stream_index) {
212 if code > 0 {
213 on_code(code)?;
214 }
215 }
216 *stream_index += 1;
217 }
218 Ok(false)
219}
220
221fn adjust_orpheus_logits(
222 logits: &mut [f32],
223 stream_index: usize,
224 token_counts: &std::collections::HashMap<u32, u32>,
225 repetition_penalty: f32,
226 apply_penalty: bool,
227 lm_device: Device,
228 lm_decode_on_cpu: bool,
229) {
230 if use_snac_logit_mask_for(lm_device, lm_decode_on_cpu) {
231 mask_logits_for_snac_slot(logits, stream_index);
232 }
233 if apply_penalty {
234 apply_repetition_penalty(logits, token_counts, repetition_penalty);
235 }
236}
237
238pub struct BackboneModel {
239 engine: Mutex<LmEngine>,
240 weights: PathBuf,
241 n_ctx: u32,
242 lm_device: Device,
243 lm_decode_on_cpu: bool,
244 pub seed: Option<u32>,
245}
246
247impl BackboneModel {
248 pub fn weights_path(&self) -> &Path {
249 &self.weights
250 }
251
252 pub fn load(path: &Path, n_ctx: u32) -> Result<Self> {
253 Self::load_on(path, n_ctx, Device::Cpu)
254 }
255
256 pub fn load_on(path: &Path, n_ctx: u32, device: Device) -> Result<Self> {
257 Self::load_on_with(path, n_ctx, device, BackboneLoadOptions::for_device(device))
258 }
259
260 pub fn load_on_with(
261 path: &Path,
262 n_ctx: u32,
263 device: Device,
264 opts: BackboneLoadOptions,
265 ) -> Result<Self> {
266 validate_standard_device("orpheus", device)?;
267 let path_str = path
268 .to_str()
269 .ok_or_else(|| anyhow::anyhow!("non-utf8 weights path"))?;
270
271 let compile_cap = compile_seq_cap_for(n_ctx, &opts);
272 log_soft_memory_budget();
273 let lm_device = device;
274 let lm_decode_on_cpu = matches!(lm_device, Device::Cpu)
275 || matches!(lm_device, Device::Gpu | Device::Vulkan)
276 || (lm_device == Device::Metal
277 && matches!(
278 opts.metal_prefill.resolve(),
279 MetalGgufPrefillMode::CpuF32
280 | MetalGgufPrefillMode::Auto
281 | MetalGgufPrefillMode::PackedGguf,
282 ));
283
284 let engine = if use_fast_kv(lm_device, &opts) {
285 let mut gguf = GgufLoader::from_file(path_str).context("open GGUF for KV generator")?;
286 if gguf.architecture() != "llama" {
287 bail!(
288 "rlx-orpheus: expected llama GGUF at {}; got `{}`",
289 path.display(),
290 gguf.architecture()
291 );
292 }
293 let cfg = rlx_llama32::llama32_cfg_from_gguf(gguf.file())?;
294 let mut generator = Llama32Generator::from_loader_at_mode(
295 cfg,
296 &mut gguf,
297 lm_device,
298 path,
299 if lm_device == Device::Cpu {
300 MetalGgufPrefillMode::CpuF32
301 } else {
302 opts.metal_prefill.resolve()
303 },
304 )?
305 .with_compile_seq_cap(compile_cap);
306 let decode = synthesis_decode_plan(compile_cap, &opts);
307 let use_bucket = matches!(decode, DecodeStrategy::Bucket { .. });
308 generator = match decode {
309 DecodeStrategy::Bucket { max_past } => generator.with_decode_cache(max_past),
310 DecodeStrategy::Dynamic { cache_capacity } => {
311 generator.with_dynamic_decode_cache(cache_capacity)
312 }
313 };
314 if use_bucket && use_compile_caches() {
315 generator = generator.with_prefill_cache(prefill_cache_capacity());
316 }
317 let decode_on_cpu = lm_decode_on_cpu;
318 eprintln!(
319 "[orpheus/backbone] kv-cache LM on {lm_device:?} {} (prefill={:?}, decode={}, compile_cap={compile_cap}, decode={})",
320 path.display(),
321 if lm_device == Device::Cpu {
322 MetalGgufPrefillMode::CpuF32
323 } else {
324 opts.metal_prefill.resolve()
325 },
326 if decode_on_cpu { "cpu" } else { "device" },
327 if use_bucket { "bucket" } else { "dynamic" },
328 );
329 LmEngine::Kv(Box::new(generator))
330 } else {
331 let base = LlamaBaseConfig::from_gguf_path(path)
332 .with_context(|| format!("parse GGUF {}", path.display()))?;
333 if base.arch != "llama" {
334 bail!(
335 "rlx-orpheus: expected llama GGUF at {}; got `{}`",
336 path.display(),
337 base.arch
338 );
339 }
340 let runner = Llama32RunnerBuilder::default()
341 .weights(path)
342 .max_seq(n_ctx as usize)
343 .device(lm_device)
344 .packed_weights(use_packed_lm(lm_device))
345 .stream(false)
346 .sample(SampleOpts::greedy())
347 .build()
348 .context("build Llama32Runner for Orpheus")?;
349 eprintln!(
350 "[orpheus/backbone] {} on {device:?} {}",
351 if use_packed_lm(device) {
352 "packed GGUF prefill"
353 } else {
354 "f32 runner"
355 },
356 path.display()
357 );
358 LmEngine::Packed(Box::new(runner))
359 };
360
361 Ok(Self {
362 engine: Mutex::new(engine),
363 weights: path.to_path_buf(),
364 n_ctx,
365 lm_device,
366 lm_decode_on_cpu,
367 seed: Some(GenerationConfig::default().seed as u32),
368 })
369 }
370
371 pub fn generate_codes(&self, prompt: &str, cfg: &GenerationConfig) -> Result<Vec<i32>> {
372 let prompt_ids = encode_prompt_from_gguf(&self.weights, prompt)
373 .with_context(|| format!("tokenize prompt for {}", self.weights.display()))?;
374 self.generate_codes_from_prompt(&prompt_ids, cfg)
375 }
376
377 pub fn generate_codes_from_prompt(
378 &self,
379 prompt_ids: &[u32],
380 cfg: &GenerationConfig,
381 ) -> Result<Vec<i32>> {
382 let mut codes = Vec::new();
383 self.generate_codes_from_prompt_streaming(prompt_ids, cfg, |c| {
384 codes.push(c);
385 Ok(())
386 })?;
387 Ok(codes)
388 }
389
390 pub fn generate_codes_from_prompt_streaming(
391 &self,
392 prompt_ids: &[u32],
393 cfg: &GenerationConfig,
394 mut on_code: impl FnMut(i32) -> Result<()>,
395 ) -> Result<()> {
396 if prompt_ids.len() as u32 > self.n_ctx {
397 bail!(
398 "prompt too long: {} tokens > n_ctx={}",
399 prompt_ids.len(),
400 self.n_ctx
401 );
402 }
403
404 let lm_device = self.lm_device;
405 let lm_decode_on_cpu = self.lm_decode_on_cpu;
406 let mut engine = self
407 .engine
408 .lock()
409 .map_err(|e| anyhow::anyhow!("backbone lock poisoned: {e}"))?;
410
411 let mut stream_index = 0usize;
412
413 match &mut *engine {
414 LmEngine::Kv(generator) => {
415 let sample = sample_opts(cfg);
416 let apply_penalty = cfg.repetition_penalty > 1.0;
417 let mut token_counts = std::collections::HashMap::<u32, u32>::new();
418 generator.prefill(prompt_ids);
419 eprintln!(
420 "[orpheus/backbone] generating up to {} tokens (prompt len={})",
421 cfg.max_new_tokens,
422 prompt_ids.len()
423 );
424 for step in 0..cfg.max_new_tokens {
425 let penalty = cfg.repetition_penalty;
426 let slot_ix = stream_index;
427 let next = generator
428 .step_cached_adjust(sample, step as u64, |logits| {
429 adjust_orpheus_logits(
430 logits,
431 slot_ix,
432 &token_counts,
433 penalty,
434 apply_penalty,
435 lm_device,
436 lm_decode_on_cpu,
437 );
438 })
439 .context("Orpheus LM cached decode step")?;
440 if step > 0 && step % 7 == 0 {
441 eprintln!(
442 "[orpheus/backbone] step {}/{} ({} tokens in history)",
443 step,
444 cfg.max_new_tokens,
445 generator.tokens().len()
446 );
447 }
448 if push_orpheus_stream_token(next, &mut stream_index, &mut on_code)? {
449 break;
450 }
451 *token_counts.entry(next).or_insert(0) += 1;
452 }
453 if std::env::var("ORPHEUS_DEBUG_TOKENS").ok().as_deref() == Some("1") {
454 let generated = generator.tokens()[prompt_ids.len()..].to_vec();
455 eprintln!(
456 "[orpheus/backbone] raw tokens ({}): {:?}",
457 generated.len(),
458 &generated[..generated.len().min(16)]
459 );
460 }
461 }
462 LmEngine::Packed(runner) => {
463 let sample = sample_opts(cfg);
464 let mut history = prompt_ids.to_vec();
465 let mut token_counts = std::collections::HashMap::<u32, u32>::new();
466 for step in 0..cfg.max_new_tokens {
467 let slot_ix = stream_index;
468 let mut logits = runner.predict_logits(&history)?;
469 adjust_orpheus_logits(
470 &mut logits,
471 slot_ix,
472 &token_counts,
473 cfg.repetition_penalty,
474 true,
475 lm_device,
476 lm_decode_on_cpu,
477 );
478 let next = sample_token_at(&logits, sample, step as u64) as u32;
479 if push_orpheus_stream_token(next, &mut stream_index, &mut on_code)? {
480 break;
481 }
482 *token_counts.entry(next).or_insert(0) += 1;
483 history.push(next);
484 }
485 if std::env::var("ORPHEUS_DEBUG_TOKENS").ok().as_deref() == Some("1") {
486 let generated = history[prompt_ids.len()..].to_vec();
487 eprintln!(
488 "[orpheus/backbone] raw tokens ({}): {:?}",
489 generated.len(),
490 &generated[..generated.len().min(16)]
491 );
492 }
493 }
494 }
495 Ok(())
496 }
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502 use crate::backbone::BackboneLoadOptions;
503 use crate::tokens::STOP_TOKEN_ID;
504
505 #[test]
506 fn synthesis_defaults_to_dynamic_decode() {
507 let _guard = EnvGuard::unset("ORPHEUS_BUCKET_DECODE");
508 let plan = synthesis_decode_plan(128, &BackboneLoadOptions::synthesis());
509 assert!(matches!(plan, DecodeStrategy::Dynamic { .. }));
510 }
511
512 #[test]
513 fn reference_parity_allows_bucket_when_env_set() {
514 let _guard = EnvGuard::set("ORPHEUS_BUCKET_DECODE", "1");
515 let plan = synthesis_decode_plan(128, &BackboneLoadOptions::reference_parity());
516 let _ = plan;
518 }
519
520 struct EnvGuard {
522 key: &'static str,
523 prev: Option<String>,
524 }
525
526 impl EnvGuard {
527 fn set(key: &'static str, val: &str) -> Self {
528 let prev = std::env::var(key).ok();
529 unsafe { std::env::set_var(key, val) };
530 Self { key, prev }
531 }
532
533 fn unset(key: &'static str) -> Self {
534 let prev = std::env::var(key).ok();
535 unsafe { std::env::remove_var(key) };
536 Self { key, prev }
537 }
538 }
539
540 impl Drop for EnvGuard {
541 fn drop(&mut self) {
542 match &self.prev {
543 Some(v) => unsafe { std::env::set_var(self.key, v) },
544 None => unsafe { std::env::remove_var(self.key) },
545 }
546 }
547 }
548
549 #[test]
550 fn stop_token_yields_no_codes() {
551 let mut codes = Vec::new();
552 let tokens = [STOP_TOKEN_ID];
553 let mut stream_index = 0usize;
554 for &tok in &tokens {
555 let stop = push_orpheus_stream_token(tok, &mut stream_index, &mut |c| {
556 codes.push(c);
557 Ok(())
558 })
559 .expect("push token");
560 if stop {
561 break;
562 }
563 }
564 assert!(codes.is_empty());
565 }
566}