调试

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

内核中的运行时 print

print(arg1, , sep=’ ‘, end=’n’)

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

  1. @ti.kernel
  2. def inside_taichi_scope():
  3. x = 233
  4. print('hello', x)
  5. #=> hello 233
  6. print('hello', x * 2 + 200)
  7. #=> hello 666
  8. print('hello', x, sep='')
  9. #=> hello233
  10. print('hello', x, sep='', end='')
  11. print('world', x, sep='')
  12. #=> hello233world233
  13. m = ti.Matrix([[2, 3, 4], [5, 6, 7]])
  14. print('m =', m)
  15. #=> m = [[2, 3, 4], [5, 6, 7]]
  16. v = ti.Vector([3, 4])
  17. print('v =', v)
  18. #=> 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() 被调用:

  1. import taichi as ti
  2. ti.init(arch=ti.cuda)
  3. @ti.kernel
  4. def kern():
  5. print('inside kernel')
  6. print('before kernel')
  7. kern()
  8. print('after kernel')
  9. ti.sync()
  10. print('after sync')

得到:

  1. before kernel
  2. after kernel
  3. inside kernel
  4. after sync

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

注解

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

  1. import taichi as ti
  2. ti.init(arch=ti.cpu)
  3. a = ti.var(ti.f32, 4)
  4. @ti.kernel
  5. def foo():
  6. a[0] = 1.0
  7. print('a[0] = ', a[0]) # 正确
  8. print(f'a[0] = {a[0]}') # 错误,不支持 f-string
  9. print("a[0] = %f" % a[0]) # 错误, 不支持格式化字符串
  10. foo()

编译时 ti.static_print

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

  1. x = ti.var(ti.f32, (2, 3))
  2. y = 1
  3. @ti.kernel
  4. def inside_taichi_scope():
  5. ti.static_print(y)
  6. # => 1
  7. ti.static_print(x.shape)
  8. # => (2, 3)
  9. ti.static_print(x.dtype)
  10. # => DataType.float32
  11. for i in range(4):
  12. ti.static_print(i.dtype)
  13. # => DataType.int32
  14. # 只会打印一次

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

内核中的运行时 assert

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

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

  1. ti.init(arch=ti.cpu, debug=True)
  2. x = ti.var(ti.f32, 128)
  3. @ti.kernel
  4. def do_sqrt_all():
  5. for i in x:
  6. assert x[i] >= 0
  7. 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 一样。

例如:

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

优雅的 Taichi 作用域的栈回溯

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

  1. import taichi as ti
  2. ti.init()
  3. @ti.func
  4. def func3():
  5. ti.static_assert(1 + 1 == 3)
  6. @ti.func
  7. def func2():
  8. func3()
  9. @ti.func
  10. def func1():
  11. func2()
  12. @ti.kernel
  13. def func0():
  14. func1()
  15. func0()

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

  1. Traceback (most recent call last):
  2. File "misc/demo_excepthook.py", line 20, in <module>
  3. func0()
  4. File "/root/taichi/python/taichi/lang/kernel.py", line 559, in wrapped
  5. return primal(*args, **kwargs)
  6. File "/root/taichi/python/taichi/lang/kernel.py", line 488, in __call__
  7. self.materialize(key=key, args=args, arg_features=arg_features)
  8. File "/root/taichi/python/taichi/lang/kernel.py", line 367, in materialize
  9. taichi_kernel = taichi_kernel.define(taichi_ast_generator)
  10. File "/root/taichi/python/taichi/lang/kernel.py", line 364, in taichi_ast_generator
  11. compiled()
  12. File "misc/demo_excepthook.py", line 18, in func0
  13. func1()
  14. File "/root/taichi/python/taichi/lang/kernel.py", line 39, in decorated
  15. return fun.__call__(*args)
  16. File "/root/taichi/python/taichi/lang/kernel.py", line 79, in __call__
  17. ret = self.compiled(*args)
  18. File "misc/demo_excepthook.py", line 14, in func1
  19. func2()
  20. File "/root/taichi/python/taichi/lang/kernel.py", line 39, in decorated
  21. return fun.__call__(*args)
  22. File "/root/taichi/python/taichi/lang/kernel.py", line 79, in __call__
  23. ret = self.compiled(*args)
  24. File "misc/demo_excepthook.py", line 10, in func2
  25. func3()
  26. File "/root/taichi/python/taichi/lang/kernel.py", line 39, in decorated
  27. return fun.__call__(*args)
  28. File "/root/taichi/python/taichi/lang/kernel.py", line 79, in __call__
  29. ret = self.compiled(*args)
  30. File "misc/demo_excepthook.py", line 6, in func3
  31. ti.static_assert(1 + 1 == 3)
  32. File "/root/taichi/python/taichi/lang/error.py", line 14, in wrapped
  33. return foo(*args, **kwargs)
  34. File "/root/taichi/python/taichi/lang/impl.py", line 252, in static_assert
  35. assert cond
  36. AssertionError

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

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

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

这样结果会是:

  1. ========== Taichi Stack Traceback ==========
  2. In <module>() at misc/demo_excepthook.py:21:
  3. --------------------------------------------
  4. @ti.kernel
  5. def func0():
  6. func1()
  7. func0() <--
  8. --------------------------------------------
  9. In func0() at misc/demo_excepthook.py:19:
  10. --------------------------------------------
  11. func2()
  12. @ti.kernel
  13. def func0():
  14. func1() <--
  15. func0()
  16. --------------------------------------------
  17. In func1() at misc/demo_excepthook.py:15:
  18. --------------------------------------------
  19. func3()
  20. @ti.func
  21. def func1():
  22. func2() <--
  23. @ti.kernel
  24. --------------------------------------------
  25. In func2() at misc/demo_excepthook.py:11:
  26. --------------------------------------------
  27. ti.static_assert(1 + 1 == 3)
  28. @ti.func
  29. def func2():
  30. func3() <--
  31. @ti.func
  32. --------------------------------------------
  33. In func3() at misc/demo_excepthook.py:7:
  34. --------------------------------------------
  35. ti.enable_excepthook()
  36. @ti.func
  37. def func3():
  38. ti.static_assert(1 + 1 == 3) <--
  39. @ti.func
  40. --------------------------------------------
  41. AssertionError

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

注解

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

调试技巧

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

静态类型系统

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

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

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

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

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

  1. [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 作为浮点值:

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

高级优化

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

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

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

  1. import taichi as ti
  2. ti.init(advanced_optimization=False)
  3. ...

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