{ "cells": [ { "cell_type": "markdown", "id": "254ec04e", "metadata": { "origin_pos": 0 }, "source": [ "# Tính toán không đồng bộ\n", ":label:`sec_async`\n", "\n", "Máy tính ngày nay là các hệ thống song song cao, bao gồm nhiều lõi CPU (thường là nhiều luồng trên mỗi lõi), nhiều yếu tố xử lý trên mỗi GPU, và thường là nhiều GPU trên mỗi thiết bị. Nói tóm lại, chúng ta có thể xử lý nhiều thứ khác nhau cùng một lúc, thường là trên các thiết bị khác nhau. Thật không may Python không phải là một cách tuyệt vời để viết mã song song và không đồng bộ, ít nhất là không có một số trợ giúp bổ sung. Rốt cuộc, Python là một luồng và điều này không có khả năng thay đổi trong tương lai. Các khuôn khổ học sâu như MXNet và TensorFlow áp dụng mô hình lập trình không đồng bộ* * để cải thiện hiệu suất, trong khi PyTorch sử dụng trình lập lịch riêng của Python dẫn đến sự đánh đổi hiệu suất khác. Đối với PyTorch, theo mặc định, các hoạt động GPU là không đồng bộ. Khi bạn gọi một chức năng sử dụng GPU, các hoạt động được xếp hàng vào thiết bị cụ thể, nhưng không nhất thiết phải được thực hiện cho đến sau này. Điều này cho phép chúng tôi thực hiện song song nhiều tính toán hơn, bao gồm các hoạt động trên CPU hoặc các GPU khác. \n", "\n", "Do đó, hiểu cách thức hoạt động của lập trình không đồng bộ giúp chúng ta phát triển các chương trình hiệu quả hơn, bằng cách chủ động giảm các yêu cầu tính toán và phụ thuộc lẫn nhau. Điều này cho phép chúng tôi giảm chi phí bộ nhớ và tăng mức sử dụng bộ xử lý.\n" ] }, { "cell_type": "code", "execution_count": 1, "id": "10a8f2d7", "metadata": { "execution": { "iopub.execute_input": "2022-04-26T12:18:19.839289Z", "iopub.status.busy": "2022-04-26T12:18:19.838496Z", "iopub.status.idle": "2022-04-26T12:18:22.730040Z", "shell.execute_reply": "2022-04-26T12:18:22.729062Z" }, "origin_pos": 2, "tab": [ "pytorch" ] }, "outputs": [], "source": [ "import os\n", "import subprocess\n", "import numpy\n", "import torch\n", "from torch import nn\n", "from d2l import torch as d2l" ] }, { "cell_type": "markdown", "id": "4ccd6aed", "metadata": { "origin_pos": 3 }, "source": [ "## Asynchrony qua Backend\n" ] }, { "cell_type": "markdown", "id": "e7005a18", "metadata": { "origin_pos": 5, "tab": [ "pytorch" ] }, "source": [ "Đối với một khởi động hãy xem xét vấn đề đồ chơi sau: chúng tôi muốn tạo ra một ma trận ngẫu nhiên và nhân nó. Chúng ta hãy làm điều đó cả trong NumPy và trong tensor PyTorch để thấy sự khác biệt. Lưu ý rằng PyTorch `tensor` được xác định trên GPU.\n" ] }, { "cell_type": "code", "execution_count": 2, "id": "d4919aa1", "metadata": { "execution": { "iopub.execute_input": "2022-04-26T12:18:22.734049Z", "iopub.status.busy": "2022-04-26T12:18:22.733606Z", "iopub.status.idle": "2022-04-26T12:18:27.653074Z", "shell.execute_reply": "2022-04-26T12:18:27.651151Z" }, "origin_pos": 7, "tab": [ "pytorch" ] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "numpy: 1.0858 sec\n", "torch: 0.0013 sec\n" ] } ], "source": [ "# Warmup for GPU computation\n", "device = d2l.try_gpu()\n", "a = torch.randn(size=(1000, 1000), device=device)\n", "b = torch.mm(a, a)\n", "\n", "with d2l.Benchmark('numpy'):\n", " for _ in range(10):\n", " a = numpy.random.normal(size=(1000, 1000))\n", " b = numpy.dot(a, a)\n", "\n", "with d2l.Benchmark('torch'):\n", " for _ in range(10):\n", " a = torch.randn(size=(1000, 1000), device=device)\n", " b = torch.mm(a, a)" ] }, { "cell_type": "markdown", "id": "6797a189", "metadata": { "origin_pos": 9, "tab": [ "pytorch" ] }, "source": [ "Đầu ra điểm chuẩn thông qua PyTorch là đơn đặt hàng cường độ nhanh hơn. Sản phẩm chấm NumPy được thực thi trên bộ xử lý CPU trong khi phép nhân ma trận PyTorch được thực thi trên GPU và do đó sau này dự kiến sẽ nhanh hơn nhiều. Nhưng sự khác biệt thời gian rất lớn cho thấy một cái gì đó khác phải xảy ra. Theo mặc định, các hoạt động GPU không đồng bộ trong PyTorch. Buộc PyTorch hoàn thành tất cả các tính toán trước khi trả về cho thấy những gì đã xảy ra trước đây: tính toán đang được thực hiện bởi backend trong khi frontend trả về kiểm soát Python.\n" ] }, { "cell_type": "code", "execution_count": 3, "id": "5e6ea285", "metadata": { "execution": { "iopub.execute_input": "2022-04-26T12:18:27.662552Z", "iopub.status.busy": "2022-04-26T12:18:27.662077Z", "iopub.status.idle": "2022-04-26T12:18:27.675998Z", "shell.execute_reply": "2022-04-26T12:18:27.674665Z" }, "origin_pos": 11, "tab": [ "pytorch" ] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Done: 0.0029 sec\n" ] } ], "source": [ "with d2l.Benchmark():\n", " for _ in range(10):\n", " a = torch.randn(size=(1000, 1000), device=device)\n", " b = torch.mm(a, a)\n", " torch.cuda.synchronize(device)" ] }, { "cell_type": "markdown", "id": "e96d7b88", "metadata": { "origin_pos": 13, "tab": [ "pytorch" ] }, "source": [ "Nói rộng ra, PyTorch có lối vào để tương tác trực tiếp với người dùng, ví dụ, thông qua Python, cũng như một phụ trợ được hệ thống sử dụng để thực hiện tính toán. Như thể hiện trong :numref:`fig_frontends`, người dùng có thể viết các chương trình PyTorch bằng nhiều ngôn ngữ lối vào khác nhau, chẳng hạn như Python và C ++. Bất kể ngôn ngữ lập trình lối vào được sử dụng, việc thực hiện các chương trình PyTorch xảy ra chủ yếu trong phần phụ trợ của các triển khai C ++. Các hoạt động do ngôn ngữ frontend phát hành được chuyển sang phần phụ trợ để thực hiện. Phụ trợ quản lý các luồng riêng của nó liên tục thu thập và thực hiện các nhiệm vụ xếp hàng đợi. Lưu ý rằng để làm việc này, phụ trợ phải có khả năng theo dõi các phụ thuộc giữa các bước khác nhau trong biểu đồ tính toán. Do đó, không thể song song hóa các hoạt động phụ thuộc vào nhau.\n" ] }, { "cell_type": "markdown", "id": "63efeca4", "metadata": { "origin_pos": 14 }, "source": [ "![Programming language frontends and deep learning framework backends.](../img/frontends.png)\n", ":width:`300px`\n", ":label:`fig_frontends`\n", "\n", "Chúng ta hãy nhìn vào một ví dụ đồ chơi khác để hiểu biểu đồ phụ thuộc tốt hơn một chút.\n" ] }, { "cell_type": "code", "execution_count": 4, "id": "d448156f", "metadata": { "execution": { "iopub.execute_input": "2022-04-26T12:18:27.683992Z", "iopub.status.busy": "2022-04-26T12:18:27.683218Z", "iopub.status.idle": "2022-04-26T12:18:27.746703Z", "shell.execute_reply": "2022-04-26T12:18:27.733952Z" }, "origin_pos": 16, "tab": [ "pytorch" ] }, "outputs": [ { "data": { "text/plain": [ "tensor([[3., 3.]], device='cuda:0')" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "x = torch.ones((1, 2), device=device)\n", "y = torch.ones((1, 2), device=device)\n", "z = x * y + 2\n", "z" ] }, { "cell_type": "markdown", "id": "e96b84c7", "metadata": { "origin_pos": 17 }, "source": [ "![The backend tracks dependencies between various steps in the computational graph.](../img/asyncgraph.svg)\n", ":label:`fig_asyncgraph`\n", "\n", "Đoạn mã ở trên cũng được minh họa trong :numref:`fig_asyncgraph`. Bất cứ khi nào Python frontend thread thực hiện một trong ba câu lệnh đầu tiên, nó chỉ đơn giản trả về nhiệm vụ cho hàng đợi phụ trợ. Khi kết quả của câu lệnh cuối cùng cần phải được * printed*, luồng giao diện người dùng Python sẽ chờ thread phụ trợ C++ kết thúc tính toán kết quả của biến `z`. Một lợi ích của thiết kế này là luồng giao diện người dùng Python không cần phải thực hiện các tính toán thực tế. Do đó, có rất ít tác động đến hiệu suất tổng thể của chương trình, bất kể hiệu suất của Python. :numref:`fig_threading` minh họa cách giao diện người dùng và phụ trợ tương tác. \n", "\n", "![Interactions of the frontend and backend.](../img/threading.svg)\n", ":label:`fig_threading`\n", "\n", "## Rào cản và chặn\n" ] }, { "cell_type": "markdown", "id": "74d05aad", "metadata": { "origin_pos": 22 }, "source": [ "## Cải thiện tính toán\n" ] }, { "cell_type": "markdown", "id": "69102e9b", "metadata": { "origin_pos": 26 }, "source": [ "## Tóm tắt\n", "\n", "* Các khuôn khổ học sâu có thể tách các lối vào Python từ một phụ trợ thực thi. Điều này cho phép chèn nhanh không đồng bộ các lệnh vào phụ trợ và song song liên quan.\n", "* Asynchrony dẫn đến một lối vào khá đáp ứng. Tuy nhiên, hãy thận trọng không để lấp đầy hàng đợi tác vụ vì nó có thể dẫn đến tiêu thụ bộ nhớ quá mức. Bạn nên đồng bộ hóa cho mỗi minibatch để giữ lối vào và phụ trợ xấp xỉ đồng bộ hóa.\n", "* Các nhà cung cấp chip cung cấp các công cụ phân tích hiệu suất tinh vi để có được cái nhìn sâu sắc hơn nhiều về hiệu quả của việc học sâu.\n" ] }, { "cell_type": "markdown", "id": "0d173fb2", "metadata": { "origin_pos": 28 }, "source": [ "## Bài tập\n" ] }, { "cell_type": "markdown", "id": "205bb10a", "metadata": { "origin_pos": 30, "tab": [ "pytorch" ] }, "source": [ "1. Trên CPU, điểm chuẩn các hoạt động nhân ma trận tương tự trong phần này. Bạn vẫn có thể quan sát sự không đồng bộ thông qua backend?\n" ] }, { "cell_type": "markdown", "id": "74a40658", "metadata": { "origin_pos": 32, "tab": [ "pytorch" ] }, "source": [ "[Discussions](https://discuss.d2l.ai/t/2564)\n" ] } ], "metadata": { "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }