Автор: Зубинский А.
Название статьи: NVIDIA Cuda: унификация графики и вычислений
Источник:ITC.ua, Издательский Дом ITC, 2007.
http://itc.ua/node/27969/
Есть простое инженерное правило – специализированные устройства для применения в конкретной области всегда лучше универсальных. Оно остается справедливым вне зависимости от любых технических ухищрений, ведь, на самом деле, определяет и цель проектного процесса, и его следствие, результат. Иными словами, устройство проектируют специализированным именно для того, чтобы оно в чем-то было лучше универсальных.
Различие показателей производительности (естественно, при решении определенных классов задач) специализированных и универсальных вычислителей (или процессоров) хорошо известно. Например, цифровые сигнальные процессоры давно обогнали универсальные CPU по гигафлопам, специфическим для задач обработки сигналов в реальном времени. Но, хоть мощный «сугубо сигнальный» процессор в архитектуре современного ПК фактически не предусмотрен, отыскать пример, удачно демонстрирующий обсуждаемую разницу между специальным и универсальным, в де-факто стандартном ПК несложно. Им является вычислитель графического акселератора (GPU, Graphics Processing Unit).
Вычислительная производительность современных GPU в десятки раз выше мощности многоядерных процессоров примерно того же поколения. Но особый интерес представляют даже не показатели этой производительности, а динамика набора гигафлопов. Если еще три-четыре года назад быстродействие GPU NVIDIA GeForce NX 5800 и центральных процессоров было фактически идентичным (около десяти–двадцати гигафлоп), то к концу прошлого года «числодробительные» способности процессоров выросли всего в пару раз, зато GPU NVIDIA GeForce 8800 GTX демонстрирует почти три с половиной сотни гигафлоп! Естественно, если подобный отрыв сохранится еще несколько лет (а скорее всего, именно так и будет – слишком велико различие в архитектурах универсальных CPU и GPU), решение задачи максимальной утилизации возможностей GPU представляется весьма интересным и даже – многообещающим.
О чем идет речь? Парк персональных компьютеров огромен, суммарная вычислительная мощность их GPU просто колоссальна, при этом GPU – вовсе не уникальные суперкомпьютеры, а давно уже – обычный потребительский продукт. В то же время многие отрасли науки, связанные с масштабными вычислительными экспериментами, остро нуждаются в по-настоящему больших вычислительных мощностях. Возможность нестандартной утилизации производительности GPU обещает такие мощности предоставить, причем независимо от уникальных сверхсистем. Именно по этой причине появление технологии NVIDIA CUDA и программных продуктов, поддерживающих ее, можно считать явлением хоть и в какой-то мере закономерным, но не утрачивающим из-за этой закономерности интересность. Хотя бы потому, что нестандартное использование де-факто стандартных узлов – это, вообще-то, прерогатива хакеров. И когда крупная компания начинает развивать хакинг-технологию, – это очень хорошо и свидетельствует и в пользу того, что времена талантливых инженерных решений вовсе не закончились (как любят апокалиптично утверждать некоторые скептики), и, собственно, в пользу творческого хакинга – процесса, заставляющего «старых собак вытворять новые трюки».
Привычное, традиционное восприятие графического акселератора программистом – интерфейс (API), ориентированный на решение задач визуализации. Чаще всего этой архитектурной модели вполне достаточно – в конце концов, графические API для того и создавались, чтобы скрыть всю бездну аппаратно-микропрограммных нюансов графических акселераторов за унифицированными, независимыми от «железа», программными вызовами. Разработчики CUDA (Compute Unified Device Architecture, унифицированная архитектура вычислительных устройств) ставили перед собой в какой-то мере более сложную задачу – одновременно открыть программисту доступ к аппаратным средствам графического акселератора и не «опускаться» на уровень этих самых аппаратных средств. То есть, архитектурная модель CUDA должна по возможности максимально соответствовать реальной аппаратно-микропрограммной архитектуре графического акселератора, при этом используемые абстракции должны быть по возможности более высокоуровневыми, чтобы программист, решающий ресурсоемкие вычислительные задачи (а основное назначение CUDA – именно научные вычисления), не занимался совершенно чуждым ему низкоуровневым «ассемблерным» кодированием.
Но прежде чем мы начнем ознакомление с предложенным NVIDIA способом решения такой непростой задачи, давайте вспомним об одной сравнительно недавно популярной и уже неплохо забытой архитектуре супервычислителя – Connection Machine (машина соединений). Это, если так можно сказать, машина, выдающаяся во всех отношениях. Ее модель CM-5 отличалась настолько выразительным футуристическим дизайном, что создатели фильма «Парк Юрского периода» в результате кастинга выбрали именно ее (а не имитацию) на роль главного компьютера острова Исла Нублар. Кстати, кастингу команды Стивена Спилберга следует отдать должное еще и потому, что архитектурно CM-5 практически идеально подходила и для решения сложных задач ИИ (искусственного интеллекта, что подчеркивается также спецификой системы программирования CM-5, основанной на Lisp), и для вычислений – идеальная комбинация, востребованная биологами, генными инженерами и специалистами по репродуктивному клонированию (именно этим и занимались на Исла Нубларе). И еще одно «кстати» – производитель супервычислителя CM-5, компания Thinking Machines, хоть и продержалась на рынке недолго, но оставила богатое наследие. Как техническое – после банкротства в августе 1994 г. ее аппаратные наработки приобрела Sun Microsystems (и они помогли компании выйти на рынок сверхвычислений), так и культурное – только краткое описание истории работы в Thinking Machines уже пожилого и тяжело больного, но не теряющего жажды жизни знаменитого Ричарда Фейнмана, само по себе является вкладом в современную Культуру (краткие воспоминания основателя Thinking Machines о Р. Фейнмане: www.longnow.org/views/essays/articles/ArtFeynman.php).
После такого краткого знакомства можно смело утверждать, что архитектура CM-5 столь же нетривиальна, как и вообще все, что связано с компьютерами Thinking Machines. Рассчитанная на масштабирование до миллионов процессорных элементов, CM-5 относится к практически ею же и сформированному классу синхронизированных MIMD-вычислителей. Большинство традиционных MIMD-систем (Multiple Instruction stream, Multiple Data stream, много потоков команд, обрабатывающих много потоков данных) основывается на распределенной памяти (т. е. у каждого процессорного элемента есть доступ только к своему уникальному адресному пространству памяти) и механизме обмена сообщениями. Аппаратно-программную реализацию такого механизма часто называют сетью обмена данными (data network). В CM-5 такая сеть присутствует, но к ней добавлена еще одна отдельная сеть – управляющая, для обмена специальными сообщениями синхронизации вычислительных процессов и управляющими командами. Связанные этими двумя сетями вычислительные элементы CM-5 образуют два 20-мерных гиперкуба – каждый элемент может обмениваться и данными с двадцатью своими «соседями», и (одновременно) – управляющей информацией (изучением эффективности механизмов буферизации в таких гиперкубах как раз и занимался Ричард Фейнман).
Архитектура CUDA в чем-то напоминает CM-5. Во-первых, аппаратными средствами. Разве что в CUDA MIMD-архитектура формируется множеством 32-разрядных SIMD-процессоров (Single Instruction Multiple Data, один поток вычислений, обрабатывающих несколько потоков данных). Каждый вычислитель SIMD-процессора – это, по сути, фрагмент полноценного CPU: собственные арифметико-логическое устройство, шина данных, регистровая память. Общим же для всего SIMD-процессора является декодер команд, управляющий работой всех вычислителей, что и обеспечивает синхронное исполнение ими одной и той же последовательности операций над разными потоками данных. Инструментальные средства и API CUDA создают видение программистом этого вычислителя как относительно универсального устройства, способного независимо, параллельно исполнять множество потоков вычислений посредством множества 32-битовых самостоятельных CPU.
Увы, компьютерная терминология далека от совершенства, поэтому значение термина «поток» в этом контексте следует уточнить – под потоком понимается некоторая исполняющаяся сущность – а именно функция, значение которой вычисляется на основании определенного набора входных данных. Если решаемая задача допускает формализацию на уровне многочисленных независимо и одновременно выполняемых копий функции, обрабатывающих разные наборы данных, – появляется возможность использовать производительность GPU для ускорения вычислений. Роль функции-потока в CUDA столь велика, что бинарное представление их одновременно исполняемых наборов (в системе команд вычислителей GPU) принято называть ядрами (kernels), а каждому потоку присваивать идентификатор. Формирование ядер осуществляется с помощью кросс-компилятора, исполняющегося обычным центральным процессором компьютера, из кода, написанного на весьма C-подобном языке программирования.
Второе специфическое для CUDA понятие – блок потоков (thread block). По сути, блок потоков – это исполняемая сущность ядра, потоки которой обладают возможностью коммуникации. То есть, если ядро – это статический бинарный код, то блок потоков – исполняющиеся вычислителями GPU функции-потоки. Весьма уместной может быть также аналогия с CM-5: блок потоков – это, по сути, исполняющийся на вычислителях CM-5 код, но с одним существенным нюансом – сеть передачи данных здесь реализована не коммутацией пакетов, а с помощью разделяемой памяти. Управляющая сеть также присутствует, например, программист может задать точку синхронизации потоков одного блока, а исполнение некоторых потоков при этом будет автоматически «заторможено» для одновременного достижения потоками исполнений требуемой точки. Следует еще раз явно назвать упомянутое в определении блока потоков значительное ограничение – не существует механизмов, позволяющих потокам из разных блоков обмениваться данными и синхронизировать свое исполнение. Так же, как и каждый отдельный поток, блок потоков имеет собственный идентификатор, так что полный «адрес» потока в CUDA образуется парой «идентификатор блока», «идентификатор потока в блоке».
Каждому потоку доступны следующие ресурсы – регистровая память потока, его локальная память, разделяемая в пределах блока потоков память, а также пул адресных пространств, предназначенных для обмена данными с основным вычислителем системы (иными словами – с программно-аппаратными средствами компьютера, в который вставлена используемая в качестве вычислительного акселератора видеокарта, его принято называть хостом).
Основу аппаратных средств CUDA-вычислителя образует потоковый процессор (SP, Streaming Processor), 32-битовое арифметико-логическое устройство которого и предоставляет каждому потоку вычислительные возможности.
Восемь SP, каждый со своим модулем регистровой памяти емкостью 32 KB, объединяются в потоковый мультипроцессор (SM, Streaming Multiprocessor) – вычислительную машину SIMD-архитектуры, имеющую собственные механизм выборки и декодирования команд, независимые кэши команд и данных (констант), диспетчер потоков и блок памяти емкостью 16 KB, разделяемой между всеми восемью SP. Работающий на тактовой частоте 1,35 GHz, SM способен предоставлять более чем семистам потокам вычислительную мощность порядка 20 GFLOPS. SP выполняет одну SIMD-команду за один машинный такт.
Два SM, дополненные более высокоуровневой кэш-памятью команд и данных, образуют кластер обработки текстур (TPC, Texture Processing Cluster). И, наконец, из восьми TPC формируется собственно CUDA-вычислитель, – массив потоковых процессоров (SPA, Streaming Processor Array).
Таким образом, в распоряжении CUDA-программиста имеется вычислительная система, пусть весьма сложная из-за разнообразия адресных пространств и специфических механизмов, но все же оснащенная 128 32-разрядными арифметико-логическими устройствами, способными за один такт исполнять такие команды, как умножение с накоплением (обычно обозначаются MADD, смысл этой трехоперандной операции понятен из псевдокода A=A+B*C). Потенциальная пиковая производительность такой системы – 346 GFLOPS. Это одновременно и немало, и не очень много, если учесть тот факт, что пиковая производительность четырехъядерных процессоров класса Core 2 Duo совсем немного не дотягивает до 100 GFLOPS.
Сложная архитектура вычислителя, естественно, не может не отразиться на языке программирования. Разработчики NVIDIA в качестве его основы приняли беспроигрышный C, но не смогли обойтись без модификаций. Во-первых, различные адресные пространства, доступные CUDA-программисту, нашли отражение в различных спецификаторах имен функций и переменных. Впрочем, краткого описания архитектуры вполне достаточно для знакомства с ними – если CUDA фактически создает самостоятельную параллельную вычислительную систему, дополняющую возможности хост-машины, значит, инструментарий должен позволять разрабатывать программы как для CUDA-вычислителя, так и для хоста. При написании последних используется спецификатор _host_, а из соображений достижения высокой производительности исполняемые CUDA-вычислителем функции разделяются на два класса – «локальные», доступ к которым (или вызов которых) возможен из адресного пространства вычислителя, и «командные», доступ к которым осуществляется из адресного пространства хоста. Для них используются спецификаторы _device_ и _global_ соответственно.
Кроме явного специфицирования (_device_, _global_ и т. п.) от программиста требуются также знания многочисленных ограничений. Например, «командные функции» (_global_) не могут возвращать значения и принимать параметры, для хранения которых требуется более 256 байтов, все исполняемые CUDA-вычислителем функции не могут быть рекурсивными и т.д. (подобные ограничения – вовсе не новость для программистов, специализирующихся на разработке встраиваемого ПО, но CUDA ориентирована не на них). Спецификаторы переменных в «CUDA C» частично пересекаются со спецификаторами функций (_device_) и отражают множественные нюансы сложной организации вычислителя. Ключевым является спецификатор _shared_, указывающий, что переменная располагается в разделяемой памяти блока потоков и, соответственно, доступна всем потокам. К совершенно немыслимым расширениям языка C следует отнести синтаксис совсем уж не от мира сего спецификаторов, используемых при вызове ядра (так что если вы столкнетесь со списком из трех коротких имен, заключенным в дивные скобки <<< ... >>>, не пугайтесь – это всего лишь указание среде времени исполнения о настройке режима исполнения ядра).
Кроме «отклонений от нормы» на языковом уровне, CUDA предусматривает многоуровневую модель поддержки разработки программ. В нее входят среды времени исполнения программ на хост-машине и вычислителе, набор специфических для параллельного вычислителя библиотек и драйверы – последние предназначены для того, чтобы обещаемая разработчиками CUDA переносимость программ на все видеокарты с GPU семейств NVIDIA 8800 и старше не стала просто обещанием.
Если архитектуру CUDA трудно назвать простой и прозрачной, то для объективности оценок производительности нелегко даже найти подходящие эпитеты. Дело в том, что все существующие на сегодняшний день оценки, по сути, ни о чем не говорят, ведь одновременно и не существует формальных способов утилизации потенциальной производительности GPU, и нет никакой уверенности в том, что уже решенные задачи решены не просто оптимально, а вообще – удачно. И все же... В материалах специалистов NVIDIA и Иллинойсского Университета (Дэвид Кирк, Вен-Мэй У, 2007 г.) демонстрируется пиковое 47-кратное превосходство CUDA-вычислителя над современным двухъядерным процессором при решении задач вычислительной биологии и 197-кратное – для специфических финансовых вычислений. В то же время ряд независимых тестов демонстрирует более скромные, но тем не менее значимые оценки – например, CUDA-вычислитель в несколько раз быстрее справляется с решением задачи умножения больших неразреженных матриц, чем двухпроцессорная машина с двухъядерными CPU класса Opteron 275, при этом программа минимально оптимизировалась под архитектуру.
Естественно, для заинтересованных в серьезных вычислительных мощностях пользователей рабочих станций CUDA не является единственно возможным выбором. Так, компания ClearSpeed серийно выпускает низкопотребляющие (около 10 Вт) PCI-X-платы вычислительных акселераторов с производительностью порядка 50 GFLOPS, при этом способные работать с 64-битовыми числами с плавающей точкой (возможности CUDA сегодня ограничены 32-битовыми вычислениями, правда, конструкторы NVIDIA обещают в обозримом будущем 64-битовую аппаратную платформу). Но если в современной рабочей станции высокопроизводительная видеокарта – обязательный элемент, то возможность «забесплатно» получить с ее помощью дополнительные сотню-другую десятков миллиардов операций с плавающей точкой в секунду трудно назвать лишней. В первую очередь, в образовании, для подготовки программистов, специализирующихся в разработке ПО массово-параллельных систем. Использование CUDA в образовании подкрепляется неплохим уровнем документации и наличием учебных материалов свободного доступа (очень хороший курс для желающих самостоятельно изучить CUDA доступен на сайте факультета вычислительной техники Университета штата Иллинойс). Немаловажно и то, что инструментальные средства CUDA распространяются с открытыми исходными текстами и являются ответвлением весьма серьезного Open Source проекта Qlogic Pathscale – компилятора и библиотек для параллельных кластерных 64-битовых вычислителей.