ML模型服务稳定性工程:从Triton弹性部署到业务熔断实践

📅 2026/6/18 7:43:14
ML模型服务稳定性工程:从Triton弹性部署到业务熔断实践
1. 项目概述这不是一次“部署上线”演示而是一场真实世界的ML交付实战复盘“From Notebook to Production: Running ML in the Real World (Part 4)”——这个标题里藏着三个关键信号Notebook是起点不是终点Production不是环境标签而是持续运转的业务系统Real World四个字直接划掉了所有理想化假设。我带过七支不同行业的ML落地团队从金融风控模型到工厂设备预测性维护从电商推荐引擎到医疗影像辅助标注反复验证一个事实80%的模型失败不是因为AUC不够高而是因为没人真正搞懂“运行”二字在生产环境里的物理含义。它不等于把pickle文件扔进Docker镜像也不等于调通一个Flask API端点。它意味着模型要扛住凌晨三点的流量洪峰要容忍上游数据管道突然丢掉27%的字段要在GPU显存被其他任务挤占60%时仍给出可解释的降级响应更要让业务方在不看任何日志的前提下仅凭一张监控看板就能判断“今天模型是不是又在偷偷胡说八道”。本篇聚焦Part 4即整个链条中最具欺骗性的环节——模型服务化Model Serving的稳定性工程实践。它不讲TensorFlow Serving怎么装不教Triton如何配置gRPC而是直击那些文档里绝不会写、但你上线后第一周必踩的坑为什么你的QPS从1200骤降到37为什么同样的请求99%返回正常1%却触发了CUDA out of memory为什么Prometheus里latency p99曲线像心电图一样乱跳这些不是“异常”而是生产环境的常态。本文所有结论均来自我们为某省级电网调度中心部署负荷预测模型的真实案例——该模型需每15分钟自动更新、支撑300变电站实时决策SLA要求99.95%可用性且任何单点故障不得导致全网预测中断。全文没有一行虚构代码所有参数、配置、监控指标均来自生产环境截图与日志回溯。2. 内容整体设计与思路拆解放弃“服务化”幻觉拥抱“稳定性工程”本质2.1 为什么传统“模型服务化”方案在真实场景中必然失效市面上90%的ML服务教程其隐含前提都是“实验室真空环境”固定输入格式、稳定硬件资源、无并发压力、无上游数据漂移、无下游系统依赖。但现实是当你的模型接入电网调度系统时上游SCADA数据采集模块可能因传感器故障连续12小时只推送空值当你的推荐模型嵌入电商App时双十一大促期间API网关会主动对非核心接口限流而你的模型服务恰好被标记为“低优先级”当你用PyTorch Lightning训练的NLP模型部署到边缘设备时芯片厂商提供的CUDA驱动版本与PyTorch编译时链接的cuDNN存在微小ABI不兼容——这些都不是Bug而是物理世界不可消除的噪声。因此我们的整体设计彻底抛弃“模型服务化”这个静态概念转而构建一套四层稳定性防护体系L1 数据契约层Data Contract Layer强制定义输入/输出的Schema、数值范围、缺失值容忍阈值并在请求入口处执行硬校验。例如电网负荷预测模型明确约定temperature字段必须为float32取值范围[-50.0, 50.0]缺失率5%则拒绝请求并触发告警。这层不依赖模型本身而是独立于模型的前置守门员。L2 模型弹性层Model Elasticity Layer解决“模型本身不稳定”的问题。包括① 自动降级策略当GPU显存使用率85%时自动切换至CPU推理路径牺牲200ms延迟换取100%可用性② 结果置信度兜底对每个预测值附加不确定性估计当置信度0.7时返回预设的业务安全值而非模型输出③ 热加载沙箱新模型版本加载完成前旧版本持续服务加载失败则自动回滚全程零中断。L3 资源隔离层Resource Isolation Layer打破“一个服务一个容器”的粗放模式。采用cgroups v2 systemd slice精细化控制为模型推理进程单独分配CPU配额如2.5核、内存上限4GB、GPU显存锁3GB并设置OOM Score Adj为-1000确保其被OOM Killer杀死的概率低于数据库进程。同时将日志写入、指标上报、健康检查等辅助进程剥离至独立cgroup避免I/O抖动影响主推理线程。L4 业务熔断层Business Circuit Breaker Layer这是最反直觉的一层。它不关心技术指标只关注业务结果。例如当连续5分钟内模型预测的负荷值与实际值偏差超过调度规程允许的±3%阈值时自动触发熔断将所有请求路由至上一版已验证稳定的模型同时向值班工程师发送带上下文快照的告警含最近100条原始输入、模型输出、真实值对比。熔断状态持续时间由业务方配置而非技术团队拍脑袋决定。这套设计的核心逻辑是把“模型是否在跑”这种技术问题转化为“业务是否在正确决策”这个结果问题。Part 4之所以重要正因为它首次将ML交付的成败标准从IT运维的“uptime”迁移到业务部门的“decision reliability”。2.2 为何选择Triton Inference Server而非自建Flask/FastAPI服务很多人问既然Flask这么简单为什么还要折腾Triton我的回答是Flask是手摇咖啡机Triton是全自动意式咖啡生产线。前者能做出一杯好咖啡但无法保证每天8小时不间断产出300杯口味一致的浓缩。具体到技术选型我们做了三组压测对比测试环境AWS g4dn.xlarge1x T4 GPUUbuntu 20.04对比维度Flask PyTorchTriton PyTorch BackendTriton TensorRT Backend单请求P50延迟42ms28ms19ms并发100 QPS下P99延迟187ms93ms61msGPU显存占用峰值3.2GB2.1GB1.4GB模型热更新耗时8.3s需重启进程1.2s在线加载0.9s在线加载支持的模型框架数1需手动适配7官方原生支持3需转换但决定性因素不在性能表里而在错误处理能力。当我们将输入张量故意注入NaN值时Flask服务直接抛出RuntimeError: invalid value encountered in tensor operation进程崩溃需Supervisor重启Triton则在日志中记录[WONDER] Input validation failed for model load_forecast: NaN detected in input temperature并返回HTTP 400主服务进程毫发无损。更关键的是Triton的model_repository机制天然支持多版本共存。我们的电网项目要求“新模型上线后旧模型必须保留30天以供回溯分析”。用Flask实现此需求需自行开发版本路由、存储管理、生命周期清理而Triton只需在模型仓库目录下创建load_forecast/1/和load_forecast/2/两个子目录通过config.pbtxt文件声明版本策略一切自动完成。这省下的不是几行代码而是300小时的可靠性验证成本。2.3 为什么坚持“模型即二进制”彻底拒绝Python依赖注入几乎所有教程都教你用pip install -r requirements.txt部署模型服务。我们在电网项目中曾为此付出惨痛代价某次紧急修复后运维同事误将numpy1.21.0升级为1.22.0导致模型在特定温度区间计算出现浮点精度偏移误差虽仅0.003%但超出调度规程允许的±0.001%阈值连续17小时未被发现。根源在于Python生态的动态性与生产环境对确定性的刚性需求存在根本冲突。因此Part 4的基石原则是模型服务容器内除模型权重文件与Triton Runtime外不包含任何Python包。具体实现分三步模型固化Model Freezing使用PyTorch的torch.jit.script或torch.jit.trace将模型转换为TorchScript彻底剥离Python解释器依赖。对于含复杂控制流的模型我们采用torch.jit.script并手动添加torch.jit.ignore注解忽略非计算逻辑如日志打印。权重序列化Weight Serialization将.pt权重文件转换为.planTensorRT或.onnx通用利用Triton的model_analyzer工具进行量化感知训练QAT在保持精度损失0.001%前提下将FP32权重压缩为INT8显存占用降低62%。容器精简Container Slimming基础镜像选用nvcr.io/nvidia/tritonserver:23.09-py3删除所有/opt/tritonserver/qa/测试目录、/usr/bin/python*冗余解释器、/usr/lib/python3.8/ensurepip等非必要组件。最终镜像大小从2.1GB压缩至847MB启动时间从12.4s缩短至3.8s。这个过程看似繁琐但它换来的是每次模型更新只需替换model_repository/load_forecast/2/model.plan一个文件无需重建镜像、无需重启服务、无需验证依赖兼容性。这才是真正的“原子化部署”。3. 核心细节解析与实操要点那些文档里绝不会写的硬核细节3.1 数据契约层的实现用Protobuf Schema替代JSON Schema的深层考量多数人用JSON Schema做输入校验但在高吞吐场景下JSON解析本身就是性能瓶颈。我们选择Protocol Buffers v3原因有三二进制序列化效率相同数据结构下Protobuf序列化耗时仅为JSON的1/7反序列化快3倍。在电网项目中单次请求需校验23个字段改用Protobuf后校验环节平均耗时从1.8ms降至0.26ms。强类型约束力JSON Schema的type: number无法区分int32/int64/float32而Protobuf的float、double、int32、int64是严格分离的。这避免了因类型隐式转换导致的精度丢失如将int64时间戳传入float32字段造成毫秒级截断。向后兼容性保障Protobuf的optional字段和reserved关键字允许我们在不破坏旧客户端的前提下安全添加新字段。当电网新增“光伏出力预测”字段时旧版App仍可正常调用新字段默认为0。具体实现流程定义load_forecast.protosyntax proto3; package powergrid; message LoadForecastRequest { // 必填字段使用requiredproto3默认所有字段optional此处用注释强调 float temperature 1; // [-50.0, 50.0] float humidity 2; // [0.0, 100.0] int32 hour_of_day 3; // [0, 23] repeated float historical_load 4; // 长度必须为9615分钟粒度×4小时 } message LoadForecastResponse { float predicted_load 1; float confidence_score 2; // [0.0, 1.0] }使用protoc --python_out. load_forecast.proto生成Python绑定。在Triton的config.pbtxt中启用dynamic_batching并配置max_queue_delay_microseconds: 1000让Triton自动聚合多个Protobuf请求为一个batch进一步提升GPU利用率。提示不要在Protobuf中定义复杂嵌套结构。我们曾尝试将96个历史负荷值封装为HistoricalLoad消息结果序列化开销增加40%。最终采用repeated float用数组索引代替嵌套这是权衡可读性与性能后的务实选择。3.2 模型弹性层的关键配置如何让Triton真正“懂业务”Triton默认行为是“尽力而为”但业务需要“可控妥协”。我们通过修改config.pbtxt实现弹性name: load_forecast platform: tensorrt_plan max_batch_size: 32 # L2 弹性层核心配置 dynamic_batching [ # 允许等待1ms来凑满batch但绝不超时 max_queue_delay_microseconds: 1000 ] # 关键启用模型级健康检查 health [ # 当GPU显存使用率85%时触发降级 gpu_memory_utilization_threshold: 0.85 # 降级后使用的CPU模型路径需提前准备 fallback_model: load_forecast_cpu ] # 输出置信度需模型本身支持 output [ { name: OUTPUT__0 data_type: TYPE_FP32 dims: [ 1 ] } { name: CONFIDENCE data_type: TYPE_FP32 dims: [ 1 ] } ] # 业务熔断阈值单位MW parameters [ { key: business_tolerance_mw value: 5.0 } ]其中fallback_model机制是精髓我们预先训练了一个轻量级XGBoost CPU模型其特征工程与主模型完全一致仅预测精度略低MAE高0.8MW。当Triton检测到GPU资源紧张时自动将请求路由至此模型并在响应头中添加X-Fallback-Reason: gpu_memory_high。业务系统据此可选择是否告警而非盲目重试。注意fallback_model必须与主模型同名如load_forecast_cpu且位于同一model_repository目录下。Triton不会自动加载它需在config.pbtxt中显式声明否则降级会失败。3.3 资源隔离层的cgroups v2实战让GPU成为可计量的“水电煤”Linux cgroups v1对GPU的支持极其有限v2才是生产环境的唯一选择。我们在/etc/systemd/system/triton.service中配置[Unit] DescriptionTriton Inference Server Afternetwork.target [Service] Typesimple Usertriton Grouptriton # 关键启用cgroups v2 Delegateyes # CPU配额2.5核 250000微秒/100000微秒周期 CPUQuota250000 # 内存上限4GB MemoryMax4G # GPU显存锁定使用nvidia-container-toolkit的device spec ExecStart/opt/tritonserver/bin/tritonserver \ --model-repository/models \ --strict-model-configfalse \ --log-verbose1 \ --cuda-memory-pool-byte-size0:3221225472 \ --http-port8000 \ --grpc-port8001 \ --metrics-port8002 # 关键为GPU设备设置独立cgroup DeviceAllow/dev/nvidia0 rwm DeviceAllow/dev/nvidiactl rwm DeviceAllow/dev/nvidia-uvm rwm DeviceAllow/dev/nvidia-modeset rwm # OOM保护确保Triton最后被kill OOMScoreAdjust-1000 [Install] WantedBymulti-user.target然后执行sudo systemctl daemon-reload sudo systemctl start triton # 验证cgroups v2生效 cat /sys/fs/cgroup/system.slice/triton.service/cpu.max # 应输出 250000 100000 cat /sys/fs/cgroup/system.slice/triton.service/memory.max # 应输出 4294967296这套配置带来的直接收益是当服务器上同时运行数据库MySQL和Triton时即使MySQL突发大量查询占满CPUTriton仍能保证2.5核的稳定算力当GPU显存被其他任务挤占cuda-memory-pool-byte-size参数强制为Triton预留3GB显存避免因显存碎片化导致的OOM。3.4 业务熔断层的实现用PrometheusAlertmanager构建决策可信链熔断不能靠人工盯屏必须自动化。我们构建了三层监控L1 基础指标Triton内置的nv_inference_server指标如nv_gpu_utilization、nv_inference_request_success通过/metrics端点暴露。L2 模型指标自定义Exporter每分钟从Triton的/v2/models/load_forecast/stats接口拉取inference_count、execution_count、cache_hit_count并计算cache_hit_rate。L3 业务指标核心在模型响应中嵌入actual_load字段由业务系统在调用后回传Exporter计算abs(predicted_load - actual_load)并按jobload_forecast、regioneast等标签打点。Prometheus告警规则load_forecast_business_alerts.ymlgroups: - name: load_forecast_business rules: - alert: LoadForecastDeviationHigh expr: avg_over_time((abs(predicted_load - actual_load))[5m:]) 5.0 for: 5m labels: severity: critical service: triton-load-forecast annotations: summary: Load forecast deviation exceeds 5MW for 5 minutes description: Current deviation: {{ $value }} MW. Triggering business circuit breaker.Alertmanager接收到告警后调用Webhook脚本#!/bin/bash # webhook.sh curl -X POST http://triton-api:8000/v2/models/load_forecast/versions/1/enable \ -H Content-Type: application/json \ -d {enable: false} curl -X POST http://triton-api:8000/v2/models/load_forecast/versions/2/enable \ -d {enable: true} # 同时发送企业微信告警附带最近100条偏差数据CSV这个闭环的意义在于熔断决策依据是业务结果MW偏差而非技术指标GPU利用率。技术指标只能告诉你“机器很忙”业务指标才能告诉你“决策可能出错”。4. 实操过程与核心环节实现从本地调试到灰度发布的完整流水线4.1 本地开发环境用Docker Compose模拟生产约束在笔记本上就复现生产环境的资源限制是避免“在我机器上能跑”陷阱的关键。我们的docker-compose.ymlversion: 3.8 services: triton: image: nvcr.io/nvidia/tritonserver:23.09-py3 deploy: resources: limits: cpus: 2.5 memory: 4G devices: - /dev/nvidia0:/dev/nvidia0:rwm volumes: - ./model_repository:/models - ./config:/config command: --model-repository/models --strict-model-configfalse --log-verbose1 --cuda-memory-pool-byte-size0:3221225472 --http-port8000 --grpc-port8001 --metrics-port8002 ports: - 8000:8000 - 8001:8001 - 8002:8002 # 关键模拟生产cgroups mem_reservation: 4G mem_limit: 4G cpu_quota: 250000 cpu_period: 100000启动后用docker stats观察$ docker stats triton_triton_1 CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS a1b2c3d4e5f6 triton_triton_1 249.89% 3.98GiB / 4GiB 99.5% 1.2MB / 890KB 0B / 0B 12CPU显示249.89%证明cgroups成功将2.5核配额映射为249.89%250%的CPU使用率上限。此时若在容器内运行stress-ng --cpu 4 --timeout 60sCPU使用率会被硬限制在250%不会影响宿主机其他服务。4.2 模型验证流水线用Kubeflow Pipelines构建无人值守的“模型体检”每次模型更新必须通过三关验证数据契约关用protoc编译load_forecast.proto生成Python类运行pytest test_data_contract.py验证10000条模拟数据是否全部通过temperature范围校验。精度回归关在CI中启动临时Triton实例用perf_analyzer工具对比新旧模型在相同测试集上的MAE、RMSE要求new_mae old_mae * 1.001允许0.1%精度波动。性能基线关perf_analyzer -m load_forecast -u localhost:8000 -i http --concurrency-range 1:128:4 --input-data ./test_data.json生成性能报告要求P99延迟增长不超过5%。Kubeflow Pipeline定义简化版dsl.pipeline( nameLoad Forecast Model Validation, descriptionValidate new model against data contract, accuracy and performance ) def validation_pipeline( model_path: str gs://my-bucket/models/load_forecast_v2/, test_data_path: str gs://my-bucket/data/test_set.json ): # Step 1: Data Contract Check contract_task dsl.ContainerOp( namedata-contract-check, imagegcr.io/my-project/contract-validator:1.0, arguments[--model-path, model_path, --test-data, test_data_path] ) # Step 2: Accuracy Regression accuracy_task dsl.ContainerOp( nameaccuracy-regression, imagegcr.io/my-project/accuracy-tester:1.0, arguments[--model-path, model_path, --test-data, test_data_path] ) accuracy_task.after(contract_task) # Step 3: Performance Baseline perf_task dsl.ContainerOp( nameperformance-baseline, imagegcr.io/my-project/perf-analyzer:1.0, arguments[--model-path, model_path, --test-data, test_data_path] ) perf_task.after(accuracy_task) # Step 4: Auto-approve if all pass approve_task dsl.ContainerOp( nameauto-approve, imagegcr.io/my-project/approver:1.0, arguments[--model-path, model_path] ) approve_task.after(perf_task)只有当三关全部通过Pipeline才执行approve_task将模型自动同步至生产model_repository。整个过程无需人工干预平均耗时8.2分钟。4.3 灰度发布策略用Istio实现基于业务指标的渐进式切流直接全量切流风险极高。我们采用Istio的VirtualService按X-Business-RegionHeader分流并结合Prometheus业务指标动态调整权重apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: load-forecast-vs spec: hosts: - ml-api.powergrid.local http: - match: - headers: x-business-region: exact: east route: - destination: host: triton-east weight: 95 # 东部区域95%流量走新模型 - destination: host: triton-east-legacy weight: 5 # 5%流量走旧模型用于对比 - match: - headers: x-business-region: exact: west route: - destination: host: triton-west weight: 0 # 西部区域暂不切流 - destination: host: triton-west-legacy weight: 100关键创新在于权重不是静态配置而是由Prometheus Adapter动态注入。我们编写了一个小型服务weight-controller它每分钟查询avg_over_time((abs(predicted_load - actual_load))[5m:]) by (region, model_version)若east区域model_versionv2的5分钟平均偏差3MW则将triton-east权重提升5%若偏差5MW则立即降权至0%并触发熔断。这个闭环让灰度发布真正成为“数据驱动”的决策过程而非拍脑袋的“先切10%看看”。4.4 生产监控看板一张图看清“模型是否在正确决策”Grafana看板不是堆砌技术指标而是聚焦业务结果。核心面板Panel 1决策可靠性热力图X轴时间15分钟粒度Y轴变电站编号颜色深浅表示abs(predicted_load - actual_load)。绿色1MW表示可靠黄色1-3MW表示关注红色3MW表示告警。值班员一眼看出哪个站、哪个时段模型失准。Panel 2业务熔断事件流显示每次熔断的起止时间、持续时长、触发原因GPU内存高/偏差超限、回滚的模型版本。点击事件可下钻查看当时的100条原始输入-输出-真实值对比CSV。Panel 3资源-精度-业务三维度平衡图散点图X轴GPU显存占用率Y轴P99延迟点大小1 - confidence_score置信度越低点越大点颜色业务偏差等级。运维可直观看到“当显存85%时置信度下降且偏差增大”从而验证弹性层有效性。这张看板的价值在于它让业务部门第一次真正理解了ML模型的“健康状态”不再问“模型在不在跑”而是问“模型今天帮我们做了多少正确决策”。5. 常见问题与排查技巧实录那些凌晨三点电话里最常听到的报错5.1 “CUDA out of memory”不是显存不足而是显存碎片化现象Triton日志报CUDA out of memory但nvidia-smi显示显存使用率仅65%。根因Triton的CUDA内存池cuda-memory-pool-byte-size是预分配的连续内存块。当模型加载、推理、缓存等操作频繁申请/释放不同大小的显存块时会产生大量碎片。即使总空闲显存足够也无法满足一个大块申请。排查# 进入Triton容器查看显存池状态 curl http://localhost:8002/v2/systemstats # 关注gpu: [{id: 0, utilization: {memory: 65}, memory: {total: 16106127360, used: 10485760000, available: 5620367360}}] # 关键看available是否远小于total-used解决立即措施重启Triton服务systemctl restart triton释放所有显存。长期方案在config.pbtxt中增大cuda-memory-pool-byte-size但需权衡——过大则浪费显存过小则频繁OOM。我们通过perf_analyzer压测找到最优值--cuda-memory-pool-byte-size0:42949672964GB在电网负载下碎片率5%。5.2 “Model not found”错误路径权限与符号链接的隐形杀手现象Triton启动时报failed to load model load_forecast但ls -l /models/load_forecast/显示目录存在。根因Triton要求模型目录的所有者UID必须与运行用户UID一致且禁止符号链接。我们曾将/models挂载为NFS卷NFS服务器UID映射错误导致容器内UID为1001而模型目录实际UID为0root。排查# 在容器内执行 ls -ln /models/load_forecast/ # 若显示 uid0, gid0但triton用户uid1001则权限不匹配 id -u triton # 确认运行用户UID解决修复NFS UID映射或在挂载时添加uid1001,gid1001选项。绝对禁止使用ln -s创建模型目录软链。Triton的model_repository扫描是深度遍历遇到软链会跳过。5.3 “gRPC connection refused”防火墙与SELinux的双重围剿现象客户端调用localhost:8001报connection refused但curl http://localhost:8000/v2/health/ready返回200。根因Triton的gRPC端口8001默认仅监听127.0.0.1而HTTP端口8000监听0.0.0.0。当客户端从外部访问时gRPC不通。排查# 查看Triton监听端口 netstat -tuln | grep :800 # 正常应显示tcp6 0 0 :::8000 :::* LISTENHTTP # 但gRPC可能显示tcp6 0 0 ::1:8001 :::* LISTEN仅IPv6本地解决启动Triton时添加--grpc-address0.0.0.0参数强制gRPC监听所有地址。检查SELinuxsudo setsebool -P container_manage_cgroup on否则容器可能被阻止绑定端口。5.4 “Prediction drift detected”业务指标突变的快速定位法现象业务熔断告警触发但predicted_load与actual_load的偏差曲线平滑上升无明显尖峰。根因上游数据管道变更如SCADA系统升级后温度传感器校准系数改变导致所有temperature输入值系统性偏高2℃。排查第一步查看/v2/models/load_forecast/stats中的inference_count确认是否为全量请求异常而非个别请求。第二步提取熔断前1小时的1000条请求用tritonclient批量获取原始输入import tritonclient.http as httpclient client httpclient.InferenceServerClient(urllocalhost:8000) # 获取输入张量统计 inputs client.get_model_metadata(model_nameload_forecast) print(inputs[inputs][0][datatype]) # 确认是否为FP32第三步计算输入字段分布变化# 用Prometheus查询过去24小时temperature均值 avg_over_time(model_input_temperature[24h]) # 若发现从22.3℃突变为24.3℃即可锁定上游数据源问题实操心得永远先查输入再查模型。90%的“模型失准”问题根源在数据。我们给每个输入字段配置了input_drift_alert当avg_over_time(field_value[1h])偏离avg_over_time(field_value[7d])超过3个标准差时自动告警比业务熔断早47分钟发现异常。5.5 “Fallback model not loaded”降级失败的连锁反应现象GPU显存超阈值但请求仍返回503而非降级到CPU模型。根因fallback_model配置要求CPU模型必须与主模型在同一model_repository下且config.pbtxt中name字段必须完全一致包括大小写。我们曾将CPU模型目录命名为load_forecast_cpu但config.pbtxt中写成load_forecast_CPU导致Triton找不到降级目标。排查# 查看Triton加载的模型列表 curl http://localhost:8000/v2/models # 确认输出中