Mooncake TENT GDS

导言

一开始我只知道一件事:Mooncake 通过 TENT 接入了 NVIDIA GPUDirect Storage。真正沿代码往下追时,三个问题很快连在了一起:TENT 到底怎样调度一次传输,GDS 向上提供了哪些能力,以及新后端怎样实现同一套契约。

最关键的判断是:TENT 不是 GDS 的一层薄封装,而是负责 Segment、内存注册、后端选择、批次和状态推进的统一运行时;GdsTransport 才负责把 File Segment 请求翻译成 cuFile Batch I/O。 理清这条分工后,submitTransferTaskscuFileBatchIOGetStatus 和新后端接口会自然落在同一张图里。

本文以 Mooncake 提交 89da2c3a 为代码截面,重点阅读:

三个问题其实是一条链

如果只盯着 cuFileBatchIOSubmit,TENT 看起来像一个多余的中间层。但把对象关系展开后,边界很清楚:

  1. 应用描述意图:本地 buffer、远端 Segment、偏移、长度和读写方向。
  2. TENT 解释意图:解析 Segment,检查本地内存,选择 transport,把公共 Batch 拆成 transport 私有 SubBatch。
  3. 后端执行意图:GDS 把一个逻辑 Request 切成 cuFile slice,提交到底层设备,并把完成事件重新聚合成 TENT 状态。

下图把控制面、统一运行时和 GDS 数据面放在一起。阅读时先抓住中间那条竖向边界:**Transport 接口以上属于 TENT,接口以下才属于 GDS。**

Mooncake TENT 与 GDS 架构

图:TENT 统一管理 Segment、buffer、selector 与 Batch;GdsTransport 管理文件 handle、GPU buffer 注册、cuFile batch handle、slice 参数和完成事件。

这也解释了为什么实现一个新后端不能只提供 send/receive:底层数据搬运 API 只覆盖“执行”,而 TENT 还需要知道它能搬什么、怎样分配批次、怎样查询进度、何时可以释放资源。

TENT 怎样跑一次请求

TENT 的运行逻辑可以分为启动、资源登记、请求提交和状态推进四段。它们不是四条独立路径,而是前一段为后一段准备可验证的对象。

启动与后端选择

TransferEngineImpl 启动时创建配置、拓扑、控制服务、Segment Manager 和 Transport Selector,然后装载编译期存在、运行时启用的 transport。GDS 同时受两道门控制:

1
2
3
4
#ifdef USE_GDS
if (conf_->get("transports/gds/enable", false))
transport_list_[GDS] = std::make_shared<GdsTransport>();
#endif

第一道门是构建时的 USE_GDS,第二道门是配置项 transports/gds/enable。对应源码在 transport_loader.cpp:87-90

装载不等于选中。对 File Segment,当前默认 selector 顺序是:

1
GDS → IOURING

这来自 transport_selector.cpp:84-102。某些设计文档会把 RDMA 也画在文件回退链上,但在这个固定提交中,默认代码就是 {GDS, IOURING};讨论真实行为时应以代码截面为准。

GdsTransport::install 读取 io_batch_depth,默认值是 32,并声明 dram_to_filegpu_to_file 两项 capability。Selector 正是用 Segment 类型、本地内存类型和 capability 判断该 transport 是否匹配。

Segment 与内存登记

文件通过 file://path 进入 TENT。openSegmentSegmentManager 创建 FileSegmentDesc,但这一步只把路径和元数据变成 Segment ID,并没有立即执行 POSIX opencuFileHandleRegister

GDS 第一次看到这个 target_id 时,findFileContext 才会延迟创建 GdsFileContext

1
2
3
4
5
file://path
→ File SegmentDesc / SegmentID
→ findFileContext(target_id)
→ open(path, O_RDWR | O_DIRECT)
→ cuFileHandleRegister

这一区分很重要:Segment 是 TENT 的寻址对象,CUfileHandle_t 是 GDS 的执行对象。 新后端可以复用相同的 file:// Segment,但在自己的 context 中创建完全不同的文件句柄。

本地内存走另一条登记链。registerLocalMemory 生成 BufferDesc,再调用各 transport 的 addMemoryBuffer。GDS 只对 location.type() == "cuda" 的 buffer 调用 cuFileBufRegister,成功后把 GDS 加入 desc.transports。这样 selector 不只知道“系统装了 GDS”,还知道“这块 buffer 已具备 GDS 能力”。

提交与轮询

一次请求的完整时序如下。图比较长,因为它刻意保留了正常完成、部分失败、取消和资源隔离四条分支;第一次阅读可以只跟中间的蓝色主线。

Mooncake TENT GDS 请求时序

图:公共 Request 先被 TENT 解析、合并和分组,再进入 GDS SubBatch;cuFile 返回的 slice 事件经过 cookie 缓存和 range 聚合,最后才变成公共 task 状态。

从调用关系看,关键链路是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TransferEngine::submitTransfer
→ TransferEngineImpl::prepareSubmit
→ 解析本地 buffer 与目标 Segment
→ TransportSelector 选择 GDS 或回退后端
→ 合并、授权并形成 PreparedSubmit
→ TransferEngineImpl::commitPreparedSubmit
→ 按 transport 分组
→ transport->allocateSubBatch(...)
→ transport->submitTransferTasks(...)

TransferEngine::getTransferStatus
→ transport->getTransferStatus(...)
→ GdsTransport::updateBatchStatus(...)
→ cuFileBatchIOGetStatus(...)
→ slice event 聚合为 task 状态

这里存在两层容量,不要混为一个概念:

  • 公共 Batch 容量:限制调用者能放入多少个逻辑 task;
  • **GDS io_batch_depth**:限制一个 GDS SubBatch 能容纳多少个物理 slice。

当前 GDS 将每个请求切成不超过 16 MiB 的 slice。默认 depth 为 32 时,一个 SubBatch 最多容纳 32 个 slice,粗略对应 512 MiB 的满尺寸数据;多个 Request 会共享这 32 个槽位。16 MiB 的选择理由没有写在源码中,因此它是当前实现策略,不是所有新后端都应该复制的 GDS 标准常数。

Mooncake 用了哪些 GDS API

NVIDIA GDS 对应用主要暴露的是 cuFile API。按功能理解,比背一串函数名更有用:它既包含驱动与资源登记,也包含同步 I/O、Batch I/O、stream 异步 I/O 和属性/统计接口。Mooncake TENT 的 GdsTransport 只使用其中一部分。

API 族 代表接口 Mooncake 是否使用 在 TENT 中的职责
驱动生命周期 cuFileDriverOpencuFileDriverClose 只使用 Open GdsTransport 构造时通过 std::call_once 初始化一次
文件句柄 cuFileHandleRegistercuFileHandleDeregister 把 POSIX fd 登记为 CUfileHandle_t
GPU buffer cuFileBufRegistercuFileBufDeregister 登记和注销 CUDA buffer
同步 I/O cuFileReadcuFileWrite TENT GDS 不走逐次同步读写
Batch I/O cuFileBatchIOSetUpSubmitGetStatusCancelDestroy SubBatch 的创建、提交、轮询、取消与销毁
stream 异步 I/O cuFileReadAsynccuFileWriteAsync 当前实现不以 CUDA stream 作为完成模型
属性与观测 properties、stats、version 相关接口 当前实现没有用它们确认实际 direct path 或导出 cuFile 统计

把调用放回生命周期,可以得到更直观的映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
进程/对象初始化
cuFileDriverOpen

文件首次使用
POSIX open(O_RDWR | O_DIRECT)
cuFileHandleRegister

CUDA buffer 登记
cuFileBufRegister

SubBatch 分配与执行
cuFileBatchIOSetUp
cuFileBatchIOSubmit
cuFileBatchIOGetStatus
cuFileBatchIOCancel // 失败清理,best effort

资源释放
cuFileBatchIODestroy
cuFileBufDeregister
cuFileHandleDeregister
POSIX close

POSIX 与 GDS 的边界

opencloseO_DIRECT 是 POSIX 文件接口,不是 GDS API。GDS 通过 cuFileHandleRegister 接管已打开的 fd,使后续 cuFile 请求能够引用它。

cuFileDriverOpen 位于 gds_transport.cpp:242-246;文件 handle 位于 gds_transport.cpp:32-68;buffer 登记位于 gds_transport.cpp:564-585

新后端要实现什么

TENT 的 Transport 基类没有用纯虚函数强迫子类实现所有能力,很多默认实现只是返回 NotImplemented。这意味着“类能编译”不代表“后端已接通”。

对一个类似 GDS 的文件传输后端,最小可运行契约如下:

TENT 接口 后端必须回答的问题 GDS 的答案
install/uninstall 怎样初始化配置、依赖和全局资源;怎样停止并清理 保存 runtime 对象,设置 depth/caps;清理 batch、pool 与元数据
capabilities 能处理哪些本地内存与 Segment 组合 dram_to_file=truegpu_to_file=true
allocateSubBatch 一组请求需要哪些稳定存储、队列槽位和底层 handle 分配 GdsSubBatch,复用或创建 CUfileBatchHandle_t
freeSubBatch 何时能安全复用或销毁底层资源 正常批次回池;仍可能被 cuFile 引用的失败批次进入 quarantine
submitTransferTasks 怎样把 TENT Request 翻译、切片并提交 构造 CUfileIOParams_t,调用 cuFileBatchIOSubmit
getTransferStatus 怎样轮询、聚合进度和错误 GetStatus 得到 slice event,再按 IOParamRange 聚合
add/removeMemoryBuffer 是否需要预注册本地内存,怎样管理生命周期 CUDA buffer 调用 cuFileBufRegister/Deregister
getName 配置、日志和诊断中怎样标识后端 返回 "gds"
cancellation 已提交工作能否取消,何时可释放用户 buffer GDS 未公开覆盖 task cancel;内部 batch cancel 只用于失败清理

sendNotification/receiveNotification 属于可选能力,文件后端通常不需要。supportsCancellation 默认返回 false;如果新后端要向公共 API 声明可取消,就必须同时实现 cancelTransferTask,并遵守“取消是 best effort,调用者仍要轮询到终态”的契约。

类本身之外还要接通四个位置:

  1. 类型与名字:在 TransportType、字符串解析以及 C/Python 绑定中加入新类型;
  2. 构建与装载:CMake 找到 SDK 后定义构建宏,TransportLoader 根据配置创建实例;
  3. 选择策略:为正确的 Segment、本地内存类型和 capability 配置优先级与回退;
  4. Segment/Buffer 语义:决定能否复用 file:// 和现有内存注册;如果底层需要额外 endpoint 或专用 handle,应通过 transport 私有 context 或属性承载。

最容易漏掉的接口

新后端往往先实现 submitTransferTasks,却忽略 freeSubBatch 的安全条件。异步设备仍可能引用参数数组或用户 buffer 时,释放或复用 SubBatch 会变成 use-after-free 或 DMA 写入已复用内存。GDS 的 handle pool、终态检查与 quarantine 正是在解决这个问题。

提交代码逐行读

在进入 submitTransferTasks 前,allocateSubBatch 已经准备好三类关键存储:

  • io_params:交给 cuFile 的 slice 参数数组,预留到 io_batch_depth,避免构造期间反复扩容;
  • io_eventscuFileBatchIOGetStatus 写入的临时事件数组;
  • cached_events:按 slice 长期保存的稳定状态,因为一次轮询不保证返回所有事件。

cuFileBatchIOSetUp 被源码明确标为耗时操作,因此 BatchHandle 会进入对象池复用。准备好这些对象后,才执行下面的 GdsTransport::submitTransferTasks

下面保留原函数的所有语句,只增加中文注释。原仓库已有的英文注释也原样保留。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// 实现 TENT Transport 契约:把一组逻辑 Request 追加到 GDS SubBatch 并一次提交。
Status GdsTransport::submitTransferTasks(
SubBatchRef batch, const std::vector<Request>& request_list) {
// 当前实现规定一个物理 cuFile slice 最大为 16 MiB。
const static size_t kMaxSliceSize = 16ull << 20;

// 公共接口只给出 Transport::SubBatch*,这里校验并转回 GDS 私有类型。
auto gds_batch = dynamic_cast<GdsSubBatch*>(batch);
// 类型不匹配说明调用者传入了别的 transport 的 SubBatch,不能继续解释其内存。
if (!gds_batch)
return Status::InvalidArgument("Invalid GDS sub-batch" LOC_MARK);

// 统计本次 request_list 最终会展开成多少个 CUfileIOParams_t。
size_t num_params = 0;
// io_params 可能已有前一组任务,因此记住本次追加区域的起点。
size_t first_param_index = gds_batch->io_params.size();
// 逐个逻辑请求计算 ceil(length / 16 MiB)。
for (auto& request : request_list)
num_params += (request.length + kMaxSliceSize - 1) / kMaxSliceSize;

// depth 约束的是物理 slice 数,不是 request_list 中的逻辑请求数。
if (first_param_index + num_params > io_batch_depth_)
return Status::TooManyRequests("Exceed batch capacity" LOC_MARK);

// 每个 Request 对应一个公开 task,下面为它建立 slice 范围。
for (auto& request : request_list) {
// target_id 是 TENT 的 File Segment ID;此处延迟取得或创建 GDS 文件 context。
GdsFileContext* context = findFileContext(request.target_id);
// context 不存在或 POSIX open / cuFileHandleRegister 失败时,Segment 不可执行。
if (!context || !context->ready())
return Status::InvalidArgument("Invalid remote segment" LOC_MARK);

// IOParamRange 负责把一个逻辑 task 映射到连续的物理 slice 区间。
IOParamRange range;
// 当前 io_params 尾部就是这个 task 的第一个 slice 下标。
range.base = gds_batch->io_params.size();

// 按 16 MiB 步长遍历请求;最后一个 slice 可以更短。
for (size_t offset = 0; offset < request.length;
offset += kMaxSliceSize) {
// 取剩余长度与 16 MiB 的较小值,得到当前 slice 的真实大小。
size_t length = std::min(kMaxSliceSize, request.length - offset);
// slice_id 是它在整个 SubBatch io_params 中的稳定下标。
const size_t slice_id = gds_batch->io_params.size();

// 创建一个 cuFile Batch I/O 参数。原实现逐字段填写所需成员。
CUfileIOParams_t params;
// 指明这不是同步或 stream 请求,而是 Batch API 参数。
params.mode = CUFILE_BATCH;
// TENT READ 表示文件读入本地 buffer;WRITE 表示本地 buffer 写入文件。
params.opcode =
(request.opcode == Request::READ) ? CUFILE_READ : CUFILE_WRITE;

// Use a one-based slice index so every completion can be cached
// independently, including the first slice (cookie 0 is avoided).
// cookie 作为完成事件的关联键;使用 slice_id + 1,刻意避开空指针值 0。
params.cookie = reinterpret_cast<void*>(
static_cast<std::uintptr_t>(slice_id + 1));

// request.source 在 TENT Request 中保存本地 buffer 地址;GDS 把它作为设备基址。
params.u.batch.devPtr_base = request.source;
// 当前 slice 相对该基址的字节偏移。
params.u.batch.devPtr_offset = offset;
// 当前 slice 在目标文件中的绝对偏移。
params.u.batch.file_offset = request.target_offset + offset;
// 当前 slice 要传输的字节数。
params.u.batch.size = length;
// 使用与 target_id 对应、已经登记到 cuFile 的文件 handle。
params.fh = context->getHandle();

// 保存参数;真正提交时 cuFile 会得到这段 vector 的连续内存。
gds_batch->io_params.push_back(params);

// 为该 slice 建立一份稳定状态缓存,初始内容清零。
CUfileIOEvents_t cached_event{};
// 复制同一个 cookie,使后续完成事件能映射回 slice_id。
cached_event.cookie = params.cookie;
// 请求刚构造完成但尚未观察到终态,先记为 PENDING。
cached_event.status = CUFILE_PENDING;
// 缓存下标与 slice_id 一一对应。
gds_batch->cached_events.push_back(cached_event);

// 当前逻辑 task 的物理 slice 数加一。
range.count++;
}

// 保存该 task 的 [base, base + count) 映射,供 getTransferStatus 聚合。
gds_batch->io_param_ranges.push_back(range);
}

// 将本次新追加的 num_params 个参数一次性交给 cuFile Batch I/O。
auto result =
cuFileBatchIOSubmit(gds_batch->batch_handle->handle, num_params,
&gds_batch->io_params[first_param_index], 0);
// cuFile API 自身返回失败时,转换成 TENT Status;此时不是异步完成事件失败。
if (result.err != CU_FILE_SUCCESS)
return Status::InternalError(
std::string("Failed to submit GDS batch IO: Code ") +
std::to_string(result.err) + LOC_MARK);

// 返回 OK 只表示提交成功,数据传输仍在异步进行,必须继续轮询状态。
return Status::OK();
}

这段代码最值得带走的不是某个 cuFile 字段,而是三层映射:

1
2
3
4
TENT Request
└── IOParamRange {base, count}
└── N 个 CUfileIOParams_t
└── cookie = slice_id + 1

IOParamRange 解决“一个 task 被切成多个 slice”,cookie 解决“完成事件可能乱序或稀疏返回”。少了任何一层,公共 task_id 都无法稳定对应底层完成事件。

状态代码逐行读

cuFileBatchIOGetStatus 不在 getTransferStatus 中直接裸调,而是被封装在 updateBatchStatus 中。原因是 GetStatus 写入的是一次轮询的临时结果,而 TENT 需要跨多次轮询保留每个 slice 的稳定状态。

下面同样保留原函数语句,只添加中文注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 从 cuFile 取回当前可见的完成事件,并合并到 per-slice 稳定缓存。
Status GdsTransport::updateBatchStatus(GdsSubBatch* batch) {
// io_events 是本次 GetStatus 的输出区,cached_events 是跨轮询缓存;二者都必须覆盖全部参数。
if (batch->io_events.size() < batch->io_params.size() ||
batch->cached_events.size() < batch->io_params.size()) {
// 缓冲区不足意味着后续写入或索引会越界,因此直接报告内部错误。
return Status::InternalError(
"GDS completion buffers are smaller than the submitted IO "
"set" LOC_MARK);
}

// 输入时表示 io_events 最多可接收多少个事件;调用返回后会被改写为实际事件数。
unsigned num_events = static_cast<unsigned>(batch->io_params.size());
// 空批次没有事件可查,也避免把空 vector 的 data 交给底层。
if (num_events == 0) return Status::OK();

// 轮询 cuFile Batch I/O 的完成队列。
auto result =
cuFileBatchIOGetStatus(batch->batch_handle->handle, 0, &num_events,
batch->io_events.data(), nullptr);
// handle:allocateSubBatch 中 SetUp、submitTransferTasks 中 Submit 的同一个 batch handle。
// 第二个参数 0:min_nr 为 0,不要求等待至少一个事件,因而用于非阻塞轮询。
// &num_events:in/out 参数;输入输出数组容量,返回本轮实际写入的事件个数。
// io_events.data():本轮临时事件输出数组,事件顺序不用于直接定位 task。
// nullptr:不提供超时对象;配合 min_nr=0,调用者通过重复 polling 推进状态。

// 这里检查的是“查询动作”本身是否成功,不是某个 slice 的 I/O 状态。
if (result.err != CU_FILE_SUCCESS) {
// 将 cuFile 错误码提升为 TENT 内部错误,交给上层处理批次失败与资源隔离。
return Status::InternalError(
std::string("Failed to get GDS batch status: Code ") +
std::to_string(result.err) + LOC_MARK);
}

// 只遍历本轮真正返回的 num_events 个事件,而不是整个输出数组容量。
for (size_t index = 0; index < num_events; ++index) {
// 取出一个 cuFile completion;它通过 cookie 而不是数组位置关联原 slice。
const auto& event = batch->io_events[index];
// 提交时把 one-based slice ID 编码成 void*,此处还原成整数。
const auto cookie = reinterpret_cast<std::uintptr_t>(event.cookie);

// cookie=0 未被本实现分配;超过缓存大小也说明事件不能安全映射。
if (cookie == 0 || cookie > batch->cached_events.size()) {
// 记录异常事件,但不让错误 cookie 造成越界访问。
LOG(ERROR) << "Invalid GDS batch IO cookie: " << cookie;
// 跳过这个无法归属的 completion,继续处理同批次其他事件。
continue;
}

// one-based cookie 减一后,正好得到提交时的 slice_id。
auto& cached_event = batch->cached_events[cookie - 1];
// 缓存尚未终态时接受新状态;缓存已终态时只接受另一个终态,避免倒退回 PENDING。
if (!isTerminalCuFileStatus(cached_event.status) ||
isTerminalCuFileStatus(event.status)) {
// 用本轮事件更新该 slice 的稳定缓存。
cached_event = event;
}
}

// 本轮 completion 已合并完成;是否整个 task 终态由上层聚合函数判断。
return Status::OK();
}

GetStatus 返回后,getTransferStatus 还要完成三件事:

  1. 按 range 聚合:读取 [range.base, range.base + range.count) 中的所有 cached event,累计完成字节,并把 cuFile 状态映射为 TENT 状态;
  2. 保留首个失败:某个 slice 先失败、其他 slice 仍 pending 时,把失败记入 known_failure,避免后续取消状态掩盖原始错误;
  3. 等待物理终态:可以请求 cuFileBatchIOCancel,但在所有 slice 都进入终态前,公共 task 仍保持 PENDING,因为 cuFile 可能还在访问用户 buffer。

失败优先级按 FAILED > TIMEOUT > CANCELED > INVALID 聚合。已完成字节数使用 max 保证多次轮询单调不减。只有整个底层 batch 都不再被 cuFile 引用,GdsSubBatch 才能把 handle 放回池中;否则它进入 quarantine,后续清理线程继续轮询到安全终态。

内部取消不等于公共取消

GdsTransport 有私有 cancelBatch,用于某个 slice 失败后的整批 best-effort 清理;但它没有覆盖 supportsCancellation()cancelTransferTask(),所以不能据此推导公共 GDS task 已支持按任务取消。

TENT 还有一个容易忽略的回退边界:提交阶段 submitTransferTasks 直接返回错误时,当前运行时把 task 标成 UNSPEC;轮询阶段则只在状态为 FAILED 时触发自动跨 transport failover。对应代码分别在 transfer_engine_impl.cpp:1859-1876transfer_engine_impl.cpp:2398-2415。因此,新后端不能假设“任何错误都会自动尝试下一个 transport”。

边界与验证

沿源码可以确认 TENT 与 GDS 的软件契约,但还不能仅凭代码确认机器上的实际数据路径。

  • GDS 不等于必然直通:cuFile 可能根据平台、文件系统和配置进入 compatibility mode,经 POSIX 与 CPU bounce buffer 完成 I/O。Mooncake 当前没有查询 properties 或 stats 来证明某次请求实际走了 direct path。
  • 文件以读写方式打开GdsFileContext 固定使用 O_RDWR | O_DIRECT,即使上层只想读文件,也需要对应的打开权限。
  • 注册基址需要实机校验:buffer 登记使用 desc.addr,提交时却直接把 request.source 填入 devPtr_base。如果后者是注册区间内部指针而非原始注册基址,需要结合实际 cuFile 版本验证是否仍命中 registered-buffer fast path。
  • 16 MiB 不是外部规范:源码没有解释该值的性能依据;新后端应根据自己的最大请求、对齐、队列深度和真实 KV block 分布测量。
  • 本文没有替代硬件测试:结论来自固定提交源码与 NVIDIA API 语义,没有在特定 GPU、NVMe 和文件系统组合上跑端到端带宽、CPU 占用或兼容模式验证。

实机验证至少应同时记录:nvidia-fs/cuFile 环境检查、目标文件系统支持情况、registered 与 unregistered buffer、不同 slice/depth、CPU 占用、吞吐与 P99,以及失败后 buffer 何时可安全复用。

最终判断

现在可以把开头的三个问题收束成一句话:TENT 用统一对象和生命周期定义“传输应该怎样被管理”,GDS 用 cuFile 定义“文件与 GPU 数据具体怎样被搬运”。

TENT 的主线是 Segment 与 buffer 登记、capability 选择、公共 Batch 到 SubBatch 的拆分、状态推进与有限回退;GDS 的主线是 fd/handle 和 GPU buffer 登记、Batch handle 复用、16 MiB 切片、cookie 关联、完成事件缓存,以及失败后的取消与隔离。

因此,接入新后端时可以直接复用 TENT 的上半层,但不能只替换 cuFileBatchIOSubmit。真正要实现的是从 installfreeSubBatch 的完整异步契约,尤其是:底层何时不再引用参数和用户内存。 只要这个终态边界讲清楚,提交、轮询、取消和资源回收才会同时正确。

参考文献

Author

Shaojie Tan

Posted on

2026-08-26

Updated on

2026-08-26

Licensed under