[Python, Java] Удав укрощает Graal VM

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
05-Ноя-2020 01:30


В мире Java за последнее время произошло много интересных событий. Одним из таких событий стал выход первой production ready версии Graal VM.
Лично у меня Graal давно вызывает нескрываемый интерес и я пристально слежу за докладами и последними новостями в этой области. Одно время попался на глаза доклад Криса Талингера. В нём Крис рассказывает как в Twitter удалось получить значительный выигрыш в производительности, применив для настройки Graal алгоритмы машинного обучения. У меня появилось стойкое желание попробовать подобное самому. В этой статье хочу поделится тем, что в итоге получилось.  
Эксперимент
Для реализации эксперимента мне понадобились:
  • cвежий Graal VM Community Edition. На момент написания статьи это 20.2.0
  • выделенное облачное окружение для нагрузочного тестирования
  • NewRelic для сбора метрик
  • генератор тестовой нагрузки
  • программа на Python и набор скриптов, для реализации самого алгоритма ML

Если описать задачу сухим языком математики, то она будет выглядеть так
Найти такие значения параметров $inline$A = (a_1,a_2,..,a_n)$inline$ при которых функция
$inline$f(x_1,x_2,..,x_n)$inline$ принимает максимальное значение.
Я решил минимизировать потребление процессорного времени и выбрал такую целевую функцию:
$$display$$f=1/mean(CPUUtilization)$$display$$
Чем меньше нагружен процессор, тем ближе целевая функция к единице.
В качестве параметров которые нужно найти взял те же, что и в докладе:
-Dgraal.MaximumInliningSize -Dgraal.TrivialInliningSize  -Dgraal.SmallCompiledLowLevelGraphSize

Все они отвечают за инлайнинг. Это важная оптимизация, которая позволяет
сэкономить на вызове методов.
С постановкой задачи оптимизации разобрались. Теперь пришло время пройтись по шагам
алгоритма оптимизации:
  • алгоритм делает предположение о том какие параметры оптимальные
  • меняет конфигурацию JVM и запускает нагрузочный тест
  • снимает метрики и вычисляет значение целевой функции
  • делает новое предположение на основе значения целевой функции

Процесс повторяется несколько раз.

В качестве алгоритма поиска Twitter предлагает использовать байесовскую оптимизацию. Она лучше справляется с зашумленными функциями и не требует большого количества итераций.
Байесовская оптимизация работает путем построения апостериорного распределения функций,
которое наилучшим образом её описывает. По мере роста количества наблюдений улучшается апостериорное распределение и алгоритм становится более определённым в том, какие регионы в пространстве параметров стоит изучить.
Получение метрик из NewRelic
Для работы с NewRelic REST API необходимо узнать свои APP_ID и API_KEY.
APP_ID — это уникальный идентификатор приложения в системе. Его можно найти в разделе APM.
API_KEY необходимо создать или узнать из настроек профиля в NewRelic.
Структура ответа для всех метрик приблизительно одинакова и имеет следующий вид:
{
  "metric_data": {
    "from": "time",
    "to": "time",
    "metrics_not_found": "string",
    "metrics_found": "string",
    "metrics": [
      {
        "name": "string",
        "timeslices": [
          {
            "from": "time",
            "to": "time",
            "values": "hash"
          }
        ]
      }
    ]
  }
}

В итоге метод для получения метрик будет таким:
def request_metrics(params):
    app_id = "APP_ID"
    url = "https://api.newrelic.com/v2/applications/"+ app_id + "/metrics/data.json"
    headers = {'X-Api-Key':"API_KEY"}
    response = requests.get(url, headers=headers, params=params)
    return response.json()

Для получения CPU Utilzation значение params следующее:
params = {
    'names[]': "CPU/User/Utilization",
    'values[]': "percent",
    'from': timerange[0],
    'to': timerange[1],
    'raw': "false"
}

timerange хранит значения времени начала и конца нагрузочного теста.
Далее парсим запрос и извлекаем необходимые метрики
def get_timeslices(response_json, value_name):
    metrics = response_json['metric_data']['metrics'][0]
    timeslices = metrics['timeslices']
    values = []
    for t in timeslices:
        values.append(t['values'][value_name])
    return values

Алгоритм оптимизации
Перейдем к самому интересному — поиску оптимальных параметров.
Для реализации байесовской оптимизации взял уже готовую библиотеку BayesianOptimization.
Сначала создадим метод для вычисления целевой функции.
def objective_function(maximumInliningSize, trivialInliningSize, smallCompiledLowLevelGraphSize):
    update_config(int(maximumInliningSize), int(trivialInliningSize), int(smallCompiledLowLevelGraphSize))
    timerange = do_test()
    data = get_results(timerange)
    return calculate(data)

Метод _updateconfig вызывает скрипт, который обновляет конфиг приложения. Далее в _dotest происходит вызов скрипта для запуска нагрузочного теста.
Каждое изменение конфигурации требует перезапуска JVM и первые несколько минут идёт фаза прогрева. Эту фазу необходимо отфильтровать, откинув первые минуты.
В методе calculate вычисляем целевую функцию:
value = 1 / (mean(filtered_data))

Необходимо ограничить поиск
pbounds = {
            'maximumInliningSize': (200, 500),
           'trivialInliningSize': (10, 25),
           'smallCompiledLowLevelGraphSize': (200, 650)
           }

Так как улучшение должно быть относительно настроек по умолчанию, то я добавил соответствующую точку
optimizer.probe(
        params={"maximumInliningSize": 300.0,
                "trivialInliningSize": 10.0,
                "smallCompiledLowLevelGraphSize": 300.0},
        lazy=True,
        )

Окончательный метод ниже
def autotune():
    pbounds = {
                'maximumInliningSize': (200, 500),
               'trivialInliningSize': (10, 25),
               'smallCompiledLowLevelGraphSize': (200, 650)
               }
    optimizer = BayesianOptimization(
        f=objective_function,
        pbounds=pbounds,
        random_state=1,
    )
    optimizer.probe(
    params={"maximumInliningSize": 300.0,
            "trivialInliningSize": 10.0,
            "smallCompiledLowLevelGraphSize": 300.0},
    lazy=True,
    )
    optimizer.maximize(
        init_points=2,
        n_iter=10,
    )
    print(optimizer.max)

В данном примере _objectivefunction выполнится 12 раз и в конце выведет значение
параметров, при которых наша целевая функция была максимальная. Чем больше итераций, то тем точнее мы приближаемся к максимуму функции.
При необходимости все итерации можно вывести вот так:
for i, res in enumerate(optimizer.res):
    print("Iteration {}: \n\t{}".format(i, res))

Код выведет значения целевой функции и параметров для каждой итерации.
Iteration 0:
    {'target': 0.02612330198537095, 'params': {'maximumInliningSize': 300.0, 'smallCompiledLowLevelGraphSize': 300.0, 'trivialInliningSize': 10.0}}
Iteration 1:
    {'target': 0.02666666666666667, 'params': {'maximumInliningSize': 325.1066014107722, 'smallCompiledLowLevelGraphSize': 524.1460220489712, 'trivialInliningSize': 10.001715622260173}}
...

Результаты
Сравнил два прогона нагрузочного теста с настройками по умолчанию и вычисленными в ходе эксперимента.
На графике можно заметить, что CPU Utilization уменьшился для случая Graal

Среднее время отклика также незначительно снизилось:

Пропускная способность ограничена сверху и никак не менялась для обоих прогонов.

Заключение
В итоге удалось получить снижение нагрузки CPU в среднем на 4-5%.
Для нашего проекта такая экономия на CPU не существенна, но для proof of concept
результат достаточно неплохой.
С2 много лет оптимизировали под Java и поэтому соревноваться Graal с С2 пока сложно. Больше выгоды можно получить от связки Graal с другими JVM языками, такими как Scala и Kotlin.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_python, #_java, #_graalvm, #_python, #_performance_optimization, #_python, #_java
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 17:00
Часовой пояс: UTC + 5