使用 Python 編寫 CUDA 核心的 1,001 種方法 [S72449] ![image](https://hackmd.io/_uploads/SJw81Ch2yx.png) Leo Fang,Python CUDA 技術主管, NVIDIA 您必須編寫一個 CUDA 核心。你知道不離開 Python 就可以做到這一點嗎?我們將探索使用 Python 編寫 CUDA 核心的最佳實踐,使開發人員能夠充分利用 GPU 加速的潛力。清楚了解 CUDA 核心的結構和功能,學習如何在 Python 應用程式中有效地實現它們。我們將介紹實現高效能的關鍵概念,包括記憶體管理、執行緒組織和最佳化技術。 重要提示:由於座位數量有限,強烈建議提前到達。與會者將以先到先得的原則入場。 關鍵要點: 探索 CuPy、Numba、cuda.cooperative 和 nvmath.device 等技術,讓您能夠以 Python 原生方式編寫高效的 CUDA 內核 了解應採用的最佳實務以及應避免的常見陷阱 學習使用 Python 優化和調試 GPU 程式碼的策略 獲得使用 Python 編寫強大的 CUDA 核心所需的技能和見解,使您能夠加速應用程式並提高專案的運算效率 主題:開發和最佳化 - 程式語言/編譯器 行業細分:所有行業 技術等級:技術 - 中級 目標對象:開發人員/工程師 所有目標受眾類型:開發人員/工程師 3 月 20 日,星期四 凌晨 2:00 - 凌晨 2:40 中部標準時間 AI逐字稿 這是一個系列講座。我們今年正在為GTC(GPU Technology Conference)準備這些內容。這次的講座是關於Python的演講。這已經是連續第三次的講座了,我們一直在探討所有與Python相關的事物。而這場演講特別聚焦於如何編寫內核(kernels)。到目前為止,我相信在座的大多數人都已經看過那些關於Python技術棧(Python stack)的圖表,這些圖表涵蓋了Python程式碼中的所有層面——從運行時(runtime)編譯器無法提供的功能,一直到支援設備層(device layer),再到框架(frameworks)和領域特定語言(DSLs,Domain-Specific Languages)。這意味著什麼呢?這意味著我們希望能夠全面支持整個Python生態系統(Python ecosystem)。對Python的使用者和開發者來說,這具體代表什麼呢?就是這個圖表中的所有元素應該都能夠互相混合使用,就像你所期待的那樣,不再受限於單一的Python套件(packages)。你應該能夠同時改進多個Python套件,並在你的使用者程式碼(user code)中將它們結合起來使用。這是我們正在努力實現的一個重要使命。 在這場演講中,我將特別帶領大家從技術棧的基礎開始,一步步了解如何以不同的方式開發內核。為了做到這一點,我想使用一個教學範例來說明,也就是「片段歸約」(segment reduction)。這個問題是用來展示我們如何編寫內核,以不同的方式解決同一個問題,這一點非常重要。問題的陳述如下:假設我們有一個一維緩衝區(1D buffer)或是陣列(array),裡面存放了許多不同的片段(segments),這些片段分別由紅色、藍色、黃色等顏色標記。我們希望對每個不同的片段進行歸約運算(reduction)。例如,紅色片段的所有數字加總起來得到10;藍色片段加總起來得到26,以此類推。這就是我們的問題陳述。 假設你之前沒有聽過我們今年提供的其他教育講座或這些新材料,你可能會好奇:我該如何用CUDA(Compute Unified Device Architecture)來實現這個功能呢?你可能會試著參考一些線上資源,然後嘗試用C++自己編寫內核程式碼。正如你稍後會看到的,這並不是現在唯一的方法。但假設我們真的採用這種方式,我們會怎麼做呢?我們會先引入Thrust庫(Thrust library)來填充輸入向量(input vectors),然後定義問題的規模(problem size),並在設備端(device)準備好所需的緩衝區(buffers)。接著,我們會啟動自己編寫的內核來執行計算,在內核運算完成後進行同步(synchronize),然後將資料從設備端拷貝回主機端(host),以便進行後處理(post-processing)或驗證(verification)等工作。 所以,真正的問題在於:我們該如何編寫這個內核呢? 好吧,嗯?所以呢,有很多方法可以做到這一點。這是其中一種方式,我們可以將一個片段(segment)分配給一個區塊(block)。比如說,如果我們有3個片段,我們就啟動3個區塊;如果有100萬個片段,我們就啟動100萬個區塊,以此類推。為了讓這一切盡可能通用,我將這個內核(kernel)寫成了一連串的模板內核(template kernel)。這樣我就可以對輸入資料型別(input data types)、偏移型別(offset types)進行模板化處理。同時,我還能將區塊大小(block size)和每個執行緒處理的項目數(items per thread)編碼為模板參數(template parameters),這樣它們就可以在編譯時(compile time)指定,並由編譯器用來協助優化(optimization)。為了簡單起見,在這場演講中,我假設片段大小是我們指定的區塊大小的倍數。 那麼我們要做什麼呢?我們首先分配共享記憶體(shared memory),因為我們希望區塊中的所有執行緒(threads)能夠彼此通訊並在後續協作。接下來,我們會計算後續所需的索引(indexes),也就是我在這個區塊中作為一個執行緒的位置,以及我在整個片段集合中的片段位置。有了這兩個數字,我們就可以計算偏移量(offset),並為每個執行緒載入指定的資料,將資料載入到暫存器(register)中,然後進行部分聚合(partial aggregation)。因為我們假設片段的大小可能遠大於區塊大小,所以我們需要透過這個區塊來覆蓋整個片段。完成這一步後,每個執行緒都會有自己的部分聚合結果。接著,我們將這些結果寫入共享記憶體,並對區塊中的所有執行緒進行同步(synchronize)。 最後,我們使用共享記憶體中的資料進行部分歸約(partial reduction)。基本上,這意味著區塊的一半會使用自己擁有的資料,以及區塊另一半的資料,進行計算並寫回共享記憶體。然後,區塊的四分之一會做同樣的事情,八分之一也是如此。最終,只有一個執行緒會產生我們所需的最终結果,並將其寫回全域記憶體(global memory)。對吧?我們稍後會再回顧這個圖表。 這是一個教學範例,它確實解決了問題。它的表現還算不錯,對於與效能(performance)相關的講座來說也沒什麼問題。如果你想了解更多關於效能的內容,可以參考我同事喬治(George)的演講,那是今天稍早之前的場次。但在這裡,你會發現,要編寫這樣的內核,你需要了解很多東西,比如CUDA程式模型(CUDA programming model)、執行緒與記憶體層次結構(thread and memory hierarchy),以及GPU架構(GPU architecture),才能讓它具備高效能。此外,你還需要在這些硬體機制(GPU machinery)之上表達你的演算法(algorithm),以便在內核中完成計算。最後,隨著你的程式規模擴大…… 這不僅僅是一個玩具程式碼(toy code)。你會想要了解C++的完整工作流程(C++ completion workflow),這樣你才能知道如何建立你的專案(project),並將它交付給你的使用者(users)。這需要學習的東西真的很多。但我想強調一點:我不會深入探討這些內容的效能細節(performance details),因為這些已經被我同事們在許多精彩的演講中涵蓋了。他們從程式設計模型(programming model)中學到的東西是可以跨語言轉移的(transferable across languages)。所以無論你是從C++講座還是Python講座中學到這些,只要你掌握了它們的運作原理,這些知識都能通用,對吧? 如果你是一位追求效率的開發者(developer),並且習慣編寫C++程式碼,就像我剛才展示的那樣,那麼這對你來說很棒。我們還有更多進階的系列講座(sequence talks),今天稍早由我的同事喬治(Georgie)和普萊斯(Price)已經講過了。如果你還沒參加,可以去看看錄影(recording)。對吧?這場演講的重點是,我們希望保持高效(productive),並且希望留在Python的環境中。但問題來了:留在Python中如何幫助你,讓你的開發生活(developed life)更輕鬆呢?對吧? 我在這裡要對比的是典型的Python專案工作流程(Python project workflow),它允許使用者內核(user kernel)在GPU上從Python端啟動(launched on GPU)。這樣就不需要依賴那些傳統的NVCC機制(NVCC machinery),這些機制通常仰賴主機編譯器(host compiler),像是GCC或MSVC,這取決於你是使用Linux還是Windows。我們必須提供多種不同的方式,將你的使用者程式碼(user code)或使用者內核編譯成可在GPU上執行的機器碼(machine code)。這些黑盒部分(black parts)代表了這樣的機制(machinery)。這些黑盒的實現方式各有不同,在不同的專案中,它們可能是由低階(low-level)技術堆疊(stack)、中間層編譯器(MIR-rated compiler)、優化技術(optimizations),或者第一方編譯器庫(first-party compiler libraries)——像是NVRT、NVLink和NVVM——所混合構成的。 關鍵在於,因為我們有了這些線上編譯(online compilation)或高效編譯機制(cheap completion of machineries),我們不再需要依賴C++的主機編譯器。這對於交付你的Python專案來說非常重要,因為你不會想要要求使用者預先安裝GCC或MSVC——這些東西不像應用程式(app)那樣可以輕鬆安裝(installable),而且安裝和設定工作環境(working environment)的過程一點也不有趣。為了進一步說明這個問題,舉個簡單的例子:假設我們剛才寫了一個內核,作為一個函式庫開發者(library developer),我們希望支援多種資料型別(data types)。我之前提到過,我們的內核已經足夠通用,可以支援片段中儲存的雙精度浮點數(double)、單精度浮點數(float)或整數(integer)值。 好吧,所以你可能會像右邊這樣編寫分派器(dispatcher),試圖根據容器型別(container type)分派到不同的資料型別(data types)。但這意味著什麼呢?這意味著所有這些型別都必須在你的專案編譯時(compile time)或構建時(build time)預先實例化(pre-instantiate)。對吧?相比之下,在Python中你會怎麼做呢?通常,你只會實例化你需要的東西,並且只編譯你需要的部分,這是由輸入參數(input arguments)決定的。比如說,如果你提供給內核(kernel)一個陣列(array),它是float64型別,那麼我們就只會編譯一個針對float64的內核,以此類推。這種動態編譯(decompletion)讓我們能夠減少二進位檔案的大小(binary size),並縮短編譯時間(compile time),因為你不需要一次性地構建整個世界(whole world)。你可以根據需求逐步構建你的內核,然後快取最終的輸出(cache the final output)。 為了說明這一點,我想使用CUDA Core(CUDA Core),這是我們的一個新專案,來強調這個觀點。CUDA Core 是我們從Python內部存取CUDA API(CUDA API)功能的新方式。它為你提供了所有常見的程式碼物件(code objects),例如初學者會用到的串流(stream)、事件(event)、設備(device)和記憶體分配(memory allocation)。除此之外,我們還支援所有線上編譯工具鏈(online compiler toolchains),包括NVRTC、NVJLink,以及後來的NVVM。這樣你就可以使用CUDA Core的功能來編寫和編譯你的程式碼。這裡右邊展示的是,我剛才在頭檔案中寫的那段程式碼,我可以直接將它作為Python字串(Python string)載入。對吧?然後我可以指定內核的模板特化(kernel specialization template),比如我想實例化一個適用於float型別、特定區塊大小(block size)和每個執行緒處理的項目數(items per thread)的內核。接著,我可以編譯這個特定的內核,將它載入到設備(device)並在GPU上啟動它。我甚至可以與參考實作(reference implementation)——比如從CUDA中來的——進行比較,以檢查結果。 這是一個非常棒的機制,因為這意味著所有那些C++相關的編譯資訊,例如模板參數(template parameters)、常數(constants)等等,現在都變成了Python的執行時(runtime)資訊,可以在執行時決定,而且完全在Python環境中實現。所以,在這個例子中,所有C++的模板內核(template kernels)都變成了執行時實例化的東西。即使你是一位C++開發者(developer),我也不建議你開發多個內核。我強烈推薦你試試在Python中開發,這樣可以快速迭代(iterate)。一旦你完成了程式碼,你可以將它移回你的C++程式碼庫(codebase)。正如我所說的,它涵蓋了大量的CUDA功能(CUDA functionality),我們正在擴展這個專案。目前,它已經支援了多種平台,包括Linux和Windows等所有主流執行環境(runtimes),並且支援最新的CUDA主要版本(major versions),目前是11和12。 但是,如果你看看這段程式碼,對吧?我們這裡有一群Python開發者,我們真的不想寫C++,如果可以避免的話。對吧?因為它有很多需要學習的東西。如果可能的話,我們真的更希望留在Python環境中。那麼,我們該怎麼做呢? 所以,這裡我要介紹一種在Python中編寫CUDA內核(CUDA kernels)的方法,那就是使用Numba(Numba code)。這是一個開源專案(open source project),由Anaconda團隊(Anaconda team)支持,並得到了Numba團隊和相關社群的協助。我們正在與Numba團隊密切合作,將CUDA目標(CUDA target)從上游的Numba專案中獨立出來,這樣可以減輕Numba團隊的維護負擔(maintenance burden),同時加速這個專案的發展,並將它與Numba的發佈週期(release cycle)解耦(decouple)。這裡我展示了一個簡單的程式碼範例,在C++中,它基本上是取一個輸入陣列(input array),將每個元素平方(square),然後將結果寫入輸出陣列(output array)。同樣的邏輯,在下面的範例中,我們可以看到使用Numba可以用更清晰的語法(cleaner syntax)來編寫相同的程式碼。Python的語法中包含了一些特性,例如使用「**」運算符,這在標準Python中表示二次方(power of two)。 有了Numba程式碼(Numba code),讓我們重新審視剛才的程式碼。我們之前說過,這段程式碼很難寫,因為你需要了解很多東西,才能編寫它、檢查效能(performance)、驗證正確性(correctness)等等。那如果我們用Numba CUDA重寫這段程式碼呢?它會是什麼樣子?嗯,這就是結果,這就是它的樣子。我剛才提到,所有那些模板參數(template parameters)在Python中變成了執行時參數(runtime parameters)。所以你可以在啟動內核之前,先明確指定區塊大小(block size)和每個執行緒處理的項目數(items per thread),然後在內核中使用它們。但除此之外,邏輯本身並沒有太大改變,你仍然需要載入資料(load data)、執行計算(computation),並知道如何編寫平行執行緒歸約(parallel thread reduction)等等。 那麼,我們完全擺脫C++了嗎?絕對是的。這現在是一個純Python範例(pure Python example)。你可以將這個內核插回我剛才展示的CUDA Core範例中,這樣就完全是Python實現了。但這是否意味著它更容易編寫呢?不一定。就像我說的,你還是需要了解很多東西,比如GPU架構(GPU architecture)等等,才能把程式碼寫得漂亮又乾淨。特別是,在這段程式碼中,有兩部分我真的很想去除。我希望有人已經幫我完成了這些工作,這樣我就不必自己去搞清楚如何高效載入資料(load perform)。我不想知道如何以高效的方式載入資料(efficient load),甚至是向量化的載入(vectorized load),也不知道如何使用指令級並行(instruction-level parallelism)。同樣的,對於歸約(reduction),我真的不想去學習執行緒歸約(thread reduction)的細節。對吧?那我該怎麼辦呢? 嗯,以前這涉及到將所有這些程式碼編寫為設備函數(device functions),這樣我們就可以把這些邏輯抽象出來,並提供設備函數庫(device function libraries)。但以前這有一個問題。問題在於,我們唯一能支援Python設備函數庫的方式,要麼是透過C++,這帶來了很大的開銷(overhead);我們希望把它放回Python環境中,要麼是將它作為PTX(PTX)格式發佈。但PTX並不支援CUDA次要版本(minor version)的廣泛相容性(virgin compatibility)。所以你會遇到麻煩,這取決於使用者的驅動程式版本(driver version)。它可能無法載入(loadable),也可能無法連結(linkable)。對吧? 從CUDA 12.0版本開始,出現了一個新的函式庫叫做NVJLink(NVJLink),它終於幫我們解決了這些問題。它的邏輯是這樣的:如果你正在發佈設備函數庫(device libraries),無論是用C++還是Python編寫,你會希望將它們編譯成一種叫做LTOIR(LTOIR)的格式。這種格式能被NVJLink識別,NVJLink可以用來將多個部分連結在一起,包括你的使用者內核(user kernel)、使用者設備函數(user device functions),以及第三方或第一方的設備函數庫(third-party or first-party device libraries)。它會將所有這些設備函數連結起來並內聯(inline),這樣就不會有額外的函數呼叫開銷(function call overhead)。這讓我們能夠生成一個效能媲美C++對應版本的內核(kernel)。基本上,這在某種程度上延長了Numba內核(Numba kernel)的生命週期,我的一位同事馬可(Marco)曾經很好地整理了一份說明,解釋了Numba內部的處理流程(internal pipeline)。 現在,我們終於有了一個完整的解決方案,來提供Python的設備函數和設備函數庫(device libraries)。接下來我要給你們看兩個例子。第一個例子是一個新的CUDA Python專案,我們稱它為CUDA Cooperative(CUDA Cooperative)。它基於CUPC(CUPC)這款C++函式庫,提供了一套高效能的C++演算法(algorithms)。我們找到了一個方法,利用這些機制(machinery)把它們整合起來,並讓它們暴露給Numba CUDA的使用者(Numba CUDA users)。你需要做的是,再次指定所有的模板參數(template parameters)和你想要處理的資料型別(type)。然後,你使用這些參數來定義你的載入設備函數(load device function)。這些函數會生成LTOIR格式的程式碼(LTOIR generated)。對於歸約(reduction),我們需要定義二元運算(binary operation)。在這個例子中,它只是簡單的「A + B」。我們希望對某個片段(segment)進行高效能的加總(performance sum),對吧? 有了這個使用者定義的Python函數(user-defined Python function),我們可以在幕後使用工具生成LTOIR(LTOIR),然後生成設備函數(device function)。這讓我們能夠實現一個區塊級的歸約(block-wide reduction)。這樣,我的用戶內核(user kernel),原本在幻燈片上占滿整整一頁,現在只剩下半頁,變得非常乾淨且易於管理。你只需要將資料載入到本地暫存器(local registers),然後準備共享記憶體(shared memory)供演算法使用,接著呼叫區塊歸約(block reduce)來執行區塊級的歸約運算,最後將資料寫回全域記憶體(global memory)。對吧? 這個函式庫(library)目前仍在開發中。我們還沒有推出任何測試版(beta release),但它完全是公開開發的(developed in the public)。所以今天你就可以複製(clone)這個儲存庫(repository),從原始碼(source)構建並試用它。這種方法最棒的特點在於,它不僅支援構建基本的資料型別(data types),像是浮點數(floats)、雙精度浮點數(doubles)等等,還允許支援在Python中部分定義的自訂資料型別(custom data types)和自訂運算子(custom operators)。對吧?因為它會將整個套件(package)編譯成GPU相容的格式。如果你採用這種方式,你會發現這個CUDA方法的專案體積非常小。而且它是開源的(open source)。對吧?在這個過程中,你可以使用CU(CUDA Utility)提供的區塊載入(block load)和區塊歸約(block reduce)功能。 請給我們回饋(feedback),我們會在封裝(packaging)和相關細節上繼續努力,讓它更容易安裝、更方便使用。但最重要的是,我們想知道你希望從這個函式庫中獲得什麼,你需要什麼功能。請試試看吧!這裡有一些觀察。剛才我給你們看過一個圖表,裡面有黃色的箭頭表示多個執行緒(threads)協同工作,最終產生一個結果。如果你分解這些步驟,你會發現有些是並行步驟(parallel steps),有些是每個執行緒獨立執行的序列步驟(serial steps)。所有執行緒是並行運行的(run in parallel),但最終它們需要一起合作,才能生成最終輸出(final output)。這是某些部分的特性(quality)。 這意味著,當你編寫我們所謂的CUDA單指令多資料(SIMD, Single Instruction, Multiple Data)內核(CUDA SIMD kernel)時,這種程式模型(programming model)是我們在Numba CUDA和C++端使用的。當你編寫這樣一個簡單的內核時,你仍然需要協作API(cooperative APIs)來幫助你生成最終輸出,並執行一些非平凡的任務(nontrivial tasks),比如掃描(scan)、歸約(reduction),甚至更高級的操作(made more)。對吧? 說到矩陣乘法(matrix multiplication),這是看待同一個問題的另一種方式,並探索不同的機制(machinery)。片段歸約(segment reduction)其實可以看作是一個矩陣向量乘法(matrix-vector multiplication)。如果你把這些片段範圍(range of segments)想像成一個二維矩陣(2D matrix),然後乘以一個全為1的向量(vector of ones),那麼這就變成了同一個問題,只是以不同的方式表述。但為什麼要這樣思考呢?對吧?我們之所以想用這種方式思考,是因為它讓我們能夠探索不同的機制和不同的設備函數庫(device libraries)。在這個例子中,我使用了NVMath Device(NVMath Device)來在我的Numba CUDA內核(Numba CUDA kernel)中執行矩陣乘法(matrix multiplication)。 具體來說,過程是這樣的:我先宣告區塊大小(chunk size)和陣列的資料型別(data type of array)。然後,我使用NVMath Device的矩陣乘法功能(matrix multiplication function)來宣告一個最小的設備函數(minimal device function)。這個函數會告訴我需要在內核中分配的共享記憶體大小(shared memory size)。於是,在我的內核中,我可以像之前處理矩陣乘法一樣,分配共享記憶體(shared memory)給A、B、C三個變數,並計算C = A × B。對吧?接下來,我們的矩陣A會從由多個片段組成的二維矩陣中載入資料(load data)。而對於矩陣B,你甚至不需要載入任何東西。對吧?如果我們事先知道這個向量全都是1,那麼我們可以直接在共享記憶體中將它初始化為全1(initialize with ones),根本不需要在全域記憶體(global memory)中實體化這個陣列(materialize the array)。 然後,我們對區塊中的所有執行緒(threads in the block)進行同步(synchronize),因為我們正在準備共享記憶體。接著,我們呼叫這個矩陣乘法API(matrix multiplication API)來計算C = A × B。計算完成後,我們再次同步。最後,我們將共享記憶體中的C資料寫入輸出陣列(output array)。這種方式讓我們能以不同的角度表達問題,並利用矩陣乘法的機制來計算相同的問題。這裡的重點是,它在幕後使用了張量核心(tensor cores)來進行計算(computation)。 我們已經看過很多這樣的協作案例(cooperative cases)。矩陣乘法是協作的,加總(sum)也是協作的。所有的執行緒都需要參與其中,才能產生最終結果(final result)。有了這些理解,我們正在引入一個新的程式設計模型(programming model),叫做CUDA Tile(CUDA Tile)。這個模型內建了協作的本質(cooperative nature)。我們不再單獨控制和管理每個執行緒(individual threads),而是控制我們稱之為「瓦片區塊」(tile block)的單位。每個瓦片區塊會將一塊資料(chunk of data)載入到暫存器(registers)中。然後,我們對這些載入的瓦片(loaded tiles)執行非純量運算(non-scalar operations)。你可以執行像A + B這樣的運算,它會對兩個瓦片中的所有元素進行逐一加總(element-wise summation)。 然後我們將結果全部儲存回全域記憶體(global memory)。這種方式讓你可以用非常簡潔的三行程式碼來處理這個特定的案例:載入資料(load)、沿著某個軸進行加總(sum over the axis),然後將最終的瓦片(tile)儲存回去。關於更多與瓦片(tile)相關的講座,我的同事史蒂芬·瓊斯(Steven Jones)在昨天的演講中介紹了CUDA的新功能。如果你有興趣,請務必去看看。 這是我們提出的新舊程式設計模型(programming model)的對比。兩者都受到支援。CUDA Tile(CUDA Tile)是CUDA的擴展,它讓你可以將執行緒計算邏輯(thread multiplication logic)與資料載入邏輯(data loading logic)分開。這樣你就不需要控制所有的個別執行緒(individual threads),也不用擔心如何進行指標運算(pointer arithmetic)、如何計算全域陣列的偏移量(offset into a global array)等等,因為這些事情都由編譯器(compiler)替你處理。正如我之前說的,協作的本質(cooperative nature)非常重要。有了這種程式設計模型,基本上所有的運算都是非純量的(non-scalar)。這些瓦片(tiles)可以被視為瓦片陣列(tile arrays),它們只是內核(kernel)中的陣列。這種方式讓你可以表達我剛才提到的所有想法,比如掃描(scan)、原地計算(compute in place)和矩陣乘法(matrix multiplication)。對吧?這讓你在CUDA內核中可以編寫非純量的程式碼(non-scalar code)。 今年晚些時候,我們將釋出CUDA Tile(CUDA Tile),你就可以試用它了。之前我們展示了C++和Numba程式碼(Numba code)中的平方內核(square kernel),但現在有了這個瓦片程式設計模型(tile programming model),我們可以用同樣的方式做到這一點。我們可以從全域記憶體載入資料(load data from global memory),然後計算平方(square),——抱歉,不是平方根(square root),是平方——然後將輸出的瓦片(output tile)寫回全域記憶體。 對於熟悉NVIDIA Warp(NVIDIA Warp)的人來說,NVIDIA Warp 是一個可微框架(differentiable framework),讓你能為幾何模擬(geometric simulations)、空間計算(spatial computing)和AI工作負載(AI workload)編寫內核。這個新的Numba庫Warp(Warp library)的核心在於,當你編寫內核時,它有能力為你合成反向模式(reverse mode)的自動微分內核(auto-differentiable kernels)。所以當你在螢幕上編寫這樣的內核時,它會為你生成一個反向內核(backward kernel),讓你能直接將它插入到你的機器學習(machine learning)或模擬工作負載(simulation workloads)中,然後自動提供反向計算(backward computation)。這樣你就不需要自己準備這些東西了。最近,Warp也引入了這個新的瓦片程式設計模型(tile programming model),讓你可以在Warp內核中編寫類似的瓦片程式碼(tile-like code),這和我剛才講的內容幾乎一樣。請去看看吧!Warp現在已將許可證更改為Apache 2.0(Apache 2.0),並在GitHub上開源(open source)。對吧? 我之前提到過張量核心(tensor cores),對吧?我們讓Python能夠利用張量核心。但如果你是個追求極致的專家(ninja),你可能會想要完全掌控張量核心。卡爾斯(Carles)的團隊今年也將宣布對CuPy(CuPy)的Python支援,讓你可以直接在Python程序中編寫CuPy的領域特定語言(DSL, Domain-Specific Language)。這樣,你就能完全存取所有底層功能(APIs),控制張量核心,並編寫出極速內核(speed-of-light kernel),從我們的GPU中榨取出最大的效能(maximum performance)。這裡最好的部分是,因為不再有C++相關的機制(C++ machinery),也不需要處理和編譯C++模板(C++ template processing and compiling),這大幅降低了編譯時間(compiler time)。編譯速度變得非常快,讓你能在Python中快速且高效地迭代你的內核(iterate over your kernels),同時仍然保持相同的效能。 所以,請去看看吧!CuPy的相關講座將在星期五舉行。你會從CuPy團隊那裡學到更多細節。對吧? 到目前為止,我已經向你們介紹了一些用來編寫內核(kernels)的工具。這些工具目前主要來自NVIDIA,但也有許多社群工具(community tools),它們同樣能讓你表達相同的想法。例如,你可以使用CuPy的歸約內核(CuPy reduction kernel)來編寫相同的片段歸約(segment reduction),或者使用CuPy的API(CuPy API)以CuPy語法(CuPy syntax)編寫一個高效能的內核,這與Numba CUDA(Numba CUDA)的語法非常相似。或者你也可以使用Triton(Triton),對吧?Triton提供了基於區塊的程式設計模型(block-based programming model)。你還可以手動計算指標偏移量(pointer offsets),然後用這種方式編寫內核。你可以用Triton做到同樣的事情。所以,市場上真的有很多工具,無論是第一方(first-party)還是第三方(third-party)的,我們都支援它們。 那麼問題來了:哪個工具是編寫我的內核的最佳選擇呢?對吧?我可以挑選的東西太多了,什麼才是我的最佳解決方案(best solution)?答案當然是「看情況」(it depends)。很遺憾,我無法給你一個簡單明瞭的答案。因為你需要考慮的限制條件(constraints)實在太多了,對吧?比如,如果你已經陷入了依賴地獄(dependency hell),需要解決這個問題,那麼你可能會想要利用現有的套件(existing packages)。有些套件可能已經提供了編寫內核的方法,你或許應該先試試這些。又或者,如果你的支援矩陣(support matrix)很龐大,而某些套件無法涵蓋你想要支援的範圍——比如不同的Python版本、不同的CUDA版本(CUDA versions)、驅動程式版本(driver versions)、作業系統(operating systems),以及CPU和GPU架構(CPU/GPU architectures)——那你也得把這些因素考慮進去。此外,這還取決於你想如何封裝(package)和部署(deploy)你的Python專案,以及你的專案能提供什麼樣的使用者體驗(user experience)。 對吧?也許你的組織或公司對於某些套件有偏好,或者限制了你能使用或不能使用的工具。這些事情也需要納入你的決策考量(decision making)。當然,還有一些效能方面的考量(performance considerations),我今天無法一一涵蓋。但重點是,要選出最佳選擇(best choice),你需要考慮的事情實在太多了。所以,最好的選擇其實是——如果你能完全避免自己編寫內核的話。對吧? 我們想告訴你的是,現在有非常多的函式庫(libraries)和套件(packages)可供使用,它們提供了各種解決方案。或許已經有人考慮過你的問題,並為你打造了一個高效能的解決方案(performance solution)。所以,你真的應該去看看社群(community)裡正在發生什麼,外面有些什麼資源(in the wild)。看看是否有人已經幫你解決了問題,然後直接使用它們,而不是自己從頭編寫一個內核(kernel),因為這是一件非常不簡單的事情(highly nontrivial)。 這是我們創建的一個新專案,我們稱它為CUDA Parallel(CUDA Parallel)。這個專案讓你不需要自己編寫內核,卻仍然能獲得與C++版本(C++ counterpart)相同的功能和效能。在這個例子中,它基於Thrust C++函式庫(Thrust C++ library),讓你能在Python中編寫類似Thrust的程式碼(Thrust-like code),並存取Thrust和CuPy暴露出的高效演算法(period algorithms)。在這種情況下,你需要創建迭代器(iterators),然後以某種方式定義你的問題,將這些迭代器融合到你的問題定義中。最後,你可以在主機端(host)啟動這個演算法(launch the algorithm)。對吧?這個函式庫提供的主機API(host APIs)讓你能編寫相同的內核,並獲得優異的效能。幕後的實現依然基於LTOIR(LTOIR)和NVJLink(NVJLink),為你生成高效的內核。 這是公開開發(open development)的專案,所以請去試試看。我們很快會進行封裝相關工作(packaging-related work),釋出一個版本,讓你更容易安裝(easier to install)。但在那之前,我們還是很想聽聽你的回饋(feedback)。我們剛剛提到過NVMath Python(NVMath Python),它既有設備API(device APIs),也有主機API(host APIs)。所以,我們可以用NVMath或任何這些Python套件來完成相同的矩陣向量乘法(matrix-vector multiplication)。對吧?在這種情況下,你唯一需要付出的代價是,你得在全域記憶體(global memory)中實體化一個全1的向量(vector of ones),這樣才能進行矩陣向量乘法。但或許你的工作負載(workload)中已經有了這個向量。如果是這樣的話,這可能只是一個很小的操作(cheap operation)。 最後,對於那些熟悉Python中陣列程式設計(array programming)的人來說,看看這個問題的定義,甚至是我在某個程式碼片段中展示的驗證程式碼(verification code),你可能已經注意到,這其實就是對二維矩陣(2D matrix)的連續列(contiguous rows)進行加總(sum)。對吧?你可以用一行程式碼完成這件事,就是「xp.sum()」,其中axis設為-1(xp.sum with axis=-1)。對吧?為什麼會這樣呢? 這真的很棒,因為它具有可攜性(portable)。它符合陣列API標準(Array API standard)嗎?因為現在有很多Python陣列函式庫(Python array libraries),像是CuPy(CuPy)、NumPy(NumPy)、JAX(JAX)、Dask(Dask Python),它們都遵循Python的陣列API標準(Python Array API standard)。這意味著,只要你寫了一次程式碼,就可以用不同的陣列函式庫來試試看。你可以把「from cupy import xp」改成「from torch import xp」,然後看看結果如何。這讓你能快速探索(explore)各種不同的函式庫。最後,回到我們最初的C++解決方案(C++ solution),我們真正想做的是用CuPy的設備端片段歸約(CuPy device segmented reduction)來取代那些手寫的內核(handwritten kernels)。 總結來說,我們為你提供了許多工具,讓你能編寫內核(kernels),從而提升生產力(productivity)。你可以在Python中快速迭代(iterate),但仍然保持與C++相同的效能(performance)。對吧?如果沒有現代編譯器技術(modern compiler technologies),像是MIR(MIR)、NVJLink(NVJLink)這些東西,這是不可能的。所以,現在Python設備函數庫(Python device libraries)已經成為現實,我們真的可以發佈Python設備函數庫了。新的CUDA Tile程式設計模型(CUDA Tile programming model)技術更進一步,讓許多協作設備API(cooperative device APIs)成為程式設計模型的一部分。對吧? 但有一個壞消息是,如果你能完全避免自己編寫內核(writing kernels),如果外面已經有現成的解決方案(solution in the wild),你應該先試試看,看看效果如何,再決定是否要捲起袖子(roll your sleeves)自己動手寫內核。所以,根據你的限制條件(constraints)、效能目標(performance targets)、個人喜好(personal tastes)等等,混合搭配(mix and match)這些工具來選擇最適合你的方案。我們在這裡隨時回答你的問題。事實上,今天下午我們會有一場與專家面對面的活動(connect with exposition),讓你能與我們交流。我們可以回答你的問題,提供回饋(feedback)和指引(guidance)。同時,如果你對CUDA Python團隊(CUDA Python team)有任何需求,我們也很想聽聽你的意見。 今年真的很不一樣,因為我們正在打造許多與開發者相關的內容(developer-related content),包括不同的講座(talks)和主題軌(tracks),涵蓋CUDA程式設計(CUDA programming)的各種話題。所以,請現在就去看看。如果你錯過了任何講座,之後都可以觀看錄影(recording)。最後,我想感謝所有的團隊成員。我們有一個很棒的團隊(big routine),多個團隊合作,創造了這些內容,為Python使用者提供了非常棒且友善的解決方案(Python solutions)。我要感謝整個團隊,也感謝你們的聆聽。 講者2:謝謝你,LEO。我們還有時間回答幾個問題。 講者3:在Blackwell中,你們引入了FP4(FP4),這是一種縮放區塊型別(scaled block type)。 講者1:是的。 講者3:我還沒看到有人討論如何在Python中使用它。 講者1:嗯,你指的是MXFP8(MXFP8)吧? 講者3:或者是FP4,或者FP—— 講者1:FP4。 講者3:基本上是一個縮放—— 講者1:比例因子(scale factor)。是的,最近我們推出了一些東西,已經為FP8和MXFP8提供了0.3.0版本的支援(0.3.0 support)。但我認為FP4(FP4)的支援已經在我們的路線圖(roadmap)上了。 講者4:對吧? 講者1:所以我們最終都會實現這個目標。對吧,好吗? 講者3:在另一個版本中,我想看到一個使用它的例子。 講者1:當然可以。 講者2:嗯,有了這個,如果我想在Python中提供我的內核(kernels),然後使用它們並將它們預編譯(pre-compile)或連結(link)到C++或其他環境中,這背後的故事是什麼? 講者1:是的,對。這個問題涉及到你如何在Python中捕捉(catch)這些內核。因為最終你可能會生成PTX(PTX)或CUDA二進位檔案(CUDA binary),對吧?我的建議是直接生成CUDA二進位檔案。但你需要有一個方法來識別這個二進位檔案是對應哪個內核的——抱歉,我的意思是,這個二進位檔案是為哪個內核生成的,對吧?你需要能夠將輸出的成品(output artifact)與你想在C++中替換的內核關聯起來。 講者2:是的,所以…… 講者1:所以你需要有一個方法來識別它。你可以使用驅動程式API(Driver API)將它們載入回來。 講者2:嘿,謝謝你的文件。嗯,另一個在Python中非常受歡迎的函式庫是Triton(Triton)。我想知道你能不能告訴我們CUDA Tile(CUDA Tile)和Triton有什麼不同?比如,它試圖解決什麼問題?它和Triton相比解決了什麼不同的東西? 講者1:嗯,抱歉,你剛才的問題是什麼來著?哪個問題? 講者2:有一個很受歡迎的領域叫做Triton,它就像是讓你把東西寫在資料的瓦片(tiles)上。我想知道CUDA Tile和它有什麼不同? 講者1:哦,我其實不太熟悉那個函式庫。所以我需要查一下,才能給你一個更好的回答。我試著理解,你說的是Triton(Triton)對吧?我覺得它們只是不同的程式設計模型(programming model),編寫內核(kernels)的方式不同而已。 講者4:所以這真的是很多東西啊。嗯……