单条 embedding 一切正常。可一旦 RAG 索引器一次推送 64 或 128 个 chunk,本地 embedding 服务——无论是 Ollama 跑 nomic-embed-text、llama-server 跑 bge-large-en-v1.5、vLLM 的 embed 模式,还是 sentence-transformers 写的 FastAPI 服务——就开始 OOM、卡死,或者处理几个 batch 后返回 500 并悄悄丢掉其余请求。
最快修复: 先砍 batch、再砍单条上下文。Ollama 用 Modelfile 加一行 PARAMETER num_batch 64(默认是 512),并把输入预截断到约 512 token;llama-server 设 --ubatch-size 64 --ctx-size 512;sentence-transformers 设 model.max_seq_length = 512 并 encode(..., batch_size=16)。然后把客户端并发压到 2-4。embedding 模型会把整个 batch 一次性穿过每一层 encoder,所以峰值显存按 batch_size × 最长序列² 增长,而不是按平均值——一条超长文本就能拖垮一个本来没问题的 batch。
如果你在用 Ollama,崩溃日志里写着 caching disabled but unable to fit entire input in a batch,直接跳到原因 3——这是 v0.13.x 的已知回归。
先判断你属于哪一类
| 现象 | 最可能的原因 | 跳转 |
|---|---|---|
| 显存飙到 100% 然后进程死掉 | batch 超出显存 | 原因 1 |
| 只在某条是未切分的超大文档时崩溃 | 离群序列长度 / padding 爆炸 | 原因 2 |
Ollama 日志:caching disabled but unable to fit entire input in a batch | num_batch 过大 / v0.13.x 回归 | 原因 3 |
| 单条正常,并发 worker 一上就死 | 并发 forward pass | 原因 4 |
小显卡上 model.max_seq_length 打印出 4096/8192 | sentence-transformers 长上下文默认值 | 原因 5 |
很慢、队列越堆越大然后 OOM;ollama ps 显示 CPU | embedding 没跑在 GPU 上 | 原因 6 |
| llama-server 每个请求占用巨量内存 | embedding 模式 / pooling 没开 | 原因 7 |
常见原因
按命中率从高到低排列。
1. batch 太大,超出可用显存
embedding 模型会把 batch 里每一条同时穿过 encoder。以 bge-large-en-v1.5(335M 参数,fp32)为例,128 条、每条 512 token 的 batch,仅输入表示就需要约 128 × 512 × 1024 × 4 字节 ≈ 268 MB,这还没算注意力矩阵和中间激活。在 8 GB 显卡上,256+ 的 batch 几乎必 OOM。
怎么判断: 发送 batch 时跑 nvidia-smi dmon -s m -d 1。如果显存爬到上限随后进程死掉,原因就是 batch。
2. 一条离群超长文本把整个 batch 撑大
batch 会按其中最长的一条做 padding。把一条 10 token 的 chunk 和一条 2000 token 的 chunk 放进同一个 batch,整个 batch 就按 2000 token 计算尺寸。由于注意力显存随序列长度的平方增长,batch 里哪怕只有一页未切分的 PDF,64 条的 batch 也会 OOM,尽管其余 63 条都很短。
怎么判断: 每次调用前打印 max(len(t) for t in batch)(统计 token 数,不是字符数)。如果最大值远高于平均值,就是 padding 在放大显存。
3. Ollama:num_batch 过大(以及 v0.13.x 回归)
Ollama 运行时的 batch 默认 num_batch = 512(继承自 llama.cpp)。对长上下文的 embedding 输入,这是经典的 OOM 触发点,调小即可解决。截至 2026 年 6 月还有一个特定回归:Ollama v0.13.0–v0.13.2 在 embedding 时会崩溃,panic 信息是 caching disabled but unable to fit entire input in a batch,而同样的负载在 v0.12.11 上正常。注意两个 embedding 接口形状不同:现代的 /api/embed 接受 input 字符串或数组、返回 fp32 并做 L2 归一化;旧的 /api/embeddings 只接受单个 prompt 字符串。
怎么判断: 查 ollama --version。如果你在 0.13.0–0.13.2 且看到那条 panic,回退到 0.12.11,或调小 num_batch。通过 Modelfile 设置(PARAMETER num_batch 64)——保持 >= 32,否则 llama.cpp 不会启用 prompt-eval 的 cuBLAS kernel。也可以在同一个 Modelfile 里加 PARAMETER num_ctx 2048 限制 embedding 上下文,避免过长输入把运行时搞崩。
4. 多个 RAG worker 并发发送 embedding 请求
如果索引器派生多个并行 worker,各自向同一服务 POST 一个 batch,服务端可能在前面的 forward pass 还没释放显存时就启动多个新的,于是实际并发 batch 等于 worker 数 × batch_size。8 个 worker 各发 32 条,表现得就像一个 256 条的 batch。
怎么判断: 数一下并行 worker 数,乘以每次调用的 batch 大小。如果这个乘积远大于单个 batch 能承受的量,原因就是并发。
5. sentence-transformers 的长上下文 max_seq_length 默认值
SentenceTransformer.encode() 默认 batch_size=32,但单条上限来自 model.max_seq_length,它因模型而异——经典 BERT 系封顶 512,而不少现代 embedding 模型默认 4096 或 8192。在小显卡上,这个长上下文默认值即便中等 batch 也会 OOM。
怎么判断: print(model.max_seq_length)。如果是 4096/8192 且你的 GPU 小于 16 GB,除非真的需要长上下文,否则降到 512。
6. Ollama embedding 模型没用上 GPU
某些环境下,当生成 GPU 繁忙时 Ollama 会把 embedding 模型放到 CPU 跑。CPU embedding 慢 20-100 倍,于是批量负载下请求队列越堆越长,直到内存里的队列本身 OOM。
怎么判断: 负载中跑 ollama ps,看 Processor 列。如果显示 100% CPU(或任何 CPU 占比),说明 embedding 模型没完全跑在 GPU 上。
7. llama-server 其实没进入 embedding 模式
llama-server 需要 --embeddings 才会暴露 OpenAI 兼容的 /v1/embeddings 接口,且模型的 pooling 模式不能是 none。pooling 不对时,它要么报错,要么回退到按请求分配生成 buffer,导致显存和延迟暴涨。
怎么判断: 检查启动命令里是否有 --embeddings 以及一个 --pooling 值(mean 或 cls;rank 是给 reranker 用的)。若 --pooling 缺失或为 none,改正它。
最短修复路径
Step 1:减小 batch 并加退避(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:调小 Ollama 的 num_batch(并限制 embedding 上下文)
如果是运行时本身 OOM,要减小运行时使用的 batch,而不仅仅是请求里的 batch。写一个小 Modelfile:
FROM nomic-embed-text
PARAMETER num_batch 64
PARAMETER num_ctx 2048
构建并使用:ollama create nomic-embed-batched -f Modelfile。保持 num_batch >= 32。PARAMETER num_ctx 2048 这一行用来限制单次请求的上下文。truncate 保持默认(true),让过长输入被裁剪,而不是把运行时搞崩。
Step 3:用正确的 embedding flag 启动 llama-server
./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 是实际一次计算的物理 batch——保持在 32-64 以约束峰值注意力显存,而 --batch-size 可以更大以保证调度吞吐。
Step 4:在 sentence-transformers 里强制最大序列长度
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-large-zh-v1.5")
model.max_seq_length = 512 # 覆盖模型默认值(可能是 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:按长度排序 batch 以减少 padding 浪费
def embed_sorted(texts: list[str], model, batch_size: int = 32) -> list:
# 把长度相近的归到一起,短 chunk 就不会被 padding 到某条长文本的长度
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) # 恢复原始顺序
for orig_idx, emb in zip(original_indices, embeddings_sorted):
result[orig_idx] = emb
return result
Step 6:限制并发 embedding worker 数
import asyncio
sem = asyncio.Semaphore(2) # 最多 2 个并发 embedding 请求
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):限制并发序列数
如果你用 vLLM 服务 embedding,默认的 --max-num-seqs 是按吞吐调的,不适合 8 GB 显卡。把它压下来并限制模型长度:
vllm serve BAAI/bge-large-zh-v1.5 \
--task embed \
--max-num-seqs 32 \
--max-model-len 512 \
--gpu-memory-utilization 0.85 \
--port 8001
如何确认已修复
- 重跑那个曾经崩溃的 batch。进程必须跑完,且没有掉到运行时重启。
- 在一次完整索引过程中盯着
nvidia-smi dmon -s m -d 1(或ollama ps)——峰值显存应该稳定在远低于显卡上限处,而不是钉在 100%。 - 核对数量:
len(embeddings) == len(texts)。悄悄丢请求(而非崩溃)正是过载服务的失败方式。 - 抽查一条向量的维度(
len(embeddings[0]))是否与模型匹配(nomic-embed-text是 768,bge-large-en-v1.5是 1024)——以此证明条目没有被截断成空。
预防建议
- 大模型(335M+ 参数)从
batch_size8-32 起,小模型(约 110M)从 32-64 起,再边看显存边调大。 - 除非确实需要长上下文,否则强制
max_seq_length = 512——大多数 RAG chunk 本就该是 128-512 token。 - embedding 前按长度排序 batch,减少 padding 开销(和训练里的动态 padding 是同一思路)。
- 用一个带请求队列的 embedding 服务,而不是多个并行服务抢同一块显存。
- 全量索引前先用
nvidia-smi dmon -s m -d 1监控显存。 - 把 embedding 模型放在专用 GPU 或独立显存分配上,与任何生成模型隔离。
- 生产环境固定 Ollama 版本——embedding 行为在 0.12.x 与 0.13.x 之间发生过变化,升级前先测。
- 加熔断逻辑,遇到 429/500 时暂停并重试,而不是死命冲服务。
常见问答 (FAQ)
Q:llama-server 里 --batch-size 和 --ubatch-size 有什么区别?
A:--batch-size 是调度器接受的逻辑 batch;--ubatch-size 是一次实际计算的物理微批次。对 embedding,把 --ubatch-size 降到 32-64 以约束峰值注意力显存,同时让 --batch-size 大一些保吞吐。
Q:我的 Ollama embedding 崩溃,报 caching disabled but unable to fit entire input in a batch,怎么办?
A:这条 panic 出现在 Ollama v0.13.0–v0.13.2(在 v0.12.11 上正常)。截至 2026 年 6 月,要么回退版本(用 ollama --version 确认构建),要么用 Modelfile 的 PARAMETER num_batch 64 调小运行时 batch,并在同一个 Modelfile 里用 PARAMETER num_ctx 2048 限制上下文。
Q:该用 /api/embed 还是 /api/embeddings?
A:用 /api/embed。它接受 input 数组(真正的批处理)、返回 fp32 并做 L2 归一化。旧的 /api/embeddings 只接受一个 prompt 字符串且不归一化,混用两者会让余弦相似度算错。
Q:为什么只在第 50 个 batch 崩溃,而不是第一个?
A:内存碎片。前面的 batch 分配再释放,但分配器返回的是零散的碎块。当后面某个 batch 需要一整块连续大区域时,分配就失败了。在 Linux 上,启动前设 MALLOC_ARENA_MAX=2 可减轻碎片。
Q:我需要纯 CPU 的 embedding 服务,多大的 batch 安全?
A:CPU 上用 sentence-transformers,长序列 batch_size 取 1-4 安全,短(128 token)chunk 取约 16。用 htop 盯着 RAM;进程逼近系统内存上限就把 batch 减半。
Q:能在同一个 llama-server 实例上同时跑 embedding 和生成吗? A:不建议。实例在启动时分配固定的 KV cache,要么按生成(大的自回归 cache)、要么按 embedding(无需自回归 cache)来调。给两种负载各起一个实例、用不同端口。
相关阅读
- 本地 RAG 索引重建慢到无法忍受
- LM Studio 加载模型时显存 OOM
- 本地模型冷启动后首 token 极慢
- vLLM 报 context length exceeded
- 多 GPU 没分配上,模型只跑在卡 0
标签: #local-llm #ollama #排查