原文: http://numba.pydata.org/numba-doc/latest/user/troubleshoot.html
一般建议您应该只尝试编译代码中的关键路径。如果在某些更高级别的代码中有一段性能关键的计算代码,则可以在单独的函数中分解性能关键代码,并使用 Numba 编译单独的函数。让 Numba 专注于那一小部分性能关键代码有几个优点:
- 它降低了击中不支持功能的风险;
- 它减少了编译时间;
- 它允许您更容易地演化编译函数之外的更高级代码。
Numba 无法编译代码可能有多种原因,并引发错误。一个常见原因是您的代码依赖于不受支持的 Python 功能,尤其是在 nopython 模式中。请参阅支持的 Python 功能列表。如果您发现其中列出的内容仍然无法编译,请报告错误。
当 Numba 尝试编译代码时,它首先尝试计算出所有正在使用的变量的类型,这样就可以生成代码的特定类型实现,可以编译成机器代码。 Numba 无法编译的常见原因(特别是在 nopython 模式中)是类型推断失败,本质上 Numba 无法确定代码中所有变量的类型应该是什么。
例如,让我们考虑一下这个简单的函数:
@jit(nopython=True)
def f(x, y):
return x + y
如果你用两个数字来称呼它,Numba 能够正确地推断出这些类型:
>>> f(1, 2)
3
但是如果用一个元组和一个数字来调用它,Numba 无法说出添加元组和数字的结果是什么,因此编译错误:
>>> f(1, (2,))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<path>/numba/numba/dispatcher.py", line 339, in _compile_for_args
reraise(type(e), e, None)
File "<path>/numba/numba/six.py", line 658, in reraise
raise value.with_traceback(tb)
numba.errors.TypingError: Failed at nopython (nopython frontend)
Invalid use of + with parameters (int64, tuple(int64 x 1))
Known signatures:
* (int64, int64) -> int64
* (int64, uint64) -> int64
* (uint64, int64) -> int64
* (uint64, uint64) -> uint64
* (float32, float32) -> float32
* (float64, float64) -> float64
* (complex64, complex64) -> complex64
* (complex128, complex128) -> complex128
* (uint16,) -> uint64
* (uint8,) -> uint64
* (uint64,) -> uint64
* (uint32,) -> uint64
* (int16,) -> int64
* (int64,) -> int64
* (int8,) -> int64
* (int32,) -> int64
* (float32,) -> float32
* (float64,) -> float64
* (complex64,) -> complex64
* (complex128,) -> complex128
* parameterized
[1] During: typing of intrinsic-call at <stdin> (3)
File "<stdin>", line 3:
错误消息可以帮助您找出出错的地方:“无效使用+与参数(int64,tuple(int64 x 1))”将被解释为“Numba 遇到了一个类型为整数和 1 元组整数的变量的添加分别,并且不知道任何此类操作“。
请注意,如果您允许对象模式:
@jit
def g(x, y):
return x + y
编译将成功,编译的函数将在 Python 运行时提升:
>>> g(1, (2,))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'tuple'
Numba 无法编译代码的另一个常见原因是它无法静态地确定函数的返回类型。最可能的原因是返回类型取决于仅在运行时可用的值。同样,当使用 nopython 模式时,这通常是有问题的。类型统一的概念只是试图找到一种可以安全地表示两个变量的类型。例如,64 位浮点数和 64 位复数可以用 128 位复数表示。
作为类型统一失败的示例,此函数具有在运行时根据 <cite>x</cite> 的值确定的返回类型:
In [1]: from numba import jit
In [2]: @jit(nopython=True)
...: def f(x):
...: if x > 10:
...: return (1,)
...: else:
...: return 1
...:
In [3]: f(10)
尝试执行此功能,错误如下:
TypingError: Failed at nopython (nopython frontend)
Can't unify return type from the following types: tuple(int64 x 1), int64
Return of: IR name '$8.2', type '(int64 x 1)', location:
File "<ipython-input-2-51ef1cc64bea>", line 4:
def f(x):
<source elided>
if x > 10:
return (1,)
^
Return of: IR name '$12.2', type 'int64', location:
File "<ipython-input-2-51ef1cc64bea>", line 6:
def f(x):
<source elided>
else:
return 1
错误消息“无法统一以下类型的返回类型:tuple(int64 x 1),int64”应该读作“Numba 找不到可以安全地表示整数和整数的 1 元组的类型”。
正如之前提到的,Numba 编译代码的第一部分涉及计算所有变量的类型。对于列表,列表必须包含相同类型的项目,或者如果可以从稍后的某个操作推断出类型,则列表可以为空。不可能的是有一个被定义为空并且没有可推断类型的列表(即无类型列表)。
例如,这是使用已知类型的列表:
from numba import jit
@jit(nopython=True)
def f():
return [1, 2, 3] # this list is defined on construction with `int` type
这是使用空列表,但可以推断出类型:
from numba import jit
@jit(nopython=True)
def f(x):
tmp = [] # defined empty
for i in range(x):
tmp.append(i) # list type can be inferred from the type of `i`
return tmp
这是使用空列表,无法推断类型:
from numba import jit
@jit(nopython=True)
def f(x):
tmp = [] # defined empty
return (tmp, x) # ERROR: the type of `tmp` is unknown
虽然有点做作,如果你需要一个空列表并且无法推断出类型,但是你知道列表是什么类型,这个“技巧”可以用来指示打字机制:
from numba import jit
import numpy as np
@jit(nopython=True)
def f(x):
# define empty list, but instruct that the type is np.complex64
tmp = [np.complex64(x) for x in range(0)]
return (tmp, x) # the type of `tmp` is known, but it is still empty
编译 JIT 函数缓慢的最常见原因是 nopython 模式中的编译失败并且 Numba 编译器已回落到对象模式。与常规 Python 解释相比,对象模式目前几乎没有提供加速,其主要观点是允许内部优化称为循环提升:此优化将允许编译内部循环在 nopython 模式中,无论这些内部循环包围什么代码。
要确定函数的类型推断是否成功,可以在编译函数上使用 inspect_types()
方法。
例如,让我们采取以下功能:
@jit
def f(a, b):
s = a + float(b)
return s
当使用数字调用时,此函数应该很快,因为 Numba 能够将数字类型转换为浮点数。让我们来看看:
>>> f(1, 2)
3.0
>>> f.inspect_types()
f (int64, int64)
--------------------------------------------------------------------------------
# --- LINE 7 ---
@jit
# --- LINE 8 ---
def f(a, b):
# --- LINE 9 ---
# label 0
# a.1 = a :: int64
# del a
# b.1 = b :: int64
# del b
# $0.2 = global(float: <class 'float'>) :: Function(<class 'float'>)
# $0.4 = call $0.2(b.1, ) :: (int64,) -> float64
# del b.1
# del $0.2
# $0.5 = a.1 + $0.4 :: float64
# del a.1
# del $0.4
# s = $0.5 :: float64
# del $0.5
s = a + float(b)
# --- LINE 10 ---
# $0.7 = cast(value=s) :: float64
# del s
# return $0.7
return s
在没有尝试理解太多 Numba 中间表示的情况下,仍然可以看到所有变量和临时值都已正确推断其类型:例如 a 具有int64
类型, $ 0.5 的类型有float64
等。
但是,如果 b 作为字符串传递,编译将回退到对象模式,因为 Numba 当前不支持带有字符串的 float()构造函数:
>>> f(1, "2")
3.0
>>> f.inspect_types()
[... snip annotations for other signatures, see above ...]
================================================================================
f (int64, str)
--------------------------------------------------------------------------------
# --- LINE 7 ---
@jit
# --- LINE 8 ---
def f(a, b):
# --- LINE 9 ---
# label 0
# a.1 = a :: pyobject
# del a
# b.1 = b :: pyobject
# del b
# $0.2 = global(float: <class 'float'>) :: pyobject
# $0.4 = call $0.2(b.1, ) :: pyobject
# del b.1
# del $0.2
# $0.5 = a.1 + $0.4 :: pyobject
# del a.1
# del $0.4
# s = $0.5 :: pyobject
# del $0.5
s = a + float(b)
# --- LINE 10 ---
# $0.7 = cast(value=s) :: pyobject
# del s
# return $0.7
return s
在这里,我们看到所有变量最终都被输入为pyobject
。这意味着该函数是在对象模式下编译的,并且值作为通用 Python 对象传递,而 Numba 没有尝试查看它们以推断其原始值。这是您在关心代码速度时要避免的情况。
有几种方法可以理解函数在 nopython 模式下无法编译的原因:
-
传递 nopython = True ,这将引发一个错误,指出出现了什么问题(见上文我的代码不编译);
-
通过设置
NUMBA_WARNINGS
环境变量启用警告;例如,上面的f()
功能:>>> f(1, 2) 3.0 >>> f(1, "2") example.py:7: NumbaWarning: Function "f" failed type inference: Internal error at <numba.typeinfer.CallConstrain object at 0x7f6b8dd24550>: float() only support for numbers File "example.py", line 9 @jit example.py:7: NumbaWarning: Function "f" was compiled in object mode without forceobj=True. @jit 3.0
为了调试代码,可以禁用 JIT 编译,这使得jit
装饰器(以及装饰器njit
和autojit
)就像它们不执行任何操作一样,并且装饰函数的调用调用原始 Python 函数而不是编译版本。可以通过将 NUMBA_DISABLE_JIT
环境变量设置为1
来切换。
启用此模式时,vectorize
和guvectorize
装饰器仍将导致 ufunc 的编译,因为这些函数没有直接的纯 Python 实现。
在jit
装饰器中设置debug
关键字参数(例如@jit(debug=True)
)可以在 jitted 代码中发出调试信息。要进行调试,需要 GDB 7.0 或更高版本。目前,可以使用以下调试信息:
- 函数名称将显示在回溯中。但是,没有类型信息。
- 源位置(文件名和行号)可用。例如,用户可以通过绝对文件名和行号设置断点;例如
break /path/to/myfile.py:6
。 - 当前函数中的局部变量可以用
info locals
显示。 - 带
whatis myvar
的变量类型。 - 变量值
print myvar
或display myvar
。- 简单的数字类型,即 int,float 和 double,以其原生表示形式显示。但是,假设整数被签名。
- 其他类型显示为字节序列。
已知的问题:
- 步进很大程度上取决于优化级别。
- 在完全优化(相当于 O3)时,大多数变量都被优化掉了。
- 没有优化(例如
NUMBA_OPT=0
),当单步执行代码时,源位置会跳转。 - 在 O1 优化(例如
NUMBA_OPT=1
),步进稳定但一些变量被优化。
- 启用调试信息后,内存消耗会显着增加。编译器发出额外信息( DWARF )以及指令。使用调试信息,发出的目标代码可以大 2 倍。
内部细节:
-
由于 Python 语义允许变量绑定到不同类型的值,因此 Numba 在内部为每种类型创建变量的多个版本。所以代码如下:
x = 1 # type int x = 2.3 # type float x = (1, 2, 3) # type 3-tuple of int
每个分配将存储到不同的变量名称。在调试器中,变量将是
x
,x$1
和x$2
。 (在 Numba IR 中,它们是x
,x.1
和x.2
。) -
启用调试时,将禁用内联函数。
python 源码:
|
1
2
3
4
5
6
7
8
9
10
11
|
from numba import njit
@njit(debug=True)
def foo(a):
b = a + 1
c = a * 2.34
d = (a, b, c)
print(a, b, c, d)
r= foo(123)
print(r)
|
在终端:
$ NUMBA_OPT=1 gdb -q python
Reading symbols from python...done.
(gdb) break /home/user/chk_debug.py:5
No source file named /home/user/chk_debug.py.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (/home/user/chk_debug.py:5) pending.
(gdb) run chk_debug.py
Starting program: /home/user/miniconda/bin/python chk_debug.py
...
Breakpoint 1, __main__::foo$241(long long) () at chk_debug.py:5
5 b = a + 1
(gdb) n
6 c = a * 2.34
(gdb) bt
#0 __main__::foo$241(long long) () at chk_debug.py:6
#1 0x00007ffff7fec47c in cpython::__main__::foo$241(long long) ()
#2 0x00007fffeb7976e2 in call_cfunc (locals=0x0, kws=0x0, args=0x7fffeb486198,
...
(gdb) info locals
a = 0
d = <error reading variable d (DWARF-2 expression error: `DW_OP_stack_value' operations must be used either alone or in conjunction with DW_OP_piece or DW_OP_bit_piece.)>
c = 0
b = 124
(gdb) whatis b
type = i64
(gdb) whatis d
type = {i64, i64, double}
(gdb) print b
$2 = 124
通过设置环境变量NUMBA_DEBUGINFO=1
,可以为整个应用程序启用调试。这将设置jit
中debug
选项的默认值。通过设置debug=False
可以关闭各个功能的调试。
请注意,启用调试信息会显着增加每个已编译函数的内存消耗。对于大型应用程序,这可能会导致内存不足错误。
Numba(版本 0.42.0 及更高版本)具有一些与gdb
支持 CPU 相关的附加功能,可以更轻松地调试程序。以下描述的所有gdb
相关函数以相同的方式工作,而不管它们是从标准 CPython 解释器调用还是以 nopython 模式或对象模式编译的代码。
注意
这个功能是实验性的!
警告
如果从 Jupyter 或pdb
模块旁边使用此功能会发生意外情况。它的行为是无害的,很难预测!
Numba 的gdb
相关函数使用gdb
二进制,如果需要,可以通过 NUMBA_GDB_BINARY
环境变量配置该二进制文件的位置和名称。
注意
Numba 的gdb
支持要求gdb
能够附加到另一个进程。在某些系统(特别是 Ubuntu Linux)上,ptrace
上的默认安全限制阻止了这种情况。 Linux 安全模块 <cite>Yama</cite> 在系统级强制执行此限制。有关此模块的文档以及更改其行为的安全隐患,请参见 Linux 内核文档。 Ubuntu Linux 安全文档讨论了如何根据ptrace_scope
调整 <cite>Yama</cite> 的行为,以便允许所需的行为。
警告
不建议在同一程序中多次调用numba.gdb()
和/或numba.gdb_init()
,可能会发生意外情况。如果程序中需要多个断点,则通过numba.gdb()
或numba.gdb_init()
启动gdb
一次,然后使用numba.gdb_breakpoint()
注册其他断点位置。
添加gdb
支持的最简单功能是numba.gdb()
,它在呼叫位置将:
- 启动
gdb
并将其附加到正在运行的进程。 - 在
numba.gdb()
函数调用的站点创建断点,附加的gdb
将在此处暂停执行,等待用户输入。
使用此功能是最好的例子,继续上面使用的示例:
|
1
2
3
4
5
6
7
8
9
10
11
12
|
from numba import njit, gdb
@njit(debug=True)
def foo(a):
b = a + 1
gdb() # instruct Numba to attach gdb at this location and pause execution
c = a * 2.34
d = (a, b, c)
print(a, b, c, d)
r= foo(123)
print(r)
|
在终端中(线路上的...
本身表示为简洁起见未显示的输出):
$ NUMBA_OPT=0 python demo_gdb.py
Attaching to PID: 27157
GNU gdb (GDB) Red Hat Enterprise Linux 8.0.1-36.el7
...
Attaching to process 27157
...
Reading symbols from <elided for brevity> ...done.
0x00007f0380c31550 in __nanosleep_nocancel () at ../sysdeps/unix/syscall-template.S:81
81 T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
Breakpoint 1 at 0x7f036ac388f0: file numba/_helperlib.c, line 1090.
Continuing.
Breakpoint 1, numba_gdb_breakpoint () at numba/_helperlib.c:1090
1090 }
(gdb) s
Single stepping until exit from function _ZN5numba7targets8gdb_hook8hook_gdb12$3clocals$3e8impl$242E5Tuple,
which has no line number information.
__main__::foo$241(long long) () at demo_gdb.py:7
7 c = a * 2.34
(gdb) l
2
3 @njit(debug=True)
4 def foo(a):
5 b = a + 1
6 gdb() # instruct Numba to attach gdb at this location and pause execution
7 c = a * 2.34
8 d = (a, b, c)
9 print(a, b, c, d)
10
11 r= foo(123)
(gdb) p a
$1 = 123
(gdb) p b
$2 = 124
(gdb) p c
$3 = 0
(gdb) n
8 d = (a, b, c)
(gdb) p c
$4 = 287.81999999999999
在上面的例子中可以看出,代码的执行在numba_gdb_breakpoint
函数结束时gdb()
函数调用的位置暂停(这是用gdb
注册为断点的 Numba 内部符号)。此时发出step
会移动到已编译的 Python 源的堆栈帧。从那里可以看出,已经评估了变量a
和b
,但c
没有,正如通过打印它们的值所证明的那样,这正如gdb()
调用的位置所预期的那样。发出next
然后评估线7
和c
被赋予一个值,如最终印刷品所示。
numba.gdb()
提供的功能(启动并将gdb
连接到执行进程并在断点处暂停)也可作为两个单独的功能使用:
numba.gdb_init()
此函数在调用站点注入代码以启动并将gdb
附加到执行进程,但不会暂停执行。numba.gdb_breakpoint()
此函数在调用站点注入代码,该代码将调用在 Numbagdb
支持中注册为断点的特殊numba_gdb_breakpoint
函数。这将在下一节中演示。
此功能可实现更复杂的调试功能。再次,以示例为动机,调试'segfault'(内存访问违规信令SIGSEGV
):
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
from numba import njit, gdb_init
import numpy as np
@njit(debug=True)
def foo(a, index):
gdb_init() # instruct Numba to attach gdb at this location, but not to pause execution
b = a + 1
c = a * 2.34
d = c[index] # access an address that is a) invalid b) out of the page
print(a, b, c, d)
bad_index = int(1e9) # this index is invalid
z = np.arange(10)
r = foo(z, bad_index)
print(r)
|
在终端中(线路上的...
本身表示为简洁起见未显示的输出):
$ python demo_gdb_segfault.py
Attaching to PID: 5444
GNU gdb (GDB) Red Hat Enterprise Linux 8.0.1-36.el7
...
Attaching to process 5444
...
Reading symbols from <elided for brevity> ...done.
0x00007f8d8010a550 in __nanosleep_nocancel () at ../sysdeps/unix/syscall-template.S:81
81 T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
Breakpoint 1 at 0x7f8d6a1118f0: file numba/_helperlib.c, line 1090.
Continuing.
0x00007fa7b810a41f in __main__::foo$241(Array<long long, 1, C, mutable, aligned>, long long) () at demo_gdb_segfault.py:9
9 d = c[index] # access an address that is a) invalid b) out of the page
(gdb) p index
$1 = 1000000000
(gdb) p c
$2 = "p\202\017\364\371U\000\000\000\000\000\000\000\000\000\000\n\000\000\000\000\000\000\000\b\000\000\000\000\000\000\000\240\202\017\364\371U\000\000\n\000\000\000\000\000\000\000\b\000\000\000\000\000\000"
(gdb) whatis c
type = {i8*, i8*, i64, i64, double*, [1 x i64], [1 x i64]}
(gdb) x /32xb c
0x7ffd56195068: 0x70 0x82 0x0f 0xf4 0xf9 0x55 0x00 0x00
0x7ffd56195070: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffd56195078: 0x0a 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7ffd56195080: 0x08 0x00 0x00 0x00 0x00 0x00 0x00 0x00
在gdb
输出中,可以注意到numba_gdb_breakpoint
功能被注册为断点(其符号在numba/_helperlib.c
中),捕获了SIGSEGV
信号,并且发生访问冲突的行是打印。
继续作为调试会话演示的示例,可以打印第一个index
,显然是 1e9。打印c
会产生大量字节,因此需要查找类型。 c
的类型根据其DataModel
显示数组c
的布局(在 Numba 源numba.datamodel.models
中查看布局,ArrayModel
在下面显示为了方便)。
class ArrayModel(StructModel):
def __init__(self, dmm, fe_type):
ndim = fe_type.ndim
members = [
('meminfo', types.MemInfoPointer(fe_type.dtype)),
('parent', types.pyobject),
('nitems', types.intp),
('itemsize', types.intp),
('data', types.CPointer(fe_type.dtype)),
('shape', types.UniTuple(types.intp, ndim)),
('strides', types.UniTuple(types.intp, ndim)),
]
从gdb
(type = {i8*, i8*, i64, i64, double*, [1 x i64], [1 x i64]}
)检查的类型直接对应于ArrayModel
的成员。鉴于 segfault 来自无效访问,检查数组中的项目数并将其与请求的索引进行比较将是有益的。
检查c
,(x /32xb c
)的存储器,前 16 个字节是meminfo
指针和parent
pyobject
对应的两个i8*
。接下来的两组 8 字节是分别对应于nitems
和itemsize
的i64
/ intp
类型。显然它们的值是0x0a
和0x08
,这是有意义的,因为输入数组a
有 10 个元素,类型为int64
,宽度为 8 个字节。因此很明显,segfault 来自包含10
项的数组中索引1000000000
的无效访问。
下一个示例演示了如何使用通过调用numba.gdb_breakpoint()
函数定义的多个断点:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
from numba import njit, gdb_init, gdb_breakpoint
@njit(debug=True)
def foo(a):
gdb_init() # instruct Numba to attach gdb at this location
b = a + 1
gdb_breakpoint() # instruct gdb to break at this location
c = a * 2.34
d = (a, b, c)
gdb_breakpoint() # and to break again at this location
print(a, b, c, d)
r= foo(123)
print(r)
|
在终端中(线路上的...
本身表示为简洁起见未显示的输出):
$ NUMBA_OPT=0 python demo_gdb_breakpoints.py
Attaching to PID: 20366
GNU gdb (GDB) Red Hat Enterprise Linux 8.0.1-36.el7
...
Attaching to process 20366
Reading symbols from <elided for brevity> ...done.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
Reading symbols from /lib64/libc.so.6...Reading symbols from /usr/lib/debug/usr/lib64/libc-2.17.so.debug...done.
0x00007f631db5e550 in __nanosleep_nocancel () at ../sysdeps/unix/syscall-template.S:81
81 T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
Breakpoint 1 at 0x7f6307b658f0: file numba/_helperlib.c, line 1090.
Continuing.
Breakpoint 1, numba_gdb_breakpoint () at numba/_helperlib.c:1090
1090 }
(gdb) step
__main__::foo$241(long long) () at demo_gdb_breakpoints.py:8
8 c = a * 2.34
(gdb) l
3 @njit(debug=True)
4 def foo(a):
5 gdb_init() # instruct Numba to attach gdb at this location
6 b = a + 1
7 gdb_breakpoint() # instruct gdb to break at this location
8 c = a * 2.34
9 d = (a, b, c)
10 gdb_breakpoint() # and to break again at this location
11 print(a, b, c, d)
12
(gdb) p b
$1 = 124
(gdb) p c
$2 = 0
(gdb) continue
Continuing.
Breakpoint 1, numba_gdb_breakpoint () at numba/_helperlib.c:1090
1090 }
(gdb) step
11 print(a, b, c, d)
(gdb) p c
$3 = 287.81999999999999
从gdb
输出可以看出,执行在第 8 行暂停作为断点被击中,并且在发出continue
之后,它再次在第 11 行处破坏,其中下一个断点被击中。
以下示例非常复杂,它按照上面的示例从一开始就使用gdb
检测执行,但它也使用线程并利用断点功能。此外,并行部分的最后一次迭代调用函数work
,在这种情况下实际上只是对glibc
的free(3)
的绑定,但同样可能是一些因未知原因呈现段错误的函数。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
from numba import njit, prange, gdb_init, gdb_breakpoint
import ctypes
def get_free():
lib = ctypes.cdll.LoadLibrary('libc.so.6')
free_binding = lib.free
free_binding.argtypes = [ctypes.c_void_p,]
free_binding.restype = None
return free_binding
work = get_free()
@njit(debug=True, parallel=True)
def foo():
gdb_init() # instruct Numba to attach gdb at this location, but not to pause execution
counter = 0
n = 9
for i in prange(n):
if i > 3 and i < 8: # iterations 4, 5, 6, 7 will break here
gdb_breakpoint()
if i == 8: # last iteration segfaults
work(0xBADADD)
counter += 1
return counter
r = foo()
print(r)
|
在终端中(线路上的...
本身表示为简洁起见未显示的输出),请注意NUMBA_NUM_THREADS
的设置为 4,以确保并行部分中有 4 个线程运行:
$ NUMBA_NUM_THREADS=4 NUMBA_OPT=0 python demo_gdb_threads.py
Attaching to PID: 21462
...
Attaching to process 21462
[New LWP 21467]
[New LWP 21468]
[New LWP 21469]
[New LWP 21470]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
0x00007f59ec31756d in nanosleep () at ../sysdeps/unix/syscall-template.S:81
81 T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
Breakpoint 1 at 0x7f59d631e8f0: file numba/_helperlib.c, line 1090.
Continuing.
[Switching to Thread 0x7f59d1fd1700 (LWP 21470)]
Thread 5 "python" hit Breakpoint 1, numba_gdb_breakpoint () at numba/_helperlib.c:1090
1090 }
(gdb) info threads
Id Target Id Frame
1 Thread 0x7f59eca2f740 (LWP 21462) "python" [email protected]@GLIBC_2.3.2 ()
at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
2 Thread 0x7f59d37d4700 (LWP 21467) "python" [email protected]@GLIBC_2.3.2 ()
at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
3 Thread 0x7f59d2fd3700 (LWP 21468) "python" [email protected]@GLIBC_2.3.2 ()
at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
4 Thread 0x7f59d27d2700 (LWP 21469) "python" numba_gdb_breakpoint () at numba/_helperlib.c:1090
* 5 Thread 0x7f59d1fd1700 (LWP 21470) "python" numba_gdb_breakpoint () at numba/_helperlib.c:1090
(gdb) thread apply 2-5 info locals
Thread 2 (Thread 0x7f59d37d4700 (LWP 21467)):
No locals.
Thread 3 (Thread 0x7f59d2fd3700 (LWP 21468)):
No locals.
Thread 4 (Thread 0x7f59d27d2700 (LWP 21469)):
No locals.
Thread 5 (Thread 0x7f59d1fd1700 (LWP 21470)):
sched$35 = '\000' <repeats 55 times>
counter__arr = '\000' <repeats 16 times>, "\001\000\000\000\000\000\000\000\b\000\000\000\000\000\000\000\370B]\"hU\000\000\001", '\000' <repeats 14 times>
counter = 0
(gdb) continue
Continuing.
[Switching to Thread 0x7f59d27d2700 (LWP 21469)]
Thread 4 "python" hit Breakpoint 1, numba_gdb_breakpoint () at numba/_helperlib.c:1090
1090 }
(gdb) continue
Continuing.
[Switching to Thread 0x7f59d1fd1700 (LWP 21470)]
Thread 5 "python" hit Breakpoint 1, numba_gdb_breakpoint () at numba/_helperlib.c:1090
1090 }
(gdb) continue
Continuing.
[Switching to Thread 0x7f59d27d2700 (LWP 21469)]
Thread 4 "python" hit Breakpoint 1, numba_gdb_breakpoint () at numba/_helperlib.c:1090
1090 }
(gdb) continue
Continuing.
Thread 5 "python" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7f59d1fd1700 (LWP 21470)]
__GI___libc_free (mem=0xbadadd) at malloc.c:2935
2935 if (chunk_is_mmapped(p)) /* release mmapped memory. */
(gdb) bt
#0 __GI___libc_free (mem=0xbadadd) at malloc.c:2935
#1 0x00007f59d37ded84 in $3cdynamic$3e::__numba_parfor_gufunc__0x7ffff80a61ae3e31$244(Array<unsigned long long, 1, C, mutable, aligned>, Array<long long, 1, C, mutable, aligned>) () at <string>:24
#2 0x00007f59d17ce326 in __gufunc__._ZN13$3cdynamic$3e45__numba_parfor_gufunc__0x7ffff80a61ae3e31$244E5ArrayIyLi1E1C7mutable7alignedE5ArrayIxLi1E1C7mutable7alignedE ()
#3 0x00007f59d37d7320 in thread_worker ()
from <path>/numba/numba/npyufunc/workqueue.cpython-37m-x86_64-linux-gnu.so
#4 0x00007f59ec626e25 in start_thread (arg=0x7f59d1fd1700) at pthread_create.c:308
#5 0x00007f59ec350bad in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:113
在输出中可以看到有 4 个线程被启动并且它们都在断点处中断,进一步Thread 5
接收到信号SIGSEGV
并且后向跟踪显示它来自__GI___libc_free
,其中包含无效地址mem
,如预期的那样。
numba.gdb()
和numba.gdb_init()
函数都接受无限的字符串参数,这些参数在初始化时将作为命令行参数直接传递给gdb
,这样可以轻松地在其他函数上设置断点并执行重复的调试任务,而无需手动每次都输入它们。例如,此代码在附加gdb
的情况下运行并在_dgesdd
上设置断点(例如,传递给 LAPACK 的双精度除法和征服者 SVD 函数的参数需要调试)。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
from numba import njit, gdb
import numpy as np
@njit(debug=True)
def foo(a):
# instruct Numba to attach gdb at this location and on launch, switch
# breakpoint pending on , and then set a breakpoint on the function
# _dgesdd, continue execution, and once the breakpoint is hit, backtrace
gdb('-ex', 'set breakpoint pending on',
'-ex', 'b dgesdd_',
'-ex','c',
'-ex','bt')
b = a + 10
u, s, vh = np.linalg.svd(b)
return s # just return singular values
z = np.arange(70.).reshape(10, 7)
r = foo(z)
print(r)
|
在终端中(一行上的...
本身表示出于简洁而未显示的输出),请注意不需要交互来中断和回溯:
$ NUMBA_OPT=0 python demo_gdb_args.py
Attaching to PID: 22300
GNU gdb (GDB) Red Hat Enterprise Linux 8.0.1-36.el7
...
Attaching to process 22300
Reading symbols from <py_env>/bin/python3.7...done.
0x00007f652305a550 in __nanosleep_nocancel () at ../sysdeps/unix/syscall-template.S:81
81 T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
Breakpoint 1 at 0x7f650d0618f0: file numba/_helperlib.c, line 1090.
Continuing.
Breakpoint 1, numba_gdb_breakpoint () at numba/_helperlib.c:1090
1090 }
Breakpoint 2 at 0x7f65102322e0 (2 locations)
Continuing.
Breakpoint 2, 0x00007f65182be5f0 in mkl_lapack.dgesdd_ ()
from <py_env>/lib/python3.7/site-packages/numpy/core/../../../../libmkl_rt.so
#0 0x00007f65182be5f0 in mkl_lapack.dgesdd_ ()
from <py_env>/lib/python3.7/site-packages/numpy/core/../../../../libmkl_rt.so
#1 0x00007f650d065b71 in numba_raw_rgesdd ([email protected]=100 'd', jobz=<optimized out>, [email protected]=65 'A', [email protected]=10,
[email protected]=7, [email protected]=0x561c6fbb20c0, [email protected]=10, s=0x561c6facf3a0, u=0x561c6fb680e0, ldu=10, vt=0x561c6fd375c0,
ldvt=7, work=0x7fff4c926c30, lwork=-1, iwork=0x7fff4c926c40, info=0x7fff4c926c20) at numba/_lapack.c:1277
#2 0x00007f650d06768f in numba_ez_rgesdd (ldvt=7, vt=0x561c6fd375c0, ldu=10, u=0x561c6fb680e0, s=0x561c6facf3a0, lda=10,
a=0x561c6fbb20c0, n=7, m=10, jobz=65 'A', kind=<optimized out>) at numba/_lapack.c:1307
#3 numba_ez_gesdd (kind=<optimized out>, jobz=<optimized out>, m=10, n=7, a=0x561c6fbb20c0, lda=10, s=0x561c6facf3a0,
u=0x561c6fb680e0, ldu=10, vt=0x561c6fd375c0, ldvt=7) at numba/_lapack.c:1477
#4 0x00007f650a3147a3 in numba::targets::linalg::svd_impl::$3clocals$3e::svd_impl$243(Array<double, 2, C, mutable, aligned>, omitted$28default$3d1$29) ()
#5 0x00007f650a1c0489 in __main__::foo$241(Array<double, 2, C, mutable, aligned>) () at demo_gdb_args.py:15
#6 0x00007f650a1c2110 in cpython::__main__::foo$241(Array<double, 2, C, mutable, aligned>) ()
#7 0x00007f650cd096a4 in call_cfunc ()
from <path>/numba/numba/_dispatcher.cpython-37m-x86_64-linux-gnu.so
...
对于 Numba 应用程序的高级用户和调试程序,了解概述的gdb
绑定的一些内部实现细节非常重要。 numba.gdb()
和numba.gdb_init()
功能通过将以下内容注入函数的 LLVM IR 来工作:
- 在函数的调用站点首先注入
getpid(3)
的调用以获取执行进程的 PID 并将其存储以供以后使用,然后注入fork(3)
调用:- 在父母:
- 注入一个呼叫
sleep(3)
(因此在gdb
加载时暂停)。 - 注入
numba_gdb_breakpoint
功能(仅numba.gdb()
执行此操作)。
- 注入一个呼叫
- 在孩子:
- 使用参数
numba.config.GDB_BINARY
,attach
命令和先前记录的 PID 注入execl(3)
。 Numba 有一个特殊的gdb
命令文件,其中包含断开符号numba_gdb_breakpoint
然后finish
的指令,这是为了确保程序在断点处停止,但它停止的帧是编译的 Python 帧(或者一个step
远离,取决于优化)。此命令文件也会添加到参数中,最后添加任何用户指定的参数。
- 使用参数
- 在父母:
在numba.gdb_breakpoint()
的呼叫站点,呼叫被注入特殊的numba_gdb_breakpoint
符号,该符号已经注册并被立即作为中断位置和finish
进行检测。
结果,例如, numba.gdb()
调用将导致程序中的 fork,父进程将在子进程启动gdb
时暂停,并将其附加到父进程并告诉父进程继续。启动的gdb
将numba_gdb_breakpoint
符号注册为断点,当父母继续并停止睡眠时,它将立即调用孩子将要打破的numba_gdb_breakpoint
。额外的numba.gdb_breakpoint()
调用会创建对已注册断点的调用,因此程序也会在这些位置中断。
CUDA Python 代码可以使用 CUDA Simulator 在 Python 解释器中运行,允许使用 Python 调试器或 print 语句进行调试。要启用 CUDA 仿真器,请将环境变量 NUMBA_ENABLE_CUDASIM
设置为 1.有关 CUDA 仿真器的更多信息,请参阅 CUDA 仿真器文档。
通过将debug
参数设置为cuda.jit
到True
(@cuda.jit(debug=True)
),Numba 将在编译的 CUDA 代码中发出源位置。与 CPU 目标不同,只有文件名和行信息可用,但不会发出变量类型信息。该信息足以使用 cuda-memcheck 调试内存错误。
例如,给出以下 cuda python 代码:
|
1
2
3
4
5
6
7
8
9
|
import numpy as np
from numba import cuda
@cuda.jit(debug=True)
def foo(arr):
arr[cuda.threadIdx.x] = 1
arr = np.arange(30)
foo[1, 32](arr) # more threads than array elements
|
我们可以使用cuda-memcheck
来查找内存错误:
$ cuda-memcheck python chk_cuda_debug.py
========= CUDA-MEMCHECK
========= Invalid __global__ write of size 8
========= at 0x00000148 in /home/user/chk_cuda_debug.py:6:cudapy::__main__::foo$241(Array<__int64, int=1, C, mutable, aligned>)
========= by thread (31,0,0) in block (0,0,0)
========= Address 0x500a600f8 is out of bounds
...
=========
========= Invalid __global__ write of size 8
========= at 0x00000148 in /home/user/chk_cuda_debug.py:6:cudapy::__main__::foo$241(Array<__int64, int=1, C, mutable, aligned>)
========= by thread (30,0,0) in block (0,0,0)
========= Address 0x500a600f0 is out of bounds
...