全部术语

GLOSSARY ENTRY

推理 Benchmark、容量模型与 Profiler

  • Benchmark Methodology
  • Memory Capacity Model
  • Profiler
  • NVTX
  • Goodput

在先验证输出语义一致的前提下,明确 workload、计时边界、同步、warmup、统计量与内存口径,再用 profiler 解释而非替代测量,并以 SLO goodput 连接微基准和服务结果。

先定义问题,数字才有含义

推理 benchmark 不是一个 time.time() 差值。至少要先固定:

  • 语义:A/B 输出应相等到明确容差;采样路径则比较分布或固定随机流。
  • workload:模型/权重、prompt/output 长度分布、batch/concurrency、dtype、sampling、cache 命中率。
  • 测量边界:单 kernel、一个 decode step、模型 forward、TTFT/TPOT,还是包含 queue/tokenization/network 的端到端请求。
  • 环境:GPU、driver、CUDA/PyTorch/runtime 版本、clock/power、并行度与 backend 配置。
  • 统计:warmup、样本数、median/p95/p99、异常值与是否包含 cold start。

只报告平均 tokens/s 会掩盖 queue 和尾延迟;只报告 p99 又不能说明容量。服务层至少同时观察 TTFT、TPOT、throughput 与满足目标的 goodput。

三种计时边界

对同一 X:[256,1024] @ W:[1024,1024] FP16 GEMM,本机示例得到:

方法本次中位数包含什么
CPU submit、无同步5.971 µsPython/ATen dispatch 与 launch 提交;GPU 可能尚未完成
perf_counter + 前后 cuda.synchronize()22.786 µshost 侧等待、launch 和 GPU 完成的 wall-clock 边界
同 stream CUDA Events20.480 µs两个 event 间的 device timeline elapsed;不含 event 前的 host queue time

这些数值只属于当前 shape、RTX 3080 和软件版本。Event 并非“永远最真实”:多 streams、跨设备、排队、CPU preprocessing、network 与 scheduler 延迟要用相应 trace/端到端边界。每轮强制同步也会破坏生产中的 overlap,所以微基准和真实服务 trace 应同时存在。

Warmup 与统计

Warmup 用来排除 lazy module loading、allocator 初始化、autotuning、JIT/compile、cache coldness 和 graph capture 等一次性成本。若 cold start 本身是目标,就应单独报告 first request,而不是与 steady-state 混成一个平均数。

Median 对偶发 OS 抖动较稳健;p95/p99 对 SLO 更相关,但需要足够样本。并发服务还应保存 arrival trace 和 completed/cancelled/failed 分类,避免只对幸存请求统计。

正确性优先的可运行计时与容量实验

实验问题:未同步提交、同步 wall-clock 与 CUDA Event 有何差异?PyTorch allocatedreservedpeak 是否会在临时 tensor 删除后表现不同?给定权重/运行时预算,逻辑 KV token capacity 如何计算?

import platform,statistics,time
import torch

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

def median_us(samples): return statistics.median(samples)

def benchmark_cuda(fn,warmup=10,rounds=30):
    for _ in range(warmup): fn()
    torch.cuda.synchronize(); submit=[]; synchronized=[]; events=[]
    for _ in range(rounds):
        start=time.perf_counter(); fn()
        submit.append((time.perf_counter()-start)*1e6)
        torch.cuda.synchronize(); start=time.perf_counter(); fn(); torch.cuda.synchronize()
        synchronized.append((time.perf_counter()-start)*1e6)
        begin=torch.cuda.Event(enable_timing=True); end=torch.cuda.Event(enable_timing=True)
        begin.record(); fn(); end.record(); end.synchronize()
        events.append(begin.elapsed_time(end)*1e3)
    return median_us(submit),median_us(synchronized),median_us(events)

def benchmark_cpu(fn,warmup=5,rounds=20):
    for _ in range(warmup): fn()
    samples=[]
    for _ in range(rounds):
        start=time.perf_counter(); fn(); samples.append((time.perf_counter()-start)*1e6)
    value=median_us(samples); return value,value,value

def capacity_model(total_gib,weights_gib,runtime_gib,layers,kv_heads,head_dim,element_bytes,utilization):
    usable=int(total_gib*utilization*2**30)
    fixed=int((weights_gib+runtime_gib)*2**30)
    kv_per_token=2*layers*kv_heads*head_dim*element_bytes
    return usable,fixed,kv_per_token,max(0,(usable-fixed)//kv_per_token)

def main():
    dtype=torch.float16 if DEVICE.type=="cuda" else torch.float32
    m,k,n=(256,1024,1024) if DEVICE.type=="cuda" else (64,256,256)
    x=torch.randn(m,k,device=DEVICE,dtype=dtype)
    weight=torch.randn(k,n,device=DEVICE,dtype=dtype)
    output=x@weight; reference=x.float()@weight.float()
    tol=(2e-3,2e-2) if dtype==torch.float16 else (1e-5,1e-6)
    torch.testing.assert_close(output.float(),reference,rtol=tol[0],atol=tol[1])
    fn=lambda:x@weight

    if DEVICE.type=="cuda":
        submit_us,sync_us,event_us=benchmark_cuda(fn)
        torch.cuda.synchronize()
        base_a=torch.cuda.memory_allocated(); base_r=torch.cuda.memory_reserved()
        torch.cuda.reset_peak_memory_stats()
        temporary=torch.empty((2048,2048),device=DEVICE,dtype=torch.float32)
        temporary.fill_(1); fn(); torch.cuda.synchronize()
        peak_a=torch.cuda.max_memory_allocated(); peak_r=torch.cuda.max_memory_reserved()
        del temporary; torch.cuda.synchronize()
        after_a=torch.cuda.memory_allocated(); after_r=torch.cuda.memory_reserved()
    else:
        submit_us,sync_us,event_us=benchmark_cpu(fn)
        base_a=base_r=peak_a=peak_r=after_a=after_r=0

    usable,fixed,kv_per_token,token_capacity=capacity_model(
        20,14,1.5,32,8,128,2,.90)
    assert kv_per_token==131072 and token_capacity==20480
    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={dtype} X={tuple(x.shape)} W={tuple(weight.shape)}")
    print(f"correctness_max_abs_diff={(output.float()-reference).abs().max():.3e}")
    print(f"timing_median_us submit_without_sync={submit_us:.3f} synchronized_wall={sync_us:.3f} cuda_event={event_us:.3f}")
    if DEVICE.type=="cuda":
        print("memory_MiB "
              f"baseline_allocated={base_a/2**20:.3f} baseline_reserved={base_r/2**20:.3f} "
              f"peak_allocated={peak_a/2**20:.3f} peak_reserved={peak_r/2**20:.3f} "
              f"after_allocated={after_a/2**20:.3f} after_reserved={after_r/2**20:.3f}")
    print(f"capacity usable_GiB={usable/2**30:.3f} fixed_GiB={fixed/2**30:.3f} "
          f"KV_bytes_per_token={kv_per_token} token_capacity={token_capacity}")

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

完整文件位于 examples/benchmark_capacity.py。2026-08-13 实测 output/reference 最大差 6.058e-02,在 FP16 容差内通过。临时 16 MiB tensor 使 allocated 从 12.125 升至 peak 28.625 MiB;删除后 allocated 回到 12.125 MiB,reserved 仍为 44 MiB,展示 caching allocator 保留可复用 segment,而不是等同泄漏。

脚本没有比较两个算法的快慢;它只固定计时与内存口径。若做 A/B,必须让两边使用同输入/shape/dtype/device、先 assert_close、各自 warmup,并避免第一条路径为第二条路径预热不对称资源。

容量模型:理论 payload 不是可服务上限

单 rank 可用于动态状态的预算可写成:

Mdynamic=uMdeviceMweightsMruntimeMworkspaceMsafetyM_{dynamic}=uM_{device}-M_{weights}-M_{runtime}-M_{workspace}-M_{safety}

uu 是允许使用的显存比例。标准 KV 每 token bytes 为:

mtoken=2LHkvDsm_{token}=2LH_{kv}Ds

所以跨所有 active sequences 的逻辑 token 上限:

Tcapacity=MdynamicmtokenT_{capacity}=\left\lfloor\frac{M_{dynamic}}{m_{token}}\right\rfloor

代入 20 GiB device、u=0.9u=0.9、14 GiB weights、1.5 GiB 合并 runtime/safety、L=32,Hkv=8,D=128,s=2L=32,H_{kv}=8,D=128,s=2:可用 18 GiB,固定 15.5 GiB,KV 每 token 131,072 bytes(128 KiB),得到 20,480 active cached tokens。这不是“可同时服务 20,480 个请求”;10 条各 2,048 tokens 已用尽该逻辑预算,且尚未显式计入 block fragmentation、prefill activations、CUDA Graph pools、NCCL buffers 与峰值 workspace。

PagedAttention减少外部碎片并按 blocks 管理容量,但最后一个 partial block、prefix sharing/refcount 与 scheduler reservation 仍改变可 admission tokens。KV 量化改变 payload/metadata,MLA改变缓存字段,容量公式要跟实际 layout 一致。

allocated、reserved、peak、理论容量

  • theoretical payload:由 shape × dtype 算出的活 tensor 数据。
  • allocated:PyTorch allocator 当前交给 live tensors 的 bytes。
  • reserved:allocator 从 CUDA 保留的 segments,包含 allocated 与可复用空闲区。
  • peak allocated/reserved:reset 后窗口内高水位;测量窗口必须覆盖目标路径。
  • device free/used:还包含其他进程、CUDA context、非 PyTorch allocations。

只看 nvidia-smi 不能区分这些口径,只看 allocated 又可能忽略 graph/NCCL/custom allocator。容量测试应实际跑 admission/OOM 边界,并用理论模型解释差值。

Profiler、NVTX 与 Goodput

Profiler 用来把时间归因到 operator、kernel、memcpy、collective 和 idle gap;它不是 benchmark 替代品,因为 tracing 自身有开销。Backend Dispatch页面展示了如何用 profiler 证明实际 SDPA/GEMM 路径。

GPU Runtime用 stream/event 建立跨队列 happens-before,并实测 torch.compile pointwise fusion 的 kernel count;这些 trace 证据仍需本页的同步、warmup 和端到端边界才能形成性能结论。

NVTX range 应围绕稳定语义区间,例如 request/queueprefilldecode_stepa2a_dispatch,并携带 request/batch/shape 标识;不要给每个细小 Python helper 加 range 造成 trace 噪声。多 stream 时还要查看依赖与 overlap,不能把横向长度直接机械相加。

若 SLO 定义为 TTFT≤xTPOT≤y,请求 goodput 可以写成:

G=#{r:completed(r)TTFTrxTPOTry}TwallG=\frac{\#\{r:\operatorname{completed}(r)\land TTFT_r\le x\land TPOT_r\le y\}}{T_{wall}}

必须说明 cancelled/failed 是否进入分母、输出长度分布与测量窗口。吞吐提高但大量请求违反 SLO 时,goodput 可以下降。Continuous Batching的 scheduler benchmark 应在同一 arrival trace 上同时报告 queue、TTFT、TPOT、throughput 与 goodput。

服务可靠性、Backpressure 与 Observability继续定义在线证据:低基数 metrics 保存总体分布,request traces 连接 gateway/scheduler/worker 因果链,结构化 logs 记录离散状态跃迁;只对成功请求统计延迟会产生 survivor bias。

参考资料