故障排除和技巧

要编译什么

一般的建议是,您应该只尝试编译代码中的关键路径。如果您在一些高级代码中有一段性能关键的计算代码,您可以将性能关键的代码分解为一个单独的函数,并使用 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 找不到一种可以安全表示整数的 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(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 正在使用的 gdb 安装的信息,包括加载 gdb 打印扩展的路径,可以使用 numba -g 命令显示。为获得最佳结果,请确保 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

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

已知问题

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

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

内部细节

  • 由于 Python 语义允许变量绑定到不同类型的值,Numba 内部为每种类型创建变量的多个版本。所以对于类似这样的代码

    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。)

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

调试的 JIT 选项

  • debug (布尔值)。设置为 True 可启用调试信息。默认为 False

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

  • _dbg_extend_lifetimes (布尔值)。设置为 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 解释器调用,还是从nopython 模式对象模式编译的代码调用。

注意

此功能是实验性的!

警告

如果从 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_breakpoint 函数,该函数在 Numba 的 gdb 支持中注册为断点。这将在下一节中演示。

此功能支持更复杂的调试能力。再次以示例为驱动,调试“段错误”(内存访问违规信号 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_freemem 中包含无效地址,正如预期。

使用 gdb 命令语言

numba.gdb()numba.gdb_init() 函数都接受无限个字符串参数,这些参数在 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() 会这样做)。

    • 在子进程中

      • 注入对 execl(3) 的调用,参数为 numba.config.GDB_BINARYattach 命令以及前面记录的 PID。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 代码可以使用 CUDA 模拟器在 Python 解释器中运行,从而可以使用 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
...