TT-Lang Introduction
TTNN covers a large territory of standard ops — matmul, attention, layernorm, convolution. But ML research moves faster than op libraries. The moment you want a fusion pattern that TTNN doesn’t expose, a non-standard attention variant, a custom activation function with a specific numerical property, you need to go lower. TT-Lang is that lower level, without requiring C++.
What TT-Lang Is
TT-Lang is a Python DSL that compiles to Tensix assembly. You write Python-like syntax with decorators that declare data-movement intent. The compiler translates that intent into reader kernels, compute kernels, and writer kernels. The three-kernel model you read about in Chapter 1 becomes the explicit structure of every TT-Lang program.
The key design principle: explicit data movement. Where TTNN hides the read/compute/write split, TT-Lang exposes it as the primary vocabulary. You declare what the reader fetches from where, what compute does to tiles in registers, what the writer sends where. No implicit sharing. No hidden transfers.
This explicitness is intentional and strategic. It makes TT-Lang programs easy for AI coding agents to generate, verify, and debug — because the spec is complete in the source code. The reader section tells you exactly what arrives. The compute section is pure math on those arrivals. The writer section is exactly what leaves. No ambiguity remains.
The Kernel Decorators
TT-Lang programs are organized around the ttl module’s decorators — verified against the
real, installed package rather than assumed. ttl is not part of the factory tooling venv
(~/.tenstorrent-venv holds only tt-smi/tt-flash, as elsewhere in this guide) or the
TT-Metalium container (confirmed absent from both, live) — it comes from pip install tt-lang
into a venv of your own, the same way the tt-lang-intro lesson
sets one up (python3 -m venv ttlang-venv && ... && pip install tt-lang tt-lang-setup):
@ttl.operation(grid=...)— the outer program;grid="auto"lets the compiler size it@ttl.compute()— runs on the FPU; consumes filled dataflow buffers, does the math, fills the output buffer@ttl.datamovement()— runs on a data-movement RISC core; there’s one decorator for both directions, not separate reader/writer ones — a function’s role (producer vs. consumer) comes from whether it calls.reserve()(fill a slot) or.wait()(drain a slot) on a given buffer, not from its decorator
A minimal vector addition kernel in TT-Lang looks like this:
import ttl
import ttnn
TILE_SIZE = 32
@ttl.operation(grid="auto")
def eltwise_add(a_in: ttnn.Tensor, b_in: ttnn.Tensor, out: ttnn.Tensor) -> None:
row_tiles = a_in.shape[0] // TILE_SIZE
col_tiles = a_in.shape[1] // TILE_SIZE
# Typed dataflow buffers (DFBs) — one slot per tile, depth 2 (double-buffer)
a_dfb = ttl.make_dataflow_buffer_like(a_in, shape=(1, 1), block_count=2)
b_dfb = ttl.make_dataflow_buffer_like(b_in, shape=(1, 1), block_count=2)
out_dfb = ttl.make_dataflow_buffer_like(out, shape=(1, 1), block_count=2)
@ttl.compute()
def compute():
for row in range(row_tiles):
for col in range(col_tiles):
with a_dfb.wait() as a_blk, b_dfb.wait() as b_blk, out_dfb.reserve() as o_blk:
o_blk.store(a_blk + b_blk) # element-wise add in L1
@ttl.datamovement()
def read():
for row in range(row_tiles):
for col in range(col_tiles):
with a_dfb.reserve() as a_blk, b_dfb.reserve() as b_blk:
ttl.copy(a_in[row:row+1, col:col+1], a_blk).wait()
ttl.copy(b_in[row:row+1, col:col+1], b_blk).wait()
@ttl.datamovement()
def write():
for row in range(row_tiles):
for col in range(col_tiles):
with out_dfb.wait() as o_blk:
ttl.copy(o_blk, out[row:row+1, col:col+1]).wait()
Three functions, three processors, one core. They run concurrently. The dataflow buffers
between them are the synchronization mechanism — reserve() blocks until a slot is free to
fill, wait() blocks until a slot is filled and ready to drain. This backpressure propagation
means the pipeline self-regulates. Every tile makes one DRAM read (read) and one DRAM write
(write); the + happens entirely in L1, inside compute.
Single-Core Data Flow
Here is what happens at the hardware level when vector_add runs on one Tensix core:
One Tensix core running all three TT-Lang sections concurrently.
TT-Lang vs TTNN: When to Use Which
They are not competing tools. They are different entry points into the same hardware, appropriate for different problems:
| Situation | Use |
|---|---|
| Standard ops: matmul, attention, layernorm, conv | TTNN — highly optimized, already there |
| Custom op that TTNN doesn’t expose | TT-Lang — write it in Python, no C++ required |
| Performance-critical custom fusion | TT-Metalium C++ — maximum control, no Python overhead |
| AI-agent-generated kernels | TT-Lang — explicit structure, agent-verifiable output |
| Production inference serving | TTNN via vLLM — already integrated |
The usual path: start with TTNN. When you hit a wall — a pattern that TTNN can’t express, a fusion the compiler misses, a numerical property you need to enforce — drop to TT-Lang. Write the custom section in TT-Lang, combine it with TTNN for the standard sections.
The TT-Lang Playground
You don’t need a QB2 to experiment with TT-Lang. The ttlang-sim browser-based simulator lets you write kernels, inspect the circular buffer state, and verify correctness without hardware.
For the structured lesson with exercises and a graded environment:
The lesson runs inside VS Code with the TT-VSCode Toolkit extension. It uses a local simulator so compilation is instant. After the lesson, running the same kernel on QB2 hardware is a one-line change.
read data-movement function and compute, and between compute and write, is organized as dataflow buffers (DFBs) — fixed-size ring structures, made with ttl.make_dataflow_buffer_like(). When a producer fills a slot (.reserve()), it stalls until a consumer drains one (.wait()) if the ring is full. This backpressure propagation is how three concurrent functions stay synchronized without explicit locks. The hardware implements the buffer arbitration; you just see reserve() and wait(). Understanding this explains why tile count and L1 size set the performance envelope: a kernel that fully pipelines needs at least two blocks in each buffer simultaneously — hence block_count=2 above.