Задача
Рекомендации устаревали быстрее, чем пересчитывались. Просмотры и покупки идут непрерывно, ассортимент меняется, товар уходит из наличия — а матрица оценок обновлялась крупными пакетами, потому что каждый полный проход стоил минут машинного времени. В итоге покупателю показывали персональную выдачу, отставшую и от его последних действий, и от текущего состояния каталога.
Ограничение было не в качестве модели, а в стоимости обновления. CPU-реализация выполняла значительную часть работы последовательно и многократно обходила одни и те же данные — сопоставление признаков, вычисление весов, агрегацию промежуточных результатов, отбор Top-N. С ростом клиентской базы стоимость полного пересчёта увеличивалась почти пропорционально размеру матрицы, то есть проблема со временем усугублялась сама.
Требование заказчика сужало пространство решений: ускорить существующий алгоритм, не меняя его бизнес-логику и не ухудшая качество результатов. Переписать модель было нельзя — она собственная и настроенная.
Решение
Профилирование и граница переноса. Сначала измерили, где именно уходит время, и выделили вычислительное ядро, занимавшее основную часть расчёта. На GPU уехало только оно. Подготовка входных данных и оркестрация остались на CPU: переносить всё подряд означало бы получить сложную систему с тем же временем ответа.
Компактное представление разреженных данных. Матрица взаимодействий «клиент — товар» разрежена: подавляющее большинство ячеек пусты. Хранить её целиком в видеопамяти бессмысленно, поэтому взаимодействия преобразовали в компактный формат — это одновременно уменьшило объём копирования и сняло часть бессмысленных чтений.
Разбиение на независимые блоки. Расчёт разложили по блокам пользователей и товарных кандидатов: каждый CUDA-блок обрабатывает свою часть матрицы, потоки внутри блока параллельно считают оценки. Независимость блоков — то, что позже позволило масштабировать решение на несколько устройств без общей синхронизации.
Раскладка памяти. Данные в памяти GPU расположены так, чтобы соседние потоки читали последовательные участки. Часто используемые параметры алгоритма помещены в быструю память, промежуточные значения агрегируются внутри блока. Именно эта перестройка, а не сам факт переноса циклов на видеокарту, дала основной эффект.
Top-N на стороне GPU. Редукции и первичный отбор лучших кандидатов выполняются до возврата результата. На CPU приходит небольшой список рекомендаций, а не вся рассчитанная матрица оценок — объём передаваемых данных уменьшился на порядки. Для GPU-задач это типичная ловушка: быстрое ядро легко обесценивается дорогой обратной передачей.
Перекрытие передачи и счёта. Работа разделена по CUDA streams: пока один пакет считается, следующий уже загружается в видеопамять, а результат предыдущего возвращается на сервер. Неизменившиеся части каталога и признаков кэшируются на GPU между пересчётами, что дополнительно сокращает копирование.
Пакеты и несколько видеокарт. Полная матрица может не поместиться в видеопамять, поэтому расчёт идёт пакетами, а размер пакета выбирается по доступной памяти конкретной карты. Для больших запусков пользовательские сегменты распределяются между несколькими GPU, и итоговые списки собираются независимо — синхронизировать всю матрицу между устройствами не требуется.
Деградация вместо падения. Система контролирует переполнение памяти и в ответ уменьшает пакет или выполняет неподдерживаемую операцию на CPU. Благодаря этому один и тот же код работает на разных поколениях видеокарт, и под каждый сервер не нужна своя версия алгоритма.
CPU-реализация как эталон. Старая версия осталась источником истины для проверки. На фиксированных наборах пользователей результаты сравнивались по трём признакам: численные значения с допустимой погрешностью, состав Top-N и порядок рекомендаций. Отдельные тесты закрывали граничные случаи — пустую историю клиента, новые товары, крайне популярные позиции, большие разреженные сегменты и повторяемость результата от запуска к запуску.
Честные замеры. Производительность измерялась после прогрева, на одинаковых данных и отдельно по стадиям: подготовка, передача, GPU-расчёт, обратное копирование. Иначе быстрое вычислительное ядро скрывает дорогую загрузку данных, и общая цифра ускорения оказывается вымышленной.
Результат
Полный расчёт рекомендаций сократился с нескольких минут на CPU до нескольких секунд на GPU — ориентировочно в десятки раз. Магазин смог обновлять персональные предложения для сотен тысяч клиентов заметно чаще, и выдача перестала отставать от текущего поведения покупателя и состояния каталога.
Рост каталога и клиентской базы перестал линейно увеличивать задержку: расчёт масштабируется пакетами и распределяется между видеокартами, а матрица больше доступной видеопамяти перестала быть препятствием.
Главное, что получил заказчик, — ускорение без переписывания модели. Результаты новой реализации совпадают с исходным алгоритмом в пределах заданной численной точности, поэтому качество выдачи не пришлось проверять заново на живых пользователях.