资讯动态

8.2 解析与作业构建:chunk 解析、IB 提取与 amdgpu_job 分配

发布时间:2026/9/8 18:54:48 来源:尧图企业网站定制
8.1 勾勒了amdgpu_cs_ioctl的九步全景指出整条路径始于对用户态提交请求的解析终于向调度器入队并返回 fence。本节聚焦最前端的第 ①—③ 阶段内核如何把用户递交的一堆「chunk」翻译为内核可执行的amdgpu_job并把每一段间接缓冲IB的描述填充就位。这一步不涉及任何 BO 加锁与驻留那是 8.3 的主题但它决定了后续所有阶段将要操作的作业骨架——有几个 job、每个 job 挂几段 IB、领队是谁、user-fence 写到哪里。一次提交从用户态看只是一个drm_amdgpu_cs结构其中携带若干 chunk从内核态看则要经历「准备上下文 → 清点 → 备料 → 装配」四个动作。对应到源码就是amdgpu_cs_ioctl顶部依次调用的三个函数ramdgpu_cs_parser_init(parser,adev,filp,data);/* ① 准备上下文 */...ramdgpu_cs_pass1(parser,data);/* ② 清点 备料分配 job */...ramdgpu_cs_pass2(parser);/* ③ 装配 IB */...ramdgpu_cs_parser_bos(parser,data);/* ④ 交给 8.3 */用户态drm_amdgpu_cs chunks① parser_init准备 parser 上下文② pass1清点 chunk 分配 job③ pass2填充 IB 描述④ parser_bos8.3 BO 加锁与驻留1. 入口与 parser_init为整次提交铺开一张「状态桌」贯穿命令提交全程的核心数据结构是struct amdgpu_cs_parser下称 parser。它像一张摊开的工作台从解析到入队的每个阶段都往上面添置或取用零件解析出的 chunk 数组、分配出的 job、待锁定的 BO 集合、收集到的依赖 fence全都挂在 parser 上。amdgpu_cs_parser_init负责把这张桌子清空并摆好最初的几样工具staticintamdgpu_cs_parser_init(structamdgpu_cs_parser*p,...){if(cs-in.num_chunks0)return-EINVAL;memset(p,0,sizeof(*p));p-adevadev;p-filpfilp;p-ctxamdgpu_ctx_get(fpriv,cs-in.ctx_id);/* 取出提交所属的 ctx */if(!p-ctx)return-EINVAL;amdgpu_sync_create(p-sync);/* 依赖 fence 的容器供 8.4 收集用 */drm_exec_init(p-exec,DRM_EXEC_INTERRUPTIBLE_WAIT|DRM_EXEC_IGNORE_DUPLICATES,0);/* BO 加锁上下文供 8.3 使用 */return0;}这里有三件事值得留意amdgpu_ctx_get依据用户传入的ctx_id取出提交上下文amdgpu_ctx。ctx 承载了调度实体entity与「代际generation」信息——后者用于识别 GPU 复位后上下文是否已失效pass1 末尾会用它做一致性校验。amdgpu_sync_create与drm_exec_init在此刻只是初始化容器p-sync要到 8.4 才装入依赖 fencep-exec要到 8.3 才真正锁定 BO。提前初始化是为了让后续阶段无需再关心「有没有准备好」。除p-ctx与两个容器外parser 的其余字段被memset清零意味着 gang 规模、job 数组、bo_list 等都从「空」开始由 pass1 逐步填入。可跳过的实现细节若你只想把握主干记住一句话即可——parser_init只是「开桌铺料」取出 ctx 并初始化好后续要用的两个空容器。真正有内容的工作从 pass1 开始。2. pass1先「清点」再「备料」amdgpu_cs_pass1的职责可以概括为一句话把用户态的 chunk 数组安全地拷进内核逐个分类清点最后据此分配出恰好数量的 job。之所以要分成「先清点、后分配」两步是因为分配一个amdgpu_job时必须提前知道它将挂载几段 IB——而这个数目只有把所有 IB chunk 数过一遍才能确定。2.1 把 chunk 拷进内核提交请求里的 chunk 以「指针数组」的形式存放在用户空间pass1 首先把这层指针数组整体拷入再逐个把每个 chunk 的数据体拷入chunk_arraymemdup_array_user(u64_to_user_ptr(cs-in.chunks),cs-in.num_chunks,sizeof(uint64_t));...for(i0;ip-nchunks;i){copy_from_user(user_chunk,chunk_ptr,sizeof(structdrm_amdgpu_cs_chunk));p-chunks[i].chunk_iduser_chunk.chunk_id;p-chunks[i].length_dwuser_chunk.length_dw;p-chunks[i].kdatavmemdup_array_user(...,size,sizeof(uint32_t));...}每个 chunk 都带有一个chunk_id标明其类型pass1 用一个switch分门别类并对每种类型先做长度下限校验if (size sizeof(...)) goto free_partial_kdata;确保用户传入的数据体足以容纳对应结构防止后续解析越界。2.2 IB chunk只清点不装配对AMDGPU_CHUNK_ID_IB类型pass1 调用amdgpu_cs_p1_ib——注意它此刻只统计数量不填充内容staticintamdgpu_cs_p1_ib(structamdgpu_cs_parser*p,structdrm_amdgpu_cs_chunk_ib*chunk_ib,unsignedint*num_ibs){intramdgpu_cs_job_idx(p,chunk_ib);/* 该 IB 应归入哪个 job */if(r0)returnr;if(num_ibs[r]amdgpu_ring_max_ibs(chunk_ib-ip_type))return-EINVAL;(num_ibs[r]);/* 仅仅计数 */p-gang_leader_idxr;return0;}要理解这里的num_ibs[r]得先引入gang 提交gang submit的概念。一次amdgpu_cs可以同时向多个不同的引擎队列投放 IB例如同一提交里既有 GFX 又有 Compute这些并列的 job 组成一个「gang」被要求作为一个整体一起上场。amdgpu_cs_job_idx正是把每段 IB 归类到它所属的队列staticintamdgpu_cs_job_idx(structamdgpu_cs_parser*p,structdrm_amdgpu_cs_chunk_ib*chunk_ib){amdgpu_ctx_get_entity(p-ctx,chunk_ib-ip_type,chunk_ib-ip_instance,chunk_ib-ring,entity);/* (引擎类型,实例,ring) → 调度实体 */for(i0;ip-gang_size;i)/* 已经见过这个实体 */if(p-entities[i]entity)returni;/* 复用同一个 gang 槽位 */if(iAMDGPU_CS_GANG_SIZE)return-EINVAL;p-entities[i]entity;/* 新实体扩大 gang */p-gang_sizei1;returni;}于是同一队列的多段 IB 会落到同一个 gang 槽位num_ibs[r]累加不同队列的 IB 则各占一个槽位。pass1 结束时gang_size就是本次提交要创建的 job 数num_ibs[i]则是第 i 个 job 要挂载的 IB 段数。gang_leader_idx记录最后一段 IB 所属的槽位作为整个 gang 的「领队」——领队 job 承载 user-fence 地址等 gang 级别的信息。2.3 其余 chunkuser-fence 与 bo_listpass1 在switch中还处理另外两类需要提前解析的 chunkchunk 类型处理函数动作AMDGPU_CHUNK_ID_IBamdgpu_cs_p1_ib归类到 gang 槽位并计数num_ibs[]AMDGPU_CHUNK_ID_FENCEamdgpu_cs_p1_user_fence查出 user-fence BOp-uf_bo并记录写入偏移AMDGPU_CHUNK_ID_BO_HANDLESamdgpu_cs_p1_bo_handles依据用户句柄数组建立p-bo_list各类DEPENDENCIES/SYNCOBJ_*—此阶段跳过留待 pass2 与 8.4 处理amdgpu_cs_p1_user_fence通过drm_gem_object_lookup找到那块用于回写完成信号的 BO校验其大小必须为一页且偏移合法、且不是 userptr并以p-uf_bo引用持有。amdgpu_cs_p1_bo_handles把用户递交的 BO 句柄数组转换为内核的amdgpu_bo_list即本次提交引用的完整 BO 集合——这正是 8.3 要逐个加锁与驻留的对象清单。而所有依赖类 chunkDEPENDENCIES、SYNCOBJ_IN/OUT、timeline 等在 pass1 里只是break掠过它们不影响 job 的形状因而无需在「清点」阶段介入留到 pass2/8.4 再解析。3. amdgpu_job 的分配把清点结果变成实体清点完毕pass1 末尾据此为每个 gang 槽位分配一个真正的amdgpu_jobfor(i0;ip-gang_size;i){retamdgpu_job_alloc(p-adev,vm,p-entities[i],vm,num_ibs[i],p-filp-client_id,GFP_KERNEL,p-jobs[i]);.../* 按 enforce_isolation 策略设置隔离/清理着色器标志 */}p-gang_leaderp-jobs[p-gang_leader_idx];if(p-ctx-generation!p-gang_leader-generation){ret-ECANCELED;/* ctx 已因 GPU 复位而失效 */gotofree_all_kdata;}if(p-uf_bo)p-gang_leader-uf_addruf_offset;/* user-fence 写入地址落到领队 */amdgpu_vm_set_task_info(vm);/* 记录任务信息便于故障诊断 */几处要点amdgpu_job_alloc传入num_ibs[i]这正是前面「先清点」的意义——分配 job 时就按精确的 IB 段数备好job-ibs[]数组pass2 只需往里填即可无需再动态扩容。代际一致性校验p-ctx-generation ! gang_leader-generation返回-ECANCELED。当 GPU 发生复位TDR后旧 ctx 的代际会被推进此检查确保不会把作业提交到一个已经「失效」的上下文中。user-fence 落到领队若提交请求了 user-fence其写入地址uf_addr只记录在 gang_leader 上——整个 gang 完成后由领队统一回写完成信号。至此本次提交的作业骨架已经成型几个空的 job 各自知道自己属于哪个引擎、要挂几段 IB、谁是领队、user-fence 写往何处。缺的只是每段 IB 的具体描述——这是 pass2 的活。4. pass2把每段 IB 的描述填进 jobamdgpu_cs_pass2再次遍历所有 chunk但这一次是真正处理而非清点。它的switch分支覆盖 IB 与各类依赖 chunk其中依赖类分支p2_dependencies、p2_syncobj_*属于「依赖与同步」的主题将在 8.4 展开。本节只关注 IB 分支amdgpu_cs_p2_ibstaticintamdgpu_cs_p2_ib(structamdgpu_cs_parser*p,structamdgpu_cs_chunk*chunk,...){structdrm_amdgpu_cs_chunk_ib*chunk_ibchunk-kdata;intramdgpu_cs_job_idx(p,chunk_ib);/* 与 pass1 相同的归类定位到对应 job */jobp-jobs[r];ibjob-ibs[job-num_ibs];/* 取出 job 中下一个空的 IB 槽 *//* 一系列合法性检查 */if(ring-no_user_submission)return-EINVAL;/* 内核队列禁止用户提交 */if(p-uf_boring-funcs-no_user_fence)return-EINVAL;/* MM 引擎不支持 user-fence */.../* CE CS 拦截、GFX 抢占 IB 的 CE/DE 计数上限校验 */ramdgpu_ib_get(p-adev,vm,...,AMDGPU_IB_POOL_DELAYED,ib);/* 分配 IB 管理结构 */ib-gpu_addrchunk_ib-va_start;/* IB 在 GPU 地址空间中的起始 VA */ib-length_dwchunk_ib-ib_bytes/4;/* IB 长度以 dword 计 */ib-flagschunk_ib-flags;return0;}核心动作只有两步取槽与填料。取槽ib job-ibs[job-num_ibs]。由于 pass1 已按精确数量分配好ibs[]数组这里的自增不会越界job-num_ibs从 0 开始随填充递增最终等于 pass1 清点出的段数。填料amdgpu_ib_get为该 IB 备好底层管理结构随后把用户描述里的三项关键信息落位——gpu_addrIB 数据在 GPU 虚拟地址空间中的位置、length_dw长度、flags如 PREAMBLE、PREEMPT 等。注意gpu_addr只是记录了一个 GPU VA此刻并不校验它是否真的映射有效那要等 8.3 完成 BO 驻留、8.5 完成页表回填之后才成立。填充过程中的若干检查体现了引擎能力约束内核内部队列不接受用户提交、多媒体引擎不支持 user-fence、GFX 抢占场景下 CE/DE 各自最多一段可抢占 IB 等。这些检查一旦失败即中止整次提交。5. 形象化小结一次「按单备餐」的流程若把命令提交比作后厨接单出餐第 ①—③ 阶段恰好对应「接单—清点—开工单—备料」四步后厨动作CS 阶段关键产物铺开操作台、拿出订单夹与料盒①parser_init空的 parserctx、sync、exec容器逐条读订单、数清每个灶台要几道菜②pass1清点gang_size、num_ibs[]、bo_list、uf_bo按灶台开出恰好数量的工单②pass1分配各amdgpu_job含 gang_leader把每道菜的用料填进对应工单③pass2填好的job-ibs[]gpu_addr / 长度 / flags贯穿其间的设计要点是**「先清点、后分配」**只有先把所有 IB chunk 数过一遍才能一次性为每个 job 分配尺寸精确的 IB 数组让 pass2 的填充零动态扩容、零越界。而依赖 chunk 在这三步里始终「靠边站」——它们不改变作业的形状因而被推迟到 8.4 统一收集。可跳过的实现细节若你只关心接口与主干记住三点即可——(1) pass1 清点出「几个 job、每个挂几段 IB」并据此分配 job(2) pass2 把每段 IB 的gpu_addr/长度/flags填进对应 job(3) 这一整章都还没碰 BO 锁与驻留。gang、CE/DE 抢占、user-fence 落位等细节可在需要时再回看。作业骨架已经就绪但此刻它引用的所有 BO 还散落在各处、既未锁定也未必驻留在 GPU 可访问的内存域。下一节 8.3 将进入第 ④ 阶段amdgpu_cs_parser_bos讲解drm_exec如何无死锁地一次性锁定全部 BO、ttm_bo_validate如何把它们拉回合法内存域为后续的依赖收集与入队铺平道路。

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

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

免费获取报价