1. 这个项目到底在解决什么问题第一次看到 ai-engineering-from-scratch 这个标题我脑子里蹦出来的第一个念头是又是一个教人调包的教程但仔细琢磨了一下 from scratch 这几个字我意识到它想做的事情可能完全不一样。市面上讲 AI 工程的内容绝大多数都是从pip install transformers开始然后告诉你model.fit()就完事了。可真到了生产环境你会发现真正难的地方根本不在于会不会调 API而在于你能不能搞清楚一次推理请求从进入到返回中间到底经历了什么。这个项目的核心定位我理解是从零开始构建 AI 工程能力而不是从零开始训练一个大模型。这两件事有本质区别。前者关注的是一个 AI 系统从数据准备、模型选型、推理服务搭建、性能优化到上线监控的完整链路你需要掌握哪些底层能力。后者关注的是如何用数学和代码实现一个 Transformer。对于绝大多数工程师来说前者才是日常工作中真正需要的东西。我之所以对这个方向特别有感触是因为我自己就踩过这个坑。早些年做推荐系统的时候我觉得自己模型调得还不错AUC 也挺好看结果一上线就崩了——延迟高得离谱QPS 上不去显存动不动就爆。后来才发现问题根本不在模型本身而在于我完全不懂推理引擎是怎么工作的不知道 KV Cache 是怎么回事不知道 batch size 和延迟之间的权衡曲线长什么样。这些东西没有一个pip install能帮你解决。所以这个项目适合谁呢我认为有三类人特别值得关注第一类是有一定编程基础但没怎么接触过 AI 系统部署的开发者你想知道一个模型从 notebook 到线上服务中间要过几道坎第二类是做过后端或数据工程想转方向到 AI 基础设施的工程师你有系统思维但缺 AI 领域的特定知识第三类是在小团队里什么都得自己干的全栈工程师老板说把这个模型部署一下你得知道从哪里下手。这个项目要解决的核心问题是填补会调包和能扛住生产流量之间的巨大鸿沟。它不会教你写 attention 的数学公式但会告诉你为什么你的推理服务在并发上来之后会 OOM它不会教你反向传播的推导但会告诉你量化到 INT8 之后精度掉了两个点该怎么排查。2. 从零构建 AI 工程能力的核心思路拆解2.1 为什么从零不等于重新造轮子很多人对 from scratch 有误解觉得什么都得自己写。不是这样的。从零构建 AI 工程能力的核心思路是你要理解每一层的原理但不必自己实现每一层。这就像学操作系统你不需要自己写一个 Linux 内核但你需要知道进程调度、内存管理、文件系统大概是怎么回事否则遇到性能问题你连从哪里开始查都不知道。具体到 AI 工程这个领域我认为需要从零理解的核心层次是这样的最底层是硬件和驱动层你得知道 GPU 的显存是怎么分配的为什么有时候nvidia-smi显示显存没满但程序还是 OOM往上是推理引擎层你得知道 ONNX Runtime、TensorRT、vLLM 这些引擎各自适合什么场景它们的调度策略有什么不同再往上是服务层你得知道怎么设计一个能扛住并发的推理 API怎么做动态 batching怎么做超时和降级最上面才是应用层也就是大多数人日常接触的那部分。这个分层思路的好处是它让你在学习的时候有一个清晰的路线图。你不会一上来就被各种框架和工具搞晕而是知道每个工具在整体架构中处于什么位置解决的是什么问题。2.2 方案选型背后的取舍逻辑在 AI 工程实践中几乎每一个技术决策都是在多个维度之间做取舍。我拿几个最常见的场景来举例说明。推理引擎的选择。你有三个主流选项ONNX Runtime、TensorRT 和原生 PyTorch。ONNX Runtime 的优势是跨平台、部署简单、社区活跃适合快速验证和中小规模部署TensorRT 的优势是极致性能在 NVIDIA 显卡上能比原生 PyTorch 快好几倍但缺点是绑定硬件、调试困难、对模型结构有要求原生 PyTorch 的优势是灵活、调试方便适合研究和快速迭代但性能最差。怎么选我的经验是如果你的 QPS 需求在 100 以下ONNX Runtime 足够了如果到了 1000 以上而且用的是 NVIDIA 显卡那就得上 TensorRT如果还在实验阶段模型结构天天变那就老老实实用 PyTorch。批处理策略的选择。静态 batching 实现简单但延迟不可控因为你要等凑够一个 batch 才能处理动态 batching 延迟可控但实现复杂需要处理超时和优先级。我实测下来的经验是如果你的服务对延迟敏感比如实时对话用动态 batching超时设 10-20ms如果对吞吐量更敏感比如离线批量处理用静态 batchingbatch size 设大一点。量化方案的选择。FP32 精度最高但最慢FP16 精度损失很小但速度提升明显INT8 速度最快但精度损失需要评估。我的建议是先上 FP16这是性价比最高的选择大多数场景下精度损失可以忽略不计如果还不够快再考虑 INT8但一定要做充分的精度评估特别是对于分类任务INT8 量化后类别不平衡的问题可能会被放大。2.3 为什么这个思路能避免学了就忘我见过太多人学 AI 工程的方式是看一篇博客跟着敲一遍代码跑通了然后就忘了。为什么因为没有建立知识之间的关联。你学 vLLM 的时候只学了 vLLM 的 API不知道它底层的 PagedAttention 是怎么回事不知道它和 Continuous Batching 的关系那换个框架你又得从头学。从零构建的思路之所以有效是因为它强迫你建立因果链条。你知道因为 GPU 显存有限所以需要量化因为量化会损失精度所以需要校准数据集因为校准数据集的质量直接影响量化效果所以需要精心准备。这一整条链路是逻辑自洽的你理解了其中一环就能推导出下一环。这样学到的知识是活的不是死的。3. 核心细节解析与实操要点3.1 推理服务的性能瓶颈到底在哪里很多人做 AI 工程第一步就搞错了方向。他们花大量时间优化模型结构却不知道瓶颈根本不在模型计算上。我做过一个实测一个 BERT-base 的模型在 V100 上做一次推理纯计算时间大约是 5ms但端到端的延迟可能是 50ms。那多出来的 45ms 花在哪里了我列了一个典型的推理请求时间分布表你可以对照看看自己的服务是不是也这样阶段典型耗时优化空间网络传输1-5ms压缩请求体、使用 HTTP/2请求解析与预处理5-20ms使用更快的 tokenizer、批处理预处理模型推理5-50ms量化、算子融合、使用推理引擎后处理2-10ms简化后处理逻辑、异步处理响应序列化与返回1-5ms使用更高效的序列化格式从表里可以看出来预处理和后处理加起来可能比模型推理本身还耗时。这就是为什么我经常说先别急着换推理引擎先把你的预处理逻辑优化一遍。我见过一个项目光是 tokenizer 就花了 30ms换成一个 fast tokenizer 之后直接降到 3ms比换推理引擎的效果还明显。3.2 显存管理AI 工程中最容易翻车的地方显存管理是 AI 工程中最容易出问题、也最难排查的环节。我总结了几个常见的显存陷阱和对应的解决方案。陷阱一碎片化导致的 OOM。你可能会遇到这种情况nvidia-smi显示显存还有 2GB 空闲但程序就是报 OOM。这通常是因为显存碎片化——空闲的显存不是连续的无法分配一个大块。解决方案是设置PYTORCH_CUDA_ALLOC_CONFexpandable_segments:True让 PyTorch 使用可扩展的显存段减少碎片。陷阱二KV Cache 占用过大。在做大模型推理时KV Cache 会随着序列长度线性增长。一个 7B 的模型如果序列长度是 2048batch size 是 8KV Cache 可能就要占用好几 GB 的显存。解决方案是使用 PagedAttentionvLLM 的核心技术它把 KV Cache 分成固定大小的块来管理大大减少了浪费。陷阱三多进程显存不释放。如果你用 DataLoader 的num_workers 0每个 worker 进程都会占用一份显存。解决方案是设置pin_memoryFalse或者在 worker 中只做 CPU 操作把 GPU 操作放到主进程。注意显存问题一定要在开发阶段就暴露出来不要等到上线才发现。我的做法是在本地用一个小显存的显卡比如 8GB做开发这样任何显存问题都会立刻暴露。3.3 动态 Batching 的实现细节与参数调优动态 Batching 是提升推理吞吐量最有效的手段之一但实现起来有很多细节需要注意。我以自己实现过的一个动态 Batching 服务为例讲讲关键点。核心思路是维护一个请求队列当队列长度达到max_batch_size或者等待时间超过max_wait_time时就取出当前队列中的所有请求组成一个 batch 进行推理。听起来简单但实际实现时有几个坑。第一个坑是超时时间的设置。设得太短batch 凑不大吞吐量上不去设得太长延迟增加用户体验变差。我的经验值是对于实时对话场景max_wait_time设在 10-20ms对于离线处理场景可以设在 100ms 甚至更长。第二个坑是batch 内请求长度不一致。如果 batch 里有一个特别长的序列整个 batch 的推理时间都会被拉长。解决方案是使用长度感知的调度策略把长度相近的请求放在同一个 batch 里。vLLM 的 Continuous Batching 就是做这个事情的。第三个坑是错误处理。如果 batch 中某个请求处理失败了怎么保证其他请求不受影响我的做法是在推理前对每个请求做校验把明显有问题的请求提前剔除推理时如果发生异常逐个重试。4. 实操过程与核心环节实现4.1 从零搭建一个可用的推理服务我以搭建一个文本分类推理服务为例完整走一遍从零开始的流程。这个例子虽然简单但涵盖了 AI 工程的核心环节你可以把其中的思路迁移到更复杂的场景。第一步环境准备与依赖管理。我强烈建议使用 Docker 来管理环境因为 AI 工程的依赖关系非常复杂CUDA 版本、PyTorch 版本、推理引擎版本之间都有兼容性要求。我的 Dockerfile 大致是这样的FROM nvidia/cuda:12.1-runtime-ubuntu22.04 RUN apt-get update apt-get install -y python3.10 python3-pip RUN pip install torch2.1.0 --index-url https://download.pytorch.org/whl/cu121 RUN pip install onnxruntime-gpu1.16.0 transformers4.35.0 fastapi0.104.0 uvicorn0.24.0这里有几个细节使用runtime而不是devel镜像因为部署时不需要编译工具可以减小镜像体积固定所有依赖的版本号避免因为版本更新导致的行为变化使用onnxruntime-gpu而不是onnxruntime前者支持 GPU 加速。第二步模型导出与优化。把 PyTorch 模型导出为 ONNX 格式然后做算子融合和量化import torch from transformers import AutoModelForSequenceClassification, AutoTokenizer model AutoModelForSequenceClassification.from_pretrained(your-model) tokenizer AutoTokenizer.from_pretrained(your-model) model.eval() dummy_input tokenizer(example text, return_tensorspt, paddingmax_length, max_length128) 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_size, 1: sequence_length}, attention_mask: {0: batch_size, 1: sequence_length}, logits: {0: batch_size} }, opset_version14 )导出时设置dynamic_axes很关键这样模型才能支持动态的 batch size 和序列长度。opset_version建议用 14 或更高因为低版本对某些算子的支持不好。第三步推理服务实现。用 FastAPI 搭建服务核心是动态 Batching 的实现import asyncio import numpy as np import onnxruntime as ort from fastapi import FastAPI from pydantic import BaseModel app FastAPI() session ort.InferenceSession(model.onnx, providers[CUDAExecutionProvider]) class Request(BaseModel): text: str request_queue [] queue_lock asyncio.Lock() async def batch_processor(): while True: await asyncio.sleep(0.01) # 10ms 等待窗口 async with queue_lock: if not request_queue: continue batch request_queue[:32] # 最多 32 个 request_queue.clear() # 批量推理 inputs tokenizer([r[text] for r in batch], return_tensorsnp, paddingTrue) outputs session.run(None, dict(inputs)) for i, r in enumerate(batch): r[future].set_result(outputs[0][i].tolist())这个实现虽然简化了很多但核心逻辑是完整的维护一个请求队列定期取出 batch 进行推理然后通过 Future 把结果返回给对应的请求。4.2 性能测试与调优的完整流程服务搭起来之后怎么知道它的性能到底行不行我一般会做三个层次的测试。第一层单请求延迟测试。用curl或者 Python 脚本发单个请求测量端到端延迟。这个数字告诉你服务在空载情况下的响应速度。我一般会测 100 次取 P50、P95 和 P99。第二层并发压力测试。用wrk或者locust模拟并发请求逐步增加并发数观察延迟和吞吐量的变化。你会看到一个典型的曲线并发数增加吞吐量先上升后趋于平缓延迟则是指数上升。那个拐点就是你的服务的最佳工作点。第三层长时间稳定性测试。用固定的并发数跑几个小时观察显存占用、延迟、错误率的变化。这一步主要是发现内存泄漏和显存碎片问题。我整理了一个性能测试的检查清单你可以直接拿去用测试项工具关注指标合格标准单请求延迟curl/requestsP50/P95/P99P99 100ms并发吞吐wrk/locustQPS根据业务需求显存占用nvidia-smi峰值显存 显卡显存的 80%长时间稳定性自定义脚本延迟漂移4小时内漂移 10%错误率服务日志5xx 比例 0.1%4.3 监控与告警的搭建服务上线之后没有监控就等于裸奔。我一般会监控这几个核心指标请求量QPS、延迟分布P50/P95/P99、错误率、显存占用、GPU 利用率。这些指标可以用 Prometheus Grafana 来采集和展示。具体实现上我会在推理服务中埋点记录每个请求的处理时间、batch size、是否出错等信息然后通过 Prometheus 的 Python client 暴露出来。Grafana 面板上我会设置几个关键的告警规则P99 延迟超过 200ms 持续 5 分钟、错误率超过 1% 持续 1 分钟、显存占用超过 90% 持续 1 分钟。提示告警阈值不要设得太敏感否则你会被频繁的告警搞得麻木。我的经验是先跑一周观察指标的波动范围然后取正常波动范围的上限作为告警阈值。5. 常见问题与排查技巧实录5.1 推理结果不一致的排查思路这个问题我遇到过好几次表现是同一个输入有时候返回结果 A有时候返回结果 B。排查思路一般是这样的。首先检查是否有随机性。模型本身可能有 dropout 层没有关掉或者用了随机采样策略。解决方案是在推理前调用model.eval()并且设置torch.no_grad()。其次检查数值精度问题。FP16 和 FP32 的结果可能有细微差异如果模型对数值敏感这个差异可能被放大。解决方案是统一使用 FP32 做推理或者做精度对齐测试。最后检查并发问题。如果多个请求共享了同一个 session 或者 buffer可能会出现数据竞争。解决方案是确保每个请求有独立的输入输出 buffer或者使用线程安全的推理引擎。5.2 显存泄漏的定位方法显存泄漏是最难排查的问题之一因为它的表现是渐进的可能跑几个小时才出问题。我的排查方法是这样的。第一步用torch.cuda.memory_summary()打印显存分配详情看看是哪个部分在增长。如果是 PyTorch 的缓存内存在增长那可能是没有及时释放中间变量如果是 CUDA 上下文内存在增长那可能是创建了太多的 session 或者 stream。第二步用tracemalloc或者objgraph追踪 Python 对象的增长看看是不是有对象没有被回收。常见的原因是循环引用比如在闭包中引用了外部对象。第三步如果以上都排查不出来那就用二分法注释掉一半的代码跑一段时间看显存是否还在增长逐步缩小范围。5.3 常见问题速查表我把 AI 工程实践中常见的问题和解决方案整理成了一个速查表方便你遇到问题时快速定位问题现象可能原因排查方法解决方案服务启动就 OOM模型太大/显存碎片检查模型大小和显存占用量化模型/设置 expandable_segments延迟忽高忽低动态 batching 参数不合理观察 batch size 分布调整 max_wait_time 和 max_batch_size吞吐量上不去预处理是瓶颈profile 各阶段耗时优化 tokenizer/使用异步预处理精度下降明显量化过度对比量化前后输出使用 FP16 或做量化感知训练服务跑一段时间就崩显存泄漏监控显存变化趋势定位泄漏点/定期重启服务GPU 利用率低数据加载是瓶颈检查 DataLoader 配置增加 num_workers/使用预取5.4 几个我踩过的坑和对应的经验坑一盲目追求低延迟。我曾经为了把延迟从 50ms 降到 30ms花了两周时间做各种优化结果上线后发现用户根本感知不到这个差异。后来我学乖了先搞清楚业务对延迟的真实要求再决定投入多少精力优化。坑二忽略冷启动问题。推理服务第一次处理请求时需要加载模型、初始化 CUDA 上下文这个过程可能需要几十秒。如果服务是弹性伸缩的新实例启动后立刻接收流量就会导致大量超时。解决方案是配置就绪探针确保服务完全初始化后才接收流量。坑三没有做降级方案。有一次推理服务因为显存问题挂了整个系统都不可用。后来我加了一个降级方案如果推理服务不可用就返回一个默认结果或者走规则引擎。虽然效果差一些但至少系统不会完全挂掉。坑四低估了数据预处理的重要性。我见过一个项目模型推理只花了 10ms但预处理花了 100ms因为他们在预处理中做了大量的字符串操作和正则匹配。后来把预处理逻辑用 C 重写延迟直接降了一个数量级。这些经验告诉我AI 工程的核心不是把模型调得多好而是把整个系统的每一个环节都做到位。模型只是其中一环而且往往不是最耗时的那一环。6. 从零构建的知识体系该如何持续演进AI 工程这个领域变化非常快新的推理引擎、新的优化技术、新的硬件层出不穷。但底层的东西变化很慢显存管理的基本原理、批处理的调度逻辑、性能优化的方法论这些是相对稳定的。我的建议是把 70% 的精力花在理解底层原理上30% 的精力用来跟进新工具和新框架。这样无论技术怎么变你都能快速适应。具体到学习路径上我建议按照硬件层→推理引擎层→服务层→应用层的顺序来构建知识体系。每学一层都要动手写代码验证不要只看文档。比如学推理引擎的时候自己动手把一个模型导出为 ONNX 和 TensorRT对比两者的性能和精度差异学服务层的时候自己实现一个简单的动态 Batching 服务感受一下参数调优的过程。我在实际工作中发现那些真正能把 AI 系统做稳的人往往不是最懂模型的人而是最懂系统的人。他们知道瓶颈可能出现在哪里知道怎么用最小的代价解决最大的问题。这种能力只能通过一个个项目实战积累出来没有捷径。