Распределенное обучение и вычисления в сети. Доклад Яндекса

Все эксперты по сетям знакомы с принципом end-to-end, когда специфичные для конкретной задачи фичи реализовываются на конечных нодах, а промежуточные ноды должны только передавать данные, не взаимодействуя с ними. Но есть случаи, когда полезны вычисления внутри сети — с использованием устройств сети, занятых передачей трафика. Один из примеров таких задач — распределенный ML. В докладе Дмитрий Афанасьев дал краткое введение в особенности вычислений для распределенного ML, паттерны обмена данными и коллективные операции. Вторая половина доклада — о том, как редукция увеличивает производительность при обучении, и о некоторых реализациях.— Меня зовут Дмитрий Афанасьев, я сетевой архитектор Яндекса. И я сегодня расскажу про достаточно экзотические — по крайней мере пока — технологии. Но думаю, что они будут становиться менее экзотическими, и шансы с ними встретиться возрастают.
ub84xdkt7fhnoseeok5xcnq5p2g.jpeg

Итак, распределенное обучение для machine learning и вычисления в сети. Что я хочу рассказать? Сначала очень быстрое введение про то, что вообще такое нейросети, как они устроены, какие у них есть режимы функционирования и как они обучаются. Дальше специфика распределенного обучения, как устроен трафик при распределенном обучении и как он может ложиться на топологию сети Compute Implementations.

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

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

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

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


Итак, что такое нейросети? Слева на картинке у нас биологический нейрон с кучей своих запчастей. Справа у нас его математический аналог. Устроен просто. У него есть входы, на которые поступают какие-то сигналы. У этих входов есть веса. И еще есть смещение.
ayjcwwgqea4aushqws17eunmdgm.jpeg
Ссылка со слайда

Внутри происходит следующее — вектор входных активаций умножается на вектор весов и добавляется смещение, и все это подается на вход функции активации, которая выдает некоторый сигнал на выход. Собственно, все.

Функция активации для всех наших последующих упражнений с этой конструкцией должна быть монотонной, и очень желательно, чтобы она была как-то нормально дифференцируема, потому что на этом построено обучение.
zwba2-qsskj5qnfnt8izytve4iu.jpeg
Ссылка со слайда

Вот то же самое, здесь чуть более подробно про веса и так далее. Видно, что здесь вычисляется классическое скалярное возведение весов на входы. Добавляем смещение, отправляем результат дальше.
7dpbobhgd7ab4cptxzls2w6jzf4.jpeg

Из таких нейронов мы собираем собственно нейронную сеть. У нее есть входной уровень, который ничего не делает, это точки, в которые поступают какие-то входы. Дальше идут один или несколько слоев, которые производят обработку. Слои, которые не являются ни входными, ни выходными, мы называем скрытыми.
-1a3zfupa7gbhzggja6be6evdtw.jpeg

Слоев может быть много. Нетрудно заметить, что нейросети имеют такую слоистую структуру, то есть соединения внутри не произвольные, а мы производим обработку в одном слое, то, что получилось, подается на следующий и так далее. Это будет важно дальше, когда мы посмотрим на то, как они обучаются.
uk3ysiwaali_tl3ivcq0kmko9la.jpeg
Ссылка со слайда

Наконец, у нейросетей есть два основных режима функционирования или два способа с ними работать.

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

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

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

То есть мы прогоняем прямой путь, получаем результат. Смотрим на ошибку, на отклонение от того, что должно было получиться. И распространяем эту ошибку в обратном направлении по сети, чтобы скорректировать коэффициенты и уменьшить ошибку.
g-m1pom8xolx4rnand1gdnqvwla.jpeg

Таким образом, задача обучения — это задача минимизации. Вся нейросеть в итоге выдает нам результирующую функцию, и у нас есть результаты, которые мы хотели бы получить в идеале. Функция разницы между тем, что выдала нейросеть, и тем, что хотелось получить — это некоторая функция потерь, и мы ее хотим минимизировать.
s6fjg-i6tmxen4cp7a73fl-4fj4.jpeg

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

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

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

А когда мы проходим через весь набор входных семплов, это называется эпохой обучения, и дальше начинаем прогонять весь набор по новой.
02asmi0axdl3rm9hax_pdqncptk.jpeg

Чтобы вычислить градиент после того, как мы прогнали вычисления в прямом направлении, мы идем обратно по сети. Не будем смотреть уравнения детально, но это действительно обратное распространение, то есть эффективный метод расчета учитывает структуру сети.
aommhn4xrytk_vmmtnho6pgajxy.jpeg
vt4ixzx630kxkgfis58vsiraiuu.jpeg

Этот эффективный метод расчета называется алгоритмом обратного распространения или backpropagation algorithm.
h23ivg-krvats1admxwoayr8liw.jpeg

Он значительно эффективнее, чем если бы мы пытались считать градиент грубой силой за счет того, что он учитывает структуру сети и использует расчеты, которые были сделаны на разных этапах.
v5l_ejhl5mpmv9vf0ua4wfcw-ho.jpeg

Процесс можно представить как состоящий из следующих потоков распространения данных и прохождения вычислений. Здесь — прямое распространение вычислений через сеть.
kgw1fztb9as3smnfi8vb6c1ocr0.jpeg

А здесь — обратное. То есть, действительно, распространение данных и вычислений проходит в обратном порядке по сети и вычисления должны достичь тех точек в этом графе, тех весов и смещений, которые мы оптимизируем.
39e-vawfngrrua1gr5-2i1j-xn0.jpeg

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

Основано на туториале «Fundamentals of Scaling Out DL Training» Паулиуса Мицикевичуса (Nvidia) на симпозиуме HotChips

Теперь, когда мы примерно разобрались с тем, что происходит и как вообще устроена нейросеть внутри, мы можем посмотреть, что происходит с вычислениями.
2sjfb0gwom1ri3pgnrovtm7chpq.jpeg

Мы стартуем со случайно инициализированными весами и итерируем наши данные по minibatch за проход в прямом направлении, затем в обратном направлении и апдейтим веса.

Как я уже говорил, сейчас нас интересует обучение, потому что inference — более простая задача, в ней меньше вычислений, и не возникает таких больших объемов передачи данных.
wlylqunm4bgj3twnch9haxdwccw.jpeg

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

Если minibatch имеет размер больше единицы, то входом становится уже не вектор, а матрица. И каждый промежуточный результат тоже является матрицей. На практике так обычно и происходит, и, более того, это эффективно, потому что GPU заточен на работу с матрицами.
fhnrunyppwk7j7gouwqmg1bzuyg.jpeg

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

В итоге, когда мы прогнали вычисления в прямом направлении по сети, у нас получается какой-то результат. И у нас есть значения, которые мы хотели бы получить. Взяв и то, и другое, считаем функцию потерь от них. Получается результат потерь, который мы и хотим минимизировать в итоге.
fozdd5cg7y-46j2g_gbrmckvxpc.jpeg

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

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

По структуре вычисления очень похожи на то, что было в прямом направлении, но матрица оказывается транспонированной.
o7-fb-rnodjgnlzi27j-rwwmuym.jpeg

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

Важно, что оптимизации могут использовать значительно больше памяти, чем сама модель. Особенно методы, которые хранят дополнительное состояние, например момент. Это связано с желанием улучшить входимость и уменьшить, например, застревание градиентного спуска на каких-то участках.
cr-3ez4obi7xlovt181edhhb8nw.jpeg

Как выглядит стадия вычислений на каждом уровне? На прямом пути это матричные вычисления, где мы входные активации умножаем на матрицу весов и получаем выходные активации. На обратном — градиенты весов и активаций.

Наконец, мы берем матрицу весов, добавляем к ней матрицу апдейтов весов и получаем новые веса.

Важно, что эти вычисления можно распараллеливать, потому что, вообще говоря, вычислительная задача обучения нейросети — сложная, в ней очень много вычислений. Один из способов как-то ускорить процесс — распараллеливать, потому что иначе обучение некоторых сетей может занимать недели или месяцы, что очень печально. Хочется быстрее. Как можно параллелить? По-разному.
pttntuqhei7akoaa-t1ly_jkvmq.jpeg

Один из методов — Data Parallel, другой — Model Parallel. Внутри модельного параллелизма можно еще делать параллелизм внутри уровней и между уровнями.

Data Parallel — это когда мы разным нодам в сети отдаем вычисления на разных входных данных. То есть мы действительно разбиваем данные и считаем на разных нодах результат для разных сэмплов.

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

Каждая из моделей параллелизма подразумевает разную структуру потоков данных между вычислительными нодами. Мы будем фокусироваться на параллелизме данных, который устроен проще и весьма популярен.
rikaidsvbkkwussehizvxgacdtk.jpeg

Что происходит при распараллеливании по данным? Каждый worker получает полную копию всей нейросети и отвечает за вычисления только на части данных. На прямом проходе он считает входные активации для своей части minibatch, и никаких коммуникаций не происходит.

А вот на обратном проходе появляются коммуникации, потому что мы считаем градиенты и активации для своей части minibatch, и мы считаем вклад для градиентов весов на основании своей порции minibatch, после чего все workers должны просуммировать свои вклады в апдейт весов. То есть каждый worker считает только свою долю. Чтобы посчитать истинную модификацию весов на этом minibatch, нужно просуммировать или редуцировать результат всех workers.
0c78lu9kwewuhiu4kzr8k0cwqce.jpeg

Как это будет выглядеть в разбивке на ноды по разным workers? Вот иллюстрация. Они все работают с одной и той же матрицей весов, но с разными входными данными.
oez3omyt-zjt2vfqdas1hi5vdsy.jpeg
На обратном проходе они тоже работают с разными данными, потом это все нужно собрать.
mv4whznrv86ipv2lw9jky8adnlm.jpeg

Здесь мы подходим к самому интересному — к коммуникациям. Основная операция, которую мы здесь видим, — так называемый Allreduce. Это очень типовая операция в суперкомпьютерных вычислениях. В мире суперкомпьютеров так называемые групповые или коллективные операции очень типовые, с ними все знакомы. Но за пределами этого мира они достаточно малоизвестны.

Allreduce — одна из самых популярных из них. Ее смысл в том, что мы сначала редуцируем или собираем вместе, в данном случае суммируем промежуточные результаты с каждой ноды, а потом на все ноды распространяем получившийся результат.
ez-tbvtnuvyg7twsoz5zncuhol4.jpeg

Есть довольно много способов реализовать Allreduce. Эти способы имеют разные затраты по трафику, разные структуры обмена данными. Хуже всего наивный способ. Это когда каждый worker просто отдает свои результаты всем остальным, и каждый сам выполняет все суммирование. Никто так не делает.

К эффективным методам относятся кольцевые редукции — чуть позже покажу, как они выглядят — и так называемые One-shot reduction, то есть редукции за один проход, которые обычно используются в типологиях с коммутаторами, где все ноды видят других — достижимых, хоть и не обязательно через один ход. Такие типы редукции могут использовать иерархические оптимизации. 
6_odzpaikyupzw5kq2whkj8-gqq.jpeg

Есть довольно большой коммуникационный overhead, который растет вместе со степенью параллелизма. И есть способы его оптимизации, на которые мы дальше посмотрим.
c5lcrwyquxbnj24-_vsocb2rfcu.jpegj2rvazlw3hmvz5fy60hmwfnimxy.jpegzujyir46xaksr3yohw1kxmr8bwe.jpeg

Что касается модельного параллелизма внутри уровня и между уровнями, я просто привожу картинки о том, как это устроено с точки зрения разделения задач. Но мы не будем в них углубляться.
fduoi39obspd3boodrk-v-mjacq.jpeg

Почему важно оптимизировать коммуникации между нодами? Это графики, снятые с тестового кластера с тестовой нагрузкой. Синее — GPU power, то есть энергопотребление GPU, которое является довольно хорошим индикатором того, насколько занят GPU и насколько он активно считает. 

Другой график — обмен данными. Здесь видно, что наш дорогущий GPU простаивает, пока идет обмен и синхронизация данных. В жизни очень хотелось бы минимизировать этот простой. 
dob5wziermzf1beslulongjru60.jpeghlc67dvim_r56qvr3rz4bwvtsdu.jpeg


Как устроены потоки трафика и как они связаны с интерконнектами? Небольшое напоминание про коллективные или групповые операции, какие они бывают.
5labafclmruumns1rhdjhajqhwi.jpeg
Ссылка со слайда

Бывают операции broadcast, когда один набор данных раздается всем. Бывает scatter, когда есть набор данных, и мы по кусочку раздаем его остальным нодам. Gather — строго обратное, собираем данные вместе по кусочкам. 

И reduction, когда мы собираем куски и вычисляем из них что-то новое. Например, суммируем. Хотя редукция не обязана быть суммированием, в случае нейросети она является суммированием. Важная свойство редукции — то, что размер блока данных после редукции не меняется. То есть мы собрали по вектору какой-то размерности с каждой ноды, просуммировали, получилось не в n раз больше, а столько же данных, но в которых уже учтены промежуточных результаты.
-u_tl5xtmkikmt7fdqorl1w0c3u.jpeg
Ссылка со слайда

Как я уже говорил, наивная реализация One-shot reduction за один проход не эффективна.
mri3xnhjd01swc88e-lfsf8uink.jpeg
Ссылка со слайда

Есть другие реализации, например так называемый кольцевой Allreduce.
kje1plee0kox9vcuqm0iwldtnli.jpeg

Он сводится к тому, что на первой фазе ноды по замкнутому кольцу коммуницируют промежуточный результат друг другу. Причем со смещением.
hcorrmfdbexpumdtzusjlgnlva8.jpeg

В итоге каждая нода получает свою завершенную часть результата.
ziiybxw7w5m6-l0rdo_he6c3liw.jpeg

И потом реплицирует на все остальные.
5xwhn2hojw47m-tgozlhgn88bpy.jpegkuhubsyxusvc-zjgynug1r47vgm.jpeg
Ссылка со слайда

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

Потому что с чем мы обычно сталкиваемся в IP-сетях? IP сети, которые мы строим, — всё это коммутируемые или indirect-сети. Ноды в них подключены не напрямую к другим нодам, если мы говорим про вычислительные ноды, а к коммутаторам. И только через них видят другие ноды.

А есть еще интерконнекты/сети, которые называются прямыми, direct, или же point-to-point. Это когда в одной ноде совмещены и линки для коммуникации с соседями, и вычислительные элементы. Многие суперкомпьютерные сети и многие кластеры для high performance-компьютинга исторически так и выглядят. Это неспроста. 
ouvcgksyooaqg-s1kqsjxedigwq.jpeg
Ссылка со слайда

Дело в том, что многие коллективные операции очень хорошо ложатся на такую топологию интерконнекта. Если мы говорим конкретно про машинное обучение, то например, Google, как известно, делает свои тензорные процессоры для машинного обучения. А еще он делает на них кластеры. И в этих кластерах топология — как раз 2-D torus, что позволяет кольцевой Allreduce непосредственно отобразить на топологию этого кластера. 
kqhyjnhraf8mzmrgyhfuhqqgmxg.jpeg
Ссылка со слайда

Но конечно, не все могут делать свои акселераторы и специализированные интерконнекты для них. Что же можно сделать? А если бы свитчи тоже умели что-то считать?


Итак, вычисление в сетях на сетевых элементах.
tvycoawhbnlnlgbdj4hxfa7t44m.jpeg
Ссылка со слайда

Мы посмотрим на две реализации. Первая — SHARP, Scalable Hierarchical Aggregation and Reduction Protocol. Это реализация вычислений в сети от компании Mellanox, теперь Nvidia.
e4akj91b2sh4m9a8khza_v6_1bs.jpeg

И это production ready-реализация. Она оптимизирует все ту же задачу расчета редукций. Один из способов такого расчета — построение дерева, по которому могут идти редукции. Это логическое дерево. Оно отображается на топологию сети. Если мы говорим про индиректную или switched-сеть, то отображаться оно может довольно неудачно. Например, вызывая перегрузку на определенных линках.
bxmvhus3z-fgsopmhxyzjfldhsk.jpeg

Пример одной из реализации Allreduce — Recursive Doubling. Он происходит в несколько этапов.
gjmvedhjdn0i6e8v_6jlu2wxhpe.jpeg

Что делает SHARP? Во-первых, SHARP основан на том, что внутри коммутаторов, в данном случае это InfiniBand-коммутаторы, реально есть логические устройства, которые могут что-то считать. Считать им нужно не так много, в данном случае достаточно суммировать. Но они могут работать со всеми популярными типами данных, которые нужны в HPC и в машинном обучении.

В случае с SHARP у нас также строится дерево агрегации, это логическое дерево, наложенное на физическую топологию сети.
f5cis0znzlc_5l1_lohpmlaa88u.jpegxl6yt0ccutjge6ceuqz_6xq7gdi.jpeg

Что происходит? У нод появляются промежуточные расчеты данных. Они отсылают эти данные по дереву, и при распространении вверх по этому дереву происходит редукция. У нас есть switch снизу. Ему несколько нод присылают свои данные для редукции. Он редуцирует эти наборы данных до одного. 

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

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

И поскольку здесь речь идет об InfiniBand, у которого надежный транспорт, и есть реализация надежного мультикаста, это очень эффективная операция. Этот номер не прошел бы так легко в произвольной пакетной сети, где есть потери. Нам пришлось бы подставлять некоторое количество подпорок. Но в случае с InfiniBand это работает очень неплохо. 
01lymgbqahqxy4lruzdq3jn_3de.jpegu_bd-synjfwvwuag0khrlabalts.jpeg

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

То есть мы не просто сделали полезную работу, выполнив операцию редукции. Мы еще и значительно уменьшили объем передаваемых дальше данных.
o4yrqnvpwuj7sk2jtr3r-j0yd3g.jpeg

На графике видно, как использование SHARP влияет на использование операции Allreduce. Влияет не только на полосу, а еще и на Latency. Видно, что Latency остается примерно постоянной. Конечно, на самом деле нет. Latency является логарифмической. Потому что если посмотреть на то, что было здесь, это на самом деле подмножество внутри сети. И Latency при использовании SHARP зависит в основном от количества уровней сети, а не от количества нод, принимающих участие в вычислениях. 

С учетом того, что сейчас доступны свитчи довольно большого радикса, это позволяет иметь довольно хорошо ограниченную сверху Latency даже в очень больших кластерах.
uc1vvowl5-e5yxzt00e5owee11q.jpeg

Но улучшается не только полоса и задержка. 

Есть данные тестов с обучением различных нейросетей. В зависимости от того, какие модели мы обучаем, сколько нод в кластере, и от другой специфики, есть выигрыш от единиц процентов до весьма заметных 10–16% на некоторых моделях. 

Это очень много с учетом того, какими цифровыми молотилками являются современные GPU и как соотносится стоимость самой вычислительной части нод GPU со стоимостью интерконнекта. 10% роста производительности могут пару раз окупить весь интерконнект.

sands.kaust.edu.sa/project/switchml

И еще одна реализация. Она не production ready, но куда более доступна, потому что подразумевает менее экзотическое оборудование. 

То есть в исследовательских целях она более доступна, и, вообще, больше шансов до нее добраться, даже если у вас нет большого кластера. Это SwitchML.
k3n3mmaotlw4hfaxf9oajfbck4u.jpeg

Та же проблема — распределенное обучение, Data Parallel, worker посчитали свои промежуточные результаты.
u-j0-x1fmcqtx4icfa2xosnnmge.jpeg5btvyf71vhkfy9bllsnnqakouqg.jpeg

Их нужно собрать вместе, и мы хотим сделать это эффективнее.
af7vzsbuuntr4nbrja3a779rcos.jpeg

И хотим сделать это на switch.
1kloxbl37efhcgxrsepst7qv4q4.jpeg

На чем это можно было бы сделать? Тут мы вспоминаем, что у нас есть программируемые чипы от компании Barefoot, теперь Intel. У них нет поддержки операций с плавающей запятой и продвинутой арифметики, но в целом с числами они работать умеют. Выясняется, что на этом тоже можно собрать конструкцию для редукции в сети.
3whvpocnvmov3hrkif2d3vtlelo.jpeg

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

Switch же будет выполнять суммирование векторов, а также отслеживать, как заполняются слоты для вычислений. 
1mzzj_nvohwrhs5c4o7uhmvav9k.jpeg

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

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

Switch при помощи bitmap отслеживают, какие worker уже прислали свои данные для данного слота вычислений.
wsc6d8jtqnj5j6mom_zeijfud6k.jpeg

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

gdc3vg1e6xsqyp7avqth7n7pxrw.jpeg

Это тестовый кластер, на котором производились эксперименты.
elybtwxos35llmdhwlfyjw7a-n0.jpeg

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

Из экспериментов видно, что производительность не очень зависит от количества workers, в отличие от реализаций, не использующих вычисления на switch.
rhbujvqbf-kaqvrednvhwupovee.jpeg

SwithML достаточно неплохо себя ведет при разумных значениях количества потерь. 1% — для типовой сети это уже как-то очень много. Меньше 0,1% — это то, на что можно ориентироваться. Даже 0,1% — много в контролируемой среде.
ywzb6yq2oenlo1nipyi27b5kru8.jpeg

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

И про SHARP, и про SwitchML есть статьи и презентации. Они в открытом доступе, их можно посмотреть.

© Habrahabr.ru