Главная · Windows Vista · Нужна ли cuda. Что такое NVIDIA CUDA? Модель программирования CUDA

Нужна ли cuda. Что такое NVIDIA CUDA? Модель программирования CUDA

И предназначен для трансляции host-кода (главного, управляющего кода) и device-кода (аппаратного кода) (файлов с расширением.cu) в объектные файлы, пригодные в процессе сборки конечной программы или библиотеки в любой среде программирования, например в NetBeans .

В архитектуре CUDA используется модель памяти грид , кластерное моделирование потоков и SIMD -инструкции. Применима не только для высокопроизводительных графических вычислений, но и для различных научных вычислений с использованием видеокарт nVidia. Ученые и исследователи широко используют CUDA в различных областях, включая астрофизику , вычислительную биологию и химию, моделирование динамики жидкостей, электромагнитных взаимодействий, компьютерную томографию, сейсмический анализ и многое другое. В CUDA имеется возможность подключения к приложениям, использующим OpenGL и Direct3D . CUDA - кроссплатформенное программное обеспечение для таких операционных систем как Linux , Mac OS X и Windows .

22 марта 2010 года nVidia выпустила CUDA Toolkit 3.0, который содержал поддержку OpenCL .

Оборудование

Платформа CUDA Впервые появились на рынке с выходом чипа NVIDIA восьмого поколения G80 и стала присутствовать во всех последующих сериях графических чипов, которые используются в семействах ускорителей GeForce , Quadro и NVidia Tesla .

Первая серия оборудования, поддерживающая CUDA SDK, G8x, имела 32-битный векторный процессор одинарной точности , использующий CUDA SDK как API (CUDA поддерживает тип double языка Си, однако сейчас его точность понижена до 32-битного с плавающей запятой). Более поздние процессоры GT200 имеют поддержку 64-битной точности (только для SFU), но производительность значительно хуже, чем для 32-битной точности (из-за того, что SFU всего два на каждый потоковый мультипроцессор, а скалярных процессоров - восемь). Графический процессор организует аппаратную многопоточность, что позволяет задействовать все ресурсы графического процессора. Таким образом, открывается перспектива переложить функции физического ускорителя на графический ускоритель (пример реализации - nVidia PhysX). Также открываются широкие возможности использования графического оборудования компьютера для выполнения сложных неграфических вычислений: например, в вычислительной биологии и в иных отраслях науки.

Преимущества

По сравнению с традиционным подходом к организации вычислений общего назначения посредством возможностей графических API, у архитектуры CUDA отмечают следующие преимущества в этой области:

Ограничения

  • Все функции, выполнимые на устройстве, не поддерживают рекурсии (в версии CUDA Toolkit 3.1 поддерживает указатели и рекурсию) и имеют некоторые другие ограничения

Поддерживаемые GPU и графические ускорители

Перечень устройств от производителя оборудования Nvidia с заявленной полной поддержкой технологии CUDA приведён на официальном сайте Nvidia: CUDA-Enabled GPU Products (англ.) .

Фактически же, в настоящее время на рынке аппаратных средств для ПК поддержку технологии CUDA обеспечивают следующие периферийные устройства :

Версия спецификации GPU Видеокарты
1.0 G80, G92, G92b, G94, G94b GeForce 8800GTX/Ultra, 9400GT, 9600GT, 9800GT, Tesla C/D/S870, FX4/5600, 360M, GT 420
1.1 G86, G84, G98, G96, G96b, G94, G94b, G92, G92b GeForce 8400GS/GT, 8600GT/GTS, 8800GT/GTS, 9600 GSO, 9800GTX/GX2, GTS 250, GT 120/30/40, FX 4/570, 3/580, 17/18/3700, 4700x2, 1xxM, 32/370M, 3/5/770M, 16/17/27/28/36/37/3800M, NVS420/50
1.2 GT218, GT216, GT215 GeForce 210, GT 220/40, FX380 LP, 1800M, 370/380M, NVS 2/3100M
1.3 GT200, GT200b GeForce GTX 260, GTX 275, GTX 280, GTX 285, GTX 295, Tesla C/M1060, S1070, Quadro CX, FX 3/4/5800
2.0 GF100, GF110 GeForce (GF100) GTX 465, GTX 470, GTX 480, Tesla C2050, C2070, S/M2050/70, Quadro Plex 7000, Quadro 4000, 5000, 6000, GeForce (GF110) GTX 560 TI 448, GTX570, GTX580, GTX590
2.1 GF104, GF114, GF116, GF108, GF106 GeForce 610M, GT 430, GT 440, GTS 450, GTX 460, GTX 550 Ti, GTX 560, GTX 560 Ti, 500M, Quadro 600, 2000
3.0 GK104, GK106, GK107 GeForce GTX 690, GTX 680, GTX 670, GTX 660 Ti, GTX 660, GTX 650 Ti, GTX 650, GT 640, GeForce GTX 680MX, GeForce GTX 680M, GeForce GTX 675MX, GeForce GTX 670MX, GTX 660M, GeForce GT 650M, GeForce GT 645M, GeForce GT 640M
3.5 GK110
Nvidia GeForce для настольных компьютеров
GeForce GTX 590
GeForce GTX 580
GeForce GTX 570
GeForce GTX 560 Ti
GeForce GTX 560
GeForce GTX 550 Ti
GeForce GTX 520
GeForce GTX 480
GeForce GTX 470
GeForce GTX 465
GeForce GTX 460
GeForce GTS 450
GeForce GTX 295
GeForce GTX 285
GeForce GTX 280
GeForce GTX 275
GeForce GTX 260
GeForce GTS 250
GeForce GT 240
GeForce GT 220
GeForce 210
GeForce GTS 150
GeForce GT 130
GeForce GT 120
GeForce G100
GeForce 9800 GX2
GeForce 9800 GTX+
GeForce 9800 GTX
GeForce 9800 GT
GeForce 9600 GSO
GeForce 9600 GT
GeForce 9500 GT
GeForce 9400 GT
GeForce 9400 mGPU
GeForce 9300 mGPU
GeForce 8800 GTS 512
GeForce 8800 GT
GeForce 8600 GTS
GeForce 8600 GT
GeForce 8500 GT
GeForce 8400 GS
Nvidia GeForce для мобильных компьютеров
GeForce GTX 580M
GeForce GTX 570M
GeForce GTX 560M
GeForce GT 555M
GeForce GT 540M
GeForce GT 525M
GeForce GT 520M
GeForce GTX 485M
GeForce GTX 480M
GeForce GTX 470M
GeForce GTX 460M
GeForce GT 445M
GeForce GT 435M
GeForce GT 425M
GeForce GT 420M
GeForce GT 415M
GeForce GTX 285M
GeForce GTX 280M
GeForce GTX 260M
GeForce GTS 360M
GeForce GTS 350M
GeForce GTS 160M
GeForce GTS 150M
GeForce GT 335M
GeForce GT 330M
GeForce GT 325M
GeForce GT 240M
GeForce GT 130M
GeForce G210M
GeForce G110M
GeForce G105M
GeForce 310M
GeForce 305M
GeForce 9800M GTX
GeForce 9800M GT
GeForce 9800M GTS
GeForce 9700M GTS
GeForce 9700M GT
GeForce 9650M GS
GeForce 9600M GT
GeForce 9600M GS
GeForce 9500M GS
GeForce 9500M G
GeForce 9300M GS
GeForce 9300M G
GeForce 9200M GS
GeForce 9100M G
GeForce 8800M GTS
GeForce 8700M GT
GeForce 8600M GT
GeForce 8600M GS
GeForce 8400M GT
GeForce 8400M GS
Nvidia Tesla *
Tesla C2050/C2070
Tesla M2050/M2070/M2090
Tesla S2050
Tesla S1070
Tesla M1060
Tesla C1060
Tesla C870
Tesla D870
Tesla S870
Nvidia Quadro для настольных компьютеров
Quadro 6000
Quadro 5000
Quadro 4000
Quadro 2000
Quadro 600
Quadro FX 5800
Quadro FX 5600
Quadro FX 4800
Quadro FX 4700 X2
Quadro FX 4600
Quadro FX 3700
Quadro FX 1700
Quadro FX 570
Quadro FX 470
Quadro FX 380 Low Profile
Quadro FX 370
Quadro FX 370 Low Profile
Quadro CX
Quadro NVS 450
Quadro NVS 420
Quadro NVS 290
Quadro Plex 2100 D4
Quadro Plex 2200 D2
Quadro Plex 2100 S4
Quadro Plex 1000 Model IV
Nvidia Quadro для мобильных компьютеров
Quadro 5010M
Quadro 5000M
Quadro 4000M
Quadro 3000M
Quadro 2000M
Quadro 1000M
Quadro FX 3800M
Quadro FX 3700M
Quadro FX 3600M
Quadro FX 2800M
Quadro FX 2700M
Quadro FX 1800M
Quadro FX 1700M
Quadro FX 1600M
Quadro FX 880M
Quadro FX 770M
Quadro FX 570M
Quadro FX 380M
Quadro FX 370M
Quadro FX 360M
Quadro NVS 5100M
Quadro NVS 4200M
Quadro NVS 3100M
Quadro NVS 2100M
Quadro NVS 320M
Quadro NVS 160M
Quadro NVS 150M
Quadro NVS 140M
Quadro NVS 135M
Quadro NVS 130M
  • Модели Tesla C1060, Tesla S1070, Tesla C2050/C2070, Tesla M2050/M2070, Tesla S2050 позволяют производить вычисления на GPU с двойной точностью.

Особенности и спецификации различных версий

Feature support (unlisted features are
supported for all compute capabilities)
Compute capability (version)
1.0 1.1 1.2 1.3 2.x

32-bit words in global memory
Нет Да

floating point values in global memory
Integer atomic functions operating on
32-bit words in shared memory
Нет Да
atomicExch() operating on 32-bit
floating point values in shared memory
Integer atomic functions operating on
64-bit words in global memory
Warp vote functions
Double-precision floating-point operations Нет Да
Atomic functions operating on 64-bit
integer values in shared memory
Нет Да
Floating-point atomic addition operating on
32-bit words in global and shared memory
_ballot()
_threadfence_system()
_syncthreads_count(),
_syncthreads_and(),
_syncthreads_or()
Surface functions
3D grid of thread block
Technical specifications Compute capability (version)
1.0 1.1 1.2 1.3 2.x
Maximum dimensionality of grid of thread blocks 2 3
Maximum x-, y-, or z-dimension of a grid of thread blocks 65535
Maximum dimensionality of thread block 3
Maximum x- or y-dimension of a block 512 1024
Maximum z-dimension of a block 64
Maximum number of threads per block 512 1024
Warp size 32
Maximum number of resident blocks per multiprocessor 8
Maximum number of resident warps per multiprocessor 24 32 48
Maximum number of resident threads per multiprocessor 768 1024 1536
Number of 32-bit registers per multiprocessor 8 K 16 K 32 K
Maximum amount of shared memory per multiprocessor 16 KB 48 KB
Number of shared memory banks 16 32
Amount of local memory per thread 16 KB 512 KB
Constant memory size 64 KB
Cache working set per multiprocessor for constant memory 8 KB
Cache working set per multiprocessor for texture memory Device dependent, between 6 KB and 8 KB
Maximum width for 1D texture
8192 32768
Maximum width for 1D texture
reference bound to linear memory
2 27
Maximum width and number of layers
for a 1D layered texture reference
8192 x 512 16384 x 2048
Maximum width and height for 2D
texture reference bound to
linear memory or a CUDA array
65536 x 32768 65536 x 65535
Maximum width, height, and number
of layers for a 2D layered texture reference
8192 x 8192 x 512 16384 x 16384 x 2048
Maximum width, height and depth
for a 3D texture reference bound to linear
memory or a CUDA array
2048 x 2048 x 2048
Maximum number of textures that
can be bound to a kernel
128
Maximum width for a 1D surface
reference bound to a CUDA array
Not
supported
8192
Maximum width and height for a 2D
surface reference bound to a CUDA array
8192 x 8192
Maximum number of surfaces that
can be bound to a kernel
8
Maximum number of instructions per
kernel
2 million

Пример

CudaArray* cu_array; texture< float , 2 > tex; // Allocate array cudaMalloc( & cu_array, cudaCreateChannelDesc< float> () , width, height ) ; // Copy image data to array cudaMemcpy( cu_array, image, width* height, cudaMemcpyHostToDevice) ; // Bind the array to the texture cudaBindTexture( tex, cu_array) ; // Run kernel dim3 blockDim(16 , 16 , 1 ) ; dim3 gridDim(width / blockDim.x , height / blockDim.y , 1 ) ; kernel<<< gridDim, blockDim, 0 >>> (d_odata, width, height) ; cudaUnbindTexture(tex) ; __global__ void kernel(float * odata, int height, int width) { unsigned int x = blockIdx.x * blockDim.x + threadIdx.x ; unsigned int y = blockIdx.y * blockDim.y + threadIdx.y ; float c = texfetch(tex, x, y) ; odata[ y* width+ x] = c; }

Import pycuda.driver as drv import numpy drv.init () dev = drv.Device (0 ) ctx = dev.make_context () mod = drv.SourceModule (""" __global__ void multiply_them(float *dest, float *a, float *b) { const int i = threadIdx.x; dest[i] = a[i] * b[i]; } """ ) multiply_them = mod.get_function ("multiply_them" ) a = numpy.random .randn (400 ) .astype (numpy.float32 ) b = numpy.random .randn (400 ) .astype (numpy.float32 ) dest = numpy.zeros_like (a) multiply_them( drv.Out (dest) , drv.In (a) , drv.In (b) , block= (400 , 1 , 1 ) ) print dest-a*b

CUDA как предмет в вузах

По состоянию на декабрь 2009 года, программная модель CUDA преподается в 269 университетах по всему миру. В России обучающие курсы по CUDA читаются в Санкт-Петербургском политехническом университете , Ярославском государственном университете им. П. Г. Демидова , Московском , Нижегородском , Санкт-Петербургском , Тверском , Казанском , Новосибирском , Новосибирском государственном техническом университете Омском и Пермском государственных университетах, Международном университете природы общества и человека «Дубна» , Ивановском государственном энергетическом университете , Белгородский государственный университет , МГТУ им. Баумана , РХТУ им. Менделеева , Межрегиональном суперкомпьютерном центре РАН, . Кроме того, в декабре 2009 года было объявлено о начале работы первого в России научно-образовательного центра «Параллельные вычисления», расположенного в городе Дубна , в задачи которого входят обучение и консультации по решению сложных вычислительных задач на GPU.

На Украине курсы по CUDA читаются в Киевском институте системного анализа.

Ссылки

Официальные ресурсы

  • CUDA Zone (рус.) - официальный сайт CUDA
  • CUDA GPU Computing (англ.) - официальные веб-форумы, посвящённые вычислениям CUDA

Неофициальные ресурсы

Tom"s Hardware
  • Дмитрий Чеканов. nVidia CUDA: вычисления на видеокарте или смерть CPU? . Tom"s Hardware (22 июня 2008 г.). Архивировано
  • Дмитрий Чеканов. nVidia CUDA: тесты приложений на GPU для массового рынка . Tom"s Hardware (19 мая 2009 г.). Архивировано из первоисточника 4 марта 2012. Проверено 19 мая 2009.
iXBT.com
  • Алексей Берилло. NVIDIA CUDA - неграфические вычисления на графических процессорах. Часть 1 . iXBT.com (23 сентября 2008 г.). Архивировано из первоисточника 4 марта 2012. Проверено 20 января 2009.
  • Алексей Берилло. NVIDIA CUDA - неграфические вычисления на графических процессорах. Часть 2 . iXBT.com (22 октября 2008 г.). - Примеры внедрения NVIDIA CUDA. Архивировано из первоисточника 4 марта 2012. Проверено 20 января 2009.
Другие ресурсы
  • Боресков Алексей Викторович. Основы CUDA (20 января 2009 г.). Архивировано из первоисточника 4 марта 2012. Проверено 20 января 2009.
  • Владимир Фролов. Введение в технологию CUDA . Сетевой журнал «Компьютерная графика и мультимедиа» (19 декабря 2008 г.). Архивировано из первоисточника 4 марта 2012. Проверено 28 октября 2009.
  • Игорь Осколков. NVIDIA CUDA – доступный билет в мир больших вычислений . Компьютерра (30 апреля 2009 г.). Проверено 3 мая 2009.
  • Владимир Фролов. Введение в технологию CUDA (1 августа 2009 г.). Архивировано из первоисточника 4 марта 2012. Проверено 3 апреля 2010.
  • GPGPU.ru . Использование видеокарт для вычислений
  • . Центр Параллельных Вычислений

Примечания

См. также

– набор низкоуровневых программных интерфейсов (API ) для создания игр и других высокопроизводительных мультимедиа-приложений. Включает поддержку высокопроизводительной 2D - и 3D -графики, звука и устройств ввода.

Direct3D (D3D ) – интерфейс вывода трёхмерных примитивов (геометрических тел). Входит в .

OpenGL (от англ. Open Graphics Library , дословно – открытая графическая библиотека) – спецификация, определяющая независимый от языка программирования кросс-платформенный программный интерфейс для написания приложений, использующих двухмерную и трёхмерную компьютерную графику. Включает более 250 функций для рисования сложных трёхмерных сцен из простых примитивов. Используется при создании видеоигр, виртуальной реальности, визуализации в научных исследованиях. На платформе Windows конкурирует с .

OpenCL (от англ. Open Computing Language , дословно – открытый язык вычислений) – фреймворк (каркас программной системы) для написания компьютерных программ, связанных с параллельными вычислениями на различных графических (GPU ) и ( ). В фреймворк OpenCL входят язык программирования и интерфейс программирования приложений (API ). OpenCL обеспечивает параллелизм на уровне инструкций и на уровне данных и является реализацией техники GPGPU .

GPGPU (сокр. от англ. General-P urpose G raphics P rocessing U nits , дословно – GPU общего назначения) – техника использования графического процессоравидеокарты для общих вычислений, которые обычно проводит .

Шейдер (англ. shader ) – программа построения теней на синтезируемых изображениях, используется в трёхмерной графике для определенияокончательных параметров объекта или изображения. Как правило, включает произвольной сложности описание поглощения и рассеяния света, наложениятекстуры, отражения и преломления, затенения, смещения поверхности и эффекты пост-обработки. Сложные поверхности могут быть визуализированы припомощи простых геометрических форм.

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

SDK (сокр. от англ. Software Development Kit ) – набор инструментальных средств разработки программного .

CPU (сокр. от англ. Central Processing Unit , дословно – центральное/основное/главное вычислительное устройство) – центральный (микро) ;устройство, исполняющее машинные инструкции; часть аппаратного обеспечения , отвечающая за выполнение вычислительных операций (заданныхоперационной системой и прикладным программным ) и координирующая работу всех устройств .

GPU (сокр. от англ. Graphic Processing Unit , дословно – графическое вычислительное устройство) – графический процессор; отдельное устройство илиигровой приставки, выполняющее графический рендеринг (визуализацию). Современные графические процессоры очень эффективно обрабатывают иреалистично отображают компьютерную графику. Графический процессор в современных видеоадаптерах применяется в качестве ускорителя трёхмернойграфики, однако его можно использовать в некоторых случаях и для вычислений (GPGPU ).

Проблемы CPU

Долгое время повышение производительности традиционных в основном происходило за счёт последовательного увеличения тактовой частоты (около 80% производительности определяла именно тактовая частота) с одновременным увеличением количества транзисторов на одном кристалле. Однако дальнейшее повышение тактовой частоты (при тактовой частоте более 3,8 ГГц чипы попросту перегреваются!) упирается в ряд фундаментальных физических барьеров (поскольку технологический процесс почти вплотную приблизился к размерам атома: , а размеры атома кремния – приблизительно 0,543 нм):

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

Во-вторых, преимущества более высокой тактовой частоты частично сводятся на нет из-за задержек при обращении к памяти, так как время доступа к памяти не соответствует возрастающим тактовым частотам;

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

Развитие GPU

Параллельно с шло (и идет!) развитие GPU :

Ноябрь 2008 г. – Intel представила линейку 4-ядерных Intel Core i7 , в основу которых положена микроархитектура нового поколения Nehalem . Процессоры работают на тактовой частоте 2,6-3,2 ГГц. Выполнены по 45-нм техпроцессу.

Декабрь 2008 г. – начались поставки 4-ядерного AMD Phenom II 940 (кодовое название – Deneb ). Работает на частоте 3 ГГц, выпускается по техпроцессу 45-нм.

Май 2009 г. – компания AMD представила версию графического процессора ATI Radeon HD 4890 с тактовой частотой ядра, увеличенной с 850 МГц до 1 ГГц. Это первый графический процессор, работающий на частоте 1 ГГц. Вычислительная мощность чипа, благодаря увеличению частоты, выросла с 1,36 до 1,6 терафлоп. Процессор содержит 800 (!) вычислительных ядер, поддерживает видеопамять GDDR5 , DirectX 10.1 , ATI CrossFireX и все другие технологии, присущие современным моделям видеокарт. Чип изготовлен на базе 55-нм технологии.

Основные отличия GPU

Отличительными особенностями GPU (по сравнению с ) являются:

– архитектура, максимально нацеленная на увеличение скорости расчёта текстур и сложных графических объектов;

– пиковая мощность типичного GPU намного выше, чем у ;

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

«Кризис жанра»

«Кризис жанра» для назрел к 2005 г., – именно тогда появились . Но, несмотря на развитие технологии , рост производительности обычных заметно снизился. В то же время производительность GPU продолжает расти. Так, к 2003 г. и кристаллизовалась эта революционная идея – использовать для нужд вычислительную мощь графического . Графические процессоры стали активно использоваться для «неграфических» вычислений (симуляция физики, обработка сигналов, вычислительная математика/геометрия, операции с базами данных, вычислительная биология, вычислительная экономика, компьютерное зрение и т.д.).

Главная проблема заключалась в том, что не было никакого стандартного интерфейса для программирования GPU . Разработчики использовали OpenGL или Direct3D , но это было очень удобно. Корпорация NVIDIA (один из крупнейших производителей графических, медиа- и коммуникационных процессоров, а также беспроводных медиа-процессоров; основана в 1993 г.) занялась разработкой некоего единого и удобного стандарта, – и представила технологию CUDA .

Как это начиналось

2006 г. – NVIDIA демонстрирует CUDA™ ; начало революции в вычислениях на GPU .

2007 г. – NVIDIA выпускает архитектуру CUDA (первоначальная версия CUDA SDK была представлена 15 февраля 2007 г.); номинация «Лучшая новинка» от журнала Popular Science и «Выбор читателей» от издания HPCWire .

2008 г. – технология NVIDIA CUDA победила в номинации «Техническое превосходство» от PC Magazine .

Что такое CUDA

CUDA (сокр. от англ. Compute Unified Device Architecture , дословно – унифицированная вычислительная архитектура устройств) – архитектура (совокупность программных и аппаратных средств), позволяющая производить на GPU вычисления общего назначения, при этом GPU фактически выступает в роли мощного сопроцессора.

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

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

Уральский, ведущий специалист по технологиям NVIDIA , сравнивая GPU и , говорит так : « – это внедорожник. Он ездит всегда и везде, но не очень быстро. А GPU – это спорткар. На плохой дороге он просто никуда не поедет, но дайте хорошее покрытие, – и он покажет всю свою скорость, которая внедорожнику и не снилась!..».

Возможности технологии CUDA

Я расскажу о ключевых моментах компилятора CUDA, интерфейсе CUDA runtime API, ну, и в заключение, приведу пример использования CUDA для несложных математических вычислений.

Приступим.

Вычислительная модель GPU:

Рассмотрим вычислительную модель GPU более подробно.

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

CUDA и язык C:

Сама технология CUDA (компилятор nvcc.exe) вводит ряд дополнительных расширений для языка C, которые необходимы для написания кода для GPU:
  1. Спецификаторы функций, которые показывают, как и откуда буду выполняться функции.
  2. Спецификаторы переменных, которые служат для указания типа используемой памяти GPU.
  3. Спецификаторы запуска ядра GPU.
  4. Встроенные переменные для идентификации нитей, блоков и др. параметров при исполнении кода в ядре GPU .
  5. Дополнительные типы переменных.
Как было сказано, спецификаторы функций определяют, как и откуда буду вызываться функции. Всего в CUDA 3 таких спецификатора:
  • __host__ - выполнятся на CPU, вызывается с CPU (в принципе его можно и не указывать).
  • __global__ - выполняется на GPU, вызывается с CPU.
  • __device__ - выполняется на GPU, вызывается с GPU.
Спецификаторы запуска ядра служат для описания количества блоков, нитей и памяти, которые вы хотите выделить при расчете на GPU. Синтаксис запуска ядра имеет следующий вид:

MyKernelFunc<<>>(float* param1,float* param2), где

  • gridSize – размерность сетки блоков (dim3), выделенную для расчетов,
  • blockSize – размер блока (dim3), выделенного для расчетов,
  • sharedMemSize – размер дополнительной памяти, выделяемой при запуске ядра,
  • cudaStream – переменная cudaStream_t, задающая поток, в котором будет произведен вызов.
Ну и конечно сама myKernelFunc – функция ядра (спецификатор __global__). Некоторые переменные при вызове ядра можно опускать, например sharedMemSize и cudaStream.

Так же стоит упомянуть о встроенных переменных:

  • gridDim – размерность грида, имеет тип dim3. Позволяет узнать размер гридa, выделенного при текущем вызове ядра.
  • blockDim – размерность блока, так же имеет тип dim3. Позволяет узнать размер блока, выделенного при текущем вызове ядра.
  • blockIdx – индекс текущего блока в вычислении на GPU, имеет тип uint3.
  • threadIdx – индекс текущей нити в вычислении на GPU, имеет тип uint3.
  • warpSize – размер warp’а, имеет тип int (сам еще не пробовал использовать).
Кстати, gridDim и blockDim и есть те самые переменные, которые мы передаем при запуске ядра GPU, правда, в ядре они могут быть read only.

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

CUDA host API:

Перед тем, как приступить к непосредственному использованию CUDA для вычислений, необходимо ознакомиться с так называемым CUDA host API, который является связующим звеном между CPU и GPU. CUDA host API в свою очередь можно разделить на низкоуровневое API под названием CUDA driver API, который предоставляет доступ к драйверу пользовательского режима CUDA, и высокоуровневое API – CUDA runtime API. В своих примерах я буду использовать CUDA runtime API.

В CUDA runtime API входят следующие группы функций:

  • Device Management – включает функции для общего управления GPU (получение инфор-мации о возможностях GPU, переключение между GPU при работе SLI-режиме и т.д.).
  • Thread Management – управление нитями.
  • Stream Management – управление потоками.
  • Event Management – функция создания и управления event’ами.
  • Execution Control – функции запуска и исполнения ядра CUDA.
  • Memory Management – функции управлению памятью GPU.
  • Texture Reference Manager – работа с объектами текстур через CUDA.
  • OpenGL Interoperability – функции по взаимодействию с OpenGL API.
  • Direct3D 9 Interoperability – функции по взаимодействию с Direct3D 9 API.
  • Direct3D 10 Interoperability – функции по взаимодействию с Direct3D 10 API.
  • Error Handling – функции обработки ошибок.

Понимаем работу GPU:

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

Задача. Требуется вычислить сумму двух векторов размерностью N элементов.

Нам известна максимальные размеры нашего блока: 512*512*64 нитей. Так как вектор у нас одномерный, то пока ограничимся использованием x-измерения нашего блока, то есть задействуем только одну полосу нитей из блока (рис. 3).

Заметим, что x-размерность блока 512, то есть, мы можем сложить за один раз векторы, длина которых N <= 512 элементов. В прочем, при более массивных вычислениях, можно использовать большее число блоков и многомерные массивы. Так же я заметил одну интересную особенность, возможно, некоторые из вас подумали, что в одном блоке можно задействовать 512*512*64 = 16777216 нитей, естественно это не так, в целом, это произведение не может превышать 512 (по крайней мере, на моей видеокарте).

В самой программе необходимо выполнить следующие этапы:

  1. Получить данные для расчетов.
  2. Скопировать эти данные в GPU память.
  3. Произвести вычисление в GPU через функцию ядра.
  4. Скопировать вычисленные данные из GPU памяти в ОЗУ.
  5. Посмотреть результаты.
  6. Высвободить используемые ресурсы.
Переходим непосредственно к написанию кода:

Первым делом напишем функцию ядра, которая и будет осуществлять сложение векторов:

// Функция сложения двух векторов
__global__ void addVector(float * left, float * right, float * result)
{
//Получаем id текущей нити.
int idx = threadIdx.x;

//Расчитываем результат.
result = left + right;
}


Таким образом, распараллеливание будет выполнено автоматически при запуске ядра. В этой функции так же используется встроенная переменная threadIdx и её поле x, которая позволяет задать соответствие между расчетом элемента вектора и нитью в блоке. Делаем расчет каждого элемента вектора в отдельной нити.

Пишем код, которые отвечает за 1 и 2 пункт в программе:

#define SIZE 512
__host__ int main()
{
//Выделяем память под вектора
float * vec1 = new float ;
float * vec2 = new float ;
float * vec3 = new float ;

//Инициализируем значения векторов
for (int i = 0; i < SIZE; i++)
{
vec1[i] = i;
vec2[i] = i;
}

//Указатели на память видеокарте
float * devVec1;
float * devVec2;
float * devVec3;

//Выделяем память для векторов на видеокарте
cudaMalloc((void **)&devVec1, sizeof (float ) * SIZE);
cudaMalloc((void **)&devVec2, sizeof (float ) * SIZE);
cudaMalloc((void **)&devVec3, sizeof (float ) * SIZE);

//Копируем данные в память видеокарты
cudaMemcpy(devVec1, vec1, sizeof (float ) * SIZE, cudaMemcpyHostToDevice);
cudaMemcpy(devVec2, vec2, sizeof (float ) * SIZE, cudaMemcpyHostToDevice);

}


* This source code was highlighted with Source Code Highlighter .

Для выделения памяти на видеокарте используется функция cudaMalloc , которая имеет следующий прототип:
cudaError_t cudaMalloc(void** devPtr, size_t count), где

  1. devPtr – указатель, в который записывается адрес выделенной памяти,
  2. count – размер выделяемой памяти в байтах.
Возвращает:
  1. cudaSuccess – при удачном выделении памяти
  2. cudaErrorMemoryAllocation – при ошибке выделения памяти
Для копирования данных в память видеокарты используется cudaMemcpy, которая имеет следующий прототип:
cudaError_t cudaMemcpy(void* dst, const void* src ,size_t count, enum cudaMemcpyKind kind), где
  1. dst – указатель, содержащий адрес места-назначения копирования,
  2. src – указатель, содержащий адрес источника копирования,
  3. count – размер копируемого ресурса в байтах,
  4. cudaMemcpyKind – перечисление, указывающее направление копирования (может быть cudaMemcpyHostToDevice, cudaMemcpyDeviceToHost, cudaMemcpyHostToHost, cudaMemcpyDeviceToDevice).
Возвращает:
  1. cudaSuccess – при удачном копировании
  2. cudaErrorInvalidValue – неверные параметры аргумента (например, размер копирования отрицателен)
  3. cudaErrorInvalidDevicePointer – неверный указатель памяти в видеокарте
  4. cudaErrorInvalidMemcpyDirection – неверное направление (например, перепутан источник и место-назначение копирования)
Теперь переходим к непосредственному вызову ядра для вычисления на GPU.

dim3 gridSize = dim3(1, 1, 1); //Размер используемого грида
dim3 blockSize = dim3(SIZE, 1, 1); //Размер используемого блока


addVector<<>>(devVec1, devVec2, devVec3);


* This source code was highlighted with Source Code Highlighter .

В нашем случае определять размер грида и блока необязательно, так как используем всего один блок и одно измерение в блоке, поэтому код выше можно записать:
addVector<<<1, SIZE>>>(devVec1, devVec2, devVec3);

* This source code was highlighted with Source Code Highlighter .


Теперь нам остаеться скопировать результат расчета из видеопамяти в память хоста. Но у функций ядра при этом есть особенность – асинхронное исполнение, то есть, если после вызова ядра начал работать следующий участок кода, то это ещё не значит, что GPU выполнил расчеты. Для завершения работы заданной функции ядра необходимо использовать средства синхронизации, например event’ы. Поэтому, перед копированием результатов на хост выполняем синхронизацию нитей GPU через event.

Код после вызова ядра:

//Выполняем вызов функции ядра
addVector<<>>(devVec1, devVec2, devVec3);

//Хендл event"а
cudaEvent_t syncEvent;

CudaEventCreate(&syncEvent); //Создаем event
cudaEventRecord(syncEvent, 0); //Записываем event
cudaEventSynchronize(syncEvent); //Синхронизируем event

//Только теперь получаем результат расчета
cudaMemcpy(vec3, devVec3, sizeof (float ) * SIZE, cudaMemcpyDeviceToHost);


* This source code was highlighted with Source Code Highlighter .

Рассмотрим более подробно функции из Event Managment API.

Event создается с помощью функции cudaEventCreate , прототип которой имеет вид:
cudaError_t cudaEventCreate(cudaEvent_t* event), где

  1. *event – указатель для записи хендла event’а.
Возвращает:
  1. cudaSuccess – в случае успеха
  2. cudaErrorMemoryAllocation – ошибка выделения памяти
Запись event’а осуществляется с помощью функции cudaEventRecord , прототип которой имеет вид:
cudaError_t cudaEventRecord(cudaEvent_t event, CUstream stream), где
  1. event – хендл хаписываемого event’а,
  2. stream – номер потока, в котором записываем (в нашем случае это основной нулевой по-ток).
Возвращает:
  1. cudaSuccess – в случае успеха
  2. cudaErrorInvalidValue – неверное значение
  3. cudaErrorInitializationError – ошибка инициализации
  4. cudaErrorPriorLaunchFailure – ошибка при предыдущем асинхронном запуске функции
Синхронизация event’а выполняется функцией cudaEventSynchronize. Данная функция ожидает окончание работы всех нитей GPU и прохождение заданного event’а и только потом отдает управление вызывающей программе. Прототип функции имеет вид:
cudaError_t cudaEventSynchronize(cudaEvent_t event), где
  1. event – хендл event’а, прохождение которого ожидается.
Возвращает:
  1. cudaSuccess – в случае успеха
  2. cudaErrorInitializationError – ошибка инициализации
  3. cudaErrorPriorLaunchFailure – ошибка при предыдущем асинхронном запуске функции
  4. cudaErrorInvalidValue – неверное значение
  5. cudaErrorInvalidResourceHandle – неверный хендл event’а
Понять, как работает cudaEventSynchronize, можно из следующей схемы:

На рисунке 4 блок «Ожидание прохождения Event’а» и есть вызов функции cudaEventSynchronize.

Ну и в заключении выводим результат на экран и чистим выделенные ресурсы.

//Результаты расчета
for (int i = 0; i < SIZE; i++)
{
printf("Element #%i: %.1f\n" , i , vec3[i]);
}

//
// Высвобождаем ресурсы
//

CudaEventDestroy(syncEvent);

CudaFree(devVec1);
cudaFree(devVec2);
cudaFree(devVec3);

Delete vec1; vec1 = 0;
delete vec2; vec2 = 0;
delete vec3; vec3 = 0;


* This source code was highlighted with Source Code Highlighter .

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

Заключение

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

P.S.: Получилось не очень кратко. Надеюсь, что не утомил. Если нужен весь исходный код, то могу выслать на почту.
P.S.S: Задавайте вопросы.

Теги:

  • CUDA
  • gpgpu
  • nvidia
Добавить метки

Позвольте обратиться к истории - вернуться в 2003 год, когда Intel и AMD участвовали в совместной гонке за самый мощный процессор. Всего за несколько лет в результате этой гонки тактовые частоты существенно выросли, особенно после выхода Intel Pentium 4.

Но гонка быстро приближалась к пределу. После волны огромного прироста тактовых частот (между 2001 и 2003 годами тактовая частота Pentium 4 удвоилась с 1,5 до 3 ГГц), пользователям пришлось довольствоваться десятыми долями гигагерц, которые смогли выжать производители (с 2003 до 2005 тактовые частоты увеличились всего с 3 до 3,8 ГГц).

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



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

Почему же они не зашли в тот же тупик, как разработчики архитектуры CPU? Причина очень простая: центральные процессоры разрабатываются для получения максимальной производительности на потоке инструкций, которые обрабатывают разные данные (как целые числа, так и числа с плавающей запятой), производят случайный доступ к памяти и т.д. До сих пор разработчики пытаются обеспечить больший параллелизм инструкций - то есть выполнять как можно большее число инструкций параллельно. Так, например, с Pentium появилось суперскалярное выполнение, когда при некоторых условиях можно было выполнять две инструкции за такт. Pentium Pro получил внеочередное выполнение инструкций, позволившее оптимизировать работу вычислительных блоков. Проблема заключается в том, что у параллельного выполнения последовательного потока инструкций есть очевидные ограничения, поэтому слепое повышение числа вычислительных блоков не даёт выигрыша, поскольку большую часть времени они всё равно будут простаивать.

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



Нажмите на картинку для увеличения.

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



Нажмите на картинку для увеличения.

Да здравствует GeForce FX!

Два мира долгое время оставались разделёнными. Мы использовали CPU (или даже несколько CPU) для офисных задач и интернет-приложений, а GPU хорошо подходили лишь для ускорения визуализации. Но одна особенность изменила всё: а именно, появление программируемых GPU. Поначалу центральным процессорам было нечего бояться. Первые так называемые программируемые GPU (NV20 и R200) вряд ли представляли угрозу. Число инструкций в программе оставалось ограниченным около 10, они работали над весьма экзотическими типами данных, такими как 9- или 12-битными числами с фиксированной запятой.



Нажмите на картинку для увеличения.

Но закон Мура вновь показал себя с лучшей стороны. Увеличение числа транзисторов не только позволило повысить количество вычислительных блоков, но и улучшило их гибкость. Появление NV30 можно считать существенным шагом вперёд по нескольким причинам. Конечно, геймерам карты NV30 не очень понравились, однако новые графические процессоры стали опираться на две особенности, которые были призваны изменить восприятие GPU уже не только как графических акселераторов.

  • Поддержка вычислений с плавающей запятой одинарной точности (пусть даже это и не соответствовало стандарту IEEE754);
  • поддержка числа инструкций больше тысячи.

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

Идея использования графических акселераторов для математических расчётов не нова. Первые попытки были сделаны ещё в 90-х годах прошлого века. Конечно, они были очень примитивными - ограничиваясь, по большей части, использованием некоторых аппаратно заложенных функций, например, растеризации и Z-буферов для ускорения таких задач, как поиск маршрута или вывод диаграмм Вороного .



Нажмите на картинку для увеличения.

В 2003 году, с появлением эволюционировавших шейдеров, была достигнута новая планка - на этот раз выполнение матричных вычислений. Это был год, когда целая секция SIGGRAPH ("Computations on GPUs/Вычисления на GPU") была выделена под новую область ИТ. Эта ранняя инициатива получила название GPGPU (General-Purpose computation on GPU, универсальные вычисления на GPU). И ранним поворотным моментом стало появление .

Чтобы понять роль BrookGPU, нужно разобраться, как всё происходило до его появления. Единственным способом получить ресурсы GPU в 2003 году было использование одного из двух графических API - Direct3D или OpenGL. Следовательно, разработчикам, которые хотели получить возможности GPU для своих вычислений, приходилось опираться на два упомянутых API. Проблема в том, что они не всегда являлись экспертами в программировании видеокарт, а это серьёзно осложняло доступ к технологиям. Если 3D-программисты оперируют шейдерами, текстурами и фрагментами, то специалисты в области параллельного программирования опираются на потоки, ядра, разбросы и т.д. Поэтому сначала нужно было привести аналогии между двумя мирами.

  • Поток (stream) представляет собой поток элементов одного типа, в GPU он может быть представлен текстурой. В принципе, в классическом программировании есть такой аналог, как массив.
  • Ядро (kernel) - функция, которая будет применяться независимо к каждому элементу потока; является эквивалентом пиксельного шейдера. В классическом программировании можно привести аналогию цикла - он применяется к большому числу элементов.
  • Чтобы считывать результаты применения ядра к потоку, должна быть создана текстура. На CPU эквивалента нет, поскольку там есть полный доступ к памяти.
  • Управление местоположением в памяти, куда будет производиться запись (в операциях разброса/scatter), осуществляется через вершинный шейдер, поскольку пиксельный шейдер не может изменять координаты обрабатываемого пикселя.

Как можно видеть, даже с учётом приведённых аналогий, задача не выглядит простой. И на помощь пришёл Brook. Под этим названием подразумеваются расширения к языку C ("C with streams", "C с потоками"), как назвали их разработчики в Стэнфорде. По своей сути, задача Brook сводилась к сокрытию от программиста всех составляющих 3D API, что позволяло представить GPU как сопроцессор для параллельных вычислений. Для этого компилятор Brook обрабатывал файл.br с кодом C++ и расширениями, после чего генерировал код C++, который привязывался к библиотеке с поддержкой разных выходов (DirectX, OpenGL ARB, OpenGL NV3x, x86).



Нажмите на картинку для увеличения.

У Brook есть несколько заслуг, первая из которых заключается в выводе GPGPU из тени, чтобы с этой технологией могли знакомиться и широкие массы. Хотя после объявления о проекте ряд ИТ-сайтов слишком оптимистично сообщил о том, что выход Brook ставит под сомнение существование CPU, которые вскоре будут заменены более мощными GPU. Но, как видим, и через пять лет этого не произошло. Честно говоря, мы не думаем, что это вообще когда-либо случится. С другой стороны, глядя на успешную эволюцию CPU, которые всё более ориентируются в сторону параллелизма (больше ядер, технология многопоточности SMT, расширение блоков SIMD), а также и на GPU, которые, напротив, становятся всё более универсальными (поддержка расчётов с плавающей запятой одинарной точности, целочисленные вычисления, поддержка расчётов с двойной точностью), похоже, что GPU и CPU вскоре попросту сольются. Что же тогда произойдёт? Будут ли GPU поглощены CPU, как в своё время произошло с математическими сопроцессорами? Вполне возможно. Intel и AMD сегодня работают над подобными проектами. Но ещё очень многое может измениться.

Но вернёмся к нашей теме. Преимущество Brook заключалось в популяризации концепции GPGPU, он существенно упростил доступ к ресурсам GPU, что позволило всё большим пользователям осваивать новую модель программирования. С другой стороны, несмотря на все качества Brook, предстоял ещё долгий путь, прежде чем ресурсы GPU можно будет использовать для вычислений.

Одна из проблем связана с разными уровнями абстракции, а также, в частности, с чрезмерной дополнительной нагрузкой, создаваемой 3D API, которая может быть весьма ощутима. Но более серьёзной можно считать проблему совместимости, с которой разработчики Brook ничего не могли сделать. Между производителями GPU существует жёсткая конкуренция, поэтому они нередко оптимизируют свои драйверы. Если подобные оптимизации, по большей части, хороши для геймеров, они могут в один момент покончить с совместимостью Brook. Поэтому сложно представить использование этого API в промышленном коде, который будет где-то работать. И долгое время Brook оставался уделом исследователей-любителей и программистов.

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

Исследователи, изначально вовлечённые в проект Brook, быстро присоединились к командам разработчиков в Санта-Кларе, чтобы представить глобальную стратегию для развития нового рынка. Идея заключалась в создании комбинации аппаратного и программного обеспечения, подходящего для задач GPGPU. Поскольку разработчики nVidia знают все секреты своих GPU, то на графическое API можно было и не опираться, а связываться с графическим процессором через драйвер. Хотя, конечно, при этом возникают свои проблемы. Итак, команда разработчиков CUDA (Compute Unified Device Architecture) создала набор программных уровней для работы с GPU.



Нажмите на картинку для увеличения.

Как можно видеть на диаграмме, CUDA обеспечивает два API.

  • Высокоуровневый API: CUDA Runtime API;
  • низкоуровневый API: CUDA Driver API.

Поскольку высокоуровневый API реализован над низкоуровневым, каждый вызов функции уровня Runtime разбивается на более простые инструкции, которые обрабатывает Driver API. Обратите внимание, что два API взаимно исключают друг друга: программист может использовать один или другой API, но смешивать вызовы функций двух API не получится. Вообще, термин "высокоуровневый API" относителен. Даже Runtime API таков, что многие сочтут его низкоуровневым; впрочем, он всё же предоставляет функции, весьма удобные для инициализации или управления контекстом. Но не ожидайте особо высокого уровня абстракции - вам всё равно нужно обладать хорошим набором знаний о nVidia GPU и о том, как они работают.

С Driver API работать ещё сложнее; для запуска обработки на GPU вам потребуется больше усилий. С другой стороны, низкоуровневый API более гибок, предоставляя программисту дополнительный контроль, если нужно. Два API способны работать с ресурсами OpenGL или Direct3D (только девятая версия на сегодня). Польза от такой возможности очевидна - CUDA может использоваться для создания ресурсов (геометрия, процедурные текстуры и т.д.), которые можно передать на графическое API или, наоборот, можно сделать так, что 3D API будет отсылать результаты рендеринга программе CUDA, которая, в свою очередь, будет выполнять пост-обработку. Есть много примеров таких взаимодействий, и преимущество заключается в том, что ресурсы продолжают храниться в памяти GPU, их не требуется передавать через шину PCI Express, которая по-прежнему остаётся "узким местом".

Впрочем, следует отметить, что совместное использование ресурсов в видеопамяти не всегда проходит идеально и может привести к некоторым "головным болям". Например, при смене разрешения или глубины цвета, графические данные приоритетны. Поэтому если требуется увеличить ресурсы в кадровом буфере, то драйвер без проблем сделает это за счёт ресурсов приложений CUDA, которые попросту "вылетят" с ошибкой. Конечно, не очень элегантно, но такая ситуация не должна случаться очень уж часто. И раз уж мы начали говорить о недостатках: если вы хотите использовать несколько GPU для приложений CUDA, то вам нужно сначала отключить режим SLI, иначе приложения CUDA смогут "видеть" только один GPU.

Наконец, третий программный уровень отдан библиотекам - двум, если быть точным.

  • CUBLAS, где есть необходимые блоки для вычислений линейной алгебры на GPU;
  • CUFFT, которая поддерживает расчёт преобразований Фурье - алгоритм, широко используемый в области обработки сигналов.

Перед тем, как мы погрузимся в CUDA, позвольте определить ряд терминов, разбросанных по документации nVidia. Компания выбрала весьма специфическую терминологию, к которой трудно привыкнуть. Прежде всего, отметим, что поток (thread) в CUDA имеет далеко не такое же значение, как поток CPU, а также и не является эквивалентом потока в наших статьях о GPU. Поток GPU в данном случае является базовый набор данных, которые требуется обработать. В отличие от потоков CPU, потоки CUDA очень "лёгкие", то есть переключение контекста между двумя потоками - отнюдь не ресурсоёмкая операция.

Второй термин, часто встречающийся в документации CUDA - варп (warp) . Здесь путаницы нет, поскольку в русском языке аналога не существует (разве что вы не являетесь фанатом Start Trek или игры Warhammer). На самом деле термин взят из текстильной промышленности, где через основную пряжу (warp yarn), которая растянута на станке, протягивается уточная пряжа (weft yarn). Варп в CUDA представляет собой группу из 32 потоков и является минимальным объёмом данных, обрабатываемых SIMD-способом в мультипроцессорах CUDA.

Но подобная "зернистость" не всегда удобна для программиста. Поэтому в CUDA, вместо работы с варпами напрямую, можно работать с блоками/block , содержащими от 64 до 512 потоков.

Наконец, эти блоки собираются вместе в сетки/grid . Преимущество подобной группировки заключается в том, что число блоков, одновременно обрабатываемых GPU, тесно связано с аппаратными ресурсами, как мы увидим ниже. Группировка блоков в сетки позволяет полностью абстрагироваться от этого ограничения и применить ядро/kernel к большему числу потоков за один вызов, не думая о фиксированных ресурсах. За всё это отвечают библиотеки CUDA. Кроме того, подобная модель хорошо масштабируется. Если GPU имеет мало ресурсов, то он будет выполнять блоки последовательно. Если число вычислительных процессоров велико, то блоки могут выполняться параллельно. То есть, один и тот же код может работать на GPU как начального уровня, так и на топовых и даже будущих моделях.

Есть ещё пара терминов в CUDA API, которые обозначают CPU (хост/host ) и GPU (устройство/device ). Если это небольшое введение вас не испугало, то настало время поближе познакомиться с CUDA.

Если вы регулярно читаете Tom"s Hardware Guide, то архитектура последних GPU от nVidia вам знакома. Если нет, мы рекомендуем ознакомиться со статьёй "nVidia GeForce GTX 260 и 280: новое поколение видеокарт ". Что касается CUDA, то nVidia представляет архитектуру несколько по-другому, демонстрируя некоторые детали, раньше остававшиеся скрытыми.

Как можно видеть по иллюстрации выше, ядро шейдеров nVidia состоит из нескольких кластеров текстурных процессоров (Texture Processor Cluster, TPC) . Видеокарта 8800 GTX, например, использовала восемь кластеров, 8800 GTS - шесть и т.д. Каждый кластер, по сути, состоит из текстурного блока и двух потоковых мультипроцессоров (streaming multiprocessor) . Последние включают начало конвейера (front end), выполняющее чтение и декодирование инструкций, а также отсылку их на выполнение, и конец конвейера (back end), состоящий из восьми вычислительных устройств и двух суперфункциональных устройств SFU (Super Function Unit) , где инструкции выполняются по принципу SIMD, то есть одна инструкция применяется ко всем потокам в варпе. nVidia называет такой способ выполнения SIMT (single instruction multiple threads, одна инструкция, много потоков). Важно отметить, что конец конвейера работает на частоте в два раза превосходящей его начало. На практике это означает, что данная часть выглядит в два раза "шире", чем она есть на самом деле (то есть как 16-канальный блок SIMD вместо восьмиканального). Потоковые мультипроцессоры работают следующим образом: каждый такт начало конвейера выбирает варп, готовый к выполнению, и запускает выполнение инструкции. Чтобы инструкция применилась ко всем 32 потокам в варпе, концу конвейера потребуется четыре такта, но поскольку он работает на удвоенной частоте по сравнению с началом, потребуется только два такта (с точки зрения начала конвейера). Поэтому, чтобы начало конвейера не простаивало такт, а аппаратное обеспечение было максимально загружено, в идеальном случае можно чередовать инструкции каждый такт - классическая инструкция в один такт и инструкция для SFU - в другой.

Каждый мультипроцессор обладает определённым набором ресурсов, в которых стоит разобраться. Есть небольшая область памяти под названием "Общая память/Shared Memory" , по 16 кбайт на мультипроцессор. Это отнюдь не кэш-память: программист может использовать её по своему усмотрению. То есть, перед нами что-то близкое к Local Store у SPU на процессорах Cell. Данная деталь весьма любопытная, поскольку она подчёркивает, что CUDA - это комбинация программных и аппаратных технологий. Данная область памяти не используется для пиксельных шейдеров, что nVidia остроумно подчёркивает "нам не нравится, когда пиксели разговаривают друг с другом".

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

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

Мультипроцессор имеет 8 192 регистра, которые общие для всех потоков всех блоков, активных на мультипроцессоре. Число активных блоков на мультипроцессор не может превышать восьми, а число активных варпов ограничено 24 (768 потоков). Поэтому 8800 GTX может обрабатывать до 12 288 потоков в один момент времени. Все эти ограничения стоило упомянуть, поскольку они позволяют оптимизировать алгоритм в зависимости от доступных ресурсов.

Оптимизация программы CUDA, таким образом, состоит в получении оптимального баланса между количеством блоков и их размером. Больше потоков на блок будут полезны для снижения задержек работы с памятью, но и число регистров, доступных на поток, уменьшается. Более того, блок из 512 потоков будет неэффективен, поскольку на мультипроцессоре может быть активным только один блок, что приведёт к потере 256 потоков. Поэтому nVidia рекомендует использовать блоки по 128 или 256 потоков, что даёт оптимальный компромисс между снижением задержек и числом регистров для большинства ядер/kernel.

С программной точки зрения CUDA состоит из набора расширений к языку C, что напоминает BrookGPU, а также нескольких специфических вызовов API. Среди расширений присутствуют спецификаторы типа, относящиеся к функциям и переменным. Важно запомнить ключевое слово __global__ , которое, будучи приведённым перед функцией, показывает, что последняя относится к ядру/kernel - эту функцию будет вызывать CPU, а выполняться она будет на GPU. Префикс __device__ указывает, что функция будет выполняться на GPU (который, кстати, CUDA и называет "устройство/device") но она может быть вызвана только с GPU (иными словами, с другой функции __device__ или с функции __global__). Наконец, префикс __host__ опционален, он обозначает функцию, которая вызывается CPU и выполняется CPU - другими словами, обычную функцию.

Есть ряд ограничений, связанных с функциями __device__ и __global__: они не могут быть рекурсивными (то есть вызывать самих себя), и не могут иметь переменное число аргументов. Наконец, поскольку функции __device__ располагаются в пространстве памяти GPU, вполне логично, что получить их адрес не удастся. Переменные тоже имеют ряд квалификаторов, которые указывают на область памяти, где они будут храниться. Переменная с префиксом __shared__ означает, что она будет храниться в общей памяти потокового мультипроцессора. Вызов функции __global__ немного отличается. Дело в том, при вызове нужно задать конфигурацию выполнения - более конкретно, размер сетки/grid, к которой будет применено ядро/kernel, а также размер каждого блока. Возьмём, например, ядро со следующей подписью.

__global__ void Func(float* parameter);

Оно будет вызываться в виде

Func<<< Dg, Db >>> (parameter);

где Dg является размером сетки, а Db - размером блока. Две этих переменных относятся к новому типу вектора, появившегося с CUDA.

API CUDA содержит функции для работы с памятью в VRAM: cudaMalloc для выделения памяти, cudaFree для освобождения и cudaMemcpy для копирования памяти между RAM и VRAM и наоборот.

Мы закончим данный обзор весьма интересным способом, которым компилируется программа CUDA: компиляция выполняется в несколько этапов. Сначала извлекается код, относящийся к CPU, который передаётся стандартному компилятору. Код, предназначенный для GPU, сначала преобразовывается в промежуточный язык PTX. Он подобен ассемблеру и позволяет изучать код в поисках потенциальных неэффективных участков. Наконец, последняя фаза заключается в трансляции промежуточного языка в специфические команды GPU и создании двоичного файла.

Просмотрев документацию nVidia, так и хочется попробовать CUDA на неделе. Действительно, что может быть лучше оценки API путём создания собственной программы? Именно тогда большинство проблем должны выплыть на поверхность, пусть даже на бумаге всё выглядит идеально. Кроме того, практика лучше всего покажет, насколько хорошо вы поняли все принципы, изложенные в документации CUDA.

В подобный проект погрузиться довольно легко. Сегодня для скачивания доступно большое количество бесплатных, но качественных инструментов. Для нашего теста мы использовали Visual C++ Express 2005, где есть всё необходимое. Самое сложное заключалось в том, чтобы найти программу, портирование которой на GPU не заняло бы несколько недель, и вместе с тем она была бы достаточно интересная, чтобы наши усилия не пропали даром. В конце концов, мы выбрали отрезок кода, который берёт карту высот и рассчитывает соответствующую карту нормалей. Мы не будем детально углубляться в эту функцию, поскольку в данной статье это вряд ли интересно. Если быть кратким, то программа занимается искривлением участков: для каждого пикселя начального изображения мы накладываем матрицу, определяющую цвет результирующего пикселя в генерируемом изображении по прилегающим пикселям, используя более или менее сложную формулу. Преимущество этой функции в том, что её очень легко распараллелить, поэтому данный тест прекрасно показывает возможности CUDA.


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

Ещё раз повторим, что целью теста являлось знакомство с утилитами CUDA SDK, а не сравнительное тестирование версий под CPU и GPU. Поскольку это была первая наша попытка создания программы CUDA, мы не особо надеялись получить высокую производительность. Так как данная часть кода не является критической, то версия под CPU была не оптимизирована, поэтому прямое сравнение результатов вряд ли интересно.

Производительность

Однако мы замерили время выполнения, чтобы посмотреть, есть ли преимущество в использовании CUDA даже с самой грубой реализацией, или нам потребуется длительная и утомительная практика, чтобы получить какой-то выигрыш при использовании GPU. Тестовая машина была взята из нашей лаборатории разработки - ноутбук с процессором Core 2 Duo T5450 и видеокартой GeForce 8600M GT, работающей под Vista. Это далеко не суперкомпьютер, но результаты весьма интересны, поскольку тест не "заточен" под GPU. Всегда приятно видеть, когда nVidia демонстрирует огромный прирост на системах с монстрообразными GPU и немалой пропускной способностью, но на практике многие из 70 миллионов GPU с поддержкой CUDA на современном рынке ПК далеко не такие мощные, поэтому и наш тест имеет право на жизнь.

Для изображения 2 048 x 2 048 пикселей мы получили следующие результаты.

  • CPU 1 поток: 1 419 мс;
  • CPU 2 потока: 749 мс;
  • CPU 4 потока: 593 мс
  • GPU (8600M GT) блоки по 256 потоков: 109 мс;
  • GPU (8600M GT) блоки по 128 потоков: 94 мс;
  • GPU (8800 GTX) блоки по 128 потоков/ 256 потоков: 31 мс.

По результатам можно сделать несколько выводов. Начнём с того, что, несмотря на разговоры об очевидной лени программистов, мы модифицировали начальную версию CPU под несколько потоков. Как мы уже упоминали, код идеален для этой ситуации - всё, что требуется, это разбить начальное изображение на столько зон, сколько существует потоков. Обратите внимание, что от перехода от одного потока на два на нашем двуядерном CPU ускорение получилось почти линейное, что тоже указывает на параллельную природу тестовой программы. Весьма неожиданно, но версия с четырьмя потоками тоже оказалась быстрее, хотя на нашем процессоре это весьма странно - можно было, напротив, ожидать падения эффективности из-за накладных расходов на управление дополнительными потоками. Как можно объяснить такой результат? Сложно сказать, но, возможно, виновен планировщик потоков под Windows; в любом случае, результат повторяем. С текстурами меньшего размера (512x512) прирост от разделения на потоки был не такой выраженный (примерно 35% против 100%), и поведение версии с четырьмя потоками было логичнее, без прироста по сравнению с версией на два потока. GPU работал всё ещё быстрее, но уже не так выражено (8600M GT была в три раза быстрее, чем версия с двумя потоками).



Нажмите на картинку для увеличения.

Второе значимое наблюдение - даже самая медленная реализация GPU оказалась почти в шесть раз быстрее, чем самая производительная версия CPU. Для первой программы и неоптимизированной версии алгоритма результат очень даже ободряющий. Обратите внимание, что мы получили ощутимо лучший результат на небольших блоках, хотя интуиция может подсказывать об обратном. Объяснение простое - наша программа использует 14 регистров на поток, и с 256-поточными блоками требуется 3 584 регистра на блок, а для полной нагрузки процессора требуется 768 потоков, как мы показывали. В нашем случае это составляет три блока или 10 572 регистра. Но мультипроцессор имеет всего 8 192 регистра, поэтому он может поддерживать активными только два блока. Напротив, с блоками по 128 потоков нам требуется 1 792 регистра на блок; если 8 192 поделить на 1 792 и округлить до ближайшего целого, то мы получим четыре блока. На практике число потоков будет таким же (512 на мультипроцессор, хотя для полной нагрузки теоретически нужно 768), но увеличение числа блоков даёт GPU преимущество гибкости по доступу к памяти - когда идёт операция с большими задержками, то можно запустить выполнение инструкций другого блока, ожидая поступления результатов. Четыре блока явно снижают задержки, особенно с учётом того, что наша программа использует несколько доступов в память.

Анализ

Наконец, несмотря на то, что мы сказали выше, мы не смогли устоять перед искушением и запустили программу на 8800 GTX, которая оказалась в три раза быстрее 8600, независимо от размера блоков. Можно подумать, что на практике на соответствующих архитектурах результат будет в четыре или более раз выше: 128 АЛУ/шейдерных процессоров против 32 и более высокая тактовая частота (1,35 ГГц против 950 МГц), но так не получилось. Скорее всего, ограничивающим фактором оказался доступ к памяти. Если быть более точным, доступ к начальному изображению осуществляется как к многомерному массиву CUDA - весьма сложный термин для того, что является не более, чем текстурой. Но ест несколько преимуществ.

  • доступы выигрывают от кэша текстур;
  • мы используем wrapping mode, в котором не нужно обрабатывать границы изображения, в отличие от версии CPU.

Кроме того, мы можем получить преимущество от "бесплатной" фильтрации с нормализованной адресацией между вместо и , но в нашем случае это вряд ли полезно. Как вы знаете, 8600 оснащён 16 текстурными блоками по сравнению с 32 у 8800 GTX. Поэтому между двумя архитектурами соотношение всего два к одному. Добавьте к этому разницу в частотах, и мы получим соотношение (32 x 0,575) / (16 x 0,475) = 2,4 - близко к "трём к одному", что мы получили на самом деле. Данная теория также объясняет, почему размер блоков многое на G80 не меняет, поскольку АЛУ всё равно упирается в текстурные блоки.



Нажмите на картинку для увеличения.

Кроме многообещающих результатов, наше первое знакомство с CUDA прошло очень хорошо, учитывая не самые благоприятные выбранные условия. Разработка на ноутбуке под Vista подразумевает, что придётся использовать CUDA SDK 2.0, всё ещё находящееся в состоянии бета-версии, с драйвером 174.55, который тоже бета-версия. Несмотря на это мы не можем сообщить о каких-либо неприятных сюрпризах - только начальные ошибки во время первой отладки, когда наша программа, всё ещё весьма "глючная" попыталась адресовать память за пределами выделенного пространства.

Монитор начал дико мерцать, затем экран почернел... пока Vista не запустила службу восстановления драйвера, и всё стало в порядке. Но всё же несколько удивительно это наблюдать, если вы привыкли видеть типичную ошибку Segmentation Fault на стандартных программах, подобно нашей. Наконец, небольшая критика в сторону nVidia: во всей документации, доступной для CUDA, нет небольшого руководства, которое бы шаг за шагом рассказывало о том, как настроить окружение разработки под Visual Studio. Собственно, проблема невелика, поскольку в SDK есть полный набор примеров, которые можно изучить для понимания каркаса для приложений CUDA, но руководство для новичков не помешало бы.



Нажмите на картинку для увеличения.

nVidia представила CUDA с выпуском GeForce 8800. И в то время обещания казались весьма соблазнительными, но мы придержали свой энтузиазм до реальной проверки. Действительно, в то время это казалось больше разметкой территории, чтобы оставаться на волне GPGPU. Без доступного SDK сложно сказать, что перед нами не очередная маркетинговая пустышка, из которой ничего не получится. Уже не в первый раз хорошая инициатива была объявлена слишком рано и в то время не вышла на свет из-за недостатка поддержки - особенно в столь конкурентном секторе. Теперь, через полтора года после объявления, мы с уверенностью можем сказать, что nVidia сдержала слово.

SDK довольно быстро появился в бета-версии в начале 2007 года, с тех пор он быстро обновлялся, что доказывает значимость этого проекта для nVidia. Сегодня CUDA весьма приятно развивается: SDK доступен уже в бета-версии 2.0 для основных операционных систем (Windows XP и Vista, Linux, а также 1.1 для Mac OS X), а для разработчиков nVidia выделила целый раздел сайта.

На более профессиональном уровне впечатление от первых шагов с CUDA оказалось очень даже позитивным. Если даже вы знакомы с архитектурой GPU, вы легко разберётесь. Когда API выглядит понятным с первого взгляда, то сразу же начинаешь полагать, что получишь убедительные результаты. Но не будет ли теряться вычислительное время от многочисленных передач с CPU на GPU? И как использовать эти тысячи потоков практически без примитива синхронизации? Мы начинали наши эксперименты со всеми этими опасениями в уме. Но они быстро рассеялись, когда первая версия нашего алгоритма, пусть и весьма тривиального, оказалась существенно быстрее, чем на CPU.

Так что CUDA - это не "палочка-выручалочка" для исследователей, которые хотят убедить руководство университета купить им GeForce. CUDA - уже полностью доступная технология, которую может использовать любой программист со знанием C, если он готов потратить время и усилия на привыкание к новой парадигме программирования. Эти усилия не будут потеряны даром, если ваши алгоритмы хорошо распараллеливаются. Также мы хотели бы поблагодарить nVidia за предоставление полной и качественной документации, где найдут ответы начинающие программисты CUDA.

Что же требуется CUDA, чтобы стать узнаваемым API? Если говорить одним словом: переносимость. Мы знаем, что будущее ИТ кроется в параллельных вычислениях - сегодня уже каждый готовится к подобным изменениям, и все инициативы, как программные, так и аппаратные, направлены в этом направлении. Однако на данный момент, если смотреть на развитие парадигм, мы находится ещё на начальном этапе: мы создаём потоки вручную и стараемся спланировать доступ к общим ресурсам; со всем этим ещё как-то можно справиться, если количество ядер можно пересчитать по пальцам одной руки. Но через несколько лет, когда число процессоров будет исчисляться сотнями, такой возможности уже не будет. С выпуском CUDA nVidia сделала первый шаг в решении этой проблемы - но, конечно, данное решение подходит только для GPU от этой компании, да и то не для всех. Только GF8 и 9 (и их производные Quadro/Tesla) сегодня могут работать с программами CUDA. И новая линейка 260/280, конечно.



Нажмите на картинку для увеличения.

nVidia может хвастаться тем, что продала 70 миллионов CUDA-совместимых GPU по всему миру, но этого всё равно мало, чтобы стать стандартом де-факто. С учётом того, что конкуренты не сидят, сложа руки. AMD предлагает собственный SDK (Stream Computing), да и Intel объявила о решении (Ct), хотя оно ещё не доступно. Грядёт война стандартов, и на рынке явно не будет места для трёх конкурентов, пока другой игрок, например, Microsoft, не выйдет с предложением общего API, что, конечно, облегчит жизнь разработчикам.

Поэтому у nVidia есть немало трудностей на пути утверждения CUDA. Хотя технологически перед нами, без сомнения, успешное решение, ещё остаётся убедить разработчиков в его перспективах - и это будет сделать нелегко. Впрочем, судя по многим недавним объявлениям и новостям по поводу API, будущее выглядит отнюдь не печальным.

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

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

Технология GPGPU (General-Purpose Graphics Processing Units — графический процессор общего назначения) долгое время существовала только в теоретических выкладках мозговитых академиков. А как иначе? Предложить кардинально изменить сложившийся за десятилетия вычислительный процесс, доверив расчет его параллельных веток видеокарте, — на это только теоретики и способны.

Логотип технологии CUDA напоминает о том, что выросла она в недрах
3D-графики.

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

Благодаря CUDA технологии GPGPU стали мейнстримом. И ныне только самый недальновидный и покрытый толстым слоем лени разработчик систем программирования не заявляет о поддержке своим продуктом CUDA. IT-издания почли за честь изложить подробности технологии в многочисленных пухлых научно-популярных статьях, а конкуренты срочно уселись за лекала и кросскомпиляторы, чтобы разработать нечто подобное.

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

Вот только пишут так, словно продолжают обсуждать GPGPU в толстых научных журналах. Забрасывают читателя грудой терминов типа «grid», «SIMD», «warp», «хост», «текстурная и константная память». Погружают его по самую маковку в схемы организации графических процессоров nVidia, ведут извилистыми тропами параллельных алгоритмов и (самый сильный ход) показывают длинные листинги кода на языке Си. В результате получается, что на входе статьи мы имеем свежего и горящего желанием понять CUDA читателя, а на выходе — того же читателя, но с распухшей головой, заполненной кашей из фактов, схем, кода, алгоритмов и терминов.

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

Далеко не везде

CUDA поддерживается высокопроизводительными суперкомпьютерами
nVidia Tesla.

И все же прежде, чем взглянуть на результаты трудов CUDA на поприще облегчения жизни рядового пользователя, стоит уяснить все ее ограничения. Точно как с джинном: любое желание, но одно. У CUDA тоже есть свои ахиллесовы пятки. Одна из них — ограничения платформ, на которых она может трудиться.

Перечень видеокарт производства nVidia, поддерживающих CUDA, представлен в специальном списке, именуемом CUDA Enabled Products. Список весьма внушительный, но легко классифицируемый. В поддержке CUDA не отказывают:

    Модели nVidia GeForce 8-й, 9-й, 100-й, 200-й и 400-й серий с минимумом 256 мегабайт видеопамяти на борту. Поддержка распространяется как на карты для настольных систем, так и на мобильные решения.

    Подавляющее большинство настольных и мобильных видеокарт nVidia Quadro.

    Все решения нетбучного ряда nvidia ION.

    Высокопроизводительные HPC (High Performance Computing) и суперкомпьютерные решения nVidia Tesla, используемые как для персональных вычислений, так и для организации масштабируемых кластерных систем.

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

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

...но сам процесс

Как работает CUDA? Как объяснить сложный процесс параллельных вычислений на особой аппаратной архитектуре GPU так, чтобы не погрузить читателя в пучину специфических терминов?

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

Архитектурно центральный процессор (CPU) и его графический собрат (GPU) устроены по-разному. Если проводить аналогию с миром автопрома, то CPU — универсал, из тех, которые называют «сарай». Выглядит легковым авто, но при этом (с точки зрения разработчиков) «и швец, и жнец, и на дуде игрец». Выполняет роль маленького грузовика, автобуса и гипертрофированного хечбэка одновременно. Универсал, короче. Цилиндров-ядер у него немного, но они «тянут» практически любые задачи, а внушительная кэш-память способна разместить кучу данных.

А вот GPU — это спорткар. Функция одна: доставить пилота на финиш как можно быстрее. Поэтому никакой большой памяти-багажника, никаких лишних посадочных мест. Зато цилиндров-ядер в сотни раз больше, чем у CPU.

Благодаря CUDA разработчикам программ GPGPU не требуется вникать в сложности программи-
рования под такие графические движки, как DirectX и OpenGL

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

Так какой же может быть тандем из универсала и спорткара? Работа CUDA происходит примерно так: программа выполняется на CPU до тех пор, пока в ней появляется участок кода, который можно выполнить параллельно. Тогда, вместо того, чтобы он медленно выполнялся на двух (да пусть даже и восьми) ядрах самого крутого CPU, его передают на сотни ядер GPU. При этом время выполнения этого участка сокращается в разы, а значит, сокращается и время выполнения всей программы.

Технологически для программиста ничего не меняется. Код CUDA-программ пишется на языке Си. Точнее, на особом его диалекте «С with streams» (Си с потоками). Разработанное в Стэнфорде, это расширение языка Си получило название Brook. В качестве интерфейса, передающего Brook-код на GPU, выступает драйвер видеокарты, поддерживающей CUDA. Он организует весь процесс обработки этого участка программы так, что для программиста GPU выглядит как сопроцессор CPU. Очень похоже на использование математического сопроцессора на заре персональных компьютеров. С появлением Brook, видеокарт с поддержкой CUDA и драйверов для них любой программист стал способен в своих программах обращаться к GPU. А ведь раньше этим шаманством владел узкий круг избранных, годами оттачивающих технику программирования под графические движки DirectX или OpenGL.

В бочку этого пафосного меда — дифирамбов CUDA — стоит положить ложку дегтя, то бишь ограничений. Далеко не любая задача, которую нужно запрограммировать, подходит для решения с помощью CUDA. Добиться ускорения решения рутинных офисных задач не получится, а вот доверить CUDA обсчет поведения тысячи однотипных бойцов в World of Warcraft — пожалуйста. Но это задача, высосанная из пальца. Рассмотрим же примеры того, что CUDA уже очень эффективно решает.

Труды праведные

CUDA — весьма прагматичная технология. Реализовав ее поддержку в своих видеокартах, компания nVidia весьма справедливо рассчитывала на то, что знамя CUDA будет подхвачено множеством энтузиастов как в университетской среде, так и в коммерции. Так и случилось. Проекты на базе CUDA живут и приносят пользу.

NVIDIA PhysX

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

Одна из возможностей физического движка NVIDIA PhysX — реалистичная работа с тканями.

Реализовать алгоритмы компьютерной симуляции базовых физических законов — дело очень трудоемкое. Наиболее известными компаниями на этом поприще являются ирландская компания Havok с ее межплатформенным физическим Havok Physics и калифорнийская Ageia — прародитель первого в мире физического процессора (PPU — Physics Processing Unit) и соответствующего физического движка PhysX. Первая из них, хотя и приобретена компанией Intel, активно трудится сейчас на поприще оптимизации движка Havok для видеокарт ATI и процессоров AMD. А вот Ageia с ее движком PhysX стала частью nVidia. При этом nVidia решила достаточно сложную задачу адаптации PhysX под технологию CUDA.

Возможным это стало благодаря статистике. Статистически было доказано, что, какой бы сложный рендеринг ни выполнял GPU, часть его ядер все равно простаивает. Именно на этих ядрах и работает движок PhysX.

Благодаря CUDA львиная доля вычислений, связанных с физикой игрового мира, стала выполняться на видеокарте. Освободившаяся мощь центрального процессора была брошена на решение других задач геймплея. Результат не заставил себя ждать. По оценкам экспертов, прирост производительности игрового процесса с PhysX, работающем, на CUDA возрос минимум на порядок. Выросло и правдоподобие реализации физических законов. CUDA берет на себя рутинный расчет реализации трения, тяготения и прочих привычных нам вещей для многомерных объектов. Теперь не только герои и их техника идеально вписываются в законы привычного нам физического мира, но и пыль, туман, взрывная волна, пламя и вода.

CUDA-версия пакета сжатия текстур NVIDIA Texture Tools 2

Нравятся реалистичные объекты в современных играх? Стоит сказать спасибо разработчикам текстур. Но чем больше реальности в текстуре, тем больше ее объем. Тем больше она занимает драгоценной памяти. Чтобы этого избежать, текстуры предварительно сжимают и динамически распаковывают по мере надобности. А сжатие и распаковка — это сплошные вычисления. Для работы с текстурами nVidia выпустила пакет NVIDIA Texture Tools. Он поддерживает эффективное сжатие и распаковку текстур стандарта DirectX (так называемый ВЧЕ-формат). Вторая версия этого пакета может похвастаться поддержкой алгоритмов сжатия BC4 и BC5, реализованных в технологии DirectX 11. Но главное то, что в NVIDIA Texture Tools 2 реализована поддержка CUDA. По оценке nVidia, это дает 12-кратный прирост производительности в задачах сжатия и распаковки текстур. А это значит, что фреймы игрового процесса будут грузиться быстрее и радовать игрока своей реалистичностью.

Пакет NVIDIA Texture Tools 2 заточен под работу с CUDA. Прирост производительности при сжатии и распаковке текстур налицо.

Использование CUDA позволяет существенно повысить эффективность видеослежки.

Обработка видеопотока в реальном времени

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

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

Благодаря CUDA появилась возможность реализации алгоритмов одновременного слежения за множеством объектов в видеопотоке. При этом процесс происходит в реальном масштабе времени, а видео является полноценным 30 fps. По сравнению с реализацией такого алгоритма на современных многоядерных CPU CUDA дает двух-, трехкратный прирост производительности, а это, согласитесь, немало.

Конвертирование видео, фильтрация аудио

Видеоконвертер Badaboom — первая ласточка, использующая CUDA для ускорения конвертирования.

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

А вот CUDA с ним справляется на ура. Первая ласточка — конвертер Badaboom от компании Elevental. Разработчики Badaboom, выбрав CUDA, не просчитались. Тесты показывают, что стандартный полуторачасовый фильм на нем конвертируется в формат iPhone/iPod Touch менее чем за двадцать минут. И это при том, что при использовании только CPU этот процесс занимает больше часа.

Помогает CUDA и профессиональным меломанам. Любой из них полцарства отдаст за эффективный FIR-кроссовер — набор фильтров, разделяющих звуковой спектр на несколько полос. Процесс этот весьма трудоемкий и при большом объеме аудиоматериала заставляет звукорежиссера сходить на несколько часов «покурить». Реализация FIR-кроссовера на базе CUDA ускоряет его работу в сотни раз.

CUDA Future

Сделав технологию GPGPU реальностью, CUDA не собирается почивать на лаврах. Как это происходит повсеместно, в CUDA работает принцип рефлексии: теперь не только архитектура видеопроцессоров nVidia влияет на развитие версий CUDA SDK, а и сама технология CUDA заставляет nVidia пересматривать архитектуру своих чипов. Пример такой рефлексии — платформа nVidia ION. Ее вторая версия специально оптимизирована для решения CUDA-задач. А это означает, что даже в относительно недорогих аппаратных решениях потребители получат всю мощь и блестящие возможности CUDA.