AMD KFD驱动分析

11 篇文章 4 订阅
5 篇文章 0 订阅
本文详细解释了如何通过/opendev/kfd管理多个HSA设备,涉及open_device、ioctl操作如AMDKFD_IOC_GET_PROCESS_APERTURES_NEW来获取GPUID,以及KFD与GPU上下文、VM初始化的关系,特别提到了N卡与A卡驱动的区别和KFD的使用限制。
摘要由CSDN通过智能技术生成

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的情况。


结束

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
以下是一个简单的ROS键盘控制小车底盘运动的C++代码示例: ```cpp #include <ros/ros.h> #include <geometry_msgs/Twist.h> #include <stdio.h> #include <stdlib.h> #include <termios.h> #include <unistd.h> #include <fcntl.h> #define KEYCODE_W 0x77 #define KEYCODE_S 0x73 #define KEYCODE_A 0x61 #define KEYCODE_D 0x64 #define KEYCODE_Q 0x71 #define KEYCODE_E 0x65 int kfd = 0; struct termios cooked, raw; void quit(int sig) { tcsetattr(kfd, TCSANOW, &cooked); ros::shutdown(); exit(0); } int main(int argc, char** argv) { ros::init(argc, argv, "teleop_twist_keyboard"); ros::NodeHandle nh; ros::Publisher twist_pub = nh.advertise<geometry_msgs::Twist>("cmd_vel", 1); signal(SIGINT,quit); char c; bool dirty=false; // get the console in raw mode tcgetattr(kfd, &cooked); memcpy(&raw, &cooked, sizeof(struct termios)); raw.c_lflag &=~ (ICANON | ECHO); // Setting a new line, then end of file raw.c_cc[VEOL] = 1; raw.c_cc[VEOF] = 2; tcsetattr(kfd, TCSANOW, &raw); puts("Reading from keyboard"); puts("---------------------------"); puts("Use arrow keys to move the robot."); for(;;) { // get the next event from the keyboard if(read(kfd, &c, 1) < 0) { perror("read():"); exit(-1); } geometry_msgs::Twist twist; switch(c) { case KEYCODE_W: twist.linear.x = 1.0; dirty = true; break; case KEYCODE_S: twist.linear.x = -1.0; dirty = true; break; case KEYCODE_A: twist.angular.z = 1.0; dirty = true; break; case KEYCODE_D: twist.angular.z = -1.0; dirty = true; break; case KEYCODE_Q: twist.linear.y = 1.0; dirty = true; break; case KEYCODE_E: twist.linear.y = -1.0; dirty = true; break; } if(dirty ==true) { twist_pub.publish(twist); dirty=false; } } return(0); } ``` 这个代码将读取键盘输入并将运动控制命令发布到ROS话题/cmd_vel上。可以通过订阅该话题的节点来控制小车底盘的运动。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

papaofdoudou

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值