Numba:"大規模優化武器"

Numba如何節省您的時間並加速代碼

Numba:

Numba是一個Python編譯器,專門用於數值函數,允許您使用直接用Python編寫的高性能函數來加速應用程序。

Numba使用LLVM從純Python代碼生成優化的機器代碼。 通過幾個簡單的更改,我們的Python代碼(面向函數)可以"及時"優化,從而獲得與C,C ++相似的性能,而無需更改語言。

您可以在我的GitHub中找到整個代碼! :)

目錄

  • 什麼是Numba?
  • 第一步:為CPU編譯
  • Numba for GPU
  • 結論

什麼是Numba?

Numba是允許您使用CPU和GPU加速Python代碼(數字函數)的編譯器:

  • 函數編譯器:Numba編譯Python函數,而不是整個應用程序或部分代碼。 基本上,Numba是另一個Python模塊,可提高我們函數的性能。
  • 即時:(動態翻譯)Numba在執行之前立即將字節碼(比機器代碼抽象的中間代碼)翻譯成機器代碼,以提高執行速度。
  • 數值集中:Numba專注於數值數據,例如int,float,complex。 目前,將其與字符串數據一起使用存在一些限制。

Numba並不是在CUDA中編程的唯一方法,它通常直接用C / C ++進行編程。 但是Numba允許您直接使用Python進行編程,並且只需對我們的代碼進行少量更改即可針對CPU和GPU對其進行優化。 關於Python,還有其他選擇,例如pyCUDA,下面是它們之間的比較:

CUDA C / C ++:

  • 這是CUDA中最常見,最靈活的編程方式
  • 加速C,C ++中的應用程序。

pyCUDA

  • 這是用於Python的最有效的CUDA表單
  • 它要求在我們的Python代碼中編程C,並且通常需要進行許多代碼修改。
Numba:

Example PyCUDA

Numba

  • 效率不如pyCUDA
  • 它允許您用Python編寫代碼並進行少量修改來對其進行優化
  • 它還為CPU優化了Python代碼

目標

演講的目的如下:

  • 使用Numba在CPU上編譯函數
  • 瞭解Numba的工作原理
  • 在GPU中加速Numpy ufunc
  • 使用Numba編寫內核(下一教程)

第一步:為CPU編譯

Numba除了能夠加速GPU中的功能之外,還可以用於優化CPU中的功能。 為此,使用Python裝飾器(函數修飾符)。

首先,我們將開始評估函數hypot,以嘗試Numba的工作方式。 我們需要在函數中使用裝飾器@jit。

<code>from numba import jitimport numpy as npimport math@jitdef hypot(x, y):  return math.sqrt(x*x + y*y)# Numba functionhypot(3.0, 4.0)# Python functionhypot.py_func(3.0, 4.0)/<code>

>>> # Numba function

>>> hypot(3.0, 4.0)

5.0


>>> # Python function

>>> hypot.py_func(3.0, 4.0)

5.0

Numba中的結果與Python函數中的結果相同,因為Numba將函數的原始實現保存在.py_func中。

Benchmarking

自然地,衡量我們的代碼性能,檢查Numba是否真的運行良好並觀察Python實現與Numba實現之間的區別非常重要。 此外,math庫已經包含了hypot函數,我們也可以對其進行評估。

<code>import math# Python function%timeit hypot.py_func(3.0, 4.0)# Numba function%timeit hypot(3.0, 4.0)# math function%timeit math.hypot(3.0, 4.0)/<code>

>>> # Python function

>>> %timeit hypot.py_func(3.0, 4.0)


The slowest run took 17.62 times longer than the fastest. This could mean that an intermediate result is being cached. 1000000 loops, best of 3: 260 ns per loop


>>> # Numba function

>>> %timeit hypot(3.0, 4.0)


The slowest run took 33.89 times longer than the fastest. This could mean that an intermediate result is being cached. 1000000 loops, best of 3: 216 ns per loop


>>> # math function

>>> %timeit math.hypot(3.0, 4.0)


The slowest run took 105.55 times longer than the fastest. This could mean that an intermediate result is being cached. 10000000 loops, best of 3: 133 ns per loop


math.hypot函數甚至比Numba還快!! 這是因為Numba會給每個函數調用帶來一定的開銷,該開銷要大於Python函數調用的開銷,因此非常快的函數(如上一個)會受到此影響。

(但是,如果您從另一個函數調用Numba函數,則開銷很少,如果編譯器將該函數集成到另一個函數中,則有時甚至為零。總之,請檢查這些函數是否真的在Numba中加速了)。


Numba如何工作?

當我們初始化hypot函數時:

Numba:

How Numba works

  • IR 中間人
  • 字節碼分析 中間代碼比機器代碼更抽象
  • LLVM低層虛擬機,用於開發編譯器的基礎結構
  • NVVM是基於LLVM的IR編譯器,旨在表示GPU內核

每行python之前都有幾行Numba IR代碼。 最有用的是查看向我們展示Numba如何處理變量的類型註釋,例如,在"pyobject"中,它表示Numba不知道np.sin函數,並且他應該從Python調用它。 我們可以使用.inspect_types()檢查該hypot的過程。

<code>


示例:創建分形

我們將測量使用Mandelbrot集創建分形的性能,我們將看到Numba如何幫助我們改善性能。

<code>

1 loop, best of 3: 4.62 s per loop

<matplotlib.image.axesimage>

Numba:

使用Mandelbrot集生成分形大約需要4.62秒,現在我們要使用Numba改善性能,我們只需要添加@jit裝飾器即可。

<code>

The slowest run took 4.17 times longer than the fastest. This could mean that an intermediate result is being cached. 1 loop, best of 3: 52.4 ms per loop


我們可以觀察到如何將建立分形的時間從4.62秒減少到52.4 ms……並且僅通過添加裝飾器就可以做到這一點!

一些常見錯誤

我們已經說過,Numba僅適用於數字函數,儘管Numba可以編譯並運行任何Python代碼,但是有些類型的數據尚不能編譯(例如字典),並且編譯它們也沒有任何意義。

>>> @jit

>>> def dictionary(dict_test):

>>> return dict_test['house']dictionary({'house': 2, 'car': 35})2


但是它並沒有失敗! 我們已經說過Numba不會編譯字典……這裡的意思是Numba創建了2個函數,其中一個在Python中,另一個在Numba中。 因此,在這裡我們看到了python解決方案,我們可以通過執行nopython = True來驗證這一點。

jit(nopython = True)等效於njit

<code>


Numba for GPU

使用Numba在GPU中進行編程的方法有兩種:

  1. ufuncs / gufuncs__
  2. CUDA Python內核(下一個教程)

功能ufunc

GPU的主要設計功能之一是能夠並行處理數據,因此numpy(ufunc)的通用功能是在GPU編程中實現它們的理想選擇。

注意:ufunc是對numpy數組的每個元素執行相同操作的函數。 例如:

<code>

動手:為GPU創建功能ufunc

如前所述,由於ufunc函數具有並行性,因此是將其與GPU配合使用的理想選擇。 因此,Numba無需使用C就可以創建編譯的ufunc函數。為此,我們必須使用裝飾器@vectorize。

讓我們從使用@vectorize編譯和優化CPU ufunc的示例開始。

<code>
<code>

而不是使用CPU來編譯和執行先前的功能,我們將在GPU中使用CUDA,為此,我們必須使用"目標屬性"。 我們將指出每個變量的類型(參數和返回值)。

return_value_type(argument1_value_type,arguments2_value_type,...)

為此,我們將使用先前的函數,該函數期望2個int64值並返回另一個int64值。 我們將指定target ='cuda'以便能夠在GPU中執行它。

<code>

array([ 24, 343, 15, 9])

我們可以檢查在CPU或GPU上運行它的速度:

>>> %timeit np.add(a, b) # Numpy en CPU


The slowest run took 38.66 times longer than the fastest. This could mean that an intermediate result is being cached. 1000000 loops, best of 3: 511 ns per loop


>>> %timeit add_ufunc_gpu(a, b) # Numpy en GPU


The slowest run took 4.01 times longer than the fastest. This could mean that an intermediate result is being cached. 1000 loops, best of 3: 755 µs per loop


GPU比CPU慢!!! 安靜,這有一個解釋……但是首先讓我們看看調用該函數時會發生什麼……

當我們執行此功能時,Numba會產生:

  • 編譯CUDA內核以在輸入數組的所有元素上並行執行ufunc函數
  • 將輸入和輸出分配給GPU內存
  • 將輸入複製到GPU
  • 運行CUDA內核
  • 將結果從GPU複製回CPU
  • 以numpy數組形式返回結果

與用C實現相比,Numba允許您以更簡潔的方式執行這些類型的任務。

為什麼GPU比CPU慢?

  • 我們的輸入量太小:GPU使用一次並行處理數千個值的並行處理來獲得更好的性能。 我們的輸入是4或64維,我們需要更大的數組來保持GPU的佔用。
  • 非常簡單的計算:與調用CPU函數相比,將計算結果發送到GPU需要很多"精力"。 如果我們的函數不需要過多的數學計算(通常稱為算術強度),那麼GPU所花費的時間可能比CPU中更長。
  • Numba將數據複製到GPU。
  • 輸入的變量類型大於必要的變量:我們的示例使用int64,我們可能不需要它們。 實際上,在CPU中,32位和64位具有相同的計算速度,但是在GPU中,64位具有稍微提高的計算速度(與32位相比,其速度可分別降低多達24倍)。 因此,在GPU中執行功能時,請記住這一點,這一點很重要。

考慮到這一點,我們將嘗試應用在前面幾點中學到的知識,以瞭解在GPU上運行是否真的比在CPU上運行更快。 我們將要計算一個密度函數,這對於較大的數組來說是一個稍微複雜的操作。

在給定平均值和sigma的情況下,讓我們計算x中的高斯密度函數的值:

<code>

>>> %timeit norm_pdf.pdf(x, loc=mean, scale=sigma) # CPU function

10 loops, best of 3: 60.8 ms per loop


>>> %timeit gaussian_dens_gpu(x, mean, sigma) # GPU function

100 loops, best of 3: 6.88 ms per loop


是啊!

我們甚至可以使用Numba定義要在CPU中執行的功能。

<code>import math sqrt_pi = np.float32((2*math.pi)**0.5)@vectorizedef gaussian_dens_cpu(x, mean, sigma):    return math.exp(-0.5 * ((x - mean) / sigma)**2) / (sigma * sqrt_pi)  x = np.random.uniform(-3, 3, size=1000000).astype(np.float32)mean = np.float32(0.0)sigma = np.float32(1.0)%timeit gaussian_dens_cpu(x, mean, sigma) # CPU/<code>

>>> %timeit gaussian_dens_cpu(x, mean, sigma) # CPU

10 loops, best of 3: 23.6 ms per loop



它甚至比用Python編寫的函數還要快,但比在GPU中執行的函數要慢。

不幸的是,有一些函數不在ufunc定義的範圍內,因此,為了在GPU中執行不滿足該要求的函數,我們使用cuda.jit。 我們可以使用在GPU上運行的"設備功能"。

注意:"設備功能"是隻能從內核或另一個"設備"功能調用的功能。

<code>

>>> %timeit polar_distance(rho1, theta1, rho2, theta2)


The slowest run took 23.16 times longer than the fastest. This could mean that an intermediate result is being cached. 1 loop, best of 3: 10.2 ms per loop


結論

總結起來,Numba是一個Python編譯器,專門用於數值函數,它使您可以使用直接用Python編寫的高性能函數來加速應用程序。

它是一個穩定的工具,可讓您優化面向代碼的數組以進行操作。 由於它的易用性(只需一個裝飾器!)為我們提供了一個非常強大的工具來改善代碼的性能。

歡迎提出建議和評論。 跟著我,謝謝您的閱讀! :)

(本文翻譯自Alejandro Diaz Santos的文章《Numba: "weapon of mass optimization"》,參考:https://towardsdatascience.com/numba-weapon-of-mass-optimization-43cdeb76c7da)


分享到:


相關文章: