1 userspace-api: NVIDAIA UMA 用户态使用方式(NUMA 感知迁移)

本篇讲清楚 用户态如何触发 / 控制 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_locationpolicy->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


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, &params);
    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 要点

  1. 两个入口都能带节点 :一次性迁移用 UVM_MIGRATE.cpuNumaNode;长期倾向用 UVM_SET_PREFERRED_LOCATION.preferredCpuNumaNode
  2. 优先级 :显式 cpuNumaNode > preferred_nid > 内核默认。
  3. managed 允许 -1 (不限定),pageable 不允许 -1
  4. 不保证严格落点 :内核尽力(__GFP_THISNODE),失败会回退到任意节点或把控制权交回用户态(pageable 的重试协议)。
  5. Grace-Hopper / EGM 等 :迁到「集成 GPU」实际等价于迁到该 GPU 最近的 CPU 节点,见 04-special-hardware-ats

内核侧如何消费这些参数,见 02-kernel-migration-flow03-numa-node-selection