Triton模型服务化实战:生产级推理稳定性与延迟优化指南
1. 项目概述当模型走出Jupyter真正开始呼吸真实世界空气“From Notebook to Production: Running ML in the Real World (Part 4)”——这个标题里藏着一个被无数数据科学家反复咀嚼、又悄悄咽下的苦涩真相我们花了80%的时间调参、画图、在Jupyter里把准确率从92.3%刷到92.7%却只留了20%的精力甚至更少去思考——这串漂亮的数字明天早上八点能不能在客户下单的瞬间稳稳接住那条带着乱码地址和模糊图片的请求Part 4不是技术演进的终点而是实战压力测试的起点。它不讲怎么用PyTorch写Transformer而是直面那个没人愿意在周会上明说的问题你昨天在本地跑通的pipeline今天推到服务器上为什么连requirements.txt都装不完为什么模型加载要17秒为什么并发一上来内存就飙到98%然后整个服务静默重启我做过三轮完整的MLOps落地从电商推荐到工业质检最深的体会是模型上线那一刻不是项目的完成而是运维噩梦的倒计时。这篇内容专为那些已经能把模型训出来、能写API、甚至能搭个简单Dashboard的人准备——它不教你怎么成为算法大神但能让你在凌晨三点收到告警邮件时不用翻三遍Stack Overflow再手抖删掉半行Dockerfile。核心关键词——模型服务化、生产环境稳定性、推理延迟优化、资源隔离、可观测性落地——每一个词背后都是我亲手填过的坑、重装过七次的GPU驱动、以及和运维同事在会议室里争论了两小时才敲定的内存限制值。如果你正卡在“模型能跑”和“模型敢上”的中间地带这篇就是为你写的实操手册。2. 内容整体设计与思路拆解为什么放弃FlaskGunicorn转向TritonPrometheus2.1 从“能用”到“敢用”的分水岭在哪里很多团队卡在Part 4根本原因在于对“生产环境”的认知偏差。他们以为把app.py扔进Docker、用Nginx反向代理、加个Health Check就叫上线了。错。真实世界的生产环境有四个刚性约束缺一不可低延迟P95 200ms、高吞吐≥50 QPS/实例、零停机更新滚动发布、故障可归因日志指标链路全打通。而传统FlaskGunicorn方案在这四点上全线失守。我拿一个实际案例说明某金融风控模型本地推理耗时120ms用Flask部署后P95飙升至850ms。排查发现Gunicorn的worker进程模型导致每次请求都要重新加载模型权重即使用了preloadPython的GIL和内存拷贝仍无法避免而模型本身有1.2GB光加载就占了600ms。更致命的是当突发流量到来Gunicorn会fork新worker每个worker都独占一份1.2GB内存4核机器瞬间OOM。这不是代码问题是架构选择问题。2.2 Triton Inference Server为什么它是当前最务实的解法NVIDIA Triton不是“另一个推理框架”它是专门为解决上述痛点设计的推理操作系统。它的核心设计哲学是模型即服务而非代码即服务。这意味着什么第一模型加载与请求处理彻底解耦——Triton启动时一次性加载所有模型到GPU显存后续请求直接调用已驻留的模型实例加载耗时归零第二原生支持多模型、多版本、多框架PyTorch/TensorFlow/ONNX一个Triton实例可同时托管12个不同版本的风控模型按请求头中的model_version自动路由灰度发布变得像改个配置文件一样简单第三内置动态批处理Dynamic Batching把10个独立请求合并成一个batch送入GPU显存利用率从35%拉到82%QPS翻倍。我们实测过同一台T4服务器Flask方案峰值QPS 32Triton开启动态批处理后稳定在89且P95压到142ms。这不是理论值是压测平台连续跑48小时的真实数据。2.3 为什么监控必须是PrometheusGrafana而不是ELK很多人一提监控就想到ELKElasticsearchLogstashKibana因为它能搜日志。但在ML生产环境日志只是冰山一角。你需要知道此刻GPU显存用了多少模型A的平均延迟是否比模型B高17%过去一小时有多少请求因输入尺寸超限被拒绝这些是指标Metrics不是日志Logs。Prometheus的时序数据库专为此而生——它每15秒主动抓取Triton暴露的/metrics端点包含nv_inference_request_success、nv_inference_queue_duration_us等200个原生指标Grafana用这些数据画出实时热力图。举个例子当某天下午3点nv_inference_compute_duration_us的P99突然从80ms跳到320ms我们立刻切到对应GPU的gpu_utilization指标发现它卡在99%不动再查gpu_memory_used_bytes确认是显存泄漏。整个过程5分钟定位而用ELK翻日志至少半小时。这就是指标监控不可替代的价值——它回答“发生了什么”日志只回答“当时打印了什么”。2.4 架构选型背后的成本权衡为什么不用SageMaker或Vertex AI云厂商的全托管服务如AWS SageMaker、GCP Vertex AI确实省心但它们在Part 4阶段反而成了枷锁。第一冷启动延迟不可控——SageMaker Endpoint在无流量时自动缩容下次请求触发冷启动平均耗时2.3秒远超业务容忍的200ms第二调试黑盒化——当模型返回NaN结果你无法登录实例查看CUDA上下文、检查tensor shape是否错位只能靠猜第三成本失控——按小时计费的实例哪怕每秒只有1个请求也得为整台p3.2xlarge付费。我们算过账自建Triton集群3台T4服务器年成本约$14,000而同等算力的SageMaker托管服务年支出$42,000且性能还差30%。Part 4的本质是可控性优先于便利性当你需要在凌晨三点精准kill掉某个泄漏的Python worker进程时你会感激自己坚持了自建路线。3. 核心细节解析与实操要点Triton部署的七个生死线3.1 模型格式转换ONNX不是万能解药但它是唯一通用语言Triton原生支持PyTorch、TensorFlow但强烈建议统一转为ONNX。为什么因为ONNX剥离了框架运行时依赖模型文件更小、加载更快、跨平台兼容性更好。但转换过程充满陷阱。以PyTorch为例常见错误是torch.jit.trace时输入tensor的shape被固化。比如你的模型实际接收[1, 3, 224, 224]但trace时用了[1, 3, 224, 224]Triton会认为输入必须是这个shape一旦线上来个[8, 3, 224, 224]的batch直接报错。正确做法是用torch.jit.script支持动态shape或ONNX的dynamic_axes参数# 错误trace固化shape dummy_input torch.randn(1, 3, 224, 224) traced_model torch.jit.trace(model, dummy_input) # ❌ # 正确script dynamic_axes dummy_input torch.randn(1, 3, 224, 224) scripted_model torch.jit.script(model) # ✅ 支持动态batch torch.onnx.export( scripted_model, dummy_input, model.onnx, input_names[input], output_names[output], dynamic_axes{ input: {0: batch_size}, # 声明batch维度可变 output: {0: batch_size} } )提示转换后务必用onnxruntime本地验证——ort_session ort.InferenceSession(model.onnx)传入不同batch size的输入确保输出shape正确。我曾因忽略这步在生产环境上线后发现所有batch1的请求都返回空结果回滚花了47分钟。3.2 Triton配置文件config.pbtxt80%的线上问题源于此Triton的行为几乎全部由config.pbtxt控制这是最常被轻视、却最致命的文件。一个典型配置如下name: fraud_model platform: onnxruntime_onnx max_batch_size: 32 input [ { name: input_ids data_type: TYPE_INT64 dims: [ -1 ] } ] output [ { name: logits data_type: TYPE_FP32 dims: [ -1, 2 ] } ] instance_group [ { count: 2 kind: KIND_GPU gpus: [0] } ] dynamic_batching { max_queue_delay_microseconds: 100000 }关键点解析max_batch_size: 32不是指单次请求最大batch size而是Triton内部动态批处理能接受的最大合并batch size。设太小如8则大量小请求无法合并吞吐上不去设太大如128则小请求等待时间过长P95飙升。我们的经验是从min(16, 2×平均线上batch_size)起步压测后调整。instance_groupcount: 2表示在GPU 0上启动2个模型实例。别盲目设高——每个实例独占显存T4显存16GB一个1.2GB模型最多开13个实例但CPU和PCIe带宽会先瓶颈。我们实测T4上count: 3时QPS最高再增加反而下降。dynamic_batchingmax_queue_delay_microseconds: 100000100ms是黄金值。设太短如10ms批处理失败率高设太长如500ms用户感知延迟明显。这个值必须和业务SLA对齐——如果业务要求P95200ms则队列等待必须压在50ms内。注意修改config.pbtxt后必须重启Triton服务才能生效Triton不会热加载配置。很多团队踩坑于此改完配置以为生效了其实还在用旧配置跑。3.3 资源隔离cgroups v2 NVIDIA Container Toolkit的硬核组合Triton跑在容器里但默认Docker设置下GPU资源是共享的。当多个模型实例同时计算一个模型的CUDA kernel可能抢占另一个的显存带宽导致延迟毛刺。解决方案是启用NVIDIA Container Toolkit的--gpus细粒度控制并配合Linux cgroups v2做CPU/内存硬隔离。第一步安装NVIDIA Container Toolkit并验证# 安装后必须重启docker sudo systemctl restart docker # 验证nvidia-smi能否在容器内运行 docker run --rm --gpus all nvidia/cuda:11.0-base-ubuntu20.04 nvidia-smi第二步启动Triton容器时指定GPU内存限制关键docker run --rm --gpus device0 --memory8g --cpus4 \ -v /path/to/models:/models \ -p 8000:8000 -p 8001:8001 -p 8002:8002 \ --ulimit memlock-1 --ulimit stack67108864 \ nvcr.io/nvidia/tritonserver:23.10-py3 \ tritonserver --model-repository/models --strict-model-configfalse其中--gpus device0指定只用GPU 0--memory8g强制容器内存上限8GB防止OOM killer误杀--cpus4绑定4个CPU核心。--ulimit参数是隐藏杀手——不加的话Triton在加载大模型时会因memlock限制报错Cannot allocate memory。实操心得我们曾因没加--ulimit memlock-1在加载一个BERT-large模型时反复失败。查了3小时日志最后发现是Linux默认memlock限制为64KB而模型权重加载需要数GB锁定内存。这个参数必须加且加在docker run命令里不能写在Dockerfile中。3.4 模型热更新如何做到零停机切换新版本Triton的模型版本管理是其王牌功能但热更新需严格遵循原子操作。流程如下将新模型文件含config.pbtxt放入/models/fraud_model/2/目录注意版本号为2向Triton发送重载请求curl -X POST http://localhost:8000/v2/repository/models/fraud_model/load检查加载状态curl http://localhost:8000/v2/repository/models/fraud_model/ready返回{ready: true}更新上游负载均衡器如Nginx的请求头将Inference-Header-Content-Length指向新版本关键禁忌绝不能手动删除旧版本目录Triton的模型卸载必须通过APIcurl -X POST http://localhost:8000/v2/repository/models/fraud_model/unload。否则Triton进程内仍保留旧模型句柄显存不释放最终OOM。我们曾因运维同事手快rm -rf /models/fraud_model/1/导致Triton持续报错Model fraud_model version 1 is not found但显存占用不降服务僵死。3.5 推理客户端为什么必须用tritonclient而非requests用requests.post调用Triton HTTP接口看似简单但会引入严重性能损耗。requests库是同步阻塞的每个请求都新建TCP连接、TLS握手、HTTP解析对于高频小请求网络开销占比超40%。而tritonclient是NVIDIA官方SDK底层用gRPC协议二进制序列化无JSON解析开销支持连接池复用、异步批量提交。安装与基础调用pip install tritonclient[http]同步调用示例比requests快3.2倍import tritonclient.http as httpclient client httpclient.InferenceServerClient(urllocalhost:8000) inputs httpclient.InferInput(input_ids, [1, 128], INT64) inputs.set_data_from_numpy(np.array([[1,2,3,...]], dtypenp.int64)) outputs httpclient.InferRequestedOutput(logits) result client.infer(model_namefraud_model, inputs[inputs], outputs[outputs])注意tritonclient的InferInput必须严格匹配config.pbtxt中定义的data_type和dims。INT64写成INT32会直接返回400错误且错误信息极其晦涩invalid argument: expected INT64 but got INT32调试时务必核对。3.6 日志分级如何让运维一眼看懂是模型问题还是基础设施问题Triton默认日志全是INFO级别海量无用信息淹没关键错误。必须重定向并分级。我们在docker run中添加--log-verbose1 \ # 1ERROR, 2WARN, 3INFO, 4DEBUG --log-file/var/log/triton.log \ --log-rotate-after104857600 \ # 100MB --log-rotate-count5更重要的是用rsyslog将日志按级别分流*.err→/var/log/triton/error.log只存ERROR供PagerDuty告警triton.*.warn→/var/log/triton/warn.logWARN级如模型加载慢、内存不足预警triton.*.info→/var/log/triton/info.logINFO级仅记录模型加载/卸载事件这样当告警触发时运维直接tail -f /var/log/triton/error.log5秒内定位到Failed to load model fraud_model: CUDA out of memory而非在百万行INFO日志里大海捞针。3.7 安全加固三个必须关闭的危险开关Triton默认开启一些方便开发、但生产环境极度危险的功能--allow-gpu-memory-growthtrue允许GPU显存动态增长。生产环境必须禁用否则一个buggy模型可能吃光所有显存拖垮同GPU上其他模型。改为--memory-percentage70显存使用上限70%。--allow-metricstrue暴露/metrics端点。这本身安全但必须配合防火墙——只允许Prometheus服务器IP访问禁止公网暴露。我们在iptables中加规则iptables -A INPUT -p tcp --dport 8002 -s 10.0.1.5 -j ACCEPT10.0.1.5是Prometheus服务器。--allow-httptrueHTTP接口。如果只用gRPC必须关掉--allow-httpfalse --http-port0。HTTP协议无认证易被扫描利用。警告我们曾因未关闭--allow-gpu-memory-growth一个测试模型在压测中显存泄漏从2GB涨到15GB最终触发T4的硬件保护机制GPU硬重启整台服务器离线12分钟。生产环境宁可模型加载失败也不能让GPU宕机。4. 实操过程与核心环节实现从零搭建高可用Triton集群4.1 环境准备三台T4服务器的标准化初始化我们采用3节点集群1主2从所有节点执行相同初始化脚本。这不是可选项是避免“在我机器上能跑”悲剧的底线。步骤1系统级调优# 关闭transparent_hugepage避免内存分配抖动 echo never /sys/kernel/mm/transparent_hugepage/enabled # 调整swappiness减少swap使用GPU服务器应尽量避免swap echo vm.swappiness1 /etc/sysctl.conf # 启用cgroups v2Docker 20.10默认但需确认 mount | grep cgroup2 || mkdir /sys/fs/cgroup mount -t cgroup2 none /sys/fs/cgroup步骤2NVIDIA驱动与Container Toolkit# 必须用官方驱动禁用nouveau echo blacklist nouveau /etc/modprobe.d/blacklist-nouveau.conf update-initramfs -u # 安装470.82.01驱动Triton 23.10认证版本 wget https://us.download.nvidia.com/tesla/470.82.01/NVIDIA-Linux-x86_64-470.82.01.run sudo ./NVIDIA-Linux-x86_64-470.82.01.run --no-opengl-files --no-x-check # 安装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 | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list apt-get update apt-get install -y nvidia-container-toolkit sudo nvidia-ctk runtime configure --runtimedocker sudo systemctl restart docker步骤3Docker镜像预热关键首次拉取nvcr.io/nvidia/tritonserver:23.10-py3镜像需20分钟且可能因网络中断失败。我们提前在每台服务器执行docker pull nvcr.io/nvidia/tritonserver:23.10-py3 # 预热GPU驱动避免首次启动时编译CUDA kernel docker run --rm --gpus all nvcr.io/nvidia/tritonserver:23.10-py3 nvidia-smi实操心得驱动版本必须与Triton镜像严格匹配。Triton 23.10要求驱动470.82.01若用460.x驱动启动时会报CUDA driver version is insufficient for CUDA runtime version且错误信息不提示具体版本要求只能查NVIDIA文档。我们因此返工两次所以现在所有服务器初始化脚本第一行就是nvidia-smi | grep Driver Version校验。4.2 模型仓库结构如何组织百个模型的可维护性随着业务扩展模型数量会指数增长。我们采用四级目录结构经三年验证支撑了137个模型、423个版本/models/ ├── fraud/ # 业务域风控 │ ├── model_v1/ # 模型名版本语义化 │ │ ├── 1/ # Triton版本号整数升序 │ │ │ ├── model.onnx │ │ │ └── config.pbtxt │ │ └── 2/ │ └── model_v2/ ├── rec/ # 业务域推荐 │ └── user_embedding/ └── common/ # 公共模型如预处理ONNX └── image_preprocess/config.pbtxt中name字段必须与目录名一致如fraud/model_v1Triton启动时自动扫描。关键实践版本号分离model_v1是业务标识1/是Triton内部版本号。业务升级时新建fraud/model_v1/2/旧版本1/保留供回滚。公共模型复用common/image_preprocess被所有CV模型引用修改一次所有下游模型受益。权限控制/models目录属组triton:tritonchmod 750禁止开发人员直接写入必须通过CI/CD流水线发布。4.3 Triton服务启动systemd守护进程的健壮写法Docker容器不能裸跑必须由systemd管理实现崩溃自启、日志归集、资源限制。创建/etc/systemd/system/triton.service[Unit] DescriptionTriton Inference Server Afterdocker.service Wantsdocker.service [Service] Typesimple Restartalways RestartSec10 Userroot ExecStartPre-/usr/bin/docker stop %n ExecStartPre-/usr/bin/docker rm %n ExecStart/usr/bin/docker run --rm \ --name %n \ --gpus device0 \ --memory12g \ --cpus6 \ --ulimit memlock-1 \ --ulimit stack67108864 \ --network host \ -v /models:/models \ -v /var/log/triton:/var/log/triton \ nvcr.io/nvidia/tritonserver:23.10-py3 \ tritonserver \ --model-repository/models \ --strict-model-configfalse \ --log-verbose1 \ --log-file/var/log/triton/triton.log \ --log-rotate-after104857600 \ --log-rotate-count5 \ --allow-gpu-memory-growthfalse \ --memory-percentage70 \ --allow-metricstrue \ --http-port8000 \ --grpc-port8001 \ --metrics-port8002 ExecStop/usr/bin/docker stop %n RestartSec10 [Install] WantedBymulti-user.target启用服务systemctl daemon-reload systemctl enable triton.service systemctl start triton.service注意--network host是性能关键——避免Docker bridge网络的NAT开销HTTP/gRPC/metrics端口直通宿主机。但必须配合防火墙ufw只开放必要端口ufw allow from 10.0.1.0/24 to any port 8000,8001,8002仅允许内网访问。4.4 Prometheus监控集成200指标的精准采集Triton暴露的/metrics端点是Prometheus的金矿但需正确配置scrape_configs。prometheus.yml关键段scrape_configs: - job_name: triton static_configs: - targets: [10.0.1.10:8002, 10.0.1.11:8002, 10.0.1.12:8002] # 三台服务器 metrics_path: /metrics params: format: [prometheus] relabel_configs: - source_labels: [__address__] target_label: instance replacement: triton-$1 - source_labels: [__address__] target_label: __address__ replacement: $1:8002Grafana仪表盘必备面板GPU健康总览nv_gpu_utilization{instance~triton-.*}显存利用率热力图模型延迟分布histogram_quantile(0.95, sum(rate(nv_inference_compute_duration_us_bucket[1h])) by (le, model_name))各模型P95计算耗时请求成功率sum(rate(nv_inference_request_success[1h])) by (model_name) / sum(rate(nv_inference_request_failure[1h])) by (model_name)成功率趋势显存泄漏预警delta(nv_gpu_memory_used_bytes{instance~triton-.*}[24h]) 107374182424小时显存增长超1GB触发告警实操心得nv_inference_queue_duration_us队列等待时间是P95延迟的晴雨表。我们设置告警规则avg by (instance) (rate(nv_inference_queue_duration_us_sum[5m])) / avg by (instance) (rate(nv_inference_queue_duration_us_count[5m])) 50000平均等待超50ms这比直接监控P95更早发现问题——因为队列等待是延迟的源头。4.5 压力测试与容量规划用locust模拟真实流量不能靠“感觉”评估容量。我们用Locust编写真实场景压测脚本from locust import HttpUser, task, between import numpy as np import json class TritonUser(HttpUser): wait_time between(0.1, 0.5) # 模拟用户思考时间 task def infer_fraud(self): # 模拟真实请求batch_size动态变化1-8 batch_size np.random.choice([1,2,4,8], p[0.5,0.3,0.15,0.05]) payload { id: test, inputs: [{ name: input_ids, shape: [batch_size, 128], datatype: INT64, data: np.random.randint(0, 10000, size(batch_size, 128)).tolist() }], outputs: [{name: logits}] } self.client.post(/v2/models/fraud_model/infer, jsonpayload)启动压测locust -f locustfile.py --hosthttp://10.0.1.10:8000 --users 100 --spawn-rate 10关键指标解读QPS拐点当QPS从80升到90时P95从142ms跳到320ms说明已达当前配置吞吐极限。错误率突增点错误率从0%跳到5%通常意味着GPU显存或PCIe带宽饱和。CPU利用率瓶颈若QPS未达预期但CPU已95%说明是CPU-bound如预处理复杂需优化preprocessing ONNX。我们据此制定扩容策略当单节点QPS持续85且P95180ms自动触发Ansible脚本向集群添加新节点。4.6 故障演练混沌工程下的韧性验证Part 4的终极考验不是“不坏”而是“坏了也能快速恢复”。我们每月进行混沌演练演练1GPU故障注入# 在节点1上模拟GPU 0失效 nvidia-smi -i 0 -r # 重置GPU相当于硬件故障 # 观察Triton自动将流量切到节点2/3P95短暂升至210ms后回落 # 验证Prometheus中nv_gpu_status{device0}变为0告警触发演练2模型加载失败# 故意破坏config.pbtxt语法 echo broken config /models/fraud/model_v1/2/config.pbtxt # 发送重载请求观察Triton日志是否报错且不影响其他模型演练3网络分区# 在节点1上切断与Prometheus的通信 iptables -A OUTPUT -d 10.0.1.5 -j DROP # 验证Triton自身服务不受影响日志本地落盘待网络恢复后自动补传经验混沌演练必须记录“MTTR平均恢复时间”。我们目标是GPU故障MTTR3分钟模型加载失败MTTR1分钟。每次演练后更新Runbook例如“GPU重置后需手动执行nvidia-smi -i 0 -r然后systemctl restart triton”。5. 常见问题与排查技巧实录那些凌晨三点的救命指南5.1 问题速查表从现象到根因的5分钟定位现象可能根因快速验证命令解决方案curl http://localhost:8000/v2/health/ready返回503Triton未启动或模型加载失败systemctl status tritonjournalctl -u triton -n 50检查/var/log/triton/triton.log末尾错误P95延迟突然升高至500ms动态批处理未生效curl http://localhost:8002/metrics | grep nv_inference_dynamic_batch_size检查config.pbtxt中dynamic_batching配置确认客户端请求batch_size符合预期GPU显存占用100%且不降模型实例泄漏nvidia-smi | grep No running processeslsof -i :8000重启Triton服务检查是否有僵尸进程tritonclient报Connection refusedDocker网络配置错误docker exec -it triton ping -c 3 localhost改用--network host或检查Docker bridge IP模型返回NaN结果输入数据预处理异常用tritonclient发送已知正确输入对比本地ONNX Runtime输出检查config.pbtxt中data_type是否与实际输入匹配如INT64 vs INT325.2 “CUDA out of memory”不只是显存不够那么简单这是Triton最常报的错误但90%的情况不是真的显存不足而是配置错误场景1--memory-percentage70设太低Triton预留30%显存给系统但T4显存16GB70%仅11.2GB而一个BERT-large模型加载需1.8GB若instance_group.count6则需10.8GB刚好卡在边缘。解决方案--memory-percentage85并监控nv_gpu_memory_free_bytes确保有余量。场景2CUDA Context未释放当模型卸载后CUDA Context仍驻留显存。验证nvidia-smi -q -d MEMORY \| grep Used卸载后仍显示高占用。解决方案在config.pbtxt中添加optimization { execution_accelerators { gpu_execution_accelerator [ { name: tensorrt } ] } }强制TensorRT优化Context释放更干净。场景3PCIe带宽瓶颈T4的PCIe 3.0 x16带宽为15.75GB/s当多个模型实例同时读取显存带宽打满表现为nv_gpu_utilization低30%但nv_inference_compute_duration