libhsakmt 内存分配中的 NUMA 使用流程与实现
2026/8/4 11:13:30 网站建设 项目流程

本文分析 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_gpusrc/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);
NoSubstitutepolicy语义
置位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. 整体流程图

否, 分页

hsaKmtAllocMemory
(系统内存, HostAccess)

NonPaged?
(pinned)

走 KFD 直接分配
不做 NUMA 绑定

mmap 匿名页, 拿到 VA

hsakmt_get_direct_link_cpu
GPU node → 直连 CPU node

bind_mem_to_numa

NoNUMABind /
无 NUMA?

NoSubstitute?

返回 -EFAULT

放行(默认节点)

NoSubstitute?

mbind MPOL_BIND
(硬约束)

mbind MPOL_PREFERRED
(软偏好)

madvise + 建 userptr BO


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.cgpu_get_direct_link_cpu
校验并返回 NUMA idlibhsakmt/src/topology.chsakmt_get_direct_link_cpu
NUMA 绑定核心libhsakmt/src/fmm.cbind_mem_to_numa
分页系统内存分配主流程libhsakmt/src/fmm.cfmm_allocate_host_gpu
aperture 释放时复位 NUMA policylibhsakmt/src/fmm.cmmap_aperture_release/reserved_aperture_release

🔗关联阅读

  • NUMA-01 你的内存不是一整块:看懂 NUMA 与机器拓扑
  • NUMA-02:一段内存到底在哪个 node:用户态 NUMA 编程接口
  • linux VMA 的 NUMA 信息使用流程:从 vm_policy 到物理页落点

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询