ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零搭建AI工程能力:后端老兵的踩坑与复盘

从零搭建AI工程能力:后端老兵的踩坑与复盘 1. 从零搭建AI工程能力一个后端老兵的踩坑与复盘“ai-engineering-from-scratch”这个标题第一次看到的时候我愣了一下。不是因为陌生恰恰相反是因为太熟悉了——过去两年里我身边至少有七八个后端、前端甚至运维的朋友都在问同一个问题我想转AI工程但不知道从哪下手是不是得先啃完那本花书再刷几百道算法题我的答案从来都是不用。AI工程和AI研究是两条路。研究岗确实需要扎实的数学功底和论文复现能力但工程岗要的是另一套东西——把模型跑起来、把推理服务部署稳、把数据管道搭通、把成本压下来。这套能力完全可以从零开始按工程化的路径一步步搭起来。这个项目标题“ai-engineering-from-scratch”吸引我的地方就在于它强调“from scratch”——不是从调包开始而是从理解每一层在干什么开始。我自己的经历也印证了这一点三年前我从纯后端转做AI工程最开始就是无脑调transformers的pipeline结果线上QPS一上来就崩显存泄漏查了两天才发现是tokenizer的并行处理没做对。从那以后我就明白AI工程的核心竞争力不在于你会不会调API而在于你知不知道API底下发生了什么。这篇文章适合谁看如果你是后端、全栈、数据工程背景想系统性地补齐AI工程能力或者你是刚入行的算法工程师发现学校教的和生产环境完全是两回事再或者你是技术负责人想给团队搭一套从零起步的AI工程学习路径——那这篇内容应该能帮你省下不少试错时间。我会按“整体设计思路→核心细节解析→实操过程→常见问题排查”的顺序把这条路上真正重要的东西拆开讲。2. 整体设计思路为什么我不建议一上来就学框架2.1 先搞清楚AI工程到底在工程什么很多人把AI工程等同于“训练模型”这是个巨大的认知偏差。在实际生产环境里训练只占整个工作量的两到三成剩下七八成全是围绕模型的工程问题数据怎么清洗和版本化、特征怎么存储和读取、推理服务怎么部署和扩缩容、模型怎么监控和回滚、GPU资源怎么调度和计费。我见过太多人花三个月学PyTorch结果入职后发现每天干的事是写Dockerfile、调Kubernetes的resource limit、用Prometheus看推理延迟的P99。不是说PyTorch不重要而是说如果你只学PyTorch你会发现自己离“能干活”还差一大截。所以“from scratch”的第一层含义是先把AI工程的完整链路画出来。我习惯把它分成五层数据层、训练层、推理层、服务层、监控层。每一层都有自己独立的技术栈和核心问题层与层之间的接口才是工程化的关键。2.2 为什么选Python而不是其他语言做主线这个问题我被问过无数次。答案很直接AI生态的底层库几乎全是Python优先你绕不开。但“from scratch”不意味着你要用Python写所有东西。我的建议是Python做胶水和模型层C/Rust做性能敏感的服务层Go做调度和网关层。具体来说数据清洗和特征工程用Python的pandas/polars训练用PyTorch推理服务如果追求极致性能可以用ONNX Runtime的C接口或者Triton Inference Server网关和调度用Go写。这样分工的好处是每一层都用最适合的工具而不是硬把所有东西塞进一个语言里。我自己的项目里推理服务最开始用Flask写QPS到50就顶不住了。后来换成FastAPI ONNX Runtime同样的硬件QPS到了300。再后来把预处理逻辑用Rust重写QPS到了800。这个过程让我深刻体会到AI工程的性能优化往往不在模型本身而在模型前后的那些“脏活累活”。2.3 从零搭建的学习路径应该长什么样基于上面的分层思路我设计了一条四阶段的学习路径每个阶段都有明确的产出物而不是“学完某某课程”这种模糊目标。第一阶段单机推理闭环。目标是在一台机器上从加载模型到提供HTTP接口完整跑通。产出物是一个能接收JSON请求、返回预测结果的API服务。这个阶段要搞懂的东西包括模型格式PyTorch的.pth、ONNX的.onnx、TensorRT的.engine、推理引擎ONNX Runtime、TensorRT、OpenVINO、服务框架FastAPI、Flask、Triton。第二阶段数据管道与特征管理。目标是让模型能持续拿到干净、一致的数据。产出物是一个定时任务能从数据源拉取原始数据、做清洗和特征计算、写入特征存储。这个阶段的核心是理解数据版本化和特征一致性——训练时用的特征和推理时用的特征必须完全一致否则模型效果会断崖式下跌。第三阶段容器化与编排。目标是把推理服务打包成容器用Kubernetes做编排和扩缩容。产出物是一个Helm Chart或者Kustomize配置能一键部署到集群。这个阶段要搞懂的东西包括Docker多阶段构建、GPU资源调度、健康检查、滚动更新。第四阶段监控与可观测性。目标是能实时看到服务的延迟、吞吐、错误率、GPU利用率并且能追踪每个请求的完整链路。产出物是一套Grafana面板加告警规则。这个阶段的核心是理解AI服务特有的监控指标——比如推理延迟的分布、batch size的影响、显存碎片化。这四个阶段走下来大概需要三到六个月取决于你每天能投入多少时间。但走完之后你就具备了独立搭建和维护AI推理服务的能力这比会调几个模型要值钱得多。3. 核心细节解析那些文档里不会写的关键点3.1 模型格式转换为什么ONNX不是万能药ONNX经常被宣传成“一次导出到处运行”的银弹但实际用起来坑不少。我拿一个BERT-base的文本分类模型做过测试PyTorch原生推理、ONNX Runtime、TensorRT三种方式的对比结果如下推理方式延迟ms吞吐QPS显存占用MB转换难度PyTorch原生45221200无ONNX Runtime2835900低TensorRT1280600高看起来TensorRT全面胜出但转换难度那一栏的“高”不是随便写的。TensorRT对动态shape的支持有限如果你的输入长度变化很大要么做padding到固定长度浪费算力要么用optimization profile配置复杂。而且TensorRT的版本和CUDA版本强绑定升级一次环境可能就要重新转换。我的建议是先用ONNX Runtime跑通它的兼容性最好社区支持也最完善。等业务稳定了再针对性地用TensorRT优化那几个最耗时的模型。不要一上来就追求极致性能工程上的第一原则永远是“先跑通再优化”。3.2 批处理策略batch size不是越大越好批处理是提升推理吞吐最直接的手段但batch size的选择是个技术活。理论上batch越大GPU利用率越高吞吐越大。但实际上有三个约束显存上限、延迟要求、请求到达模式。显存上限很好理解batch size翻倍激活值占用的显存也大致翻倍。延迟要求则经常被忽略如果业务要求P99延迟在100ms以内那batch size就不能太大因为攒batch本身需要时间。请求到达模式更微妙如果请求是稀疏的攒一个大batch可能要等很久反而增加了延迟。我常用的策略是动态批处理dynamic batching也就是设置一个时间窗口比如10ms和一个最大batch size比如32窗口内到的请求攒成一个batch窗口结束或者攒满就送GPU。Triton Inference Server原生支持这个功能自己实现也不复杂。注意动态批处理的时间窗口要根据你的延迟预算来定。如果P99要求是100ms模型推理本身要30ms那窗口最多设50ms留20ms给网络和排队。3.3 特征一致性训练和推理之间的隐形杀手这个问题我在三个不同的项目里都遇到过每次都是线上出问题才发现。具体表现是模型离线评估AUC 0.85上线后效果只有0.6。排查半天最后发现是训练时用的特征计算逻辑和推理时的不一致。最常见的坑是时间窗口。训练时计算“过去7天用户点击次数”用的是离线数仓里的数据按天分区。推理时计算同样的特征用的是实时流里的数据按事件时间窗口。两个窗口的边界定义不一样导致同一个用户在训练集和推理时的特征值不同。解决这个问题的标准做法是特征存储feature store比如Feast或者Tecton。核心思想是特征的計算逻辑只写一次训练时从离线存储读历史值推理时从在线存储读最新值但计算逻辑完全一致。如果不想引入这么重的组件至少要做到把特征计算逻辑封装成一个独立的模块训练和推理都调用同一个函数并且写单元测试保证两边输出一致。3.4 GPU利用率为什么你的显卡在摸鱼nvidia-smi显示GPU利用率100%不代表你的模型真的在高效计算。我见过太多情况是GPU在等数据不是在算数据。数据加载、预处理、后处理这些CPU上的操作如果不够快GPU就会频繁空转。诊断这个问题的方法很简单用Nsight Systems或者PyTorch Profiler抓一段trace看GPU kernel之间的间隙。如果间隙很大说明CPU侧是瓶颈。常见的优化手段包括用DALI做GPU加速的数据预处理、用多进程DataLoader、把tokenization放到GPU上做、用CUDA Stream做异步拷贝。我自己的经验是一个典型的NLP推理服务如果不做任何优化GPU利用率可能只有30%到40%。做了数据管道优化之后能到70%以上。剩下的30%是GPU在等网络IO或者在做一些无法并行的操作这部分很难再压榨了。4. 实操过程从零搭一个可用的推理服务4.1 环境准备与依赖管理我强烈建议用conda或者uv做Python环境管理不要用系统自带的pip。CUDA版本、cuDNN版本、PyTorch版本这三者的兼容性是个雷区用conda能省很多事。# 创建环境 conda create -n ai-eng python3.11 conda activate ai-eng # 安装PyTorch根据你的CUDA版本选择 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 # 安装推理相关 pip install onnx onnxruntime-gpu fastapi uvicorn pydantic # 安装监控 pip install prometheus-client这里有个细节onnxruntime-gpu和onnxruntime是两个不同的包装了GPU版本就不需要装CPU版本。而且onnxruntime-gpu的版本要和CUDA版本匹配比如CUDA 12.1对应onnxruntime-gpu 1.17以上。4.2 模型导出与优化以HuggingFace的模型为例导出ONNX的标准流程如下from transformers import AutoTokenizer, AutoModelForSequenceClassification import torch model_name distilbert-base-uncased-finetuned-sst-2-english tokenizer AutoTokenizer.from_pretrained(model_name) model AutoModelForSequenceClassification.from_pretrained(model_name) model.eval() # 构造示例输入 dummy_input tokenizer(This is a sample, return_tensorspt) # 导出ONNX torch.onnx.export( model, (dummy_input[input_ids], dummy_input[attention_mask]), model.onnx, input_names[input_ids, attention_mask], output_names[logits], dynamic_axes{ input_ids: {0: batch, 1: sequence}, attention_mask: {0: batch, 1: sequence}, logits: {0: batch} }, opset_version14 )导出之后用ONNX Runtime的optimize_model做图优化import onnxruntime as ort from onnxruntime.transformers import optimizer optimized_model optimizer.optimize_model( model.onnx, model_typebert, num_heads12, hidden_size768 ) optimized_model.save_model_to_file(model_optimized.onnx)这一步能带来大概20%到30%的延迟下降主要是做了算子融合和常量折叠。4.3 推理服务实现用FastAPI写一个最小可用的推理服务from fastapi import FastAPI from pydantic import BaseModel import onnxruntime as ort import numpy as np from transformers import AutoTokenizer app FastAPI() # 全局加载模型和tokenizer session ort.InferenceSession( model_optimized.onnx, providers[CUDAExecutionProvider, CPUExecutionProvider] ) tokenizer AutoTokenizer.from_pretrained(distilbert-base-uncased-finetuned-sst-2-english) class Request(BaseModel): text: str class Response(BaseModel): label: str score: float app.post(/predict, response_modelResponse) async def predict(req: Request): inputs tokenizer( req.text, return_tensorsnp, paddingTrue, truncationTrue, max_length128 ) outputs session.run( None, { input_ids: inputs[input_ids].astype(np.int64), attention_mask: inputs[attention_mask].astype(np.int64) } ) logits outputs[0] probs np.exp(logits) / np.exp(logits).sum(axis-1, keepdimsTrue) label_id int(probs.argmax()) return Response( labelmodel.config.id2label[label_id], scorefloat(probs[0][label_id]) )启动命令uvicorn main:app --host 0.0.0.0 --port 8000 --workers 1这里有个关键点workers不要设成大于1。因为每个worker都会加载一份模型到GPU显存会成倍增加。GPU推理服务的并发应该靠批处理来实现而不是多进程。4.4 容器化与部署Dockerfile用多阶段构建把模型转换和运行时分开# 阶段一模型转换 FROM nvidia/cuda:12.1.0-devel-ubuntu22.04 AS builder RUN apt-get update apt-get install -y python3.11 python3-pip COPY requirements.txt . RUN pip install -r requirements.txt COPY export_model.py . RUN python export_model.py # 阶段二运行时 FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04 RUN apt-get update apt-get install -y python3.11 python3-pip COPY requirements-runtime.txt . RUN pip install -r requirements-runtime.txt COPY --frombuilder /app/model_optimized.onnx /app/model.onnx COPY main.py /app/ WORKDIR /app CMD [uvicorn, main:app, --host, 0.0.0.0, --port, 8000]运行时镜像用runtime而不是devel能省好几个GB。模型文件从builder阶段拷贝过来保证运行时镜像里没有训练相关的依赖。Kubernetes部署的resource limit设置resources: limits: nvidia.com/gpu: 1 memory: 8Gi cpu: 4 requests: nvidia.com/gpu: 1 memory: 6Gi cpu: 2GPU的request和limit必须相等这是Kubernetes的规则。memory的request可以比limit小给调度器留一些余量。5. 常见问题与排查技巧实录5.1 推理延迟突然飙升的排查顺序线上服务延迟飙升按以下顺序排查能覆盖90%的情况看GPU利用率。如果利用率很低但延迟高说明瓶颈在CPU侧或者IO侧。看CPU利用率。如果某个核跑满可能是Python的GIL问题考虑把预处理逻辑用C扩展或者Rust重写。看显存占用。如果显存接近上限GPU会开始做显存交换延迟会指数级上升。看请求的输入长度分布。如果突然来了很多长文本padding到max_length会浪费大量算力。看是否有其他进程在抢GPU。用nvidia-smi看进程列表确认没有训练任务在跑。我遇到过一次诡异的情况延迟从20ms涨到200ms查了一圈发现是某个同事在同一个节点上跑了一个Jupyter Notebook那个Notebook加载了模型但没做推理只是占着显存。GPU显存不够推理服务被迫用CPU跑延迟自然就上去了。5.2 显存泄漏的定位方法PyTorch的显存泄漏通常来自两个地方一是计算图没有释放二是缓存没有清理。定位方法是在每次推理前后打印torch.cuda.memory_allocated()和torch.cuda.memory_reserved()。import torch def log_memory(tag): allocated torch.cuda.memory_allocated() / 1024**2 reserved torch.cuda.memory_reserved() / 1024**2 print(f[{tag}] allocated: {allocated:.1f}MB, reserved: {reserved:.1f}MB)如果allocated持续增长说明有张量没有被释放。常见原因是把推理放在了torch.no_grad()外面或者把输出张量存到了一个全局列表里。如果allocated稳定但reserved持续增长说明是缓存碎片化可以用torch.cuda.empty_cache()缓解但根本解法是重启服务。提示ONNX Runtime的显存管理比PyTorch好很多如果PyTorch的显存问题实在搞不定可以考虑切到ONNX Runtime。5.3 模型版本管理与回滚模型文件动辄几百MB用Git LFS管理会很痛苦。我的做法是用对象存储S3或者MinIO存模型文件用数据库存元数据版本号、训练时间、评估指标、对应的代码commit。部署的时候服务启动时从对象存储拉取指定版本的模型。回滚的时候只需要改数据库里的版本号然后触发一次滚动更新。整个过程可以在两分钟内完成。这个方案的关键是模型文件不可变每次训练产生一个新版本永远不覆盖旧版本。存储成本很低但换来的是完整的可追溯性。5.4 常见问题速查表现象可能原因排查方法解决方案延迟高但GPU利用率低CPU预处理瓶颈Profiler抓trace用DALI或Rust加速预处理显存OOMbatch size过大打印显存占用减小batch size或做梯度累积吞吐上不去没有批处理看请求日志开启动态批处理模型效果差特征不一致对比训练和推理特征值统一特征计算逻辑服务启动慢模型加载耗时计时加载过程用内存映射或预热多卡利用率不均数据并行不均衡看每张卡的利用率用NVIDIA MPS或Triton6. 一些个人体会和后续扩展方向这套从零搭建的路径我自己走过一遍也带过两个新人走过。最大的感受是AI工程的难点不在AI在工程。模型本身有开源社区兜底但怎么把它变成一个稳定、高效、可维护的服务这个没有标准答案只能靠一个个项目踩出来。如果要把这套东西继续扩展我建议往两个方向走。一个是推理优化深入学TensorRT、Triton、CUDA编程把单卡性能压榨到极致。另一个是平台化学Kubernetes Operator、KubeFlow、MLflow把整个流程自动化。这两个方向没有优劣之分取决于你的业务场景更需要哪个。最后分享一个小技巧每次上线新模型之前用Locust或者wrk做一次压力测试把P50、P95、P99延迟和吞吐都记录下来。这些数据不仅能帮你发现性能问题还能在出故障的时候快速判断是不是模型本身的问题。我现在的习惯是没有压测报告的模型不允许上线。这个规矩帮我挡掉了至少三次潜在的生产事故。
返回列表