Skip to content

feat: add RDMA transport for ViT output - #1353

Open
ydshi0 wants to merge 1 commit into
alibaba:mainfrom
ydshi0:feature/rdma_transport
Open

feat: add RDMA transport for ViT output#1353
ydshi0 wants to merge 1 commit into
alibaba:mainfrom
ydshi0:feature/rdma_transport

Conversation

@ydshi0

@ydshi0 ydshi0 commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

本提交主要完成两项工作:

  1. 建设通用 Tensor RDMA 传输能力,具备 Tensor 导出、远端read读取、QP 并发、显存池管理和资源释放能力,并与具体业务解耦。
  2. 实现 ViT 计算流程与输出通信解耦:ViT 完成 forward 后只将计算结果交给 transfer() 接口,LLM 侧通过 fetch() 获取最终结果;通信层通过 gRPC 传递请求、能力协商、descriptor/receipt、超时和 lease release 等控制信息,并调用通用 RDMA 完成数据传输。

通用 Tensor RDMA 能力

通用层只定义 Tensor 所有者和读取者之间的数据面:

角色 公共接口 职责
A 端,Tensor 所有者 RdmaExport::create(tensors) 将 CUDA Tensor 装入注册 GPU slot,生成 descriptor 和 lease
B 端,Tensor 读取者 RdmaRead::read(descriptors) 校验 descriptor,分配本地 GPU buffer,发起单边 RDMA READ 并恢复 Tensor
A 端控制面 RdmaExport::release(lease_ids) 在 B 端完成读取后归还 slot,保留 MR 供后续请求复用

descriptor 携带 endpoint、远端地址、rkey、payload 大小和 Tensor manifest;manifest 描述每个 Tensor 的 shape、dtype、offset 和 nbytes。外源提供公共接口、wire schema、provider factory 和 no-op 实现,内源通过同一接口注册 Barex provider。

显存池与生命周期

A/B 两端使用注册 GPU pool:slot 按需分配,release 后回到 free queue,MR 保持注册以供复用。pool-owned 显存默认总上限 8 GiB;新 bucket OOM 时执行有界 Shrink 50% → 重试 → Shrink 100% → 最后重试,仍失败则向业务通信层返回错误,由业务决定是否回退。

Pool-owned Tensor slot 与 PD KV Cache User-MR 使用不同的容量和回收路径。Shrink 只注销空闲 pool-owned slot,不处理正在 READ 的 busy slot,也不会释放 PD/KV Cache 已有显存。正常路径由 B 端通过业务控制面归还 lease,遗漏的 release 由 A 端 TTL GC 兜底。

ViT 通信层解耦

业务层只调用统一接口:LLM 侧调用 fetch(request),ViT forward 完成后调用 transfer(request, res)。grpc-inline/RDMA 选择、receipt 消费、deadline、fallback 和 release 全部位于通信层,业务代码不处理 descriptor 或 lease。

sequenceDiagram
    participant LB as LLM 业务层
    participant LT as LLM 通信层
    participant VB as ViT 业务层
    participant VT as ViT 通信层

    LB->>LT: fetch(request)
    LT->>VB: request(含 LLM 能力声明)
    VB->>VB: ViT forward → res
    VB->>VT: transfer(request, res)
    VT->>VT: 选择 backend,生成 receipt
    VT-->>VB: receipt
    VB-->>LT: 返回 receipt
    LT->>LT: 匹配 reader,消费 receipt
    LT-->>LB: MultimodalOutput
Loading

MM_TRANSPORT_MODE=auto 时 LLM 按请求声明 RDMA 能力,ViT 优先选择 RDMA;初始化、导出、READ 或协议校验失败时,通信层撤销能力并最多重发一次 inline 请求。grpc 模式始终使用原内联路径,非法 mode 在配置阶段直接拒绝。

ViT RDMA 适配

ViT 是通用 Tensor RDMA 的第一个业务适配。生产端 MMRdmaExporter 将完整输出导出为一个或多个 slot;消费端 MMRdmaReader 批量读取 descriptor,按 role 和 manifest 重组 Tensor,再按 split_size 恢复每张图片的 embedding。

sequenceDiagram
    participant T as LLM 通信层
    participant R as MMRdmaReader
    participant D as RDMA NIC
    participant C as ViT 通信层

    T->>R: consume(receipt)
    R->>R: 校验 descriptors/manifests
    R->>D: batch RDMA READ(descriptors)
    D-->>R: payload 直达 LLM GPU
    R->>R: 恢复并 clone Tensor<br/>按 role/split_size 重组
    R->>C: releaseAsync(lease ids)
    R-->>T: MultimodalOutput
Loading

关键实现

  • 一次请求覆盖 embedding、position ids 和 extra inputs;业务 role 位于 MMRdmaSlotPB,通用 manifest 只描述 Tensor 布局。
  • 单 slot 默认上限 1 GiB。embedding 超限时沿第 0 维切分,并按固定顺序打包成多个有序 slot;中途失败会回滚已创建的 lease。
  • READ 成功后异步 release;失败和 discard 路径同步 release。

配置

MM_TRANSPORT_MODE=auto   # 默认优先 RDMA,失败回退 grpc
MM_TRANSPORT_MODE=grpc   # 强制 grpc 

端到端性能

B300 单机实验使用 Qwen3-VL-32B-Instruct BF16,Encoder、Prefill、Decode 独立部署且各 TP=1。输入为一张 2048 x 1365 JPEG 图片和 prompt Describe this image.;每次请求的 ViT 输出为 112.7 MB,cache 均关闭。

阶段 RDMA gRPC inline
ViT 计算 356.002 ± 7.198 ms 363.840 ± 14.107 ms
ViT 输出传输 6.070 ± 1.075 ms 457.927 ± 8.523 ms
LLM 部分 178.664 ± 5.767 ms 186.377 ± 4.457 ms
TTFT 540.737 ± 9.399 ms 1008.144 ± 16.126 ms

RDMA 将 ViT 输出传输时间从 457.927 ms 降至 6.070 ms,降低 98.67%;TTFT 减少 467.407 ms,降低 46.36%。本轮实验使用 MTU=4096QP=8,RDMA READ 平均有效带宽约为 69 GB/s

可观测性与验证

  • [RDMA-BW] 记录通用数据面 READ bytes、耗时和带宽。
  • [MM-RDMA-HIT] 证明 ViT 请求实际命中 RDMA

@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 #1353

Status: LGTM

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

Reviewed: commit 4227e2715a64 · 2026-08-30 15:40 UTC+8

lgtm ready to ci

Non-blocking Suggestions

P2

  • RDMA 读取占满整个剩余 deadline,read_timeout_ms 在调用点未生效且预算耗尽被误记为端点故障 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:207
    • 建议:调用点改为 std::min(rdma_config_->read_timeout_ms, remaining - kFallbackReserveMs),显式为降级轮次预留最小预算,并让 eager 构造路径也持有 RdmaConfig(或让 MMRdmaReader 始终持有而非 optional)。同时把「预算耗尽」与「RDMA 读失败」拆成两种返回原因:前者只按 deadline 失败上报、不计入 RdmaCircuitBreaker,并给独立 metric reason(如 rdma_budget_exhausted)。若确定该上限由仓外 provider 依据 RdmaConfig 施加,请改掉 help 与头注释的措辞并在 readAllSlots 就近注明责任归属。
  • releaseAsync 队列饱和时在请求线程同步发 release RPC,违反头文件显式约定 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:127
    • 建议:饱和分支不要占用消费者线程:保留有界队列 + release_queue_full 计数后直接丢弃(依赖 encoder 侧 slot_gc_timeout_ms),或交由溢出线程/极短 deadline(如 50ms)异步发送。若确要保留同步兜底,至少改为 std::min(release_timeout_ms_, 剩余预算) 并同步修正 MMRemoteOutputTransport.h:81 的约定文字。另建议把 reportRpcClientErrorRTP_LLM_LOG_WARNING(:120-122)移出 release_mutex_ 临界区。
  • 析构 drain 未按 endpoint 合批且无总预算,进程退出耗时不可控 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:49
    • 建议:抽出与 releaseLoop 共用的 endpoint→handles 聚合函数,析构时先合批再发送;同时为整段 drain 设置总预算(复用 DeadlineBudget,上限 1×release_timeout_ms 或固定 2s),超预算即放弃剩余批次并打 WARNING,交由 encoder 侧 TTL GC 回收。
  • RdmaExport/RdmaRead 的所有权与拉平顺序契约未声明,读侧只校验数量不校验形状 @ rtp_llm/cpp/rdma_transport/RdmaTransport.h:60
    • 建议:在 RdmaTransport.h 显式写明:create()/createBatch() 是否必须拷入自有已注册槽、若为零拷贝则必须持有入参张量直到 release()、入参是否要求 is_contiguous()read() 返回顺序必须严格等于「descriptor 顺序 × descriptor 内 tensors 顺序」。同时在 readAllSlots 中把 manifest 的 TensorMeta.shape/nbytes/dtype 与返回张量逐项比对,不符即返回 false 走降级;并在 exportSlots 入口对输入加 is_contiguous() 断言或就地 .contiguous(),不把该前置条件只压在 Python 调用方。
  • RdmaConfig 定义在引擎配置聚合头,导致底层 rdma_transport 反向依赖引擎配置 @ rtp_llm/cpp/config/ConfigModules.h:336
    • 建议:把 RdmaConfig 下移到 rtp_llm/cpp/rdma_transport/(或无依赖的 RdmaTransportTypes.h 目标),由 MMTransportConfig 反向 include 该轻量 header,从而去掉 rdma_transport → config_modules 依赖;若保留在 config 目录,至少拆成独立 cc_library(参照本 PR 新增的 :mm_transport_mode)并重命名为 MMRdmaConfig 与 Python 侧对齐。
  • Embedding 入口未接入 extractVitConfig,新增 125s 固定 deadline 且 --mm_timeout_ms 抬不动 @ rtp_llm/cpp/multimodal_processor/RemoteMultimodalProcessor.h:44
    • 建议:在 Embedding 入口同样调用 extractVitConfig,或让 grpcOnlyTransportConfig() 接收由 mm_timeout_ms 派生的 default_rpc_timeout_ms——既保留「Embedding 路径固定 inline gRPC 数据面」的设计意图,又让两条入口的超时语义一致。若确要长期保持差异,请在 RemoteMultimodalProcessor.h:21 注释与 --mm_timeout_ms 的 help 中显式写明「Embedding 路径不消费该值,deadline 固定 125s」。
  • RDMA provider 首次构造落在请求关键路径上,ViT 侧还持有 GIL @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaExporter.cc:30
    • 建议:ViT 侧把 extractRdmaConfig(需 GIL)与 createRdmaExport(纯 C++)拆开,在后者外层加 py::gil_scoped_release,或在 server 启动阶段完成构建(try_create 已在启动时探测 available(),顺带构建不增加失败面)。LLM 侧建议在 createMMRemoteOutputTransport 中投递一次后台预热,或让 advertise() 在初始化进行中直接返回 false 使本次请求走 inline;两侧都为构造耗时打点以定位首请求毛刺。
  • RDMA 空导出降级无指标且每请求打 WARNING,一次初始化失败即永久降级 @ rtp_llm/multimodal/transport/rdma/backend.py:106
    • 建议:为空导出分支补 self._report_error("rdma_export_empty")(reason 与异常分支区分)并把逐请求 warning 改为首次 + 限频采样;为 enabled()==False 与构造失败两条路径补 warning 与 rdma_exporter_init_failed 指标,并给瞬时失败加冷却期后的有界重试(记录失败时间戳,超过冷却期允许再试一次)。若确定不重试,请在注释与日志中显式声明该 fail-permanently 契约,并让 supports() 直接返回 False 以省掉每请求加锁空转。
  • 可恢复的 RDMA 降级复用请求级错误指标,污染服务端错误 QPS @ rtp_llm/multimodal/transport/rdma/backend.py:181
    • 建议:为传输层降级引入独立计数指标(例如带 reason tag 的 VIT_OUTPUT_TRANSPORT_DEGRADE_QPS),仅保留确实造成远端资源泄漏的 rdma_release_error 继续复用错误指标;或在告警侧显式排除 reasonrdma_ 开头的样本,并在代码注释中写明该约定。
  • 描述符解析失败时回滚不完整,未解析槽的租约只能等 slot GC @ rtp_llm/multimodal/transport/rdma/backend.py:127
    • 建议:让回滚覆盖全部已导出槽:对剩余原始 bytes 用嵌套 try 尽力解析出 lease_id 后一并传给 _roll_back;更稳妥的是让 export_embedding 同时返回 lease_id 列表(或新增 release_batch(descriptor_bytes)),使 Python 侧不依赖解析成功即可归还所有槽。同时补一条「非法描述符位于中间」的用例。
  • 用 getattr 字面量探测 available,把符号缺失与 provider 不可用折叠为同一路径 @ rtp_llm/multimodal/transport/rdma/backend.py:44
    • 建议:给 DisabledMMRdmaExporter 补一个与 enabled() 对称的 @staticmethod available() -> False,此处直接调用 MMRdmaExporter.available(),去掉字面量 getattr;这样契约缺失会以 AttributeError 落到 except 分支的 warning,而不是与「provider 不可用」混同。同时把日志改为准确表述,例如 "[VIT] RDMA provider unavailable; mm output falls back to inline bytes",与 factory.py 的措辞对齐。
  • 超时常量、RDMA 默认值与 mode 字面量在四到五处重复,仅靠注释维持一致 @ rtp_llm/cpp/config/ConfigModules.h:362
    • 建议:C++ 侧让 MMTransportConfig 复用已有具名常量(或把两个常量下沉到 rtp_llm/cpp/config 共享),VitConfigExtract.h:15 改为 kDefaultVitRpcTimeoutMs - kVitRpcTimeoutMarginMs 之类的派生表达式,mode 统一用 kMMTransportModeAuto/kMMTransportModeGrpc;Python 侧让 argparse 的 default= 直接引用 MMRdmaConfig() 属性,mode 改为 Literal["auto","grpc"]。跨语言无法共享处加断言(C++ 默认值 vs DEFAULT_MM_TIMEOUT_MS + 5000、C++ 常量集 vs MM_TRANSPORT_MODES),让漂移在 CI 暴露而非靠注释。
  • ACCL_USE_NICS 的物理 index / local_rank 双键空间回退可能静默绑定错误网卡且日志误导 @ rtp_llm/config/server_config_setup.py:643
    • 建议:单一化 ACCL_NIC_GPU_AFFINITY 的键语义(建议固定为物理 GPU index),删除会造成误导的隐式回退,miss 时打 WARNING 并放弃绑定;若确需兼容旧 rank-keyed 表,用显式开关或表内 keying 标识区分,并让成功日志区分「physical 命中」与「local_rank 回退」。UUID 匹配要求完整相等或至少校验唯一命中。同时在已有的 rtp_llm/config/test/server_config_setup_test.py 中补参数化用例,覆盖 _physical_gpu_index(未设置/数字列表/UUID/越界/缩写 UUID 多卡)与 _configure_gpu_nic_affinity(显式 ACCL_USE_NICS 优先、非法 JSON、非对象、查表 miss、双键冲突)。
  • load_gpu_nic_affinity 依赖 CWD 相对文件,多 ViT worker 并发启动存在读写竞争且失败仅 INFO @ rtp_llm/config/server_config_setup.py:589
    • 建议:在进程私有目录执行并按绝对路径读取(tempfile.mkdtemp() 作为 subprocess.run(cwd=...),读完清理),或由父进程在拉起 worker 前统一执行一次、子进程通过环境变量继承,worker 侧退化为「已设置则直接返回」。把失败日志提升为 WARNING 并打印 e.stderr;并考虑把「整表拒绝」放宽为「逐条过滤」,保留合法映射并在日志中列出被丢弃的键。
  • 新增的配置抽取层与传输工厂映射零测试,deadline 派生公式与 11 个属性契约完全未覆盖 @ rtp_llm/cpp/config/VitConfigExtract.h:15
    • 建议:把不依赖 pybind 的部分抽成纯函数(如 resolveWorkerRpcTimeoutMs(configured_ms, margin_ms))放进零依赖的 :mm_transport_mode 目标并补 gtest,覆盖 mm_timeout_ms 为 0/负/极大三种边界与合法非法 mode;在 test/BUILD 增补对 transport:mm_remote_output_transport 的依赖并断言非法 mode 抛 std::invalid_argumentgrpc 模式一次 fetch 不广告 RDMA。字段映射侧补一条 Python 断言(VitConfig().output_transport 的完整属性名集合与默认值 mode=="auto"qp_count==8max_slot_bytes==1024**3release_timeout_ms==1000)即可闭环。
  • libmm_rdma_exporter 的 pybind 契约在 C++ 与 Python 两端都被替换,序列化格式无真实覆盖 @ rtp_llm/cpp/pybind/MMRdmaExporterInit.cc:5
    • 建议:参照同目录 config_pickle_test(pybind/BUILD:11-19 的 data = [...] 形态)新增以 //:mm_rdma_exporter 为 data 的 py_test:断言 from rtp_llm.ops import MMRdmaExporter 得到的不是 DisabledMMRdmaExporteravailable() 可调用且返回 bool、export_embedding/release/enabled 属性存在(开源构建下 available() 恒 False,无需硬件)。同时在 mm_rdma_transport_test 增加一条走公开 exportEmbedding 的用例,把返回的每段 py::bytesMMRdmaSlotPB::ParseFromString 解回来断言 roles 顺序与 descriptor 字段。
  • GrpcMMControlClient 的后台线程、有界队列与析构 drain 无任何单测 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:27
    • 建议:把 stub 发送抽象为可注入函数(或提供可注入 MultimodalRpcPool 的测试构造入口、进程内 fake MultimodalRpcService),补充聚焦单测:饱和边界 1023/1024/1025 handles、饱和时是否走同步路径并上报 release_queue_full、析构时队列非空是否全部 drain 且计数归零、stopping_ 后入队是否转同步。
  • model_rpc_server 排除了 MultimodalPbConverter.cc 但未补显式 deps,仅靠传递依赖链接 @ rtp_llm/cpp/model_rpc/BUILD:67
    • 建议:在 deps 中显式加入 ":multimodal_pb_converter",与 ":tensor_pb_convert" 保持同一约定;并把 MultimodalPbConverter.h 一并加入 hdrs 的 exclude 列表,使头文件归属与实现归属一致,链接关系不再依赖 multimodal_processor 的内部依赖选择。
  • 导出器 .so 为序列化一个小消息静态链入整份 model_rpc gRPC/protobuf @ rtp_llm/cpp/multimodal_processor/transport/rdma/BUILD:29
    • 建议:把 MMRdmaSlotPB(含 Role 枚举)下移到 rtp_llm/cpp/rdma_transport/proto/tensor_rdma.protomodel_rpc_service.proto 通过已有 import 引用,使 mm_rdma_exporter 只依赖 tensor_rdma_cc 这个小目标;若必须保留现依赖,请在 PR 描述中说明已实测 ViT 进程同时加载两个 .so 不会出现重复 descriptor 注册。
  • 新增 smoke qwen3_vl_rdma 硬断言仅真实 provider 才有的日志标记,无 provider 可用性门控 @ rtp_llm/test/smoke/suites_h20_oss.bzl:569
    • 建议:给该用例加上与 provider 可用性对应的门控:在 defs.bzl 增加 requires_rdma 语义的 tag(或用 select() 绑定到带真实 provider 的 arch 配置),使无 provider 的构建不生成/不选中该 target;并显式传入 RDMA 相关 smoke_args,使失败原因可区分「未走 RDMA」与「无硬件」。作为最低要求,请在 PR 描述中说明该用例只在带真实 provider 且具备 RDMA NIC 的节点上运行。
  • ViT proxy 跨 worker 重试会孤立已导出的 RDMA slot 且无任何可观测信号 @ rtp_llm/server/vit_proxy_server.py:501
    • 建议:至少补一条可归因信号:重试前若 request.support_rdma 为真,按 worker 维度上报 rdma_slots_possibly_orphaned 并打 WARNING,便于把「RDMA 命中率下降」关联到 proxy 重试;更彻底的做法是重试前对失败 worker 主动发一次按 request_id 维度的清理(需 worker 侧支持)。若接受现状,请在 PR 描述中明确「proxy 重试孤立的 slot 由 worker TTL GC 承担」并给出 slot 池容量下限建议。

P3

  • RdmaReadResult::lease_ids 为死字段,读侧本地资源释放归属不明确 @ rtp_llm/cpp/rdma_transport/RdmaTransport.h:53
    • 建议:若无语义直接从 RdmaReadResult 删除;若确实代表本地缓冲区租约,则在头文件注明由谁在何时释放,并在 readAllSlots 成功/失败两条路径都做对应处理(例如用与 SlotLease 同构的 RAII 包装)。
  • 熔断器以 endpoint 为 key,代理模式下单个异常 worker 会关闭整个代理的 RDMA @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.h:38
    • 建议:在代理模式下把熔断粒度与可观测维度对齐到 worker:可让 ViT 在 MultimodalOutputPB 或 descriptor 中回传 worker 标识,LLM 以 endpoint + worker 作为熔断 key 与 metric tag;若暂不改协议,请在 RdmaCircuitBreaker 附近注释说明「代理模式下熔断粒度为整个代理」,避免运维误判为单卡问题。
  • 熔断器用例依赖进程级静态表与 30 秒时间窗,不可重复执行且恢复路径未覆盖 @ rtp_llm/cpp/multimodal_processor/test/MMRemoteOutputTransportTest.cc:275
    • 建议:给 RdmaCircuitBreaker 增加仅测试使用的 reset(endpoint)(或让静态表/时钟可注入),在 SetUp 中清理该 endpoint 状态使用例在 --gtest_repeat 下可重复;同时至少补一条 recordSuccess 复位计数的用例(无需时钟),若要覆盖开窗过期则把 now 抽为可注入参数。
  • pybind 导出的 C++ VitConfig 未同步 output_transport,pickle 静默丢字段 @ rtp_llm/cpp/config/ConfigModules.h:367
    • 建议:若确认 ops.VitConfig 已无 Python 消费者,直接从 rtp_llm/ops/__init__.py 的导出列表移除该类型,避免留下契约不完整、pickle 有损的公开类型;若要保留,则为三个新结构体注册 pybind 类型、给 VitConfigdef_readwrite("output_transport", ...) 并把它纳入 py::pickle(同时放宽 setter 的 size 校验以兼容旧 payload)。
  • proto_deps 需手工与 import 保持同步且不具备传递性 @ rtp_llm/cpp/model_rpc/proto/BUILD:29
    • 建议:proto_deps 改为引用 tf_proto_library_cc 已生成的传递性 filegroup(即目标 proto 包的 *_proto_srcsbazel/tf_proto.bzl 会自动聚合其自身 protodeps),这样新增嵌套 import 时无需再改 model_rpc/proto/BUILD
  • releaseNow 获取连接失败时静默返回,无日志与指标 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:144
    • 建议:在该分支补 metrics_->reportRpcClientError(endpoint, kReasonConnectionError) 并按频率打 WARNING,使 release 通道的连接故障与 request() 保持同等可观测性。
  • 新增文件未通过 pre-commit 格式化(clang-format 对齐、isort 顺序) @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:22
    • 建议:提交前跑一遍仓库 pre-commit(clang-format + isort),避免后续改动这些文件时被格式化 diff 淹没真实逻辑变更。

Checklist Findings (22 fail / 51 total)

General Principles Checklist

  • [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue 导出器 .so 为序列化一个小消息静态链入整份 model_rpc gRPC/protobuf
    mm_rdma_exporter 依赖 //rtp_llm/cpp/model_rpc/proto:model_rpc_service_cc_proto,只为产出 MMRdmaSlotPB 的序列化字节(MMRdmaExporter.h 返回 std::vector<py::bytes>)。该 proto 目标同时带入完整 model_rpc_service 消息集、gRPC service stub 与 alwayslink=Truegrpc_buffer_list_link_fix(proto/BUILD:32-51)。而根 BUILD:187-198libmm_rdma_exporter.so 未采用 th_transformer(BUILD:200-204 用 srcs = [":rtp_compute_ops"] 去重)的写法,会独立静态包含一份 gRPC/protobuf:wheel 体积增加,且同时加载 libth_transformer.so 与本 .so 的进程存在两次 descriptor 注册的加载期风险
  • [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue pybind 导出的 C++ VitConfig 未同步 output_transport,pickle 静默丢字段
    VitConfig 新增成员 MMTransportConfig output_transport(:369),但该结构体也是 pybind 导出类型:pybind/ConfigInit.cc:2015-2030py::class_<VitConfig> 只有 .def_readwrite("vit_separation", ...)py::pickle getter 为 py::make_tuple(self.vit_separation)、setter 校验 t.size() != 1,因此 Python 侧既读写不到 output_transport,pickle 往返也会把整棵传输配置重置为默认;MMTransportConfig/MMControlConfig/RdmaConfig 三个新结构体均未注册 pybind 类型。之所以只判 P3:全仓 Python 的 VitConfig 均来自 rtp_llm.config.py_config_modules(该类含 output_transport),`rtp
  • [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue 导出器 .so 为序列化一个小消息静态链入整份 model_rpc gRPC/protobuf
    mm_rdma_exporter 依赖 //rtp_llm/cpp/model_rpc/proto:model_rpc_service_cc_proto,只为产出 MMRdmaSlotPB 的序列化字节(MMRdmaExporter.h 返回 std::vector<py::bytes>)。该 proto 目标同时带入完整 model_rpc_service 消息集、gRPC service stub 与 alwayslink=Truegrpc_buffer_list_link_fix(proto/BUILD:32-51)。而根 BUILD:187-198libmm_rdma_exporter.so 未采用 th_transformer(BUILD:200-204 用 srcs = [":rtp_compute_ops"] 去重)的写法,会独立静态包含一份 gRPC/protobuf:wheel 体积增加,且同时加载 libth_transformer.so 与本 .so 的进程存在两次 descriptor 注册的加载期风险
  • [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue releaseNow 获取连接失败时静默返回,无日志与指标
    request()pool_.getConnection 失败时会上报 kReasonConnectionError 并返回错误(:59-63),而 releaseNow 在 :144-146 if (!connection_status.ok()) { return; } 直接静默返回,既无日志也无指标。RPC 失败尚有 :156-160 的 WARNING,唯独连接获取失败这条路径完全不可见——此时 lease 只能等 encoder 侧 TTL GC 回收,现场无法从任何信号判断是否发生了大规模 release 丢失。
  • [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue RDMA 读取占满整个剩余 deadline,read_timeout_ms 在调用点未生效且预算耗尽被误记为端点故障
    readAllSlots 直接把请求剩余预算当作单次读取超时:const int64_t remaining = context.budget.remainingMs(); ... reader_->read(descriptors, remaining);(:207-211),从未与 RdmaConfig::read_timeout_ms 取 min,而 ConfigModules.h:341 注释与 --mm_rdma_read_timeout_ms 的 help 都写明「实际预算取 min(请求剩余时间, 该值)」。若 provider 不自行封顶,读取挂起会耗尽预算,degradeToTerminal 随后在 context.budget.exhausted() 处直接返回 MM_PROCESS_ERROR(MMRemoteOutputTransport.cc:118-120),本可 inline 成功的请求变成请求级失败。另 remaining<=0 返回 false 与真实读失败共用返回值,consume 无差别 `noteFailu
  • [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue ViT proxy 跨 worker 重试会孤立已导出的 RDMA slot 且无任何可观测信号
    proxy 在 stub.RemoteMultimodalEmbedding(request, timeout=attempt_timeout_s)(:501)抛 grpc.RpcError 时(:521,:539)会换 worker 重试,而 self._transport_router.record_receipt(worker_address, response)(:505)只在成功返回后执行。若第一个 worker 已完成 ViT 前向并导出 RDMA slot,只是响应在 proxy 侧超时/丢失,这些 lease_id 既不进入 _handle_routes 也不会随响应到达 LLM,因此永远收不到 ReleaseRdmaLease,只能等该 worker 的 slot_gc_timeout_ms(默认 60s)回收。proxy 只上报 grpc_error,没有任何指标或日志指示「本次重试可能孤立了远端已注册显存」,超时风暴下可静默耗尽 slot 池。
  • [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue releaseNow 获取连接失败时静默返回,无日志与指标
    request()pool_.getConnection 失败时会上报 kReasonConnectionError 并返回错误(:59-63),而 releaseNow 在 :144-146 if (!connection_status.ok()) { return; } 直接静默返回,既无日志也无指标。RPC 失败尚有 :156-160 的 WARNING,唯独连接获取失败这条路径完全不可见——此时 lease 只能等 encoder 侧 TTL GC 回收,现场无法从任何信号判断是否发生了大规模 release 丢失。
  • [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue RDMA 空导出降级无指标且每请求打 WARNING,一次初始化失败即永久降级
    try_transfer 两条降级出口不对称:异常分支(:99-104)既打日志又 _report_error("rdma_export_error"),而 if not desc_bytes_list:(:106-114)只有逐请求 logging.warning、无任何指标——但按 exportSlots/createBatch 语义,slot 超限、MR 池耗尽、create 失败整体回滚这些最常见失败恰好都返回空列表走这条路径,主要降级原因在监控上不可归因,持续失败时构成热路径日志洪泛。_get_exporter(:63-75)用 _exporter_init_attempted(:66 先置 True)做一次性初始化:构造抛异常仅 warning、enabled() 为 False(:71-72)既无日志也无指标,而 MMTransportConfig.mode 默认即 auto,一次瞬时故障(端口占用、NIC 忙)会让整个进程生命周期永久退回 inline。
  • [6.1] Quality — 逻辑变更未混入无关格式化 → issue 新增文件未通过 pre-commit 格式化(clang-format 对齐、isort 顺序)
    仓库 .clang-format 第 6-7 行开启 AlignConsecutiveAssignments/AlignConsecutiveDeclarations,但 MMGrpcTransport.cc:22-25 四个连续常量的 = 分成两个对齐列(kReasonConnectionError/kReasonGrpcErrorkReasonReleaseQueueFull/kMaxPendingReleaseHandles 宽度不同),成员声明区 :188-196 同样存在两组不同宽度的对齐,说明该文件未经 clang-format 处理。Python 侧 multimodal/transport/grpc/backend.py:7-11rtp_llm.multimodal.transport.base 排在 rtp_llm.multimodal.mm_process_engine 之前,与 isort(--profile=black)字典序不符;rdma/backend.py:14-19 同样。
  • [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue proto_deps 需手工与 import 保持同步且不具备传递性
    generate_grpc_proto 新增的 proto_deps 在实现里只做 inputs = [proto_file] + ctx.files.proto_deps(bazel/py_proto.bzl:20),没有传递闭包。当前 tensor_rdma.proto 不 import 其他文件所以可用,但一旦它将来 import 第三个 proto,model_rpc_service_py 的 protoc 会以 file not found 失败,且报错位置与真正需要修改的 BUILD 相距较远。同一份依赖关系在此 BUILD 里被写了三遍(protodeps @:7、proto_deps @:29、py_library.deps @:56),任何一处漏改都只在特定构建路径上暴露。
  • [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue RdmaReadResult::lease_ids 为死字段,读侧本地资源释放归属不明确
    RdmaReadResult 新增 std::vector<std::string> lease_ids,但全仓检索无任何生产代码读取:唯一写入方是测试 fake(MMRemoteOutputTransportTest.cc:122),RdmaTransport.cc:53 的同名变量只是 createBatch 内的局部回滚列表。MMRdmaReader::consume 释放的是 handlesOf(receipt)(远端 lease),与 read() 返回的 lease_ids 无关。在一个需要仓外实现的新接口上,这种歧义会让实现者无法判断该 lease 是否代表本地已注册缓冲区、是否必须由调用方归还,而 RDMA 读路径正是本地缓冲区泄漏最易发生处。
  • [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue RdmaExport/RdmaRead 的所有权与拉平顺序契约未声明,读侧只校验数量不校验形状
    create(const std::vector<torch::Tensor>&) 只注明「失败时 lease_id 为空」,未规定 provider 是拷入自有已注册槽还是就地注册调用方显存,也未说明入参是否须连续;仓内所有调用方在返回后立即释放本地引用(MMRdmaExporter.cc:98groups 是局部变量,rdma/backend.py:91embtry_transfer 局部)。同样地 read()(:69)返回的 tensors 是跨 descriptor 拉平数组,顺序契约未声明,而 readAllSlots 只校验 result.tensors.size() != roles->size()(:216),assembleMMRdmaOutput 随后按遇到顺序 torch::cat(:52)。某 provider 多 QP 并行后按完成顺序追加时,数量与 split_total 校验都会通过,embedding 行序被静默置换。
  • [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue 熔断器以 endpoint 为 key,代理模式下单个异常 worker 会关闭整个代理的 RDMA
    RdmaCircuitBreakercontext.endpoint 为进程级静态表的 key(:38,实现见 MMRdmaReader.cc:70-98)。但在 proxy 部署下 LLM 只认识代理地址:vit_proxy_server.py:501 才把请求分发到具体 worker,_transport_router 也只按 handle 记录 worker。因此某一个 worker 的 RDMA 链路异常累计 3 次后,会对整个代理开启 30s 熔断,把其余健康 worker 的 RDMA 一并关掉;恢复端仅靠 recordSuccess 清零,在少量坏 worker 存在时会反复开合。同时 reportCircuitStatetarget tag 也只有代理地址,无法定位到具体 worker。
  • [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue 熔断器用例依赖进程级静态表与 30 秒时间窗,不可重复执行且恢复路径未覆盖
    RdmaCircuitBreaker 的计数表是函数局部 static(MMRdmaReader.cc:70-73),kFailuresToOpen=3kOpenSeconds=30open_until 直读 steady_clock 且无可注入时钟。uniqueEndpoint(name) 实际只返回 "127.0.0.1:" + name(:23-25),并不跨执行唯一:同进程重复执行(--gtest_repeat)时电路已开,循环体内的 EXPECT_TRUE(advertised_rdma[0])(:284)会失败;反之若循环到 after.fetch(:289)之间停顿超过 30s,EXPECT_FALSE(:294)又会失败。同时只覆盖「连续失败后停止 advertise」一个方向,recordSuccess 复位与开窗过期后重新放量两条迁移均无断言。
  • [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue 熔断器用例依赖进程级静态表与 30 秒时间窗,不可重复执行且恢复路径未覆盖
    RdmaCircuitBreaker 的计数表是函数局部 static(MMRdmaReader.cc:70-73),kFailuresToOpen=3kOpenSeconds=30open_until 直读 steady_clock 且无可注入时钟。uniqueEndpoint(name) 实际只返回 "127.0.0.1:" + name(:23-25),并不跨执行唯一:同进程重复执行(--gtest_repeat)时电路已开,循环体内的 EXPECT_TRUE(advertised_rdma[0])(:284)会失败;反之若循环到 after.fetch(:289)之间停顿超过 30s,EXPECT_FALSE(:294)又会失败。同时只覆盖「连续失败后停止 advertise」一个方向,recordSuccess 复位与开窗过期后重新放量两条迁移均无断言。

RTP-LLM Checklist

  • [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue pybind 导出的 C++ VitConfig 未同步 output_transport,pickle 静默丢字段
    VitConfig 新增成员 MMTransportConfig output_transport(:369),但该结构体也是 pybind 导出类型:pybind/ConfigInit.cc:2015-2030py::class_<VitConfig> 只有 .def_readwrite("vit_separation", ...)py::pickle getter 为 py::make_tuple(self.vit_separation)、setter 校验 t.size() != 1,因此 Python 侧既读写不到 output_transport,pickle 往返也会把整棵传输配置重置为默认;MMTransportConfig/MMControlConfig/RdmaConfig 三个新结构体均未注册 pybind 类型。之所以只判 P3:全仓 Python 的 VitConfig 均来自 rtp_llm.config.py_config_modules(该类含 output_transport),`rtp
  • [I] 代码质量 — 同一功能用统一工具函数 → issue 超时常量、RDMA 默认值与 mode 字面量在四到五处重复,仅靠注释维持一致
    同一组语义常量(worker 预算 120s、客户端余量 5s、两者之和 125s)有 5 份独立字面量:ConfigModules.h:362,364MMRemoteOutputTransport.h:20-21kDefaultVitRpcTimeoutMs/kVitRpcTimeoutMarginMsVitConfigExtract.h:15 硬编码 120 * 1000py_config_modules.py:270DEFAULT_MM_TIMEOUT_MS、proxy 侧 5.0s。RDMA 默认值另有三份(py_config_modules.py:273-281vit_group_args.py 的 argparse default=ConfigModules.h:336-348)。合法 mode 集合同样双份维护(MMTransportMode.h:8-9MM_TRANSPORT_MODES),且在已有具名常量前提下 ConfigModules.h:358 与 `RemoteMultim

Python Static-First Checklist

  • [P.A] 静态结构与类型纪律 — 字符串分发用 Enum/Literal → issue 超时常量、RDMA 默认值与 mode 字面量在四到五处重复,仅靠注释维持一致
    同一组语义常量(worker 预算 120s、客户端余量 5s、两者之和 125s)有 5 份独立字面量:ConfigModules.h:362,364MMRemoteOutputTransport.h:20-21kDefaultVitRpcTimeoutMs/kVitRpcTimeoutMarginMsVitConfigExtract.h:15 硬编码 120 * 1000py_config_modules.py:270DEFAULT_MM_TIMEOUT_MS、proxy 侧 5.0s。RDMA 默认值另有三份(py_config_modules.py:273-281vit_group_args.py 的 argparse default=ConfigModules.h:336-348)。合法 mode 集合同样双份维护(MMTransportMode.h:8-9MM_TRANSPORT_MODES),且在已有具名常量前提下 ConfigModules.h:358 与 `RemoteMultim
  • [P.A] 静态结构与类型纪律 — 禁止 getattr/setattr literal 访问 → issue 用 getattr 字面量探测 available,把符号缺失与 provider 不可用折叠为同一路径
    available = getattr(MMRdmaExporter, "available", None),随后 if available is None or not available(): return None。需要该兜底是因为降级替身 DisabledMMRdmaExporter(ops/init.py:308-317)只实现了 enabled(),其基类 EmptyClass 没有 __getattr__。结果是「pybind 绑定缺失/改名」与「provider 明确报告不可用」两种语义共用同一条 INFO 日志:若 def_static("available", ...)(MMRdmaExporter.cc:166)被改名或漏注册,RDMA 会在所有环境静默失效,只留一行 info。该分支日志文案「skip GPU-NIC affinity」(:46-48)也与实际语义无关——亲和性由 load_gpu_nic_affinity 负责且此处并未跳过。
  • [P.A] 静态结构与类型纪律 — 禁止 hasattr 做控制流分支 → issue 用 getattr 字面量探测 available,把符号缺失与 provider 不可用折叠为同一路径
    available = getattr(MMRdmaExporter, "available", None),随后 if available is None or not available(): return None。需要该兜底是因为降级替身 DisabledMMRdmaExporter(ops/init.py:308-317)只实现了 enabled(),其基类 EmptyClass 没有 __getattr__。结果是「pybind 绑定缺失/改名」与「provider 明确报告不可用」两种语义共用同一条 INFO 日志:若 def_static("available", ...)(MMRdmaExporter.cc:166)被改名或漏注册,RDMA 会在所有环境静默失效,只留一行 info。该分支日志文案「skip GPU-NIC affinity」(:46-48)也与实际语义无关——亲和性由 load_gpu_nic_affinity 负责且此处并未跳过。
  • [P.C] 并发与异步 — async def 中禁止 blocking 调用 → issue RDMA provider 首次构造落在请求关键路径上,ViT 侧还持有 GIL
    ViT 侧 MMRdmaExporter(const RdmaConfig&) 在构造体内直接 createRdmaExport(rdma_config)(:31),全程未 py::gil_scoped_release(对比同文件 exportEmbedding:140 与 release:159 都显式释放);该构造由 rdma/backend.py:70_get_exporter() 在第一个真正广告 RDMA 的 gRPC handler 线程中懒执行,此时持有 GIL,RDMA 设备打开/建链的毫秒到秒级阻塞会冻结 ViT server 全部 handler 线程。LLM 侧对称问题在 MMRdmaReader::ensureReader(MMRdmaReader.cc:257):std::call_once 让并发首请求全部串在同一次构造上,且该耗时全额计入 DeadlineBudget
  • [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue libmm_rdma_exporter 的 pybind 契约在 C++ 与 Python 两端都被替换,序列化格式无真实覆盖
    跨语言契约包含:模块名须等于 copy_so 产出的 libmm_rdma_exporter.so、静态方法 available()、实例方法 enabled()/export_embedding(embedding,pos_id,extra_inputs)/release(handles)、以及每段返回字节可被 MMRdmaSlotPB 解析。但 C++ 测试借 -fno-access-control(test/BUILD:17)直接调用私有两参构造与私有 exportSlots,绕过真正对外的 exportEmbedding(含 GIL 释放与 SerializeAsString()py::bytes);Python 侧 mm_output_transport_test.py:62 把 exporter 整体换成 MagicMock(),返回值由测试自己拼装。pybind/BUILD 也没有以 //:mm_rdma_exporter 为 data 的 py_test。绑定名或参数名漂移会被 try_transfer

Strengths

  • 数据面协议错误被显式判为不可消费而非静默降级:MMRemoteOutputTransport.cc:88-95 对未 advertise 的匹配结果先 discard() 归还远端 slot 再返回 MM_PROCESS_ERROR,避免 RDMA 回执被 terminal 解成看似合法的空 MultimodalOutputgrpc 模式下 reader 仍挂在链上专为识别并释放非法回执,并有专门单测固化。
  • 降级是「一次有界重试」而非候选循环:degradeToTerminal 先对所有 reader withdraw() 再重发,并拒绝仍携带数据面回执的回退响应;单测以 {"request","read","release","request","terminal"} 精确断言只多花一次 ViT forward。
  • 接口收窄到类型层面:MMTerminalReceiptReader::consumefinal 包住 consumeTerminal,兜底路径在类型上无法再请求重试,从根上排除降级死循环。
  • 租约生命周期用 RAII 收敛:SlotLease 析构默认同步 release、成功路径显式转异步;RdmaExport::createBatch 在任一分组失败时回滚此前全部 lease 并返回空(RdmaTransport.cc:52-62),Python _roll_back 同向兜底,两端都不把 GC 当首选。
  • MultimodalPbConverter::inlineOutputFromPb 把原 RTP_LLM_CHECK_WITH_INFO 改为返回 ErrorInfo 并 try/catch 兜住 torch 异常,畸形远端响应不再在 FT_CORE_DUMP_ON_EXCEPTION 下 abort 进程,同时新增 split_totaldim()==0、pos/extra 数量三项一致性校验。
  • 开源可构建性与回滚路径完整:arch_select.bzl:59-65 落到 rdma_transport_no_implhasRdmaImplementation() 为假时 advertise() 自然不广告;libmm_rdma_exporter 独立成 so 且缺库时降级为 DisabledMMRdmaExporterenabled() 恒 False)而非通用 EmptyClass--mm_transport_mode grpc 在 LLM/ViT 任一侧单独设置都能安全回滚。
  • ViT proxy 的 lease 路由 fail-closed:同一 handle 被两个 worker 声明即毒化路由并上报 rdma_handle_collision,TTL 由 slot_gc_timeout_ms + 5s 推导,覆盖 collision/过期/deadline 耗尽/单 worker 失败四类用例。
  • LocalRpcServer::init 把多模态处理器种类判定前移到引擎构造之前并返回 FAILED_PRECONDITION,配置错误不再等模型加载完才暴露,错误串直接指向 Python 侧决策函数。
  • 测试对 mock 边界有克制:Python 侧 _tensors_look_cuda() 只伪造 is_cuda 谓词,torch.cat/.contiguous 与由此派生的 split_size 全部真实执行;C++ 侧只 fake 控制面与 provider,RecordingExport 真实回放 offset/align/dtype 布局做字节级往返校验。
  • 依赖切分有意图且写进注释:multimodal_types/multimodal_pb_converter 打断 multimodal_processor ↔ model_rpc_server 依赖环;mm_processor_config_test 刻意依赖瘦目标从而不链 torch、无需 GPU 节点。
  • smoke 框架新增 assert_log_patterns,明确解决「golden 相同无法区分快路径与静默回退」的盲区,方向正确。

roles->push_back(slot.roles(i));
}
}
const int64_t remaining = context.budget.remainingMs();

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] RDMA 读取占满整个剩余 deadline,read_timeout_ms 在调用点未生效且预算耗尽被误记为端点故障

readAllSlots 直接把请求剩余预算当作单次读取超时:const int64_t remaining = context.budget.remainingMs(); ... reader_->read(descriptors, remaining);(:207-211),从未与 RdmaConfig::read_timeout_ms 取 min,而 ConfigModules.h:341 注释与 --mm_rdma_read_timeout_ms 的 help 都写明「实际预算取 min(请求剩余时间, 该值)」。若 provider 不自行封顶,读取挂起会耗尽预算,degradeToTerminal 随后在 context.budget.exhausted() 处直接返回 MM_PROCESS_ERROR(MMRemoteOutputTransport.cc:118-120),本可 inline 成功的请求变成请求级失败。另 remaining<=0 返回 false 与真实读失败共用返回值,consume 无差别 `noteFa...

建议: 调用点改为 std::min(rdma_config_->read_timeout_ms, remaining - kFallbackReserveMs),显式为降级轮次预留最小预算,并让 eager 构造路径也持有 RdmaConfig(或让 MMRdmaReader 始终持有而非 optional)。同时把「预算耗尽」与「RDMA 读失败」拆成两种返回原因:前者只按 deadline 失败上报、不计入 RdmaCircuitBreaker,并给独立 metric reason(如 rdma_budget_exhausted)。若确定该上限由仓外 provider 依据 RdmaConfig 施加,请改掉 help 与头注释的措辞并在 readAllSlots 就近注明责任归属。

Checklist: [6.1] 回滚路径:风险行为存在运维回滚手段

Comment thread rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc Outdated
Comment thread rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc Outdated
public:
virtual ~RdmaExport() = default;
// Returns an empty descriptor (lease_id is empty) when export fails.
virtual RdmaDescriptor create(const std::vector<torch::Tensor>& tensors) = 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.

[P2] RdmaExport/RdmaRead 的所有权与拉平顺序契约未声明,读侧只校验数量不校验形状

create(const std::vector<torch::Tensor>&) 只注明「失败时 lease_id 为空」,未规定 provider 是拷入自有已注册槽还是就地注册调用方显存,也未说明入参是否须连续;仓内所有调用方在返回后立即释放本地引用(MMRdmaExporter.cc:98groups 是局部变量,rdma/backend.py:91embtry_transfer 局部)。同样地 read()(:69)返回的 tensors 是跨 descriptor 拉平数组,顺序契约未声明,而 readAllSlots 只校验 result.tensors.size() != roles->size()(:216),assembleMMRdmaOutput 随后按遇到顺序 torch::cat(:52)。某 provider 多 QP 并行后按完成顺序追加时,数量与 split_total 校验都会通过,embedding 行序被静默置换。

建议:RdmaTransport.h 显式写明:create()/createBatch() 是否必须拷入自有已注册槽、若为零拷贝则必须持有入参张量直到 release()、入参是否要求 is_contiguous()read() 返回顺序必须严格等于「descriptor 顺序 × descriptor 内 tensors 顺序」。同时在 readAllSlots 中把 manifest 的 TensorMeta.shape/nbytes/dtype 与返回张量逐项比对,不符即返回 false 走降级;并在 exportSlots 入口对输入加 is_contiguous() 断言或就地 .contiguous(),不把该前置条件只压在 Python 调用方。

Checklist: [6.1] LSP:子类/重写保持基类契约

Comment thread rtp_llm/cpp/config/ConfigModules.h Outdated
Comment thread rtp_llm/cpp/multimodal_processor/test/MMRemoteOutputTransportTest.cc Outdated
int64_t rpc_timeout_margin_ms = 5 * 1000;
};

struct VitConfig {

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] pybind 导出的 C++ VitConfig 未同步 output_transport,pickle 静默丢字段

VitConfig 新增成员 MMTransportConfig output_transport(:369),但该结构体也是 pybind 导出类型:pybind/ConfigInit.cc:2015-2030py::class_<VitConfig> 只有 .def_readwrite("vit_separation", ...)py::pickle getter 为 py::make_tuple(self.vit_separation)、setter 校验 t.size() != 1,因此 Python 侧既读写不到 output_transport,pickle 往返也会把整棵传输配置重置为默认;MMTransportConfig/MMControlConfig/RdmaConfig 三个新结构体均未注册 pybind 类型。之所以只判 P3:全仓 Python 的 VitConfig 均来自 rtp_llm.config.py_config_modules(该类含 output_transport),`...

建议: 若确认 ops.VitConfig 已无 Python 消费者,直接从 rtp_llm/ops/__init__.py 的导出列表移除该类型,避免留下契约不完整、pickle 有损的公开类型;若要保留,则为三个新结构体注册 pybind 类型、给 VitConfigdef_readwrite("output_transport", ...) 并把它纳入 py::pickle(同时放宽 setter 的 size 校验以兼容旧 payload)。

Checklist: [6.1] 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全;[I] 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理

Comment thread rtp_llm/cpp/model_rpc/proto/BUILD Outdated
return;
}
auto connection_status = pool_.getConnection(endpoint);
if (!connection_status.ok()) {

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] releaseNow 获取连接失败时静默返回,无日志与指标

request()pool_.getConnection 失败时会上报 kReasonConnectionError 并返回错误(:59-63),而 releaseNow 在 :144-146 if (!connection_status.ok()) { return; } 直接静默返回,既无日志也无指标。RPC 失败尚有 :156-160 的 WARNING,唯独连接获取失败这条路径完全不可见——此时 lease 只能等 encoder 侧 TTL GC 回收,现场无法从任何信号判断是否发生了大规模 release 丢失。

建议: 在该分支补 metrics_->reportRpcClientError(endpoint, kReasonConnectionError) 并按频率打 WARNING,使 release 通道的连接故障与 request() 保持同等可观测性。

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

Comment thread rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc Outdated
@ydshi0
ydshi0 force-pushed the feature/rdma_transport branch from 4227e27 to 665a81d Compare August 30, 2026 09:45
@ydshi0
ydshi0 force-pushed the feature/rdma_transport branch from 665a81d to aa2042d Compare August 30, 2026 10:27

@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 #1353

Status: LGTM

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

Reviewed: commit aa2042dc4ba9 · 2026-08-30 19:27 UTC+8

lgtm ready to ci

Non-blocking Suggestions

P2

  • read_timeout_ms 在调用点未生效,RDMA 读挂死吃光整个请求预算并使 inline 兜底失效 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:207
    • 建议:改为 std::min(remaining, rdma_config_->read_timeout_ms)(<=0 视为不限制),使 3s 默认单次上限真正生效并给降级留出预算;同时把「预算耗尽」与「读失败」分成两条返回路径,预算耗尽不调用 noteFailure、用独立 reason 上报,避免熔断器被上游延迟污染。补一条「读超时后成功走 inline」与「预算耗尽不增加熔断计数」的单测。若有意把该值交给 provider 自行处理,请修正 help 与注释。
  • mm_processor_ 为空时 LocalRpcServer 静默跳过多模态处理,与 PrefillRpcServer 错误语义不一致 @ rtp_llm/cpp/model_rpc/LocalRpcServer.cc:169
    • 建议:在 prepareInput 补一条显式分支:output->multimodal_inputs 非空而 mm_processor_ == nullptr 时返回 ErrorInfo(ErrorCode::MM_NOT_SUPPORTED_ERROR, ...) 并打 WARNING,与 PrefillRpcServer 对齐,把配置/路由错位暴露为请求级失败而不是最难排查的静默错误结果。
  • RDMA 描述符反序列化不校验 offset/nbytes 越界,把安全边界推给不在本仓的 provider @ rtp_llm/cpp/rdma_transport/RdmaTransport.cc:90
    • 建议:把校验前移到 fromProto,让所有 provider 自动继承:逐 tensor 检查 nbytes == prod(shape)*element_size(dtype)offset+nbytes <= payload_bytesoffsetkRdmaSlotAlign 对齐、shape 各维非负、tensors 非空、host 非空且 port != 0;任一不满足返回 false,readAllSlots 已有的失败分支会自然回落 inline bytes。补一条「拒绝越界 manifest」的单测。
  • RdmaRead::read 未声明返回顺序与所有权契约,读侧只校验数量不校验形状 @ rtp_llm/cpp/rdma_transport/RdmaTransport.h:69
    • 建议:在 RdmaRead::read 声明上方补契约注释:(1) 返回张量按入参 descriptor 顺序、descriptor 内按 manifest 顺序展平;(2) 返回张量拥有独立存储,生命周期不依赖 RdmaRead 实例、后续 read() 或远端 lease;(3) 失败时 tensors 必须为空。并把 readAllSlots 的校验从「仅数量」加强为逐项比对返回张量的 shape/dtype 与对应 TensorMeta,不匹配即降级,把静默损坏转成可观测 retry。
  • exportSlots 对非 contiguous 张量会静默导出错误数据 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaExporter.cc:35
    • 建议:在 exportSlots 入口对 embeddingpos_id、每个 extrais_contiguous() 检查:不连续时 WARNING 后返回 false(与其他失败分支一致回落 inline),或显式 .contiguous() 后再导出。把不变量固定在 C++ 边界比让各 Python 调用方自行记得可靠,并补一条非 contiguous 输入被拒绝的用例。
  • RDMA provider 懒构造缺异常保护,可让「RDMA 不破坏 inline 兜底」的不变量失效 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:257
    • 建议:在 call_once 的 lambda 内包 try/catch:捕获后 WARNING 并让 reader_ 保持 nullptr,退化为「无 provider」走 inline 兜底,同时让 once_flag 正常置位避免每请求重复构造;补一条「provider 构造抛异常 → advertise 返回 false 且不抛」的单测。
  • RDMA 与 inline 路径返回的 tensor device 契约不一致且无覆盖 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:36
    • 建议:明确并统一契约:若允许 device 驻留,请在 MultimodalOutput 定义处注明「device 由传输模式决定,消费方须自行 .to(device)」并补消费方接受 device 张量的测试;若下游要求 host,则对 extra_inputs 做与 pos_id 一致的处理。当前「只归一化一部分」最容易在换模型时踩中。
  • releaseAsync 队列饱和时在调用线程同步发 RPC,违背基类显式约定 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:126
    • 建议:在有 TTL GC 兜底的前提下,队列饱和时更适合「丢弃 + 计数指标 + 限频告警」,或把溢出交给独立溢出批次线程,让 releaseAsync 永不阻塞调用方;若确要保留同步兜底,请在基类注释中写明该例外与最坏延迟量级,并把 kMaxPendingReleaseHandlesrelease_timeout_ms 做成可配置项以便线上回滚。
  • 析构排空未按 endpoint 合批且无总预算,进程关停耗时不可控 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:49
    • 建议:把「把 deque 折叠为 endpoint→handles」抽成私有辅助函数供 releaseLoop 与析构共用,将 N 次串行 RPC 压缩为按 endpoint 数量计的少量 RPC;并给析构排空加总预算(复用 DeadlineBudget,如整体不超过一个 release_timeout_ms_),耗尽后 WARNING 说明剩余 lease 交由 TTL GC 回收并跳出。
  • GrpcMMControlClient 的并发、生命周期与 gRPC 错误分类全部无测试覆盖 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:27
    • 建议:把 gRPC 错误分类抽成可直接测试的自由函数(输入 grpc::Status → 输出 ErrorInfo),覆盖 UNAVAILABLE/DEADLINE_EXCEEDED/CANCELLED/RESOURCE_EXHAUSTED 与携带结构化错误消息四类;把「发送 release」抽成可注入 sender 接口,为队列计数增减、饱和回退、析构 drain、stopping_ 期间调用 releaseAsync 的竞态补 gtest,并在 transport/grpc/BUILD 增加 cc_test_wrapper 使这些不变量进入 CI。
  • RDMA exporter 初始化失败后永久闭锁,两条最常见降级无指标且每请求打 WARNING @ rtp_llm/multimodal/transport/rdma/backend.py:66
    • 建议:补 _report_error("rdma_init_error")_report_error("rdma_export_empty"),使 reason 维度能区分「对端未请求 RDMA」与「本机 RDMA 已失效」;把 :106-114 的逐请求 WARNING 降为 DEBUG 或首次打印一次;为初始化失败提供有界退避重试。若坚持一次性闭锁,请在日志中显式写明「RDMA disabled for the lifetime of this process」,并置终态 _disabled 标志让后续快速返回而不取锁。另 try_createavailable() 为 false 时打的是「skip GPU-NIC affinity」(:46-48),与实际行为无关,建议一并修正。
  • 描述符解析中途失败时回滚不完整,失败点之后的 lease 只能等 slot GC @ rtp_llm/multimodal/transport/rdma/backend.py:127
    • 建议:失败时不要立即中断收集:先对剩余 desc_bytes_list 做 best-effort 解析,汇总所有可取到的 lease_id 后一次性 release;或让 exportEmbedding 额外返回本批次 handle 列表,使 Python 侧回滚不依赖描述符能否解析成功。
  • 用 getattr 字面量探测 available,与 ops 侧 fallback 契约不一致且使 enabled() 成为死代码 @ rtp_llm/multimodal/transport/rdma/backend.py:44
    • 建议:给 DisabledMMRdmaExporter@staticmethod available() -> bool: return False,把 :44-45 改为 if not MMRdmaExporter.available(): return None,去掉字面量 getattr 探测;同时修正或删除该 stub 中关于 enabled() 的 docstring,让「模块缺失」与「provider 构造失败」两种降级各有唯一入口。
  • 可恢复的 RDMA 降级复用请求级服务端错误指标,污染错误 QPS @ rtp_llm/multimodal/transport/rdma/backend.py:181
    • 建议:为传输层降级单独引入一个指标(例如 VIT_OUTPUT_TRANSPORT_FALLBACK_QPS,保留现有 reason 维度),把 VIT_RPC_SERVER_ERROR_QPS_METRIC 留给真正返回错误给调用方的路径;若不便新增指标,至少加一个 recoverable=true 的 tag,让看板与告警可以过滤。
  • RdmaOutputBackend 的懒初始化与生命周期分支在 CI 中零覆盖 @ rtp_llm/multimodal/transport/rdma/backend.py:40
    • 建议:新增用例通过 patch rtp_llm.ops 上的 MMRdmaExporter 覆盖四种情形:available() 为 False 时 try_create 返回 None;构造抛异常后连续两次 try_transfer 都回落且只尝试构造一次;enabled() 为 False 时不缓存 exporter;close() 之后 _get_exporter() 返回 None、release() 不再下发 handle 且不抛错。
  • RDMA provider 首次构造落在请求关键路径上且 ViT 侧全程持有 GIL @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaExporter.cc:30
    • 建议:在 exportEmbedding/release 之外,也把 provider 构造放到 py::gil_scoped_release 作用域内(extractRdmaConfig 先在持 GIL 时完成,再释放 GIL 调用 createRdmaExport);或在 ViT worker 启动阶段(非请求路径)显式预热一次 exporter,让首请求不承担构造成本。请同时在注释中说明选择了哪种策略及原因。
  • 新增 libmm_rdma_exporter.so 的加载路径无任何测试兜底,失败被 BaseException 静默降级 @ rtp_llm/ops/__init__.py:339
    • 建议:新增一个不依赖 GPU 的轻量 py_test,data//:mm_rdma_exporter,断言 from rtp_llm.ops import MMRdmaExporter 得到的不是 DisabledMMRdmaExporterMMRdmaExporter.available 可调用(返回值不断言,以兼容无 provider 构建);并把该降级日志提升到 warning,使「RDMA 模块未加载」在生产日志中可见。建议顺带验证「同进程内可与 libth_transformer 共存」。
  • 导出器 .so 为序列化一个小消息静态链入整份 model_rpc proto/gRPC,且与 libth_transformer 存在描述符重复注册风险 @ rtp_llm/cpp/multimodal_processor/transport/rdma/BUILD:29
    • 建议:把 MMRdmaExporter.h:12 改为 include multimodal_output.pb.h,BUILD:29 依赖从 model_rpc_service_cc_proto 收窄为 //rtp_llm/cpp/model_rpc/proto:multimodal_output,既消除依赖过宽也移除潜在的描述符重复注册面;并在开发镜像实测「同进程先后 import 两个 .so」的行为,把结论写进根 BUILD 该 target 的注释。
  • FlexLB pom 的 java_package 注入不幂等,第二次非 clean 构建会因重复 option 失败 @ rtp_llm/flexlb/flexlb-grpc/pom.xml:120
    • 建议:把 :120-123 的 match 改为与 :116-119 同构的幂等写法;更推荐给两处 <copy>overwrite="true",让每次都从源文件重新拷贝,两条规则都天然幂等。建议 CI 补一次「同一 workspace 连续两次 mvn -pl rtp_llm/flexlb/flexlb-grpc compile」的验证。另 tensor_rdma.proto 未注入 java_package,其生成类会落在顶层包 rdma_transport,与另外两个 proto 刻意统一到 org.flexlb.engine.grpc 不一致,建议一并处理。
  • 超时/RDMA 默认值与 mode 字面量在四到五处重复,本 PR 新增的 mode 常量未被复用 @ rtp_llm/cpp/config/ConfigModules.h:358
    • 建议:config_modules 增加一条对零依赖 mm_transport_mode 的 dep 后复用 kMMTransportModeAuto/GrpcgrpcOnlyTransportConfig 改用 kMMTransportModeGrpc;把 125s = 120s + 5s 的推导收敛到单一来源(VitConfigExtract.h120*1000 抽为具名常量并与 MMRemoteOutputTransport.h 的两个常量统一);Python 侧让 py_config_modulesvit_group_argsdefault= 共用同一组模块级常量,并修正 :266 指向错误结构体的注释。
  • Python→C++ 配置抽取桥零测试,10 个属性名与 deadline 派生公式均未覆盖且未进启动日志 @ rtp_llm/cpp/config/VitConfigExtract.h:15
    • 建议:补一个不依赖 GPU 的 pybind 层测试(可参照 MMProcessorConfigTest.cc 的轻量做法):构造真实 py_config_modules.VitConfig() 后调用 extractVitConfig,逐字段断言与 Python 默认值一致且 default_rpc_timeout_ms == mm_timeout_ms + rpc_timeout_margin_ms,并覆盖 Nonemm_timeout_ms=0、非法 mode 三个边界;同时在 VitConfig::to_string() 补印这两个派生字段,并在 VitConfigExtract.h 注明必需属性清单。
  • 通用 RdmaConfig 置于引擎配置聚合头,使底层 rdma_transport 反向依赖高层配置模块 @ rtp_llm/cpp/config/ConfigModules.h:336
    • 建议:把 RdmaConfig 下沉到 rtp_llm/cpp/rdma_transport/(或新建零依赖的 rdma_config cc_library),ConfigModules.h 反向 include 该轻量头并在 MMTransportConfig 中持有它;这样 rdma_transport 不再依赖 config_modules,也让 provider 实现方只需一个小头文件即可对接。
  • Embedding 入口固定 125s deadline,--mm_timeout_ms 抬不动且与 LLM 入口语义分叉 @ rtp_llm/cpp/multimodal_processor/RemoteMultimodalProcessor.h:44
    • 建议:让 RtpEmbeddingOp::init 也复用 extractVitConfig 并把 vit_config.output_transport 传给四参构造(若要保持 embedding 路径固定 inline,可在传入前把 mode 覆写为 kMMTransportModeGrpc,从而只保留 deadline 的正确派生);或让 grpcOnlyTransportConfig() 接受调用方传入的 default_rpc_timeout_ms。同时把 "grpc" 换成常量,并在注释中写明该入口为何刻意不启用 RDMA。
  • lease_id 的全局唯一性是 proxy 路由的隐含前提,冲突后 release 会被持续毒化 @ rtp_llm/multimodal/transport/proxy_router.py:80
    • 建议:在 tensor_rdma.protolease_id 字段与 RdmaExport::create 声明处显式写明「必须在 ViT 集群内全局唯一(建议 host/port + 单调计数或 UUID)」作为 provider 验收条件;同时给 rdma_handle_collision 指标配告警,并在冲突率超阈值时记一条聚合 ERROR,让「所有 release 静默失效」能被及早发现,而不是只表现为 slot 内存缓慢上涨。
  • ViT proxy 跨 worker 重试会孤立已导出的 RDMA slot 且无针对性可观测信号 @ rtp_llm/server/vit_proxy_server.py:507
    • 建议:在 failover 分支中,对「本次转发的 request 带 support_rdma=True」的情形上报一个独立 reason(例如 rdma_slot_possibly_orphaned)并打 WARNING、携带 worker 地址,使该窗口在 kmonitor 上可量化;更彻底的做法是在 failover 前对上一个 worker 发一次 best-effort 回收。若接受现状,请在注释中写明「重试遗留的 slot 依赖 TTL GC」这一取舍。
  • 新增 smoke qwen3_vl_rdma 硬断言仅真实 provider 才有的日志标记,无可用性门控 @ rtp_llm/test/smoke/suites_h20_oss.bzl:569
    • 建议:给该用例加 provider 可用性门控:或按构建配置/select() 只在链接真实 provider 时注册,或把断言放宽为「命中 [MM-RDMA-HIT] 或命中明确的 inline 回落标记」二者之一,并另加一条 MM_TRANSPORT_MODE=grpc 用例守住强制 inline 语义。同时在用例注释中写明它依赖真实 RDMA provider 与可用 NIC。
  • assembleMMRdmaOutput 五条拒绝条件与 EXTRA_INPUT 回传路径无断言 @ rtp_llm/cpp/multimodal_processor/test/MMRdmaTransportTest.cc:232
    • 建议:补一组表驱动负向用例覆盖上述 5 条拒绝条件,并把 round-trip 扩展为带 extra_inputs 的版本(数量匹配时断言正确填充、不匹配时返回 false);这几条都是纯 CPU 断言,可直接放进现有 binary。另在 MMRemoteOutputTransportTest 增加一个 manifest 自相矛盾的 receipt 用例,验证它同样走「一次有界降级」。第 234-240 行两次调用复用同一个 output 对象,建议每次断言前重建。
  • MMRdmaExporter 的公开入口与无 provider 降级契约全部被测试绕过 @ rtp_llm/cpp/multimodal_processor/test/MMRdmaTransportTest.cc:149
    • 建议:补两条用例:(1) 用公开构造 MMRdmaExporter(RdmaConfig{}) 断言 enabled() == false 且导出返回 false,为「无 provider 构建安全降级为 inline」建立 C++ 守卫;(2) 用 RecordingExport 驱动 exportEmbedding,断言成功时 py::bytes 数量等于 slot 数、反序列化回 MMRdmaSlotPB 后 roles/descriptor 与 exportSlots 一致,fail_at=0 时返回空 vector。若 GIL 在 gtest 中不便处理,可改为在 Python 侧对真实模块做一个最小 round-trip 替代全 mock。
  • arch_select.bzl 新增两项跨仓构建契约,内部 @arch_config 覆盖版本必须同步且所有平台强制构建该 .so @ arch_config/arch_select.bzl:59
    • 建议:在 PR 描述中显式列出需联动的内部仓改动(同步 copy_all_so 的新 .sordma_transport_deps() 定义;内部若有真实 provider 应在该处指向真实实现而非 rdma_transport_no_impl),并在 CI 确认非 CUDA 平台 //:mm_rdma_exporter 可构建;若某平台不需要该 .socopy_all_so() 改用 select() 按平台裁剪。
  • load_gpu_nic_affinity 用 CWD 相对路径读取中间文件,多进程存在读写竞争且降级仅记 info @ rtp_llm/config/server_config_setup.py:589
    • 建议:用 tempfile.TemporaryDirectory() 作为 subprocess.run(..., cwd=tmp_dir) 的工作目录并按绝对路径读取,天然隔离并发与陈旧文件且自动清理;或让父进程在 spawn 之前完成一次探测、通过环境变量下发给所有子进程。同时把 JSON 解析失败与结构校验失败从 info 提升为 warning,并在消息中写明「将使用默认网卡,RDMA 带宽可能下降」。
  • ACCL_USE_NICS 的物理 index / local_rank 双键空间回退可能静默绑定错误网卡且日志误导 @ rtp_llm/config/server_config_setup.py:643
    • 建议:把两种键空间显式区分:命中物理 index 时按现状打印;走 local_rank 回退时改为 WARNING 并写明「亲和表键空间未确认,可能绑定到非本卡网卡」;或在回退前校验表的键集合是否与 CUDA_VISIBLE_DEVICES 的物理 index 集合一致,不一致则放弃派生而不是猜测。
  • 新增的 GPU-NIC 亲和推导逻辑无单测,而该模块已有测试文件 @ rtp_llm/config/server_config_setup.py:525
    • 建议:在既有 server_config_setup_test.py 中用 pytest.mark.parametrize 补齐用例,mock subprocess.run 返回构造好的 index,uuid CSV,至少覆盖:数字形式直接返回、UUID 前缀命中、UUID 未命中返回 None、local_rank 越界返回 None,以及 affinity 仅有 str(local_rank) 键时的回落路径(并断言此时日志能区分两种键空间)。

P3

  • RDMA 命中路径存在每请求一条的 INFO 日志,且该标记已被 smoke 断言绑定 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:176
    • 建议:改为「首次命中打印一次」(std::call_once 或原子标志),既满足 smoke 的存在性断言又消除逐请求噪声;命中率本身用 kmonitor 计数器暴露(例如在 reportCircuitState 旁加 hit 计数),比逐请求日志更适合线上观测。
  • releaseNow 获取连接失败时静默返回,且失败 RPC 计入正常延迟统计 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:144
    • 建议:在连接失败与 RPC 失败两条分支都调用 metrics_->reportRpcClientError(endpoint, ...)(reason 分别用 connection_error / release_failed),连接失败分支补一条带 endpoint 与 handle 数量的 WARNING,使 lease 泄漏可被 kmonitor 告警;同时把 reportRpcMetrics 移入 status.ok() 分支或为失败样本单独打点。
  • RdmaReadResult::lease_ids 为死字段,读侧本地资源释放归属不明确 @ rtp_llm/cpp/rdma_transport/RdmaTransport.h:53
    • 建议:删除该字段并同步更新测试 fake;若确有「读侧本地注册资源需按 lease 释放」的未来需求,改为在 RdmaRead 上定义显式方法或用 RAII 句柄承载,并在头文件写明归属与生命周期,而不是留一个无人消费的返回字段。
  • pybind 导出的 C++ VitConfig 未同步 output_transport,pickle 静默丢字段 @ rtp_llm/cpp/config/ConfigModules.h:367
    • 建议:同步暴露 output_transport(含 MMTransportConfig/MMControlConfig/RdmaConfig 的类注册)并纳入 pickle 元组,或在 VitConfig 定义处注明「pybind 仅导出 vit_separation,output_transport 不参与 pickle」;建议在既有 config_pickle_test 中加一条往返后 output_transport 保持不变的断言固化该约定。
  • model_rpc_server 的 hdrs glob 未排除 MultimodalPbConverter.h,lean target 边界只在 srcs 侧生效 @ rtp_llm/cpp/model_rpc/BUILD:70
    • 建议:把 MultimodalPbConverter.h 一并加入 hdrs glob 的 exclude 列表,让头文件与实现归属同一 target;或在 model_rpc_server.deps 中显式加上 :multimodal_pb_converter,与 :tensor_pb_convert 保持一致,使依赖不依赖传递路径。
  • 熔断器以 endpoint 为 key,代理模式下单个异常 worker 会关闭整个代理的 RDMA @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.h:38
    • 建议:短期在指标与日志中补充可区分 worker 的信息(例如让代理在收据里回填 worker 标识,LLM 侧作为熔断 key 的一部分或至少作为 metric tag);若暂不改协议,请在 RdmaCircuitBreaker 声明处注释说明「proxy 模式下 key 粒度为整个代理,单 worker 故障会放大」,让运维读到 rdma_circuit_open 时知道其真实含义。
  • 熔断器用例依赖进程级静态表与 30 秒时间窗,不可重复执行且恢复路径未覆盖 @ rtp_llm/cpp/multimodal_processor/test/MMRemoteOutputTransportTest.cc:275
    • 建议:给 RdmaCircuitBreaker 增加 test-only 的清空入口(如 static void resetForTest()),或把 breaker 改为可注入成员而非进程级 static,并在 SetUp() 调用;这样可去掉「endpoint 名字全局唯一」的隐式约定,也顺便覆盖「熔断到期后自动恢复」这条目前没有断言的路径。若暂不改生产代码,请在 BUILD 上标注该 test 不可 repeat 并把 30s 时间窗依赖写进注释。
  • proto genproto alias 适配在两个包重复实现,且新增的一份缺注释与 visibility @ rtp_llm/cpp/model_rpc/proto/BUILD:43
    • 建议:把这层命名适配收敛到 bazel/tf_proto.bzl(在 tf_proto_library_cc 内部自动生成 {name}_genproto alias 并置为 public),消除两处重复;若暂不改 macro,至少给该 alias 补上与 rdma_transport/proto 一致的注释与 visibility = ["//visibility:public"],并在 generate_grpc_protoproto_deps 上方注明「需与该 .proto 的 import 列表手工保持一致」。
  • 新增文件未通过 pre-commit 格式化 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:22
    • 建议:在提交前运行仓库的 pre-commit(clang-format / isort),把这些格式差异从功能提交中剥离,避免后续任何触碰这些文件的改动都带上无关的重排 diff。

Checklist Findings (23 fail / 51 total)

General Principles Checklist

  • [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue model_rpc_server 的 hdrs glob 未排除 MultimodalPbConverter.h,lean target 边界只在 srcs 侧生效
    srcs glob 已排除 MultimodalPbConverter.cc(:67)以打断 model_rpc_server ↔ multimodal_processor 依赖环,但 hdrs = glob(["*.h"], exclude=["RPCPool.h"])(:70-73)仍把 MultimodalPbConverter.h 计入 model_rpc_server,而其 deps 显式列了 :tensor_pb_convert(:79)却未列 :multimodal_pb_converter——与同类的 TensorPbConvert 处理方式不对称。目前无人从 model_rpc_server 内部 include 它,故不会立刻出错;但一旦有人这样 include,编译会通过、链接只能靠 //rtp_llm/cpp/multimodal_processor 这条传递依赖侥幸成立。
  • [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue pybind 导出的 C++ VitConfig 未同步 output_transport,pickle 静默丢字段
    VitConfig 新增了 output_transport 成员(:369),但其 pybind 注册(ConfigInit.cc:2015-2030)仍只 def_readwrite("vit_separation", ...)py::pickle 的 getter 也只 make_tuple(self.vit_separation)、setter 校验 t.size() != 1。任何经由 C++ VitConfig 的 pickle 往返(如跨 multiprocessing.spawn 传参)都会把整份传输配置静默重置为默认值(mode=auto、125s),而不是报错。当前主链路在 RtpLLMOp 侧从 Python 对象重新抽取,未依赖该 pickle,故线上暂不可达。
  • [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue 通用 RdmaConfig 置于引擎配置聚合头,使底层 rdma_transport 反向依赖高层配置模块
    RdmaConfig(:336-349)是与多模态无关的通用传输参数(bind_ip/port/qp_count/超时/slot 上限),却定义在聚合式引擎配置头中;RdmaTransport.h:10 因此 include 该头并让 rdma_transport/BUILD:14 依赖 //rtp_llm/cpp/config:config_modules——最底层的传输抽象反过来依赖整份引擎配置(内含 CacheStoreConfig、Speculative 等数十个无关结构),任何引擎配置字段变动都会触发传输层重编。本 PR 已为「只需两个模式常量」的场景刻意拆出零依赖的 mm_transport_mode 目标(cpp/config/BUILD:53-58),说明拆分成本很低。
  • [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue 熔断器以 endpoint 为 key,代理模式下单个异常 worker 会关闭整个代理的 RDMA
    RdmaCircuitBreaker::table() 以 LLM 侧看到的 endpoint 字符串为 key(:38)。在 ViT proxy 部署下 LLM 只认识代理这一个 endpoint,代理背后可能有 N 个 worker;若其中一个 worker 的 RDMA 异常(NIC 故障、slot 注册失败),失败会累积到同一个 key 上,kFailuresToOpen=3 次即把整个代理的 RDMA 关闭 kOpenSeconds=30,其余健康 worker 的 GPUDirect 能力被一并牺牲;reportCircuitStatetarget tag 同样只有代理地址,排障时看不到 worker 维度。
  • [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue arch_select.bzl 新增两项跨仓构建契约,内部 @arch_config 覆盖版本必须同步且所有平台强制构建该 .so
    本 PR 在 @arch_config 上新增两项契约:copy_all_so() 无条件追加 copy_so("@rtp_llm//:mm_rdma_exporter")(:11,rtp_llm/libs/BUILD:58 已硬引用生成的 libmm_rdma_exporter_so),以及全新函数 rdma_transport_deps()(:59-65,rtp_llm/cpp/rdma_transport/BUILD 已 load 并调用)。仓库通过 --override_repository@arch_config 整体替换为内部实现,若内部覆盖未同步这两处,Bazel 在加载阶段即失败且失败点与本改动看不出关联;该覆盖文件不在本 worktree 内,评审侧无法确认。copy_all_soselect(),意味着 cpu/arm/rocm/ppu 全部平台的 wheel 都必须能构建该 .so
  • [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue 熔断器用例依赖进程级静态表与 30 秒时间窗,不可重复执行且恢复路径未覆盖
    RdmaCircuitBreaker::table()/mutex() 是函数内 static(MMRdmaReader.cc:70-78),无 reset 接口;测试靠 uniqueEndpoint(name) 隔离,而该函数只是 "127.0.0.1:" + name(:23-25),对同一 case 的重复执行并不唯一。circuitBreakerStopsAdvertisingAfterRepeatedFailures127.0.0.1:circuit 上累积 3 次失败使熔断打开 30s;同进程第二轮执行(--gtest_repeat=2)时 advertise() 直接返回 false,第 283 行的 ASSERT_TRUE(h.fetch(endpoint).ok()) 会走「未 advertise 却收到 rdma receipt」的协议错误路径而失败。该 case 还隐含「3 次循环需在 30s 内完成」的时间窗依赖,熔断恢复路径也无断言。
  • [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue releaseNow 获取连接失败时静默返回,且失败 RPC 计入正常延迟统计
    releaseNowpool_.getConnection(endpoint) 失败时(:143-146)直接 return,既不打日志也不上报指标;对比同文件 request() 的 :60-63 会上报 kReasonConnectionError。编码侧持续不可达时所有 lease 都无法释放,只能靠 TTL GC 回收,运维侧看不到任何异常信号;RPC 本身失败(:156-160)虽打 WARNING 但同样无指标。另外 request() 的 reportRpcMetrics(:75)在 status.ok() 判断之前无条件执行,失败 RPC 的耗时与字节数会混入 rtp_llm_vit_rpc_client_rt_us 等正常统计。
  • [6.1] Quality — PR description 说明动机与设计 → issue arch_select.bzl 新增两项跨仓构建契约,内部 @arch_config 覆盖版本必须同步且所有平台强制构建该 .so
    本 PR 在 @arch_config 上新增两项契约:copy_all_so() 无条件追加 copy_so("@rtp_llm//:mm_rdma_exporter")(:11,rtp_llm/libs/BUILD:58 已硬引用生成的 libmm_rdma_exporter_so),以及全新函数 rdma_transport_deps()(:59-65,rtp_llm/cpp/rdma_transport/BUILD 已 load 并调用)。仓库通过 --override_repository@arch_config 整体替换为内部实现,若内部覆盖未同步这两处,Bazel 在加载阶段即失败且失败点与本改动看不出关联;该覆盖文件不在本 worktree 内,评审侧无法确认。copy_all_soselect(),意味着 cpu/arm/rocm/ppu 全部平台的 wheel 都必须能构建该 .so
  • [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue RDMA 命中路径存在每请求一条的 INFO 日志,且该标记已被 smoke 断言绑定
    [MM-RDMA-HIT] multimodal embedding read over rdma, %d slot(s) 位于 consume() 的成功返回路径,即每个 RDMA 命中的多模态请求都打一条 INFO;该文件其余 INFO 只在初始化路径(每进程一次)。成功路径的观测已由 reportCircuitState/reportRpcMetrics 覆盖,这条日志在高 QPS 多模态服务上属纯增量日志量,并会淹没同文件中真正需关注的 WARNING(read 失败、manifest 不一致)。注意它同时被 suites_h20_oss.bzl:581assert_log_patterns 依赖,直接降级为 DEBUG 会让该 smoke 失效。
  • [6.1] Quality — 逻辑变更未混入无关格式化 → issue 新增文件未通过 pre-commit 格式化
    MMGrpcTransport.cc:22-25 的常量块与 :193-196 的成员声明对齐不一致(kReasonReleaseQueueFullrelease_queue_ 等多出一列空格);MMRdmaReader.cc:10-13 连续 4 个空行,assembleMMRdmaOutput(:17-20)、MMRdmaReader::readAllSlots(:184-187)、createMMRdmaReader(:263-266)的续行参数缩进超出开括号位置,与 .clang-format(4 空格、120 列、参数对齐)不符;MMRdmaExporter.cc:35-38MMRdmaExporter.h:44 同样存在过度缩进/错位。仓库已配置 clang-format pre-commit hook,说明这些文件提交时未跑过。
  • [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue proto genproto alias 适配在两个包重复实现,且新增的一份缺注释与 visibility
    tf_proto_library_cc(protodeps=...) 内部引用 {name}_genproto,而规则实际产出 {name}_cc_genproto,于是本 PR 在两个包各写了一份 alias 适配。rdma_transport/proto/BUILD:13-18 有说明注释且所在包声明了 package(default_visibility = public)model_rpc/proto/BUILD:43-46 既无注释,所在包也没有 package() 声明,alias 保持 private。同包引用当前可用,但其他包一旦想 protodeps = [".../proto:multimodal_output"] 就会因 multimodal_output_genproto 不可见而失败,且报错指向一个开发者没写过的 target 名。同文件 :29-32 的 proto_deps 也需手工与 .proto 的 import 保持同步且不具备传递性。
  • [6.1] Software Engineering — ISP:调用方不依赖无关大接口 → issue 通用 RdmaConfig 置于引擎配置聚合头,使底层 rdma_transport 反向依赖高层配置模块
    RdmaConfig(:336-349)是与多模态无关的通用传输参数(bind_ip/port/qp_count/超时/slot 上限),却定义在聚合式引擎配置头中;RdmaTransport.h:10 因此 include 该头并让 rdma_transport/BUILD:14 依赖 //rtp_llm/cpp/config:config_modules——最底层的传输抽象反过来依赖整份引擎配置(内含 CacheStoreConfig、Speculative 等数十个无关结构),任何引擎配置字段变动都会触发传输层重编。本 PR 已为「只需两个模式常量」的场景刻意拆出零依赖的 mm_transport_mode 目标(cpp/config/BUILD:53-58),说明拆分成本很低。
  • [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue RdmaReadResult::lease_ids 为死字段,读侧本地资源释放归属不明确
    RdmaReadResult::lease_ids(:53)全仓唯一写入点是测试 fake(MMRemoteOutputTransportTest.cc:122),生产代码从不读取——MMRdmaReader 的归还路径完全走 handlesOf(receipt) 从收据取 handle(MMRdmaReader.cc:226-233)。因此该字段既是投机性 API,又让「读侧是否需要按 lease 释放本地资源」这一职责边界含糊:新 provider 实现者无法判断填不填、填了谁消费。
  • [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue releaseAsync 队列饱和时在调用线程同步发 RPC,违背基类显式约定
    MMRemoteOutputTransport.h:81 明确约定 releaseAsync「Success-path release must not delay the consumer. Implementations own all queued data.」,release() 上方也写明「Best-effort; encoder slot GC is the backstop.」。但 releaseAsyncpending_release_handles_ + handles.size() > kMaxPendingReleaseHandles(1024) 时置 send_sync = true,随后于 :126-128 在调用线程执行 releaseNow(..., release_timeout_ms_)release_timeout_ms 默认 1000ms(ConfigModules.h:353),即高负载或 ViT 侧变慢时,成功读取路径会被最多 1s 的同步 gRPC 阻塞在推理线程上。
  • [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue MMRdmaExporter 的公开入口与无 provider 降级契约全部被测试绕过
    makeAdapter()test_copts-fno-access-control 调用私有构造与私有 exportSlotsMMRdmaExporter.h:35-41),因此公开入口 exportEmbeddingMMRdmaExporter.cc:134-153gil_scoped_releaseSerializeAsString()py::bytes,失败返回空 vector)与 enabled() 都未被执行;Python 侧又把 export_embedding 整个 mock 掉,而 rdma/backend.py:106 正依赖「返回空列表表示失败」这一约定。同时开源构建下 createRdmaExport() 返回 nullptr、exportSlots 在 :39 早退——这是开源 CI 里唯一可达的生产路径,却无任何断言;reader 侧同类路径已由 Harness(with_transport=false) 覆盖,exporter 侧完全缺失。
  • [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue 熔断器用例依赖进程级静态表与 30 秒时间窗,不可重复执行且恢复路径未覆盖
    RdmaCircuitBreaker::table()/mutex() 是函数内 static(MMRdmaReader.cc:70-78),无 reset 接口;测试靠 uniqueEndpoint(name) 隔离,而该函数只是 "127.0.0.1:" + name(:23-25),对同一 case 的重复执行并不唯一。circuitBreakerStopsAdvertisingAfterRepeatedFailures127.0.0.1:circuit 上累积 3 次失败使熔断打开 30s;同进程第二轮执行(--gtest_repeat=2)时 advertise() 直接返回 false,第 283 行的 ASSERT_TRUE(h.fetch(endpoint).ok()) 会走「未 advertise 却收到 rdma receipt」的协议错误路径而失败。该 case 还隐含「3 次循环需在 30s 内完成」的时间窗依赖,熔断恢复路径也无断言。
  • [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue 新增的 GPU-NIC 亲和推导逻辑无单测,而该模块已有测试文件
    _physical_gpu_index_configure_gpu_nic_affinity 引入 8 条以上分支:CUDA_VISIBLE_DEVICES 未设置 / 纯数字 / GPU-UUID / local_rank 越界 / 非 GPU- 前缀 / nvidia-smi 调用失败 / UUID 未被报告 / 命中物理索引 / 回落 str(local_rank)。全仓检索这两个函数仅命中定义文件本身;而同模块已存在 rtp_llm/config/test/server_config_setup_test.py(16 个用例,无一涉及这两个函数),说明本 PR 未新增对应覆盖。GPU→NIC 映射选错会直接影响本 PR 新增 RDMA 链路的连通性与带宽。

RTP-LLM Checklist

  • [I] 代码质量 — 同一功能用统一工具函数 → issue proto genproto alias 适配在两个包重复实现,且新增的一份缺注释与 visibility
    tf_proto_library_cc(protodeps=...) 内部引用 {name}_genproto,而规则实际产出 {name}_cc_genproto,于是本 PR 在两个包各写了一份 alias 适配。rdma_transport/proto/BUILD:13-18 有说明注释且所在包声明了 package(default_visibility = public)model_rpc/proto/BUILD:43-46 既无注释,所在包也没有 package() 声明,alias 保持 private。同包引用当前可用,但其他包一旦想 protodeps = [".../proto:multimodal_output"] 就会因 multimodal_output_genproto 不可见而失败,且报错指向一个开发者没写过的 target 名。同文件 :29-32 的 proto_deps 也需手工与 .proto 的 import 保持同步且不具备传递性。

Python Static-First Checklist

  • [P.A] 静态结构与类型纪律 — 禁止 getattr/setattr literal 访问 → issue 用 getattr 字面量探测 available,与 ops 侧 fallback 契约不一致且使 enabled() 成为死代码
    :44-45 available = getattr(MMRdmaExporter, "available", None) 靠「属性是否存在」做控制流分支,用于兼容 ops/__init__.py:341 注入的 DisabledMMRdmaExporter(该类只定义 enabled(),基类 EmptyClass__getattr__,故 getattr 必然为 None)。但该 stub 的 docstring 写「The ViT server asks enabled() right after construction」,而实际顺序是 try_create 先探 available 并直接返回 None(factory.py:23 是唯一构造入口),stub 永不会被构造——DisabledMMRdmaExporter.enabled() 是死代码。一旦 pybind 侧重命名或移除 available(现为 def_static),探测会静默返回 None,RDMA 被无声关闭而非报错。
  • [P.A] 静态结构与类型纪律 — 禁止 hasattr 做控制流分支 → issue 用 getattr 字面量探测 available,与 ops 侧 fallback 契约不一致且使 enabled() 成为死代码
    :44-45 available = getattr(MMRdmaExporter, "available", None) 靠「属性是否存在」做控制流分支,用于兼容 ops/__init__.py:341 注入的 DisabledMMRdmaExporter(该类只定义 enabled(),基类 EmptyClass__getattr__,故 getattr 必然为 None)。但该 stub 的 docstring 写「The ViT server asks enabled() right after construction」,而实际顺序是 try_create 先探 available 并直接返回 None(factory.py:23 是唯一构造入口),stub 永不会被构造——DisabledMMRdmaExporter.enabled() 是死代码。一旦 pybind 侧重命名或移除 available(现为 def_static),探测会静默返回 None,RDMA 被无声关闭而非报错。
  • [P.B] 错误处理 — 禁止 bare except 或静默吞异常 → issue 新增 libmm_rdma_exporter.so 的加载路径无任何测试兜底,失败被 BaseException 静默降级
    _load_rdma_opsexcept BaseException 捕获一切失败,把 MMRdmaExporter 替换成 DisabledMMRdmaExporter 并只打一行 logging.info(:339-344);__getattr__ 也只以 required=False 调用。而全仓无任何 py_test 把 //:mm_rdma_exporter 列入 datamultimodal/test/BUILD:101-109 的 deps 只有 //rtp_llm:testlib,且把 export_embedding 整个 mock 掉)。因此模块名与 PYBIND11_MODULE(libmm_rdma_exporter) 不匹配、registerMMRdmaExporter 符号缺失、whl_package_libs 漏打包等回归都会让 CI 保持全绿,而生产端静默退回 inline 字节。同目录 config_pickle_test(`data = ["//:rtp_compute_ops"
  • [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue MMRdmaExporter 的公开入口与无 provider 降级契约全部被测试绕过
    makeAdapter()test_copts-fno-access-control 调用私有构造与私有 exportSlotsMMRdmaExporter.h:35-41),因此公开入口 exportEmbeddingMMRdmaExporter.cc:134-153gil_scoped_releaseSerializeAsString()py::bytes,失败返回空 vector)与 enabled() 都未被执行;Python 侧又把 export_embedding 整个 mock 掉,而 rdma/backend.py:106 正依赖「返回空列表表示失败」这一约定。同时开源构建下 createRdmaExport() 返回 nullptr、exportSlots 在 :39 早退——这是开源 CI 里唯一可达的生产路径,却无任何断言;reader 侧同类路径已由 Harness(with_transport=false) 覆盖,exporter 侧完全缺失。
  • [P.G] 测试规范 — 数据驱动测试用 pytest.mark.parametrize → issue 新增的 GPU-NIC 亲和推导逻辑无单测,而该模块已有测试文件
    _physical_gpu_index_configure_gpu_nic_affinity 引入 8 条以上分支:CUDA_VISIBLE_DEVICES 未设置 / 纯数字 / GPU-UUID / local_rank 越界 / 非 GPU- 前缀 / nvidia-smi 调用失败 / UUID 未被报告 / 命中物理索引 / 回落 str(local_rank)。全仓检索这两个函数仅命中定义文件本身;而同模块已存在 rtp_llm/config/test/server_config_setup_test.py(16 个用例,无一涉及这两个函数),说明本 PR 未新增对应覆盖。GPU→NIC 映射选错会直接影响本 PR 新增 RDMA 链路的连通性与带宽。

Strengths

  • 降级语义被显式建模且有界:ConsumeResult 三态配合 degradeToTerminal(先 withdraw 全部候选 → 检查 budget → 只再发一次请求)保证「最多一次额外 ViT forward」;MMTerminalReceiptReader::consume 声明为 final 并把 API 收窄为 consumeTerminal,从类型上杜绝兜底路径再次触发候选轮询。
  • 「未广告的数据面收据」被当作协议错误而非降级(MMRemoteOutputTransport.cc:88-95)并同步 discard() 归还远端 slot;理由充分——RDMA 收据的 inline 字段为空,降级会被 terminal 解成「看似合法实则错误」的输出,属静默数据损坏,必须响亮失败,且有单测固化。
  • 开源默认路径安全:RdmaTransportNoImpl 使 createRdmaRead/Export 返回 nullptr → advertise() 为 false、available() 为 false,mode=auto 在无 provider 构建下自动等价于旧的 inline bytes 行为,无需任何配置迁移。
  • 资源生命周期用 RAII 收口:SlotLease 析构默认同步归还、成功路径 releaseAsync()RdmaExport::createBatch 在任一分组失败时回滚已创建的全部 lease,并有 rollsBackEarlierLeasesOnCreateFailure 单测。
  • 配置贯通经逐项核对无漂移:py_config_modules(250/3000/8/60000/1GiB/1000/auto)与 ConfigModules.hvit_group_args.pydefault= 三处完全一致;bind_to 直接绑到 vit_config.output_transport.{control,rdma} 嵌套对象,与 extractRdmaConfig 的属性名一一对应。
  • proto 拆分保持兼容:model_rpc_service_py_proto 补齐 depspy_proto.bzl 新增 proto_depsprotoc -I. 在沙箱可解析;已确认 FlexLB Java 侧无源码引用被迁移的消息,wire 与编译均兼容。
  • 多模态 ingress 归属被收敛为单一决策表(resolveMMProcessorKindmm_ingress.py 双向注释互指,错误串直接点名对端函数),且 FAILED_PRECONDITION 判断置于 NormalEngine 构造之前,配置错误无需先付模型加载代价。
  • 把内联在 RemoteMultimodalProcessor 的 gRPC 解析抽为 MultimodalPbConverter,并把 RTP_LLM_CHECK 换成 ErrorInfo + try/catch,避免畸形远端数据在 FT_CORE_DUMP_ON_EXCEPTION 下 abort 进程,且补了单测。
  • 依赖环治理干净且自带说明:multimodal_pb_convertermodel_rpc_server 的 srcs glob 中 exclude 对应 .cc,使 multimodal_processor 可反向依赖而不成环,也避免同一 .cc 被两个 target 重复编译。
  • 测试把假边界压到最小:MMRemoteOutputTransportTest.cc:238{"request","read","release","request","terminal"} 完整调用序列同时锁定「一次有界降级」与「归还先于重试」;realInlineTerminalDecodesAndRejectsInconsistentResponses 用真实 terminal 覆盖畸形响应返回 error 而非 abort;Python 侧 _tensors_look_cuda 只伪造 is_cuda 谓词而保留真实 torch.cat/.contiguous

roles->push_back(slot.roles(i));
}
}
const int64_t remaining = context.budget.remainingMs();

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] read_timeout_ms 在调用点未生效,RDMA 读挂死吃光整个请求预算并使 inline 兜底失效

vit_group_args.py:333 承诺「实际预算取 min(请求剩余时间, 该值)」,ConfigModules.h:341 注释亦写「Per-read limit, capped by the remaining request deadline」。但 readAllSlots 只取 context.budget.remainingMs() 并整份传给 reader_->read(descriptors, remaining);全仓检索 read_timeout_ms 仅命中配置抽取、结构体、to_string 与 argparse 绑定,无任何调用点消费。单次读因此可占满 125s 预算,随后 degradeToTerminalMMRemoteOutputTransport.cc:118)因 budget.exhausted() 直接返回「fallback exhausted request deadline」。另 remaining <= 0 早退(:208)会走到 noteFailure,本地预算耗尽被计入端点熔...

建议: 改为 std::min(remaining, rdma_config_->read_timeout_ms)(<=0 视为不限制),使 3s 默认单次上限真正生效并给降级留出预算;同时把「预算耗尽」与「读失败」分成两条返回路径,预算耗尽不调用 noteFailure、用独立 reason 上报,避免熔断器被上游延迟污染。补一条「读超时后成功走 inline」与「预算耗尽不增加熔断计数」的单测。若有意把该值交给 provider 自行处理,请修正 help 与注释。

metrics_reporter_));
}

return grpc::Status::OK;

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/LocalRpcServer.cc:169(不在 diff 展示范围内,就近挂载)

[P2] mm_processor_ 为空时 LocalRpcServer 静默跳过多模态处理,与 PrefillRpcServer 错误语义不一致

改动前只要 is_multimodal 为真,mm_processor_ 必被赋值(LOCAL/REMOTE 二选一)。改动后 resolveMMProcessorKindMMProcessorConfig.h:64)在 !ownsMultimodalIngress(role_type, tp_rank) 时返回 NONE,即 tp_rank>0 与 DECODE/VIT/FRONTEND 角色下 mm_processor_ 为 nullptr,为空的组合集合严格变大。而 prepareInput 的条件是 mm_processor_ != nullptr && output->multimodal_inputs,为空时直接跳过特征替换并返回 OK,模型带着未解析占位符继续推理。同仓 PrefillRpcServer.cc:286-293 对同一情形返回 MM_NOT_SUPPORTED_ERRORsetRetryable(false)

建议:prepareInput 补一条显式分支:output->multimodal_inputs 非空而 mm_processor_ == nullptr 时返回 ErrorInfo(ErrorCode::MM_NOT_SUPPORTED_ERROR, ...) 并打 WARNING,与 PrefillRpcServer 对齐,把配置/路由错位暴露为请求级失败而不是最难排查的静默错误结果。

Comment thread rtp_llm/cpp/rdma_transport/RdmaTransport.cc Outdated
class RdmaRead {
public:
virtual ~RdmaRead() = default;
virtual RdmaReadResult read(const std::vector<RdmaDescriptor>& descriptors, int64_t timeout_ms = 0) = 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.

[P2] RdmaRead::read 未声明返回顺序与所有权契约,读侧只校验数量不校验形状

read(descriptors, timeout_ms) 声明处无任何契约注释,但上层有两个强假设:一是 result.tensors 必须严格按「descriptor 顺序 × descriptor 内 manifest 顺序」展平,因为 readAllSlots 把 roles 同样展平后按位置一一对应、assembleMMRdmaOutputtorch::cat 重组;而校验只有 result.tensors.size() != roles->size()MMRdmaReader.cc:216),乱序不可发现——embedding 被 max_slot_bytes 切成多个全 EMBEDDING 分片时乱序仍满足 split_total == embedding.size(0),结果是行被静默打乱。二是返回张量必须自持存储,因为 consume 成功后立即 lease.releaseAsync()

建议:RdmaRead::read 声明上方补契约注释:(1) 返回张量按入参 descriptor 顺序、descriptor 内按 manifest 顺序展平;(2) 返回张量拥有独立存储,生命周期不依赖 RdmaRead 实例、后续 read() 或远端 lease;(3) 失败时 tensors 必须为空。并把 readAllSlots 的校验从「仅数量」加强为逐项比对返回张量的 shape/dtype 与对应 TensorMeta,不匹配即降级,把静默损坏转成可观测 retry。

Comment thread rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaExporter.cc Outdated
"MultimodalPbConverter.cc",
],
),
hdrs = glob(

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_server 的 hdrs glob 未排除 MultimodalPbConverter.h,lean target 边界只在 srcs 侧生效

srcs glob 已排除 MultimodalPbConverter.cc(:67)以打断 model_rpc_server ↔ multimodal_processor 依赖环,但 hdrs = glob(["*.h"], exclude=["RPCPool.h"])(:70-73)仍把 MultimodalPbConverter.h 计入 model_rpc_server,而其 deps 显式列了 :tensor_pb_convert(:79)却未列 :multimodal_pb_converter——与同类的 TensorPbConvert 处理方式不对称。目前无人从 model_rpc_server 内部 include 它,故不会立刻出错;但一旦有人这样 include,编译会通过、链接只能靠 //rtp_llm/cpp/multimodal_processor 这条传递依赖侥幸成立。

建议:MultimodalPbConverter.h 一并加入 hdrs glob 的 exclude 列表,让头文件与实现归属同一 target;或在 model_rpc_server.deps 中显式加上 :multimodal_pb_converter,与 :tensor_pb_convert 保持一致,使依赖不依赖传递路径。

Checklist: [6.1] 依赖方向:无循环依赖/跨层惊喜

Comment thread rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.h Outdated
Comment thread rtp_llm/cpp/multimodal_processor/test/MMRemoteOutputTransportTest.cc Outdated
Comment thread rtp_llm/cpp/model_rpc/proto/BUILD Outdated
Comment thread rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc Outdated
@ydshi0
ydshi0 force-pushed the feature/rdma_transport branch from aa2042d to 5d6a586 Compare August 30, 2026 19:43
loomts

This comment was marked as off-topic.

@ydshi0
ydshi0 force-pushed the feature/rdma_transport branch from 5d6a586 to f0f2411 Compare August 31, 2026 05:48

@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 #1353

Status: BLOCKING

Summary: P0/0 · P1/1 · P2/25 · P3/10

Reviewed: commit f0f2411a9bb5 · 2026-08-31 15:00 UTC+8

Blocking Issues

P1

  • FlexLB pom 的 java_package 注入不幂等,不带 clean 的第二次构建必然使 protoc 失败 @ rtp_llm/flexlb/flexlb-grpc/pom.xml:120
    • 建议:复用 :117 的幂等匹配写法(在 syntax 与插入点之间吞掉任意已存在的 option java_package 行),或给两个 <copy>overwrite="true" 让每次构建都从干净副本开始。两处 proto 的注入逻辑建议合并为一份可复用配置,避免再次分叉。

Non-blocking Suggestions

P2

  • RDMA 读通道持续故障时缺少熔断,每个多模请求被放大成两次 ViT 前向且不会自愈 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:97
    • 建议:在 MMRdmaReader 内加连续失败熔断:用 std::atomic 统计连续 retry,超阈值后在冷却期内让 advertise() 直接返回 false,consume 成功即复位,冷却结束再放量试探。把 kReasonRdmaReadError/kReasonRdmaManifestError 与熔断开合状态通过已注入但在本文件完全未使用的 metrics_ 上报,使「RDMA 已被永久降级」可告警。同时在发布说明中明确 --mm_transport_mode grpc 是该数据面的一键回滚开关。
  • rdma_transport 扩展点未声明内存所有权、返回顺序、条目数量与 lease_id 唯一性契约 @ rtp_llm/cpp/rdma_transport/RdmaTransport.h:59
    • 建议:在两个纯虚函数上写明契约:create 必须在返回前完成拷贝或自行持有输入张量 storage 引用直到对应 lease 被 release,须为每个入参张量输出一条同序 TensorMeta,且 lease_id 跨进程全局唯一;read 返回张量的数量与顺序必须严格对应入参 descriptor 展平顺序、须支持并发调用。更稳妥的是在 RdmaTransport.cc 提供按 lease_id 持有 std::vector<torch::Tensor> 的保管容器,把生命周期从 provider 约定上移为框架保证;并把测试里的 RecordingExport 提炼为 conformance 夹具,新 provider 接入必须通过。
  • RDMA 描述符反序列化不校验 offset/nbytes 越界,把内存安全边界推给不在本仓的 provider @ rtp_llm/cpp/rdma_transport/RdmaTransport.cc:90
    • 建议:在 fromProto 内补齐结构性校验并在任一不成立时返回 false:payload_bytes > 0;每条 tensor 满足 offset % kRdmaSlotAlign == 0nbytes > 0offset + nbytes <= payload_bytes(用无符号溢出安全的写法)、nbytes 等于 shape 元素数 × dtype 字节数;nic_keys 非空。同时补一组畸形 PB 用例,把「越界描述符必须被拒绝而非下传 provider」固定下来。
  • LLM→ViT 的 gRPC 截止时间完全由客户端可控的 mm_timeout_ms 决定,缺服务端上限钳制 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc:19
    • 建议:在 resolveRpcTimeoutMs 内用配置上限钳制请求侧值(例如 std::min(request_timeout, default_rpc_timeout_ms) 或新增 max_rpc_timeout_ms),并在被钳制时打一条限频 WARNING 便于定位异常客户端。同时补边界用例:全 0/负值取默认、多输入取最大正值 + margin、超过上限被钳制。
  • release 队列饱和时在持锁区上报指标与打日志,并把最长 1s 的同步 release 压到推理成功路径 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:105
    • 建议:把指标上报与日志移出 release_mutex_(锁内只置标志,出锁后再上报);日志改为按时间窗或计数聚合的限频输出。同步回退的超时应从请求剩余预算派生而非固定 release_timeout_ms_,或改为「入队替换最旧批次 + 上报丢弃量」,让导出侧 slot GC 兜底,而不把 ViT 的慢反压到推理延迟上。
  • 析构排空未按 endpoint 合批且无总预算,进程关停耗时不可控 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:49
    • 建议:复用 releaseLoop 已有的按 endpoint 聚批逻辑(抽成公共函数),先把 pending 合并成 unordered_map<endpoint, vector<handle>> 再逐 endpoint 发一次;同时给整个 drain 阶段设一个总预算(如 2×release_timeout_ms_),超时后放弃剩余批次并打印一条含剩余 handle 数的 WARNING,把兜底明确交给 encoder 侧 slot GC。
  • 唯一的生产 gRPC 控制面客户端零测试覆盖 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:27
    • 建议:把 releaseNow 抽成可注入的 sender/sink(构造参数传 std::function 或小接口),或把队列逻辑抽成不含 gRPC 的 ReleaseQueue,使线程与队列语义可在不起 gRPC server 的前提下测试。补齐五类用例:未满走异步并最终按 endpoint 合批一次、恰好/超过 1024 转同步并上报 release_queue_full、析构 drain 不丢句柄、stopping_releaseAsync 走同步分支、三类 gRPC 错误各自的返回码映射;并覆盖 handles 为空与 budget 耗尽两种输入。
  • exporter 惰性构造在请求线程内持 GIL 完成 RDMA 设备初始化 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaExporter.cc:165
    • 建议:把 provider 构造移到启动阶段(create_mm_output_transport 内):启动期慢不影响在线请求,也顺带消除首个 RDMA 请求的长尾延迟。若坚持惰性构造,则需在 C++ 侧拆出「先在持 GIL 时取完配置、再 gil_scoped_release 执行 provider 构造」的两段式入口(直接给 py::objectpy::initcall_guard 不安全,extractRdmaConfig 本身需要 GIL)。
  • exporter 初始化失败后进程内永久闭锁,两条最常见降级无指标且逐请求打 WARNING @ rtp_llm/multimodal/transport/rdma/backend.py:66
    • 建议:在 enabled() 为 false、构造抛异常、desc_bytes_list 为空三个分支各补一次含 rdma_config 关键字段的 WARNING 与一次降级指标(rdma_init_failed / rdma_export_empty);对稳定型降级原因做限频(首次 + 每 N 秒一次的聚合日志),明细计数交给下一条建议的降级指标承载;把 factory.py:28 的 "enabled" 改为 "deferred/registered",避免启动日志与真实运行状态冲突。
  • RDMA 降级复用 VIT_RPC_SERVER_ERROR_QPS_METRIC,成功请求污染错误率 @ rtp_llm/multimodal/transport/rdma/backend.py:188
    • 建议:新增独立的降级计数指标(例如 VIT_OUTPUT_TRANSPORT_FALLBACK_QPS,tag 带 reason),把 rdma_init_failed/rdma_export_empty/rdma_export_error/rdma_descriptor_invalid 以及 C++ 侧的 rdma_read_error/rdma_manifest_error 全部纳入;错误 QPS 指标仅保留真正导致请求失败的场景(如 rdma_release_error),让「降级率」与「错误率」在监控上可分离。
  • 用字面量 getattr 探测 MMRdmaExporter.available,掩盖 fallback 类的接口缺口 @ rtp_llm/multimodal/transport/rdma/backend.py:44
    • 建议:给 DisabledMMRdmaExporter 增加静态 available()(返回 False),调用处直接 MMRdmaExporter.available(),去掉 getattr 字面量探测;把「未注册」与「不可用」两种情况的日志区分开,并修正替身类 docstring。
  • 惰性构造状态机、close、transport 工厂选路与新 .so 导入均无测试覆盖 @ rtp_llm/multimodal/transport/rdma/backend.py:40
    • 建议:补三类用例:(1) try_createavailable() 为 False 与属性缺失(DisabledMMRdmaExporter)时均返回 None;(2) _get_exporter 在构造抛异常或 enabled() 为 False 时只尝试一次、后续请求直接降级,且 close() 之后不再构造;(3) create_mm_output_transport 三种 mode 的组装与 ValueErrorpytest.raisesmatch)。另补一个最小 py_test,以 //:mm_rdma_exporterdata 断言 from rtp_llm.ops import MMRdmaExporter 得到真实类且 available() 可调用;顺带把 ops/__init__.py:342 的加载失败日志改为携带 etraceback.format_exc(),与 _load_compute_ops:301 对齐。
  • 新增 .so 未沿用 th_transformer 的 librtp_compute_ops 链接约定 @ BUILD:187
    • 建议:请对构建产物执行 readelf -d 确认存在 NEEDED librtp_compute_ops.so;若不存在,与 th_transformer 保持一致,在 cc_binary(mm_rdma_exporter)srcs = [":rtp_compute_ops"]。同时在 PR 描述中记录「libmm_rdma_exporter.solibth_transformer.so 不会在同一进程共存」这一前提(ViT 进程只加载前者、LLM 进程只加载后者),否则重复的 protobuf descriptor 注册会在同进程加载时冲突。
  • arch_select.bzl 新增两项跨仓构建契约,覆盖版本必须同步且缺失时仅静默降级 @ arch_config/arch_select.bzl:11
    • 建议:在 PR 描述或迁移说明中显式标注「内部 bazel/arch_select.bzl 需同步新增 rdma_transport_deps() 并在 copy_all_so() 中加入 mm_rdma_exporter」,并配一条跨平台(PPU/ROCm/ARM)构建验证。若希望降低耦合,可让 whl_package_libs 对该 so 使用 select() 或由 arch_select.bzl 提供可为空的 filegroup。另建议在 ViT 启动摘要中输出最终生效的 transport 后端,让「打包缺 so 导致 RDMA 静默关闭」可被发现。
  • model_rpc_server 导出 MultimodalPbConverter.h 但未声明其实现目标,链接只靠一条传递路径 @ rtp_llm/cpp/model_rpc/BUILD:70
    • 建议:按 tensor_pb_convert 的既有模式,在 model_rpc_serverdeps 中显式加上 :multimodal_pb_converter,让头文件与实现的依赖关系不再依赖 multimodal_processor 这条传递路径;若该路径将来被调整,依赖 model_rpc_server 的消费方不会出现 undefined symbol。
  • 生产实际调用的 resolveAndLogMMProcessorKind 未被新测试覆盖,INVALID→fail-fast 语义无回归保护 @ rtp_llm/cpp/multimodal_processor/test/MMProcessorConfigTest.cc:10
    • 建议:在 mm_processor_config_test 中补 2~3 条对 resolveAndLogMMProcessorKind 的断言:INVALID 组合下 ok()==falseerror 非空(可用 HasSubstr);NONE 与 LOCAL/REMOTE 组合下 ok()==trueerror 为空。该函数为 header inline、无 torch 依赖,无需改动 BUILD 依赖即可加入现有 CPU-only 用例。
  • transport 工厂装配、mode 校验、RPC 超时解析与请求方向 PB 序列化四段新逻辑无任何用例调用 @ rtp_llm/cpp/multimodal_processor/test/BUILD:62
    • 建议:补三组用例:直接调用 resolveRpcTimeoutMs 覆盖「全部非正 → 默认值」「多输入取最大正值 + margin」「单输入 0/负值」;以 MMTransportConfig 驱动 createMMRemoteOutputTransport,断言 mode="grpc" 时不写 support_rdmaauto 时写入,并用 EXPECT_THROW 钉住非法 mode 的 fail-fast;在已链接 :multimodal_pb_convertermm_remote_output_transport_test 中补一个往返用例,构造 9 个字段均非默认值的 MultimodalInput,调用 inputsToPb 后逐字段断言 PB 内容与 crop_positions 顺序。
  • FakeControlClient 把同步 release 与 releaseAsync 记成同一条轨迹,成功路径的非阻塞归还契约无法断言 @ rtp_llm/cpp/multimodal_processor/test/MMRemoteOutputTransportTest.cc:93
    • 建议:给 fake 拆分两条独立轨迹,例如 released_syncreleased_async(或 log 分别写 "release_sync" / "release_async")。随后在成功路径用例断言 released_async.size()==1 && released_sync.empty(),在读失败与未协商数据面两个用例反向断言只发生同步 release。三条释放路径的延迟语义各自被钉住,且不需要改动被测代码。
  • 导出器测试绕过公共 pybind 边界,assembleMMRdmaOutput 的多数守卫仍无覆盖 @ rtp_llm/cpp/multimodal_processor/test/MMRdmaTransportTest.cc:232
    • 建议:补一条经 exportEmbedding 的用例,断言成功时返回的 bytes 可被 MMRdmaSlotPB 解析、失败时返回空 vector;再为 assembleMMRdmaOutput 剩余五条守卫各补一条最小反例。这些断言都是 CPU-only 纯逻辑,成本很低。
  • GPU-NIC 亲和键语义由 local rank 改为物理 GPU index,属线上行为翻转且无回滚开关 @ rtp_llm/config/server_config_setup.py:639
    • 建议:把 key 语义显式化而非靠优先级猜测:新增 env(如 ACCL_NIC_GPU_AFFINITY_KEY_SPACE=physical|local_rank)由部署方声明,并提供一个开关允许一键退回按 local_rank 取键;命中回退分支时改用 logging.warning 明示「按 local rank 语义解释」,并对「两种 key 同时存在且不一致」告警。在 release note 中列出该行为变更与空串语义变化,并把「affinity 是否生效、最终 NIC」写入启动摘要便于灰度对比。
  • npu_nic_affinity.json 按进程 CWD 相对路径读取,残留旧文件会被误读且失败仅记 info @ rtp_llm/config/server_config_setup.py:589
    • 建议:用 tempfile.TemporaryDirectory() 作为 subprocess.run(cwd=...),再按该目录下的绝对路径读取 JSON,退出时随上下文自动清理;open 显式指定 encoding="utf-8"。把这条失败日志提到 logging.warning,使「预期启用 affinity 但实际未启用」在生产日志中可检索;并把「导出原始 map」与「严格校验后派生 ACCL_USE_NICS」解耦,避免 value 形态变化导致整体拒绝。
  • 新增的 GPU-NIC 亲和性解析与回退逻辑无任何单元测试覆盖 @ rtp_llm/config/server_config_setup.py:525
    • 建议:补一组 pytest.mark.parametrize 用例,至少覆盖:CUDA_VISIBLE_DEVICES 未设置 / 恒等 / 偏移("4,5" 时 local_rank 1 应得 "5")/ UUID 命中与未命中(mock subprocess.run 返回 CSV)/ local_rank 越界 / 条目为空串;load_gpu_nic_affinityrun_affinity 不存在、JSON 非 object、value 为空串时返回 False 且不设置 env;_configure_gpu_nic_affinity 在已有 ACCL_USE_NICS 时不覆盖,以及物理 key 命中与 local rank 回退两条路径的最终取值。
  • online_eval proto 加载改为硬依赖 rtp_llm 包可导入,全部驱动脚本的 PYTHONPATH 不满足该前提 @ rtp_llm/flexlb/tools/online_eval/online_eval/proto_utils.py:116
    • 建议:在 _add_generated_package_paths 之前显式把 str(REPO_ROOT) 插入 sys.path(若不存在),或退回「把 out 加入 sys.path + 独立顶层包名」的自包含方案。建议同时补一条只依赖 PYTHONPATH=tools/online_eval 的最小用例,锁住「该工具不依赖仓根可导入」这一前提,避免 -m unittest(会把 CWD 入 path)掩盖回归。
  • MM transport 契约常量在 Python/C++ 多处各自硬编码,margin 仅 C++ 侧可配 @ rtp_llm/cpp/config/ConfigExtract.h:47
    • 建议:给这组跨语言常量建立单一来源:ConfigExtract.h:47 的兜底改为复用具名常量或 MMTransportConfig 已有默认值;rpc_timeout_margin_ms 要么纳入 MMControlConfig 从 Python 下发、要么让 vit_proxy_server.py 的 margin 从同一来源派生。另补一个跨语言一致性测试(断言 MM_TRANSPORT_MODESvalidateMMTransportMode 可接受集合相等),避免新增 mode 时只改一侧;并把最终生效的 deadline 写入启动摘要日志。
  • transport core 仍依赖 fat model_rpc_service.pb.h,弱化了 multimodal_output.proto 拆分的解耦目标 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.h:13
    • 建议:把两个头文件的 include 改为 rtp_llm/cpp/model_rpc/proto/multimodal_output.pb.h,BUILD 依赖换成 //rtp_llm/cpp/model_rpc/proto:multimodal_output_cc。需要 EmptyPB/MultimodalRpcServiceStub 的只有 grpc/MMGrpcTransport.cc,它已依赖 model_rpc_pool,无需改动。

P3

  • lease 归还在超时非正、连接失败、预算耗尽与 RPC 失败四条分支均无指标信号 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:140
    • 建议:在 releaseNow 的两个早退分支、release() 的预算耗尽分支与 RPC 失败分支上报独立 reason 的指标(如 release_skipped_timeout/release_connection_error/release_rpc_error),连接失败分支补一条含 endpoint 与 handle 数的 WARNING,使「lease 未归还」可与 encoder 侧 GC 计数对账。
  • pybind 导出的 C++ VitConfig 未同步 output_transport,pickle 会静默丢字段 @ rtp_llm/cpp/config/ConfigModules.h:356
    • 建议:要么补全 output_transport 的绑定与 pickle 状态(需同时绑定 MMTransportConfig/MMControlConfig/RdmaConfig),要么在 VitConfig 的 pickle lambda 旁加一行注释,明确 output_transport 只在 C++ 进程内生效、不参与 Python 侧序列化。
  • gRPC deadline 解析逻辑在 proxy_router 与 vit_proxy_server 重复实现,并用 hasattr 做控制流分支 @ rtp_llm/multimodal/transport/proxy_router.py:21
    • 建议:保留 vit_proxy_server.py 中已抽好的 _get_context_time_remaining_seconds + _resolve_context_deadline_seconds,下沉到已有的 rtp_llm/utils/grpc_util.py,两处统一引用。下沉时把 hasattr 改为 getattr(context, "time_remaining", None) 取到 callable 后再判断(或定义显式 Protocol);测试侧统一使用 FakeContext 而非裸 MagicMock
  • proto_utils 用 setattr 搬运 7 个消息名,仅为支撑单一调用点 @ rtp_llm/flexlb/tools/online_eval/online_eval/proto_utils.py:74
    • 建议:直接把 mock_engine.py 改为从 multimodal_output_pb2TensorPB(可让 ensure_proto_modules 额外返回该模块),删掉整段 setattr 兼容垫片与已失效的 module_name 参数;若确需过渡期,请在注释中写明移除条件与负责人。
  • ViT 侧未校验 roles 与 tensors 数量一致,zip 静默截断使字节指标偏小 @ rtp_llm/multimodal/transport/rdma/backend.py:147
    • 建议:在构造 receipt 前显式校验 len(slot.roles) == len(slot.rdma_descriptor.tensors),不一致时按 rdma_descriptor_invalid 走已有的回滚 + 降级分支并上报指标,把这条与 LLM 侧一致的不变量在产出端就地拦下,而不是让接收端用一次额外前向发现。
  • ParseFromString 返回值判定为误导性死代码 @ rtp_llm/multimodal/transport/rdma/backend.py:123
    • 建议:删除该返回值判定,直接 slot.ParseFromString(desc_bytes) 并依赖外层捕获;若希望保留显式意图,可改为单独 except DecodeError 分支并注释说明畸形 payload 以异常而非返回值呈现。
  • transport 指标标签词汇与配置 mode 词汇不一致 @ rtp_llm/multimodal/transport/grpc/backend.py:14
    • 建议:把 TRANSPORT_BYTES 的值统一为 "grpc"(复用 MM_TRANSPORT_MODE_GRPC 常量),或在配置项 help 文案中显式写明「grpc 通道在指标中记为 bytes」,避免两套词汇长期并存。
  • RDMA 成功路径每请求输出一条 INFO 级 [MM-RDMA-HIT] 日志 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:140
    • 建议:降为 RTP_LLM_LOG_DEBUG,并通过 metrics_ 上报带 transport tag 的命中次数/读取槽数/读取字节,使成功与降级两侧在监控上对齐(同时补上熔断项所缺的可观测面)。若灰度期确需日志,改为首次命中 + 每 N 秒一次的限频输出。
  • proto import 列表在三处重复维护,且 create_grpc_proto 的 data 列表已过期成为误导性同步点 @ rtp_llm/cpp/model_rpc/proto/BUILD:17
    • 建议:优先方案:在 py_proto.bzl 中让 generate_grpc_proto 直接消费 tf_proto_library_cc 生成的 *_proto_srcs filegroup(已递归收集 protodeps 的 proto 源文件),彻底消除手工 proto_deps 列表。最小化方案:删除 create_grpc_proto 中已失效的 data 属性,使 proto_deps 成为唯一同步点。
  • ConfigExtract.h 把 pybind11 转换下沉到 config 层,且 config_modules 反向依赖 rdma_transport @ rtp_llm/cpp/config/BUILD:44
    • 建议:考虑把 ConfigExtract.h 移到 rtp_llm/cpp/pybind/(或新建 config/pybind 子包),让 rtp_llm/cpp/config 保持无 pybind 依赖;RdmaConfig 若要被 ConfigModules.h 内嵌,可下沉到 rtp_llm/cpp/config(或中立的 common_config 包)由 rdma_transport 反向依赖。当前 rdma_config 是零依赖 header-only,实际构建代价可忽略,故仅作方向性建议。

Checklist Findings (20 fail / 51 total)

General Principles Checklist

  • [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue ConfigExtract.h 把 pybind11 转换下沉到 config 层,且 config_modules 反向依赖 rdma_transport
    新增的 config_extract target 头文件 ConfigExtract.h:3 直接 #include <pybind11/pybind11.h> 并做 py::object 属性抽取(:12-37),因此 BUILD 里必须挂上 //:rtp_compute_ops。这让原本纯 C++ 结构体定义的 rtp_llm/cpp/config 包新增了 Python 运行时耦合,而同类 py→C++ 抽取代码在仓库内惯例是放在 rtp_llm/cpp/pybind/。同时 config_modules(几乎被全仓引用的底层 target)在 config/BUILD:29 新增了对 //rtp_llm/cpp/rdma_transport:rdma_config 的依赖,形成 config 层反向依赖 transport 包的边。
  • [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue pybind 导出的 C++ VitConfig 未同步 output_transport,pickle 会静默丢字段
    VitConfig 新增成员 MMTransportConfig output_transport(:356),但 rtp_llm/cpp/pybind/ConfigInit.ccpy::class_<VitConfig> 仍只 def_readwrite("vit_separation")py::picklemake_tuple 也只序列化 vit_separation、反序列化时校验 t.size() != 1。当前实际链路(Python 侧传入 Python VitConfig,C++ VitConfig 只存在于 EngineInitParams)不会触发该问题,但一旦有 Python 代码构造或 pickle C++ VitConfigoutput_transport 会被静默重置为默认值(mode=auto、rdma port=0),排查成本高。
  • [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue ConfigExtract.h 把 pybind11 转换下沉到 config 层,且 config_modules 反向依赖 rdma_transport
    新增的 config_extract target 头文件 ConfigExtract.h:3 直接 #include <pybind11/pybind11.h> 并做 py::object 属性抽取(:12-37),因此 BUILD 里必须挂上 //:rtp_compute_ops。这让原本纯 C++ 结构体定义的 rtp_llm/cpp/config 包新增了 Python 运行时耦合,而同类 py→C++ 抽取代码在仓库内惯例是放在 rtp_llm/cpp/pybind/。同时 config_modules(几乎被全仓引用的底层 target)在 config/BUILD:29 新增了对 //rtp_llm/cpp/rdma_transport:rdma_config 的依赖,形成 config 层反向依赖 transport 包的边。
  • [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue RDMA 成功路径每请求输出一条 INFO 级 [MM-RDMA-HIT] 日志
    consume() 成功返回前无条件执行 RTP_LLM_LOG_INFO("[MM-RDMA-HIT] multimodal embedding read over rdma, %d slot(s)", ...)(:140-141)。consume 对每个带多模输入且命中 RDMA 的 generate 请求恰好调用一次,因此这是稳态成功路径上的每请求 INFO 日志。多模在线服务 QPS 较高时会淹没日志、抬高 IO,而其承载信息(命中量/成功率)本属指标语义——ViT 侧已用 transport tag 区分 bytes/rdma,LLM 侧缺的正是对应的成功侧指标。
  • [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue GPU-NIC 亲和键语义由 local rank 改为物理 GPU index,属线上行为翻转且无回滚开关
    旧逻辑固定用 affinity[str(local_rank)];新逻辑先用 _physical_gpu_index(local_rank) 解析物理 GPU 号再查 affinity.get(physical_gpu),仅当取不到时才回退 affinity.get(str(local_rank))(:639-643)。当 CUDA_VISIBLE_DEVICES="4,5,6,7" 而映射是整机 0-7 全量时两个键都存在,local_rank=0 会从旧 key "0" 切到新 key "4"——已部署服务的 NIC 绑定在升级后整体改变,回退分支不触发,错配是静默的,成功路径仅一条 logging.info(:654)。同一份 JSON 被赋予两种互斥 key 语义、无法从内容区分。另 :617 由 is None 改为 if configured_nics:ACCL_USE_NICS="" 的语义从「已设置」变为「未设置并继续推导」。
  • [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue online_eval proto 加载改为硬依赖 rtp_llm 包可导入,全部驱动脚本的 PYTHONPATH 不满足该前提
    旧实现靠 sys.path.insert(0, out) + 扁平模块名,工具完全自包含;改造后全文已无任何 sys.path 操作,改为 _add_generated_package_pathsimportlib.import_module("rtp_llm.cpp")(:123)并就地改写其 __path__(:126),要求仓根在 sys.path 上。但全部驱动一律为 PYTHONPATH="${SCRIPT_DIR}" python3 "${SCRIPT_DIR}/<script>.py"(SCRIPT_DIR=tools/online_eval,见 flexlb_behavior_test.sh:391、master_kill_restart_test.sh:263、engine_disconnect_ttft_test.sh:303 等),tests/test_mock_engine.py 亦只插 TOOL_DIR,无一处引入仓根;而 ensure_proto_modules() 位于 mock_engine_cluster.py
  • [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue ParseFromString 返回值判定为误导性死代码
    Python protobuf 的 Message.ParseFromString() 返回已解析的字节数(int)而非 bool,解析失败抛 DecodeError。因此 if not slot.ParseFromString(desc_bytes)(:123)只在 desc_bytes 为空时成立,而这已被上一行 if not desc_bytes(:120)拦截。真正的畸形 descriptor 靠外层 except Exception(:128)捕获(mm_output_transport_test.py:72 传入 b"\x80" 走的正是这条路)。行为正确,但这是按 C++ 版语义写的跨语言误用,留下看似生效实则不可达的校验。
  • [6.1] Quality — PR description 说明动机与设计 → issue arch_select.bzl 新增两项跨仓构建契约,覆盖版本必须同步且缺失时仅静默降级
    copy_all_so() 无条件新增 copy_so("@rtp_llm//:mm_rdma_exporter")(:11),rtp_llm/libs/BUILD:58whl_package_libs 无条件引用 :libmm_rdma_exporter_so;新增的 rdma_transport_deps()(:59-65)被 rtp_llm/cpp/rdma_transport/BUILDload(...) 硬引入。内部仓通过 --override_repository 替换 @arch_config:若内部 arch_select.bzl 未同步补 rdma_transport_deps,内部构建在 load 阶段直接失败;若补了它却漏了 copy_all_so 中的 mm_rdma_exporter,analysis 阶段失败或 wheel 缺 so,而 ops/__init__.py:342_load_rdma_ops 只以 logging.info 记录并回落到 `DisabledMMR
  • [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue RDMA 成功路径每请求输出一条 INFO 级 [MM-RDMA-HIT] 日志
    consume() 成功返回前无条件执行 RTP_LLM_LOG_INFO("[MM-RDMA-HIT] multimodal embedding read over rdma, %d slot(s)", ...)(:140-141)。consume 对每个带多模输入且命中 RDMA 的 generate 请求恰好调用一次,因此这是稳态成功路径上的每请求 INFO 日志。多模在线服务 QPS 较高时会淹没日志、抬高 IO,而其承载信息(命中量/成功率)本属指标语义——ViT 侧已用 transport tag 区分 bytes/rdma,LLM 侧缺的正是对应的成功侧指标。
  • [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue proto import 列表在三处重复维护,且 create_grpc_proto 的 data 列表已过期成为误导性同步点
    同一份传递 import 关系现在同时存在于 .proto 的 import 语句、tf_proto_library_ccprotodepsgenerate_grpc_protoproto_deps 三处,BUILD 只能靠 :29、:55 的注释 "Keep this list in sync with the imports in ..." 提醒。同时 create_grpc_protodata(:17-20)仍只列 flexlb_schedule_service.protomodel_rpc_service.proto,未含新增的两个 proto。功能上不出错——bazel/py_proto.bzl 已把 inputs = [proto_file] + ctx.files.proto_deps 作为 action 输入,该 data 实际已无作用——但它与 proto_deps 并存形成两份含义不同的手工清单。
  • [6.1] Software Engineering — ISP:调用方不依赖无关大接口 → issue transport core 仍依赖 fat model_rpc_service.pb.h,弱化了 multimodal_output.proto 拆分的解耦目标
    本次专门新建 multimodal_output.proto 以便被 multimodal_processor 共享而不产生依赖环,MMRdmaExporter 也正确地只依赖 multimodal_output_cc(rdma/BUILD:28)。但 MMRemoteOutputTransport.h:13MMRdmaReader.h(及 MMRdmaReader.cc:7)仍 include model_rpc_service.pb.h,对应 BUILD 依赖也是 model_rpc_service_cc_proto。这两个头文件实际只需要 MultimodalInputsPB/MultimodalOutputPB/MMRdmaSlotPB,却把整个 RpcService/FlexLB 服务定义拉进 multimodal_processor 子树,并经工厂头传递给 RemoteMultimodalProcessor.h
  • [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue proto_utils 用 setattr 搬运 7 个消息名,仅为支撑单一调用点
    _ensure_proto_modules 用字面量名单 + getattr/setattrTensorPBMMPreprocessConfigPBMultimodalInputPBMultimodalInputsPBMMRdmaSlotPBMultimodalOutputPBReleaseLeasePB 挂回聚合模块(:74-83),注释自述是过渡兼容层。但全仓真实消费者只有 mock_engine.pypb2.TensorPB 一处。名单一旦与 multimodal_output.proto 漂移,删名会让 getattr 抛 AttributeError,新增消息则静默缺失;动态属性注入还让 pyright/flake8 与 IDE 跳转完全失效。
  • [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue release 队列饱和时在持锁区上报指标与打日志,并把最长 1s 的同步 release 压到推理成功路径
    MMRemoteOutputTransport.h:79 明确写着成功路径的归还不得拖慢消费者,但 releaseAsyncpending_release_handles_ 超过 kMaxPendingReleaseHandles=1024 时置 send_sync,随后在调用线程执行 releaseNow(..., release_timeout_ms_)(:127,默认 1000ms)——该调用点正是 MMRdmaReader.cc:142 成功路径上的 lease.releaseAsync(),处于推理请求路径。同时指标上报(:120)与 WARNING(:121)都发生在 std::lock_guard<std::mutex> lock(release_mutex_) 临界区内(:111-125),饱和期每请求一条 WARNING 并让其他线程在锁上排队。
  • [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue 唯一的生产 gRPC 控制面客户端零测试覆盖
    全仓 createGrpcMMControlClient 只有 :213 定义与 MMRemoteOutputTransportFactory.cc:30 生产调用,测试目录零引用;MMRemoteOutputTransportTest.cc:63 全程用 FakeControlClient,仅用到 createGrpcInlineReceiptReader()。于是 release_thread_ 工作线程、release_mutex_/release_cv_ 停机握手、1024 饱和转同步(:114)、析构后同步 drain(:34-52)、releaseLoop 按 endpoint 聚批(:163-186),以及 request() 的「结构化业务错误 / UNAVAILABLE+DEADLINE_EXCEEDED 可重试 / 其余不可重试」三分类(:77-91),全部从未被执行过。
  • [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue online_eval proto 加载改为硬依赖 rtp_llm 包可导入,全部驱动脚本的 PYTHONPATH 不满足该前提
    旧实现靠 sys.path.insert(0, out) + 扁平模块名,工具完全自包含;改造后全文已无任何 sys.path 操作,改为 _add_generated_package_pathsimportlib.import_module("rtp_llm.cpp")(:123)并就地改写其 __path__(:126),要求仓根在 sys.path 上。但全部驱动一律为 PYTHONPATH="${SCRIPT_DIR}" python3 "${SCRIPT_DIR}/<script>.py"(SCRIPT_DIR=tools/online_eval,见 flexlb_behavior_test.sh:391、master_kill_restart_test.sh:263、engine_disconnect_ttft_test.sh:303 等),tests/test_mock_engine.py 亦只插 TOOL_DIR,无一处引入仓根;而 ensure_proto_modules() 位于 mock_engine_cluster.py
  • [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue 新增的 GPU-NIC 亲和性解析与回退逻辑无任何单元测试覆盖
    _physical_gpu_index(:525-568)、load_gpu_nic_affinity(:571-612)、_configure_gpu_nic_affinity(:615-660)共约 135 行新增逻辑,含 CUDA_VISIBLE_DEVICES 未设置/空串/越界/负数/数字形/GPU- UUID 形分支、nvidia-smi CSV 逐行 partition、JSON 结构校验与两级 key 回退。在 rtp_llm/config/test/server_config_setup_test.py 中检索 affinity/ACCL/physical_gpu 无任何命中;另一处仅把 load_gpu_nic_affinity 作为 patch 目标 mock 掉。这些函数是纯 Python + env/subprocess 边界,用 monkeypatch 覆盖成本很低,而选错 NIC 只会静默降低 RDMA 带宽、几乎不可能在 CI 中暴露。

RTP-LLM Checklist

  • [I] 代码质量 — 同一功能用统一工具函数 → issue proto import 列表在三处重复维护,且 create_grpc_proto 的 data 列表已过期成为误导性同步点
    同一份传递 import 关系现在同时存在于 .proto 的 import 语句、tf_proto_library_ccprotodepsgenerate_grpc_protoproto_deps 三处,BUILD 只能靠 :29、:55 的注释 "Keep this list in sync with the imports in ..." 提醒。同时 create_grpc_protodata(:17-20)仍只列 flexlb_schedule_service.protomodel_rpc_service.proto,未含新增的两个 proto。功能上不出错——bazel/py_proto.bzl 已把 inputs = [proto_file] + ctx.files.proto_deps 作为 action 输入,该 data 实际已无作用——但它与 proto_deps 并存形成两份含义不同的手工清单。

Python Static-First Checklist

  • [P.A] 静态结构与类型纪律 — 禁止 getattr/setattr literal 访问 → issue proto_utils 用 setattr 搬运 7 个消息名,仅为支撑单一调用点
    _ensure_proto_modules 用字面量名单 + getattr/setattrTensorPBMMPreprocessConfigPBMultimodalInputPBMultimodalInputsPBMMRdmaSlotPBMultimodalOutputPBReleaseLeasePB 挂回聚合模块(:74-83),注释自述是过渡兼容层。但全仓真实消费者只有 mock_engine.pypb2.TensorPB 一处。名单一旦与 multimodal_output.proto 漂移,删名会让 getattr 抛 AttributeError,新增消息则静默缺失;动态属性注入还让 pyright/flake8 与 IDE 跳转完全失效。
  • [P.A] 静态结构与类型纪律 — 禁止 hasattr 做控制流分支 → issue gRPC deadline 解析逻辑在 proxy_router 与 vit_proxy_server 重复实现,并用 hasattr 做控制流分支
    proxy_router.py:21 _context_deadline_secondsvit_proxy_server.py:119 _resolve_context_deadline_seconds 语义逐行一致:同样的 hasattr(context, "time_remaining") 分支、同样的 numbers.Real 守卫、同样的 remaining <= 0 → None、同样的 time.monotonic() + min(...),连告警文案都相同。二者在同一进程内(vit_proxy_server.py 直接 import MMOutputProxyRouter),属同一 PR 新增的两份拷贝。真实 grpc.ServicerContext 恒有 time_remaininghasattr 分支实际只为兼容测试替身,把替身形状固化成生产控制流。本 PR 已新增共享模块 rtp_llm/utils/grpc_util.py,却只放了张量转换工具。
  • [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue 导出器测试绕过公共 pybind 边界,assembleMMRdmaOutput 的多数守卫仍无覆盖
    全部用例都调用内部方法 adapter.exportSlots(...)(:163/:180/:195/:203/:213,依赖 test/BUILD 的 -fno-access-control),而 Python 实际调用的公共边界 exportEmbedding(MMRdmaExporter.cc:134-153)——含 gil_scoped_releaseSerializeAsString 与「失败返回空 vector」语义——从未被执行。assembleMMRdmaOutput 的负向用例只有 rejectsUnsetRoleAndInconsistentSplit(:232)覆盖 ROLE_UNSPECIFIED 与 split 总量不符两条,其余守卫(tensors/roles 数量不等、重复 POS_ID、embedding_chunks 为空、pos_id 行数不符、extra 数量不符)仍无覆盖。

Strengths

  • proto 外迁克制且 wire 兼容:迁出的 5 个消息不引入 package,消息名与字段号完全不变,新增字段为纯 optional 扩展;pom.xml:106-115 同步预建两级目录并 stage 新 proto,bazel/py_proto.bzl 新增 proto_deps 让传递闭包闭合,跨语言消费者无遗漏,Java 侧亦无消费者受影响。
  • 协议违规与降级被严格区分:MMRemoteOutputTransport.cc:75-82 对「ViT 返回从未 advertise 的数据面回执」直接 discard() 释放远端 slot 并报错而非降级——因 RDMA 回执的内联字段为空,降级会解出貌似合法的错数据;测试有专门用例锁住该语义。
  • 降级在类型层面被限定为一轮:MMTerminalReceiptReader::consumefinal 封死终端读取器再次 retry,degradeToTerminalwithdraw() 全部候选能力再重试,budget.exhausted() 时直接失败,且 :113-118 对「兜底响应仍带 RDMA 回执」再次 discard 并报错。
  • QueryConverter::transMMOutput 的进程级 RTP_LLM_CHECK 改写为 inlineOutputFromPbErrorResult,并补齐空 embedding、split_size 总量、pos_id 与 extra_input 数量一致性校验加 try/catch:畸形远端响应从「打挂后端」变为「单请求失败」。
  • 资源释放路径细致:SlotLease RAII 保证 retry/异常任何路径都归还远端 slot;createBatchRdmaTransport.cc:50-64)在任一 group 失败时回滚全部已创建 lease 并有 fail_at=2 专用用例;GrpcMMControlClient 析构在同一锁作用域内置 stopping_ 并 swap 出待发队列,join 后再同步补发,避免「关闭即泄漏 lease」。
  • 多模态入口归属被抽成纯枚举决策表 resolveMMProcessorKind,非法组合在 LocalRpcServer.cc:73-76 于引擎构造前 fail-fast 并返回带完整上下文的 FAILED_PRECONDITIONprepareInput(:170-177)把「有多模输入但未配置 mm_processor」从静默忽略改为 MM_NOT_SUPPORTED_ERROR,消除一类静默错答。
  • 构建面对循环依赖处理清晰:新增 :multimodal_pb_converter:mm_processor_config:multimodal_error 三个精简 target 打断 multimodal_processormodel_rpc_server 的环并有注释说明动机,mm_processor_config_test 因此成为不链接 torch、无需 GPU 节点的纯逻辑用例。
  • 开源构建零负担:hasRdmaImplementation() 为 false 时 advertise() 恒退化为内联,arch_select.bzl:59-65rdma_transport_deps() 默认别名到 no-impl 并注释「同 factory API、无 provider」;libmm_rdma_exporter 模块名在 PYBIND11_MODULEcc_binarycopy_solibs/BUILDops/__init__.py 五处严格一致且提供 Python 降级替身。
  • MMRdmaExporter.cc:140/159exportEmbedding/release 上正确 gil_scoped_releaseassembleMMRdmaOutput 显式把 POS_ID .to(torch::kCPU)MMRdmaReader.cc:169-177 把读超时与请求剩余预算取 min,设备与时限语义都被想过。

Comment thread rtp_llm/flexlb/flexlb-grpc/pom.xml Outdated

@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 #1353 (non-blocking suggestions)

35 条 P2/P3 建议,不阻塞合并。阻塞判定与完整摘要见上一条 review。


} // namespace

bool MMRdmaReader::advertise(const std::string& /*endpoint*/, MultimodalInputsPB& request_pb) {

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] RDMA 读通道持续故障时缺少熔断,每个多模请求被放大成两次 ViT 前向且不会自愈

advertise()(:97-103)只判断 ensureReader(),成功后每请求无条件 set_support_rdma(true),无失败计数或时间窗熔断;ensureReader()call_once(:219)只 latch「构造失败」。读失败与 manifest 不一致仅返回 retry(:129、:137),degradeToTerminal 会再发一次 RemoteMultimodalEmbedding。故「描述符可解析但内存读不通」这类持续故障下,每请求 = 一次浪费的 ViT 前向 + 最长 read_timeout_ms(默认 3000) + release + 第二次 ViT 前向,永不自愈。mode 双侧默认 auto,属默认路径;测试注释提到 "circuit breaker" 但机制未实现。

建议:MMRdmaReader 内加连续失败熔断:用 std::atomic 统计连续 retry,超阈值后在冷却期内让 advertise() 直接返回 false,consume 成功即复位,冷却结束再放量试探。把 kReasonRdmaReadError/kReasonRdmaManifestError 与熔断开合状态通过已注入但在本文件完全未使用的 metrics_ 上报,使「RDMA 已被永久降级」可告警。同时在发布说明中明确 --mm_transport_mode grpc 是该数据面的一键回滚开关。

public:
virtual ~RdmaExport() = default;
// Returns an empty descriptor (lease_id is empty) when export fails.
virtual RdmaDescriptor create(const std::vector<torch::Tensor>& tensors) = 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.

[P2] rdma_transport 扩展点未声明内存所有权、返回顺序、条目数量与 lease_id 唯一性契约

create 的注释只写「失败时返回空 descriptor」,未规定导出槽是拷贝输入张量还是别名其显存。而 exportSlotsembedding.narrow(...) 这类临时 view 传入 createBatch(MMRdmaExporter.cc:72、:119)后即返回;Python 侧 emb/pos/extras 是局部变量(rdma/backend.py:91-98),try_transfer 返回即引用归零、caching allocator 可复用该显存,而 LLM 侧稍后才读。read(:68)未规定返回张量数量与顺序须严格对应 descriptor 展平顺序(assembleMMRdmaOutput 只校验数量、依赖顺序 cat),也未声明须支持并发(所有请求共享同一 reader_)。另有两条未成文契约:每个入参张量须对应恰好一条同序 TensorMeta(否则 :153 拒收)、lease_id 须跨 worker 全局唯一(proxy_router.py:80 路由前提)。唯一实现全返回...

建议: 在两个纯虚函数上写明契约:create 必须在返回前完成拷贝或自行持有输入张量 storage 引用直到对应 lease 被 release,须为每个入参张量输出一条同序 TensorMeta,且 lease_id 跨进程全局唯一;read 返回张量的数量与顺序必须严格对应入参 descriptor 展平顺序、须支持并发调用。更稳妥的是在 RdmaTransport.cc 提供按 lease_id 持有 std::vector<torch::Tensor> 的保管容器,把生命周期从 provider 约定上移为框架保证;并把测试里的 RecordingExport 提炼为 conformance 夹具,新 provider 接入必须通过。

Comment thread rtp_llm/cpp/rdma_transport/RdmaTransport.cc Outdated
Comment thread rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc Outdated
releaseNow(endpoint, handles, std::min(release_timeout_ms_, remaining));
}

void releaseAsync(std::string endpoint, std::vector<std::string> handles) override {

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] release 队列饱和时在持锁区上报指标与打日志,并把最长 1s 的同步 release 压到推理成功路径

MMRemoteOutputTransport.h:79 明确写着成功路径的归还不得拖慢消费者,但 releaseAsyncpending_release_handles_ 超过 kMaxPendingReleaseHandles=1024 时置 send_sync,随后在调用线程执行 releaseNow(..., release_timeout_ms_)(:127,默认 1000ms)——该调用点正是 MMRdmaReader.cc:142 成功路径上的 lease.releaseAsync(),处于推理请求路径。同时指标上报(:120)与 WARNING(:121)都发生在 std::lock_guard<std::mutex> lock(release_mutex_) 临界区内(:111-125),饱和期每请求一条 WARNING 并让其他线程在锁上排队。

建议: 把指标上报与日志移出 release_mutex_(锁内只置标志,出锁后再上报);日志改为按时间窗或计数聚合的限频输出。同步回退的超时应从请求剩余预算派生而非固定 release_timeout_ms_,或改为「入队替换最旧批次 + 上报丢弃量」,让导出侧 slot GC 兜底,而不把 ViT 的慢反压到推理延迟上。

Checklist: [6.1] LSP:子类/重写保持基类契约

Comment thread rtp_llm/multimodal/transport/rdma/backend.py Outdated
from rtp_llm.multimodal.mm_process_engine import MMEmbeddingRes
from rtp_llm.utils.grpc_util import trans_from_tensor

TRANSPORT_BYTES = "bytes"

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] transport 指标标签词汇与配置 mode 词汇不一致

配置侧取值为 MM_TRANSPORT_MODE_GRPC = "grpc"(py_config_modules.py:268,与 C++ 侧 kMMTransportModeGrpc 一致),但 inline 通道在指标里的 transport tag 值是 TRANSPORT_BYTES = "bytes"(:14、:29、:37)。同一条数据面在配置与监控中有两套名字,运维看到 mm_transport_mode=grpc 后需要额外知识才能在面板上对应到 transport=bytes 的时间线。

建议:TRANSPORT_BYTES 的值统一为 "grpc"(复用 MM_TRANSPORT_MODE_GRPC 常量),或在配置项 help 文案中显式写明「grpc 通道在指标中记为 bytes」,避免两套词汇长期并存。

return ConsumeResult::retry(kReasonRdmaManifestError);
}

RTP_LLM_LOG_INFO("[MM-RDMA-HIT] multimodal embedding read over rdma, %d slot(s)",

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] RDMA 成功路径每请求输出一条 INFO 级 [MM-RDMA-HIT] 日志

consume() 成功返回前无条件执行 RTP_LLM_LOG_INFO("[MM-RDMA-HIT] multimodal embedding read over rdma, %d slot(s)", ...)(:140-141)。consume 对每个带多模输入且命中 RDMA 的 generate 请求恰好调用一次,因此这是稳态成功路径上的每请求 INFO 日志。多模在线服务 QPS 较高时会淹没日志、抬高 IO,而其承载信息(命中量/成功率)本属指标语义——ViT 侧已用 transport tag 区分 bytes/rdma,LLM 侧缺的正是对应的成功侧指标。

建议: 降为 RTP_LLM_LOG_DEBUG,并通过 metrics_ 上报带 transport tag 的命中次数/读取槽数/读取字节,使成功与降级两侧在监控上对齐(同时补上熔断项所缺的可观测面)。若灰度期确需日志,改为首次命中 + 每 N 秒一次的限频输出。

Checklist: [6.1] 可观测性:日志/指标/超时可操作、非噪声;[6.1] 无 per-forward 调试日志 / 噪声热路径输出

Comment thread rtp_llm/cpp/model_rpc/proto/BUILD
Comment thread rtp_llm/cpp/config/BUILD Outdated
@ydshi0
ydshi0 force-pushed the feature/rdma_transport branch from f0f2411 to 9c51e3d Compare August 31, 2026 07:38

@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 #1353

Status: BLOCKING

Summary: P0/1 · P1/0 · P2/29 · P3/8

Reviewed: commit 9c51e3d46b73 · 2026-08-31 16:42 UTC+8

Blocking Issues

P0

  • generate_grpc_proto 的 proto_srcs 变为强制属性但 flexlb_schedule_service_py 未传,Bazel 加载期直接失败 @ rtp_llm/cpp/model_rpc/proto/BUILD:85
    • 建议:推荐去掉 mandatory(改为 attr.label_list(allow_files = True, default = [])ctx.files.proto_srcs 缺省为空列表,对无 import 的 proto 与改动前行为等价),这样共享 rule 契约的收紧不要求全部历史调用点同步改动;若坚持 mandatory,则须给 flexlb_schedule_service_py 显式传 proto_srcs = "flexlb_schedule_service.proto"。改完后用 bazel query //rtp_llm/cpp/model_rpc/proto:all 确认整包可加载,并对 //rtp_llm/server:server 做一次 analysis 验证。

Non-blocking Suggestions

P2

  • MM transport 契约常量在 Python/C++ 五处各自硬编码,margin 仅 C++ 侧可配 @ rtp_llm/cpp/pybind/ConfigExtract.h:47
    • 建议:收敛为单一事实来源:在 ConfigModules.h 定义 kDefaultMMTimeoutMs 与 margin 具名常量,default_rpc_timeout_msConfigExtract.h:47 的兜底值与 MMRemoteOutputTransport.h 的两个常量都引用它;Python MMTransportConfig 补上 rpc_timeout_margin_ms 并在 vit_group_args.py 增加对应 arg/env,由 ConfigExtract 只做透传。同时修正 py_config_modules.py:266 已被违反的对齐注释,并补一条跨语言默认值一致性断言测试防止漂移。
  • extractVitConfig 未被 embedding 入口复用,该路径 deadline 被隐式钉在 125s @ rtp_llm/cpp/pybind/ConfigExtract.h:39
    • 建议:让 embedding 入口也调用 extractVitConfig,随后在结果上显式覆写 output_transport.mode = kMMTransportModeGrpc(保留现有意图注释),把「embedding 路径固定走 inline gRPC」表达成一次显式覆盖而非一份不再演进的副本。同时确认改动前该路径是否本来没有 deadline——若是,则 125s 截断属行为变更,需在发布说明中列出。
  • rdma_transport 扩展点未声明内存所有权、返回顺序、条目数量与 lease_id 唯一性契约 @ rtp_llm/cpp/rdma_transport/RdmaTransport.h:59
    • 建议:在 RdmaExport::createRdmaRead::read 的声明处逐条写明契约:入参内存所有权与保活责任(推荐「实现必须拷贝进自有已注册 slot,调用方无需保活」)、返回条目数量与顺序必须与入参严格对应、lease_id 唯一性与释放幂等性。若结论是需要调用方保活,则 RdmaOutputBackend 必须把 emb/pos/extras 的引用与 lease 一起持有到 release();若是拷贝语义,也请在 try_transfer 处加一行注释固定该前提。
  • RDMA 描述符反序列化不校验 offset/nbytes 越界,把内存安全边界推给不在本仓的 provider @ rtp_llm/cpp/rdma_transport/RdmaTransport.cc:90
    • 建议:在 fromProto 中对每个 TensorMetaPB 增加边界校验(offset + nbytes <= payload_bytesnbytes == numel(shape) * itemsize(dtype)、各维非负),任一不满足即返回 false 让既有的 inline 降级路径接管;并在 readAllSlots 中把 manifest 的 shape/dtyperesult.tensors[i]sizes()/scalar_type() 逐个比对,不一致时返回 false 触发回退。此处是跨节点不可信输入的第一道边界,校验成本远低于潜在的越界读。
  • RDMA 降级原因被丢弃且两个 metrics_ 成员从未使用,数据面回退率不可观测 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc:100
    • 建议:在 degradeToTerminalfetch 的协议违规分支调用 metrics_->reportRpcClientError(endpoint, reason)(reason 已是稳定枚举字符串,可直接作 tag),或新增 rtp_llm_vit_rdma_degrade_qps 并为「未 advertise 回执」补一条独立 reason;MMRdmaReader::metrics_ 建议用于上报 RDMA 读耗时与失败计数。新数据面放量前,回退率指标是前置条件;若确认不需要,则应删除两个未使用成员与 ConsumeResult::reason(),避免留下「看起来已埋点」的假象。
  • RDMA 读通道持续故障时缺少熔断,每个多模请求被放大成两次 ViT 前向且不会自愈 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:97
    • 建议:在 MMRdmaReader 中加入轻量熔断:按滑动窗口统计连续/比例失败,超阈值后让 advertise() 在一段冷却期内返回 false(自然回落 inline,单次前向),冷却结束再放行探测请求。熔断开合应上报指标与 INFO 日志,使「已熔断」状态在线上可见;阈值与冷却时长走 MMTransportConfig 暴露,保留运维干预入口。
  • LLM→ViT 的 gRPC 截止时间完全由客户端可控的 mm_timeout_ms 决定,缺服务端上限钳制 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc:19
    • 建议:在 resolveRpcTimeoutMs 中对请求侧取值做 std::min(client_timeout_ms, server_max_timeout_ms) 钳制,上限来自 MMTransportConfig(可复用即将补齐的 Python 配置项),被钳制时记录一条含原始值的可聚合 WARNING 或指标,使「客户端要求的超时被服务端收窄」可观测。同时为该纯函数补参数化单测(空 inputs、全非正、单个正值、多输入取最大加 margin、超上限被钳位五种边界)。
  • release 队列饱和时在持锁区上报指标与打日志,并把最长 1s 的同步 release 压到推理成功路径 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:105
    • 建议:把指标上报与日志移出临界区(锁内仅置 send_sync 标记,出锁后再上报);对饱和场景考虑改为丢弃最旧 batch 并计数(encoder 侧 TTL GC 是兜底),或用一个独立的溢出线程发送,避免把 best-effort 归还的延迟直接计入推理请求的响应时间。
  • 析构排空未按 endpoint 合批且无总预算,进程关停耗时不可控 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:49
    • 建议:抽出 drainBatches(std::deque<ReleaseTask>&)releaseLoop 与析构两处复用,使析构也按 endpoint 合并成单次 ReleaseRdmaLease;并为整段收尾加一个总预算(DeadlineBudget 或固定 shutdown 上限),超出后放弃剩余 lease——注释已说明 encoder 侧 TTL GC 是兜底,放弃是安全的,不应让进程退出被远端故障拖住。
  • 唯一的生产 gRPC 控制面客户端零测试覆盖 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:27
    • 建议:把 releaseNow 抽成可注入的 sink(或用 in-process gRPC 假服务端),至少覆盖四个边界:单次 handles 超过 kMaxPendingReleaseHandles 时全部走同步;队列饱和时 release_queue_full 被上报且 handles 不丢;析构时残留 batch 被同步发出;handles 为空时不产生 RPC。同时断言 pending_release_handles_ 在入队/出队/析构后回到 0,防止计数漂移导致永久同步降级。
  • exporter 惰性构造在请求线程内持 GIL 完成 RDMA 初始化,且一次失败即进程内永久闭锁 @ rtp_llm/multimodal/transport/rdma/backend.py:60
    • 建议:把 provider 构造移出请求路径:在 try_create 成功探测后于 ViT server 启动阶段(或后台线程)异步初始化,失败在启动日志中可见,supports() 在就绪前返回 False(自然走 inline bytes)。若坚持懒构建,至少在 pybind 构造中释放 GIL,为「初始化失败导致永久降级」补带 reason 维度的降级计数指标,并对逐请求 warning 限频(首次告警 + 周期汇总);若确定不重试,请在注释中写明理由。
  • 用字面量 getattr 探测 MMRdmaExporter.available,掩盖 fallback 类的接口缺口 @ rtp_llm/multimodal/transport/rdma/backend.py:41
    • 建议:为 exporter 定义显式 Protocol/ABC(包含 available()/enabled()/export_embedding()/release()),让 DisabledMMRdmaExporter 显式实现全部方法并把 available 声明为类属性,改为直接访问 MMRdmaExporter.available(),使接口缺口在导入/类型检查阶段就暴露;若必须保留反射以兼容旧 so,请在此处注释说明兼容目标与可移除条件。
  • 惰性构造状态机、close、transport 工厂选路与新 .so 导入均无测试覆盖 @ rtp_llm/multimodal/transport/rdma/backend.py:37
    • 建议:用轻量 fake 替换 rtp_llm.ops.MMRdmaExporter(无需真实硬件,mock.patch target 指向使用处模块),补充:available() 为 False 时 try_create 返回 None;构造抛异常时降级且第二次调用不再尝试构造;enabled() 为 False 时 _exporter 保持 None;close() 之后 _get_exporter/release 均为安全 no-op。让测试形态与生产构造形态一致。
  • Python 侧 MMOutputTransport.transfer 缺少异常护栏,后端抛异常会使请求失败而非降级到 inline @ rtp_llm/multimodal/transport/base.py:107
    • 建议:在 transfer 的候选后端循环中对 supports/try_transfer 加 try/except,捕获后 logging.exceptioncontinue 到下一个后端,最终仍落到 self._terminal.transfer(...);同时在 MMTransportBackend.try_transfer 的 docstring 中说明「异常等价于返回 None」,把降级语义写进基类契约,避免后续新增后端各自重复实现宽泛捕获。
  • LLM 侧 RDMA provider 懒初始化占用请求预算且无上限,首个请求会被误报为预算耗尽 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:219
    • 建议:把 provider 初始化移出请求关键路径(在 createMMRemoteOutputTransport 里异步预热,或在引擎 init 阶段同步做一次);或至少在 ensureReader() 前检查剩余预算、初始化耗时超阈值时记录专门的 warning/指标,并让由初始化拖累导致的失败携带可区分的错误信息。
  • 新增 .so 未沿用 th_transformer 的 librtp_compute_ops 链接约定,且与 libth_transformer.so 静态重复链接同一套 protobuf 描述符 @ BUILD:187
    • 建议:二选一:(1) 让 libmm_rdma_exporter.soth_transformer 复用 rtp_compute_ops 那样通过共享库复用已有 .so 中的 proto 实现,避免第二份静态描述符并统一链接约定;(2) 保留现状但补一个进程级守护测试(同一 Python 进程内依次导入两个模块),把隐式约定变成会失败的显式断言,并在 target 注释中写明「不得与 th_transformer 共载」的原因。无论哪种,请确认产物的 DT_NEEDED 正确。
  • arch_select.bzl 新增两项跨仓构建契约,且 mm_rdma_exporter 无条件进入全平台构建与 wheel @ arch_config/arch_select.bzl:11
    • 建议:把 mm_rdma_exportercopy_sowhl_package_libs 条目改为按平台 select()(或由 arch_select 暴露一个在不支持平台返回空的入口),使构建期裁剪与运行时可选加载语义一致;若确实要求全平台强编,请在 PR 描述中说明并移除运行时降级分支。同时确认覆盖 @arch_config 的内部 arch_select.bzl 已同步新增 rdma_transport_deps() 并把 mm_rdma_exporter 纳入其 copy_all_so()
  • model_rpc_server 导出 MultimodalPbConverter.h 但未声明其实现目标,链接只靠一条传递路径 @ rtp_llm/cpp/model_rpc/BUILD:70
    • 建议:把 :multimodal_pb_converter 显式加入 model_rpc_server 的 deps(hdrs 与实现目标成对声明),或把 MultimodalPbConverter.h 一并从 hdrs 的 glob 中 exclude,让消费者直接依赖 //rtp_llm/cpp/model_rpc:multimodal_pb_converter,使「谁提供该实现」在 BUILD 上显式可读。
  • pybind 层与 multimodal transport 层形成包级双向依赖 @ rtp_llm/cpp/pybind/BUILD:37
    • 建议:把 py::object → RdmaConfig 的转换下沉到中立层(例如 rtp_llm/cpp/config 下新增 config_py_extract 目标)由两侧共同依赖;或让 MMRdmaExporter 只保留接受 const RdmaConfig& 的构造函数(MMRdmaExporter.cc:30),把 py::object 重载(:27-28) 连同 extractRdmaConfig 一起留在 MMRdmaExporterInit.cc 中完成转换,使依赖保持单向向下。
  • transport core 仍依赖 fat model_rpc_service.pb.h,弱化了 multimodal_output.proto 拆分的解耦目标 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.h:13
    • 建议:把 MMRemoteOutputTransport.hmm_rdma_reader 的 proto 依赖收窄到 multimodal_output_cc,仅在确实需要 RemoteMultimodalEmbedding stub 的实现文件(MMGrpcTransport.cc)中保留 model_rpc_service.grpc.pb.h 与对应 BUILD 依赖,让「谁需要 service 定义」在 BUILD 上一目了然。
  • 生产装配入口、mode 校验、超时解析与两条降级失败分支均无用例覆盖 @ rtp_llm/cpp/multimodal_processor/test/MMRemoteOutputTransportTest.cc:173
    • 建议:补一条直接调用 createMMRemoteOutputTransport 的用例,对 mode="auto"/"grpc" 分别断言 advertise 行为差异并对非法 mode 断言拒绝;构造函数已把 default_rpc_timeout_ms 暴露为形参(MMRemoteOutputTransport.h:188),给 Harness 增加可传 0 的重载以覆盖 deadline 耗尽分支;用 responses = {rdmaReceipt, rdmaReceipt} + read_ok=false 覆盖第二批 lease 的 discard;并把 fake 的两条归还路径记成不同标记(如 release_sync/release_async)。
  • 导出器测试绕过公共 pybind 边界,公开入口与 assembleMMRdmaOutput 的多数守卫仍无覆盖 @ rtp_llm/cpp/multimodal_processor/test/MMRdmaTransportTest.cc:149
    • 建议:为 MMRdmaExporter::exportEmbedding 增加走公开 API 的用例(覆盖 slot 序列化与 enabled()==false 短路);把 slot 规划逻辑提取为不依赖类状态的自由函数(与 assembleMMRdmaOutput 对称)并公开测试,减少对 -fno-access-control 的依赖;并对 assembleMMRdmaOutput 的每条守卫补一条最小反例用例。
  • 生产实际调用的 resolveAndLogMMProcessorKind 未被新测试覆盖,INVALID→fail-fast 语义无回归保护 @ rtp_llm/cpp/multimodal_processor/test/MMProcessorConfigTest.cc:10
    • 建议:补三条断言:INVALID 组合下 resolveAndLogMMProcessorKind(...).ok() 为 false 且 error 非空并包含 vit_separation/role_type/tp_rank 关键字段;NONE 组合下 ok() 为 true 而 kind == NONE;以及 EXPECT_EQ(resolveMMProcessorKind(true, VIT_SEPARATION_ROLE, true, PDFUSION, 0), MMProcessorKind::INVALID),使 ROLE 维在 has_local_engine 两个取值上都被钉住。
  • 新打包的 libmm_rdma_exporter.so 无导入冒烟覆盖,加载失败被静默吞掉 @ rtp_llm/ops/__init__.py:327
    • 建议:补一个最小 py_test(data = ["//:mm_rdma_exporter"]),直接 from libmm_rdma_exporter import MMRdmaExporter 并断言其可调用,使链接/加载问题在 CI 暴露;把兜底日志从 logging.info 提升为 logging.warning 并带上异常信息,并把 except BaseException 收窄为 ImportError/OSError
  • GPU-NIC 亲和键语义由 local rank 改为物理 GPU index,rank 兜底可能静默绑错网卡且无回滚开关 @ rtp_llm/config/server_config_setup.py:639
    • 建议:把兜底条件收窄为「确认没有发生设备重映射」:仅当 CUDA_VISIBLE_DEVICES 未设置或 physical_gpu == str(local_rank) 时才允许按 rank 键查表;发生重映射且物理键缺失时 logging.warning 并返回 False,让上层保持 ACCL 默认选路。成功日志中显式标注命中来源(physical 键 / rank 兜底键),并考虑提供一个 env 开关以便线上快速回退到旧的 rank 键语义。
  • npu_nic_affinity.json 按进程 CWD 相对路径读取,残留旧文件会被误读且失败仅记 info @ rtp_llm/config/server_config_setup.py:589
    • 建议:用 tempfile.TemporaryDirectory() 作为 subprocess.run(..., cwd=tmpdir) 的工作目录并按绝对路径读取该目录下的文件,从结构上排除陈旧文件;异常分支把 CalledProcessError.stderr 一并输出,并将失败日志级别提升为 warning,使 NIC 亲和性缺失在部署时可被发现。
  • 新增的 GPU-NIC 亲和性解析与回退逻辑无任何单元测试覆盖 @ rtp_llm/config/server_config_setup.py:525
    • 建议:在既有测试文件中补参数化用例并 patch subprocess.runmock.patch target 指向使用处模块),至少覆盖:CUDA_VISIBLE_DEVICES 未设置退化为 str(local_rank)"4,5,6,7" + local_rank=0 命中键 "4";物理键缺失时的回退路径;local_rank 越界返回 None;GPU-xxx 条目下 mock csv 输出并断言解析出的 index;以及 load_gpu_nic_affinity 的「文件不存在 / JSON 非法 / 值非字符串 / 成功」四条路径,断言 ACCL_USE_NICS 最终取值与返回值。
  • online_eval proto 加载改为硬依赖 rtp_llm 包可导入,且 rdma_transport 子包未打补丁 @ rtp_llm/flexlb/tools/online_eval/online_eval/proto_utils.py:116
    • 建议:在 _add_generated_package_paths 之前幂等地 sys.path.insert(0, str(REPO_ROOT)),使 rtp_llm 在纯源码树环境下也可导入;并把 rtp_llm.cpp.rdma_transport.proto 一并加入补丁列表,不再依赖命名空间包的隐式继承。更稳的做法是用 importlib.util.spec_from_file_location 按依赖序(tensor_rdma_pb2multimodal_output_pb2model_rpc_service_pb2)显式注册进 sys.modules。无论采用哪种方案,请补一个直接调用 ensure_proto_modules() 的用例。
  • mm_processor_ 空指针拒绝逻辑与 PrefillRpcServer 逐字重复,且新分支无测试 @ rtp_llm/cpp/model_rpc/LocalRpcServer.cc:171
    • 建议:抽出共享辅助(例如返回该 ErrorInforequireMultimodalProcessor()),两个 RpcServer 复用同一份错误码与文案;并在 LocalRpcServerTest 补一条「携带 multimodal 输入且 mm_processor_ 为空 → 返回 MM_NOT_SUPPORTED_ERROR」用例,与 Prefill 侧覆盖对齐。

P3

  • lease 归还在超时非正、连接失败、预算耗尽与 RPC 失败四条分支均无指标信号 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:140
    • 建议:给这三条早退分支各加一条含 endpoint 与 handles 数量的 RTP_LLM_LOG_WARNING,并复用 metrics_->reportRpcClientError(endpoint, "release_connection_error" / "release_budget_exhausted"),让「依赖 GC 兜底」显式可观测。预算耗尽的场景可考虑改走 releaseAsync(异步队列不受请求 deadline 约束),避免把 best-effort 释放和请求预算绑死。
  • RDMA 成功路径每请求输出一条 INFO 级 [MM-RDMA-HIT] 日志 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:140
    • 建议:删除该 INFO 日志,改为通过 metrics_ 上报 RDMA 命中 QPS 与 slot 数(与本 draft 中「metrics_ 成员从未使用」一并处理);若排障期确需保留,请降级为 DEBUG 或加采样开关。
  • pybind 导出的 C++ VitConfig 未同步 output_transport,pickle 会静默丢字段 @ rtp_llm/cpp/config/ConfigModules.h:356
    • 建议:在 ConfigInit.ccVitConfigoutput_transportdef_readwrite 与 pickle 项(并同步注册 MMTransportConfig/MMControlConfig/RdmaConfig),或在 ConfigModules.h 该字段旁加注释说明「不经 pybind 往返、仅由 extractVitConfig 填充」,让后续新增字段的人知道导出面的边界。
  • 多模态 ingress 归属判定在 Python/C++ 双份实现,漂移时由静默降级变为启动硬失败 @ rtp_llm/async_decoder_engine/rpc_engine.py:45
    • 建议:将 role / tp_rank / vit_separation 的归属判定收敛到单一实现(把 ownsMultimodalIngress 通过 pybind 暴露给 rpc_engine.py 复用,或让 Python 只读取 C++ 决策结果),消除漂移可能;若短期保留双份,请在发布说明中列出会新触发启动失败的组合。
  • cuda13 的 nvshmem 链接补丁未应用到同文件既有的 multimodal_processor_test @ rtp_llm/cpp/multimodal_processor/test/BUILD:28
    • 建议:把 cuda13_torch_link_deps 一并加到 multimodal_processor_test 的 deps,或按 model_rpc/test/BUILD 的方式抽出包内共享 test_deps 列表,使同包 target 的链接修补一致生效。
  • 降级重试复用原始 deadline,mm_timeout_ms 较小时 RDMA 读失败会直接变成请求失败 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc:104
    • 建议:为降级重试预留独立的最小预算(例如 max(remaining, min_fallback_budget_ms),或在首轮 advertise RDMA 时按 margin 预扣一份 fallback 配额),并在预算不足而放弃 fallback 时上报可聚合的降级失败指标,使「RDMA 使小超时请求变差」在放量前可被发现。
  • gRPC deadline 解析逻辑在 proxy_router 与 vit_proxy_server 重复实现,并用 hasattr 做控制流分支 @ rtp_llm/multimodal/transport/proxy_router.py:21
    • 建议:把 deadline 解析抽成 transport 包内的单一辅助函数,供 proxy_router 与 vit_proxy_server 共同调用,并在注释中指向 C++ 侧 resolveRpcTimeoutMs 作为同源公式;用显式类型(Protocol 或 dataclass)替代 hasattr 分支。
  • proto_utils 用 setattr 搬运 7 个消息名,仅为支撑单一调用点 @ rtp_llm/flexlb/tools/online_eval/online_eval/proto_utils.py:74
    • 建议:改为在调用点直接从 multimodal_output_pb2 导入所需的具体消息(拆分后这是它们的正规位置),删除 setattr 搬运;若确需过渡期兼容,请把消息名列表连同「拆分完成后即删除」的条件写进注释,并补一个断言 7 个名字都可解析的用例。

Checklist Findings (23 fail / 51 total)

General Principles Checklist

  • [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue transport core 仍依赖 fat model_rpc_service.pb.h,弱化了 multimodal_output.proto 拆分的解耦目标
    本 PR 把多模消息拆到 multimodal_output.proto 并提供了精简的 multimodal_output_cc 目标(mm_rdma_exporter 已只依赖它),但 transport core 头文件仍 include 聚合的 model_rpc_service.pb.h(:13)(含全部引擎 RPC service 定义),mm_rdma_reader(transport/rdma/BUILD:8) 与 MMRdmaReader.cc:7 也仍依赖 model_rpc_service_cc_proto。结果是只需要 MultimodalInputsPB/MultimodalOutputPB 的传输层及其全部消费者(含测试目标)继续背上完整引擎服务定义的编译与链接闭包,拆分收益只落地了一半。
  • [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue pybind 导出的 C++ VitConfig 未同步 output_transport,pickle 会静默丢字段
    ConfigModules.h:354-359VitConfig 新增了 MMTransportConfig output_transport 成员(:356),但 pybind 注册处(ConfigInit.cc:2015-2027,本 PR 未改动)仍只有 .def_readwrite("vit_separation", ...),且 py::pickle 的 getter 只 make_tuple(self.vit_separation)、setter 只还原一个字段。因此任何经由该 pybind 类型往返(pickle / 跨进程传递)的 VitConfig 都会静默丢弃全部 transport 配置,退回结构体默认值(含 mode=auto 与 125s deadline)。当前主链路使用 Python 侧 py_config_modules.py::VitConfig,故暂未暴露,属新增字段未同步导出面的潜在陷阱。
  • [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue pybind 层与 multimodal transport 层形成包级双向依赖
    pybind/BUILD:36-46 新增 cc_library(name = "config_extract") 并设为 public visibility;multimodal_processor/transport/rdma/BUILD:29 把它列入 mm_rdma_exporter 的 deps,MMRdmaExporter.cc:7 直接 include rtp_llm/cpp/pybind/ConfigExtract.h;同时 pybind/BUILD:108-117mm_rdma_exporter_pybind 又反向依赖 transport/rdma:mm_rdma_exporter。结果是 pybind(Python 适配层)同时位于 transport 包的上游与下游,核心传输库被迫携带 pybind 适配职责。Bazel 目标级当前无环,但「绑定层」概念已泄漏进传输实现层,后续任何人在 ConfigExtract.h 追加依赖都可能造成真正的目标环。
  • [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue RDMA 成功路径每请求输出一条 INFO 级 [MM-RDMA-HIT] 日志
    MMRdmaReader.cc:140-141 在 RDMA 读成功后执行 RTP_LLM_LOG_INFO("[MM-RDMA-HIT] multimodal embedding read over rdma, %d slot(s)", ...)。这是数据面成功路径的逐请求日志,高 QPS 多模服务下会显著放大日志量,且其信息量(命中 + slot 数)本应由指标承载而非日志;同一文件的失败路径(:126-128、:134-136)才是需要 WARNING 的场景。
  • [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue 降级重试复用原始 deadline,mm_timeout_ms 较小时 RDMA 读失败会直接变成请求失败
    resolveRpcTimeoutMs(:9-20) 把整个请求预算定为 max(mm_timeout_ms) + rpc_timeout_margin_ms(margin 默认 5s),degradeToTerminal(:104-107) 在同一 budget 上再发起一次完整 ViT forward,并在 context.budget.exhausted() 时直接返回 "data-plane fallback exhausted request deadline"。若首轮 forward 已消耗掉大部分预算(例如 mm_timeout_ms=2000、首轮耗时 1.8s),RDMA 读失败后的 inline 重试几乎必然因预算不足而失败——即启用 RDMA 反而让本可成功的请求失败,而该分支目前也无测试覆盖。
  • [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue online_eval proto 加载改为硬依赖 rtp_llm 包可导入,且 rdma_transport 子包未打补丁
    _add_generated_package_paths(:116-126) 改为 importlib.import_module("rtp_llm.cpp") / ("rtp_llm.cpp.model_rpc.proto") 后改写其 __path__,而重构后的 proto_utils.py 全文已无任何 sys.path 写入(旧实现通过 sys.path.insert 导入扁平模块名,不依赖 rtp_llm 包)。online_eval 的入口脚本一律以 PYTHONPATH="${SCRIPT_DIR}" 启动,sys.path[0] 是工具目录而非仓库根,因此 import rtp_llm.cpp 只能依赖 rtp_llm 已装入 site-packages;未安装时 ensure_proto_modules() 的多个调用点会在首次调用即 ModuleNotFoundError。另 rtp_llm/cpp/rdma_transport/ 及其 proto/ 均无 __init__.py,`tensor_
  • [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue 降级重试复用原始 deadline,mm_timeout_ms 较小时 RDMA 读失败会直接变成请求失败
    resolveRpcTimeoutMs(:9-20) 把整个请求预算定为 max(mm_timeout_ms) + rpc_timeout_margin_ms(margin 默认 5s),degradeToTerminal(:104-107) 在同一 budget 上再发起一次完整 ViT forward,并在 context.budget.exhausted() 时直接返回 "data-plane fallback exhausted request deadline"。若首轮 forward 已消耗掉大部分预算(例如 mm_timeout_ms=2000、首轮耗时 1.8s),RDMA 读失败后的 inline 重试几乎必然因预算不足而失败——即启用 RDMA 反而让本可成功的请求失败,而该分支目前也无测试覆盖。
  • [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue RDMA 成功路径每请求输出一条 INFO 级 [MM-RDMA-HIT] 日志
    MMRdmaReader.cc:140-141 在 RDMA 读成功后执行 RTP_LLM_LOG_INFO("[MM-RDMA-HIT] multimodal embedding read over rdma, %d slot(s)", ...)。这是数据面成功路径的逐请求日志,高 QPS 多模服务下会显著放大日志量,且其信息量(命中 + slot 数)本应由指标承载而非日志;同一文件的失败路径(:126-128、:134-136)才是需要 WARNING 的场景。
  • [6.1] Software Engineering — DIP:高层策略不依赖非必要具体细节 → issue pybind 层与 multimodal transport 层形成包级双向依赖
    pybind/BUILD:36-46 新增 cc_library(name = "config_extract") 并设为 public visibility;multimodal_processor/transport/rdma/BUILD:29 把它列入 mm_rdma_exporter 的 deps,MMRdmaExporter.cc:7 直接 include rtp_llm/cpp/pybind/ConfigExtract.h;同时 pybind/BUILD:108-117mm_rdma_exporter_pybind 又反向依赖 transport/rdma:mm_rdma_exporter。结果是 pybind(Python 适配层)同时位于 transport 包的上游与下游,核心传输库被迫携带 pybind 适配职责。Bazel 目标级当前无环,但「绑定层」概念已泄漏进传输实现层,后续任何人在 ConfigExtract.h 追加依赖都可能造成真正的目标环。
  • [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue gRPC deadline 解析逻辑在 proxy_router 与 vit_proxy_server 重复实现,并用 hasattr 做控制流分支
    proxy_router.py:21-33_context_deadline_secondsvit_proxy_server.py:93-129_get_context_time_remaining_seconds / _resolve_context_deadline_seconds 是同一语义的两份实现(同样 min(max_timeout, context.time_remaining()) 再转 monotonic 截止点),二者必须与 C++ 侧 resolveRpcTimeoutMs 保持一致才不会出现客户端/代理预算错配,但没有任何共享点或一致性断言;两处都用 hasattr(context, "time_remaining")(proxy_router.py:23、vit_proxy_server.py:94)做控制流分支来兼容不同 context 对象形态,属运行时反射式判定,静态检查无法覆盖。
  • [6.1] Software Engineering — ISP:调用方不依赖无关大接口 → issue transport core 仍依赖 fat model_rpc_service.pb.h,弱化了 multimodal_output.proto 拆分的解耦目标
    本 PR 把多模消息拆到 multimodal_output.proto 并提供了精简的 multimodal_output_cc 目标(mm_rdma_exporter 已只依赖它),但 transport core 头文件仍 include 聚合的 model_rpc_service.pb.h(:13)(含全部引擎 RPC service 定义),mm_rdma_reader(transport/rdma/BUILD:8) 与 MMRdmaReader.cc:7 也仍依赖 model_rpc_service_cc_proto。结果是只需要 MultimodalInputsPB/MultimodalOutputPB 的传输层及其全部消费者(含测试目标)继续背上完整引擎服务定义的编译与链接闭包,拆分收益只落地了一半。
  • [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue proto_utils 用 setattr 搬运 7 个消息名,仅为支撑单一调用点
    proto_utils.py:74-83setattrmultimodal_output_pb2 的 7 个消息名(TensorPBMMPreprocessConfigPBMultimodalInputPBMultimodalInputsPBMMRdmaSlotPBMultimodalOutputPBReleaseLeasePB)逐个搬到 model_rpc_service_pb2 模块对象上,以维持 proto 拆分前的扁平访问形态,而实际只服务于单一调用点。这是运行时的模块级属性注入:静态检查与 IDE 无法解析,任何一侧增删消息都不会有编译期或测试期信号,且注入顺序与命名空间包 __path__ 补丁强耦合。
  • [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue Python 侧 MMOutputTransport.transfer 缺少异常护栏,后端抛异常会使请求失败而非降级到 inline
    base.py:104-113transfer 直接调用 backend.supports(...)backend.try_transfer(...),全程无 try/except;而 MMTransportBackend.try_transfer 的契约(:36-40) 只规定「不可服务时返回 None」,并未要求 no-throw。当前唯一候选后端 RdmaOutputBackend.try_transfer 内部做了宽泛捕获(rdma/backend.py:96),故现网暂时安全,但同一文件里的 release(:119-129) 与 close(:131-138) 都对后端调用做了 try/except 兜底,transfer 反而是唯一没有兜底的路径,与 C++ 侧 fetch 显式区分 success/retry/failure 的处理方式也不对称。
  • [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue cuda13 的 nvshmem 链接补丁未应用到同文件既有的 multimodal_processor_test
    本 PR 在 test/BUILD:6-14 引入 cc_import(cuda13_torch_nvshmem)cuda13_torch_link_deps,但只挂到两个新 target(:68、:93);同文件既有的 multimodal_processor_test(:21-43) 同样使用 torch_deps()(:28) 且以 TEST_USING_DEVICE=CUDA(:40) 跑 CUDA,却未追加该 select。对比 rtp_llm/cpp/model_rpc/test/BUILD 的做法是把 cuda13_torch_link_deps 拼进共享 test_deps,包内所有 cc_test 一致生效。当前写法会让该包在 --config=cuda13 下只有部分测试可链接。
  • [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue mm_processor_ 空指针拒绝逻辑与 PrefillRpcServer 逐字重复,且新分支无测试
    新增的 mm_processor_ == nullptr 分支(:171-177) 使用了与 PrefillRpcServer.cc:298-306 完全相同的 ErrorCode::MM_NOT_SUPPORTED_ERROR 与字面量 "multimodal inputs require a configured multimodal processor",两处仅日志前缀不同。Prefill 侧同语义分支已有测试覆盖,而本次新增的 LocalRpcServer::prepareInput 分支在 test/LocalRpcServerTest.cc 中没有对应用例(该测试文件未在本 PR 改动)。该分支还把「有 multimodal 输入但无 processor」从静默忽略改为显式报错,属对外错误语义变化。
  • [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue 新增的 GPU-NIC 亲和性解析与回退逻辑无任何单元测试覆盖
    本次新增 _physical_gpu_index(:525)、load_gpu_nic_affinity(:571)、_configure_gpu_nic_affinity(:615) 共约 130 行纯 Python 逻辑,分支包括 CUDA_VISIBLE_DEVICES 未设置/纯数字/GPU- UUID(经 nvidia-smi 反查)/其它形式/local_rank 越界、nvidia-smi 调用失败、UUID 未被报告、JSON 非法与非 dict、物理键与 rank 键双路查表。同模块已有 rtp_llm/config/test/server_config_setup_test.py 且有成熟的 mock.patch.dict(os.environ, ...) 模式,但其全部 test_* 均与本次改动无关(无一涉及 ACCL/affinity),该文件也不在本 PR 改动列表中——上一条「rank 兜底误绑」缺陷当前无法被任何测试发现。

RTP-LLM Checklist

  • [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue pybind 导出的 C++ VitConfig 未同步 output_transport,pickle 会静默丢字段
    ConfigModules.h:354-359VitConfig 新增了 MMTransportConfig output_transport 成员(:356),但 pybind 注册处(ConfigInit.cc:2015-2027,本 PR 未改动)仍只有 .def_readwrite("vit_separation", ...),且 py::pickle 的 getter 只 make_tuple(self.vit_separation)、setter 只还原一个字段。因此任何经由该 pybind 类型往返(pickle / 跨进程传递)的 VitConfig 都会静默丢弃全部 transport 配置,退回结构体默认值(含 mode=auto 与 125s deadline)。当前主链路使用 Python 侧 py_config_modules.py::VitConfig,故暂未暴露,属新增字段未同步导出面的潜在陷阱。
  • [I] 代码质量 — 同一功能用统一工具函数 → issue gRPC deadline 解析逻辑在 proxy_router 与 vit_proxy_server 重复实现,并用 hasattr 做控制流分支
    proxy_router.py:21-33_context_deadline_secondsvit_proxy_server.py:93-129_get_context_time_remaining_seconds / _resolve_context_deadline_seconds 是同一语义的两份实现(同样 min(max_timeout, context.time_remaining()) 再转 monotonic 截止点),二者必须与 C++ 侧 resolveRpcTimeoutMs 保持一致才不会出现客户端/代理预算错配,但没有任何共享点或一致性断言;两处都用 hasattr(context, "time_remaining")(proxy_router.py:23、vit_proxy_server.py:94)做控制流分支来兼容不同 context 对象形态,属运行时反射式判定,静态检查无法覆盖。

Python Static-First Checklist

  • [P.A] 静态结构与类型纪律 — 禁止 getattr/setattr literal 访问 → issue proto_utils 用 setattr 搬运 7 个消息名,仅为支撑单一调用点
    proto_utils.py:74-83setattrmultimodal_output_pb2 的 7 个消息名(TensorPBMMPreprocessConfigPBMultimodalInputPBMultimodalInputsPBMMRdmaSlotPBMultimodalOutputPBReleaseLeasePB)逐个搬到 model_rpc_service_pb2 模块对象上,以维持 proto 拆分前的扁平访问形态,而实际只服务于单一调用点。这是运行时的模块级属性注入:静态检查与 IDE 无法解析,任何一侧增删消息都不会有编译期或测试期信号,且注入顺序与命名空间包 __path__ 补丁强耦合。
  • [P.A] 静态结构与类型纪律 — 禁止 hasattr 做控制流分支 → issue gRPC deadline 解析逻辑在 proxy_router 与 vit_proxy_server 重复实现,并用 hasattr 做控制流分支
    proxy_router.py:21-33_context_deadline_secondsvit_proxy_server.py:93-129_get_context_time_remaining_seconds / _resolve_context_deadline_seconds 是同一语义的两份实现(同样 min(max_timeout, context.time_remaining()) 再转 monotonic 截止点),二者必须与 C++ 侧 resolveRpcTimeoutMs 保持一致才不会出现客户端/代理预算错配,但没有任何共享点或一致性断言;两处都用 hasattr(context, "time_remaining")(proxy_router.py:23、vit_proxy_server.py:94)做控制流分支来兼容不同 context 对象形态,属运行时反射式判定,静态检查无法覆盖。
  • [P.B] 错误处理 — 禁止 bare except 或静默吞异常 → issue npu_nic_affinity.json 按进程 CWD 相对路径读取,残留旧文件会被误读且失败仅记 info
    load_gpu_nic_affinity 执行 /usr/local/bin/run_affinity 后用 open("npu_nic_affinity.json")(:589) 从进程 CWD 读取结果并写入 ACCL_NIC_GPU_AFFINITY(:607),subprocess.run(:582-588) 既不指定 cwd=,也不做时间戳或内容新鲜度校验。若 CWD 残留上一次运行(甚至不同机型)的同名文件,而本次 run_affinity 未覆盖它却仍以 0 退出,旧拓扑会被静默采纳并影响 RDMA 网卡选择。失败分支用 except Exception 整体兜住且仅 logging.info(:599-605),未输出已捕获的 stderr,线上排障只能看到异常类型。本 PR 已为该函数新增 timeout 与 JSON 结构校验,并新增了 ViT 侧调用点,风险面扩大。
  • [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue 导出器测试绕过公共 pybind 边界,公开入口与 assembleMMRdmaOutput 的多数守卫仍无覆盖
    makeAdapter(:149-151) 直接构造 MMRdmaExporter(exporter, max_slot_bytes)(私有 2 参构造),用例随后调用私有 exportSlots(:163)。这依赖 test/BUILD:16-18test_copts-fno-access-control 才能编译(据此已剔除「编译失败」的误判),但代价是公开入口 exportEmbedding / release / py::object 配置构造完全无覆盖,enabled()==false 的短路路径与 pybind 参数转换也未被验证。同时 assembleMMRdmaOutput(MMRdmaReader.cc:18-69) 的多数守卫(重复 POS_ID、ROLE_UNSPECIFIEDsplit_total 不匹配、extra_inputs.size() 不匹配)仍无直接用例。
  • [P.G] 测试规范 — 数据驱动测试用 pytest.mark.parametrize → issue 新增的 GPU-NIC 亲和性解析与回退逻辑无任何单元测试覆盖
    本次新增 _physical_gpu_index(:525)、load_gpu_nic_affinity(:571)、_configure_gpu_nic_affinity(:615) 共约 130 行纯 Python 逻辑,分支包括 CUDA_VISIBLE_DEVICES 未设置/纯数字/GPU- UUID(经 nvidia-smi 反查)/其它形式/local_rank 越界、nvidia-smi 调用失败、UUID 未被报告、JSON 非法与非 dict、物理键与 rank 键双路查表。同模块已有 rtp_llm/config/test/server_config_setup_test.py 且有成熟的 mock.patch.dict(os.environ, ...) 模式,但其全部 test_* 均与本次改动无关(无一涉及 ACCL/affinity),该文件也不在本 PR 改动列表中——上一条「rank 兜底误绑」缺陷当前无法被任何测试发现。

Strengths

  • 协议违规与可降级失败被严格区分:MMRemoteOutputTransport.cc:75-82 对未 advertise 却返回 RDMA 回执的 ViT 直接 discard() 归还远端 lease 并报协议错误,而非降级——因为该回执的内联张量字段为空,降级会解出「看似合法但错误」的 embedding;测试对这一版本偏斜场景有专门用例。
  • 数据面抽象职责清晰:MMReceiptReader(可降级候选)与 MMTerminalReceiptReader 分离,把「降级只花一次额外 ViT forward」固化到接口层;MMRemoteOutputTransportTest.cc:218-234h.log 断言完整事件序列 {request, read, release, request, terminal} 并校验第二轮 support_rdma 已撤回,是行为序列断言而非仅比对返回值。
  • 内联解码从 RTP_LLM_CHECK_WITH_INFO(在 FT_CORE_DUMP_ON_EXCEPTION 下会 abort 进程)改为返回 ErrorInfo,把远端畸形数据从「进程崩溃」降级为「请求失败」,且测试用真实的 createGrpcInlineReceiptReader()(非 fake)守卫这条路径。
  • 租约生命周期用 RAII SlotLease 管理:成功路径 releaseAsync() 不阻塞消费者,失败/异常路径析构同步释放;队列有界(kMaxPendingReleaseHandles=1024),饱和时退化为同步发送而非丢弃;releaseAsync 三条分支均无 use-after-move;releaseLoop:172-184 按 endpoint 合并成单次批量 RPC。
  • 回滚语义被认真对待:RdmaExport::createBatchRdmaTransport.cc:50-64)在任一 group 失败时释放此前全部 lease 并返回空,测试用 RecordingExport::fail_at 精确断言;Python 侧 _roll_backrdma/backend.py:151)在 descriptor 解析失败时主动归还已解析 lease,而非一律等 slot_gc_timeout_ms 兜底。
  • 开源/无 provider 构建的降级路径完整:rdma_transport_no_impl 返回 nullptr,MMRdmaReader 仍注册(只有它能识别 RDMA 回执并归还 slot),advertise() 返回 false,std::call_once 保证初始化失败只告警一次,且注释明确说明该路径不持 GIL、不能用 py::gil_scoped_release
  • 启动期 fail-fast 前移:LocalRpcServer.cc:64-74 把 ingress 归属判定放到 NormalEngine 构造之前,非法组合返回 FAILED_PRECONDITION 并终止启动;prepareInput:170-177 消除了「有 multimodal 输入但被静默丢弃」的旧行为。
  • 依赖切分有意为之且注释到位:RdmaConfig.h/MMTransportMode.h 拆为最小 cc_library 供多方共享;multimodal_pb_converter 单独成 target 以打破 model_rpc↔multimodal_processor 依赖环;mm_processor_config 标注 Torch-free 并使 mm_processor_config_test 无需 GPU 节点。
  • 测试按「是否需要 torch/GPU」正确分层:mm_processor_config_test 不链 torch、不带 exec_properties;另两个测试用注释交代「断言是 CPU-only,但链接 torch 会拉入 libtorch_cuda」的调度约束,降低后续误删 exec_properties 的概率。
  • 传输重构完整保留 rtp_llm_vit_rpc_client_error_qps / _rt_us / _request_bytes / _response_bytes 四个指标名(MMRemoteOutputTransport.cc:37,50-52),kmonitor 看板无需迁移;ViT 侧 report_output_metrics 新增 transport tag 以区分数据面。

Comment thread rtp_llm/cpp/model_rpc/proto/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 #1353 (non-blocking suggestions)

37 条 P2/P3 建议,不阻塞合并。阻塞判定与完整摘要见上一条 review。

cfg.vit_separation = static_cast<VitSeparation>(vit_config.attr("vit_separation").cast<int>());
cfg.output_transport = extractMMTransportConfig(vit_config.attr("output_transport"));
const int64_t configured_timeout_ms = vit_config.attr("mm_timeout_ms").cast<int64_t>();
const int64_t worker_timeout_ms = configured_timeout_ms > 0 ? configured_timeout_ms : 120 * 1000;

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] MM transport 契约常量在 Python/C++ 五处各自硬编码,margin 仅 C++ 侧可配

同一组超时语义分散在五处:ConfigExtract.h:47120 * 1000py_config_modules.py:270DEFAULT_MM_TIMEOUT_MS = ******ConfigModules.h:349,351125 * 1000 / 5 * 1000MMRemoteOutputTransport.h:20-21kDefaultVitRpcTimeoutMs / kVitRpcTimeoutMarginMs。Python MMTransportConfig(py_config_modules.py:289-293) 只有 mode/control/rdmaextractMMTransportConfig(ConfigExtract.h:31-37) 也只读这三项,因此 rpc_timeout_margin_ms 无任何 Python/env/CLI 入口,是伪装成配置项的常量;而 py_config_modules.py:266 的注释仍宣称与 C++ `...

建议: 收敛为单一事实来源:在 ConfigModules.h 定义 kDefaultMMTimeoutMs 与 margin 具名常量,default_rpc_timeout_msConfigExtract.h:47 的兜底值与 MMRemoteOutputTransport.h 的两个常量都引用它;Python MMTransportConfig 补上 rpc_timeout_margin_ms 并在 vit_group_args.py 增加对应 arg/env,由 ConfigExtract 只做透传。同时修正 py_config_modules.py:266 已被违反的对齐注释,并补一条跨语言默认值一致性断言测试防止漂移。

return cfg;
}

inline VitConfig extractVitConfig(const py::object& vit_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.

[P2] extractVitConfig 未被 embedding 入口复用,该路径 deadline 被隐式钉在 125s

LLM 路径已改为 extractVitConfig(vit_config)RtpLLMOp.cc:215),并用 mm_timeout_ms + rpc_timeout_margin_ms 覆写 default_rpc_timeout_msConfigExtract.h:44-48);但同一 th_transformer_lib 目标下的 embedding 引擎入口仍保留被替换掉的手写提取(RtpEmbeddingOp.cc:51-53),只取 vit_separation。其 RemoteMultimodalProcessor 走 3 参构造 → grpcOnlyTransportConfig()default_rpc_timeout_ms 保持结构体默认的 125s,而 resolveRpcTimeoutMs 仅在单请求 mm_timeout_ms > 0 时用请求值。于是 embedding 服务把 --mm_timeout_ms 调大(长视频场景)时请求仍在 125s 被截断;后续给 `extractVit...

建议: 让 embedding 入口也调用 extractVitConfig,随后在结果上显式覆写 output_transport.mode = kMMTransportModeGrpc(保留现有意图注释),把「embedding 路径固定走 inline gRPC」表达成一次显式覆盖而非一份不再演进的副本。同时确认改动前该路径是否本来没有 deadline——若是,则 125s 截断属行为变更,需在发布说明中列出。

public:
virtual ~RdmaExport() = default;
// Returns an empty descriptor (lease_id is empty) when export fails.
virtual RdmaDescriptor create(const std::vector<torch::Tensor>& tensors) = 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.

[P2] rdma_transport 扩展点未声明内存所有权、返回顺序、条目数量与 lease_id 唯一性契约

RdmaExport::create(:58-59) 的注释只有一句 "Returns an empty descriptor (lease_id is empty) when export fails.",未声明:(1) 入参 tensor 的内存所有权——rdma/backend.py:88-95emb/pos/extras 都是函数局部引用,export_embedding 返回后即可被 CUDA caching allocator 复用,而 LLM 侧要到收到 receipt 后才发起 RDMA read;MMRdmaExporter.cc 还会用 narrow 生成共享底层存储的视图。(2) RdmaRead::read(:68) 未声明返回张量必须与 descriptor 展平后顺序严格一致,而 assembleMMRdmaOutput 按下标把 roles[i]mm_tensors[i] 配对;顺序错乱且各 chunk 形状恰好相同时 torch::cat 不报错,会产出行序错误但形状合法的 embed...

建议:RdmaExport::createRdmaRead::read 的声明处逐条写明契约:入参内存所有权与保活责任(推荐「实现必须拷贝进自有已注册 slot,调用方无需保活」)、返回条目数量与顺序必须与入参严格对应、lease_id 唯一性与释放幂等性。若结论是需要调用方保活,则 RdmaOutputBackend 必须把 emb/pos/extras 的引用与 lease 一起持有到 release();若是拷贝语义,也请在 try_transfer 处加一行注释固定该前提。

Comment thread rtp_llm/cpp/rdma_transport/RdmaTransport.cc Outdated
ErrorResult<MultimodalOutput> MMRemoteOutputTransport::degradeToTerminal(const std::string& endpoint,
MultimodalInputsPB& request_pb,
DeliveryContext& context,
const std::string& /*reason*/) {

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] RDMA 降级原因被丢弃且两个 metrics_ 成员从未使用,数据面回退率不可观测

MMRdmaReader.cc:15-16 定义了 kReasonRdmaReadError/kReasonRdmaManifestError 并通过 ConsumeResult::retry(reason) 上报,MMRemoteOutputTransport.cc:92 取出 result.reason() 传入 degradeToTerminal,但 :100 的形参写成 const std::string& /*reason*/ 被直接丢弃。全仓检索 metrics_ 确认:MMRemoteOutputTransport.h:212MMRdmaReader.h:48 的成员仅出现在构造函数初始化列表中,两个 .cc 零读取,只有 MMGrpcTransport.cc 真正使用 metrics。结果是 RDMA 读失败、manifest 不一致、协议违规三类事件只有 WARNING 日志,无法量化降级率与原因分布,也就无法为 mm_transport_mode 回滚到 grpc 提供判据。

建议:degradeToTerminalfetch 的协议违规分支调用 metrics_->reportRpcClientError(endpoint, reason)(reason 已是稳定枚举字符串,可直接作 tag),或新增 rtp_llm_vit_rdma_degrade_qps 并为「未 advertise 回执」补一条独立 reason;MMRdmaReader::metrics_ 建议用于上报 RDMA 读耗时与失败计数。新数据面放量前,回退率指标是前置条件;若确认不需要,则应删除两个未使用成员与 ConsumeResult::reason(),避免留下「看起来已埋点」的假象。

Comment thread rtp_llm/async_decoder_engine/rpc_engine.py

test_copts = [
"-fno-access-control",
] + copts()

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/multimodal_processor/test/BUILD:28(不在 diff 展示范围内,就近挂载)

[P3] cuda13 的 nvshmem 链接补丁未应用到同文件既有的 multimodal_processor_test

本 PR 在 test/BUILD:6-14 引入 cc_import(cuda13_torch_nvshmem)cuda13_torch_link_deps,但只挂到两个新 target(:68、:93);同文件既有的 multimodal_processor_test(:21-43) 同样使用 torch_deps()(:28) 且以 TEST_USING_DEVICE=CUDA(:40) 跑 CUDA,却未追加该 select。对比 rtp_llm/cpp/model_rpc/test/BUILD 的做法是把 cuda13_torch_link_deps 拼进共享 test_deps,包内所有 cc_test 一致生效。当前写法会让该包在 --config=cuda13 下只有部分测试可链接。

建议:cuda13_torch_link_deps 一并加到 multimodal_processor_test 的 deps,或按 model_rpc/test/BUILD 的方式抽出包内共享 test_deps 列表,使同包 target 的链接修补一致生效。

Checklist: [6.1] 分布式/跨平台变更有对应覆盖

for (auto& reader : readers_) {
reader->withdraw(request_pb);
}
if (context.budget.exhausted()) {

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] 降级重试复用原始 deadline,mm_timeout_ms 较小时 RDMA 读失败会直接变成请求失败

resolveRpcTimeoutMs(:9-20) 把整个请求预算定为 max(mm_timeout_ms) + rpc_timeout_margin_ms(margin 默认 5s),degradeToTerminal(:104-107) 在同一 budget 上再发起一次完整 ViT forward,并在 context.budget.exhausted() 时直接返回 "data-plane fallback exhausted request deadline"。若首轮 forward 已消耗掉大部分预算(例如 mm_timeout_ms=2000、首轮耗时 1.8s),RDMA 读失败后的 inline 重试几乎必然因预算不足而失败——即启用 RDMA 反而让本可成功的请求失败,而该分支目前也无测试覆盖。

建议: 为降级重试预留独立的最小预算(例如 max(remaining, min_fallback_budget_ms),或在首轮 advertise RDMA 时按 margin 预扣一份 fallback 配额),并在预算不足而放弃 fallback 时上报可聚合的降级失败指标,使「RDMA 使小超时请求变差」在放量前可被发现。

Checklist: [6.1] 回滚路径:风险行为存在运维回滚手段;[6.1] 错误语义:fail-fast/retry/fallback/silent 行为显式

HANDLE_ROUTE_GC_SAFETY_SECONDS = 5.0


def _context_deadline_seconds(context, max_timeout_seconds: float) -> Optional[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.

[P3] gRPC deadline 解析逻辑在 proxy_router 与 vit_proxy_server 重复实现,并用 hasattr 做控制流分支

proxy_router.py:21-33_context_deadline_secondsvit_proxy_server.py:93-129_get_context_time_remaining_seconds / _resolve_context_deadline_seconds 是同一语义的两份实现(同样 min(max_timeout, context.time_remaining()) 再转 monotonic 截止点),二者必须与 C++ 侧 resolveRpcTimeoutMs 保持一致才不会出现客户端/代理预算错配,但没有任何共享点或一致性断言;两处都用 hasattr(context, "time_remaining")(proxy_router.py:23、vit_proxy_server.py:94)做控制流分支来兼容不同 context 对象形态,属运行时反射式判定,静态检查无法覆盖。

建议: 把 deadline 解析抽成 transport 包内的单一辅助函数,供 proxy_router 与 vit_proxy_server 共同调用,并在注释中指向 C++ 侧 resolveRpcTimeoutMs 作为同源公式;用显式类型(Protocol 或 dataclass)替代 hasattr 分支。

Checklist: [6.1] DRY:重复非平凡逻辑被抽取或显式复用;[P.A] 禁止 hasattr 做控制流分支;[I] 同一功能用统一工具函数

Comment thread rtp_llm/flexlb/tools/online_eval/online_eval/proto_utils.py Outdated
@ydshi0
ydshi0 force-pushed the feature/rdma_transport branch 2 times, most recently from 7b59d16 to ad6e39f Compare August 31, 2026 09:11
@rtp-llm-review-bot

rtp-llm-review-bot commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

PR #1353 第 12 轮评审 — BLOCKED(1)

  • 标题:feat: add RDMA transport for ViT output(@ydshi0,open)
  • head 2702a57fb971 · base f1a54a5f1171 · delta 10 文件 · discover 5/5 成功

阻塞项(P0/P1,support≥3 且过全部门)

[P1] generate_grpc_proto 新增 mandatory proto_srcs 后 flexlb_schedule_service_py 未传该属性导致构建失败

  • rtp_llm/cpp/model_rpc/proto/BUILD:85 · 类别 build · 票数 5/5 · 首见 r6
  • 依据:py_proto.bzl 将 proto_srcs 声明为 attr.label(allow_files=True, mandatory=True),而 rtp_llm/cpp/model_rpc/proto/BUILD:85-89 的 generate_grpc_proto(name="flexlb_schedule_service_py") 仅传 proto 与 create_grpc_proto,未传 proto_srcs(补丁只给 model_rpc_service_py 与 multimodal_output_py 补了 proto_srcs,未改动 flexlb_schedule_service_py)。依赖链已核实:BUILD:92-93 的 flexlb_schedule_service_py_proto 依赖该目标,rtp_llm/server/BUILD:21 的 server 又依赖 flexlb_schedule_service_py_proto。Bazel 分析 flexlb_schedule_service_py 时会报 missing value for mandatory attribute 'proto_srcs',导致 //rtp_llm/server:server 构建失败。
  • 建议:为 flexlb_schedule_service_py 补上 proto_srcs = ":flexlb_schedule_service_proto_srcs"(或将该 attr 改为非 mandatory,缺省时仅以 proto_file 作为 inputs)。
  • 事实核验:hold — verified(py_proto.bzl 将 proto_srcs 声明为 mandatory=);verified(flexlb_schedule_service_py 目标未传 proto_sr);verified(补丁只给 model_rpc_service_py 与 multimodal_o);verified(依赖链闭合:flexlb_schedule_service_py_proto 依);verified(该目标在生产构建中可达(server 是实际构建目标,会触发对 flexlb_s);verified(Bazel 分析 flexlb_schedule_service_py 时会因 )

非阻塞发现(并集报告:单票也保留,降级不丢弃)

  • [P2] load_gpu_nic_affinity 的 subprocess.run 无 timeout,run_affinity 挂起会阻塞整个 ViT server 启动 — rtp_llm/config/server_config_setup.py:534 · 票数 5/5
  • [P2] _resolve_rpc_timeout_seconds 将未设置 timeout 的输入按 default 参与 max,混合请求的 RPC 超时被抬高到默认值 — rtp_llm/server/vit_proxy_server.py:82 · 票数 1/5
  • [P3] resolveRpcTimeoutMs 对 configured_timeout_ms + rpc_timeout_margin_ms 存在 int64 有符号溢出 — rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc:16 · 票数 2/5
  • [P3] load_gpu_nic_affinity 在所有 ViT server 启动路径无条件调用,可能与 RDMA 可选设计冲突 — rtp_llm/start_server.py:335 · 票数 1/5
  • [P3] MMReceiptReader::discard 默认实现为空,未覆写的 reader 在未广告数据面路径会泄漏远端 slot — rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.h:175 · 票数 1/5
  • [P3] ACCL_NIC_GPU_AFFINITY 被设置为空字符串后,后续 is not None 检查误判为已加载 — rtp_llm/config/server_config_setup.py:560 · 票数 1/5
  • [P3] setup_cuda_device_and_accl_env 在 EngineConfig.create 之后调用,CUDA 设备设置可能过晚且可能与父进程加载的 ACCL 亲和性冲突 — rtp_llm/multimodal/vit_start_server.py:37 · 票数 1/5

已确认修复(recheck)

  • [P1] mm_rdma_read_timeout_ms 配置未在 RDMA 读取路径生效,挂起的 read 会耗尽整个请求 deadline 并阻断 inline 回退 — MMRdmaReader.cc 第 173-178 行已新增 read_timeout_ms = std::min(remaining, rdma_config_->read_timeout_ms),并作为 reader_->read 的超时参数,读路径已生效。
  • [P2] lazy RDMA reader 初始化异常会沿 advertise 传播,可能使请求路径抛异常 — ensureReader 在 MMRdmaReader.cc 第 219-229 行用 try/catch 包裹 createRdmaRead,捕获 std::exception 和未知异常后 reset 并返回 false,advertise 不再因初始化异常向外抛 C++ 异常。
  • [P3] qp_count 从 Python int 直接 cast 到 uint32_t,缺少范围校验 — 文件 rtp_llm/cpp/config/MMTransportConfigExtract.h 在当前 head 不存在,原发现的 qp_count cast 代码已随文件删除而消失。
  • [P3] _physical_gpu_index 用 startswith 匹配 GPU UUID,可能误匹配前缀 — _physical_gpu_index 函数已在本轮删除,当前 setup_cuda_device_and_accl_env 第 589-591 行直接按 str(local_rank) 查表,不再调用 startswith 匹配 GPU UUID。
  • [P2] RdmaCircuitBreaker 失败计数在熔断窗口过期后不重置,首次熔断后单次失败即重新熔断 — 本轮改动已整体删除 RdmaCircuitBreaker 的 table/mutex/open/recordFailure/recordSuccess 实现及 advertise/consume 中的 circuit_ 调用(见 diff 删除块与当前文件第 97–143 行),相关失败计数与熔断逻辑不再存在。
  • [P3] release 在 RPC 成功前就 pop 路由,瞬时失败/超时后路由丢失且无法重试 — 当前 release 在锁内仅 get 路由(第 110 行),并在 stub.ReleaseRdmaLease 成功后才在锁内按 selected_routes 等值校验后 pop(第 164–167 行),失败、超时或 deadline 耗尽路径不再提前删除路由。
  • [P2] model_rpc_service 的 cc protodeps 与 py proto_deps 对 tensor_rdma 的依赖不对称 — 当前 BUILD 全文无 multimodal_output 目标,也未出现 tensor_rdma/protodeps;第4-11行 model_rpc_service 的 tf_proto_library_cc 无 protodeps,该不对称依赖已不存在。
  • [P2] tf_proto_library_cc 的 protodeps 疑似引用库目标而非 genproto 目标 — 当前第4-11行 tf_proto_library_cc 已无 protodeps 参数,且文件中不存在 multimodal_output 目标,问题依赖已移除。
  • [P3] MMRdmaReader 的 metrics_ 成员被构造却从未使用,RDMA 读失败缺少直接指标上报 — MMRdmaReader.h:27-28 的两个构造函数已不再接收/初始化 metrics_,且 49-51 成员列表中已无 metrics_,原先的死代码成员已移除。
  • [P3] row_bytes 用 tensorBytes/rows 计算,非整行对齐时按行切块可能产生跨行不完整 chunk — 第 61-67 行已有 alignUp(row_bytes, kRdmaSlotAlign) > max_slot 判断并 return false,max_slot < kRdmaSlotAlign(含 [256, align))时该条件恒成立,会回退 bytes 而不会产生超限 chunk。
  • [P2] rdma/control 配置为 None 时 extractVitConfig 抛异常导致 init 崩溃 — 发现针对的文件 rtp_llm/cpp/config/ConfigExtract.h 在当前 head 已不存在,相关代码被删除,问题不再成立。
  • [P3] roleTypeName 与 RoleTypes.h 已有的 roleTypeToString 功能重复 — 本轮改动删除了 MMProcessorConfig.h 中重复的 roleTypeName 函数,并在第 67、92 行改用 RoleTypes.h 的 roleTypeToString,重复映射已消除。
  • [P2] backend.py 从 multimodal_output_pb2 导入 MultimodalInputsPB/MultimodalOutputPB,疑似应为 model_rpc_service_pb2 — 当前代码第7-11行已将导入源改为 model_rpc_service_pb2,并从中导入 MMRdmaSlotPB、MultimodalInputsPB、MultimodalOutputPB,与发现建议一致。
  • [P3] tensor_rdma.proto 未注入 java_package,Java 生成类落在 rdma_transport 包,与其余 proto 的 org.flexlb.engine.grpc 不一致 — 当前 pom.xml 第 106-113 行的 copy/replaceregexp 仅处理 engine_rpc_service.proto 与 flexlb_schedule_service.proto,已不再复制 tensor_rdma.proto,因此该文件缺失 java_package 注入的问题在当前构建配置中已不存在。
  • [P3] inputs 新增 proto_srcs 但 create_grpc_proto 的 arguments 未携带依赖 proto 路径 — 当前代码第20行 inputs = [proto_file] 且 attrs(第36-46行)无 proto_srcs,原描述中新增 proto_srcs 导致传递依赖 pb2 未声明输出的状态已不存在。
  • [P1] proto_srcs 用 attr.label(allow_files=True) 会拒绝 filegroup 目标导致构建失败 — 当前 bazel/py_proto.bzl 的 generate_grpc_proto 规则已无 proto_srcs 属性,仅在第37-40行定义 proto=attr.label(allow_single_file=[".proto"]),原 attr.label(allow_files=True) 拒绝 filegroup 的问题已不存在。
  • [P3] libmm_rdma_exporter_so 直接加入 data 而其他库走 copy 规则,打包方式不一致 — whl_package_libs 的 data(第 58–64 行)现全部为直接 so 目标(:libmm_rdma_exporter_so、:librtp_compute_ops_so 等),不再混用 copy_* 规则目标,原打包方式不一致已消除。
  • [P2] _resolve_context_deadline_seconds 将 grpc time_remaining() 返回的 timedelta 误判为非 Real,导致 status check 的客户端 deadline 恒被忽略 — 当前 head 第116-123行的 _resolve_status_check_deadline_seconds 已删除原 isinstance(context_remaining_s, numbers.Real) 判断,直接使用 _get_context_time_remaining_seconds 的返回值参与 min(timeout_s, context_remaining_s),timedelta 被误判为 None 的问题已消除。
  • [P2] _ensure_proto_modules 的 module_name 参数被忽略,调用方传入的模块名不再决定生成物路径与导入名 — 当前代码中 _ensure_proto_modules 在第55-56行用 module_name 生成 pb2_path/grpc_path,第64-65行用 module_name 拼接导入名,module_name 已实际参与路径与导入名计算,问题不再成立。
  • [P3] GrpcMMControlClient 析构时同步 drain 未发送的 release,可能阻塞进程退出至 release_timeout_ms — 析构函数(第35-53行)已改为清空 release_queue_ 并仅记录 abandoned_handles,不再同步 drain;releaseLoop 在每批 releaseNow 前检查 stopping_(第183-189行),join 最多等待一个在途 RPC,不再批量阻塞 release_timeout_ms。

本轮 KPI

{
 "reps": 5,
 "agentic_reps": 2,
 "diff_truncated": false,
 "patch_bytes": 49090,
 "coverage": 1.0,
 "shards": 1,
 "discover_ok": 5,
 "discover_fail": 0,
 "discover_attempts": 5,
 "clusters": 7,
 "synth_fail": 0,
 "confirm": {
  "candidates": 0,
  "to_block": 0,
  "confirm_fail": 0
 },
 "blocking_dedup_merged": 0,
 "fact_check": {
  "checked": 0,
  "hold": 0,
  "refuted": 0,
  "parse_fail": 0
 },
 "model": "whale/DeepSeek-V4-Pro-0813",
 "round_index": 12,
 "delta_files": 10,
 "full_files": 65,
 "new_findings": 6,
 "merged": 1,
 "recheck": {
  "fixed": 2,
  "still_open": 35,
  "cannot_substantiate": 3,
  "fail": 0
 },
 "blocking": 1,
 "blocking_on_unchanged": 0
}

本报告由 rtp-llm-agent-platform 自动生成,同一 PR 的后续轮次就地更新同一条评论。


rtp-llm-agent-platform review · 第 12 轮 · head 2702a57fb971 · BLOCKED(1) · 同一 PR 的结论就地更新这一条评论

@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 #1353

Status: BLOCKING

Summary: P0/0 · P1/1 · P2/20 · P3/11

Reviewed: commit ad6e39f0af4d · 2026-08-31 18:42 UTC+8

Blocking Issues

P1

  • ViT 侧 RDMA 导出未建立 CUDA 流序,LLM 可能读到未完成计算的显存 @ rtp_llm/multimodal/transport/rdma/backend.py:95
    • 建议:在 export_embedding 之前显式建立可见性:对 emb.devicetorch.cuda.current_stream(emb.device).synchronize(),或在 MMRdmaExporter::exportSlots 内插入 event 并等待。若同步点确实由 build-selected provider 的 create() 承担,请在 RdmaTransport.h:59create 声明处把「调用方已保证张量内容对 NIC 可见」写成硬契约,并在 PR 描述中说明同步点落在哪一层,否则任何新 provider 都会重犯。

Non-blocking Suggestions

P2

  • RDMA 持续故障时缺少熔断,每请求放大为两次 ViT forward 且不自愈 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:113
    • 建议:1) 给 MMRdmaReader 增加滑动窗口连续失败计数与冷却半开重试,达阈值后 advertise 直接返回 false,使故障态收敛为单次 forward。2) 为降级重试保留独立预算(或在 resolveRpcTimeoutMs 之外给 fallback 追加一份配额),避免数据面故障把本可成功的请求变成失败请求。3) 熔断落地前建议把 mm_transport_mode 默认值改为 grpc,由部署显式开启 auto
  • 降级原因被丢弃、reader 未注入 metrics,数据面回退率完全不可观测 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc:100
    • 建议:把 MMTransportMetricsPtr 注入 reader 与 DeliveryContext,用 ConsumeResult::reason() 打点降级 QPS(rdma_read_error/rdma_manifest_error/unadvertised_receipt),并把 [MM-RDMA-HIT] 的每请求 INFO 改为命中计数指标(或降到 DEBUG);同时给 releaseNow 的失败分支补 reportRpcClientError,与 kReasonReleaseQueueFull 的既有做法保持一致。
  • RdmaExport/RdmaRead 未声明内存所有权与返回顺序契约,在库 fake 拷贝数据反而掩盖该要求 @ rtp_llm/cpp/rdma_transport/RdmaTransport.h:59
    • 建议:在 create/read 声明处补明确契约注释:create 必须持有传入张量的存储引用直至 lease 被 release 或 TTL 回收(并说明 release 后 remote_addr 何时失效),read 必须按 descriptor 与 manifest 顺序返回等量 tensors;或把 create 改为按值接收 std::vector<torch::Tensor> 以在类型上表达保活意图。同时在 readAllSlots 逐个比对 result.tensors[i]descriptor.tensors[i] 的 dtype/shape,使顺序错配从静默损坏变为可检测的 retry,并补一个只持引用、不拷数据的 fake。
  • RDMA 描述符反序列化不校验尺寸一致性与上限,与导出侧 max_slot_bytes 不对称 @ rtp_llm/cpp/rdma_transport/RdmaTransport.cc:90
    • 建议:在 rdma_transport::fromProto 把上述一致性检查作为解析失败条件返回 false(现有调用点已按 false 走 inline 回退,无需新增分支);并在 MMRdmaReader::readAllSlotsrdma_config_->max_slot_bytes 对单 slot payload_bytes 与 slot 总数做上限校验,超限即拒绝并降级,使读侧与导出侧的尺寸约定对称。
  • extractVitConfig 未被 embedding 入口复用,output_transport 与 mm_timeout_ms 在该路径被静默忽略 @ rtp_llm/cpp/pybind/ConfigExtract.h:39
    • 建议:让 RtpEmbeddingOp::init 同样调用 extractVitConfig;若确需把 embedding 路径钉在 grpc 数据面,则显式覆盖 mode = kMMTransportModeGrpc 而保留超时等其余字段。若决定冻结该路径,请在 grpcOnlyTransportConfig() 处补注释与 WARNING 日志,明确「用户超时配置在此路径不生效、固定为 125s」,避免运维按配置预期排查超时。
  • releaseAsync 在队列饱和或停机时退化为同步 RPC,违反基类"不得延迟消费者"契约 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:105
    • 建议:保持 releaseAsync 严格非阻塞:饱和时按「丢弃最旧批次 + 计数指标(依赖 encoder 侧 TTL GC 兜底)」处理,或扩大/分片队列,而不是把 RPC 拉回调用线程;若确要保留同步兜底,请限制在 stopping_ 分支,并在基类注释中显式放宽契约、写明最坏阻塞时长。同时把日志与打点移出持锁区,并把 kMaxPendingReleaseHandles 提升为与 MMControlConfig 同源的可配置项。
  • 析构排空未按 endpoint 合批且无总预算,进程关停耗时不可控 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:49
    • 建议:把 releaseLoop 的按 endpoint 合并逻辑抽成私有 drain(std::deque<ReleaseTask>&&) 供两处复用,并为析构 drain 设置单一总预算(如 2~3s 或复用 DeadlineBudget),超时即放弃剩余批次并以 WARNING 记录放弃条数,以有界时间换取可预期的停机时长。
  • RDMA 读失败路径同步 release,阻塞请求线程并侵蚀降级预算 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:95
    • 建议:失败路径同样走 releaseAsync(远端 slot GC 已是 backstop,不需要在请求线程上确认释放);若确要保留同步语义,请给它一个独立的、不从请求 budget 扣减的小额超时,并在注释中说明为何失败路径比成功路径更需要同步确认。
  • config_extract 位于 pybind 层却被 transport 数据面反向依赖,形成包级双向依赖 @ rtp_llm/cpp/pybind/BUILD:37
    • 建议:把 ConfigExtract.h 下沉到 rtp_llm/cpp/config/(如新增 config:py_config_extract,其实际依赖只有 pybind11 + ConfigModules.h + MMTransportMode.h),或让 MMRdmaExporter 只保留 const RdmaConfig& 构造函数、把 py::object 重载移入 MMRdmaExporterInit.cc。同时把 namespace py = pybind11; 收进 namespace rtp_llm 内部。
  • exporter 惰性构造在请求线程内持 GIL 完成 RDMA 初始化,且一次失败即进程内永久闭锁 @ rtp_llm/multimodal/transport/rdma/backend.py:67
    • 建议:把 provider 构造移到 create_mm_output_transport 的服务启动路径(启动期阻塞可接受,失败可立即降级并打印一次),或在 MMRdmaExporter 构造函数中先用 GIL 抽完 config、再用 py::gil_scoped_release 包住 createRdmaExport;同时为「RDMA 不可用/初始化失败」补一个 kmonitor 计数,让线上能区分「未启用」与「启用后降级」。
  • GPU-NIC 亲和键语义由 local rank 改为物理 GPU index,无单测、无回滚开关、兜底日志会误导 @ rtp_llm/config/server_config_setup.py:639
    • 建议:在已存在的 rtp_llm/config/test/server_config_setup_test.py(已有 patch.dict(os.environ) 基础设施)补参数化单测,mock subprocess.run 返回构造好的 index,uuid 文本:覆盖未设 CUDA_VISIBLE_DEVICES"4,5,6,7" + local_rank=0 命中 affinity["4"]、UUID 命中/未命中、local_rank 越界返回 None、ACCL_USE_NICS 已设时保持不变、affinity 非 dict/空 dict/NIC 空串。同时提供强制 rank-only 查表的环境开关作为线上回滚手段,在 :642 回退分支单独打 WARNING,并把 :654 的成功日志改为区分「按物理 GPU 命中」与「按 local_rank 兜底」。
  • 亲和性产物按进程 CWD 相对路径读取,异常捕获过宽且失败仅记 info @ rtp_llm/config/server_config_setup.py:589
    • 建议:改为在 tempfile.TemporaryDirectory() 内以 cwd= 运行 run_affinity 并按该目录下的绝对路径读取,消除进程间竞争与工作目录污染;把异常拆成「执行失败」与「内容解析失败」两类分支,各自给出准确 message、提升到 WARNING 并输出捕获的 stderr(补 text=True);再增加显式开关(如 ACCL_AUTO_NIC_AFFINITY)作为运维回滚手段。
  • online_eval proto 加载新增对 rtp_llm 包的硬依赖,并对真实包 path 做全局副作用 @ rtp_llm/flexlb/tools/online_eval/online_eval/proto_utils.py:64
    • 建议:在 proto_utils.py 内把已存在的 REPO_ROOT(:14)显式插入 sys.path 后再 import_module(或先 try/except 给出「请在仓库根或安装了 rtp_llm 的解释器下运行」的明确错误);并把 _add_generated_package_paths 的作用范围收敛为「把生成目录加到 sys.path 让命名空间包自然合并」或在函数末尾恢复原 __path__,避免对共享包做不可逆改写。让调用点直接 import multimodal_output_pb2 后即可删除 setattr 兼容层。
  • 新 .so 无条件进入全平台构建,与 libth_transformer 重复静态链接 protobuf 且加载失败静默 @ BUILD:187
    • 建议:补一条最小加载 smoke(py_test 即可):同一解释器中先后触发 ops.RtpLLMOpops.MMRdmaExporter,断言两者都是真实类型而非 EmptyClass/DisabledMMRdmaExporter,以确认 descriptor 不会重复注册;把 _load_rdma_ops 失败分支日志从 logging.info 提到 logging.warning,使打包缺失或 ABI 不匹配可发现。若确认两 .so 永不同进程加载,请在根 BUILD 注释写明该前提,并考虑把 mm_rdma_exporter 的构建按平台 select 收敛。另请在 PR 描述中明确内源 arch_select.bzl 已同步 rdma_transport_deps()
  • LocalRpcServer 新增的多模态拒绝路径缺测试,且在批量接口上被压平为 UNKNOWN_ERROR @ rtp_llm/cpp/model_rpc/LocalRpcServer.cc:171
    • 建议:把 :323 的错误填充改为透传 err.code() 对应的 PB 错误码而非固定 UNKNOWN_ERROR,使两个入口对同一失败条件返回一致的可重试性语义;并在 LocalRpcServerTest.cc 补两例:mm_processor_ 为空且请求带 mm 输入时断言返回 MM_NOT_SUPPORTED_ERRORmultimodal_inputs 为空 optional / 空 vector 时断言不触发拒绝(对应本次新增的 !empty() 条件)。
  • MultimodalPbConverter 新增的两条校验分支与请求方向转换无测试覆盖 @ rtp_llm/cpp/model_rpc/MultimodalPbConverter.cc:49
    • 建议:在 MMRemoteOutputTransportTest.cc:272 的现有 TEST 内追加两个子块:split_size={2,3} + 5 行 embedding 但 position_id 只有 4 行,断言返回 error;split_size={2,3} + 3 个 multimodal_extra_input,断言返回 error 且文案含 extra_input count。同时为 inputsToPb 补一个 round-trip 用例(构造 MultimodalInputinputsToPb → 断言 url/type 与 MMPreprocessConfigPB 各字段含 crop_positionsmm_timeout_ms 均正确回填)。
  • 生产装配入口、默认 auto 懒加载路径与三个新增纯函数均无用例覆盖 @ rtp_llm/cpp/multimodal_processor/test/BUILD:72
    • 建议:给该 cc_test 补 transport:mm_remote_output_transport 依赖并新增用例:分别以 mode="auto"/"grpc"createMMRemoteOutputTransport,断言 advertise 能力差异与自定义超时体现在实际 deadline 上;用极小 read_timeout_msRdmaConfig 构造 MMRdmaReader 配合可记录入参的 fake 断言 clamp 生效,并让 provider 构造失败以断言 advertise 返回 false、重复 fetch 不反复初始化。再加一个不依赖 Harness 的 TEST 覆盖 resolveRpcTimeoutMs(无正值→默认、多输入取最大+margin、负值忽略)与 validateMMTransportModeEXPECT_THROW;并在 MMProcessorConfigTest.cc(header-only inline,无需改 BUILD)补 resolveAndLogMMProcessorKind 的 INVALID→!ok() 且 error 含 vit_separation/tp_rank、NONE/REMOTE→ok() 三组断言。
  • assembleMMRdmaOutput 的协议守卫只覆盖 2 条,rdma_transport 包无测试目录 @ rtp_llm/cpp/multimodal_processor/test/MMRdmaTransportTest.cc:232
    • 建议:把该用例改写为 TEST_P 或补 4 条断言,逐条覆盖 mm_tensors.size() != roles.size()、两个 POS_ID、只有 POS_ID/EXTRA_INPUT 而无 EMBEDDING、extra_inputs.size() != split_sizes.size()(均为纯 CPU tensor 构造,成本极低)。另新增 rtp_llm/cpp/rdma_transport/test/(只需依赖 :rdma_transport:tensor_rdma_cc,无需 GPU),覆盖 toProto/fromProto 往返一致性、未知 data_type 返回 false、dst == nullptr 返回 false。
  • 唯一的生产 gRPC 控制面客户端零测试覆盖,四条并发路径均无验证 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:213
    • 建议:把 MultimodalRpcPool/stub 抽到可注入接口(或用 in-process gRPC server),补一个针对 GrpcMMControlClient 的聚焦单测,覆盖三条边界:未满走异步且最终按 endpoint 合批发出、越界走同步、析构时残留 task 全部发送;并断言 pending_release_handles_ 在异步与同步两条路径上均能归零。
  • ViT 侧传输后端的选路与惰性生命周期无任何测试保护 @ rtp_llm/multimodal/transport/factory.py:13
    • 建议:补三个 factory 用例(注入可控假 exporter 工厂即可,无需硬件):mode=grpc / transport_config=NoneMMOutputTransport._backends 为空;mode=auto 且 MMRdmaExporter.available() 返回 False 时同样为空且不抛异常;mode 非法时 assertRaises(ValueError) 并带 match(P.P.G.2)。再补 _get_exporter 的「enabled() 为 False 不缓存 / 异常只尝试一次 / close() 后返回 None」三条。

P3

  • MM transport 超时契约常量在 Python 与 C++ 五处各自硬编码,仅靠注释约束 @ rtp_llm/cpp/pybind/ConfigExtract.h:47
    • 建议:以单一具名常量收敛(如在 MMTransportMode.h 定义 kDefaultMMWorkerTimeoutMs,让 ConfigModules.h:349ConfigExtract.h 的兜底都由它推导,MMRemoteOutputTransport.h 的两个常量直接引用同一来源);Python 侧通过已导出的配置模块读取而非再写字面量;若确实无法共享,至少补一个断言各处默认值一致的小测试。
  • DisabledMMRdmaExporter 缺少 available(),迫使调用方用字面量 getattr 探测能力 @ rtp_llm/multimodal/transport/rdma/backend.py:41
    • 建议:给 DisabledMMRdmaExporter 补一个 @staticmethod available() -> bool: return False(与它已有的 enabled() 对称),调用方即可直接写 if not MMRdmaExporter.available(): return None,能力探测的契约由替身类自身保证,不再需要反射。
  • RDMA 探测日志文案与实际降级行为不符,enabled 为 False 时无任何日志 @ rtp_llm/multimodal/transport/rdma/backend.py:43
    • 建议:把该日志改为与其他降级点一致的表述(如 "[VIT] RDMA implementation unavailable; mm output falls back to inline bytes"),并在 enabled() 为 False 的分支补一条 warning,使 factory.py 的 "enabled" 结论可被后续日志纠正。
  • supports() 未校验 tensor dtype,与 inline 路径的 fail-fast 语义不一致 @ rtp_llm/multimodal/transport/rdma/backend.py:74
    • 建议:在 supports() 中加一条 dtype 白名单校验(embedding/position_ids/extra_input 全部落在 fp32/int32/fp16/bf16 内才返回 True),白名单与 grpc_util 共用一处常量;不满足时让 RDMA 后端直接弃权,从而保留 inline 路径原有的显式报错行为。
  • RDMA 路径将 pos_id 单独搬到 CPU,与 embedding 的设备语义不对称且无说明 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:40
    • 建议:补一行注释说明 pos_id 必须驻留 host 的具体原因(指向消费该字段的下游代码),或若并非必需则去掉该拷贝以避免每请求一次 D2H 同步;同时建议在 MultimodalOutput 定义处把「mm_features 允许位于设备内存、mm_position_ids 恒为 host」写成显式契约。
  • transport core 仍依赖 fat model_rpc_service.pb.h,弱化了 proto 拆分的解耦目标 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.h:13
    • 建议:把 transport core 与 mm_rdma_reader 的 include 与 BUILD 依赖改为 multimodal_output.pb.h / multimodal_output_cc(这几个消息已全部搬迁且 full name 不变,仅 gRPC stub 相关部分需保留在 grpc 子目标),使拆分在生产者与消费者两侧都成立。
  • LLM→ViT 的 gRPC 截止时间完全由客户端可控的 mm_timeout_ms 决定,缺服务端上限钳制 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc:19
    • 建议:在 resolveRpcTimeoutMs 中用一个服务端上限(可复用 MMTransportConfig::default_rpc_timeout_ms 或新增 max_rpc_timeout_ms)对结果做 std::min 钳制,并在被钳制时打一条 WARNING,使客户端请求无法把服务端资源占用时长拉到配置之外;vit_proxy_server.py 的同形逻辑建议一并加同一上限。
  • model_rpc_server 导出 MultimodalPbConverter.h 但未声明其实现目标 @ rtp_llm/cpp/model_rpc/BUILD:70
    • 建议:把 MultimodalPbConverter.h 加入 hdrs glob 的 exclude 列表(与 srcs 保持对称),并在 model_rpc_server 的 deps 中显式加上 :multimodal_pb_converter;若 model_rpc 内确实无人使用该头,则仅做 exclude 即可。
  • create_grpc_proto 的 data 清单已失效却未清理,与 proto_srcs 形成两套机制 @ rtp_llm/cpp/model_rpc/proto/BUILD:17
    • 建议:删除 create_grpc_protodata 属性(它对代码生成无实际作用),让 proto_srcs 成为唯一的 proto 输入声明来源;或反过来把全部 4 个 proto 都补进 data 并加注释说明其用途,避免清单与事实不符。
  • tensor_rdma.proto 未注入 java_package,Java 生成类落到与其余消息不同的包 @ rtp_llm/flexlb/flexlb-grpc/pom.xml:112
    • 建议:给 tensor_rdma.proto 的复制步骤补上与另两个 proto 相同的 replaceregexp 注入(或在 proto 源文件中直接声明 option java_package),使三个 proto 的 Java 包保持一致;若刻意让 RDMA 描述符独立成包,请在 pom 中加注释说明该差异是有意为之。
  • 内联测试数据违反 extra_input/pos_id 每图一份的生产不变量,且注释宣称覆盖了不存在的熔断 @ rtp_llm/multimodal/test/mm_output_transport_test.py:149
    • 建议:把测试数据改为每图一份(2 个 position_ids、2 个 extra_input),使 inline receipt 与 inlineOutputFromPb 的不变量一致,顺带让这个用例真正成为端到端契约的一道保护;并删除或改写 :131 的 circuit breaker 注释(例如改为「对端未请求 RDMA」),待熔断实现落地后再补对应用例。

Checklist Findings (23 fail / 48 total)

General Principles Checklist

  • [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue model_rpc_server 导出 MultimodalPbConverter.h 但未声明其实现目标
    srcs glob 已排除 MultimodalPbConverter.cc(:67),但 hdrs glob(:70-73,只排除 RPCPool.h)仍把 MultimodalPbConverter.h 收进 model_rpc_server,而该 target 的 deps(:75-96)没有 :multimodal_pb_converter。对比同样被 srcs 排除的 TensorPbConvert.cc,其对应 target :tensor_pb_convert 是显式声明的(:79),只有本 PR 新增的这个缺失。当前实现符号只靠 //rtp_llm/cpp/multimodal_processor(:90)的传递依赖提供;一旦 model_rpc 包内某个 .cc 开始 include 这个头(编译能过,因为头就在自己的 hdrs 里),链接是否成功就取决于这条偶然的传递路径。
  • [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue tensor_rdma.proto 未注入 java_package,Java 生成类落到与其余消息不同的包
    prepare-protoengine_rpc_service.proto(:118-121)与 multimodal_output.proto(:122-125)都用 replaceregexp 注入 option java_package = "org.flexlb.engine.grpc",但 :112-114 复制 tensor_rdma.proto 时没有对应注入。该 proto 自身声明 package rdma_transport;tensor_rdma.proto:3)且无 java_package/java_outer_classname 选项,其 Java 类会落在顶级包 rdma_transport 而非 FlexLB 命名空间。已确认当前无 Java 代码引用 RdmaDescriptorPB 等被搬迁/新增消息,故不构成源码级破坏,但 MMRdmaSlotPB.rdma_descriptor 一旦被 Java 侧访问就会暴露这处不一致。
  • [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue config_extract 位于 pybind 层却被 transport 数据面反向依赖,形成包级双向依赖
    新增 header-only 目标 //rtp_llm/cpp/pybind:config_extract(:36-46,deps 含重目标 //:rtp_compute_ops)被下层数据面反向依赖:transport/rdma/BUILD:29 直接依赖它,MMRdmaExporter.cc:7,28 include 该头并在构造函数中调用 extractRdmaConfig;同时 pybind/BUILD:112mm_rdma_exporter_pybind 又依赖 transport/rdma:mm_rdma_exporter,形成 pybind ↔ transport/rdma 包级双向依赖。MMRdmaExporter 因此无法在不引入语言绑定层的前提下被非 Python 调用方复用,且 //:rtp_compute_ops 会随 header-only 目标传递给所有消费者。此外 ConfigExtract.h:8 在头文件全局作用域定义 namespace py = pybind11;,污染所有包含方。
  • [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue RDMA 探测日志文案与实际降级行为不符,enabled 为 False 时无任何日志
    :43-45 在 RDMA 实现不可用时打印 "[VIT] RDMA implementation unavailable; skip GPU-NIC affinity",但此分支与 GPU-NIC affinity(由 load_gpu_nic_affinity 负责的 ACCL_NIC_GPU_AFFINITY)毫无关系,实际后果是本进程 ViT 输出退回 inline gRPC bytes;同文件其他降级点文案均为 "falling back to bytes",仅此处不一致,排障会被引向错误方向。另外 :68-69exporter.enabled() 返回 False 时既不记日志也不再重试,而 factory.py:28 已提前打印 "mm rdma output backend enabled",日志会长期给出与运行时行为相反的结论。
  • [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue 新 .so 无条件进入全平台构建,与 libth_transformer 重复静态链接 protobuf 且加载失败静默
    _新 target mm_rdma_exporter(linkshared=1,:187-198)经 pybind:mm_rdma_exporter_pybindtransport/rdma:mm_rdma_exporter 直接依赖 model_rpc/proto:multimodal_output_ccrdma/BUILD:28),而 libth_transformer.somodel_rpc_service_cc_proto(protodeps 含 :multimodal_output)含同一批 descriptor;这是本仓第一个与它重复包含 proto 生成单元的独立 .so,且与 th_transformer(:200-214,srcs = [":rtp_compute_ops"])的链接约定不一致。同时 arch_config/arch_select.bzl:11copy_all_so() 无条件加入该 target,使它成为 CPU/ARM/ROCm/PPU 全平台必建产物。加载侧 `ops/_init
  • [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue RDMA 路径将 pos_id 单独搬到 CPU,与 embedding 的设备语义不对称且无说明
    assembleMMRdmaOutput 中 embedding chunk(:34)与 extra_input(:44)保持 reader_->read 返回的原设备(GPUDirect 场景为显存),唯独 mm_position_id = mm_tensors[i].to(torch::kCPU)(:40)被显式搬到 host。inline 路径(inlineOutputFromPb)产出全部为 CPU 张量,LOCAL 路径经 mm_process_engine.py:525 只对 position_ids 做 .cpu(),三条路径的设备组合各不相同。该 .to(kCPU) 没有注释说明是下游对 pos_id 的硬约束还是顺手的兼容处理,后续维护者既无法判断能否去掉,也无法判断 inline 与 RDMA 的设备差异是否属于契约。
  • [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue LLM→ViT 的 gRPC 截止时间完全由客户端可控的 mm_timeout_ms 决定,缺服务端上限钳制
    resolveRpcTimeoutMs(:9-20)取所有 input 中最大的正 mm_timeout_ms 加 margin,没有任何上限钳制,结果直接成为 DeadlineBudget(:59)并落到 MMGrpcTransport.cc:68ClientContext deadline。而 mm_timeout_ms 对外可写:它是 MMPreprocessConfigPB 的字段 9,经 MultimodalPbConverter.cc:88preprocessConfigToPb 原样透传,客户端因此可把一次 ViT RPC 的占用拉到任意长度。注意这仍优于改动前(原 RemoteMultimodalProcessor 完全没有 deadline,等于无限等待),故记为加固项而非回归。
  • [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue 降级原因被丢弃、reader 未注入 metrics,数据面回退率完全不可观测
    degradeToTerminal(..., const std::string& /*reason*/)(:100)显式丢弃 reason,kReasonRdmaReadError/kReasonRdmaManifestError 构造即弃。MMTransportMetrics 只有 reportRpcClientError/reportRpcMetrics,工厂只把它传给 createGrpcMMControlClientMMRemoteOutputTransportFactory.cc:30),readers 与 DeliveryContext 都不持有 metrics。因此命中/降级路径唯一信号是 MMRdmaReader.cc:156每请求 INFO 日志 [MM-RDMA-HIT],既无 QPS 指标又是热路径噪声。此外 MMGrpcTransport.cc:139-161releaseNow 在超时非正、连接失败、RPC 失败三条早退分支上同样无任何指标,只有一条 WARNING。同目录 `kReas
  • [6.1] Quality — 逻辑变更未混入无关格式化 → issue create_grpc_proto 的 data 清单已失效却未清理,与 proto_srcs 形成两套机制
    py_binary(create_grpc_proto).data 仍只列 flexlb_schedule_service.protomodel_rpc_service.proto(:17-20)。本 PR 新增的 multimodal_output.protordma_transport/proto/tensor_rdma.proto 都没有加进来,实际是靠新增的 proto_srcs 属性(:29、:51)把 <name>_proto_srcs filegroup 声明为 action inputs、protoc 以 -I. 从 execroot 解析,data 里的 runfiles 副本路径从来不会被用到。两套机制并存且 data 是一份「半真」清单,后续维护者按它增删会得到错误结论。
  • [6.1] Software Engineering — DIP:高层策略不依赖非必要具体细节 → issue config_extract 位于 pybind 层却被 transport 数据面反向依赖,形成包级双向依赖
    新增 header-only 目标 //rtp_llm/cpp/pybind:config_extract(:36-46,deps 含重目标 //:rtp_compute_ops)被下层数据面反向依赖:transport/rdma/BUILD:29 直接依赖它,MMRdmaExporter.cc:7,28 include 该头并在构造函数中调用 extractRdmaConfig;同时 pybind/BUILD:112mm_rdma_exporter_pybind 又依赖 transport/rdma:mm_rdma_exporter,形成 pybind ↔ transport/rdma 包级双向依赖。MMRdmaExporter 因此无法在不引入语言绑定层的前提下被非 Python 调用方复用,且 //:rtp_compute_ops 会随 header-only 目标传递给所有消费者。此外 ConfigExtract.h:8 在头文件全局作用域定义 namespace py = pybind11;,污染所有包含方。
  • [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue tensor_rdma.proto 未注入 java_package,Java 生成类落到与其余消息不同的包
    prepare-protoengine_rpc_service.proto(:118-121)与 multimodal_output.proto(:122-125)都用 replaceregexp 注入 option java_package = "org.flexlb.engine.grpc",但 :112-114 复制 tensor_rdma.proto 时没有对应注入。该 proto 自身声明 package rdma_transport;tensor_rdma.proto:3)且无 java_package/java_outer_classname 选项,其 Java 类会落在顶级包 rdma_transport 而非 FlexLB 命名空间。已确认当前无 Java 代码引用 RdmaDescriptorPB 等被搬迁/新增消息,故不构成源码级破坏,但 MMRdmaSlotPB.rdma_descriptor 一旦被 Java 侧访问就会暴露这处不一致。
  • [6.1] Software Engineering — ISP:调用方不依赖无关大接口 → issue transport core 仍依赖 fat model_rpc_service.pb.h,弱化了 proto 拆分的解耦目标
    本 PR 把 TensorPB/MultimodalInputsPB/MultimodalOutputPB/ReleaseLeasePB 拆到独立的 multimodal_output.proto,并为 mm_rdma_exporter 正确地只依赖 lean 的 multimodal_output_cctransport/rdma/BUILD:28)。但 transport core 的头文件仍 include model_rpc_service.pb.h(本行),mm_rdma_reader 的 BUILD 依赖(transport/rdma/BUILD:8)与 MMRdmaReader.cc:10 同样如此,于是消费侧仍被迫拖进包含全部 GenerateInput/WorkerStatus/服务定义的 fat descriptor,拆分带来的编译与依赖收敛只在导出侧生效。
  • [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue DisabledMMRdmaExporter 缺少 available(),迫使调用方用字面量 getattr 探测能力
    available = getattr(MMRdmaExporter, "available", None) / if available is None or not available():(:41-42)用字面量 getattr 做控制流分支。其唯一目的是区分真实 pybind 类(MMRdmaExporter.cc:167 导出静态方法 available)与 ops/__init__.py:308-317 的降级替身 DisabledMMRdmaExporter —— 后者继承 EmptyClass,只定义了 enabled(),没有 available(),两者形成不对称契约。
  • [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue ViT 侧 RDMA 导出未建立 CUDA 流序,LLM 可能读到未完成计算的显存
    :88torch.concat(res.embeddings).contiguous() 只把 kernel 入队当前 CUDA stream,:95exporter.export_embedding 随即注册显存并返回 descriptor,RPC 立即回包;NIC 的 RDMA read 不参与 CUDA stream 排序。MMRdmaExporter.ccexportSlots/exportEmbedding 全程无 stream/event 操作(仅 narrow/toProto),在 rtp_llm/multimodal/** 全目录检索 synchronize|current_stream|record_stream|cuda.Event 零命中。对比 inline 路径:grpc_util.py:50t = t.cpu() 提供隐式同步点,拆出 RDMA 路径后该同步点被移除。默认 mode=auto,失败表现是静默错误 embedding 而非报错。
  • [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue ViT 侧传输后端的选路与惰性生命周期无任何测试保护
    create_mm_output_transport 是 ViT 侧是否启用 RDMA 的唯一入口,含三条分支:None/grpc 不注册后端(:17)、autotry_create 返回 None 时告警降级(:23-26)、非法 mode 抛 ValueError(:31)。但 mm_output_transport_test.py 全程直接 RdmaOutputBackend(self.exporter) 构造(:66、:221),该路径下 _exporter_init_attempted__init__ 即为 True(backend.py:33),因此既未调用 create_mm_output_transport 也未调用 try_create/_get_exporter。于是「配置 auto 但环境无 RDMA 实现时是否确实回落到纯 inline」这一最关键的上线行为、available() 缺失/False/抛异常三种情形、enabled() 为 False 时不缓存、一次性锁存、`close(
  • [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue 内联测试数据违反 extra_input/pos_id 每图一份的生产不变量,且注释宣称覆盖了不存在的熔断
    test_payload_is_encoded_inline(:146-160)用 2 个 embedding(split_size=[2,3],共 5 行)但只给 1 个 position_ids(2 行)和 1 个 extra_input。该 receipt 送到 LLM 侧会被 MultimodalPbConverter.cc:49split_total(5) != mm_position_id.size(0)(2))与 :58(extra_input_size(1) != split_sizes.size()(2))双双拒绝,即测试固定下来的是一份生产端必然报错的载荷。另外 :131 的注释「Peer did not ask for RDMA (this also covers its circuit breaker being open)」宣称覆盖了 LLM 侧熔断,但全仓不存在任何熔断实现,该注释会误导后续读者以为已有覆盖。

RTP-LLM Checklist

  • [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue RdmaExport/RdmaRead 未声明内存所有权与返回顺序契约,在库 fake 拷贝数据反而掩盖该要求
    重组逻辑隐含两条未写入契约的强假设。一是 read(:68)返回的 tensors 必须严格按「descriptor 顺序 × manifest 顺序」展平,因为 readAllSlots 按该顺序累积 roles、assembleMMRdmaOutput 直接 torch::cat(embedding_chunks, 0);顺序不同时 split_total == embedding.size(0) 仍会通过,结果是静默 embedding 错乱。二是 create 必须在 lease 有效期内持有导出张量存储:exportEmbedding 按值收下 tensor,函数返回后引用即归还 caching allocator,而 LLM 是在 RPC 之后才读;create 参数是 const 引用,注释只写了「失败时 lease_id 为空」。测试 fake RecordingExport::createMMRdmaTransportTest.cc:87)用 memcpy 拷进自有 buffer,因此永远不会暴露该约束。
  • [I] 代码质量 — 同一功能用统一工具函数 → issue MM transport 超时契约常量在 Python 与 C++ 五处各自硬编码,仅靠注释约束
    同一个 120s worker 预算与 5s margin 被写在五处:ConfigExtract.h:47 的字面量 120 * 1000py_config_modules.py:270DEFAULT_MM_TIMEOUT_MS = ******ConfigModules.h:349-351default_rpc_timeout_ms = 125 * 1000(即 ******+5000 的手工展开)与 rpc_timeout_margin_msMMRemoteOutputTransport.h:20-21kDefaultVitRpcTimeoutMs/kVitRpcTimeoutMarginMsvit_proxy_server.py 的 margin 秒数。py_config_modules.py:266MMRemoteOutputTransport.h:19 的注释自己就承认这几处必须手工保持一致,而 Python 侧根本没有 margin 字段,任一处调整都会让 `default_rpc_tim

Python Static-First Checklist

  • [P.A] 静态结构与类型纪律 — 禁止 getattr/setattr literal 访问 → issue DisabledMMRdmaExporter 缺少 available(),迫使调用方用字面量 getattr 探测能力
    available = getattr(MMRdmaExporter, "available", None) / if available is None or not available():(:41-42)用字面量 getattr 做控制流分支。其唯一目的是区分真实 pybind 类(MMRdmaExporter.cc:167 导出静态方法 available)与 ops/__init__.py:308-317 的降级替身 DisabledMMRdmaExporter —— 后者继承 EmptyClass,只定义了 enabled(),没有 available(),两者形成不对称契约。
  • [P.B] 错误处理 — 禁止 bare except 或静默吞异常 → issue 亲和性产物按进程 CWD 相对路径读取,异常捕获过宽且失败仅记 info
    load_gpu_nic_affinity 执行 /usr/local/bin/run_affinity 后以相对路径 open("npu_nic_affinity.json")(:589)读取产物,结果取决于调用进程 CWD;该函数在 start_server.py:337start_backend_server.py:384vit_start_server.py:40无条件调用(不限多模态/RDMA 部署)。父进程失败时多个 worker 会并发执行并读写同一 CWD 文件,可能读到被截断的内容并留下残留。except Exception(:599)把 subprocess 失败、文件缺失、JSON 解析失败与自己在 :593/:598 主动 raise ValueError 的形状校验全部统一记为 "run %s failed" 且仅 logging.info;而同链路的 _configure_gpu_nic_affinity 对同类问题用 warning。:582subprocess.run 捕获了 st
  • [P.F] 语言陷阱 — 禁止模块级 import 副作用 → issue online_eval proto 加载新增对 rtp_llm 包的硬依赖,并对真实包 __path__ 做全局副作用
    _ensure_proto_modules 在 :64 无条件调用 _add_generated_package_paths(out),后者(:116-126)importlib.import_module("rtp_llm.cpp")"rtp_llm.cpp.model_rpc.proto",并把生成目录 insert(0, ...) 进真实包的 __path__(进程级全局副作用);:71-73 还要求 rtp_llm.cpp.model_rpc.proto.multimodal_output_pb2 可导入。而本文件已无任何 sys.path 操作,本目录下所有运行脚本一律只设 PYTHONPATH="${SCRIPT_DIR}"(= tools/online_eval),从未加入仓库根。未安装 rtp_llm 的解释器下所有 online_eval 入口都会在 proto 加载阶段 ModuleNotFoundError。另外 :74-83 用 setattr 把 7 个消息名搬回聚合模块,只为支撑单一历史调用点。
  • [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue 内联测试数据违反 extra_input/pos_id 每图一份的生产不变量,且注释宣称覆盖了不存在的熔断
    test_payload_is_encoded_inline(:146-160)用 2 个 embedding(split_size=[2,3],共 5 行)但只给 1 个 position_ids(2 行)和 1 个 extra_input。该 receipt 送到 LLM 侧会被 MultimodalPbConverter.cc:49split_total(5) != mm_position_id.size(0)(2))与 :58(extra_input_size(1) != split_sizes.size()(2))双双拒绝,即测试固定下来的是一份生产端必然报错的载荷。另外 :131 的注释「Peer did not ask for RDMA (this also covers its circuit breaker being open)」宣称覆盖了 LLM 侧熔断,但全仓不存在任何熔断实现,该注释会误导后续读者以为已有覆盖。
  • [P.G] 测试规范 — pytest.raises 带 match 参数 → issue ViT 侧传输后端的选路与惰性生命周期无任何测试保护
    create_mm_output_transport 是 ViT 侧是否启用 RDMA 的唯一入口,含三条分支:None/grpc 不注册后端(:17)、autotry_create 返回 None 时告警降级(:23-26)、非法 mode 抛 ValueError(:31)。但 mm_output_transport_test.py 全程直接 RdmaOutputBackend(self.exporter) 构造(:66、:221),该路径下 _exporter_init_attempted__init__ 即为 True(backend.py:33),因此既未调用 create_mm_output_transport 也未调用 try_create/_get_exporter。于是「配置 auto 但环境无 RDMA 实现时是否确实回落到纯 inline」这一最关键的上线行为、available() 缺失/False/抛异常三种情形、enabled() 为 False 时不缓存、一次性锁存、`close(

Strengths

  • proto 拆分严格保持 wire 兼容:multimodal_output.proto 不声明 package,消息 full name 与字段号完全不变,support_rdma(2)、output_rdma_slots(5) 均为新增字段;tf_proto_library_cc*_genproto 命名差异用显式 alias 补齐(proto/BUILD:40-45),generate_grpc_proto 新增可选 proto_srcs 把传递 import 声明为 action input,C++/Python/Java 三侧生成链一致。
  • 新旧任一侧未升级都自然退化为 inline gRPC:新 LLM + 旧 ViT 收到无 slot 的 receipt 走 terminal,新 ViT + 旧 LLM 因 support_rdma 缺省 false 不导出;开源构建经 arch_select.bzl:59-65rdma_transport_deps() 指向 rdma_transport_no_implcreateRdmaRead 返回 nullptr → advertise 恒 false,新数据面完全惰性。
  • MMRemoteOutputTransportTest.cc 把两条易被忽略的不变量固化为用例:receiptForANeverAdvertisedPlaneIsAProtocolErrorNotADegrade(:250)明确把「未协商却收到 RDMA receipt」定为协议错误而非静默降级,注释写清「RDMA receipt 的 inline 字段为空,降级即解出看似合法的错数据」;readFailureDegradesInExactlyOneExtraRoundWithCapabilityWithdrawn(:217)用完整调用序列 {request, read, release, request, terminal} 同时防住「降级变候选循环」与「slot 泄漏到编码侧 GC」。
  • MultimodalPbConverter::inlineOutputFromPb 把原 transMMOutputRTP_LLM_CHECK_WITH_INFOFT_CORE_DUMP_ON_EXCEPTION 下 abort 整个进程)改为返回 ErrorInfo,并补齐 split_size 合计、pos_id 行数、extra_input 个数三处校验加 try/catch,远端畸形数据不再能打挂 LLM 进程。
  • 生命周期与回滚处理细致:RdmaExport::createBatchRdmaTransport.cc:52-62)任一 group 失败即回滚全部已建 lease;SlotLease 以 RAII 保证归还;ViT 侧 _roll_back 在 descriptor 解析失败时先归还已解析 slot;GrpcMMControlClient 析构先 swap 出待发队列再 join,避免丢弃关闭期 release。
  • 超时治理是实质改进:原 RemoteMultimodalProcessorgrpc::ClientContext 完全没有 deadline,新实现引入 DeadlineBudget 统一约束 RPC、RDMA 读、release 与回退重试。
  • LocalRpcServer::init 用纯函数决策表 resolveAndLogMMProcessorKind 取代散落的隐式条件,非法组合在创建 NormalEngine 之前返回 FAILED_PRECONDITIONprepareInputMM_NOT_SUPPORTED_ERRORPrefillRpcServer 既有实现同形,是一致性收敛而非新增分叉。
  • Python 与 C++ 配置默认值逐项对齐:py_config_modules.py:273-293 的 7 个 RDMA 字段 + release_timeout_ms + modeRdmaConfig.h:9-22ConfigModules.h:338-352 完全一致;mode 取值集合在 argparse choicesvit_group_args.py:299)、factory.py 分发、MMTransportMode.h:11 三处一致,validateMMTransportMode 在 pybind 抽取阶段即 fail-fast。
  • BUILD 目标拆分动机写进注释且真实打断包级环:multimodal_pb_convertermm_processor_configmm_remote_output_transport_core 等 lean target 让跨包复用不拖进 model_rpc_server 全量引擎依赖;mm_processor_config_test 因此不 link torch,可落 CPU 节点,另两个 cc_test 从 H20 降为 A10,并在注释里解释了「断言纯 CPU 但 link torch 会拖进 libtorch_cuda」这一非显然约束。
  • _physical_gpu_index 修正真实缺陷:CUDA_VISIBLE_DEVICES=4,5,6,7 时旧逻辑取 affinity["0"](错网卡),新逻辑取 affinity["4"],并给 run_affinity/nvidia-smi 补上 30s/10s timeout 与 JSON 形状校验。

extras = []
if res.extra_input is not None and len(res.extra_input) > 0:
extras = [e.to(device=emb.device).contiguous() for e in res.extra_input]
desc_bytes_list = exporter.export_embedding(emb, pos, extras)

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] ViT 侧 RDMA 导出未建立 CUDA 流序,LLM 可能读到未完成计算的显存

:88torch.concat(res.embeddings).contiguous() 只把 kernel 入队当前 CUDA stream,:95exporter.export_embedding 随即注册显存并返回 descriptor,RPC 立即回包;NIC 的 RDMA read 不参与 CUDA stream 排序。MMRdmaExporter.ccexportSlots/exportEmbedding 全程无 stream/event 操作(仅 narrow/toProto),在 rtp_llm/multimodal/** 全目录检索 synchronize|current_stream|record_stream|cuda.Event 零命中。对比 inline 路径:grpc_util.py:50t = t.cpu() 提供隐式同步点,拆出 RDMA 路径后该同步点被移除。默认 mode=auto,失败表现是静默错误 embedding 而非报错。

建议:export_embedding 之前显式建立可见性:对 emb.devicetorch.cuda.current_stream(emb.device).synchronize(),或在 MMRdmaExporter::exportSlots 内插入 event 并等待。若同步点确实由 build-selected provider 的 create() 承担,请在 RdmaTransport.h:59create 声明处把「调用方已保证张量内容对 NIC 可见」写成硬契约,并在 PR 描述中说明同步点落在哪一层,否则任何新 provider 都会重犯。

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 #1353 (non-blocking suggestions)

31 条 P2/P3 建议,不阻塞合并。阻塞判定与完整摘要见上一条 review。


} // namespace

bool MMRdmaReader::advertise(const std::string& /*endpoint*/, MultimodalInputsPB& request_pb) {

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] RDMA 持续故障时缺少熔断,每请求放大为两次 ViT forward 且不自愈

MMTransportConfig::mode 默认 autoConfigModules.h:345),链接 provider 即开启。advertise(:113-119)只依赖 ensureReader(),构造成功后对每个请求恒置 support_rdma=true;读失败仅返回 ConsumeResult::retry(:145/:153),fetch 随即 degradeToTerminal,其中 control_->request 是全新 RemoteMultimodalEmbedding,即完整重跑 ViT forward。故 NIC 故障下每请求固定付 1 次 forward + 最长 read_timeout_ms(3000ms) 失败读 + 同步 release + 第 2 次完整 forward。且降级沿用同一 DeadlineBudget:客户端设较小 mm_timeout_ms 时会撞 MMRemoteOutputTransport.cc:104budget.exhausted() ...

建议: 1) 给 MMRdmaReader 增加滑动窗口连续失败计数与冷却半开重试,达阈值后 advertise 直接返回 false,使故障态收敛为单次 forward。2) 为降级重试保留独立预算(或在 resolveRpcTimeoutMs 之外给 fallback 追加一份配额),避免数据面故障把本可成功的请求变成失败请求。3) 熔断落地前建议把 mm_transport_mode 默认值改为 grpc,由部署显式开启 auto

ErrorResult<MultimodalOutput> MMRemoteOutputTransport::degradeToTerminal(const std::string& endpoint,
MultimodalInputsPB& request_pb,
DeliveryContext& context,
const std::string& /*reason*/) {

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] 降级原因被丢弃、reader 未注入 metrics,数据面回退率完全不可观测

degradeToTerminal(..., const std::string& /*reason*/)(:100)显式丢弃 reason,kReasonRdmaReadError/kReasonRdmaManifestError 构造即弃。MMTransportMetrics 只有 reportRpcClientError/reportRpcMetrics,工厂只把它传给 createGrpcMMControlClientMMRemoteOutputTransportFactory.cc:30),readers 与 DeliveryContext 都不持有 metrics。因此命中/降级路径唯一信号是 MMRdmaReader.cc:156每请求 INFO 日志 [MM-RDMA-HIT],既无 QPS 指标又是热路径噪声。此外 MMGrpcTransport.cc:139-161releaseNow 在超时非正、连接失败、RPC 失败三条早退分支上同样无任何指标,只有一条 WARNING。同目录 `kR...

建议:MMTransportMetricsPtr 注入 reader 与 DeliveryContext,用 ConsumeResult::reason() 打点降级 QPS(rdma_read_error/rdma_manifest_error/unadvertised_receipt),并把 [MM-RDMA-HIT] 的每请求 INFO 改为命中计数指标(或降到 DEBUG);同时给 releaseNow 的失败分支补 reportRpcClientError,与 kReasonReleaseQueueFull 的既有做法保持一致。

Checklist: [6.1] 无 per-forward 调试日志 / 噪声热路径输出

public:
virtual ~RdmaExport() = default;
// Returns an empty descriptor (lease_id is empty) when export fails.
virtual RdmaDescriptor create(const std::vector<torch::Tensor>& tensors) = 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.

[P2] RdmaExport/RdmaRead 未声明内存所有权与返回顺序契约,在库 fake 拷贝数据反而掩盖该要求

重组逻辑隐含两条未写入契约的强假设。一是 read(:68)返回的 tensors 必须严格按「descriptor 顺序 × manifest 顺序」展平,因为 readAllSlots 按该顺序累积 roles、assembleMMRdmaOutput 直接 torch::cat(embedding_chunks, 0);顺序不同时 split_total == embedding.size(0) 仍会通过,结果是静默 embedding 错乱。二是 create 必须在 lease 有效期内持有导出张量存储:exportEmbedding 按值收下 tensor,函数返回后引用即归还 caching allocator,而 LLM 是在 RPC 之后才读;create 参数是 const 引用,注释只写了「失败时 lease_id 为空」。测试 fake RecordingExport::createMMRdmaTransportTest.cc:87)用 memcpy 拷进自有 buffer,因此永远不会暴露该约束。

建议:create/read 声明处补明确契约注释:create 必须持有传入张量的存储引用直至 lease 被 release 或 TTL 回收(并说明 release 后 remote_addr 何时失效),read 必须按 descriptor 与 manifest 顺序返回等量 tensors;或把 create 改为按值接收 std::vector<torch::Tensor> 以在类型上表达保活意图。同时在 readAllSlots 逐个比对 result.tensors[i]descriptor.tensors[i] 的 dtype/shape,使顺序错配从静默损坏变为可检测的 retry,并补一个只持引用、不拷数据的 fake。

Checklist: [I] 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理

Comment thread rtp_llm/cpp/rdma_transport/RdmaTransport.cc Outdated
return cfg;
}

inline VitConfig extractVitConfig(const py::object& vit_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.

[P2] extractVitConfig 未被 embedding 入口复用,output_transport 与 mm_timeout_ms 在该路径被静默忽略

RtpLLMOp.cc:215 已切换为 extractVitConfig(vit_config),使 mm_timeout_ms 决定 default_rpc_timeout_msConfigExtract.h:46-48)。但同目录 RtpEmbeddingOp.cc:53 仍是旧的手写抽取(只取 vit_separation),其 VIT_SEPARATION_REMOTE 分支(:104)走三参构造 → RemoteMultimodalProcessor.h:45-49grpcOnlyTransportConfig()default_rpc_timeout_ms 固定为结构体默认 125s(ConfigModules.h:349),output_transport.* 全部丢弃。而 MMGrpcTransport.cc:68 已按该预算设置 ClientContext deadline,故 embedding 服务配置 mm_timeout_ms > ****** 时请求会在 125s 被 DEAD...

建议:RtpEmbeddingOp::init 同样调用 extractVitConfig;若确需把 embedding 路径钉在 grpc 数据面,则显式覆盖 mode = kMMTransportModeGrpc 而保留超时等其余字段。若决定冻结该路径,请在 grpcOnlyTransportConfig() 处补注释与 WARNING 日志,明确「用户超时配置在此路径不生效、固定为 125s」,避免运维按配置预期排查超时。

Comment thread rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc Outdated
"MultimodalPbConverter.cc",
],
),
hdrs = glob(

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_server 导出 MultimodalPbConverter.h 但未声明其实现目标

srcs glob 已排除 MultimodalPbConverter.cc(:67),但 hdrs glob(:70-73,只排除 RPCPool.h)仍把 MultimodalPbConverter.h 收进 model_rpc_server,而该 target 的 deps(:75-96)没有 :multimodal_pb_converter。对比同样被 srcs 排除的 TensorPbConvert.cc,其对应 target :tensor_pb_convert 是显式声明的(:79),只有本 PR 新增的这个缺失。当前实现符号只靠 //rtp_llm/cpp/multimodal_processor(:90)的传递依赖提供;一旦 model_rpc 包内某个 .cc 开始 include 这个头(编译能过,因为头就在自己的 hdrs 里),链接是否成功就取决于这条偶然的传递路径。

建议:MultimodalPbConverter.h 加入 hdrs glob 的 exclude 列表(与 srcs 保持对称),并在 model_rpc_server 的 deps 中显式加上 :multimodal_pb_converter;若 model_rpc 内确实无人使用该头,则仅做 exclude 即可。

Checklist: [6.1] 依赖方向:无循环依赖/跨层惊喜

Comment thread rtp_llm/cpp/model_rpc/proto/BUILD
Comment thread rtp_llm/flexlb/flexlb-grpc/pom.xml Outdated
def test_payload_is_encoded_inline(self):
terminal = GrpcInlineOutputBackend()

result = terminal.transfer(

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] 内联测试数据违反 extra_input/pos_id 每图一份的生产不变量,且注释宣称覆盖了不存在的熔断

test_payload_is_encoded_inline(:146-160)用 2 个 embedding(split_size=[2,3],共 5 行)但只给 1 个 position_ids(2 行)和 1 个 extra_input。该 receipt 送到 LLM 侧会被 MultimodalPbConverter.cc:49split_total(5) != mm_position_id.size(0)(2))与 :58(extra_input_size(1) != split_sizes.size()(2))双双拒绝,即测试固定下来的是一份生产端必然报错的载荷。另外 :131 的注释「Peer did not ask for RDMA (this also covers its circuit breaker being open)」宣称覆盖了 LLM 侧熔断,但全仓不存在任何熔断实现,该注释会误导后续读者以为已有覆盖。

建议: 把测试数据改为每图一份(2 个 position_ids、2 个 extra_input),使 inline receipt 与 inlineOutputFromPb 的不变量一致,顺带让这个用例真正成为端到端契约的一道保护;并删除或改写 :131 的 circuit breaker 注释(例如改为「对端未请求 RDMA」),待熔断实现落地后再补对应用例。

Checklist: [6.1] 边界 case 覆盖(空、单元素、最大值);[P.G] mock/fake/stub 不得替代本次声称覆盖的生产边界

@ydshi0
ydshi0 force-pushed the feature/rdma_transport branch from ad6e39f to 5bed256 Compare August 31, 2026 11:13

@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 #1353

Status: LGTM

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

Reviewed: commit 5bed256cb6bc · 2026-08-31 20:13 UTC+8

lgtm ready to ci

Non-blocking Suggestions

P2

  • RDMA 持续故障时缺少熔断,每请求放大为两次 ViT forward 且不自愈 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:113
    • 建议:在 MMRdmaReader 内按连续失败次数加一个带恢复窗口的开关:超过阈值后暂停 advertise(),窗口到期再放行一次探测,避免故障期无限重复双次前向。配合下一条的降级指标,让「已熔断」这一状态既可观测也可自动恢复。
  • 降级原因被丢弃、reader 未注入 metrics,数据面回退率完全不可观测 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc:100
    • 建议:把 MMTransportMetricsPtr 下沉给 MMRemoteOutputTransportMMRdmaReader,以 reason 为 tag 上报降级 QPS(rdma_read_error|rdma_manifest_error|never_advertised),并补一个带 transport=rdma|inline tag 的成功计数与 release_failed 计数,让 60s slot GC 兜底的触发频次可见;同时让 degradeToTerminal 真正使用 reason。[MM-RDMA-HIT] 降为 DEBUG 或删除,避免高 QPS 下每请求一行日志。
  • 降级重试只检查预算是否耗尽、未预留最小预算,可能把可恢复的 RDMA 失败变成硬失败 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc:104
    • 建议:在 degradeToTerminal 中加最小预算门限(如 remainingMs() < rpc_timeout_margin_ms_,或可配置的 min_fallback_budget_ms)时直接放弃重试,并返回携带原始 reason 的明确错误,而不是发出一个注定超时的 RPC,在 ViT 侧白烧一次前向。
  • releaseAsync 在队列饱和或停机时退化为同步 RPC,违反基类「不得延迟消费者」契约 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:105
    • 建议:「不丢弃租约」的取舍合理,但不应以阻塞消费方为代价:encoder 侧已有 slot_gc_timeout_ms TTL GC 兜底,建议饱和时改为丢弃队首旧任务后入队(保持有界且不阻塞),或至少把同步兜底 timeout 改为请求剩余预算并压到远小于 1s。同时把 reportRpcClientErrorRTP_LLM_LOG_WARNING 移出 release_mutex_ 临界区。
  • 析构排空未按 endpoint 合批且无总预算,进程关停耗时不可控 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:49
    • 建议:把「按 endpoint 聚合 + 逐 endpoint 发送」抽成私有辅助方法(如 drainBatches(std::deque<ReleaseTask>&&))供 releaseLoop 与析构共用,消除重复逻辑;同时为析构排空设置一次性总预算(复用一个 DeadlineBudget,超时即放弃剩余 handle 交给 TTL GC 并打一条 warning),保证关机时延有界。
  • 唯一的生产 gRPC 控制面客户端零测试覆盖,四条并发路径均无验证 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:213
    • 建议:把实际发送动作抽为受保护虚函数或注入式 callable(如 std::function<void(const std::string&, const std::vector<std::string>&, int64_t)>),用计数与可阻塞的 fake sender 补齐「队列刚满 / 溢出不阻塞调用方 / 按 endpoint 合批 / 析构时队列非空」四个 gtest,无需起真实 gRPC 服务。这是本 PR 唯一的多线程状态机,回归表现为 lease 泄漏或推理线程被阻塞。
  • exportSlots 不校验张量连续性与设备一致性,非连续输入会静默导出错误数据 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaExporter.cc:60
    • 建议:在 exportSlots 入口统一校验 is_contiguous()、dtype 属于 TensorDataType 支持集合、以及 embedding/pos_id/extra 设备一致,不满足时记 WARNING 并 return false(自然走 inline 回退),或在内部先 .contiguous() 归一化。并补一条非连续张量输入的单测,同时让测试 fake 停止替调用方做 .contiguous()
  • RDMA 描述符反序列化不校验尺寸一致性与上限,与导出侧 max_slot_bytes 不对称 @ rtp_llm/cpp/rdma_transport/RdmaTransport.cc:90
    • 建议:把 fake 里那两条断言上移到 fromProtoreadAllSlots:校验 prod(shape) * itemsize(dtype) == nbytesoffset + nbytes <= payload_bytes,并对 payload_bytes 设一个与导出侧 max_slot_bytes 对称的上限;在 assembleMMRdmaOutput 中追加逐 slot 的 shape/nbytes 一致性比对。不满足时 WARNING 并返回 false 走一次降级,使 provider 侧 manifest bug 表现为「降级到 inline」而不是「静默错误 embedding」。
  • RdmaRead::read 未声明返回顺序与内存所有权契约,在库 fake 拷贝数据反而掩盖该要求 @ rtp_llm/cpp/rdma_transport/RdmaTransport.h:73
    • 建议:参照 create 的写法为 read 补对称契约注释:返回张量必须按 descriptor 顺序、descriptor 内按 manifest 顺序严格一一对应;read 返回即表示数据对调用方当前流可见;tensors 的所有权归调用方,provider 不得在 release 后回收其底层内存;status 非 ok 时 tensors 必须为空。若实现只能返回内部缓冲视图,则应显式要求消费侧 clone 并在 readAllSlots 中落实。
  • RdmaExport::createBatch 只在 lease_id 为空时回滚,create() 抛异常时前序 lease 泄漏 @ rtp_llm/cpp/rdma_transport/RdmaTransport.cc:51
    • 建议:把 createBatch 的循环包进 try/catch,异常时复用同一段回滚逻辑再 rethrow(或返回 {});更稳妥的做法是用一个 RAII 守卫持有已创建的 lease_ids,只有整批成功时才解除守卫。
  • ViT proxy 的租约释放路由只以 lease_id 为键,而协议未约束其跨 worker 全局唯一 @ rtp_llm/multimodal/transport/proxy_router.py:80
    • 建议:在 tensor_rdma.proto 中以注释明确约定 lease_id 必须全局唯一(例如 host:port 前缀 + 进程内单调计数 + 随机后缀),并在 provider 契约文档中同步;同时把 proxy 侧路由键改为 (worker_addr, lease_id) 复合键(收据已携带来源 worker),或在冲突时保留首个映射并仅对冲突项上报指标,避免一次命名冲突同时毒化两个真实租约的释放通路。

Checklist Findings (12 fail / 48 total)

General Principles Checklist

  • [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue ViT proxy 的租约释放路由只以 lease_id 为键,而协议未约束其跨 worker 全局唯一
    _handle_routesslot.rdma_descriptor.lease_id 为唯一键映射 worker 地址;record_receipt(:80-84)一旦发现同一 handle 来自两个 worker,就把它移出路由表并写入 _handle_collisionsrelease()(:102)随后跳过转发——冲突会同时毒化两个真实租约,两侧都要等 slot_gc_timeout_ms(默认 60s)才回收。而 tensor_rdma.proto:27lease_id 只是 string,无任何全局唯一性约定,生成方是仓外 provider;若其用进程内计数器命名(仓内 fake 正是 lease-0 这种),多 worker proxy 下冲突将是常态,释放通路整体退化为纯 GC 兜底。
  • [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue releaseAsync 在队列饱和或停机时退化为同步 RPC,违反基类「不得延迟消费者」契约
    基类明确要求「Success-path release must not delay the consumer」(MMRemoteOutputTransport.h:79),而 MMRdmaReader.cc:158 正是在 consume() 成功路径(gRPC 请求线程)调用 releaseAsync()。队列在 kMaxPendingReleaseHandles = 1024 饱和或 stopping_ 时走 else 分支 releaseNow(endpoint, handles, release_timeout_ms_)(:127):既未使用请求剩余预算(对比 :102 的 std::min(release_timeout_ms_, remaining)),也可能超出请求 deadline,ViT 挂住时每个后续请求额外阻塞约 1s。另外 :120-122 的打点与 WARNING 位于 release_mutex_ 临界区内(至 :125),会连带阻塞其他请求线程入队。
  • [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue 析构排空未按 endpoint 合批且无总预算,进程关停耗时不可控
    releaseLoop(:163-186)把队列按 endpoint 合并成 batches,每个 endpoint 只发一次 ReleaseRdmaLease;但析构(:49-51)对换出的 pending 是逐 task 调用 releaseNow(task.endpoint, task.handles, release_timeout_ms_),既未合批也无整体时间预算。kMaxPendingReleaseHandles = 1024 约束的是句柄总数而非任务数,单句柄任务可达 1024 个;每次 deadline 为 1000ms 且串行,对端可连通但不响应时最坏累计约 1024 秒才结束析构。注释本身已承认「A failed RPC is still covered by the encoder-side TTL GC」。
  • [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue ViT proxy 的租约释放路由只以 lease_id 为键,而协议未约束其跨 worker 全局唯一
    _handle_routesslot.rdma_descriptor.lease_id 为唯一键映射 worker 地址;record_receipt(:80-84)一旦发现同一 handle 来自两个 worker,就把它移出路由表并写入 _handle_collisionsrelease()(:102)随后跳过转发——冲突会同时毒化两个真实租约,两侧都要等 slot_gc_timeout_ms(默认 60s)才回收。而 tensor_rdma.proto:27lease_id 只是 string,无任何全局唯一性约定,生成方是仓外 provider;若其用进程内计数器命名(仓内 fake 正是 lease-0 这种),多 worker proxy 下冲突将是常态,释放通路整体退化为纯 GC 兜底。
  • [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue RdmaExport::createBatch 只在 lease_id 为空时回滚,create() 抛异常时前序 lease 泄漏
    createBatch(:47-66)仅在 descriptor.lease_id.empty() 时回收已创建的 lease。create() 是虚函数,实现涉及 ibverbs/CUDA,抛异常是现实可能;此时异常直接穿出 createBatchexportSlots(两者都无 try/catch)、再穿过 exportEmbedding 到 Python 由 backend.py:96 捕获降级,而 descriptors 中已成功的 lease 不会被 release,只能等 RdmaConfig::slot_gc_timeout_ms(默认 60s)兜底。默认 max_slot_bytes = 1GiB,多 slot 场景下一次异常可能让可观的已注册显存被占住 60s,高 QPS 下持续累积。
  • [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue 降级原因被丢弃、reader 未注入 metrics,数据面回退率完全不可观测
    MMRdmaReader.cc:145/153retry(kReasonRdmaReadError/kReasonRdmaManifestError) 上报原因,fetch:92 也把 reason 传下来,但 degradeToTerminal 形参写作 const std::string& /*reason*/ 直接丢弃——两个常量成为死代码。MMTransportMetrics 注释称「shared by the control client and readers」(MMRemoteOutputTransport.h:49),但工厂只把 metrics 交给控制客户端(Factory.cc:30),reader 与 transport 都不持有它;releaseNow 失败仅 WARNING。唯一信号是 MMRdmaReader.cc:156 的每请求一行 [MM-RDMA-HIT] INFO 日志。
  • [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue 析构排空未按 endpoint 合批且无总预算,进程关停耗时不可控
    releaseLoop(:163-186)把队列按 endpoint 合并成 batches,每个 endpoint 只发一次 ReleaseRdmaLease;但析构(:49-51)对换出的 pending 是逐 task 调用 releaseNow(task.endpoint, task.handles, release_timeout_ms_),既未合批也无整体时间预算。kMaxPendingReleaseHandles = 1024 约束的是句柄总数而非任务数,单句柄任务可达 1024 个;每次 deadline 为 1000ms 且串行,对端可连通但不响应时最坏累计约 1024 秒才结束析构。注释本身已承认「A failed RPC is still covered by the encoder-side TTL GC」。
  • [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue releaseAsync 在队列饱和或停机时退化为同步 RPC,违反基类「不得延迟消费者」契约
    基类明确要求「Success-path release must not delay the consumer」(MMRemoteOutputTransport.h:79),而 MMRdmaReader.cc:158 正是在 consume() 成功路径(gRPC 请求线程)调用 releaseAsync()。队列在 kMaxPendingReleaseHandles = 1024 饱和或 stopping_ 时走 else 分支 releaseNow(endpoint, handles, release_timeout_ms_)(:127):既未使用请求剩余预算(对比 :102 的 std::min(release_timeout_ms_, remaining)),也可能超出请求 deadline,ViT 挂住时每个后续请求额外阻塞约 1s。另外 :120-122 的打点与 WARNING 位于 release_mutex_ 临界区内(至 :125),会连带阻塞其他请求线程入队。
  • [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue 唯一的生产 gRPC 控制面客户端零测试覆盖,四条并发路径均无验证
    GrpcMMControlClient 定义在匿名 namespace,唯一入口 createGrpcMMControlClient 全仓只被 MMRemoteOutputTransportFactory.cc:30 调用;测试用的是 FakeControlClientMMRemoteOutputTransportTest.cc:63),对真实客户端只覆盖了 createGrpcInlineReceiptReader()。本次新增的后台 release_thread_、condvar 唤醒、1024 句柄有界队列、饱和转同步、stopping_ 同步兜底、跨线程 pending_release_handles_ 记账、析构 swap+join+同步排空全部无覆盖。releaseNow 直接调 pool_.getConnection,无可替换发送接口。
  • [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue 唯一的生产 gRPC 控制面客户端零测试覆盖,四条并发路径均无验证
    GrpcMMControlClient 定义在匿名 namespace,唯一入口 createGrpcMMControlClient 全仓只被 MMRemoteOutputTransportFactory.cc:30 调用;测试用的是 FakeControlClientMMRemoteOutputTransportTest.cc:63),对真实客户端只覆盖了 createGrpcInlineReceiptReader()。本次新增的后台 release_thread_、condvar 唤醒、1024 句柄有界队列、饱和转同步、stopping_ 同步兜底、跨线程 pending_release_handles_ 记账、析构 swap+join+同步排空全部无覆盖。releaseNow 直接调 pool_.getConnection,无可替换发送接口。
  • [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue RDMA 描述符反序列化不校验尺寸一致性与上限,与导出侧 max_slot_bytes 不对称
    收据来自另一进程/另一台机器,属跨信任边界输入。但 fromProto(:90-117)只拒绝未知 dtype 枚举,不校验 TensorMetaPB.nbytesshape×itemsize 是否自洽、也不校验 offset + nbytes <= payload_bytes;消费侧 readAllSlots 只做 slot.roles_size() == tensors_size()MMRdmaReader.cc:169)与 tensors.size() == roles->size()(:208)两项检查。注意 RDMA_TENSOR_FLOAT32 = 0 是 proto3 默认值,provider 漏设 data_type 时 BF16 缓冲会被当 FP32 解释。而测试 fake 恰好做了这两条断言(MMRdmaTransportTest.cc:130)——校验写在了测试里而不是生产路径里。

Python Static-First Checklist

  • [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue exportSlots 不校验张量连续性与设备一致性,非连续输入会静默导出错误数据
    tensorBytesnumel() * element_size() 当字节跨度,exportSlots 据此推导 row_bytes = tensorBytes(embedding) / rows(:60)与 slotFootprint,并用 embedding.narrow(0, start, len) 切块交给 provider——两步都隐含「按行连续」,而 provider 只能按 data_ptr() + nbytes 搬运。exportEmbedding(:135)是 pybind 公开 API(:166 注册),C++ 侧既不校验 is_contiguous()/dtype,也不校验三类张量是否同设备;仅靠唯一调用方 rdma/backend.py:88-94.contiguous()/.to(device=...) 维持不变量。更糟的是测试 fake 在 memcpy 前也调用 tensors[i].contiguous()MMRdmaTransportTest.cc:86),非连续输入在单测中无法

Strengths

  • 协议违规 fail-loud 而非静默降级:MMRemoteOutputTransport.cc:74-82 对「从未 advertise 却收到 RDMA 收据」直接报错并 discard() 归还远端 slot,MMRemoteOutputTransportTest.cc:250-267 有专用用例并写清了不能降级的理由——RDMA 收据的 inline 字段为空,降级会解出「看似合法但内容为空」的 embedding。
  • 上一轮 P1 以接口契约方式收口:RdmaTransport.h:58-63 把隐式的 CUDA 流序假设写成 create() 的显式同步契约,责任归属清晰。
  • 降级预算共享而非重置:DeadlineBudgetfetch 入口一次建立(MMRemoteOutputTransport.cc:59),RPC、RDMA 读、release、回退请求共用同一预算,降级严格限一次;单测以 log == {request, read, release, request, terminal}requests == 2 钉住轮次与释放顺序。
  • 租约生命周期用 RAII 表达(MMRdmaReader.cc:89-109):失败路径析构同步归还,保证回退请求前远端资源已释放;成功路径 releaseAsync() 不阻塞消费者,encoder 侧另有 slot_gc_timeout_ms TTL GC 兜底。
  • MultimodalPbConverter::inlineOutputFromPb 把原先会让进程 abort 的 RTP_LLM_CHECK_WITH_INFO 换成 ErrorInfo 返回,并补齐 embedding 维度、split_size 求和、pos_id 行数、extra_input 数量四类校验,把「打挂进程」降级为「单请求失败」。
  • mode=grpc 时仍注册一个无 provider 的 MMRdmaReaderMMRemoteOutputTransportFactory.cc:24),使非法 RDMA 收据仍能被识别并归还,而不是落进 inline 解码路径。
  • 依赖环被显式打破:新增 mm_processor_config(torch-free)、multimodal_errormodel_rpc:multimodal_pb_converter 等精简 target 解开 model_rpcmultimodal_processor 的环,BUILD 内写明动机。
  • 回滚与开源可构建性都留了路:rdma_transport_no_impl 提供同签名空实现使 auto 自动回退,--mm_transport_mode=grpc 是一键关闭开关;MMProcessorDecision 在 engine 构造前 fail-fast 返回 FAILED_PRECONDITION
  • RemoteMultimodalEmbedding 调用无任何 deadline,新实现按 mm_timeout_ms + margin 显式设置 gRPC deadline,并在 _positive_int/_rdma_port/validateMMTransportMode 三处做启动期校验。
  • ViT 侧输出指标带 transport=rdma|bytes tag(base.py:67-89)并按 MMRdmaSlotPB.Role 分别累计字节;vit_metrics_test.py 删除的 3 个用例已在 mm_output_transport_test.py 中有等价替代且已接入 py_test


} // namespace

bool MMRdmaReader::advertise(const std::string& /*endpoint*/, MultimodalInputsPB& request_pb) {

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] RDMA 持续故障时缺少熔断,每请求放大为两次 ViT forward 且不自愈

advertise(:113-119)只要 ensureReader() 曾成功就恒返回 true,无连续失败计数、无熔断、无恢复窗口;ConfigModules.h:345mode 默认 auto。读失败或 manifest 不一致时走 degradeToTerminal,而该路径会重新发一次完整 RemoteMultimodalEmbedding;已核实 vit_rpc_server.py:99 每次都执行 mm_embedding_rpc 完整前向、无结果缓存。因此 NIC/QP/provider 稳态故障时,每个多模态请求都多跑一次完整 ViT 前向(ViT 算力近翻倍、TTFT 抬升),且系统永不收敛。

建议:MMRdmaReader 内按连续失败次数加一个带恢复窗口的开关:超过阈值后暂停 advertise(),窗口到期再放行一次探测,避免故障期无限重复双次前向。配合下一条的降级指标,让「已熔断」这一状态既可观测也可自动恢复。

ErrorResult<MultimodalOutput> MMRemoteOutputTransport::degradeToTerminal(const std::string& endpoint,
MultimodalInputsPB& request_pb,
DeliveryContext& context,
const std::string& /*reason*/) {

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] 降级原因被丢弃、reader 未注入 metrics,数据面回退率完全不可观测

MMRdmaReader.cc:145/153retry(kReasonRdmaReadError/kReasonRdmaManifestError) 上报原因,fetch:92 也把 reason 传下来,但 degradeToTerminal 形参写作 const std::string& /*reason*/ 直接丢弃——两个常量成为死代码。MMTransportMetrics 注释称「shared by the control client and readers」(MMRemoteOutputTransport.h:49),但工厂只把 metrics 交给控制客户端(Factory.cc:30),reader 与 transport 都不持有它;releaseNow 失败仅 WARNING。唯一信号是 MMRdmaReader.cc:156 的每请求一行 [MM-RDMA-HIT] INFO 日志。

建议:MMTransportMetricsPtr 下沉给 MMRemoteOutputTransportMMRdmaReader,以 reason 为 tag 上报降级 QPS(rdma_read_error|rdma_manifest_error|never_advertised),并补一个带 transport=rdma|inline tag 的成功计数与 release_failed 计数,让 60s slot GC 兜底的触发频次可见;同时让 degradeToTerminal 真正使用 reason。[MM-RDMA-HIT] 降为 DEBUG 或删除,避免高 QPS 下每请求一行日志。

Checklist: [6.1] 无 per-forward 调试日志 / 噪声热路径输出

for (auto& reader : readers_) {
reader->withdraw(request_pb);
}
if (context.budget.exhausted()) {

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] 降级重试只检查预算是否耗尽、未预留最小预算,可能把可恢复的 RDMA 失败变成硬失败

degradeToTerminal 仅在 context.budget.exhausted()remainingMs() <= 0)时拒绝重试,否则无条件再发一次完整 RemoteMultimodalEmbedding。回退请求让 ViT 重做整个前向(含下载与预处理),首次耗时不退还。当首次尝试接近预算上限(默认 mm_timeout_ms + 5s,通常 125s)时,剩余毫秒级预算下的重试必然以 DEADLINE_EXCEEDED 结束:原本「RDMA 读失败但 inline 可用」的请求被转成硬失败,错误信息还丢掉了原始 reason。mode 默认 auto,该路径默认开启。

建议:degradeToTerminal 中加最小预算门限(如 remainingMs() < rpc_timeout_margin_ms_,或可配置的 min_fallback_budget_ms)时直接放弃重试,并返回携带原始 reason 的明确错误,而不是发出一个注定超时的 RPC,在 ViT 侧白烧一次前向。

releaseNow(endpoint, handles, std::min(release_timeout_ms_, remaining));
}

void releaseAsync(std::string endpoint, std::vector<std::string> handles) override {

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] releaseAsync 在队列饱和或停机时退化为同步 RPC,违反基类「不得延迟消费者」契约

基类明确要求「Success-path release must not delay the consumer」(MMRemoteOutputTransport.h:79),而 MMRdmaReader.cc:158 正是在 consume() 成功路径(gRPC 请求线程)调用 releaseAsync()。队列在 kMaxPendingReleaseHandles = 1024 饱和或 stopping_ 时走 else 分支 releaseNow(endpoint, handles, release_timeout_ms_)(:127):既未使用请求剩余预算(对比 :102 的 std::min(release_timeout_ms_, remaining)),也可能超出请求 deadline,ViT 挂住时每个后续请求额外阻塞约 1s。另外 :120-122 的打点与 WARNING 位于 release_mutex_ 临界区内(至 :125),会连带阻塞其他请求线程入队。

建议: 「不丢弃租约」的取舍合理,但不应以阻塞消费方为代价:encoder 侧已有 slot_gc_timeout_ms TTL GC 兜底,建议饱和时改为丢弃队首旧任务后入队(保持有界且不阻塞),或至少把同步兜底 timeout 改为请求剩余预算并压到远小于 1s。同时把 reportRpcClientErrorRTP_LLM_LOG_WARNING 移出 release_mutex_ 临界区。

Checklist: [6.1] 可观测性:日志/指标/超时可操作、非噪声;[6.1] LSP:子类/重写保持基类契约

Comment thread rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc Outdated
static_cast<long>(embedding.dim()));
return false;
}
const uint64_t row_bytes = tensorBytes(embedding) / static_cast<uint64_t>(rows);

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] exportSlots 不校验张量连续性与设备一致性,非连续输入会静默导出错误数据

tensorBytesnumel() * element_size() 当字节跨度,exportSlots 据此推导 row_bytes = tensorBytes(embedding) / rows(:60)与 slotFootprint,并用 embedding.narrow(0, start, len) 切块交给 provider——两步都隐含「按行连续」,而 provider 只能按 data_ptr() + nbytes 搬运。exportEmbedding(:135)是 pybind 公开 API(:166 注册),C++ 侧既不校验 is_contiguous()/dtype,也不校验三类张量是否同设备;仅靠唯一调用方 rdma/backend.py:88-94.contiguous()/.to(device=...) 维持不变量。更糟的是测试 fake 在 memcpy 前也调用 tensors[i].contiguous()MMRdmaTransportTest.cc:86),非连续输入在单测...

建议:exportSlots 入口统一校验 is_contiguous()、dtype 属于 TensorDataType 支持集合、以及 embedding/pos_id/extra 设备一致,不满足时记 WARNING 并 return false(自然走 inline 回退),或在内部先 .contiguous() 归一化。并补一条非连续张量输入的单测,同时让测试 fake 停止替调用方做 .contiguous()

Checklist: [P.G] mock/fake/stub 不得替代本次声称覆盖的生产边界

Comment thread rtp_llm/cpp/rdma_transport/RdmaTransport.cc Outdated
class RdmaRead {
public:
virtual ~RdmaRead() = default;
virtual RdmaReadResult read(const std::vector<RdmaDescriptor>& descriptors, int64_t timeout_ms = 0) = 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.

[P2] RdmaRead::read 未声明返回顺序与内存所有权契约,在库 fake 拷贝数据反而掩盖该要求

RdmaExport::create 已补上明确的同步契约(:58-63),但对侧 RdmaRead::read(:70-74)一行注释也没有。MMRdmaReader::readAllSlots 把多个 descriptor 的 manifest 按顺序压平成 roles,仅以数量对齐校验,随后 assembleMMRdmaOutput 按下标与 roles[i] 逐一配对(:31-53)——隐式要求 provider 返回严格等于「descriptor 顺序 × manifest 顺序」的展开。返回张量的所有权同样未声明,而这些张量会经 torch::cat/split 产出视图交给引擎长期持有,consume() 还在 releaseAsync() 之前完成组装。仓内 fake 恰好 view.clone()MMRdmaTransportTest.cc:133)满足了这个未写明的要求。

建议: 参照 create 的写法为 read 补对称契约注释:返回张量必须按 descriptor 顺序、descriptor 内按 manifest 顺序严格一一对应;read 返回即表示数据对调用方当前流可见;tensors 的所有权归调用方,provider 不得在 release 后回收其底层内存;status 非 ok 时 tensors 必须为空。若实现只能返回内部缓冲视图,则应显式要求消费侧 clone 并在 readAllSlots 中落实。

Comment thread rtp_llm/cpp/rdma_transport/RdmaTransport.cc Outdated
if handle in self._handle_collisions:
continue
existing = self._handle_routes.get(handle)
if existing is not None and existing[0] != worker_address:

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] ViT proxy 的租约释放路由只以 lease_id 为键,而协议未约束其跨 worker 全局唯一

_handle_routesslot.rdma_descriptor.lease_id 为唯一键映射 worker 地址;record_receipt(:80-84)一旦发现同一 handle 来自两个 worker,就把它移出路由表并写入 _handle_collisionsrelease()(:102)随后跳过转发——冲突会同时毒化两个真实租约,两侧都要等 slot_gc_timeout_ms(默认 60s)才回收。而 tensor_rdma.proto:27lease_id 只是 string,无任何全局唯一性约定,生成方是仓外 provider;若其用进程内计数器命名(仓内 fake 正是 lease-0 这种),多 worker proxy 下冲突将是常态,释放通路整体退化为纯 GC 兜底。

建议:tensor_rdma.proto 中以注释明确约定 lease_id 必须全局唯一(例如 host:port 前缀 + 进程内单调计数 + 随机后缀),并在 provider 契约文档中同步;同时把 proxy 侧路由键改为 (worker_addr, lease_id) 复合键(收据已携带来源 worker),或在冲突时保留首个映射并仅对冲突项上报指标,避免一次命名冲突同时毒化两个真实租约的释放通路。

Checklist: [6.1] 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全;[6.1] 状态不变量:创建/更新/失败/重试/回滚路径有效

@LLLLKKKK
LLLLKKKK dismissed stale reviews from themself August 31, 2026 12:26

LGTM:阻断已解除(rtpcli 自动清除旧红标)

@ydshi0
ydshi0 force-pushed the feature/rdma_transport branch from 5bed256 to 79ac9d4 Compare September 1, 2026 06:25

@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 #1353

Status: BLOCKING

Summary: P0/0 · P1/1 · P2/28 · P3/12

Reviewed: commit 79ac9d40ef09 · 2026-09-01 15:29 UTC+8

Blocking Issues

P1

  • protobuf 拆分后生成模块链接清单未同步,按文档与多机脚本部署会导入失败 @ rtp_llm/cpp/model_rpc/proto/model_rpc_service.proto:3
    • 建议:在 install.md 与 multi_local_executor.sh 中补链 multimodal_output_pb2.pytensor_rdma_pb2.py 及其 grpc 产物(含所需子目录),并更新 .gitignore;更稳妥的做法是遍历 proto 输出目录自动软链,避免清单再次漂移。同时在 PR 说明中标注 TensorPBMultimodalInputPB 等符号的 Python 导入位置已迁移,提醒仓外自行生成 stub 的调用方需同时拷贝三个 proto 文件。

Non-blocking Suggestions

P2

  • 默认 auto 模式会静默改变既有多模态部署的数据面 @ rtp_llm/config/py_config_modules.py:291
    • 建议:默认保持 grpc,由部署显式开启 auto;若必须默认启用,请在发布说明中给出平台矩阵、LLM 与 ViT 两侧版本兼容矩阵、滚动升级顺序与一键回滚开关。
  • RDMA 持续故障时缺少熔断,每请求放大为两次 ViT forward 且不自愈 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:113
    • 建议:按 endpoint 增加连续失败计数与熔断冷却:达到阈值后暂停 advertise,冷却期后小流量探测恢复;并补充"跨请求持续失败"的回归测试,断言熔断生效后不再产生额外 ViT 请求。
  • 降级原因被丢弃、reader 未注入 metrics,数据面回退率完全不可观测 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc:100
    • 建议:将 metrics 注入 reader,新增按 endpoint/plane/reason 打标的降级计数与 RDMA 命中计数,由 degradeToTerminal()consume() 成功路径分别上报;把 [MM-RDMA-HIT] 改为命中计数指标,或降为 DEBUG 并限频。测试中断言上报次数。
  • 降级重试只检查预算是否耗尽、未预留最小预算,可能把可恢复的 RDMA 失败变成硬失败 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc:104
    • 建议:为 fallback 预留一个最小可用预算(例如可配置的 min_fallback_budget_ms),不足时直接返回带原始降级 reason 的明确错误;并补充小预算场景的单测,断言不再发出注定超时的第二次请求。
  • releaseAsync 在队列饱和或停机时退化为同步 RPC,违反基类「不得延迟消费者」契约 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:105
    • 建议:溢出时保持异步语义:或丢弃入队并依赖导出侧 slot_gc_timeout_ms TTL GC 兜底,或扩容/分片队列;如确需背压,应在基类契约与文档中显式声明该例外,并补充队列饱和下的延迟测试。
  • 析构排空未按 endpoint 合批且无总预算,进程关停耗时不可控 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:49
    • 建议:析构时复用 releaseLoop() 的 endpoint 合批逻辑,并为整轮 drain 设置统一的全局关停预算(例如总计 1-2s);预算耗尽即放弃,交由导出侧 TTL GC 回收。补充 endpoint 不可达时的关停耗时测试。
  • 唯一的生产 gRPC 控制面客户端零测试覆盖 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:213
    • 建议:将 gRPC stub 调用抽为可注入接缝(或注入 in-process gRPC server),补充队列饱和、停机后提交、析构排空耗时、重复释放幂等及各状态码映射的单测。
  • exportSlots 不校验张量连续性与设备一致性,非连续输入会静默导出错误数据 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaExporter.cc:35
    • 建议:在 exportSlots 入口对每个张量断言 contiguous、同 device、dtype 受支持,不满足时返回 false 走 inline 并告警;补充非连续/跨设备输入的测试,使生产边界不再依赖调用方自觉。
  • RDMA 描述符反序列化不校验尺寸一致性与上限,与导出侧 max_slot_bytes 不对称 @ rtp_llm/cpp/rdma_transport/RdmaTransport.cc:90
    • 建议:在交给 provider 前校验必填字段非空、shape 各维非负、nbytes 与 shape×元素字节一致,并以防溢出形式校验 offset <= payload_bytesnbytes <= payload_bytes - offset,同时限制 manifest 条数与单 slot 上限。
  • RdmaRead::read 未声明返回顺序与内存所有权契约 @ rtp_llm/cpp/rdma_transport/RdmaTransport.h:73
    • 建议:在 RdmaRead::readRdmaExport::create/createBatch 注释中明确展平顺序、逐下标对应、返回内存所有权与允许的失败形式,并在契约测试中固化顺序断言。
  • createBatch 只在 lease_id 为空时回滚,create() 抛异常时前序 lease 泄漏 @ rtp_llm/cpp/rdma_transport/RdmaTransport.cc:51
    • 建议:用 try/catch 或 RAII 守卫包裹循环,异常路径同样执行 release(lease_ids) 后再重新抛出或返回空;并在接口注释中明确 create() 的异常语义与资源归属。
  • ViT 代理的租约释放路由只以 lease_id 为键,而生产者契约未约束其跨 worker 全局唯一 @ rtp_llm/multimodal/transport/proxy_router.py:79
    • 建议:在 RdmaExport::createMMOutputProxyRouter 的契约注释中明确 lease_id 必须跨 exporter 进程全局唯一(UUID 或"进程标识 + 计数器"),或由代理生成不透明 handle 映射到 (worker, provider_lease_id);并为冲突指标配置告警。
  • deadline 耗尽时代理已提前摘除租约路由,导致重试永久失路由 @ rtp_llm/multimodal/transport/proxy_router.py:105
    • 建议:先只读查询路由并在对应分组发送成功后再摘除;对未发送或发送失败的分组保留/恢复映射,并把 deadline 检查提前到摘除之前。
  • RDMA 数据面反向依赖 pybind 配置提取模块,形成包级双向依赖 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaExporter.cc:7
    • 建议:删除数据面类的 py::object 构造函数与 py::bytes 返回,改由 MMRdmaExporterInit.ccpy::init lambda 调用 extractRdmaConfig 并完成 bytes 转换;transport 层只接受 RdmaConfig、返回 protobuf 或普通 C++ 数据,依赖方向恢复为单向 pybind → transport。
  • MultimodalPbConverter 仍拉入完整 gRPC service 依赖,BUILD 注释的「Lean」未达成 @ rtp_llm/cpp/model_rpc/MultimodalPbConverter.h:7
    • 建议:统一改为 include multimodal_output.pb.h,并把 multimodal_pb_convertertensor_pb_convert 的 proto 依赖收敛到 //rtp_llm/cpp/model_rpc/proto:multimodal_output_cc
  • ViT RPC 超时预算与 margin 在四处独立编码,改一处会静默失配 @ rtp_llm/cpp/pybind/ConfigExtract.h:47
    • 建议:在 C++ 配置层定义唯一具名 worker 默认预算与 margin 常量,client deadline 一律由「该常量 + margin」派生,其余位置全部引用同一来源;Python 侧注释注明同步契约,并增加锁定「Python 默认预算 ⇒ C++ 默认 deadline」推导关系的测试。
  • mm_processor_ 为空时 HTTP 与 gRPC 入口的多模态语义不一致 @ rtp_llm/cpp/model_rpc/LocalRpcServer.cc:171
    • 建议:统一两个入口的语义:先判断是否携带多模态输入,再在 processor 为空时返回明确错误;并为 prepareInput 与 HTTP 入口各补一例「processor 为空 + 携带多模态输入」的测试。
  • GPU-NIC 亲和物理键缺失时回退 local rank 会绑错网卡,且整段逻辑无单测 @ rtp_llm/config/server_config_setup.py:642
    • 建议:物理 GPU 与 local rank 不同时不要静默使用 rank 键,仅对可明确判定为 local-rank 格式的旧映射回退,否则返回失败并记 warning;补充 mock nvidia-smi 与亲和工具的单测,覆盖未设置/数字/UUID/非法项/越界 rank、显式 ACCL_USE_NICS、非法 JSON、物理键缺失与 rank 回退。
  • RDMA exporter 与其依赖的 compute ops 不在同一 runfiles 目录 @ rtp_llm/libs/BUILD:50
    • 建议:将 librtp_compute_ops_so 一并加入 libs(或让 _load_rdma_ops 先调用 _load_compute_ops),确保两者在 runfiles 同目录;并把 exporter 导入失败日志提升为 warning 并保留原始异常。
  • RDMA 懒初始化一次失败即永久静默降级,且构造在持锁持 GIL 下进行 @ rtp_llm/multimodal/transport/rdma/backend.py:60
    • 建议:在接流前预初始化 exporter,或在提取配置后释放 GIL 再创建 provider;增加"永久失败"快速判定以避免加锁,并补充带原因的降级指标与显式重试策略。
  • 多 worker 使用固定 RDMA 端口会让多数 worker 永久回退 gRPC @ rtp_llm/server/server_args/vit_group_args.py:312
    • 建议:在 vit_server_count > 1 && mm_rdma_port != 0 时启动即 fail-fast,或按 worker ID 自动偏移端口;同时增加 exporter 初始化失败指标,并在参数 help 中写明该约束。
  • ReleaseRdmaLease 缺少指标,释放持续失败只能等 GC 兜底 @ rtp_llm/server/vit_rpc_server.py:164
    • 建议:让 backend 返回可统计的结果,补充 release 数量、耗时、失败原因、活跃 slot 数与 GC 回收量指标,并增加 worker 侧异常隔离测试。
  • roles 与 tensors 的等长逐下标对应契约未写入 proto 注释 @ rtp_llm/cpp/model_rpc/proto/multimodal_output.proto:54
    • 建议:在 roles 字段注释中明确「与 rdma_descriptor.tensors 等长且逐下标一一对应」,并说明多个 EXTRA_INPUT 按原始顺序排列的要求。
  • 多模态 protobuf 双向字段映射被拆分在两个类中 @ rtp_llm/cpp/model_rpc/QueryConverter.cc:256
    • 建议:把 PB → 多模态输入的解码也迁入 MultimodalPbConverter,使双向字段映射集中维护,并补充一个往返(round-trip)测试固化全字段。
  • C++ 传输编排、工厂装配与决策入口的关键分支未被测试 @ rtp_llm/cpp/multimodal_processor/test/MMRemoteOutputTransportTest.cc:217
    • 建议:增加可注入超时与装配点的 harness,补充 deadline 耗尽、回退仍携 RDMA receipt、resolveRpcTimeoutMs 表驱动用例并断言请求次数与句柄释放;为工厂增加聚焦测试(两种模式的能力声明、超时透传、非法模式 EXPECT_THROW);为 resolveAndLogMMProcessorKind 补 INVALID/LOCAL/REMOTE/NONE 四例,分别断言 ok() 与 error 是否为空。
  • Python 侧 RDMA 探测、懒初始化与传输生命周期缺少测试 @ rtp_llm/multimodal/test/mm_output_transport_test.py:66
    • 建议:用 fake exporter 覆盖 unavailable、disabled、构造异常、只尝试一次、close 后回退、supports=False 跳过与 release/close 故障隔离场景。
  • BF16 与空输出的指标分支在测试迁移后失去覆盖 @ rtp_llm/multimodal/test/mm_output_transport_test.py:196
    • 建议:补充 BF16 embedding 字节数断言,并以空 MMEmbeddingRes 验证空 receipt、空 split_size 与各项 payload 指标为零。
  • 新增传输配置、CLI/环境变量参数与代理路由生产分支缺少回归测试 @ rtp_llm/server/server_args/vit_group_args.py:294
    • 建议:增加 CLI 与环境变量两条路径的参数化测试,断言全部字段的绑定落点,并覆盖端口 0/65535/65536、正数、非负数、非法模式与 max_slot_bytes=0;构造带真实 MMTransportConfig 的 router,断言 TTL、下界钳制与传给 ReleaseRdmaLease 的 timeout。

P3

  • RDMA 交付的设备张量会在 prefill 前置路径触发整块 D2H 同步 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:72
    • 建议:在 PR 说明中给出含该 D2H 的端到端收益基准,或评估在设备上计算哈希以消除该同步;避免收益预期与实际不符。
  • 新增代码未通过仓库强制的格式化规则 @ rtp_llm/cpp/model_rpc/QueryConverter.cc:315
    • 建议:对本次改动的全部 C++ 与 Python 文件运行仓库 pre-commit(clang-format / black / isort),并删除未使用导入,避免后续修改产生无关格式 diff。
  • RDMA 回退路径日志描述不准确,禁用替身接口也不一致 @ rtp_llm/multimodal/transport/rdma/backend.py:41
    • 建议:日志改为「回退 inline bytes」;为 DisabledMMRdmaExporter 增加返回 False 的静态 available(),调用方直接调用统一接口;注册阶段说明初始化延迟,并在 exporter 真正成功/失败时记录准确状态。
  • 重复实现已有的角色字符串转换与 deadline 解析 @ rtp_llm/cpp/multimodal_processor/MMProcessorConfig.h:38
    • 建议:roleTypeName 直接复用 roleTypeToString,或在 RoleTypes.h 提供统一的无分配字符串接口;把 deadline 解析下沉为双方共享的 gRPC 工具函数。
  • GPU-NIC 亲和文件依赖进程当前目录,且失败日志级别过弱 @ rtp_llm/config/server_config_setup.py:589
    • 建议:明确并使用绝对输出路径且在日志中打印实际路径;工具存在但执行/读取/解析失败时提升为 warning。
  • ViT worker 重复加载 GPU-NIC 亲和并存在并发探测风险 @ rtp_llm/multimodal/vit_start_server.py:40
    • 建议:只保留父进程加载,worker 仅调用 setup_cuda_device_and_accl_env;若子进程调用是为独立启动兜底,应显式区分入口并使用绝对输出路径。
  • engine 停止异常会跳过 transport 清理 @ rtp_llm/server/vit_rpc_server.py:178
    • 建议:用 try/finally 保证 _transport.close() 始终执行,并补充 engine 停止失败时仍完成 transport 清理的测试。
  • proto_utils 留有死参数并修改全局模块状态 @ rtp_llm/flexlb/tools/online_eval/online_eval/proto_utils.py:50
    • 建议:删除无用参数;在 docstring 中明确该工具仅适用于独立进程及其模块副作用;调用方迁移到 multimodal_output_pb2 后移除兼容补丁。
  • tensor_rdma 生成类脱离 FlexLB Java 包约定 @ rtp_llm/flexlb/flexlb-grpc/pom.xml:112
    • 建议:为 tensor_rdma.proto 注入明确的 org.flexlb Java 包,并把「复制 proto + 注入包名」抽成统一的可复用步骤。
  • 测试直接构造私有状态并全局替换标准库时钟 @ rtp_llm/server/test/vit_proxy_server_test.py:1110
    • 建议:统一通过 record_receipt() 建立路由;注入局部时钟或 patch 模块内独立导入的 monotonic 名称。
  • protobuf 规则的无条件调试输出放大构建日志噪声 @ bazel/py_proto.bzl:12
    • 建议:删除这四条 print(),必要诊断改为显式启用的调试机制(如 --define 开关)。
  • 新增 Python 子包缺少 init.py @ rtp_llm/multimodal/transport/__init__.py:1
    • 建议:为两个子目录补充 __init__.py,与父包及仓库现有包结构保持一致。

Checklist Findings (23 fail / 48 total)

General Principles Checklist

  • [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue MultimodalPbConverter 仍拉入完整 gRPC service 依赖,BUILD 注释的「Lean」未达成
    该类只使用已独立到 multimodal_output.proto 的消息,头文件却仍 include model_rpc_service.pb.hmodel_rpc/BUILD:29multimodal_pb_converter 及其依赖的 tensor_pb_convert 也都依赖 model_rpc_service_cc_proto(含 grpc codegen 与 grpc_buffer_list_link_fix)。因此 multimodal processor 与传输层仍被完整 service 代码生成拉入,BUILD:21 注释声称的「Lean … without a dependency cycle」只兑现了「无环」。MMRdmaReader.cc:10 同样 include 全量头,而 MMRdmaExporter.h 已改用 multimodal_output.pb.h,两侧不一致。
  • [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue roles 与 tensors 的等长逐下标对应契约未写入 proto 注释
    MMRdmaReader::readAllSlots(MMRdmaReader.cc:169-183)只校验 slot.roles_size() == slot.rdma_descriptor().tensors_size(),随后按下标把 roles[i] 赋给第 i 个张量;Python encoder 也用 zip(slot.roles, slot.rdma_descriptor.tensors) 遵循同一约定。但 roles 字段注释未说明等长与逐下标对应关系,也未说明重复 EXTRA_INPUT 的顺序语义。第三方或 Java 实现即使长度正确,也可能因顺序错位而静默把 pos_id 当成 embedding,产生错误推理结果而非报错。
  • [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue MultimodalPbConverter 仍拉入完整 gRPC service 依赖,BUILD 注释的「Lean」未达成
    该类只使用已独立到 multimodal_output.proto 的消息,头文件却仍 include model_rpc_service.pb.hmodel_rpc/BUILD:29multimodal_pb_converter 及其依赖的 tensor_pb_convert 也都依赖 model_rpc_service_cc_proto(含 grpc codegen 与 grpc_buffer_list_link_fix)。因此 multimodal processor 与传输层仍被完整 service 代码生成拉入,BUILD:21 注释声称的「Lean … without a dependency cycle」只兑现了「无环」。MMRdmaReader.cc:10 同样 include 全量头,而 MMRdmaExporter.h 已改用 multimodal_output.pb.h,两侧不一致。
  • [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue protobuf 规则的无条件调试输出放大构建日志噪声
    _generate_grpc_proto_impl 每次分析都无条件 print() 四条路径(:12-15)。本次新增 multimodal_output_pytensor_rdma_py 两个规则实例,使所有依赖这些 proto 的构建分析日志进一步增加。
  • [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue 析构排空未按 endpoint 合批且无总预算,进程关停耗时不可控
    析构函数 swap 出 pending 队列后,对每个 ReleaseTask 逐个调用 releaseNow(..., release_timeout_ms_),每个任务重新获得完整 1s 超时。正常工作线程 releaseLoop()(同文件:165-184)会按 endpoint 合并成 batches,析构路径没有复用该合并逻辑;队列上限为 1024 个 handle 而每个任务可只含一个 handle,ViT 可连接但挂起时关停耗时可线性增长到分钟级别。
  • [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue proto_utils 留有死参数并修改全局模块状态
    _ensure_proto_modulesmodule_name 参数(:52)在函数体内未被使用。该函数还把临时生成目录插入真实包的 __path__(:126),并向 model_rpc_service_pb2 动态挂载 7 个消息(:83);这些进程级副作用会改变后续模块解析结果与模块公开属性。
  • [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue 多 worker 使用固定 RDMA 端口会让多数 worker 永久回退 gRPC
    proxy 模式下多个 worker 由同一份配置 spawn(vit_start_server.py 共享 py_env_configs.vit_config),各自用相同 mm_rdma_bind_ip/port 延迟构造 exporter。--mm_rdma_port 默认 0(随机端口)尚可,一旦显式指定非 0,通常只有一个进程能绑定成功;其余构造异常在 rdma/backend.py:70 被吞掉且 _exporter_init_attempted 已置位,永久不再重试,也没有独立的降级指标或健康状态,表现为静默性能回退。参数 help(:317)未说明该约束。
  • [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue 降级原因被丢弃、reader 未注入 metrics,数据面回退率完全不可观测
    reader 区分 rdma_read_errorrdma_manifest_error 并通过 ConsumeResult::retry(reason) 传出,fetch() 也把它传给 degradeToTerminal(),但该形参签名为 const std::string& /*reason*/ 被直接忽略。MMTransportMetrics 只有 RPC 错误、RT 与字节数三类指标,metrics 也只注入 gRPC 控制面客户端而未注入 reader。相反,成功路径在 MMRdmaReader.cc:156 每请求输出 [MM-RDMA-HIT] INFO 日志,高 QPS 下日志量随请求线性增长。
  • [6.1] Quality — 逻辑变更未混入无关格式化 → issue 新增代码未通过仓库强制的格式化规则
    多模态转换函数迁出后,QueryConverter.cc:315-319 留下 5 个连续空行,违反 .clang-formatMaxEmptyLinesToKeep: 1MMGrpcTransport.cc:22-25 与 :193-196 的常量、成员声明对齐也不符合仓库 clang-format。Python 侧 vit_proxy_server.py:10import numbers 未按 stdlib 字母序排列、model_rpc_client.py/vit_rpc_server.py/proto_utils.py 的 import 顺序与行宽不符合 black + isort --profile=blackmm_output_transport_test.py:10 还保留了未使用的 MultimodalOutputPB 导入。
  • [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue tensor_rdma 生成类脱离 FlexLB Java 包约定
    Maven 预处理为 engine_rpc_service.proto(:118-121)与 multimodal_output.proto(:122-125)注入了 option java_package = "org.flexlb.engine.grpc",但对同样被复制的 tensor_rdma.proto 未做注入。后者声明 package rdma_transport,生成类会落入独立 Java 包。全仓检索确认目前无 Java 代码引用这些消息,但包布局与其余 FlexLB 生成代码不一致。
  • [6.1] Software Engineering — ISP:调用方不依赖无关大接口 → issue RDMA 数据面反向依赖 pybind 配置提取模块,形成包级双向依赖
    本行 include rtp_llm/cpp/pybind/ConfigExtract.h,使 transport/rdma:mm_rdma_exporter 依赖 pybind:config_extract,而 pybind:mm_rdma_exporter_pybind 又依赖该 exporter,构成 pybind ↔ transport/rdma 双向包依赖。根因是数据面类直接提供 py::object 构造函数(同文件:27)并返回 py::bytes(:135),迫使底层实现及其 C++ 测试理解 Python 对象,并为解析 7 个 RDMA 字段传递依赖整个 config_modules//:rtp_compute_ops
  • [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue proto_utils 留有死参数并修改全局模块状态
    _ensure_proto_modulesmodule_name 参数(:52)在函数体内未被使用。该函数还把临时生成目录插入真实包的 __path__(:126),并向 model_rpc_service_pb2 动态挂载 7 个消息(:83);这些进程级副作用会改变后续模块解析结果与模块公开属性。
  • [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue RdmaRead::read 未声明返回顺序与内存所有权契约
    MMRdmaReader::readAllSlots 把多个 descriptor 的 tensor 展平后与 roles 按下标一一对应,只校验总数相等;assembleMMRdmaOutput 完全依赖该顺序赋予语义。但 read() 的声明未规定返回 tensor 必须按 descriptor 顺序、descriptor 内按 tensors 顺序返回,也未说明返回内存的所有权与生命周期。不同 provider 顺序不一致时,会静默把 POS_ID 当作 EMBEDDING 使用而不报错。同样,createBatch 注释声称"any group fails 即全部回滚",但实现只覆盖 lease_id 为空这一种失败形式。
  • [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue 唯一的生产 gRPC 控制面客户端零测试覆盖
    GrpcMMControlClient 新增后台线程、有界队列、饱和同步降级、析构排空与 gRPC 状态码到 ErrorCode 的映射(UNAVAILABLE/DEADLINE_EXCEEDED → MM_REMOTE_RPC_FAILED,其余 → UNKNOWN_ERROR),但全仓检索确认没有任何测试调用 createGrpcMMControlClientMMRemoteOutputTransportTest.cc 的 5 个用例全部使用自定义假 MMControlClient。因此队列边界、stopping_ 竞态、析构残留任务与真实状态码映射均无可执行验证。
  • [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue 测试直接构造私有状态并全局替换标准库时钟
    三处用例(:1110、:1121、:1145)直接写入 router._handle_routes,绕过生产写入口 record_receipt(),因此守不住路由元组格式与消费语义的契约;一旦 record_receipt 改变元组结构,测试仍会通过。对 rtp_llm.multimodal.transport.proxy_router.time.monotonic 的 patch(:1124)实际修改共享标准库模块属性,同进程并发测试中可能影响其他组件。
  • [6.1] Tests — 被删除测试有等价替代覆盖 → issue BF16 与空输出的指标分支在测试迁移后失去覆盖
    输出编码指标测试已从 vit_metrics_test.py(该文件仍保留 preprocess 指标用例)迁到本文件,但替代用例只使用 fp32/int32/fp16,而 grpc/backend.py:17-23_tensor_pb_bytes 仍统计 bf16_data,该分支无任何断言。GrpcInlineOutputBackend._build_receipt()(同文件:45-47)在 embeddings 为空时返回空 MultimodalOutputPB(),这条返回形态(空 split_size、payload 指标全 0)也未经 inline backend 与指标上报验证。
  • [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue 新增传输配置、CLI/环境变量参数与代理路由生产分支缺少回归测试
    本次新增 9 个 CLI/环境变量参数与 4 个自定义转换器(_convert_mm_transport_mode_rdma_port_positive_int_non_negative_int),并首次把参数绑定到 output_transport.rdma/control 嵌套对象;全仓检索确认没有任何测试命中 MM_TRANSPORT_MODEMM_RDMA_*。同时生产服务始终向 MMOutputProxyRouter 传入 transport_config(TTL 由 slot_gc_timeout_ms + 5s 推导、release deadline 由 release_timeout_ms 推导),但 vit_proxy_server_test.py 的 4 个 router 用例全部传 transport_config=None,只覆盖默认 120s/1s。绑定异常在 _apply_config_bindings 中只记 warning,字段漂移会静默保留默认值。

RTP-LLM Checklist

  • [I] 代码质量 — 删除或重命名内部 file、registry entry、model name、metric enum、op binding、plugin symbol 时,必须全仓搜索消费者,并提供替代实现、迁移说明或 smoke 覆盖;只有暴露到 HTTP/RPC/config/persisted format 时才按外部兼容性处理 → issue protobuf 拆分后生成模块链接清单未同步,按文档与多机脚本部署会导入失败
    本行新增 import "rtp_llm/cpp/model_rpc/proto/multimodal_output.proto",后者再 import tensor_rdma.proto,因此生成的 model_rpc_service_pb2.py 会连锁导入 multimodal_output_pb2tensor_rdma_pb2。但 docs/start/install.md:35-36rtp_llm/test/perf_test/multi_node/multi_local_executor.sh:100-101 仍只 ln -sf 两个旧生成文件,.gitignore:44-45 也未覆盖新产物。按这两条入口构建后启动服务,import 阶段即 ModuleNotFoundError。Bazel 目标链本身正确,问题仅在手工链接清单。
  • [I] 代码质量 — 同一功能用统一工具函数 → issue 新增 Python 子包缺少 __init__.py
    transport/grpctransport/rdma 目录下均无 __init__.py(该目录仅有 backend.py),当前只依赖隐式命名空间包与 BUILD 中的 Python 文件 glob。若后续切换为常规包发现或使用打包工具的 find_packages(),这两个子包可能被静默遗漏。

Python Static-First Checklist

  • [P.A] 静态结构与类型纪律 — 禁止 getattr/setattr literal 访问 → issue RDMA 回退路径日志描述不准确,禁用替身接口也不一致
    实现不可用时日志称「skip GPU-NIC affinity」(:44),但亲和已由启动流程独立执行,真正发生的是输出回退 inline bytes。该分支还用 getattr(MMRdmaExporter, "available", None) 做字符串属性探测:真实 exporter 通过 def_static("available", ...)(MMRdmaExporter.cc:167)始终提供该方法,而 DisabledMMRdmaExporter(ops/init.py:308)只提供 enabled(),迫使调用方动态探测。factory.py:28 又在 exporter 尚未懒构造时就记录「backend enabled」,掩盖随后的初始化失败。
  • [P.G] 测试规范 — mock.patch target 是使用处而非定义处 → issue 测试直接构造私有状态并全局替换标准库时钟
    三处用例(:1110、:1121、:1145)直接写入 router._handle_routes,绕过生产写入口 record_receipt(),因此守不住路由元组格式与消费语义的契约;一旦 record_receipt 改变元组结构,测试仍会通过。对 rtp_llm.multimodal.transport.proxy_router.time.monotonic 的 patch(:1124)实际修改共享标准库模块属性,同进程并发测试中可能影响其他组件。
  • [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue 测试直接构造私有状态并全局替换标准库时钟
    三处用例(:1110、:1121、:1145)直接写入 router._handle_routes,绕过生产写入口 record_receipt(),因此守不住路由元组格式与消费语义的契约;一旦 record_receipt 改变元组结构,测试仍会通过。对 rtp_llm.multimodal.transport.proxy_router.time.monotonic 的 patch(:1124)实际修改共享标准库模块属性,同进程并发测试中可能影响其他组件。
  • [P.G] 测试规范 — 数据驱动测试用 pytest.mark.parametrize → issue 新增传输配置、CLI/环境变量参数与代理路由生产分支缺少回归测试
    本次新增 9 个 CLI/环境变量参数与 4 个自定义转换器(_convert_mm_transport_mode_rdma_port_positive_int_non_negative_int),并首次把参数绑定到 output_transport.rdma/control 嵌套对象;全仓检索确认没有任何测试命中 MM_TRANSPORT_MODEMM_RDMA_*。同时生产服务始终向 MMOutputProxyRouter 传入 transport_config(TTL 由 slot_gc_timeout_ms + 5s 推导、release deadline 由 release_timeout_ms 推导),但 vit_proxy_server_test.py 的 4 个 router 用例全部传 transport_config=None,只覆盖默认 120s/1s。绑定异常在 _apply_config_bindings 中只记 warning,字段漂移会静默保留默认值。

Strengths

  • 候选数据面与 inline terminal 彻底分离,fetch() 失败最多额外付出一次 ViT 请求,正确性始终由 inline 兜底。
  • 双端能力握手 fail-closed:未声明却收到 RDMA receipt 时先 discard() 释放远端 slot,再返回明确协议错误,测试已固化该行为。
  • proto 迁移保持空 package、消息全名与字段号不变,wire format 向后兼容;embedding_endpoint.pygrpc_util.pyproxy_router.py 等仓内消费者均已同步改用 multimodal_output_pb2
  • Python MMRdmaConfig/MMControlConfig/MMTransportConfig 与 C++ extractRdmaConfig/extractMMControlConfig/extractMMTransportConfig 字段名与默认值逐项对齐(250/3000/8/60000/1GB/1000),跨语言配置无漂移。
  • SlotLease RAII、createBatch 空 lease 回滚、Python 侧 _roll_back(descriptor 解析失败仍释放已解析 lease)覆盖了主要异常释放路径。
  • 代理路由实现 handle 冲突 fail-closed、TTL 过期、幂等释放与单 worker 故障隔离,并上报 rdma_handle_collision 指标。
  • exportEmbeddingrelease 在进入 provider 前显式 py::gil_scoped_releaseensureReader 注释说明了 gRPC 工作线程不持 GIL 的前提,跨语言边界考虑周全。
  • 开源构建提供 rdma_transport_no_implDisabledMMRdmaExporter,保持统一工厂 API 并自动回退 inline gRPC。
  • RDMA 测试使用内存伪实现覆盖分块、重组、回滚与非法 receipt,无 RDMA 设备也可运行。

Comment thread rtp_llm/cpp/model_rpc/proto/model_rpc_service.proto Outdated

@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 #1353 (non-blocking suggestions)

40 条 P2/P3 建议,不阻塞合并。阻塞判定与完整摘要见上一条 review。


class MMTransportConfig:
def __init__(self):
self.mode: str = MM_TRANSPORT_MODE_AUTO

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] 默认 auto 模式会静默改变既有多模态部署的数据面

远端多模态输出此前固定走 inline gRPC;现在 Python MMTransportConfig.mode、CLI --mm_transport_mode(vit_group_args.py:300)与 C++ MMTransportConfig::mode(ConfigModules.h:345)默认均为 autoMMRemoteOutputTransportFactory.cc 在 auto 下装配 lazy RDMA reader,因此链接了真实 provider 的存量部署升级后无需改任何配置即自动切到 RDMA 数据面,只有显式设置 MM_TRANSPORT_MODE=grpc 才能恢复旧行为;滚动升级期间两端能力还可能不一致。

建议: 默认保持 grpc,由部署显式开启 auto;若必须默认启用,请在发布说明中给出平台矩阵、LLM 与 ViT 两侧版本兼容矩阵、滚动升级顺序与一键回滚开关。


} // namespace

bool MMRdmaReader::advertise(const std::string& /*endpoint*/, MultimodalInputsPB& request_pb) {

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] RDMA 持续故障时缺少熔断,每请求放大为两次 ViT forward 且不自愈

advertise() 只要 ensureReader() 成功就恒定声明 support_rdma,不记录 endpoint 健康状态。读取失败仅让当前请求走 ConsumeResult::retry,而 degradeToTerminal()(MMRemoteOutputTransport.cc:107)会重新发起一次 control_->request(),即 ViT 侧再执行一次完整 forward。reader 构造成功但远端 NIC/QP/网络持续故障时,每个请求都先等 RDMA 读失败(最长 read_timeout_ms,默认 3000ms),再重跑一次 ViT,延迟与 ViT 算力开销持续翻倍且无任何探测恢复机制。

建议: 按 endpoint 增加连续失败计数与熔断冷却:达到阈值后暂停 advertise,冷却期后小流量探测恢复;并补充"跨请求持续失败"的回归测试,断言熔断生效后不再产生额外 ViT 请求。

ErrorResult<MultimodalOutput> MMRemoteOutputTransport::degradeToTerminal(const std::string& endpoint,
MultimodalInputsPB& request_pb,
DeliveryContext& context,
const std::string& /*reason*/) {

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] 降级原因被丢弃、reader 未注入 metrics,数据面回退率完全不可观测

reader 区分 rdma_read_errorrdma_manifest_error 并通过 ConsumeResult::retry(reason) 传出,fetch() 也把它传给 degradeToTerminal(),但该形参签名为 const std::string& /*reason*/ 被直接忽略。MMTransportMetrics 只有 RPC 错误、RT 与字节数三类指标,metrics 也只注入 gRPC 控制面客户端而未注入 reader。相反,成功路径在 MMRdmaReader.cc:156 每请求输出 [MM-RDMA-HIT] INFO 日志,高 QPS 下日志量随请求线性增长。

建议: 将 metrics 注入 reader,新增按 endpoint/plane/reason 打标的降级计数与 RDMA 命中计数,由 degradeToTerminal()consume() 成功路径分别上报;把 [MM-RDMA-HIT] 改为命中计数指标,或降为 DEBUG 并限频。测试中断言上报次数。

Checklist: [6.1] 无 per-forward 调试日志 / 噪声热路径输出

for (auto& reader : readers_) {
reader->withdraw(request_pb);
}
if (context.budget.exhausted()) {

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] 降级重试只检查预算是否耗尽、未预留最小预算,可能把可恢复的 RDMA 失败变成硬失败

degradeToTerminal() 仅以 context.budget.exhausted()(即 remainingMs() <= 0)作为放弃条件。当请求通过 mm_preprocess_config.mm_timeout_ms 设置了较小预算、RDMA 读又耗掉大部分时间时,剩余可能只有数十毫秒,此时仍会发起一次完整 ViT forward 请求并必然 DEADLINE_EXCEEDED,白付一次往返且返回的错误码来自超时而非真实降级原因,排障时容易误判。

建议: 为 fallback 预留一个最小可用预算(例如可配置的 min_fallback_budget_ms),不足时直接返回带原始降级 reason 的明确错误;并补充小预算场景的单测,断言不再发出注定超时的第二次请求。

releaseNow(endpoint, handles, std::min(release_timeout_ms_, remaining));
}

void releaseAsync(std::string endpoint, std::vector<std::string> handles) override {

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] releaseAsync 在队列饱和或停机时退化为同步 RPC,违反基类「不得延迟消费者」契约

MMRemoteOutputTransport.h:79 明确规定「Success-path release must not delay the consumer」,但实现在 stopping_pending_release_handles_ + handles.size() > kMaxPendingReleaseHandles(1024)时置 send_sync=true,直接执行 releaseNow()(deadline 为 release_timeout_ms_,默认 1000ms)。控制面变慢导致后台线程积压时,每次成功的 RDMA 读取都会在推理返回前同步阻塞最长 1s,恰好把「读取成功」变成延迟尖刺。

建议: 溢出时保持异步语义:或丢弃入队并依赖导出侧 slot_gc_timeout_ms TTL GC 兜底,或扩容/分片队列;如确需背压,应在基类契约与文档中显式声明该例外,并补充队列饱和下的延迟测试。

Comment thread rtp_llm/flexlb/tools/online_eval/online_eval/proto_utils.py
Comment thread rtp_llm/flexlb/flexlb-grpc/pom.xml Outdated
def test_release_deadline_exhaustion_skips_workers(self, report):
connection_pool = MagicMock()
router = MMOutputProxyRouter(connection_pool)
router._handle_routes["handle"] = ("worker", time.monotonic())

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] 测试直接构造私有状态并全局替换标准库时钟

三处用例(:1110、:1121、:1145)直接写入 router._handle_routes,绕过生产写入口 record_receipt(),因此守不住路由元组格式与消费语义的契约;一旦 record_receipt 改变元组结构,测试仍会通过。对 rtp_llm.multimodal.transport.proxy_router.time.monotonic 的 patch(:1124)实际修改共享标准库模块属性,同进程并发测试中可能影响其他组件。

建议: 统一通过 record_receipt() 建立路由;注入局部时钟或 patch 模块内独立导入的 monotonic 名称。

Checklist: [6.1] 新逻辑有聚焦单测 + 相关集成/smoke 测试;[P.G] mock.patch target 是使用处而非定义处;[P.G] mock/fake/stub 不得替代本次声称覆盖的生产边界

Comment thread bazel/py_proto.bzl
@@ -0,0 +1,8 @@
"""ViT-side multimodal output transport."""

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] 新增 Python 子包缺少 init.py

transport/grpctransport/rdma 目录下均无 __init__.py(该目录仅有 backend.py),当前只依赖隐式命名空间包与 BUILD 中的 Python 文件 glob。若后续切换为常规包发现或使用打包工具的 find_packages(),这两个子包可能被静默遗漏。

建议: 为两个子目录补充 __init__.py,与父包及仓库现有包结构保持一致。

Checklist: [I] 同一功能用统一工具函数

@ydshi0
ydshi0 force-pushed the feature/rdma_transport branch from 79ac9d4 to b7486d7 Compare September 1, 2026 10:07

@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 #1353

Status: BLOCKING

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

Reviewed: commit b7486d7f42ee · 2026-09-01 18:51 UTC+8

Blocking Issues

P1

  • 混合显式与默认超时会错误缩短整批请求期限 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc:9
    • 建议:逐项应用默认值后再取最大值并加 margin;同步修正 C++ 和 proxy,增加混合显式值与默认值测试。
  • TensorPB 在校验载荷长度前即被物化,畸形响应可导致越界读取 @ rtp_llm/cpp/model_rpc/MultimodalPbConverter.cc:24
    • 建议:物化前校验字段存在、维度非负、元素数无溢出、dtype 与载荷字段匹配且字节数精确一致,并增加畸形载荷测试。
  • 物理 GPU 优先查找破坏既有 local-rank 映射 @ rtp_llm/config/server_config_setup.py:639
    • 建议:记录映射来源和键空间:用户预置值保持 local-rank 语义,自动探测结果使用物理 GPU;增加设备重排和键冲突测试。

Non-blocking Suggestions

P2

  • 默认 auto 模式会静默改变既有多模态部署的数据面 @ rtp_llm/config/py_config_modules.py:291
    • 建议:默认保持 grpc 并显式启用 auto,或提供滚动升级、灰度和回滚说明。
  • RDMA 描述符反序列化缺少尺寸和边界校验 @ rtp_llm/cpp/rdma_transport/RdmaTransport.cc:102
    • 建议:在公共反序列化边界统一校验维度、算术溢出、字节数、偏移范围、载荷上限和必要字段。
  • RDMA provider 的并发契约未定义 @ rtp_llm/cpp/rdma_transport/RdmaTransport.h:53
    • 建议:明确 provider 必须线程安全并增加并发压力测试;否则在适配层统一加锁。
  • 亲和探测结果依赖共享当前目录 @ rtp_llm/config/server_config_setup.py:589
    • 建议:在独立临时目录运行工具并通过绝对路径读取,完成后清理结果文件。
  • 多 worker 使用固定 RDMA 端口会让部分 worker 永久回退 gRPC @ rtp_llm/start_server.py:373
    • 建议:多 worker 模式强制使用端口 0、按 worker 分配唯一端口,或启动时拒绝冲突配置。
  • 不支持多模态的请求被错误映射为 INTERNAL @ rtp_llm/cpp/model_rpc/LocalRpcServer.cc:171
    • 建议:统一 HTTP 与 gRPC 的拒绝行为,并将该错误映射为 INVALID_ARGUMENTFAILED_PRECONDITION
  • 首次瞬态初始化失败会永久关闭 RDMA @ rtp_llm/multimodal/transport/rdma/backend.py:61
    • 建议:区分永久不支持与瞬态失败,对后者使用有界退避重试,并覆盖首次失败、后续成功场景。
  • 降级重试未预留可执行的最小预算 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc:104
    • 建议:设置最小 fallback 预算;预算不足时直接返回保留原始原因的错误。
  • 降级原因被丢弃,数据面回退率不可观测 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc:100
    • 建议:将有限原因集合传入 MMTransportMetrics,按 endpoint 和原因记录降级计数,并保留原始错误链。
  • ReleaseRdmaLease 缺少结果指标 @ rtp_llm/server/vit_rpc_server.py:168
    • 建议:增加释放成功、失败、handle 数量和 GC 兜底指标,同时保留 best-effort RPC 语义。
  • RDMA 热路径日志缺少限频 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:156
    • 建议:成功命中改为指标或 DEBUG,失败日志按原因限频并配套分类计数。
  • 新增 pybind 共享库缺少真实加载测试 @ BUILD:187
    • 建议:增加携带真实共享库的 py_test,验证导入、注册、available()、构造及无实现平台回退。
  • 生产 gRPC 控制客户端和传输工厂缺少集成测试 @ rtp_llm/cpp/multimodal_processor/transport/grpc/MMGrpcTransport.cc:28
    • 建议:使用进程内 gRPC 服务覆盖生产工厂、请求、释放合批、队列饱和、错误映射和并发关闭。
  • RDMA 数据面反向依赖 pybind 配置提取层 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaExporter.cc:7
    • 建议:把 Python 对象到 RdmaConfig 的转换移入 pybind 初始化层,数据面 exporter 仅接受纯 C++ 配置。

P3

  • RDMA 持续故障时每个请求都会重复一次 ViT forward @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:113
    • 建议:按 endpoint 和失败类型增加有界熔断、退避与恢复探测,并通过指标评估是否需要默认启用。
  • C++ VitConfig 绑定未同步新增配置树 @ rtp_llm/cpp/config/ConfigModules.h:356
    • 建议:同步绑定、pickle 和字符串输出,或明确废弃该配置入口。
  • 独立 converter 的头文件仍被聚合目标重复导出 @ rtp_llm/cpp/model_rpc/BUILD:70
    • 建议:从聚合目标的 hdrs 中排除该头文件,并要求调用方依赖 :multimodal_pb_converter

Checklist Findings (15 fail / 123 total)

General Principles Checklist

  • [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue 独立 converter 的头文件仍被聚合目标重复导出
    MultimodalPbConverter.cc/.h 已归属独立目标,但 model_rpc_serverhdrs glob 只排除了 RPCPool.h,仍会导出 converter 头文件,使调用方可绕过显式依赖边界。
  • [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue C++ VitConfig 绑定未同步新增配置树
    VitConfig 新增 output_transport,但 pybind 属性、pickle 和 C++ to_string() 仍只处理 vit_separation,通过该绑定保存或诊断配置时会丢失新字段。
  • [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue 独立 converter 的头文件仍被聚合目标重复导出
    MultimodalPbConverter.cc/.h 已归属独立目标,但 model_rpc_serverhdrs glob 只排除了 RPCPool.h,仍会导出 converter 头文件,使调用方可绕过显式依赖边界。
  • [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue RDMA 持续故障时每个请求都会重复一次 ViT forward
    reader 初始化成功后会为每个请求重新声明 RDMA;持续 read 或 manifest 故障时,每次都在首轮计算后再执行一次 inline forward,没有 endpoint 级退避或恢复探测。
  • [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue 默认 auto 模式会静默改变既有多模态部署的数据面
    构建包含可用 RDMA provider 时,未设置新参数的既有部署会由 inline gRPC 自动切换至 RDMA,升级本身改变了传输方式和资源生命周期。
  • [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue RDMA 持续故障时每个请求都会重复一次 ViT forward
    reader 初始化成功后会为每个请求重新声明 RDMA;持续 read 或 manifest 故障时,每次都在首轮计算后再执行一次 inline forward,没有 endpoint 级退避或恢复探测。
  • [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue RDMA 持续故障时每个请求都会重复一次 ViT forward
    reader 初始化成功后会为每个请求重新声明 RDMA;持续 read 或 manifest 故障时,每次都在首轮计算后再执行一次 inline forward,没有 endpoint 级退避或恢复探测。
  • [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue RDMA 热路径日志缺少限频
    每次 RDMA 成功均输出 INFO;Python exporter 异常、空描述符和容量失败也逐请求输出 WARNING。正常高 QPS 或持续故障时日志量随请求数线性增长。
  • [6.1] Software Engineering — ISP:调用方不依赖无关大接口 → issue RDMA 数据面反向依赖 pybind 配置提取层
    RDMA exporter 直接包含 pybind/ConfigExtract.h,其 BUILD 目标因此依赖 pybind 和顶层 compute shared library;随后 pybind exporter 目标又依赖该数据面目标,形成包级反向依赖。
  • [6.1] Software Engineering — LSP:子类/重写保持基类契约 → issue RDMA provider 的并发契约未定义
    多线程 gRPC 服务共享 exporter 和 reader,pybind 导出与释放还会释放 GIL;接口只规定同步与所有权,没有要求 create/read/release 并发安全,调用方也未统一串行化。
  • [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue 生产 gRPC 控制客户端和传输工厂缺少集成测试
    编排测试只注入 FakeControlClient,没有调用生产工厂或 GrpcMMControlClient。连接失败、deadline、队列饱和、endpoint 合批、关闭竞态及同步/异步释放误换均未覆盖。
  • [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue 生产 gRPC 控制客户端和传输工厂缺少集成测试
    编排测试只注入 FakeControlClient,没有调用生产工厂或 GrpcMMControlClient。连接失败、deadline、队列饱和、endpoint 合批、关闭竞态及同步/异步释放误换均未覆盖。
  • [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue 生产 gRPC 控制客户端和传输工厂缺少集成测试
    编排测试只注入 FakeControlClient,没有调用生产工厂或 GrpcMMControlClient。连接失败、deadline、队列饱和、endpoint 合批、关闭竞态及同步/异步释放误换均未覆盖。

RTP-LLM Checklist

  • [I] 代码质量 — 同一功能用统一工具函数 → issue 混合显式与默认超时会错误缩短整批请求期限
    resolveRpcTimeoutMs() 只对正值取最大值。输入超时为 [1000,-1] 时客户端预算仅 6000ms;worker 会先把 -1 解析为默认 120000ms。Python proxy 使用相同错误算法,合法请求会被提前取消。

Python Static-First Checklist

  • [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue 生产 gRPC 控制客户端和传输工厂缺少集成测试
    编排测试只注入 FakeControlClient,没有调用生产工厂或 GrpcMMControlClient。连接失败、deadline、队列饱和、endpoint 合批、关闭竞态及同步/异步释放误换均未覆盖。

Strengths

  • protobuf 采用新增字段协商 RDMA,旧客户端仍可使用 inline gRPC。
  • RDMA 失败仅进行一次有界回退,租约使用 RAII 和批量回滚。
  • 提供强制 grpc 模式作为运维回滚路径。
  • 新增测试覆盖分片重组、回滚、路由冲突和处理器决策。


namespace rtp_llm {

int64_t resolveRpcTimeoutMs(const MultimodalInputsPB& request,

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] 混合显式与默认超时会错误缩短整批请求期限

resolveRpcTimeoutMs() 只对正值取最大值。输入超时为 [1000,-1] 时客户端预算仅 6000ms;worker 会先把 -1 解析为默认 120000ms。Python proxy 使用相同错误算法,合法请求会被提前取消。

建议: 逐项应用默认值后再取最大值并加 margin;同步修正 C++ 和 proxy,增加混合显式值与默认值测试。

Checklist: [I] 同一功能用统一工具函数

ErrorResult<MultimodalOutput> MultimodalPbConverter::inlineOutputFromPb(const MultimodalOutputPB& output_pb) {
// Convert malformed remote data into an error instead of propagating torch exceptions.
try {
torch::Tensor mm_embedding = TensorPbConvert::pbToTorch(output_pb.multimodal_embedding()), mm_position_id;

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] TensorPB 在校验载荷长度前即被物化,畸形响应可导致越界读取

inlineOutputFromPb() 直接调用 pbToTorch();后者依据远端 shape 对 protobuf 字节执行 from_blob(...).clone(),却不校验字段存在和载荷长度。空 PB 会按默认 FP32 标量读取,截断载荷也会越界,外层异常捕获无法阻止未定义行为。

建议: 物化前校验字段存在、维度非负、元素数无溢出、dtype 与载荷字段匹配且字节数精确一致,并增加畸形载荷测试。

Comment thread rtp_llm/config/server_config_setup.py Outdated

@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 #1353 (non-blocking suggestions)

17 条 P2/P3 建议,不阻塞合并。阻塞判定与完整摘要见上一条 review。


class MMTransportConfig:
def __init__(self):
self.mode: str = MM_TRANSPORT_MODE_AUTO

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] 默认 auto 模式会静默改变既有多模态部署的数据面

构建包含可用 RDMA provider 时,未设置新参数的既有部署会由 inline gRPC 自动切换至 RDMA,升级本身改变了传输方式和资源生命周期。

建议: 默认保持 grpc 并显式启用 auto,或提供滚动升级、灰度和回滚说明。

Checklist: [6.1] 回滚路径:风险行为存在运维回滚手段

}
}

bool fromProto(const ::RdmaDescriptorPB& src, RdmaDescriptor* dst) {

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] RDMA 描述符反序列化缺少尺寸和边界校验

fromProto() 接受负 shape、任意 nbytes、溢出的 offset+nbytes 和不受限的 payload_bytes,也不验证 shape×dtype 与 nbytes 一致,随后直接交给 provider。

建议: 在公共反序列化边界统一校验维度、算术溢出、字节数、偏移范围、载荷上限和必要字段。

std::vector<torch::Tensor> tensors;
};

class RdmaExport {

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] RDMA provider 的并发契约未定义

多线程 gRPC 服务共享 exporter 和 reader,pybind 导出与释放还会释放 GIL;接口只规定同步与所有权,没有要求 create/read/release 并发安全,调用方也未统一串行化。

建议: 明确 provider 必须线程安全并增加并发压力测试;否则在适配层统一加锁。

Checklist: [6.1] LSP:子类/重写保持基类契约

Comment thread rtp_llm/config/server_config_setup.py Outdated
Comment thread rtp_llm/start_server.py

if vit_server_count > 1:
logging.info(
f"[VIT_SERVER] Starting in PROXY mode: 1 proxy + {vit_server_count} workers "

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/start_server.py:373(不在 diff 展示范围内,就近挂载)

[P2] 多 worker 使用固定 RDMA 端口会让部分 worker 永久回退 gRPC

所有 ViT worker 接收同一份 output_transport.rdma 配置。固定非零端口时各进程竞争同一监听地址;初始化失败的 worker 又不会重试,因此健康检查可能正常但数据面持续降级。

建议: 多 worker 模式强制使用端口 0、按 worker 分配唯一端口,或启动时拒绝冲突配置。

inline constexpr const char* kReasonReleaseClientStop = "release_client_stopping";
inline constexpr size_t kMaxPendingReleaseHandles = 1024;

class GrpcMMControlClient: public MMControlClient {

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] 生产 gRPC 控制客户端和传输工厂缺少集成测试

编排测试只注入 FakeControlClient,没有调用生产工厂或 GrpcMMControlClient。连接失败、deadline、队列饱和、endpoint 合批、关闭竞态及同步/异步释放误换均未覆盖。

建议: 使用进程内 gRPC 服务覆盖生产工厂、请求、释放合批、队列饱和、错误映射和并发关闭。

Checklist: [6.1] 分布式/跨平台变更有对应覆盖;[6.1] 新逻辑有聚焦单测 + 相关集成/smoke 测试;[6.1] 边界 case 覆盖(空、单元素、最大值);[P.G] mock/fake/stub 不得替代本次声称覆盖的生产边界

#include <limits>
#include <utility>

#include "rtp_llm/cpp/pybind/ConfigExtract.h"

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] RDMA 数据面反向依赖 pybind 配置提取层

RDMA exporter 直接包含 pybind/ConfigExtract.h,其 BUILD 目标因此依赖 pybind 和顶层 compute shared library;随后 pybind exporter 目标又依赖该数据面目标,形成包级反向依赖。

建议: 把 Python 对象到 RdmaConfig 的转换移入 pybind 初始化层,数据面 exporter 仅接受纯 C++ 配置。

Checklist: [6.1] ISP:调用方不依赖无关大接口


} // namespace

bool MMRdmaReader::advertise(const std::string& /*endpoint*/, MultimodalInputsPB& request_pb) {

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] RDMA 持续故障时每个请求都会重复一次 ViT forward

reader 初始化成功后会为每个请求重新声明 RDMA;持续 read 或 manifest 故障时,每次都在首轮计算后再执行一次 inline forward,没有 endpoint 级退避或恢复探测。

建议: 按 endpoint 和失败类型增加有界熔断、退避与恢复探测,并通过指标评估是否需要默认启用。

Checklist: [6.1] 可观测性:日志/指标/超时可操作、非噪声;[6.1] 状态不变量:创建/更新/失败/重试/回滚路径有效;[6.1] 错误语义:fail-fast/retry/fallback/silent 行为显式

VitSeparation vit_separation = VitSeparation::VIT_SEPARATION_LOCAL;
std::string to_string() const;
VitSeparation vit_separation = VitSeparation::VIT_SEPARATION_LOCAL;
MMTransportConfig output_transport;

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] C++ VitConfig 绑定未同步新增配置树

VitConfig 新增 output_transport,但 pybind 属性、pickle 和 C++ to_string() 仍只处理 vit_separation,通过该绑定保存或诊断配置时会丢失新字段。

建议: 同步绑定、pickle 和字符串输出,或明确废弃该配置入口。

Checklist: [6.1] 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全

"MultimodalPbConverter.cc",
],
),
hdrs = glob(

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] 独立 converter 的头文件仍被聚合目标重复导出

MultimodalPbConverter.cc/.h 已归属独立目标,但 model_rpc_serverhdrs glob 只排除了 RPCPool.h,仍会导出 converter 头文件,使调用方可绕过显式依赖边界。

建议: 从聚合目标的 hdrs 中排除该头文件,并要求调用方依赖 :multimodal_pb_converter

Checklist: [6.1] 依赖方向:无循环依赖/跨层惊喜;[6.1] 分层边界:新概念在正确层级,不泄漏内部

Build a reusable Tensor RDMA transport contract around an owner-side
RdmaExport API and a reader-side RdmaRead API. Define stable descriptors,
per-tensor manifests, lease-based slot ownership, batch reads, provider
selection, and a no-op implementation so applications do not depend on a
specific RDMA library.

Decouple separated ViT communication from its data plane. Keep gRPC as the
control plane for capability negotiation, requests, receipts, deadlines, and
lease release, while routing output delivery through transport backends and
receipt readers. The ViT and multimodal processor business paths now consume a
single transport interface instead of branching on grpc-inline or RDMA.

Adapt the complete ViT output to the common RDMA contract. Export embedding,
position IDs, and extra inputs from registered GPU slots, return descriptors in
the receipt, and reconstruct the original MultimodalOutput on the LLM side.
Oversized outputs are split into ordered slots without changing split_size
semantics.

Keep the ViT exporter and LLM receipt reader in separate build targets. Expose
the exporter through libmm_rdma_exporter.so so a ViT process does not load the
LLM and Embedding engine bindings in libth_transformer.so.

Validate descriptors and manifests, bound in-flight slot memory, release slots
through the control plane, and reclaim abandoned leases with GC. RDMA setup,
export, or READ failures use one bounded grpc-inline fallback. The open-source
build provides the transport contract and fallback; the internal build supplies
the Barex provider. MM_TRANSPORT_MODE=auto tries RDMA first, while
MM_TRANSPORT_MODE=grpc forces inline delivery; invalid modes fail fast. Configure
GPU-NIC affinity during service startup using the physical GPU mapping.
@ydshi0
ydshi0 force-pushed the feature/rdma_transport branch from b7486d7 to 2702a57 Compare September 1, 2026 14:02

@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 #1353

Status: BLOCKING

Summary: P0/0 · P1/6 · P2/12 · P3/3

Reviewed: commit 2702a57fb971 · 2026-09-01 22:46 UTC+8

Blocking Issues

P1

  • 外部亲和性程序可无限阻塞服务启动 @ rtp_llm/config/server_config_setup.py:535
    • 建议:设置明确的短超时,捕获 subprocess.TimeoutExpired 后告警并降级启动,并覆盖超时路径。
  • 非法角色字段可能终止 RPC 服务进程 @ rtp_llm/cpp/model_rpc/QueryConverter.cc:52
    • 建议:将角色解析改为返回 ErrorResult,在 RPC 边界映射为 INVALID_ARGUMENT,并增加未知字符串和双写冲突的真实 RPC 测试。
  • 非零 local rank 的 VIT 调度线程仍会使用 cuda:0 @ rtp_llm/multimodal/vit_start_server.py:64
    • 建议:传递显式的 cuda:{local_rank},并增加非零 local rank 的调度线程设备测试。
  • RDMA 初始化未绑定请求所属 GPU @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:244
    • 建议:显式传播 local device id,并在 reader/exporter 创建、读写及释放期间使用设备 guard;增加非零 rank 多 GPU 测试。
  • 不可信 RDMA 描述符在完整校验前进入 provider @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:168
    • 建议:在调用 provider 前执行溢出安全的端点、lease、shape、dtype 字节数及范围校验,并增加畸形描述符测试。
  • 测试未覆盖真实 RDMA 生产边界 @ rtp_llm/cpp/multimodal_processor/test/BUILD:62
    • 建议:增加真实共享库加载和 Python/C++ 配置边界测试,并在 RDMA 平台 CI 中执行多 GPU provider export→read→release smoke。

Non-blocking Suggestions

P2

  • 默认 auto 模式会静默改变既有多模态部署的数据面 @ rtp_llm/config/py_config_modules.py:291
    • 建议:保持 grpc 为兼容默认值并要求显式启用,或提供清晰的迁移、灰度和回滚说明。
  • 亲和探测结果依赖共享当前目录 @ rtp_llm/config/server_config_setup.py:549
    • 建议:为每次探测创建独立临时目录,通过 cwd 或显式输出路径读取本次结果,并在完成后清理。
  • 多 worker 使用固定 RDMA 端口会让部分 worker 永久回退 gRPC @ rtp_llm/start_server.py:373
    • 建议:代理模式下按 worker 分配唯一端口,或禁止多 worker 使用固定端口并在启动前报错。
  • 不支持多模态的请求被错误映射为 INTERNAL @ rtp_llm/cpp/model_rpc/LocalRpcServer.cc:171
    • 建议:将该错误映射为 INVALID_ARGUMENTUNIMPLEMENTED,并增加 RPC 状态测试。
  • 首次瞬态初始化失败会永久关闭 RDMA @ rtp_llm/multimodal/transport/rdma/backend.py:61
    • 建议:区分永久不支持与瞬态失败,对后者使用有上限、带退避和限频日志的可恢复状态机。
  • RDMA provider 的并发契约未定义 @ rtp_llm/cpp/rdma_transport/RdmaTransport.h:53
    • 建议:明确并验证 provider 的线程安全契约;若 provider 不保证并发安全,应在适配层串行化相关操作。
  • 降级重试未预留可执行的最小预算 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc:104
    • 建议:为 inline 降级定义最小剩余预算;不足时直接返回原始数据面错误。
  • 降级原因被丢弃,数据面回退率不可观测 @ rtp_llm/cpp/multimodal_processor/transport/MMRemoteOutputTransport.cc:100
    • 建议:按 endpoint 和有限枚举 reason 上报回退计数、结果及耗时,避免高基数标签。
  • ReleaseRdmaLease 缺少结果指标 @ rtp_llm/server/vit_rpc_server.py:168
    • 建议:增加释放请求数、handle 数、耗时、失败原因及 GC 兜底计数,并让控制面暴露可判定的结果。
  • RDMA 热路径日志缺少限频 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:156
    • 建议:成功路径改为指标或 debug,预期回退与重复故障使用采样或限频日志。
  • RDMA 数据面反向依赖 pybind 配置提取层 @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaExporter.cc:7
    • 建议:将 py::objectRdmaConfig 的转换放入独立 pybind bridge,保持 transport 仅依赖纯 C++ 配置。
  • TensorPB 新增校验缺少边界测试 @ rtp_llm/cpp/model_rpc/test/QueryConverterTest.cc:279
    • 建议:增加数据驱动测试,覆盖全部 dtype、负维度、两类溢出、过长和过短 payload、零尺寸及 scalar round-trip。

P3

  • RDMA 持续故障时每个请求都会重复一次 ViT forward @ rtp_llm/cpp/multimodal_processor/transport/rdma/MMRdmaReader.cc:113
    • 建议:增加 endpoint 级短期熔断和探测恢复机制,连续读取失败后暂时停止通告 RDMA。
  • C++ VitConfig 绑定未同步新增配置树 @ rtp_llm/cpp/config/ConfigModules.h:356
    • 建议:同步绑定、字符串输出和 pickle schema,并增加 round-trip 测试;或明确该 C++ 类型不再作为公开 Python 配置接口。
  • 独立 converter 的头文件仍被聚合目标重复导出 @ rtp_llm/cpp/model_rpc/BUILD:70
    • 建议:从聚合目标的 glob 中排除该头文件,并让消费者显式依赖 :multimodal_pb_converter

Checklist Findings (11 fail / 123 total)

General Principles Checklist

  • [6.1] Architecture — 依赖方向:无循环依赖/跨层惊喜 → issue 独立 converter 的头文件仍被聚合目标重复导出
    MultimodalPbConverter.h 已归属 :multimodal_pb_converter,但 :model_rpc_server 的头文件 glob 仅排除 RPCPool.h,同一头文件仍由两个 target 导出,消费者可绕过新的精简依赖边界。
  • [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue C++ VitConfig 绑定未同步新增配置树
    C++ VitConfig 新增 output_transport,但当前 pybind 类、to_string() 和 pickle 状态仍只处理 vit_separation。直接使用该绑定的消费者无法读取、设置或持久化新增配置。
  • [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue 独立 converter 的头文件仍被聚合目标重复导出
    MultimodalPbConverter.h 已归属 :multimodal_pb_converter,但 :model_rpc_server 的头文件 glob 仅排除 RPCPool.h,同一头文件仍由两个 target 导出,消费者可绕过新的精简依赖边界。
  • [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue RDMA 热路径日志缺少限频
    每次 RDMA 成功都会输出 INFO,读取或 manifest 失败则逐请求输出 WARNING;Python exporter 的容量回退也逐请求告警。持续流量或 provider 故障会形成日志风暴。
  • [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue RDMA 持续故障时每个请求都会重复一次 ViT forward
    provider 可初始化但持续读取失败时,advertise() 仍会为每个请求通告 RDMA;失败后 degradeToTerminal() 再次调用完整 ViT RPC,因此故障期间每个请求都会重复 forward。
  • [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue RDMA 持续故障时每个请求都会重复一次 ViT forward
    provider 可初始化但持续读取失败时,advertise() 仍会为每个请求通告 RDMA;失败后 degradeToTerminal() 再次调用完整 ViT RPC,因此故障期间每个请求都会重复 forward。
  • [6.1] Quality — 无 per-forward 调试日志 / 噪声热路径输出 → issue RDMA 热路径日志缺少限频
    每次 RDMA 成功都会输出 INFO,读取或 manifest 失败则逐请求输出 WARNING;Python exporter 的容量回退也逐请求告警。持续流量或 provider 故障会形成日志风暴。
  • [6.1] Tests — 分布式/跨平台变更有对应覆盖 → issue 多 worker 使用固定 RDMA 端口会让部分 worker 永久回退 gRPC
    所有 ViT worker 接收同一份 py_env_configs,未按 worker 调整 output_transport.rdma.port。用户配置非零固定端口时,只有首个 exporter 能绑定,其余 worker 初始化失败并永久降级。
  • [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue TensorPB 新增校验缺少边界测试
    新增反序列化测试只覆盖 FP32 payload 过短;负维度、元素数和字节数溢出、过长 payload、零尺寸、scalar 解码及其他 dtype 的成功路径均未覆盖。
  • [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue TensorPB 新增校验缺少边界测试
    新增反序列化测试只覆盖 FP32 payload 过短;负维度、元素数和字节数溢出、过长 payload、零尺寸、scalar 解码及其他 dtype 的成功路径均未覆盖。

Python Static-First Checklist

  • [P.G] 测试规范 — mock/fake/stub 不得替代本次声称覆盖的生产边界 → issue 测试未覆盖真实 RDMA 生产边界
    C++ 测试明确使用 fake control/provider,Python 测试注入 MagicMock 并伪造 CUDA;没有测试实际加载 libmm_rdma_exporter,也未跨真实 gRPC 控制面执行 export/read/release、验证 pybind 配置提取或覆盖非零 rank、并发和 GC。

Strengths

  • protobuf 仅新增字段号,旧客户端仍可使用 inline gRPC。
  • TensorPB 已在物化前校验形状、溢出和载荷长度。
  • RDMA 失败回退有界,lease 具备 RAII、异步释放和 GC 兜底。
  • Python/C++ 传输配置及默认超时基本一致,无 RDMA 构建可降级运行。
  • 单元测试覆盖分片重组、回滚、代理路由和异常 receipt。

return False

try:
subprocess.run(

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] 外部亲和性程序可无限阻塞服务启动

后端及 ViT worker 启动前同步执行 subprocess.run(),但未设置超时。探测程序卡死时异常处理不会执行,父进程将永久停在 worker 创建之前,服务无法进入就绪状态。

建议: 设置明确的短超时,捕获 subprocess.TimeoutExpired 后告警并降级启动,并覆盖超时路径。

@@ -313,100 +313,6 @@ std::vector<RoleAddr> QueryConverter::getRoleAddrs(const GenerateConfigPB* confi
return role_addrs;

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/QueryConverter.cc:52(不在 diff 展示范围内,就近挂载)

[P1] 非法角色字段可能终止 RPC 服务进程

未知 role_str 使用 RTP_LLM_FAIL,双写冲突使用断言;开启异常 core dump 时会直接 abort(),关闭时异常也会逃出无保护的 GenerateStreamCall/BatchGenerateCall 转换路径。畸形 RPC 请求因此可能终止服务。

建议: 将角色解析改为返回 ErrorResult,在 RPC 边界映射为 INVALID_ARGUMENT,并增加未知字符串和双写冲突的真实 RPC 测试。

setup_cuda_device_and_accl_env(engine_config.parallelism_config.local_rank)

model_config = ModelFactory.create_model_config(
model_args=py_env_configs.model_args,

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/multimodal/vit_start_server.py:64(不在 diff 展示范围内,就近挂载)

[P1] 非零 local rank 的 VIT 调度线程仍会使用 cuda:0

启动线程按 local_rank 调用 set_device(),随后却向 MMProcessEngine 传入裸 "cuda"MMScheduler 在新线程执行,且只为 cuda:N 设置设备;新线程因此默认使用 GPU 0,非零 rank 的权重与输入可能跨设备。

建议: 传递显式的 cuda:{local_rank},并增加非零 local rank 的调度线程设备测试。

// This method is called from gRPC worker threads, which do not hold the
// Python GIL. Reader construction is entirely C++/RDMA and must not use
// py::gil_scoped_release here (it requires the GIL to be held first).
std::call_once(reader_once_, [this]() {

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] RDMA 初始化未绑定请求所属 GPU

ensureReader() 在 gRPC 工作线程中调用 createRdmaRead(),但 RdmaConfig 不携带 device id,调用前也无 device guard。ViT exporter 同样在 gRPC 线程中惰性构造,非零 rank 可能在 GPU 0 初始化资源或分配读取结果。

建议: 显式传播 local device id,并在 reader/exporter 创建、读写及释放期间使用设备 guard;增加非零 rank 多 GPU 测试。

std::vector<MMRdmaSlotPB::Role>* roles) {
std::vector<rdma_transport::RdmaDescriptor> descriptors;
descriptors.reserve(static_cast<size_t>(receipt.output_rdma_slots_size()));
for (const auto& slot : receipt.output_rdma_slots()) {

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] 不可信 RDMA 描述符在完整校验前进入 provider

fromProto() 仅验证 dtype,host、port、地址、lease、shape、offset、nbytes 和 payload_bytes 随后直接传给 provider。负维度、字节数不匹配、算术溢出或越过 payload 的范围均未在 RDMA 操作前拒绝。

建议: 在调用 provider 前执行溢出安全的端点、lease、shape、dtype 字节数及范围校验,并增加畸形描述符测试。


# LLM-side output transport orchestration: receipt dispatch, the bounded degrade, protocol
# violations and slot-release ordering. Both the control plane and the RDMA transport are fakes.
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.

[P1] 测试未覆盖真实 RDMA 生产边界

C++ 测试明确使用 fake control/provider,Python 测试注入 MagicMock 并伪造 CUDA;没有测试实际加载 libmm_rdma_exporter,也未跨真实 gRPC 控制面执行 export/read/release、验证 pybind 配置提取或覆盖非零 rank、并发和 GC。

建议: 增加真实共享库加载和 Python/C++ 配置边界测试,并在 RDMA 平台 CI 中执行多 GPU provider export→read→release smoke。

Checklist: [P.G] mock/fake/stub 不得替代本次声称覆盖的生产边界

@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 #1353 (non-blocking suggestions)

15 条 P2/P3 建议,不阻塞合并。阻塞判定与完整摘要见上一条 review。


class MMTransportConfig:
def __init__(self):
self.mode: str = MM_TRANSPORT_MODE_AUTO

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] 默认 auto 模式会静默改变既有多模态部署的数据面

MMTransportConfig.mode 默认设为 auto。只要构建包含真实 provider,未配置新参数的既有部署就会自动协商 RDMA,而不是继续使用原 inline gRPC 数据面。

建议: 保持 grpc 为兼容默认值并要求显式启用,或提供清晰的迁移、灰度和回滚说明。

)
return False

json_path = "npu_nic_affinity.json"

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] 亲和探测结果依赖共享当前目录

探测程序未指定工作目录,随后固定读取相对路径 npu_nic_affinity.json。并发启动、工作目录变化或遗留文件可能导致进程读取其他实例或陈旧的 NIC 映射。

建议: 为每次探测创建独立临时目录,通过 cwd 或显式输出路径读取本次结果,并在完成后清理。

Comment thread rtp_llm/start_server.py

if vit_server_count > 1:
logging.info(
f"[VIT_SERVER] Starting in PROXY mode: 1 proxy + {vit_server_count} workers "

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/start_server.py:373(不在 diff 展示范围内,就近挂载)

[P2] 多 worker 使用固定 RDMA 端口会让部分 worker 永久回退 gRPC

所有 ViT worker 接收同一份 py_env_configs,未按 worker 调整 output_transport.rdma.port。用户配置非零固定端口时,只有首个 exporter 能绑定,其余 worker 初始化失败并永久降级。

建议: 代理模式下按 worker 分配唯一端口,或禁止多 worker 使用固定端口并在启动前报错。

Checklist: [6.1] 分布式/跨平台变更有对应覆盖

output = QueryConverter::transQuery(&input_pb);
if (mm_processor_ != nullptr && output->multimodal_inputs) {
if (output->multimodal_inputs.has_value() && !output->multimodal_inputs->empty()) {
if (mm_processor_ == nullptr) {

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] 不支持多模态的请求被错误映射为 INTERNAL

prepareInput() 返回 MM_NOT_SUPPORTED_ERROR,但 transErrorCodeToGrpc() 未映射该错误码,最终返回默认 INTERNAL。客户端无法区分不支持的请求与服务端故障。

建议: 将该错误映射为 INVALID_ARGUMENTUNIMPLEMENTED,并增加 RPC 状态测试。

with self._exporter_lock:
if self._closed or self._exporter_init_attempted:
return self._exporter
self._exporter_init_attempted = True

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] 首次瞬态初始化失败会永久关闭 RDMA

_exporter_init_attempted 在构造前永久置真,失败后不再尝试;C++ reader 同样使用 std::call_once。首次请求遭遇瞬态端口、设备或 provider 错误后,进程余下生命周期只能回退 gRPC。

建议: 区分永久不支持与瞬态失败,对后者使用有上限、带退避和限频日志的可恢复状态机。

#include <limits>
#include <utility>

#include "rtp_llm/cpp/pybind/ConfigExtract.h"

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] RDMA 数据面反向依赖 pybind 配置提取层

transport 实现为解析 Python 对象直接包含 cpp/pybind/ConfigExtract.h,对应 BUILD 目标还依赖 pybind 配置提取层,使底层数据面反向依赖顶层绑定层。

建议:py::objectRdmaConfig 的转换放入独立 pybind bridge,保持 transport 仅依赖纯 C++ 配置。

EXPECT_EQ(tensor_pb.shape_size(), 0);
}

TEST_F(QueryConverterTest, TransTensorRejectsMismatchedPayloadSize) {

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] TensorPB 新增校验缺少边界测试

新增反序列化测试只覆盖 FP32 payload 过短;负维度、元素数和字节数溢出、过长 payload、零尺寸、scalar 解码及其他 dtype 的成功路径均未覆盖。

建议: 增加数据驱动测试,覆盖全部 dtype、负维度、两类溢出、过长和过短 payload、零尺寸及 scalar round-trip。

Checklist: [6.1] 新逻辑有聚焦单测 + 相关集成/smoke 测试;[6.1] 边界 case 覆盖(空、单元素、最大值)


} // namespace

bool MMRdmaReader::advertise(const std::string& /*endpoint*/, MultimodalInputsPB& request_pb) {

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] RDMA 持续故障时每个请求都会重复一次 ViT forward

provider 可初始化但持续读取失败时,advertise() 仍会为每个请求通告 RDMA;失败后 degradeToTerminal() 再次调用完整 ViT RPC,因此故障期间每个请求都会重复 forward。

建议: 增加 endpoint 级短期熔断和探测恢复机制,连续读取失败后暂时停止通告 RDMA。

Checklist: [6.1] 状态不变量:创建/更新/失败/重试/回滚路径有效;[6.1] 错误语义:fail-fast/retry/fallback/silent 行为显式

VitSeparation vit_separation = VitSeparation::VIT_SEPARATION_LOCAL;
std::string to_string() const;
VitSeparation vit_separation = VitSeparation::VIT_SEPARATION_LOCAL;
MMTransportConfig output_transport;

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] C++ VitConfig 绑定未同步新增配置树

C++ VitConfig 新增 output_transport,但当前 pybind 类、to_string() 和 pickle 状态仍只处理 vit_separation。直接使用该绑定的消费者无法读取、设置或持久化新增配置。

建议: 同步绑定、字符串输出和 pickle schema,并增加 round-trip 测试;或明确该 C++ 类型不再作为公开 Python 配置接口。

Checklist: [6.1] 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全

"MultimodalPbConverter.cc",
],
),
hdrs = glob(

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] 独立 converter 的头文件仍被聚合目标重复导出

MultimodalPbConverter.h 已归属 :multimodal_pb_converter,但 :model_rpc_server 的头文件 glob 仅排除 RPCPool.h,同一头文件仍由两个 target 导出,消费者可绕过新的精简依赖边界。

建议: 从聚合目标的 glob 中排除该头文件,并让消费者显式依赖 :multimodal_pb_converter

Checklist: [6.1] 依赖方向:无循环依赖/跨层惊喜;[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.

4 participants