NomicBERT
The rotary-embedding encoder behind nomic-embed-text v1, v1.5 and Snowflake's arctic-embed-m-long, with dynamic NTK length scaling.
NomicBERT replaces BERT's position table with rotary embeddings and its feed-forward with a SwiGLU block, and fuses the query, key and value projections into one matrix. Three checkpoints share it: nomic-ai/nomic-embed-text-v1, nomic-ai/nomic-embed-text-v1.5 and Snowflake/snowflake-arctic-embed-m-long, which declares the same model_type.
What it is
An encoder-only transformer with bidirectional attention, a two-row token type table and post-LayerNorm blocks at 1e-12 with no final norm. The rotation spans the full head, rotate-half, at the base of 1000 every checkpoint declares, and the bias-free feed-forward is fc2(fc11(x) * silu(fc12(x))). The config must say activation_function: "swiglu" with prenorm and causal both false.
How candding implements it
The three checkpoints differ in how the rotary table behaves past the trained length of 2048 tokens:
| Checkpoint | rotary_scaling_factor | Past 2048 tokens |
|---|---|---|
| nomic-ai/nomic-embed-text-v1 | 2 | dynamic NTK: the base is rescaled to the batch's length |
| Snowflake/snowflake-arctic-embed-m-long | 2 | dynamic NTK, the same |
| nomic-ai/nomic-embed-text-v1.5 | null | the configured base at every length, as its reference embeds |
base' = base * ((factor * seq / max_trained) - (factor - 1)) ^ (dim / (dim - 2))The rescaled table is built for the longest sequence in the current batch, so a text in a batch that passes the trained length embeds differently from the same text alone. No golden fixture reaches 2048 tokens, so no test compares rescaled vectors with the reference; unit tests check the formula itself.
nomic-ai/nomic-embed-text-v1.5's model card applies a LayerNorm with no affine parameters, at 1e-5, between mean pooling and Matryoshka truncation, a step its modules.json omits; its descriptor sets layer_norm_after_pooling, which the pipeline runs for it alone.
The default length is 8192 for all three, from sentence_bert_config.json. Rotary tables grow to whatever a batch needs, so the family reports no position limit, and the default or .max_length(n) caps an input.
Pooling and templates
| Models | Pooling | Query template | Passage template |
|---|---|---|---|
| nomic-ai/nomic-embed-text-v1, v1.5 | mean | search_query: {text} | search_document: {text} |
| Snowflake/snowflake-arctic-embed-m-long | cls | Represent this sentence for searching relevant passages: {text} | none |
The nomic model card also names clustering: and classification: prefixes, which a descriptor has no slot for: prepend one to the text and call embed. nomic-ai/nomic-embed-text-v1.5 supports Matryoshka truncation and ships no Normalize module; the pooling and templates page has both. All three use the bert-base-uncased WordPiece tokenizer.
Supported models
| Model | Family | Dim | Pooling | Max length | Dtypes | CPU | Metal | CUDA | License |
|---|---|---|---|---|---|---|---|---|---|
| nomic-ai/nomic-embed-text-v1 | NomicBERT | 768 | mean | 8192 | f32f16bf16 | verified | verified | verified | Apache-2.0 |
| nomic-ai/nomic-embed-text-v1.5 | NomicBERT | 768 | mean | 8192 | f32f16bf16 | verified | verified | verified | Apache-2.0 |
| Snowflake/snowflake-arctic-embed-m-long | NomicBERT | 768 | cls | 8192 | f32f16bf16 | verified | verified | verified | Apache-2.0 |
Dtypes: bold is the default the builder loads; plain text is verified, muted is untested, struck through is unsupported, each colored by the dtype's own Metal status so a processor-only refusal does not read as unsupported everywhere; hover a dtype for its own cpu/metal/cuda breakdown. BF16 is refused on the CPU for every row: candle has no CPU BF16 matmul.
Pitfalls
rotary_emb_basehas no safe default: read it from the checkpoint. A wrong base still gives finite, unit-length vectors.- Skipping the dynamic NTK rescaling for
v1or the Snowflake checkpoint, or applying it tov1.5, changes nothing at or under 2048 tokens, so only a longer input shows it. fc12is the gate andfc11the up-projection; the two have the same shape, so a swap still runs.layer_norm_after_poolingbelongs tov1.5alone; set wrongly, it changes each vector's direction and nothing else.Wqkv's rows are(three, heads, head_dim); read as(heads, three, head_dim), every projection mixes query, key and value in a tensor of the right shape.config.json'smax_position_embeddingsis not the length cap; used as one, it capsv1.5at 2048 tokens.