资讯动态

6.2 ASIC 无关抽象:amd_ip_blocks

发布时间:2026/8/22 12:19:24 来源:尧图企业网站定制
上一篇6.1提到lib/amdgpu要回答的第二个难题寄存器布局、dispatch 细节、compute pipeline 编程方式全都随 ASICGFX9/10/11/12变化。如果每个测试都写死某代硬件的魔数加一款新芯片就得改一大片。这一篇把amd_ip_blocks.{c,h}amd_ip_blocks_ex.c的扩展机制讲清楚它长什么样、初始化时发生了什么、测试代码为什么只需要一句get_ip_block()就能拿到对的实现。一、核心数据结构整套抽象有三层结构体理解它们的包含关系是理解一切的前提。ip_blocks[] 数组funcsamdgpu_ips: amdgpu_ip_blocks_device全局单例最多 AMD_IP_MAX 个槽amdgpu_ip_block_versiontype / major / minor / rev / *funcsamdgpu_ip_funcsfamily_id 一大张函数指针表struct amdgpu_ip_blocks_device一个进程级全局单例amdgpu_ips内部是ip_blocks[AMD_IP_MAX]数组 num_ip_blocks计数。它记录「当前注册了哪些引擎」但要强调这是一份全局单例、单设备语义的账本不是 per-device 状态。同一进程里对多台设备各调一次setup_amdgpu_ip_blocks会把块重复追加进这同一个数组、并让共享 funcs 的硬件身份互相覆盖结构体旁/* we may improve later */就是在说这个限制。多 GPU 测试因此走igt_multi_fork_foreach_gpufork 子进程让每个进程各持一份副本来回避冲突。struct amdgpu_ip_block_version描述一个引擎的一个版本字段是typeGFX / COMPUTE / DMA…、major/minor/rev和一个指向操作表的funcs。struct amdgpu_ip_funcs真正干活的函数指针表。前几个字段是运行期填入的硬件身份family_id、chip_external_rev、chip_rev后面是一整排回调write_linear、copy_linear、const_fill、get_reg_offset、gfx_program_compute、gfx_dispatch_direct以及一堆 hang/reset 测试专用的钩子。type用的枚举是enum amd_ip_block_typeenumamd_ip_block_type{AMD_IP_GFX0,AMD_IP_COMPUTE,AMD_IP_DMA,AMD_IP_UVD,AMD_IP_VCE,AMD_IP_UVD_ENC,AMD_IP_VCN_DEC,AMD_IP_VCN_ENC,AMD_IP_VCN_UNIFIEDAMD_IP_VCN_ENC,AMD_IP_VCN_JPEG,AMD_IP_VPE,AMD_IP_MAX,};二、amdgpu_ip_funcs硬件原语回调表amdgpu_ip_funcs是整套抽象里真正「干活」的一层——它不描述「是什么硬件」而是回答「在这块引擎上某个操作该发什么 PM4 包」。字段分两类开头几个标量是运行期身份和常量后面一大排是按用途分组的函数指针。先看变量setup时回填或静态给定变量作用family_id/chip_external_rev/chip_rev硬件身份family_id是amd_ip_blocks_ex_init分流的唯一依据见 §五align_maskring / buffer 的对齐掩码nop本引擎的 NOP 包字GFX 与 SDMA 不同deadbeaf/pattern测试用的哨兵值与填充模式——写进去再读回来比对回调按业务分四组。1. PM4 原语组一次内存操作 一段命令流这组签名都是(func, ring_context, uint32_t *pm4_dw)——把要发的 PM4 dword 追加进ring_context的命令缓冲并回填本次写了多少 dword。它们是所有 ring 测试的地基回调作用write_linear往一段线性地址写一串值——最基础的「提交能跑通、写得对」验证bad_write_linear故意发一个畸形/越权的 write多带cmd_error参数用于错误注入 / 负路径测试write_linear_atomic原子写变体验证 atomic 包const_fill按常量填一段内存copy_linear内存拷贝compare/compare_patternCPU 侧把结果读回来跟期望值 / pattern 比对不是发包是校验 helperwait_reg_mem发WAIT_REG_MEM轮询某地址直到满足条件——引擎内同步原语get_reg_offset语义枚举 → 真实寄存器偏移详见 §六write / copy / fill / compare / wait 就是测试需要的最小操作集而同一个操作在 GFX ring 和 SDMA ring 上 PM4 编码完全不同。抽成回调后同一个测试只要换ip_blockGFX ↔ DMA就能在两条引擎上各跑一遍测试体一行不用改。2. 用户队列生命周期组回调作用userq_create/userq_submit/userq_destroy用户态队列user-mode queue的建 / 提 / 销业务动机内核同时支持「传统 CS ioctl 提交」和「用户态队列直提」两套模型测试要覆盖两条路径。把队列生命周期抽成回调上层提交逻辑就能在两种模型间复用。3. compute 流水线组启动一个 shader回调作用gfx_program_compute编排最小 compute 流水线写PGM_LO、RSRC1/2、USER_DATA、NUM_THREAD等 SH 寄存器为一次裸 PM4 的 shader 启动做准备gfx_dispatch_direct发DISPATCH_DIRECT带 grid 尺寸 代次相关的 initiator flags就是 §五 那个 gfx90x0/ 其余0x45的坑gfx_write_confirm发带WR_CONFIRM的WRITE_DATA——等写真正落地常用来给 shader 完成打标记gfx_emit_nops往 ring 里补count个 NOP对齐 / 占位gfx_write_data_mem更通用的WRITE_DATA可指定engine_sel和是否WR_CONFIRM业务动机要在测试里真跑一段 compute shader就得「配流水线 dispatch」而这套编码随代次变化最剧烈——所以这组正是amd_ip_blocks_ex_init里按family_id替换的重点对象§五。4. hang/reset 钩子组定向制造故障考验驱动恢复回调制造的故障 / 恢复路径wait_reg_mem_hang轮询一个永远不会到来的值 → 队列「干净地」挂住用于 per-queue reset 测试priv_fault_hang先发非法 opcode 触发CP_BAD_OPCODE_ERROR再挂住 → 驱动走 per-queue reset 恢复priv_inst_hang从非特权用户队列发特权INDIRECT_BUFFERPRIV 位→ 触发特权指令故障priv_fault_badcount_hang发一个 count 字段错误、又没有包体的坏包 → 流水线找不到包尾中途卡死vmid / per-queue reset 救不回来只能靠 gfx pipe reset业务动机GPU reset / 恢复测试需要确定性地制造出不同「类别」的故障每一类走的恢复路径不同per-queue reset vs pipe reset。把「如何制造某类 hang」抽成回调既能按代次给出正确的坏包又让 reset 测试主体保持通用。5. 回调共享的载体amdgpu_ring_context上面 PM4 原语组的签名都是(func, ring_context, uint32_t *pm4_dw)——回调本身无状态所以「这次提交的具体参数」必须由一个上下文对象携带。amdgpu_ring_context就是测试主体与回调之间的**共享参数袋 **测试填好 buffer 和传输长度回调把 PM4 写进去并回填产出的 dword 数再由amdgpu_test_exec_cs_helper()提交。字段按用途分几组分组代表字段作用传输参数write_length/write_length2、secure、priority本次要写/拷多少字节、是否安全提交、队列优先级PM4 缓冲pm4/pm4_size/pm4_dw及*2回调把包写进pm4用pm4_dw回填实际 dword 数out 参数约定Buffer 句柄与地址bo…bo4、bo_mc…bo_mc4、bo_cpu…、resources[]、va_handle*参与本次操作的 BO、其 GPU 地址与 CPU 映射resources[]供 bo_list 提交提交上下文context_handle、ib_info、ibs_request、hw_ip_info、err_codes传统 CS 提交所需的 context / IB / 请求以及回填的错误码用户队列资源queue/shadow/doorbell/rptr/wptr/csa/eop、queue_cpu、wptr_cpu、db_handle、queue_id、timeline_syncobj_handle/point、submit_mode、userq_paramsuser-mode queue 提交路径用到的一整套 ring/doorbell/fence 资源一个 context 同时容纳「传统 CS」和「用户队列」两套字段正是为了让同一个测试主体在两种提交模型间切换时只换submit回调、不动数据组织。amd_basic.c里几乎每个测试都先calloc一个ring_context、填参数、调ip_block-funcs-xxx(...)、最后free就是这个用法的典型。三、三个静态 ip_block版本号是幌子family_id 才是真身第一个反直觉的点在这里无论你的卡是 GFX9 还是 GFX12注册进amdgpu_ips的永远是这三个「v8」块structamdgpu_ip_block_versiongfx_v8_x_ip_block{.typeAMD_IP_GFX,.major8,.minor0,.rev0,.funcsgfx_v8_x_ip_funcs};structamdgpu_ip_block_versioncompute_v8_x_ip_block{.typeAMD_IP_COMPUTE,.major8,.minor0,.rev0,.funcsgfx_v8_x_ip_funcs};structamdgpu_ip_block_versionsdma_v3_x_ip_block{.typeAMD_IP_DMA,.major3,.minor0,.rev0,.funcssdma_v3_x_ip_funcs};注意两点compute和gfx共用同一张gfx_v8_x_ip_funcs。.major 8是硬编码常量跟真实 GFX 版本无关。setup_amdgpu_ip_blocks()里甚至有断言守着它igt_assert_eq(compute_v8_x_ip_block.major, 8)。所以这里的major 8是「这套抽象的版本号」不是「硬件的 GFX 版本」。要判断真实硬件代次唯一可靠来源是funcs-family_id不是ip_block-major。这个区别很关键——如果你误把ip_block-major当成 GFX 版本去做 dispatch 分支在所有卡上都会拿到 8直接回归。那「v8 的操作表」怎么在 GFX12 上还能正确工作答案是初始化时会把随 ASIC 变化的那几个回调替换掉。四、初始化主线setup_amdgpu_ip_blockssetup_amdgpu_ip_blocks(major, minor, amdinfo, device)每打开一个设备就调用一次。但注意它不是为该设备建一套私有 ip blocks——它把同一批全局静态块追加进全局amdgpu_ips、并把这台设备的身份覆盖写进那张共享 funcs。流程如下识别芯片family_id chip_external_rev → CHIP_xxx推导 chip_classCHIP_xxx → GFX6..GFX12注册三个静态块ip_block_add(gfx/compute/sdma)回填硬件身份每个 funcs-family_id amdinfo-family_id按 family_id 装 ASIC 相关回调ip_block_ex_setup → amd_ip_blocks_ex_init断言校验family_id 与 amdinfo 一致识别芯片用identify_chip宏把amdinfo-family_idchip_external_rev映射到具体CHIP_NAVI10/CHIP_GFX1200之类。推导 chip_class再把CHIP_xxx归到GFX6…GFX12。这决定后面走哪条初始化分支——GFX7 到 GFX12 都走同一条「注册三块 装回调」的路径。回填身份注册后遍历所有块把内核查到的family_id/chip_external_rev/chip_rev写进各自的funcs。这一步之后funcs-family_id才有值amd_ip_blocks_ex_init靠它分流。装 ASIC 相关回调对每个块调amdgpu_device_ip_block_ex_setup()进而调amd_ip_blocks_ex_init()。其中「注册」本身很朴素就是往数组里塞staticintamdgpu_device_ip_block_add(structamdgpu_ip_block_version*ip_block_version){if(amdgpu_ips.num_ip_blocksAMD_IP_MAX)return-1;amdgpu_ips.ip_blocks[amdgpu_ips.num_ip_blocks]ip_block_version;return0;}五、amd_ip_blocks_ex_init按 family_id 打补丁amd_ip_blocks_ex_init()是「v8 操作表」能适配所有代的秘密。它先装一套 default 实现再按family_id用更专的实现覆盖会变的那几个回调voidamd_ip_blocks_ex_init(structamdgpu_ip_funcs*funcs){funcs-gfx_program_computegfx_program_compute_default;funcs-gfx_dispatch_directgfx_dispatch_direct_default;funcs-gfx_write_confirmgfx_write_confirm_default;/* ... hang/reset 钩子 ... */switch(funcs-family_id){caseAMDGPU_FAMILY_RV:caseAMDGPU_FAMILY_NV:caseAMDGPU_FAMILY_VGH:funcs-gfx_dispatch_directgfx_dispatch_direct_gfx9;break;caseAMDGPU_FAMILY_YC:caseAMDGPU_FAMILY_GC_10_3_6:caseAMDGPU_FAMILY_GC_10_3_7:funcs-gfx_dispatch_directgfx_dispatch_direct_gfx10;break;caseAMDGPU_FAMILY_GC_11_0_0:caseAMDGPU_FAMILY_GC_11_0_1:caseAMDGPU_FAMILY_GC_11_5_0:funcs-gfx_program_computegfx_program_compute_gfx11;funcs-gfx_dispatch_directgfx_dispatch_direct_gfx11;break;caseAMDGPU_FAMILY_GC_12_0_0:funcs-gfx_program_computegfx_program_compute_gfx12;funcs-gfx_dispatch_directgfx_dispatch_direct_gfx11;break;default:...}}一个具体的坑gfx_dispatch_direct_gfx9发的是flags 0x00000000而 gfx10/11/12 用的是0x00000045。差异就在 bit0COMPUTE_SHADER_EN0x45 0b1000101——gfx9 这一档故意清掉它。对普通 dispatch 测试无碍但要真正启动 compute shader 时0x0会让 shader 起不来。注意触发替换的条件在amdgpu_device_ip_block_ex_setup里只有当某个块缺少gfx_program_compute/gfx_dispatch_direct/gfx_write_confirm时才调amd_ip_blocks_ex_init。静态表里这几个指针初始为空所以第一次一定会触发。六、寄存器偏移get_reg_offset 的另一条分流除了 dispatch寄存器偏移也随代变化。这条走的是funcs-get_reg_offset参数是一个语义化枚举而不是裸地址enumgeneral_reg{COMPUTE_PGM_LO,COMPUTE_PGM_RSRC1,COMPUTE_TMPRING_SIZE,COMPUTE_USER_DATA_0,COMPUTE_USER_DATA_1,COMPUTE_RESOURCE_LIMITS,COMPUTE_NUM_THREAD_X,};调用方写get_reg_offset(COMPUTE_USER_DATA_0)由 funcs 上的get_reg_offset回调查表返回真实偏移。但这里有个容易误解的点得说清楚和 dispatch 不同get_reg_offset并没有按代次分流。两张静态 funcs 表都把.get_reg_offset写死成gfx_v8_0_get_reg_offsetamd_ip_blocks.c:1087、:1107而amd_ip_blocks_ex_init从不覆盖它只换 dispatch/program。也就是说GFX8–GFX12 拿到的永远是 v8 那张表。它能通用是因为这几个 compute SH 寄存器的偏移在 GFX8–GFX12 上架构稳定而不是「换代自动查到不同偏移」。gfx_v9_0_get_reg_offset虽然定义了但从未挂进任何 funcs 表且内部只是return gfx_v8_0_get_reg_offset(...)amd_gfx_v9_0.c:28属于预留位。所以这层抽象在 reg offset 上的真正价值是语义化枚举避免魔数而不是 per-family swap。七、消费侧测试代码只需get_ip_block对测试代码来说上面这些初始化全是幕后。用起来只有一句conststructamdgpu_ip_block_version*get_ip_block(amdgpu_device_handle device,enumamd_ip_block_typetype){inti;if(g_chip.dev!device)returnNULL;for(i0;iamdgpu_ips.num_ip_blocks;i)if(amdgpu_ips.ip_blocks[i]-typetype)returnamdgpu_ips.ip_blocks[i];returnNULL;}拿到块之后典型用法是ip_block-funcs-write_linear(...)或ip_block-funcs-get_reg_offset(...)。因为funcs已经在初始化时按当前 ASIC 打好了补丁测试代码写一次就能在所有支持的卡上跑。八、关键结论注册进系统的永远是gfx/compute/sdma三个静态 v8/v3 块major是抽象版本号常量不是GFX 硬件版本。判断硬件代次唯一可靠来源是funcs-family_idip_block-major恒为 8/3拿它做分支必然出错。amdgpu_ip_funcs一张回调表按业务分四组PM4 原语write/copy/fill/compare/wait换ip_block即可 GFX↔SDMA 复跑、用户队列生命周期覆盖 CS ioctl 与用户态队列两套提交模型、compute 流水线programdispatch随代次变化最剧烈是 ex_init 重点替换对象、hang/reset 钩子定向制造不同故障类别对应 per-queue vs pipe reset。ASIC 差异主要收敛在amd_ip_blocks_ex_init它按family_id替换 dispatch/program 回调。而get_reg_offset并未按代次分流——它恒为gfx_v8_0_get_reg_offset靠的是 compute SH 寄存器偏移在 GFX8–GFX12 上的架构稳定语义化枚举只是为了避免魔数。测试代码只依赖get_ip_block() 语义化的 funcs 调用不接触任何代次魔数——这正是这层抽象存在的意义。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价