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

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

Современный видеоускоритель — это ещё и невероятно сложная система, которая постоянно становится всё сложнее. Новые возможности, вроде сеточных шейдеров (mesh shader) и графов задач (Work Graphs) хорошо описывает фраза «два шага вперёд, шаг назад». С каждой новой возможностью связана некая базовая задача, поддержка решения которой реализована не полностью.

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

В апреле прошлого года я проводил коллоквиум с таким же названием, как у этой статьи, на факультете информатики в Калифорнийском университете в Санта-Крузе. Этот материал является дополнением к тому моему выступлению.

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

Я уже много лет работаю над Vello. Это — продвинутый движок для рендеринга двумерной векторной графики. Процессор выгружает описание сцены в упрощённом бинарном формате, напоминающем SVG, после чего вычислительные шейдеры решают все остальные задачи, выдавая, в итоге, отрендеренное 2D-изображение. Вычислительные шейдеры парсят древовидные структуры, решают сложные задачи вычислительной геометрии для расширения обводки (stroke expansion), применяют алгоритмы, напоминающие алгоритмы сортировки, для разбиения объектов на группы (binning). Это, по сути, простой компилятор, выдающий оптимизированный байт-код, нечто вроде программы для каждого блока пикселей размером 16x16, а потом интерпретирующий эту программу. Правда, что меня всё сильнее расстраивает, этот механизм не может работать в ограниченном объёме памяти. На каждом этапе работы создаются промежуточные структуры данных, при этом количество и размер этих структур непредсказуемым образом зависит от входных данных. Например, изменение единственной трансформации в закодированной сцене может привести к тому, что система, в ходе формирования сцены, будет работать по плану рендеринга, который кардинально отличается от исходного.

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

Подробности о конкретной проблеме — это интересно, но они выходят за рамки этой статьи. Тем, кому это интересно, рекомендую посмотреть документ по Potato — гибридной архитектуре 2D-рендеринга, использующей процессор и видеокарту. Там исследуется вопрос о том, как далеко можно зайти, выполняя планирование на CPU с учётом ограниченных ресурсов GPU при использовании GPU для выполнения реальных операций с пикселями. В этом документе так же затрагиваются вопросы, посвящённые некоторым достаточно новым расширениям стандартной модели выполнения, характерной для GPU. Все они отличаются сложностью, они не являются переносимыми, и ни одно из них, похоже, не решает проблему.

По существу, не должно возникать абсолютной необходимости в выделении больших буферов для хранения промежуточных результатов. Так как эти буферы будут использоваться на этапах работы, которые идут за этапом выделения памяти, гораздо эффективнее будет помещать соответствующие данные в очереди, размера которых хватает для того, чтобы вместить достаточное количество элементов, создаваемых в процессе работы. Это, к тому же, позволит воспользоваться уже имеющимися механизмами, распараллеливающими выполнение кода. На GPU многие операции устроены так, что они уже работают как очереди (стандартный конвейер, состоящий из вершинного шейдера, фрагментного шейдера и операций растеризации — классический пример подобного подхода). Поэтому тут вопрос заключается лишь в том, чтобы дать разработчикам приложений доступ к этим внутренним механизмам. В публикации GRAMPS 2009 года предлагается именно это направление развития графических конвейеров, то же самое сделано и в проекте Brook — предшественнике CUDA.

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

Параллельные компьютеры прошлого

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

Суперкомпьютеры серии Connection Machine

Я говорю здесь об этой системе не потому, что она отличалась особенно перспективной архитектурой, а из-за того, что она является чистейшим выражением мечты о хорошем параллельном компьютере. Первый суперкомпьютер из серии Connection Machine (CM) был выпущен в 1985 году. Он содержал более 64 тысяч процессоров, скомпонованных в гиперкуб. Количество отдельных потоков в этой системе, даже по современным меркам, очень велико, хотя каждый отдельный процессор отличался очень низкой вычислительной мощностью.

Возможно, главная роль, которую сыграл суперкомпьютер Connection Machine, заключается в том, что он дал толчок к огромному объёму исследований параллельных алгоритмов. Базовая работа Беллока, посвящённая алгоритму префиксных сумм, была, в основном, написана благодаря существованию Connection Machine. Мне, кроме того, кажется крайне интересной ранняя работа о сортировке на CM-2.

Connection Machine 1 (1985) в KIT / Informatics / TECO (источник — KIT TECO, CC0)
Connection Machine 1 (1985) в KIT / Informatics / TECO (источник — KIT TECO, CC0)

Архитектура Cell

Ещё один важный новаторский пример практической реализации параллельных вычислений — это процессор, построенный на основе архитектуры Cell, который вошёл в состав игровой консоли PlayStation 3 в 2006 году. Объёмы продаж этого устройства были довольно-таки высокими (около 87,5 миллионов штук), применяли консоль для решения разных интересных задач, в том числе — в высокопроизводительных вычислениях, но на этом всё и закончилось. Уже в PlayStation 4 компания Sony перешла на довольно-таки стандартный графический конвейер, основанный на видеокарте семейства Radeon.

Возможно, одной из главных сложностей в применении архитектуры Cell была её модель программирования. В той её версии, которая использовалась в PS3, было 8 параллельных ядер, поддерживавших 128-битные SIMD-инструкции, у каждого из которых было 256 Кб статической оперативной памяти (SRAM). Программисту нужно было самостоятельно копировать данные в локальную SRAM, а после этого ядро, обращаясь к этой памяти, уже могло проводить какие-то вычисления. Тут практически не было поддержки высокоуровневых инструментов программирования. В результате тому, кому хотелось писать программы для этой платформы, приходилось кропотливо проектировать и реализовывать параллельные алгоритмы.

Но, как бы там ни было, архитектура Cell, по большей части, соответствует моим требованиям к «хорошему параллельному компьютеру». Отдельные ядра могут выполнять программы, никак друг от друга не зависящие, в системе предусмотрена глобальная очередь заданий.

Общая производительность процессора, основанного на архитектуре Cell, находилась в районе 200 гигафлопсов, что по тем временам выглядело впечатляюще, но сильно уступает сегодняшним GPU или даже CPU (Intel i9-13900K характеризуется показателем в примерно 850 гигафлопсов, а процессор семейства Ryzen 7, относящийся к средне-высокому классу, способен выдать что-то около 600 гигафлопсов).

Проект Larrabee

Возможно, в истории проектирования GPU самый трогательный пример непройденного пути являет собой архитектура Larrabee. В работе, представленной на конференции SIGGRAPH в 2008 году, были приведены убедительные аргументы в поддержку этой архитектуры, но проект, в итоге, провалился. Сложно точно судить о том, почему это произошло, но я думаю, что причиной этого могла быть плохая его реализация со стороны Intel. Его мог бы ждать успех, если бы компания проявила больше настойчивости, если бы провела ещё несколько итераций, устраняя недостатки исходной версии. По своей сути, система, основанная на Larrabee — это стандартный x86-компьютер, обладающий широкими (512 битов) SIMD-блоками, в составе которого имеется немного специализированного аппаратного обеспечения, рассчитанного на оптимизацию графических задач. Большинство графических функций реализовано программно. Если бы этот проект получился, он очень легко бы дал мне то, чего мне хочется. Создание заданий и постановка их в очередь выполняется программно, всё это может делаться на полностью динамической основе и с возможностью настройки самых мелких деталей.

Но проект Larrabee не исчез бесследно — его частицы продолжают жить. Новый набор инструкций AVX10 — это плод эволюции набора инструкций AVX-512 из Larrabee, он поддерживает 32 канала для операций с числами f16. Том Форсайт, один из создателей проекта, утверждает, что Larrabee, на самом деле, не провалился, и что его успех — это оставленное им техническое наследие. Технологии из Larrabee использовались при создании процессора Intel Xeon Phi, выпущенного ограниченным тиражом. Ещё один важный аспект этого наследия — компилятор ISPC. Статья Матта Фарра об истории ISPC проясняет и некоторые факты о проекте Larrabee.

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

Ещё один аспект, который, определённо, помешал развитию Larrabee — это программная часть проекта. Писать хорошие программы — это всегда непросто, особенно, когда речь идёт об инновационных направлениях. Драйверы Larrabee не давали доступа к специальным возможностям аппаратного обеспечения, поддерживающего гибкое управление вычислительными функциями. Производительность в традиционных 3D-сценах, основанных на треугольниках, не оправдывала ожиданий. При этом система, с применением стандартного интерфейса OpenGL, весьма неплохо показала себя на CAD-задачах, где нужно работать со множеством сглаженных линий.

Изменяющаяся рабочая нагрузка

Со временем даже в играх вычисления превращаются во всё более значимую часть общей рабочей нагрузки (а в сфере ИИ вычислений — это и есть всё, чем занимается система). Анализ игры Starfield, который провели в блоге Chips and Cheese, показал, что примерно половина рабочего времени этой игры уходит на вычисления. Система рендеринга Nanite тоже использует вычисления — даже для растеризации маленьких треугольников, так как специализированное «железо» эффективнее стандартного лишь на треугольниках, превышающих определённый размер. По мере того, как в играх всё чаще применяется фильтрация изображений, глобальное освещение, и графические примитивы вроде гауссовых пятен (Gaussian splatting), эта тенденция, почти наверняка, будет продолжаться.

В 2009 году Тим Суини выступил с интригующим докладом «The end of the GPU roadmap», в котором сделал предположение о том, что сама концепция видеоускорителей полностью исчезнет, уступив место универсальным вычислительным средствам с высокой степенью параллелизма. Такого пока не случилось, хотя можно было наблюдать некоторые подвижки в этом направлении. Это — вышеописанный проект Larrabee, это прорывная публикация 2011 года о проекте cudaraster, где была показана реализация традиционного 3D-конвейера растеризации изображений с использованием универсальных вычислительных средств. Авторы той публикации выяснили (если говорить упрощённо), что их подход был примерно в 2 раза медленнее, чем использование аппаратных блоков с фиксированной функциональностью. Существуют и более современные академические проекты, посвящённые проектированию GPU с использованием массивов ядер RISC-V. Стоит отметить, что в сравнительно недавней публикации Tellusim говорится о том, что системы рендеринга, напоминающие cudaraster, и работающие на современных процессорах, уже почти сравнялись по производительности со специализированными устройствами для обработки графики.

В замечательной презентации, которую провёл Эндрю Лауритцен в 2017 году (Future Directions for Compute-for-Graphics) раскрыто множество проблем, связанных с внедрением продвинутых вычислительных техник в графические конвейеры. С тех времён в этом направлении достигнуты определённые успехи, но в той презентации затрагиваются, во многом, те же темы, которым я посвятил эту статью. Кроме того, стоит взглянуть на комментарии Джоша Барчака, где он упоминает модель программирования GRAMPS и говорит о трудностях, связанных с поддержкой языков программирования.

Пути, ведущие к хорошему параллельному компьютеру

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

Большой массив ядер: возрождение Cell

Перспективы, которые открывала архитектура Cell, до сих пор выглядят привлекательными. Современные CPU высшего класса содержат более 100 миллиардов транзисторов, в то время как достаточно быстрое ядро RISC-процессора можно создать, использовав на порядки меньшее количество транзисторов. Почему бы не разместить на одном чипе сотни или даже тысячи подобных ядер? Для максимизации пропускной способности системы каждое ядро можно оснастить собственным SIMD-блоком. И, на самом деле, существует как минимум два ИИ-ускорителя, основанных на этой идее — это Esperanto и Tenstorrent. Мне особенно интересен последний, так как он обладает опенсорсным программным стеком.

При этом в данной сфере, безусловно, существуют и определённые сложности. Для реализации подобных идей одного только процессорного ядра недостаточно — ему нужна локальная память с высокой пропускной способностью и средства коммуникации с другими ядрами. Одна из причин того, что под Cell было так тяжело писать программы, заключается в маленьком размере локальной памяти этой системы, и в том, что программисту необходимо было самостоятельно этой памятью управлять. А именно, программа сама, в явном виде, должна была организовывать передачу данных по сети, получая и отправляя информацию. В сфере CPU (и GPU) наблюдается тенденция к виртуализации всего и вся, когда имеется абстракция большого общего пула памяти, которым пользуются все ядра. При этом тот, кому важна производительность, всё равно должен учитывать особенности работы с кеш-памятью, но, если не обращать на это внимания, программа всё равно будет работать. Можно создать достаточно интеллектуальный компилятор, который приспосабливает высокоуровневое описание задачи к конкретному аппаратному обеспечению (именно этот подход используется в программном стеке Tenstorrent, называемом TT-Buda и ориентированном на ИИ-нагрузки). По аналогии с идеей использования параллелизма на уровне инструкций в архитектуре VLIW, пример Itanium можно назвать предостережением для будущих поколений разработчиков подобных систем.

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

Большинство архитектур, предусматривающих наличие «больших массивов ядер», нацелены на ускорение ИИ-вычислений, и это неспроста: такие вычисления очень требовательны к огромной пропускной способности при низком энергопотреблении. Поэтому альтернативы традиционным подходам, использующим обычные CPU, выглядят весьма привлекательно. Хороший обзор этого вопроса можно найти в выступлении Иэна Катресса «New Silicon for Supercomputers».

Выполнение команд Vulkan на стороне GPU

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

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

Обратите внимание на то, что разработчики API видеоускорителей, так или иначе, постепенно изобретают более сложные, но и более ограниченные версии этого механизма. Хотя и нельзя сделать так, чтобы API Vulkan работал бы непосредственно в шейдере, но, используя последнее расширение Vulkan (VK_EXT_device_generated_commands), можно поместить некоторые команды в буфер команд из шейдера. Графический API Metal тоже обладает подобными возможностями (подробности о переносимости этого подхода смотрите здесь). Среди возможностей, всё ещё отсутствующих в таких API, можно отметить косвенный запуск команд для того, чтобы рекурсивно создавать новые задания. Похоже, что архитекторы этих API не слишком сильно прониклись идеями Хофштадтера.

Интересно подумать о возможности вызова API Vulkan непосредственно из шейдеров. Так как API Vulkan выражен в терминах языка C, то одним из требований тут является возможность выполнения кода, написанного на C. Сейчас идут эксперименты в этом направлении (взгляните на проект vcc), но на практике подобное пока неприменимо. Конечно, платформа CUDA может выполнять C-код. В CUDA 12.4, кроме того, появилась поддержка условных узлов (conditional nodes), а в CUDA 12.0 — поддержка функции запуска вычислительных графов напрямую с GPU (device graph launch), что значительно снижает задержки.

Графы задач

Графы задач (work graphs) — это недавно вышедшее расширение модели выполнения кода на GPU. Если описать это в двух словах, то получается, что программа организуется в виде графа, состоящего из узлов (вычислительных ядер) и рёбер (очередей), причём всё это работает параллельно. По мере того, как узел формирует выходные данные, заполняя свои выходные очереди, GPU запускает ядра (на уровне рабочих групп) для дальнейшей обработки этих данных. Это, по большей части, современное воплощение идей GRAMPS.

Хотя всё это выглядит очень интересно, и хотя этот подход, весьма вероятно, пригодится для решения широкого множества графических задач, графы задач, кроме того, обладают серьёзными ограничениями. Я поинтересовался тем — смогу ли я использовать их в существующей архитектуре Vello, и обнаружил три основных проблемы. Во-первых — они неспособны легко описывать слияния, когда ход работы в узле зависит от синхронизированных входных данных, поступающих из двух разных очередей. В Vello активно используются слияния, например — в одном ядре идёт обсчёт ограничивающего прямоугольника для рисованного объекта (с агрегированием нескольких сегментов контура), а в другом — обработка графических сущностей, находящихся в пределах этого прямоугольника. Во-вторых — граф не гарантирует соблюдение порядка обработки элементов, помещённых в очередь. А 2D-графика требует определённого порядка выполнения операций (усы тигра необходимо нарисовать поверх его морды). В-третьих, графы задач не поддерживают работу с элементами переменного размера.

Особенно меня расстраивает отсутствие поддержки порядка выполнения операций, так как обычные 3D-конвейеры, совершенно точно, это поддерживают. В частности, кроме прочего, делается это для того, чтобы предотвратить появление Z-fighting-артефактов (они возникают, когда объекты борются друг с другом за возможность друг друга перекрыть). В этом замечательном материале вы можете найти рассказ о том, как аппаратное обеспечение GPU обеспечивает гарантию порядка смешивания пикселей. С использованием нового функционала невозможно адекватно сымитировать традиционный графический конвейер, использующий вершинные и фрагментные шейдеры. Понятно, что обеспечение гарантий порядка смешивания пикселей в параллельных системах — это дорогое удовольствие. Но, в идеале, должен быть способ его включения в тех случаях, когда оно нужно, или хотя бы способ связать графы задач с другим механизмом (с какой-нибудь сортировкой, которую можно достаточно эффективно реализовать на GPU), который поможет привести порядок элементов к желаемому. В результате графы задач кажутся мне технологией, которую хорошо описывает фраза «два шага вперёд, шаг назад».

Конвергентная эволюция процессоров

В теории, при выполнении на традиционном многоядерном CPU сильно распараллеленных задач, всё делается так же, как и на GPU, и если архитектура CPU будет хорошо оптимизирована с точки зрения эффективности работы, то она вполне сможет конкурировать с архитектурой GPU. Вероятно, так могло бы звучать техническое задание проекта Larrabee, и, скорее всего, именно этой идеей вдохновлялись авторы более свежих академических проектов, вроде того, в рамках которого ведётся работа над Vortex. Возможно, главная сложность здесь — энергоэффективность решений. Существует тенденция, в рамках которой архитектуры CPU делятся на те, где оптимизируется одноядерная производительность (P-ядра, performance cores, производительные ядра), и на те, где оптимизируется энергоэффективность (efficiency cores, эффективные ядра, E-ядра), при этом ядра обоих типов обычно присутствуют на одних и тех же чипах. По мере того, как эффективные ядра становятся распространённее, может оказаться так, что выигрыш в производительности будет у алгоритмов, рассчитанных на параллельное выполнение. Это, в свою очередь, может подтолкнуть производителей процессоров к улучшению энергоэффективности E-ядер и к размещению на чипах ещё большего количества таких ядер, несмотря даже на то, что в однопоточных задачах их производительность будет выглядеть скромно.

Сильная сторона этого подхода в том, что он не меняет существующей модели выполнения программ, в результате чего можно продолжать использовать существующие языки программирования и инструменты. К сожалению, большинство существующих языков плохо подходит для описания и использования параллельных механизмов — как на уровне SIMD, так и на уровне потоков. Шейдеры отличаются более сильно ограниченной моделью выполнения кода, но при работе с ними, по крайней мере, ясно то, как эффективно их выполнять в параллельной среде. А в том, что касается параллелизма на уровне потоков, проблему представляет собой потеря производительности при переключениях контекста. Хочется надеяться, что в достаточно новых языках, вроде Mojo, эта проблема будет решена, и что они, возможно, будут адаптированы к моделям выполнения кода, напоминающим модели, применяемые в GPU.

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

Может — необходимое аппаратное обеспечение уже существует?

Возможно, что уже продаётся такое «железо», которое соответствует моим требованиям к «хорошему параллельному компьютеру», но раскрытию его потенциала пока мешает программное обеспечение. У видеокарт обычно имеется «командный процессор», который, совместно с драйвером, работающим на хосте, разбивает команды, относящиеся к рендерингу графики и к вычислениям, на фрагменты, которые должны быть запущены на конкретных блоках выполнения команд. Этот командный процессор, в любом случае, скрыт, и не может выполнять пользовательский код. Открытие доступа к нему может оказаться весьма интересным новшеством. Что-то подобное можно найти в заметках Ханса-Кристиана Арнтцена, касающихся реализации графов задач в опенсорсных драйверах.

Архитектуры разных GPU отличаются по уровню распределения возможностей, встроенных в аппаратное обеспечение и реализуемых командным процессором. Возможность программирования чего-либо — это отличный способ придания системам большей гибкости. Главное ограничение тут — секретность, в которой компании держат сведения об архитектурах устройств. Даже в GPU с опенсорсными драйверами доступ к прошивке (то есть — к тому, что выполняется на командном процессоре) закрыт. Конечно, с этим связана проблема безопасности. Открытие доступа к командному процессору из пользовательского кода серьёзно увеличивает количество потенциальных уязвимостей. Но с исследовательской точки зрения, если абстрагироваться от соображений безопасности, было бы интересно изучить то, на что способны командные процессоры видеокарт.

Ещё одно интересное направление развития техники — это появление «Accelerated Processing Units» — чипов, где GPU и CPU пользуются одним и тем же адресным пространством. С концептуальной точки зрения эти чипы похожи на интегрированные видеокарты, но они редко обладают такой производительностью, которая могла бы кого-то заинтересовать. По опыту знаю, что запуск на подобных устройствах существующих API (Vulkan для вычисления шейдеров, или одного из современных вариантов OpenCL) не даёт значительных преимуществ в плане сокращения задержек, связанных с синхронизацией CPU и GPU. Всё дело — в дополнительной нагрузке на систему, которую создают операции переключения контекста. Можно сделать так, чтобы высокоприоритетный или выделенный поток быстро обрабатывал бы элементы, помещаемые в очередь задачами, работающими на GPU. Главная идея тут заключается в применении очередей, работающих на полной вычислительной мощности, а не механизма удалённого вызова процедур, с которым сопряжено возникновение задержек, возможно — весьма длительных.

Сложность

Если взглянуть на общую картину, то окажется, что одной из основных особенностей экосистемы GPU является её невероятная сложность. Тут имеется основной модуль для выполнения параллельных вычислений, затем — множество узкоспециализированных аппаратных блоков (количество которых постоянно растёт, особенно — с появлением в видеокартах новых возможностей вроде трассировки лучей), затем — тут присутствуют большие и громоздкие механизмы, обеспечивающие планирование и выполнение заданий. Всё начинается с базового механизма планирования запуска шейдеров (тут используется 3D-сетка, описывающая координаты x, y и z, задаваемые 16-битными значениями), затем всё это дополняется различными расширениями для косвенного кодирования команд.

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

Для мира GPU характерны и другие сложности, возникающие по самым разным причинам. Так, здесь существует множество конкурирующих API, каждый из которых обладает собственной семантикой, немного отличающейся от других. Это особенно сильно усложняет задачу написания кода, который спокойно работал бы на разных платформах.

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

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

Всё это очень сильно контрастирует с миром CPU. Современные процессоры тоже чрезвычайно сложны, они состоят из миллиардов транзисторов, но в их основе лежит гораздо более простая вычислительная модель. С точки зрения программиста написание кода для Apple M3, состоящего из 25 миллиардов транзисторов, не сильно отличается от, скажем, работы с Cortex M0, который можно создать, воспользовавшись всего 48000 транзисторов. Аналогично — реализация низкопроизводительного ядра RISC-V — это проект, с которым вполне справится и студент. Конечно, процессор M3 решает гораздо больше задач и обладает гораздо большими возможностями — это и предсказание ветвлений, и суперскалярное выполнение инструкций, и многоуровневая организация памяти, и слияние операций, и прочее подобное, связанное с производительностью. Но при этом M3, что вполне очевидно, занимается тем же самым, что и чип, проще устроенный и гораздо меньший его по размеру.

В прошлом замене специализированных аппаратных систем на универсальные способствовали экономические факторы, но в наши дни положение меняется. По сути, речь идёт о том, что если некий проект оптимизируют, ориентируясь на количество транзисторов в применяемых вычислительных блоках, то после его оптимизации можно будет почти полностью загрузить работой универсальную и не слишком эффективную систему. А применение в таком проекте специализированного аппаратного обеспечения оправдано лишь в тех случаях, когда задача способна полноценно использовать его возможности. Но после того, как перестал работать закон масштабирования Деннарда, и нас теперь, в основном, ограничивает энергопотребление систем, а не количество транзисторов, из которых они созданы, специализированное аппаратное обеспечение оказалось в более выигрышной позиции. Его просто можно отключить в том случае, если оно не используется для решения некоей задачи. Вероятно, время вычислительной модели, основанной исключительно на RISC, подошло к концу. Вместо неё мне хотелось бы видеть системы, в которых применялось бы гибкое ядро (может быть — RISC-V-ядро), которое выступает в роли модуля, управляющего специализированными ускорителями. Именно такой подход использует, кроме прочих подобных проектов, проект Vortex.

Итоги

В выступлении, которое архитектор GPU Nvidia Эрик Линдхольм сделал незадолго до выхода на пенсию, он сказал (обсуждая создание задач и системы очередей): «моя карьера была посвящена тому, чтобы делать системы более гибкими, лучше поддающимися программированию. Эта работа ещё не завершена. Остался ещё один шаг, который, как мне кажется, нужно сделать, и я двигался в этом направлении, много лет работая в Nvidia Research». Я с этим согласен, и мои собственные проекты значительно бы от этого выиграли. Теперь, когда он ушёл на пенсию, не вполне ясно то, кто именно продолжит его дело. Что может произойти в этой сфере? Возможно — Nvidia, как уже бывало, нарушит привычный ход дел, перебив развитие своей линейки видеоускорителей использованием нового подхода. Может — какая-нибудь новая компания разработает ИИ-ускоритель, создав огромный массив маломощных процессоров с векторными блоками, который можно будет программировать. Не исключено, что E-ядра CPU разовьются и станут настолько эффективными, что смогут конкурировать с GPU.

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

В любом случае, у любознательных людей есть возможность исследовать альтернативную вселенную, в которой существует хороший параллельный компьютер. Соответствующие архитектуры можно моделировать на FPGA, как в проекте Vortex, а прототипы алгоритмов можно создавать на многоядерных CPU, поддерживающих SIMD-блоки большой ширины. Ещё можно поразмышлять о том, как мог бы выглядеть хороший язык программирования для подобных машин, хотя такие размышления были бы омрачены досадным фактом отсутствия реального «железа», на котором могли бы выполняться программы, написанные на таком языке.

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

О, а приходите к нам работать? ? ?

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

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

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде

Комментарии (3)


  1. NickDoom
    30.06.2025 10:02

    Ну вот когда Нвидиа собрались добавить в свои графические процы ещё и CPU, я робко надеялся, что это будет что-то типа «128-ядерный 80486, выполняющий на каждом ядре за один такт 1-5 команд, данные которых не зависят друг от друга», благо они собаку съели на параллельном доступе к памяти от кучи ядер — Нвидиа же.

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

    Но они оказались недостаточно смелыми для таких экспериментов :( Верните мне мой 1986-й! Верните смелых разработчиков!


    1. ciuafm
      30.06.2025 10:02

      Купите б.у. Xeon Phi и наслаждайтесь...


    1. miksoft
      30.06.2025 10:02

      за один такт 1-5 команд

      не нужно занимать пол-кристалла всякими хитрыми спекулирующими конвейерами

      А это точно не противоречивые требования?