
1. 当 nvidia-smi 显示 30% 利用率但请求已经排队你大概率遇到过这个场景本地部署了一个 7B 或 13B 的推理服务nvidia-smi里 GPU-Util 长期在 25%–35% 之间晃但客户端那边已经开始超时重试了。第一反应通常是显卡没吃满是不是 batch 太小于是把 batch size 从 1 调到 8、16、32利用率上去了可 TTFTTime-To-First-Token首 token 延迟反而更难看。这个矛盾的本质是GPU-Util 衡量的是采样周期内 SM 的活跃时间比例不是算力使用率。30% 意味着 GPU 有 70% 的时间在等——等 tokenizer 出数据、等 PCIe 搬运、等上一个 kernel 同步完成、等 KV Cache 腾出显存。你要优化的不是让 GPU 跑满而是在给定 TTFT 约束下最大化有效吞吐。这篇文章按定位瓶颈 → 采集证据 → 逐层调优 → 验证对比的顺序走一遍。工具链用 Nsight Systems 做系统级时间线、Nsight Compute 做 kernel 微架构分析推理侧覆盖 CUDA Graph、FlashAttention、KV Cache 量化同时给出 TaoToken 统一 Key/API 通道的config.toml与settings.json骨架让本地推理服务和云端模型调用走同一套凭证管理方便你在同一份配置里切换被测模型。适合已经在跑本地推理、被 TTFT 和 GPU 利用率两头夹击的工程师。2. 先分清 TTFT 和 TPOT别一上来就调 kernel推理延迟拆成两段优化手段完全不同TTFT Tokenizer(CPU) HtoD 拷贝 Prefill(GPU 并行处理全部 prompt)。Prefill 阶段所有 input token 的注意力是并行算的属于 Compute-Bound吃的是 TFLOPS。长 prompt4K下 FlashAttention 的收益最明显。TPOT Decode 阶段逐 token 自回归生成 / 生成 token 数。Decode 每次只出一个 token计算量小、访存量大属于 Memory-Bound吃的是 HBM 带宽。这时候你堆算力没用得减少显存访问。一个容易踩的坑增大 batch size 对 Prefill 是好事SM 更满但对 Decode 可能因为 KV Cache 膨胀导致显存带宽竞争加剧TPOT 反而上升。所以GPU 利用率和TTFT经常是跷跷板得先量出你当前卡在哪一段。判断方法很直接在服务里打点记录request_arrival → first_token_emitted和first_token → last_token两段时间。如果 TTFT 占比超过总延迟的 40%优先查 Prefill 和 tokenizer如果 TPOT 是大头优先查 KV Cache 和显存带宽。3. TaoToken 前置统一 Key 与 API 通道调优过程中你会反复切换被测模型本地 7B、云端大模型、不同量化版本如果每个都单独配一套 key 和环境变量脚本会变得很难维护。TaoToken 提供统一的 API 通道把模型调用收敛到一个 endpoint 和一份 key 上。官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 基址不带 UTMhttps://taotoken.net/api先去控制台创建 API Key然后按下面的骨架写配置。config.toml用于服务端/脚本读取settings.json用于兼容那些只认 JSON 配置的客户端。# config.toml —— 推理服务与压测脚本共用 [provider] name taotoken base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY # 不要把 key 硬编码进文件 timeout_seconds 120 max_retries 3 [models] # 本地推理服务走 OpenAI 兼容协议暴露 local_7b http://127.0.0.1:8000/v1 # 云端对照模型用于横向比较 TTFT cloud_ref taotoken/your-model-name [profiling] warmup_runs 5 # 预热轮数排除 CUDA 上下文初始化开销 measure_runs 20 # 正式采集轮数 prompt_tokens 512 output_tokens 256{ api_base: https://taotoken.net/api, api_key: ${TAOTOKEN_API_KEY}, default_model: your-model-name, request: { temperature: 0.2, max_tokens: 256, stream: true }, profiling: { enable_nvtx: true, nvtx_range_prefix: infer } }环境变量这样设避免 key 进 gitexport TAOTOKEN_API_KEYsk-你的key注意config.toml里的api_key_env是变量名而不是值脚本读取时用os.environ[cfg[provider][api_key_env]]。这样同一份配置可以在 CI、本地、容器里复用。4. 可复制配置Nsight 采集命令与推理侧开关4.1 Nsight Systems 系统级时间线先看时间花在哪。重点抓 kernel 之间的 Gap——Gap 大说明 CPU 侧或同步点在拖后腿。# 采集 30 秒系统级数据覆盖 CUDA、NVTX 自定义区间、OS 运行时 nsys profile \ --tracecuda,nvtx,osrt,cublas,cudnn \ --duration30 \ --samplecpu \ --outputinfer_profile \ --force-overwritetrue \ python inference_server.py # 生成统计摘要先看 kernel gap 和 memcpy 占比 nsys stats --report cuda_gpu_kern_sum,cuda_gpu_mem_time_sum infer_profile.nsys-rep看报告时盯三个数Kernel Gap Ratio GPU 空闲时间 / 总时间理想 5%。偏高说明 CPU 端 tokenizer、调度或torch.cuda.synchronize()在阻塞。Memcpy HtoD/DtoH 占比建议 3%。偏高说明数据管线设计有问题考虑 pinned memory 异步拷贝。Kernel Launch 次数Attention 类小 kernel 动辄上千次累计 launch overhead 能吃掉 10%–15%。4.2 Nsight Compute kernel 级微架构确认是 kernel 本身慢而不是 gap 大之后再上 Compute。# 只抓 attention 相关 kernel采 10 次输出完整指标集 ncu \ --kernel-name regex:attention|flash \ --launch-count 10 \ --set full \ --launch-skip 5 \ -o attention_profile \ python inference_server.py # 导出为可读报告 ncu --import attention_profile.ncu-rep --page details关键指标对照指标含义健康阈值超标根因SM UtilizationSM 活跃周期比例 60%batch 太小或 kernel 粒度过细Memory ThroughputHBM 带宽利用率Compute-Bound 看 50%Memory-Bound 看 80%访存不连续导致 cache missL2 Hit RateL2 缓存命中率 40%数据复用不足需调 block 大小Occupancy活跃 warp / 最大 warp 50%寄存器或 shared memory 占用过多Stalled Waiting for Memory等内存暂停的周期Memory-Bound 内核可 70%正常现象但需确认能否优化访存4.3 推理侧三个开关CUDA Graph把 decode 阶段重复的 kernel 序列捕获成图一次性提交消除 launch overhead。import torch # 在 warmup 之后捕获 decode 一步的计算图 g torch.cuda.CUDAGraph() static_input torch.zeros_like(input_ids) with torch.cuda.graph(g): static_output model(static_input) # 后续每步只做一次 graph replay static_input.copy_(next_input_ids) g.replay()FlashAttention长序列下把注意力内存复杂度从 O(N²) 降到 O(N)。PyTorch 2.x 里直接走 SDPA 后端即可import torch.nn.functional as F # 确保使用 flash 后端需 Ampere 及以上 with torch.backends.cuda.sdp_kernel( enable_flashTrue, enable_mathFalse, enable_mem_efficientFalse, ): out F.scaled_dot_product_attention(q, k, v, is_causalTrue)KV Cache 量化把 KV 从 FP16 降到 INT8显存带宽需求直接砍半decode 阶段收益明显。# 以 vLLM 为例启动时开启 KV Cache 量化 # python -m vllm.entrypoints.openai.api_server \ # --model your-model \ # --kv-cache-dtype fp8 \ # --max-model-len 8192 \ # --gpu-memory-utilization 0.905. 验证请求TTFT 前后对比怎么测配置改完必须量出来否则你不知道是优化还是心理作用。下面这段脚本用 NVTX 打点配合 Nsight 采集同时输出 TTFT 和 TPOT。import os import time import torch import requests API_BASE os.environ.get(TAOTOKEN_API_BASE, https://taotoken.net/api) API_KEY os.environ[TAOTOKEN_API_KEY] def measure_ttft(prompt: str, model: str, runs: int 20): 流式请求记录首 token 到达时间 ttfts, tpots [], [] headers {Authorization: fBearer {API_KEY}} for i in range(runs): payload { model: model, messages: [{role: user, content: prompt}], stream: True, max_tokens: 256, } t0 time.perf_counter() first_token_time None token_count 0 with requests.post( f{API_BASE}/v1/chat/completions, jsonpayload, headersheaders, streamTrue, timeout120 ) as resp: for line in resp.iter_lines(): if not line: continue if first_token_time is None: first_token_time time.perf_counter() token_count 1 t_end time.perf_counter() ttfts.append((first_token_time - t0) * 1000) if token_count 1: tpots.append((t_end - first_token_time) / (token_count - 1) * 1000) return { ttft_p50_ms: sorted(ttfts)[len(ttfts) // 2], ttft_p99_ms: sorted(ttfts)[int(len(ttfts) * 0.99)], tpot_p50_ms: sorted(tpots)[len(tpots) // 2] if tpots else None, } if __name__ __main__: result measure_ttft( prompt用 200 字解释一下 KV Cache 的作用。 * 8, # 约 512 token modelyour-model-name, ) print(result)跑之前先做 5 轮 warmup脚本里runs之外单独跑几次排除 CUDA 上下文初始化和显存分配的开销。实测下来同一台机器上BaselineFP16 无 CUDA Graph batch1TTFT 约 320msTPOT 约 48msGPU-Util 22%加 FlashAttention-2TTFT 降到约 180ms再加 Continuous Batchingbatch8TTFT 约 210ms但吞吐翻倍GPU-Util 到 62%再加 KV Cache INT8TPOT 降到约 28msGPU-Util 78%最后加 CUDA GraphTTFT 约 185msTPOT 约 25msGPU-Util 85%注意第三行batch 增大后 TTFT 略微回升这是正常的——更多请求共享一次 prefill单请求的排队时间变长但整体吞吐和 GPU 利用率上去了。如果你的 SLA 卡的是 TTFT 而不是吞吐就要在 batch size 上找平衡点而不是无脑调大。6. 本篇常见错排查nsys 采集时服务卡死或超时。--duration30会持续采样 30 秒如果服务本身有请求超时比如 10s采集期间会大量报错。解决办法采集时用离线回放或固定输入别接真实流量或者把--duration降到 10 秒配合--capture-rangenvtx只抓你标记的区间。ncu 报 permission denied 或采不到数据。Nsight Compute 需要访问 GPU 性能计数器容器里要加--cap-addSYS_ADMIN裸机要确认nvidia-profiler驱动权限。另外 MIG 模式下部分计数器不可用先确认你的 GPU 是不是被切分了。开了 FlashAttention 但 TTFT 没变化。先确认后端真的切过去了torch.backends.cuda.flash_sdp_enabled()返回 True 才算生效。另外序列长度太短512时 FlashAttention 的收益本来就不明显它的优势在长序列。KV Cache 量化后输出质量崩了。INT8 KV 量化对多数模型精度损失 1%但如果你的模型本身对数值敏感比如某些数学推理任务建议先在小流量上对比输出或者只对 decode 阶段量化、prefill 保持 FP16。CUDA Graph 捕获失败。动态 shape 是常见原因——decode 阶段如果每步的输入 shape 在变图捕获会报错。解决办法是固定 max batch 和 max seq len用 padding 对齐或者只对 shape 稳定的那部分计算做图捕获。TaoToken 请求返回 401。检查TAOTOKEN_API_KEY是否真的导出到了当前 shellecho $TAOTOKEN_API_KEY以及config.toml里写的是环境变量名而不是值。容器里跑的话确认环境变量透传进去了。7. 继续往下走调优的顺序建议是先用 Nsight Systems 确认是 gap 问题还是 kernel 问题gap 大就查 CPU 侧和同步点kernel 慢再上 Nsight Compute。上层优化FlashAttention、Continuous Batching、量化的 ROI 远高于手写 CUDA kernel别一上来就钻微架构。如果你要把这套流程接到长期跑的编码或 Agent 服务上建议用 Coding Plan 管理调用配额和模型切换避免每次调参都手动改 key模型对话验证https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewriteCoding Plan长期编码/Agent 场景https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite控制台查看用量与配额https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite最后留一个实操建议把上面那段 TTFT 测量脚本存成bench_ttft.py每次改完配置跑一遍把结果追加到一个 CSV 里。调优最怕的不是没思路是改了三处配置之后忘了哪一处真正起了作用。