分類: PyTorch

  • ZLUDA 收官之戰 – 它還只是個孩子啊!

    ZLUDA 收官之戰 – 它還只是個孩子啊!

    承上文:

    ZLUDA 拓荒之路 – 榨乾 Intel CPU 算力的中短期方案

     

    先說結論:

    ZLUDA 目前在我的環境測試起來並不支援一些好棒棒框架,

    像是 PyTorch、Numba 之類的,雖然我沒有測試,但是有足夠信心認為 Tenserflow 也不支援。

    先附上我的環境:

    OS: Ubuntu 22.04

    Intel GPU: UHD 770

    我的測試方式是下載 ZLUDA 的 Releases 2 的版本

    這個解開會有一個資料夾內含兩個 .so 檔,

    將想執行的檔案放在與 .so 檔同個資料夾下執行即可。

    但是像作者一樣測試 GeekBench 5.2.3 的話是會過的,

    數據差別不大,至於為什麼測出沒有 Sobel 測項?

    因為要有那個測項要付錢啊啊啊啊啊!
    GeekBench 5.2.3 官網下載點

    接著我要勘誤上篇關於 CUDA 與 ZLUDA 的引用關係,

    後來發現 ZLUDA 使用上應該是要完全取代 CUDA 的,

    所以你不裝 CUDA 它也會跑得起來。

    但由於 ZLUDA 並沒有支援 CUDA 的所有功能,

    加上我猜測它是針對某版的 CUDA 來開發的,

    所以 API 串接那邊也會報錯,而 CUDA 本家會動態連結這些函式,

    導致 ZLUDA 的支援性很低。

    最後來談談這東西的未來性還有有志之士可以怎麼發展下去?

    我認為這東西會踩到 Intel 把拔的 oneAPI 計畫,

    短時間內應該就這樣了。

    所以除非有誰與 Nvidia, Intel 同時具有競爭關係又做 CPU + GPU?

    ZLUDA 才會復活。

    (望向蘇媽)

    有志之士的話可以接著試試看,

    因為我曾經在 Python 3.7 及上述環境中讓 Numba 呼叫到 Intel UHD 770 的硬體。

    以下是給有志之士的簡易指南:

    1.確認自己手上有(消費級 Intel CPU 超過 8代 或  Intel XE CPU)且 (有內顯)

    2.裝 Intel 內顯驅動(Ubuntu 20.04 可以參考這篇, 22.04 也可以參考但記得不用降內核)

    3.確認內顯驅動有裝好xD

    4.裝 Python3.7 + Numba 0.49 – 0.58 版本,我印象當時是裝這區間。

    5.下載 ZLUDA 的 Releases 2 的版本 來測試

    裝驅動的時候要特別注意,

    我在那邊卡很久,不一定第一次會裝好,

    重複裝的時候不要反安裝到這個 gawk libc6-dev udev,

    這套件就算你不 –purge 都會幫你把 Gnome 還有一拖拉庫東西 拆掉 :)

    最後講一下 Intel 的 oneAPI,

    我用過裡面的 Intel® oneAPI Base Toolkit + Intel® AI Analytics Toolkit(PyTorch 最佳化)

    用同一台機器來計算同一份呼叫到 PyTorch 的檔案,

    用 Intel 方案的環境會算得比純 PyTorch 版本還要慢。

    就是大家可以收一收回家了,感謝各位。

    如果有志之士想討論 or Intel 官方想維護一下自己的東西(#。

    歡迎聯絡我:

    wuyiulin@gmail.com

  • ZLUDA 拓荒之路 – 榨乾 Intel CPU 算力的中短期方案

     

    在開始壞壞之前,我們先了解一下 ZLUDA 是什麼?

    ZLUDA 是一款很有理想的開源套件,

    號稱能模擬閉源的 CUDA ,只用 Intel CPU 就能執行現有的 CUDA code。

    但實際用下去發現還是有一點小限制,

    畢竟作者本人在 2021 年跳坑了(用膝蓋猜是被 I社把拔的 Arc 顯卡利益衝突到)。

    目前 Linux 系列只支援到 5.19 Kernel,

    如果你跟我一樣用 Ubuntu 22.04 也是失去支援。

    Windows 戰場就優質許多,

    只要你是臺正常的家用主機應該都能用!

    畢竟這年代誰還用奔騰與賽揚或是 Atom 當家用 CPU 你說對吧?

    但還是跟一票嵌入式 CPU 說掰掰了,

    因為 ZLADA 主要壓榨的是 UHD 顯示晶片的算力。

    話不多說,先來下載在 windows 上面用:

    下載連結

    下載完解壓縮,你的框架套件(PyTorch 等)也不用重裝,

    就直接:

    zluda_wuth.exe -- python your_py_file.py

    就能跑了,484很方便?

    Ummmm,通常是不會那麼方便啦。

    有幾點要特別注意,因為這貨是模擬 CUDA 所以不是真的尻 CUDA 出來用。

    你的框架套件配合 CUDA 版本要看 nvidia-smi 上面你裝的 Nvidia Driver 支援的比較準,

    而不是 nvcc -V 出來的真實 CUDA 版本。

    不然注入 zlida_with.exe 的時候會說你的 Driver 與 CUDA mismatch。

    聽起來有點模糊對不對?

    簡單來說,

    如果你下 nvidia-smi 發現自己 Nvidia Driver 是 53X.xx 版,配合的 CUDA 是 12.2,

    而 nvcc -V 出來的真實 CUDA 版本是 10.0?

    那麼你 PyTorch 就要按照 Nvidia Driver 配合的 CUDA 裝,而不是裝 cu100 系列的 PyTorch。

    這點要特別注意!

    還有就是如果在 conda 環境內遇到 Python3.8 的 dll loss 問題,

    需要重新安裝 Python ?

    切記在 conda 裡面重新安裝 Python ,套件會繼承下來,

    這會與之前舊有的 PyTorch 連動產生一些問題。

    像是好事的 PyTorch 1.8.1 會喜歡幫你更新 typing_extensions,

    如果你重新安裝了低版本的 Python (e.g. 3.8 -> 3.7)

    會遇到 typing_extensions 錯誤,而且長得很像你程式寫錯:

    File “C:UsersuserNameanaconda3envsmyenvlibsite-packagestyping_extensions.py”, line 874
        def TypedDict(typename, fields=_marker, /, *, total=True, **kwargs):
                                                ^
    SyntaxError: invalid syntax


    莫急莫慌莫害怕,先查一下 typing-extensions 版本是不是被繼承?

    是的話直接:

    pip show typing-extensions
    pip uninstall typing_extensions
    pip install typing_extensions
    

    我印象還因為 zluda 處理了一些 deadlock 問題,

    還好天公疼憨人讓我順手解開了。

    以上就能讓 ZLUDA 在 Windows 上順利跑起來!

    結論時間:

    ZLUDA 真的能取代低階 GPU (_050, _060)的 inference task 嗎?

    以下情況是拿 Alder Lake 的 i5 去測的結論,

    某些情況會比純 CPU inference 快一點,

    但要 Costdown 還是想多了。

    有進步,但不多!

  • 使用 PyTorch 實做 2D 影像捲積

    使用 PyTorch 實做 2D 影像捲積

     承上篇:Structural Similarity(SSIM) 的 PyTorch 實現

    由於我 3D 影像處理做太多(X) 2D 影像處理還沒有恢復記憶(O)

    上次在刻高斯濾波的時候忘記 PyTorch Kit 的 Convolution 一個很重要的特性:

    PyTorch 會參考其他通道的資訊啊!

    其實這點也是無可厚非,畢竟 PyTorch 的初衷是給深度學習用的框架,

    韓信點兵多多益善嘛!

    所以 PyTorch 的 Convolution 流程大概是這樣:

    顯而易見地,沒經過額外處理的話 SSIM值會超出邊界(0,1),

    這違反了 SSIM 這項指標的意義。

    所以我們要來看一下隔壁棚 2D影像處理套件 – OpenCV 怎麼做 2D 影像的 Convolution?

    在 OpenCV 裡面,每個通道都是用同一顆 Kernel,但是會分離通道做 Convolution,

    如下圖:

    得到這個資訊後,我打算用分 Groups 來解這題。

    PyTorch 中的 2D Convolution 有兩種,

    一種是 torch.nn.Conv2d

    另一是 torch.nn.functional.conv2d

    前者只要設定好 Kernel 大小就能動,後者則可以提供使用自己 Kernel 的 API。

    筆者這次使用後者,就以後者的 Document 來解釋:

    首先我們要知道自己要分幾組 Group?

    我們希望每個通道都各自做 Convolution ,而 RGB 圖片有三個通道,

    所以我們的 Groups 應該設定三組。

    再來需要注意上圖中 torch.nn.functional.conv2d 中的 weight,

    這裡的 weight 代表的是 Kernel。

    既然改了 Groups ,Kernel 的相應尺寸也要記得改變。

    原本是 Kernel.shape = {3, 3, H, W} 要變成 {3, 3/3 = 1, H, W}。

    這樣就能開心地模擬 OpenCV 的 2D Convolution 啦!

    筆者在手刻 Kernel 的時候也遇到一些光怪陸離的問題,

    預計下一篇會來寫這部份。

  • Structural Similarity(SSIM) 的 PyTorch 實現

    Structural Similarity(SSIM) 的 PyTorch 實現

    SSIM 是一種指標,用於比較兩張圖的相異程度。

    指標主要參考三個面向:

    • 亮度 Luminance

      $$l(xy) = frac{2mu_xmu_y+C1}{mu^2_xmu^2_y+C1}$$

    • 對比度 Contrast

      $$c(xy) = frac{2sigma_xsigma_y+C2}{sigma^2_xsigma^2_y+C2}$$

    • 結構相似度 Structure

      $$s(xy) = frac{sigma_{xy}+C3}{sigma_xsigma_y+C3}$$

    以上的 $$C1 = K_1l^2, C2 = K_2l^2,C3 的部份可以簡化為 C3 = frac{C2}{2}$$

    $$k_1 = 0.01, k_2 = 0.03, l  則為圖片的灰度值。$$

    整體的 SSIM 為三項相乘:

    $$SSIM(x,y) = [l(x,y)]^alpha[c(x,y)]^beta[s(x,y)]^gamma$$

    在這邊 $$alpha, beta, gamma $$ 都簡化為 1。

    這樣我們就可以簡化 SSIM 為:

    然後我看到其他人有用 Numpy 實現,

    就想著是不是自己能寫一版 PyTorch 來用?

    大概會遇到兩個問題:

    1.手刻 Gaussian Kernel 要塞在哪?

    2.轉換資料至 GPU 上的時候會有誤差。

    第一個問題當然是不能用 torch.nn.Conv2d() 來解,

    因為這個 API 不能自己定義 Kernel,而且我們不希望這個 Kernel 的權重跑掉。

    (Doc在此)

    所以我們要用 torch.nn.functional.conv2d 

    (Doc在此)

    接著我們要手刻一下 Gaussian Kernel:

    
    def gaussian_fn(M, std):
        n = torch.arange(0, M) - (M - 1.0) / 2.0
        sig2 = 2 * std * std
        w = torch.exp(-n ** 2 / sig2)
        return w
    def gkern(kernlen=256, std=128):
        """Returns a 2D Gaussian kernel array."""
        gkern1d = gaussian_fn(kernlen, std=std) 
        gkern2d = torch.outer(gkern1d, gkern1d)
        return gkern2d
    
    

    這個方法我是從這邊看來的,

    簡單來說就是從 1d Gaussian 用矩陣乘法造 2d Gaussian。

    把整體程式碼寫出來:

    
    def gaussian_fn(M, std):
        n = torch.arange(0, M) - (M - 1.0) / 2.0
        sig2 = 2 * std * std
        w = torch.exp(-n ** 2 / sig2)
        return w
    
    def gkern(kernlen=256, std=128):
        """Returns a 2D Gaussian kernel array."""
        gkern1d = gaussian_fn(kernlen, std=std) 
        gkern2d = torch.outer(gkern1d, gkern1d)
        return gkern2d
    
    def ssim_torch(img1, img2, kernel_size=11):
        start_time = time.time()
        C1 = (0.01 * 255)**2
        C2 = (0.03 * 255)**2
        transf = transforms.ToTensor()
        
        guassian_filter = gkern(kernel_size, std=1.5).expand(3, 1, kernel_size, kernel_size)
        H, W, C = img1.shape[0], img1.shape[1], img1.shape[2] 
        img1, img2 = torch.from_numpy(img1), torch.from_numpy(img2)
        img1B, img1G, img1R = img1[:,:,0], img1[:,:,1], img1[:,:,2]
        img2B, img2G, img2R = img2[:,:,0], img2[:,:,1], img2[:,:,2]
        img1B, img1G, img1R = img1B.unsqueeze(0), img1G.unsqueeze(0), img1R.unsqueeze(0)
        img2B, img2G, img2R = img2B.unsqueeze(0), img2G.unsqueeze(0), img2R.unsqueeze(0)
        img1 = torch.cat((img1B, img1G, img1R), dim=0).unsqueeze(0).float()
        img2 = torch.cat((img2B, img2G, img2R), dim=0).unsqueeze(0).float()
    
        mu1 = F.conv2d(img1, weight=guassian_filter, stride=1, groups=3)
        mu2 = F.conv2d(img2, weight=guassian_filter, stride=1, groups=3)
        mu1_sq = mu1**2
        mu2_sq = mu2**2
        mu1_mu2 = mu1 * mu2
        sigma1_sq = F.conv2d(img1**2, weight=guassian_filter, stride=1, groups=3) - mu1_sq
        sigma2_sq = F.conv2d(img2**2, weight=guassian_filter, stride=1, groups=3) - mu2_sq
        sigma12 = F.conv2d(img1 * img2, weight=guassian_filter, stride=1, groups=3) - mu1_mu2
        ssim_map = ((2 * mu1_mu2 + C1) *
                    (2 * sigma12 + C2)) / ((mu1_sq + mu2_sq + C1) *
                                           (sigma1_sq + sigma2_sq + C2))
        print("This is torch_SSIM computer time: " + "{:.3f}".format((time.time()-start_time)))
    
        return ssim_map.mean().detach().numpy()
      
    
    

    這邊的 sigma_sq 是先把元素數量 N 提出來,

    看起來有點不直覺要想一下。

    如果你跟我一樣追求效率,就會想把 tensor.Tensor 轉成  tensor.Tensor.cuda 拿來加速。

    這樣會遇到兩個小問題:

    • 在單張圖片(2592 x 1520)下,轉成  tensor.Tensor.cuda 反而比較慢。
    • 轉換後兩張相同圖片精度不是 1。

    第一個小問題應該是轉換時間差,

    第二個問題應該是 CPU 轉 GPU 的 tensor Loss。

    結論:

    • 在單張圖片下, PyTorch 版本的 SSIM 演算法速度會比 Numpy 快兩倍。 
    • 如果圖片不多,可以用 PyTorch(CPU) 轉比較快。

    Ref.

    https://stackoverflow.com/questions/60534909/gaussian-filter-in-pytorch

    https://blog.csdn.net/a2824256/article/details/115013851

    附上 Github Repo

    如何對本文有任何疑問或書寫錯誤,

    歡迎留言或是聯絡我:

    wuyiulin@gmail.com

    2023-11-13 分層捲積的更新

    基於本部落格此篇文章的實現,

    雖然寫得有點醜但親測可用!