//! Error type shared by the whole crate. The enum is here; the length budgets and the device refusals it names,
//! with their messages, have a submodule each.
mod device;
mod length;
mod memory;
#[cfg(test)]
mod tests;
use std::path::PathBuf;
use candle_core::DType;
pub use length::LengthKind;
use thiserror::Error;
use crate::registry::Backend;
use crate::registry::Catalog;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum CanddingError {
/// A model the cache lacks, in a build without the `download` feature; `hint` is the `candding fetch` command.
#[error(
"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}`"
)]
ModelNotFound { id: String, hint: String },
#[error(
"model `{id}` is not in the hugging face cache, and HF_HUB_OFFLINE forbids downloading it"
)]
Offline { id: String },
#[error(
"model `{id}` is gated: accept its license on huggingface.co, then set HF_TOKEN to an access token of that account"
)]
GatedModel { id: String },
/// The hub answers an anonymous request for a private repository the same way as for a missing one.
#[error(
"model `{id}` was not found on the hugging face hub: check the id, or set HF_TOKEN if the repository is private"
)]
RepositoryNotFound { id: String },
#[error("model `{id}` has no revision `{revision}` on the hugging face hub")]
RevisionNotFound { id: String, revision: String },
#[error("could not reach the hugging face hub to download `{id}`: {reason}")]
Network { id: String, reason: String },
#[error("downloading `{id}` failed: {reason}")]
DownloadFailed { id: String, reason: String },
/// A path the hub listed, or the commit it named, that would not stay inside the cache; refused before any download.
#[error("the hub's answer for `{id}` names `{path}`, which {reason}; nothing was downloaded")]
UnsafeHubPath {
id: String,
path: String,
reason: String,
},
#[error("unsupported architecture `{0}`")]
UnsupportedArchitecture(String),
/// An id `catalog` does not carry and no descriptor for it: nothing on disk states its head or conventions, so it
/// is refused rather than guessed.
#[error(
"model `{id}` is not in the {} registry, and no descriptor was supplied: an unregistered {} model has no snapshot convention to infer {} from",
.catalog.name(),
.catalog.name(),
.catalog.unstated_facts()
)]
UnregisteredModel { id: String, catalog: Catalog },
/// An id only `catalog` registers, refused by the dense builder with or without a descriptor: its encoder would
/// otherwise run through the dense pipeline and return a vector with no error.
#[error(
"model `{model}` is registered in the {} catalog, not the dense one: load it with {}",
.catalog.name(),
.catalog.entry_point()
)]
NonDenseModel { model: String, catalog: Catalog },
/// The catalog refuses `dtype` for this model on every backend.
#[error(
"model `{model}` cannot run in {}: the catalog marks that dtype unsupported for this model",
.dtype.as_str()
)]
UnsupportedDtype { model: String, dtype: DType },
/// `dtype` cannot run on `backend`: candle has no CPU BF16 matmul, or the catalog refuses that one cell.
#[error(
"model `{model}` cannot run on {}: {} is not supported",
.backend.name(),
device::refusal(.backend, .dtype)
)]
UnsupportedOnDevice {
model: String,
backend: Backend,
dtype: DType,
},
#[error("model `{0}` does not support matryoshka truncation")]
MrlUnsupported(String),
#[error("invalid matryoshka dimension {requested}: model dimension is {dim}")]
InvalidDim { requested: usize, dim: usize },
#[error("dense module activation `{0}` is not supported; expected Identity or Tanh")]
UnsupportedDenseActivation(String),
/// Six sentence-transformers pooling modes exist and candding implements three; the other three return a
/// different vector from a mean, so a repository that asks for one is refused rather than pooled as a mean.
#[error(
"pooling mode `{0}` is not implemented; candding pools `pooling_mode_cls_token`, `pooling_mode_mean_tokens` or `pooling_mode_lasttoken`"
)]
UnsupportedPooling(String),
/// A `modules.json` type candding does not run cannot be skipped: it sits in the pipeline the repository
/// publishes and the vectors it produces are part of the model's output.
#[error(
"modules.json entry `{0}` is not implemented; candding runs Transformer, Pooling, Dense and Normalize modules"
)]
UnsupportedModule(String),
/// A `modules.json` type only the sparse pipeline runs, reached through `TextEmbeddingBuilder`.
#[error(
"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"
)]
SparseOnlyModule(String),
/// A caller-supplied descriptor names `SparseHead::MiniCoil` without the second repository its per-word files
/// come from.
#[error(
"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"
)]
MissingHeadSource,
/// `SparseTextEmbeddingBuilder` was given `Qdrant/bm25`, which has no encoder; `hint` names its entry point.
#[error(
"`{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"
)]
ComputedSparseModel { hint: String },
/// A multi-vector descriptor's `projection_in` differs from the hidden size of the checkpoint's encoder.
#[error(
"declared projection_in {declared} does not match the encoder's real hidden size {actual}: the registry entry and the checkpoint's own weights disagree"
)]
ProjectionInMismatch { declared: usize, actual: usize },
/// A `MultiVectorHead::StanfordColbert` descriptor without the `query_length` its fixed-length query padding needs.
#[error(
"model `{model}` runs MultiVectorHead::StanfordColbert, which requires MultiVectorDescriptor::query_length to build a fixed-length, `[MASK]`-padded query tokenizer; this descriptor names none"
)]
MissingQueryLength { model: String },
#[error("tensor `{name}` is missing from `{file}`")]
MissingTensor { file: PathBuf, name: String },
#[error("dense modules on disk {found:?} do not match the descriptor's {expected:?}")]
DenseMismatch {
expected: Vec<String>,
found: Vec<String>,
},
#[error("missing file `{0}` in model directory")]
MissingFile(PathBuf),
/// A `.npy` file whose header cannot be read: no magic bytes, an unknown format version, or a truncated header.
#[error("`{file}` is not a readable numpy array file: {reason}")]
NpyHeader { file: PathBuf, reason: String },
/// A `.npy` array in Fortran order; only C order is read, and nothing is transposed.
#[error(
"`{0}` stores its array in fortran (column-major) order; candding reads only C (row-major) order and refuses rather than silently transposing"
)]
NpyFortranOrder(PathBuf),
/// A `.npy` dtype with no plain candle counterpart; `f4`, `f8`, `i2`, `i4`, `i8`, `u1` and `u4` are read.
#[error(
"`{file}` declares numpy dtype `{descr}`, which this reader does not map to a candle tensor dtype"
)]
NpyUnsupportedDtype { file: PathBuf, descr: String },
/// A `.npy` byte order other than little-endian or native, or `|` on a dtype wider than one byte.
#[error(
"`{file}` declares byte order `{order}` in dtype `{descr}`; this reader only reads little-endian (`<`) or native (`=`) arrays, plus `|` for single-byte dtypes"
)]
NpyUnsupportedByteOrder {
file: PathBuf,
order: char,
descr: String,
},
/// A `.npy` data section whose size differs from what its shape and dtype require.
#[error(
"`{file}` declares shape {shape:?} but its data section holds {actual_bytes} bytes, not the {expected_bytes} its shape and dtype require"
)]
NpyShapeMismatch {
file: PathBuf,
shape: Vec<usize>,
expected_bytes: usize,
actual_bytes: usize,
},
/// A refetch helps a registered id only: an unregistered pickle-only repository is fetched with the safetensors
/// patterns, so the message also names the download a refetch cannot supply.
#[error(
"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"
)]
MissingWeights(PathBuf),
#[error("model directory `{0}` does not exist")]
MissingDir(PathBuf),
#[error("tokenizer error: {0}")]
Tokenizer(String),
/// An `EncoderInput` whose `lengths` do not hold one real-token count per batch row.
#[error(
"{lengths} lengths for a batch of {rows} rows: EncoderInput needs one real-token length per row"
)]
LengthsMismatch { lengths: usize, rows: usize },
/// A length with no room for one real token beside the tokens `kind` names; the tokenizers crate would otherwise
/// subtract them unchecked, a panic in a debug build and a wrapped budget in a release one.
#[error("{}", length::message(.requested, .minimum, .kind))]
LengthTooSmall {
requested: usize,
minimum: usize,
kind: LengthKind,
},
/// A rendered prompt whose ids do not end in the ids the template's suffix encodes to alone: the tokenizer merges
/// the suffix into the text before it, or the rendering and the template disagree, so the cut cannot restore it.
#[error(
"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"
)]
PromptSuffixMismatch {
template: String,
expected: Vec<u32>,
found: Vec<u32>,
},
/// An instruction handed to a reranker whose input is a segment pair: the pair has no slot to put it in, and
/// scoring the pair without it would silently drop what the caller asked for.
#[error(
"model `{model}` reads a (query, document) segment pair, which has no instruction slot; only a prompt reranker takes an instruction"
)]
InstructionOnPairInput { model: String },
/// A checkpoint whose `config.json` ties its embeddings while its weights also carry an `lm_head.weight`, so the
/// logit-score head cannot tell which matrix produces the logits.
#[error(
"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"
)]
TiedEmbeddingsWithLmHead,
/// An untied checkpoint without the `lm_head.weight` its logits come from.
#[error(
"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"
)]
MissingLmHead,
/// A registry entry's `Unembedding` and the checkpoint's own `tie_word_embeddings` disagree: the head would read its
/// two rows from another matrix than the one the entry was verified with.
#[error(
"the entry says the logit-score head's unembedding is {declared}, but config.json says tie_word_embeddings is {tie_word_embeddings}"
)]
UnembeddingDisagreesWithConfig {
declared: String,
tie_word_embeddings: bool,
},
/// A flash checkpoint's entry and its `config.json` name different position schemes, or one names none: the
/// family's two implementation repositories default `position_embedding_type` differently.
#[error(
"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"
)]
PositionSchemeDisagreesWithConfig { declared: String, config: String },
/// A logit-score token id the checkpoint's vocabulary does not reach.
#[error("logit-score token id {token_id} is outside the vocabulary of {vocab_size} tokens")]
LogitTokenOutOfRange { token_id: u32, vocab_size: usize },
#[error("config error: {0}")]
Config(#[from] serde_json::Error),
#[error(transparent)]
Candle(#[from] candle_core::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
/// `backend` is not compiled into this build of candding.
#[error(
"device error: the {} backend is not compiled in; build candding with its `{}` feature",
.backend.name(),
.backend.name()
)]
BackendNotCompiled { backend: Backend },
/// A compiled-in `backend` could not create the device; `reason` is candle's message.
#[error("device error: the {} device could not be created: {reason}", .backend.name())]
Device { backend: Backend, reason: String },
/// The weights need more than `backend` has available, so the load stops before any of them is read.
#[error(
"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",
memory::gb(.needed_bytes),
.backend.name(),
memory::gb(.available_bytes)
)]
InsufficientMemory {
model: String,
backend: Backend,
needed_bytes: u64,
available_bytes: u64,
},
}
pub type Result<T> = std::result::Result<T, CanddingError>;