Tile Language (tile-lang)

repository·main·Indexed 25 days ago

https://github.com/tile-ai/tilelang

A Pythonic domain-specific language and compiler framework built on top of TVM for generating high-performance GPU and CPU kernels, such as GEMM and FlashAttention. It provides low-level optimization capabilities, including loop pipelining, L2 cache swizzling, and parallel data movement, without requiring manual assembly or low-level C++.

Tokens
57.7K
Snippets
129
Records
268
Agent score
92%

What's inside tilelang

  1. Overview of Tile Language

    main
    Tile Language (tile-lang) is a domain-specific language (DSL) designed for developing high-performance GPU/CPU kernels, such as GEMM, Dequant GEMM, FlashAttention, and LinearAttention. It provides a Pythonic syntax while utilizing a compiler infrastructure built on top of TVM, allowing developers to achieve low-level optimizations with high productivity.
  2. Overview of Tile Language (tile-lang)

    main
    Tile Language (tile-lang) is a concise domain-specific language (DSL) designed for developing high-performance GPU/CPU kernels such as GEMM, Dequant GEMM, FlashAttention, and LinearAttention. It uses a Pythonic syntax and sits on top of the TVM compiler infrastructure, allowing for low-level optimizations with high developer productivity.
  3. Understand the TileLang Backend Architecture

    main

    TileLang uses a multi-backend architecture designed to keep the frontend language surface backend-neutral while making backend ownership explicit. The architecture is split into two layers:

    1. Python Layer:

      • tilelang/backend/: Contains shared infrastructure (pass-pipeline registration, host/device-codegen registration, and shared utilities).
      • tilelang/<backend>/: Contains backend-specific implementations (pass pipelines, codegen entry registration, tile-op implementations, and intrinsics).
    2. Native (C++) Layer:

      • src/<backend>/: Contains C++ op lowering, codegen, runtime modules, and stubs.
      • src/backend/: Reserved for shared native backend helpers.
  4. Understand the purpose of CUDA and ROCm stubs

    main

    TileLang implements stub libraries to improve portability and compatibility:

    CUDA Stubs

    • cuda_stub (libstub_cuda.so): Allows importing TileLang on systems without a GPU (like CI nodes) by lazy-loading libcuda.so only when needed.
    • cudart_stub (libstub_cudart.so) and nvrtc_stub (libstub_nvrtc.so): Resolves SONAME versioning mismatches (e.g., libcudart.so.11 vs libcudart.so.12). They attempt to reuse CUDA libraries already loaded by frameworks like PyTorch to ensure a single build works across different CUDA versions.

    ROCm Stubs

    • hip_stub (libstub_hip.so): Allows importing TileLang on systems without ROCm installed by lazy-loading libamdhip64.so. It uses RTLD_DEFAULT / RTLD_NEXT to interoperate with frameworks that have already loaded HIP symbols.
    • hiprtc_stub (libstub_hiprtc.so): Lazily loads libhiprtc.so and exposes the minimal HIPRTC API subset used by TileLang/TVM.
  5. Use Layout Inference for complex buffer shapes

    main
    TileLang uses Layout Inference to automatically deduce required buffer shapes and optimal layouts based on Tile-Operators like T.gemm and T.copy. For example, when using policy=FullCol in a T.gemm operation, TileLang can infer how to partition results across warpgroups and how to manage shared memory for subsequent operations.
  6. Choose a TileLang programming interface

    main

    TileLang provides three levels of abstraction depending on your hardware knowledge and optimization needs. You can mix these interfaces within the same kernel:

    1. Beginner Level (Hardware-Unaware): Focuses on basic logic without worrying about memory hierarchies or hardware-specific optimizations. Note: This interface is not yet fully implemented.
    2. Developer Level (Hardware-Aware with Tile Library): Uses a Tile Library of predefined, optimized operations and patterns. Ideal for users who understand GPU memory hierarchies but want to avoid low-level threading details.
    3. Expert Level (Hardware-Aware with Thread Primitives): Provides direct access to thread primitives for fine-grained control over threading models, memory coalescing, and specialized optimizations.
  7. Debug TileLang programs using IR Lower Trace

    main
    To diagnose where index calculations, copy logic, or kernel operations deviate from intended behavior, use the IR Lower Trace tool. This tool provides automatic, pass-by-pass visibility into every Intermediate Representation (IR) transformation, including the final code generation step. This allows you to pinpoint exactly which pass introduces an unexpected change in your program.
  8. Namespace usage in headers and implementation

    main

    To keep public APIs predictable and implementation files clean, follow these namespace rules:

    • Public/Shared Headers: Avoid using namespace in headers that define cross-module APIs, ObjectRef/ObjectNode types, or FFI-visible fields. Use explicit qualifications or narrow aliases.
    • Implementation Files (.cc): You may use file-local namespace imports (e.g., using namespace tirx;) to improve readability when dealing with dense IR or DSL expressions.
    • FFI Types: Prefer explicit names like ffi::Any, ffi::Array, ffi::Map, and ffi::make_object in core code.
  9. Debug TileLang generation and correctness issues

    main

    A recommended three-pronged approach for resolving issues in TileLang programs is:

    1. Inspect IR transformations: Use the IR Lower Trace tool to see how the code changes through each pass.
    2. Observe pass-level diffs: Use tools to see changes between IR states (note that TL_LOWER_TRACE is the modern replacement for the older Pass Diff tool).
    3. Use runtime prints: Use T.print to observe actual values during execution.
  10. Quick Start: Instrument a TileLang CUDA kernel with IKET

    main

    To profile a kernel, wrap the compilation process in an iket.session(...). This ensures the session is active when tilelang.compile(...) generates CUDA source. You can use iket.range as a context manager and iket.mark for instant events within your T.prim_func.

    import tilelang
    import tilelang.language as T
    from tilelang.tools.cuda import iket
    
    
    def instrumented_add(n: int, threads: int = 128):
        @T.prim_func
        def main(
            A: T.Tensor((n,), T.float32),
            B: T.Tensor((n,), T.float32),
            C: T.Tensor((n,), T.float32),
        ):
            with T.Kernel(T.ceildiv(n, threads), threads=threads) as bx:
                with iket.range("block_total"):
                    for tx in T.Parallel(threads):
                        i = bx * threads + tx
                        if i < n:
                            iket.mark("before_store")
                            C[i] = A[i] + B[i]
                            iket.mark("after_store")
    
        return main
    
    
    with iket.session(output_dir="/tmp/tilelang_iket"):
        program = instrumented_add(1024)
        kernel = tilelang.compile(
            program,
            out_idx=-1,
            target="cuda",
            execution_backend="cython",
        )
  11. Implement an elementwise operator in TileLang

    main

    To implement an elementwise operator, define a function that returns a T.prim_func. All kernel logic must reside within a T.Kernel(...) scope.

    Key concepts:

    • T.Kernel(grid_size, threads=threads): Defines the grid size and threads per block. The returned values (e.g., bx) correspond to CUDA's blockIdx.
    • T.Parallel(...): Used inside the kernel to process the data tile assigned to the block.
    • Block-level programming: Code inside T.Kernel operates at the block level. TileLang automatically handles the mapping to individual threads and applies optimizations during compilation.
    def elementwise_add(N, threads=256, dtype=T.bfloat16):
        @T.prim_func
        def main(A: T.Tensor((N), dtype), B: T.Tensor((N), dtype), C: T.Tensor((N), dtype)):
            with T.Kernel(T.ceildiv(N, threads), threads=threads) as (b_x):
                # vector add.
                for i in T.Parallel(threads):
                    C[b_x * threads + i] = A[b_x * threads + i] + B[b_x * threads + i]
    
        return main