candding
Models

Qwen3

The Qwen3-Embedding decoder family, from the instruction prompt and last-token pooling to the dtype rules and memory each size needs.

Qwen3-Embedding turns the Qwen3 decoder into an embedder: the text is read left to right, the hidden state of the final token is the vector, and an instruction placed in front of a query steers what "similar" means. Three sizes share one implementation and one prompt.

What it is

A decoder-only transformer: causal attention, so each token only sees the tokens before it; rotary position embeddings instead of a learned position table; RMSNorm before each sublayer rather than LayerNorm after it; a gated MLP with the SiLU activation; grouped-query attention with eight key-value heads shared by the query heads; and a per-head RMSNorm on the queries and keys before the rotation. There is no token type embedding and no language-model head in the embedding checkpoints. The tokenizer appends the end-of-sequence token to every input, so the last real token is always that marker, and its hidden state after the final norm is the embedding.

ModelLayersHiddenHeadsKV headsHead sizeIntermediateVector
Qwen/Qwen3-Embedding-0.6B28102416812830721024
Qwen/Qwen3-Embedding-4B36256032812897282560
Qwen/Qwen3-Embedding-8B364096328128122884096

All three use a rotary base of one million, an RMSNorm epsilon of 1e-6, and a byte-level BPE tokenizer with NFC normalization and about 151 thousand entries.

How candding implements it

The family lives in candding/src/models/qwen3/ and assembles shared layers rather than defining its own math.

FileWhat it holds
config.rsthe serde view of config.json, with the Qwen3 defaults for the optional fields
attention.rsthe four projections, the per-head query and key norms, the rotation and the call into the shared attention function
layer.rsone pre-norm block: attention with its residual, then the gated MLP with its residual
model.rsthe token embedding, the causal mask built once per forward, the loop over the blocks and the final norm

The pieces the family shares with the rest of the crate are the ones that decide correctness. layers::rms_norm computes in F32 and returns the input dtype, as transformers does. layers::rope builds the cosine and sine tables once per model from rope_theta and applies the rotate-half convention through candle's kernel; positions start at zero for every row, and because padding is on the right that is where the reference puts them too. layers::mlp is the SwiGLU block. The attention call is the shared function described on the devices and dtypes page: on Metal candle's kernel handles the grouped heads and the causal flag itself, elsewhere the key and value heads are tiled and a triangular additive mask is added. The causal mask carries no padding term, because a real token can never attend to a padding position that comes after it, and the padded positions are discarded by pooling.

The checkpoints carry bare tensor names, embed_tokens, layers.N and norm, without the model. prefix of a full causal language model; the loader accepts both. The 8B checkpoint ships an untied language-model head that is never read.

Pooling, templates and instructions

Pooling is last_token: the hidden state at index length - 1 of each row, which under right padding is the appended end-of-sequence token. Queries carry an instruction, passages carry nothing.

PathTemplate
query_embedInstruct: {instruction}\nQuery:{text}
passage_embed, embedthe text as it is

Query: is followed by the text with no space, exactly as the model card and the repository's prompt file write it. The default instruction is Given a web search query, retrieve relevant passages that answer the query; query_embed_with_instruction replaces it for one call, for example with Given a question, retrieve Wikipedia passages that answer the question or a code-search instruction. The instruction is part of the query side only; passages are embedded without one so that one passage index serves every task.

Every size supports Matryoshka truncation: embed_dim, query_embed_dim and passage_embed_dim keep a prefix of the vector and re-normalize it, and any dimension from 32 up to the full size is accepted.

The default maximum length is 8192 tokens. The models accept longer inputs, 32768 for the small size and 40960 for the two large ones, and the builder's max_length raises the limit up to that ceiling; attention memory grows with the square of the length, which is why the default stops earlier.

Dtypes and memory

The checkpoints are stored in BF16. The 0.6B model loads in F32 by default; the 4B and 8B models load in BF16 by default, because their F32 weights alone are 16 and 32 gigabytes. .dtype(...) on the builder overrides the default, with one rule from candle itself: BF16 has no CPU matmul, so an explicit BF16 request on CPU is refused with CanddingError::UnsupportedOnDevice and a BF16 default on CPU falls back to F32 with a warning. F16 runs everywhere, slowly on CPU.

ModelF32 weightsF16 or BF16 weightsDownload
Qwen/Qwen3-Embedding-0.6Babout 2.4 GBabout 1.2 GB1.2 GB
Qwen/Qwen3-Embedding-4Babout 16 GBabout 8 GB8.0 GB
Qwen/Qwen3-Embedding-8Babout 32 GBabout 15 GB15.1 GB

The catalog table below carries the verification status per dtype; the testing page explains the tolerance column each dtype is held to and why the 8B reference was produced in BF16.

Supported models

ModelFamilyDimPoolingMax lengthDtypesCPUMetalCUDALicense
Qwen/Qwen3-Embedding-0.6BQwen31024last_token8192f32f16bf16verifiedverifieduntestedApache-2.0
Qwen/Qwen3-Embedding-4BQwen32560last_token8192pendingpendingpendinguntestedApache-2.0
Qwen/Qwen3-Embedding-8BQwen34096last_token8192pendingpendingpendinguntestedApache-2.0

Pitfalls

  • The query prompt has no space after Query:; adding one changes the tokens and the vector.
  • The end-of-sequence token comes from the tokenizer's post-processor, not from the model code; token_count on an empty string returns 1, and a port that appends the token itself would double it.
  • Last-token pooling must read the row length from the attention mask; the final column of a right-padded batch is padding for every row but the longest.
  • The query and key norms apply per head, over the head size, after the projection and before the rotation; normalizing the whole projection or rotating first gives plausible but wrong vectors.
  • The rotary tables are computed in F32 and cast to the model dtype, as transformers casts its cosine and sine to the activation dtype; building them directly in F16 loses positions past a few thousand.
  • The causal mask on the matmul path uses the dtype's most negative finite value, not negative infinity, so a fully masked row stays finite.
  • config.json has no pad_token_id; the padding id comes from the pad_token named in tokenizer_config.json, and because padded positions never reach the output it only has to be a valid id.
  • Loading a BF16 checkpoint as F32 is lossless; loading it as F16 rounds every weight, and the F16 run is held to the half-precision tolerance column like BF16.

References

Zhang et al. (2025). Qwen3 Embedding: Advancing Text Embedding and Reranking Through Foundation Models. arXiv:2506.05176
Yang et al. (2025). Qwen3 Technical Report. arXiv:2505.09388
Kusupati et al. (2022). Matryoshka Representation Learning. arXiv:2205.13147

On this page