Feature/grammar logits processor - #1006
Conversation
AI Code Review - PR #1006Status: LGTM Summary: P0/0 · P1/0 · P2/0 · P3/0 lgtm ready to ci Checklist ✅ (56 items passed)Strengths
|
af49d85 to
5264266
Compare
This comment was marked as outdated.
This comment was marked as outdated.
b52f379 to
fcb4f67
Compare
2e4e56c to
b5363f3
Compare
b715e53 to
4ddc021
Compare
0a8b2ae to
5f9c27f
Compare
Tighten grammar matcher, tokenizer bindings, and mask handling. Align config, protobuf, raw request, and speculative decoding paths. Expand fallback, recursion, tokenizer, API, and kernel coverage. Squashed from 16 commits.
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1006
Status: BLOCKING
Summary: P0/0 · P1/6 · P2/21 · P3/5
Reviewed: commit 7edfc2dfd3c1 · 2026-08-03 18:58 UTC+8
Blocking Issues
P1
- 开启树解码的 MTP 部署会拒绝全部请求 @
rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:846- 建议:二者择一并补测试:(1) 若 tree/recommendation 在 MTP 下语义安全,为其声明合适的
MtpProcessorMode(或新增「按 cap=propose_step 透传、不施加约束」模式)使准入通过;(2) 若确实不支持,把校验前移到引擎初始化期——MtpExecutor构造时发现sp_config.tree_decode_config非空即 fail-fast 退出,而不是让每个请求收到processor_index=N, mode=unsupported这类难定位错误。同时补一条「MTP + 树解码开启」用例断言期望行为(启动拒绝或正常放行)。
- 建议:二者择一并补测试:(1) 若 tree/recommendation 在 MTP 下语义安全,为其声明合适的
- json_format 叠加空/
{}形态 response_format 时 finalize 不完整,请求在 RPC 边界被硬拒 @rtp_llm/config/response_format_builder.py:232- 建议:把
json_format的清理收敛到一处:_project_legacy_json_format改为「response_format解析结果为 None 时也执行 legacy 投影」,或在_resolve_grammar_constraint末尾无条件self.config.json_format = False。补参数化单测覆盖json_format=True × response_format ∈ {"", " ", {}, "{}"},断言最终落到json_schema={"type":"object"}且validate_finalized通过。
- 建议:把
- think 模式改为硬依赖 grammar 后端,beam / 多序列由可用变为硬失败且无回退开关 @
rtp_llm/config/response_format_builder.py:80- 建议:在 PR description / release note 显式声明该不兼容变更与受影响组合(think + beam、think +
num_return_sequences>1、无 grammar 后端的 think 部署);并提供过渡路径:保留一条不依赖 xgrammar 的 think 预算实现作为 fallback,或提供服务级开关在 grammar 关闭时把 think 请求降级为无约束生成而非报错。补「think + beam」「think + 后端缺失」两条用例锁定期望错误码与文案。
- 建议:在 PR description / release note 显式声明该不兼容变更与受影响组合(think + beam、think +
- 启动期强制构建 grammar tokenizer 元数据,不受支持的 tokenizer 直接启动失败且无逃生开关 @
rtp_llm/models/base_model.py:329- 建议:二者取一:(1) 对「tokenizer 家族不受支持 / 无 stop token」这类可预期情况降级为
logging.warning并保持tokenizer_info_json为空,复用 C++ 已有 backend-disabled 语义,只让真正请求结构化输出的请求失败;(2) 保留 fail-fast,但提供显式开关(恢复GRAMMAR_BACKEND=none或新增--grammar_require_tokenizer_info)。无论哪种,请在 PR description 列出受支持的 tokenizer 清单,便于存量模型灰度前自查。
- 建议:二者取一:(1) 对「tokenizer 家族不受支持 / 无 stop token」这类可预期情况降级为
- tiling 流的采样行布局与 logits processor 区间错位,grammar 掩码可写错行 @
rtp_llm/cpp/normal_engine/NormalSamplerInputGatherer.cc:206- 建议:把
setLogitsProcessorInputs的行宽计算与fillSamplerCommonInputs统一抽成一个 helper(如samplerRows(stream, score_batch))两处共用,消除同一布局的两份实现;并在插入结束后断言idx == sampler_inputs.batch_size,让不一致 fail-fast。补一个「tiling 流 + grammar 流同批」的单测,断言 grammar 掩码落在正确行。
- 建议:把
- env 参数回填新增 callable 分支后,自定义 type= 参数被序列化成不可反解的取值 @
rtp_llm/server/server_args/generate_args_from_env_clean.py:73- 建议:把
callable(arg_type)分支限制为「转换结果是str/int/float/bool/list才采用,否则回退为原始 env 字符串」(该字符串本身即合法输入);或在format_argument_value中对 enum 优先取getattr(value, "name", None)。同一 hunk 把argparse.ArgumentTypeError纳入静默回落默认值(:77),建议至少打一条 WARNING 保留失败信息。补最小单测覆盖GRPC_CONFIG_JSON、enum、List[int]、非法值四类,断言输出不含空格与对象 repr 且可被setup_args()再次解析。该 hunk 与语法约束特性无关,建议拆出独立评审。
- 建议:把
Non-blocking Suggestions
P2
- 对外配置 --grammar_backend/GRAMMAR_BACKEND 被直接移除,缺少弃用期与服务级禁用开关 @
rtp_llm/server/server_args/grammar_group_args.py:6- 建议:保留
--grammar_backend一个大版本并标记 deprecated:取值none时跳过XGrammarBackend::create(等价于tokenizer_info_json为空的禁用路径),解析到该参数打印一次下线计划 warning;或新增--disable_grammar_backend承接关闭语义。在server_args_test.py补「传入 deprecated 参数不报错且语义正确」的回归用例,在ConfigInit.cc:1352注释补一句「旧值 none 不再具备关闭语义」,并在 release note 写明迁移方式。
- 建议:保留
- 核心依赖 xgrammar 指向个人 GitHub fork,存在供应链与可复现性风险 @
3rdparty/xgrammar/repositories.bzl:7- 建议:pin 上游正式 tag/commit,把 fork 中 structural-tag
max_tokens改动落成仓库内 patch 并通过patches=[...]引入,与 cutlass / rules_python 保持一致,并把声明合并进deps/git.bzl的git_deps()。若短期必须依赖 fork,至少改为http_archive+sha256或内部镜像;把xgrammar.BUILD:1注释改为「上游版本 + fork commit + 相对上游差异」,显式记录 picojson 来源保证,并在 PR description 写明上游化计划。
- 建议:pin 上游正式 tag/commit,把 fork 中 structural-tag
- proto field 73 双向停用后旧客户端约束被静默丢弃,且转换层缺少 field 73/74 测试锚点 @
rtp_llm/cpp/model_rpc/QueryConverter.cc:100- 建议:在解析
GenerateConfigPB时对has_response_format()为真的请求打一条限频 warning 或上报计数指标,让版本错配可观测;补两条QueryConverterTest用例:仅设置 field 73 时断言既定语义(拒绝则断言 INVALID_PARAMS,忽略则显式断言 typed 字段均为空),设置 field 74 后断言generate_config取值为真,并在model_rpc_client_test.py补 Python 侧写入断言。在 proto 注释或 release note 写明 field 73/74 的最小兼容版本与推荐升级顺序。
- 建议:在解析
- RPC 边界由 validate() 降级为 validate_finalized(),数值/类型兜底校验消失 @
rtp_llm/cpp/model_rpc/model_rpc_client.py:110- 建议:从
validate()拆出纯只读的校验函数(clamp/改写留在入口、断言进只读版本),在trans_input中与validate_finalized并列调用,恢复边界兜底;或在GenerateConfig上加「已校验」标记并在trans_input断言其存在,使新增入口漏调用时 fail-fast。建议新增语义自明的finalize_for_engine(...)收口,add_thinking_params保留薄壳;若确定不再需要边界兜底,请把「数值校验唯一责任方是请求入口」写进model_rpc_client.py:108-110注释与add_thinking_params的 docstring。
- 建议:从
- grammar 编译错误在 factory 边界被统一压平为 INVALID_PARAMS,丢失可重试语义 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:124- 建议:在 factory 按
absl::StatusCode映射:kInvalidArgument→INVALID_PARAMS、kResourceExhausted→MALLOC_FAILED(或既有可重试系统错误码)、其余 →EXECUTION_EXCEPTION;补一条单测断言「后端返回 ResourceExhausted 时 factory 不返回 INVALID_PARAMS」,避免上游精心构造的错误分类在边界处失效。
- 建议:在 factory 按
- grammarBackend() 为进程级可变 static,init 与请求线程存在数据竞争且无法重置 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:72- 建议:把后端所有权下移到 engine/executor 对象(随其生命周期创建与销毁),由 stream 通过引用获取;若短期必须保留 static,至少改为原子读写或加读写锁,并在
init()中对「已初始化且tokenizer_info_json不同」的情况 fail-fast 报错,把注释里的约束变成可执行检查。
- 建议:把后端所有权下移到 engine/executor 对象(随其生命周期创建与销毁),由 stream 通过引用获取;若短期必须保留 static,至少改为原子读写或加读写锁,并在
- ThinkModeLogitsProcessor 装配点移除后沦为死代码,max_batch_size 形参同时失效 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:99- 建议:若 think 约束已完全由 Python 归一化为 structural_tag,请在本 PR 内删除
ThinkModeLogitsProcessor.{h,cc}、其 BUILD 条目与单测,以及仅服务于它的memFill,并同步更新docs/backend/logits_processor.md;若需保留作为过渡实现,请在文件头注明「已下线、仅供回滚」并把单测改为断言「工厂在 think 模式下不再产出该处理器」。同时删除无用的max_batch_size形参,避免调用方误以为它仍影响构造。
- 建议:若 think 约束已完全由 Python 归一化为 structural_tag,请在本 PR 内删除
- GrammarConfig 保留无消费者的 tokenizer_info_json / override_stop_tokens,与 ModelConfig 同名字段混淆 @
rtp_llm/cpp/config/ConfigModules.h:377- 建议:从
GrammarConfig移除这两个字段(连同 pybind、pickle 元组、to_string()、.pyi),让 tokenizer 元数据只有ModelConfig一个来源,pickle 兼容按现有 legacy 分支模式再加一层长度/类型判定;本 PR 已在改 pickle 布局,此时迁移成本最低。若确为后续能力预留,请在ConfigModules.h就地注释「当前未被消费,真实来源为ModelConfig::tokenizer_info_json」。
- 建议:从
- grammar_terminate_without_stop_token 在 Python 与 C++/HTTP 两侧语义分裂 @
rtp_llm/cpp/engine_base/stream/GenerateConfig.h:236- 建议:明确该标志是「引擎内部派生量」而非用户可填字段:Python 侧改用
@property或不与 wire 字段同名的名字,消除括号漏写风险;C++ 侧若不希望外部覆盖该派生量,则在 RawRequest 转换处忽略客户端传值并删除对应断言,或在 Python 侧对该 leftover key 返回明确 400 而非静默丢弃。两侧注释写明唯一权威计算点。
- 建议:明确该标志是「引擎内部派生量」而非用户可填字段:Python 侧改用
- prepareSpeculative 在缺少 bitmask 输出缓冲时静默放行全部 draft token @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:560- 建议:拆分这三个条件:
matcher_为空或propose_step <= 0保持透传;bitmask_cpu_out == nullptr返回明确ErrorInfo(或 cap=0 并打一条限频 error 日志),使 runner 侧装配缺陷在测试与线上都能被发现;补一条「缺少 bitmask 输出缓冲」的负向单测断言不会返回propose_step。
- 建议:拆分这三个条件:
- 语法终止后仅接受单一 eos_token_id,多 EOS 及 EOS 被抑制场景请求硬失败 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:629- 建议:把终止态的合法终止集合从单个 id 扩展为集合(可复用
tokenizer_info的stop_token_ids或special_tokens.stop_words_id_list中的单 token 项),解码侧对集合内所有 id 放行、提交侧按集合判定;或在装配时校验「模型存在多个 eos 时拒绝安装 grammar 并给出明确原因」。补两条用例:多 EOS 模型提交非首个 EOS、min_new_tokens未满足时终止态的期望行为(明确是等待还是报错)。
- 建议:把终止态的合法终止集合从单个 id 扩展为集合(可复用
- MultiSeqLogitsProcessor 写 EOS 掩码缺少 vocab 边界校验 @
rtp_llm/cpp/models/logits_processor/MultiSeqLogitsProcessor.cc:39- 建议:抽出共用的 eos 合法性校验(置于
BitmaskUtils或BaseLogitsProcessor),在MultiSeqLogitsProcessor::process中先判断eos_token_id_ >= 0 && (size_t)eos_token_id_ < vocab_size,越界时返回与GRAMMAR_EOS_OUT_OF_VOCAB语义等价的ErrorInfo而不是继续写内存,并补一条越界 eos 的负向单测。
- 建议:抽出共用的 eos 合法性校验(置于
- XGrammarBackend::create 以 RAW 重建 TokenizerInfo 并丢弃 special_token_ids,该路径零覆盖 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:81- 建议:补直连测试:用
<0x20>类夹具构造 BYTE_FALLBACK 与 BYTE_LEVEL 词表,分别走serializeTokenizerInfo→XGrammarBackend::create,断言重建后GetSpecialTokenIds()/GetDecodedVocab()/GetStopTokenIds()与直接DeserializeJSON一致,并对同一 grammar 断言fillBitmask位图逐位相同。若 xgrammar 侧无法保证等价,应推动上游补齐序列化缺失的 token-id 查找表,而不是在 RTP 侧靠解码后词表重建。
- 建议:补直连测试:用
- Python 端 grammar vocab 取 max 与 C++ 端硬拒策略不一致,启动成功但每个 grammar 请求首步失败 @
rtp_llm/config/grammar_tokenizer_info.py:23- 建议:统一两侧策略:或在 Python 侧把 grammar vocab 夹紧到
model_vocab_size(对被丢弃的高位 token 记一条 WARN),或在XGrammarBackend::create拿到GetVocabSize()后立即与模型 vocab 比对,超出即在启动日志报错并禁用 grammar,而不是留到请求期。补一个tokenizer_vocab > model_vocab的单测锁定选定语义。
- 建议:统一两侧策略:或在 Python 侧把 grammar vocab 夹紧到
- legacy structures/triggers 形态的 structural_tag 在 thinking 模式下被拒且错误文案误导 @
rtp_llm/config/grammar_constraint.py:143- 建议:在
_structural_tag_format_node中对 legacy 形态做等价转换(把structures/triggers包装为可嵌入的 format 节点),或明确拒绝但把文案改为「thinking 模式下 structural_tag 必须使用 format 形态,structures/triggers 形态暂不支持」;同时拆分 :137 与 :143 的重复文案,并补 legacy 形态 × thinking 的单测。
- 建议:在
- grammar_constraint_only 校验 fail-open,且完全短路 golden 比对 @
rtp_llm/test/smoke/openai_comparer.py:230- 建议:两处静默 return 改为抛
SmokeException(QueryStatus.VALID_FAILED, ...),与DashGrpcComparer对齐;或在读不到response_format时回退到generate_config中的 typed grammar 字段。同时不要完全短路 golden,至少保留finish_reason与reasoning_tokens的区间断言(如reasoning_tokens <= max_thinking_tokens),避免约束通过但截断 / 预算行为回退时无人发现。
- 建议:两处静默 return 改为抛
- smoke 约束校验器多处静默放行且无自身单测 @
rtp_llm/test/smoke/grammar_constraint_validator.py:236- 建议:补一个纯 Python 的 py_test(无需 GPU),对每个 verifier 提供正反例:空 schema、空
begin/end的 tag、缺required与properties的 glm_xml、stop_after_first下的尾随多余文本。把上述静默分支统一改为抛「validator 不支持该形状」的ValueError,并把 :112 与 :236 的 schema 守卫抽成同一 helper,消除同类校验的两份实现。
- 建议:补一个纯 Python 的 py_test(无需 GPU),对每个 verifier 提供正反例:空 schema、空
- 「MTP + grammar(thinking OFF)」组合未真正进 CI,任务文件命名与注释具有误导性 @
rtp_llm/test/smoke/suites_h20_oss.bzl:268- 建议:要么新增一条真正开启 MTP(
--sp_type eagle --sp_model_type … --gen_num_per_cycle N)且--think_mode 0的 smoke case,覆盖非 reasoning 分支的 rollback 与 T0 接受逻辑;要么把q_r_mtp_grammar.json改名并同步修正其_comment,使文件名与注释不再承诺 CI 未实际验证的覆盖面。
- 建议:要么新增一条真正开启 MTP(
- 分词器家族探测函数在单测中被整体 mock,决定「模型能否启动」的分支零覆盖 @
rtp_llm/config/test/grammar_tokenizer_info_test.py:73- 建议:为三个探测函数各补直接单测(最小 fake:内层暴露伪 tiktoken
Encoding、vocab_files_names含 tiktoken、mergeable_ranks存在/不存在、sp_model.PieceToId/IdToPiece/vocab_size),并覆盖「三类都不匹配」的负向分支,断言抛出的错误类型与文案。保留现有拼装用例的 mock,但不要让 mock 成为这些生产边界的唯一覆盖。
- 建议:为三个探测函数各补直接单测(最小 fake:内层暴露伪 tiktoken
- XGrammarBackendTest 未覆盖 create() 的 nullptr 降级路径、ebnf 与未知 key_type @
rtp_llm/cpp/engine_base/grammar/test/XGrammarBackendTest.cc:50- 建议:补齐:(1) 空串与非法 JSON 的
create()应返回 nullptr——这是运维上「静默关闭约束解码」的关键行为,必须被锁住;(2)ebnf与未知 key_type 各一条用例,后者断言kInvalidArgument与「Unknown grammar key_type」文案;(3) 一条optionsFromConfig映射断言,覆盖compiler_cache_bytes为 0/负数与num_workers非法时的 clamp 行为。
- 建议:补齐:(1) 空串与非法 JSON 的
- PackedMaskLogits 的共用输入校验分支缺少负向与边界测试 @
rtp_llm/models_py/bindings/core/test/PackedMaskLogitsCpuFallbackTest.cc:11- 建议:补一组参数化负向用例,至少覆盖
vocab_size = 0、vocab_size > logits_columns、mask 宽度不足、row_indices数量与 mask 行数不匹配四类,断言异常信息包含对应提示;再补一条mask_rows == 0的 no-op 用例确认 logits 未被修改。这些校验在 CPU 路径即可触发,无需 GPU 资源。
- 建议:补一组参数化负向用例,至少覆盖
P3
- MTP 解码下每步都会做一次不会被消费的 bitmask 填充 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:656- 建议:让
refreshAfterCommit惰性化:提交时只更新committed_output_len_并把device_mask_state_置为失效,真正buildState推迟到process()首次需要掩码时执行(apply()已有 token_len 不匹配即重建的分支可复用)。normal decode 行为不变,MTP 路径省掉每步一次无用填充;若保留现状,请补一条注释说明该冗余是有意为之。
- 建议:让
- 掩码哨兵由 -INFINITY 改为 -FLT_MAX,既有非 grammar 调用方数值语义变化 @
rtp_llm/models_py/bindings/common/kernels/mask_logits.cu:45- 建议:在
mask_logits侧显式约定半精度哨兵取值(统一 clamp 到该 dtype 的 lowest 而非溢出为-inf),并让测试断言这个约定值而非接受两种可能;补一条 CPU/GPU 一致性用例(相同 logits 与 packed mask 走两条路径,同 dtype 逐元素比对)。同时在 PR description 说明 fp32 哨兵变更对既有调用方的可见影响。
- 建议:在
- SpecLogitsVerifyRunner::run 的失败语义不一致 @
rtp_llm/cpp/models/logits_processor/SpecLogitsVerifyRunner.cc:275- 建议:明确并统一策略:若这些形状断言确为「不可能发生」的内部不变量,请在函数头注释写清「违反即 fail-fast 终止本批」的理由;若希望与 bad_alloc 一致地逐流降级,则把
std::exception纳入 catch 并转为makeFailureResultForActiveStreams,同时把测试断言从EXPECT_THROW改为检查processor_errors。
- 建议:明确并统一策略:若这些形状断言确为「不可能发生」的内部不变量,请在函数头注释写清「违反即 fail-fast 终止本批」的理由;若希望与 bad_alloc 一致地逐流降级,则把
- CudaMaskLogitsOpTest 保留调试打印且用浮点 == 直接断言 @
rtp_llm/models_py/bindings/cuda/ops/tests/CudaMaskLogitsOpTest.cc:48- 建议:删除
MYDEBUG_*打印(或改为失败时才输出的SCOPED_TRACE);fp32 断言改为EXPECT_FLOAT_EQ,对哨兵位置断言== -FLT_MAX、未掩码位置用EXPECT_NEAR,并把索引写进断言消息以便定位。
- 建议:删除
- 接口签名改造混入大量无关 clang-format 重排 @
rtp_llm/cpp/models/logits_processor/test/RecommendationLogitsProcessorTest.cc:1- 建议:后续把纯格式化拆成独立 commit(或独立 PR)先落地,再提交语义变更,使逻辑变更的 diff 保持可审;本 PR 如不便拆分,请在 PR description 显式列出这些文件中属于语义变更的具体断言,方便评审定点复核。
Checklist Violations (18 fail / 58 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
核心依赖 xgrammar 指向个人 GitHub fork,存在供应链与可复现性风险
new_git_repository(name="xgrammar", remote=<个人账号 fork>, commit="ac8a29c...", init_submodules=False),无 sha256 / 镜像 / vendoring / patch。该仓库被改名、删除或强推即导致含 CI 与发版的全仓构建失败,而它是本 PR 全部结构化输出能力的唯一来源。仓库既有约定是 git 依赖统一登记在deps/git.bzl的git_deps(),自研改动用patches=[...]承载(cutlass、rules_python 均如此),本 PR 另开repositories.bzl并在WORKSPACE单独 load,形成第二套注册入口。xgrammar.BUILD:1注释写「v0.2.2」与实际 pin 的 fork commit 无法互证,且init_submodules=False下仍 glob3rdparty/picojson/picojson.h,来源仅靠注释保证。 - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
掩码哨兵由 -INFINITY 改为 -FLT_MAX,既有非 grammar 调用方数值语义变化
diff 把NegativeInfinity<T>()改为MaskedLogitValue<T>():float 返回-FLT_MAX(:43-46),__half/__nv_bfloat16显式特化为-inf(:48-56);CPU 回退static_cast<scalar_t>(-FLT_MAX)(CudaOps.cc:194)在半精度下同样溢出为-inf。该改动同时把 Tree/Recommendation/MultiSeq 等既有非 grammar 调用方的 fp32 哨兵从-INFINITY变为-FLT_MAX:整行被掩码时 softmax 由 NaN 变为近似均匀分布。测试把两种取值都固化为期望(CudaMaskLogitsOpTest.cc:56 断言-FLT_MAX、:88 只断言isinf),使差异无法被察觉。 - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
grammar_terminate_without_stop_token 在 Python 与 C++/HTTP 两侧语义分裂
C++ 侧该标志是JSONIZE(grammar_terminate_without_stop_token)的可写 bool 字段(:56、:236),InferenceDataTypeTest.cc:65-73明确验证 HTTP RawRequest 可直接置 true,并经QueryConverter.cc:106无条件透传、参与LogitsProcessorFactory.cc:113-114判定。Python 侧它是派生方法GenerateConfig.grammar_terminate_without_stop_token()(generate_config.py:563-566,非 pydantic 字段),_apply_updates改用key in model_fields判定(:448-456)后,客户端传该 key 会被静默丢弃。同一 JSON key 在两个请求入口行为不同;方法名与 wire 字段同名,将来漏写括号的 `if config.grammar_terminate_without_stop_token: - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
proto field 73 双向停用后旧客户端约束被静默丢弃,且转换层缺少 field 73/74 测试锚点
model_rpc_service.proto:139-143把response_format = 73标[deprecated = true]并新增grammar_terminate_without_stop_token = 74;QueryConverter.cc:100-106只读 typed 字段与 field 74,已无任何response_format读取,Python 侧_trans_jsonable_options也只写 69-72(model_rpc_client.py:76-79)。滚动升级期「旧前端 + 新引擎」时旧前端仍把约束写在 field 73,新引擎既不读取也不告警,结构化约束静默失效、输出退化为自由文本;反向错配时 field 74 同样被静默忽略。QueryConverterTest.cc全文检索response_format与grammar_terminate_without_stop_token均无命中,两条路径都没有测试锚定。 - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
对外配置 --grammar_backend/GRAMMAR_BACKEND 被直接移除,缺少弃用期与服务级禁用开关
init_grammar_group_args现只剩--constrained_json_disable_any_whitespace/--grammar_num_workers/--grammar_compiler_cache_bytes;原--grammar_backend(envGRAMMAR_BACKEND,help "xgrammar or none")整段删除,GrammarConfig字段与 pybind 绑定同步移除,全仓仅剩ConfigInit.cc:1352-1355注释「grammar_backend was removed; validate and discard it」。后果有三:存量脚本显式传该参数时严格parse_args(server_args.py:295)直接退出;依赖GRAMMAR_BACKEND=none关闭约束解码的部署升级后该 env 被静默忽略、行为变为常开;legacy pickle 分支把旧值读出即丢弃,"none"的关闭语义在反序列化后被静默还原。 - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
grammarBackend() 为进程级可变 static,init 与请求线程存在数据竞争且无法重置
grammarBackend()返回函数内static std::shared_ptr<XGrammarBackend>的非 const 引用(:66-74),init()直接对它赋值(:79),而createLogitsProcessors在请求线程里auto& backend = grammarBackend();后判空并解引用(:106-111)。shared_ptr的读写不是原子的,进程内若发生二次init()(NormalExecutor.cc:129与MtpExecutor.cc:325各有一处调用、模型热加载、测试串联)即为未定义行为。该 static 永不释放,持有整份 TokenizerInfo 与编译缓存直至进程退出,换 tokenizer 后仍复用旧后端。注释已承认这一约束,但代码层面没有任何断言或保护。 - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
SpecLogitsVerifyRunner::run 的失败语义不一致
run()用 try/catch 把std::bad_alloc与c10::Error转成makeFailureResultForActiveStreams(:275-281),即降级为「只有相关流报错、同批其他流继续」。但同一函数体内的RTP_LLM_CHECK_WITH_INFO(draft 列数不符、行数超 int32 等)抛出的异常不在 catch 列表内,会一路穿过runSpecLogitsVerifyIfNeeded/decodeStep/MtpExecutor::process,把同批全部流一起打挂;MtpExecutorTest.cc的EXPECT_THROW把该行为固化成契约,与makeFailureResultForActiveStreams的设计意图相反。 - [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue
CudaMaskLogitsOpTest 保留调试打印且用浮点 == 直接断言
三个子块各保留两次printBufferData(logits, "MYDEBUG_BEFORE_MASK_LOGITS")/"MYDEBUG_AFTER_MASK_LOGITS"(:48-50、:78-80、:108-110),带MYDEBUG前缀的临时调试输出被提交进 CI;fp32 断言使用ASSERT_TRUE(logits_ptr[i] == expect_vec[i])(:64)对浮点做精确相等比较,失败时也不会打印实际值与下标。 - [6.1] Quality — 逻辑变更未混入无关格式化 → issue
接口签名改造混入大量无关 clang-format 重排
该测试文件约千行 diff 绝大部分为 clang-format 重排,真正的功能性变更只有process()返回类型适配与掩码期望值改为BaseLogitsProcessor::neg_inf;rtp_llm/test/smoke/case_runner.py亦为同类混合。在这种规模的重排中,评审无法在合理成本内逐行排除「重排里夹带断言语义微调」的可能。 - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
smoke 约束校验器多处静默放行且无自身单测
该模块是 grammar smoke 的唯一正确性判据,但全仓仅被两个 comparer import,test/smoke/BUILD中无任何 py_test。四处可致校验形同虚设的分支::236-238 把fmt.get("json_schema") or {}交给 :31 的if not schema: return,键名写错即任意合法 JSON 通过——而同类的 :112-113 却显式raise ... schema missing,宽严不一;:185-186 遇到begin/end为空的 tag 直接continue且matched保持 0;:147required与properties皆缺时退化为空列表;stop_after_first语义完全未被校验。新增的dash_structural_tag_think.json正走 structural_tag 分支,完全依赖此校验器。 - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
MTP 解码下每步都会做一次不会被消费的 bitmask 填充
acceptCommittedLocked每次提交成功后无条件调用decode_mask_builder_->refreshAfterCommit(:656),内部buildState(:303-320)→prepareBitmask→fillMatcherBitmask会走一次完整的 xgrammarFillNextTokenBitmask。但 MTP 路径上MtpBatchStreamProcessor::gatherSpecSamplerInput只调allocateSamplerInputs(:120-121),后者显式logits_processor_states_ptr.reset()(NormalSamplerInputGatherer.cc:117)且全文无setLogitsProcessorInputs调用,即 grammar 的process()从不执行、掩码全部来自SpecLogitsVerifyRunner。因此device_mask_state_被逐步构建却从不使用。尚无 - [6.1] Software Engineering — SRP:模块/类职责单一 → issue
RPC 边界由 validate() 降级为 validate_finalized(),数值/类型兜底校验消失
diff 把trans_input中的input_py.generate_config.validate()(覆盖 top_k/top_p/max_new_tokens/num_beams/stop_words_list/calculate_loss/prompt-logits 等约二十余项区间与类型校验)替换为ResponseFormatBuilder.validate_finalized()——后者仅校验json_format为假、response_format为 None、grammar 字段唯一且为 str(response_format_builder.py:112-131)。全量校验被搬进add_thinking_params()(generate_config.py:525-561),该函数现同时承担「解析 think token + 全量校验 + grammar finalize」三职,仅少数请求入口触发;绕过入口直接构造GenerateInput的路径可把非法值直送引擎。 - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
掩码哨兵由 -INFINITY 改为 -FLT_MAX,既有非 grammar 调用方数值语义变化
diff 把NegativeInfinity<T>()改为MaskedLogitValue<T>():float 返回-FLT_MAX(:43-46),__half/__nv_bfloat16显式特化为-inf(:48-56);CPU 回退static_cast<scalar_t>(-FLT_MAX)(CudaOps.cc:194)在半精度下同样溢出为-inf。该改动同时把 Tree/Recommendation/MultiSeq 等既有非 grammar 调用方的 fp32 哨兵从-INFINITY变为-FLT_MAX:整行被掩码时 softmax 由 NaN 变为近似均匀分布。测试把两种取值都固化为期望(CudaMaskLogitsOpTest.cc:56 断言-FLT_MAX、:88 只断言isinf),使差异无法被察觉。 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
XGrammarBackendTest 未覆盖 create() 的 nullptr 降级路径、ebnf 与未知 key_type
XGrammarBackend::create有三条「返回 nullptr 静默关闭结构化输出」的分支(tokenizer_info_json.empty():66、DeserializeJSON失败 :72、GetVocabSize() <= 0:86)另加 catch-all :96;compile()支持json/regex/ebnf/structural_tag并对未知类型返回 InvalidArgument;optionsFromConfig(:102-108)负责constrained_json_disable_any_whitespace取反、num_workersclamp 到 ≥1、compiler_cache_bytes <= 0转 -1。测试全部经makeBackend()成功路径构造,key_type 仅用到 json/regex/structural_tag:ebnf、未知 key_type、三条 nullptr 降级与 options 归一均无断言。 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
PackedMaskLogits 的共用输入校验分支缺少负向与边界测试
CudaOps.cc:44的validatePackedMaskLogitsInputs是 CUDA 路径与 CPU 回退路径共用的前置校验,覆盖 logits 维度与最后一维 stride、mask 必须是 int32 二维、packed_allow_mask.size(0)与row_indices.numel()相等、vocab_size落在(0, logits_columns]、mask 宽度不得小于(vocab_size + 31) / 32等多组RTP_LLM_CHECK_WITH_INFO。新增的四个 CPU 回退用例全部走正向路径(:19、:45、:63、:82 均为ASSERT_NO_THROW),没有任何一条断言这些校验会按预期抛错,也未覆盖mask_rows == 0的提前返回。
RTP-LLM Checklist
- [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue
ThinkModeLogitsProcessor 装配点移除后沦为死代码,max_batch_size 形参同时失效
createLogitsProcessors只构造 grammar / tree / recommendation / multi-seq 四类(:100-144),并注释「do not add a legacy processor fallback」。全仓搜索确认ThinkModeLogitsProcessor::fromGenerateInput已无生产调用点,但ThinkModeLogitsProcessor.{h,cc}仍列在models/BUILD:55,68,ThinkModeLogitsProcessorTest.cc仍在 CI 中通过make_shared直接构造并绿灯,形成「测试通过但代码不在链路上」的假覆盖;仅服务于它的BaseLogitsProcessor::memFill亦随之失活。同时max_batch_size形参在新函数体内已无任何使用,却仍留在LogitsProcessorFactory.h的公开签名里。 - [I] 代码质量 — 同一功能用统一工具函数 → issue
MultiSeqLogitsProcessor 写 EOS 掩码缺少 vocab 边界校验
cur_logit_mask_host_ptr[eos_token_id_] = 0;(:39)直接以未校验的eos_token_id_为下标写入长度为vocab_size = inputs.logits.size(1)的裸uint8_t缓冲,模型配置的 eos 若超出 logits 宽度即为一次静默堆越界写。该行本身在本 PR 之前已存在(本次只改了返回类型),但同一 PR 在 grammar 路径上恰恰为完全相同的语义新增了专门错误码与校验(GRAMMAR_EOS_OUT_OF_VOCAB,GrammarLogitsProcessor.cc:82-87),两处处理方式不统一。
Python Static-First Checklist
- [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
分词器家族探测函数在单测中被整体 mock,决定「模型能否启动」的分支零覆盖
_build_tokenizer_info帮助方法同时mock.patch.object掉_is_fast_tokenizer、_is_tiktoken_tokenizer、_is_sentencepiece_tokenizer、_is_byte_level_tokenizer四个探测函数(:73-92),因此所有test_build_tokenizer_info_serializes_*用例实际只验证「给定分支标志后 metadata 拼装正确」。全文件中只有_is_byte_level_tokenizer另有直接用例(:101-113);_is_tiktoken_tokenizer依赖的_has_tiktoken_encoding/_has_tiktoken_vocab_file/_has_mergeable_ranks与_is_sentencepiece_tokenizer依赖的_iter_sentencepiece_candidates/ `has_sentencepiece_api
Strengths
acceptCommittedLocked用统一rollback_commit闭包把 matcher、committed_output_len_、缓存 mask 视作同一提交单元,任一 token 失败即整批回滚,回滚再失败则markFinished并拼接原始 cause,不留半可见前缀(GrammarLogitsProcessor.cc:576-660)。- 终止态刻意保持
TERMINATED而非FINISHED,保证min_new_tokens/ignore_eos延迟结束时仍只允许 EOS,不退回无约束生成,注释与单测同时锁定该语义(GrammarLogitsProcessor.cc:624-628)。 - 投机校验用
ProvisionalSpecAcceptTracker保证 spec-verify 只做试探性 accept、返回前必定回滚,并有「两次相同调用得到相同 cap」的单测固化该不变量。 DecodeMaskBuilder用pending_bitmask_uploads_显式记录 H2D event 并在复用 pinned 缓冲前synchronize(),注释明确「正确性不得依赖后续 sampler 的 D2H 同步」——这是复用 pinned buffer 最易漏掉的点。clearBitmaskTokenRange/forceTokenInBitmask对同字、跨字、末字对齐与越界 token 分别处理,逐组手工验算正确并有专项单测;packed_mask_logits对越界 word 取「不允许」而非越界读。XGrammarBackend把bad_alloc/runtime_error/ 其它异常分别映射为 ResourceExhausted / InvalidArgument / Unknown,用户语法非法与系统可重试在状态码层可区分;create()为 noexcept 并以 nullptr 表示优雅降级。- proto 兼容处理规范:field 73 保留并标
[deprecated = true]、注释写明投影关系、新增 field 74 而非复用旧编号(model_rpc_service.proto:139-143)。 GrammarConfigpickle 用py::isinstance<py::str>(t[0])区分 legacy 五元组与当前布局,并有config_pickle_test.py覆盖当前往返、legacy 读取、伪造短 state 被拒三类场景。- 日志脱敏到位:
GrammarConfig::to_string()与GenerateConfig::debugString()对 grammar / tokenizer 内容只输出长度,不输出原文。 grammar_group_args.py把default=改为读取 C++grammar_config.<field>,消除 Python 与ConfigModules.h两份默认值漂移。GrammarLogitsProcessorTest.cc使用真实 xgrammar 后端而非 mock,覆盖 undersized bitmask、grammar vocab 超模型 vocab、越界 EOS、all-true mask 视为 no-op、整批 draft 回滚等负向场景,并断言具体ErrorCode与错误文案片段。
| model_vocab_size=int(self.model_config.vocab_size or 0), | ||
| stop_token_ids=self._collect_tokenizer_info_stop_token_ids(), | ||
| ) | ||
| except Exception as e: |
There was a problem hiding this comment.
[P1] 启动期强制构建 grammar tokenizer 元数据,不受支持的 tokenizer 直接启动失败且无逃生开关
load_tokenizer() 无条件调用 _fill_grammar_tokenizer_info()(:301),任何异常在 :329-332 被升级为 RuntimeError,即模型初始化失败、进程起不来。触发条件均非人为构造:build_grammar_tokenizer_info_json 在 fast/tiktoken/sentencepiece 三类探测全部落空时 raise ValueError("Unsupported tokenizer type")(grammar_tokenizer_info.py:192);stop_token_ids 为空同样 raise(:155-156)。C++ 侧本有降级路径:tokenizer_info_json 为空时 XGrammarBackend::create 返回 nullptr,仅让 grammar 请求报错(LogitsProcessorFactory.cc:107-111);但本 PR 同时删除了 GRAMMAR_BACKEND(原 help "xgrammar or no...
建议: 二者取一:(1) 对「tokenizer 家族不受支持 / 无 stop token」这类可预期情况降级为 logging.warning 并保持 tokenizer_info_json 为空,复用 C++ 已有 backend-disabled 语义,只让真正请求结构化输出的请求失败;(2) 保留 fail-fast,但提供显式开关(恢复 GRAMMAR_BACKEND=none 或新增 --grammar_require_tokenizer_info)。无论哪种,请在 PR description 列出受支持的 tokenizer 清单,便于存量模型灰度前自查。
| auto result = makeResult(shape); | ||
| result.processor_errors = std::move(merge_result.processor_errors); | ||
| return result; | ||
| } catch (const std::bad_alloc& e) { |
There was a problem hiding this comment.
[P3] SpecLogitsVerifyRunner::run 的失败语义不一致
run() 用 try/catch 把 std::bad_alloc 与 c10::Error 转成 makeFailureResultForActiveStreams(:275-281),即降级为「只有相关流报错、同批其他流继续」。但同一函数体内的 RTP_LLM_CHECK_WITH_INFO(draft 列数不符、行数超 int32 等)抛出的异常不在 catch 列表内,会一路穿过 runSpecLogitsVerifyIfNeeded / decodeStep / MtpExecutor::process,把同批全部流一起打挂;MtpExecutorTest.cc 的 EXPECT_THROW 把该行为固化成契约,与 makeFailureResultForActiveStreams 的设计意图相反。
建议: 明确并统一策略:若这些形状断言确为「不可能发生」的内部不变量,请在函数头注释写清「违反即 fail-fast 终止本批」的理由;若希望与 bad_alloc 一致地逐流降级,则把 std::exception 纳入 catch 并转为 makeFailureResultForActiveStreams,同时把测试断言从 EXPECT_THROW 改为检查 processor_errors。
Checklist: [6.1] 错误语义:fail-fast/retry/fallback/silent 行为显式
| printBufferData(logits, "MYDEBUG_AFTER_MASK_LOGITS"); | ||
|
|
||
| auto logits_cpu = logits.cpu().contiguous(); | ||
| auto logits_ptr = logits_cpu.data_ptr<float>(); |
There was a problem hiding this comment.
📍 实际位置 rtp_llm/models_py/bindings/cuda/ops/tests/CudaMaskLogitsOpTest.cc:48(不在 diff 展示范围内,就近挂载)
[P3] CudaMaskLogitsOpTest 保留调试打印且用浮点 == 直接断言
三个子块各保留两次 printBufferData(logits, "MYDEBUG_BEFORE_MASK_LOGITS") / "MYDEBUG_AFTER_MASK_LOGITS"(:48-50、:78-80、:108-110),带 MYDEBUG 前缀的临时调试输出被提交进 CI;fp32 断言使用 ASSERT_TRUE(logits_ptr[i] == expect_vec[i])(:64)对浮点做精确相等比较,失败时也不会打印实际值与下标。
建议: 删除 MYDEBUG_* 打印(或改为失败时才输出的 SCOPED_TRACE);fp32 断言改为 EXPECT_FLOAT_EQ,对哨兵位置断言 == -FLT_MAX、未掩码位置用 EXPECT_NEAR,并把索引写进断言消息以便定位。
Checklist: [6.1] 无 per-forward 调试日志 / 噪声热路径输出
| @@ -9,25 +9,22 @@ using namespace std; | |||
| namespace rtp_llm { | |||
There was a problem hiding this comment.
📍 实际位置 rtp_llm/cpp/models/logits_processor/test/RecommendationLogitsProcessorTest.cc:1(不在 diff 展示范围内,就近挂载)
[P3] 接口签名改造混入大量无关 clang-format 重排
该测试文件约千行 diff 绝大部分为 clang-format 重排,真正的功能性变更只有 process() 返回类型适配与掩码期望值改为 BaseLogitsProcessor::neg_inf;rtp_llm/test/smoke/case_runner.py 亦为同类混合。在这种规模的重排中,评审无法在合理成本内逐行排除「重排里夹带断言语义微调」的可能。
建议: 后续把纯格式化拆成独立 commit(或独立 PR)先落地,再提交语义变更,使逻辑变更的 diff 保持可审;本 PR 如不便拆分,请在 PR description 显式列出这些文件中属于语义变更的具体断言,方便评审定点复核。
Checklist: [6.1] 逻辑变更未混入无关格式化
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1006
Status: BLOCKING
Summary: P0/0 · P1/5 · P2/23 · P3/5
Reviewed: commit e002829a175e · 2026-08-04 11:22 UTC+8
Blocking Issues
P1
- processor_errors 行坐标与 dispatcher 的 batch_idx_in 不一致,tiling 同批时语法错误归属错误 stream @
rtp_llm/cpp/normal_engine/NormalOutputDispatcher.cc:79- 建议:在
dispatch中额外维护sampler_row_idx,按与setLogitsProcessorInputs相同的samplerRows(stream)累加后传给collectStreamSamplerError(success索引一并修正),并把samplerRows()提升为GenerateStream公共方法或移入共享头,避免两处各写一份口径。同时在collectStreamSamplerError内加RTP_LLM_CHECK固化「processor_errors 行 == sampler 输入行」不变量,并在NormalOutputDispatcherTest.cc补一条「context stream(num_return_sequences>1) + 语法 stream 混批」用例,断言错误落在正确的 stream 上。
- 建议:在
- think 模式被无条件转为 grammar 约束解码,且 think + beam / n>1 由可用变为硬失败 @
rtp_llm/config/response_format_builder.py:80- 建议:为 reasoning envelope 增加显式开关(如
--enable_reasoning_grammar_envelope,默认关闭或按模型灰度),使「未请求结构化输出的 think 流量」能退回原有非约束路径;同时在 PR description 与发布说明中明确标注think + n>1 / beam search由可用变为 400 类错误,便于上游适配。若确定默认开启,请补充 think 模式下 envelope 编译耗时与 TPOT 的对比数据,说明常规 think 流量的性能回退可接受。
- 建议:为 reasoning envelope 增加显式开关(如
- 启动期强制构建 grammar tokenizer 元数据,不受支持的 tokenizer 直接启动失败且无逃生开关 @
rtp_llm/models/base_model.py:329- 建议:复用 C++ 侧已有的降级语义:新增显式开关(如
--grammar_disable/ 恢复--grammar_backend=none),开启时捕获异常仅logging.warning并保持tokenizer_info_json为空,让携带约束的请求由LogitsProcessorFactory.cc:107的现有分支在请求级失败,而非阻塞引擎启动。这样既保留 fail-fast 默认行为,又给运维留下回滚手段。同时补一条「不受支持 tokenizer 仍可正常启动、非 grammar 请求可用」的单测;若坚持默认 fail-fast,请在发布说明中列出受影响的 tokenizer 类型与stop_token_ids为空的模型清单。
- 建议:复用 C++ 侧已有的降级语义:新增显式开关(如
- --grammar_backend / GRAMMAR_BACKEND 被直接移除,存量启动脚本升级即退出且失去唯一关闭手段 @
rtp_llm/server/server_args/grammar_group_args.py:6- 建议:保留
--grammar_backend/GRAMMAR_BACKEND一个发布周期作为 deprecated 别名:仍注册参数,取值xgrammar时接受并打 WARN,取值none时映射到明确的关闭路径(跳过_fill_grammar_tokenizer_info,让XGrammarBackend::create走已有的「empty → structured output disabled」分支,携带约束的请求返回明确错误)。并在server_args_test.py补一条「传入已废弃参数不导致启动失败」的回归用例;若确定硬删除,需在 PR description 与发布说明给出迁移指引,并确认部署模板 / 压测脚本 / smoke 配置已无该参数。
- 建议:保留
- MTP 下非 grammar logits processor 由静默降级变为请求级失败,无开关与迁移路径 @
rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:850- 建议:三者择一并明确该不兼容:(1)为
RecommendationLogitsProcessor/MultiSeqLogitsProcessor实现mtpCapability()与prepareSpeculative(),让 MTP 真正支持;(2)增加引擎级开关(如MTP_ALLOW_UNSUPPORTED_LOGITS_PROCESSOR)保留旧的降级行为并配合限流 WARN,便于线上回滚;(3)若确认该组合从未被支持,请在 PR description 与发布说明中列出受影响配置(combo_token_size、num_return_sequences>1、beam + MTP),并在MtpExecutorTest.cc补一条断言错误码与错误信息稳定的用例。
- 建议:三者择一并明确该不兼容:(1)为
Non-blocking Suggestions
P2
- proto field 73 被引擎单向停用后旧客户端约束静默丢弃,且转换层缺少 field 73/74 测试锚点 @
rtp_llm/cpp/model_rpc/QueryConverter.cc:100- 建议:增加
if (config_proto->has_response_format())分支:过渡期至少打一条限流 WARN(含 trace_id)说明该字段已废弃且被忽略;更安全的做法是四个 typed 字段均为空而 field 73 已设置时直接返回ERROR_GENERATE_CONFIG_FORMAT,让老客户端得到明确失败。并在QueryConverterTest.cc补「仅设置 field 73」与「field 74 →grammar_terminate_without_stop_token」两条用例锁定行为,同时在 PR description 写明前端与引擎的最小版本配套要求。
- 建议:增加
- 核心依赖 xgrammar 指向个人账号 fork,构建不可复现且无镜像与回滚手段 @
3rdparty/xgrammar/repositories.bzl:7- 建议:收敛到与其它三方依赖一致的入口:pin 上游 tag(
http_archive或组织内可控镜像)并补完整性校验,把 structural-tagmax_tokens改动以 Bazelpatches = [...]形式放进3rdparty/xgrammar/,使补丁可评审、可追溯、可回滚。若短期必须用 fork,请改为组织账号或内部镜像,并把xgrammar.BUILD:1的版本注释改为「上游基线 tag/commit + 本地补丁摘要 + 上游 PR 链接 + 责任人」,使构建来源可审计。
- 建议:收敛到与其它三方依赖一致的入口:pin 上游 tag(
- matcher 进入 FINISHED 后解码不再施加任何约束(fail-open),与 MTP 路径语义相反 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:411- 建议:把
FINISHED的语义与TERMINATED对齐:case FINISHED: return forceToken(logits, eos_token_id);,eos 越界时继续返回GRAMMAR_EOS_OUT_OF_VOCAB。这样即使上游误吞首次错误,也只会提前收尾而不会产出违反 schema 的内容。并补一条单测:构造buildState失败后再次process,断言仍只放行 EOS。
- 建议:把
- grammar 编译错误在 factory 边界被压平为 INVALID_PARAMS,丢失可重试语义 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:124- 建议:在
createLogitsProcessors中按matcher_or.status().code()分派:kResourceExhausted/kUnknown映射到资源型或可重试错误码,仅kInvalidArgument映射INVALID_PARAMS;并补两条单测,分别用非法 schema 与模拟资源耗尽的 mock backend,断言 stream 上报的错误码不同。
- 建议:在
- 语法编译在请求路径同步执行,无超时/规模上限,耗时仅 DEBUG 可见且无指标 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:131- 建议:补可运维护栏:(1)在
keyFromGenerateConfig/createMatcherFromKey入口对key_string长度设上限并返回INVALID_PARAMS;(2)为compile增加耗时阈值或全局并发上限,超限返回可重试状态码;(3)把elapsed_ms、缓存命中/未命中、失败计数上报为 metric,或至少把成功路径改为超阈值才打 INFO。可参考 vLLM/SGLang 将 grammar 编译放到后台线程池并带超时的做法。
- 建议:补可运维护栏:(1)在
- ThinkModeLogitsProcessor 装配点移除后沦为死代码,think 预算在直连路径静默失效 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:99- 建议:二选一并在 PR description 说明该行为变更:(a) 确认 think 预算已全部由 Python 归一化为 structural_tag,则同 PR 删除
ThinkModeLogitsProcessor.{h,cc}、BUILD 条目与其测试,更新docs/backend/logits_processor.md与dash_sc/think.py注释,移除max_batch_size形参及调用点实参,并在QueryConverter对「设置了in_think_mode/max_thinking_tokens但四个 typed 字段全空」的请求打 WARN 或 fail-fast;(b) 若需兼容直连 gRPC 的裸GenerateConfig,则保留创建分支,或至少在头文件/文档标注 deprecated 并写明迁移路径与删除计划。
- 建议:二选一并在 PR description 说明该行为变更:(a) 确认 think 预算已全部由 Python 归一化为 structural_tag,则同 PR 删除
- GrammarConfig 保留无消费者的 tokenizer_info_json / override_stop_tokens,与 ModelConfig 同名字段混淆 @
rtp_llm/cpp/config/ConfigModules.h:377- 建议:删除这两个字段及其
def_readwrite、pickle 槽位、to_string()输出、.pyi与config_pickle_test.py断言,保留ModelConfig::tokenizer_info_json为唯一真源。若override_stop_tokens属后续规划,请先不要暴露到 Python,或在本 PR 补上真正消费它的逻辑(例如create()重建TokenizerInfo时覆盖 stop_token_ids)与单测;若确需保留仅为 pickle 布局兼容,请在字段声明处注明并在to_string()中标记 deprecated。
- 建议:删除这两个字段及其
- grammar_terminate_without_stop_token 在 Python 派生与 HTTP 外部可写两侧语义分裂 @
rtp_llm/cpp/engine_base/stream/GenerateConfig.h:236- 建议:明确该字段定位:若属服务端内部派生量,则从
RawRequest的 JSONIZE 列表移除、由 HTTP 入口按同一套ResponseFormatBuilder规则计算,测试改为断言外部传入被忽略;同时把该语义改为可从已 finalize 的内容重新推导(例如在 envelope 中标记 final format 类型),或声明为普通字段并在_apply_updates中排除用户覆盖。补一条单测:GenerateConfig.model_validate(cfg.model_dump())后取值与原值一致。
- 建议:明确该字段定位:若属服务端内部派生量,则从
- RPC 边界由 validate() 降级为 validate_finalized(),数值/类型兜底校验消失 @
rtp_llm/cpp/model_rpc/model_rpc_client.py:110- 建议:把
validate()尾部唯一的写操作_normalize_grammar_fields()拆出,保留一个纯只读的validate_types(),在trans_input中与validate_finalized()一起调用——既满足「序列化不得改写共享配置」的新约束,又保住 RPC 边界最后一道类型/范围防线;或把validate()调用点移到各入口所有 mutation 之后。若确定不恢复,请在trans_input注释与validate()docstring 写明该不变量,并补一条「stop_words_list含非法值在何处被拒绝」的单测。
- 建议:把
- ResponseFormatBuilder.apply 原地改写配置且异常时无回滚,调用方重试会作用于半成品状态 @
rtp_llm/config/response_format_builder.py:91- 建议:让
apply()具备失败原子性:在投影前快照response_format / json_format / json_schema / regex / ebnf / structural_tag与三个_reasoning_*私有属性,try/except中失败即回填快照后再抛出;或改为先构建目标值、全部校验通过后一次性提交到 config。并补一条「apply 抛异常后 config 各字段与调用前完全一致」的单测。
- 建议:让
- Python 端 grammar vocab 取 max 与 C++ 端硬拒策略不一致,启动成功但每个 grammar 请求首步失败 @
rtp_llm/config/grammar_tokenizer_info.py:23- 建议:在
_build_encoded_vocab或build_grammar_tokenizer_info_json中显式处理tokenizer_vocab_size > model_vocab_size:要么启动期打 WARN 并说明「请扩容模型 vocab 或裁剪 added tokens」,要么把 grammar vocab 截断到model_vocab_size并对被裁掉的 token id 记 WARN;同时补一条单测断言该场景的行为(当前无覆盖)。
- 建议:在
- legacy structures/triggers 形态的 structural_tag 在 think 模式下被硬拒且错误文案误导 @
rtp_llm/config/grammar_constraint.py:143- 建议:在
_structural_tag_format_node中补齐 legacy 形态处理:识别structures/triggers并转换为等价format节点后参与 envelope 拼接;若确认不打算支持 reasoning 包裹,请把错误信息改为明确的「think 模式暂不支持 structures/triggers 形态的 structural_tag,请改用 format 形态」,并在generate_config_test.py补一条断言该错误信息的用例。
- 建议:在
- MultiSeqLogitsProcessor 写 EOS 掩码缺少 vocab 边界校验,存在越界写风险 @
rtp_llm/cpp/models/logits_processor/MultiSeqLogitsProcessor.cc:39- 建议:在
process()开头校验eos_token_id_ >= 0 && static_cast<size_t>(eos_token_id_) < vocab_size,不满足时返回ErrorInfo(ErrorCode::INVALID_PARAMS, ...),与 grammar 路径的GRAMMAR_EOS_OUT_OF_VOCAB处理保持一致;并在RecommendationLogitsProcessorTest或同目录补一条 eos 越界的负向用例。
- 建议:在
- MTP 掩码行索引对 streams 顺序与 allStreams 顺序存在隐式依赖,缺少断言 @
rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:1045- 建议:在
runSpecLogitsVerify内显式基于StreamGroups(streams).allStreams()建立顺序,或对每个 stream 断言!stream->isContextStream();并在SpecLogitsVerifyRunner::LaunchTask的注释中写明「stream_idx 必须与 target logits 的行分组顺序一致」,补一条负向断言长期锁死该契约。
- 建议:在
- XGrammarBackend::create 以 RAW 重建 TokenizerInfo 的核心兼容逻辑仅有恒等夹具覆盖,fillBitmask 无任何断言 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:81- 建议:补充用例:构造 BYTE_FALLBACK(含
<0x0A>等字节 token)且add_prefix_space=true的 metadata、以及 BYTE_LEVEL metadata,经xgrammar_impl::serializeTokenizerInfo序列化后交给XGrammarBackend::create,断言重建后GetSpecialTokenIds()与重建前一致,再对确定性语法(如固定 regex)调用RtpGrammarMatcher::fillBitmask,用bitmaskAllowsToken断言特定 token id 被放行/被屏蔽;同时补「tokenizer_info_json 为空 → 返回 nullptr」与「ebnf / 未知 key_type」三条用例。
- 建议:补充用例:构造 BYTE_FALLBACK(含
- grammar_constraint_only 校验存在静默放行路径,且完全短路 golden 比对 @
rtp_llm/test/smoke/openai_comparer.py:230- 建议:对齐 fail-fast 语义:取不到约束来源时抛
SmokeException(QueryStatus.VALID_FAILED, ...),并回退读取query.generate_config上的四个 typed 字段;is_stream分支同样改为显式抛出并说明暂不支持流式 grammar 校验。建议把「取约束 + 校验 + 失败包装」抽成两个 comparer 共用的函数,消除双份语义。
- 建议:对齐 fail-fast 语义:取不到约束来源时抛
- smoke 约束校验器多处静默放行且无自身单测 @
rtp_llm/test/smoke/grammar_constraint_validator.py:236- 建议:把这些静默分支改为显式失败(抛
ValueError并说明缺失的字段),确实允许为空的场景请加注释说明理由;并为grammar_constraint_validator补一组纯 Python 单测,覆盖正向通过、regex 不匹配、json_schema 类型不符、triggered_tags 缺 begin/end、reasoning sequence 元素数非法等形态,确保校验器自身在 CI 中受保护。
- 建议:把这些静默分支改为显式失败(抛
- 并发 smoke 的结果聚合逻辑可能掩盖失败 @
rtp_llm/test/smoke/case_runner.py:196- 建议:改为显式聚合:
ret = all(r.ret for r in results),失败时汇总所有ret == False的 result 的query_status与err_msg,并累加total_count;不要用str(result)差异来推断成败。
- 建议:改为显式聚合:
- 「MTP + grammar(thinking OFF)」组合未真正进 CI,数据文件注释与实际消费配置不一致 @
rtp_llm/test/smoke/suites_h20_oss.bzl:267- 建议:新增一条 MTP + grammar + thinking OFF 的 smoke(复用现有数据文件并补齐
--sp_model_type/--sp_type/--sp_checkpoint_path与--think_mode 0),使SpecLogitsVerifyRunner的 cap 截断与掩码纠正真正被端到端执行;同时把q_r_mtp_grammar.json改名或修正其_comment,使数据文件描述与实际消费它的 suite 配置一致,避免后续读者据注释误判覆盖面。
- 建议:新增一条 MTP + grammar + thinking OFF 的 smoke(复用现有数据文件并补齐
- 新增 config_pickle_test 目标缺少运行期依赖与 exec_properties,与仓库既有先例不一致 @
rtp_llm/cpp/pybind/BUILD:11- 建议:对齐仓库先例:在
data中补//:rtp_compute_ops(必要时加//:th_transformer),并通过deps引入 torch requirement,或改为先import rtp_llm.ops复用其.so查找与sys.path装配逻辑。若该测试确为 CPU-only 设计,请在 BUILD 加注释说明并显式声明exec_properties;提交前实际执行一次该目标,确认三个用例真实通过而非因 import 失败空跑。
- 建议:对齐仓库先例:在
- 新增 PB 字段 grammar_terminate_without_stop_token 无任何序列化断言 @
rtp_llm/cpp/model_rpc/test/model_rpc_client_test.py:174- 建议:在现有
cases中为每个 grammar 字段补一个期望布尔值并断言input_pb.generate_config.grammar_terminate_without_stop_token;额外补两个用例:_reasoning_grammar_terminate_without_stop_token为 True 时字段被置位、无任何 grammar 约束时字段为 False,确保该跨语言派生逻辑有回归保护。
- 建议:在现有
- tokenizer 家族探测函数在单测中被整体 mock,决定「模型能否启动」的分支零覆盖 @
rtp_llm/config/test/grammar_tokenizer_info_test.py:73- 建议:为三个家族探测函数补直测:用轻量 fake 对象覆盖「tiktoken 内层 Encoding」「vocab_files_names 含 tiktoken」「mergeable_ranks 存在」「sp_model / tokenizer.sp_model / tok 三个候选位置」「候选缺少 PieceToId 等 API」以及「三者均不匹配」六类形态,断言返回值;
build_tokenizer_infoharness 可保留 mock,但至少新增一条不 mock 探测函数的端到端用例。
- 建议:为三个家族探测函数补直测:用轻量 fake 对象覆盖「tiktoken 内层 Encoding」「vocab_files_names 含 tiktoken」「mergeable_ranks 存在」「sp_model / tokenizer.sp_model / tok 三个候选位置」「候选缺少 PieceToId 等 API」以及「三者均不匹配」六类形态,断言返回值;
- test_grammar_defaults 退化为恒真断言,且 env 通道完全无覆盖 @
rtp_llm/server/server_args/test/server_args_test.py:423- 建议:把
test_grammar_defaults改为断言硬编码期望值(num_workers == 8、constrained_json_disable_any_whitespace is False、compiler_cache_bytes == 512 * 1024 * 1024),使 C++ 默认值变更能被显式发现;并新增一条 env 用例(设置三个env_name且sys.argv = ["prog"]触发 env 分支),断言取值与类型正确,使「绑定缺失」这一失效模式真正可被捕获。
- 建议:把
P3
- BUILD 元数据与实际内容不符:dlpack 版本注释错误、nanobind exclude 永不生效 @
3rdparty/dlpack/BUILD:6- 建议:修正 dlpack 注释为真实版本与来源(上游 tag/commit + 抓取日期),并确认 v1.2 确为所 pin xgrammar commit 要求的版本;建议把
dlpack_headers的可见性收紧到3rdparty/xgrammar与 grammar 相关包,避免 include 路径无必要地扩散到全仓 torch 编译单元。同时删除xgrammar.BUILD中无效的exclude(或改为cpp/**/*.cc配合有效 exclude),并把关于cpp/testing.cc的说明移出 srcs。
- 建议:修正 dlpack 注释为真实版本与来源(上游 tag/commit + 抓取日期),并确认 v1.2 确为所 pin xgrammar commit 要求的版本;建议把
- MTP 解码下每步都会做一次不会被消费的全词表 bitmask 重建 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:656- 建议:让
GrammarLogitsProcessor记录当前是否处于 speculative 模式(例如在首次prepareSpeculative时置位,或由构造参数传入),该模式下跳过refreshAfterCommit;或把 decode mask 改为惰性生成(process()首次需要时才构建)。补一条单测断言 MTP 路径下不会触发 decode mask 重建。
- 建议:让
- SpecLogitsVerifyRunner::run 的失败语义不一致,形状校验失败会逃出 try 块 @
rtp_llm/cpp/models/logits_processor/SpecLogitsVerifyRunner.cc:275- 建议:统一失败语义:把可归因于单请求/单批的校验(draft 列数、
propose_step、vocab_size上限)改为返回makeFailureResultForActiveStreams(...)的请求级错误,只对真正的进程级编程不变量保留RTP_LLM_CHECK;或补一个catch (const std::exception&)兜底,并在注释中写明两类校验的分界。同时为「非法 draft 列数」补一条断言错误落到 active stream 而非抛出的用例。
- 建议:统一失败语义:把可归因于单请求/单批的校验(draft 列数、
- CudaMaskLogitsOpTest 保留 MYDEBUG 调试打印且用浮点 == 直接断言 @
rtp_llm/models_py/bindings/cuda/ops/tests/CudaMaskLogitsOpTest.cc:48- 建议:删除
MYDEBUG_*打印(或改为仅失败时输出),并把断言改为EXPECT_FLOAT_EQ(哨兵位置可保留精确比较)配合<< "idx=" << i输出上下文,便于定位。
- 建议:删除
- 接口签名改造混入无关 clang-format 重排,抬高评审与回溯成本 @
rtp_llm/cpp/model_rpc/QueryConverter.cc:130- 建议:后续同类改造建议把格式化拆成独立的 format-only commit(PR 内单独一个 commit 即可),使逻辑 commit 的 diff 只包含行为变更;本 PR 可在 description 中显式列出「纯格式化文件清单」,帮助评审者与后续回溯者跳过噪声。
Checklist Violations (20 fail / 58 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
核心依赖 xgrammar 指向个人账号 fork,构建不可复现且无镜像与回滚手段
xgrammar_deps()用new_git_repository指向个人账号 fork 的某个 commit,注释说明这是「带 structural-tag max_tokens 支持」的未上游化分支,且init_submodules = False、无sha256/shallow_since等完整性锚点。整个 grammar 特性硬依赖它:grammar_xgrammar、xgrammar_tokenizer_info及 5 个新增 cc_test 全部经@xgrammar//:xgrammar传导,WORKSPACE无条件加载。init_submodules = False与xgrammar.BUILD:22对3rdparty/picojson/picojson.h的 glob 还隐式依赖该 fork 把 picojson 内联进树。fork 被删除、改名、转私有或强推该 commit,WORKSPACE解析立即失败且无镜像可切;xgrammar.BUILD:1写的「v0.2.2」与实 - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
BUILD 元数据与实际内容不符:dlpack 版本注释错误、nanobind exclude 永不生效
3rdparty/dlpack/BUILD:6注释写「We vendor the v0.8 header verbatim instead.」,但同 PR 落盘的3rdparty/dlpack/include/dlpack/dlpack.h:19,22为DLPACK_MAJOR_VERSION 1/DLPACK_MINOR_VERSION 2,实为 v1.2。同时dlpack_headers处于default_visibility = public且includes = ["include"],该搜索路径会沿xgrammar_headers → grammar_xgrammar → logits_processor传播到大量同时链接 libtorch 的编译单元(torch 自带一份 dlpack 头)。另外3rdparty/xgrammar/xgrammar.BUILD:38-41对glob(["cpp/*.cc", "cpp/support/*.cc"])设置 `exclude = ["cpp/nanobin - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
grammar_terminate_without_stop_token 在 Python 派生与 HTTP 外部可写两侧语义分裂
C++ 侧JSONIZE(grammar_terminate_without_stop_token)(:236)使该字段成为 raw HTTP 请求体可直接置位的外部字段,新增用例InferenceDataTypeTest.cc:65-72正是断言这一点,且未设置任何 grammar 约束时置 true 无效果、无校验、无文档。而 OpenAI/Python 路径下该值是服务端派生量:response_format_builder.py:176-179=json_schema is not None or config._reasoning_grammar_terminate_without_stop_token。后者是 pydantic PrivateAttr、不参与model_dump();think 模式下_wrap_final_format_with_reasoning_envelope(:312-318)先写该私有标志再清空json_schema,因此任何链路把GenerateConfig经 dict 往返重建后标志静默变 - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
MTP 解码下每步都会做一次不会被消费的全词表 bitmask 重建
acceptCommittedLocked在每次提交成功后无条件调用decode_mask_builder_->refreshAfterCommit(*matcher_, committed_output_len_)(:656),其内部buildState→prepareBitmask会重填整个词表的缓存 bitmask 并写入 pinned buffer。该缓存只在process()→applyDeviceMaskState路径被消费,而 MTP 路径从不设置logits_processor_states_ptr(全仓仅NormalSamplerInputGatherer.cc:221写入),掩码全部由prepareSpeculative+applyMaskToLogits独立生成。因此在 MTP 部署下,每个 commit 都为一个永不被读取的 bitmask 做一次vocab/32字的填充,waitForPendingBitmaskUploads()的事件同步也被无谓触发。 - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
核心依赖 xgrammar 指向个人账号 fork,构建不可复现且无镜像与回滚手段
xgrammar_deps()用new_git_repository指向个人账号 fork 的某个 commit,注释说明这是「带 structural-tag max_tokens 支持」的未上游化分支,且init_submodules = False、无sha256/shallow_since等完整性锚点。整个 grammar 特性硬依赖它:grammar_xgrammar、xgrammar_tokenizer_info及 5 个新增 cc_test 全部经@xgrammar//:xgrammar传导,WORKSPACE无条件加载。init_submodules = False与xgrammar.BUILD:22对3rdparty/picojson/picojson.h的 glob 还隐式依赖该 fork 把 picojson 内联进树。fork 被删除、改名、转私有或强推该 commit,WORKSPACE解析立即失败且无镜像可切;xgrammar.BUILD:1写的「v0.2.2」与实 - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
SpecLogitsVerifyRunner::run 的失败语义不一致,形状校验失败会逃出 try 块
run()只捕获std::bad_alloc与c10::Error并转成makeFailureResultForActiveStreams(:275-281),即「把 active stream 全部标错、引擎继续运行」。但同一 try 块内的RTP_LLM_CHECK_WITH_INFO(:248-255,以及materializeDraftTokensToCpu的 draft 列数校验)走的是rtp_llm::myAssert,在开启 core dump 时直接abort()、否则抛RTP_EXCEPTION——两者都不会被上述两个 catch 命中,会逃出run()传播到MtpExecutor::decodeStep。同一函数内对「由运行期张量形状决定的异常」与「资源类异常」采用了两套互斥策略,运维上表现为整机重启而非单请求失败,且没有注释说明哪些校验是有意 fail-fast。 - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
SpecLogitsVerifyRunner::run 的失败语义不一致,形状校验失败会逃出 try 块
run()只捕获std::bad_alloc与c10::Error并转成makeFailureResultForActiveStreams(:275-281),即「把 active stream 全部标错、引擎继续运行」。但同一 try 块内的RTP_LLM_CHECK_WITH_INFO(:248-255,以及materializeDraftTokensToCpu的 draft 列数校验)走的是rtp_llm::myAssert,在开启 core dump 时直接abort()、否则抛RTP_EXCEPTION——两者都不会被上述两个 catch 命中,会逃出run()传播到MtpExecutor::decodeStep。同一函数内对「由运行期张量形状决定的异常」与「资源类异常」采用了两套互斥策略,运维上表现为整机重启而非单请求失败,且没有注释说明哪些校验是有意 fail-fast。 - [6.1] Quality — Commit 原子、message 与行为匹配 → issue
接口签名改造混入无关 clang-format 重排,抬高评审与回溯成本
逐行核对 diff 可见同一 hunk 内混合了语义变更与纯格式化:transGenerateConfig中删除TRANS_OPTIONAL(response_format)、新增 field 74 读取属真实行为变更,而同 hunk 里TRANS_OPTIONAL(task_id)的位置调整、in_think_mode/max_thinking_tokens的等号对齐重排、combo_token_size/enable_cross_sequence_ban等赋值的对齐重排均为机械产物。同类混合还出现在case_runner.py顶部 import 的整体重排与NormalOutputDispatcher.cc的 include 顺序调整。多为仓库 pre-commit(black/isort/clang-format)钩子结果、作者可控性有限,故按 P3 记录,但确实使「哪一行改变了行为」难以在 review 与后续 bisect 中辨识。 - [6.1] Quality — Mega-PR 已拆分为独立变更 → issue
ThinkModeLogitsProcessor 装配点移除后沦为死代码,think 预算在直连路径静默失效
工厂第一步原为创建ThinkModeLogitsProcessor,现改为注释「Thinking-only configs are not supported here; do not add a legacy processor fallback」(:98-99)并整段删除。全仓确认生产代码已无任何构造点(仅其自身fromGenerateInput与测试),但.h/.cc仍在models/BUILD:55/68编译、logits_processor/test/BUILD:24仍在跑其测试、docs/backend/logits_processor.md:8与dash_sc/think.py:167仍把它描述为在用行为。更关键的是in_think_mode/max_thinking_tokens仍被QueryConverter.cc:107-108解析、被GenerateConfig.h:302JSONIZE,但已无任何消费者:不经ResponseFormatBuilder的直连客户端设置思考预算 - [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue
CudaMaskLogitsOpTest 保留 MYDEBUG 调试打印且用浮点 == 直接断言
该用例在本 PR 中随掩码哨兵值变更被修改(期望向量由-INFINITY改为-FLT_MAX),但仍保留三组printBufferData(logits, "MYDEBUG_BEFORE_MASK_LOGITS")/"MYDEBUG_AFTER_MASK_LOGITS"调试输出(:48/50、78/80、108/110,均为既有行),会把整个 logits 缓冲打进 CI 日志;同时 :64 用ASSERT_TRUE(logits_ptr[i] == expect_vec[i])做浮点精确相等断言,失败时不会打印下标与实际值,定位成本高。 - [6.1] Quality — 逻辑变更未混入无关格式化 → issue
接口签名改造混入无关 clang-format 重排,抬高评审与回溯成本
逐行核对 diff 可见同一 hunk 内混合了语义变更与纯格式化:transGenerateConfig中删除TRANS_OPTIONAL(response_format)、新增 field 74 读取属真实行为变更,而同 hunk 里TRANS_OPTIONAL(task_id)的位置调整、in_think_mode/max_thinking_tokens的等号对齐重排、combo_token_size/enable_cross_sequence_ban等赋值的对齐重排均为机械产物。同类混合还出现在case_runner.py顶部 import 的整体重排与NormalOutputDispatcher.cc的 include 顺序调整。多为仓库 pre-commit(black/isort/clang-format)钩子结果、作者可控性有限,故按 P3 记录,但确实使「哪一行改变了行为」难以在 review 与后续 bisect 中辨识。 - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
GrammarConfig 保留无消费者的 tokenizer_info_json / override_stop_tokens,与 ModelConfig 同名字段混淆
生产路径的真源是本 PR 新增的ModelConfig::tokenizer_info_json:base_model.py:324写入,LogitsProcessorFactory.cc:79通过XGrammarBackend::create(model_config.tokenizer_info_json, grammar_config)消费;optionsFromConfig只读constrained_json_disable_any_whitespace/num_workers/compiler_cache_bytes。全仓搜索确认GrammarConfig::tokenizer_info_json与override_stop_tokens既无 CLI/env 写入方也无任何读取点,只出现在 pybinddef_readwrite(ConfigInit.cc:1326/1328)、pickle 元组、ConfigModules.cc:393的to_string()、.pyi:548, - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
MTP 解码下每步都会做一次不会被消费的全词表 bitmask 重建
acceptCommittedLocked在每次提交成功后无条件调用decode_mask_builder_->refreshAfterCommit(*matcher_, committed_output_len_)(:656),其内部buildState→prepareBitmask会重填整个词表的缓存 bitmask 并写入 pinned buffer。该缓存只在process()→applyDeviceMaskState路径被消费,而 MTP 路径从不设置logits_processor_states_ptr(全仓仅NormalSamplerInputGatherer.cc:221写入),掩码全部由prepareSpeculative+applyMaskToLogits独立生成。因此在 MTP 部署下,每个 commit 都为一个永不被读取的 bitmask 做一次vocab/32字的填充,waitForPendingBitmaskUploads()的事件同步也被无谓触发。 - [6.1] Software Engineering — SRP:模块/类职责单一 → issue
RPC 边界由 validate() 降级为 validate_finalized(),数值/类型兜底校验消失
原input_py.generate_config.validate()被替换为ResponseFormatBuilder.validate_finalized()。后者只检查json_format/response_format是否已收敛、grammar 约束唯一且为 str(response_format_builder.py:111-130);而validate()(generate_config.py:587起)还覆盖 top_k/top_p/max_new_tokens/num_beams/stop_words_list 的类型与区间、banned_combo_token_ids每项长度须等于combo_token_size、prompt_logits 区间等。各入口虽都会 finalize(add_thinking_params内部 :550 调validate()),但 finalize 之后仍有写操作:pipeline.py随后执行add_stop_ids_from_str追加 `sto - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
「MTP + grammar(thinking OFF)」组合未真正进 CI,数据文件注释与实际消费配置不一致
新增 suitesmoke_h20_grammar_heavy已挂入maga_model_smoke,但两条用例的覆盖面与数据文件描述不符:qwen35_grammar_concurrent_no_mtp的smoke_args完全没有--sp_model_type/--sp_type,即不开 MTP,却消费名为q_r_mtp_grammar.json的数据文件,且该文件_comment自述为「MTP + grammar (no reasoning)…xgrammar must be active from the first output token because thinking is disabled」。另一条qwen35_grammar_pd_mtp_reasoning开 MTP 但--think_mode 1,语法在</think>之前处于 passthrough。结果是本 PR 风险最高的组合——「MTP 投机验证 + 首 token 即生效的语法掩码」——在 CI 中没有任何端到端覆盖 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
CudaMaskLogitsOpTest 保留 MYDEBUG 调试打印且用浮点 == 直接断言
该用例在本 PR 中随掩码哨兵值变更被修改(期望向量由-INFINITY改为-FLT_MAX),但仍保留三组printBufferData(logits, "MYDEBUG_BEFORE_MASK_LOGITS")/"MYDEBUG_AFTER_MASK_LOGITS"调试输出(:48/50、78/80、108/110,均为既有行),会把整个 logits 缓冲打进 CI 日志;同时 :64 用ASSERT_TRUE(logits_ptr[i] == expect_vec[i])做浮点精确相等断言,失败时不会打印下标与实际值,定位成本高。 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
新增 PB 字段 grammar_terminate_without_stop_token 无任何序列化断言
model_rpc_client.py:150-152新增写入generate_config_pb.grammar_terminate_without_stop_token = input_py.generate_config.grammar_terminate_without_stop_token(),其取值由response_format_builder.py:176-179派生,C++ 侧在LogitsProcessorFactory.cc:113-114与grammar_key.key_type == "json"取或后决定 matcher 是否在语法终结时提前结束。但新测试test_trans_input_writes_typed_grammar_fields_consistently的 4 个 case 只断言四个 StringValue 字段互斥与response_format未置位(:200-205),完全没有断言 field 74,也未覆盖「json_schema 场景应为 True、regex/ebnf/struc
RTP-LLM Checklist
- [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue
GrammarConfig 保留无消费者的 tokenizer_info_json / override_stop_tokens,与 ModelConfig 同名字段混淆
生产路径的真源是本 PR 新增的ModelConfig::tokenizer_info_json:base_model.py:324写入,LogitsProcessorFactory.cc:79通过XGrammarBackend::create(model_config.tokenizer_info_json, grammar_config)消费;optionsFromConfig只读constrained_json_disable_any_whitespace/num_workers/compiler_cache_bytes。全仓搜索确认GrammarConfig::tokenizer_info_json与override_stop_tokens既无 CLI/env 写入方也无任何读取点,只出现在 pybinddef_readwrite(ConfigInit.cc:1326/1328)、pickle 元组、ConfigModules.cc:393的to_string()、.pyi:548, - [I] 代码质量 — 同一功能用统一工具函数 → issue
BUILD 元数据与实际内容不符:dlpack 版本注释错误、nanobind exclude 永不生效
3rdparty/dlpack/BUILD:6注释写「We vendor the v0.8 header verbatim instead.」,但同 PR 落盘的3rdparty/dlpack/include/dlpack/dlpack.h:19,22为DLPACK_MAJOR_VERSION 1/DLPACK_MINOR_VERSION 2,实为 v1.2。同时dlpack_headers处于default_visibility = public且includes = ["include"],该搜索路径会沿xgrammar_headers → grammar_xgrammar → logits_processor传播到大量同时链接 libtorch 的编译单元(torch 自带一份 dlpack 头)。另外3rdparty/xgrammar/xgrammar.BUILD:38-41对glob(["cpp/*.cc", "cpp/support/*.cc"])设置 `exclude = ["cpp/nanobin
Python Static-First Checklist
- [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
test_grammar_defaults 退化为恒真断言,且 env 通道完全无覆盖
参数默认值本次由硬编码常量改为从同一结构体读取(grammar_group_args.py:11/19/33的default=grammar_config.<field>)。测试随后写expected = type(g)()并逐字段比对(:428-435),即拿「由 C++ 默认值派生出的 argparse 默认值」与「新建 C++ 默认值」互比,必然相等;若某个bind_to被整体漏掉,该字段仍保持 C++ 默认值,断言依旧通过,而 docstring 声称这是 "Regression guard for the wiring"。setUp已os.environ.clear()却未构造任何 env 场景,全仓搜索确认CONSTRAINED_JSON_DISABLE_ANY_WHITESPACE/GRAMMAR_NUM_WORKERS/GRAMMAR_COMPILER_CACHE_BYTES三个 env 通道完全无覆盖。
Strengths
- 错误传播链完整可用:
ErrorCode新增 grammar 专用码,GRAMMAR_BITMASK_BUFFER_TOO_SMALL/GRAMMAR_VOCAB_EXCEEDS_MODEL_VOCAB/GRAMMAR_EOS_OUT_OF_VOCAB均有位级单测;compileWithErrorClassification把bad_alloc归为ResourceExhausted、runtime_error归为InvalidArgument,区分了用户可见拒绝与系统性可重试故障。 - 提交状态被当作一个原子单元:
acceptCommittedLocked把 matcher token 数、committed_output_len_与缓存掩码整体提交,任一 token 失败即rollback_commit统一回滚,二次失败时markFinished()兜底;ProvisionalSpecAcceptTracker保证prepareSpeculative只读不改已提交状态。 - MTP 投机验证的行语义自洽:
initializeCompactRows的row_indices = stream_idx * (P+1) + offset与gatherSpecSamplerInput的 target logits 行布局一致,fillSpecVerifyRow的第offset行严格对应「已接受 draft[0, offset) 之后」的 matcher 状态。 - 语法终止后保持
TERMINATED而非FINISHED有注释论证(GrammarLogitsProcessor.cc:625-628):min_new_tokens/ignore_eos延后完成时仍只放行 EOS,避免了「语法完成即恢复无约束生成」这一典型缺陷。 - pinned host buffer 的异步 H2D 生命周期被显式管理:
waitForPendingHostUploads()与DecodeMaskBuilder::waitForPendingBitmaskUploads()都在改写 host buffer 前同步事件,注释明确写出「正确性不得依赖后续 sampler 的 D2H 同步」。 - 采样行口径被统一修正:
setLogitsProcessorInputs改用samplerRows(stream)并新增idx == sampler_inputs.batch_size断言;gatherSpecSamplerInput补上finished_mask.zero_(),修掉 MTP 路径读未初始化内存的隐患。 - 约束语义在入口收敛干净:
GrammarConstraint用frozen dataclass+Literal固化字段名,collect_from_config+_validate_grammar_constraints把「四字段最多设一个」收敛为单点判定;validate_finalized把「引擎收到未归一化配置」从静默降级变成 fail-fast。 normalize_think_tag替换了散落三处的encode("utf-8").decode("unicode_escape"),顺带修掉该写法对非 ASCII think tag 产生 mojibake 的既有缺陷,属真实缺陷修复。- 测试贴近真实故障模式:
GrammarLogitsProcessorTest做逐 token 位级断言(undersized bitmask、grammar vocab > model vocab、terminated 后强制 EOS、整批 commit 回滚、provisional accept 回滚幂等);XGrammarBackendTest用static_assert把 matcher/backend 的不可移动契约固化在编译期;packed_mask_logits_cpu_fallback_test覆盖紧凑行映射、越界行、非连续输入与三种 dtype,且不依赖 CUDA。 - 跨语言兼容处理到位:proto field 73 保留并标
[deprecated = true]、字段号未复用,新增 field 74 独立占位;GrammarConfig::__setstate__用py::isinstance<py::str>(t[0])区分 legacy 五元组,config_pickle_test.py覆盖当前布局往返、legacy 加载与伪造短布局被拒三条路径。
| model_vocab_size=int(self.model_config.vocab_size or 0), | ||
| stop_token_ids=self._collect_tokenizer_info_stop_token_ids(), | ||
| ) | ||
| except Exception as e: |
There was a problem hiding this comment.
[P1] 启动期强制构建 grammar tokenizer 元数据,不受支持的 tokenizer 直接启动失败且无逃生开关
_fill_grammar_tokenizer_info 在 load_tokenizer() 中无条件执行(:301),与请求是否使用 grammar 无关,注释自述为「startup-time compatibility contract」;任何异常一律升级为 RuntimeError 终止启动(:329-332)。build_grammar_tokenizer_info_json 在 tokenizer 既非 PreTrainedTokenizerFast、也非 tiktoken、也非 sentencepiece 时 raise ValueError("Unsupported tokenizer type"),stop_token_ids 为空时同样抛错(grammar_tokenizer_info.py:155-156),而 _collect_tokenizer_info_stop_token_ids 会丢弃负 id。结果是自定义 tokenizer 包装类、或未配置 stop token 的存量部署升级后完全无法启动。C++ ...
建议: 复用 C++ 侧已有的降级语义:新增显式开关(如 --grammar_disable / 恢复 --grammar_backend=none),开启时捕获异常仅 logging.warning 并保持 tokenizer_info_json 为空,让携带约束的请求由 LogitsProcessorFactory.cc:107 的现有分支在请求级失败,而非阻塞引擎启动。这样既保留 fail-fast 默认行为,又给运维留下回滚手段。同时补一条「不受支持 tokenizer 仍可正常启动、非 grammar 请求可用」的单测;若坚持默认 fail-fast,请在发布说明中列出受影响的 tokenizer 类型与 stop_token_ids 为空的模型清单。
| auto result = makeResult(shape); | ||
| result.processor_errors = std::move(merge_result.processor_errors); | ||
| return result; | ||
| } catch (const std::bad_alloc& e) { |
There was a problem hiding this comment.
[P3] SpecLogitsVerifyRunner::run 的失败语义不一致,形状校验失败会逃出 try 块
run() 只捕获 std::bad_alloc 与 c10::Error 并转成 makeFailureResultForActiveStreams(:275-281),即「把 active stream 全部标错、引擎继续运行」。但同一 try 块内的 RTP_LLM_CHECK_WITH_INFO(:248-255,以及 materializeDraftTokensToCpu 的 draft 列数校验)走的是 rtp_llm::myAssert,在开启 core dump 时直接 abort()、否则抛 RTP_EXCEPTION——两者都不会被上述两个 catch 命中,会逃出 run() 传播到 MtpExecutor::decodeStep。同一函数内对「由运行期张量形状决定的异常」与「资源类异常」采用了两套互斥策略,运维上表现为整机重启而非单请求失败,且没有注释说明哪些校验是有意 fail-fast。
建议: 统一失败语义:把可归因于单请求/单批的校验(draft 列数、propose_step、vocab_size 上限)改为返回 makeFailureResultForActiveStreams(...) 的请求级错误,只对真正的进程级编程不变量保留 RTP_LLM_CHECK;或补一个 catch (const std::exception&) 兜底,并在注释中写明两类校验的分界。同时为「非法 draft 列数」补一条断言错误落到 active stream 而非抛出的用例。
Checklist: [6.1] 状态不变量:创建/更新/失败/重试/回滚路径有效;[6.1] 错误语义:fail-fast/retry/fallback/silent 行为显式
| printBufferData(logits, "MYDEBUG_AFTER_MASK_LOGITS"); | ||
|
|
||
| auto logits_cpu = logits.cpu().contiguous(); | ||
| auto logits_ptr = logits_cpu.data_ptr<float>(); |
There was a problem hiding this comment.
📍 实际位置 rtp_llm/models_py/bindings/cuda/ops/tests/CudaMaskLogitsOpTest.cc:48(不在 diff 展示范围内,就近挂载)
[P3] CudaMaskLogitsOpTest 保留 MYDEBUG 调试打印且用浮点 == 直接断言
该用例在本 PR 中随掩码哨兵值变更被修改(期望向量由 -INFINITY 改为 -FLT_MAX),但仍保留三组 printBufferData(logits, "MYDEBUG_BEFORE_MASK_LOGITS") / "MYDEBUG_AFTER_MASK_LOGITS" 调试输出(:48/50、78/80、108/110,均为既有行),会把整个 logits 缓冲打进 CI 日志;同时 :64 用 ASSERT_TRUE(logits_ptr[i] == expect_vec[i]) 做浮点精确相等断言,失败时不会打印下标与实际值,定位成本高。
建议: 删除 MYDEBUG_* 打印(或改为仅失败时输出),并把断言改为 EXPECT_FLOAT_EQ(哨兵位置可保留精确比较)配合 << "idx=" << i 输出上下文,便于定位。
Checklist: [6.1] 无 per-forward 调试日志 / 噪声热路径输出;[6.1] 新逻辑有聚焦单测 + 相关集成/smoke 测试
| // 生成式推荐:组合 token 约束 | ||
| generate_config->combo_token_size = config_proto->combo_token_size(); | ||
| generate_config->enable_cross_sequence_ban = config_proto->enable_cross_sequence_ban(); | ||
| generate_config->combo_token_size = config_proto->combo_token_size(); |
There was a problem hiding this comment.
[P3] 接口签名改造混入无关 clang-format 重排,抬高评审与回溯成本
逐行核对 diff 可见同一 hunk 内混合了语义变更与纯格式化:transGenerateConfig 中删除 TRANS_OPTIONAL(response_format)、新增 field 74 读取属真实行为变更,而同 hunk 里 TRANS_OPTIONAL(task_id) 的位置调整、in_think_mode/max_thinking_tokens 的等号对齐重排、combo_token_size / enable_cross_sequence_ban 等赋值的对齐重排均为机械产物。同类混合还出现在 case_runner.py 顶部 import 的整体重排与 NormalOutputDispatcher.cc 的 include 顺序调整。多为仓库 pre-commit(black/isort/clang-format)钩子结果、作者可控性有限,故按 P3 记录,但确实使「哪一行改变了行为」难以在 review 与后续 bisect 中辨识。
建议: 后续同类改造建议把格式化拆成独立的 format-only commit(PR 内单独一个 commit 即可),使逻辑 commit 的 diff 只包含行为变更;本 PR 可在 description 中显式列出「纯格式化文件清单」,帮助评审者与后续回溯者跳过噪声。
Checklist: [6.1] Commit 原子、message 与行为匹配;[6.1] 逻辑变更未混入无关格式化
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1006
Status: BLOCKING
Summary: P0/0 · P1/5 · P2/23 · P3/8
Reviewed: commit f6b0be2ba478 · 2026-08-04 16:03 UTC+8
Blocking Issues
P1
- think 模式被无条件转为 grammar 约束解码,think + beam / n>1 由可用变为硬失败且强依赖 backend @
rtp_llm/config/response_format_builder.py:80- 建议:把 envelope 从「think 模式必开」降级为「按需开」:仅当存在用户 grammar 约束、或确实需要用 grammar 强制
max_thinking_tokens边界时才包裹;n>1/ beam 场景退化为不加 envelope 并保留原 think 行为,而不是拒绝请求。同时为「backend 不可用 + thinking 请求」补显式降级路径(跳过 envelope 并打点告警),避免单点初始化失败放大为推理模型全量不可用。该不兼容点需在 PR description 与发布说明中列出,并补齐 beam /n>1+ think 的用例。
- 建议:把 envelope 从「think 模式必开」降级为「按需开」:仅当存在用户 grammar 约束、或确实需要用 grammar 强制
- --grammar_backend / GRAMMAR_BACKEND 被直接移除,存量启动脚本升级即退出且失去唯一关闭手段 @
rtp_llm/server/server_args/grammar_group_args.py:4- 建议:保留
--grammar_backend(env 同名)一个过渡版本:取值xgrammar时打一次 deprecation 告警并忽略,取值none时映射为「清空model_config.tokenizer_info_json/ 不初始化 backend」;或新增等价显式开关(如--disable_grammar)。无论哪种方案都必须保留一个「关闭结构化输出」的运维回滚手段,并在 PR description 与发布说明中给出迁移说明;同时在ServerArgsGrammarConfigTest补一条用例钉住所选策略(接受并忽略,或SystemExit且错误信息含迁移提示)。
- 建议:保留
- 启动期强制构建 grammar tokenizer 元数据,不受支持的 tokenizer 直接阻塞模型加载且无逃生开关 @
rtp_llm/models/base_model.py:329- 建议:区分「grammar 能力可用性」与「模型可服务性」:对
Unsupported tokenizer type/ 空 stop_token_ids 降级为 ERROR 日志 + 指标并把tokenizer_info_json留空,让 C++ 已有的 "constraint backend is disabled" 分支在真正收到 grammar 请求时报错。若坚持 fail-fast,请补一个默认宽松的显式开关(如--grammar_require_tokenizer_info)作为回滚手段,并统一real_tokenizer is None的处理策略,同时在发布说明中列出受影响的 tokenizer 类型。
- 建议:区分「grammar 能力可用性」与「模型可服务性」:对
- MTP 下非 grammar logits processor 变为请求级硬失败,且 tree_decode_config 启动期抛错,均无开关与迁移路径 @
rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:850- 建议:先确认线上是否存在 MTP + 生成式推荐(
combo_token_size > 0)、MTP + 多序列返回或 MTP + tree decode 流量。若存在,为硬拒绝加可运维开关(如SpeculativeExecutionConfig的 strict 标志,默认拒绝、异常时可回退),或为RecommendationLogitsProcessor提供SPEC_VERIFY实现;若确认无此类流量,请在 PR description 与发布说明中显式列出这两项不兼容变更与规避方式。同时把触发的 processor 名称与配置字段名带入 reason(当前默认文案 "processor supports normal decoding only" 无法定位)。
- 建议:先确认线上是否存在 MTP + 生成式推荐(
- 核心依赖 xgrammar 指向个人账号 fork,构建不可复现且无校验、镜像与回滚手段 @
3rdparty/xgrammar/repositories.bzl:7- 建议:改为可校验、可托管的形式:优先推动
max_tokens能力上游合并后 pin 上游 tag;短期至少改用带sha256的http_archive(指向组织内镜像或归档 tarball)或纳入统一依赖管理,并把 fork 迁到团队组织账号下,在repositories.bzl中补上游 PR/issue 链接与升级流程。同时把xgrammar.BUILD:1的 v0.2.2 注释改为实际 fork commit 与基线版本,避免版本声明与实际内容脱节。
- 建议:改为可校验、可托管的形式:优先推动
Non-blocking Suggestions
P2
- legacy structures/triggers 形态的 structural_tag 在 think 模式下必然报错且错误文案误导 @
rtp_llm/config/grammar_constraint.py:143- 建议:在归一化阶段(
_resolve_grammar_constraint)就把 legacystructures/triggers统一转换为format形状,使下游只处理单一规范形态;若确实无法转换,则在_structural_tag_format_node中为该形状构造等价 sequence 元素,并把错误信息拆分为「缺 format」与「legacy 形状不支持 reasoning 包裹」两条。补一条 think 模式 + legacy structural_tag 的用例锁定行为。
- 建议:在归一化阶段(
- proto field 73 被引擎单向停用后旧客户端约束静默丢弃,且转换层缺少 field 73/74 测试锚点 @
rtp_llm/cpp/model_rpc/QueryConverter.cc:100- 建议:在
transGenerateConfig中显式检测「field 73 有值且 69-72 全空」并返回INVALID_PARAMS(提示改用 typed 字段或升级 frontend),至少要INTERVAL_LOG告警并上报指标;C++RawRequest的 Jsonize 侧做一致处理,不要让 HTTP 直连客户端静默降级。在QueryConverterTest.cc补 field 73/74 的测试锚点,并在发布说明中固定「先升级 engine 再升级 frontend」的顺序约束。
- 建议:在
- grammar 编译错误在 factory 边界被压平为 INVALID_PARAMS,丢失可重试语义 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:124- 建议:在 factory 层按
absl::StatusCode映射对应ErrorCode(kInvalidArgument → INVALID_PARAMS,kResourceExhausted → MALLOC_FAILED或既有可重试码,kUnknown → EXECUTION_EXCEPTION),并补一条用例断言系统类失败不会被降级为INVALID_PARAMS。
- 建议:在 factory 层按
- backend 初始化四类失败统一 fail-soft,服务端配置故障以用户参数错误返回且无可观测信号 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:73- 建议:按失败类别统一策略:
tokenizer_info_json为空视为「结构化输出未开启」,保持静默禁用;反序列化失败 / vocab 为空 / 构造异常属配置或数据损坏,应与 Python 侧一致地启动期 fail-fast,或至少暴露一个 gauge 并纳入健康检查。运行期错误码改用服务端错误语义,与真正的用户输入错误区分开。
- 建议:按失败类别统一策略:
- SpecLogitsVerifyRunner::run 的形状校验异常逃出 catch,失败爆炸半径与设计不一致 @
rtp_llm/cpp/models/logits_processor/SpecLogitsVerifyRunner.cc:275- 建议:统一失败语义:把 draft 张量形状 / 定义性校验从
RTP_LLM_CHECK_WITH_INFO改为返回ErrorInfo并走makeFailureResultForActiveStreams,或在run()追加catch (const std::exception&)兜底(prepareSpeculative是虚扩展点,第三方实现抛出的非c10::Error异常同样会逃逸)。若确认这些断言属「引擎内部不变量、违反即不可继续」,请在注释中写明为何选择批级失败,并补一条「非法 draft 形状只影响活跃流、其余流 cap 保持 propose_step」的测试。
- 建议:统一失败语义:把 draft 张量形状 / 定义性校验从
- create() 以 RAW 重建 TokenizerInfo 时丢弃 special_token_ids,且无跨语言边界一致性用例 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:81- 建议:若 fork 版本支持,直接传入序列化得到的
special_token_ids;否则补一条用例:构造含<|im_end|>等特殊 token 与 BYTE_LEVEL/BYTE_FALLBACK 词表的 fixture,走完「serializeTokenizerInfo → create → 读 backend 内部 tokenizer_info」链路并断言GetSpecialTokenIds()/GetDecodedVocab()与序列化前一致。若确实无法保留,在注释中记录该差异及其对 mask 的影响范围。
- 建议:若 fork 版本支持,直接传入序列化得到的
- ThinkModeLogitsProcessor 装配点移除后沦为死代码,think 预算在直连路径静默失效 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:99- 建议:明确二选一并落到代码:若确认废弃,同批删除
ThinkModeLogitsProcessor.{h,cc}、BUILD 条目与单测,并同步更新docs/backend/logits_processor.md与think.py注释;若保留为回退,则补上「backend 不可用且仅 think 模式」的回退分支与用例。无论哪种,都建议对config.in_think_mode && grammar_key.empty()显式 fail-fast(提示 think 约束须以 grammar 形式下发),避免直连 gRPC / HTTP 客户端静默丢失 thinking 预算。
- 建议:明确二选一并落到代码:若确认废弃,同批删除
- GrammarConfig 保留无消费者的 tokenizer_info_json / override_stop_tokens,与 ModelConfig 同名字段形成双源 @
rtp_llm/cpp/config/ConfigModules.h:377- 建议:收敛为单一真源:删除
GrammarConfig上的两个字段及其 pybind 绑定、pickle 载荷与索引、.pyi、to_string()与对应测试断言;或反过来让LogitsProcessorFactory::init只从grammar_config读取、ModelConfig不再持有。若因兼容必须双留,请在头文件与 pybind docstring 中注明哪一个是生效来源,并让to_string()只打印实际生效值。
- 建议:收敛为单一真源:删除
- grammar_terminate_without_stop_token 在 Python 派生与 C++/HTTP 两侧语义分裂且双份实现 @
rtp_llm/cpp/engine_base/stream/GenerateConfig.h:236- 建议:选定单一权威侧:或让 Python 成为唯一决策方、C++ 去掉
|| key_type == "json"兜底;或让 C++ 成为唯一决策方、Python 仅传递 reasoning envelope 标记。并在字段注释中写明适用范围「仅用于 reasoning envelope 的最终格式为 json_schema」,同时在InferenceDataTypeTest.cc补一条「只设该布尔、不带 grammar 字段」的边界用例锁定其为 no-op。
- 建议:选定单一权威侧:或让 Python 成为唯一决策方、C++ 去掉
- Python 端 grammar vocab 取 max 且空洞填空串,与 C++ 端硬拒策略不一致 @
rtp_llm/config/grammar_tokenizer_info.py:23- 建议:对齐两端契约:Python 侧在
tokenizer_vocab_size > model_vocab_size时按模型 vocab 截断(并对被截断的高位 id 记 WARNING)或在构建阶段报出可读错误,不要留到每个请求首步才失败。空洞改用 special token 形态占位(如f"<|unused_token_{i}|>")使 xgrammar 能将其排除,尾部 padding 可不填充、交给 metadata 的vocab_size处理;并补一条端到端断言:对含空洞的 vocab 编译简单 grammar,验证空洞 id 在 bitmask 中被屏蔽。
- 建议:对齐两端契约:Python 侧在
- ResponseFormatBuilder.apply 原地改写配置且异常时无回滚,dash_sc 重试作用于半成品状态 @
rtp_llm/config/response_format_builder.py:243- 建议:让
apply()具备原子性:先在局部变量中完成解析、归一化与全部校验,全部成功后再一次性写回config;或在入口对相关字段(response_format/json_format/ 四个 typed 字段 / 三个私有标志)做快照,异常时恢复后再抛出。dash_sc 侧的兜底重试应改为直接向上返回首次错误,或在明确恢复快照后再重试,并为「首次 finalize 失败后重试」补一条用例。
- 建议:让
- MultiSeqLogitsProcessor 写 EOS 掩码缺少 vocab 边界校验,存在越界写风险 @
rtp_llm/cpp/models/logits_processor/MultiSeqLogitsProcessor.cc:39- 建议:在写入前校验
eos_token_id_ >= 0 && static_cast<size_t>(eos_token_id_) < vocab_size,越界时返回ErrorInfo(可复用 grammar 路径的GRAMMAR_EOS_OUT_OF_VOCAB语义或INVALID_PARAMS),并补一条eos_token_id >= vocab_size的边界用例断言返回错误而非崩溃。
- 建议:在写入前校验
- 新增 GRAMMAR_ 错误码未指定 ExceptionCategory,用户侧原因被一律归类为 internal* @
rtp_llm/config/exceptions.py:45- 建议:为每个新码显式声明 category:模型输出违反语法(
GRAMMAR_PARSER_REJECTED_TOKEN/GRAMMAR_NON_EOS_AFTER_TERMINAL)宜用INVALID_OUTPUT;配置/词表不匹配(GRAMMAR_VOCAB_EXCEEDS_MODEL_VOCAB/GRAMMAR_EOS_OUT_OF_VOCAB/GRAMMAR_BITMASK_BUFFER_TOO_SMALL)保持INTERNAL但在注释中写明理由,并补一条断言 category 映射的用例。
- 建议:为每个新码显式声明 category:模型输出违反语法(
- 语法编译在请求路径同步执行,无规模上限与超时,耗时仅 DEBUG 可见且无指标 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:131- 建议:为编译入口补可运维手段:对
key_string长度 / 嵌套深度设可配上限并在超限时返回可定位的INVALID_PARAMS;把elapsed_ms、缓存命中率与失败分类计数接入 metrics reporter,成功路径改为「超过阈值才 WARNING」;同时评估把首次编译移出建流线程(异步预编译或带超时的等待),避免单个请求的编译时间放大为调度延迟。
- 建议:为编译入口补可运维手段:对
- grammar_constraint_only 校验存在静默放行路径,且完全短路 golden 比对 @
rtp_llm/test/smoke/openai_comparer.py:230- 建议:将两处静默
return改为 fail-fast:无法定位约束时按DashGrpcComparer抛SmokeException;stream 场景要么显式抛「grammar_constraint_only 暂不支持 stream」,要么在case_runner侧对二者同时为真提前拒绝。建议把「取约束字段并校验非空」抽到grammar_constraint_validator作为两个 comparer 的共用入口,避免策略再次分叉。
- 建议:将两处静默
- grammar smoke 的唯一断言 oracle 自身无任何单测 @
rtp_llm/test/smoke/grammar_constraint_validator.py:307- 建议:新增一个纯 CPU 的
py_test,对validate_json_schema_instance/validate_triggered_tags/validate_structural_format/validate_reasoning_sequence各补正反例(含 enum / pattern / maxLength / minItems、bare trigger、缺 format、未支持的format.type),并显式覆盖「空 schema 是否应放行」的期望语义。该测试与 smoke 无耦合,可常驻单测流水线。
- 建议:新增一个纯 CPU 的
- XGrammarBackend / RtpGrammarMatcher 的失败、边界、并发与缓存淘汰路径无测试覆盖 @
rtp_llm/cpp/engine_base/grammar/test/XGrammarBackendTest.cc:50- 建议:补充四类用例:空串 / 非法 tokenizer info JSON 断言
create()返回 nullptr;未知key_type断言状态码与消息;rollback越界断言返回错误且numAcceptedTokens()不变为负、rollback(0)为 OkStatus 且不触碰 matcher;多线程并发对同一/不同 grammar key 调用createMatcherFromKey并断言 matcher 状态互不干扰,同时用很小的compiler_cache_bytes覆盖淘汰路径。
- 建议:补充四类用例:空串 / 非法 tokenizer info JSON 断言
- tokenizer 家族探测函数与 native 序列化接缝在单测中被整体 mock @
rtp_llm/config/test/grammar_tokenizer_info_test.py:73- 建议:为
_is_tiktoken_tokenizer与_is_sentencepiece_tokenizer各补一组不 mock 自身的用例(最小 fake 对象覆盖各条探测分支与全不命中),并补一个「同时命中 fast 与 sentencepiece」的优先级用例固化 if 链顺序。另新增一条不 mockrtp_llm.ops的用例:让 py_test 依赖//rtp_llm:ops,直接from rtp_llm.ops import serialize_grammar_tokenizer_info并断言最小 vocab 返回的 JSON 可解析,把「Python 包装 →.so导出 → runfiles/wheel 可见」纳入 CI。
- 建议:为
- 「MTP + grammar(thinking OFF)」未真正进 CI,数据文件名与注释同实际消费配置矛盾 @
rtp_llm/test/smoke/suites_h20_oss.bzl:267- 建议:补一个
qwen35_grammar_mtp_no_reasoningcase:沿用q_r_mtp_grammar.json,在smoke_args中加上与 reasoning case 一致的--sp_model_type/--sp_type/--gen_num_per_cycle并保持--think_mode 0。同时把当前无 MTP case 使用的数据文件重命名为q_r_grammar.json并修正其_comment,suite 头部注释同步反映实际覆盖的轴。
- 建议:补一个
- 新增 config_pickle_test 目标缺少运行期依赖与 exec_properties,与仓库既有先例不一致 @
rtp_llm/cpp/pybind/BUILD:11- 建议:对齐既有约定:在
data中补齐伴生.so(至少//:rtp_compute_ops),并按同类目标显式声明exec_properties。若已确认该.so在当前构建配置下无未解析的运行期依赖、可在 CPU 机上加载,请在 BUILD 注释中写明该结论(参考 CPU fallback 测试目标的注释写法),避免后续维护者误判。
- 建议:对齐既有约定:在
- 新增 PB 字段 grammar_terminate_without_stop_token 与 RPC 边界 fail-fast 契约在客户端侧零断言 @
rtp_llm/cpp/model_rpc/test/model_rpc_client_test.py:174- 建议:在现有参数化用例中为每个 case 增加
input_pb.generate_config.grammar_terminate_without_stop_token的期望值断言;并补一个用例:构造GenerateConfig(response_format=...)或json_format=True后调用trans_input,用assertRaises+match断言在序列化层 fail-fast,把新的边界契约固化在客户端侧。
- 建议:在现有参数化用例中为每个 case 增加
- grammar BUILD 注释声明的 Tier 依赖约束未通过 visibility 落地 @
rtp_llm/cpp/engine_base/grammar/BUILD:35- 建议:把
grammar_xgrammar与grammar_cpp_lib的 visibility 收窄为白名单(如 grammar 包自身、cpp/models/logits_processor及其 test 包),仅对确实需要被根BUILD引用的 target 保留 public,并在注释中写明白名单即是 Tier 约束的执行点,让分层约束由构建图强制而非注释约定。
- 建议:把
- 「模型 vocab」在普通 decode 与 MTP verify 两条路径取自不同来源 @
rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:1042- 建议:统一 vocab 来源:MTP 侧同样以
model_output.logits.size(1)作为模型 vocab 传给LaunchTask;或在MtpExecutor构造 / 首次 forward 后加一条vocab_size_ == logits.size(1)的显式断言并注释说明该不变量,避免两条路径各自解释「模型 vocab」。
- 建议:统一 vocab 来源:MTP 侧同样以
P3
- MTP 解码下每次 commit 都重建一份不会被消费的全词表 bitmask @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:656- 建议:改为懒加载:
refreshAfterCommit只作废缓存(记录新的token_len并置UNSET),由process()首次访问时再buildState;或由MtpExecutor显式告知 processor 当前处于 SPEC_VERIFY 模式而跳过刷新。补一条「仅updateStatus而不process时不触发 bitmask 填充」的测试防回归,并在优化前后给出 grammar + MTP 组合的稳态吞吐对比以确认收益。
- 建议:改为懒加载:
- matcher 进入 FINISHED 后 decode 不再施加任何约束,与 MTP 路径强制 EOS 语义相反 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:411- 建议:把 decode 路径的
FINISHED与TERMINATED统一为强制 EOS(或至少保留 mask),使熔断语义在两条路径一致;若刻意保留 fail-open,请在 switch 分支上注释说明「FINISHED 必然伴随已上报错误、流即将终止」这一前提,并补一条断言该前提的测试。
- 建议:把 decode 路径的
- MTP 掩码行索引对 streams 与 allStreams 顺序存在隐式依赖,缺少断言 @
rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:1045- 建议:在
runSpecLogitsVerify或gatherSpecSamplerInput入口加一条显式断言(如比对 streamId 序列,或改为按 stream 指针建立映射而非位置序号),并在MtpExecutorTest中补一条「流顺序不同」的用例固化该不变量。
- 建议:在
- renderer 约束扩展点在 finalize 之后执行,且三处调用路径不一致 @
rtp_llm/openai/openai_endpoint.py:587- 建议:把
_apply_renderer_chat_constraints移到add_thinking_params之前(即在_extract_generation_config内部、finalize 之前调用),或在钩子返回后重新执行一次ResponseFormatBuilder(...).apply(),并统一三处调用路径。同时在钩子的契约注释中写明「只能在 finalize 之前设置 grammar 字段」,并补一个 renderer 设置json_schema+ think 模式的用例固化该顺序。
- 建议:把
- BUILD 元数据与实际内容不符:dlpack 版本注释错误、nanobind exclude 永不生效 @
3rdparty/dlpack/BUILD:6- 建议:把 dlpack 注释更正为实际 vendored 版本(v1.2)并记录来源 commit/tag,便于后续与 xgrammar、torch 的 dlpack 版本做兼容性核对;xgrammar 顶部注释改为「fork commit + 基线版本」;删除永不生效的
exclude,或把srcsglob 改为递归cpp/**/*.cc后再保留该 exclude 以表达真实意图。
- 建议:把 dlpack 注释更正为实际 vendored 版本(v1.2)并记录来源 commit/tag,便于后续与 xgrammar、torch 的 dlpack 版本做兼容性核对;xgrammar 顶部注释改为「fork commit + 基线版本」;删除永不生效的
- GrammarConfig pickle 仅靠 t[0] 类型区分新旧布局,缺少显式版本标记 @
rtp_llm/cpp/pybind/ConfigInit.cc:1351- 建议:在 state 首位加入显式版本标记(如
py::make_tuple("v2", ...))或改用py::dictstate,__setstate__按版本号分派并对未知版本报错;同时补一个「5 元组但版本/类型不匹配」的拒绝用例。
- 建议:在 state 首位加入显式版本标记(如
- CudaMaskLogitsOpTest 保留 MYDEBUG 调试打印且用浮点 == 直接断言 @
rtp_llm/models_py/bindings/cuda/ops/tests/CudaMaskLogitsOpTest.cc:48- 建议:删除
MYDEBUG_*打印,或改为仅在断言失败时输出(受环境变量控制);把精确相等断言改为ASSERT_FLOAT_EQ(掩码值为-FLT_MAX/-inf时可用专门的std::isinf断言),使失败信息包含期望与实际值,与 CPU fallback 测试保持一致的断言风格。
- 建议:删除
- test_grammar_defaults 与被测默认值同源,且 env 绑定通道完全无覆盖 @
rtp_llm/server/server_args/test/server_args_test.py:423- 建议:把默认值用例改为断言字面量(
False/8/512 * 1024 * 1024),使 C++ 默认值变更时测试主动失败并提示同步;如仍需校验 wiring,可另保留一条与新建实例比较的断言。同时补一条通过os.environ设置两个 env 后再_setup()的用例,覆盖 env→config 绑定路径。
- 建议:把默认值用例改为断言字面量(
Checklist Violations (18 fail / 58 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
grammar BUILD 注释声明的 Tier 依赖约束未通过 visibility 落地
该 BUILD 顶部即package(default_visibility = ["//visibility:public"])(:4);grammar_xgrammar的注释写明「xgrammar-specific impl; only Tier 0 (this package) and Tier 1 (logits_processor) may depend」(:35),grammar_cpp_lib的注释写明「Transitional umbrella; deleted once callers move onto :grammar_xgrammar」(:58)且在 :64 显式重申 public。约束只存在于注释中:任何 package 都可直接依赖 xgrammar 专有实现,后续想删除 umbrella 或替换 backend 时无法通过构建图发现违规调用方。 - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
BUILD 元数据与实际内容不符:dlpack 版本注释错误、nanobind exclude 永不生效
3rdparty/dlpack/BUILD:6注释声称「We vendor the v0.8 header verbatim instead.」,但 vendored 的include/dlpack/dlpack.h:19,22定义DLPACK_MAJOR_VERSION 1/DLPACK_MINOR_VERSION 2,实际是 v1.2 —— 两个版本的DLManagedTensorVersionedABI 差异显著,注释会误导后续升级与跨组件对齐判断。同理3rdparty/xgrammar/xgrammar.BUILD:1注释声称 v0.2.2,与所 pin 的 fork commit 无法互相印证;:38-41 的exclude = ["cpp/nanobind/**"]相对cpp/*.cc与cpp/support/*.cc两个 glob 永不匹配,属无效声明。 - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
grammar BUILD 注释声明的 Tier 依赖约束未通过 visibility 落地
该 BUILD 顶部即package(default_visibility = ["//visibility:public"])(:4);grammar_xgrammar的注释写明「xgrammar-specific impl; only Tier 0 (this package) and Tier 1 (logits_processor) may depend」(:35),grammar_cpp_lib的注释写明「Transitional umbrella; deleted once callers move onto :grammar_xgrammar」(:58)且在 :64 显式重申 public。约束只存在于注释中:任何 package 都可直接依赖 xgrammar 专有实现,后续想删除 umbrella 或替换 backend 时无法通过构建图发现违规调用方。 - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
MTP 解码下每次 commit 都重建一份不会被消费的全词表 bitmask
MTP decode 的采样输入由gatherSpecSamplerInput构造,它经allocateSamplerInputs执行sampler_inputs.logits_processor_states_ptr.reset()(NormalSamplerInputGatherer.cc:118),赋值只发生在普通gather()路径的setLogitsProcessorInputs(同文件 :219)。因此 MTP decode 从不调用GrammarLogitsProcessor::process,mask 完全由SpecLogitsVerifyRunner的 per-row bitmask 提供。但每次acceptCommittedLocked结束时仍无条件执行decode_mask_builder_->refreshAfterCommit(:656),其buildState(:346, :373)会走prepareBitmask(首次pin_memory()、其后fill_(-1) - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
backend 初始化四类失败统一 fail-soft,服务端配置故障以用户参数错误返回且无可观测信号
create()对「元数据为空」(:66-69,INFO)、「反序列化失败」(:72-76,ERROR)、「vocab 为空」(:86-89,ERROR)、「构造抛异常」(:96-99,ERROR)四类失败统一return nullptr,仅落日志,无启动期 fail-fast、健康检查或指标。而 Python 侧base_model.py:329-332对同类「部署/数据配置错误」是直接RuntimeError硬失败,两层策略相反。叠加response_format_builder.py让 think 请求一律带 structural_tag,backend 为空时每个 think / 结构化请求都在LogitsProcessorFactory.cc:107-111返回INVALID_PARAMS,服务端配置故障被伪装成用户输入非法,运维只能靠翻启动日志发现。 - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
GrammarConfig pickle 仅靠 t[0] 类型区分新旧布局,缺少显式版本标记
__setstate__只校验t.size() != 5(:1347),而新旧 state 都是 5 元组、长度无法区分,实际靠py::isinstance<py::str>(t[0])判定 legacy(:1351,旧布局首位是grammar_backend字符串)。当前新布局首位是 bool 故可工作,但该判据是隐式的:一旦后续增删字段后新布局仍为 5 元组、或首字段类型变为字符串,就会整体错位地按 legacy 分支解释。config_pickle_test.py:43-50只覆盖 3/4 元组被拒,未覆盖「长度相同但布局不同」这一真正的歧义场景。 - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
matcher 进入 FINISHED 后 decode 不再施加任何约束,与 MTP 路径强制 EOS 语义相反
applyDeviceMaskState把FINISHED与UNSET/NOOP并列直接return ErrorInfo::OkStatus()(:409-412),即不写任何 mask(fail-open);而 MTP 侧fillSpecVerifyRow对matcher.finished()的处理是forceTokenInBitmask(row, W, eos_token_id)(fail-closed)。经复核,全部markFinished()调用点(:46/51/57/83/93/154/239/592/600/609)均与错误返回同步,且错误经StreamUpdateInfo终止该流,因此当前无法构造实际放行窗口;但两条路径对同一熔断状态采取相反策略,后续任何「熔断但不终止流」的改动都会退化成静默无约束生成。 - [6.1] Quality — Commit 原子、message 与行为匹配 → issue
BUILD 元数据与实际内容不符:dlpack 版本注释错误、nanobind exclude 永不生效
3rdparty/dlpack/BUILD:6注释声称「We vendor the v0.8 header verbatim instead.」,但 vendored 的include/dlpack/dlpack.h:19,22定义DLPACK_MAJOR_VERSION 1/DLPACK_MINOR_VERSION 2,实际是 v1.2 —— 两个版本的DLManagedTensorVersionedABI 差异显著,注释会误导后续升级与跨组件对齐判断。同理3rdparty/xgrammar/xgrammar.BUILD:1注释声称 v0.2.2,与所 pin 的 fork commit 无法互相印证;:38-41 的exclude = ["cpp/nanobind/**"]相对cpp/*.cc与cpp/support/*.cc两个 glob 永不匹配,属无效声明。 - [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue
CudaMaskLogitsOpTest 保留 MYDEBUG 调试打印且用浮点 == 直接断言
三个用例各在 kernel 调用前后执行printBufferData(logits, "MYDEBUG_BEFORE_MASK_LOGITS")/"MYDEBUG_AFTER_MASK_LOGITS"(:48/50、:78/80、:108/110),把整块 logits 缓冲刷进测试日志;断言侧使用ASSERT_TRUE(logits_ptr[i] == expect_vec[i])(:64)对浮点做精确相等比较,失败时不会打印期望值与实际值。前者是临时调试遗留("MYDEBUG" 命名即说明其性质),后者在 fp16/bf16 转 float 的路径上比ASSERT_FLOAT_EQ更脆弱且更难排障;同批新增的PackedMaskLogitsCpuFallbackTest已统一使用EXPECT_FLOAT_EQ/std::isinf。 - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
grammar_terminate_without_stop_token 在 Python 派生与 C++/HTTP 两侧语义分裂且双份实现
Python 侧是派生方法GenerateConfig.grammar_terminate_without_stop_token()(generate_config.py:563),规则在response_format_builder.py:176-179:json_schema is not None即 True,否则取 reasoning envelope 私有标记;请求里同名 key 会被_apply_updates静默丢弃(不在model_fields中)。C++ 侧LogitsProcessorFactory.cc:113-114又独立实现config.grammar_terminate_without_stop_token || grammar_key.key_type == "json",同一条策略两端各写一遍;同时GenerateConfig.h:236将其JSONIZE,使裸 HTTP 客户端可任意填写。两个入口对同一字段的可写性与语义不对称,且无测试锁定两端等价性。 - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
MTP 解码下每次 commit 都重建一份不会被消费的全词表 bitmask
MTP decode 的采样输入由gatherSpecSamplerInput构造,它经allocateSamplerInputs执行sampler_inputs.logits_processor_states_ptr.reset()(NormalSamplerInputGatherer.cc:118),赋值只发生在普通gather()路径的setLogitsProcessorInputs(同文件 :219)。因此 MTP decode 从不调用GrammarLogitsProcessor::process,mask 完全由SpecLogitsVerifyRunner的 per-row bitmask 提供。但每次acceptCommittedLocked结束时仍无条件执行decode_mask_builder_->refreshAfterCommit(:656),其buildState(:346, :373)会走prepareBitmask(首次pin_memory()、其后fill_(-1) - [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue
matcher 进入 FINISHED 后 decode 不再施加任何约束,与 MTP 路径强制 EOS 语义相反
applyDeviceMaskState把FINISHED与UNSET/NOOP并列直接return ErrorInfo::OkStatus()(:409-412),即不写任何 mask(fail-open);而 MTP 侧fillSpecVerifyRow对matcher.finished()的处理是forceTokenInBitmask(row, W, eos_token_id)(fail-closed)。经复核,全部markFinished()调用点(:46/51/57/83/93/154/239/592/600/609)均与错误返回同步,且错误经StreamUpdateInfo终止该流,因此当前无法构造实际放行窗口;但两条路径对同一熔断状态采取相反策略,后续任何「熔断但不终止流」的改动都会退化成静默无约束生成。 - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
新增 config_pickle_test 目标缺少运行期依赖与 exec_properties,与仓库既有先例不一致
新增 py_test 只声明srcs与data = ["//:th_transformer_config"],无deps、无exec_properties(:11-15),而测试在模块导入期就执行from libth_transformer_config import GrammarConfig(config_pickle_test.py:4)。根BUILD:96-106的th_transformer_config是linkshared目标,linkopts 仅-Wl,-rpath='$$ORIGIN',其依赖链th_transformer_config_lib → py_utils → //:rtp_compute_ops指向另一个linkshared目标,需要librtp_compute_ops.so同目录才能加载。仓库内同样依赖该.so的 py_test(rtp_llm/utils/test/BUILD:71-76等)一律同时提供三个.so并指定 `exec_prope - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
test_grammar_defaults 与被测默认值同源,且 env 绑定通道完全无覆盖
grammar_group_args.py:11/19/33已把 CLI 默认值改为default=grammar_config.<field>,而该用例的期望值取自type(g)()新建实例(:428),两侧同源于同一份 C++ 默认值。因此若 C++ 侧改掉num_workers或compiler_cache_bytes默认值(后者是 512MiB 编译缓存上限,直接影响进程内存占用),这条自称 "Regression guard for the wiring" 的用例仍会通过。此外该测试类没有任何用例覆盖GRAMMAR_NUM_WORKERS/GRAMMAR_COMPILER_CACHE_BYTES的 env→config 绑定路径,而 env 是该配置面的主要线上入口。 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
CudaMaskLogitsOpTest 保留 MYDEBUG 调试打印且用浮点 == 直接断言
三个用例各在 kernel 调用前后执行printBufferData(logits, "MYDEBUG_BEFORE_MASK_LOGITS")/"MYDEBUG_AFTER_MASK_LOGITS"(:48/50、:78/80、:108/110),把整块 logits 缓冲刷进测试日志;断言侧使用ASSERT_TRUE(logits_ptr[i] == expect_vec[i])(:64)对浮点做精确相等比较,失败时不会打印期望值与实际值。前者是临时调试遗留("MYDEBUG" 命名即说明其性质),后者在 fp16/bf16 转 float 的路径上比ASSERT_FLOAT_EQ更脆弱且更难排障;同批新增的PackedMaskLogitsCpuFallbackTest已统一使用EXPECT_FLOAT_EQ/std::isinf。
RTP-LLM Checklist
- [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue
ThinkModeLogitsProcessor 装配点移除后沦为死代码,think 预算在直连路径静默失效
createLogitsProcessors删除了ThinkModeLogitsProcessor::fromGenerateInput注入,仅留注释「do not add a legacy processor fallback」(:98-99)。全仓检索显示该类此后只被cpp/models/BUILD:55,68、logits_processor/test/BUILD:24与自身单测引用,生产侧无构造点,而docs/backend/logits_processor.md:8与rtp_llm/dash_sc/think.py:167仍将其描述为 think 预算控制手段。同时QueryConverter.cc:107-108仍把 proto 的in_think_mode/max_thinking_tokens写入GenerateConfig;只设这两项而无 grammar 字段时keyFromGenerateConfig返回空 key,函数返回空列表且 status ok —— 既不约束也不报错,仅 :11 - [I] 代码质量 — 同一功能用统一工具函数 → issue
「模型 vocab」在普通 decode 与 MTP verify 两条路径取自不同来源
普通 decode 以实际 logits 宽度作为模型 vocab:validateGrammarVocabFitsModel(*matcher_, logits_row.size(0), "decode")(GrammarLogitsProcessor.cc:518),mask 落地与 tail 填充亦按logits_row.size(0)。MTP 侧改用ModelConfig的vocab_size_:task.vocab_size = vocab_size_(:1042,来源 :238),由此推导bitmask_words与 verify 侧的validateGrammarVocabFitsModel;但 mask 应用时又换回sampler_inputs.vocab_size = model_output.logits.size(1)(MtpBatchStreamProcessor.cc:142, :155)。当 padded vocab 使二者不等时,同一 grammar 在普通 decode 通过、在 MTP verif
Python Static-First Checklist
- [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
tokenizer 家族探测函数与 native 序列化接缝在单测中被整体 mock
build_tokenizer_info辅助方法同时mock.patch.object掉_is_fast_tokenizer/_is_tiktoken_tokenizer/_is_sentencepiece_tokenizer/_is_byte_level_tokenizer(:73-92),并用mock.patch.dict(sys.modules, {"rtp_llm.ops": fake_ops})替换整个rtp_llm.ops(:69-72)。因此:(1) 决定vocab_type取 RAW/BYTE_FALLBACK/BYTE_LEVEL、并决定「模型能否启动」的_is_tiktoken_tokenizer(三条鸭子类型探测)与_is_sentencepiece_tokenizer(三个候选探测)完全无覆盖;(2)rtp_llm/ops/__init__.py:193新增的serialize_grammar_tokenizer_info包装(函数体内延迟 import)零覆盖,唯一触
Strengths
- 错误语义端到端显式化:
process/updateStatus返回std::optional<ErrorInfo>,LogitsProcessorStates::batchProcess按 interval 回填,collectStreamSamplerError用注释明确「processor 错误与 sampling success 均使用 sampler 输入坐标」并按batch_idx_in索引,修掉了上一轮 beam 扩展下的错误归属问题,且有专门用例锁定。 - committed 状态原子性设计到位:
acceptCommittedLocked用rollback_commit把 matcher token 数、committed_output_len_、缓存 mask 视为同一提交单元,多 token 批量失败整批回滚,回滚失败则markFinished()熔断,不留半可见前缀;GenerateStream::validateLogitsProcessorState再做双向长度校验。 - 终止态处理细致:刻意让 matcher 停在 TERMINATED 而非 FINISHED,并在
GrammarLogitsProcessor.cc:625-628注释说明min_new_tokens/ignore_eos延迟完成时仍须只放行 EOS,而不是回退成无约束生成。 - MTP 校验严格遵守基类契约:
verifySpecDraftAndFillBitmask无条件回滚 provisional accept,回滚失败与验证失败叠加时两个错误串联上报;pinned host buffer 用pending_bitmask_uploads_+ event 显式管理,覆写前先synchronize(),不依赖后续 sampler 的隐式 D2H 顺序。 - 编译错误分类清晰:
compileWithErrorClassification把bad_alloc/runtime_error/ 其他分别映射为 ResourceExhausted / InvalidArgument / Unknown,并有 malformed json schema 的分类断言。 - 掩码 kernel 与 CPU fallback 语义一致且 fail-closed:
packed_mask_logits与applyPackedMaskLogitsCpuFallback都对越界logits_row跳过、对超出bitmask_words的 tail 一律屏蔽,掩码值按 dtype 与BaseLogitsProcessor::neg_inf转换后一致,fallback 在非 CPU 设备上有显式 copy-back。 - wire 兼容处理规范:proto 保留
response_format = 73 [deprecated = true]不复用编号、新字段用 74;GrammarConfigpickle 用py::isinstance<py::str>(t[0])兼容 legacy 五元组(避免cast<bool>陷阱),并覆盖新布局往返、legacy 降级、伪造短元组被拒三类场景。 - 修掉了真实陷阱:
_apply_updates改用key in type(self).model_fields判定消费(generate_config.py:448-456),避免请求参数覆盖同名方法;gatherSpecSamplerInput补sampler_inputs.finished_mask.zero_(),修掉 spec 路径未初始化即被读取的隐患,均有断言。 - 配置面防漂移:
grammar_group_args.py全部使用default=grammar_config.<field>消除 Python/C++ 默认值漂移,help 与optionsFromConfig的std::max(1,...)/>0 ? ... : -1语义一致;GrammarConfig::to_string()与debugString()对大 JSON 只输出 size。 - 测试面明显扩张:
GrammarLogitsProcessorTest覆盖 vocab 越界、EOS 越界、bitmask 缓冲过小、拒绝 token 整批回滚、terminated 后强制 EOS 与 spec cap 各分支;PackedMaskLogitsCpuFallbackTest刻意不带exec_properties,在无 GPU 环境固化三种 dtype 的掩码语义。
| self.restore_final_constraint(self.config, saved_constraint) | ||
| return saved_constraint | ||
|
|
||
| if self.config.in_think_mode and ( |
There was a problem hiding this comment.
[P1] think 模式被无条件转为 grammar 约束解码,think + beam / n>1 由可用变为硬失败且强依赖 backend
apply() 在 in_think_mode 为真时::80-87 无条件拒绝 has_num_beams() 或 num_return_sequences > 1;:102-105 即使用户未传任何 grammar 字段,也用 {"type":"any_text"} 包一层 structural_tag envelope。in_think_mode 来源是服务端 THINK_MODE 或请求 enable_thinking,与是否请求结构化输出无关。后果:推理模型上原本可用的 n>1 / beam 请求直接失败;且每个 thinking 请求都依赖 grammar backend,一旦 XGrammarBackend::create 返回 nullptr(该函数四类失败均 fail-soft),LogitsProcessorFactory.cc:107-111 会让全部 thinking 请求返回 INVALID_PARAMS。
建议: 把 envelope 从「think 模式必开」降级为「按需开」:仅当存在用户 grammar 约束、或确实需要用 grammar 强制 max_thinking_tokens 边界时才包裹;n>1 / beam 场景退化为不加 envelope 并保留原 think 行为,而不是拒绝请求。同时为「backend 不可用 + thinking 请求」补显式降级路径(跳过 envelope 并打点告警),避免单点初始化失败放大为推理模型全量不可用。该不兼容点需在 PR description 与发布说明中列出,并补齐 beam / n>1 + think 的用例。
| model_vocab_size=int(self.model_config.vocab_size or 0), | ||
| stop_token_ids=self._collect_tokenizer_info_stop_token_ids(), | ||
| ) | ||
| except Exception as e: |
There was a problem hiding this comment.
[P1] 启动期强制构建 grammar tokenizer 元数据,不受支持的 tokenizer 直接阻塞模型加载且无逃生开关
load_tokenizer()(:294-301)对所有继承 BaseModel 的模型(含从不使用结构化输出的 embedding / reranker)无条件调用 _fill_grammar_tokenizer_info(),其中任何异常在 :329-332 被统一升级为 RuntimeError。触发条件:tokenizer 既非 fast、也非 tiktoken、也非 sentencepiece(grammar_tokenizer_info.py:192 抛 ValueError),或 stop_token_ids 为空(同文件 :156)。而 C++ 侧 XGrammarBackend::create 对元数据为空/损坏一律 fail-soft(返回 nullptr、仅打日志),两层策略相反;同一函数内 real_tokenizer is None 却是静默 return(:320-321),与 :311-315 注释宣称的 fail-fast 自相矛盾。
建议: 区分「grammar 能力可用性」与「模型可服务性」:对 Unsupported tokenizer type / 空 stop_token_ids 降级为 ERROR 日志 + 指标并把 tokenizer_info_json 留空,让 C++ 已有的 "constraint backend is disabled" 分支在真正收到 grammar 请求时报错。若坚持 fail-fast,请补一个默认宽松的显式开关(如 --grammar_require_tokenizer_info)作为回滚手段,并统一 real_tokenizer is None 的处理策略,同时在发布说明中列出受影响的 tokenizer 类型。
| ) | ||
| if request.debug_info: | ||
| config.return_output_ids = True | ||
| return config |
There was a problem hiding this comment.
📍 实际位置 rtp_llm/openai/openai_endpoint.py:587(不在 diff 展示范围内,就近挂载)
[P3] renderer 约束扩展点在 finalize 之后执行,且三处调用路径不一致
_extract_generation_config 在 :287 调用 config.add_thinking_params(...),后者在 generate_config.py:550-561 直接执行 ResponseFormatBuilder(...).apply() 完成 finalize。而 _apply_renderer_chat_constraints(:298-305,通过 getattr(renderer, "apply_chat_completion_constraints", None) 的鸭子类型扩展点)在 :587 与 :712 才执行,:627 那条路径完全不调用它。任何在该钩子里写 config.json_schema / response_format 的 renderer,都会与已生成的 structural_tag envelope 共存,在 RPC 边界触发 validate_finalized 的 "only one grammar constraint" / "must be finalized",...
建议: 把 _apply_renderer_chat_constraints 移到 add_thinking_params 之前(即在 _extract_generation_config 内部、finalize 之前调用),或在钩子返回后重新执行一次 ResponseFormatBuilder(...).apply(),并统一三处调用路径。同时在钩子的契约注释中写明「只能在 finalize 之前设置 grammar 字段」,并补一个 renderer 设置 json_schema + think 模式的用例固化该顺序。
| c.num_workers = t[2].cast<int>(); | ||
| c.tokenizer_info_json = t[3].cast<std::string>(); | ||
| c.override_stop_tokens = t[4].cast<std::vector<int32_t>>(); | ||
| if (py::isinstance<py::str>(t[0])) { |
There was a problem hiding this comment.
[P3] GrammarConfig pickle 仅靠 t[0] 类型区分新旧布局,缺少显式版本标记
__setstate__ 只校验 t.size() != 5(:1347),而新旧 state 都是 5 元组、长度无法区分,实际靠 py::isinstance<py::str>(t[0]) 判定 legacy(:1351,旧布局首位是 grammar_backend 字符串)。当前新布局首位是 bool 故可工作,但该判据是隐式的:一旦后续增删字段后新布局仍为 5 元组、或首字段类型变为字符串,就会整体错位地按 legacy 分支解释。config_pickle_test.py:43-50 只覆盖 3/4 元组被拒,未覆盖「长度相同但布局不同」这一真正的歧义场景。
建议: 在 state 首位加入显式版本标记(如 py::make_tuple("v2", ...))或改用 py::dict state,__setstate__ 按版本号分派并对未知版本报错;同时补一个「5 元组但版本/类型不匹配」的拒绝用例。
Checklist: [6.1] 状态不变量:创建/更新/失败/重试/回滚路径有效
| printBufferData(logits, "MYDEBUG_AFTER_MASK_LOGITS"); | ||
|
|
||
| auto logits_cpu = logits.cpu().contiguous(); | ||
| auto logits_ptr = logits_cpu.data_ptr<float>(); |
There was a problem hiding this comment.
📍 实际位置 rtp_llm/models_py/bindings/cuda/ops/tests/CudaMaskLogitsOpTest.cc:48(不在 diff 展示范围内,就近挂载)
[P3] CudaMaskLogitsOpTest 保留 MYDEBUG 调试打印且用浮点 == 直接断言
三个用例各在 kernel 调用前后执行 printBufferData(logits, "MYDEBUG_BEFORE_MASK_LOGITS") / "MYDEBUG_AFTER_MASK_LOGITS"(:48/50、:78/80、:108/110),把整块 logits 缓冲刷进测试日志;断言侧使用 ASSERT_TRUE(logits_ptr[i] == expect_vec[i])(:64)对浮点做精确相等比较,失败时不会打印期望值与实际值。前者是临时调试遗留("MYDEBUG" 命名即说明其性质),后者在 fp16/bf16 转 float 的路径上比 ASSERT_FLOAT_EQ 更脆弱且更难排障;同批新增的 PackedMaskLogitsCpuFallbackTest 已统一使用 EXPECT_FLOAT_EQ / std::isinf。
建议: 删除 MYDEBUG_* 打印,或改为仅在断言失败时输出(受环境变量控制);把精确相等断言改为 ASSERT_FLOAT_EQ(掩码值为 -FLT_MAX / -inf 时可用专门的 std::isinf 断言),使失败信息包含期望与实际值,与 CPU fallback 测试保持一致的断言风格。
Checklist: [6.1] 无 per-forward 调试日志 / 噪声热路径输出;[6.1] 边界 case 覆盖(空、单元素、最大值)
| importlib.reload(rtp_llm.server.server_args.server_args) | ||
| return rtp_llm.server.server_args.server_args.setup_args() | ||
|
|
||
| def test_grammar_defaults(self): |
There was a problem hiding this comment.
[P3] test_grammar_defaults 与被测默认值同源,且 env 绑定通道完全无覆盖
grammar_group_args.py:11/19/33 已把 CLI 默认值改为 default=grammar_config.<field>,而该用例的期望值取自 type(g)() 新建实例(:428),两侧同源于同一份 C++ 默认值。因此若 C++ 侧改掉 num_workers 或 compiler_cache_bytes 默认值(后者是 512MiB 编译缓存上限,直接影响进程内存占用),这条自称 "Regression guard for the wiring" 的用例仍会通过。此外该测试类没有任何用例覆盖 GRAMMAR_NUM_WORKERS / GRAMMAR_COMPILER_CACHE_BYTES 的 env→config 绑定路径,而 env 是该配置面的主要线上入口。
建议: 把默认值用例改为断言字面量(False / 8 / 512 * 1024 * 1024),使 C++ 默认值变更时测试主动失败并提示同步;如仍需校验 wiring,可另保留一条与新建实例比较的断言。同时补一条通过 os.environ 设置两个 env 后再 _setup() 的用例,覆盖 env→config 绑定路径。
Checklist: [6.1] 新逻辑有聚焦单测 + 相关集成/smoke 测试
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1006
Status: BLOCKING
Summary: P0/0 · P1/5 · P2/27 · P3/6
Reviewed: commit 5bfbc870c4dc · 2026-08-05 13:08 UTC+8
Blocking Issues
P1
- 核心依赖 xgrammar 指向个人账号 fork,构建不可复现且无校验、镜像与回滚手段 @
3rdparty/xgrammar/repositories.bzl:7- 建议:至少落实其一:1) 把该 commit 镜像到组织/内部仓库并改用镜像地址(保持 commit pin);2) 改为
http_archive+sha256的固定归档并并入deps/git.bzl统一审计管理。同时在repositories.bzl与xgrammar.BUILD头部注明「上游 tag + fork 相对上游的 patch 清单 + 上游 PR 链接 + 上游化计划与责任人」,避免核心受限解码链路长期依赖不可控的外部个人仓库。
- 建议:至少落实其一:1) 把该 commit 镜像到组织/内部仓库并改用镜像地址(保持 commit pin);2) 改为
- --grammar_backend / GRAMMAR_BACKEND 被直接移除,存量启动脚本升级即退出且失去唯一关闭手段 @
rtp_llm/server/server_args/grammar_group_args.py:4- 建议:保留一个 deprecated no-op flag 过渡一个版本:仍注册
--grammar_backend/GRAMMAR_BACKEND,命中时打印一次 WARNING 说明已废弃并忽略取值,并在server_args_test.py补一条「旧参数不导致启动失败」的断言。若none在运维侧被当作 grammar 关停手段使用,请为新的 xgrammar 约束解码路径补一个明确总开关(如GrammarConfig.enabled/--disable_grammar),保留一键回滚能力,并在发布说明中列出被删除的 flag/env。
- 建议:保留一个 deprecated no-op flag 过渡一个版本:仍注册
- 启动期强制构建 grammar tokenizer 元数据,不受支持的 tokenizer 直接阻塞模型加载且无逃生开关 @
rtp_llm/models/base_model.py:329- 建议:把「构建 grammar 元数据」与「引擎可启动」解耦并区分失败类别:
stop_token_ids缺失等可判定的配置错误保留 fail-fast;tokenizer 家族无法识别时降级为 WARNING 并留空tokenizer_info_json,由已有的 C++ 禁用路径承接(后续 grammar 请求返回明确错误)。若坚持 fail-fast,请新增显式开关(如grammar_require_tokenizer_info,默认 false)并在server_args暴露为运维回滚手段,同时补一条「不支持的 tokenizer → 引擎仍可启动、grammar 请求返回明确错误」的单测,并在发布说明中给出受影响 tokenizer 清单。
- 建议:把「构建 grammar 元数据」与「引擎可启动」解耦并区分失败类别:
- think 模式被无条件转为 grammar 约束解码,think + beam / n>1 由可用变为硬失败且强依赖 backend @
rtp_llm/config/response_format_builder.py:80- 建议:1) 在 PR description / 发布说明中把「think + beam / num_return_sequences>1 不再支持」列为 breaking change 并给出替代方案;2) 提供回退路径:无用户 grammar 时可通过配置继续用 token 计数方式限制 thinking 预算,而非一律构造
any_text信封;3) 补充 THINK_MODE 开关前后的 TPOT/吞吐对比数据。若确认无条件包裹是唯一实现,请在apply()处注释说明为何 thinking 预算必须由 grammar 承担,便于排障时判断该路径能否关闭。
- 建议:1) 在 PR description / 发布说明中把「think + beam / num_return_sequences>1 不再支持」列为 breaking change 并给出替代方案;2) 提供回退路径:无用户 grammar 时可通过配置继续用 token 计数方式限制 thinking 预算,而非一律构造
- MTP 下非 grammar logits processor 变为请求级硬失败,且 tree_decode_config 启动期抛错,均无开关与迁移路径 @
rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:850- 建议:1) 若属有意收紧,在发布说明中列为 breaking change(含「MTP 场景必须 unset TREE_DECODE_CONFIG」),并补真实组合用例(
MTP + combo_token_size>0、MTP + num_return_sequences>1、MTP + TREE_DECODE_CONFIG)断言错误码与错误信息;2) 若需保留可用性,为MtpProcessorCapability增加「MTP 下可安全忽略」的第三种模式(如IGNORED_IN_SPEC),让与投机验证无关的 processor 走告警降级而非拒流;3) 至少提供环境变量在灰度期把拒流/启动拒绝降级为告警;4)tree_decode_config断言抛出前先打一条 ERROR 日志,便于容器反复重启时定位。
- 建议:1) 若属有意收紧,在发布说明中列为 breaking change(含「MTP 场景必须 unset TREE_DECODE_CONFIG」),并补真实组合用例(
Non-blocking Suggestions
P2
- proto field 73 被引擎单向停用后旧客户端约束静默丢弃,且转换层缺少 field 73/74 测试锚点 @
rtp_llm/cpp/model_rpc/QueryConverter.cc:100- 建议:在
transGenerateConfig中检测config_proto->has_response_format():若为 true 且 69-72 全为空,返回INVALID_PARAMS让请求 fail-fast;至少也应RTP_LLM_INTERVAL_LOGWARNING 并打一个 metric,便于灰度期发现版本错配。同时在QueryConverterTest::testTransInput中补 field 73 present 时的行为断言与 field 74 的往返断言,固化跨版本 wire 行为。
- 建议:在
- grammar 编译错误在 factory 边界被压平为 INVALID_PARAMS,丢失可重试语义 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:124- 建议:按
matcher_or.status().code()映射后再包装:kInvalidArgument → INVALID_PARAMS、kResourceExhausted → MALLOC_FAILED(或 8000+ 可重试码)、其余→ EXECUTION_EXCEPTION,并在日志中保留原始 status code。建议把该映射抽成XGrammarBackend.h旁的小函数,使「分类」与「消费」保持同处,并补一条断言非 InvalidArgument 状态不会返回INVALID_PARAMS的单测。
- 建议:按
- backend 初始化四类失败统一 fail-soft,服务端配置故障以用户参数错误返回且无可观测信号 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:66- 建议:区分「未配置(tokenizer_info 为空)→ 静默禁用」与「已配置但初始化失败 → 显式失败」:后者建议启动即失败或暴露可上报的 metric/状态位;
LogitsProcessorFactory::init中 backend 为nullptr时输出一条启动期 ERROR 与可操作指引;同时把请求侧错误码改为服务端类错误码(非INVALID_PARAMS),使运维有明确的回滚判据。
- 建议:区分「未配置(tokenizer_info 为空)→ 静默禁用」与「已配置但初始化失败 → 显式失败」:后者建议启动即失败或暴露可上报的 metric/状态位;
- 解码路径失败退化为无约束,与 MTP 路径强制 EOS 的 fail-safe 极性相反且被缓存放大 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:287- 建议:统一 fail-safe 极性,建议解码路径也收敛到「仅允许 EOS」:新增
FORCE_EOS模式(或复用TERMINATED)替代失败时的finishedState,使apply即便错误被上层吞掉或后续命中缓存分支也不会退化为无约束生成;同时删除 :288 这行无效调用或合并其返回状态。请在GrammarLogitsProcessorTest.cc补一条「buildState 失败后 logits 仅 EOS 可选」的用例。
- 建议:统一 fail-safe 极性,建议解码路径也收敛到「仅允许 EOS」:新增
- SpecLogitsVerifyRunner::run 的形状校验异常逃出 catch,失败爆炸半径与设计不一致 @
rtp_llm/cpp/models/logits_processor/SpecLogitsVerifyRunner.cc:275- 建议:明确并统一失败策略:若这些检查确为「调用方 bug、应 fail-fast」,请在
run()注释中写清「CHECK 类不变量违规为进程级致命错误,不做逐流降级」并说明与bad_alloc/c10::Error处理不同的理由;若希望隔离到单流,改为返回makeFailureResultForActiveStreams(task, ...)或补一个catch (const std::exception&)兜底,避免一个流的形状异常影响同批其他流。
- 建议:明确并统一失败策略:若这些检查确为「调用方 bug、应 fail-fast」,请在
- ThinkModeLogitsProcessor 装配点移除后沦为死代码,think 预算在直连 gRPC 路径静默失效 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:99- 建议:在 factory 中对
config.in_think_mode && config.max_thinking_tokens > 0 && grammar_key.empty()显式返回INVALID_PARAMS(错误信息说明「thinking 约束须由 response_format/structural_tag 承载」),把静默忽略改为 fail-fast;删除已无生产构造点的ThinkModeLogitsProcessor及其 BUILD/测试条目并同步docs/backend/logits_processor.md,或在头文件注明保留原因与移除时间点;同时移除失效的max_batch_size形参,并在 proto 字段注释中说明新语义。
- 建议:在 factory 中对
- GrammarConfig 保留无消费者的 tokenizer_info_json / override_stop_tokens,与 ModelConfig 同名字段形成双源 @
rtp_llm/cpp/config/ConfigModules.h:377- 建议:二选一收敛单一真源:(a)删除
GrammarConfig这两个字段,同步清理 pybind、pickle 元组布局判定、to_string()、.pyi与config_pickle_test.py,让ModelConfig::tokenizer_info_json成为唯一来源;(b)保留字段但让XGrammarBackend::create仍以GrammarConfig为唯一入口、由初始化路径从ModelConfig填入。无论哪种,to_string()都不应再打印已无消费者的字段;若override_stop_tokens计划后续接线,请加// TODO(owner): 尚未接线。
- 建议:二选一收敛单一真源:(a)删除
- grammar_terminate_without_stop_token 在 raw HTTP 与 Python 两入口语义分裂,且 wire 字段 74 零断言 @
rtp_llm/cpp/engine_base/stream/GenerateConfig.h:236- 建议:明确契约归属并对齐两入口:若属服务端内部派生量,从
RawRequest/GenerateConfig的外部 JSONIZE 解析中剥离(或解析后强制重算),并把该 C++ 用例改为断言「外部传入被忽略」;若确实开放给 raw 入口,则 Python 侧也需接受并校验,且在 proto 字段 74 注释与接口文档中登记语义与优先级。同时补两条测试:model_rpc_client_test.py断言 think + json_schema 场景下 PB 字段 74 为 true、非 grammar 请求为 false;QueryConverterTest断言字段 74 的往返。
- 建议:明确契约归属并对齐两入口:若属服务端内部派生量,从
- Python 端 grammar vocab 取 max 与 C++ 端硬拒策略不一致,导致 fail-late 且模型 vocab 来源不统一 @
rtp_llm/config/grammar_tokenizer_info.py:23- 建议:在 Python 构建元数据时即比较 tokenizer 词表与
model_vocab_size:超出时按明确策略处理(截断到模型 vocab 并 WARNING,或启动期报错并给出可操作提示),避免把一个启动期即可判定的配置不匹配推迟成 100% 请求失败。同时把「模型 vocab」收敛为单一来源并让两条 C++ 路径复用同一判定,补一条「tokenizer vocab > model vocab」在两条路径下行为一致的单测固定该策略。
- 建议:在 Python 构建元数据时即比较 tokenizer 词表与
- RPC 边界移除 generate_config.validate() 后失去兜底校验 @
rtp_llm/cpp/model_rpc/model_rpc_client.py:110- 建议:把
validate()中无副作用的类型/范围检查抽为独立只读方法(如check_value_ranges()),在validate_finalized之前调用,既不违反「序列化不得修改 config」的新约束,又恢复 RPC 边界的 fail-fast;若坚持不校验,请补一条防回归测试固化「所有入口在构造 GenerateInput 前必须已 validate」这一不变量,并用assertRaisesRegex覆盖validate_finalized在未 finalize 时的拒绝分支。
- 建议:把
- response_format 的判定与投影存在多份平行实现,OpenAI wire 模型对象被漏检 @
rtp_llm/config/generate_config.py:39- 建议:让
_has_grammar_constraint()复用parse_response_format()+GrammarConstraint.from_response_format(),删除_response_format_is_grammar这份平行实现,并清理api_datatype.py:8被遮蔽的导入或重命名 wire 类;在GrammarConstraint上提供单一write_to_config(config)承载 name→字段映射,两处投影链改为「清空全部 grammar 字段 +constraint.write_to_config(config)」;_compact_json直接复用dump_compact_json。补一条「OpenAI 端 json_schema + num_beams 被 Python 侧拒绝」的用例。
- 建议:让
- ResponseFormatBuilder.apply 原地改写配置且异常时无回滚 @
rtp_llm/config/response_format_builder.py:89- 建议:把
apply()改为「先在局部变量上完成解析与信封构造、全部成功后再一次性写回 config」,或在入口处快照相关字段并在 except 中恢复;并补一条「apply 抛异常后 config 与调用前等价」的单测,防止后续重试路径踩到半成品状态。
- 建议:把
- grammar backend 使用函数内可变静态变量,init 与 createLogitsProcessors 之间缺少同步 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:66- 建议:把 backend 所有权从进程级静态移到 engine/executor(例如通过
EngineInitParams或 executor 成员持有std::shared_ptr<XGrammarBackend>,由GenerateStream构造时显式传入),同时解除多 engine/多 tokenizer 的隐含限制。若短期保留静态方案,至少改为std::call_once一次性初始化或用读写锁/原子 shared_ptr 保护,并把注释中的前提升级为运行期断言。
- 建议:把 backend 所有权从进程级静态移到 engine/executor(例如通过
- GrammarLogitsProcessor::updateStatus 的 token 布局校验过松,全量位置布局下会静默提交错误 token @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:535- 建议:将断言收紧为
RTP_LLM_CHECK_WITH_INFO(new_tokens.size(1) == num_new_tokens, ...)并在错误信息中带上size(1)/num_new_tokens,把「仅支持增量布局」从隐式不变量变为显式契约;或参照RecommendationLogitsProcessor按stride > num_new_tokens计算 offset。同时补一个size(1) > num_new_tokens的负例测试。
- 建议:将断言收紧为
- MultiSeqLogitsProcessor 写 EOS 掩码缺少 vocab 边界校验,存在越界写风险 @
rtp_llm/cpp/models/logits_processor/MultiSeqLogitsProcessor.cc:39- 建议:在写入前加边界校验:
eos_token_id_ < 0 || eos_token_id_ >= vocab_size时返回ErrorInfo(ErrorCode::OUT_OF_VOCAB_RANGE 或 GRAMMAR_EOS_OUT_OF_VOCAB, ...)(带上两个数值),并补一条eos_token_id_ >= vocab_size的负例单测,与 grammar 路径的处理保持一致。
- 建议:在写入前加边界校验:
- 语法编译在请求路径同步执行,无规模上限与超时,耗时仅 DEBUG 可见且无指标 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:131- 建议:补充可观测与保护:把成功路径的 elapsed_ms / bytes 上报为 metric(或至少改为
RTP_LLM_INTERVAL_LOG),对key_string长度设上限并对编译耗时设阈值告警;评估把首次编译移出请求接入路径(异步预热 + 首次请求等待带超时),避免单个复杂 schema 拖累整批请求的排队。
- 建议:补充可观测与保护:把成功路径的 elapsed_ms / bytes 上报为 metric(或至少改为
- legacy structures/triggers 形态的 structural_tag 在 think 模式下必然报错且错误文案误导 @
rtp_llm/config/grammar_constraint.py:143- 建议:要么在
_structural_tag_format_node中显式支持structures/triggers形态(转换为等价format节点),要么把错误文案改为「legacy structures/triggers 形态的 structural_tag 不支持与 reasoning 信封组合,请改用 format 形态」,并在generate_config_test.py补一条该形态在 think 模式下的拒绝用例,固化两种入参形态的支持边界。
- 建议:要么在
- grammar_constraint_only 校验存在静默放行路径,且完全短路 golden 比对 @
rtp_llm/test/smoke/openai_comparer.py:230- 建议:与 dash comparer 对齐 fail-loud:取不到约束来源时抛
SmokeException(QueryStatus.VALID_FAILED, ...);is_stream为真时要么实现流式拼接后校验,要么显式报「暂不支持该组合」。同时在该分支保留finish_reason/usage等确定性字段的比对,仅豁免 content/reasoning_content 与时延字段。建议把「取约束 + 校验 + 缺失即失败」抽成grammar_constraint_validator中的共享入口供两个 comparer 复用,避免同一规则两处实现继续漂移。
- 建议:与 dash comparer 对齐 fail-loud:取不到约束来源时抛
- grammar smoke 的唯一断言 oracle 自身无任何单测 @
rtp_llm/test/smoke/grammar_constraint_validator.py:307- 建议:为该模块补一组 py_test:对 regex / json_schema / structural_tag 各给出至少一条应通过与一条应失败的样本(含空字符串、被
length截断的半个 JSON、thinking 段未闭合等边界),断言失败样本确实抛出异常。这类用例纯 CPU、无模型依赖,成本很低但能把「oracle 有效」变成可执行契约。
- 建议:为该模块补一组 py_test:对 regex / json_schema / structural_tag 各给出至少一条应通过与一条应失败的样本(含空字符串、被
- grammar smoke 覆盖轴与 fixture 注释不符,reasoning + 并发 零覆盖且引用不存在的用例与实现 @
rtp_llm/test/smoke/data/model/qwen35/q_r_mtp_grammar_reasoning.json:5- 建议:补上 thinking ON +
concurrency_test = True的 grammar 用例(多流共享编译缓存与 per-stream reasoning 状态正是最易出错处),并让并发用例真正带上 MTP 参数或重命名数据文件;否则请修正两个_comment:删除不存在的配套用例与实现文件引用、删除「actual→expect 回填」的错误描述,并显式写明当前未覆盖的轴组合及原因。同时删除已不参与比对的 golden 正文,或按上一条恢复确定性字段比对。
- 建议:补上 thinking ON +
- tokenizer 家族探测函数在单测中被整体 mock,最易出错的归类逻辑零直接覆盖 @
rtp_llm/config/test/grammar_tokenizer_info_test.py:73- 建议:补充针对探测函数本身的单测:用最小 fake 覆盖
_has_tiktoken_encoding(伪造__module__="tiktoken.core"的内部对象)、_has_tiktoken_vocab_file、_has_mergeable_ranks与_iter_sentencepiece_candidates的三条属性路径,以及三类都不命中的否定用例;并为仓内既有的 tiktoken 系与 sentencepiece 系 tokenizer 各加一条不 mock 探测函数的归类断言,确保Unsupported tokenizer type不会在存量模型上误触发。
- 建议:补充针对探测函数本身的单测:用最小 fake 覆盖
- 新增 config_pickle_test 目标缺少运行期依赖,pickle 布局迁移的唯一回归测试很可能加载失败 @
rtp_llm/cpp/pybind/BUILD:11- 建议:对齐
rtp_llm/utils/test/BUILD的既有写法:data补上//:rtp_compute_ops(必要时含//:th_transformer)、补deps/exec_properties;或直接改为from rtp_llm.ops import GrammarConfig(该模块已导出该符号并会完成 torch 与 libpython 预加载)。合并前请本地实跑一次该 py_test 目标,确认可运行而非仅构建成功。
- 建议:对齐
- vendored dlpack 头文件版本与 BUILD 注释不符且 include 目录全局传播 @
3rdparty/dlpack/BUILD:6- 建议:先修正注释为实际版本;并评估与 torch 内置 dlpack 对齐(优先直接复用 torch 提供的头文件),或改用
strip_include_prefix/ 私有 target 把该 include 目录只暴露给 xgrammar 编译单元,避免全仓 TU 受包含顺序影响。同时随附 dlpack 的 LICENSE 与来源 commit 说明,便于开源合规追溯。
- 建议:先修正注释为实际版本;并评估与 torch 内置 dlpack 对齐(优先直接复用 torch 提供的头文件),或改用
- xgrammar 静态库被同时链入两个同进程加载的 Python 扩展且无符号隔离 @
BUILD:109- 建议:给
//:th_grammar_tokenizer_info增加符号本地化(-fvisibility=hidden或版本脚本 /--exclude-libs),或把xgrammar_tokenizer_info收窄为只依赖 xgrammar 的 tokenizer-info 子集,避免整库进入第二个扩展;若确认当前 dlopen 语义下重复是安全的,请在grammar/BUILD或根BUILD注释中写清依据(加载标志、无跨 .so 对象传递),并补一条同时导入两个扩展的 py 级断言。
- 建议:给
- grammar BUILD 注释声明的 Tier 依赖约束未通过 visibility 落地 @
rtp_llm/cpp/engine_base/grammar/BUILD:35- 建议:把
:grammar_xgrammar与:xgrammar_tokenizer_info的 visibility 收紧为显式白名单(如//rtp_llm/cpp/engine_base/grammar:__subpackages__、//rtp_llm/cpp/models/logits_processor:__pkg__及所需 test package),只让:grammar_cpp_lib保持 public;同时把包级default_visibility从 public 收回,使注释中的 Tier 约束由 Bazel 强制。若暂不收紧,请在注释中说明原因与收紧时间点。
- 建议:把
- encoded_vocab 允许 bytes,但 ops 边界声明为 List[str],跨语言类型契约自相矛盾 @
rtp_llm/ops/__init__.py:194- 建议:把包装与
.pyi的入参统一为Sequence[Union[str, bytes]]/list[str | bytes],并在.pyi与PyGrammarTokenizerInfo.cc注释中写明 bytes 与 latin-1 的往返约定,保持 Python 声明与 C++ 实际接受的类型一致。
- 建议:把包装与
- 新增 GRAMMAR_ 错误码未指定 ExceptionCategory,用户侧原因被一律归类为 internal* @
rtp_llm/config/exceptions.py:45- 建议:至少为 608/609 标注
ExceptionCategory.INVALID_OUTPUT,611/612 视语义考虑UNSUPPORTED,610/613 保留 INTERNAL;并在注释中补一句「数值与ErrorCode.h保持一致,category 为 Python 侧对外分类、不参与跨语言同步」,避免后续同步时误改。
- 建议:至少为 608/609 标注
P3
- GrammarConfig pickle 仅靠首元素类型区分新旧布局,缺少显式版本标记 @
rtp_llm/cpp/pybind/ConfigInit.cc:1346- 建议:在元组首位引入显式版本号(如
("v2", ...)),__setstate__按版本号分派并对未知版本抛明确错误;保留现有isinstance分支仅用于识别历史 legacy 布局,并在注释中写明「新增/删除字段必须递增版本号」。
- 建议:在元组首位引入显式版本号(如
- 契约注释与实现不一致:BitmaskUtils 声明 abort 实际抛异常 @
rtp_llm/cpp/models/logits_processor/BitmaskUtils.h:30- 建议:把
BitmaskUtils.h:30的注释改为「throw(除非开启 core-dump 配置)」,并在BaseLogitsProcessor.h的虚函数注释中写清「实现可对调用方违约抛异常,仅业务级失败走 ErrorInfo」,使两类失败通道的边界显式。
- 建议:把
- response_format 解析失败时异常链断裂 @
rtp_llm/config/response_format_builder.py:194- 建议:在 :194-198 分支补上
from e,与相邻RecursionError分支保持一致。
- 建议:在 :194-198 分支补上
- 新增测试的断言强度不足,默认值与拒绝路径用例近乎恒真 @
rtp_llm/test/generate_config_test.py:553- 建议:把
_assert_rejected拆成「仅 validate」与「仅 apply」两条独立断言,并用assertRaisesRegex锁定具体拒绝原因;test_grammar_defaults改为断言具体常量(num_workers == 8、compiler_cache_bytes == 512 * 1024 * 1024、constrained_json_disable_any_whitespace is False),使默认值调整必须显式改测试。
- 建议:把
- CudaMaskLogitsOpTest 保留 MYDEBUG 调试打印且用浮点 == 直接断言 @
rtp_llm/models_py/bindings/cuda/ops/tests/CudaMaskLogitsOpTest.cc:48- 建议:删除 MYDEBUG 打印(如确需保留,收敛到日志级别开关后再打印);断言改为
EXPECT_FLOAT_EQ或EXPECT_EQ以便失败时输出具体数值,-FLT_MAX等哨兵值可继续用精确比较但建议加<< "idx=" << i的失败上下文。
- 建议:删除 MYDEBUG 打印(如确需保留,收敛到日志级别开关后再打印);断言改为
- xgrammar.BUILD 的 nanobind exclude 永不生效且版本注释无法核对 @
3rdparty/xgrammar/xgrammar.BUILD:38- 建议:删除无效的 exclude(或改为真实需排除的路径),并把 :1 注释改为「upstream v0.2.2 + <fork 相对上游的补丁清单> @ commit 」,与前述依赖治理建议一并落地。
Checklist Violations (25 fail / 58 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
grammar BUILD 注释声明的 Tier 依赖约束未通过 visibility 落地
:grammar_xgrammar上方注释写明「xgrammar-specific impl; only Tier 0 (this package) and Tier 1 (logits_processor) may depend」(:35),但该 package 顶部是package(default_visibility = ["//visibility:public"])(:4),:grammar_cpp_lib又显式visibility = ["//visibility:public"](:64)。因此任意 target 都可直接依赖 xgrammar 实现库,注释描述的分层约束没有任何构建期强制——而本 PR 的分层设计(Tier 0 实现 / Tier 1 消费)正是避免 xgrammar 符号扩散到全仓的关键。 - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
vendored dlpack 头文件版本与 BUILD 注释不符且 include 目录全局传播
BUILD 注释写「We vendor the v0.8 header verbatim」,但新增的3rdparty/dlpack/include/dlpack/dlpack.h定义DLPACK_MAJOR_VERSION 1/DLPACK_MINOR_VERSION 2(:19、:22),并包含DLPackVersion、DLManagedTensorVersioned与 DLPack Python C-API,属 v1.x。该 target 以includes = ["include"]在 public 默认可见性下导出(:1、:7-11),并经@xgrammar//:xgrammar_headers传递到logits_processor → models → engine_base/stream全链路;torch 自带的dlpack.h使用同一 include guardDLPACK_DLPACK_H_(:6-7),同一 TU 内最终生效的定义由包含顺序决定。 - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
grammar BUILD 注释声明的 Tier 依赖约束未通过 visibility 落地
:grammar_xgrammar上方注释写明「xgrammar-specific impl; only Tier 0 (this package) and Tier 1 (logits_processor) may depend」(:35),但该 package 顶部是package(default_visibility = ["//visibility:public"])(:4),:grammar_cpp_lib又显式visibility = ["//visibility:public"](:64)。因此任意 target 都可直接依赖 xgrammar 实现库,注释描述的分层约束没有任何构建期强制——而本 PR 的分层设计(Tier 0 实现 / Tier 1 消费)正是避免 xgrammar 符号扩散到全仓的关键。 - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
新增 GRAMMAR_* 错误码未指定 ExceptionCategory,用户侧原因被一律归类为 internal
608-613 六个错误码均未指定ExceptionCategory(:45-50),按__new__默认(:19)落到ExceptionCategory.INTERNAL。下游用exception_type.category决定对外错误规格与指标聚合,因此GRAMMAR_PARSER_REJECTED_TOKEN(608) /GRAMMAR_NON_EOS_AFTER_TERMINAL(609) 这类「模型输出不满足语法」的失败,与GRAMMAR_VOCAB_EXCEEDS_MODEL_VOCAB(611) 这类引擎/模型配置错误在外部完全同形;同类语义的OUT_OF_VOCAB_RANGE(8101) 已有INVALID_OUTPUT分类可复用。 - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
语法编译在请求路径同步执行,无规模上限与超时,耗时仅 DEBUG 可见且无指标
compile()在createMatcherFromKey内同步执行CompileJSONSchema/CompileRegex/CompileGrammar/CompileStructuralTag(:131-152),而该调用位于LogitsProcessorFactory::createLogitsProcessors(:122)即每个 GenerateStream 的构造路径上。对入参key_string既无长度/复杂度上限也无超时;缓存未命中时(复杂 schema、regex 灾难性构造)编译时间完全由用户输入决定,可能阻塞请求接入线程。logCompileResult在成功路径只写RTP_LLM_LOG_DEBUG(:44-50),生产日志级别下编译耗时与产物字节数均不可见,也没有任何 metric 可用于容量规划或限流。 - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
GrammarConfig pickle 仅靠首元素类型区分新旧布局,缺少显式版本标记
__setstate__先校验t.size() != 5(:1347),再用py::isinstance<py::str>(t[0])判断是否为 legacy 布局(首元素为已删除的grammar_backend字符串),否则按当前布局解析(:1351-1369)。新旧两种布局长度相同,判别完全依赖首字段类型的偶然差异:一旦未来在首位新增字符串字段,或再删一个字段使长度再次为 5,反序列化会静默错位并把错误值写入配置。config_pickle_test.py覆盖了当前两种布局与伪造短元组,但无法防住这类未来演进。 - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
契约注释与实现不一致:BitmaskUtils 声明 abort 实际抛异常
BitmaskUtils.h:30注释写「Out-of-range token_id is a caller bug; abort instead of returning an all-disabled row」,而forceTokenInBitmask(BitmaskUtils.cc:54-58)实际经RTP_LLM_CHECK_WITH_INFO → myAssert抛异常(仅在开启 core-dump 配置时才 abort),注释与行为不一致;BaseLogitsProcessor.h新增的std::optional<ErrorInfo>通道也未覆盖这些 CHECK 类路径,读者无法从注释判断该失败会终止进程还是向上抛。 - [6.1] Quality — Commit 原子、message 与行为匹配 → issue
xgrammar.BUILD 的 nanobind exclude 永不生效且版本注释无法核对
srcs = glob(["cpp/*.cc", "cpp/support/*.cc"], exclude = ["cpp/nanobind/**"])(:33-42)——两个 glob 模式都只匹配单层目录,cpp/nanobind/**不可能命中其中任何路径,该 exclude 为无效配置。同时 :1 注释声明「xgrammar v0.2.2」,而实际 pin 的是个人 fork 的某个 commit(repositories.bzl:8),两者之间没有可验证的对应关系,读者无从判断该 fork 相对上游的改动范围。 - [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue
CudaMaskLogitsOpTest 保留 MYDEBUG 调试打印且用浮点 == 直接断言
三处用例均在 kernel 调用前后各插入一次printBufferData(logits, "MYDEBUG_BEFORE_MASK_LOGITS")/"MYDEBUG_AFTER_MASK_LOGITS"(:48-50、:78-80、:108-110),把整块 logits 打印到测试输出,属遗留调试痕迹;同时断言使用ASSERT_TRUE(logits_ptr[i] == expect_vec[i])(:64)对 float 做精确相等比较,失败时只报false而不给出 expected/actual 数值,定位成本高。 - [6.1] Quality — 逻辑变更未混入无关格式化 → issue
CudaMaskLogitsOpTest 保留 MYDEBUG 调试打印且用浮点 == 直接断言
三处用例均在 kernel 调用前后各插入一次printBufferData(logits, "MYDEBUG_BEFORE_MASK_LOGITS")/"MYDEBUG_AFTER_MASK_LOGITS"(:48-50、:78-80、:108-110),把整块 logits 打印到测试输出,属遗留调试痕迹;同时断言使用ASSERT_TRUE(logits_ptr[i] == expect_vec[i])(:64)对 float 做精确相等比较,失败时只报false而不给出 expected/actual 数值,定位成本高。 - [6.1] Software Engineering — DIP:高层策略不依赖非必要具体细节 → issue
grammar backend 使用函数内可变静态变量,init 与 createLogitsProcessors 之间缺少同步
grammarBackend()返回函数内static std::shared_ptr<XGrammarBackend>的非 const 引用(:66-74),init()(:79)对其整体赋值,createLogitsProcessors(:106)在每个请求的 GenerateStream 构造路径上读取它,两者之间没有互斥或原子语义(shared_ptr的并发赋值与拷贝构造是数据竞争)。注释只声明了「每个 rank 一个进程、一个 engine」的前提,未覆盖LogitsProcessorFactory::init实际存在两处调用点(NormalExecutor.cc:129与MtpExecutor.cc:329)、warm-up executor 与正式 executor 先后 init 的情形;一旦某个 executor 在另一 engine 的流仍在构造时重新 init,就会同时命中写与读。 - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
xgrammar 静态库被同时链入两个同进程加载的 Python 扩展且无符号隔离
新增cc_binary(name = "th_grammar_tokenizer_info", linkshared = 1)(:108-119)依赖grammar:grammar_tokenizer_info_python → :xgrammar_tokenizer_info → @xgrammar//:xgrammar;同时models/BUILD的:logits_processor依赖grammar:grammar_cpp_lib → grammar_xgrammar → @xgrammar//:xgrammar,该目标又被//:rtp_compute_ops/libth_transformer链接。xgrammar.BUILD:56为linkstatic = True且 copts 未加-fvisibility=hidden,新扩展也未设符号本地化 linkopts。而rtp_llm/ops/__init__.py会在同一进程内先后导入libth_grammar_tokenizer_info与 - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
xgrammar.BUILD 的 nanobind exclude 永不生效且版本注释无法核对
srcs = glob(["cpp/*.cc", "cpp/support/*.cc"], exclude = ["cpp/nanobind/**"])(:33-42)——两个 glob 模式都只匹配单层目录,cpp/nanobind/**不可能命中其中任何路径,该 exclude 为无效配置。同时 :1 注释声明「xgrammar v0.2.2」,而实际 pin 的是个人 fork 的某个 commit(repositories.bzl:8),两者之间没有可验证的对应关系,读者无从判断该 fork 相对上游的改动范围。 - [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue
解码路径失败退化为无约束,与 MTP 路径强制 EOS 的 fail-safe 极性相反且被缓存放大
DecodeMaskBuilder::apply在buildState失败(:286-289)及两处catch(:294-299)中都置device_mask_state_ = finishedState(...),而applyDeviceMaskState对FINISHED直接return OkStatus()(:411-412)——即不施加任何 mask,logits 完全无约束;紧随的applyDeviceMaskState(...)(:288)返回值被丢弃且在 FINISHED 下恒为 OkStatus,是死代码。该状态还会被缓存:下一次process()若committed_output_len_未变即命中 :280-283 缓存分支,返回 OkStatus 且不加 mask,而 matcher 本身并未markFinished()。相对地 MTP 路径failSpecVerifyRow(:92-96)、validateSpecVerifyMatcher、`verifySpecDra - [6.1] Software Engineering — OCP:本地扩展点优先于修改中心逻辑 → issue
response_format 的判定与投影存在多份平行实现,OpenAI wire 模型对象被漏检
validate()依赖_has_grammar_constraint()→_response_format_is_grammar()拦截「grammar + beam search / num_return_sequences > 1」,但该函数只识别None、config 侧ResponseFormat(generate_config.py:19 导入)、str、dict四种形态(:40-53)。而openai_endpoint.py:199把request.response_format原样赋给config.response_format,其类型是api_datatype.py:162本地定义的ResponseFormat(该定义遮蔽了同文件 :8 对 config 类的导入),pydantic 已按 :191 的 Union 解析为该类实例:四个分支全不命中并返回 False,Python 侧拦截失效。真正权威的判定是parse_response_format+ `GrammarCo - [6.1] Software Engineering — SRP:模块/类职责单一 → issue
grammar backend 使用函数内可变静态变量,init 与 createLogitsProcessors 之间缺少同步
grammarBackend()返回函数内static std::shared_ptr<XGrammarBackend>的非 const 引用(:66-74),init()(:79)对其整体赋值,createLogitsProcessors(:106)在每个请求的 GenerateStream 构造路径上读取它,两者之间没有互斥或原子语义(shared_ptr的并发赋值与拷贝构造是数据竞争)。注释只声明了「每个 rank 一个进程、一个 engine」的前提,未覆盖LogitsProcessorFactory::init实际存在两处调用点(NormalExecutor.cc:129与MtpExecutor.cc:329)、warm-up executor 与正式 executor 先后 init 的情形;一旦某个 executor 在另一 engine 的流仍在构造时重新 init,就会同时命中写与读。 - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
grammar smoke 覆盖轴与 fixture 注释不符,reasoning + 并发 零覆盖且引用不存在的用例与实现
该 fixture 的_comment声称与q_r_mtp_grammar_reasoning_concurrent.json(thinking ON + 并发)配对,并引用GrammarManager.cc:473-474说明历史 bug;全仓检索确认该 JSON 与GrammarManager.cc均不存在(本 PR 实现文件是XGrammarBackend.cc/GrammarLogitsProcessor.cc)。suites_h20_oss.bzl:263-290新增的smoke_h20_grammar_heavy只有两个用例:qwen35_grammar_concurrent_no_mtp(--think_mode 0+concurrency_test = True,消费q_r_mtp_grammar.json但 smoke_args 中并无任何 MTP 参数)与qwen35_grammar_pd_mtp_reasoning(--think_mode 1,无并发)。即注释自认最高风险 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
新增测试的断言强度不足,默认值与拒绝路径用例近乎恒真
_assert_rejected(:551-556)把cfg.validate()与ResponseFormatBuilder(cfg).apply()放进同一个assertRaises(FtRuntimeException)块:validate()抛异常时apply()根本不执行,无法区分校验发生在哪一层,把校验在两层间迁移也不会被发现(同写法见 :1039/:1052/:1061 附近)。另server_args_test.py:423-435的test_grammar_defaults以type(g)()的 C++ 默认值为期望,而grammar_group_args.py:11/19/33的 argparsedefault又取自同一个grammar_config实例,两侧同源:ConfigModules.h:375-380的默认值(num_workers=8、compiler_cache_bytes=512MiB)被误改时该测试仍然通过。 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
grammar smoke 覆盖轴与 fixture 注释不符,reasoning + 并发 零覆盖且引用不存在的用例与实现
该 fixture 的_comment声称与q_r_mtp_grammar_reasoning_concurrent.json(thinking ON + 并发)配对,并引用GrammarManager.cc:473-474说明历史 bug;全仓检索确认该 JSON 与GrammarManager.cc均不存在(本 PR 实现文件是XGrammarBackend.cc/GrammarLogitsProcessor.cc)。suites_h20_oss.bzl:263-290新增的smoke_h20_grammar_heavy只有两个用例:qwen35_grammar_concurrent_no_mtp(--think_mode 0+concurrency_test = True,消费q_r_mtp_grammar.json但 smoke_args 中并无任何 MTP 参数)与qwen35_grammar_pd_mtp_reasoning(--think_mode 1,无并发)。即注释自认最高风险
RTP-LLM Checklist
- [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue
GrammarConfig 保留无消费者的 tokenizer_info_json / override_stop_tokens,与 ModelConfig 同名字段形成双源
生产链路只有一条:base_model.py:324写入model_config.tokenizer_info_json,LogitsProcessorFactory.cc:79读取model_config.tokenizer_info_json;XGrammarBackend::create里的GrammarConfig只被读compiler_cache_bytes(:102-108)。全仓 grep 确认无任何代码读写grammar_config.tokenizer_info_json与override_stop_tokens,但两者仍通过ConfigInit.cc:1326/1328暴露为 Python 可读写属性、占用 pickle 元组第 3/4 位、出现在ConfigModules.cc:392-393的to_string()与libth_transformer_config.pyi:548,并有config_pickle_test.py:23/31/40为其断言往返。结果是同一 - [I] 代码质量 — 同一功能用统一工具函数 → issue
grammar_constraint_only 校验存在静默放行路径,且完全短路 golden 比对
compare_result(:249-251)一旦读到grammar_constraint_only,调用_validate_grammar_constraint后直接return,跳过 usage / aux_info / extra_outputs / golden 全部比对;_validate_grammar_constraint自身还有两条静默返回:if self.is_stream: return(:230-231)与if not response_format: return(:232-234)。当前新增 fixture 均为stream: false且约束写在query.response_format,故静默放行是潜在风险(未来用例声明 stream 或改放约束位置即零断言变绿);而「短路 golden」是已生效的现状:finish_reason/usage/aux_info一律不校验,例如 grammar 用例因length截断而非stop结束不会被发现。同 PR 的 `dash_gr
Python Static-First Checklist
- [P.B] 错误处理 — 异常链用 raise X from Y → issue
response_format 解析失败时异常链断裂
同一函数内except RecursionError as e分支使用了raise FtRuntimeException(...) from e(:189-193),紧邻的except (JSONDecodeError, ValidationError, TypeError) as e分支却未加from e(:194-198)。pydanticValidationError的结构化明细(字段路径、失败原因)只存在于原始异常对象上,丢失__cause__后日志里只剩str(e)拼接的单行信息,排查用户 schema 报错时无法回溯具体校验位置。 - [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
tokenizer 家族探测函数在单测中被整体 mock,最易出错的归类逻辑零直接覆盖
build_tokenizer_info辅助方法同时mock.patch.object了_is_fast_tokenizer、_is_tiktoken_tokenizer、_is_sentencepiece_tokenizer、_is_byte_level_tokenizer(:73-92),因此 fast/tiktoken/sentencepiece 三条分支的用例只验证 metadata 组装,未验证「给定真实形态的 tokenizer 能否被正确归类」;全文件仅_is_byte_level_tokenizer与_build_encoded_vocab有直接用例。而归类逻辑正是最易出错处:_has_tiktoken_encoding依赖type(inner).__module__前缀、_has_tiktoken_vocab_file依赖vocab_files_names["vocab_file"]含"tiktoken"子串、_iter_sentencepiece_candidates依赖三条属 - [P.G] 测试规范 — pytest.raises 带 match 参数 → issue
新增测试的断言强度不足,默认值与拒绝路径用例近乎恒真
_assert_rejected(:551-556)把cfg.validate()与ResponseFormatBuilder(cfg).apply()放进同一个assertRaises(FtRuntimeException)块:validate()抛异常时apply()根本不执行,无法区分校验发生在哪一层,把校验在两层间迁移也不会被发现(同写法见 :1039/:1052/:1061 附近)。另server_args_test.py:423-435的test_grammar_defaults以type(g)()的 C++ 默认值为期望,而grammar_group_args.py:11/19/33的 argparsedefault又取自同一个grammar_config实例,两侧同源:ConfigModules.h:375-380的默认值(num_workers=8、compiler_cache_bytes=512MiB)被误改时该测试仍然通过。 - [P.H] 类型标注 — 输入参数用 Sequence/Mapping/Iterable,返回值用具体类型 → issue
encoded_vocab 允许 bytes,但 ops 边界声明为 List[str],跨语言类型契约自相矛盾
grammar_tokenizer_info.py:4定义VocabToken = Union[str, bytes],_build_encoded_vocab返回List[VocabToken]并原样传给serialize_grammar_tokenizer_info;grammar_tokenizer_info_test.py的test_native_serializer_accepts_raw_bytes_vocab明确以 bytes vocab 调用原生扩展,PyGrammarTokenizerInfo.cc也同时处理 str/bytes,证明 bytes 是被支持的真实输入。但rtp_llm/ops/__init__.py:193-194的包装声明encoded_vocab: List[str],libth_grammar_tokenizer_info.pyi:5同样声明list[str],静态检查会把生产用法判为类型错误,真实契约(含 latin-1 往返约定)无处可查。
Strengths
- 错误语义显式化并逐流收敛:
process/updateStatus/prepareSpeculative统一返回ErrorInfo/ErrorResult,LogitsProcessorStates::batchProcess按 interval 归属到具体行,SamplerOutput::processor_errors保证单流失败不影响同批其他流,替代了原void接口下的静默失败。 - 提交语义具备原子性:
acceptCommittedLocked用rollback_commit闭包同时回滚 matcher token 数、committed_output_len_与缓存 mask,回滚失败降级markFinished()防半提交;配套UpdateStatusRollsBackEntireRejectedBatch单测。 prepareSpeculative用ProvisionalSpecAcceptTracker保证成功/失败都回滚到 committed 状态,投机校验不污染主状态机;applySpecVerifyResult的 cap 语义与SpeculativeSampler「末位必为 target token」约定严格对齐。- 异步 H2D 生命周期显式管理:
waitForPendingBitmaskUploads/waitForPendingHostUploads在复用 pinned host buffer 前同步 event,注释明确「正确性不得依赖后续 sampler 的 D2H 同步」。 - MTP mask 只为活跃流分配紧凑行(
buildActiveStreamLayout+logits_row_indices),避免按全 batch 分配[B*(P+1), V/32],并有行数与行号映射断言。 - grammar 词表小于模型词表时,
clearBitmaskTokenRange与applyDeviceMaskState尾部fill_(neg_inf)双路径截断;单字/跨字/end_bit==0三类边界均有单测。 compileWithErrorClassification按bad_alloc/runtime_error/其他分类为ResourceExhausted/InvalidArgument/Unknown,并在头文件写明契约;logCompileResult记录耗时、字节数与 invalid 标记。- 掩码常量统一为
BaseLogitsProcessor::neg_inf(原-INFINITY),避免全掩码行 softmax 产生 NaN;gatherSpecSamplerInput补finished_mask.zero_()修复 MTP 下未初始化 mask 被读取的隐患。 - 单向数据流干净:
ResponseFormatBuilder在请求入口一次性把 response_format 投影为 typed 字段,validate_finalized()在 RPC 边界 fail-fast,引擎侧只消费 typed 字段;restore_final_constraint显式保存/恢复约束而不反推 structural-tag AST。 PyGrammarTokenizerInfo.cc完整处理 str/bytes vocab、PySequence_Fast引用计数与错误分支,未让 C++ 异常穿越 CPython 边界;BUILD 把 tokenizer 序列化拆为不含 torch/CUDA 的独立目标,使其单测可在纯 CPU worker 跑真实 native 序列化。- 测试密度高且不用 mock 替代生产边界:
GrammarLogitsProcessorTest用真实XGrammarBackend+ 128 字符词表覆盖 EOS 越界、bitmask 过小、spec cap、provisional 回滚,并在 CUDA 上验证 fp32/fp16/bf16;PackedMaskLogitsCpuFallbackTest独立成 CPU-only 目标;config_pickle_test把 pickle 新旧布局做成可执行契约。
| model_vocab_size=int(self.model_config.vocab_size or 0), | ||
| stop_token_ids=self._collect_tokenizer_info_stop_token_ids(), | ||
| ) | ||
| except Exception as e: |
There was a problem hiding this comment.
[P1] 启动期强制构建 grammar tokenizer 元数据,不受支持的 tokenizer 直接阻塞模型加载且无逃生开关
load_tokenizer 无条件调用 _fill_grammar_tokenizer_info(:301),后者仅在元数据已存在或 tokenizer 为 None 时跳过,任何异常都 raise RuntimeError(:329-332),注释确认这是有意的 fail-fast。而 build_grammar_tokenizer_info_json 仅识别 fast / tiktoken / sentencepiece 三类,其余一律 raise ValueError("Unsupported tokenizer type: ..."),且无条件要求 get_vocab() 可用、stop_token_ids 非空。因此从不使用结构化输出的部署只要 tokenizer 属慢速 WordPiece/BPE 或自定义实现,升级后即启动失败并可能 crash-loop;配合上一条 P1,已无任何 CLI/env 可绕过。对比 C++ 侧 XGrammarBackend::create 在元数据为空时是优雅降级,两侧策略相反。
建议: 把「构建 grammar 元数据」与「引擎可启动」解耦并区分失败类别:stop_token_ids 缺失等可判定的配置错误保留 fail-fast;tokenizer 家族无法识别时降级为 WARNING 并留空 tokenizer_info_json,由已有的 C++ 禁用路径承接(后续 grammar 请求返回明确错误)。若坚持 fail-fast,请新增显式开关(如 grammar_require_tokenizer_info,默认 false)并在 server_args 暴露为运维回滚手段,同时补一条「不支持的 tokenizer → 引擎仍可启动、grammar 请求返回明确错误」的单测,并在发布说明中给出受影响 tokenizer 清单。
| self.restore_final_constraint(self.config, saved_constraint) | ||
| return saved_constraint | ||
|
|
||
| if self.config.in_think_mode and ( |
There was a problem hiding this comment.
[P1] think 模式被无条件转为 grammar 约束解码,think + beam / n>1 由可用变为硬失败且强依赖 backend
apply() 在 in_think_mode 且 has_num_beams() 或 num_return_sequences > 1 时直接抛 ERROR_INPUT_FORMAT_ERROR(:80-87);改动前 thinking 预算由 ThinkModeLogitsProcessor 的 token 计数实现(其 updateMultiSeqStatus/insert 本身支持多序列),故该组合从可用变为硬失败。其次,即使请求完全没有用户约束(constraint is None),只要 in_think_mode 为真也会 _wrap_final_format_with_reasoning_envelope({"type": "any_text"})(:104-105)改写为 structural_tag 信封;openai_endpoint.py:271 的 enable_thinking = bool(generate_env_config.think_mode) 意味着 THINK_MODE 部署下 100%...
建议: 1) 在 PR description / 发布说明中把「think + beam / num_return_sequences>1 不再支持」列为 breaking change 并给出替代方案;2) 提供回退路径:无用户 grammar 时可通过配置继续用 token 计数方式限制 thinking 预算,而非一律构造 any_text 信封;3) 补充 THINK_MODE 开关前后的 TPOT/吞吐对比数据。若确认无条件包裹是唯一实现,请在 apply() 处注释说明为何 thinking 预算必须由 grammar 承担,便于排障时判断该路径能否关闭。
| const bool is_context_stream = stream->isContextStream(); | ||
| // Capability compatibility is a stream admission property, so reject it | ||
| // before either prefill or decode can publish an output token. | ||
| if (auto error = validateMtpCompatibility(stream->getAllLogitsProcessorPtr()); error.has_value()) { |
There was a problem hiding this comment.
[P1] MTP 下非 grammar logits processor 变为请求级硬失败,且 tree_decode_config 启动期抛错,均无开关与迁移路径
prepareStreams 对每条流调用 validateMtpCompatibility(:45-55),任一 processor 的 mtpCapability().mode == UNSUPPORTED 即 reportError 并 continue(流既不进 prefill 也不进 decode);BaseLogitsProcessor::mtpCapability() 默认返回 UNSUPPORTED,全仓仅 GrammarLogitsProcessor.h 覆写,故 Tree/Recommendation/MultiSeq 全部命中。而 LogitsProcessorFactory.cc:136 仍会为 combo_token_size > 0 的请求创建 RecommendationLogitsProcessor:改动前此类请求在 MTP decode 中虽不生效但能正常返回,改动后一律失败。构造函数另新增 `RTP_LLM_CHECK_WITH_INFO(params.sp_config.tree_dec...
建议: 1) 若属有意收紧,在发布说明中列为 breaking change(含「MTP 场景必须 unset TREE_DECODE_CONFIG」),并补真实组合用例(MTP + combo_token_size>0、MTP + num_return_sequences>1、MTP + TREE_DECODE_CONFIG)断言错误码与错误信息;2) 若需保留可用性,为 MtpProcessorCapability 增加「MTP 下可安全忽略」的第三种模式(如 IGNORED_IN_SPEC),让与投机验证无关的 processor 走告警降级而非拒流;3) 至少提供环境变量在灰度期把拒流/启动拒绝降级为告警;4) tree_decode_config 断言抛出前先打一条 ERROR 日志,便于容器反复重启时定位。
|
|
||
| bool bitmaskAllowsToken(const int32_t* bitmask, size_t words, int32_t token_id); | ||
|
|
||
| // Out-of-range token_id is a caller bug; abort instead of returning an all-disabled row. |
There was a problem hiding this comment.
[P3] 契约注释与实现不一致:BitmaskUtils 声明 abort 实际抛异常
BitmaskUtils.h:30 注释写「Out-of-range token_id is a caller bug; abort instead of returning an all-disabled row」,而 forceTokenInBitmask(BitmaskUtils.cc:54-58)实际经 RTP_LLM_CHECK_WITH_INFO → myAssert 抛异常(仅在开启 core-dump 配置时才 abort),注释与行为不一致;BaseLogitsProcessor.h 新增的 std::optional<ErrorInfo> 通道也未覆盖这些 CHECK 类路径,读者无法从注释判断该失败会终止进程还是向上抛。
建议: 把 BitmaskUtils.h:30 的注释改为「throw(除非开启 core-dump 配置)」,并在 BaseLogitsProcessor.h 的虚函数注释中写清「实现可对调用方违约抛异常,仅业务级失败走 ErrorInfo」,使两类失败通道的边界显式。
Checklist: [6.1] 错误语义:fail-fast/retry/fallback/silent 行为显式
| ExceptionType.ERROR_INPUT_FORMAT_ERROR, | ||
| "response_format exceeds the supported JSON nesting depth", | ||
| ) from e | ||
| except (JSONDecodeError, ValidationError, TypeError) as e: |
There was a problem hiding this comment.
[P3] response_format 解析失败时异常链断裂
同一函数内 except RecursionError as e 分支使用了 raise FtRuntimeException(...) from e(:189-193),紧邻的 except (JSONDecodeError, ValidationError, TypeError) as e 分支却未加 from e(:194-198)。pydantic ValidationError 的结构化明细(字段路径、失败原因)只存在于原始异常对象上,丢失 __cause__ 后日志里只剩 str(e) 拼接的单行信息,排查用户 schema 报错时无法回溯具体校验位置。
建议: 在 :194-198 分支补上 from e,与相邻 RecursionError 分支保持一致。
Checklist: [P.B] 异常链用 raise X from Y
|
|
||
| def _assert_rejected(self, exception_type: ExceptionType, **fields): | ||
| cfg = GenerateConfig(**fields) | ||
| with self.assertRaises(FtRuntimeException) as ctx: |
There was a problem hiding this comment.
[P3] 新增测试的断言强度不足,默认值与拒绝路径用例近乎恒真
_assert_rejected(:551-556)把 cfg.validate() 与 ResponseFormatBuilder(cfg).apply() 放进同一个 assertRaises(FtRuntimeException) 块:validate() 抛异常时 apply() 根本不执行,无法区分校验发生在哪一层,把校验在两层间迁移也不会被发现(同写法见 :1039/:1052/:1061 附近)。另 server_args_test.py:423-435 的 test_grammar_defaults 以 type(g)() 的 C++ 默认值为期望,而 grammar_group_args.py:11/19/33 的 argparse default 又取自同一个 grammar_config 实例,两侧同源:ConfigModules.h:375-380 的默认值(num_workers=8、compiler_cache_bytes=512MiB)被误改时该测试仍然通过。
建议: 把 _assert_rejected 拆成「仅 validate」与「仅 apply」两条独立断言,并用 assertRaisesRegex 锁定具体拒绝原因;test_grammar_defaults 改为断言具体常量(num_workers == 8、compiler_cache_bytes == 512 * 1024 * 1024、constrained_json_disable_any_whitespace is False),使默认值调整必须显式改测试。
Checklist: [6.1] 新逻辑有聚焦单测 + 相关集成/smoke 测试;[P.G] pytest.raises 带 match 参数
| printBufferData(logits, "MYDEBUG_AFTER_MASK_LOGITS"); | ||
|
|
||
| auto logits_cpu = logits.cpu().contiguous(); | ||
| auto logits_ptr = logits_cpu.data_ptr<float>(); |
There was a problem hiding this comment.
📍 实际位置 rtp_llm/models_py/bindings/cuda/ops/tests/CudaMaskLogitsOpTest.cc:48(不在 diff 展示范围内,就近挂载)
[P3] CudaMaskLogitsOpTest 保留 MYDEBUG 调试打印且用浮点 == 直接断言
三处用例均在 kernel 调用前后各插入一次 printBufferData(logits, "MYDEBUG_BEFORE_MASK_LOGITS") / "MYDEBUG_AFTER_MASK_LOGITS"(:48-50、:78-80、:108-110),把整块 logits 打印到测试输出,属遗留调试痕迹;同时断言使用 ASSERT_TRUE(logits_ptr[i] == expect_vec[i])(:64)对 float 做精确相等比较,失败时只报 false 而不给出 expected/actual 数值,定位成本高。
建议: 删除 MYDEBUG 打印(如确需保留,收敛到日志级别开关后再打印);断言改为 EXPECT_FLOAT_EQ 或 EXPECT_EQ 以便失败时输出具体数值,-FLT_MAX 等哨兵值可继续用精确比较但建议加 << "idx=" << i 的失败上下文。
Checklist: [6.1] 无 per-forward 调试日志 / 噪声热路径输出;[6.1] 逻辑变更未混入无关格式化
| "cpp/*.cc", | ||
| "cpp/support/*.cc", | ||
| ], | ||
| exclude = [ |
There was a problem hiding this comment.
[P3] xgrammar.BUILD 的 nanobind exclude 永不生效且版本注释无法核对
srcs = glob(["cpp/*.cc", "cpp/support/*.cc"], exclude = ["cpp/nanobind/**"])(:33-42)——两个 glob 模式都只匹配单层目录,cpp/nanobind/** 不可能命中其中任何路径,该 exclude 为无效配置。同时 :1 注释声明「xgrammar v0.2.2」,而实际 pin 的是个人 fork 的某个 commit(repositories.bzl:8),两者之间没有可验证的对应关系,读者无从判断该 fork 相对上游的改动范围。
建议: 删除无效的 exclude(或改为真实需排除的路径),并把 :1 注释改为「upstream v0.2.2 + <fork 相对上游的补丁清单> @ commit 」,与前述依赖治理建议一并落地。
Checklist: [6.1] Commit 原子、message 与行为匹配;[6.1] KISS/YAGNI:无投机性抽象
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1006
Status: BLOCKING
Summary: P0/0 · P1/4 · P2/21 · P3/10
Reviewed: commit 09e572104dec · 2026-08-05 19:11 UTC+8
Blocking Issues
P1
- --grammar_backend / GRAMMAR_BACKEND 无过渡删除,存量启动脚本升级即退出且失去唯一关停开关 @
rtp_llm/server/server_args/grammar_group_args.py:6- 建议:二选一并在 release note 给出迁移说明:(1) 保留一个发布周期的 deprecated no-op
--grammar_backend(解析后打 warning);(2) 新增--grammar_enable/GRAMMAR_ENABLE(默认 true),关闭时跳过build_grammar_tokenizer_info()与XGrammarBackend::create,结构化请求返回明确错误。并在server_args_test.py把旧 flag 的处理方式(接受并忽略、或显式拒绝并提示迁移)固化为断言。
- 建议:二选一并在 release note 给出迁移说明:(1) 保留一个发布周期的 deprecated no-op
- 启动期强制构建 grammar tokenizer 元数据,不支持的 tokenizer 直接阻塞模型加载且无逃生开关 @
rtp_llm/config/grammar_tokenizer_info.py:195- 建议:把「tokenizer 类型不受支持」这类可预期能力缺口降级为
logging.warning+ 返回空串,交由XGrammarBackend::create走已有关闭分支,失败推迟为首个 grammar 请求的明确错误(LogitsProcessorFactory.cc:108-110文案已就绪);stop_token_ids缺失等配置写错类问题可继续 fail-fast。若坚持 fail-fast,必须配合上一条的--grammar_enable开关,并补一条覆盖「不受支持 tokenizer 时引擎启动行为」的启动路径测试。
- 建议:把「tokenizer 类型不受支持」这类可预期能力缺口降级为
- think 模式改为硬依赖 grammar 约束,think+beam/n>1 由可用变报错且直连 gRPC 预算静默失效 @
rtp_llm/config/response_format_builder.py:79- 建议:(1)在
createLogitsProcessors对「in_think_mode && max_thinking_tokens > 0但 grammar_key 为空」显式 fail-fast 返回ErrorInfo,避免直连 gRPC 静默降级,并补 QueryConverter/GenerateStream 层用例;(2)think+beam/n>1 的能力回退需明示:或提供 DFA 回退实现(配合服务级开关),或在发布说明中作为 breaking change 列出并给出替代方案;(3)补一条「in_think_mode 且无 response_format」的端到端用例,断言错误码或约束生效行为稳定。
- 建议:(1)在
- MTP 能力校验默认拒绝所有未声明 processor,tree_decode_config 变为启动期抛异常 @
rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:47- 建议:从「静默不生效」改为「显式报错」方向正确,但用基类默认值把「未声明」等同「不支持」属对既有子类契约的隐式收紧:请为确实兼容 MTP 的 processor(如 Recommendation)显式声明能力,而非依赖默认值;构造函数硬断言改为启动配置校验阶段返回结构化错误,避免异常从构造函数逃逸;在发布说明列出受影响组合(
combo_token_size>0、num_return_sequences>1、TREE_DECODE_CONFIG+MTP)与迁移方式,并补回归用例断言错误码稳定。
- 建议:从「静默不生效」改为「显式报错」方向正确,但用基类默认值把「未声明」等同「不支持」属对既有子类契约的隐式收紧:请为确实兼容 MTP 的 processor(如 Recommendation)显式声明能力,而非依赖默认值;构造函数硬断言改为启动配置校验阶段返回结构化错误,避免异常从构造函数逃逸;在发布说明列出受影响组合(
Non-blocking Suggestions
P2
- proto 字段 73 删除未补 reserved,与同文件既有惯例不一致且留下 wire 号复用隐患 @
rtp_llm/cpp/model_rpc/proto/model_rpc_service.proto:138- 建议:补
reserved 73;与reserved "response_format";,与 :103-104 保持一致;在 PR description / release note 写明升级顺序(frontend 先于 engine)。并在model_rpc_client_test.py增补一例:构造仅含旧字段号的原始字节串交给新版GenerateConfigPB解析,断言其落入 unknown fields 且不影响 typed 字段解析,把兼容契约变成可执行断言。
- 建议:补
- grammar 编译失败的状态码在 factory 边界被压平为 INVALID_PARAMS,丢失可重试语义 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:120- 建议:按
matcher_or.status().code()分派:kInvalidArgument→INVALID_PARAMS,kResourceExhausted→MALLOC_FAILED或既有可重试码,其余 →EXECUTION_EXCEPTION;并让「服务端 grammar 未就绪」与「用户语法非法」在错误信息上可区分。补一条单测断言「编译期 ResourceExhausted 不会被上报为 INVALID_PARAMS」。
- 建议:按
- 编译异常分类把 logic_error 标为可重试,backend 初始化失败与编译耗时仅有日志、无指标与规模上限 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:28- 建议:把
std::logic_error(含invalid_argument/length_error)显式归入InvalidArgumentError,仅保留真正未知异常给UnknownError,并补一条 logic_error 类语法错误的分类断言(与已有CompileMalformedJsonSchemaIsInvalid形成完整覆盖);为「backend 初始化失败」与「编译耗时/失败率、缓存命中率」增加可告警指标(而非仅日志),并对超大 schema 设置尺寸上限或编译超时,为高延迟编译提供运维可见性与限流依据。
- 建议:把
- SpecLogitsVerifyRunner::run 的数据相关校验异常逃出 catch,失败爆炸半径与 fail-closed 设计不一致 @
rtp_llm/cpp/models/logits_processor/SpecLogitsVerifyRunner.cc:234- 建议:把数据相关校验改为返回
makeFailureResultForActiveStreams(...)的显式错误分支,或在 catch 末尾增加catch (const std::exception&)兜底(保留 bad_alloc/c10::Error 的精细映射在前);真正的编程期不变量(B/P/V>0、int32 容量)可保留断言但应与数据相关校验分开表达。相应把MtpExecutorTest的断言从「抛异常」改为「返回 fail-closed 结果且 processor_errors 携带明确错误码」。
- 建议:把数据相关校验改为返回
- decode 与 MTP 两条 grammar mask 路径重复实现,decode 侧逐流 pinned 分配与同步 H2D 落在采样路径 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:268- 建议:抽取 decode 与
SpecLogitsVerifyRunner共用的 batched mask apply 工具,或把 decode 收敛到「compact 行 + 行索引」形态,在LogitsProcessorStates::batchProcess层聚合本 step 全部 grammar 流后一次上传;pinned/GPU 暂存区上移为批级/引擎级共享池并把分配移到流创建或预热阶段。若改用non_blocking=true,需配合record_stream()/event 保护复用;若保持同步拷贝,请在该行写明这一不变量,并给出并发 grammar 流的 decode 吞吐对比数据以量化收益。
- 建议:抽取 decode 与
- 非 CUDA 构建下 pin_memory() 抛异常,grammar 解码在 CPU/ARM 目标上不可用且零覆盖 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:236- 建议:用
#if USING_CUDA(或运行期at::cuda::is_available())包住.pin_memory()/pinned_memory(true),非 CUDA 退化为普通 CPU 张量(CPU kernel 不需要 pinned);或在 BUILD 中把 grammar 依赖收敛到加速器配置并对 CPU 配置给出明确的「不支持」路径。补一个不依赖 GPU 的DecodeMaskBuilderCPU 用例(可参考PackedMaskLogitsCpuFallbackTest的组织方式)做回归守护。
- 建议:用
- grammar backend 存放在函数内可变静态变量,init 重复构建且无同步与隔离手段 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:66- 建议:把 backend 所有权移到 Engine/Executor(放入运行时上下文或
Executor成员),由createLogitsProcessors以参数接收,消除进程级静态状态、避免重复构建,并让多引擎与单测场景可隔离。若短期保留现状,至少让init()在tokenizer_info_json未变化时跳过重建,并在注释中明确「仅允许启动阶段调用一次」。
- 建议:把 backend 所有权移到 Engine/Executor(放入运行时上下文或
- response_format 为 text 或解析失败时静默清空调用方显式设置的 typed 约束 @
rtp_llm/config/response_format_builder.py:221- 建议:当
constraint is None且被清空的 typed 字段原本非空时,至少记一条限频 warning(说明 response_format 优先并列出被丢弃字段名),或直接抛ERROR_INPUT_FORMAT_ERROR让调用方感知冲突;统一""/{}/"text"三者对 typed 字段的处理语义并写入 docstring;JSON 解析失败分支建议保留 typed 字段或直接报错,而非静默降级为 text。
- 建议:当
- has_bounded_region 只检查 max_tokens,漏检 max_chars 导致守卫可被绕过 @
rtp_llm/config/grammar_constraint.py:56- 建议:把判定扩展为
any(current.get(k) is not None for k in ("max_tokens","max_chars")),并把字段名元组抽为模块级常量便于跟随 xgrammar 能力演进;在generate_config_test.py现有 bounded-region 用例旁补一条只带max_chars的 structural_tag 用例,断言in_think_mode下被拒绝。若确认所 pin 版本不支持max_chars,请修正repositories.bzl注释避免误导。
- 建议:把判定扩展为
- legacy structures/triggers 形态的 structural_tag 在 think 模式下必然失败且错误文案误导 @
rtp_llm/config/grammar_constraint.py:143- 建议:要么在
_structural_tag_format_node中支持 legacystructures/triggers形态(转换为等价format节点后再包信封),要么把错误信息改为明确的能力声明,例如「thinking 模式暂不支持 legacy structures/triggers 形态的 structural_tag,请改用 format 形态」;并补一条 legacy 形态 +in_think_mode的用例把预期行为固化。
- 建议:要么在
- 新增 GRAMMAR_ 错误码全部落入 INTERNAL 分类,与错误语义不匹配* @
rtp_llm/config/exceptions.py:45- 建议:按语义显式标注分类:608/609 用
ExceptionCategory.INVALID_OUTPUT;611/612 可保持 INTERNAL 但在注释写明「部署期配置错误、不可重试」,并注明这批码位于「8000 以上可重试」区间之外,避免后续维护者按码段推断语义。
- 建议:按语义显式标注分类:608/609 用
- ThinkModeLogitsProcessor 沦为生产不可达代码,max_batch_size 形参失效,文档与实现不符 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:99- 建议:在本 PR 或紧随的清理 PR 中删除
ThinkModeLogitsProcessor及其 BUILD/测试条目,移除失效的max_batch_size形参与调用点实参,并同步更新docs/backend/logits_processor.md与dash_sc/think.py注释;若计划保留作为 think 回退实现,则必须有实际装配点或明确开关/TODO,避免长期存在「编译通过但永不执行」的状态机代码。
- 建议:在本 PR 或紧随的清理 PR 中删除
- tokenizer 家族探测函数在单测中被整体 mock,最易出错的分派链零直接覆盖 @
rtp_llm/config/test/grammar_tokenizer_info_test.py:73- 建议:为三个探测函数补直接用例:tiktoken Encoding /
vocab_files_names['vocab_file']含 tiktoken / 仅有mergeable_ranks;sp_model、tokenizer.sp_model、tok三个 sentencepiece 挂载点;_has_sentencepiece_api缺任一方法;_get_hf_tokenizer_json在backend_tokenizer.to_str缺失时抛 ValueError。并保留至少一条只 mockserialize_grammar_tokenizer_info、不 patch 探测函数的用例,断言分派优先级(同时具备 fast 与 sentencepiece 特征时应走 fast)与 metadata 内容一致。
- 建议:为三个探测函数补直接用例:tiktoken Encoding /
- TokenizerInfo 以 RAW 重建的兼容性 workaround 缺少等价性测试 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:81- 建议:补一条针对重建路径的用例:对 BYTE_FALLBACK / BYTE_LEVEL 两类 metadata,分别断言重建后的
GetDecodedVocab()与GetSpecialTokenIds()与反序列化结果一致,重点覆盖 vocab 空洞的""槽位仍被判为 special、以及形如<|...|>的普通文本 token 不被误判。
- 建议:补一条针对重建路径的用例:对 BYTE_FALLBACK / BYTE_LEVEL 两类 metadata,分别断言重建后的
- grammar smoke 校验存在静默放行路径、完全短路 golden 比较,且唯一 oracle 无单测 @
rtp_llm/test/smoke/openai_comparer.py:230- 建议:把两处静默
return改为抛SmokeException(QueryStatus.VALID_FAILED, ...),与dash_grpc_comparer对齐;或在task_info/case_runner层校验grammar_constraint_only与stream互斥。约束校验通过后继续比较finish_reason/usage/aux_info等确定性字段(当前「MTP 是否真正接受 token」完全无断言),否则删除 fixture 中的死result并修正_comment。同时为grammar_constraint_validator.validate_constraint补一组正反例单测(合法/非法 JSON、regex 不匹配、structural_tag 缺 tag)。
- 建议:把两处静默
- 声称验证 CPU fallback 的测试强绑 CUDA 与 GPU 执行属性,非 CUDA 平台无覆盖 @
rtp_llm/models_py/bindings/core/test/BUILD:53- 建议:若 CPU fallback 需在非 CUDA 平台生效,把 CUDA deps 放进
select({"//:using_cuda": [...], "//conditions:default": []})并去掉exec_properties,使其在 ROCm/CPU 配置下进入默认门禁;若该 fallback 只存在于 CUDA 构建,请把注释改为「仅验证 CUDA 构建下 CPU 张量走 fallback」,避免注释与实际覆盖范围不符。
- 建议:若 CPU fallback 需在非 CUDA 平台生效,把 CUDA deps 放进
- MultiSeqLogitsProcessor 写 EOS 掩码缺少 vocab 边界校验,存在越界写风险 @
rtp_llm/cpp/models/logits_processor/MultiSeqLogitsProcessor.cc:39- 建议:在
process开头(或构造点)校验eos_token_id_ >= 0 && eos_token_id_ < vocab_size,不满足时返回明确ErrorInfo(可复用GRAMMAR_EOS_OUT_OF_VOCAB的同类语义或新增专用码),并补一条 eos 越界的用例断言返回错误而非越界写,与 grammar 路径保持一致。
- 建议:在
- xgrammar pin 在上游未合并特性分支 commit,且另开一套依赖声明入口 @
3rdparty/xgrammar/repositories.bzl:8- 建议:把 xgrammar 声明并入
git_deps()保持单一入口;pin 改为「上游主干/tag commit +3rdparty/xgrammar/000x-*.patch」或带sha256的http_archive,使依赖内容寻址、可离线复现、anytext-budget 改动可审计可回滚。若必须 pin 分支 commit,请补内部镜像与shallow_since,并在文件头注明「上游合并后切回 tag」的迁移路径。
- 建议:把 xgrammar 声明并入
- vendored dlpack 头版本与 BUILD 注释不符,且未记录 upstream 出处与 license @
3rdparty/dlpack/BUILD:6- 建议:把注释更正为实际版本(1.2),在 BUILD 或同目录 README 记录 vendored 文件的 upstream 仓库与 commit,并补上 dlpack license 以满足三方代码入库要求;同时说明为何该版本与所 pin 的 xgrammar commit 兼容(至少注明
DLTensor布局未变),visibility 建议收敛到实际使用方。
- 建议:把注释更正为实际版本(1.2),在 BUILD 或同目录 README 记录 vendored 文件的 upstream 仓库与 commit,并补上 dlpack license 以满足三方代码入库要求;同时说明为何该版本与所 pin 的 xgrammar commit 兼容(至少注明
- pybind 类型存根遗漏 terminate_without_stop_token,跨语言配置契约不完整 @
rtp_llm/ops/libth_transformer_config.pyi:544- 建议:在存根的
class GrammarConfig中补terminate_without_stop_token: bool(可一并补def __repr__(self) -> str: ...);若存根为自动生成,请重新生成并确认 CI 存在「存根与 pybind 绑定一致性」检查,避免后续新增字段再次漏同步。
- 建议:在存根的
- 新增 grammar CLI 参数的默认值断言为同源自证,env 分支与边界值零覆盖 @
rtp_llm/server/server_args/test/server_args_test.py:423- 建议:
test_grammar_defaults改为断言字面默认值(False / False / 8 / 536870912);补一条 env 用例(保持sys.argv=["prog"],设置三个环境变量后_setup(),断言 bool/int 转换正确);补--grammar_compiler_cache_bytes 0、--grammar_num_workers 0边界用例,断言 Python 侧原样透传、语义由 C++ 兜底;并为--grammar_backend增加显式回归断言。
- 建议:
P3
- 大范围格式化重排混入逻辑变更,跨语言 SYNC 注释被折断 @
rtp_llm/cpp/models/logits_processor/RecommendationLogitsProcessor.cc:52- 建议:把纯格式化重排拆成独立的 formatting-only 提交或 PR,让功能提交只含行为变更;重排时用
// clang-format off/on保护带语义的多行注释。至少在 PR 描述中列明哪些文件属于纯格式化,并恢复被折断的 SYNC/CHECKLIST 注释分行。
- 建议:把纯格式化重排拆成独立的 formatting-only 提交或 PR,让功能提交只含行为变更;重排时用
- 请求级 grammar_terminate_without_stop_token 被解析但全仓无消费者 @
rtp_llm/cpp/engine_base/stream/GenerateConfig.h:57- 建议:若确定该策略只在服务级生效,删除
GenerateConfig中的字段与JSONIZE条目(raw JSON 的未知键本就被忽略),避免「可设置但无效」的伪契约;若确需请求级覆盖,则在LogitsProcessorFactory创建 matcher 时以该字段覆盖Options.terminate_without_stop_token,补 raw-JSON→matcher 行为用例,并在 proto 与 Python 侧对齐字段可见性。
- 建议:若确定该策略只在服务级生效,删除
- BitmaskUtils 头注释声明 abort,实现实际抛异常 @
rtp_llm/cpp/models/logits_processor/BitmaskUtils.h:30- 建议:把注释改为准确描述(如 "throws via RTP_LLM_CHECK_WITH_INFO; callers must not rely on process abort"),或若确实希望不可恢复则改用语义一致的 fail-fast 机制;并在调用侧明确该异常是否被期望捕获。
- matcher_ 空值契约在三个入口不一致,且 process 的空值分支为 fail-open @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:377- 建议:在构造函数中
RTP_LLM_CHECK_WITH_INFO(matcher_ != nullptr, ...),使「matcher 恒非空」成为类不变量,随后删除process()/updateStatus()中冗余的空检查;若确需保留可空语义,则process()应返回明确ErrorInfo而不是静默放行,并为prepareSpeculative补同样的空检查。
- 建议:在构造函数中
- GrammarConfig pickle 靠首元素类型嗅探区分布局,且 repr 与 to_string 双份渲染 @
rtp_llm/cpp/pybind/ConfigInit.cc:1354- 建议:在
__getstate__首位加显式版本号并按版本严格校验 tuple 长度、未知版本 fail-fast;异常文案改为"Invalid GrammarConfig state!"与邻近实现对齐。__repr__直接复用.def("__repr__", &GrammarConfig::to_string),或在 C++ 侧提供单一渲染函数供两者共用。
- 建议:在
- grammar BUILD 声明的 Tier 依赖约束未通过 visibility 落地,且过渡层无历史调用方 @
rtp_llm/cpp/engine_base/grammar/BUILD:35- 建议:把
grammar_xgrammar的visibility显式收窄为["//rtp_llm/cpp/engine_base/grammar:__subpackages__", "//rtp_llm/cpp/models:__pkg__", ...],让注释中的分层约束由构建系统强制;删除空壳grammar_cpp_lib并让现有调用方直接依赖:grammar_xgrammar,或修正注释说明它长期存在的理由。
- 建议:把
- grammar 字段归一化在 GenerateConfig 与 grammar_constraint 中存在两份平行实现 @
rtp_llm/config/generate_config.py:743- 建议:让
_normalize_grammar_fields直接复用normalize_grammar_value/dump_compact_json并删除私有_compact_json;_has_grammar_constraint改为基于GrammarConstraint.collect_from_config(额外叠加json_format),把「约束字段清单」收敛为单一真源。
- 建议:让
- enqueue 对同一请求重复调用 trans_input,在 validate() 变为有副作用后依赖未验证的幂等性 @
rtp_llm/cpp/model_rpc/model_rpc_client.py:556- 建议:删除 :556 的重复调用,只在选定地址后构造一次
input_pb;若确需提前校验,改为显式调用input_py.generate_config.validate()而不是构造整个 PB。同时在model_rpc_client_test.py补一条断言:对同一GenerateInput连续两次trans_input,产出的 PB 与 config 状态均相同,把幂等性固化为契约。
- 建议:删除 :556 的重复调用,只在选定地址后构造一次
- processor 长度不一致这一内部不变量被归入 UNKNOWN_ERROR,与本 PR 的错误分类风格不一致 @
rtp_llm/cpp/engine_base/stream/GenerateStream.cc:964- 建议:为该不变量分配专用错误码(如
PROCESSOR_STATE_MISMATCH),生产代码与测试断言同步更新;错误信息已带 processor 索引与两侧长度,建议保留以便直接定位。
- 建议:为该不变量分配专用错误码(如
- assertRaises 包裹两条语句,第二条断言语句实际不会执行且无法定位抛出阶段 @
rtp_llm/test/generate_config_test.py:1040- 建议:删除冗余的第二条语句,或把两个阶段拆成两次独立断言;并改用
assertRaisesRegex绑定各阶段特有的错误消息,使断言能唯一定位抛出点。
- 建议:删除冗余的第二条语句,或把两个阶段拆成两次独立断言;并改用
Checklist Violations (21 fail / 58 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
grammar BUILD 声明的 Tier 依赖约束未通过 visibility 落地,且过渡层无历史调用方
grammar_xgrammar上方注释写「only Tier 0 (this package) and Tier 1 (logits_processor) may depend」(:35),但文件顶部package(default_visibility = ["//visibility:public"])(:4)使该约束对 Bazel 完全不可见,任何包都可直接依赖 xgrammar 实现。grammar_cpp_lib(:59-66)注释为「Transitional umbrella; deleted once callers move onto :grammar_xgrammar」,但其依赖方(grammar/test/BUILD、models/BUILD:78、logits_processor/test/BUILD)全部随本 PR 新增,不存在需迁移的历史调用方;该目标无 srcs/hdrs,只是deps = [":grammar_xgrammar"]的空壳,依赖图多一跳且注释误导后续维护者。 - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
新增 grammar CLI 参数的默认值断言为同源自证,env 分支与边界值零覆盖
test_grammar_defaults用expected = type(g)()与解析结果比较(:428-439),而 CLI 默认值本身取自同一份GrammarConfig默认值(grammar_group_args.py 的default=grammar_config.*),恒等成立:C++ 侧num_workers(8) /compiler_cache_bytes(512MB) 被误改时该守卫不会失败。setUp执行os.environ.clear(),test_grammar_cmd_args(:441-460)只覆盖命令行路径,而每个旗标都声明了env_name(GRAMMAR_TERMINATE_WITHOUT_STOP_TOKEN/GRAMMAR_NUM_WORKERS/GRAMMAR_COMPILER_CACHE_BYTES),env 分支零断言;compiler_cache_bytes<=0、num_workers<=0两个语义边界亦未覆盖;`--grammar_backe - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
grammar BUILD 声明的 Tier 依赖约束未通过 visibility 落地,且过渡层无历史调用方
grammar_xgrammar上方注释写「only Tier 0 (this package) and Tier 1 (logits_processor) may depend」(:35),但文件顶部package(default_visibility = ["//visibility:public"])(:4)使该约束对 Bazel 完全不可见,任何包都可直接依赖 xgrammar 实现。grammar_cpp_lib(:59-66)注释为「Transitional umbrella; deleted once callers move onto :grammar_xgrammar」,但其依赖方(grammar/test/BUILD、models/BUILD:78、logits_processor/test/BUILD)全部随本 PR 新增,不存在需迁移的历史调用方;该目标无 srcs/hdrs,只是deps = [":grammar_xgrammar"]的空壳,依赖图多一跳且注释误导后续维护者。 - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
processor 长度不一致这一内部不变量被归入 UNKNOWN_ERROR,与本 PR 的错误分类风格不一致
validateLogitsProcessorState在committedOutputLen()与outputTokenLen()不一致时返回ErrorInfo(ErrorCode::UNKNOWN_ERROR, ...)(:963-968),新增用例GenerateStreamTest.cc把该行为固化为期望。而本 PR 同时引入了GRAMMAR_VOCAB_EXCEEDS_MODEL_VOCAB、GRAMMAR_EOS_OUT_OF_VOCAB、GRAMMAR_PARSER_REJECTED_TOKEN、GRAMMAR_VERIFY_EXCEPTION等专用码并逐一断言,把这一明确的内部状态不变量归入UNKNOWN_ERROR,会让线上排障无法从错误码区分该场景。 - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
xgrammar pin 在上游未合并特性分支 commit,且另开一套依赖声明入口
xgrammar_deps()已改为直连上游mlc-ai/xgrammar(较上一轮的个人 fork 有明显改进),但仍用new_git_repositorypincommit = "60fc70ee...",上一行注释自述该 commit 来自feat/anytext-budget分支(:4-8):非 release tag、无sha256、无shallow_since、未采用仓库既有的「上游 commit + patches」模式。本 PR 能力强依赖该分支(多处用例依赖any_text.max_tokens),分支被 force-push 或对象不可达时bazel fetch失败且无本地回退。同时仓库其他 git 依赖统一声明在git_deps(),本 PR 在WORKSPACE新增独立入口,形成第二套声明面。 - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
enqueue 对同一请求重复调用 trans_input,在 validate() 变为有副作用后依赖未验证的幂等性
enqueue在 :556 与 :578 各调用一次trans_input(input_py),:556 构造的input_pb在其间未被使用、随即被覆盖,即每个请求都要构造两遍完整GenerateConfigPB并执行两遍 grammar 归一化与 JSON 压缩。本 PR 将trans_input改为在 :109 调用generate_config.validate(),该调用会就地改写 config 并写入_reasoning_envelope_applied/_reasoning_final_constraint(response_format_builder.py:72-77在特定状态下会调用restore_final_constraint清空 typed 字段)。当前两次调用间in_think_mode不变故未发散,但正确性所依赖的 finalize 幂等性没有任何测试断言。 - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
matcher_ 空值契约在三个入口不一致,且 process 的空值分支为 fail-open
process()(:308-310)与updateStatus()(:344-346)都以if (!matcher_) return std::nullopt;开头,而prepareSpeculative直接prepareSpecMask(*matcher_, ...)(:377)解引用;构造函数(:299-302)也未断言非空。当前唯一生产构造点(LogitsProcessorFactory.cc:122)与XGrammarBackend::createMatcher(已拒绝 null)保证非空,故暂不触发;但三入口契约不一致,未来新增构造路径易退化为空指针解引用。更值得注意的是process()在 matcher 为空时返回nullopt,语义等于「静默取消约束」而非报错。_ - [6.1] Quality — Mega-PR 已拆分为独立变更 → issue
大范围格式化重排混入逻辑变更,跨语言 SYNC 注释被折断
RecommendationLogitsProcessor.cc/.h与其测试的实际语义改动仅为返回类型改std::optional<ErrorInfo>及-INFINITY统一为BaseLogitsProcessor::neg_inf,但文件被整体 clang-format 重排(测试文件近 1200 行 diff 几乎全为空白对齐);:51-53 原本分行的「SYNC: 必须与 Pythongenerate_config.py::_check_cross_seq_ban_compatibility保持一致」与 CHECKLIST 注释被折进行中,跨语言同步契约可读性明显下降。case_runner.py数百行 diff 中真正的逻辑改动只有 3 处(:383-384 注入grammar_constraint_only等),其余为 black 重排,抬高了 review、git blame、bisect 与 revert 成本。 - [6.1] Quality — 逻辑变更未混入无关格式化 → issue
大范围格式化重排混入逻辑变更,跨语言 SYNC 注释被折断
RecommendationLogitsProcessor.cc/.h与其测试的实际语义改动仅为返回类型改std::optional<ErrorInfo>及-INFINITY统一为BaseLogitsProcessor::neg_inf,但文件被整体 clang-format 重排(测试文件近 1200 行 diff 几乎全为空白对齐);:51-53 原本分行的「SYNC: 必须与 Pythongenerate_config.py::_check_cross_seq_ban_compatibility保持一致」与 CHECKLIST 注释被折进行中,跨语言同步契约可读性明显下降。case_runner.py数百行 diff 中真正的逻辑改动只有 3 处(:383-384 注入grammar_constraint_only等),其余为 black 重排,抬高了 review、git blame、bisect 与 revert 成本。 - [6.1] Software Engineering — DIP:高层策略不依赖非必要具体细节 → issue
grammar backend 存放在函数内可变静态变量,init 重复构建且无同步与隔离手段
grammarBackend()返回函数内static std::shared_ptr<XGrammarBackend>(:72),由init()直接赋值(:79)。注释声明「每 rank 单进程单引擎」的前提,但init()有两个调用点(NormalExecutor.cc:129、MtpExecutor.cc:328),每次调用都重新XGrammarBackend::create:重复反序列化并重建 TokenizerInfo、重新拉起num_workers个编译线程;写入无任何同步,仅靠「启动早于服务」的隐含假设,也没有 reset/teardown。工厂同时承担「创建 processor」与「持有全局 backend 生命周期」两项职责——新增的GrammarLogitsProcessorTest正是绕开工厂直接构造 backend 才可测。 - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
enqueue 对同一请求重复调用 trans_input,在 validate() 变为有副作用后依赖未验证的幂等性
enqueue在 :556 与 :578 各调用一次trans_input(input_py),:556 构造的input_pb在其间未被使用、随即被覆盖,即每个请求都要构造两遍完整GenerateConfigPB并执行两遍 grammar 归一化与 JSON 压缩。本 PR 将trans_input改为在 :109 调用generate_config.validate(),该调用会就地改写 config 并写入_reasoning_envelope_applied/_reasoning_final_constraint(response_format_builder.py:72-77在特定状态下会调用restore_final_constraint清空 typed 字段)。当前两次调用间in_think_mode不变故未发散,但正确性所依赖的 finalize 幂等性没有任何测试断言。 - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
grammar BUILD 声明的 Tier 依赖约束未通过 visibility 落地,且过渡层无历史调用方
grammar_xgrammar上方注释写「only Tier 0 (this package) and Tier 1 (logits_processor) may depend」(:35),但文件顶部package(default_visibility = ["//visibility:public"])(:4)使该约束对 Bazel 完全不可见,任何包都可直接依赖 xgrammar 实现。grammar_cpp_lib(:59-66)注释为「Transitional umbrella; deleted once callers move onto :grammar_xgrammar」,但其依赖方(grammar/test/BUILD、models/BUILD:78、logits_processor/test/BUILD)全部随本 PR 新增,不存在需迁移的历史调用方;该目标无 srcs/hdrs,只是deps = [":grammar_xgrammar"]的空壳,依赖图多一跳且注释误导后续维护者。 - [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue
MTP 能力校验默认拒绝所有未声明 processor,tree_decode_config 变为启动期抛异常
validateMtpCompatibility(:45-55)遍历stream->getAllLogitsProcessorPtr(),任一mtpCapability().mode == UNSUPPORTED即在prepareStreams(:849-852)reportError拒绝该流。全仓仅GrammarLogitsProcessor.h:29覆写了mtpCapability(),Tree / Recommendation / MultiSeq / ThinkMode全部继承BaseLogitsProcessor.h:44-46的默认 UNSUPPORTED(MtpProcessorCapability.mode默认值即 UNSUPPORTED)。因此 MTP 下combo_token_size>0(Recommendation 的updateStatus明确支持 MTP 多 token 布局)或num_return_sequences>1的请求由「运行」变为稳定 `INVALID_PARA - [6.1] Software Engineering — SRP:模块/类职责单一 → issue
grammar backend 存放在函数内可变静态变量,init 重复构建且无同步与隔离手段
grammarBackend()返回函数内static std::shared_ptr<XGrammarBackend>(:72),由init()直接赋值(:79)。注释声明「每 rank 单进程单引擎」的前提,但init()有两个调用点(NormalExecutor.cc:129、MtpExecutor.cc:328),每次调用都重新XGrammarBackend::create:重复反序列化并重建 TokenizerInfo、重新拉起num_workers个编译线程;写入无任何同步,仅靠「启动早于服务」的隐含假设,也没有 reset/teardown。工厂同时承担「创建 processor」与「持有全局 backend 生命周期」两项职责——新增的GrammarLogitsProcessorTest正是绕开工厂直接构造 backend 才可测。 - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
声称验证 CPU fallback 的测试强绑 CUDA 与 GPU 执行属性,非 CUDA 平台无覆盖
注释写 "Exercise the CPU fallback without initializing or allocating a GPU."(:53),但packed_mask_logits_cpu_fallback_test(:54-65)的 deps 无条件包含//rtp_llm/models_py/bindings/cuda/ops:flashinfer_runtime,且声明exec_properties = {"gpu": "H20"}。该目标只能在 CUDA 构建配置 + GPU 主机上调度,而runtimeApplyPackedMaskLogits的 CPU 分支恰是 ROCm/CPU 构建下唯一实现路径——这些配置下该测试既不编译也不运行,与前述pin_memory()无平台守卫叠加,跨平台正确性实际上没有 CI 护栏。 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
assertRaises 包裹两条语句,第二条断言语句实际不会执行且无法定位抛出阶段
test_update_rejects_malformed_envelope(:1040-1042)与test_update_and_pop_rejects_invalid_json_envelope(:1053-1055)都把cfg.validate()与ResponseFormatBuilder(cfg).apply()放进同一assertRaises块。validate()自身已经调用finalize_response_format()→ResponseFormatBuilder.finalize()→apply()(generate_config.py:729-732、response_format_builder.py:108-112),因此第二条语句在异常路径下永不执行,断言也无法判断异常来自哪一阶段——同类语义在一处期望ERROR_INPUT_FORMAT_ERROR、另一处期望UNSUPPORTED_OPERATION,正是这种不确定性的体现。 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
MultiSeqLogitsProcessor 写 EOS 掩码缺少 vocab 边界校验,存在越界写风险
process以vocab_size = inputs.logits.size(1)(:21)分配 host mask,memset整行置 1 后执行cur_logit_mask_host_ptr[eos_token_id_] = 0;(:39),对eos_token_id_是否落在[0, vocab_size)无任何校验;fromGenerateInput(:5-16)也不校验注入的 eos。若 eos 配置与实际 logits vocab 不一致,此处发生堆越界写。同一 PR 在 grammar 路径为完全相同的风险专门新增了GRAMMAR_EOS_OUT_OF_VOCAB检查(GrammarLogitsProcessor.cc:283-287),两处策略不一致;本 PR 已把该函数签名改为可返回std::optional<ErrorInfo>,具备低成本报错能力。
RTP-LLM Checklist
- [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue
ThinkModeLogitsProcessor 沦为生产不可达代码,max_batch_size 形参失效,文档与实现不符
工厂不再构造ThinkModeLogitsProcessor且已移除头文件包含(:98-99),全仓搜索后该类仅被自身与ThinkModeLogitsProcessorTest.cc引用(生产死代码),但models/BUILD:55,68仍编译、单测仍在跑。连带影响:createLogitsProcessors的max_batch_size形参在函数体(:88-141)内已完全不被引用(原唯一使用者即 ThinkMode 装配),调用方GenerateStream.cc仍照旧传maxBatchSize();docs/backend/logits_processor.md:8仍把它描述为生效组件,rtp_llm/dash_sc/think.py:167注释也仍称 C++ 侧会强制 phase-1,形成「文档与测试都说它在工作,实际生产不可达」的偏差。 - [I] 代码质量 — 同一功能用统一工具函数 → issue
grammar 字段归一化在 GenerateConfig 与 grammar_constraint 中存在两份平行实现
_validate_fields末尾仍调用_normalize_grammar_fields()(:725、:743-747),内部用本文件私有的_compact_json(:29-32)压缩json_schema/structural_tag;而新增的grammar_constraint.py已提供dump_compact_json/normalize_grammar_value承担同一职责,ResponseFormatBuilder._resolve_grammar_constraint走的是后者。同理_has_grammar_constraint(:734-741)与GrammarConstraint.collect_from_config(grammar_constraint.py:95-106)是两份「哪些字段构成约束」的清单(前者额外含json_format)。两处并存后,新增约束字段类型时必然出现漂移。
Python Static-First Checklist
- [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
tokenizer 家族探测函数在单测中被整体 mock,最易出错的分派链零直接覆盖
辅助方法build_tokenizer_info(:55-99)同时 patch 了_is_fast_tokenizer(:73-77)、_is_tiktoken_tokenizer(:78-82)、_is_sentencepiece_tokenizer(:83-87)、_is_byte_level_tokenizer(:88-92)并用假模块替换rtp_llm.ops,因此所有test_build_tokenizer_info_*都绕过生产分派开关,只验证 metadata 字典拼装。全文件仅_is_byte_level_tokenizer有直接用例;风险最高的鸭子类型判定——_has_tiktoken_encoding(按type().__module__前缀,:66-75)、_has_tiktoken_vocab_file(子串匹配,:78-83)、_has_mergeable_ranks、_iter_sentencepiece_candidates/_has_sentencepiece_api(:115- - [P.G] 测试规范 — pytest.raises 带 match 参数 → issue
assertRaises 包裹两条语句,第二条断言语句实际不会执行且无法定位抛出阶段
test_update_rejects_malformed_envelope(:1040-1042)与test_update_and_pop_rejects_invalid_json_envelope(:1053-1055)都把cfg.validate()与ResponseFormatBuilder(cfg).apply()放进同一assertRaises块。validate()自身已经调用finalize_response_format()→ResponseFormatBuilder.finalize()→apply()(generate_config.py:729-732、response_format_builder.py:108-112),因此第二条语句在异常路径下永不执行,断言也无法判断异常来自哪一阶段——同类语义在一处期望ERROR_INPUT_FORMAT_ERROR、另一处期望UNSUPPORTED_OPERATION,正是这种不确定性的体现。
Strengths
- 依赖收敛干净:xgrammar 类型完全封在
engine_base/grammar包内,GrammarLogitsProcessor.h仅前向声明 matcher,第三方头未泄漏到 models 层;RtpGrammarMatcher显式删除拷贝/移动并用static_assert固化到编译期。 - 错误以
absl::Status/ErrorInfo返回而非抛异常:XGrammarBackend::create声明noexcept,tokenizer info 为空/反序列化失败/vocab 为空/构造抛异常四类情况均返回nullptr;matcherCall模板统一收敛 xgrammar 异常。 - 投机校验状态不变量严谨:
prepareSpecMask以provisional_accepts试探并回滚,失败统一fail_closed()(markFinished+ 强制 EOS-only bitmask);acceptCommittedLocked的rollback_commit保证多 token 批量提交「全成功或全回滚」。 - 新增
committedOutputLen()+GenerateStream::validateLogitsProcessorState()做 parser/stream 长度对账,把潜在错位从静默错误变成显式 error;MtpBatchStreamProcessor.cc:122补上finished_mask.zero_(),修掉 spec 路径未初始化隐患。 - 单一事实来源:
grammar_group_args.py的 default 全部读取 C++GrammarConfig结构体默认值;PythonGrammarConstraint+ResponseFormatBuilder把 one-of 约束规范化收敛为单一投影路径,C++keyFromGenerateConfig再独立复核互斥,形成双层防护。 BitmaskUtils::clearBitmaskTokenRange对同字/跨字、end_bit == 0的位推导正确,并有跨字与同字两个针对性用例。- 测试使用真实 xgrammar 后端而非 mock:
GrammarLogitsProcessorTest覆盖初始 mask、预算耗尽、终止后强制 EOS、整批 rollback、prepareSpeculativecap 语义与 fp32/fp16/bf16;packed-mask 提供 GPU 与 CPU 两套同构用例;config_pickle_test.py为 current/legacy/残缺三种 pickle 布局建立回归。
| default="xgrammar", | ||
| help="Grammar backend type: xgrammar or none", | ||
| ) | ||
| grammar_group.add_argument( |
There was a problem hiding this comment.
[P1] --grammar_backend / GRAMMAR_BACKEND 无过渡删除,存量启动脚本升级即退出且失去唯一关停开关
init_grammar_group_args 现只注册 4 个旗标(:6-49),原 --grammar_backend(env GRAMMAR_BACKEND,help 声明支持 none)被整块删除,全仓仅 ConfigInit.cc:1356-1357 的注释提及。server_args 走严格 parse_args,仍带 --grammar_backend xgrammar 的启动脚本会 SystemExit;GRAMMAR_BACKEND=none 因未注册被静默忽略。同一 PR 为 pickle 专门写了 legacy 兼容分支(ConfigInit.cc:1354),CLI/env 侧却无等价过渡;删除后不存在任何服务级 grammar 关停手段,而 grammar 本 PR 新成为启动期强依赖,运维只能整体回滚版本。
建议: 二选一并在 release note 给出迁移说明:(1) 保留一个发布周期的 deprecated no-op --grammar_backend(解析后打 warning);(2) 新增 --grammar_enable/GRAMMAR_ENABLE(默认 true),关闭时跳过 build_grammar_tokenizer_info() 与 XGrammarBackend::create,结构化请求返回明确错误。并在 server_args_test.py 把旧 flag 的处理方式(接受并忽略、或显式拒绝并提示迁移)固化为断言。
|
|
||
| std::optional<ErrorInfo> validateMtpCompatibility(const std::vector<BaseLogitsProcessorPtr>& processors) { | ||
| for (size_t i = 0; i < processors.size(); ++i) { | ||
| const auto capability = processors[i]->mtpCapability(); |
There was a problem hiding this comment.
[P1] MTP 能力校验默认拒绝所有未声明 processor,tree_decode_config 变为启动期抛异常
validateMtpCompatibility(:45-55)遍历 stream->getAllLogitsProcessorPtr(),任一 mtpCapability().mode == UNSUPPORTED 即在 prepareStreams(:849-852)reportError 拒绝该流。全仓仅 GrammarLogitsProcessor.h:29 覆写了 mtpCapability(),Tree / Recommendation / MultiSeq / ThinkMode 全部继承 BaseLogitsProcessor.h:44-46 的默认 UNSUPPORTED(MtpProcessorCapability.mode 默认值即 UNSUPPORTED)。因此 MTP 下 combo_token_size>0(Recommendation 的 updateStatus 明确支持 MTP 多 token 布局)或 num_return_sequences>1 的请求由「运行」变为稳定 `INVALID_P...
建议: 从「静默不生效」改为「显式报错」方向正确,但用基类默认值把「未声明」等同「不支持」属对既有子类契约的隐式收紧:请为确实兼容 MTP 的 processor(如 Recommendation)显式声明能力,而非依赖默认值;构造函数硬断言改为启动配置校验阶段返回结构化错误,避免异常从构造函数逃逸;在发布说明列出受影响组合(combo_token_size>0、num_return_sequences>1、TREE_DECODE_CONFIG+MTP)与迁移方式,并补回归用例断言错误码稳定。
Checklist: [6.1] LSP:子类/重写保持基类契约
| @@ -136,7 +136,6 @@ message GenerateConfigPB { | |||
| google.protobuf.StringValue regex = 70; | |||
| google.protobuf.StringValue ebnf = 71; | |||
| google.protobuf.StringValue structural_tag = 72; | |||
There was a problem hiding this comment.
[P2] proto 字段 73 删除未补 reserved,与同文件既有惯例不一致且留下 wire 号复用隐患
GenerateConfigPB 现止于 structural_tag = 72;(:138),被删的 response_format = 73 未补 reserved 73; / reserved "response_format";。同文件删除字段一贯保留占位:reserved 42(:103-104)、reserved 5/12(:324-333)、reserved 1 to 8(:620-622),:317 注释明确「field numbers stay reserved even though mixed-version ... deployment」。经核验旧引擎未用该字段构建约束(pre-PR LogitsProcessorFactory 无 grammar 分支),故不构成功能回归;风险是 73 号被将来新字段以不同 wire type 复用后,混版部署中旧客户端字节流被误解析。
建议: 补 reserved 73; 与 reserved "response_format";,与 :103-104 保持一致;在 PR description / release note 写明升级顺序(frontend 先于 engine)。并在 model_rpc_client_test.py 增补一例:构造仅含旧字段号的原始字节串交给新版 GenerateConfigPB 解析,断言其落入 unknown fields 且不影响 typed 字段解析,把兼容契约变成可执行断言。
| alwayslink = True, | ||
| ) | ||
|
|
||
| # xgrammar-specific impl; only Tier 0 (this package) and Tier 1 (logits_processor) may depend. |
There was a problem hiding this comment.
[P3] grammar BUILD 声明的 Tier 依赖约束未通过 visibility 落地,且过渡层无历史调用方
grammar_xgrammar 上方注释写「only Tier 0 (this package) and Tier 1 (logits_processor) may depend」(:35),但文件顶部 package(default_visibility = ["//visibility:public"])(:4)使该约束对 Bazel 完全不可见,任何包都可直接依赖 xgrammar 实现。grammar_cpp_lib(:59-66)注释为「Transitional umbrella; deleted once callers move onto :grammar_xgrammar」,但其依赖方(grammar/test/BUILD、models/BUILD:78、logits_processor/test/BUILD)全部随本 PR 新增,不存在需迁移的历史调用方;该目标无 srcs/hdrs,只是 deps = [":grammar_xgrammar"] 的空壳,依赖图多一跳且注释误导后续维护者。
建议: 把 grammar_xgrammar 的 visibility 显式收窄为 ["//rtp_llm/cpp/engine_base/grammar:__subpackages__", "//rtp_llm/cpp/models:__pkg__", ...],让注释中的分层约束由构建系统强制;删除空壳 grammar_cpp_lib 并让现有调用方直接依赖 :grammar_xgrammar,或修正注释说明它长期存在的理由。
Checklist: [6.1] 依赖方向:无循环依赖/跨层惊喜;[6.1] 分层边界:新概念在正确层级,不泄漏内部;[6.1] KISS/YAGNI:无投机性抽象
| or _response_format_is_grammar(self.response_format) | ||
| ) | ||
|
|
||
| def _normalize_grammar_fields(self): |
There was a problem hiding this comment.
[P3] grammar 字段归一化在 GenerateConfig 与 grammar_constraint 中存在两份平行实现
_validate_fields 末尾仍调用 _normalize_grammar_fields()(:725、:743-747),内部用本文件私有的 _compact_json(:29-32)压缩 json_schema/structural_tag;而新增的 grammar_constraint.py 已提供 dump_compact_json / normalize_grammar_value 承担同一职责,ResponseFormatBuilder._resolve_grammar_constraint 走的是后者。同理 _has_grammar_constraint(:734-741)与 GrammarConstraint.collect_from_config(grammar_constraint.py:95-106)是两份「哪些字段构成约束」的清单(前者额外含 json_format)。两处并存后,新增约束字段类型时必然出现漂移。
建议: 让 _normalize_grammar_fields 直接复用 normalize_grammar_value/dump_compact_json 并删除私有 _compact_json;_has_grammar_constraint 改为基于 GrammarConstraint.collect_from_config(额外叠加 json_format),把「约束字段清单」收敛为单一真源。
Checklist: [I] 同一功能用统一工具函数
| ) | ||
| for i in range(len(input_py.generate_config.banned_combo_token_ids)): | ||
| banned_combo = generate_config_pb.banned_combo_token_ids.rows.add() | ||
| banned_combo.values.extend(input_py.generate_config.banned_combo_token_ids[i]) |
There was a problem hiding this comment.
📍 实际位置 rtp_llm/cpp/model_rpc/model_rpc_client.py:556(不在 diff 展示范围内,就近挂载)
[P3] enqueue 对同一请求重复调用 trans_input,在 validate() 变为有副作用后依赖未验证的幂等性
enqueue 在 :556 与 :578 各调用一次 trans_input(input_py),:556 构造的 input_pb 在其间未被使用、随即被覆盖,即每个请求都要构造两遍完整 GenerateConfigPB 并执行两遍 grammar 归一化与 JSON 压缩。本 PR 将 trans_input 改为在 :109 调用 generate_config.validate(),该调用会就地改写 config 并写入 _reasoning_envelope_applied / _reasoning_final_constraint(response_format_builder.py:72-77 在特定状态下会调用 restore_final_constraint 清空 typed 字段)。当前两次调用间 in_think_mode 不变故未发散,但正确性所依赖的 finalize 幂等性没有任何测试断言。
建议: 删除 :556 的重复调用,只在选定地址后构造一次 input_pb;若确需提前校验,改为显式调用 input_py.generate_config.validate() 而不是构造整个 PB。同时在 model_rpc_client_test.py 补一条断言:对同一 GenerateInput 连续两次 trans_input,产出的 PB 与 config 状态均相同,把幂等性固化为契约。
Checklist: [6.1] 状态不变量:创建/更新/失败/重试/回滚路径有效;[6.1] DRY:重复非平凡逻辑被抽取或显式复用
| def test_update_rejects_malformed_envelope(self): | ||
| cfg = GenerateConfig() | ||
| cfg.update({"response_format": {"type": "json_schema"}}) | ||
| with self.assertRaises(FtRuntimeException) as ctx: |
There was a problem hiding this comment.
[P3] assertRaises 包裹两条语句,第二条断言语句实际不会执行且无法定位抛出阶段
test_update_rejects_malformed_envelope(:1040-1042)与 test_update_and_pop_rejects_invalid_json_envelope(:1053-1055)都把 cfg.validate() 与 ResponseFormatBuilder(cfg).apply() 放进同一 assertRaises 块。validate() 自身已经调用 finalize_response_format() → ResponseFormatBuilder.finalize() → apply()(generate_config.py:729-732、response_format_builder.py:108-112),因此第二条语句在异常路径下永不执行,断言也无法判断异常来自哪一阶段——同类语义在一处期望 ERROR_INPUT_FORMAT_ERROR、另一处期望 UNSUPPORTED_OPERATION,正是这种不确定性的体现。
建议: 删除冗余的第二条语句,或把两个阶段拆成两次独立断言;并改用 assertRaisesRegex 绑定各阶段特有的错误消息,使断言能唯一定位抛出点。
Checklist: [6.1] 新逻辑有聚焦单测 + 相关集成/smoke 测试;[P.G] pytest.raises 带 match 参数
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1006
Status: BLOCKING
Summary: P0/0 · P1/5 · P2/28 · P3/8
Reviewed: commit f5a419490f5a · 2026-08-06 01:47 UTC+8
Blocking Issues
P1
- 启动期强制构建 grammar tokenizer 元数据,不支持的 tokenizer 直接阻塞模型加载且无逃生开关 @
rtp_llm/config/grammar_tokenizer_info.py:195- 建议:把「构建失败」与「必须失败」解耦:默认构建失败时记 ERROR 并返回空字符串,交由已就绪的 C++ 降级路径在真正的 grammar 请求上返回 INVALID_PARAMS;仅当服务级 think 模式开启(每个请求都走 grammar)或运维显式要求严格模式时才 fail-fast。同时提供显式开关(如
--grammar_enable,可与下一条的 kill-switch 合并实现),保证线上 tokenizer/xgrammar 异常时不回滚镜像即可拉起服务。
- 建议:把「构建失败」与「必须失败」解耦:默认构建失败时记 ERROR 并返回空字符串,交由已就绪的 C++ 降级路径在真正的 grammar 请求上返回 INVALID_PARAMS;仅当服务级 think 模式开启(每个请求都走 grammar)或运维显式要求严格模式时才 fail-fast。同时提供显式开关(如
- --grammar_backend / GRAMMAR_BACKEND 无过渡删除,存量启动脚本升级即退出且失去唯一关停开关 @
rtp_llm/server/server_args/grammar_group_args.py:6- 建议:保留
--grammar_backend(GRAMMAR_BACKEND)为已废弃参数:解析后打印一次 deprecation warning 并忽略,或把none映射为新的关闭开关(与上一条的--grammar_enable合并实现),下个版本再删。并在 PR description / 发布说明中列出本次移除的运维面(--grammar_backend、GRAMMAR_BACKEND、pybind 上的override_stop_tokens),说明「关闭语法约束」的新方式。
- 建议:保留
- think 模式改为硬依赖 grammar 约束,think + beam / num_return_sequences>1 由可用变报错 @
rtp_llm/config/response_format_builder.py:71- 建议:在 PR description / 发布说明中明确列出该不兼容变更(think 自本版起需要 grammar backend 可用,且不再支持 beam / num_return_sequences>1),并给出受影响入口与回滚口径;为「backend 不可用但请求带 in_think_mode」提供可运维兜底(显式配置项决定回退旧 DFA 行为还是拒绝),而不是让全部 think 请求在 stream 构造期失败。
- MTP 能力校验默认拒绝所有未声明 processor,tree_decode_config 变为启动期抛异常 @
rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:230- 建议:保留 fail-fast 方向但补两点:一是把启动期检查从 assert 改为返回可读的启动失败状态,并提供服务级开关(默认严格)允许把不兼容组合降级为 WARN + 关闭 MTP;二是在 PR description / release note 中列出受影响组合与回滚手段。
prepareStreams的拒绝建议同时打一条限频告警日志,便于线上按触发类型定位。
- 建议:保留 fail-fast 方向但补两点:一是把启动期检查从 assert 改为返回可读的启动失败状态,并提供服务级开关(默认严格)允许把不兼容组合降级为 WARN + 关闭 MTP;二是在 PR description / release note 中列出受影响组合与回滚手段。
- raw generate 通道的 response_format 因 setattr 绕过校验而必然 400 @
rtp_llm/config/generate_config.py:153- 建议:二选一:(1)在
update()/update_and_pop()中对response_format执行ResponseFormat.model_validate(value)(或开启validate_assignment=True),使 raw 与 OpenAI 通道行为一致;(2)若确定 raw 通道不再支持该字段,把错误文案改为可执行提示(明确指向json_schema/regex/ebnf/structural_tag),并在 PR 描述与对外文档中列为 breaking change。
- 建议:二选一:(1)在
Non-blocking Suggestions
P2
- proto 字段 73 删除未补 reserved,与同文件既有惯例不一致且留下 wire 号复用隐患 @
rtp_llm/cpp/model_rpc/proto/model_rpc_service.proto:138- 建议:补
reserved 73;与reserved "response_format";,与同 message 的reserved 42保持一致;并在 PR 说明中写明「旧 frontend + 新 backend 时 response_format 不再生效」及升级顺序(先升 frontend)。
- 建议:补
- grammar 编译失败的状态码在 factory 边界被压平为 INVALID_PARAMS,丢失可重试语义 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:120- 建议:在工厂做一次
absl::StatusCode→ErrorCode的显式映射:kInvalidArgument→ INVALID_PARAMS,kResourceExhausted→ MALLOC_ERROR/可重试码,其余 → EXECUTION_EXCEPTION;并补一条断言该映射关系的单测,避免后端好不容易分好的类别在边界被抹平。
- 建议:在工厂做一次
- 编译异常分类把 logic_error 标为可重试,且编译无时间预算、输入上限与指标 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:28- 建议:补
catch (const std::logic_error&)并归入InvalidArgument(不加 retryable 字样);对key_string长度设上限并在超限时返回 INVALID_PARAMS;给编译加时间预算,超时返回可归因错误码而非占满请求线程;把elapsed_ms与失败计数上报为 metric 以观测编译长尾。
- 建议:补
- 非 CUDA 构建下 pin_memory() 抛异常,grammar 解码在 CPU/ARM 目标上不可用且零覆盖 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:236- 建议:条件化固定内存:仅在
#if USING_CUDA且目标 logits 位于 CUDA 设备时使用pin_memory(),否则退回普通 CPU 张量(或 try 失败后保留非 pinned 缓冲并只记一次 WARNING)。补一个不依赖 GPU 的GrammarLogitsProcessorCPU 用例,避免该分支长期无覆盖。
- 建议:条件化固定内存:仅在
- decode 侧复用 pinned host buffer 缺少 event 保护,未复用仓内既有解法 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:268- 建议:二选一:(1)复用
Sampler的轮转 slot +torch::Event保护reusable_bitmask_cpu_;(2)若确认调用点始终存在同步屏障,则在DecodeMaskBuilder上补一段与SpecLogitsVerifyRunner.h同风格的 single-flight 注释,写清依赖的同步点,避免后续在 CUDA graph 化的 decode 路径上复用时静默读到被覆写的掩码。
- 建议:二选一:(1)复用
- grammar backend 存放在函数内可变静态变量,init 重复构建且无同步与隔离手段 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:66- 建议:把 backend 所有权上移到 engine/executor 并以参数注入工厂,彻底移除进程级可变静态;若本次不便重构,至少把接口拆成
setGrammarBackend()与返回值拷贝的getGrammarBackend(),并给init()加幂等保护(配置相同则跳过重建)+std::call_once,把「只允许启动期写一次」变成代码强制。另建议把keyFromGenerateConfig与 beam/多序列校验拆到独立的 grammar 请求解析单元。
- 建议:把 backend 所有权上移到 engine/executor 并以参数注入工厂,彻底移除进程级可变静态;若本次不便重构,至少把接口拆成
- ThinkModeLogitsProcessor 沦为生产不可达代码,max_batch_size 形参失效,文档与实现不符 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:99- 建议:二选一并在本 PR 内闭环:删除
ThinkModeLogitsProcessor.{h,cc}、其单测与 BUILD 条目,或明确保留为 backend 不可用时的显式回退分支并补选择逻辑单测。无论哪种都请同步更新docs/backend/logits_processor.md与dash_sc/think.py注释,移除失效的max_batch_size形参与实参,并说明引擎侧 think 字段现为「接收但不生效」以免后续读者误判。
- 建议:二选一并在本 PR 内闭环:删除
- 新增 GRAMMAR_ 错误码全部落入 INTERNAL 分类,与错误语义不匹配* @
rtp_llm/config/exceptions.py:45- 建议:按语义补分类:608/609 用
INVALID_OUTPUT,611/612 视定位用BAD_REQUEST或保留 INTERNAL 但在注释中标注为启动配置问题,610/613 保留 INTERNAL;并补一条按分类映射的断言测试,避免后续新增码再次默认落入 INTERNAL。
- 建议:按语义补分类:608/609 用
- json_format 删除后 raw generate 通道静默丢弃,OpenAI 通道对 false 也返回 422 @
rtp_llm/config/generate_config.py:444- 建议:统一为 fail-fast:
json_format: false/None 视为未设置以保持兼容,为真时抛与 raw 通道一致的错误;raw 通道在_validate_fields()或 remain 键检查中识别json_format并同样报错,提示改用response_format。两侧各补一条用例,并在发布说明中列出破坏性变更清单。
- 建议:统一为 fail-fast:
- has_bounded_region 只检查 max_tokens,漏检 max_chars 导致守卫可被绕过 @
rtp_llm/config/grammar_constraint.py:61- 建议:把判定改为
max_tokens与max_chars任一非空即视为 bounded region(建议把预算字段名集中为一个常量集合,后续新增只改一处),并补两条用例分别用max_tokens/max_chars锁定拒绝行为。
- 建议:把判定改为
- legacy structures/triggers 形态的 structural_tag 在 think 模式下必然失败且错误文案误导 @
rtp_llm/config/grammar_constraint.py:51- 建议:把形状校验与归一化统一到
GrammarConstraint:将 legacystructures/triggers转换为{"type":"structural_tag","format":{...}},或在validate_not_empty旁增加validate_shape,让直传字段与response_format复用同一套校验;若确定不支持该形状,请在入口提前拒绝并给出准确文案,并补一条「legacy 形状 + thinking」的用例锁定行为。
- 建议:把形状校验与归一化统一到
- vocab 空洞以空字符串填充,语义未明确且缺少屏蔽覆盖 @
rtp_llm/config/grammar_tokenizer_info.py:28- 建议:明确空洞语义并补覆盖:或改用不可能被生成的唯一占位串(如
f"<|unused_{i}|>"),或在 C++ 构建 TokenizerInfo 后显式把空串 token 标记为不可生成;同时补一个端到端用例,构造带空洞的 vocab 并断言未映射 id 在生成的 bitmask 中为禁止位。
- 建议:明确空洞语义并补覆盖:或改用不可能被生成的唯一占位串(如
- MultiSeqLogitsProcessor 写 EOS 掩码缺少 vocab 边界校验,存在越界写风险 @
rtp_llm/cpp/models/logits_processor/MultiSeqLogitsProcessor.cc:39- 建议:在
process()起始处校验eos_token_id_ >= 0 && eos_token_id_ < vocab_size,越界时返回与forceEos语义一致的错误码(可复用GRAMMAR_EOS_OUT_OF_VOCAB),并补一条eos_token_id == vocab_size的边界用例,使两条 EOS 掩码路径共享同一边界契约。
- 建议:在
- stream_idx 语义跨两个不同容器隐式对齐,缺少一致性断言 @
rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:1061- 建议:不要依赖两个容器的偶然同序:让
runSpecLogitsVerify也从stream_groups.allStreams()取流并作为唯一真源,或在入口加显式不变量断言(如断言 context streams 为空);同时补一个「decode 批次含多流」的单测,断言 stream_idx→logits row 映射与流身份一致。
- 建议:不要依赖两个容器的偶然同序:让
- TokenizerInfo 以 RAW 重建的兼容性 workaround 缺少等价性测试,create 降级分支零覆盖 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:81- 建议:补一组等价性用例:对同一份 BYTE_LEVEL 与 BYTE_FALLBACK 词表,分别用原生构造与本 workaround 构造 TokenizerInfo,断言同一 grammar 下 fillBitmask 结果逐位一致。再补四条轻量降级用例(空 json、非法 json、空词表、未知 key_type),断言返回
nullptr或kInvalidArgument且 message 非空——均不需要 GPU。
- 建议:补一组等价性用例:对同一份 BYTE_LEVEL 与 BYTE_FALLBACK 词表,分别用原生构造与本 workaround 构造 TokenizerInfo,断言同一 grammar 下 fillBitmask 结果逐位一致。再补四条轻量降级用例(空 json、非法 json、空词表、未知 key_type),断言返回
- grammar smoke 校验存在静默放行路径、完全短路 golden 比较,且唯一 oracle 无单测 @
rtp_llm/test/smoke/openai_comparer.py:230- 建议:对齐 dash 侧 fail-closed:
response_format缺失与is_stream两条分支均显式抛SmokeException,并把「取约束描述 + 缺失即失败」抽成两个 comparer 共用的辅助函数。同时补一个纯 stdlib 的grammar_constraint_validator_test.py,对每个validate_*覆盖通过/违规样本(尤其 dangling trigger、excludes命中、at_least_one未满足、reasoning 未出现 end marker),并把空 schema 分支改为抛错。
- 建议:对齐 dash 侧 fail-closed:
- tokenizer 家族探测函数在单测中被整体 mock,生产导入入口亦无覆盖 @
rtp_llm/config/test/grammar_tokenizer_info_test.py:73- 建议:为三个探测函数各补一组 fake tokenizer 直测(tiktoken 各命中路径、sentencepiece 各候选属性、全不命中返回 False),
_get_hf_tokenizer_json补backend_tokenizer缺失与to_str非 callable 两个失败分支;另补一个直接走from rtp_llm.ops import serialize_grammar_tokenizer_info的用例,使打包位置与模块名解析回归能在单测阶段暴露。
- 建议:为三个探测函数各补一组 fake tokenizer 直测(tiktoken 各命中路径、sentencepiece 各候选属性、全不命中返回 False),
- xgrammar pin 在上游未合并特性分支 commit,且另开一套依赖声明入口 @
3rdparty/xgrammar/repositories.bzl:8- 建议:优先把依赖指向上游已合并的 tag/commit,并把仓库内需要的差异沉淀为
3rdparty/xgrammar/*.patch(与 flashinfer/cutlass 既有做法一致);若必须跟随未合并分支,请在注释中记录上游 PR 链接与切换到正式版本的时间点。同时建议把xgrammar_deps()合并进git_deps(),避免依赖声明入口分裂。
- 建议:优先把依赖指向上游已合并的 tag/commit,并把仓库内需要的差异沉淀为
- vendored dlpack 头版本与 BUILD 注释不符,且未记录 upstream 出处与 license @
3rdparty/dlpack/BUILD:6- 建议:把注释更正为实际版本并补记 upstream 仓库与 commit/tag、随附 Apache-2.0 license 文件;建议同时确认所选版本与 libtorch 自带 dlpack 头在 DLTensor 布局上兼容,并在注释中写明该结论与选择该版本的原因(xgrammar matcher.h 的 DLTensor 用法所需),便于后续升级判断。
- pybind 类型存根遗漏 terminate_without_stop_token,跨语言配置契约不完整 @
rtp_llm/ops/libth_transformer_config.pyi:544- 建议:按仓库自带的 stub 生成流程重新生成
.pyi并补齐terminate_without_stop_token: bool与__repr__,确认 stub 字段集合与def_readwrite列表一一对应;建议重新生成而非手改,避免下次继续漂移。
- 建议:按仓库自带的 stub 生成流程重新生成
- GrammarConfig unpickle 兼容阶梯包含仅存在于本分支中间提交的布局,且靠类型嗅探区分 @
rtp_llm/cpp/pybind/ConfigInit.cc:1366- 建议:只保留「当前 5 元组 + base main 的 legacy 布局」两条路径,删除中间布局分支与对应用例;更稳的做法是在 tuple 首位放显式 schema version(或改用 dict state),未知版本 fail-fast,并把报错文案补上
GrammarConfig前缀。若该 pickle 仅用于同一二进制的 spawn 传递,请在注释中写明该场景以说明为何不需要跨版本兼容层。
- 建议:只保留「当前 5 元组 + base main 的 legacy 布局」两条路径,删除中间布局分支与对应用例;更稳的做法是在 tuple 首位放显式 schema version(或改用 dict state),未知版本 fail-fast,并把报错文案补上
- 新增 grammar 相关测试目标的执行属性与 data 依赖不符合仓库惯例 @
rtp_llm/cpp/engine_base/grammar/test/BUILD:13- 建议:给
xgrammar_backend_cpp_test补exec_properties与env(或把 deps 收窄到不含 torch 的目标并注明),并删除未使用的torch_depsload;把config_pickle_test的 data 改为//rtp_llm/libs:libth_transformer_config_so(或frontend_libs),并与同类 py_test 一致补exec_properties。
- 建议:给
- 声称验证 CPU fallback 的测试强绑 CUDA 依赖与 GPU 执行属性,非 CUDA 平台无覆盖 @
rtp_llm/models_py/bindings/core/test/BUILD:53- 建议:要么去掉
exec_properties与 CUDA-only deps,让该 target 真正成为 CPU-only 用例并加入非 CUDA 构建的测试集;要么修正注释,明确它只是「在 GPU 机器上走 CPU 分支」,并另建一个可在无 GPU 环境执行的 target 覆盖 fallback。
- 建议:要么去掉
- MTP decode 端到端接线与 dispatcher 边界分支缺少 CI 级覆盖 @
rtp_llm/cpp/normal_engine/speculative/test/MtpExecutorTest.cc:611- 建议:在 MTP decode 用例里给 stream 挂
FakeGrammarSpecLogitsProcessor,断言目标 logits 被掩码、accept_len被 cap 截断、accept_tokens[cap]被替换为目标模型 token,把 verify → gather → sample → apply 的接线顺序纳入门禁。NormalOutputDispatcherTest补三个用例:cur_batch_size=2且错误只在第二行、success_cpu={true,false}、以及两类错误同时命中时的优先级。
- 建议:在 MTP decode 用例里给 stream 挂
- 新增 grammar CLI 参数的默认值断言为同源自证,env 分支与边界值零覆盖 @
rtp_llm/server/server_args/test/server_args_test.py:423- 建议:补一个 env 用例:设置
GRAMMAR_TERMINATE_WITHOUT_STOP_TOKEN=1、GRAMMAR_COMPILER_CACHE_BYTES=67108864、CONSTRAINED_JSON_DISABLE_ANY_WHITESPACE=1后调用_setup(),断言落到grammar_config的值与类型正确(bool 而非字符串);再补GRAMMAR_COMPILER_CACHE_BYTES=0与GRAMMAR_NUM_WORKERS=0两条边界断言,固化「<=0 表示不限」与「clamp 到至少 1」的契约。
- 建议:补一个 env 用例:设置
- enqueue 对同一请求重复调用 trans_input,而 validate() 已变为有副作用 @
rtp_llm/cpp/model_rpc/model_rpc_client.py:556- 建议:删除 :556 这次无用调用,只在选定目标地址后构建一次
input_pb,让「validate 的原地改写只发生一次」成为显式约束而非依赖内部幂等标记;如需保留「校验失败早于地址选择抛出」的意图,改为只调用input_py.generate_config.validate()。同时把_trans_jsonable_option简化为「None 跳过、否则直接赋值」并把类型收窄为Optional[str],使 dict→紧凑 JSON 的唯一实现留在dump_compact_json。
- 建议:删除 :556 这次无用调用,只在选定目标地址后构建一次
- normalize_think_tag 转义语义收窄未记录,env 配置的旧转义静默改变行为 @
rtp_llm/config/response_format.py:15- 建议:在
test_normalize_think_tag中显式补齐语义边界:为\t/\uXXXX/ 字面反斜杠各写一条断言,明确「仅\n参与解转义,其余保持原样」;并在 PR description 或配置文档中记录这次收窄与迁移建议(改用真实字符或仅用\n),避免被当作无意回归。
- 建议:在
- 新增 grammar 字段测试未覆盖 trans_input 就地 finalize 与 dict 形态归一化 @
rtp_llm/cpp/model_rpc/test/model_rpc_client_test.py:171- 建议:补一个
GenerateConfig(response_format=...)用例,调用trans_input后断言generate_config_pb.json_schema.value为期望紧凑 JSON 且config.response_format is None,把「trans_input 会就地 finalize 配置」这一新契约固化;再补一个 dict 入参用例(如json_schema={"type":"object"})断言 pb 上得到紧凑串,把旧用例的覆盖补回来。
- 建议:补一个
P3
- 大范围格式化重排混入逻辑变更,跨语言 SYNC 注释被折断 @
rtp_llm/cpp/models/logits_processor/RecommendationLogitsProcessor.cc:52- 建议:将纯格式化重排拆为独立的 format-only commit(或独立 PR),使功能 commit 的 diff 只包含签名适配、
neg_inf替换与grammar_constraint_only透传等语义变更;若仓库正在推进全量 black/clang-format 落地,建议单独一次性提交并在.git-blame-ignore-revs中登记,同时确认折断的 SYNC 注释仍能被跨语言一致性检查识别。
- 建议:将纯格式化重排拆为独立的 format-only commit(或独立 PR),使功能 commit 的 diff 只包含签名适配、
- 请求级 grammar_terminate_without_stop_token 被解析但全仓无消费者 @
rtp_llm/cpp/engine_base/stream/GenerateConfig.h:57- 建议:要么删除该字段(连同
to_string/JSONIZE登记),要么在解析到true时打印一次限频 WARNING 说明「请求级开关已废弃,实际由服务级 GRAMMAR_TERMINATE_WITHOUT_STOP_TOKEN 决定」,避免调用方误以为可按请求覆盖服务策略。
- 建议:要么删除该字段(连同
- 运行期形状校验以异常逃出窄 catch,失败爆炸半径为整个 decode step @
rtp_llm/cpp/models/logits_processor/SpecLogitsVerifyRunner.cc:234- 建议:若确认这些条件属引擎内部编程契约,请在
run()的注释中写明「形状违约按编程错误抛出、不做 per-stream 降级」,让两种错误语义的差异显式;若希望限制爆炸半径,则复用已有的makeFailureResultForActiveStreams(task, error)把失败写入受影响 stream 的processor_errors,并在DecodeMaskBuilder::apply末尾追加catch (const std::exception&)兜底,使底层异常退化为单条流错误。
- 建议:若确认这些条件属引擎内部编程契约,请在
- BitmaskUtils 头注释声明 abort,实现实际抛异常 @
rtp_llm/cpp/models/logits_processor/BitmaskUtils.h:30- 建议:把注释改为准确描述(「以 RTP_LLM_CHECK 失败,默认抛 RTPException;配置开启 core dump 时 abort」),使调用方能据此决定是否需要在自己的 try 中兜底。
- matcher_ 空值契约在三个入口不一致,且 process 的空值分支为 fail-open @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:377- 建议:统一契约(推荐前者):在构造函数用
RTP_LLM_CHECK_WITH_INFO(matcher_ != nullptr, ...)把非空固化为类不变量,随后删除三处冗余判空——工厂路径已保证非空,冗余判空反而把「掩码未生效」伪装成正常路径;或在prepareSpeculative补同样判空并让mtpCapability()在无 matcher 时返回UNSUPPORTED。
- 建议:统一契约(推荐前者):在构造函数用
- grammar BUILD 声明的 Tier 依赖约束未通过 visibility 落地,且过渡层无历史调用方 @
rtp_llm/cpp/engine_base/grammar/BUILD:35- 建议:把 Tier 约束落到
visibility(例如grammar_xgrammar显式限定为["//rtp_llm/cpp/engine_base/grammar:__subpackages__", "//rtp_llm/cpp/models/logits_processor:__pkg__"],必要时白名单 test 包),让约束由构建系统强制;同时删除grammar_cpp_lib并把新增调用方直接指向grammar_xgrammar,避免引入一个从第一天起就是技术债的过渡目标。
- 建议:把 Tier 约束落到
- processor 长度不一致这一内部不变量被归入 UNKNOWN_ERROR @
rtp_llm/cpp/engine_base/stream/GenerateStream.cc:964- 建议:为该不变量新增专用错误码(如
LOGITS_PROCESSOR_STATE_MISMATCH)或复用与 grammar 状态相关的细分码,使告警与排查可按码定位;错误信息中已含 processor_index 与两侧长度,保持即可。
- 建议:为该不变量新增专用错误码(如
- 测试辅助函数失败时返回空 bundle,导致 CI 段错误而非可读失败 @
rtp_llm/cpp/models/logits_processor/test/GrammarLogitsProcessorTest.cc:58- 建议:把校验改为
ASSERT_*(配合 void 返回的辅助函数 + 出参),或在返回后于各调用点加ASSERT_TRUE(proc.proc && proc.matcher),保证编译失败时给出带 status 文本的可读失败。
- 建议:把校验改为
Checklist Violations (22 fail / 58 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
grammar BUILD 声明的 Tier 依赖约束未通过 visibility 落地,且过渡层无历史调用方
:35注释写「xgrammar-specific impl; only Tier 0 (this package) and Tier 1 (logits_processor) may depend」,但该 package 顶部是package(default_visibility = ["//visibility:public"])(:4),任何目标都能直接依赖grammar_xgrammar,约束仅存在于注释。同时grammar_cpp_lib(:58-66)被标注为「Transitional umbrella; deleted once callers move onto :grammar_xgrammar」的纯 passthrough,而本 PR 是该依赖的首次引入(models/BUILD:78与两处 test BUILD),不存在需要迁移的历史调用方,属新增即废弃的过渡层。 - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
normalize_think_tag 转义语义收窄未记录,env 配置的旧转义静默改变行为
新实现normalize_think_tag仅value.replace(r"\n", "\n")(:13-15),而被替换的旧实现为encode("utf-8").decode("unicode_escape");openai_response_test.py:2569新增断言normalize_think_tag(r"<think>\t") == r"<think>\t",即\t不再解转义,generate_config_test.py也把 fixture 从真实换行改为字面量。通过环境变量配置think_start_tag/think_end_tag且使用\t、\xNN、\uXXXX的既有部署,其 tag 会由「解转义后的控制字符」变为「字面反斜杠序列」,进而影响ReasoningFormat.tag_end与 structural_tag 包装,而测试只锁定了新语义。 - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
grammar BUILD 声明的 Tier 依赖约束未通过 visibility 落地,且过渡层无历史调用方
:35注释写「xgrammar-specific impl; only Tier 0 (this package) and Tier 1 (logits_processor) may depend」,但该 package 顶部是package(default_visibility = ["//visibility:public"])(:4),任何目标都能直接依赖grammar_xgrammar,约束仅存在于注释。同时grammar_cpp_lib(:58-66)被标注为「Transitional umbrella; deleted once callers move onto :grammar_xgrammar」的纯 passthrough,而本 PR 是该依赖的首次引入(models/BUILD:78与两处 test BUILD),不存在需要迁移的历史调用方,属新增即废弃的过渡层。 - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
processor 长度不一致这一内部不变量被归入 UNKNOWN_ERROR
validateLogitsProcessorState(:953-971)在 processor 已提交长度与 stream 输出长度不一致时返回ErrorInfo(ErrorCode::UNKNOWN_ERROR, ...)。这是本 PR 新引入的内部一致性检查,语义明确(processor 状态与权威 token 历史漂移),却使用最泛化的错误码,与同 PR 为 grammar 场景专门新增GRAMMAR_PARSER_REJECTED_TOKEN/GRAMMAR_VOCAB_EXCEEDS_MODEL_VOCAB等细分码的风格不一致;线上按错误码聚合告警时,该失败会与collectStreamSamplerError产出的采样失败等其他 UNKNOWN_ERROR 混在一起。 - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
xgrammar pin 在上游未合并特性分支 commit,且另开一套依赖声明入口
注释指明 pin 的是上游feat/anytext-budget分支(commit = "60fc70ee..."),未合入 main。第三方仓库的 feature 分支被 force-push 或删除后该 commit 可能被 GC,届时new_git_repository无法 fetch,所有构建(含历史 tag 重建)都会失败,且目录内没有 patch 文件或镜像兜底。另外仓库既有依赖统一在deps/git.bzl的git_deps()中声明,本 PR 在WORKSPACE额外 load 了一套xgrammar_deps(),形成两条并行的依赖注册路径。 - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
运行期形状校验以异常逃出窄 catch,失败爆炸半径为整个 decode step
run()的 try 内用RTP_LLM_CHECK_WITH_INFO校验运行期张量形状与规模(:234-241、:133-142),但 catch 只覆盖std::bad_alloc与c10::Error(:261-265)。AssertUtils.cc:13-22抛出的RTPException(public std::runtime_error)不属这两类,会逃出run()与decodeStep,把单批形状异常放大为整个 decode step 失败(core dump 配置开启时甚至abort()),与同函数内 processor 错误按 per-streamprocessor_errors优雅降级(:185-188)不一致。MtpExecutorTest.cc:714的EXPECT_THROW表明该抛出是有意设计,故此处按 P3 记录。 - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
matcher_ 空值契约在三个入口不一致,且 process 的空值分支为 fail-open
process()(:308)、updateStatus()(:344)与acceptCommittedLocked()(:387)都以!matcher_早退,说明该类把空 matcher 当成合法状态,且process的早退是 fail-open(不施加任何约束);但prepareSpeculative直接prepareSpecMask(*matcher_, ...)解引用(:377),且GrammarLogitsProcessor.h:29-31的mtpCapability()无条件返回SPEC_VERIFY。同一前置条件在同一个类里给出两套矛盾契约:普通解码静默放行,MTP 路径会在SpecLogitsVerifyRunner.cc:184空指针解引用崩溃。 - [6.1] Quality — Mega-PR 已拆分为独立变更 → issue
大范围格式化重排混入逻辑变更,跨语言 SYNC 注释被折断
RecommendationLogitsProcessor.cc与其测试合计产生千行级 diff,但既无新增也无删除TEST_F,语义变更仅process/updateStatus新签名适配与约 13 处断言常量替换(-INFINITY→BaseLogitsProcessor::neg_inf),其余全部为 clang-format 列对齐重排;测试文件里出现@@ -252,206 +250,206 @@这类等量 hunk,// SYNC: 以下判定条件必须与 Python ...注释也被重排折断并与上一句合并。rtp_llm/test/smoke/case_runner.py约 460 行 diff 中仅 2 行为功能变更。在 132 文件 PR 中这类噪声让真实语义变更难以定位,也污染后续 bisect。 - [6.1] Quality — 逻辑变更未混入无关格式化 → issue
大范围格式化重排混入逻辑变更,跨语言 SYNC 注释被折断
RecommendationLogitsProcessor.cc与其测试合计产生千行级 diff,但既无新增也无删除TEST_F,语义变更仅process/updateStatus新签名适配与约 13 处断言常量替换(-INFINITY→BaseLogitsProcessor::neg_inf),其余全部为 clang-format 列对齐重排;测试文件里出现@@ -252,206 +250,206 @@这类等量 hunk,// SYNC: 以下判定条件必须与 Python ...注释也被重排折断并与上一句合并。rtp_llm/test/smoke/case_runner.py约 460 行 diff 中仅 2 行为功能变更。在 132 文件 PR 中这类噪声让真实语义变更难以定位,也污染后续 bisect。 - [6.1] Software Engineering — DIP:高层策略不依赖非必要具体细节 → issue
grammar backend 存放在函数内可变静态变量,init 重复构建且无同步与隔离手段
grammarBackend()返回函数内static std::shared_ptr<XGrammarBackend>的非 const 引用,init()(:79)直接赋值,而createLogitsProcessors在请求线程中auto& backend = grammarBackend();后立即解引用(:106-118)。注释承认「进程内单引擎」假设,但无call_once或断言保护:NormalExecutor.cc与MtpExecutor.cc:328都会调init(),warm-up 与正式 executor 会重复执行XGrammarBackend::create(重复反序列化整份词表);一旦出现引擎重建/热加载即为对 shared_ptr 的无同步并发读写。该工厂同时还持有 static backend、解析GrammarKeyCpp、校验 beam/多序列、装配所有 processor。 - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
enqueue 对同一请求重复调用 trans_input,而 validate() 已变为有副作用
enqueue在 :556 与 :578 各调用一次trans_input(input_py),:556 的结果被立即覆盖丢弃,其间无使用者。该重复为存量代码,但trans_input:109现在会调用GenerateConfig.validate(),而本 PR 把validate()从「纯字段校验」扩展为「校验 + response_format 投影 + grammar 归一化 + finalize 断言」,于是每个请求在前端热路径上多做一整轮校验与语法投影,外加一次被丢弃的完整GenerateInputPB构建;当前不出现二次包装仅依赖ResponseFormatBuilder.apply的_reasoning_envelope_applied幂等位。同文件_trans_jsonable_option(:67-68)的 dict 分支在validate()之后已不可达,与dump_compact_json构成重复实现。 - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
grammar BUILD 声明的 Tier 依赖约束未通过 visibility 落地,且过渡层无历史调用方
:35注释写「xgrammar-specific impl; only Tier 0 (this package) and Tier 1 (logits_processor) may depend」,但该 package 顶部是package(default_visibility = ["//visibility:public"])(:4),任何目标都能直接依赖grammar_xgrammar,约束仅存在于注释。同时grammar_cpp_lib(:58-66)被标注为「Transitional umbrella; deleted once callers move onto :grammar_xgrammar」的纯 passthrough,而本 PR 是该依赖的首次引入(models/BUILD:78与两处 test BUILD),不存在需要迁移的历史调用方,属新增即废弃的过渡层。 - [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue
matcher_ 空值契约在三个入口不一致,且 process 的空值分支为 fail-open
process()(:308)、updateStatus()(:344)与acceptCommittedLocked()(:387)都以!matcher_早退,说明该类把空 matcher 当成合法状态,且process的早退是 fail-open(不施加任何约束);但prepareSpeculative直接prepareSpecMask(*matcher_, ...)解引用(:377),且GrammarLogitsProcessor.h:29-31的mtpCapability()无条件返回SPEC_VERIFY。同一前置条件在同一个类里给出两套矛盾契约:普通解码静默放行,MTP 路径会在SpecLogitsVerifyRunner.cc:184空指针解引用崩溃。 - [6.1] Software Engineering — SRP:模块/类职责单一 → issue
grammar backend 存放在函数内可变静态变量,init 重复构建且无同步与隔离手段
grammarBackend()返回函数内static std::shared_ptr<XGrammarBackend>的非 const 引用,init()(:79)直接赋值,而createLogitsProcessors在请求线程中auto& backend = grammarBackend();后立即解引用(:106-118)。注释承认「进程内单引擎」假设,但无call_once或断言保护:NormalExecutor.cc与MtpExecutor.cc:328都会调init(),warm-up 与正式 executor 会重复执行XGrammarBackend::create(重复反序列化整份词表);一旦出现引擎重建/热加载即为对 shared_ptr 的无同步并发读写。该工厂同时还持有 static backend、解析GrammarKeyCpp、校验 beam/多序列、装配所有 processor。 - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
声称验证 CPU fallback 的测试强绑 CUDA 依赖与 GPU 执行属性,非 CUDA 平台无覆盖
注释写「Exercise the CPU fallback without initializing or allocating a GPU」(:53),但该 target 仍声明exec_properties = {"gpu": "H20"}(:64),deps 含//rtp_llm/models_py/bindings/cuda/ops:flashinfer_runtime(:60)并+ torch_deps()(:62)。结果是一个自称无需 GPU 的用例仍占用 H20 执行槽位,且真正的非 CUDA 平台(ROCm/ARM/CPU 构建)依然没有任何 packed-mask 覆盖——与GrammarLogitsProcessor无条件pin_memory()的跨平台缺陷互为盲区。 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
新增 grammar 字段测试未覆盖 trans_input 就地 finalize 与 dict 形态归一化
proto 字段 73 删除后,response_format能继续工作完全依赖trans_input内validate()把它投影为 typed 字段。但新用例(:171-209)4 个 case 全部只设置已是字符串的json_schema/regex/ebnf/structural_tag,并断言config.model_dump() == config_before_rpc(不发生改写)——该不变量恰好只在「无 response_format」时成立。被替换掉的旧用例原本覆盖了response_format与 dict 形态经序列化后落到 pb 的紧凑串,替换后这段端到端形态转换在 RPC 边界上无任何覆盖。 - [6.1] Tests — 被删除测试有等价替代覆盖 → issue
新增 grammar 字段测试未覆盖 trans_input 就地 finalize 与 dict 形态归一化
proto 字段 73 删除后,response_format能继续工作完全依赖trans_input内validate()把它投影为 typed 字段。但新用例(:171-209)4 个 case 全部只设置已是字符串的json_schema/regex/ebnf/structural_tag,并断言config.model_dump() == config_before_rpc(不发生改写)——该不变量恰好只在「无 response_format」时成立。被替换掉的旧用例原本覆盖了response_format与 dict 形态经序列化后落到 pb 的紧凑串,替换后这段端到端形态转换在 RPC 边界上无任何覆盖。 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
测试辅助函数失败时返回空 bundle,导致 CI 段错误而非可读失败
makeProcessorFromKey用非致命的EXPECT_TRUE校验 backend 与createMatcherFromKey,失败时return {}(:61-69),返回的ProcessorBundle中proc/matcher均为空shared_ptr;而operator->()直接return proc.get()(:53-55),全部调用方紧接着执行proc->process(...)或*proc.matcher。一旦 grammar 编译失败(例如 xgrammar pin 变化导致 structural_tag 语法不被接受),测试进程以段错误退出,CI 里看不到EXPECT_TRUE打印的matcher_or.status().ToString()。
RTP-LLM Checklist
- [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue
请求级 grammar_terminate_without_stop_token 被解析但全仓无消费者
C++GenerateConfig保留grammar_terminate_without_stop_token(注释「Legacy raw-request compatibility only」),并在to_string(:186)与JSONIZE(:237)中登记;但全仓搜索显示除该文件与 Python 侧「字段已移除」的断言用例外没有任何读取点——matcher 行为完全由服务级GrammarConfig.terminate_without_stop_token经XGrammarBackend.cc:106决定。generate_config_test.py:947-977甚至断言它不在GenerateConfig.model_fields且会留在update_and_pop的 remain 中。结果是一个可被 JSON 传入、会被打印、却静默无效的字段。 - [I] 代码质量 — 同一功能用统一工具函数 → issue
xgrammar pin 在上游未合并特性分支 commit,且另开一套依赖声明入口
注释指明 pin 的是上游feat/anytext-budget分支(commit = "60fc70ee..."),未合入 main。第三方仓库的 feature 分支被 force-push 或删除后该 commit 可能被 GC,届时new_git_repository无法 fetch,所有构建(含历史 tag 重建)都会失败,且目录内没有 patch 文件或镜像兜底。另外仓库既有依赖统一在deps/git.bzl的git_deps()中声明,本 PR 在WORKSPACE额外 load 了一套xgrammar_deps(),形成两条并行的依赖注册路径。
Python Static-First Checklist
- [P.A] 静态结构与类型纪律 — 禁止 getattr/setattr literal 访问 → issue
raw generate 通道的 response_format 因 setattr 绕过校验而必然 400
字段由Optional[Union[str, Dict[str, Any]]]收紧为Optional[ResponseFormat](diff 已确认),但GenerateConfig(:86-92)未声明任何model_config,即未开启validate_assignment;raw 路径经structure/request_extractor.py:51的update_and_pop以setattr赋值(:444-450),dict/str 不会被 pydantic 转换。随后_validate_fields()→project_response_format()的isinstance(..., ResponseFormat)必然为假,抛response_format must be a validated ResponseFormat object。generate_config_test.py:935-1000已把字符串与 dict 两种载荷的拒绝固化为预期。 - [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
tokenizer 家族探测函数在单测中被整体 mock,生产导入入口亦无覆盖
build_tokenizer_info辅助方法同时 patch 掉_is_fast_tokenizer、_is_tiktoken_tokenizer、_is_sentencepiece_tokenizer、_is_byte_level_tokenizer四个探测函数(:71-93),因此 serialize 用例只验证 metadata 组装。除_is_byte_level_tokenizer有直测(:101-113)外,_is_tiktoken_tokenizer、_is_sentencepiece_tokenizer、_get_hf_tokenizer_json均无任何测试,而它们直接决定vocab_type(RAW/BYTE_LEVEL/BYTE_FALLBACK)与add_prefix_space,取值错误会导致 bitmask 错位而非报错。同时用例用mock.patch.dict(sys.modules, {"rtp_llm.ops": fake_ops})整体替换生产导入入口。
Strengths
- 错误语义端到端闭环:
processor_errors经SamplerOutput→collectStreamSamplerError(NormalOutputDispatcher.cc:24-32 显式注明使用 sampler-input 坐标以兼容 beam 扩批)→StreamUpdateInfo.error_info,违规 token 在 append 之前 early-return,不进入用户可见历史。 - MTP 状态不变量处理严谨:
prepareSpecMask用provisional_accepts精确回滚试探性 accept,rollback 失败时拼接原始错误并markFinished()fail-closed;acceptCommittedLocked的rollback_commit保证整批提交原子性,不残留半提交前缀。 - 新增
committedOutputLen()+GenerateStream::validateLogitsProcessorState()(GenerateStream.cc:953)把「processor 状态与权威 token 历史漂移」变成显式错误,而非静默产出不合语法结果。 - 第三方异常不穿透主循环:
XGrammarBackend::create声明noexcept,对空 metadata / 反序列化失败 / 空词表三条路径统一返回nullptr;RtpGrammarMatcher把 xgrammar 异常收敛为absl::Status。 - 位掩码边界处理完整且有针对性覆盖:
clearBitmaskTokenRange正确区分同字/跨字与end_bit == 0,forceTokenInBitmask对越界 fail-fast;packed-mask 在真实 CUDA kernel 与 CPU fallback 两侧同构覆盖 fp32/fp16/bf16、非连续视图与row_indices越界。 - 跨语言契约正反两面都锁住:
config_pickle_test.py对多种历史 pickle 布局逐一断言并锁定伪造短布局必须抛错;grammar_tokenizer_info_test.py通过 runfiles 直接加载原生.so验证 bytes 词表往返,未用 mock 顶替 native 边界。 - 配置默认值单一来源:grammar CLI 参数
default=全部读取grammar_config.<field>,--grammar_num_workers(C++std::max(1,...))与--grammar_compiler_cache_bytes(<=0不限)的 help 与XGrammarBackend.cc:107-108严格一致;to_string/__repr__只输出tokenizer_info_json长度,避免 MB 级词表进日志。 gatherSpecSamplerInput补上finished_mask.zero_()(MtpBatchStreamProcessor.cc:122),修掉投机批次上torch::empty分配的finished_mask未初始化即被读取的历史隐患。
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1006
Status: BLOCKING
Summary: P0/0 · P1/6 · P2/23 · P3/4
Reviewed: commit 0f4ffd95fa5b · 2026-08-06 12:48 UTC+8
Blocking Issues
P1
- grammar tokenizer 元信息在启动期无条件构建,不支持的 tokenizer 直接阻塞模型加载且无逃生开关 @
rtp_llm/config/grammar_tokenizer_info.py:195- 建议:新增服务级开关(如
--grammar_enable/GRAMMAR_ENABLE,默认 true),关闭时跳过构建并让tokenizer_info_json保持为空,复用XGrammarBackend::create已有的「空串即禁用」路径;或把「tokenizer 类型不支持」降级为 WARNING + 返回空串,让失败在首个 grammar 请求上以LogitsProcessorFactory.cc:108已有的明确文案暴露,同时保留stop_token_ids为空等真正致命配置的 fail-fast。请与下一条发现一并决策出单一失败策略,补一条覆盖rtp_llm_op.start()启动路径的用例(现有test_build_tokenizer_info_rejects_unsupported_tokenizer只覆盖函数级),并在发布说明中列出受影响的 tokenizer 类型与stop_token_ids要求。
- 建议:新增服务级开关(如
- 非空 tokenizer 元信息反序列化/构造失败被静默降级,与 Python 侧声明的启动期 fail-fast 契约相反 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:73- 建议:区分「未配置」与「配置失败」:
tokenizer_info_json.empty()时保持返回 nullptr(对应上一条的显式开关);非空但反序列化/构造失败时向上返回错误让引擎启动失败(与 Python 一致),或至少上报一个明确的「grammar backend disabled」启动指标/健康位,并把当前 ERROR 日志升级为带原因分类的启动结论日志。同时补一条「非空但非法 JSON」的用例。
- 建议:区分「未配置」与「配置失败」:
- --grammar_backend / GRAMMAR_BACKEND 无过渡删除,存量启动脚本升级即退出且失去唯一关停开关 @
rtp_llm/server/server_args/grammar_group_args.py:6- 建议:二选一:(1)保留
--grammar_backend一个版本,标记 deprecated、接受xgrammar并忽略、非法值报错、打印一次 WARNING,下个版本再删;(2)立即删除但新增等价服务级开关(如--grammar_enable=0)以复现原backend=none语义(跳过build_grammar_tokenizer_info()、tokenizer_info_json保持为空)。并在 PR description、docs/backend/logits_processor.md与发布说明中把该 flag/env 与override_stop_tokens明确列为 breaking change 及替代项,同时在ServerArgsGrammarConfigTest中补一条固化该行为的用例。
- 建议:二选一:(1)保留
- think 模式改为硬依赖 grammar 约束,thinking + beam search / num_return_sequences>1 由可用变报错 @
rtp_llm/config/response_format_compiler.py:71- 建议:把「think 模式是否使用 grammar 包裹」与「是否存在用户 grammar 约束」解耦:仅当
final_constraint is not None时才构造 reasoning 包裹并施加 beam/多序列限制,无用户约束的纯 thinking 请求走原有非 grammar 路径。若确实要统一到 grammar 实现,请在 PR description 标注为 breaking change,在docs/backend/sampling_params.md与docs/backend/logits_processor.md写清该限制与替代方案,并补一条 thinking + beam search /n>1的回归用例(当前 8 条 grammar smoke 用例均未覆盖该组合)。
- 建议:把「think 模式是否使用 grammar 包裹」与「是否存在用户 grammar 约束」解耦:仅当
- MTP 准入硬拒绝所有未声明能力的 logits processor,tree_decode_config 变为启动期抛异常 @
rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:45- 建议:补齐灰度与回滚:为
RecommendationLogitsProcessor/MultiSeqLogitsProcessor显式实现mtpCapability(),reason 中写明触发配置(combo_token_size>0、num_return_sequences>1)与可操作指引;并增加默认放行/拒绝可配的开关(如sp_config.mtp_reject_incompatible_logits_processor),或先以 WARNING + 指标观察一个版本再切换为拒绝。tree_decode_config建议改为启动期 WARNING + 自动禁用 tree decode 而非让引擎起不来。同时在docs/backend/logits_processor.md补一张「processor × MTP」兼容矩阵,在 PR description 标注行为不兼容变更,并补一条 Recommendation + MTP 的回归用例(MtpExecutorTest目前只用FakeNormalDecodeOnlyProcessor覆盖)。
- 建议:补齐灰度与回滚:为
- json_format 删除后同一控制项在 raw / OpenAI / DashSC 三条协议上语义不一致,raw 通道静默丢弃 @
rtp_llm/structure/request_extractor.py:50- 建议:统一为一种语义:或在
GenerateConfig加mode="before"的 model_validator 把json_format=True翻译为response_format={"type":"json_object"}并打一次 deprecation 日志、json_format=False视为无操作(与 DashSC 对齐);或在request_extractor对残留的json_format及其它已下线别名 fail-fast 抛ERROR_GENERATE_CONFIG_FORMAT(与 OpenAI 对齐)。无论哪种都请在test_generate_config_validators.py增加 raw 路径断言,并在发布说明中列出json_format、extra_configs内 grammar 字段(api_datatype.py:209-217 新增禁止)以及从请求级降为服务级的grammar_terminate_without_stop_token(现被update()静默忽略,generate_config_test.py:912-942 已固化)这三处下线项。
- 建议:统一为一种语义:或在
Non-blocking Suggestions
P2
- proto 字段 73 删除未补 reserved,与同文件既有惯例不一致且留下 wire 号复用隐患 @
rtp_llm/cpp/model_rpc/proto/model_rpc_service.proto:138- 建议:在
GenerateConfigPB中补reserved 73;与reserved "response_format";,并为grammar_terminate_without_stop_token补相应号位与名称,与同 message 的reserved 42写法一致。若本仓明确只支持 same-build 部署,请像BroadcastLoadRequestPB那样在注释中写清该契约,使后续维护者不必反复推断;若存在混版窗口,建议在QueryConverter::transGenerateConfig对残留的已废弃 grammar 输入显式 fail-fast 或上报告警指标。
- 建议:在
- grammar 编译失败的状态码在 factory 边界被压平为 INVALID_PARAMS,丢失可重试语义 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:120- 建议:在 factory 按
absl::StatusCode分派:kInvalidArgument→INVALID_PARAMS、kResourceExhausted→MALLOC_FAILED(或既有可重试码段)、其余 →EXECUTION_EXCEPTION;并把std::logic_error从「retryable」档拆出。补一条单测断言「OOM 类编译失败不落到 BAD_REQUEST 类别」。若确实不打算区分,则删除 backend 侧的分类逻辑,避免留下误导性抽象。
- 建议:在 factory 按
- ThinkModeLogitsProcessor 沦为生产不可达代码,in_think_mode 无编译产物时约束被静默丢弃 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:100- 建议:在该分支补显式 fail-fast:
if (grammar_key.empty() && config.in_think_mode) return ErrorInfo(ErrorCode::INVALID_PARAMS, "in_think_mode requires a compiled grammar constraint; ...");若要保持宽松,至少RTP_LLM_INTERVAL_LOG(300, WARN, ...)并上报指标使「thinking 约束丢失」可观测,并补一条该入参组合的单测。同时决定ThinkModeLogitsProcessor的去留:删除实现、单测与 BUILD 条目并去掉失效的max_batch_size形参、清理dash_sc/think.py:167注释;或在头文件写明保留原因与重新启用条件。
- 建议:在该分支补显式 fail-fast:
- 进程级 grammar backend 存放于 models 层函数内可变静态量,分层泄漏且 init/read 无同步 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:66- 建议:把 backend 所有权上移到
EngineInitParams/executor(由NormalExecutor/MtpExecutor持有std::shared_ptr<XGrammarBackend>并作为参数传入createLogitsProcessors),消除静态可变状态、跨层依赖与 warm-up 重复构建;若本次不便重构,至少改为std::call_once初始化或加锁访问,并在注释中写明「重复 init 会替换 backend」的可见性约束与 warm-up 路径为何安全。
- 建议:把 backend 所有权上移到
- prepareSpeculative 缺少 matcher_ 空指针校验,与同类方法的不变量相反 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:377- 建议:推荐在构造函数中
RTP_LLM_CHECK_WITH_INFO(matcher != nullptr, ...)让「matcher 非空」成为类不变量并移除三处冗余判空(不变量集中在一处,符合 KISS),并补一条构造期拒绝空 matcher 的单测;若保留可空语义,则prepareSpeculative必须补同样的空值早返回并返回ErrorInfo,同时让mtpCapability()在 matcher 为空时返回UNSUPPORTED+ 原因。
- 建议:推荐在构造函数中
- prepareSpecMask 未校验 bitmask 尺寸契约,且 run() 窄 catch 让请求级错误升级为整个 decode step 失败 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:47- 建议:在
prepareSpecMask入口显式校验bitmask_cpu_out != nullptr与bitmask_size_int32 >= SpecLogitsProcessorRequest::bitmaskWordCount(vocab_size),不满足返回ErrorInfo(ErrorCode::GRAMMAR_BITMASK_BUFFER_TOO_SMALL, ...),把进程级 abort 降级为请求级失败;并把SpecLogitsVerifyRunner::run的 catch 扩到std::exception,用makeFailureResultForActiveStreams把失败收敛到相关流。同时在SpecLogitsProcessorRequest注释中把该尺寸关系写成显式契约,便于跨模块调用方遵守。
- 建议:在
- decode 掩码逐流下发且拷贝为同步语义,抵消 pinned 收益;非 CUDA 目标 grammar 解码零覆盖 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:268- 建议:复用 MTP 侧已有的合并思路:在 sampler 层收集本 step 所有 grammar 流的 CPU bitmask 行,拼成
[rows, words]一次性拷贝并调用带row_indices的批量重载,消除重复实现。若本次不做批量化,至少把拷贝改为non_blocking=true并配套双缓冲或 CUDA event,确保 pinned 源缓冲在下次prepareMask复写前拷贝已完成,并在注释中记录「逐流下发」的已知性能上限。另请把pin_memory()改为按设备可用性条件申请(或失败回退为普通 CPU tensor),并为 ROCm/CPU 目标补一条 grammar decode 覆盖用例。
- 建议:复用 MTP 侧已有的合并思路:在 sampler 层收集本 step 所有 grammar 流的 CPU bitmask 行,拼成
- 新增 6 个 GRAMMAR_ 错误码全部落入 INTERNAL 分类,与错误语义不匹配* @
rtp_llm/config/exceptions.py:45- 建议:按语义显式标注 category:608/609 →
INVALID_OUTPUT(与OUT_OF_VOCAB_RANGE对齐);611/612 视定位为配置问题保留INTERNAL但在注释中写明属部署错误、或改为BAD_REQUEST;610/613 保留INTERNAL。并补一条断言各码 category 的用例,避免后续新增码再次静默落入默认分支。
- 建议:按语义显式标注 category:608/609 →
- legacy structures/triggers 形态的 structural_tag 归一化后无法编译,且两条报错文案完全相同 @
rtp_llm/config/grammar_constraint.py:68- 建议:要么在
normalize_grammar_value中把 legacystructures/triggers显式转换为formatDSL,要么在入口即拒绝并给出「请改用 format 形式」的明确报错;同时把_structural_tag_format_node的两条文案拆开(缺type与缺format分别提示),并补一条 legacy 形状的用例固化预期。
- 建议:要么在
- has_bounded_region 只检查 max_tokens,漏检 max_chars 导致 reasoning 包裹守卫可被绕过 @
rtp_llm/config/grammar_constraint.py:79- 建议:把判定改为「
type in (any_text, any_tokens)且max_tokens或max_chars任一非空」,报错文案同步列出两个键,并把预算键名抽为模块级常量以便随 xgrammar 能力扩展单点维护;补两条用例(max_tokens/max_chars各一)覆盖该守卫。若确认当前 xgrammar 版本对max_chars尚不生效,请在注释中写明并说明何时需要放开。
- 建议:把判定改为「
- update() / update_and_pop() 的补偿清单硬编码且遗漏 response_format @
rtp_llm/config/generate_config.py:441- 建议:把补偿逻辑改为遍历
GRAMMAR_FIELD_NAMES,并对response_format走一次ResponseFormat.model_validate(None 时跳过),使字段清单只有一处来源;或直接为GenerateConfig开启model_config = ConfigDict(validate_assignment=True)让 setattr 自动走校验并删除整段手工补偿。补一条「update({"response_format": {合法 envelope}})后prepare_for_engine成功」的用例,与既有的三条拒绝非法 envelope 用例互补。
- 建议:把补偿逻辑改为遍历
- MultiSeqLogitsProcessor 写 EOS 掩码缺少 vocab 边界校验,存在堆越界写风险 @
rtp_llm/cpp/models/logits_processor/MultiSeqLogitsProcessor.cc:39- 建议:在 :35 循环前校验
eos_token_id_落在[0, vocab_size),越界时返回ErrorInfo(复用OUT_OF_VOCAB_RANGE或新增 EOS 越界语义)而非静默越界写;并补一条eos_token_id >= vocab_size的用例,与 grammar 路径形成一致覆盖。顺带建议把logit_mask_host_tensor.to(torch::kCUDA)(:43)改为.to(logits.device()),避免在非 CUDA 目标上硬编码设备。
- 建议:在 :35 循环前校验
- TokenizerInfo 以 VocabType::RAW 重建的等价性缺少测试,create 降级分支零覆盖 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:81- 建议:在
XGrammarBackendTest.cc补:(1)对 BYTE_FALLBACK 与 BYTE_LEVEL 两种 fixture vocab,分别用原始TokenizerInfo与create()重包装后的 backend 生成同一 regex/json grammar 的 bitmask,逐 word 断言相等;(2)覆盖 vocab 非 32 对齐时最后一个 word 的 padding 位;(3)create("")、非法 JSON 返回 nullptr 与vocabSize()的用例。
- 建议:在
- grammar smoke 门禁存在静默放行分支,两个 comparer 语义相反,且唯一 oracle 无自测 @
rtp_llm/test/smoke/openai_comparer.py:230- 建议:(1)两处静默
return改为 fail-loud,与DashGrpcComparer对齐(streaming 分支提示「grammar_constraint_only 暂不支持流式用例」),并兼容extra_configs中的 grammar 字段;(2)把「取约束字段 + 校验 + 包装 SmokeException」抽到grammar_constraint_validator中供两个 comparer 复用;(3)为校验器补一组纯 CPU py_test(regex / json_schema / triggered_tags / sequence 的正反例,含additionalProperties=false、minItems/maxItems、bare trigger 残留);(4)把「校验器不支持该关键字」与「输出违反约束」拆成两类异常,前者映射VALID_FAILED,避免工具能力缺口被误判为产品缺陷。
- 建议:(1)两处静默
- smoke 校验器把显式 required: [] 当作全部字段必填 @
rtp_llm/test/smoke/grammar_constraint_validator.py:147- 建议:区分「字段缺失」与「字段为空列表」:
required = schema["required"] if "required" in schema else list((schema.get("properties") or {}).keys())。并在上一条建议新增的校验器单测中补一个required: []用例固化该语义。
- 建议:区分「字段缺失」与「字段为空列表」:
- 声称验证 CPU fallback 的测试强绑 CUDA 依赖与 H20 执行属性,非 CUDA 平台无覆盖 @
rtp_llm/models_py/bindings/core/test/BUILD:53- 建议:二选一并让声明与注释一致:(a) 若确实只需 CPU,把 CUDA-only 依赖收进
select()或替换为平台无关的exec_ctx_ops,去掉exec_properties,使该目标进入非 CUDA 构建的测试门禁;(b) 若受exec_ops_test_lib链接结构限制无法脱离 CUDA 宿主,则改写注释为「仅走 CPU tensor 分发路径,但仍需 CUDA 链接宿主」,并在 BUILD 中记录非 CUDA 平台的覆盖缺口与后续计划。同时把纯转发目标flashinfer_runtime直接替换为:flashinfer或改用alias()表达意图。
- 建议:二选一并让声明与注释一致:(a) 若确实只需 CPU,把 CUDA-only 依赖收进
- grammar 包 BUILD 声明的 Tier 边界未由 visibility 落地,且过渡 umbrella target 无存量调用方 @
rtp_llm/cpp/engine_base/grammar/BUILD:58- 建议:把
grammar_xgrammar的visibility收敛为显式白名单(如["//rtp_llm/cpp/engine_base/grammar:__subpackages__", "//rtp_llm/cpp/models/logits_processor:__subpackages__"]),让注释中的 tier 约束由 Bazel 真正强制;同时让本 PR 新增的三个调用方直接依赖:grammar_xgrammar并删除grammar_cpp_lib,或在注释中明确它承载的是后续多 backend 抽象并给出对应扩展点。
- 建议:把
- xgrammar pin 指向上游未合并的 feature 分支 commit,且另开一套依赖声明入口 @
3rdparty/xgrammar/repositories.bzl:8- 建议:优先把 xgrammar 声明并入既有
git.bzl统一入口以保持依赖清单单点可审计;并在注释中补上所依赖的上游 PR/分支链接与合并状态、最低所需能力清单、以及「上游合并后切回 tag」的退出计划。条件允许时补shallow_since与内部只读镜像(或与 dlpack 同样归档 tarball + sha256),使离网构建与回滚可复现。
- 建议:优先把 xgrammar 声明并入既有
- vendored dlpack 头版本与 BUILD 注释不符,且未记录 upstream 出处与 license @
3rdparty/dlpack/BUILD:6- 建议:把注释更正为实际 vendored 版本(v1.2),写明 upstream 仓库、commit/tag 与获取时间并附 license 归属文件;同时确认该版本与 torch 内置 dlpack 的
DLTensor布局兼容,或在注释中说明本仓只使用DLTensor这一稳定子集,使第三方代码来源可审计、后续升级有据可查。
- 建议:把注释更正为实际 vendored 版本(v1.2),写明 upstream 仓库、commit/tag 与获取时间并附 license 归属文件;同时确认该版本与 torch 内置 dlpack 的
- 跨语言类型存根与实际绑定不一致,配置与原生扩展契约均不完整 @
rtp_llm/ops/libth_transformer_config.pyi:544- 建议:在
libth_transformer_config.pyi的GrammarConfig中补terminate_without_stop_token: bool与def __repr__(self) -> str: ...,与def_readwrite绑定集合完全对齐(若存根由工具生成,请在本 PR 重新生成并提交);同时把libth_grammar_tokenizer_info.pyi与ops/__init__.py的encoded_vocab类型改为Sequence[Union[str, bytes]],使声明、实现与测试三者一致。
- 建议:在
- GrammarConfig::setstate 四条布局靠类型嗅探分派,size==6 优先级会让未来新增字段自毁当前格式 @
rtp_llm/cpp/pybind/ConfigInit.cc:1366- 建议:若无跨版本 pickle 契约,收敛为「主干 legacy 5 元组(首元素为 str)」与「当前 5 元组」两条分支,删除两条中间态分支及对应伪造用例;若确需多布局,请在元组首位写入显式 schema version 并按 version 分派,消除「新增字段就撞旧分支」的顺序耦合,并在注释中写明该契约的来源与生命周期。
- 新增 grammar CLI 参数的默认值断言同源自证,env 分支与文档化边界零覆盖 @
rtp_llm/server/server_args/test/server_args_test.py:423- 建议:把默认值断言改为硬编码期望(
num_workers == 8、compiler_cache_bytes == 512 * 1024 * 1024、两个 bool 为 False),使其成为真正的默认值回归护栏;补一个 env-only 用例(清空sys.argv,仅设置三个环境变量);再补--grammar_compiler_cache_bytes 0与--grammar_num_workers 0两个边界用例,覆盖 help 文本承诺的语义。
- 建议:把默认值断言改为硬编码期望(
- tokenizer 家族探测函数在单测中被整体 mock,生产分派逻辑无真实覆盖 @
rtp_llm/config/test/grammar_tokenizer_info_test.py:73- 建议:保留分支参数化用例,但至少补一组不 mock 探测函数的用例:构造真实的
PreTrainedTokenizerFast(可用极小的内嵌 tokenizer.json)与一个具备sp_model的 sentencepiece 桩对象,断言build_grammar_tokenizer_info_json选中正确分支、产出的vocab_type/add_prefix_space符合预期,并让「三条判定都不命中 → Unsupported tokenizer type」用例也走真实谓词。
- 建议:保留分支参数化用例,但至少补一组不 mock 探测函数的用例:构造真实的
P3
- processor 与 stream 长度不一致这一内部不变量被归入 UNKNOWN_ERROR @
rtp_llm/cpp/engine_base/stream/GenerateStream.cc:964- 建议:改用
ErrorCode::GRAMMAR_VERIFY_EXCEPTION(或新增LOGITS_PROCESSOR_STATE_MISMATCH),使该不变量违规在错误码维度可独立观测与告警;消息中已带 processor_index 与两侧长度,建议一并上报为指标而非仅依赖日志文本匹配。
- 建议:改用
- normalize_think_tag 转义语义收窄未记录,旧 env 配置行为静默改变 @
rtp_llm/config/response_format.py:13- 建议:在 PR description 与
normalize_think_tagdocstring 中明确「仅支持\n」并列出受影响的 env 取值形态;若需兼容,可白名单式扩展\t/\r等常见转义,或在检测到其它反斜杠转义时打印一次 WARNING 提示改用真实字符,避免运维在无提示情况下踩坑。
- 建议:在 PR description 与
- GrammarConfig.repr 与 to_string() 重复维护同一份字段清单,且 bool 输出为 0/1 @
rtp_llm/cpp/pybind/ConfigInit.cc:1330- 建议:改为
.def("__repr__", &GrammarConfig::to_string),或让 lambda 直接调用c.to_string()再包一层类名前缀,使字段清单只在ConfigModules.cc维护一份;同时在to_string()的oss上加std::boolalpha以贴合 Python 惯例。若确需与其他 config 类不同的调试输出,建议统一为所有 config 类补__repr__,而非只为 GrammarConfig 开特例。
- 建议:改为
- 文档未同步 GrammarLogitsProcessor、grammar 服务级参数与 processor × MTP 兼容矩阵 @
docs/backend/logits_processor.md:8- 建议:在该文档补:
GrammarLogitsProcessor的能力与约束(单序列、与 beam/多返回序列互斥、think 模式包裹方式)、一张 processor × MTP 兼容矩阵(含默认 UNSUPPORTED 的语义与 reason 文案)、新增/删除的 grammar 服务级参数清单,并同步修正ThinkModeLogitsProcessor的现状描述。
- 建议:在该文档补:
Checklist Violations (19 fail / 58 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
grammar 包 BUILD 声明的 Tier 边界未由 visibility 落地,且过渡 umbrella target 无存量调用方
grammar_xgrammar上方注释写明「only Tier 0 (this package) and Tier 1 (logits_processor) may depend」(:35),但文件头已是package(default_visibility = ["//visibility:public"])(:4),grammar_cpp_lib还额外显式声明visibility = ["//visibility:public"](:64),Bazel 层面任何 target 都可依赖,分层约束仅存在于注释。同时grammar_cpp_lib注释称「Transitional umbrella; deleted once callers move onto :grammar_xgrammar」(:58),但其 deps 只有:grammar_xgrammar,且全部三个调用方均为本 PR 新增(rtp_llm/cpp/models/BUILD:78、engine_base/grammar/test/BUILD:16、`logits_pro - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
normalize_think_tag 转义语义收窄未记录,旧 env 配置行为静默改变
新实现normalize_think_tag只做value.replace(r"\n", "\n")(:13-15)。diff 显示原逻辑在四个调用点均为value.encode("utf-8").decode("unicode_escape")(think 参数装配、dash_sc tag 归一化、openai 侧 end-think token 计算),即支持\t、\xNN、\uXXXX、\\等全部 Python 转义;新用例self.assertEqual(r"<think>\t", normalize_think_tag(r"<think>\t"))(openai_response_test.py:2569)明确固化了「\t不再解转义」。因此THINK_END_TAG/THINK_START_TAG中含非\n转义的存量部署,其end_think_token_ids会静默改变(generate_config.py:545-550 用该 tag 做tokenizer.encode),表现为 thi - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
grammar 包 BUILD 声明的 Tier 边界未由 visibility 落地,且过渡 umbrella target 无存量调用方
grammar_xgrammar上方注释写明「only Tier 0 (this package) and Tier 1 (logits_processor) may depend」(:35),但文件头已是package(default_visibility = ["//visibility:public"])(:4),grammar_cpp_lib还额外显式声明visibility = ["//visibility:public"](:64),Bazel 层面任何 target 都可依赖,分层约束仅存在于注释。同时grammar_cpp_lib注释称「Transitional umbrella; deleted once callers move onto :grammar_xgrammar」(:58),但其 deps 只有:grammar_xgrammar,且全部三个调用方均为本 PR 新增(rtp_llm/cpp/models/BUILD:78、engine_base/grammar/test/BUILD:16、`logits_pro - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
processor 与 stream 长度不一致这一内部不变量被归入 UNKNOWN_ERROR
validateLogitsProcessorState()在processor->committedOutputLen()与outputTokenLen()不一致时返回ErrorInfo(ErrorCode::UNKNOWN_ERROR, "logits processor committed output length mismatch: ...")(:964-967)。该分支是本 PR 新引入的、明确的内部状态机不变量违规(processor 与 stream 权威 token 历史失配,属框架 bug),却复用了语义上「原因不明」的 514;NormalOutputDispatcher.cc:38的 sampler 失败也用同一码,线上无法按错误码区分「采样失败」与「processor 状态失配」。而本 PR 已新增GRAMMAR_VERIFY_EXCEPTION(613) 这一更贴切的码。 - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
xgrammar pin 指向上游未合并的 feature 分支 commit,且另开一套依赖声明入口
xgrammar_deps()用new_git_repository直接钉commit = "60fc70ee...",注释标明来自feat/anytext-budget: structural-tag max_tokens/max_chars enforcement(:4)——即整套 structural-tag 预算能力依赖一个未合并进上游的特性分支提交,该分支被 force-push 或删除即构建不可复现,且无sha256/镜像/归档 tarball 兜底(同目录 dlpack 恰恰因为内网不可达才被 vendored)。同时WORKSPACE已有统一入口@rtp_deps//:git.bzl的git_deps()(:27-29),本 PR 却在 :31-33 另开一条xgrammar_deps()加载路径,依赖声明出现两套机制、不再有单一可审计来源;注释也未记录「若该 commit 被改写如何回滚」。 - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
GrammarConfig::__setstate__ 四条布局靠类型嗅探分派,size==6 优先级会让未来新增字段自毁当前格式
__setstate__现需区分 4 种布局,并用运行时类型嗅探(py::isinstance<py::str>(t[0])、py::isinstance<py::int_>(t[3]))而非本文件既有的按 size 判定(同文件 FIFOSchedulerConfig 用t.size()==4)。关键缺陷:if (t.size() == 6)(:1366)排在类型嗅探之前,而当前__getstate__(:1341-1345)输出 5 元组——一旦将来新增第 6 个字段,新产出的 6 元组会命中「Previous layout」分支,对实为compiler_cache_bytes的t[3]执行cast<std::vector<int32_t>>()并抛「GrammarConfig unpickle error」,即新格式自身无法反序列化。其中 :1366 与 :1379 两条布局在主干从未发布,仅存在于本特性分支中间提交;而该 pickle 实际只用于同构建内 multiprocessing spawn,无跨版本契约,`confi - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
processor 与 stream 长度不一致这一内部不变量被归入 UNKNOWN_ERROR
validateLogitsProcessorState()在processor->committedOutputLen()与outputTokenLen()不一致时返回ErrorInfo(ErrorCode::UNKNOWN_ERROR, "logits processor committed output length mismatch: ...")(:964-967)。该分支是本 PR 新引入的、明确的内部状态机不变量违规(processor 与 stream 权威 token 历史失配,属框架 bug),却复用了语义上「原因不明」的 514;NormalOutputDispatcher.cc:38的 sampler 失败也用同一码,线上无法按错误码区分「采样失败」与「processor 状态失配」。而本 PR 已新增GRAMMAR_VERIFY_EXCEPTION(613) 这一更贴切的码。 - [6.1] Quality — PR description 说明动机与设计 → issue
文档未同步 GrammarLogitsProcessor、grammar 服务级参数与 processor × MTP 兼容矩阵
本 PR 只在该文档补了一句「Tree Decode cannot be enabled together with MTP」(:68),但:built-in processor 列表仍把ThinkModeLogitsProcessor列为在用(:8,其fromGenerateInput已无生产调用方),完全没有GrammarLogitsProcessor的条目;新增的--grammar_terminate_without_stop_token/--grammar_num_workers/--grammar_compiler_cache_bytes与被删除的--grammar_backend均未记录;prepareStreams新引入的「未声明mtpCapability的 processor 一律被 MTP 拒绝」这一准入规则也无任何描述。 - [6.1] Software Engineering — DIP:高层策略不依赖非必要具体细节 → issue
进程级 grammar backend 存放于 models 层函数内可变静态量,分层泄漏且 init/read 无同步
grammarBackend()返回函数内static std::shared_ptr<XGrammarBackend> backend;(:72),由init()(:79)赋值、由createLogitsProcessors()(:106)在每个GenerateStream构造时读取,注释自陈「Process-wide by design... Do not use this factory for multiple engines with different tokenizers in one process」。问题:(1) 由 tokenizer 决定的引擎级状态被放进rtp_llm/cpp/models计算层静态工厂,models/BUILD:78,82因此新增对engine_base/grammar与config:config_modules的依赖,层次边界被拉平;(2) 函数内 static 的线程安全只覆盖首次初始化、不覆盖后续赋值,而init有两个调用点(`NormalExecutor.cc - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
GrammarConfig.__repr__ 与 to_string() 重复维护同一份字段清单,且 bool 输出为 0/1
ConfigInit.cc:1330-1338的__repr__lambda 手工拼接constrained_json_disable_any_whitespace/terminate_without_stop_token/num_workers/compiler_cache_bytes四个字段,与ConfigModules.cc的GrammarConfig::to_string()(同四字段 +tokenizer_info_json_size)内容重复仅格式不同,而to_string已通过.def("to_string", ...)(:1329)暴露,后续新增字段需改两处才不漂移。这也是整个ConfigInit.cc中唯一一个__repr__,与其余数十个 config 类的绑定风格不一致。另外两处oss << bool未加std::boolalpha,Python 侧实际看到constrained_json_disable_any_whitespace=1,易被误读为整 - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
GrammarConfig::__setstate__ 四条布局靠类型嗅探分派,size==6 优先级会让未来新增字段自毁当前格式
__setstate__现需区分 4 种布局,并用运行时类型嗅探(py::isinstance<py::str>(t[0])、py::isinstance<py::int_>(t[3]))而非本文件既有的按 size 判定(同文件 FIFOSchedulerConfig 用t.size()==4)。关键缺陷:if (t.size() == 6)(:1366)排在类型嗅探之前,而当前__getstate__(:1341-1345)输出 5 元组——一旦将来新增第 6 个字段,新产出的 6 元组会命中「Previous layout」分支,对实为compiler_cache_bytes的t[3]执行cast<std::vector<int32_t>>()并抛「GrammarConfig unpickle error」,即新格式自身无法反序列化。其中 :1366 与 :1379 两条布局在主干从未发布,仅存在于本特性分支中间提交;而该 pickle 实际只用于同构建内 multiprocessing spawn,无跨版本契约,`confi - [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue
prepareSpeculative 缺少 matcher_ 空指针校验,与同类方法的不变量相反
同一个类里process()(:308)与updateStatus()(:344)都以if (!matcher_) return std::nullopt;开头,acceptCommittedLocked(:387)再判一次,说明作者视matcher_ == nullptr为合法状态。但prepareSpeculative直接prepareSpecMask(*matcher_, ...)解引用(:377),构造函数(:299-302)未拒绝 nullptr,mtpCapability()(GrammarLogitsProcessor.h:29)又无条件返回SPEC_VERIFY,SpecLogitsVerifyRunner只校验item.processor != nullptr而无法感知内部 matcher 为空。生产路径当前不可达(工厂只从非空createMatcherFromKey()结果构造),但同一对象在两条路径上契约相反:普通解码静默跳过、MTP 热路径解引用空指针,后续新增构造点或测试 fixt - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
声称验证 CPU fallback 的测试强绑 CUDA 依赖与 H20 执行属性,非 CUDA 平台无覆盖
packed_mask_logits_cpu_fallback_test的注释是「Exercise the CPU fallback without initializing or allocating a GPU.」(:53),但目标本身声明exec_properties = {"gpu": "H20"}(:64),并依赖 CUDA-only 的//rtp_llm/models_py/bindings/cuda/ops:flashinfer_runtime(:60)与:exec_ops_test_lib(均未包在select({"//:using_cuda": ...})中)。结果是注释与声明自相矛盾:runtimeApplyPackedMaskLogits的 CPU 分支只在 CUDA 机器上被验证,ROCm/ARM/纯 CPU 构建既不构建也不运行该目标——而这恰是 grammar decode 在非 CUDA 编译下唯一的实际执行路径(GrammarLogitsProcessor.cc:259-270 被#if USING_CUDA - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
tokenizer 家族探测函数在单测中被整体 mock,生产分派逻辑无真实覆盖
测试 helper 在调用build_grammar_tokenizer_info_json(:94)之前,用mock.patch.object同时替换掉_is_fast_tokenizer、_is_tiktoken_tokenizer、_is_sentencepiece_tokenizer、_is_byte_level_tokenizer四个探测函数(:73-92),即生产代码中决定走哪条元数据分支、以及vocab_type取 BYTE_LEVEL/BYTE_FALLBACK/RAW 的全部判定逻辑都被短路,rtp_llm.ops也被 fake 模块替换。虽然另有独立用例直接测这些谓词(:101-122),但「探测 → 分派 → 元数据形状」这条串联链路没有任何端到端断言——而它既是「不支持的 tokenizer 阻塞启动」这条 P1 的判定点,也是XGrammarBackend.cc:81RAW 重包装是否安全的输入来源。 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
新增 grammar CLI 参数的默认值断言同源自证,env 分支与文档化边界零覆盖
test_grammar_defaults用expected = type(g)()(:428)取期望值,而grammar_group_args.py的default=同样读取新建GrammarConfig()的字段,两侧同源,因此该断言无法发现默认值本身被改动(如num_workers8→1、compiler_cache_bytes512MiB→0,见 ConfigModules.h:378/381,均会影响全部部署却仍然通过),与类 docstring 声称的「Regression guard for the wiring」不符。此外两个新用例只走sys.argv(:443-453),未覆盖env_name路径(GRAMMAR_TERMINATE_WITHOUT_STOP_TOKEN、GRAMMAR_COMPILER_CACHE_BYTES、CONSTRAINED_JSON_DISABLE_ANY_WHITESPACE)——而 env 是本仓生产配置的主要方式;help 明确承诺的边界语义(`compile
RTP-LLM Checklist
- [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue
文档未同步 GrammarLogitsProcessor、grammar 服务级参数与 processor × MTP 兼容矩阵
本 PR 只在该文档补了一句「Tree Decode cannot be enabled together with MTP」(:68),但:built-in processor 列表仍把ThinkModeLogitsProcessor列为在用(:8,其fromGenerateInput已无生产调用方),完全没有GrammarLogitsProcessor的条目;新增的--grammar_terminate_without_stop_token/--grammar_num_workers/--grammar_compiler_cache_bytes与被删除的--grammar_backend均未记录;prepareStreams新引入的「未声明mtpCapability的 processor 一律被 MTP 拒绝」这一准入规则也无任何描述。 - [I] 代码质量 — 同一功能用统一工具函数 → issue
GrammarConfig.__repr__ 与 to_string() 重复维护同一份字段清单,且 bool 输出为 0/1
ConfigInit.cc:1330-1338的__repr__lambda 手工拼接constrained_json_disable_any_whitespace/terminate_without_stop_token/num_workers/compiler_cache_bytes四个字段,与ConfigModules.cc的GrammarConfig::to_string()(同四字段 +tokenizer_info_json_size)内容重复仅格式不同,而to_string已通过.def("to_string", ...)(:1329)暴露,后续新增字段需改两处才不漂移。这也是整个ConfigInit.cc中唯一一个__repr__,与其余数十个 config 类的绑定风格不一致。另外两处oss << bool未加std::boolalpha,Python 侧实际看到constrained_json_disable_any_whitespace=1,易被误读为整
Python Static-First Checklist
- [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
tokenizer 家族探测函数在单测中被整体 mock,生产分派逻辑无真实覆盖
测试 helper 在调用build_grammar_tokenizer_info_json(:94)之前,用mock.patch.object同时替换掉_is_fast_tokenizer、_is_tiktoken_tokenizer、_is_sentencepiece_tokenizer、_is_byte_level_tokenizer四个探测函数(:73-92),即生产代码中决定走哪条元数据分支、以及vocab_type取 BYTE_LEVEL/BYTE_FALLBACK/RAW 的全部判定逻辑都被短路,rtp_llm.ops也被 fake 模块替换。虽然另有独立用例直接测这些谓词(:101-122),但「探测 → 分派 → 元数据形状」这条串联链路没有任何端到端断言——而它既是「不支持的 tokenizer 阻塞启动」这条 P1 的判定点,也是XGrammarBackend.cc:81RAW 重包装是否安全的输入来源。 - [P.H] 类型标注 — 输入参数用 Sequence/Mapping/Iterable,返回值用具体类型 → issue
跨语言类型存根与实际绑定不一致,配置与原生扩展契约均不完整
ConfigInit.cc:1325新增.def_readwrite("terminate_without_stop_token", &GrammarConfig::terminate_without_stop_token),grammar_group_args.py:17,19以bind_to与default=grammar_config.terminate_without_stop_token直接读写该属性;但libth_transformer_config.pyi:544-556的class GrammarConfig只声明compiler_cache_bytes/constrained_json_disable_any_whitespace/num_workers/tokenizer_info_json,既缺该字段也缺同批新增的__repr__,静态类型检查会在 CLI 绑定处报 attr-defined。同类问题还有原生扩展:PyGrammarTokenizerInfo.cc:33-44
Strengths
committedOutputLen()+GenerateStream::validateLogitsProcessorState()(GenerateStream.cc:953-971)把「processor 状态机必须与 stream 权威 token 历史一致」从调用顺序约定变成每步可检测的显式不变量,配合StreamUpdateInfo::error_info先判错再提交,失败批次不留半提交前缀。- MTP 采用显式补偿事务:
prepareSpecMask用provisional_accepts精确rollback(GrammarLogitsProcessor.cc:131),rollback 失败时串联原始错误并markFinished()fail-closed;acceptCommittedLocked的rollback_commit(:397-415)记录old_matcher_len/old_output_len,多 token 提交任一失败即整批回滚。 - 刻意保持 TERMINATED 而非 FINISHED 并注释说明
min_new_tokens/ignore_eos下必须继续只放行 EOS(:424-431),属经过推敲的状态机设计;失败方向统一 fail-closed(仅放行 EOS)而非放开约束。 clearBitmaskTokenRange(BitmaskUtils.cc:63-97)对同字/跨字、begin_bit==0、end_bit==0(末尾 32 对齐)、越界钳制全部正确区分;bitmaskAllowsToken与forceTokenInBitmask对越界分别采取「拒绝」与「显式 abort」两种明示策略。- 错误回传链完整闭合:
collectStreamSamplerError(NormalOutputDispatcher.cc:26-32)按 sampler 输入坐标取错并注释说明「beam 改变 batch 时输出坐标会漂移」,避免错位归因。 RtpGrammarMatcher用统一matcherCall模板把第三方异常收敛为ErrorInfo,禁用拷贝与移动并用static_assert锁住,杜绝 xgrammar 异常穿越到调度层。SpecLogitsVerifyRunner只为真正含 grammar processor 的流分配紧凑行(buildActiveStreamLayout+logits_row_indices),task.active.empty()直接短路,普通 MTP 请求不引入额外 D2H 与 kernel launch。- 外部契约收敛质量高:
GrammarConstraint用 frozen dataclass 承载「四字段互斥」且GRAMMAR_FIELD_NAMES由get_args(Literal)派生(grammar_constraint.py:15-18);ResponseFormatJSONSchema用model_serializer保住 OpenAI 线上字段名schema;validate_engine_ready在 RPC 边界只做只读断言(response_format_compiler.py:181)。 GrammarConfig::to_string()只打印tokenizer_info_json_size而非整词表,acceptToken拒绝日志固定 DEBUG,未污染 per-token 热路径。- 测试扎实且未用 mock 替代生产边界:
GrammarLogitsProcessorTest.cc全部走真实XGrammarBackend::create(:45)并断言到错误码级,用「两次相同prepareSpeculative得到相同 cap」钉住回滚幂等性;grammar_tokenizer_info_test.py:164从 runfiles 直接加载.so验证 raw bytes 词表往返;ServerArgsGrammarConfigTest.setUp(server_args_test.py:400-415)在改动全局状态前先addCleanup,规避 setUp 中途失败污染整个 suite。
| encoded_vocab, json.dumps(metadata, separators=(",", ":")) | ||
| ) | ||
|
|
||
| raise ValueError(f"Unsupported tokenizer type: {type(tokenizer)}") |
There was a problem hiding this comment.
[P1] grammar tokenizer 元信息在启动期无条件构建,不支持的 tokenizer 直接阻塞模型加载且无逃生开关
build_grammar_tokenizer_info_json 对未命中 fast/tiktoken/sentencepiece 三条判定的 tokenizer 一律 raise ValueError("Unsupported tokenizer type: ...")(:195),stop_token_ids 为空也 raise(:158-159)。base_model.py:319-328 捕获后升级为 RuntimeError 重抛,而 rtp_llm_op.py:30-32 在 start() 中无条件调用它填充 grammar_config.tokenizer_info_json,调用处无 try 包裹。GrammarConfig(ConfigModules.h:374-383)只有 5 个字段,grammar_group_args.py 也无 enable/disable 开关。结果:任何自定义/慢速 HF tokenizer、或缺少单 token stop word 的模型,即使从不使用结构化输出,引擎也起不来,运...
建议: 新增服务级开关(如 --grammar_enable/GRAMMAR_ENABLE,默认 true),关闭时跳过构建并让 tokenizer_info_json 保持为空,复用 XGrammarBackend::create 已有的「空串即禁用」路径;或把「tokenizer 类型不支持」降级为 WARNING + 返回空串,让失败在首个 grammar 请求上以 LogitsProcessorFactory.cc:108 已有的明确文案暴露,同时保留 stop_token_ids 为空等真正致命配置的 fail-fast。请与下一条发现一并决策出单一失败策略,补一条覆盖 rtp_llm_op.start() 启动路径的用例(现有 test_build_tokenizer_info_rejects_unsupported_tokenizer 只覆盖函数级),并在发布说明中列出受影响的 tokenizer 类型与 stop_token_ids 要求。
| Options opts = optionsFromConfig(cfg); | ||
| auto result = xgrammar::TokenizerInfo::DeserializeJSON(tokenizer_info_json); | ||
| if (std::holds_alternative<xgrammar::SerializationError>(result)) { | ||
| RTP_LLM_LOG_ERROR("XGrammarBackend::create: tokenizer info deserialize failed (%s); disabling grammar", |
There was a problem hiding this comment.
[P1] 非空 tokenizer 元信息反序列化/构造失败被静默降级,与 Python 侧声明的启动期 fail-fast 契约相反
base_model.py:310-314 明确写下契约:「Grammar tokenizer metadata is a startup-time compatibility contract... Fail fast below: accepting ordinary requests with empty metadata would defer an unsupported tokenizer ... until the first grammar request」,并在异常时 raise RuntimeError。但 C++ 侧对非空 tokenizer_info_json 的三条失败路径——DeserializeJSON 失败(:72-76)、GetVocabSize() <= 0(:86-89)、构造抛异常(:96-99)——全部只打 ERROR 日志并 return nullptr;LogitsProcessorFactory::init(:79)把 nullptr 直接存入进程级 static,引擎照常完成启动并对外健康,此后...
建议: 区分「未配置」与「配置失败」:tokenizer_info_json.empty() 时保持返回 nullptr(对应上一条的显式开关);非空但反序列化/构造失败时向上返回错误让引擎启动失败(与 Python 一致),或至少上报一个明确的「grammar backend disabled」启动指标/健康位,并把当前 ERROR 日志升级为带原因分类的启动结论日志。同时补一条「非空但非法 JSON」的用例。
There was a problem hiding this comment.
防御行为,已添加防御throw
| ) -> "ResponseFormatPlan": | ||
| final_constraint = _resolve_final_constraint(config) | ||
|
|
||
| if config.in_think_mode: |
There was a problem hiding this comment.
[P1] think 模式改为硬依赖 grammar 约束,thinking + beam search / num_return_sequences>1 由可用变报错
ResponseFormatPlan.compile 中只要 config.in_think_mode 为真(:71),就先在 :72-78 拒绝 has_num_beams() or num_return_sequences > 1(文案自陈「because it uses grammar-constrained decoding」),再无条件构造 reasoning structural_tag 包裹(无用户约束时 final_format = {"type":"any_text"},:84-96);对照 :98-105 的非 think 分支,同样的限制仅在 final_constraint is not None 时才施加。而 in_think_mode 可由服务级 THINK_MODE 全局打开(generate_config.py:537-541:enable_thinking is None 时取 generate_env_config.think_mode),OpenAI 的 n 又直接映射为 `num_return_s...
建议: 把「think 模式是否使用 grammar 包裹」与「是否存在用户 grammar 约束」解耦:仅当 final_constraint is not None 时才构造 reasoning 包裹并施加 beam/多序列限制,无用户约束的纯 thinking 请求走原有非 grammar 路径。若确实要统一到 grammar 实现,请在 PR description 标注为 breaking change,在 docs/backend/sampling_params.md 与 docs/backend/logits_processor.md 写清该限制与替代方案,并补一条 thinking + beam search / n>1 的回归用例(当前 8 条 grammar smoke 用例均未覆盖该组合)。
| return tensor.scalar_type() == torch::kInt32 ? tensor.contiguous() : tensor.to(torch::kInt32).contiguous(); | ||
| } | ||
|
|
||
| std::optional<ErrorInfo> validateMtpCompatibility(const std::vector<BaseLogitsProcessorPtr>& processors) { |
There was a problem hiding this comment.
[P1] MTP 准入硬拒绝所有未声明能力的 logits processor,tree_decode_config 变为启动期抛异常
validateMtpCompatibility()(:45-55)遍历 stream->getAllLogitsProcessorPtr(),任一 mtpCapability().mode == UNSUPPORTED 即返回 INVALID_PARAMS;prepareStreams(:849-852)随即 stream->reportError(...) 并 continue,该流不再进入 prefill/decode。全仓仅 GrammarLogitsProcessor.h:29 重写了 mtpCapability(),Recommendation/MultiSeq/Tree/ThinkMode 均继承 BaseLogitsProcessor.h:44 的默认 UNSUPPORTED(reason 仅为泛化文案);而 RecommendationLogitsProcessor::fromGenerateInput 只要 combo_token_size > 0 就返回非空(:34-36),`MultiSeqLogitsPro...
建议: 补齐灰度与回滚:为 RecommendationLogitsProcessor/MultiSeqLogitsProcessor 显式实现 mtpCapability(),reason 中写明触发配置(combo_token_size>0、num_return_sequences>1)与可操作指引;并增加默认放行/拒绝可配的开关(如 sp_config.mtp_reject_incompatible_logits_processor),或先以 WARNING + 指标观察一个版本再切换为拒绝。tree_decode_config 建议改为启动期 WARNING + 自动禁用 tree decode 而非让引擎起不来。同时在 docs/backend/logits_processor.md 补一张「processor × MTP」兼容矩阵,在 PR description 标注行为不兼容变更,并补一条 Recommendation + MTP 的回归用例(MtpExecutorTest 目前只用 FakeNormalDecodeOnlyProcessor 覆盖)。
There was a problem hiding this comment.
原先也不应该同时开启 现在只是显示拒绝
| fake_ops.serialize_grammar_tokenizer_info = serializer | ||
| with ( | ||
| mock.patch.dict(sys.modules, {"rtp_llm.ops": fake_ops}), | ||
| mock.patch.object( |
There was a problem hiding this comment.
[P2] tokenizer 家族探测函数在单测中被整体 mock,生产分派逻辑无真实覆盖
测试 helper 在调用 build_grammar_tokenizer_info_json(:94)之前,用 mock.patch.object 同时替换掉 _is_fast_tokenizer、_is_tiktoken_tokenizer、_is_sentencepiece_tokenizer、_is_byte_level_tokenizer 四个探测函数(:73-92),即生产代码中决定走哪条元数据分支、以及 vocab_type 取 BYTE_LEVEL/BYTE_FALLBACK/RAW 的全部判定逻辑都被短路,rtp_llm.ops 也被 fake 模块替换。虽然另有独立用例直接测这些谓词(:101-122),但「探测 → 分派 → 元数据形状」这条串联链路没有任何端到端断言——而它既是「不支持的 tokenizer 阻塞启动」这条 P1 的判定点,也是 XGrammarBackend.cc:81 RAW 重包装是否安全的输入来源。
建议: 保留分支参数化用例,但至少补一组不 mock 探测函数的用例:构造真实的 PreTrainedTokenizerFast(可用极小的内嵌 tokenizer.json)与一个具备 sp_model 的 sentencepiece 桩对象,断言 build_grammar_tokenizer_info_json 选中正确分支、产出的 vocab_type/add_prefix_space 符合预期,并让「三条判定都不命中 → Unsupported tokenizer type」用例也走真实谓词。
Checklist: [6.1] 新逻辑有聚焦单测 + 相关集成/smoke 测试;[P.G] mock/fake/stub 不得替代本次声称覆盖的生产边界
| .def_readwrite("override_stop_tokens", &GrammarConfig::override_stop_tokens) | ||
| .def_readwrite("compiler_cache_bytes", &GrammarConfig::compiler_cache_bytes) | ||
| .def("to_string", &GrammarConfig::to_string) | ||
| .def("__repr__", |
There was a problem hiding this comment.
[P3] GrammarConfig.repr 与 to_string() 重复维护同一份字段清单,且 bool 输出为 0/1
ConfigInit.cc:1330-1338 的 __repr__ lambda 手工拼接 constrained_json_disable_any_whitespace / terminate_without_stop_token / num_workers / compiler_cache_bytes 四个字段,与 ConfigModules.cc 的 GrammarConfig::to_string()(同四字段 + tokenizer_info_json_size)内容重复仅格式不同,而 to_string 已通过 .def("to_string", ...)(:1329)暴露,后续新增字段需改两处才不漂移。这也是整个 ConfigInit.cc 中唯一一个 __repr__,与其余数十个 config 类的绑定风格不一致。另外两处 oss << bool 未加 std::boolalpha,Python 侧实际看到 constrained_json_disable_any_whitespace=1,易被误...
建议: 改为 .def("__repr__", &GrammarConfig::to_string),或让 lambda 直接调用 c.to_string() 再包一层类名前缀,使字段清单只在 ConfigModules.cc 维护一份;同时在 to_string() 的 oss 上加 std::boolalpha 以贴合 Python 惯例。若确需与其他 config 类不同的调试输出,建议统一为所有 config 类补 __repr__,而非只为 GrammarConfig 开特例。
Checklist: [6.1] DRY:重复非平凡逻辑被抽取或显式复用;[I] 同一功能用统一工具函数
| @@ -65,6 +65,8 @@ Tree Decode is enabled when the model loads a valid config file (global effect; | |||
|
|
|||
There was a problem hiding this comment.
📍 实际位置 docs/backend/logits_processor.md:8(不在 diff 展示范围内,就近挂载)
[P3] 文档未同步 GrammarLogitsProcessor、grammar 服务级参数与 processor × MTP 兼容矩阵
本 PR 只在该文档补了一句「Tree Decode cannot be enabled together with MTP」(:68),但:built-in processor 列表仍把 ThinkModeLogitsProcessor 列为在用(:8,其 fromGenerateInput 已无生产调用方),完全没有 GrammarLogitsProcessor 的条目;新增的 --grammar_terminate_without_stop_token / --grammar_num_workers / --grammar_compiler_cache_bytes 与被删除的 --grammar_backend 均未记录;prepareStreams 新引入的「未声明 mtpCapability 的 processor 一律被 MTP 拒绝」这一准入规则也无任何描述。
建议: 在该文档补:GrammarLogitsProcessor 的能力与约束(单序列、与 beam/多返回序列互斥、think 模式包裹方式)、一张 processor × MTP 兼容矩阵(含默认 UNSUPPORTED 的语义与 reason 文案)、新增/删除的 grammar 服务级参数清单,并同步修正 ThinkModeLogitsProcessor 的现状描述。
Checklist: [6.1] PR description 说明动机与设计;[I] 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1006
Status: BLOCKING
Summary: P0/0 · P1/7 · P2/22 · P3/7
Reviewed: commit 5927b376ffeb · 2026-08-06 15:23 UTC+8
Blocking Issues
P1
- 删除 --grammar_backend / GRAMMAR_BACKEND 破坏存量启动,并移除唯一的 grammar 关停开关 @
rtp_llm/server/server_args/grammar_group_args.py:5- 建议:二选一保留回滚能力:保留
--grammar_backend并标注 deprecated(取none时把tokenizer_info_json置空以跳过后端构建并打一次 WARNING),或新增等价的--grammar_enable=false/GRAMMAR_DISABLE。同时在发布说明 / breaking-changes 文档中列出--grammar_backend、GRAMMAR_BACKEND、override_stop_tokens的移除与替代路径,并补一条「传入已删除 flag」的断言用例,避免启动契约再被静默破坏。
- 建议:二选一保留回滚能力:保留
- grammar tokenizer 元信息在启动期无条件构建,不支持的 tokenizer 直接阻塞模型加载且无逃生口 @
rtp_llm/config/grammar_tokenizer_info.py:195- 建议:保留 fail-fast 的同时提供服务级逃生口(可与上一条的关停开关合并实现):关闭时跳过
build_grammar_tokenizer_info()并让XGrammarBackend::create返回 nullptr,结构化输出请求沿现有 "constraint backend is disabled" 路径返回明确 4xx。这样 tokenizer 兼容性问题只降级为「结构化输出不可用」,不会放大为「服务不可启动」。另建议在启动错误信息中点明是 grammar 元数据构建失败及所需 tokenizer 类型,便于现场定位。
- 建议:保留 fail-fast 的同时提供服务级逃生口(可与上一条的关停开关合并实现):关闭时跳过
- think 模式改为硬依赖 grammar 约束,thinking + beam search / num_return_sequences>1 由可用变报错 @
rtp_llm/config/response_format_compiler.py:71- 建议:确认该组合此前是否为支持形态;若是,请在 PR 描述与发布说明中显式声明这是有意的兼容性变更并给出迁移路径,或对
final_constraint is None的 think 请求保留非 grammar 路径(不构造信封),避免功能回退与额外热路径开销。无论哪种取舍,都补一条「think + num_return_sequences>1」的用例把最终语义钉住。
- 建议:确认该组合此前是否为支持形态;若是,请在 PR 描述与发布说明中显式声明这是有意的兼容性变更并给出迁移路径,或对
- prepare_for_engine 非幂等,raw 批量请求共享同一 GenerateConfig,think 模式下从第 2 个 prompt 起整批失败 @
rtp_llm/pipeline/pipeline.py:117- 建议:让
prepare_for_engine幂等:在ResponseFormatPlan.compile中识别「约束已是引擎侧信封」并直接复用,或在 config 上记录一次性编译标记;同时让批量路径按 prompt 复制GenerateConfig(_get_adapter已有copy.copy先例)。测试侧补一条「同一 config 连续prepare_for_engine两次,structural_tag结构不变且不抛异常」的用例固化该不变量;若确认只允许调用一次,则validate_engine_ready需能识别嵌套信封而非仅比对 normalized。
- 建议:让
- in_think_mode / max_thinking_tokens 在引擎侧变为静默 no-op,ThinkModeLogitsProcessor 生产不可达 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:100- 建议:在
createLogitsProcessors补显式准入:config.in_think_mode && config.max_thinking_tokens > 0 && grammar_key.empty()时返回ErrorInfo(ErrorCode::INVALID_PARAMS, ...)(推荐 fail-fast,提示调用方必须下发已归一化的structural_tag),至少也应RTP_LLM_INTERVAL_LOG(WARN)。同时在 proto/GenerateConfig注释中把max_thinking_tokens标注为「仅 Python 归一化前有效」,并补一条「in_think_mode 无 grammar」的用例。
- 建议:在
- json_format 删除后 raw 入口静默丢弃,三条协议语义分叉,response_format 字符串形态同时被拒 @
rtp_llm/structure/request_extractor.py:50- 建议:统一三条入口语义:raw 路径对
json_format等已废弃 grammar 键显式抛ERROR_INPUT_FORMAT_ERROR(复用 OpenAI 侧同一文案)或映射为response_format={"type":"json_object"};为response_format增加mode="before"校验器接受 JSON 字符串(复用load_json_field)以保持旧客户端可用。建议把「取约束 + 缺失即报错」抽成三条入口共用的工具函数,并在 PR 描述 / 发布说明中给出这两个字段在三条协议上的迁移矩阵。
- 建议:统一三条入口语义:raw 路径对
- MTP 新增启动期 abort 与请求期硬拒绝,把既有可运行组合打死且无过渡开关 @
rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:230- 建议:启动侧改为明确的 config 校验错误(或自动关闭 tree decode 并 WARN),避免升级即无法拉起;准入侧对
Recommendation/MultiSeq给出更精确的 reason,并明确其在 MTP 下的定位(实现SPEC_VERIFY,或在文档与发布说明中显式声明不再支持)。在 PR 描述与docs/backend/logits_processor.md中列出全部新增拒绝条件与错误码,便于运维排障与回滚决策。
- 建议:启动侧改为明确的 config 校验错误(或自动关闭 tree decode 并 WARN),避免升级即无法拉起;准入侧对
Non-blocking Suggestions
P2
- proto 删除 response_format 字段 73 未加 reserved,违反本文件既有约定并留下字段号复用风险 @
rtp_llm/cpp/model_rpc/proto/model_rpc_service.proto:138- 建议:在
structural_tag = 72;之后补reserved 73;与reserved "response_format";,与本文件其余删除字段保持一致,并加一行注释说明response_format已由前端编译为json_schema/regex/ebnf/structural_tag四个 typed 字段。
- 建议:在
- grammar 编译失败的状态码在 factory 边界被压平为 INVALID_PARAMS,丢失可重试语义 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:120- 建议:在 factory 边界按
absl::StatusCode映射:kInvalidArgument→INVALID_PARAMS,kResourceExhausted→ 容量类错误码(如MALLOC_FAILED),其余 →EXECUTION_EXCEPTION/UNKNOWN_ERROR,使后端已做的分类在对外错误码上保持可见;日志中带上原始 status code 与异常类型名,并补一条覆盖非 InvalidArgument 分支的用例。
- 建议:在 factory 边界按
- 新增 6 个 GRAMMAR_ 错误码未指定 ExceptionCategory,全部退化为 INTERNAL* @
rtp_llm/config/exceptions.py:45- 建议:按语义显式标注分类:
GRAMMAR_PARSER_REJECTED_TOKEN/GRAMMAR_NON_EOS_AFTER_TERMINAL/GRAMMAR_VERIFY_EXCEPTION用ExceptionCategory.INVALID_OUTPUT(对齐OUT_OF_VOCAB_RANGE);配置类错误若确定保留 INTERNAL,请补注释说明取舍,并确认 DashSC / OpenAI 两条通道的对外错误码映射符合预期。
- 建议:按语义显式标注分类:
- pin_memory() 未做平台保护,CPU/ARM 构建下 grammar 解码整条路径失效 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:236- 建议:把 pinned 分配收敛到确实需要 H2D 的场景:用
#if USING_CUDA || USING_ROCM包裹.pin_memory(),或按logits.is_cuda()在首次applyState时才升级为 pinned buffer,非加速器构建保持普通 CPU 张量。并为非 CUDA 构建补一条GrammarLogitsProcessor级用例(现有PackedMaskLogitsCpuFallbackTest只覆盖 kernel 层,未覆盖本处分配路径)。
- 建议:把 pinned 分配收敛到确实需要 H2D 的场景:用
- FINISHED 模式在 decode 路径 fail-open,且被掩码构建失败路径复用为缓存态 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:162- 建议:失败态不要复用语义为「matcher 已完成」的
FINISHED:新增FAILED模式,命中缓存时继续返回原始错误,或按 spec 路径语义只放行 EOS(复用forceEos)。同时把 FINISHED 的兜底方向写进GrammarLogitsProcessor.h或DecodeMaskBuilder注释并统一两条路径(建议 decode 也 fail-closed 或至少 WARNING),避免结构化输出请求在无任何信号的情况下返回不符合 schema 的文本。
- 建议:失败态不要复用语义为「matcher 已完成」的
- 投机路径未校验位掩码缓冲区尺寸,专用错误码 GRAMMAR_BITMASK_BUFFER_TOO_SMALL 是死码 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:55- 建议:在
prepareSpecMask开头补显式校验:bitmask_size_int32 >= bitmaskWordCount(vocab_size)不成立时返回ErrorInfo(ErrorCode::GRAMMAR_BITMASK_BUFFER_TOO_SMALL, ...),让已声明的错误码落地,并在GrammarLogitsProcessorTest补一个「缓冲区过小」负向用例。若确认由调用方保证,则删除 C++/Python 两侧的死错误码,并把该不变量写进SpecLogitsProcessor.h注释。
- 建议:在
- 单流错误隔离不完整,RTP_LLM_CHECK 抛出的异常越过 optional 边界 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:168- 建议:在
process/updateStatus/prepareSpeculative与SpecLogitsVerifyRunner::run统一补catch (const std::exception&)+catch (...),转换为ErrorInfo(可复用detail::grammarMaskBuildError风格),确保任何内部不变量违例只失败当前请求;同时把BitmaskUtils.h:30注释改为准确描述(默认抛std::runtime_error,仅在开启 core dump 配置时 abort)。
- 建议:在
- @xgrammar 依赖指向上游未合并分支的 commit,缺少镜像、校验与降级路径 @
3rdparty/xgrammar/repositories.bzl:8- 建议:按 dlpack 的既有做法把 xgrammar 固化为可控来源:改用带
sha256的http_archive指向内部镜像或 vendored tarball,并在注释中记录对应上游 PR、所需的 anytext-budget 特性与「何时切回上游 release」的收敛计划;至少补shallow_since。srcs建议改为显式文件列表,或保留 glob 但注明升级时需 review 新增源文件;另外仓库已有统一依赖加载入口,新增独立xgrammar_deps()会分裂依赖管理约定。
- 建议:按 dlpack 的既有做法把 xgrammar 固化为可控来源:改用带
- //rtp_llm:ops 新增 data 使全部 Python 目标在构建期强依赖 xgrammar 编译 @
rtp_llm/BUILD:195- 建议:移除
ops的这项data,改由真正调用serialize_grammar_tokenizer_info的目标声明(后端加载模型路径),打包侧已由whl_package_libs覆盖。若确需在ops层兜底,请补注释说明理由,并确认 CPU/ARM/ROCm 三条构建线都能获取@xgrammar;另建议顺带确认frontend_libs不含该 so 是否符合 FRONTEND 角色的预期。
- 建议:移除
- 声称验证 CPU fallback 的测试被 CUDA 依赖与 H20 执行属性锁死,非 CUDA 平台仍零覆盖 @
rtp_llm/models_py/bindings/core/test/BUILD:53- 建议:用
select({"//:using_cuda": [...], "//conditions:default": []})包住 CUDA 依赖并去掉exec_properties,让该 target 能在 CPU/ROCm/ARM 门禁上真实构建执行;若确因链接 libtorch_cuda 才需要 GPU 宿主,请补上与speculative/test/BUILD:22同样的理由注释并修正上方注释。同时把四组断言抽成共享 helper(传入设备与同步回调),由两个 target 各自复用,避免断言体漂移。
- 建议:用
- grammar smoke 在 OpenAI 通路上存在三条静默放行路径,且与 DashSc 通路语义相反 @
rtp_llm/test/smoke/openai_comparer.py:230- 建议:对齐
dash_grpc_comparer的 fail-fast 语义:response_format缺失、choices为空时抛SmokeException(QueryStatus.VALID_FAILED, ...);is_stream分支改为显式报「grammar_constraint_only 暂不支持流式用例」而非静默放行(若需支持,应先聚合 delta 再走validate_constraint)。建议把「取约束 + 缺失即报错」抽成两个 comparer 共用的工具函数,避免语义再次分叉。
- 建议:对齐
- grammar smoke 的唯一断言实现无单测,且含静默放行与过严分支 @
rtp_llm/test/smoke/grammar_constraint_validator.py:31- 建议:新增纯 stdlib、无需 GPU 的 py_test,用
subTest/参数化覆盖正反样本:合法/非法 regex、缺 required 字段、additionalProperties: false下的多余键、enum/pattern/maxLength/minItems/maxItems越界、glm_xml pair 外有多余文本、残留裸 trigger、at_least_one未命中、reasoning 序列缺 end marker、不支持的format.type。同时把if not schema: return与缺 begin/end 的continue改为显式抛ValueError,并把 :147 改为区分「未提供 required」与「显式空 required」。
- 建议:新增纯 stdlib、无需 GPU 的 py_test,用
- tokenizer 家族探测函数在单测中被整体 mock,决定启动成败的分派逻辑无真实覆盖 @
rtp_llm/config/test/grammar_tokenizer_info_test.py:73- 建议:为三个探测函数补真实断言(只暴露
mergeable_ranks的 fake、只暴露 sentencepiece API 的 fake、三者都不满足的对象),并覆盖「三者均 False」与「eos_token_id缺失导致stop_token_ids为空」这两个启动失败边界。再补一条端到端最小用例:真实 tokenizer 目录 →build_grammar_tokenizer_info_json→XGrammarBackend::create→ 对 BYTE_LEVEL/BYTE_FALLBACK 词表断言掩码允许集合正确,把 RAW 重建的等价性纳入回归网。
- 建议:为三个探测函数补真实断言(只暴露
- GrammarConfig 新增字段未同步到 .pyi 类型存根 @
rtp_llm/ops/libth_transformer_config.pyi:544- 建议:重新生成并提交
.pyi,补齐terminate_without_stop_token: bool与def __repr__(self) -> str: ...。为长期防漂移,可增加一个轻量测试或 CI 步骤,对比GrammarConfig的def_readwrite属性集合与存根声明是否一致。
- 建议:重新生成并提交
- GrammarConfig::setstate 靠类型嗅探分派 4 种布局,arity 6 已被未发布布局永久占用 @
rtp_llm/cpp/pybind/ConfigInit.cc:1348- 建议:改为显式版本号方案:
__getstate__首元素写入 schema version,__setstate__按 version 分派;仅保留 main 上真实存在过的 legacy 5 元组作为特例,其余状态直接抛明确错误,并相应精简测试。这样新增字段时不会破坏既有分支判定,也去掉了对元素类型的猜测。
- 建议:改为显式版本号方案:
- SpecLogitsVerifyRunner 复用 pinned 缓冲的 single-flight 契约仅靠注释约束 @
rtp_llm/cpp/models/logits_processor/SpecLogitsVerifyRunner.cc:202- 建议:参照
Sampler的 slot 轮转 + event 机制,或至少在run()入口加「上一次结果已被消费」的断言/序号校验,把隐式同步依赖变成显式契约;生命周期可保证后再把copy_改为non_blocking=true并配套 event 等待。
- 建议:参照
- MultiSeqLogitsProcessor 写 EOS 掩码缺少 vocab 边界校验,存在堆越界写风险 @
rtp_llm/cpp/models/logits_processor/MultiSeqLogitsProcessor.cc:39- 建议:在写入前校验
eos_token_id_ >= 0 && eos_token_id_ < vocab_size,不满足时返回ErrorInfo(ErrorCode::OUT_OF_VOCAB_RANGE, ...),并补一条越界 eos 的负向用例;亦可在 factory 装配处统一前置校验,与 grammar 路径共用同一条不变量。顺带确认 :43 的to(torch::kCUDA)在非 CUDA 构建上的行为是否需要同样收敛。
- 建议:在写入前校验
- MTP grammar smoke fixture 命名与实际 suite 接线不一致,thinking 关闭 + MTP + grammar 组合零覆盖 @
rtp_llm/test/smoke/data/model/qwen35/q_r_mtp_grammar.json:5- 建议:三选一并同步注释:把该 fixture 重命名为不含 MTP 字样并修正
_comment;给qwen35_grammar_concurrent_no_mtp补上 MTP 参数使名称与内容一致;或新增一个 thinking 关闭 + MTP 的 case 补齐该组合(推荐后者,它是prepareSpeculative唯一的端到端验证路径)。同时清理q_r_mtp_grammar_reasoning.json:5中对不存在文件与旧实现的引用。
- 建议:三选一并同步注释:把该 fixture 重命名为不含 MTP 字样并修正
- ThinkModeLogitsProcessor 沦为生产不可达代码,文档仍将其列为生效处理器且缺 GrammarLogitsProcessor 章节 @
docs/backend/logits_processor.md:8- 建议:明确取舍:若确定废弃,删除
ThinkModeLogitsProcessor.{h,cc}、BUILD 条目与测试,并清理think.py的过期注释;若保留作 fallback,则在工厂中恢复其创建路径。无论哪种都需更新文档:修正/删除 ThinkMode 段落并说明思考预算现由 Python 归一化为structural_tag;新增GrammarLogitsProcessor章节,列出支持的约束类型、与 beam /num_return_sequences/ MTP 的兼容矩阵、错误码含义与 4 个服务参数;并在 PR 描述中列出本 PR 的全部行为不兼容点清单。
- 建议:明确取舍:若确定废弃,删除
- xgrammar 词表空洞以空串填充,pad token 是否被屏蔽无任何覆盖 @
rtp_llm/config/grammar_tokenizer_info.py:28- 建议:改用一定不会被任何语法匹配的占位符填充空洞,或在
GrammarLogitsProcessor内显式清除这些 id 的 bit(clearBitmaskTokenRange已具备能力)。无论选哪种,都应新增断言:构造带空洞的词表后fillBitmask结果中空洞 id 必须不可选——这条用例本身即可确定该风险是否真实存在。
- 建议:改用一定不会被任何语法匹配的占位符填充空洞,或在
- 新增 grammar 服务参数的 env 通路与 compiler_cache_bytes 边界值缺少测试 @
rtp_llm/server/server_args/test/server_args_test.py:396- 建议:补三类用例:设置 4 个
GRAMMAR_*/CONSTRAINED_JSON_*env 后断言落到grammar_config对应字段(布尔项覆盖str2bool的"0"/"false");--grammar_compiler_cache_bytes取 0 与负值的端到端 unlimited 语义断言;把默认值断言改为显式常量,使其成为真正的回归护栏。
- 建议:补三类用例:设置 4 个
- _trans_jsonable_option 与 validate_engine_ready 的失败分支无测试 @
rtp_llm/cpp/model_rpc/test/model_rpc_client_test.py:173- 建议:补两条用例:构造
json_schema内含不可序列化对象(如set)的 config,用assertRaisesRegex断言异常文案包含字段名;构造response_format未被prepare_for_engine处理的 config,断言trans_input抛FtRuntimeException,确保该边界不会在后续重构中退化为静默通过。
- 建议:补两条用例:构造
P3
- prepareSpeculative 未与 process / updateStatus 保持 matcher_ 空值处理一致 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:377- 建议:统一该不变量:推荐在构造函数中
RTP_LLM_CHECK_WITH_INFO(matcher_ != nullptr, ...)并删除其余方法的冗余空判断;若要保留可空语义,则prepareSpeculative应在加锁后返回INVALID_PARAMS,且mtpCapability()在 matcher 为空时返回UNSUPPORTED。
- 建议:统一该不变量:推荐在构造函数中
- setIntervalError 对越界 interval 静默丢弃处理器错误 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorStates.cc:12- 建议:在 clamp 后判断是否真的写入过:若
begin >= end则至少RTP_LLM_LOG_WARNING(或直接RTP_LLM_CHECK)暴露区间越界,避免约束失效被静默吞掉。
- 建议:在 clamp 后判断是否真的写入过:若
- processor 与 stream 长度不一致这一内部不变量被归入 UNKNOWN_ERROR @
rtp_llm/cpp/engine_base/stream/GenerateStream.cc:964- 建议:改用能表达内部不变量违约的错误码(如
EXECUTION_EXCEPTION,或新增一个 processor 状态不一致专用码),并在消息中保留 processor 索引与两侧长度;若确定保留 UNKNOWN_ERROR,请补注释说明取舍,便于告警规则区分。
- 建议:改用能表达内部不变量违约的错误码(如
- grammar 包 BUILD 声明的 Tier 边界未由 visibility 落地,过渡 umbrella target 无存量调用方 @
rtp_llm/cpp/engine_base/grammar/BUILD:58- 建议:把 Tier 边界落到
visibility:grammar_xgrammar显式限定为["//rtp_llm/cpp/engine_base/grammar:__subpackages__", "//rtp_llm/cpp/models/logits_processor:__pkg__"],让注释中的约束由构建系统强制,并按需为测试单独放行;同时删除grammar_cpp_lib让调用方直接依赖grammar_xgrammar,或在注释中说明其保留原因与删除条件。
- 建议:把 Tier 边界落到
- grammar cpp 测试缺少与同类一致的执行环境声明,并残留未使用的 load @
rtp_llm/cpp/engine_base/grammar/test/BUILD:12- 建议:补上与同类一致的
exec_properties = {'gpu':'H20'}(cc_test 再加env = {"TEST_USING_DEVICE": "CUDA"}),或在注释中明确说明为何不需要 GPU 宿主;同时删除未使用的torch_depsload,并把config_pickle_test一并对齐。
- 建议:补上与同类一致的
- MTP propose 侧 EngineInitParams 未传播 grammar_config @
rtp_llm/cpp/pybind/multi_gpu_gpt/RtpLLMOp.cc:207- 建议:在
prepareMTPEngineInitParams中显式拷贝grammar_config(与其余子配置一致),或在该函数处加注释说明 MTP params 故意不携带 grammar 配置及其原因,避免后续维护者误用默认值。
- 建议:在
- compile 错误分类仅有 json schema 一个负向用例,regex/ebnf/structural_tag 可能被误报为可重试系统错误 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:29- 建议:对 regex(如未闭合
[)、EBNF(如缺失规则)、structural_tag(如缺format)各补一个EXPECT_EQ(status().code(), absl::StatusCode::kInvalidArgument)的负向用例;同时在catch (const std::exception&)分支日志中加上异常类型名,便于线上区分误分类。
- 建议:对 regex(如未闭合
Checklist Violations (17 fail / 58 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
grammar 包 BUILD 声明的 Tier 边界未由 visibility 落地,过渡 umbrella target 无存量调用方
:35 注释声明「only Tier 0 (this package) and Tier 1 (logits_processor) may depend」,但 :4 是package(default_visibility = ["//visibility:public"]),grammar_xgrammar对全仓可见,注释里的分层约束没有任何机制保证。:58-66 的grammar_cpp_lib注释为「Transitional umbrella; deleted once callers move onto :grammar_xgrammar」,但全仓只有本包测试grammar/test/BUILD:16与rtp_llm/cpp/models/BUILD依赖它——不存在需要迁移的存量调用方,该 target 属于引入即多余的间接层。 - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
GrammarConfig 新增字段未同步到 .pyi 类型存根
ConfigInit.cc:1325新增.def_readwrite("terminate_without_stop_token", ...)、:1330 新增__repr__;grammar_group_args.py:17以bind_to=(grammar_config, "terminate_without_stop_token")写入该字段,config_pickle_test.py、server_args_test.py也实际读写。但存根libth_transformer_config.pyi:544-556的class GrammarConfig只声明compiler_cache_bytes/constrained_json_disable_any_whitespace/num_workers/tokenizer_info_json,缺terminate_without_stop_token与__repr__。存根由生成脚本产出且无 CI 一致性校验,静态检查与 IDE 会 - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
grammar 包 BUILD 声明的 Tier 边界未由 visibility 落地,过渡 umbrella target 无存量调用方
:35 注释声明「only Tier 0 (this package) and Tier 1 (logits_processor) may depend」,但 :4 是package(default_visibility = ["//visibility:public"]),grammar_xgrammar对全仓可见,注释里的分层约束没有任何机制保证。:58-66 的grammar_cpp_lib注释为「Transitional umbrella; deleted once callers move onto :grammar_xgrammar」,但全仓只有本包测试grammar/test/BUILD:16与rtp_llm/cpp/models/BUILD依赖它——不存在需要迁移的存量调用方,该 target 属于引入即多余的间接层。 - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
processor 与 stream 长度不一致这一内部不变量被归入 UNKNOWN_ERROR
validateLogitsProcessorState(:953-971)用committedOutputLen()与outputTokenLen()做一致性校验,这是很有价值的新增护栏;但违约时返回ErrorInfo(ErrorCode::UNKNOWN_ERROR, ...),与采样失败等未分类故障共用同一错误码。该条件是明确的内部状态机违约(processor 状态与流权威历史脱节),线上无法按错误码单独定位。本 PR 已新增 6 个GRAMMAR_*专用错误码,此处保留 UNKNOWN_ERROR 与该取向不一致。 - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
@xgrammar 依赖指向上游未合并分支的 commit,缺少镜像、校验与降级路径
new_git_repository在构建期直接从公网 clonecommit = "60fc70ee...",注释写明该 commit 来自feat/anytext-budget(上游未合并的特性分支),既无镜像、无sha256/shallow_since,也无内容校验。该分支被删除或 force-push 后 commit 可能不可达,导致所有构建(含 CI)在无任何代码变更的情况下同时失败且无法离线复现。同 PR 的3rdparty/dlpack/BUILD已明确说明「xgrammar 上游以 submodule 拉取 dlpack,该地址在内部构建环境不可达,因此改为 vendored」——即已确认该环境对公网拉取存在限制,却仍让核心依赖走构建期 git clone。此外xgrammar.BUILD用glob(["cpp/*.cc", ...])收集源文件,上游新增文件会静默进入编译单元。 - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
MTP propose 侧 EngineInitParams 未传播 grammar_config
:207 新增params.grammar_config = engine_config.attr("grammar_config").cast<GrammarConfig>();,但只作用于 score 模型的EngineInitParams。prepareMTPEngineInitParams(:27-98)在构造每个 MTPEngineInitParams时逐项传入了十余个子配置(:73-90),唯独没有grammar_config(它不是构造参数),因此每个 MTP params 携带默认构造的GrammarConfig(tokenizer_info_json为空)。当前尚未触发问题:NormalExecutor.cc与MtpExecutor.cc调用LogitsProcessorFactory::init时传入的都是 score 模型的params.grammar_config;但后续若有代码读取 MTP params 的该字段,会拿到与服务配置不一致的静默默认值。 - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
processor 与 stream 长度不一致这一内部不变量被归入 UNKNOWN_ERROR
validateLogitsProcessorState(:953-971)用committedOutputLen()与outputTokenLen()做一致性校验,这是很有价值的新增护栏;但违约时返回ErrorInfo(ErrorCode::UNKNOWN_ERROR, ...),与采样失败等未分类故障共用同一错误码。该条件是明确的内部状态机违约(processor 状态与流权威历史脱节),线上无法按错误码单独定位。本 PR 已新增 6 个GRAMMAR_*专用错误码,此处保留 UNKNOWN_ERROR 与该取向不一致。 - [6.1] Quality — PR description 说明动机与设计 → issue
ThinkModeLogitsProcessor 沦为生产不可达代码,文档仍将其列为生效处理器且缺 GrammarLogitsProcessor 章节
全仓搜索确认ThinkModeLogitsProcessor仅剩自身.h/.cc、test/ThinkModeLogitsProcessorTest.cc、rtp_llm/cpp/models/BUILD条目与dash_sc/think.py一条已过期注释;fromGenerateInput(ThinkModeLogitsProcessor.cc:82)已无生产调用方,但仍被编入目标且测试仍在跑不可达代码。同时docs/backend/logits_processor.md:8依旧写着「ThinkModeLogitsProcessor: Think-mode control … by set max_thinking_tokens」;本次对该文档的唯一改动是 :68 新增一句 Tree Decode 与 MTP 互斥,全文搜索grammar仅命中该行。新引入的GrammarLogitsProcessor(四类约束、MTPSPEC_VERIFY、6 个GRAMMAR_*错误码、4 个 `--grammar*_ - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
声称验证 CPU fallback 的测试被 CUDA 依赖与 H20 执行属性锁死,非 CUDA 平台仍零覆盖
:53 注释写明 "Exercise the CPU fallback without initializing or allocating a GPU.",但该 target 在 :64 仍声明exec_properties = {"gpu": "H20"}(占用稀缺机位),且deps(:60)无条件包含 CUDA-only 的//rtp_llm/models_py/bindings/cuda/ops:flashinfer_runtime。结果是这条本应覆盖 ROCm/ARM/CPU 构建的 fallback 用例只能在 CUDA + H20 环境构建运行,而它要保护的正是「没有 CUDA 时runtimeApplyPackedMaskLogits仍正确」这一场景。此外PackedMaskLogitsCpuFallbackTest.cc的四个用例与ExecOpsTest.cc的四个testRuntimeApplyPackedMaskLogits*断言体逐字重复,仅设备与同步调用不同。 - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
grammar 包 BUILD 声明的 Tier 边界未由 visibility 落地,过渡 umbrella target 无存量调用方
:35 注释声明「only Tier 0 (this package) and Tier 1 (logits_processor) may depend」,但 :4 是package(default_visibility = ["//visibility:public"]),grammar_xgrammar对全仓可见,注释里的分层约束没有任何机制保证。:58-66 的grammar_cpp_lib注释为「Transitional umbrella; deleted once callers move onto :grammar_xgrammar」,但全仓只有本包测试grammar/test/BUILD:16与rtp_llm/cpp/models/BUILD依赖它——不存在需要迁移的存量调用方,该 target 属于引入即多余的间接层。 - [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue
prepareSpeculative 未与 process / updateStatus 保持 matcher_ 空值处理一致
process()(:308-310)、updateStatus()(:344-346)与acceptCommittedLocked()(:387)都以!matcher_短路,说明该类把「matcher 可能为空」当作合法状态;但prepareSpeculative在 :377 直接prepareSpecMask(*matcher_, ...)解引用,构造函数(:299-302)也未拒绝空 matcher,而mtpCapability()无条件返回SPEC_VERIFY,因此 MTP 路径一定会调用它。当前生产路径由createMatcherFromKey保证非空,属不可达;但三个入口对同一不变量的表达不一致,后续若新增构造路径(含单测)传入空 matcher,会从「安全跳过」变成 MTP 解码热路径上的空指针解引用。_ - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
grammar cpp 测试缺少与同类一致的执行环境声明,并残留未使用的 load
xgrammar_backend_cpp_test依赖grammar_cpp_lib,后者经grammar_xgrammar(BUILD:54)携带torch_deps(),默认 CUDA 配置下解析为 CUDA 版 libtorch,二进制启动时需要相应运行时;但该 target 既无env也无exec_properties,而同 PR 的speculative/test/BUILD:22-26对完全相同的情形写了理由注释并显式声明exec_properties = {'gpu':'H20'}。此外本文件 :2 的load(... "torch_deps")未被使用,是死 load。rtp_llm/cpp/pybind/BUILD的config_pickle_test存在同类问题(data 含原生 so 但无 GPU 槽位)。 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
compile 错误分类仅有 json schema 一个负向用例,regex/ebnf/structural_tag 可能被误报为可重试系统错误
compileWithErrorClassification只把std::runtime_error映射为InvalidArgumentError(用户语法错误),其余std::exception一律UnknownError("unexpected error (retryable): ...")(:29-31)。这条分类直接决定用户传坏语法时得到 4xx 还是「可重试」的系统错误。但XGrammarBackendTest.cc的 13 个用例中只有CompileMalformedJsonSchemaIsInvalid(:117)一个负向用例,CompileRegex/CompileGrammar/CompileStructuralTag(:147-153)三条分支在畸形输入下的异常类型完全未验证;由于依赖锚定在上游 feature 分支 commit,异常类型层次也不受版本保护,一旦某类异常不派生自std::runtime_error,坏语法会被当作系统故障反复重试。 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
compile 错误分类仅有 json schema 一个负向用例,regex/ebnf/structural_tag 可能被误报为可重试系统错误
compileWithErrorClassification只把std::runtime_error映射为InvalidArgumentError(用户语法错误),其余std::exception一律UnknownError("unexpected error (retryable): ...")(:29-31)。这条分类直接决定用户传坏语法时得到 4xx 还是「可重试」的系统错误。但XGrammarBackendTest.cc的 13 个用例中只有CompileMalformedJsonSchemaIsInvalid(:117)一个负向用例,CompileRegex/CompileGrammar/CompileStructuralTag(:147-153)三条分支在畸形输入下的异常类型完全未验证;由于依赖锚定在上游 feature 分支 commit,异常类型层次也不受版本保护,一旦某类异常不派生自std::runtime_error,坏语法会被当作系统故障反复重试。
RTP-LLM Checklist
- [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue
ThinkModeLogitsProcessor 沦为生产不可达代码,文档仍将其列为生效处理器且缺 GrammarLogitsProcessor 章节
全仓搜索确认ThinkModeLogitsProcessor仅剩自身.h/.cc、test/ThinkModeLogitsProcessorTest.cc、rtp_llm/cpp/models/BUILD条目与dash_sc/think.py一条已过期注释;fromGenerateInput(ThinkModeLogitsProcessor.cc:82)已无生产调用方,但仍被编入目标且测试仍在跑不可达代码。同时docs/backend/logits_processor.md:8依旧写着「ThinkModeLogitsProcessor: Think-mode control … by set max_thinking_tokens」;本次对该文档的唯一改动是 :68 新增一句 Tree Decode 与 MTP 互斥,全文搜索grammar仅命中该行。新引入的GrammarLogitsProcessor(四类约束、MTPSPEC_VERIFY、6 个GRAMMAR_*错误码、4 个 `--grammar*_ - [I] 代码质量 — 同一功能用统一工具函数 → issue
grammar cpp 测试缺少与同类一致的执行环境声明,并残留未使用的 load
xgrammar_backend_cpp_test依赖grammar_cpp_lib,后者经grammar_xgrammar(BUILD:54)携带torch_deps(),默认 CUDA 配置下解析为 CUDA 版 libtorch,二进制启动时需要相应运行时;但该 target 既无env也无exec_properties,而同 PR 的speculative/test/BUILD:22-26对完全相同的情形写了理由注释并显式声明exec_properties = {'gpu':'H20'}。此外本文件 :2 的load(... "torch_deps")未被使用,是死 load。rtp_llm/cpp/pybind/BUILD的config_pickle_test存在同类问题(data 含原生 so 但无 GPU 槽位)。
Python Static-First Checklist
- [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
tokenizer 家族探测函数在单测中被整体 mock,决定启动成败的分派逻辑无真实覆盖
辅助方法 :71-93 同时mock.patch.object掉_is_fast_tokenizer/_is_tiktoken_tokenizer/_is_sentencepiece_tokenizer/_is_byte_level_tokenizer,并把rtp_llm.ops整体替换为 fake,因此 dispatch 系列用例只验证「分支选中后 metadata 怎么拼」,不验证「分支会不会选对」。除_is_byte_level_tokenizer有 5 条直测外,另三个基于 duck-typing 的探测函数(含_has_tiktoken_encoding/_has_tiktoken_vocab_file/_has_mergeable_ranks三条 or 分支与_iter_sentencepiece_candidates三个候选路径)无任何直测;而它们既决定服务能否启动(raise ValueError: Unsupported tokenizer type),也决定vocab_type
Strengths
- 错误传播链路完整闭环:
process/updateStatus返回ErrorInfo→LogitsProcessorStates::batchProcess按 interval 归属到 row →SamplerOutput.processor_errors→GenerateStream::update/specUpdate先报错再返回,坏 token 不会被发布;MTP 侧经MtpBatchStreamProcessor把 per-stream 错误落到spec_update_info.error_info,两条路径口径一致。 - MTP spec-verify 状态机严谨:
prepareSpecMask用provisional_accepts计数并在返回前强制rollback,真正提交推迟到updateStatus();acceptCommittedLocked的rollback_commit闭包保证多 token 批量提交「全成功或整批回滚」,回滚失败时把原始错误与回滚错误一并上报。 acceptCommittedLocked刻意保留 TERMINATED 而非 FINISHED,并在 :424-427 说明原因(min_new_tokens/ignore_eos延迟结束时仍需只允许 EOS),是容易做错的语义细节。- 位掩码边界严谨:
clearBitmaskTokenRange对begin_word == end_word、begin_bit == 0、end_bit == 0三类退化分别处理,grammar vocab 小于 model vocab 时差集显式清零,不依赖 xgrammar 对 padding 位的行为;bitmaskAllowsToken对越界 token 返回 false。 packed_mask_logits内核对compact_row/vocab_idx/logits_row全部做边界短路,word_idx >= bitmask_words时默认allowed=false(fail-closed),偏移用 int64 计算避免溢出。SpecLogitsVerifyRunner::ensureBuffersFit用精确相等(而非>=)约束列数,使bitmask_cpu_out + offset * bitmask_words的行 stride 假设恒成立,避免narrow视图 stride 错位。GrammarConstraint用Literal+get_args把 grammar 字段清单收敛到单一来源,统一 collect/clear/normalize/resolve/apply 语义,彻底消除旧代码中json_format/json_schema/response_format的隐式覆盖优先级;ResponseFormat用extra="forbid"+model_validator(mode="after")做 type↔payload 交叉校验,并以model_serializer(mode="wrap")保证schema字段名往返一致。- 「请求入口编译一次、RPC 边界只读断言」的职责切分清晰:
prepare_response_format负责 compile+install,validate_engine_ready只做只读校验并显式 fail-fast;DashSC 两阶段用restore_final_constraint复用首次保存的 final constraint 而非二次编译。 - 跨语言错误码零漂移:
exceptions.py:45-50与ErrorCode.h:19-25的 608-613 数值与命名逐一对应,ErrorCode.h:131还提供名称映射,便于线上排障。 - 三方接入规范:
xgrammar.BUILD把上游cpp/*.h设为 private、只暴露xgrammar/*.h;dlpack 改为 vendored 并注明动机;th_grammar_tokenizer_info作为不含torch_deps()的独立.so,使 tokenizer 序列化测试能在纯 CPU 执行器上加载真实扩展。 - 测试密度高且未删除任何
TEST/TEST_F:GrammarLogitsProcessorTest覆盖 fp32/fp16/bf16、越界词表/EOS、all-true mask no-op、终止后强制 EOS、整批 draft 拒绝后的完整回滚、prepareSpeculative幂等;ExecOpsTest/CPU fallback 覆盖 vocab 非 32 对齐、越界 row、非连续输入;config_pickle_test用__reduce__伪造历史布局驱动真实__setstate__。 GenerateStream::validateLogitsProcessorState用committedOutputLen()与outputTokenLen()做一致性校验,把「processor 状态机与流权威历史脱节」这类静默错位变成显式错误;GrammarConfig::__repr__与to_string()均不输出tokenizer_info_json内容。
| copts = copts(), | ||
| ) | ||
|
|
||
| # Transitional umbrella; deleted once callers move onto :grammar_xgrammar. |
There was a problem hiding this comment.
[P3] grammar 包 BUILD 声明的 Tier 边界未由 visibility 落地,过渡 umbrella target 无存量调用方
:35 注释声明「only Tier 0 (this package) and Tier 1 (logits_processor) may depend」,但 :4 是 package(default_visibility = ["//visibility:public"]),grammar_xgrammar 对全仓可见,注释里的分层约束没有任何机制保证。:58-66 的 grammar_cpp_lib 注释为「Transitional umbrella; deleted once callers move onto :grammar_xgrammar」,但全仓只有本包测试 grammar/test/BUILD:16 与 rtp_llm/cpp/models/BUILD 依赖它——不存在需要迁移的存量调用方,该 target 属于引入即多余的间接层。
建议: 把 Tier 边界落到 visibility:grammar_xgrammar 显式限定为 ["//rtp_llm/cpp/engine_base/grammar:__subpackages__", "//rtp_llm/cpp/models/logits_processor:__pkg__"],让注释中的约束由构建系统强制,并按需为测试单独放行;同时删除 grammar_cpp_lib 让调用方直接依赖 grammar_xgrammar,或在注释中说明其保留原因与删除条件。
Checklist: [6.1] 依赖方向:无循环依赖/跨层惊喜;[6.1] 分层边界:新概念在正确层级,不泄漏内部;[6.1] KISS/YAGNI:无投机性抽象
| "@xgrammar//:xgrammar", | ||
| ] | ||
|
|
||
| cc_test( |
There was a problem hiding this comment.
[P3] grammar cpp 测试缺少与同类一致的执行环境声明,并残留未使用的 load
xgrammar_backend_cpp_test 依赖 grammar_cpp_lib,后者经 grammar_xgrammar(BUILD:54)携带 torch_deps(),默认 CUDA 配置下解析为 CUDA 版 libtorch,二进制启动时需要相应运行时;但该 target 既无 env 也无 exec_properties,而同 PR 的 speculative/test/BUILD:22-26 对完全相同的情形写了理由注释并显式声明 exec_properties = {'gpu':'H20'}。此外本文件 :2 的 load(... "torch_deps") 未被使用,是死 load。rtp_llm/cpp/pybind/BUILD 的 config_pickle_test 存在同类问题(data 含原生 so 但无 GPU 槽位)。
建议: 补上与同类一致的 exec_properties = {'gpu':'H20'}(cc_test 再加 env = {"TEST_USING_DEVICE": "CUDA"}),或在注释中明确说明为何不需要 GPU 宿主;同时删除未使用的 torch_deps load,并把 config_pickle_test 一并对齐。
Checklist: [6.1] 分布式/跨平台变更有对应覆盖;[I] 同一功能用统一工具函数
| py_eplb); | ||
| params.nccl_comm_config = engine_config.attr("nccl_comm_config").cast<NcclCommConfig>(); | ||
| params.server_config = engine_config.attr("server_config"); | ||
| params.grammar_config = engine_config.attr("grammar_config").cast<GrammarConfig>(); |
There was a problem hiding this comment.
[P3] MTP propose 侧 EngineInitParams 未传播 grammar_config
:207 新增 params.grammar_config = engine_config.attr("grammar_config").cast<GrammarConfig>();,但只作用于 score 模型的 EngineInitParams。prepareMTPEngineInitParams(:27-98)在构造每个 MTP EngineInitParams 时逐项传入了十余个子配置(:73-90),唯独没有 grammar_config(它不是构造参数),因此每个 MTP params 携带默认构造的 GrammarConfig(tokenizer_info_json 为空)。当前尚未触发问题:NormalExecutor.cc 与 MtpExecutor.cc 调用 LogitsProcessorFactory::init 时传入的都是 score 模型的 params.grammar_config;但后续若有代码读取 MTP params 的该字段,会拿到与服务配置不一致的静默默认值。
建议: 在 prepareMTPEngineInitParams 中显式拷贝 grammar_config(与其余子配置一致),或在该函数处加注释说明 MTP params 故意不携带 grammar 配置及其原因,避免后续维护者误用默认值。
Checklist: [6.1] 状态不变量:创建/更新/失败/重试/回滚路径有效
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1006
Status: BLOCKING
Summary: P0/0 · P1/6 · P2/28 · P3/4
Reviewed: commit 06ef47fa041c · 2026-08-06 17:04 UTC+8
Blocking Issues
P1
- 删除 --grammar_backend / GRAMMAR_BACKEND 破坏存量启动,并移除唯一的 grammar 关停开关 @
rtp_llm/server/server_args/grammar_group_args.py:5- 建议:保留一个已废弃的
--grammar_backend/GRAMMAR_BACKEND一个版本:取xgrammar时打 WARNING 后忽略;取none时跳过build_grammar_tokenizer_info(),让XGrammarBackend::create走已有的「tokenizer_info_json为空返回 nullptr」路径,并由createLogitsProcessors对 grammar 请求返回明确的「功能未启用」错误。或提供等价的显式开关(如--enable_grammar/--grammar_disable)。同时在 PR 描述与docs/backend/给出「删除项 → 新增项」的参数迁移对照表,保证「grammar 出问题可运维回滚」的能力不丢失,下个版本再彻底移除。
- 建议:保留一个已废弃的
- grammar tokenizer 元信息在启动期无条件构建,不支持的 tokenizer 直接阻塞模型加载且无逃生口 @
rtp_llm/config/grammar_tokenizer_info.py:195- 建议:把「元数据构建失败」与「服务可启动」解耦:新增服务级开关(如
GrammarConfig::require_tokenizer_info,或与上一条共用--enable_grammar),默认保持当前 fail-fast 以尽早暴露配置问题;关闭时降级为logging.warning+ 返回空串,交由 C++ 已有的 backend-disabled 分支在请求期以明确错误码拒绝单个 grammar 请求。若要保留 fail-fast,至少把「tokenizer 类型不受支持」(降级为 backend 关闭 + WARNING)与「tokenizer 受支持但 metadata 非法」(中止启动)分级。函数级已有test_build_tokenizer_info_rejects_unsupported_tokenizer,建议补的是覆盖 `rtp_llm_op...
- 建议:把「元数据构建失败」与「服务可启动」解耦:新增服务级开关(如
- proto 删除 response_format 字段 73 未加 reserved,混版部署下 grammar 约束被静默丢弃 @
rtp_llm/cpp/model_rpc/proto/model_rpc_service.proto:138- 建议:在
structural_tag = 72;之后补reserved 73;与reserved "response_format";,与第 103-104 行保持一致。若灰度期确实可能收到旧客户端该字段,建议保留字段号仅作兼容读取,QueryConverter检测到时返回ERROR_GENERATE_CONFIG_FORMAT并提示「请升级 frontend 或改用 json_schema / regex / ebnf / structural_tag」,实现 fail-fast 而非静默降级。并在发布说明中写明response_format自本版起由 Pythonprepare_for_engine投影为 typed 字段、不再上线传输,便于混部窗口定位约束失效。
- 建议:在
- think 模式改为硬依赖 grammar 约束,thinking + beam search / num_return_sequences>1 由可用变报错 @
rtp_llm/config/response_format_compiler.py:71- 建议:请在 PR 描述与发布说明中把该组合明确标注为不兼容变更,并二选一:(a) 仅当请求实际携带最终约束(
final_constraint is not None)时才拒绝 beam/多序列,纯 thinking 请求保留ThinkModeLogitsProcessor路径(让_reasoning_envelope在无最终约束时不安装引擎约束);(b) 若确定统一走 grammar,则提供过渡开关(如THINK_MODE_ALLOW_MULTI_SEQ,开启时退回旧处理器并打 WARNING),并补「thinking + beam 的替代方案」文档。无论哪种,请补一条「thinking + num_return_sequences>1」的用例固化期望行为(明确错误码与文案,或明确降级)。
- 建议:请在 PR 描述与发布说明中把该组合明确标注为不兼容变更,并二选一:(a) 仅当请求实际携带最终约束(
- in_think_mode / max_thinking_tokens 在引擎侧变为静默 no-op,ThinkModeLogitsProcessor 生产不可达 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:99- 建议:二选一并在 PR description 标注为不兼容变更:(1) 若 think 模式统一走 grammar,则在
createLogitsProcessors显式 fail-fast —— 当in_think_mode && max_thinking_tokens != 0 && grammar_key.empty()时返回ErrorInfo(INVALID_PARAMS, ...)提示必须先经 response_format 编译成 structural_tag,并同批删除ThinkModeLogitsProcessor.{h,cc}、其单测与 BUILD 条目,避免为不可达代码提供虚假信心;同时评估 proto 中这两个字段是否该标注为 deprecated。(2) 若需兜底,则在grammar_key为空或 `backend == nul...
- 建议:二选一并在 PR description 标注为不兼容变更:(1) 若 think 模式统一走 grammar,则在
- MTP 新增启动期 abort 与请求期硬拒绝,把既有可运行组合打死且无过渡开关 @
rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:230- 建议:fail-fast 方向正确,但请补齐可控性:(1) 在 PR description 中明确改动前 MTP + tree decode / recommendation / 多序列的实际行为(崩溃或约束静默失效),据此判定是否为用户可感知的不兼容变更;(2) 为该拒绝加 kmonitor 计数(按 processor 类型打点),便于上线前确认现网 MTP 集群是否存在此类流量;(3) 提供可运维回滚开关(如
MTP_ALLOW_UNSUPPORTED_LOGITS_PROCESSOR,开启时退化为 WARN + 放行旧行为);(4) 在docs/backend/logits_processor.md中与 tree decode 一样列出完整互斥矩阵。
- 建议:fail-fast 方向正确,但请补齐可控性:(1) 在 PR description 中明确改动前 MTP + tree decode / recommendation / 多序列的实际行为(崩溃或约束静默失效),据此判定是否为用户可感知的不兼容变更;(2) 为该拒绝加 kmonitor 计数(按 processor 类型打点),便于上线前确认现网 MTP 集群是否存在此类流量;(3) 提供可运维回滚开关(如
Non-blocking Suggestions
P2
- grammar 编译失败的状态码在 factory 边界被压平为 INVALID_PARAMS,丢失可重试语义 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:120- 建议:在 factory 侧按
absl::StatusCode映射:kInvalidArgument→INVALID_PARAMS,kResourceExhausted→MALLOC_FAILED(或既有可重试码段),其余 →EXECUTION_EXCEPTION;抽一个grammarStatusToErrorCode()供后续复用,并补一条断言「编译期资源类失败不会被报成INVALID_PARAMS」的单测。
- 建议:在 factory 侧按
- 新增 6 个 GRAMMAR_ 错误码未指定 ExceptionCategory,全部退化为 INTERNAL* @
rtp_llm/config/exceptions.py:45- 建议:逐个补 category:
GRAMMAR_VOCAB_EXCEEDS_MODEL_VOCAB/GRAMMAR_EOS_OUT_OF_VOCAB→BAD_REQUEST或UNSUPPORTED(视其是配置问题还是请求问题),GRAMMAR_PARSER_REJECTED_TOKEN/GRAMMAR_NON_EOS_AFTER_TERMINAL→INVALID_OUTPUT,缓冲/校验类保留INTERNAL并加注释说明。补一条对这 6 个码逐一断言category的表驱动用例,避免后续新增码再次漏标。
- 建议:逐个补 category:
- pin_memory() 未做平台保护,CPU/ARM/ROCm 构建下 grammar 解码整条路径失效 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:236- 建议:
prepareMask()仅在目标 logits 位于加速器设备(或at::getAccelerator().has_value())时才pin_memory(),否则退化为普通 CPU 张量;把applyState的设备暖存条件从编译期#if USING_CUDA改为运行期!logits.device().is_cpu()判断,使 ROCm 也能复用设备侧缓冲。补一个不依赖 GPU 的grammar_logits_processor_cpu_test锁住该路径。若 CPU/ARM 明确不支持 grammar,则应在启动期显式拒绝而非每步返回泛化错误。
- 建议:
- LogitsProcessorFactory::init 非幂等,单次启动重复重建后端并可被空配置静默降级 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:79- 建议:让
init()幂等且不可降级:仅在grammarBackend() == nullptr且tokenizer_info_json非空时创建;若已有后端而新传入的tokenizer_info_json与之不一致,则RTP_LLM_FAIL明确报错而不是静默覆盖。更彻底的做法是把该shared_ptr从 factory static 移到EngineBase/Executor成员,消除跨 executor 的生命周期耦合。补一个「重复 init(含 warm-up 路径)后后端仍可用」的单测。
- 建议:让
- FINISHED 模式在 decode 路径 fail-open,且被掩码构建失败路径复用为缓存态 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:162- 建议:把「构建失败」与「解析器正常终止」拆成两个状态(如新增
DeviceMaskMode::BUILD_FAILED),命中缓存时对BUILD_FAILED复现原错误或改走forceEos;或在 build 失败路径同时matcher.markFinished(),与process()的 vocab 校验分支保持一致语义;最低限度也应不要以失败的accepted_token_len写入缓存(保留token_len = -1使下次必然重建)。补一条「同一 accepted_token_len 上连续两次 apply,第二次仍返回错误或仍只放行 EOS」的单测。
- 建议:把「构建失败」与「解析器正常终止」拆成两个状态(如新增
- spec verify 的不变量校验以异常抛出,且掩码构造与应用使用两个不同 vocab 来源 @
rtp_llm/cpp/models/logits_processor/SpecLogitsVerifyRunner.cc:234- 建议:统一 vocab 来源:
applyMaskToLogits直接使用构造 mask 时的task.vocab_size(或随LaunchResult一起返回),并在runSpecLogitsVerify入口或启动期一次性断言model_config_.vocab_size与目标 logits 宽度一致。同时把run()的 catch 扩展到std::exception(或改用makeFailureResultForActiveStreams统一兜底),使形状类不变量违规也按 stream 归位,避免单请求问题放大为整批失败。
- 建议:统一 vocab 来源:
- @xgrammar 依赖指向上游未合并分支 commit,且绕过仓库集中式 git 依赖注册 @
3rdparty/xgrammar/repositories.bzl:5- 建议:(1) 把 xgrammar 迁入
git_deps()以保持单一依赖注册入口,删除 WORKSPACE 中的第二个 load;(2) 改用可校验、可镜像的形式(内部镜像或带 sha256 的http_archive),把「上游分支消失」这一供应链风险闭环;(3) 在3rdparty/xgrammar/下补一份说明:所需上游 PR / issue 链接、该 commit 相对主线的差异清单、待上游合并后切回 release tag 的计划,以及升级时必须重跑的等价性回归(见 RAW 重建那条)。
- 建议:(1) 把 xgrammar 迁入
- //rtp_llm:ops 新增 data 使全部 Python 目标在构建期强依赖 xgrammar 编译 @
rtp_llm/BUILD:195- 建议:把该 data 依赖下移到实际需要它的最小目标(
grammar_tokenizer_info.py所在的库,或既有的//rtp_llm:config_ops),或新增py_library(name = "grammar_ops", data = ["//:th_grammar_tokenizer_info"])供真实消费者显式依赖,使:ops保持轻量。同时评估把 grammar .so 做成运行期可选(import 失败即降级为 backend 关闭),与本 PR 的关停开关一并落地。
- 建议:把该 data 依赖下移到实际需要它的最小目标(
- 新增 native 扩展的生产导入链路被 mock 完全绕过,且缺 Bazel 依赖声明 @
rtp_llm/config/test/BUILD:18- 建议:为
//rtp_llm:config补上//rtp_llm:ops依赖(或把grammar_tokenizer_info.py移到已依赖:ops的目标下,如复用既有config_ops),并在 py_test 中新增一条不 mockrtp_llm.ops、不 mock 类型判定函数的用例,走真实from rtp_llm.ops import serialize_grammar_tokenizer_info完成一次最小 vocab 序列化。这样可把「.so未进 runfiles / 包装函数改名 / 包依赖缺失」这类只在部署期暴露的问题前移到单测。
- 建议:为
- tokenizer 家族探测函数在单测中被整体 mock,决定 vocab_type 的分派逻辑无真实覆盖 @
rtp_llm/config/test/grammar_tokenizer_info_test.py:73- 建议:参照已有
_is_byte_level_tokenizer的写法为上述谓词及其 helper 补参数化单测:覆盖 tiktokenEncoding内嵌、vocab_files_names缺失/类型异常、mergeable_ranks、sp_model/tokenizer.sp_model/tok三种 sentencepiece 挂载位置,以及三类都不匹配的 fallback;并保留至少一条不 mock 任何探测器、只 mockserialize_grammar_tokenizer_info的端到端用例,锁住「fast 优先于 tiktoken 优先于 sentencepiece」的顺序与最终vocab_type。
- 建议:参照已有
- TokenizerInfo 的 RAW 重建等价性无断言,生产词表类型仅靠重型 smoke 间接覆盖 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:82- 建议:新增用例:用 BYTE_FALLBACK(含
<0x0A>、▁前缀)与 BYTE_LEVEL(含Ġ)两种 metadata 经XGrammarBackend::create()后取出内部tokenizer_info_(测试已开-fno-access-control),断言GetDecodedVocab()/GetStopTokenIds()/GetAddPrefixSpace()/GetSpecialTokenIds()与直接DeserializeJSON的结果逐项相等;并在该词表上跑一次createMatcherFromKey+fillBitmask,验证含多字节内容的掩码与用原始 TokenizerInfo 构造的结果一致。把这条等价性作为 xgrammar 版本升级的回归闸门。
- 建议:新增用例:用 BYTE_FALLBACK(含
- 声称验证 CPU fallback 的测试被 CUDA 依赖与 H20 执行属性锁死,非 CUDA 平台仍零覆盖 @
rtp_llm/models_py/bindings/core/test/BUILD:53- 建议:去掉该 target 的
exec_properties与 CUDA 相关 deps,把 deps 收敛到不引入 CUDA runtime 的最小集合,使其能在无 GPU 的执行池上运行;若exec_ops_test_lib无法与 CUDA 解耦,则先抽出一个纯 CPU 的packed_mask_logits_cpu库供测试链接。并把该 CPU target 纳入默认 wildcard 测试门禁,让它真正成为非 GPU 平台的回归闸门。
- 建议:去掉该 target 的
- grammar smoke 在 OpenAI 通路上存在三条静默放行路径,且与 DashSc 通路语义相反 @
rtp_llm/test/smoke/openai_comparer.py:230- 建议:把三条静默返回改为 fail-fast,与
dash_grpc_comparer.py对齐(流式暂不支持时也应显式失败而非通过);同时保留一条确定性断言下限(至少校验len(choices) >= 1与finish_reason与 golden 一致),避免grammar_constraint_only把usage/finish_reason回归一并屏蔽。建议把「取 response_format / 缺失即报错」抽成两个 comparer 共用的辅助函数,消除重复实现带来的语义漂移。
- 建议:把三条静默返回改为 fail-fast,与
- grammar_constraint_validator 的 structural_tag→json_schema 分支在 schema 缺失时静默通过 @
rtp_llm/test/smoke/grammar_constraint_validator.py:31- 建议:在
validate_structural_format的 json_schema 分支显式校验 schema 非空后再下发;同时收紧validate_json_schema_instance的if not schema: return(例如仅在调用方显式传入「允许空 schema」标志时才放行),使这个测试 oracle 的默认行为是 fail-closed。并为该 validator 自身补一组不依赖模型的纯 Python 单测(合法 / 违反 schema / 缺 schema / 缺 json_schema 键各一条),避免唯一的 smoke 断言实现自身无覆盖。
- 建议:在
- MTP grammar smoke fixture 命名与实际 suite 接线不一致,thinking 关闭 + MTP + grammar 组合零覆盖 @
rtp_llm/test/smoke/data/model/qwen35/q_r_mtp_grammar.json:5- 建议:(1) 新增一条 MTP + grammar +
--think_mode 0的 smoke 用例(为该 fixture 配上带--sp_model_type/--sp_type的 smoke_args),使 spec-verify 的 cap 回截与 bitmask 合并逻辑在真实模型上被执行一次;并把当前无 MTP 的基线 fixture 重命名(如q_r_grammar_no_mtp.json)以消除命名与接线的矛盾。(2) 注释与实现二选一对齐:要么实现注释描述的「约束通过后用 actual 覆盖非确定性字段、其余仍走常规比对」让 golden 的usage/finish_reason真正生效,要么改写注释为「golden result 仅用于 schema 校验」并把result块裁剪到 pydantic 解析所需的最小字段。
- 建议:(1) 新增一条 MTP + grammar +
- NormalOutputDispatcherTest 未构造其注释声称守护的 beam 展开场景 @
rtp_llm/cpp/normal_engine/test/NormalOutputDispatcherTest.cc:13- 建议:补充
cur_batch_size=2且batch_idx_in=0的用例,断言不会越界读到下一个 stream 的错误;再补一个success_cpu定义为{true,false}的用例,确认只保留首个错误,并明确processor_errors与success_cpu的优先级符合预期(当前实现是processor_errors优先)。
- 建议:补充
- clearBitmaskTokenRange 的字对齐与单字分支无测试覆盖 @
rtp_llm/cpp/models/logits_processor/BitmaskUtils.cc:63- 建议:补一组针对
BitmaskUtils的纯 CPU 参数化单测(无需 GPU,可放独立 cc_test),至少覆盖(32,64)(两端对齐、单字)、(33,40)(同字内非对齐)、(0,64)(多字全清)、(10,100)且words=2(上界 clamp)、(96,200)(end 超出words*32)、(70,70)与(70,35)(早退不改动位图),逐位比对期望的 int32 位模式。
- 建议:补一组针对
- MultiSeqLogitsProcessor 写 EOS 掩码缺少 vocab 边界校验,存在堆越界写风险 @
rtp_llm/cpp/models/logits_processor/MultiSeqLogitsProcessor.cc:39- 建议:在
fromGenerateInput中一次性校验eos_token_id >= 0,并在process()内以if (eos_token_id_ < 0 || static_cast<size_t>(eos_token_id_) >= vocab_size) return ErrorInfo(...)收敛(新签名已支持返回错误,无需新增接口,可复用GRAMMAR_EOS_OUT_OF_VOCAB或新增等价码)。补两条单测:eos 恰为vocab_size - 1(合法边界)与 eos>= vocab_size(期望返回错误而非越界写)。
- 建议:在
- GrammarConfig 新增字段未同步到 .pyi 类型存根 @
rtp_llm/ops/libth_transformer_config.pyi:544- 建议:按既有流程重新生成 stub(或手工补齐):在
GrammarConfig中加入terminate_without_stop_token: bool与def __repr__(self) -> str: ...,保持 pyi 与ConfigInit.cc暴露面一致。建议在 CI 中加一条 stub 一致性检查(对比 pybind 暴露的属性名集合与 pyi 声明),避免这类漂移长期累积。
- 建议:按既有流程重新生成 stub(或手工补齐):在
- GrammarConfig::setstate 靠运行时类型嗅探分派 4 种布局,并静默丢弃 legacy 语义 @
rtp_llm/cpp/pybind/ConfigInit.cc:1347- 建议:收敛为「当前布局 + 至多一个已发布的 legacy 布局」,删除依赖运行时类型嗅探的分支及
config_pickle_test.py中对应的未发布布局用例;若确需跨版本兼容,改为在元组首位写入显式 version 号。保留 legacy 分支时,在丢弃grammar_backend/override_stop_tokens时输出一条含被丢弃值的 WARNING,使配置语义变化在日志中可追溯。
- 建议:收敛为「当前布局 + 至多一个已发布的 legacy 布局」,删除依赖运行时类型嗅探的分支及
- response_format 由宽松直通改为严格校验,收紧了 OpenAI 兼容外部契约 @
rtp_llm/config/response_format.py:37- 建议:方向上认同「入口 fail-fast 优于静默透传」,但这是外部 HTTP 契约的破坏性收紧,建议:(1) 在 PR 描述与用户文档中列出「此前被接受、现在返回 4xx」的
response_format写法及等价新写法;(2) 确认这些错误被分类为ExceptionCategory.BAD_REQUEST(4xx)而非 5xx,避免调用方误判为服务故障;(3) 对顶层平铺schema这类高频历史写法,评估是否值得在parse_response_format中做一次性归一化 + deprecation 日志,给下游一个灰度窗口。
- 建议:方向上认同「入口 fail-fast 优于静默透传」,但这是外部 HTTP 契约的破坏性收紧,建议:(1) 在 PR 描述与用户文档中列出「此前被接受、现在返回 4xx」的
- legacy structural_tag(structures/triggers)在 think 与非 think 模式下受理结果不一致 @
rtp_llm/config/grammar_constraint.py:64- 建议:明确取舍:若不支持 legacy
structures/triggers,在normalize_grammar_value()阶段即以ERROR_INPUT_FORMAT_ERROR拒绝并给出「请使用 type=structural_tag + format」的指引(与ResponseFormat入口保持一致),同时删除该补全分支;若要支持,则补一个 structures/triggers →format节点的转换。两种做法都应为 think / 非 think 两条路径各加一个用例。
- 建议:明确取舍:若不支持 legacy
- update() / update_and_pop() 静默吞掉 response_format 解析异常,且解析逻辑重复 @
rtp_llm/config/generate_config.py:477- 建议:抽取
_coerce_grammar_field(key, value)供两处复用。同时保住诊断信息:捕获后不要pass,把原始异常存入私有属性(如_deferred_response_format_error),在prepare_for_engine()/_resolve_final_constraint抛出时用raise ... from链上真实原因;至少补一条logging.warning。若确认request_extractor两阶段合并不再需要延迟报错,更简洁的做法是让异常在update()处直接抛出,与构造路径的field_validator行为一致。
- 建议:抽取
- THINK_*_TAG 转义解析语义收窄为仅处理 \n @
rtp_llm/config/response_format.py:14- 建议:要么显式扩展到常见转义(
\n/\r/\t),要么保留新语义但在docs/backend/与 PR 描述中写明「THINK_*_TAG仅支持\n字面转义」,并对含其它转义序列的配置在启动时给出 WARN,便于运维发现。补一条覆盖含\t/\uXXXX配置的用例以固定所选语义。
- 建议:要么显式扩展到常见转义(
- processor 与 stream 长度不一致这一内部不变量被归入 UNKNOWN_ERROR @
rtp_llm/cpp/engine_base/stream/GenerateStream.cc:953- 建议:为该不变量分配专用错误码(例如复用
EXECUTION_EXCEPTION或新增GRAMMAR_PROCESSOR_STATE_MISMATCH),并在返回前补一条 ERROR 级日志记录 processor 类型、processor_len、stream_len 与 stream_id;同时评估是否需要 kmonitor 计数,使该 case 在现网可被主动监控而不是只能事后翻日志。
- 建议:为该不变量分配专用错误码(例如复用
- ThinkModeLogitsProcessor 文档未更新,且全文缺 GrammarLogitsProcessor 章节 @
docs/backend/logits_processor.md:8- 建议:补一节 GrammarLogitsProcessor:支持的约束类型(
json_schema/regex/ebnf/structural_tag与response_format的映射关系)、think 模式如何折叠为 structural_tag envelope、启用与关停方式(含本轮要求补回的开关)、MTP spec-verify 的行为、与 beam /num_return_sequences/ recommendation / tree decode 的完整互斥矩阵、4 个--grammar_*参数含义与默认值、6 个错误码及其可重试性;同时把第 8 行的 ThinkModeLogitsProcessor 条目改为「已由 grammar 约束替代」或直接删除,并列出被移除参数的迁移对照。
- 建议:补一节 GrammarLogitsProcessor:支持的约束类型(
- 新增 grammar 服务参数的默认值断言与被测默认值同源,且 env 通路无覆盖 @
rtp_llm/server/server_args/test/server_args_test.py:423- 建议:对影响线上语义的字段断言字面量期望值(
terminate_without_stop_token期望 False、num_workers期望 8、compiler_cache_bytes期望 51210241024),把默认值本身纳入回归保护;字段与 argparse 的绑定关系可保留现有同源比较。并参照同文件test_env_vars_set_to_py_env_configs的写法补一条环境变量绑定用例,以及compiler_cache_bytes <= 0(无限制语义)的边界用例。
- 建议:对影响线上语义的字段断言字面量期望值(
- 大量与 grammar 功能无关的格式化重排混入本次改动 @
rtp_llm/cpp/models/logits_processor/test/RecommendationLogitsProcessorTest.cc:79- 建议:把纯格式化改动拆到独立的 format-only commit(或独立 PR),使本 PR 的 diff 只保留 grammar 相关语义变更;若因 lint gate 必须同批提交,请在 PR 描述中逐文件标注「仅格式化」与「含逻辑变更」,以保住本次评审与后续
git blame/bisect的可用性。同时在 PR 描述中显式说明neg_inf哨兵值统一的动因与影响面,并补齐当前缺失的不兼容变更清单与迁移说明(见各条 P1)。
- 建议:把纯格式化改动拆到独立的 format-only commit(或独立 PR),使本 PR 的 diff 只保留 grammar 相关语义变更;若因 lint gate 必须同批提交,请在 PR 描述中逐文件标注「仅格式化」与「含逻辑变更」,以保住本次评审与后续
P3
- prepareSpeculative 未与 process / updateStatus 保持 matcher_ 空值处理一致 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:377- 建议:统一不变量:推荐在构造函数中
RTP_LLM_CHECK_WITH_INFO(matcher != nullptr, ...)把「非空」固化为类不变量,并删除其余三处冗余判空;若确需允许空 matcher(降级为 no-op processor),则在prepareSpeculative开头补if (!matcher_) return ErrorResult<int>(request.propose_step);之类的显式语义,并补一条对应单测。
- 建议:统一不变量:推荐在构造函数中
- prepareSpecMask 的 EOS 越界出口未 fail-closed,且 GRAMMAR_BITMASK_BUFFER_TOO_SMALL 为死码 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:48- 建议:在 EOS 越界分支也走
fail_closed(EOS 非法时退化为整行清零),使「任何错误出口都 fail-closed」成为无例外的显式契约;并在SpecLogitsProcessorRequest注释中声明「prepareSpeculative返回错误时 cap 必须按 0 处理」,配一条RTP_LLM_CHECK或单测锁定该耦合。对GRAMMAR_BITMASK_BUFFER_TOO_SMALL:要么在prepareSpeculative入口补一次bitmask_size_int32 >= bitmaskWordCount(vocab_size)的防御校验并用该码返回,要么删除该未使用的错误码。
- 建议:在 EOS 越界分支也走
- 服务级 --grammar_terminate_without_stop_token 与 C++ 同名 per-request 字段并存,后者解析即忽略 @
rtp_llm/cpp/engine_base/stream/GenerateConfig.h:57- 建议:二选一并保持单一入口:要么删除该 dead 字段(同时移除 JSONIZE 与 debugString 打印),要么在请求携带该字段且与服务级策略不一致时打 WARNING 或返回
INVALID_PARAMS,明确「per-request 不可覆盖」这一契约。若保留仅为兼容旧 raw 请求不报错,请把注释扩写为「解析但忽略」,并在docs/backend/中同步说明。
- 建议:二选一并保持单一入口:要么删除该 dead 字段(同时移除 JSONIZE 与 debugString 打印),要么在请求携带该字段且与服务级策略不一致时打 WARNING 或返回
- 新增 grammar 测试 target 与 BUILD 声明存在重复、失效配置与未落地的 Tier 边界 @
rtp_llm/cpp/models/logits_processor/test/BUILD:65- 建议:改为
deps = test_deps + [grammar 与 exec_ops 相关新增项];若需排除device_test_utils/config_modules,可拆出更小的base_test_deps供两类 target 共用。删除无消费者的TEST_USING_DEVICE或补注释说明其理由,并清理grammar/test/BUILD的未使用 load。同时把grammar_xgrammar的 visibility 显式收敛到 Tier 0 / Tier 1 两个包(覆盖 package 默认值),让注释声明的边界由构建系统真正落地。
- 建议:改为
Checklist Violations (19 fail / 58 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
新增 native 扩展的生产导入链路被 mock 完全绕过,且缺 Bazel 依赖声明
生产链路是grammar_tokenizer_info.py:150的from rtp_llm.ops import serialize_grammar_tokenizer_info(函数内 lazy import)。但grammar_tokenizer_info_test.py的build_tokenizer_info用mock.patch.dict(sys.modules, {"rtp_llm.ops": fake_ops})完全绕开该包装(第 68-72 行),唯一触达 native 的用例用importlib.util.spec_from_file_location按 runfiles 路径直接加载.so,同样不经rtp_llm/ops/__init__.py。同时该 py_test 只声明deps = ["//rtp_llm:config"](BUILD:25-27),而rtp_llm/BUILD:329-340的config目标 deps 仅含//rtp_llm/distribute与 - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
ThinkModeLogitsProcessor 文档未更新,且全文缺 GrammarLogitsProcessor 章节
该文档在本 PR 中被修改(新增第 68 行「Tree Decode cannot be enabled together with MTP」),但第 6-9 行的 Built-in processors 仍只有 MultiSeq / ThinkMode / Tree 三项,且把ThinkModeLogitsProcessor列为生效处理器——而它在createLogitsProcessors中已无构造点。全文没有任何GrammarLogitsProcessor/ xgrammar /response_format字样,也没有新增的--grammar_terminate_without_stop_token/--grammar_num_workers/--grammar_compiler_cache_bytes说明、--grammar_backend的移除说明、6 个GRAMMAR_*错误码,以及 grammar 与 MTP / beam /num_return_sequences>1/ recommenda - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
新增 grammar 测试 target 与 BUILD 声明存在重复、失效配置与未落地的 Tier 边界
同一 BUILD 文件顶部已定义test_deps(第 10-19 行,含device_test_utils、config_modules、gtest、cuda_headers、cudart、torch_deps()),前三个 target 均复用;新增的grammar_logits_processor_test(第 65-86 行)重新手写一份 deps 并重复列出cuda_impl、gtest/gtest_main、cuda_headers、cudart、torch_deps()。同时该 target 设env = {"TEST_USING_DEVICE": "CUDA"}(第 82-84 行),但GrammarLogitsProcessorTest.cc的 25 个用例全部是裸TEST(...),文件内无TEST_F/DeviceTestBase/DeviceFactory引用,该环境变量在本 target 内无消费者。另engine_base/grammar/BUILD:35注释声称「onl - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
processor 与 stream 长度不一致这一内部不变量被归入 UNKNOWN_ERROR
validateLogitsProcessorState()在committedOutputLen()与outputTokenLen()不一致时返回ErrorInfo(ErrorCode::UNKNOWN_ERROR, "logits processor committed output length mismatch: processor_index=...")(第 963-967 行)。这是纯内部状态机不变量被破坏的信号(processor 状态与 stream 权威 token 历史脱节),属于必须排查的引擎 bug,却与网络异常、未分类第三方错误共用同一个UNKNOWN_ERROR码;本 PR 同批新增了 6 个GRAMMAR_*专用码,却未为此路径分配可检索的码,线上按错误码聚合时该问题无法被单独发现或告警。 - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
@xgrammar 依赖指向上游未合并分支 commit,且绕过仓库集中式 git 依赖注册
xgrammar_deps()用new_git_repository直接从上游 GitHub 拉取一个 commit,第 4 行注释标明该 commit 来自特性分支「feat/anytext-budget: structural-tag max_tokens/max_chars enforcement」,即依赖上游未合并代码;声明中无shallow_since、无 sha256/归档校验、无内部镜像,一旦该分支被 force-push 或删除即无法复现构建。同时仓库既有约定是 git 依赖集中声明在@rtp_deps的git_deps()(WORKSPACE:27-29,cutlass、flashinfer、googletest、abseil 等均在此),本 PR 却新建3rdparty/xgrammar/repositories.bzl并在WORKSPACE:31-33单独 loadxgrammar_deps(),形成第二套依赖注册入口,后续统一升级易漏改一处。 - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
prepareSpecMask 的 EOS 越界出口未 fail-closed,且 GRAMMAR_BITMASK_BUFFER_TOO_SMALL 为死码
prepareSpecMask的其余错误出口都经fail_closedlambda(第 56-60 行)执行markFinished()+forceTokenInBitmask,唯独第一个分支(eos_token_id < 0 || >= request.vocab_size,第 48-53 行)只markFinished()并返回错误,不触碰任何 bitmask 行。当前正确性依赖两处外部行为:SpecLogitsVerifyRunner.cc:175每次调用前无条件fillAllAllowBitmask(proc_mask)(因此不存在未初始化数据被 AND 的问题),以及第 183 行int cap = 0;在!cap_or.ok()时把spec_cap压到 0。但该行仍以 allow-all 而非仅 EOS 放行,与同函数其他出口语义不一致。另外GRAMMAR_BITMASK_BUFFER_TOO_SMALL(610) 在ErrorCode.h:22与exceptions.py:47 - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
服务级 --grammar_terminate_without_stop_token 与 C++ 同名 per-request 字段并存,后者解析即忽略
新增 flag 的 help 明确写「This is a service-level policy applied to every grammar request」,实际行为也如此:XGrammarBackend.cc:109与第 180 行只从GrammarConfig::terminate_without_stop_token取值,ConfigModules.h:376注释亦写明「Requests cannot override it」。但GenerateConfig.h:57同时存在 per-request 的bool grammar_terminate_without_stop_token(第 56 行注释自认「Legacy raw-request compatibility only」),并在第 237 行JSONIZE、第 186 行打进debugString;全仓 grep 显示除声明/序列化/打印外没有任何读取处。Python 侧GenerateConfig亦不含该字段(`generate_config_test. - [6.1] Quality — Commit 原子、message 与行为匹配 → issue
大量与 grammar 功能无关的格式化重排混入本次改动
多个分片在 diff 层独立观察到同一模式:RecommendationLogitsProcessorTest.cc的真实语义改动只有把ASSERT_TRUE(... == -FLT_MAX || ... == -INFINITY)收紧为ASSERT_FLOAT_EQ(BaseLogitsProcessor::neg_inf, ...)(13 处,如第 79、141、1057 行),其余为 clang-format 参数对齐重排;case_runner.py的真实逻辑改动只有grammar_constraint_only透传两行,同 diff 夹带近 200 行 isort/black 重排;openai_comparer.py、dash_grpc_comparer.py、dash_sc/test/codec_test.py、model_rpc_client_test.py同样模式。在 135 个改动文件的 PR 中,掩码哨兵值由-INFINITY统一为-FLT_MAX这类行为变更极易被漏看。 - [6.1] Quality — Mega-PR 已拆分为独立变更 → issue
大量与 grammar 功能无关的格式化重排混入本次改动
多个分片在 diff 层独立观察到同一模式:RecommendationLogitsProcessorTest.cc的真实语义改动只有把ASSERT_TRUE(... == -FLT_MAX || ... == -INFINITY)收紧为ASSERT_FLOAT_EQ(BaseLogitsProcessor::neg_inf, ...)(13 处,如第 79、141、1057 行),其余为 clang-format 参数对齐重排;case_runner.py的真实逻辑改动只有grammar_constraint_only透传两行,同 diff 夹带近 200 行 isort/black 重排;openai_comparer.py、dash_grpc_comparer.py、dash_sc/test/codec_test.py、model_rpc_client_test.py同样模式。在 135 个改动文件的 PR 中,掩码哨兵值由-INFINITY统一为-FLT_MAX这类行为变更极易被漏看。 - [6.1] Quality — PR description 说明动机与设计 → issue
ThinkModeLogitsProcessor 文档未更新,且全文缺 GrammarLogitsProcessor 章节
该文档在本 PR 中被修改(新增第 68 行「Tree Decode cannot be enabled together with MTP」),但第 6-9 行的 Built-in processors 仍只有 MultiSeq / ThinkMode / Tree 三项,且把ThinkModeLogitsProcessor列为生效处理器——而它在createLogitsProcessors中已无构造点。全文没有任何GrammarLogitsProcessor/ xgrammar /response_format字样,也没有新增的--grammar_terminate_without_stop_token/--grammar_num_workers/--grammar_compiler_cache_bytes说明、--grammar_backend的移除说明、6 个GRAMMAR_*错误码,以及 grammar 与 MTP / beam /num_return_sequences>1/ recommenda - [6.1] Quality — 逻辑变更未混入无关格式化 → issue
大量与 grammar 功能无关的格式化重排混入本次改动
多个分片在 diff 层独立观察到同一模式:RecommendationLogitsProcessorTest.cc的真实语义改动只有把ASSERT_TRUE(... == -FLT_MAX || ... == -INFINITY)收紧为ASSERT_FLOAT_EQ(BaseLogitsProcessor::neg_inf, ...)(13 处,如第 79、141、1057 行),其余为 clang-format 参数对齐重排;case_runner.py的真实逻辑改动只有grammar_constraint_only透传两行,同 diff 夹带近 200 行 isort/black 重排;openai_comparer.py、dash_grpc_comparer.py、dash_sc/test/codec_test.py、model_rpc_client_test.py同样模式。在 135 个改动文件的 PR 中,掩码哨兵值由-INFINITY统一为-FLT_MAX这类行为变更极易被漏看。 - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
新增 grammar 测试 target 与 BUILD 声明存在重复、失效配置与未落地的 Tier 边界
同一 BUILD 文件顶部已定义test_deps(第 10-19 行,含device_test_utils、config_modules、gtest、cuda_headers、cudart、torch_deps()),前三个 target 均复用;新增的grammar_logits_processor_test(第 65-86 行)重新手写一份 deps 并重复列出cuda_impl、gtest/gtest_main、cuda_headers、cudart、torch_deps()。同时该 target 设env = {"TEST_USING_DEVICE": "CUDA"}(第 82-84 行),但GrammarLogitsProcessorTest.cc的 25 个用例全部是裸TEST(...),文件内无TEST_F/DeviceTestBase/DeviceFactory引用,该环境变量在本 target 内无消费者。另engine_base/grammar/BUILD:35注释声称「onl - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
服务级 --grammar_terminate_without_stop_token 与 C++ 同名 per-request 字段并存,后者解析即忽略
新增 flag 的 help 明确写「This is a service-level policy applied to every grammar request」,实际行为也如此:XGrammarBackend.cc:109与第 180 行只从GrammarConfig::terminate_without_stop_token取值,ConfigModules.h:376注释亦写明「Requests cannot override it」。但GenerateConfig.h:57同时存在 per-request 的bool grammar_terminate_without_stop_token(第 56 行注释自认「Legacy raw-request compatibility only」),并在第 237 行JSONIZE、第 186 行打进debugString;全仓 grep 显示除声明/序列化/打印外没有任何读取处。Python 侧GenerateConfig亦不含该字段(`generate_config_test. - [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue
prepareSpeculative 未与 process / updateStatus 保持 matcher_ 空值处理一致
process()(第 308 行)、updateStatus()(第 344 行)、acceptCommittedLocked()(第 387 行)都以if (!matcher_)起手并静默返回,构造函数(第 299-302 行)为 public 且直接接收shared_ptr、无非空断言,说明该类把「matcher 为空」视为合法状态;但prepareSpeculative第 377 行直接prepareSpecMask(*matcher_, ...)解引用,而GrammarLogitsProcessor.h:29的mtpCapability()无条件返回SPEC_VERIFY,MTP 路径必然调用它。结果是同一个非法构造在普通解码下静默 no-op,在 MTP 下直接空指针崩溃。生产路径上LogitsProcessorFactory.cc:118-122只在createMatcherFromKey成功后构造,因此当前不可触发。_ - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
声称验证 CPU fallback 的测试被 CUDA 依赖与 H20 执行属性锁死,非 CUDA 平台仍零覆盖
packed_mask_logits_cpu_fallback_test上方注释写明「Exercise the CPU fallback without initializing or allocating a GPU」(第 53 行),但该 target 仍设exec_properties = {"gpu": "H20"}(第 64 行),且 deps 引入//rtp_llm/models_py/bindings/cuda/ops:flashinfer_runtime(第 60 行)。结果是这个本应验证「无 GPU 环境下 CPU fallback 可用」的测试,只能在 H20 GPU 机器上运行并链接 CUDA 运行时——恰恰无法证明它声称保障的属性;结合上文pin_memory()未做平台保护,CPU/ARM/ROCm 构建下的 grammar 掩码路径仍然零覆盖。 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
新增 grammar 服务参数的默认值断言与被测默认值同源,且 env 通路无覆盖
test_grammar_defaults以expected = type(g)()作为期望值(第 428-439 行),而grammar_group_args.py:11/19/31/45的 argparsedefault本身就取自同一个GrammarConfig默认值,两侧同源:把ConfigModules.h:377的terminate_without_stop_token默认从 false 改成 true(作用于所有 grammar 请求的服务级终止策略)后测试仍通过,只有bind_to字段名写错才会被发现。grammar_group_args.py也是整个 server_args 包中唯一使用default=<config>.<field>的文件(4 处),其余参数组均写字面量。此外两个新增 env(GRAMMAR_TERMINATE_WITHOUT_STOP_TOKEN/GRAMMAR_COMPILER_CACHE_BYTES)只覆盖了 CLI 路径,env_name绑定路径无用例 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
新增 grammar 服务参数的默认值断言与被测默认值同源,且 env 通路无覆盖
test_grammar_defaults以expected = type(g)()作为期望值(第 428-439 行),而grammar_group_args.py:11/19/31/45的 argparsedefault本身就取自同一个GrammarConfig默认值,两侧同源:把ConfigModules.h:377的terminate_without_stop_token默认从 false 改成 true(作用于所有 grammar 请求的服务级终止策略)后测试仍通过,只有bind_to字段名写错才会被发现。grammar_group_args.py也是整个 server_args 包中唯一使用default=<config>.<field>的文件(4 处),其余参数组均写字面量。此外两个新增 env(GRAMMAR_TERMINATE_WITHOUT_STOP_TOKEN/GRAMMAR_COMPILER_CACHE_BYTES)只覆盖了 CLI 路径,env_name绑定路径无用例
Python Static-First Checklist
- [P.B] 错误处理 — 禁止 bare except 或静默吞异常 → issue
update() / update_and_pop() 静默吞掉 response_format 解析异常,且解析逻辑重复
update()第 474-483 行与update_and_pop()第 506-513 行逐字重复同一段解析逻辑,且都写成try: value = parse_response_format(value) except (TypeError, ValueError): pass,随后仍setattr把未校验的原始 str/dict 写入类型为Optional[ResponseFormat]的字段(setattr不触发 validator)。原始错误信息(哪个键、哪段 JSON 非法)被完全丢弃,最终只剩_resolve_final_constraint一句泛化的「response_format must be a validated ResponseFormat object」。兄弟字段语义也不一致:非法json_schema/structural_tag由parse_json_grammar_value立即抛出带字段名的错误。 - [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
grammar smoke 在 OpenAI 通路上存在三条静默放行路径,且与 DashSc 通路语义相反
_compare_result在qr_info.get("grammar_constraint_only")为真时直接self._validate_grammar_constraint(actual_result); return(第 249-251 行),跳过usage、aux_info、finish_reason、choices 数量等全部确定性字段比对。而 `validate_gramma
Strengths
- MTP 投机验证的状态机处理严谨:
prepareSpecMask用provisional_accepts精确记账并在返回前matcher.rollback()还原已提交态;acceptCommittedLocked的rollback_commit闭包(GrammarLogitsProcessor.cc:397-414)以old_matcher_len/old_output_len保证多 token 批量提交失败时不残留任何可见前缀,回滚失败一律升级为markFinished()+ 串联原因,解析器状态不会静默漂移;RollsBackProvisionalAccepts/UpdateStatusRollsBackEntireRejectedBatch把这两条不变量固化成断言,且用「两次相同调用得到相同 cap」锁定 rollback 幂等而非依赖快照。 - 错误语义从「processor 内部静默或
RTP_LLM_CHECK崩溃」升级为按 batch 行归位:collectStreamSamplerError(NormalOutputDispatcher.cc:13-44)明确注释「processor errors 与 sampling success 都用 sampler-input 坐标,beam 改变 batch 时输出坐标会分叉」,并用set_first_error保序;MTP 侧经SpeculativeSamplerOutput.processor_errors→MtpBatchStreamProcessor.cc:551按 stream 下标对齐,单请求语法错误不再牵连同批其他请求。 - xgrammar「
fillBitmask返回 false 表示全放行、不填掩码」这一易错约定被两条路径一致处理:decode 用mask_required跳过 kernel,spec 先fill_n(kBitmaskAllowAll)再交给 xgrammar;grammar_vocab_size < model_vocab_size的尾部在 decode 与 spec(clearBitmaskTokenRange)两侧都补齐,未留下 padding token 被采样的漏洞。 - 后端错误分级设计到位:
compileWithErrorClassification(XGrammarBackend.cc:22-32)刻意区分bad_alloc→ResourceExhausted(retryable)、runtime_error→InvalidArgument(用户语法错误)、其他 →Unknown(retryable),并在头文件写明该契约;RtpGrammarMatcher::matcherCall统一把第三方异常收敛成ErrorResult,避免异常穿透采样热路径。 create()中 RAW 重建的注释(XGrammarBackend.cc:79-81)准确解释了动因(xgrammar 不序列化 token-id 查找表)与为何不会二次解码 BYTE_LEVEL/BYTE_FALLBACK,是一处有价值的边界知识沉淀。- 终态语义区分到位:明确解释为何终止后保持
TERMINATED而非FINISHED——min_new_tokens/ignore_eos延迟结束时必须继续只放行 EOS,而不是恢复无约束生成,这是同类实现常漏的坑。 SpecLogitsVerifyRunner只为真正持有 MTP 处理器的 stream 分配 compact 行,用logits_row_indices映射回全局 logits 行,避免[B*(P+1), ceil(V/32)]全量 bitmask;has2DCapacity要求列数严格相等以固定行 stride,使merged_base + compact_slot * words_per_stream的指针算术成立。- 热路径开销削减实在:
gatherSpecSamplerInput补上finished_mask.zero_()(MtpBatchStreamProcessor.cc:122),修复了allocateSamplerInputs用torch::empty分配布尔掩码后 MTP 路径从未初始化的隐患;同时去掉model_output.logits.clone()(第 151-153 行有注释说明可复用),省掉每步[B*(P+1), vocab]拷贝,并有测试断言复用同一data_ptr。 - 约束收敛的 Python 侧设计清晰:
GRAMMAR_FIELD_NAMES由Literal经get_args派生使字段清单单一来源;ResponseFormatPlan.compile()为纯函数、apply_to_config()负责落盘,消除旧实现在validate()内「边校验边改写」的副作用;RPC 边界改为只读断言validate_engine_ready()(model_rpc_client.py:119-122 有注释说明「assert without mutating」)。 GenerateStream::validateLogitsProcessorState()(GenerateStream.cc:953-967)引入 processor 与 stream 权威 token 历史的 parity 校验,把「processor 状态机与 token 历史脱节」这类隐性 bug 转成显式错误。- 掩码哨兵值迁移全仓一致:
RecommendationLogitsProcessorTest.cc13 处断言统一改用BaseLogitsProcessor::neg_inf,并从宽松的ASSERT_TRUE(x == -FLT_MAX || x == -INFINITY)收紧为ASSERT_FLOAT_EQ,Tree / MtpExecutor 测试同步。 - 跨语言与打包链路闭环:
exceptions.py:45-50的 6 个 grammar 错误码与ErrorCode.h:20-25取值命名逐一对齐;arch_select.bzl新增的th_grammar_tokenizer_info.so 在根BUILD、rtp_llm/BUILDdata 与rtp_llm/libs/BUILD的whl_package_libs三处均补齐;新增config_pickle_test.py为 pybind pickle 这类通常无人测的边界补上覆盖,含「伪造短布局必须被拒绝」的负向用例。 server_args_test.py:400-412在改动全局os.environ/sys.argv之前用addCleanup注册恢复并写明「setUp 自身抛异常时 tearDown 会被跳过」的理由,避免污染同进程内其他用例。PackedMaskLogitsCpuFallbackTest.cc与ExecOpsTest.cc用同一组期望值交叉验证 CPU 与 CUDA 两条实现路径,覆盖 compact row mapping、单行 identity、越界 row 跳过、非连续输入回写四类边界;3rdparty/xgrammar/xgrammar.BUILD与3rdparty/dlpack/BUILD均把公开头/内部头拆分并注明动机,可追溯性好。
| @@ -3,27 +3,47 @@ | |||
|
|
|||
| def init_grammar_group_args(parser, grammar_config): | |||
| grammar_group = parser.add_argument_group("Grammar Configuration") | |||
There was a problem hiding this comment.
[P1] 删除 --grammar_backend / GRAMMAR_BACKEND 破坏存量启动,并移除唯一的 grammar 关停开关
init_grammar_group_args 现只注册 --constrained_json_disable_any_whitespace / --grammar_terminate_without_stop_token / --grammar_num_workers / --grammar_compiler_cache_bytes 四项;--grammar_backend(env GRAMMAR_BACKEND,原默认 xgrammar,可取 none)与 GrammarConfig 同名成员一并删除(ConfigModules.h:374-383 已无该字段),全仓仅在 ConfigInit.cc:1355 注释残留「grammar_backend was removed」。后果两条:server_args.py 走严格 parse_args,存量脚本传该参数直接 SystemExit;只设环境变量的部署因 dest 不在映射中被静默忽略,GRAMMAR_BACKEND=none 的关停语义消失——这也是下一条启动...
建议: 保留一个已废弃的 --grammar_backend / GRAMMAR_BACKEND 一个版本:取 xgrammar 时打 WARNING 后忽略;取 none 时跳过 build_grammar_tokenizer_info(),让 XGrammarBackend::create 走已有的「tokenizer_info_json 为空返回 nullptr」路径,并由 createLogitsProcessors 对 grammar 请求返回明确的「功能未启用」错误。或提供等价的显式开关(如 --enable_grammar / --grammar_disable)。同时在 PR 描述与 docs/backend/ 给出「删除项 → 新增项」的参数迁移对照表,保证「grammar 出问题可运维回滚」的能力不丢失,下个版本再彻底移除。
There was a problem hiding this comment.
不需要关停开关, 之前dash sc引入的变量 可以删除。
| encoded_vocab, json.dumps(metadata, separators=(",", ":")) | ||
| ) | ||
|
|
||
| raise ValueError(f"Unsupported tokenizer type: {type(tokenizer)}") |
There was a problem hiding this comment.
[P1] grammar tokenizer 元信息在启动期无条件构建,不支持的 tokenizer 直接阻塞模型加载且无逃生口
build_grammar_tokenizer_info_json() 对非 fast / 非 tiktoken / 非 sentencepiece 的 tokenizer 抛 ValueError: Unsupported tokenizer type(第 158-159 行另有 stop_token_ids cannot be empty);base_model.py:319-328 升级为 RuntimeError;rtp_llm_op.py:30-32 在 start() 中无条件用其结果填充 grammar_config.tokenizer_info_json;C++ 侧 XGrammarBackend::create 对任何失败 RTP_LLM_LOG_ERROR(... aborting startup); throw;(第 96-102 行),而 LogitsProcessorFactory::init 在 Executor 构造函数中无条件调用它。因此自定义 tokenizer、或 eos 未配置且无单 tok...
建议: 把「元数据构建失败」与「服务可启动」解耦:新增服务级开关(如 GrammarConfig::require_tokenizer_info,或与上一条共用 --enable_grammar),默认保持当前 fail-fast 以尽早暴露配置问题;关闭时降级为 logging.warning + 返回空串,交由 C++ 已有的 backend-disabled 分支在请求期以明确错误码拒绝单个 grammar 请求。若要保留 fail-fast,至少把「tokenizer 类型不受支持」(降级为 backend 关闭 + WARNING)与「tokenizer 受支持但 metadata 非法」(中止启动)分级。函数级已有 test_build_tokenizer_info_rejects_unsupported_tokenizer,建议补的是覆盖 `rtp_llm_op...
There was a problem hiding this comment.
预期行为, 启动必须带grammar, 构建失败就fail
| @@ -136,7 +136,6 @@ message GenerateConfigPB { | |||
| google.protobuf.StringValue regex = 70; | |||
| google.protobuf.StringValue ebnf = 71; | |||
| google.protobuf.StringValue structural_tag = 72; | |||
There was a problem hiding this comment.
[P1] proto 删除 response_format 字段 73 未加 reserved,混版部署下 grammar 约束被静默丢弃
GenerateConfigPB 字段现止于 structural_tag = 72;(第 138 行),删除的 google.protobuf.StringValue response_format = 73; 未补 reserved 73; reserved "response_format";。同一 message 第 103-104 行已有 reserved 42; reserved "inter_request_id";,本文件另有 reserved 5、reserved 12、reserved 1 to 8 等多处,第 317 行注释更明确说明保留 field number 是为混部版本考虑,本次是唯一例外。同时 QueryConverter.cc 只保留 TRANS_OPTIONAL(json_schema/regex/ebnf/structural_tag),model_rpc_client.py 也删除对应序列化。滚动升级/PD 分离期旧 frontend 仍发 73 时被当作 unknown field 静默忽略,请...
建议: 在 structural_tag = 72; 之后补 reserved 73; 与 reserved "response_format";,与第 103-104 行保持一致。若灰度期确实可能收到旧客户端该字段,建议保留字段号仅作兼容读取,QueryConverter 检测到时返回 ERROR_GENERATE_CONFIG_FORMAT 并提示「请升级 frontend 或改用 json_schema / regex / ebnf / structural_tag」,实现 fail-fast 而非静默降级。并在发布说明中写明 response_format 自本版起由 Python prepare_for_engine 投影为 typed 字段、不再上线传输,便于混部窗口定位约束失效。
There was a problem hiding this comment.
dash sc带入的, 无需传递
| ) -> "ResponseFormatPlan": | ||
| final_constraint = _resolve_final_constraint(config) | ||
|
|
||
| if config.in_think_mode: |
There was a problem hiding this comment.
[P1] think 模式改为硬依赖 grammar 约束,thinking + beam search / num_return_sequences>1 由可用变报错
ResponseFormatPlan.compile 第 71-78 行:只要 config.in_think_mode 为真,且 has_num_beams() 或 num_return_sequences > 1,一律抛 ERROR_INPUT_FORMAT_ERROR(理由为「thinking mode ... uses grammar-constrained decoding」),即使该请求完全没有 response_format / grammar 字段;LogitsProcessorFactory.cc:101-105 在引擎侧做同样拒绝。但被替换掉的 ThinkModeLogitsProcessor 本身支持多序列——ThinkModeLogitsProcessor.cc 实现 updateMultiSeqStatus、.h:77 提供 insert(others, num) 做 beam 展开,其单测以 batch 4 运行。即服务端 --think_mode 1 的集群上,任何带 beam 或 `num_ret...
建议: 请在 PR 描述与发布说明中把该组合明确标注为不兼容变更,并二选一:(a) 仅当请求实际携带最终约束(final_constraint is not None)时才拒绝 beam/多序列,纯 thinking 请求保留 ThinkModeLogitsProcessor 路径(让 _reasoning_envelope 在无最终约束时不安装引擎约束);(b) 若确定统一走 grammar,则提供过渡开关(如 THINK_MODE_ALLOW_MULTI_SEQ,开启时退回旧处理器并打 WARNING),并补「thinking + beam 的替代方案」文档。无论哪种,请补一条「thinking + num_return_sequences>1」的用例固化期望行为(明确错误码与文案,或明确降级)。
There was a problem hiding this comment.
预期行为, think完全通过grammar约束, 暂不支持 beam search / num_return_sequences
| GrammarKeyCpp grammar_key = std::move(grammar_key_result.value()); | ||
|
|
||
| // Thinking constraints reach the execution layer as normalized grammar. | ||
| // Thinking-only configs are not supported here; do not add a legacy processor fallback. |
There was a problem hiding this comment.
[P1] in_think_mode / max_thinking_tokens 在引擎侧变为静默 no-op,ThinkModeLogitsProcessor 生产不可达
createLogitsProcessors 删除了 ThinkModeLogitsProcessor::fromGenerateInput 调用,注释写明「Thinking-only configs are not supported here; do not add a legacy processor fallback」。全仓检索确认 in_think_mode / max_thinking_tokens 仍完整走 model_rpc_service.proto:93-94 → QueryConverter.cc → GenerateConfig(并 JSONIZE、打进 debugString),但 C++ 侧唯一消费点已是 LogitsProcessorFactory.cc:113-116 一条仅在 grammar_key 非空时执行的 debug 日志。只有走 Python ResponseFormatPlan.compile 的请求会被折叠成 structural_tag;直接以 in_think_mode=true 且不...
建议: 二选一并在 PR description 标注为不兼容变更:(1) 若 think 模式统一走 grammar,则在 createLogitsProcessors 显式 fail-fast —— 当 in_think_mode && max_thinking_tokens != 0 && grammar_key.empty() 时返回 ErrorInfo(INVALID_PARAMS, ...) 提示必须先经 response_format 编译成 structural_tag,并同批删除 ThinkModeLogitsProcessor.{h,cc}、其单测与 BUILD 条目,避免为不可达代码提供虚假信心;同时评估 proto 中这两个字段是否该标注为 deprecated。(2) 若需兜底,则在 grammar_key 为空或 `backend == nul...
There was a problem hiding this comment.
预期行为, 完全通过structual tag 控制max_thinking_tokens
| auto logits_cpu = sampler_inputs.logits.cpu(); | ||
| auto data = logits_cpu.data_ptr<float>(); | ||
| ASSERT_TRUE(data[30] == -std::numeric_limits<float>::max() || data[30] == -INFINITY); | ||
| ASSERT_FLOAT_EQ(BaseLogitsProcessor::neg_inf, data[30]); |
There was a problem hiding this comment.
[P2] 大量与 grammar 功能无关的格式化重排混入本次改动
多个分片在 diff 层独立观察到同一模式:RecommendationLogitsProcessorTest.cc 的真实语义改动只有把 ASSERT_TRUE(... == -FLT_MAX || ... == -INFINITY) 收紧为 ASSERT_FLOAT_EQ(BaseLogitsProcessor::neg_inf, ...)(13 处,如第 79、141、1057 行),其余为 clang-format 参数对齐重排;case_runner.py 的真实逻辑改动只有 grammar_constraint_only 透传两行,同 diff 夹带近 200 行 isort/black 重排;openai_comparer.py、dash_grpc_comparer.py、dash_sc/test/codec_test.py、model_rpc_client_test.py 同样模式。在 135 个改动文件的 PR 中,掩码哨兵值由 -INFINITY 统一为 -FLT_MAX 这类行为变更极易被漏看。
建议: 把纯格式化改动拆到独立的 format-only commit(或独立 PR),使本 PR 的 diff 只保留 grammar 相关语义变更;若因 lint gate 必须同批提交,请在 PR 描述中逐文件标注「仅格式化」与「含逻辑变更」,以保住本次评审与后续 git blame / bisect 的可用性。同时在 PR 描述中显式说明 neg_inf 哨兵值统一的动因与影响面,并补齐当前缺失的不兼容变更清单与迁移说明(见各条 P1)。
Checklist: [6.1] Commit 原子、message 与行为匹配;[6.1] Mega-PR 已拆分为独立变更;[6.1] 逻辑变更未混入无关格式化
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1006
Status: BLOCKING
Summary: P0/0 · P1/7 · P2/23 · P3/7
Reviewed: commit 0bc86a066107 · 2026-08-06 21:24 UTC+8
Blocking Issues
P1
- fp32 掩蔽哨兵值在生产 kernel 与新断言间分叉,cuda_mask_logits_op_test 必然失败 @
rtp_llm/models_py/bindings/cuda/ops/tests/CudaMaskLogitsOpTest.cc:56- 建议:先确定「被屏蔽 logits」的唯一真相值。若目标是与
BaseLogitsProcessor::neg_inf对齐,则把mask_logitskernel 也改用MaskedLogitValue<T>(),并把ExecOpsTest.cc:360的std::isinf改为对-FLT_MAX的精确断言;若mask_logits有意保留-INFINITY,则回退本用例期望值,并把MaskedLogitValue上方「Match BaseLogitsProcessor::neg_inf」的注释限定为仅适用于 packed 路径。无论选哪种,都应把该常量收敛到单一来源,并补一条跨mask_logits/packed_mask_logits/ CPU fallback 三条路径的一致性断言——注意BaseLogitsProcessor::maskLogits本身也分叉:CUDA 走cudaMaskLogits(-INFINITY),非 CUDA 走masked_fill_(neg_inf)(-FLT_MAX)。
- 建议:先确定「被屏蔽 logits」的唯一真相值。若目标是与
- proto 删除 response_format 字段 73 未加 reserved,混版滚动升级静默丢弃结构化约束 @
rtp_llm/cpp/model_rpc/proto/model_rpc_service.proto:138- 建议:在
structural_tag = 72;之后补reserved 73;与reserved "response_format";,与本文件 103-104 行保持一致。若需覆盖混部窗口,建议 engine 侧对仍携带该 unknown field 的请求以ERROR_GENERATE_CONFIG_FORMAT显式拒绝并打印告警,而不是静默降级;并在 PR description 或 proto 注释中写明 frontend/engine 的升级顺序要求(可参考同文件 KV-cache 协议注释的写法)。
- 建议:在
- think 模式无条件改为 grammar 约束解码,thinking + beam search / num_return_sequences>1 由可用变 400 @
rtp_llm/config/response_format_compiler.py:71- 建议:区分「用户显式要求 grammar」与「think 模式内部使用 reasoning envelope」:
final_constraint is None时 think 模式应保持旧行为放行 beam/n>1(例如该组合下不下发 envelope,或按 sequence 各自建 matcher),仅final_constraint is not None时才拒绝。若确认限制不可避免,请在docs/backend/logits_processor.md与 PR description 中声明为 breaking change,并补一条覆盖THINK_MODE=1 + num_return_sequences>1的用例明确迁移方式。
- 建议:区分「用户显式要求 grammar」与「think 模式内部使用 reasoning envelope」:
- 删除 --grammar_backend / GRAMMAR_BACKEND,存量启动脚本硬失败且移除唯一的 grammar 关停开关 @
rtp_llm/server/server_args/grammar_group_args.py:6- 建议:保留一个已废弃的 no-op
--grammar_backend(接受取值、打 deprecation WARNING 后忽略)以免存量脚本启动失败,同时新增显式--grammar_enable/GRAMMAR_ENABLE(或恢复grammar_backend=none语义)作为运维回滚手段:关闭时跳过 grammar 元数据构建,并让 grammar 请求走LogitsProcessorFactory.cc:107-111既有的「backend is disabled」分支快速失败。请在 release note 中列出该 CLI 参数、环境变量与override_stop_tokens的移除及迁移方式,并补一条断言旧参数行为的单测。
- 建议:保留一个已废弃的 no-op
- grammar tokenizer 元信息成为所有语言模型的启动硬依赖,不支持的 tokenizer 直接阻塞模型加载且无逃生口 @
rtp_llm/config/grammar_tokenizer_info.py:195- 建议:把「启动即失败」降级为「按请求失败」:新增(或复用上一条建议的)服务级开关,开关关闭或元数据构建失败时记 WARNING 并返回空串,让
XGrammarBackend::create走tokenizer_info_json.empty()分支返回nullptr(:67-70 已有该分支),仅在实际发起 grammar 请求时返回明确的INVALID_PARAMS。同时把create对反序列化失败的 rethrow 改为可配置行为。若坚持 fail-fast,请在 PR description 中列出受影响的 tokenizer 类型清单、非 CUDA 平台结论与规避手段。
- 建议:把「启动即失败」降级为「按请求失败」:新增(或复用上一条建议的)服务级开关,开关关闭或元数据构建失败时记 WARNING 并返回空串,让
- in_think_mode / max_thinking_tokens 仍在 wire 上,但引擎侧已无任何执行者,退化为静默 no-op @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:99- 建议:明确该 RPC 入参的引擎侧契约并二选一:1)若 think 预算此后只由 structural_tag 承载,则在
QueryConverter.cc对「in_think_mode=true且四个 typed grammar 字段均为空」的请求返回显式错误或至少打一条RTP_LLM_INTERVAL_LOGWARN,并在 proto 注释中标注这两个字段已由 frontend 编译消费、engine 不再执行;2)若需继续支持裸 gRPC 客户端,则恢复一条 think-only 的降级构造路径。同时在 PR description 中写明 frontend 必须先于 engine 升级。
- 建议:明确该 RPC 入参的引擎侧契约并二选一:1)若 think 预算此后只由 structural_tag 承载,则在
- MTP 新增启动期 abort 与请求期硬拒绝,把既有可运行组合打死且无过渡开关 @
rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:230- 建议:fail-fast 方向正确,但需补齐迁移与运维面:1)在 release note 中列出受影响组合(MTP +
TREE_DECODE_CONFIG、combo_token_size>0的推荐类请求、num_return_sequences>1);2)把启动期 abort 改为「启动告警 + 该能力不可用」,或提供默认开启、可临时关闭的校验开关,避免只能回滚镜像;3)为请求级拒绝路径补一条可聚合的RTP_LLM_INTERVAL_LOGWARN 或 metric,便于上线后快速发现被拒流量。
- 建议:fail-fast 方向正确,但需补齐迁移与运维面:1)在 release note 中列出受影响组合(MTP +
Non-blocking Suggestions
P2
- grammar 编译失败的状态码在 factory 边界被压平为 INVALID_PARAMS,丢失可重试语义 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:120- 建议:在 factory 处按
matcher_or.status().code()映射:kResourceExhausted→ErrorCode::MALLOC_FAILED(或其他可重试码),kInvalidArgument→INVALID_PARAMS,其余归EXECUTION_EXCEPTION;并补一条单测断言资源类失败不会被映射成INVALID_PARAMS,把分类语义固化为回归。
- 建议:在 factory 处按
- 新增 6 个 GRAMMAR_ 错误码未指定 ExceptionCategory,对外一律退化为 internal* @
rtp_llm/config/exceptions.py:45- 建议:按语义逐一补齐 category:用户输入/配置类(
GRAMMAR_VOCAB_EXCEEDS_MODEL_VOCAB、GRAMMAR_EOS_OUT_OF_VOCAB)→BAD_REQUEST;模型输出违背语法类(GRAMMAR_PARSER_REJECTED_TOKEN、GRAMMAR_NON_EOS_AFTER_TERMINAL)→INVALID_OUTPUT;确属内部不变量破坏的(GRAMMAR_BITMASK_BUFFER_TOO_SMALL、GRAMMAR_VERIFY_EXCEPTION)保留 INTERNAL 并加注释说明。同时在servicer_test.py补一条按 category 断言 Dash 错误码映射的用例,避免后续新增码再次漏标。
- 建议:按语义逐一补齐 category:用户输入/配置类(
- pin_memory() 无平台保护,非 CUDA 构建下 grammar 解码以无关错误失败,且 CPU fallback 测试被 GPU 执行机锁死 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:236- 建议:把 pinning 收敛到
#if USING_CUDA分支内(或按logits.is_cuda()决定是否使用 pinned buffer),非 CUDA 路径用普通 CPU tensor。若该平台确实不支持结构化输出,应在XGrammarBackend::create阶段 fail-fast 并给出明确的平台不支持提示,而不是让每个请求在首个 decode step 以误导性错误失败。同时让packed_mask_logits_cpu_fallback_test真正脱离 CUDA deps 与 GPU 执行属性(或在 ROCm/ARM 门禁中新增等价 target);若受构建约束暂时做不到,请修正该注释为「在 GPU 主机上执行 CPU 分支逻辑」。
- 建议:把 pinning 收敛到
- 受约束 decode 每步触发一次全流同步的 H2D 拷贝,抵消 pinned buffer 收益 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:268- 建议:若同步是有意为之(避免下一步复写 pinned buffer 时拷贝未完成),请在该行加注释说明,并考虑改为双缓冲 +
non_blocking=true+torch::Event记录,把同步从「每 token 全流阻塞」降级为「仅在缓冲复用前等待」;若非有意,直接改为non_blocking=true并配合双缓冲。建议同时补一条 decode 吞吐对比数据(开/关 grammar),量化该路径对整 batch decode 循环的影响。
- 建议:若同步是有意为之(避免下一步复写 pinned buffer 时拷贝未完成),请在该行加注释说明,并考虑改为双缓冲 +
- xgrammar 依赖钉在上游未合并特性分支 commit,且绕开仓库统一依赖入口 @
3rdparty/xgrammar/repositories.bzl:5- 建议:把
xgrammar声明合并进仓库统一入口(git_deps()或带sha256的http_archive),移除WORKSPACE中的额外 load;并把 commit 固定到上游 tag 或已合并进主干的提交,必要时改为「上游稳定 tag + 本仓 patch」的既有模式。若必须使用特性分支产物,请先在内部镜像固化归档并补shallow_since,同时在 PR description 写明升级/回滚步骤——当前一旦上游分支被删除或 force-push,该 commit 不可 fetch,所有干净构建与离线 CI 会不可复现地失败。
- 建议:把
- //rtp_llm:ops 新增数据依赖,放大了全仓 Python 目标的构建面 @
rtp_llm/BUILD:196- 建议:移除
//rtp_llm:ops上的data,仅在真正需要加载libth_grammar_tokenizer_info.so的目标上声明(rtp_llm/config/test/BUILD已是正确示例),运行期分发继续依赖arch_select.copy_all_so与rtp_llm/libs:whl_package_libs。若确实希望import rtp_llm.ops后即可直接调用serialize_grammar_tokenizer_info,请在 BUILD 中注释说明该权衡,并评估对 CPU 门禁构建时长的影响。
- 建议:移除
- MTP verify 去掉 logits clone 后原地改写模型输出缓冲,缺少 return_logits/softmax 的等价保护 @
rtp_llm/cpp/normal_engine/speculative/MtpBatchStreamProcessor.cc:153- 建议:保留复用优化但把隐式不变量显式化:在
gatherSpecSamplerInput中加RTP_LLM_CHECK_WITH_INFO(!stream->returnLogits() && !stream->calculateSoftmaxProbs(), ...),或与普通路径一致地在这两种情况下 clone。同时把注释改为指明具体消费点(updateDecodePostDraftModelInput只读all_hidden_states)并说明与NormalSamplerInputGatherer的差异原因,便于后续维护者验证。
- 建议:保留复用优化但把隐式不变量显式化:在
- 反序列化后用 VocabType::RAW 重建 TokenizerInfo,等价性无断言且无非 RAW 词表回归 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:82- 建议:补一个经
create()的等价性回归:对 BYTE_LEVEL(含Ġ前缀)与 BYTE_FALLBACK(含<0x0A>等 byte token)两类 fixture,断言GetDecodedVocab()、GetSpecialTokenIds()、GetStopTokenIds()、vocabSize()与逐 tokenacceptToken/ 首个fillBitmask结果同直接用原 vocab_type 构造的对照实现一致。若确认上游DeserializeJSON确实缺失 lookup,请在注释中给出上游 issue/commit 依据——真实 byte-level 模型上一旦不等价,表现是静默允许/禁止错误 token,而现有测试恒为绿。
- 建议:补一个经
- tokenizer 家族探测函数在单测中被整体 mock,决定 vocab_type 的分派逻辑无真实覆盖 @
rtp_llm/config/test/grammar_tokenizer_info_test.py:71- 建议:为三个判定函数补直接单测:tiktoken 分支覆盖 tiktoken Encoding、
vocab_files_names["vocab_file"]含 tiktoken、存在mergeable_ranks三条正例各一及对应负例;sentencepiece 分支覆盖sp_model/tokenizer.sp_model/tok三处候选与 API 不全的负例。构建测试只 mockserialize_grammar_tokenizer_info这一原生扩展边界即可(该 target 已通过data = ["//:th_grammar_tokenizer_info"]具备加载真实.so的条件,可参考同文件test_native_serializer_accepts_raw_bytes_vocab)。
- 建议:为三个判定函数补直接单测:tiktoken 分支覆盖 tiktoken Encoding、
- smoke 的 grammar 校验在 OpenAI 通路上可能零校验静默通过,且与 DashSc 通路语义相反 @
rtp_llm/test/smoke/openai_comparer.py:230- 建议:把 OpenAI 路径对齐 DashSc 的 fail-loud 语义:
response_format缺失时抛SmokeException(QueryStatus.VALID_FAILED, ...);is_stream为真时同样显式失败(「流式 grammar 校验尚未支持」)或在smoke_test层面禁止grammar_constraint_only与stream: true组合,或对流式响应先拼接增量再复用同一validate_constraint。更好的做法是把「取 grammar 字段 + 缺失即报错」抽成grammar_constraint_validator.py中的公共入口供两个 comparer 共用。
- 建议:把 OpenAI 路径对齐 DashSc 的 fail-loud 语义:
- 结构化标签校验器对无 begin/end 的 tag 与缺失 schema 静默放行,削弱约束校验强度 @
rtp_llm/test/smoke/grammar_constraint_validator.py:185- 建议:把两处静默分支改为显式抛出
ValueError,分别说明「validator 尚不支持该 tag 形态(缺少 begin/end 或使用 token 型 end)」与「format.json_schema 缺失」,与同文件其它不支持分支(inner_type未知见 :195-198、fmt_type未知见 :243-246)的 fail-loud 风格保持一致,避免 smoke 在无声弱化校验的情况下通过。
- 建议:把两处静默分支改为显式抛出
- xgrammar_backend_cpp_test 缺少执行机声明,且 grammar 包的 Tier 边界仅有注释未落地 @
rtp_llm/cpp/engine_base/grammar/test/BUILD:12- 建议:按同 PR 已建立的约定补齐
env = {"TEST_USING_DEVICE": "CUDA"}与exec_properties = {'gpu': 'H20'};若确认该 target 在无 GPU 主机上也能加载 libtorch,请在 BUILD 中写明理由,避免与相邻文件注释冲突。同时删除test/BUILD:2未使用的torch_depsload(或在 deps 中显式加上以明示依赖),并把grammar_xgrammar的 Tier 约束落到visibility白名单上,使注释与构建图一致;若目标是让它成为真正的 CPU-only 测试,建议评估从grammar_xgrammar移除该包源文件并未使用的torch_deps()。
- 建议:按同 PR 已建立的约定补齐
- MTP grammar smoke fixture 命名与实际 suite 接线不一致,thinking 关闭 + MTP + grammar 组合零覆盖 @
rtp_llm/test/smoke/data/model/qwen35/q_r_mtp_grammar.json:5- 建议:二选一并同步注释:把该 fixture 重命名为
q_r_grammar_concurrent_no_mtp.json并修正两个_comment中的配对描述;或给qwen35_grammar_concurrent_no_mtp补上 MTP 参数使其名副其实(此时应另加一条 no-MTP 基线)。无论哪种,都请让文件名、_comment与 suite 名称三者一致,避免后续排障按错误前提定位。
- 建议:二选一并同步注释:把该 fixture 重命名为
- logits processor 文档未同步,且 ThinkModeLogitsProcessor 与 max_batch_size 形参成为死代码 @
docs/backend/logits_processor.md:8- 建议:在同一 PR 内完成收尾:1)删除或改写 ThinkModeLogitsProcessor 条目,说明 think budget 现由 structural_tag 的
any_text.max_tokens承载,并同步修正dash_sc/think.py:167;2)新增结构化输出章节,覆盖四字段互斥(LogitsProcessorFactory.cc:39-46)、不支持 beam search 与num_return_sequences>1(:101-105)、tokenizer_info_json为空即静默关闭(XGrammarBackend.cc:67-70)、bitmask 位语义(1=allow)、grammar vocab 大于模型 vocab 的报错语义,以及 SpecLogitsVerifyRunner 的 cap 定义、fail-closed 约定与 MTP 兼容性矩阵;3)明确ThinkModeLogitsProcessor去向(删除类与 BUILD 条目,或在头文件注明仅供历史用例保留),并移除已失效的max_batch_size形参及其调用点。
- 建议:在同一 PR 内完成收尾:1)删除或改写 ThinkModeLogitsProcessor 条目,说明 think budget 现由 structural_tag 的
- 服务级 terminate_without_stop_token 与同名请求级字段并存,请求级设置解析即被静默丢弃 @
rtp_llm/cpp/engine_base/stream/GenerateConfig.h:57- 建议:二选一:1)删除
GenerateConfig里的grammar_terminate_without_stop_token,让未知字段按现有 raw-request 规则处理;2)若必须保留以兼容旧请求体,则在该字段为 true 且与服务级策略不一致时打一条限流后的 WARN,明确「请求级设置被服务级策略覆盖」,并在文档中写明该字段已降级为 no-op、这是服务级策略而非请求级参数。
- 建议:二选一:1)删除
- BitmaskUtils 的同字/末字对齐分支与越界边界缺少测试覆盖 @
rtp_llm/cpp/models/logits_processor/BitmaskUtils.cc:63- 建议:在
GrammarLogitsProcessorTest.cc参数化补齐clearBitmaskTokenRange的同字、32 对齐起点与终点、空区间(end <= begin)、越界起点(begin >= words*32)等用例;为bitmaskAllowsToken补充负 token id 与token_id/32 == words的用例;为forceTokenInBitmask补充越界触发断言的 death test,把这些分支固化为回归。
- 建议:在
- MultiSeqLogitsProcessor 写 EOS 掩码缺少 vocab 边界校验,存在堆越界写风险 @
rtp_llm/cpp/models/logits_processor/MultiSeqLogitsProcessor.cc:39- 建议:在
process内写掩码前加一次边界校验,越界时返回ErrorInfo(ErrorCode::GRAMMAR_EOS_OUT_OF_VOCAB, ...)或既有的OUT_OF_VOCAB_RANGE(现在返回类型已支持),与GrammarLogitsProcessor::forceEos保持一致的错误语义;并补一条eos_token_id >= vocab_size的单测固化该行为。
- 建议:在
- finalize 之后 update() 仍可改写 grammar 字段,不会重新编译且报错信息误导 @
rtp_llm/config/generate_config.py:487- 建议:在 finalize 完成后加入状态标记(可复用已有的
_reasoning_envelope_applied思路):update()/setattr命中 grammar 字段时,要么重置该标记并要求重新 finalize,要么抛出指向真实原因的错误(例如「grammar 字段已在请求入口固化,不可在 enrichment 阶段修改」);并为 finalize 后再设 grammar 字段补一条断言用例。
- 建议:在 finalize 完成后加入状态标记(可复用已有的
- response_format 入参收紧为 extra="forbid",旧客户端多传字段由静默忽略变成 400 @
rtp_llm/config/response_format.py:37- 建议:对嵌套的
ResponseFormatJSONSchema改用extra="ignore"(保留顶层ResponseFormat的extra="forbid"以拦截拼错字段),对 OpenAI 侧字段演进保持前向兼容;并在 PR description /docs/中列出被收紧的请求形状与等价写法,同时补充非法字符串与多余字段两类用例,固化期望的错误码与错误信息。
- 建议:对嵌套的
- legacy structural_tag 两条入口受理结果不一致,json_schema 布尔与空 dict 边界未校验 @
rtp_llm/config/grammar_constraint.py:64- 建议:统一 legacy 形态的处理:要么在
normalize_grammar_value阶段就把structures/triggers归一化为format结构使三条路径一致,要么两处都显式拒绝并给出「legacy structural_tag 已废弃,请改用 format」的迁移提示,并补两条用例分别覆盖 think 与非 think 下同一 legacy 请求体。同时在parse_json_grammar_value或validate_not_empty中显式处理布尔与空 schema:json_schema is False以ERROR_INPUT_FORMAT_ERROR拒绝并说明「schema false 无可生成 token」;json_schema == {}/True明确按「无约束」处理(返回 None)或同样拒绝,二选一后在generate_config_test.py补断言。
- 建议:统一 legacy 形态的处理:要么在
- 单一 grammar 约束规则存在两份重复实现,存在行为漂移风险 @
rtp_llm/config/response_format_compiler.py:138- 建议:让
_resolve_final_constraint在处理完response_format冲突分支后直接复用GrammarConstraint.resolve_from_config(config)并对返回值调normalized(),删除重复的validate_not_empty循环、len > 1判断与重复文案,使「每请求仅一个 grammar 约束」这条规则只有一处实现。
- 建议:让
- GrammarConfig::setstate 靠运行时类型嗅探分派 4 种布局,含两条生产不可达死分支 @
rtp_llm/cpp/pybind/ConfigInit.cc:1347- 建议:只保留 legacy 布局与当前布局两条分支,删除两条仅存在于开发中间态的兼容分支,并同步移除
config_pickle_test.py中对应的两个用例(保留当前格式往返、legacy 五元组、伪造短元组三条)。若确需长期跨版本兼容,改为在 state 首位放显式 schema version 整数按版本号分派,避免类型嗅探。
- 建议:只保留 legacy 布局与当前布局两条分支,删除两条仅存在于开发中间态的兼容分支,并同步移除
- grammar 编译位于请求准入同步路径,成功路径仅 DEBUG 日志且无耗时指标 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:46- 建议:补充可观测性与保护:1)对超过阈值(例如 100ms)的编译输出
RTP_LLM_INTERVAL_LOG(..., WARN, ...),或把elapsed_ms上报为 kmonitor 指标并区分 cache hit/miss;2)记录compiler_cache_bytes使用量或命中率,便于容量调参;3)评估是否需要对key_string长度/schema 规模设上限,避免单个超大 schema 拖住准入线程。
- 建议:补充可观测性与保护:1)对超过阈值(例如 100ms)的编译输出
P3
- ASSERT_FLOAT_EQ 无法区分 -FLT_MAX 与 -INFINITY,掩蔽哨兵断言被弱化 @
rtp_llm/cpp/models/logits_processor/test/TreeLogitsProcessorTest.cc:238- 建议:对哨兵值这类需要精确匹配的常量改用位级或精确比较(比较
std::bit_cast<uint32_t>后的位模式,或沿用同目录RecommendationLogitsProcessorTest.cc:400中data1[j] == BaseLogitsProcessor::neg_inf的精确==写法),使掩蔽值一旦回退到-INFINITY就能被测试捕获。
- 建议:对哨兵值这类需要精确匹配的常量改用位级或精确比较(比较
- GrammarLogitsProcessorTest 辅助函数构造失败时返回空 bundle,会以空指针崩溃代替干净失败 @
rtp_llm/cpp/models/logits_processor/test/GrammarLogitsProcessorTest.cc:58- 建议:把辅助函数内的
EXPECT_TRUE改为通过出参返回并用ASSERT_TRUE,或在operator->()中先做非空检查;最简做法是让每个用例在makeProcessor(...)之后立即ASSERT_TRUE(bundle.proc)。
- 建议:把辅助函数内的
- 新增 golden 数据的说明注释引用了不存在的源文件与不存在的用例文件 @
rtp_llm/test/smoke/data/model/qwen35/q_r_mtp_grammar_reasoning.json:5- 建议:把
GrammarManager.cc:473-474/ReasonerGrammarObject更新为本 PR 中实际承载该状态复位逻辑的文件与符号名(建议只写符号名,避免行号腐化);删除对q_r_mtp_grammar_reasoning_concurrent.json的引用,或在本 PR 中一并补齐该用例并接入smoke_h20_grammar_heavy。
- 建议:把
- GrammarConfig 新增字段未同步到 .pyi 类型存根 @
rtp_llm/ops/libth_transformer_config.pyi:544- 建议:重新生成
libth_transformer_config.pyi并提交(或手工补terminate_without_stop_token: bool),使存根与 pybind 绑定、pickle state 布局三者一致。若希望后续不再漂移,建议加一个「重新生成 stub 后 diff 为空」的校验用例。
- 建议:重新生成
- model_rpc_client 新增的两条错误分支缺负向测试,且内部契约违约上报为用户输入错误 @
rtp_llm/cpp/model_rpc/model_rpc_client.py:74- 建议:补两个带
match断言的用例:1)给 config 赋含不可序列化对象的structural_tag,断言报错带字段名;2)构造response_format未 finalize 的 config,断言trans_input抛出「must be prepared before engine serialization」。同时建议把validate_engine_ready的失败改为可区分的内部错误类型,或在抛出前logging.error打印request_id/trace_id与失败字段名(此时input_pb.request_info已填好,成本极低)。
- 建议:补两个带
- processor 与 stream 长度不一致这一内部不变量被归入 UNKNOWN_ERROR @
rtp_llm/cpp/engine_base/stream/GenerateStream.cc:964- 建议:为该不变量分配专用错误码(或复用本 PR 新增的
GRAMMAR_VERIFY_EXCEPTION同层语义的内部码),并在返回前打一条带stream_id/processor_index的RTP_LLM_LOG_ERROR,使该路径在日志与指标上可归因。
- 建议:为该不变量分配专用错误码(或复用本 PR 新增的
- 大量与 grammar 功能无关的格式化重排混入本次改动 @
rtp_llm/cpp/models/logits_processor/test/RecommendationLogitsProcessorTest.cc:1- 建议:把纯格式化重排拆到独立的先导 commit 或独立 PR,使功能 commit 的 diff 只包含行为变更;若已难以拆分,请在 PR description 中显式列出「仅格式化」的文件清单便于 reviewer 跳过。后续建议把 clang-format 作为 pre-commit 全量执行一次并单独提交,避免每个功能 PR 都夹带格式噪声。
Checklist Violations (20 fail / 58 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
//rtp_llm:ops 新增数据依赖,放大了全仓 Python 目标的构建面
py_library(name = "ops")新增data = ["//:th_grammar_tokenizer_info"](:195-197)。//rtp_llm:ops被大量 py_library / py_test 依赖,加入该 data 后,任何间接依赖ops的目标(包括纯 CPU、与 grammar 无关的 Python 单测)都会触发构建//:th_grammar_tokenizer_info→grammar_tokenizer_info_python→xgrammar_tokenizer_info→@xgrammar//:xgrammar,即编译整个 xgrammar。仓内既有约定并非如此:.so由各 py_test 自行声明data或经rtp_llm/libs:whl_package_libs打包引入;本 PR 自己的rtp_llm/config/test/BUILD:22-24已是 per-test 写法,与此处重复。另ops/__init__.py:197已是函数内懒 - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
GrammarConfig 新增字段未同步到 .pyi 类型存根
ConfigInit.cc:1325新增def_readwrite("terminate_without_stop_token", ...),grammar_group_args.py:17通过bind_to写入,server_args_test.py与config_pickle_test.py都直接读写该属性。但本 PR 已同步修改的libth_transformer_config.pyi:544-556中GrammarConfig只声明了compiler_cache_bytes/constrained_json_disable_any_whitespace/num_workers/tokenizer_info_json,独缺新字段(也缺__repr__),说明存根是手改而漏项。仓库无 mypy 门禁,CI 不会拦住,只会让 IDE 与类型检查对合法访问报假错。 - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
xgrammar_backend_cpp_test 缺少执行机声明,且 grammar 包的 Tier 边界仅有注释未落地
xgrammar_backend_cpp_test未声明env与exec_properties。它依赖:grammar_cpp_lib→:grammar_xgrammar,后者 deps 含torch_deps()(grammar/BUILD:54),在 CUDA 配置下会链接 CUDA 版 libtorch。本 PR 新增的两个同类「纯 CPU 逻辑但链接 libtorch」target 都显式声明了执行机:speculative/test/BUILD:22-26甚至专门注释「CPU-only logic but normal_engine links libtorch_cuda; H20 host needed for loader」,core/test/BUILD:64同样如此。仅本 target 例外,在无 GPU 执行机上存在动态库加载失败风险。另test/BUILD:2载入torch_deps但从未使用;grammar/BUILD:35声明「只有 Tier 0/Tier 1 可依赖」,但 :4 是 - [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue
processor 与 stream 长度不一致这一内部不变量被归入 UNKNOWN_ERROR
validateLogitsProcessorState(:953-971)在processor->committedOutputLen()与outputTokenLen()不一致时返回ErrorInfo(ErrorCode::UNKNOWN_ERROR, ...)。该场景是引擎内部状态机破损(processor 与 stream 提交长度失去 parity),与真正「原因未知」的运行时错误混在同一个码上;UNKNOWN_ERROR = 514在 Python 侧同样未指定 category(落入 INTERNAL),因此监控无法把这类可定位的内部不变量破坏与其它未知错误区分开,也无法据此配置针对性告警。 - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
xgrammar 依赖钉在上游未合并特性分支 commit,且绕开仓库统一依赖入口
new_git_repository(name="xgrammar", remote=<上游公开仓库 mlc-ai/xgrammar>, commit="60fc70ee..."),上一行注释标明该 commit 来自feat/anytext-budget特性分支(structural-tag max_tokens/max_chars enforcement),不是 tag 也不是已合并进主干的提交,且无sha256、shallow_since或内部镜像回退。本仓库其他 git 型三方依赖统一声明在git_deps()中(WORKSPACE:27-29),本 PR 另建3rdparty/xgrammar/repositories.bzl并在WORKSPACE:31-33追加独立 load + 调用,形成第二套并行依赖声明机制。关键耦合:think 预算依赖_reasoning_envelope生成的any_text.max_tokens,该语义正来自这条未合并分支。 - [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue
finalize 之后 update() 仍可改写 grammar 字段,不会重新编译且报错信息误导
add_thinking_params的 docstring(:581-586)把「grammar 字段必须在本次调用前完备,后续 enrichment 只能改 grammar 无关字段」定为契约,但没有运行时保护:update()(:487-489)与update_and_pop()(:510-513)仍会通过_parse_update_value(:462-475)写入json_schema/structural_tag/response_format。think 模式下structural_tag已被 envelope 占用,此时再设json_schema会在model_rpc_client.py:122的validate_engine_ready报「only one grammar constraint … may be set per request」,而调用方主观上只设了一个;非 think 场景补一个裸structural_tag则报「must be normalized before e - [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue
processor 与 stream 长度不一致这一内部不变量被归入 UNKNOWN_ERROR
validateLogitsProcessorState(:953-971)在processor->committedOutputLen()与outputTokenLen()不一致时返回ErrorInfo(ErrorCode::UNKNOWN_ERROR, ...)。该场景是引擎内部状态机破损(processor 与 stream 提交长度失去 parity),与真正「原因未知」的运行时错误混在同一个码上;UNKNOWN_ERROR = 514在 Python 侧同样未指定 category(落入 INTERNAL),因此监控无法把这类可定位的内部不变量破坏与其它未知错误区分开,也无法据此配置针对性告警。 - [6.1] Quality — Commit 原子、message 与行为匹配 → issue
大量与 grammar 功能无关的格式化重排混入本次改动
两个分片团队独立逐段比对后一致确认:该测试文件约 1100 行改动为 clang-format 纯重排(无新增/删除断言),TreeLogitsProcessorTest.cc除签名适配外亦以格式化为主。本 PR 共 135 文件,横跨三方依赖接入、grammar 后端、MTP 投机验证、smoke 体系、配置重构五个独立主题;这类无语义变更进一步放大 diff 体积,使真正的逻辑改动(如本次 P1 涉及的掩蔽常量分叉与TreeLogitsProcessorTest.cc:238的期望值调整)更难在 review 中被发现,也让 blame 与后续 cherry-pick 成本上升。 - [6.1] Quality — Mega-PR 已拆分为独立变更 → issue
大量与 grammar 功能无关的格式化重排混入本次改动
两个分片团队独立逐段比对后一致确认:该测试文件约 1100 行改动为 clang-format 纯重排(无新增/删除断言),TreeLogitsProcessorTest.cc除签名适配外亦以格式化为主。本 PR 共 135 文件,横跨三方依赖接入、grammar 后端、MTP 投机验证、smoke 体系、配置重构五个独立主题;这类无语义变更进一步放大 diff 体积,使真正的逻辑改动(如本次 P1 涉及的掩蔽常量分叉与TreeLogitsProcessorTest.cc:238的期望值调整)更难在 review 中被发现,也让 blame 与后续 cherry-pick 成本上升。 - [6.1] Quality — PR description 说明动机与设计 → issue
logits processor 文档未同步,且 ThinkModeLogitsProcessor 与 max_batch_size 形参成为死代码
本 PR 修改了该文档,但唯一新增是第 68 行「Tree Decode cannot be enabled together with MTP」。第 8 行的 Built-in processors 仍写着 ThinkModeLogitsProcessor「constrains tokens within the thinking segment by set max_thinking_tokens(internally uses a string-contain DFA)」,而该处理器已无任何生产构造点(全仓仅剩rtp_llm/cpp/models/BUILD:55,68、自身单测与rtp_llm/dash_sc/think.py:167的过期注释)。全文检索 grammar / json_schema / structural_tag / xgrammar 零命中,即本次引入的 GrammarLogitsProcessor、SpecLogitsVerifyRunner、四个互斥请求字段、新增服务参数与错误码全部缺失。另 `createLogitsProcessors - [6.1] Quality — 逻辑变更未混入无关格式化 → issue
大量与 grammar 功能无关的格式化重排混入本次改动
两个分片团队独立逐段比对后一致确认:该测试文件约 1100 行改动为 clang-format 纯重排(无新增/删除断言),TreeLogitsProcessorTest.cc除签名适配外亦以格式化为主。本 PR 共 135 文件,横跨三方依赖接入、grammar 后端、MTP 投机验证、smoke 体系、配置重构五个独立主题;这类无语义变更进一步放大 diff 体积,使真正的逻辑改动(如本次 P1 涉及的掩蔽常量分叉与TreeLogitsProcessorTest.cc:238的期望值调整)更难在 review 中被发现,也让 blame 与后续 cherry-pick 成本上升。 - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
单一 grammar 约束规则存在两份重复实现,存在行为漂移风险
_resolve_final_constraint(:138-148)完整复写了GrammarConstraint.resolve_from_config(grammar_constraint.py:146-156)的逻辑:逐个validate_not_empty()→len(...) > 1抛UNSUPPORTED_OPERATION→ 用" / ".join(GRAMMAR_FIELD_NAMES)拼错误文案,连文案都是复制的。两处分别被请求入口(ResponseFormatPlan.compile)与 RPC 边界断言(validate_engine_ready:201)调用;后续任何一处调整(新增 grammar 字段、改错误码、给validate_not_empty增加 dict 空值判断)都可能只改一半,出现「入口通过、发车前 400」这类难以定位的不一致。 - [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue
GrammarConfig::__setstate__ 靠运行时类型嗅探分派 4 种布局,含两条生产不可达死分支
__getstate__(:1340-1346)只产出当前 5 元组,历史上真实发布过的只有首元素为grammar_backend字符串的 legacy 5 元组(由 :1352 分支处理)。但__setstate__额外维护「6 元组」(:1366-1372)与「含 override_stop_tokens 的 5 元组」(:1379-1385)两条分支,这两种 state 只能由本特性分支的中间提交产生,生产不可达,却被config_pickle_test.py两个用例长期锁定,后续任何字段增删都要维护 4 条分支。分派依赖py::isinstance<py::int_>(t[3]),而 Python 中bool是int的子类;6 元组分支还把t[3]硬编码为vector<int32_t>,未来任何新 6 字段布局都会直接抛 "Invalid state"。 - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
MTP grammar smoke fixture 命名与实际 suite 接线不一致,thinking 关闭 + MTP + grammar 组合零覆盖
该 fixture 的_comment自述为 "MTP + grammar (no reasoning)",但suites_h20_oss.bzl:267-274把它接成qwen35_grammar_concurrent_no_mtp,smoke_args中既无--sp_model_type也无--sp_type,并显式--think_mode 0,注释也写明 "no MTP"。smoke_h20_grammar_heavy中另一条 grammar 用例(:276-288)才是 PD + MTP,但--think_mode 1。因此「MTP + grammar + thinking OFF」这条组合实际零覆盖,而文件名与注释会让读者误以为已覆盖;q_r_mtp_grammar_reasoning.json的注释也声称与它配对形成 thinking ON/OFF 两轴。 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
model_rpc_client 新增的两条错误分支缺负向测试,且内部契约违约上报为用户输入错误
本 PR 在 RPC 边界新增两条失败路径:_trans_jsonable_option把json.dumps的TypeError/ValueError/RecursionError转为FtRuntimeException(ERROR_INPUT_FORMAT_ERROR)(:74-78);trans_input新增validate_engine_ready前置断言(:122)。model_rpc_client_test.py全文无任何assertRaises,只有成功路径用例,两条新分支零覆盖;由于GenerateConfig未开启validate_assignment,直接赋一个不可 JSON 序列化的structural_tag是可达的。另validate_engine_ready失败说明入口漏调 prepare(内部接线 bug),却以ERROR_INPUT_FORMAT_ERROR返回,监控上无法与真实用户输入错误区分,且未记录 request_id。 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
model_rpc_client 新增的两条错误分支缺负向测试,且内部契约违约上报为用户输入错误
本 PR 在 RPC 边界新增两条失败路径:_trans_jsonable_option把json.dumps的TypeError/ValueError/RecursionError转为FtRuntimeException(ERROR_INPUT_FORMAT_ERROR)(:74-78);trans_input新增validate_engine_ready前置断言(:122)。model_rpc_client_test.py全文无任何assertRaises,只有成功路径用例,两条新分支零覆盖;由于GenerateConfig未开启validate_assignment,直接赋一个不可 JSON 序列化的structural_tag是可达的。另validate_engine_ready失败说明入口漏调 prepare(内部接线 bug),却以ERROR_INPUT_FORMAT_ERROR返回,监控上无法与真实用户输入错误区分,且未记录 request_id。
RTP-LLM Checklist
- [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue
新增 golden 数据的说明注释引用了不存在的源文件与不存在的用例文件
该文件_comment把GrammarManager.cc:473-474与ReasonerGrammarObject列为历史 bug 位置,并声称与q_r_mtp_grammar_reasoning_concurrent.json配对形成 thinking ON + 并发轴。全仓搜索GrammarManager、ReasonerGrammarObject、q_r_mtp_grammar_reasoning_concurrent只命中该注释自身,三者在仓内均不存在(本 PR 的 grammar 实现是XGrammarBackend.cc/RtpGrammarMatcher.cc/GrammarLogitsProcessor.cc)。这些注释是后续排障的主要线索来源,指向失效会直接误导读者。 - [I] 代码质量 — 同一功能用统一工具函数 → issue
单一 grammar 约束规则存在两份重复实现,存在行为漂移风险
_resolve_final_constraint(:138-148)完整复写了GrammarConstraint.resolve_from_config(grammar_constraint.py:146-156)的逻辑:逐个validate_not_empty()→len(...) > 1抛UNSUPPORTED_OPERATION→ 用" / ".join(GRAMMAR_FIELD_NAMES)拼错误文案,连文案都是复制的。两处分别被请求入口(ResponseFormatPlan.compile)与 RPC 边界断言(validate_engine_ready:201)调用;后续任何一处调整(新增 grammar 字段、改错误码、给validate_not_empty增加 dict 空值判断)都可能只改一半,出现「入口通过、发车前 400」这类难以定位的不一致。
Python Static-First Checklist
- [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
smoke 的 grammar 校验在 OpenAI 通路上可能零校验静默通过,且与 DashSc 通路语义相反
compare_result(:249-251)在grammar_constraint_only为真时调用_validate_grammar_constraint后直接return,完全跳过 golden 比对。而该方法有两条无声提前返回:if self.is_stream: return(:230-231,注释「streaming smoke for grammar isn't used today」)与if not response_format: return(:233-234)。任一命中时该 query 既不做约束校验也不做 golden 比对,最终被记为 OK。对照dash_grpc_comparer.py:220-228,同一 flag 下取不到 grammar 字段会raise SmokeException(QueryStatus.VALID_FAILED, ...),两个 comparer 的失败语义相反。当前数据文件恰好都带response_format且非流式,问题被掩盖。 - [P.G] 测试规范 — pytest.raises 带 match 参数 → issue
model_rpc_client 新增的两条错误分支缺负向测试,且内部契约违约上报为用户输入错误
本 PR 在 RPC 边界新增两条失败路径:_trans_jsonable_option把json.dumps的TypeError/ValueError/RecursionError转为FtRuntimeException(ERROR_INPUT_FORMAT_ERROR)(:74-78);trans_input新增validate_engine_ready前置断言(:122)。model_rpc_client_test.py全文无任何assertRaises,只有成功路径用例,两条新分支零覆盖;由于GenerateConfig未开启validate_assignment,直接赋一个不可 JSON 序列化的structural_tag是可达的。另validate_engine_ready失败说明入口漏调 prepare(内部接线 bug),却以ERROR_INPUT_FORMAT_ERROR返回,监控上无法与真实用户输入错误区分,且未记录 request_id。
Strengths
- 错误语义全链路贯通且拦截点正确:
process/updateStatus返回std::optional<ErrorInfo>→SamplerOutput::processor_errors→StreamSpecUpdateInfo::error_info,拦截发生在 append token 之后、publish output 之前,违规 token 不会吐给用户。 - 掩码构造一律 fail-closed 而非 fail-open:
prepareSpecMask在 vocab 越界、EOS 越界、fillBitmask/rollback 失败等任一路径都markFinished()并把首行压成仅 EOS;mergeProcessorMasks(SpecLogitsVerifyRunner.cc:190-197)在prepareSpeculative失败时把 cap 归零,未受约束的 token 不会漏出。 acceptCommittedLocked用old_matcher_len/old_output_len保证多 token 提交失败不留下任何可见前缀,回滚再失败时二次markFinished(),状态机不停在不确定态。- 分层收敛干净:
engine_base/grammar/BUILD:35显式声明只有 Tier 0/1 可依赖,XGrammarBackend.h只对外暴露GrammarKeyCpp,GrammarLogitsProcessor.h仅前向声明RtpGrammarMatcher,xgrammar 类型未泄漏到采样层。 - matcher 异常边界干净:
matcherCall模板把上游所有抛出统一收敛为GRAMMAR_VERIFY_EXCEPTION,接口全部[[nodiscard]]且禁用拷贝/移动,并用static_assert(XGrammarBackendTest.cc:39-42)把不可移动语义固化为编译期契约。 - 错误分类可运维:
compileWithErrorClassification(XGrammarBackend.cc:22-32)把bad_alloc归 ResourceExhausted(可重试)、runtime_error归 InvalidArgument(用户输入),让「用户 schema 写错」与「系统资源不足」在日志侧可区分。 - 词表越界处理两条路径语义一致:
applyState对[grammar_vocab, logits_vocab)补neg_inf,spec 路径用clearBitmaskTokenRange做等价裁剪;has2DCapacity强制size(1)==W保证行 stride 与指针算术一致。 - 性能设计克制:
task.active为空时run()直接短路不触发 D2H;只为带 SPEC_VERIFY 的流分配紧凑行并用logits_row_indices映射回原始行,避免[B*(P+1), V]全量掩码;多处理器 cap 取 min、bitmask 取 AND。 - 顺带修掉既有缺陷:MTP 路径此前从未填充的
sampler_inputs.finished_mask现显式zero_()(MtpBatchStreamProcessor.cc:122)并有断言覆盖。 - 配置面单一来源:
GRAMMAR_FIELD_NAMES由Literal经get_args派生;--grammar_num_workers的 help 明确为max_compiler_threads(单次编译内并行度),消除长期运维误解;序列化只在_trans_jsonable_option的 protobuf 边界做一次json.dumps,异常统一带字段名并raise ... from e。 - 打包链路闭环:新增
//:th_grammar_tokenizer_info独立.so,arch_select.copy_all_so、rtp_llm/libs:whl_package_libs同步补齐,未留悬空引用。 - 测试贴近生产边界:
GrammarLogitsProcessorTest用真实XGrammarBackend+ 128 个 ASCII 单字符词表使 token id 与字符对齐,覆盖 cap 语义、terminated 强制 EOS、整批回滚、grammar vocab 溢出、EOS 越界、all-true no-op、structural_tag reasoning budget;config_pickle_test.py用__reduce__伪造历史 state 布局做逐布局回归。
| -INFINITY, 0.887f, 0.99999f, 0.1f, 0.2f, 0.3f, 0.f, 0.f, 0.99f, 0.989f, | ||
| 0.221f, 0.f, 0.f, 0.1f, 0.2f, 0.321f, 0.f, 0.4432f, 0.44f, -INFINITY, | ||
| 0.221f, 0.f, 0.f, 0.1f, -INFINITY, 0.321f, 0.f, 0.4432f, 0.44f, 0.01f, | ||
| 0.f, -FLT_MAX, 0.f, -FLT_MAX, 0.2f, 0.3f, 0.f, 0.f, 0.f, 0.01f, |
There was a problem hiding this comment.
[P1] fp32 掩蔽哨兵值在生产 kernel 与新断言间分叉,cuda_mask_logits_op_test 必然失败
该用例把 fp32 期望值改为 -FLT_MAX,并在第 64 行用严格 == 比较。但调用链 runtimeMaskLogits(CudaOps.cc:319-321)→ invokeMaskLogits<float> → mask_logits kernel 本 PR 未改,仍写 NegativeInfinity<T>() = -INFINITY(mask_logits.cu:83);新增的 MaskedLogitValue<T>()(-FLT_MAX)只被 packed_mask_logits 使用(:121)。同 PR 的 ExecOpsTest.cc:360 对同一 runtimeMaskLogits/fp32 断言 std::isinf,两处期望互斥。cuda_mask_logits_op_test(cuda/ops/tests/BUILD:29)无 manual 标签且带 exec_properties={'gpu':'H20'},会在门禁执行。
建议: 先确定「被屏蔽 logits」的唯一真相值。若目标是与 BaseLogitsProcessor::neg_inf 对齐,则把 mask_logits kernel 也改用 MaskedLogitValue<T>(),并把 ExecOpsTest.cc:360 的 std::isinf 改为对 -FLT_MAX 的精确断言;若 mask_logits 有意保留 -INFINITY,则回退本用例期望值,并把 MaskedLogitValue 上方「Match BaseLogitsProcessor::neg_inf」的注释限定为仅适用于 packed 路径。无论选哪种,都应把该常量收敛到单一来源,并补一条跨 mask_logits / packed_mask_logits / CPU fallback 三条路径的一致性断言——注意 BaseLogitsProcessor::maskLogits 本身也分叉:CUDA 走 cudaMaskLogits(-INFINITY),非 CUDA 走 masked_fill_(neg_inf)(-FLT_MAX)。
| @@ -136,7 +136,6 @@ message GenerateConfigPB { | |||
| google.protobuf.StringValue regex = 70; | |||
| google.protobuf.StringValue ebnf = 71; | |||
| google.protobuf.StringValue structural_tag = 72; | |||
There was a problem hiding this comment.
[P1] proto 删除 response_format 字段 73 未加 reserved,混版滚动升级静默丢弃结构化约束
GenerateConfigPB 字段列表停在 structural_tag = 72;(第 138 行),紧接第 139 行即 },既无 reserved 73; 也无 reserved "response_format";;而同文件 103-104 行已有既定约定 reserved 42; reserved "inter_request_id";。QueryConverter.cc:100-103 也只保留四个 typed 字段的 TRANS_OPTIONAL。后果一:滚动升级期旧 frontend 仍在 73 号位写 response_format,proto3 按 unknown field 忽略,请求从「受约束解码」静默退化为自由生成,无错误码也无日志。后果二:73 号位未保留,将来复用为其他类型时与旧 peer 会错位解析。
建议: 在 structural_tag = 72; 之后补 reserved 73; 与 reserved "response_format";,与本文件 103-104 行保持一致。若需覆盖混部窗口,建议 engine 侧对仍携带该 unknown field 的请求以 ERROR_GENERATE_CONFIG_FORMAT 显式拒绝并打印告警,而不是静默降级;并在 PR description 或 proto 注释中写明 frontend/engine 的升级顺序要求(可参考同文件 KV-cache 协议注释的写法)。
| ) -> "ResponseFormatPlan": | ||
| final_constraint = _resolve_final_constraint(config) | ||
|
|
||
| if config.in_think_mode: |
There was a problem hiding this comment.
[P1] think 模式无条件改为 grammar 约束解码,thinking + beam search / num_return_sequences>1 由可用变 400
ResponseFormatPlan.compile 中只要 config.in_think_mode 为真,72-78 行就对 has_num_beams() or num_return_sequences > 1 直接抛 ERROR_INPUT_FORMAT_ERROR,与用户是否请求结构化输出无关;随后 84-96 行必然生成 structural_tag reasoning envelope(final_constraint 为 None 时用 {"type":"any_text"} 兜底)。对照 98-105 行的非 think 分支,同一限制仍以 final_constraint is not None 为前提,说明这是 think 路径新增的无条件语义。openai_endpoint.py:218-219 把 request.n 写入 num_return_sequences,:292 随后调用 add_thinking_params,因此 THINK_MODE=1 部署下的 n=2 请求整条失败,文档未记录该限制。
建议: 区分「用户显式要求 grammar」与「think 模式内部使用 reasoning envelope」:final_constraint is None 时 think 模式应保持旧行为放行 beam/n>1(例如该组合下不下发 envelope,或按 sequence 各自建 matcher),仅 final_constraint is not None 时才拒绝。若确认限制不可避免,请在 docs/backend/logits_processor.md 与 PR description 中声明为 breaking change,并补一条覆盖 THINK_MODE=1 + num_return_sequences>1 的用例明确迁移方式。
| default="xgrammar", | ||
| help="Grammar backend type: xgrammar or none", | ||
| ) | ||
| grammar_group.add_argument( |
There was a problem hiding this comment.
[P1] 删除 --grammar_backend / GRAMMAR_BACKEND,存量启动脚本硬失败且移除唯一的 grammar 关停开关
init_grammar_group_args 现只注册 4 个参数(whitespace / terminate / num_workers / cache_bytes),原 --grammar_backend(默认 "xgrammar",help 曾写 "xgrammar or none")与 override_stop_tokens 一并删除;ConfigModules.h 的 GrammarConfig 也已无该字段。全仓搜索 grammar_backend/GRAMMAR_BACKEND 仅命中 ConfigInit.cc:1354-1355 的 pickle 兼容注释。EnvArgumentParser.parse_args 最终走 argparse,未注册选项直接 exit(2),故仍带 --grammar_backend xgrammar 的启动命令无法拉起;环境变量分支只遍历已注册参数的 _env_mappings,存量 GRAMMAR_BACKEND 被静默丢弃。新参数组中不存在任何服务级 grammar...
建议: 保留一个已废弃的 no-op --grammar_backend(接受取值、打 deprecation WARNING 后忽略)以免存量脚本启动失败,同时新增显式 --grammar_enable / GRAMMAR_ENABLE(或恢复 grammar_backend=none 语义)作为运维回滚手段:关闭时跳过 grammar 元数据构建,并让 grammar 请求走 LogitsProcessorFactory.cc:107-111 既有的「backend is disabled」分支快速失败。请在 release note 中列出该 CLI 参数、环境变量与 override_stop_tokens 的移除及迁移方式,并补一条断言旧参数行为的单测。
| encoded_vocab, json.dumps(metadata, separators=(",", ":")) | ||
| ) | ||
|
|
||
| raise ValueError(f"Unsupported tokenizer type: {type(tokenizer)}") |
There was a problem hiding this comment.
[P1] grammar tokenizer 元信息成为所有语言模型的启动硬依赖,不支持的 tokenizer 直接阻塞模型加载且无逃生口
build_grammar_tokenizer_info_json 在 tokenizer 不属于 fast / tiktoken / sentencepiece 三类时 raise ValueError("Unsupported tokenizer type")(:195),stop_token_ids 为空时同样抛错(:159)。base_model.py:309-328 把任何异常包成 RuntimeError 继续抛出(注释自述这是 "startup-time compatibility contract"),而 model_factory.py:212-215 对所有 TaskType.LANGUAGE_MODEL 无条件调用它。C++ 侧 XGrammarBackend::create 在 JSON 反序列化失败时也不返回 nullptr,而是 RTP_LLM_LOG_ERROR(...aborting startup) 后 rethrow(:96-102),该调用位于 executor 构造函数内。结果:完全不使用结构化输出的模型...
建议: 把「启动即失败」降级为「按请求失败」:新增(或复用上一条建议的)服务级开关,开关关闭或元数据构建失败时记 WARNING 并返回空串,让 XGrammarBackend::create 走 tokenizer_info_json.empty() 分支返回 nullptr(:67-70 已有该分支),仅在实际发起 grammar 请求时返回明确的 INVALID_PARAMS。同时把 create 对反序列化失败的 rethrow 改为可配置行为。若坚持 fail-fast,请在 PR description 中列出受影响的 tokenizer 类型清单、非 CUDA 平台结论与规避手段。
| "model_type": "qwen35_moe", | ||
| "model_path": "/mnt/nas1/hf/Qwen3.5-35B-A3B-FP8", | ||
| "grammar_constraint_only": true, | ||
| "_comment": "MTP + grammar + reasoning (sequential, thinking ON). Pairs with q_r_mtp_grammar.json (thinking OFF) and q_r_mtp_grammar_reasoning_concurrent.json (thinking ON + concurrency). Each axis has had a real bug in living memory: (1) MTP rollback non-reasoning active_steps formula bug; (2) ReasonerGrammarObject per-stream tokens_after_think_end reset (GrammarManager.cc:473-474); (3) MTP prefill bonus token T0 must be accepted into matcher; (4) grammar fork/DFS in spec decoding. With reasoning enabled, the matcher is in 'free-accept' (mask off) until </think> is seen, then locks onto the schema. Under MTP this means: the spec engine drafts N tokens including possibly </think>, the matcher must transition state synchronously, AND rollback must use the reasoning-mode active_steps formula. If any of those four breaks, the output is either non-JSON or never closes <think>. grammar_constraint_only=true because reasoning_content is model-version-dependent.", |
There was a problem hiding this comment.
[P3] 新增 golden 数据的说明注释引用了不存在的源文件与不存在的用例文件
该文件 _comment 把 GrammarManager.cc:473-474 与 ReasonerGrammarObject 列为历史 bug 位置,并声称与 q_r_mtp_grammar_reasoning_concurrent.json 配对形成 thinking ON + 并发轴。全仓搜索 GrammarManager、ReasonerGrammarObject、q_r_mtp_grammar_reasoning_concurrent 只命中该注释自身,三者在仓内均不存在(本 PR 的 grammar 实现是 XGrammarBackend.cc / RtpGrammarMatcher.cc / GrammarLogitsProcessor.cc)。这些注释是后续排障的主要线索来源,指向失效会直接误导读者。
建议: 把 GrammarManager.cc:473-474 / ReasonerGrammarObject 更新为本 PR 中实际承载该状态复位逻辑的文件与符号名(建议只写符号名,避免行号腐化);删除对 q_r_mtp_grammar_reasoning_concurrent.json 的引用,或在本 PR 中一并补齐该用例并接入 smoke_h20_grammar_heavy。
Checklist: [I] 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理
| ... | ||
| def to_string(self) -> str: | ||
| ... | ||
| class GrammarConfig: |
There was a problem hiding this comment.
[P3] GrammarConfig 新增字段未同步到 .pyi 类型存根
ConfigInit.cc:1325 新增 def_readwrite("terminate_without_stop_token", ...),grammar_group_args.py:17 通过 bind_to 写入,server_args_test.py 与 config_pickle_test.py 都直接读写该属性。但本 PR 已同步修改的 libth_transformer_config.pyi:544-556 中 GrammarConfig 只声明了 compiler_cache_bytes / constrained_json_disable_any_whitespace / num_workers / tokenizer_info_json,独缺新字段(也缺 __repr__),说明存根是手改而漏项。仓库无 mypy 门禁,CI 不会拦住,只会让 IDE 与类型检查对合法访问报假错。
建议: 重新生成 libth_transformer_config.pyi 并提交(或手工补 terminate_without_stop_token: bool),使存根与 pybind 绑定、pickle state 布局三者一致。若希望后续不再漂移,建议加一个「重新生成 stub 后 diff 为空」的校验用例。
Checklist: [6.1] 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全
| value = json.dumps(value, ensure_ascii=False, separators=(",", ":")) | ||
| try: | ||
| value = json.dumps(value, ensure_ascii=False, separators=(",", ":")) | ||
| except (TypeError, ValueError, RecursionError) as e: |
There was a problem hiding this comment.
[P3] model_rpc_client 新增的两条错误分支缺负向测试,且内部契约违约上报为用户输入错误
本 PR 在 RPC 边界新增两条失败路径:_trans_jsonable_option 把 json.dumps 的 TypeError/ValueError/RecursionError 转为 FtRuntimeException(ERROR_INPUT_FORMAT_ERROR)(:74-78);trans_input 新增 validate_engine_ready 前置断言(:122)。model_rpc_client_test.py 全文无任何 assertRaises,只有成功路径用例,两条新分支零覆盖;由于 GenerateConfig 未开启 validate_assignment,直接赋一个不可 JSON 序列化的 structural_tag 是可达的。另 validate_engine_ready 失败说明入口漏调 prepare(内部接线 bug),却以 ERROR_INPUT_FORMAT_ERROR 返回,监控上无法与真实用户输入错误区分,且未记录 request_id。
建议: 补两个带 match 断言的用例:1)给 config 赋含不可序列化对象的 structural_tag,断言报错带字段名;2)构造 response_format 未 finalize 的 config,断言 trans_input 抛出「must be prepared before engine serialization」。同时建议把 validate_engine_ready 的失败改为可区分的内部错误类型,或在抛出前 logging.error 打印 request_id / trace_id 与失败字段名(此时 input_pb.request_info 已填好,成本极低)。
Checklist: [6.1] 新逻辑有聚焦单测 + 相关集成/smoke 测试;[6.1] 边界 case 覆盖(空、单元素、最大值);[P.G] pytest.raises 带 match 参数
| const auto& processor = processors[i]; | ||
| const auto processor_output_len = processor->committedOutputLen(); | ||
| if (processor_output_len.has_value() && processor_output_len.value() != stream_output_len) { | ||
| return ErrorInfo(ErrorCode::UNKNOWN_ERROR, |
There was a problem hiding this comment.
[P3] processor 与 stream 长度不一致这一内部不变量被归入 UNKNOWN_ERROR
validateLogitsProcessorState(:953-971)在 processor->committedOutputLen() 与 outputTokenLen() 不一致时返回 ErrorInfo(ErrorCode::UNKNOWN_ERROR, ...)。该场景是引擎内部状态机破损(processor 与 stream 提交长度失去 parity),与真正「原因未知」的运行时错误混在同一个码上;UNKNOWN_ERROR = 514 在 Python 侧同样未指定 category(落入 INTERNAL),因此监控无法把这类可定位的内部不变量破坏与其它未知错误区分开,也无法据此配置针对性告警。
建议: 为该不变量分配专用错误码(或复用本 PR 新增的 GRAMMAR_VERIFY_EXCEPTION 同层语义的内部码),并在返回前打一条带 stream_id / processor_index 的 RTP_LLM_LOG_ERROR,使该路径在日志与指标上可归因。
Checklist: [6.1] 可观测性:日志/指标/超时可操作、非噪声;[6.1] 错误语义:fail-fast/retry/fallback/silent 行为显式
| @@ -9,25 +9,22 @@ using namespace std; | |||
| namespace rtp_llm { | |||
There was a problem hiding this comment.
📍 实际位置 rtp_llm/cpp/models/logits_processor/test/RecommendationLogitsProcessorTest.cc:1(不在 diff 展示范围内,就近挂载)
[P3] 大量与 grammar 功能无关的格式化重排混入本次改动
两个分片团队独立逐段比对后一致确认:该测试文件约 1100 行改动为 clang-format 纯重排(无新增/删除断言),TreeLogitsProcessorTest.cc 除签名适配外亦以格式化为主。本 PR 共 135 文件,横跨三方依赖接入、grammar 后端、MTP 投机验证、smoke 体系、配置重构五个独立主题;这类无语义变更进一步放大 diff 体积,使真正的逻辑改动(如本次 P1 涉及的掩蔽常量分叉与 TreeLogitsProcessorTest.cc:238 的期望值调整)更难在 review 中被发现,也让 blame 与后续 cherry-pick 成本上升。
建议: 把纯格式化重排拆到独立的先导 commit 或独立 PR,使功能 commit 的 diff 只包含行为变更;若已难以拆分,请在 PR description 中显式列出「仅格式化」的文件清单便于 reviewer 跳过。后续建议把 clang-format 作为 pre-commit 全量执行一次并单独提交,避免每个功能 PR 都夹带格式噪声。
Checklist: [6.1] Commit 原子、message 与行为匹配;[6.1] Mega-PR 已拆分为独立变更;[6.1] 逻辑变更未混入无关格式化
- centralize and validate response format preparation across request paths - harden tokenizer initialization and remove obsolete configuration plumbing - preserve legacy mask-logits behavior while keeping packed-mask semantics isolated
LLLLKKKK
left a comment
There was a problem hiding this comment.
AI Code Review - PR #1006
Status: BLOCKING
Summary: P0/0 · P1/6 · P2/26 · P3/7
Reviewed: commit 1bdc5e78da76 · 2026-08-06 23:35 UTC+8
Blocking Issues
P1
- proto 字段 73 response_format 删除未加 reserved,混版滚动升级静默丢弃结构化约束 @
rtp_llm/cpp/model_rpc/proto/model_rpc_service.proto:138- 建议:补
reserved 73;与reserved "response_format";(model_rpc_client_test.py的assertNotIn(..., fields_by_name)在 reserved 下仍成立)。同时在QueryConverter::transGenerateConfig检测该 unknown field,读到非空值即返回ERROR_INPUT_FORMAT_ERROR并打 warning + 指标,把「静默丢约束」改成可观测的 fail-fast;并在 PR description 写明 frontend 与 engine 的升级顺序要求。
- 建议:补
- 删除 --grammar_backend / GRAMMAR_BACKEND,存量启动脚本硬失败且移除唯一的 grammar 关停开关 @
rtp_llm/server/server_args/grammar_group_args.py:6- 建议:二选一:(a) 保留
--grammar_backend为 deprecated alias,接受xgrammar/none并打 WARN,none映射为「不注入 tokenizer_info_json」,复用XGrammarBackend::create已有的 empty →nullptr关闭路径;(b) 新增等价的--grammar_enable(默认 true)作为显式 kill switch,并让启动期元信息构建失败可经该开关降级。两者都需在 PR description/文档列出被删 CLI/env 与迁移方式,并补一条断言旧参数行为的回归测试。
- 建议:二选一:(a) 保留
- grammar tokenizer 元信息成为所有语言模型的启动硬依赖,不支持的 tokenizer 直接阻塞上线且无逃生口 @
rtp_llm/config/grammar_tokenizer_info.py:195- 建议:区分「grammar 能力缺失」与「启动失败」:构建失败时记 error 并把
tokenizer_info_json置空(与real_tokenizer is None分支一致),由首个 grammar 请求返回明确的UNSUPPORTED_OPERATION(GRAMMAR_*码已具备)。若坚持启动期 fail-fast(注释已声明为有意设计),必须同时提供运维旁路开关,并为「不支持的 tokenizer」「空 stop_token_ids」两条路径补启动期集成测试与受影响 model/tokenizer 清单。
- 建议:区分「grammar 能力缺失」与「启动失败」:构建失败时记 error 并把
- think 模式无条件改为 grammar 约束解码,thinking + beam search / num_return_sequences>1 由可用变 400 @
rtp_llm/config/response_format_compiler.py:71- 建议:把 reasoning envelope 与
in_think_mode解耦:仅在存在最终约束或显式开启 reasoning 约束时才包裹,其余 think 请求维持原非 grammar 路径;若确需全量包裹,请为 beam / 多返回序列补降级分支(退回非 grammar think 而非直接 400),并在 backend 关闭时于启动期显式告警或拒绝。同时在docs/backend/logits_processor.md与 PR description 标注该行为变更、互斥关系与全量 grammar 的吞吐实测数据。
- 建议:把 reasoning envelope 与
- in_think_mode / max_thinking_tokens 仍在 wire 上,但引擎侧已无任何执行者,退化为静默 no-op @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:99- 建议:明确该组合的语义并落实:对「
in_think_mode=true且四个 grammar 字段全空」的请求在createLogitsProcessors或validate_engine_ready显式返回UNSUPPORTED_OPERATION(至少 WARNING + 指标),把静默降级变为可观测;若确定这两个字段不再驱动引擎行为,请在 proto 注释标注 deprecated 并说明已由 structural_tag 承载,同时删除ThinkModeLogitsProcessor.{h,cc}、其单测与 BUILD 条目及createLogitsProcessors未使用的max_batch_size形参。
- 建议:明确该组合的语义并落实:对「
- MTP 新增启动期 abort 与请求期硬拒绝,把既有可运行组合打死且无过渡开关 @
rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:230- 建议:为两处收紧提供可回退开关(例如「仅告警不拒绝」的兼容模式),并在 release note/文档列出受影响配置组合,尤其点明用户可直接触发的 MTP +
num_return_sequences>1;至少把tree_decode_config冲突从构造函数中途 CHECK 改为返回结构化启动错误,避免半构造对象抛异常;并为 MultiSeq/Recommendation 显式声明能力,而不是依赖基类默认值把它们一并拒掉。
- 建议:为两处收紧提供可回退开关(例如「仅告警不拒绝」的兼容模式),并在 release note/文档列出受影响配置组合,尤其点明用户可直接触发的 MTP +
Non-blocking Suggestions
P2
- grammar 编译失败的状态码在 factory 边界被压平为 INVALID_PARAMS,丢失可重试语义 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:120- 建议:按
absl::StatusCode显式映射:kInvalidArgument→INVALID_PARAMS,kResourceExhausted→MALLOC_FAILED或 8000 段可重试码,其他 → 可重试的未知错误码;去掉 message 中的(retryable)文本,让重试语义只由 ErrorCode 承载;补一条断言 ResourceExhausted 不被映射为INVALID_PARAMS的单测。
- 建议:按
- 新增 6 个 GRAMMAR_ 错误码未指定 ExceptionCategory,对外一律退化为 internal* @
rtp_llm/config/exceptions.py:45- 建议:逐码显式声明:
GRAMMAR_PARSER_REJECTED_TOKEN/GRAMMAR_NON_EOS_AFTER_TERMINAL→INVALID_OUTPUT;GRAMMAR_VOCAB_EXCEEDS_MODEL_VOCAB/GRAMMAR_EOS_OUT_OF_VOCAB→BAD_REQUEST或UNSUPPORTED;GRAMMAR_BITMASK_BUFFER_TOO_SMALL/GRAMMAR_VERIFY_EXCEPTION保留 INTERNAL 但显式书写并注明理由。补一条参数化用例断言每个新码的 category 与预期 Dash error spec 一致。
- 建议:逐码显式声明:
- matcher FINISHED 状态在 decode 与 MTP 两条路径的兜底语义相反,decode 侧会完全放开约束 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:204- 建议:统一为 fail-closed:
applyState的 FINISHED 分支改为forceEos(logits, eos_token_id)(至少保留 tail 屏蔽),并在DeviceMaskMode枚举处注释「FINISHED 表示 matcher 已停机,必须只允许 EOS」。若刻意保留放开语义,请在GrammarLogitsProcessor.h把「上层必须在返回 ErrorInfo 时终止 stream」写成显式契约,并补一条markFinished()之后 decode 可观测行为的断言,防止后续重构悄悄改变失败模式。
- 建议:统一为 fail-closed:
- pin_memory() 无平台保护,非 CUDA 构建下 grammar 解码以无关错误失败,且 CPU fallback 测试被 GPU 执行机锁死 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:236- 建议:用
#if USING_CUDA(HIP 同样支持锁页主机内存,可扩为USING_CUDA || USING_ROCM让 ROCm 复用 device 掩码路径)包裹.pin_memory(),非 CUDA 退化为普通 CPU 张量;把 fallback 目标的 CUDA 专属 deps 放进select()并去掉exec_properties,使其真正在非 CUDA 配置下被验证。若 cpu/arm/rocm 不在支持范围,请在引擎启动期以明确信息拒绝开启结构化输出,并在头文件与文档声明平台支持范围。
- 建议:用
- 受约束 decode 每步触发一次全流同步的 H2D 拷贝,且缓存 mask 是复用缓冲区的视图 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:268- 建议:改为
non_blocking=true,并配套解决源缓冲复用:让DeviceMaskState持有独立clone()快照,或用at::cuda::CUDAEvent/record_stream为在途拷贝建立依赖。若短期不改,请在该行与packed_allow_mask_cpu声明处注释说明这是有意的同步点及其代价、别名生命周期只到下一次 build,并给出多语法并发下的 decode 步延迟基准。
- 建议:改为
- 进程级 static grammar backend 生命周期未绑定 executor,warm-up 阶段被重复重建 @
rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:66- 建议:将 backend 下沉为 engine/executor 成员并由
createLogitsProcessors显式传入。过渡期至少:(1) warm-up 路径跳过XGrammarBackend::create(warm-up 的 fake input 不带约束);(2)init()缓存tokenizer_info_json哈希,相同则复用(顺带消除重复构建),不同则RTP_LLM_CHECK_WITH_INFO显式失败而非静默覆盖;(3) 用std::once_flag或原子交换消除无同步写。
- 建议:将 backend 下沉为 engine/executor 成员并由
- 反序列化后用 VocabType::RAW 重建 TokenizerInfo,等价性无断言且无非 RAW 词表回归 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:82- 建议:新增等价性测试:用 BYTE_LEVEL(含
\u0120前缀、多字节 token)与 BYTE_FALLBACK(含<0x0A>)两种词表,一路直接构造 matcher、一路经SerializeJSON → create → createMatcherFromKey,对同一 grammar 断言fillBitmask输出逐字相等,并断言GetSpecialTokenIds()/GetVocabSize()/GetAddPrefixSpace()一致;在 :79-86 注释写明「GetVocabType()降级为 RAW 后哪些 xgrammar 行为被认为不受影响」这一前提。
- 建议:新增等价性测试:用 BYTE_LEVEL(含
- padded / 稀疏词表空洞用空字符串占位,缺少「空洞 token id 被 grammar 屏蔽」的验证 @
rtp_llm/config/grammar_tokenizer_info.py:28- 建议:补一条稀疏词表用例(如
{"a":0,"c":2}+model_vocab_size=4)编译任意 grammar,断言空洞 id 在fillBitmask结果中为 0;若实测被放行,改用 xgrammar 会判为 special / 不可匹配的哨兵占位,或在DecodeMaskBuilder::build与prepareSpecMask中显式清掉这些 bit,并把该不变量写入注释。
- 建议:补一条稀疏词表用例(如
- xgrammar 依赖绕开仓库统一入口,并钉在上游未合并的特性分支 commit 上 @
3rdparty/xgrammar/repositories.bzl:5- 建议:把 xgrammar 声明并入
git_deps()(保留xgrammar.BUILDoverlay),与 cutlass 等保持一致;改用带sha256的归档或内部镜像固定该 commit,并在注释写明所依赖的上游特性与合入计划,上游合并后切到 tag;修正 dlpack 注释为真实原因并补许可证声明。另建议给xgrammar.BUILD中依赖 submodule 路径的glob加allow_empty = False,把缺文件变成分析期明确失败。
- 建议:把 xgrammar 声明并入
- //rtp_llm:ops 无条件 data 依赖新 so,把 xgrammar 编译放大到全仓 Python 目标 @
rtp_llm/BUILD:195- 建议:保持 Bazel 侧与 Python 侧同样惰性:把该 data 依赖下沉到真正调用
serialize_grammar_tokenizer_info的目标,或拆出独立的py_library(name="ops_grammar");若确需全局可用,请在 BUILD 注释写明原因,避免后续被误删或继续扩散。
- 建议:保持 Bazel 侧与 Python 侧同样惰性:把该 data 依赖下沉到真正调用
- 服务级策略与同名请求级字段并存,raw 请求设置的 grammar_terminate_without_stop_token 解析即被静默丢弃 @
rtp_llm/cpp/engine_base/stream/GenerateConfig.h:57- 建议:删除
GenerateConfig::grammar_terminate_without_stop_token及其to_string()/JSONIZE条目,让服务级配置成为该策略的唯一来源;若确实计划支持请求级覆盖,请在本 PR 内接线到createMatcher、补端到端测试并说明与服务级的优先级,同时修正ConfigModules.h:376的注释,避免两处描述互相打架。
- 建议:删除
- GrammarConfig 新增字段未同步到 .pyi 类型存根,跨语言类型契约漂移 @
rtp_llm/ops/libth_transformer_config.pyi:544- 建议:重新运行存根生成工具或手动补
terminate_without_stop_token: bool,使.pyi与 pybind 注册一致;并考虑增加一条「存根字段集合与 pybind 注册一致」的 CI 校验,避免同类漂移复发。
- 建议:重新运行存根生成工具或手动补
- smoke 的 grammar 校验在 OpenAI 通路上可能零断言静默通过,且与 DashSc 通路语义相反 @
rtp_llm/test/smoke/openai_comparer.py:230- 建议:把两个静默
return改为与DashGrpcComparer对齐的 fail-closed(缺约束抛VALID_FAILED,流式显式抛「暂不支持」或实现聚合后校验),校验前断言choices非空且每个 choice 都必须命中约束,并让约束来源同时覆盖generate_config字段;抽共享 helper 承载前置校验,并把 flag 改为 per-query 声明,避免同 task 内非 grammar query 一并丢失 golden 比对。
- 建议:把两个静默
- 结构化标签校验器对无 begin/end 的 tag 与缺失 schema 静默放行,且自身缺少单测 @
rtp_llm/test/smoke/grammar_constraint_validator.py:185- 建议:补一个纯 CPU
py_test:正反样本覆盖 required/enum/pattern/maxLength/minItems/additionalProperties 的违规必须抛ValueError;把空 schema、缺items、缺 begin/end 等放行分支的行为显式固化为断言或改为报错;覆盖 excludes 命中、悬空 trigger、at_least_one未满足与 reasoning 缺 end marker、suffix 未输出等失败路径;并把matched改为在remaining上统计以统一口径。
- 建议:补一个纯 CPU
- tokenizer 家族探测函数在单测中被整体 mock,决定能否启动与 vocab_type 的分派逻辑无真实覆盖 @
rtp_llm/config/test/grammar_tokenizer_info_test.py:71- 建议:为三个探测函数补不 mock 自身的参数化用例:分别构造带
mergeable_ranks、带vocab_files_names["vocab_file"]含 tiktoken 子串、带具备PieceToId/IdToPiece/vocab_size的sp_model以及三者都不满足的 fake tokenizer,断言分类结果;再补一条不 mock 的端到端用例(from rtp_llm.ops import serialize_grammar_tokenizer_info+ 小词表调用build_grammar_tokenizer_info_json,相应补:ops依赖),覆盖真实 import 解析与本 PR 新增的打包约定。
- 建议:为三个探测函数补不 mock 自身的参数化用例:分别构造带
- xgrammar_backend_cpp_test 缺少执行环境声明,与同类新测目标判据互相打架 @
rtp_llm/cpp/engine_base/grammar/test/BUILD:12- 建议:统一判据:确认 CUDA libtorch 能否在无 GPU executor 上加载,然后给
xgrammar_backend_cpp_test补齐exec_properties/env,或反向去掉兄弟目标的 GPU 约束并更新注释,避免两处判据不一致导致偶发 CI 失败。
- 建议:统一判据:确认 CUDA libtorch 能否在无 GPU executor 上加载,然后给
- MTP grammar smoke fixture 与实际 suite 接线不一致,thinking 关闭 + MTP + grammar 组合零覆盖 @
rtp_llm/test/smoke/data/model/qwen35/q_r_mtp_grammar.json:5- 建议:重命名该 fixture 以匹配实际接线(或为其补齐 MTP 参数使名称成立),并补一个「MTP + grammar + thinking OFF」的 smoke 目标;把
_comment修正为事实描述(约束通过即整体跳过 golden 比对),并把result精简为约束校验所需的最小结构,或在约束校验后追加一次「仅比对确定性字段」的比对。
- 建议:重命名该 fixture 以匹配实际接线(或为其补齐 MTP 参数使名称成立),并补一个「MTP + grammar + thinking OFF」的 smoke 目标;把
- response_format 入参收紧为 extra="forbid",历史入参形态由静默忽略变成 4xx @
rtp_llm/config/response_format.py:37- 建议:顶层两个模型改用
extra="ignore"并对被忽略键打一次限流 warning;extra_configs内的 grammar 字段先降级为 warning + 自动提升到顶层,一个版本后再改为拒绝。若确需立即forbid,请在 PR description 与文档中列出被拒绝的历史入参形态与迁移写法。
- 建议:顶层两个模型改用
- legacy structural_tag 两条入口受理结果不一致,且 json_schema 布尔/空 dict 边界未校验 @
rtp_llm/config/grammar_constraint.py:64- 建议:统一两条入口:在
normalize_grammar_value把 legacystructures/triggers真正转换为{"type":"structural_tag","format":{...}}规范形态,或按与_check_payload相同的规则在归一化阶段 fail-fast;validate_not_empty扩展为拒绝False/空 dict(或明确记录其语义),并补直传 legacy 形态、json_schema=False、json_schema={}三条用例锁定行为。
- 建议:统一两条入口:在
- MultiSeqLogitsProcessor 写 EOS 掩码缺少 vocab 边界校验,存在堆越界写风险 @
rtp_llm/cpp/models/logits_processor/MultiSeqLogitsProcessor.cc:39- 建议:在循环前补
RTP_LLM_CHECK_WITH_INFO(eos_token_id_ >= 0 && (size_t)eos_token_id_ < vocab_size, ...),或复用 grammar 侧GRAMMAR_EOS_OUT_OF_VOCAB的语义返回ErrorInfo;并补一条 eos 越界/为负时被拒绝、不发生越界写的单测。
- 建议:在循环前补
- applySpecVerifyResult 裸指针写入前未校验 accept_tokens 形状 @
rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:106- 建议:在写入前补
RTP_LLM_CHECK_WITH_INFO(accept_tokens[i].dim() == 2 && accept_tokens[i].is_contiguous() && accept_tokens[i].size(1) == old_len, ...),把下游隐含不变量前移到实际写入点。
- 建议:在写入前补
- GrammarConfig::setstate 靠运行时类型嗅探分派 4 种布局,含两条生产不可达分支 @
rtp_llm/cpp/pybind/ConfigInit.cc:1347- 建议:删除两条生产不可达分支,仅保留 legacy(
t[0]为 str)与 current 两条路径并恢复单一长度校验,同步删除对应用例;若需长期兼容,改为在元组首位放显式 version tag 并按 version 分派——后续任一字段类型变化都会让嗅探静默走错分支并写入错误配置。
- 建议:删除两条生产不可达分支,仅保留 legacy(
- finalize 之后 update() 仍可改写 grammar 字段,不会重新编译且报错信息误导 @
rtp_llm/config/generate_config.py:487- 建议:在
update()/update_and_pop()中检测「已 finalize(_reasoning_envelope_applied)且本次更新触及GRAMMAR_FIELD_NAMES或in_think_mode」,抛出信息明确的错误(提示 grammar 字段必须在 finalize 之前设置)或显式重置标志并重新编译;并把 docstring 的口头契约补成断言,配两条用例分别覆盖「finalize 后改写 grammar 字段」与「finalize 后仅改写非 grammar 字段」。
- 建议:在
- 单一 grammar 约束规则存在三份重复实现,存在行为漂移风险 @
rtp_llm/config/response_format_compiler.py:140- 建议:Python 两处合并为一个入口(compiler 直接复用
GrammarConstraint.resolve_from_config);C++ 侧保留为最后防线,但把错误码与文案与 Python 对齐,并在注释中说明「此为 defense-in-depth,规范校验在 Python 入口」。
- 建议:Python 两处合并为一个入口(compiler 直接复用
- grammar 编译位于请求准入同步路径,成功路径仅 DEBUG 日志且无耗时指标 @
rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:46- 建议:为编译耗时、缓存命中/未命中、编译后内存大小上报 metrics(复用现有
metrics_reporter_体系),并把成功日志改为按耗时阈值提级(例如超过阈值打 INFO/WARN),使慢 schema 在生产可被发现。
- 建议:为编译耗时、缓存命中/未命中、编译后内存大小上报 metrics(复用现有
- logits processor 文档未同步,ThinkModeLogitsProcessor 与 max_batch_size 形参成为死代码 @
docs/backend/logits_processor.md:8- 建议:补
GrammarLogitsProcessor小节(约束来源、MTP verify 流程、失败即 fail-closed 到 EOS 的语义、支持平台),把 think 约束说明改为指向response_format_compiler.py生成的 structural_tag,并更新源文件清单与 zh_CN po;同时二选一并明示:删除ThinkModeLogitsProcessor及其单测/BUILD 条目与max_batch_size形参,或在头文件标注「已由 grammar 取代、仅保留待删除」并给出删除时间点。
- 建议:补
P3
- processor 与 stream 长度不一致这一内部不变量被归入 UNKNOWN_ERROR @
rtp_llm/cpp/engine_base/stream/GenerateStream.cc:964- 建议:为该不变量分配专用错误码(如
GRAMMAR_PROCESSOR_STATE_MISMATCH,或复用EXECUTION_EXCEPTION),并在 PythonExceptionType侧同步声明与 category;至少在返回前打一条带 stream id 与 processor index 的 ERROR 日志并上报独立指标,使该失败可被单独观测。
- 建议:为该不变量分配专用错误码(如
- prepareSpeculative 未做 matcher_ 空指针防护,与同类方法契约不一致 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:377- 建议:统一契约:若 matcher 必须非空,在构造函数中
RTP_LLM_CHECK_WITH_INFO(matcher_ != nullptr, ...)并删除其余方法的判空;否则在prepareSpeculative中补 nullptr 分支并返回明确ErrorInfo。
- 建议:统一契约:若 matcher 必须非空,在构造函数中
- 语法处理器未强校验增量 token 布局,full-position 张量会静默污染解析器 @
rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:349- 建议:将断言收紧为
new_tokens.size(1) == num_new_tokens(或返回ErrorInfo),把「本 processor 只接受增量布局」变成显式契约,并补一条stride > num_new_tokens被拒绝的用例。
- 建议:将断言收紧为
- max_rollback_tokens 硬编码 200,与实际回滚需求量级不匹配 @
rtp_llm/cpp/engine_base/grammar/RtpGrammarMatcher.h:19- 建议:由
propose_step(或GrammarConfig新增字段)推导max_rollback_tokens,非投机场景可取 0/1;若确需保守余量,把 200 提为具名常量并注明推导依据。
- 建议:由
- max_thinking_tokens=0 仍会生成 any_text.max_tokens=0 的退化 reasoning 包裹 @
rtp_llm/config/response_format_compiler.py:92- 建议:在
compile中显式处理边界:max_thinking_tokens <= 0时按「不启用 reasoning 包裹」处理(等价in_think_mode=False分支),或抛带明确信息的ERROR_INPUT_FORMAT_ERROR;并补max_thinking_tokens=0的参数化用例。
- 建议:在
- RPC 边界新增的失败路径与 bool 取值缺少测试覆盖,且存在恒真断言 @
rtp_llm/cpp/model_rpc/test/model_rpc_client_test.py:173- 建议:补三条用例:不可序列化
structural_tag(含 set/自引用)断言抛FtRuntimeException且消息含字段名;未 finalize 的response_format经trans_input被拒绝;json_schema=True/False→ pb 值"true"/"false"。同时把恒真断言移出 subTest 循环,或改为断言「该服务级策略不得下沉为请求级 proto 字段」这一真实意图。
- 建议:补三条用例:不可序列化
- 大量与 grammar 功能无关的格式化重排混入本次改动 @
rtp_llm/test/smoke/case_runner.py:383- 建议:把纯格式化重排拆成独立的 formatting-only commit(或独立 PR),使功能 diff 可被单独审阅与回滚;
RecommendationLogitsProcessorTest.cc这类大文件重排尤其建议单独提交,并在 PR description 中列出「仅格式化」的文件清单,同时确认其断言现代化未改变任何用例语义。
- 建议:把纯格式化重排拆成独立的 formatting-only commit(或独立 PR),使功能 diff 可被单独审阅与回滚;
Checklist Violations (14 fail / 58 total)
General Principles Checklist
- [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue
//rtp_llm:ops 无条件 data 依赖新 so,把 xgrammar 编译放大到全仓 Python 目标
py_library(name="ops")新增data = ["//:th_grammar_tokenizer_info"](:195-197),而该cc_binary经grammar_tokenizer_info_python → xgrammar_tokenizer_info → @xgrammar//:xgrammar拉入整份 xgrammar C++ 编译。凡依赖:ops的 Python 目标(含纯前端、纯 CPU 单测)都被动承担该构建成本,而rtp_llm/ops/__init__.py:193-201的serialize_grammar_tokenizer_info本身是函数内惰性导入。仓库既有约定是消费方自行声明 so:本 PR 的config_pickle_test与grammar_tokenizer_info_test都各自显式写了所需data。 - [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue
response_format 入参收紧为 extra="forbid",历史入参形态由静默忽略变成 4xx
ResponseFormat(:37)与ResponseFormatJSONSchema(:20)均设extra="forbid";api_datatype.py:179的字段类型由Optional[Union[ResponseFormat, str, Dict[str, Any]]]收窄为Optional[ResponseFormat];同时新增_reject_nested_structured_output_fields(:205-217),凡extra_configs中出现response_format/json_format/任一 grammar 字段即抛 ValueError。改前任意 dict 可透传、非法 JSON 字符串仅告警、extra_configs={"json_schema":...}是可用路径;改后{"type":"json_object","schema":{...}}这类 vLLM/SGLang 风格入参、response_format="not-json"与全部 extra_ - [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue
进程级 static grammar backend 生命周期未绑定 executor,warm-up 阶段被重复重建
grammarBackend()返回函数内static std::shared_ptr<XGrammarBackend>,init()(:79)对其做无同步赋值,注释自述「Do not use this factory for multiple engines with different tokenizers in one process without first moving this state to the engine/executor」。NormalExecutor.cc:129与MtpExecutor.cc:328均在构造函数中无条件调用init(),而NormalEngine的 prefill/decode warm-up 各自new NormalExecutor(..., warm_up=true)(NormalEngine.cc:248、279)后立即替换,正式 executor 再建一次,因此单次启动会执行 2~3 次XGrammarBackend::create,每次都要 `DeserializeJSON - [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue
xgrammar 依赖绕开仓库统一入口,并钉在上游未合并的特性分支 commit 上
仓库已有@rtp_deps//:git.bzl的git_deps()作为全部 git 三方依赖的唯一入口(WORKSPACE:27-29,含带 build_file overlay 的 cutlass 系列),本 PR 另建3rdparty/xgrammar/repositories.bzl并在 WORKSPACE:31-33 单独 load+调用,依赖声明入口一分为二。该new_git_repository直接从公网拉一个 commit,注释标明来自未合入主干的特性分支:分支被 force-push 或删除后构建不可复现;未提供sha256/shallow_since,既无完整性校验也导致全量 clone。同 PR 的3rdparty/dlpack/BUILD:3-6以「上游用 submodule 拉 dlpack、构建环境不可达」为由 vendoring,与此处直接 clone 同域仓库并显式init_submodules = False的事实矛盾,vendored 目录也无 LICENSE。 - [6.1] Quality — Commit 原子、message 与行为匹配 → issue
大量与 grammar 功能无关的格式化重排混入本次改动
case_runner.py约 460 行 diff 中真实逻辑变更只有三处:__init__增加self.remote_kvcm_server初始化(修finally路径 AttributeError)、去掉多余的str(str(...))嵌套、以及if task_info.grammar_constraint_only: q_r["grammar_constraint_only"] = True(:383-384);其余全为 black 风格重排。同样情况出现在dash_grpc_comparer.py、openai_comparer.py、model_rpc_client_test.py、dash_sc/test/*.py与RecommendationLogitsProcessorTest.cc(已核对其 diff 起始段落,为纯 clang-format 参数对齐重排),使本已 133 文件的 PR 更难审阅,也让 review 无法逐行确认重排前后等价。 - [6.1] Quality — Mega-PR 已拆分为独立变更 → issue
大量与 grammar 功能无关的格式化重排混入本次改动
case_runner.py约 460 行 diff 中真实逻辑变更只有三处:__init__增加self.remote_kvcm_server初始化(修finally路径 AttributeError)、去掉多余的str(str(...))嵌套、以及if task_info.grammar_constraint_only: q_r["grammar_constraint_only"] = True(:383-384);其余全为 black 风格重排。同样情况出现在dash_grpc_comparer.py、openai_comparer.py、model_rpc_client_test.py、dash_sc/test/*.py与RecommendationLogitsProcessorTest.cc(已核对其 diff 起始段落,为纯 clang-format 参数对齐重排),使本已 133 文件的 PR 更难审阅,也让 review 无法逐行确认重排前后等价。 - [6.1] Quality — 逻辑变更未混入无关格式化 → issue
大量与 grammar 功能无关的格式化重排混入本次改动
case_runner.py约 460 行 diff 中真实逻辑变更只有三处:__init__增加self.remote_kvcm_server初始化(修finally路径 AttributeError)、去掉多余的str(str(...))嵌套、以及if task_info.grammar_constraint_only: q_r["grammar_constraint_only"] = True(:383-384);其余全为 black 风格重排。同样情况出现在dash_grpc_comparer.py、openai_comparer.py、model_rpc_client_test.py、dash_sc/test/*.py与RecommendationLogitsProcessorTest.cc(已核对其 diff 起始段落,为纯 clang-format 参数对齐重排),使本已 133 文件的 PR 更难审阅,也让 review 无法逐行确认重排前后等价。 - [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue
单一 grammar 约束规则存在三份重复实现,存在行为漂移风险
「四个 grammar 字段最多只能设一个」这条规则被实现三次:_resolve_final_constraint(response_format_compiler.py:140-145,抛UNSUPPORTED_OPERATION)、GrammarConstraint.resolve_from_config(grammar_constraint.py:150-155,同类型同文案)、以及 C++ 侧keyFromGenerateConfig(LogitsProcessorFactory.cc:39-46,返回INVALID_PARAMS且错误文本格式不同)。三处的字段遍历顺序、错误码与文案各自维护,任一处新增字段或调整语义都需要同步修改三个位置,容易出现「Python 放行、C++ 拒绝」或错误码不一致的漂移。 - [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue
xgrammar_backend_cpp_test 缺少执行环境声明,与同类新测目标判据互相打架
该cc_test(:12-20)依赖grammar_cpp_lib,后者经torch_deps()链接 CUDA 版 libtorch,但目标既无env也无exec_properties。而本 PR 新增的同类目标全部声明了 GPU 资源,其中speculative_sampler_test注释明确写「CPU-only logic but normal_engine links libtorch_cuda; H20 host needed for loader」。两者依赖形态相同而结论相反:若加载器确需 GPU 主机,该测试在 CPU executor 上会整体失败或抖动;若不需要,则兄弟目标白占稀缺 GPU 名额。 - [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue
RPC 边界新增的失败路径与 bool 取值缺少测试覆盖,且存在恒真断言
本 PR 在 RPC 边界引入三处新行为:json.dumps失败转FtRuntimeException(ERROR_INPUT_FORMAT_ERROR)(model_rpc_client.py:73-79)、validate_engine_ready硬拒绝未 finalize 的response_format(:122)、以及json_schema支持 bool 取值(grammar_constraint.py:45);而test_trans_input_writes_typed_grammar_fields_consistently的 4 个 subTest 全为 str/dict 成功路径,三条均无用例。此外 :204-211 对response_format/grammar_terminate_without_stop_token不在 proto 中的assertNotIn断言恒真,且在 4 个 subTest 内重复执行。 - [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue
RPC 边界新增的失败路径与 bool 取值缺少测试覆盖,且存在恒真断言
本 PR 在 RPC 边界引入三处新行为:json.dumps失败转FtRuntimeException(ERROR_INPUT_FORMAT_ERROR)(model_rpc_client.py:73-79)、validate_engine_ready硬拒绝未 finalize 的response_format(:122)、以及json_schema支持 bool 取值(grammar_constraint.py:45);而test_trans_input_writes_typed_grammar_fields_consistently的 4 个 subTest 全为 str/dict 成功路径,三条均无用例。此外 :204-211 对response_format/grammar_terminate_without_stop_token不在 proto 中的assertNotIn断言恒真,且在 4 个 subTest 内重复执行。
RTP-LLM Checklist
- [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue
logits processor 文档未同步,ThinkModeLogitsProcessor 与 max_batch_size 形参成为死代码
本 PR 修改了该文档(第 68 行新增 MTP 与 Tree Decode 互斥说明),但第 8 行的「Built-in processors」仍声称ThinkModeLogitsProcessor通过max_thinking_tokens约束 think 段落,而工厂已不再创建它;核心新增件GrammarLogitsProcessor、SpecLogitsVerifyRunner与新的mtpCapability协商契约在文档中完全缺失,第 80 行起的「Related source files」也未列出rtp_llm/cpp/engine_base/grammar/**;第 19 行仍称 Tree 支持 beam/num_return_sequences,未提示与 MTP 的新互斥关系。代码侧ThinkModeLogitsProcessor.{h,cc}现仅被自身单测/BUILD/注释引用,createLogitsProcessors的max_batch_size形参(:86)也已完全未被使用。 - [I] 代码质量 — 同一功能用统一工具函数 → issue
单一 grammar 约束规则存在三份重复实现,存在行为漂移风险
「四个 grammar 字段最多只能设一个」这条规则被实现三次:_resolve_final_constraint(response_format_compiler.py:140-145,抛UNSUPPORTED_OPERATION)、GrammarConstraint.resolve_from_config(grammar_constraint.py:150-155,同类型同文案)、以及 C++ 侧keyFromGenerateConfig(LogitsProcessorFactory.cc:39-46,返回INVALID_PARAMS且错误文本格式不同)。三处的字段遍历顺序、错误码与文案各自维护,任一处新增字段或调整语义都需要同步修改三个位置,容易出现「Python 放行、C++ 拒绝」或错误码不一致的漂移。
Python Static-First Checklist
- [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue
tokenizer 家族探测函数在单测中被整体 mock,决定能否启动与 vocab_type 的分派逻辑无真实覆盖
build_tokenizer_info辅助方法(:68-93)用mock.patch.dict(sys.modules, {"rtp_llm.ops": fake_ops})替换整个 ops 模块,并同时 mock_is_fast_tokenizer/_is_tiktoken_tokenizer/_is_sentencepiece_tokenizer/_is_byte_level_tokenizer,因此全部test_build_tokenizer_info_*只验证「分派结果 → metadata JSON」映射,连..._rejects_unsupported_tokenizer也是在四个探测全被 mock 为 False 的前提下断言。除_is_byte_level_tokenizer外,另三条探测(按type().__module__、按 vocab 文件名含子串、按sp_model/tokenizer.sp_model/tok依次探测)零覆盖,而它们现在直接决定服务能否启动与 `vocab_typ
Strengths
- 错误传播端到端闭环:
process/updateStatus返回std::optional<ErrorInfo>→SamplerOutput.processor_errors→StreamUpdateInfo.error_info(MTP 侧另有SpeculativeSamplerOutput.processor_errors),GenerateStream::update/specUpdate在发布 token 前先报错返回,出错 token 不外泄。 - MTP 投机验证状态机纪律扎实:
prepareSpecMask以provisional_accepts精确记账并在返回前rollback,回滚失败升级为 fail-closed(markFinished()+ 仅放行 EOS),保证「验证绝不污染已提交状态」(GrammarLogitsProcessor.cc:129-142)。 - 多 token 提交具备全批次原子性:
acceptCommittedLocked以numAcceptedTokens()差值闭包同时回退 matcher 与committed_output_len_,不残留半提交前缀;并刻意区分 TERMINATED 与 FINISHED,覆盖min_new_tokens/ignore_eos延迟结束时仍只放行 EOS 的场景(:423-437 有注释说明理由)。 - 词表边界双向收口:
validateVocabSize拒绝 grammar vocab 超过 logits vocab,clearBitmaskTokenRange与 decode 侧 tailfill_(neg_inf)共同保证 grammar 词表之外 token 永不可采样;bitmaskAllowsToken对负数与越界一律判否。 - 约束归一化收口到单一 owner:
GrammarConstraint以Literal+get_args派生GRAMMAR_FIELD_NAMES(单一来源),序列化只在_trans_jsonable_option发生一次,并把TypeError/ValueError/RecursionError转为带字段名的ERROR_INPUT_FORMAT_ERROR且raise ... from e保留异常链。 validate_engine_ready是只读断言(response_format_compiler.py:192-206),配合model_rpc_client_test.py的model_dump()前后对比,把「RPC 边界不得改写 grammar」固化为可测不变量。- 新增
validateLogitsProcessorState()用committedOutputLen()与outputTokenLen()校验,把「processor 状态机与 stream 历史脱节」从静默错配转为显式错误,并有对应单测。 - 错误码跨语言逐一对齐:
ErrorCode.h608-613 与exceptions.py取值、命名一致,使ExceptionType(error_details.error_code)不会因未知码抛 ValueError。 - 单测以真实 xgrammar 后端而非 mock 驱动:
GrammarLogitsProcessorTest覆盖 all-true mask、终态后仅 EOS、跨 32 位字边界、cap=0、整批回滚、reasoning budget 强制收尾;packed mask 同时提供 CUDA 与 CPU 两套等价断言,并把边界用例从manual大套件拆成可跑目标。 - 构建分层与打包链路完整:xgrammar overlay 拆为 public headers / private internal headers / impl 三层,
libth_grammar_tokenizer_info.so由cc_binary → copy_all_so → whl_package_libs → py_library.data闭合;GrammarConfig删字段时提供了多版本 pickle 迁移路径。 - 可观测性取舍良好:
GrammarConfig::to_string()/__repr__只输出tokenizer_info_json_size,避免整份词表进日志;RTP_LLM_LOG_DEBUG宏先判级别再求值参数,热路径无额外 D2H 成本。
| @@ -136,7 +136,6 @@ message GenerateConfigPB { | |||
| google.protobuf.StringValue regex = 70; | |||
| google.protobuf.StringValue ebnf = 71; | |||
| google.protobuf.StringValue structural_tag = 72; | |||
There was a problem hiding this comment.
[P1] proto 字段 73 response_format 删除未加 reserved,混版滚动升级静默丢弃结构化约束
GenerateConfigPB 现最大字段号为 72(structural_tag),diff 删除了 StringValue response_format = 73; 却未补 reserved 73。同文件删字段一律保留占位(103-104、324-325、444-445、620-622、662-663),317 行注释明确要求混合版本部署下保留字段号。QueryConverter.cc 的 TRANS_OPTIONAL(response_format) 同批删除,说明引擎此前确实消费该字段。灰度期旧 frontend → 新引擎时 73 成为 unknown field 被静默丢弃:约束消失但请求 200 返回,无日志无指标;编号被后续以别的类型复用还会造成类型错配。
建议: 补 reserved 73; 与 reserved "response_format";(model_rpc_client_test.py 的 assertNotIn(..., fields_by_name) 在 reserved 下仍成立)。同时在 QueryConverter::transGenerateConfig 检测该 unknown field,读到非空值即返回 ERROR_INPUT_FORMAT_ERROR 并打 warning + 指标,把「静默丢约束」改成可观测的 fail-fast;并在 PR description 写明 frontend 与 engine 的升级顺序要求。
| default="xgrammar", | ||
| help="Grammar backend type: xgrammar or none", | ||
| ) | ||
| grammar_group.add_argument( |
There was a problem hiding this comment.
[P1] 删除 --grammar_backend / GRAMMAR_BACKEND,存量启动脚本硬失败且移除唯一的 grammar 关停开关
init_grammar_group_args 现仅注册 4 个参数,--grammar_backend(原 env_name="GRAMMAR_BACKEND"、默认 xgrammar、help "xgrammar or none")与 ConfigModules.h:374-383 中的字段一并删除,全仓仅剩 ConfigInit.cc:1354 一处历史注释。两个后果:server_args.py 最终走严格 argparse,沿用 --grammar_backend xgrammar 的启动脚本 SystemExit,服务起不来;纯 env 部署更隐蔽——GRAMMAR_BACKEND=none 被静默忽略,语义由「关闭约束」反转为「恒开」,而 model_factory.py:212 已无条件构建元信息,无任何回滚手段。
建议: 二选一:(a) 保留 --grammar_backend 为 deprecated alias,接受 xgrammar/none 并打 WARN,none 映射为「不注入 tokenizer_info_json」,复用 XGrammarBackend::create 已有的 empty → nullptr 关闭路径;(b) 新增等价的 --grammar_enable(默认 true)作为显式 kill switch,并让启动期元信息构建失败可经该开关降级。两者都需在 PR description/文档列出被删 CLI/env 与迁移方式,并补一条断言旧参数行为的回归测试。
| encoded_vocab, json.dumps(metadata, separators=(",", ":")) | ||
| ) | ||
|
|
||
| raise ValueError(f"Unsupported tokenizer type: {type(tokenizer)}") |
There was a problem hiding this comment.
[P1] grammar tokenizer 元信息成为所有语言模型的启动硬依赖,不支持的 tokenizer 直接阻塞上线且无逃生口
model_factory.py:212-215 对所有 TaskType.LANGUAGE_MODEL 无条件调用 model.build_grammar_tokenizer_info();base_model.py:319-328 仅在 get_real_tokenizer() 为 None 时 return "",其余任何异常都包装成 RuntimeError 终止启动。失败点包括 :195 Unsupported tokenizer type(非 HF fast / tiktoken / sentencepiece 的自研或 slow tokenizer)、:159 stop_token_ids cannot be empty(_collect_tokenizer_info_stop_token_ids 在 eos 为负且无单 token stop word 时返回空表)、:11 tokenizer vocab is empty。从不使用结构化输出的模型也可能完全无法上线,且同 PR 已删除唯一的关闭开关。
建议: 区分「grammar 能力缺失」与「启动失败」:构建失败时记 error 并把 tokenizer_info_json 置空(与 real_tokenizer is None 分支一致),由首个 grammar 请求返回明确的 UNSUPPORTED_OPERATION(GRAMMAR_* 码已具备)。若坚持启动期 fail-fast(注释已声明为有意设计),必须同时提供运维旁路开关,并为「不支持的 tokenizer」「空 stop_token_ids」两条路径补启动期集成测试与受影响 model/tokenizer 清单。
| ) -> "ResponseFormatPlan": | ||
| final_constraint = _resolve_final_constraint(config) | ||
|
|
||
| if config.in_think_mode: |
There was a problem hiding this comment.
[P1] think 模式无条件改为 grammar 约束解码,thinking + beam search / num_return_sequences>1 由可用变 400
ResponseFormatPlan.compile 仅以 config.in_think_mode 为闸门:为真时无条件构造 structural_tag reasoning envelope(用户未提结构化输出时 final_format 退化为 {"type":"any_text"},:84-96),并在 :72-78 对 has_num_beams() or num_return_sequences > 1 抛 ERROR_INPUT_FORMAT_ERROR。改前该冲突判断只覆盖存在 grammar 约束的情况(现 :98-105 分支),think + 多序列原本可用。add_thinking_params(generate_config.py:611)末尾无条件 finalize,in_think_mode 默认即 bool(think_mode),故 THINK_MODE 集群每请求都要付 xgrammar 编译与 bitmask 成本;backend 关闭时 LogitsProcessorFactory.cc:107 会让...
建议: 把 reasoning envelope 与 in_think_mode 解耦:仅在存在最终约束或显式开启 reasoning 约束时才包裹,其余 think 请求维持原非 grammar 路径;若确需全量包裹,请为 beam / 多返回序列补降级分支(退回非 grammar think 而非直接 400),并在 backend 关闭时于启动期显式告警或拒绝。同时在 docs/backend/logits_processor.md 与 PR description 标注该行为变更、互斥关系与全量 grammar 的吞吐实测数据。
| GrammarKeyCpp grammar_key = std::move(grammar_key_result.value()); | ||
|
|
||
| // Thinking constraints reach the execution layer as normalized grammar. | ||
| // Thinking-only configs are not supported here; do not add a legacy processor fallback. |
There was a problem hiding this comment.
[P1] in_think_mode / max_thinking_tokens 仍在 wire 上,但引擎侧已无任何执行者,退化为静默 no-op
createLogitsProcessors 删除了 ThinkModeLogitsProcessor::fromGenerateInput 调用,注释声明「Thinking-only configs are not supported here; do not add a legacy processor fallback」。但 proto 93-94 行仍保留 in_think_mode = 32 / max_thinking_tokens = 33,QueryConverter.cc:106-107 仍写入 GenerateConfig,全 rtp_llm/cpp 对二者的唯一使用是 :113-116 的一行 DEBUG 日志;全仓搜索确认 ThinkModeLogitsProcessor 只被自身单测、BUILD 与文档引用。任何不经新 Python 编译链路的 gRPC 客户端设置 think 预算后将被完全忽略,无错误、无日志、无指标。
建议: 明确该组合的语义并落实:对「in_think_mode=true 且四个 grammar 字段全空」的请求在 createLogitsProcessors 或 validate_engine_ready 显式返回 UNSUPPORTED_OPERATION(至少 WARNING + 指标),把静默降级变为可观测;若确定这两个字段不再驱动引擎行为,请在 proto 注释标注 deprecated 并说明已由 structural_tag 承载,同时删除 ThinkModeLogitsProcessor.{h,cc}、其单测与 BUILD 条目及 createLogitsProcessors 未使用的 max_batch_size 形参。
| } | ||
| RTP_LLM_CHECK(new_tokens.dim() == 2); | ||
| RTP_LLM_CHECK(new_tokens.scalar_type() == torch::kInt32); | ||
| RTP_LLM_CHECK(new_tokens.size(1) >= num_new_tokens); |
There was a problem hiding this comment.
[P3] 语法处理器未强校验增量 token 布局,full-position 张量会静默污染解析器
updateStatus 仅断言 new_tokens.size(1) >= num_new_tokens,随后从 data_ptr<int32_t>() 起始位置连续读取 n 个 token(:358-361)。同仓 RecommendationLogitsProcessor.cc:350-356 以 use_token_offset = stride > num_new_tokens 明确支持 full-position 布局并注释说明「两种布局都存在」。当前仅靠工厂拒绝 beam/num_return_sequences>1 间接排除该输入;一旦上游改用 full-position 调用,prompt 前缀会被当作生成 token 提交给解析器,而 committed_output_len_ 仍按 n 递增,parity 校验也发现不了。
建议: 将断言收紧为 new_tokens.size(1) == num_new_tokens(或返回 ErrorInfo),把「本 processor 只接受增量布局」变成显式契约,并补一条 stride > num_new_tokens 被拒绝的用例。
| public: | ||
| RtpGrammarMatcher(std::shared_ptr<xgrammar::CompiledGrammar> compiled, | ||
| bool terminate_without_stop_token = false, | ||
| int max_rollback_tokens = 200); |
There was a problem hiding this comment.
[P3] max_rollback_tokens 硬编码 200,与实际回滚需求量级不匹配
构造参数默认 max_rollback_tokens = 200(:19)并落为成员默认值(:49),XGrammarBackend::createMatcher(XGrammarBackend.cc:180)只传 terminate_without_stop_token,从不覆盖该默认值。实际回滚上界很小且可静态推导:prepareSpecMask 最多回滚 propose_step,acceptCommittedLocked 最多回滚 propose_step + 1(MTP 常见 1~4)。xgrammar 会按该上限保留回滚历史,等于为每条并发 grammar 流常驻远超需要的解析栈快照,且无配置项可调。
建议: 由 propose_step(或 GrammarConfig 新增字段)推导 max_rollback_tokens,非投机场景可取 0/1;若确需保守余量,把 200 提为具名常量并注明推导依据。
| engine_constraint = GrammarConstraint( | ||
| "structural_tag", | ||
| _reasoning_envelope( | ||
| reasoning_format, |
There was a problem hiding this comment.
[P3] max_thinking_tokens=0 仍会生成 any_text.max_tokens=0 的退化 reasoning 包裹
compile 在 in_think_mode 为真时把 config.max_thinking_tokens 原样传入 _reasoning_envelope,最终落到 {"type":"any_text","max_tokens": max_thinking_tokens}(:35-44),无非正值校验。validate()(generate_config.py:741-745)虽在 think 模式下检查 is_positive_integer(max_thinking_tokens),但 utils/check_util.py:1-2 中该 helper 实为 value >= 0,0 可通过。OpenAI 入口的 disable_thinking() 覆盖了部分场景,而 raw pipeline 路径不传 enable_thinking,THINK_MODE=1 下显式传 max_thinking_tokens: 0 会得到 max_tokens: 0 的退化 grammar 而非「不开启思考」。
建议: 在 compile 中显式处理边界:max_thinking_tokens <= 0 时按「不启用 reasoning 包裹」处理(等价 in_think_mode=False 分支),或抛带明确信息的 ERROR_INPUT_FORMAT_ERROR;并补 max_thinking_tokens=0 的参数化用例。
| self.assertEqual( | ||
| generate_config_pb.structural_tag.value, | ||
| '{"format":{"type":"json_schema"}}', | ||
| def test_trans_input_writes_typed_grammar_fields_consistently(self): |
There was a problem hiding this comment.
[P3] RPC 边界新增的失败路径与 bool 取值缺少测试覆盖,且存在恒真断言
本 PR 在 RPC 边界引入三处新行为:json.dumps 失败转 FtRuntimeException(ERROR_INPUT_FORMAT_ERROR)(model_rpc_client.py:73-79)、validate_engine_ready 硬拒绝未 finalize 的 response_format(:122)、以及 json_schema 支持 bool 取值(grammar_constraint.py:45);而 test_trans_input_writes_typed_grammar_fields_consistently 的 4 个 subTest 全为 str/dict 成功路径,三条均无用例。此外 :204-211 对 response_format/grammar_terminate_without_stop_token 不在 proto 中的 assertNotIn 断言恒真,且在 4 个 subTest 内重复执行。
建议: 补三条用例:不可序列化 structural_tag(含 set/自引用)断言抛 FtRuntimeException 且消息含字段名;未 finalize 的 response_format 经 trans_input 被拒绝;json_schema=True/False → pb 值 "true"/"false"。同时把恒真断言移出 subTest 循环,或改为断言「该服务级策略不得下沉为请求级 proto 字段」这一真实意图。
Checklist: [6.1] 新逻辑有聚焦单测 + 相关集成/smoke 测试;[6.1] 边界 case 覆盖(空、单元素、最大值)
| # comparers never see these fields so this is a no-op for them. | ||
| q_r["_model_path"] = task_info.tokenizer_path or task_info.model_path | ||
| q_r["_model_type"] = task_info.model_type | ||
| if task_info.grammar_constraint_only: |
There was a problem hiding this comment.
[P3] 大量与 grammar 功能无关的格式化重排混入本次改动
case_runner.py 约 460 行 diff 中真实逻辑变更只有三处:__init__ 增加 self.remote_kvcm_server 初始化(修 finally 路径 AttributeError)、去掉多余的 str(str(...)) 嵌套、以及 if task_info.grammar_constraint_only: q_r["grammar_constraint_only"] = True(:383-384);其余全为 black 风格重排。同样情况出现在 dash_grpc_comparer.py、openai_comparer.py、model_rpc_client_test.py、dash_sc/test/*.py 与 RecommendationLogitsProcessorTest.cc(已核对其 diff 起始段落,为纯 clang-format 参数对齐重排),使本已 133 文件的 PR 更难审阅,也让 review 无法逐行确认重排前后等价。
建议: 把纯格式化重排拆成独立的 formatting-only commit(或独立 PR),使功能 diff 可被单独审阅与回滚;RecommendationLogitsProcessorTest.cc 这类大文件重排尤其建议单独提交,并在 PR description 中列出「仅格式化」的文件清单,同时确认其断言现代化未改变任何用例语义。
Checklist: [6.1] Commit 原子、message 与行为匹配;[6.1] Mega-PR 已拆分为独立变更;[6.1] 逻辑变更未混入无关格式化
No description provided.