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):

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.

🤖 The three-kernel model maps cleanly to LLM prompting. Describe what the reader fetches (tensor shapes, dtypes, source addresses). Describe what compute does (the mathematical operation, tile count). Describe what the writer sends (destination, same tile count). An AI coding agent can fill in the exact TT-Lang syntax from that spec with high reliability. The explicit structure eliminates the ambiguity that causes hallucination in implicit GPU kernel code.

Single-Core Data Flow

Here is what happens at the hardware level when vector_add runs on one Tensix core:

⬡ Tensix Grid — Blackhole (P100/P150/P300c / QB2)

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.

🔬 Dataflow buffers as the memory model. The L1 SRAM between the 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.

Next: Profiling & Optimization →