Skip to content

08. Programming Models CUDA Triton | 编程模型演进 ​

难度: Medium | 环境: GPU optional

🚀 云端运行环境

本章节的实战代码可以点击以下链接在免费 GPU 算力平台上直接运行:

Open In ColabOpen In Studio (国内推荐:魔搭社区免费实例)

标签: 算子编程, CUDA, Triton | 目标人群: 编译与系统入门者


本节导读 ​

从 PyTorch 写法走到 CUDA / Triton,并不是为了“更底层”本身,而是因为框架级算子组合并不总能贴住硬件执行粒度。只要中间张量太多、launch 太碎,或者数据布局和 tile 组织没法显式控制,性能上限就会很快被访存和调度开销卡住。

本节比较框架、CUDA 与 Triton 对执行粒度、数据搬运和并行组织的控制范围,为后续算子优化与内核实现建立编程模型入口。学习重点是判断现有抽象缺少哪一层控制权,而不是默认下沉到自定义内核;CUDA 执行层级在第 15–16 节展开,Triton Program 映射在第 18 节展开。

关键词: grid, block, kernel

从张量表达式到 CUDA 与 Triton 映射


前置阅读 ​

导语: 先掌握张量表达和 GPU 资源模型,再比较框架、CUDA 与 Triton 分别暴露哪些控制能力。

Q1:为什么我们需要用 CUDA 或 Triton 编写自定义算子? ​

PyTorch 原生操作方便,但它默认把很多执行细节交给框架和后端。

当一个计算链里出现大量小算子、重复的 HBM 往返、或者对布局和 tile 有强约束时,原生写法往往会留下更多中间张量和 launch 开销。自定义 CUDA 或 Triton 的价值,不是“更底层”本身,而是把算子组织方式重新对齐到硬件执行粒度。

所以这类问题的关键,不是“要不要手写 kernel”,而是“哪些数据流和执行流必须由我们显式控制”。

Q1小验证:为什么原生操作可能慢 ​

先想是不是中间张量太多。

python
def memory_traffic(num_ops, tensor_bytes, fused=False):
    """估算逐算子链与完全融合链的外部读写字节量。"""
    if num_ops <= 0 or tensor_bytes <= 0:
        raise ValueError('num_ops and tensor_bytes must be positive')
    reads = num_ops if not fused else 1
    writes = num_ops if not fused else 1
    return {'reads': reads, 'writes': writes, 'traffic_bytes': (reads + writes) * tensor_bytes}

cases = [
    ('separate_ops', memory_traffic(3, 8_000_000, fused=False)),
    ('fused_ops', memory_traffic(3, 8_000_000, fused=True)),
    ('longer_chain', memory_traffic(5, 8_000_000, fused=False)),
]
for name, traffic in cases:
    print(name, '->', traffic)
assert cases[1][1]['traffic_bytes'] < cases[0][1]['traffic_bytes']

Q2:CUDA 的线程层级结构与 GPU 硬件执行单元如何对应? ​

CUDA 的层级不是语法装饰,而是把“任务怎么切”映射到“硬件怎么调度”。

  • grid 决定一次 kernel launch 覆盖多少独立工作;
  • block 决定哪一组线程能共享数据和同步;
  • warp 是硬件实际调度的基本单位;
  • thread 是最细的计算粒度。

理解这层映射后,就能看出为什么同样的代码,线程数、block 数和 warp 利用率不同,性能会差很多。

Q2小验证:层级怎么对应 ​

把执行层级记清楚,后面读 kernel 会更顺。

python
def block_launch_stats(num_threads, warp_size=32):
    """计算 Warp 数、空闲 lane 和线程槽利用率。"""
    if num_threads <= 0 or warp_size <= 0:
        raise ValueError('num_threads and warp_size must be positive')
    warps = (num_threads + warp_size - 1) // warp_size
    active_lanes = num_threads
    wasted_lanes = warps * warp_size - num_threads
    occupancy = active_lanes / (warps * warp_size)
    return {'warps': warps, 'wasted_lanes': wasted_lanes, 'occupancy': round(occupancy, 2)}

for threads in [48, 64, 96, 128]:
    print(threads, 'threads ->', block_launch_stats(threads))
assert block_launch_stats(48)['wasted_lanes'] == 16
try:
    block_launch_stats(0)
except ValueError:
    pass
else:
    raise AssertionError('空 Block 必须报错')

Q3:跨线程共享和同步机制如何限制和优化性能? ​

跨线程共享真正解决的是“同一份数据能不能在片上被复用”。

如果数据只被读一次,shared memory 和同步的开销可能不值;如果同一份数据会被 block 内多个线程反复使用,先放到 shared memory 再同步,通常能换回更多 HBM 访问的节省。

所以这里不是“共享越多越好”,而是“复用收益是否足以覆盖同步代价”。

Q3小验证:共享为什么会和同步绑定 ​

复用越多,协作就越重要。

python
def shared_tradeoff(reuse_times, sync_points, tile_bytes, bandwidth_bytes_per_us, sync_cost_us):
    """把减少的 HBM 字节换算成时间,并与同步时间比较。"""
    if reuse_times <= 0 or sync_points < 0 or tile_bytes <= 0 or bandwidth_bytes_per_us <= 0 or sync_cost_us < 0:
        raise ValueError('invalid reuse or timing inputs')
    saved_hbm_bytes = max(reuse_times - 1, 0) * tile_bytes
    saved_hbm_us = saved_hbm_bytes / bandwidth_bytes_per_us
    synchronization_us = sync_points * sync_cost_us
    return {'saved_hbm_bytes': saved_hbm_bytes, 'saved_hbm_us': saved_hbm_us, 'synchronization_us': synchronization_us, 'net_saved_us': saved_hbm_us - synchronization_us, 'worth_it': saved_hbm_us > synchronization_us}

for reuse_times, sync_points in [(1, 0), (2, 1), (4, 1), (4, 3)]:
    print((reuse_times, sync_points), '->', shared_tradeoff(reuse_times, sync_points, 64_000, 16_000, 2.0))
assert shared_tradeoff(1, 0, 64_000, 16_000, 2.0)['worth_it'] is False
assert shared_tradeoff(4, 1, 64_000, 16_000, 2.0)['worth_it'] is True

Q4:Triton 为什么能降低算子开发门槛? ​

Triton 的抽象重点,不是“帮你自动变快”,而是把 tile、block 和布局这些样板性细节收起来,让你先把张量切分方式写清楚。

和 CUDA 相比,它并不是取消底层约束,而是把底层约束收束成更少的显式概念:布局、mask、tile 和 program 关系。

所以 Triton 的门槛更低,是因为它减少了模板代码;但它的性能边界,仍然取决于你对执行粒度和数据布局的理解。

Q4小验证:Triton 抽象了什么 ​

先看块组织,再看算子细节。

python
def triton_candidate(num_elements, block_size, layout_contiguous, dtype_supported, max_padding_ratio=0.25):
    """检查布局、dtype 和边界填充是否满足教学候选门槛。"""
    if num_elements <= 0 or block_size <= 0 or not 0 <= max_padding_ratio < 1:
        raise ValueError('invalid shape or padding threshold')
    programs = (num_elements + block_size - 1) // block_size
    padding_ratio = (programs * block_size - num_elements) / (programs * block_size)
    reasons = []
    if not layout_contiguous:
        reasons.append('non_contiguous_layout')
    if not dtype_supported:
        reasons.append('unsupported_dtype')
    if padding_ratio > max_padding_ratio:
        reasons.append('padding_ratio')
    return {'ready': not reasons, 'programs': programs, 'padding_ratio': padding_ratio, 'reasons': reasons}

ready = triton_candidate(1000, 128, True, True)
blocked = triton_candidate(1000, 128, False, True)
assert ready['ready'] is True
assert blocked['reasons'] == ['non_contiguous_layout']
print(ready)
print(blocked)

编程模型与执行路径 ​

  • 自定义算子不是为了炫技,而是为了更好地控制执行路径。
  • CUDA 和 Triton 是不同范式,但目标都在靠近硬件。
  • 共享和同步是一起看的,不能只谈一个。
  • 编程模型的核心是映射,不是语法本身。

相关阅读 ​

导语: 如果想继续把编程模型和 kernel 组织方式补完整,可以接着看这些页。


Released under the MIT License.