【问题标题】:Numba Compatable MemoizationNumba 兼容记忆
【发布时间】:2020-07-27 02:05:25
【问题描述】:

我刚刚发现了 numba,并了解到最佳性能需要在大多数函数中添加@njit,这样 numba 很少退出 LLVM 模式。

我仍然有一些可以从记忆中受益的昂贵/查找功能,但到目前为止,我的尝试都没有找到一个可以无错误编译的可行解决方案。

  • @njit 之前使用常见的装饰器函数会导致 numba 无法进行类型推断。
  • @njit 之后使用装饰器无法编译装饰器
  • Numba 不喜欢使用 global 变量,即使在使用 numba.typed.Dict 时也是如此
  • Numba 不喜欢使用闭包来存储可变状态
  • 从其他 @njit 函数调用时,删除 @njit 也会导致类型错误

在 numba 中工作时向函数添加 memoization 的正确方法是什么?

import functools
import time

import fastcache
import numba
import numpy as np
import toolz
from numba import njit

from functools import lru_cache
from fastcache import clru_cache
from toolz import memoize



# @fastcache.clru_cache(None)  # BUG: Untyped global name 'expensive': cannot determine Numba type of <class 'fastcache.clru_cache'>
# @functools.lru_cache(None)   # BUG: Untyped global name 'expensive': cannot determine Numba type of <class 'functools._lru_cache_wrapper'>
# @toolz.memoize               # BUG: Untyped global name 'expensive': cannot determine Numba type of <class 'function'>
@njit
# @fastcache.clru_cache(None)  # BUG: AttributeError: 'fastcache.clru_cache' object has no attribute '__defaults__'
# @functools.lru_cache(None)   # BUG: AttributeError: 'functools._lru_cache_wrapper' object has no attribute '__defaults__'
# @toolz.memoize               # BUG: CALL_FUNCTION_EX with **kwargs not supported
def expensive():
    bitmasks = np.array([ 1 << n for n in range(0, 64) ], dtype=np.uint64)
    return bitmasks



# @fastcache.clru_cache(None)  # BUG: Untyped global name 'expensive_nojit': cannot determine Numba type of <class 'fastcache.clru_cache'>
# @functools.lru_cache(None)   # BUG: Untyped global name 'expensive_nojit': cannot determine Numba type of <class 'fastcache.clru_cache'>
# @toolz.memoize               # BUG: Untyped global name 'expensive_nojit': cannot determine Numba type of <class 'function'>
def expensive_nojit():
    bitmasks = np.array([ 1 << n for n in range(0, 64) ], dtype=np.uint64)
    return bitmasks


# BUG: Failed in nopython mode pipeline (step: analyzing bytecode)
#      Use of unsupported opcode (STORE_GLOBAL) found
_expensive_cache = None
@njit
def expensive_global():
    global _expensive_cache
    if _expensive_cache is None:
        bitmasks = np.array([ 1 << n for n in range(0, 64) ], dtype=np.uint64)
        _expensive_cache = bitmasks
    return _expensive_cache


# BUG: The use of a DictType[unicode_type,array(int64, 1d, A)] type, assigned to variable 'cache' in globals,
#      is not supported as globals are considered compile-time constants and there is no known way to compile
#      a DictType[unicode_type,array(int64, 1d, A)] type as a constant.
cache = numba.typed.Dict.empty(
    key_type   = numba.types.string,
    value_type = numba.uint64[:]
)
@njit
def expensive_cache():
    global cache
    if "expensive" not in cache:
        bitmasks = np.array([ 1 << n for n in range(0, 64) ], dtype=np.uint64)
        cache["expensive"] = bitmasks
    return cache["expensive"]


# BUG: Cannot capture the non-constant value associated with variable 'cache' in a function that will escape.
@njit()
def _expensive_wrapped():
    cache = []
    def wrapper(bitmasks):
        if len(cache) is None:
            bitmasks = np.array([ 1 << n for n in range(0, 64) ], dtype=np.uint64)
            cache.append(bitmasks)
        return cache[0]
    return wrapper
expensive_wrapped = _expensive_wrapped()

@njit
def loop(count):
    for n in range(count):
        expensive()
        # expensive_nojit()
        # expensive_cache()
        # expensive_global)
        # expensive_wrapped()

def main():
    time_start = time.perf_counter()

    count = 10000
    loop(count)

    time_taken = time.perf_counter() - time_start
    print(f'{count} loops in {time_taken:.4f}s')


loop(1)  # precache numba
main()

# Pure Python: 10000 loops in 0.2895s
# Numba @njit: 10000 loops in 0.0026s

【问题讨论】:

  • 这是你真实的例子吗?在需要时通过一些优化计算昂贵的函数实际上比内存查找更快(摆脱列表理解并将预分配的内存块传递给函数)。在 Numba 中使用真正的全局变量很复杂,需要变通方法。例如,您可以将内存地址(作为 int64)传递给已编译的函数并使用它。 (需要将 int64 内存地址转换为指针的低级可调用对象)。例如。 stackoverflow.com/a/61550054/4045774
  • 感谢您提供链接和使用指针的想法。我的真实代码稍微复杂一些(Kaggle Connect 4 Competition),所以我尝试将 numba 简化为这个问题的极简 POC 测试用例。

标签: python numba memoization


【解决方案1】:

您已经提到您的真实代码更复杂,但看看您的最小示例,我建议使用以下模式:

@njit
def loop(count):
    expensive_result = expensive()
    for i in range(count):
        do_something(count, expensive_result)

您可以在循环之外预先计算它并将结果提供给循环体,而不是使用缓存。我建议您显式传递每个参数,而不是使用全局变量(总是,但尤其是在使用 numba jit 时)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-04-10
    • 2013-12-04
    相关资源
    最近更新 更多