Привет, Хабр! Мы вдруг поняли, что наша последняя статья про Эльбрус вышла год назад. Поэтому мы решили исправить эту досадную оплошность, ведь мы не бросили эту тему! Сложно представить распознавание без нейронных сетей, поэтому мы расскажем о том, как мы запустили 8-битные сетки на Эльбрусе и что из этого получилось. Вообще, модель с 8-битными коэффициентами и входами и 32-битными промежуточными вычислениями крайне популярна. Например, Google [1] и Facebook [2] завели ее собственные реализации, которые оптимизируют доступ в память, задействуют SIMD и позволяют ускорить вычисления на 25% и больше без заметного снижения точности (это конечно зависит от архитектуры нейронной сети и вычислителя, но нужно же было объяснить, насколько это круто?).  
 Идея замены вещественных чисел на целые для ускорения вычислений витает в воздухе: И 8-битная нейросетевая модель [3] позволяет удовлетворить этот запрос. Она крайне полезна при работе со специализированными, встраиваемыми и мобильными устройствами, когда нет видеокарты или не хочется ее использовать. Рассматриваемая модель оперирует 8-битными целыми беззнаковыми коэффициентами, которые перед умножением преобразуются в 16-битные, то же самое происходит со входными сигналами. Для суммирования используется 32-битный аккумулятор. Перед вызовом следующего слоя выполняется нормировка, чтобы снова получить беззнаковые 8-битные значения. Таким образом, переполнения при вычислениях исключены и полученная сумма произведений является верной при любых входных данных.  Самое основное, что нужно сделать для реализации такой модели — это реализовать сверточные слои. Сверточный слой состоит из собственно свертки (см. Рис. 1) и нелинейной функции активации:   где  — точка выходного изображения,  — результат свертки,  — входное изображение,  — матрица фильтра. Рис. 1. Свертка многоканального изображения с одним фильтром размера C x X x Y. Вычислительно эффективная реализация свертки для центральных процессоров — это реализация в виде одной операции умножения матрицы на матрицу [4]. В ней предлагается представить сверточные фильтры в виде одной матрицы, в которой каждой строке соответствует один фильтр, все коэффициенты которого выписаны последовательно. На основе входного изображения формируется матрица, в которой одному столбцу соответствует одно "приложение" фильтра — т.е. коэффициенты входного изображения, на которые умножаются коэффициенты фильтра. Число столбцов соответствует числу выходов одного сверточного фильтра, т.е. числу слагаемых в сумме (1). При этом чаще всего используется "перемежающийся" порядок перечисления элементов, т.е. последовательно расположены первый элемент первого канала фильтра (или изображения), первый элемент второго канала фильтра и т.д.  Поэтому нашей основной задачей была оптимальная реализация матричного умножения для 8-битных матриц с 32-битными результатом. Все-таки Эльбрусы пока не стоят в каждом офисе, поэтому немного напомним, что же это за архитектура.  Эльбрус – микропроцессорная архитектура, построенная по принципу широкого командного слова. Это означает, что процессор Эльбрус способен исполнять за один такт несколько элементарных команд, которые вместе образуют одну большую инструкцию — широкое командное слово. Их формирование происходит полностью на этапе компиляции программы оптимизирующим компилятором. Он способен выполнить глубокий анализ исходного кода и планирование команд по вычислительным устройствам эффективнее, чем аппаратные планировщики команд, так как на этапе компиляции доступно значительно больше как временных, так и вычислительных ресурсов [5-7]. Архитектурно это реализовано с помощью 6 наборов вычислительных устройств на каждом ядре процессора (каждый со своим конвейером операций), которые мы будем называть вычислительными каналами. Эти каналы не идентичны: каждый содержит арифметико-логические устройства, поддерживающее лишь некоторые из доступных операций. Так, например, сложение целых чисел поддерживается всеми каналами, в то время как большая часть векторных операций, выполняемых над упакованными в широкие регистры числами, доступны только на двух каналах. Это означает, что использование векторных операций не всегда оправдано в плане производительности.  С развитием архитектуры Эльбрус АЛУ для каждого вычислительного канала претерпевали некоторые изменения. Информация по основным вычислительным операциям приведена в Таблице 1 для различных версий системы команд (СК). Длина машинного слова предполагается равной 32 бита. Таблица 1. Параметры АЛУ в зависимости от версии системы команд. В таблице через "/" приводится информация о числе каналов, поддерживающих операцию, и длительность в тактах.  Кроме того, в архитектуре Эльбрус предусмотрена аппаратная поддержка быстрого доступа в память в виде иерархической системы кэш-памяти и устройства предварительной подкачки массивов APB (array prefetch buffer). APB обеспечивает быструю загрузку n-линейных массивов, хранящихся в основной памяти вычислителя. В отличие от кэш-памяти, нацеленной на предподкачку часто используемых данных, APB оптимизирует время доступа к массивам, использующимся небольшое число раз и обрабатывающимся преимущественно последовательно. Применение APB имеет некоторые ограничения: При этом следует понимать, что APB будет эффективен: Для вычислительно эффективной реализации матричного умножения необходимо обеспечить максимальную загрузку вычислителей процессора, а также загрузку необходимых данных из памяти перед вычислениями, так как это время способно на порядок превышать время выполнения элементарных арифметических операций. Отличительной особенностью матричного умножения является тот факт, что один и тот же элемент матрицы может использоваться для вычисления нескольких элементов результата. Поэтому в статье Goto [8] предложен метод загрузки элементов матриц в зависимости от их размеров и параметров подсистемы памяти вычислителя. Основная идея заключается в том, чтобы обрабатывать матрицы блоками, причем размеры блоков выбираются таким образом, чтобы блоки помещались в кэш первого и второго уровня соответственно. Это позволяет обеспечить быстрый доступ к элементам матриц и использовать их многократно для вычисления вклада этих элементов в целый блок результата. Мы использовали этот алгоритм, с тем лишь отличием, что внутри блока матрицы элементы подверглись операции упаковки (см. Рис. 2-3). Задача операции упаковки — обеспечить оптимальный порядок элементов для их последующей загрузки в регистры и использования векторных команд. Упаковку необходимо выполнить всего один раз для каждого блока, т.е. ее сложность линейно зависит от числа элементов матрицы. Кроме того, упаковка позволила решить проблему с невозможностью использовать APB из-за произвольного выравнивания адресов элементов матриц: было выполнено дополнение нулевыми элементами до ближайшего размера, кратного 8 (для 64-битных регистров) для операндов умножения и до размера кратного 2 для результата.  Рис. 2. Схема упаковки левой матрицы. Рис 3. Схема упаковки правой матрицы. Предложенный алгоритм можно записать в виде псевдокода следующим образом: При этом lhs и rhs — левый и правый операнд матричного умножения соответственно, block_l(.), block_r(.) — методы, которые осуществляют разбиение на блоки lhs и rhs соответственно. Методы pack_rhs и pack_lhs реализуют упаковку блоков матриц, pack_res — упаковку результата, а unpack_res — распаковку результата. Метод kernel выполняет вычисления над упакованными блоками и помещает их в упакованный блок результата. Псевдокод метода kernel приведен ниже: Здесь pshufb — инструкция, перемешивающая элементы двух векторов согласно заданному правилу (магическое число на месте третьего аргумента), punpckhbh — инструкция, чередующая элементы старших половин входных 16-битных векторов, punpcklbh — инструкция, чередующая элементы младших половин входных 16-битных векторов, pmaddh — инструкция, выполняющая длинное умножение соответствующих элементов входных 16-битных векторов и последующее сложение соседних произведений. В основном цикле программы дополнительно была выполнена раскрутка цикла с целью минимизации обращений в память за счёт использования регистров. Размер обрабатываемого блока результата составил nr х mr = 12 х 8. В результате, одна итерация основного цикла занимает 8 тактов и обрабатывает 48 элементов результата. Для вещественного умножения длительность одной итерации цикла составляет 8 тактов для СК версии 3, поскольку загрузка данных выполняется за 14 команд буфера подкачки массивов, а вычисления — за 48 комбинированных команд умножение+сложение, которые распределяются по 8 широким командам. В Таблице 2 приведены результаты замеров времени умножения двух 8-битных матриц предложенным способом и классического умножения на Эльбрус-4С, реализованного с использованием библиотеки высокопроизводительных функций EML. Каждое измерение было усреднено по N = 10^6 итерациям. Таблица 2. Время работы умножения двух матриц А и В с помощью предложенной реализации и с помощью EML для матриц разных размеров. Можно видеть, что для Эльбрус-4С реализация целочисленного матричного умножения не превосходит по скорости высокооптимизированную вещественную реализацию за счет дополнительных затрат на упаковку данных. Для последующих поколений процессоров Эльбрус этот разрыв только усугубится вследствие увеличения количества АЛУ, работающих с вещественными числами. Мы рассмотрели популярную 8-битную нейросетевую модель, которая успешно используется на x86 и ARM, которые составляют большую часть процессорного рынка на сегодняшний день. До тех пор, пока не начнут появляться железки с нейропроцессорами в составе, это самый широкий класс пользовательских устройств, на которых запускается распознавание. Однако ускорение при использовании 8-битных коэффициентов в нейронной сети не является ее неотъемлемым свойством и сильно привязано к архитектуре вычислителя. Так, оказалось, что архитектура Эльбрус хорошо приспособлена для вещественных вычислений (в плане числа вычислителей она лучше, чем x86 и ARM), а вот с операциями над целыми числами все не так хорошо: она предоставляет до 6 64-битных вещественных АЛУ, в то время как АЛУ для векторных целочисленных операций (64 или 128 битных) доступны лишь на 2 из 6 каналов каждого ядра процессора. Тем не менее, мы реализовали умножение 8-битных матриц с 32-битными результатами промежуточных вычислений. Эта реализация оказалась сравнимой по вычислительной производительности с оптимизированной вещественной реализацией для процессоров с СК версии 3 (Эльбрус-4С), но не превосходит ее для процессоров с СК версий 4 и 5 (Эльбрус-8С и Эльбрус-8СВ). Что это значит? Мы в очередной раз убедились, что архитектура целевого устройства крайне важна и должна учитываться при разработке. Так, уже ставшие привычными вычисления в целых числах плохо “ложатся” на Эльбрусы потому, что его вещественная производительность не ниже целочисленной. Это хорошо для каких-то алгоритмов и представляет трудности для других, поэтому при обсуждении методов повышения производительности не стоит забывать об особенностях оборудования. Ну а мы все еще можем ускорять сетки иначе, например, уменьшая число коэффициентов и/или упрощая их структуру.  Есть ли какой-то смысл в 8-битных моделях на Эльбрусе? Ну, возможно в экзотических ситуациях, когда необходимо экономить память на запоминающем устройстве или сжать модель, чтобы она помещалась в кэш-память или оперативную память (если доступ к запоминающему устройству особо медленный). P.S. Статья написана по материалам нашей журнальной публикации
Limonova E. E., Neyman-Zade M. I., Arlazarov V. L. Special aspects of matrix operation implementations for low-precision neural network model on the Elbrus platform // Вестник ЮУрГУ. Математическое моделирование и программирование. — 2020. — Т. 13. — № 1. — С. 118-128. — DOI: 10.14529/mmp200109.