?

Log in

No account? Create an account
 
 
29 August 2015 @ 02:48 pm
GPU inside: конструируем GPU сами - вычисления  
Прежде чем рассказать о том, как на самом деле устроены современные GPU, я предлагаю вместе подумать о том, как их вообще можно было сделать. "Отбросьте невозможное и останется невероятное". Проанализировав различные подходы к процессоростроению с точки зрения поставленных перед GPU задач, мы увидим какие из них в этом случае работают лучше, а какие - хуже, и тогда вас перестанет удивлять, как странно эти GPU устроены и почему они так непохожи на своих братьев-CPU.

Итак, задача стоящая перед CPU - как можно быстрее выполнить заданную последовательность команд, один-единственный поток. Это очевидно для десктопных CPU, но и серверные проектируются с той же целью, поскольку часть их нагрузки всё равно малопоточная. Отсюда и используемые в них решения - высокая частота, суперскалярность, большие кеши и т.д. Всё - лишь бы побыстрей обработать одну последовательность команд и переключиться на следующую. Единственным исключением из этой идеологии была линейка Sun Niagara и неудивительно, что именно в ней впервые появились "родовые черты" GPU.


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

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


Таким образом, современный дискурс характеризуется противостоянием между процессорами, ориентированными на задержку (latency) вычислений - это CPU, и процессорами, ориентированными на потоковую производительность (throughput) вычислений - это GPU. Разница в целях определяет и разницу в выбираемых архитектурных решениях.

Частота: для CPU, понятно, чем больше тем лучше - быстрее выполним одно задание. Однако большая частота требует и большего напряжения питания, а общее энергопотребление пропорционально частоте и квадрату напруги. Поэтому GPU идут иным путём - ограничивая частоту, они зато помещают больше ALU/ядер на кристалле. Такой подход позволяет максимизировать потоковую производительность при заданном энергопотреблении - а GPU и так уже упираются в потолок тепла, которое можно отвести с одного чипа. Кстати, Niagara была первым процессором, "обменявшим" частоту на увеличение числа ядер.

Теперь перейдём к собственно архитектуре. На площади современного GPU можно разместить, скажем, 10 тыщ ALU - при условии, что там не будет ничего иного. Однако в реальности все эти ALU нужно снабдить потоком команд и данных, а затем "отвести" результаты их вычислений. Поэтому задача состоит в том, чтобы найти такую структуру управляющей логики и кешей, которая отнимет мимимум места у ALU, но при этом сможет "прокормить" данными все оставшиеся. Как видите, эта задача диаметрально противоположна стоящей перед разработчиками CPU, которые, утрированно говоря, десятилетиями искали способы использовать дополнительное пространство кристалла для того, чтобы ускорить доставку данных к одному-единственному ALU. Поэтому далеко не все их архитектурные находки подходят для GPU.


SIMD: это наиболее "дешёвый" способ поставить десятки ALU там, где раньше был всего один. Всего лишь расширяем шины данных к "располневшему" ALU плюс регистры, которыми он оперирует - и всё готово. Поэтому SIMD стал не только "обязательной программой" для всех throughput-ориентированных устройств, но и общепринятым среди CPU способом дёшево и сердито повысить свою потоковую производительность.

Например в Intel CPUs вычислительный тракт за десять лет расширился с 64 до 512 бит, у NVidia это 32*32 бита, а у AMD - 64*32 бита. Заметим, что и GPU, и CPU при любом обращении к памяти считывают в кэш сразу порядка 64..256 байт (одну или две кэш-строки), так что SIMD отлично сочетается с современной иерархией памяти+кэша, позволяя одной-двумя командами обработать целую строку кэша, раз уж всё равно из памяти меньше не прочтёшь.

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


Конвейерность - это способ выжать максимум из процессорных ALU, научив запускать их очередную команду каждый такт. В CPU она используется повсеместно, начиная с 80-х годов, например у Intel с i486. При этом каждая отдельная команда по прежнему выполняется несколько тактов, однако конвейерная организация позволяет начать выполнение очередной команды каждый такт. Разумеется, конвейеризация в полной мере используется и в GPU, позволяя достичь производительности в одну арифметическую операцию на каждый ALU каждый такт.

Недостатом конвейеризации является то, что результат операции появляется лишь через несколько тактов, и в течении этих тактов для достижения макс. производительности нужно изыскивать возможности запускать команды, не использующие этот результат. Вероятно, реализация конвейеризации в GPU максимально упрощена для уменьшения объёма оборудования, и в результате этого в NVidia GPU, например, результат любой операции становится доступен только через 11 тактов. Впрочем, в CPU дела обстоят не легче - хотя результаты обычно появляются всего за 1-4 такта, но сами CPU могут выполнять по 3-8 команд за такт, так что иметь десяток промежуточных команд между командами, которые зависимы по данным, всё так же необходимо. И уже в CPU были внедрены два решения этой проблемы - многопоточность (hyper-threading) и внеочередное выполнение (OoOE, out-of-order execution).


Внеочередное выполнение - отличный способ повысить однопоточную производительность, применяемый в "больших ядрах" всех производителей CPU. Суть его в том, что процессор просматривает поток команд на десятки команд вперёд и выполняет их в порядке готовности данных, откладывая выполнение тех, чьи данные ещё не готовы. Однако это требует большого объёма оборудования для "спекулятивного" выполнения и разрешения конфликтов, поэтому в "малых ядрах", таких как старый Атом или ARM A7/A53, не используется. Не используется он и в GPU, поскольку следующий способ требует меньше оборудования и гарантирует полную загрузку ALU, и поэтому для них подходит больше.


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

Таким образом, если на ядре активно 11 потоков, то каждый их них будет получать получать управление раз в 11 тактов. Если вспомнить, что в NVidia GPU результаты выполнения команды появляются как раз через 11 тактов, то получается что с точки зрения одного потока дело будет выглядеть так, будто он исполняется на процессоре с 11-кратно меньшей частотой, зато с выполнением любой команды за 1 такт. И тем самым проблема 11-тактной задержки результата оказывается полностью решена!

Платой за это оказывается, разумеется, возросшее количество потоков выполнения и смехотворная проихзводительность каждого отдельного потока - представьте себе в современных условиях i486 на 100 МГц. Топовый GPU NVidia, Titan X, включает 96 ядер, таким образом для его полной загрузки необходима примерно тысяча потоков! Для CPU это, разумеется, величина несусветная, но GPU и были созданы для решения задач с массовым параллелизмом, где приходится обрабатывать тысячи и миллионы элементов данных. И как видите, архитектура, где один GPU включает сотню ядер, каждое из них выполняет десяток потоков, и в каждом потоке данные обрабатываются блоками по 32 или 64 элемента - оказалась для них наиболее выгодной, позволившей потратить минимум оборудования на то, чтобы загрузить работой тысячи своих ALU.


Суперскалярность, т.е. выполнение нескольких инструкций за один такт, в различных вариантах (VLIW, in-order, out-of-order) используется в CPU для того, чтобы ускорить выполнение однопоточного кода и для того, чтобы задействовать в одном ядре несколько конвейеров и тем самым увеличить соотношение площадей ALU и управляющей логики на кристалле. Хотя первая задача не стоит для GPU, а вторая успешно решается использованием широких SIMD ALU (и простых ядер), тем не менее в некотором ограниченном виде суперскалярность в GPU всё же применяется.

Например, в NVidia Maxwell каждое ядро помимо SIMD ALU содержит также LD/ST unit (SIMD конвейер чтения/записи) и SFU (special functions unit, вычисляющий тригонометрические и другие мат. функции). Для максимального использования оборудования ядро может запускать до двух команд каждый такт - команду ALU и команду LD/ST/SFU, таким образом позволяя достигать пиковой производительности в одну ALU команду/такт в реальных программах, где помимо вычислений требуется ещё и чтение/запись данных.


Таким образом, из сугубо теоретических рассуждений мы пришли к выводу: GPU должен содержать множество ядер, работающих на небольшой частоте. Каждое ядро включает конвейеризованное широкое SIMD ALU и переключается по циклу между 10-20 потоками выполнения. Именно так и устроены реальные GPU от AMD и Nvidia, и так же была устроена Niagara (за исключением отсутствия SIMD). В следующем посте мы рассмотрим как должна выглядеть подсистема кеширования GPU.
 
 
 
Поисковик-затейникitman on August 31st, 2015 03:41 am (UTC)
Спасибо, интересно. А как это соотносится с тем факто, что если в одном потоке GPU случается branch, то он выполняется только в тех потоках, где этот branch есть, а остальные ждут, потом выполняются другие и так далее до точки, где ветки сходятся?
Булат Зиганшинjustbulat on August 31st, 2015 10:54 am (UTC)
Это как раз самая интересная деталь устройства GPU. Каждый поток (я говорю о реальных SIMD потоках, не скалярных абстракциях из CUDA со товарищи) имеет служебный регистр с маской выполнения, первоначально установленной на 1111..111b. Все SIMD операции, производимые потоком, ограничиваются данной маской - т.е. не попадающие под неё SIMD lanes не модифицируются и не записываются в память.

При обработке того, что в скалярном коде описывается как ветвление, GPU на самом деле получает некую маску для своих 32 или 64 simd lanes. Эта маска накладывается на текущий маскировочный регистр и GPU определяет - с какой маской надо обработать первую ветку и с какой - вторую. Если хотя бы одна из этих масок пуста, то GPU просто переходит к другой ветке. Если же обработки требуют обе ветки, то GPU запоминает текущую маску/адрес выполнения и обрабатывает с новой маской ту ветку, у которой в маске осталось меньше бит. Когда эта ветка выполнена до конца, он изменяет маску и обрабатывает вторую ветвь. После обработки второй ветки он восстанавливает первоначальную маску и продолжает выполнение общего для обоих веток кода.

Поскольку такая ситуация может происходить рекурсивно, для сохранения этих данных используется небольшой стёк на 5/6 элементов. Ну а поскольку этот механизм довольно накладен, то при обработке небольших последовательностей команд используется обычное маскирование как в AVX512.

Можно сказать, что этот механизм - наиболее существенное отличие GPU от CPU, которое позволяет их проихводителям говорить о том, что у них SIMT, а не SIMD.
Поисковик-затейникitman on September 1st, 2015 10:06 am (UTC)
Понятно, спасибо. А тут мой знакомый говорит, что требуются специальные операции синхронизации потоков. Это в каком случае?
anonim_legion on March 19th, 2016 11:35 pm (UTC)
А будет ли продолжение?
Булат Зиганшинjustbulat on March 20th, 2016 12:09 am (UTC)
постараюсь. тем более что вторую часть (общие принципы работа с памятью) я наполовину написал. впроем есть и плюс - за полгода я основательно влез в архитектуру GPU и начал понимать как под них оптимизировать. так что и статьи должны получиться куда лучше чем я мог бы написать тогда