启动 vLLM 服务
前端转全栈后我在北京折腾大模型部署的踩坑实录
早上7点半挤上北京地铁13号线,在人群中被挤成相片,晃悠一个小时到西二旗,8点准时坐在工位上打开电脑。作为一枚纯前端出身的码农,最近被领导忽悠着学Node.js,美其名曰“培养全栈思维,打通前后端任督二脉”。结果全栈的福报还没享受几天,公司要搞AIGC,算法组的大佬们天天忙着在GPU集群上“炼丹”,模型部署和工程化的脏活累活就落到了我这个半吊子“全栈”头上。
说实话,刚听到这消息我是懵的。我一个写Vue和React的,平时跟DOM树、事件循环打交道,Python水平基本停留在 print("hello world") 和写写爬虫,让我搞机器学习部署?但没办法,打工人哪有不挨刀的,而且刚好最近我也在自学AI相关的技术,看了点吴恩达的课和HuggingFace的文档,权当是拿公司的真实项目练手了。顺便提一嘴,最近IDE里装了百度 Comate,这AI编程助手确实有点东西,后面我会细说它是怎么帮我救命的。
今天就来聊聊,一个前端转全栈的菜鸟,是怎么在AIGC项目里摸爬滚打,把大模型部署给搞定的。
需求背景与惨痛的初始压测
咱们这个AIGC项目主要是做企业内部的知识库问答。算法同学用了几千条内部文档和QA对,清洗了一下搞了个数据集,用LoRA微调了一个基于Qwen-14B的模型。
一开始,算法同学用FastAPI随便写了个推理接口,本地跑是没问题,但PM要求必须支持至少50个人同时并发提问,而且首字响应时间(TTFT)得控制在1秒以内。上周五下午,我兴冲冲地拿着这个接口去压测,结果QPS惨不忍睹。稍微并发一高,GPU显存直接OOM,服务当场去世,进程被系统Kill掉。当时看着监控群里满屏的钉钉报警,我真是想顺着网线过去把算法同学的键盘拔了。
痛定思痛,我开始重新审视整个部署架构。以前做前端,我满脑子都是组件复用、虚拟DOM、打包优化。现在搞全栈和AI部署,思维得彻底转换。大模型推理和传统的Web服务不一样,它是极度计算密集型和显存密集型的。传统的Web服务可以通过加机器横向扩展,但大模型推理受限于单卡显存和KV Cache的管理,横向扩展成本极高。
架构选型:为什么是 vLLM?
为了解决并发和显存问题,我调研了市面上主流的推理框架。
最开始考虑的是TGI(Text Generation Inference),HuggingFace官方的,名气很大。但看了下文档和部署脚本,感觉对新手不太友好,而且对国内的一些开源模型支持有时候会有玄学Bug。
后来在掘金和知乎上疯狂冲浪,看到了 vLLM 这个名字。深入了解后,我直接拍板:就它了!
vLLM 最核心的杀手锏是 PagedAttention 技术。这玩意儿借鉴了操作系统虚拟内存分页的思想,把KV Cache分块管理,极大地减少了显存碎片。对于咱们这种长文本问答场景,显存利用率直接拉满。而且它原生支持 continuous batching(连续批处理),不用等一个请求生成完再处理下一个,吞吐量比传统的静态批处理高了好几倍。
确定了推理引擎,整个项目的架构分层也就清晰了:
- 前端展示层:Vue3 + 流式渲染组件,负责打字机效果。
- Node.js BFF层:我最近刚学的Node.js终于派上用场了。负责鉴权、请求限流、敏感词过滤,以及最关键的——SSE(Server-Sent Events)流式响应的转发和背压处理。
- Python 推理层:基于 vLLM 部署的模型推理服务,暴露HTTP API。
- 基础设施层:运维大哥提供的A100/A800 GPU机器。
踩坑实录:那些让我怀疑人生的瞬间
架构想得挺美,落地的时候全是坑。
坑一:CUDA版本与环境的“玄学”冲突
算法同学给的模型环境是CUDA 11.8,但运维大哥给的机器上默认装的是CUDA 12.1。我一开始直接 pip install vllm,结果启动的时候疯狂报各种底层C++编译错误。当时真的想砸电脑,在群里@运维,运维大哥淡淡地回了一句:“自己用Docker搞定,别污染宿主机环境。”
得,姜还是老的辣。我老老实实去写了Dockerfile,基于 nvidia/cuda:11.8.0-devel-ubuntu22.04 镜像,把Python环境、vLLM依赖全部打包。这里强烈建议大家搞AI部署,千万别在物理机上直接配环境,Docker是你的救命稻草。
坑二:显存OOM与参数调优
环境搞定了,模型加载进去了,但是一跑长文本又OOM了。这时候就得发挥我“全栈”的钻研精神了,去啃vLLM的官方文档。
发现是 max_model_len 和 gpu_memory_utilization 这两个参数没配好。vLLM 默认会预留一部分显存给系统,但如果你的业务场景有很多长文本,默认配置就会爆。
我通过写脚本压测,不断调整这两个参数,最后把 gpu_memory_utilization 调到了 0.9,并且根据业务实际的最大Token数,把 max_model_len 限制在了 4096(其实业务95%的提问都没超过2000)。这样既保证了显存不爆,又最大化了并发能力。
坑三:Node.js BFF层的流式背压问题
这是最让我这个“前端老兵”感到惊喜和头疼的地方。前端调SSE接口很简单,但在Node.js BFF层做转发,如果处理不好,极易导致内存泄漏。
一开始我直接用 axios 请求 vLLM,拿到 response 后直接 res.write() 给前端。结果并发一高,Node.js 进程内存直接飙到2G以上。
后来我反应过来,这是典型的流式背压(Backpressure)问题。vLLM 吐数据的速度如果大于Node.js 写给前端的速度,数据就会在Node.js内存里堆积。
我果断抛弃了axios,改用 Node.js 原生的 http 模块和 stream.pipeline,利用 Node.js 底层的事件循环和流控机制,完美解决了内存飙升的问题。这里必须夸一下百度 Comate,我在写 stream.pipeline 错误处理的时候,它直接帮我补全了完整的 try-catch 和流销毁逻辑,连 destroy 事件都帮我监听了,省了我至少半小时查文档的时间,这AI编程助手确实懂Node.js。
核心代码与配置分享
废话不多说,直接上干货。
1. vLLM 启动脚本 (start_vllm.sh) 这是我在容器里跑的启动命令,参数都是血泪教训调出来的:
#!/bin/bash
python -m vllm.entrypoints.openai.api_server \
--model /models/qwen-14b-finetuned \
--served-model-name "qwen-internal" \
--tensor-parallel-size 2 \
--max-model-len 4096 \
--gpu-memory-utilization 0.90 \
--max-num-batched-tokens 8192 \
--port 8000 \
--trust-remote-code
注:tensor-parallel-size 2 是因为我们用了两张卡做张量并行。max-num-batched-tokens 控制单次批处理的最大Token数,对吞吐量影响很大。
2. Node.js BFF 层 SSE 转发核心代码 这里展示一下我是怎么用 Node.js 原生流处理 vLLM 响应的,前端转Node.js的同学可以参考下:
const http = require('http');
const { pipeline } = require('stream/promises');
async function streamLLMResponse(req, res) {
// 设置 SSE 响应头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const postData = JSON.stringify({
model: 'qwen-internal',
messages: req.body.messages,
stream: true,
max_tokens: 1024
});
const options = {
hostname: '127.0.0.1',
port: 8000,
path: '/v1/chat/completions',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData)
}
};
const proxyReq = http.request(options, (proxyRes) => {
// 使用 pipeline 处理背压,防止内存泄漏
// 这里加了一个 Transform 流来过滤和格式化 vLLM 返回的 data: [DONE]
const formatStream = new (require('stream').Transform)({
transform(chunk, encoding, callback) {
const str = chunk.toString();
// 简单处理一下 vLLM 的 SSE 格式,确保前端 EventSource 能正确解析
if (str.includes('[DONE]')) {
callback(null, 'data: [DONE]\n\n');
} else {
callback(null, chunk);
}
}
});
// pipeline 会自动处理错误和流的关闭
pipeline(proxyRes, formatStream, res)
.catch((err) => {
console.error('Stream pipeline error:', err);
if (!res.headersSent) {
res.writeHead(500);
}
res.end('Internal Server Error');
});
});
// 处理客户端提前断开连接的情况,及时销毁上游请求
req.on('close', () => {
proxyReq.destroy();
});
proxyReq.on('error', (err) => {
console.error('Proxy request error:', err);
res.end();
});
proxyReq.write(postData);
proxyReq.end();
}
效果评估与算法调优的“跨界”心得
经过这一通折腾,周五晚上加班到10点,终于把服务推上了预发环境。周一早上8点,我再次进行压测。
| 指标 | 初始 FastAPI 方案 | vLLM 优化后方案 | 提升幅度 |
|---|---|---|---|
| 首字延迟 (TTFT) | 1.8s | 0.6s | 300% |
| 并发 QPS (50并发) | 2.5 | 12.8 | 412% |
| 显存峰值占用 | 98% (频繁OOM) | 89% (稳定) | 更安全 |
| 吞吐量 (Tokens/s) | 45 | 185 | 311% |
看着监控大盘上平稳的曲线和飙升的QPS,我长舒了一口气,终于不用半夜被报警电话叫醒了。
除了工程部署上的优化,因为最近在学习AI,我也厚着脸皮去参与了算法同学的一些模型调优讨论。我发现,纯靠改模型权重来解决业务问题,性价比其实很低。
比如在效果评估时,我们发现模型有时候会“胡说八道”(幻觉)。算法同学第一反应是:加数据,继续微调。但我结合业务场景一想,内部知识库的文档更新很频繁,微调根本跟不上。于是我提议引入 RAG(检索增强生成)架构。 我们在 Node.js 层加了向量数据库(Milvus)的调用,把用户问题先做 Embedding 检索,把相关的内部文档片段拼接到 Prompt 里,再扔给 vLLM 推理。 结果出奇的好!不仅幻觉大幅减少,而且连模型都不用重新微调了,直接省了几天算力成本。PM知道后,还特意在周会上夸了我一句“有架构思维”,听得我心里美滋滋的。
总结与碎碎念
回顾这一个多月从纯前端到硬啃大模型部署的经历,真的是痛并快乐着。
我最大的感悟是,技术底层的逻辑其实是相通的。前端对事件循环、流式处理、并发连接的理解,在搞 Node.js BFF 层和 AI 推理服务调度时,反而成了一种降维打击。以前觉得 AI 离自己很远,是算法大佬的专属,现在自己亲手把模型部署上线,看着它一行行吐出文字,那种成就感是无与伦比的。
当然,这也离不开工具的加持。像百度 Comate 这样的 AI 编程助手,在写那些繁琐的底层流处理、Dockerfile 配置时,确实帮我节省了大量查文档和试错的时间,让我能把更多精力放在架构设计和业务逻辑上。
好了,不说了,北京晚高峰的地铁又要开始挤了。今天代码写得顺,没出Bug,晚上打算早点下班,去吃顿好的犒劳一下自己。明天还得继续研究怎么把 vLLM 的推理结果做量化(Quantization),听说能进一步压榨显存,又是一场硬仗啊。
全栈之路漫漫其修远兮,吾将上下而求索。共勉!

评论 0