故障排除和提示

编译什么

一般的建议是,你应该只尝试编译代码中的关键路径。如果你在高级代码中有一段性能关键的计算代码,你可以将性能关键代码分解到一个单独的函数中,并使用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:

错误信息帮助你找出问题所在:“Invalid use of + with parameters (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 位复数。

作为一个类型统一失败的例子,这个函数的返回类型是在运行时根据 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

错误信息“Can’t unify return type from the following types: tuple(int64 x 1), int64”应理解为“Numba 无法找到一个可以安全表示一个整数元组和一个整数的类型”。

我的代码存在未类型化的列表问题

之前所述,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(forceobj=True) 太慢

对象模式 相比于常规的 Python 解释几乎没有加速,其主要目的是允许一种称为 循环提升 的内部优化。这种优化将允许在 nopython 模式 下编译内部循环,无论这些内部循环周围的代码是什么。如果内部循环使用了 nopython 模式 不支持的类型或操作,编译仍可以回退到 对象模式

禁用JIT编译

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

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

使用 GDB 调试 JIT 编译的代码

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

  • 函数名将连同类型信息和值(如果可用)一起显示在回溯中。

  • 源位置(文件名和行号)是可用的。例如,用户可以通过绝对文件名和行号设置断点;例如 break /path/to/myfile.py:6

  • 可以使用 info args 显示当前函数的参数

  • 当前函数中的局部变量可以通过 info locals 显示。

  • 变量的类型可以通过 whatis myvar 显示。

  • 变量的值可以通过 print myvardisplay myvar 显示。

    • 简单的数值类型,即 int、float 和 double,以它们的原生表示形式显示。

    • 其他类型则显示为基于Numba内存模型表示的类型结构。

此外,Numba 的 gdb 打印扩展可以加载到 gdb 中(如果 gdb 支持 Python),以允许以原生 Python 的方式打印变量。该扩展通过将 Numba 的内存模型表示重新解释为 Python 类型来实现这一点。可以通过使用 numba -g 命令显示 Numba 正在使用的 gdb 安装信息,包括加载 gdb 打印扩展的路径。为了获得最佳效果,请确保 gdb 使用的 Python 可以访问 NumPy 模块。以下是 gdb 信息的一个示例输出:

  $ numba -g
  GDB info:
  --------------------------------------------------------------------------------
  Binary location                               : <some path>/gdb
  Print extension location                      : <some python path>/numba/misc/gdb_print_extension.py
  Python version                                : 3.8
  NumPy version                                 : 1.20.0
  Numba printing extension supported            : True

  To load the Numba gdb printing extension, execute the following from the gdb prompt:

  source <some python path>/numba/misc/gdb_print_extension.py

  --------------------------------------------------------------------------------

已知问题:

  • 步进在很大程度上取决于优化级别。在完全优化(相当于 O3)时,大多数变量都会被优化掉。通常使用 jit 选项 _dbg_optnone=True 或环境变量 NUMBA_OPT 来调整优化级别,以及使用 jit 选项 _dbg_extend_lifetimes=True``(如果 ``debug=True,则默认开启)或 NUMBA_EXTEND_VARIABLE_LIFETIMES 来延长变量的生命周期到其作用域的末尾,以便获得更接近 Python 执行语义的调试体验。

  • 启用调试信息后,内存消耗显著增加。编译器会发出额外的信息(DWARF)以及指令。带有调试信息的目标代码大小可能会增加一倍。

内部细节:

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

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

    每个赋值将存储到不同的变量名中。在调试器中,这些变量将是 x, x$1x$2。(在Numba IR中,它们是 x, x.1x.2。)

  • 当启用调试时,LLVM IR 级别的函数内联将被禁用。

JIT 调试选项

  • debug (bool)。设置为 True 以启用调试信息。默认为 False

  • _dbg_optnone (bool)。设置为 True 以禁用函数上的所有 LLVM 优化传递。默认为 False。有关禁用优化的全局设置,请参阅 NUMBA_OPT

  • _dbg_extend_lifetimes (bool)。设置为 True 以延长对象的生命周期,使其更接近 Python 的语义。当 debug=True 时自动设置为 True;否则,默认为 False。用户可以显式地将此选项设置为 False 以保留编译代码的正常执行语义。有关延长对象生命周期的全局选项,请参阅 NUMBA_EXTEND_VARIABLE_LIFETIMES

示例调试用法

Python 源代码:

 1from numba import njit
 2
 3@njit(debug=True)
 4def foo(a):
 5    b = a + 1
 6    c = a * 2.34
 7    d = (a, b, c)
 8    print(a, b, c, d)
 9
10r = foo(123)
11print(r)

在终端中:

  $ NUMBA_OPT=0 NUMBA_EXTEND_VARIABLE_LIFETIMES=1 gdb -q python
  Reading symbols from python...
  (gdb) break test1.py:5
  No source file named test1.py.
  Make breakpoint pending on future shared library load? (y or [n]) y
  Breakpoint 1 (test1.py:5) pending.
  (gdb) run test1.py
  Starting program: <path>/bin/python test1.py
  ...
  Breakpoint 1, __main__::foo_241[abi:c8tJTC_2fWgEeGLSgydRTQUgiqKEZ6gEoDvQJmaQIA](long long) (a=123) at test1.py:5
  5           b = a + 1
  (gdb) info args
  a = 123
  (gdb) n
  6           c = a * 2.34
  (gdb) info locals
  b = 124
  c = 0
  d = {f0 = 0, f1 = 0, f2 = 0}
  (gdb) n
  7           d = (a, b, c)
  (gdb) info locals
  b = 124
  c = 287.81999999999999
  d = {f0 = 0, f1 = 0, f2 = 0}
  (gdb) whatis b
  type = int64
  (gdb) whatis d
  type = Tuple(int64, int64, float64) ({i64, i64, double})
  (gdb) n
  8           print(a, b, c, d)
  (gdb) print b
  $1 = 124
  (gdb) print d
  $2 = {f0 = 123, f1 = 124, f2 = 287.81999999999999}
  (gdb) bt
  #0  __main__::foo_241[abi:c8tJTC_2fWgEeGLSgydRTQUgiqKEZ6gEoDvQJmaQIA](long long) (a=123) at test1.py:8
  #1  0x00007ffff06439fa in cpython::__main__::foo_241[abi:c8tJTC_2fWgEeGLSgydRTQUgiqKEZ6gEoDvQJmaQIA](long long) ()

下面是另一个示例,它使用了上面提到的 Numba gdb 打印扩展,注意当扩展通过 source 加载后,打印格式的变化:

Python 源代码:

 1  from numba import njit
 2  import numpy as np
 3
 4  @njit(debug=True)
 5  def foo(n):
 6      x = np.arange(n)
 7      y = (x[0], x[-1])
 8      return x, y
 9
10  foo(4)

在终端中:

  $ NUMBA_OPT=0 NUMBA_EXTEND_VARIABLE_LIFETIMES=1 gdb -q python
  Reading symbols from python...
  (gdb) set breakpoint pending on
  (gdb) break test2.py:8
  No source file named test2.py.
  Breakpoint 1 (test2.py:8) pending.
  (gdb) run test2.py
  Starting program: <path>/bin/python test2.py
  ...
  Breakpoint 1, __main__::foo_241[abi:c8tJTC_2fWgEeGLSgydRTQUgiqKEZ6gEoDvQJmaQIA](long long) (n=4) at test2.py:8
  8           return x, y
  (gdb) print x
  $1 = {meminfo = 0x55555688f470 "\001", parent = 0x0, nitems = 4, itemsize = 8, data = 0x55555688f4a0, shape = {4}, strides = {8}}
  (gdb) print y
  $2 = {0, 3}
  (gdb) source numba/misc/gdb_print_extension.py
  (gdb) print x
  $3 =
  [0 1 2 3]
  (gdb) print y
  $4 = (0, 3)

全局覆盖调试设置

通过设置环境变量 NUMBA_DEBUGINFO=1 可以为整个应用程序启用调试。这会设置 jitdebug 选项的默认值。可以通过设置 debug=False 在单个函数上关闭调试。

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

nopython 模式下使用 Numba 的直接 gdb 绑定

Numba(版本0.42.0及更高版本)有一些与CPU的``gdb``支持相关的额外功能,这些功能使得调试程序更加容易。以下描述的所有``gdb``相关功能,无论它们是从标准的CPython解释器调用,还是在:term:`nopython模式`或:term:`对象模式`下编译的代码中调用,其工作方式都是相同的。

备注

此功能是实验性的!

警告

如果在 Jupyter 中使用或与 pdb 模块一起使用,此功能会执行意外的操作。它的行为是无害的,只是难以预测!

设置

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

备注

Numba 的 gdb 支持需要 gdb 能够附加到另一个进程的能力。在一些系统(特别是 Ubuntu Linux)上,默认的安全限制会阻止 ptrace 进行此操作。这种限制是通过 Linux 安全模块 Yama 在系统级别强制执行的。有关此模块的文档及其行为更改的安全影响,可以在 Linux 内核文档 中找到。Ubuntu Linux 安全文档 讨论了如何调整 Yama 关于 ptrace_scope 的行为,以便允许所需的行为。

基本 gdb 支持

警告

在同一个程序中多次调用 numba.gdb() 和/或 numba.gdb_init() 是不推荐的,可能会发生意外情况。如果在一个程序中需要多个断点,可以通过 numba.gdb()numba.gdb_init() 启动一次 gdb,然后使用 numba.gdb_breakpoint() 来注册额外的断点位置。

添加 gdb 支持的最简单函数是 numba.gdb(),它在调用位置将:

  • 启动 gdb 并附加到正在运行的进程。

  • numba.gdb() 函数调用处创建一个断点,附加的 gdb 将在此处暂停执行,等待用户输入。

使用此功能的最佳方式是通过示例来激发,继续使用上面的示例:

 1from numba import njit, gdb
 2
 3@njit(debug=True)
 4def 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
11r= foo(123)
12print(r)

在终端中(单独一行的 ... 表示为了简洁未展示的输出):

$ NUMBA_OPT=0 NUMBA_EXTEND_VARIABLE_LIFETIMES=1 python demo_gdb.py
...
Breakpoint 1, 0x00007fb75238d830 in numba_gdb_breakpoint () from numba/_helperlib.cpython-39-x86_64-linux-gnu.so
(gdb) s
Single stepping until exit from function numba_gdb_breakpoint,
which has no line number information.
0x00007fb75233e1cf in numba::misc::gdb_hook::hook_gdb::_3clocals_3e::impl_242[abi:c8tJTIeFCjyCbUFRqqOAK_2f6h0phxApMogijRBAA_3d](StarArgTuple) ()
(gdb) s
Single stepping until exit from function _ZN5numba4misc8gdb_hook8hook_gdb12_3clocals_3e8impl_242B44c8tJTIeFCjyCbUFRqqOAK_2f6h0phxApMogijRBAA_3dE12StarArgTuple,
which has no line number information.
__main__::foo_241[abi:c8tJTC_2fWgEeGLSgydRTQUgiqKEZ6gEoDvQJmaQIA](long long) (a=123) 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) b 9
Breakpoint 2 at 0x7fb73d1f7287: file demo_gdb.py, line 9.
(gdb) c
Continuing.

Breakpoint 2, __main__::foo_241[abi:c8tJTC_2fWgEeGLSgydRTQUgiqKEZ6gEoDvQJmaQIA](long long) (a=123) at demo_gdb.py:9
9           print(a, b, c, d)
(gdb) info locals
b = 124
c = 287.81999999999999
d = {f0 = 123, f1 = 124, f2 = 287.81999999999999}

从上面的例子可以看出,代码在 numba_gdb_breakpoint 函数的末尾 gdb() 函数调用处暂停执行(这是 Numba 内部符号,注册为 gdb 的断点)。在此处发出两次 step 命令会移动到编译后的 Python 源代码的堆栈帧。从那里可以看到,变量 ab 已经被求值,但 c 还没有,正如通过打印它们的值所展示的那样,这正是由于 gdb() 调用的位置所预期的。在第 9 行发出 break 命令然后继续执行,会导致第 7 行的求值。变量 c 被赋值,这在断点命中时 info locals 的输出中可以看到。

使用 gdb 运行

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

  • numba.gdb_init() 这个函数在调用点注入代码以启动并附加 gdb 到正在执行的进程,但不会暂停执行。

  • numba.gdb_breakpoint() 这个函数在调用点注入代码,该代码将调用注册为Numba的``gdb``支持中的断点的特殊``numba_gdb_breakpoint``函数。这在下一节中进行了演示。

此功能支持更复杂的调试能力。再次以示例为动机,调试一个 ‘segfault’(内存访问违规信号 SIGSEGV):

 1  from numba import njit, gdb_init
 2  import numpy as np
 3
 4  # NOTE debug=True switches bounds-checking on, but for the purposes of this
 5  # example it is explicitly turned off so that the out of bounds index is
 6  # not caught!
 7  @njit(debug=True, boundscheck=False)
 8  def foo(a, index):
 9      gdb_init() # instruct Numba to attach gdb at this location, but not to pause execution
10      b = a + 1
11      c = a * 2.34
12      d = c[index] # access an address that is a) invalid b) out of the page
13      print(a, b, c, d)
14
15  bad_index = int(1e9) # this index is invalid
16  z = np.arange(10)
17  r = foo(z, bad_index)
18  print(r)

在终端中(单独一行的 ... 表示为了简洁未展示的输出):

$ NUMBA_OPT=0 python demo_gdb_segfault.py
...
Program received signal SIGSEGV, Segmentation fault.
0x00007f5a4ca655eb in __main__::foo_241[abi:c8tJTC_2fWgEeGLSgydRTQUgiqKEZ6gEoDvQJmaQIA](Array<long long, 1, C, mutable, aligned>, long long) (a=..., index=1000000000) at demo_gdb_segfault.py:12
12          d = c[index] # access an address that is a) invalid b) out of the page
(gdb) p index
$1 = 1000000000
(gdb) p c
$2 = {meminfo = 0x5586cfb95830 "\001", parent = 0x0, nitems = 10, itemsize = 8, data = 0x5586cfb95860, shape = {10}, strides = {8}}
(gdb) whatis c
type = array(float64, 1d, C) ({i8*, i8*, i64, i64, double*, [1 x i64], [1 x i64]})
(gdb) p c.nitems
$3 = 10

gdb 输出中可以注意到捕获了一个 SIGSEGV 信号,并且打印了发生访问冲突的行。

继续以调试会话演示为例,首先可以打印 index ,显然它是 1e9。打印 c 显示它是一个结构,因此需要查找类型,可以看到它是一个 array(float64, 1d, C) 类型。鉴于段错误来自无效访问,检查数组中的项目数量并与请求的索引进行比较将是有益的。检查结构 cnitems 成员显示有 10 个项目。因此,很明显,段错误来自对包含 10 个项目的数组中的索引 1000000000 的无效访问。

在代码中添加断点

下一个示例展示了通过调用 numba.gdb_breakpoint() 函数定义的多个断点的使用:

 1from numba import njit, gdb_init, gdb_breakpoint
 2
 3@njit(debug=True)
 4def 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
13r= foo(123)
14print(r)

在终端中(单独一行的 ... 表示为了简洁未展示的输出):

$ NUMBA_OPT=0 python demo_gdb_breakpoints.py
...
Breakpoint 1, 0x00007fb65bb4c830 in numba_gdb_breakpoint () from numba/_helperlib.cpython-39-x86_64-linux-gnu.so
(gdb) step
Single stepping until exit from function numba_gdb_breakpoint,
which has no line number information.
__main__::foo_241[abi:c8tJTC_2fWgEeGLSgydRTQUgiqKEZ6gEoDvQJmaQIA](long long) (a=123) 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) c
Continuing.

Breakpoint 1, 0x00007fb65bb4c830 in numba_gdb_breakpoint ()
from numba/_helperlib.cpython-39-x86_64-linux-gnu.so
(gdb) step
11          print(a, b, c, d)
(gdb) p c
$3 = 287.81999999999999

gdb 输出可以看出,执行在第8行暂停,因为触发了断点,而在发出 continue 命令后,它在第11行再次中断,因为触发了下一个断点。

并行区域中的调试

以下示例相当复杂,它从一开始就按照上述示例使用 gdb 进行检测,但它还使用了线程并利用了断点功能。此外,并行部分的最后一次迭代调用了函数 work,在这种情况下,它实际上只是 glibcfree(3) 的一个绑定,但同样可以是某个因未知原因导致段错误的复杂函数。

 1  from numba import njit, prange, gdb_init, gdb_breakpoint
 2  import ctypes
 3
 4  def get_free():
 5      lib = ctypes.cdll.LoadLibrary('libc.so.6')
 6      free_binding = lib.free
 7      free_binding.argtypes = [ctypes.c_void_p,]
 8      free_binding.restype = None
 9      return free_binding
10
11  work = get_free()
12
13  @njit(debug=True, parallel=True)
14  def foo():
15      gdb_init() # instruct Numba to attach gdb at this location, but not to pause execution
16      counter = 0
17      n = 9
18      for i in prange(n):
19          if i > 3 and i < 8: # iterations 4, 5, 6, 7 will break here
20              gdb_breakpoint()
21
22          if i == 8: # last iteration segfaults
23              work(0xBADADD)
24
25          counter += 1
26      return counter
27
28  r = foo()
29  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" pthread_cond_wait@@GLIBC_2.3.2 ()
    at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
2    Thread 0x7f59d37d4700 (LWP 21467) "python" pthread_cond_wait@@GLIBC_2.3.2 ()
    at ../nptl/sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185
3    Thread 0x7f59d2fd3700 (LWP 21468) "python" pthread_cond_wait@@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 包含无效地址,正如预期。

使用 gdb 命令语言

无论是 numba.gdb() 还是 numba.gdb_init() 函数,都接受无限数量的字符串参数,这些参数将在 gdb 初始化时直接作为命令行参数传递给 gdb。这使得设置其他函数的断点并执行重复的调试任务变得容易,而无需每次手动输入它们。例如,这段代码在附加 gdb 的情况下运行,并在 _dgesdd 上设置了一个断点(例如,假设需要调试传递给 LAPACK 的双精度分治 SVD 函数的参数)。

 1  from numba import njit, gdb
 2  import numpy as np
 3
 4  @njit(debug=True)
 5  def foo(a):
 6      # instruct Numba to attach gdb at this location and on launch, switch
 7      # breakpoint pending on , and then set a breakpoint on the function
 8      # _dgesdd, continue execution, and once the breakpoint is hit, backtrace
 9      gdb('-ex', 'set breakpoint pending on',
10          '-ex', 'b dgesdd_',
11          '-ex','c',
12          '-ex','bt')
13      b = a + 10
14      u, s, vh = np.linalg.svd(b)
15      return s # just return singular values
16
17  z = np.arange(70.).reshape(10, 7)
18  r = foo(z)
19  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 (kind=kind@entry=100 'd', jobz=<optimized out>, jobz@entry=65 'A', m=m@entry=10,
    n=n@entry=7, a=a@entry=0x561c6fbb20c0, lda=lda@entry=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
...

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 帧(或距离优化后的帧一步之遥)。此命令文件也被添加到参数中,最后添加任何用户指定的参数。

在调用 numba.gdb_breakpoint() 的地方,会注入对特殊符号 numba_gdb_breakpoint 的调用,该符号已被注册并作为断点位置进行检测,并立即执行 finish

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

调试 CUDA Python 代码

使用模拟器

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

调试信息

通过将 debug 参数设置为 cuda.jitTrue (即 @cuda.jit(debug=True)),Numba 将在编译的 CUDA 代码中发出源位置信息。与 CPU 目标不同,仅提供文件名和行信息,不发出变量类型信息。这些信息足以使用 cuda-memcheck 调试内存错误。

例如,给定以下 cuda python 代码:

1import numpy as np
2from numba import cuda
3
4@cuda.jit(debug=True)
5def foo(arr):
6    arr[cuda.threadIdx.x] = 1
7
8arr = np.arange(30)
9foo[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
...