[Анализ исходного кода] Сервер параметров машинного обучения ps-lite (1) ----- PostOffice

машинное обучение глубокое обучение

0x00 сводка

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

Эта статья является первой в серии серверов параметров, в которой представлена ​​общая конструкция PS-lite и базовый модуль Postoffice.

0x01 Сводка

1.1 Что такое сервер параметров

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

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

  1. Подготовка данных: процесс обучения получает вес и данные (данные + метка);
  2. Расчет вперед: в процессе обучения используются данные для расчета вперед, чтобы получить потерю = f (вес, данные и метка);
  3. Обратный вывод: взяв обратную производную от потерь, можно получить производную grad = b(потеря, вес, данные и метка);
  4. Обновить вес: вес -= град * лр;
  5. Прийти к 1 и перейти к следующей итерации;

Если мы используем обучение сервера параметров, мы можем выполнить вышеуказанные шаги следующим образом:

  1. Доставка параметров: сервер сервера параметров отправляет вес каждому воркеру (или воркер вытягивает его сам), а воркер является клиентом сервера параметров;
  2. Параллельные вычисления: каждый работник выполняет свои собственные вычисления (включая прямое вычисление и обратное вычисление);
  3. Коллекция градаций: сервер сервера параметров получает градацию от каждого рабочего процесса и завершает слияние (или рабочий процесс сам ее подталкивает);
  4. Обновить вес: сервер сервера параметров сам применяет градацию к весу;
  5. Прийти к 1 и перейти к следующей итерации;

Подробности следующие:

          FP/BP    +--------+  Gather/Sum                                       FP/BP            +-------+    Gather/Sum
      +----------> | grad 1 +------+                                    +----------------------> |grad 2 +-----------+
      |            +--------+      |                                    |                        +-------+           |
+-----+----+                       v                     +--------------+-------------------+                        v
|          |                   +---+----------+  Update  |                                  |                 +------+-----+ Update   +------------------+
| weight 1 |                   | total grad 1 +--------->+weight 2 = weight 1 - total grad 1|                 |total grad 2+--------> |weight 2 = ...... |
|          |                   +---+----------+          |                                  |                 +------+-----+          +------------------+
+-----+----+                       ^                     +--------------+-------------------+                        ^
      |   FP/BP    +--------+      |                                    |       FP/BP            +-------+           |
      +----------> | grad 2 +------+                                    +----------------------> |grad 2 +-----------+
                   +--------+  Gather/Sum                                                        +-------+    Gather/Sum

Телефон такой:

Следовательно, мы можем вывести роль каждого модуля в сервере параметров:

  • Сервер (Сервер): Храните параметры модели машинного обучения, получайте градиенты, отправленные клиентом, завершайте слияние и обновляйте параметры локальной модели.

  • Клиент (Клиент или Работник):

    • Получить последние параметры с сервера;
    • Рассчитать градиент относительно параметров обучения в соответствии с функцией потерь, используя данные обучения и прогнозные значения, рассчитанные на основе последних параметров;
    • Отправить градиент на сторону сервера;
  • Планировщик: управление серверными/клиентскими узлами, полная синхронизация данных между узлами, добавление/удаление узлов и другие функции.

1.2 Прослеживаемость истории

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

До появления серверов параметров большинство алгоритмов распределенного машинного обучения реализовывались посредством периодической синхронизации, например, all-reduce для коллективной связи или шаг сокращения для систем, подобных map-reduce. Но есть две проблемы с регулярной синхронизацией:

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

Итак, когда появился async sgd, кто-то придумал сервер параметров.

Концепция сервера параметров возникла из параллельной структуры LDA, предложенной Алексом Смолой в 2010 году. Используя распределенный Memcached в качестве хранилища для хранения общих параметров, он обеспечивает эффективный механизм синхронизации параметров модели между разными Worker в распределенной системе, и каждому Worker нужно сохранять только данные, полученные при расчетах. достаточно, и это позволяет избежать остановки и синхронизации всех процессов в один момент времени. Однако независимая пара kv требует больших затрат на связь, а серверную сторону сложно программировать.

Второе поколение Джеффа Дина из Google также предложило первое поколение решения Google Brain: DistBelief. DistBelief распределяет и хранит огромные модели глубокого обучения в глобальном сервере параметров, а вычислительные узлы передают информацию через сервер параметров, что хорошо решает задачу распределенного обучения алгоритмов SGD и L-BFGS.

Затем есть сервер параметров, разработанный группой DMLC Ли Му. Согласно документу, сервер параметров относится к серверу параметров третьего поколения, который имеет более общий дизайн. Архитектура включает группу серверов и несколько рабочих групп.

1.3 Структура диссертации

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

Объясните каждый модуль в общей архитектуре на схеме:

  • менеджер ресурсов: распределение ресурсов и менеджер. Сервер параметров использует существующие в отрасли системы управления ресурсами, такие как пряжа и k8s.

  • Обучающие данные. Десятки миллиардов обучающих данных обычно хранятся в распределенной файловой системе (такой как HDFS), и менеджер ресурсов равномерно распределяет их между всеми рабочими процессами.

  • Узлы сервера параметров разделены на группу серверов и несколько рабочих групп.

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

    • Каждый сервер в группе серверов отвечает только за некоторые назначенные им глобальные общие параметры (серверы совместно поддерживают глобальный общий параметр), и здесь реализован общий оптимизатор.
    • Серверы взаимодействуют друг с другом для резервного копирования/миграции параметров.
    • В группе серверов есть узел диспетчера серверов, который отвечает за поддержание согласованности метаданных сервера, таких как статус узла и назначение параметров. Как правило, никакой логики нет, только когда узел сервера присоединяется или выходит, вносятся некоторые коррективы для поддержания согласованного хэша.
  • рабочая группа: Рабочие, запрошенные в задаче обучения, используемые для прямого процесса и вычисления градиента.

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

При распределенном вычислении градиентов поток данных системы выглядит следующим образом:

Роль каждого шага на рисунке:

  1. Рабочий узел вычисляет градиент веса модели на основе образцов в пакете;
  2. Рабочий отправляет градиент на сервер в виде ключ-значение;
  3. Сервер выполняет градиентные обновления весов модели в соответствии с указанным оптимизатором;
  4. Рабочий загружает с сервера веса последней модели;

Два приведенных выше рисунка основаны на их исходном коде. ps-lite — более поздняя версия кода, поэтому некоторые функции в ps-lite не предусмотрены.

1.4 История разработки PS-Lite

Я нашел в Интернете немного истории разработки PS-Lite, чтобы увидеть их эволюцию.

Первое поколение — это параметры, разработанные и оптимизированные для конкретных алгоритмов (таких как логистическая регрессия и LDA) для решения крупномасштабных задач промышленного машинного обучения (десятки миллиардов примеров и функции размера данных 10–100 ТБ).

Позже были предприняты попытки создать общую структуру с открытым исходным кодом для алгоритмов машинного обучения. Проект находится по адресу dmlc/parameter_server.

Ввиду растущего спроса со стороны других проектов был создан ps-lite, который предоставляет чистый API для передачи данных и облегченную реализацию. Реализация основана на dmlc/parameter_server, но с рефакторингом средства запуска заданий, файлового ввода-вывода и кода алгоритма машинного обучения, такого как dmlc-core и wormhole, для разных проектов.

API и реализация были подвергнуты дальнейшему рефакторингу по сравнению с версией 1 на основе уроков, извлеченных во время разработки dmlc/mxnet. Основные изменения включают в себя:

  • меньше зависимостей от библиотек;
  • Более гибкие пользовательские обратные вызовы для облегчения привязки к другим языкам;
  • Позвольте пользователям (например, механизму зависимостей mxnet) управлять согласованностью данных;

1.5 система ps-lite в целом

ps-lite на самом деле представляет собой структуру для реализации Paramter Server, в которой определенные политики, связанные с обработкой параметров, должны реализовываться самими пользователями.

Сервер параметров содержит три роли: Worker, Server, Scheduler. Конкретные отношения следующие:

Конкретные ролевые функции:

  • рабочий (рабочий узел):Некоторые из них выполняют конвейер данных, расчеты вперед и градиента, передают градиент веса модели на серверный узел в форме ключ-значение и извлекают последний вес модели из узла сервера;
  • сервер (сервисный узел):Несколько, ответственных за реагирование на push- и pull-запросы воркера, хранение, поддержку и обновление весов моделей для использования каждым воркером (каждый сервер поддерживает только часть модели);
  • планировщик (управляющий узел):В системе только один. Отвечает за мониторинг пульса всех узлов, выделение идентификатора узла и установление связи между рабочими и серверами, его также можно использовать для отправки управляющих сигналов другим узлам и сбора их хода.

Преимущества внедрения планировщика заключаются в следующем:

  • Внедрение модуля планировщика сформирует более классическую трехролевую распределенную системную архитектуру, роли и обязанности рабочих и серверов остаются неизменными, а модуль планировщика имеет больше возможностей:

    • Возьмем на себя взаимодействие только с системой планирования ресурсов более низкого уровня Prajna (по аналогии с пряжей, мезосом);
    • Добавлены дополнительные функции мониторинга работоспособности и контроля рабочих процессов и серверов;
  • Еще одним преимуществом внедрения модуля планировщика является то, что он оставляет место для реализации параллелизма моделей;

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

Студентов, знакомых с распределенными системами, может волновать одноточечная проблема модуля планировщика, которую лучше решить с помощью протоколов paxos, таких как raft и zab.

1.6 Базовые модули

Вот некоторые основные модули системы ps-lite:

  • Environment: класс переменной среды одноэлементного режима, который передаетstd::unordered_map<std::string, std::string> kvsНабор kvs поддерживается для сохранения всех имен и значений переменных среды;

  • PostOffice: глобальный класс управления в одноэлементном режиме, узел имеет PostOffice в течение своего времени существования и полагается на членов своего класса для управления узлом;

  • Van: Коммуникационный модуль, отвечающий за сетевое взаимодействие с другими узлами и фактическую отправку и получение сообщений. PostOffice имеет члена Van;

  • SimpleApp: Родительский класс KVServer и KVWorker, предоставляет простые функции Request, Wait, Response, Process, KVServer и KVWorker переписывают эти функции в соответствии со своими задачами;

  • Customer: каждый объект SimpleApp содержит член класса Customer, который должен быть зарегистрирован в PostOffice. Этот класс в основном отвечает за:

    • Отслеживайте ответы на сообщения, отправленные SimpleApp;
    • Поддерживать очередь сообщений узла для получения сообщений для узла;
  • Node: Информационный класс, в котором хранится соответствующая информация об этом узле.Каждый узел может быть однозначно идентифицирован по имени хоста + порту.

0x02 Запуск системы

2.1 Как начать

Как видно из примера в исходниках, всю систему можно запустить с помощью предоставленного ps-lite скрипта local.sh, где test_connection — скомпилированная исполняемая программа.

./local.sh 2 3 ./test_connection

2.2 Сценарий запуска

Конкретный код local.sh выглядит следующим образом. Обратите внимание, что в сценарии оболочки есть три смены, поэтому в сценарии всегда используется $1.

Для нашего примера параметр скрипта соответствует

  • DMLC_NUM_SERVER равно 2;
  • DMLC_NUM_WORKER равно 3;
  • бин это ./test_connection;

Как видно из скрипта, этот скрипт делает две вещи:

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

  • Запускайте несколько разных ролей локально. Таким образом, ps-lite использует несколько разных процессов (программ) для совместной работы для выполнения работы.

    • Сначала запустите узел планировщика. Это делается для того, чтобы зафиксировать количество серверов и рабочих, а узел планировщика управляет адресами всех узлов.
    • Запустите узел Worker или Server. Каждый узел должен знать IP-адрес и порт узла планировщика. Подключитесь к узлу планировщика при запуске, привяжите локальный порт и зарегистрируйте свою информацию в узле планировщика (сообщите свой собственный IP, порт).
    • Планировщик ожидает регистрации всех рабочих узлов, присваивает им идентификатор и передает информацию об узле (например, рабочему узлу необходимо знать IP-адрес и порт узла-сервера, а узлу-серверу необходимо знать IP-адрес и порт рабочего узла). порт). На данный момент узел планировщика готов.
    • После того, как Worker или Server получает информацию, отправленную планировщиком, он устанавливает соединение с соответствующим узлом. На данный момент рабочий или сервер готов и будет официально запущен.

детали следующим образом:

#!/bin/bash
# set -x
if [ $# -lt 3 ]; then
    echo "usage: $0 num_servers num_workers bin [args..]"
    exit -1;
fi
​
# 对环境变量进行各种配置,此后不同节点都会从这些环境变量中获取信息
export DMLC_NUM_SERVER=$1
shift
export DMLC_NUM_WORKER=$1
shift
bin=$1
shift
arg="$@"
​
# start the scheduler
export DMLC_PS_ROOT_URI='127.0.0.1'
export DMLC_PS_ROOT_PORT=8000
export DMLC_ROLE='scheduler'
${bin} ${arg} &
​
​
# start servers
export DMLC_ROLE='server'
for ((i=0; i<${DMLC_NUM_SERVER}; ++i)); do
    export HEAPPROFILE=./S${i}
    ${bin} ${arg} &
done
​
# start workers
export DMLC_ROLE='worker'
for ((i=0; i<${DMLC_NUM_WORKER}; ++i)); do
    export HEAPPROFILE=./W${i}
    ${bin} ${arg} &
done
​
wait

2.3 Пример программы

Мы все еще используем официальный пример, чтобы увидеть.

ps-lite использует язык C++, в котором рабочий процесс, сервер и планировщик используют один и тот же набор кодов. Это заставит студентов, привыкших к Java и python, чувствовать себя некомфортно, и всем нужно адаптироваться к этапу.

Для этого примера программы поначалу будет непонятно, почему каждый раз при запуске программы в коде запускаются планировщик, воркер и сервер? На самом деле, как видно из комментариев ниже, конкретное исполнение определяется переменными окружения. Если на этот раз для переменной среды установлено значение server, планировщик и рабочий процесс не будут запущены.

#include <cmath>
#include "ps/ps.h"
​
using namespace ps;
​
void StartServer() {
  if (!IsServer()) {
    return;
  }
  auto server = new KVServer<float>(0);
  server->set_request_handle(KVServerDefaultHandle<float>()); //注册functor
  RegisterExitCallback([server](){ delete server; });
}
​
void RunWorker() {
  if (!IsWorker()) return;
  KVWorker<float> kv(0, 0);
​
  // init
  int num = 10000;
  std::vector<Key> keys(num);
  std::vector<float> vals(num);
​
  int rank = MyRank();
  srand(rank + 7);
  for (int i = 0; i < num; ++i) {
    keys[i] = kMaxKey / num * i + rank;
    vals[i] = (rand() % 1000);
  }
​
  // push
  int repeat = 50;
  std::vector<int> ts;
  for (int i = 0; i < repeat; ++i) {
    ts.push_back(kv.Push(keys, vals)); //kv.Push()返回的是该请求的timestamp
​
    // to avoid too frequency push, which leads huge memory usage
    if (i > 10) kv.Wait(ts[ts.size()-10]);
  }
  for (int t : ts) kv.Wait(t);
​
  // pull
  std::vector<float> rets;
  kv.Wait(kv.Pull(keys, &rets));
​
  // pushpull
  std::vector<float> outs;
  for (int i = 0; i < repeat; ++i) {
    // PushPull on the same keys should be called serially
    kv.Wait(kv.PushPull(keys, vals, &outs));
  }
​
  float res = 0;
  float res2 = 0;
  for (int i = 0; i < num; ++i) {
    res += std::fabs(rets[i] - vals[i] * repeat);
    res2 += std::fabs(outs[i] - vals[i] * 2 * repeat);
  }
  CHECK_LT(res / repeat, 1e-5);
  CHECK_LT(res2 / (2 * repeat), 1e-5);
  LL << "error: " << res / repeat << ", " << res2 / (2 * repeat);
}
​
int main(int argc, char *argv[]) {
  // start system
  Start(0); // Postoffice::start(),每个node都会调用到这里,但是在 Start 函数之中,会依据本次设定的角色来不同处理,只有角色为 scheduler 才会启动 Scheduler。
  // setup server nodes
  StartServer(); // Server会在其中做有效执行,其他节点不会有效执行。
  // run worker nodes
  RunWorker(); // Worker 会在其中做有效执行,其他节点不会有效执行。
  // stop system
  Finalize(0, true); //结束。每个节点都需要执行这个函数。
  return 0;
}
​

Среди них KVServerDefaultHandle — функтор, который используется для обработки запроса, полученного сервером от воркера, следующим образом:

/**
 * \brief an example handle adding pushed kv into store
 */
template <typename Val>
struct KVServerDefaultHandle { //functor,用与处理server收到的来自worker的请求
    // req_meta 是存储该请求的一些元信息,比如请求来自于哪个节点,发送给哪个节点等等
    // req_data 是发送过来的数据
    // server 是指向当前server对象的指针  
  void operator()(
      const KVMeta& req_meta, const KVPairs<Val>& req_data, KVServer<Val>* server) {
    size_t n = req_data.keys.size();
    KVPairs<Val> res;
    if (!req_meta.pull) { //收到的是pull请求
      CHECK_EQ(n, req_data.vals.size());
    } else { //收到的是push请求
      res.keys = req_data.keys; res.vals.resize(n);
    }
    for (size_t i = 0; i < n; ++i) {
      Key key = req_data.keys[i];
      if (req_meta.push) { //push请求
        store[key] += req_data.vals[i]; //此处的操作是将相同key的value相加
      }
      if (req_meta.pull) {  //pull请求
        res.vals[i] = store[key];
      }
    }
    server->Response(req_meta, res);
  }
  std::unordered_map<Key, Val> store;
};
​

0x03 Postoffice

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

  • Все три роли узла полагаются на Postoffice для управления, и каждый узел имеет одиночный PostOffice в течение всего срока его службы.

  • Как мы уже говорили, особенность ps-lite заключается в том, что воркер, сервер, планировщик используют один и тот же набор кода, как и Postoffice, поэтому лучше описать его отдельно.

  • Со стороны Планировщика, как следует из названия, Почтовое отделение — это почтовое отделение, которое можно рассматривать как адресную книгу, центр управления, в который записывается информация обо всех узлах в системе (система, состоящая из планировщика, сервера и рабочего узла). коллективно). Конкретные функции заключаются в следующем:

    • Поддерживается объект Van, который отвечает за подтягивание, связь и управление командами всей сети, например, добавление узлов, удаление узлов, восстановление узлов и т. д.;
    • Отвечает за управление базовой информацией всего кластера, такой как получение количества рабочих и серверов, управление адресами всех узлов, получение распределения функций на стороне сервера, обмен между рангами рабочих и серверов. и идентификатор узла, идентификатор роли узла и т. д.;
    • Отвечает за Барьерную функцию;
  • На стороне сервера/работника отвечает за:

    • Настройте некоторую информацию о текущем узле, например тип (сервер, рабочий) текущего узла, идентификатор узла и преобразование ранга рабочего/серверного узла в идентификатор узла.
    • Функция маршрутизации: отвечает за соответствующие отношения между ключом и сервером.
    • Барьерная функция;

Обратите внимание: все эти коды относятся к классу Postoffice, а не разделены на несколько модулей по ролям.

3.1 Определения

UML-диаграмма класса выглядит следующим образом:

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

Основные переменные следующие:

  • van_ : базовый коммуникационный объект;
  • customers_ : какие клиенты в данный момент находятся в этом узле;
  • node_ids_ : таблица сопоставления идентификаторов узлов;
  • server_key_ranges_ : объект диапазона ключей сервера
  • is_worker , is_server, is_scheduler_ : указывает тип этого узла;
  • heartbeats_ : объект сердцебиения узла;
  • барьер_done_ : Переменная синхронизации барьера;

Основные функции следующие:

  • InitEnvironment: Инициализировать переменные среды и создать объекты фургона;
  • Start : установить инициализацию связи;
  • Finalize : узел блокирует выход;
  • Управление: выход из состояния блокировки шлагбаума;
  • Барьер: войти в состояние блокировки барьера;
  • Обновить сердцебиение:
  • GetDeadNodes : Получить мертвые узлы в соответствии с heartbeats_;

детали следующим образом:

class Postoffice {
  /**
   * \brief start the system
   *
   * This function will block until every nodes are started.
   * \param argv0 the program name, used for logging.
   * \param do_barrier whether to block until every nodes are started.
   */
  void Start(int customer_id, const char* argv0, const bool do_barrier);
  /**
   * \brief terminate the system
   *
   * All nodes should call this function before existing.
   * \param do_barrier whether to do block until every node is finalized, default true.
   */
  void Finalize(const int customer_id, const bool do_barrier = true);
  /**
   * \brief barrier
   * \param node_id the barrier group id
   */
  void Barrier(int customer_id, int node_group);
  /**
   * \brief process a control message, called by van
   * \param the received message
   */
  void Manage(const Message& recv);
  /**
   * \brief update the heartbeat record map
   * \param node_id the \ref Node id
   * \param t the last received heartbeat time
   */
  void UpdateHeartbeat(int node_id, time_t t) {
    std::lock_guard<std::mutex> lk(heartbeat_mu_);
    heartbeats_[node_id] = t;
  }
  /**
   * \brief get node ids that haven't reported heartbeats for over t seconds
   * \param t timeout in sec
   */
  std::vector<int> GetDeadNodes(int t = 60);  
 private:  
 void InitEnvironment();  
  Van* van_;
  mutable std::mutex mu_;
  // app_id -> (customer_id -> customer pointer)
  std::unordered_map<int, std::unordered_map<int, Customer*>> customers_;
  std::unordered_map<int, std::vector<int>> node_ids_;
  std::mutex server_key_ranges_mu_;
  std::vector<Range> server_key_ranges_;
  bool is_worker_, is_server_, is_scheduler_;
  int num_servers_, num_workers_;
  std::unordered_map<int, std::unordered_map<int, bool> > barrier_done_;
  int verbose_;
  std::mutex barrier_mu_;
  std::condition_variable barrier_cond_;
  std::mutex heartbeat_mu_;
  std::mutex start_mu_;
  int init_stage_ = 0;
  std::unordered_map<int, time_t> heartbeats_;
  Callback exit_callback_;
  /** \brief Holding a shared_ptr to prevent it from being destructed too early */
  std::shared_ptr<Environment> env_ref_;
  time_t start_time_;
  DISALLOW_COPY_AND_ASSIGN(Postoffice);
}; 

3.2 Функция сопоставления идентификаторов

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

  • 1, 2 и 4 обозначают Scheduler, ServerGroup и WorkerGroup соответственно.
  • SingleWorker: ранг * 2 + 9; SingleServer: ранг * 2 + 8.
  • Любой набор узлов может быть идентифицирован одним идентификатором, равным сумме всех идентификаторов.

3.2.1 Концепция

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

  • Идентификатор узла — это уникальный идентификатор физического узла, который может однозначно соответствовать двойному кортежу хост + порт.

  • Группа узлов — это логическое понятие, каждая группа может содержать несколько идентификаторов узлов. В ps-lite есть три группы: группа планировщика, группа серверов, рабочая группа.

  • Идентификатор группы узлов — это уникальный идентификатор группы узлов.

    • ps-lite использует числа 1, 2 и 4 для обозначения Scheduler, ServerGroup и WorkerGroup соответственно. Каждое число представляет набор узлов, равный сумме всех идентификаторов узлов этого типа. Например, 2 представляет группу серверов, которая представляет собой комбинацию всех серверных узлов.

    • Почему выбирают эти три числа? Поскольку три значения в двоичном формате — «001, 010, 100» соответственно, поэтому, если вы хотите отправлять сообщения нескольким группам, вы можете напрямую ИЛИ несколько идентификаторов групп узлов.

    • То есть любое число в 1-7 представляет собой определенную комбинацию Scheduler/ServerGroup/WorkerGroup.

      • Если вы хотите отправить запрос на все рабочие узлы, установите идентификатор целевого узла запроса на 4.

      • Предположим, воркер хочет отправлять запросы на все серверные узлы и узлы планировщика одновременно, просто установите id целевого узла запроса равным 3, потому что 3 = 2 + 1 = kServerGroup + kScheduler.

      • Если вы хотите отправлять сообщения всем узлам, установите значение 7.

3.2.2 Реализация логических групп

Три логические группы определяются следующим образом:

/** \brief node ID for the scheduler */
static const int kScheduler = 1;
/**
 * \brief the server node group ID
 *
 * group id can be combined:
 * - kServerGroup + kScheduler means all server nodes and the scheuduler
 * - kServerGroup + kWorkerGroup means all server and worker nodes
 */
static const int kServerGroup = 2;
/** \brief the worker node group ID */
static const int kWorkerGroup = 4;
    for (int i = 0; i < num_workers_; ++i) {
      int id = WorkerRankToID(i);
      for (int g : {id, kWorkerGroup, kWorkerGroup + kServerGroup,
                    kWorkerGroup + kScheduler,
                    kWorkerGroup + kServerGroup + kScheduler}) {
        node_ids_[g].push_back(id);
      }
    }

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

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

3.2.3 Rank vs node id

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

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

    for (int i = 0; i < num_workers_; ++i) {
      int id = WorkerRankToID(i);
      for (int g : {id, kWorkerGroup, kWorkerGroup + kServerGroup,
                    kWorkerGroup + kScheduler,
                    kWorkerGroup + kServerGroup + kScheduler}) {
        node_ids_[g].push_back(id);
      }
    }

Конкретные правила расчета следующие:

  /**
   * \brief convert from a worker rank into a node id
   * \param rank the worker rank
   */
  static inline int WorkerRankToID(int rank) {
    return rank * 2 + 9;
  }
  /**
   * \brief convert from a server rank into a node id
   * \param rank the server rank
   */
  static inline int ServerRankToID(int rank) {
    return rank * 2 + 8;
  }
  /**
   * \brief convert from a node id into a server or worker rank
   * \param id the node id
   */
  static inline int IDtoRank(int id) {
#ifdef _MSC_VER
#undef max
#endif
    return std::max((id - 8) / 2, 0);
  }
​
  • SingleWorker: ранг * 2 + 9;
  • SingleServer: ранг * 2 + 8;

И этот алгоритм гарантирует, что идентификатор сервера будет четным, а идентификатор узла нечетным.

Таким образом, мы можем знать, что идентификатор 1-7 представляет группу узлов, а идентификатор отдельного узла начинается с 8.

Конкретные правила расчета следующие:

3.2.4 Group vs node

Поскольку иногда запросы отправляются на несколько узлов, ps-lite использует карту для храненияФактический набор узлов узла, соответствующий каждой группе узлов / отдельному узлу., то есть определить набор идентификаторов узлов, соответствующий каждому значению идентификатора.

std::unordered_map<int, std::vector<int>> node_ids_ 
​
    for (int i = 0; i < num_workers_; ++i) {
      int id = WorkerRankToID(i);
      for (int g : {id, kWorkerGroup, kWorkerGroup + kServerGroup,
                    kWorkerGroup + kScheduler,
                    kWorkerGroup + kServerGroup + kScheduler}) {
        node_ids_[g].push_back(id);
      }
    }
​

Эти пять идентификаторов соответствуют, то есть их нужно добавить к пяти элементам, соответствующим 4, 4 + 1, 4 + 2, 4 + 1 + 2, 12 в таблице сопоставления node_ids_. Это внутреннее условие цикла for в приведенном выше коде. То есть node_ids_[4], node_ids_[5], node_ids_[6], node_ids_[7], node_ids_[12], всем нужно добавить 12 в конец вектора.

  • 12 (сам)
  • 4 (кВоркерГрупп)
  • 4+1 (kWorkerGroup + kScheduler)
  • 4+2 (kWorkerGroup + kServerGroup)
  • 4+1+2, (kWorkerGroup + kServerGroup + kScheduler)

Следовательно, чтобы реализовать функцию «установка любого числа в 1-7 может быть отправлена ​​на все соответствующие ему узлы», для каждого нового узла ему необходимо соответствовать нескольким идентификаторам (узлу, группе узлов), этим группам идентификаторов являются узлами, с которыми этот узел может взаимодействовать. Например, для рабочего 2 его идентификатор узла равен 2 * 2 + 8 = 12, поэтому его необходимо объединить с

  • Идентификатор от 1 до 7 представляет группу узлов;
  • Последующие идентификаторы (8, 9, 10, 11...) представляют отдельные узлы. Двойные числа 8, 10, 12... представляют рабочий 0, рабочий 1, рабочий 2,... т.е. (2n+8), 9, 11, 13,... представляют сервер 0, сервер 1, сервер 2. , ... т.е. (2n + 9);

Вспомним предыдущую информацию об узле:

Как использовать этот node_ids_? Нам все еще нужно взглянуть на предыдущий код:

3.3 Представление параметров

между воркерами и серверамиpushиpullобщаться. Worker отправляет вычисленный градиент на сервер через push, а затем обновляет параметры с сервера через pull.

3.3.1 Формат КВ

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

Параметры выражены как k-v, форма более натуральная, простая для понимания и программной реализации.

3.3.2 key-values

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

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

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

Но это имеет два преимущества:

  • Уменьшить сетевой трафик
  • Это делает возможными операции на векторном уровне, так что можно использовать функции оптимизации многих линейных библиотек, таких как BLAS, LAPACK, ATLAS и т. д.

3.3.3 Работа с диапазоном

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

ps-lite позволяет пользователям использоватьRange PushиRange Pullработать.

3.4 Функция маршрутизации (keyslice)

Функция маршрутизации относится к тому, как Worker узнает, на какие серверы отправлять сообщение при выполнении Push/Pull.

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

PS Lite размещает логику маршрутизации на стороне Worker и использует стратегию разделения диапазона, то есть каждый Сервер имеет свой собственный фиксированный диапазон ключей, за который он отвечает. Этот диапазон определяется при запуске воркера. Детали следующим образом:

  • Тип данных ключа параметра определяется в зависимости от того, установлен ли макрос USE_KEY32 при компиляции PS Lite, либо 32-битное целое число без знака, либо 64-битное.
  • Основываясь на типе данных ключа, определите верхнюю границу его диапазона. Например, верхняя граница для uint32_t — 4294967295.
  • Разделите область действия по верхней границе ключевого поля и количеству серверов, полученному при запуске (то есть значению переменной среды DMLC_NUM_SERVER).
  • Диапазон ключей, поддерживаемый каждым сервером, равномерно распределен от малого до большого с помощью uint32_t / uint64_t. Учитывая верхнюю границу MAX и количество серверов N, диапазон, за который отвечает i-й сервер, равен[MAX/N*i, MAX/N*(i+1)).
  • Существуют определенные требования к построению хеш-значения ключа, чтобы избежать перекоса ключа между серверами (например, 32-разрядный, 16-разрядный, 8-разрядный, 4-разрядный, 2-разрядный обмен старшими и младшими битами).
  • Рабочие клавиши push и pull нарезаются в порядке возрастания для достижения нулевого копирования.

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

Конкретная реализация выглядит следующим образом:

Во-первых, ключ ps-lite поддерживает только тип int.

#if USE_KEY32
/*! \brief Use unsigned 32-bit int as the key type */
using Key = uint32_t;
#else
/*! \brief Use unsigned 64-bit int as the key type */
using Key = uint64_t;
#endif
/*! \brief The maximal allowed key value */
static const Key kMaxKey = std::numeric_limits<Key>::max();
​

Во-вторых, разделите диапазон int поровну

const std::vector<Range>& Postoffice::GetServerKeyRanges() {
  if (server_key_ranges_.empty()) {
    for (int i = 0; i < num_servers_; ++i) {
      server_key_ranges_.push_back(Range(
          kMaxKey / num_servers_ * i,
          kMaxKey / num_servers_ * (i+1)));
    }
  }
  return server_key_ranges_;
}
​

3.5 Инициализация среды

Из предыдущего анализа мы можем узнать, что ps-lite управляет определенными узлами через переменные среды.

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

Переменные среды включают в себя: роль узла, количество рабочих и серверов, IP-адрес, порт и т. д.

Функция InitEnvironment предназначена для создания фургона, получения количества рабочих и серверов и получения типа этого узла.

void Postoffice::InitEnvironment() {
  const char* val = NULL;
  std::string van_type = GetEnv("DMLC_PS_VAN_TYPE", "zmq");
  van_ = Van::Create(van_type);
  val = CHECK_NOTNULL(Environment::Get()->find("DMLC_NUM_WORKER"));
  num_workers_ = atoi(val);
  val =  CHECK_NOTNULL(Environment::Get()->find("DMLC_NUM_SERVER"));
  num_servers_ = atoi(val);
  val = CHECK_NOTNULL(Environment::Get()->find("DMLC_ROLE"));
  std::string role(val);
  is_worker_ = role == "worker";
  is_server_ = role == "server";
  is_scheduler_ = role == "scheduler";
  verbose_ = GetEnv("PS_VERBOSE", 0);
}
​

3.6 Старт

В основном:

  • Вызовите InitEnvironment() для инициализации среды и создания объекта VAN;
  • node_ids_initialization. Определите набор идентификаторов узлов, соответствующий каждому значению идентификатора, в соответствии с количеством рабочих и серверных узлов. Конкретная логика была проанализирована ранее.
  • Стартовый фургон, здесь будут различные взаимодействия (есть синхронное ожидание ADD_NODE, которое отличается от последующего барьерного ожидания);
  • Если это первый вызов PostOffice::Start, инициализируйте элемент start_time_;
  • Если требуемый барьер установлен, Барьер вызывается для ожидания/обработки, и, наконец, система запускается равномерно. То есть все Узлы подготавливают и отправляют Сообщение, которое требует синхронизации Планировщику для первой синхронизации;

Конкретный код выглядит следующим образом:

void Postoffice::Start(int customer_id, const char* argv0, const bool do_barrier) {
  start_mu_.lock();
  if (init_stage_ == 0) {
    InitEnvironment();
​
    // init node info.
    // 对于所有的worker,进行node设置
    for (int i = 0; i < num_workers_; ++i) {
      int id = WorkerRankToID(i);
      for (int g : {id, kWorkerGroup, kWorkerGroup + kServerGroup,
                    kWorkerGroup + kScheduler,
                    kWorkerGroup + kServerGroup + kScheduler}) {
        node_ids_[g].push_back(id);
      }
    }
    // 对于所有的server,进行node设置
    for (int i = 0; i < num_servers_; ++i) {
      int id = ServerRankToID(i);
      for (int g : {id, kServerGroup, kWorkerGroup + kServerGroup,
                    kServerGroup + kScheduler,
                    kWorkerGroup + kServerGroup + kScheduler}) {
        node_ids_[g].push_back(id);
      }
    }
    // 设置scheduler的node
    for (int g : {kScheduler, kScheduler + kServerGroup + kWorkerGroup,
                  kScheduler + kWorkerGroup, kScheduler + kServerGroup}) {
      node_ids_[g].push_back(kScheduler);
    }
    init_stage_++;
  }
  start_mu_.unlock();
​
  // start van
  van_->Start(customer_id);
​
  start_mu_.lock();
  if (init_stage_ == 1) {
    // record start time
    start_time_ = time(NULL);
    init_stage_++;
  }
  start_mu_.unlock();
  // do a barrier here
  if (do_barrier) Barrier(customer_id, kWorkerGroup + kServerGroup + kScheduler);
}
​

3.7 Barrier

3.7.1 Синхронизация

Как правило, запланированный узел реализует синхронизацию каждого узла путем подсчета. Конкретно:

  • Каждый узел отправит запрос команды Control::BARRIER узлу расписания после выполнения указанной команды и заблокирует себя до тех пор, пока не получит соответствующий ответ от планировщика перед разблокировкой;
  • После того, как планирующий узел получит запрос, он подсчитает его локально, чтобы увидеть, равно ли количество полученных запросов количеству барьерных_групп. Если они равны, это означает, что каждая машина выполнила указанную команду. узел расписания будет отчитываться перед каждой барьерной_группой, машина отправляет ответное сообщение и разблокирует его.

3.7.2 Инициализация

ps-lite использует Барьер для управления инициализацией системы, то есть все готовы двигаться вперед вместе. Это необязательный параметр. детали следующим образом:

  • Планировщик ожидает, пока все рабочие и серверы отправят информацию о БАРЬЕРЕ;
  • по окончанииADD_NODEПосле этого каждый узел войдет в указанную группуBarrierБлокировать механизм синхронизации (отправить БАРЬЕР планировщику), чтобы убедиться, что каждый узел вышеуказанного процесса завершен;
  • Все узлы (воркеры и серверы, включая планировщик) ждут ответа планировщика после получения информации БАРЬЕР всех узлов;
  • Наконец, все узлы получают ответ планировщика.BarrierВыйти из состояния блокировки после сообщения;
3.7.2.1 Ожидание сообщения БАРЬЕР

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

Обратите внимание, что при вызове

if (do_barrier) Barrier(customer_id, kWorkerGroup + kServerGroup + kScheduler);  
​
void Postoffice::Barrier(int customer_id, int node_group) {
  if (GetNodeIDs(node_group).size() <= 1) return;
  auto role = van_->my_node().role;
  if (role == Node::SCHEDULER) {
    CHECK(node_group & kScheduler);
  } else if (role == Node::WORKER) {
    CHECK(node_group & kWorkerGroup);
  } else if (role == Node::SERVER) {
    CHECK(node_group & kServerGroup);
  }
​
  std::unique_lock<std::mutex> ulk(barrier_mu_);
  barrier_done_[0][customer_id] = false;
  Message req;
  req.meta.recver = kScheduler;
  req.meta.request = true;
  req.meta.control.cmd = Control::BARRIER;
  req.meta.app_id = 0;
  req.meta.customer_id = customer_id;
  req.meta.control.barrier_group = node_group; // 记录了等待哪些
  req.meta.timestamp = van_->GetTimestamp();
  van_->Send(req); // 给 scheduler 发给 BARRIER
  barrier_cond_.wait(ulk, [this, customer_id] { // 然后等待
      return barrier_done_[0][customer_id];
    });
}
​

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

3.7.2.2 Обработка сообщений БАРЬЕР

Действие для обработки ожидания находится в классе Van, и мы освобождаем его заранее.

Конкретная логика ProcessBarrierCommand выглядит следующим образом:

  • Если msg->meta.request имеет значение true, это означает, что планировщик получает сообщение для обработки.

    • Планировщик увеличивает запрос Барьера.
    • Когда Планировщик получает последний запрос (счетчик равен общему количеству узлов в этой группе), он очищает счетчик и отправляет команду на завершение Барьера. В настоящее время для meta.request установлено значение false;
    • Отправить на все узлы в этой группеrequest==falseизBARRIERИнформация.
  • Если msg->meta.request имеет значение false, это означает, что ответ на сообщение получен, и барьер можно снять, поэтому он обрабатывается и вызывается функция Manage.

    • Функция Manage конвертирует всех клиентов, соответствующих app_idbarrier_done_Установите значение true, затем уведомите все переменные состояния ожиданияbarrier_cond_.notify_all().
void Van::ProcessBarrierCommand(Message* msg) {
  auto& ctrl = msg->meta.control;
  if (msg->meta.request) {  // scheduler收到了消息,因为 Postoffice::Barrier函数 会在发送时候做设置为true。
    if (barrier_count_.empty()) {
      barrier_count_.resize(8, 0);
    }
    int group = ctrl.barrier_group;
    ++barrier_count_[group]; // Scheduler会对Barrier请求进行计数
    if (barrier_count_[group] ==
        static_cast<int>(Postoffice::Get()->GetNodeIDs(group).size())) { // 如果相等,说明已经收到了最后一个请求,所以发送解除 barrier 消息。
      barrier_count_[group] = 0;
      Message res;
      res.meta.request = false; // 回复时候,这里就是false
      res.meta.app_id = msg->meta.app_id;
      res.meta.customer_id = msg->meta.customer_id;
      res.meta.control.cmd = Control::BARRIER;
      for (int r : Postoffice::Get()->GetNodeIDs(group)) {
        int recver_id = r;
        if (shared_node_mapping_.find(r) == shared_node_mapping_.end()) {
          res.meta.recver = recver_id;
          res.meta.timestamp = timestamp_++;
          Send(res);
        }
      }
    }
  } else { // 说明这里收到了 barrier respones,可以解除 barrier了。具体见上面的设置为false处。
    Postoffice::Get()->Manage(*msg);
  }
}
​
​

Функция «Управление» просто убирает барьер.

void Postoffice::Manage(const Message& recv) {
  CHECK(!recv.meta.control.empty());
  const auto& ctrl = recv.meta.control;
  if (ctrl.cmd == Control::BARRIER && !recv.meta.request) {
    barrier_mu_.lock();
    auto size = barrier_done_[recv.meta.app_id].size();
    for (size_t customer_id = 0; customer_id < size; customer_id++) {
      barrier_done_[recv.meta.app_id][customer_id] = true;
    }
    barrier_mu_.unlock();
    barrier_cond_.notify_all(); // 这里解除了barrier
  }
}
​

Конкретные инструкции заключаются в следующем:

                                                    +
    Scheduler                                       |                  Worker
        +                                           |                     +
        |                                           |                     |
        |                                           |                     |
        +--------------------------------+          |                     +-----------------+
        |                                |          |                     |                 |
        |                                |          |                     |                 |
        |                                |          |                     |                 |
        |                                v          |                     |                 v
        |                         receiver_thread_  |                     |           receiver_thread_
        |                                +          |                     |                 |
        |                                |          |                     |                 |
        v              BARRIER           |          |   BARRIER           v                 |
Postoffice::Barrier +----------------->  | <---------------------+ Postoffice::Barrier      |
        +                                |          |                     +                 |
        |                                |          |                     |                 |
        |                                |          |                     |                 |
        |                                |          |                     |                 |
        |                                v          |                     |                 |
        v                                           |                     v                 |
 barrier_cond_.wait          ProcessBarrierCommand  |               barrier_cond_.wait      |
        |                                +          |                     |                 |
        |                                |          |                     |                 |
        |                  All Nodes OK  |          |                     |                 |
        |                                |          |                     |                 |
        |                 +--------------+          |   BARRIER           |                 |
        |                 |              +---------------------------------------------->   |
        |                 |  BARRIER     |          |                     |                 |
        |                 +------------> |          |                     |                 |
        |                                |          |                     |                 |
        |                                |          |                     |                 |
        +<-------------------------------<          |                     | <---------------+
        |          barrier_cond_.notify_all         |                     |    barrier_cond_.notify_all
        v                                           |                     v
                                                    +
​
​

Телефон такой:

На данный момент мы завершили предварительный анализ Postoffice, а остальные функции разберем в следующих статьях в сочетании с Van и Customer.

0xEE Личная информация

★★★★★★Думая о жизни и технологиях★★★★★★

Публичный аккаунт WeChat:мысли Росси

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

ссылка 0xFF

Введение в проектирование и реализацию MXNet

Самое полное понимание PS-lite в истории

ps-lite углубленная интерпретация исходного кода

анализ исходного кода ps-lite

Масштабируемая распределенная архитектура машинного обучения на основе сервера параметров

анализ кода ps-lite

примечания к коду ps-lite

Введение в распределенный TensorFlow

Распределенное машинное обучение (часть 1) — параллельные вычисления и машинное обучение

Распределенное машинное обучение (средний уровень) — параллельные вычисления и машинное обучение

Распределенное машинное обучение (часть 2) — федеративное обучение

анализ исходного кода ps-lite

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