Prhub

#2218 ci: version TITO metrics by session server

原始 PR 作者 guapisolo 合并时间 2026-08-08 03:09 文件变更 17 提交数 5 评论 5 代码增减 +193 / -42

执行摘要

TITO 指标按 session server 版本分列,统一 W&B 与 CI 门禁 key

此前 rollout/tito_session_mismatch_rate/... 只有无版本 key,无法区分历史序列来自 session server v1 还是 v2,W&B 与 CI history 的 key 形状也不统一。PR body 明确目标:"Emit v1/v2 in the producer metric namespace so W&B and CI history use the same keys",即让 producer 直接产出带版本段的 key,同时为两个版本分别注册历史门禁,保证双版本并存期间的历史指标互不混淆。

值得精读。核心看点是三层 key 治理:producer 在源头产出带版本段的 key、collector 保持直接白名单、gate 默认值按已知噪声放宽约束——这种“key 形状单一事实来源 + 显式白名单 + 有依据的宽松阈值”的组合适合迁移到其它多版本并存的指标场景。另一个值得关注的设计是结构约束测试:test_normal_session_e2e_declares_both_tito_history_gates 用解析器扫描 e2e 目录,从机制上杜绝新增用例漏注册 gate。review 中关于 drive-by 变更拆分的流程讨论也值得团队借鉴。

讨论亮点

Shi-Dong 对 test_r3_router_equivalence.py 的 disabled 标记提出流程建议:"Seems a drive-by? Next time it'd be better to send a separate PR for this kind of changes.",作者回复 "yes. I will sent a standalone pr",最终合并版本中该文件确实已移出本 PR(对应 "tests split" commit)。对 test_minimax_m27.py 的同类建议("Same here. Probably deserve a standalone PR."),该文件仍保留在本 PR 中。Shi-Dong 还质疑 test_session_v1_v2_parity.pyest_time 从 480 降到 190 的依据,作者回答 "From ci-e2e-time-tune skills in miles repo",即数据来自仓库内 skill 基于实际 CI 运行统计的调优结果。

实现拆解

  1. producer 端版本化(miles/ray/rollout/metrics.py_compute_metrics_from_samples):读取样本 metadata["tito_session_mismatch"] 后,先由 args.use_session_server 推导版本段——True 兼容映射为 "v1",显式字符串 "v1" / "v2" 原样透传,随后用 assert 拦截其它取值(fail-fast)。所有 TITO 指标(总体 mismatch 率与 4 种子类型)统一挂到 tito_session_mismatch_rate/{v1|v2} 前缀下,ci_test 的严格类型硬断言也改用版本化前缀。
  2. collector 白名单扩展(miles/utils/tracking_utils/ci_history.py):TARGET_METRIC_KEYS 增加 rollout/tito_session_mismatch_rate/v1/assistant_textv2/assistant_text 两个 key。只有 assistant_text 进历史门禁——严格类型(special_token_countspecial_token_typenon_assistant_text)由 metrics 侧的 ci_test 硬断言保证为 0,无需进 history gate。
  3. 历史门禁注册(tests/ci/metric_history/register.py):GATE_DEFAULTS 为两个版本 key 配置宽松约束(steps: "last"rel_up: 1.0rel_down: 1.0abs_floor_up: 0.1),因为已知 Nemotron 的 trailing-newline drift 会使 assistant_text 在纯文本 turn 上必然 mismatch,阈值过严会产生无效告警。
  4. e2e 用例声明与结构约束:test_session_server_multi_role 目录下 7 个用例各注册两个 register_ci_gate(...)tests/ci/test/test_metric_selection.py 新增 test_normal_session_e2e_declares_both_tito_history_gates,用解析器扫描 e2e 目录,强制每个用例必须同时声明 v1 / v2 两个 gate,从机制上杜绝新增模型漏注册。
  5. CI 调度配套:test_nemotron3.py 从 120B Super(TP2 + EAGLE)换成 30B Nano(TP1、无 EAGLE,因 Nano 无 MTP head 且单卡即可容纳);依据 run 31143566906 的实测耗时重调各文件 est_timetest_r3_router_equivalence.py(review 后拆出)与 test_minimax_m27.py 因 Miles Router 弃用 / MiniMax-M2.7 不活跃而标记 disabled,释放 GPU 容量。
  6. 测试配套:fast 测试锁定 producer 版本化 key 形状并覆盖 log_rollout_data 端到端 fan-out;tests/ci/test/test_ci_history.py 验证 v1 / v2 序列在 attempt merge 后保持分离(_merge_attempt_records);metric selection 测试锁定 GATE_DEFAULTS 的宽松初始约束。
文件 模块 状态 重要度
miles/ray/rollout/metrics.py 指标计算 modified 6.23
tests/fast/ray/rollout/test_metrics.py 指标测试 modified 6.64
miles/utils/tracking_utils/ci_history.py 指标采集 modified 4.9
tests/ci/metric_history/register.py 门禁注册 modified 4.6
tests/ci/test/test_metric_selection.py 门禁校验 modified 5.55
tests/ci/test/test_ci_history.py 历史测试 modified 5.59
tests/e2e/sglang/test_session_server_multi_role/test_nemotron3.py 会话测试 modified 4.71
tests/e2e/sglang/test_session_server_multi_role/test_deepseekv4.py 会话测试 modified 3.66

关键符号

_compute_metrics_from_samples log_rollout_data CiHistoryBackend.log register_ci_gate _merge_attempt_records

关键源码片段

miles/ray/rollout/metrics.py core-logic

核心源码改动:所有 TITO mismatch 指标改为版本化 key 前缀,并新增 `use_session_server` 的 fail-fast 断言,是本次 key 治理的源头。

def _compute_metrics_from_samples(args, samples):
    """汇总一批样本的 rollout 指标;TITO mismatch 按 session server 版本分列。    key 设计:版本段(v1 / v2)在 producer 侧生成,使 W&B 与 CI history
    共用同一 key 形状;collector 侧只需维护直接白名单。
    """
    response_lengths = [sample.effective_response_length for sample in samples]
​
    log_dict = {}
    log_dict |= dict_add_prefix(compute_statistics(response_lengths), "response_len/")
    log_dict |= _compute_zero_std_metrics(args, samples)
    log_dict |= _compute_spec_metrics(args, samples)
    log_dict |= _compute_prefix_cache_metrics(args, samples)
    log_dict |= _compute_reward_cat_metrics(args, samples)
    log_dict["repetition_frac"] = np.mean([int(has_repetition(s.response)) for s in samples]).item()
    log_dict["truncated_ratio"] = np.mean([int(s.status == Sample.Status.TRUNCATED) for s in samples]).item()
​
    oldest_versions = [s.oldest_weight_version for s in samples if s.oldest_weight_version is not None]
    if oldest_versions:
        log_dict |= dict_add_prefix(compute_statistics(oldest_versions), "weight_version/")
        mixed = sum(1 for s in samples if len(set(s.weight_versions)) > 1)
        log_dict["weight_version/mixed_version_ratio"] = mixed / len(samples)
​
    # TITO session mismatch 指标:本 PR 核心改动,从无版本 key 改为版本化 key。
    tito_vals = [s.metadata.get("tito_session_mismatch") for s in samples]
    tito_vals = [v for v in tito_vals if v is not None]
    if tito_vals:
        # `use_session_server=True` 兼容映射为 `v1`;显式字符串 `"v1"` / `"v2"`
        # 原样透传。未版本化配置下携带 TITO 元数据会触发 assert,快速失败。
        session_server_version = "v1" if args.use_session_server is True else args.use_session_server
        assert session_server_version in ("v1", "v2"), "TITO metrics require session server v1 or v2"
        metric_prefix = f"tito_session_mismatch_rate/{session_server_version}"
​
        # 总体 mismatch 率与 4 种子类型全部挂到版本化前缀下。
        log_dict[metric_prefix] = np.mean([len(v) > 0 for v in tito_vals]).item()
        for mtype in ("special_token_count", "special_token_type", "non_assistant_text", "assistant_text"):
            log_dict[f"{metric_prefix}/{mtype}"] = np.mean(
                [any(m.get("type") == mtype for m in v) for v in tito_vals]
            ).item()
​
        if args.ci_test:
            # CI 模式下,三种严格类型必须为 0——出现即代表 TITO 算法或
            # chat template 缺陷;assistant_text 非关键(token 继承自
            # pretokenized prefix),仅记录不拦截。
            for strict_type in ("special_token_count", "special_token_type", "non_assistant_text"):
                rate = log_dict.get(f"{metric_prefix}/{strict_type}", 0)
                assert rate == 0, (
                    f"{metric_prefix}/{strict_type}={rate:.4f} must be 0 — "
                    "this indicates a bug in the TITO algorithm or chat template. "
                    "Please check your tito model and chat template."
                )
​
    return log_dict
miles/utils/tracking_utils/ci_history.py core-logic

collector 白名单 `TARGET_METRIC_KEYS` 扩展两个版本化 TITO key,是 CI history 门禁的捕获边界。

# CI history collector 捕获的指标 key 直接白名单(不做前缀通配)。
# 设计约束:producer(miles/ray/rollout/metrics.py)在源头写入版本段,
# collector 这里保持直接枚举,v1 / v2 两个序列各自成线,互不合并。
TARGET_METRIC_KEYS: tuple[str, ...] = (
    "train/grad_norm",
    "train/ppo_kl",
    "train/train_rollout_logprob_abs_diff",
    "train/train_rollout_kl",
    "rollout/raw_reward",
    # 只有 assistant_text 进入历史门禁:严格类型(special_token_count 等)
    # 在 ci_test 下由 metrics 侧硬断言为 0,无需再进 history gate。
    "rollout/tito_session_mismatch_rate/v1/assistant_text",
    "rollout/tito_session_mismatch_rate/v2/assistant_text",
)
tests/ci/metric_history/register.py test-coverage

`GATE_DEFAULTS` 新增两个宽松门禁默认值,直接决定 TITO assistant_text 序列在 CI history 上的告警灵敏度。

# GATE_DEFAULTS:未显式声明约束时各指标的历史门禁默认值。
# TITO assistant_text 刻意宽松:rel_up / rel_down 均为 1.0(允许序列相对
# 上一步翻倍或归零),abs_floor_up = 0.1 提供绝对值告警下限——已知
# Nemotron 的 trailing-newline drift 会在每个纯文本 turn 上产生 mismatch,
# 阈值过严会导致常态性误报。
"rollout/tito_session_mismatch_rate/v1/assistant_text": {
    "steps": "last",
    "constraint": {"rel_up": 1.0, "abs_floor_up": 0.1, "rel_down": 1.0},
},
"rollout/tito_session_mismatch_rate/v2/assistant_text": {
    "steps": "last",
    "constraint": {"rel_up": 1.0, "abs_floor_up": 0.1, "rel_down": 1.0},
},

评论区精华

test_r3_router_equivalence.py 的 drive-by 改动 style

Shi-Dong 指出该文件的 disabled 标记属于顺带改动:"Seems a drive-by? Next time it'd be better to send a separate PR for this kind of changes."

结论:guapisolo 回复 "yes. I will sent a standalone pr",最终合并版本中该文件已不在改动列表,说明已拆出。 · 已解决

test_minimax_m27.py 的 disabled 与 est_time 调整 style

Shi-Dong 对 MiniMax-M2.7 的禁用作同样建议:"Same here. Probably deserve a standalone PR."

结论:作者未直接回复;该文件的禁用与 est_time 调整仍保留在本 PR 中(未拆分)。 · acknowledged

test_session_v1_v2_parity.py 的 est_time 下调依据 question

Shi-Dong 质疑 est_time 从 480 降到 190 的来历:"Curious why we need this change and how the number 190 comes from."

结论:guapisolo 回答 "From ci-e2e-time-tune skills in miles repo",即数据来自仓库内 skill 基于实际 CI 运行统计的调优。 · 已解决

风险与影响

  • 指标 key 结构变更(兼容性):旧的无版本 key rollout/tito_session_mismatch_rate/assistant_text 将不再产出,依赖旧 key 的 W&B 面板、告警或外部 consumers 会断线;这是有意统一,但需确认下游面板已完成迁移。
  • fail-fast 断言(可用性):_compute_metrics_from_samplesassert session_server_version in ("v1", "v2") 会在任何未配置 use_session_server(如 None)但样本携带 TITO 元数据的路径上直接崩溃;若存在未走参数默认值的调试或本地工作流会受影响。
  • 门禁约束宽松(漏检风险):GATE_DEFAULTSrel_up / rel_down = 1.0abs_floor_up = 0.1 使 assistant_text 序列只有绝对值超过 0.1 才告警,真实退化若长期低于该阈值会被漏掉——这是针对 Nemotron 已知漂移的有意权衡。
  • 布尔 / 字符串双态语义:True 隐式映射 v1,未来引入 v3 时需同步更新此映射、断言、白名单和门禁三处。
  • CI 覆盖调整:Miles Router 与 MiniMax-M2.7 用例被禁用,相关回归覆盖移出常驻 CI;est_time 重调基于单次 run 实测,不同负载下可能再次漂移。
  • 用户与团队:所有启用 session server 的 rollout 训练产生的 TITO 指标 key 变化;CI history gate 新增 2 个序列,每个 session e2e 用例必须显式声明双版本 gate(结构约束测试强制)。
  • 系统:producer 命名空间、TARGET_METRIC_KEYS 白名单、GATE_DEFAULTS 三层 key 治理职责明确,未来新增版本只需同步三处。
  • CI:est_time 重调与禁用弃用用例释放 H200 GPU 容量;Nemotron 覆盖从 120B 缩到 30B,CI 上验证的模型规格变小。
  • 团队流程:本 PR 顺带暴露了“CI 调度与指标功能混提”的流程问题,reviewer 明确要求后续 drive-by 变更拆分独立 PR。
指标 key 结构变更(旧 key 断线) fail-fast 断言依赖参数双态语义 CI 门禁约束刻意宽松 依赖父 PR #2217

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论