Skip to main content

rig_candle/
profile.rs

1//! Central definitions for the model/checkpoint combinations Rig can execute.
2
3use std::collections::HashSet;
4
5use candle_core::quantized::GgmlDType;
6use serde::{Deserialize, Serialize};
7
8use crate::CandleError;
9
10pub const BEGIN_OF_TEXT: &str = "<|begin_of_text|>";
11pub const START_HEADER: &str = "<|start_header_id|>";
12pub const END_HEADER: &str = "<|end_header_id|>";
13pub const END_OF_TURN: &str = "<|eot_id|>";
14pub const IM_START: &str = "<|im_start|>";
15pub const IM_END: &str = "<|im_end|>";
16pub(crate) const END_OF_TEXT: &str = "<|endoftext|>";
17pub const SMOLLM2_DEFAULT_SYSTEM_PROMPT: &str =
18    "You are a helpful AI assistant named SmolLM, trained by Hugging Face";
19
20/// Explicit conversation and generated-output protocol selected from validated artifacts.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum ConversationProtocol {
24    /// Meta Llama 3 instruct control-token format.
25    Llama3,
26    /// Hugging Face SmolLM2 instruct (ChatML-style) format.
27    SmolLm2,
28    /// Qwen3 ChatML/Hermes tool-calling format.
29    Qwen3,
30}
31
32/// Backwards-compatible name for [`ConversationProtocol`].
33pub type ModelFamily = ConversationProtocol;
34
35/// Transformer architecture used to execute a loaded checkpoint.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub enum ModelArchitecture {
39    /// Candle's Llama implementation, including compatible SmolLM2 checkpoints.
40    Llama,
41    /// Candle's Qwen3 implementation with per-head query/key normalization.
42    Qwen3,
43}
44
45/// Quantized tensor encoding detected in a GGUF checkpoint.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum Quantization {
49    /// Mixed GGUF tensors whose primary matrix encoding is Q4_K.
50    Q4K,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub(crate) enum ArtifactFormat {
55    Safetensors,
56    Gguf,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub(crate) enum LoaderBackend {
61    LlamaSafetensors,
62    LlamaGguf,
63    Qwen3Gguf,
64}
65
66#[derive(Debug)]
67pub(crate) struct ConfigIdentity {
68    pub(crate) model_type: &'static str,
69    pub(crate) architecture: &'static str,
70    pub(crate) required: bool,
71}
72
73#[derive(Debug, Clone, Copy)]
74pub(crate) struct DimensionRequirement {
75    pub(crate) field: &'static str,
76    pub(crate) value: usize,
77}
78
79#[derive(Debug)]
80pub(crate) struct ConfigRequirements {
81    pub(crate) hidden_act: Option<&'static str>,
82    pub(crate) attention_bias: Option<bool>,
83    pub(crate) mlp_bias: Option<bool>,
84    pub(crate) rope_interleaved: Option<bool>,
85    pub(crate) tie_word_embeddings: Option<bool>,
86    pub(crate) rms_norm_eps: Option<f64>,
87    pub(crate) rope_theta: Option<f64>,
88    pub(crate) bos_token_id: Option<u32>,
89    pub(crate) eos_token_id: Option<u32>,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub(crate) enum TokenizerVocabulary {
94    ModelCapacity,
95    Exact(usize),
96}
97
98#[derive(Debug)]
99pub(crate) struct MetadataStringRequirement {
100    pub(crate) key: &'static str,
101    pub(crate) value: &'static str,
102}
103
104#[derive(Debug)]
105pub(crate) struct GgufRequirements {
106    pub(crate) file_type: u32,
107    pub(crate) quantization_version: usize,
108    pub(crate) metadata_strings: &'static [MetadataStringRequirement],
109    pub(crate) chat_template_markers: &'static [&'static str],
110    pub(crate) allowed_tensor_dtypes: &'static [GgmlDType],
111    pub(crate) token_embedding_dtypes: &'static [GgmlDType],
112    pub(crate) norm_dtypes: &'static [GgmlDType],
113    pub(crate) matrix_dtypes: &'static [GgmlDType],
114    pub(crate) mixed_matrix_dtypes: &'static [GgmlDType],
115    pub(crate) tensors_per_layer: Option<usize>,
116}
117
118#[derive(Debug)]
119pub(crate) struct ProfileDefinition {
120    pub(crate) name: &'static str,
121    pub(crate) architecture: ModelArchitecture,
122    pub(crate) protocol: ConversationProtocol,
123    pub(crate) artifact_format: ArtifactFormat,
124    pub(crate) loader: LoaderBackend,
125    pub(crate) quantization: Option<Quantization>,
126    pub(crate) config_identity: ConfigIdentity,
127    pub(crate) config_dimensions: &'static [DimensionRequirement],
128    pub(crate) config_requirements: ConfigRequirements,
129    pub(crate) tokenizer_tokens: &'static [&'static str],
130    pub(crate) tokenizer_vocabulary: TokenizerVocabulary,
131    pub(crate) start_token: &'static str,
132    pub(crate) end_token: &'static str,
133    pub(crate) context_limit_cap: Option<usize>,
134    pub(crate) gguf: Option<GgufRequirements>,
135}
136
137const LLAMA3_PROFILE: ProfileDefinition = ProfileDefinition {
138    name: "Llama 3 safetensors",
139    architecture: ModelArchitecture::Llama,
140    protocol: ConversationProtocol::Llama3,
141    artifact_format: ArtifactFormat::Safetensors,
142    loader: LoaderBackend::LlamaSafetensors,
143    quantization: None,
144    config_identity: ConfigIdentity {
145        model_type: "llama",
146        architecture: "LlamaForCausalLM",
147        required: false,
148    },
149    config_dimensions: &[],
150    config_requirements: ConfigRequirements {
151        hidden_act: None,
152        attention_bias: None,
153        mlp_bias: None,
154        rope_interleaved: None,
155        tie_word_embeddings: None,
156        rms_norm_eps: None,
157        rope_theta: None,
158        bos_token_id: None,
159        eos_token_id: None,
160    },
161    tokenizer_tokens: &[
162        "<|begin_of_text|>",
163        "<|start_header_id|>",
164        "<|end_header_id|>",
165        "<|eot_id|>",
166    ],
167    tokenizer_vocabulary: TokenizerVocabulary::ModelCapacity,
168    start_token: BEGIN_OF_TEXT,
169    end_token: END_OF_TURN,
170    context_limit_cap: None,
171    gguf: None,
172};
173
174const SMOLLM2_PROFILE: ProfileDefinition = ProfileDefinition {
175    name: "SmolLM2-360M-Instruct Q4_K_M GGUF",
176    architecture: ModelArchitecture::Llama,
177    protocol: ConversationProtocol::SmolLm2,
178    artifact_format: ArtifactFormat::Gguf,
179    loader: LoaderBackend::LlamaGguf,
180    quantization: Some(Quantization::Q4K),
181    config_identity: ConfigIdentity {
182        model_type: "llama",
183        architecture: "LlamaForCausalLM",
184        required: true,
185    },
186    config_dimensions: &[
187        DimensionRequirement {
188            field: "hidden_size",
189            value: 960,
190        },
191        DimensionRequirement {
192            field: "intermediate_size",
193            value: 2560,
194        },
195        DimensionRequirement {
196            field: "vocab_size",
197            value: 49_152,
198        },
199        DimensionRequirement {
200            field: "num_hidden_layers",
201            value: 32,
202        },
203        DimensionRequirement {
204            field: "num_attention_heads",
205            value: 15,
206        },
207        DimensionRequirement {
208            field: "num_key_value_heads",
209            value: 5,
210        },
211        DimensionRequirement {
212            field: "max_position_embeddings",
213            value: 8192,
214        },
215    ],
216    config_requirements: ConfigRequirements {
217        hidden_act: Some("silu"),
218        attention_bias: Some(false),
219        mlp_bias: Some(false),
220        rope_interleaved: Some(false),
221        tie_word_embeddings: Some(true),
222        rms_norm_eps: Some(1e-5),
223        rope_theta: Some(100_000.0),
224        bos_token_id: None,
225        eos_token_id: None,
226    },
227    tokenizer_tokens: &["<|im_start|>", "<|im_end|>"],
228    tokenizer_vocabulary: TokenizerVocabulary::ModelCapacity,
229    start_token: IM_START,
230    end_token: IM_END,
231    context_limit_cap: Some(4096),
232    gguf: Some(GgufRequirements {
233        file_type: 15,
234        quantization_version: 2,
235        metadata_strings: &[
236            MetadataStringRequirement {
237                key: "general.basename",
238                value: "smollm2",
239            },
240            MetadataStringRequirement {
241                key: "tokenizer.ggml.model",
242                value: "gpt2",
243            },
244            MetadataStringRequirement {
245                key: "tokenizer.ggml.pre",
246                value: "smollm",
247            },
248        ],
249        chat_template_markers: &[],
250        allowed_tensor_dtypes: &[
251            GgmlDType::F32,
252            GgmlDType::Q4K,
253            GgmlDType::Q5_0,
254            GgmlDType::Q6K,
255            GgmlDType::Q8_0,
256        ],
257        token_embedding_dtypes: &[],
258        norm_dtypes: &[],
259        matrix_dtypes: &[],
260        mixed_matrix_dtypes: &[],
261        tensors_per_layer: None,
262    }),
263};
264
265const QWEN3_PROFILE: ProfileDefinition = ProfileDefinition {
266    name: "Qwen3-4B Q4_K_M GGUF",
267    architecture: ModelArchitecture::Qwen3,
268    protocol: ConversationProtocol::Qwen3,
269    artifact_format: ArtifactFormat::Gguf,
270    loader: LoaderBackend::Qwen3Gguf,
271    quantization: Some(Quantization::Q4K),
272    config_identity: ConfigIdentity {
273        model_type: "qwen3",
274        architecture: "Qwen3ForCausalLM",
275        required: true,
276    },
277    config_dimensions: &[
278        DimensionRequirement {
279            field: "hidden_size",
280            value: 2560,
281        },
282        DimensionRequirement {
283            field: "intermediate_size",
284            value: 9728,
285        },
286        DimensionRequirement {
287            field: "num_hidden_layers",
288            value: 36,
289        },
290        DimensionRequirement {
291            field: "num_attention_heads",
292            value: 32,
293        },
294        DimensionRequirement {
295            field: "num_key_value_heads",
296            value: 8,
297        },
298        DimensionRequirement {
299            field: "head_dim",
300            value: 128,
301        },
302        DimensionRequirement {
303            field: "max_position_embeddings",
304            value: 40_960,
305        },
306        DimensionRequirement {
307            field: "vocab_size",
308            value: 151_936,
309        },
310    ],
311    config_requirements: ConfigRequirements {
312        hidden_act: Some("silu"),
313        attention_bias: Some(false),
314        mlp_bias: None,
315        rope_interleaved: None,
316        tie_word_embeddings: Some(true),
317        rms_norm_eps: Some(1e-6),
318        rope_theta: Some(1_000_000.0),
319        bos_token_id: Some(151_643),
320        eos_token_id: Some(151_645),
321    },
322    tokenizer_tokens: &["<|endoftext|>", "<|im_start|>", "<|im_end|>"],
323    tokenizer_vocabulary: TokenizerVocabulary::Exact(151_669),
324    start_token: END_OF_TEXT,
325    end_token: IM_END,
326    context_limit_cap: Some(4096),
327    gguf: Some(GgufRequirements {
328        file_type: 15,
329        quantization_version: 2,
330        metadata_strings: &[
331            MetadataStringRequirement {
332                key: "general.basename",
333                value: "qwen3",
334            },
335            MetadataStringRequirement {
336                key: "general.size_label",
337                value: "4b",
338            },
339            MetadataStringRequirement {
340                key: "general.finetune",
341                value: "instruct-awq",
342            },
343            MetadataStringRequirement {
344                key: "tokenizer.ggml.model",
345                value: "gpt2",
346            },
347            MetadataStringRequirement {
348                key: "tokenizer.ggml.pre",
349                value: "qwen2",
350            },
351        ],
352        chat_template_markers: &[
353            "# Tools",
354            "<tools></tools>",
355            "<tool_call>",
356            "<tool_response>",
357            "enable_thinking",
358        ],
359        allowed_tensor_dtypes: &[GgmlDType::F32, GgmlDType::Q4K, GgmlDType::Q6K],
360        token_embedding_dtypes: &[GgmlDType::Q6K],
361        norm_dtypes: &[GgmlDType::F32],
362        matrix_dtypes: &[GgmlDType::Q4K],
363        mixed_matrix_dtypes: &[GgmlDType::Q4K, GgmlDType::Q6K],
364        tensors_per_layer: Some(11),
365    }),
366};
367
368#[derive(Debug, Clone)]
369pub(crate) struct ValidatedProfile {
370    pub(crate) definition: &'static ProfileDefinition,
371    pub(crate) vocab_size: usize,
372    pub(crate) context_limit: usize,
373    pub(crate) stop_tokens: HashSet<u32>,
374}
375
376impl ValidatedProfile {
377    pub(crate) fn new(
378        definition: &'static ProfileDefinition,
379        vocab_size: usize,
380        configured_context_limit: usize,
381        stop_tokens: HashSet<u32>,
382    ) -> Result<Self, CandleError> {
383        if stop_tokens.is_empty() {
384            return Err(CandleError::MissingStopToken);
385        }
386        let context_limit = definition
387            .context_limit_cap
388            .map_or(configured_context_limit, |cap| {
389                configured_context_limit.min(cap)
390            });
391        Ok(Self {
392            definition,
393            vocab_size,
394            context_limit,
395            stop_tokens,
396        })
397    }
398}
399
400pub(crate) fn definition_for(
401    protocol: ConversationProtocol,
402    artifact_format: ArtifactFormat,
403) -> Result<&'static ProfileDefinition, CandleError> {
404    let definition = match protocol {
405        ConversationProtocol::Llama3 => &LLAMA3_PROFILE,
406        ConversationProtocol::SmolLm2 => &SMOLLM2_PROFILE,
407        ConversationProtocol::Qwen3 => &QWEN3_PROFILE,
408    };
409    if definition.artifact_format != artifact_format {
410        return Err(CandleError::UnsupportedModelFamily(format!(
411            "{} requires {:?} artifacts",
412            definition.name, definition.artifact_format
413        )));
414    }
415    Ok(definition)
416}
417
418pub(crate) fn validate_identity(
419    definition: &ProfileDefinition,
420    model_type: Option<&str>,
421    architectures: &[String],
422) -> Result<(), CandleError> {
423    let expected = &definition.config_identity;
424    let model_type_mismatch = model_type
425        .is_some_and(|model_type| model_type != expected.model_type)
426        || (expected.required && model_type.is_none());
427    let architecture_mismatch = (!architectures.is_empty()
428        && !architectures
429            .iter()
430            .any(|architecture| architecture == expected.architecture))
431        || (expected.required && architectures.is_empty());
432    if model_type_mismatch || architecture_mismatch {
433        return Err(CandleError::UnsupportedModelFamily(format!(
434            "{} requires model_type `{}` and architecture `{}`",
435            definition.name, expected.model_type, expected.architecture
436        )));
437    }
438    Ok(())
439}
440
441pub(crate) fn validate_dimensions(
442    definition: &ProfileDefinition,
443    actual: &[(&'static str, usize)],
444) -> Result<(), CandleError> {
445    for requirement in definition.config_dimensions {
446        let actual = actual
447            .iter()
448            .find_map(|(field, value)| (*field == requirement.field).then_some(*value))
449            .ok_or_else(|| {
450                CandleError::Configuration(format!(
451                    "internal profile validation omitted `{}`",
452                    requirement.field
453                ))
454            })?;
455        if actual != requirement.value {
456            return Err(CandleError::ArtifactMismatch {
457                artifact: "config.json",
458                reason: format!(
459                    "{} requires {}={}, found {actual}",
460                    definition.name, requirement.field, requirement.value
461                ),
462            });
463        }
464    }
465    Ok(())
466}
467
468pub(crate) struct ConfigValues<'a> {
469    pub(crate) hidden_act: Option<&'a str>,
470    pub(crate) attention_bias: Option<bool>,
471    pub(crate) mlp_bias: Option<bool>,
472    pub(crate) rope_interleaved: Option<bool>,
473    pub(crate) tie_word_embeddings: bool,
474    pub(crate) rms_norm_eps: f64,
475    pub(crate) rope_theta: f64,
476    pub(crate) bos_token_id: Option<u32>,
477    pub(crate) eos_token_id: Option<u32>,
478}
479
480pub(crate) fn validate_config_requirements(
481    definition: &ProfileDefinition,
482    actual: &ConfigValues<'_>,
483) -> Result<(), CandleError> {
484    let expected = &definition.config_requirements;
485    let mismatch = expected
486        .hidden_act
487        .is_some_and(|value| actual.hidden_act != Some(value))
488        || expected
489            .attention_bias
490            .is_some_and(|value| actual.attention_bias != Some(value))
491        || expected
492            .mlp_bias
493            .is_some_and(|value| actual.mlp_bias != Some(value))
494        || expected
495            .rope_interleaved
496            .is_some_and(|value| actual.rope_interleaved != Some(value))
497        || expected
498            .tie_word_embeddings
499            .is_some_and(|value| actual.tie_word_embeddings != value)
500        || expected
501            .rms_norm_eps
502            .is_some_and(|value| (actual.rms_norm_eps - value).abs() > f64::EPSILON)
503        || expected
504            .rope_theta
505            .is_some_and(|value| (actual.rope_theta - value).abs() > f64::EPSILON)
506        || expected
507            .bos_token_id
508            .is_some_and(|value| actual.bos_token_id != Some(value))
509        || expected
510            .eos_token_id
511            .is_some_and(|value| actual.eos_token_id != Some(value));
512    if mismatch {
513        return Err(CandleError::ArtifactMismatch {
514            artifact: "config.json",
515            reason: format!(
516                "{} configuration invariants do not match its validated profile",
517                definition.name
518            ),
519        });
520    }
521    Ok(())
522}
523
524pub(crate) fn validate_tokenizer_requirements(
525    definition: &ProfileDefinition,
526    tokenizer: &tokenizers::Tokenizer,
527    vocab_size: usize,
528    configured_bos: Option<u32>,
529    configured_eos: &[u32],
530) -> Result<(), CandleError> {
531    let actual_vocabulary = tokenizer.get_vocab_size(true);
532    match definition.tokenizer_vocabulary {
533        TokenizerVocabulary::ModelCapacity if actual_vocabulary != vocab_size => {
534            return Err(CandleError::TokenizerVocabularyMismatch {
535                expected: vocab_size,
536                actual: actual_vocabulary,
537            });
538        }
539        TokenizerVocabulary::Exact(expected) if actual_vocabulary != expected => {
540            return Err(CandleError::ArtifactMismatch {
541                artifact: "tokenizer.json",
542                reason: format!(
543                    "{} requires {expected} defined tokenizer IDs within model capacity {vocab_size}, found {actual_vocabulary}",
544                    definition.name
545                ),
546            });
547        }
548        _ if actual_vocabulary > vocab_size => {
549            return Err(CandleError::TokenizerVocabularyMismatch {
550                expected: vocab_size,
551                actual: actual_vocabulary,
552            });
553        }
554        _ => {}
555    }
556    for &token in definition.tokenizer_tokens {
557        let id = tokenizer
558            .token_to_id(token)
559            .ok_or(CandleError::MissingSpecialToken { token })?;
560        if id as usize >= vocab_size {
561            return Err(CandleError::TokenIdOutOfRange {
562                token: token.to_string(),
563                id,
564                vocab_size,
565            });
566        }
567        if !tokenizer.get_added_vocabulary().is_special_token(token) {
568            return Err(CandleError::SpecialTokenNotMarked { token });
569        }
570    }
571    let start_id =
572        tokenizer
573            .token_to_id(definition.start_token)
574            .ok_or(CandleError::MissingSpecialToken {
575                token: definition.start_token,
576            })?;
577    if configured_bos.is_some_and(|configured| configured != start_id) {
578        return Err(CandleError::ArtifactMismatch {
579            artifact: "bos_token_id",
580            reason: format!(
581                "configured BOS ID does not match '{}' ID {start_id}",
582                definition.start_token
583            ),
584        });
585    }
586    let end_id =
587        tokenizer
588            .token_to_id(definition.end_token)
589            .ok_or(CandleError::MissingSpecialToken {
590                token: definition.end_token,
591            })?;
592    if !configured_eos.is_empty() && !configured_eos.contains(&end_id) {
593        return Err(CandleError::ArtifactMismatch {
594            artifact: "eos_token_id",
595            reason: format!(
596                "configured EOS IDs do not contain '{}' ID {end_id}",
597                definition.end_token
598            ),
599        });
600    }
601    Ok(())
602}
603
604#[cfg(test)]
605#[allow(clippy::panic_in_result_fn)]
606mod tests {
607    use std::collections::HashSet;
608
609    use super::*;
610
611    #[test]
612    fn supported_profiles_centralize_backend_format_and_context() -> Result<(), CandleError> {
613        let llama = definition_for(ConversationProtocol::Llama3, ArtifactFormat::Safetensors)?;
614        assert_eq!(llama.loader, LoaderBackend::LlamaSafetensors);
615        assert_eq!(llama.architecture, ModelArchitecture::Llama);
616        assert_eq!(llama.quantization, None);
617        assert_eq!(llama.context_limit_cap, None);
618        assert_eq!(
619            llama.tokenizer_vocabulary,
620            TokenizerVocabulary::ModelCapacity
621        );
622        assert_eq!(llama.start_token, BEGIN_OF_TEXT);
623        assert_eq!(llama.end_token, END_OF_TURN);
624        assert!(llama.gguf.is_none());
625
626        let smol = definition_for(ConversationProtocol::SmolLm2, ArtifactFormat::Gguf)?;
627        assert_eq!(smol.loader, LoaderBackend::LlamaGguf);
628        assert_eq!(smol.quantization, Some(Quantization::Q4K));
629        let smol = ValidatedProfile::new(smol, 49_152, 8192, HashSet::from([1]))?;
630        assert_eq!(smol.context_limit, 4096);
631        assert_eq!(
632            smol.definition.config_requirements.rope_theta,
633            Some(100_000.0)
634        );
635        assert_eq!(smol.definition.end_token, IM_END);
636        assert!(smol.definition.gguf.as_ref().is_some_and(|requirements| {
637            requirements
638                .allowed_tensor_dtypes
639                .contains(&GgmlDType::Q5_0)
640        }));
641
642        let qwen = definition_for(ConversationProtocol::Qwen3, ArtifactFormat::Gguf)?;
643        assert_eq!(qwen.loader, LoaderBackend::Qwen3Gguf);
644        assert_eq!(qwen.architecture, ModelArchitecture::Qwen3);
645        assert_eq!(qwen.quantization, Some(Quantization::Q4K));
646        let qwen = ValidatedProfile::new(qwen, 151_936, 40_960, HashSet::from([151_645]))?;
647        assert_eq!(qwen.context_limit, 4096);
648        assert_eq!(
649            qwen.definition.tokenizer_vocabulary,
650            TokenizerVocabulary::Exact(151_669)
651        );
652        assert_eq!(
653            qwen.definition.config_requirements.eos_token_id,
654            Some(151_645)
655        );
656        assert!(
657            qwen.definition
658                .gguf
659                .as_ref()
660                .is_some_and(|requirements| requirements.tensors_per_layer == Some(11)
661                    && requirements.chat_template_markers.contains(&"<tool_call>"))
662        );
663        Ok(())
664    }
665
666    #[test]
667    fn profiles_reject_unsupported_artifact_combinations_and_empty_stops() {
668        assert!(matches!(
669            definition_for(ConversationProtocol::SmolLm2, ArtifactFormat::Safetensors),
670            Err(CandleError::UnsupportedModelFamily(_))
671        ));
672        assert!(matches!(
673            definition_for(ConversationProtocol::Qwen3, ArtifactFormat::Safetensors),
674            Err(CandleError::UnsupportedModelFamily(_))
675        ));
676        assert!(matches!(
677            definition_for(ConversationProtocol::Llama3, ArtifactFormat::Gguf),
678            Err(CandleError::UnsupportedModelFamily(_))
679        ));
680        assert!(matches!(
681            ValidatedProfile::new(&LLAMA3_PROFILE, 8, 16, HashSet::new()),
682            Err(CandleError::MissingStopToken)
683        ));
684    }
685}