Skip to main content

mistralrs_core/pipeline/
auto.rs

1use super::hf::{hf_access_error, remote_issue_from_api_error, RemoteAccessIssue};
2use super::{
3    DiffusionLoaderBuilder, DiffusionLoaderType, EmbeddingLoaderBuilder, EmbeddingLoaderType,
4    EmbeddingSpecificConfig, Loader, ModelKind, ModelPaths, MultimodalLoaderBuilder,
5    MultimodalLoaderType, MultimodalSpecificConfig, NormalLoaderBuilder, NormalLoaderType,
6    NormalSpecificConfig, SpeechLoader, TokenSource,
7};
8use crate::utils::{progress::ProgressScopeGuard, tokens::get_token};
9use crate::Ordering;
10use crate::{DeviceMapSetting, IsqType, PagedAttentionConfig, Pipeline, TryIntoDType};
11use anyhow::Result;
12use candle_core::Device;
13use hf_hub::{
14    api::sync::{ApiBuilder, ApiError, ApiRepo},
15    Cache, Repo, RepoType,
16};
17use serde::Deserialize;
18use std::io;
19use std::path::Path;
20use std::path::PathBuf;
21use std::sync::Arc;
22use std::sync::Mutex;
23use tracing::{debug, info, warn};
24
25/// Automatically selects the appropriate loader based on repository/config metadata.
26pub struct AutoLoader {
27    model_id: String,
28    normal_builder: Mutex<Option<NormalLoaderBuilder>>,
29    multimodal_builder: Mutex<Option<MultimodalLoaderBuilder>>,
30    embedding_builder: Mutex<Option<EmbeddingLoaderBuilder>>,
31    loader: Mutex<Option<Box<dyn Loader>>>,
32    hf_cache_path: Option<PathBuf>,
33}
34
35pub struct AutoLoaderBuilder {
36    normal_cfg: NormalSpecificConfig,
37    multimodal_cfg: MultimodalSpecificConfig,
38    embedding_cfg: EmbeddingSpecificConfig,
39    chat_template: Option<String>,
40    tokenizer_json: Option<String>,
41    model_id: String,
42    jinja_explicit: Option<String>,
43    no_kv_cache: bool,
44    xlora_model_id: Option<String>,
45    xlora_order: Option<Ordering>,
46    tgt_non_granular_index: Option<usize>,
47    lora_adapter_ids: Option<Vec<String>>,
48    hf_cache_path: Option<PathBuf>,
49}
50
51impl AutoLoaderBuilder {
52    #[allow(clippy::too_many_arguments)]
53    pub fn new(
54        normal_cfg: NormalSpecificConfig,
55        multimodal_cfg: MultimodalSpecificConfig,
56        embedding_cfg: EmbeddingSpecificConfig,
57        chat_template: Option<String>,
58        tokenizer_json: Option<String>,
59        model_id: String,
60        no_kv_cache: bool,
61        jinja_explicit: Option<String>,
62    ) -> Self {
63        Self {
64            normal_cfg,
65            multimodal_cfg,
66            embedding_cfg,
67            chat_template,
68            tokenizer_json,
69            model_id,
70            jinja_explicit,
71            no_kv_cache,
72            xlora_model_id: None,
73            xlora_order: None,
74            tgt_non_granular_index: None,
75            lora_adapter_ids: None,
76            hf_cache_path: None,
77        }
78    }
79
80    pub fn with_xlora(
81        mut self,
82        model_id: String,
83        order: Ordering,
84        no_kv_cache: bool,
85        tgt_non_granular_index: Option<usize>,
86    ) -> Self {
87        self.xlora_model_id = Some(model_id);
88        self.xlora_order = Some(order);
89        self.no_kv_cache = no_kv_cache;
90        self.tgt_non_granular_index = tgt_non_granular_index;
91        self
92    }
93
94    pub fn with_lora(mut self, adapters: Vec<String>) -> Self {
95        self.lora_adapter_ids = Some(adapters);
96        self
97    }
98
99    pub fn hf_cache_path(mut self, path: PathBuf) -> Self {
100        self.hf_cache_path = Some(path);
101        self
102    }
103
104    pub fn build(self) -> Box<dyn Loader> {
105        let Self {
106            normal_cfg,
107            multimodal_cfg,
108            embedding_cfg,
109            chat_template,
110            tokenizer_json,
111            model_id,
112            jinja_explicit,
113            no_kv_cache,
114            xlora_model_id,
115            xlora_order,
116            tgt_non_granular_index,
117            lora_adapter_ids,
118            hf_cache_path,
119        } = self;
120
121        let mut normal_builder = NormalLoaderBuilder::new(
122            normal_cfg,
123            chat_template.clone(),
124            tokenizer_json.clone(),
125            Some(model_id.clone()),
126            no_kv_cache,
127            jinja_explicit.clone(),
128        );
129        if let (Some(id), Some(ord)) = (xlora_model_id.clone(), xlora_order.clone()) {
130            normal_builder =
131                normal_builder.with_xlora(id, ord, no_kv_cache, tgt_non_granular_index);
132        }
133        if let Some(ref adapters) = lora_adapter_ids {
134            normal_builder = normal_builder.with_lora(adapters.clone());
135        }
136        if let Some(ref path) = hf_cache_path {
137            normal_builder = normal_builder.hf_cache_path(path.clone());
138        }
139
140        let mut multimodal_builder = MultimodalLoaderBuilder::new(
141            multimodal_cfg,
142            chat_template,
143            tokenizer_json.clone(),
144            Some(model_id.clone()),
145            jinja_explicit,
146        );
147        if let Some(ref adapters) = lora_adapter_ids {
148            multimodal_builder = multimodal_builder.with_lora(adapters.clone());
149        }
150        if let Some(ref path) = hf_cache_path {
151            multimodal_builder = multimodal_builder.hf_cache_path(path.clone());
152        }
153
154        let mut embedding_builder =
155            EmbeddingLoaderBuilder::new(embedding_cfg, tokenizer_json, Some(model_id.clone()));
156        if let Some(ref adapters) = lora_adapter_ids {
157            embedding_builder = embedding_builder.with_lora(adapters.clone());
158        }
159        if let Some(ref path) = hf_cache_path {
160            embedding_builder = embedding_builder.hf_cache_path(path.clone());
161        }
162
163        Box::new(AutoLoader {
164            model_id,
165            normal_builder: Mutex::new(Some(normal_builder)),
166            multimodal_builder: Mutex::new(Some(multimodal_builder)),
167            embedding_builder: Mutex::new(Some(embedding_builder)),
168            loader: Mutex::new(None),
169            hf_cache_path,
170        })
171    }
172}
173
174#[derive(Deserialize)]
175struct AutoConfig {
176    #[serde(default)]
177    architectures: Vec<String>,
178}
179
180struct ConfigArtifacts {
181    contents: Option<String>,
182    sentence_transformers_present: bool,
183    repo_files: Vec<String>,
184    remote_access_issue: Option<RemoteAccessIssue>,
185}
186
187enum Detected {
188    Normal(NormalLoaderType),
189    Multimodal(MultimodalLoaderType),
190    Embedding(Option<EmbeddingLoaderType>),
191    Diffusion(DiffusionLoaderType),
192    Speech(crate::speech_models::SpeechLoaderType),
193}
194
195impl AutoLoader {
196    fn try_get_file(
197        api: &ApiRepo,
198        model_id: &Path,
199        file: &str,
200    ) -> std::result::Result<Option<PathBuf>, ApiError> {
201        if model_id.exists() {
202            let path = model_id.join(file);
203            if path.exists() {
204                info!("Loading `{}` locally at `{}`", file, path.display());
205                Ok(Some(path))
206            } else {
207                Ok(None)
208            }
209        } else {
210            api.get(file).map(Some)
211        }
212    }
213
214    fn list_local_repo_files(model_root: &Path) -> Vec<String> {
215        fn collect_files(root: &Path, dir: &Path, out: &mut Vec<String>) -> io::Result<()> {
216            for entry in std::fs::read_dir(dir)? {
217                let entry = entry?;
218                let path = entry.path();
219                if path.is_dir() {
220                    collect_files(root, &path, out)?;
221                } else if let Ok(rel) = path.strip_prefix(root) {
222                    out.push(rel.to_string_lossy().replace('\\', "/"));
223                }
224            }
225            Ok(())
226        }
227
228        if !model_root.is_dir() {
229            return Vec::new();
230        }
231
232        let mut files = Vec::new();
233        if collect_files(model_root, model_root, &mut files).is_err() {
234            return Vec::new();
235        }
236        files
237    }
238
239    fn read_config_from_path(&self, paths: &dyn ModelPaths) -> Result<ConfigArtifacts> {
240        let config_path = paths.get_config_filename();
241        let contents = match std::fs::read_to_string(config_path) {
242            Ok(contents) => Some(contents),
243            Err(err) if err.kind() == io::ErrorKind::NotFound => None,
244            Err(err) => return Err(err.into()),
245        };
246        let model_root = Path::new(&self.model_id);
247        let repo_files = if model_root.exists() {
248            Self::list_local_repo_files(model_root)
249        } else {
250            Vec::new()
251        };
252        let sentence_transformers_present = Self::has_sentence_transformers_sibling(config_path)
253            || repo_files
254                .iter()
255                .any(|f| f == "config_sentence_transformers.json");
256        Ok(ConfigArtifacts {
257            contents,
258            sentence_transformers_present,
259            repo_files,
260            remote_access_issue: None,
261        })
262    }
263
264    fn read_config_from_hf(
265        &self,
266        revision: Option<String>,
267        token_source: &TokenSource,
268        silent: bool,
269    ) -> Result<ConfigArtifacts> {
270        let cache = self
271            .hf_cache_path
272            .clone()
273            .map(Cache::new)
274            .unwrap_or_default();
275        let mut api = ApiBuilder::from_cache(cache)
276            .with_progress(!silent)
277            .with_token(get_token(token_source)?);
278        if let Some(cache_dir) = crate::hf_hub_cache_dir() {
279            api = api.with_cache_dir(cache_dir);
280        }
281        let api = api.build()?;
282        let revision = revision.unwrap_or_else(|| "main".to_string());
283        let api = api.repo(Repo::with_revision(
284            self.model_id.clone(),
285            RepoType::Model,
286            revision,
287        ));
288        let model_id = Path::new(&self.model_id);
289        let mut remote_access_issue = None;
290        let contents = match Self::try_get_file(&api, model_id, "config.json") {
291            Ok(Some(path)) => Some(std::fs::read_to_string(&path)?),
292            Ok(None) => None,
293            Err(err) => {
294                let issue = remote_issue_from_api_error(model_id, Some("config.json"), &err);
295                warn!(
296                    "Auto loader could not fetch `config.json` for `{}`: {}",
297                    self.model_id, issue.message
298                );
299                remote_access_issue = Some(issue);
300                None
301            }
302        };
303        let sentence_transformers_present =
304            model_id.join("config_sentence_transformers.json").exists()
305                || Self::fetch_sentence_transformers_config(&api, model_id);
306        let repo_files = if model_id.exists() {
307            Self::list_local_repo_files(model_id)
308        } else {
309            crate::api_dir_list!(api, model_id, false).collect::<Vec<_>>()
310        };
311        Ok(ConfigArtifacts {
312            contents,
313            sentence_transformers_present,
314            repo_files,
315            remote_access_issue,
316        })
317    }
318
319    fn has_sentence_transformers_sibling(config_path: &Path) -> bool {
320        config_path
321            .parent()
322            .map(|parent| parent.join("config_sentence_transformers.json").exists())
323            .unwrap_or(false)
324    }
325
326    fn fetch_sentence_transformers_config(api: &ApiRepo, model_id: &Path) -> bool {
327        if model_id.exists() {
328            return false;
329        }
330        match api.get("config_sentence_transformers.json") {
331            Ok(_) => true,
332            Err(err) => {
333                debug!(
334                    "No `config_sentence_transformers.json` found for `{}`: {err}",
335                    model_id.display()
336                );
337                false
338            }
339        }
340    }
341
342    fn detect(&self, artifacts: &ConfigArtifacts) -> Result<Detected> {
343        if let Some(tp) = DiffusionLoaderType::auto_detect_from_files(&artifacts.repo_files) {
344            return Ok(Detected::Diffusion(tp));
345        }
346
347        if let Some(ref config) = artifacts.contents {
348            if let Some(tp) =
349                crate::speech_models::SpeechLoaderType::auto_detect_from_config(config)
350            {
351                return Ok(Detected::Speech(tp));
352            }
353        }
354
355        if artifacts.sentence_transformers_present {
356            if let Some(ref config) = artifacts.contents {
357                let cfg: AutoConfig = serde_json::from_str(config)?;
358                if let Some(name) = cfg.architectures.first() {
359                    if let Ok(tp) = EmbeddingLoaderType::from_causal_lm_name(name) {
360                        info!(
361                            "Detected `config_sentence_transformers.json`; using embedding loader `{tp}`."
362                        );
363                        return Ok(Detected::Embedding(Some(tp)));
364                    }
365                }
366            }
367            if artifacts.contents.is_none() {
368                if let Some(issue) = artifacts.remote_access_issue.as_ref() {
369                    return Err(hf_access_error(Path::new(&self.model_id), issue));
370                }
371            }
372            info!(
373                "Detected `config_sentence_transformers.json`; routing via auto embedding loader."
374            );
375            return Ok(Detected::Embedding(None));
376        }
377
378        // Detect Mistral-native models that use params.json instead of config.json
379        if artifacts.contents.is_none() && artifacts.repo_files.iter().any(|f| f == "params.json") {
380            // Voxtral uses params.json with a "multimodal" key containing "whisper_model_args"
381            info!("Detected `params.json` in repo; routing as Voxtral.");
382            return Ok(Detected::Multimodal(MultimodalLoaderType::Voxtral));
383        }
384
385        let config = artifacts.contents.as_ref().ok_or_else(|| {
386            if let Some(issue) = artifacts.remote_access_issue.as_ref() {
387                hf_access_error(Path::new(&self.model_id), issue)
388            } else {
389                anyhow::anyhow!(
390                    "Auto loader could not determine model type: missing `config.json` and no diffusion/speech markers found."
391                )
392            }
393        })?;
394        let cfg: AutoConfig = serde_json::from_str(config)?;
395        if cfg.architectures.len() != 1 {
396            anyhow::bail!("Expected exactly one architecture in config");
397        }
398        let name = &cfg.architectures[0];
399        if let Ok(tp) = MultimodalLoaderType::from_causal_lm_name(name) {
400            return Ok(Detected::Multimodal(tp));
401        }
402        let tp = NormalLoaderType::from_causal_lm_name(name)?;
403        Ok(Detected::Normal(tp))
404    }
405
406    fn ensure_loader(&self, artifacts: &ConfigArtifacts) -> Result<()> {
407        let mut guard = self.loader.lock().unwrap();
408        if guard.is_some() {
409            return Ok(());
410        }
411        match self.detect(artifacts)? {
412            Detected::Normal(tp) => {
413                let builder = self
414                    .normal_builder
415                    .lock()
416                    .unwrap()
417                    .take()
418                    .expect("builder taken");
419                let loader = builder.build(Some(tp)).expect("build normal");
420                *guard = Some(loader);
421            }
422            Detected::Multimodal(tp) => {
423                let builder = self
424                    .multimodal_builder
425                    .lock()
426                    .unwrap()
427                    .take()
428                    .expect("builder taken");
429                let loader = builder.build(Some(tp));
430                *guard = Some(loader);
431            }
432            Detected::Embedding(tp) => {
433                let builder = self
434                    .embedding_builder
435                    .lock()
436                    .unwrap()
437                    .take()
438                    .expect("builder taken");
439                let loader = builder.build(tp);
440                *guard = Some(loader);
441            }
442            Detected::Diffusion(tp) => {
443                let loader = DiffusionLoaderBuilder::new(Some(self.model_id.clone())).build(tp);
444                *guard = Some(loader);
445            }
446            Detected::Speech(tp) => {
447                let loader: Box<dyn Loader> = Box::new(SpeechLoader {
448                    model_id: self.model_id.clone(),
449                    dac_model_id: None,
450                    arch: tp,
451                    cfg: None,
452                });
453                *guard = Some(loader);
454            }
455        }
456        Ok(())
457    }
458}
459
460impl Loader for AutoLoader {
461    #[allow(clippy::type_complexity, clippy::too_many_arguments)]
462    fn load_model_from_hf(
463        &self,
464        revision: Option<String>,
465        token_source: TokenSource,
466        dtype: &dyn TryIntoDType,
467        device: &Device,
468        silent: bool,
469        mapper: DeviceMapSetting,
470        in_situ_quant: Option<IsqType>,
471        paged_attn_config: Option<PagedAttentionConfig>,
472    ) -> Result<Arc<tokio::sync::Mutex<dyn Pipeline + Send + Sync>>> {
473        let _progress_guard = ProgressScopeGuard::new(silent);
474        let config = self.read_config_from_hf(revision.clone(), &token_source, silent)?;
475        self.ensure_loader(&config)?;
476        self.loader
477            .lock()
478            .unwrap()
479            .as_ref()
480            .unwrap()
481            .load_model_from_hf(
482                revision,
483                token_source,
484                dtype,
485                device,
486                silent,
487                mapper,
488                in_situ_quant,
489                paged_attn_config,
490            )
491    }
492
493    #[allow(clippy::type_complexity, clippy::too_many_arguments)]
494    fn load_model_from_path(
495        &self,
496        paths: &Box<dyn ModelPaths>,
497        dtype: &dyn TryIntoDType,
498        device: &Device,
499        silent: bool,
500        mapper: DeviceMapSetting,
501        in_situ_quant: Option<IsqType>,
502        paged_attn_config: Option<PagedAttentionConfig>,
503    ) -> Result<Arc<tokio::sync::Mutex<dyn Pipeline + Send + Sync>>> {
504        let _progress_guard = ProgressScopeGuard::new(silent);
505        let config = self.read_config_from_path(paths.as_ref())?;
506        self.ensure_loader(&config)?;
507        self.loader
508            .lock()
509            .unwrap()
510            .as_ref()
511            .unwrap()
512            .load_model_from_path(
513                paths,
514                dtype,
515                device,
516                silent,
517                mapper,
518                in_situ_quant,
519                paged_attn_config,
520            )
521    }
522
523    fn get_id(&self) -> String {
524        self.model_id.clone()
525    }
526
527    fn get_kind(&self) -> ModelKind {
528        self.loader
529            .lock()
530            .unwrap()
531            .as_ref()
532            .map(|l| l.get_kind())
533            .unwrap_or(ModelKind::Normal)
534    }
535}