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



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

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

Использование других форматов, отличных от JPEG, в качестве базовых для организации такого хранилища изображений не кажется оправданным. Конечно, есть стандартные, широко используемые форматы, которые дают лучшее сжатие при аналогичном качестве (JPEG2000, WebP), но скорость кодирования и декодирования таких изображений очень мала по сравнению с JPEG, поэтому имеет смысл выбрать именно JPEG качестве базового формата для хранения оригинальных фотографий, которые при необходимости будут отмасштабированы в реальном времени после получения запроса от пользователя.

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

Описание алгоритма ресайза на лету


Итак, входными данными являются файлы формата JPEG, причём для достижения быстрого декодирования (это верно как для CPU, так и для GPU), сжатые изображения должны иметь встроенные рестарт-маркеры. Эти маркеры описаны в стандарте JPEG и часть кодеков умеет с ними работать, остальные умеют их не замечать. Если таких маркеров у джипегов нет, их можно заранее добавить с помощью утилиты jpegtran. При добавлении маркеров изображение не изменяется, но размер файла становится чуть больше. В итоге получаем следующую схему работы:

  1. Получаем данные изображения из памяти CPU
  2. Если есть цветовой профиль, получаем его из EXIF секции и сохраняем
  3. Копируем картинку на видеокарту
  4. Декодируем JPEG
  5. Делаем ресайз по алгоритму Ланцоша (уменьшение)
  6. Накладываем резкость
  7. Кодируем изображение по алгоритму JPEG
  8. Копируем изображение на хост
  9. Добавляем в полученный файл исходный цветовой профиль

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

Также возможен вариант решения, когда декодирование джипегов выполняется на многоядерном CPU с помощью библиотеки libjpeg-turbo. В этом случае каждая картинка декодируется в отдельном потоке CPU, а все остальные действия выполняются на видеокарте. При наличии большого количества ядер CPU так может получиться даже быстрее, но будет серьёзный проигрыш в латентности. Если латентность при декодировании джипега на одном ядре CPU является допустимой, то этот вариант может оказаться очень быстрым, особенно для случая, когда исходные джипеги имеют небольшое разрешение. При увеличении разрешения исходного изображения, время декодирования джипега в одном потоке CPU будет увеличиваться, поэтому этот вариант может подойти только для небольших разрешений.

Базовые требования к задаче ресайза для веба


  • Желательно не хранить на сервере десятки копий каждого изображения в разных разрешениях, а быстро создавать нужную картинку с правильным разрешением сразу при получении запроса. Это важно для уменьшения размера хранилища, так как в противном случае придётся хранить много разных копий каждого изображения.
  • Задачу нужно решить максимально быстро. Это вопрос о качестве предоставляемой услуги с точки зрения уменьшения времени отклика на запрос пользователя.
  • Качество отправленного изображения должно быть высоким.
  • Размер файла для отправленного изображения должен быть как можно меньше, а его разрешение в точности должно соответствовать размеру окна, в котором оно появится. Тут важны следующие моменты:

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

б) Экранный ресайз дополнительно потребляет энергию устройства.

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

Описание общей схемы работы


  1. Получаем от пользователей изображения в любых форматах и в любых разрешениях. Оригиналы храним в отдельной базе данных (если нужно).
  2. В оффлайне с помощью ImageMagick или аналогичного софта, сохраняем цветовой профиль, преобразуем исходные оригинальные изображения к стандартному формату BMP или PPM, после чего делаем ресайз до разрешения 1К или 2К и сжимаем в JPEG, затем утилитой jpegtran добавляем рестарт-маркеры с заданным фиксированным интервалом.
  3. Составляем базу данных из таких 1К или 2К изображений.
  4. При получении запроса от пользователя получаем информацию о картинке и размере окна, где должно быть показано это изображение.
  5. Изображение находим в базе данных и отправляем ресайзеру.
  6. Ресайзер получает файл изображения, декодирует, делает ресайз, шарп, кодирует и вставляет исходный цветовой профиль в полученный джипег. После этого отдаёт картинку внешней программе.
  7. На каждой видеокарте можно запустить несколько потоков, а в компьютере можно установить несколько видеокарт — таким образом достигается масштабирование производительности.
  8. Всё это можно сделать на базе видеокарт NVIDIA Tesla (например, Р40 или V100), поскольку видеокарты NVIDIA GeForce не предназначены для непрерывной длительной работы, а у NVIDIA Quadro есть много видео выходов, которые в данном случае не нужны. Для решения этой задачи требования к размеру памяти GPU минимальные.
  9. Также из базы с приготовленными изображениями можно динамически выделить кеш для часто используемых файлов. Там имеет смысл хранить часто используемые изображения по статистике предыдущего периода.



Параметры программы


  1. Ширина и высота нового изображения. Они могут быть любыми и их лучше задавать в явном виде.
  2. Режим прореживания JPEG (subsampling). Есть три варианта: 4:2:0, 4:2:2 и 4:4:4, но обычно используют 4:4:4 или 4:2:0. Максимальное качество у 4:4:4, минимальный размер кадра у 4:2:0. Прореживание делается для цветоразностных компонент, которые зрение человека воспринимает не так хорошо, как яркостную. Для каждого режима прореживания есть свой оптимальный интервал для рестарт-маркеров для достижения максимальной скорости кодирования или декодирования.
  3. Качество сжатия JPEG и режим прореживания при создании базы данных изображений.
  4. Шарп делается в окне 3х3, сигмой (радиусом) можно управлять.
  5. Качество сжатия JPEG и режим прореживания при кодировании финальной картинки. Обычно качество не менее 90% означает, что это сжатие «визуально без потерь», т.е. неподготовленный пользователь не должен увидеть артефакты алгоритма JPEG при стандартных условиях просмотра. Считается, что для подготовленного пользователя нужно 93-95%. Чем больше это значение, тем больше размер кадра, отправляемого пользователю, и тем больше время декодирования и кодирования.

Важные ограничения


Рестарт-маркеры. Мы можем быстро декодировать на видеокарте изображения в формате JPEG только в том случае, если внутри него находятся рестарт-маркеры. В официальном стандарте JPEG эти маркеры описаны, это стандартный параметр. Если рестарт-маркеров нет, то распараллелить декодирование картинки на видеокарте нельзя, что приведёт к очень малой скорости декодера. Поэтому нужна база приготовленных изображений, в которых есть эти маркеры.

Фиксированный алгоритм для кодека изображений. Декодирование и кодирование изображений по алгоритму JPEG является на сегодняшний день самым быстрым вариантом.

Разрешение изображений в приготовленной базе данных может быть любым, а в качестве вариантов мы рассмотрим 1К и 2К (можно взять и 4К). Также можно сделать не только уменьшение, но и увеличение изображений при ресайзе.

Производительность быстрого ресайза


Мы протестировали приложение для быстрого ресайза из комплекта Fastvideo SDK на видеокарте NVIDIA Tesla V100 (OS Windows Server 2016, 64-bit, драйвер 24.21.13.9826) на 24-битных изображениях 1k_wild.ppm и 2k_wild.ppm с разрешением 1К и 2К (1280х720 и 1920х1080). Тесты проведены для разного количества потоков, запущенных на одной видеокарте. При этом требуется не более 110 МБайт памяти на видеокарте на один поток. На 4 потока нужно не более 440 МБайт.

Сначала сжимаем исходные изображения в JPEG c качеством 90%, с прореживанием 4:2:0 или 4:4:4. Затем декодируем и делаем ресайз в 2 раза по ширине и высоте, делаем шарп, потом опять кодируем c качеством 90% в 4:2:0 или в 4:4:4. Исходные данные лежат в оперативной памяти, финальная картинка размещается там же.

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

Пример командной строки для 24-битного изображения 1К
PhotoHostingSample.exe -i 1k_wild.90.444.jpg -o 1k_wild.640.jpg -outputWidth 640 -q 90 -s 444 -sharp_after 0.95 -repeat 200

Бенчмарк для обработки одного изображения 1K в одном потоке


Декодирование (включая пересылку данных на видеокарту): 0,70 мс
Ресайз в два раза (по ширине и по высоте): 0,27 мс
Шарп: 0,02 мс
Кодирование JPEG (включая пересылку данных с видеокарты): 0,20 мс
Суммарное время на один кадр: 1,2 мс

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


Качество Прореживание Ресайз Потоки Частота кадров (Гц)
1 90% 4:4:4 / 4:2:0 2 раза 1 868 / 682
2 90% 4:4:4 / 4:2:0 2 раза 2 1039 / 790
3 90% 4:4:4 / 4:2:0 2 раза 3 993 / 831
4 90% 4:4:4 / 4:2:0 2 раза 4 1003 / 740


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


Качество Прореживание Ресайз Потоки Частота кадров (Гц)
1 90% 4:4:4 / 4:2:0 2 раза 1 732 / 643
2 90% 4:4:4 / 4:2:0 2 раза 2 913 / 762
3 90% 4:4:4 / 4:2:0 2 раза 3 891 / 742
4 90% 4:4:4 / 4:2:0 2 раза 4 923 / 763


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

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

Резюме


Результаты тестов показали, что для видеокарты NVIDIA Tesla V100 скорость обработки 1К и 2К изображений максимальна при запуске 2-4 потоков одновременно, и составляет от 800 до 1000 кадров в секунду на одну видеокарту. Обработка 1К картинок выполняется быстрее, чем 2К, а работа с изображениями 4:2:0 всегда выполняется медленнее, чем с 4:4:4. Для получения окончательного результата по производительности, нужно точно определить все параметры программы и оптимизировать её под конкретную модель видеокарты.

Латентность порядка одной миллисекунды — это неплохой результат. Насколько известно, такую латентность нельзя получить для аналогичной задачи ресайза на CPU (даже при отсутствии необходимости кодирования и декодирования джипегов), так что это ещё один важный аргумент в пользу применения видеокарт в высокопроизводительных решениях по обработке изображений.

Для обработки одного миллиарда джипегов в сутки с разрешениями 1K или 2K, может потребоваться до 16 видеокарт NVIDIA Tesla V100. Некоторые из наших заказчиков уже используют это решение, другие тестируют его в своих задачах.

Ресайз джипегов на видеокарте может быть очень полезен не только для веб-сервисов. Есть огромное количество высокопроизводительных приложений по обработке изображений, где такой функционал может быть востребован. Например, быстрый ресайз очень часто необходим практически для любой схемы обработки изображений, полученных от камер, перед выводом картинки на монитор. Такое решение может работать для Windows/Linux на любых видеокартах NVIDIA: Tegra K1/X1/X2/Xavier, GeForce GT/GTX/RTX, Quadro, Tesla.

Преимущества решения с быстрым ресайзом на видеокарте


  • Значительное уменьшение размера хранилища для исходных изображений
  • Сокращение первичных затрат на стоимость инфраструктуры (железо и софт)
  • Улучшение качества сервиса благодаря малому времени отклика
  • Сокращение исходящего трафика
  • Меньшее потребление энергии на устройствах пользователей
  • Надёжность и скорость представленного решения, которое уже было протестировано на огромных наборах данных
  • Уменьшение времени разработки для вывода на рынок таких приложений для ОС Linux и Windows
  • Масштабируемость решения, которое может работать как на одиночной видеокарте, так и в составе кластера
  • Быстрый возврат инвестиций для таких проектов

Кому это может быть интересно


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

Разработчики ПО могут использовать эту библиотеку, которая обеспечивает латентность порядка нескольких миллисекунд для ресайза джипегов с разрешением 1К, 2К и 4К на видеокарте.

По всей видимости, этот подход может оказаться более быстрым, чем решение NVIDIA DALI для быстрого декодирования джипегов, ресайза и подготовки изображений на этапе тренировки нейросетей для Deep Learning.

Что ещё можно сделать


  • Кроме ресайза и шарпа можно в существующий алгоритм добавить кроп, повороты на 90/180/270, наложение ватермарка, управление яркостью и контрастом.
  • Оптимизация решения для видеокарт NVIDIA Tesla P40 и V100.
  • Дополнительная оптимизация производительности декодера JPEG.
  • Режим пачки для декодирования джипегов на видеокарте.

Комментарии (27)


  1. NickViz
    27.09.2018 10:19
    +1

    а чего цену постеснялись указать? или опять — свяжитесь с нами и мы оценим сколько с вас слупить можно?


    1. homm
      28.09.2018 03:15
      +1

      Давайте вместе посчитаем. Возьмем для примера цены AWS EC2:


      p3.2xlarge инстанс имеет на борту одну NVIDIA Tesla V100 GPU и стоит $3.06 в час.
      Столько же стоит c5.18xlarge инстанс на новых процессорах Intel c 36 ядрами.


      Сравнивать буду с одной из самых быстрых бесплатных библиотек для обработки изображений Pillow-SIMD.


      На странице бенчмарков пока что нет результатов c5 инстансов, поэтому возьмем результаты для c4. Уверяю, что c5 работают не медленнее.


      Сразу выпишу интересующие нас операции:


      Jpeg load speed = 181 Mpx/s
      Lanczos resize speed = 300 Mpx/s
      Sharp filter speed = 524 Mpx/s
      Jpeg save speed = 166 Mpx/s

      Скорости ресайза именно в два раза нет (потому что при уменьшении в кратное количество раз всегда можно считерить и сделать быстрее, так что для бенмарков такие значения лучше не брать), есть результаты для уменьшения в 1,25 раз (238 Mpx/s) и результаты для уменьшения в 8 раз (658 Mpx/s). Мне лень сейчас делать бенчмарк, поэтому я «на глазок» взял 300 Mpx/s.


      Все результаты приведены для одного ядра.


      Сравнивать будем на 2K разрешении, так как это разрешение показало значительно более хорошие результату на Tesla V100.


      Jpeg load time = 1920*1080 / 10**6 / 181 = 0.0115 s
      Lanczos resize time = 1920*1080 / 10**6 / 300 = 0.0069 s
      Sharp filter time = 960*540 / 10**6 / 524 = 0.001 s
      Jpeg save time = 960*540 / 10**6 / 166 = 0.003 s
      Total speed = 1 / (0.0115 + 0.0069 + 0.001 + 0.003) = 44 op/s

      Итого, 44 операции в секунду на одном ядре. Или 44?36 = 1584 операции в секунду на процессоре за ту же цену.


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


      Минус только в задержке. В случае обработки на CPU одно изображение все равно будет ресайзится минимум за 23 мс, тогда как на GPU этот показатель от 1,1 до 1,5 мс.


      1. homm
        28.09.2018 04:04

        Тут оказывается вышла libjpeg-turbo 2.0, я потестил, можно накинуть еще 15% к скорости распаковки и запаковки. А так как это самая дорогая операция, мы получаем уже не 44, а 48 операций в секунду на ядро, или 1722 операции в секунду за те же деньги.


      1. fyodorser Автор
        28.09.2018 12:24

        С интересом читал Ваши статьи про ресайз. Результаты очень интересные.

        Спасибо за информацию, тут есть что обсудить.
        1. Для видеокарты мы рассмотрели худший вариант, т.е. работу с изображениями небольшого разрешения, поскольку при этом видеокарта остаётся недозагруженной. Этот вариант является наилучшим для CPU, особенно если ещё предположить, что производительность будет расти линейно с увеличением количества ядер. Но нужно принимать во внимание и такие подробности:
        а) В этом процессоре частота 3.5 ГГц только в турбо-режиме (в обычном 2.5 ГГц) и неизвестно, какая частота будет при максимальной загрузке. Это нужно проверять, без тестов нельзя.
        б) 36 виртуальных ядер (18 реальных) — это отлично. Мы как раз сделали тесты с libjpeg-turbo и точно знаем, что умножение на 36 является излишне оптимистичным. Для декодирования джипегов в многопоточном режиме нужно умножать примерно на 20 для такого железа, но это тоже нужно проверять для всей схемы обработки, а не только для декодера.
        2. Что касается цен, то в данном случае Tesla V100 — не оптимальный вариант, по стоимости сравнимый с последними CPU Intel Xeon. В этой видеокарте есть ядра для аппаратного ускорения нейросетей, которые в данной задаче никак не используются. Поэтому я и написал, что оптимальный по цене вариант можно выбрать для заданного набора параметров задачи, так что практически в любом варианте подойдут менее дорогие видеокарты. Тогда при корректном сравнении придётся взять более слабый CPU и ситуация станет совсем другой.
        3. Мы сделали тест с ресайзом в 2 раза исключительно для удобства понимания, никакие упрощённые алгоритмы при этом не использовались. Чтобы тест показывал производительность с разных сторон, имеет смысл уменьшать картинку 1920х1080 не в 2-4 раза, а до разрешения 1919х1079 (можно до 1904х1071, чтобы не искажать отношение сторон). Тогда мы и увидим настоящую производительность ресайза. Когда делают ресайз 2560->320, то таким образом явно улучшают результат для CPU. Ситуация, когда нужен ресайз менее чем в 2 раза, вполне рабочая. У нас ресайз в 2 раза делается за 0.27 мс, а ресайз до 1919х1079 занимает 0.39 мс. Интересно, сколько получается на CPU?

        как вы видите, для видеокарты при переходе от 2К к 1К производительность практически не растет

        4. Действительно, при уменьшении не растёт, при увеличении — растёт. Мы как раз работаем на оптимизацией работы с маленькими разрешениями, так что точно будет быстрее. Возможно, в пару раз. А для картинок 4К производительность получается в разы больше, поскольку при таком разрешении можно загрузить видеокарту как следует.
        5. Что касается предварительной подготовки изображений, то делать её придётся в любом случае. Пользователь запросто может прислать картинку на 8 или 12 МПикс, а сохранять её в таком виде не стоит. Поэтому придётся декодировать, ресайзить и кодировать в оффлайне. В этот момент мы и добавляем маркеры нашим кодером, а jpegtran обычно используется только в тех случаях, когда нужно добавлять маркеры без выполнения ресайза, т.е. очень редко. Если же не добавлять маркеры, а использовать исходное большое разрешение, то для такого случая производительность нашего решения будет ещё выше, если сравнивать с аналогичным решением на CPU.


        1. homm
          28.09.2018 12:47

          36 виртуальных ядер (18 реальных) — это отлично.

          У c5.18xlarge инстансов 72 виртуальных ядра, 36 реальных. Так что умножение на 36 является пессимистичным в моих расчетах.


          В данном случае Tesla V100 — не оптимальный вариант. Оптимальный по цене вариант можно выбрать для заданного набора параметров задачи.

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


          Когда делают ресайз 2560->320, то таким образом явно улучшают результат для CPU.

          Я не понимаю, при чем тут 2560>320. Результаты для ресайза 2560>2048 и 2560>320 — это те цифры, которые были у меня на руках без дополнительных бенчмарков. Я их довольно грубо экстраполировал до 2048>1024, но не думаю, что моя оценка имеет точность меньше ±25%.


          Интересно, сколько получается на CPU?

          Будет что-то близкое к результату 2560>2048.


          Пользователь запросто может прислать картинку на 8 или 12 МПикс, а сохранять её в таком виде не стоит.

          Стоит или не стоит — зависит от задачи, тут уже не разработчику библиотек решать.


          1. fyodorser Автор
            28.09.2018 13:06

            Так сделайте измерения на 72 виртуальных CPU и покажите результат. Без умножений и экстраполяций. Мы показали как есть на реальном железе.
            Стоимость будет меньше, а скорость ещё выше вот на этой видеокарте:
            www.nvidia.com/en-us/design-visualization/quadro/rtx-6000
            Мы за пользователя ничего не решаем, я всего лишь сказал, что для 8 МПикс картинок производительность решения на видеокарте станет больше благодаря более высокому разрешению картинки. Это просто факт.


            1. homm
              28.09.2018 17:55
              +2

              Так сделайте измерения на 72 виртуальных CPU и покажите результат.

              А вы мне что? )


              Ну ладно, раз вы так вежливо попросили, сделаю несколько более приближенный к реальности тест.


              Во-первых я сделал в репозитории pillow-perf ветку fastvideo-competition, в которой переписан тест full_cycle, чтобы максимально соответствовать тому, что делаете вы. Там 4 тесткейса:


              • Load+save — только загрузка и сохранение jpeg.
              • +resize — добавляется ресайз посередине. По большей части два этих кейса просто прогревают CPU, чтобы успел выключиться TurboBoost, про который вы упоминали
              • +sharp — добавляется шарп. Это основной тесткейс, результаты которого нас интересуют
              • +sharp — еще один тест, который дублирует предыдущий, чтобы когда какой-то процесс заканчивал работу, нагрузка на CPU не падала и результат не искажался

              Тесты запускаются командой ./pillow-perf/testsuite/run.py full_cycle -s 1920x1080 -n 1024 и никак иначе, без параметра -s во-первых будут неправильно считаться мегапиксели в секунду, во-вторых я поставил там assert ) Результаты будут выводиться для медианного запуска из 1024 попыток.


              Теперь, где будем запускать. Ваше предложение запускать «на 72 виртуальных CPU» очень заманчиво, но не выглядит разумным. Запуская кучу инстансов помельче вы получаете отказоустойчивость и лучшую гранулярность (очень маловероятно, что вам нужно обрабатывать ровно 1500 картинок в секунду, ни больше, ни меньше). Впрочем, жестить и запускать 36 машин тоже не стоит. Я остановился на одном инстансе c5.2xlarge с 8 виртуальными и 4 физическими ядрами. Думаю, не нужно доказывать, что два запущенных рядом одинаковых инстанса будут работать ровно в два раза быстрее, чем один. А 9 ровно в 9 раз быстрее, что по цене и производительности в точности равно одному c5.18xlarge инстансу.


              Всего будет 6 тестов: один поток, 4 потока, 8 потоков и всё еще раз, но с libjpeg-turbo 2.0.


              Многопоток будет запускаться такой командой:


              time (./run.py full_cycle -s 1920x1080 -n 1024 &      ./run.py full_cycle -s 1920x1080 -n 1024 &      ./run.py full_cycle -s 1920x1080 -n 1024 &      ./run.py full_cycle -s 1920x1080 -n 1024 &      wait)

              Ладно, поехали.


              Один поток, libjpeg-turbo 1.4.2


              $ time ./run.py full_cycle -s 1920x1080 -n 1024
              
              Full cycle 1920?1080 RGB image
                  Load+save           0.02544 s    81.51 Mpx/s
                  +resize             0.02222 s    93.33 Mpx/s
                  +sharp              0.02335 s    88.81 Mpx/s
                  +sharp              0.02335 s    88.80 Mpx/s
              
              real    1m36.680s
              user    1m36.228s
              sys     0m1.004s

              Получается 0.02335 s на процессинг одной картинки. Моя оценка была, напомню, 0.0224 s. Дальше в секундах измерять не имеет смысла, буду писать фрупут в мегапикселях и в операциях в секунду. Ну и не буду писать результаты остальных тестов, чтобы экономить место.


              4 потока, libjpeg-turbo 1.4.2


                  +sharp              0.02317 s    89.49 Mpx/s
                  +sharp              0.02352 s    88.16 Mpx/s
                  +sharp              0.02354 s    88.09 Mpx/s
                  +sharp              0.02356 s    88.00 Mpx/s
              real    1m37.357s
              user    6m25.228s
              sys     0m2.800s

              1/0.02317 + 1/0.02352 + 1/0.02354 + 1/0.02356 = 171 операция в секунду
              89.49 + 88.16 + 88.09 + 88.00 = 353 Mpx/s


              Проверяем: 353/1920/1080*10**6 = 171. Сходится, как ни странно )


              8 потоков, libjpeg-turbo 1.4.2


                  +sharp              0.03690 s    56.19 Mpx/s
                  +sharp              0.03690 s    56.19 Mpx/s
                  +sharp              0.03805 s    54.50 Mpx/s
                  +sharp              0.03803 s    54.53 Mpx/s
                  +sharp              0.03806 s    54.48 Mpx/s
                  +sharp              0.03802 s    54.53 Mpx/s
                  +sharp              0.03810 s    54.43 Mpx/s
                  +sharp              0.03811 s    54.42 Mpx/s
              real    2m36.108s
              user   20m31.172s
              sys     0m6.672s

              439 Mpx/s, 212 запусков в секунду.


              Один поток, libjpeg-turbo 2.0


              $ time ./run.py full_cycle -s 1920x1080 -n 1024
              
              Full cycle 1920?1080 RGB image
                  Load+save           0.02120 s    97.81 Mpx/s
                  +resize             0.01968 s   105.37 Mpx/s
                  +sharp              0.02063 s   100.53 Mpx/s
                  +sharp              0.02057 s   100.79 Mpx/s
              
              real    1m24.265s
              user    1m23.808s
              sys     0m0.912s

              Полный вывод, чтобы просто сравнить с libjpeg-turbo 1.4.2.


              4 потока, libjpeg-turbo 2.0


                  +sharp              0.02049 s   101.20 Mpx/s
                  +sharp              0.02079 s    99.74 Mpx/s
                  +sharp              0.02085 s    99.45 Mpx/s
                  +sharp              0.02118 s    97.91 Mpx/s
              real    1m25.715s
              user    5m36.984s
              sys     0m2.768s

              398 Mpx/s, 192 запусков в секунду.


              8 потоков, libjpeg-turbo 2.0


                  +sharp              0.03201 s    64.77 Mpx/s
                  +sharp              0.03203 s    64.74 Mpx/s
                  +sharp              0.03246 s    63.88 Mpx/s
                  +sharp              0.03246 s    63.88 Mpx/s
                  +sharp              0.03303 s    62.77 Mpx/s
                  +sharp              0.03303 s    62.77 Mpx/s
                  +sharp              0.03296 s    62.91 Mpx/s
                  +sharp              0.03295 s    62.92 Mpx/s
              real    2m10.901s
              user   17m12.692s
              sys     0m5.824s

              508 Mpx/s, 245 запусков в секунду.


              Ну и сводная табличка:


              |             | MPx/s | Op/s c5.2xlarge | Op/s c5.18xlarge |
              |-------------|-------|-----------------|------------------|
              | 1c, lib 1.4 | 89    | 43              | -                |
              | 4c, lib 1.4 | 353   | 171             | 1539             |
              | 8c, lib 1.4 | 439   | 212             | 1908             |
              | 1c, lib 2.0 | 101   | 48              | -                |
              | 4c, lib 2.0 | 398   | 192             | 1728             |
              | 8c, lib 2.0 | 508   | 245             | 2205             |

              Спасибо за подсказку с тредами, я был уверен, что это бесполезная фигня )


              1. fyodorser Автор
                29.09.2018 00:08

                Спасибо за информацию, мы у Вас тоже учимся :)
                Будем использовать этот софт вместе с ценой для Amazon в качестве Price/Performance эталона для дальнейших разработок.


  1. AlexAV1000
    27.09.2018 12:06

    Для сжатия, используются вычисления с одинарной или двойной точностью?


  1. fyodorser Автор
    27.09.2018 12:09

    В алгоритме JPEG есть несколько стадий, которые не стоит считать в целых числах: цветовое преобразование, дискретное косинусное и квантование. Мы их считаем с одинарной точностью.


    1. homm
      28.09.2018 03:27

      Однако же в libjpeg-turbo все операции выполняются в целых числах: https://libjpeg-turbo.org/About/SIMDCoverage


      1. fyodorser Автор
        28.09.2018 11:23

        Я знаю, что libjpeg-turbo считает в целых числах, но не думаю, что это правильно. Получается, что при кодировании приходится делать три округления, когда можно сделать только одно. То же самое происходит и при декодировании. Джипег не является беспотерьным алгоритмом именно из-за этих округлений, но ничего хорошего они не несут.


        1. homm
          28.09.2018 12:56

          Джипег не является беспотерьным алгоритмом именно из-за этих округлений

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


          1. fyodorser Автор
            28.09.2018 13:18

            Расскажите нам про DCT.
            Ок, округляйте после каждой стадии, как и libjpeg-turbo 2.0.


  1. nomspls103
    27.09.2018 13:56

    Подскажите пожалуйста, есть ли смысл изучать CUDA если далеко не все смогут запустить ваши проекты у себя (т.к. эта проприетарщина только у NVIDIA). Может, для проектов подобного рода лучше выбрать OpenCL? Ведь он гарантированно есть на любой видеокарте, а значит вы не столкнётесь внезапно с тем что у вашего заказчика есть рабочие станции с видеокартами AMD и эту CUDA придётся срочно портировать на OpenCL. Почему не писать на OpenCL изначально, чтобы избежать подобных рисков?


    1. fyodorser Автор
      27.09.2018 13:57

      Можно для начала посмотреть на рыночные доли NVIDIA и AMD — они сильно отличаются в пользу NVIDIA, т.е. таких карточек намного больше. Большой плюс NVIDIA в том, что у них есть полный спектр видеокарт — мобильные, для ноута/десктопа, серверные. Вот в этом документе есть впечатляющий список приложений на CUDA — www.nvidia.com/content/gpu-applications/PDF/gpu-applications-catalog.pdf
      Приложений для CUDA не много, а очень много. К сожалению, никогда не видел аналогичного документа для OpenCL.

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

      Обычно заказчик покупает рабочие станции для оптимизированных решений, а не наоборот. А срочно портировать OpenCL на CUDA вряд ли придётся: такая разработка и отладка обычно занимает много времени, поэтому лучше сразу подумать, на каком железе и где всё это будет работать. Конечно, есть ситуации когда это не так (например, случай с Маком), но я говорю о часто встречаемых, которых видел немало.

      С моей точки зрения есть смысл изучать CUDA. Тем более сейчас, когда тематика по нейросетям и искусственному интеллекту явно на подъёме. По этой тематике приложений очень много и работы тоже.


      1. aleksandros
        27.09.2018 19:14

        С год назад баловался майнингом, запускал OpenCL- и CUDA-версии разных майнеров. Результат был практически один в один. Ну, конечно, задачка та ещё


  1. windrider
    27.09.2018 16:27

    Интересно бы узнать относительную производительность. То есть, та же задача, с теми же картинками, только без CUDA (напр. libjpeg-turbo), сколько времени занимала?


  1. Sun-ami
    27.09.2018 18:41

    Интересно, а не быстрее ли будет, если делать ресайз в частотной области, а не в пространственной — не выполняя прямое и обратное косинусное преобразование? Хотя бы при кратном ресайзе. Кто нибудь пробовал это делать?


    1. kosmos89
      27.09.2018 19:18

      Это даже для свертки далеко не всегда быстрее. Думаю для ресайза вообще бесперспективно.


    1. fyodorser Автор
      27.09.2018 22:03

      Дело в том, что дискретное косинусное преобразование выполняется не над всем кадром, а над каждым блоком 8х8 исходного изображения. Поэтому вряд ли можно в частотной области сделать ресайз для джипега с произвольным коэффициентом масштабирования. А для ресайза в степень двойки есть такой простой пример: компонент DC (пиксел с координатами 0,0 в блоке 8х8 после косинусного преобразования) является средней яркостью блока 8х8, но даже ресайз в 8 раз по ширине и высоте так делать нельзя, получится очень грубо. Такое усреднение даёт очень приблизительный результат и любой бикубик или ланцош сделает всё гораздо лучше.

      В случае с JPEG2000, где по сути используется пространственно-частотное преобразование, ресайз в 2 раза по ширине и высоте — это компонента LL вейвлетного разложения, которая получается очень просто, но там разложение делают для всей картинки или для отдельного тайла, но не для блока 8х8.


  1. homm
    28.09.2018 00:59

    В оффлайне с помощью ImageMagick, OpenCV или аналогичного софта, сохраняем цветовой профиль

    Забавно, что вы упоминаете OpenCV, потому что в ней нет вообще никакой возможности получить цветовой профиль. OpenCV — это библиотека компьютерного зрения, а не работы с зоопарком графических форматов.


    затем утилитой jpegtran добавляем рестарт-маркеры с заданным фиксированным интервалом.

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


    1. fyodorser Автор
      28.09.2018 11:44

      Про OpenCV — это ошибка, исправлю.
      Наши бенчмарки декодера были сделаны для рестарт интервала 2. Наилучший для нашего декодера рестарт интервал равен единице, но при этом размер файла может увеличиться до 15% по сравнению со случаем без маркеров, поэтому остановились на двойке.


  1. lostmsu
    28.09.2018 18:24

    Мне кажется всё это можно было бы существенно ускорить, если хранить кэш оригиналов прямо в GPU RAM.


    1. fyodorser Автор
      28.09.2018 21:32

      Сжатая картинка занимает 0.2-0.5 МБайт, память видеокарты не более 24 ГБ, так что прямо в GPU можно хранить не более 100 тысяч таких картинок. Если количество изображений для кеша имеет такой порядок, то можно попробовать. Правда, сэкономить удастся только на копировании в видеокарту, а это и так делается очень быстро через x16 PCIE-3.0, т.е. со скоростью порядка 10-11 ГБайт/с. Возможно, такой кеш лучше хранить на быстром диске где-то рядом.


      1. lostmsu
        28.09.2018 21:37

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


  1. fyodorser Автор
    28.09.2018 21:56

    Если хранить несжатые изображения, то на это потребуется в 10-15 раз больше места. Размер памяти на Tesla V100 равен 32 ГБ. Так, конечно, было бы быстрее, но кажется, что для кеша мало места.