taichi学习笔记(1)-基础使用
taichi的安装非常简单,只需要在python环境中安装即可。
1 | |
本篇的内容均来自官网教程,主要记录的是一些关键信息,更多细节可以查看原始文档:
- Docs: https://docs.taichi-lang.org/docs/overview
Quick Start
Julia Fractal
第一个简单例子是使用taichi绘制Julia分形图案。
1 | |
效果如下:
如果在headless的服务器上,可以通过保存图像来并转化为video来进行可视化。
1 | |
Physical Simulation
以下程序可以用来完成一个物理仿真程序,它模拟了一块布料落到小球上的过程。
1 | |
最终的仿真效果如下:
关于该程序,除了注释,还有一些需要注意的地方:
- Arch的尝试顺序:如果指定了
ti.gpu,那么系统会以此尝试ti.cuda,ti.vulkan,ti.opengl/ti.Metal- 其中Vulkan是一个低开销,跨平台的二维/三维图形与计算的API,与OpenGL类似,Vulkan针对全平台即时3D图形程序而设计,并提供高性能与更均衡的CPU和GPU占用
for i in ti.grouped(x)是taichi 的一个重要特性。这意味着这个 for 循环会自动遍历x的所有元素,无论其形状如何,都是一个一维数组,省去了你编写多层 for 循环的麻烦,而返回的索引则基本是tuple的格式,能够一步索引到对应的元素
Core Concept Introduction
Kernels & Functions
taichi的语法与Python非常类似,但是并不完全相同。taichi提供了两个装饰器,分别对应taichi kernel和taichi function。
@ti.kernel:被修饰的函数称为taichi kernel,这些函数是taichi运行时运行任务的入口,必须要由python代码直接调用。@ti.func:被修饰的函数称为taichi function,这些函数是taichi kernel的组成部分,只能由另一个taichi function或者kernel调用。
有了这两个装饰器,我们就可以定义两个概念,taichi作用域和python作用域。在taichi kernel或者taichi function内部的代码都属于taichi作用域,这些代码在运行的时候会在多核CPU或者GPU上并行编译并执行,以实现高性能计算,可以对应CUDA中的device side;其余部份的代码则均属于python作用域,可以对应CUDA中的host side。
taichi 函数和taichi kernel不由Python的解释器执行,而是被JIT编译器接管并部署到并行多核CPU或者GPU上。
关于taichi kernels,有如下需要注意的地方:
taichi kernel是taichi执行的入口点,只能被Python作用域中的代码调用,不允许在taichi作用域中被调用。即无论是taichi kernel还是taichi function都不允许调用taichi kernel;
taichi kernel中的for循环会被自动并行处理,但是需要注意的是,只有最外层的for会被并行,嵌套的for不会被并行处理。同时被并行的循环不支持break,不过内层循环支持;
一个taichi kernel最多允许有一个返回值,最多允许一个返回语句;
taichi kernel的参数和返回值需要类型提示,如果没有参数或者没有返回值,则相应不需要类型提示;
taichi kernel可以接受多个参数,包括标量,ti.types中支持的类型,但是其中不包括Python中的objects
- 值传递的类型:标量、
ti.types.matrix(),ti.types.vector(),ti.types.struct(), - 引用传递的类型:
ti.types.ndarray(),ti.template()
- 值传递的类型:标量、
可以定义多个taichi kernel,这些kernel彼此独立,按照首次调用的顺序进行编译和执行,编译后的kernel会被缓存,以减少后续调用的启动开销;
如果taichi kernel中用到了python全局变量,会被视为编译时常量,即在kernel被编译的时候,这些变量的值在kernel就被记录为常量,而不会跟踪后续的更改;
关于taichi function,有如下需要注意的地方:
- taichi function只能在taichi作用域中被调用,即只能在taichi kernel或者taichi function中调用taichi function;
- taichi function并不支持函数内的递归调用,因为taichi function在编译的时候是inline展开,最终得到单个大型function。而递归调用会导致编译时函数调用堆栈的无限扩展;
- taichi function不强制需要类型提示,不过也建议使用;
- taichi function可以有多个返回值,但是同样不能有多个return语句
Type System
ti.types模块中定义了所有支持的数据类型,这些数据类型分为两类:原始数据类型和复合数据类型。
- 原始数据类型:包括常用的数字数据类型
- 复合数据类型:包括类似数组或者结构体的数据类型
taichi是一种静态类型编程语言,即taichi作用域中的变量类型在编译的时候被确定,一旦某个变量被声明了是某个类型(例如第一次被赋值),后续就始终维持该类型,不能被分配其他类型的值。如果赋值了不同类型的值,会首先尝试转换,如果无法自动转换,则会抛出错误。
当然也可以显式地进行数据类型转换。
taichi中的原始数据类型均为标量,例如ti.i32,ti.f32等,以下是不同后端支持的taichi原始数据类型说明,其中
○。表示需要后端扩展。
| Backend | i8 | i16 | i32 | i64 | u8 | u16 | u32 | u64 | f16 | f32 | f64 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| CPU | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| CUDA | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| OpenGL | ✗ | ✗ | ✓ | ○ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ | ✓ |
| Metal | ✓ | ✓ | ✓ | ✗ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | ✗ |
| Vulkan | ○ | ○ | ✓ | ○ | ○ | ○ | ✓ | ○ | ✓ | ✓ | ○ |
默认的原始数据类型是ti.i32和ti.f32,可以在调用init的时候指定默认的原始数据类型。同时python中的int和float,如果在对应指的就是taichi中默认的原始数据类型。
1 | |
taichi中的复合数据类型则是由用户定义的数据类型,由多个元素组成,支持的复合数据类型包括vector、matrix、ndarray和struct。
1 | |
- 注意利用
@ti.dataclass能够完成与ti.types.struct相同的事情,同时我们可以在这个类中定义成员函数,更好的实现面向对象编程(OOP) - 而关于类的初始化,则同时提供按照顺序,按照关键字参数进行初始化的功能。需要注意也支持部分指定,未指定的成员自动设置为0
- 复合类型中只有vector和matrix支持类型转换,其中类型转换是按照元素执行的
Data Containers
taichi中的数据容器是field,这个概念借用于数学和物理学。field是taichi中的一种全局数据容器,可以直接从python作用域和taichi作用域访问。taichi中的field被定义为某种元素的多维数组,字段的元素可以是标量scalar、向量vector、矩阵matrix或者结构体struct。
1 | |
- 注意taichi总仅支持维度小于等于8的field
1 | |
- vector field表示元素是矢量的vector,vector可能代表像素的RGB,可能代表粒子的位置,空间中的引力场等
xyzw,rgba是语法糖,不过需要满足向量的维度不能超过4
1 | |
- matrix field中元素是matrix,不过较大的矩阵会导致更长的编译时间和更差的性能
- 矩阵的维度尽可能保持小,但是后面的shape可以大,并且并行化就是体现在后面的shape
1 | |
- struct
field中的对应元素可以调用对应的方法,例如利用
.fill()方法填充所有的值
[Advanced] AoS和SoA
AoS和SoA表示两种不同的布局方式,分别是Array of Structures(AoS)和Structure of Arrays(SoA)。
考虑一个具有四个像素、每个像素具有RGB颜色通道的图像,AoS和SoA的内存布局方式如下:
1
2
3# address: low ...................... high
# AoS: RGBRGBRGBRGBRGBRGB.............
# SoA: RRRRR...RGGGGGGG...GBBBBBBB...B不同内存布局的使用主要看我们需要什么样的访问方式:
- 如果我们要计算每个像素的灰度,那么并行单位应该要访问RGB,这时候选择AoS布局具有更好的内存访问模式
- 如果我们要计算每个通道的均值,那么并行单位应该要访问通道的所有值,这个时候SoA布局则是更好的方式
taichi中提供了非常方便的方式在两种内存布局中转换:
1
2
3
4
5
6
7
8
9
10
11
12
13x = ti.field(ti.f32, shape=M)
y = ti.field(ti.f32, shape=M)
# SoA内存布局
ti.root.dense(ti.i, M).place(x)
ti.root.dense(ti.i, M).place(y)
# address: low ................................. high
# x[0] x[1] x[2] ... y[0] y[1] y[2] ...
# AoS内存布局
ti.root.dense(ti.i, M).place(x, y)
# address: low .............................. high
# x[0] y[0] x[1] y[1] x[2] y[2] ...
这里还需要简要地补充介绍一下taichi.ndarray,它是一个保存连续多维数据的数组对象,作用与numpy中的numpy.ndarray类似,但是底层内存分配在taichi上。在大多数情况下,我们可以使用field作为数据容器,但是field的内部结构布局可能比较复杂,外部库很难直接解释或者使用存储在ti.field中的计算结果。而ndarray则是中分配一个连续的内存块,以便与外部进行直接的数据交换。
简而言之,field主要用于在复杂数据布局下最大化性能,如果仅处理密集数据或需要外部互操作,那么只需使用taichi.ndarray即可。
如果要与外部的array进行交互:
1 | |
- 类似的方法包括
from_numpy()/from_torch(),该方法会将外部数组复制到field中 - 如果taichi
function或者kernel的参数使用了
ti.types.ndarray()作为类型提示,则也可以接收numpy或者torch数组,但是此时仅支持连续的数组,并且是引用传递
OOP Support
taichi本身是一种面向数据的编程语言(Data-Oriented Programming,DOP),这种面向数据的设计认为一切都是可以采取行动的数据,将功能和数据区分开来。DOP的方式使得模块化变得困难。为了实现模块化代码,taichi借用了一些面向对象编程(OOP)的概念,这种混合方案被称为面向对象的数据编程(Objective Data-Oriented Programming, ODOP)。
ODOP方案允许我们在Class类中组织数据和方法,并且调用方法在taichi作用域内操作数据。taichi提供了两种不同类型的方法来实现该目的,分别是@ti.data_oriented和@ti.dataclass。
@ti.data_oriented:适用于数据在python作用域内主动更新,而在taichi
kernel中进行跟踪。
1 | |
@ti.data_oriented的属性会随着类的继承而延续下去,如果一个类的任何祖先类具有该装饰器,则它可以调用对应的taichi kernel
@ti.dataclass:提供了更多的灵活性,可以将taichi
function定义为类本身的方法。
1 | |
- 不支持taichi dataclass的继承
Visualization
首先介绍最基础的taichi GUI,它可以用于可视化data container中的数据,同时对绘制原始几何图形提供了有限的支持。
GUI
这里需要注意的是,taichi采用标准笛卡尔坐标系定义像素坐标,即原点在左下角,向右是x的正方向,向上是y的正方向。
1 | |
调用set_image()方法的时候,GUI系统会将图像数据转化成可显示的格式,并将结果复制到窗口缓冲区,当窗口很大的时候,这会导致巨大的负载,难以实现高FPS
如果只需要调用set_image()方法而不使用其他任何绘图命令,则可以启用
fast_gui模式以获得更好的性能。此模式允许 taichi GUI 将图像数据直接写入帧缓冲区而无需额外复制,并显著提高 FPS
taichi GUI也支持事件处理,包括鼠标和键盘的控制方式。
1 | |
同时taichi GUI系统还提供了一些组件来自定义控制界面,这里就简单跳过
GGUI
然后是更高级的GUI系统,GGUI。它使用GPU进行渲染,使得3D场景的渲染速度更快。GGUI可以绘制的类型包括:
- 2D Canvas:用于绘制简单2D几何图形
- 3D Scene:渲染3D mesh和粒子,提供可配置的摄像头和光源
- GUI控件:包括按钮、文本框等
1 | |
- 其中,几何图形的位置是[0.0, 1.0]之间的浮点数,表示几何图形在画布上的相对位置;
- 每帧结束后画布都会被清除。需要始终在渲染循环中调用这些方法。
1 | |
- 其中几何图形的位置/中心应该位于世界空间坐标中;
- 需要在渲染循环中调用光源配置等方法;
camera.track_user_inputs提供一个非常直接的方式来控制相机(wasdeq来移动相机);- 这里的
vertex_offset,vertex_count,index_offset和index_count控制粒子和mesh的可见部分,可以用于渲染指定部分粒子/mesh
1 | |
更多的使用案例可以参考官方的examples。
Result Export
最后介绍一些将渲染结果导出为图像或者视频的功能。
1 | |
Debug
taichi提供了一些机制来进行debug:
1 | |