Skip to content

Feature/grammar logits processor - #1006

Merged
LLLLKKKK merged 20 commits into
mainfrom
feature/grammar_logits_processor
Aug 6, 2026
Merged

Feature/grammar logits processor#1006
LLLLKKKK merged 20 commits into
mainfrom
feature/grammar_logits_processor

Conversation

@wanglining97

Copy link
Copy Markdown
Collaborator

No description provided.

@wanglining97
wanglining97 requested a review from LLLLKKKK as a code owner May 15, 2026 07:51
@LLLLKKKK

Copy link
Copy Markdown
Collaborator

AI Code Review - PR #1006

Status: LGTM

Summary: P0/0 · P1/0 · P2/0 · P3/0

lgtm ready to ci

Checklist ✅ (56 items passed)

Strengths

  • 增量改动把 GenerateStream 对 GrammarLogitsProcessor 的具体依赖降为 BaseLogitsProcessor 接口,降低了 stream 与 grammar 模块的循环依赖风险。
  • MTP 侧仍只在确认存在 grammarMatcher 的 processor 上取 triton bitmask ops 和 acceptTokens,行为边界保持清晰。

@wanglining97
wanglining97 force-pushed the feature/grammar_logits_processor branch 2 times, most recently from af49d85 to 5264266 Compare May 28, 2026 09:29
@LLLLKKKK

This comment was marked as outdated.

@wanglining97
wanglining97 force-pushed the feature/grammar_logits_processor branch 3 times, most recently from b52f379 to fcb4f67 Compare June 2, 2026 08:52
@alibaba alibaba deleted a comment from LLLLKKKK Jun 2, 2026
@alibaba alibaba deleted a comment from LLLLKKKK Jun 2, 2026
@alibaba alibaba deleted a comment from LLLLKKKK Jun 2, 2026
@alibaba alibaba deleted a comment from LLLLKKKK Jun 2, 2026
@wanglining97
wanglining97 force-pushed the feature/grammar_logits_processor branch 5 times, most recently from 2e4e56c to b5363f3 Compare June 3, 2026 03:04
@alibaba alibaba deleted a comment from LLLLKKKK Jun 3, 2026
@alibaba alibaba deleted a comment from LLLLKKKK Jun 3, 2026
@alibaba alibaba deleted a comment from LLLLKKKK Jun 3, 2026
@wanglining97
wanglining97 force-pushed the feature/grammar_logits_processor branch 5 times, most recently from b715e53 to 4ddc021 Compare June 3, 2026 13:47
@alibaba alibaba deleted a comment from LLLLKKKK Jun 3, 2026
@wanglining97
wanglining97 force-pushed the feature/grammar_logits_processor branch 3 times, most recently from 0a8b2ae to 5f9c27f Compare June 8, 2026 06:41
@alibaba alibaba deleted a comment from LLLLKKKK Jun 10, 2026
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 LLLLKKKK left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 + 树解码开启」用例断言期望行为(启动拒绝或正常放行)。
  • 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 + 后端缺失」两条用例锁定期望错误码与文案。
  • 启动期强制构建 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 清单,便于存量模型灰度前自查。
  • 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.bzlgit_deps()。若短期必须依赖 fork,至少改为 http_archive + sha256 或内部镜像;把 xgrammar.BUILD:1 注释改为「上游版本 + fork commit + 相对上游差异」,显式记录 picojson 来源保证,并在 PR description 写明上游化计划。
  • 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 映射:kInvalidArgumentINVALID_PARAMSkResourceExhaustedMALLOC_FAILED(或既有可重试系统错误码)、其余 → EXECUTION_EXCEPTION;补一条单测断言「后端返回 ResourceExhausted 时 factory 不返回 INVALID_PARAMS」,避免上游精心构造的错误分类在边界处失效。
  • grammarBackend() 为进程级可变 static,init 与请求线程存在数据竞争且无法重置 @ rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:72
    • 建议:把后端所有权下移到 engine/executor 对象(随其生命周期创建与销毁),由 stream 通过引用获取;若短期必须保留 static,至少改为原子读写或加读写锁,并在 init() 中对「已初始化且 tokenizer_info_json 不同」的情况 fail-fast 报错,把注释里的约束变成可执行检查。
  • 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 形参,避免调用方误以为它仍影响构造。
  • 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 而非静默丢弃。两侧注释写明唯一权威计算点。
  • 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_infostop_token_idsspecial_tokens.stop_words_id_list 中的单 token 项),解码侧对集合内所有 id 放行、提交侧按集合判定;或在装配时校验「模型存在多个 eos 时拒绝安装 grammar 并给出明确原因」。补两条用例:多 EOS 模型提交非首个 EOS、min_new_tokens 未满足时终止态的期望行为(明确是等待还是报错)。
  • MultiSeqLogitsProcessor 写 EOS 掩码缺少 vocab 边界校验 @ rtp_llm/cpp/models/logits_processor/MultiSeqLogitsProcessor.cc:39
    • 建议:抽出共用的 eos 合法性校验(置于 BitmaskUtilsBaseLogitsProcessor),在 MultiSeqLogitsProcessor::process 中先判断 eos_token_id_ >= 0 && (size_t)eos_token_id_ < vocab_size,越界时返回与 GRAMMAR_EOS_OUT_OF_VOCAB 语义等价的 ErrorInfo 而不是继续写内存,并补一条越界 eos 的负向单测。
  • XGrammarBackend::create 以 RAW 重建 TokenizerInfo 并丢弃 special_token_ids,该路径零覆盖 @ rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:81
    • 建议:补直连测试:用 <0x20> 类夹具构造 BYTE_FALLBACK 与 BYTE_LEVEL 词表,分别走 serializeTokenizerInfoXGrammarBackend::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 的单测锁定选定语义。
  • 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_reasonreasoning_tokens 的区间断言(如 reasoning_tokens <= max_thinking_tokens),避免约束通过但截断 / 预算行为回退时无人发现。
  • smoke 约束校验器多处静默放行且无自身单测 @ rtp_llm/test/smoke/grammar_constraint_validator.py:236
    • 建议:补一个纯 Python 的 py_test(无需 GPU),对每个 verifier 提供正反例:空 schema、空 begin/end 的 tag、缺 requiredproperties 的 glm_xml、stop_after_first 下的尾随多余文本。把上述静默分支统一改为抛「validator 不支持该形状」的 ValueError,并把 :112 与 :236 的 schema 守卫抽成同一 helper,消除同类校验的两份实现。
  • 「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 未实际验证的覆盖面。
  • 分词器家族探测函数在单测中被整体 mock,决定「模型能否启动」的分支零覆盖 @ rtp_llm/config/test/grammar_tokenizer_info_test.py:73
    • 建议:为三个探测函数各补直接单测(最小 fake:内层暴露伪 tiktoken Encodingvocab_files_names 含 tiktoken、mergeable_ranks 存在/不存在、sp_model.PieceToId/IdToPiece/vocab_size),并覆盖「三类都不匹配」的负向分支,断言抛出的错误类型与文案。保留现有拼装用例的 mock,但不要让 mock 成为这些生产边界的唯一覆盖。
  • 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 行为。
  • PackedMaskLogits 的共用输入校验分支缺少负向与边界测试 @ rtp_llm/models_py/bindings/core/test/PackedMaskLogitsCpuFallbackTest.cc:11
    • 建议:补一组参数化负向用例,至少覆盖 vocab_size = 0vocab_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
  • 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.bzlgit_deps(),自研改动用 patches=[...] 承载(cutlass、rules_python 均如此),本 PR 另开 repositories.bzl 并在 WORKSPACE 单独 load,形成第二套注册入口。xgrammar.BUILD:1 注释写「v0.2.2」与实际 pin 的 fork commit 无法互证,且 init_submodules=False 下仍 glob 3rdparty/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-143response_format = 73[deprecated = true] 并新增 grammar_terminate_without_stop_token = 74QueryConverter.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_formatgrammar_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(env GRAMMAR_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:129MtpExecutor.cc:325 各有一处调用、模型热加载、测试串联)即为未定义行为。该 static 永不释放,持有整份 TokenizerInfo 与编译缓存直至进程退出,换 tokenizer 后仍复用旧后端。注释已承认这一约束,但代码层面没有任何断言或保护。
  • [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue SpecLogitsVerifyRunner::run 的失败语义不一致
    run() 用 try/catch 把 std::bad_allocc10::Error 转成 makeFailureResultForActiveStreams(:275-281),即降级为「只有相关流报错、同批其他流继续」。但同一函数体内的 RTP_LLM_CHECK_WITH_INFO(draft 列数不符、行数超 int32 等)抛出的异常不在 catch 列表内,会一路穿过 runSpecLogitsVerifyIfNeeded / decodeStep / MtpExecutor::process,把同批全部流一起打挂;MtpExecutorTest.ccEXPECT_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_infrtp_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 直接 continuematched 保持 0;:147 requiredproperties 皆缺时退化为空列表;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)→ prepareBitmaskfillMatcherBitmask 会走一次完整的 xgrammar FillNextTokenBitmask。但 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_workers clamp 到 ≥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:44validatePackedMaskLogitsInputs 是 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,68ThinkModeLogitsProcessorTest.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」的单测固化该不变量。
  • DecodeMaskBuilderpending_bitmask_uploads_ 显式记录 H2D event 并在复用 pinned 缓冲前 synchronize(),注释明确「正确性不得依赖后续 sampler 的 D2H 同步」——这是复用 pinned buffer 最易漏掉的点。
  • clearBitmaskTokenRange / forceTokenInBitmask 对同字、跨字、末字对齐与越界 token 分别处理,逐组手工验算正确并有专项单测;packed_mask_logits 对越界 word 取「不允许」而非越界读。
  • XGrammarBackendbad_alloc / runtime_error / 其它异常分别映射为 ResourceExhausted / InvalidArgument / Unknown,用户语法非法与系统可重试在状态码层可区分;create() 为 noexcept 并以 nullptr 表示优雅降级。
  • proto 兼容处理规范:field 73 保留并标 [deprecated = true]、注释写明投影关系、新增 field 74 而非复用旧编号(model_rpc_service.proto:139-143)。
  • GrammarConfig pickle 用 py::isinstance<py::str>(t[0]) 区分 legacy 五元组与当前布局,并有 config_pickle_test.py 覆盖当前往返、legacy 读取、伪造短 state 被拒三类场景。
  • 日志脱敏到位:GrammarConfig::to_string()GenerateConfig::debugString() 对 grammar / tokenizer 内容只输出长度,不输出原文。
  • grammar_group_args.pydefault= 改为读取 C++ grammar_config.<field>,消除 Python 与 ConfigModules.h 两份默认值漂移。
  • GrammarLogitsProcessorTest.cc 使用真实 xgrammar 后端而非 mock,覆盖 undersized bitmask、grammar vocab 超模型 vocab、越界 EOS、all-true mask 视为 no-op、整批 draft 回滚等负向场景,并断言具体 ErrorCode 与错误文案片段。

Comment thread rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc
Comment thread rtp_llm/config/response_format_builder.py Outdated
Comment thread rtp_llm/config/response_format_builder.py Outdated
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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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 清单,便于存量模型灰度前自查。

Comment thread rtp_llm/cpp/normal_engine/NormalSamplerInputGatherer.cc
Comment thread rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc Outdated
Comment thread rtp_llm/models_py/bindings/common/kernels/mask_logits.cu
auto result = makeResult(shape);
result.processor_errors = std::move(merge_result.processor_errors);
return result;
} catch (const std::bad_alloc& e) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] SpecLogitsVerifyRunner::run 的失败语义不一致

run() 用 try/catch 把 std::bad_allocc10::Error 转成 makeFailureResultForActiveStreams(:275-281),即降级为「只有相关流报错、同批其他流继续」。但同一函数体内的 RTP_LLM_CHECK_WITH_INFO(draft 列数不符、行数超 int32 等)抛出的异常不在 catch 列表内,会一路穿过 runSpecLogitsVerifyIfNeeded / decodeStep / MtpExecutor::process,把同批全部流一起打挂;MtpExecutorTest.ccEXPECT_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>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

📍 实际位置 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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

📍 实际位置 rtp_llm/cpp/models/logits_processor/test/RecommendationLogitsProcessorTest.cc:1(不在 diff 展示范围内,就近挂载)

[P3] 接口签名改造混入大量无关 clang-format 重排

该测试文件约千行 diff 绝大部分为 clang-format 重排,真正的功能性变更只有 process() 返回类型适配与掩码期望值改为 BaseLogitsProcessor::neg_infrtp_llm/test/smoke/case_runner.py 亦为同类混合。在这种规模的重排中,评审无法在合理成本内逐行排除「重排里夹带断言语义微调」的可能。

建议: 后续把纯格式化拆成独立 commit(或独立 PR)先落地,再提交语义变更,使逻辑变更的 diff 保持可审;本 PR 如不便拆分,请在 PR description 显式列出这些文件中属于语义变更的具体断言,方便评审定点复核。

Checklist: [6.1] 逻辑变更未混入无关格式化

@LLLLKKKK LLLLKKKK left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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) 累加后传给 collectStreamSamplerErrorsuccess 索引一并修正),并把 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 流量的性能回退可接受。
  • 启动期强制构建 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 为空的模型清单。
  • --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_sizenum_return_sequences>1、beam + MTP),并在 MtpExecutorTest.cc 补一条断言错误码与错误信息稳定的用例。

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-tag max_tokens 改动以 Bazel patches = [...] 形式放进 3rdparty/xgrammar/,使补丁可评审、可追溯、可回滚。若短期必须用 fork,请改为组织账号或内部镜像,并把 xgrammar.BUILD:1 的版本注释改为「上游基线 tag/commit + 本地补丁摘要 + 上游 PR 链接 + 责任人」,使构建来源可审计。
  • 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 编译放到后台线程池并带超时的做法。
  • 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.mddash_sc/think.py 注释,移除 max_batch_size 形参及调用点实参,并在 QueryConverter 对「设置了 in_think_mode/max_thinking_tokens 但四个 typed 字段全空」的请求打 WARN 或 fail-fast;(b) 若需兼容直连 gRPC 的裸 GenerateConfig,则保留创建分支,或至少在头文件/文档标注 deprecated 并写明迁移路径与删除计划。
  • GrammarConfig 保留无消费者的 tokenizer_info_json / override_stop_tokens,与 ModelConfig 同名字段混淆 @ rtp_llm/cpp/config/ConfigModules.h:377
    • 建议:删除这两个字段及其 def_readwrite、pickle 槽位、to_string() 输出、.pyiconfig_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_vocabbuild_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」三条用例。
  • 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 共用的函数,消除双份语义。
  • 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_statuserr_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 配置一致,避免后续读者据注释误判覆盖面。
  • 新增 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_info harness 可保留 mock,但至少新增一条不 mock 探测函数的端到端用例。
  • test_grammar_defaults 退化为恒真断言,且 env 通道完全无覆盖 @ rtp_llm/server/server_args/test/server_args_test.py:423
    • 建议:把 test_grammar_defaults 改为断言硬编码期望值(num_workers == 8constrained_json_disable_any_whitespace is Falsecompiler_cache_bytes == 512 * 1024 * 1024),使 C++ 默认值变更能被显式发现;并新增一条 env 用例(设置三个 env_namesys.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。
  • 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_stepvocab_size 上限)改为返回 makeFailureResultForActiveStreams(...) 的请求级错误,只对真正的进程级编程不变量保留 RTP_LLM_CHECK;或补一个 catch (const std::exception&) 兜底,并在注释中写明两类校验的分界。同时为「非法 draft 列数」补一条断言错误落到 active stream 而非抛出的用例。
  • 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_xgrammarxgrammar_tokenizer_info 及 5 个新增 cc_test 全部经 @xgrammar//:xgrammar 传导,WORKSPACE 无条件加载。init_submodules = Falsexgrammar.BUILD:223rdparty/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,22DLPACK_MAJOR_VERSION 1 / DLPACK_MINOR_VERSION 2,实为 v1.2。同时 dlpack_headers 处于 default_visibility = publicincludes = ["include"],该搜索路径会沿 xgrammar_headers → grammar_xgrammar → logits_processor 传播到大量同时链接 libtorch 的编译单元(torch 自带一份 dlpack 头)。另外 3rdparty/xgrammar/xgrammar.BUILD:38-41glob(["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),其内部 buildStateprepareBitmask 会重填整个词表的缓存 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_xgrammarxgrammar_tokenizer_info 及 5 个新增 cc_test 全部经 @xgrammar//:xgrammar 传导,WORKSPACE 无条件加载。init_submodules = Falsexgrammar.BUILD:223rdparty/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_allocc10::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_allocc10::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:8dash_sc/think.py:167 仍把它描述为在用行为。更关键的是 in_think_mode / max_thinking_tokens 仍被 QueryConverter.cc:107-108 解析、被 GenerateConfig.h:302 JSONIZE,但已无任何消费者:不经 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_jsonbase_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_jsonoverride_stop_tokens 既无 CLI/env 写入方也无任何读取点,只出现在 pybind def_readwriteConfigInit.cc:1326/1328)、pickle 元组、ConfigModules.cc:393to_string().pyi:548
  • [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue MTP 解码下每步都会做一次不会被消费的全词表 bitmask 重建
    acceptCommittedLocked 在每次提交成功后无条件调用 decode_mask_builder_->refreshAfterCommit(*matcher_, committed_output_len_)(:656),其内部 buildStateprepareBitmask 会重填整个词表的缓存 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,数据文件注释与实际消费配置不一致
    新增 suite smoke_h20_grammar_heavy 已挂入 maga_model_smoke,但两条用例的覆盖面与数据文件描述不符:qwen35_grammar_concurrent_no_mtpsmoke_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-114grammar_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_jsonbase_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_jsonoverride_stop_tokens 既无 CLI/env 写入方也无任何读取点,只出现在 pybind def_readwriteConfigInit.cc:1326/1328)、pickle 元组、ConfigModules.cc:393to_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,22DLPACK_MAJOR_VERSION 1 / DLPACK_MINOR_VERSION 2,实为 v1.2。同时 dlpack_headers 处于 default_visibility = publicincludes = ["include"],该搜索路径会沿 xgrammar_headers → grammar_xgrammar → logits_processor 传播到大量同时链接 libtorch 的编译单元(torch 自带一份 dlpack 头)。另外 3rdparty/xgrammar/xgrammar.BUILD:38-41glob(["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/33default=grammar_config.<field>)。测试随后写 expected = type(g)() 并逐字段比对(:428-435),即拿「由 C++ 默认值派生出的 argparse 默认值」与「新建 C++ 默认值」互比,必然相等;若某个 bind_to 被整体漏掉,该字段仍保持 C++ 默认值,断言依旧通过,而 docstring 声称这是 "Regression guard for the wiring"。setUpos.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 均有位级单测;compileWithErrorClassificationbad_alloc 归为 ResourceExhaustedruntime_error 归为 InvalidArgument,区分了用户可见拒绝与系统性可重试故障。
  • 提交状态被当作一个原子单元:acceptCommittedLocked 把 matcher token 数、committed_output_len_ 与缓存掩码整体提交,任一 token 失败即 rollback_commit 统一回滚,二次失败时 markFinished() 兜底;ProvisionalSpecAcceptTracker 保证 prepareSpeculative 只读不改已提交状态。
  • MTP 投机验证的行语义自洽:initializeCompactRowsrow_indices = stream_idx * (P+1) + offsetgatherSpecSamplerInput 的 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 路径读未初始化内存的隐患。
  • 约束语义在入口收敛干净:GrammarConstraintfrozen 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 回滚幂等);XGrammarBackendTeststatic_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 加载与伪造短布局被拒三条路径。

Comment thread rtp_llm/cpp/normal_engine/NormalOutputDispatcher.cc
Comment thread rtp_llm/config/response_format_builder.py Outdated
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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] 启动期强制构建 grammar tokenizer 元数据,不受支持的 tokenizer 直接启动失败且无逃生开关

_fill_grammar_tokenizer_infoload_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 为空的模型清单。

Comment thread rtp_llm/server/server_args/grammar_group_args.py
Comment thread rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc
Comment thread 3rdparty/dlpack/BUILD
Comment thread rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc Outdated
auto result = makeResult(shape);
result.processor_errors = std::move(merge_result.processor_errors);
return result;
} catch (const std::bad_alloc& e) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] SpecLogitsVerifyRunner::run 的失败语义不一致,形状校验失败会逃出 try 块

run() 只捕获 std::bad_allocc10::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_stepvocab_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>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

📍 实际位置 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();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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 LLLLKKKK left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 的用例。
  • --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 类型。
  • 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" 无法定位)。
  • 核心依赖 xgrammar 指向个人账号 fork,构建不可复现且无校验、镜像与回滚手段 @ 3rdparty/xgrammar/repositories.bzl:7
    • 建议:改为可校验、可托管的形式:优先推动 max_tokens 能力上游合并后 pin 上游 tag;短期至少改用带 sha256http_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)就把 legacy structures/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 映射对应 ErrorCodekInvalidArgument → INVALID_PARAMSkResourceExhausted → MALLOC_FAILED 或既有可重试码,kUnknown → EXECUTION_EXCEPTION),并补一条用例断言系统类失败不会被降级为 INVALID_PARAMS
  • 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」的测试。
  • 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 的影响范围。
  • ThinkModeLogitsProcessor 装配点移除后沦为死代码,think 预算在直连路径静默失效 @ rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:99
    • 建议:明确二选一并落到代码:若确认废弃,同批删除 ThinkModeLogitsProcessor.{h,cc}、BUILD 条目与单测,并同步更新 docs/backend/logits_processor.mdthink.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 载荷与索引、.pyito_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 端 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 中被屏蔽。
  • 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 映射的用例。
  • 语法编译在请求路径同步执行,无规模上限与超时,耗时仅 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:无法定位约束时按 DashGrpcComparerSmokeException;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 无耦合,可常驻单测流水线。
  • 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 家族探测函数与 native 序列化接缝在单测中被整体 mock @ rtp_llm/config/test/grammar_tokenizer_info_test.py:73
    • 建议:为 _is_tiktoken_tokenizer_is_sentencepiece_tokenizer 各补一组不 mock 自身的用例(最小 fake 对象覆盖各条探测分支与全不命中),并补一个「同时命中 fast 与 sentencepiece」的优先级用例固化 if 链顺序。另新增一条不 mock rtp_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_reasoning case:沿用 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,把新的边界契约固化在客户端侧。
  • grammar BUILD 注释声明的 Tier 依赖约束未通过 visibility 落地 @ rtp_llm/cpp/engine_base/grammar/BUILD:35
    • 建议:把 grammar_xgrammargrammar_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」。

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 路径的 FINISHEDTERMINATED 统一为强制 EOS(或至少保留 mask),使熔断语义在两条路径一致;若刻意保留 fail-open,请在 switch 分支上注释说明「FINISHED 必然伴随已上报错误、流即将终止」这一前提,并补一条断言该前提的测试。
  • MTP 掩码行索引对 streams 与 allStreams 顺序存在隐式依赖,缺少断言 @ rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:1045
    • 建议:在 runSpecLogitsVerifygatherSpecSamplerInput 入口加一条显式断言(如比对 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,或把 srcs glob 改为递归 cpp/**/*.cc 后再保留该 exclude 以表达真实意图。
  • GrammarConfig pickle 仅靠 t[0] 类型区分新旧布局,缺少显式版本标记 @ rtp_llm/cpp/pybind/ConfigInit.cc:1351
    • 建议:在 state 首位加入显式版本标记(如 py::make_tuple("v2", ...))或改用 py::dict state,__setstate__ 按版本号分派并对未知版本报错;同时补一个「5 元组但版本/类型不匹配」的拒绝用例。
  • 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 —— 两个版本的 DLManagedTensorVersioned ABI 差异显著,注释会误导后续升级与跨组件对齐判断。同理 3rdparty/xgrammar/xgrammar.BUILD:1 注释声称 v0.2.2,与所 pin 的 fork commit 无法互相印证;:38-41 的 exclude = ["cpp/nanobind/**"] 相对 cpp/*.cccpp/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 语义相反
    applyDeviceMaskStateFINISHEDUNSET / NOOP 并列直接 return ErrorInfo::OkStatus()(:409-412),即不写任何 mask(fail-open);而 MTP 侧 fillSpecVerifyRowmatcher.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 —— 两个版本的 DLManagedTensorVersioned ABI 差异显著,注释会误导后续升级与跨组件对齐判断。同理 3rdparty/xgrammar/xgrammar.BUILD:1 注释声称 v0.2.2,与所 pin 的 fork commit 无法互相印证;:38-41 的 exclude = ["cpp/nanobind/**"] 相对 cpp/*.cccpp/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-179json_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 语义相反
    applyDeviceMaskStateFINISHEDUNSET / NOOP 并列直接 return ErrorInfo::OkStatus()(:409-412),即不写任何 mask(fail-open);而 MTP 侧 fillSpecVerifyRowmatcher.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 只声明 srcsdata = ["//:th_transformer_config"],无 deps、无 exec_properties(:11-15),而测试在模块导入期就执行 from libth_transformer_config import GrammarConfigconfig_pickle_test.py:4)。根 BUILD:96-106th_transformer_configlinkshared 目标,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_workerscompiler_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,68logits_processor/test/BUILD:24 与自身单测引用,生产侧无构造点,而 docs/backend/logits_processor.md:8rtp_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 侧改用 ModelConfigvocab_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 状态原子性设计到位:acceptCommittedLockedrollback_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 顺序。
  • 编译错误分类清晰:compileWithErrorClassificationbad_alloc / runtime_error / 其他分别映射为 ResourceExhausted / InvalidArgument / Unknown,并有 malformed json schema 的分类断言。
  • 掩码 kernel 与 CPU fallback 语义一致且 fail-closed:packed_mask_logitsapplyPackedMaskLogitsCpuFallback 都对越界 logits_row 跳过、对超出 bitmask_words 的 tail 一律屏蔽,掩码值按 dtype 与 BaseLogitsProcessor::neg_inf 转换后一致,fallback 在非 CPU 设备上有显式 copy-back。
  • wire 兼容处理规范:proto 保留 response_format = 73 [deprecated = true] 不复用编号、新字段用 74;GrammarConfig pickle 用 py::isinstance<py::str>(t[0]) 兼容 legacy 五元组(避免 cast<bool> 陷阱),并覆盖新布局往返、legacy 降级、伪造短元组被拒三类场景。
  • 修掉了真实陷阱:_apply_updates 改用 key in type(self).model_fields 判定消费(generate_config.py:448-456),避免请求参数覆盖同名方法;gatherSpecSamplerInputsampler_inputs.finished_mask.zero_(),修掉 spec 路径未初始化即被读取的隐患,均有断言。
  • 配置面防漂移:grammar_group_args.py 全部使用 default=grammar_config.<field> 消除 Python/C++ 默认值漂移,help 与 optionsFromConfigstd::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 (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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 的用例。

Comment thread rtp_llm/server/server_args/grammar_group_args.py
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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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:192ValueError),或 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 类型。

Comment thread rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc
Comment thread 3rdparty/xgrammar/repositories.bzl Outdated
)
if request.debug_info:
config.return_output_ids = True
return config

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

📍 实际位置 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 模式的用例固化该顺序。

Comment thread 3rdparty/dlpack/BUILD
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])) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

📍 实际位置 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):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] test_grammar_defaults 与被测默认值同源,且 env 绑定通道完全无覆盖

grammar_group_args.py:11/19/33 已把 CLI 默认值改为 default=grammar_config.<field>,而该用例的期望值取自 type(g)() 新建实例(:428),两侧同源于同一份 C++ 默认值。因此若 C++ 侧改掉 num_workerscompiler_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 LLLLKKKK left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.bzlxgrammar.BUILD 头部注明「上游 tag + fork 相对上游的 patch 清单 + 上游 PR 链接 + 上游化计划与责任人」,避免核心受限解码链路长期依赖不可控的外部个人仓库。
  • --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。
  • 启动期强制构建 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 清单。
  • 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 承担,便于排障时判断该路径能否关闭。
  • 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>0MTP + num_return_sequences>1MTP + TREE_DECODE_CONFIG)断言错误码与错误信息;2) 若需保留可用性,为 MtpProcessorCapability 增加「MTP 下可安全忽略」的第三种模式(如 IGNORED_IN_SPEC),让与投机验证无关的 processor 走告警降级而非拒流;3) 至少提供环境变量在灰度期把拒流/启动拒绝降级为告警;4) tree_decode_config 断言抛出前先打一条 ERROR 日志,便于容器反复重启时定位。

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_LOG WARNING 并打一个 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_PARAMSkResourceExhausted → 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),使运维有明确的回滚判据。
  • 解码路径失败退化为无约束,与 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 可选」的用例。
  • 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&) 兜底,避免一个流的形状异常影响同批其他流。
  • 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 字段注释中说明新语义。
  • GrammarConfig 保留无消费者的 tokenizer_info_json / override_stop_tokens,与 ModelConfig 同名字段形成双源 @ rtp_llm/cpp/config/ConfigModules.h:377
    • 建议:二选一收敛单一真源:(a)删除 GrammarConfig 这两个字段,同步清理 pybind、pickle 元组布局判定、to_string().pyiconfig_pickle_test.py,让 ModelConfig::tokenizer_info_json 成为唯一来源;(b)保留字段但让 XGrammarBackend::create 仍以 GrammarConfig 为唯一入口、由初始化路径从 ModelConfig 填入。无论哪种,to_string() 都不应再打印已无消费者的字段;若 override_stop_tokens 计划后续接线,请加 // TODO(owner): 尚未接线
  • 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」在两条路径下行为一致的单测固定该策略。
  • 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 保护,并把注释中的前提升级为运行期断言。
  • 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,把「仅支持增量布局」从隐式不变量变为显式契约;或参照 RecommendationLogitsProcessorstride > 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 拖累整批请求的排队。
  • 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 复用,避免同一规则两处实现继续漂移。
  • grammar smoke 的唯一断言 oracle 自身无任何单测 @ rtp_llm/test/smoke/grammar_constraint_validator.py:307
    • 建议:为该模块补一组 py_test:对 regex / json_schema / structural_tag 各给出至少一条应通过与一条应失败的样本(含空字符串、被 length 截断的半个 JSON、thinking 段未闭合等边界),断言失败样本确实抛出异常。这类用例纯 CPU、无模型依赖,成本很低但能把「oracle 有效」变成可执行契约。
  • 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 正文,或按上一条恢复确定性字段比对。
  • 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 不会在存量模型上误触发。
  • 新增 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 说明,便于开源合规追溯。
  • 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],并在 .pyiPyGrammarTokenizerInfo.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 侧对外分类、不参与跨语言同步」,避免后续同步时误改。

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 分支保持一致。
  • 新增测试的断言强度不足,默认值与拒绝路径用例近乎恒真 @ rtp_llm/test/generate_config_test.py:553
    • 建议:把 _assert_rejected 拆成「仅 validate」与「仅 apply」两条独立断言,并用 assertRaisesRegex 锁定具体拒绝原因;test_grammar_defaults 改为断言具体常量(num_workers == 8compiler_cache_bytes == 512 * 1024 * 1024constrained_json_disable_any_whitespace is False),使默认值调整必须显式改测试。
  • CudaMaskLogitsOpTest 保留 MYDEBUG 调试打印且用浮点 == 直接断言 @ rtp_llm/models_py/bindings/cuda/ops/tests/CudaMaskLogitsOpTest.cc:48
    • 建议:删除 MYDEBUG 打印(如确需保留,收敛到日志级别开关后再打印);断言改为 EXPECT_FLOAT_EQEXPECT_EQ 以便失败时输出具体数值,-FLT_MAX 等哨兵值可继续用精确比较但建议加 << "idx=" << i 的失败上下文。
  • 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),并包含 DLPackVersionDLManagedTensorVersioned 与 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 guard DLPACK_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:129MtpExecutor.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:56linkstatic = 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::applybuildState 失败(:286-289)及两处 catch(:294-299)中都置 device_mask_state_ = finishedState(...),而 applyDeviceMaskStateFINISHED 直接 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 导入)、strdict 四种形态(:40-53)。而 openai_endpoint.py:199request.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:129MtpExecutor.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-435test_grammar_defaultstype(g)() 的 C++ 默认值为期望,而 grammar_group_args.py:11/19/33 的 argparse default 又取自同一个 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_jsonLogitsProcessorFactory.cc:79 读取 model_config.tokenizer_info_jsonXGrammarBackend::create 里的 GrammarConfig 只被读 compiler_cache_bytes(:102-108)。全仓 grep 确认无任何代码读写 grammar_config.tokenizer_info_jsonoverride_stop_tokens,但两者仍通过 ConfigInit.cc:1326/1328 暴露为 Python 可读写属性、占用 pickle 元组第 3/4 位、出现在 ConfigModules.cc:392-393to_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)。pydantic ValidationError 的结构化明细(字段路径、失败原因)只存在于原始异常对象上,丢失 __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-435test_grammar_defaultstype(g)() 的 C++ 默认值为期望,而 grammar_group_args.py:11/19/33 的 argparse default 又取自同一个 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_infogrammar_tokenizer_info_test.pytest_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/ErrorResultLogitsProcessorStates::batchProcess 按 interval 归属到具体行,SamplerOutput::processor_errors 保证单流失败不影响同批其他流,替代了原 void 接口下的静默失败。
  • 提交语义具备原子性:acceptCommittedLockedrollback_commit 闭包同时回滚 matcher token 数、committed_output_len_ 与缓存 mask,回滚失败降级 markFinished() 防半提交;配套 UpdateStatusRollsBackEntireRejectedBatch 单测。
  • prepareSpeculativeProvisionalSpecAcceptTracker 保证成功/失败都回滚到 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 词表小于模型词表时,clearBitmaskTokenRangeapplyDeviceMaskState 尾部 fill_(neg_inf) 双路径截断;单字/跨字/end_bit==0 三类边界均有单测。
  • compileWithErrorClassificationbad_alloc/runtime_error/其他分类为 ResourceExhausted/InvalidArgument/Unknown,并在头文件写明契约;logCompileResult 记录耗时、字节数与 invalid 标记。
  • 掩码常量统一为 BaseLogitsProcessor::neg_inf(原 -INFINITY),避免全掩码行 softmax 产生 NaN;gatherSpecSamplerInputfinished_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 新旧布局做成可执行契约。

Comment thread 3rdparty/xgrammar/repositories.bzl Outdated
Comment thread rtp_llm/server/server_args/grammar_group_args.py
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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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 (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] think 模式被无条件转为 grammar 约束解码,think + beam / n>1 由可用变为硬失败且强依赖 backend

apply()in_think_modehas_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:271enable_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()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] MTP 下非 grammar logits processor 变为请求级硬失败,且 tree_decode_config 启动期抛错,均无开关与迁移路径

prepareStreams 对每条流调用 validateMtpCompatibility(:45-55),任一 processor 的 mtpCapability().mode == UNSUPPORTEDreportErrorcontinue(流既不进 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>0MTP + num_return_sequences>1MTP + 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] 新增测试的断言强度不足,默认值与拒绝路径用例近乎恒真

_assert_rejected(:551-556)把 cfg.validate()ResponseFormatBuilder(cfg).apply() 放进同一个 assertRaises(FtRuntimeException) 块:validate() 抛异常时 apply() 根本不执行,无法区分校验发生在哪一层,把校验在两层间迁移也不会被发现(同写法见 :1039/:1052/:1061 附近)。另 server_args_test.py:423-435test_grammar_defaultstype(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 == 8compiler_cache_bytes == 512 * 1024 * 1024constrained_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>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

📍 实际位置 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_EQEXPECT_EQ 以便失败时输出具体数值,-FLT_MAX 等哨兵值可继续用精确比较但建议加 << "idx=" << i 的失败上下文。

Checklist: [6.1] 无 per-forward 调试日志 / 噪声热路径输出;[6.1] 逻辑变更未混入无关格式化

Comment thread 3rdparty/xgrammar/xgrammar.BUILD Outdated
"cpp/*.cc",
"cpp/support/*.cc",
],
exclude = [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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 LLLLKKKK left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 的处理方式(接受并忽略、或显式拒绝并提示迁移)固化为断言。
  • 启动期强制构建 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 时引擎启动行为」的启动路径测试。
  • 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」的端到端用例,断言错误码或约束生效行为稳定。
  • MTP 能力校验默认拒绝所有未声明 processor,tree_decode_config 变为启动期抛异常 @ rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:47
    • 建议:从「静默不生效」改为「显式报错」方向正确,但用基类默认值把「未声明」等同「不支持」属对既有子类契约的隐式收紧:请为确实兼容 MTP 的 processor(如 Recommendation)显式声明能力,而非依赖默认值;构造函数硬断言改为启动配置校验阶段返回结构化错误,避免异常从构造函数逃逸;在发布说明列出受影响组合(combo_token_size>0num_return_sequences>1TREE_DECODE_CONFIG+MTP)与迁移方式,并补回归用例断言错误码稳定。

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() 分派:kInvalidArgumentINVALID_PARAMSkResourceExhaustedMALLOC_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 吞吐对比数据以量化收益。
  • 非 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 的 DecodeMaskBuilder CPU 用例(可参考 PackedMaskLogitsCpuFallbackTest 的组织方式)做回归守护。
  • grammar backend 存放在函数内可变静态变量,init 重复构建且无同步与隔离手段 @ rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:66
    • 建议:把 backend 所有权移到 Engine/Executor(放入运行时上下文或 Executor 成员),由 createLogitsProcessors 以参数接收,消除进程级静态状态、避免重复构建,并让多引擎与单测场景可隔离。若短期保留现状,至少让 init()tokenizer_info_json 未变化时跳过重建,并在注释中明确「仅允许启动阶段调用一次」。
  • 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 中支持 legacy structures/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 以上可重试」区间之外,避免后续维护者按码段推断语义。
  • ThinkModeLogitsProcessor 沦为生产不可达代码,max_batch_size 形参失效,文档与实现不符 @ rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:99
    • 建议:在本 PR 或紧随的清理 PR 中删除 ThinkModeLogitsProcessor 及其 BUILD/测试条目,移除失效的 max_batch_size 形参与调用点实参,并同步更新 docs/backend/logits_processor.mddash_sc/think.py 注释;若计划保留作为 think 回退实现,则必须有实际装配点或明确开关/TODO,避免长期存在「编译通过但永不执行」的状态机代码。
  • tokenizer 家族探测函数在单测中被整体 mock,最易出错的分派链零直接覆盖 @ rtp_llm/config/test/grammar_tokenizer_info_test.py:73
    • 建议:为三个探测函数补直接用例:tiktoken Encoding / vocab_files_names['vocab_file'] 含 tiktoken / 仅有 mergeable_rankssp_modeltokenizer.sp_modeltok 三个 sentencepiece 挂载点;_has_sentencepiece_api 缺任一方法;_get_hf_tokenizer_jsonbackend_tokenizer.to_str 缺失时抛 ValueError。并保留至少一条只 mock serialize_grammar_tokenizer_info、不 patch 探测函数的用例,断言分派优先级(同时具备 fast 与 sentencepiece 特征时应走 fast)与 metadata 内容一致。
  • TokenizerInfo 以 RAW 重建的兼容性 workaround 缺少等价性测试 @ rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:81
    • 建议:补一条针对重建路径的用例:对 BYTE_FALLBACK / BYTE_LEVEL 两类 metadata,分别断言重建后的 GetDecodedVocab()GetSpecialTokenIds() 与反序列化结果一致,重点覆盖 vocab 空洞的 "" 槽位仍被判为 special、以及形如 <|...|> 的普通文本 token 不被误判。
  • 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_onlystream 互斥。约束校验通过后继续比较 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」,避免注释与实际覆盖范围不符。
  • 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」或带 sha256http_archive,使依赖内容寻址、可离线复现、anytext-budget 改动可审计可回滚。若必须 pin 分支 commit,请补内部镜像与 shallow_since,并在文件头注明「上游合并后切回 tag」的迁移路径。
  • vendored dlpack 头版本与 BUILD 注释不符,且未记录 upstream 出处与 license @ 3rdparty/dlpack/BUILD:6
    • 建议:把注释更正为实际版本(1.2),在 BUILD 或同目录 README 记录 vendored 文件的 upstream 仓库与 commit,并补上 dlpack license 以满足三方代码入库要求;同时说明为何该版本与所 pin 的 xgrammar commit 兼容(至少注明 DLTensor 布局未变),visibility 建议收敛到实际使用方。
  • 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 注释分行。
  • 请求级 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_xgrammarvisibility 显式收窄为 ["//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 状态均相同,把幂等性固化为契约。
  • 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/BUILDmodels/BUILD:78logits_processor/test/BUILD)全部随本 PR 新增,不存在需迁移的历史调用方;该目标无 srcs/hdrs,只是 deps = [":grammar_xgrammar"] 的空壳,依赖图多一跳且注释误导后续维护者。
  • [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue 新增 grammar CLI 参数的默认值断言为同源自证,env 分支与边界值零覆盖
    test_grammar_defaultsexpected = 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_nameGRAMMAR_TERMINATE_WITHOUT_STOP_TOKEN / GRAMMAR_NUM_WORKERS / GRAMMAR_COMPILER_CACHE_BYTES),env 分支零断言;compiler_cache_bytes<=0num_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/BUILDmodels/BUILD:78logits_processor/test/BUILD)全部随本 PR 新增,不存在需迁移的历史调用方;该目标无 srcs/hdrs,只是 deps = [":grammar_xgrammar"] 的空壳,依赖图多一跳且注释误导后续维护者。
  • [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue processor 长度不一致这一内部不变量被归入 UNKNOWN_ERROR,与本 PR 的错误分类风格不一致
    validateLogitsProcessorStatecommittedOutputLen()outputTokenLen() 不一致时返回 ErrorInfo(ErrorCode::UNKNOWN_ERROR, ...)(:963-968),新增用例 GenerateStreamTest.cc 把该行为固化为期望。而本 PR 同时引入了 GRAMMAR_VOCAB_EXCEEDS_MODEL_VOCABGRAMMAR_EOS_OUT_OF_VOCABGRAMMAR_PARSER_REJECTED_TOKENGRAMMAR_VERIFY_EXCEPTION 等专用码并逐一断言,把这一明确的内部状态不变量归入 UNKNOWN_ERROR,会让线上排障无法从错误码区分该场景。
  • [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue xgrammar pin 在上游未合并特性分支 commit,且另开一套依赖声明入口
    xgrammar_deps() 已改为直连上游 mlc-ai/xgrammar(较上一轮的个人 fork 有明显改进),但仍用 new_git_repository pin commit = "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_constraintresponse_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: 必须与 Python generate_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: 必须与 Python generate_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:129MtpExecutor.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_constraintresponse_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/BUILDmodels/BUILD:78logits_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:129MtpExecutor.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 边界校验,存在越界写风险
    processvocab_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 仍编译、单测仍在跑。连带影响:createLogitsProcessorsmax_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 为空/构造抛异常四类情况均返回 nullptrmatcherCall 模板统一收敛 xgrammar 异常。
  • 投机校验状态不变量严谨:prepareSpecMaskprovisional_accepts 试探并回滚,失败统一 fail_closed()markFinished + 强制 EOS-only bitmask);acceptCommittedLockedrollback_commit 保证多 token 批量提交「全成功或全回滚」。
  • 新增 committedOutputLen() + GenerateStream::validateLogitsProcessorState() 做 parser/stream 长度对账,把潜在错位从静默错误变成显式 error;MtpBatchStreamProcessor.cc:122 补上 finished_mask.zero_(),修掉 spec 路径未初始化隐患。
  • 单一事实来源:grammar_group_args.py 的 default 全部读取 C++ GrammarConfig 结构体默认值;Python GrammarConstraint + ResponseFormatBuilder 把 one-of 约束规范化收敛为单一投影路径,C++ keyFromGenerateConfig 再独立复核互斥,形成双层防护。
  • BitmaskUtils::clearBitmaskTokenRange 对同字/跨字、end_bit == 0 的位推导正确,并有跨字与同字两个针对性用例。
  • 测试使用真实 xgrammar 后端而非 mock:GrammarLogitsProcessorTest 覆盖初始 mask、预算耗尽、终止后强制 EOS、整批 rollback、prepareSpeculative cap 语义与 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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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 的启动脚本会 SystemExitGRAMMAR_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 的处理方式(接受并忽略、或显式拒绝并提示迁移)固化为断言。

Comment thread rtp_llm/config/grammar_tokenizer_info.py
Comment thread rtp_llm/config/response_format_builder.py Outdated

std::optional<ErrorInfo> validateMtpCompatibility(const std::vector<BaseLogitsProcessorPtr>& processors) {
for (size_t i = 0; i < processors.size(); ++i) {
const auto capability = processors[i]->mtpCapability();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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>0num_return_sequences>1TREE_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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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/BUILDmodels/BUILD:78logits_processor/test/BUILD)全部随本 PR 新增,不存在需迁移的历史调用方;该目标无 srcs/hdrs,只是 deps = [":grammar_xgrammar"] 的空壳,依赖图多一跳且注释误导后续维护者。

建议:grammar_xgrammarvisibility 显式收窄为 ["//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:无投机性抽象

Comment thread rtp_llm/config/generate_config.py Outdated
or _response_format_is_grammar(self.response_format)
)

def _normalize_grammar_fields(self):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

📍 实际位置 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_constraintresponse_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:重复非平凡逻辑被抽取或显式复用

Comment thread rtp_llm/cpp/engine_base/stream/GenerateStream.cc
def test_update_rejects_malformed_envelope(self):
cfg = GenerateConfig()
cfg.update({"response_format": {"type": "json_schema"}})
with self.assertRaises(FtRuntimeException) as ctx:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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 LLLLKKKK left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 异常时不回滚镜像即可拉起服务。
  • --grammar_backend / GRAMMAR_BACKEND 无过渡删除,存量启动脚本升级即退出且失去唯一关停开关 @ rtp_llm/server/server_args/grammar_group_args.py:6
    • 建议:保留 --grammar_backendGRAMMAR_BACKEND)为已废弃参数:解析后打印一次 deprecation warning 并忽略,或把 none 映射为新的关闭开关(与上一条的 --grammar_enable 合并实现),下个版本再删。并在 PR description / 发布说明中列出本次移除的运维面(--grammar_backendGRAMMAR_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 的拒绝建议同时打一条限频告警日志,便于线上按触发类型定位。
  • 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。

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::StatusCodeErrorCode 的显式映射: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 的 GrammarLogitsProcessor CPU 用例,避免该分支长期无覆盖。
  • 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 路径上复用时静默读到被覆写的掩码。
  • grammar backend 存放在函数内可变静态变量,init 重复构建且无同步与隔离手段 @ rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:66
    • 建议:把 backend 所有权上移到 engine/executor 并以参数注入工厂,彻底移除进程级可变静态;若本次不便重构,至少把接口拆成 setGrammarBackend() 与返回值拷贝getGrammarBackend(),并给 init() 加幂等保护(配置相同则跳过重建)+ std::call_once,把「只允许启动期写一次」变成代码强制。另建议把 keyFromGenerateConfig 与 beam/多序列校验拆到独立的 grammar 请求解析单元。
  • ThinkModeLogitsProcessor 沦为生产不可达代码,max_batch_size 形参失效,文档与实现不符 @ rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:99
    • 建议:二选一并在本 PR 内闭环:删除 ThinkModeLogitsProcessor.{h,cc}、其单测与 BUILD 条目,或明确保留为 backend 不可用时的显式回退分支并补选择逻辑单测。无论哪种都请同步更新 docs/backend/logits_processor.mddash_sc/think.py 注释,移除失效的 max_batch_size 形参与实参,并说明引擎侧 think 字段现为「接收但不生效」以免后续读者误判。
  • 新增 GRAMMAR_ 错误码全部落入 INTERNAL 分类,与错误语义不匹配* @ rtp_llm/config/exceptions.py:45
    • 建议:按语义补分类:608/609 用 INVALID_OUTPUT,611/612 视定位用 BAD_REQUEST 或保留 INTERNAL 但在注释中标注为启动配置问题,610/613 保留 INTERNAL;并补一条按分类映射的断言测试,避免后续新增码再次默认落入 INTERNAL。
  • 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。两侧各补一条用例,并在发布说明中列出破坏性变更清单。
  • has_bounded_region 只检查 max_tokens,漏检 max_chars 导致守卫可被绕过 @ rtp_llm/config/grammar_constraint.py:61
    • 建议:把判定改为 max_tokensmax_chars 任一非空即视为 bounded region(建议把预算字段名集中为一个常量集合,后续新增只改一处),并补两条用例分别用 max_tokens / max_chars 锁定拒绝行为。
  • legacy structures/triggers 形态的 structural_tag 在 think 模式下必然失败且错误文案误导 @ rtp_llm/config/grammar_constraint.py:51
    • 建议:把形状校验与归一化统一到 GrammarConstraint:将 legacy structures/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),断言返回 nullptrkInvalidArgument 且 message 非空——均不需要 GPU。
  • 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 分支改为抛错。
  • tokenizer 家族探测函数在单测中被整体 mock,生产导入入口亦无覆盖 @ rtp_llm/config/test/grammar_tokenizer_info_test.py:73
    • 建议:为三个探测函数各补一组 fake tokenizer 直测(tiktoken 各命中路径、sentencepiece 各候选属性、全不命中返回 False),_get_hf_tokenizer_jsonbackend_tokenizer 缺失与 to_str 非 callable 两个失败分支;另补一个直接走 from rtp_llm.ops import serialize_grammar_tokenizer_info 的用例,使打包位置与模块名解析回归能在单测阶段暴露。
  • xgrammar pin 在上游未合并特性分支 commit,且另开一套依赖声明入口 @ 3rdparty/xgrammar/repositories.bzl:8
    • 建议:优先把依赖指向上游已合并的 tag/commit,并把仓库内需要的差异沉淀为 3rdparty/xgrammar/*.patch(与 flashinfer/cutlass 既有做法一致);若必须跟随未合并分支,请在注释中记录上游 PR 链接与切换到正式版本的时间点。同时建议把 xgrammar_deps() 合并进 git_deps(),避免依赖声明入口分裂。
  • 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 列表一一对应;建议重新生成而非手改,避免下次继续漂移。
  • GrammarConfig unpickle 兼容阶梯包含仅存在于本分支中间提交的布局,且靠类型嗅探区分 @ rtp_llm/cpp/pybind/ConfigInit.cc:1366
    • 建议:只保留「当前 5 元组 + base main 的 legacy 布局」两条路径,删除中间布局分支与对应用例;更稳的做法是在 tuple 首位放显式 schema version(或改用 dict state),未知版本 fail-fast,并把报错文案补上 GrammarConfig 前缀。若该 pickle 仅用于同一二进制的 spawn 传递,请在注释中写明该场景以说明为何不需要跨版本兼容层。
  • 新增 grammar 相关测试目标的执行属性与 data 依赖不符合仓库惯例 @ rtp_llm/cpp/engine_base/grammar/test/BUILD:13
    • 建议:给 xgrammar_backend_cpp_testexec_propertiesenv(或把 deps 收窄到不含 torch 的目标并注明),并删除未使用的 torch_deps load;把 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}、以及两类错误同时命中时的优先级。
  • 新增 grammar CLI 参数的默认值断言为同源自证,env 分支与边界值零覆盖 @ rtp_llm/server/server_args/test/server_args_test.py:423
    • 建议:补一个 env 用例:设置 GRAMMAR_TERMINATE_WITHOUT_STOP_TOKEN=1GRAMMAR_COMPILER_CACHE_BYTES=67108864CONSTRAINED_JSON_DISABLE_ANY_WHITESPACE=1 后调用 _setup(),断言落到 grammar_config 的值与类型正确(bool 而非字符串);再补 GRAMMAR_COMPILER_CACHE_BYTES=0GRAMMAR_NUM_WORKERS=0 两条边界断言,固化「<=0 表示不限」与「clamp 到至少 1」的契约。
  • 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
  • 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 注释仍能被跨语言一致性检查识别。
  • 请求级 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,避免引入一个从第一天起就是技术债的过渡目标。
  • 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_tagvalue.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.bzlgit_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_allocc10::Error(:261-265)。AssertUtils.cc:13-22 抛出的 RTPExceptionpublic std::runtime_error)不属这两类,会逃出 run()decodeStep,把单批形状异常放大为整个 decode step 失败(core dump 配置开启时甚至 abort()),与同函数内 processor 错误按 per-stream processor_errors 优雅降级(:185-188)不一致。MtpExecutorTest.cc:714EXPECT_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-31mtpCapability() 无条件返回 SPEC_VERIFY。同一前置条件在同一个类里给出两套矛盾契约:普通解码静默放行,MTP 路径会在 SpecLogitsVerifyRunner.cc:184 空指针解引用崩溃。
  • [6.1] Quality — Mega-PR 已拆分为独立变更 → issue 大范围格式化重排混入逻辑变更,跨语言 SYNC 注释被折断
    RecommendationLogitsProcessor.cc 与其测试合计产生千行级 diff,但既无新增也无删除 TEST_F,语义变更仅 process/updateStatus 新签名适配与约 13 处断言常量替换(-INFINITYBaseLogitsProcessor::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 处断言常量替换(-INFINITYBaseLogitsProcessor::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.ccMtpExecutor.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-31mtpCapability() 无条件返回 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.ccMtpExecutor.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_inputvalidate() 把它投影为 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_inputvalidate() 把它投影为 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),返回的 ProcessorBundleproc/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_tokenXGrammarBackend.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.bzlgit_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:51update_and_popsetattr 赋值(:444-450),dict/str 不会被 pydantic 转换。随后 _validate_fields()project_response_format()isinstance(..., ResponseFormat) 必然为假,抛 response_format must be a validated ResponseFormat objectgenerate_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_errorsSamplerOutputcollectStreamSamplerError(NormalOutputDispatcher.cc:24-32 显式注明使用 sampler-input 坐标以兼容 beam 扩批)→ StreamUpdateInfo.error_info,违规 token 在 append 之前 early-return,不进入用户可见历史。
  • MTP 状态不变量处理严谨:prepareSpecMaskprovisional_accepts 精确回滚试探性 accept,rollback 失败时拼接原始错误并 markFinished() fail-closed;acceptCommittedLockedrollback_commit 保证整批提交原子性,不残留半提交前缀。
  • 新增 committedOutputLen() + GenerateStream::validateLogitsProcessorState()(GenerateStream.cc:953)把「processor 状态与权威 token 历史漂移」变成显式错误,而非静默产出不合语法结果。
  • 第三方异常不穿透主循环:XGrammarBackend::create 声明 noexcept,对空 metadata / 反序列化失败 / 空词表三条路径统一返回 nullptrRtpGrammarMatcher 把 xgrammar 异常收敛为 absl::Status
  • 位掩码边界处理完整且有针对性覆盖:clearBitmaskTokenRange 正确区分同字/跨字与 end_bit == 0forceTokenInBitmask 对越界 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 未初始化即被读取的历史隐患。

Comment thread rtp_llm/config/grammar_tokenizer_info.py
Comment thread rtp_llm/server/server_args/grammar_group_args.py
Comment thread rtp_llm/config/response_format_builder.py Outdated
Comment thread rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc
Comment thread rtp_llm/config/generate_config.py
Comment thread rtp_llm/cpp/models/logits_processor/BitmaskUtils.h
Comment thread rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc
Comment thread rtp_llm/cpp/engine_base/grammar/BUILD
Comment thread rtp_llm/cpp/engine_base/stream/GenerateStream.cc

@LLLLKKKK LLLLKKKK left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 中补一条固化该行为的用例。
  • 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.mddocs/backend/logits_processor.md 写清该限制与替代方案,并补一条 thinking + beam search / n>1 的回归用例(当前 8 条 grammar smoke 用例均未覆盖该组合)。
  • MTP 准入硬拒绝所有未声明能力的 logits processor,tree_decode_config 变为启动期抛异常 @ rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:45
    • 建议:补齐灰度与回滚:为 RecommendationLogitsProcessor/MultiSeqLogitsProcessor 显式实现 mtpCapability(),reason 中写明触发配置(combo_token_size>0num_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
    • 建议:统一为一种语义:或在 GenerateConfigmode="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_formatextra_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 分派:kInvalidArgumentINVALID_PARAMSkResourceExhaustedMALLOC_FAILED(或既有可重试码段)、其余 → EXECUTION_EXCEPTION;并把 std::logic_error 从「retryable」档拆出。补一条单测断言「OOM 类编译失败不落到 BAD_REQUEST 类别」。若确实不打算区分,则删除 backend 侧的分类逻辑,避免留下误导性抽象。
  • 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 注释;或在头文件写明保留原因与重新启用条件。
  • 进程级 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 路径为何安全。
  • 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 != nullptrbitmask_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 覆盖用例。
  • 新增 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 的用例,避免后续新增码再次静默落入默认分支。
  • legacy structures/triggers 形态的 structural_tag 归一化后无法编译,且两条报错文案完全相同 @ rtp_llm/config/grammar_constraint.py:68
    • 建议:要么在 normalize_grammar_value 中把 legacy structures/triggers 显式转换为 format DSL,要么在入口即拒绝并给出「请改用 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_tokensmax_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 目标上硬编码设备。
  • TokenizerInfo 以 VocabType::RAW 重建的等价性缺少测试,create 降级分支零覆盖 @ rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:81
    • 建议:在 XGrammarBackendTest.cc 补:(1)对 BYTE_FALLBACK 与 BYTE_LEVEL 两种 fixture vocab,分别用原始 TokenizerInfocreate() 重包装后的 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=falseminItems/maxItems、bare trigger 残留);(4)把「校验器不支持该关键字」与「输出违反约束」拆成两类异常,前者映射 VALID_FAILED,避免工具能力缺口被误判为产品缺陷。
  • 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() 表达意图。
  • grammar 包 BUILD 声明的 Tier 边界未由 visibility 落地,且过渡 umbrella target 无存量调用方 @ rtp_llm/cpp/engine_base/grammar/BUILD:58
    • 建议:把 grammar_xgrammarvisibility 收敛为显式白名单(如 ["//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),使离网构建与回滚可复现。
  • vendored dlpack 头版本与 BUILD 注释不符,且未记录 upstream 出处与 license @ 3rdparty/dlpack/BUILD:6
    • 建议:把注释更正为实际 vendored 版本(v1.2),写明 upstream 仓库、commit/tag 与获取时间并附 license 归属文件;同时确认该版本与 torch 内置 dlpack 的 DLTensor 布局兼容,或在注释中说明本仓只使用 DLTensor 这一稳定子集,使第三方代码来源可审计、后续升级有据可查。
  • 跨语言类型存根与实际绑定不一致,配置与原生扩展契约均不完整 @ rtp_llm/ops/libth_transformer_config.pyi:544
    • 建议:在 libth_transformer_config.pyiGrammarConfig 中补 terminate_without_stop_token: booldef __repr__(self) -> str: ...,与 def_readwrite 绑定集合完全对齐(若存根由工具生成,请在本 PR 重新生成并提交);同时把 libth_grammar_tokenizer_info.pyiops/__init__.pyencoded_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 == 8compiler_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」用例也走真实谓词。

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_tag docstring 中明确「仅支持 \n」并列出受影响的 env 取值形态;若需兼容,可白名单式扩展 \t/\r 等常见转义,或在检测到其它反斜杠转义时打印一次 WARNING 提示改用真实字符,避免运维在无提示情况下踩坑。
  • 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:78engine_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:78engine_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.bzlgit_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_bytest[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/grammarconfig: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.ccGrammarConfig::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_bytest[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_VERIFYSpecLogitsVerifyRunner 只校验 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:81 RAW 重包装是否安全的输入来源。
  • [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue 新增 grammar CLI 参数的默认值断言同源自证,env 分支与文档化边界零覆盖
    test_grammar_defaultsexpected = type(g)()(:428)取期望值,而 grammar_group_args.pydefault= 同样读取新建 GrammarConfig() 的字段,两侧同源,因此该断言无法发现默认值本身被改动(如 num_workers 8→1、compiler_cache_bytes 512MiB→0,见 ConfigModules.h:378/381,均会影响全部部署却仍然通过),与类 docstring 声称的「Regression guard for the wiring」不符。此外两个新用例只走 sys.argv(:443-453),未覆盖 env_name 路径(GRAMMAR_TERMINATE_WITHOUT_STOP_TOKENGRAMMAR_COMPILER_CACHE_BYTESCONSTRAINED_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.ccGrammarConfig::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:81 RAW 重包装是否安全的输入来源。
  • [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,19bind_todefault=grammar_config.terminate_without_stop_token 直接读写该属性;但 libth_transformer_config.pyi:544-556class 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 采用显式补偿事务:prepareSpecMaskprovisional_accepts 精确 rollback(GrammarLogitsProcessor.cc:131),rollback 失败时串联原始错误并 markFinished() fail-closed;acceptCommittedLockedrollback_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==0end_bit==0(末尾 32 对齐)、越界钳制全部正确区分;bitmaskAllowsTokenforceTokenInBitmask 对越界分别采取「拒绝」与「显式 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_NAMESget_args(Literal) 派生(grammar_constraint.py:15-18);ResponseFormatJSONSchemamodel_serializer 保住 OpenAI 线上字段名 schemavalidate_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)}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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-32start()无条件调用它填充 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 要求。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

预期行为

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",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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 nullptrLogitsProcessorFactory::init(:79)把 nullptr 直接存入进程级 static,引擎照常完成启动并对外健康,此后...

建议: 区分「未配置」与「配置失败」:tokenizer_info_json.empty() 时保持返回 nullptr(对应上一条的显式开关);非空但反序列化/构造失败时向上返回错误让引擎启动失败(与 Python 一致),或至少上报一个明确的「grammar backend disabled」启动指标/健康位,并把当前 ERROR 日志升级为带原因分类的启动结论日志。同时补一条「非空但非法 JSON」的用例。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

防御行为,已添加防御throw

Comment thread rtp_llm/server/server_args/grammar_group_args.py
) -> "ResponseFormatPlan":
final_constraint = _resolve_final_constraint(config)

if config.in_think_mode:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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.mddocs/backend/logits_processor.md 写清该限制与替代方案,并补一条 thinking + beam search / n>1 的回归用例(当前 8 条 grammar smoke 用例均未覆盖该组合)。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

不再支持

return tensor.scalar_type() == torch::kInt32 ? tensor.contiguous() : tensor.to(torch::kInt32).contiguous();
}

std::optional<ErrorInfo> validateMtpCompatibility(const std::vector<BaseLogitsProcessorPtr>& processors) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] MTP 准入硬拒绝所有未声明能力的 logits processor,tree_decode_config 变为启动期抛异常

validateMtpCompatibility()(:45-55)遍历 stream->getAllLogitsProcessorPtr(),任一 mtpCapability().mode == UNSUPPORTED 即返回 INVALID_PARAMSprepareStreams(: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>0num_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 覆盖)。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

原先也不应该同时开启 现在只是显示拒绝

fake_ops.serialize_grammar_tokenizer_info = serializer
with (
mock.patch.dict(sys.modules, {"rtp_llm.ops": fake_ops}),
mock.patch.object(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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 不得替代本次声称覆盖的生产边界

Comment thread rtp_llm/cpp/engine_base/stream/GenerateStream.cc
Comment thread rtp_llm/config/response_format.py
.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__",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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.ccGrammarConfig::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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

📍 实际位置 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 LLLLKKKK left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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_backendGRAMMAR_BACKENDoverride_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 类型,便于现场定位。
  • 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」的用例把最终语义钉住。
  • 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 描述 / 发布说明中给出这两个字段在三条协议上的迁移矩阵。
  • 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 中列出全部新增拒绝条件与错误码,便于运维排障与回滚决策。

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 映射:kInvalidArgumentINVALID_PARAMSkResourceExhausted → 容量类错误码(如 MALLOC_FAILED),其余 → EXECUTION_EXCEPTION/UNKNOWN_ERROR,使后端已做的分类在对外错误码上保持可见;日志中带上原始 status code 与异常类型名,并补一条覆盖非 InvalidArgument 分支的用例。
  • 新增 6 个 GRAMMAR_ 错误码未指定 ExceptionCategory,全部退化为 INTERNAL* @ rtp_llm/config/exceptions.py:45
    • 建议:按语义显式标注分类:GRAMMAR_PARSER_REJECTED_TOKEN/GRAMMAR_NON_EOS_AFTER_TERMINAL/GRAMMAR_VERIFY_EXCEPTIONExceptionCategory.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 层,未覆盖本处分配路径)。
  • FINISHED 模式在 decode 路径 fail-open,且被掩码构建失败路径复用为缓存态 @ rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc:162
    • 建议:失败态不要复用语义为「matcher 已完成」的 FINISHED:新增 FAILED 模式,命中缓存时继续返回原始错误,或按 spec 路径语义只放行 EOS(复用 forceEos)。同时把 FINISHED 的兜底方向写进 GrammarLogitsProcessor.hDecodeMaskBuilder 注释并统一两条路径(建议 decode 也 fail-closed 或至少 WARNING),避免结构化输出请求在无任何信号的情况下返回不符合 schema 的文本。
  • 投机路径未校验位掩码缓冲区尺寸,专用错误码 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 / prepareSpeculativeSpecLogitsVerifyRunner::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 固化为可控来源:改用带 sha256http_archive 指向内部镜像或 vendored tarball,并在注释中记录对应上游 PR、所需的 anytext-budget 特性与「何时切回上游 release」的收敛计划;至少补 shallow_sincesrcs 建议改为显式文件列表,或保留 glob 但注明升级时需 review 新增源文件;另外仓库已有统一依赖加载入口,新增独立 xgrammar_deps() 会分裂依赖管理约定。
  • //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」。
  • 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_jsonXGrammarBackend::create → 对 BYTE_LEVEL/BYTE_FALLBACK 词表断言掩码允许集合正确,把 RAW 重建的等价性纳入回归网。
  • GrammarConfig 新增字段未同步到 .pyi 类型存根 @ rtp_llm/ops/libth_transformer_config.pyi:544
    • 建议:重新生成并提交 .pyi,补齐 terminate_without_stop_token: booldef __repr__(self) -> str: ...。为长期防漂移,可增加一个轻量测试或 CI 步骤,对比 GrammarConfigdef_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 中对不存在文件与旧实现的引用。
  • 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 语义断言;把默认值断言改为显式常量,使其成为真正的回归护栏。
  • _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_inputFtRuntimeException,确保该边界不会在后续重构中退化为静默通过。

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)暴露区间越界,避免约束失效被静默吞掉。
  • 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 边界落到 visibilitygrammar_xgrammar 显式限定为 ["//rtp_llm/cpp/engine_base/grammar:__subpackages__", "//rtp_llm/cpp/models/logits_processor:__pkg__"],让注释中的约束由构建系统强制,并按需为测试单独放行;同时删除 grammar_cpp_lib 让调用方直接依赖 grammar_xgrammar,或在注释中说明其保留原因与删除条件。
  • 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_deps load,并把 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&) 分支日志中加上异常类型名,便于线上区分误分类。

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:16rtp_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:17bind_to=(grammar_config, "terminate_without_stop_token") 写入该字段,config_pickle_test.pyserver_args_test.py 也实际读写。但存根 libth_transformer_config.pyi:544-556class 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:16rtp_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 在构建期直接从公网 clone commit = "60fc70ee...",注释写明该 commit 来自 feat/anytext-budget(上游未合并的特性分支),既无镜像、无 sha256/shallow_since,也无内容校验。该分支被删除或 force-push 后 commit 可能不可达,导致所有构建(含 CI)在无任何代码变更的情况下同时失败且无法离线复现。同 PR 的 3rdparty/dlpack/BUILD 已明确说明「xgrammar 上游以 submodule 拉取 dlpack,该地址在内部构建环境不可达,因此改为 vendored」——即已确认该环境对公网拉取存在限制,却仍让核心依赖走构建期 git clone。此外 xgrammar.BUILDglob(["cpp/*.cc", ...]) 收集源文件,上游新增文件会静默进入编译单元。
  • [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue MTP propose 侧 EngineInitParams 未传播 grammar_config
    :207 新增 params.grammar_config = engine_config.attr("grammar_config").cast<GrammarConfig>();,但只作用于 score 模型的 EngineInitParamsprepareMTPEngineInitParams(:27-98)在构造每个 MTP EngineInitParams 时逐项传入了十余个子配置(:73-90),唯独没有 grammar_config(它不是构造参数),因此每个 MTP params 携带默认构造的 GrammarConfigtokenizer_info_json 为空)。当前尚未触发问题:NormalExecutor.ccMtpExecutor.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/.cctest/ThinkModeLogitsProcessorTest.ccrtp_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(四类约束、MTP SPEC_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:16rtp_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/BUILDconfig_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/.cctest/ThinkModeLogitsProcessorTest.ccrtp_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(四类约束、MTP SPEC_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/BUILDconfig_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 返回 ErrorInfoLogitsProcessorStates::batchProcess 按 interval 归属到 row → SamplerOutput.processor_errorsGenerateStream::update/specUpdate 先报错再返回,坏 token 不会被发布;MTP 侧经 MtpBatchStreamProcessor 把 per-stream 错误落到 spec_update_info.error_info,两条路径口径一致。
  • MTP spec-verify 状态机严谨:prepareSpecMaskprovisional_accepts 计数并在返回前强制 rollback,真正提交推迟到 updateStatus()acceptCommittedLockedrollback_commit 闭包保证多 token 批量提交「全成功或整批回滚」,回滚失败时把原始错误与回滚错误一并上报。
  • acceptCommittedLocked 刻意保留 TERMINATED 而非 FINISHED,并在 :424-427 说明原因(min_new_tokens/ignore_eos 延迟结束时仍需只允许 EOS),是容易做错的语义细节。
  • 位掩码边界严谨:clearBitmaskTokenRangebegin_word == end_wordbegin_bit == 0end_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 错位。
  • GrammarConstraintLiteral + get_args 把 grammar 字段清单收敛到单一来源,统一 collect/clear/normalize/resolve/apply 语义,彻底消除旧代码中 json_format/json_schema/response_format 的隐式覆盖优先级;ResponseFormatextra="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-50ErrorCode.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_FGrammarLogitsProcessorTest 覆盖 fp32/fp16/bf16、越界词表/EOS、all-true mask no-op、终止后强制 EOS、整批 draft 拒绝后的完整回滚、prepareSpeculative 幂等;ExecOpsTest/CPU fallback 覆盖 vocab 非 32 对齐、越界 row、非连续输入;config_pickle_test__reduce__ 伪造历史布局驱动真实 __setstate__
  • GenerateStream::validateLogitsProcessorStatecommittedOutputLen()outputTokenLen() 做一致性校验,把「processor 状态机与流权威历史脱节」这类静默错位变成显式错误;GrammarConfig::__repr__to_string() 均不输出 tokenizer_info_json 内容。

Comment thread rtp_llm/server/server_args/grammar_group_args.py
Comment thread rtp_llm/config/grammar_tokenizer_info.py
Comment thread rtp_llm/config/response_format_compiler.py
Comment thread rtp_llm/pipeline/pipeline.py Outdated
Comment thread rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc
Comment thread rtp_llm/cpp/engine_base/stream/GenerateStream.cc
copts = copts(),
)

# Transitional umbrella; deleted once callers move onto :grammar_xgrammar.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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:16rtp_llm/cpp/models/BUILD 依赖它——不存在需要迁移的存量调用方,该 target 属于引入即多余的间接层。

建议: 把 Tier 边界落到 visibilitygrammar_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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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/BUILDconfig_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>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] MTP propose 侧 EngineInitParams 未传播 grammar_config

:207 新增 params.grammar_config = engine_config.attr("grammar_config").cast<GrammarConfig>();,但只作用于 score 模型的 EngineInitParamsprepareMTPEngineInitParams(:27-98)在构造每个 MTP EngineInitParams 时逐项传入了十余个子配置(:73-90),唯独没有 grammar_config(它不是构造参数),因此每个 MTP params 携带默认构造的 GrammarConfigtokenizer_info_json 为空)。当前尚未触发问题:NormalExecutor.ccMtpExecutor.cc 调用 LogitsProcessorFactory::init 时传入的都是 score 模型的 params.grammar_config;但后续若有代码读取 MTP params 的该字段,会拿到与服务配置不一致的静默默认值。

建议:prepareMTPEngineInitParams 中显式拷贝 grammar_config(与其余子配置一致),或在该函数处加注释说明 MTP params 故意不携带 grammar 配置及其原因,避免后续维护者误用默认值。

Checklist: [6.1] 状态不变量:创建/更新/失败/重试/回滚路径有效

Comment thread rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc

@LLLLKKKK LLLLKKKK left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 自本版起由 Python prepare_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」的用例固化期望行为(明确错误码与文案,或明确降级)。
  • 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...
  • 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 一样列出完整互斥矩阵。

Non-blocking Suggestions

P2

  • grammar 编译失败的状态码在 factory 边界被压平为 INVALID_PARAMS,丢失可重试语义 @ rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:120
    • 建议:在 factory 侧按 absl::StatusCode 映射:kInvalidArgumentINVALID_PARAMSkResourceExhaustedMALLOC_FAILED(或既有可重试码段),其余 → EXECUTION_EXCEPTION;抽一个 grammarStatusToErrorCode() 供后续复用,并补一条断言「编译期资源类失败不会被报成 INVALID_PARAMS」的单测。
  • 新增 6 个 GRAMMAR_ 错误码未指定 ExceptionCategory,全部退化为 INTERNAL* @ rtp_llm/config/exceptions.py:45
    • 建议:逐个补 category:GRAMMAR_VOCAB_EXCEEDS_MODEL_VOCAB / GRAMMAR_EOS_OUT_OF_VOCABBAD_REQUESTUNSUPPORTED(视其是配置问题还是请求问题),GRAMMAR_PARSER_REJECTED_TOKEN / GRAMMAR_NON_EOS_AFTER_TERMINALINVALID_OUTPUT,缓冲/校验类保留 INTERNAL 并加注释说明。补一条对这 6 个码逐一断言 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() == nullptrtokenizer_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 归位,避免单请求问题放大为整批失败。
  • @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 重建那条)。
  • //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 的关停开关一并落地。
  • 新增 native 扩展的生产导入链路被 mock 完全绕过,且缺 Bazel 依赖声明 @ rtp_llm/config/test/BUILD:18
    • 建议:为 //rtp_llm:config 补上 //rtp_llm:ops 依赖(或把 grammar_tokenizer_info.py 移到已依赖 :ops 的目标下,如复用既有 config_ops),并在 py_test 中新增一条不 mock rtp_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 补参数化单测:覆盖 tiktoken Encoding 内嵌、vocab_files_names 缺失/类型异常、mergeable_rankssp_model / tokenizer.sp_model / tok 三种 sentencepiece 挂载位置,以及三类都不匹配的 fallback;并保留至少一条不 mock 任何探测器、只 mock serialize_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 版本升级的回归闸门。
  • 声称验证 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 平台的回归闸门。
  • grammar smoke 在 OpenAI 通路上存在三条静默放行路径,且与 DashSc 通路语义相反 @ rtp_llm/test/smoke/openai_comparer.py:230
    • 建议:把三条静默返回改为 fail-fast,与 dash_grpc_comparer.py 对齐(流式暂不支持时也应显式失败而非通过);同时保留一条确定性断言下限(至少校验 len(choices) >= 1finish_reason 与 golden 一致),避免 grammar_constraint_onlyusage / finish_reason 回归一并屏蔽。建议把「取 response_format / 缺失即报错」抽成两个 comparer 共用的辅助函数,消除重复实现带来的语义漂移。
  • 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_instanceif 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 解析所需的最小字段。
  • NormalOutputDispatcherTest 未构造其注释声称守护的 beam 展开场景 @ rtp_llm/cpp/normal_engine/test/NormalOutputDispatcherTest.cc:13
    • 建议:补充 cur_batch_size=2batch_idx_in=0 的用例,断言不会越界读到下一个 stream 的错误;再补一个 success_cpu 定义为 {true,false} 的用例,确认只保留首个错误,并明确 processor_errorssuccess_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: booldef __repr__(self) -> str: ...,保持 pyi 与 ConfigInit.cc 暴露面一致。建议在 CI 中加一条 stub 一致性检查(对比 pybind 暴露的属性名集合与 pyi 声明),避免这类漂移长期累积。
  • GrammarConfig::setstate 靠运行时类型嗅探分派 4 种布局,并静默丢弃 legacy 语义 @ rtp_llm/cpp/pybind/ConfigInit.cc:1347
    • 建议:收敛为「当前布局 + 至多一个已发布的 legacy 布局」,删除依赖运行时类型嗅探的分支及 config_pickle_test.py 中对应的未发布布局用例;若确需跨版本兼容,改为在元组首位写入显式 version 号。保留 legacy 分支时,在丢弃 grammar_backend / override_stop_tokens 时输出一条含被丢弃值的 WARNING,使配置语义变化在日志中可追溯。
  • 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 日志,给下游一个灰度窗口。
  • 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 两条路径各加一个用例。
  • 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_tagresponse_format 的映射关系)、think 模式如何折叠为 structural_tag envelope、启用与关停方式(含本轮要求补回的开关)、MTP spec-verify 的行为、与 beam / num_return_sequences / recommendation / tree decode 的完整互斥矩阵、4 个 --grammar_* 参数含义与默认值、6 个错误码及其可重试性;同时把第 8 行的 ThinkModeLogitsProcessor 条目改为「已由 grammar 约束替代」或直接删除,并列出被移除参数的迁移对照。
  • 新增 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)。

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) 的防御校验并用该码返回,要么删除该未使用的错误码。
  • 服务级 --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/ 中同步说明。
  • 新增 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:150from rtp_llm.ops import serialize_grammar_tokenizer_info(函数内 lazy import)。但 grammar_tokenizer_info_test.pybuild_tokenizer_infomock.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-340config 目标 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_utilsconfig_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_depsgit_deps()WORKSPACE:27-29,cutlass、flashinfer、googletest、abseil 等均在此),本 PR 却新建 3rdparty/xgrammar/repositories.bzl 并在 WORKSPACE:31-33 单独 load xgrammar_deps(),形成第二套依赖注册入口,后续统一升级易漏改一处。
  • [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue prepareSpecMask 的 EOS 越界出口未 fail-closed,且 GRAMMAR_BITMASK_BUFFER_TOO_SMALL 为死码
    prepareSpecMask 的其余错误出口都经 fail_closed lambda(第 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:22exceptions.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.pydash_grpc_comparer.pydash_sc/test/codec_test.pymodel_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.pydash_grpc_comparer.pydash_sc/test/codec_test.pymodel_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.pydash_grpc_comparer.pydash_sc/test/codec_test.pymodel_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_utilsconfig_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:29mtpCapability() 无条件返回 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_defaultsexpected = type(g)() 作为期望值(第 428-439 行),而 grammar_group_args.py:11/19/31/45 的 argparse default 本身就取自同一个 GrammarConfig 默认值,两侧同源:把 ConfigModules.h:377terminate_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_defaultsexpected = type(g)() 作为期望值(第 428-439 行),而 grammar_group_args.py:11/19/31/45 的 argparse default 本身就取自同一个 GrammarConfig 默认值,两侧同源:把 ConfigModules.h:377terminate_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_tagparse_json_grammar_value 立即抛出带字段名的错误。
  • [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue grammar smoke 在 OpenAI 通路上存在三条静默放行路径,且与 DashSc 通路语义相反
    _compare_resultqr_info.get("grammar_constraint_only") 为真时直接 self._validate_grammar_constraint(actual_result); return(第 249-251 行),跳过 usageaux_infofinish_reason、choices 数量等全部确定性字段比对。而 `validate_gramma

Strengths

  • MTP 投机验证的状态机处理严谨:prepareSpecMaskprovisional_accepts 精确记账并在返回前 matcher.rollback() 还原已提交态;acceptCommittedLockedrollback_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_errorsMtpBatchStreamProcessor.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_allocResourceExhausted(retryable)runtime_errorInvalidArgument(用户语法错误)、其他 → 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),修复了 allocateSamplerInputstorch::empty 分配布尔掩码后 MTP 路径从未初始化的隐患;同时去掉 model_output.logits.clone()(第 151-153 行有注释说明可复用),省掉每步 [B*(P+1), vocab] 拷贝,并有测试断言复用同一 data_ptr
  • 约束收敛的 Python 侧设计清晰:GRAMMAR_FIELD_NAMESLiteralget_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.cc 13 处断言统一改用 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 在根 BUILDrtp_llm/BUILD data 与 rtp_llm/libs/BUILDwhl_package_libs 三处均补齐;新增 config_pickle_test.py 为 pybind pickle 这类通常无人测的边界补上覆盖,含「伪造短布局必须被拒绝」的负向用例。
  • server_args_test.py:400-412 在改动全局 os.environ / sys.argv 之前addCleanup 注册恢复并写明「setUp 自身抛异常时 tearDown 会被跳过」的理由,避免污染同进程内其他用例。
  • PackedMaskLogitsCpuFallbackTest.ccExecOpsTest.cc 用同一组期望值交叉验证 CPU 与 CUDA 两条实现路径,覆盖 compact row mapping、单行 identity、越界 row 跳过、非连续输入回写四类边界;3rdparty/xgrammar/xgrammar.BUILD3rdparty/dlpack/BUILD 均把公开头/内部头拆分并注明动机,可追溯性好。

@@ -3,27 +3,47 @@

def init_grammar_group_args(parser, grammar_config):
grammar_group = parser.add_argument_group("Grammar Configuration")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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 出问题可运维回滚」的能力不丢失,下个版本再彻底移除。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

不需要关停开关, 之前dash sc引入的变量 可以删除。

encoded_vocab, json.dumps(metadata, separators=(",", ":"))
)

raise ValueError(f"Unsupported tokenizer type: {type(tokenizer)}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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 升级为 RuntimeErrorrtp_llm_op.py:30-32start()无条件用其结果填充 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...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

预期行为, 启动必须带grammar, 构建失败就fail

@@ -136,7 +136,6 @@ message GenerateConfigPB {
google.protobuf.StringValue regex = 70;
google.protobuf.StringValue ebnf = 71;
google.protobuf.StringValue structural_tag = 72;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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 5reserved 12reserved 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 字段、不再上线传输,便于混部窗口定位约束失效。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

dash sc带入的, 无需传递

) -> "ResponseFormatPlan":
final_constraint = _resolve_final_constraint(config)

if config.in_think_mode:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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」的用例固化期望行为(明确错误码与文案,或明确降级)。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

预期行为, 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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-94QueryConverter.ccGenerateConfig(并 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...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

预期行为, 完全通过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]);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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.pydash_grpc_comparer.pydash_sc/test/codec_test.pymodel_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] 逻辑变更未混入无关格式化

Comment thread rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc
Comment thread rtp_llm/cpp/models/logits_processor/GrammarLogitsProcessor.cc
Comment thread rtp_llm/cpp/engine_base/stream/GenerateConfig.h
Comment thread rtp_llm/cpp/models/logits_processor/test/BUILD

@LLLLKKKK LLLLKKKK left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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_logits kernel 也改用 MaskedLogitValue<T>(),并把 ExecOpsTest.cc:360std::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)。
  • 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_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 的移除及迁移方式,并补一条断言旧参数行为的单测。
  • grammar tokenizer 元信息成为所有语言模型的启动硬依赖,不支持的 tokenizer 直接阻塞模型加载且无逃生口 @ rtp_llm/config/grammar_tokenizer_info.py:195
    • 建议:把「启动即失败」降级为「按请求失败」:新增(或复用上一条建议的)服务级开关,开关关闭或元数据构建失败时记 WARNING 并返回空串,让 XGrammarBackend::createtokenizer_info_json.empty() 分支返回 nullptr(:67-70 已有该分支),仅在实际发起 grammar 请求时返回明确的 INVALID_PARAMS。同时把 create 对反序列化失败的 rethrow 改为可配置行为。若坚持 fail-fast,请在 PR description 中列出受影响的 tokenizer 类型清单、非 CUDA 平台结论与规避手段。
  • 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_LOG WARN,并在 proto 注释中标注这两个字段已由 frontend 编译消费、engine 不再执行;2)若需继续支持裸 gRPC 客户端,则恢复一条 think-only 的降级构造路径。同时在 PR description 中写明 frontend 必须先于 engine 升级。
  • MTP 新增启动期 abort 与请求期硬拒绝,把既有可运行组合打死且无过渡开关 @ rtp_llm/cpp/normal_engine/speculative/MtpExecutor.cc:230
    • 建议:fail-fast 方向正确,但需补齐迁移与运维面:1)在 release note 中列出受影响组合(MTP + TREE_DECODE_CONFIGcombo_token_size>0 的推荐类请求、num_return_sequences>1);2)把启动期 abort 改为「启动告警 + 该能力不可用」,或提供默认开启、可临时关闭的校验开关,避免只能回滚镜像;3)为请求级拒绝路径补一条可聚合的 RTP_LLM_INTERVAL_LOG WARN 或 metric,便于上线后快速发现被拒流量。

Non-blocking Suggestions

P2

  • grammar 编译失败的状态码在 factory 边界被压平为 INVALID_PARAMS,丢失可重试语义 @ rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:120
    • 建议:在 factory 处按 matcher_or.status().code() 映射:kResourceExhaustedErrorCode::MALLOC_FAILED(或其他可重试码),kInvalidArgumentINVALID_PARAMS,其余归 EXECUTION_EXCEPTION;并补一条单测断言资源类失败不会被映射成 INVALID_PARAMS,把分类语义固化为回归。
  • 新增 6 个 GRAMMAR_ 错误码未指定 ExceptionCategory,对外一律退化为 internal* @ rtp_llm/config/exceptions.py:45
    • 建议:按语义逐一补齐 category:用户输入/配置类(GRAMMAR_VOCAB_EXCEEDS_MODEL_VOCABGRAMMAR_EOS_OUT_OF_VOCAB)→ BAD_REQUEST;模型输出违背语法类(GRAMMAR_PARSER_REJECTED_TOKENGRAMMAR_NON_EOS_AFTER_TERMINAL)→ INVALID_OUTPUT;确属内部不变量破坏的(GRAMMAR_BITMASK_BUFFER_TOO_SMALLGRAMMAR_VERIFY_EXCEPTION)保留 INTERNAL 并加注释说明。同时在 servicer_test.py 补一条按 category 断言 Dash 错误码映射的用例,避免后续新增码再次漏标。
  • 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 分支逻辑」。
  • 受约束 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 循环的影响。
  • xgrammar 依赖钉在上游未合并特性分支 commit,且绕开仓库统一依赖入口 @ 3rdparty/xgrammar/repositories.bzl:5
    • 建议:把 xgrammar 声明合并进仓库统一入口(git_deps() 或带 sha256http_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_sortp_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() 与逐 token acceptToken / 首个 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 不全的负例。构建测试只 mock serialize_grammar_tokenizer_info 这一原生扩展边界即可(该 target 已通过 data = ["//:th_grammar_tokenizer_info"] 具备加载真实 .so 的条件,可参考同文件 test_native_serializer_accepts_raw_bytes_vocab)。
  • 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_onlystream: true 组合,或对流式响应先拼接增量再复用同一 validate_constraint。更好的做法是把「取 grammar 字段 + 缺失即报错」抽成 grammar_constraint_validator.py 中的公共入口供两个 comparer 共用。
  • 结构化标签校验器对无 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_deps load(或在 deps 中显式加上以明示依赖),并把 grammar_xgrammar 的 Tier 约束落到 visibility 白名单上,使注释与构建图一致;若目标是让它成为真正的 CPU-only 测试,建议评估从 grammar_xgrammar 移除该包源文件并未使用的 torch_deps()
  • 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 名称三者一致,避免后续排障按错误前提定位。
  • 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 形参及其调用点。
  • 服务级 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、这是服务级策略而非请求级参数。
  • 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 字段补一条断言用例。
  • response_format 入参收紧为 extra="forbid",旧客户端多传字段由静默忽略变成 400 @ rtp_llm/config/response_format.py:37
    • 建议:对嵌套的 ResponseFormatJSONSchema 改用 extra="ignore"(保留顶层 ResponseFormatextra="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_valuevalidate_not_empty 中显式处理布尔与空 schema:json_schema is FalseERROR_INPUT_FORMAT_ERROR 拒绝并说明「schema false 无可生成 token」;json_schema == {} / True 明确按「无约束」处理(返回 None)或同样拒绝,二选一后在 generate_config_test.py 补断言。
  • 单一 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 整数按版本号分派,避免类型嗅探。
  • 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 拖住准入线程。

P3

  • ASSERT_FLOAT_EQ 无法区分 -FLT_MAX 与 -INFINITY,掩蔽哨兵断言被弱化 @ rtp_llm/cpp/models/logits_processor/test/TreeLogitsProcessorTest.cc:238
    • 建议:对哨兵值这类需要精确匹配的常量改用位级或精确比较(比较 std::bit_cast<uint32_t> 后的位模式,或沿用同目录 RecommendationLogitsProcessorTest.cc:400data1[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_indexRTP_LLM_LOG_ERROR,使该路径在日志与指标上可归因。
  • 大量与 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_infogrammar_tokenizer_info_pythonxgrammar_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.pyconfig_pickle_test.py 都直接读写该属性。但本 PR 已同步修改的 libth_transformer_config.pyi:544-556GrammarConfig 只声明了 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 未声明 envexec_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 也不是已合并进主干的提交,且无 sha256shallow_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:122validate_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_configgrammar_constraint.py:146-156)的逻辑:逐个 validate_not_empty()len(...) > 1UNSUPPORTED_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 中 boolint 的子类;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_mtpsmoke_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_optionjson.dumpsTypeError/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_optionjson.dumpsTypeError/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 数据的说明注释引用了不存在的源文件与不存在的用例文件
    该文件 _commentGrammarManager.cc:473-474ReasonerGrammarObject 列为历史 bug 位置,并声称与 q_r_mtp_grammar_reasoning_concurrent.json 配对形成 thinking ON + 并发轴。全仓搜索 GrammarManagerReasonerGrammarObjectq_r_mtp_grammar_reasoning_concurrent 只命中该注释自身,三者在仓内均不存在(本 PR 的 grammar 实现是 XGrammarBackend.cc / RtpGrammarMatcher.cc / GrammarLogitsProcessor.cc)。这些注释是后续排障的主要线索来源,指向失效会直接误导读者。
  • [I] 代码质量 — 同一功能用统一工具函数 → issue 单一 grammar 约束规则存在两份重复实现,存在行为漂移风险
    _resolve_final_constraint(:138-148)完整复写了 GrammarConstraint.resolve_from_configgrammar_constraint.py:146-156)的逻辑:逐个 validate_not_empty()len(...) > 1UNSUPPORTED_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_optionjson.dumpsTypeError/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_errorsStreamSpecUpdateInfo::error_info,拦截发生在 append token 之后、publish output 之前,违规 token 不会吐给用户。
  • 掩码构造一律 fail-closed 而非 fail-open:prepareSpecMask 在 vocab 越界、EOS 越界、fillBitmask/rollback 失败等任一路径都 markFinished() 并把首行压成仅 EOS;mergeProcessorMasksSpecLogitsVerifyRunner.cc:190-197)在 prepareSpeculative 失败时把 cap 归零,未受约束的 token 不会漏出。
  • acceptCommittedLockedold_matcher_len/old_output_len 保证多 token 提交失败不留下任何可见前缀,回滚再失败时二次 markFinished(),状态机不停在不确定态。
  • 分层收敛干净:engine_base/grammar/BUILD:35 显式声明只有 Tier 0/1 可依赖,XGrammarBackend.h 只对外暴露 GrammarKeyCppGrammarLogitsProcessor.h 仅前向声明 RtpGrammarMatcher,xgrammar 类型未泄漏到采样层。
  • matcher 异常边界干净:matcherCall 模板把上游所有抛出统一收敛为 GRAMMAR_VERIFY_EXCEPTION,接口全部 [[nodiscard]] 且禁用拷贝/移动,并用 static_assertXGrammarBackendTest.cc:39-42)把不可移动语义固化为编译期契约。
  • 错误分类可运维:compileWithErrorClassificationXGrammarBackend.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_NAMESLiteralget_args 派生;--grammar_num_workers 的 help 明确为 max_compiler_threads(单次编译内并行度),消除长期运维误解;序列化只在 _trans_jsonable_option 的 protobuf 边界做一次 json.dumps,异常统一带字段名并 raise ... from e
  • 打包链路闭环:新增 //:th_grammar_tokenizer_info 独立 .soarch_select.copy_all_sortp_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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P1] fp32 掩蔽哨兵值在生产 kernel 与新断言间分叉,cuda_mask_logits_op_test 必然失败

该用例把 fp32 期望值改为 -FLT_MAX,并在第 64 行用严格 == 比较。但调用链 runtimeMaskLogitsCudaOps.cc:319-321)→ invokeMaskLogits<float>mask_logits kernel 本 PR 未改,仍写 NegativeInfinity<T>() = -INFINITYmask_logits.cu:83);新增的 MaskedLogitValue<T>()-FLT_MAX)只被 packed_mask_logits 使用(:121)。同 PR 的 ExecOpsTest.cc:360 对同一 runtimeMaskLogits/fp32 断言 std::isinf,两处期望互斥。cuda_mask_logits_op_testcuda/ops/tests/BUILD:29)无 manual 标签且带 exec_properties={'gpu':'H20'},会在门禁执行。

建议: 先确定「被屏蔽 logits」的唯一真相值。若目标是与 BaseLogitsProcessor::neg_inf 对齐,则把 mask_logits kernel 也改用 MaskedLogitValue<T>(),并把 ExecOpsTest.cc:360std::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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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-219request.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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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.hGrammarConfig 也已无该字段。全仓搜索 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)}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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::createtokenizer_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.",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] 新增 golden 数据的说明注释引用了不存在的源文件与不存在的用例文件

该文件 _commentGrammarManager.cc:473-474ReasonerGrammarObject 列为历史 bug 位置,并声称与 q_r_mtp_grammar_reasoning_concurrent.json 配对形成 thinking ON + 并发轴。全仓搜索 GrammarManagerReasonerGrammarObjectq_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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] GrammarConfig 新增字段未同步到 .pyi 类型存根

ConfigInit.cc:1325 新增 def_readwrite("terminate_without_stop_token", ...)grammar_group_args.py:17 通过 bind_to 写入,server_args_test.pyconfig_pickle_test.py 都直接读写该属性。但本 PR 已同步修改的 libth_transformer_config.pyi:544-556GrammarConfig 只声明了 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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] model_rpc_client 新增的两条错误分支缺负向测试,且内部契约违约上报为用户输入错误

本 PR 在 RPC 边界新增两条失败路径:_trans_jsonable_optionjson.dumpsTypeError/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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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_indexRTP_LLM_LOG_ERROR,使该路径在日志与指标上可归因。

Checklist: [6.1] 可观测性:日志/指标/超时可操作、非噪声;[6.1] 错误语义:fail-fast/retry/fallback/silent 行为显式

@@ -9,25 +9,22 @@ using namespace std;
namespace rtp_llm {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

📍 实际位置 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 LLLLKKKK left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.pyassertNotIn(..., 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 与迁移方式,并补一条断言旧参数行为的回归测试。
  • grammar tokenizer 元信息成为所有语言模型的启动硬依赖,不支持的 tokenizer 直接阻塞上线且无逃生口 @ rtp_llm/config/grammar_tokenizer_info.py:195
    • 建议:区分「grammar 能力缺失」与「启动失败」:构建失败时记 error 并把 tokenizer_info_json 置空(与 real_tokenizer is None 分支一致),由首个 grammar 请求返回明确的 UNSUPPORTED_OPERATIONGRAMMAR_* 码已具备)。若坚持启动期 fail-fast(注释已声明为有意设计),必须同时提供运维旁路开关,并为「不支持的 tokenizer」「空 stop_token_ids」两条路径补启动期集成测试与受影响 model/tokenizer 清单。
  • 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 的吞吐实测数据。
  • in_think_mode / max_thinking_tokens 仍在 wire 上,但引擎侧已无任何执行者,退化为静默 no-op @ rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:99
    • 建议:明确该组合的语义并落实:对「in_think_mode=true 且四个 grammar 字段全空」的请求在 createLogitsProcessorsvalidate_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 显式声明能力,而不是依赖基类默认值把它们一并拒掉。

Non-blocking Suggestions

P2

  • grammar 编译失败的状态码在 factory 边界被压平为 INVALID_PARAMS,丢失可重试语义 @ rtp_llm/cpp/models/logits_processor/LogitsProcessorFactory.cc:120
    • 建议:按 absl::StatusCode 显式映射:kInvalidArgumentINVALID_PARAMSkResourceExhaustedMALLOC_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_TERMINALINVALID_OUTPUTGRAMMAR_VOCAB_EXCEEDS_MODEL_VOCAB/GRAMMAR_EOS_OUT_OF_VOCABBAD_REQUESTUNSUPPORTEDGRAMMAR_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 可观测行为的断言,防止后续重构悄悄改变失败模式。
  • 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 或原子交换消除无同步写。
  • 反序列化后用 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 行为被认为不受影响」这一前提。
  • 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::buildprepareSpecMask 中显式清掉这些 bit,并把该不变量写入注释。
  • xgrammar 依赖绕开仓库统一入口,并钉在上游未合并的特性分支 commit 上 @ 3rdparty/xgrammar/repositories.bzl:5
    • 建议:把 xgrammar 声明并入 git_deps()(保留 xgrammar.BUILD overlay),与 cutlass 等保持一致;改用带 sha256 的归档或内部镜像固定该 commit,并在注释写明所依赖的上游特性与合入计划,上游合并后切到 tag;修正 dlpack 注释为真实原因并补许可证声明。另建议给 xgrammar.BUILD 中依赖 submodule 路径的 globallow_empty = False,把缺文件变成分析期明确失败。
  • //rtp_llm:ops 无条件 data 依赖新 so,把 xgrammar 编译放大到全仓 Python 目标 @ rtp_llm/BUILD:195
    • 建议:保持 Bazel 侧与 Python 侧同样惰性:把该 data 依赖下沉到真正调用 serialize_grammar_tokenizer_info 的目标,或拆出独立的 py_library(name="ops_grammar");若确需全局可用,请在 BUILD 注释写明原因,避免后续被误删或继续扩散。
  • 服务级策略与同名请求级字段并存,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 上统计以统一口径。
  • 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_sizesp_model 以及三者都不满足的 fake tokenizer,断言分类结果;再补一条不 mock 的端到端用例(from rtp_llm.ops import serialize_grammar_tokenizer_info + 小词表调用 build_grammar_tokenizer_info_json,相应补 :ops 依赖),覆盖真实 import 解析与本 PR 新增的打包约定。
  • 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 失败。
  • 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 精简为约束校验所需的最小结构,或在约束校验后追加一次「仅比对确定性字段」的比对。
  • 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 把 legacy structures/triggers 真正转换为 {"type":"structural_tag","format":{...}} 规范形态,或按与 _check_payload 相同的规则在归一化阶段 fail-fast;validate_not_empty 扩展为拒绝 False/空 dict(或明确记录其语义),并补直传 legacy 形态、json_schema=Falsejson_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 分派——后续任一字段类型变化都会让嗅探静默走错分支并写入错误配置。
  • finalize 之后 update() 仍可改写 grammar 字段,不会重新编译且报错信息误导 @ rtp_llm/config/generate_config.py:487
    • 建议:在 update()/update_and_pop() 中检测「已 finalize(_reasoning_envelope_applied)且本次更新触及 GRAMMAR_FIELD_NAMESin_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 入口」。
  • grammar 编译位于请求准入同步路径,成功路径仅 DEBUG 日志且无耗时指标 @ rtp_llm/cpp/engine_base/grammar/XGrammarBackend.cc:46
    • 建议:为编译耗时、缓存命中/未命中、编译后内存大小上报 metrics(复用现有 metrics_reporter_ 体系),并把成功日志改为按耗时阈值提级(例如超过阈值打 INFO/WARN),使慢 schema 在生产可被发现。
  • 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),并在 Python ExceptionType 侧同步声明与 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
  • 语法处理器未强校验增量 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_formattrans_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 中列出「仅格式化」的文件清单,同时确认其断言现代化未改变任何用例语义。

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_binarygrammar_tokenizer_info_python → xgrammar_tokenizer_info → @xgrammar//:xgrammar 拉入整份 xgrammar C++ 编译。凡依赖 :ops 的 Python 目标(含纯前端、纯 CPU 单测)都被动承担该构建成本,而 rtp_llm/ops/__init__.py:193-201serialize_grammar_tokenizer_info 本身是函数内惰性导入。仓库既有约定是消费方自行声明 so:本 PR 的 config_pickle_testgrammar_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:129MtpExecutor.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.bzlgit_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.pyopenai_comparer.pymodel_rpc_client_test.pydash_sc/test/*.pyRecommendationLogitsProcessorTest.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.pyopenai_comparer.pymodel_rpc_client_test.pydash_sc/test/*.pyRecommendationLogitsProcessorTest.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.pyopenai_comparer.pymodel_rpc_client_test.pydash_sc/test/*.pyRecommendationLogitsProcessorTest.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 段落,而工厂已不再创建它;核心新增件 GrammarLogitsProcessorSpecLogitsVerifyRunner 与新的 mtpCapability 协商契约在文档中完全缺失,第 80 行起的「Related source files」也未列出 rtp_llm/cpp/engine_base/grammar/**;第 19 行仍称 Tree 支持 beam/num_return_sequences,未提示与 MTP 的新互斥关系。代码侧 ThinkModeLogitsProcessor.{h,cc} 现仅被自身单测/BUILD/注释引用,createLogitsProcessorsmax_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_errorsStreamUpdateInfo.error_info(MTP 侧另有 SpeculativeSamplerOutput.processor_errors),GenerateStream::update/specUpdate 在发布 token 前先报错返回,出错 token 不外泄。
  • MTP 投机验证状态机纪律扎实:prepareSpecMaskprovisional_accepts 精确记账并在返回前 rollback,回滚失败升级为 fail-closed(markFinished() + 仅放行 EOS),保证「验证绝不污染已提交状态」(GrammarLogitsProcessor.cc:129-142)。
  • 多 token 提交具备全批次原子性:acceptCommittedLockednumAcceptedTokens() 差值闭包同时回退 matcher 与 committed_output_len_,不残留半提交前缀;并刻意区分 TERMINATED 与 FINISHED,覆盖 min_new_tokens/ignore_eos 延迟结束时仍只放行 EOS 的场景(:423-437 有注释说明理由)。
  • 词表边界双向收口:validateVocabSize 拒绝 grammar vocab 超过 logits vocab,clearBitmaskTokenRange 与 decode 侧 tail fill_(neg_inf) 共同保证 grammar 词表之外 token 永不可采样;bitmaskAllowsToken 对负数与越界一律判否。
  • 约束归一化收口到单一 owner:GrammarConstraintLiteral + get_args 派生 GRAMMAR_FIELD_NAMES(单一来源),序列化只在 _trans_jsonable_option 发生一次,并把 TypeError/ValueError/RecursionError 转为带字段名的 ERROR_INPUT_FORMAT_ERRORraise ... from e 保留异常链。
  • validate_engine_ready 是只读断言(response_format_compiler.py:192-206),配合 model_rpc_client_test.pymodel_dump() 前后对比,把「RPC 边界不得改写 grammar」固化为可测不变量。
  • 新增 validateLogitsProcessorState()committedOutputLen()outputTokenLen() 校验,把「processor 状态机与 stream 历史脱节」从静默错配转为显式错误,并有对应单测。
  • 错误码跨语言逐一对齐:ErrorCode.h 608-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.socc_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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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.ccTRANS_OPTIONAL(response_format) 同批删除,说明引擎此前确实消费该字段。灰度期旧 frontend → 新引擎时 73 成为 unknown field 被静默丢弃:约束消失但请求 200 返回,无日志无指标;编号被后续以别的类型复用还会造成类型错配。

建议:reserved 73;reserved "response_format";model_rpc_client_test.pyassertNotIn(..., 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(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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)}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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_OPERATIONGRAMMAR_* 码已具备)。若坚持启动期 fail-fast(注释已声明为有意设计),必须同时提供运维旁路开关,并为「不支持的 tokenizer」「空 stop_token_ids」两条路径补启动期集成测试与受影响 model/tokenizer 清单。

) -> "ResponseFormatPlan":
final_constraint = _resolve_final_constraint(config)

if config.in_think_mode:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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 > 1ERROR_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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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 = 33QueryConverter.cc:106-107 仍写入 GenerateConfig,全 rtp_llm/cpp 对二者的唯一使用是 :113-116 的一行 DEBUG 日志;全仓搜索确认 ThinkModeLogitsProcessor 只被自身单测、BUILD 与文档引用。任何不经新 Python 编译链路的 gRPC 客户端设置 think 预算后将被完全忽略,无错误、无日志、无指标。

建议: 明确该组合的语义并落实:对「in_think_mode=true 且四个 grammar 字段全空」的请求在 createLogitsProcessorsvalidate_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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] 语法处理器未强校验增量 token 布局,full-position 张量会静默污染解析器

updateStatus 仅断言 new_tokens.size(1) >= num_new_tokens,随后从 data_ptr<int32_t>() 起始位置连续读取 n 个 token(:358-361)。同仓 RecommendationLogitsProcessor.cc:350-356use_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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] max_rollback_tokens 硬编码 200,与实际回滚需求量级不匹配

构造参数默认 max_rollback_tokens = 200(:19)并落为成员默认值(:49),XGrammarBackend::createMatcher(XGrammarBackend.cc:180)只传 terminate_without_stop_token,从不覆盖该默认值。实际回滚上界很小且可静态推导:prepareSpecMask 最多回滚 propose_stepacceptCommittedLocked 最多回滚 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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[P3] max_thinking_tokens=0 仍会生成 any_text.max_tokens=0 的退化 reasoning 包裹

compilein_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):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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_formattrans_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:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[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.pyopenai_comparer.pymodel_rpc_client_test.pydash_sc/test/*.pyRecommendationLogitsProcessorTest.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] 逻辑变更未混入无关格式化

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants