DAY 84:閱讀 Driver API和CUDA Context

我們正帶領大家開始閱讀英文的《CUDA C Programming Guide》,今天是第84天,我們正在講解Driver API,希望在接下來的16天裏,您可以學習到原汁原味的CUDA,同時能養成英文閱讀的習慣。

關注微信公衆號,可以看到之前的閱讀

本文共計879字,閱讀時間30分鐘

I. Driver API

This appendix assumes knowledge of the concepts described in CUDA C Runtime.

The driver API is implemented in the cuda dynamic library (cuda.dll or cuda.so) which is copied on the system during the installation of the device driver. All its entry points are prefixed with cu.

It is a handle-based, imperative API: Most objects are referenced by opaque handles that may be specified to functions to manipulate the objects.

The objects available in the driver API are summarized in Table 15.

Table 15. Objects Available in the CUDA Driver API

The driver API must be initialized with cuInit() before any function from the driver API is called. A CUDA context must then be created that is attached to a specific device and made current to the calling host thread as detailed in Context.

Within a CUDA context, kernels are explicitly loaded as PTX or binary objects by the host code as described in Module. Kernels written in C must therefore be compiled separately into PTX or binary objects. Kernels are launched using API entry points as described in Kernel Execution.

Any application that wants to run on future device architectures must load PTX, not binary code. This is because binary code is architecture-specific and therefore incompatible with future architectures, whereas PTX code is compiled to binary code at load time by the device driver.

Here is the host code of the sample from Kernels written using the driver API:

Full code can be found in the vectorAddDrv CUDA sample.

I.1. Context

A CUDA context is analogous to a CPU process. All resources and actions performed within the driver API are encapsulated inside a CUDA context, and the system automatically cleans up these resources when the context is destroyed. Besides objects such as modules and texture or surface references, each context has its own distinct address space. As a result, CUdeviceptr values from different contexts reference different memory locations.

A host thread may have only one device context current at a time. When a context is created with cuCtxCreate(), it is made current to the calling host thread. CUDA functions that operate in a context (most functions that do not involve device enumeration or context management) will return CUDA_ERROR_INVALID_CONTEXT if a valid context is not current to the thread.

Each host thread has a stack of current contexts. cuCtxCreate() pushes the new context onto the top of the stack. cuCtxPopCurrent() may be called to detach the context from the host thread. The context is then "floating" and may be pushed as the current context for any host thread. cuCtxPopCurrent() also restores the previous current context, if any.

A usage count is also maintained for each context. cuCtxCreate() creates a context with a usage count of 1. cuCtxAttach() increments the usage count and cuCtxDetach() decrements it. A context is destroyed when the usage count goes to 0 when calling cuCtxDetach() or cuCtxDestroy().

Usage count facilitates interoperability between third party authored code operating in the same context. For example, if three libraries are loaded to use the same context, each library would callcuCtxAttach() to increment the usage count and cuCtxDetach() to decrement the usage count when the library is done using the context. For most libraries, it is expected that the application will have created a context before loading or initializing the library; that way, the application can create the context using its own heuristics, and the library simply operates on the context handed to it. Libraries that wish to create their own contexts - unbeknownst to their API clients who may or may not have created contexts of their own - would use cuCtxPushCurrent() andcuCtxPopCurrent() as illustrated in Figure 21.

Figure 21. Library Context Management

本文備註/經驗分享:

今天這個章節是關於CUDA Driver API. 和大部分的人經常使用的簡化版本的CUDA Runtime API不同,CUDA還有另外一個功能更強大,當然使用起來也更麻煩的API接口。就是今天我們所說的Driver API. Driver API將完整的CUDA功能展現給用戶,實際上,我們之前所用到的CUDA Runtime API,只是構建在Driver API上的另外一層包裝而已。雖然底層的這個版本功能更加強大,但是更加繁瑣的用途限制了它的應用。所以你正常的日常生活中看到的總是runtime api。 而Driver API實際上類似於OpenCL。如果非要將使用難度列表的話,大致的 難度:OpenCL > CUDA (Drvier API) > CUDA (Runtime API) 所以你看,實際上還是比OpenCL容易一些的。 這也是我們爲何之前做一些培訓的時候,當底下的人鬧場,說,“我們今天要聽OpenCL”的時候,我們總是建議用戶從CUDA Runtime API開始的原因。簡單的東西能快速讓你用上GPU。需要更強的功能,則建議逐步遷移到更復雜的API上。 好了。那麼爲何要用Driver API? 既然總是存在一個更易用的Runtime API的情況下? 主要原因有這麼3點: (1)Runtime API太“C語言”化了: 特別是它引入的爲了方便使用的混合編譯(CPU上的C/C++代碼和GPU上的CUDA C代碼混合在一起編譯)。使得宿主的語言幾乎總是限定在C/C++上。 有的時候這點是無法忍受的,例如請想想一下一位VB用戶需要使用CUDA的時候,難道要直接告訴他,你用不了? 而有了Driver API,任何只要存在和C二進制接口兼容的語言(例如VB,C#,Go,Python,等等。)都可以使用CUDA。 實際上,因爲現在幾乎所有的語言都提供了到C的二進制接口(即:至少能載入C編譯生成的二進制靜態或者動態庫),所以學習使用Driver API,能將你的CUDA能力擴展到幾乎所有平臺),實際上,已經存在了很多C#,Go,Python等語言到CUDA的接口了。 (2)你需要更強的控制力。很多平臺支持二次開發,以往這些平臺或者軟件上的二次開發好的代碼,只能在CPU上運行。如果直接使用Runtime API的話,首先需要這些二次開發能target CUDA C,同時甚至還可能需要附帶上符合CUDA C和CUDA Runtime API的編譯器。 有的時候這個很難做到。而Driver API提供了更底層的接口,二次開發後,直接生成一種叫PTX的中間描述代碼(純文本格式的),就可以直接運行了。非常簡單。 (3)點則是很多軟件爲了更好的穩定性,需要使用Driver API。因爲Runtime API簡化掉了Context管理,雖然說易用了很多,但是也降低了穩定性:試想這樣一個場景,當用戶調用了多個第三方的庫,而該第三方的庫使用了GPU代碼,則如果這些庫是用Runtime API開發的,則一旦任何一個庫掛掉,都會影響到其他使用GPU代碼的,鏈接到本應用中的庫的。而使用Driver API。因爲Driver API具有CUDA Context管理功能,可以隔離在同一個GPU上運行的多個不同的上下文,一定程度能抵抗這種互相的干擾。甚至更重要的,一個雷達控制的GPU應用,安全和穩定性的管理非常重要。爲了避免本CPU進程中的一不小心某地方的kernel出錯,而導致整體應用掛掉,使用Driver API無可避免(因爲NV設計的時候,CUDA估計被設計的很脆弱,一旦runtime api中某步kernel出錯,整體默認當前設備將無法繼續運行,所有當前數據均將被自動銷燬。對高穩定性要求的程序來說,這個是不能忍受的。) 然後既然知道了Drvier API具有這些優點(以及,難用的缺點),用戶在下面的閱讀中,心裏需要有點數。我來根據本章節,簡單的描述一下幾個重要概念。(注意,本手冊中的Driver API部分只是一個簡單描述。想深入瞭解的用戶應當充分閱讀單獨的Driver API手冊). CUDA Driver API引入了這個幾個東西: (1)Context, Module, Function 這三個東西以前在Runtime API中都不存在。或者說存在,但是全自動的。 Context和Module,這兩個對應的是在Runtime API中,第一次調用任何常規Runtime API函數,所引入的那個初始化延遲。 一些Runtime API的用戶可能會發現,在第一次調用某些CUDA Runtime函數的時候,會自動的有相當大的初始化開銷。實際上這個開銷就是Runtime在爲你自動進行Context創建,Module載入之類的操作,只是這些操作中Runtime裏面是全自動的。如今在Driver API中,它們均必須需要用戶手工的建立載入等。但用戶也換來了在更方便的實際創建它們的靈活性。各有利弊。 而這裏的CUDA Function,實際上在Runtime API也不存在(或者說自動管理的),用戶只要書寫自己的函數的名字即可,然後用<<<>>>啓動。 如何啓動對應的這個名字的GPU kernel,在Runtime API上是全自動的。如今,也需要手工管理。 本章節的前面的部分有一段代碼,演示瞭如何使用driver api: 你會看到這樣幾個有意思的過程:

// Create context

CUcontext cuContext;

cuCtxCreate(&cuContext, 0, cuDevice);

// Create module from binary file

CUmodule cuModule;

cuModuleLoad(&cuModule, "VecAdd.ptx");

這兩個過程對來自Runtime API的用戶來說,是新鮮的。 第一部分是創建CUDA Context,這個以前是自動的,如今多出來了,需要手寫。 第二部分則是載入你的module,什麼是module?裏面含有了你需要用的靜態全局數據,也含有你的GPU Kernel代碼。 用戶需要手工的從文件,或者加密的網絡傳輸流,或者其他方面,得到GPU上的代碼,並將它載入到GPU中。 以前這些過程也不存在:你之前是GPU代碼自動嵌入在你的exe或者可執行文件中,不需要手工載入的。如今也需要手工載入了。而且這裏還需要有明確的PTX和CUBIN之分(這個下次說)。 雖然很麻煩,但也換來了好處。 例如一些需要很好的加密的軟件,可以將自己的GPU部分代碼(kernel代碼),放置到一個授權服務器,或者需要登錄的服務器上,只有當有正確的用戶名密碼或者權限後,實際的GPU kernel,纔會自動的從服務器上傳輸過來(通過加密的網絡流例如),然後再會被加載到GPU中,然後纔會可能執行。 這種比直接GPU Kernel嵌入在exe中之類的有時候要更好更安全。例如用戶購買了3個月的授權,第4個月開始,服務器可以拒絕提供GPU Kernel。 類似這樣的靈活性都是以前的Runtime API所沒有的。 再往下看這些代碼。這裏一段也很有意思:

這段只相當於以前的Runtime API的一句話: VecAdd<<<>>>() 但是這裏要明確的獲取kernel的指針,然後通過指針來啓動kernel。請注意這個雖然繁瑣了很多,但已經是CUDA Driver API經過簡化後的版本了,在2010年的時候,CUDA Driver API(在CUDA 3.2的時候)進行了一次化簡,引入了一些不太兼容的改動,你今天看到的這樣麻煩的Driver API代碼,已經是簡單好多的了。但是有失就有得,現在用戶可以方便的將kernel指針在自己的代碼中進行傳遞,甚至對kernel的簽名進行描述,進行很多靈活的多的調用方式的。 還是很方便的。 這是今天的章節的綜合描述部分。 用戶可以看到,以前的最簡單的代碼,現在都需要用戶自己來。 但這種操作卻可能帶來靈活性的多的應用領域。然後具體的這裏面需要用戶手工操作的概念,我們需要分成好幾天來說明。

今天會說明一個基本的概念:CUDA Context,也就是本章節的後半部分。 首先說,這裏引入了一個“用戶不透明類型”的概念。這個概念相當於Win32 API中的句柄(HANDLE),或者用戶可以直接理解成void *,也就是說用戶不需要知道維護一個(例如CUDA Context)的類型具體是什麼東西,只需要知道它存在即可。 例如CUDA Context只是一個CUcontext,具體的context是什麼結構的,用戶不需要知道。只要會用即可。(NV故意不告訴你) 回到正題,你看到現在很多類型是用CU或者很多函數是小寫cu開頭的,這是driver api的一個特點: 所有的RUNTIME API,都是4個字母cuda開頭;而所有的Driver API,則都是2個字母cu開頭。這樣用戶可以快速區分到自己在用什麼(特別是有一些技巧允許你混用driver和runtime api的時候),至於以前用戶天天問,cutil開頭的是什麼?這些以前我們的NV上海公司寫的例子時候所用的開頭,並非正式API! 請不要使用它們。 同時這些代碼也總是容易引發一些問題,好在現在的CUDA附贈的例子中,含有這些開頭的代碼已經被刪除了。你如果在維護老代碼的時候看到它們,請儘量去除它們。這不是正式的API! 然後回到CUDA Context上。這裏上去說明,這等價於CPU上的進程。這種解釋其實讓很多用戶感到迷惑。因爲並不是很多人知道一個基本操作系統概念:進程是資源分配的單位,線程是調度執行的單位的。所以這裏我明確的說一下,CUDA Context是一種從一張具體的GPU上,切分下來的一部分資源。 在同一個GPU上,可能同時存在1個或者多個CUDA Context的。每個Context之間是隔離的,在一個Context中有效的東西(例如某個指針,指向一段顯存;或者某個紋理對象),只能在這一個Context中使用。它在另外一個Context上(哪怕是同一個GPU的另外一個Context上)是不能用的。這種是CUDA Context之間的隔離性。好處是一個Context掛掉後,不會影響另外一個Context裏的東西。因爲實際上我們總是在使用多卡,現在的日子。 實際上一個應用中執行的過程它,如果是在多卡平臺上,它(使用了Driver API後)可能會創建多個CUDA Context的,有N張GPU上,每張GPU只有1個Context的情況;也有1張GPU上,存在N個CUDA Context的情況;還會有N張GPU上,存在M個CUDA Context的情況。這些Context上,正常情況下,資源都是互相隔離的。(什麼時候是可以互相共享的,後面再說) 然後這段裏還提出了一個隱藏參數的概念,還記得剛纔我說過,CUDA Driver API雖然要比CUDA Runtime API簡單,但依然要比OpenCL複雜的話嗎? 很多時候,很多函數操作需要一個Context對象,爲了簡化用戶每次調用的時候,指定一個context參數的麻煩,

CUDA Driver API提出了一個當前context的概念,這個當前context,不需要每次調用的時候指定,只需要設定好,就可以連續使用。方便了很多。來自Runtime API用戶一定程度的可以將它等效成cudaSetDevice, 注意這是一個cuda開頭的runtime api函數,(但是存在1張卡上多個Context的例外情況,這裏只是一種大致等效)。 這種設定好了就能用的方式,簡化了不少調用的麻煩。但也帶來了這個隱藏的概念的開銷。用戶需要知道如何設定context爲當前context,以及設定後對具體某個cpu線程有效。本段落裏簡單的提到了有Set/Get方式設定,Push/Pop方式設定。前者適合你有明確的對象的時候使用(例如都是自己的代碼),後者往往適合對象指向不明的時候(例如一個庫代碼,可以直接pop掉自己的context,返回調用者的CUDA Context)使用。但這不是絕對的。 注意本段落的這些只是讓你有一些概念性的東西,具體它們怎麼用,這裏沒說,你需要參考隨着CUDA自帶的Driver API手冊去看它們,Driver API的Context章節。這章節大約不到20頁吧。 也可以參考自帶的例子中,含有drv字樣的代碼去看看。這些都是入門的第一手資料(沒錯。你看到的其他除了NV爲你提供的手冊和例子外的東西,都是別人根據前面這兩個給你總結出來的二手貨),然後這裏還有一個引用計數的概念,這個東西大家可能都比較熟悉,可以直接看一下本章節的描述。 此外,本章節沒說,但需要強調的是,一個CUDA Context中的任何一個kernel,掛掉後,則整個Context中的所有東西都會失效(例如所有的緩衝區,kernel對象,紋理對象,stream等等),這點需要注意。 高度可靠性的東西應當使用CUDA Context隔離開(也就是建立多個Context,一個掛了另外一個不影響),甚至有的時候需要使用多卡,在另外一張卡上建立Context,以便將可能的影響降低到最低。大致這樣。更多內容我們後續繼續說。 需要對用戶說明的是,因爲CUDA Driver API是先有的(2008甚至更早就有了),而OpenCL是後起的,所以你看到OpenCL和Driver API很多地方這麼像,並非前者抄襲後者,很多人對我們說,CUDA Driver API抄襲OCL,這很讓人啼笑皆非。(類似的說法還有BSD是抄襲自Linux的) 請注意出現的前後時間關係。但無論是否抄襲,這種相似性,帶來了用戶遷移的成本降低。如果一家單位的代碼,必須從CUDA遷移到OpenCL。 我建議走這樣的一個漸進的流程: 從CUDA Runtime API -> Driver API -> OCL 這樣一定程度的能帶來成本的降低。實際上,在學習完Driver API後,你基本上OCL也會了,太多的東西是類似的。

有不明白的地方,請在本文後留言

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章