Как я могу скомпилировать функцию C в numpy ufunc и загрузить ее динамически? - PullRequest
0 голосов
/ 23 октября 2018

У меня есть некоторый код Python, который автоматически генерирует функцию C.Эта функция принимает в качестве входных данных несколько значений типа double и возвращает значение типа double, вызывая по пути различные функции из стандартной библиотеки C.

Одна из вещей, которую я хотел бы сделать с этим, - это скомпилировать ее в numpy ufunc и загрузитьэто в работающий процесс Python.Я просто хочу, чтобы функция запускалась поэлементно для своих входных массивов numpy, например, например, numpy minimum, с разумной скоростью.

Я был удивлен, что не смог найти четких инструкций или примеров, как это сделать.этот.У Numpy есть четкие инструкции по написанию расширений, но не ясно, как я могу загрузить их в текущий процесс Python.С помощью ctypes я могу скомпилировать свою функцию и загрузить ее, без проблем, но не ясно, как сделать ее функцией ufunc, а не обычной функцией Python.Cython также может сделать это, и если я использую pyximport, он даже создаст разделяемую библиотеку для меня, что идеально, потому что тогда я могу распространять ее, не беспокоясь о том, как создать код C в другой системе.Но опять же, не ясно, как сделать ufunc вместо обычной функции.

TL; DR: как я могу взять простую функцию C, скомпилировать ее в ufunc и загрузить ее динамически?Чем надежнее и меньше шаблонов, тем лучше.

1 Ответ

0 голосов
/ 24 октября 2018

Одной из идей может быть использование numba для создания ufuncs и cffi для компиляции c-кода.

Например, если мы хотим удвоить значениекаждый элемент в массиве numpy, т. е. имеющий следующую C-функцию в виде строки:

double f(double a){
    return 2.0*a;
}

, возможное решение - следующий прототип:

import numba as nb
import cffi

def create_ufunc(code):
    # 1. step: compile the C-code and load the resulting extension
    ffibuilder = cffi.FFI()
    ffibuilder.cdef("double f(double a);", override=True)
    built_module=ffibuilder.verify(source=code)
    fun = built_module.f

    # 2. step: create an ufunc out of the compiled C-function
    @nb.vectorize([nb.float64(nb.float64)])
    def f(x):
      return fun(x)
    return f

А теперь:

import numpy as np
a=np.arange(6).astype(np.float64)
my_f1=create_ufunc("double f(double a){return 2.0*a;}")
my_f1(a)
# array([  0.,   2.,   4.,   6.,   8.,  10.])

или, если мы хотим умножить на 10.0:

my_f2=create_ufunc("double f(double a){return 10.0*a;}")
# array([  0.,  10.,  20.,  30.,  40.,  50.])

Очевидно, показывая, что возможно, этот прототип нуждается в некоторой полировке.Например, хотя и компактный, verify устарел, и двойной вызов create_ufunc с одним и тем же кодом приведет к предупреждению.

Еще одна проблема: указанная выше версия не компилируется в режиме nopython, несмотря на то, что функции cffi поддерживаются numba .Не уверен, что здесь происходит не так?Ниже приведен обходной путь: более сложная версия, которая создается в режиме nopython.

Однако, это, вероятно, все еще хорошая отправная точка.


Кажется, что возможно скомпилироватьnumba в режиме nopython, если мы используем out-of-line (compile) вместо in-line (verify) API-режим:

import numba as nb
import cffi
import zlib
import importlib
import numba.cffi_support as nbcffi

def create_ufunc(code):
    # 1. step: compile the C-code and load the resulting extension
    # create a different so/dll for different codes
    # and load it
    module_name="myufunc"+str(zlib.adler32(code.encode('ascii')))
    ffibuilder = cffi.FFI()
    ffibuilder.cdef("double f(double a);", override=True)
    ffibuilder.set_source(module_name=module_name,source=code)
    ffibuilder.compile(verbose=True)
    loaded = importlib.import_module(module_name)


    # 2. step: create an ufunc out of the compiled C-function
    # out-of-line modules must be registered in numba:      
    nbcffi.register_module(loaded)
    fun = loaded.lib.f

    @nb.vectorize([nb.float64(nb.float64)], nopython=True)
    def f(x):
      return fun(x)
    return f

Важные сведения:

  • Для каждого code существует новое расширение (so / pyd-file).Мы различаем их по хеш-значению переданных code.
  • . Со временем будет довольно много myufuncXXXX.so -файлов, можно подумать о реализации инфраструктуры, аналогичной используемой * 1050.*.
  • ffibuilder.compile(verbose=True) только для целей отладки, вероятно, verbose=False имеет больше смысла в выпуске.
...