2017-02-09 10 views
0

Numba가 해결할 수있는 문제가 있습니다. (a) 간단한 작업을 데이터를 단일 단계로 통합하여 # 1 핫 스폿 (메모리 대역폭) 및 (b) 타사 C 함수를 ufunc로 즉시 처리하여 쿼리 시스템 사용자에게 더 많은 기능을 제공 할 수 있습니다.Numba 함수에 대한 개체 코드 마샬링

쿼리를 분할하고 결과를 수집하고 Numpy를 실제로 실행하는 노드를 계산하는 축약 노드가 네트워크에 있습니다. Numba 컴파일이 계산 노드에서 발생하면 같은 쿼리에 대해 서로 다른 데이터 파티션에서 작업하므로 중복 된 노력이 필요합니다. 동일한 쿼리는 동일한 Numba 컴파일을 의미합니다. 또한 Numba 컴파일 중 가장 단순한 것조차도 96 밀리 초가 걸립니다. 수백만 포인트에 달하는 쿼리 계산을 실행하는 한 계산 노드에서 더 나은 시간을 제공합니다.

그래서 나는 축적 노드에서 한 번 Numba 편집 을 그들은 그것을 실행할 수 있도록 다음 가 컴퓨팅 노드에를 보내려고합니다. 둘 다 동일한 하드웨어를 가지고 있으므로 오브젝트 코드가 호환 가능하다는 것을 보증 할 수 있습니다.

이 기능에 대한 Numba API를 검색했지만 찾지 못했습니다 (문서가없는 numba.serialize 모듈 제외). 그 목적이 무엇인지 잘 모르겠습니다. 이 솔루션은 Numba 패키지의 "기능"이 아니라 Numba 및/또는 LLVM에 대한 내부자 지식을 활용하는 기술입니다. 누구든지 객체 코드에서 마샬링하고 재구성하는 방법을 알고 있습니까? 두 컴퓨터에 Numba를 설치하면 대상 컴퓨터에서 너무 비싼 작업을 수행 할 수 없습니다.

+0

numba 코드가 동일한 입력 유형으로 매번 동일한 프로세스에서 실행되는 경우 처음에는 jit 페널티 만 지불하면 시작할 때 수행하는 "워밍업"절차가있을 수 있으므로 첫 번째 쿼리에서 지불하십시오. 또는 Numba의 사전 컴파일 기능을 볼 수도 있습니다. 하지만 유스 케이스 제약을 완전히 이해하지 못하고있다. – JoshAdel

+0

또한 이것은 핵심 개발자로부터 응답을 얻을 수있는 Numba 전자 메일 목록에 더 적합한 질문 유형입니다. – JoshAdel

+0

코드는 모든 쿼리마다 다릅니다. Numba 목록에 대해서도 물어볼 것이지만 Numba 질문 만이 아니기 때문에 여기에서 묻습니다. 해결책은 Numba 자체가 아니라 LLVM을 직접 통과 할 수 있습니다. (비슷하게, Numpy보다는 ctypes를 사용하여 Numpy 문제를 해결했습니다. Numpy가 직접적으로 말했던 것보다 더 많은 LLVM 지식을 가진 사람의 도움을 받기를 바랍니다.) –

답변

0

좋아요, 해결책은 Numba에서 llvmlite 라이브러리를 많이 사용합니다.

직렬화 기능을 우리가 Numba 일부 함수를 정의

첫째을 얻기.

import numba 

@numba.jit("f8(f8)", nopython=True) 
def example(x): 
    return x + 1.1 

, 당신은 그것이 ELF 인코딩 된 바이트 배열 (bytes 객체가 아닌 str 당신이 만약이 있다고 볼 수는 elfbytes을 인쇄 할 경우 우리는

cres = example.overloads.values()[0] # 0: first and only type signature 
elfbytes = cres.library._compiled_object 

와 오브젝트 코드에 액세스 할 수 있습니다 파이썬 3에서). 이것은 공유 라이브러리 나 실행 파일을 컴파일 할 때 파일에 들어갈 수 있기 때문에 동일한 아키텍처, 동일한 라이브러리 등이있는 모든 컴퓨터에 이식 할 수 있습니다.

이 번들에는 몇 가지 기능이 있습니다.

print(cres.library.get_llvm_str()) 

우리가 __main__.example$1.float64라는 원하는 일을 우리는 그것 LLVM IR에 입력 한 서명을 볼 수 있습니다 다음 LLVM IR를 덤핑으로 볼 나중에 참조 할 수 있도록

define i32 @"__main__.example$1.float64"(double* noalias nocapture %retptr, { i8*, i32 }** noalias nocapture readnone %excinfo, i8* noalias nocapture readnone %env, double %arg.x) #0 { 
entry: 
    %.14 = fadd double %arg.x, 1.100000e+00 
    store double %.14, double* %retptr, align 8 
    ret i32 0 
} 

를 적어 둡니다을 : 첫 번째 인수가에 대한 포인터이 결과로 덮어 쓰게되면 두 번째와 세 번째 인수는 사용되지 않는 포인터가되고 마지막 인수는 double 입력이됩니다.

(프로그래밍 방식으로 [x.name for x in cres.library._final_module.functions]을 사용하여 함수 이름을 얻을 수 있으며 Numba가 실제로 사용하는 진입 점은 cres.fndesc.mangled_name입니다.)

모든 ELF 및 함수 서명을 모든 컴퓨팅을 수행하는 컴퓨터로 컴파일하는 모든 컴퓨터에서 전송합니다.

우리가 전혀 Numba과 llvmlite를 사용하는거야, 컴퓨팅 시스템에서 이제 다시

를 읽기 (this page 다음).

def object_compiled_hook(ll_module, buf): 
    pass 

def object_getbuffer_hook(ll_module): 
    return elfbytes 

engine.set_object_cache(object_compiled_hook, object_getbuffer_hook) 

는 같은 엔진을 마무리 : LLVM 실행 엔진 만들기

import llvmlite.binding as llvm 

llvm.initialize() 
llvm.initialize_native_target() 
llvm.initialize_native_asmprinter() # yes, even this one 

을 : 초기화

target = llvm.Target.from_default_triple() 
target_machine = target.create_target_machine() 
backing_mod = llvm.parse_assembly("") 
engine = llvm.create_mcjit_compiler(backing_mod, target_machine) 

을 그리고 지금은 elfbytes라는 우리의 ELF로드가 자사의 캐싱 메커니즘을 납치 우리는 IR을 컴파일했지만 실제로는 그 단계를 건너 뛰었습니다. 엔진은 디스크 기반 캐시에서 발생한다고 생각하여 ELF를로드합니다.

engine.finalize_object() 

이제이 엔진의 공간에서 우리의 기능을 발견 할 것입니다. 다음이 0L을 반환하면 잘못된 것이 있습니다. 함수 포인터 여야합니다.

func_ptr = engine.get_function_address("__main__.example$1.float64") 

이제는 func_ptr을 우리가 호출 할 수있는 ctypes 함수로 해석해야합니다. 수동으로 서명을 설정해야합니다.

import ctypes 
pdouble = ctypes.c_double * 1 
out = pdouble() 

pointerType = ctypes.POINTER(None) 
dummy1 = pointerType() 
dummy2 = pointerType() 

#      restype first then argtypes... 
cfunc = ctypes.CFUNCTYPE(ctypes.c_int32, pdouble, pointerType, pointerType, ctypes.c_double)(func_ptr) 

그리고 지금 우리는 그것을 호출 할 수

cfunc(out, dummy1, dummy2, ctypes.c_double(3.14)) 
print(out[0]) 
# 4.24, which is 3.14 + 1.1. Yay! 

더 많은 합병증

JITed 기능은 결국, 당신은 많은 값을 통해 꽉 루프를 수행 할 (배열 입력이있는 경우 파이썬이 아닌 컴파일 된 코드에서) Numba는 Numpy 배열을 인식하는 코드를 생성합니다. 예외 객체에 대한 포인터와 Numpy 배열에 별도의 매개 변수로 제공되는 모든 메타 데이터를 포함하여이 호출 규칙은 상당히 복잡합니다. 그것은 이 아니고은 Numpy의 ctypes 인터페이스에서 사용할 수있는 진입 점을 생성합니다.

그러나 매우 높은 수준의 진입 점은 Python *args, **kwds을 인수로 사용하여 내부적으로 파싱합니다. 사용 방법은 다음과 같습니다.

첫째, 누구의 이름으로 시작하는 기능을 찾아 "으로 CPython을."

name = [x.name for x in cres.library._final_module.functions if x.name.startswith("cpython.")][0] 

그들 중 정확히 하나가 있어야합니다.

func_ptr = engine.get_function_address(name) 

을 세 PyObject* 인수 한 PyObject* 반환 값으로 캐스팅 그런 다음, 직렬화 및 역 직렬화 후, 상기 한 방법을 사용하여 함수 포인터를 얻는다. (LLVM은 이것들이 i8*라고 생각합니다.)

class PyTypeObject(ctypes.Structure): 
    _fields_ = ("ob_refcnt", ctypes.c_int), ("ob_type", ctypes.c_void_p), ("ob_size", ctypes.c_int), ("tp_name", ctypes.c_char_p) 

class PyObject(ctypes.Structure): 
    _fields_ = ("ob_refcnt", ctypes.c_int), ("ob_type", ctypes.POINTER(PyTypeObject)) 

PyObjectPtr = ctypes.POINTER(PyObject) 

cpythonfcn = ctypes.CFUNCTYPE(PyObjectPtr, PyObjectPtr, PyObjectPtr, PyObjectPtr)(fcnptr) 

이 세 가지 인수의 첫 번째는 폐쇄 (함수가 액세스하는 전역 변수)이며, 나는 우리가 필요하지 않은 가정거야. 클로저 대신 명시 적 인수를 사용하십시오. 우리는 CPython의 id() 구현이 PyObject 포인터를 만들기 위해 포인터 값을 반환한다는 사실을 사용할 수 있습니다.

def wrapped(*args, **kwds): 
     closure =() 
     return cpythonfcn(ctypes.cast(id(closure), PyObjectPtr), ctypes.cast(id(args), PyObjectPtr), ctypes.cast(id(kwds), PyObjectPtr)) 

는 이제 기능은 원래 Numba 디스패처 기능과 같은

wrapped(whatever_numpy_arguments, ...) 

로 호출 할 수 있습니다.

모든 그 후 결론

, 그것은 그것의 가치가 있었다? Numba로 엔드 투 엔드 컴파일을하는 것은 쉬운 방법입니다.이 간단한 기능을 사용하려면 50ms가 걸립니다. 기본 -O2 대신 -O3을 요청하면이 속도를 40 % 느리게 할 수 있습니다.

미리 컴파일 된 ELF 파일에서 스 플라이 싱하는 데는 0.5ms가 걸리지 만 100 배 더 빠릅니다. 더욱 복잡한 함수에서는 컴파일 시간이 길어 지지만 splicing-in 절차는 모든 함수에 대해 항상 0.5ms가 걸립니다.

제 신청서의 경우 이는 절대적으로 가치가 있습니다. 즉, 한 번에 10MB의 계산을 수행 할 수 있고 컴파일 (작업 준비)보다는 컴퓨팅 (실제 작업)을하는 데 대부분의 시간을 소비 할 수 있습니다. 이것을 100 배로 늘리면 한 번에 1GB로 계산을 수행해야합니다. 한 머신이 100GB의 주문으로 제한되어 있으며 100 개의 주문 프로세스에서 공유되어야하므로 문제가 너무 세분화 될 수 있기 때문에 리소스 제한,로드 밸런싱 문제 등이 발생할 위험이 더 큽니다. .

그러나 다른 응용 프로그램의 경우 50ms는 아무것도 아닙니다. 모두 응용 프로그램에 따라 다릅니다.

+0

아, 궁극적으로 Numpy Ufuncs를 타겟팅하고 있습니다. 이것을'PyUFuncGenericFunction'에 랩핑하고'PyUFunc_FromFuncAndData'를 호출하여 이것을 ufunc로 만들 수 있습니다. 내 예제에서는 루프에 넣으면 ctypes 오버 헤드가 용납되지 않습니다. Numba로 컴파일 된 함수 안에 루프를 넣고 10MB의 데이터를 한 번만 호출하면됩니다. –