feat: support context custom output processors - #1432
Conversation
PR #1432 评审:LGTM
无阻塞项非阻塞发现
已确认修复
自动代码评审 · head |
rtp-llm-review-bot
left a comment
There was a problem hiding this comment.
lgtm ready to ci
评审版本:808d2b0cf46822cfbadfbf29695070034ba967c7
7b14b84 to
f390088
Compare
| for (int i = 0; i < 3; ++i) { | ||
| ASSERT_EQ(output_ids_vector[i], i); | ||
| } | ||
| ASSERT_TRUE(output_pb.has_custom_output()); |
There was a problem hiding this comment.
[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
f390088 to
aa69bdc
Compare
| 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"); |
There was a problem hiding this comment.
[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
16f5dca to
c4ce33f
Compare
| } | ||
| if (custom_output_.defined()) { | ||
| generate_output.custom_output = | ||
| custom_output_.size(0) == 1 ? custom_output_ : custom_output_.narrow(0, i, 1); |
There was a problem hiding this comment.
[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 |
There was a problem hiding this comment.
[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 ( |
There was a problem hiding this comment.
[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. |
There was a problem hiding this comment.
[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, | |||
| } | |||
|
|
|||
There was a problem hiding this comment.
[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 |
There was a problem hiding this comment.
[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; |
There was a problem hiding this comment.
[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; |
There was a problem hiding this comment.
[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
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.
c4ce33f to
85a3ac9
Compare
|
|
||
| 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 = |
There was a problem hiding this comment.
[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) { |
There was a problem hiding this comment.
[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
|
internal source has been updated, please review the changes! |
rtp-llm-review-bot
left a comment
There was a problem hiding this comment.
lgtm ready to ci
评审版本:85a3ac9c850a10e3ede9467772d7ecc951a12451
Normal LLM generation can return deployment-defined postprocessing for one selected prompt-token position. The existing
CustomModule/CustomHandlerreceives selected rows from the model's final normalized output and returns a tensor; no MLP or activation is prescribed.custom_outputor OpenAIextra_outputs.custom_output.ValueErroror 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:OUTPUT_DISPATCHER_WORKER_COUNT=2, confirmed in service logs. Generated IDs and custom-output tensors match the corresponding serial configuration exactly.n=2passed.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.