五个层次,不是一种“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 block 或 SDPA 的数学契约。是否真正采用某个 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 Event | stream timeline dependency/timestamp | “record 时 kernel 已完成” |
| synchronize | host 或 stream 等待边界 | “计时不需要同步” |
Fusion 减少什么,不减少什么
对:
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 依赖造成全局串行。