49
Практика программирования на технологии CUDA. Оглавление Оглавление ........................................................................................................... 1 Введение ............................................................................................. 2 Краткий обзор существующих многоядерных процессоров .......... 2 Архитектура процессора Intel Nehalem ............................................ 3 Архитектура процессора AMD Istanbul ............................................ 4 1.1.Архитектура процессора IBM Cell .............................................. 5 2.Краткий обзор основных схем построения кластеров ................. 6 Схема сети «звезда» ........................................................................... 7 Схема сети «кольцо» .......................................................................... 8 Схема сети «3D тор» .......................................................................... 9 Основные термины курса ................................................................ 10 Архитектура графических адаптеров Nvidia .................................. 11 Архитектура чипа G80 ..................................................................... 12 Архитектура чипа G200 ................................................................... 13 Архитектура чипа Fermi ................................................................. 14 Программная часть технологии CUDA. ....................................................... 17 Спецификаторы для функций ...................................................................... 20 Спецификаторы для переменных ................................................................ 22 Новые типы в CUDA ..................................................................................... 22 Встроенные переменные ............................................................................... 23 Директива запуска ядра ................................................................................. 24 Работа с памятью в CUDA ............................................................................ 25 Использование глобальной памяти. ............................................................. 28 Вычисление числа Пи .................................................................................... 29 Вычисление центра масс четверти круга (Задача) ...................................... 31

CUDA программирование

Embed Size (px)

DESCRIPTION

CUDA (Compute Unified Device Architecture) — программно-аппаратная архитектура, позволяющая производить вычисления с использованием графических процессоров NVIDIA, поддерживающих технологию GPGPU (произвольных вычислений на видеокартах)

Citation preview

Page 1: CUDA программирование

Практика программирования на технологии CUDA.

ОглавлениеОглавление ........................................................................................................... 1

Введение ............................................................................................. 2

Краткий обзор существующих многоядерных процессоров .......... 2

Архитектура процессора Intel Nehalem ............................................ 3

Архитектура процессора AMD Istanbul ............................................ 4

1.1.Архитектура процессора IBM Cell .............................................. 5

2.Краткий обзор основных схем построения кластеров ................. 6

Схема сети «звезда» ........................................................................... 7

Схема сети «кольцо» .......................................................................... 8

Схема сети «3D тор» .......................................................................... 9

Основные термины курса ................................................................ 10

Архитектура графических адаптеров Nvidia .................................. 11

Архитектура чипа G80 ..................................................................... 12

Архитектура чипа G200 ................................................................... 13

Архитектура чипа Fermi ................................................................. 14

Программная часть технологии CUDA. ....................................................... 17

Спецификаторы для функций ...................................................................... 20

Спецификаторы для переменных ................................................................ 22

Новые типы в CUDA ..................................................................................... 22

Встроенные переменные ............................................................................... 23

Директива запуска ядра ................................................................................. 24

Работа с памятью в CUDA ............................................................................ 25

Использование глобальной памяти. ............................................................. 28

Вычисление числа Пи .................................................................................... 29

Вычисление центра масс четверти круга (Задача) ...................................... 31

Page 2: CUDA программирование

Использование объединения при работе с глобальной памятью ............... 31

Использование разделяемой памяти ............................................................ 33

Задача перемножения матриц. Глобальная память. .................................. 35

Задача перемножения матриц. Разделяемая память. ................................. 36

Использование константной памяти. .......................................................... 40

Использование текстурной памяти. ............................................................ 40

Особенности линейной текстурной памяти. ............................................... 42

Особенности cudaArray текстурной памяти. .............................................. 43

Использование текстурной памяти (Задача) ................................................ 46

Постановка задачи на проект ........................................................................ 47

Заключение ..................................................................................................... 49

ВведениеУченым часто приходится сталкиваться с трудными вычислительными

задачами, будь то расчет полета космической ракеты, формы нового самолета, или поведения нового лекарства. Так или иначе, такого рода задачи требуют большое количество вычислительных мощностей. До недавнего времени, чаще всего, такие задачи решались исключительно на вычислительные кластерах с большим количеством узлов. Однако в 2006 году компания Nvidia анонсировала технологию CUDA, предназначенную для написания программ под графические адаптеры (ГА) производства Nvidia. Такой шаг позволил эффективно использовать графические адаптеры для научных расчетов, таким образом, позволив значительно повысить производительность персональных компьютеров.

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

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

ГА, требуется понять, чем же принципиально они отличаются от

Page 3: CUDA программирование

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

Введем общепринятую классификацию вычислительных систем по Флинну (Таблица 1).

Таблица 1. Классификация вычислительных систем по Флинну.

Single Instruction Multiple InstructionSingle Data SISD MISDMultiple Data SIMD MIMD

Для этого рассмотрим несколько самых распространенных многоядерных вычислительных систем:

• Процессор Intel

• Процессор Amd

• Процессор Cell

Архитектура процессора Intel Nehalem

Рисунок 1. Схематичное изображение строения процессора Intel Nehalem

Page 4: CUDA программирование

Как видно из Рисунка 1, процессор Intel Nehalem содержит 4 независимых процессорных ядра, каждое из которых обладает полной функциональностью центрального процессора. Такое ядро способно обрабатывать системные прерывания, работать с устройствами ввода-вывода, то есть абсолютно полноценно поддерживать операционную систему. Каждое ядро содержит кэши первого уровня для данных и инструкций, содержит логику выборки инструкций и кэш данных второго уровня. Все ядра абсолютно симметрично присоединены к КЭШу третьего уровня и к QPI (QuickPath Interconnect) – системе присоединения процессоров к чипсету. Так же они присоединены к IMC (Integrated Memory Controller) – система связи с памятью, пришедшая взамен северного моста. В некоторых версиях современных процессоров Intel так же присутствует встроенный графический контроллер.

Архитектура процессора AMD Istanbul

Рисунок 2. Схематичное изображение строения процессора AMD Istanbul

Из Рисунка 2 видно, что в архитектуре AMD Istanbul предусмотрено 6 независимых ядер, каждое из которых, на данном уровне абстракции неотличимо от процессорного ядра Intel. Кроме того видно, что в AMD Istanbul, так же как и в Intel Nehalem есть кэш 3го уровня, общий для всех ядер, есть встроенный северный мост и система связи между ядрами внутри процессора.

Page 5: CUDA программирование

Вообще, два представителя центральных процессоров семейства Intel и AMD с вычислительной точки зрения очень похожи. Оба относятся MIMD классу, если рассматривать их в целом, а каждое ядро относится к SISD классу. Кроме того, они обладают одними и теми же преимуществами и недостатками (Таблица 2).

Таблица 2. Преимущества и недостатки архитектур стандартных центральных процессоров Intel и AMD

Преимущества Недостатки• Самостоятельность ядер, а,

следовательно, возможность выполнять независимые вычислительные задачи

• Высокая частота вычислительного ядра

• Большая точность (80 или 128 бит) внутренних вычислительных операций

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

• Большая «стоимость» создания отдельного потока

• Необходимость выполнения когерентности КЭШей.

1.1. Архитектура процессора IBM Cell

Рисунок 3. Схематичное изображение строения процессора IBM Cell

Page 6: CUDA программирование

Архитектура процессора Cell уже сильно отличается от стандартной (см. Рисунок 3). Имеется восемь SPE (Synergistic Processing Elements), каждый из которых является мощным вычислителем чисел с плавающей точкой. Процессор Cell уже относится к SIMD архитектуре, то есть он является векторной машиной, уже неспособной одновременно выполнять несколько принципиально различных задач. Процессор Cell адаптирован под увеличение пропускной способности памяти.

Таблица 3. Преимущества и недостатки архитектур стандартных центральных процессоров IBM Cell

Преимущества Недостатки• Высокая скорость расчета

чисел с плавающей точкой• Эффективная работа с

памятью

• Ядра не самостоятельны. Процессор является специализированным

2. Краткий обзор основных схем построения кластеров

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

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

Суперкомпьютер – очень большой кластер.

Есть много способов объединять вычислительные узлы в кластере, рассмотрим самые распространенные из них:

• Схему звезда

• Схему кольцо

• Схему 3D тор

Page 7: CUDA программирование

Схема сети «звезда»

Рисунок 4. Схема построения сети "Звезда"

Суть схемы построения сети «звезда» заключается в том, что явно существует головная машина, которая часто имеет выход во внешний мир и выполняет роль DHCP сервера для остальных машин кластера. Все машины кластера присоединены к одному роутеру, образуя, таким образом, звезду, с центром в роутере (Рисунок 4). Особенности архитектуры схемы звезда можно посмотреть в Таблице 4.

Таблица 4. Преимущества и недостатки схемы построения сети «звезда»

Преимущества Недостатки• Все узлы равнозначны

(одинаковое время доступа между любыми двумя из них)

• На узле требуется только одно сетевое устройство

• Большая нагрузка на головную машину или центральный роутер

Page 8: CUDA программирование

Схема сети «кольцо»

Рисунок 5. Схема построения сети "Кольцо"

Схема «кольцо» изначально не предполагает существование явно выделенного головного узла. Конечно, какой-то из узлов должен выполнять функции точки входа в кластер и осуществлять связь с внешним миром. Схема предполагает подключение каждого узла к двум соседним, так что в итоге архитектура сети замыкается в кольцо (Рисунок 5).

Таблица 5. Преимущества и недостатки схемы построения сети «кольцо»

Преимущества Недостатки• Требует малое число кабелей • Дает преимущества на

линейных задачах малой связанности

• Между некоторыми узлами получается очень большое время доступа

• Требует 2 сетевых адаптера на каждом узле

Page 9: CUDA программирование

Схема сети «3D тор»

Рисунок 6. Схема построения сети "3D тор"

Схема «3D тор» является трехмерным обобщением схемы «кольцо». Схема предполагает подключение каждого узла к 4м соседним, так что в итоге архитектура сети замыкается в тор (Рисунок 6).

Таблица 6. Преимущества и недостатки схемы построения сети «3D тор»

Преимущества Недостатки• На 3х мерных задачах с малой

связанностью дает огромные преимущества

• Между некоторыми узлами получается очень большое время доступа

• Требует 4 сетевых адаптера на каждом узле

Page 10: CUDA программирование

На самом деле, кластера редко имеют одну сеть. Чаще всего делается 2 сети по схеме звезда, одна из которых обладает не слишком большой пропускной способностью (1Гб/с Ethernet) и выполняет функции управления. Вторая сеть, имеющая высокую пропускную способность (10Гб/с Myrinet, или 40Гб/с Infiniband), осуществляет передачу данных.

Суперкомпьютеры чаще устроены ещё сложнее и в своей архитектуре имеют до 5 различных сетей.

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

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

• Хост (Host) – центральный процессор, который управляет выполнением программы.

• Устройство (Device) – видеокарта, являющаяся сопроцессором к центральному процессору (хосту).

• Ядро (Kernel) – Параллельная часть алгоритма, выполняется на гриде.

• Грид (Grid) – объединение блоков, которые выполняются на одном устройстве.

• Блок (Block) – объединение потоков, которое выполняется целиком на одном SM. Имеет свой уникальный идентификатор внутри грида.

• Тред (Thread) – единица выполнения программы. Имеет свой уникальный идентификатор внутри блока.

Page 11: CUDA программирование

• Варп (Warp) – 32 последовательно идущих треда, выполняется физически одновременно.

Архитектура графических адаптеров Nvidia

Одним из наиболее распространенных видом гибридных машин является гибридная машина на основе графических адаптеров компании Nvidia. Для того чтобы эффективно уметь программировать под такого рода машины необходимо детально понимать устройство графического адаптера с аппаратной точки зрения. Любая графическая карта может быть схематически изображена следующим образом (Рисунок 7).

Рисунок 7. Схематичное изображение графического адаптера

Все вычислительные ядра на графическом адаптере объединены в независимые блоки TPC (Texture process cluster) количество которых зависит как от версии чипа (G80 – максимум 8, G200 – максимум 10), так и просто от конкретной видеокарты внутри чипа (GeForce 220GT – 2, GeForce 275 – 10). Так же, как от видеокарты к видеокарте может меняться количество TPC, так же может меняться и количество DRAM партиций и соответственно общий объем оперативной памяти. DRAM партиции имеют кэш второго уровня и объединены между собой коммуникационной сетью, в которую, так же подключены все TPC. Любая видеокарта подключается к CPU через мост, который может быть, как

Page 12: CUDA программирование

интегрирован в CPU (Intel Core i7), так и дискретным (Intel Core 2 Duo).

От чипа к чипу (от G80 до G200) менялись детали в TPC, а общая архитектура оставалась одинаковой. В новом чипе Fermi произошли изменения и в общей архитектуре, поэтому о нем речь пойдет отдельно.

Архитектура чипа G80

Рисунок 8. Архитектура чипа G80

Основные составляющие TPC:

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

• SM – потоковый мультипроцессор, самостоятельный вычислительный модуль, именно на нем осуществляется выполнение блока. В архитектуре чипа G80 в одном TPC находится 2 SM.

Основные составляющие SM:

• SP – потоковый процессор, непосредственно вычислительный модуль, способен совершать арифметические операции с целочисленными

Page 13: CUDA программирование

операндами и с операндами с плавающей точкой (одинарная точность). Не является самостоятельной единицей, управляется SM.

• SFU – модуль сложных математических функций. Проводит вычисления сложных математических функций (exp, sqr, log). Использует вычислительные мощности SP.

• Регистровый файл – единый банк регистров, на каждом SM имеется 32Кб. Самый быстрый тип памяти на графическом адаптере.

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

• Кэш констант – место кэширования особого типа памяти (константной). Об особенностях применения речь пойдет позже.

• Кэш инструкций, блок выборки инструкций – управляющая система SM. Не играет роли при программировании.

Итого, чип G80 имеет максимально 128 вычислительных модулей (SP) способных выполнять вычисления с целыми числами и числами с плавающей точкой с одинарной точностью. Такого функционала было недостаточно для многих научных задач, требовалась двойная точность.

Архитектура чипа G200

Page 14: CUDA программирование

Рисунок 9. Архитектура чипа G200

В чипе G200 происходят следующие изменения по сравнению с чипом G80:

• Число SM в TPC увеличено с двух до 3х.

• Максимальное количество SP увеличено до 240.

• Появился блок работы с числами с двойной точностью, который в качестве вычислительных мощностей использует одновременно все 8 SP. Таким образом, скорость расчета double в 8 раз меньше чем скорость расчета float. Что по-прежнему является не очень приемлемым для многих научных задач.

Архитектура чипа Fermi

Page 15: CUDA программирование

Рисунок 10. Архитектура чипа Fermi

Ключевыми архитектурными особенностями Fermi являются:

• Третье поколение потокового мультипроцессора (SM):- 32 ядра CUDA на SM, вчетверо больше, чем у GT200;- восьмикратный прирост производительности в FP-операциях двойной точности в сравнении с предшественником;- два блока Warp Scheduler на SM вместо одного;- 64 КБ ОЗУ с конфигурируемым разделением на разделяемую память и L1-кэш.

• Второе поколение набора инструкций параллельного выполнения потоков (Parallel Thread Execution, PTX 2.0):- унифицированное адресное пространство с полной поддержкой С++;- оптимизация для OpenCL и DirectCompute;- полная 32- и 64-битная точность в соответствии с IEEE 754-2008;- инструкции доступа к памяти для поддержки перехода на 64-

Page 16: CUDA программирование

битную адресацию;- улучшенная производительность предсказаний.

• Улучшенная подсистема памяти:- иерархия NVIDIA Parallel DataCache с конфигурируемым L1-кэшем и общим L2;- поддержка ECC, впервые на GPU;- существенно увеличенная производительность операций чтения и записи в память.

• Движок NVIDIA GigaThread:- десятикратное ускорение процедуры контекстного переключения;- параллельное выполнение ядер;- непоследовательное исполнение потоков.

Подведем итоги по характеристикам перечисленных чипов (Таблица 7)

Таблица 7. Сравнение характеристик различных чипов Nvidia

Архитектура G80 GT200 Fermi

Год вывода на рынок 2006 2008 2009

Число транзисторов, млн. 681 1400 3000

Количество CUDA-ядер 128 240 512

Объем разделяемой памяти на SM, Кб

16 1648 или 16 (конфигурируется)

Объем кэш-памяти первого уровня в расчете на SM, Кб

0 016 или 48 (конфигурируется)

Объем кэш-памяти второго уровня, Кб

0 0 768

Функция ECC Отсутствует Отсутствует Имеется

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

Page 17: CUDA программирование

Возможности графических адаптеров обозначаются при помощи Compute Capability, старшая цифра которой обозначает версию архитектуры, а младшая небольшим изменениям внутри архитектуры. На данный момент существуют 1.0, 1.1, 1.2, 1.3, 2.0 Compute Capability. В частности, Compute Capability влияет на правила работы с глобальной памятью.

Таблица 8. Примеры Compute Capability

GPU Compute CapabilityGeForce 8800GTX 1.0GeForce 9800GTX 1.1GeForce 210 1.2GeForce 275GTX 1.3Tesla C2050 2.0

На этом разговор об аппаратной части технологии CUDA можно считать законченным.

Программная часть технологии CUDA.Повторим основные термины:

• Тред (Thread) – единица выполнения программы. Имеет свой уникальный идентификатор внутри блока.

• Варп (Warp) – 32 последовательно идущих треда, выполняется физически одновременно.

• Блок (Block) – объединение потоков, которое выполняется целиком на одном SM. Имеет свой уникальный идентификатор внутри грида.

• Грид (Grid) – объединение блоков, которые выполняются на одном устройстве.

• Ядро (Kernel) – Параллельная часть алгоритма, выполняется на гриде.

Page 18: CUDA программирование

• Устройство (Device) – видеокарта, являющаяся сопроцессором к центральному процессору (хосту).

• Хост (Host) – центральный процессор, который управляет выполнением программы.

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

Вычислительную конфигурацию процессов можно представить так:

Рисунок 11. Вычислительная конфигурация грида

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

Существуют ограничения на размеры грида и блока, они приведены в таблице 11

Page 19: CUDA программирование

Таблица 9. Ограничения на размер грида и блока

Грид Блок

X 65536 512

Y 65536 512

Z 1 64

Всего 4294967296 512

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

• Грид выполняется на всей графической карте одновременно. Нет возможности указать, например, только 2 TPC из 10.

• Одновременно на графической карте может выполняться несколько ядер, а значит и гридов.

• Блок выполняется целиком на одном SM. Независимо от его размера.

• На одном SM одновременно может выполнятся до 8ми блоков. Количество блоков определяется ограничениями по регистрам и разделяемой памяти

Для реализации программы под ГА компания Nvidia сделала свои расширения для языка С и выпустила компилятор NVCC для сборки таких программ, ввела в обиход новое расширение *.cu, для файлов, которые содержат CUDA вызовы. К расширениям языка С относятся:

• спецификаторы для функций и переменных

• новые встроенные типы

• встроенные переменные (внутри ядра)

• директива для запуска ядра из C кода

Page 20: CUDA программирование

Спецификаторы для функций В CUDA добавлено несколько спецификаторов функций, которые

позволяют определить, откуда запускается и где выполняется данная функция (таблица 10).

Таблица 10. Спецификаторы функций

Спецификатор Выполняется на Может вызываться из

__device__ device device

__global__ device host

__host__ host host

• Спецификатор __global__ применяется для функций, которые задают ядро (в них передаются несколько специфических переменных). Функции __global__ могут возвращать только void. Спецификатор __global__ применяется исключительно обособленно.

• Спецификаторы __host__ и __device__ могут применяться одновременно для задания функций, выполняющихся и на хосте и на устройстве (компилятор сам создает обе версии кода).

• Обособленный спецификатор __host__ можно опускать.

Для старых версий видеокарт (до чипа Fermi) существуют дополнительные ограничения на выполняемые на видеокарте функции:

• Нельзя брать адрес от функции (за исключением __global__)

• Нет стека, а, следовательно, нет рекурсии

• Нет static переменных внутри функций

• Не поддерживается переменное число аргументов в функциях.

Page 21: CUDA программирование

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

Page 22: CUDA программирование

Спецификаторы для переменных Кроме спецификаторов функций, в CUDA добавлено несколько спецификаторов переменных, которые в основном задают тот или иной особый тип памяти (таблица 11).

Таблица 11. Спецификаторы переменных

Спецификатор Находится Доступна Вид доступа

__device__ device device R

__constant__ device device / host R / W

__shared__ device block RW / __syncthreads()

• Спецификатор __device__ является аналог const на центральном процессоре

• Спецификатор __shared__ применяется для задания разделяемой памяти и не может быть инициализирован при объявлении и, как правило, требует явной синхронизации.

• Запись в __constant__ может выполнять только через специальные функции с CPU. Модификатор используется для объявления переменных, хранящихся в константной памяти, речь о которой пойдет позже.

• Все спецификаторы нельзя применять к полям структур или union.

Новые типы в CUDAВ CUDA добавлены множество векторных типов, для удобства

копирования и доступа к данным.

Page 23: CUDA программирование

• (u/) char, char2, char3, char4• (u/) int, int2, int3, int4• float, float2, float3, float4• longlong, longlong2• double, double2

Для создания переменных таких типов требуется применять функции вида make_(тип)(размерность), например:

char2 a = make_char2(‘a’,’b’);

printf(“%c %c”, a.x, a.y);

float4 b =make_float4(1.0, 2.0, 3.0, 4.0);

printf(“%f %f”, b.x, b.y, b.z, b.w);

Для всех типов в cuda > 3.0 определены покомпонентные операции.

Так же существует специальный тип dim3, основанный на типе uint3, имеющий нормальный конструктор и умеющий инициализировать недостающие координаты единицами. Данный тип используется для задания параметров запуска ядра.

dim3 block = dim3 (16,16, 2);

dim3 grid = dim3 (1000);

Обращаем ваше внимание на то, что именно единицами! Если вы напишете

dim3 block = dim3 (16,16, 0);

программа будет выполняться на блоке с количеством тредов 0! То есть расчет производиться не будет.

Встроенные переменныеВ CUDA существует несколько особых переменных, которые

существуют внутри каждого вычислительного ядра, И позволяют отличать один тред от другого.

dim3 gridDim – содержит в себе информацию о конфигурации грида при запуске ядра.

Page 24: CUDA программирование

uint3 blockIdx – координаты текущего блока внутри грида.

dim3 blockDim – размерность блока при запуске ядра.

uint3 threadIdx – координаты текущего треда внутри блока.

int warpSize – размер варпа (на данный момент всегда равен 32).

Директива запуска ядраДля запуска ядра используются специальные директивы для задания

параметров ядра и передачи ему необходимых параметров.

Объявление функции для ядра с параметром params:

__global__ void Kernel_name(params);

Запуск ядра:

Kernel_name<<<grid, block, mem, stream>>> ( params ), где

dim3 grid – конфигурация грида для запуска. Размер грида указывается в количестве блоков по каждой координате.

dim3 block – конфигурация блока при запуске. Размер блока задается в количестве тредов по каждой координате.

size_t mem -- количество разделяемой памяти на блок, которая выделяется для данного запуска под динамическое выделение внутри ядра.

cudaStream_t stream – описание потока, в котором запускается данное ядро.

Пример общей схемы программы:

#define BS 256 // Размер блока #define N 1024 //Всего элементов для расчета//Объявляем ядро__global__ void kernel (int* data){

//Проводим вычисление абсолютной координаты в линейных блоке и гриде. int idx = blockIdx.x * BS + threadIdx.x;…some code…

}int main (){

Page 25: CUDA программирование

//Объявляем указатель на массив на ГА int* data;//Задаем конфигурацию ядра(Всего N тредов, конфигурация

линейная)dim3 block = dim3(BS);dim3 grid = dim3(N / BS);…some code…//Запускаем ядро с заданной конфигурацией и передаем ему параметрыkernel <<<grid, block>>> (data);

…some code…} Однако, в этом коде нет одной очень важной части – это работы с памятью. Мы только обозначили передачу параметра в ядро, но при этом реально нигде его не использовали. Поэтому переходим к следующей теме – использование памяти в CUDA.

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

В CUDA есть два уровня API: низкоуровневый драйвер-API и высокоуровневый runtime-API. Runtime-API реализован через драйвер-API, он обладает меньшей гибкостью, но более удобен для написания программ. Оба API не требуют явной инициализации и для использования дополнительных типов и прочих расширений языка С не требуется подключать дополнительные заголовочные файлы. Все функции драйвер-API начинаются с приставки cu, все функции runtime-API начинаются с приставки cuda. Практически все функции из обоих API возвращают значение t_cudaError, которое принимает значение cudaSuccess в случае успеха. В данном методическом пособии мы будем говорить исключительно про Runtime-API.

Функции в API делятся на синхронные и асинхронные. Синхронные запуски являются блокирующими, асинхронные наоборот. Процессы копирования, запуска ядра, инициализация памяти могут быть асинхронными. При использовании асинхронных вызовов необходимо всегда помнить о синхронизации перед использованием их результатов.

Page 26: CUDA программирование

Приведем примеры нескольких полезных функций из Runtime-API:

char* cudaGetErrorString(cudaError_t) – функция расшифровки кода ошибки в её текст. cudaError_t cudaGetLastError() – определение последней произошедшей ошибки, часто используется после вызова ядра для проверки правильности его работы.

cudaError_t cudaThreadSynchronize() – команда синхронизации, необходимая после любого асинхронного вызова.

cudaError_t cudaGetDeviceCount(int *) – функция определяет количество устройств, доступных для вычисления под CUDA.

cudaError_t cudaGetDevicePropertis (cudaDeviceProp * props, int deviceNo ) – определяет параметры конкретного устройства по его номеру. В частности, позволяет определить Compute Capability из полей major и minor структуры cudaDeviceProp.

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

Общие правила работы с памятью в CUDA:

• Обращение к памяти происходит 32/64/128-битовые слова

• При обращении к t[i]

o sizeof(t [0]) равен 4/8/16 байтам

o t [i] выровнен по sizeof ( t [0] )

• Вся выделяемая память всегда выровнена по 256 байт

• При использовании типов данных не кратных 4/8/16 желательно использовать__align__(size)

Рисунок 12. Расположение данных в памяти

Page 27: CUDA программирование

Переходим к разговору о типах памяти в CUDA.

Таблица 12. Типы памяти в CUDA

Тип памяти Доступ Выделяется на Скорость работы

Регистры R/W Per-thread Высокая(on-chip)

Локальная R/W Per-thread Низкая (DRAM)

Shared R/W Per-block Высокая(on-chip)

Глобальная R/W Per-grid Низкая (DRAM)

Constant R/O Per-grid Высокая(L1 cache)

Texture R/O Per-grid Высокая(L1 cache)

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

Локальная – используется для хранения локальных переменных когда регистров не хватает, скорость доступа низкая, так как расположена в DRAM партициях. Выделяется отдельно для каждого треда.

Разделяемая – 16Кб (или 48Кб на Fermi) на SM, используется для хранения массивов данных, используемых совместно всеми тредами в блоке. Расположена на чипе, имеет чуть меньшую скорость доступа чем регистры (около 10 тактов). Выделяется на блок.

Глобальная – основная память видеокарты (на данный момент максимально 6Гб на Tesla c2070). Используется для хранения больших массивов данных. Расположена на DRAM партициях и имеет медленную скорость доступа (около 80 тактов). Выделяется целиком на грид.

Константная – память, располагающаяся в DRAM партиции, кэшируется специальным константным кэшем. Используется для передачи параметров в ядро, превышаюших допустимые размеры для параметров ядра. Выделяется целиком на грид.

Page 28: CUDA программирование

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

Обращение к памяти в CUDA осуществляется одновременно из половины варпа.

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

Использование глобальной памяти.Как уже говорилось выше, глобальная это основная память в CUDA,

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

Основные функции работы с глобальной памятью, все они вызываются на хосте:

cudaError_t cudaMalloc ( void ** devPtr, size_t size );

Выделение size памяти на устройстве и записывание адреса в devPtr

cudaError_t cudaMallocPitch ( void ** devPtr, size_t * pitch, size_t width, size_t height );

Выделение памяти под двухмерный массив размером width * height, возвращает указатель devPtr на память и смещение для каждой строки pitch

cudaError_t cudaFree ( void * devPtr );

Освобождение памяти по адресу devPtr на устройстве

Page 29: CUDA программирование

cudaError_t cudaMemset (void* devPtr, int value, size_t count );

Заполнение памяти по адресу devPtr значениями value на размер count

cudaError_t cudaMemcpy ( void * dst, const void * src, size_t count, enum cudaMemcpyKind kind );

Копирование данных между устройством и хостом

dst – указатель на память приемник

src – указатель на память источник

count – размер копируемой памяти в байтах

kind – направление копирования может принимать значения:

• cudaMemcpyHostToDevice – c хоста на устройство

• cudaMemcpyDeviceToHost – с устройства на хост

• cudaMemcpyDeviceToDevice – с устройства на устройство

• cudaMemcpyHostToHost – с хоста на хост

cudaError_t cudaMemcpyAsync ( void * dst, const void * src, size_t count, enum cudaMemcpyKind kind, cudaStream_t stream );

Аналогично cudaMemcpy только асинхронно в потоке stream.

Для иллюстрации основ работы с глобальной памяти приведем пример задачи нахождения числа Пи:

Вычисление числа ПиТребуется вычислить число пи, методом интегрирование четверти окружности радиуса 1 (Рисунок 13).

#include <stdio.h>#include <time.h>

#define CUDA_FLOAT float#define GRID_SIZE 256#define BLOCK_SIZE 256

Рисунок 13. Вычисление числа Пи

Page 30: CUDA программирование

// Проверка на ошибку выполнения функций из cuda APIvoid check_cuda_error(const char *message){

cudaError_t err = cudaGetLastError();if(err!=cudaSuccess)

printf("ERROR: %s: %s\n", message, cudaGetErrorString(err));}

__global__ void pi_kern(CUDA_FLOAT *res){

int n = threadIdx.x + blockIdx.x * BLOCK_SIZE;CUDA_FLOAT x0 = n * 1.f / (BLOCK_SIZE * GRID_SIZE); // Начало

отрезка интегрированияCUDA_FLOAT y0 = sqrtf(1 - x0 * x0);CUDA_FLOAT dx = 1.f / (1.f * BLOCK_SIZE * GRID_SIZE); // Шаг

интегрированияCUDA_FLOAT s = 0; // Значение интеграла по отрезку, данному

текущему тредуCUDA_FLOAT x1, y1;x1 = x0 + dx;y1 = sqrtf(1 - x1 * x1);s = (y0 + y1) * dx / 2.f; // Площадь трапецииres[n] = s; // Запись результата в глобальную память

}

int main(int argc, char** argv){

cudaSetDevice(DEVICE);// Выбор устройстваcheck_cuda_error("Error selecting device");CUDA_FLOAT *res_d; // Результаты на устройствеCUDA_FLOAT res[GRID_SIZE * BLOCK_SIZE]; // Результаты в

хостовой памятиcudaMalloc((void**)&res_d, sizeof(CUDA_FLOAT) * GRID_SIZE *

BLOCK_SIZE); // Выделение памяти на CPUcheck_cuda_error("Allocating memory on GPU");// Рамеры грида и блока на GPUdim3 grid(GRID_SIZE);dim3 block(BLOCK_SIZE);

pi_kern<<<grid, block>>>(res_d); // Запуск ядраcudaThreadSynchronize(); // Ожидаем завершения работы ядраcheck_cuda_error("Executing kernel");cudaMemcpy(res, res_d, sizeof(CUDA_FLOAT) * GRID_SIZE * BLOCK_SIZE,

cudaMemcpyDeviceToHost); // Копируем результаты на хостcheck_cuda_error("Copying results from GPU");cudaFree(res_d); // Освобождаем память на GPUcheck_cuda_error("Freeing device memory");CUDA_FLOAT pi = 0;for (int i=0; i < GRID_SIZE * BLOCK_SIZE; i++){

pi += res[i];}pi *= 4;

printf("PI = %.12f\n",pi);return 0;

Page 31: CUDA программирование

}

В данном примере все операции происходят с типом CUDA_FLOAT, который может быть как float так и double, что бы читатель смог проверить работоспособность ГА на одинарной и на двойной точности. Однако данный пример является идеальным, используется один линейный массив, происходит только последовательная запись. Такие условия выполняются далеко не всегда, поэтому необходимо уточнить некоторые особенности эффективного использования глобальной памяти.

Вычисление центра масс четверти круга (Задача)

Аналогично вычислению числа Пи, читателю предлагается самому модифицировать программу, для нахождения координат центра масс. См рисунок 14.

Использование объединения при работе с глобальной памятью

ГА умеет объединять ряд запросов к глобальной памяти в один блок (транзакцию) при условии:

• Независимо происходит для каждого half-warp’а

• Длина блока должна быть 32/64/128 байт

• Блок должен быть выровнен по своему размеру

Кроме того, условия выполнения объединения зависят от Compute Capability (Таблица 13):

Таблица 13. Условия возникновения объединения

СС 1.0, 1.1 СС >= 1.2

Рисунок 14. Нахождение центра масс

Page 32: CUDA программирование

Нити обращаются к • 32-битовым словам, давая

64-байтовый блок • 64-битовым словам, давая

128-байтовый блок

Все 16 слов лежат в пределах блока

k-ая нить half-warp’а обращается к k-му слову блока

Нити обращаются к• 8-битовым словам, дающим

один 32-байтовый сегмент• 16-битовым словам, дающим

один 64-байтовый сегмент• 32-битовым словам, дающим

один 128-байтовый сегмент

Получающийся сегмент выровнен по своему размеру

Рисунок 15. Пример существования объединения

Page 33: CUDA программирование

Рисунок 16. Пример отсутствия объединения для СС 1.0,1.1

Для СС >= 1.2 в первом случае (Рисунок 16) будет существовать, так как блок выровнен по своему размеру.В случае если объединение не произошло в СС 1.0, 1.1 будет проведено 16 отдельных транзакций, а в СС >= 1.2 будет создано несколько блоков объедиения, которые покрывают запрашиваемую область. Использование объединения позволяет значительно повысить производительность программы. Однако существует способ ещё сильнее ускорить производительность ГА, в случае если обращение к данным сильно локализовано внутри блока – использование разделяемой памяти.

Использование разделяемой памятиОсновной особенностью разделяемой памяти, является то, что она

сильно оптимизирована под обращение к ней одновременно 16 тредов. Для повышения пропускной способности вся разделяемая память разбита на 16 банков, состоящих из 32 битовых слов, причем, последовательно идущие слова попадают в последовательно идущие блоки. Обращение к каждому банку происходит независимо. Если несколько тредов из полуварпа обращаются к одному и тому же банку, то возникает конфликт банков, то есть последовательное чтение, что замедляет скорость работы программы.

Page 34: CUDA программирование

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

Часто использование разделяемой памяти требует явной синхронизации внутри блока __syncthreads();

Приведем несколько простых примеров:

#define BS 256; //Размер блока__global__ void kern(float* data){//Создается массив float в //разделяемой памяти:

__shared__ float a[BS];int idx = blockIdx.x * BS + threadIdx.x;

//Копируем из глобальной памяти в разделяемую

a[idx] = data[idx];//Перед использованием надо быть уверенным что все данные скопированы. Синхронизируем.

__syncthreads();//Используем

data[idx] = a[idx]+ a[(idx + 1) % BS];

}

Конфликтов не возникает, так как float занимают 32 бита, и последовательные float располагаются в последовательных банках.

#define BS 256; //Размер блока__global__ void kern(short* data){//Создается массив float в //разделяемой памяти:

__shared__ short a[BS];int idx = blockIdx.x * BS + threadIdx.x;

//Копируем из глобальной памяти в разделяемую

a[idx] = data[idx];//Перед использованием надо быть уверенным что все данные скопированы. Синхронизируем.

__syncthreads();//Используем

data[idx] = a[idx]+ a[(idx + 1) % BS];

}

Рисунок 17. Расположение float в разделяемой памяти

Рисунок 18. Расположение short в разделяемой памяти

Page 35: CUDA программирование

Возникают конфликты 2го порядка, так как short занимают 16 бит, и два последовательных short располагаются в одном банке. Аналогично если объявлять массив char, возникнет конфликт 4го порядка, так как char это 8 бит.

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

Задача перемножения матриц. Глобальная память. В ядре нам будут доступны следующие переменные:WA, WBBS – размер блока по любому измерению (блок квадратный)BX = blockIdx.x BY = blockIdx.y TX = threadIdx.x TY = threadIdx.y Все матрицы хранятся в линейных массивах.Каждый тред будет считать отдельный элемент матрицы C. Для этого ему понадобится строка из A и столбец из B. Так как мы используем только глобальную память, строка и столбец будут полностью считываться каждым тредом!

#define BLOCK_SIZE 16__global__ void matMult ( float * a, float * b, float * с, int wa, int wb ) { int bx = blockIdx.x; int by = blockIdx.y; int tx = threadIdx.x; int ty = threadIdx.y; float sum = 0.0f;

Рисунок 19. Перемножение матриц с глобальной памятью.

Page 36: CUDA программирование

int ia = wa * BLOCK_SIZE * by + wa * ty; int ib = BLOCK_SIZE * bx + tx; int ic = wb * BLOCK_SIZE * by + BLOCK_SIZE * bx; for ( int k = 0; k < n; k++ ) sum += a [ia + k] * b [ib + k * wb]; c [ic + wb * ty + tx] = sum; }

Таким образом, происходят:

• 2 * WA обращения к глобальной памяти

• 2 * WA арифметические операции

Если посмотреть, на что больше всего ГА тратит времени при работе, то получается следующая картина (рисунок 20)

Рисунок 20. Профиль программы перемножения матриц с использованием глобальной памяти

ТО видно, что большую часть времени (около 85%) ушло на работу с памятью. Попробуем применить разделяемую память.

Задача перемножения матриц. Разделяемая память.

Page 37: CUDA программирование
Page 38: CUDA программирование
Page 39: CUDA программирование

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

С = A1 * B1 + A2 * B2 + ….

И каждую пару блоков будем копировать в разделяемую память.

Данную задачу предлагается решить читателю самостоятельно!

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

• WA / 8 обращения к глобальной памяти

• 2 * WA арифметические операции

И профиль программы имеет примерно такой вид (Рисунок 22):

Рисунок 22. Профиль программы перемножения матриц с использованием разделяемой памяти

Рисунок 21. Перемножение матриц с разделяемой памятью.

Page 40: CUDA программирование

Теперь вычисления занимают 81% времени, а обращения к памяти лишь 13% (Рисунок 20). Достигается ускорение более чем на порядок.

Использование константной памяти.Константная память используется тогда, когда в ядро необходимо

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

__constant__ float contsData [256]; -- объявление глобальной переменной с именем contsData для использования в качестве константной памяти.

cudaMemcpyToSymbol ( constData, hostData, sizeof ( data ), 0, cudaMemcpyHostToDevice ); --копирование данных с центрального процессора в константную память.

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

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

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

• Объем данных не влезает в shared память• Паттерн доступа хаотичный• Данные переиспользуются разными потоками

Page 41: CUDA программирование

Для использования текстурной памяти необходимо задать объявление текстуры как глобальную переменную:

texture< type , dim, tex_type> g_TexRef;

• Type – тип хранимых переменных• Dim – размерность текстуры (1, 2, 3)• Tex_type – тип возвращаемых значений

o cudaReadModeNormalizedFloat o cudaReadModeElementType

Кроме того, для более полного использования возможностей текстурной памяти можно задать описание канала:struct cudaChannelFormatDesc {

int x, y, z, w; enum cudaChannelFormatKind f;

}; Задает формат возвращаемого значения

• int x, y, z, w; - число [0,32] проекция исходного значения по битам

• cudaChannelFormatKind – тип возвращаемого значения

o cudaChannelFormatKindSigned – знаковые int o cudaChannelFormatKindUnsigned – беззнаковые

int o cudaChannelFormatKindFloat – float

В CUDA существуют два типа текстур линейная и cudaArray (Таблица 14).

Таблица 14. Типы текстурной памяти в CUDA

Линейная cudaArrayМожно использовать обычную глобальную память Ограничения: • Только для одномерных

массивов• Нет фильтрации• Доступ по целочисленным

координатам • Обращение по адресу вне

допустимого диапазона

Позволяет организовывать данные в 1D/ 2D/3D массивы данных вида:• 1/2/4 компонентные векторы • 8/16/32 bit signed/unsigned integers • 32 bit float • 16 bit float (driver API)Доступ по семейству функций tex1D()/tex2D()/tex3D()

Page 42: CUDA программирование

возвращает нольДоступ: tex1Dfetch(tex, int)

Особенности линейной текстурной памяти.Линейная текстурная память не обладает никакими особыми свойствами

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

Привязывание линейного массива:

cudaError_t cudaBindTexture(size_t shift, texref tex, &src, size_t size));

• Shift – смещение при привязки к массиву (к одному массиву можно привязать несколько тектсур)

• Tex – объявленная текстура

• Src – массив в глобальной памяти, к которому привязывается текстура

• Size – размер привязываемой области в байтах

Привязывание «двумерного массива» (в глобальной памяти он все равно хранится как линейный, и обращение к нему идел по одной координате):

cudaError_t cudaBindTexture2D(size_t shift, texref tex, &src, &channelDesc, int width,int height,int pitch);

• Shift – смещение при привязки к массиву (к одному массиву можно привязать несколько текстур)

• Tex – объявленная текстура

Page 43: CUDA программирование

• Src – массив в глобальной памяти, к которому привязывается текстура

• channelDesc – описание канала (см выше)

• width – ширина двумерного массива

• height – высота двумерного массива

• pitch – смещение каждой строки

После окончания работы с текстурой её надо «отвязать»:

cudaError_t cudaUnbindTexture(texref tex);

Все вышеприведенные функции вызываются с хоста.

На устройстве используется функция, которая достает значение из текстуры:

tex1Dfetch(texRef tex, int index);• Tex – объявленная текстура

• Index – индекс вынимаемого значения в линейном массиве

Особенности cudaArray текстурной памяти.В качестве основы для такого типа памяти выступает специальный контейнер cudaArray, который является черным ящиком для приложений. Использование cudaArray обосновано тогда, когда мы хотим создать 2х или трехмерную структуру, или нас важны преобразования, которые ГА может совершать аппаратно с исходными данными:

• Нормализация координат (перевод [W, H] => [0,1])

• Преобразование координат

o Clamp – координата обрезается по границе

o Wrap – координата заворачивается

• Фильтрация (при обращении по float координате)

Page 44: CUDA программирование

o Point – возвращается ближайшее заданное значение

o Linear – производится билинейная интерполяция

Для использования cudaArray текстурной памяти требуется объявить переменную указатель на cudaArray:

cudaArray * a;

Затем необходимо выделить память под данные на ГА:

cudaError_t cudaMallocArray(struct cudaArray ** arrayPtr, const struct cudaChannelFormatDesc *

channelDesc, size_t width, size_t height);

• arrayPtr – указатель на cudaArray

• channelDesc – описание канала (см выше)

• width – ширина массива

• height – высота массива

Затем скопировать в выделенную память данные:

cudaError_t cudaMemcpyToArray(struct cudaArray * dst, size_t wOffset, size_t hOffset, const void * src, size_t count, enum cudaMemcpyKind kind)

• arrayPtr – указатель на cudaArray

• wOffset – смещение по горизонтали при привязки к массиву

• hOffset – смещение по вертикали при привязки к массиву

• Src – массив в памяти хоста, к который копируется

• count – размер данных в байтах

Page 45: CUDA программирование

• kind – направление копирования(см cudaMemcpy)

После того как данные скопированы, можно осуществлять привязку cudaArray массива к текстуре:

cudaError_t cudaBindTextureToArray (const struct textureReference *tex, const struct cudaArray *array, const struct cudaChannelFormatDesc *desc);

• Tex – объявленная текстура

• array – массив в cudaArray, к которому привязывается текстура

• channelDesc – описание канала (см выше)

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

На устройстве используется функция, которая достает значение из текстуры:

tex1D (texRef tex, float x);• Tex – объявленная текстура

• x – индекс вынимаемого значения в линейном массиве

tex2D (texRef tex, float x, float y);• Tex – объявленная текстура

• x, y – индексы вынимаемого значения в двухмерном массиве

tex3D (texRef tex, float x, float y, float z);• Tex – объявленная текстура

• x, y, z – индексы вынимаемого значения в трехмерном массиве

Page 46: CUDA программирование

Итого общая схема работы с текстурной памятью выглядит так (Рисунок 23).

Рисунок 23. Общая схема работы с текстурной памятью

Использование текстурной памяти (Задача)Написать программу МД моделирования

леннард-джонсовской жидкости:

• Входной файл :

x1 y1 z1 x2 y2 z2 x3 y3 z3

…..

• Шаг по времени 1нс

• Количество шагов 1000

• Тип взаимодействия Леннард-Джонс

• Задать потенциал интерполяционной таблицей

Page 47: CUDA программирование

• Принять радиус действия потенциала -- 5

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

Постановка задачи на проектВ качестве итоговой задачи, для проверки своих знаний по CUDA,

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

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

В рамках данной школы будут рассматриваться 2 типа клеточных автоматов: Жизнь и Кохомото-Ооно. Оба основаны на 3х мерной кубической решётке с целочисленными значениями.В обоих вариантах для определения значения клетки на следующем шаге используется значение данной клетки на текущем шаге и значения клеток, удалённых от данной не более чем на 1 по каждой координате, на текущем шаге.

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

Жизнь:Возможные значения клеток: {0,1}Рождение клетки – переход клетки из состояния 0 в 1 – происходит при числе соседей 6 или 7Сохранение клетки – переход из 1 в 1 – происходит при числе соседей 4, 5, 6 или 7В других случаях клетка умирает – переходит в 0Формат входного файла:WX WY WZ Tw1 x1 y1 z1...wm xm ym zm

Page 48: CUDA программирование

0 0 0 0

WX, WY, WZ – размеры поля, натуральные числаT – тип симуляции:1 – “Жизнь”wi – тип i-й клетки (для “Жизни” – 1)xi, yi, zi – координаты i-й клетки,

Формат запуска программы:

./life test.in 100 5 test.out

Файл начальных данных

Количество шагов

Выводить если номершага % 5 == 0

Имя выходного файла

Формат выходного файла:w11 x11 y11 z11…w1m x1m y1m z1m0 0 0 0...wn1 xn1 yn1 zn1…wnk xnk ynk znk0 0 0 0

wji – тип i-й клетки на j-м шаге (всегда1) xji, yji, zji – координаты i-й клетки на j-м шагеПример входного файла с иллюстрацией:20 20 20 11 5 7 51 5 8 51 6 8 51 7 5 51 8 5 5 1 8 6 51 5 7 61 5 8 61 6 8 61 7 5 61 8 5 61 8 6 6

Page 49: CUDA программирование

0 0 0 0

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

Самыми лучшими задачами для видеокарты являются задачи, для которых практически не требуется работа с памятью. Чуть хуже решаются задачи, у которых памяти требуется много, но при этом есть возможность использовать разделяемую память. Ещё меньше скорость будет получена, когда требуется доступ по случайному адресу, но есть переиспользование данных (используется текстурная память). Совсем плохо будут решаться задачи у которых не выполняется ни одно из этих требований. Кроме того, если задача требует просто большого количества памяти (несколько гигабайт), то, скорее всего, задачу невозможно эффективно решить на ГА вообще.