Автоматическая смешанная точность с использованием PyTorch

Введение

Более крупные модели глубокогоARN требуют большей вычислительной мощи и ресурсов памяти. Быстреее обучение глубоких нейронных сетей было достигнуто за счет развития новых техник. Вместо FP32 (формата вещественных чисел с полной точностью), вы можете использовать FP16 (формат вещественных чисел с полуточностью), и исследователи обнаружили, что их сочетание является лучшим вариантом.

Смешанной точности позволяет тренироваться с полуточностью, при этом сохраняя большую часть точности сети с полной точностью. термин “смешанная точность” указывает на то, что эта стратегия использует и полную, и полуточную представления.

В этой интродукции к обучению с использованием Смешанной Точности (AMP) с PyTorch мы демонстрируем, как работает эта техника, исходя из процесса использования AMP, и обсуждаем более сложные приложения техник AMP с конструкциями кода для пользователей, чтобы впоследствии интегрировать их собственный код.

Предварительные требования

Основные понятия PyTorch: знакомство с PyTorch, включая его основные понятия, такие как тензоры, модули и цикл обучения.

Основы глубокого обучения: понятия, такие как нейронные сети, обратное распространение и оптимизация.

Знание смешанной точности обучения: Знание преимуществ и недостатков смешанной точности обучения, включая уменьшение использования памяти и ускорение вычислений.

Доступ к совместимому оборудованию: ГPU, поддерживающий смешанную точность, таких как GPU NVIDIA с Tensor Cores (например, архитектуры Volta, Turing, Ampere).

Установка Python и CUDA: Рабочий Python-环境 с установленным PyTorch и настроенным CUDA для ускорения GPU.

Общее о представлениях о смешанной точности

Как и у большинства фреймворков deep learning, PyTorch обычно тренируется с использованием 32-битных floating-pointных данных (FP32). FP32, в свою очередь, не обязательно необходим для успеха. Возможно использовать 16-битные floating-pointные для нескольких операций, где FP32 требует больше времени и памяти.

В результате этого, инженеры NVIDIA разработали технику, позволяющую выполнять смешанную точность обучения в FP32 для нескольких операций, в то время как большая часть сети работает в FP16.

  • Преобразуйте модель, чтобы использовать тип данных float16, где возможно.
  • maintenance float32 главных весов, чтобы накапливать обновления весов каждый итерация.
  • Использование увеличения потертости для сохранения маленьких значений градиента.

Мешаная точность в PyTorch

Для тренировки с мешаной точностью PyTorch уже встроенно множество функций.
Параметры модуля преобразуются в FP16, когда вы вызываете метод .half(), а данные тензора — когда вы вызываете .half(). Быстрое арифmetic FP16 будет использоваться для выполнения операций на этих модулях или тензорах. NVIDIA математические библиотеки (cuBLAS и cuDNN) хорошо поддерживаются PyTorch. Данные из ipeline FP16 обрабатываются с помощью Tensor Cores для выполнения GEMMs и конвульвенций. Чтобы использовать Tensor Cores в cuBLAS, размерности GEMM ([M, K] x [K, N] -> [M, N]) должны быть кратными 8.

Presentation of Apex

Utility Apex mixed precision is designed to increase training speed while maintaining the accuracy and stability of single-precision training. Apex can perform operations in FP16 or FP32, automatically handle master parameter conversion, and automatically scale losses.

Apex был создан для упрощения процесса включения смешанной точности обучения в модели исследователям. Amp (сокращение от Automatic Mixed-Precision) является одной из особенностей Apex, это легкая расширения PyTorch. Для пользователей достаточно добавить несколько строк в их сети, чтобы использовать смешанную точность обучения с Amp. Apex был представлен на CVPR 2018, и надо отметить, что сообщество PyTorch с момента выпуска Apex оказывает сильную поддержку этому продукту.

Для использования Amp вам нужны только небольшие изменения в рабочей модели, чтобы не обходиться сложными конверсиями при создании или запуске скрипта.При использовании PyTorch в нестандартных сценариях Amp могут понадобиться настройки, для этого предусмотрены хуки.

Amp обеспечивает все преимущества смешанной точности обучения без необходимости оперировать умножением потертости или явно управлять конверсией типов. Сайт GitHub Apex содержит инструкции по установке, а официальную документацию API можно найти здесь.

Как работает Amp

Амп использует модель с белым и чёрным списками на уровне логики. Операции тензоров в PyTorch включают функции нейронных сетей, такие как torch.nn.functional.conv2d, простые математические функции, такие как torch.log, и методы тензоров, такие как torch.Tensor. add__. В этом мире существуют три основные категории функций:

  • Белый список: Функции, которые могли бы выиграть от ускорения математики FP16. Типичными приложениями являются матричное умножение и конволюция.
  • Чёрный список: Входные данные должны быть в FP32 для функций, где 16 бит точности может быть недостаточно.
  • Все остальное (те функции, которые остались): Функции, которые могут выполняться в FP16, но трата на преобразование FP32 в FP16 для их выполнения в FP16 не оправдана, так как ускорение не является значимым.

Задача Ампа является простой, по крайней мере, в теории. Amp определяет, является ли функция PyTorch белым списком, чёрным списком или ничем между. Если функция в белом списке, все аргументы должны быть конвертированы в FP16; если в чёрном списке, все аргументы должны быть в FP32. Если она нигде не указана, просто убедитесь, что все аргументы имеют одинаковый тип. Это политика не так проста в применении, как это кажется на первый взгляд.

Использование Amp вместе с моделью PyTorch

Для включения Amp в текущий скрипт PyTorch следуйте этим шагам:

  • Используйте библиотеку Apex для импорта Amp.
  • Инициализируйте Amp, чтобы оно могло внести необходимые изменения в модель, оптимизатор и внутренние функции PyTorch.
  • Обратите внимание на места, где происходит backpropagation (.backward()), чтобы Amp мог одновременно умножать потерю и очищать состояние на каждом итерационном уровне.

Шаг 1

Для первого шага достаточно одной строки кода:

from apex import amp

Шаг 2

Для выполнения этого шага должны быть уже определены модель нейронной сети и оптимизатор, используемые для тренировки, и этот шаг также состоит из одной строки.

model, optimizer = amp.initialize(model, optimizer, opt_level="O1")

Дополнительные настройки позволяют вам гибко настроить корректировки типов тензоров и операций Amp. Функция amp.initialize() принимает множество параметров, из которых мы будем указать только три:

  • models (torch.nn.Module или список torch.nn.Modules) – Модели для изменения/преобразования.
  • оптимизаторы (необязательно, torch.optim.Optimizer или список torch.optim.Optimizers) – Оптимизаторы для изменения/преобразования. Обязательно для обучения, не обязательно для идентификации.
  • opt_level (строка, необязательно, по умолчанию=“O1”) – Уровень чистой или смешанной точности оптимизации. Приемлемые значения: “O0”, “O1”, “O2” и “O3”, описаны в detail выше. Есть четыре уровня оптимизации:

O0 для FP32 тренировки: Это не операция. Необходимость в этом нет, так как ваша исходная модель должна быть уже в FP32, и O0 может помочь установить базовую точность.

O1 для Смешанной точности (рекомендуется для типичного использования): Изменяет все методы Tensor и Torch, используя схему входного преобразования с белым и черным списками. В FP16 белый список операций, таких как Tensor Core-friendly операции, такие как GEMMs и convolutions, выполняются. Softmax, например, является операцией черного списка, требующей FP32 точности.除非另外说明,否则O1 также использует динамическую установку масштаба потертости.

O2 для ” nearly FP16″ Смешанной точности: O2 преобразует веса модели в FP16, исправляет метод forward модели для преобразования данных входа в FP16, сохраняет batchnorms в FP32, поддерживает FP32 главные веса, обновляет param_groups оптимизатора, чтобы операция optimizer.step() действовала непосредственно на FP32 веса и реализует динамическую установку масштаба потертости (если она переопределена). Unlike O1, O2 не исправляет Torch функции или методы Tensor.

O3 для тренировки с FP16: O3, возможно, не так устойчив, как O1 и O2, в отношении истинного смешанного прецизии. В результате, может быть выгодно установить базовую скорость для вашей модели, против которой эффективность O1 и O2 может быть оценена.
额外的属性覆盖 keep_batchnorm_fp32=True в O3 может помочь вам определить “скорость света”, если ваша модель использует батч нормализацию, включая cudnn батч нормализацию.

O0 и O3 не являются истинным смешанным значением, но они помогают установить базовые значения для точности и скорости, соответственно. ИмPLEMENTACIJN смешанного значения определяется как O1 и O2.
Вы можете попробовать оба варианта и увидеть, который более эффективен и точнее для вашей конкретной модели.

Шаг 3

Убедитесь, что вы идентифицировали место, где происходит обратный прогон в вашем коде.
Вот несколько строк кода, которые выглядят как это:

loss = criterion(…)
loss.backward()
optimizer.step()

Шаг 4

Utilizing the Amp context manager, you can enable loss scaling by simply wrapping the backward pass:

loss = criterion(…)
with amp.scale_loss(loss, optimizer) as scaled_loss:
    scaled_loss.backward()
optimizer.step()

Вот все. Теперь вы можете запустить скрипт с включенной тренировкой смешанной точности.

捕捉函数调用

PyTorch не имеет статического модельного объекта или графа, на который можно закрепиться и добавить каста, упомянутый выше, поскольку он является таким гибким и динамичным. Используя призвук “надстройка устаревших функций“, Amp может динамически преобразовать параметры.

К примеру, вы можете использовать следующий код, чтобы убедиться, что аргументы метода torch.nn.functional.linear всегда преобразуются в fp16:

orig_linear = torch.nn.functional.linear
def wrapped_linear(*args):
 casted_args = []
  for arg in args:
    if torch.is_tensor(arg) and torch.is_floating_point(arg):
      casted_args.append(torch.cast(arg, torch.float16))
    else:
      casted_args.append(arg)
  return orig_linear(*casted_args)
torch.nn.functional.linear = wrapped_linear

尽管Amp может добавить усовершенствования, чтобы сделать код более устойчивым, вызов Amp.init() эффективно позволяет внедрять настройки устаревших функций в все соответствующие функции PyTorch, чтобы аргументы были правильно преобразованы в runtime.

Minimizing Casts

Каждая масса преобразуется только один раз от FP32 в FP16 на каждом итерации, поскольку Amp содержит внутреннюю кэш-таблицу всех преобразований параметров и использует их по мере необходимости. В каждом итерации контекстный менеджер для обратного прохода информирует Amp о том, когда необходимо очистить кэш.

Автокаста и учитывание градиентов с использованием PyTorch

“Автоматизированное тренирование смешанной точности” означает комбинацию torch.cuda.amp.autocast и torch.cuda.amp.GradScaler. Используя torch.cuda.amp.autocast, можно установить автоматическую кастацию только для определенных областей. Автоматическая кастация автоматически выбирает точность для операций GPU, чтобы оптимизировать эффективность, сохраняя при этом точность.

Инстанции torch.cuda.amp.GradScaler simplify the gradient scaling steps. Gradient scaling reduces gradient underflow, which helps networks with float16 gradients achieve better convergence.

Вот пример кода, показывающий, как использовать autocast() для получения автоматической смешанной точности в PyTorch:

# Создает модель и оптимизатор с DEFAULT точностью
model = Net().cuda()
optimizer = optim.SGD(model.parameters(), ...)

# Создает GradScaler раз once на начале тренировки.
scaler = GradScaler()

for epoch in epochs:
    for input, target in data:
        optimizer.zero_grad()

        # Запускает forward пропуск с autocasting.
        with autocast(device_type='cuda', dtype=torch.float16):
            output = model(input)
            loss = loss_fn(output, target)

        # Ops обратного хода выполняются в той же dtype, что и выбрана autocast для соответствующих ops передвижения.
        scaler.scale(loss).backward()

        # scaler.step() first деDesнкалирует градиенты оптимизатора для assigned параметров.
   
        scaler.step(optimizer)

        # обновляет масштаб для следующего итерации.
        scaler.update()

Если передача данных для одного определенного оператора выполнена с использованием float16, то обратный проход для этого оператора будет генерировать float16 градиенты, и float16 может не быть в состоянии представить градиенты с малыми магнитудами.

Обновления связанных параметров будут потеряны, если эти значения очищены до нуля («недолив»).

Масштабирование градиента является техникой, использующей коэффициент масштабирования для умножения потертостей сети и затем выполняющей обратный проход по увеличенной потертости, чтобы избегать недолива. Также необходимо умножить обратно идущие градиенты через сеть этим же коэффициентом. Consequently, значения градиентов имеют большую магнитуду, что предотвращает их очистку до нуля.

Перед обновлением параметров для каждого параметра должны быть де-масштабированы градиенты (.grad атрибут), чтобы коэффициент масштабирования не влиял на учебную скорость. Both autocast и GradScaler могут использоваться независимо, так как они реализованы как модули.

работа с немасштабированными градиентами

Комprimirovaniye gradienov

Мы можем увеличить все градиенты использованием метода Scaler.scale(Loss).backward().Property .grad параметров между backward() и scaler.step(optimizer) должны быть де-экспонентированы перед их изменением или проверкой. Если вы хотите ограничить глобальную норму (см. torch.nn.utils.clip_grad_norm_()) или максимальную величину (см. torch.nn.utils.clip_grad_value_()) нашего набора градиентов меньше или равно определенному значению (какое-то пользовательское ограничение), вы можете использовать технику, называемую “ограничением градиентов”.

Ограничение без де-экспоненциации приведет к тому, что норма/максимальная величина градиентов будет умножена, тем самым дезактивируя просящую вас отметку (которая должна была быть границей для неразкрашенных градиентов). Градиенты, содержащиеся в указаных параметрах оптимизатора, де-экспоненциации подвергаются с помощью scaler.unscale (optimizer).
Вы можете де-экспоненциацию градиентов других параметров, которые ранее были переданы другому оптимизатору (например, оптимизатору1), используя scaler.unscale (optimizer1). Мы можем иллюстрировать этот принцип, добавив две строки кода:

# Де-экспоненциация градиентов параметров оптимизатора в месте
        scaler.unscale_(optimizer)
# Т.к. градиенты параметров оптимизатора уже де-экспоненциации, обычное ограничение: 
        torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm)

Работа с умноженными градиентами

Собирание градиентов

Собирание градиентов основано на абсурдно простой концепции. Вместо обновления параметров модели, оно ждет и собирает градиенты между последовательными batches, чтобы вычислить потерю и градиент.

После определенного количества batches параметры обновляются в зависимости от скользящих градиентов. Вот кусочек кода о том, как использовать умножение градиентов с pytorch:

scaler = GradScaler()

for epoch in epochs:
    for i, (input, target) in enumerate(data):
        with autocast():
            output = model(input)
            loss = loss_fn(output, target)
            # нормализуем потерю 
            loss = loss / iters_to_accumulate

        # Суммирует умноженные градиенты.
        scaler.scale(loss).backward()
          # обновление весов
        if (i + 1) % iters_to_accumulate == 0:
            # можете разностить_ здесь, если требуется 
            scaler.step(optimizer)
            scaler.update()
            optimizer.zero_grad()
  • Собирание градиентов добавляет градиенты за адекватный размер batch batch_per_iter * iters_to_accumulate.
    Скорость должна быть калибрирована для эффективного batch; это означает проверку на неограниченные/NaN оценки, пропуск шага, если обнаружены какие-либо неограниченные/NaN, и обновление скорости до точности эффективного batch.
    Также важно сохранять градиенты с умноженными и постоянными коэффициентами скольжения, когда градиенты для определенного эффективного batch добавляются вместе.

Если градиенты еще не умножены (или коэффициент умножения изменится) до завершения накопления, следующий обратный прогон добавит умноженные градиенты к неумноженным (или градиентам, умноженным на иную сталь) после чего невозможно восстановить неумноженные накопленные градиенты шага, который должен быть применен.

  • Вы можете деумножить градиенты, используя unscale вскоре до шага, после того, как все умноженные градиенты для последующего шага были накоплены.
    Чтобы обеспечить полностью эффективный batch, просто вызывайте update в конце каждого итерационного цикла, где ранее вызывали step
  • enumerate(data) функцию позволяет нам сохранять трек на индекс batch, во время итерации по данным.
  • Разделите текущий ущерб на iters_to_accumulate(loss / iters_to_accumulate). Это уменьшает вклад каждой мини-пачки, которую мы обрабатываем, normalizing ущерб. Если вы среднее значение ущерба внутри каждой пачки, деление уже правильно и дополнительной нормализации не требуется. Этот шаг может быть не необходимым в зависимости от того, как вы вычисляете ущерб.
  • Когда мы используем scaler.scale(loss).backward(), PyTorch накапливает умноженные градиенты и сохраняет их до тех пор, пока мы не вызовем optimizer.zero_grad().

Штраф по градиентам

При реализации штрафа по градиентам используется torch.autograd.grad(), чтобы построить градиенты, которые комбинируются в значение штрафа, а затем добавляются к потере. Л2-штраф без умножения или автоCAST-а приведен в примере ниже.

for epoch in epochs:
    for input, target in data:
        optimizer.zero_grad()
        output = model(input)
        loss = loss_fn(output, target)

        # Создает градиенты
        grad_prams = torch.autograd.grad(outputs=loss,
                                          inputs=model.parameters(),
                                          create_graph=True)

        # вычисляет оператор штрафа и добавляет его к потере
        grad_norm = 0
        for grad in grad_prams:
            grad_norm += grad.pow(2).sum()
        grad_norm = grad_norm.sqrt()
        loss = loss + grad_norm

        loss.backward()

        # Вы можете здесь кlip градиенты

        optimizer.step()

Тензоры, предоставленные torch.autograd.grad(), должны быть умножены для реализации штрафа по градиентам. Необходимо размаслить градиенты перед их комбинацией для получения значения штрафа. Так как вычисление оператора штрафа является частью предварительного прохода, оно должно происходить внутри контекста автоCAST.
Для того же L2-штрафа выглядит это thus:

scaler = GradScaler()

for epoch in epochs:
    for input, target in data:
        optimizer.zero_grad()
        with autocast():
            output = model(input)
            loss = loss_fn(output, target)

        # Выполняем умножение потерь для обратного прохода autograd.grad, #resulting #scaled_grad_prams
        scaled_grad_prams = torch.autograd.grad(outputs=scaler.scale(loss),
                                                 inputs=model.parameters(),
                                                 create_graph=True)

        # Создаем grad_prams перед вычислением штрафа (grad_prams должен быть #unscaled). 
        # Так как никакий оптимизатор не владеет scaled_grad_prams, используется обычное деление #вместо scaler.unscale_:
        inv_scaled = 1./scaler.get_scale()
        grad_prams = [p * inv_scaled for p in scaled_grad_prams]

        # Вычисляется и добавляется штрафной терм к потере. 
        with autocast():
            grad_norm = 0
            for grad in grad_prams:
                grad_norm += grad.pow(2).sum()
            grad_norm = grad_norm.sqrt()
            loss = loss + grad_norm

        # Применяется умножение для вызова обратного прохода.
        # Аккумулируются правильно умноженные листья градиентов.
        scaler.scale(loss).backward()

        # Тут можно выполнить unscale_ 

        # step() и update() продолжаются как обычно.
        scaler.step(optimizer)
        scaler.update()

Работа с несколькими моделями, потерями и оптимизаторами

Скаляр.масштаб должен вызываться на каждом убытке в вашей сети, если у вас много из них.
Если у вас много оптимизаторов в сети, вы можете выполнить скаляр.dezкалиibr на любом из них, и вам следует вызывать скаляр.шаг на каждом из них.然而, скаляр.обновление должен использоваться только один раз, после выполнения шагов всех оптимизаторов, используемых в этой итерации:

scaler = torch.cuda.amp.GradScaler()

for epoch in epochs:
    for input, target in data:
        optimizer1.zero_grad()
        optimizer2.zero_grad()
        with autocast():
            output1 = model1(input)
            output2 = model2(input)
            loss1 = loss_fn(2 * output1 + 3 * output2, target)
            loss2 = loss_fn(3 * output1 - 5 * output2, target)

       #尽管保留图形与amp无关,但它出现在这个示例中,因为两个backward()调用共享某些图区域。 
        scaler.scale(loss1).backward(retain_graph=True)
        scaler.scale(loss2).backward()

        #如果您希望查看或调整它们所拥有的参数的梯度,您可以指定哪些优化器需要显式dezкалиibr。.
        scaler.unscale_(optimizer1)

        scaler.step(optimizer1)
        scaler.step(optimizer2)

        scaler.update()

Каждый оптимизатор проверяет свои градиенты на Inf/NaN и делает индивидуальный выбор о том, следует ли пропустить шаг.某些 оптимизаторы могут пропустить шаг, в то время как другие могут не делать этого.Пропуск шага происходит раз в несколько сотен итераций; поэтому он не должен повлиять на конvergence. для моделей с несколькими оптимизаторами, вы можете сообщить о проблеме, если после добавления градиентного масштабирования увидите плохую конvergence.

Работа с несколькими GPU

Одна из основных проблем с моделями глубокого обучения заключается в том, что они становятся слишком крупными, чтобы обучаться на одном GPU. Train a model on a single GPU can take too long, and Multi-GPU training is required to get models ready as quickly as possible. A well-known researcher was able to shorten the ImageNet training period from two weeks to 18 minutes or train the most extensive and most advanced Transformer-XL in two weeks instead of four years.

DataParallel и DistributedDataParallel

Без посредственности в качестве, PyTorch предлагает лучшую комбинацию удобства использования и контроля. nn.DataParallel и nn.parallel.DistributedDataParallel являются двумя функциями PyTorch для распределения тренировки по нескольким GPU. Вы можете использовать这些 легкие в использование обертки и изменения, чтобы обучать сеть на нескольких GPU.

DataParallel в单个进程

На одном компьютере DataParallel помогает распределить тренировку по многим GPU.
Посмотрим ближе на то, как DataParallel на самом деле работает в практике.
при использовании DataParallel для обучения нейронной сети, происходят следующие стадии:

Источник:

  • Мини-бbatch разбивается на GPU:0.
  • Разбиение и распределение мини-бcatch по всем доступным GPU.
  • Копирование модели на GPU.
  • Проход вперёд происходит на всех GPU.
  • Расчитывать потерю в отношении выходов сети на GPU:0, а также возвращать потери на различных GPU. Gradients должны быть вычислены на каждом GPU.
  • Суммирование градиентов на GPU:0 и применение оптимизатора для обновления модели.

Обратите внимание, что вопросы, которые обсуждаются здесь, относятся исключительно к autocast. поведение GradScaler остается неизменным. Не важно, создает torch.nn.DataParallel потоки для каждого устройства для выполнения предварительного прохода. Состояние autocast сообщается в каждом из них, и следующий код будет работать:

model = Model_m()
p_model = nn.DataParallel(model)

# Устанавливает autocast в основном потоке
with autocast():
    # Будет autocasting в p_model. 
    output = p_model(input)
    # loss_fn также autocast
    loss = loss_fn(output)

DistributedDataParallel, один GPU на процесс

Документация по torch.nn.parallel.DistributedDataParallel рекомендует использовать один GPU на процесс для максимальной производительности. В этом случае DistributedDataParallel внутри не запускает нити;鉴于此, использование autocast и GradScaler не затрагивается.

DistributedDataParallel, несколько GPU на процесс

В этом месте torch.nn.parallel.DistributedDataParallel может запустить вспомогательную нить, чтобы выполнять фронт-проход на каждом устройстве, подобно torch.nn.DataParallel.修正方法 те же самые: применять autocast в качестве части метода forward вашей модели, чтобы убедиться, что он включен в вспомогательных нитях.

Заключение

В этой статье мы рассмотрели:

  • Узнали о Apex.
  • Узнали, как работает Amps.
  • Узнали, как выполнять масштабирование градиентов, обрезку градиентов, накопление градиентов и штраф по градиентам.
  • Узнали, как работать с несколькими моделями, потерями и оптимизаторами.
  • Узнали, как выполнять DataParallel в одиночном процессе, когда работаем с несколькими GPU.

Ссылки

https://developer.nvidia.com/blog/apex-pytorch-easy-mixed-precision-training/
https://nvidia.github.io/apex/amp.html
https://discuss.pytorch.org/t/accumulating-gradients/30020
https://towardsdatascience.com/how-to-scale-training-on-multiple-gpus-dae1041f49d2

Source:
https://www.digitalocean.com/community/tutorials/automatic-mixed-precision-using-pytorch