Skip to content

1.15。故障排除和提示

原文: http://numba.pydata.org/numba-doc/latest/user/troubleshoot.html

1.15.1。编译什么

一般建议您应该只尝试编译代码中的关键路径。如果在某些更高级别的代码中有一段性能关键的计算代码,则可以在单独的函数中分解性能关键代码,并使用 Numba 编译单独的函数。让 Numba 专注于那一小部分性能关键代码有几个优点:

  • 它降低了击中不支持功能的风险;
  • 它减少了编译时间;
  • 它允许您更容易地演化编译函数之外的更高级代码。

1.15.2。我的代码没有编译

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'

1.15.3。我的代码有一个类型统一问题

Numba 无法编译代码的另一个常见原因是它无法静态地确定函数的返回类型。最可能的原因是返回类型取决于仅在运行时可用的值。同样,当使用 nopython 模式时,这通常是有问题的。类型统一的概念只是试图找到一种可以安全地表示两个变量的类型。例如,64 位浮点数和 64 位复数可以用 128 位复数表示。

作为类型统一失败的示例,此函数具有在运行时根据 x 的值确定的返回类型:

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 元组的类型”。

1.15.4。我的代码有一个无类型列表问题

正如之前提到的,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

1.15.5。编译的代码太慢

编译 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()功能:

    ```py

    f(1, 2) 3.0 f(1, "2") example.py:7: NumbaWarning: Function "f" failed type inference: Internal error at : 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

    ```

1.15.6。禁用 JIT 编译

为了调试代码,可以禁用 JIT 编译,这使得jit装饰器(以及装饰器njitautojit)就像它们不执行任何操作一样,并且装饰函数的调用调用原始 Python 函数而不是编译版本。可以通过将 NUMBA_DISABLE_JIT 环境变量设置为1来切换。

启用此模式时,vectorizeguvectorize装饰器仍将导致 ufunc 的编译,因为这些函数没有直接的纯 Python 实现。

1.15.7。使用 GDB 调试 JIT 编译代码

jit装饰器中设置debug关键字参数(例如@jit(debug=True))可以在 jitted 代码中发出调试信息。要进行调试,需要 GDB 7.0 或更高版本。目前,可以使用以下调试信息:

  • 函数名称将显示在回溯中。但是,没有类型信息。
  • 源位置(文件名和行号)可用。例如,用户可以通过绝对文件名和行号设置断点;例如break /path/to/myfile.py:6
  • 当前函数中的局部变量可以用info locals显示。
  • whatis myvar的变量类型。
  • 变量值print myvardisplay myvar
    • 简单的数字类型,即 int,float 和 double,以其原生表示形式显示。但是,假设整数被签名。
    • 其他类型显示为字节序列。

已知的问题:

  • 步进很大程度上取决于优化级别。
    • 在完全优化(相当于 O3)时,大多数变量都被优化掉了。
    • 没有优化(例如NUMBA_OPT=0),当单步执行代码时,源位置会跳转。
    • 在 O1 优化(例如NUMBA_OPT=1),步进稳定但一些变量被优化。
  • 启用调试信息后,内存消耗会显着增加。编译器发出额外信息( DWARF )以及指令。使用调试信息,发出的目标代码可以大 2 倍。

内部细节:

  • 由于 Python 语义允许变量绑定到不同类型的值,因此 Numba 在内部为每种类型创建变量的多个版本。所以代码如下:

    ```py x = 1 # type int x = 2.3 # type float x = (1, 2, 3) # type 3-tuple of int

    ```

    每个分配将存储到不同的变量名称。在调试器中,变量将是xx$1x$2。 (在 Numba IR 中,它们是xx.1x.2。)

  • 启用调试时,将禁用内联函数。

1.15.7.1。调试用法示例

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

1.15.7.2。全局覆盖调试设置

通过设置环境变量NUMBA_DEBUGINFO=1,可以为整个应用程序启用调试。这将设置jitdebug选项的默认值。通过设置debug=False可以关闭各个功能的调试。

请注意,启用调试信息会显着增加每个已编译函数的内存消耗。对于大型应用程序,这可能会导致内存不足错误。

1.15.8。在nopython模式 中使用 Numba 的直接gdb绑定

Numba(版本 0.42.0 及更高版本)具有一些与gdb支持 CPU 相关的附加功能,可以更轻松地调试程序。以下描述的所有gdb相关函数以相同的方式工作,而不管它们是从标准 CPython 解释器调用还是以 nopython 模式对象模式编译的代码。

注意

这个功能是实验性的!

警告

如果从 Jupyter 或pdb模块旁边使用此功能会发生意外情况。它的行为是无害的,很难预测!

1.15.8.1。设置

Numba 的gdb相关函数使用gdb二进制,如果需要,可以通过 NUMBA_GDB_BINARY 环境变量配置该二进制文件的位置和名称。

注意

Numba 的gdb支持要求gdb能够附加到另一个进程。在某些系统(特别是 Ubuntu Linux)上,ptrace上的默认安全限制阻止了这种情况。 Linux 安全模块 Yama 在系统级强制执行此限制。有关此模块的文档以及更改其行为的安全隐患,请参见 Linux 内核文档Ubuntu Linux 安全文档讨论了如何根据ptrace_scope调整 Yama 的行为,以便允许所需的行为。

1.15.8.2。基本gdb支持

警告

不建议在同一程序中多次调用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 源的堆栈帧。从那里可以看出,已经评估了变量ab,但c没有,正如通过打印它们的值所证明的那样,这正如gdb()调用的位置所预期的那样。发出next然后评估线7c被赋予一个值,如最终印刷品所示。

1.15.8.3。运行gdb

numba.gdb()提供的功能(启动并将gdb连接到执行进程并在断点处暂停)也可作为两个单独的功能使用:

  • numba.gdb_init()此函数在调用站点注入代码以启动并将gdb附加到执行进程,但不会暂停执行。
  • numba.gdb_breakpoint()此函数在调用站点注入代码,该代码将调用在 Numba gdb支持中注册为断点的特殊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)),
        ]

gdbtype = {i8*, i8*, i64, i64, double*, [1 x i64], [1 x i64]})检查的类型直接对应于ArrayModel的成员。鉴于 segfault 来自无效访问,检查数组中的项目数并将其与请求的索引进行比较将是有益的。

检查c,(x /32xb c)的存储器,前 16 个字节是meminfo指针和parent pyobject对应的两个i8*。接下来的两组 8 字节是分别对应于nitemsitemsizei64 / intp类型。显然它们的值是0x0a0x08,这是有意义的,因为输入数组a有 10 个元素,类型为int64,宽度为 8 个字节。因此很明显,segfault 来自包含10项的数组中索引1000000000的无效访问。

1.15.8.4。向代码 添加断点

下一个示例演示了如何使用通过调用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 行处破坏,其中下一个断点被击中。

1.15.8.5。并行区域调试

以下示例非常复杂,它按照上面的示例从一开始就使用gdb检测执行,但它也使用线程并利用断点功能。此外,并行部分的最后一次迭代调用函数work,在这种情况下实际上只是对glibcfree(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,如预期的那样。

1.15.8.6。使用gdb命令语言

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
...

1.15.8.7。 gdb绑定如何工作?

对于 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_BINARYattach命令和先前记录的 PID 注入execl(3)。 Numba 有一个特殊的gdb命令文件,其中包含断开符号numba_gdb_breakpoint然后finish的指令,这是为了确保程序在断点处停止,但它停止的帧是编译的 Python 帧(或者一个step远离,取决于优化)。此命令文件也会添加到参数中,最后添加任何用户指定的参数。

numba.gdb_breakpoint()的呼叫站点,呼叫被注入特殊的numba_gdb_breakpoint符号,该符号已经注册并被立即作为中断位置和finish进行检测。

结果,例如, numba.gdb()调用将导致程序中的 fork,父进程将在子进程启动gdb时暂停,并将其附加到父进程并告诉父进程继续。启动的gdbnumba_gdb_breakpoint符号注册为断点,当父母继续并停止睡眠时,它将立即调用孩子将要打破的numba_gdb_breakpoint。额外的numba.gdb_breakpoint()调用会创建对已注册断点的调用,因此程序也会在这些位置中断。

1.15.9。调试 CUDA Python 代码

1.15.9.1。使用模拟器

CUDA Python 代码可以使用 CUDA Simulator 在 Python 解释器中运行,允许使用 Python 调试器或 print 语句进行调试。要启用 CUDA 仿真器,请将环境变量 NUMBA_ENABLE_CUDASIM 设置为 1.有关 CUDA 仿真器的更多信息,请参阅 CUDA 仿真器文档

1.15.9.2。调试信息

通过将debug参数设置为cuda.jitTrue@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
...



回到顶部