情感识别模型部署实战:从ONNX转换到FastAPI服务化指南

情感识别模型部署实战:从ONNX转换到FastAPI服务化指南 前阵子接了个情感识别模型的部署需求本来以为把训练好的权重往服务器上一放写个接口就完事了结果从环境配置到服务上线前后折腾了快两周。整个过程里踩的坑比训练模型时还多尤其是模型格式转换、推理性能优化、外部端调用这几个环节几乎每一步都有雷。这篇文章我就把这些坑和对应的解决方案完整记录下来给后面要做本地模型部署、特别是情感识别这类文本模型的朋友做个参考。先说清楚这篇内容解决什么问题如果你手头有一个训练好的情感识别模型想把它从Jupyter Notebook里挪到实际业务环境中让手机App、网页后端或者内部系统能稳定调用那这里面的选型思路、部署架构、配置参数和排错方法基本都能直接用。适合刚接触模型部署的算法工程师也适合要接手AI服务的后端开发。1. 先想清楚情感识别模型部署到底部署的是什么1.1 情感识别任务的两种技术路线情感识别在工程落地时通常有两套完全不同的实现路线路线A微调小型预训练模型。拿BERT、RoBERTa、XLNet这类模型在情感标注数据上做微调得到一个几亿参数的小模型一般用PyTorch或TensorFlow保存为权重文件。这种模型大小在100MB到500MB之间单条文本推理耗时几十毫秒用CPU也能跑。路线B用大语言模型做零样本情感分析。直接让Qwen、Llama这类大模型对文本进行情感判断模型输出“积极/消极/中性”的结论。这种方案不需要标注数据泛化能力强但模型体积大从几个GB到几十个GB都有推理速度慢而且通常需要GPU才能实时响应。两条路线没有绝对的优劣关键看你的业务约束。我这次的需求是给一个移动端应用提供短文本情感判断接口对响应时间有要求同时服务器只有一张8G显存的显卡。综合评估下来我最终采用的是双轨方案对比项微调小型模型大语言模型模型体积100MB-500MB4GB-70GB推理延迟50ms左右2s以上硬件要求CPU/低配GPU高配GPU情感识别效果领域内表现好通用性好、可解释性强部署复杂度低高典型场景实时评分、舆情判断复杂情感分析、对话场景默认的实时接口走路线A用一个微调过的中文情感分类模型转成ONNX格式后用ONNX Runtime推理单条请求延迟控制在100ms以内。路线B作为兜底处理那些短文本无法判断、语境复杂的输入。这样既能保证线上服务的响应速度又能在复杂文本上维持准确率。1.2 部署链路拆解模型从训练机到生产环境要过几道关很多刚开始接触部署的人最容易犯的错就是以为部署就是把模型文件复制过去然后加载起来。实际上一个模型要真正在生产环境稳定跑起来至少要经过这样一条完整的链路训练产物.pt/.pth权重 → 格式转换ONNX/量化 → 推理引擎加载 → 业务预处理封装 → API服务化 → 鉴权与流量控制 → 外部调用每一环都是单独的课题。格式转换要考虑算子兼容性推理引擎要处理并发和显存分配服务化要解决超时、异常捕获、日志记录外部调用要考虑跨网络访问和接口安全。我在实际操作中就是因为在“格式转换”和“服务化”两个环节理解不够深导致前期反复返工。部署前还有一件很重要的事先把需求约束定义清楚。比如预估峰值并发是多少10 QPS还是100 QPS单次请求允许的最大响应时间是多少模型是跑在纯CPU环境还是GPU环境谁在调用这个接口手机App、网页后端还是内部脚本是否需要支持流式输出这些问题看起来基础但直接影响后续的模型选型和部署方案。比如你目标设备是手机端那就不能直接上一个十几GB的大模型必须考虑模型量化甚至蒸馏到几十MB的小模型。我这次因为一开始没确认移动端的网络环境走了不少弯路后来把模型量化到INT8才解决问题。2. 环境配置与模型准备版本不对后面全是坑2.1 硬件和基础软件版本怎么搭配先说我这次部署的硬件环境一台带NVIDIA RTX 3060显卡的服务器8G显存32G内存系统是Ubuntu 22.04。为什么推荐在Linux上部署因为生产环境绝大多数都是Linux而且NVIDIA驱动、CUDA、Docker这些工具链在Linux下最省心。如果你用的是Windows建议用WSL2跑Ubuntu效果接近原生Linux。软件版本搭配是第一个大坑。PyTorch、CUDA、cuDNN、ONNX Runtime这些软件之间的版本关系非常敏感版本不匹配会出现各种莫名其妙的报错比如“CUDA error: no kernel image is available”。这里给出一个我验证过的稳定组合直接抄作业# 基础环境 Ubuntu 22.04 Python 3.10 CUDA 11.8 cuDNN 8.6.0 PyTorch 2.0.1 # 创建虚拟环境 conda create -n emotion_deploy python3.10 conda activate emotion_deploy # 安装PyTorchCUDA 11.8对应版本 pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 推理引擎 pip install onnxruntime-gpu1.15.1 pip install fastapi uvicorn[standard]这里要特别说明为什么用PyTorch 2.0.1而不是更新的版本。我当时试过PyTorch 2.1以上版本ONNX导出倒是没问题但导出的模型在ONNX Runtime GPU上推理时偶尔会出现结果不一致的情况排查了两天才发现是PyTorch和ONNX Runtime的算子实现差异导致的。后来统一到PyTorch 2.0.1 ONNX Runtime 1.15.1这个组合一切正常。部署环境追求稳定不追求最新。还有一个Windows上常见的坑如果你在Windows 11上安装Ollama或直接跑Python推理可能会遇到显卡在PyTorch里识别不到的问题也就是torch.cuda.is_available()返回False。这时候去NVIDIA官网安装对应的驱动然后验证一下nvidia-smi如果这个命令能正常显示GPU信息说明驱动没问题。接着在Python里执行import torch print(torch.__version__) print(torch.cuda.is_available())如果返回False先检查PyTorch版本是不是CPU版本用pip list | grep torch查看再检查CUDA版本是否匹配。这个问题在Windows 11上出现的频率非常高注意PyTorch的下载来源一定不能是默认PyPI要显式指定CUDA版本。2.2 模型下载与格式转换HuggingFace下载到一半就断模型训练好之后接下来要解决的就是“怎么把权重文件搞到服务器上”。如果你用的是BERT这类公开预训练模型微调的产物大概率要从HuggingFace下载基础模型文件。但国内访问HuggingFace经常超时下载到一半断掉的情况很常见。最简单的解决方案是配置镜像环境变量export HF_ENDPOINThttps://hf-mirror.com这个镜像网站同步HuggingFace的模型文件结构完全一致通过设置环境变量就能让huggingface_hub库优先走镜像下载。实测下载速度提升非常明显。如果你有多个模型要下载建议写个Python脚本统一处理下载过程中加个resume_downloadTrue参数断了还能断点续传。模型下载之后尽量把它从PyTorch权重转成ONNX格式。为什么转ONNX因为ONNX Runtime比PyTorch原生的推理引擎更加轻量启动速度快CPU/GPU都支持而且部署时不依赖完整的PyTorch环境只装一个几十MB的ONNX Runtime库就能跑。这对于服务端部署来说太友好了不用为每个模型单独维护一套Python环境。下面是我用来导出ONNX的实战代码import torch from transformers import AutoTokenizer, AutoModelForSequenceClassification model_name your_finetuned_model_path model AutoModelForSequenceClassification.from_pretrained(model_name) tokenizer AutoTokenizer.from_pretrained(model_name) model.eval() # 构造一个样例输入batch_size1序列长度128 dummy_input torch.randint(0, 1000, (1, 128)) inputs { input_ids: dummy_input, attention_mask: torch.ones(1, 128, dtypetorch.long), token_type_ids: torch.zeros(1, 128, dtypetorch.long) } torch.onnx.export( model, tuple(inputs.values()), emotion_model.onnx, input_names[input_ids, attention_mask, token_type_ids], output_names[logits], dynamic_axes{input_ids: {0: batch_size, 1: seq_len}, attention_mask: {0: batch_size, 1: seq_len}, token_type_ids: {0: batch_size, 1: seq_len}, logits: {0: batch_size}}, opset_version14 ) print(ONNX导出完成)这里两个细节要重点说明第一dynamic_axes一定要配置。如果不配置模型序列长度就被固定成128真实场景下超过128个字的文本会直接报错。配置了动态轴之后“batch_size”和“seq_len”都允许变化灵活性高很多。但要注意序列长度过长会导致推理变慢建议在上层接口限制最大输入长度我这边设置的是256。第二opset_version参数ONNX Runtime 1.15.1对应Opset 14-17都没问题但别用太高的版本否则老版本推理引擎不兼容。我习惯用14兼容性最好目前还没遇到过算子不支持的报错。如果你追求更极致的性能还可以考虑量化。INT8量化后的模型体积能缩小到原来的四分之一推理速度提升两倍以上但同时会有轻微的精度损失。我这次在8G显存的服务器上跑并发量化后模型从180MB降到了50MB这对多实例部署非常关键。转ONNX后用onnxruntime.transformers工具做量化python -m onnxruntime.quantization.preprocess --model_input emotion_model.onnx --model_output emotion_model_preprocessed.onnx这里会遇到一个实际问题BERT类的模型包含Embedding层和Attention层直接对整个模型做动态量化时某些算子可能不支持量化比如LayerNorm。这时候不需要硬扛用onnxruntime.quantization.quantize_dynamic接口指定只量化部分算子的方式来处理from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( model_inputemotion_model.onnx, model_outputemotion_model_int8.onnx, weight_typeQuantType.QUInt8 )量化完了一定要做精度验证拿一批标注好的测试集跑一遍对比原始模型和量化模型的准确率差距。正常来说在1%以内是可以接受的超过2%就说明量化对模型的伤害太大要改回FP16或者换一种量化策略。3. 推理服务化从一行命令到稳定可用的API接口3.1 用FastAPI搭建情感识别推理服务模型文件准备好之后下一步就是把它封装成一个可以远程调用的API。我最开始直接用Flask写接口后来在并发测试时发现Flask的同步模型很容易被慢请求阻塞一个耗时长的请求会把后续所有请求都堵住。换成FastAPI之后异步处理能力明显更强。FastAPI基于Starlette原生支持异步接口配合uvicorn运行对IO密集型的推理服务非常合适。核心的服务代码长这样import time import numpy as np import onnxruntime as ort from fastapi import FastAPI, HTTPException from pydantic import BaseModel from transformers import AutoTokenizer app FastAPI(titleEmotion Recognition API) # 全局加载模型避免每次请求都重复初始化 tokenizer AutoTokenizer.from_pretrained(your_tokenizer_path) sess_options ort.SessionOptions() sess_options.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL sess_options.intra_op_num_threads 4 session ort.InferenceSession(emotion_model_int8.onnx, sess_options, providers[CUDAExecutionProvider]) class TextInput(BaseModel): text: str def predict(text: str) - dict: inputs tokenizer(text, max_length256, truncationTrue, paddingmax_length, return_tensorsnp) feed { input_ids: inputs[input_ids], attention_mask: inputs[attention_mask], token_type_ids: inputs[token_type_ids] } start time.time() logits session.run([logits], feed)[0] elapsed time.time() - start scores logits[0].astype(np.float64) # 假设label 0消极1积极 label int(np.argmax(scores)) positive_prob 1.0 / (1.0 np.exp(-scores[1] scores[0])) return { label: positive if label 1 else negative, confidence: float(positive_prob) if label 1 else float(1 - positive_prob), latency_ms: round(elapsed * 1000, 2) } app.post(/predict) async def predict_endpoint(input_data: TextInput): if not input_data.text.strip(): raise HTTPException(status_code400, detail文本不能为空) try: result predict(input_data.text) except Exception as e: raise HTTPException(status_code500, detailstr(e)) return result app.get(/health) async def health_check(): return {status: ok}几个容易踩的细节第一模型和Tokenizer要放在全局作用域加载不能在请求函数里初始化。我第一次写这个接口的时候把InferenceSession创建写在了predict函数内部结果每个请求都要重新加载模型单次推理时间直接从50ms飙到2秒并发一上来直接把内存拖爆。第二FastAPI的响应结构最好用Pydantic模型定义一下不要直接返回字典。我当时直接返回了numpy类型的数据结果序列化的时候报错——float32不是JSON支持的格式必须转成Python原生的float和int。上面代码里我已经做了astype(np.float64)转换但这只是一个例子生产环境建议把所有numpy类型都显式转一遍。第三ONNX Runtime的provider选择。如果你服务器有NVIDIA GPU一定要显式指定CUDAExecutionProvider。不指定的情况下ONNX Runtime优先使用CPU执行GPU等于白装。我之前测试时跑出来的延迟总在200ms以上后来发现provider没指定改成GPU后延迟降到了30ms。如果GPU初始化失败ONNX Runtime会抛异常这时候要检查CUDA和cuDNN版本也可以用providers参数的备选机制让程序在GPU不可用时自动回退到CPUsession ort.InferenceSession(emotion_model_int8.onnx, sess_options, providers[CUDAExecutionProvider, CPUExecutionProvider])写完服务代码后启动命令也有讲究uvicorn emotion_api:app --host 0.0.0.0 --port 8000 --workers 2--workers 2表示启动两个进程可以利用多核CPU。但注意每个进程都会加载一份模型到显存里所以workers数量要结合显存大小来定。8G显存下一个200MB的模型开4个worker绰绰有余但如果你用的模型本身占2G显存那开2个worker就可能是极限了。3.2 手机端怎么调用电脑或服务器上的部署模型接口写好后紧接着的问题就是客户端的手机App怎么访问到这台服务器上的模型服务这里分两种情况情况一手机和服务器在同一局域网内比如公司内网、同一WiFi环境直接把手机访问地址改成服务器的局域网IP加端口。通过ip addr或ipconfig查一下服务器IP比如192.168.1.100那手机端请求地址就是http://192.168.1.100:8000/predict。这里有个大坑uvicorn启动时如果绑定的是127.0.0.1外部设备是访问不了的必须绑定0.0.0.0。我在Windows上测试时一开始用的--host 127.0.0.1手机怎么都连不上还以为是防火墙问题后来发现就是绑定地址的事。情况二手机和服务器不在同一网络需要跨公网访问很多开发者在本地电脑部署好模型后想让外网手机访问这时候就需要内网穿透工具。我试过的方式是在服务器上跑一个frp客户端把本地8000端口映射到一台有公网IP的中转服务器上手机访问中转服务器的地址就能转发到本地的推理服务。用这种方案要特别注意安全。因为服务一旦暴露到公网任何人都可以调用你的推理接口如果被恶意刷流量轻则服务器卡死重则产生高额的流量费用。所以一定要在接口层做鉴权最简单的方案是加一个Token验证在FastAPI里加一个中间件来处理from fastapi import Header, HTTPException VALID_TOKEN your_secret_token app.middleware(http) async def verify_token(request, call_next): if request.url.path /health: return await call_next(request) token request.headers.get(X-Token) if token ! VALID_TOKEN: raise HTTPException(status_code401, detailInvalid token) return await call_next(request)手机端在请求头里带上X-Token字段即可。Token别用明文写死在前端至少要做一次混淆或者从服务端动态获取。这里多说一句我见过好几个人把公网暴露的模型服务当成“临时测试”不管安全问题结果被薅了几个月流量才发现的这种事一点都不新鲜。4. 高频踩坑实录每个问题都曾让我怀疑人生4.1 本地模型加载慢、响应慢Ollama和vLLM都卡我在测试阶段用的是Ollama来部署一个开源大模型做情感分析兜底方案症状很典型第一次调用模型时等了好几十秒才返回后续调用倒是快一些但一旦隔几分钟不调用再调用又是几十秒的加载时间。这是因为Ollama默认在模型空闲一段时间后会主动释放显存下次调用需要重新从磁盘加载模型到显存。这个问题在hermes agent这类Agent类应用里尤其明显因为Agent会频繁触发模型调用每次都要加载模型整体运行慢得让人崩溃。解决办法是修改Ollama的启动参数设置OLLAMA_KEEP_ALIVE环境变量来延长模型在显存中的存活时间# 在启动Ollama前设置单位是秒14400表示4小时 export OLLAMA_KEEP_ALIVE14400 ollama serve如果你用Systemd管理Ollama服务则修改服务文件加上环境变量[Service] EnvironmentOLLAMA_KEEP_ALIVE14400设置之后模型会常驻显存首次加载后的后续请求延迟会大大降低。代价是显存一直被占用如果同时跑其他模型可能会显存不足需要根据自己的实际使用情况平衡。如果你部署的是超大模型比如70B的量化版Ollama可能直接跑不动这时候可以考虑vLLM。vLLM通过PagedAttention机制大幅优化了显存利用率和吞吐量适合并发请求量大的场景。vLLM启动服务也很简单vllm serve your_model_path --port 8001 --tensor-parallel-size 1 --max-model-len 4096但vLLM对模型格式和硬件有要求比如支持的模型架构有限不是所有模型都能直接加载。在8G显存的小卡上vLLM能跑7B左右的量化模型已经是极限了再大就得靠CPU offload。另一个导致模型响应慢的隐藏因素是上下文长度设置。我遇到过一种情况模型加载了但每次推理都特别慢后来检查发现是num_ctx默认设置过大4096甚至更大导致每次请求都要处理大量padding token。在不影响效果的前提下把上下文长度调小到512或256推理速度会有质的提升。这个参数在Ollama里通过Modelfile设置FROM qwen2.5:7b PARAMETER num_ctx 5124.2 GitHub和HuggingFace下载失败依赖装不上部署过程中最让人崩溃的不是代码问题而是“装个包都装不上”。pip install正常但从GitHub clone代码仓库、从HuggingFace拖模型文件经常下载到一半就超时。GitHub打不开或者访问慢几乎是每个国内开发者都遇到的问题。一个比较实用的方案是使用GitHub的镜像加速地址。比如要clone某个仓库把https://github.com/前缀替换成https://ghproxy.com/https://github.com/或类似的加速服务就能明显提升下载速度。如果你只是需要下载单个release文件用镜像站更省心。不过镜像站时有变动如果失效了就换一个或者直接去Gitee上找同名的镜像仓库。HuggingFace这边前面已经提到用环境变量HF_ENDPOINThttps://hf-mirror.com。但要注意有些源码里会硬编码HuggingFace的地址或者使用huggingface_hub的底层API光设置环境变量不管用。这种情况下可以在代码里手动指定import os os.environ[HF_ENDPOINT] https://hf-mirror.com # 或者直接构造下载地址 from huggingface_hub import hf_hub_download file_path hf_hub_download(repo_idbert-base-chinese, filenamepytorch_model.bin)下载完成后建议把模型文件集中放在一个目录比如/models/然后通过环境变量或者配置文件引用路径不要散落在各个项目目录里。这样后续模型更新、版本回退都方便管理。还有个很容易被忽略的地方因为网络问题下载的模型文件不完整但库不会报错。比如某个分片文件下载了99%就停住了加载模型时才发现缺了某些权重。这种问题很隐蔽检查方式是在模型加载时开启完整性校验或者下载后对比文件MD5。HuggingFace每个文件都提供了SHA256值下载后对比一下最稳妥。4.3 显存溢出、内存爆掉服务直接崩溃情感识别如果用的是中小型BERT模型显存占用还好说8G显存完全够用。但如果你同时加载多个模型或者用了大语言模型方案显存溢出就是家常便饭。我遇到最典型的一种情况服务刚启动时一切正常跑了一百多个请求后突然报CUDA out of memory然后整个服务崩溃掉。原因有两个一是没有对模型输出张量做释放PyTorch的默认缓存机制会把已释放的显存留在缓存池里看起来显存一直是满的二是ONNX Runtime在连续执行推理时部分中间张量缓存没有再分配导致显存碎片化。解决思路分几层第一层按需加载。不要一开始就把所有可能用到的模型全部加载进显存。把推理服务拆成多个独立进程每个进程只负责一个模型需要哪个模型就启动哪个进程。这样单个模型占用的显存是可控的。第二层控制batch size。如果接口要支持批量预测不要一次性把一个很大的batch塞进去。BERT模型显存占用和序列长度、batch size呈线性关系把batch size调小能直接降低显存峰值。我最终在接口层面限制单次请求最多只处理一条文本放弃batch模式换来了稳定性。第三层定期清理缓存。在PyTorch里可以调用torch.cuda.empty_cache()在ONNX Runtime里可以配置session.set_providers时传入arena_extend_strategy: kSameAsRequested来减少显存过度申请sess_options ort.SessionOptions() sess_options.add_session_config_entry(session.intra_op.allow_spinning, 1) # 限制显存扩展策略 sess_options.add_session_config_entry(gpu_mem_limit, str(3 * 1024 * 1024 * 1024)) # 限制为3G provider_options [ { device_id: 0, arena_extend_strategy: kSameAsRequested, } ] session ort.InferenceSession(emotion_model.onnx, sess_options, providers[CUDAExecutionProvider], provider_optionsprovider_options)gpu_mem_limit参数可以限制ONNX Runtime最多使用多少显存避免它把显存全占满导致其他程序无法运行。实测下来把limit设置成3G对模型推理速度影响不大但系统稳定性提升了一个档次。如果服务器上同时跑了好几个Python服务内存也可能不够用。一个常见问题是在FastAPI里使用了全局缓存来加速响应缓存数据越积越多内存持续上涨。这时候要设置缓存上限比如用functools.lru_cache(maxsize128)限制缓存条目数或者用Redis做外部缓存。4.4 中文乱码、响应编码、JSON格式化问题情感识别模型处理的大多是中文文本部署时乱码问题特别常见。表现形式有两种第一种是请求发出去模型输入显示乱码识别结果一塌糊涂第二种是模型返回结果在客户端显示乱码。第一种情况通常是编码不一致导致的。客户端发送请求时用的是GBK编码但服务端默认按UTF-8解析中文就变成了乱码。排查方法是在服务端打印接收到的原始请求体看是不是正常的UTF-8。我建议客户端统一通过HTTP的Content-Type: application/json; charsetutf-8发送请求JSON本身强制UTF-8编码基本能规避这类问题。第二种情况更隐蔽。FastAPI在返回JSON时默认会把非ASCII字符转成Unicode转义序列比如“积极”会变成\u79ef\u6781这在功能上是正常的但如果你在终端里直接打印响应或者在日志里看就是一堆\u开头的乱码。要解决显示问题一方面在FastAPI返回数据时设置ensure_asciiFalse另一方面前端拿到数据后按JSON解析不要直接展示原始字符串。如果你在请求返回结果中遇到了类似{label: positive, confidence: 0.98}这样的数据但不能正常展示中文多半就是编码问题把服务端的响应头和客户端请求头的charset统一成UTF-8就能解决。这里附上一个Python客户端调用的参考代码import requests url http://127.0.0.1:8000/predict headers { Content-Type: application/json; charsetutf-8, X-Token: your_secret_token } data {text: 这款手机的屏幕显示效果很细腻色彩还原度极高} resp requests.post(url, jsondata, headersheaders) print(resp.status_code) print(resp.json())如果你在Windows的终端里运行可能还会遇到终端编码导致的乱码那是终端的问题不是接口的问题。换用Windows Terminal或者IDE自带终端设置UTF-8编码即可。4.5 端口占用、进程僵死服务为什么突然挂了服务部署之后还要面对进程管理问题。最常见的场景你改了代码想重启服务结果端口被之前的进程占着新进程起不来。启动新的uvicorn时报错[Errno 98] Address already in use解决办法是找到占用端口的进程并杀掉lsof -i :8000 kill -9 PID如果不确定PID是什么还可以用fuser -k 8000/tcp一键结束占用端口的进程。但这么操作有个风险有可能误杀其他服务最好还是先lsof看清楚再动手。此外FastAPI服务跑久了可能因为异常请求导致进程僵死表现为“服务明明在运行但请求全部超时”。这种情况建议用进程守护工具比如Linux下的systemdWindows下的NSSM让服务崩溃后能自动重启。我的做法是写一个systemd服务文件[Unit] DescriptionEmotion API Service Afternetwork.target [Service] Userwww-data WorkingDirectory/opt/emotion_service ExecStart/opt/conda/envs/emotion_deploy/bin/uvicorn emotion_api:app --host 0.0.0.0 --port 8000 --workers 2 Restartalways RestartSec3 [Install] WantedBymulti-user.target设置Restartalways后服务异常退出会自动拉起间隔3秒。这比手动重启可靠得多。用systemd管理后还可以用journalctl -u emotion_api -f实时看日志排查问题非常方便。另外一个隐蔽的坑是服务进程没崩溃但停止响应通常是因为某个请求死循环或者占用了线程池所有线程。FastAPI的线程池默认大小有限如果推理任务阻塞时间太长新的请求只能排队表现就是“服务看起来挂了”。解决方案是在业务层面对推理时间做超时控制比如用asyncio.wait_for包裹推理调用超过3秒直接返回错误避免线程池被打满。5. 性能调优与持续运维从能用走向好用5.1 延迟和并发怎么调优压测结果说话服务能跑通只是第一步上线前必须做压力测试。我压测下来发现情感识别模型部署的瓶颈往往不在GPU计算上而是在数据预处理和序列化环节。Tokenizer把文本转成input_ids这个过程比较耗时尤其是max_length设置得很大时padding操作会浪费大量时间。压测我用的是wrk或者locust简单场景用wrk就够wrk -t4 -c100 -d30s -s post.lua http://127.0.0.1:8000/predictpost.lua里定义了POST请求的body内容。压测结果出来后根据延迟分布来做优化。我总结出“三板斧”第一模型预热。服务启动后第一次推理会因为初始化算子、显存分配等原因特别慢有时候达到几百毫秒。解决办法是在服务启动后在后台线程跑一次空推理把模型真正“热”起来。上面的服务代码里可以在startup事件里加一个预热逻辑import asyncio app.on_event(startup) async def warmup(): loop asyncio.get_event_loop() await loop.run_in_executor(None, predict, 预热文本)第二减少预处理开销。Tokenizer的paddingmax_length把所有文本都补到256短文本也会浪费计算。可以改成动态padding只在batch内部padding到当前batch的最大长度。不过如果你的接口是单条预测直接用paddingTrue会更合适。另外中文BERT模型通常使用字级别的Tokenizer不要用词级别的否则字典会巨大预处理也更慢。第三结果缓存。如果业务上有大量重复文本可以加一层缓存。相同或相近的情感判断没必要每次都推理。我在本地用diskcache做了一个简单的缓存文本的MD5作为key结果作为value命中率在20%左右接口平均延迟直接降了20%。当然缓存要注意时效性如果业务对情感判断的实时性要求高慎用。5.2 日志、监控和模型迭代一个都不能少服务上了生产环境没有日志和监控等于裸奔。我吃过一次亏某个模型上线后用户反馈准确率下降但因为没记录请求日志根本不知道用户实际输入了什么无从分析。所以部署的最初就要把日志加上。FastAPI里可以用logging模块记录关键信息包括请求时间、输入文本注意脱敏、模型预测结果、推理耗时、报错堆栈。日志格式建议是JSON格式方便后续接入ELK或Loki等日志系统import json import logging logger logging.getLogger(emotion_api) # 在业务代码中记录 logger.info(json.dumps({ request_time: time.time(), input: input_data.text[:100], label: result[label], confidence: result[confidence], latency_ms: result[latency_ms] }, ensure_asciiFalse))监控方面至少要看三件事一是QPS和平均延迟二是显存和内存占用三是错误率。简单的做法是用prometheus_client把指标暴露出来接上Grafana做可视化。如果你不想一开始就上这么重的监控系统写个定时脚本每5分钟检查一次服务健康状态再配合systemd的自动重启也能应付小规模业务。模型迭代是最后一个容易忽视的环节。模型部署上去不是终点用户的文本分布会变模型的准确率会下降。我建议把线上实际请求都存下来定期抽样标注形成闭环的测试集。维护模型版本号接口返回结果里带上模型版本方便排查问题。新模型上线前在老模型和新模型上跑同一批测试集对比准确率、F1等指标达标了再切换。这里有一张我常用的模型版本信息表放在服务配置文件里模型版本发布时间测试集准确率线上平均延迟回滚标记v1.02024-03-010.92185ms-v1.12024-05-120.93482ms当前v1.2-beta2024-06-010.94288ms灰度中版本管理和回滚策略在模型部署里往往被排在最后但一旦线上出问题你可能需要10分钟内回滚到上一个版本。没有版本记录和备份到时候只能对着一个看不见内容的模型文件干瞪眼。我个人在实际操作中最大的体会是模型部署更像一场“环境工程”而不是“算法工程”大多数坑不在模型本身而在环境、版本、进程、网络这些看起来“没技术含量”的细节上。每次踩坑其实都在加深对整套链路底层机制的理解。最后再分享一个小技巧部署前先在命令行里把整条推理流程跑通再封装成HTTP服务。很多人一上来就写API结果出了Bug分不清是模型的问题、数据处理的问题还是框架的问题。先在Jupyter或者命令行里跑通单条样本的完整推理确认输出正确再封装成FastAPI接口。这样定位问题的范围会小很多能省掉一半的排查时间。