4. ROCm HIP hipMemAdvise 到 libksamkt中的 hsaKmtSVMSetAttr 调用链分析

1. 概述

本文档梳理 HIP/ROCclr 上层运行时如何把用户的内存建议(hipMemAdvise / hipMemAdvise_v2)一层层翻译成 ROCr 的 SVM 属性设置调用 hsa_amd_svm_attributes_set,再经 ROCr 核心Runtime::SetSvmAttrib 转换为 libhsakmt(thunk)的 hsaKmtSVMSetAttr,最终通过 AMDKFD_IOC_SVM ioctl 由 KFD 内核驱动完成 HMM/SVM range 的属性更新。

这条链路对应 CUDA 的 cudaMemAdvise 语义(read-mostly、preferred location、accessed-by 等),是 ROCm 统一虚拟内存(Unified/Managed Memory, HMM)迁移与访问控制的核心路径。

整条链路经历了 四次属性表示的翻译

层次 属性类型
HIP hipMemoryAdvise 枚举
ROCclr amd::MemoryAdvice 枚举
ROCr API hsa_amd_svm_attribute_pair_tHSA_AMD_SVM_ATTRIB_*
libhsakmt HSA_SVM_ATTRIBUTEHSA_SVM_ATTR_* / HSA_SVM_FLAG_*
KFD ioctl kfd_ioctl_svm_args / drm_amdgpu_svm_attributeKFD_IOCTL_SVM_ATTR_*

2. 调用链全景

#mermaid-svg-AQ3gm055a5loIVA5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AQ3gm055a5loIVA5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AQ3gm055a5loIVA5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AQ3gm055a5loIVA5 .error-icon{fill:#552222;}#mermaid-svg-AQ3gm055a5loIVA5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AQ3gm055a5loIVA5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AQ3gm055a5loIVA5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AQ3gm055a5loIVA5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AQ3gm055a5loIVA5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AQ3gm055a5loIVA5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AQ3gm055a5loIVA5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AQ3gm055a5loIVA5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AQ3gm055a5loIVA5 .marker.cross{stroke:#333333;}#mermaid-svg-AQ3gm055a5loIVA5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AQ3gm055a5loIVA5 p{margin:0;}#mermaid-svg-AQ3gm055a5loIVA5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AQ3gm055a5loIVA5 .cluster-label text{fill:#333;}#mermaid-svg-AQ3gm055a5loIVA5 .cluster-label span{color:#333;}#mermaid-svg-AQ3gm055a5loIVA5 .cluster-label span p{background-color:transparent;}#mermaid-svg-AQ3gm055a5loIVA5 .label text,#mermaid-svg-AQ3gm055a5loIVA5 span{fill:#333;color:#333;}#mermaid-svg-AQ3gm055a5loIVA5 .node rect,#mermaid-svg-AQ3gm055a5loIVA5 .node circle,#mermaid-svg-AQ3gm055a5loIVA5 .node ellipse,#mermaid-svg-AQ3gm055a5loIVA5 .node polygon,#mermaid-svg-AQ3gm055a5loIVA5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AQ3gm055a5loIVA5 .rough-node .label text,#mermaid-svg-AQ3gm055a5loIVA5 .node .label text,#mermaid-svg-AQ3gm055a5loIVA5 .image-shape .label,#mermaid-svg-AQ3gm055a5loIVA5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-AQ3gm055a5loIVA5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AQ3gm055a5loIVA5 .rough-node .label,#mermaid-svg-AQ3gm055a5loIVA5 .node .label,#mermaid-svg-AQ3gm055a5loIVA5 .image-shape .label,#mermaid-svg-AQ3gm055a5loIVA5 .icon-shape .label{text-align:center;}#mermaid-svg-AQ3gm055a5loIVA5 .node.clickable{cursor:pointer;}#mermaid-svg-AQ3gm055a5loIVA5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AQ3gm055a5loIVA5 .arrowheadPath{fill:#333333;}#mermaid-svg-AQ3gm055a5loIVA5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AQ3gm055a5loIVA5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AQ3gm055a5loIVA5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AQ3gm055a5loIVA5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AQ3gm055a5loIVA5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AQ3gm055a5loIVA5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AQ3gm055a5loIVA5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AQ3gm055a5loIVA5 .cluster text{fill:#333;}#mermaid-svg-AQ3gm055a5loIVA5 .cluster span{color:#333;}#mermaid-svg-AQ3gm055a5loIVA5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AQ3gm055a5loIVA5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AQ3gm055a5loIVA5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-AQ3gm055a5loIVA5 .icon-shape,#mermaid-svg-AQ3gm055a5loIVA5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AQ3gm055a5loIVA5 .icon-shape p,#mermaid-svg-AQ3gm055a5loIVA5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AQ3gm055a5loIVA5 .icon-shape rect,#mermaid-svg-AQ3gm055a5loIVA5 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AQ3gm055a5loIVA5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AQ3gm055a5loIVA5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AQ3gm055a5loIVA5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 应用: hipMemAdvise / hipMemAdvise_v2

(HIP Runtime API)
hip_hmm.cpp

解析 location + advice
amd::Device::SetSvmAttributes

(device.hpp 虚接口)
roc::Device::SetSvmAttributesInt

(rocdevice.cpp)

advice → HSA attribute 映射
Hsa::svm_attributes_set

(rocrctx.hpp 静态包装)
ROCR_DYN(hsa_amd_svm_attributes_set)

(rocrctx.cpp 动态符号)
hsa_amd_svm_attributes_set

(ROCr Runtime / hsa_ext_amd.cpp)
Runtime::SetSvmAttrib

(runtime.cpp)

HSA attrib → HSA_SVM_ATTR / flags
hsaKmtSVMSetAttr

(libhsakmt/src/svm.c)

node_id→gpu_id, 组装 ioctl 参数
hsakmt_ioctl(AMDKFD_IOC_SVM)

(内核 KFD 驱动设置 SVM range 属性)

3. 各层详解

3.1 HIP API 层:hip_hmm.cpp

用户调用 hipMemAdvise(dev_ptr, count, advice, device)(对应 CUDA cudaMemAdvise)。

HIP 层完成参数校验和 location 解析,然后转发到 ROCclr device 层。

关键逻辑(projects/clr/hipamd/src/hip_hmm.cpp):

cpp 复制代码
// 1. 解析 location(Device / Host / HostNuma / HostNumaCurrent)
int  targetDevice = hipCpuDeviceId;
bool use_cpu      = true;

switch (location.type) {
  case hipMemLocationTypeDevice:          // 迁移/偏好到某个 GPU
    targetDevice = location.id;
    use_cpu      = false;
    break;
  case hipMemLocationTypeHostNuma:        // 指定 NUMA 节点
    targetDevice = location.id;
    use_cpu      = true;
    break;
  // ... HostNumaCurrent / Host
}

// 2. 选择目标 amd::Device
amd::Device* dev = (use_cpu || isAdviseReadMostly)
                       ? g_devices[0]->devices()[0]
                       : g_devices[targetDevice]->devices()[0];

// 3. 转发到 device 层
if (!dev->SetSvmAttributes(dev_ptr, count,
                           static_cast<amd::MemoryAdvice>(advice),
                           use_cpu, targetDevice)) {
  return hipErrorInvalidValue;
}

枚举对齐hip_hmm.cpp 顶部用一组 static_assert 保证 HIP 的 hipMemAdvise* 枚举值与 ROCclr 内部 amd::MemoryAdvice 枚举值一一对应, 因此可以直接 static_cast 转换,无需查表映射。

cpp 复制代码
static_assert(static_cast<uint32_t>(hipMemAdviseSetReadMostly)
                  == amd::MemoryAdvice::SetReadMostly, "...");
// SetPreferredLocation / SetAccessedBy / SetCoarseGrain ... 同理

3.2 ROCclr 抽象层:amd::Device::SetSvmAttributes

device.hpp 中定义的纯虚接口,屏蔽后端差异(ROCm / PAL):

cpp 复制代码
// projects/clr/rocclr/device/device.hpp
virtual bool SetSvmAttributes(const void* dev_ptr, size_t count,
                              amd::MemoryAdvice advice,
                              bool use_cpu = false, int numa_id = 0) const = 0;

ROCm 后端实现(rocdevice.cpp)只是转调内部实现,first_alloc=false

cpp 复制代码
bool Device::SetSvmAttributes(const void* dev_ptr, size_t count,
                              amd::MemoryAdvice advice,
                              bool use_cpu, int numa_id) const {
  constexpr bool kFirstAlloc = false;
  return SetSvmAttributesInt(dev_ptr, count, advice, kFirstAlloc, use_cpu);
}

3.3 核心映射层:Device::SetSvmAttributesInt

这是整条链路的核心------把 amd::MemoryAdvice 翻译成hsa_amd_svm_attribute_pair_t 属性对(rocdevice.cpp)。

advice → HSA 属性映射表
ROCclr amd::MemoryAdvice HSA hsa_amd_svm_attribute_t value
SetReadMostly HSA_AMD_SVM_ATTRIB_READ_MOSTLY true
UnsetReadMostly HSA_AMD_SVM_ATTRIB_READ_MOSTLY false
SetPreferredLocation HSA_AMD_SVM_ATTRIB_PREFERRED_LOCATION CPU/GPU agent handle
UnsetPreferredLocation HSA_AMD_SVM_ATTRIB_PREFERRED_LOCATION 0(无偏好)
SetAccessedBy(首次分配) HSA_AMD_SVM_ATTRIB_AGENT_ACCESSIBLE agent handle
SetAccessedBy(后续更新) HSA_AMD_SVM_ATTRIB_AGENT_ACCESSIBLE_IN_PLACE agent handle
UnsetAccessedBy HSA_AMD_SVM_ATTRIB_AGENT_ACCESSIBLE agent handle
SetCoarseGrain HSA_AMD_SVM_ATTRIB_GLOBAL_FLAG COARSE_GRAINED
UnsetCoarseGrain HSA_AMD_SVM_ATTRIB_GLOBAL_FLAG FINE_GRAINED
关键代码
cpp 复制代码
bool Device::SetSvmAttributesInt(const void* dev_ptr, size_t count,
                                 amd::MemoryAdvice advice, bool first_alloc,
                                 bool use_cpu, int numa_id) const {
  // 仅在硬件支持 HMM 时才真正下发
  if (info().hmmSupported_) {
    std::vector<hsa_amd_svm_attribute_pair_t> attr;

    switch (advice) {
      case amd::MemoryAdvice::SetReadMostly:
        attr.push_back({HSA_AMD_SVM_ATTRIB_READ_MOSTLY, true});
        break;

      case amd::MemoryAdvice::SetPreferredLocation:
        if (use_cpu) {
          attr.push_back({HSA_AMD_SVM_ATTRIB_PREFERRED_LOCATION,
                          getCpuAgent(numa_id).handle});
        } else {
          attr.push_back({HSA_AMD_SVM_ATTRIB_PREFERRED_LOCATION,
                          getBackendDevice().handle});
        }
        break;

      case amd::MemoryAdvice::SetAccessedBy: {
        // 首次分配用 AGENT_ACCESSIBLE(允许 page fault 触发迁移)
        // 后续更新用 AGENT_ACCESSIBLE_IN_PLACE(原地访问,不迁移)
        const uint64_t attrib = (first_alloc)
            ? HSA_AMD_SVM_ATTRIB_AGENT_ACCESSIBLE
            : HSA_AMD_SVM_ATTRIB_AGENT_ACCESSIBLE_IN_PLACE;
        if (use_cpu) {
          attr.push_back({attrib, getCpuAgent().handle});
        } else if (first_alloc) {
          // 首次分配:为所有可访问 GPU 开放访问
          for (const auto dev : devices()) {
            if (static_cast<Device*>(dev)->getBackendDevice().handle != 0) {
              attr.push_back({attrib,
                  static_cast<Device*>(dev)->getBackendDevice().handle});
            }
          }
        } else {
          attr.push_back({attrib, getBackendDevice().handle});
        }
        break;
      }

      case amd::MemoryAdvice::SetCoarseGrain:
        attr.push_back({HSA_AMD_SVM_ATTRIB_GLOBAL_FLAG,
                        HSA_AMD_SVM_GLOBAL_FLAG_COARSE_GRAINED});
        break;
      // ... 其余 Unset* 分支
      default:
        return false;
    }

    // 下发到 ROCr
    hsa_status_t status = Hsa::svm_attributes_set(
        const_cast<void*>(dev_ptr), count, attr.data(), attr.size());
    if (status != HSA_STATUS_SUCCESS) {
      LogPrintfError("hsa_amd_svm_attributes_set() failed. "
                     "Advice: %d, status: %d", advice, status);
      return false;
    }
  } else {
    // 无 HMM 支持则静默忽略(不报错,保持 API 兼容)
    LogWarning("hsa_amd_svm_attributes_set() is ignored, because no HMM support");
  }
  return true;
}

3.4 动态符号包装层:Hsa::svm_attributes_set

ROCclr 不直接链接 ROCr,而是运行时 dlopen + dlsym 加载符号,避免硬依赖 libhsa-runtime64.so

静态包装(projects/clr/rocclr/device/rocm/rocrctx.hpp):

cpp 复制代码
static hsa_status_t svm_attributes_set(void* ptr, size_t size,
                                       hsa_amd_svm_attribute_pair_t* attribute_list,
                                       size_t attribute_count) {
  return ROCR_DYN(hsa_amd_svm_attributes_set)(
      ptr, size, attribute_list, attribute_count);
}

函数指针成员(rocrctx.hpp)与动态加载(rocrctx.cpp):

cpp 复制代码
// rocrctx.hpp: 保存函数指针
decltype(::hsa_amd_svm_attributes_set)* hsa_amd_svm_attributes_set_;

// rocrctx.cpp: 初始化时从 ROCr 动态解析符号
GET_ROCR_SYMBOL(hsa_amd_svm_attributes_set);

3.5 ROCr 运行时层:hsa_amd_svm_attributes_set

进入 ROCr 实现(runtime/hsa-runtime/core/runtime/hsa_ext_amd.cpp),它只是一层薄封装,实际逻辑转发到 Runtime::SetSvmAttrib

cpp 复制代码
hsa_status_t hsa_amd_svm_attributes_set(void* ptr, size_t size,
                                        hsa_amd_svm_attribute_pair_t* attribute_list,
                                        size_t attribute_count) {
  TRY;
  IS_OPEN();
  return core::Runtime::runtime_singleton_->SetSvmAttrib(
      ptr, size, attribute_list, attribute_count);
  CATCH;
}

API 表分发:hsa_amd_svm_attributes_set 通过 hsa_api_traceamd_ext_api.hsa_amd_svm_attributes_set_fn 注册,hsa_table_interface.cpp 中的同名符号最终调用到 AMD::hsa_amd_svm_attributes_set

3.6 ROCr 核心翻译层:Runtime::SetSvmAttrib

这是 ROCr 侧的核心(runtime/hsa-runtime/core/runtime/runtime.cpp):把 HSA 公共属性 HSA_AMD_SVM_ATTRIB_* 翻译成 libhsakmt 的 HSA_SVM_ATTRIBUTEHSA_SVM_ATTR_*)与 flag 位掩码(HSA_SVM_FLAG_*)。

HSA 属性 → thunk 属性/flag 映射表
HSA_AMD_SVM_ATTRIB_* 转换结果
GLOBAL_FLAG=FINE_GRAINED `set_flags
GLOBAL_FLAG=COARSE_GRAINED `clear_flags
READ_ONLY set/clear HSA_SVM_FLAG_GPU_RO
HIVE_LOCAL set/clear HSA_SVM_FLAG_HIVE_LOCAL
READ_MOSTLY set/clear HSA_SVM_FLAG_GPU_READ_MOSTLY
GPU_EXEC set/clear HSA_SVM_FLAG_GPU_EXEC
MIGRATION_GRANULARITY HSA_SVM_ATTR_GRANULARITY(上限 log2=18,即 1GB)
PREFERRED_LOCATION HSA_SVM_ATTR_PREFERRED_LOC(agent→node_id,null→INVALID_NODEID)
AGENT_ACCESSIBLE(GPU) HSA_SVM_ATTR_ACCESS(node_id)
AGENT_ACCESSIBLE(CPU) `set_flags
AGENT_ACCESSIBLE_IN_PLACE(GPU) HSA_SVM_ATTR_ACCESS_IN_PLACE(node_id)
AGENT_NO_ACCESS(GPU) HSA_SVM_ATTR_NO_ACCESS(node_id)
关键代码
cpp 复制代码
// 布尔类属性 → flag 位掩码
case HSA_AMD_SVM_ATTRIB_READ_MOSTLY:
  if (value) set_flags   |= HSA_SVM_FLAG_GPU_READ_MOSTLY;
  else       clear_flags |= HSA_SVM_FLAG_GPU_READ_MOSTLY;
  break;

// 位置/访问类属性 → HSA_SVM_ATTRIBUTE(agent handle 转 node_id)
case HSA_AMD_SVM_ATTRIB_PREFERRED_LOCATION: {
  Agent* agent = ConvertAllowNull(value);
  attribs.push_back(kmtPair(HSA_SVM_ATTR_PREFERRED_LOC,
                            agent ? agent->node_id() : INVALID_NODEID));
  break;
}
case HSA_AMD_SVM_ATTRIB_AGENT_ACCESSIBLE: {
  Agent* agent = Convert(value);
  if (agent->device_type() == Agent::kAmdCpuDevice)
    set_flags |= HSA_SVM_FLAG_HOST_ACCESS;        // CPU 走 flag
  else
    attribs.push_back(kmtPair(HSA_SVM_ATTR_ACCESS, agent->node_id()));  // GPU 走 attr
  break;
}

// 汇总 flag 更新
if (clear_flags) attribs.push_back(kmtPair(HSA_SVM_ATTR_CLR_FLAGS, clear_flags));
if (set_flags)   attribs.push_back(kmtPair(HSA_SVM_ATTR_SET_FLAGS, set_flags));

// 页对齐后下发到 thunk
uint8_t* base = AlignDown((uint8_t*)ptr, 4096);
uint8_t* end  = AlignUp((uint8_t*)ptr + size, 4096);
size_t   len  = end - base;
HSAKMT_STATUS error = HSAKMT_CALL(
    hsaKmtSVMSetAttr(base, len, attribs.size(), &attribs[0]));
if (error != HSAKMT_STATUS_SUCCESS)
  throw AMD::hsa_exception(HSA_STATUS_ERROR, "hsaKmtSVMSetAttr failed.");

关键转换点:

  • agent handle → node_id :ROCr 用 Agent::node_id() 把 HSA agent 句柄转换成KFD 拓扑节点 ID。
  • CPU vs GPU 分流 :CPU 访问用 HSA_SVM_FLAG_HOST_ACCESS 位;GPU 访问用HSA_SVM_ATTR_ACCESS[_IN_PLACE] 属性携带具体 node_id。
  • flag 合并 :所有布尔属性汇聚成 set_flags/clear_flags 两个位掩码, 分别打包成一个 HSA_SVM_ATTR_SET_FLAGS / HSA_SVM_ATTR_CLR_FLAGS 属性。
  • 页对齐ptr 向下、size 向上对齐到 4KB 页边界(KFD ioctl 强制要求)。

3.7 libhsakmt(thunk)层:hsaKmtSVMSetAttr

thunk 入口(libhsakmt/src/svm.c)转调带 context 的版本,最终落到 KFD ioctl:

c 复制代码
HSAKMT_STATUS HSAKMTAPI
hsaKmtSVMSetAttr(void *start_addr, HSAuint64 size, unsigned int nattr,
                 HSA_SVM_ATTRIBUTE *attrs) {
  return hsaKmtSVMSetAttrCtx(&hsakmt_primary_kfd_ctx, start_addr, size, nattr, attrs);
}

hsaKmtSVMSetAttrCtx 有两条编译路径:

(a) 传统 KFD ioctl 路径(默认):

c 复制代码
struct kfd_ioctl_svm_args *args;
args->start_addr = (uint64_t)start_addr;
args->size       = size;
args->op         = KFD_IOCTL_SVM_OP_SET_ATTR;
args->nattr      = nattr;
memcpy(args->attrs, attrs, sizeof(*attrs) * nattr);

// node_id → gpu_id(access / preferred_loc 类属性需要)
for (i = 0; i < nattr; i++) {
  if (attrs[i].type == KFD_IOCTL_SVM_ATTR_PREFERRED_LOC &&
      attrs[i].value == INVALID_NODEID) {
    args->attrs[i].value = KFD_IOCTL_SVM_LOCATION_UNDEFINED;
    continue;
  }
  hsakmt_validate_nodeid(ctx, attrs[i].value, &args->attrs[i].value);
}

// 变长 ioctl:attrs 大小编码进 ioctl number 高位
r = hsakmt_ioctl(ctx->fd,
                 AMDKFD_IOC_SVM + (s_attr << _IOC_SIZESHIFT), args);

(b) DRM render-node 路径USE_DRM_AMDGPU_SVM,virtio/passthrough 场景):

hsaKmtSVMSetAttrCtx_drmHSA_SVM_ATTRIBUTE 翻译成 struct drm_amdgpu_svm_attributeAMDGPU_SVM_ATTR_*),再调用

amdgpu_svm_set_attr(deviceHandle, ...)

注意 thunk 层又做了一次 node_id → gpu_id 的转换:ROCr 传下来的是 KFD 拓扑 node id,而 ioctl 需要的是 GPU id,由 hsakmt_validate_nodeid 完成。

3.8 KFD 内核层:AMDKFD_IOC_SVM

hsakmt_ioctl 通过 ioctl(fd, AMDKFD_IOC_SVM, args) 陷入内核。KFD 驱动(kfd_ioctl_svmsvm_ioctlsvm_range_set_attr)根据 op 与属性列表,定位/切分对应的 svm_range,更新其 preferred_loc、access 位图、flags、granularity 等,并按需触发 HMM 迁移或页表更新。

4. 关键设计要点

4.1 HMM 能力检查(优雅降级)

只有 info().hmmSupported_ 为真时才真正调用 ROCr,否则打印 warning 并返回成功。这保证了在不支持 HMM/XNACK 的旧硬件上,hipMemAdvise 不会失败,只是变成 no-op。

4.2 首次分配 vs 后续更新的语义区别

SetAccessedBy 根据 first_alloc 选择不同 HSA 属性:

  • 首次分配first_alloc=true)→ HSA_AMD_SVM_ATTRIB_AGENT_ACCESSIBLE
    允许 page fault 触发按需迁移,并为所有可访问 GPU 开放访问。
  • 后续更新first_alloc=false)→ HSA_AMD_SVM_ATTRIB_AGENT_ACCESSIBLE_IN_PLACE
    原地访问、不触发迁移,仅针对当前 device。

注释指出:理论上 XNACK 开启时 HMM 应能自动更新页表,但当前实现仍显式为所有 device 开放访问以规避该限制。

4.3 CPU / NUMA 目标处理

use_cpu 为真时目标 agent 取 getCpuAgent(numa_id),支持把内存的 preferred location 或 accessed-by 指向指定 NUMA 节点的 CPU;否则取 getBackendDevice()(GPU agent)。

4.4 动态符号解耦

ROCR_DYN + GET_ROCR_SYMBOL 机制让 ROCclr 与 ROCr 版本解耦,新旧 ROCr 之间只要符号存在即可工作,缺失符号时可优雅处理。

5. 相关源码位置速查

层次 文件 关键符号
HIP API projects/clr/hipamd/src/hip_hmm.cpp hipMemAdvise,枚举 static_assert
抽象接口 projects/clr/rocclr/device/device.hpp amd::Device::SetSvmAttributesenum MemoryAdvice
ROCm 实现 projects/clr/rocclr/device/rocm/rocdevice.cpp SetSvmAttributesInt(advice→HSA 映射)
动态包装 projects/clr/rocclr/device/rocm/rocrctx.hpp Hsa::svm_attributes_set
符号加载 projects/clr/rocclr/device/rocm/rocrctx.cpp GET_ROCR_SYMBOL(hsa_amd_svm_attributes_set)
ROCr 接口 runtime/hsa-runtime/inc/hsa_ext_amd.h hsa_amd_svm_attributes_set 声明
ROCr 薄封装 runtime/hsa-runtime/core/runtime/hsa_ext_amd.cpp hsa_amd_svm_attributes_set(转 SetSvmAttrib
ROCr 核心翻译 runtime/hsa-runtime/core/runtime/runtime.cpp Runtime::SetSvmAttrib(HSA→thunk 映射、node_id 转换)
thunk 入口 libhsakmt/src/svm.c hsaKmtSVMSetAttr / hsaKmtSVMSetAttrCtx
thunk DRM 路径 libhsakmt/src/svm.c hsaKmtSVMSetAttrCtx_drmamdgpu_svm_set_attr
thunk 接口 libhsakmt/include/hsakmt/hsakmt.h hsaKmtSVMSetAttr 声明
KFD ioctl 内核 kfd_chardev.c AMDKFD_IOC_SVMsvm_ioctlsvm_range_set_attr

6. 小结

hipMemAdvise 的本质是把用户对托管内存的访问/迁移建议,逐层翻译、逐层下沉:

复制代码
hipMemoryAdvise → amd::MemoryAdvice → HSA_AMD_SVM_ATTRIB_*
  → HSA_SVM_ATTR_* / HSA_SVM_FLAG_* → kfd_ioctl_svm_args → KFD svm_range

整条链路有两个核心翻译点:

  • ROCclr 侧 rocdevice.cpp::SetSvmAttributesInt:advice→HSA 属性映射,配合 info().hmmSupported_ 实现跨硬件优雅降级,配合 first_alloc 区分首次分配与运行时调整的迁移语义。
  • ROCr 侧 runtime.cpp::SetSvmAttrib:HSA 属性→thunk 属性/flag 映射,完成 agent handle→node_id 转换、CPU/GPU 访问分流、flag 位掩码合并、页对齐。

再经 libhsakmt hsaKmtSVMSetAttr(node_id→gpu_id)打包成变长 AMDKFD_IOC_SVM ioctl,最终由 KFD 更新对应 svm_range 的属性并按需触发 HMM 迁移/页表更新。


7. 设计讨论:SVM Range 是进程级资源

在调试时,你会发现在hip层是通过device调用的,但到rocr层,device信息丢了。但与SVM 的底层设计有关:

SVM Range 是进程级的资源,不是绑定于特定设备的内存。

对比

特性 传统设备内存 (hipMalloc) SVM Range (hipMallocManaged)
归属 绑定特定 GPU 进程级资源
标识 device memory handle 虚拟地址 (ptr, size)
设备角色 内存拥有者 内存访问者

API 设计印证

c 复制代码
hsa_amd_svm_attributes_set(
    void* ptr,        // 虚拟地址标识 range(主参数)
    size_t size,
    attrs,            // 设备信息在属性值中,不是 API 主参数
    attr_count
);

设备信息从"调用者身份"转换 为"属性值"(如 PREFERRED_LOCATION 的 value),因为设备是这段内存的访问者 ,不是拥有者

这与内核 HMM 设计一致:VMA 属于进程,设备只是注册为可以访问它的实体。

初次调试这个调用栈时,会有些疑惑,感觉上下不对称。不知道大家有没有这个感觉。