Ollama + bge-m3 Embedding 產生 NaN 導致寫入失敗:完整診斷與修復

Ollama + bge-m3 embedding 對長中文文字產生 NaN,root cause 是 flash attention F16 overflow。一個環境變數修復:OLLAMA_FLASH_ATTENTION=0

Ollama + bge-m3 Embedding 產生 NaN 導致寫入失敗:完整診斷與修復

如果你在用 Ollama 跑 bge-m3 做 embedding,遇到這個錯誤:

failed to encode response: json: unsupported value: NaN (status code: 500)

恭喜,你踩到一個已知但還沒被修好的 bug。這篇記錄我從「完全看不到錯誤」到「找到 root cause 並修復」的完整過程。

症狀

我的 mem0 MCP server 負責 AI Agent 的記憶寫入。幾個月來,add_memory 對長中文文字穩定失敗(短文字偶爾能過),但錯誤訊息只顯示一個莫名其妙的 re module 錯誤——完全沒有幫助。

因為有意義的記憶通常都是長文字(決策紀錄、技術筆記、願景描述),實際使用上大部分有價值的寫入都會失敗。所有 Agent 都回報過寫入失敗,但沒人知道為什麼。

第一層:被吞掉的錯誤訊息

第一個問題出在我的 server.py 裡的錯誤處理函數:

def _safe_error(exc):
    msg = str(exc)
    msg = re.sub(r"/[^\s:]+\.\w{1,4}", "<internal>", msg)  # 用了 re
    # ...

但檔案頂部沒有 import rere 只在另一個函數裡有 local import,scope 不到這裡。

結果:所有 except handler 呼叫 _safe_error(e) 時,NameError: name 're' is not defined 覆蓋了真正的錯誤。你永遠看不到底層發生了什麼。

修復:加一行 import re 到檔案頂部。

第二層:真正的錯誤浮出水面

修完 import re 後,重啟 server,再測一次 add_memory——真正的錯誤出現了:

failed to encode response: json: unsupported value: NaN (status code: 500)

查 Ollama server 的 log:

POST /api/embeddings → 200 OK    # 第一次 embedding(dedup 搜尋)
POST /collections/mem0_memories/points/query → 200 OK   # Qdrant dedup
POST /api/embeddings → 500        # 第二次 embedding(寫入)
llm embedding error: failed to encode response: json: unsupported value: NaN

第一次 embedding 成功,第二次失敗。bge-m3 對特定輸入產生了 NaN embedding vector。

Root Cause:Flash Attention F16 Overflow

查了 Ollama 的 GitHub issues,找到:

根因:llama.cpp 在 flash attention 開啟時,embedding 模式(無 KV cache)會做 F32 → F16 轉換。如果中間值超過 F16 最大值(≈65504),就會 overflow 成 Inf,softmax 之後變成 NaN

長文字(尤其是中文 ~150 字以上)更容易觸發,因為 attention score 的數值範圍更大。

修復:一個環境變數

# 關閉 flash attention
export OLLAMA_FLASH_ATTENTION=0

如果你用 systemd 管理 Ollama:

# /etc/systemd/system/ollama.service
[Service]
Environment="OLLAMA_FLASH_ATTENTION=0"
sudo systemctl daemon-reload
sudo systemctl restart ollama

關閉 flash attention 後,embedding 會稍慢一點,但數值穩定。測試結果:NaN 完全消失,寫入成功。

額外防護:NaN 安全的 JSON 序列化

即使 Ollama 端修好了,server 端也應該有防護。我加了一個 helper:

def _safe_json(obj, **kwargs):
    """JSON dumps with NaN protection."""
    return json.dumps(_sanitize_nan(obj), default=str, allow_nan=False, **kwargs)

def _sanitize_nan(obj):
    if isinstance(obj, float) and (math.isnan(obj) or math.isinf(obj)):
        return None
    if isinstance(obj, dict):
        return {k: _sanitize_nan(v) for k, v in obj.items()}
    if isinstance(obj, list):
        return [_sanitize_nan(v) for v in obj]
    return obj

你該注意的 warning

如果你的 Ollama log 出現這行:

init: embeddings required but some input tokens were not marked as outputs -> overriding

這本身是無害的(很多 embedding model 都會出現),但它代表 Ollama 正在 override 模型的 tensor 配置。搭配 flash attention 開啟時,這個 override 路徑更容易觸發 NaN。

TL;DR

問題 解法
Ollama + bge-m3 embedding 產生 NaN OLLAMA_FLASH_ATTENTION=0
NaN 導致 500 Internal Server Error server 端加 _sanitize_nan() 防護
錯誤訊息被其他 bug 覆蓋看不到 檢查你的 error handler 有沒有自己壞掉

等 Ollama PR #14739 合併後,可以考慮重新開啟 flash attention 測試。在那之前,關掉它是最穩的 workaround。

如果你也在用 bge-m3 做中文 embedding 而且偶爾遇到神秘的 500 error——現在你知道為什麼了。


我是江中喬,一位具有 TPM 與產品管理背景的 AI 系統建構者,目前專注於 AI 認知增強系統與多 Agent 協作架構的設計與實踐。