常用鏈接(上面的論壇帖子里也有)
- GAMES 的主頁 games-cn.org 【201相關鏈接在:導航欄/在線課程/GAMES201】
- Taichi 論壇 forum.taichi.graphics/c/games201/ ; 記得在 GAMES201的版塊發帖,其余板塊用的是英文
- 課程課件 github.com/taichi-dev/games201/releases
- Taichi 主頁 github.com/taichi-dev/taichi
- Taichi 中文文檔 taichi.readthedocs.io/zh_CN/latest
- 直播地址 webinar.games-cn.org
Taichi
是基於python開發的高性能圖形編程語言,為了解決python速度較慢的問題,特別設計了編譯器,進一步提高了圖形編程應用的生產力和可移植性。按Boss胡的說法是你只要裝好了Taichi,別人的代碼你copy過來基本上就可以直接用了,而不像OpenGL的一些你還得安裝很多其他依賴庫才能跑代碼。
Taichi還有如下特點:
- 數據導向、並行性,megakernels
- 將數據結構和計算解耦
- Access spatially sparse tensors as if they are dense
- 支持可微編程
下面對Taichi做正式介紹。
1. 安裝Taichi
python3 -m pip install taichi
2. 初始化
在執行任何Taichi操作之前都需要使用對Taichi做初始化操作,例如:
import taichi as ti
ti.init(arch=ti.cuda)
上面的arch
參數可以制定如下幾種backend:
ti.cpu
(默認值)ti.x64/arm/cuda/opengl/metal
ti.gpu
3. 數據類型
Taichi先支持如下幾種
- Signed integers:
ti.i8 / i16 / i32 / i164
- Unsigned integers:
ti.u8 / u16 / u32 / u64
- Float-point numbers:
ti.f32 / f64
ti.i32
和ti.f32
是最常用的數據類型,目前的布爾類型使用ti.i32
表示。
4. Tensors
Tensor也就是張量,它是Taichi里的第一公民(first-citizen)。其實在Pytorch和TensorFlow其實也用了這個概念,不過在Taichi有丟丟不一樣:
- Tensor其實是一個多維的數組(arrays)
- Tensor里的一個元素可以是標量(scalar),向量(vector),也可以是一個矩陣(matrix),但是在Pytorch和TensorFlow里,一個tensor里每一個元素就只是一個標量,這個在下面的代碼示例可以體會到區別。
- Tensor里的元素只能通過
a[i,j,k]
這樣的語法來訪問,像a[i][j][k]
這樣我通過實驗發現貌似只能訪問tensor某個元素內的某個子元素,具體看下面的例子 - 越界訪問是未定義的行為
- (高級功能)Tensor可以使空間稀疏的
舉例子:
import taichi as ti
ti.init()
a = ti.var(dt=ti.f32, shape=(4,3))
b = ti.Vector(3, dt=ti.f32, shape=(2,2))
c = ti.Matrix(2,2, dt=ti.f32, shape=(4,3))
loss = ti.var(dt=ti.f32, shape=())
a[2,2] = 1
print(f"a[2,2]={a[2,2]}")
b[1,1] = [0,2,4]
print(f"b[1,1]={b[1,1][0],b[1,1][1],b[1,1][2]}")
loss[None] = 3
print(f"loss[None] = {loss[None]}")
>>>
a[2,2]=1.0
b[1,1]=(0.0, 2.0, 4.0)
loss[None] = 3.0
上面的變量a
你不要把它理解成是一個標量,其實它是一個tensor,只不過它的每一個元素是標量而已。具體來說a
的維度是\(4\times3\),然后我們可以用a[2,2]
來對指定位置賦值;
同理b
也是一個大小為\(2\times2\)的tensor,只不過它的每一個元素是一個長度為3的vector。上面代碼中通過b[1,1]=[0,2,4]
來對[1,1]位置上的向量元素賦值,那么最后如何訪問這個向量呢?如上面的代碼所示,首先使用a[i,j]
來定位到向量元素,然后再像a[0]
來訪問向量里某個具體的元素。
變量c
是一個大小為\(4\times3\)的tensor,其中的每一個元素是一個大小為\(2\times2\)的矩陣。
變量loss
是一個只有一個元素的tensor,即0d的tensor,所以shape=()
,另外需要注意的是在訪問loss的值(讀/寫)時需要loss[None]=0
,因為我們都知道python語法里如果直接直接loss=0
,那么這個loss
就不再是原來的loss
了。
5. Kernels
在Taichi里,kernel
簡單理解就是用來計算的東西。Taichi的kernel語言是compiled, statically-typed, lexically-scoped,parallel and differentiable
你必須使用@ti.kernel
對函數裝飾后才能使用taichi的kernel功能。而且kernel的參數和返回值必須帶有類型提示,例如
@ti.kernel
def hello(i: ti.i32):
a = 40
print("Hello world!", a+i)
hello(2) # "Hello world! 42"
@ti.kernel
def calc(c: ti.i32): -> ti.i32
c += 1
return c
6. Functions
taichi里的functions可以被kernel或者其他的function調用,但是function不能調用kernel,kernel不能調用kernel。一個比較方便理解的方法就是ti.func
類似於cuda編程里的local function,而ti.kernel
則是global function。
例子:
@ti.func
def triple(x):
return x * 3
@ti.kernel
def triple_array(a):
for i in range(10):
a[i] = triple(a[i])
7. 數學函數
7.1 scalar math
7.2 Matrices and linear algebra
8. for-loops
Taichi里有兩種loop:
- Range-for loops:這個其實和python里的for循環沒什么區別,Range-for loops可以內嵌;
- Struct-for loops:因為Taichi支持稀疏化張量,而這個for循環就是為了解決稀疏張量的讀取問題。
8.1 range-for loops
For循環里的最外層在taichi kernel里會自動並行化。例如下面第一個例子中的for循環會自動並行化,而第二個例子中由於有一個if判斷語句,所以后面的for循環就不是並行化的了。
@ti.kernel
def foo():
for i in range(10): # Parallelized :-)
...
@ti.kernel
def bar(k: ti.i32):
if k > 42:
for i in range(10): # Serial :-(
...
8.2 Struct-for loops
import taichi as ti
ti.init(arch=ti.gpu)
n = 320
pixels = ti.var(dt=ti.f32, shape=(n * 2, n))
@ti.kernel
def paint(t: ti.f32):
for i, j in pixels:# Parallized over all pixels
pixels[i, j] = i * 0.001 + j * 0.002 + t
paint(0.3)
Struct-for loops會遍歷如下所有的tensor坐標:(0, 0), (0, 1), (0, 2), ..., (0, 319), (1, 0), ..., (639, 319)
9. 原子操作(Atomic operations)
在Taichi中,augmented assignments (比如 x[i] += 1) 會自動使用原子操作。
看下面的例子:
@ti.kernel
def sum():
for i in x:
# Approach 1: OK
total[None] += x[i]
# Approach 2: OK
ti.atomic_add(total[None], x[i])
# Approach 3: Wrong result (the operation is not atomic.)
total[None] = total[None] + x[i]
可以看到上面三種操作里只有前兩種是對的,最后一種是會有潛在問題的,因為它首先會先讀取total[None]
,然后再做加法操作,但是很有可能你在讀取的時候已經有其他的縣城對這個作了修改,換句話說第三種方式不是原子操作,所以是有問題的。
10. Taichi-scope v.s. Python-scope
Taichi-scope: 任何使用
ti.kernel
和tf.func
裝飾都屬於taichi scope,該scope內的代碼會使用Taichi編譯器編譯,然后可以運行在並行設備上。
Python-scope: taichi-scope以外的就是python scope
import taichi as ti
ti.init()
a=ti.var(dt=ti.f32,shape=(42,63)) # A tensor of 42x63 scalars
b=ti.Vector(3,dt=ti.f32,shape=4)#A tensor of 4x 3D vectors
C=ti.Matrix(2,2,dt=ti.f32,shape=(3,5)) # A tensor of 3x5 2x2 matrices
@ti.kernel
def foo():
a[3,4]=1
print('a[3,4]=',a[3,4])
#”a[3,4]=1.000000”
b[2]=[6,7,8]
print('b[0]=',b[0],',b[2]=',b[2])
#”b[0]=[[0.000000],[0.000000],[0.000000]],b[2]=[[6.000000],[7.000000],[8.000000]]”
C[2,1][0,1]=1
print('C[2,1]=',C[2,1])
#C[2,1]=[[0.000000,1.000000],[0.000000,0.000000]]
foo()
11. Taichi 程序的階段(phase)
一個Taichi代碼可以分成如下幾個階段:
- 初始化:
ti.init()
- tensor分配:
ti.var, ti.Vector, ti.Matrix
- 計算(啟動kernels, 獲取python-scope的tensors)
- 可選: 重啟Taichi系統
ti.reset()
,包括了清除內存、銷毀所有變量和kernels
目前版本中一旦啟動kernel或者訪問python-scope下的某個tensor后,就不能再allocate tensor。
import taichi as ti
ti.init()
n=320
pixels=ti.var(dt=ti.f32,shape=(n*2,n))
@ti.func
def complex_sqr(z):
return ti.Vector([z[0]**2-z[1]**2,z[1]*z[0]*2])
@ti.kernel
def paint(t:ti.f32):
for i,j in pixels:#Parallized over all pixels
c=ti.Vector([-0.8,ti.cos(t)*0.2])
z=ti.Vector([i/n-1,j/n-0.5])*2
iterations=0
while z.norm()<20 and iterations<50:
z=complex_sqr(z)+c
iterations+=1
pixels[i,j]=1-iterations*0.02
gui=ti.GUI("JuliaSet",res=(n*2,n))
for i in range(1000000):
paint(i*0.03)
gui.set_image(pixels)
gui.show()
親測有效:
12. debug mode
使用ti.init(debug=True, arch=ti.cpu)
可以幫助檢查諸如訪問越界等問題。
import taichi as ti
ti.init(debug=True, arch=ti.cpu)
a = ti.var(ti.i32, shape=(10))
b = ti.var(ti.i32, shape=(10))
@ti.kernel
def shift():
for i in range(10):
a[i] = b[i + 1] # Runtime error in debug mode
shift()