本篇聚焦 managed / HMM 路径回迁到 CPU 时具体如何选节点、如何分配物理页、失败如何回退、以及如何按 NUMA 节点跟踪驻留。这是「回迁是否考虑 NUMA」最核心的答案所在。
1. per-NUMA-node 的 CPU 驻留跟踪
UVM 的 VA block 不是笼统地记「这页在 CPU 上」,而是按 NUMA 节点分别记录。
结构 uvm_va_block_cpu_node_state_t(uvm_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()
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 4148100 :
preferred_location与preferred_nid应合并为一个「处理器+节点」类型(uvm_va_policy.h)。
7. 一句话结论
回迁到 CPU 的 managed/HMM 路径是 NUMA 感知的 :按「显式请求 → preferred_nid → 就近」选节点,用 __GFP_THISNODE 严格分配并按节点跟踪驻留;严格分配失败时优雅回退到任意节点,功能正确性优先。
特殊硬件(EGM / 集成 GPU / C2C)与 ATS 缺页的 NUMA 行为见 04-special-hardware-ats。