Как перевести вычисления на видеокарту
Вычисления на видеокарте, руководство, лёгкий уровень
Это руководство поясняет работу простейшей программы, производящей вычисления на GPU. Вот ссылка на проект Юнити этой программы:
Она рисует фрактал Мандельброта.
Я не буду пояснять каждую строчку кода, укажу только необходимые действия для реализации вычислений на GPU. Поэтому, лучше всего открыть код программы в Юнити и там смотреть, как используются поясняемые мной строчки кода.
Шейдер, который рисует фрактал, написан на языке HLSL. Ниже приведён его текст. Я кратко прокомментировал значимые строки, а развёрнутые объяснения будут ниже.
Внимательный читатель скажет: автор, поясни! Размер текстуры — 1024х1024, а количество потоков — 32х32. Как же параметр id.xy адресует все пиксели текстуры?
Внимательный, но неопытный в вопросах вычислений на GPU читатель перебьёт: позвольте! А откуда следует, что количество потоков 32×32? И как понимать «id.xy»?
Второму я отвечу так: директива [numthreads(32,32,1)] говорит, что у нас 32х32х1 потоков. При этом, потоки образуют трёхмерную сетку, потому что параметр id принимает значения в виде координат пространства 32x32x1. Диапазон значений id.x [0, 31], диапазон значений id.y [0, 31], а id.z равен 0. А id.xy — это краткая запись uint2(id.x, id.y)
Именно 32×32 потоков у нас было бы (этой я уже отвечаю первому внимательному читателю), если бы мы вызвали этот кернел со стороны CPU командой
Видите эти три единицы? Это то же самое, что цифры в директиве [numthreads(32,32,1)], они умножаются друг с другом.
Если бы мы запустили шейдер вот с такими параметрами:
То по оси x у нас было бы 32 * 2 = 64, по оси у 32 * 4 = 128, то есть всего — 64х128 потоков. Параметры просто перемножаются по каждой оси.
Но нашем случае кернел запущен так:
Что даёт нам в итоге 1024х1024 потока. И значит, индекс id.xy будет принимать значения, покрывающие всё пространство текстуры 1024х1024
Так сделано для удобства. Даные хранятся в массивах, каждый поток осуществляет одну и ту же операцию над единицей данных, и мы создаём количество потоков равным количеству единиц данных, при чём так, чтобы индекс потока адресовал свою единицу данных. Очень удобно.
Вот и всё, что нужно знать про шейдерный код нашей фракталорисующей программы.
Теперь рассмотрим, что мы сделали на стороне CPU, чтобы запустить шейдерный код.
Объявляем переменные: шейдер, буффер и текстуру
Инициализируем текстуру, не забыв включить enableRandomWrite
Инииализируем буффер, задав количество объектов и размер объекта. И записываем данные предварительно наполненного массива цветов в видеопамять
Инициализируем шейдер и задаём для кернела текстуру и буффер, чтобы он мог писать в них данные
В этом состоит подготовка данных. Теперь остаётся только запустить кернел шейдера
После выполнения этой команды текстура заполняется цветами, которые мы сразу видим, потому что текстура RenderTexture использована в качестве mainTexture для компонента Image, на который смотрит камера.
Как GPU-вычисления буквально спасли меня на работе. Пример на Python
Сегодня мы затрагиваем актуальнейшую тему — Python для работы с GPU. Автор рассматривает пример, тривиальный в своей монструозности, и демонстрирует решение, сопровождая его обширными листингами. Приятного чтения!
Никого из нас в той или иной форме не обошел хайп вокруг GPU-вычислений, развернувшийся в последнее время. Прежде, чем вы станете читать далее, поясню: я не эксперт по GPU. Мой путь в мире GPU только начинается. Но эта технология сегодня достигла такой мощи, что, вооружившись ею, можно решать целую уйму задач. Мне на работе поручили задачу, на выполнение которой машина тратила целые часы, а прогресса так и не было видно. Но, стоило мне взяться за GPU – и проблема стала решаться за секунды. Задачу, на выполнение которой ориентировочно требовалось 2 суток, я смог решить всего за 20 секунд.
В следующих разделах я детально опишу эту задачу. Также мы обсудим, как и когда использовать GPU для решения любых подобных задач. Итак, читаем внимательно – поверьте, вы не пожалеете. Сначала мы вникнем в детали задачи, затем освоимся с GPU и, наконец, воспользуемся GPU для решения этой задачи. Я буду пользоваться библиотекой Python Numba и графическим процессором Nvidia Volta V100 16GB GPU.
1. Подробное описание задачи
В сфере розничной торговли часто приходится искать похожие или наиболее близкие объекты. Мне дали список позиций, каждая из которых была представлена k латентными атрибутами. Итак, мне было поручено найти топ-3 наиболее схожих позиций к каждой из позиций списка. Метрикой схожести в данной задаче было выбрано косинусное сходство. Вот как выглядели мои данные.
Список позиций данных с 64-латентными признаками
Мне дали список, в котором было около 10⁵ позиций. Поиск 3 наиболее схожих позиций для каждой из них потребовал бы проверить косинусное сходство с каждым без исключения элементов в списке. Получалось бы n * k операций, где n – количество позиций, а k – атрибуты на каждую позицию. Потребовалось бы получить скалярное произведение данной позиции с каждой из остальных позиций в списке.
Код для нахождения трех позиций, максимально подобных заданной
Теперь, при нахождении топ-3 схожих для всех позиций в списке, сложность умножается еще на n. Окончательная сложность оказывается равна O(n*n*k) = O(n²k).
Код для нахождения трех наиболее схожих позиций для каждой позиции в списке
Тестовый прогон и оценка времени
Я запустил код, попробовав найти 3 наиболее похожие позиции из подмножества, содержащего n = 10³ позиций с k = 64. На выполнение этой задачи при помощи Python потребовалось около 17 секунд со средней скоростью 3.7 *10⁶ операций в секунду. Код был хорошо оптимизирован с применением операций и массивов Numpy. Отмечу, что все эти операции последовательно выполняются на CPU.
Прогон для n = 10³ позиций
Вывод: время, потребовавшееся для n = 10³ позиций
Далее я увеличил тестовое подмножество до n =10⁴ позиций. Поскольку сложность равна O(n²k), длительность выполнения возросла в 100 раз (поскольку n возросла в 10 раз). На выполнение кода ушло 1700 секунд = 28,33 минуты.
Вывод: время, потребовавшееся на обработку n = 10⁴ позиций
Далее подходим к самому важному: оценке времени, которое потребуется на обработку полноценного списка из 10⁵ позиций. Посчитав, увидим, что временная сложность вновь возрастет в 100 раз, поскольку временная сложность алгоритма равна O(n²k).
Ориентировочное время = 1700 * 100 секунд = 2834 минуты = 47,2 часа
О, Господи! Так долго.
Вероятно, вы уже догадываетесь, что на самом деле мне удалось все сделать очень быстро, воспользовавшись силой GPU. На самом деле, выигрыш во времени при использовании GPU просто шокирует. Цифры я оставлю на закуску, а пока предлагаю вам познакомиться с миром GPU.
2. Сравнение CPU и GPU
Центральный процессор (CPU), в сущности, является мозгом любого вычислительного устройства: он выполняет записанные в программе инструкции, осуществляя управляющие, логические операции, а также операции ввода/вывода.
Правда, у современных CPU по-прежнему не так много ядер, и базовая структура и назначение CPU – обработка сложных вычислений – в сущности, не изменились. На самом деле, CPU лучше всего подходит для решения задач, заключающихся в разборе или интерпретации сложной логики, содержащейся в коде.
В свою очередь, графический процессор (GPU) обладает более мелкими логическими ядрами, которых, однако, гораздо больше (речь идет об арифметико-логических устройствах (АЛУ), управляющих элементах и кэш-памяти), которые спроектированы с расчетом на параллельную обработку в целом идентичных и сравнительно простых операций.
У GPU больше арифметических логических устройств (ALU), чем у типичного CPU, поэтому повышена способность параллельной обработки простых операций
Рисунок: сравнение CPU и GPU
3. Когда использовать вычисления на GPU
CPU лучше подходит для выполнения сложных линейных задач. Несмотря на то, что ядра CPU мощнее, GPU позволяют эффективнее и быстрее обрабатывать задачи, связанные с ИИ, машинным и глубоким обучением. GPU справляется с рабочими нагрузками, распараллеливая схожие операции.
Представление об операциях с точки зрения GPU: возьмем, к примеру, операцию поиска слова в документе. Ее можно выполнить последовательно, перебрав одно за другим все слова в документе, либо параллельно, то есть, построчно, либо с поиском по конкретному слову.
Представление об операциях с точки зрения CPU — есть такие операции, например, расчет ряда Фибоначчи, которые нельзя распараллелить. Ведь найти очередное число можно лишь после того, как вычислишь предыдущие два. Такие операции лучше всего подходят для CPU.
В свою очередь, такие операции, как сложение и перемножение матриц, легко выполняются с применением GPU, поскольку большинство из этих операций в матричных ячейках являются независимыми друг от друга, схожи по природе, и поэтому могут быть распараллелены.
4. Кратко о CUDA
CUDA – это платформа для параллельных вычислений и модель API, созданная компанией Nvidia. С помощью этого API можно использовать процессор с поддержкой CUDA-графики для широкого спектра вычислений. Такой подход получил название GPGPU (неспециализированные вычисления на графических процессорах). Здесь о них рассказано подробнеe.
Numba – это свободно распространяемый JIT-компилятор, транслирующий подмножество Python и NumPy в быстрый машинный код при помощи LLVM, это делается средствами пакета llvmlite на Python. В этом пакете предлагается ряд вариантов по распараллеливанию кода на Python для CPU и GPU, в самом коде при этом зачастую достаточно минимальных изменений. См. подробнеe.
Работая с процессором Nvidia Volta V100 16GB GPU, я пользовался библиотекой Numba.
потоки, блоки и гриды
CUDA организует параллельные вычисления при помощи таких абстракций как потоки, блоки и гриды.
Поток: поток CUDA – это назначенная цепочка инструкций, поступающих/текущих в ядро CUDA (на самом деле, это просто конвейер). На лету на одном и том же ядре CUDA может существовать до 32 потоков (в таком случае заполняются все звенья этого конвейера). Это выполнение ядра с заданным индексом. Каждый поток использует свой индекс для доступа элементов в массиве таким образом, что вся совокупность имеющихся потоков совместно обрабатывает все множество данных.
Грид: Это группа блоков. Между блоками отсутствует всякая синхронизация.
CUDA: потоки, блоки, гриды
Но где же именно выполняются потоки, блоки и гриды? В случае архитектуры G80 GPU от Nvidia, вычисления, по-видимому, распределены следующим образом:
Грид → GPU: Весь грид обрабатывается единственным GPU-процессором.
Блок → МП: Процессор GPU организован как коллекция мультипроцессоров, где каждый мультипроцессор отвечает за обработку одного или более блоков в гриде. Один блок никогда не делится между несколькими МП.
Поток → ПП: Каждый МП далее подразделяется на процессоры потоков (ПП), и каждый ПП обрабатывает один или более потоков в блоке.
Я позаимствовал некоторый материал из этой отлично написанной статьи. Рекомендую почитать ее внимательно.
5. Простая программа для сложения массивов на Python, использующая GPU
Как я уже говорил в самом начале, суть данной статьи – помочь широкой аудитории понять силу GPU и приобрести интуитивное представление, как применять GPU для решения повседневных задач. Перед тем, как начинать писать код для GPU, возможно, придется провести некоторые предварительные исследования. Для этого давайте разберем в качестве примера программу для сложения массивов.
Допустим, у нас есть два массива, ‘a’ и ‘b’ размера ‘n’. Мы хотим сгенерировать массив ‘c’, такой, чтобы каждый элемент массива c являлся суммой элементов с теми же индексами из массивов ‘a’ и ‘b’. Но в данном случае для решения задачи мы применим не последовательные вычисления, а параллельные, которые делаются при помощи GPU.
Мы запустим n потоков/ядер. Индекс, под которым работает каждый конкретный поток, можно вывести из следующей формулы:
index = cuda.blockIdx.x * cuda.blockDim.x + cuda.threadIdx.x
В случае с двумерной матрицей в индексе присутствует два компонента, означающих строку и столбец, которые можно вывести следующим образом:
row = cuda.blockIdx.x * cuda.blockDim.x + cuda.threadIdx.x
col = cuda.blockIdx.x * cuda.blockDim.x + cuda.threadIdx.x
Также нам придется определить количество потоков на блок, скажем, tpb и блоков на грид, допустим bpg. Воспользуемся стандартными числами для них.
Ниже приведены реализации данного решения как для CPU, так и для GPU. Посмотрите оба листинга, чтобы уловить разницу.
Последовательная реализация для CPU.
Параллельная реализация для GPU
6. Нахождение 3 наиболее похожих позиций для каждой позиции в списке при помощи GPU
Хорошенько вникнув в теорию и практику, возвращаемся к исходно поставленной задаче: найти топ-3 похожих позиции для каждой позиции в списке при помощи вычислений на GPU.
В данном случае основная идея заключается в том, что у нас n позиций, и мы запустим n потоков. Каждый поток будет работать параллельно с остальными и независимо от них, вычисляя по 3 наиболее похожие позиции для каждой позиции в списке. За каждую позицию будет отвечать один поток.
Реализация для GPU
Код для нахождения 3 позиций, наиболее похожих на данную
Общее время, затраченное GPU для нахождения топ-3 схожих позиций для каждой позиции в списке, составило 481 мс (0,5 секунд). Еще 20 секунд потребовалось для копирования данных с устройства на хост и с хоста на устройство.
7. Вывод
Задача, решение которой на CPU потребовало бы около 2 дней, на GPU была решена за 20,5 секунд. Это оказалось возможно только благодаря природе задачи. Поиск 3 наиболее похожих позиций для ‘A’ не зависит от поиска 3 наиболее похожих позиций для ‘B’. Мы воспользовались этим фактом и применили параллелизм, предоставляемый GPU, для ускорения процесса. Также пример иллюстрирует, задачи какого типа удобнее всего решать при помощи могучего GPU.
Вычисляем на видеокартах. Технология OpenCL. Часть 0. Краткая история GPGPU
22 июня автор курса «Разработчик C++» в Яндекс.Практикуме Георгий Осипов провёл вебинар «Вычисляем на видеокартах. Технология OpenCL».
Мы подготовили для вас его текстовую версию, для удобства разбив её на смысловые блоки.
Цикл будет полезен и тем, кто уже знаком с OpenCL: в нём мы поделимся некоторыми хаками и неочевидными наблюдениями из собственного опыта.
CPU — в помойку?
В статье будем рассматривать технологию GPGPU. Разберёмся, что значат все эти буквы. Начнем с последних трёх — GPU. Все знают аббревиатуру CPU — Central Processor Unit, или центральный процессор. А GPU — Graphic Processor Unit. Это графический процессор. Он предназначен для решения графических задач.
Но перед GPU есть ещё буквы GP. Они расшифровываются как General-Purpose. В аббревиатуре опускают словосочетание Computing on. Если собрать всё вместе, получится General-Purpose Computing on Graphic Processor Unit, что по-русски — вычисления общего назначения на графическом процессоре.
То есть процессор графический, но мы почему-то хотим вычислять на нём что-то, что вообще к графике никакого отношения не имеет. Например, прогноз погоды, майнинг биткоинов. Моя задача в ближайшее время — объяснить, зачем нужно на процессоре для графики обучать, например, нейросети.
Самое распространённое место, где можно обнаружить GPU, — ваша родная видеокарта. Всем известно, что если вставить в неё штекер монитора, появится картинка. На самом деле видеокарта — это не только гнездо для монитора, но и полноценное вычислительное устройство.
Давайте немного отвлечёмся от видеокарты и вернёмся в 2000 год, когда процессоры CPU набирали всё больше и больше мегагерц. Компания Intel тогда прогнозировала, что к 2010 году тактовая частота одного ядра процессора достигнет 10 гигагерц. Сейчас мы знаем, что эти ожидания не сбылись.
По графику видно: рост действительно был очень хороший, но где-то в районе 2004–2005 года он резко остановился. И дальше процессоры стали даже немножко терять в мощности. Развитие центральных процессоров в итоге пошло за счёт увеличения количества ядер. Сейчас в мощных компьютерах у CPU может быть 16 ядер. Но тактовая частота ядра за последние 10 лет не росла.
Если посмотреть на другой график, где сравниваются CPU и GPU, можно увидеть, что процессоры в видеокартах эту тенденцию перехватили. Графический процессор продолжил наращивать мощность и во многом опередил центральные процессоры по производительности. Удивительный факт — процессор, который находится в видеокарте, выполняет гораздо больше операций с плавающей точкой в секунду, чем главный, центральный процессор.
Источник. По вертикали GFLOPS, т. е. миллиарды арифметических операций с плавающей точкой в секунду
Остаётся вопрос: в чём GPU уступают CPU? Может, дело не в процессоре, а в памяти? Для сравнения возьмём современную оперативную память DDR4 и графическую память GDDR5. Но и тут оказывается, что графический контроллер во много раз превосходит обычный CPU: доступ к графической памяти у GPU гораздо быстрее — 400 Гб/сек против 35 Гб/сек.
И ещё сравним какой-нибудь современный процессор и не очень современную видеокарту. Например, GeForce GTX 1080 Ti, которую сейчас можно купить только на Авито, и довольно современный процессор Core i9 с хорошей памятью.
Получается, видеокарта обгоняет процессор… И если Core i9 может сделать 460 миллиардов арифметических операций с плавающей точкой в секунду, то GTX1080Ti уже 11 триллионов. В общем, GTX намного превосходит Core i9 и по производительности, и по скорости доступа к памяти.
Когда я увидел это сравнение, у меня возник вопрос: а может, этот медленный CPU вообще не нужен? Что если выбросить его, оставить только GPU и все задачи решать на графическом адаптере? Ответ такой: GPU, конечно, очень классный, но годится не для любых задач. Если избавиться от CPU, всё будет немножечко лагать. Так делать не надо.
GPU хорош для массового параллелизма, когда одновременно выполняется огромное количество очень похожих вычислений. Но тут есть такие весы. На одной чаше latency — задержка, на другой флопсы — количество операций. Например, если операция длится секунду, это очень долго. Но если вы одновременно можете выполнить 100 миллиардов таких операций, у вас получились 100 миллиардов операций в секунду. Для CPU это довольно плохо, потому что нам нужно, чтобы всё работало последовательно. А для GPU — нормально. Главное — не задержка, а результат. Числа в этом примере, конечно, сильно утрированы, реальный параллелизм GPU — порядка нескольких тысяч.
Массовый параллелизм
Хороший пример задачи массового параллелизма — Ray Tracing, или трассировка лучей. В экране много пикселей, и из каждого пикселя выпускается луч, чтобы получить качественный рендер всей сцены. Эти лучи можно обрабатывать параллельно, независимо вычисляя цвет каждого пикселя экрана. Значит, это задача массового параллелизма. Существует прекрасный сайт, на котором вы можете, не отходя от браузера, попрактиковаться в написании трассировщика лучей, работающего на GPU. Там можно найти немало любопытных примеров.
Майнинг биткоинов. Чтобы намайнить биткоины, нужно вычислить много хешей и найти среди них обладающий определёнными свойствами. И у этой задачи, к сожалению, или к счастью, нет существенно лучшего решения, чем просто вычислять хеши, пока вам наконец не повезёт. Хеши считаются независимо, и это можно делать параллельно. Значит, это задача массового параллелизма. Майнинг прекрасно ложится на видеокарту.
Обучение нейросетей. Основная затратная операция, которая используется в Deep Learning, — свёртка изображений. Это вычислительно сложная операция, ведь для подсчёта одного значения свёртки с ядром 5⨯5 нужно произвести 25 умножений и 24 сложения. Радует одно: все значения можно считать независимо. А значит, это задача массового параллелизма.
Многие учёные используют GPU в своих задачах, потому что научных данных часто бывает очень много, они долго обрабатываются, и графический процессор тут иногда применим. Если вы один из таких учёных, то спешу вас обрадовать: на самом деле в программировании под GPU нет ничего сложного, и в этом цикле статей мы напишем полноценную программу под него.
Тем, кто серьёзно заинтересовался вычислениями на видеокартах, я рекомендую прекрасные лекции Николая Полярного. Там он разбирает эту тему гораздо подробнее, чем я. С его разрешения я взял вот эту картинку, которая хорошо показывает соотношение CPU и GPU.
Здесь приведены усреднённые данные. На блок L1/Local memory пока не смотрите, разберём его чуть позже. Видно, что CPU может иметь производительность в 20 гигафлопс, а GPU — в 5000 гигафлопс. Канал памяти у GPU гораздо выше. Но есть одно узкое местечко — шина PCI-E. Она соединяет RAM и GRAM и пропускает всего 8 Гб. Глядя на эту картинку, попробуйте ответить на вопрос: есть ли смысл решать все задачи массового параллелизма с помощью графического процессора?
Предположим, задача — сложить два массива чисел по 10 миллиардов: первое складываем с первым, второе со вторым и так далее. Нужно получить сумму, третий набор чисел. Поручать ли такую задачу графическому процессору?
Понятно, что это задача массового параллелизма. Но есть один момент. Для того, чтобы эти числа сложить на GPU, их нужно сначала передать на GRAM по узенькому мостику в 8 Гб/сек. Видеокарта посчитает всё очень быстро, это факт. Но потом вам нужно будет эти данные передавать по мостику обратно. И окажется, что гораздо быстрее просто передать по большему каналу в CPU, и он своими жалкими 20 гигафлопсами сложит числа быстрее, чем если бы они ехали по шине в GPU.
Правда, уже есть некоторые решения этой проблемы. Технология NVLink позволяет ускорить передачу данных в видеопамять с 8 до 40 Гб/сек. Более того, у некоторых дешёвых видеокарт нет видеопамяти, и они могут обращаться напрямую к RAM. Но мы не будем затрагивать этот момент.
Краткая история GPGPU
Технология GPGPU появилась не сразу. До этого графический адаптер использовался исключительно по назначению. Но когда люди увидели, какие у GPU мощности, им захотелось использовать эти мощности для своих неграфических задач. Тогда приходилось выкручиваться и как-то маскировать свою неграфическую задачу под графическую. Видеокарта думала, что рисует треугольники, а на самом деле вычисляла и обрабатывала научные данные. К счастью, разработчики видеокарт увидели эту проблему и пошли навстречу. Так появилась OpenCL и другие технологии, о которых я немного расскажу.
До появления OpenCL возникла очень популярная технология, отличающаяся только одной буквой, — OpenGL. Она решала графические задачи, нужные в первую очередь для видеоигр.
Я выделил три этапа развития OpenGL:
Первая версия появилась в 1994 году. В 2001 году произошло важное событие: стало возможным написание шейдеров на языке GLSL. Если раньше вы могли просить нарисовать треугольники с конкретными параметрами и текстурами, то в 2001 году появилась возможность запускать свой код на видеокарте, то есть самому писать обработчик, который будет вычислять цвет пикселя и его положение. В 2008 году разработка OpenGL перешла консорциуму Khronos Group. Запомните это название, оно вам ещё встретится.
Идея, лежащая в основе OpenGL, состоит в том, что любую геометрическую фигуру можно приблизить треугольниками. Отрисовывать их OpenGL может с огромной скоростью. Если видеоигра, использующая OpenGL, содержит шейдеры, то они будут скомпилированы в момент запуска игры на компьютере пользователя. Код шейдеров будет исполнен непосредственно процессором видеокарты — это и есть та лазейка, которую использовали ранее для неграфических вычислений с OpenGL.
Назвать OpenGL современной технологией уже нельзя. Ей на смену пришли Vulkan и Metal. И если OpenGL направлена на графику, то Vulkan и Metal более универсальны. Останавливаться на них не будем.
Как я уже сказал, если вам нужна графика, то можно либо замаскировать задачу под графическую, либо использовать специальную технологию, предназначенную для решения неграфических задач на видеокартах. Для этого есть две основных технологии — OpenCL, виновница сегодняшнего торжества, и её конкурент CUDA.
Главное их отличие друг от друга — в том, что CUDA поддерживает только видеокарты NVIDIA. Никакие другие адаптеры запустить CUDA-код не смогут. С другой стороны, в этом есть плюсы. Вы знаете, на каких видеокартах код будет запускаться, поэтому под них можно его хорошо отладить и предварительно скомпилировать.
У карт NVIDIA есть несколько версий API, и вы решаете, под какую версию API из доступных скомпилировать код CUDA. Если выбрать какую-то одну, код на более старых видеокартах не пойдёт, а на более новых не будет супероптимальным. Поэтому лучше компилировать сразу под несколько версий.
В OpenCL возможности предкомпиляции нет, потому что вы не знаете, на каком устройстве код будет запускаться. Исключения, конечно, бывают. Например, вы заключили договор с клиентом, и он говорит, что программа будет запускаться на такой-то видеокарте. Тогда вы можете скомпилировать под неё и отправлять заказчику только собранный OpenCL-код. Но для конечного пользователя так лучше не делать.
Обе эти технологии ушли или уходят с macOS, на ней остаётся Metal. Но что я хочу сказать: видеокарта одна, и бэкенд, на котором всё будет выполняться, один. А значит, любые технологии похожи между собой.
В следующей части мы разберём основные понятия, которые необходимо знать, чтобы написать программу с использованием OpenCL.
