.. raw:: html
.. raw:: html
.. raw:: html
.. _sec_use_gpu:
GPU
===
.. raw:: html
Trong phần giới thiệu của cuốn sách này, chúng ta đã thảo luận về sự
tăng trưởng đột phá của năng lực tính toán trong hai thập niên vừa qua.
Một cách ngắn gọn, hiệu năng GPU đã tăng lên gấp 1000 lần trong mỗi thập
niên kể từ năm 2000. Điều này mang lại cơ hội to lớn nhưng kèm theo đó
là một nhu cầu không hề nhỏ để cung cấp hiệu năng tính toán như vậy.
.. raw:: html
+-----------------+-----------------+-----------------+-----------------+
| Thập niên | Tập dữ liệu | Bộ nhớ | Số Phép tính |
| | | | Dấu phẩy động |
| | | | trên Giây |
+=================+=================+=================+=================+
| 1970 | 100 (Iris) | 1 KB | 100 KF (Intel |
| | | | 8080) |
+-----------------+-----------------+-----------------+-----------------+
| 1980 | 1 K (Giá nhà | 100 KB | 1 MF (Intel |
| | tại Boston) | | 80186) |
+-----------------+-----------------+-----------------+-----------------+
| 1990 | 10 K (Nhận diện | 10 MB | 10 MF (Intel |
| | ký tự quang | | 80486) |
| | học) | | |
+-----------------+-----------------+-----------------+-----------------+
| 2000 | 10 M (các trang | 100 MB | 1 GF (Intel |
| | web) | | Core) |
+-----------------+-----------------+-----------------+-----------------+
| 2010 | 10 G (quảng | 1 GB | 1 TF (NVIDIA |
| | cáo) | | C2050) |
+-----------------+-----------------+-----------------+-----------------+
| 2020 | 1 T (mạng xã | 100 GB | 1 PF (NVIDIA |
| | hội) | | DGX-2) |
+-----------------+-----------------+-----------------+-----------------+
.. raw:: html
Trong phần này, ta bắt đầu thảo luận cách khai thác hiệu năng tính toán
này cho việc nghiên cứu. Đầu tiên ta sẽ tìm hiểu cách sử dụng một GPU
duy nhất, rồi sau này tiến tới nhiều GPU và nhiều máy chủ (cùng với
nhiều GPU). Bạn có thể đã nhận ra MXNet ``ndarray`` trông gần như giống
hệt NumPy, nhưng chúng có một vài điểm khác biệt quan trọng. Một trong
những tính năng chính khiến cho MXNet khác với NumPy là MXNet hỗ trợ
nhiều loại phần cứng đa dạng.
.. raw:: html
Trong MXNet, mỗi mảng có một bối cảnh. Cho tới giờ, tất cả các biến và
phép toán liên quan đều được giao cho CPU theo mặc định. Các bối cảnh
thường có thể là nhiều GPU khác. Mọi thứ còn có thể trở nên rối rắm hơn
khi ta triển khai công việc trên nhiều máy chủ. Bằng cách chỉ định bối
cảnh cho các mảng một cách thông minh, ta có thể giảm thiểu thời gian
truyền tải dữ liệu giữa các thiết bị. Ví dụ, khi huấn luyện mạng nơ-ron
trên máy chủ có GPU, ta thường muốn các tham số mô hình nằm ở trên GPU.
.. raw:: html
.. raw:: html
.. raw:: html
Nói ngắn gọn, với những mạng nơ-ron phức tạp và dữ liệu quy mô lớn, việc
chỉ sử dụng CPU để tính toán có thể sẽ không hiệu quả. Trong phần này,
ta sẽ thảo luận về cách sử dụng một GPU NVIDIA duy nhất cho việc tính
toán. Đầu tiên, hãy chắc chắn rằng bạn đã lắp đặt ít nhất một GPU
NVIDIA. Sau đó, hãy `tải
CUDA `__ và làm theo gợi ý
để thiết lập đường dẫn hợp lý. Một khi các bước chuẩn bị đã được hoàn
thành, ta có thể dùng lệnh ``nvidia-smi`` để xem thông tin card đồ họa.
.. code:: python
!nvidia-smi
.. parsed-literal::
:class: output
Thu Aug 13 03:12:01 2020
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 430.50 Driver Version: 430.50 CUDA Version: 10.1 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
|===============================+======================+======================|
| 0 TITAN Xp On | 00000000:04:00.0 Off | N/A |
| 35% 60C P2 187W / 250W | 11751MiB / 12196MiB | 49% Default |
+-------------------------------+----------------------+----------------------+
| 1 TITAN Xp On | 00000000:06:00.0 Off | N/A |
| 38% 64C P2 92W / 250W | 11751MiB / 12196MiB | 60% Default |
+-------------------------------+----------------------+----------------------+
| 2 TITAN Xp On | 00000000:07:00.0 Off | N/A |
| 30% 40C P8 10W / 250W | 0MiB / 12196MiB | 0% Default |
+-------------------------------+----------------------+----------------------+
| 3 TITAN Xp On | 00000000:08:00.0 Off | N/A |
| 23% 21C P8 9W / 250W | 0MiB / 12196MiB | 0% Default |
+-------------------------------+----------------------+----------------------+
| 4 TITAN Xp On | 00000000:0C:00.0 Off | N/A |
| 23% 21C P8 8W / 250W | 11767MiB / 12196MiB | 0% Default |
+-------------------------------+----------------------+----------------------+
| 5 TITAN Xp On | 00000000:0D:00.0 Off | N/A |
| 23% 21C P8 7W / 250W | 0MiB / 12196MiB | 0% Default |
+-------------------------------+----------------------+----------------------+
| 6 TITAN Xp On | 00000000:0E:00.0 Off | N/A |
| 26% 46C P2 83W / 250W | 5697MiB / 12196MiB | 0% Default |
+-------------------------------+----------------------+----------------------+
| 7 TITAN Xp On | 00000000:0F:00.0 Off | N/A |
| 23% 20C P8 8W / 250W | 0MiB / 12196MiB | 0% Default |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: GPU Memory |
| GPU PID Type Process name Usage |
|=============================================================================|
| 0 10621 C /opt/conda/bin/python 11739MiB |
| 1 10734 C /opt/conda/bin/python 11739MiB |
| 4 30712 C /usr/bin/python3 11757MiB |
| 6 25599 C /usr/bin/python3 5687MiB |
+-----------------------------------------------------------------------------+
.. raw:: html
Tiếp theo, cần chắc chắn rằng ta đã cài đặt phiên bản GPU của MXNet. Nếu
phiên bản CPU của MXNet đã được cài đặt trước, ta cần phải gỡ bỏ nó. Ví
dụ, hãy sử dụng lệnh ``pip uninstall mxnet``, sau đó cài đặt phiên bản
MXNet tương ứng với phiên bản CUDA. Giả sử như bạn đã cài CUDA 9.0, bạn
có thể cài phiên bản MXNet có hỗ trợ CUDA 9.0 bằng lệnh
``pip install mxnet-cu90``. Để chạy các chương trình trong phần này, bạn
cần ít nhất hai GPU.
.. raw:: html
Yêu cầu này có vẻ khá phung phí với hầu hết các bộ máy tính để bàn nhưng
lại rất dễ dàng nếu ta dùng các dịch vụ đám mây, chẳng hạn ta có thể
thuê một máy chủ AWS EC2 đa GPU. Hầu hết các phần khác trong cuốn sách
này *không* yêu cầu đa GPU. Tuy nhiên, việc này chỉ để minh họa cách dữ
liệu được truyền giữa các thiết bị khác nhau.
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
Thiết bị Tính toán
------------------
.. raw:: html
MXNet có thể chỉ định các thiết bị, chẳng hạn như CPU và GPU, cho việc
lưu trữ và tính toán. Mặc định, MXNet tạo dữ liệu trong bộ nhớ chính và
sau đó sử dụng CPU để tính toán. Trong MXNet, CPU và GPU có thể được chỉ
định bởi ``cpu ()`` và ``gpu ()``. Cần lưu ý rằng ``cpu()`` (đơn thuần
hoặc thêm bất kỳ số nguyên nào trong ngoặc đơn) có nghĩa là sử dụng tất
cả các CPU và bộ nhớ vật lý. Điều này có nghĩa các tính toán của MXNet
sẽ cố gắng tận dụng tất cả các lõi CPU. Tuy nhiên, ``gpu()`` đơn thuần
chỉ đại diện cho một card đồ họa và bộ nhớ đồ họa tương ứng. Nếu có
nhiều GPU, chúng tôi sử dụng ``gpu(i)`` để thể hiện GPU thứ :math:`i`
(với :math:`i` bắt đầu từ 0). Ngoài ra, ``gpu(0)`` và ``gpu()`` tương
đương nhau.
.. code:: python
from mxnet import np, npx
from mxnet.gluon import nn
npx.set_np()
npx.cpu(), npx.gpu(), npx.gpu(1)
.. parsed-literal::
:class: output
(cpu(0), gpu(0), gpu(1))
.. raw:: html
Ta có thể truy vấn số lượng GPU có sẵn thông qua ``num_gpus()``.
.. code:: python
npx.num_gpus()
.. parsed-literal::
:class: output
2
.. raw:: html
Bây giờ ta định nghĩa hai hàm chức năng thuận tiện cho việc chạy mã kể
cả khi GPU được yêu cầu không tồn tại.
.. code:: python
# Saved in the d2l package for later use
def try_gpu(i=0):
"""Return gpu(i) if exists, otherwise return cpu()."""
return npx.gpu(i) if npx.num_gpus() >= i + 1 else npx.cpu()
# Saved in the d2l package for later use
def try_all_gpus():
"""Return all available GPUs, or [cpu(),] if no GPU exists."""
ctxes = [npx.gpu(i) for i in range(npx.num_gpus())]
return ctxes if ctxes else [npx.cpu()]
try_gpu(), try_gpu(3), try_all_gpus()
.. parsed-literal::
:class: output
(gpu(0), cpu(0), [gpu(0), gpu(1)])
.. raw:: html
``ndarray`` và GPU
------------------
.. raw:: html
Mặc định, các đối tượng ``ndarray`` được tạo trên CPU. Do đó, ta sẽ thấy
định danh ``@cpu(0)`` mỗi khi ta in một ``ndarray``.
.. code:: python
x = np.array([1, 2, 3])
x.ctx
.. parsed-literal::
:class: output
cpu(0)
.. raw:: html
Điều quan trọng cần lưu ý là bất cứ khi nào ta muốn làm các phép toán
trên nhiều số hạng, chúng cần phải ở trong cùng một bối cảnh. Chẳng hạn,
nếu ta tính tổng hai biến, ta cần đảm bảo rằng cả hai đối số đều nằm
trên cùng một thiết bị — nếu không thì MXNet sẽ không biết nơi lưu trữ
kết quả hoặc thậm chí cách quyết định nơi thực hiện tính toán.
.. raw:: html
.. raw:: html
.. raw:: html
Lưu trữ trên GPU
~~~~~~~~~~~~~~~~
.. raw:: html
Có một số cách để lưu trữ một ``ndarray`` trên GPU. Ví dụ: ta có thể chỉ
định một thiết bị lưu trữ với tham số ``ctx`` khi tạo một\ ``ndarray``.
Tiếp theo, ta tạo biến ``ndarray`` là ``a`` trên ``gpu(0)``. Lưu ý rằng
khi in ``a`` ra màn hình, thông tin thiết bị sẽ trở thành ``@gpu(0)``.
``ndarray`` được tạo trên GPU nào chỉ chiếm bộ nhớ của GPU đó. Ta có thể
sử dụng lệnh ``nvidia-smi`` để xem việc sử dụng bộ nhớ GPU. Nói chung,
ta cần đảm bảo rằng ta không tạo dữ liệu vượt quá giới hạn bộ nhớ GPU.
.. code:: python
x = np.ones((2, 3), ctx=try_gpu())
x
.. parsed-literal::
:class: output
array([[1., 1., 1.],
[1., 1., 1.]], ctx=gpu(0))
.. raw:: html
Giả sử bạn có ít nhất hai GPU, đoạn mã sau sẽ tạo ra một mảng ngẫu nhiên
trên ``gpu(1)``.
.. code:: python
y = np.random.uniform(size=(2, 3), ctx=try_gpu(1))
y
.. parsed-literal::
:class: output
array([[0.67478997, 0.07540122, 0.9956977 ],
[0.09488854, 0.415456 , 0.11231736]], ctx=gpu(1))
.. raw:: html
.. raw:: html
.. raw:: html
Sao chép
~~~~~~~~
.. raw:: html
Nếu ta muốn tính :math:`\mathbf{x} + \mathbf{y}` thì ta cần quyết định
nơi thực hiện phép tính này. Chẳng hạn, như trong
:numref:`fig_copyto`, ta có thể chuyển :math:`\mathbf{x}` sang
``gpu(1)`` và thực hiện phép tính ở đó. *Đừng* chỉ thêm ``x + y`` vì
điều này sẽ dẫn đến một lỗi. Hệ thống thời gian chạy sẽ không biết phải
làm gì và gặp lỗi bởi nó không thể tìm thấy dữ liệu trên cùng một thiết
bị.
.. raw:: html
.. _fig_copyto:
.. figure:: ../img/copyto.svg
Lệnh ``copyto`` sao chép các mảng đến thiết bị mục tiêu
.. raw:: html
Lệnh ``copyto`` sao chép dữ liệu sang một thiết bị khác để ta có thể
cộng chúng. Vì :math:`\mathbf{y}` tồn tại trên GPU thứ hai, ta cần di
chuyển :math:`\mathbf{x}` trước khi ta có thể cộng chúng lại.
.. code:: python
z = x.copyto(try_gpu(1))
print(x)
print(z)
.. parsed-literal::
:class: output
[[1. 1. 1.]
[1. 1. 1.]] @gpu(0)
[[1. 1. 1.]
[1. 1. 1.]] @gpu(1)
.. raw:: html
.. raw:: html
.. raw:: html
Bây giờ dữ liệu đã ở trên cùng một GPU (cả :math:`\mathbf{z}` và
:math:`\mathbf{y}`), ta có thể cộng lại. Trong những trường hợp như vậy
MXNet lưu kết quả tại cùng thiết bị với các toán hạng. Trong trường hợp
này là ``@gpu(1)``.
.. code:: python
y + z
.. parsed-literal::
:class: output
array([[1.6747899, 1.0754012, 1.9956977],
[1.0948886, 1.415456 , 1.1123173]], ctx=gpu(1))
.. raw:: html
Giả sử biến ``z`` hiện đang được lưu trong GPU thứ hai (gpu(1)). Điều gì
sẽ xảy ra nếu ta gọi ``z.copyto(gpu(1))``? Hàm này sẽ tạo một bản sao
của biến và cấp phát vùng nhớ mới cho bản sao, ngay cả khi biến đang có
trong thiết bị. Có những lúc mà tuỳ thuộc vào môi trường thực thi lệnh,
hai biến có thể đã ở trên cùng một thiết bị. Do đó chúng ta muốn chỉ tạo
bản sao khi các biến tồn tại ở các ngữ cảnh khác nhau. Trong các trường
hợp đó, ta có thể gọi ``as_in_ctx()``. Nếu biến đó đã tồn tại trong ngữ
cảnh thì hàm này không thực hiện lệnh nào. Trên thực tế, trừ trường hợp
đặc biệt bạn muốn tạo bản sao, hãy sử dụng ``as_in_ctx()``.
.. code:: python
z = x.as_in_ctx(try_gpu(1))
z
.. parsed-literal::
:class: output
array([[1., 1., 1.],
[1., 1., 1.]], ctx=gpu(1))
.. raw:: html
Cần lưu ý rằng, nếu ``ctx`` của biến nguồn và biến đích là giống nhau,
hàm ``as_in_ctx`` sẽ khiến biến đích có cùng vùng nhớ với biến nguồn.
.. code:: python
y.as_in_ctx(try_gpu(1)) is y
.. parsed-literal::
:class: output
True
.. raw:: html
Hàm ``copyto`` luôn luôn cấp phát vùng nhớ mới cho biến đích.
.. code:: python
y.copyto(try_gpu(1)) is y
.. parsed-literal::
:class: output
False
.. raw:: html
.. raw:: html
.. raw:: html
Những lưu ý bên lề
~~~~~~~~~~~~~~~~~~
.. raw:: html
Mọi người sử dụng GPU để thực hiện việc tính toán trong học máy vì họ kỳ
vọng chúng sẽ nhanh hơn. Nhưng việc truyền các biến giữa các bối cảnh
lại diễn ra chậm. Do đó, chúng tôi mong bạn chắc chắn 100% rằng bạn muốn
thực hiện một việc nào đó thật chậm trước khi chúng tôi để bạn thực hiện
nó. Nếu MXNet chỉ thực hiện việc sao chép tự động mà không gặp sự cố thì
có thể bạn sẽ không nhận ra được mình đã có những đoạn mã chưa tối ưu
đến nhường nào.
.. raw:: html
Thêm vào đó, việc truyền dữ liệu giữa các thiết bị (CPU, GPU và các máy
khác) *chậm hơn nhiều* so với việc thực hiện tính toán. Nó cũng làm cho
việc song song hóa trở nên khó hơn nhiều, vì chúng ta phải chờ cho dữ
liệu được gửi đi (hoặc được nhận về) trước khi chúng ta có thể tiến hành
nhiều tác vụ xử lý tính toán hơn. Đây là lý do tại sao các hoạt động sao
chép nên được dành sự lưu tâm lớn. Quy tắc nằm lòng là nhiều xử lý tính
toán nhỏ thì tệ hơn nhiều so với một xử lý tính toán lớn. Hơn nữa, xử lý
nhiều phép tính toán cùng một thời điểm thì tốt hơn nhiều so với nhiều
xử lý tính toán đơn lẻ nằm rải rác trong chương trình (trừ khi là bạn
hiểu rõ mình đang làm gì). Lý do là ở tình huống này những hoạt động như
vậy có thể gây tắc nghẽn nếu một thiết bị phải chờ một thiết bị khác
trước khi nó có thể làm điều gì đó khác. Việc này hơi giống việc bạn
phải xếp hàng mua cà phê thay vì đặt trước qua điện thoại và biết được
khi nào nó đã sẵn sàng để đến lấy.
.. raw:: html
Sau cùng, khi chúng ta in các ``ndarray`` hoặc chuyển các ``ndarray``
sang định dạng Numpy, nếu dữ liệu không có trong bộ nhớ chính, MXNet sẽ
sao chép nó tới bộ nhớ chính trước tiên, dẫn tới việc tốn thêm thời gian
chờ cho việc truyền dữ liệu. Thậm chí tệ hơn, điều đáng sợ lúc này là nó
phụ thuộc vào Bộ Khóa Phiên dịch Toàn cục (*Global Interpreter Lock*)
khiến mọi thứ phải chờ Python hoàn tất.
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
.. raw:: html
Gluon và GPU
------------
.. raw:: html
Tương tự, mô hình của Gluon có thể chỉ định thiết bị dựa vào tham số
``ctx`` trong quá trình khởi tạo. Đoạn mã dưới đây khởi tạo các tham số
của mô hình trên GPU (sau này chúng ta sẽ thấy nhiều ví dụ về cách chạy
các mô hình trên GPU, đơn giản bởi chúng phần nào sẽ cần khả năng tính
toán mạnh hơn).
.. code:: python
net = nn.Sequential()
net.add(nn.Dense(1))
net.initialize(ctx=try_gpu())
.. raw:: html
Khi đầu vào là một ``ndarray`` trên GPU, Gluon sẽ tính toán kết quả trên
cùng GPU đó.
.. code:: python
net(x)
.. parsed-literal::
:class: output
array([[0.04995865],
[0.04995865]], ctx=gpu(0))
.. raw:: html
Hãy kiểm chứng lại rằng các tham số của mô hình được lưu trên cùng GPU.
.. code:: python
net[0].weight.data().ctx
.. parsed-literal::
:class: output
gpu(0)
.. raw:: html
Tóm lại, khi dữ liệu và các tham số ở trên cùng thiết bị, ta có thể huấn
luyện mô hình một cách hiệu quả. Ta sẽ xem xét một vài ví dụ như thế
trong phần tiếp theo.
.. raw:: html
Tóm tắt
-------
.. raw:: html
- MXNet có thể chỉ định các thiết bị thực hiện việc lưu trữ và tính
toán như CPU hay GPU. Mặc định, MXNet tạo dữ liệu trên bộ nhớ chính
và sử dụng CPU để tính toán.
- MXNet yêu cầu tất cả dữ liệu đầu vào *nằm trên cùng thiết bị* trước
khi thực hiện tính toán, tức cùng một CPU hoặc cùng một GPU.
- Hiệu năng có thể giảm đáng kể nếu di chuyển dữ liệu một cách không
cẩn thận. Một lỗi thường gặp là: việc tính toán mất mát cho các
minibatch trên GPU rồi in kết quả ra cửa sổ dòng lệnh (hoặc ghi kết
quả vào mảng NumPy) sẽ kích hoạt Bộ Khóa Phiên dịch Toàn cục làm tất
cả GPU dừng hoạt động. Sẽ tốt hơn nếu cấp phát bộ nhớ cho việc ghi
lại quá trình hoạt động (*logging*) ở GPU và chỉ di chuyển các bản
ghi lớn.
.. raw:: html
Bài tập
-------
.. raw:: html
1. Thử một tác vụ có khối lượng tính toán lớn, ví dụ như nhân các ma
trận kích thước lớn để thấy sự khác nhau về tốc độ giữa CPU và GPU.
Và với tác vụ có khối lượng tính toán nhỏ thì sao?
2. Làm thế nào để đọc và ghi các tham số của mô hình trên GPU?
3. Đo thời gian thực hiện 1000 phép nhân ma trận kích thước
:math:`100 \times 100` và ghi lại giá trị chuẩn
:math:`\mathrm{tr} M M^\top` của từng kết quả, rồi so sánh với việc
lưu tất cả giá trị chuẩn tại một bản ghi ở GPU và chỉ trả về bản ghi
đó.
4. Đo thời gian thực hiện hai phép nhân ma trận tại hai GPU cùng lúc so
với việc thực hiện chúng lần lượt trên cùng một GPU (gợi ý: bạn sẽ
thấy tỉ lệ gần như tuyến tính).
.. raw:: html
.. raw:: html
Thảo luận
---------
- `Tiếng Anh `__
- `Tiếng Việt `__
Những người thực hiện
---------------------
Bản dịch trong trang này được thực hiện bởi:
- Đoàn Võ Duy Thanh
- Trần Yến Thy
- Lê Khắc Hồng Phúc
- Nguyễn Mai Hoàng Long
- Nguyễn Văn Cường
- Phạm Hồng Vinh
- Phạm Minh Đức
- Vũ Hữu Tiệp