顯示具有 study 標籤的文章。 顯示所有文章
顯示具有 study 標籤的文章。 顯示所有文章

2018年8月14日 星期二

OpenCL Image 的妙用





曾提過許多的 OpenCL 的 wrapper 都不重視 Image 這個型別, 僅僅提供 Buffer 的物件, 這樣的作法除了錯誤地認為都僅只是 memory buffer 外, 忽略了 GPU 架構設計上的本質是相當可惜的.

在 CPU 上 memory access latency 的降低仰賴的是 cache, 對於 GPU 而言靠的是 wavefront(warp)/threads 的切換來吸收, 這裡以 Imagination 曾經介紹過其 PowerVR Rouge 的運作機制 來做說明 (其他的 GPU有作法不同但本質相同或類似的方式):

Rogue GPU 一個 Warp 基本上是 32-theads, 內部有著 16組 Warp slot 作排程切換, 由於每個 Warp 中的 32-threads 是以 lock-step 進行, 因此一旦有 thread 的執行狀態沒有被滿足整組 Warp 即被切為 idle state(紅色). 而如同上圖, GPU 是在期間一一切換尋找 ready state (黃色) 的 warp. 而如同影響 CPU 最大的一般, 其中最常出現的即為 memory latency. 一旦 memory latency 過高, 或是 bandwidth 不足就會發生下圖類似 CPU 的 memory stall 的情況:

那麼這與 Image的使用有何關連? - 因為多數的 GPU texture unit 有著自己的 memory pipeline 與 cache.  這表示以 texture unit 搭配既有的 memory pipeline 合併使用, 因此能夠相較於僅使用 Buffer 的情況下, 提供 PE/ALU 更高的 memory throughput.

像是 Qualcomm 提及(TP 即為為 Texture Pipe):
Texture pipe
而像是 ARM Mali GPU 的說明也很清楚:
「Mali Texture Cache」的圖片搜尋結果

其他 Intel, NV, AMD GPU 都有著類似的方式.

在 cltk 中提供了一個簡單的範例 - gemm, 其中 gemm.cl 有兩個 kernel, 兩者主要差異在於一個完全使用 Buffer 而另一個有使用 Image. 相信很多人有疑問, 為何 GEMM 能夠以 Image 方式實作. 這仰賴兩件事
  1. 以整數座標, 並且不使用 sampler 的方式來透過 read_imagef 來讀取 Image
  2. data layout 的擺放方式為 CL_RGBA
滿足上述的方式, 就可以依序讀取 float4 (或是指定的資料型別)的向量.

使用 image 的 kernel:
__kernel void sgemm_buf_img(
   __global const float *A,
   const int lda,
   __global float *C,
   const int ldc,
   const int m,
   const int n,
   const int k,
   __read_only image2d_t Bi
){
    int gx = get_global_id(0) << 2;
    int gy = get_global_id(1) << 3;

    if ((gx < n) && (gy < m)){
        float4 a[8];
        float4 b[4];
        float4 c[8];

        for (int i = 0; i < 8; i++){
            c[i] = 0.0f;
        }
        A += gy * lda;

        for (int pos = 0; pos < k; pos += 4){
            for (int i = 0; i < 4; i++){
                b[i] = read_imagef(Bi, (int2)(gx >> 2, pos + i));
            }

            for (int i = 0; i < 8; i++){
                a[i] = vload4(0, A + mul24(i, lda) + pos);
                c[i] += a[i].x * b[0] + a[i].y * b[1] + a[i].z * b[2] + a[i].w * b[3];
            }

        }

        for (int i = 0; i < 8; i++){
            int C_offs = mul24((gy + i), ldc) + gx;
            vstore4(c[i], 0, C + C_offs);
        }
    }
}
不使用 Image 的 Kernel:
__kernel void sgemm_buf_only(
   __global const float *A,
   const int lda,
   __global float *C,
   const int ldc,
   const int m,
   const int n,
   const int k,
   __global const float *B
){

    int gx = get_global_id(0) << 2;
    int gy = get_global_id(1) << 3;

    if ((gx < n) && (gy < m)){
        float4 a[8];
        float4 b[4];
        float4 c[8];

        for (int i = 0; i < 8; i++){
            c[i] = 0.0f;
        }
        A += gy * lda;

        for (int pos = 0; pos < k; pos += 4){
            for (int i = 0; i < 4; i++){
                b[i] = vload4(0, B + (pos + i)*ldc + gx);
            }

            for (int i = 0; i < 8; i++){
                a[i] = vload4(0, A + mul24(i, lda) + pos);
                c[i] += a[i].x * b[0] + a[i].y * b[1] + a[i].z * b[2] + a[i].w * b[3];
            }

        }

        for (int i = 0; i < 8; i++){
            int C_offs = mul24((gy + i), ldc) + gx;
            vstore4(c[i], 0, C + C_offs);
        }
    }
}

以下為在個人的 Geforce 860M 上的執行結果
$ LD_LIBRARY_PATH=. ./gemm
build log:
cltk_image_alloc w[1024] h[1024] p[1024] us[16]
B pitch 1024 usize 16
pitch 16384
main test overhead gemm takes 103910 us
check result 1...
main test overhead gemm_buf takes 130500 us
check result 2...
兩者間有著近 30% 的效能差距, 若搭配 local memory 的使用或是在手機平台, 效能差距比例應會更明顯.

這個 GEMM 的範例主要參考自 Qualcomm 的 Optimization 文章, 並撰寫對比的 kernel

2018年7月26日 星期四

Vulkan Learning Resource



這篇主要在於集中整理先前在 Facebook 分享的各類 Vulkan 資源

Tutorial 類

Vulkan Tutorial
相當高品質的 Vulkan 教學網站, 甚至提供了電子書版本
https://vulkan-tutorial.com/

LunarG Tutorial
這是 LunarG 提供的 Vulkan SDK 文件的一部份
https://vulkan.lunarg.com/doc/sdk/latest/linux/tutorial/html/index.html
另外 LunarG 提供了相當有用的 Spec 參照
https://vulkan.lunarg.com/doc/view/latest/linux/vkspec.html

Intel  - API without Secrets: Introduction to Vulkan series
這系列文為 March, 2016 開始連載, 至今都還在更新, Part 7 為 April, 2018 發表, 日後會持續在此更新文章聯結
Part 0: Preface
Part 1: The Beginning
Part 2: Swap Chain
Part 3: First Triangle
Part 4: Vertex Attribute
Part 5: Staging Resource
Part 6: Descriptor Set
Part 7: Uniform Buffers — Using Buffers in Shaders

ARM
Vulkan API Tutorials

Misc
http://ogldev.org/www/tutorial50/tutorial50.html
https://gist.github.com/graphitemaster/e162a24e57379af840d4


主題文章

在各大 GPU vendor 有著許多說明 Vulkan 特定設計或設運作機制的說明文章, 不一定是 programming 教學, 文章都有所要深入探討的主題.

GPUOpen/AMD
以下項目主要是 GPUOpen 頁面上 Vulkan 標籤選取的

Nvidia
以下項目主要透過 Nvidia Developer 頁面的 Vulkan 標籤取得
ARM
Imagination
Intel
Khronos

2018年7月23日 星期一

在 Vulkan 中使用 OpenCL Kernel


OpenCL kernel 在 Vulkan 上的使用

OpenCL 自 2008 年底釋出 1.0 至今過了十個年頭,
在 GPGPU 面向上也累積了相當的開發者社群,
儘管 Desktop 上熱烈程度不如 Nvidia CUDA,
而 Mobile 上因 Google 的錯誤決定去強推 RenderScript,
造成手機商在配置與否的態度不一
這樣的情況造成 OpenCL 的使用程度上既不熱烈,
然而許多情況下又是唯一方案


在 Vulkan 的推出後,
由於其基於 SPIR-V 標準的特性, 讓許多應用能夠依附於此之上
其中之一即為 OpenCL kernel 的使用
透過預先以 compiler 將 OpenCL kernel 轉為 SPIR-V binary
再搭配 Vulkan 載入與使用 Compute Shader 的方式, 得以間接的使用 OpenCL 語法

所需的第一個工具為:
https://github.com/google/clspv
clspv 為將 OpenCL kernel 編譯出 SPIR-V 的 Compiler

再來即為 Vulkan 對應 OpenCL Host Code 的使用流程
網路上可以找到下列三篇提及 OpenCL Host => Vulkan 流程轉換的 Blog 文
OpenCL -> Vulkan: A Porting Guide Part I
OpenCL -> Vulkan: A Porting Guide Part II
OpenCL -> Vulkan: A Porting Guide Part III
以及該系列文作者於 Khronos Munich 2017 的簡報: OpenCL to Vulkan: A Porting Guide

若跳過對 OpenCL 與 Vulkan 的對應, 想直接切入 Vulkan Compute Shader 流程
該作者的 A simple Vulkan Compute example 一文以及 GitHub gist 幫助就相當大

個人透過上述幾篇, 實作了上述 Compute Example 等義的 CL kernel
並整合開源放上 https://github.com/champyen/vkcl
以 Intel HD Graphics 500 搭配 Mesa Vulkan 實作, 能正確執行

儘管因此能夠撰寫 OpenCL kernel 來在 Vulkan 環境中使用, 然而並不代表沒有任何限制
相關限制都在 clspv 的 OpenCLCOnVulkan 文件中

Kernel
  • argument 非 buffer/image 型別, 會透過整合到一個 buffer 方式傳入 
  • WorkGroupSize 無法透過 host 指定, 似乎只能以 __attribute__ ((reqd_work_group_size(x, y, z))) 方式指定
  • 無法呼叫其他 Kernel
  • 不能使用 local 作為 argument
  • 不能使用 half 作為 argument
(事實上, 相關 argument 自 Host 傳遞到 Kernel 的機制也並不是很方便)

Types
  • boolean 無法作為 global, constant, 而 sizeof 為未定義行為
  • 8-bit, 64-bit, event_t 型別無法使用
  • 沒有長度為 8, 16 的向量 (也就是儘支援向量長度 2, 3, 4)
  • pointer 的限制很多
Built-In Functions
這部分相當的多, 都是無法使用, 簡短提示如下:
  • atomic: atomic_xchg()
  • common: step(), smooth_step()
  • conversion: 俱備 rounding, saturation 都不可使用
  • math, integer, rational: 這部分特定的 function 無法使用, 請參考說明
  • async, shuffle, printf, vload/vstore: 請直接當不存在 ...

Vulkan GLSL Compute Shader

OpenCL kernel 在 Vulkan 1.0 上運作需要兩個 extension:

  • SPV_KHR_variable_pointers
  • SPV_KHR_storage_buffer_storage_class
此兩個 extension 於 Vulkan 1.1 中成為核心特性 (因此日後可以基於上述限制來使用 OpenCL)
若考量 Vulkan 1.0 裝置的相容性, 那麼採用 GLSL Compute Shader 方式撰寫是不錯的選擇

此外 Vulkan GLSL compute shader 的相關範例也並不多, 以下是看到覺得不錯的
https://github.com/Erkaman/vulkan_minimal_compute
https://github.com/alexhultman/libvc
https://arm-software.github.io/vulkan-sdk/basic_compute.html

撰寫 Vulkan GLSL compute shader 也需要 compiler 產生 SPIR-V
這時 LunarG 的 Vulkan Toolchain 能夠直接下載使用
https://vulkan.lunarg.com/sdk/home

update: 值得一提的是與 OpenGL GLSL compute shader 的不同, OpenGL GLSL compute shader 制定時, 能使用的 builtins 受限於既有 GLSL 部分, Vulkan 制定的時候對其做了擴充, 能使用的函式較接近原有的 OpenCL:
https://www.khronos.org/registry/spir-v/specs/unified1/GLSL.std.450.mobile.html

2018年7月12日 星期四

接近完成階段的 Huge Page Swapping

來源: The final step for huge-page swapping

現今的系統有著大量的記憶體, 軟體對於記憶體的需求也愈來愈高, 以 4KB-page 為單位對於系統的 MMU 與 TLB 消耗了大量的 entries. 為了避免這方面系統平台的 overhead, 通常會採用 Huge-page (在 x86 上 Huge-page 為 4MB, arm64 為 2M/1G(4KB-page) or 512M(64KB-page)), 來提高系統的效能.

然而長久以來, 管理者都避免對 Huge-page 作 swapping 的動作, 主要在於 Huge-page swapout 在目前 Linux kernel 的實作 - Transparent Huge Page(THP), 會將 Huge-page 切分為 4K-page 再做 swapout, 一方面以往硬碟的寫出效能不佳, 二來需要一次寫出大量的資料, 最後是 swapin 後是以 4KB-page 的方式回復.

隨著非揮發性記憶體在軟硬體上的進展, 改變了相關的生態, 讓 swapping 這件事情再次地變得有趣. 而 THP 也從 2016 年開始帶來了相關的變革, 這些變動來大幅地增進了效能:
  1. Linux 4.13 中 THP 延遲了 Huge-page 的拆分動作, 直到 swap entries 被配置之後, kernel 才會做拆分. (因此在 DRAM 中有短暫的時間消耗了 Page-Table Entries)
  2. 緊接著在 Linux 4.14 中再次將拆分的動作延遲到寫入 swap device 之後, (因此在 DRAM 中都保持著 Huge-page)
  3. 目前已經實作了 Huge-page 後續管理的部份, 主要在於攔截可能會讓 Huge-page 拆分的 kernel event, 並且在 swapin 時儘可能地維持住 Huge-page 的方式. 一旦系統有著餘裕提供 DRAM Huge-page, 那麼就可以享有其帶來的好處.

依照在 patch 提交時所附上的 benchmark 的數據顯示, 未修改的 kernel 僅有 4.5% 的資料最後以 huge-page 存放. 而套用 patch 後提升為 96.6%. Inter-process interrupt 降低了 96% 此外 spinlock 等待的時間自 13.9% 降為 0.1%. Swapping 的 I/O throughput 效能增加超過 1000%. 通常 kernel 工作者必須很努力才能取得 1% 規模的效能增益, 如此的增進幅度是罕見的.

2018年7月9日 星期一

關於 ARM 主動與 RISC-V 比較的個人看法

看起來 RISC-V 開始對 ARM 有所威脅了, 先前一直冷處理 RISC-V, 希望能讓其淡出, 然而在某些面向已經無法忽視, 因此 ARM 設立了 risc-basics.com 這個網站針對五點面向與 RISC-V 相較:
  •  Cost: 任何 RISC ISA 授權的成本僅佔創建商用處理器從設計到交付的一小部分.  
個評: 製作 ARM 相容處理器的 ARM Architecture License 是最高等級的授權, 付出的成本最高, 成本最低的是直接使用 ARM 推出的硬體設計, 並且隨著客製彈性降低而費用降低. 其中選擇的依據還是要依照應用需求決定你的付出成本. 所佔比例是依照應用與量能決定比例, 而非這麼輕描淡寫的是"小部分".
  •  Fragmentation Risk: RISC-V指令集架構允許 IP 商曾加自有擴充. 這表示著每個實作可能不同或客制.
個評: 付費取得 ARM Architecture License 授權的公司(最著名的莫過 Apple, 而取得 Architecture License 的按照 wikipedia 包含了: Apple, AppliedMicro, Broadcom, Cavium (now: Marvell), Nvidia, Qualcomm, and Samsung)一樣可以依照意願修改與延伸, 使用了 ARM processor 也並非沒有碰到這問題的可能.
  •  Design Assurance: 處理器設計的verification 和 validation 可能佔總設計時間的75%. 使用RISC-V可以修改指令集架構意味著對中央處理單元, 進行昂貴的重新驗證以及軟體工具的客製, 這些增加了設計成本.
個評: 開發與修改處理器主要還是應用面向的考量, 處理器設計的更動帶來的利益與設計增加的成本這是科技公司策略上該考量到的, 而且自主架構有利於技術的提升以及產品的獨特性及差異性, 而 ARM 設計上有著自己的步調無法配合所有廠商, 再者也無法一體適用所有應用, 像是 ARM 遲遲沒有推出對於 throughput 有幫助的 SMT 設計, 而客戶遷就使用 ARM 設計造成的落後(效能與市場)是設計代工的 ARM 不會替客戶考量的損失.
  •  A Large, Supportive Ecosystem: 重要的是架構上得到全球及成熟的合作夥伴生態系統的良好支持, 提供各種軟件, 服務和設計支持. 這保證了市場選擇, 產品質量和最佳的上市時間. RISC-V生態系尚未達到此發展階段.
個評: 目前得到的領先多少是因時空有利條件綁架造成的結果. 儘管 RISC-V 尚未達到同樣質量的階段, 然而 ARM 與 RISC-V 並非是同一個起點, RISC-V 起跑較晚, 但從 ISA design, compiler 到 OS 發展都是有著各方面多年累積的經驗, 這些都能加速軟硬體架構的成熟速度, 加上受到開放社群的熱烈支持與投入開發. RISC-V 快速的成長也是不可忽視的事實.
  •  Security: Cyber​​threats 意味強韌的著芯片安全性並非選擇性問題。基於RISC-V的產品相對較新,並且尚未受益於合作夥伴和行業專家多年的審查.
個評: 採用了 ARM 安全方案的 Qualcomm 也曾爆出安全漏洞, 安全問題很難單以這樣陳述的方式簡單說明俱備優勢, 架構上 ARM 也未能從 Spectre 問題逃脫, 從重大事件上觀察, 可以知道 Linus 說的 "security problems are primarily "just bugs."" 其實是有道理的.

延伸閱讀:
有興趣了解 ARM 與這類 IP vendor 的商業模式, 可以參考 Anandtech 的這篇: "The ARM Diaries, Part 1: How ARM's Business Model Works" 文章, 特別是 page 3 的 Type of Licenses

2017年9月4日 星期一

Kirin 970 NPU - 看圖說故事

從 Image Sensors World 看到了華為旗艦手機晶片 Kirin 970 中 NPU 的投影片
其中有趣的是第五張圖


"25X performance, 50X efficiency"
相較於 CPU 有著 25倍性能 與 50倍的效率
由於 Performance 有相當多比較的面向, 因此不清楚是用絕對的指標(total GFLOPs, or per core) 或是相對指標 (OPs/cycle/core), 甚至是實際應用的時間來做比對(eg. FPS)
但是若是 Efficiency 談的比較可能的是 Performance Per Watt (另外還有 area efficiency, 甚至一起綜合看)
Efficiency 比較上為了有較好的數字, 比較可能的是與大核等級的 CPU 相比, 因此這裡以 Kirin 970 中的 Cortex-A73 為對比
以 NEON 的 GFLOPS 來看2.4Ghz Cortex-A73 單核的 GFLOPs 為:
2.4Ghz * 4 lane * 2 mad * 2 unit = 38.4 GFLOPS 
(寫到這就知道 Performance 不是以絕對 GFLOPS 計算)
而若 TSMC 10nm 製程能將 2.4Ghz Cortex-A7x 最大功耗(Peak Power)壓低至 1 Watt/Core(這是個人經驗粗略推估數字), 那麼 50X 效率可以推得:
38.4 x 50 = 1920 GFLOPs/Watt =  1.92 TFlops/Watt
(寫到這覺得蠻剛好的)
又可以從投影片上得知華為對外宣稱其 Kirin 970 內的 NPU 有著高達 1.92 TFLOPS/s (FP16) computation throughput




那麼可以由上面的比較, 對於 NPU 的最大功耗能推估為 1 Watt
以此看推估觀察運作的功耗圖介於 0.3~0.7 Watt 看來也是合理的(畢竟應該有著 Thermal/Power 管理面向的 DVFS)


2017年5月11日 星期四

有趣的 cache line 效應

在忙著處理一些個人事務的時候, Jim Huang 請我幫忙回覆學弟妹碰到的 一些問題
主要是 Jim Huang 修改了之前 Matrix Transpose 的例子作為範例給學弟妹們練功使用
而學弟妹碰到了一個問題, 就是對於 Matrix Transpose 的寬做調整時
執行時間與長寬度的調整的分佈圖如下:
如上圖所示, 隨著對寬作 4096 + (i*64)的調整時經分佈呈現鋸齒狀的變化!
依照文中敘述的實驗平台為俱備 6MB cache 的 Core-i5 6300HQ
以 Intel Skylake 架構而言, 為 64 bytes cache line, 8-way associative cache
基本上俱備 6MB/64/8 = 12288 sets (entries)
以這個 int matrix 而言, 原始的寬度為 4096x4=16384 bytes
16384/64 = 256
因此每增加一行記憶體基本上會跳躍 256 cache line index

接著再觀察 naive transpose 的實作:
https://github.com/yangyang95/prefetcher/blob/master/impl/naive_transpose.c
若 buffer 對應的 cache line index offset 為 X

首先考慮最簡單的 i == 0 的情況
對於第 n 行的 cache line index 計算為: (X + 256*n) % 12288
1. 12288 / 256 = 48 (自 cache line 0 ~ 12288, 每次跳 256 可以消耗 48 行)
2. 12288 % 256 = 0 (每次的 cache line index shift)
所以填完 output 第一行來說, 這也代表著當資料(二維座標以(x,y)表示)自 (0, 0), (0, 1) .... (0, 4095) 要讀 (1, 0) 時
而相關的 cache line 早已被反覆地填入了 4096/48 = 85.33次 > 8
當 CPU 需要讀入 (1, 0) 時, 因為讀入 (0, 0) 所讀入的 cache line 有相當高的機會早已被 replace

接著當每次寬度增加 64 時(也就是 256 byte, 4 cache lines)
這裡我們先考慮 i == 1 的情況
對於第 n 行的 cache line index 計算為: (X + 260*n) % 12288
1. 12288 / 260 = 47.26 ...
2. 12288 % 260 = 68
260 與 68 兩者最小公倍數為 4420
4420/68 = 65  (表示反覆繞 cache line index 0 ~ 12288 到第 65 次才開始 cache line index 有所重疊)
而 4160/47.25 約為 88.02, 88.02 / 65 = 1.35 < 8
這表示在讀取 (1, 0) 時, 有很大的機會 (0, 0) 所填入的 cache line 還在

再考慮 i == 8 的情況
對於第 n 行的 cache line index 計算為: (X + 288*n) % 12288
1. 12288 / 288 = 42.6666....
2. 12288 % 288 = 192
而 288 與 192 兩者最小公倍數為 576
576/192 = 3 (表示反覆繞填 cache line index 0 ~ 12288 到第 3 次 cache line index 就有所重疊)
而 4608/42.666 約為 108, 108 / 3 = 36 > 8
這表示在讀取 (1, 0) 時, (0, 0) 所填入的 cache line 有相當高的機會早已被 replace

由類似的計算可以得知, 造成這樣的時間分佈的原因, 而 SSE 版本的成因也是基於相同道理
而這樣的效應更顯示程式的記憶體使用方式, data layout 與 cache 影響程式的效能甚巨

2017年4月2日 星期日

"ARM Compute Library for computer vision and machine learning" III - 總結

在實作上核心的實作是在各功能的 Kernel 類別實作中
因此若想瞭解可以去研讀各個繼承 IKernel 的 CL/NEON 實作
由系列文 II 多少可以了解 ARM Compute Library 是如何的工具
在官方的介紹也說明了這是 - a collection of low-level software functions
這樣的好處是設計簡單且易於使用, 若所需要功能不複雜, 其所提供的工具也相當堪用
但是若一個目的是需要使用多個 Kernel 的串連來達成
如此的應用就需要更進階的方式來作優化
以 OpenVX 來說即為其 Graph pipeline
基本上需要透過一個更為高階的抽象層
為問題帶入各個 stage 的分割與相關排程的分析
對於 ARM Compute Library 而言每個 function 需要 I/O image buffer
能接近這樣的方式在於兩個 stage 間以 Window + Thread 的 Tiling 方式
如此也僅是有限度地利用 data locality 的特性增加 cache 的有效性
況且對於 Load/Store 指令可是一個都沒能因此節省
(這需要能作 operation fusion 的 compiler)
這即是 ARM Compute Library 在效能與進階功能上的局限


然而若需要進一步解決上述的局限
會需要能針對 temporal/spatial scheduling 作 dynamic code generation 的 compiler以及 runtime
實作複雜度亦會大幅增加 (即 OpenVX/Halide 或類似的實作)

2017年1月25日 星期三

解析 Qualcomm Hexagon 680 架構 III - Thread Model and Execution Model

Thread Model

最後讓我們講述 Hexagon 680 的 Thread Model
在 Qualcomm 開發 DSP 的過程中一直都有 Hardware Threads 的支援
因此在談 Hexagon 680 的 Thread Model 前, 讓我們先來看看 V5x 的 HW Threads

上圖為 Qualcomm 在 2013 年 Hotchips 會議上投影片的 Page 11
從這圖可以看到 Hexagon 680 的 V6 基本上與 V5 架構差異不大
也是 4-slot VLIW, 兩個是 Data Unit, 兩個是 eXecution Unit
同個 DSP 核心具有3個 Thread Context, 在硬體設計上已考慮簡化多工上的需求

在 2015 年度 Hotchips 中 Qualcomm 簡報的 Page 8:
由此可見, V6 Scalar DSP 以 2Ghz 頻率運作
俱備 4 HW Threads 支援, 每個 thread 以 500Mhz 運作
而 HVX coprocessor 以 1Ghz 頻率運作, 具有兩個 context, 一樣每個 thread 以 500 Mhz 運作
因此同一時間可以有兩個 Thread 控制著 HVX, 另外兩個處理 Scalar 工作
但光以上看投影片, 不知道是否有人懷疑過, 或許 V6 DSP 為 quad core, 而 HVX 為 dual core?

這裡我們回到 HVX 官方文件當中:
從這張圖可以看出, V6 DSP 具有的是 1C4T, 而 HVX 為 1C2T
但是, 官方在投影片與到此中所說明的, 並非 Hexagon 680 所有的模式
如果還有印象, 在系列文II當中留了一些不確定:
1. HVX 對於 L2 的總寬度為 512b x2, 而非 1024b x 1
2. 對於 VPF/VRF 的描述, 我加了 "基本上" 這個有所保留的詞

再談這之前, 先來介紹 HVX 的 Vector Modes:

也就是 HVX 中支援了兩種不同長度的 Vector
  • 64B 模式, Vector 寬度為 512 bits
  • 128B 模式, Vector 寬度為 1024 BITS
而這樣的模式並不是像 NEON 中分為長短 vector 並且可以混用, 而是如上圖, 直接將所有的單元(VX, VPF, VRF), 以 512b 寬度的方式等分為二

所以除了 1C2T(128B) 的方式外, 事實上 HVX 還存在另一種 HW thread 的型態 - 1C4T (64B)
在這個模式下, 4 個 V6 HW Threads 各自都可以控制一個 64B Vector Mode 的 VX Thread Context. 所以 Hexagon 680 提供多元且彈性的運作模式. 而這也說明了 系列文II 當中沒交代清楚的部分.

Execution Mode


Qualcomm 曾經官方撰文介紹過 Hexagon DSP 的軟體開發
在 Qualcomm 裝置上, 在 DSP 上執行工作需要幾個步驟:
  1. 下載與安裝 Qualcomm Hexagon SDK
  2. 將你於 DSP 上實作的功能 API 以其 IDL (Interface Description Language) 方式描述.
  3. SDK 會自動產生 Header files 與 stub (下圖中應用處理器端, 也就是 ARM 處理器端提供給 client 的呼叫介面, client 通常是其他的 library 或是 executable)及 skel (DSP 端去呼叫你實作功能的進入點) 等相關函式庫.
  4. 在 DSP 端實作你的 API, 並與第3點中自動產生的 skel 函式庫 link 成為一個 shared object 並放置於該平台裝置中.
  5. 將你的 native APP 與 link 到第3點自動產生的 stub 來呼叫你的 API.
  6. (選擇性) 建立你的 API 專屬的 Java bindings 好讓 APK 能夠使用.
而這些背後所仰賴的機制的核心即是上圖所描述的 FastRPC

一旦 Native APP 呼叫所需的 function, 便會執行自動產生的 Stub lib, 它會 透過 ARM 與 DSP 間橋接的 Driver-Framework, 載入先前所產生的 DSP 端的 shared object, 而 DSP 端上的 OS 會動態地載入該 object, 並且以 Skel 介面處理來自 ARM 處理器端的請求.

2017年1月22日 星期日

解析 Qualcomm Hexagon 680 架構 II - Hexagon Vector eXtension (HVX)

在講解 680 中的 SIMD 單元 - HVX 之前, 還是先以系列文 I 的 blocks diagram開頭, 並且今日重點會是文中提到第3點的官方文件

從 blocks diagram 中可以看到 HVX 由三個主要部分所組成
  • VX : Vector Extension Unit, 也就是 HVX 的 SIMD 指令執行單元
  • VPF : Vector Predicate register File, 這部分是用來處理 SIMD 的中較不易處理的 branch, if-else 等 divergence. 以 8bit 運算來說, 若需要提供 SIMD 中每個 lane 所需的 true/false boolean 資訊, 需要 1024/8 = 128 bit, 而 Predication Register 寬度, 基本上就是 128b.
  • VRF : Vector Register File, 這即是 VX unit 中用來存放的暫存器, HVX 為 1024b vector, 因此基本上這部分為 1024b
而對外的介面有兩個:
  • Co-Processor Instruction Port: 用來與 V6x 處理器溝通的, V6x DSP 是透過這個介面將指令送給 HVX 單元執行
  • 兩組 512b-wide L2-cache/L2-TCM port: 相較 V6x 處理器對於 L2 的頻寬為 256b port, HVX 對於總寬度為 512b x2 , 這樣的設計很重要, 這讓 HVX 能夠更快的取得所需要的 vector 資料, 由於 HVX 為寬度 1024b vector 的 SIMD Unit, 因此這樣的寬度是有必要的. 至於為何是兩組 512b 而非一組 1024b, 這部分與其 thread model 有關, 在下一篇會介紹這個部分.

首先來談 VPF/VRF 的 Register File 部分, 在 HVX 中分別有著 32組 vector register 以 V0 ~ V31 表示, 而 predicate register 共有 4組, 以 Q0 ~ Q3 表示, 這些表示法用在 SDK toolchain 所產生的 .s assembly 中, 對於 V0 ~ V31, 並沒有區分資料型別, 因此為了表示使用的精確度, 在指令中對於 register 使用的方式必須有標記所
從語法中可以看出兩點, 首先是並不支援 float 運算(當然以這樣的notation 要新增也不是難事就是了), 再者是 SIMD 的 operation 與 data type 是脫勾的, 重點是搭配的 register data type, 以上面的例子來說
V2.h = VADD(V3.h, V4.h)
這個 asm 指令是指套用的是 add 指令, 並且資料型別是 h 所代表的 signed halfword (也就是通常 C 中的 short), 因此這行所代表的意義為: 將 V3 與 V4 的資料以 signed 16b 的方式相加後存入 V2. 這樣的 asm 設計也直接影響到了 HVX 在 C intrinsics 上的 coding style, 而這樣的設計其實有好有壞. 好處顯而易見的是 asm 很簡單, 而且能有效的降低寶貴的 register 浪費, 但這樣的設計當 export 到 C 的層次的時候, 就缺乏了對於各資料型別的 vector type, 因此沒有對應型別的 operator overloading, 但這點若採用 C++ 去自己實作也並不難就是了.

接著進入 HVX 的指令類別, HVX 有著六類的指令類型, 在系列文 I 當中也討論過 V6x 各個 slot 的支援, 而 HVX 也是使用相同 VLIW4 的設計, 針對這六類支援的指令, 於官方文件中有著下列表格:
從表格中可以出六類分別為:
  • Aligned Memory Load
  • Aligned Memory Store
  • Unaligned Memory Load/Store
  • Multiplies and special ALU
  • Vextract
  • Simple ALU, Permute, Shift
對於表格的呈現方式, 各位或許會覺得很奇怪, 為何不是像 V6x 處理器中那樣, 是以 slot 為主要分類, 而是以 instruction type 為主要分類. 這主因在於對於同一 cycle 中4個指令的使用上搭配要考慮的不是 slot 本身而已, 因為對於 HVX 來說支援每個 instruction type 所動用到內部的 "resource" 不同. 那什麼是 resource  呢?

在官方文件中有說明, HVX 本身由六種 resource 所組成, 這些 resource 讓 SIMD 指令得以運作
因此弄懂每個指令背後所動用到的 resource 也是重要的:
等等!!! 不是6類嗎, 怎麼變成11類了? 其實細看一下, 你會發現它只是將一些指令細分, 特別是第4類與第6類再去細分, 但是基本上還是那六類. 像是 Multiply 區分了 8b 與 16b 的部分, 這裡請注意, 8b 與 16b 這兩者分別使用的 Multiply Resource 數目不同, 所以這裡讓我們以官方在 Hotchips 中的 slides 對於效能作進一步解釋的說明:
上面這張 Qualcomm 官方投影片很常出現在一些淺談 Hexagon 680 與 HVX 的文章中, 但請注意其中的第2大點的 8b 與 16b 的乘法數目, 相信很多人很難理解(或者根本沒弄懂)為何 8b 的乘法數目為 16b 的 4倍, 因為 1024/8 = 128, 而 1024/16 = 64, 單以 SIMD 的寬度是無法解釋這樣的落差, 但這時若將指令背後 resource 的消耗數目納入考量就會很清楚了, 因為 HVX 處理器中提供了兩組 Multiply Resource, 而 16b 的乘法指令按照上面提供的表格一次即會消耗完兩組,  而 8b 的乘法指令只會消耗一組, 因此同一個 cycle 中, 至多只能塞入一個 16b SIMD 乘法 也就是 1024/16 = 64 個, 而 8b 可以一次執行兩個 8b SIMD 乘法, 也就是 (1024/8)*2 = 256 個.

所以若不考慮 data dependency 因素, 一個指令能否再塞進一個 VLIW 指令中以同一cycle執行, 這背後必須要同時考量 slot 是否 conflict, 以及 resource 是否有餘裕, 這兩者同時都滿足, 才能夠塞進同一個. 對於這樣的考量, 官方文件提供了一份整理好的總表
在這份總表中, 查找所需滿足的條件, 即反映了是否需要額外的 cycle 去執行所想加入的指令. 另外需要注意一件事, 請注意 resource 使用的平衡性, 像是 shift unit 只有一個, 千萬不要不明就裡, 將乘法全以 shift 替換就以為是優化, 在這樣的情況下, 有可能增加的 cycle 數目是以倍數來計, 對於這樣的處理器, 優化的策略在於儘量在乘法與Shift所需要的運算數目中取得平衡. 其他比較常考慮的就是 load/store unit.

對於 HVX 指令中是否有讓我覺得印象深刻的? 我個人認為是 vdelta/vrdelta 以及 permute 所屬指令, 這個類別與一般 DSP 常提供的 PACK/UNPACK 太不同了, 它提供了一個 general permute network 來做兩個 vector register data 的次序調移.
其實當下我覺得這東西一時間我只想到 sorting network, 而且還搞不清楚狀況, 直到看到了一個圖解, 發現這 1 cycle 運算真是潛力無窮, 所有 1-1 mapping 的方式的次序改變, 只要能算得出 Vv, 就可以快速地作 Vu 至 Vd 的轉換. 而對於像我腦子這麼簡單的人, 它也提供了入門版的 shuffle 與 pack 指令.
最後來談 HVX 的資料取得方式來源 - L2-cache/L2-TCM, 對於一般 DSP 在資料傳輸與核心運算的平行部分, 儘管 HVX 文件沒有特別強調 DMA, 但實際上 HVX 提供了對應功能的 L2FETCH, 以一個多媒體為訴求的 DSP  來說, 其也支援了 1D/2D 資料的傳輸.

而到此並沒有特別的地方. 值得一提的是, 這裡的 Level-2 是只對於 V6x 而言, 但對於 HVX 的角度來說, 它就是如同 L1 的存在, 最特別地方在於 V6x 架構中 L2 可以扮演兩個角色, 在 block diagram 中已經標明了
  • L2 data cache
  • L2-TCM
這設計是很有趣的, 因為這兩種記憶體各自有著不同且互補的特性:
  • cache本身有著 transparent 特性, 對於 programmer 本身並不需要特別改變, 只需要注意沒有不利於 cache 管理機制的存取方式,  cache 能夠帶來效能的 scalability (隨著 cache 的加大而效能改善), 功能上的 portability (無論 cache 大小, 功能本身的正確性不會影響), 這樣的特性對於一些高度 locality 的程式很有幫助, 也能降低程式的實作複雜度, 但是 cache 本身有著難以精確控制的特性, 對於一些資料的存取並不能有時間上的保證.
  • Tightly-Coupled Memory (TCM), TCM 有著指定的位置, 距離 processor 很近, 有著存取時間上的保證, 但是由於使用上 Programmer 必須意識到它的存在, 並且自己管理, 所以 TCM 若容量增大也在不修改程式的情況下無法直接增加效能; TCM 若容量變小在不修改程式的情況下程式可能無法正常運作. 但是它卻能有時間上存取的保證, 因此對於一些像是 Lookup-Table (LUT) 的需求, TCM 顯得相當重要
在 V6x 中能將 L2-cache 做分割, 雖然相關機制未知, 但是運作上能將部分 L2 作為 Cache 而部分作為 TCM 是標注在 V6x 的 features list 中:
下篇文, 沒意外的話也是系列文最後一篇, 要談論的是 Hexagon 680 的 Thread Model 以及 Execution Model

2017年1月21日 星期六

解析 Qualcomm Hexagon 680 架構 I - V6x 架構

Qualcomm S835 於不久前發佈
其中內建了強大的 Hexagon 682 DSP
雖然在 S835 的發佈宣傳文中僅說明了新增了對於 TensorFlow 與 Halide Language 的支援
但是還是有發佈專文說明 Hexagon 682 DSP 即便與 Adreno 540 GPU 相較下
在 TensorFlow 與 Machine Learing 方面的效能與功耗效率的其表現出色
儘管 DSP 處理器這字眼對於程式工作人員並不陌生
但除了特定領域面向的工作者外, 有機會接觸者並不多
這與應用平台的可見度與普及性
以及各家 DSP 相關的軟硬體在架構與軟體資訊較為封閉有關

Qualcomm 自 S820 加入的 Hexagon 680 這顆運算性能強大的 DSP
(事實上先前即有 V5 的 DSP,  但是 HVX 讓 V6 增色不少)
相關文件與 SDK 皆有公開釋出, 所以是個很好的說明範例
而 Hexagon 682 應該與 680 相去不遠
因此這裡以系列短文介紹探討這顆 DSP 的架構

探討的內容為 Qualcomm 官網內的一份簡報與兩分文件 (需註冊帳號, 免費)
1. Architecture of the Hexagon 680 DSP for Mobile Image and Computer Vision
2. Hexagon V60/V61 Programmer's Reference Manual
3. Hexagon V60 HVX Programmer's Reference Manual

Hexagon 68x 為 Hexagon V6x 架構的處理器
而 HVX 為 Hexagon Vector Extension 的縮寫, 以 Coprocessor 的方式與 V6x DSP 整合運作
下圖取自官方文件 3. 的系統 blocks diagram
首先讓我們先以上圖來討論 Hexagon V6x DSP
Hexagon DSP 主要由 5 個部分所組成, 其中各個 block 意義如下:
  • D$ - Data Cache
  • RF - Register File
  • I$ - Instruction Cache
  • XU - eXecution Unit
  • MMU - Memory Management Unit
另外有3個對外的控制介面, 分別是:
  • AXI, AHB 對外的 Bus 與 Host 端系統整合
  • 256b-wide L2-cache port
  • Co-Processor Instruction Port 控制 Hexagon VX Co-processor
其中值得注意的地方是 AXI/AHB 以及 L2-cache port 中, V6x 內部中以 L2 Control 與 cache state/tag table 的 function block 連接著, 整個 Hexagon DSP 的資料, 都仰賴著 V6x processor 統一對外透過 AXI/AHB 而取得, 若了存入 L2-cache, 相關資訊則會以 L2 state/tag 紀錄, V6x processor 本身即能夠完全掌握系統中 L1 與 L2 一致性控制的管理. 而 HVX co-processor 即是在 V6x processor 控制流程與 data control 的方式下, 以兩個 512b-wide L2-cache port 存取資料來運作. 即便 HVX 所需資料發生 cache miss, 負責處理這問題的是 V6x proccessor 本身.

首先是 Cache Size, MMU 的相關資訊:
這樣的設計方式與其他 DSP 不同的地方在於它的 instruction 處理部分, 預設是採用 I-Cache 的方式, 以架構來說,  缺乏 I-TCM 的方式對於一些 hard realtime 的應用稍有不利的地方, 但是其支援了 hardware instruction prefetch 的能力彌補來這部分. 而 MMU 部分提供了 4GB 的定址空間, 作為 DSP 的 Virtual Address 與 Physical Address 間著轉換. 由於對於 ARMv8 系統都提供了 > 4GB 定址空間, 因此對於 V6x DSP 使用的記憶體區間應有軟體上或硬體上的限制.

接著來探討 V6x 的剩下的 XU 與 RF, 讓我們用官方的 architecture 圖來說明
透過官方架構圖, 可以看到需要探究的東西增加了, Control Registers 基本上是控制流程(Loop Regs, Predicate Regs)與提供流程相關(Status Regs, Program Counter)資訊的用途, 這裡就捨去不談, 這裡主要談 XU 衍生的 Sequencer + S0 ~ S3 以及 RF 所代表的 General Registers R0 ~ R31

對於 S0 ~ S3 來說, S 代表的是 Slot 的意思, 看到這個字眼稍有敏銳的人就知道接著要說的是 Very Large Instruction Word (VLIW), V6x processor 本身即為 VLIW 架構, 許多的 DSP 都採用這樣的方式, 其優點是:
  • 簡化硬體設計: 對於 DSP 來說若採用 SuperScalar 架構會增加面積在 hardware instruction scheduler 的實作上, 除了增加成本外也增加 power/thermal 的風險
  • 可精確分析與預期效能: 由於 VLIW 的指令排程是透過 compiler 來完成, 而硬體上來說的處理相對簡單, 因此執行上的時間, 若以 single loop iteration 而言 compiler 即能提供資訊, runtime 所需時間, 透過 simulator/emulator 就能得到很精確的時間估測
對於 VLIW 而言, 弄清楚每個 slot 能夠處理的指令是一個重點, 以下為 V6x 的 Slot-Instruction 配置說明:
可以看出, 除了共通的 ALU32 指令外, 每個 Slot  都有著其所被賦予的任務:
  • Slot 0, 1: 主要負責記憶體的 load & store, 其中 Slot 0 更負責了 cache 維護, maintenance 與匯流排的操作.
  • Slot 2, 3: 負責主要的負責數理運算, 以及 Vector 相關指令, 程式內部的流程控制 (J/JR/CR)
到這邊可能會有人想喊 "等等!!! Vector 運算 !?"
是的, V6x 本身即支援了初步的 64b-wide Vector 運算, 下圖為官方的指令範例:
但 V6x 有一點是特別的, 就是它的 Register File
主要是它將 General Register 與 Vector Register 整合在一起:
在操作上亦提供了便利的 32/64b Register Notation
對於 V6x 就以此簡短的介紹, 有興趣者可以再透過官方文件深入
下一篇會介紹讓 Hexagon V6x 大幅提升計算能力的 HVX

在 ARM 平台上使用 Function Multi-Versioning (FMV) - 以使用 Android NDK 為例

Function Multi-Versioning (FMV) 過往的 CPU 發展歷程中, x86 平台由於因應各種應用需求的提出, 而陸陸續續加入了不同的指令集, 此外也可能因為針對市場做等級區隔, 支援的數量與種類也不等. 在 Linux 平台上這些 CPU 資訊可以透過...