0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PyTorchの `Linear` 層はCPU・GPU・FPGA・ASICの中で何になるのか?

0
Posted at

はじめに

機械学習では、こんなコードを何気なく書きます。

import torch
import torch.nn as nn

layer = nn.Linear(784, 256)

たった1行です。

しかしハードウェアの視点から見ると、この1行はかなり面白いものです。

同じ nn.Linear(784, 256) でも、

  • CPU
  • GPU
  • FPGA
  • ASIC

では、実際の実装が大きく異なります。

では、

PyTorchで書いた1層のLinearは、最終的にハードウェアの中で何になるのか?

を追ってみます。

1. nn.Linear(784, 256) は何をしているのか

PyTorchのLinear層は、数学的には

$$
\mathbf{y} =
\mathbf{x}W^T+\mathbf{b}
$$

を計算しています。

今回、

nn.Linear(784, 256)

なので、

$$
W\in\mathbb{R}^{256\times784}
$$

です。

つまり重みの数は、

$$
256\times784=200704\text{ 個}
$$

さらにbiasが256個あります。

したがって、このたった1層だけでも

$200704+256=200960$個のパラメータを持っています。

FP32なら1つ4 byteなので、

$$
200960\times4=803840\ {\rm byte}
$$

約785 KiBです。

2. Pythonから見るとただの行列

まずPython側では非常に単純です。

x = torch.randn(1, 784)

layer = nn.Linear(784, 256)

y = layer(x)

入力は、

x.shape = [1, 784]

重みは、

weight.shape = [256, 784]

出力は、

y.shape = [1, 256]

となります。

内部で行っているのは本質的に、

y = x @ weight.T + bias

です。

つまり、$
784
$個の入力と重みを掛けて足す処理を、256個の出力ニューロンについて行います。

1つの出力について、

$$
y_i=\sum_{j=0}^{783}w_{ij}x_j+b_i
$$

です。

したがって必要な乗算回数は、

$$
784\times256=200704
$$

回です。

加算もほぼ同程度必要になります。

この、

$$
\boxed{\text{Multiply + Accumulate}}
$$

すなわちMAC演算こそが、ニューラルネットワーク向けハードウェアの中心です。

3. CPUではどうなるか

まずCPUで実行してみます。

device = "cpu"

layer = nn.Linear(784, 256).to(device)
x = torch.randn(1, 784, device=device)

y = layer(x)

PyTorchから見るとこれだけですが、CPU内部ではもっと複雑です。

ざっくり書くと、

PyTorch
   ↓
Linear
   ↓
Matrix multiplication
   ↓
CPU kernel / BLAS
   ↓
SIMD命令
   ↓
FMA

という流れです。

重みは最初、メインメモリ上に存在します。

DRAM

w00
w01
w02
...
w255,783

計算するときは、

DRAM
 ↓
L3 Cache
 ↓
L2 Cache
 ↓
L1 Cache
 ↓
Register
 ↓
SIMD / FMA

と運ばれます。

例えば概念的には、

x0 × w0
x1 × w1
x2 × w2
x3 × w3
      ↓
   accumulate

です。

CPUではLinear専用回路が存在するわけではない

ここが重要です。

CPUは基本的には、

「これはニューラルネットワークのLinear層だ」

とは考えていません。

単に、

大きな行列積を計算している

だけです。

例えばAVX系SIMDなら、複数の値を一度にレジスタへ読み込み、

Register A

x0 x1 x2 x3 x4 x5 x6 x7

Register B

w0 w1 w2 w3 w4 w5 w6 w7

として、

x0*w0
x1*w1
...
x7*w7

を並列計算します。

さらにFMA、

$$
a\times b+c
$$

を使って、

acc = acc + x * w

を高速に実行します。

つまりCPUでは、

$$
\boxed{
\text{Linear}
\rightarrow
\text{行列積}
\rightarrow
\text{SIMD命令列}
}
$$

となります。

4. GPUに持っていく

次はGPUです。

コード上では、

device = "cuda"

layer = nn.Linear(784, 256).to(device)
x = torch.randn(1, 784, device=device)

y = layer(x)

程度の変更しかありません。

しかし、ハードウェア内部では状況が大きく変わります。

CPUでは少数の高性能コアで演算していました。

GPUでは、

$$
\boxed{\text{大量の演算器を並列に動かす}}
$$

方向になります。

5. GPUの中では重みはHBM/GDDRへ行く

重み行列

$$
W_{256\times784}
$$

はGPUメモリへ転送されます。

CPU DRAM

weight
  │
  │ PCIe / Unified memory etc.
  ↓

GPU HBM / GDDR

weight

そこから、

HBM
 ↓
L2 Cache
 ↓
Shared Memory / Cache
 ↓
Register
 ↓
CUDA Core / Tensor Core

とデータが移動します。

6. GPUではLinearが行列積として処理される

バッチサイズを $(B) $とすると、

$$
X\in\mathbb{R}^{B\times784}
$$

なので、

$$
Y
=XW^T
$$

は、

$$
(B\times784)\times(784\times256)
$$

という行列積です。

例えば、

x.shape

が

[1024, 784]

なら、$
1024\times784
$と$
784\times256
$の行列積になります。

ここまで大きくなるとGPUの得意分野です。

7. Tensor Coreではさらに小さな行列単位で計算する

GPUには通常の演算器に加えて、行列積に特化したTensor Coreがあります。

概念的には、

$$
D=A\times B+C
$$

のような演算を、小さな行列ブロック単位で実行します。

例えば巨大な

$$
1024\times784
$$

と

$$
784\times256
$$

の行列を、

┌───┬───┬───┐
│ A │ A │ A │
├───┼───┼───┤
│ A │ A │ A │
└───┴───┴───┘

のような小さいタイルへ分割します。

重み側も同様です。

┌───┬───┐
│ W │ W │
├───┼───┤
│ W │ W │
├───┼───┤
│ W │ W │
└───┴───┘

そして各タイルについて高速に行列積を実行します。

つまりGPUでは、

$$
\boxed{
\text{Linear}
\rightarrow
\text{GEMM}
\rightarrow
\text{Tile}
\rightarrow
\text{Tensor Core}
}
$$

となります。

8. CPUとGPUの最大の違いは?

ここまでなら、

GPUはCPUをたくさん並べただけでは?

と思うかもしれません。

大きな違いの一つは、

$$
\boxed{\text{並列度}}
$$

です。

CPUは、

少数の高性能コア

GPUは、

大量の比較的小さい演算器

を持っています。

Linearのような演算は、

$$
y_i
=\sum_j w_{ij}x_j
$$

という同じ計算を大量に繰り返すため、GPUとの相性が非常に良いわけです。

9. ではFPGAに持っていくとどうなるか

ここから少し世界が変わります。

CPUやGPUでは、

nn.Linear(...)

を実行するために既存のプロセッサを使いました。

FPGAでは、

Linearを計算する回路そのものを作る

ことができます。

例えば、

Input Buffer
     ↓
Multiplier
     ↓
Adder
     ↓
Accumulator
     ↓
Output

という回路です。

10. FPGAでは重みをBRAMに入れられる

重みは例えばBRAMへ格納できます。

今回なら、200704個のweightがあります。

INT8へ量子化すれば、

$$
200704\times8
=1605632\ {\rm bit}
$$

です。

およそ、$
1.61\ {\rm Mbit}
$になります。

これをFPGA内部のBRAMやURAMへ配置できます。

イメージすると、

              FPGA

┌───────────────────────────┐
│                           │
│   ┌───────────────┐       │
│   │ Weight BRAM   │       │
│   │               │       │
│   │ w0            │       │
│   │ w1            │       │
│   │ w2            │       │
│   │ ...           │       │
│   └───────┬───────┘       │
│           ↓               │
│      ┌────────┐           │
│ x ──→│  DSP   │──→ Acc    │
│      └────────┘           │
│                           │
└───────────────────────────┘

となります。

11. FPGAのDSPブロックがMACになる

FPGAには乗算・加算向けのDSPブロックがあります。

概念的には、

$$
P=A\times B+C
$$

です。

つまり、

activation ─┐
            × ──┐
weight ─────┘   │
                + → output
accumulator ────┘

となります。

ニューラルネットワークの

$$
acc\leftarrow acc+xw
$$

そのものです。

12. 何個MACを並べるかを自分で決められる

FPGAで面白いのはここです。

例えばMACを1個だけ使えば、

MAC0

x0*w0
↓
x1*w1
↓
x2*w2
↓
...

と順番に200704回計算できます。

非常に省回路ですが遅いです。

逆に256個並べれば、

         x0,x1,...x783

           ↓

MAC ─────→ y0
MAC ─────→ y1
MAC ─────→ y2
...
MAC ─────→ y255

と256ニューロンを並列に計算できます。

さらに極端に言えば、200704 個の乗算器を作れば、全weightについて同時並列に計算することも理論上は可能です。

もちろん現実にはリソース不足になります。

したがってFPGA設計では、

$$
\boxed{
\text{速度}
\leftrightarrow
\text{回路面積}
}
$$

のトレードオフを設計者が決めることになります。

13. FPGAではループが「時間」から「空間」になる

ソフトウェアなら、

for i in range(256):
    for j in range(784):
        y[i] += x[j] * w[i][j]

と書きます。

CPUでは、このループを時間方向に高速実行します。

一方FPGAでは、この一部を回路として展開できます。

例えば、

x0 ─×w0──┐
x1 ─×w1──┤
x2 ─×w2──┼→ Adder Tree → y
x3 ─×w3──┤
...      │

のように、

forループをハードウェアの空間へ展開する

ことができます。

これはCPU/GPUとFPGAの大きな違いです。

14. Pipelineも自由に作れる

さらに、

Clock 1
Multiply

Clock 2
Add

Clock 3
Accumulate

Clock 4
Output

のようにパイプライン化できます。

一度パイプラインが満たされれば、

CLK1  data0
CLK2  data1
CLK3  data2
CLK4  data3
...

と毎クロック新しいデータを投入できます。

つまりFPGAでは、

$$
\boxed{
\text{ニューラルネットワークのデータフロー}=\text{回路のデータフロー}
}
$$

にかなり近づきます。

15. 重みそのものを回路へ埋め込むこともできる

さらに、推論専用でweightが変わらない場合を考えます。

例えば、$
w=37
$なら、$
37x
$を毎回乗算器で計算する必要はありません。

$$
37=32+4+1
$$

なので、

$$
37x
=(x<<5)+(x<<2)+x
$$

とできます。

つまり、

        ┌── <<5 ──┐
x ──────┼── <<2 ──┼→ ADD → y
        └─────────┘

です。

この場合、

weight = 37

というデータそのものがなくなっています。

37という情報は、

$$
\boxed{\text{回路構造}}
$$

になっています。

16. ASICではどうなるか

ASICではさらに専用化できます。

FPGAでは、

  • LUT
  • Programmable Switch
  • DSP Block
  • BRAM

など既に存在する回路を組み合わせました。

ASICでは、

必要な回路そのものを設計する

ことができます。

例えばLinear専用アクセラレータなら、

              ASIC

┌──────────────────────────────┐
│                              │
│     Weight SRAM              │
│   ┌─────────────┐            │
│   │ w w w w w   │            │
│   │ w w w w w   │            │
│   └──────┬──────┘            │
│          ↓                   │
│   ┌────────────────────┐     │
│   │ MAC MAC MAC MAC    │     │
│   │ MAC MAC MAC MAC    │     │
│   │ MAC MAC MAC MAC    │     │
│   └────────────────────┘     │
│              ↓               │
│        Accumulator           │
│              ↓               │
│         Output SRAM          │
│                              │
└──────────────────────────────┘

のようにできます。

17. ASICではWeight SRAMをMACの真横に置ける

ここはかなり重要です。

AIチップでは計算そのものだけでなく、

$$
\boxed{\text{データを運ぶこと}}
$$

に大きなコストがかかります。

CPUの場合、

DRAM
 ↓
Cache
 ↓
Register
 ↓
ALU

GPUでも、

HBM
 ↓
Cache
 ↓
Register
 ↓
Tensor Core

です。

ASICなら、

SRAM | MAC

という配置を最初から設計できます。

つまりweightを使う場所のすぐ近くに保存できます。

18. なぜ「重みを動かさない」が重要なのか

例えばMACを1000個並べたとします。

各MACが毎クロック8 bitのweightを必要とすると、

$$
1000\times8
=8000\ {\rm bit/cycle}
$$

必要です。

1 GHzなら、

$$
8\times10^{12}\ {\rm bit/s}
$$

つまり8 Tbit/sです。

MACを増やせば増やすほど、

どうやってweightを供給するのか?

が問題になります。

そこでASICでは、

SRAM
 ↓
MAC
 ↓
MAC
 ↓
MAC

のようにデータを再利用したり、

同じweightを複数回使ったりします。

19. Weight Stationaryという考え方

例えば、$
w
$をMAC内部に保持します。

       w
       ↓
    ┌─────┐
x → │ MAC │ → output
    └─────┘

そして入力だけを、

x0
x1
x2
x3
...

と流していきます。

これが、

Weight Stationary

というデータフローです。

weightを動かさないことで、$
\text{memory access}
$を減らします。

20. CPU→GPU→FPGA→ASICを並べてみる

ここまでを同じLinear層について整理すると面白いです。

PyTorchでは全部、

nn.Linear(784, 256)

です。

しかしCPUでは、

Linear
 ↓
Matrix Multiply
 ↓
SIMD Instructions
 ↓
ALU / FMA

GPUでは、

Linear
 ↓
GEMM
 ↓
Matrix Tile
 ↓
Tensor Core

FPGAでは、

Linear
 ↓
RTL / HLS
 ↓
BRAM + DSP
 ↓
Pipeline

ASICでは、

Linear
 ↓
Architecture
 ↓
SRAM + MAC Array
 ↓
Standard Cells / SRAM Macro
 ↓
Transistors

になります。

つまり、

PyTorch

nn.Linear(784,256)

       │
       ├──────── CPU
       │          ↓
       │        SIMD
       │
       ├──────── GPU
       │          ↓
       │      Tensor Core
       │
       ├──────── FPGA
       │          ↓
       │       DSP + BRAM
       │
       └──────── ASIC
                  ↓
              SRAM + MAC
                  ↓
             Transistors

です。

21. 同じ200704個のweightでも「存在の仕方」が違う

ここがこの記事で一番重要なところです。

数学的には、すべて同じ

$$
W\in\mathbb{R}^{256\times784}
$$

です。

しかしCPUでは、

DRAMやCacheに置かれたデータ

です。

GPUでは、

HBMからTensor Coreへ供給される行列データ

です。

FPGAでは、

BRAMのデータ、あるいは論理回路

になります。

ASICでは、

専用SRAMのbit、場合によっては固定された回路構造

になります。

つまり、

$$
\boxed{
\text{同じ数学的パラメータでも、
ハードウェアによって物理的な姿は全く違う}
}
$$

ということです。

22. さらに下へ行くと「weightとは何か?」が変わる

CPUでweightを見れば、

32-bit floating point data

です。

FPGAなら、

8-bit BRAM data

かもしれません。

ASICなら、

SRAM Cell

です。

さらにAnalog Compute-in-Memoryまで行けば、$
w
$をコンダクタンス$
G
$として持たせることさえできます。

つまり、

PyTorch
weight = Tensor

CPU
weight = memory data

GPU
weight = HBM data

FPGA
weight = BRAM / logic

ASIC
weight = SRAM / circuit

Analog CIM
weight = physical quantity

と、抽象度を下げるたびに「重み」の意味が変わっていきます。

まとめ

image.png

今回見たのは、たった1行です。

nn.Linear(784, 256)

数学的には、

$$
y=xW^T+b
$$

という単純な式です。

しかし、その下では、

CPUなら、

$$
\text{命令として実行}
$$

GPUなら、

$$
\text{大量並列行列演算}
$$

FPGAなら、

$$
\text{演算そのものを回路化}
$$

ASICなら、

$$
\text{専用データパスとしてシリコン化}
$$

されています。

個人的にハードウェアの視点で機械学習を見るとき、一番面白いのはここだと思います。

Pythonでは、

layer(x)

というたった一行だったものが、

ハードウェアを一段ずつ降りていくことで、

Tensor

↓

Matrix

↓

Bit

↓

Register / SRAM

↓

Multiplier

↓

Adder

↓

Logic Gate

↓

Transistor

へと変わっていきます。

そしてAIアクセラレータの設計では、

「掛け算をどう高速化するか」

だけではなく、

200704個のweightを、どこに置き、どうMACへ運び、何回再利用するか

が非常に重要になります。

ソフトウェア上ではただの nn.Linear。

しかしハードウェアから見ると、その1行の裏には巨大な設計空間が広がっています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?