.. 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