Skip to content

feat: support context custom output processors - #1432

Merged
CHW0218 merged 2 commits into
mainfrom
feature/custom-output-main-20260915
Sep 23, 2026
Merged

CHW0218 merged 2 commits into
mainfrom
feature/custom-output-main-20260915

Conversation

@CHW0218

@CHW0218 CHW0218 commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Normal LLM generation can return deployment-defined postprocessing for one selected prompt-token position. The existing CustomModule/CustomHandler receives selected rows from the model's final normalized output and returns a tensor; no MLP or activation is prescribed.

  • Select available prefill rows, invoke the handler once per batch on TP rank 0, and retain the result during decode. Reuse existing model outputs and the current model, stream and RPC lifecycle.
  • Preserve prefix caching and batch/multi-sequence response contracts. A selected position covered by prefix reuse produces no custom output. Return native custom_output or OpenAI extra_outputs.custom_output.
  • Keep token selection, processing and optional weights in the deployment module. Selector input rejections (ValueError or an out-of-range position) report INVALID_PARAMS; unexpected execution errors report EXECUTION_EXCEPTION.

Current scope is Python-model PDFUSION without speculative decoding or prefill CP. TP uses eager prefill and optional decode graphs; generation-prefill graphs require TP1. Handlers must not perform TP collectives. Model implementations, attention and graph layouts are reused.

Rebased onto 017648a37d. The output-dispatch conflict is resolved by retaining the upstream worker pool and capturing each request's compact custom-output row index by value when its task is queued. The existing custom-output dispatch test now uses the upstream serial/parallel fixture (0 and 2 workers), without adding another test implementation.

zhangjianning.zjn's paged-prefill graph fix is already on main as 9cb90a4e52, so its duplicate cherry-pick has been removed from this PR. Its original authorship remains in main. The one-line access-logger test runtime dependency remains a separate commit, 9e979f0249.

Post-rebase validation on this exact head (85a3ac9c85), CUDA12.9/H20, passed:

  • All 40 gather/dispatch cases, including custom-output dispatch with 0 and 2 workers; model/graph integration (10 passed, one existing skip); four frontend test methods.
  • Five full-checkpoint Qwen3 BF16 HTTP configurations: plain-generation TP1 prefill-graph baseline, custom TP1 eager/prefill graph, and custom TP2 eager/decode graph. Generated greedy token IDs match the baseline.
  • Two additional TP2 eager/decode-graph configurations with OUTPUT_DISPATCHER_WORKER_COUNT=2, confirmed in service logs. Generated IDs and custom-output tensors match the corresponding serial configuration exactly.
  • Cache-hit omission, selection error 605 and recovery, batch/beam/multi-sequence response contracts, OpenAI prefill extra outputs and SSE n=2 passed.

Both runs used the repository test-execution workflow, a non-root container, GPU lock, remote build cache, local execution and uncached test results. Changed-line clang-format and whitespace checks passed. No source or tests were changed after these runs. This validates the listed integration configurations, not an end-to-end performance or ROCm claim.

Custom-output feature: 37 files, +917/-39, including 477 added test/BUILD lines and 54 documentation lines. Full PR including the independent logger fix: 38 files, +918/-39, including 478 added test/BUILD lines. Current-head hosted checks and required approval remain prerequisites for merging.

Comment thread rtp_llm/cpp/pybind/multi_gpu_gpt/RtpLLMOp.cc Outdated
Comment thread rtp_llm/cpp/model_rpc/model_rpc_client.py Outdated
Comment thread rtp_llm/cpp/model_rpc/model_rpc_client.py
Comment thread rtp_llm/cpp/engine_base/stream/StreamCacheResource.cc Outdated
Comment thread docs/examples/custom_output_processor_example.py Outdated
Comment thread rtp_llm/cpp/normal_engine/NormalModelInputGatherer.cc
Comment thread rtp_llm/cpp/engine_base/executor_base/PostLayersProcessor.cc Outdated
Comment thread rtp_llm/cpp/model_rpc/QueryConverter.cc
Comment thread rtp_llm/cpp/cache/HybridKVCacheAllocator.cc Outdated
Comment thread rtp_llm/cpp/engine_base/executor_base/PostLayersProcessor.cc Outdated
Comment thread rtp_llm/cpp/engine_base/stream/GenerateTypes.h
Comment thread rtp_llm/cpp/models/PyWrappedModel.cc
Comment thread rtp_llm/cpp/engine_base/stream/GenerateStream.h
@rtp-llm-review-bot

rtp-llm-review-bot commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

PR #1432 评审:LGTM

  • 标题:feat: support context custom output processors(@CHW0218,open)
  • 评审版本:85a3ac9c850a

无阻塞项

非阻塞发现

已确认修复

  • [P1] initRPCServer 在未持有 GIL 的情况下对 py::object 做 incref/decref,存在引用计数竞争 — 当前代码中 gil_scoped_acquire(第352行)所在作用域从第351行持续到第426行才关闭,setPostLayersProcessor(第408行)与 post_layers_handler 清空(第413行)均在该作用域内持有 GIL,原数据竞争问题已不存在。
  • [P2] 显式 per-request 位置会静默丢弃 CUSTOM_OUTPUT_EXPECTED_TOKEN_ID 校验 — trans_input 第535-538行改为统一调用 selector.write_to(input_pb, position_override),而 write_to 第457-458行无条件写入 custom_output_expected_token_id,位置覆盖不再丢弃 expected_token_id 校验。
  • [P1] trans_output 用流索引 i 直接索引 custom_output,混合 decode/context 批次时会错位或越界 — 发现前提已不成立:QueryConverter.cc:368 的 stackBuffersToTensorPB 用 RTP_LLM_CHECK_WITH_INFO(tensor_opt.has_value(), "Inconsistent tensor presence in a batch for stacking.") 强制 all-or-nothing(并非只堆叠已定义源),且 transResponse 按流调用(LocalRpcServer.cc:222/463),故 output_pb.custom_output 恒有 num_outputs 行,model_rpc_client.py:863 的 all_custom_output[i] 不会错位/越界。
  • [P3] incrKVBlock 设置的 max_reuse_len 在 decode 路径未被消费 — max_reuse_len 字段已从 MallocInfo 结构体(rtp_llm/cpp/cache/Types.h:61-78)中完全移除,incrKVBlock(StreamCacheResource.cc:379-414)也不再设置该值,原第 458 行的死代码赋值已删除
  • [P2] 示例注释与返回值矛盾:注释称用 PRE_FINAL_NORM,实际返回 POST_FINAL_NORM — 第 71–73 行注释已改为说明默认使用 final normalized output(POST_FINAL_NORM),且仅在 head 训练于最后 decoder block 输出时才切换为 PRE_FINAL_NORM,与 return HiddenStateStage.POST_FINAL_NORM 一致,矛盾已消除。
  • [P1] custom_output_indexes 用 prefixLength() 计算索引,但边界校验用 reuseLength(),两者不一致时选中错误的 token — 前提不成立:GenerateStream.cc:477-482 中 prefixLength() 与 reuseLength() 均直接返回同一成员 reuse_length_,故第549行 target_position - stream->prefixLength() 等价于 target_position - reuseLength(),不存在所述偏移,属误报。
  • [P2] per-request custom_output_token_position 可绕过 last_hidden_states 的校验,位置选择数据被当作 last_hidden_states 传入 — PyWrappedModel.cc:1216-1218 新增 TORCH_CHECK(usesSelectedHiddenStates() || !custom_output_indexes.defined() || numel()==0, "custom output token selection requires a handler declaring selected_hidden_states"),per-request 选择输入在 handler 声明 last_hidden_states 时会被拒绝,绕开 env 校验的路径已被封堵。
  • [P2] trans_output 用循环下标 i 索引 custom_output,混合 decode/context 批次可能错位 — QueryConverter.cc 第419行 stackBuffersToTensorPB 已改为 RTP_LLM_CHECK_WITH_INFO(tensor_opt.has_value(), ...) 强制校验批次内所有项存在,不再跳过未定义张量,因此堆叠行数恒等于 source_outputs.size(),Python 端 all_custom_output[i] 不会错位。
  • [P2] max_reuse_len 截断后仍 drop-last,导致 Hybrid 比 SingleType 少复用 1 个 block — 第 229-233 行已调整顺序:先 drop-last(第 229 行 match_keys 去掉末 key),再按 max_reuse_len/reuse_unit_tokens 截断(第 230-233 行),截断发生在去尾之后,不再重复丢弃完整块,与 SingleType 的 min(size-1, floor(max_reuse_len/block)) 语义一致。
  • [P3] parseSelectorEnv 对可能为 NULL 的 value 直接调用 strtol,依赖调用方前置判空 — 第24-26行新增 if (value == nullptr) throw ...,parseSelectorEnv 已自行判空,不再依赖调用方前置判空
  • [P3] custom_output_token_position 注释 "-1 selects the last token" 与实际语义不符 — 第101行注释已改为 "Resolved zero-based position in input_ids; -1 means no selector (last-token fallback)",明确 -1 表示无 selector、last-token 回退在别处实现,不再是误导性的 "-1 selects the last token"。
  • [P3] customOutputIndexes 对 CUDA 张量调用 hold_host 是空操作,注释与行为不符 — needsPreFinalNormCapture 已整体删除,prepareAttentionInputs(L822)/updateKVCacheKernelBlockId(L843) 不再因该判断跳过 CUDA graph 准备,can_run_graph(L966-967) 不再因 pre_final_norm_output_indexes.defined() 禁用 graph,forward 的捕获(L951)改用 custom_output_pre_norm_ 且 layer-microbatch 在 L890 提前 return 到 forwardMicroBatched,注释与行为已一致。
  • [P3] StreamUpdateInfo 新增 const torch::Tensor 成员使结构体不可拷贝/移动赋值 — 作者反驳成立:GenerateStream.h 第 33–43 行本 PR 前即已有 new_tokens/hidden_states/logits/softmax_probs/cum_log_probs/all_probs/loss/src_batch_indices/all_hidden_states 共 9 个 const torch::Tensor 成员,拷贝/移动赋值早已被删除;第 52 行新增的 custom_output 未引入新的不可赋值限制,发现的前提不成立。
  • [P3] custom_output 处理失败时无条件覆盖 sampler 错误信息 — 第 396 行的条件已从无条件覆盖改为 if (context_batch_idx >= 0 && !model_output.custom_output_error.empty() && !error_info.has_value()),新增 !error_info.has_value() 保证 sampler 错误存在时不再被 custom output 错误覆盖。
  • [P2] customOutputIndexes 在 pre_final_norm 路径对同一输入重复执行 H2D 拷贝与 index_select,且两次调用结果可能不一致 — customOutputIndexes 的两个调用点互斥:L893-894 微批路径提前 return forwardMicroBatched,仅其 callForwardPostLayers(…,false) 在 L1152 触发 index_select;普通路径 L1045 传 skip_final_layernorm=true,L1152 被 !skip_final_layernorm 短路,pre_final_norm_hidden 直接取 Python 返回值(L1238),故每次前向仅调用一次,不存在重复 H2D/index_select。
  • [P2] 从 .py 文件加载的处理器模块未注册到 sys.modules,模块内相对导入会失败 — create_post_layers_module 函数及 CUSTOM_OUTPUT_PROCESSOR 相关代码路径已整体删除(当前 utils.py 全文仅 80 行),全仓库 grep custom_output_processor/create_post_layers_module/post_layers_handler 均无匹配,该 .py 加载与 handler 检查逻辑已不存在
  • [P3] custom_output 类型注解未覆盖 handler 可返回的 int32 输出 — rtp_llm/utils/base_model_datatypes.py:12 新增 CustomOutput = Union[List[Union[float, int]], List[List[Union[float, int]]]],frontend_worker.py:47 与 :54 改用该类型,已覆盖 int32 输出
  • [P2] setHandler 服务端 env 校验与前端逐请求选择器解耦,split 部署下会误判启动失败 — PostLayersProcessor.cc、setHandler、parseSelectorEnv 及 CUSTOM_OUTPUT_* 环境变量已全部删除;现由后端 NormalEngine.cc:159/747 直接调用 py_model.custom_output_handler.select_token_position(CustomModule 方法),不再经 std::getenv 校验,前后端 env 分叉问题不复存在。
  • [P2] tpSyncModelInputs 对 custom_output_indexes 无条件 collect,与条件分配不一致 — collect lambda 在第453-457行已内置 t.defined() && t.numel() > 0 守卫,第477行的无条件 collect(inputs.custom_output_indexes) 对 undefined 张量会被安全跳过,不再送入广播,问题已消除。
  • [P2] tpSyncModelInputs 对可能未定义的 custom_output_indexes 无条件 collect — ModelTypes.cc 的 collect lambda(442-446 行)已增加 t.defined() && t.numel()>0 守卫,且 collect 列表中(448-487 行)已不含 custom_output_indexes,不再无条件 collect 未定义张量。
  • [P1] 单个请求的 custom_output 位置被 prefix cache 消费时用 RTP_LLM_CHECK_WITH_INFO 抛异常,会中止整个 step 而非仅失败该流 — 第 541–548 行已删除会抛异常的 RTP_LLM_CHECK_WITH_INFO,改为 target_position >= stream->prefixLength() 时写入索引、否则写 -1,不再中止整个 step。
  • [P3] customOutputIndexes 插入 GptModelInputIndex 枚举中间,改变后续枚举值 — 本轮改动在 GptModelInputIndex 中删除了 customOutputIndexes(diff 中 - customOutputIndexes,),当前 head 第 88–90 行 lmOutputIndexes 后直接是 comboPositionIds,中插已移除,后续枚举值不再偏移。
  • [P3] 部署级 expected_token_id 会被套用到请求级 position 覆盖值上,可能造成误校验失败 — 本轮改动已删除整个 CustomOutputSelector 类(含 write_to 中无条件写入 expected_token_id 的逻辑)以及 trans_input 中写入 custom_output_token_position/expected_token_id 的代码块,当前文件已无任何 custom_output_token_position / expected_token_id / CUSTOM_OUTPUT 相关写入(grep 无匹配),误校验路径不复存在。
  • [P3] trans_input 在 selector=None 时每次调用重复解析 env,与“解析一次”注释矛盾 — CustomOutputSelector 类及其 from_env 解析逻辑、trans_input 的 selector 参数及 if selector is None 回退分支已被整体删除(diff 中 -380~-458 行删除类,-515~-525 行删除 selector 回退;当前 head 第 438 行 trans_input 仅接受 input_py),重复解析 env 的问题已不存在。
  • [P3] str2nonnegative_int32 的 except ValueError 分支不可达 — str2nonnegative_int32 已整体删除,当前 util.py 全文38行仅含 str2bool 与 str2_cp_rotate_method,原 except ValueError 死代码随函数一并移除,问题不再存在。
  • [P2] load_module 注册到 sys.modules 但未设置 package,.py 处理器内相对导入仍失败 — import_util.py 的 load_module(第10-23行)已不再执行 sys.modules[module_name]=imported_module,且全仓库无任何调用方(grep 仅命中定义处),动态 .py processor 加载路径已整体移除,原相对导入失败场景不再存在。
  • [P1] pre_final_norm custom output 在 TP 多卡下 generation-prefill graph 状态各 rank 不一致 — model_supported 条件(514-517 行)已删除 && !custom_output_pre_norm_,使 generation-prefill graph 的启用判定在 TP 各 rank 一致;同时 553 行新增 capture_pre_final_norm = custom_output_pre_norm_ 把 rank0 特有的 pre-final-norm 捕获需求下沉到仅影响本地 index_select 的图参数,而非整体 gate 图。
  • [P2] 新增测试的 values 结构与 _format_response_new 单序列返回不匹配,count=1/2 会失败 — frontend_worker.py 第 301-304 行 _format_response_new 现已用列表推导遍历 generate_outputs 的每个序列并保留 tensor 形状(custom_outputs=[seq.custom_output.tolist()...]),count=1/2 时返回结构与测试 values 一致,不再只取 generate_outputs[0]。
  • [P2] incrKVBlock 设置的 max_reuse_len 在增量分配路径未被消费,chunked prefill 后续 chunk 可能复用 custom output token — incrKVBlock (StreamCacheResource.cc:386-394) 已不再设置 max_reuse_len,且 maxReusableMatchKeys 签名缩减为两参数(KVCacheAllocator.h:209),NormalModelInputGatherer.cc:543 改为仅当 position >= prefixLength 时才填充 custom_output_indexes,原 max_reuse_len 未被消费的问题随机制整体删除而不复存在。
  • [P3] max_reuse_len 字段注释为 token 数上限却赋值为位置索引,语义混用 — MallocInfo 结构体(cache/Types.h:61 起)已删除 max_reuse_len 字段,initKVBlock(StreamCacheResource.cc:183-228)也不再对其赋值,语义混用问题随之消除。
  • [P2] custom_output_indexes 只校验下界,chunked/incremental prefill 下选中 token 落在后续 chunk 时产生越界索引 — 发现前提不成立:custom_output_indexes 的写入位于 538–547 行 per-sequence 循环 for (i<current_batch_size) 内(每序列写一个索引,与 dispatcher 178 行按 cur_batch_size 推进一致),且无 chunked/incremental prefill(CompleteTokenIds.cc:46 seq_length_=inputLength 一次载入全量、NormalEngine.cc:749 校验 position<inputLength),故不存在越界索引。
  • [P2] custom_output 行数不匹配时静默丢失而非报错 — dispatchSingleStream 第 213-216 行新增了 custom_output_batch_idx + cur_batch_size > custom_output_cpu.size(0) 检查,命中时调用 stream->reportError(EXECUTION_EXCEPTION, "custom output row count mismatch") 并 return,不再静默丢失。
  • [P3] 示例中 custom_weight_info 注册键与 init 查找键前缀不一致 — 发现针对的 docs/examples/custom_output_processor_example.py 在当前 head 已不存在(read 返回 ENOENT,docs/examples 目录也不存在),该误导性示例已被删除,不再存在被用户照抄的路径。
  • [P1] customOutputIndexes 对已被 ensureModelInputsOnCuda 移到 CUDA 的 tensor 执行 hold_host 与非阻塞 H2D — 前提不成立:TensorHolder.h:16 的 hold_host 仅对 is_cpu() 张量保留引用,PyWrappedModel.cc:526 对已 CUDA 的 indexes 执行 .to(kCUDA,non_blocking) 为同设备同 dtype 空操作,且 ensureModelInputsOnCuda(NormalExecutor.cc:448) 仅在 RTP_LLM_DEVICE_INPUT 开启时才迁移,两条路径互斥、均无重复 H2D,属误报。
  • [P3] custom_output_processor_test 是纯 CPU 测试却申请 H20 GPU — rtp_llm/test/BUILD 中已无 custom_output_processor_test 目标,全文件无 H20(仅第421行注释说明无需 H20),对应测试及 processor-file loader 已删除,问题不复存在。
  • [P2] ImportError 回退范围缩小,internal 模块内部抛出的 ImportError 不再回退到 external 模块 — 第22-31行已将 create_internal_module 调用包进 try/except ImportError,非 LANGUAGE_MODEL 任务在内部 ImportError 时记录日志并继续走 external 回退,修复了 else 分支不在 try 内的问题;LANGUAGE_MODEL 分支因无 external 回退而显式 raise 属有意设计。
  • [P2] custom_output_selector_ 仅在设置过 custom_output_handler 的 rank(tp_rank==0)绑定,TP 下 custom_output_token_position 只在 rank 0 被设置 — 当前代码第 156–163 行已引入 construction_guard(cleanup_on_failure 在析构自定义输出选择器前先 py::gil_scoped_acquire 再置空),且析构函数第 331–335 行同样在持 GIL 下释放 custom_output_selector_,已修复构造失败时无 GIL 释放 py::object 引用的问题。
  • [P1] custom output 处理器在 generation-prefill CUDA graph 路径下永远不会被调用 — run_custom 执行块位于 line 1221,处于 if (lm_head) 块(line 1173)内,而 final_layernorm 块在 line 1168 已闭合;run_custom 判定(line 1144)与 skip_final_layernorm 无关,故 graph 路径 skip=true 时 handler 仍会被调用,原发现的前提不成立。
  • [P2] LANGUAGE_MODEL 下 create_internal_module 抛出的任意 ImportError(含函数内部缺失传递依赖)会中止启动,而非仅当 head 缺失时 — import_util.py:38-44 中 import_optional_internal_source_entrypoint 仅当 e.name 是 provider 路径本身或其前缀时返回 False,传递依赖缺失(e.name 非该路径)会 re-raise,普通 ImportError 也不被捕获,故 broken provider 不会被 utils.py:22 的 return None 静默禁用,发现前提不成立。
  • [P3] select_token_position 未在初始化时校验,未实现该方法的 handler 会让每个请求在运行时失败 — makeStream 在 line 752 增加 !input->fake_query 条件,且 createMinFakeStream 在 line 592 设置 fake_input->fake_query = true,warmup 假输入不再触发选择器调用,原 warmup 失败问题已修复。
  • [P2] custom_output 偏移按 cur_batch_size 推进与 gatherer 每请求一行语义可能错位 — gatherer 在 NormalModelInputGatherer.cc:522-545 按 currentBatchSize 逐序列(token_idx 每次递增,非每请求一行)写入 custom_output_indexes,与 NormalOutputDispatcher.cc:178 的 offset+=cur_batch_size 语义一致;测试 NormalBatchStreamProcessorTest.cc:1027-1059 验证 num_return_sequences=2 时每序列各得一行,发现前提“每请求一行”不成立,无错位。
  • [P3] 文档声称 float16/bfloat16 输出 "without value conversion",但实现会将其转为 float32 — docs/custom_output.md 第16-17行已将入参/返回形状分开描述为 'receives [rows, hidden_size] and returns [rows] or [rows, width]',第22-23行明确 dtype 经 RPC 保留、HTTP 序列化为数值数组;实现 runCustomOutput 仅将输入 cast 到模型 data_type 并原样返回输出(PyWrappedModel.cc:1244),原发现的两处文档错误均已修正。
  • [P3] custom_output_indexes 按流分配却按序列写入,num_return_sequences>1 时缓冲区越界 — 分配在 NormalModelInputGatherer.cc:338-339 已按 total_context_batch_size(每序列)而非每流,且 546 行写入值含 ctx.token_idx 偏移使各序列条目互异,测试 testCustomOutputDispatch(NormalBatchStreamProcessorTest.cc:1032)验证 num_return_sequences=2 时得到 {1,3},无越界无重复。
  • [P3] pre_final_norm + layer-microbatch 非图路径下 customOutputIndexes 被调用两次,产生冗余 H2D 与 hold_host — forward() 第893-895行在 enable_layer_micro_batch 时先 return forwardMicroBatched,早于第954-959行的 customOutputIndexes 调用,micro-batch 路径不再执行第一次调用,仅 forwardPostLayers 第1152行调用一次,双重 H2D 已消除。
  • [P1] layer-microbatch + pre_final_norm 路径下 pre_final_norm_hidden 未定义导致 custom output 必然报错 — layer-microbatch 分支现走 forwardMicroBatched,其在第 725 行以 skip_final_layernorm=false 调用 callForwardPostLayers(hidden_states, inputs, false),forwardPostLayers 第 1149-1153 行因此会在做 final norm 前执行 pre_final_norm_hidden = torch::index_select(hidden, 0, customOutputIndexes(inputs)),不再触发第 1228 行的 defined() TORCH_CHECK。
  • [P3] 文档把 extend_forward_args/extend_forward 列为 handler 契约,但基类默认实现与 selected_hidden_states 契约不兼容且未提示必须覆写 — docs/custom_output.md:10 以 "The handler implements" 明确这些方法为 handler 必须实现(即需覆写),第 17 行显式要求 extend_forward_args() 返回 ["selected_hidden_states"],且 initializeCustomOutput(rtp_llm/cpp/models/PyWrappedModel.cc:531-533)在启动时校验并拒绝未覆写的不兼容 handler,基类默认实现的差异已被契约声明与运行时校验双重覆盖。
  • [P3] customOutputIndexes 按 context 请求数校验行数,与 gatherer 按序列写入 custom_output_indexes 的语义冲突 — 第523-524行注释已改为“already-staged CUDA indexes require no host retention or additional copy”,且 TensorHolder::hold_host(TensorHolder.h:16)仅对 is_cpu() 张量持有、对 CUDA 张量直接跳过而无断言,故原“可能异常”的担忧不成立,代码行为本身正确。
  • [P2] custom_output_error 未随 RPC 序列化,split 部署下 handler 失败静默丢失 — custom_output_error 在 NormalOutputDispatcher.cc:406-410 已转为 ErrorInfo 写入 update_info,经 GenerateStream::update(reportUpdateErrorWithoutLock, GenerateStream.cc:1195)→NormalGenerateStream::nextOutput 返回 stream error→LocalRpcServer::pollStreamOutput 调 serializeErrorMsg(LocalRpcServer.cc:172-192) 将 error_code/error_message 编码进 ErrorDetailsPB 跨 RPC 传输,并非经 FlattenOutputPB 丢失。
  • [P3] customOutputIndexes 在 pre-norm + C++ layer-microbatch 路径被冗余调用两次 — L893-894 的 enable_layer_micro_batch 早退使 micro-batch 路径直接 return forwardMicroBatched,不再执行 L955 的 customOutputIndexes;而正常路径 L1044 以 skip_final_layernorm=true 进入 forwardPostLayers,L1149 的 !skip_final_layernorm 分支被跳过,两处调用互斥,不存在重复 H2D。
  • [P2] batch_infer 重构为 _format_response_new 后丢失 beam search 的 beam_responses 赋值 — _format_response_new 在非 num_return_sequences 分支(frontend_worker.py 第 328 行)返回 self._format_response(...),而 _format_response 第 240-241 行仍保留 has_num_beams() 时 aux_info.beam_responses = generate_texts,未丢失 beam_responses 赋值。
  • [P1] custom_output_token_position 被 reserved 导致 split 部署下打分位置无法跨 RPC 传输 — 位置由后端 NormalEngine::makeStream 进程内设置(NormalEngine.cc:749),并被同进程 NormalModelInputGatherer 读取(NormalModelInputGatherer.cc:543),二者间无 GenerateInputPB 序列化;proto 266-267 保留的是已删除的客户端字段,发现所假设的跨 RPC 边界不存在
  • [P2] forwardPostLayers 中 pre_final_norm 的 index_select 分支在现有调用点下为死代码 — forwardMicroBatched 末尾第 724 行 return callForwardPostLayers(hidden_states, inputs, false) 以 skip_final_layernorm=false 调用,使 forwardPostLayers 第 1149 行 !skip_final_layernorm 分支可达,原“恒为 true/死代码”的结论不成立,属漏看调用方。
  • [P2] testCustomOutputDispatch 中 num_return_sequences=2 的 context 流触发 gatherer 按序列写入而 buffer 按请求分配的越界路径 — 发现前提不成立:StreamGroups.h:42 中 total_context_batch_size 按 currentBatchSize 累加,num_return_sequences=2 时 currentBatchSize=2(GenerateStream.cc:335-342),分配大小与写入次数均为 2,不存在 buffer=1 写 2 的越界路径。
  • [P2] ensureModelInputsOnCuda/checkModelInputsOnCuda 无条件处理可选张量 custom_output_indexes — 当前代码中 to_cuda(NormalExecutor.cc 第453-456行)与 check(第491-494行)均已包含 !tensor.defined() 守卫,custom_output_indexes 为未定义张量时会被直接跳过,原发现所述无条件处理可选张量的问题已不存在。
  • [P3] int32 custom_output 的 RPC 序列化路径缺少测试覆盖 — QueryConverterTest.cc 第 259–299 行的 CustomOutputResponseSerialization 现遍历 {torch::kInt32, TensorPB::INT32}(第 263 行),并用 int32 值(第 266–267 行)经 transResponse→transTensor 做往返校验(第 284–288 行),覆盖了 int32_data 的序列化/反序列化路径。
  • [P3] custom_output_indexes 加入日志宏但该张量在多数请求中为 undefined,日志路径可能解引用未定义张量 — 宏展开路径已由 addTensorBytes(第62–64行)和 addTensor(第105–107行)对 !tensor.defined() 前置返回/写入空 IValue,snapshotTensor(第91–93行)同样有保护,未定义张量不会被解引用。
  • [P1] dispatcher 按序列数消费 custom_output 行,而 gatherer/model 每请求只产出一行,num_return_sequences>1 时触发 row count mismatch 或跨请求错位 — NormalModelInputGatherer.cc 第538行 for (auto i = 0; i < current_batch_size; ++i) 循环内第545行才执行 custom_output_count++,每条序列各生成一个 custom_output_indexes 条目,与 dispatcher 第177-178行按 cur_batch_size 消费的契约一致,发现所依据的“每 stream 仅一行”前提在当前代码中不成立。
  • [P2] batch_infer 重构为调用 _format_response_new,可能丢失 beam search 的 aux.beam_responses — _format_response_new 在非多序列分支(第 328–329 行)委托 self._format_response,后者第 240–241 行在 has_num_beams() 时仍执行 aux_info.beam_responses = generate_texts,beam search 路径未丢失该字段。
  • [P1] custom_output_indexes 为可选 tensor,未定义时传入 to_cuda/check 可能崩溃或误报 — to_cuda lambda(NormalExecutor.cc:454)与 check lambda(NormalExecutor.cc:492)均新增了 !tensor.defined() 提前返回,custom_output_indexes 未定义时不再触发 .to()/is_cuda() 崩溃。
  • [P2] layer-microbatch 的 pre_final_norm 路径对 customOutputIndexes 重复做 H2D 拷贝与 CUDA 分配 — forward() 在 893-894 行对 enable_layer_micro_batch 提前 return forwardMicroBatched,layer-microbatch 路径根本不会执行到 954-959 行的 customOutputIndexes;该路径唯一一次调用在 forwardPostLayers 1152 行(skip_final_layernorm=false),而普通路径 skip_final_layernorm=true 不会进 1149 分支,两处调用互斥,不存在重复 H2D 拷贝。
  • [P2] batch_infer 内联逻辑重构为 _format_response_new 后,beam_responses 处理可能丢失 — 当前 batch_infer(frontend_worker.py:162-205)已恢复内联组装,第 167-171 行在 gc.aux_info 且 gc.has_num_beams() 时执行 aux.beam_responses = generate_texts 并 asdict(aux),beam_responses 处理已保留,不再经由 _format_response_new。
  • [P3] pre_final_norm + layer-microbatch 路径下 customOutputIndexes 被重复调用,产生两次 H2D 拷贝与重复 hold_host — 发现的前提不成立:forward() 在第 893-894 行对 enable_layer_micro_batch 直接 return forwardMicroBatched(inputs) 提前返回,因此第 955 行的 customOutputIndexes(inputs) 在 layer-microbatch 路径下根本不会执行;forwardMicroBatched(607-737 行)也不调用 customOutputIndexes,该路径仅在 forwardPostLayers 第 1152 行调用一次,不存在两次 H2D/重复 hold_host。
  • [P0] num_return_sequences>1 时 handler 收到同一 token 的 N 个重复行,而非每请求一行 — 越界前提不成立:total_context_batch_size 是各 context stream 的 currentBatchSize 之和(StreamGroups.h:49),currentBatchSize 在 prefill 阶段已按 num_return_sequences 展开(GenerateStream.cc:334-342),故 num_return_sequences=2 时分配=写入=2,无越界写;'按请求数分配'的论断与代码不符
  • [P2] 输入侧未新增 custom_output_indexes 的 proto 字段,split 部署下模型侧可能收不到索引 — 发现的前提不成立:custom output 在 RtpLLMOp.cc:210-212 强制要求 RoleType::PDFUSION(拒绝 P/D split),且 custom_output_indexes 由模型侧 NormalEngine.cc:758 的 selector 本地计算 custom_output_token_position 后,在 NormalModelInputGatherer.cc:338 本地填充,无需经 proto 传输 GptModelInputs,故不存在输入侧缺失字段导致索引丢失的问题。
  • [P1] custom_output_offset 按 cur_batch_size(num_return_sequences)推进,但 gatherer 每 stream 仅写入一次索引时行数不匹配 — NormalModelInputGatherer.cc:542-546 中 custom_output_count++ 位于 for (auto i = 0; i < current_batch_size; ++i) 循环体内(currentExecuteTokens(i) 之后),每 sequence 写一行,与 dispatcher 第 177-179 行按 cur_batch_size 推进 custom_output_offset 一一对应,原发现所述“每 stream 只写一行”的前提不成立。
  • [P2] decode chunk 的 extra_outputs 非 None 时覆盖 prefill 的 custom_output,导致流式响应中分数丢失 — NormalGenerateStream.cc:236-238 在 prefill 保存 custom_output_ 并在 139-141 于每次输出(含 decode)重放,故有分请求 decode 的 custom_output 非 None,走 openai_endpoint.py:521-529 合并分支不丢分;else 分支(531)仅无分请求触发,无 custom_output 可丢,发现前提不成立。
  • [P2] makeStream 在获取 GIL 前对 py::object 做真值判断,无 GIL 调用 PyObject_IsTrue — custom_output_selector_ 声明为 py::object(NormalEngine.h:76),其 operator bool 是 m_ptr!=nullptr 指针判空而非 PyObject_IsTrue,且实际 Python 调用已在第753行 gil_scoped_acquire 之后(第756行),catch 亦在 GIL 内,所描述的 GIL 违规不存在。
  • [P2] 选择器报错后 stream 仍被无条件 enqueue,若 scheduler 不跳过已报错 stream 将浪费完整 forward — FIFOScheduler.cc:351-358 已在 admission 前对 hasError() 的 stream 执行 moveToNext 并 continue(跳过容量/KV 准入),enqueueGroup 路径在 502-507 同样剔除已报错成员,故选择失败的 stream 不会进入 forward;发现的前提(scheduler 不跳过)不成立,问题已被既有调度逻辑覆盖。
  • [P1] custom_output 按 num_return_sequences 行数下发,与模型按请求产出一行的语义不匹配 — NormalModelInputGatherer.cc 第537-545行内层 for(i<current_batch_size) 按序列写 custom_output_count++,与 NormalOutputDispatcher.cc 第178/217行按 cur_batch_size 一致,且 docs/custom_output.md 明确 per-sequence 语义,模型侧并非每请求只产一行,mismatch 前提不成立
  • [P2] 可选张量 custom_output_indexes 加入 MODEL_INPUT_TENSORS 日志宏,未定义时可能被无守卫访问 — 宏 MODEL_INPUT_TENSORS 仅用于两处且均先做 defined() 守卫:addTensorBytes(第61-63行)和 addTensor(第105-107行)对未定义张量提前 return/插入空 IValue,custom_output_indexes 未定义时不会崩溃或记录垃圾数据。
  • [P2] forward() 中 num_valid_tokens 路径的 callForwardPostLayers 未传递 pre_final_norm_hidden_states,pre_final_norm custom output 在该路径下触发 TORCH_CHECK 失败 — num_valid_tokens 路径(1034–1042 行)仅在 enable_prefill_cp 下可达,而第 543 行 TORCH_CHECK(!enable_prefill_cp, "custom output does not support context parallel yet") 已禁止 CP 下启用 custom output,故该路径不会触发 1228 行的 pre_final_norm TORCH_CHECK;且 1149–1152 行已改为本地 index_select 计算 pre_final_norm_hidden,不再依赖传入参数。
  • [P2] post_final_norm 路径在 generation-prefill graph(skip_final_layernorm=true)下 custom output 块被整体跳过,静默不产出分数 — custom output 计算块(第1222–1249行 GptModelOutputs outputs; if (run_custom){...})已从 if (weights_.final_layernorm && !skip_final_layernorm)(第1155行)分支内移出,现位于 if (lm_head)(第1175行)块内,不再受 skip_final_layernorm 门控,post_final_norm 在 skip_final_layernorm=true 下也会执行 run_custom 并产出 custom_output。
  • [P1] custom output 计算位于 final_norm 分支内,但 forward() 两处调用均传 skip_final_layernorm=true,Python 模型路径永不执行 — run_custom/custom_output 计算块已从 final_layernorm 分支(1155-1170)移出,现位于 if (lm_head) 块内(1223-1249),不再受 skip_final_layernorm=true 影响,因此该路径可正常执行。
  • [P3] customOutputIndexes 对已 staged 到 CUDA 的 indexes 仍调用 hold_host,与注释意图不符 — hold_host 在 TensorHolder.h:16 已校验 tensor.defined() && tensor.device().is_cpu(),对 CUDA indexes 是 no-op,且 PyWrappedModel.cc:523-524 注释已明确 'already-staged CUDA indexes require no host retention',与行为一致;同时 forward() 在 893-895 行对 enable_layer_micro_batch 提前 return,microbatch 路径不再触发 962 行的首次 customOutputIndexes 拷贝。
  • [P2] construction_guard 的 cleanup 在 GIL 已持有时获取 GIL 可能死锁或断言失败 — 唯一生产构造点 LocalRpcServer.cc:150-152 在 new NormalEngine 前用 pybind11::gil_scoped_release release; 释放 GIL 并断言 !PyGILState_Check()(注释明确“running engine init with gil held may cause program hang”),因此构造期间外层调用方不持有 GIL,cleanup_on_failure 的 gil_scoped_acquire 不会发生二次获取;发现所依赖的“外层调用方仍持有 GIL”前提不成立。
  • [P1] run_custom 逻辑位于 skip_final_layernorm 分支内,generation-prefill graph / python_norm 路径下 custom output handler 不会被调用 — custom output 块位于 1222–1249 行,处于 1175 行 if (lm_head) 分支内,而 final_layernorm 分支在 1170 行已闭合,故 skip_final_layernorm=true 不影响其执行,原发现前提不成立
  • [P3] customOutputIndexes 在 pre_final_norm + layer-microbatch 路径被重复调用,首次调用结果被丢弃 — forward() 在 line 893-894 对 enable_layer_micro_batch 提前 return forwardMicroBatched(inputs),在到达 line 954-959 的首次 customOutputIndexes 调用前即返回;forwardMicroBatched(line 607)自身不调用 customOutputIndexes,最终经 callForwardPostLayers(...,false) 在 forwardPostLayers line 1152 仅调用一次 customOutputIndexes,故 layer-microbatch 路径不存在重复调用。
  • [P2] extra_outputs 合并条件过窄,后续 chunk 携带非 custom_output 字段时会覆盖并丢失 prefill 的 custom_output — 生产者 NormalGenerateStream.cc:139-141/236-238 将 prefill 的 custom_output_ 持久化并附加到每个后续 decode 输出,custom_renderer.py:701-702 也会在 decode chunk 中带上 custom_output,因此不存在“decode 含 logits 但 custom_output 为 None”的序列,openai_endpoint.py:521-531 的 else 分支在带分请求上不会触发,prefill 的 custom_output 不会被丢弃。
  • [P2] custom_output_token_position(int)与 prefixLength()/inputLength() 比较存在有符号/无符号提升,破坏文档化的 -1 跳过语义 — 发现的前提不成立:GenerateStream.h:228 与 :235 中 inputLength() 和 prefixLength() 均声明为 int(而非 size_t),因此 NormalModelInputGatherer.cc:543、NormalOutputDispatcher.cc:163、NormalEngine.cc:758 的 int 与 int 比较不发生无符号提升,-1 跳过语义未被破坏。
  • [P2] 可选张量 custom_output_indexes 被无条件 to_cuda/check,无 custom output 请求可能失败 — to_cuda 宏(第453-456行)与 check 宏(第491-494行)均以 if (!tensor.defined()) return; 开头,未定义的 custom_output_indexes 会被安全跳过,不再抛错。
  • [P2] 测试代码访问 NormalEngine 私有成员 custom_output_selector_,缺少 friend 声明将编译失败 — BUILD 文件第 6-8 行定义 test_copts = ["-fno-access-control"] + copts(),且第 72 行 pywrapped_model_cache_store_integration_test_lib 使用 copts = test_copts,该测试目标已禁用访问控制,故第 884 行 engine.custom_output_selector_ 的私有成员访问(NormalEngine.h:76 private)不会导致编译失败,原发现不成立。
  • [P2] makeStream 在可能已持有 GIL 的 enqueue 路径上再次获取 GIL,存在死锁风险 — makeStream 第 771–773 行已新增 if (!selection_error.empty()) stream->reportError(INVALID_PARAMS, selection_error),selector 异常不再静默丢弃;GIL 方面 pybind11 的 gil_scoped_acquire 对当前线程可重入,不存在所主张的递归锁死锁。
  • [P1] 非 graph 的 pre_final_norm 路径在 forwardPostLayers 中用 post-norm 的 hidden 重新 index_select,覆盖了 Python 模型返回的 pre-norm 行 — 非 graph 的 Python eager 路径(forward() 的 else 分支)在 1044-1045 行同样以 skip_final_layernorm=true 调用 callForwardPostLayers 并透传 py_model_outputs.pre_final_norm_hidden_states,故 1149 行 !skip_final_layernorm 分支不会执行、不会覆盖 Python 返回的 pre-norm 行;唯一 skip_final_layernorm=false 的调用在 725 行 forwardMicroBatched,该路径由 C++ 负责归一化、hidden 为 pre-norm,index_select 取值正确。原发现对 skip_final_layernorm 与 graph 的对应关系前提不成立。
  • [P2] prefixLength() 返回 KV cache 复用长度而非系统提示词长度,与 custom_output_token_position 的调整语义混用 — 发现所述的 gather/dispatch 不一致不成立:NormalModelInputGatherer.cc:545 与 NormalOutputDispatcher.cc:163 使用完全相同的 position >= stream->prefixLength() 条件,且 prefixLength() 确定性地返回 reuse_length_(GenerateStream.cc:477-478,与 reuseLength() 同值),测试中 cached 流 position=0 而 setReuseLength(1) 使其在两侧一致被排除(0>=1 为假),2 个 custom_output_indexes 来自 scored context 的 2 个序列而非 cached;本轮注释(543-544)澄清每序列一个索引的语义,属误报而非真实缺陷。
  • [P3] 非生成任务 internal 模块导入失败由 ERROR 级 logging.exception 降为 DEBUG 级 logging.debug,掩盖真实错误 — 当前代码第36行为 logging.exception("internal module not found, using external module"),已恢复 ERROR 级带 traceback 的记录,不再静默降级为 debug。
  • [P1] custom_output 计算被置于 skip_final_layernorm 守卫的 if 块内,graph/python_norm 路径走 else 分支静默不产出 — run_custom 块现位于 if (lm_head) 块内(1222–1249 行),而 if (weights_.final_layernorm && !skip_final_layernorm) 守卫已于 1170 行闭合,故 skip_final_layernorm=true 时 run_custom 仍会执行,原发现的前提已不成立。
  • [P1] custom output 行与 stream 的对应依赖 allStreams() 与 contextStreams() 顺序一致,无显式校验 — NormalModelInputGatherer.cc:537-545 的 custom_output_count++ 位于 current_batch_size 内层循环内(每序列一行而非每流一行),num_return_sequences=2 时产出 2 行与 dispatcher 的 cur_batch_size=2 消费一致,且 StreamGroups.h:283-285 allStreams 将 context_streams 按原序拼接在 decode 之后保证顺序一致,原发现前提不成立。
  • [P3] pre_final_norm 路径每个 forward 对 customOutputIndexes 调用两次,重复 H2D 拷贝与 hold_host — 两处 customOutputIndexes 调用互斥:普通 Python 路径在 :1044-1045 以 skip_final_layernorm=true 调用 callForwardPostLayers,使 :1149 的 use_pre_final_norm && !skip_final_layernorm 分支不执行(仅 :955 调用一次);微批路径在 :893-894 提前 return 跳过 :955,仅经 :725 的 skip_final_layernorm=false 在 :1149 调用一次,不存在同一 forward 的二次 H2D/hold_host。
  • [P2] customOutputIndexes 对已在 CUDA 的 custom_output_indexes 无条件调用 hold_host,与注释语义矛盾 — TensorHolder.h:16-19 的 hold_host 仅在 tensor.defined() && device().is_cpu() 时入列,CUDA 张量本就不会被保留,故 PyWrappedModel.cc:525 的无条件调用与注释语义一致,不存在矛盾。
  • [P2] testCustomOutputDispatch 直接构造 torch::kCUDA tensor,无 CUDA 可用性保护 — BUILD 中 batch_stream_processor_test 目标已声明 GPU 保护:env TEST_USING_DEVICE=CUDA(第74-76行)+ exec_properties gpu=A10(第77行)并依赖 cudart,且测试夹具为 DeviceTestBase(NormalBatchStreamProcessorTest.cc:44),故该用例不会在 CPU CI 裸跑,原发现前提不成立。
  • [P1] generation-prefill cuda graph 回放路径未把 pre_final_norm_hidden_states 传入 callForwardPostLayers — 当前代码中 generation-prefill graph 分支(980-995 行)不再提前 return,而是与普通 forward 分支一起落到 1044-1045 行的公共返回,传入 std::move(py_model_outputs.pre_final_norm_hidden_states);原发现所引用的 1041 行 num_valid_tokens 返回由 1034 行 enable_prefill_cp 守卫,而 initializeCustomOutput 第 541 行 TORCH_CHECK 明确拒绝 prefill CP 与 custom output 组合,二者互斥,故该问题已不存在。
  • [P2] formatResponse 单序列 custom_output 取 front() 与 getCustomOutput 的 back() 约定不一致 — 第 97–100 行已对 generate_outputs 为空提前 return,且 custom_outputs 每元素都 push_back(第 146 行),故 front() 不会越界;第 178 行注释明确说明 num_return_sequences>0 时保留序列维(含 ==1)与 Python 一致,front() 丢弃多序列属有意约定而非缺陷。
  • [P1] runCustomOutput 返回的 CUDA tensor 在异常路径之外未校验 is_cuda,依赖后续 copyToPinnedCpuAsync 隐式处理 — run_custom 执行块位于 line 1221 的 if(lm_head)(1173)内,而非 final_layernorm 分支(1153-1168),skip_final_layernorm=true 不会跳过 handler;is_cuda 由 runCustomOutput 的 line 558 output.device()==rows.device() 显式校验(rows 来自 CUDA activation),两处问题在当前代码均已不成立。
  • [P1] custom_output_offset 按 cur_batch_size 递增而模型按请求返回一行,num_return_sequences>1 时行数错配 — 当前代码中 NormalModelInputGatherer.cc:545-547 将 custom_output_count++ 内嵌在 for (auto i = 0; i < current_batch_size; ++i) 序列循环里,每个 context 序列各写一个 index(num_return_sequences=2 时产生 2 个),与 NormalOutputDispatcher.cc:178 的 custom_output_offset += cur_batch_size 及 :217 的 custom_output_cpu.narrow(0, idx, cur_batch_size) 行数一致,发现所述“每 stream 仅一次递增”的前提已不成立。
  • [P2] custom_output_token_position 被 reserved 不随 RPC 序列化,split 部署下模型侧 gatherer 读到默认 -1 导致 custom output 静默失效 — 发现的前提有误:selector 在模型侧 NormalEngine::makeStream(NormalEngine.cc:750-762,注释明确“Select in the model process after any RPC deserialization... The position is not an RPC input”)运行,由 LocalRpcServer::prepareInput→transQuery 反序列化后经 engine_->enqueue 触发,custom_output_token_position 无需经 RPC 序列化;且 RtpLLMOp.cc:210-212 显式拒绝 P/D 分离(要求 PDFUSION 无非投机解码),reserved 该字段不会导致 custom output 静默失效。
  • [P2] TORCH_CHECK 错误信息把 per-sequence 语义误述为 per-context-request — 错误信息已在 522 行改为 'per context sequence'、556 行改为 'per selected context sequence',且 523-524 行注释已改写为准确说明 hold_host 对 CUDA tensor 是 no-op(TensorHolder.h:16 中 hold_host 仅在 is_cpu() 时保留),不再误导。
  • [P3] custom_output_indexes 按序列重复写入同一 token position,handler 收到重复行并重复计算 — NormalModelInputGatherer.cc:543-544 新增注释明确『Return sequences occupy separate context rows; preserve one selected index per sequence』,PyWrappedModel.cc:521 的 TORCH_CHECK 文案由『per context request』改为『per context sequence』,且 NormalBatchStreamProcessorTest.cc:1028 验证 num_return_sequences=2 时 custom_output_indexes 返回每序列一个索引(如 {1,3}),证明按序列写入是预期行为而非重复计算 bug。
  • [P2] MODEL_INPUT_TENSORS 新增 custom_output_indexes,未定义时日志路径可能解引用空 tensor — ModelInputsLogger 的 addTensorBytes(:62-64)、snapshotTensor(:91-93)、addTensor(:105-108) 均对 !tensor.defined() 提前返回或写入 None IValue,MODEL_INPUT_TENSORS 宏经这些 helper 展开,未定义 custom_output_indexes 不会被解引用,问题不存在。
  • [P3] initializeCustomOutput 未校验 final_layernorm 存在,无 final norm 的模型会静默不产出 custom_output — 当前代码中 custom output 块位于 if(lm_head)(1175–1249)内而非 if(final_layernorm)(1155–1170)内,1278 的 else 是无 lm_head 分支;initializeCustomOutput 第539行已 TORCH_CHECK(weights_.lm_head) 校验真正的先决条件,final_layernorm 并非 custom output 的必需前提,原发现的括号误读前提不成立。
  • [P3] custom_output_indexes 的 narrow 依赖 processContextStreams 单次调用假设,多次调用会丢失已收集的索引 — 全仓仅一处调用 processContextStreams(gather() 第674行),且 gather() 每次经 allocateModelInputBuffers(第671行)新建 model_input、custom_output_indexes 在338行新分配,不存在对同一 model_input 多次调用的分批/重试路径,故 narrow 不会覆盖已收集索引;本轮新增注释(543-544行)明确该单次契约,发现所述多调用场景不成立。
  • [P1] generation-prefill CUDA graph 路径(skip_final_layernorm=true)不执行 custom output 计算,handler 静默不调用 — run_custom 分支(第1223行)已从 if (weights_.final_layernorm && !skip_final_layernorm) 块(第1155-1170行)移出到 if (lm_head) 块(第1175行)内,且 pre_final_norm_hidden 经 callForwardPostLayers(第1044-1045行)传入,第1226-1238行在 skip_final_layernorm=true 时仍会执行 runCustomOutput。
  • [P2] generation-prefill 带 num_valid_tokens 的路径未传 pre_final_norm_hidden,pre_norm custom output 在该路径报错 — initializeCustomOutput() 在 PyWrappedModel.cc:543 有 TORCH_CHECK(!device_props_.enable_prefill_cp, "custom output does not support context parallel yet"),custom_output_pre_norm_ 仅在 custom_output_enabled_ 置真(547 行)后才生效;而 1042 行路径由 enable_prefill_cp && has_context_request 守卫,二者互斥,故该失败组合不可达。
  • [P2] custom_output_indexes 条件分配但无条件传入 to_cuda/check,未启用 custom output 的请求可能触发检查失败 — to_cuda lambda 第 454 行 if (!tensor.defined() || tensor.is_cuda()) return; 与 check lambda 第 492 行 if (!tensor.defined()) return; 均显式跳过 undefined tensor,故 custom_output_indexes 未定义时不会再触发检查失败。
  • [P3] customOutputIndexes 在 pre_final_norm + layer-microbatch 路径被重复调用 — forward() 在 893-895 行对 enable_layer_micro_batch 提前 return forwardMicroBatched(inputs),早于 954-959 行的 custom_output_pre_norm_ 分支,layer-microbatch 路径不再执行第一次 customOutputIndexes 调用,仅 forwardPostLayers:1152 调用一次,重复调用已消除。
  • [P1] post_final_norm 路径下 context_rows 取自 final layernorm 之后的 hidden,与 pre_final_norm 语义不一致 — 行1152 torch::index_select 返回独立副本而非视图,且行1159-1169 final layernorm 均为 hidden = ... 重新赋值(rmsnorm 乘法、layer_norm 返回新张量)而非 in-place,故 pre_final_norm_hidden 保存的是 norm 前数据,与行1240 post 分支取 norm 后 hidden 正确区分,发现所述 aliasing 不成立
  • [P2] beam search 下 custom_output 行数与 dispatch 偏移不一致,prefill 请求报 row count mismatch — 原发现前提不成立:outputTokenLen()=seqLength()-inputLength() 仅在 dispatchSingleStream 末尾 stream->update()(第454行) 内 complete_token_ids_->update()(第1213行) 才推进,而 dispatch 在 update 之前(第157/206行)读取 currentBatchSize(),故 prefill 时 currentBatchSize()==numBeams(0)==1,与 gather(第527行)与 custom_output 单行一致,不存在行数不匹配
  • [P3] pre_final_norm 非 graph 路径重复调用 customOutputIndexes 造成两次 H2D — 普通非 microbatch 路径在 L1042-1043 以 skip_final_layernorm=true 调用 callForwardPostLayers,使 L1147 的 !skip_final_layernorm 守卫将 L1150 的第二次 customOutputIndexes 排除;而 microbatch 路径在 L891-893 提前 return,未执行 L952-955 的首次 customOutputIndexes。两处 H2D 调用互斥,不存在同一 forward 内两次 H2D,作者回复成立。
  • [P1] skip_final_layernorm=true 时 custom_output 处理器不会被调用,pre_final_norm_hidden 被丢弃 — custom output 的 run_custom 块现位于 1223 行,处于独立的 if (lm_head) 块(1173 行)内,而非 final_layernorm 条件(1153–1168 行)内;skip_final_layernorm=true 时仍会执行,且 pre_norm 用 1043 行 std::move 转发的 pre_final_norm_hidden(1228–1236 行)。
  • [P2] 非 graph 路径新增 KV/query batch 严格相等校验,原代码直接透传,若存在合法不一致场景会误报 — 非 graph 路径下 active_batch_size(=paged_kv_last_page_len_d.numel(),C++ fillParamsMhaDevice L667 与 refreshBuffer L418 均按 input_lengths.size(0) 设形状) 与 batch_size(=qo_indptr.numel()-1,L393-394 取 cu_seqlens_device[:input_lengths.size(0)+1]) 恒等于 input_lengths.size(0),故 L493-494 断言恒成立、无法拒绝合法输入,属误报,无需代码改动。
  • [P2] _paged_kv_indices_buf 未用 .view(-1) 解耦,page_indice_d 在 replay 缩窄时该 graph 别名同步缩窄 — 第412行仍为直接别名(无.view(-1))且本轮无代码改动,409-411注释与480-490填充逻辑佐证其有意为之,但该发现是否为真实缺陷取决于仓库外 flashinfer 依赖 plan()/page.cuh 的契约(不可验证),无法定论 fixed 或 still_open。
  • [P2] context 流判定依赖 allStreams() decode-first 隐式排序,custom_output 可能静默丢失或串行 — 改动块(NormalOutputDispatcher.cc:160-162)将 context 判定从 batch_idx_in - total_decode_batch_size 的 decode-first 隐式排序改为显式 stream->isContextStream(),且 copyToPinnedCpuAsync(该文件 22-25 行)本就对 undefined tensor 判空并在 CUDA 拷贝后才置 need_sync,两处脆弱依赖均已消除
  • [P2] layer-microbatch pre-norm 路径下 customOutputIndexes 在同一 forward 内被调用两次,产生冗余 H2D 拷贝 — 行539 initializeCustomOutput 的 TORCH_CHECK(!enable_prefill_cp) 使 num_valid_tokens(CP) 分支下 custom_output_enabled_ 恒为 false、run_custom/use_pre_final_norm 为 false,不会触发行1226 的误报;行891-892 微批次提前返回且行1147 的 !skip_final_layernorm 守卫使 customOutputIndexes 在单次 forward 内至多调用一次(行952 与 1150/1238 互斥),标题与正文所述问题在当前代码均不成立。
  • [P2] generation-prefill graph 的 num_valid_tokens 分支未传递 pre_final_norm_hidden_states,pre-norm 模式下可能丢失 Python 返回的选中行并误报协议错误 — 该发现的前提有误:line 1040 的四参调用位于 if (device_props_.enable_prefill_cp && has_context_request) 的 CP 输出聚合分支(非 generation-prefill graph 分支),且 initializeCustomOutput() 在 line 539 有 TORCH_CHECK(!device_props_.enable_prefill_cp, ...),使 custom output(含 pre-norm)与 CP 互斥,故该分支在 pre-norm 模式下不可达,描述的协议错误不会发生。
  • [P1] custom output 计算块嵌套在 skip_final_layernorm 为 false 的分支内,Python model 路径永不执行 runCustomOutput — 该发现系对控制流的误读:run_custom 块位于第 1173 行 if (lm_head) 分支内(第 1221 行 if (run_custom)),而 final_layernorm 分支在第 1168 行已闭合,两者是并列关系;第 1276 行的 elseif (lm_head) 的无 lm_head 分支,与 skip_final_layernorm 无关,故 Python model 路径会正常执行 runCustomOutput
  • [P3] customOutputToJson 模板对 dim 0 或 >2 维 tensor 无防御,tensor.size(1) 会越界 — 越界主张不成立:custom_output 的唯一生产入口 PyWrappedModel.cc:1240 调用 runCustomOutput,其第554-555行已用 TORCH_CHECK 强制 output.dim()==1||2 且 numel()>0(异常被捕获写入 custom_output_error 而非 custom_output),故 dim 0/>2 无法到达 GenerateStreamWrapper.cc:16 的 tensor.size(1)。
  • [P2] customOutputIndexes 对已 CUDA 化的 indexes 仍调用 hold_host,与注释矛盾且生产路径未覆盖 — 发现的前提不成立:TensorHolder.h:16-18 中 hold_host 仅对 is_cpu() 张量执行 push_back,CUDA 张量为 no-op,且 .to(kCUDA) 对已 CUDA 张量同样无拷贝,故 customOutputIndexes 第 525 行对 CUDA indexes 调用 hold_host 与注释一致、无断言/UB 风险,不存在所述缺陷。
  • [P2] ensureModelInputsOnCuda/checkModelInputsOnCuda 无条件处理可选的 custom_output_indexes,未定义时可能失败 — to_cuda(第453-455行 if (!tensor.defined() || tensor.is_cuda()) return;)和 check(第491-493行 if (!tensor.defined()) return;)均已有 defined 守卫,第483/508行对 custom_output_indexes 的调用会安全跳过未定义张量。
  • [P2] ensureModelInputsOnCuda/checkModelInputsOnCuda 对可选 custom_output_indexes 无条件 to_cuda/check — NormalExecutor.cc 第 454 行 to_cuda 内 if (!tensor.defined() || tensor.is_cuda()) return; 与第 492 行 check 内 if (!tensor.defined()) return; 均对 undefined 的 custom_output_indexes 做了防御,发现所假设的“未做 defined 防御”前提不成立。
  • [P2] 用 context_batch_idx>=0 判定 context stream 依赖 allStreams() decode-first 隐式顺序 — 第 160–162 行 has_custom_output 判定已改为直接调用 stream->isContextStream(),不再使用 context_batch_idx>=0 推导,原发现的隐式顺序依赖已消除。
  • [P2] pre-final-norm 路径 customOutputIndexes 被调用两次,重复 H2D 拷贝与 hold_host — 两个 customOutputIndexes 调用点互斥、无重复 H2D:microbatch 在 892 行提前 return 不经过 954 行首站点、723 行以 skip=false 调用仅触发 1148 行;普通 forward 虽经 954 行,但 1040-1041 行以 skip_final_layernorm=true 调用使 1145 行 !skip_final_layernorm 为假,故每次 forward 至多调用一次,作者驳斥成立。
  • [P3] customOutputIndexes 在 pre_final_norm 路径被 forward 与 forwardPostLayers 各调用一次,重复执行 hold_host 与 H2D 拷贝 — 发现所依据的“重复调用”在当前代码不成立:forward() 在 891-892 行对 microbatch 提前 return(不经过 954 行首次调用),非 microbatch 路径在 1040-1041 行以 skip_final_layernorm=true 调用 post-layers,使 1145 行的二次调用被 !skip_final_layernorm 守卫挡住;microbatch 在 723 行以 false 调用且同样不经过 954 行,故单次 forward 只会执行一次 customOutputIndexes,无法证实重复 hold_host/H2D。
  • [P1] skip_final_layernorm=true 路径(Python 侧完成 final norm,如 Qwen3Model)下 custom_output 被静默丢弃 — 前提不成立:run_custom 块位于 if(lm_head)(第1171行)分支内而非 final_layernorm 分支,pre_final_norm_hidden 在 use_pre_final_norm=true 时于第1234行被正确消费;第1274行的 else 是无 lm_head 路径(返回 {Tensor(),Tensor(),hidden}),并非 skip_final_layernorm 路径,故 custom_output 未被静默丢弃。
  • [P1] makeStream 对 decode 请求也无差别调用 select_token_position,handler 按 prefill 全 prompt 设计时可能在 decode 输入上抛异常或返回越界 position — makeStream 仅按请求调用一次(NormalEngine.cc:755-756 注释“Select once…Decode iterations reuse this stream”及 custom_module.py:95 docstring“Called once per request”),不存在 decode 输入;且 pybind 2.13.6 的 error_already_set 构造即 fetch/清除错误、析构仅 decref 不 restore,760-762 的 catch 不会残留 pending error,发现两点前提均不成立。
  • [P1] custom output 执行逻辑位于 final_layernorm && !skip_final_layernorm 块内,graph 路径 skip_final_layernorm=true 时不执行 — run_custom 现于第 1144-1146 行在 final_layernorm 块外计算,其执行块(第 1220-1247 行)已移入 if (lm_head) 块内而非 if (weights_.final_layernorm && !skip_final_layernorm) 块,故 skip_final_layernorm=true 的 graph 路径不再跳过 custom output。
  • [P2] initializeCustomOutput 强制 extend_forward_args 返回 selected_hidden_states,未迁移的既有 custom module 在初始化阶段即失败 — run_custom 块现已位于 if (lm_head) 分支内(第 1170 行 if (lm_head) 起,第 1222 行 if (run_custom)),不再被 if (weights_.final_layernorm && !skip_final_layernorm)(第 1151 行)门控,final_layernorm 为空时 custom output 仍会计算,静默失败已消除。
  • [P2] generation-prefill CUDA graph 路径调用 callForwardPostLayers 未传 pre_final_norm_hidden,pre_final_norm 自定义输出可能触发 TORCH_CHECK — 初始化时第539行 TORCH_CHECK(!device_props_.enable_prefill_cp) 使 custom output 与 CP 互斥,故第1040行 CP 路径在 custom_output_pre_norm_ 为 true 时不可达;而 generation-prefill CUDA graph 实际汇入第1042-1043行已传 pre_final_norm_hidden。
  • [P2] ChatRender 向 render_stream_response_final 系列方法新增第 5 个参数,但仅更新 custom_renderer.py,其他 renderer 实现可能抛 TypeError — custom_renderer.py 第1593/1680行已设 custom_output=None 默认参数,且全仓 grep 仅 custom_renderer.py 定义 render_stream_response_final/render_stream_response_final_blocking,其余 renderer 均继承 CustomChatRenderer,不存在其他实现会抛 TypeError
  • [P2] dispatcher 独立重建 gatherer 的 custom_output 行选择条件,跨层隐式契约脆弱 — copyToPinnedCpuAsync 在第23行已有 if (!tensor.defined() || !tensor.is_cuda()) 防护,custom_output 未定义时直接返回原 tensor,不会触发 .to()/.copy_() 崩溃;第159-164行的注释也已就地记录 allStreams() 的 context 顺序契约。
  • [P3] 行数不匹配的防御性 early return 跳过了 stream->update(),stream 未消费本步输出 — 该 early return(NormalOutputDispatcher.cc:213-215)虽仍在 stream->update() 之前,但其引发的状态不一致担忧已被既有错误生命周期消除:reportError 在 GenerateStateMachine.cc:23-28 中 Error 事件最高优先级、下次 moveToNext 即置 FINISHED 并 releaseResource,且 NormalGenerateStream.cc:45-48 nextOutput 在队列输出前先返回 error_info,故失败 stream 不会继续被调度或消费本步输出,与同文件 229/240/246 行同类 reportError+return 错误路径一致,属误报而非未修复缺陷。
  • [P3] forward 对空 custom_output_indexes 仍调用 customOutputIndexes,与 forwardPostLayers 的 numel 前置检查不一致 — 第 952-955 行的 forward 守卫已加入 && inputs.custom_output_indexes.numel() > 0,与 forwardPostLayers 的 numel 前置检查口径一致,空 custom_output_indexes 不再调用 customOutputIndexes。
  • [P2] ensureModelInputsOnCuda/checkModelInputsOnCuda 对可选的 custom_output_indexes 无条件 to_cuda/check — to_cuda 与 check 两个 lambda 现已对未定义 tensor 提前返回(第 454 行 if (!tensor.defined() || tensor.is_cuda()) return;、第 492 行 if (!tensor.defined()) return;),因此对可选的 custom_output_indexes 无条件调用不再触发失败。
  • [P2] checkModelInputsOnCuda 对条件性未定义的 custom_output_indexes 无条件 check — to_cuda 在第454行 if (!tensor.defined() || tensor.is_cuda()) return;、check 在第492行 if (!tensor.defined()) return; 均已显式处理 undefined tensor,custom_output_indexes 无条件调用不再导致崩溃/断言。
  • [P2] need_all_logits 分支未传递 pre_final_norm_hidden_states,pre_final_norm 自定义输出在 return_all_logits 请求下触发协议错误 — 非CP分支(PyWrappedModel.cc:1040-1041)已通过 std::move(py_model_outputs.pre_final_norm_hidden_states) 传入 pre_final_norm,且 initializeCustomOutput() 在 539 行 TORCH_CHECK(!device_props_.enable_prefill_cp) 强制 custom output(含 pre_final_norm)与 CP 互斥,使 finding 所指的 CP need_all_logits 分支(1038 行)在 pre_final_norm 自定义输出场景下不可达,故描述的协议错误已无法触发。
  • [P1] generation-prefill CUDA graph 路径下 custom_output 在 forwardPostLayers else 分支丢失 — custom_output 构造已从 if (weights_.final_layernorm && !skip_final_layernorm) 分支迁入 if (lm_head) 分支(第 1219–1245 行),final_layernorm 块(1151–1166)不再提前 return;graph 路径经第 1041 行传入 pre_final_norm_hidden_states 且 cuda_graph_runner.cc:1135–1138 捕获并返回,故 skip_final_layernorm=true 时 custom_output 不再丢失。
  • [P2] pre_norm 分支的 index_select 位于 try/catch 之外,c10::Error 不会转为 custom_output_error 而逃逸 — pre_norm 路径整体被移除(initializeCustomOutput 删除了 custom_output_pre_norm_ 与 hidden_state_stage 校验,forwardPostLayers 删除了 try/catch 之外的 pre_final_norm index_select),当前仅剩的 index_select 已位于 try/catch 内(第 1197-1202 行),c10::Error 会被捕获转为 custom_output_error。
  • [P3] customOutputIndexes 在 pre_final_norm + layer_micro_batch(eager)路径被重复调用,forward 中暂存的索引未被微批 Python 模型消费 — 误报:layer_micro_batch 在 891-892 行提前 return forwardMicroBatched,根本不会执行 954 行的 customOutputIndexes 暂存;普通路径经 1040-1041 行以 skip_final_layernorm=true 调 callForwardPostLayers,跳过 1145-1148 行的 index_select(改用 Python 返回的 pre_final_norm_hidden,见 1222-1234 行),故 customOutputIndexes 每次 forward 至多调用一次,不存在重复调用。
  • [P1] runCustomOutput 抛出的 py::error_already_set 被 catch(const std::exception&) 吞掉后未 discard,遗留 pending Python error — custom output 处理已移入独立的 if (lm_head) 块(1219-1246 行),而 final_layernorm 块在 1167 行即闭合,因此 skip_final_layernorm=true 不再跳过 custom output 执行;标题所述异常所有权问题按 pybind11 语义 error_already_set 构造时即 PyErr_Fetch 清除错误指示器,catch 后不存在 pending Python error。
  • [P3] customOutputToJson 未防护 dim==0 / dim>2 的 tensor — runCustomOutput 在 PyWrappedModel.cc:553 已用 TORCH_CHECK 限定 output.dim()==1||2 且 numel>0,且下游 dispatcher/stream 的 narrow(0,·)(NormalOutputDispatcher.cc:217、NormalGenerateStream.cc:141)与 RPC 的 stack/index(QueryConverter.cc:stackBuffersToTensorPB、model_rpc_client.py:863)均保持 rank,customOutputToJson 无路径收到 dim==0/dim>2 的 tensor。
  • [P2] callForwardPostLayers 第一个调用点未传递 pre_final_norm_hidden,pre-norm 自定义输出在该路径下会触发 TORCH_CHECK 失败 — 第1038行的调用点位于 enable_prefill_cp 的 CP 分支,而 initializeCustomOutput 第539行 TORCH_CHECK(!device_props_.enable_prefill_cp) 使 custom output(含 pre_final_norm)与 context parallel 互斥,故该路径不可达第1224行的 pre_final_norm TORCH_CHECK,发现所担心的失败场景不存在。
  • [P1] makeStream 中 select_token_position 抛出的 py::error_already_set 被 catch(const std::exception&) 吞掉后未 discard,遗留 pending Python error — 第761行注释已明确说明 pybind11 在 error_already_set 构造时即通过 PyErr_Fetch 取走并清除了 Python error,不存在 pending error 残留;第762行 error.what() 取消息并经 reportError 上报,处理正确,原发现前提不成立。
  • [P2] custom_output_indexes 在无 custom output 请求时未定义,to_cuda/checkModelInputsOnCuda 无条件处理可能崩溃 — to_cuda 在 NormalExecutor.cc:454 以 if (!tensor.defined() || tensor.is_cuda()) return; 显式跳过 undefined,check 在 :492 以 if (!tensor.defined()) return; 显式跳过 undefined,故对未启用 custom output 的 undefined custom_output_indexes 安全,不会崩溃。
  • [P3] layer-microbatch pre-norm 路径下 custom_output_indexes 被重复 H2D 拷贝 — forward() 在 line 891-894 对 enable_layer_micro_batch 提前 return forwardMicroBatched(line 892 注释明确说明跳过 ordinary-forward 的 index staging),因此 line 955 的第一次 customOutputIndexes 在 microbatch 路径根本不会执行,forwardPostLayers line 1149 的 index_select 是唯一一次拷贝,不存在重复 H2D。
  • [P3] customOutputIndexes 在 pre_final_norm + micro-batch 路径下同一 forward 内被调用两次,产生重复 H2D 拷贝 — 两处调用互斥:enable_layer_micro_batch 时 forward() 在第 893 行提前 return 走 forwardMicroBatched,不触达第 952 行 staging;普通路径 callForwardPostLayers 均传 skip_final_layernorm=true(第 1021/1041 行),使 forwardPostLayers 第 1146 行 use_pre_final_norm && !skip_final_layernorm 为假,customOutputIndexes 每条路径仅调用一次,无重复 H2D 拷贝。
  • [P3] customOutputIndexes 对已 staging 到 CUDA 的 indexes 仍无条件调用 hold_host — TensorHolder.h 第16-20行 hold_host 内部已有 tensor.device().is_cpu() 过滤,CUDA tensor 不会被保留,且 .to(kCUDA) 对已 CUDA 张量为 no-op,故发现所担心的 host 保留/同步开销并不存在,注释与实现一致。
  • [P2] generation-prefill graph 捕获时 pre_final_norm_output_indexes 被 customOutputIndexes 以捕获请求的行数覆盖,max_bs_ 容量失效 — 发现前提不成立:captureOneGraphInstance 直接调用原始 Python forward(cuda_graph_runner.cc:1972/2007 py_forward_method_),绕过 PyWrappedModel::forward 的 953-955 赋值;prepareCaptureModelInputs 原样保留 max_bs_ 大小的张量(:2147),replay 时 prepareInputData 用 indexes.narrow(0,0,count).copy_(...) 写入窄视图而非缩小图张量(:306-312),max_bs_ 容量不会失效。
  • [P2] custom_output_indexes 为 undefined 时仍被无条件 to_cuda/check,普通批次可能触发断言 — to_cuda 在第 454 行已有 if (!tensor.defined() || tensor.is_cuda()) return;,check 在第 492 行已有 if (!tensor.defined()) return;,custom_output_indexes 为 undefined 时会被安全跳过,不会触发断言。
  • [P1] checkModelInputsOnCuda 对未定义的 custom_output_indexes 断言 defined,无 custom output 的请求会触发崩溃 — checkModelInputsOnCuda 的 check lambda 在 492-494 行已对未定义张量提前 return(if (!tensor.defined()) { return; }),不再对 custom_output_indexes 断言 defined,故该崩溃问题已修复。
  • [P2] customOutputIndexes 注释与实现矛盾:对已 CUDA 化的索引仍无条件 hold_host — 该发现为误报:hold_host 实现(rtp_llm/models_py/bindings/core/TensorHolder.h:16)仅在 tensor defined 且 device().is_cpu() 时才持有,CUDA tensor 会被直接跳过,不存在拷贝或断言失败;PyWrappedModel.cc:523 的无条件调用与 521-522 行注释一致,且 .to(kCUDA) 对已 CUDA 的 tensor 为 no-op,原担忧不成立。
  • [P3] selector 抛出的所有异常统一归类为 INVALID_PARAMS,掩盖 handler 内部错误 — makeStream 现已按异常类型区分:越界(758-759)与 ValueError(766)归 INVALID_PARAMS,其他 Python 异常及 C++/转换异常(766、769)归 EXECUTION_EXCEPTION,ErrorInfo 保存类别后于 780 行上报,原问题已修复。
  • [P3] 测试用 1D tensor 模拟 custom_output,与生产 RPC 路径的 [1,width] 形状不一致 — 第57-58行 product((0,1,2),(False,True)) 中 matrix=True 分支使用 2D 值 [[[-0.5,1.5]],[[2.0,3.0]]],已同时覆盖 [rows] 与 [rows,width] 两种合法形状,原发现所指仅用 1D 的问题已消除。
  • [P2] to_cuda/checkModelInputsOnCuda 对可能未定义的 custom_output_indexes 无条件调用 — to_cuda(NormalExecutor.cc:453-455)以 !tensor.defined() || tensor.is_cuda() 提前返回,check(:491-493)以 !tensor.defined() 提前返回,对 undefined 的 custom_output_indexes 已短路防护,不存在无条件访问 .to()/.device() 的问题。
  • [P2] 文档声称 TP rank 0 上 hidden states 为 replicated,但 forwardPostLayers 直接对 hidden 做 index_select,未做 all-gather — docs/custom_output.md 第37-39行已明确说明 backbone 在 attention o_proj 与 DenseMLP down_proj 做 TP all_reduce、hidden 为 replicated 且 handler 无需 all-gather;代码 causal_attention.py:91-92 与 dense_mlp.py:103 均确认 all_reduce(Group.TP),故 PyWrappedModel.cc:1198 的 index_select 作用于全宽 replicated hidden,不成立。
  • [P2] custom_output_indexes 条件分配却被无条件 to_cuda/check,未定义张量可能触发崩溃 — ensureModelInputsOnCuda 的 to_cuda lambda(第452-455行)与 checkModelInputsOnCuda 的 check lambda(第490-493行)均已对 !tensor.defined() 提前 return,未定义 custom_output_indexes 不再被 to_cuda/check 访问 device,崩溃路径已消除。

自动代码评审 · head 85a3ac9c850a · LGTM

@rtp-llm-review-bot rtp-llm-review-bot 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.

lgtm ready to ci

评审版本:808d2b0cf46822cfbadfbf29695070034ba967c7

Comment thread rtp_llm/cpp/models/PyWrappedModel.cc Outdated
Comment thread rtp_llm/cpp/normal_engine/NormalOutputDispatcher.cc
Comment thread rtp_llm/cpp/models/PyWrappedModel.cc Outdated
Comment thread rtp_llm/models/downstream_modules/utils.py Outdated
Comment thread rtp_llm/frontend/frontend_worker.py Outdated
Comment thread rtp_llm/cpp/engine_base/executor_base/PostLayersProcessor.cc Outdated
Comment thread rtp_llm/cpp/models/ModelTypes.cc Outdated
Comment thread rtp_llm/cpp/models/ModelTypes.cc
Comment thread rtp_llm/cpp/normal_engine/NormalGenerateStream.cc
Comment thread rtp_llm/cpp/normal_engine/NormalModelInputGatherer.cc Outdated
Comment thread rtp_llm/cpp/models/ModelTypes.h Outdated
Comment thread rtp_llm/cpp/model_rpc/model_rpc_client.py Outdated
@CHW0218
CHW0218 force-pushed the feature/custom-output-main-20260915 branch from 7b14b84 to f390088 Compare September 22, 2026 07:00
for (int i = 0; i < 3; ++i) {
ASSERT_EQ(output_ids_vector[i], i);
}
ASSERT_TRUE(output_pb.has_custom_output());

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] fp16/bf16 custom_output 的 RPC 序列化路径缺少测试覆盖

docs/custom_output.md 宣称 "Float32, float16, bfloat16 and int32 outputs retain their dtype through RPC",且 runCustomOutput(PyWrappedModel.cc)的 TORCH_CHECK 显式放行 kFloat16/kBFloat16 输出 dtype;但 QueryConverterTest 仅用 torch::kInt32 覆盖 transResponse→transTensor 往返(本行),model_rpc_client_test.py 的 test_custom_output_without_aux_info 同样仅测 int32。fp16/bf16 涉及半精度/bf16 的 tobytes/frombuffer 字节布局,若 transTensor/trans_tensor 对这些 dtype 的解析存在缺陷(如字节序、宽度换算),会在生产环境静默产生错误数值,而文档承诺的 dtype 保留路径完全无测试兜底。

建议:在 QueryConverterTest 与 model_rpc_client_test 中补充 fp16/bf16 custom_output 的往返序列化断言,校验 dtype 与数值一致(例如用 torch.testing.assert_close 且 rtol=0/atol=0 验证 bf16 位模式)。

评审版本:f39008897bb8

@CHW0218
CHW0218 force-pushed the feature/custom-output-main-20260915 branch from f390088 to aa69bdc Compare September 22, 2026 07:54
check(model_input.prefix_lengths, "prefix_lengths");
check(model_input.sequence_lengths_plus_1, "sequence_lengths_plus_1");
check(model_input.lm_output_indexes, "lm_output_indexes");
check(model_input.custom_output_indexes, "custom_output_indexes");

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] custom_output_indexes 条件性定义却被 checkModelInputsOnCuda 无条件检查,未定义张量可能触发 TORCH_CHECK 失败

NormalModelInputGatherer 中 custom_output_indexes 仅在 needs_custom_output_indexes 为真时分配(if (needs_custom_output_indexes) { model_input.custom_output_indexes = torch::empty(...); }),decode step 或无 custom output 请求的批次该字段保持 undefined。而本补丁在 ensureModelInputsOnCuda / checkModelInputsOnCuda 中无条件加入 to_cuda(model_input.custom_output_indexes, ...)check(model_input.custom_output_indexes, ...)(+482、+507 行),与同列表内其余始终分配的字段(lm_output_indexes、prefix_lengths 等)不同。若 to_cuda/check 的 lambda 未对 tensor.defined() 做守卫,则每次无 custom output 的 forward(含所有 decode 步)都会在 is_cuda() 为 false 的未定义张量上失败。补丁中看不到这两个 lambda 的实现,无法确认其是否跳过未定义张量。

建议:在 to_cuda 与 check 调用前加 if (model_input.custom_output_indexes.defined()) 守卫,或在 checkModelInputsOnCuda 的 check lambda 内统一跳过未定义张量,避免条件性定义字段被无条件校验。

评审版本:aa69bdc5565b

@CHW0218
CHW0218 force-pushed the feature/custom-output-main-20260915 branch 2 times, most recently from 16f5dca to c4ce33f Compare September 22, 2026 08:43
}
if (custom_output_.defined()) {
generate_output.custom_output =
custom_output_.size(0) == 1 ? custom_output_ : custom_output_.narrow(0, i, 1);

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] custom_output 在 prepareGenerateOutput 中按持久成员 custom_output_ 判空,prefill 之后每个 decode step 都会重复下发陈旧的 custom_output

prepareGenerateOutput 中 custom_output_.size(0) == 1 ? custom_output_ : custom_output_.narrow(0, i, 1):当自定义输出只有一行时,循环内每个 generate_output 都拿到完整张量而非按 i 取行。按 gatherer 逻辑(每个 return sequence 写一个 index,handler 契约要求 output.size(0)==rows.size(0))size(0)==1 仅出现在单序列流,此分支纯属冗余;但一旦该不变量被破坏(例如 handler 返回行数少于选中行数),它会静默把序列 0 的结果复制给所有序列,掩盖了 dispatcher 的 row count mismatch 检查本应暴露的数据正确性问题。

建议:将判空改为按当前 step 的 update_info 判空,例如 if (update_info.custom_output.defined()),或在消费后清空 custom_output_,与 loss/prompt_logits 的 gating 保持一致。

评审版本:c4ce33f6d69d

// engine_creator routes embedding tasks through RtpEmbeddingOp, not this generation op.
if (py::hasattr(model, "custom_module") && !model.attr("custom_module").is_none()) {
TORCH_CHECK(!params.py_model.is_none(), "custom output requires a Python model");
RTP_LLM_CHECK_WITH_INFO(params.pd_sep_config.role_type == RoleType::PDFUSION && !propose_params

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] custom_output_pre_norm_ 只在 tp_rank==0 初始化,多卡 TP 下 generation-prefill cuda graph 启用状态在 rank 间不一致

RtpLLMOp::init 中仅在 params.parallelism_config.tp_rank == 0 时执行 params.py_model.attr("custom_output_handler") = ...。PyWrappedModel 构造函数通过 py::hasattr(py_model_, "custom_output_handler") 决定是否 initializeCustomOutput,非 rank0 上 custom_output_enabled_ 恒为 false,但 forwardPostLayers 仍在所有 rank 执行且 custom_output_indexes 仍被 gather/广播到所有 rank,非 rank0 静默丢弃索引,任何 rank 间行为不一致(如 GptModelOutputs 被 TP 同步)都可能引发未定义张量问题。

评审版本:c4ce33f6d69d

task_type = config.task_type
if task_type == TaskType.LANGUAGE_MODEL:
# An absent provider is optional; a broken installed generation provider is not.
if (

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] except ImportError 对 LANGUAGE_MODEL 直接 re-raise,捕获范围覆盖 create_internal_module 函数体内部抛出的 ImportError

新增代码在 try 块之前调用 import_optional_internal_source_entrypoint("models.downstream_modules.utils")。若内部源已安装但导入时报错(例如 entrypoint 模块自身 import 失败或抛非 ImportError 异常),该异常发生在 try/except ImportError 之外,不会被下方 except 捕获,LANGUAGE_MODEL 任务会在错误位置抛出未处理异常,与注释宣称的"broken installed generation provider is not"语义不符。

建议:将 create_internal_module(config, tokenizer) 的调用移出 try 块,或仅对 from ... import 语句的 ImportError 做 re-raise;对函数体抛出的 ImportError 单独处理(如记录日志后 return None),使“模块缺失/无 head”与“构造失败”两条路径语义分离。

评审版本:c4ce33f6d69d

TORCH_CHECK(indexes.defined() && indexes.dim() == 1 && indexes.size(0) <= context_batch_size,
"custom output indexes must contain at most one row per context sequence");
// Device-input staging is optional. Retain CPU indexes for async H2D;
// already-staged CUDA indexes require no host retention or additional copy.

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] initializeCustomOutput 强制 extend_forward_args 返回 ["selected_hidden_states"],base CustomHandler 默认返回旧列表会导致模型初始化直接崩溃

customOutputIndexes 中注释写明 'already-staged CUDA indexes require no host retention or additional copy',但代码无条件执行 buffer_holder_.hold_host(indexes),随后 indexes.to(torch::kCUDA, non_blocking=true)。在 eager 路径 NormalExecutor::ensureModelInputsOnCuda 已将 custom_output_indexes 转为 CUDA 后,hold_host 收到的是 CUDA 张量而非 host 张量,行为与注释矛盾,可能把设备张量误放入 host 持有器。

建议:在 initializeCustomOutput 对 extend_forward_args 不匹配时给出可降级的处理(如记录 warning 并禁用 custom_output,而非 TORCH_CHECK 硬失败),或仅在 handler 显式声明支持 custom_output 时才启用该路径。

评审版本:c4ce33f6d69d

@@ -380,6 +404,10 @@ void NormalOutputDispatcher::dispatchSingleStream(GenerateStreamPtr stream,
}

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] custom output 处理器错误被 sampler 错误静默覆盖

if (custom_output_batch_idx >= 0 && !model_output.custom_output_error.empty() && !error_info.has_value()):当 collectStreamSamplerError 已产生 error_info 时,custom_output_error(部署侧 handler 失败,已在 forwardPostLayers 记录)不会附加到 error_info,客户端只看到 sampler 错误。handler 失败属于部署级故障,与采样失败同时发生时被完全隐藏,不利于定位(只能依赖 RTP_LLM_LOG_ERROR 日志)。

建议:在 error_info 已存在时仍将 custom_output_error 附加到错误信息(如追加 detail 或单独字段),避免部署级失败被采样错误掩盖。

评审版本:c4ce33f6d69d

auto input_tokens = stream->currentExecuteTokens(i);
auto input_masks = stream->textTokensMask();
const int position = stream->generateInput()->custom_output_token_position;
// Return sequences occupy separate context rows; preserve one selected

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] custom_output 索引位置计算混用系统前缀偏移与缓存复用长度,存在越界/错位风险

GenerateTypes.h::updatePrefix 在插入系统前缀后将 custom_output_token_position += prefix_length(系统前缀长度),而 gatherer 用 position >= stream->prefixLength()(测试表明 prefixLength 为缓存复用长度)并计算 ctx.token_idx + position - stream->prefixLength()。当系统前缀长度与缓存复用长度不一致(例如系统前缀未被完全缓存复用)时,position - prefixLength() 的偏移量会错位,可能写出负索引或指向错误 token 行。

建议:统一坐标系:在 updatePrefix 后记录系统前缀偏移,gatherer 中显式用 (position - 系统前缀长度) 与复用长度比较,避免将两个不同长度混用。

评审版本:c4ce33f6d69d

// Selected context rows; undefined when no handler ran on this step.
torch::Tensor custom_output;
// The dispatcher turns handler failures into per-stream execution errors.
std::string custom_output_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] custom_output_error 与 GptModelOutputs.custom_output 未纳入 model RPC 序列化路径

custom_output_error 在 PyWrappedModel.cc:1203 设置、在 NormalOutputDispatcher.cc:407 由 dispatcher 消费(PDFUSION 下模型与引擎同进程,可正常工作)。但 grep 全仓确认该字段未出现在 QueryConverter.cc / model_rpc_client.py / model_rpc_service.proto 的任何序列化代码中——transResponse 仅把 GenerateOutput.custom_output 写入 FlattenOutputPB(custom_output=10),而 GptModelOutputs 中的 custom_output(OpData.h:129)与 custom_output_error(OpData.h:132)两个字段均未进入任何 RPC。仓库中存在 DecodeRpcServer/PrefillRpcServer 的 P/D 分离模型 RPC 路径(docs/custom_output.md 声明 P/D 分离不支持)。若未来在该路径启用 custom output,这两个字段会被静默丢弃,handler 失败不会上报到引擎、custom_output 结果也不会回传。

建议:若计划支持 P/D 分离,需把 GptModelOutputs.custom_output 与 custom_output_error 加入模型 RPC 的 proto 定义与序列化/反序列化;否则在文档中明确这两个字段仅限同进程 PDFUSION 场景(当前文档已声明 P/D 不支持,可仅作注释补充)。

评审版本:c4ce33f6d69d

torch::from_blob(const_cast<int*>(prefix_prompt.data()), {(int64_t)prefix_prompt.size()}, torch::kInt32);
input_ids = torch::cat({prefix_tensor, input_ids}, 0);
if (custom_output_token_position >= 0) {
custom_output_token_position += prefix_length;

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] updatePrefix 用 prefix_length 调整 position,gatherer 用 stream->prefixLength() 回减,两者语义耦合脆弱

updatePrefix 中 custom_output_token_position += prefix_length(+ 行),而 NormalModelInputGatherer::processContextStreams 用 position - stream->prefixLength() 计算行索引。若 stream->prefixLength() 包含 system prefix 之外的 reuse 长度(测试中 cached->setReuseLength(1) 即体现 reuse 与 system prefix 分离),则 prefix_length 与 stream->prefixLength() 不相等,position - prefixLength() 会错位到错误的 token 行,customOutputIndexes 选取错误的 hidden 行。现有单测仅覆盖无 reuse 的简单 updatePrefix 场景。

建议:统一使用同一长度来源(例如都基于 stream 的 prefixLength),或在 updatePrefix 后同步更新 stream 的 prefix 长度,并为 system prefix + reuse 组合补测试。

评审版本:c4ce33f6d69d

netaddi
netaddi previously approved these changes Sep 23, 2026
access_logger_test imports rtp_llm.ops, which loads libth_transformer_config.so during module initialization. Declare the runtime data dependency so Bazel includes the library in the test runfiles.
Reuse CustomModule/CustomHandler and the normal generation pipeline to return
custom tensor results for one selected prefill token per request. Keep token
selection, postprocessing and optional weights in deployment modules.

Select positions after RPC deserialization and multimodal expansion, compact
available context rows, and process the existing final-normalized output in
one batched handler call. Preserve ordinary generation and response formats;
omit custom output when prefix reuse covers the selected token.
@CHW0218
CHW0218 force-pushed the feature/custom-output-main-20260915 branch from c4ce33f to 85a3ac9 Compare September 23, 2026 02:43

auto task = [&, stream, batch_idx_in, batch_idx_out, token_offset, dispatch_stream]() {
// Assign compact rows in gather order before handing streams to workers.
const bool has_custom_output =

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] dispatchSingleStream 在 custom output 行数不匹配时提前 return,跳过 token 分发与 stream 状态更新

dispatcher 侧 has_custom_output = isContextStream() && custom_output_token_position >= prefixLength(),随后 custom_output_offset += cur_batch_size(按 stream 粒度、以 currentBatchSize 为步长);而 NormalModelInputGatherer.cc 的 processContextStreams 里按序列循环 custom_output_count++(position >= prefixLength() 时每序列写一个 index)。两者分处不同文件、无共享断言。只要 currentBatchSize() 与该 stream 被 gatherer 写入的序列数不一致(例如 beam search 展开、或将来 speculative 布局变化),或 allStreams() 与 contextStreams() 对 context stream 的相对顺序不一致,custom_output_batch_idx 就会指向错误的行区间。而现有防御 check 只判断 custom_output_batch_idx + cur_batch_size > size(0),仅能捕获总行数不足,无法捕获总数恰好相等但按 stream 错配(顺序颠倒/单 stream 行数互换)的静默错位。

建议:改为与 sampler 错误处理一致:记录错误但继续执行 token 分发与 stream->update,将错误信息放入 update_info.error_info 中,避免提前 return 破坏 dispatch 的记账与流状态完整性。

评审版本:85a3ac9c850a

query->generate_config->max_new_tokens = 2;
auto stream =
make_shared<NormalGenerateStream>(query, model_config, RuntimeConfig{}, ResourceContext{}, nullptr);
if (decode) {

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] testCustomOutputDispatch 仅覆盖单个 has_custom_output 的 stream,多 stream 的 custom_output_offset 累加路径未被验证

测试的 6 组 {score_rows, cached_first} 组合中,context stream 的 custom_output_token_position=0、prefixLength=0(has_custom_output=true),cached stream 经 setReuseLength(1) 后 position 0 < prefixLength(has_custom_output=false),decode stream 为 decode(false)。因此整个测试里 custom_output_offset 只会从 0 推进一次,从未出现两个 stream 同时 has_custom_output=true 的场景,offset 累加与 batch_idx 分配的核心逻辑(custom_output_offset += cur_batch_size 跨 stream 的累积正确性)完全没有被断言覆盖。

建议:增加一个两个 context stream 均 position >= prefixLength 且行数不同的用例,断言第二个 stream 的 batch_custom_output 取到正确的行区间(例如 stream A 2 行、stream B 1 行,模型返回 3 行时 B 应取第 3 行而非前两行)。

评审版本:85a3ac9c850a

Comment thread rtp_llm/cpp/normal_engine/NormalOutputDispatcher.cc
Comment thread rtp_llm/cpp/normal_engine/NormalOutputDispatcher.cc
Comment thread rtp_llm/cpp/normal_engine/NormalOutputDispatcher.cc
Comment thread rtp_llm/cpp/normal_engine/NormalOutputDispatcher.cc
@wht21

wht21 commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

internal source has been updated, please review the changes!

@rtp-llm-review-bot rtp-llm-review-bot 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.

lgtm ready to ci

评审版本:85a3ac9c850a10e3ede9467772d7ecc951a12451

@CHW0218
CHW0218 merged commit efae45c into main Sep 23, 2026
16 of 19 checks passed
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.

4 participants