← Back
Home › Systems › AI and ML infrastructure

embedding-service

Converts text, images or code into vectors where similar meaning is nearby, serving online queries and bulk indexing jobs to many callers.

OpenAI embeddings

Open the embedding-service pageembedding-service SIMD programminghardware-os: Process 4-16 lanes per instruction with AVX2, AVX-512 or NEON via Rust std::arch, with runtime dispatch and a portable fallback.›Byte-pair encoding (BPE)tokenizationmachine-learning: Learn a subword vocabulary by repeatedly merging the most frequent adjacent pair; byte-level BPE encodes any input losslessly with 32k-200k tokens.›Contrastive learningmachine-learning: Train an encoder so two views of the same thing score higher than every other pair in the batch; InfoNCE with a temperature, no labels needed.›Embedding inferencemachine-learning: Serve a pretrained embedding model: tokenize, batch requests, run the encoder, pool and normalize vectors, cache them; no trainingMatryoshka embeddingsmachine-learning: Training embeddings whose prefixes are usable at lower dimensions for cheaper searchDynamic batchingml-training: Queue independent requests and dispatch them as one batch when it is full or the oldest has waited max_delay; bounded extra latency buys throughput.›FlashAttentionml-training: Exact attention computed tile by tile in on-chip SRAM with online softmax, so the N×N score matrix never touches HBM; 2-4x faster, O(N) memory.›Sequence packingml-training: Packing variable-length sequences into fixed-size batches with attention masks to avoid padding wasteTransformer blockml-training: The decoder-only pre-norm block (attention plus MLP on a residual stream) behind every modern LLM, with exact shapes, parameter, FLOP and KV-cache counts.›Weight quantizationml-training: Store weights as 8/4-bit integers with per-group scales; decode is memory-bound, so 4-bit weights give about 3-4x tokens/s and fit 4x larger models.›HNSWsearch-sort: Hierarchical Navigable Small World: a layered proximity graph searched greedily from a sparse top layer; the default in-memory approximate nearest-neighbour index.›IVF-PQsearch-sort: Inverted file plus product quantization: cluster vectors into lists, store each as a few bytes of codes, scan only the nearest lists via table lookups.›LRU and CLOCKsystems: LRU evicts the least recently used entry via hash map plus linked list; CLOCK approximates it with a reference bit, so hits take no lock.›Text normalizationmachine-learning: Rewrite messy text into one canonical form for matchers and models, or into spoken words for speech output (and back), keeping a map to the original offsets.›Data augmentationmachine-learning: Generate label-preserving variants of each training example on the fly to enlarge the effective dataset and bake in invariances.›Embeddingsmachine-learning: A learned table mapping discrete ids to dense vectors: gather forward, sparse scatter-add backward, row-wise optimizers to keep huge tables cheap.›Loss functionsmachine-learning: The scalar a model minimizes: cross-entropy for classes and tokens, MSE or Huber for values, each with a stable form and simple gradient.›Negative samplingmachine-learning: Train by contrasting each observed positive with a few sampled negatives instead of normalising over every candidate; the choice of sampler decides bias and quality.›Unicode text tokenization forsearchsearch-sort: Turn multilingual text into index terms: segment words (UAX 29 style, CJK bigrams), normalize, case-fold and accent-fold, identically at index and query time.›MPMC bounded queueconcurrency: Vyukov's array queue for many producers and consumers: each slot carries a sequence number, so one CAS per operation claims a slot without locks.›Backpressuresystems: Bounded queues and credit-based flow so slow consumers throttle producersHierarchical timing wheelsystems: Timers bucketed by expiry into wheels of slots, coarser wheels for far deadlines: O(1) insert and cancel for millions of timeouts.›Kernel fusionml-training: Merge chains of memory-bound GPU ops into one kernel, so intermediates stay in registers instead of round-tripping through HBM.›Tiled matrix multiplicationml-training: Block GEMM into tiles held in shared memory and registers, so each byte loaded from DRAM feeds many FMAs; the core of fast GEMM.›Low precision number formatsnumerics: Bf16, fp16, fp8 (E4M3, E5M2), MX block formats and int4: ranges, rounding, scaling+ 7 moreAdamWml-training: Adam's per-parameter adaptive steps with weight decay applied directly to the weights; the default optimizer for transformers and LLM pretraining.›Reverse-mode automaticdifferentiationml-training: Record the forward computation on a tape, then sweep it backwards once to get the gradient of a scalar loss for all inputs.›False sharingconcurrency: Independent variables written by different cores on one cache line force the line to bounce between cores; pad or regroup hot data to fix it.›Memory orderingconcurrency: What Relaxed, Acquire, Release and SeqCst atomics actually guarantee, what they cost on x86 and ARM, and which one each pattern needs.›Slab allocatorsystems: Pre-carve pages into equal-size slots per size class with an in-place free list: O(1) alloc and free, no external fragmentation, cache-warm reuse.›Cuda graphsml-training: Capturing a sequence of GPU kernel launches once and replaying it to remove CPU launch overheadGpu memory coalescing bankinghardware-os: Coalesced global loads, shared memory bank conflicts, L2 and HBM bandwidth on GPUsCUDA and GPU languageslanguages: Programming GPUs: thousands of lockstep threads, bound by memory bandwidth; libraries first, Triton for fused kernels, CUDA C++ for peak speed.›CPU caches and the memoryhierarchyhardware-os: Sizes, line widths and latencies of L1/L2/L3/DRAM on modern x86 and Apple M-series, and the data-layout rules that follow from them.›

How it is built

Architecture overview

An embedding service turns text (or images, or code) into fixed-length vectors such that similar meaning means nearby vectors, and serves that conversion to many callers at a steady price per token. Three jobs share the name: an online path (a user's query is embedded in a few milliseconds and searched against an index), a bulk path (a whole corpus is embedded once, then re-embedded whenever the model changes) and a search index that holds the results. The model is an encoder Transformer block stack trained with Contrastive learning; the service around it is mostly about not wasting compute on padding and repeats: batching by token count, packing short texts together, caching repeated inputs, and keeping the output small. The idea that makes it work is that an embedding is only meaningful relative to the model that produced it, so the model version is part of every vector's identity and of every cache key.

Parts:

 online:  query text ─► cache? ──hit──► vector ─► index search ─► ids + scores
                          │ miss
                          ▼
                   tokenizer ─► batcher (token budget, length-sorted) ─► model (GPU/CPU) ─► pool + normalise
                                                                              │                  │
                                                                      cache write ◄──────────────┘
 bulk:    corpus shards ─► workers (tokenize, pack, embed in large batches) ─► vectors + model-version tag
                                                          └──► index build (HNSW graph or IVF-PQ codes)
 swap:    new model ─► embed everything ─► build second index ─► switch alias ─► drop old index

The online and bulk paths run the same model code but want opposite things: online wants the lowest latency at small batches, bulk wants the highest throughput at the largest batch the memory allows. Many deployments run them as separate pools sharing the model artifact.

How the parts fit together

Model

Most production text embedders are encoder-only transformers (BERT-like), though decoder-based embedders exist. The encoder reads the whole input with bidirectional attention (Transformer block) and emits one hidden vector per token (embedding-inference (no page yet) is the block for running a pretrained encoder at serving time). A pooling step collapses the per-token vectors to one: the first token's vector or the mean over tokens, depending on how the model was trained. The result is usually L2-normalised, so cosine similarity becomes a dot product, which is what the index computes.

The model gets its geometry from Contrastive learning: pairs known to be related (a question and its answer passage, a title and its body) are pulled together and other items in the batch are pushed away. Sentence-BERT is the standard reference for fine-tuning an encoder this way (Reimers and Gurevych 2019). Two properties matter for the service. First, the training task fixes what "similar" means, so a model trained for retrieval is not automatically good at clustering. Second, vectors from different models are not comparable: the service must tag every stored vector with the model id and revision.

Matryoshka embeddings (no page yet) train the first k dimensions of the vector to be a usable embedding on their own (Kusupati et al. 2022). The service can then store or return a shortened vector by truncating and re-normalising. OpenAI's embedding API exposes this through a dimensions parameter, with default sizes of 1536 for text-embedding-3-small and 3072 for text-embedding-3-large (guide). Shorter vectors shrink the index and speed up search at some loss of accuracy; the loss is measured on your own retrieval set, not assumed.

Tokenizer

The tokenizer (Byte-pair encoding (BPE) tokenization) is part of the model, not a utility: a different vocabulary gives different ids and different vectors, so it ships in the same artifact and is versioned with it. (Some encoders use WordPiece, a close relative; the contract is the same.) It also fixes the unit of cost: services meter and rate-limit by tokens. The model has a maximum input length (the OpenAI guide lists 8192 tokens for its three current-generation models); the tokenizer truncates or rejects longer input, and the policy is a visible API decision, since silent truncation hides the tail of a long document from search. Tokenization is CPU work and, at high request rates, a real part of the latency; run it in a separate thread pool from the accelerator loop.

Batcher and packer

The accelerator is efficient on large batches, and requests arrive one at a time. Dynamic batching collects requests until a budget is reached or the oldest has waited too long. For embeddings the right budget is tokens per batch, not requests per batch, because cost grows with total tokens and attention cost grows with length: a batch of 8 requests of 512 tokens is far heavier than 8 requests of 16 tokens. Hugging Face's Text Embeddings Inference advertises exactly this, "token-based dynamic batching" (docs).

Padding is the second waste. A batch is a rectangle, so a 10-token query batched with a 500-token document is padded to 500. Two fixes: sort or bucket requests by length before forming batches, and Sequence packing (no page yet), which concatenates the sequences into one long row and tells attention where each one ends, so no compute goes to padding. Packing needs the attention kernel to respect sequence boundaries; that is what the variable-length interface of FlashAttention provides. Bulk pipelines pack aggressively because they have time to sort a whole shard by length.

Inference kernels

FlashAttention computes exact attention without materialising the full length-by-length score matrix, so memory grows linearly with length and long inputs (thousands of tokens) become affordable. Embedding requests are mostly prefill-style (the whole input in one pass, no token-by-token generation), so the kernel work is dominated by large matrix multiplies and attention, which suits the accelerator well; there is no growing attention cache as in text generation.

Weight quantization (fp16 or bf16 as the baseline, int8 or lower when measured acceptable) raises throughput and cuts memory so more model instances fit per device. The check that matters for embeddings is not perplexity but retrieval quality (recall at k on a labelled set) and vector drift: how far the quantized model's vectors move from the reference model's. If drift is large, old and new vectors in the same index will disagree.

For CPU serving (small models, cost-sensitive or edge cases), SIMD programming is the mechanism: vectorised dot products, int8 dot-product instructions and cache-friendly matrix layouts. It also matters on the index side, where distance computations over many vectors are the inner loop.

Embedding cache

Real traffic repeats: the same popular query, the same document re-submitted unchanged. A cache keyed by (model id and revision, truncation and dimension settings, hash of the text) returns the stored vector with no model call. An eviction policy such as LRU and CLOCK keeps the hot set; CLOCK is cheaper on a concurrent cache because a hit sets a bit instead of reordering a list. The key must include everything that changes the output, or a model upgrade silently serves old vectors. Query caches pay off online; for bulk corpora, de-duplication of identical chunks before embedding gives the same saving.

Vector index

The index answers "which stored vectors are closest to this one". Exact search compares against all vectors, which is fine for thousands and hopeless for hundreds of millions. Two approximate structures are the standard choices:

  • HNSW: a layered proximity graph; query time is low and recall high, memory is the full vectors plus the graph links, and inserts are supported incrementally. Choose it when the index fits in RAM and updates are continuous.
  • IVF-PQ: vectors are assigned to coarse cells (inverted file) and stored as short product-quantization codes; search probes a few cells and compares compressed codes. Memory per vector drops by an order of magnitude or more, at lower recall. Choose it when the corpus is too large for RAM at full precision, often with a re-ranking step that fetches full vectors for the top candidates.

The contract between model and index: dimension, metric (cosine or dot product), normalisation, and model revision must match, and the index records them. Matryoshka truncation is the cheap lever between the two: the same model can fill a short-vector index for recall-oriented first-stage search and a full-length one for re-ranking.

Bulk pipeline

Embedding a corpus is a batch job: read shards, tokenize, length-sort and pack, run large batches at high accelerator utilisation, write vectors with their ids and model tag, build the index. It must be resumable (a failed shard re-runs alone) and idempotent (re-embedding a shard overwrites its vectors, no duplicates). When the model changes, the entire corpus is re-embedded into a new index while the old one keeps serving; the alias is switched when the new index is complete and validated. There is no safe way to mix old and new vectors in one index.

Data and control flow

One online query:

  1. The API authenticates the caller and checks the token rate limit (tokens counted by the tokenizer).
  2. The cache is consulted with the full key; a hit returns at once.
  3. On a miss the text is tokenized, truncated or rejected at the model limit, and queued.
  4. The batcher forms a batch by token budget (queries are short, so many fit); the model runs; pooled vectors are normalised and optionally truncated to the requested dimension.
  5. Vectors are written to the cache and returned. If the caller asked for a search, the vector goes to the index (HNSW graph walk, or IVF cell probe plus code scan), and ids and scores come back.
  6. Time goes mostly to the model forward pass for a cache miss and to index search for a hit; the queue wait is the knob that trades one against the other.

One model upgrade:

  1. The new model is evaluated on a labelled retrieval set against the current one.
  2. The bulk pipeline embeds all corpus shards with the new model, tagging each vector with the new revision.
  3. A second index is built and checked for recall against the first on held-out queries.
  4. Query traffic is moved to the new model and index together, never one without the other, usually via a single alias that names both.
  5. The old index and cache entries are dropped after a rollback window.

Scaling

  1. Padding and idle accelerators: length-bucketing, packing and token-budget batching; these double or triple effective throughput on mixed-length traffic before any hardware is added (order of, depends on the length mix).
  2. Repeats: a cache for queries, de-duplication for corpora.
  3. Model cost: a smaller or quantized model, lower precision, shorter maximum input length; verify on retrieval metrics each step.
  4. Index memory: shorter vectors (Matryoshka), then IVF-PQ compression with re-ranking, then sharding the index across machines by cell or by id range with a merge of the top-k from each shard.
  5. Query rate on the index: replicate the index (read replicas), since search is read-only between builds.
  6. Bulk time: add workers; shards are independent, so throughput scales almost linearly.

Failure modes

FailureSymptomCauseFix
Model mismatchsearch returns unrelated results after a deployquery vectors from model B against an index of model Amodel revision stored in index and checked on every query; one alias for both
Silent truncationlong documents match only on their openinginput cut at the model limit without noticechunk documents before embedding; return a truncation flag
Padding blow-uplatency spikes when one long text joins a batchbatching by request countbatch by token budget; length-sorted queues; packing
Cache poisoning by upgradeold vectors served after a model changecache key lacks model revisioninclude revision and all output-affecting settings in the key
Quantization driftrecall falls on some topicscalibration set unlike traffic, or aggressive bit widthevaluate recall at k per segment; keep a higher-precision fallback
Index recall collapsegood queries miss near neighboursHNSW search width too small, IVF probing too few cells, PQ too coarsetune the search-time parameter against a recall target; re-rank with full vectors
Normalisation bugscores outside the expected range, odd rankingsnormalised at write but not at query, or twicenormalise in one place; test that stored vectors have unit norm
Re-embed outageindex unavailable during upgradein-place rebuildbuild the new index beside the old one and switch the alias

Real implementations

namewhat it isrepolicencenotes
Text Embeddings Inference (TEI)Rust inference server for embedding and rerank models with token-based dynamic batching and Flash Attentionhuggingface/text-embeddings-inferenceApache-2.0docs; exports Prometheus metrics
sentence-transformersPython library for training and running embedding modelsUKPLab/sentence-transformersApache-2.0docs; efficiency notes
Infinityembedding server with batching over several backendsmichaelfeil/infinityMIT
FAISSlibrary of vector indexes including IVF and PQ variants and HNSWfacebookresearch/faissMITthe usual building block for the index part
OpenAI embeddings APIhosted service: text-embedding-3-small and -large with a dimensions parameternot open sourceproprietary serviceguide

The alternative: call a hosted embedding API. It removes the model, batcher and capacity planning, and costs a network round trip per request, a per-token price, and a dependency on the vendor's model revisions. Self-host when volume makes the per-token price larger than the cost of a GPU you can keep busy, when data cannot leave your network, or when you need a model fine-tuned on your own pairs.

Sources

  • N. Reimers, I. Gurevych, Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks, EMNLP 2019. paper
  • A. Kusupati et al., Matryoshka Representation Learning, NeurIPS 2022. paper
  • T. Dao et al., FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness, 2022. paper
  • OpenAI embeddings guide (dimensions parameter, default sizes, maximum input) and Hugging Face Text Embeddings Inference documentation, linked above.

Similar systems

Share the most building blocks with this one.