Prhub

#2881 Dashboard: handle multiple TITO leaves per sample index

原始 PR 作者 Shi-Dong 合并时间 2026-09-03 04:35 文件变更 11 提交数 5 评论 6 代码增减 +376 / -70

执行摘要

修复 Dashboard 无法区分共享 sample_index 的 TITO v2 叶子样本,引入 occurrence 键解决数据混淆与 API 500 错误。

正如 PR body 所述:'TITO v2 can emit multiple leaf samples that intentionally share one Sample.index。The dashboard previously keyed all rollout and train data only by that index, conflating distinct leaves。On a real run this made /api/rollout/549/summary fail with HTTP 500 because two rank-0 rows for sample 35147 had different lengths.'

该 PR 值得精读。它不是一个简单的 bugfix,而是针对一个设计上的歧义(共享 sample_index)提出的系统性解决方案,贯穿了后端数据模型、API 契约、前端路由和 UI 展示。review 过程中由自动化工具指出并推动修复的两个关键正确性问题(KeyError 和 occurrence 编号错配)极具洞察力,展示了对数据一致性和边缘情况的深入考虑。其引入的 (sample_index, occurrence) 键模型和对历史数据的兼容处理方式,对类似场景有很好的借鉴意义。

讨论亮点

核心讨论由 claude[bot] 发起并推动,主要围绕两个正确性缺陷:

  1. load_joined 中的 KeyError 风险:claude[bot] 指出,新代码在循环中直接使用 index[key[0]] 查找,当样本在训练数据中完全缺失时(partial train coverage),会引发 KeyError 导致 API 500,而旧代码通过遍历 index.items() 天然容忍此情况。作者在后续提交 (4acef78c) 中修复此问题,使用 index.get(key[0], []) 并在无匹配时跳过构建 TrainRow。
  2. trajectory_messages 中的 occurrence 编号错配:claude[bot] 敏锐地发现,轨迹侧车文件只包含有对话记录的样本,因此其内部 occurrence 编号可能与全样本列表的编号不一致(例如,当第一个叶子样本没有对话时,第二个叶子样本的编号会错位)。这可能导致 API 404 或返回错误数据。作者在后续提交 (25169568) 中修复此问题:在 debug_data.py 的 trajectory_rows 函数中计算并持久化正确的 sample_occurrence;在 dump_reader.py 中读取侧车文件时优先使用文件中记录的 sample_occurrence,并对旧版侧车文件(无此字段)采用基于文件位置的回退逻辑。Zhichenzzz 对此也表示同意。
  3. 可选改进意见:claude[bot] 在 views_rollout.js 的 anatomy 面板中提出了一个非阻塞建议:rowsByIndex 的映射逻辑在遇到重复 sample_index 时只保留最后一个 occurrence,导致点击 lane 时跳转到错误的叶子。此问题在本次合并时未修复,被标记为可选。

实现拆解

  1. 核心数据模型与 Join 逻辑重构 (dump_reader.py):新增 SampleKey = tuple[int, int] 类型表示 (sample_index, occurrence) 键。新增 _sample_keys(samples) 函数为每个样本生成唯一的 occurrence 编号。新增 _matching_train_locations 函数用于在旧式训练数据转储中,通过比对响应长度、总长度和 tokens 内容来匹配正确的物理行,而非仅靠 sample_index。JoinedRollout.train_rows 字典的键从 int 改为 SampleKey。SUMMARY_VERSION 从 5 升至 6 使旧缓存失效,SUMMARY_COLUMNS 新增 sample_occurrence 列。
  2. API 层与后端数据生产适配:server.py 中 sample_messages 和 sample_tokens 接口新增 occurrence 查询参数(默认 0 以保持向后兼容)。debug_data.py 中的 trajectory_rows 函数现在为每个样本在侧车 JSONL 文件中持久化正确的 sample_occurrence。
  3. 前端路由与视图更新:app.js 的路由解析现在从 URL 参数中提取 occurrence。views_rollout.js 和 views_tokens.js 引入新的 sampleHash 函数,生成包含 occurrence 参数的哈希路由。UI 元素如散点图标签、组导航面板均更新为使用完整的 (sample_index, sample_occurrence) 键进行定位和跳转。
  4. 测试覆盖:dummy_dump.py 新增 duplicate_first_sample_index 参数以模拟多叶子场景。新增关键测试用例:test_duplicate_sample_indices_keep_tito_leaves_distinct (验证 join 逻辑)、test_partial_train_coverage_degrades_to_null_rows (验证训练数据缺失时的降级处理)、test_trajectory_occurrences_follow_the_full_sample_order (验证轨迹侧车文件的 occurrence 编号正确性) 和 test_duplicate_tito_leaves_are_independently_viewable (端到端 API 测试)。
文件 模块 状态 重要度
miles/dashboard/dump_reader.py 仪表盘读取器 modified 8.76
miles/dashboard/static/views_tokens.js Token 视图 modified 7.3
miles/dashboard/static/views_rollout.js Rollout 视图 modified 7.0

关键符号

_sample_keys _matching_train_locations load_joined trajectory_messages sampleHash openTokens groupNavPanel sample_messages sample_tokens

关键源码片段

miles/dashboard/static/views_tokens.js core-logic

Token 查看视图,实现了携带 occurrence 参数的路由跳转和 UI 导航,是前端适配的关键。

function sampleHash(rolloutId, row, evaluation) {
  const params = new URLSearchParams();
  // 仅在 occurrence 非零时添加到查询参数,保持 occurrence=0 的向后兼容
  if (row.sample_occurrence) params.set("occurrence", String(row.sample_occurrence));
  if (evaluation) params.set("eval", "1");
  const query = params.toString();
  return `#/rollout/${rolloutId}/sample/${row.sample_index}${query ? `?${query}` : ""}`;
}
​
async function groupNavPanel(rolloutId, sampleIndex, sampleOccurrence, evaluation) {
  const { rows } = await api(`/api/rollout/${rolloutId}/summary`, { eval: evaluation });
  // 根据完整的 (sample_index, sample_occurrence) 查找当前选中行
  const selected = rows.find(
    (row) => row.sample_index === sampleIndex && row.sample_occurrence === sampleOccurrence,
  );
  const groupIndex = selected?.group_index;
  // 兄弟节点按 (sample_index, sample_occurrence) 排序
  const siblings = groupIndex == null
    ? []
    : rows
      .filter((row) => row.group_index === groupIndex)
      .sort((a, b) => a.sample_index - b.sample_index || a.sample_occurrence - b.sample_occurrence);
  if (siblings.length < 2) return null; // no group, or a lone sample: nothing to navigate
  const position = siblings.indexOf(selected);
  const goto = (row) => (location.hash = sampleHash(rolloutId, row, evaluation));
  // ... 其余导航按钮逻辑使用 goto(siblings[position - 1]) 等
}
miles/dashboard/static/views_rollout.js core-logic

Rollout 概览视图,集成了新的 sample_occurrence 列显示和跳转逻辑。

function sampleHash(rolloutId, row, evaluation) {
  const params = new URLSearchParams();
  if (row.sample_occurrence) params.set("occurrence", String(row.sample_occurrence));
  if (evaluation) params.set("eval", "1");
  const query = params.toString();
  return `#/rollout/${rolloutId}/sample/${row.sample_index}${query ? `?${query}` : ""}`;
}
// ... 在 renderRollout 中
const openTokens = (row) => (location.hash = sampleHash(rolloutId, row, evaluation));
// 在散点图标签中,清晰显示叶子编号
label: `sample ${r.sample_index}${r.sample_occurrence ? ` #${r.sample_occurrence + 1}` : ""}\n` +
  `reward=${fmtNum(r[rewardKey])} len=${r.response_length}` +
  (r.mean_abs_lp_diff !== null ? `\n|lp diff|=${fmtNum(r.mean_abs_lp_diff)}` : ""),

评论区精华

load_joined 函数中 KeyError 崩溃风险 正确性

claude[bot] 指出新代码在循环中直接使用 `index[key[0]]` 查找,当样本在训练数据中完全缺失时会引发 KeyError,而旧代码通过遍历 `index.items()` 天然容忍此情况。

结论:问题已确认并修复。作者在后续提交 (`4acef78c`) 中使用 `index.get(key[0], [])` 并在无匹配时跳过构建 TrainRow,保持了与旧代码相同的降级行为。 · 已解决

trajectory_messages 中 occurrence 编号错配 正确性

claude[bot] 发现轨迹侧车文件只包含有对话记录的样本,其内部 occurrence 编号可能与全样本列表的编号不一致,导致 API 404 或返回错误数据。Zhichenzzz 对此分析表示同意。

结论:问题已确认并修复。作者在 `debug_data.py` 的 `trajectory_rows` 中计算并持久化正确的 `sample_occurrence`;在 `dump_reader.py` 中优先使用侧车文件中的 `sample_occurrence`,并对旧版文件采用回退逻辑。新增测试 `test_trajectory_occurrences_follow_the_full_sample_order` 验证了修复。 · 已解决

Anatomy 面板中 rowsByIndex 映射的 occurrence 丢失 正确性

claude[bot] 在 views_rollout.js 中指出,`rowsByIndex` 使用 `sample_index` 作为键,当遇到重复 sample_index 时只保留最后一个 occurrence,导致点击 lane 时跳转到错误的叶子。

结论:此问题被识别为非阻塞的可选改进,本次合并时未修复。 · 未解决(非阻塞)

风险与影响

主要风险为向后兼容性与引入的逻辑复杂度。

  1. 向后兼容性:新引入的 occurrence 参数默认值为 0,确保了现有 URL 和 API 调用在不传递此参数时仍能工作。SUMMARY_VERSION 的升级会自动使旧的 summary parquet 缓存失效。_matching_train_locations 和侧车文件的回退逻辑确保了能处理旧格式的训练数据和轨迹文件。
  2. 逻辑复杂度:引入 occurrence 后,数据流中多了一个维度,load_joined 和 trajectory_messages 的匹配逻辑变得更复杂,特别是在处理部分训练覆盖和历史数据时。但变更逻辑清晰,测试覆盖充分(249 passed),回归风险相对可控。

此变更修复了 Dashboard 在使用 TITO v2 训练时的核心功能缺陷,直接影响所有查看此类训练转储数据的用户。影响范围集中在 miles/dashboard 模块及其依赖的数据读取与生成部分。修复前,相关 API 可能 500 崩溃,无法查看特定样本。修复后,用户可以正确区分和查看每个叶子样本,提升了工具的可靠性与用户体验。变更本身不改变训练核心逻辑,对训练性能和稳定性无直接影响。

核心数据模型变更 向后兼容性依赖默认值

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论