Repository navigation
[性能回归] 0.1.193 长会话本地耗时从 1.3s 升至 3.2s:无用 SimHash 计算与重复历史转换 #2824
Description
问题
bili 从 0.1.192 自动更新到 0.1.193 后,同一长会话每轮 local 从约 1.3 秒升至约 3.2 秒;已定位到两项请求入口新增开销,离线 A/B 可消除大部分回归且保持待转发 body 完全一致。
线上证据
时间 2026-10-12 00:13 更新。仍为同一会话、同一 k3-256k 模型,入站仅从 24408→24410 条消息,大小均约 31.9 MiB。更新前最近 10 请求 local 中位数 1336 ms,更新后最近 10 请求为 3175.5 ms(约 2.38 倍,增加 1839.5 ms)。新版第一请求 3986 ms,其余 16 请求仍为 3072–3370 ms。
local 是读取 body 前到转发前的本地阶段,尚未进入上游生成,因此本次回归不能用上游推理慢或首轮冷启动解释。
根因 1:chainFingerprint 无条件计算不用的 SimHash
server/handle.ts 的已识别会话分支调用 chainFingerprint。#2807 改成 wire-neutral 域后,该方法调用 neutralFingerprints(turns),后者无条件计算每个 turn 的 sketchForCanonical,但 chainFingerprint 返回值完全不包含 sketches,identified note 也不保存 sketches。
affinity-simhash.ts 的 SKETCH_MEMO_CAP=8192。重建真实长历史有 24447 个 neutral turns:每轮调用 sketch 24447 次,首轮真正计算 SimHash 22431 次,后续仍为 22391 次。顺序扫描超过缓存容量的输入使 LRU 持续淘汰下一轮需要的项,无法暖起来。
同输入交替顺序 8 组 A/B,去掉前 2 组,chainFingerprint 中位数:
- 0.1.192:521.24 ms
- 0.1.193:1939.74 ms
- 临时原型(仅 chainFingerprint 调用 neutralFingerprints 时不生成 sketch):602.81 ms
8 组原版与原型返回值均 deepEqual。setSimhashAdoptionEnabled(false) 也无法避免这部分计算:仍有 22391 次 SimHash、约 2040 ms。
建议将确定性 hash 与相似度 sketch 的需求分开;不改变 wire-neutral 身份语义,不通过扩大 LRU 掩盖无用计算。其他只使用 .hashes 的 neutralFingerprints 调用方也需一起审计;匿名相似度收养所需的 sketches 必须保留。
根因 2:无图片请求也额外转换两次完整历史
旧版图片索引受 stripImages 条件保护。新版常规请求先 buildIncomingImageIndex,准备后再 refreshIncomingImageIndex(内部再次 build);每次 build 都重新 wire→core 全历史转换后才发现没有图片。
在无图片 fixture 的完整请求暖轮中:旧版 openaiToCore 1 次/166 ms;新版 3 次/518 ms。其中两次图片索引构建合计约 392 ms,包含转换耗时,不能重复相加。新增转换约 351 ms。
建议复用 prepare 已产生的 core 消息或只在确有图片时建立索引,保留图片 ref 刷新和恢复契约。
完整请求 A/B
采用真实历史近似重建、同一状态副本、模拟 HTTP 入口,固定时钟;禁止真实上游转发与持久化,跳过 preflight。fixture 24463 条消息/42.2 MiB,reasoning 和部分 tool-result 前缀未逐字还原客户端策略,不是逐字 HTTP 重放,绝对耗时不等于线上预测。
临时原型仅跳过上面的无用 sketches,并针对已确认没有图片的 fixture 返回空图片索引(验证用途,尚不是生产补丁)。暖轮:
| 请求 | 新版 local | 原型 local |
|---|---|---|
| 第 2 轮 | 3363 ms | 1943 ms |
| 第 3 轮 | 3862 ms | 1946 ms |
三轮最终待转发 body 的 SHA256 都是 95a62eb6684d98451c14d46a959b393725196be921cf1b523b5abf54142fbe16。kernel 本身仅有几十毫秒级附加成本,未显示秒级回归。
复现方向与回归覆盖
在 v0.1.192 和 v0.1.193 分别创建含 >8192 个不同 turn 的全历史请求,多次原样重发并计时 new PrefixAffinityResolver().chainFingerprint(messages)。输入应包含足够不同的 canonical 内容,避免所有 turn 共用一个 memo key。加入无图片、明确会话 ID 的真实请求入口场景,检查每轮转换次数和输出一致性。
回归覆盖应确保 identified 路径不计算不用的 sketches、匿名路径仍能相似度收养、超过 8192 的重发不会再次全量计算 sketch、无图片请求不做两次额外 wire→core;保留实际图片恢复/ref 刷新场景。
重复问题筛查
已检索 open/closed 的 simhash/perf、neutralFingerprints、affinity/性能、0.1.193/耗时、SKETCH_MEMO_CAP、图片索引/performance,未找到相同回归。#2791/#2807 是跨协议连续性修复,本问题是其 identified 指纹调用产生的 CPU 回归;#2817 的模型切换子会话 fold 继承问题不同。调查时 master 的 prefix-affinity.ts 与 v0.1.193 相同,仍存在该无用计算。
Activity
ranxianglei commentedon Oct 11, 2026
[bot] 🏷
两个根因均已独立复现并修复,PR #2828 已开:https://github.com/ranxianglei/billion-context/pull/2828(3 commits:fix RC1 / fix RC2 / test,基于 master 47386e6)
结构化结论(八问)
chainFingerprint迁到 wire-neutral 域后走融合 hash+sketch 路径,但其返回值不含 sketches、identifiednote()也不存 → identified 会话每轮白算 ~22K 次 SimHash;SKETCH_MEMO_CAP=8192对超过自身长度的顺序扫描无法暖起来,重发也全量重算。neutralFingerprints(turns, sketchCap = 0)——确定性 hash 与相似度 sketch 按调用方需求分离。仅resolve()(唯一会读 sketches 的消费者,simhash adoption)计算且封顶SKETCH_MAX_DEPTH=2048;chainFingerprint/findResumeParent/findResumeParentWithin/chainContinues传 0。所有返回值与改前逐字节相同,只降 CPU。顺手删除了 dsh anthropic-messages lane bypasses native-intercept fetch patch: anonymous pfa-* session, fold ledger orphaned, silent ~182s re-fold on cross-protocol switch #2791 后零引用的 v1 死代码itemFingerprints(同机制族,独立标注于 commit message)。bodyMightCarryImages(bodyText, protocol)原文预检,JSON.parse 后算一次(protocol 此时已稳定)。刻意 over-approximate:markers 覆盖messageImageBytes索引的全部 shape(openai/responses/anthropic source.url 的 data-URL;anthropicsource.type:"base64";googleinlineData顶层+functionResponse 嵌套),误报代价是一次浪费转换、漏报不可能丢图。谓词为 false 时两处产出与今天无图 body 的 build 结果完全相同(空索引/原索引不动);有图会话两个 build 照跑,stripImages 滑窗剥离的图片无恢复通道,且滑窗每轮击穿 prompt cache(billion-context-pi#593 同族缺口) #1995/建议:compress.stripImages默认开启——否则带图消息不可压缩会形成只升不降的输入地板撑爆上下文 #2607 ref 刷新+恢复契约保留。simhash,coverage≥0.9,测试钉住);暖重发零重算(memo 不再被无用条目冲刷);全部可索引图片 shape 仍被索引(非空洞验证:每个 fixture 同时断言 build 出 ≥1 条);负例保持空索引;guarded ≡ unconditional 全语料 deep-equal。定向套件 prefix-affinity/simhash/image-restore/fold-images-archive 58/58 + orphan 5/5 + typecheck 绿;全量套件交 CI。pruneRetrieveImgExports)路径未动。{type:"image_url"}(非数组)会触发预检但 kernel 从不为非数组 content sidecar 图片——两种构建都返回 ∅,属 kernel 既有行为,不在本 PR 范围(已在 PR 描述标注 audited-safe)。行为变更披露(CPU-only):identified 路径 SimHash/请求 ~22K→0;匿名冷 resolve sketch N→≤2048(
note()原本就 slice 到 2048、alignment 从不读 stored cap 之外,可证等价);无图请求额外 wire→core 转换 2→0;有图请求完全不变。回归测试
tests/issue2824-regression.test.ts(7 个用例)钉住 issue 要求的性质:identified 路径零 sketch、>8192 重发零重算、cap 外收养仍有效、无图请求零额外转换、有图场景保留。判定:两个根因都在源头修掉(计算需求分离 / 转换前预检),不是症状层打掩码;CI 绿后可合。Merge pull request #2828 from ranxianglei/2026-10-12_issue2824-sketch…
fix: compute SimHash sketches only where adoption can read them (ranx…
fix: skip incoming-image index builds for provably image-free bodies (r…
test: pin ranxianglei#2824 perf regression (sketch opt-in + image-ind…
kernel: openai non-array content tolerance + golden regen + inverted …
image-restore: conversion failures stay fail-open but WARN once per s…