本篇讲清楚 用户态如何触发 / 控制 UVM 迁移,尤其是回迁到 CPU 时如何指定 NUMA 节点。分三个层面:CUDA 高层 API、UVM 用户库 API、底层 ioctl 协议。
1. 三层接口关系
应用 / CUDA Runtime
│ cudaMemPrefetchAsync / cudaMemAdvise / cudaMallocManaged
▼
libnvidia-uvm 用户库 (UvmMigrate / UvmSetPreferredLocation ...)
│ ioctl(/dev/nvidia-uvm, UVM_MIGRATE, ...)
▼
UVM 内核驱动 (uvm_api_migrate, uvm_api_set_preferred_location ...)
开源仓库 kernel-open/nvidia-uvm/uvm_ioctl.h 定义了内核 ABI;CUDA/UVM 用户库是这些 ioctl 的封装。下面以 ioctl 结构体为准(这是可复现的 ground truth)。
2. 主动迁移:UVM_MIGRATE
定义见 uvm_ioctl.h:
c
#define UVM_MIGRATE UVM_IOCTL_BASE(51)
typedef struct
{
NvU64 base; // IN 起始虚拟地址
NvU64 length; // IN 字节长度
NvProcessorUuid destinationUuid; // IN 目标处理器 UUID(GPU 的 UUID;CPU 用特殊 UUID)
NvU32 flags; // IN UVM_MIGRATE_FLAG_*
NvU64 semaphoreAddress; // IN 异步完成信号(可选)
NvU32 semaphorePayload; // IN
NvS32 cpuNumaNode; // IN ★ 目标 CPU NUMA 节点(回迁到 CPU 时使用)
NvU64 userSpaceStart; // OUT 需用户态补做的区间起点
NvU64 userSpaceLength; // OUT 需用户态补做的区间长度
NV_STATUS rmStatus; // OUT
} UVM_MIGRATE_PARAMS;
2.1 cpuNumaNode 语义(关键)
当目标是 CPU 时,cpuNumaNode 指定希望页面落到哪个 NUMA 节点。合法性校验(见 uvm_ioctl.h 注释 与 uvm_api_migrate 中的校验,uvm_migrate.c):
cpuNumaNode 被视为非法,若:
- 小于
-1; - 大于等于系统最大节点数;
- 对应一张已注册的 GPU 的内存节点;
- 不在
node_possible_map内; - 该节点没有 online 的内存(
!nv_numa_node_has_memory)。
特殊值:
cpuNumaNode == -1 (NUMA_NO_NODE):对 managed 内存 表示「不限定节点,交给内核/策略决定」;对 pageable 内存 则是非法(pageable 路径必须给出确定节点,见下文重试协议)。
2.2 flags
| flag | 含义 |
|---|---|
UVM_MIGRATE_FLAG_ASYNC |
异步;配合 semaphoreAddress/Payload 完成通知 |
UVM_MIGRATE_FLAG_SKIP_CPU_MAP |
目标为 CPU 时跳过建立 CPU 映射(仅测试构建可用),使迁移可完全异步 |
UVM_MIGRATE_FLAG_NO_GPU_VA_SPACE |
允许目标 GPU 未注册 GPU VA space(仅测试) |
2.3 Pageable 内存的「用户态-内核态协作」重试协议
对系统分配(pageable)内存,一次 ioctl 未必能完成,用户库需要按内核返回码循环处理(见 uvm_ioctl.h 注释):
NV_WARN_NOTHING_TO_DO:内核遇到 file-backed vma 或无 GPU 可驱动拷贝。用户态改用move_pages(2)迁移userSpaceStart/userSpaceLength指示的区间,然后从该 vma 之后继续。NV_ERR_MORE_PROCESSING_REQUIRED:在目标 CPU 节点分配失败。用户态换一个 CPU NUMA 节点 (遵循线程的 NUMA 策略)重试;若无更多节点可试,则改用UVM_POPULATE_PAGEABLE在任意节点补页。NV_OK:成功(仅保证页面被 populate,不保证一定落在请求节点)。
这段协议正是「回迁到 CPU 时考虑 NUMA」在用户态的体现:内核会尽力落在
cpuNumaNode,落不下就把决策权交回用户态换节点重试。
3. 首选位置策略:UVM_SET_PREFERRED_LOCATION
定义见 uvm_ioctl.h:
c
#define UVM_SET_PREFERRED_LOCATION UVM_IOCTL_BASE(42)
typedef struct
{
NvU64 requestedBase; // IN
NvU64 length; // IN
NvProcessorUuid preferredLocation; // IN 首选处理器(CPU 或某 GPU)
NvS32 preferredCpuNumaNode; // IN ★ 首选位置为 CPU 时的首选 NUMA 节点
NV_STATUS rmStatus; // OUT
} UVM_SET_PREFERRED_LOCATION_PARAMS;
作用:为一段 VA range 设置「首选驻留位置」。之后任何原因触发的「回迁到 CPU」------无论是显式 UVM_MIGRATE(cpuNumaNode=-1)、缺页、还是驱逐------只要没有更高优先级的显式节点,就会优先落到 preferredCpuNumaNode。
内核侧写入 policy->preferred_location 与 policy->preferred_nid:
- managed:
uvm_va_range_set_preferred_location()(uvm_va_range.c); - HMM/系统内存:
uvm_va_policy_set_preferred_location()(uvm_va_policy.c)。
对应 CUDA:cudaMemAdvise(ptr, size, cudaMemAdviseSetPreferredLocation, device);CPU 节点粒度的首选位置对应较新的 cudaMemLocation(node 类型)语义。
UVM_UNSET_PREFERRED_LOCATION(IOCTL 43)清除策略,preferred_nid 回到 NUMA_NO_NODE。
4. 查询当前策略
UVM_TOOLS_GET_PROCESSOR_UUID_TABLE / 通过 uvm_test 路径可读回策略。内核在导出参数时把 policy->preferred_nid 写入 params->preferred_cpu_nid:
- managed:uvm_va_range.c(默认
NUMA_NO_NODE,有策略时填policy->preferred_nid); - HMM:uvm_hmm.c。
5. 典型用户态用法示例
5.1 CUDA 高层
c
// 分配 managed 内存
void *p;
cudaMallocManaged(&p, N);
// 建议首选落在 CPU 的 NUMA node 2(较新 CUDA 用 cudaMemLocation 表达 node)
cudaMemLocation loc = { .type = cudaMemLocationTypeHostNuma, .id = 2 };
cudaMemAdvise_v2(p, N, cudaMemAdviseSetPreferredLocation, loc);
// 在 GPU 上算完后,把数据预取回 CPU node 2
cudaMemLocation host = { .type = cudaMemLocationTypeHostNuma, .id = 2 };
cudaMemPrefetchAsync_v2(p, N, host, /*flags=*/0, stream);
cudaStreamSynchronize(stream);
// 此时 p 的页面会尽量驻留在 node 2 的 DRAM 上
5.2 直接走 ioctl(做驱动/系统软件时)
c
int fd = open("/dev/nvidia-uvm", O_RDWR);
// 回迁 [base, base+len) 到 CPU 的 NUMA node 1
UVM_MIGRATE_PARAMS params = {0};
params.base = base;
params.length = len;
params.destinationUuid = UVM_CPU_UUID; // CPU 的约定 UUID
params.cpuNumaNode = 1; // ★ 目标 NUMA 节点
params.flags = 0; // 同步
for (;;) {
ioctl(fd, UVM_MIGRATE, ¶ms);
if (params.rmStatus == NV_OK)
break;
else if (params.rmStatus == NV_WARN_NOTHING_TO_DO) {
// 用 move_pages() 处理 [userSpaceStart, userSpaceLength),再从其后继续
move_pages_range(params.userSpaceStart, params.userSpaceLength, /*node=*/1);
params.base = params.userSpaceStart + params.userSpaceLength;
params.length = original_end - params.base;
}
else if (params.rmStatus == NV_ERR_MORE_PROCESSING_REQUIRED) {
params.cpuNumaNode = pick_another_numa_node(); // 换节点重试
params.base = params.userSpaceStart;
}
else {
// 真正的错误
break;
}
}
注意:
UVM_CPU_UUID是 UVM 约定用于表示「CPU 处理器」的特殊 UUID;实际值以用户库/头文件为准。
6. 用户态需要记住的 NUMA 要点
- 两个入口都能带节点 :一次性迁移用
UVM_MIGRATE.cpuNumaNode;长期倾向用UVM_SET_PREFERRED_LOCATION.preferredCpuNumaNode。 - 优先级 :显式
cpuNumaNode>preferred_nid> 内核默认。 - managed 允许
-1(不限定),pageable 不允许-1。 - 不保证严格落点 :内核尽力(
__GFP_THISNODE),失败会回退到任意节点或把控制权交回用户态(pageable 的重试协议)。 - Grace-Hopper / EGM 等 :迁到「集成 GPU」实际等价于迁到该 GPU 最近的 CPU 节点,见 04-special-hardware-ats。
内核侧如何消费这些参数,见 02-kernel-migration-flow 与 03-numa-node-selection。