3 numa-node-selection: NUMA 目标节点选择与 CPU chunk 分配

本篇聚焦 managed / HMM 路径回迁到 CPU 时具体如何选节点、如何分配物理页、失败如何回退、以及如何按 NUMA 节点跟踪驻留。这是「回迁是否考虑 NUMA」最核心的答案所在。


1. per-NUMA-node 的 CPU 驻留跟踪

UVM 的 VA block 不是笼统地记「这页在 CPU 上」,而是按 NUMA 节点分别记录

结构 uvm_va_block_cpu_node_state_tuvm_va_block.h):

c 复制代码
typedef struct {
    uvm_page_mask_t resident;   // 该 NUMA 节点上「有一致副本」的页
    uvm_page_mask_t allocated;  // 该 NUMA 节点上「已分配 CPU chunk」的页
    unsigned long   chunks;     // 该节点的 CPU chunk 存储(opaque)
} uvm_va_block_cpu_node_state_t;

block 里为每个有内存的 NUMA 节点维护一份这样的状态(block->cpu.node_state[]uvm_va_block.h),并有一个聚合的 block->cpu.resident / block->cpu.allocated

访问器:

  • block_node_state_get(block, nid)uvm_va_block.c);
  • uvm_va_block_cpu_is_page_resident_on(block, nid, page_index)uvm_va_block.c);
  • uvm_va_block_resident_mask_get(block, UVM_ID_CPU, nid)nid == NUMA_NO_NODE 时返回聚合掩码。

意义:这让「同一页可以在不同 NUMA 节点间迁移」成为可跟踪的一等操作------回迁到 CPU 不只是「回到 CPU」,而是「回到某个具体 node」。


2. 目标节点归一化:uvm_va_block_context_get_node()

uvm_va_block.c

c 复制代码
static int uvm_va_block_context_get_node(uvm_va_block_t *block,
                                         uvm_va_block_region_t region,
                                         uvm_va_block_context_t *va_block_context)
{
    const uvm_va_policy_t *policy = uvm_va_policy_get_region(block, region);

    if (va_block_context->make_resident.dest_nid != NUMA_NO_NODE)
        return va_block_context->make_resident.dest_nid;  // ① 迁移请求显式节点
    if (policy->preferred_nid != NUMA_NO_NODE)
        return policy->preferred_nid;                     // ② preferred location 的节点
    return numa_mem_id();                                 // ③ 当前就近节点兜底
}

优先级链:显式请求 > preferred_nid > 就近


3. 核心分配:block_populate_pages_cpu()

uvm_va_block.c。这是把 CPU 页在指定 NUMA 节点上分配出来的地方。

3.1 确定 preferred_nid 与是否 STRICT

c 复制代码
int preferred_nid = block_context->make_resident.dest_nid;

// 测试注入(调试用)
if (block_test && block_test->cpu_chunk_allocation_target_id != NUMA_NO_NODE)
    preferred_nid = block_test->cpu_chunk_allocation_target_id;

// 请求未指定则用 range 的首选节点
if (preferred_nid == NUMA_NO_NODE)
    preferred_nid = policy->preferred_nid;

if (preferred_nid != NUMA_NO_NODE) {
    uvm_va_block_cpu_node_state_t *node_state = block_node_state_get(block, preferred_nid);
    allocated_mask = &node_state->allocated;              // 只看该节点的已分配位图
    alloc_flags |= UVM_CPU_CHUNK_ALLOC_FLAGS_STRICT;      // ★ 严格落在该节点
}
else {
    allocated_mask = &block->cpu.allocated;               // 聚合位图
}
  • 指定了节点 → 加 STRICT 标志(底层用 __GFP_THISNODE),且只认该节点 allocated 位图(已在别的节点上的页不算数,会重新在目标节点分配 → 实现「换 node」)。
  • 未指定节点 → 用聚合位图,任意 CPU 节点都算已分配。

3.2 逐页/逐 chunk 分配

对区域内每个尚未在目标节点分配/驻留的页:

c 复制代码
if (uvm_page_mask_test(allocated_mask, page_index) ||
    uvm_va_block_cpu_is_page_resident_on(block, preferred_nid, page_index)) {
    // 已在目标节点,跳过
    continue;
}

allocation_sizes = block_calculate_largest_alloc_size(...);   // 尽量用大 chunk(含 2M)
...
status = block_alloc_cpu_chunk(block, allocation_sizes, chunk_alloc_flags, preferred_nid, &chunk);

3.3 STRICT 失败 → 回退到 NUMA_NO_NODE(关键容错)

c 复制代码
if (status == NV_WARN_MORE_PROCESSING_REQUIRED) {
    alloc_flags &= ~UVM_CPU_CHUNK_ALLOC_FLAGS_STRICT;   // 放弃严格
    preferred_nid = NUMA_NO_NODE;                       // 不再限定节点
    block_context->make_resident.dest_nid = NUMA_NO_NODE;
}
else if (status != NV_OK) {
    return status;
}

语义:尽力(best-effort)落在目标节点,落不下就退回「任意节点」而不是让整个迁移失败。这保证了功能正确性优先,NUMA 优化其次。


4. 物理页分配:uvm_cpu_chunk_alloc*()

文件:uvm_pmm_sysmem.c

uvm_cpu_chunk_alloc()L493)→ uvm_cpu_chunk_alloc_page()L427):

c 复制代码
if ((alloc_flags & UVM_CPU_CHUNK_ALLOC_FLAGS_STRICT) && nid != NUMA_NO_NODE)
    kernel_alloc_flags |= __GFP_THISNODE;      // ★ 强制本节点,不跨节点回退

if (alloc_flags & UVM_CPU_CHUNK_ALLOC_FLAGS_ALLOW_MOVABLE)
    kernel_alloc_flags |= GFP_HIGHUSER_MOVABLE;
else
    kernel_alloc_flags |= GFP_HIGHUSER;

if (alloc_size > PAGE_SIZE)                     // 大 chunk:避免高延迟内存整理
    kernel_alloc_flags |= __GFP_COMP | __GFP_NORETRY | __GFP_NOWARN;

if (alloc_flags & UVM_CPU_CHUNK_ALLOC_FLAGS_ZERO)
    kernel_alloc_flags |= __GFP_ZERO;

if (nid == NUMA_NO_NODE)
    page = alloc_pages(kernel_alloc_flags, get_order(alloc_size));
else {
    UVM_ASSERT(node_isset(nid, node_online_map));
    page = alloc_pages_node(nid, kernel_alloc_flags, get_order(alloc_size));  // ★ 指定节点
}

if (page && (alloc_flags & UVM_CPU_CHUNK_ALLOC_FLAGS_STRICT))
    UVM_ASSERT(page_to_nid(page) == nid);      // STRICT 下断言确实落在目标节点

分配标志汇总(uvm_pmm_sysmem.h):

flag 作用
STRICT __GFP_THISNODE,强制目标节点,不跨节点回退(失败向上抛,触发 §3.3 回退)
ALLOW_MOVABLE 允许从 movable zone 分配(默认不允许,保证可被 GPU 直接映射)
ZERO 分配即清零(当高阶 chunk 内不是所有页都已驻留时使用)
ACCOUNT 计入 cgroup 内存核算(有 mm、非 HMM 时)

5. make_resident 之后:更新 per-node 状态

拷贝完成后,node_pages_mask 记录「本次落到该节点的页」,随后 block_add_cpu_chunk() / block 状态更新会把这些页记入对应节点的 allocated / resident 位图(uvm_va_block.c)。这样后续查询 uvm_va_block_cpu_is_page_resident_on(block, nid, ...) 能反映真实 NUMA 落点。


6. 待优化点(源码 TODO)

代码显式标注了两个与 NUMA 相关的已知次优点:

  • Bug 4158598 :staging(中转)分配使用 NUMA_NO_NODE 是次优的(uvm_va_block.c)。当 GPU→GPU 迁移需要 CPU 中转,中转页没有 NUMA 倾向。
  • Bug 4148100preferred_locationpreferred_nid 应合并为一个「处理器+节点」类型(uvm_va_policy.h)。

7. 一句话结论

回迁到 CPU 的 managed/HMM 路径是 NUMA 感知的 :按「显式请求 → preferred_nid → 就近」选节点,用 __GFP_THISNODE 严格分配并按节点跟踪驻留;严格分配失败时优雅回退到任意节点,功能正确性优先。

特殊硬件(EGM / 集成 GPU / C2C)与 ATS 缺页的 NUMA 行为见 04-special-hardware-ats

相关推荐
DeeplyMind2 小时前
1 userspace-api: NVIDAIA UMA 用户态使用方式(NUMA 感知迁移)
cuda numa