2020年9月9日 星期三

正體中文資訊考古 - OS/2 T4.0 正體中文版


後續會陸續把中文相關的檔案上傳 (4.0 的附屬光碟, 3.0 中文版光碟)
主要是最近找了一下 archive.org
發現 OS/2 4.0 原文的東西相當多
 
像是這個 collection, 有人把很多語言版本都上傳了(但是沒有正體中文版)
加上近年其實可以感受到原有的網路資訊逐漸消失
為了做一些歷史資料保存與見證
就花了點時間把 OS/2 T4.0 iso 檔案上傳了
T4.0 真的就是懷舊了, 一點可用性都沒有
原以為更新到 fixpack 5 (OS/2 4.0 最後的 fixpack)就有機會能夠使用 SVGA
但 ... 果然我想太多了 (是說裡面的 telnet 還可以用)
裝了 Netscape Communicator 4.61, 但是因為 https 加密演算不支援, 加上也不支援 HTML4 所以寸步難行
 
話說 eCS 1.2 中文版不難找, 
主要是版本蠻新的, 
有些可能還有在使用 (甚至還能安裝 GCC 9.2)
所以不管是 eCS or OS/2 MCP2/4.52 暫時決定還是不要放到 archive.org
 (也的確 archive.org 上相關檔案不多)
 
 
 
 
 
 
 


2020年7月19日 星期日

以 halide 實作 block matching 演算法 - block matching with Halide language

由於 multi-frame 與 multi-view 的影像處理需求的提升,  image alignment 的需求日益提升, 而相關的演算也很多, 像是基於 pyramid-based, 9-steps, hexagon or diamond search. 而大小的部份也有著 match / apply 不同大小的 non-local means, 然而這些演算的基本還是源自於特定 search window 的 block matching. 本文重點在於以 Halide language 描述 matching algorithm, 至於 scheduling 的優化不在此文重點. (或是擇日分享)

Step 0 - 輸入格式 (input format - single channel as example)
首先假設要處理的是 image 的某個 channel, multi-channel 的影像則僅是套用到多個 frame 或是以 channel 輸入, 同常有一張是 base image 這裡以 in 代表, 另一張為 reference image 以 ref 代表.

Step 1 - 邊界處理, 用來避免 search block 範圍超過邊界 (boundary handling, to avoid error of part of target block is out of frame)
Func input = BoundaryConditions::repeat_edge(in);
Func refer = BoundaryConditions::repeat_edge(ref);

Step 2 - 用以代表 block 內所有 input 與 reference 的 pixel 的座標 (setup the RDom that represent all pixels of corresponding input & refer block)
// the position in motion vector output table
Var bid_x, bid_y;
// variable for motion vector
Var mv_x, mv_y;
RDom rblk(0, BLOCK_SIZE, 0, BLOCK_SIZE)
// position in input frame
Expr blk_x = (bid_x * BLOCK_SIZE) + rblk.x;
Expr blk_y = (bid_y * BLOCK_SIZE) + rblk.y;
// position in reference frame
Expr ref_x = blk_x + mv_x;
Expr ref_y = blk_y + mv_y;

Step 3 - 建立比對標準, 這裡以 SAD 為例 ( matching criteria calculation (Sum of Absolute Difference(SAD) as example))
Expr in_val = i16(input(blk_x, blk_y);
Expr ref_val = i16(refer(ref_x, ref_y));
Func sad;
sad(mv_x, mv_y, bid_x, bid_y) = sum(abs(in_val - ref_val));
Step 4 - 搜尋與輸出, 這裡是透過 RDom + argmin 搭配來做比對搜尋並取得結果 (search and output, here we use RDom + argmin for matching and finding)
// setup search window
RDom search_win(-range/2, range, -range/2, range);
// argmin will find the one in search_window which get minimal sad.
Tuple me = argmin(sad(search_win.x, search_win.y, bid_x, bid_y));
// output the motion estimation result to MV table
Var ch;
out_mv(bid_x, bid_y, ch) = me[ch];
比較不直覺的地方在於兩個 RDom 套用的層次不同, 第一個 RDom 用來計算 matching criteria, 二個 RDom 是用來作為 motion estimation 尋找 mv, 總之這是以 search window 的方式作窮舉的 block matching 定義, 然而可以延伸與改進用在一開始所說的各種 matching 演算法上


2020年7月5日 星期日

撰寫各 x86 CPU 平台適用的 compiler vector extension code

在套用 gcc / clang 的 -ftree-vectorize or compiler vector extension 時來產生適當的 SIMD optimization 時,  同時會產生一個問題 - 當編譯時套用的 architecture 參數(e.g.: -mavx2) 所使用的指令集與運作時的 CPU 不一致時可能會出現 SIGILL (illegal instruction) 的問題. 過往可能會採用 CPU feature detection + dispatching code 的撰寫, 大致上邏輯如下:
// AVX-512 version
void __foo_avx512(void){  ... }
// AVX version
void __foo_avx2(void){ ... }
// SSE4 version
void __foo_sse4(void){  ... }
// C version
void __foo(void){  ... }

void foo(void)
{
    if(_avx512_available())
        __foo_avx512();
    else if(_avx2_available())
        __foo_avx2();
    else if(_sse4_available())
        __foo_sse4();
    else
        __foo();
}
這是過去長久以來很平常的作法, 而最近研究在接觸 Clear Linux, 研究了一下 Intel 採用的方式, 發現了有趣的事(這些 Intel 都有在 2016 年的簡報 - Improving Linux Performance with GCC latest technologies 中說明), 主要是 gcc 4.8 中已新增了 Function Multi-Versioning (FMV)的功能, 能透過函數屬性更方便地做到原本的目的, 以上述的例子來說就會變成:
// AVX-512 version
__attribute__ ((target ("avx512f")))
void foo(void){  ... }
// AVX2 version
__attribute__ ((target ("avx2")))
void foo(void){ ... }
// SSE4 version
__attribute__ ((target ("sse4.2")))
void foo(void){  ... }
// C version
__attribute__ ((target ("default")))
void foo(void){  ... }
使用 FMV 功能的好處首先所有的 function 都是相同的名稱, 僅是屬性中的 target 不同, 而GCC 會自動產生偵測 CPU 功能並從中選擇對應 foo 的 code, 這點可以省去維護上面 dispatching code 的問題(若有多個函數,  增刪都會是很冗長的工作). 使用 __attribute__ ((target ("TARGET_NAME"))) 的語法方式目前已被 gcc / icc / clang 所採用.

然而並不是所有人都有心力以 SIMD ISA 去實作版本, 這時複製多份相同的 C code 來做不同 SIMD ISA 的 auto-vectorization 似乎不是很實際的方式, 因此 gcc 進一步提供了有趣的屬性功能 - target_clones:
// C version
__attribute__ ((target_clones ("avx512f", "avx2", "sse4.2", "default")))
void foo(void){  ... }
而 GCC FMV 功能亦能夠與 compiler vector extension 合併使用, 因此以 compiler vector 實作後函數可以使用上述 target_clones 的方式, 如此 compiler 會自動針對不同的 SIMD ISA 生出多版本的, 以個人常用來教學的 tiled 8x8 gemm 來說就會像是:
#define TSIZE 8
#if defined (__clang__)
typedef float vfloat __attribute__((ext_vector_type(TSIZE)));
#else
typedef float vfloat __attribute__ ((vector_size (TSIZE*4)));
#endif

__attribute__ ((target_clones ("avx512f", "avx2", "avx", "sse4.2", "default")))
void gemm_vec(float *a, int sa, float *b, int sb, float *c, int sc)
{
    vfloat vb[TSIZE];
        for(int y = 0; y < TSIZE; y++){
            vb[y] = *((vfloat*)(b + sb*y));
        }
        for(int y = 0; y < TSIZE; y++){
            vfloat vc = *((vfloat*)(c + sc*y));
            vfloat va = *((vfloat*)(a + sa*y));
                for(int x = 0; x < TSIZE; x++){
            vc += va[x] * vb[x];
                }
                *((vfloat*)(c + sc*y)) = vc;
        }
}
接著以 gcc 編譯:
$ gcc -O3 -shared mm.c -o libgemm.so
透過 objdump 觀察
$ objdump -t libgemm.so
可以看到中間有如下的輸出:
0000000000001720 l     F .text    0000000000000c96 gemm_vec.default.4
00000000000023c0 l     F .text    00000000000005df gemm_vec.avx512f.0
00000000000029a0 l     F .text    00000000000005df gemm_vec.avx2.1
0000000000002f80 l     F .text    000000000000060c gemm_vec.avx.2
0000000000003590 l     F .text    0000000000000c95 gemm_vec.sse4_2.3

此外值得一提的是在 2019 年初 Clear Linux 計劃中 Intel 釋出了一個 FMV patch generator, 透過 make-fmv-patch 這個工具能針對  C/C++ code 自動產生對應的 FMV 屬性 patch, 套用後重新編譯即可產生一體適用的 SIMD optimized libraries / executables


2020年6月23日 星期二

Halide 與處理器架構上匹配性問題心得

老生常談, 老狗學不會新把戲 - 今晚來談談 Halide 與架構的匹配性

這幾年間 Halide 是個人常用的工具之一
上週與朋友一同用餐, 言談間討論到了 Halide
其中多少談論到一些處理器比較的部份
這文章分享的並非什麼新東西或是驚人的發現
基本上於不同地方的私下討論個人都早已分享過
在此僅是反覆說明自己對一件事情的看法 - 為何 Halide 套用在不同計算架構平台上效益落差很大

這個問題在不同場合, 不同的對象一同討論過(甚至當時在 MTK 工作時分析架構於內部都提及過)
個人的觀點是 - 不同平台上 locality 這部份的影響 (另外的 redundant work / parallelism 多數架構上這都不構成問題)
一言以蔽之 - memory model / architecture
必須說 locality 是 Halide 對於計算考量核心精神中 scheduling 三角的一角, 對於 Halide 而言而能凸顯這面向的還是以 transparent cache 為主, 任何需要透過搬動 (無論是額外的 SIMD, 俱備 DMA 與否) 來 reuse data 的 scratchpad / TCM ... 等等, 非 cache based 的方案都面臨幾個問題:
  • reuse data 的額外開銷, 而且這部份隨著要使用的資料愈多 cost 也愈高 (house keeping code, 2D/3D DMA usage ... etc)
  • cascade or fused kernel 每增加一級, data layout 的安排複雜度(無論對 compiler or coder 而言)呈指數增加
  • 再者一旦決定了 scheduling, 基本上也不俱備 scalability. 增加 size 無法改善效率, 但是減少 size 會讓原有的 code 無法運行. (cache-based 架構則是會有隨著 size 不同而呈現的效能曲線)
這幾個因素讓 Halide 在一些架構上, 在這個 Halide 最重要概念的三角形中的 Y 軸 (也就是 locality) 僅僅存在著數量非常有限的選擇, 最終這個 scheduling space 的三角形因為 control & complexity 而退化為小面積, 線或更甚者為數點. 而最終還是會落入需要經驗導向的 flow 調整, 別出心裁的發想, 特殊指令的套用 .. 等等
 

 

2020年5月10日 星期日

clang & gcc vector extension 探究心得

這文章換整了把一個以前納悶許久的功能做了研究與釐清後的心得 - vector selection in GCC / Clang
在 GCC / Clang 的 vector extension 中 "?:" operation 的使用, 基本上搜尋 "gcc vector extension" or "clang vector extension" 你一定會看到文件對於 ?: 這操作寫著是支援的 甚至 clang 的文件 這麼列著.

 
 
然而按照常用的 CL 使用方式去寫 code 會發現沒有辦法編譯過.
對於 OpenCL 中的使用, 請參考 Programming with OpenCL C 一書的 Vector Operators - Conditional Operator

對於 gcc/clang 支援的 ?: 是指 a ? b : c 這樣的計算中
的 a 必須是 scalar type, b, c 可以是 scalar or vector ... 因此如此是無法達到最好用的 vector element selection 功能, 也就是當我們撰寫 c = a > b ? a : b; 時於 vector 上等同於:
u32x8 c = {
    a[0] > b[0] ? a[0] : b[0],
    a[1] > b[1] ? a[1] : b[1],
    a[2] > b[2] ? a[2] : b[2],
    a[3] > b[3] ? a[3] : b[3],
    a[4] > b[4] ? a[4] : b[4],
    a[5] > b[5] ? a[5] : b[5],
    a[6] > b[6] ? a[6] : b[6],
    a[7] > b[7] ? a[7] : b[7],
};
以這個例子來說當宣告:
u32x8 a, b, c;
...
c = a > b ? a : b;
對於 x86 AVX ISA 而言你會希望 compiler 能使用下列 intrinsics / instruction:
_mm256_max_epu32 / vpmaxud
但是很不幸的是在 gcc / clang 這樣的 code 無法被編譯, 在不考慮直接使用 AVX intrincs 的情況下, 搜尋後會得到兩個可能的建議

1. 使用 loop 讓 compiler 優化, 也就是:**

for(int i = 0; i < 8; i++)
    c[i] = a[i] > b[i] ? a[i] : b[i];
如此你會得到如下圖的使用 loop 結果, 雖然有使用 AVX register, 但是計算上完全沒有好處
 

 

2. 這個結果比較需要技巧才會找到, 也就是 bitwise operation:**

u32x8 d = (u32x8)(a > b);
u32x8 c = (d & a) | (~d & b);
如此你會得到圖三的結果, 以 code generation 的結果這可能是相對較好的方式
 

然而這完全比不上直接使用 avx intrinsics 的下圖的結果:
u32x8 c = _mm256_max_epu32(a, b);

 

g++/clang++

然而還是有最後希望 - GCC/g++, 事實上 gcc 文件中如下圖有提到 C++ 模式支援 element-wise


由於個人印象中並沒有這樣的方式, 查了一下 是 GCC 5 / Clang 10 開始支援這樣的模式 (只能說在最需要使用的時候 android gcc, Google 只用到了 4.9 ... 之後的版本 vector extension 沒特別去用, 加上逐漸重心轉到使用 clang), 如此可以支援一開始期望的語法, 而且 code generation是很漂亮的(g++:下圖1/clang++:下圖2, g++ 的比較好), 非 clang 不可時透過 g++/clang++ 編譯產生 .s 再整入 clang project 是唯一可以考慮的作法 (否則就是 code 可讀性與移植性變差 + 努力查 intrinsics table 了)
1. 
 

2.

 
除此之後後續做了些實驗額外得知了一些限制/資訊

gcc & clang compatibility

在先前 gcc / clang 各自有著自己的 vector extension 與宣告方式, 個人也針對兩者做了不同宣告, 然而有趣的地方實驗過程中, 曾經忘了切換, 然而可以順利編譯過, 這也表示 gcc / clang 互相吃了對方的 vector extension 的宣告方式, 因此個人一開始以為在各自 compiler 上兩個 vector extension 應該是互通的. 這點在昨日得知 clang 在實作時會考量儘量與 gcc 一致, 猜想 gcc 在一些特性上也對 clang 做了類似的考量.

OpenCL-like vector swizzle

這是被詢問的問題, 因此注意到了差異, 基本上只有 clang 提供了接近 OpenCL 的 vector swizzle 方式:
u32x4 a, b, c;
...
c.s02 = a.s02; // or c.xz = a.xz;
c.s13 = b.s02; // or c.yw = b.xz;
當然還可以針對特定 vector lane 作 value assign.
比較麻煩的是 initial value, clang 並沒有提供如同 OpenCL 的彈性, 而且必須是以 { ... } 的方式一一擺放, 而不如 OpenCL 以 ( ... ) 且能夾雜大小不同的 vector 作為數個數值. 在 clang 上要使用這個特性, 必須使用 clang 專屬的 vector extension 宣告方式.

vector as array

最早是 clang 開始提供的, 而且不只是 vector extension, 對於 SIMD intrinsics 的 data type clang 都支援. 這個功能相當實用, 在 android 7 時期導入 clang 可以說幫了大忙, 這點對於 C Model 與 SIMD 版本實作的轉換與驗證簡化不少.
使用上類似:
u32x4 a, b, c, vout;
...
vout = fa*a + fb*b + c;
for(int i = 0; i < 4; i++){
    assert(out[ofs+i] == vout[i]);
}
後續 gcc 也在 vector 支援了這個功能, 然而必須使用 gcc 自己的 vector extension 宣告方式

2020年3月8日 星期日

Sampling v. tracing 閱讀心得

對 2016 的 "Sampling v. tracing" 演講心得紀錄文有點相見恨晚, 昨日至今零碎時間內快速地研讀了兩次, 因此附上一下粗略心得分享:
系統效能分析是困難的, 近日因為效能分析方向自己想做的事情, 加上對目前在各大討論網站提倡的主流方法有一些疑惑沒解開, 因此做了一些 Google 搜尋而找到此篇
當然現在主流是 perf / eBPF 這類 sampling-based 分析工具, 而這些也是各大討論區與網站主要提倡的工具, 然而真正地面對效能分析時, 很多時候必須弄清楚要發覺與處理的的問題本質為何?
這篇首先指出一個關鍵:
"Sampling profilers, the most common performance debugging tool, are notoriously bad at debugging problems caused by tail latency because they aggregate events into averages. But tail latency is, by definition, not average."
再者在許多問題的分析上, 從搜尋結果有相當高的可能會得到使用 sampling profiler 的結果, 然而因為對要處理的問題了解不足的結果, 直接套用建議後會發現: "But tools like OProfile are useless since they'll only tell us what's going on when our RPC is actively executing. What we really care about is what our thread is blocked on and why."
在文中舉了一個 Google 內實際發生的例子 - disk read latency 分析, 看似正常的分佈圖然而有著不合理的高延遲存在, (BTW 個人特別喜歡這段的一句 "each of you think of a guess, and you'll find you're all wrong"), 進而發現長時間廣泛存在於系統層面的問題. 而修正的獲益足以支付分析者十年以上的人事成本. 而這分析的例子主要在於提出 - "is this because of some flaw in existing profilers, or can profilers provide enough information that you don't need to use tracing tools to track down rare, long-tail, performance bugs?"
在討論 sampling limitation 之後, 文末段最後有3個問題, 其中最引人注目應該是:
3. Why are sampling profilers dominant?
除了說明認為未來 profiler 會愈來愈像 tracing tool 外, 也直接的回答了 - "but if we look at the state of things today, the easiest options are all classical profilers.", perf 提供運作的時間, 其他的基本工具提供消耗了多少記憶體, 結由這兩個數值能處理主要的效能問題.
此外也指出將現有公開工具湊在一起追蹤效能問題將會是個很困難的體驗, 文中提了一個實際的 case 做例證.
對於 tracing 的問題在於建立的困難度 - 對此通常有兩個選擇
1. 自己實作 - 基本上當然是 event + timestamp, 然而這當中還有該如何針對你的問題建立真的需要存下的 trace.(像是 lock & waiting) 來減少 overhead.
2. 從既有工具挑選你所需要的 - 然而不幸的是這些 overhead 成本都很高, 因此無法在特定規模以上在背景運作來復現出現的問題.
個人觀點:
1. sampling profiler 主要還是建立在 "比較" 的基準上找尋問題點, 通常是版本變換造成行為不同的系統問題. 以此能以成本較低的方式找出, 但若認為一個 workload 是 "正常" 基本上要找出優化的是不容易的, 這類問題像是 task dispatch 與 thread synchronization.
2. profiling budget 觀念的建立 - 為了能在實際發生問題的當下作用而非事後的分析(因為很多可能是實驗環境而無法 reproducible), 對於產生需要的資訊建立 overhead 要求. 在此之下建立能達成目的的 framework.
3. 統計數據的解讀能力 - 對整體與極端數值的解讀能力是分析問的的根本. 文中對於 disk latency 的 histogram 解釋能力即是一例.
4. sampling 是個好工具, 但必須了解其極限, 以及發現可能問題開如何找下一步

2018年12月8日 星期六

Spatial Tutorial - General Matrix Multiply (GeMM)

在本節中, 將學習關於下列的 Spatial 元件:
  • MemReduce 與 MemFold
  • Multi-dimensional Banking
請注意有許多 Spatial 應用能夠在此找到.

Overview

General Matrix Multiply (GEMM) 是個在線性代數, 機械學習, 統計與其他領域中常見的運算. 它提供了一個相較先前教學更為有趣的權衡空間, 因為有著許多分割計算的方式. 這些包含著使用 blocking(分割為區塊), inner products(內積), outer products(外積) 以及 systolic array 等技巧. 在此教學中, 將展示如何建立使用外積分塊的 GEMM 應用, 並且留給使用者自行嘗試建立使用內積的 GEMM 版本. 後續的教學將會展示如何在其他應用中使用 shift registers 以及 systolic array, 但相同的技巧也能一併追加應用在 GEMM.
下圖的動畫顯示基本使用外積的方式計算 C = A x B

Basic implementation

import spatial.dsl._

@spatial object MatMult_outer extends SpatialApp {
  type X = FixPt[TRUE,_16,_16]

  def main(args: Array[String]): Unit = {
    // Get sizes for matrices from command line
    val m = args(0).to[Int]
    val n = args(1).to[Int]
    val p = args(2).to[Int]

    // Generate data for input matrices A and B, and initialize C
    val a = (0::m, 0::p){(i,j) => ((i + j * p) % 8).to[X] }
    val b = (0::p, 0::n){(i,j) => ((i + j * n) % 8).to[X] }
    val c_init = (0::m, 0::n){(_,_) => 0.to[X] }

    // Communicate dimensions to FPGA with ArgIns
    val M = ArgIn[Int]
    val N = ArgIn[Int]
    val P = ArgIn[Int]
    setArg(M,m)
    setArg(N,n)
    setArg(P,p)

    // Create pointers to matrices
    val A = DRAM[X](M, P)
    val B = DRAM[X](P, N)
    val C = DRAM[X](M, N)

    // Set up parallelizations
    val op = 1
    val mp = 1
    val ip = 1

    val bm = 16
    val bn = 64
    val bp = 64

    setMem(A, a)
    setMem(B, b)
    setMem(C, c_init)

    Accel {
      // Tile by output regions in C
      Foreach(M by bm par op, N by bn par op) { (i,j) =>
        val tileC = SRAM[X](bm, bn)
                                                   
        // Prefetch C tile
        tileC load C(i::i+bm, j::j+bn par ip)
                                                   
        // Accumulate on top of C tile over all tiles in P dimension
        MemFold(tileC)(P by bp) { k =>
          val tileA = SRAM[X](bm, bp)
          val tileB = SRAM[X](bp, bn)
          val accum = SRAM[X](bm, bn)
                                 
          // Load A and B tiles
          Parallel {
            tileA load A(i::i+bm, k::k+bp)
            tileB load B(k::k+bp, j::j+bn)
          }
          
          // Perform matrix multiply on tile
          MemReduce(accum)(bp by 1 par mp){ kk =>
            val tileC_partial = SRAM[X](bm,bn)
            Foreach(bm by 1, bn by 1 par ip){ (ii,jj) =>
              tileC_partial(ii,jj) = tileA(ii,kk) * tileB(kk,jj)
            }
            tileC_partial
          }{_+_}
        }{_+_}
                                                   
        // Store C tile to DRAM
        C(i::i+bm, j::j+bn par ip) store tileC
      }
    }
    
    // Fetch result on host
    val result = getMatrix(C)

    // Compute correct answer
    val gold = (0::m, 0::n){(i,j) => 
      Array.tabulate(p){k => a(i,k) * b(k,j)}.reduce{_+_}
    }

    // Show results
    println(r"expected cksum: ${gold.map(a => a).reduce{_+_}}")
    println(r"result cksum: ${result.map(a => a).reduce{_+_}}")
    printMatrix(gold, "Gold: ")
    printMatrix(result, "Result: ")

    assert(gold == result)
  }

}
上面的程式展示了如何使用內積與 tiling 來計算矩陣乘法. 在此將帶過在此程式中導入的新結構. 下圖動畫展示了在此範例中所使用的 tiling 設計.
為了產生 a, b, 與 c_init 等在 Host 端的資料結構, 使用了用以產生一個 Matrix的語法. 精確地說是 (0::m, 0::p){(i,j) => … }. 這能用以產生高達 5D 的資料結構. 為了產生 c_init, 以底線來標示 iterators, 這是由於他們並沒有明確地被使用在此函數, 這在 Scala 中這是相當普遍的作法.
接著, 設定所有的平行因子為 1. 在下節中, 將進一步討論與解釋這些對於所產生硬體的影響.
在此版本的矩陣乘法中, 選擇在最外部的 loop 中透過水平掃過記憶體的方式, 一一計算不同 C 的不同區塊. 也有著其他的次序產生有效的矩陣乘法, 但此這是最小 DRAM 傳遞的方式之一.
由於這是一個一般的矩陣乘法, 並沒有保證實際上矩陣 C 有被初始化為0. 因此正確的作法是在既有存在於 C 中的數值之上做疊加. 在此應用的最外層 loop 為一個 3-stage coarse-grain 的 pipeline. 而 prefetch 行為第一個 stage.
由於並不想第一個 iteration (k = 0) 覆蓋掉 prefetch 至 tileC 中的數值, 因此這裡選擇使用 MemFold controller. 並且建立了一個 local accum sram, 用來存放兩個來自AB 的 tile 乘法的部份結果. 整個 MemFold 組成了最外部 pipeline 的第二個 stage.
這裡以使用外積的方式來計算兩個 tile 的矩陣乘法. 這裡最外層的 loop 逐步穿過內部, 直到完成這局部的矩陣. 換句話說 kktileA 選擇了一個欄位以及自 tileB 的一列. 對於 MemReduceMemFold controllers 必須針對其 map 函數傳回一個 SRAM, 所以為此目的建構了 tileC_partial .
此演算中內層的 loop 逐一走過 tileA 的所有列以及 tileB 的所有欄. 這有效進行在由 kk 選出的兩向量中的外積運算(窮舉的成對乘法).
MemReduce 回傳此 SRAM, 所以此特定 MemReduce 為外部 MemFold map 函數的輸出. 這也是為何有著兩個顯得奇特多餘的 {_+_} lambdas.
最後的步驟, 並且是最外層 pipeline 的第三個 stage 為將 tileC 傳輸回 DRAM 的存放動作. 下圖動畫示範如何以 triple-buffered 記憶體的方式思考 tileC.
這裡使用了 getMatrixprintMatrix 函式來自 DRAM 取得資料並顯示這些資料結構的內容. 同樣地, 能夠使用 getTensor3, getTensor4, 與 getTensor5, 作為搭配 print 印出高維度的資料結構.

Multi-dimensional Banking

import spatial.dsl._

@spatial object MatMult_outer extends SpatialApp {
  type X = FixPt[TRUE,_16,_16]

  def main(args: Array[String]): Unit = {
    val m = args(0).to[Int]
    val n = args(1).to[Int]
    val p = args(2).to[Int]

    val a = (0::m, 0::p){(i,j) => ((i + j * p) % 8).to[X] }
    val b = (0::p, 0::n){(i,j) => ((i + j * n) % 8).to[X] }
    val c_init = (0::m, 0::n){(_,_) => 0.to[X] }

    val M = ArgIn[Int]
    val N = ArgIn[Int]
    val P = ArgIn[Int]
    setArg(M,m)
    setArg(N,n)
    setArg(P,p)

    val A = DRAM[X](M, P)
    val B = DRAM[X](P, N)
    val C = DRAM[X](M, N)

    // *** Set mp and ip > 1
    val op = 1
    val mp = 2
    val ip = 4

    val bm = 16
    val bn = 64
    val bp = 64

    setMem(A, a)
    setMem(B, b)
    setMem(C, c_init)

    Accel {
      Foreach(M by bm par op, N by bn par op) { (i,j) =>
        val tileC = SRAM[X](bm, bn)
                                                   
        tileC load C(i::i+bm, j::j+bn par ip)
                                                   

        MemFold(tileC)(P by bp) { k =>
          val tileA = SRAM[X](bm, bp)
          val tileB = SRAM[X](bp, bn)
          val accum = SRAM[X](bm, bn)
                                 
          Parallel {
            tileA load A(i::i+bm, k::k+bp)
            // *** Parallelize writer by 8
            tileB load B(k::k+bp, j::j+bn par 8)
          }
          
          MemReduce(accum)(bp by 1 par mp){ kk =>
            val tileC_partial = SRAM[X](bm,bn)
            Foreach(bm by 1, bn by 1 par ip){ (ii,jj) =>
              tileC_partial(ii,jj) = tileA(ii,kk) * tileB(kk,jj)
            }
            tileC_partial
          }{_+_}
        }{_+_}
                                                   
        C(i::i+bm, j::j+bn par ip) store tileC
      }
    }
    
    val result = getMatrix(C)

    val gold = (0::m, 0::n){(i,j) => 
      Array.tabulate(p){k => a(i,k) * b(k,j)}.reduce{_+_}
    }

    println(r"expected cksum: ${gold.map(a => a).reduce{_+_}}")
    println(r"result cksum: ${result.map(a => a).reduce{_+_}}")
    printMatrix(gold, "Gold: ")
    printMatrix(result, "Result: ")

    assert(gold == result)
  }

}
這裡展示 Banking 如何在 Spatial 中作用, 以使用 tileB 作為示範記憶體. Banking 的計算方式基於 “Theory and algorithm for generalized memory partitioning in high-level synthesis” (Wang et al), 並有著次要的修改以及在 pattern 搜尋上的改進.
基本上, 對記憶體切分 bank 是藉由察看所有的存取動作並找出一個沒有衝突的 banking 方案. 更多細節請參考 關於 Spatial 論文的 Appendix A. 將 N 維度記憶體以 1D 空間方式作 bank 切分, 僅在失敗與資源利用上不必要的浪費才以多層的 bank 方式切分.
這裡設置 mp = 2ip = 4. 若檢查上面使用到此平行的應用, 將發現 mp 平行化外部的 pipe 而 ip 平行化內部的 pipe. 下圖動畫顯示一個對於 tileB 在此平行化(僅考慮唯讀)下可能的 banking 方案. 請注意這並非唯一可行的方案, 其他分層與平面的方案是存在的.
這裡選擇以 8 併行的方式平行化對 tileB 的 writer. 這表示上面的 banking 方案不再可行. 需要靜態對 SRAM 作 bank 切分來保證在記憶體存取中沒有任何 bank 衝突. 在這樣的情況中, compiler 可能會選擇如同下圖動畫顯示的平坦的 banking 方案. 請注意這動畫僅顯示以 mp = 2ip = 1 所設置的 reader, 而其他不同 reader 能夠嘗試調整這些參數, 藉此來看對於記憶體會發生什麼改變.

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

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