Skip to main content

rlx_moshi/
backend.rs

1//! Unified Moshi LM backend — eager CPU, Candle GPU, optional compiled path.
2
3use crate::checkpoint::MoshiCheckpoint;
4use crate::config::{GenerateConfig, MoshiVariant};
5use crate::generate::GenerateState;
6use crate::lm::LmModel;
7use crate::sampling::LogitsProcessor;
8use crate::weights::{open_lm, open_lm_from_weights};
9use anyhow::{Result, ensure};
10use rlx_runtime::Device;
11use std::path::Path;
12
13#[cfg(feature = "gpu-lm")]
14use crate::gpu::{GpuGenerateState, GpuLm, gpu_lm_available, rlx_device_to_candle};
15
16#[cfg(feature = "compiled-lm")]
17pub use crate::compiled_lm::{CompiledGenerateState, CompiledLm, compiled_lm_available};
18
19/// How to load and run the Moshi temporal + DepFormer stack.
20pub enum MoshiLm {
21    Eager(LmModel),
22    #[cfg(feature = "gpu-lm")]
23    Gpu(GpuLm),
24}
25
26/// Per-generation autoregressive state (CPU or GPU).
27pub enum MoshiGenState {
28    Eager(GenerateState),
29    #[cfg(feature = "gpu-lm")]
30    Gpu(GpuGenerateState),
31}
32
33impl MoshiLm {
34    pub fn open(
35        model_dir: &Path,
36        variant: MoshiVariant,
37        checkpoint: MoshiCheckpoint,
38        device: Device,
39    ) -> Result<Self> {
40        if checkpoint.is_mlx() && device == Device::Cpu {
41            let weights_path = checkpoint.lm_weights_path(model_dir);
42            let cfg = variant.lm_config();
43            let weights =
44                crate::mlx_weights::load_eager_weight_map(&weights_path, checkpoint, &cfg)?;
45            return Ok(Self::Eager(open_lm_from_weights(cfg, weights)?));
46        }
47        if checkpoint.is_mlx() {
48            #[cfg(not(feature = "gpu-lm"))]
49            anyhow::bail!(
50                "checkpoint {:?} requires `gpu-lm` or `--device cpu`",
51                checkpoint
52            );
53        }
54        let weights_path = checkpoint.lm_weights_path(model_dir);
55        let cfg = variant.lm_config();
56
57        #[cfg(feature = "compiled-lm")]
58        if device != Device::Cpu && compiled_lm_available(device) && checkpoint.gpu_loadable() {
59            return Ok(Self::Gpu(CompiledLm::open(
60                &weights_path,
61                variant,
62                checkpoint,
63                device,
64            )?));
65        }
66
67        #[cfg(all(feature = "gpu-lm", not(feature = "compiled-lm")))]
68        if device != Device::Cpu && gpu_lm_available(device) && checkpoint.gpu_loadable() {
69            return Ok(Self::Gpu(GpuLm::open(
70                &weights_path,
71                variant,
72                checkpoint,
73                device,
74            )?));
75        }
76
77        if device != Device::Cpu && checkpoint.gpu_loadable() {
78            #[cfg(feature = "gpu-lm")]
79            if gpu_lm_available(device) {
80                return Ok(Self::Gpu(GpuLm::open(
81                    &weights_path,
82                    variant,
83                    checkpoint,
84                    device,
85                )?));
86            }
87            #[cfg(not(feature = "gpu-lm"))]
88            anyhow::bail!(
89                "device {device:?} requested but crate built without `gpu-lm` — rebuild with --features gpu-lm,metal"
90            );
91            #[cfg(feature = "gpu-lm")]
92            anyhow::bail!("device {device:?} not available for Moshi GPU LM");
93        }
94
95        let lm = if checkpoint.is_gguf() {
96            let weights = crate::gguf::load_gguf_weight_map(&weights_path, &cfg)?;
97            open_lm_from_weights(cfg, weights)?
98        } else {
99            open_lm(model_dir, cfg)?
100        };
101        Ok(Self::Eager(lm))
102    }
103
104    pub fn config(&self) -> &crate::config::LmConfig {
105        match self {
106            Self::Eager(m) => m.config(),
107            #[cfg(feature = "gpu-lm")]
108            Self::Gpu(m) => m.config(),
109        }
110    }
111
112    pub fn reset_state(&mut self) {
113        match self {
114            Self::Eager(m) => m.reset_state(),
115            #[cfg(feature = "gpu-lm")]
116            Self::Gpu(_) => {}
117        }
118    }
119
120    pub fn new_gen_state(
121        &self,
122        max_steps: usize,
123        text_lp: LogitsProcessor,
124        audio_lp: LogitsProcessor,
125        gen_cfg: GenerateConfig,
126    ) -> Result<MoshiGenState> {
127        match self {
128            Self::Eager(_) => Ok(MoshiGenState::Eager(GenerateState::new(
129                max_steps, text_lp, audio_lp, gen_cfg,
130            ))),
131            #[cfg(feature = "gpu-lm")]
132            Self::Gpu(g) => Ok(MoshiGenState::Gpu(
133                g.new_generate_state(max_steps, text_lp, audio_lp, &gen_cfg)?,
134            )),
135        }
136    }
137}
138
139impl MoshiGenState {
140    pub fn config(&self) -> &GenerateConfig {
141        match self {
142            Self::Eager(s) => s.config(),
143            #[cfg(feature = "gpu-lm")]
144            Self::Gpu(s) => s.config(),
145        }
146    }
147
148    pub fn step_idx(&self) -> usize {
149        match self {
150            Self::Eager(s) => s.step_idx(),
151            #[cfg(feature = "gpu-lm")]
152            Self::Gpu(s) => s.step_idx(),
153        }
154    }
155
156    pub fn text_tokens(&self) -> &[u32] {
157        match self {
158            Self::Eager(s) => s.text_tokens(),
159            #[cfg(feature = "gpu-lm")]
160            Self::Gpu(s) => s.text_tokens(),
161        }
162    }
163
164    pub fn step(&mut self, lm: &mut MoshiLm, text_token: u32, input_audio: &[u32]) -> Result<u32> {
165        match (self, lm) {
166            (Self::Eager(s), MoshiLm::Eager(m)) => s.step(m, text_token, input_audio),
167            #[cfg(feature = "gpu-lm")]
168            (Self::Gpu(s), MoshiLm::Gpu(_m)) => s.step(text_token, input_audio),
169            #[allow(unreachable_patterns)]
170            _ => {
171                ensure!(false, "Moshi LM / generation backend mismatch");
172                Ok(0)
173            }
174        }
175    }
176
177    pub fn last_audio_frame(&self) -> Option<Vec<u32>> {
178        match self {
179            Self::Eager(s) => s.last_audio_frame(),
180            #[cfg(feature = "gpu-lm")]
181            Self::Gpu(s) => s.last_audio_frame(),
182        }
183    }
184
185    pub fn reset(
186        &mut self,
187        lm: &mut MoshiLm,
188        max_steps: usize,
189        text_lp: LogitsProcessor,
190        audio_lp: LogitsProcessor,
191    ) -> Result<()> {
192        match (self, lm) {
193            (Self::Eager(s), MoshiLm::Eager(m)) => {
194                let _ = (max_steps, text_lp, audio_lp);
195                s.reset(m);
196                Ok(())
197            }
198            #[cfg(feature = "gpu-lm")]
199            (Self::Gpu(s), MoshiLm::Gpu(g)) => s.reset(g, max_steps, text_lp, audio_lp),
200            #[allow(unreachable_patterns)]
201            _ => anyhow::bail!("Moshi LM / generation backend mismatch"),
202        }
203    }
204}
205
206/// Pick the LM device: honor CPU requests, fall back when GPU preset or backend is unavailable.
207pub fn resolve_lm_device(requested: Device, checkpoint: MoshiCheckpoint) -> Device {
208    if requested == Device::Cpu {
209        return Device::Cpu;
210    }
211    if !checkpoint.gpu_loadable() {
212        return Device::Cpu;
213    }
214    #[cfg(feature = "gpu-lm")]
215    {
216        if gpu_lm_available(requested) {
217            return requested;
218        }
219    }
220    Device::Cpu
221}
222
223#[cfg(feature = "gpu-lm")]
224pub fn candle_device_for(device: Device) -> Result<candle::Device> {
225    rlx_device_to_candle(device)
226}