company_banner

Эффективные методы сжатия данных при тренировке нейросетей. Лекция в Яндексе

    Не так давно в Яндекс приезжал Геннадий Пехименко — профессор Университета Торонто и PhD Университета Карнеги-Меллон. Он прочитал лекцию об алгоритмах кодирования, которые позволяют обходить проблему ограничения памяти GPU при обучении глубоких нейронных сетей.


    — Я вхожу в несколько групп Университета Торонто. Одна из них — Computer Systems and Networking Group. Есть еще моя собственная группа — EcoSystem Group. Как видно из названий групп, я не специалист непосредственно в машинном обучении. Но нейронные сети сейчас достаточно популярны, и людям, которые занимаются компьютерной архитектурой и сетями, компьютерными системами, приходится сталкиваться с этими приложениями на постоянной основе. Поэтому последние полтора-два года этой темой я тоже плотно занимаюсь.

    Расскажу, как правильно делать компрессии в памяти, в процессорном кэше. Это была тема моей докторской диссертации в Америке, в Карнеги-Меллон. И это поможет понять, с какими проблемами мы столкнулись, когда хотели применить подобные механизмы для других приложений, таких как нейронные сети.

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

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

    Одна из главных проблем на пути энергоэффективности заключается в том, что у многих ключевых приложений, используемых и в облачных вычислениях, и на различных мобильных устройствах, есть серьезные затраты по данным — как по пересылке, так и по хранению. Это и современные БД, и графические карточки, и конечно, машинное обучение. Все это требует очень серьезных ресурсов со всех уровней стека, от ядра вплоть до сетевых ресурсов.

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


    В результате одна арифметическая операция сложения в плане энергии будет стоить вам приблизительно 1 пикоджоуль. При этом одна операция с плавающей точкой, floating point, будет стоить приблизительно 20 пикоджоулей. Если ты захочешь прочитать 4 или 8 байт из памяти, это будет тебе стоить по крайней мере на два порядка больше по энергии. И это существенная проблема. Любая попытка работы с памятью стоит достаточно дорого. Причем неважно, про какие устройства мы говорим. Ситуация одинаковая что для мобильных телефонов, что для крупных кластеров и суперкомпьютеров.

    Из этого следует, что очень многие ресурсы, которые есть даже у текущего мобильного телефона, не могут быть в полной мере использованы по энергетическим ресурсам. Если взять современный телефон, неважно, Android или iPhone, имеющуюся пропускную способность между памятью и ядром мы можем использовать в пике и только приблизительно на 50%. Если мы этого не сделаем, телефон будет перегреваться, а вернее никто ему перегреваться не даст — будет понижена частота шины при общении между памятью и ядром, и производительность тоже упадет.

    Многие ресурсы сейчас невозможно использовать, если не применять хитрые оптимизации.


    Один из способов борьбы с нехваткой различных ресурсов на разных уровнях — это сжатие данных. Это не новая оптимизация, ее успешно применяли и для сетей, и для дисков, все мы пользовались различными утилитами. Скажем, на Linux многие пользовались утилитами gzip или BZip2. Все эти утилиты очень успешно применялись на этом уровне. Применяются обычно алгоритмы или на базе Huffman encoding или Lempel-Ziv.

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

    Я расскажу, почему так произошло, и что можно сделать, чтобы компрессия была доступна на различных уровнях, то есть сжатие непосредственно в кеше. Cache compression — имеется в виду, что сжатие происходит непосредственно в железе, то есть меняется часть логики самого процессорного кеша. Вкратце расскажу, какие у этого бонусы. Расскажу про компрессию в памяти, какие проблемы там. Кажется, что это одно и то же, но заимплементировать эффективно компрессию в памяти — совсем другие, не такие, как в кеше. Расскажу про совместную работу с компанией NVidia, где мы сделали Bandwidth Compression в реальном железе для современных GPU, и оптимизация, что мы сделали, есть в последнем поколении GPU карточек — Volt. И расскажу про совсем радикальную оптимизацию, когда исполнение происходит напрямую на сжатых данных без декомпрессии вообще.

    Несколько слов о сжатии в кеше. Это была статья на конференции PACT в 2012 году, и работа была совместно с компанией Intel. Чтобы было понятно, какая основная проблема, если ты хочешь превратить твой 2 МБ или 4 МБ процессорный кеш в 4 МБ или 8 МБ. Ты сделал L-компрессию, и в чем же проблема?

    Грубо говоря, когда у тебя происходит операция обращения к памяти, если мы говорим про x86 архитектуру, load или store в память, то если данных нет на регистрах, то мы идем в кеш первого уровня. Обычно это три-четыре такта на современном процессоре. Если данные есть, они идут на CPU обратно. Если их нет, то запрос в память идет дальше по иерархии, доходит в L2-кеш.


    L2-кеш на большинстве интеловских процессорах 15-20 тактов, в зависимости от размера кеша. И дальше данные обычно едут обратно, если были найдены в L2-кеше, если ты не пошел в память. Данные идут и на процессор сразу, и в L1-кеш, сохраняются, если вдруг ты продолжаешь эти данные переиспользовать, чтобы они были ближе процессору.

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

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

    Во многом из-за этого Intel в какой-то момент остановился на разработке компрессия кеша. У них была целая группа, которая там занималась, они в 2005-2006 году разработали алгоритм, который давал декомпрессию порядка 5 тактов. Эта задержка увеличилась приблизительно на 30%, но кеш становился почти в два раза больше. Тем не менее их дизайнеры посмотрели на большинство приложений и сказали, что это слишком затратно.

    Когда я начал работать над этой темой в 2011 году, они сказали, что если ты сможешь сделать чего-то за 1–2 такта, то это можно будет сделать в реальном железе, попробовать.

    Я пробовал разные алгоритмы, и одна из причин, почему не получилось использовать алгоритмы, которые уже имелись в литературе, это то, что все они были сделаны изначально в софте. В софте другие ограничения, люди используют различные словари и тому подобные техники. Если эти техники пытаться сделать в реальном железе, они достаточно медленно работают. IBM сделал алгоритм Lempel-Ziv полностью такой, как в gzip используется, полностью в железе, и декомпрессия занимала 64 такта. Понятно, в кеше ты не будешь такое применять, только в памяти.

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

    Я увидел, что парадоксально много нулей, от 20% до 30%. Если вы возьмете большой пакет приложений от Intel, там 200 разных приложений, которые люди используют для вычислений — очень много нулей. Это инициализация, это матрицы с большим количеством нулей, это нулевые указатели. Причин для этого много.

    Очень часто есть повторяющиеся значения. В каком-то небольшом участке памяти, в кеше, могут повторяться очень похожие значения. Это, например, и если вы работаете с графикой, у вас куча пикселей, и если у вас часть картинки с одинаковыми цветами, то у вас все пиксели, подряд идущие, будут одинаковые. Кроме того, narrow values, однобайтовые и двухбайтовые значения, которые хранятся в 2, 4 и 8 байтах. Почему так происходит и чья эта ошибка? Откуда такая избыточность?

    Избыточность связана с тем, как мы программируем код. Используем какой-то язык, например, С++. Если мы хотим выделить память под какой-то объект, допустим, под целый массив, представьте, что в массиве мы храним статистику о каких-то событиях, и эти события могут быть как очень частыми, скажем, миллиарды, так и единичные. Например, обращение в память с какой-то конкретной инструкцией. Большинство инструкций в память не обращается, но некоторые могут обращаться миллиарды раз за время запуска.

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

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

    Я увидел много типов избыточности. Первый вопрос — насколько их много?


    Здесь эксперимент, в котором я брал периодически данные из кеша второго уровня, сохранял снепшот этого кеша и смотрел, какое количество там нулей, повторяющихся значений. На оси Х различные приложения из пакета SPEC2006, который используется активно в компьютерной архитектуре, а также другие разные приложения от Intel — это и БД, и различные web workflow, такие как сервер Apachi, например. И здесь предположение, что это 2-мегабайтовый L2-кеш.

    Можно заметить, что есть большая вариативность между избыточностью в разных приложениях, но даже эти очень простые паттерны встречаются достаточно часто. Только они покрывают 43% всех cache lines, все данных, что хранятся в кеше.

    Можно придумать что-нибудь достаточно простое, что покроет эти и, может, какие-то другие паттерны, и даст нам хорошую производительность по компрессии.

    Однако вопрос в том, что же роднит эти паттерны? Придется ли мне делать какой-то алгоритм компрессии, который будет работать специфично для каждого из этих паттернов или можно сделать что-то общее?

    Общей была идея наблюдения. Все эти значения, они могут быть и большие, и маленькие, между ними очень маленькая разница. Грубо говоря, динамический диапазон значений в каждой конкретной линии кеша очень маленький. И можно представить значения, хранящиеся в кеше, например 32-байтовую линию кеша можно представить просто с помощью Base+Delta Encoding.


    Взять, например, первое значение за базу, а все остальные представить как смещение от этой базы. И поскольку значения друг от друга отличаются в большинстве случаев не сильно, то наша дельта помещается в один байт, и вместо 32 или 64 байтов мы можем обойтись всего 12 байтами, и сохранить порядка 20 байт места.

    Не буду рассказывать обо всех деталях, как имплементировать это в реальном железе. Мы сделали реальный прототип, написали на Verilog прототип, погоняли его на современных FPGA, с Intel поговорили по поводу имплементации. Можно сделать алгоритм, основанный на этой идее, который будет требовать в декомпрессии всего один-два такта. Этот алгоритм применим, и он дает и хорошую компрессию…


    Лучшие предыдущие работы, которые применялись в кеше, давали порядка 50% дополнительного места. Это не чистая компрессия — она может дать намного больше — это реальный бонус эффективной компрессии, то есть насколько кеш выглядит больше для пользователей. Тут еще всякие проблемы с фрагментацией и так далее, которые нужно решать.

    Компрессия у нас на уровне лучших предыдущих механизмов, которые были у Intel, но главный выигрыш в середине слайда — это декомпрессия. Предыдущие алгоритмы, у них лучшая декомпрессия была 5-9 тактов. Нам удалось сделать за 1-2 такта, при этом компрессия у нас тоже достаточно эффективная.

    Алгоритмы такого рода можно делать в реальном железе и применять в кеше, например, в памяти.

    Эффект от применения такой оптимизации в кеше ведет к тому, что кеш выглядит для пользователя часто почти в два раза больше по эффективности. Что это значит? Если посмотреть на современный процессор, на фотографии, там самих ядер почти нет. Там бóльшую часть места занимают процессорные кеши — 40-50% запросто и у IBM, и у Intel. На самом деле Intel не может просто взять и удвоить кеш, там просто нет места для большего количества кеша. И такие оптимизации, которые стоят всего несколько процентов от самого ядра, конечно, очень интересны.

    Мы работали с различными оптимизациями во второй работе, о которой сегодня не буду рассказывать, о том, как же работать с такими кешами, у которых теперь cache lines могут быть различного размера. Все эти проблемы были успешно решены.

    Хочу рассказать о нашей третьей работе, которая тоже была сделана с Intel, о том как сжимать память.

    Какие там проблемы?


    Основная проблема, что если в Linux или Windows у меня есть страница памяти 4 Кб, то для того, чтобы ее сжать, нужно решить следующую проблему: нужно решить проблему того, как изменятся адреса данных на этой странице. Изначально у тебя есть 4 КБ, и в ней каждая cache line тоже 64 байта. И смещение любой линии кеша внутри этой страницы памяти найти тривиально: берешь 64 и умножаешь на нужное тебе смещение.

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

    Можно сказать, что можно их где-то сохранить. А где их сохранить? Либо снова в памяти, либо в кеше. Но если ты хочешь для каждой памяти сохранить все смещения, у тебя никакого кеша не хватит, тебе нужны ресурсы порядка сотен МБ, чтобы обслужить всю память. Ты не можешь сохранить эти данные в чипе, а в памяти ты не хочешь их хранить, потому что каждое обращение к памяти — это теперь будет несколько обращений в память. Вначале ты будешь идти за метаданными, а потом за реальными данными.


    Вторая проблема, с которой все сталкивались, когда работали с ОС, это фрагментация данных. Здесь она становится экстремально сложной, потому что каждая страница занимает тоже различное место в памяти. При этом в виртуальном адресном пространстве все страницы по-прежнему 4 Кб, но после компрессии они все занимают совершенно разные размеры. И это проблема, как же теперь с этим пустым место работать. ОС не в курсе, что мы разрешили страницы сделать меньшего размера, она просто не видит этих фрагментированных кусков. Просто так, ничего не поменяв, мы не получим бонусы от компрессии.


    Что мы предложили для решения этой проблемы? Сжатие с использованием линейного коэффициента. Мы наложили набор определенных ограничений, которые подробно описаны в статье, но суть сводится к тому, что если я применяю компрессию для памяти, я применяю такой алгоритм, который гарантирует, что в каждой cache line на этой странице либо сжимается с определенным коэффициентом, допустим, 4 к 1, 3 к 1 или не сжимается вовсе.

    Мы теряем потенциально что-то в плане сжатия данных, потому что мы налагаем дополнительные ограничения, но при этом главный бонус в том, что дизайн очень прост, его можно реализовать, что мы успешно делали, например, в Linux.

    Linearly Compressed Pages (LCP), техника, что мы предложили, справляется с главной проблемой, что теперь адреса всех данных находятся достаточно просто. У нас есть небольшой блок метаданных, который хранится на этой же странице, и есть либо оригинальные данные, которые хранятся в сжатом виде, либо так называемый exception storage, там хранятся cache lines, которые мы не смогли сжать таким способом.

    Основываясь на этих идеях, нам удалось получить и хорошее сжатие данных, и самое главное, я сравниваю с лучшими предыдущими работами, которые были сделаны в основном IBM, у них тоже были хорошие алгоритмы для сжатия.

    У нас компрессия не была особо лучше, чем у них, но самое главное, мы получили больше памяти, не платя за это дополнительную производительность. Грубо говоря, они получили больше память, но за это у них была потеря производительности, пусть небольшая, но была. И потеря по энергоэффективности. А нам удалось в плюсе получить все, мы получили и производительность лучше, и более низкие энергозатраты.

    Вкратце о последней работе, что есть в реальном железе. Это наша работа с Nvidia. Проблема была в энергоэффективности при транспортировке данных по каналам связи из памяти в чип. В графических карточках памяти самой намного меньше, но намного больше пропускная способность этой памяти, в 5-6 раз запросто на большинстве графических карточек. Потому что многим графическим приложениям требуется сумасшедший объем этой bandwidth, чтобы эффективно обрабатывать большие массивы данных.

    Проблема, с которой столкнулась Nvidia, когда я там делал свою стажировку в 2014 году, была связана с тем, что они заимплементировали компрессию в реальном железе, и она практически на всех приложениях давала хорошие бонусы. Но проблема была в том, что на некоторых приложениях вдруг наблюдался следующий эффект: вдруг включался такой механизм, который называется DVFS, который замечал, что транспортировка памяти начинает тратить сумасшедшее количество энергоресурсов, и чтобы эту проблему победить, есть определенный переключатель на канале связи, который если видит, что энергопотребление превышает допустимые пороги, то надо повышать частоту передачи данных.

    Частота понижается — эти проводки, которыми соединена память с чипом, охлаждаются. Проблема в том, что когда ты понижаешь частоту, у тебя падает скорость передачи. Компрессия в итоге оказывала негативное влияние на производительность. Было понятно, что это как-то связано с компрессией, но это случалось не всегда, и непонятно, как с этим бороться. С этой проблемой они пришли в отдел исследований Nvidia к одному из моих менторов, с которым эту проблему исследовали.


    Проблема была в том, что здесь нужно понимать не только низкоуровневый механизм, но надо понимать и физику процесса. Покажу на простом примере. Представьте, вы пересылали по проводам, скажем, 0011. По четырем проводам вы переслали четыре бита. А теперь вы пересылаете 0101. С точки зрения пересылки памяти, если ты переслал 0 и пересылаешь снова 0, энергия практически не тратится, потому что тебе не нужно производить так называемый bit toggle, тебе не надо менять напряжение на этом пине, и энергия, грубо говоря, пропорциональна квадрату напряжения, и энергия тратится тогда, когда ты переходишь с 0 на 1 или с 1 на 0. Если у тебя много бит переключения происходит на пин, у тебя тратится много энергии. А если биты не меняются, энергия практически не тратится. Инженеры хорошо знакомы с этим феноменом, но программисты об этом обычно не задумываются.

    Эти так называемые тогглы — это причина того, что у нас тратится существенная энергия для пересылки памяти. И фактически для большинства протоколов, допустим, для networks of chip или для DRM количество энергии, затрачиваемой на пересылку данных, прямо пропорциональна количеству тогглов.

    Что же происходит? Почему компрессия ни с того ни с сего может сделать ситуацию настолько плохой?


    Вот пример одного из участков данных из одного приложения Nvidia, с реальными данными. Пересылаются эти данные по частям, обычно 32 байта или 16 байт, эти куски данных называются флитами. Как они пересылаются? Вначале первая часть, потом по пинам вторая часть. Если посмотрим, что произошло, если мы сделаем обычный XOR, то у нас всего два бита флипнуло на 16 или 8 байт, потому что данные были очень хорошо уложены до этого, они были очень регулярные.

    Теперь мы применили какой-то алгоритм, например, frequent pattern compression, алгоритм от Intel, и вуаля, у нас каждый второй бит практически флипает. Количество флипов может вырасти и в 15, и в 16 раз. Компрессия фундаментально меняет энтропию, количество информации, которая хранится в каждом бите, и кроме того полностью ломает alignment. То есть если у тебя байты были очень хорошо уложены, то теперь они уложены совершенно случайным образом. Это позволяет тебе пересылать меньше, но тебе приходится пересылать дороже. Это проблема, с которой Nvidia и столкнулась. Я проанализировал их приложение, и те, где возникали у них проблемы, это те, где было сумасшедшее превышение средних порогов этих тогглов. Могло быть так, что раньше их было 2%, а потом становилось 40%. И это причина того, что энергоэффективность падала.

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


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

    Эту свою экспертизу я стал применять порядка полутора лет назад к различным приложениям, и одно из основных приложений, которое я применял, я тогда работал в Microsoft Research в Редмонде, это был machine learning.

    Расскажу о том, где же я их применял, какие результаты нам удалось получить, и какие интересные проблемы нам там удалось решить, и какие еще остались. Основной фокус этой части будет на очень растиражированной ныне DNNs. Я буду фокусироваться собственно на тренировке данных. Есть много других интересных алгоритмов, но DNNs — это то, с чем мы столкнулись в майкрософтовском дата-центре в Ajura, очень много пользователей тренируют различные виды сетей, и очень много из них тренирует их, мягко говоря, неэффективно. Это значит, что производительность от потенциального железа меньше 10%. Нам было интересно посмотреть, почему так происходит, что пользователи делают не так, что делают неправильно потенциально создатели всех популярных фреймворков: TensorFlow, CNTK, MXNet. Они все тоже недостаточно оптимизированы, на мой взгляд, несмотря на потраченные там ресурсы.

    Фокус на DNNs связан с тем, что это было то самое приложение, которое большую часть серверов нагружала, которые использовали GPU.


    DNNs используется для большого количества приложений. Это и перевод, и распознавание речи, и система рекомендаций, и детектирование объектов, и классификация картинок. Разные типы сетей применяются. На мой взгляд, там до сих пор меньше науки, и очень много магии вуду, но эти сетки тренируются на реальных машинах, тратят реальные ресурсы. Какими бы они ни были, мы должны их достаточно эффективно наоптимизировать.

    Фокус моей работы в основном связан с тренировкой, а не с использованием сетей. Обе проблемы очень важны, как тренировка сетей, и так и inference.


    Когда я начал заниматься этой проблемой, у Google уже был TPU первой версии, сделанный в реальном железе, и соревноваться с несколькими компаниями, которые уже делают имплементацию inferent в реальном железе мне казалось не особо интересным, поэтому я фокусировался на тренировке, которая была очень важна для Microsoft.

    Чем же они отличаются фундаментально? Почему оптимизация и железо для них нужно потенциально разное? Если посмотреть на анонсы от Google, все видели, что TPU первой версии годился вообще только для inference. Второй годится частично для тренировки, но тоже не особенно хорошо. Первый шаг, у тренировки inference, он одинаковый. Это так называемый forward pass. Ты проходишь по всем уровням сети, генерируешь какой-то ответ, и в inference на этом все заканчивается. Ты получил результат — это предсказание твоей сетки. К сожалению, на тренировке на этом все не заканчивается. Тебе нужно делать так называемый backward pass, тебе нужно пропагировать градиенты по уровням сетей, минимизировать свою ошибку, повторять этот процесс многократно для всех своих входных данных.

    Это основное отличие, особенно для проблемы, с которой мы столкнулись. А главная проблема для нас оказалась в том, что эти сетки потребляют большое количество памяти. Память потребляется потому, что между этими слоями генерируется промежуточные результаты, которые в англоязычной литературе обычно называются feature maps или activations. В inference эти данные тоже вычисляются, но они потребляются следующим уровнем и больше ни для чего не нужны. При back propagation, типичном алгоритме, который используется для тренировки нейросетей, все эти результаты нужны на обратном проходе, чтобы правильно вычислить градиенты. Грубо говоря, если у меня есть уровень L2, я вынужден хранить его результат, весь forward pass и весь обратный, потому что он понадобится аж в самом конце, и никакого варианта у меня здесь нет, эти данные нужны, чтобы правильно вычислить градиент.

    Если у меня есть глубокие сетки, мы знаем, есть сетки и по 100, и 200 уровней, например, ResNet, используемые для image classification. Все эти промежуточные данные надо где-то хранить. Результат этого — это то, что у нас, если взять ResNet даже 101 уровень, стандартная сетка, которая используется, если ты хочешь более-менее нормальный mini batch использовать, скажем, 64 картинки за раз, тебе требуется часто больше памяти, чем влезает на одну графическую карточку, больше 16 Гб. Самая даже дорогая карточка, P100 Pascal или даже Volt100, там нет больше 16 Гб. И если оно не влезает в память одной графической карточки, надо использовать несколько.

    Для компании, такой как Facebook, не проблема купить и много. Но вопрос в том, что если ты смотришь, что карточка используется только для ее памяти, ее вычислительные ресурсы полноценно не используются. И глупо использовать много карточек только для того, чтобы покрывать нехватающую память.

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


    Первый шаг, который мы сделали, чтобы проанализировать и понять, куда деваются эти данные, мы сделали профиль по популярным сеткам, использующийся для классификации картинок. На оси X здесь AlexNet, с который начиналась эта DNN революция, и другие популярные сетки: Overfeat, VGG16, Inception (эта сетка известна так же как Google version 3, одна из популярных сеток, которую использует Google). В скобках указан размер mini batch. Это параметр, указывающий, сколько картинок мы обрабатываем в параллели. И данные, что использовались для этого теста, это классический ImageNet, классификация картинок приблизительно 220 на 220 пикселей глубиной 3, и на выходе 1000 классов: кошки, собачки, люди и так далее.

    Куда уходит память? Даже на достаточно маленьких старых сетях AlexNet потребление памяти уже было несколько ГБ, это в 2011 году, когда карточки были еще меньше. И основная масса памяти, чем глубже сетки мы делаем, тем больше памяти потребляется этими feature maps, промежуточными результатами.

    А почему не веса? Веса действительно важны, но веса не меняются, не растут линейно с изменением размера mini batch, когда ты начинаешь обрабатывать больше и больше картинок в параллели, веса для них используются те же самые, а вот параметры активации растут линейно с изменением количества inputs. Для чего мы увеличиваем количество картинок в параллели? Чтобы хорошо использовать GPU ресурсы. Современный GPU может в параллели запросто запускать 8000 threads, и чтобы его нагрузить полноценно, нужно в прааллели обрабатывать большое количество картинок, иначе GPU не будет полностью загружен. А раз так, зачем его использовать? Он не будет более эффективным, чем даже CPU или ISAC.

    Чтобы использовать эффективно акселератор, его надо полностью загружать. И тут возникает проблема, связанная с памятью. Это одна из главных проблем, и когда мы стали ее исследовать, главная проблема, с которой мы столкнулись, что в моей области, в компьютерной архитектуре, в эффективных системах, все сфокусированы на DNN inference, на наших ключевых конференциях, таких как ISCA, MICRO, в 2015 году было по 15 статей на одной, посвященной deep neural networks, и все занимались inference, все оптимизировали только веса.

    Во-первых, архитекторам проще оптимизировать inference, веса уже найдены, и тебе нужно просто сделать один forward pass, больших затрат по памяти там нет, и можно оптимизировать конволюционные уровни до потери сознания, и радоваться полученным результатам.

    Проблема, что когда ты эту проблему решил, тренировке это особо не помогает. Все методы, что они предложили для inference, такие как удалять какие-то веса, использовать квантизацию, фактически это взять 32-битный floating point, плавучку, и превратит ее в 16 бит, 8 или 4. Все эти механизмы для тренировки не работают, потому что теряется качество вычислений, и обычно используется какой-то алгоритм stochastic gradient descend, он просто перестает сходиться. Мы пересмотрели эти алгоритмы, никто из них к тренировке напрямую не применим. Также они не очень хорошо ложатся на современные GPU, все эти статьи предлагают сделать новый ISAC, что-то похоже на TPU или давайте используем в лучшем случае какую-то программируемую логику типа FPGA для этого. На GPU все эти техники не очень хорошо ложились.


    Мы решили посмотреть, что мы можем сделать нового и интересного специально для тренировки этих сетей. Одно из основных наблюдений, что мы сделали, что если посмотреть на то, как используются данные, которые хранятся так долго в памяти, основная причина, почему они хранятся там так долго, это то, что у нас есть одно место, где данные генерируются, например, на первом уровне LX, и на следующем уровне идет потребление результатов предыдущего уровня. После этого может пройти еще сотня уровней, пока мы идем, пока потребуется переиспользовать эти данные. И ради этого все время мы храним эти данные. Но использование сгенерированных результатов всего в двух местах: сразу же и после.

    У нас есть большой промежуток времени, где эти данные не используются никак. Мы предлагаем использовать какие-то новые потенциально алгоритмы кодирования этих данных, и хранить данные на этом промежутке в сжатом виде. И только когда эти данные понадобятся, на этом конкретном уровне их разжимать. Причем нейронные сети для этой оптимизации очень хорошо устроены. Мне все данные сразу ни в какой момент не нужны. В каждый момент я обрабатываю один уровень, один за одним. И мне не нужно в каждый момент декомпрессировать, разжимать все данные. В каждый момент я работаю с одним, максимум с двумя уровнями. Это позволяет мне сжимать и разжимать данные.

    Все вроде хорошо, но вопрос сводится к тому, как же это сжатие и разжатие данных делать эффективно? Ведь в основном все нейронные сети сейчас хранят данные в виде плавающей арифметики, которая сжимается не очень хорошо. И кроме того, обычно если сжимается, то сжимается с потерей значения. Основная наша идея в том, что не надо пытаться применить алгоритмы, даже те, хорошие. Хотя мы попробовали их применить, ни один из них не заработал достаточно эффективно. Нужно использовать какие-то новые алгоритмы и хорошее понимание того, что же конкретно происходит в нейронной сети.


    Вот первое, что мы заметили, работая над этим исследованием: большая часть активации этих feature maps на самом деле имеет очень определенные типы. Несмотря на то, что уровней много, разных типов уровней в нейронных сетях, которые есть сейчас, очень немного. Это, например, конволюционные уровни, это Relu уровни, где просто вычисляется rectified linear function или pulling layer, фактически маленький фильтр, который идет по картинке или по промежуточному результату, так называемая нормализация и так далее.

    Несмотря на то, что уровней может быть сотня, разных уровней не много. Есть определенные уровни, которые доминируют над другими. Мы заметили, что очень большое количество пар во всех этих сетках, один уровень, закачивающийся в Relu, идущий в pulling layer. Есть большое количество layers, за которыми следуют конволюционные уровни и много других.

    Интересно, что Relu используется изначально вместо, скажем, гиперболического тангенса или какой-то другой математической нелинейной функции — потому что она хорошо и быстро вычисляется. Но у нее есть и другое прекрасное свойство — ее градиент тоже вычисляется очень быстро.


    На самом деле, если мы используем Relu, то на обратном проходе. Для чего мы храним данные, так это чтобы вычислить ее градиент. Но Relu — очень простая функция, она дает то же самое значение, если значение было положительным, либо 0, если значение было отрицательное. Ее градиент — это просто знак, либо плюс, либо минус.

    Когда мы это поняли современно случайно, мы анализировали распределение значений во время тренировки сети, мы заметили, что она by modal. На самом деле она стремится к двум значениям. Мы подумали, что вроде нет никакой математической логики, почему она стремится к двум значениям во время тренировки. Оказалось, она стремится либо к плюсу, либо к минусу. И прежде чем делать какие-то мощные оптимизации, мы просто взяли из 32 бит, и все, кроме первого бита, 31 затерли мусором. И увидели, что тренировка происходит прекрасно, ничего не меняется.

    На самом деле мы храним полные 32 бита, а нужен из них только один. И таких трюков много. Связано это с тем, что уровни используемые очень простые. Например, pulling layer — это часто окно 2 на 2, где ты выбираешь максимальное значение, 2 на 2 или 3 на 3. Хранятся полные значения и потом находится из них максимум на обратном проходе, но на сам деле все, что тебе нужно, это индекс максимального значения. Сейчас хранятся все значения в TensorFlow, в CNTK, во всех фреймворках, что я смотрел, в полном объеме. Связано это с тем, когда я поговорил с ребятами, занимающимися machine learning, они сказали, что так правильно. Но они обычно не делают сами непосредственно фреймворки. А те, кто делают фреймворки, обычно не запариваются по поводу математики, они вызывают какую-то функцию вычисления градиента, им все равно, там линейная функция или тангенс.

    Из-за того, грубо говоря, что две области между собой не особо говорят, там получилось, что есть большое количество возможностей для оптимизации этой тренировки. И уровни Relu можно сжать в 32 раза, pulling layer можно сжать в 8 раз. Вся память потребляется, все жалуются, в том числе ребята из TensorFlow, что им не хватает памяти. Но там есть большие ресурсы по сжатию памяти без какой-либо потери качества. Мы во всех этих оптимизациях не теряем ничего по качеству.

    Мы использовали стандартную бинаризацию, мы хранили вместо 32 бит 1 бит. Есть деталь, что нам непосредственно нужно полное сгенерированное значение на следующем уровне, поэтому мы не можем сразу закодировать данные. Поэтому когда генерится результат, мы создаем две копии. Одна полная 32-битная, она сразу же потребляется следующим уровнем, и память может быть освобождена. А для второго использования мы храним уже однобитную копию.

    Эта оптимизация требует всего один бит, и она lossless, нет потери качества.


    Вторая идея основана на том, что Relu в силу своей семантики генерирует кучу нулей. И все отрицательные результаты превращаются в нули. Если провести измерения, взять одну из сеток, мы взяли VGG16, потренировали ее в течение 10 эпох, и посмотрели, зеленым здесь показано общее количество нулей — чем выше, тем лучше. 1х соответствует 100% нулей, и 0,6 — 60% нулей. Это то, сколько нулей хранится в этих активациях.

    Дальше по графику разные уровни, скажем, Relu_1, Relu_2 и так далее, и сколько нулей в каждом из этих уровней. Можно заметить, что их много, больше 60%, ближе к 70%. Хотя на этом графике не всегда четко, но можно сказать, чем дольше длится тренировка, тем больше нулей становится на многих уровнях. По эпохам у нас добавляется количество нулей, но через некоторое время оно стабилизируется.

    Что это значит? Если у нас только нули, их можно более эффективно представлять. Но если не использовать новое железо, все нужно делать, используя GPU. Нужно писать свой CUDA код, который будет все эти оптимизации проводит. Звучит это легко, но хорошего спарс-матричного представления в CUDA не было. Мы попробовали использовать то, что NVidia предлагает, и оно заточено на количество нулей выше 99%. Само представление на это заточено. Нам пришлось делать наше собственное представление, которое работает хорошо в диапазоне от 50% до 80% нулей. Эти нули могут быть эффективно убраны, используя GPU инструкции. В идеале все эти оптимизации можно сделать и в реальном железе, и, скажем, в CUDNN библиотеке, что мы Nvidia и предложили. Но пока мы это делаем, используя CUDA код, и даже это работает достаточно эффективно.

    Наконец, мы сделали несколько наблюдений, что можно использовать определенные оптимизации с потерей качества, так называемые Lossy Encoding, но делать это надо аккуратно. В предыдущей литературе, особенно ребята из IBM предлагали брать определенный уровень. Скажем, L1 сгенерировал какие-то результаты. Берем 32 бита, превращаем их в 8 или в 16, и уже как-то разбираемся с ошибкой, которая накапливается.

    Проблема в том, что если ты занимает 32-битные числа 16-битными числами, у тебя эта ошибка накапливается со временем, она начинается здесь, потом растет, и в итоге, скажем, AlexNet, если заменить 32 бита на 16 и сделать по-честному тренировку, не одну эпоху, как они сделали в своей статье, а потренировать ее как положено, когда ты достигаешь правильного уровня тренировки на validation. Ты видишь, что она перестает нормально тренироваться, и эта оптимизация не работает. Хотя да, она не ломается, ничего не происходит, но она перестает нормально тренироваться. А все потому, что если потеря качества происходит так рано, она пропагируется через всю сетку, и это неэффективно.

    Но на самом деле тебе эта потеря качества тебе так рано не нужна. То, что тебе надо, это взять оригинальную версию, не сжимать и передать ее на следующий уровень. Помните, было два использования, первое и второе. Первое использование — делать без потери качества. Ошибка на forward pass вообще не происходит. И хранить только уже версию, используемую на втором проходе, на backward pass, только ее уже хранить в сжатом виде. Ошибка вставляется только на backward pass, она распространяется значительно меньше.

    Что нам удалось получить?


    Здесь я показываю, как идет тренировка с различными оптимизациями. Здесь есть наша база — это традиционная тренировка AlexNet с FP32, 32 битами. Если сделать то, что IBM предлагал, сделать All-FP16, все тренировать в 16 битах, и на наших результатах она не тренируется. Можно избирательно какие-то структуры данных в 16 бит, оно будет работать, но если все делать в 16 бит, и вычисление, и хранение, она в какой-то момент перестает тренироваться.

    Но если делать тренировку так, как мы предлагаем, либо ошибка возникает только на обратном проходе, либо использовать нашу технику с задержкой внедрения ошибки, то можно тренировать и с 16 битами, и даже с 8 битами. Ошибка после тренировки падает практически идентичным образом, эти три кривые практически одинаковые.

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


    Мы предложили систему, которую мы назвали Gist, дословно с английского это будет означать просто «суть». Мы взяли DNN, взяли execution graph, который используется для вычислений, и наша система по конкретному описанию нейронной сетки, скажем, мы использовали CNTK в наших экспериментах в основном, поскольку эксперименты делались в Microsoft. Она находится различные возможности для сжатия данных и вставляет наш CUDA код в эти места. Мы получаем новый модифицированный execution graph, который используется различными, и TensorFlow, и MXNet, все используют его часто в своей модели вычислений. Мы просто добавляем маленькие GPU ядра вычислений, которые производят компрессию и декомпрессию там, где она нужна. И это не требует не изменения железа, которое ты используешь, ни изменения даже библиотеки.

    Мы в нашей статей показали, что если бы это сделать в библиотеке, например, cuDNN, которая используется часто для работы с нейросетями, можно получить намного больше бонусов. Но внедрить в продукт требует некоторого времени. Мы над этим тоже работаем. Аллокация памяти для новых структур происходит иногда не только самим фреймворком, таким как TensorFlow. GIST сам выделяет некоторые структуры данных под себя.


    Вопрос, какие бонусы мы с этого можем получить, не теряя качество тренировки? Это очень существенный момент. То, что затратило большое количество ресурсов, доказать, что после того, как мы все это делаем, мы все тренируем, 90-100 эпох с таким же качеством, как без наших оптимизаций. И оно не занимает дольше времени. Мы показываем на пяти популярных сетках, что мы до двух раз уменьшаем количество потребляемой памяти, и можно в два раза более крупные модели или более глубокие тренировать с тем же железом. При этом замедление тренировки от 6% до 8%, в зависимости от того, какие оптимизации ты применяешь, что обычно приемлемо, поскольку тренируют эти сетки часто неделями. Это основная часть по оптимизациям, которые мы недавно делали, когда я еще работал в Microsoft.

    Расскажу о том, что я делал со своими студентами в Университете Торонто. Основная проблема, с которой я столкнулся, работая и в Microsoft, и когда еще над PhD работал, в том, что в DNN Training, не смотря на то, что все про него говорят и все работают, хороших бенчмарков, которые лежат в открытом доступе вне компании, нет. И даже внутри компании ограниченный доступ много к чему. Когда я в Microsoft работал, я просил дать самую лучшую LSTM сетку, которую вы на speech используете, я хочу посмотреть, какие там есть возможности. Невозможно было получить какой-то нормальный код от кого-то, хотя вроде все над этим работают.

    Эта проблема существенная. Чтобы в университетах делать полезные исследования, нужно работать над теми проблемами, которые сейчас актуальны. В моем комьюнити все работают в основном над image classification, все оптимизируют AlexNet, ResNet и так дале, и не смотрят ни на что другое. Image classification — очень важная проблема, понятно, всем важно распознавание образов, чтобы их грамотно затегали на Facebook. Но есть куча других интересных проблем, и machine learning не состоит только из этого.

    Мы нашли, что часто выводы, если делать правильные бенчмарки, могут меняться кардинально. Например, анекдотический случай. Девелопер, разрабатывающий MXNet, который активно поддерживается Amazon, они когда проводят маркетинг своей сетки, говорят, что мы работаем быстрее TensorFlow. Если взять ResNet 50, действительно, мы померили на «паскалевских» карточках, они на 40% быстрее, чем TensorFlow работают. Значит ли это, что MXNet быстрее, чем TensorFlow? На этом приложении да. Но бенчмарки не состоят из одного приложения. Мы взяли LSTM, один сделанный для MXNet с такими же параметрами, и такой же сделанный для TensorFlow, причем не как-то сами, официальные из Amazon и из Google, и TensorFlow в 2,5 раза быстрее на переводе. Это означает, что не все так просто. Нельзя делать выводы и бенчмарки только на одном результате.

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

    Мы взяли, понятно, Image Classification, куда без него, он есть как бенчмарк. Мы взяли object detection, и лучшие алгоритмы, что мы нашли, используют так называемый Faster RCNNs, модели 16-17-го года мы используем из Google и из MIT, самые свежие. В переводе мы используем две разные группы моделей: старый классический LSTM, но им, может, полгода. Одна nmt — топовая сетка Google, она open source. И очень похожая сетка sockeye от Amazon. Их мы сравнивали.

    Мы использовали также новую модель Transformer, которая использует Attention layers. Их используют года с 2013-го, но это первая сетка, которую я знаю, которая вообще отказалась, по сути, от конволюционных уровней и использует именно Attention layers. И девелоперы из Google утверждали, что она очень хорошо ложится на современный GPU, хорошо тренируется, все это было очень интересно проверить на реальных бенчмарках.

    У нас есть speech recognition, где мы тоже используем LSTM. Мы используем adversarial networks, у нас есть reinforcement learning, который Google очень эффективно использовал в AlphaGo недавно. У нас есть как supervise, так и n-supervise learning. Ну и image classification. Это то, что мы своими силами пытаемся покрыть. Модели все 16-17-го года, максимально хорошие.

    Естественно, интересно сравнивать их на разных платформах. Мы смотрим на TensorFlow, CNTK, MXNet. Не буду показывать сравнительные результаты, мы еще не все закончили, но это то, что входит в наши планы. Мы берем модель на TensorFlow максимально эффективную, по-честному сравниваем ее с CNTK или MXNet. Это значит, что мы выбираем все значения гиперпараметров, которые в нашей власти, одинаково. И там есть параметры, которые есть только в одном фреймворке, тогда ты выбираешь оптимально для этой сетки.

    Смотрим на различное железо. Безусловно можно использовать GPU. На мой взгляд, использование CPU для большинства этих сеток неэффективно. Вариант либо использовать, если ты в академии, либо GPU, либо FPGA, потому что ISAC нам никто не даст в прямой доступ из Google. Может, потом они поделятся виртуальными машинами с TensorFlow, но пока это время не пришло для большинства.

    Мы используем разные метрики: производительность, энергоэффективность, насколько хорошо эти сетки тренируются, это тоже важно, нужно сравнивать их по-честному. Все тренируются от разного количества.

    Очень большое количество запросов, в том числе из компаний, было о том, что все хотели понять, куда память девается. Помните, мы делали профилировщик, который показывал, куда ушла память на CNTK. Его сделать ушло почти месяц. Не так просто отследить, куда какие структуры данных ушли. Если взять TensorFlow и поискать, где они аллоцируют память, у тебя будет 2000 мест, где аллоцируют память, а куда потом уходят, через какой пул, отследить достаточно сложно. И никаких хороших профилировочных тулз нет. Мы их потихоньку делаем, мы сделали для CNTK, для MXNet. TensorFlow очень удобный для использования фреймворк, но очень тяжелый для изменений. Там отследить все тяжело, но мы пытаемся и там потихоньку вместе с Google сделать… Ты запустил свою сетку на тренировку, мы сразу можем показать, куда твоя память ушла, на какие структуры данных, и ты можешь понять, как это оптимизировать.

    Покажу пару интересных примеров того, что мы нашли.


    Я анонсировал, что мы сравнивали TensorFlow с MXNet на моделях. Оригинальная модель, одна называется NMT из Google, а другая Sockeye. У них разные параметры, но чтобы их по-честному сравнить, мы взяли у них одинаковое количество LSTM уровней, одинаковый размер batch, то есть все гиперпараметры одинаковые, одинаковый алгоритм тренировки.

    Что интересно, мы сами удивились, этот график показывает тренировочные шаги, мини-эпохи, и по Y у нас идет blue score. Это метрика, которая говорит, насколько хорошо мы тренируемся при переводе. Мы смотрим, насколько близко мы переводим к человеку. В данном случае используется перевод с английского на вьетнамский.

    Что можно заметить? Модели разные, но если смотреть только на тренировочные шаги, забыть про время, то для blue score порядка 20 они тренируются практически идентично. Наша гипотеза в том, что если ты правильно подобрал все гиперпараметры, математика там очень одинаковая. Они используют один и тот же SGD. Да, он описан разными людьми, какие-то флуктуации возможны, но он ведет себя очень похоже на практически максимальный.

    После этого они немного по-разному ведут, потому что у них learning rate очень сильно меняется, у каждого по-своему, поэтому дальше они расходятся. Но вначале тренировка с точки зрения математики и вычислений у них очень одинаковая. И казалось бы, это означает, что разницы нет, но на самом деле, если посмотреть на время, то она тренируется в два с половиной раза быстрее, чем другая. И связано это с тем, что TensorFlow на данный момент значительно лучше оптимизирован для LSTM уровней. Он использует хорошую вариацию CUDNN, а MXNet использует свою библиотеку для использования LSTM. То есть математически они делают одно и то же, но производительность может отличаться в разы.

    Мы проводили анализ, один из моих студентов этим сейчас занимается, насколько же хороша эффективность того же TensorFlow, вроде как он в два с половиной раза эффективнее MXNet. Выяснилось, если посмотреть на GPU, как эффективно LSTM на нем запущен, только порядка 30% ядер GPU используется. 70% вообще простаивают. И те, что используются, не все математические плюсики и умножения используют эффективно. То есть эффективность даже на TensorFlow порядка 10-15%, не выше. И при этом он в два с половиной раза быстрее, чем MXNet. То есть резервы там еще очень солидные.

    Мы работаем над улучшением LSTM, но и другие тоже работают, Nvidia анонсировали новую версию CUDNN с неделю назад, на которой LSTM должен намного ускориться. Как всегда бывает при маркетинге, используются всякие хитрые трюки. Если посмотреть на результаты пресс-релиза, что их СEО делал, там сказано, что CUDNN в 2,5 раза теперь быстрее на LSTM. Но если внимательно посмотришь на график, там сравнивается «паскалевская» карточка P100 на CUDNN 8 версии, и «вольтовская» карточка 100 на CUDNN 9 версии. Две карточки между собой сами по себе в два раза по скорости отличаются. То есть ускорение там в основном не за счет библиотеки, а за счет карточки. Но при маркетинге обычно такие детали игнорируются.


    Также мы активно исследуем reinforcement learning, там вообще никаких бенчмарков нет. Мы потихоньку разбираемся, что сейчас люди публикуют на ведущих конференциях типа NIPS или ICLR, и эти модели стараемся использовать. Одна из проблем, что очень часто machine learning исследователи могут это на MATLAB сделать и не заморачиваться делать это на потенциально нормально сети. И потом все их гиперпараметры, которые они натюнили в свои сетки, они недоступны. Ты берешь их сетку, как написано в статье, начинаешь тренировать, а ничего не тренируется. И на это уходит колоссальное количество времени. Причем такое случается даже с ребятами из Google, но по крайней мере, они хотя бы на вопросы быстро отвечают.

    Очень часто все эти гиперпараметры, ты чуть-чуть один параметр в два раза поменял здесь и там, и сетка может начать тренироваться. Очень много интересного происходит.


    Смотрим на reinforcement learning, сравниваем MXNet с TensorFlow, строим интересные графики, с этим связанные.

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

    На самом деле есть интересная идея, которую я еще в своей диссертации начал пробовать, и недавно попробовал на реальных приложениях: можно с некоторыми алгоритмами продолжать запускать код, не разжимая их. Что это значит? Сейчас у нас есть хеш, память, процессор. Если здесь все сжато, прежде чем что-то делать, вначале надо распаковать, потом снова запаковать и уложить обратно. И это как налог на вычисления, все время приходится платить. Например, Google говорил, что компрессия и декомпрессия в их облачных вычислениях может достигать 5-7%.


    Всего cloud tax у них достигает 20–25%, это пересылка данных, копирование, компрессия и шифрование. На любое вычисление, что ты делаешь в Google Cloud, 25% — это побочные вычисления, которые с твоими вычислениями никак не связаны.

    И часть этой проблемы — компрессия. Вопрос, можно ли извлечь какой-то бонус из того, что данные у тебя в сжатом виде? И не только избежать декомпрессии, а может, даже сделать лучше, чем в начале.


    Я взял пример из БД, его можно легко и быстро описать. В БД часто есть массив данных. Или, если мы говорим про стрим-процессинг, новые данные могут постоянно идти с какого-то количества серверов. И есть какие-то ключи, по которым хранятся какие-то значения. Обычно у тебя БД могут быть в виде колонок или строк. Если локальность и производительность тебе важна, ты делаешь большое количество поисков, то ты хранишь все значения отдельно от ключей, потому что потом по ним идешь. Например, если ты написал запрос where value = 10, тебе по каждой value надо идти. И если ты хочешь хорошо использовать память, лучше хранить их вместе.

    У нас есть массив таких значений, которые занимают 4 байта в памяти.


    Допустим, мы применили один из алгоритмов, про который я рассказывал, Base+Delta Encoding, у него какая-то метадата. Скажем, он хорошо сжал эти данные до 1 байта, в четыре раза. Все прекрасно.

    Допустим, в этих данных надо поискать что-то. Раньше, когда не было компрессии, что ты делал? Если ты ищешь что-то в массиве данных, тебе надо линейное количество сравнений произвести, чтобы найти эти данные. Если стандартно поступить с компрессией, то надо их разжать, затратить какую-то энергию, и опять сделать n четырехбайтовых сравнений. Но если внимательно посмотреть, то преобразования, которые определенные виды компрессии делают, сохраняют очень важные данные о качестве. Пусть данные и меняются, но при Base+Delta Encoding порядок сохраняется. Грубо говоря, сравнение двух величин не изменится, если сравнить в виде Base+Delta Encoding.

    Что это значит? Когда я ищу значения, то не беру этот массив по большому массиву, а преобразовываю в декомпрессионный вид. Возьму это значение и попробую его представить с имеющейся базой с определенным смещением.

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

    Если получается, что моя дельта после такого вычитания в 1 байт не влезает — все, поиск закончен. После этого ты знаешь, что она не может совпасть ни с одним из имеющихся значений, потому что все эти дельты — однобайтовые. Либо тебе потребуется одно сравнение, либо тебе не повезло и после этого надо сравнивать только дельты. А дельты однобайтовые. И если ты знаком с векторным программированием, ты можешь взять CND-инструкции Intel и засунуть туда больше этих сравнений. Вместо четырехбайтовых сравнений можно засунуть сразу четыре, потому что они всего 1 байт занимают, и в виде вектора их сравнить. Количество сравнений у тебя может уменьшиться в 4 или 8 раз.

    И это бонус, мелкая задержка. Мы можем на порядок увеличить количество сравнений и полностью избежать декомпрессии. Сейчас мы работаем над тем, как подобные трюки применить и для нейронных сетей, и для других. То есть мы хотим вообще не разжимать данные, а запускать код прямо на сжатых данных. Это все.
    • +41
    • 8,2k
    • 7
    Яндекс 303,92
    Как мы делаем Яндекс
    Поделиться публикацией
    Комментарии 7
    • НЛО прилетело и опубликовало эту надпись здесь
      • +1
        Дата-процессинг это не поиск? Окей…
        • 0
          Вы вообще текст читали, хотя бы первые 2 абзаца про то кто там докладчик?
        • +1
          Очень сильно. Прямо, вау.
          В том году, а может и в 2016 зимой, был в mail.ru на конференции по нейронным сетям, где представитель NVidia как раз рассказывал, что в железе и фреймворках была реализована оптимизация ускорения работы алгоритмов обучения за счёт использования целых чисел вместо плавучек. С потерей качества вычислений, но с приростом в скорости.

          Проблема, что когда ты эту проблему решил, тренировке это особо не помогает. Все методы, что они предложили для inference, такие как удалять какие-то веса, использовать квантизацию, фактически это взять 32-битный floating point, плавучку, и превратит ее в 16 бит, 8 или 4. Все эти механизмы для тренировки не работают, потому что теряется качество вычислений, и обычно используется какой-то алгоритм stochastic gradient descend, он просто перестает сходиться. Мы пересмотрели эти алгоритмы, никто из них к тренировке напрямую не применим. Также они не очень хорошо ложатся на современные GPU, все эти статьи предлагают сделать новый ISAC, что-то похоже на TPU или давайте используем в лучшем случае какую-то программируемую логику типа FPGA для этого. На GPU все эти техники не очень хорошо ложились


          И если ему верить, то есть группа задач, где возможна потеря точности вычислений. Не готов ввязываться в спор по этому вопросу. Помню, что тогда понял, как мало я знаю про нейронные сети. И сейчас такое же чувство.

          Спасибо за статью.
          • +2
            Нашел. Максим Милаков, рассказ про TenzorRT:

            Пересмотрел, да, там про Inference. В комментарии выше я был не прав.
          • 0
            Отличная статья. Спасибо, интересно)
            • 0
              Раз нашли решение хранить не байты, а биты, то сильно ли замедляет доступ к единичным битам? Может ли оказаться, что в будущих аппаратных нейросетях выгоднее будет ускорить такой доступ? Или там банально AND-ится?

              Очень интересная статья. Жаль, что не редактировалась (выдрали автоматические субтитры с ютюба?), поэтому читать было сложновато.

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

              Самое читаемое