candding

Testing

How candding proves its outputs: golden references generated in Python, eleven tests per dense model, fixed and per-checkpoint tolerances, and device parity.

An embedding can be wrong and still look right: finite, unit length and plausibly ranked. Every catalog model is therefore compared with stored outputs of its reference implementation, at fixed tolerances.

What "verified" means

A catalog cell reads verified when that device's check below passed in that dtype, pending when a run is planned, untested when no run has reached the registry, and unsupported when the builder refuses the combination.

DeviceCheck
CPUthe golden suite, within the tolerances
Metaljust parity for a dense entry whose F32 weights fit twice in memory, all but Qwen3-Embedding-4B and -8B: a cosine of at least 0.999 between the CPU's and Metal's vector for each fixture input; the golden suite on Metal for every other entry
CUDAthe golden suites in F32 and F16 on an NVIDIA T4, from the cuda workflow's full sweep of every entry but the non-commercial ones; a cell the sweep could not run reads untested, and one it measured failing reads unsupported, so the builder refuses it, as the table below lists; every BF16 cell reads untested, since candle builds no BF16 kernels below sm_80 and the T4 is sm_75
EntriesCUDA cellReadsWhy
Qwen/Qwen3-Embedding-4B and -8B, Qwen/Qwen3-Reranker-4B and -8BF32untested16 GB of F32 weights or more, over the sweep's 7 GB ceiling for the T4's 16 GB
Qwen/Qwen3-Embedding-8B, Qwen/Qwen3-Reranker-8BF16untestedout of memory on the T4: the weights load, and the first forward pass does not fit beside them
jinaai/jina-embeddings-v2-base-de, jinaai/jina-embeddings-v2-base-zhF16unsupportedNaN vectors on the T4, as on the CPU
Qwen/Qwen3-Reranker-0.6BF16unsupportedscores[0][4] misses its ceiling, at 1.06 times the allowed difference
google/embeddinggemma-300mF32untestedits repository is gated, and no full sweep has run with the HF_TOKEN secret that fetches it
naver/splade-code-06B, jinaai/jina-colbert-v2, jinaai/jina-reranker-v2-base-multilingualF32 and F16untestednon-commercial, so no workflow fetches them

A dtype refused on every device, such as EmbeddingGemma's F16, reads unsupported on CUDA too. The unit tests, cargo test --workspace, run offline on synthetic checkpoints of all four pipelines; the golden and parity tests need the model-tests feature and the weights in the cache.

Golden references

scripts/gen_reference.py writes the dense references with sentence-transformers, in F32 on the CPU, forcing F32 parameters where a repository declares torch_dtype: float16. It reads candding describe <id> --json, so templates and maximum length match candding's, and pins the tokenizer's normalizer and pre-tokenizer to the published tokenizer.json. Other outputs are checked against their own reference:

Reference implementationOutputs checked against it
FlagEmbeddingBAAI/bge-m3's sparse and multi-vector heads
fastembedthe BM25, BM42 and miniCOIL heads, and prithivida/Splade_PP_en_v1 beside sentence-transformers
colbert-aicolbert-ir/colbertv2.0, and answerdotai/answerai-colbert-small-v1 beside sentence-transformers
the model cardnomic-ai/nomic-embed-text-v1.5
the repository's own scriptAlibaba-NLP/gte-multilingual-base
the checkpoint's own model in transformersthe eight rerankers, under the rerank suite
  • Inputs. Nine, shared by every suite: an empty and a whitespace-only string, a multilingual line, a Rust snippet, a text longer than 512 tokens, and three texts of different lengths for a mixed batch.
  • Fixtures. A dense model's directory under candding/tests/fixtures/ holds reference.safetensors, a raw, query and passage row per input, and meta.json, which records the weight commit, the descriptor, the library versions and the token counts.
  • Whitespace. The sentence-transformers release in scripts/remote-code/ strips each input and the one in scripts/ does not; the generator removes the strip, so both projects' references match candding, which never strips.
  • Qwen3-Embedding-8B. Its reference is BF16 from Apple's MPS backend, since its F32 weights do not fit in the generating machine's memory.

Remote code

The jina-embeddings-v2, NomicBERT and GTE new repositories and two rerankers ship their architecture as remote code, so their references run in scripts/remote-code/, a second uv project that pins transformers 4. The generator runs remote code only when --trust-remote-code names the architecture at a pinned revision, and asserts that the loaded class is the repository's own, since a stock class of the same name initializes missing tensors randomly.

The dense tests

TestWhat it checks
raw, query, passageembed, query_embed and passage_embed against the reference's rows, so a wrong template fails
single equals batchedeach input alone against its row of a batched run, so padding cannot leak into a result
unit normevery row of a normalizing model has length one
determinismtwo runs are bit-identical
truncationthe long input stops at the maximum length or at the reference's token count, and the empty string's count matches the reference's
Matryoshkaa model without Matryoshka support refuses a dimension before tokenizing, and a truncated vector equals the re-normalized prefix of the full one
STS smokethe Spearman correlation on STS-B pairs stays within a fixed slack of the reference's
snapshot headthe first eight values of the first vector on the CPU, as a readable diff

Each extra dtype has a module of six: raw, query, passage, single equals batched, unit norm and STS. A dtype the catalog refuses on the device becomes a check of the builder's error, and every module that computes reads back weights_dtype, so a load that dropped the requested dtype cannot pass in F32.

Tolerances

CheckF32 on CPUF32 on Metal or CUDAF16, any deviceBF16, any device
cosine to the reference≥ 0.9999≥ 0.999≥ 0.998≥ 0.995
max abs difference after normalizing both≤ 1e-4≤ 1e-2≤ 5e-2≤ 5e-2
single versus batched≤ 1e-5≤ 1e-3≤ 5e-3≤ 5e-3
unit norm1e-51e-31e-31e-3
STS-B Spearman≥ reference − 0.02≥ reference − 0.05≥ reference − 0.05≥ reference − 0.05

A BF16 model is held to the BF16 column; every other half-precision run, an F16 model or an F32 model against a BF16 reference, to the F16 column. The BF16 floor is lower because sentence-transformers' own BF16 runs miss the F16 floor. Tolerances are never loosened to let a result pass.

Sparse and multi-vector suites

SuiteModulesTests
golden_sparsesplade, bge_m3, splade_code, minicoil and bm42, each with _f16 and _bf16, plus splade_code_batch_toleranceraw, query and passage rows, determinism and token counts; the sparse page has the ceilings
golden_multi_vectorone per checkpoint and dtyperaw, query and passage rows, determinism, token counts and the late-interaction scores; see multi-vector output

Qdrant/bm25 has no golden module; a pinned unit-test suite checks it in the default run.

The rerank suite

golden_rerank has a module per checkpoint, such as ms_marco or qwen3_reranker_0_6b, with _f16 and _bf16 siblings. just golden-rerank <id> writes each fixture in F32 on the CPU from the descriptor in scripts/rerank/:

  • a pair through AutoModelForSequenceClassification, on ids from the raw tokenizers backend, since a transformers tokenizer call turns a pair with an empty document into one segment;
  • a prompt through AutoModelForCausalLM on the prompt rendered with the default instruction;
  • the windowed reranker through its own rerank call, its windows recorded id for id.

Each module scores all 81 (query, document) pairs of the nine inputs, which candding batches per query with padding while the reference scores them one at a time. The default-dtype module also checks token counts, determinism, one pair's ids and that sorting a row is a total order. Before any score is compared, the fixture's head, activation and input must equal the registry entry's.

DtypeAllowed absolute difference from the reference scoreOrder of each query's documents
F321e-4the reference's, except exact ties
F16, BF16twice PyTorch's own largest deviation at that dtype for the checkpoint, on the CPU and MPS, recorded in the fixture as half_precision_max_absnot checked, since half precision ties or swaps close scores

The 4B and 8B references load through an offloading route that just golden-rerank-route-check confirms bitwise equal to a plain load. The rerank page has each checkpoint's measured figures.

Running the suites

just test                     # unit tests, no weights
just fetch BAAI/bge-small-en-v1.5
just test-models              # the golden suites on the CPU
just test-metal               # the same on the accelerator
just parity                   # the CPU against the accelerator
cargo test -p candding --features model-tests --test golden_bert bge_small_en_v1_5
CANDDING_TEST_DEVICE=auto cargo test -p candding --features metal,model-tests --test golden_rerank ms_marco_f16:: -- --test-threads=1 --nocapture
just golden BAAI/bge-small-en-v1.5

There is one golden_<family> binary per dense family, plus golden_qwen3_4b, golden_qwen3_8b, golden_sparse, golden_multi_vector and golden_rerank; a module name filters one model. just test-models and just test-metal run the sparse, multi-vector and rerank modules one per process, since a module keeps its model loaded; the recipes include the large Qwen3 runs. Regenerate a reference only when the descriptor changes, and commit the fixture with that change.

CI

The ci workflow runs the unit suite and these golden modules on every push to main and every pull request; the rest of the catalog runs locally.

Test binaryModels or modules, each with its default, F16 and BF16 module
golden_bertBAAI/bge-small-en-v1.5, sentence-transformers/all-MiniLM-L6-v2, intfloat/multilingual-e5-small
golden_mpnet, golden_nomic_bert, golden_modernbert, golden_gte_newsentence-transformers/all-mpnet-base-v2, nomic-ai/nomic-embed-text-v1.5, Alibaba-NLP/gte-modernbert-base, Alibaba-NLP/gte-base-en-v1.5
golden_jina_bertjinaai/jina-embeddings-v2-small-en and -base-en
golden_sparsesplade, minicoil and bm42
golden_rerankms_marco and jina_reranker_v1_tiny
golden_qwen3Qwen/Qwen3-Embedding-0.6B's raw comparison and token-count test, and its BF16 refusal module only
  • The cuda workflow runs the same modules on an NVIDIA T4, or, dispatched with sweep: full, the golden modules of every entry but the non-commercial ones, as candding/tests/cuda_sweep derives them from the golden sources and the registry; it records BF16 as skipped below sm_80 and a module whose weights the T4 cannot hold as skipped, and contributing lists every workflow.
  • EmbeddingGemma stays out of ci, since its repository is gated; the full sweep fetches it with the HF_TOKEN secret, which only its model-fetch step receives.
  • naver/splade-code-06B, jinaai/jina-colbert-v2 and jinaai/jina-reranker-v2-base-multilingual stay out for their non-commercial licences: workflow_license_exclusion.rs fails the default test run if a workflow names one outside a comment.

On this page