В numba я хочу передать переменную конфигурации в функцию как константу времени компиляции. В частности, то, что я хочу сделать, это
@njit
def physics(config):
flagA = config.flagA
flagB = config.flagB
aNumbaList = List()
for i in range(100):
if flagA:
aNumbaList.append(i)
else:
aNumbaList.append(i/10)
return aNumbaList
Если переменные конфигурации являются константами времени компиляции, это прошло бы, но это не так, поэтому выдает мне ошибки, говорящие, что есть два кандидата.
There are 2 candidate implementations:
- Of which 2 did not match due to:
...
...
Я просмотрел протокол одного из собраний Numba и обнаружил, что есть способ сделать это Встреча Numba: 05.03.2024 Я попробовал это, но все равно выдает ту же ошибку.
Вот код с сообщением об ошибке:
.. code:: ipython3
from numba import jit, types, njit
from numba.extending import overload
from numba.typed import List
import functools
.. code:: ipython3
class Config():
def __init__(self, flagA, flagB):
self._flagA = flagA
self._flagB = flagB
@property
def flagA(self):
return self._flagA
@property
def flagB(self):
return self._flagB
.. code:: ipython3
@functools.cache
def obj2strkeydict(obj, config_name):
# unpack object to freevars and close over them
tmp_a = obj.flagA
tmp_b = obj.flagB
assert isinstance(config_name, str)
tmp_force_heterogeneous = config_name
@njit
def configurator():
d = {'flagA': tmp_a,
'flagB': tmp_b,
'config_name': tmp_force_heterogeneous}
return d
# return a configuration function that returns a string-key-dict
# representation of the configuration object.
return configurator
.. code:: ipython3
@njit
def physics(cfig_func):
config = cfig_func()
flagA = config['flagA']
flagB = config['flagB']
aNumbaList = List()
for i in range(100):
if flagA:
aNumbaList.append(i)
else:
aNumbaList.append(i/10)
return aNumbaList
.. code:: ipython3
def demo():
configuration1 = Config(True, False)
jit_config1 = obj2strkeydict(configuration1, 'config1')
physics(jit_config1)
.. code:: ipython3
demo()
::
---------------------------------------------------------------------------
TypingError Traceback (most recent call last)
Cell In[83], line 1
----> 1 demo()
Cell In[82], line 4, in demo()
2 configuration1 = Config(True, False)
3 jit_config1 = obj2strkeydict(configuration1, 'config1')
----> 4 physics(jit_config1)
File ~/anaconda3/envs/tardis/lib/python3.11/site-packages/numba/core/dispatcher.py:468, in _DispatcherBase._compile_for_args(self, *args, **kws)
464 msg = (f"{str(e).rstrip()} \n\nThis error may have been caused "
465 f"by the following argument(s):\n{args_str}\n")
466 e.patch_message(msg)
--> 468 error_rewrite(e, 'typing')
469 except errors.UnsupportedError as e:
470 # Something unsupported is present in the user code, add help info
471 error_rewrite(e, 'unsupported_error')
File ~/anaconda3/envs/tardis/lib/python3.11/site-packages/numba/core/dispatcher.py:409, in _DispatcherBase._compile_for_args.<locals>.error_rewrite(e, issue_type)
407 raise e
408 else:
--> 409 raise e.with_traceback(None)
TypingError: Failed in nopython mode pipeline (step: nopython frontend)
- Resolution failure for literal arguments:
No implementation of function Function(<function impl_append at 0x7fd87d253920>) found for signature:
>>> impl_append(ListType[int64], float64)
There are 2 candidate implementations:
- Of which 2 did not match due to:
Overload in function 'impl_append': File: numba/typed/listobject.py: Line 592.
With argument(s): '(ListType[int64], float64)':
Rejected as the implementation raised a specific error:
TypingError: Failed in nopython mode pipeline (step: nopython frontend)
No implementation of function Function(<intrinsic _cast>) found for signature:
>>> _cast(float64, class(int64))
There are 2 candidate implementations:
- Of which 2 did not match due to:
Intrinsic in function '_cast': File: numba/typed/typedobjectutils.py: Line 22.
With argument(s): '(float64, class(int64))':
Rejected as the implementation raised a specific error:
TypingError: cannot safely cast float64 to int64. Please cast explicitly.
raised from /home/sam/anaconda3/envs/tardis/lib/python3.11/site-packages/numba/typed/typedobjectutils.py:75
During: resolving callee type: Function(<intrinsic _cast>)
During: typing of call at /home/sam/anaconda3/envs/tardis/lib/python3.11/site-packages/numba/typed/listobject.py (600)
File "../anaconda3/envs/tardis/lib/python3.11/site-packages/numba/typed/listobject.py", line 600:
def impl(l, item):
casteditem = _cast(item, itemty)
^
raised from /home/sam/anaconda3/envs/tardis/lib/python3.11/site-packages/numba/core/typeinfer.py:1086
- Resolution failure for non-literal arguments:
None
During: resolving callee type: BoundFunction((<class 'numba.core.types.containers.ListType'>, 'append') for ListType[int64])
During: typing of call at /tmp/ipykernel_9889/739598600.py (11)
File "../../../tmp/ipykernel_9889/739598600.py", line 11:
<source missing, REPL/exec in use?>
Любая помощь или любая ссылка на соответствующий материал действительно мне помогут. Спасибо.
В Numba глобальные переменные являются константами времени компиляции, поэтому вы можете использовать их по своему усмотрению. Вот пример:
import numba as nb # v0.58.1
flagA = True
@nb.njit
def physics(flagA):
aNumbaList = nb.typed.List()
for i in range(100):
if flagA:
aNumbaList.append(i)
else:
aNumbaList.append(i/10)
return aNumbaList
Это работает без ошибок, а передача flagA
в параметре приводит к ошибке, поскольку элементы в if
и else
относятся к разным типам.
При этом глобальные переменные не очень хороши с точки зрения разработки программного обеспечения, и вы можете захотеть скомпилировать функцию для другой конфигурации во время выполнения (например, на основе процесса инициализации, избегая при этом записи в глобальные переменные).
Альтернативное решение — вернуть функцию, которая считывает переменную, определенную в родительской функции, чтобы она считалась глобальной для функции и, следовательно, константой времени компиляции. Переменная, прочитанная скомпилированной функцией, может быть передана в качестве параметра родительской функции. Вот пример:
import numba as nb
def make_physics(flagA):
@nb.njit
def fun():
aNumbaList = nb.typed.List()
for i in range(100):
if flagA:
aNumbaList.append(i)
else:
aNumbaList.append(i/10)
return aNumbaList
return fun
physics = make_physics(True) # Compile a specialized function every time it is called
physics() # Call the compiled function generated just before
Это также не приводит к какой-либо ошибке и фактически работает так, как задумано. Вот сгенерированный ассемблерный код функции physics
, показывающий, что проверка flagA
во время выполнения в основном цикле отсутствует:
[...]
movq %rax, %r12 ; r12 = an allocated Python object (the list?)
movq 24(%rax), %rax
movq %r14, (%rax)
xorl %ebx, %ebx ; i = 0
movabsq $NRT_incref, %r13
movabsq $numba_list_append, %rbp
leaq 48(%rsp), %r15 ; (r15 is a pointer on i)
.LBB0_6: ; Main loop
movq %r12, %rcx
callq *%r13 ; Call NRT_incref(r12)
movq %rbx, 48(%rsp)
movq %r14, %rcx
movq %r15, %rdx
callq *%rbp ; Call numba_list_append(r14, pointer_of(i))
testl %eax, %eax
jne .LBB0_7 ; Stop the loop if numba_list_append returned a non-zero value
incq %rbx ; i += 1
movq %r12, %rcx
movabsq $NRT_decref, %rax
callq *%rax ; Call NRT_decref(r12)
cmpq $100, %rbx
jne .LBB0_6 ; Loop as long as i < 100
[...]
Что касается реального варианта использования, мемоизация и кэширование функций Numba могут помочь избежать многократной компиляции целевой функции для одной и той же конфигурации.
Спасибо за подробный ответ, Джером. Итак, я попробовал подход с использованием глобальных переменных, и вот в чем проблема: пока ваша глобальная переменная одинакова во всей программе, она работает нормально, но если значение будет изменено в середине выполнения, программа будет использовать то же глобальное значение, что и раньше, и не обновлять его. Вам придется перекомпилировать его, чего Numba не рекомендует. Я попробую метод закрытия и вернусь к вам.
Я рассматриваю возможность использования перегрузки или сгенерированного_jit для возврата специализированных функций на основе значения конфигурации. Вы порекомендуете это? Кроме того, есть ли что-то, на что мне следует обратить внимание при их использовании, чтобы это не влияло на время выполнения программы?
generated_jit
AFAIK устарел и теперь заменен на overload
. Дело в том, что AFAIK overload
ограничен перегрузкой на основе типов, тогда как вам, похоже, нужна перегрузка на основе значений. Поэтому я думаю, что ни один из двух не подходит для вашего случая использования. Кроме того, поведение generated_jit
похоже на решение, представленное в ответе, за исключением того, что функция не всегда перекомпилируется при каждом вызове make_physics
(я думаю, что она похожа на нее после добавления запоминания).
Я согласен насчет глобальных переменных, но в данном случае решение make_physics
подходит. Вы можете вызывать его с другой конфигурацией и генерировать разные функции. На основе этого вы можете написать свою собственную перегрузку CPython. Мемоизацию можно легко поддержать с помощью @functools.lru_cache(maxsize=64)
(кэшировать до 64 конфигураций — используйте None
без привязки за счет, возможно, большого количества скомпилированных функций в памяти). Вы также можете предварительно скомпилировать все конфигурации вручную при запуске программы и в свою собственную систему кеширования (что невозможно с generated_jit
).
Обратите внимание, что функцию необходимо перекомпилировать столько раз, сколько существует конфигурация, поскольку вы хотите, чтобы она была константой времени компиляции.
Да. Если посмотреть на кодовую базу, сгенерированный_jit был удален начиная с Numba 0.59.0. Что касается использования перегрузки, один из способов заставить ее работать — передать переменные конфигурации как литералы Numba. Я могу использовать свойство .literal_value. Но я думаю, что решения make_physical кажутся простыми. Спасибо за вашу помощь.
Вам следует создать один файл .py и протестировать его, а не фрагменты кода ipython (jupyter?). Если вы это сделаете, вы должны получить дополнительное сообщение об ошибке, которое будет гораздо более понятным.