AMD GPU任務調度(3) —— fence機制

硬件基礎

  • AMD GPU的fence機制標記一個GPU pipeline的事件,用於CPU與GPU之間的同步。當CPU寫入一個fence後,所有與該fence相關的GPU IP核,其上的Ring Buffer cache都會被flush。fence提供一種機制用於保證用戶態程序下發地渲染命令可以順序地被執行,從而保證應用程序渲染相關數據的一致性。fence機制的硬件基礎是AMD GPU提供的EOP(end-of-pipie event)渲染命令。

command format

  • CPU與GPU通過Ring Buffer實現渲染命令的提交,渲染命令作爲一個packet被提交到Ring Buffer上,AMD規定packet的格式,它被分爲兩部分,Header和IT_BODY(information body),如下圖所示:
    在這裏插入圖片描述
  • packet的HEADER最高2bit代表packet的類型,AMD將packet分爲了3類,分別是0,2,3。每類packet的頭部除了高2bit代表類型以外其它字段與packet類型相關,其中type-0和type-2是用作寫寄存器的packet,type-3用做發送渲染命令以及一些特殊操作的命令,比如IB和Fence操作。type-3 packet的格式如下:
    在這裏插入圖片描述
  • HEADER由4個字段組成,如下:
  1. PREDICATE:待分析
  2. IT_OPCODE:操作碼,用於描述具體的渲染命令類型或者特殊操作,比如Indirect Buffer和事件觸發操作
  3. COUNT:描述IT_BODY的大小的字段,4字節爲單位。它的值爲IT_BODY長度減1。
  4. TYPE:packet類型,這裏是3
  • AMD驅動中有如下宏定義type-3類型的packet:
#define		PACKET_TYPE3	3
#define 	PACKET3(op, n)	((PACKET_TYPE3 << 30) |	 (((op) & 0xFF) << 8) |	 ((n) & 0x3FFF) << 16)
#define		PACKET3_EVENT_WRITE_EOP				0x47

/* 組裝一個操作碼爲PACKET3_EVENT_WRITE_EOP的type-3類型的HEADER,並設置其IT_BODY長度爲5 DWORD */
PACKET3(PACKET3_EVENT_WRITE_EOP, 4)			

end-of-pipe event

  • EOP(end-of-pipe)作爲一個特殊的渲染操作,它的主要功能是在GPU的硬件模塊flush cache之後,往CPU可以訪問的一個內存地址處寫入時間戳或者EOP packet定義的特殊序號。通過這樣的方式,就可以讓CPU感知到相應的處理已經完成,從而實現同步。AMD GPU定義了一個type-3 packet專門用於實現fence機制,packet格式如下:
    在這裏插入圖片描述
  • fence的packet包主要用來指示GPU在end-of-pipe之後應該在哪個內存空間,寫入什麼值,因此packet中有兩個最主要的信息是序列號和地址,GPU在end-of-pipe之後會根據這兩個信息產生寫內存事件或者中斷。下面依次介紹每個字段含義:
  1. HEADER:packet包頭部,IT_OPCODE爲event_write_eop(0x47),COUNT爲4。
  2. event initiator:GPU產生事件是寫入VGT_EVENT_INITIATOR寄存器的值
  3. event type:事件類型,GPU提供了幾種事件類型,包括Cache Flush,Cache Flush And Tnval,Bottom Of Pipe等,通常情況下選擇第二種,在cach flush並且使無效之後產生事件
  4. address_lo/address_hi:事件產生時寫入的內存地址
  5. interrupt select:選擇以何種方式產生事件,包括Send Interrupt Only,Send Interrupt when Write Confirm is received from the MC
  6. data select:產生事件時發送的數據長度
  7. data_lo/data_hi:事件產生時要寫的數據
  • AMD fence機制的常用場景是每次CPU提交渲染命令之後,發送一個fence到GPU,同時啓動一個定時器每隔一段事件查詢一次EOP中給出的內存地址是否被填入了指定的數據。之後,就可以處理其它事情了。在定時器處理例程中如果查詢到內存地址被填入指定數據,說明渲染命令已經處理完成。CPU根據這個信息,就可以繼續提交新的渲染命令或者處理一些同步相關事情了。下面是amdgpu驅動中一段填充fence信息到Ring Buffer的操作:
amdgpu_ring_write(ring, PACKET3(PACKET3_EVENT_WRITE_EOP, 4));			/* 1 */
amdgpu_ring_write(ring, (EOP_TCL1_ACTION_EN |							/* 2 */
			 EOP_TC_ACTION_EN |
			 EOP_TC_WB_ACTION_EN |
			 EVENT_TYPE(CACHE_FLUSH_AND_INV_TS_EVENT) |
			 EVENT_INDEX(5)));
amdgpu_ring_write(ring, addr & 0xfffffffc);								/* 3 */	
amdgpu_ring_write(ring, (upper_32_bits(addr) & 0xffff) |				/* 4 */
		  DATA_SEL(write64bit ? 2 : 1) | INT_SEL(int_sel ? 2 : 0));
amdgpu_ring_write(ring, lower_32_bits(seq));							/* 5 */
amdgpu_ring_write(ring, upper_32_bits(seq));
1. 組裝HEADER,將IT_OPCODE和COUNT填入HEADER中
2. 設置event的類型
3. 寫入內存地址,因爲內存地址是雙字爲單位,4字節對齊的,因此低2位會設置爲0
4. 寫入餘下的內存地址,同時設置數據發送的位數已經中斷產生方式
5. 填入要往內存地址寫入的數據

軟件基礎

問題

  • 設想這樣一個場景,用戶態應用往Ring Buffer提交渲染命令,GPU設備着手處理,渲染命令提交完成後返回到用戶態空間,GPU異步執行渲染操作。此時用戶態應用繼續處理其它事務,它下發ioctl命令讓顯示設備輸出剛剛發給GPU渲染的那一幀畫面,通常的做法是,這個ioctl命令陷入內核態等待GPU渲染完成,獲取渲染buffer的地址,然後將其設置爲顯示設備的掃描地址,最後返回用戶態空間。整個過程可以歸納爲向GPU發送渲染數據,然後讓顯示設備輸出,更高效的做法是在內核空間一次性將這兩個事情做完,這樣就可以節省一次內核態與用戶態上下文切換的開銷。出現這種問題的本質是內核態缺乏一種機制,沒法讓兩個硬件單元(GPU和display)同步數據,因此只能返回用戶態,讓用戶態程序來負責這兩個事情,間接完成數據同步。
  • dma-fence的設計就是用於解決這類問題,它用來實現多個硬件之間共享buffer的同步,同樣是上面的場景,當用戶態應用往Ring Buffer提交渲染命令後,將GPU處理的渲染buffer共享給顯示設備,同時將一個dma-fence關聯到這個buffer上並往dma-fence添加一個回調函數,當GPU處理完buffer之後,觸發buffer關聯的dma-fence上的回調,回調函數中,就可以將渲染buffer的地址設置爲顯示設備的掃描地址,完成原本用戶態應用負責的事情。
  • 對於amdgpu來說,一個Ring Buffer就是一個共享buffer,它上面可以關聯dma-fence,當GPU執行完Ring Buffer上的渲染命令時,通過附加在buffer上面的dma-fence,就可以通知其它硬件模塊並觸發相應回調函數

dma-fence

struct dma_fence {
	const struct dma_fence_ops *ops;									/* 1 */
	union {
		struct list_head cb_list;										/* 2 */
		/* @cb_list replaced by @timestamp on dma_fence_signal() */
		ktime_t timestamp;
		/* @timestamp replaced by @rcu on dma_fence_release() */
		struct rcu_head rcu;
	};
	u64 context;														/* 3 */
	u64 seqno;															/* 4 */
	unsigned long flags;												/* 5 */
	struct kref refcount;												/* 6 */
}
1. 定義dma-fence等待,釋放,完成等操作
2. 維護該dma-fence完成後需要觸發的回調函數
3. fence所在上下文,同一個上下文可以有多個fence,通過序列號seqno區分,每個fence有唯一一個序列號不同。在不同上下文中fence序列號可以相
同,fence序列號只在同一個上下文起到區分不同fence的作用。對於amdgpu這種應用場景,一個fence上下文就是一個Ring Buffer,一個fence通常關聯
到這個Ring Buffer上的一個任務
4. fence關聯的序號,當amdgpu驅動提交一個任務到Ring Buffer上時,就增加這個序列號並分配一個dma-fence關聯到該任務,用於跟蹤任務是否完成
5. 用於標記該fence的狀態,當fence完成時會被標記爲DMA_FENCE_FLAG_SIGNALED_BIT,表示fence已經完成,其它對該fence感興趣的模塊會據此執行相應的事務
6. 該fence的引用計數,可以有多個硬件模塊或者軟件驅動引用同一個fence

amdgpu-fence

  • amdgpu驅動使用內核提供的dma-fence機制實現Host驅動和GPU的同步,amdgpu驅動將dma-fence附加到一個GPU IP的Ring Buffer上,因此封裝了一個amdgpu_fence結構體用於將dma-fence與Ring Buffer關聯起來。注意,一個Ring Buffer上可以包含若干個fence,因此多個amdgpu_fence的ring成員可能指向同一個環。
struct amdgpu_fence {
	struct dma_fence base;			/* Ring Buffer上的dma-fence */

	/* RB, DMA, etc. */
	struct amdgpu_ring		*ring;	/* amdgpu fence所在的Ring Buffer,amdgpu fence與amdgpu ring是多對一的關係 */
};

dma_fence_ops

  • 內核的dma-fence只提供框架,可以讓內核其它模塊定製自己的實現,包括如何等待fence完成,fence完成時如何觸發相應的回調,fence如何被釋放等等,這個定製接口通過定義如下結構體dma_fence_ops來實現
struct dma_fence_ops {
    bool (*enable_signaling)(struct dma_fence *fence);			/* 使能fence完成操作 */
    bool (*signaled)(struct dma_fence *fence);					/* 向fence發信號,標記fence完成,通常這是在硬件fence完成時中斷處理中實現 */
    signed long (*wait)(struct dma_fence *fence,				/* 等待fence完成 */
                bool intr, signed long timeout);
    void (*release)(struct dma_fence *fence);					/* 釋放fence */
	......
}
/* amdgpu 驅動定製的fence接口 */
static const struct dma_fence_ops amdgpu_fence_ops = {
	.get_driver_name = amdgpu_fence_get_driver_name,
	.get_timeline_name = amdgpu_fence_get_timeline_name,
	.enable_signaling = amdgpu_fence_enable_signaling,
	.release = amdgpu_fence_release,
};

amdgpu_fence_driver

struct amdgpu_fence_driver {
	uint64_t			gpu_addr;									/* 1 */
	volatile uint32_t		*cpu_addr;								/* 2 */
	/* sync_seq is protected by ring emission lock */
	uint32_t			sync_seq;									/* 3 */
	atomic_t			last_seq;									/* 4 */
	struct amdgpu_irq_src		*irq_src;							/* 5 */
	unsigned			irq_type;
	struct timer_list		fallback_timer;							/* 6 */
	unsigned			num_fences_mask;							/* 7 */
	struct dma_fence		**fences;								/* 8 */
};
1. GPU可訪問的內存地址,該地址是GPU產生fence事件時將序列號寫入的地址,GPU寫gpu_addr地址,CPU從cpu_addr地址讀出值
2. fence機制中gpu地址對應的cpu地址,驅動側通過讀取這個地址的內容,與序列號比較,如果相同,說明GPU fence事件產生
3. GPU往gpu_addr寫入的序列號,單調遞增。初始值爲0,CPU每執行一個job,就會往GPU ring上發送一個fence,序列號就加1。fence的序列號用來
區分不同job。假設CPU往ring buffer上先後放了兩個job,此時產生了GPU產生了end-of-pipe中斷,CPU的中斷處理函數例程中可以通過序列號區分是哪 
個job對應的渲染命令被GPU處理完成。
4. 從cpu_addr中取出的GPU寫入的序列號
5. 待分析
6. 定時器,用於每隔一段事件查詢是否有fence被通知(signaled)
7. fence個數掩碼,用於查詢fences數組中的dma_fence,使其不越界
8. fences數組,調度器每提交一個job,相應地會增加fence的序列號,同時會生成一個dma_fence對象放到fences數組中。

API

amdgpu_fence_driver_init_ring

  • 初始化一個GPU IP上Buffer Ring的fence_driver,每個Buffer Ring上維護這一個fence driver,它用來記錄提交到Ring Buffer上的job的完成情況。爲什麼要用這個才能記錄?因爲CPU提交渲染到Ring Buffer讓GPU執行是異步,只能通過fence機制來確認job的完成情況。fence driver爲上層渲染命令的同步提供底層基礎,否則上層是無法知道渲染命令是否完成,當上層應用下發的渲染命令前後有依賴時,無法保證順序
int amdgpu_fence_driver_init_ring(struct amdgpu_ring *ring,
                  unsigned num_hw_submission)
{       
    ring->fence_drv.sync_seq = 0;												/* 1 */
    atomic_set(&ring->fence_drv.last_seq, 0);
    ring->fence_drv.initialized = false;         								/* 2 */
    timer_setup(&ring->fence_drv.fallback_timer, amdgpu_fence_fallback, 0);		/* 3 */
    ring->fence_drv.num_fences_mask = num_hw_submission * 2 - 1;				/* 4 */
    ring->fence_drv.fences = kcalloc(num_hw_submission * 2, sizeof(void *),		/* 5 */
                     GFP_KERNEL);
	......
}
1. 將當前序列號和上一次的序列號初始化爲0,sync_seq在真正發送fence的時候會加1,因此sync_seq從1開始,last_seq從0開始。
2. fence driver是否啓動,這裏設置爲false,fence driver在啓動start ring接口中設置爲true
3. 設置定時器,該定時器用於查詢是否有完成的fence。除了GPU IP核中斷例程中會處理完成的fence外,這裏註冊的定時器回調函數也會週期性查詢fence是否完成。
4. 設置fence個數的掩碼,amd gpu調度器中允許同時有num_hw_submission個任務提交到Ring Buffer。調度器每提交一個任務,至少要發送一個fence用於跟蹤當前任務是否完成。同時在提交任務之前,可能刷GPU的緩存,這時也會發送一個fence用於跟蹤當前緩存是否刷新完成,因此一個任務最多可能會有2個fence發送。所以設置的fence個數掩碼爲num_hw_submission * 2 - 1。注意,num_hw_submission是2的冪級數。
5. 爲fence driver的fences結構體分配空間,個數爲num_hw_submission的2倍

amdgpu_fence_driver_start_ring

  • fence driver的初始化主要完成基本成員的賦初值,但關鍵的gpu和cpu地址沒有設置,這個設置在start ring中完成。
int amdgpu_fence_driver_start_ring(struct amdgpu_ring *ring,
                   struct amdgpu_irq_src *irq_src,
                   unsigned irq_type)
{       
    ring->fence_drv.cpu_addr = &adev->wb.wb[ring->fence_offs];						/* 1 */
    ring->fence_drv.gpu_addr = adev->wb.gpu_addr + (ring->fence_offs * 4);
    
    amdgpu_fence_write(ring, atomic_read(&ring->fence_drv.last_seq));				/* 2 */
    amdgpu_irq_get(adev, irq_src, irq_type);
    
    ring->fence_drv.irq_src = irq_src;
    ring->fence_drv.irq_type = irq_type;
    ring->fence_drv.initialized = true;												/* 3 */
    ......
}
1. 設置fence driver的gpu和cpu地址
2. 將last_seq序列號寫入fence driver的cpu地址中,由於last_seq初始值爲0,因此cpu地址中讀出的序列號爲0,在提交任務時,當前序列號sync_seq會
先加1再發送fence,中斷處理函數和定時器函數中通過判斷sync_seq與last_seq是否相等,來判斷是否有任務在執行或者有flush正在刷新
3. 將fence driver的初始化設置爲true

amdgpu_fence_emit

  • 向GPU發送一個fence,具體實現是往Ring Buffer上填入fence內容。真正地發送在設置Ring Buffer寫偏移時觸發。
int amdgpu_fence_emit(struct amdgpu_ring *ring, struct dma_fence **f,
		      unsigned flags)
{
	struct amdgpu_fence *fence;
	fence = kmem_cache_alloc(amdgpu_fence_slab, GFP_KERNEL);						/* 1 */

	seq = ++ring->fence_drv.sync_seq;												/* 2 */
	fence->ring = ring;																/* 3 */
	dma_fence_init(&fence->base, &amdgpu_fence_ops,
		       &ring->fence_drv.lock,
		       adev->fence_context + ring->idx,
		       seq);
	amdgpu_ring_emit_fence(ring, ring->fence_drv.gpu_addr,							/* 4 */
			       seq, flags | AMDGPU_FENCE_FLAG_INT);
			       
	ptr = &ring->fence_drv.fences[seq & ring->fence_drv.num_fences_mask];			/* 5 */
	if (unlikely(rcu_dereference_protected(*ptr, 1))) {							
		struct dma_fence *old;

		old = dma_fence_get_rcu_safe(ptr);
		dma_fence_wait(old, false);													/* 6 */
		dma_fence_put(old);															/* 7 */
	}

	rcu_assign_pointer(*ptr, dma_fence_get(&fence->base));							/* 8 */
	......
}
1. 爲amdgpu-fence結構體分配內存
2. 增加序列號,每發送一次fence,序列號自增一次
3. 設置fence所在的環
4. 往Ring Buffer中填入fence信息,即之前介紹的EOP packet
5. 取出序列號在fence數組中對應元素,判斷是否爲空,如果爲空,將該元素指向新分配的fence。通常情況下,fence數組對應位置不會存在fence,如果存在,我們認爲它是之前未完成的fence,我們等待fence完成,然後再釋放老的fence。
6. 等到老的fence完成
7. 完成後釋放老的fence,如果是最後一個fence的引用被釋放,則刪除老的fence
8. 設置fence數組對應元素指向即將發送的新fence。

dma_fence_add_callback

  • fence被髮送後,發送fence的線程就可以轉而去處理其它事務,當fence完成時會觸發相應的回調函數,一個dmp-fence可以註冊多個回調函數,但一個回調函數只能註冊到一個dmap-fence上,即dma-fence與callback時一對多的關係。amdgpu驅動中fence的回調函數註冊在dma_fence_add_callback中實現,如下:
int dma_fence_add_callback(struct dma_fence *fence, struct dma_fence_cb *cb,
			   dma_fence_func_t func)
{
	if (test_bit(DMA_FENCE_FLAG_SIGNALED_BIT, &fence->flags)) {		/* 1 */
		INIT_LIST_HEAD(&cb->node);
		return -ENOENT;
	}

	if (__dma_fence_enable_signaling(fence)) {						/* 2 */
		cb->func = func;
		list_add_tail(&cb->node, &fence->cb_list);
	}
	......
}
1. 檢測fence是否已經完成,如果已經完成,直接返回,調用者跳過回調函數的註冊,直接執行就可以了
2. 判斷fence是否註冊了觸發fence完成的函數,如果有,滿足條件,將回調函數添加到dma-fence的回調函數鏈表中。

amdgpu_fence_process

  • 當GPU完成渲染任務後,會觸發EOP中斷,amdgpu在中斷處理函數中會比較EOP packet中填入的序列號和GPU往GPU地址空間寫入的值是否相同,如果相同,說明GPU完成了渲染操作或者完成的flush cache的操作,過程如下:
bool amdgpu_fence_process(struct amdgpu_ring *ring)
{
	struct amdgpu_fence_driver *drv = &ring->fence_drv;
	uint32_t seq, last_seq;

	do {
		last_seq = atomic_read(&ring->fence_drv.last_seq);								/* 1 */
		seq = amdgpu_fence_read(ring);												

	} while (atomic_cmpxchg(&drv->last_seq, last_seq, seq) != last_seq);				/* 2 */

	if (unlikely(seq == last_seq))														/* 3 */
		return false;

	last_seq &= drv->num_fences_mask;													/* 4 */
	seq &= drv->num_fences_mask;

	do {
		struct dma_fence *fence, **ptr;

		++last_seq;																		/* 5 */
		last_seq &= drv->num_fences_mask;
		ptr = &drv->fences[last_seq];

		/* There is always exactly one thread signaling this fence slot */
		fence = rcu_dereference_protected(*ptr, 1);
		RCU_INIT_POINTER(*ptr, NULL);
		dma_fence_signal(fence);									/* 6 */
		dma_fence_put(fence);
	} while (last_seq != seq);
	......
}
1. 取出上一次fence的序列號last_seq和這一次fence的序列號seq
2. 用這一次fence的序列號更新上次任務的序列號,原子操作
3. 如果兩次fence的序列號相等,說明當前沒有fence完成,直接返回false,表示fence還處在未完成階段
4. 計算序號在fences數組中的索引
5. 逐次增加last_seq,從fences數組中取出對應的dma-fence,更新fence的狀態,最終觸發fence上的回調,直到last_seq等於seq。在fence完成的回調
處理中,多個fence可能合併一次性處理。
6. 更新fence的狀態爲完成(signaled)
7. 釋放fence

應用舉例

  • GPU調度器通過fence機制爲上層應用提供了不同硬件之間數據的同步方法,GPU調度器本身也通過fence機制跟蹤任務的完成情況。下面通過分析GPU調度器執行任務的過程,進一步理解fence機制

調度任務

  • 調度器按從高到低的順序,依次從對應的運行隊列中選擇合適的調度實體,取出調度實體的任務,將任務關聯IB包含的渲染命令提交到Ring Buffer上。調度任務的執行在drm_sched_main函數中實現,GPU調度器利用fence跟蹤渲染任務是否執行完,還可以利用fence機制註冊回調函數,讓GPU渲染任務執行完成後觸發任務相關的收尾工作。處理其中fence相關的操作如下:
static int drm_sched_main(void *param)
{
	while (!kthread_should_stop()) {
		......
		fence = sched->ops->run_job(sched_job);							/* 1 */		

		if (!IS_ERR_OR_NULL(fence)) {
			r = dma_fence_add_callback(fence, &sched_job->cb,			/* 2 */
						   drm_sched_process_job);
			if (r == -ENOENT)
				drm_sched_process_job(fence, &sched_job->cb);			/* 3 */
			dma_fence_put(fence);										/* 4 */
		}
		......
	}
}
1. 執行渲染任務,該動作的實質是將渲染命令放到Ring Buffer上,然後讓GPU異步處理,run_job返回一個fence,上層通過此fence註冊回調函數
2. 往fence上註冊回調函數,當該fence完成時,執行drm_sched_process_job函數
3. 如果此時fence已經完成,說明GPU已經將渲染任務完成,不再需要註冊回調,直接運行回調函數即可
4. 釋放fence

觸發回調

  • 從上面的流程中可以看到,渲染任務執行完了會觸發drm_sched_process_job回調,該函數會將調度相關的fence設置爲完成,從而鏈式地觸發其它監聽模塊的回調,具體如下:
static void drm_sched_process_job(struct dma_fence *f, struct dma_fence_cb *cb)
{
	struct drm_sched_job *s_job = container_of(cb, struct drm_sched_job, cb);
	struct drm_sched_fence *s_fence = s_job->s_fence;						/* 獲取job關聯的dma-fence */

	dma_fence_get(&s_fence->finished);										/* 引用finished dma-fence */
	drm_sched_fence_finished(s_fence);										/* 通知監聽finished dma-fence的其它模塊,job已經處理完成,可以觸發對應回調了 */
	dma_fence_put(&s_fence->finished);										/* 釋放finished dma-fence */
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章