Функция оптимизации Python Numba со словарем наборов

Я пытаюсь выполнить моделирование 100K + Монте-Карло и много раз вызывать функцию simulate для этого. Я изучаю использование Nubma для этого и мог бы использовать некоторую помощь, в частности, мне нужно делать пересечения и объединения наборов.

Это минимальный рабочий пример кода, который я пытаюсь оптимизировать. Кажется, что Numba не поддерживает словари наборов, и если бы я использовал словари массивов numpy, то у меня нет доступа к np.intersect1d и np.union1d, любая помощь будет оценена по преобразованию этого в Numba, или, возможно, это плохо подходит для оптимизации Numba?

import random
import numpy as np

from numba import types, jit
from numba.typed import Dict, List

n_positions = 100
position_coverage = 10
min_dist = 5

n_ids = 5

near_dict = {x: set(range(x, x + position_coverage)) 
                if x < n_positions - position_coverage else 
                set(range(x, n_positions)) for x in range(n_positions)}
# near_dict = {0: {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, 1: {1, 2, 3, 4, 5, 6, 7, ...
close_dict = {x: set(range(x, x + min_dist)) 
                if x < n_positions - position_coverage + min_dist else 
                set(range(x, n_positions)) for x in range(n_positions)}
# close_dict = {0: {0, 1, 2, 3, 4}, 1: {1, 2, 3, 4, 5}, 2: {2, 3, 4, 5, ...

near_dict = {k: near_dict[k] for k in random.sample(near_dict.keys(), n_ids)}

@jit(nopython=True)
def simulate(near_dict, close_dict):
    out_dict = {}
    to_pos = None
    to_close = set()
    for id, near in sorted(near_dict.items(), key=lambda x: random.random()):
        if to_pos is None:
            _ixs_to = near
        else:
            to_close |= close_dict[to_pos]
            _ixs_to = near - to_close
        if not _ixs_to:
            return {} # failed no reachable positions
        to_pos = random.choice(tuple(_ixs_to))
        out_dict[id] = to_pos
    return out_dict

if __name__ == '__main__':
    simulate(near_dict, close_dict)

person William Grimes    schedule 20.01.2021    source источник
comment
Здравствуйте, что вы пытаетесь смоделировать, пожалуйста? Это немного сложно расшифровать только по коду.   -  person Shamis    schedule 20.01.2021
comment
Кроме того, всегда ли close_dict имеет такую ​​(или подобную) форму? Из того, что я вижу, скорее всего (если не лучше) удалить все вместе и вместо этого использовать функцию для получения значений на ходу. Или, если numba жалуется, встройте функцию. Операции над множествами всегда будут медленными, поэтому, если вы собираетесь повысить производительность, скорее всего, будет хорошей идеей найти способ избавиться от них.   -  person Shamis    schedule 20.01.2021
comment
@Shamis Я моделирую вариант этой en.wikipedia.org/wiki/Set_cover_problem размещения узлов на графике, где near_dict={id1: pos1, id2: pos2, id3: pos3} где каждый узел должен быть перемещен в положение в пределах его доступных позиций near_dict[pos], а не в пределах close_dict[pos]. Таким образом, узлы перемещаются в пределах некоторых ограничений, Close dict всегда в этой форме, а значения предварительно вычисляются, я не уверен, что было бы быстрее вычислять на ходу. Я не уверен, как покончить с наборами здесь   -  person William Grimes    schedule 20.01.2021
comment
это и это может иметь значение. В конце концов, я не думаю, что вы можете использовать numba, не жертвуя гибкостью и структурой python. Возможно, ваша проблема лучше подходит для другого языка. Также вопрос - какой обычный размер графика? Является ли граф большим или это просто вопрос количества узлов, которые вы хотите перемещать?   -  person Shamis    schedule 20.01.2021


Ответы (1)


Превращение в Нумбу:

Это было утомительно, так как Numba накладывает много ограничений. Конечно, было бы лучше опубликовать этот код пошагово, однако тогда ответ будет слишком длинным.
Короче говоря, чтобы преобразовать код в Numba и использовать его полную оптимизацию с помощью nopython, вам нужно следовать примерно этим правилам. .
Отказ от ответственности: я не эксперт по нумбе, и этот раздел основан только на наблюдениях:

  • Никаких объектов, кроме основных и пустых. Это означает отсутствие наборов и словарей.
  • Функции могут возвращать только одно значение, а не массив.
  • Все должно быть напечатано или достаточно просто, чтобы Numba автоматически определял тип.

Есть, вероятно, больше. Я не слишком уверен. Так что в основном, как следует из названия, если вы идете по маршруту nopython, забудьте, что вы находитесь в python. Либо код должен быть очень простым (как в нескольких циклах с простым вычислением), либо вам придется зарезать код до забвения. (Или есть какой-то другой трюк, который я еще не нашел.)
Редактировать: я нашел трюк - numba теперь поддерживает словарь даже в nojit, если вы заранее объявляете все типы. Он также поддерживает наборы. Однако он не может хранить наборы в словаре, поэтому в вашем случае вам придется сначала хранить наборы в массиве, а затем использовать словарь как своего рода таблицу перевода из id в индекс. Не невозможно, но определенно не просто.

Код, который скорее всего работает. Однако часть его устарела, так как я отказался от попыток избавиться от последнего [] в пользу массива numpy. Кроме того, он не оптимизирован, и я не измерял производительность.

import random
import numpy as np
from numba import jit
n_positions = 100
position_coverage = 10
min_dist = 5
n_ids = 5
near_keys = random.sample(range(n_positions), n_ids)


@jit(nopython=True)
    def simulate(near_keys):
    to_pos = -1
    _ixs_to = np.zeros(1)
    to_return = []
    for id in sorted(near_keys, key=lambda x: random.random()):
        if id < n_positions:
            near = np.arange(id, id + position_coverage)
        else:
            near = np.arange(id, n_positions)

        print(id, near)
        if to_pos == -1:
            _ixs_to = near
        else:
            to_close = []
            a = int(n_positions - position_coverage + min_dist)
            to_pos = int(to_pos)
        if to_pos < a:
            pos_values = np.arange(to_pos, to_pos + min_dist)
        else:
            pos_values = np.arange(to_pos, n_positions)

        for i in pos_values:
            if not i in to_close:
                to_close.append(i)

        _ixs_to = np.zeros(1, dtype=np.int64)
        for i in near:
            if not i in to_close:
                _ixs_to = np.append(_ixs_to, i)
        if _ixs_to[0] == 0:
            _ixs_to = _ixs_to[1:]

        if _ixs_to.ndim == 0:
            return [-1] # failed no reachable positions
        np.random.rand()
        to_pos = np.random.choice(_ixs_to)
        to_return.append(id)
        to_return.append(to_pos)
        return to_return

if __name__ == '__main__':
    print(simulate(near_keys))

Так что же случилось с вашим довольно красивым, хотя, может быть, излишне сложным кодом?

Во-первых, я заменил наборы генерирующими функциями:
near_dict = {x: set(range(x, x + position_coverage)) if x < n_positions - position_coverage else set(range(x, n_positions)) for x in range(n_positions)}
стал

def get_near(x):
    if x < n - position_coverage:
        return set(range(x, x + position_coverage))
    else:
        return set(range(x, n_positions)

Затем я переписал объединения и различия множеств так, чтобы они не требовались. Это было сделано путем перебора массивов и проверки значений для их удаления или добавления. Обратите внимание, что это та часть, где это, скорее всего, медленнее, если только операция объединения в чистом питоне не медленнее, чем неоптимизированное наивное объединение, которое я сделал. Если вы хотите оптимизировать код, это одно из мест, с которого можно начать.

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

Читая документы, я заметил еще одну вещь: Numba не позволяет создавать массивы в nopython и в таком случае возвращается к объектному режиму. Это означает, что вам нужно будет перерабатывать массивы вместо их воссоздания в коде выше, чтобы полностью использовать этот режим.

Последним шагом было сгладить все несоответствия типов и убедиться, что все работает вместе. В результате получается код, который Numba nopython хочет запускать, но, к сожалению, довольно запутанный и не использующий практически никаких удобств Python. Он также ужасно неоптимизирован. Однако, если вы хотите применить numba и впоследствии получить дешевое распараллеливание, которое оно обеспечивает, это, скорее всего, необходимо. Если кто-то, более знакомый с numba, поправит меня и укажет направление, более удобное для пользователя, я с радостью уберу этот ответ как вводящий в заблуждение.

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

Несколько советов:

  • Я бы сначала занялся профилированием и оптимизацией вашего кода. Легче профилировать код до того, как вы начнете делать параллельные махинации. Если этого окажется недостаточно, это самое время попробовать и:
  • посмотрите в многопроцессорную библиотеку python, если такого ускорения будет достаточно для ты. Если этого недостаточно, то:
  • попробуйте переписать свой уже оптимизированный код для numba и, наконец, если этого все еще недостаточно:
  • попробуйте C, Rust, C++, Java или что-то подобное. Вы можете быть удивлены.

Заключительное слово: Хотя я достаточно уверен в том, как заменил наборы функциями, которые затем встроил, я не совсем уверен, что все сделал правильно. Рекомендуется осторожность.

person Shamis    schedule 20.01.2021