Перейти к содержимому
Ступень 55.7 GPU изнутри — архитектура, ядра, несколько карт

Пишем ядра на Triton

Модель программирования Triton — программы-блоки, program_id, маски, load и store — и первые ядра своими руками: сложение векторов, слитый softmax с обратным проходом в autograd, RMSNorm, тайловый матмул с автоподбором и forward FlashAttention, с проверкой против PyTorch и замером скорости на RTX 4080.

УРОКstage-5/5-7-gpu-internals/03-triton-kernels.mdx2 ч 30 минУрок 3 из 4
После урока вы сможете
  • Объяснить модель программирования Triton (сетка программ, program_id, tl.arange, маски, tl.load и tl.store) и чем она отличается от CUDA
  • Написать на Triton сложение векторов, слитый softmax и RMSNorm, проверить их через torch.testing.assert_close и замерить ГБ/с
  • Встроить своё ядро в autograd через torch.autograd.Function
  • Написать тайловый матмул с tl.dot и автоподбором конфигураций и сравнить его с cuBLAS

В уроке 5.7.1 torch.compile сам сливал поэлементные операции в ядра на Triton, в уроке 5.7.2 вы написали FlashAttention на NumPy. Но NumPy не умеет класть блоки в SRAM, а torch.compile не придумает online softmax за вас: он сливает то, что видит в графе, и не меняет алгоритм. Когда нужен новый алгоритм — своё внимание, слитый слой с квантованием, особый вид разреженности, — ядро пишут руками.

Раньше это значило CUDA C++: сотни строк про потоки, варпы, shared memory и синхронизацию. TritonTritonEN Triton (language and compiler)Язык и компилятор GPU-ядер на Python (OpenAI, открытый под MIT). Программист описывает работу одного блока данных, а распределение по потокам, shared memory и тензорные ядра берёт на себя компилятор. На нём написаны ядра torch.compile, flash-linear-attention, многие ядра vLLM. (Филипп Тилле, статья 2019 года; с 2021-го развивается в OpenAI, лицензия MIT) позволяет писать ядра на Python-подобном языке, думая блоками данных, а не отдельными потоками. На Triton написаны ядра, которые генерирует torch.compile, библиотека flash-linear-attention из урока 5.5.3, ядро NSA из урока 5.5.2, многие ядра vLLM. Triton поддерживает видеокарты NVIDIA с compute capability 8.0 и выше, то есть и вашу Ada (8.9). Ставить отдельно ничего не нужно: пакет triton приходит вместе с PyTorch для Linux.

Ядро на Triton — это функция с декоратором @triton.jit, которая описывает работу одной программы. Программа обрабатывает целый блок данных: вектор из 1024 чисел или тайл 64×6464 \times 64. При запуске вы задаёте сетку — сколько программ запустить; каждая программа узнаёт свой номер через tl.program_id и по нему вычисляет, какой кусок данных её.

CUDA C++Triton
Пишем код дляодного потокаодной программы (блока данных)
Единица данныхскалярвектор или тайл (tl.arange, двумерные блоки)
Shared memoryвыделяем и синхронизируем самираспределяет компилятор
Тензорные ядраособые инструкции (WMMA, MMA)tl.dot(a, b)
Объединение доступов к памятиследим самиделает компилятор
Контрольполныйменьше, но обычно хватает

Пять функций, на которых держится почти всё:

  • tl.program_id(axis) — номер программы в сетке (аналог номера блока потоковблок потоковEN thread block (CTA)До 1024 потоков, которые выполняются на одном SM и делят его shared memory. Ядро запускается сеткой (grid) из многих блоков. из урока 5.7.1);
  • tl.arange(0, BLOCK) — вектор индексов 0,1,…,BLOCK−10, 1, \dots, \text{BLOCK} - 1; BLOCK должен быть степенью двойки и константой компиляции (tl.constexpr);
  • tl.load(ptr + offs, mask=..., other=...) — прочитать блок из видеопамяти; ptr — адрес начала тензора, offs — смещения;
  • tl.store(ptr + offs, value, mask=...) — записать блок;
  • маска mask — булев вектор: где False, чтения и записи нет. Без неё последний неполный блок вылезет за конец массива.

Начнём с ядра, которое не делает ничего умного, зато показывает всю механику: сетку, номера программ, маски, запуск и проверку.

"""vector_add.py — первое ядро на Triton: сложение векторов. python vector_add.py"""
import os
import torch
import triton
import triton.language as tl
# без GPU: TRITON_INTERPRET=1 python vector_add.py — ядро выполнит интерпретатор на CPU (медленно)
DEVICE = "cpu" if os.environ.get("TRITON_INTERPRET") == "1" else "cuda"
@triton.jit
def add_kernel(x_ptr, y_ptr, out_ptr, n, BLOCK_SIZE: tl.constexpr):
pid = tl.program_id(axis=0) # номер программы (блока) в сетке: 0, 1, 2, …
offs = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) # индексы чисел, за которые отвечает этот блок
mask = offs < n # последний блок может вылезти за конец массива
x = tl.load(x_ptr + offs, mask=mask) # загрузка из видеопамяти (лишнее не читаем)
y = tl.load(y_ptr + offs, mask=mask)
tl.store(out_ptr + offs, x + y, mask=mask) # запись обратно (лишнее не пишем)
def add(x, y):
out = torch.empty_like(x)
n = out.numel()
grid = lambda meta: (triton.cdiv(n, meta["BLOCK_SIZE"]),) # сколько блоков запустить
add_kernel[grid](x, y, out, n, BLOCK_SIZE=1024)
return out
torch.manual_seed(0)
x = torch.rand(98_432, device=DEVICE) # 98 432 не делится на 1024 — маска пригодится
y = torch.rand(98_432, device=DEVICE)
torch.testing.assert_close(add(x, y), x + y) # упадёт с подробным сообщением, если ответ неверен
print("add: совпадает с x + y")
if DEVICE == "cuda":
for n in [2**20, 2**24, 2**27]:
x = torch.rand(n, device=DEVICE)
y = torch.rand(n, device=DEVICE)
for name, fn in [("triton", lambda: add(x, y)), ("torch ", lambda: x + y)]:
ms = triton.testing.do_bench(fn) # среднее время в миллисекундах, с прогревом
gbs = 3 * n * x.element_size() / (ms * 1e-3) / 1e9 # прочитали 2 вектора, записали 1
print(f"n = 2^{n.bit_length() - 1}, {name}: {ms:.3f} мс, {gbs:6.0f} ГБ/с")

Разберём запуск. grid — функция, которая по параметрам запуска возвращает размер сетки: ⌈98 432/1024⌉=97\lceil 98\,432 / 1024 \rceil = 97 программ. Программа 96 отвечает за индексы 98 304…99 32798\,304 \dots 99\,327, из которых существуют только 128 — маска отрежет остальные. add_kernel[grid](...) компилирует ядро при первом вызове (несколько секунд) и кладёт результат в кэш на диске. Тензоры PyTorch передаются в ядро как указатели на их первый элемент.

torch.testing.assert_close — главный инструмент проверки: сравнивает с допуском, подобранным под тип данных, и при ошибке печатает, сколько элементов разошлось и где самое большое расхождение.

Без видеокарты. Переменная TRITON_INTERPRET=1 включает интерпретатор: ядро выполняется на CPU поверх NumPy. Медленно, но для проверки корректности хватает, и внутри ядра работает обычный print. Так проверялись все ядра этого урока.

Чего ждать на RTX 4080 (оценка, а не замер). Сложение — memory-bound с интенсивностью 1/121/12 во FP32 (1 FLOP на 12 байт). На больших векторах и Triton, и x + y должны выйти на 80–90% от паспортных 716.8 ГБ/с; на векторе в 2202^{20} чисел (12 МБ трафика) — заметно меньше: ядро заканчивается за десятки микросекунд, и накладные расходы запуска становятся заметны. Обогнать PyTorch на сложении нельзя — оба упираются в одну и ту же память. Выигрыш начинается там, где можно слить операции.

Softmax по строкам — типичная операция-«клей» (во внимании, в выходном слое). Наивная версия на PyTorch — 5 ядер:

ШагЧитаетПишет
x.max(dim=1)xxвектор максимумов
x - x_maxxxzz
torch.exp(z)zznum
num.sum(dim=1)numвектор сумм
num / sumnumответ

Пять чтений и три записи матрицы — 8 проходов (векторы длины MM не в счёт). Слитое ядро читает строку один раз, считает всё в регистрах и пишет один раз: 2 прохода, в 4 раза меньше трафика. Ядро ниже — одна программа на строку, вся строка — один блок; если строка короче блока, хвост читается как −∞-\infty и не попадает ни в максимум, ни в сумму (e−∞=0e^{-\infty} = 0).

К прямому проходу добавим обратный. Для y=softmax(x)y = \mathrm{softmax}(x) и градиента dydy по выходу (якобиан softmax из урока 1.3.2):

dx=y⊙(dy−⟨dy,y⟩),dx = y \odot \big(dy - \langle dy, y \rangle\big),

где ⊙\odot — поэлементное умножение, а ⟨dy,y⟩=∑jdyj yj\langle dy, y \rangle = \sum_j dy_j\, y_j — одно число на строку. Это тоже редукция по строке, то есть то же ядро другой формулы. Чтобы autograd знал о наших ядрах, оборачиваем их в torch.autograd.Function:

"""softmax_triton.py — слитый softmax по строкам: прямой и обратный проход, autograd.Function, бенчмарк."""
import os
import torch
import triton
import triton.language as tl
DEVICE = "cpu" if os.environ.get("TRITON_INTERPRET") == "1" else "cuda"
@triton.jit
def softmax_fwd_kernel(x_ptr, y_ptr, n_cols, stride, BLOCK_SIZE: tl.constexpr):
row = tl.program_id(0) # одна программа — одна строка
cols = tl.arange(0, BLOCK_SIZE)
mask = cols < n_cols # BLOCK_SIZE — степень двойки, строка может быть короче
x = tl.load(x_ptr + row * stride + cols, mask=mask, other=-float("inf")).to(tl.float32)
x = x - tl.max(x, axis=0) # устойчивость (урок 1.3.2)
num = tl.exp(x) # exp(-inf) = 0: хвост не попадает в сумму
y = num / tl.sum(num, axis=0)
tl.store(y_ptr + row * stride + cols, y.to(y_ptr.dtype.element_ty), mask=mask)
@triton.jit
def softmax_bwd_kernel(y_ptr, dy_ptr, dx_ptr, n_cols, stride, BLOCK_SIZE: tl.constexpr):
row = tl.program_id(0)
cols = tl.arange(0, BLOCK_SIZE)
mask = cols < n_cols
y = tl.load(y_ptr + row * stride + cols, mask=mask, other=0.0).to(tl.float32)
dy = tl.load(dy_ptr + row * stride + cols, mask=mask, other=0.0).to(tl.float32)
dx = y * (dy - tl.sum(dy * y, axis=0)) # градиент softmax: y ⊙ (dy − ⟨dy, y⟩)
tl.store(dx_ptr + row * stride + cols, dx.to(dx_ptr.dtype.element_ty), mask=mask)
def _launch(kernel, *tensors):
n_rows, n_cols = tensors[0].shape
out = torch.empty_like(tensors[0])
kernel[(n_rows,)](*tensors, out, n_cols, out.stride(0), BLOCK_SIZE=triton.next_power_of_2(n_cols))
return out
class TritonSoftmax(torch.autograd.Function):
"""Своё ядро как обычная операция PyTorch: autograd вызовет backward сам."""
@staticmethod
def forward(ctx, x):
y = _launch(softmax_fwd_kernel, x.contiguous())
ctx.save_for_backward(y) # для градиента достаточно выхода
return y
@staticmethod
def backward(ctx, dy):
(y,) = ctx.saved_tensors
return _launch(softmax_bwd_kernel, y, dy.contiguous())
triton_softmax = TritonSoftmax.apply
def naive_softmax(x): # «как написал бы человек»: 5 ядер в eager-режиме
x_max = x.max(dim=1, keepdim=True).values
z = x - x_max
num = torch.exp(z)
return num / num.sum(dim=1, keepdim=True)
torch.manual_seed(0)
x = torch.randn(1823, 781, device=DEVICE, requires_grad=True) # «неудобные» размеры: проверяем маски
y = triton_softmax(x)
torch.testing.assert_close(y, torch.softmax(x, dim=1))
dy = torch.randn_like(y)
(gx,) = torch.autograd.grad(y, x, dy)
(gx_ref,) = torch.autograd.grad(torch.softmax(x, dim=1), x, dy)
torch.testing.assert_close(gx, gx_ref)
print("softmax: прямой и обратный проход совпадают с torch.softmax")
if DEVICE == "cuda":
compiled = torch.compile(naive_softmax)
for n_cols in [256, 1024, 4096, 16384]:
x = torch.randn(4096, n_cols, device=DEVICE, dtype=torch.bfloat16)
line = []
for name, fn in [("naive", naive_softmax), ("torch", lambda t: torch.softmax(t, 1)),
("compile", compiled), ("triton", triton_softmax)]:
ms = triton.testing.do_bench(lambda: fn(x))
line.append(f"{name} {2 * x.numel() * x.element_size() / (ms * 1e-3) / 1e9:5.0f}")
print(f"n_cols = {n_cols:5d}, ГБ/с: " + " ".join(line))

Обратите внимание на .to(tl.float32) после загрузки: ядро принимает тензоры BF16, но считает экспоненты и суммы во FP32 и только при записи приводит обратно. Это та же логика, что у autocast (урок 5.4.1): хранить узко, считать широко.

Чего ждать (оценка). naive — в несколько раз медленнее остальных. torch (встроенное ядро PyTorch), compile (torch.compile сам сгенерирует почти такое же ядро на Triton) и triton — близко друг к другу и к пределу памяти. При n_cols = 16384 строка в BF16 занимает 32 КБ, и ядру с одной программой на строку может не хватить регистров — производительность просядет. Официальный туториал Triton разбирает, как это лечить.

Сделайте сами: слитый RMSNorm (forward)

RMSNorm из урока 4.5.3: y=x/mean(x2)+ε⋅wy = x / \sqrt{\mathrm{mean}(x^2) + \varepsilon} \cdot w. По образцу softmax напишите ядро «одна программа — одна строка». Подсказки: хвост читайте как 0 (он не меняет сумму квадратов), веса ww — одни на все строки, считайте во FP32. Решение:

import os
import torch
import triton
import triton.language as tl
DEVICE = "cpu" if os.environ.get("TRITON_INTERPRET") == "1" else "cuda"
@triton.jit
def rmsnorm_kernel(x_ptr, w_ptr, y_ptr, n_cols, stride, eps, BLOCK_SIZE: tl.constexpr):
row = tl.program_id(0)
cols = tl.arange(0, BLOCK_SIZE)
mask = cols < n_cols
x = tl.load(x_ptr + row * stride + cols, mask=mask, other=0.0).to(tl.float32) # считаем во FP32
w = tl.load(w_ptr + cols, mask=mask, other=0.0).to(tl.float32)
rms = tl.sqrt(tl.sum(x * x, axis=0) / n_cols + eps) # нули в хвосте сумму не меняют
tl.store(y_ptr + row * stride + cols, (x / rms * w).to(y_ptr.dtype.element_ty), mask=mask)
def rmsnorm(x, w, eps=1e-6):
y = torch.empty_like(x)
rmsnorm_kernel[(x.shape[0],)](x, w, y, x.shape[1], x.stride(0), eps,
BLOCK_SIZE=triton.next_power_of_2(x.shape[1]))
return y
x = torch.randn(512, 896, device=DEVICE) # 896 — ширина Qwen2.5-0.5B
w = torch.randn(896, device=DEVICE)
ref = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + 1e-6) * w
torch.testing.assert_close(rmsnorm(x, w), ref)
print("rmsnorm: совпадает")

В PyTorch без компиляции RMSNorm — это pow, mean, add, rsqrt, два умножения: шесть ядер. Сравните ГБ/с своего ядра, наивной версии и torch.nn.functional.rms_norm.

Теперь самое интересное — матмул из практики урока 5.7.1, но на настоящей SRAM и тензорных ядрах. Каждая программа считает тайл CC размером BLOCK_M×BLOCK_N\text{BLOCK\_M} \times \text{BLOCK\_N}: идёт по KK тайлами, загружает тайл AA и тайл BB и накапливает tl.dot(a, b, acc) во FP32. Двумерные блоки адресов строятся «бродкастингом» (урок 0.3.2): offs_m[:, None] — столбец, offs_k[None, :] — строка, их сумма — таблица адресов тайла.

Две новые вещи:

  • порядок программ группами (GROUP_M). Если нумеровать тайлы CC построчно, подряд работающие программы читают одну полосу AA и разные полосы BB — по всей ширине матрицы. При группировке по 8 строк тайлов подряд идут программы с 8 полосами AA и несколькими полосами BB, и все эти данные успевают переиспользоваться из L2. Так преодолевается предел «тайл в shared memory даёт 64 FLOP/байт» из урока 5.7.1;
  • автоподбор (@triton.autotune). Лучшие размеры тайлов, число варпов и стадий конвейера загрузки (num_stages — сколько тайлов грузится заранее, пока считаются предыдущие) зависят от карты и размеров. Triton прогонит все конфигурации на первом вызове с новыми (M, N, K) и запомнит лучшую.
"""matmul_triton.py — тайловый матмул на Triton с автоподбором тайлов и сравнение с cuBLAS."""
import os
import torch
import triton
import triton.language as tl
DEVICE = "cpu" if os.environ.get("TRITON_INTERPRET") == "1" else "cuda"
CONFIGS = [
triton.Config({"BLOCK_M": 128, "BLOCK_N": 128, "BLOCK_K": 32}, num_stages=3, num_warps=8),
triton.Config({"BLOCK_M": 128, "BLOCK_N": 64, "BLOCK_K": 32}, num_stages=4, num_warps=4),
triton.Config({"BLOCK_M": 64, "BLOCK_N": 128, "BLOCK_K": 32}, num_stages=4, num_warps=4),
triton.Config({"BLOCK_M": 64, "BLOCK_N": 64, "BLOCK_K": 64}, num_stages=3, num_warps=4),
]
if DEVICE == "cpu": # интерпретатор не умеет замерять время, а автоподбору нужны замеры:
CONFIGS = CONFIGS[-1:] # с одной конфигурацией Triton пропускает перебор
@triton.autotune(configs=CONFIGS, key=["M", "N", "K"]) # перебрать конфигурации и запомнить лучшую для (M, N, K)
@triton.jit
def matmul_kernel(a_ptr, b_ptr, c_ptr, M, N, K,
stride_am, stride_ak, stride_bk, stride_bn, stride_cm, stride_cn,
BLOCK_M: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr, GROUP_M: tl.constexpr):
# 1. Какой тайл C считает эта программа. Порядок «группами по GROUP_M строк тайлов»:
# соседние программы берут одни и те же полосы A и B, и те приходят из L2, а не из видеопамяти
pid = tl.program_id(0)
num_pid_m = tl.cdiv(M, BLOCK_M)
num_pid_n = tl.cdiv(N, BLOCK_N)
num_pid_in_group = GROUP_M * num_pid_n
first_pid_m = (pid // num_pid_in_group) * GROUP_M
group_size_m = min(num_pid_m - first_pid_m, GROUP_M)
pid_m = first_pid_m + ((pid % num_pid_in_group) % group_size_m)
pid_n = (pid % num_pid_in_group) // group_size_m
# 2. Указатели на первые тайлы A (BLOCK_M × BLOCK_K) и B (BLOCK_K × BLOCK_N)
offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
offs_k = tl.arange(0, BLOCK_K)
a_ptrs = a_ptr + offs_m[:, None] * stride_am + offs_k[None, :] * stride_ak # двумерная сетка адресов
b_ptrs = b_ptr + offs_k[:, None] * stride_bk + offs_n[None, :] * stride_bn
# 3. Идём по K тайлами, накапливая сумму во FP32 (в регистрах)
acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
for k in range(0, tl.cdiv(K, BLOCK_K)):
k_left = K - k * BLOCK_K # сколько столбцов K осталось: маска для хвоста
a = tl.load(a_ptrs, mask=(offs_m[:, None] < M) & (offs_k[None, :] < k_left), other=0.0)
b = tl.load(b_ptrs, mask=(offs_k[:, None] < k_left) & (offs_n[None, :] < N), other=0.0)
acc = tl.dot(a, b, acc) # матмул тайлов на тензорных ядрах: acc += a @ b
a_ptrs += BLOCK_K * stride_ak # сдвиг к следующему тайлу по K
b_ptrs += BLOCK_K * stride_bk
# 4. Записываем тайл C, приведя к типу выхода
c_ptrs = c_ptr + offs_m[:, None] * stride_cm + offs_n[None, :] * stride_cn
tl.store(c_ptrs, acc.to(c_ptr.dtype.element_ty), mask=(offs_m[:, None] < M) & (offs_n[None, :] < N))
def matmul(a, b):
M, K = a.shape
K2, N = b.shape
assert K == K2, "внутренние размеры не совпадают"
c = torch.empty((M, N), device=a.device, dtype=a.dtype)
grid = lambda meta: (triton.cdiv(M, meta["BLOCK_M"]) * triton.cdiv(N, meta["BLOCK_N"]),)
matmul_kernel[grid](a, b, c, M, N, K, a.stride(0), a.stride(1), b.stride(0), b.stride(1),
c.stride(0), c.stride(1), GROUP_M=8)
return c
torch.manual_seed(0)
dtype = torch.bfloat16 if DEVICE == "cuda" else torch.float32
a = torch.randn(1000, 700, device=DEVICE, dtype=dtype) # размеры не кратны тайлам
b = torch.randn(700, 900, device=DEVICE, dtype=dtype)
torch.testing.assert_close(matmul(a, b), a @ b, atol=0.05, rtol=0.02) # допуск под BF16
print("matmul: совпадает с a @ b")
if DEVICE == "cuda":
for n in [1024, 2048, 4096, 8192]:
a = torch.randn(n, n, device=DEVICE, dtype=dtype)
b = torch.randn(n, n, device=DEVICE, dtype=dtype)
line = []
for name, fn in [("cuBLAS", lambda: a @ b), ("triton", lambda: matmul(a, b))]:
ms = triton.testing.do_bench(fn)
line.append(f"{name} {2 * n**3 / (ms * 1e-3) / 1e12:5.1f} TFLOPS")
print(f"n = {n}: " + " ".join(line))

Проверим, что конфигурации помещаются в shared memory Ada (до 99 КБ на блок): самая большая — тайлы AA 128×32128 \times 32 и BB 32×12832 \times 128 в BF16, по 8 КБ, три стадии конвейера: 3⋅16=483 \cdot 16 = 48 КБ.

Чего ждать (оценка). На n=4096…8192n = 4096 \dots 8192 cuBLAS в BF16 — порядка 70–85% от паспортных 97.5 TFLOPS; Triton — от 80% до почти 100% от cuBLAS (официальный туториал утверждает, что результат «на уровне cuBLAS»). На n=1024n = 1024 оба заметно ниже: программ (8⋅8=648 \cdot 8 = 64 при тайле 128) меньше, чем SM (76), и часть карты простаивает. Попробуйте добавить в список свои конфигурации и посмотрите, какую выберет автоподбор (matmul_kernel.best_config).

ИнструментЧто этоКогда брать
torch.compiletorch.compileEN torch.compileКомпилятор PyTorch 2: TorchDynamo перехватывает граф операций модели, TorchInductor генерирует слитые ядра (на GPU — на Triton). Ускоряет обучение и инференс без изменения кода модели; первый вызов долгий из-за компиляции.автоматическое слияние, генерирует Tritonвсегда сначала: бесплатно и часто достаточно
Tritonядра на Python, блокаминовый алгоритм: своё внимание, слитый слой, квантованный матмул
CUDA C++полный контроль над потоками и памятьюпоследние проценты скорости, особые инструкции железа, библиотеки
CUTLASS / CuTe (NVIDIA)шаблоны C++ (и CuTe DSL на Python) для матмулов и вниманияFlashAttention-3, ядра под Hopper и Blackwell
TileLangязык тайловых ядер на Python (Пекинский университет)ядра DeepSeek, переносимость на разные ускорители

Порядок действий на практике: профайлер (урок 5.7.1) → torch.compile → если узкое место осталось и понятно, какой алгоритм нужен, — Triton → и только если и этого мало, CUDA. Ядро без теста против эталонной реализации на PyTorch не пишут вообще: ошибка в маске даёт не падение, а тихо неверные числа.

Видеокарты в браузере нет, но модель программирования Triton можно потрогать и на NumPy. В задании — «мини-Triton»: load и store с масками и launch, который вызывает программы сетки по очереди. Напишите на нём три ядра урока; тест на трафик сверит матмул со счётом из урока 5.7.1.

Ядра в стиле Triton на NumPynumpy · нужен интернет6 тестов
import numpy as np

# ── Мини-Triton на NumPy: готовые помощники ──────────────────────────────────
# Настоящий Triton компилирует ядро для GPU и запускает все программы сетки параллельно.
# Здесь launch вызывает программы по очереди, а load/store ведут себя как tl.load/tl.store.
STATS = {"loads": 0}                                  # сколько чисел прочитано через load


def arange(n):
    """Как tl.arange(0, n)."""
    return np.arange(n)


def load(arr, offs, mask, other=0.0):
    """Как tl.load: читает плоский массив arr по индексам offs там, где mask, иначе — other.
    Чтение мимо массива без маски — ошибка IndexError, как сбой памяти на GPU."""
    flat = arr.reshape(-1)
    out = np.full(offs.shape, other, dtype=float)
    out[mask] = flat[offs[mask]]
    STATS["loads"] += int(np.count_nonzero(mask))
    return out


def store(arr, offs, values, mask):
    """Как tl.store: пишет values в плоский массив arr по индексам offs там, где mask."""
    flat = arr.reshape(-1)                            # вид на тот же массив, без копии
    flat[offs[mask]] = values[mask]


def launch(kernel, grid, *args):
    """Как kernel[grid](*args): вызывает kernel(pid, *args) для каждого pid сетки; pid — кортеж."""
    for pid in np.ndindex(*grid):
        kernel(pid, *args)


def cdiv(a, b):
    """Как triton.cdiv: деление с округлением вверх."""
    return (a + b - 1) // b


# ── Ваша часть ───────────────────────────────────────────────────────────────
def add_kernel(pid, x, y, out, n, BLOCK):
    """Программа номер pid[0] складывает свой кусок из BLOCK чисел: out = x + y.
    Последний блок может вылезти за конец массивов — нужна маска offs < n."""
    # TODO: offs = pid[0] * BLOCK + arange(BLOCK); mask; load, load, store
    ...


def softmax_kernel(pid, x, out, n_cols, BLOCK):
    """Программа номер pid[0] считает устойчивый softmax строки pid[0] матрицы x (n_rows × n_cols).
    BLOCK ≥ n_cols — степень двойки; хвост блока читается как -inf, чтобы не попасть в сумму."""
    # TODO
    ...


def matmul_kernel(pid, A, B, C, M, N, K, BM, BN, BK):
    """Программа (pid_m, pid_n) считает тайл C[pid_m·BM : …, pid_n·BN : …] = A @ B.
    A: (M, K), B: (K, N), C: (M, N) — плоские индексы строка·ширина + столбец.
    Цикл по K тайлами по BK: загрузить тайл A (BM × BK) и тайл B (BK × BN) с масками, acc += a @ b."""
    pid_m, pid_n = pid
    offs_m = pid_m * BM + arange(BM)
    offs_n = pid_n * BN + arange(BN)
    acc = np.zeros((BM, BN))
    # TODO: цикл по k0 in range(0, K, BK), маски a_mask и b_mask, load, acc += a @ b
    ...
    # TODO: store тайла C с маской


def triton_matmul(A, B, BM=16, BN=16, BK=16):
    """Запуск matmul_kernel на сетке (cdiv(M, BM), cdiv(N, BN)). Возвращает C."""
    M, K = A.shape
    N = B.shape[1]
    C = np.zeros((M, N))
    # TODO: launch(...)
    return C


if __name__ == "__main__":
    x, y = np.arange(10.0), np.ones(10)
    out = np.zeros(10)
    launch(add_kernel, (cdiv(10, 4),), x, y, out, 10, 4)
    print("add:", out)   # ждём [1, 2, ..., 10]
Проверь себя6 вопросов
1 Вектор из 10 000 чисел складывается ядром с BLOCK_SIZE = 1024. Сколько программ нужно запустить?
2 Что произойдёт, если убрать маску из tl.load в ядре сложения при nn, не кратном размеру блока?
3 Почему хвост строки в ядре softmax читается с other=-float("inf"), а в ядре RMSNorm — с other=0.0?
4 Наивный softmax на PyTorch делает 8 проходов по матрице, слитое ядро — 2. Во сколько раз меньше трафик памяти у слитого ядра?
5 Какие утверждения о torch.autograd.Function верны? Выберите все.Можно выбрать несколько ответов
6 Матмул 1024×10241024 \times 1024 с тайлом CC 128×128128 \times 128. Сколько программ в сетке? (Сравните с числом SM у RTX 4080 — 76.)
  • Ядро на Triton описывает одну программу, которая обрабатывает блок данных; программы запускаются сеткой и узнают свой номер через tl.program_id. Маски в tl.load/tl.store защищают последний неполный блок.
  • Каждое ядро сверяют с PyTorch через torch.testing.assert_close и замеряют triton.testing.do_bench в ГБ/с или TFLOPS — с оглядкой на roofline. Без GPU помогает TRITON_INTERPRET=1.
  • Выигрыш дают слияние (softmax, RMSNorm: в разы меньше трафика) и тайлы на тензорных ядрах (tl.dot) с порядком обхода, который бережёт L2. В обучение ядро встраивается через torch.autograd.Function.
  • Порядок действий: профайлер → torch.compile → Triton → CUDA. Альтернативы для особых случаев — CUTLASS/CuTe и TileLang, на котором пишет ядра DeepSeek.

Дальше: Четыре видеокарты: DDP и FSDP на практике — от одной карты ко всем четырём: обучение GPT через torchrun, замер масштабирования и узкое место PCIe.