История одной #perf оптимизации
Для #impulse мне нужен видеоплеер, с gui на #imgui. Нужен и нужен, с #LLM это очень просто, он случился у веня за 1 вечер, мне нравится.
Но вот в его рамках случилась одна интересная #perf задачка - fused scale + color conversion.
Задача довольно простая - на выход из декодера у тебя есть текстура в цветовом формате A и разрешением XxY, ее надо отмасштабировать в разрешение X2xY2, одновременно поменяв цветовое пространство, чаще всего в RGBA, или линейное.
В ffmpeg есть софтверный скейлер, он хорошо оптимизирован на CPU, но на 4к довольно таких неплохо заметен в профиле.
Challenge accepted!
Далее прямо по стадиям, как я это оптимизировал.
1) Ffmpeg отдает дескриптор формата, его можно загрузить в shader, как константные данные, и тупо написать код, который проинтерпретирует этот формат, для всех пикселей поверхности.
Это ОЧЕНЬ неэффективно делать на CPU, но на GPU это дало хорошее ускорение, и на самом деле, тут уже можно было бы остановиться, потому что GPU, и уже быстрее ffmpeg. Но тогда я был бы не я!
2) Cпросил клоду "вот тут у нас куча мертвых ветвей в шейдере, которые гейтуются внешними константами, которые мы ему передаем как параметры".
Машина мне сказала, чтобы я не ссал, и что оптимизатор драйвера выкинет эти ветви сам.
"Ага", сказал я, и посадил клоду пилить стенд - взять 50 самых частых комбинаций типов поверхностей, которые надо туда-сюда преобразовывать, написать руками 50 шейдеров, и оптимизировать их, один за одним, каждый раз проверяя, что наш generic шейдер выдает тот же самые результат, что и оптимизированный, и что он совпадает с результатом swscaler от ffmpeg.
Результат - generic шейдер в 3 - 4 раза медленнее оптимального.
3) Посмотрели с клодой на данные, поняли, что шейдер можно представить как формат входа X передаточная функция (12 штук примерно) x выход. Всего 2000 разных шейдеров, каждый вход/выход/передаточная запрограммированы руками. Сделали из шейдера jinja шаблон, кодогенератор встроили в сборку, 2000 шейдеров строятся несколько секунд. Результат - ну примерно x2 от руками оптимизированных, то есть, еще в два раза хуже.
4) Подумали, посмотрели на разницу между кодогенерацией и ручным кодом, поняли, что некоторые константные знания - "фактоиды" (как вот вход - выход - передаточная выше) позволяют значительно все ускорить, но тогда декартово произведение всех возможных вариантов становится настолько огромным, что предкомпиляция не выглядит работающим решением.
В итоге переписали шейдер на кастомном DSL, сразу в виде IR на Python.
Вернее, генератор IR шейдера в зависимости от всех возможных известных нам фактоидов, плюс несколько пассов оптимизации IR поверх.
Встроили в тот же кодоген, уже без текста и jinja шаблонов, результат - ровно x1, сгенеренные шейдеры не хуже, чем написанные руками.
5) Можно было бы остановиться, но лучшее - враг хорошего!
И я сказал клоде, что раз уж теперь у нас есть IR, который мы сами и оптимизируем, то зачем нам материализация в виде текста? А если так, то переноси IR прямо в с++ код, шейдер поверх этого IR в C++ код, и генери spir v на ходу, по запросу, без предварительной кодогенерации.
Машина сидит, пыхтит, работает, красота.
UPD: вести с полей:
"Все факты, включая размеры и шаги то, что даст рантайм на этапе 2: 0.924. В последней строке yuv444p и yuyv обгоняют ручные на 10%", так что мы еще и обогнали сами себя же!
Думаю, наш fused scale + color conversion - самый быстрый на рынке!
UPD: забыл вот что еще написать, под macos мой плеер жрет в два раза меньше CPU/GPU, чем IINA, а это так-то вполне себе SOTA на рынке.
Post #4499
1.26K
- 🔥 28
- ❤ 5
- 👍 4
- 🥱 3
- 👌 2
- 🆒 1