Mooncake TENT Request Path
导言
上一篇文章把 TENT 的请求路径压缩成了一串箭头。那串箭头没有错,但它隐藏了代码走读时最容易断掉的几处连接:公共 Request 在哪里变成 TaskInfo,为什么 selector 会返回 GDS,一个逻辑 task 怎样展开成多个 cuFile slice,以及完成事件怎样重新聚合成公共状态。
本文固定在 Mooncake 提交 89da2c3a,只追踪一次成功的 GDS 读取。每个关键节点都给出实际会执行的 C++ 片段;代码语句保持原样,只增加中文走读注释和明确的省略标记。
先给出结论:一个 TENT 文件读取不会从 submitTransfer 直接跳到 cuFileBatchIOSubmit。中间至少发生五次对象换手:
Request
→ PreparedSubmit::Owner
→ Batch::TaskInfo
→ GdsSubBatch::IOParamRange
→ CUfileIOParams_t[1..N]
→ CUfileIOEvents_t[1..N]
→ TransferStatus
先限定这条路径¶
“正常路径”必须有明确边界,否则代码里的可选机制会被误写成必经步骤。本文采用以下条件:
- Mooncake 构建时定义了
USE_GDS,配置中显式设置transports/gds/enable=true。 - 不自定义 transport policy,文件 Segment 使用默认候选顺序
GDS → IOURING。 - 使用默认
enable_runtime_queue=false,因此prepareSubmit后直接进入commitPreparedSubmit。 - 只提交一个
Request::READ。默认的merge_requests=true仍会执行,但单请求不会发生合并。 file://指向普通文件,GDS 文件 handle、batch handle 和 cuFile I/O 均成功。- 调用者主动轮询状态,并在看到
COMPLETED后释放 Batch。
这些默认值和 GDS 开关分别落在下面两处。这里要注意一个看起来有些反直觉的组合:runtime queue 默认关闭,request merge 默认开启,而 GDS 即使编译进来,运行时也默认关闭。
// transfer_engine_impl.cpp:构造运行时默认行为。
merge_requests_ = conf_->get("merge_requests", true);
max_failover_attempts_ = conf_->get("max_failover_attempts", 3);
enable_auto_failover_on_poll_ =
conf_->get("enable_auto_failover_on_poll", true);
enable_progress_worker_ = conf_->get("enable_progress_worker", false);
runtime_queue_config_.enabled = conf_->get("enable_runtime_queue", false);
源码:transfer_engine_impl.cpp:348-354。
// transport_loader.cpp:只有构建和配置两道门都打开,GDS 对象才存在。
#ifdef USE_GDS
if (conf_->get("transports/gds/enable", false))
transport_list_[GDS] = std::make_shared<GdsTransport>();
#endif
源码:transport_loader.cpp:87-90。
下图只画上述成功路径。蓝色实线是实际函数调用,灰色虚线是状态返回;图里没有画 queue、failover、staging 和 cancellation。
图:基于 Mooncake 89da2c3a 源码整理的自绘时序图。公共 task 进入 GDS 后可能展开为多个 slice,轮询结果再沿相反方向聚合为 TransferStatus。
请求进入 TENT 前有什么¶
Request 描述的是谁搬到哪里¶
公共请求本身很薄:操作方向、本地指针、目标 Segment、目标偏移和长度是执行路径真正需要的字段。
struct Request {
enum OpCode { READ, WRITE };
OpCode opcode;
void* source;
SegmentID target_id;
uint64_t target_offset;
size_t length;
int priority = PRIO_HIGH;
std::optional<std::string> policy_name;
TransportType transport_hint = UNSPEC;
uint64_t deadline_ns = 0;
IntentType intent_type = IntentType::INTENT_UNSPEC;
};
源码:types.h:132-153。
source 这个名字在 READ 路径上容易误导:GDS 最终把它填入 CUfileIOParams_t::devPtr_base,所以读文件时它实际是本地接收 buffer;写文件时才是本地数据源。target_offset 始终是文件偏移。
Segment handle 还不是文件 handle¶
调用者先用 file://path 得到 SegmentID。openSegment 在这一刻只登记名字与 ID,并没有执行 POSIX open:
Status TransferEngineImpl::openSegment(SegmentID& handle,
const std::string& segment_name) {
if (segment_name.empty() || segment_name == local_segment_name_) {
handle = LOCAL_SEGMENT_ID;
return Status::OK();
}
// [走读] file://path 也先走这里:只得到 TENT SegmentID。
return metadata_->segmentManager().openRemote(handle, segment_name);
}
源码:transfer_engine_impl.cpp:640-647,ID 映射见 segment_manager.cpp:47-60。
第一次解析这个 Segment 时,getRemote 才根据 file:// 构造 FileSegmentDesc 并用 stat 确认文件存在;真正的 open(O_DIRECT) 和 cuFileHandleRegister 还要等到 GDS 提交阶段。对应代码分别在 segment_manager.cpp:142-158、segment_manager.cpp:192-217 和 gds_transport.cpp:32-68。
Batch 只是公共 task 容器¶
allocateBatch(1) 分配的是 TENT 公共 Batch。此时还没有 GDS SubBatch,也没有 cuFile batch handle:
BatchID TransferEngineImpl::allocateBatch(size_t batch_size) {
Batch* batch = Slab<Batch>::Get().allocate();
if (!batch) return (BatchID)0;
batch->max_size = batch_size;
batch->task_list.reserve(batch_size);
BatchID batch_id = (BatchID)batch;
// [省略] 把 Batch 加入 active/alive registry。
return batch_id;
}
源码:transfer_engine_impl.cpp:898-912。
节点一:公共 API 进入实现层¶
TransferEngine 的公开函数没有调度逻辑,它把 Batch、请求列表和状态对象直接交给 TransferEngineImpl:
Status TransferEngine::submitTransfer(
BatchID batch_id, const std::vector<Request>& request_list) {
// [走读] 公开 API 到实现层没有额外线程切换或数据复制。
return impl_->submitTransfer(batch_id, request_list);
}
Status TransferEngine::getTransferStatus(BatchID batch_id, size_t task_id,
TransferStatus& task_status) {
return impl_->getTransferStatus(batch_id, task_id, task_status);
}
源码:transfer_engine.cpp:142-145、transfer_engine.cpp:171-174。
实现层先 retain Batch,再准备请求。由于本文限定 runtime queue 关闭,shouldQueueSubmit 返回 false,所以当前线程会直接执行 commitPreparedSubmit:
Status TransferEngineImpl::submitTransfer(
BatchID batch_id, const std::vector<Request>& request_list,
const Notification* notifi, QueueOwnerKind owner_kind) {
Batch* batch = nullptr;
CHECK_STATUS(retainBatch(batch_id, batch));
BatchRef batch_ref(*this, batch);
const size_t start_task_id = batch_ref.get()->task_list.size();
PreparedSubmit prepared;
CHECK_STATUS(prepareSubmit(batch_ref.get(), request_list, prepared));
if (shouldQueueSubmit(prepared, owner_kind)) {
// [省略] enable_runtime_queue=true 时的 admission/dispatch 路径。
} else {
// [走读] 默认配置命中这里,提交在调用线程内继续。
CHECK_STATUS(commitPreparedSubmit(batch_ref.get(), prepared));
}
// [省略] 可选 notification hook。
return batch_ref.release();
}
源码:transfer_engine_impl.cpp:2160-2187。
这里的返回值只表示准备与底层提交是否成功,不表示文件读取已经完成。NVIDIA 对 cuFile Batch API 的定义是:提交调用本身同步返回,但 I/O 相对 host thread 异步执行,完成要通过 status API 查询。cuFile Batch API
节点二:准备请求并选择 GDS¶
Request 先变成 PreparedSubmit::Owner¶
prepareSubmit 做三件事:校验 hint、尝试合并连续请求、为每个合并后的 owner 解析 transport。对本文的单请求,merged.request_list 仍只有一个元素:
Status TransferEngineImpl::prepareSubmit(
Batch* batch, const std::vector<Request>& request_list,
PreparedSubmit& prepared) {
// [省略] batch 与 transport_hint 校验。
prepared = PreparedSubmit{};
const size_t start_task_id = batch->task_list.size();
prepared.submit_time = std::chrono::steady_clock::now();
auto merge_boundaries =
merge_requests_
? resolveRequestBoundaries(metadata_.get(), request_list)
: std::vector<RequestBoundaryInfo>{};
auto merged =
mergeRequests(request_list, merge_boundaries, merge_requests_);
prepared.owners.reserve(merged.request_list.size());
for (const auto& request : merged.request_list) {
PreparedSubmit::Owner owner;
owner.request = request;
// [走读] transport_index=0:取当前 policy 的第一个可用候选。
owner.route = resolveTransport(owner.request, 0);
// [省略] TCP 才可能进入 staging policy;GDS 不走该分支。
prepared.owners.push_back(std::move(owner));
}
// [省略] 建立 public task 到 merged owner 的映射。
return Status::OK();
}
源码:transfer_engine_impl.cpp:1650-1696。
这一步结束后还没有 TaskInfo,只有一份临时计划:
Owner.request保存可能经过合并的物理请求;Owner.route保存 selector 的结果;PreparedSubmit::Task保存公共 task ID 与 owner 的映射。
selector 为什么返回 GDS¶
getTransportType 先读取 target_id 对应的 Segment 描述,再把本地指针识别成 CPU 或 CUDA memory。对 File Segment,它不会查远端 buffer 的 transport 列表,而是构造文件选择上下文:
if (desc->type == SegmentType::File) {
// [走读] 文件请求的候选来自 policy,不来自 BufferDesc::transports。
ctx.segment_type = SegmentType::File;
ctx.same_machine = true;
ctx.local_memory_type = local_mtype;
ctx.remote_memory_type = MTYPE_CPU;
ctx.buffer_transports = nullptr;
} else {
// [省略] Memory Segment 路径。
}
return transport_selector_->select(ctx, transport_list_, transport_index,
hint);
源码:transfer_engine_impl.cpp:1217-1313。
默认 file_storage policy 给出的原始候选是 {GDS, IOURING}。selector 按顺序跳过不存在或 capability 不匹配的 transport;GDS 安装时声明了 dram_to_file=true 和 gpu_to_file=true,因此满足本文条件时,第一个候选就是 GDS。
const auto& raw = !matching_policy->transports.empty()
? matching_policy->transports
: context.buffer_transports ? *context.buffer_transports
: kEmpty;
auto candidates = reorderWithHint(raw, hint);
if (!candidates) return result;
for (size_t i = 0; i < candidates->size(); ++i) {
TransportType type = (*candidates)[i];
if (!isTransportAvailable(type, context, available_transports))
continue;
if (transport_index == 0) {
// [走读] 默认文件 policy 的第一个可用项是 GDS。
result.transport = type;
break;
}
--transport_index;
}
源码:默认 policy 见 transport_selector.cpp:84-102,capability 检查见 transport_selector.cpp:456-515,候选选择见 transport_selector.cpp:518-593。
选择 GDS 不等于证明走了 direct path
selector 只证明 TENT 选择了 GdsTransport。实际 I/O 是否完全避开 bounce buffer 还取决于 buffer 注册、对齐、文件系统、拓扑和 cuFile 兼容模式;TENT 这条路径没有读取 cuFile stats 来证明物理数据路径。
节点三:公共 task 进入 GDS SubBatch¶
commitPreparedSubmit 才把临时计划写进长期存在的 Batch::task_list。正常单请求会生成一个 TaskInfo,状态初始化为 PENDING,type 设为 GDS:
for (const auto& task_plan : prepared.tasks) {
size_t task_id = task_plan.task_id;
size_t merged_task_id = task_plan.merged_task_index;
auto& task = batch->task_list[task_id];
const auto& owner = prepared.owners[merged_task_id];
auto& merged_request = owner.request;
// [省略] 已合并请求的 derived-task 分支。
task.failover_count = 0;
task.xport_priority = 0;
task.status = PENDING;
task.request = merged_request;
task.staging = false;
task.start_time = prepared.submit_time;
task.dispatch_time = prepared.submit_time;
task.type = owner.route.transport; // [走读] 此处为 GDS。
task.device_mask = owner.route.device_mask;
if (!batch->sub_batch[task.type]) {
auto& transport = transport_list_[task.type];
auto status = transport->allocateSubBatch(
batch->sub_batch[task.type], batch->max_size);
// [省略] allocate 失败处理。
attachProgressNotifier(batch, batch->sub_batch[task.type]);
}
task.sub_task_id = -1;
task.derived = false;
physical_task_id_list[task.type].push_back(task_id);
// [省略] owner 映射与循环收尾。
}
源码:transfer_engine_impl.cpp:1727-1802。
allocateSubBatch 创建的是 GDS 私有容器。它从池中取 BatchHandle;池为空时调用 cuFileBatchIOSetUp,然后准备参数、事件和稳定状态缓存:
// [省略] 从 handle_pool_ 尝试取得 BatchHandle。
if (!batch_handle || batch_handle->max_nr != io_batch_depth_) {
batch_handle = new BatchHandle();
batch_handle->max_nr = io_batch_depth_;
auto result =
cuFileBatchIOSetUp(&batch_handle->handle, io_batch_depth_);
// [省略] SetUp 失败清理。
}
gds_batch->batch_handle = batch_handle;
gds_batch->max_size = max_size;
gds_batch->io_events.resize(io_batch_depth_);
gds_batch->io_params.clear();
gds_batch->io_params.reserve(io_batch_depth_);
gds_batch->io_param_ranges.clear();
gds_batch->cached_events.clear();
gds_batch->cached_events.reserve(io_batch_depth_);
gds_batch->reusable = true;
gds_batch->cancel_requested = false;
TENT 随后用 sub_batch->size() 计算 sub_task_id,收集这个 transport 的请求,并真正调用后端:
int next_sub_task_id = static_cast<int>(sub_batch->size());
for (const auto physical_task_id : group) {
for (const auto public_task_id :
public_tasks_by_physical_owner.at(physical_task_id)) {
batch->task_list[public_task_id].sub_task_id = next_sub_task_id;
}
++next_sub_task_id;
}
std::vector<Request> requests;
requests.reserve(group.size());
for (const auto task_id : group)
requests.push_back(batch->task_list[task_id].request);
// [走读] type=GDS,因此虚调用落到 GdsTransport::submitTransferTasks。
auto status = transport->submitTransferTasks(sub_batch, requests);
// [省略] 同步提交失败处理。
源码:transfer_engine_impl.cpp:1829-1877。
到这里,公共 task 与 GDS task 已经有了稳定映射:
节点四:GDS 把一个 task 展开为 slice¶
GdsTransport::submitTransferTasks 做的不是简单参数转发。它先延迟打开文件,再把每个逻辑请求切成不超过 16 MiB 的物理 slice:
Status GdsTransport::submitTransferTasks(
SubBatchRef batch, const std::vector<Request>& request_list) {
const static size_t kMaxSliceSize = 16ull << 20;
auto gds_batch = dynamic_cast<GdsSubBatch*>(batch);
// [省略] gds_batch 类型校验。
size_t num_params = 0;
size_t first_param_index = gds_batch->io_params.size();
for (auto& request : request_list)
num_params +=
(request.length + kMaxSliceSize - 1) / kMaxSliceSize;
if (first_param_index + num_params > io_batch_depth_)
return Status::TooManyRequests("Exceed batch capacity" LOC_MARK);
for (auto& request : request_list) {
// [走读] 第一次见到 target_id 时才创建真正的文件执行对象。
GdsFileContext* context = findFileContext(request.target_id);
if (!context || !context->ready())
return Status::InvalidArgument("Invalid remote segment" LOC_MARK);
IOParamRange range;
range.base = gds_batch->io_params.size();
for (size_t offset = 0; offset < request.length;
offset += kMaxSliceSize) {
size_t length = std::min(kMaxSliceSize, request.length - offset);
const size_t slice_id = gds_batch->io_params.size();
CUfileIOParams_t params;
params.mode = CUFILE_BATCH;
params.opcode =
(request.opcode == Request::READ) ? CUFILE_READ : CUFILE_WRITE;
// [走读] cookie 使用 slice_id+1,轮询时可回填稳定缓存。
params.cookie = reinterpret_cast<void*>(
static_cast<std::uintptr_t>(slice_id + 1));
params.u.batch.devPtr_base = request.source;
params.u.batch.devPtr_offset = offset;
params.u.batch.file_offset = request.target_offset + offset;
params.u.batch.size = length;
params.fh = context->getHandle();
gds_batch->io_params.push_back(params);
CUfileIOEvents_t cached_event{};
cached_event.cookie = params.cookie;
cached_event.status = CUFILE_PENDING;
gds_batch->cached_events.push_back(cached_event);
range.count++;
}
// [走读] 一个逻辑 task 对应 [base, base+count) 这段 slice。
gds_batch->io_param_ranges.push_back(range);
}
auto result =
cuFileBatchIOSubmit(gds_batch->batch_handle->handle, num_params,
&gds_batch->io_params[first_param_index], 0);
// [省略] cuFile 同步返回错误的处理。
return Status::OK();
}
源码:gds_transport.cpp:437-490。文件上下文的延迟创建见 gds_transport.cpp:404-435。
把一个 20 MiB 的抽象例子代入上述循环,映射会变成:
| 逻辑对象 | devPtr_offset |
file_offset |
size |
cookie |
|---|---|---|---|---|
| slice 0 | 0 | target_offset |
16 MiB | 1 |
| slice 1 | 16 MiB | target_offset + 16 MiB |
4 MiB | 2 |
这个例子只是在代入源码常量,不是性能测试。可以确认的是切片方式;源码没有解释为什么选择 16 MiB,因此不能把它写成 GDS 的通用最优值。
节点五:轮询把 slice 重新聚合成 task¶
TENT 先根据 sub_task_id 找回 GDS task¶
调用者开始轮询后,TransferEngineImpl::pollTaskStatus 从公共 TaskInfo 取出 transport 和 sub_task_id,再调用相应后端:
Status TransferEngineImpl::pollTaskStatus(Batch* batch, size_t task_id,
TransferStatus& task_status) {
auto& task = batch->task_list[task_id];
// [省略] staging 与 UNSPEC 路径。
auto& transport = transport_list_[task.type];
auto& sub_batch = batch->sub_batch[task.type];
if (!transport || !sub_batch) {
return Status::InvalidArgument("Transport not available" LOC_MARK);
}
// [走读] task.type=GDS,sub_task_id 指向 IOParamRange。
return transport->getTransferStatus(sub_batch, task.sub_task_id,
task_status);
}
源码:transfer_engine_impl.cpp:2376-2396。
cuFile event 先写临时数组,再按 cookie 固化¶
GDS 的一次 status 调用会查询整个 cuFile batch。io_events 只是本轮返回的 scratch buffer,cached_events 才是按 slice 保存的稳定状态:
Status GdsTransport::updateBatchStatus(GdsSubBatch* batch) {
unsigned num_events = static_cast<unsigned>(batch->io_params.size());
if (num_events == 0) return Status::OK();
auto result =
cuFileBatchIOGetStatus(batch->batch_handle->handle, 0, &num_events,
batch->io_events.data(), nullptr);
// [省略] GetStatus 错误处理。
for (size_t index = 0; index < num_events; ++index) {
const auto& event = batch->io_events[index];
const auto cookie = reinterpret_cast<std::uintptr_t>(event.cookie);
if (cookie == 0 || cookie > batch->cached_events.size())
continue;
auto& cached_event = batch->cached_events[cookie - 1];
if (!isTerminalCuFileStatus(cached_event.status) ||
isTerminalCuFileStatus(event.status)) {
// [走读] 用提交时的 cookie 把事件写回对应 slice。
cached_event = event;
}
}
return Status::OK();
}
只有全部 slice 终结,task 才完成¶
getTransferStatus 先查 IOParamRange。若它还在 PENDING,就更新 batch、聚合 [base, base+count),累加已完成字节;在本文的成功路径上,所有 slice 都是 CUFILE_COMPLETE 后才把 range 置为 COMPLETED:
auto& range = gds_batch->io_param_ranges[task_id];
if (range.status != PENDING) {
status = TransferStatus{range.status, range.transferred_bytes};
return Status::OK();
}
auto update_status = updateBatchStatus(gds_batch);
// [省略] 轮询失败、slice 失败与 best-effort cancel 分支。
bool all_terminal = false;
auto task_status = aggregateTransferStatus(
gds_batch->cached_events, range.base, range.count, all_terminal);
range.transferred_bytes =
std::max(range.transferred_bytes, task_status.transferred_bytes);
if (all_terminal) {
// [走读] 正常路径 known_failure 仍为 PENDING,采用聚合出的 COMPLETED。
range.status = range.known_failure != PENDING ? range.known_failure
: task_status.s;
gds_batch->reusable = allBatchIOsTerminal(gds_batch);
}
status = TransferStatus{range.status, range.transferred_bytes};
return Status::OK();
源码:gds_transport.cpp:492-561,聚合规则见 gds_transport.cpp:94-150。仓库测试也明确验证两个完成 slice 的字节数会相加并返回 COMPLETED:gds_transport_status_test.cpp:207-216。
节点六:终态之后才释放资源¶
调用者看到终态后执行 freeBatch。默认 queue 关闭且没有额外引用时,TENT 会再次确认整个 Batch 已终结,依次释放各 transport 的 SubBatch,再回收公共 Batch:
if (!runtime_queue_config_.enabled && batch->runtime_refs == 0 &&
!batch->free_requested) {
TransferStatus overall_status;
auto status = getTransferStatus(batch_id, overall_status);
if (status.ok() && overall_status.s != PENDING) {
for (size_t type = 0; type < kSupportedTransportTypes; ++type) {
auto& transport = transport_list_[type];
auto& sub_batch = batch->sub_batch[type];
if (transport && sub_batch)
transport->freeSubBatch(sub_batch);
}
// [省略] 从 registry 移除 Batch。
Slab<Batch>::Get().deallocate(batch);
return Status::OK();
}
}
源码:transfer_engine_impl.cpp:914-950。
GDS 的正常释放不会立刻销毁昂贵的 cuFile batch handle,而是把它放回 handle_pool_,供下一次 allocateSubBatch 复用:
if (reusable) {
{
std::lock_guard<std::mutex> lock(handle_pool_lock_);
handle_pool_.push_back(gds_batch->batch_handle);
}
gds_batch->batch_handle = nullptr;
Slab<GdsSubBatch>::Get().deallocate(gds_batch);
}
batch = nullptr;
return Status::OK();
这一步补全了资源生命周期:cuFileBatchIOSetUp 创建的 handle 属于 GDS pool,不属于某一个公共 task;CUfileIOParams_t、event cache 和 IOParamRange 才属于本次 SubBatch。
把整条路径再串一次¶
现在沿 20 MiB 读取的抽象例子,把对象与状态放在同一张表里:
| 时刻 | 执行函数 | 关键对象 | 状态或变化 |
|---|---|---|---|
| 进入 API | TransferEngine::submitTransfer |
Request |
READ、本地 buffer、File SegmentID、offset、20 MiB |
| 准备 | prepareSubmit |
PreparedSubmit::Owner |
单请求不合并;selector 解析出 GDS |
| 提交计划 | commitPreparedSubmit |
Batch::TaskInfo |
status=PENDING、type=GDS、sub_task_id=0 |
| 分配后端批次 | GdsTransport::allocateSubBatch |
GdsSubBatch |
获得 cuFile batch handle;初始化 range、params、events |
| 翻译请求 | submitTransferTasks |
IOParamRange{base=0,count=2} |
20 MiB 被切成 16 MiB 与 4 MiB 两个 slice |
| 设备提交 | cuFileBatchIOSubmit |
CUfileIOParams_t[2] |
host 同步返回提交结果,I/O 异步推进 |
| 状态轮询 | cuFileBatchIOGetStatus |
CUfileIOEvents_t[2] |
event 通过 cookie 写回 cached_events |
| 状态聚合 | aggregateTransferStatus |
TransferStatus |
两个 slice 均完成后返回 COMPLETED 与 20 MiB |
| 释放 | freeBatch → freeSubBatch |
Batch / SubBatch / handle | 公共 Batch 回收,cuFile batch handle 回池 |
最容易混淆的是三种“数量”:
Batch::max_size限制公共 task 数;GdsSubBatch::io_param_ranges.size()是 GDS 逻辑 task 数;GdsSubBatch::io_params.size()是切片后的 cuFile 物理 I/O 数,受io_batch_depth限制。
事实核查¶
按照“外部可验证事实—推导—判断”拆分后,本文的关键结论可以分成四类:
| 待核查说法 | 结论 | 证据与修正 |
|---|---|---|
默认请求会直接进入 commitPreparedSubmit |
已证实 | enable_runtime_queue 默认 false;若用户显式开启 queue,这条主线必须插入 admission 与 dispatch。 |
| File Segment 默认优先 GDS | 基本成立,但需要收窄 | 默认 policy 是 GDS → IOURING,但 GDS 还必须通过构建、配置、实例存在和 capability 检查。 |
openSegment(file://...) 会打开文件 |
明显错误 | 它只登记 SegmentID;stat 在描述解析时发生,open(O_DIRECT) 与 cuFileHandleRegister 在 GDS 首次提交时发生。 |
registerLocalMemory 会自动对探测到的 CUDA buffer 调用 cuFileBufRegister |
证据不支持这一宽泛说法 | 该实现判断的是 MemoryOptions.location,不是探测后的 BufferDesc.location;MemoryOptions 默认值和默认 permission 重载留下的是 "*"。只有显式 CUDA location 才进入 GDS 注册分支。 |
未显式 cuFileBufRegister 就不能提交 GDS |
明显错误 | NVIDIA 明确说明 buffer registration 是可选的;未注册 buffer 可能使用 cuFile 内部预注册 buffer,并多一次 copy。Best Practices |
submitTransfer 返回成功表示读取完成 |
明显错误 | 它只证明准备和 cuFileBatchIOSubmit 没有同步报错;完成状态来自后续轮询。 |
| 一个 TENT task 对应一个 cuFile I/O | 基本成立,但需要收窄为一对多 | 每个 task 对应一个 IOParamRange,再按 16 MiB 上限展开为一个或多个 CUfileIOParams_t。 |
看到 COMPLETED 时所有 slice 都已终结 |
已证实 | all_terminal 为真后才更新 range 终态;完成字节聚合也有仓库单元测试覆盖。 |
这次核查里最关键的修正不是函数名,而是不要把“选择了 GDS transport”升级成“已经证明物理 direct path”。源码能高置信度证明控制流、对象映射和调用参数;它不能替代一台真实 GDS 机器上的 cuFile stats、对齐检查和端到端数据校验。
结语¶
回头看开头那串箭头,它缺的并不是更多函数名,而是对象身份。
一次成功的 TENT GDS 读取,真正的主线是:Request 先变成调度计划,再变成公共 TaskInfo;GDS 用 sub_task_id 把它映射到 IOParamRange,按 16 MiB 切成 cuFile slice;完成事件依靠 cookie 回填,全部 slice 终结后才重新聚合为公共 TransferStatus。最后,公共 Batch 被释放,昂贵的 cuFile batch handle 则留在池里等待下一次请求。
目前可以相信到这里:固定提交下的成功控制流已经由源码和仓库测试闭环;真实硬件是否走 direct path、性能是否最优,仍需在 GDS 环境中用 cuFile stats 和实际数据校验。
参考文献¶
- Mooncake
89da2c3a - TENT
transfer_engine_impl.cpp - TENT
transport_selector.cpp - TENT
gds_transport.cpp - NVIDIA GDS cuFile API Reference
- NVIDIA GDS Best Practices Guide
