Local Embedding Server Crashes Under Batched Requests

Ollama, llama-server, vLLM, or sentence-transformers crashes or OOMs on batched embeddings. Fix batch size, num_batch, sequence length, and concurrency — with the exact flags.

Single embeddings work. Then your RAG indexer pushes 64 or 128 chunks at once and the local embedding server — Ollama serving nomic-embed-text, llama-server with bge-large-en-v1.5, vLLM in embed mode, or a sentence-transformers FastAPI service — OOMs, hangs, or starts returning 500s after a few batches while silently dropping the rest.

Fastest fix: cut the batch and the per-item context first. For Ollama, lower the runner batch with a Modelfile PARAMETER num_batch 64 (default is 512) and pre-truncate inputs to ~512 tokens; for llama-server set --ubatch-size 64 --ctx-size 512; for sentence-transformers set model.max_seq_length = 512 and encode(..., batch_size=16). Then cap client concurrency to 2-4. Embedding models run the entire batch through every encoder layer in one forward pass, so peak memory scales with batch_size × longest_sequence², not with the average — one outlier document can sink an otherwise-fine batch.

If you are on Ollama and the crash log says caching disabled but unable to fit entire input in a batch, jump straight to cause 3 — that is a known v0.13.x regression.

Which bucket are you in?

SymptomMost likely causeGo to
VRAM spikes to 100% then the process diesBatch too large for VRAMCause 1
Crashes only when one chunk is a huge un-split documentOutlier sequence length / padding blowupCause 2
Ollama log: caching disabled but unable to fit entire input in a batchnum_batch too high / v0.13.x regressionCause 3
Works one at a time, dies under parallel workersConcurrent forward passesCause 4
model.max_seq_length prints 4096/8192 on a small GPUsentence-transformers long-context defaultCause 5
Slow, queue grows, then OOM; ollama ps shows CPUEmbeddings not on GPUCause 6
llama-server uses huge memory per requestEmbedding mode/pooling not enabledCause 7

Common causes

Ordered by hit rate, highest first.

1. Batch size too large for available VRAM

Embedding models run every item in the batch simultaneously through the encoder. For bge-large-en-v1.5 (335M params, fp32), a batch of 128 items at 512 tokens each needs roughly 128 × 512 × 1024 × 4 bytes ≈ 268 MB just for the input representations, before attention matrices and intermediate activations. On an 8 GB GPU, batches of 256+ OOM almost every time.

How to spot it: run nvidia-smi dmon -s m -d 1 while sending batches. If VRAM climbs to the ceiling and the process then dies, the batch is the cause.

2. One outlier long sequence makes the whole batch huge

A batch is padded to its longest member. Mix a 10-token chunk with a 2,000-token chunk and the whole batch is sized for 2,000 tokens. Because attention memory grows with the square of sequence length, a single un-split PDF page in a batch of 64 can OOM even though the other 63 chunks are tiny.

How to spot it: log max(len(t) for t in batch) (token count, not characters) before each call. If the max is far above the average, padding is amplifying memory.

3. Ollama: num_batch too high (and the v0.13.x regression)

Ollama’s runner batch defaults to num_batch = 512 (inherited from llama.cpp). On large-context embedding inputs this is the classic OOM trigger, fixed by lowering it. As of June 2026 there is also a specific regression: Ollama v0.13.0–v0.13.2 crash on embeddings with the panic caching disabled but unable to fit entire input in a batch, where the same workload runs fine on v0.12.11. Note the two embedding endpoints have different shapes: the modern /api/embed takes an input string-or-array, returns fp32 and L2-normalizes; the legacy /api/embeddings takes a single prompt string only.

How to spot it: check ollama --version. If you are on 0.13.0–0.13.2 and see that panic, downgrade to 0.12.11 or lower num_batch. Set it via a Modelfile (PARAMETER num_batch 64) — keep it >= 32, or llama.cpp won’t use the cuBLAS prompt-eval kernels. You can also bound embedding context in the same Modelfile with PARAMETER num_ctx 2048 so over-long inputs don’t blow up the runner.

4. Concurrent embedding requests from multiple RAG workers

If your indexer spawns parallel workers that each POST a batch to the same server, the server can start several forward passes before earlier ones free their memory, so the effective concurrent batch is workers × batch_size. Eight workers sending 32-item batches behaves like a 256-item batch.

How to spot it: count your parallel workers and multiply by the per-call batch size. If that product is much larger than what a single batch tolerates, concurrency is the cause.

5. sentence-transformers long-context max_seq_length default

SentenceTransformer.encode() defaults to batch_size=32, but the per-item ceiling comes from model.max_seq_length, which varies by model — classic BERT-based models cap at 512, while several modern embedding models default to 4096 or 8192. On a small GPU that long-context default OOMs at even moderate batch sizes.

How to spot it: print(model.max_seq_length). If it is 4096/8192 and your GPU is under 16 GB, drop it to 512 unless you truly need long contexts.

6. Ollama embedding model not GPU-accelerated

On some setups Ollama runs the embedding model on CPU when the generation GPU is busy. CPU embedding is 20-100x slower, so under batched load the request queue grows until the in-memory queue itself OOMs.

How to spot it: run ollama ps while under load and read the Processor column. If it shows 100% CPU (or any CPU share), the embedding model is not fully on the GPU.

7. llama-server not actually in embedding mode

llama-server needs --embeddings to expose the OpenAI-compatible /v1/embeddings endpoint, and the model must use a pooling mode other than none. Without the right pooling it either errors or falls back to per-request generation buffers, inflating memory and latency.

How to spot it: check your startup command for --embeddings and a --pooling value (mean or cls; rank is for rerankers). If --pooling is missing or none, fix it.

Shortest path to fix

Step 1: Reduce batch size and add backoff (Ollama /api/embed)

import time
import requests

def embed_with_retry(texts: list[str], batch_size: int = 16) -> list:
    embeddings = []
    for i in range(0, len(texts), batch_size):
        batch = texts[i:i + batch_size]
        for attempt in range(3):
            try:
                resp = requests.post(
                    "http://localhost:11434/api/embed",
                    json={"model": "nomic-embed-text", "input": batch},
                    timeout=60,
                )
                resp.raise_for_status()
                embeddings.extend(resp.json()["embeddings"])
                break
            except Exception:
                if attempt == 2:
                    raise
                time.sleep(2 ** attempt)
    return embeddings

Step 2: Lower Ollama’s num_batch (and cap embedding context)

If the runner itself OOMs, drop the batch the runner uses, not just the request batch. Create a small Modelfile:

FROM nomic-embed-text
PARAMETER num_batch 64
PARAMETER num_ctx 2048

Build and use it: ollama create nomic-embed-batched -f Modelfile. Keep num_batch >= 32. The PARAMETER num_ctx 2048 line bounds the per-request context. Leave truncate at its default (true) so over-long inputs are clipped instead of crashing the runner.

Step 3: Start llama-server with proper embedding flags

./llama-server \
  -m models/bge-large-en-v1.5-Q8_0.gguf \
  --embeddings \
  --pooling mean \
  --ctx-size 512 \
  --batch-size 512 \
  --ubatch-size 64 \
  --n-gpu-layers 99 \
  --port 8081

--ubatch-size is the physical batch actually computed at once — keep it at 32-64 to bound peak attention memory while --batch-size stays larger for scheduling throughput.

Step 4: Enforce max sequence length in sentence-transformers

from sentence_transformers import SentenceTransformer

model = SentenceTransformer("BAAI/bge-large-en-v1.5")
model.max_seq_length = 512  # Override the model default (may be 4096/8192)

def embed_documents(texts: list[str]) -> list:
    return model.encode(
        texts,
        batch_size=16,
        show_progress_bar=True,
        convert_to_numpy=True,
        normalize_embeddings=True,
    ).tolist()

Step 5: Sort batches by length to minimize padding waste

def embed_sorted(texts: list[str], model, batch_size: int = 32) -> list:
    # Group similar lengths so short chunks aren't padded up to a long one
    indexed = sorted(enumerate(texts), key=lambda x: len(x[1]), reverse=True)
    sorted_texts = [t for _, t in indexed]
    original_indices = [i for i, _ in indexed]

    embeddings_sorted = model.encode(sorted_texts, batch_size=batch_size)

    result = [None] * len(texts)  # Restore original order
    for orig_idx, emb in zip(original_indices, embeddings_sorted):
        result[orig_idx] = emb
    return result

Step 6: Limit concurrent embedding workers

import asyncio

sem = asyncio.Semaphore(2)  # At most 2 concurrent embedding requests

async def embed_chunk(session, chunk):
    async with sem:
        async with session.post(
            "http://localhost:11434/api/embed",
            json={"model": "nomic-embed-text", "input": [chunk]},
        ) as resp:
            data = await resp.json()
            return data["embeddings"][0]

Step 7 (vLLM): cap concurrent sequences

If you serve embeddings with vLLM, the default --max-num-seqs is tuned for throughput, not for an 8 GB card. Pin it down and bound the model length:

vllm serve BAAI/bge-large-en-v1.5 \
  --task embed \
  --max-num-seqs 32 \
  --max-model-len 512 \
  --gpu-memory-utilization 0.85 \
  --port 8001

How to confirm it’s fixed

  1. Re-run the batch that used to crash. The process must finish without dropping to a runner restart.
  2. Watch nvidia-smi dmon -s m -d 1 (or ollama ps) during a full index pass — peak VRAM should plateau well under the card’s limit, not pin at 100%.
  3. Check the count: len(embeddings) == len(texts). Silent drops, not crashes, are the failure mode of an over-loaded server.
  4. Spot-check one vector’s dimension (len(embeddings[0])) matches the model (nomic-embed-text is 768, bge-large-en-v1.5 is 1024) — proof the items weren’t truncated to empty.

Prevention

  • Start with batch_size 8-32 for large models (335M+ params) and 32-64 for small ones (~110M), then tune upward while watching VRAM.
  • Enforce max_seq_length = 512 unless you genuinely need longer contexts — most RAG chunks should be 128-512 tokens anyway.
  • Sort batches by length before embedding to cut padding overhead (the same idea as dynamic padding in training).
  • Run one embedding server with a request queue instead of several parallel servers fighting for the same VRAM.
  • Monitor VRAM with nvidia-smi dmon -s m -d 1 before a full-scale indexing run.
  • Keep the embedding model on a dedicated GPU or VRAM allocation, separate from any generation model.
  • Pin your Ollama version in production — embedding behavior changed between 0.12.x and 0.13.x, so test before upgrading.
  • Add circuit-breaker logic that pauses and retries on a 429/500 instead of hammering the server.

FAQ

Q: What’s the difference between --batch-size and --ubatch-size in llama-server? A: --batch-size is the logical batch the scheduler accepts; --ubatch-size is the physical micro-batch actually computed in one pass. For embeddings, lower --ubatch-size to 32-64 to cap peak attention memory while keeping --batch-size larger for throughput.

Q: My Ollama embeddings crash with caching disabled but unable to fit entire input in a batch. What now? A: That panic appears on Ollama v0.13.0–v0.13.2 (it works on v0.12.11). As of June 2026, either downgrade with ollama --version confirming the build, or lower the runner batch via a Modelfile PARAMETER num_batch 64 and bound context with PARAMETER num_ctx 2048 in the same Modelfile.

Q: Should I use /api/embed or /api/embeddings? A: Use /api/embed. It accepts an input array (real batching), returns fp32, and L2-normalizes. The older /api/embeddings only takes one prompt string and skips normalization, so cosine similarity can be off if you mix the two.

Q: Why does the server crash only on the 50th batch, not the first? A: Memory fragmentation. The first batches allocate and free memory, but the allocator hands back fragmented blocks. When a later batch needs one large contiguous region, the allocation fails. On Linux, setting MALLOC_ARENA_MAX=2 before launching can reduce fragmentation.

Q: I need a CPU-only embedding server — what batch size is safe? A: On CPU with sentence-transformers, batch_size 1-4 is safe for long sequences and ~16 for short (128-token) chunks. Watch RAM with htop; if the process nears your system RAM limit, halve the batch.

Q: Can I run embeddings and generation on one llama-server instance? A: Not recommended. The instance allocates a fixed KV cache at startup tuned for either generation (large autoregressive cache) or embedding (no autoregressive cache). Run separate instances on different ports.

Tags: #local-llm #ollama #Troubleshooting