在 libhsakmt 和 rocr-runtime 专栏里我们上来都会讲HSA系统的 topology 发现因为这是HSA 系统初始化的最重要部分。这里我们也会用这个topology概念来分析hip层的初始化。这样能让人明白上下层的技术串联。1. 核心结论HIP topology 发现是ROCr 暴露 agent topologyROCclr 负责枚举和建模HIP 最后消费 ROCclr 的 device registry。整体链路是ROCr/HSA-hsa_iterate_agents 得到 CPU/GPU agent-ROCclr ROCm 后端 roc::Device 包装 GPU agent-ROCclr amd::Device registry 保存统一 device 视图-HIP hip::g_devices 建立 HIP runtime 的 device ordinal分层职责如下层级负责什么ROCr/HSA枚举系统中的 CPU/GPU agent、memory pool、ISA、link/P2P 信息ROCclr ROCm 后端把 GPUhsa_agent_t包装成roc::DeviceROCclr core把后端 device 注册成统一的amd::DeviceHIP runtime基于amd::Device创建hip::Device形成 HIP API 看到的 device 列表所以本节重点关注的是agent 是在哪里发现的、如何被过滤、如何变成 ROCclr device、最后如何进入 HIP 的 device 表。2. 初始化入口HIP 的 topology 发现是在第一次进入 HIP API 时触发 runtime 初始化。先给出完整调用栈后面再逐层拆开任意首次 HIP Runtime API-HIP_INIT_API/HIP_INIT_API_R-std::call_once(hip::g_ihipInitialized,hip::init,status)-hip::init(bool*status)-amd::Runtime::init()-amd::Device::init()-roc::Device::init()# Linux ROCm/HSA 路径-Hsa::LoadLib()-Hsa::init()-Hsa::system_get_major_extension_table(...)-Hsa::iterate_agents(roc::Device::iterateAgentCallback,nullptr)-HSA_AGENT_INFO_DEVICE 区分 CPU/GPU agent-CPU agent:iterateCpuMemoryPoolCallback-cpu_agents_-GPU agent:gpu_agents_.push_back(agent)-HIP_VISIBLE_DEVICES/CUDA_VISIBLE_DEVICES 过滤 gpu_agents_-foreach gpu agent:newroc::Device(agent)-roc::Device::create()-查询 ISA/BDF/domain/memory pools/P2P 等 topology 信息-setupCpuAgent()选择 NUMA 距离合适的 CPU agent-context_newamd::Context({this},info)-roc::Device::registerDevice()-建立 ROCclr device 间 P2P 关系-amd::Device::getDevices(CL_DEVICE_TYPE_GPU,false)-foreach amd::Device:newhip::Device(amd_device-context(),ordinal)-hip::Device::Create()创建 HIP runtime mempool 等对象-hip::g_devices.push_back(device)这条栈的关键分界是ROCr/HSA 发现 agentROCclr 建模和注册 deviceHIP 最后创建自己的hip::Device表。典型入口是HIP_INIT_API第一次进入 HIP API-HIP_INIT_API-std::call_once(hip::g_ihipInitialized,hip::init,status)hip::init()的关键逻辑在projects/clr/hipamd/src/hip_context.cpp。核心流程voidinit(bool*status){amd::IS_HIPtrue;GPU_NUM_MEM_DEPENDENCY0;if(!amd::Runtime::init()){*statusfalse;return;}conststd::vectoramd::Device*devicesamd::Device::getDevices(CL_DEVICE_TYPE_GPU,false);for(size_t i0;idevices.size();i){amd::Device*constamd_devicedevices[i];auto*devicenewDevice(amd_device-context(),static_castunsignedint(i));device-Create();g_devices.push_back(device);}}这里有两个重点hip::init()先调用amd::Runtime::init()真正的底层设备枚举发生在这个调用内部。hip::init()之后通过amd::Device::getDevices(CL_DEVICE_TYPE_GPU, false)取出 ROCclr 已经注册好的 GPU device再为每个amd::Device创建一个hip::Device。这里的CL_DEVICE_TYPE_GPU很容易让人误解。它不是说 HIP 在这里调用 OpenCL API 做设备发现而是因为 ROCclr 原本就是 HIP 和 OpenCL 共用的 runtime 基础层内部设备分类沿用了 OpenCL 的cl_device_type类型系统typedefcl_bitfield cl_device_type;#defineCL_DEVICE_TYPE_GPU(12)ROCclr 的amd::Device会把 GPU 设备标记成info_.type_CL_DEVICE_TYPE_GPU;而amd::Device::getDevices(type, offlineDevices)只是按这个 bitmask 过滤 ROCclr device registryboolDevice::IsTypeMatching(cl_device_type type,boolofflineDevices){return(info_.type_type)!0;}所以这里的CL_前缀是 ROCclr 历史和共享抽象留下来的命名并不代表 HIP topology 发现依赖 OpenCL runtime API。HIP 在这里想表达的语义只是从 ROCclr 已注册的设备里取 GPU 类型的设备。3. ROCclr Runtime 如何触发设备发现amd::Runtime::init()在 ROCclr 层关键路径是projects/clr/rocclr/platform/runtime.cpp。它会调用Device::init()也就是 ROCclr 的全局设备初始化入口。进一步进入projects/clr/rocclr/device/device.cpp这里会选择后端amd::Device::init() - roc::Device::init() # ROCm/HSA 后端 - pal::Device::init() # PAL 后端按平台/配置选择在 Linux ROCm 路径下重点是roc::Device::init()。4. ROCr Agent 枚举发生在哪里ROCr agent 枚举发生在projects/clr/rocclr/device/rocm/rocdevice.cpp。核心流程roc::Device::init()-Hsa::LoadLib()-Hsa::init()-Hsa::iterate_agents(iterateAgentCallback,nullptr)iterateAgentCallback会查询每个 agent 的类型hsa_status_tDevice::iterateAgentCallback(hsa_agent_t agent,void*data){hsa_device_type_t dev_typeHSA_DEVICE_TYPE_CPU;Hsa::agent_get_info(agent,HSA_AGENT_INFO_DEVICE,dev_type);if(dev_typeHSA_DEVICE_TYPE_CPU){AgentInfo info{agent,{0},{0},{0}};Hsa::agent_iterate_memory_pools(agent,Device::iterateCpuMemoryPoolCallback,info);cpu_agents_.push_back(info);}elseif(dev_typeHSA_DEVICE_TYPE_GPU){gpu_agents_.push_back(agent);}}也就是说ROCr/HSA 暴露的是一个 agent 列表ROCclr 把它拆成cpu_agents_ # CPU agentfine/coarse/kernarg memory pool 信息 gpu_agents_ # GPU agent 列表5. 可见设备过滤枚举出所有 GPU agent 后ROCclr 会根据环境变量过滤 GPU 列表。HIP 模式下优先使用HIP_VISIBLE_DEVICES如果它为空则使用CUDA_VISIBLE_DEVICES非 HIP 模式则使用GPU_DEVICE_ORDINAL代码逻辑大致是std::string ordinalsamd::IS_HIP?((HIP_VISIBLE_DEVICES[0]!\0)?HIP_VISIBLE_DEVICES:CUDA_VISIBLE_DEVICES):GPU_DEVICE_ORDINAL;过滤支持两类输入数字 ordinal例如0,2UUID 风格字符串例如GPU-...过滤完成后gpu_agents_valid_agents;这一步决定了后续 HIP 看到的 device ordinal。也就是说HIP 的device 0并不一定是系统物理 GPU 0而是过滤后的gpu_agents_[0]。6. 从 GPU Agent 创建 ROCclr Device过滤完成后ROCclr 为每个 GPU agent 创建一个roc::Devicefor(autoagent:gpu_agents_){std::unique_ptrDeviceroc_device(newDevice(agent));roc_device-create();roc_device.release()-registerDevice();}roc::Device直接保存底层 agenthsa_agent_tgetBackendDevice()const{returnbkendDevice_;}它在create()阶段会进一步查询和建立大量设备信息包括HSA_AGENT_INFO_NAME HSA_AMD_AGENT_INFO_CHIP_ID HSA_AGENT_INFO_PROFILE HSA_AMD_AGENT_INFO_BDFID HSA_AMD_AGENT_INFO_DOMAIN agent ISA GPU memory pools P2P/link 信息 HDP flush 信息 cooperative queue 信息这些信息最终填充到 ROCclr 的amd::Device::info()、后端 memory pool、P2P 列表等结构里。7. CPU Agent 和 NUMA 选择ROCr topology 不只有 GPU agent也包括 CPU agent。CPU agent 被保存到roc::Device::cpu_agents_每个 CPU agent 同时记录它的 memory poolsstructAgentInfo{hsa_agent_t agent;hsa_amd_memory_pool_t fine_grain_pool;hsa_amd_memory_pool_t coarse_grain_pool;hsa_amd_memory_pool_t kern_arg_pool;hsa_amd_memory_pool_t ext_fine_grain_pool;};每个roc::Device会选择一个与当前 GPU NUMA 距离更合适的 CPU agentroc::Device::setupCpuAgent() - 遍历 cpu_agents_ - 根据 link / NUMA 信息选择 CPU agent - cpu_agent_info_ 指向选中的 AgentInfo这对 HMM/SVM、fine-grain system memory、kernarg allocation、CPU/GPU 访问路径都很重要。8. P2P Topology 建立所有roc::Device注册完成后ROCclr 会重新取 active GPU devicesdevicesgetDevices(CL_DEVICE_TYPE_GPU,false);然后建立 P2P 关系for(autodevice1:devices){for(autoagent:static_castDevice*(device1)-p2pAgents()){for(autodevice2:devices){if(agent.handlestatic_castDevice*(device2)-getBackendDevice().handle){device2-p2pDevices_.push_back(as_cl(device1));device1-p2p_access_devices_.push_back(device2);}}}}这里的核心是把 ROCr/HSA 层发现的 agent 互访能力转换成 ROCclr device 之间的 P2P 关系。后续hipDeviceCanAccessPeer、peer copy、peer access enable/disable 等 HIP API 都会依赖这些底层信息。9.amd::Context的作用当 ROCclr 完成 agent 枚举、过滤、roc::Device创建和注册后控制流回到hip::init()。这一段最容易绕是因为中间多了一个 ROCclr contextHIP 创建hip::Device时不是直接拿hsa_agent_t也不是直接保存roc::Device*而是拿amd_device-context()。先把三个概念分开hsa_agent_t / roc::Device 表示具体 GPU agent / 后端设备 amd::Context 表示一组 ROCclr 资源归属到哪些 device hip::Device 表示 HIP runtime 看到的 device ordinal 和 CUDA-like 语义也就是说roc::Device更偏“设备本体”amd::Context更偏“资源归属域”hip::Device更偏“HIP API 语义包装”。先看 ROCclr 侧。在roc::Device::create()里每个roc::Device会为自己创建一个只包含自身的 ROCclr contextamd::Context::Info info{0};std::vectoramd::Device*devices{this};// Create a dummy contextcontext_newamd::Context(devices,info);这意味着每个已经注册的 ROCclr GPU device 都带着一个 contextroc::Device - amd::Device::context_ - amd::Context(devices { this })这里的this就是当前这个roc::Device。所以在 HIP 的 device 初始化路径里每个roc::Device自带的 context 都是单 device contextroc::Device A - amd::Context(devices { roc::Device A }) roc::Device B - amd::Context(devices { roc::Device B }) roc::Device C - amd::Context(devices { roc::Device C })为什么不让 HIP 直接保存roc::Device*因为在 ROCclr 的设计里很多 runtime 资源不是直接挂在裸 agent 上而是挂在 context 上。device 只表示“能执行的设备”context 表示“一组资源归属到哪些设备”。amd::Context至少提供了几类能力能力说明设备集合devices_保存这个 context 关联的amd::Device*列表生命周期管理构造时device-retain()析构时device-release()SVM 设备集合收集支持 SVM 的设备到svmAllocDevice_内存分配域hostAlloc、svmAlloc、svmFree都以 context 为归属域队列/命令归属hip::Stream继承amd::HostQueue队列需要绑定 context图形互操作状态GL/D3D/EGL 等 interop 信息保存在Context::Info/glenv_中因此amd::Context在这里不是为了发现 topology而是为了把发现到的 device 变成一个可以承载 memory、queue、program、SVM、interop 等 runtime 资源的 ROCclr 执行域。topology 已经由 ROCr/ROCclr device 初始化完成context 解决的是后续资源应该归属于哪个 device 集合的问题。这也是为什么 HIP 后面不是保存roc::Device*本身而是保存amd::Context*context_;然后通过context_-devices()再回到具体的amd::Device/roc::Device。需要注意amd::Context这个类型本身并不要求只能包含一个 device。它内部保存的是std::vectorDevice*devices_;所以它天然可以表达 multi-device context。只是在roc::Device::create()这条路径里ROCclr 给每个 GPU device 创建的是一个只包含自身的 contextHIP 后续创建hip::Device时用的就是这个 per-device context。另外hip::init()末尾还会创建一个不同的host_contexthost_contextnewamd::Context(devices,amd::Context::Info());这里传入的devices是所有 ROCclr GPU device 列表。它和每个roc::Device自带的 per-device context 不是同一个对象context创建位置包含哪些 device主要用途per-device contextroc::Device::create(){ this roc::Device }给单个hip::Device、stream、memory pool 等 HIP device 资源使用host_contexthip::init()所有可见 GPU device给 host/SVM/system memory 等跨设备或默认选择路径使用所以这一层可以理解成ROCr/HSA 发现 agent - roc::Device 保存真实 hsa_agent_t - amd::Context 给这个 device 建立资源归属域 - hip::Device 保存这个 context形成 HIP API 看到的 device这里的实现我只能说复杂的一批。估计有原因是为了兼容 CUDA 的概念。不过既然现在是这么设计的那就给我们一个说服自己的理由下面是自我安慰的理由第一roc::Device 只适合表达“设备是什么”不适合表达“资源属于谁”。比如 memory、stream、program、kernel、SVM allocation、interop 状态这些东西都需要一个统一的归属域。这个归属域就是 amd::Context。如果 HIP 直接拿 roc::Device*后面每个资源对象都还要重新回答“这个资源绑定哪些 device、生命周期怎么管理、SVM 选择哪个 device、interop 状态挂在哪里”等问题。第二amd::Context 是 ROCclr 复用 OpenCL 模型留下来的核心抽象。OpenCL 天然有 context而且 context 可以包含多个 device。ROCclr 是 HIP 和 OpenCL 共用的 runtime 基础层所以它不会为了 HIP 单独绕开 context。HIP 虽然表面上是 CUDA-like current device 模型但底下还是复用 ROCclr 的 context/memory/queue/program 抽象。第三per-device context 让 HIP 的 CUDA-like device 语义更容易落到 ROCclr 上。HIP 的 hip::Device 通常对应一个 device ordinal而 ROCclr 的资源创建又需要 context。10. HIP Device 的创建理解了amd::Context之后再看 HIP device 的创建就顺了。roc_device-registerDevice()会把roc::Device放进 ROCclr 全局 device registry。HIP 这时不会再直接枚举 HSA agent而是读取 ROCclr registryconststd::vectoramd::Device*devicesamd::Device::getDevices(CL_DEVICE_TYPE_GPU,false);这里的devices[i]是amd::Device*在 ROCm 后端下实际对象是roc::Device*。然后 HIP 会遍历这个 ROCclr GPU device 列表每循环到一个roc::Device就创建一个对应的hip::Device包装。for(size_t i0;idevices.size();i){amd::Device*constamd_devicedevices[i];amd_device-SetActiveWait(true);auto*devicenewDevice(amd_device-context(),static_castunsignedint(i));if(!device||!device-Create()){*statusfalse;return;}g_devices.push_back(device);}这个循环可以理解成devices[0] roc::Device* - new hip::Device(..., 0) - hip::g_devices[0] devices[1] roc::Device* - new hip::Device(..., 1) - hip::g_devices[1] devices[2] roc::Device* - new hip::Device(..., 2) - hip::g_devices[2]这里的关键点是newhip::Device(amd_device-context(),i)随后device-Create()创建的是 HIP runtime 自己需要的对象例如default_mem_pool_ graph_mem_pool_ default_managed_mem_pool_ current_mem_pool_ current_managed_mem_pool_所以HIP runtime 的全局 device 表是hip::g_devices:std::vectorhip::Device*它的 index 就是 HIP API 看到的 device ordinal。这个 ordinal 的来源是过滤后的 gpu_agents_ 顺序 - roc::Device 注册顺序 - amd::Device::getDevices() 返回顺序 - hip::g_devices 下标准确说hip::Device间接对应ROCr agent但不是直接封装 agent 的对象。更准确的对象关系是hip::Device-amd::Context-amd::Device-roc::Device-hsa_agent_t bkendDevice_也就是说层级对象角色HIP runtimehip::DeviceCUDA/HIP runtime 语义里的 device/context 包装ROCclramd::Device后端无关的 device 抽象ROCclr ROCm 后端roc::DeviceROCm/HSA 后端 device直接持有hsa_agent_tROCr runtimehsa_agent_t系统中的真实 CPU/GPU agentlibhsakmtnodeid系统中发现的计算节点直接持有hsa_agent_t的是roc::Devicehip::Device是基于 ROCclr device/context 再包出来的 HIP runtime 对象。11. 调试建议如果想调试 HIP device/topology 发现优先看这些断点hip::init amd::Runtime::init amd::Device::init roc::Device::init roc::Device::iterateAgentCallback roc::Device::create amd::Device::registerDevice amd::Device::getDevices如果想确认“HIP device N 对应哪个 HSA agent”可以沿这个路径看hip::g_devices[N]-hip::Device::asContext()-context-devices()[0]-static_castroc::Device*(amd_device)-getBackendDevice()-hsa_agent_t.handle如果想确认过滤是否生效重点检查HIP_VISIBLE_DEVICES CUDA_VISIBLE_DEVICES GPU_DEVICE_ORDINAL roc::Device::gpu_agents_ hip::g_devices.size()12. 总结HIP topology 发现的设计可以概括为ROCr 负责暴露真实 agent topologyCPU/GPU agent、memory pool、ISA、link/P2P 等信息。ROCclr ROCm 后端负责枚举和包装 agentroc::Device直接持有hsa_agent_t。ROCclr core 负责注册统一 device 抽象amd::Device是 HIP/OpenCL 共用的后端无关设备视图。HIP 负责创建 CUDA runtime 语义的 device 表hip::g_devices是用户通过 HIP API 看到的设备集合。因此HIP 层的 topology 发现不是“HIP 直接扫 ROCr agent”而是ROCr agent topology-ROCclr roc::Device-ROCclr amd::Device registry-HIP hip::Device说实话还是有些绕的不同的库、不同命名空间、不同的概念表达。不过按照我们提供的概念和顺序应该还是可以很好理解的。建议先去理解libhsakmt层的概念和实现那里相对单纯些。ROCm rocr-libhsakmt实现深度剖析建议先理解这个库的实现和概念。