AMD GPU任務調度(2)—— 內核態分析

簡介

  • 內核態的GPU驅動需要處理用戶態驅動下發的渲染命令,對於每個用戶態的進程,在提交渲染命令前首先通過mesa驅動創建屬於自己的上下文,然後往上下文關聯的cmdbuf中填入渲染命令然後下發。渲染命令並不是一條一條下發給內核,而是批量統一放到一個內存chunk中,這個chunk內存空間是用戶態已經向內核申請好的,由內核DRM框架管理,因此用戶態下發的實際動作就是下發ioctl命令字然後把chunk的指針告訴內核,內核只要獲取到這個地址將其放到內核的IB對象中就可以了。

GPU調度示意圖

在這裏插入圖片描述

數據結構組織圖

在這裏插入圖片描述

CS管理數據結構

amdgpu_cs_chunk

  • 對應用戶態的drm_amdgpu_cs_chunk結構,內核在接收用戶態渲染命令後,會將用戶態下發的chunk對應保存在內核態的chunk中
struct amdgpu_cs_chunk {
    uint32_t        chunk_id;				/* 1 */
    uint32_t        length_dw;				/* 2 */
    void            *kdata;					/* 3 */
}; 
1. chunk類型,用戶態和內核態共同約定的chunk類型,不同chunk類型chunk組成不同,對應的解析方式不同
2. chunk空間的大小
3. chunk存放的渲染命令

amdgpu_cs_parser

  • parser用戶存放用戶態下發的渲染命令相關的信息,並關聯對應的GPU設備,文件設備,渲染命令提交上下文等。當用戶態驅動下發命令字時,它的所有信息都放在parser中,之後內核態的所有操作都攜帶parser對象,從parser中取需要的數據
struct amdgpu_cs_parser {
    struct amdgpu_device    *adev;						/* 1 */
    struct drm_file     *filp;							/* 2 */
    struct amdgpu_ctx   *ctx;							/* 3 */

    /* chunks */
    unsigned        nchunks;							/* 4 */
    struct amdgpu_cs_chunk  *chunks;

    /* scheduler job object */
    struct amdgpu_job   *job;							/* 5 */
    struct drm_sched_entity *entity;					/* 6 */   
	......
};
1. amdgpu設備在內核drm框架的下的抽象,每個GPU設備關聯一個
2. amdgpu字符設備關聯的文件抽象,用戶態驅動通過打開/dev/dri/card0字符設備下發ioctl命令字,該成員是內核file結構在drm框架下的封
裝,drm_file結構是基於struct file實現的,因此它內部會指向一個struct file的成員
3. GPU渲染命令提交上下文,它包含所有GPU硬件IP核的調度實體
4. 具體的渲染命令以chunk的形式存放在amdgpu_cs_chunk對象中,每個chunk可以存放數個渲染命令,任務提交的時候
5. GPU IP核ring buffer上的調度器job,每個渲染命令上下文的提交需要關聯一個job,這個job會指向存放渲染命令的內存空間
6. GPU IP核ring buffer上的調度實體,

amdgpu_ib

  • IB是內核存放渲染命令的基本單位,應用程序下發的chunk,最終會被內核放到ib中,每個chunk了對應一個ib
struct amdgpu_ib {
    struct amdgpu_sa_bo     *sa_bo;
    uint32_t            length_dw;		/* 1 */
    uint64_t            gpu_addr;		/* 2 */
    ......
}; 
1. ib空間的大小,4字節爲單位
2. ib空間的起始地址,這個地址屬於GTT,是用戶態通過GEM的API申請的GPU虛擬地址,GPU可訪問

GPU調度數據結構

drm_sched_job

  • 該job是調度實體管理其上job隊列的元素,一個調度實體可以通過job queue管理多個job。
struct drm_sched_job {
    struct spsc_node        queue_node;		/* 1 */
    struct drm_gpu_scheduler    *sched;		/* 2 */
    enum drm_sched_priority     s_priority;	/* 3 */
    struct drm_sched_entity  *entity;		/* 4 */
}; 
1. 用於鏈入調度實體隊列的元素
2. job所在的調度器
3. job優先級,待分析
4. job所在的調度實體

amdgpu_job

  • amdgpu_job更接近上層,上層驅動下發的渲染命令被存放在ib中,amdgpu_job就是ib的封裝,它的內部有一個調度job,會指向調度實體。
struct amdgpu_job {
    struct drm_sched_job    base;		/* 1 */
    struct amdgpu_ib    *ibs;				/* 2 */
    uint32_t        num_ibs;
	......
};
1. 調度實體的job隊列管理的job
2. 渲染命令存放的IB空間起始地址以及IB個數

drm_sched_entity

struct drm_sched_entity {   
    struct list_head        list;									/* 1 */
    struct drm_sched_rq     *rq;    								/* 2 */
    struct drm_gpu_scheduler        **sched_list;					/* 3 */
    enum drm_sched_priority         priority;						/* 4 */
    struct spsc_queue       job_queue;								/* 5 */
};
1. 一個運行隊列可以管理多個調度實體,該成員用於鏈入調度隊列
2. 調度實體所在的運行隊列
3. 調度實體所在的ring buffer上的調度器
4. 調度實體的優先級,每個優先級上都有一個運行隊列,管理屬於該優先級的調度實體
5. 調度實例的job隊列,管理多個job

drm_sched_rq

struct drm_sched_rq {
    struct drm_gpu_scheduler    *sched;				/* 1 */
    struct list_head        entities;				/* 2 */
    struct drm_sched_entity     *current_entity;	/* 3 */
}; 
1. 運行隊列所屬的調度器
2. 運行隊列管理的調度實體鏈表投
3. 當前正在處理的調度實體

drm_gpu_scheduler

struct drm_gpu_scheduler {
    const struct drm_sched_backend_ops  *ops;					/* 1 */
    uint32_t            hw_submission_limit;					/* 2 */
    long                timeout;								/* 3 */
    struct delayed_work     work_tdr;							/* 4 */
    const char          *name;									/* 5 */
    struct drm_sched_rq     sched_rq[DRM_SCHED_PRIORITY_MAX];	/* 5 */
    wait_queue_head_t       wake_up_worker;						/* 6 */
    atomic_t            hw_rq_count;							/* 7 */
    struct task_struct      *thread;							/* 8 */			
};
1. 調度器執行任務的回調,核心成員就是run_job,每個IP核上的ring buffer處理job的方式可能不一樣,調度器真正執行job的時候就調用ops的run_job函數
2. 允許調度器同時執行job任務的上限,只有當前調度器執行的任務小於這個值是,才能取出job執行
3. 允許調度執行任務的最長時間,如果任務執行時間超時,內核會調用超時回調處理函數drm_sched_job_timedout。這個功能通過內核的延時工作隊列實現,其初始化在內核線程初始化中完成
4. timeout超時工作隊列
5. 調度器管理的運行隊列,每個運行隊列有一個或者多個調度實體,每個實體有一個或者多個調度job
6. 當調度隊列是一個內核線程,沒有任務處理時它進入睡眠狀態,調度隊列通過該等待隊列成員休眠,當驅動有job到達時,通過此運行隊列喚醒調度隊列內核線程
7. 當前調度器處理的job個數,開始執行job時計數加1,完成時減1
8. 調度隊列是個內核線程,thread指向線程的結構體

流程

  • GPU處理任務的調度流程從ioctl回調函數amdgpu_cs_ioctl開始介紹,當用戶態驅動打開/dev/dri/cardX下發DRM_IOCTL_AMDGPU_CS命令字時,會觸發該函數,內核態ioctl命令字接口定義如下:
const struct drm_ioctl_desc amdgpu_ioctls_kms[] = {
	......
	DRM_IOCTL_DEF_DRV(AMDGPU_CS, amdgpu_cs_ioctl, DRM_AUTH|DRM_RENDER_ALLOW)
	......
}

流程圖

  • TODO

整體流程

  • amdgpu_cs_ioctl函數非常複雜,我們首先分析函數本身,提煉出幾個重要的步驟,再進一步分析下去
int amdgpu_cs_ioctl(struct drm_device *dev, void *data, struct drm_file *filp)
{   
    struct amdgpu_device *adev = dev->dev_private;
    union drm_amdgpu_cs *cs = data;
    struct amdgpu_cs_parser parser = {};
    
    parser.adev = adev;
    parser.filp = filp;

    amdgpu_cs_parser_init(&parser, data);						/* 1 */
    amdgpu_cs_ib_fill(adev, &parser);							/* 2 */

    amdgpu_cs_dependencies(adev, &parser);						/* 3 */
    amdgpu_cs_parser_bos(&parser, data);
    amdgpu_cs_vm_handling(&parser);

    amdgpu_cs_submit(&parser, cs);					/* 4 */
}
1. 解析用戶態下發的信息,主要是將下發渲染命令存放到內核態的chunk結構中,並初始化一個任務,用作之後提交
2. 從chunk中解析得到數據,將其填充到任務的ib中,當調度器執行一個任務時,可以找到該任務相關的渲染命令。除了填充任務,這裏還會初始
化任務的調度實體,任務畢竟和具體業務相關,要把它放到調度隊列上,需要一個調度隊列可以識別的調度實體
3. 待分析
4. 將渲染命令封裝成任務並且創建調度實體之後,就是把調度實體放到調度隊列上,通知調度器工作了

保存渲染命令

static int amdgpu_cs_parser_init(struct amdgpu_cs_parser *p, union drm_amdgpu_cs *cs)
{                  
    struct amdgpu_fpriv *fpriv = p->filp->driver_priv;
    struct amdgpu_vm *vm = &fpriv->vm;
    uint64_t *chunk_array_user;
    uint64_t *chunk_array;

    chunk_array = kmalloc_array(cs->in.num_chunks, sizeof(uint64_t), GFP_KERNEL);			/* 1 */     
    p->ctx = amdgpu_ctx_get(fpriv, cs->in.ctx_id);
    /* get chunks */
    chunk_array_user = u64_to_user_ptr(cs->in.chunks);
    copy_from_user(chunk_array, chunk_array_user,											/* 2 */
               sizeof(uint64_t)*cs->in.num_chunks)

    p->nchunks = cs->in.num_chunks;															/* 3 */
    p->chunks = kmalloc_array(p->nchunks, sizeof(struct amdgpu_cs_chunk),					/* 4 */
                GFP_KERNEL);

    for (i = 0; i < p->nchunks; i++) {														/* 5 */
		......
        p->chunks[i].chunk_id = user_chunk.chunk_id;										
        p->chunks[i].length_dw = user_chunk.length_dw;
		copy_from_user(p->chunks[i].kdata, cdata, size)										
		......
        }
    }

    amdgpu_job_alloc(p->adev, num_ibs, &p->job, vm);									/* 6 */
	......
}
1. 分配存放用戶態數據地址的指針數組,空間大小由用戶態下發的數據決定,這裏是cs->in.num_chunks個
2. 拷貝用戶態數據的指針到內核態的指針數組
3. 使用用戶態數據初始化parser的部分結構
4. 分配真正的保存用戶態數據的空間,大小是amdgpu_cs_chunk,個數是num_chunks個,爲保存用戶態數據做準備
5. 依次拷貝每個chunk的數據,這之後,用戶態的chunk數據已經全部保存到內核parser的chunk中
6. 初始化本次提交的job

初始化job

  • job初始化是在parser過程中完成的,它由一個調度實體的job_queue管理,一個調度實體中可以有多個job
int amdgpu_job_alloc(struct amdgpu_device *adev, unsigned num_ibs,
             struct amdgpu_job **job, struct amdgpu_vm *vm)
{           
    size_t size = sizeof(struct amdgpu_job);						/* 1 */
    size += sizeof(struct amdgpu_ib) * num_ibs;					
    *job = kzalloc(size, GFP_KERNEL);  
    
    /*
     * Initialize the scheduler to at least some ring so that we always
     * have a pointer to adev.
     */
    (*job)->base.sched = &adev->rings[0]->sched;					/* 2 */
    (*job)->ibs = (void *)&(*job)[1];								/* 3 */
    (*job)->num_ibs = num_ibs;
    ......
}
1. 爲job分配空間,分配的大小是job的大小和num_ibs個amdgpu_ib的大小,從這裏可以看出,job結構體下面還掛着num_ibs,因此需要這麼多空間
2. 初始化job所在的調度器,將其默認指向GPU IP設備上的第一個ring buffer調度隊列
3. 設置ibs,將其指向amdgpu_job的尾部,這樣內核多分出amdgpu_job的空間就可以用作存放ibs,之後,job的ibs會被填入渲染命令

填充IB

  • 解析parser中從用戶態拷貝的chunk數據,將它放到任務的ibs數組中,這樣任務被調度的時候,可以訪問這些ibs
static int amdgpu_cs_ib_fill(struct amdgpu_device *adev,
                 struct amdgpu_cs_parser *parser)
{       
    struct amdgpu_ring *ring;     
    for (i = 0, j = 0; i < parser->nchunks && j < parser->job->num_ibs; i++) {		/* 1 */
        struct amdgpu_cs_chunk *chunk;
        struct amdgpu_ib *ib;
        struct drm_amdgpu_cs_chunk_ib *chunk_ib;
        struct drm_sched_entity *entity;
    
        chunk = &parser->chunks[i];												/* 2 */
        ib = &parser->job->ibs[j];
        chunk_ib = (struct drm_amdgpu_cs_chunk_ib *)chunk->kdata;				/* 3 */

        amdgpu_ctx_get_entity(parser->ctx, chunk_ib->ip_type,					/* 4 */
                      chunk_ib->ip_instance, chunk_ib->ring,&entity);

        if (parser->entity && parser->entity != entity)							/* 5 */
            return -EINVAL;

        /* Return if there is no run queue associated with this entity.
         * Possibly because of disabled HW IP*/
        if (entity->rq == NULL)													/* 6 */
            return -EINVAL;

        parser->entity = entity;												/* 7 */

        ring = to_amdgpu_ring(entity->rq->sched);
        r =  amdgpu_ib_get(adev, vm, ring->funcs->parse_cs ?
                   chunk_ib->ib_bytes : 0, ib);

        ib->gpu_addr = chunk_ib->va_start;										/* 8 */
        ib->length_dw = chunk_ib->ib_bytes / 4;
        ib->flags = chunk_ib->flags;

        j++;
    }
	......
}
1. 針對每個chunk,依次讀取它關聯的數據,將其填充到job的ibs數組中
2. 分別獲取chunk地址和job中存放ib的地址,我們的主要任務就是讓ibs數組中的每個ib指向這裏的每個chunk
3. 取出chunk中包含的數據所在地址
4. 一個chunk對應着一個ring buffer,一次提交的所有渲染命令,必須保證是往同一個ring buffer上提交的,這裏根據chunk對應的IP類型核ring
buffer索引,可以確認這個chunk上的渲染命令是往哪個IP核的哪個ring buffer上提交。對於每個IP核上的ring buffer,每個上下文都有一個對應的
調度實體。這裏會通過chunk所在ring buffer的類型取獲取這個實體,如果沒有,就會創建
5. 獲取到調度實體之後,需要比較各個chunk的調度實體是否一樣,如果不一樣,說明多個chunk會提交渲染命令到不同的ring buffer,不允許這
樣,同時也能看到,一個提交的上下文只對應唯一的ring buffer和調度實體
6. 如果調度實體上沒有運行隊列,返回出錯
7. 將調度實體放到parser上,所有chunk都使用這個調度實體
8. 填寫ib的地址,將其設置指向一個chunk_ib,這個本函數的核心動作

初始化entity

  • 調度實體的初始化在amdgpu_ctx_get_entity中實現,當不能獲取entity時,就會創建一個
int amdgpu_ctx_get_entity(struct amdgpu_ctx *ctx, u32 hw_ip, u32 instance,
              u32 ring, struct drm_sched_entity **entity)
{           
    if (hw_ip >= AMDGPU_HW_IP_NUM) {								/* 1 */
        DRM_ERROR("unknown HW IP type: %d\n", hw_ip);
        return -EINVAL;
    }       
         
    if (ring >= amdgpu_ctx_num_entities[hw_ip]) {					/* 2 */
        DRM_DEBUG("invalid ring: %d %d\n", hw_ip, ring);
        return -EINVAL;
    }
        
    if (ctx->entities[hw_ip][ring] == NULL) {						/* 3 */
        amdgpu_ctx_init_entity(ctx, hw_ip, ring);
    }
        
    *entity = &ctx->entities[hw_ip][ring]->entity;					/* 4 */
}
1. AMD GPU硬件IP模塊只有AMDGPU_HW_IP_NUM個,如果超出這個範圍,認爲是無法識別的IP模塊
2. 每個IP模塊上的ring buffer只有amdgpu_ctx_num_entities[hw_ip]個,超出後也認爲無法識別
3. 如果提交上下文中沒有對應的調度實體,就創建一個,從這裏可以看到,每個提交上下都可以擁有一個entity,這裏說是創建,實際上在內部是
引用,因爲每個IP核的ring buffer上有調度器,調度器管理了調度隊列,我們只需要把調度器實體所在的運行隊列指向調度器的運行隊列就可以了
4. 將找到的調度實體返回給調用者
  • 繼續分析調度實體的初始化函數amdgpu_ctx_init_entity
static int amdgpu_ctx_init_entity(struct amdgpu_ctx *ctx, const u32 hw_ip, const u32 ring)
{
    struct amdgpu_ctx_entity *entity;
    struct drm_gpu_scheduler **scheds = NULL, *sched = NULL;
    unsigned num_scheds = 0;

    entity = kcalloc(1, offsetof(typeof(*entity), fences[amdgpu_sched_jobs]),			/* 1 */
             GFP_KERNEL);
             
    switch (hw_ip) {																	/* 2 */
    case AMDGPU_HW_IP_GFX:
        sched = &adev->gfx.gfx_ring[0].sched;
        scheds = &sched;
        num_scheds = 1;
        break;
    case AMDGPU_HW_IP_COMPUTE:
	......
    drm_sched_entity_init(&entity->entity, priority, scheds, num_scheds,&ctx->guilty);  /* 3 */ 
	ctx->entities[hw_ip][ring] = entity;												/* 4 */
1. 分配entity空間
2. 根據chunk所在的IP,找到對應ring buffer上所有調度器的基地址
3. 初始化entity
4. 將初始化好的entity放到GPU渲染上下文中amdgpu_ctx
  • 分析drm_sched_entity_init,它的核心操作就是設置entity的運行隊列,將其指向對應ring buffer調度器上維護的隊列中,注意,這裏我們看到的是將entity上的運行隊列指向了IP核上的第一個ring buffer的調度器,後面會根據調度器上的任務數,選擇合適的運行隊列
drm_sched_entity_init
    entity->rq = &sched_list[0]->sched_rq[entity->priority];

提交任務

static int amdgpu_cs_submit(struct amdgpu_cs_parser *p, union drm_amdgpu_cs *cs)                
{   
    struct drm_sched_entity *entity = p->entity;									/* 1 */
    struct amdgpu_job *job;
    job = p->job;																								
    drm_sched_job_init(&job->base, entity, &fpriv->vm);								/* 2 */
    	drm_sched_entity_select_rq(entity)
    	sched = entity->rq->sched;
    	job->sched = sched;
    	job->entity = entity;
    	job->s_priority = entity->rq - sched->sched_rq;

    drm_sched_entity_push_job(&job->base, entity);									/* 3 */
    ......
}
1. 獲取解析器中之前初始化的調度實體以及任務
2. 初始化調度器要用到的job,drm_sched_job,它的核心任務是設置任務的調度器,調度實體,以及任務優先機,同時還會重新爲調度實體選擇
合適的運行隊列
3. 將調度job添加到調度實體的job隊列中,之後job的選擇和執行就交給調度器了

內核線程初始化

  • GPU的調度器以內核線程的形式存在於GPU IP核的ring buffer上,因此內核線程的創建是在IP核初始化的時候,我們選取GPU的GFX IP分析,它的初始化函數是gfx_v10_0_gfx_ring_init
gfx_v10_0_gfx_ring_init
	 sprintf(ring->name, "gfx_%d.%d.%d", ring->me, ring->pipe, ring->queue)			/* 1 */
	 amdgpu_ring_init													
	 	amdgpu_fence_driver_init_ring
			drm_sched_init(&ring->sched, &amdgpu_sched_ops,							/* 2 */
                   			num_hw_submission, amdgpu_job_hang_limit,
                   			timeout, ring->name);
				sched->ops = ops;												
    			sched->hw_submission_limit = hw_submission;							
    			sched->name = name;												
    			sched->timeout = timeout;											
    			sched->hang_limit = hang_limit;			
    			INIT_DELAYED_WORK(&sched->work_tdr, drm_sched_job_timedout);	 	/* 3 */						
				sched->thread = kthread_run(drm_sched_main, sched, sched->name)		/* 4 */
				
const struct drm_sched_backend_ops amdgpu_sched_ops = {			
    .run_job = amdgpu_job_run,														/* 5 */
    .timedout_job = amdgpu_job_timedout,											/* 6 */
};
1. 設置ring buffer名字,這個名字也是調度器內核線程的名字
2. 初始化內核調度器
3. 初始化內核工作隊列,用戶處理任務調度超時的情況
4. 啓動調度器內核線程
5. 調度器運執行任務時調用的回調函數
6. 當調度器執行任務超時,調用的超時處理回調函數 

內核線程任務調度

static int drm_sched_main(void *param)
{               
    struct sched_param sparam = {.sched_priority = 1};				/* 1 */       
    sched_setscheduler(current, SCHED_FIFO, &sparam);	
                
    while (!kthread_should_stop()) {
        wait_event_interruptible(sched->wake_up_worker,				/* 2 */
                     (cleanup_job = drm_sched_get_cleanup_job(sched)) ||
                     (!drm_sched_blocked(sched) &&
                      (entity = drm_sched_select_entity(sched))) ||		
                     kthread_should_stop());
                     
        sched_job = drm_sched_entity_pop_job(entity);				/* 3 */

        atomic_inc(&sched->hw_rq_count);							/* 4 */
        fence = sched->ops->run_job(sched_job);						/* 5 */
		......
}
1. 設置內核線程的調度策略爲先入先出,使用的是實時的調度類,比完全公平調度類的優先級要高,並且這個內核線程在一個調度週期內如果沒有
執行完是不會被打斷的,保證了其執行任務的連續性
2. 在沒有任務的情況下,內核線程通常睡在等待隊列wake_up_worker上,當有任務到達的時候會被喚醒或者條件滿足的時候被喚醒
3. 從調度實體的job隊列中取出一個job,準備執行
4. 執行之前將hw_rq_count計數器加1
5. 運行任務,對於amdgpu驅動,對應的回調函數是amdgpu_job_run
  • 調度實體的選擇在drm_sched_select_entity中實現,繼續分析
drm_sched_select_entity
	drm_sched_ready																		/* 6 */
		
    /* Kernel run queue has higher priority than normal run queue*/
    for (i = DRM_SCHED_PRIORITY_MAX - 1; i >= DRM_SCHED_PRIORITY_MIN; i--) {			/* 7 */
        entity = drm_sched_rq_select_entity(&sched->sched_rq[i]);
        if (entity)  
            break;
	}
6. 在選擇調度實體前判斷是否滿足條件,如果當前執行的任務小於允許執行的任務數上限,才滿足條件,否則選取調度實體爲空,不運行任務
7. 根據調度實體的優先級,從高到低,從優先級對應的運行隊列中選取合適的調度實體,返回
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章