{ "cells": [ { "cell_type": "markdown", "id": "83f72b43", "metadata": { "origin_pos": 0 }, "source": [ "# Trình biên dịch và phiên dịch\n", ":label:`sec_hybridize`\n", "\n", "Cho đến nay, cuốn sách này đã tập trung vào lập trình bắt buộc, làm cho việc sử dụng các tuyên bố như `print`, `+`, và `if` để thay đổi trạng thái của một chương trình. Hãy xem xét ví dụ sau đây của một chương trình bắt buộc đơn giản.\n" ] }, { "cell_type": "code", "execution_count": 1, "id": "f0e64466", "metadata": { "execution": { "iopub.execute_input": "2022-04-26T12:30:08.766546Z", "iopub.status.busy": "2022-04-26T12:30:08.765755Z", "iopub.status.idle": "2022-04-26T12:30:08.776570Z", "shell.execute_reply": "2022-04-26T12:30:08.775690Z" }, "origin_pos": 1, "tab": [ "pytorch" ] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "10\n" ] } ], "source": [ "def add(a, b):\n", " return a + b\n", "\n", "def fancy_func(a, b, c, d):\n", " e = add(a, b)\n", " f = add(c, d)\n", " g = add(e, f)\n", " return g\n", "\n", "print(fancy_func(1, 2, 3, 4))" ] }, { "cell_type": "markdown", "id": "44f2e0bd", "metadata": { "origin_pos": 2 }, "source": [ "Python là một ngôn ngữ*được giải thị*. Khi đánh giá hàm `fancy_func` trên, nó thực hiện các thao tác tạo nên cơ thể của hàm * theo trình tự *. Đó là, nó sẽ đánh giá `e = add(a, b)` và lưu trữ các kết quả dưới dạng biến `e`, do đó thay đổi trạng thái của chương trình. Hai câu lệnh tiếp theo `f = add(c, d)` và `g = add(e, f)` sẽ được thực thi tương tự, thực hiện bổ sung và lưu trữ các kết quả dưới dạng biến. :numref:`fig_compute_graph` minh họa luồng dữ liệu. \n", "\n", "![Data flow in an imperative program.](../img/computegraph.svg)\n", ":label:`fig_compute_graph`\n", "\n", "Mặc dù lập trình bắt buộc là thuận tiện, nó có thể không hiệu quả. Một mặt, ngay cả khi hàm `add` được gọi nhiều lần trong suốt `fancy_func`, Python sẽ thực hiện ba cuộc gọi hàm riêng lẻ. Nếu chúng được thực thi, giả sử, trên GPU (hoặc thậm chí trên nhiều GPU), chi phí phát sinh từ trình thông dịch Python có thể trở nên áp đảo. Hơn nữa, nó sẽ cần phải lưu các giá trị biến của `e` và `f` cho đến khi tất cả các câu lệnh trong `fancy_func` đã được thực thi. Điều này là do chúng ta không biết liệu các biến `e` và `f` sẽ được sử dụng bởi các phần khác của chương trình sau khi các câu lệnh `e = add(a, b)` và `f = add(c, d)` được thực thi. \n", "\n", "## Lập trình tượng trưng\n", "\n", "Hãy xem xét phương pháp thay thế, *lập trình tượng trưng, trong đó tính toán thường chỉ được thực hiện một khi quá trình đã được xác định đầy đủ. Chiến lược này được sử dụng bởi nhiều khuôn khổ học sâu, bao gồm Theano và TensorFlow (sau này đã có được các phần mở rộng bắt buộc). Nó thường bao gồm các bước sau: \n", "\n", "1. Xác định các hoạt động sẽ được thực thi.\n", "1. Biên dịch các hoạt động thành một chương trình thực thi.\n", "1. Cung cấp các đầu vào cần thiết và gọi chương trình được biên dịch để thực hiện.\n", "\n", "Điều này cho phép tối ưu hóa một lượng đáng kể. Đầu tiên, chúng ta có thể bỏ qua trình thông dịch Python trong nhiều trường hợp, do đó loại bỏ một nút cổ chai hiệu suất có thể trở nên đáng kể trên nhiều GPU nhanh được ghép nối với một luồng Python duy nhất trên CPU. Thứ hai, một trình biên dịch có thể tối ưu hóa và viết lại mã trên vào `print((1 + 2) + (3 + 4))` hoặc thậm chí `print(10)`. Điều này là có thể kể từ khi một trình biên dịch được để xem mã đầy đủ trước khi biến nó thành hướng dẫn máy. Ví dụ, nó có thể giải phóng bộ nhớ (hoặc không bao giờ phân bổ nó) bất cứ khi nào một biến không còn cần thiết nữa. Hoặc nó có thể biến đổi mã hoàn toàn thành một phần tương đương. Để có được một ý tưởng tốt hơn, hãy xem xét mô phỏng sau đây của lập trình bắt buộc (nó là Python sau tất cả) dưới đây.\n" ] }, { "cell_type": "code", "execution_count": 2, "id": "8b7c9d31", "metadata": { "execution": { "iopub.execute_input": "2022-04-26T12:30:08.780338Z", "iopub.status.busy": "2022-04-26T12:30:08.779965Z", "iopub.status.idle": "2022-04-26T12:30:08.785553Z", "shell.execute_reply": "2022-04-26T12:30:08.784695Z" }, "origin_pos": 3, "tab": [ "pytorch" ] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "def add(a, b):\n", " return a + b\n", "\n", "def fancy_func(a, b, c, d):\n", " e = add(a, b)\n", " f = add(c, d)\n", " g = add(e, f)\n", " return g\n", "print(fancy_func(1, 2, 3, 4))\n", "10\n" ] } ], "source": [ "def add_():\n", " return '''\n", "def add(a, b):\n", " return a + b\n", "'''\n", "\n", "def fancy_func_():\n", " return '''\n", "def fancy_func(a, b, c, d):\n", " e = add(a, b)\n", " f = add(c, d)\n", " g = add(e, f)\n", " return g\n", "'''\n", "\n", "def evoke_():\n", " return add_() + fancy_func_() + 'print(fancy_func(1, 2, 3, 4))'\n", "\n", "prog = evoke_()\n", "print(prog)\n", "y = compile(prog, '', 'exec')\n", "exec(y)" ] }, { "cell_type": "markdown", "id": "91b6a110", "metadata": { "origin_pos": 4 }, "source": [ "Sự khác biệt giữa lập trình bắt buộc (giải thích) và lập trình tượng trưng như sau: \n", "\n", "* Lập trình bắt buộc dễ dàng hơn. Khi lập trình bắt buộc được sử dụng trong Python, phần lớn mã là đơn giản và dễ viết. Nó cũng dễ dàng hơn để gỡ lỗi mã lập trình bắt buộc. Điều này là do dễ dàng hơn để có được và in tất cả các giá trị biến trung gian có liên quan hoặc sử dụng các công cụ gỡ lỗi tích hợp sẵn của Python.\n", "* Lập trình tượng trưng hiệu quả hơn và dễ dàng hơn để cổng. Lập trình tượng trưng giúp tối ưu hóa mã trong quá trình biên dịch dễ dàng hơn, đồng thời có khả năng chuyển chương trình thành một định dạng độc lập với Python. Điều này cho phép chương trình được chạy trong môi trường không phải Python, do đó tránh mọi vấn đề hiệu suất tiềm ẩn liên quan đến trình thông dịch Python.\n", "\n", "## Lập trình lai\n", "\n", "Trong lịch sử hầu hết các khuôn khổ học sâu lựa chọn giữa một cách tiếp cận bắt buộc hoặc mang tính biểu tượng. Ví dụ, Theano, TensorFlow (lấy cảm hứng từ trước đây), Keras và CNTK xây dựng các mô hình tượng trưng. Ngược lại, Chainer và PyTorch có một cách tiếp cận bắt buộc. Một chế độ bắt buộc đã được thêm vào TensorFlow 2.0 và Keras trong các phiên bản sau này.\n" ] }, { "cell_type": "markdown", "id": "98f00418", "metadata": { "origin_pos": 6, "tab": [ "pytorch" ] }, "source": [ "Như đã đề cập ở trên, PyTorch dựa trên lập trình bắt buộc và sử dụng đồ thị tính toán động. Trong nỗ lực tận dụng tính di động và hiệu quả của lập trình tượng trưng, các nhà phát triển đã xem xét liệu có thể kết hợp lợi ích của cả hai mô hình lập trình hay không. Điều này dẫn đến một torchscript cho phép người dùng phát triển và gỡ lỗi bằng cách sử dụng lập trình bắt buộc thuần túy, trong khi có khả năng chuyển đổi hầu hết các chương trình thành các chương trình tượng trưng để chạy khi hiệu suất và triển khai tính toán cấp sản phẩm được yêu cầu.\n" ] }, { "cell_type": "markdown", "id": "fd70bebe", "metadata": { "origin_pos": 8 }, "source": [ "## Lai tạo lớp `Sequential`\n", "\n", "Cách dễ nhất để có được cảm nhận về cách thức hoạt động của lai là xem xét các mạng sâu với nhiều lớp. Thông thường, trình thông dịch Python sẽ cần thực thi mã cho tất cả các lớp để tạo ra một lệnh sau đó có thể được chuyển tiếp đến CPU hoặc GPU. Đối với một thiết bị điện toán (nhanh) duy nhất, điều này không gây ra bất kỳ vấn đề lớn nào. Mặt khác, nếu chúng ta sử dụng một máy chủ 8 GPU tiên tiến như một phiên bản AWS P3dn.24xlarge Python sẽ phải vật lộn để giữ cho tất cả các GPU bận rộn. Trình thông dịch Python đơn luồng trở thành nút cổ chai ở đây. Hãy để chúng tôi xem cách chúng tôi có thể giải quyết vấn đề này cho các phần quan trọng của mã bằng cách thay thế `Sequential` bằng `HybridSequential`. Chúng tôi bắt đầu bằng cách xác định một MLP đơn giản.\n" ] }, { "cell_type": "code", "execution_count": 3, "id": "2a505a6e", "metadata": { "execution": { "iopub.execute_input": "2022-04-26T12:30:08.789041Z", "iopub.status.busy": "2022-04-26T12:30:08.788670Z", "iopub.status.idle": "2022-04-26T12:30:10.836500Z", "shell.execute_reply": "2022-04-26T12:30:10.835794Z" }, "origin_pos": 10, "tab": [ "pytorch" ] }, "outputs": [ { "data": { "text/plain": [ "tensor([[ 0.0854, -0.0074]], grad_fn=)" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import torch\n", "from torch import nn\n", "from d2l import torch as d2l\n", "\n", "\n", "# Factory for networks\n", "def get_net():\n", " net = nn.Sequential(nn.Linear(512, 256),\n", " nn.ReLU(),\n", " nn.Linear(256, 128),\n", " nn.ReLU(),\n", " nn.Linear(128, 2))\n", " return net\n", "\n", "x = torch.randn(size=(1, 512))\n", "net = get_net()\n", "net(x)" ] }, { "cell_type": "markdown", "id": "65bc2919", "metadata": { "origin_pos": 13, "tab": [ "pytorch" ] }, "source": [ "Bằng cách chuyển đổi mô hình bằng hàm `torch.jit.script`, chúng ta có thể biên dịch và tối ưu hóa tính toán trong MLP. Kết quả tính toán của mô hình vẫn không thay đổi.\n" ] }, { "cell_type": "code", "execution_count": 4, "id": "2fc131cb", "metadata": { "execution": { "iopub.execute_input": "2022-04-26T12:30:10.842680Z", "iopub.status.busy": "2022-04-26T12:30:10.842346Z", "iopub.status.idle": "2022-04-26T12:30:10.949194Z", "shell.execute_reply": "2022-04-26T12:30:10.948240Z" }, "origin_pos": 16, "tab": [ "pytorch" ] }, "outputs": [ { "data": { "text/plain": [ "tensor([[ 0.0854, -0.0074]], grad_fn=)" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "net = torch.jit.script(net)\n", "net(x)" ] }, { "cell_type": "markdown", "id": "028aa333", "metadata": { "origin_pos": 19, "tab": [ "pytorch" ] }, "source": [ "Điều này có vẻ gần như quá tốt là đúng: viết cùng một mã như trước và chỉ cần chuyển đổi mô hình bằng `torch.jit.script`. Khi điều này xảy ra, mạng được tối ưu hóa (chúng tôi sẽ chuẩn hóa hiệu suất bên dưới).\n" ] }, { "cell_type": "markdown", "id": "3327804a", "metadata": { "origin_pos": 21 }, "source": [ "### Tăng tốc bằng cách lai\n", "\n", "Để chứng minh sự cải thiện hiệu suất đạt được bằng cách biên dịch, chúng tôi so sánh thời gian cần thiết để đánh giá `net(x)` trước và sau khi lai. Hãy để chúng tôi xác định một lớp để đo lần này đầu tiên. Nó sẽ có ích trong suốt chương khi chúng tôi đặt ra để đo lường (và cải thiện) hiệu suất.\n" ] }, { "cell_type": "code", "execution_count": 5, "id": "4373ab04", "metadata": { "execution": { "iopub.execute_input": "2022-04-26T12:30:10.953268Z", "iopub.status.busy": "2022-04-26T12:30:10.952674Z", "iopub.status.idle": "2022-04-26T12:30:10.957979Z", "shell.execute_reply": "2022-04-26T12:30:10.957090Z" }, "origin_pos": 22, "tab": [ "pytorch" ] }, "outputs": [], "source": [ "#@save\n", "class Benchmark:\n", " \"\"\"For measuring running time.\"\"\"\n", " def __init__(self, description='Done'):\n", " self.description = description\n", "\n", " def __enter__(self):\n", " self.timer = d2l.Timer()\n", " return self\n", "\n", " def __exit__(self, *args):\n", " print(f'{self.description}: {self.timer.stop():.4f} sec')" ] }, { "cell_type": "markdown", "id": "85d07921", "metadata": { "origin_pos": 24, "tab": [ "pytorch" ] }, "source": [ "Bây giờ chúng ta có thể gọi mạng hai lần, một lần với và một lần không có torchscript.\n" ] }, { "cell_type": "code", "execution_count": 6, "id": "12572b96", "metadata": { "execution": { "iopub.execute_input": "2022-04-26T12:30:10.961941Z", "iopub.status.busy": "2022-04-26T12:30:10.961315Z", "iopub.status.idle": "2022-04-26T12:30:15.332236Z", "shell.execute_reply": "2022-04-26T12:30:15.331529Z" }, "origin_pos": 27, "tab": [ "pytorch" ] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Without torchscript: 2.1717 sec\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "With torchscript: 2.1708 sec\n" ] } ], "source": [ "net = get_net()\n", "with Benchmark('Without torchscript'):\n", " for i in range(1000): net(x)\n", "\n", "net = torch.jit.script(net)\n", "with Benchmark('With torchscript'):\n", " for i in range(1000): net(x)" ] }, { "cell_type": "markdown", "id": "6334e5ff", "metadata": { "origin_pos": 30, "tab": [ "pytorch" ] }, "source": [ "Như đã quan sát thấy trong các kết quả trên, sau khi một phiên bản `nn.Sequential` được viết kịch bản bằng hàm `torch.jit.script`, hiệu suất tính toán được cải thiện thông qua việc sử dụng lập trình tượng trưng.\n" ] }, { "cell_type": "markdown", "id": "7f1cd55c", "metadata": { "origin_pos": 32 }, "source": [ "### Lập số sê-ri\n" ] }, { "cell_type": "markdown", "id": "7ee93abd", "metadata": { "origin_pos": 34, "tab": [ "pytorch" ] }, "source": [ "Một trong những lợi ích của việc biên dịch các mô hình là chúng ta có thể serialize (lưu) mô hình và các tham số của nó vào đĩa. Điều này cho phép chúng tôi lưu trữ một mô hình theo cách độc lập với ngôn ngữ front-end của sự lựa chọn. Điều này cho phép chúng tôi triển khai các mô hình được đào tạo cho các thiết bị khác và dễ dàng sử dụng các ngôn ngữ lập trình front-end khác. Đồng thời mã thường nhanh hơn những gì có thể đạt được trong lập trình bắt buộc. Chúng ta hãy xem chức năng `save` đang hoạt động.\n" ] }, { "cell_type": "code", "execution_count": 7, "id": "7d099336", "metadata": { "execution": { "iopub.execute_input": "2022-04-26T12:30:15.337044Z", "iopub.status.busy": "2022-04-26T12:30:15.336587Z", "iopub.status.idle": "2022-04-26T12:30:16.081604Z", "shell.execute_reply": "2022-04-26T12:30:16.080727Z" }, "origin_pos": 37, "tab": [ "pytorch" ] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "-rw-rw-r-- 1 d2l-worker d2l-worker 651K Apr 26 12:30 my_mlp\r\n" ] } ], "source": [ "net.save('my_mlp')\n", "!ls -lh my_mlp*" ] }, { "cell_type": "markdown", "id": "cf3b7b64", "metadata": { "origin_pos": 50 }, "source": [ "## Tóm tắt\n", "\n", "* Lập trình bắt buộc giúp bạn dễ dàng thiết kế các mô hình mới vì có thể viết mã với luồng điều khiển và khả năng sử dụng một lượng lớn hệ sinh thái phần mềm Python.\n", "* Lập trình tượng trưng yêu cầu chúng ta chỉ định chương trình và biên dịch nó trước khi thực hiện nó. Lợi ích là cải thiện hiệu suất.\n" ] }, { "cell_type": "markdown", "id": "fead0ccb", "metadata": { "origin_pos": 52 }, "source": [ "## Bài tập\n" ] }, { "cell_type": "markdown", "id": "922cdf79", "metadata": { "origin_pos": 54, "tab": [ "pytorch" ] }, "source": [ "1. Xem lại các mô hình mà bạn quan tâm trong các chương trước. Bạn có thể cải thiện hiệu suất tính toán của họ bằng cách triển khai lại chúng không?\n" ] }, { "cell_type": "markdown", "id": "3f051a50", "metadata": { "origin_pos": 56, "tab": [ "pytorch" ] }, "source": [ "[Discussions](https://discuss.d2l.ai/t/2490)\n" ] } ], "metadata": { "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }