上一篇 下一篇 分享链接 返回 返回顶部

AI推理低时延:在香港机房用 A10/L40S + TensorRT + Triton 动态批处理窗口,把 p95 从 120ms 打到 48ms 的完整部署与优化指南

发布人:Minchunlin 发布时间:2025-09-26 10:51 阅读量:804

那天是周五凌晨两点,香港荔枝角机房空的Grafana 大屏还在顽固地显示:p95=120ms。白天新增的 A/B 流量压上来之后,延迟抖成了“锯齿”。我站在两台刚上架的服务器前——一台插着 2×L40S,旁边是 4×A10 的推理节点,心里只想把“锯齿”磨平。
接下来 6 个小时,我从 BIOS 到驱动再到 TensorRT 引擎Triton 动态批处理窗口一路抠参数,直到清晨 7 点,延迟曲线终于从锯齿变成了平线,p95 落在 48ms。这篇文章,就是那次上线的完整复盘和“可复制”的手册。

目标与场景

目标:在香港落地低时延推理集群,兼顾 稳定的 p95合理吞吐

典型业务:多模型混部(图像分类/检索 + 轻量文本推断),请求负载不均,流量高峰/低谷交替。

关键技术点

  • A10/L40S 混部适配(不同 GPU 的并发与批大小策略不同)
  • TensorRT FP16/INT8 引擎(形状动态 + 校准)
  • Triton Inference Serverdynamic_batching + max_queue_delay_microseconds(批处理窗口)
  • 客户端微批聚合自适应窗口(把队列等待控制在“刚刚好”)

机房与硬件拓扑(实配)

节点 服务器 CPU / 内存 GPU 存储 / 网络 备注
trt-l40s-01 2U 服务器(双路) 2×Xeon(Ice Lake)/ 512GB 2×L40S(48GB) 2×7.68TB NVMe;2×25GbE 主承载低延迟实时接口
trt-a10-01 2U 服务器(单/双路) 1×或2×Xeon / 256GB 4×A10(24GB) 2×3.84TB NVMe;2×10GbE 承载吞吐与离线/次要在线流

选型心得:L40S 主要抗突发、抗抖动;A10 单卡性价比高,适合并发实例多开增吞吐。两者混部能把成本与时延都拉到更优平衡。

版本矩阵(我当时的组合,供你参考)

组件 版本
OS Ubuntu 22.04 LTS(文末附 CentOS 7 兼容做法)
NVIDIA 驱动 535/550 系列(与下方 CUDA/TensorRT 匹配)
CUDA Toolkit 12.x
TensorRT 8.6+/10.x
Triton Inference Server 24.x(容器)
Docker 24.x + nvidia-container-toolkit
监控 DCGM Exporter + Prometheus + Grafana

BIOS / 内核 / 系统前置优化(“把地基打牢”)

BIOS

  • Above 4G Decoding:开启
  • Resizable BAR:开启(部分平台)
  • NUMA:确认 GPU 与 NIC 在同一 NUMA 域优先插槽
  • C-States:适当限制深度,降低抖动

电源/频率

 
nvidia-smi -pm 1 # Persistence Mode nvidia-smi -pl 300 # 视卡而定,给足但别越保修限制 # 某些卡支持锁频,若支持: # nvidia-smi -lgc <min,max>

网络栈(减少队列积压 + 提升突发接入能力)

cat <<EOF | sudo tee /etc/sysctl.d/99-lowlatency.conf net.core.somaxconn=1024 net.core.netdev_max_backlog=16384 net.ipv4.tcp_tw_reuse=1 net.ipv4.tcp_fin_timeout=15 net.core.rmem_max=134217728 net.core.wmem_max=134217728 net.ipv4.tcp_rmem=4096 87380 134217728 net.ipv4.tcp_wmem=4096 65536 134217728 EOF sudo sysctl --system

IRQ/亲和性

  • 保证 NIC / NVMe 中断与对应 NUMA 亲和
  • Triton 进程绑核,避免与高噪声服务抢同一 L3

驱动/容器:安装与校验

# 1) 驱动(略) nvidia-smi # 2) NVIDIA Container Toolkit distribution=$(. /etc/os-release;echo $ID$VERSION_ID) curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg curl -fsSL https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | \ sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \ sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit sudo nvidia-ctk runtime configure --runtime=docker sudo systemctl restart docker # 3) DCGM Exporter(监控) docker run -d --gpus all --rm --name=dcgm-exporter -p 9400:9400 nvidia/dcgm-exporter:latest

TensorRT 引擎构建(ONNX→TRT,动态形状 + FP16/INT8)

快速路径:trtexec

trtexec --onnx=model.onnx \ --saveEngine=model_fp16.plan \ --explicitBatch --minShapes=input:1x3x224x224 \ --optShapes=input:4x3x224x224 \ --maxShapes=input:8x3x224x224 \ --fp16 --workspace=4096

INT8 校准(Python 骨架)

注:校准集必须能代表线上分布;INT8 下延迟更低、吞吐更高,但需校准谨慎。

# build_int8_engine.py import tensorrt as trt import numpy as np class MyEntropyCalibrator(trt.IInt8EntropyCalibrator2): def __init__(self, batch_iter, input_shape, cache_file): super().__init__() self.batch_iter = batch_iter self.d_input = None self.input_shape = input_shape self.cache_file = cache_file def get_batch_size(self): return self.input_shape[0] def get_batch(self, names): try: batch = next(self.batch_iter) # ndarray [N,C,H,W] except StopIteration: return None if self.d_input is None: import pycuda.driver as cuda self.d_input = cuda.mem_alloc(batch.nbytes) cuda.memcpy_htod(self.d_input, batch) return [int(self.d_input)] def read_calibration_cache(self): try: return open(self.cache_file, "rb").read() except: return None def write_calibration_cache(self, cache): open(self.cache_file, "wb").write(cache) def build_engine(onnx_path, plan_path, min_shape, opt_shape, max_shape, int8=False, cache="calib.cache"): logger = trt.Logger(trt.Logger.WARNING) builder = trt.Builder(logger) network = builder.create_network(1<<int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) parser = trt.OnnxParser(network, logger) with open(onnx_path, "rb") as f: assert parser.parse(f.read()), parser.get_error(0) config = builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 4<<30) # 4GB profile = builder.create_optimization_profile() input_tensor = network.get_input(0) profile.set_shape(input_tensor.name, min=min_shape, opt=opt_shape, max=max_shape) config.add_optimization_profile(profile) config.set_flag(trt.BuilderFlag.FP16) if int8: config.set_flag(trt.BuilderFlag.INT8) calib_iter = (np.random.randn(*opt_shape).astype(np.float32) for _ in range(100)) config.int8_calibrator = MyEntropyCalibrator(calib_iter, opt_shape, cache) engine = builder.build_engine(network, config) with open(plan_path, "wb") as f: f.write(engine.serialize()) # 用法: # build_engine("model.onnx","model_int8.plan",(1,3,224,224),(4,3,224,224),(8,3,224,224),int8=True)

Triton 模型仓库与动态批处理(把“窗口”掐准)

目录结构

models/ resnet_trt/ 1/model.plan config.pbtxt

config.pbtxt 示例(FP16/INT8,动态批处理窗口)

name: "resnet_trt" platform: "tensorrt_plan" max_batch_size: 8 input [ { name: "input" data_type: TYPE_FP16 dims: [3,224,224] } ] output [ { name: "logits" data_type: TYPE_FP16 dims: [1000] } ] instance_group [ { kind: KIND_GPU, gpus: [0], count: 2 } # L40S:更大批次可更少实例;A10:更小批次多实例 ] dynamic_batching { preferred_batch_size: [ 1, 2, 4, 8 ] max_queue_delay_microseconds: 800 # **批处理窗口**(核心旋钮) } response_cache { enable: true } # 适合重复查询的场景

经验

  • L40S:可把 preferred_batch_size 值做大一点(如 2/4/8/16);
  • A10:更偏向 1/2/4;
  • max_queue_delay_microseconds 是“窗口”,起点 500–1000μs;后续让监控驱动自适应。

Triton 容器启动(Docker Compose)

# docker-compose.yml services: triton: image: nvcr.io/nvidia/tritonserver:24.06-py3 restart: always shm_size: 1g deploy: resources: reservations: devices: - capabilities: [gpu] command: > tritonserver --model-repository=/models --exit-on-error=false --http-port=8000 --grpc-port=8001 --metrics-port=8002 --rate-limit=execution_count volumes: - /data/models:/models ports: - "8000:8000" - "8001:8001" - "8002:8002"

客户端微批聚合:把“小水滴”凑成“刚好一杯”

服务器端 max_queue_delay_microseconds 只是上限,客户端侧再做 ≤1ms 的微批窗口,能稳定把 GPU 吞吐榨干而不拉高抖动。

# client_microbatch.py import time, threading, queue import tritonclient.grpc as grpcclient import numpy as np Q = queue.Queue() client = grpcclient.InferenceServerClient("trt-l40s-01:8001") WINDOW_US = 600 # 初始窗口,后续自适应 MAX_BATCH = 8 def worker(): global WINDOW_US while True: start = time.time() batch = [] while (time.time() - start)*1e6 < WINDOW_US and len(batch) < MAX_BATCH: try: batch.append(Q.get(timeout=WINDOW_US/1e6)) except queue.Empty: pass if not batch: continue inp = np.stack([x for x in batch], axis=0).astype(np.float16) inputs = [grpcclient.InferInput("input", inp.shape, "FP16")] inputs[0].set_data_from_numpy(inp, binary_data=True) res = client.infer(model_name="resnet_trt", inputs=inputs) # 记录延迟到本地指标,供自适应模块读取 latency_ms = (time.time() - start)*1000 record_latency(latency_ms, len(batch)) def record_latency(lat, bsz): # 自行写入 Prometheus Pushgateway 或本地滑窗 pass threading.Thread(target=worker, daemon=True).start() def submit(req): Q.put(req) # 生产者线程塞请求

自适应批处理窗口(让窗口“自己会变大/变小”)

核心思路:以 p95 目标 为约束(如 50ms),如果稳定低于目标,就 每 10sWINDOW_US+50;如果超过,就 -50;在 [200, 1500]us 内夹紧。

TARGET_P95_MS = 50 WINDOW_US = 600 def tune_loop(): global WINDOW_US while True: p95 = get_recent_p95() # 从 Prometheus 或本地滑窗取 if p95 < TARGET_P95_MS * 0.9 and WINDOW_US < 1500: WINDOW_US += 50 elif p95 > TARGET_P95_MS and WINDOW_US > 200: WINDOW_US -= 50 time.sleep(10)

实测数据(节选)

测于香港机房,开 FP16;INT8 在此模型上另有 10–20% 的延迟优化(准确性通过校准保障)。

L40S 节点(单卡,实例数=2)

批大小 p50 (ms) p95 (ms) QPS(稳态)
1 14 29 650
2 16 31 980
4 19 36 1520
8 24 44 1850

A10 节点(单卡,实例数=3)

批大小 p50 (ms) p95 (ms) QPS(稳态)
1 18 36 420
2 21 41 700
4 27 55 980

组合上线后,整体 p95 从 120ms → 48ms,QPS 提升 ~1.8×。关键在于:客户端微批 + Triton 动态批处理窗口 + 按 GPU 定制的实例数/批大小

监控与告警要点(最有用的那几个)

  • DCGMsm_util, mem_copy_util, pstate, power_usage
  • Tritonnv_inference_request_duration_us, nv_inference_queue_duration_us, nv_inference_exec_count
  • 自研:客户端 发送→返回 全链路延迟,分 p50/p95,并记录 batch 实际大小分布(看窗口是否有效)
  • 告警:p95 连续 5 分钟高于阈值 + queue_duration 上升,自动把窗口收紧 100–200μs

线上“坑”与解决(都是半夜踩的)

GPU 负载不满但延迟高

排查发现 NIC 与 GPU 不在同一 NUMA,跨 NUMA 引发额外拷贝;调整插槽 + 进程绑核,p95 立刻下降 ~8ms。

INT8 推精度掉点

校准集太干净;换真实线上样本 + 叠加噪声增强,重新校准后精度回到期望,延迟仍优于 FP16。

A10 上批太大抖动

A10 多开实例、批保持在 1/2/4,吞吐反而更好;L40S 则倾向更大批。

GPU 频率时不时降档

机房某时段温度波动,pstate 频繁切换;加风道塑形 + 合理 -pl,锁定负载区间。

Triton 冷启动慢

首次加载引擎占用时间;上线前做 预热(warmup),并开启 response_cache 对热门查询生效。

网络路径 MTU 不一致

香港—内地线路部分链路 MTU 较小,观察到分片/重传;在出口侧对 PMTU 做探测与限 MTU,抖动立降。

目录结构
全文