Kernels and functions
Taichi is a domain-specific language embedded in Python. The meaning of "embedded in Python" is twofold: The code you've written in Taichi is valid Python code but is compiled and executed in Taichi's runtime; the rest of your code is treated as native Python code and executed in Python's virtual machine.
To differentiate the code for Taichi from the code for Python, we use the two decorators @ti.kernel
and @ti.func
:
- Functions decorated with
@ti.kernel
are called kernels. - Functions decorated with
@ti.func
are called Taichi functions.
Taichi scope vs. Python scopeâ
Taichi introduces the two terms "Taichi scope" and "Python scope" to make it easy to define the call range of a function or specify the effective range of a variable. So, before you proceed, familiarize yourself with these two terms.
Taichi scopeâ
The code inside a kernel or a Taichi function is in the Taichi scope. The code in the Taichi scope is compiled by Taichi's runtime and executed in parallel on CPU or GPU devices for high-performance computation.
note
Taichi scope corresponds to the device side in CUDA.
Python scopeâ
Code outside of the Taichi scope is in the Python scope. The code in the Python scope is native Python and executed by Python's virtual machine, not by Taichi's runtime.
note
Python scope corresponds to the host side in CUDA.
Kernelâ
A kernel is the entry point from which Taichi's runtime takes control and the smallest unit for runtime execution. You can define multiple kernels in your program, and kernels are independent from each other. You call a kernel the same way you call a Python function, and you are allowed to switch back and forth between Taichi's runtime and Python's virtual machine.
Taichi's runtime compiles and executes kernels in the order you call them. It stores compiled kernels in the cache so that the next call to the same kernel does not need to be compiled again.
WARNING
You must not call a kernel from inside another kernel or from inside a Taichi function. You can only call a kernel directly or from inside a native Python function. To put it differently, you can only call a kernel from the Python scope.
note
A kernel corresponds to the __global__
function in CUDA.
Argumentsâ
A kernel can take multiple arguments, supporting scalar, ti.Matrix
, and ti.Vector
as argument types. This makes it easier and more flexible to pass values from the Python scope to the Taichi scope.
WARNING
Arguments in scalar, ti.Matrix
, or ti.Vector
are passed by value, so changes to the arguments of a kernel do not affect the original variables in the caller function.
Follow these rules when defining arguments:
- Type hint kernel arguments.
- Ensure that the total number of elements in the kernel arguments does not exceed a certain upper limit (see below).
Type hint kernel argumentsâ
@ti.kernel
def my_kernel(x: ti.i32, y: ti.f32):
print(x + y)
my_kernel(24, 3.2) # The system prints 27.2
Ensure that the total number of elements in the kernel arguments does not exceed a certain upper limitâ
The upper limit for element numbers is backend-specific:
- 8 on OpenGL backend
- 64 on CPU, Vulkan, CUDA, or Metal
note
- The number of elements in a scalar argument is always 1.
- The number of the elements in a
ti.Matrix
or in ati.Vector
is the actual number of scalars inside of them.
@ti.kernel
def valid_scalar_argument(vx: ti.f32, vy: ti.f32):
v = ti.Vector([vx, vy])
...
@ti.kernel
def valid_matrix_argument(u: ti.i32, v: ti.types.matrix(2, 2, ti.i32)): # OK: has five elements in total
...
@ti.kernel
def error_too_many_arguments(u: ti.i32, v: ti.i64, w: ti.types.matrix(7, 9, ti.i64)): # Error: has 65 elements in total
...
Advanced arguments
Return valueâ
A kernel can have at most one return value, which can be a scalar, ti.Matrix
, or ti.Vector
. Follow these rules when defining the return value of a kernel:
- Type hint the return value of a kernel.
- Ensure that you have at most one return value in a kernel.
- Ensure that you have at most one return statement in a kernel.
- Ensure that the number of elements in the return value does not exceed 30.
Type hint the return value of a kernelâ
@ti.kernel
def test(x: ti.f32) -> ti.f32: # The return value is type hinted
return 1.0
In addition, the return value is automatically cast into the hinted type:
@ti.kernel
def my_kernel() -> ti.i32: # int32
return 128.32
print(my_kernel()) # 128, the return value is cast into ti.i32
At most one return value in a kernelâ
@ti.kernel
def error_multiple_return() -> (ti.i32, ti.f32):
x = 1
y = 0.5
return x, y # Compilation error: more than one return value
At most one return statement in a kernelâ
@ti.kernel
def test_sign(x):
if x >= 0:
return 1.0
else:
return -1.0
# Error: multiple return statements
As a workaround, you can save the result in a local variable and return it at the end:
@ti.kernel
def test_sign(x):
sign = 1.0
if x < 0:
sign = -1.0
return sign
# One return statement works fine
At most 30 elements in the return valueâ
N = 6
matN = ti.types.matrix(N, N, ti.i32)
@ti.kernel
def test_kernel() -> matN:
return matN([[0] * N for _ in range(N)])
# Compilation error: The number of elements is 36 > 30
Global variables are compile-time constantsâ
A kernel treats global variables as compile-time constants. This means that it takes in the current values of the global variables at the time it is compiled and that it does not track changes to them afterwards. Then, if the value of a global variable is updated between two calls of the same kernel, the second call does not use the updated value.
Let's take a look at the following example, where the global variable a
is updated after the first call of kernel_1
.
- Because
kernel_1
does not track changes toa
after it is compiled, the second call ofkernel_1
still prints1
. - Because
kernerl_2
is compiled aftera
is updated, it takes in the current value ofa
and prints2
.
import taichi as ti
ti.init()
a = 1
@ti.kernel
def kernel_1():
print(a)
@ti.kernel
def kernel_2():
print(a)
kernel_1() # 1
a = 2
kernel_1() # 1
kernel_2() # 2
Taichi functionâ
Taichi functions are the building blocks of a kernel. All Taichi functions are force-inlined. Therefore, no runtime recursion is allowed.
WARNING
You must call a Taichi function from inside a kernel or from inside another Taichi function. In other words, you must call a Taichi function from within the Taichi scope, not from within the Python scope.
note
A Taichi function corresponds to the __device__
function in CUDA.
The following example shows the difference between a kernel and a Taichi function:
# a normal python function
def foo_py():
print("I'm a python function")
@ti.func
def foo_1():
print("I'm a taichi function called by another taichi function")
@ti.func
def foo_2():
print("I'm a taichi function called by a kernel")
foo_1()
@ti.kernel
def foo_kernel():
print("I'm a kernel calling a taichi function")
foo_2()
foo_py()
#foo_func() # You cannot call a taichi function from within the python scope
foo_kernel()
Argumentsâ
A Taichi function can have multiple arguments, supporting scalar, ti.Matrix
, and ti.Vector
as argument types. Note that the restrictions applied to a kernel's arguments do not apply here:
- You are not required (but it is still recommended) to type hint arguments.
- You can have an unlimited number of elements in the arguments.
WARNING
Arguments in scalar, ti.Matrix
, or ti.Vector
are passed by value, so changes to the arguments of a Taichi function do not affect the original variables in the caller function. See the following example:
@ti.func
def my_func(x):
x = x + 1 # Will not change the original value of x
@ti.kernel
def my_kernel():
x = 24
my_func(x)
print(x) # 24
Advanced arguments
Return valuesâ
The return values of a Taichi function can be scalar, ti.Matrix
, ti.Vector
, ti.Struct
, and more. Note that:
- Unlike a kernel, a Taichi function can have multiple return values.
- You do not need (but it is still recommended) to type hint the return values of a Taichi function.
- There is no limit on the number of elements in the return values.
However, you cannot have more than one return
statement in a Taichi function.
At most one return statementâ
You can only have one return statement in a Taichi function.
@ti.func
def test_sign(x):
if x >= 0:
return 1.0
else:
return -1.0
# Error: multiple return statements
As a workaround, you can save the result in a local variable and return it at the end:
@ti.func
def test_sign(x):
sign = 1.0
if x < 0:
sign = -1.0
return sign
# One return statement works just fine
A recap: Taichi kernel vs. Taichi functionâ
Kernel | Taichi Function | |
---|---|---|
Call scope | Python scope | Taichi scope |
Type hint arguments | Required | Recommended |
Type hint return values | Required | Recommended |
Return type | Scalar/ti.Vector /ti.Matrix | Scalar/ti.Vector /ti.Matrix /ti.Struct /... |
Maximum number of elements in arguments |
| Unlimited |
Maximum number of return values in a return statement | 1 | Unlimited |
Maximum number of elements in return values | 30 | Unlimited |
Key termsâ
Backendâ
In the computer world, the term backend may have different meanings based on the context, and generally refers to any part of a software program that users do not directly engage with. In the context of Taichi, backend is the place where your code is being executed, for example cpu
, opengl
, cuda
, and vulkan
.
Compile-time recursionâ
Compile-time recursion is a technique of meta-programming. The recursion is handled by Taichi's compiler and expanded and compiled into a serial function without recursion. The recursion conditions must be constant during compile time, and the depth of the recursion must be a constant.
Force inlineâ
Force inline means that the users cannot choose whether to inline a function or not. The function will always be expanded into the caller by the compiler.
Metaprogrammingâ
Metaprogramming generally refers to the manipulation of programs with programs. In the context of Taichi, it means generating actual-running programs with compile-time computations. In many cases, this allows developers to minimize the number of code lines to express a solution.
Runtime recursionâ
Runtime recursion is the kind of recursion that happens at runtime. The compiler does not expand the recursion, and it is compiled into a function that calls itself recursively. The recursion conditions are evaluated at runtime, and the depth does not need to be a constant number.
Type hintâ
Type hinting is a formal solution to statically indicate the type of value within your code.
FAQâ
Can I call a kernel from within a Taichi function?â
No. Keep in mind that a kernel is the smallest unit for Taichi's runtime execution. You cannot call a kernel from within a Taichi function (in the Taichi scope). You can only call a kernel from the Python scope.