PTX TableGen#
This guide follows add.rn.f32 from a TVMScript call to generated inline
PTX. TIRx uses table-driven generation: one instruction description defines its
operands and modifiers, and the backend uses that description to check calls
and generate CUDA helpers. We will inspect this description and its generated
code, then see how to add support for another instruction form.
Operands and Modifiers#
The direct PTX example adds two float32 values with a writable destination:
T.ptx.add.rn.f32(result, A[tid], B[tid])
The instruction descriptions live in
python/tvm_tirx/backend/cuda/ptx/table.py.
Each InstructionEntry records the operand order and allowed modifier
values. The floating-point entry TABLE["add"] covers this form.
tokens_for checks the chosen modifiers and puts them in slot order:
from tvm_tirx.backend.cuda.ptx.table import TABLE, tokens_for
entry = TABLE["add"]
tokens = tokens_for(entry, rnd="rn", type="f32")
modifiers = {slot.name: token for slot, token in zip(entry.slots, tokens) if token}
print(entry.op_name)
print([operand.name for operand in entry.operands])
print(modifiers)
The output is:
tirx.ptx.add
['d', 'a', 'b']
{'rnd': 'rn', 'type': 'f32'}
tirx.ptx.add is the IR operator name for this entry. result
corresponds to destination d; A[tid] and B[tid]
correspond to sources a and b. The rnd slot selects rn
(round to nearest, ties to even), and the type slot selects f32.
The printed modifier map contains the choices used by this call.
Generated CUDA Helper#
python/tvm_tirx/backend/cuda/ptx/engine.py
registers the table entries as IR operators and constructs an ir.Call
with result type void for the instruction. Its arguments include the
destination and both inputs. During CUDA source generation, the backend uses
python/tvm_tirx/backend/cuda/ptx/render.py
to emit a helper for the selected modifiers and operand types:
__forceinline__ __device__ void tvm_builtin_ptx_add_rn_f32(
float& __d, float __a, float __b) {
asm volatile("add.rn.f32 %0, %1, %2;"
: "=f"(__d) : "f"(__a), "f"(__b));
}
The generated kernel calls it with the result’s storage and the two input values:
tvm_builtin_ptx_add_rn_f32(
result_ptr[0], A_ptr[((int)threadIdx.x)], B_ptr[((int)threadIdx.x)]);
The reference parameter __d refers to result_ptr[0]. The "=f"
constraint writes that destination, while the two "f" constraints read
__a and __b. This is how a void instruction call produces a result
through its destination argument. The next tensor store writes it to
C[tid] in the original example.
To inspect the helpers generated from this entry, run:
python -m tvm_tirx.backend.cuda.ptx.gen_helpers add -o /tmp/ptx_add.cuh
Extend the instruction table#
If a kernel needs a PTX form that T.ptx does not yet recognize, the
backend needs a description of its operands and modifiers. That description
allows the backend to recognize the call, check its arguments, and generate
the corresponding CUDA helper.
The full add entry already supports our example. A reduced, standalone
description of add.rn.f32 lets us inspect the fields we would use for
a new form and the helper source they produce:
from tvm_tirx.backend.cuda.ptx.render import render_variant
from tvm_tirx.backend.cuda.ptx.table import (
InstructionEntry,
ModifierSlot,
OperandSlot,
tokens_for,
)
demo = InstructionEntry(
name="add_demo",
mnemonic="add",
operands=(
OperandSlot("d", rw="w"),
OperandSlot("a"),
OperandSlot("b"),
),
slots=(
ModifierSlot("rnd", ("rn",)),
ModifierSlot("type", ("f32",)),
),
cert_arch="sm_90",
)
tokens = tokens_for(demo, rnd="rn", type="f32")
opcode, helper, source = render_variant(demo, tokens)
print(opcode)
print(helper)
The output is:
add.rn.f32
tvm_builtin_ptx_add_demo_rn_f32
mnemonic sets add in the emitted instruction, while name
identifies the description and appears in the helper name. rw="w"
marks d as the output, and the slots restrict this description to
rn.f32. The returned source is the CUDA C++ helper definition as a
string, containing the inline PTX statement shown earlier. At this stage,
demo is a standalone description used to generate that string.
To make an unsupported form available through T.ptx, add its
InstructionEntry to _ENTRIES in table.py. The backend registers
the table entries as IR operators and code-generation callbacks when it
loads them. If an existing entry already has the required operand
arrangement, extend its modifier choices and rules. Use check for
rules involving several modifiers, and imm_check for restrictions on
constant operand values. A different operand arrangement can use another
entry with the same mnemonic.
The new form then needs its own test in
tests/python/tirx/codegen/test_ptx_dialect.py
that constructs a T.ptx call in a small kernel, checks the generated
instruction, and checks that invalid inputs are rejected. Use the file’s
_assert_ptxas_ok helper on the generated CUDA source to compile the
kernel to a CUBIN with NVCC. The caller ensures the new instruction reaches
assembly, where its validity is checked for the selected GPU architecture.
The entry’s cert_arch selects a target supporting all its variants
for the table-wide assembly tests.
The PTX reference covers additional entry fields, special operands, and stub-generation tools.