TVMScript
TVMScript is a Python-based domain-specific language (DSL) for writing TVM IR. It lets users
define IRModules — containing both Relax functions and TIR PrimFuncs — using
familiar Python syntax. Although TVMScript looks like Python, it is not executed by the
Python interpreter. Instead, Python decorators extract the AST from the source code and
transform it into TVM IR through a dedicated parser and IR builder pipeline.
TVMScript serves two roles in the TVM stack:
Authoring: users write TIR kernels and Relax programs directly in TVMScript.
Roundtrip: every
IRModulecan be printed back to TVMScript viamod.script()and re-parsed to produce an equivalent module. This makes TVMScript the primary tool for inspecting, debugging, and serializing IR.
Overview
The TVMScript system has three components:
Parsing (Python source → TVM IR):
Python source (TVMScript)
│
▼ ast.parse + convert
│
Doc AST (mirror of Python AST)
│
▼ Parser (dispatch by token: ir / tirx / relax)
│
▼ IR Builder (frame stack)
│
TVM IR (IRModule, PrimFunc, relax.Function)
Printing (TVM IR → Python source):
TVM IR
│
▼ IRDocsifier (C++, dispatch by token + type)
│
Doc tree (ExprDoc, StmtDoc, ...)
│
▼ DocToPythonScript
│
TVMScript text
Parser (Python): reads Python source, converts it to a
Doc AST(a mirror of Python’sastmodule), then walks the tree using dialect-specific handlers that call into the IR builder.IR Builder (Python + C++): provides a frame-stack API where each
withblock or decorator pushes a frame. When the frame exits, the constructed IR is finalized. The builder is shared across dialects — TIR and Relax each register their own frame types.Printer (C++): converts TVM IR objects to a
Doctree (an intermediate representation of Python syntax), then formats the tree into valid TVMScript text.
Decorators
TVMScript uses three import aliases by convention:
from tvm.script import ir as I # module-level constructs
from tvm.script import tirx as T # TIR constructs
from tvm.script import relax as R # Relax constructs
The primary decorators are:
@I.ir_module: marks a Python class as anIRModule. Each method inside becomes a function in the module.@T.prim_func: marks a function as a TIRPrimFunc.@R.function: marks a function as arelax.Function.
These can be composed:
@I.ir_module
class MyModule:
@T.prim_func
def add_kernel(A: T.Buffer((128,), "float32"),
B: T.Buffer((128,), "float32"),
C: T.Buffer((128,), "float32")):
for i in range(128):
with T.sblock("compute"):
vi = T.axis.spatial(128, i)
C[vi] = A[vi] + B[vi]
@R.function
def main(x: R.Tensor((128,), "float32"),
y: R.Tensor((128,), "float32")) -> R.Tensor((128,), "float32"):
with R.dataflow():
out = R.call_tir(cls.add_kernel, (x, y),
out_sinfo=R.Tensor((128,), "float32"))
R.output(out)
return out
When Python encounters @I.ir_module, the decorator does not execute the class body.
Instead, it calls tvm.script.parse() which extracts the source code of the class,
builds a Doc AST, and hands it to the parser.
Parser Architecture
The parser lives in python/tvm/script/parser/.
Dispatch mechanism
Different IR dialects (TIR, Relax) need different handling for the same Python syntax. For
example, if ... else inside @T.prim_func creates a TIR If branch, while the same
syntax inside @R.function creates a Relax If node with different semantics.
The parser maintains a dispatch token stack (["default"] initially). When it encounters
a decorated function, it inspects the decorator to determine the token — "tirx" for
@T.prim_func, "relax" for @R.function — and pushes it onto the stack.
Each AST node type is dispatched via a virtual table:
ParseVTable[(token, node_type)] → handler function
Lookup order:
1. (current_token, node_type) e.g. ("tirx", "For")
2. ("default", node_type) e.g. ("default", "For")
3. generic_visit fallback
Dialect-specific parsers (parser/tirx/parser.py, parser/relax/parser.py) register
handlers using @dispatch.register(token, type_name) decorators.
Parse flow
The entry point is parse(program, extra_vars):
Source extraction: the program’s source code is extracted (from a class, function, or string) and converted to a Doc AST via Python’s
astmodule.AST walking: the
Parser(a subclass ofdoc.NodeVisitor) walks the Doc AST. For each node, it looks up the handler in the dispatch table.Expression evaluation: expressions like
T.grid(128, 128)are evaluated by theExprEvaluator, which resolves names against the variable table and theT./R.module namespaces.Value binding: assignment statements (
A = T.match_buffer(...)in TIR,lv = R.add(x, y)in Relax) go through dialect-specificbind_*_value()functions that register the resulting TVM objects in the parser’sVarTable.Scoping: the
VarTablemaintains a stack of frames. Entering awithblock,forloop, or function body pushes a new frame; exiting pops it. This ensures variables are scoped correctly.
Variable table
The VarTable is the parser’s symbol table:
VarTable
├── frames: [VarTableFrame, ...] ← stack of scopes
└── name2value: {str: [Any, ...]} ← name → value stack (for shadowing)
When a name is looked up, the most recent binding wins. When a frame is popped, all bindings introduced in that frame are removed.
IR Builder Architecture
The IR builder (python/tvm/script/ir_builder/, backed by C++ in src/script/ir_builder/)
provides a frame-stack API for constructing IR incrementally.
Frame stack
The core idea: each IR scope (module, function, block, loop) is a frame. Frames are pushed
on __enter__ and popped on __exit__. When a frame exits, it finalizes the IR it
represents and attaches it to the parent frame.
IRBuilder (thread-local singleton)
└── frame stack:
├── IRModuleFrame ← @I.ir_module
│ ├── PrimFuncFrame ← @T.prim_func
│ │ ├── ForFrame ← T.grid(...) / T.serial(...)
│ │ │ └── SBlockFrame ← T.sblock(...)
│ │ └── ...
│ └── FunctionFrame ← @R.function
│ └── BindingBlockFrame ← R.dataflow()
└── ...
This design means the parser never needs to build a complete IR tree in memory — it constructs IR top-down by entering and exiting frames, and each frame handles its own finalization.
TIR builder
The TIR builder (ir_builder/tirx/ir.py) provides functions that map directly to TVMScript
syntax. Key categories:
Function and block:
T.prim_func()→PrimFuncFrameT.sblock(name)→SBlockFrame(spatial block)T.init()→BlockInitFrame(reduction initialization)T.reads(...),T.writes(...)→ declare buffer access regions
Loops:
T.grid(*extents)→ForFramereturning loop variablesT.serial(start, stop),T.parallel(...),T.vectorized(...),T.unroll(...),T.thread_binding(...)→ loop with specific iterator type
Block axes:
T.axis.spatial(dom, binding)— spatial iteration axisT.axis.reduce(dom, binding)— reduction axisT.axis.remap(kinds, bindings)— shorthand for multiple axes
Buffers:
T.match_buffer(param, shape, dtype)— match function parameter to bufferT.alloc_buffer(shape, dtype)— allocate intermediate bufferT.Buffer(shape, dtype)— buffer type annotation in function signatures
Relax builder
The Relax builder (ir_builder/relax/ir.py) provides:
Function and dataflow:
R.function()→FunctionFrameR.dataflow()→BindingBlockFrameR.output(*vars)→ expose variables from a dataflow block
Emit:
R.emit(value)→ emit a binding, returns aVarR.emit_match_cast(value, struct_info)→ emit with type assertion
Type annotations:
R.Tensor(shape, dtype)— tensor struct infoR.Tuple(*fields)— tuple struct infoR.Shape(values)— shape struct infoR.Object()— opaque object struct info
Calling conventions:
R.call_tir(func, args, out_sinfo)— call a TIR functionR.call_packed(name, *args)— call a PackedFuncR.call_dps_packed(func, *args)— call using destination-passing style
Operators: the R module also re-exports all Relax operators
(R.add, R.matmul, R.nn.conv2d, etc.) so they can be used directly in TVMScript.
Printer Architecture
The printer converts TVM IR back to TVMScript text. It is implemented primarily in C++
(src/script/printer/) for performance.
Doc tree
The printer does not generate text directly. Instead, it first builds a Doc tree — an
intermediate representation that mirrors Python syntax:
Expression docs:
IdDoc,AttrAccessDoc,CallDoc,IndexDoc,OperationDoc,LiteralDoc,TupleDoc,ListDoc, etc.Statement docs:
AssignDoc,ForDoc,IfDoc,ScopeDoc(withblocks),FunctionDoc,ClassDoc,ReturnDoc,CommentDoc, etc.
For example, T.axis.spatial(128, i) is represented as:
CallDoc(
callee=AttrAccessDoc(AttrAccessDoc(IdDoc("T"), "axis"), "spatial"),
args=[LiteralDoc(128), IdDoc("i")]
)
IRDocsifier
The IRDocsifier (include/tvm/script/printer/ir_docsifier.h) is the main dispatcher.
It maintains:
A dispatch table mapping
(token, type_index)pairs to converter functions.A frame stack for tracking the current scope (similar to the builder’s frame stack).
A variable-to-name mapping to produce readable names.
Each IR dialect registers its own converters:
src/script/printer/tirx/— converts PrimFunc, Buffer, SBlock, loops, expressions.src/script/printer/relax/— converts relax.Function, bindings, struct info, operators.src/script/printer/ir/— converts IRModule, shared types.
The final step calls DocToPythonScript() (src/script/printer/doc_printer/python_doc_printer.cc)
to format the Doc tree into properly indented Python text.
Roundtrip guarantee
For any IRModule constructed through the compiler:
text = mod.script() # IR → TVMScript text
reparsed = tvm.script.from_source(text) # text → IR
tvm.ir.assert_structural_equal(mod, reparsed)
This roundtrip property is relied upon by testing infrastructure and serialization workflows.
Note that the printed text may differ from hand-written TVMScript — the printer uses canonical
forms (e.g., explicit R.emit calls, fully qualified buffer annotations) that are not required
in hand-written code.
Supported Python Syntax
TVMScript supports a subset of Python syntax. The table below summarizes what is supported and how each construct is interpreted:
Python Syntax |
TIR |
Relax |
|---|---|---|
|
Serial loop nest |
Not supported (no Relax-level |
|
Spatial block scope |
N/A |
|
N/A |
Dataflow block |
|
TIR |
Relax |
|
|
Not supported |
|
Variable binding |
Emit binding (implicit |
|
Buffer annotation |
N/A |
|
N/A |
Struct info annotation |
|
Not used |
Function return value |
|
Buffer load |
Not applicable (use operators) |
|
Buffer store |
Not applicable |
Arithmetic ( |
PrimExpr operations |
Calls to Relax operators |
Function calls |
|
|
Not supported: class definitions (except for @I.ir_module), try/except,
yield, async/await, list comprehensions, lambda, import, and global
statements.
TIR Syntax Reference
Function definition
@T.prim_func
def func_name(a: T.handle, b: T.handle):
A = T.match_buffer(a, (m, n), "float32")
B = T.match_buffer(b, (m,), "float32")
# function body
T.handle— opaque handle parameter (matched to a buffer inside the function).T.Buffer(shape, dtype)— can also be used directly in the signature:def func(A: T.Buffer((128,), "float32")).
Block and axes
for i, j in T.grid(128, 128):
with T.sblock("block_name"):
vi = T.axis.spatial(128, i)
vj = T.axis.reduce(128, j)
T.reads(A[vi, vj])
T.writes(B[vi])
# compute
T.axis.spatial/T.axis.reduce/T.axis.scan— declare axis variables with their iteration domain and binding to outer loop variables.T.axis.remap("SR", [i, j])— shorthand:S= spatial,R= reduce.T.reads(...),T.writes(...)— declare buffer regions accessed by this block.
Loop types
for i in T.serial(0, 128): # sequential
for i in T.parallel(0, 128): # parallel
for i in T.vectorized(0, 128): # vectorized
for i in T.unroll(0, 128): # unrolled
for i in T.thread_binding(0, 128, thread="threadIdx.x"): # GPU thread
Buffer operations
C = T.alloc_buffer((128, 128), "float32") # intermediate buffer
val = A[i, j] # buffer load
B[i] = val + 1.0 # buffer store
Common intrinsics
T.exp(x), T.log(x), T.sqrt(x), T.tanh(x), ... # math functions
T.cast(x, "float16") # type cast
T.if_then_else(cond, true_val, false_val) # conditional expression
T.min(a, b), T.max(a, b) # min/max
T.call_extern("func_name", *args) # external function call
T.call_packed("func_name", *args) # packed function call
T.tvm_storage_sync("shared") # GPU memory fence
Relax Syntax Reference
Function definition
@R.function
def main(x: R.Tensor((128, 128), "float32"),
y: R.Tensor((128,), "float32")) -> R.Tensor((128, 128), "float32"):
# function body
return result
R.Tensor(shape, dtype)— tensor type annotation (struct info).R.Tuple(...),R.Shape(...),R.Object()— other struct info types.R.function(private=True)— marks the function as module-private.R.function(pure=False)— marks the function as having side effects.
Dataflow blocks
with R.dataflow():
lv0 = R.add(x, y)
lv1 = R.nn.relu(lv0)
R.output(lv1)
Variables inside a R.dataflow() block are local to that block. R.output(...) exposes
variables to the outer scope.
Calling TIR functions
out = R.call_tir(cls.my_kernel, (x, y), out_sinfo=R.Tensor((128,), "float32"))
cls.my_kernel— references a TIRPrimFuncin the same module.out_sinfo— the struct info (shape and dtype) of the output tensor.
Control flow
Relax if uses plain Python if syntax. The condition must be a Relax variable with
boolean type. Both branches are required.
@R.function
def f(cond: R.Tensor((), "bool"), x: R.Tensor((128,), "float32")):
if cond:
result = R.add(x, x)
else:
result = R.multiply(x, x)
return result
Source Code Map
Path |
Contents |
|---|---|
|
Core parser: dispatch, expression evaluator, variable table, Doc AST |
|
TIR-specific parser handlers and value binding |
|
Relax-specific parser handlers and value binding |
|
|
|
IRBuilder base class and frame stack mechanism |
|
TIR frame types and builder functions ( |
|
Relax frame types and builder functions ( |
|
IRModule builder ( |
|
C++ printer: Doc tree, IRDocsifier, Python code generation |
|
TIR-specific IR-to-Doc converters |
|
Relax-specific IR-to-Doc converters |
|
C++ backend for frame stack and IR construction |
|
C++ headers: Doc classes, IRDocsifier, dispatch functor |
|
C++ headers: builder base, dialect-specific frame types |