Skip to content

【No.5】fix(metrics): deduplicate spec decoding metrics - #406

Open
AllureCurtain wants to merge 1 commit into
redai-studio:mainfrom
AllureCurtain:task5/spec-metrics-dedup
Open

AllureCurtain wants to merge 1 commit into
redai-studio:mainfrom
AllureCurtain:task5/spec-metrics-dedup

Conversation

@AllureCurtain

Copy link
Copy Markdown

What

Implement Task 5: deduplicate and counter-weight speculative decoding metrics for Agentic Rollout.

  • Preserve each committed generation's request identity and sparse accepted, proposed, verify, and completion counters through session export and JSON serialization.
  • Deduplicate shared committed generations across exported trajectories while keeping identical text from independent requests and different sessions separate.
  • Aggregate counters before computing acceptance and tokens-per-verify ratios, and expose coverage for complete counters and each ratio pair.
  • Preserve legacy integer fields and old serialized payloads, while distinguishing explicit zero counters from missing or invalid backend fields.
  • Add bilingual documentation and CPU regression coverage for the metric and export pipeline.

Why

The previous Agentic metric path averaged per-sample ratios and could count a shared generation node once for every exported trajectory. It also treated missing backend counters as zero, which could produce fabricated zero-valued ratios or divide by zero for an empty batch. Task 5 requires generation-level deduplication, weighted aggregation, explicit missing-counter handling, and compatibility documentation.

How

Generation records are keyed by the committed Agentic request ID (req_<session_id>_<sequence>), not by text or state hash. Session nodes retain sparse counter records and export them through TrainingFieldArtifact and Sample serialization. The rollout adapter validates records, deduplicates each generation once per metric batch, sums available counters, and computes ratios only from complete numerator/denominator pairs. Resumed requests require every backend attempt to report a field before that field is treated as complete; duplicate exports can fill a missing field without double-counting an existing value.

The compatibility metrics keep their existing names. New metrics report counted nodes, missing counters, full-counter coverage, acceptance-pair coverage, length-pair coverage, and legacy samples that lack generation identities.

Task 5 Acceptance

  • Independent generations with accepted/proposed 1/2 and 9/10 produce 10/12 (approximately 83.33%) instead of the arithmetic mean 0.7.
  • For exported trajectories A -> B and A -> C, A is counted once and B/C are counted; identical text from independent requests and different sessions remain distinct.
  • Empty input, zero denominators, missing fields, explicit zeros, invalid counters, resumed requests, and legacy serialized payloads have defined behavior without fabricated zero ratios or division-by-zero errors.
  • CPU integration tests cover metadata accumulation, session-tree export, serialization, deduplication, weighted aggregation, and compatibility behavior. The bilingual guide contains human-checkable examples and metric compatibility notes.

Testing

  • pre-commit run --all-files --show-diff-on-failure

  • 115 passed, 2 skipped in the focused and related regression suite:

    tests/utils/test_spec_decoding_metrics.py
    tests/test_agentic_rollout.py
    tests/utils/test_metric_utils_rloo.py
    tests/engine/rollout/test_data_source.py
    tests/engine/rollout/test_base_types.py
    

    The two skipped tests require the optional megatron.core dependency, which is unavailable in this Windows environment.

  • Manual report covering weighted aggregation, shared-node deduplication, independent identical text, missing counters, explicit zeros, legacy payloads, and empty batches.

  • git diff --check, Ruff checks, and formatting checks pass.

No GPU or multi-node integration run is claimed because this validation environment has no such hardware; the task's CPU integration requirements are covered by the regression suite above.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactoring
  • Performance improvement
  • CI/CD or build changes

Reference

# 🐛 Bug Fix

## Aggregate committed generations once

- Preserve request-scoped generation identities and sparse speculative counters through Agentic export.
- Deduplicate shared generations while keeping identical text from independent requests and separate Sessions distinct.
- Compute ratios from summed counter pairs and report full-counter and pair-level coverage.
- Preserve explicit zero, missing, invalid, resumed-request, and legacy serialized states.

---

# ✅ Tests

## Cover metric and export regressions

- Add CPU coverage for weighted aggregation, shared-node deduplication, missing fields, zero denominators, JSON round trips, and resumed requests.
- Exercise the metadata-to-SessionForest-to-TrainingFieldArtifact-to-rollout path.

---

# 📝 Documentation

## Document metric semantics and compatibility

- Add bilingual guidance for new metrics, coverage, generation identity, resume accounting, and legacy payloads.
- Add human-checkable weighted and shared-node examples.
@rai-studio-bot

Copy link
Copy Markdown
Contributor

Thanks for contributing to Relax, @AllureCurtain! 感谢你为 Relax 做出贡献!

Contribution guide / 贡献指南

Describe the problem, your changes, and how you validated them. Keep each PR focused and run pre-commit run --all-files before submitting.

请说明问题、改动和验证方式,保持 PR 聚焦,并在提交前运行 pre-commit run --all-files。

English contribution guide · 中文贡献指南

CI commands / CI 指令
Command / 指令 Usage / 用途
/rerun Retry failed CI / 重跑失败的 CI
/rerun <target> Rerun a workflow or check / 重跑指定 workflow 或检查
/cancel <workflow> Cancel an entire workflow / 取消整个 workflow
/help Show commands and targets / 查看指令和 target
/review Request a code review / 请求代码 review

Put one command on the first line of a new PR comment. Rerun/cancel require PR authorship or repository write access.

在新 PR 评论的首行写一条指令。PR 作者或有仓库写权限的贡献者可以重跑、取消 CI。

CI usage and targets · CI 用法与 target

@AllureCurtain

Copy link
Copy Markdown
Author

@rai-studio-bot review

@AllureCurtain

Copy link
Copy Markdown
Author

/review

@rai-studio-bot

rai-studio-bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Nyanpasu 审查看板

审查状态: 💬 已完成 · 有补充意见

审查版本: 71afe8f

目标分支: main

常规与深度审查完成,无阻塞缺陷。深度阶段通过独立设计参考与测试价值审计子任务完成比对:独立参考确认序列化边界携带生成记录为不可约核心,但其更小替代方案(状态哈希身份、仅键存在性)经逐项比对被否决——不满足'同 session 相同文本独立请求分别计数'的验收标准,且会失去 legacy 遥测;本 PR 的请求级身份与三字段可用性追踪为可辩护取舍。测试套件经 11 个定向变异验证全部具备检出力。最终提出 2 条非阻塞 P3 建议(死代码删除、单测预言加强),处理与否不影响合并。

审查阶段进度范围与结果
常规审查 ✅ 已完成 已检查全部 10 个已接受文件的生产调用链(_compute_spec_metrics → compute_spec_decoding_metrics;_accumulate_request_meta → append_resp → build_sample → TrainingFieldArtifact 序列化)、序列化兼容边界与文档声明;本地复跑 PR 声明测试(115 passed / 2 skipped)与 ruff / pre-commit 均通过。未发现阻塞缺陷。
深度审查 ✅ 已完成 独立设计参考(基于 merge-base 快照、需求隔离、CPU 验证部分有效)与测试价值审计(11 个定向变异 + 删除实验)均完成并已父级复核:参考方案的状态哈希去重被 PR 测试证据否决(同 session 相同文本两请求需分别计数);resume 交集语义与三态可用性追踪记为保留决策;测试套件检出力完整,含一个仅在 Docker CI 生效的环境依赖用例(已验证可检出)。生产/测试必要性审计见下方精简审查表。

审查发现

待处理
编号 严重性 问题状态规则来源
F1 Low severity 删除未被调用的 spec_token_counts_reported 辅助函数(新增死代码)
精简建议(非阻塞)
🚧 未解决 —
F2 Low severity 序列化往返单测改用直接字段断言,消除自比较弱预言 🚧 未解决 —
已解决或已取代
编号 严重性 问题状态规则来源
暂无已解决或已取代的记录。
精简审查与验证依据
审查范围进度结论
生产代码 ✅ 已完成 对 5 个生产文件逐机制审查了必要性与更小替代方案:请求级节点身份、三态可用性追踪、resume 交集语义均判定为保留(各有验收标准或文档化契约支撑);唯一可删除项为无调用点的 spec_token_counts_reported 辅助函数(已发布为 F1)。参考设计的替代方案被需求证据否决,未发现其余可安全删除/合并的生产机制。
测试 ✅ 已完成 对两个测试文件(16 个 agentic 集成用例 + 46 个单测)完成检出力与维护成本审计:11 个定向变异全部被捕获(均值回归 13 处失败、去重破坏 5 处、缺失伪造为零 8 处、resume 交集丢失 6 处等);删除实验证明 count_node_shared_by_several_samples_once 为 report_matches_hand_computed_totals 的严格子集(可合并但非必须);一个用例依赖 megatron.core 仅在 Docker CI 生效(已用桩验证其可检出,记录为环境性覆盖而非缺陷)。建议加强一个自比较弱预言用例(已发布为 F2)。

生产代码的必要性与替代方案

范围必须保留的契约更简单的方案结论依据与限制
relax/utils/types.py:35-42 新增的 spec_token_counts_reported 辅助函数 无任何调用方(全仓库检索仅定义处);其表达的'是否上报过计数'判断在 types.py:197、service.py:1649、state.py:826 均为内联实现且对象类型不同,该函数按当前签名不可复用。 直接删除该函数(已附行内删除建议,见 F1)。另注:get_spec_token_counts 在本 PR 后也无调用方,因属公开 API 删除需维护者确认,记为后续跟进。 删除 静态检索证据(零调用点);三处内联实现的输入分别为 known_totals / pending_spec_counters / node record,均非 meta_info。限制:仅静态证据,无运行时行为影响(死代码)。
spec_node_records 按请求 ID(req_<session_id>_<sequence>)键控的去重身份(service.py 提交路径 + state.py append_resp/build_sample) Task 5 验收标准:'相同文本的独立请求分别计数;session 内不误去重、跨 session 不串数据'。调用方:compute_spec_decoding_metrics 按 node key 去重求和。 独立参考设计的方案:以 (session_id, state_hash) 为去重键,不引入请求级身份(参考设计自述为其'已知边界情况')。 保留 参考方案中同一 session、同一父状态、字节相同续写的两个请求会坍缩为一个节点只计一次,直接违反验收标准;本 PR 的 test_agentic_spec_metrics_keep_identical_requests_and_sessions_apart(nodes_total=3)与 test_session_forest_keeps_counters_of_content_identical_requests 证明请求级身份满足该边界。参考设计为 CPU 隔离推导,未运行本 PR 代码。
Sample.SpecInfo 的 counts_reported(bool|None 三态)与 available_counters(list|None)(relax/utils/types.py) Task 5:区分零计数与后端未上报计数、上报覆盖率;PR 文档扩展的 spec_legacy_samples 遥测与 legacy 载荷'仅正 draft/verify 视为已上报'启发式。调用方:compute_spec_decoding_metrics、SpecInfo.add/from_dict。 参考设计 §2.1:单一 spec_counters_supplied 键存在性集合(frozenset),legacy 载荷一律视为全字段已上报,无 legacy 样本计数。 保留 存在性集合无法表达 legacy 载荷的'无法证明已上报'状态(merge-base 的 to_dict 无条件序列化全部四个键,全零 legacy 载荷会被计入覆盖率并失去 spec_legacy_samples);三态语义已在文档与 8 个以上用例中固化。代价:from_dict 的分支矩阵较复杂(父级已逐路径复核正确)。保留判断基于任务'缺失字段产生定义行为、不伪造 0%'的保守解读,任务文本未显式强制 legacy 遥测(已记录为解释性取舍而非硬性需求)。
accumulate_spec_counter_values 的跨尝试交集语义与 pending_spec_counts_reported(service.py/_accumulate_request_meta) PR 文档化的 resume 语义:'某字段只有在每次后端尝试中都被上报,才能作为完整请求计数'。调用方:InflightRequest 累积 → append_resp 提交记录。 保持 merge-base 的跨尝试求和(并集),仅增加键存在性——参考设计声称 merge-base 已累积、无需新增状态。 保留 并集求和会把部分上报的请求总值当作完整值上报(例如尝试 2 未上报 verify 时仍以尝试 1 的 verify 作为总verify);交集语义避免该歧义并有专项测试(test_agentic_spec_metrics_sum_resumed_attempts_and_report_missing_counters、test_agentic_spec_metrics_keep_resumed_request_partial_when_one_attempt_omits_a_field)。限制:任务文本仅要求'缺失字段有定义行为',未显式强制交集语义——该更严格解读已在 docs 中文档化并测试,判定为可辩护而非多余。

测试的必要性与替代方案

范围必须保留的契约更简单的方案结论依据与限制
tests/utils/test_spec_decoding_metrics.py 全部 46 个用例(含与 docs 手算示例对齐的独立预言) Task 5 验收边界的单层守护:加权聚合、去重、零/缺失区分、legacy 兼容、别名规范化、序列化往返。生产路径:compute_spec_decoding_metrics(rollout.py:4862/4940 接线)。 删除实验:移除 count_node_shared_by_several_samples_once(与 report_matches_hand_computed_totals 输入相同、断言为其 3/7 子集),重放去重破坏变异后剩余用例仍可检出;另有两个参数化分支(null_value 与 payload 2/3)产生相同结果可精简。 保留 11 个定向变异的逐项检出记录(M1-M11,含每项失败用例数);预言为文档手算示例而非输出快照。合并选项已验证安全但收益约 10 行,且具名用例的失败信息更聚焦,故保留并在本记录中留档合并方案。
tests/test_agentic_rollout.py 新增 16 个 agentic 集成用例(_SpecSessionHarness 走真实 shard 路径) Task 5 CPU 集成测试要求:贯通后端元数据处理 → 请求提交 → 轨迹导出 → TrainingFieldArtifact 序列化 → 批次聚合。真实路径:_apply_generate_result → _terminal_response_locked → append_resp/build_sample → _export_payload → TrainingFieldArtifact。 以森林层单元用例替代 e2e 家族(更小的桩面)。 保留 e2e 家族是唯一捕获序列化写入侧回归(M6:to_dict 丢失 nodes,6 处失败)的层;森林层用例无法覆盖该边界。harness 约 70 行、仅伪造 tokenizer 与 args,无逻辑 mock,与其守护的关键用户路径相称。
tests/utils/test_spec_decoding_metrics.py:237-251 test_spec_info_serialization_round_trips_nodes_and_report_flag 的自比较预言 本用例名义契约:序列化往返保留 nodes 与 counts_reported(写入侧 + 读取侧)。 直接断言往返后对象的 nodes/counts_reported 字段(替换第 251 行的自比较等式)。 替换 定向变异(to_dict 删除 nodes 键)下本用例仍通过——两侧同时丢失字段使等式恒成立,写入侧回归当前仅由 e2e 家族检出;已发布为 F2 行内建议。限制:套件整体仍检出该变异(e2e 6 处失败),故为加强而非缺口。
Powered by Nyanpasu with glm-5.3[1m] xhigh, please check the suggestions carefully.

@rai-studio-bot rai-studio-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

常规审查结论:未发现阻塞性问题(深度审查进行中)

常规审查已完成(head 71afe8f0):对照 Task 5 验收标准核对了实现路径——计数加权聚合(1/2 与 9/10 汇总为 10/12)、按提交生成节点去重、相同文本独立请求与跨 Session 隔离、零值与缺失计数的区分、覆盖率指标、旧序列化载荷兼容,以及双语文档与代码行为的一致性(含 req_<session_id>_<sequence> 标识格式、rollout/ 与 eval/<dataset>/ 日志前缀)。本地复跑 tests/utils/test_spec_decoding_metrics.py、tests/test_agentic_rollout.py 及 PR 声明的相关回归共 115 passed / 2 skipped(与 PR 描述一致),ruff 与 pre-commit 钩子均通过。

目前仅 1 条非阻塞简化建议(见行内评论:新增的 spec_token_counts_reported 辅助函数无调用点)。独立设计比较与测试价值审计仍在进行,完成后我会更新最终结论。

审查看板:#406 (comment)

Powered by Nyanpasu with glm-5.3[1m] xhigh, please check the suggestions carefully.

Comment thread relax/utils/types.py
Comment on lines +34 to +43

def spec_token_counts_reported(meta_info: dict[str, Any]) -> bool:
"""Whether the backend reported speculative decoding counters for this
request.

``get_spec_token_counts`` returns ``(0, 0)`` both for a missing report and
for a genuine zero, so coverage accounting needs this separate signal.
"""
return any(key != "completion_token_num" for key in get_spec_counter_values(meta_info))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low severity P3 · 删除未被调用的 spec_token_counts_reported 辅助函数(非阻塞简化建议)

全仓库检索显示 spec_token_counts_reported 没有任何调用点(仅 relax/utils/types.py:35 处的定义)。它所表达的"该请求是否上报过投机解码计数"判断在本 PR 的其余位置均为内联实现:

  • relax/utils/types.py:197(SpecInfo.add,基于 known_totals)
  • relax/agentic/session/service.py:1649(_accumulate_request_meta,基于 request.pending_spec_counters)
  • relax/agentic/session/state.py:826(build_sample,基于各 node record)

三处判断的对象都不是 meta_info,因此该辅助函数按当前签名无法被它们复用,属于新增的死代码,建议直接删除以避免后续维护两套"上报判定"语义。

Suggested change
def spec_token_counts_reported(meta_info: dict[str, Any]) -> bool:
"""Whether the backend reported speculative decoding counters for this
request.
``get_spec_token_counts`` returns ``(0, 0)`` both for a missing report and
for a genuine zero, so coverage accounting needs this separate signal.
"""
return any(key != "completion_token_num" for key in get_spec_counter_values(meta_info))

另注(无需在本 PR 处理):本 PR 移除 service.py 中的旧调用后,get_spec_token_counts(relax/utils/types.py:18)也不再有任何调用方;因其属于公开 API,按仓库约定删除前需另行确认,可作为后续跟进项。

@rai-studio-bot rai-studio-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

最终审查结论:无阻塞问题,2 条非阻塞建议

深度审查已完成(head 71afe8f0):独立设计参考与测试价值审计均已收敛。独立参考设计确认"序列化边界必须携带生成记录"是该需求的不可约核心,其提出的更小替代方案(按状态哈希去重、仅保留键存在性)经逐项比对被否决——按状态哈希去重无法满足"session 内不误去重、相同文本独立请求分别计数"的验收标准(本 PR 的 test_agentic_spec_metrics_keep_identical_requests_and_sessions_apart 恰好覆盖该边界);三字段可用性追踪支撑了零值/缺失区分与 spec_legacy_samples 遥测,属可辩护的设计取舍。测试套件经 11 个定向变异验证全部具备检出力(每项变异均被至少一个用例捕获;唯一例外是依赖 megatron.core 的接线用例,仅在 Docker CI 环境运行,已验证其在有 megatron 的环境下可检出)。生产与测试必要性审计已记录在看板。

结论:常规与深度审查均未发现阻塞性缺陷;2 条 P3 非阻塞建议见行内评论(relax/utils/types.py 删除无调用点的 spec_token_counts_reported;tests/utils/test_spec_decoding_metrics.py 序列化往返用例改用直接字段断言)。两者处理与否不影响合并判断。

审查看板:#406 (comment)

Powered by Nyanpasu with glm-5.3[1m] xhigh, please check the suggestions carefully.


assert info.nodes == {"req_sess-a_0": _counters(1, 2), "req_sess-a_1": {}}
assert info.counts_reported is True
assert Sample.SpecInfo.from_dict(info.to_dict()).to_dict() == info.to_dict()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low severity P3 · 直接断言往返后的字段,避免自比较弱预言(非阻塞)

第 251 行的断言是自比较预言:from_dict(to_dict()).to_dict() == to_dict()。若 to_dict 写入侧丢失字段(例如漏写 nodes),两侧同时丢失,等式依然成立,该用例无法发现。定向变异验证(在 to_dict 中删除 nodes 键):本用例仍通过,仅由 tests/test_agentic_rollout.py 的端到端序列化路径(TrainingFieldArtifact)以 6 处失败检出。建议直接断言往返后对象的字段,使写入侧回归在本用例内即可发现:

Suggested change
assert Sample.SpecInfo.from_dict(info.to_dict()).to_dict() == info.to_dict()
round_tripped = Sample.SpecInfo.from_dict(info.to_dict())
assert round_tripped.nodes == {"req_sess-a_0": _counters(1, 2), "req_sess-a_1": {}}
assert round_tripped.counts_reported is True

现有前两行断言(249-250 行)保留即可;它们只覆盖读取侧,此修改补上写入侧。

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants