Skip to content

feat: configurable embedding cache precision (float32) for DPP/SSD sort - #271

Merged
bruceding merged 4 commits into
masterfrom
features/dpp-ssd-embedding-precision
Nov 5, 2026
Merged

bruceding merged 4 commits into
masterfrom
features/dpp-ssd-embedding-precision

Conversation

@symphony09

Copy link
Copy Markdown
Collaborator

feat: DPP/SSD 排序支持可配置 embedding 缓存精度(float32)

概述

新增 EmbeddingPrecision 配置,DPP/SSD 的 embedding 缓存可用 float32 存储:payload 减半,常驻内存 dim=128 时降约 39%(高维接近 50%),排序基本不变;非法值启动即 panic,不再静默回落 float64。

改动

  • recconf:DPPSortConfig/SSDSortConfig 新增 EmbeddingPrecision(""/float64 默认全精度,float32 降精度)。
  • embedding_query.go:storeEmbedding/loadEmbedding 按精度存取;normalizeEmbeddingPrecision 校验,只接受 ""/float64/float32。
  • dpp_sort.go/ssd_sort.go:构造函数连 DB 前校验精度,非法值 panic;loadEmbeddingCache 走 store/load。

配置

在 DPPConf/SSDConf 内加一行 "EmbeddingPrecision": "float32"(大小写/空格不敏感,缺省即 float64)。

测试(sort/embedding_precision_test.go,linux 全绿)

  • 值保真:仅 f64 源→f32 缓存有有界差异,其余逐位无损。
  • 排序:无损场景顺序完全一致;f64→f32 实测 DPP 变 2/24、SSD 0/24。
  • 校验:非法值时两个构造函数均 panic。
  • 内存:float64≈1314B/条 vs float32≈803B/条(−38.9%)。

Add EmbeddingPrecision to DPPSortConfig/SSDSortConfig. Setting it to float32 stores cached embedding vectors as []float32, halving embCache memory; default (empty/float64) keeps the original []float64 path with no conversion or allocation overhead.
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@symphony09
symphony09 requested a review from bruceding November 4, 2026 08:25
Comment thread sort/embedding_query.go
Comment on lines +240 to +244
out := make([]float32, len(vec))
for i, v := range vec {
out[i] = float32(v)
}
return out

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

【阻塞 · 数值正确性】float32 转换缺少溢出保护,可将合法有限值缓存为 ±Inf

NormalizeEmb="false" 时向量未归一化、分量无界;|v| > MaxFloat32 (≈3.4e38) 的合法有限 float64(如脏数据 1e39)经 float32(v) 变为 ±Inf。上游 parseEmbedding 的 IsNaN/IsInf 校验发生在转换之前,拦不住这种情况。

故障链存在时间不对称,非常隐蔽:

  • 缓存未命中请求:item 用原始 float64 向量(cloneEmbedding(vector)),本次计算完全正常 ✅
  • 缓存已被写入 Inf ☠️:后续所有命中请求经 loadEmbedding 把 Inf 回灌到 DPP 核矩阵 / SSD 投影,Inf-Inf=NaN、0×Inf=NaN 扩散到整个矩阵,排序静默损坏且零报错
  • embCache 是 sorter 实例级共享缓存(仅 tableSuffix 变化时失效),一条脏数据持续污染所有后续请求直到过期

现有的 embMissThreshold、size-do-not-match 检查均只覆盖"缺失/长度"维度,无法拦截数值非法。

建议:转换后检查结果,有限输入变为非有限时整条向量回退为 []float64 存储(loadEmbedding 已同时兼容两种类型,缓存内混合类型是安全的,默认路径不受影响)。并请补充 EmbeddingPrecision="float32" + NormalizeEmb="false" 大数值场景的缓存冷/热回归测试。

Suggested change
out := make([]float32, len(vec))
for i, v := range vec {
out[i] = float32(v)
}
return out
out := make([]float32, len(vec))
for i, v := range vec {
f := float32(v)
// A finite float64 with |v| > MaxFloat32 (~3.4e38) converts to ±Inf.
// parseEmbedding's IsInf check runs before this conversion, and with
// NormalizeEmb="false" vectors are unbounded — fall back to []float64
// storage for this entry instead of poisoning later cache hits.
if math.IsInf(float64(f), 0) && !math.IsInf(v, 0) {
return vec
}
out[i] = f
}
return out

Comment thread sort/dpp_sort.go Outdated
Comment on lines 239 to 242
s.embCache.Put(id, s.storeEmbedding(vector))
if item, ok := itemMap[id]; ok {
item.Embedding = cloneEmbedding(vector)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

【阻塞 · 结果可复现性】缓存存舍入副本、item 存全精度副本,排序结果依赖缓存冷热(与 sort/ssd_sort.go:253 为同一缺陷)

float32 模式下,此处 Put 进缓存的是 storeEmbedding(vector) 的舍入副本,而下方 item 仍持有全精度 cloneEmbedding(vector) —— 同一向量在同一时刻存在两种表示:

  1. 分数随缓存冷热漂移:同一 item 在"填缓存的请求"与"命中缓存的请求"中打分相差 ~1e-7。DPP 贪心选择按边际增益排序,接近的候选可能翻转选择顺序 → 同数据同配置结果不可复现,离线回放 / AB 评估 / 问题排查全部失效
  2. 单请求内混合精度:命中缓存的 item(舍入版,L218)与未命中的 item(全精度版,L241)混入同一个核矩阵,item 间相对误差非均匀,违背"配置 float32 即统一 float32 行为"的契约
  3. 该双表示也放大了 embedding_query.go 的 float32 溢出问题的隐蔽性——填缓存的请求看起来完全正常

建议 round-trip 一次,缓存与 item 共用同一表示。安全性已核对:

  • 默认 float64 路径字节级不变(storeEmbedding 原样返回,loadEmbedding 的 []float64 分支即 cloneEmbedding),存量行为零影响
  • loadEmbedding 返回新克隆,不与缓存切片别名,且保留 KernelMatrix append(embs, 1) 依赖的 len+1 容量契约
Suggested change
s.embCache.Put(id, s.storeEmbedding(vector))
if item, ok := itemMap[id]; ok {
item.Embedding = cloneEmbedding(vector)
}
stored := s.storeEmbedding(vector)
s.embCache.Put(id, stored)
if item, ok := itemMap[id]; ok {
item.Embedding = s.loadEmbedding(stored)
}

Comment thread sort/ssd_sort.go Outdated
Comment on lines 253 to 256
s.embCache.Put(id, s.storeEmbedding(vector))
if item, ok := itemMap[id]; ok {
item.Embedding = cloneEmbedding(vector)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

【阻塞 · 结果可复现性】同 sort/dpp_sort.go:239:缓存与 item 双表示,结果依赖缓存冷热

float32 模式下缓存写入舍入副本、item 保留全精度副本,同一 item 在填缓存请求与命中请求中打分相差 ~1e-7,SSD sliding-window 相似度比较可能翻转;单请求内命中/未命中 item 混合两种精度,违背配置契约,结果不可复现。

已核对此处的一个额外安全点:ensurePosSimilarity 的 vector = append(vector, 1) 发生在 Put 之前(L243-245),常数 1.0 在 float32 中精确可表示,round-trip 无损。

默认 float64 路径字节级不变,修复零风险:

Suggested change
s.embCache.Put(id, s.storeEmbedding(vector))
if item, ok := itemMap[id]; ok {
item.Embedding = cloneEmbedding(vector)
}
stored := s.storeEmbedding(vector)
s.embCache.Put(id, stored)
if item, ok := itemMap[id]; ok {
item.Embedding = s.loadEmbedding(stored)
}

…tion

Address PR #271 review:

- storeEmbedding: a finite component with |v| > MaxFloat32 converts to +/-Inf in float32; fall back to []float64 for that entry instead of caching an Inf that would poison every later cache hit (reachable when NormalizeEmb=false leaves vectors unbounded).

- DPP/SSD loadEmbeddingCache: read the item embedding back from the stored value so the cache and the item share one representation, removing the cold/hot ranking drift under float32. The float64 default path stays byte-identical.
@bruceding
bruceding merged commit ac634e2 into master Nov 5, 2026
3 of 5 checks passed
@bruceding
bruceding deleted the features/dpp-ssd-embedding-precision branch November 5, 2026 03:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants