:В данной работе в качестве тестового оператора используется GreaterEqual.Логика расчета этого оператора относительно проста (выход = ввод1 >=ввод2), что направлено на максимальное сокращение времени расчета возможно, и сделать оператора как можно более трудоемким. В основном сосредоточиться на операциях с данными и планировании оператора.
Эта статья опубликована в сообществе HUAWEI CLOUD.«Трудоемкий анализ и оптимизация оператора CANN AICPU», Автор: DavilSu.
1. Цель анализа
В процессе собственно разработки CANN-операторов часто бывает, что оператор нормально функционирует, но производительность намного ниже, чем у оператора-аналога TensorFlow. В ответ на эту проблему в этой статье в качестве тестового оператора используется GreaterEqual.Логика расчета оператора относительно проста (выход = ввод1 >= ввод2), что направлено на максимально возможное сокращение времени расчета, так что трудоемкий оператор операция основана на операциях с данными и операторах в максимально возможной степени Планирование как предмет.
2. Тестовый код и знакомство с платформой
Эта тестовая платформа представляет собой сервер Ascend, предоставленный OpenLab, оснащенный Ascend910A, а номер версии CANN Toolkit — 5.0.2alpha005.
Самостоятельно разработанный тестовый код изменен со ссылкой на фиксацию cac625f243dfe7b04dbb2a82059cd0e4349f77d1, которая оптимизирована для производительности широковещательной операции. Параллельный порог устанавливается собственной разработкой: 8K с вещанием и 32K без вещания.
Оператор бенчмаркинга TensorFlow от GreaterEqual — это оператор версии TensorFlow1.15, а коммит оператора бенчмаркинга canndev — d660e086717b94b8cfb3f35a8e08046ca0461772.Эта версия оператора пытается использовать широковещательную операцию библиотеки Eigen, чтобы избежать проблемы недостаточной производительности Bcast хранилища исходного кода canndev. , но параллельные вычисления не включены.
Для тестовых данных я настроил два пакета данных, включающих операции вещания и те, которые не включают операции вещания.Тестовые данные, включающие операции вещания, делятся на два типа: количество элементов, которые должны быть переданы Tensor, равно 1, а количество элементов не 1, а int8, Int16, int32, int64, uint8, float16, float32, float64, всего 8 типов данных, поддерживаемых операторами тестов TensorFlow, для каждого типа данных установлено значение 128B, 256B, 1K, 2K, 4K, 8K , 16K, 32K, 64K, 128K, 256K, 1M, 2M, 8M, всего 14 градиентов шкалы данных.Подробная шкала данных и соответствие формы следующие:
3. Анализ производительности одного потока
Эта часть направлена на проверку разрыва в производительности между однопоточными операторами CANN и операторами TensorFlow. Чтобы избежать влияния широковещательной операции на результаты теста, для этих тестовых данных используются пакеты данных, которые не включают широковещательную операцию.
Рис. 1. Соотношение затрат времени на один поток
Можно видеть, что Оператор Cann имеет определенное преимущество в производительности по сравнению с Tensorflow для небольших размеров данных с объемом данных менее 2k, но с увеличением объема данных, производительность производительности Can Canterious, особенно ухудшится, особенно для типа данных Uint8, Степень ухудшения очень серьезная, ухудшение производительности составляет до 6,57 раз. Для стандартного типа Non-C ++ типа FLOAT16 оба они заменяются половинным типом данных в библиотеке собственных данных, а производительность результатов теста относительно близко.
Рис. 2. Требующий много времени расчет данных размером 1 КБ
Я также проверил время, необходимое для вычисления данных 1K, когда одноядерные CANN и TF вычисляют объем данных 16K-8M.
Видно, что по мере увеличения пространства, занимаемого типом данных, пропорционально увеличивается и время, затрачиваемое TensorFlow. Странно то, что int8 и uint8 CANN по времени аналогичны int16.Эта особенность также отражается в соотношении затрат времени int8 и uint8.Степень деградации производительности намного выше, чем у других типов данных.Предполагается, что это может быть потому, что int8 и uint8 расширены до 16 бит, а затем выполняются вычисления. Производительность CANN в двух типах данных float32 и float64 тоже очень странная.По мере увеличения объема данных потребление времени сильно колеблется. Конкретная ситуация была проанализирована и оптимизирована в разделе анализа векторизованного кода и производительности.
4. Сравнение производительности самостоятельно разработанного оператора и внедренного оператора на основном складе
Оператор GreaterEqual в основном хранилище Canndev пытается использовать широковещательную операцию библиотеки Eigen, чтобы избежать проблемы недостаточной производительности широковещания в исходном хранилище canndev, но параллельные вычисления не включены для ускорения. Самостоятельно разработанный оператор использует класс Bcast в хранилище canndev для вещания, уточнения и специализации, требуется ли вещание, и устанавливает параллельные пороги для разных масштабов данных.
В этой части тестируются два пакета данных, включающие операции широковещательной передачи и те, которые не включают операции широковещательной передачи, с целью проверки производительности метода, предоставляемого canndev, и операции широковещательной передачи, предоставляемой Eigen, а также преимуществ производительности операторов собственной разработки.
Рис. 3. Соотношение затрат времени без широковещательной передачи
Рис. 4. Соотношение времени выполнения операций, включая трансляцию
Из результатов видно, что когда широковещательная операция не включена, производительность самостоятельно разработанного оператора лучше, чем у существующего оператора.Когда объем данных мал, поскольку указатель управляется напрямую, он не проверяется широковещательным методом Эйгена с существующим оператором.Для обработки производительность имеет определенные преимущества.Благодаря включенной многопоточности для больших объемов данных производительность намного лучше, чем у существующих операторов.
Однако после включения широковещательной операции, поскольку порог параллельной обработки установлен на 8K, небольшой объем данных такой же, как и при однопоточной обработке данных.Можно видеть, что текущая производительность Bcast CANN уступает реализованы Eigen. Преимущества, производительность собственных операторов значительно превышает производительность существующих операторов.
По сравнению с широковещанием, реализованным Eigen, и Bcast, реализованным CANN, широковещательная операция, реализованная TensorFlow, имеет большие преимущества в производительности, а также в 8-26 раз опережает широковещательную передачу, реализованную Eigen для одного потока, и даже больше опережает CANN.
5. Параллельное сравнение порогов
Поскольку эталонным оператором является оператор Less, оптимизированный для широковещательной передачи, я настроил контрольную группу с тем же порогом, что и оператор Less (2K с широковещательной операцией и 7K без широковещательной операции), чтобы проверить его параллельный порог. Является ли он разумным. Чтобы избежать влияния широковещательной операции на результаты теста, для этих тестовых данных используются пакеты данных, которые не включают широковещательную операцию.
Результаты теста следующие:
Рис. 5. Меньший порог оператора и самостоятельно разработанный порог оператора, коэффициент затрат времени.
Видно, что параллельная установка порога оператора Less нецелесообразна.Очевидно, что увеличение масштаба данных 8K занимает много времени.Основная часть затрат времени - это затраты времени на параллельную связь, а не на расчет. Саморазработанный оператор относительно плоский, а порог определяется методом дихотомии, петлевой тест показывает, что критическая точка параллельного ускорения близка к 1.
6. Векторизованный код и анализ производительности
При анализе производительности в однопоточном режиме я заметил очень странное явление. Потребление времени int8 и int16 очень близко (как показано на рисунке 2), что и привлекло мое внимание. -точечный номер, разрядность данных, инструкции по обработке вызова данных и т. д. При обработке одинакового количества данных int8 и int16 он должен занимать int16 выше, чем int8. Наблюдая за временем выполнения операторов TensorFlow, int8 и uint8 занимают меньше времени, чем int16.
Современные процессоры часто поддерживают SIMD (Single Instruction Stream Multiple Data Streams).За счет упаковки данных в векторный регистр и выполнения нескольких вычислений данных в одной рабочей инструкции реализуется DLP (параллелизм на уровне данных) и ускоряются операции, интенсивно использующие данные. Однако процесс расчета оператора GreaterEqual не включает в себя структуру выбора ветвей, а логика расчета проста и повторяема, что подходит для ускорения с помощью SIMD.
Согласно данным, AICPU в процессоре Ascend910 представляет собой ядро TaiShan с ядрами 16. Через системный запрос он поддерживает набор инструкций AArch64, который также включает набор инструкций NEON.
Я попытался внедрить ассемблерный код в код реализации C++, чтобы добиться ручной векторизации, и производительность действительно значительно улучшилась. Хотя ручная векторизация теоретически может обеспечить наивысшую степень векторизации, из-за различных расширенных наборов инструкций SIMD, предоставляемых разными процессорами, а также сложных и изменчивых характеристик различных приложений читабельность векторизованного кода SIMD плохая, а степень портирования низкий, и его трудно продолжать оптимизировать. Учитывая, что в будущем может потребоваться перенос кода оператора на ЦП с другими архитектурами, такими как x86-64 и ARM, компилятор, наконец, выбран для автоматического создания векторной программы для SIMD-расширения целевого процессора. Программистам автоматической векторизации не нужно заботиться о структуре и наборе команд компонентов расширения SIMD, предоставляемых нижним уровнем, а нужно лишь четко выразить существующий в программе параллелизм, что во многом решает проблему низкой переносимости высокопроизводительного кода. .
Запросите содержимое кода основного склада canndev. Ключевые слова, связанные с оптимизацией векторизации, отображаются только в TFPlugin. Проверьте параметры компиляции CmakeLists.txt только для оптимизации O2. Поскольку компилятором для компиляции кода AICPU является GCC, согласно документации GCC, параметры компиляции, включенные в O2, включают следующие параметры в дополнение к параметрам оптимизации O1:
Видно, что таблица 3 не включает опции компиляции для оптимизации векторизации, поэтому мы добавляем опцию компиляции -ftree-vectorize (включая -ftree-loop-vectorize и -ftree-slp-vectorize) в CmakeLists.txt для автоматического векторизация включена, результаты оптимизации следующие:
Рис. 6. Затраты времени на однопоточный векторизованный расчет данных размером 1 КБ
Наблюдая за результатами на рисунке 6, можно увидеть, что производительность кода, оптимизированного с помощью однопоточной векторизации, значительно улучшилась. В то же время мы также можем наблюдать, что время вычисления чисел с фиксированной запятой или чисел с плавающей запятой одного и того же типа символа увеличивается пропорционально удвоению разрядности данных, что также соответствует фиксированной длине векторного регистра. модуля расширения SIMD, NEON. Длина векторного регистра составляет 128 бит, поэтому мы устанавливаем порог параллельности, который не должен рассчитываться в соответствии с количеством элементов, а должен определяться в соответствии с общим размером данных элемента.
Рисунок 7. Соотношение затрат времени на то, открывает ли FP16 временные переменные или нет
Попробуйте преобразовать половину данных в тензоре в число с плавающей запятой и сохранить его во временно открытом массиве с плавающей запятой, но производительность ухудшится, потому что стоимость присваивания после поэлементного преобразования типа данных намного больше, чем улучшение производительности принесенная векторизацией.
Рис. 8. Соотношение времени однопоточной векторизации или нет
Рис. 9. Соотношение затрат времени на многопоточную векторизацию по сравнению с многопоточной векторизацией.
Как видно из рисунка 9, после векторизации производительность всех нативных типов данных C++ лучше, чем у операторов TensorFlow.
Обратите внимание на рисунок 10. После оптимизации векторизации производительность оператора эффективно улучшилась, но мы видим, что некоторые типы данных работают хуже, когда объем данных составляет 128 КБ. Установите в соответствии с размером данных, здесь вы можете установить более мелкозернистый параллелизм пороги для разных типов данных.
Рис. 10. Соотношение времени векторизации с трансляцией или без нее (количество элементов Tensor для трансляции равно 1)
Я также проверил частный случай одноэлементной операции вещания после оптимизации векторизации, видно, что поскольку операция вещания не вызывается, а разыменовывается непосредственно одноэлементный указатель, компилятор может правильно реализовать оптимизацию векторизации для этой ситуации. Таким образом, производительность также значительно улучшилась.
Но к сожалению, когда требуется широковещательная операция, для доступа к элементам в Тензоре необходимо вызывать методы GetBroadcastXIndex и GetBroadcastYIndex класса Bcast для вычисления смещения адреса после широковещательной операции, включающей в себя более сложные вычисления, и компилятор не может вычислить его.Для оптимизации векторизации накладные расходы на открытие временного пространства и присвоение значений намного больше, чем улучшение производительности, вызванное векторизацией, поэтому, как оптимизировать этот процесс, еще предстоит изучить.
Рис. 11. Сравнение Open-FTree-Vectorize
Из рисунка 11 видно, что после включения опции компиляции -ftree-vectorize компилятор не только выполняет автоматическую SIMD-оптимизацию, но также выполняет операцию развертывания цикла, что способствует уменьшению издержек цикла, обеспечивая уровень команд. параллелизм и оптимизация планирования конвейеров инструкций.
Для типа данных float16, читая исходный код версии 3.3.9 библиотеки Eigen, мы можем видеть, что когда вычислительным устройством является ЦП, большинство вычислений (кроме оператора/) преобразуются в число с плавающей запятой, а затем вычисляются, и наконец, результат вычисления преобразуется в тип данных половинного значения. Фрагмент кода выглядит следующим образом:
Рис. 12. Определение функции operator>= с половинным типом данных в библиотеке Eigen
Эта реализация включает в себя два преобразования типов данных, и, поскольку она не вызывает собственный тип данных ARM, ее нельзя оптимизировать SIMD, она не способствует разворачиванию цикла, а фактическая эффективность вычислений намного ниже, чем у других собственных типов данных.
Рис. 13 Дизассемблированный код, GCC11.1 слева, Clang9.0.0 справа
Проконсультировавшись с официальной документацией по архитектуре ARM, я обнаружил, что Armv8.2-A включает в себя инструкции с плавающей запятой половинной точности, что позволяет избежать необходимости преобразования в и из операций с плавающей запятой одинарной точности, что приводит к повышению производительности кода. . Это означает, что AICPU может вызывать тип данных __fp16 для обеспечения встроенной поддержки вычислений с плавающей запятой половинной точности. Конечно, текущая поддержка FP16 компилятором GCC уступает поддержке Clang. В настоящее время он может оптимизировать только операторы, операции которых, такие как Add, в основном аналогичны инструкциям набора инструкций.Для оператора GreaterEqual GCC = 9.0.0 может генерировать соответствующий код набора инструкций SIMD половинной точности с плавающей запятой.
Однако __fp16 является расширением языка ARM C, на платформе X86-64, для FP16 поддерживается только первичное хранилище, вычисления должны преобразовать его в число с плавающей запятой, GCC7.3 не может компилироваться, CLANG может компилироваться. Для обеспечения переносимости кода использовать этот тип данных не рекомендуется.
Нет высокой переносимости, высокопроизводительных реализаций? Когда я прочитал журнал обновлений Eigen, найденный в выпуске обновления Eigen3.4-rc1 от 2021/04/19, Eigen :: наполовину достиг собственной поддержки ARM __fp16 и улучшил количественную оценку всей внутренней поддержки и матрицы планирования ARM NEON вычисление набора инструкций.
Рис. 14 Журнал обновлений Eigen
Рис. 15. Определение Eigen3.4.0 Half.h Eigen::half для архитектуры ARM64
Рисунок 16. Добавление дизассемблируемого кода оператора (__fp16 слева, Eigen::half версии 3.4.0 посередине, Eigen::half версии 3.3.9 справа)
Наблюдая за кодом дизассемблирования на рисунке 16, можно увидеть, что компилятор успешно вызвал набор инструкций SIMD для fp16. Код, сгенерированный Eigen::half, в основном такой же, как __fp16. По сравнению с набором инструкций SIMD, который не вызывается, а родной fp16 не включен. Код более эффективен, не только устраняет два преобразования типов, но и увеличивает количество вычисляемых данных в одном цикле (SIMD вычисляет 8 данных fp16 за раз, даже если SIMD-инструкция не включена, даже если выполняется развертывание цикла, его можно использовать только в одном цикле.Вычислить 4 данных, а объем инструкций намного больше, чем в оптимизированной версии).
Поскольку личное знакомство с исходным кодом друзей выше, чем у TensorFlow, в качестве объекта сравнения выбран PyTorch.Они сделали некоторую ручную оптимизацию на SIMD.Например, в каталоге aten/src/ATen/cpu/vec, Векторизованный класс и ряд часто используемых вычислительных функций в определенной степени позволяют избежать снижения читабельности кода, вызванного встраиванием SIMD-функций в файл реализации.В то же время ряд определений макросов среды используется для определения целевую архитектуру ЦП, включить SIMD-функции соответствующей архитектуры и далее на основе автоматической векторизации Оптимизировать фактическую производительность векторизации.
Рис. 17. Файлы в каталоге PyTorch aten/src/ATen/cpu/vec/vec256
7. Ограничения векторизации
Конечно, включение векторизации — это прекрасно? Конечно нет, векторизация имеет определенные ограничения.
1. Длина векторного регистра существующих компонентов расширения SIMD фиксирована.Если длина векторного регистра слишком велика, а количество итераций цикла или количество изоморфных операторов в базовом блоке мало, программа не может быть векторизованный.
2. SIMD оказывает большое влияние на эффективность выполнения независимо от того, является ли адрес данных непрерывным или нет.Когда адрес доступа к памяти не находится на выровненной границе, требуются дополнительные операции сдвига и слияния для получения векторных данных, соответствующих требованиям. Невыровненная структура доступа к памяти не только добавляет дополнительные операции доступа к памяти, но также добавляет специальные операции (такие как операции сдвига и слияния и т. д.) для получения векторных данных, отвечающих требованиям компонентов расширения SIMD. Поскольку логические адреса тензорных данных выровнены, эта проблема не оказывает большого влияния на поэлементные операторы.
3. Некоторым программам требуется недостаточная SIMD-векторизация из-за недостаточного количества итераций или недостаточного количества векторных параллельных операторов в базовом блоке для обеспечения достаточного параллелизма для векторных регистров.
4. Добавить SIMD-инструкции путем встраивания рукописного ассемблерного кода или внутренних функций, предоставляемых компилятором, в код реализации оператора.Теоретически, ручная векторизация может обеспечить наивысшую степень векторизации, но из-за расширенных SIMD-инструкций, предоставляемых разными процессорами.Наборы различаются , что приводит к значительной потере переносимости кода и трудностям в дальнейшей оптимизации. Однако в настоящее время автовекторизация имеет определенные ограничения на оптимизацию кода.
5. Развертывание цикла приведет к некоторому раздуванию кода.
6. Неоновая расширенная плавающая точка с плавающей запятой. В варианте оптимизации опции Compile. некоторые небезопасные расчеты с плавающей точкой для неонового кода не реализуются компилятором GCC в автоматической векторизации, что дополнительно ограничивает производительность SIMD ARM.
8. Резюме и предложения по оптимизации
Суммировать
1. Согласно текущим параметрам компиляции репозитория исходного кода canndev, производительность различных типов данных имеет большой разрыв в производительности с TensorFlow, когда размер данных превышает 4 КБ, а затраты времени int8 и uint8 являются ненормальными, поэтому это возможно вычислить и обработать согласно 16bit. Для обработки Float16 и canndev, и TensorFlow используют половину библиотеки Eigen, разрыв в производительности наименьший среди всех типов данных, но коэффициент разрыва все еще достигает 1,3x.
2. В настоящее время оператор GreaterEqual в хранилище исходного кода canndev не включает многоядерность и не специализируется на ситуации, не требующей вещания, поэтому производительность оператора без вещания намного ниже, чем у самого -развитый оператор. Когда речь идет о неодноэлементных широковещательных операциях, поскольку производительность широковещательной рассылки библиотеки Eigen лучше, чем Bcast от canndev, производительность оператора GreaterEqual в хранилище исходного кода canndev с малым объемом данных лучше, чем у саморассылаемой библиотеки. разработанный оператор После многоядерности производительность самостоятельно разработанного оператора превышает производительность оператора хранилища исходного кода.
3. Самостоятельно разработанный оператор разработан со ссылкой на оператор Less в хранилище исходного кода Логика расчета двух операторов в основном одинакова, но параллельный порог конструкции оператора Less относительно низок, что приводит к очевидному проблема для всех типов данных при размере данных 8К.Времязатратный пик, ситуация улучшилась после смещения порога параллельности назад.
4. В настоящее время автовекторизация не включена в опциях компиляции главного бина canndev.Производительность кода, который можно правильно векторизовать после включения автовекторизации, значительно улучшена, а точность вычислений не появляется при включении -funsafe Опция компиляции -math-optimizations не включена очевидное изменение.
5. Векторизация кода оператора исследуется с точки зрения инструкций по ассемблеру Половинный тип данных Eigen
Предложения по оптимизации
1. Оптимизируйте параллельный порог оператора Less, чтобы коэффициент параллельного ускорения критического объема данных был как можно ближе к 1.
2. Включите опцию автоматической векторизации компилятора -ftree-vectorize, чтобы полностью повысить вычислительную эффективность процессора за один такт.
3. Обновите версию Eigen до 3.4 и более поздних версий, укажите соответствующую архитектуру ARM при кросс-компиляции и включите поддержку fp16, например -march=armv8.2+fp16, чтобы реализовать встроенную поддержку fp16 на платформе ARM. компиляция Процессор выполняет оптимизацию SIMD и развертывание циклов, что эффективно повышает производительность Eigen::half на архитектуре ARM.
4. Оптимизировать логику реализации Bcast.Текущая версия полагается на разработчиков операторов, которые вручную определяют, требуются ли операции вещания, и извлекают три особых случая для ручной реализации (без вещания, X — элемент, Y — элемент), реализация оператора Код заполнен большим количеством избыточного кода, и такие операции, как оценка необходимости широковещания, должны быть абстрагированы, а доступ к элементам должен осуществляться через унифицированный интерфейс.
5. Оптимизируйте реализацию метода индекса элемента для BCAST, необходимо транслировать. В настоящее время производительность Bcast на складе намного ниже, чем у Tensorflow, которая отстает от трансляции библиотеки собственной библиотеки и текущей реализации Метод getbroadcastxindex не используется для оптимизации компилятора.
9. Заключение
Эта статья предназначена только для простого анализа разработчика оператора CANN и оптимизации трудоемкой работы оператора AICPU.Обсудите решения по оптимизации связи.
Нажмите «Подписаться», чтобы впервые узнать о новых технологиях HUAWEI CLOUD~