本文分析 libhsakmt(Thunk)在
hsaKmtAllocMemory分配路径中如何使用 NUMA,把系统内存绑定到离 GPU 最近的 CPU 节点,以降低 GPU 访问系统内存的跨节点开销。
1. 目标与动机
GPU 通过 PCIe / XGMI 访问系统内存(system DRAM)时,访问延迟与带宽取决于该内存物理落在哪个 CPU 内存控制器(IMC)所辖的 NUMA 节点上。落在与 GPU直连的那个 CPU 节点上最优,跨 socket 则要多走一跳一致性互连,延迟更高、带宽更低。因此 libhsakmt 在分配分页系统内存时,会主动把这段内存用 Linuxmbind(2)绑定到 GPU 直连的 CPU NUMA 节点。
一句话概括整条链路:GPU node → 反查直连 CPU node → 对该段 VA 执行mbind→ 后续缺页从该 NUMA 节点取页。
2. 关键前提:分页系统内存才走 NUMA
NUMA 绑定只作用于"分页的系统内存"(以 userptr 形式交给 KFD 的匿名页),不作用于 VRAM 或 pinned 内存:
| 内存类型 | 是否 NUMA 绑定 | 原因 |
|---|---|---|
分页系统内存(userptr,!NonPaged) | 是 | 页由 CPU 分配,NUMA policy 决定物理落点。 |
NonPaged(pinned)/ GTT / VRAM | 否 | 物理落点由 KFD / 显存控制器决定,不归 CPU NUMA policy 管。 |
对应判断在fmm_allocate_host_gpu(src/fmm.c):只有!mflags.ui32.NonPaged && svm.userptr_for_paged_mem分支才mmap匿名页并随后mbind。
3. 拓扑解析:GPU → 直连 CPU 节点
NUMA 目标节点不是由调用者传入的,而是由 GPU 节点在拓扑中反查得到。
3.1 挑选直连 CPU(gpu_get_direct_link_cpu)
位于src/topology.c,遍历该 GPU 的 IO links,挑选满足条件的对端 CPU 节点:
for(i=0;i<node_props[gpu_node].node.NumIOLinks;i++)if((props[i].IoLinkType==HSA_IOLINKTYPE_PCIEXPRESS||props[i].IoLinkType==HSA_IOLINK_TYPE_XGMI)&&props[i].Weight<=20)/* >20 是 GPU->CPU->GPU 的间接路径 */{if(!node_props[props[i].NodeTo].node.KFDGpuID)// 对端必须是 CPU 节点returnprops[i].NodeTo;}要点:
- 只认PCIe 或 XGMI直连链路;
Weight <= 20用于排除"绕经另一颗 CPU/GPU"的间接路径(权重越大越远);- 对端
KFDGpuID == 0表示它是 CPU 节点(而非 GPU)。
3.2 校验并返回 NUMA id(hsakmt_get_direct_link_cpu)
外层封装再确认该 CPU 节点确实挂有内存 bank,否则视为无效:
cpu_id=gpu_get_direct_link_cpu(gpu_node,topology_ctx->node_props);if(cpu_id==-1)returnINVALID_NODEID;for(i=0;i<node_props[cpu_id].node.NumMemoryBanks;i++)size+=node_props[cpu_id].mem[i].SizeInBytes;returnsize?(uint32_t)cpu_id:INVALID_NODEID;术语区分:分配 API 里的
PreferredNode/preferred_gpu_id指的是GPU 节点(决定用哪张卡的 aperture 与 BO);而这里得到的numa_node_id是由 GPU 反查出来的直连 CPU 节点,二者含义不同,不要混淆。
4. 绑定核心:bind_mem_to_numa
位于src/fmm.c,是唯一的 NUMA 绑定实现,本质是对mbind(2)的封装。
4.1 提前退出的两种情况
if(mflags.ui32.NoNUMABind||numa_available()==-1){if(mflags.ui32.NoSubstitute)return-EFAULT;// 要求硬绑定却无法绑 → 失败elsereturn0;// 否则静默放行,让页落在默认节点}NoNUMABind:调用者显式关闭 NUMA 绑定(对应HsaMemFlags的同名位);numa_available() == -1:系统无 NUMA 支持。
无效节点(越界 /INVALID_NODEID/ 只有一个节点)也走同样的"要么失败、要么放行"逻辑。
4.2 硬约束 vs 软偏好
intmode=MPOL_F_STATIC_NODES;...mode|=mflags.ui32.NoSubstitute?MPOL_BIND:MPOL_PREFERRED;r=mbind(mem,SizeInBytes,mode,node_mask->maskp,num_node+1,0);NoSubstitute | policy | 语义 |
|---|---|---|
| 置位 | MPOL_BIND | 必须落在直连节点,无法满足即失败(-EFAULT)。 |
| 不置位 | MPOL_PREFERRED | 尽量落在直连节点,不行则退让到其他节点。 |
MPOL_F_STATIC_NODES保证节点掩码不随进程 cpuset 变化被重映射,绑定的是物理节点。
4.3 失败处理与兼容性
if(r){if(errno==EPERM){// docker seccomp 默认拦截 mbindpr_err_once("mbind is blocked by seccomp\n");return0;// 故意放行,否则容器内无法分配系统内存}if(!mflags.ui32.NoSubstitute)// 非硬绑定时,无内存可用也忽略return0;return-EFAULT;// 硬绑定失败才真正报错}- 容器兼容:seccomp 拦截
mbind返回EPERM时返回 0,保证容器内可用; - 尽力而为:非
NoSubstitute情况下即便mbind失败也当成功(宁可换节点,也不让分配失败)。
5. 分配主流程(fmm_allocate_host_gpu)
分页系统内存路径中,NUMA 绑定发生在"拿到 VA、建立 BO 之前",确保后续缺页从正确节点取页:
if(!mflags.ui32.NonPaged&&fmm_ctx->svm.userptr_for_paged_mem){/* 1. 保留并映射匿名页的 VA */mem=aperture_allocate_area_aligned(aperture,address,size,alignment);mmap(mem,MemorySizeInBytes,PROT_READ|PROT_WRITE,MAP_ANONYMOUS|MAP_SHARED|MAP_FIXED,-1,0);/* 2. 绑定到 GPU 直连的 CPU NUMA 节点 */if(bind_mem_to_numa(node_id,mem,MemorySizeInBytes,mflags))gotoout_release_area;/* 3. 大页建议 */madvise(mem,MemorySizeInBytes,advice);// >=2MB 时 MADV_HUGEPAGE/* 4. 作为 userptr BO 交给 KFD */ioc_flags|=KFD_IOC_ALLOC_MEM_FLAGS_USERPTR;vm_obj=fmm_allocate_memory_object(ctx,preferred_gpu_id,mem,size,aperture,&mmap_offset,ioc_flags);}顺序很关键:先mbind设策略,再让内存被 touch / 建 BO,这样物理页首次分配时就落到目标 NUMA 节点,避免"先分配后迁移"的开销。
5.1 udmabuf 路径的额外容量检查
另一处调用在 udmabuf 分配路径(src/fmm.c),在绑定后还会用numa_node_size64检查目标节点剩余内存是否足够,不足则告警并可回退:
numa_node_id=hsakmt_get_direct_link_cpu(ctx,node_id);if(bind_mem_to_numa(numa_node_id,mem,size,mflags))...node_size=numa_node_size64(numa_node_id,&free_size);/* 比较 free_size 与请求 size */6. 整体流程图
7. 关键点小结
- 只绑分页系统内存:
NonPaged/GTT/VRAM 不经过 NUMA 绑定。 - 目标节点靠拓扑反查:由 GPU 节点经 IO link(PCIe/XGMI,
Weight<=20)找到直连 CPU 节点,而非调用者指定。 - 强弱可调:
NoSubstitute决定MPOL_BIND(硬约束)还是MPOL_PREFERRED(软偏好);NoNUMABind整体关闭。 - 顺序保证:先
mbind再触发缺页 / 建 BO,让首次分配即落在目标节点。 - 健壮性:seccomp(
EPERM)与无内存可用时对非硬绑定路径静默放行,保证容器与紧张场景下仍能分配成功。
8. 相关源码位置
| 功能 | 文件 | 符号 |
|---|---|---|
| 挑选 GPU 直连 CPU 节点 | libhsakmt/src/topology.c | gpu_get_direct_link_cpu |
| 校验并返回 NUMA id | libhsakmt/src/topology.c | hsakmt_get_direct_link_cpu |
| NUMA 绑定核心 | libhsakmt/src/fmm.c | bind_mem_to_numa |
| 分页系统内存分配主流程 | libhsakmt/src/fmm.c | fmm_allocate_host_gpu |
| aperture 释放时复位 NUMA policy | libhsakmt/src/fmm.c | mmap_aperture_release/reserved_aperture_release |
🔗关联阅读
- NUMA-01 你的内存不是一整块:看懂 NUMA 与机器拓扑
- NUMA-02:一段内存到底在哪个 node:用户态 NUMA 编程接口
- linux VMA 的 NUMA 信息使用流程:从 vm_policy 到物理页落点