Helion language

Viết bởi

trong

Đây là một chủ đề khá nâng cao, nằm ở giao điểm của AI Compiler, GPU Programming, PyTorch, TritonMachine Learning Systems. Tuy nhiên, nếu học đúng lộ trình thì Helion lại khá dễ hiểu vì nó được thiết kế để giấu đi sự phức tạp của Triton.

Tôi sẽ dạy theo kiểu từ nền tảng đến chuyên sâu.


Bài 1. Helion ra đời để giải quyết vấn đề gì?

Muốn hiểu Helion trước tiên phải hiểu GPU Kernel.

Giả sử ta có phép cộng hai vector.

C = A + B

Trong PyTorch chỉ cần:

C = A + B

PyTorch sẽ gọi kernel CUDA đã được viết sẵn.

Nếu bạn muốn tự viết kernel GPU thì có 3 lựa chọn.

MứcCông nghệĐộ khó
Cao nhấtCUDA C++Rất khó
Trung bìnhTritonKhó
Cao hơn PyTorchHelionDễ hơn Triton

Có thể hình dung:

PyTorch
    ↓
Helion
    ↓
Triton
    ↓
CUDA
    ↓
GPU

Helion thực chất không chạy trực tiếp trên GPU.

Nó sinh ra Triton.

Triton lại sinh ra CUDA/PTX.


Vì sao Triton vẫn còn khó?

Ví dụ viết Matrix Multiplication bằng Triton.

Bạn phải tự tính:

  • block size
  • số warp
  • pid
  • stride
  • pointer
  • masking
  • indexing

Ví dụ:

pid = tl.program_id(0)

pid_m = ...
pid_n = ...

offs_m = ...
offs_n = ...

ptr = base + ...

Khoảng 60% code chỉ để tính địa chỉ bộ nhớ.

Không phải tính toán.


Helion muốn bạn không phải làm việc đó.


Ý tưởng của Helion

Thay vì bảo GPU:

“Hãy đọc địa chỉ số 10032”

Helion cho bạn nói:

“Lấy tile này.”

Ví dụ

for tile_m, tile_n in hl.tile([m, n]):

đọc giống Python.

Nhưng Helion sẽ tự biến thành

program_id

↓

tile index

↓

pointer

↓

memory address

↓

GPU

Helion là gì?

Tài liệu viết:

Python Embedded DSL

DSL nghĩa là:

Domain Specific Language.

Tức là

Một ngôn ngữ nhỏ nằm trong Python.

Giống như:

SQL nằm trong Java

TensorFlow Graph nằm trong Python

JAX DSL

Helion DSL

Bạn vẫn viết Python.

Chỉ một số lệnh

hl.tile()

hl.zeros()

hl.range()

mang ý nghĩa đặc biệt.


Một kernel Helion gồm hai phần

Ví dụ:

@helion.kernel()
def matmul(...):

    ...

Trong kernel có hai vùng.

Phần 1

m, k = x.size()

out = torch.empty(...)

Đây là Python bình thường.

Chạy trên CPU.


Phần 2

for tile in hl.tile(...):

Đây không còn là Python nữa.

Helion sẽ dịch đoạn này thành Triton.


Nói cách khác

CPU code

↓

Helion boundary

↓

GPU code

hl.tile() là gì?

Đây là khái niệm quan trọng nhất.

Giả sử ma trận

1024 × 1024

GPU không xử lý toàn bộ.

Nó chia thành các ô.

Ví dụ

□□□□□□□□

□□□□□□□□

□□□□□□□□

□□□□□□□□

Mỗi ô gọi là

Tile

Ví dụ

64×64

thì

1024

↓

64

↓

16 tile

Theo chiều ngang.

Chiều dọc cũng vậy.

Tổng cộng

16 ×16 =256 tile

GPU sẽ chạy song song 256 tile.


Trong Helion

for tile_m, tile_n in hl.tile([m, n]):

không phải vòng lặp Python.

Mà nghĩa là

“Hãy chia ma trận thành các tile.”


Ví dụ Matrix Multiplication

Helion:

for tile_m, tile_n in hl.tile([m, n]):

    acc = hl.zeros(...)

    for tile_k in hl.tile(k):

        acc = torch.addmm(...)

Nếu viết dưới dạng sơ đồ.

        M

┌────┬────┐
│    │    │
├────┼────┤
│    │    │
└────┴────┘
      ↑

GPU xử lý từng tile

Bên trong mỗi tile

for tile_k

lại chia tiếp chiều K.

Giống GEMM chuẩn.


Điều đặc biệt của Helion

Trong Triton bạn phải tự chọn

Tile = 16

hay

Tile =32

hay

Tile =64

Helion không.

Bạn chỉ viết

hl.tile()

Nó tự thử.


Autotuning là gì?

Đây là trái tim của Helion.

Giả sử có các lựa chọn:

Tile

16

32

64

Warp

4

8

16

Indexing

pointer

block_ptr

descriptor

PID

flat

blocked

persistent

Có hàng nghìn khả năng.

Ví dụ

64
×

8 warp

×

block_ptr

×

persistent

Hay

32

×

4 warp

×

pointer

×

flat

Helion sẽ chạy thử tất cả.

Đo thời gian.

Lấy cái nhanh nhất.


Đây chính là

Autotuning

Tại sao mất tới 10 phút?

Vì Helion có thể thử

1500

2000

3000

cấu hình.

Ví dụ log

Generation 1

↓

Generation 2

↓

Generation 3

...

↓

Generation 20

Đây giống thuật toán tiến hóa.

Nó giữ cấu hình tốt.

Loại cấu hình chậm.

Sinh cấu hình mới.


Differential Evolution

Trong log có

DifferentialEvolutionSearch

Đây là thuật toán tối ưu.

Không thử ngẫu nhiên.

Mà giống tiến hóa.

Config A

Config B

↓

Lai

↓

Config C

↓

Đột biến

↓

Config D

Sau nhiều thế hệ.

Nó hội tụ.


Config là gì?

Đây là cấu hình GPU.

Ví dụ

Config(

block_sizes

num_warps

num_stages

indexing
)

Đây không phải code.

Mà là cách GPU chạy.

Ví dụ

block_size

↓

64×64

Hay

warp

↓

8

Tại sao nên lưu Config?

Sau khi autotune.

Helion in ra

Config(...)

Bạn copy.

@helion.kernel(
config=...
)

Lần sau

Không cần thử nữa.

Khởi động ngay.


Settings khác Config như thế nào?

Đây là điểm nhiều người mới học dễ nhầm lẫn.

ConfigSettings
Quyết định cách GPU thực thiQuyết định cách Helion biên dịch
Ảnh hưởng hiệu năngẢnh hưởng quá trình phát triển
Được autotuneKhông autotune
block size, num_warps, indexing…print_output_code, autotune_effort, logging…

Ví dụ:

@helion.kernel(
    autotune_effort="none",
    print_output_code=True
)

Đây là Settings: dùng để phát triển và gỡ lỗi.

Còn:

@helion.kernel(
    config=helion.Config(
        block_sizes=[64,64,64],
        num_warps=8
    )
)

Đây là Config: quyết định hiệu năng của kernel trên GPU.


Quá trình biên dịch Helion

Có thể hình dung toàn bộ quá trình như sau:

Mã Helion
      │
      ▼
Phân tích Python AST
      │
      ▼
Sinh Triton
      │
      ▼
TorchInductor
      │
      ▼
PTX/CUDA
      │
      ▼
GPU NVIDIA

Helion đóng vai trò là một lớp trừu tượng cao hơn Triton. Bạn mô tả bài toán ở mức “xử lý theo tile”, còn Helion tự sinh mã Triton và tự tối ưu.

Vì sao Helion đáng chú ý?

Nếu nhìn theo mức độ trừu tượng:

Công nghệBạn phải quan tâm
CUDAThread, Block, Shared Memory, Warp, Pointer, Synchronization
TritonTile, Pointer, Stride, PID, Mask
HelionChỉ cần Tile và các phép toán PyTorch
PyTorchChỉ cần gọi API có sẵn

Xu hướng của Helion tương tự như lịch sử phát triển của lập trình: thay vì yêu cầu lập trình viên điều khiển phần cứng ở mức thấp, nó chuyển gánh nặng tối ưu sang trình biên dịch và bộ autotuner.


Lộ trình học Helion tôi đề xuất

Để hiểu Helion một cách bài bản, nên học theo thứ tự sau:

  1. Kiến trúc GPU: Thread, Warp, Block, Grid, Shared Memory.
  2. CUDA cơ bản: hiểu cách GPU thực thi kernel.
  3. PyTorch Tensor: broadcasting, views, reductions, torch.addmm, torch.sum,…
  4. Triton: PID, tile, pointer, masking, block pointers.
  5. Helion: DSL, hl.tile(), autotuning, Config, Settings.
  6. TorchInductor và PyTorch Compiler: để hiểu Helion tích hợp vào hệ sinh thái PyTorch như thế nào.

Với nền tảng này, bạn sẽ không chỉ biết cách viết kernel bằng Helion, mà còn hiểu vì sao Helion có thể sinh ra các kernel Triton hiệu năng cao và cách đánh giá, tinh chỉnh chúng khi cần.


%pip install "torch==2.9.*" --index-url https://download.pytorch.org/whl/cu126
%pip install helion

"""
Helion Softmax Kernel Examples
==============================
This example demonstrates multiple Helion kernel implementations of the softmax function,
including a simple wrapper around PyTorch's softmax, and a numerically optimized two-pass version.
The example also includes a check function to compare these kernels against PyTorch's
built-in softmax for correctness.
"""

# %%
from __future__ import annotations
import torch
import helion
from helion._testing import run_example
import helion.language as hl


# %%
@helion.kernel(autotune_effort="quick")
def softmax(x: torch.Tensor) -> torch.Tensor:
    """
    Simple Helion kernel wrapping PyTorch's softmax function.
    Args:
        x (torch.Tensor): Input tensor of shape [n, m].
    Returns:
        torch.Tensor: Softmax output tensor of the same shape.
    """
    n, _m = x.size()
    out = torch.empty_like(x)
    for tile_n in hl.tile(n):
        out[tile_n, :] = torch.nn.functional.softmax(x[tile_n, :], dim=1)
    return out


# %%
def check(m: int, n: int) -> None:
    """
    Runs correctness checks comparing Helion softmax kernels against PyTorch's softmax.
    Args:
        m (int): Number of rows in input tensor.
        n (int): Number of columns in input tensor.
    """
    x = torch.randn([m, n], device="cuda", dtype=torch.float16)
    run_example(softmax, lambda x: torch.nn.functional.softmax(x, dim=1), (x,))


# %%
def main() -> None:
    """
    Main function to run the softmax kernel correctness check with example input size.
    """
    check(4096, 2560)


# %%
if __name__ == "__main__":
    main()

"""
Helion Softmax Kernel Examples
==============================
This example demonstrates multiple Helion kernel implementations of the softmax function,
including a simple wrapper around PyTorch's softmax, and a numerically optimized two-pass version.
The example also includes a check function to compare these kernels against PyTorch's
built-in softmax for correctness.
"""

# %%
from __future__ import annotations
import torch
import helion
from helion._testing import run_example
import helion.language as hl


# %%
@helion.kernel(autotune_effort="quick")
def softmax_two_pass(x: torch.Tensor) -> torch.Tensor:
    """
    Numerically optimized Helion kernel performing softmax in two passes.
    Args:
        x (torch.Tensor): Input tensor of shape [m, n].
    Returns:
        torch.Tensor: Softmax output tensor of the same shape.
    """
    m, n = x.size()
    out = torch.empty_like(x)
    block_size_m = hl.register_block_size(m)
    block_size_n = hl.register_block_size(n)
    for tile_m in hl.tile(m, block_size=block_size_m):
        mi = hl.full([tile_m], float("-inf"), dtype=torch.float32)
        di = hl.zeros([tile_m], dtype=torch.float32)
        for tile_n in hl.tile(n, block_size=block_size_n):
            values = x[tile_m, tile_n]
            local_amax = torch.amax(values, dim=1)
            mi_next = torch.maximum(mi, local_amax)
            di = di * torch.exp(mi - mi_next) + torch.exp(
                values - mi_next[:, None]
            ).sum(dim=1)
            mi = mi_next
        for tile_n in hl.tile(n, block_size=block_size_n):
            values = x[tile_m, tile_n]
            out[tile_m, tile_n] = torch.exp(values - mi[:, None]) / di[:, None]
    return out


# %%
def check(m: int, n: int) -> None:
    """
    Runs correctness checks comparing Helion softmax kernels against PyTorch's softmax.
    Args:
        m (int): Number of rows in input tensor.
        n (int): Number of columns in input tensor.
    """
    x = torch.randn([m, n], device="cuda", dtype=torch.float16)
    run_example(softmax_two_pass, lambda x: torch.nn.functional.softmax(x, dim=1), (x,))


# %%
def main() -> None:
    """
    Main function to run the softmax kernel correctness check with example input size.
    """
    check(4096, 2560)


# %%
if __name__ == "__main__":
    main()

Đoạn mã gồm hai cách viết Softmax bằng Helion:

KernelCách làm
softmaxGọi trực tiếp torch.nn.functional.softmax trên từng nhóm hàng
softmax_two_passTự triển khai thuật toán Softmax ổn định số học theo hai lượt

Cả hai đều được Helion biên dịch thành một GPU kernel Triton.


1. Softmax là gì?

Với một hàng dữ liệu:

[
x = [x_1,x_2,\ldots,x_n]
]

Softmax được tính như sau:

[
\operatorname{softmax}(x_i)

\frac{e^{x_i}}{\sum_j e^{x_j}}
]

Ví dụ:

[
x=[1,2,3]
]

Kết quả gần đúng:

[
[0.09,\ 0.24,\ 0.67]
]

Tổng các phần tử luôn bằng 1.

Trong đoạn mã, Softmax được tính theo chiều:

dim=1

Nghĩa là tính Softmax trên từng hàng của tensor hai chiều.


2. Cài đặt thư viện

%pip install "torch==2.9.*" --index-url https://download.pytorch.org/whl/cu126
%pip install helion

Dòng đầu cài PyTorch 2.9 có hỗ trợ CUDA 12.6.

Dòng thứ hai cài Helion.

Cú pháp %pip thường được dùng trong:

  • Jupyter Notebook
  • Google Colab
  • IPython

Nếu chạy trong file Python thông thường thì cài bằng terminal:

pip install "torch==2.9.*" --index-url https://download.pytorch.org/whl/cu126
pip install helion

3. Các thư viện được import

from __future__ import annotations
import torch
import helion
from helion._testing import run_example
import helion.language as hl
Thành phầnVai trò
torchTạo tensor và sử dụng các phép toán PyTorch
helionĐịnh nghĩa và biên dịch Helion kernel
run_exampleKiểm tra kết quả và có thể benchmark kernel
helion.language as hlCung cấp các toán tử DSL như hl.tile, hl.zeros
annotationsTrì hoãn việc đánh giá type hint

Lưu ý rằng helion._testing có dấu gạch dưới. Đây thường là API nội bộ phục vụ kiểm thử, không nhất thiết ổn định như API công khai.


4. Kernel Softmax thứ nhất

@helion.kernel(autotune_effort="quick")
def softmax(x: torch.Tensor) -> torch.Tensor:

Decorator:

@helion.kernel(...)

biến hàm Python thành một Helion kernel.

Tham số:

autotune_effort="quick"

yêu cầu Helion thực hiện autotuning ở mức nhanh.

Có thể hiểu tương đối như sau:

Mức autotuneĐặc điểm
noneKhông tìm cấu hình tối ưu
quickThử một số cấu hình, thời gian ngắn hơn
Mức cao hơnTìm kiếm rộng hơn, mất nhiều thời gian hơn

Autotuner có thể lựa chọn:

  • số hàng trong mỗi tile;
  • kích thước block;
  • số warp;
  • cách đánh địa chỉ bộ nhớ;
  • chiến lược ánh xạ Program ID;
  • cách thực hiện reduction.

4.1 Lấy kích thước tensor

n, _m = x.size()

Nếu x có shape:

[4096, 2560]

thì:

n = 4096
_m = 2560

Tuy nhiên tên biến ở đây hơi dễ gây nhầm.

Theo cách đặt tên phổ biến, nên viết:

m, n = x.size()

Trong kernel hiện tại:

  • n thực chất là số hàng;
  • _m thực chất là số cột.

Dấu _ ở đầu _m cho biết biến này không được sử dụng tiếp.

Viết rõ hơn có thể là:

num_rows, _num_cols = x.size()

4.2 Tạo tensor đầu ra

out = torch.empty_like(x)

Tạo tensor out:

  • cùng kích thước với x;
  • cùng kiểu dữ liệu;
  • cùng thiết bị;
  • chưa được khởi tạo giá trị.

Nếu x có:

shape  = [4096, 2560]
dtype  = float16
device = cuda

thì out cũng có các thuộc tính tương tự.

Dùng empty_like nhanh hơn việc khởi tạo toàn bộ tensor bằng 0 vì kernel sẽ ghi đè tất cả các giá trị.


4.3 Chia hàng thành các tile

for tile_n in hl.tile(n):

Đây không phải vòng lặp Python thông thường.

Nó có nghĩa:

Chia chiều gồm n hàng thành các tile và ánh xạ các tile đó lên các chương trình GPU.

Giả sử có 4096 hàng và autotuner chọn tile 64 hàng:

[
4096 / 64 = 64\text{ tile}
]

GPU có thể xử lý các tile này song song.

tile_n không phải một số nguyên đơn lẻ. Nó đại diện cho một tập chỉ số hàng, chẳng hạn:

tile 0: hàng 0 đến 63
tile 1: hàng 64 đến 127
tile 2: hàng 128 đến 191
...

4.4 Tính Softmax trên các hàng thuộc tile

out[tile_n, :] = torch.nn.functional.softmax(
    x[tile_n, :],
    dim=1
)

Trong đó:

x[tile_n, :]

có nghĩa:

  • lấy các hàng thuộc tile_n;
  • lấy toàn bộ cột.

Ví dụ tile chứa 64 hàng, tensor tạm có thể có shape:

[64, 2560]

Sau đó:

softmax(..., dim=1)

tính Softmax trên 2560 phần tử của từng hàng.

Kết quả được ghi vào đúng vị trí:

out[tile_n, :]

Có thể hình dung:

Tensor x: 4096 × 2560

┌──────────────────────────┐
│ tile hàng số 1           │ → softmax từng hàng
├──────────────────────────┤
│ tile hàng số 2           │ → softmax từng hàng
├──────────────────────────┤
│ tile hàng số 3           │ → softmax từng hàng
├──────────────────────────┤
│ ...                      │
└──────────────────────────┘

4.5 Trả kết quả

return out

Phần bên ngoài vòng lặp như:

x.size()
torch.empty_like()
return

được dùng để chuẩn bị kernel.

Phần bên trong:

for tile_n in hl.tile(n):

được Helion chuyển thành mã GPU.


5. Hàm kiểm tra kernel thứ nhất

def check(m: int, n: int) -> None:

Hàm nhận:

  • m: số hàng;
  • n: số cột.

5.1 Tạo dữ liệu kiểm thử

x = torch.randn(
    [m, n],
    device="cuda",
    dtype=torch.float16
)

Ví dụ:

check(4096, 2560)

sẽ tạo tensor:

shape  = [4096, 2560]
device = GPU
dtype  = float16

torch.randn tạo số ngẫu nhiên theo phân phối chuẩn.


5.2 So sánh Helion với PyTorch

run_example(
    softmax,
    lambda x: torch.nn.functional.softmax(x, dim=1),
    (x,)
)

Ba tham số chính:

Tham sốÝ nghĩa
softmaxKernel Helion cần kiểm tra
lambda x: ...Hàm PyTorch dùng làm kết quả tham chiếu
(x,)Tuple chứa đối số truyền vào

Dấu phẩy trong:

(x,)

rất quan trọng. Đây là tuple có một phần tử.

Không có dấu phẩy:

(x)

thì chỉ là biểu thức x, không phải tuple.

Về bản chất, hàm kiểm tra làm gần giống:

actual = softmax(x)
expected = torch.nn.functional.softmax(x, dim=1)

assert actual gần bằng expected

6. Hàm main

def main() -> None:
    check(4096, 2560)

Chạy kiểm thử với tensor có:

  • 4096 hàng;
  • 2560 cột.

Tổng số phần tử:

[
4096 \times 2560 = 10{,}485{,}760
]

Vì kiểu float16 dùng 2 byte mỗi phần tử, riêng một tensor chiếm khoảng:

[
10{,}485{,}760 \times 2

20{,}971{,}520\text{ byte}
]

Tức khoảng 20 MiB.


6.1 Kiểm tra chương trình có được chạy trực tiếp không

if __name__ == "__main__":
    main()

Nếu file được chạy trực tiếp:

python softmax_example.py

thì main() được gọi.

Nếu file được import từ module khác thì main() không tự chạy.

Trong Jupyter Notebook, đoạn này thường vẫn chạy vì __name__ thường bằng "__main__".


7. Kernel thứ hai: softmax_two_pass

Kernel thứ hai tự triển khai Softmax thay vì gọi trực tiếp:

torch.nn.functional.softmax

Mục tiêu là xử lý trường hợp số cột lớn mà không nhất thiết phải tải toàn bộ một hàng vào register cùng lúc.

@helion.kernel(autotune_effort="quick")
def softmax_two_pass(x: torch.Tensor) -> torch.Tensor:

8. Tại sao cần Softmax ổn định số học?

Công thức trực tiếp:

[
\frac{e^{x_i}}{\sum_j e^{x_j}}
]

có thể bị tràn số.

Ví dụ:

[
e^{1000}
]

là một số quá lớn, dễ trở thành vô cực:

inf

Softmax ổn định được tính bằng cách trừ giá trị lớn nhất:

[
\operatorname{softmax}(x_i)

\frac{e^{x_i-m}}{\sum_j e^{x_j-m}}
]

với:

[
m=\max_j x_j
]

Vì:

[
x_i-m\leq 0
]

nên:

[
e^{x_i-m}\leq 1
]

Giảm đáng kể nguy cơ overflow.


9. Chuẩn bị trong softmax_two_pass

m, n = x.size()
out = torch.empty_like(x)

Ở kernel này tên biến đúng với quy ước:

BiếnÝ nghĩa
mSố hàng
nSố cột

9.1 Đăng ký block size

block_size_m = hl.register_block_size(m)
block_size_n = hl.register_block_size(n)

Helion được yêu cầu tạo các tham số block size cho hai chiều:

  • chiều hàng m;
  • chiều cột n.

Các giá trị này có thể được autotuner lựa chọn.

Có thể hình dung:

block_size_m ∈ {1, 2, 4, 8, ...}
block_size_n ∈ {32, 64, 128, 256, ...}

Tập giá trị thực tế phụ thuộc Helion, phần cứng và không gian tìm kiếm.

Khác với:

hl.tile(m)

để Helion tự xử lý hoàn toàn, ở đây code lấy block size ra thành biến nhằm dùng lại cùng một kích thước trong nhiều vòng lặp.

Đặc biệt, block_size_n được dùng ở cả lượt thứ nhất và lượt thứ hai.


10. Vòng lặp theo tile hàng

for tile_m in hl.tile(m, block_size=block_size_m):

Kernel xử lý một nhóm hàng tại một thời điểm.

Giả sử:

m = 4096
block_size_m = 8

thì mỗi chương trình GPU xử lý 8 hàng.

Số chương trình cần thiết gần bằng:

[
4096/8=512
]

Các chương trình này có thể chạy song song.


11. Hai biến trạng thái midi

mi = hl.full(
    [tile_m],
    float("-inf"),
    dtype=torch.float32
)

di = hl.zeros(
    [tile_m],
    dtype=torch.float32
)

Mỗi hàng đang được xử lý có hai giá trị trạng thái:

BiếnÝ nghĩa
miGiá trị lớn nhất đã thấy trong các block cột trước
diTổng mũ đã được hiệu chỉnh theo giá trị lớn nhất hiện tại

mi được khởi tạo bằng:

[
-\infty
]

vì chưa đọc giá trị nào.

di được khởi tạo bằng:

[
0
]

Cả hai dùng float32 dù đầu vào là float16.

Điều này rất quan trọng để tăng độ chính xác khi:

  • tìm max;
  • tính hàm mũ;
  • cộng tổng nhiều phần tử.

12. Lượt thứ nhất: tính max và mẫu số

for tile_n in hl.tile(n, block_size=block_size_n):

Chiều cột được chia thành các block.

Giả sử:

n = 2560
block_size_n = 256

thì mỗi hàng có 10 block cột:

0–255
256–511
512–767
...
2304–2559

12.1 Đọc một block dữ liệu

values = x[tile_m, tile_n]

Nếu:

tile_m có 8 hàng
tile_n có 256 cột

thì values có shape logic:

[8, 256]

12.2 Tìm max cục bộ

local_amax = torch.amax(values, dim=1)

Tìm giá trị lớn nhất trên từng hàng trong block cột hiện tại.

Nếu:

values shape = [8, 256]

thì:

local_amax shape = [8]

Mỗi hàng có một max cục bộ.


12.3 Cập nhật max toàn cục

mi_next = torch.maximum(mi, local_amax)

So sánh:

  • mi: max của tất cả block đã xử lý;
  • local_amax: max của block hiện tại.

Kết quả:

[
m_{\text{next}}

\max(m_{\text{old}},m_{\text{local}})
]


13. Công thức cập nhật di

Đây là dòng quan trọng nhất:

di = di * torch.exp(mi - mi_next) + torch.exp(
    values - mi_next[:, None]
).sum(dim=1)

Để hiểu, trước tiên hãy xét mục tiêu.

Sau khi đọc tất cả các cột, ta muốn:

[
d_i = \sum_j e^{x_{ij}-m_i}
]

trong đó:

[
m_i=\max_j x_{ij}
]

Nhưng dữ liệu được đọc theo từng block. Khi đọc block mới, giá trị max có thể thay đổi.


13.1 Phần mới

torch.exp(values - mi_next[:, None]).sum(dim=1)

Tính:

[
\sum_{x\in\text{block mới}}e^{x-m_{\text{next}}}
]

mi_next có shape:

[tile_m]

Còn values có shape:

[tile_m, tile_n]

Cần thêm một chiều:

mi_next[:, None]

để có shape:

[tile_m, 1]

Sau đó broadcasting:

values              [tile_m, tile_n]
mi_next[:, None]    [tile_m,      1]
                    ─────────────────
kết quả             [tile_m, tile_n]

13.2 Hiệu chỉnh phần tổng cũ

di * torch.exp(mi - mi_next)

Trước đó, di được biểu diễn theo max cũ:

[
d_{\text{old}}

\sum_{\text{cũ}}e^{x-m_{\text{old}}}
]

Nhưng giờ max mới là (m_{\text{next}}).

Ta cần đổi hệ quy chiếu:

[
e^{x-m_{\text{next}}}

e^{x-m_{\text{old}}}
\cdot
e^{m_{\text{old}}-m_{\text{next}}}
]

Do đó:

[
d_{\text{old adjusted}}

d_{\text{old}}
\cdot
e^{m_{\text{old}}-m_{\text{next}}}
]

Đó chính là:

di * torch.exp(mi - mi_next)

13.3 Công thức đầy đủ

[
d_{\text{next}}

d_{\text{old}}
e^{m_{\text{old}}-m_{\text{next}}}
+
\sum_{\text{block mới}}
e^{x-m_{\text{next}}}
]

Đây thường được gọi là online softmax recurrence.

Nó cho phép tính max và tổng mũ trong cùng một lượt đọc dữ liệu, thay vì phải:

  1. đọc toàn bộ để tìm max;
  2. đọc lại để tính tổng.

Tuy nhiên toàn bộ kernel vẫn được gọi là “two-pass” vì còn một lượt thứ hai để ghi kết quả cuối cùng.


13.4 Cập nhật max

mi = mi_next

Sau khi cập nhật tổng, lưu max mới để xử lý block tiếp theo.


14. Ví dụ số cho công thức cập nhật

Giả sử một hàng được chia làm hai block.

Block thứ nhất:

[
[1,2]
]

Ta có:

[
m_{\text{old}}=2
]

[
d_{\text{old}}

e^{1-2}+e^{2-2}

e^{-1}+1
]

Xử lý block thứ hai:

[
[3,4]
]

Max mới:

[
m_{\text{next}}=4
]

Phần tổng cũ phải được chuyển từ hệ quy chiếu 2 sang hệ quy chiếu 4:

[
d_{\text{old adjusted}}

d_{\text{old}}e^{2-4}
]

Phần block mới:

[
e^{3-4}+e^{4-4}
]

Tổng cuối:

[
d_{\text{next}}

(e^{-1}+1)e^{-2}+e^{-1}+1
]

Rút gọn:

[
d_{\text{next}}

e^{-3}+e^{-2}+e^{-1}+1
]

Đúng bằng:

[
\sum_{x\in[1,2,3,4]}e^{x-4}
]


15. Kết thúc lượt thứ nhất

Sau vòng lặp đầu:

for tile_n in hl.tile(...):

ta có, với mỗi hàng:

mi = giá trị lớn nhất của toàn bộ hàng
di = tổng exp(x - mi) của toàn bộ hàng

Tức là đã có đầy đủ:

  • số trừ ổn định số học;
  • mẫu số của Softmax.

Nhưng chưa ghi kết quả đầu ra.


16. Lượt thứ hai: tính kết quả Softmax

for tile_n in hl.tile(n, block_size=block_size_n):

Kernel duyệt lại từng block cột.

values = x[tile_m, tile_n]

Đọc lại dữ liệu đầu vào.

Sau đó:

out[tile_m, tile_n] = (
    torch.exp(values - mi[:, None])
    / di[:, None]
)

Đây chính là công thức:

[
\operatorname{softmax}(x)

\frac{e^{x-m}}{d}
]

với:

[
d=\sum_j e^{x_j-m}
]

Broadcasting:

values          [block_rows, block_cols]
mi[:, None]     [block_rows,          1]
di[:, None]     [block_rows,          1]

Mỗi hàng dùng một giá trị max và một mẫu số riêng.


17. Vì sao gọi là hai lượt?

Lượt 1

Đọc x
→ tìm max
→ tính tổng exp đã chuẩn hóa

Lượt 2

Đọc lại x
→ tính exp(x - max) / tổng
→ ghi out

Sơ đồ:

                 Lượt 1
x ───────────────────────────────────┐
                                    │
       max từng hàng + mẫu số        │
                                    ▼
                               mi và di
                                    │
                 Lượt 2             │
x ───────────────────────────────────┤
                                    ▼
                         exp(x-mi) / di
                                    │
                                    ▼
                                   out

18. So sánh hai kernel

Tiêu chísoftmaxsoftmax_two_pass
Mức trừu tượngCaoThấp hơn
Cách viếtGọi PyTorch SoftmaxTự triển khai công thức
Chia tile theo hàng
Chia tile theo cộtHelion tự xử lýViết rõ trong code
Ổn định số họcDo PyTorch/Inductor xử lýThể hiện rõ bằng mi, di
Hỗ trợ hàng rất rộngPhụ thuộc compilerChủ động loop theo block cột
Dễ đọcDễ hơnKhó hơn
Kiểm soát thuật toánÍt hơnNhiều hơn

19. Điểm đáng chú ý về GPU

Một hàng có 2560 phần tử float16.

Dung lượng thô:

[
2560\times2=5120\text{ byte}
]

Nhưng trong quá trình tính Softmax, nhiều giá trị thường được chuyển sang float32.

Nếu cố giữ toàn bộ dữ liệu trung gian của nhiều hàng trong register, áp lực register có thể cao.

Kernel hai lượt cho phép xử lý chiều cột theo từng phần:

256 cột
→ 256 cột
→ 256 cột
→ ...

Thay vì bắt buộc xử lý toàn bộ 2560 cột cùng lúc.

Lợi ích tiềm năng:

  • giảm register pressure;
  • hỗ trợ chiều reduction lớn;
  • cho autotuner lựa chọn kích thước block;
  • phù hợp hơn với nhiều kích thước tensor.

Đổi lại, đầu vào phải được đọc hai lần.


20. Một Helion kernel vẫn chỉ là một GPU kernel

Mặc dù trong softmax_two_pass có ba vòng lặp:

for tile_m in hl.tile(...):
    for tile_n in hl.tile(...):
        ...
    for tile_n in hl.tile(...):
        ...

không có nghĩa là tạo ba lần gọi kernel GPU.

Ý nghĩa gần đúng:

Vòng lặpCách Helion ánh xạ
tile_m bên ngoàiÁnh xạ lên grid/program GPU
tile_n lượt 1Vòng lặp nội bộ trong mỗi program
tile_n lượt 2Vòng lặp nội bộ thứ hai trong cùng program

Một program GPU xử lý một tile hàng:

Program 0 → một nhóm hàng
Program 1 → nhóm hàng tiếp theo
Program 2 → nhóm hàng tiếp theo
...

Trong mỗi program:

lặp qua các block cột lần 1
lặp qua các block cột lần 2

21. Mã nguồn có phần bị lặp

Trong nội dung bạn gửi, phần sau xuất hiện hai lần:

from __future__ import annotations
import torch
import helion
from helion._testing import run_example
import helion.language as hl

Tiêu đề chuỗi tài liệu cũng bị lặp:

"""
Helion Softmax Kernel Examples
...
"""

Đây không phải lỗi nghiêm trọng, nhưng có thể gộp thành một file rõ ràng hơn.


22. Phiên bản tổ chức lại dễ đọc hơn

from __future__ import annotations

import torch
import helion
import helion.language as hl
from helion._testing import run_example


@helion.kernel(autotune_effort="quick")
def softmax(x: torch.Tensor) -> torch.Tensor:
    num_rows, _num_cols = x.size()
    out = torch.empty_like(x)

    for row_tile in hl.tile(num_rows):
        out[row_tile, :] = torch.nn.functional.softmax(
            x[row_tile, :],
            dim=1,
        )

    return out


@helion.kernel(autotune_effort="quick")
def softmax_two_pass(x: torch.Tensor) -> torch.Tensor:
    num_rows, num_cols = x.size()
    out = torch.empty_like(x)

    row_block_size = hl.register_block_size(num_rows)
    col_block_size = hl.register_block_size(num_cols)

    for row_tile in hl.tile(
        num_rows,
        block_size=row_block_size,
    ):
        running_max = hl.full(
            [row_tile],
            float("-inf"),
            dtype=torch.float32,
        )

        running_sum = hl.zeros(
            [row_tile],
            dtype=torch.float32,
        )

        # Lượt 1: tìm max và mẫu số.
        for col_tile in hl.tile(
            num_cols,
            block_size=col_block_size,
        ):
            values = x[row_tile, col_tile]

            local_max = torch.amax(values, dim=1)
            next_max = torch.maximum(running_max, local_max)

            running_sum = (
                running_sum
                * torch.exp(running_max - next_max)
                + torch.exp(
                    values - next_max[:, None]
                ).sum(dim=1)
            )

            running_max = next_max

        # Lượt 2: chuẩn hóa và ghi đầu ra.
        for col_tile in hl.tile(
            num_cols,
            block_size=col_block_size,
        ):
            values = x[row_tile, col_tile]

            out[row_tile, col_tile] = (
                torch.exp(values - running_max[:, None])
                / running_sum[:, None]
            )

    return out


def check(kernel, rows: int, cols: int) -> None:
    x = torch.randn(
        [rows, cols],
        device="cuda",
        dtype=torch.float16,
    )

    run_example(
        kernel,
        lambda value: torch.nn.functional.softmax(value, dim=1),
        (x,),
    )


def main() -> None:
    check(softmax, 4096, 2560)
    check(softmax_two_pass, 4096, 2560)


if __name__ == "__main__":
    main()

23. Tóm tắt luồng thực thi

Kernel đơn giản

Input x
  ↓
Chia theo nhóm hàng
  ↓
PyTorch softmax trên từng nhóm hàng
  ↓
Output

Kernel hai lượt

Input x
  ↓
Chia theo nhóm hàng
  ↓
Với mỗi nhóm hàng:
  ├─ Chia chiều cột thành block
  ├─ Lượt 1: tính running max và running sum
  ├─ Lượt 2: tính xác suất Softmax
  └─ Ghi output

Ý tưởng quan trọng nhất của softmax_two_pass là công thức cập nhật online:

[
m_{\text{new}}

\max(m_{\text{old}},m_{\text{block}})
]

[
d_{\text{new}}

d_{\text{old}}e^{m_{\text{old}}-m_{\text{new}}}
+
\sum_{\text{block}}e^{x-m_{\text{new}}}
]

Công thức này giúp tính Softmax ổn định số học khi chiều cột lớn mà không cần giữ toàn bộ hàng trong register cùng lúc.

Bình luận

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *