# 平行處理
有分為兩種層面:
- 工作層面 Task-level (process-level) parallelism
- 讓單一個工作(independent jobs)的 throughput 增加
- 例如工作的某個程序由某個核心來做
- 程序層面 Parallel processing program
- 單一個程序(program)用多個核心執行
## Amdahl’s Law
一樣,想要平行化?要先看看平行化帶來的效率提升夠不夠。
舉例來說,我們動用了 100 個核心平行化處理,來看看新的所耗時間跟舊的相比差多少:
$$
T_{\text{new}}=\frac{T_{\text{parallelizable}}}{100}+T_{\text{sequential}}
$$
或者我們換個方法表示,$T_{\text{parallelizable}}$ 跟 $T_{\text{sequential}}$ 加起來等於 1,$T_{\text{parallelizable}}$ 跟 $T_{\text{sequential}}$ 的比例分別為 $F_{\text{parallelizable}}$ 跟 $F_{\text{sequential}}$,那麼,改成用比率表示的話為:
$$
\frac{1}{\frac{F_{\text{parallelizable}}}{100}+F_{\text{sequential}}}=\frac{1}{\frac{F_{\text{parallelizable}}}{100}+(1-F_{\text{parallelizable}})}
$$
我們用了 100 顆核心,想要有 90 倍的提升應該不為過吧?,那麼我們來算算看 $F_{\text{parallelizable}}$ 要佔多少的比例:
$$
\frac{1}{\frac{F_{\text{parallelizable}}}{100}+(1-F_{\text{parallelizable}})}=90
$$
可以解得 $F_{\text{parallelizable}}=0.999$,代表我們要有非常大的部分屬於可平行化,才可以達到 90 倍的提升。
# Strong vs Weak Scaling
從上面的例子可以看到,不是狂堆核心,效能就會上去,而是跟工作的類型有關。
為此聰明的人們分出兩種類型:
- Strong Scaling
- 工作的大小固定,核心堆越高效能越好
- 這就是上面我們理想的工作
- Weak Scaling
- 工作量跟核心的需求量成正比
- 也就是說工作量要多,高核心才可以提高效能
舉個例子。
- 工作 A,他是將 10 個純量加起來,並且再做兩個 10X10 的矩陣元素相加。
- 工作 B,一樣是將 10 個純量加起來,但是再做兩個 100X100 的矩陣元素相加。
可以看到將 10 個純量加起來就是 Seq 的部分。然後我們分別用單核心跟多核心的情況模擬,並且計算 「Potential」:
- 工作 A
- 單核心:$(10+100)\times t_{\text{add}}=110\times t_{\text{add}}$
- 10 顆核心:$(10+\frac{100}{10})\times t_{\text{add}}=20\times t_{\text{add}}$
- 跟單核心相比:$\frac{110}{20}=5.5$ 倍的提升
- 但是理論上 10 顆核心應該要有 10 倍提升,所以 Potential 為 $\frac{5.5}{10}=55\%$
- 100 顆核心:$(10+\frac{100}{100})\times t_{\text{add}}=11\times t_{\text{add}}$
- 跟單核心相比:$\frac{110}{11}=10$ 倍的提升
- 但是理論上 100 顆核心應該要有 100 倍提升,所以 Potential 為 $\frac{10}{100}=10\%$
可以看出如果工作量固定的話,狂疊核心的 Potential 反而會變低。
- 工作 B
- 單核心:$(10+10000)\times t_{\text{add}}=10010\times t_{\text{add}}$
- 10 顆核心:$(10+\frac{10000}{10})\times t_{\text{add}}=1010\times t_{\text{add}}$
- 跟單核心相比:$\frac{10010}{1010}=9.9$ 倍的提升
- 但是理論上 10 顆核心應該要有 10 倍提升,所以 Potential 為 $\frac{9.9}{10}=99\%$
- 100 顆核心:$(10+\frac{10000}{100})\times t_{\text{add}}=110\times t_{\text{add}}$
- 跟單核心相比:$\frac{10010}{110}=91$ 倍的提升
- 但是理論上 100 顆核心應該要有 100 倍提升,所以 Potential 為 $\frac{91}{100}=91\%$
可以看到工作量如果變多了,100 顆核心的 Potential 會變高。
:::warning
所以我們其實可以列出一個方程式來計算這個問題:
- 單核心:$(10+x^2)\times t_{\text{add}}$
- 10 顆核心:$(10+\frac{x^2}{10})\times t_{\text{add}}$
- 跟單核心相比:$\frac{10+x^2}{10+\frac{x^2}{10}}=\frac{100+10x^2}{100+x^2}$ 倍的提升
- 但是理論上 10 顆核心應該要有 10 倍提升,所以 Potential 為 $\frac{\frac{100+10x^2}{100+x^2}}{10}=\frac{10+x^2}{100+x^2}$
- 100 顆核心:$(10+\frac{x^2}{100})\times t_{\text{add}}$
- 跟單核心相比:$\frac{10+x^2}{10+\frac{x^2}{100}}=\frac{1000+100x^2}{1000+x^2}$ 倍的提升
- 但是理論上 100 顆核心應該要有 100 倍提升,所以 Potential 為 $\frac{\frac{1000+100x^2}{1000+x^2}}{100}=\frac{10+x^2}{1000+x^2}$
所以我們可以知道,要到何種程度 100 顆才會比 10 顆好,就是:
$$
\displaylines{
\frac{10+x^2}{100+x^2}<\frac{10+x^2}{1000+x^2}\\
1000+x^2<100+x^2\\
}
$$
永遠不會比較好。
:::
Data Decomposition
Task Decomposition
Pipelining
---
# Shared Memory Multiprocessor / SMP
顧名思義,就是每個核心都共用一塊記憶體。
聰明的人們用 Memory access time 來劃分出兩個種類:
- UMA:uniform memory access
- 各個核心存取的延遲是一樣的,如那張圖,因為就只有一塊記憶體,所以大家延遲都一樣
- NUMA:nonuniform memory access
下面主要著重 UMA 的部分。
# UMA - Cache Coherency
因為每個核心都共用一塊記憶體,但是 Cache 是各自擁有的,例如下圖:
<iframe src="https://drive.google.com/file/d/1f5x-pOkedTKQLmWLwwk7iKc5RJDVqXFT/preview" height="480"></iframe>
所以必須要維持 Cache 內容的一致性。
我們的手段是透過一個叫 Snooping 的方式來處理。
# Snooping
講白話一點他就是 broadcast。上面的圖可以看到有個 Interconnection Network,這個東西又叫做 Bus,他是 broadcast 可以運作的主要結構。
大致維護的方法一個核心就是將「requests for data」的訊號給其他的核心,然後做一些操作。
在 Bus 上 Snooping 的手段,被廣泛用於小型的機器 small scale machines,也就是家用主機這樣的類別。
至於詳細的 Snooping,分為兩種:
- Write Invalidate Protocol
- Multiple readers, single writer
- 發出 request 的人叫其他人把他們的資料變為不合法
- 這個動作又叫 Write to shared data
- Read Miss 的部分,如果某個核心他沒有找到要讀的資料怎麼辦?
- Write-through:memory is always up-to-date,所以不用擔心
- Write-back:snoop in caches to find most recent copy,也就是去看其他人的 cache,把有最新 cache 的核心的資料複製過來
- 這個待會會細講,會透過資料的 validate 狀態來判斷
- Write Update Protocol
- 在 Bus 上廣播給大家,叫大家以及記憶體一起更新
- Read Miss?永遠不會發生,因為隨時大家資料都是最新的
## 優缺
Invalidate 的每次寫入操作,只需要一次的 bus 傳輸訊號,因為第一次的廣播就會讓其他人的 cache 資料都變 invalidate,自己的保持 validate,所以如果第二次寫入同個 block 並保持是 validate,就不用再次做廣播,因為其他人已經是確保是 invalidate。
除非有其他人也做寫入,就會換你的資料變成 invalidate。
Update 則可以讓 read 跟 write 之間有較低的延遲,因為每次都是一次性叫全部人一起更改,資料隨時都是最新的,所以發生 read miss 時就不用花時間去查誰的 cahce 是最新的。
下面是個流程的範例:
<iframe src="https://drive.google.com/file/d/1OgkVTkVzU-hL6MObEhbOKZ4qLsv_ffN9/preview" height="580"></iframe>
可以看到 Invalidate 中的 B,在 A 發出寫入訊號後,他的 cache 資料就變成了 invalidate,所以就不見了,並且這裡的例子是 write back,所以記憶體中的資料並沒有一起更新,這也是為何其他核心如果要拿資料的話,必須要從有最新資料的核心拿的原因。
在 Update 中則是 A 發出寫入訊號後,大家都一起更新了。
:::warning
所以如果某個處理器的規模較小, bus 能力有限,就會採用 Invalidate 這種對 bus 負擔較小的 Protocol
:::
# Invalidate Snoopy with write-back cache
下面以「狀態圖」的方式完整介紹 Invalidate Snoopy 的內容,搭配 write-back cache。
在那之前,要先說明:
- 對於每個 cache block(也就是一個 set)來說,他只會在下面的三種狀態其中一種:
- Shared:分享狀態,這個 block 是可以被其所擁有的 CPU 讀取的
- Exclusive:獨特狀態,這個 block 擁有最新的資料,其他人的都是舊的
- 此時這份資料除了可以被其所擁有的 CPU 讀取,也可以被寫入
- 但是因為是 write back 所以要設置 dirty bit。
- Invalid:cache 沒有 block 的資料,或是不是最新的,所以不能用了
下面介紹兩種角度的狀態圖。
## Request 發起人
我們一個一個的來看各個狀態:
<iframe src="https://drive.google.com/file/d/1vTRyV-JBmQKomy-QnocWJhKqNOgHQY3F/preview" height="580"></iframe>
- 當前 block 狀態是 **Invalid**:
- 如果此時發起 CPU **read**,會向 Bus 吐出「**Read Miss**」
- 代表這個 block 之後會被放入從其他人來的「**最新的資料**」
- 也因此進入了 **Shared state**,代表其他人如果想拿最新資料...
- **不可以跟他要,只能從記憶體拿**
- Shared 的意思只代表該資料目前是處於「分享的狀態」,所以記憶體的資料是合法的
- 如果此時發起 CPU **write**,會向 Bus 吐出「**Write Miss**」
- 代表這個 block 之後會被放入從其他人來的「**最新的資料**」
- 並且會對他進行寫入,成為比**目前為止最新的資料**
- 也因此進入了 **Eclusive state**,代表目前的 block 擁有最尖端的資料
- 當前 block 狀態是 **Shared**:
- 如果此時發起 CPU read,不過好家在,block 在 cache 內,所以就一樣指回自己
- 如果此時發起 CPU **read** 但是**沒有資料**,會向 Bus 吐出「**Read Miss**」
- 代表這個 block 之後會被放入從其他人來的「最新的資料」
- 所以跟剛剛 Invalid state 一樣,都是進到 Shared state
- 如果此時發起 CPU write
- **無論資料有沒有在 cache 內,都會向 Bus 吐出「Write Miss」**
- 代表目前的核心即將會擁有這個 block 最尖端的資料,也因此進入了 **Exclusive state**
- 同時也在告知其他核心,**你們的資料都過期啦**
- 當前 block 狀態是 **Exclusive state**:
- 如果此時發起 CPU read 或 write,但是好家在,block 在 cache 內
- 所以就一樣指回自己,資料一樣是維持最新穎的資料
- 如果是 read,但是沒有資料,會向 Bus 吐出「**Read Miss**」
- 並且還會將自己最頂尖的資料先寫回記憶體
- 然後就會進到 **Shared state**,代表該資料目前是處於「分享的狀態」
- 要拿資料就從記憶體去拿
- 如果是 write,但是沒有資料,會向 Bus 吐出「**Write Miss**」
- 跟 read miss 一樣,會先將自己最尖銳的資料寫回記憶體
- 然後因為是寫入的操作,所以之後擁有的 block 一樣還是最尖端的科技
- 因此停留在 **Eclusive state**
:::warning
- 沒有資料的意思是,該 block 的資料不是要 read 或 write 的資料,是一個 miss
- 簡單來說,發起 write 的時候會進到 **Exclusive**;發起 read 的時候,進到 **Shared**
- 關於「Shared state 的意思」跟「從記憶體拿」,請看下面。
:::
## Request 接收者
我們一個一個的來看各個狀態:
<iframe src="https://drive.google.com/file/d/1HKdF_gzxBPito4PWh3k6biKGmB-6kr8o/preview" height="580"></iframe>
- 當前 block 狀態是 **Invalid**:
- 可以看到並沒有任何指向外面的箭頭,關鍵在於這是「**以某個核心的角度來說**」
- 因為以某個核心來講,他雖然收到了 write 或 read miss,但是它本身該 block 的資料就是 invalid,也就不能給別人
- 所以對於最初始的狀態,如果某個 miss 的發起人沒有收到其他人的回覆,就得要從記憶體拿資料
- 當前 block 狀態是 **Shared**:
- 如果收到的請求是 write miss,並且本身的該 block 的狀態是 Shared
- 代表**該 block 的資料就要失效了**,所以會進入 Invalid state
- 這就是上面提到的,request 發起人會叫其他人把資料設成 Invalid
- 那為甚麼收到的請求是 read miss,並且本身的該 block 的狀態是 Shared,卻沒有箭頭呢?
- 其實可以看到在 Exclusive state 的時候,如果收到 read miss,會**將資料寫回記憶體**
- 也就是代表,當前最尖端的資料已經放在記憶體了,所以資料是 **Shared** 的狀態
- 也因此 Shared state 的核心如果又收到 read miss,並不會做任何動作
- 告訴你要拿請跟記憶體拿,**記憶體有最新的資料**
- 當前 block 狀態是 Eclusive state:
- 如果收到的是 read miss
- 會將最新穎的資料先寫回記憶體,然後進入 Shared state
- 代表當前該 block 的資料是最新的,最純的
- 並因此可以看到有個 abort memory access 的字樣,代表不需從記憶體讀取
- 相對的,Shared state 沒有寫的就是要從記憶體讀取
- 如果收到的是 write miss
- 一樣會將最新穎的資料先寫回記憶體
- 但是發起 write 的核心待會就會有最新的資料了,所以自己的資料即將過期
- 因此進入了 Invalid state
- 也一樣可以看到 abort memory access
## 下面是模擬
<iframe src="https://drive.google.com/file/d/17bvbRjVpe8zvHyKeasB5f8bV1cjP5nCy/preview" height="300"></iframe>)
---
# 四個 C
從上次的三個 Miss 多加了這次的第四個 Miss:Coherency Misses,代表是跟維持一致性有關而造成的 miss。
當兩個 CPU 要存取相同的 block 時,Coherency Misses 有分成兩種:
- True sharing misses
- 是真的因為該 block 的資料要維持一致性而造成的 miss
- 像上面提到的,如果某個 block 處於 Shared state,那麼要發起寫入時一定會送出 write miss
- False sharing misses
- 但是我們知道一個 block 內可能會放有很多資料,但是因為他們都在同個 block 內,所以可以會發生 A 資料的更新導致了 B 資料的 invalidate,例如下圖
<iframe src="https://drive.google.com/file/d/1PBAddUCgcCR7_34rDsH8KTzPK5UaRxti/preview" height="480"></iframe>
這圖的假設是一開始 P1 跟 P2 兩個核心內的某一個 block 內,有著 x1 跟 x2 兩個資料,並且當前是 Shared state:
- 第一時刻,P1 想要 write x1,根據上面第一個狀態圖,可知 P1 會向 Bus 發送「write miss」
- 這個 miss 是因為想要 write x1 而引起的,所以是 **True (sharing) miss**
- 但是此時 P1 該 block 就會進入 Exclusive,P2 的就會進入 Invalid
- **而倒楣的 x2 因為跟 x1 同個 block,所以害他也跟著變 Invalid**
- 第二時刻,P2 想要 read x2,因為 P2 該 block 目前是 invalid,可知 P2 會向 Bus 發送「read miss」
- **這個 miss 是因為剛剛的 write x1 所導致的 invalid,所以是 False miss**
- 而此根據上面的狀態圖,P1 跟 P2 的該 block 都會進入 shared state
- **倒楣的 x1 也就因為這樣進到了 shared state**
- 第三時刻,P1 想要 write x1,因為 P1 該 block 目前是 shared,可知 P1 會向 Bus 發送「write miss」
- **這個 miss 是因為剛剛 read x2 而引起的,所以是 False (sharing) miss**
- 但是此時 P1 該 block 就會進入 Exclusive,P2 的就會進入 Invalid
- 而倒楣的 x2 因為跟 x1 同個 block,所以害他也跟著變 Invalid
- 這件事跟剛剛第一時刻一模一樣,歷史總是驚人的相似
- 第四時刻,P2 想要 write x2,因為 P2 該 block 目前是 invalid,可知 P2 會向 Bus 發送「write miss」
- **這個 miss 是因為剛剛的 write x1 所導致的 invalid,所以是 False miss**
- 而此根據上面的狀態圖,P1 的該 block 會進入 invalid state
- 而 P2 因為是寫入操作,所以有最新的資料,目前在 Exclusive state
- **倒楣的 x1 也就因為這樣進到了 invalid state**
- 第五時刻,P1 想要 read x2,因為 P1 該 block 目前是 invalid,可知 P1 會向 Bus 發送「read miss」
- 這時有趣了,**這個 miss 是因為剛剛 write x2 而引起的**,加上現在要 read x2
- **所以現在是屬於True (sharing) miss**
- 然後 P1 跟 P2 的該 block 就都進入了 shared state,倒楣的 x1 又....
---
# Roofline model
## 1. 多處理器效能基準測試 (Benchmarks)
在介紹模型之前,投影片列出了幾種常見的平行處理效能測試標準:
* **Linpack:** 矩陣線性代數運算(常用於 Top500 超級電腦排名)。
* **SPECrate:** 平行執行 SPEC CPU 程式,測試任務級 (Job-level) 平行度。
* **SPLASH / PARSEC:** 針對共享記憶體 (Shared Memory) 的應用程式基準測試,包含多種核心運算與應用場景。
---
## 2. Roofline Model (屋頂模型) 核心概念
這個模型將硬體極限與軟體特性結合在一張圖表上,幫助開發者判斷瓶頸所在。

## **A. 兩個關鍵參數**
模型主要由以下兩個參數構成座標軸:
1. **X 軸 - 運算強度 (Arithmetic Intensity):**
* 定義:每存取一個 Byte 的記憶體,進行了多少次浮點運算 (FLOPs)。
* 公式:$\frac{\text{Floating-Point Ops}}{\text{Bytes Accessed}}$
* 意義:反映演算法對資料的重複利用率。強度越高,代表對記憶體頻寬的需求相對越低。
2. **Y 軸 - 可達到的效能 (Attainable GFLOPs/sec):**
* 系統實際能跑出的運算速度。
## **B. 「屋頂」的組成 (The Roof)**
圖表中的「屋頂」形狀由兩條線組成,代表硬體的物理極限:
1. **斜線部分 (Memory Bandwidth Limited):**
* 受限於 **Peak Memory Bandwidth (記憶體頻寬峰值)**。
* 當運算強度較低時,效能卡在==記憶體傳輸速度==,CPU 運算單元在等待資料。
2. **水平線部分 (Computation Limited):**
* 受限於 **Peak Floating-Point Performance (浮點運算峰值)**。
* 當運算強度夠高(資料讀進來後被重複運算很多次),效能瓶頸就會轉移到 ==CPU 的運算速度==上。
## **C. 效能公式**
系統能達到的效能是兩者的最小值:
$$
\displaylines{
Attainable\ GFLOPs/sec = \\
\min(\text{Peak Memory BW} \times \text{Arithmetic Intensity},\ \text{Peak FP Performance})
}
$$
- 對照上圖的話,$\text{Peak FP Performance}$ 就是 16 GFlops/sec
- 意思就是 ==CPU 的運算速度上限==是 16 GFlops/sec
- $\text{Peak Memory BW} \times \text{Arithmetic Intensity}$ 得到的就是斜線上的 y 值
- 從 x = 1 的位置我們可以知道 $\text{Peak Memory BW}$ 就是 16 bytes / sec
- 意思就是 ==記憶體傳輸速度上限是== 16 bytes / sec
- 所以 x = $\frac{1}{4}$ 的位置,剛好乘上 16 就會得到 4 GFlops/sec
:::warning
所以當演算法的運算強度拉滿後,就可以吃滿 Memory BandWidth,因此就可以達到 Peak FP Performance,或者說 CPU 的上限所以當演算法的運算強度拉滿後,就可以吃滿 Memory BandWidth,因此就可以達到 Peak FP Performance,或者說 CPU 的上限。
:::
## 3. 演算法的運算強度光譜

不同類型演算法的運算強度,決定了它們落在 Roofline Model 的哪個位置:
* **低強度 (容易受限於記憶體):** 稀疏矩陣 (Sparse Matrix)、結構化網格 (Structured Grids)。
* **中強度:** 頻譜方法 (FFTs)。
* **高強度 (容易受限於運算能力):** 稠密矩陣 (Dense Matrix, 如 BLAS3)、N-body 粒子模擬。
---
### 4. 系統比較範例 (Opteron X2 vs. X4)
投影片最後用 AMD Opteron X2 (雙核) 與 X4 (四核) 的比較來說明模型價值:

* **情境:** X4 的核心數是 X2 的兩倍(理論運算峰值倍增),但兩者共用相同的記憶體系統(頻寬相同)。
* **圖表分析:**
* **左側 (低運算強度):** 兩條線重疊。因為瓶頸是記憶體頻寬,增加核心數(X4)無法提升效能。
* **右側 (高運算強度):** X4 的「屋頂」比較高。只有當程式的運算強度夠高,或者工作集 (Working Set) 能塞進 Cache (避免存取主記憶體) 時,X4 才能發揮出比 X2 更高的效能。
### 總結
這個模型告訴我們:**單純增加 CPU 核心數或時脈不一定能變快**。如果你的程式落在 Roofline 的左側斜坡(記憶體受限),你應該優化記憶體存取(提升運算強度);如果落在右側平頂(運算受限),你才需要考慮向量化指令或更多核心。
# Bisection Bandwidth
---
CMP chip multi processor = multicore
透過 router 來做資料溝通(packet switch),每個核心就好像一個節點
又叫做 network on chip
L1 屬於 private 的 cache, L2 雖然物理上是分布的,但是實際上是屬於 shared 的 cache
老師說現在核心很多的多半是用這種
大小核 for energy efficiency,因為有時候大核能力太強了,比較耗電
---
# Hardware Multithreading
這是加速具有超純量技術的 CPU 的一個技巧,讓 CPU 可以在多個 thread 之間切換,甚至並行。

上圖是 4 stage 的 pipeline,一個 row 代表一個 cycle
- 可以看到 Superscalar 有些時候是空白的,代表該時候資源是沒有被利用的,會造成浪費;這些空白可能是因為 branch 導致的 NoOP 或是某些 hazard 的 stall。
- 所以我們希望不要有空白,也就是空白的時候就換別的 thread 去執行。
- Fine-Grained 就是大家輪流執行一個 stage,這樣就很難會有空白了
- 又叫做 Round robin
- Coarse-Grained 是直到 CPU 發現有空白的時候才換 thread 執行
但是要注意,超純量是指令級的並行,你可以發現一個 cycle 內就只有一個 thread 的指令。
最右邊是多核心處理器,一個處理器內有多個核心,自然就可以同時執行不同的 thread。
此外還有一個更著名的,Intel 稱作超執行緒,在有超純量的指令級並行的情況下,同時達成執行緒級並行:

就是最右邊的 SMT (Simultaneous-Multithreading),讓一個 cycle 可以運行不同 thread 的指令
>很像 OFDMA
---
SIMD 像是矩陣相乘,一次需要很多資料
並且由於一次只需要一次 fetch ,所以比較節能,因為 fetching 其實很耗電的,所以 SIMD 效能會比 MIMD 會比較好
而且也可以讓寫 code 的人不需要用平行化的方式去思考演算法(比較好想),物理層面都做好了
SIMD 隨著趨勢演進,就是讓一個暫存器內可以一次攜帶更多 Data 進來,前提是要保持這些資料是連續的且 aligned 的,才可以進行辨識。
Vector Architectures
好像是專門用來矩陣運算的架構?
總之就是我們設計一種很酷的暫存器,他可以存放 n 個 m bit 的值,每個 m bit 之間是獨立的。
這種暫存器需要配套的指令支援,像是 RISCV 的 V 擴展。
其用法就是可以一次性的將 2 個 n 個 m bit 的值,每個 row 各自相加等等的操作;也算是簡化了平行演算法,並且因為省去了 loop,還可以避免 control hazard
但相對的,硬體也要做出調整, ALU 必須要修改成很多個 ALU 協同運作,例如某個 ALU 負責計算第 0, 4, 8... 個 row 的值
interleaved 是之前提到過的 memory bank,也就是記憶體中的資料會分成不同的 bank 來存放,可能第一個 bank 放第餘數是 0 byte 的資料等等
MLPerf 是個專門用來比較 ML 的 Benchmark
---
這幾個章節(第 51 頁至 58 頁)主要在介紹如何利用 **Data-Level Parallelism(資料級平行化)** 來提升效能,也就是「一個指令同時處理多筆資料」。
這裡將技術分成了兩大類來探討:**SIMD 多媒體擴充指令集** 與 **向量架構 (Vector Architectures)**。
以下是詳細的章節重點說明:
### 1. 運算分類與 SIMD 概念 (Page 51-52)
* **分類 (Flynn's Taxonomy):**
* [cite_start]投影片首先釐清了電腦架構的分類。傳統單核心是 **SISD** (單一指令單一資料),而這裡要講的是 **SIMD** (單一指令多重資料) [cite: 739]。
* **SIMD 的核心精神:**
* [cite_start]**效率:** 只需要讀取一次指令 (Fetch),就能對一整排資料進行運算,這比 MIMD (多指令多資料) 更省電 [cite: 755-756]。
* [cite_start]**應用場景:** 非常適合處理矩陣運算、影像處理或聲音訊號,因為這些資料通常都是一整塊且需要進行相同的運算 [cite: 744-746]。
### 2. SIMD 多媒體擴充指令集 (Multimedia Extensions) (Page 53)
這是現代 CPU(如 Intel/AMD x86)常見的實作方式。
* **背景:** 許多媒體資料(如像素的 R、G、B)只需要 8-bit 或 16-bit,但 CPU 暫存器通常是 64-bit。
* [cite_start]**作法:** 將長暫存器切分,例如一個 64-bit 暫存器可以同時裝 8 個 8-bit 的數字,一次加法就能算完 8 個像素的亮度調整 [cite: 765-767]。
* **演進歷史:**
* **Intel MMX (1996):** 64-bit 運算。
* **SSE (1999):** 128-bit 運算,支援浮點數。
* [cite_start]**AVX (2010):** 256-bit 運算 [cite: 768-774]。
* [cite_start]**限制:** 資料在記憶體中必須是**連續且對齊 (Aligned)** 的,否則無法有效率讀取 [cite: 775]。
### 3. 向量架構 (Vector Architectures) (Page 54-57)
這是一種比上述 SIMD 擴充指令更進階、更靈活的架構(投影片以 RISC-V Vector 為例)。
* **運作方式:**
* [cite_start]使用專門的「向量暫存器」(Vector Registers),可以一次讀入大量的資料元素 [cite: 779]。
* [cite_start]**Gather / Scatter:** 這是向量架構的一大特點。它支援從記憶體中「收集 (Gather)」分散的資料到暫存器,運算完再「分散 (Scatter)」寫回,不像傳統 SIMD 被限制在連續記憶體 [cite: 780-783]。
* **程式碼比較 (DAXPY 範例):**
* 投影片比較了 $Y = a \times X + Y$ 的程式碼。
* [cite_start]**傳統 RISC-V:** 需要寫迴圈 (Loop),每次只算一個數,還要處理索引增加與跳轉指令,指令數多 [cite: 819-846]。
* [cite_start]**向量 RISC-V:** 幾乎沒有迴圈,幾個指令 (`fld.v`, `fmul.d.vs`, `fadd.d.v`) 就能算完一整排資料,大幅減少了指令讀取的頻寬需求 [cite: 847-862]。
* [cite_start]**優勢:** 編譯器更容易優化,因為它明確告訴硬體「這裡沒有迴圈相依性 (Loop-carried dependences)」,且比 MMX/SSE 這種特定擴充指令更通用 [cite: 867-872]。
### 4. 硬體實作:多通道 (Multiple Lanes) (Page 58)
* **概念:** 為了讓向量運算更快,硬體內部會將運算單元切分成多個 **Lanes (通道)**。
* **運作:**
* 假設有 4 個通道 (Lane 0 ~ Lane 3)。
* Lane 0 負責計算第 0, 4, 8... 個元素。
* Lane 1 負責計算第 1, 5, 9... 個元素。
* [cite_start]這樣可以讓硬體資源利用率最大化,同時並行處理多個資料點 [cite: 875-910]。
### 總結
這幾頁是從**軟體概念**到**硬體實作**的推進:
1. **SIMD 概念:** 一個指令算多筆資料。
2. **Multimedia Extensions (如 AVX):** x86 CPU 上的實作,要求資料連續。
3. **Vector Architectures:** 更高階的架構,支援 Gather/Scatter,程式碼更精簡。
4. **Lanes:** 硬體如何透過多通道並行來實現上述架構。