Output dimensions cluster at five values: 384, 768, 1024, 1536, and 3072, with Qwen3-Embedding-8B at 4096 as the outlier. BERT-base hidden size is 768 and GPT-2 small n_embd is also 768, but hidden size and output dimension are different concepts that happen to match when a model has no projection head. Matryoshka models let you truncate: nomic-embed-text-v1.5 loses only 0.32 MTEB points going 768 to 512, but 6.18 points going to 64. At float32, a vector costs dims x 4 bytes, so 1M vectors at 1536 dimensions is 6.14 GB of raw payload before any index overhead.
bert-base-uncased has hidden_size 768. gpt2 has n_embd 768. bge-base-en-v1.5 outputs 768-dimensional vectors. Three different numbers in three different config keys that all happen to be 768, and conflating them is why so many teams end up with a vector(1536) column full of 768-dimensional inserts.
This is a lookup page. The embedding dimensions of every model people actually deploy are in the two tables below, along with max input length, truncation support, and release date. After the tables there is the part the tables cannot carry: why hidden size and output dimension are different concepts, what Matryoshka truncation really costs in retrieval quality, when cosine and dot product are interchangeable, what a dimension count converts to in bytes, and the exact error strings each vector database throws when the numbers do not line up.
If what you actually want is a recommendation rather than a spec, skip to the end or go straight to our guide on how many dimensions your embeddings actually need, which owns the accuracy and cost curve.
Embedding Dimensions by Model: The Reference Table
Hosted API models first. "Truncatable to" means the model was trained with Matryoshka Representation Learning, so you can cut the vector short without destroying it.
Open-weight models, where the dimension is fixed by the checkpoint you download:
Three things worth flagging in that second table. all-MiniLM-L6-v2 is capped at 256 tokens by its sentence_bert_config.json and was trained at 128, so if you are feeding it 500-word chunks you are silently truncating input, not just embedding it badly. The bge-v1.5 family and the e5 family are all capped at 512 tokens, which is a real constraint on chunk size. And bge-m3 at 8192 tokens is the outlier in the BAAI lineup because it is a different architecture entirely.
For quality scores, context-window trade-offs, and the self-hosting cost math on these same models, see the 2026 embedding model roster with MTEB scores and context windows. This page deliberately carries dimensions only.
| Model | Output dims | Truncatable to | Max input tokens | Released |
|---|---|---|---|---|
| OpenAI text-embedding-3-small | 1536 | any value via dimensions | 8192 | Jan 2024 |
| OpenAI text-embedding-3-large | 3072 | any value via dimensions | 8192 | Jan 2024 |
| OpenAI text-embedding-ada-002 | 1536 | no | 8192 | legacy |
| Google gemini-embedding-2 | 3072 | 128 to 3072 (768/1536/3072 recommended) | 8192 | current |
| Google gemini-embedding-001 | 3072 | 128 to 3072 (768/1536/3072 recommended) | 2048 | GA July 14, 2025 |
| Google text-embedding-004 | 768 (documented, model retired) | output_dimensionality | n/a | retired Jan 14, 2026 |
| Cohere embed-v4.0 | 1536 default | 256, 512, 1024, 1536 | 128,000 | April 15, 2025 |
| Cohere embed-english-v3.0 | 1024 | no | 512 | v3 generation |
| Cohere embed-english-light-v3.0 | 384 | no | 512 | v3 generation |
| Cohere embed-multilingual-v3.0 | 1024 | no | 512 | v3 generation |
| Voyage voyage-3 | 1024 | no | 32,000 | Sept 18, 2024 |
| Voyage voyage-3-lite | 512 | no | 32,000 | Sept 18, 2024 |
| Voyage voyage-3-large | 1024 default (2048 max) | 256, 512, 1024, 2048 | 32,000 | Jan 7, 2025 |
| Voyage voyage-3.5 and 3.5-lite | 1024 default | 256, 512, 1024, 2048 | 32,000 | May 20, 2025 |
| Voyage voyage-4 family | 1024 default | 256, 512, 1024, 2048 | 32,000 | current |
| Model | Output dims | Truncatable to | Max input tokens | License |
|---|---|---|---|---|
| Qwen3-Embedding-8B | 4096 | 32 to 4096 | 32K | Apache-2.0 |
| Qwen3-Embedding-4B | 2560 | 32 to 2560 | 32K | Apache-2.0 |
| Qwen3-Embedding-0.6B | 1024 | 32 to 1024 | 32K | Apache-2.0 |
| BAAI bge-m3 | 1024 (dense) | no | 8192 | MIT |
| BAAI bge-large-en-v1.5 | 1024 | no | 512 | MIT |
| BAAI bge-base-en-v1.5 | 768 | no | 512 | MIT |
| BAAI bge-small-en-v1.5 | 384 | no | 512 | MIT |
| intfloat/multilingual-e5-large | 1024 | no | 512 | MIT |
| intfloat/e5-large-v2 | 1024 | no | 512 | see model card |
| intfloat/e5-base-v2 | 768 | no | 512 | see model card |
| intfloat/e5-small-v2 | 384 | no | 512 | see model card |
| google/embeddinggemma-300m | 768 | 512, 256, 128 | 2048 | Gemma |
| nomic-embed-text-v1.5 | 768 | 512, 256, 128, 64 | 8192 | Apache-2.0 |
| jina-embeddings-v3 | 1024 | 32, 64, 128, 256, 512, 768 | 8192 | see model card |
| all-MiniLM-L6-v2 | 384 | no | 256 (max_seq_length) | Apache-2.0 |
Hidden Size vs Output Dimension: Why BERT-Base and GPT-2 Small Are Both 768
A transformer's hidden size is the width of the residual stream: the vector every single token carries through every layer. Run a 512-token sequence through BERT-base and you get a 512 x 768 matrix out, one 768-vector per token.
A sentence-embedding model's output dimension is the width of the one vector that survives pooling those per-token vectors into a single document representation. Mean pooling over 512 token vectors of width 768 gives you one vector of width 768. That is the entire reason the two numbers match so often: most sentence-embedding models pool with mean or CLS and add no projection layer, so bge-base-en-v1.5 is a BERT-base encoder that takes 768 in and hands 768 out.
They diverge in two cases. When a model adds a projection head after pooling, output width is whatever the projection says and hidden size becomes irrelevant to your vector database. And when a model is trained with Matryoshka objectives, output dimension becomes a runtime parameter while hidden size stays nailed to the checkpoint.
So: a 768-dimensional vector is an array of 768 float32 numbers, 3,072 bytes on disk. No single coordinate means anything you can name. The meaning is in the direction of the whole vector, which is why cutting a non-Matryoshka vector in half does not give you "half the meaning," it gives you noise.
GPT-2's 768 is a third thing again. n_embd is GPT-2's hidden size. GPT-2 is a decoder-only language model, not a retrieval model, and its hidden states are not trained to put semantically similar documents near each other. People search for "gpt-2 embedding dimension 768 n_embd" and find the number, but the number is not a document embedding size and should not be treated as one.
Transformer Hidden Sizes: BERT, GPT-2, and Llama Config Values
Every value here is read straight from the model's config.json.
BERT-base also carries intermediate_size 3072, max_position_embeddings 512, and vocab_size 30522. The GPT-2 family is n_positions 1024 and vocab_size 50257 across all four checkpoints. Refer to the GPT-2 variants by repo name rather than parameter count: the paper's counts and the widely quoted checkpoint counts disagree, and n_embd is the number you can actually verify.
The head-dimension column is the pattern that explains almost every width you will ever see. Head dim is 64 or 128 by convention, so hidden size lands on head count times one of those two values. 768 is 12 x 64. 1600 is 25 x 64. 4096 is 32 x 128. Widths are not tuned to your corpus; they are picked to keep attention heads a convenient size.
Do Llama models tie input and output embeddings?
Not consistently. Llama-3.2-1B sets tie_word_embeddings to true with hidden_size 2048 and vocab_size 128256. Llama-3.1-8B sets it to false with hidden_size 4096 and the same 128256 vocabulary. The arithmetic is the reason. A 128256 x 4096 embedding matrix is about 525M parameters per copy. On an 8B model that is roughly 6.5 percent of the budget for an untied second copy, which the model can afford. On a 1B model at 2048 width, an untied pair would eat around 525M of roughly 1.2B total parameters, which it cannot. Small models tie, large models often untie, but read tie_word_embeddings from the config of the exact checkpoint you are loading rather than trusting the pattern.
| Model | Config key | Value | Layers | Heads | Head dim |
|---|---|---|---|---|---|
| bert-base-uncased | hidden_size | 768 | 12 | 12 | 64 |
| bert-large-uncased | hidden_size | 1024 | 24 | 16 | 64 |
| gpt2 | n_embd | 768 | 12 | 12 | 64 |
| gpt2-medium | n_embd | 1024 | 24 | 16 | 64 |
| gpt2-large | n_embd | 1280 | 36 | 20 | 64 |
| gpt2-xl | n_embd | 1600 | 48 | 25 | 64 |
| Llama-3.2-1B | hidden_size | 2048 | n/a | n/a | n/a |
| Llama-3.1-8B | hidden_size | 4096 | 32 | 32 | 128 |
Matryoshka Dimensions: What Truncation Actually Costs
Matryoshka Representation Learning (arXiv:2205.13147, first submitted May 2022) trains a nested loss over vector prefixes so that the first 256 numbers of a 768-dimensional vector are themselves a usable 256-dimensional embedding. The paper reports up to 14x smaller embedding sizes at equivalent ImageNet-1K classification accuracy and up to 14x real-world speedups on large-scale retrieval.
The single most useful published number on this is the truncation curve from the nomic-embed-text-v1.5 model card, which reports MTEB scores at every supported width:
Read the shape, not the absolute scores. Halving to 512 costs a third of a point. Going to a twelfth costs six points. The knee is between 256 and 128, and 256 is where most teams should stop. The same shape shows up in published precision tradeoffs: voyage-3-large at int8 precision with 1024 dimensions lands 0.31 percent below the same model at float precision with 2048 dimensions while using 8x less storage.
The failure mode to avoid: truncating a Matryoshka vector leaves it un-normalized. Slice the array and the L2 norm is no longer 1, so cosine scores drift and any dot-product index quietly starts ranking on magnitude. Vendors handle this inconsistently. gemini-embedding-2 auto-normalizes truncated output; gemini-embedding-001 requires manual normalization for anything below 3072. nomic's own reference flow is LayerNorm, then L2 normalize, then slice.
import numpy as np
def truncate(vec: np.ndarray, dims: int) -> np.ndarray:
"""Truncate a Matryoshka embedding and restore unit norm."""
sliced = vec[:dims]
norm = np.linalg.norm(sliced)
if norm == 0.0:
raise ValueError(f"Zero-norm vector after truncation to {dims} dims")
return sliced / normBetter still, ask for the width you want at creation time. OpenAI's docs recommend passing the dimensions parameter rather than trimming after the fact, which sidesteps the normalization problem entirely. And none of this applies to non-Matryoshka models: slicing a bge-m3 or e5-large vector is data corruption, not compression.
| Dimensions | MTEB score | Delta from 768 |
|---|---|---|
| 768 | 62.28 | baseline |
| 512 | 61.96 | -0.32 |
| 256 | 61.04 | -1.24 |
| 128 | 59.34 | -2.94 |
| 64 | 56.10 | -6.18 |
Are Embeddings Normalized? Cosine vs Dot Product
OpenAI's embeddings guide is explicit: its embeddings are normalized to length 1, which means cosine similarity can be computed slightly faster using just a dot product, and cosine similarity and Euclidean distance produce identical rankings.
That generalizes into a rule worth internalizing. If every vector in your collection has norm 1, then cosine(a, b) = dot(a, b) and cosine ranking equals Euclidean ranking. Choosing Distance.DOT over Distance.COSINE is then a free speedup with no behavioral change. If your vectors are not normalized, dot product rewards long vectors and your top-k skews toward whatever text produces large magnitudes, which usually means long documents.
all-MiniLM-L6-v2 ships a Normalize module in its sentence-transformers pipeline, so its output is unit-length. The e5 family's official usage examples normalize before comparison. For anything else, do not assume: measure np.linalg.norm(v) on a sample of your actual output and pick your distance metric from the answer, not from a blog post.
Bytes Per Vector at 384, 768, 1536, and 3072
Dimension count converts to storage with one multiplication. float32 is dims x 4 bytes, float16 is dims x 2, int8 is dims x 1, and binary quantization is dims / 8.
That is raw vector payload only. Graph indexes add substantial overhead on top, and at production scale the index is often the larger number. For what a 1536-dimension collection actually costs once HNSW metadata is counted, see our breakdown of what a 1536-dimension index actually costs in RAM.
Quantization changes the arithmetic more than dimension choice does. Qdrant documents scalar quantization from float32 to uint8 as 4x compression with error usually under 1 percent, and binary quantization as up to 32x compression with up to a 40x speedup, noting that binary works best on high-dimensional vectors. Their published binary-quantization examples reach 0.98 recall@100 on 1536-dimensional vectors with 4x oversampling, and 0.98 recall@50 on 4096-dimensional vectors with 2x oversampling. The practical read: a 3072-dimensional int8 index (3.07 GB per million) costs the same as a 768-dimensional float32 index (3.07 GB per million). Precision is usually the cheaper lever to pull.
This math matters most when the hardware is yours. In the on-premise and air-gapped deployments we build at Particula Tech, the dimension choice is not a line item on a cloud invoice, it is a decision about how many vectors fit in the RAM of a machine that already exists inside a regulated network. That constraint is exactly why the open-weight column of the first table matters: fixed, known dimensions on a checkpoint you control, with no vendor-side model update quietly changing your vector width.
| Dims | 1 vector (fp32) | 1M vectors fp32 | 1M int8 | 1M binary |
|---|---|---|---|---|
| 384 | 1,536 B | 1.54 GB | 384 MB | 48 MB |
| 768 | 3,072 B | 3.07 GB | 768 MB | 96 MB |
| 1024 | 4,096 B | 4.10 GB | 1.02 GB | 128 MB |
| 1536 | 6,144 B | 6.14 GB | 1.54 GB | 192 MB |
| 3072 | 12,288 B | 12.29 GB | 3.07 GB | 384 MB |
| 4096 | 16,384 B | 16.38 GB | 4.10 GB | 512 MB |
Dimension Mismatch Errors in Qdrant, pgvector, and Pinecone
The error strings, verbatim, so you can match what your logs are showing you.
Qdrant returns HTTP 400 with Wrong input: Vector dimension error: expected dim: 1536, got 768. The expected dim is whatever you passed as size when creating the collection. Qdrant's docs are explicit that every point in a collection must share one dimensionality and one metric:
from qdrant_client import models
client.create_collection(
collection_name="docs",
vectors_config=models.VectorParams(
size=1536, # must equal len(embedding) for every upsert
distance=models.Distance.COSINE,
),
)pgvector raises two distinct messages from its C source. On INSERT against a typed column: expected 1536 dimensions, not 768. On a distance operator between mismatched vectors: different vector dimensions 1536 and 768, with SQLSTATE 22000. It also rejects zero-dimension vectors and vectors above its compile-time ceiling.
CREATE TABLE docs (id bigserial PRIMARY KEY, embedding vector(1536)); -- INSERT of a 768-dim array raises: -- ERROR: expected 1536 dimensions, not 768
Pinecone returns HTTP 400 on upsert with Vector dimension 768 does not match the dimension of the index 384. The one-line diagnostic is to compare pc.describe_index("my-index").dimension against len(embedding).
Note the word-order reversal that trips people up when they are pattern-matching across stacks: pgvector says expected N, not M, while Pinecone says vector dimension M does not match the index N. Same bug, opposite phrasing.
The cause is almost never the database. It is a model swap that reached the read path but not the write path, a Matryoshka dimensions parameter set in one service and not another, or an embedding call that failed and returned an empty list (expected dim: 3072, got 0 is the signature of that one). Fix it by centralizing the model name and dimension in one config value that both the indexer and the query service read. If your retrieval is broken rather than erroring, our guide to vector search returning nothing covers the silent-failure version of this problem.
Which Number to Actually Pick
Short version, because the long version lives elsewhere:
And the caveat that a lookup table cannot express: a bigger dimension count is not a better model. Leaderboard position and production recall diverge more than most teams expect, which we cover in why a bigger number on the leaderboard does not mean better recall. Dimension is one column in a selection decision that also includes context window, language coverage, licensing, and per-token cost; the full version of that decision is in choosing an embedding model for RAG beyond the dimension count.
Dimension also drives budgets outside retrieval. In a semantic cache the lookup has to complete inside a few milliseconds to be worth doing at all, and vector width is the main variable, which is why embedding dimensions in a semantic cache lookup path reaches different conclusions than a RAG index would.
For the wider architecture these numbers sit inside, start from our RAG systems pillar guide. And if you are picking a dimension for a system that has to run inside your own network, that is the work we do: Particula Tech builds on-premise and private RAG for regulated environments, where the storage math and the model license are the same decision.
FAQ
Quick answers to the questions this post tends to raise.




