1./dev/kfd管理一组设备而并非单个设备,如果有多个HSA节点,则open kfd的上下文将会创建一个process,并打开为这个server上的所有可用的HSA节点分别创建上下文。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <stdint.h>
#include <linux/kfd_ioctl.h>
#define DEV_NAME "/dev/kfd"
int open_device(void)
{
int fd = -1;
if ((fd = open(DEV_NAME, O_RDWR)) < 0) {
perror("open device error");
return -1;
}
return fd;
}
int close_device(int fd)
{
close(fd);
return 0;
}
int main(void)
{
int fd, ret, i, render_fd;
struct kfd_ioctl_get_version_args args_ver;
struct kfd_ioctl_get_process_apertures_new_args args_devinfo;
struct kfd_process_device_apertures *pa;
struct kfd_ioctl_set_memory_policy_args args = {0};
struct kfd_ioctl_acquire_vm_args args_vm;
fd = open_device();
if (fd < 0) {
perror("open kfd device failure.\n");
return -1;
}
render_fd = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC);
if (fd < 0) {
perror("open render device failure.\n");
return -1;
}
ret = ioctl(fd, AMDKFD_IOC_GET_VERSION, &args_ver);
if (ret == 0) {
printf("kfd version major %d, minor %d.\n",
args_ver.major_version, args_ver.minor_version);
}
args_devinfo.num_of_nodes = 10;
args_devinfo.kfd_process_device_apertures_ptr = (unsigned long)malloc(args_devinfo.num_of_nodes * sizeof(struct kfd_process_device_apertures));
if (args_devinfo.kfd_process_device_apertures_ptr == 0) {
perror("fail to malloc device apertures buffer.\n");
return -1;
}
ret = ioctl(fd, AMDKFD_IOC_GET_PROCESS_APERTURES_NEW, &args_devinfo);
if (ret != 0) {
perror("fail to get process apertures.\n");
return -1;
}
printf("%s line %d, args_devinfo.num_of_nodes = %d.\n", __func__, __LINE__, args_devinfo.num_of_nodes);
pa = (struct kfd_process_device_apertures *)args_devinfo.kfd_process_device_apertures_ptr;
for (i = 0; i < args_devinfo.num_of_nodes; i ++) {
printf("idx %d, gpu_id 0x%x.\n"
"lds_base 0x%llx.\n"
"lds_limit 0x%llx.\n"
"gpuvm_base 0x%llx.\n"
"gpuvm_limit 0x%llx.\n"
"scratch_base 0x%llx.\n"
"scratch_limit 0x%llx.\n", i, pa[i].gpu_id, pa[i].lds_base, pa[i].lds_limit, pa[i].lds_limit, pa[i].lds_limit, pa[i].scratch_base, pa[i].scratch_limit);
}
args_vm.gpu_id = pa[0].gpu_id;
args_vm.drm_fd = render_fd;
ret = ioctl(fd, AMDKFD_IOC_ACQUIRE_VM, (void *)&args_vm);
if (ret != 0) {
perror("fail to get acquire vm.\n");
return -1;
}
args.gpu_id = pa[0].gpu_id;
args.default_policy = KFD_IOC_CACHE_POLICY_COHERENT;
args.alternate_policy = KFD_IOC_CACHE_POLICY_COHERENT;
args.alternate_aperture_base = (uintptr_t)NULL;
args.alternate_aperture_size = 0;
ret = ioctl(fd, AMDKFD_IOC_SET_MEMORY_POLICY, &args);
if (ret != 0) {
perror("fail to set memory policy.\n");
return -1;
}
printf("%s line %d, kfd test success.\n", __func__, __LINE__);
free((void *)args_devinfo.kfd_process_device_apertures_ptr);
close_device(fd);
return 0;
}
2.如果/dev/kfd管理多个设备,那么驱动如何确定当前系统调用下来要访问那个HSA设备呢?
方法是用户会在系统调用的参数中传入要操作的gpu_id.
3.既然如此,进行系统调用前用户应用必须首先知道有那些GPU_ID,那么应用是如何知道的呢?答案是通过ioctl AMDKFD_IOC_GET_PROCESS_APERTURES_NEW command.在UMD应用初始化GPU Context阶段,会首先调用AMDKFD_IOC_GET_PROCESS_APERTURES_NEW CMD查询当前server上可用的HSA设备,并返回GPU ID。
3.AMDGPU显存对象叫做buffer object,简称BO,BO底层基于ttm实现:
4.amdgpu_vm_bo_base_init构造了什么?
一个amdgpu_bo本身表示一段显存,它本身不带有context信息,当MAP到具体的卡上才带有了上下文信息,1个BO可以映射到多张卡,所以,一个BO可以对应多个VM,一个VM也可以对应多个BO,为了体现这种联系,可以将归属于同一个BO的VM关联到一起,对应到BO上,amdgpu_vm_bo_base_init就是做的这件事。其逆操作是amdgpu_vm_bo_rmv。
每个struct amdgpu_vm_bo_base->bo指向的是链表头的bo,方便从任意一个base bo开始查询。
KFD关键数据结构:
1.每个打开GPU的进程,无论open多少次,只创建一个kfd_process对象,也就是说,kfd_process每个对象1个。
2.一个KFD PROCESS对应1个PASID,也就是说,一个进程一个PASID。
3.多卡环境,1个open kfd,所有的HSA设备上下文一次性打开,关联到kfd_process对象,每个设备上下文对应一个从drm框架私有结构分配的amdgpu_vm对象。
4.对于多进程一样。
DRM RENDER设备每次打开分配一个VM和一个GFX域内的PASID,所以UMD每次初始化一个HSA设备,都需要打开一次RENDER节点,然后传入对应的GPUID调用KFD IOCTL,再次对VM进行初始化。
然后KFD框架会调用kfd_ioctl_acquire_vm,得到DRM分配的amdgpu_vm,然后在KFD框架中调用amdgpu_vm_init再次将VM对象初始化一遍(第一遍初始化是在open drm render设备时候amdgpu_driver_open_kms上下文里面)。具体可以查看调用amdgpu_vm_init的地方,它只有两处,一处是DRM,另一处就是KFD的acquire_vm调用链中。
Render设备可以被多次打开,每次打开,内部生成一个VM对象,每个HSA Device对应一个Render FD,也就对应一个VM,在ACQUIRE_VM的IOCTL命令中完成初始化,下图是打开两个RENDER FD,创建两个VM的调用堆栈:
# tracer: function
#
# entries-in-buffer/entries-written: 4/4 #P:12
#
# _-----=> irqs-off
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
a.out-3374 [004] .... 4711.142621: amdgpu_vm_init <-amdgpu_driver_open_kms
a.out-3374 [004] .... 4711.142626: <stack trace>
=> amdgpu_vm_init
=> amdgpu_driver_open_kms
=> drm_file_alloc
=> drm_open
=> drm_stub_open
=> chrdev_open
=> do_dentry_open
=> vfs_open
=> path_openat
=> do_filp_open
=> do_sys_openat2
=> do_sys_open
=> __x64_sys_openat
=> do_syscall_64
=> entry_SYSCALL_64_after_hwframe
a.out-3374 [004] .... 4711.142729: amdgpu_vm_init <-amdgpu_driver_open_kms
a.out-3374 [004] .... 4711.142733: <stack trace>
=> amdgpu_vm_init
=> amdgpu_driver_open_kms
=> drm_file_alloc
=> drm_open
=> drm_stub_open
=> chrdev_open
=> do_dentry_open
=> vfs_open
=> path_openat
=> do_filp_open
=> do_sys_openat2
=> do_sys_open
=> __x64_sys_openat
=> do_syscall_64
=> entry_SYSCALL_64_after_hwframe
/dev/render128的作用
ROCm依赖于/dev/render128节点提供的amdgpu_vm对象,但是分析代码,貌似对/dev/render128的依赖分成两个阶段,在5.4的KFD实现中,当lazy binding VM的关键函数kfd_process_device_init_vm的第二个参数为NULL时,代表不需要/dev/render128节点文件,子子流程amdgpu_amdkfd_gpuvm_create_process_vm函数会分配amdgpu_vm对象,不依赖于drm render128 file提供的amdgpu_vm文件。
在之后的的某个版本迭代中,With KFD merging into amdgpu, and with ROCm using VMs from DRM FDs, there is no use case anymore where we would not use a DRM FD to get the VM,也就是说,从之后的某个版本开始,KFD必须依赖于/dev/render128提供的amdgpu_vm对象,而不再使用KFD自行分配的amdgpu_vm对象。
所以,最新的ROCm sdk, KFD对/dev/render128的需要必不可少。
经过确认,去除kfd self create vm的改动是在v5.12-rc7的开发周期提交的。
KFD Process的释放
kfd_process对象通过引用计数进行生命期控制,由于FD释放发生在进程EXIT之前,所以最后一次释放发生在do_exit的流程中调用mmu notifier kfd_process_notifier_release, 通过mmu_notifier_put->mmu_notifier_free_rcu触发RCU回调,最终在kfd_process_ref_release中调用kfd_unref_release触发引用计数归零完成释放。
a.out-2880 [006] .... 3357.338719: kfd_unref_process <-kfd_release
a.out-2880 [006] .... 3357.338720: <stack trace>
=> kfd_unref_process
=> kfd_release
=> __fput
=> ____fput
=> task_work_run
=> exit_to_user_mode_prepare
=> syscall_exit_to_user_mode
=> do_syscall_64
=> entry_SYSCALL_64_after_hwframe
a.out-2880 [006] .... 3357.338751: mmu_notifier_put <-kfd_process_notifier_release
a.out-2880 [006] .... 3357.338753: <stack trace>
=> mmu_notifier_put
=> kfd_process_notifier_release
=> __mmu_notifier_release
=> exit_mmap
=> mmput
=> do_exit
=> do_group_exit
=> __x64_sys_exit_group
=> do_syscall_64
=> entry_SYSCALL_64_after_hwframe
kworker/6:0-2803 [006] .... 3357.351184: kfd_process_ref_release <-kfd_process_free_notifier
kworker/6:0-2803 [006] .... 3357.351194: <stack trace>
=> kfd_process_ref_release
=> kfd_process_free_notifier
=> mmu_notifier_free_rcu
=> srcu_invoke_callbacks
=> process_one_work
=> worker_thread
=> kthread
=> ret_from_fork
a.out-2880 [006] .... 3357.351315: remove_vma <-exit_mmap
a.out-2880 [006] .... 3357.351319: <stack trace>
=> remove_vma
=> exit_mmap
=> mmput
=> do_exit
=> do_group_exit
=> __x64_sys_exit_group
=> do_syscall_64
=> entry_SYSCALL_64_after_hwframe
/dev/kfd的使用限制
看下图的注释和逻辑,CHILD进程需要打开它自己的FD句柄,而不能用继承子父进程的,分析原因可能是因为KFD_PROCESS上下文的使用限制,每个进程会创建自己的KFD_PROCESS对象,但是FORK调用时,内核复制进程的时候对文件的拷贝是浅拷贝,也就是说,子进程继承自父进程的kfd设备对应的fd和父进程自身的fd指向的是同一个struct file,这样就相当于,子进程没有经过调用kfd_open创建自己的KFD_PROCESS对象的过程,也就无法用继承来的FD调用KFD的IOCTL。
如果用户态应用强行这样做,KFD_IOCTL将会返回-EBADF错误。
N卡驱动和A卡驱动的不同
HSA模式下,BO没有属主,BO在某个卡的上下文中分配,但是并不属于这张卡,而是大家一起用,当某张卡下线(RESET)时,只需要取消对BO的引用即可。
N卡是独立显卡,物理上某个BO一定位于某张卡上,在此卡的上下文中分配,并被其它卡引用,当此卡下线时,可能会存在其他卡仍然在引用这个BO的情况。