Skip to content

[设计提案] 客户端多轨 RDMA 读路径:单 Worker 多网卡并行读取同一对象 #29

Description

@123123213weqw

背景与动机

条带化大对象的多盘聚合带宽可能超过单张网卡的传输能力,使网络成为大对象 KV Cache 加载的新瓶颈:即使后端磁盘仍有余量,客户端 Worker 也可能因单条 RDMA 路径饱和而无法继续缩短读取时间。本提案在 KVService 客户端侧实现多轨(multi-rail)RDMA 读路径,让单个 Worker 通过多张 RDMA 网卡并行读取同一对象(对应 DaoCloud 智算云赛道赛题《ContextStore 多轨网络设计与实现》)。

设计概要

  • 轨(Rail)= 客户端本地一张 RDMA 网卡/端口 + 独占资源切片:verbs context、PD、CQ、QP、缓存 MR、健康位与指标;连接 = (轨, 远端端点),每条连接一个专属 worker 线程,保持 TCP 控制面"一连接一在途请求"的既有不变量。
  • 完全复用现有数据面:同一目标缓冲区在每张卡上各注册一次(rkey 仅在本设备有效),条带按轨分组后发 stripe-subset GET(沿用 tag 12/15),服务端 RDMA WRITE 的落位语义不变——不改变磁盘条带布局,不改变上层读取接口。服务端仅需既有 CS_RDMA_DEVICES 多网卡监听能力,无需改动。
  • 三级规划:validate_placement(缺失/重复/越界/长度不符在发 IO 前报类型化错误)→ build_plan(交叉相乘的字节精确加权均衡,平局偏向高权重轨)→ plan_waves(按连接上限分波)。
  • 数据完整与版本一致:每个任务携带完整 Descriptor(handle/generation/ETag/layout_version),服务端不匹配即拒;客户端四重校验——条带覆盖检查、实际字节数、num_chunks 数量、条带 xxh3-64 校验和(与服务端 twox-hash 编码一致)。
  • 传输与内存安全(晚到写防护):失败或取消的读在返回前完成静默——排空在途应答 → Stop → join worker → ibv_destroy_qp → MR 反注册;QP 销毁后残余重传打到已释放 QPN/失效 rkey,被传输层丢弃,不可能 DMA 进已释放或复用的内存。
  • 资源与背压:RailLimits(单轨/总连接数、单轨/总在途字节、io_timeout、轨冷却窗口),规划分波保证连接上限,派发线程在字节预算不足时带期限等待。
  • 可观测:RailSnapshot 每轨吞吐、时延(avg/max)、错误/超时数、在途请求与字节、注册内存、连接创建/静默数,Display 单行表格化。
  • Python Worker 入口:rdma-ffi 新增 cs_mr_* C ABI(cs_mr_new/cs_mr_read/cs_mr_rail_stats/cs_mr_free)+ src/contextstore/storage/multirail_client.py ctypes 绑定,Worker 无需 Rust 依赖。

实现与验证

  • PR: client-rs: multi-rail RDMA reads — one Worker, many NICs #28(分支 multirail-read,5 个提交,+3730/-16,15 个文件)
  • 单元测试 21 个(规划/校验/权重/端点白名单/GID 亲和/拓扑/冷却/校验和编码)
  • 硬件门控 e2e 3 个:双轨与单轨结果逐字节一致;死端点注入安全失败且同一缓冲区立即复用成功(晚到写防护);读期间对象重写 → StaleDescriptor 触发重新 lookup
  • Soft-RoCE(rxe over veth)测试床实测:512MB×5 轮双轨负载两轨各 1280 MiB 字节级精确均衡、0 错误 0 超时;系统级两进程各占一轨聚合吞吐 1.81×;瓶颈转移分析——rxe 封包/CRC 全在内核 CPU 路径,单进程 ~0.3 GiB/s 即天花板,真实网卡(硬件 DMA 卸载)无此限制
  • 上游回归全绿:make e2e、clippy(default 与 rdma 双模式)、cargo fmt、Python 集成 pytest
  • 带中文快速上手文档与一键 up/down 的 rxe 测试床脚本(参赛分支 competition:docs/multirail-quickstart-zh.md、scripts/rxe-testbed-setup.sh)

兼容性

  • 单轨配置 = 行为等价旧路径;关闭多轨或仅一条 rail 时走现有单轨路径
  • RdmaClient 公共 API 仅增不改(新增 builder 均有默认值);上游 rdma_bench/rdma-ffi 既有接口不受影响;gRPC-only 构建(无 rdma feature)零依赖变化
  • 顺带修复上游两处:--features rdma 下 build_put_request 单测缺参编译失败;tier_a 执行器 read_aligned_into_ptr_batch 未实现导致 RDMA 条带读取静默返回 bytes=0(已在文档记录,建议上游补实现)

希望与社区对齐的问题

  1. rail 配置语法 device[:port[:gid[:weight[:mtu]]]][@ep[;ep...]] 作为正式配置面是否可接受?
  2. cs_mr_* FFI 是否可作为 Python Worker 的长期多轨入口,还是希望收敛到现有 cs_rdma_* 命名风格?
  3. 硬件门控 e2e(CS_MR_* 环境变量驱动)是否值得在 CI 以 soft-roce 形式常驻?

欢迎维护者与社区评审指正,详细设计文档见 PR 描述与参赛分支 docs/multirail-design-zh.md。

Activity

  1. yanchaomei commented on Oct 1, 2026

    @yanchaomei

    更新(2026-10-01):按项目负责人的选择,下面的增量草稿 PR 已关闭,不作为本次参赛交付;我们将从 DaoCloud/ContextStore 的 main 独立实现。以下旧分支分析仅保留为技术讨论,后续会另行提交独立设计与代码链接。

    我在现有 #28 的 c155086 基础上提交了一份增量补丁,原 PR 的多轨主体实现及历史 RXE 实验仍归原作者所有:可审查的补丁 PR,新增提交 2fa729d。

    补丁主要处理两个验收风险:

    1. LookupObject 对同一存储节点只公布一个主 RDMA endpoint。现在可把这个 endpoint 按本地 Rail 静态映射到该节点的不同 listener;调度和基准不再改写 Placement,每条 Rail 的任务走独立连接。task_max_stripes 拆出的任务也有独立 QP lane。
    2. 原安全读取在部分 Rail 已写入后失败时,会留下被部分改写的调用方缓冲区;取消、校验失败等提前返回路径还可能跳过非 sticky MR 清理。普通 Rust/Python 入口现在使用请求私有暂存区,全部成功后才提交;MR 清理未确认则拆除连接并报错。另加入暂存、注册内存估算和在途字节预约上限,以及 Python 可选的读后 Descriptor 复核。

    在 Linux ARM64 容器中,发现服务端固定 O_DIRECT=0o40000 实际对应 O_DIRECTORY,导致条带测试报 ENOTDIR;补丁已修复。验证:Rust RDMA 特性 SDK 38 项单测、普通双节点 4 项集成测试,服务端 70/95 项两种特性组合单测,Python 118 项测试通过。本补丁尚未在双 RXE 或实体 HCA 上复测,也没有新的带宽数据。

    希望社区确认两点:静态 advertised->listener 路由是否适合作为首版配置,还是希望服务端在 Placement 中显式公布同一节点的所有 Rail endpoint?普通读取以失败原子性为默认、保留显式 unsafe 高性能路径,是否符合预期接口语义?

  2. yanchaomei commented on Oct 1, 2026

    @yanchaomei

    独立实现进展(2026-10-02): 我们从 DaoCloud main 的 b5c6451 独立开发,没有以 #28 为基底。公开草稿 PR #32 当前提交 81f437d;之前基于 #28 的增量草稿已关闭,不计入本实现。

    Rust SDK 的 KvClient::read_multi_rail_into 在同一个 Worker 中对同一对象的条带进行多 Rail 调度、独立 QP/CQ/MR 传输、完整汇总与版本复查,保持已有磁盘布局和上层读接口。服务端补齐 Tier A 指针流完成事件及 tag-15 回退地址映射;CQ 完成状态不确定时关闭该连接,客户端 GET 控制超时也退休自己的 QP,避免晚到 CQE/回复被下一请求误认。总/单 Rail 预算、注册内存与进程 RLIMIT_MEMLOCK 的提前背压均已覆盖。

    真实 Verbs 结果分开报告:

    • 实体 HCA 单轨: SKV node1→node2 ConnectX-6 Dx 完成同对象还原、slab/回退、断连、取消、校验损坏与恢复、超时后晚到 WRITE 和缓冲区复用。原始计数与限制见单轨 HCA JSON。两个条带目录在同一 SATA SSD,不能证明多盘收益。
    • 双 Soft-RoCE: 在同一宿主机上建立两台隔离 Ubuntu KVM 来宾,以两座独立 tap/bridge、四个 RXE 设备、两个 listener 运行标准 Verbs。单个 Worker 的 64 MiB 读取由两 Rail 各传 32 MiB,内容与单轨一致;第二 Rail 停链/恢复、旧 Generation、条带校验失败、双轨在途取消、第一轨已完成而第二轨晚到、CQ/控制回复连接退休、双轨 slab 回退均已验证。原始配对样本、拓扑和逐轨计数、故障测试收据及部署脚本已公开。
    • 性能边界: 同对象/布局/来宾/并发的 64、128、256 MiB 配对测量中,双轨相对单轨的中位时延比约 1.071×、1.086×、1.113×;并发 4、64 MiB 的聚合吞吐改善只有 1.8%,CPU 与 RSS 也上升。这些是 RXE/QEMU 软件路径结果,不能宣称实体双 HCA、PCIe/NUMA 或多 NVMe 聚合性能。七台 SKV 节点各只有一条在线物理 HCA 端口;现有 OFED 下宿主 RXE 模块不兼容,我们没有卸载共享驱动。

    目前 PR 保持 draft;官方 Actions 对 fork PR 标记 action_required、尚未启动 job,维护者 Review 与比赛平台回执未取得。双实体 HCA 性能和最终 3–5 分钟演示视频仍作为独立门槛。希望维护者优先反馈首版显式 advertised endpoint → listener 映射及旧对象无校验和时的兼容策略。

  3. yanchaomei commented on Oct 2, 2026

    @yanchaomei

    Independent main-based draft PR #32 now includes the final safety regression fix in commit c439c59: legacy complete-object GET retains slab pins, extents and registered fallback sources until QP destruction when WRITE completion is uncertain, and never reuses the old QP/CQ for fallback. An isolated RXE test produced a 2 s server CQ timeout (0/64 completions), server connection retirement, client QP retirement, and an unchanged reused destination after six seconds; receipt hashes are in the PR.

    The four-minute two-RXE demonstration release contains the MP4, cover image and complete live command/output capture JSON. It shows one Worker reading the same 64 MiB / 16-stripe object through two independent RXE rails (32 MiB each), second-link shutdown/recovery, checksum fault/recovery and a late second-rail WRITE. The video clearly labels itself an edited, narrated terminal record from a real Soft-RoCE/KVM environment. This evidence validates Verbs behavior and software scaling; it does not claim dual physical-HCA bandwidth aggregation. Maintainer design/review feedback on the draft PR is welcome.

  4. yanchaomei commented on Oct 2, 2026

    @yanchaomei

    Independent main-based PR #32 has advanced to c1c1de1. The added design note describes an optional PlacementDescriptor.rdma_rails field: KVService advertises listeners only for actual stripe owners, and the Rust client matches their fabric IDs to configured local Verbs devices. Stored stripes and existing read APIs remain unchanged; manual routes still work with old servers, and old clients ignore the new protobuf field.

    On two isolated real-Verbs RXE paths, the client configured only its local fabric-a/rxe_c0 and fabric-b/rxe_c1 paths, discovered both listeners via LookupObject, and read the same 64 MiB/16-stripe object at 32 MiB per rail. An actual second-link shutdown caused overall failure with the first rail complete, second rail failed and caller buffer unchanged; link recovery succeeded. Old-client/new-server and new-client/old-server manual reads also passed. The source-matched raw receipts preserve all outputs and SHA-256 values. Latest x86_64 release checks: server 102 passed, client 44 passed/1 intentional Mock benchmark ignored, isolated two-node E2E 4 passed, and strict RDMA client Clippy passed.

    We would welcome maintainer feedback on the Placement capability field, fabric naming/upgrade semantics and whether this discovery contract fits ContextStore's roadmap. As before, the dual-RXE results do not claim physical dual-HCA aggregation.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions