Repository navigation
feat: configurable embedding cache precision (float32) for DPP/SSD sort - #271
Conversation
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.
|
|
| out := make([]float32, len(vec)) | ||
| for i, v := range vec { | ||
| out[i] = float32(v) | ||
| } | ||
| return out |
There was a problem hiding this comment.
【阻塞 · 数值正确性】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" 大数值场景的缓存冷/热回归测试。
| 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 |
| s.embCache.Put(id, s.storeEmbedding(vector)) | ||
| if item, ok := itemMap[id]; ok { | ||
| item.Embedding = cloneEmbedding(vector) | ||
| } |
There was a problem hiding this comment.
【阻塞 · 结果可复现性】缓存存舍入副本、item 存全精度副本,排序结果依赖缓存冷热(与 sort/ssd_sort.go:253 为同一缺陷)
float32 模式下,此处 Put 进缓存的是 storeEmbedding(vector) 的舍入副本,而下方 item 仍持有全精度 cloneEmbedding(vector) —— 同一向量在同一时刻存在两种表示:
- 分数随缓存冷热漂移:同一 item 在"填缓存的请求"与"命中缓存的请求"中打分相差 ~1e-7。DPP 贪心选择按边际增益排序,接近的候选可能翻转选择顺序 → 同数据同配置结果不可复现,离线回放 / AB 评估 / 问题排查全部失效
- 单请求内混合精度:命中缓存的 item(舍入版,L218)与未命中的 item(全精度版,L241)混入同一个核矩阵,item 间相对误差非均匀,违背"配置 float32 即统一 float32 行为"的契约
- 该双表示也放大了
embedding_query.go的 float32 溢出问题的隐蔽性——填缓存的请求看起来完全正常
建议 round-trip 一次,缓存与 item 共用同一表示。安全性已核对:
- 默认 float64 路径字节级不变(
storeEmbedding原样返回,loadEmbedding的[]float64分支即cloneEmbedding),存量行为零影响 loadEmbedding返回新克隆,不与缓存切片别名,且保留 KernelMatrixappend(embs, 1)依赖的len+1容量契约
| 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) | |
| } |
| s.embCache.Put(id, s.storeEmbedding(vector)) | ||
| if item, ok := itemMap[id]; ok { | ||
| item.Embedding = cloneEmbedding(vector) | ||
| } |
There was a problem hiding this comment.
【阻塞 · 结果可复现性】同 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 路径字节级不变,修复零风险:
| 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.
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 全绿)