InvokeDirect 验证报告

日期: 2026-04-16 分支: feat/bypass-datasystem-invoke

一、功能验证

1. SDK invoke_direct 基础功能 (in-cluster)

Case 描述 结果
stateless_invoke_direct @yr.invoke 函数 .invoke_direct() 返回 ObjectRefDirect,值正确 PASS
instance_invoke_direct @yr.instance 类方法 .invoke_direct() 返回 ObjectRefDirect,值正确 PASS
invoke_vs_invoke_direct .invoke() 返回 ObjectRef.invoke_direct() 返回 ObjectRefDirect,结果一致 PASS
invoke_direct_large_data 1000 元素 list,invoke_direct 正常返回 PASS
invoke_direct_multiple_calls 连续 10 次 invoke_direct,全部正确 PASS

2. out-of-cluster 模式 (通过 Frontend)

Case 修复前 修复后
100KB invoke_direct PASS PASS
200KB invoke_direct 卡死 (timeout) PASS
500KB invoke_direct 卡死 PASS
1MB invoke_direct 卡死 PASS
4MB invoke_direct 卡死 PASS

3. 超阈值行为 (100MB)

Case 结果
99MB invoke_direct PASS,正常返回
101MB invoke_direct RuntimeError: bypass_datasystem: return value size (xxx bytes) exceeds the 104857600 bytes limit. Use invoke() instead of invoke_direct() for large return values.
60MB 普通 invoke (DS) PASS,不受阈值限制

二、修复的 Bug

Bug 1: bypass_datasystem 标志在 FunctionProxy 路径丢失

根因: functionsystem/.../invocation_handler.cppInvokeRequestToCallRequest() 没有将 InvokeOptions.bypass_datasystem 复制到 CallRequest.bypass_datasystem

影响:

  • 通过 FunctionProxy 的 invoke(非 direct connection),bypass_datasystem 始终为 false
  • Worker 端不走 bypass 路径,返回值走 DS,但 caller 端以为是 bypass 模式(不做 ref counting)
  • 结果:out-of-cluster 模式下 >100KB 数据卡死;in-cluster 模式下无 bypass 性能收益

修复:

// functionsystem/.../invocation_handler.cpp:45
callRequest->set_bypass_datasystem(request->invokeoptions().bypass_datasystem());

Bug 2: 超阈值截断产生不可反序列化的数据

根因: 超过阈值时,C++ 层截断 pickle 序列化后的 raw bytes 前 N 字节返回,破坏序列化格式。

影响: Python 反序列化时 split_buffer 断言失败,抛出无意义的 AssertionError

修复: 超阈值时直接设 ERR_PARAM_INVALID 错误码返回,Python 层收到明确的 RuntimeError

// src/libruntime/invokeadaptor/invoke_adaptor.cpp:657-665
if (req.bypass_datasystem() && bufSize > BYPASS_DS_TRUNCATION_THRESHOLD) {
    auto msg = fmt::format(
        "bypass_datasystem: return value size ({} bytes) exceeds the {} bytes limit. "
        "Use invoke() instead of invoke_direct() for large return values.",
        bufSize, BYPASS_DS_TRUNCATION_THRESHOLD);
    callResult.set_code(common::ERR_PARAM_INVALID);
    callResult.set_message(msg);
    return callResult;
}

三、性能对比

测试环境: 单节点 in-cluster,每个数据量 3 轮取平均。

size invoke_direct (ms) invoke/DS (ms) 延迟比 direct RSS (MB) DS RSS (MB) RSS 节省
1MB 8.6 6.2 1.4x 127 130 2%
5MB 40 20 2.0x 163 178 8%
10MB 68 37 1.8x 231 259 11%
20MB 144 63 2.3x 383 442 13%
40MB 375 155 2.4x 595 714 17%
60MB 620 226 2.7x 816 901 9%
80MB 816 304 2.7x 1018 1258 19%

结论

  • 延迟: invoke_direct 在大数据量(>5MB)下比 DS 路径慢 2-3x。原因是 gRPC inline 传输整个序列化消息(protobuf 编解码开销),而 DS 路径通过共享内存零拷贝传输。
  • 内存: invoke_direct 的 RSS 峰值比 DS 路径低 ~10-20%,因为跳过了 DS 的 buffer 管理和引用计数结构。
  • 适用场景: invoke_direct 适合小数据量(<5MB)+ 无需 DS 引用计数管理的场景(如一次性调用、无后续 ObjectRef 传递)。大数据量场景应使用普通 invoke。

四、配置参数

参数 文件
BYPASS_DS_TRUNCATION_THRESHOLD 100MB src/libruntime/utils/constants.h
YR_MAX_GRPC_SIZE 128MB src/dto/config.h

五、测试用例

测试代码位于 test/smoke/invoke-direct/

文件 描述
run_test.sh 安装 whl、启动 yr、运行测试、清理
test_invoke_direct.py SDK (cluster-internal) + Frontend HTTP (cluster-external) 测试
services.yaml yrlib service 定义

运行方式(需在 dev container 中):

cd test/smoke/invoke-direct && bash run_test.sh