Skip to content

Image message ids re-mint whenever an earlier image leaves the visible tail (membership-based ClusterCounter suffix) — prefix-cache breaks per fold, feeds #3748 storage growth #3751

Closed

Description

@ranxianglei

来源: #3748 (#3748) 分析 shadow 模式 index.db 无界增长(同一图片载荷被记录 56 次、11.06× 冗余)时发现

查重: 无。同家族近亲为已关闭的 #3642(切协议/切模型导致身份 churn,已由 #3658 的 canonical 种子修复)——本条是同一机制家族的残留面:单协议内、成员集合变化导致的 churn;触发器(成员进出 vs 方言切换)与残留机制(位置后缀 vs 方言相关种子)均不同,#3642 已按其范围关闭。#3748 是本缺陷的存储侧下游症状,两侧互链、各自管理。

机制(kernel/src/wire/)

所有图片部件的身份种子是常量:deriveCanonicalMessageId("user","text","[image]")(CANONICAL_IMAGE_TEXT,#3642)→ 全仓库所有会话的所有图片共享同一个 base id(生产数据中为 h_88b5433245c4751e)。唯一区分符是 ClusterCounter.next(kernel/src/wire/message-id.ts:53-57):同 base 第 N 次出现追加 _N,N 按本次转换路径中的出现次序计。

因此任何一张更早的图片退出可见尾部(compression fold、客户端 compaction、rewind),后面每张存活图片的后缀整体位移:第二张继承第一张空出的裸槽位,第三张变 _1……身份从「内容的函数」退化为「成员集合的函数」。wireIndex(body.messages 下标)同步漂移,加重 wrapper 字节波动。

复现(hermetic,已在 master 6f2b113 验证)

3 张图片 + 文本 → openaiToCore → 统一存储各存一次;中间模拟 fold 去掉第一张:

  • pass 1: h_…e / h_…e_1 / h_…e_2(wireIndex 2/4/6)
  • pass 2(去掉图 A 后):B h_…e_1 → h_…e,C h_…e_2 → h_…e_1 —— 两张存活图片全部重铸

base id 与 #3748 报告的生产数据逐字节一致。复现脚本归档于 #3748 工作区(.tmp/repro3748.mts)。

影响(不止存储)

  1. prefix-cache:重铸的新 raw id 会拿到新的 mNNNNN ref(cursor=highestUsed+1),重新请求时该位置的 tag 字节变化 → 每个图片会话每次 fold 一次缓存断点;
  2. 存储(Shadow ingest mints a new CAS key on every re-ingest: one image payload recorded 56x, 11.06x redundancy, 88.6% of content bytes are refcount=0 orphans #3748):host CAS 以含 id+wireIndex 的整个 wrapper 为哈希键 → 每次重铸写一份新 blob、留一份 rc=0 孤儿,实测约 17.8 GiB/天;
  3. messageRefs 增长:旧 raw id 留在 byRaw 映射直到 compaction-archive 剪除,长会话单调累积。

建议修法(待 owner 批准立项 — Kernel Boundary rule 1)

媒体部件的身份种子纳入载荷指纹(如 rawOpenaiContent/imageBase64 字节的 sha256 前缀):同像素跨成员变化保持 id,不同像素天然不同 base id。符合 MESSAGE-IDENTITY.md 纪律(内容派生、确定性、永不模糊相似)——指纹是精确内容哈希,不是相似度匹配。

替代方案(不推荐):位置稳定后缀(插入/删除后仍不稳定);全局计数器(同样依赖成员集合)。

已知代价:落地时存量图片 id 一次性重铸(升级期每会话一次 prefix-cache 断点;摘要引用的旧 ref 经 messageRefs 映射不受影响,decompress 照常)。

属 kernel/src/** 变更:需 kernel/package.json 版本 bump + PR 正文 kernel-change: 声明 + 人工评审。本 issue 同时是 #3748 决策请求中的 K 选项载体,批准前不实现。

Activity

  1. ranxianglei commented on Oct 18, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    查重: 不重复。 与已关闭的 #3642(切协议导致身份 churn,已由 #3658 的 canonical 种子修复)同属"身份 churn"家族,但触发器不同(本条是单协议内成员集合进出,#3642 是方言切换)、残留机制不同(本条是位置后缀随成员漂移,#3642 是方言相关种子)——本条正是 #3642 修复后的残留面。与 #3748(存储侧下游症状,open)互链、各管一层,不重复。近期 issue/PR(含 closed)检索无媒体身份/指纹相关的在途工作。

    标签: bug / dsh / scope/kernel / p2 —— 内核缺陷;生产证据来自 dsh lane(机制本身跨 lane,凡带图片的会话都受影响);根因在 kernel/src/wire;直接症状是每次 fold 一次缓存断点 + id churn(能用但持续劣化 = p2),其 p1 级磁盘耗尽后果由 #3748 承载。


    先交代验证基线:我本地工作树停在 fd3232fae(v0.1.199 / kernel 0.0.130),比本条引用的 master 落后 95 个提交。按"先定版本"纪律,我在 issue 指定的 6f2b113bd(v0.1.201 / kernel 0.0.133) 上逐行核对了代码(本地已有该 commit 对象,直接读它),不是在旧树上空谈。

    机制核对(在 6f2b113 确认成立)

    • 四个 codec(openai / responses / anthropic / google)解析图片时全走同一条路:deriveCanonicalMessageId(role,"text",CANONICAL_IMAGE_TEXT) 得到同一个常量 base id(生产实测 h_88b5433245c4751e),再套 clusters.next(base) 追加 _N。
    • ClusterCounter 每次转换 new 一个、只认"本次数组里的出现次序"、不记忆历史成员(kernel/src/wire/message-id.ts:50-57)。
    • ⇒ 任一更早图片被 fold / compaction / rewind 移出可见尾部后,后面存活图片的后缀整体位移(第二张继承裸槽位、第三张变 _1……),全部重铸;wireIndex 同步漂移。
    • 证据基础:代码路径逐行核对 + issue 自带 hermetic 复现(已在 6f2b113 验证)+ Shadow ingest mints a new CAS key on every re-ingest: one image payload recorded 56x, 11.06x redundancy, 88.6% of content bytes are refcount=0 orphans #3748 floor2 独立复现且与生产 index.db 逐字节一致。你要的话我可以在 6f2b113 再跑一遍定向复现给你确认。

    结构化八问(决策请求)

    1. 是什么问题:图片消息的身份 id 不是"这张图是什么"的函数,而是"它当前排在可见图片列表第几位"的函数。前面任何一张图被折叠移走,后面每张存活图的 id 就跟着变。
    2. 什么原因导致的:kernel 给所有图片用的是同一个常量身份种子,唯一区分符是按出现次序追加的位置后缀;而这个次序计数器每次转换从零重数、只看当前成员集合。成员一变,序号就漂。根因在 kernel/src/wire;Shadow ingest mints a new CAS key on every re-ingest: one image payload recorded 56x, 11.06x redundancy, 88.6% of content bytes are refcount=0 orphans #3748 的存储膨胀是它在 host CAS 侧的下游放大(CAS 拿含 id+wireIndex 的整个 wrapper 当哈希键,每次重铸就多写一份 blob、留一份 rc=0 孤儿)。
    3. 影响范围与严重程度:
    4. 怎么修复的(思路,待你批准才动手):把媒体身份种子从常量改成"常量 + 载荷指纹"——对归一化后的像素字节取 sha256 前缀并入种子。同像素跨成员变化保持同一 id,不同像素天然不同 base id。关键设计约束:指纹必须取自四个 codec 已经产出的共享 sidecar(imageBase64 / imageMediaType,即解码后的规范字节),不能取各协议各自的原始传输串(openai data-url / anthropic source.data / responses input_image / google inlineData 字节互不相同)——否则切协议又 churn,等于把 [PR] protocol-stable core identity: same logical conversation, same ids in every dialect (#3642) #3658 刚修好的东西打回去。远程 URL 图片(没有 base64 sidecar)退化为对 URL 串取指纹(同 URL 稳定)。这符合 MESSAGE-IDENTITY.md 纪律:精确内容哈希,不是模糊相似。
    5. 是否有回归问题:
      • 有意行为变更(必须披露):存量图片 id 一次性重铸——旧值 = 常量种子+位置后缀,新值 = 常量种子+载荷指纹。升级期每会话一次 prefix-cache 断点;摘要里引用的旧 mNNNNN ref 经 messageRefs 映射不受影响,decompress 照常。
      • 残留边界(诚实列出):① 两张逐字节相同的图片仍共享指纹 base、靠 _N 区分,其中一张折叠走后另一张会位移——仅限完全重复的图片对,实践罕见、有界;② 远程 URL 被 host 换成指向同一张图的另一 URL 会 churn(host 行为,非 bili 之责,罕见)。
      • K 只修图片身份:prefix-cache 症状完全消除 + ~93% 存储增长(图片部分)止住;但文本消息与 image wrapper 因 wireIndex 漂移导致的 CAS 重键仍在,那是 H 的领域。所以 K 是必要不充分。
    6. 是否破坏缓存:升级瞬间每会话一次性断点(有意,见第 5 问);此后图片 fold 不再断点。不动现有缓存 / 重试 / 游标机制本身。
    7. 是否改了设计方向:不改架构 / 边界 / 数据流。是在 [PR] protocol-stable core identity: same logical conversation, same ids in every dialect (#3642) #3658 已建立的"内容派生身份"框架内,把媒体部件的种子从"常量占位符"细化为"常量占位符+载荷指纹";sidecar 本就携带 imageBase64,数据流不变。
    8. 是否引入新配置:否。纯 kernel 内部实现,无新 env / config / 默认值。

    为什么只能在 kernel 修(Kernel Boundary rule 1 前置说明)

    身份种子是在 kernel 转换器解析时算出来的(deriveCanonicalMessageId + clusters.next);ledger / reconcile 全套机制(assignRefs 首见优先、order-centric reconcile、lastPassIds 回声判别)都依赖 id 跨转换路径稳定。host 侧若在转换后改写 id,会让持久化 ledger 与线上字节脱钩、reconcile 崩掉。所以这不是 host 能补的,只能改 kernel。

    需要你拍板的

    我的推荐(明确建议,不是甩选择题):现在批 K——它是根因,单独就能消掉 prefix-cache 症状并止住 ~93% 的存储增长;H 作为配套另立(修掉残留的 wireIndex 驱动重键,K+H 合起来才是 #3748 的完整闭环);G 继续挂在 #3466 step2 一起评估。

    批的话,我按 Kernel Boundary 走:kernel/package.json 版本 bump + PR 正文 kernel-change: 声明 + 人工评审;回归测试钉住三条——同像素跨成员保持 id、不同像素不同 id、切协议仍稳定。

    批准前我不动 kernel 代码。 等你对「批 K(以及 K 单独 vs K+H 的范围)」拍板。

  2. ranxianglei commented on Oct 19, 2026

    @ranxianglei
    OwnerAuthor

    修复已提交:PR #3767。

    修法:图片 core 的身份种子增加媒体指纹槽(第六槽,仅媒体 core 有)——数据图取其字节的 sha256,URL 引用图取 URL 字符串,legacy 复数形状取各部件指纹有序串。一张图变化只重铸它自己那条 core;_N 序号回归本职,仅区分字节完全相同的重复图。文本/工具消息种子不变,非图片 id 逐字节保持。

    验证:跨协议金桩新增 4 测试(局部伤害/重复图 _1/URL 退化/legacy-拆分同 id),kernel 全量 1313/1313,src 图片家族(image-restore/issue3588-offload/issue1843/issue1800/issue2558/issue3398 等)全绿。

    存量图片会话一次性 re-key,由 #3641 判决分类与 #3756 快照渲染两层兜底吸收。

  3. ranxianglei commented on Oct 19, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    独立审了 PR #3767(head 826542428,单 commit,基于当前 master / kernel 0.0.135→0.0.136)。结论:机制正确、完整、可合;有一处测试缺口建议补上再合(非阻塞,但值得当场补)。我没有本地跑全量 kernel 套件(那是 CI 的活),以下均为逐行读 diff + 把 issue 原始复现路径在改后代码里推演核对得出;我也没有合并(kernel 变更人工评审 + 人合并)。

    结构化八问(PR review)

    1. 这个 PR 要改什么:把图片消息 id 从"全族共享一个 base、靠 _N 位置后缀区分"改成"按载荷寻址"——一张图变化只重铸它自己那条 core。
    2. 根因对不对:对。根因正是"常量身份种子 + 成员敏感的位置后缀",本 PR 在源头(kernel 转换器)把种子改成内容派生,修的是根因不是症状。
    3. 影响范围:所有带图片的会话、所有 lane。存量图片会话一次性 re-key(有意行为变更,已披露),由 Snapshot as session reconstruction: identity claim order takes priority, message hash demoted to reference signal (root- | 快照即会话重构:身份认领顺序优先,消息哈希降级为参考信号(根治 #3588/#3640 一类误判) #3641 判决分类 + Snapshot rendering: proxy-lane outbound is now rendered authoritatively server-side; existing sessions are immune to ker | 快照渲染:proxy 车道出站从服务端正史渲染,存量会话对内核升级免疫(#3752 根治) #3756 快照渲染两层兜底吸收。
    4. 怎么修的(机制):canonical 种子加第六槽 mediaFingerprint,且仅媒体 core 有——数据图取 hash(imageBase64)(解码后的规范字节)、URL 引用图取 URL 字符串、legacy 复数形状取各部件指纹有序串。
    5. 是否有回归问题(我逐项核过既有行为):
      • 文本/工具 id 逐字节保持 ✓ —— deriveCanonicalMessageId 只在 mediaFingerprint !== undefined 时追加第六槽,非媒体 core 种子一字不动;openai/responses 的无图 legacy core 用 || undefined 显式不加槽,老 id 原样保留。
      • 跨协议同图同 id ✓ —— 四个 codec 我都核了取的是同一份归一化 base64 字节(openai parseDataUrl().base64 / anthropic source.data / responses parseDataUrl().base64 / google inlineData.data),不是各自传输串;所以 [PR] protocol-stable core identity: same logical conversation, same ids in every dialect (#3642) #3658 修好的跨协议稳定没被打回去。URL 图四家一致退化为 URL 串。
      • responses legacy 单图 shape 与拆分 shape 同 id ✓(合并形态 flatMap 出的指纹与拆分路径单图指纹字节相同)。
      • 唯一有意的漂移就是存量图片 re-key,已在第 3 问披露并有兜底。
    6. 是否破坏缓存:升级瞬间每会话一次断点(有意);此后图片 fold 不再断点。补充一点关系澄清:CHANGELOG 显示 Snapshot rendering: proxy-lane outbound is now rendered authoritatively server-side; existing sessions are immune to ker | 快照渲染:proxy 车道出站从服务端正史渲染,存量会话对内核升级免疫(#3752 根治) #3756(服务端快照渲染,BILI_ROLLOUT_SNAPSHOT_RENDER 默认 0%)也从渲染侧治 Image message ids re-mint whenever an earlier image leaves the visible tail (membership-based ClusterCounter suffix) — prefix-cache breaks per fold, feeds #3748 storage growth #3751——两者互补不冗余,且 [PR] Payload-addressed image message IDs: seed adds media fingerprint slot (#3751) | 图片消息 id 按载荷寻址:种子增加媒体指纹槽(#3751) #3767 是源头修法,不依赖 Snapshot rendering: proxy-lane outbound is now rendered authoritatively server-side; existing sessions are immune to ker | 快照渲染:proxy 车道出站从服务端正史渲染,存量会话对内核升级免疫(#3752 根治) #3756 灰度开启即独立生效(Snapshot rendering: proxy-lane outbound is now rendered authoritatively server-side; existing sessions are immune to ker | 快照渲染:proxy 车道出站从服务端正史渲染,存量会话对内核升级免疫(#3752 根治) #3756 关着也止住图片重铸)。
    7. 是否改了设计方向:不改架构/边界/数据流。是在 [PR] protocol-stable core identity: same logical conversation, same ids in every dialect (#3642) #3658"内容派生身份"框架内细化媒体种子;sidecar 本就携带 imageBase64,数据流不变。
    8. 是否引入新配置:无新 env/config/默认值。(种子前缀的 idSalt 是 Snapshot rendering: proxy-lane outbound is now rendered authoritatively server-side; existing sessions are immune to ker | 快照渲染:proxy 车道出站从服务端正史渲染,存量会话对内核升级免疫(#3752 根治) #3756 已加的测试缝,非本 PR。)

    发现(按重要性)

    ① 测试缺口(建议补,非阻塞):新增 4 个测试都很有牙(局部伤害/重复图 _1/URL 退化/legacy-拆分同 id),但没有一个直接复现 issue 的原始场景——"3 张不同图 → fold 去掉第一张 → 两张存活图 id 逐字节不变"。现在的 Test 1 用的是"改某张图的字节"来证内容寻址,逻辑上能推出成员变化也稳(id 只依赖自身内容),但没有把删成员这条直接钉死。同文件里 tool-call 已有"删掉两个兄弟之一只重排另一个"的对偶测试,图片这条缺了对应项,不符合该文件自己的既有家规。建议补一条:三张 distinct data-url 图 → 转换取 id → 去掉首张再转换 → assert 两张存活图 id 与前次完全相等。我可以现在就推到 PR 分支补上(一个独立标注 commit),你说一声即可——考虑到 kernel 是人评审档,我没擅自动你的分支。

    ② 小提示(仅记录,非本 PR 机制、非阻塞):google 的 fileData(URI 引用型)媒体不是独立 image core,而是挂到前序文本 core 的 raw sidecar;而 openai/anthropic/responses 的 URL 图是独立 image core(带 URL 指纹)。所以"同一张以 URL 引用的图"在 google 与其它 lane 之间 core 结构不一致——这是早于本 PR 就存在的跨协议粒度差异(#3642 那一层),大概率是有意为之(fileData 是不透明文件引用 vs 可解码图片),也不影响 #3751 的成员位移机制(fileData 从不走 ClusterCounter 当唯一区分符)。留个记号即可,不建议塞进这个 PR。

    判定

    代码层面:可合。前置条件:①(建议)补上第 ① 条的直接复现测试;② CI 的 kernel 全量 + bili 套件绿(作者已报 kernel 1313/1313、src 图片家族全绿,最终以 pushed head 的 CI 为准);③ kernel 人工评审 + 你合并。要不要我把那条测试推上去?

  4. ranxianglei commented on Oct 19, 2026

    @ranxianglei
    OwnerAuthor

    ai: zhipuai-lb/glm-5.3

    修复已合并:PR #3767(kernel 0.0.136)——图片消息 id 改为按载荷寻址(sha256 指纹当种子,URL 图用 URL 串),一张图变化只重铸它自己的 id,sibling 逐字节稳定;_N 序号回归只区分真重复。非图片消息种子不变,零迁移。

    随下一个版本发布后,本 issue 描述的「前图离场→后图全体重排→每轮 fold 断缓存」不再发生。存量图片会话升级轮有一次一次性 re-key(#3641 判决分类 + #3756 快照渲染吸收)。

  5. added a commit that references this issue on Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions