feat: monitor repeated model output - #1346
Conversation
b952977 to
7e92fbc
Compare
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1346
Status: BLOCKING
Summary: P0/0 · P1/2 · P2/12 · P3/5
Reviewed: commit 7e92fbcb3131 · 2026-08-28 17:58 UTC+8
Blocking Issues
P1
- grpc_metrics 无条件读取新属性使既有 access_log_test 打桩失效,CI 目标确定性失败 @
rtp_llm/dash_sc/grpc_metrics.py:162- 建议:在本 PR 内同步补齐该桩字段(
output_repetition_check_ms=0.0、output_repetition_result=None);更稳妥的做法是让桩直接使用真实RequestRepetitionMonitor实例(output 关闭态)按需赋值,避免后续再加字段时重复踩坑。不要在grpc_metrics侧改用getattr兜底——那会把生产边界的结构契约弱化为运行时探测。
- 建议:在本 PR 内同步补齐该桩字段(
- 删除唯一加载真实 .so 的用例,同时把 native 必需 API 从 1 项扩到 3 项 @
rtp_llm/dash_sc/test/repetition_monitor_test.py:62- 建议:恢复一个加载真实
.so的用例(可沿用_fresh_native_status()入口),至少断言:真实模块可导入、status.module_name命中预期模块名、三个必需 API 均存在,并用真实OnlineRepetitionTracker跑一段明显重复的 token 序列断言result.hit为真。.so目前仍经//rtp_llm/dash_sc:repetition_monitor的data(dash_sc/BUILD:22)进入 runfiles,请确认 import path 可解析,否则把data加回repetition_monitor_test。另建议把test_module_without_required_api_is_unavailable(:87-100)的断言按_NATIVE_TRACKER_REQUIRED_APIS参数化逐项校验并锚定"missing API"前缀(当前只断check_tool_call_loop,移除另两项仍会通过),使「必需 API 列表」本身成为被测契约。若该边界只能在 smoke 验证,请在 PR 描述中说明替代位置。
- 建议:恢复一个加载真实
Non-blocking Suggestions
P2
- kind 分类阶梯在指标层与访问日志层各写一份,含相同魔数 1/64 @
rtp_llm/dash_sc/grpc_metrics.py:170- 建议:在
repetition_monitor.py抽出唯一实现(如模块级output_repetition_kind(result: OutputRepetitionResult) -> str,或作为OutputRepetitionResult的只读属性),把 1 与 64 提为具名常量并注释取值依据;record_fields()与_report_frontend_structured_metrics均改为调用它,删除grpc_metrics.py:170-178的本地副本。这样日志与指标天然一致,repetition_monitor_test.py现有的 kind 断言即可同时守住指标标签。
- 建议:在
- native 检测器降级在指标面完全不可见,check_ms > 0 哨兵在降级路径同样成立 @
rtp_llm/dash_sc/grpc_metrics.py:162- 建议:改用显式状态而非耗时累加值作判据:仅当
output_repetition_impl == "online_cpp_pybind"(或新增显式output_checked: bool)时才上报OUTPUT_REPETITION_CHECK_RT_METRIC;并补一个可告警信号,例如新增py_rtp_output_repetition_unavailable_qps,或给现有指标加低基数impl标签(online_cpp_pybind/online_cpp_pybind_unavailable/disabled),据此配置「检测不可用比例 > 阈值」告警,作为特性静默失效的兜底。顺带统一两族哨兵约定:tool 侧是Optional[float]用is not None,output 侧是float = 0.0用> 0,同一对象上两种风格易在后续维护中误改。
- 建议:改用显式状态而非耗时累加值作判据:仅当
- 6 个新指标与 4 路 kind 分类零测试覆盖 @
rtp_llm/dash_sc/grpc_metrics.py:179- 建议:按既有
patch.object(grpc_metrics, "kmonitor")范式补一组test_done_output_repetition_family:以数据驱动方式覆盖repeat_unit_size取 1 / 64 / 65 与non_contiguous=True四条分支,断言 6 个指标各上报一次、kind/action标签取值正确、SAME_TOKEN_RUN_QPS_METRIC仅在kind == "same_token_run"时出现一次;再补hit=False、result is None、native 不可用三种情形不产生命中类指标,并与record_fields()["output_repetition_kind"]交叉断言一致。
- 建议:按既有
- 7 个新阈值参数零校验,下界被两层静默 clamp、上界无限制且启动日志与生效值不一致 @
rtp_llm/server/server_args/repetition_detection_group_args.py:28- 建议:在 args 层改用带范围校验的
type=(参照同目录util.py中str2bool抛argparse.ArgumentTypeError的写法)——CLI 与 env 两条路径都能 fail-fast,因为server_args.py:364-365对ArgumentTypeError会走self.error();并为max_period设明确上界。clamp 真源建议只保留 C++normalizeConfig()作最终防线,删除repetition_monitor.py中的重复max(...);若必须保留,请在 clamp 生效时打 WARNING 说明「请求值 X 已按下限 Y 生效」,或在装载后 normalize 并写回配置对象,使to_string()打印的就是生效值。
- 建议:在 args 层改用带范围校验的
- 7 个新参数全部缺少 help,且同一旋钮命名跨三层不统一 @
rtp_llm/server/server_args/repetition_detection_group_args.py:7- 建议:为 7 个参数补
help=,风格与同文件tool_call_loop_*一致,至少写明:作用、单位、下游实际生效下界、依赖 native.so、仅 dash-sc gRPC 路径生效、以及与--output_repetition_monitor总开关的关系。命名建议在合入前统一为--output_repetition_noncontig_*(或全拼)与对应的RTP_LLM_OUTPUT_REPETITION_NONCONTIG_*,使同一检测器配置项共享前缀并与下游字段名对应;env/flag 一旦随版本发布即成为难回收的外部契约。若认为当前命名更简洁,请在 PR 描述中记录该取舍。
- 建议:为 7 个参数补
- 7 个新参数的绑定、默认值与跨模块改名映射均无测试,bind_to 拼错会静默失效 @
rtp_llm/server/server_args/repetition_detection_group_args.py:14- 建议:扩展
test_repetition_detection_config:对 7 个新字段各断言一次默认值与一次 CLI(或 env)覆盖后的生效值,尤其覆盖发生改名的output_repetition_min_dup_tokens与 3 个noncontig_repeat_*,并包含一条通过RTP_LLM_OUTPUT_REPETITION_MONITOR=0关闭总开关的str2bool路径;可参考同文件ServerArgsGrammarConfigTest的逐字段写法。再为_build_repetition_monitor_config补一条RepetitionDetectionConfig → OutputRepetitionConfig的映射测试,把跨模块字段改名固化为测试契约。
- 建议:扩展
- 访问日志 repetition_detected / repetition_alert 语义扩宽且默认生效,下游告警口径变化 @
rtp_llm/dash_sc/repetition_monitor.py:425- 建议:在 PR 描述中明确列出这 5 个既有字段的语义扩宽,并通知访问日志下游消费方;
repetition_reason已带output_repetition:前缀可用于区分来源,建议同时说明repetition_primary_source的优先级(output 优先于 tool)。若下游告警不便同步调整,建议首个版本默认关闭该开关按集群灰度,或保持repetition_alert仅表达 tool-call loop、用新增的output_repetition*字段承载新语义,避免复用既有告警字段。
- 建议:在 PR 描述中明确列出这 5 个既有字段的语义扩宽,并通知访问日志下游消费方;
- native 缺失时访问日志字段集整体退化,默认开启放大了该退化面 @
rtp_llm/dash_sc/repetition_monitor.py:412- 建议:确认所有部署形态都打包了 native
.so(dash_sc/BUILD:22已列为data,非 bazel 打包路径需另行确认),并在 PR 描述中说明该默认值翻转对访问日志 schema 的影响。若无法保证,建议调整降级策略:native 缺失时保留原有字段键(值为None)并仅追加repetition_monitor_available=False与不可用原因,使日志 schema 不因开关默认值而收缩。另请注释说明唯一权威默认来源:目前OutputRepetitionConfig.enabled默认 True(:114)而RequestRepetitionMonitorConfig.output_config工厂默认enabled=False(:158),同一开关存在两种默认语义。
- 建议:确认所有部署形态都打包了 native
- 测试替身偏离真实 pybind 契约,配置字段改名回归会被掩盖 @
rtp_llm/dash_sc/test/repetition_monitor_test.py:41- 建议:让替身贴合真实契约:
FakeConfig用__slots__或 dataclass 限定为这 6 个字段(写未知字段即抛错)、update_many/finalize返回bool、补token_count属性;或改为对真实OnlineRepetitionConfig遍历_ensure_output_tracker写入的字段名逐个断言存在,把跨语言字段名漂移变成可失败的断言而非静默降级。同时把两个test_streaming_*用例改名为反映真实语义(字段投影),检测能力交由真实.so用例覆盖。
- 建议:让替身贴合真实契约:
- 生产读取未归一化 raw result,其偏差字段与 reset/token_count 零覆盖 @
rtp_llm/cpp/repetition/test/OnlineRepetitionTrackerTest.cc:34- 建议:新增直接驱动
OnlineRepetitionTracker的用例:updateMany分多段喂入(模拟 streaming delta)→considerFinalTail()→ 逐字段断言result()的原始值,并显式覆盖「最优候选来自流式、end_index < token_count」这一分支,把生产真正消费的 raw 语义固化下来,避免后续调整normalizeResultForEnd时误以为生产字段已被保护;再补一条reset()后tokenCount()归零、旧命中不残留的用例。若 pybind 直返未归一化结果是有意设计,请在result的 pybind 处或头文件加一行说明 raw 与 normalized 的语义差异;若detectOnlineRepetitionMax/HitOnly确无生产用途,建议移入测试辅助文件,避免头文件长期暴露无消费者的公共 API。
- 建议:新增直接驱动
- 新增 C++ 用例自造 abort 断言宏与手写 main,偏离同目录与全仓 gtest 约定 @
rtp_llm/cpp/repetition/test/OnlineRepetitionTrackerTest.cc:8- 建议:改用 gtest:把 8 个
void testXxx()拆为TEST(OnlineRepetitionTrackerTest, Xxx),RTP_EXPECT换成EXPECT_EQ/EXPECT_TRUE,删除自造宏与手写main(),并在online_repetition_tracker_test的 deps 中加入@com_google_googletest//:gtest与gtest_main,与同文件 sibling 保持一致。
- 建议:改用 gtest:把 8 个
- C++ 用例缺失空/单元素与 max_period、配置 clamp 边界覆盖 @
rtp_llm/cpp/repetition/test/OnlineRepetitionTrackerTest.cc:19- 建议:补齐:空序列与单 token 输入断言不命中且不越界;
max_period边界上下各一例(周期大于max_period时应不命中);针对normalizeConfig传入非法值(min_repeats=0时仍需 3 次重复才命中、max_period=0、non_contiguous_max_span < min_span)断言 clamp 后的实际检测行为;并至少一例断言first_detect_index与tokenCount()。
- 建议:补齐:空序列与单 token 输入断言不命中且不越界;
P3
- 默认对全量流量开启的检测器命中后无早退、无预算上限,且缺开销数据 @
rtp_llm/dash_sc/repetition_monitor.py:301- 建议:补充长输出(如 8k tokens、含高重复退化样本)× 高并发下
py_rtp_output_repetition_check_rt的 P99 与前端进程 RSS 对比数据;若暂无数据,建议首个版本默认 False 或按小比例集群通过 env 打开。实现上可加成本上界:命中后停止继续update、或对单请求设 token/耗时预算超限即停并在日志标记 truncated;hash_power_可提为进程级共享表。若确定默认开启,请在help中标注这是全量生效开关及关闭方式。
- 建议:补充长输出(如 8k tokens、含高重复退化样本)× 高并发下
- py_rtp_same_token_run_qps 可由 kind 标签完全派生,属冗余指标名 @
rtp_llm/dash_sc/grpc_metrics.py:182- 建议:优先删除该条上报及
kmonitor_metric_reporter.py:78的枚举定义,看板按kind下钻py_rtp_output_repetition_qps;若确有历史告警链路依赖该名,请在枚举处注释说明它是kind维度的冗余投影及退役计划。
- 建议:优先删除该条上报及
- 新增指标枚举缺少口径注释,check_rt 累加语义与同族指标不一致 @
rtp_llm/metrics/kmonitor_metric_reporter.py:152- 建议:为 6 个新枚举补注释,明确单位、上报频次、
output_repetition_check_rt是跨流式 delta 的累计值、以及period在 non-contiguous 下需靠kind区分语义;若需与 tool-call 同图对比,可对累计量另行命名(如_check_total_rt)。
- 建议:为 6 个新枚举补注释,明确单位、上报频次、
- 多处新增行超出仓库 black 88 列,且与相邻代码折行风格不一致 @
rtp_llm/dash_sc/grpc_metrics.py:183- 建议:提交前对本次改动的 Python 文件跑仓库既有 black +
isort --profile=black,把kmonitor_metric_reporter.py:155按 :148-150 括号换行对齐、grpc_metrics.py:183-184展开为与 :185-189 一致的多行调用;C++ 侧同时跑 clang-format(注意宏续行的行尾空格)。
- 建议:提交前对本次改动的 Python 文件跑仓库既有 black +
- testLongSameToken 名不符实,且随机用例含实现定义转换 @
rtp_llm/cpp/repetition/test/OnlineRepetitionTrackerTest.cc:104- 建议:长序列场景改用
detectOnlineRepetitionMax(或直接用 tracker 类喂完 1000 个 token)真正覆盖长输入,或把用例改名为testSameTokenRunHitsEarly;随机用例改用rng() % 1000000U + 1000000或std::uniform_int_distribution<int>。
- 建议:长序列场景改用
Checklist Findings (16 fail / 48 total)
General Principles Checklist
- [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
native 缺失时访问日志字段集整体退化,默认开启放大了该退化面
monitor_available()仅在output_config.enabled为真时才探测 native 模块(:392-395),tool 分支还额外要求配齐 markers(:398)。本 PR 之前不存在 output 开关,output 检测恒关闭,未配 markers 时monitor_available()直接返回 True、record_fields()返回完整字段集。默认置 True 后,一旦.so不可用,该函数对所有请求返回 False,record_fields()退化为 :413-419 的 5 字段集,原有repetition_detected/repetition_alert/function_tool_repeated/tool_call_loop_*等字段整体从访问日志消失,影响下游日志消费方与报表,且该退化在 kmonitor 上不可见。 - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
kind 分类阶梯在指标层与访问日志层各写一份,含相同魔数 1/64
grpc_metrics.py:170-178用三元链把OutputRepetitionResult推导为指标标签kind(non_contiguous→repeat_unit_size == 1→<= 64→ else),与repetition_monitor.py:438-447推导访问日志字段output_repetition_kind的判定顺序、四个字符串字面量、阈值 1 与 64 逐字重复,两份独立维护、互不引用,且 64 无任何注释说明来源。而access_record.py:551-558的 docstring 明确声明 monitor 是检测结论、日志字段与 kmonitor 上报的 single source of truth,即分类本应由 monitor 归口、metrics 层只做投影。任一侧调整阈值或新增桶,同一请求在指标kind与日志output_repetition_kind上就会给出不同结论,而排障恰恰依赖两者交叉比对,且无任何测试会发现该偏差。 - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
新增指标枚举缺少口径注释,check_rt 累加语义与同族指标不一致
该文件同期新增的其他指标都有注释说明口径(如DASH_SC_DSV4_PHASE2_QPS_METRIC注明「一次/请求」,VIT_EMBEDDING_BATCH_SIZE_METRIC说明取值含义),本次新增 6 项无任何注释。口径差异确实存在:output_repetition_check_ms由update_output_delta(repetition_monitor.py:313)每个流式 delta 与finalize_output(:346)累加而成,是 per-request 累计耗时;同族tool_call_loop_check_ms(:377)则是单次一次性测量。两者均以_check_rt结尾、标签集合相同,同图对比会被误当作同口径。py_rtp_output_repetition_period承载repeat_unit_size,non-contiguous 命中下语义是匹配跨度长度而非周期。 - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
默认对全量流量开启的检测器命中后无早退、无预算上限,且缺开销数据
output_repetition_monitor在py_config_modules.py:406与 argparse(:12)默认均为 True 且无附加门槛——与同组tool_call_loop_monitor不同,后者须配齐 marker 才真正运行(app.py:295-307),本参数是首个真正默认全量生效的按请求检测器。access_record.py:518-520对每帧调用update_output_delta,后者每帧重建[int(t) for t in delta_ids]并跨 pybind 调update_many;updateMany即使result_.hit已为真也继续处理后续全部 token(.cc:215-221),无早退与耗时/长度预算。每请求持有tokens_/prefix_hash_/hash_power_(各 O(n))与每 token 约 4 条span_occurrences_记录、请求内零回收。PR 未给出长输出高并发下的 RT 与 RSS 数据。 - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
默认对全量流量开启的检测器命中后无早退、无预算上限,且缺开销数据
output_repetition_monitor在py_config_modules.py:406与 argparse(:12)默认均为 True 且无附加门槛——与同组tool_call_loop_monitor不同,后者须配齐 marker 才真正运行(app.py:295-307),本参数是首个真正默认全量生效的按请求检测器。access_record.py:518-520对每帧调用update_output_delta,后者每帧重建[int(t) for t in delta_ids]并跨 pybind 调update_many;updateMany即使result_.hit已为真也继续处理后续全部 token(.cc:215-221),无早退与耗时/长度预算。每请求持有tokens_/prefix_hash_/hash_power_(各 O(n))与每 token 约 4 条span_occurrences_记录、请求内零回收。PR 未给出长输出高并发下的 RT 与 RSS 数据。 - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
native 缺失时访问日志字段集整体退化,默认开启放大了该退化面
monitor_available()仅在output_config.enabled为真时才探测 native 模块(:392-395),tool 分支还额外要求配齐 markers(:398)。本 PR 之前不存在 output 开关,output 检测恒关闭,未配 markers 时monitor_available()直接返回 True、record_fields()返回完整字段集。默认置 True 后,一旦.so不可用,该函数对所有请求返回 False,record_fields()退化为 :413-419 的 5 字段集,原有repetition_detected/repetition_alert/function_tool_repeated/tool_call_loop_*等字段整体从访问日志消失,影响下游日志消费方与报表,且该退化在 kmonitor 上不可见。 - [6.1] Quality — PR description 说明动机与设计 → issue
默认对全量流量开启的检测器命中后无早退、无预算上限,且缺开销数据
output_repetition_monitor在py_config_modules.py:406与 argparse(:12)默认均为 True 且无附加门槛——与同组tool_call_loop_monitor不同,后者须配齐 marker 才真正运行(app.py:295-307),本参数是首个真正默认全量生效的按请求检测器。access_record.py:518-520对每帧调用update_output_delta,后者每帧重建[int(t) for t in delta_ids]并跨 pybind 调update_many;updateMany即使result_.hit已为真也继续处理后续全部 token(.cc:215-221),无早退与耗时/长度预算。每请求持有tokens_/prefix_hash_/hash_power_(各 O(n))与每 token 约 4 条span_occurrences_记录、请求内零回收。PR 未给出长输出高并发下的 RT 与 RSS 数据。 - [6.1] Quality — 逻辑变更未混入无关格式化 → issue
多处新增行超出仓库 black 88 列,且与相邻代码折行风格不一致
.pre-commit-config.yaml:12-16启用 black 24.8.0 且未覆盖 line-length(默认 88),exclude仅^rtp_llm/ops|^3rdparty,本 PR 改动文件均在管辖范围。grpc_metrics.py:183(约 107 列)与 :184(约 109 列)写成单行,而同一新增块内 :185-189 与紧邻的 :163-167 同类调用都已逐参数展开,风格自相矛盾;kmonitor_metric_reporter.py:155为 91 列,而同文件 :148-150 长度相当却已括号换行;repetition_monitor.py:289-296、:302、:386、:454、:456、:485-492 亦多行超 88。.flake8的 120 列上限拦不住,问题会推迟到 pre-commit 暴露并产生无关的纯格式 diff。 - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
py_rtp_same_token_run_qps 可由 kind 标签完全派生,属冗余指标名
grpc_metrics.py:179-182先以已含kind的output_tags上报OUTPUT_REPETITION_QPS_METRIC,紧接着在kind == "same_token_run"时用完全同一份output_tags再上报SAME_TOKEN_RUN_QPS_METRIC。两次上报的数值与标签集合完全相同,因此py_rtp_same_token_run_qps恒等于py_rtp_output_repetition_qps{kind="same_token_run"},未携带任何新增维度,却多占一个全局注册的指标名,并留下「两者口径是否不同」的歧义。 - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
py_rtp_same_token_run_qps 可由 kind 标签完全派生,属冗余指标名
grpc_metrics.py:179-182先以已含kind的output_tags上报OUTPUT_REPETITION_QPS_METRIC,紧接着在kind == "same_token_run"时用完全同一份output_tags再上报SAME_TOKEN_RUN_QPS_METRIC。两次上报的数值与标签集合完全相同,因此py_rtp_same_token_run_qps恒等于py_rtp_output_repetition_qps{kind="same_token_run"},未携带任何新增维度,却多占一个全局注册的指标名,并留下「两者口径是否不同」的歧义。 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
C++ 用例缺失空/单元素与 max_period、配置 clamp 边界覆盖
main()注册的 8 个用例均为「有内容的正常序列」。未覆盖:空 token 序列(considerFinalTail的token_count_ <= 0早返回,.cc:199)、单 token、token_count == max_period边界与update()中period > max_period的历史位置淘汰分支(.cc:168-181)。normalizeConfig(.cc:10-18)的 5 条 clamp 全部无覆盖,而这些字段来自 server args、非法值会被静默改写,同一套 clamp 又在repetition_monitor.py:288-296逐条重复,两份都无测试锚定、漂移不会被发现。first_detect_index(经 pybind 落到访问日志)与tokenCount()在任何用例中均未断言;同目录 sibling 已有EmptyOutputIdsDoesNotHit,本文件缺同类保护。 - [6.1] Tests — 被删除测试有等价替代覆盖 → issue
删除唯一加载真实 .so 的用例,同时把 native 必需 API 从 1 项扩到 3 项
本 PR 删除test_packaged_native_module_imports_from_runfiles(原走真实 resolver,断言status.available并实调check_tool_call_loop),并在dash_sc/test/BUILD:67摘掉data = ["//rtp_llm/cpp/repetition:online_repetition_tracker"];同期_NATIVE_TRACKER_REQUIRED_APIS(repetition_monitor.py:26-30)由单项check_tool_call_loop扩为含OnlineRepetitionConfig/OnlineRepetitionTracker的 3 项。现存_fresh_native_status()的两处使用(:71、:88)都同时 patch 了import_module,其「让真实 resolver 运行」的语义已无人使用,全仓无任何测试导入真实.so。.so未打包或未导出新 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
testLongSameToken 名不符实,且随机用例含实现定义转换
testLongSameToken构造 1000 个相同 token,但走detectOnlineRepetitionHitOnly,该函数首次命中即 return(.cc:228-230)。按实现复算:min_duplicate_tokens=32时 period 1 的covered = token_index + 1、duplicate = covered - 1,在token_index == 32即满足阈值,实际只消费约 33 个 token,「Long」从未被执行,长序列下positions_by_token_增长与累积开销均未覆盖。另testRandomNoHit的static_cast<int>(rng() + 1000000U)(:122)在结果超过 INT_MAX 时为实现定义转换,跨平台可能得到不同 token 值。
RTP-LLM Checklist
- [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue
删除唯一加载真实 .so 的用例,同时把 native 必需 API 从 1 项扩到 3 项
本 PR 删除test_packaged_native_module_imports_from_runfiles(原走真实 resolver,断言status.available并实调check_tool_call_loop),并在dash_sc/test/BUILD:67摘掉data = ["//rtp_llm/cpp/repetition:online_repetition_tracker"];同期_NATIVE_TRACKER_REQUIRED_APIS(repetition_monitor.py:26-30)由单项check_tool_call_loop扩为含OnlineRepetitionConfig/OnlineRepetitionTracker的 3 项。现存_fresh_native_status()的两处使用(:71、:88)都同时 patch 了import_module,其「让真实 resolver 运行」的语义已无人使用,全仓无任何测试导入真实.so。.so未打包或未导出新 - [I] 代码质量 — 同一功能用统一工具函数 → issue
新增 C++ 用例自造 abort 断言宏与手写 main,偏离同目录与全仓 gtest 约定
新用例用宏RTP_EXPECT+std::abort()(:8-14)加手写main()(:159-170),cpp/repetition/test/BUILD:5-10因此不依赖 gtest。同目录 siblingtoken_tool_call_loop_guard_test(同文件 :12-21)依赖@com_google_googletest//:gtest与gtest_main,用例均为TEST(...)形式,全仓约定亦是 Google Test +cc_test_wrapper。后果:首个断言失败即 abort,其余 7 个用例不再执行,一次回归会掩盖其他失败;无 gtest XML 与逐用例名,测试详情面板无结果、--test_filter不可用;std::abort以 SIGABRT 退出并产生 coredump,会被 CI 崩溃诊断链路误判为引擎崩溃。
Python Static-First Checklist
- [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
测试替身偏离真实 pybind 契约,配置字段改名回归会被掩盖
_fake_output_native的FakeConfig是空的普通类(:42-43class FakeConfig: pass),任意属性赋值都成立;真实 pybindOnlineRepetitionConfig(OnlineRepetitionPybind.cc:12-19)只有 6 个def_readwrite且未开py::dynamic_attr(),写未定义字段会抛AttributeError。_ensure_output_tracker()(repetition_monitor.py:287-296)连续设置这 6 个字段,一旦 C++ 侧字段改名,替身测试仍通过,而生产被宽except Exception吞掉、只留online_cpp_pybind_unavailable。此外FakeTracker.update_many/finalize返回 result 对象(真实 API 返回bool)、也未提供token_count;FakeTracker对任意输入返回同一份写死
Strengths
- 跨语言配置契约干净:7 个字段在 argparse、
RepetitionDetectionConfig、OutputRepetitionConfig、pybindOnlineRepetitionConfig、C++OnlineRepetitionTracker.h:10-17五处名称与默认值完全一致。考虑到ConfigBinding.apply(server_args.py:122-126)走无校验setattr、拼错只会静默失效,且链路含 7 字段 × 2 跳改名,作者在这条最易出错的路径上没有留下任何错位。 - 指标注册与标签设计正确:新增枚举经
MetricReporter.init()遍历 enum 自动注册,避免「上报了但未注册、被report()静默丢弃」;kind仅 4 个取值、action为常量、刻意不含 request_id,标签基数有界。 - 热路径 tag 处理安全:命中分支统一用
{**tags, ...}派生新字典,未原地修改_metric_tags的lru_cache共享实例,严格遵守该函数 docstring(grpc_metrics.py:36-44)声明的只读约定,per-token 的report_chunk仍零额外分配。 - 分层边界清晰:新增上报只落在
_report_frontend_structured_metrics,report_forwarder_rpc_done保持「无 token 载荷指标」契约;update_output_delta/finalize_output均对raw_mode早返回,forwarder 的无原始载荷契约未被破坏。 - C++ 算法边界处理经核对无越界:
spanHash的prefix_hash_[start+length]、hash_power_[length]与std::equal三段迭代器范围在所有调用点均落在有效区间;非连续 span 用滚动哈希后仍以std::equal精确复核,碰撞只导致漏报不会误报,对监控类特性是正确取舍;normalizeConfig的non_contiguous_min_span >= 8同时兜住了length *= 2的死循环风险。 - BUILD 依赖收窄方向正确:
online_repetition_tracker_core让算法脱离 pybind/torch 即可被单测链接;//rtp_llm/dash_sc:repetition_monitor让纯 CPU monitor 不再拖入整个 gRPC server 依赖闭包。 - 8 个新增 C++ 用例给的是精确数值断言而非只断
hit,我按OnlineRepetitionTracker.cc逐条复算repeat_unit_size/repeat_count/partial_tail/covered/duplicate/start/end均一致,并含固定种子负例防误报。 - pybind 侧
update_many与check_tool_call_loop均用py::gil_scoped_release释放 GIL,跨界调用不阻塞其他协程;降级路径整体 fail-open,record_fields()明确注明「Emitting all-clear detection results would be a lie」,设计意图清晰。 - 变更主体为纯追加:既有
tool_call_loop_*字段、flag 名、env 名与默认值均未改动,且--output_repetition_monitor提供了可用的运维关闭开关。
| kmonitor.report(GaugeMetrics.INPUT_TOKEN_SIZE_METRIC, record.input_len, tags) | ||
| kmonitor.report(GaugeMetrics.OUTPUT_TOKEN_SIZE_METRIC, record.output_len, tags) | ||
| monitor = record.repetition_monitor | ||
| if monitor.output_repetition_check_ms > 0: |
There was a problem hiding this comment.
[P1] grpc_metrics 无条件读取新属性使既有 access_log_test 打桩失效,CI 目标确定性失败
grpc_metrics.py:161-168 取 monitor = record.repetition_monitor 后,在既有 tool 分支(:190)之前无条件读取 monitor.output_repetition_check_ms 与 .output_repetition_result,均为非 Optional 硬属性访问;access_record.py:551-560 的 property 原样返回 _repetition_monitor,无惰性兜底。既有测试 access_log_test.py:934 把它替换为只含 tool_call_loop_check_ms / tool_call_loop_result 的 SimpleNamespace,:940 随即调用 report_frontend_rpc_done 且调用处无 try/except,SimpleNamespace 缺属性必抛 AttributeError。该文件不在 diff_paths 中,而 `dash_sc/test/BUILD:5...
建议: 在本 PR 内同步补齐该桩字段(output_repetition_check_ms=0.0、output_repetition_result=None);更稳妥的做法是让桩直接使用真实 RequestRepetitionMonitor 实例(output 关闭态)按需赋值,避免后续再加字段时重复踩坑。不要在 grpc_metrics 侧改用 getattr 兜底——那会把生产边界的结构契约弱化为运行时探测。
| ) | ||
|
|
||
|
|
||
| def _fresh_native_status(): |
There was a problem hiding this comment.
[P1] 删除唯一加载真实 .so 的用例,同时把 native 必需 API 从 1 项扩到 3 项
本 PR 删除 test_packaged_native_module_imports_from_runfiles(原走真实 resolver,断言 status.available 并实调 check_tool_call_loop),并在 dash_sc/test/BUILD:67 摘掉 data = ["//rtp_llm/cpp/repetition:online_repetition_tracker"];同期 _NATIVE_TRACKER_REQUIRED_APIS(repetition_monitor.py:26-30)由单项 check_tool_call_loop 扩为含 OnlineRepetitionConfig/OnlineRepetitionTracker 的 3 项。现存 _fresh_native_status() 的两处使用(:71、:88)都同时 patch 了 import_module,其「让真实 resolver 运行」的语义已无人使用,全仓无任何测试导入真实 .so。.so 未打包或未...
建议: 恢复一个加载真实 .so 的用例(可沿用 _fresh_native_status() 入口),至少断言:真实模块可导入、status.module_name 命中预期模块名、三个必需 API 均存在,并用真实 OnlineRepetitionTracker 跑一段明显重复的 token 序列断言 result.hit 为真。.so 目前仍经 //rtp_llm/dash_sc:repetition_monitor 的 data(dash_sc/BUILD:22)进入 runfiles,请确认 import path 可解析,否则把 data 加回 repetition_monitor_test。另建议把 test_module_without_required_api_is_unavailable(:87-100)的断言按 _NATIVE_TRACKER_REQUIRED_APIS 参数化逐项校验并锚定 "missing API" 前缀(当前只断 check_tool_call_loop,移除另两项仍会通过),使「必需 API 列表」本身成为被测契约。若该边界只能在 smoke 验证,请在 PR 描述中说明替代位置。
Checklist: [6.1] 被删除测试有等价替代覆盖;[I] 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理
| ) | ||
| output = monitor.output_repetition_result | ||
| if output is not None and output.hit: | ||
| kind = ( |
There was a problem hiding this comment.
[P2] kind 分类阶梯在指标层与访问日志层各写一份,含相同魔数 1/64
grpc_metrics.py:170-178 用三元链把 OutputRepetitionResult 推导为指标标签 kind(non_contiguous → repeat_unit_size == 1 → <= 64 → else),与 repetition_monitor.py:438-447 推导访问日志字段 output_repetition_kind 的判定顺序、四个字符串字面量、阈值 1 与 64 逐字重复,两份独立维护、互不引用,且 64 无任何注释说明来源。而 access_record.py:551-558 的 docstring 明确声明 monitor 是检测结论、日志字段与 kmonitor 上报的 single source of truth,即分类本应由 monitor 归口、metrics 层只做投影。任一侧调整阈值或新增桶,同一请求在指标 kind 与日志 output_repetition_kind 上就会给出不同结论,而排障恰恰依赖两者交叉比对,且无任何测试会发现该偏差。
建议: 在 repetition_monitor.py 抽出唯一实现(如模块级 output_repetition_kind(result: OutputRepetitionResult) -> str,或作为 OutputRepetitionResult 的只读属性),把 1 与 64 提为具名常量并注释取值依据;record_fields() 与 _report_frontend_structured_metrics 均改为调用它,删除 grpc_metrics.py:170-178 的本地副本。这样日志与指标天然一致,repetition_monitor_test.py 现有的 kind 断言即可同时守住指标标签。
Checklist: [6.1] 分层边界:新概念在正确层级,不泄漏内部
| kmonitor.report(GaugeMetrics.INPUT_TOKEN_SIZE_METRIC, record.input_len, tags) | ||
| kmonitor.report(GaugeMetrics.OUTPUT_TOKEN_SIZE_METRIC, record.output_len, tags) | ||
| monitor = record.repetition_monitor | ||
| if monitor.output_repetition_check_ms > 0: |
There was a problem hiding this comment.
[P2] native 检测器降级在指标面完全不可见,check_ms > 0 哨兵在降级路径同样成立
native .so 缺失时 _ensure_output_tracker() 返回 None、impl 置 online_cpp_pybind_unavailable 且不抛异常(repetition_monitor.py:284-286),但 update_output_delta(:312-313)与 finalize_output(:345-346)的 finally 仍无条件累加耗时,异常路径(:309-311 / :342-344)亦然。因此 grpc_metrics.py:162 的 check_ms > 0 在检测彻底降级为 no-op 时仍成立,py_rtp_output_repetition_check_rt 照常上报纯空跑耗时,而命中类指标恒不增长——看板上「检测健康且零命中」与「检测根本没跑」完全同形。不可用状态只落访问日志 repetition_monitor_available,kmonitor 侧无任何可告警维度,基于该指标的告警会假阴性。
建议: 改用显式状态而非耗时累加值作判据:仅当 output_repetition_impl == "online_cpp_pybind"(或新增显式 output_checked: bool)时才上报 OUTPUT_REPETITION_CHECK_RT_METRIC;并补一个可告警信号,例如新增 py_rtp_output_repetition_unavailable_qps,或给现有指标加低基数 impl 标签(online_cpp_pybind / online_cpp_pybind_unavailable / disabled),据此配置「检测不可用比例 > 阈值」告警,作为特性静默失效的兜底。顺带统一两族哨兵约定:tool 侧是 Optional[float] 用 is not None,output 侧是 float = 0.0 用 > 0,同一对象上两种风格易在后续维护中误改。
| if output.repeat_unit_size <= 64 | ||
| else "long_span_repeat" | ||
| ) | ||
| output_tags = {**tags, "kind": kind, "action": "metric"} |
There was a problem hiding this comment.
[P2] 6 个新指标与 4 路 kind 分类零测试覆盖
全仓搜索 OUTPUT_REPETITION / SAME_TOKEN_RUN 仅命中 kmonitor_metric_reporter.py:77-78,152-155 的枚举定义、grpc_metrics.py 的上报处与参数文件的 env 名,无任何测试引用。repetition_monitor_test.py:123,153 只断言 record 层 output_repetition_kind(仅 same_token_run 与 non_contiguous_span_repeat 两桶),指标层这份副本零用例,exact_ngram_loop 与 long_span_repeat 两桶两侧均无覆盖。仓库已有完全对位的范式(access_log_test.py:930-947 的 test_done_tool_call_loop_family 断言 QPS 计数与 action=metric 标签),本次未按该范式扩展。
建议: 按既有 patch.object(grpc_metrics, "kmonitor") 范式补一组 test_done_output_repetition_family:以数据驱动方式覆盖 repeat_unit_size 取 1 / 64 / 65 与 non_contiguous=True 四条分支,断言 6 个指标各上报一次、kind/action 标签取值正确、SAME_TOKEN_RUN_QPS_METRIC 仅在 kind == "same_token_run" 时出现一次;再补 hit=False、result is None、native 不可用三种情形不产生命中类指标,并与 record_fields()["output_repetition_kind"] 交叉断言一致。
| type=int, | ||
| default=32, | ||
| ) | ||
| group.add_argument( |
There was a problem hiding this comment.
[P2] 7 个新阈值参数零校验,下界被两层静默 clamp、上界无限制且启动日志与生效值不一致
6 个新 int 参数只声明裸 type=int,不拒绝 0/负数,也不校验 noncontig_repeat_min_span_tokens <= max_span_tokens。同一套 clamp 被写了两遍且都无日志:repetition_monitor.py:288-296 的 max(3,·)/max(0,·)/max(1,·)/max(8,·)/max(2,·),与 C++ normalizeConfig()(OnlineRepetitionTracker.cc:10-18)逐条重复。后果三点:--output_repetition_min_repeats 2 实际按 3 生效,而 py_config_modules.py:419-433 的 to_string() 打印的是声明值,排障被启动日志误导;min_repeats 默认 3 恰等于下界,暴露区间下半段不可达;max_period 完全不设上界却直接决定两个 max_period+1 长度 vector(.cc:59-60)与每 token O(max_pe...
建议: 在 args 层改用带范围校验的 type=(参照同目录 util.py 中 str2bool 抛 argparse.ArgumentTypeError 的写法)——CLI 与 env 两条路径都能 fail-fast,因为 server_args.py:364-365 对 ArgumentTypeError 会走 self.error();并为 max_period 设明确上界。clamp 真源建议只保留 C++ normalizeConfig() 作最终防线,删除 repetition_monitor.py 中的重复 max(...);若必须保留,请在 clamp 生效时打 WARNING 说明「请求值 X 已按下限 Y 生效」,或在装载后 normalize 并写回配置对象,使 to_string() 打印的就是生效值。
| def init_repetition_detection_group_args(parser, repetition_detection_config): | ||
| group = parser.add_argument_group("Repetition Detection") | ||
|
|
||
| group.add_argument( |
There was a problem hiding this comment.
[P2] 7 个新参数全部缺少 help,且同一旋钮命名跨三层不统一
同文件既有 5 个 tool_call_loop_* 参数全部带 help=(:63、:71、:79、:87、:96),而新增 7 个(:7-55)全部没有,--help 只显示裸参数名与类型。这些是面向运维的外部配置面,语义无法从名字推断(min_dup_tokens 指重复覆盖 token 数还是去重后 token 数?单位是 token 还是次数?),也无从得知生效前提依赖 native .so、下界会被静默抬升、以及仅 app.py:_build_repetition_monitor_config 消费(在标准 OpenAI/HTTP 前端设置后既不生效也无告警)。命名上同一旋钮跨三层有三套写法:CLI/配置字段用缩写 noncontig_repeat_* 与 min_dup_tokens,运行期 dataclass 与 C++ 结构体用全拼 non_contiguous_* 与 min_duplicate_tokens,env 前缀也分裂为 RTP_LLM_NONCONTIG_REPEAT_* 与 `RTP_LLM_OUTPU...
建议: 为 7 个参数补 help=,风格与同文件 tool_call_loop_* 一致,至少写明:作用、单位、下游实际生效下界、依赖 native .so、仅 dash-sc gRPC 路径生效、以及与 --output_repetition_monitor 总开关的关系。命名建议在合入前统一为 --output_repetition_noncontig_*(或全拼)与对应的 RTP_LLM_OUTPUT_REPETITION_NONCONTIG_*,使同一检测器配置项共享前缀并与下游字段名对应;env/flag 一旦随版本发布即成为难回收的外部契约。若认为当前命名更简洁,请在 PR 描述中记录该取舍。
| self.output_repetition_impl = "online_cpp_pybind" | ||
| return self._output_tracker | ||
|
|
||
| def update_output_delta(self, delta_ids: Sequence[int]) -> None: |
There was a problem hiding this comment.
[P3] 默认对全量流量开启的检测器命中后无早退、无预算上限,且缺开销数据
output_repetition_monitor 在 py_config_modules.py:406 与 argparse(:12)默认均为 True 且无附加门槛——与同组 tool_call_loop_monitor 不同,后者须配齐 marker 才真正运行(app.py:295-307),本参数是首个真正默认全量生效的按请求检测器。access_record.py:518-520 对每帧调用 update_output_delta,后者每帧重建 [int(t) for t in delta_ids] 并跨 pybind 调 update_many;updateMany 即使 result_.hit 已为真也继续处理后续全部 token(.cc:215-221),无早退与耗时/长度预算。每请求持有 tokens_/prefix_hash_/hash_power_(各 O(n))与每 token 约 4 条 span_occurrences_ 记录、请求内零回收。PR 未给出长输出高并发下的 RT 与 RSS 数据。
建议: 补充长输出(如 8k tokens、含高重复退化样本)× 高并发下 py_rtp_output_repetition_check_rt 的 P99 与前端进程 RSS 对比数据;若暂无数据,建议首个版本默认 False 或按小比例集群通过 env 打开。实现上可加成本上界:命中后停止继续 update、或对单请求设 token/耗时预算超限即停并在日志标记 truncated;hash_power_ 可提为进程级共享表。若确定默认开启,请在 help 中标注这是全量生效开关及关闭方式。
Checklist: [6.1] 回滚路径:风险行为存在运维回滚手段;[6.1] 状态不变量:创建/更新/失败/重试/回滚路径有效;[6.1] PR description 说明动机与设计
| output_tags = {**tags, "kind": kind, "action": "metric"} | ||
| kmonitor.report(AccMetrics.OUTPUT_REPETITION_QPS_METRIC, 1, output_tags) | ||
| if kind == "same_token_run": | ||
| kmonitor.report(AccMetrics.SAME_TOKEN_RUN_QPS_METRIC, 1, output_tags) |
There was a problem hiding this comment.
[P3] py_rtp_same_token_run_qps 可由 kind 标签完全派生,属冗余指标名
grpc_metrics.py:179-182 先以已含 kind 的 output_tags 上报 OUTPUT_REPETITION_QPS_METRIC,紧接着在 kind == "same_token_run" 时用完全同一份 output_tags 再上报 SAME_TOKEN_RUN_QPS_METRIC。两次上报的数值与标签集合完全相同,因此 py_rtp_same_token_run_qps 恒等于 py_rtp_output_repetition_qps{kind="same_token_run"},未携带任何新增维度,却多占一个全局注册的指标名,并留下「两者口径是否不同」的歧义。
建议: 优先删除该条上报及 kmonitor_metric_reporter.py:78 的枚举定义,看板按 kind 下钻 py_rtp_output_repetition_qps;若确有历史告警链路依赖该名,请在枚举处注释说明它是 kind 维度的冗余投影及退役计划。
Checklist: [6.1] DRY:重复非平凡逻辑被抽取或显式复用;[6.1] KISS/YAGNI:无投机性抽象
| "py_rtp_tool_call_loop_current_span_tokens" | ||
| ) | ||
| TOOL_CALL_LOOP_CHECK_RT_METRIC = "py_rtp_tool_call_loop_check_rt" | ||
| OUTPUT_REPETITION_CHECK_RT_METRIC = "py_rtp_output_repetition_check_rt" |
There was a problem hiding this comment.
[P3] 新增指标枚举缺少口径注释,check_rt 累加语义与同族指标不一致
该文件同期新增的其他指标都有注释说明口径(如 DASH_SC_DSV4_PHASE2_QPS_METRIC 注明「一次/请求」,VIT_EMBEDDING_BATCH_SIZE_METRIC 说明取值含义),本次新增 6 项无任何注释。口径差异确实存在:output_repetition_check_ms 由 update_output_delta(repetition_monitor.py:313)每个流式 delta 与 finalize_output(:346)累加而成,是 per-request 累计耗时;同族 tool_call_loop_check_ms(:377)则是单次一次性测量。两者均以 _check_rt 结尾、标签集合相同,同图对比会被误当作同口径。py_rtp_output_repetition_period 承载 repeat_unit_size,non-contiguous 命中下语义是匹配跨度长度而非周期。
建议: 为 6 个新枚举补注释,明确单位、上报频次、output_repetition_check_rt 是跨流式 delta 的累计值、以及 period 在 non-contiguous 下需靠 kind 区分语义;若需与 tool-call 同图对比,可对累计量另行命名(如 _check_total_rt)。
Checklist: [6.1] 可观测性:日志/指标/超时可操作、非噪声
| kmonitor.report(AccMetrics.OUTPUT_REPETITION_QPS_METRIC, 1, output_tags) | ||
| if kind == "same_token_run": | ||
| kmonitor.report(AccMetrics.SAME_TOKEN_RUN_QPS_METRIC, 1, output_tags) | ||
| kmonitor.report(GaugeMetrics.OUTPUT_REPETITION_PERIOD_METRIC, output.repeat_unit_size, output_tags) |
There was a problem hiding this comment.
[P3] 多处新增行超出仓库 black 88 列,且与相邻代码折行风格不一致
.pre-commit-config.yaml:12-16 启用 black 24.8.0 且未覆盖 line-length(默认 88),exclude 仅 ^rtp_llm/ops|^3rdparty,本 PR 改动文件均在管辖范围。grpc_metrics.py:183(约 107 列)与 :184(约 109 列)写成单行,而同一新增块内 :185-189 与紧邻的 :163-167 同类调用都已逐参数展开,风格自相矛盾;kmonitor_metric_reporter.py:155 为 91 列,而同文件 :148-150 长度相当却已括号换行;repetition_monitor.py:289-296、:302、:386、:454、:456、:485-492 亦多行超 88。.flake8 的 120 列上限拦不住,问题会推迟到 pre-commit 暴露并产生无关的纯格式 diff。
建议: 提交前对本次改动的 Python 文件跑仓库既有 black + isort --profile=black,把 kmonitor_metric_reporter.py:155 按 :148-150 括号换行对齐、grpc_metrics.py:183-184 展开为与 :185-189 一致的多行调用;C++ 侧同时跑 clang-format(注意宏续行的行尾空格)。
Checklist: [6.1] 逻辑变更未混入无关格式化
| RTP_EXPECT(result.end_index == static_cast<int>(tokens.size())); | ||
| } | ||
|
|
||
| void testLongSameToken() { |
There was a problem hiding this comment.
[P3] testLongSameToken 名不符实,且随机用例含实现定义转换
testLongSameToken 构造 1000 个相同 token,但走 detectOnlineRepetitionHitOnly,该函数首次命中即 return(.cc:228-230)。按实现复算:min_duplicate_tokens=32 时 period 1 的 covered = token_index + 1、duplicate = covered - 1,在 token_index == 32 即满足阈值,实际只消费约 33 个 token,「Long」从未被执行,长序列下 positions_by_token_ 增长与累积开销均未覆盖。另 testRandomNoHit 的 static_cast<int>(rng() + 1000000U)(:122)在结果超过 INT_MAX 时为实现定义转换,跨平台可能得到不同 token 值。
建议: 长序列场景改用 detectOnlineRepetitionMax(或直接用 tracker 类喂完 1000 个 token)真正覆盖长输入,或把用例改名为 testSameTokenRunHitsEarly;随机用例改用 rng() % 1000000U + 1000000 或 std::uniform_int_distribution<int>。
Checklist: [6.1] 边界 case 覆盖(空、单元素、最大值)
Summary
Tests
//rtp_llm/cpp/repetition/test:online_repetition_tracker_test//rtp_llm/dash_sc/test:repetition_monitor_testBoth targets pass with
--config=cuda13.