.. _sec_use_gpu: Графические процессоры (GPU) ============================ В :numref:`tab_intro_decade` мы продемонстрировали стремительный рост вычислений за последние два десятилетия. Если говорить кратко, производительность графических процессоров (GPU) увеличивалась в 1000 раз каждые десять лет начиная с 2000 года. Это открывает широкие возможности, но также указывает на наличие значительного спроса на такие вычислительные мощности. В этом разделе мы начнем обсуждать, как использовать вычислительную мощность для ваших исследований. Сначала мы рассмотрим использование одного графического процессора, а затем — нескольких графических процессоров и нескольких серверов (с несколькими графическими процессорами). В частности, мы обсудим, как использовать один графический процессор NVIDIA для вычислений. Сначала убедитесь, что у вас установлен хотя бы один графический процессор NVIDIA. Затем скачайте `драйвер NVIDIA и CUDA `__ и следуйте инструкциям, чтобы установить соответствующий путь. После завершения этих подготовительных шагов можно использовать команду ``nvidia-smi`` для просмотра информации о видеокарте. В PyTorch каждый массив имеет устройство; мы часто называем его *контекстом*. До сих пор по умолчанию все переменные и связанные с ними вычисления назначались процессору (CPU). Обычно в качестве других контекстов выступают различные графические процессоры (GPU). Ситуация может стать ещё более запутанной, когда мы распределяем задачи между несколькими серверами. Интеллектуальное назначение массивов контекстам позволяет минимизировать время, затрачиваемое на передачу данных между устройствами. Например, при обучении нейронных сетей на сервере с GPU мы обычно предпочитаем, чтобы параметры модели находились на GPU. Для запуска программ в этом разделе требуется как минимум два графических процессора. Обратите внимание, что для большинства настольных компьютеров это может быть избыточным, однако такие ресурсы легко доступны в облаке, например, с использованием многопроцессорных инстансов AWS EC2. Почти во всех остальных разделах *не* требуется наличие нескольких графических процессоров, но здесь мы просто хотим продемонстрировать поток данных между различными устройствами. .. raw:: latex \diilbookstyleinputcell .. code:: python import torch from torch import nn from d2l import torch as d2l Устройства вычислений --------------------- Мы можем указывать устройства, такие как процессоры (CPU) и графические процессоры (GPU), для хранения и вычислений. По умолчанию тензоры создаются в основной памяти, а для вычислений используется процессор (CPU). В PyTorch устройства CPU и GPU обозначаются как ``torch.device('cpu')`` и ``torch.device('cuda')``. Следует отметить, что устройство ``cpu`` означает все физические процессоры и память. Это означает, что вычисления в PyTorch будут пытаться использовать все ядра CPU. Однако устройство ``gpu`` представляет только одну карту и соответствующую память. Если доступно несколько GPU, мы используем ``torch.device(f'cuda:{i}')`` для обозначения :math:`i^\textrm{th}` GPU (:math:`i` начинается с 0). Кроме того, ``gpu:0`` и ``gpu`` эквивалентны. .. raw:: latex \diilbookstyleinputcell .. code:: python def cpu(): #@save """Get the CPU device.""" return torch.device('cpu') def gpu(i=0): #@save """Get a GPU device.""" return torch.device(f'cuda:{i}') cpu(), gpu(), gpu(1) Мы можем запросить количество доступных GPU. .. raw:: latex \diilbookstyleinputcell .. code:: python def num_gpus(): #@save """Get the number of available GPUs.""" return torch.cuda.device_count() num_gpus() Теперь мы определим две удобные функции, которые позволяют запускать код даже в том случае, если запрошенные GPU отсутствуют. .. raw:: latex \diilbookstyleinputcell .. code:: python def try_gpu(i=0): #@save """Return gpu(i) if exists, otherwise return cpu().""" if num_gpus() >= i + 1: return gpu(i) return cpu() def try_all_gpus(): #@save """Return all available GPUs, or [cpu(),] if no GPU exists.""" return [gpu(i) for i in range(num_gpus())] try_gpu(), try_gpu(10), try_all_gpus() Тензоры и графические процессоры -------------------------------- По умолчанию тензоры создаются на процессоре. Мы можем запросить устройство, на котором расположен тензор. .. raw:: latex \diilbookstyleinputcell .. code:: python x = torch.tensor([1, 2, 3]) x.device Важно отметить, что для выполнения операций с несколькими тензорами они должны находиться на одном устройстве. Например, если мы суммируем два тензора, необходимо убедиться, что оба аргумента находятся на одном устройстве, иначе фреймворк не будет знать, где хранить результат или как определить место выполнения вычислений. Хранение данных на GPU ~~~~~~~~~~~~~~~~~~~~~~ Существует несколько способов **[сохранить тензор на GPU]**. Например, при создании тензора можно указать устройство хранения. Далее мы создадим тензор ``X`` на первом ``gpu``. Тензор, созданный на GPU, использует только память этого GPU. Мы можем использовать команду ``nvidia-smi`` для просмотра использования памяти GPU. В целом, необходимо убедиться, что мы не создаем данные, объём которых превышает лимит памяти GPU. .. raw:: latex \diilbookstyleinputcell .. code:: python X = torch.ones(2, 3, device=try_gpu()) X Предположим, что у вас есть как минимум два графических процессора. Приведённый ниже код создает случайный тензор, ``Y``, на втором графическом процессоре. .. raw:: latex \diilbookstyleinputcell .. code:: python Y = torch.rand(2, 3, device=try_gpu(1)) Y Копирование ~~~~~~~~~~~ Если мы хотим вычислить ``X + Y``, нам нужно решить, где выполнять эту операцию. Например, как показано в :numref:`fig_copyto`, мы можем переместить ``X`` на второй GPU и выполнить операцию там. *Не следует* просто складывать ``X`` и ``Y``, поскольку это приведёт к исключению. Движок времени выполнения не будет знать, что делать: он не может найти данные на одном устройстве, и выполнение завершается ошибкой. Поскольку ``Y`` находится на втором GPU, нам нужно переместить туда же ``X``, прежде чем мы сможем сложить два объекта. .. _fig_copyto: .. figure:: ../img/copyto.svg Скопируйте данные для выполнения операции на том же устройстве. .. raw:: latex \diilbookstyleinputcell .. code:: python Z = X.cuda(1) print(X) print(Z) Теперь, когда данные (как ``Z``, так и ``Y``) находятся на одном GPU, мы можем их сложить. .. raw:: latex \diilbookstyleinputcell .. code:: python Y + Z Но что, если ваша переменная ``Z`` уже находится на втором GPU? Что произойдет, если мы все равно вызовем ``Z.cuda(1)``? Она вернет ``Z`` вместо создания копии и выделения новой памяти. .. raw:: latex \diilbookstyleinputcell .. code:: python Z.cuda(1) is Z Примечания ~~~~~~~~~~ Люди используют графические процессоры (GPU) для машинного обучения, потому что ожидают высокой скорости их работы. Однако передача данных между устройствами происходит медленно: значительно медленнее, чем вычисления. Поэтому мы хотим, чтобы вы были на 100% уверены в необходимости выполнения медленной операции, прежде чем разрешить её. Если бы фреймворк для глубокого обучения автоматически выполнял копирование без сбоев, вы могли бы не заметить, что написали медленный код. Передача данных не только медленна, но и существенно усложняет параллелизацию, поскольку нам приходится ждать отправки (а точнее, получения) данных, прежде чем мы сможем продолжить выполнение других операций. Именно поэтому к операциям копирования следует относиться с особой осторожностью. Как правило, множество мелких операций гораздо хуже одной крупной. Кроме того, выполнение нескольких операций одновременно значительно лучше, чем множество одиночных операций, разбросанных по коду, если только вы точно не знаете, что делаете. Это связано с тем, что такие операции могут блокироваться, если одно устройство вынуждено ждать другое, прежде чем сможет выполнить что-то ещё. Это немного похоже на то, как если бы вы заказывали кофе в очереди, а не заранее по телефону, и обнаруживали, что он уже готов, когда вы уже пришли. Наконец, при выводе тензоров или преобразовании их в формат NumPy, если данные не находятся в основной памяти, фреймворк сначала копирует их туда, что приводит к дополнительным накладным расходам на передачу. Хуже того, в этом случае возникает глобальная блокировка интерпретатора, из-за которой всё ожидает завершения работы Python. Нейронные сети и графические процессоры --------------------------------------- Аналогично, модель нейронной сети может указывать устройства. Приведённый ниже код помещает параметры модели на GPU. .. raw:: latex \diilbookstyleinputcell .. code:: python net = nn.Sequential(nn.LazyLinear(1)) net = net.to(device=try_gpu()) В следующих главах мы рассмотрим множество примеров того, как запускать модели на графических процессорах, просто потому, что модели станут несколько более вычислительно затратными. Например, когда входные данные представляют собой тензор на GPU, модель вычисляет результат на том же GPU. .. raw:: latex \diilbookstyleinputcell .. code:: python net(X) Давайте **убедимся, что параметры модели хранятся на том же GPU.** .. raw:: latex \diilbookstyleinputcell .. code:: python net[0].weight.data.device Пусть тренировщик поддерживает GPU. .. raw:: latex \diilbookstyleinputcell .. code:: python @d2l.add_to_class(d2l.Trainer) #@save def __init__(self, max_epochs, num_gpus=0, gradient_clip_val=0): self.save_hyperparameters() self.gpus = [d2l.gpu(i) for i in range(min(num_gpus, d2l.num_gpus()))] @d2l.add_to_class(d2l.Trainer) #@save def prepare_batch(self, batch): if self.gpus: batch = [a.to(self.gpus[0]) for a in batch] return batch @d2l.add_to_class(d2l.Trainer) #@save def prepare_model(self, model): model.trainer = self model.board.xlim = [0, self.max_epochs] if self.gpus: model.to(self.gpus[0]) self.model = model Короче говоря, пока все данные и параметры находятся на одном устройстве, мы можем эффективно обучать модели. В следующих главах мы рассмотрим несколько таких примеров. Итоги ----- Мы можем указывать устройства для хранения и вычислений, например центральный процессор (CPU) или графический процессор (GPU). По умолчанию данные создаются в основной памяти и затем используются процессором для вычислений. Фреймворк глубокого обучения требует, чтобы все входные данные для вычислений находились на одном устройстве, будь то процессор или один и тот же графический процессор. Бездумное перемещение данных может привести к значительной потере производительности. Типичная ошибка заключается в следующем: вычисление функции потерь для каждого мини-батча на графическом процессоре и вывод результата пользователю через командную строку (или логирование в NumPy ``ndarray``) вызывает глобальную блокировку интерпретатора, которая останавливает работу всех графических процессоров. Гораздо лучше выделять память для логирования непосредственно на графическом процессоре и перемещать только крупные блоки логов. Упражнения ---------- 1. Попробуйте выполнить более объёмную вычислительную задачу, например умножение больших матриц, и оцените разницу в скорости между процессором (CPU) и графическим процессором (GPU). А как обстоит дело с задачей, требующей небольшого количества вычислений? 2. Как следует читать и записывать параметры модели на GPU? 3. Измерьте время, необходимое для выполнения 1000 умножений матриц на матрицы размера :math:`100 \times 100`, и вычислите норму Фробениуса результирующей матрицы, записывая результаты по одному. Сравните этот подход с ведением журнала на GPU и передачей только финального результата. 4. Измерьте время, необходимое для выполнения двух операций умножения матриц на двух графических процессорах одновременно. Сравните его с вычислениями в последовательности на одном графическом процессоре. Подсказка: вы должны увидеть почти линейное масштабирование. `Обсуждения `__