元编程

Taichi 为元编程提供了基础架构。元编程可以

  • 统一依赖维度的代码开发工作,例如2维/3维的物理仿真
  • 通过将运行时开销转移到编译时来提高运行时的性能
  • 简化 Taichi 标准库的开发

Taichi 内核是 惰性实例化 的,并且很多有计算可以发生在 编译时。即使没有模板参数,Taichi 中的每一个内核也都是模板内核。

模版元编程

你可以使用 ti.template() 作为类型提示来传递一个张量作为参数。例如:

@ti.kernel
def copy(x: ti.template(), y: ti.template()):
    for i in x:
        y[i] = x[i]

a = ti.var(ti.f32, 4)
b = ti.var(ti.f32, 4)
c = ti.var(ti.f32, 12)
d = ti.var(ti.f32, 12)
copy(a, b)
copy(c, d)

如上例所示,模板编程可以使我们复用代码,并提供了更多的灵活性。

使用组合索引(grouped indices)的对维度不依赖的编程

然而,上面提供的 copy 模板函数并不完美。例如,它只能用于复制1维张量。如果我们想复制2维张量呢?那我们需要再写一个内核吗?

@ti.kernel
def copy2d(x: ti.template(), y: ti.template()):
    for i, j in x:
        y[i, j] = x[i, j]

没有必要!Taichi 提供了 ti.grouped 语法,使你可以将 for 循环索引打包成一个分组向量,以统一不同维度的内核。例如:

@ti.kernel
def copy(x: ti.template(), y: ti.template()):
    for I in ti.grouped(y):
        # I is a vector with same dimensionality with x and data type i32
        # If y is 0D, then I = ti.Vector([]), which is equivalent to `None` when used in x[I]
        # If y is 1D, then I = ti.Vector([i])
        # If y is 2D, then I = ti.Vector([i, j])
        # If y is 3D, then I = ti.Vector([i, j, k])
        # ...
        x[I] = y[I]

@ti.kernel
def array_op(x: ti.template(), y: ti.template()):
    # if tensor x is 2D:
    for I in ti.grouped(x): # I is simply a 2D vector with data type i32
        y[I + ti.Vector([0, 1])] = I[0] + I[1]

    # then it is equivalent to:
    for i, j in x:
        y[i, j + 1] = i + j

张量元数据

有时获取张量的数据类型( tensor.dtype )和形状( tensor.shape )是很有用的。这些属性值在 Taichi 作用域和 Python 作用域中都可以访问到。

@ti.func
def print_tensor_info(x: ti.template()):
  print('Tensor dimensionality is', len(x.shape))
  for i in ti.static(range(len(x.shape))):
    print('Size alone dimension', i, 'is', x.shape[i])
  ti.static_print('Tensor data type is', x.dtype)

参阅 Tensors of scalars 以获得更多细节。

注解

对稀疏张量而言,此处会返回其完整域的形状(full domain shape)。

矩阵 & 向量元数据

获得矩阵的行和列数将有利于你编写不依赖维度的代码。例如,这可以用来统一2维和3维物理模拟器的编写。

matrix.m 等于矩阵的列数,而 matrix.n 等于矩阵的行数。同时向量被认为是只有一列的矩阵,vector.n 就是向量的维数。

@ti.kernel
def foo():
  matrix = ti.Matrix([[1, 2], [3, 4], [5, 6]])
  print(matrix.n)  # 3
  print(matrix.m)  # 2
  vector = ti.Vector([7, 8, 9])
  print(vector.n)  # 3
  print(vector.m)  # 1

编译时求值(Compile-time evaluations)

编译时计算的使用将允许在内核实例化时进行部分计算。这节省了运行时计算的开销。

  • 使用 ti.static 对编译时分支展开(对 C++17 的用户来说,这相当于是 if constexpr
enable_projection = True

@ti.kernel
def static():
  if ti.static(enable_projection): # 没有运行时开销
    x[0] = 1
  • 使用 ti.static 强制循环展开(forced loop unrolling)
@ti.kernel
def func():
  for i in ti.static(range(4)):
      print(i)

  # 相当于:
  print(0)
  print(1)
  print(2)
  print(3)

何时使用 ti.static 来进行for循环

这是一些为何应该在 for 循环时使用 ti.static 的原因。

  • 循环展开以提高性能。
  • 对向量/矩阵的元素进行循环。矩阵的索引必须为编译时常量。张量的索引可以为运行时变量。例如,如果 x 是由3维向量组成的1维张量,并可以 x[tensor_index][matrix_index] 的形式访问。第一个索引(tensor_index)可以是变量,但是第二个索引(matrix_index)必须是一个常量。

例如,向量张量(tensor of vectors)的重置代码应该为

@ti.kernel
def reset():
  for i in x:
    for j in ti.static(range(x.n)):
      # 内部循环必须被展开, 因为 j 是向量索引
      # 而不是全局张量索引
      x[i][j] = 0