调试

调试并行程序并不容易,因此Taichi提供了内置的支持,希望能帮助你调试Taichi程序。

内核中的运行时 print

print(arg1, ..., sep=' ', end='n')

在 Taichi 作用域内用 print() 调试程序。例如:

@ti.kernel
def inside_taichi_scope():
    x = 233
    print('hello', x)
    #=> hello 233

    print('hello', x * 2 + 200)
    #=> hello 666

    print('hello', x, sep='')
    #=> hello233

    print('hello', x, sep='', end='')
    print('world', x, sep='')
    #=> hello233world233

    m = ti.Matrix([[2, 3, 4], [5, 6, 7]])
    print('m =', m)
    #=> m = [[2, 3, 4], [5, 6, 7]]

    v = ti.Vector([3, 4])
    print('v =', v)
    #=> v = [3, 4]

目前,Taichi 作用域的 print 支持字符串、标量、矢量和矩阵表达式作为参数。Taichi 作用域中的 print 可能与 Python 作用域中的 print 略有不同。请参阅下面的详细信息。

警告

对于 CPU 和 CUDA 后端print 在图形 Python 层(包括 IDLE 和 Jupyter notebook)中不起作用。这是因为这些后端将输出打印到控制台而不是 GUI。如果你希望在 IDLE/ Jupyter 中使用 print ,请使用 OpenGL 或Metal 后端

警告

对于 CUDA 后端,打印的结果不会显示,直到 ti.sync() 被调用:

import taichi as ti
ti.init(arch=ti.cuda)

@ti.kernel
def kern():
    print('inside kernel')

print('before kernel')
kern()
print('after kernel')
ti.sync()
print('after sync')

得到:

before kernel
after kernel
inside kernel
after sync

请注意,主机访问或程序终止也将隐式触发 ti.sync().。

注解

请注意,Taichi 作用域中的 print 只能接收 逗号分隔参数。不应使用 f 字符串或格式化字符串。例如:

import taichi as ti
ti.init(arch=ti.cpu)
a = ti.var(ti.f32, 4)


@ti.kernel
def foo():
    a[0] = 1.0
    print('a[0] = ', a[0]) # 正确
    print(f'a[0] = {a[0]}') # 错误,不支持 f-string
    print("a[0] = %f" % a[0]) # 错误, 不支持格式化字符串

foo()

编译时 ti.static_print

有时,在Taichi 作用域中打印 Python 作用域的对象和常量(如数据类型或 SNodes)非常有用。因此,类似于 ti.static ,我们提供 ti.static_print 来打印编译时常数。它类似于 Python 作用域的 print

x = ti.var(ti.f32, (2, 3))
y = 1

@ti.kernel
def inside_taichi_scope():
    ti.static_print(y)
    # => 1
    ti.static_print(x.shape)
    # => (2, 3)
    ti.static_print(x.dtype)
    # => DataType.float32
    for i in range(4):
            ti.static_print(i.dtype)
            # => DataType.int32
            # 只会打印一次

print 不同,ti.static_print 在编译时只打印一次表达式,因此没有运行时成本。

内核中的运行时 assert

程序员可以在 Taichi 作用域内使用 assert 语句。当断言条件失败时, RuntimeError 将被触发以指示错误。

若要使 assert 正常工作,首先请确保使用 CPU 后端 运行程序。其次出于性能方面的考量, assert 仅在 debug 模式开启时有效,例如:

ti.init(arch=ti.cpu, debug=True)

x = ti.var(ti.f32, 128)

@ti.kernel
def do_sqrt_all():
    for i in x:
        assert x[i] >= 0
        x[i] = ti.sqrt(x)

完成调试后,只需设置 debug=False 。此时, assert 将被忽略,并且不会产生运行时开销。

编译时 ti.static_assert

ti.static_assert(cond, msg=None)

ti.static_print 一样,我们还提供了 assert 的静态版本: ti.static_assert 。对数据类型、维度和形状进行断言可能很有用。无论是否指定 debug=True ,它都有效。当断言失败时,它将引发一个 AssertionError ,就像 Python 作用域中的 assert 一样。

例如:

@ti.func
def copy(dst: ti.template(), src: ti.template()):
    ti.static_assert(dst.shape == src.shape, "copy() needs src and dst tensors to be same shape")
    for I in ti.grouped(src):
        dst[I] = src[I]
    return x % 2 == 1

优雅的 Taichi 作用域的栈回溯

我们都知道,Python 提供了一个有用的堆栈回溯系统,它可以帮你轻松找到问题。但有时 Taichi 作用域 的堆栈回溯(stack traceback)日志可能极其复杂且难以阅读。例如:

import taichi as ti
ti.init()

@ti.func
def func3():
    ti.static_assert(1 + 1 == 3)

@ti.func
def func2():
    func3()

@ti.func
def func1():
    func2()

@ti.kernel
def func0():
    func1()

func0()

当然,运行此代码将导致 AssertionError

Traceback (most recent call last):
  File "misc/demo_excepthook.py", line 20, in <module>
    func0()
  File "/root/taichi/python/taichi/lang/kernel.py", line 559, in wrapped
    return primal(*args, **kwargs)
  File "/root/taichi/python/taichi/lang/kernel.py", line 488, in __call__
    self.materialize(key=key, args=args, arg_features=arg_features)
  File "/root/taichi/python/taichi/lang/kernel.py", line 367, in materialize
    taichi_kernel = taichi_kernel.define(taichi_ast_generator)
  File "/root/taichi/python/taichi/lang/kernel.py", line 364, in taichi_ast_generator
    compiled()
  File "misc/demo_excepthook.py", line 18, in func0
    func1()
  File "/root/taichi/python/taichi/lang/kernel.py", line 39, in decorated
    return fun.__call__(*args)
  File "/root/taichi/python/taichi/lang/kernel.py", line 79, in __call__
    ret = self.compiled(*args)
  File "misc/demo_excepthook.py", line 14, in func1
    func2()
  File "/root/taichi/python/taichi/lang/kernel.py", line 39, in decorated
    return fun.__call__(*args)
  File "/root/taichi/python/taichi/lang/kernel.py", line 79, in __call__
    ret = self.compiled(*args)
  File "misc/demo_excepthook.py", line 10, in func2
    func3()
  File "/root/taichi/python/taichi/lang/kernel.py", line 39, in decorated
    return fun.__call__(*args)
  File "/root/taichi/python/taichi/lang/kernel.py", line 79, in __call__
    ret = self.compiled(*args)
  File "misc/demo_excepthook.py", line 6, in func3
    ti.static_assert(1 + 1 == 3)
  File "/root/taichi/python/taichi/lang/error.py", line 14, in wrapped
    return foo(*args, **kwargs)
  File "/root/taichi/python/taichi/lang/impl.py", line 252, in static_assert
    assert cond
AssertionError

分析诸如 decorated__call__ 之类晦涩的信息也许能让你的大脑过载着火。其实这些是Taichi 内部堆栈帧。直接暴露它们对普通用户几乎没有好处,并且会使回溯日志很难阅读。

为此,我们可能希望使用 ti.init(excepthook=True) ,这会与异常处理程序 挂钩(hook) ,从而使 Taichi 作用域中的堆栈回溯日志更直观且易于阅读。例如:

import taichi as ti
ti.init(excepthook=True)  # just add this option!

...

这样结果会是:

========== Taichi Stack Traceback ==========
In <module>() at misc/demo_excepthook.py:21:
--------------------------------------------
@ti.kernel
def func0():
    func1()

func0()  <--
--------------------------------------------
In func0() at misc/demo_excepthook.py:19:
--------------------------------------------
    func2()

@ti.kernel
def func0():
    func1()  <--

func0()
--------------------------------------------
In func1() at misc/demo_excepthook.py:15:
--------------------------------------------
    func3()

@ti.func
def func1():
    func2()  <--

@ti.kernel
--------------------------------------------
In func2() at misc/demo_excepthook.py:11:
--------------------------------------------
    ti.static_assert(1 + 1 == 3)

@ti.func
def func2():
    func3()  <--

@ti.func
--------------------------------------------
In func3() at misc/demo_excepthook.py:7:
--------------------------------------------
ti.enable_excepthook()

@ti.func
def func3():
    ti.static_assert(1 + 1 == 3)  <--

@ti.func
--------------------------------------------
AssertionError

看到了吧?我们的异常挂钩(exception hook)已经从回溯中删除了一些无用的 Taichi 内部堆栈帧。更重要的是,虽然在文档中不可见,但这些输出都是 彩色 的!

注解

对于 IPython / Jupyter notebook 的用户,当 ti.enable_excepthook() 触发时,IPython 原有的堆栈回溯挂钩将被 Taichi 取代。

调试技巧

即使有上面的内置工具,调试 Taichi 程序也可能会很难。在这里,我们展示了一些 Taichi 程序中可能会遇到的常见错误。

静态类型系统

Taichi 作用域中的 Python 代码被翻译成静态类型语言以实现高性能。这意味着Taichi 作用域中的代码与 Python 作用域中的代码可以有不同的行为,尤其是在类型方面。

变量的类型只 在初始化时确定,并且之后不会做更改

虽然Taichi的静态类型系统提供更好的性能,但如果程序员不小心使用了错误的类型,它可能会导致错误。例如,

@ti.kernel
def buggy():
    ret = 0  # 0 是整数, 所以 `ret` 类型是 int32
    for i in range(3):
        ret += 0.1 * i  # i32 += f32,结果依旧储存在 int32!
    print(ret)  # 会显示 0

buggy()

上面的代码显示了由于Taichi的静态类型系统而导致的常见错误。Taichi编译器应显示以下警告:

[W 06/27/20 21:43:51.853] [type_check.cpp:visit@66] [$19] Atomic add (float32 to int32) may lose precision.

这意味着Taichi不能将 float32 结果精确存储到 int32 。解决方案是初始化 ret 作为浮点值:

@ti.kernel
def not_buggy():
    ret = 0.0  # 0 是浮点数, 所以 `ret` 类型是 float32
    for i in range(3):
        ret += 0.1 * i  # f32 += f32. 成立!
    print(ret)  # 会显示 0.6

not_buggy()

高级优化

Taichi有一个先进的优化引擎,可以使你的Taichi内核是尽可能快。但是,就像 gcc -O3 一样,高级优化偶尔会导致错误,因为它过于努力了。这包括运行时错误,例如:

`RuntimeError: [verify.cpp:basic_verify@40] stmt 8 cannot have operand 7.`

你可以使用 ti.init(advanced_optimization=False) 关闭高级优化,并查看问题是否仍然存在:

import taichi as ti

ti.init(advanced_optimization=False)

...

无论是否关闭优化修复了问题,请随时在 GitHub 上报告此 Bug。谢谢!