Skip to main content

candding/
error.rs

1//! Error type shared by the whole crate. The enum is here; the length budgets and the device refusals it names,
2//! with their messages, have a submodule each.
3
4mod device;
5mod length;
6mod memory;
7#[cfg(test)]
8mod tests;
9
10use std::path::PathBuf;
11
12use candle_core::DType;
13pub use length::LengthKind;
14use thiserror::Error;
15
16use crate::registry::Backend;
17use crate::registry::Catalog;
18
19#[derive(Debug, Error)]
20#[non_exhaustive]
21pub enum CanddingError {
22  /// A model the cache lacks, in a build without the `download` feature; `hint` is the `candding fetch` command.
23  #[error(
24    "model `{id}` is not in the hugging face cache, and this build of candding cannot download it: enable its `download` feature, or fetch the model with the candding CLI, `{hint}`"
25  )]
26  ModelNotFound { id: String, hint: String },
27  #[error(
28    "model `{id}` is not in the hugging face cache, and HF_HUB_OFFLINE forbids downloading it"
29  )]
30  Offline { id: String },
31  #[error(
32    "model `{id}` is gated: accept its license on huggingface.co, then set HF_TOKEN to an access token of that account"
33  )]
34  GatedModel { id: String },
35  /// The hub answers an anonymous request for a private repository the same way as for a missing one.
36  #[error(
37    "model `{id}` was not found on the hugging face hub: check the id, or set HF_TOKEN if the repository is private"
38  )]
39  RepositoryNotFound { id: String },
40  #[error("model `{id}` has no revision `{revision}` on the hugging face hub")]
41  RevisionNotFound { id: String, revision: String },
42  #[error("could not reach the hugging face hub to download `{id}`: {reason}")]
43  Network { id: String, reason: String },
44  #[error("downloading `{id}` failed: {reason}")]
45  DownloadFailed { id: String, reason: String },
46  /// A path the hub listed, or the commit it named, that would not stay inside the cache; refused before any download.
47  #[error("the hub's answer for `{id}` names `{path}`, which {reason}; nothing was downloaded")]
48  UnsafeHubPath {
49    id: String,
50    path: String,
51    reason: String,
52  },
53  #[error("unsupported architecture `{0}`")]
54  UnsupportedArchitecture(String),
55  /// An id `catalog` does not carry and no descriptor for it: nothing on disk states its head or conventions, so it
56  /// is refused rather than guessed.
57  #[error(
58    "model `{id}` is not in the {} registry, and no descriptor was supplied: an unregistered {} model has no snapshot convention to infer {} from",
59    .catalog.name(),
60    .catalog.name(),
61    .catalog.unstated_facts()
62  )]
63  UnregisteredModel { id: String, catalog: Catalog },
64  /// An id only `catalog` registers, refused by the dense builder with or without a descriptor: its encoder would
65  /// otherwise run through the dense pipeline and return a vector with no error.
66  #[error(
67    "model `{model}` is registered in the {} catalog, not the dense one: load it with {}",
68    .catalog.name(),
69    .catalog.entry_point()
70  )]
71  NonDenseModel { model: String, catalog: Catalog },
72  /// The catalog refuses `dtype` for this model on every backend.
73  #[error(
74    "model `{model}` cannot run in {}: the catalog marks that dtype unsupported for this model",
75    .dtype.as_str()
76  )]
77  UnsupportedDtype { model: String, dtype: DType },
78  /// `dtype` cannot run on `backend`: candle has no CPU BF16 matmul, or the catalog refuses that one cell.
79  #[error(
80    "model `{model}` cannot run on {}: {} is not supported",
81    .backend.name(),
82    device::refusal(.backend, .dtype)
83  )]
84  UnsupportedOnDevice {
85    model: String,
86    backend: Backend,
87    dtype: DType,
88  },
89  #[error("model `{0}` does not support matryoshka truncation")]
90  MrlUnsupported(String),
91  #[error("invalid matryoshka dimension {requested}: model dimension is {dim}")]
92  InvalidDim { requested: usize, dim: usize },
93  #[error("dense module activation `{0}` is not supported; expected Identity or Tanh")]
94  UnsupportedDenseActivation(String),
95  /// Six sentence-transformers pooling modes exist and candding implements three; the other three return a
96  /// different vector from a mean, so a repository that asks for one is refused rather than pooled as a mean.
97  #[error(
98    "pooling mode `{0}` is not implemented; candding pools `pooling_mode_cls_token`, `pooling_mode_mean_tokens` or `pooling_mode_lasttoken`"
99  )]
100  UnsupportedPooling(String),
101  /// A `modules.json` type candding does not run cannot be skipped: it sits in the pipeline the repository
102  /// publishes and the vectors it produces are part of the model's output.
103  #[error(
104    "modules.json entry `{0}` is not implemented; candding runs Transformer, Pooling, Dense and Normalize modules"
105  )]
106  UnsupportedModule(String),
107  /// A `modules.json` type only the sparse pipeline runs, reached through `TextEmbeddingBuilder`.
108  #[error(
109    "modules.json entry `{0}` is a sparse-pipeline module; this repository publishes a sparse pipeline — load it with SparseTextEmbedding's builder instead of TextEmbedding's"
110  )]
111  SparseOnlyModule(String),
112  /// A caller-supplied descriptor names `SparseHead::MiniCoil` without the second repository its per-word files
113  /// come from.
114  #[error(
115    "SparseHead::MiniCoil needs its own per-word matrix, vocabulary and stopword list from a second repository, and this descriptor names none — supply one through SparseTextEmbeddingBuilder::descriptor with a head_source, or use a registered id"
116  )]
117  MissingHeadSource,
118  /// `SparseTextEmbeddingBuilder` was given `Qdrant/bm25`, which has no encoder; `hint` names its entry point.
119  #[error(
120    "`{hint}` computes bm25 directly, with no device or dtype and no weights to load — SparseTextEmbedding's builder is for a head that runs a real encoder, which this one does not"
121  )]
122  ComputedSparseModel { hint: String },
123  /// A multi-vector descriptor's `projection_in` differs from the hidden size of the checkpoint's encoder.
124  #[error(
125    "declared projection_in {declared} does not match the encoder's real hidden size {actual}: the registry entry and the checkpoint's own weights disagree"
126  )]
127  ProjectionInMismatch { declared: usize, actual: usize },
128  /// A `MultiVectorHead::StanfordColbert` descriptor without the `query_length` its fixed-length query padding needs.
129  #[error(
130    "model `{model}` runs MultiVectorHead::StanfordColbert, which requires MultiVectorDescriptor::query_length to build a fixed-length, `[MASK]`-padded query tokenizer; this descriptor names none"
131  )]
132  MissingQueryLength { model: String },
133  #[error("tensor `{name}` is missing from `{file}`")]
134  MissingTensor { file: PathBuf, name: String },
135  #[error("dense modules on disk {found:?} do not match the descriptor's {expected:?}")]
136  DenseMismatch {
137    expected: Vec<String>,
138    found: Vec<String>,
139  },
140  #[error("missing file `{0}` in model directory")]
141  MissingFile(PathBuf),
142  /// A `.npy` file whose header cannot be read: no magic bytes, an unknown format version, or a truncated header.
143  #[error("`{file}` is not a readable numpy array file: {reason}")]
144  NpyHeader { file: PathBuf, reason: String },
145  /// A `.npy` array in Fortran order; only C order is read, and nothing is transposed.
146  #[error(
147    "`{0}` stores its array in fortran (column-major) order; candding reads only C (row-major) order and refuses rather than silently transposing"
148  )]
149  NpyFortranOrder(PathBuf),
150  /// A `.npy` dtype with no plain candle counterpart; `f4`, `f8`, `i2`, `i4`, `i8`, `u1` and `u4` are read.
151  #[error(
152    "`{file}` declares numpy dtype `{descr}`, which this reader does not map to a candle tensor dtype"
153  )]
154  NpyUnsupportedDtype { file: PathBuf, descr: String },
155  /// A `.npy` byte order other than little-endian or native, or `|` on a dtype wider than one byte.
156  #[error(
157    "`{file}` declares byte order `{order}` in dtype `{descr}`; this reader only reads little-endian (`<`) or native (`=`) arrays, plus `|` for single-byte dtypes"
158  )]
159  NpyUnsupportedByteOrder {
160    file: PathBuf,
161    order: char,
162    descr: String,
163  },
164  /// A `.npy` data section whose size differs from what its shape and dtype require.
165  #[error(
166    "`{file}` declares shape {shape:?} but its data section holds {actual_bytes} bytes, not the {expected_bytes} its shape and dtype require"
167  )]
168  NpyShapeMismatch {
169    file: PathBuf,
170    shape: Vec<usize>,
171    expected_bytes: usize,
172    actual_bytes: usize,
173  },
174  /// A refetch helps a registered id only: an unregistered pickle-only repository is fetched with the safetensors
175  /// patterns, so the message also names the download a refetch cannot supply.
176  #[error(
177    "no weights in `{0}`: expected `model.safetensors`, `model.safetensors.index.json` or `pytorch_model.bin`. A snapshot fetched before its entry was registered can hold the configuration without the weights; delete that directory and fetch the model again. A repository whose only weights are a torch pickle and whose id no catalog carries is fetched without them, since the fetch asks for safetensors unless an entry declares the pickle format: download its `pytorch_model.bin` into `{0}` yourself, from `https://huggingface.co/<repo>/resolve/<revision>/pytorch_model.bin`, or pass the builder a local directory that holds it"
178  )]
179  MissingWeights(PathBuf),
180  #[error("model directory `{0}` does not exist")]
181  MissingDir(PathBuf),
182  #[error("tokenizer error: {0}")]
183  Tokenizer(String),
184  /// An `EncoderInput` whose `lengths` do not hold one real-token count per batch row.
185  #[error(
186    "{lengths} lengths for a batch of {rows} rows: EncoderInput needs one real-token length per row"
187  )]
188  LengthsMismatch { lengths: usize, rows: usize },
189  /// A length with no room for one real token beside the tokens `kind` names; the tokenizers crate would otherwise
190  /// subtract them unchecked, a panic in a debug build and a wrapped budget in a release one.
191  #[error("{}", length::message(.requested, .minimum, .kind))]
192  LengthTooSmall {
193    requested: usize,
194    minimum: usize,
195    kind: LengthKind,
196  },
197  /// A rendered prompt whose ids do not end in the ids the template's suffix encodes to alone: the tokenizer merges
198  /// the suffix into the text before it, or the rendering and the template disagree, so the cut cannot restore it.
199  #[error(
200    "the {template} prompt does not end in its suffix ids with this tokenizer: expected {expected:?}, found {found:?}; this tokenizer does not keep the template's suffix apart from the text before it"
201  )]
202  PromptSuffixMismatch {
203    template: String,
204    expected: Vec<u32>,
205    found: Vec<u32>,
206  },
207  /// An instruction handed to a reranker whose input is a segment pair: the pair has no slot to put it in, and
208  /// scoring the pair without it would silently drop what the caller asked for.
209  #[error(
210    "model `{model}` reads a (query, document) segment pair, which has no instruction slot; only a prompt reranker takes an instruction"
211  )]
212  InstructionOnPairInput { model: String },
213  /// A checkpoint whose `config.json` ties its embeddings while its weights also carry an `lm_head.weight`, so the
214  /// logit-score head cannot tell which matrix produces the logits.
215  #[error(
216    "config.json says tie_word_embeddings is true, and the weights also carry an lm_head.weight: the logit-score head cannot tell which matrix produces the logits"
217  )]
218  TiedEmbeddingsWithLmHead,
219  /// An untied checkpoint without the `lm_head.weight` its logits come from.
220  #[error(
221    "config.json says tie_word_embeddings is false, and the weights carry no lm_head.weight: an untied checkpoint's logits come from that matrix"
222  )]
223  MissingLmHead,
224  /// A registry entry's `Unembedding` and the checkpoint's own `tie_word_embeddings` disagree: the head would read its
225  /// two rows from another matrix than the one the entry was verified with.
226  #[error(
227    "the entry says the logit-score head's unembedding is {declared}, but config.json says tie_word_embeddings is {tie_word_embeddings}"
228  )]
229  UnembeddingDisagreesWithConfig {
230    declared: String,
231    tie_word_embeddings: bool,
232  },
233  /// A flash checkpoint's entry and its `config.json` name different position schemes, or one names none: the
234  /// family's two implementation repositories default `position_embedding_type` differently.
235  #[error(
236    "the entry declares {declared} positions and config.json's position_embedding_type is {config}: a flash checkpoint loads only when both name the same scheme"
237  )]
238  PositionSchemeDisagreesWithConfig { declared: String, config: String },
239  /// A logit-score token id the checkpoint's vocabulary does not reach.
240  #[error("logit-score token id {token_id} is outside the vocabulary of {vocab_size} tokens")]
241  LogitTokenOutOfRange { token_id: u32, vocab_size: usize },
242  #[error("config error: {0}")]
243  Config(#[from] serde_json::Error),
244  #[error(transparent)]
245  Candle(#[from] candle_core::Error),
246  #[error(transparent)]
247  Io(#[from] std::io::Error),
248  /// `backend` is not compiled into this build of candding.
249  #[error(
250    "device error: the {} backend is not compiled in; build candding with its `{}` feature",
251    .backend.name(),
252    .backend.name()
253  )]
254  BackendNotCompiled { backend: Backend },
255  /// A compiled-in `backend` could not create the device; `reason` is candle's message.
256  #[error("device error: the {} device could not be created: {reason}", .backend.name())]
257  Device { backend: Backend, reason: String },
258  /// The weights need more than `backend` has available, so the load stops before any of them is read.
259  #[error(
260    "model `{model}` needs {} for its weights, but the {} device has {} available: load it in a smaller dtype, on another device, or choose a smaller model",
261    memory::gb(.needed_bytes),
262    .backend.name(),
263    memory::gb(.available_bytes)
264  )]
265  InsufficientMemory {
266    model: String,
267    backend: Backend,
268    needed_bytes: u64,
269    available_bytes: u64,
270  },
271}
272
273pub type Result<T> = std::result::Result<T, CanddingError>;