Pull to refresh

Comments 36

Интересный подход, вместо того, чтобы выбрать задачу, на которой хорошо показать мощь GPU, вы просто взяли, и сделали на CPU больше работы под таймером. Интересно, производители нового железа тоже так показывают, что новое железо лучше?
Ну целью первой статьи было скорее показать, как написать простую программу на CUDA, чем доказать, что вычисления на GPU можно выполнить быстрее чем на CPU. В следующих статьях уже будут примеры, где CUDA вариант действительно быстрее чем CPU вариант — просто они будут несколько сложнее.
Ну знаете, такие вот публично написанные вещи навевают соответствующие мысли о компании. Например, что это нормальная практика с заказчиками :)
Не совсем Вас понимаю. Что именно имеется ввиду под «нормальная практика с заказчиками»?
Написать одно, померять другое, обнаружить фэйковое улучшение, разумеется. Или еще, найти дурацкое решение задачи, полениться найти пригодное, сделать дурацкое и сказать, мол, ну, нам лень было, потому мы вот тут подкрутили показатели.
Короче, негативные впечатления.
А где было сказано про фэйковые улучшения и подкрутку показателей? Я довольно четко написал, что для данной задачи реальный выигрыш мы не получили — однако сами вычисления выполнились быстрее. В примере для следующей статьи уже будет получено реальное ускорение, с учетом всех операций. Ну а про компанию это вообще не понятно к чему.
Повторю еще раз: цель данной конкретной статьи не состояла в написании супер-пупер быстрого кода на GPU — но в объяснении основ CUDA и написании простого примера для иллюстрации этих основ. Вы даже могли бы обратить внимание на плашку «tutorial».
Не сердитесь, то что вычисления на GPU – это быстро и круто все и так понимают, а вот пояснения принципов работы CUDA и простые примеры для тех кто делает первые шаги в этом деле лишними никогда не будут.
ну хоть что-то реальное делают на куде, а то кругом одни решатели СЛАУ
может, напишите mp3 енкодер? мне кажется, эту задачу можно легко распараллелить. вот чем реально было бы сравнить производительность CPU и GPU
Обработка изображений, много чего реализовано на CUDA в OpenCV, кстати недавно сталкивался с обучением сверточных нейронок на CUDA, прирост впечатляет
В примере к второй статье будет размытие изображения по Гауссу. К четвертой — поразрядная сортировка. К шестой — смешивание изображений по Пуассону. Насчет mp3 енкодера — я посмотрю, может действительно будет примером к одной из статей, если получится разобраться:)
Я по-быстрому погуглил — так как одним из шагов mp3 енкодинга является FFT или какой-то его аналог, а разные бенчмарки говорят, что для большого количества данных GPU реализация FFT дает определенный прирост в скорости — можно потенциально ускорить этот процесс. Однако, думаю что это нетривиальная задача. — Вот тут например пишут, что в 2008 nVidia проводила конкурс на реализацию mp3 енкодера на CUDA.
Из того, что реально работает:
У нас в разработке программа, вычисляющая специфические интегралы теперь и с использованием GPU. Пришлось изрядно повозиться, чтобы настроить одновременное вычисление рациональных функций в разных точках, но результат был действительно хорош.
К сожалению, проект пока закрытый, так что никаких точных данных дать не могу, но даже игровая десктопная GPU в несколько раз перебивает четырехядерный процессор.
Ну, с каждым годом доля GPU в суперкомпьютерах стремительно растет. Поверьте, там действительно работают реальные программы и производят реальные, полезные вычисления :)
А нет ли какого-либо способа обобщенно писать ядра для OpenCL и CUDA? Или это из области фантастики? Я бы видел это так: пишешь на обобщенном языке, применяешь тулзу с параметром --nvidia-kernel или --amd-kernel и получаешь код требующий компиляции или ядро, которое уже можно применять.
Я ни о чем таком пока не слышал и в гугле не нашел, хотя тут и говорят, что конвертация CUDA кода в OpenCL код довольно прямолинейна. Все что нашел — cu2cl — автоматический транслятор CUDA в OpenCL, естественно, с некоторыми ограничениями.
Как понимаете, все дело в определенных различиях между технологиями (которые, кстати, хорошо описаны по первой ссылке).
Спасибо! // Кармы нет чтоб +1 поставить, приходится не-по-программерски )
А зачем примешивать CUDA к OpenCL, если последняя работает и так на всех устройствах без изменения кода?
Другое дело, что обычно для оптимального кода под CPU/AVX нужно писать немного разные ядра, чем на GPU. Но все ядра работают везде, с разной оптимальностью конечно (проверено от titan и firepro, до xeon phi и CPU).
Не всё так гладко. Nvidia видеокарты не держат OpenCL 2.0. (и даже с предыдущими версиями драйвера выходят очень поздно). т.е. по факту Nvidia довольно давно забивает на поддержку OpenCL.

только начиная с OpenCL 2.0 введены atomic операции над float'ами (в CUDA они есть начиная с CC 2.0, т.е. грубо говоря начиная с GTX 4xx серии, довольно древних карт)

без atomic операций над float'ами очень многие алгоритмы (особенно различные физические симуляции и агрегации реальных вещественных данных) — становятся нежизнеспособны.

да, существует код для более ранних версий OpenCL — позволяющий реализовать костыль, имитирующий atomic операции над float'ами, но производительность эти трюки убивают в разы.

поэтому на практике — приходится писать CUDA код для Nvidia и OpenCL код для AMD как минимум ради atomic float'ов.
Никогда не заморачивался с атомик операциями, наверное в силу специфики или простоты задач. Можете привести примеры или ссылку, где атомик спасают ситуацию?

Да и устройства с opencl 2.0 я увидел реально сосвсем недавно, кажись как вышел amd драйвер омега или как его там. До этого всегда было 1.2 или даже 1.1 на нвидиа.

У нас проблема другая, девайсы с двойной точностью от нвидиа становятся с совсем невменяемой ценой(хорошо что есть пока titan). А карты amd с их новой GCN1.1 работают медленнее старых 7970 и 280Х, не смотря на большее кол-во ядер.
Доольно простой пример, где нужен атомик над float — построение гистограммы для любых float значений. Скажем, имея большое количество записей о спортсменах, вы хотите найти средний вес в зависимости от роста — тогда вам сначала нужно построить гистограмму 'рост'->'суммарный вес всех спортсменов с таким ростом', и тут уже без атомик не обойтись, а вес вполне может быть float.
да, в большинстве случаев это — построение гистограмм. могу привести пару примеров из своих симуляций:

1. есть множество силовых точек, связанных «резинками» (например, как в таких задачах en.wikipedia.org/wiki/Force-directed_graph_drawing или в задачах на симуляцию физики мягкого тела (мой случай))

при условии, что количество резинок, связанных с одной силовой точкой, может быть произвольным, и на каждом шаге симуляции нужно «повлиять» на каждую силовую точку вектором силы каждой растянутой «резинки», связанной с силовой точкой — стандартный способ избежать atomic float'ов, когда kernel совершает проход по множеству точек, и каждая точка неконкурентно аккумулирует вектора сил всех связанных с ней резинок — не подходит (количество резинок у каждой силовой точки значительно отличается, что будет запирать синхронные WARPы. к тому же, необходимо будет использовать динамические списки резинок — т.е. гораздо более сложный код менеджмента модели).

при наличии atomic float'ов — мы просто совершаем пробег по множеству резинок, каждая из которых добавляет свой вектор силы к обоем силовым точкам, к которым привязана (через atomicAdd float). т.к. две резинки могут одновременно попытаться добавить свой вектор силы к одной и той же силовой точке, без atomic float'ов такой подход не работает.

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

т.е. при желании решать реальные задачи, а не фигурно извращаться с архитектурой GPGPU — atomic float'ы + CUDA + современные nvidia GPU (и чем дальше, тем лучше с этим) позволяют просто писать SIMD код в лоб, при этом имея производительность на грани теоретического максимума.

— это наиболее простой и понятный пример применения atomic float'ов.

в моей практике была куча ситуаций, когда происходило такое распараллеленное аккумулирование векторов сил на силовые точки. во всех случаях сначала писался код в лоб с atomicAdd (т.к. это по сути мнемонический эквивалент предполагаемой операции всегда)

Затем в некоторых случаях производились попытки оптимизировать это, «вывернув» структуры данных под то, чтобы каждая силовая точка становилась аккумулятором для списка внешних воздействий. это всегда усложняло код (денормализация данных под каждый конкретный kernel), но, как ни странно — в большинстве случаев не только не увеличивало производительность, а, наоборот, уменьшало её (я для себя пришёл к выводу, что денормализованные данные занимают большое количество памяти, что приводит к резкому уменьшению эффективности работы кешей).

Таким образом, здравый смысл и добро восторжествовали, и сегодня в CUDA + NVidia окружении простой код в большинстве случаев работает наиболее эффективно, и «камень с души» о непроизведённой тонкой оптимизации оказывается снят в общем случае. Atomic float'ы рулят!

p.s. субьективно — начиная с GTX 4 серии (т.е. с того момента, как в CC 2.0 практически сошла на нет необходимость возни c global memory coalescing) смысла в попытках обеспечить когерентность чтений, использовать shared memory в кернелах, использовать хитрые алгоритмы с синхронизациями и прочий ловлевел hardcore — канули в лета, и я очень рад, что это так.

для примера: я очень плотно занимался поиском оптимального алгоритма для SPH симуляций. в CUDA samples с давних времён был включен классический пример «particles», в котором ещё остались оптимизации под СС 1.2 (GTX 2 серии и раньше, когда CUDA только зарождался). но все эти ухищрения только мешали раскрыться потенциалу GTX 7 серии (я экспериментировал на GTX Titan).

в итоге, оптимальной оказалась практика, предложенная Rama Hoetzlein (он же провёл очень хороший обзор эволюции решений этой задачи здесь: on-demand.gputechconf.com/gtc/2014/presentations/S4117-fast-fixed-radius-nearest-neighbor-gpu.pdf). К сожалению, его собственное решение fluids3.com/ содержит несколько эммм досадных недоразумений в коде, в результате чего его код из коробки выполняется медленнее, чем «particle» демка из cuda samples. Но сделанный мной гибрид обоих решений сумел быть существенно более производительным, чем оба исходных варианта.

Это я всё к чему. Итоговый код не содержит никаких низкоуровневых фокусов с __syncthreads, sharedMem, разделений структур на отдельные вектора с денормализацией и сортировкой по ключам, хитрых fastRadixSort; и при этом выполняется в разы быстрее на современных Nvidia GPU.

Поэтому, если вы видите применение низкоуровневых фич CUDA, настоятельно рекомендую убедиться в том, что автор — не писал это «по книжкам» под допотопные устройства году эдак в 2009-2010 и/или не является нанотехнологически-академическим теоретиком из серии произвели исследование на миллион и выяснили «на GTX 9800 мой код даёт x10 ускорение». Велика вероятность, что на GTX 780 он будет выполняться в лучшем случае с той же скоростью, а то и медленнее решения в лоб на тех же пресловутых atomic float'ах. И здесь по опыту — очень легко попасть в ловушку устаревших мировоззрений сумрачного гения, потратить кучу времени, заморочить голову и в итоге так и не получить оптимального кода под свою задачу.

На этом всё. Извините, накипело. Надеюсь, мой опыт кому-то будет полезен.
Вы, судя по всему, «в теме»:) Хотел спросить: то-есть если из этого примера убрать перемещение весов фильтра в общую память блока — то на GPU с Compute Capability >= 2.0 производительность останеться прежней?
Прошу прощения, не из этого примера, а из примера ко второй статье, и не «останеться» а «останется»:)
не берусь оценивать, я бы посоветовал провести эксперимент и сравнить результаты.

здесь слишком простой код, и то, что в моих «реальных» задачах даёт размытую на общем фоне оптимизацию в 2-3%, здесь легко может давать 20-30% и больше.

это, кстати, одна из ловушек: вы пишите примитивный тестовый код, он с применением тонкой оптимизации даёт вам выигрыш в 30-50%, вы усложняете код и продолжаете поддерживать общую идею тонкой оптимизации, удерживая в голове её «стоимость» в 30-50%. но эффект уже давно мог быть размыт и в реальной ситуации составлять менее 2-3%, а для вас любое изменение алгоритма — всё ещё будет обозначать тотальную концентрацию и кучу оптимизационных экспериментов.

у меня есть сложная симуляция (развитие вот этого начинания: habrahabr.ru/post/153169/), код которой расширялся и менялся десятки раз, и я множество раз проводил повторные оптимизации.

за это время я выделил для себя такие принципы:

— не стоит переоценивать теоретическую предсказуемость изменений производительности при изменениях кода. если есть 2-3 решения и хочется хороший результат — лучше реализовать их все и промерять, результаты часто оказываются очень неожиданными в силу того, что архитектура современных GPGPU очень нелинейной стала, как и поведение nvcc.

например, добавил float4 в структуру в которой их уже 8, и получил -30% производительности, хотя до этого пару раз добавлял без всякого оверхеда. значит, вылез за пределы какого-то кеша на своих данных, или количества регистров

— в итоге, ни один реальный алгоритм мне не удалось оптимизировать с использованием sharedMemory или __syncthreads. эти фокусы работают только на очень простых данных, либо на очень старых устройствах.

— разделять данные { float3 xyz; float3 fxfyfz } на два отдельных вектора — бывает полезно попробовтаь

— не стоит бояться atomic float'ов, часто они работают быстрее варианта с денормализованными данными (сгенерировать много данных для более простого пробега по ним бывает менее оптимально, несмотря на WARP блокировки — видимо, в силу того, что более компактные структуры данных лучше кешируются).

— эксперименты с параметрами CUDA компилятора часто приводят к очень интересным результатам: CC 1.x на простых симуляциях был сильно быстрее чем CC 2.0, но CC 1.x не умел atomicFloat'ы. зато CC 3.x можно заставить одновременно использовать atomicFloat'ы и генерировать менее прецезионный код ключиками "--use_fast_math –Xptxas –v,–abi=no". обещают эту фичу выпилить (там не по самым строгим стандартам считаются float'ы), но 180-250% прозиводительности заставляют меня до последнего генерировать не до конца ГОСТ, зато быстрый код.
Ясно, спасибо. Спросил потому-что практические задания для udacity-курса выполнялись на GPU с архитектурой Fermi — то-есть СС=2.x, но инструкторы все-таки советовали перемещать данные в shared memory, да я и сам замечал прирост в производительности. Я ведь правильно понимаю, дело в том, что начиная с СС 2.0 CUDA и сама перемещает часто используемые данные в L1 кэш — именно поэтому использование shared memory может и не дать прироста?
Честно говоря, не знаю. Я делюсь своим практическим опытом. В версию с автоперемещением данных в L1 кеш готов верить.

>но инструкторы все-таки советовали перемещать данные в shared memory

Думаю, доля смысла в этом есть. но очень важно не спугнуть новичков всеми этими наворотами!

Я до сих пор не видел хорошей статьи, в которой главным образом утверждалась бы аксиома «не бойтесь и пробуйте решать ваши реальный задачи в лоб без вникания в моменты тонкой оптимизации! на современных устройствах оптимизации перестали быть критически значимыми». сразу начинают нагонять страху кучей моментов, которые якобы должен удерживать в голове GPGPU программист. А в результате — community чуть менее чем никакое, приток новичков плохой, GPGPU развивается в разы медленнее, чем мог бы.

когда я 3 года назад входил «в тему», мне понадобилось много самурайского духа, чтобы сначала решиться и вкурить все эти особенности архитектуры GPGPU, а затем ещё столько же, чтобы осознать, что все эти фичи в 95% случаев не критичны и в первую очередь нужно просто решиться писать GPGPU код, простой и прямой как молоток. и всё будет хорошо работать.
>У нас проблема другая, девайсы с двойной точностью от нвидиа становятся с совсем невменяемой ценой(хорошо что есть пока titan). А карты amd с их новой GCN1.1 работают медленнее старых 7970 и 280Х, не смотря на большее кол-во ядер.

я тоже запасся двумя GTX Titan, однако, до пользы от применения двойной точности у меня ещё ни разу не дошло. греет душу, что если понадобится — есть на чём посчитать :-)

>Да и устройства с opencl 2.0 я увидел реально сосвсем недавно, кажись как вышел amd драйвер омега или как его там. До этого всегда было 1.2 или даже 1.1 на нвидиа.

Жесть конечно. Начинаю понимать, почему convnet и много других интересных вещей сходу пишутся под nivida. жить без atomic float'ов в XXI веке — это знатное извращенство.
Статья интересная, но ваш стиль корпоративного блога (боковые полоски а-ля ковер) отвлекают взгляд от чтения. Читать реально сложно.
Эх, я бы с удовольствием ковёр с таким узором на стену повесил! С компьютерными разъёмами-то :)
Ну может, чуть менее ярко =) Так-то идея классная, но немного мешается =)
Вопрос: на картинке какого разрешения проводились тестирование? А то я не нашел нигде в коде и статье.
Немаленького: 10,109 × 4,542. Для изображений стандартных размеров смысла использовать CUDA вообще бы не было — подозреваю, что даже вычислительная часть выполнялась бы медленнее, чем на CPU, не учитывая операций с памятью. Думаю, в следующих статьях (кроме 2 — она уже написана) буду проводить тестирование на входах разной размерности.
Первое стоит отметить, что в реальности при конвертации BGRA -> Gray обычно ведут расчет в целых числах.
Так же ради интереса запустил функцию преобразования BGRA -> Gray, которая работает на одном ядре i7-4770. Получил следующие результаты:
Scalar version — 54.7 ms.
SSE2 version — 20.9 ms.
AVX2 version — 19.0 ms.
Вывод — задача явно упирается в пропускную способность памяти даже для CPU. Иначе говоря, слишком мало вычислений на каждый пиксель, что бы было оправдано использование GPU, хотя наверное и подойдет как учебный пример.
Вы правы, я почти так и написал в статье:
слишком мало работы выполняется над каждым пикселом

эта задача слишком простая — с ней достаточно хорошо справляется и CPU

Но это действительно учебный пример, на котором довольно просто показать основы CUDA.
Добавил размер изображения в статью.
Sign up to leave a comment.