到目前為止,我們在基于梯度的學(xué)習(xí)方法中遇到了兩個(gè)極端:第 12.3 節(jié)使用完整數(shù)據(jù)集來計(jì)算梯度和更新參數(shù),一次一個(gè)傳遞。相反, 第 12.4 節(jié)一次處理一個(gè)訓(xùn)練示例以取得進(jìn)展。它們中的任何一個(gè)都有其自身的缺點(diǎn)。當(dāng)數(shù)據(jù)非常相似時(shí),梯度下降并不是特別有效。隨機(jī)梯度下降在計(jì)算上不是特別有效,因?yàn)?CPU 和 GPU 無法利用矢量化的全部功能。這表明可能存在介于兩者之間的東西,事實(shí)上,這就是我們迄今為止在討論的示例中一直使用的東西。
12.5.1。矢量化和緩存
決定使用小批量的核心是計(jì)算效率。在考慮并行化到多個(gè) GPU 和多個(gè)服務(wù)器時(shí),這一點(diǎn)最容易理解。在這種情況下,我們需要向每個(gè) GPU 發(fā)送至少一張圖像。每臺(tái)服務(wù)器 8 個(gè) GPU 和 16 個(gè)服務(wù)器,我們已經(jīng)達(dá)到了不小于 128 的小批量大小。
當(dāng)涉及到單個(gè) GPU 甚至 CPU 時(shí),事情就有點(diǎn)微妙了。這些設(shè)備有多種類型的內(nèi)存,通常有多種類型的計(jì)算單元和它們之間不同的帶寬限制。例如,CPU 有少量寄存器,然后是 L1、L2,在某些情況下甚至是 L3 緩存(在不同處理器內(nèi)核之間共享)。這些緩存的大小和延遲都在增加(同時(shí)它們的帶寬在減少)。可以說,處理器能夠執(zhí)行的操作比主內(nèi)存接口能夠提供的要多得多。
首先,具有 16 個(gè)內(nèi)核和 AVX-512 矢量化的 2GHz CPU 最多可以處理2?109?16?32=1012每秒字節(jié)數(shù)。GPU 的能力很容易超過這個(gè)數(shù)字的 100 倍。另一方面,中端服務(wù)器處理器的帶寬可能不會(huì)超過 100 GB/s,即不到保持處理器所需帶寬的十分之一喂。更糟糕的是,并非所有內(nèi)存訪問都是平等的:內(nèi)存接口通常為 64 位寬或更寬(例如,在 GPU 上高達(dá) 384 位),因此讀取單個(gè)字節(jié)會(huì)產(chǎn)生更寬訪問的成本。
其次,第一次訪問的開銷很大,而順序訪問相對便宜(這通常稱為突發(fā)讀取)。還有很多事情要記住,比如當(dāng)我們有多個(gè)套接字、小芯片和其他結(jié)構(gòu)時(shí)的緩存。 有關(guān)更深入的討論,請參閱此 維基百科文章。
緩解這些限制的方法是使用 CPU 高速緩存的層次結(jié)構(gòu),這些高速緩存的速度實(shí)際上足以為處理器提供數(shù)據(jù)。這是深度學(xué)習(xí)中批處理背后的驅(qū)動(dòng)力。為了簡單起見,考慮矩陣-矩陣乘法,比如 A=BC. 我們有多種計(jì)算方法A. 例如,我們可以嘗試以下操作:
-
我們可以計(jì)算 Aij=Bi,:C:,j,即,我們可以通過點(diǎn)積的方式逐元素計(jì)算它。
-
我們可以計(jì)算 A:,j=BC:,j,也就是說,我們可以一次計(jì)算一列。同樣我們可以計(jì)算 A一排Ai,:一次。
-
我們可以簡單地計(jì)算A=BC.
-
我們可以打破B和C分成更小的塊矩陣并計(jì)算A一次一個(gè)塊。
如果我們遵循第一個(gè)選項(xiàng),每次我們想要計(jì)算一個(gè)元素時(shí),我們都需要將一行和一列向量復(fù)制到 CPU 中 Aij. 更糟糕的是,由于矩陣元素是順序?qū)R的,因此當(dāng)我們從內(nèi)存中讀取兩個(gè)向量之一時(shí),我們需要訪問許多不相交的位置。第二種選擇要有利得多。在其中,我們能夠保留列向量C:,j在 CPU 緩存中,同時(shí)我們繼續(xù)遍歷B. 這將內(nèi)存帶寬要求減半,訪問速度也相應(yīng)加快。當(dāng)然,選項(xiàng) 3 是最可取的。不幸的是,大多數(shù)矩陣可能無法完全放入緩存(畢竟這是我們正在討論的內(nèi)容)。然而,選項(xiàng) 4 提供了一個(gè)實(shí)用的替代方法:我們可以將矩陣的塊移動(dòng)到緩存中并在本地將它們相乘。優(yōu)化的庫會(huì)為我們解決這個(gè)問題。讓我們看看這些操作在實(shí)踐中的效率如何。
除了計(jì)算效率之外,Python 和深度學(xué)習(xí)框架本身引入的開銷也相當(dāng)可觀。回想一下,每次我們執(zhí)行命令時(shí),Python 解釋器都會(huì)向 MXNet 引擎發(fā)送命令,而 MXNet 引擎需要將其插入計(jì)算圖中并在調(diào)度期間對其進(jìn)行處理。這種開銷可能非常有害。簡而言之,強(qiáng)烈建議盡可能使用矢量化(和矩陣)。
%matplotlib inline
import time
import numpy as np
import tensorflow as tf
from d2l import tensorflow as d2l
A = tf.Variable(tf.zeros((256, 256)))
B = tf.Variable(tf.random.normal([256, 256], 0, 1))
C = tf.Variable(tf.random.normal([256, 256], 0, 1))
由于我們將在本書的其余部分頻繁地對運(yùn)行時(shí)間進(jìn)行基準(zhǔn)測試,因此讓我們定義一個(gè)計(jì)時(shí)器。
class Timer: #@save
"""Record multiple running times."""
def __init__(self):
self.times = []
self.start()
def start(self):
"""Start the timer."""
self.tik = time.time()
def stop(self):
"""Stop the timer and record the time in a list."""
self.times.append(time.time() - self.tik)
return self.times[-1]
def avg(self):
"""Return the average time."""
return sum(self.times) / len(self.times)
def sum(self):
"""Return the sum of time."""
return sum(self.times)
def cumsum(self):
"""Return the accumulated time."""
return np.array(self.times).cumsum().tolist()
timer = Timer()
class Timer: #@save
"""Record multiple running times."""
def __init__(self):
self.times = []
self.start()
def start(self):
"""Start the timer."""
self.tik = time.time()
def stop(self):
"""Stop the timer and record the time in a list."""
self.times.append(time.time() - self.tik)
return self.times[-1]
def avg(self):
"""Return the average time."""
return sum(self.times) / len(self.times)
def sum(self):
"""Return the sum of time."""
return sum(self.times)
def cumsum(self):
"""Return the accumulated time."""
return np.array(self.times).cumsum().tolist()
timer = Timer()
class Timer: #@save
"""Record multiple running times."""
def __init__(self):
self.times = []
self.start()
def start(self):
"""Start the timer."""
self.tik = time.time()
def stop(self):
"""Stop the timer and record the time in a list."""
self.times.append(time.time() - self.tik)
return self.times[-1]
def avg(self):
"""Return the average time."""
return sum(self.times) / len(se
評(píng)論
查看更多