全部术语

GLOSSARY ENTRY

GPU Runtime:Stream、Event、Fusion 与 torch.compile

  • CUDA Stream
  • CUDA Event
  • Kernel Fusion
  • torch.compile
  • Caching Allocator

用 stream 排列设备工作、event 建立跨 stream 依赖,再通过 fusion/编译减少中间读写与 launch;allocator 管理 storage 复用,这些机制互相关联但各自不改变模型语义。

五个层次,不是一种“GPU 优化”

  • Stream:一个设备上的有序 command queue;同一 stream 按顺序执行,不同 streams 只有在依赖与硬件资源允许时才可能重叠。
  • Event:记录在 stream timeline 上的标记,可用于计时或让另一个 stream 等待;它不是 CPU barrier 的同义词。
  • Kernel fusion:把多个算子放进一个 kernel,减少 launches、intermediate HBM traffic,有时提高 locality。
  • torch.compile:捕获/变换 graph,并由 backend 生成或选择实现;fusion 是可能结果之一,不是唯一结果或保证。
  • Caching allocator:复用 CUDA memory segments,减少频繁 device allocation;reserved > allocated 不自动等于泄漏。

它们都位于执行层,不改变 Transformer blockSDPA 的数学契约。是否真正采用某个 kernel,仍由 Backend Dispatch/Fallback和编译产物决定。

Kernel Backend、Triton Blocking 与 Persistent Kernel进一步解释生成或选中的 concrete kernel 如何分块、占用 warps/片上资源,以及 persistent device-side scheduling 与普通 launch、CUDA Graph 的边界。

Stream 与 Event:显式表达 happens-before

设 default stream 创建 x,y,side stream 执行 z=x@y。安全依赖是:

default stream: produce x,y ─ record(inputs_ready) ───────── wait(work_done) ─ consume z
                                      │                              ▲
side stream:                    wait(inputs_ready) ─ matmul ─ record(work_done)

Event 被 record 后,只有排在它之前的 stream work 完成才视为 complete;other_stream.wait_event(event) 把之后 work 延迟到该 event 完成。它不会阻塞 CPU。event.synchronize()torch.cuda.synchronize() 才让 host 等待。

Memory lifetime 也要遵守 stream 依赖。PyTorch allocator 需要知道某个 tensor 在非创建 stream 上仍被使用;框架/算子通常处理常规情况,自定义 stream 代码可能需要 record_stream 或显式 event,避免 storage 被 allocator 过早复用。正确结果偶尔“看起来没坏”不能证明没有 race。

对象逻辑作用常见误解
current/default stream当前线程/设备提交工作的位置“默认 stream 会自动等所有 streams”
side stream可并发的另一有序队列“不同 stream 必然并行”
CUDA Eventstream timeline dependency/timestamp“record 时 kernel 已完成”
synchronizehost 或 stream 等待边界“计时不需要同步”

Fusion 减少什么,不减少什么

对:

Y=SiLU(X+A)(XA)Y=\operatorname{SiLU}(X+A)\odot(X-A)

Eager 可能执行 add、SiLU、sub、multiply 四个 elementwise kernels,并把中间 tensor 往返 HBM。一个 fused pointwise kernel 可对每个 element 一次加载 X,A,在 registers 中完成运算并写 Y

Fusion 的主要收益来源:

  • 更少 CPU/framework dispatch 与 kernel launches;
  • 更少 intermediate allocations 和 HBM read/write;
  • 更好的 producer-consumer locality。

它没有让必须执行的 elementwise 数学消失,也不会自动把任意两个大 GEMM 合成一个更高吞吐 GEMM。Fusion 可能因寄存器压力、code size、复杂 reduction、动态 shape、alias/mutation、unsupported op 或 graph break 失败/退化;过度 fusion 甚至降低 occupancy。

CUDA Graph减少稳定 kernel 序列的 replay launch overhead;fusion 减少 kernel 数与 intermediate IO;torch.compile可以先产生 fused kernels,再被 graph capture。三者可组合但收益来源不同。

torch.compile 的语义与动态边界

torch.compile(fn)通常由 graph capture、guards、backend lowering/codegen 组成。首次调用可能包含 compile/autotune,不应与 steady-state replay 混测。Input dtype、device、rank、shape/stride、Python globals/control flow 等条件形成 guards;不满足时可能 recompile、graph break 或 fallback eager。

部署需分别记录:

  • cold compile latency 与 cache persistence;
  • 代表性 shape buckets 的 compile count/cache hit;
  • graph breaks 与原因;
  • 编译前后输出容差;
  • profiler 中 kernel count/name 与 end-to-end latency;
  • generated code 的 workspace/allocator/graph-capture 兼容性。

“成功调用 compiled function”不证明整模型 fullgraph,也不证明每个 op 都 fused。backend="eager" 可验证 Dynamo capture 但不提供 Inductor codegen/fusion;backend 是 API 配置,实际 kernel 仍需 profiler 证据。

可运行的 stream/event 与 fusion 证据

实验问题:side stream matmul 经 event 同步后是否与 default-stream reference 一致?一个四算子 pointwise graph 经 torch.compile 后,本环境 profiler 是否把多个 eager CUDA kernels 合为更少的 compiled kernels?

import platform,warnings
import torch
import torch.nn.functional as F

torch.manual_seed(199)
DEVICE=torch.device("cuda" if torch.cuda.is_available() else "cpu")

def pointwise(x,y): return F.silu(x+y)*(x-y)

def cuda_kernel_names(fn):
    from torch.profiler import ProfilerActivity,profile
    with profile(activities=[ProfilerActivity.CPU,ProfilerActivity.CUDA]) as prof:
        output=fn()
    torch.cuda.synchronize()
    return output,[e.name for e in prof.events() if str(e.device_type)=="DeviceType.CUDA"]

def main():
    shape=(512,512) if DEVICE.type=="cuda" else (64,64)
    x=torch.randn(shape,device=DEVICE); y=torch.randn_like(x)
    if DEVICE.type=="cuda":
        side=torch.cuda.Stream(); ready=torch.cuda.Event(); done=torch.cuda.Event()
        ready.record(torch.cuda.current_stream())
        with torch.cuda.stream(side):
            side.wait_event(ready); side_output=x@y; done.record(side)
        torch.cuda.current_stream().wait_event(done)
        reference_matmul=x@y
        torch.testing.assert_close(side_output,reference_matmul)

        with warnings.catch_warnings():
            warnings.filterwarnings("ignore",message=r"`torch.jit.script_method` is deprecated.*",
                                    category=DeprecationWarning)
            compiled=torch.compile(pointwise,fullgraph=True); compiled(x,y)
        torch.cuda.synchronize()
        eager,eager_kernels=cuda_kernel_names(lambda:pointwise(x,y))
        compiled_output,compiled_kernels=cuda_kernel_names(lambda:compiled(x,y))
        torch.testing.assert_close(compiled_output,eager,rtol=1e-5,atol=1e-6)
        assert len(eager_kernels)>=3 and len(compiled_kernels)<=len(eager_kernels)
        print(f"stream_event_matmul_max_abs_diff={(side_output-reference_matmul).abs().max():.3e}")
        print(f"eager_cuda_kernel_count={len(eager_kernels)} names={eager_kernels}")
        print(f"compiled_cuda_kernel_count={len(compiled_kernels)} names={compiled_kernels}")
        print(f"compiled_vs_eager_max_abs_diff={(compiled_output-eager).abs().max():.3e}")
    else:
        eager=pointwise(x,y); compiled=torch.compile(pointwise,backend="eager",fullgraph=True)
        compiled_output=compiled(x,y); torch.testing.assert_close(compiled_output,eager)
        print("stream_event_matmul=CUDA unavailable; CPU correctness branch")
        print("kernel_counts=CUDA profiler unavailable")
        print(f"compiled_vs_eager_max_abs_diff={(compiled_output-eager).abs().max():.3e}")
    name=torch.cuda.get_device_name(0) if DEVICE.type=="cuda" else "CPU"
    print(f"python={platform.python_version()} torch={torch.__version__}")
    print(f"device={DEVICE} name={name} dtype={x.dtype} shape={shape}")

if __name__=="__main__":
    with torch.inference_mode(): main()

完整文件位于 examples/gpu_runtime_reference.py。2026-08-13 在 PyTorch 2.13.0+cu130、RTX 3080 上,stream/event matmul 与 reference 最大差 0;pointwise eager profiler 观察到 4 个 CUDA kernels,compiled 路径观察到一个 triton_poi_fused_add_mul_silu_sub_0,输出最大差 1.907e-06

这只是当前 backend、shape 和版本的 profiler 证据,不是 torch.compile 永远融合成一个 kernel 的保证。脚本没有报告性能:compile cold start 和 profiler overhead 很大,正确的性能实验应在预编译/warmup 后按 Benchmark 方法单独计时,并记录 recompiles。

Allocator、动态 batch 与常见失败

Caching allocator 从 CUDA 保留 segments,再把 blocks 分给 tensors。因此:

  • tensor 删除后 allocated 可下降,而 reserved 保持以供复用;
  • fragmentation、不同 streams 的 pending uses、CUDA Graph private pools 会让部分 reserved 暂不可复用;
  • empty_cache()释放未占用 cache 给其他进程,但不会释放 live tensors,也不自动提高当前进程可用模型容量;
  • custom kernels、NCCL、driver/context allocations 可能不在 PyTorch allocated 中。

Benchmark/容量模型给出 allocated/reserved/peak 实测。动态 Continuous Batching会改变 shapes 与 lifetimes,可能触发 recompile、选择不同 compiled graph 或破坏 CUDA Graph bucket;allocator 与 scheduler 需共同避免每 step 大量 allocation/free。

常见失败:

  • side stream 没等 producer event,就读取尚未完成的数据;
  • current stream 消费 side output 前没等 done event;
  • 临时 tensor 在 side stream 完成前离开作用域并被复用;
  • benchmark 把 compile/autotune 只算在 A,不算在 B,或完全不报告 cold start;
  • graph break 后仍宣称“整模型 compiled”;
  • compiled kernel 数减少但寄存器/occupancy 恶化,端到端反而更慢;
  • 把 reserved memory 当泄漏,盲目每 step empty_cache() 增加同步与 allocation 开销;
  • 用 stream 增加并发,却忽略 collective/default-stream 依赖造成全局串行。

参考资料