Найти в Дзене
птица говорун

течёт поток

в предыдущей заметке я рассказывал об алгоритмах используемых для создания искусственных нейросетей. также немного говорилось о там что эти системы очень энергоёмкие, вкратце упоминал основные пути повышения энергоэффективности. здесь я сосредоточусь на причинах этой самой энергонеэффективности вычислительным машин основных на классических архитектурах также рассмотрю альтернативные архитектуры. современные вычислительные системы от планшета до супер компьютера объединяет общий принцип — модель вычислений. эта модель основанная на потоке управления (Controlflow) здесь центральный процессор самый главный. обычно команды и данные хранятся в общей памяти — принстонская архитектура часто называемая архитектура фон Неймана. строго говоря, это не синонимы (архитектура фон Неймана — частный случай принстонской архитектуры), но для нашего анализа ключевым является общий принцип: наличие шины между процессором и памятью. данные из памяти загружаются в регистр команд, также данные являющимися о
Оглавление
Google TPU (TensorFlow Processing Unit)
Google TPU (TensorFlow Processing Unit)

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

дедушка фон Нейман

современные вычислительные системы от планшета до супер компьютера объединяет общий принцип — модель вычислений. эта модель основанная на потоке управления (Controlflow) здесь центральный процессор самый главный.

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

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

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

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

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

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

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

это уже не самая простая операция для реализации «в кремнии» кроме всего прочего как правило влечёт блокировку т.е. простой процессора. поэтотому чтобы дополнительно минимизировать время простоя процессора за счёт такой синхронизации в современных ОС есть такое понятие привязка процесса к определённому процессору (processor affinity).

ситуация ещё осложняется если система будет иметь физически разные процессоры со своими кешами. проблема носит названия проблема когерентности кеша (cache coherence). нужно гарантировать чтобы в кеше каждого процессора находились актуальные данные. синхронизация влечёт дополнительные блокировки и следовательно простои.

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

проблема многопроцессорных систем фон Неймана проявляется и на программном уровне. задача по распараллеливанию алгоритма — выявлению независимых участков кода и последующей синхронизации потоков — ложится на программиста. даже с использованием стандартных средств вроде POSIX Threads или OpenMP создаёт значительную сложность и является источником трудноуловимых ошибок: состояния гонки (race conditions) и взаимные блокировки (deadlocks).

Near-Memory Computing(NMC)

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

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

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

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

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

однако на пути реализации Near-Memory Computing есть препятствия, например ограничения, накладываемые процессом изготовления чипа. кроме того фактически это очень много процессоров пусть и находящихся в непосредственной близости к данным, значит существует проблема координации работы и синхронизации этих тысяч распределённых вычислительных блоков.

специализированные Dataflow-архитектуры

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

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

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

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

в Dataflow данные передаются и хранятся в виде т.н. токенов (token). токен — это структура, содержащая передаваемое значение и тег, который идентифицирует узел-получатель и контекст вычислений для возможности выполнения нескольких графов (конвейеров) вычислений одновременно. простейшая потоковая вычислительная система состоит из двух устройств: исполнительного устройства и устройства сопоставления.

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

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

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

допустим нам нужно вычислить скалярное произведение Z = (A1 * B1) + (A2 * B2) + (A3 * B3).

шаг 1: подача данных / активация умножителей

  • в память (или на входные порты) поступают токены: (A1, тег_умножителя1), (B1, тег_умножителя1).
  • как только для узла "умножитель1" доступны все необходимые входные токены (и A1, и B1), он "запускается".
  • умножитель "поглощает" эти входные токены, выполняет вычисление A1 * B1 и порождает новый выходной токен (результат1, тег_сумматора).
  • аналогично и параллельно или последовательно работают умножитель2 и умножитель3.

шаг 2: активация сумматора

  • сумматору нужны 3 входных токена. допустим, первым пришёл токен (результат1, тег_сумматора). сумматор ждёт.
  • пришел (результат2, тег_сумматора). сумматор все ещё ждёт.
  • пришел (результат3, тег_сумматора) — все условия выполнены! сумматор активируется.
  • он "поглощает" все три входных токена, вычисляет сумму и порождает финальный выходной токен (Z, тег_выхода).

на практике, в реальных ускорителях типа TPU, используется статический Dataflow, где граф вычислений реализуется «в кремнии» в виде конвейера. данные передаются между блоками через жёстко связанные с ними буферы, а не через общую память. за с чет этого достигается та самая эффективность, ради которой все затеяно.

я уже упоминал VLIW и его малопригодноcть для задач общего назначения, но что если прошить внутрь VLIW граф(конвеер) вычисления Dataflow? да конечно это будет очень узкоспециализированный процессор «заточенный» для выполнения определённой задачи. но он будет решать её быстро без лишних обменов с оперативной памятью.

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

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

  • Фаза 1 (Подача данных):
    MUL1.вход_A = A1
    MUL1.вход_B = B1
    MUL2.вход_A = A2
    MUL2.вход_B = B2
    MUL3.вход_A = A3
    MUL3.вход_B = B3
    все умножители срабатывают параллельно, например можно сделать отдельную команду которая будет запускать весь конвейер вычислений целиком.
  • Фаза 2 (Суммирование):
    ADD3.вход_1 = MUL1.выход
    ADD3.вход_2 = MUL2.выход
    ADD3.вход_3 = MUL3.выход
    сумматор срабатывает при условии когда получит данные на все три входа.
  • Фаза 3 (Вывод):
    Выходной_буфер = ADD3.выход
    Результат Z готов. можно можно выставить некий флаг сигнализирующий о конце процесса вычислений, что результат можно забрать.

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

текст создан при экспертной поддержке DeepSeek