杭州深夜死磕机器学习部署的血泪与最佳实践

Tech大数据
2026-06-24 18:19
阅读 728

凌晨两点半,滨江的夜风透过窗户吹进来,桌上的瑞幸已经凉透了。我揉了揉发酸的脖子,看着屏幕上终于跑通的压测数据,长舒了一口气。

作为一个30岁才从传统制造业转行写代码的“大龄新人”,我在这行卷得确实辛苦。白天在公司跟产品经理扯皮,晚上就喜欢一个人戴着耳机敲代码,深夜的效率高得离谱。最近业余时间一直在啃Rust,被所有权和生命周期折磨得死去活来,但一旦跑通,那种内存安全又极致性能的感觉,真的让人上头。

不过今晚不聊Rust,聊聊最近让我掉了一大把头发的事——机器学习模型的工程化部署。

业务背景:一个让人头秃的AIGC需求

上周五临下班,产品经理笑眯眯地凑过来,说要搞个“行业竞品智能分析系统”。需求听起来很简单:用爬虫去各大平台抓取竞品动态和研报,然后用AIGC大模型对数据进行深度清洗、摘要和趋势预测,最后搞个看板展示。

数据从哪来?爬虫组的老哥用Scrapy写了个分布式抓取任务,加了各种代理池和验证码破解,总算搞回来几十万条脏数据。

模型怎么选?为了控制成本,我们没上闭源API,而是选了Qwen-7B做基座。拿爬回来的高质量研报做了个几万条的指令微调数据集。这里踩了个小坑,一开始Loss死活不收敛,后来排查发现是数据清洗没做干净,混入了大量HTML标签和乱码。老老实实写正则和规则把数据洗了一遍,换了CosineAnnealingLR学习率调度器,Loss曲线才终于平滑下降。效果评估方面,除了看ROUGE分数,我们还搞了个人工盲测抽检,整体摘要的准确率和流畅度达到了业务预期。

模型训练完了,重头戏来了:怎么把这玩意儿丝滑地部署上线?

部署踩坑录:从OOM到并发地狱

刚开始,我天真地以为用HuggingFace的pipeline包一下,写个FastAPI就能交差。结果一上压测,直接教做人。

坑一:显存OOM与推理慢 7B模型全精度加载直接撑爆了一张24G的3090。换成FP16勉强塞进去,但并发一上来,显存碎片化严重,直接OOM。而且原生推理速度感人,生成一句话要等好几秒,产品经理在旁边疯狂吐槽“这比我打字还慢”。

坑二:Python网关的性能瓶颈 后来我换了vLLM作为推理后端,利用PagedAttention解决了显存碎片问题,吞吐量上来了。但是,当并发请求超过50时,FastAPI的异步事件循环被阻塞,导致流式输出(SSE)卡顿,首字延迟(TTFT)飙升。Python在处理高并发长连接时,确实有点力不从心。

为了解决这个问题,我脑子里闪过一个念头:用Rust重写推理网关!

Rust拯救世界:手搓高性能AIGC网关

说干就干。我用Axum框架写了个轻量级的推理网关,专门负责接收前端请求、做请求排队、限流,然后转发给后端的vLLM服务,最后把SSE流式数据推给前端。

这里分享一段核心的SSE转发逻辑,Rust的async/awaitStream处理起来真的太优雅了:

use axum::{
    response::{sse::{Event, Sse}, IntoResponse},
    routing::post,
    Json, Router,
};
use futures::stream::Stream;
use std::convert::Infallible;

// 处理AIGC流式响应的核心路由
async fn generate_stream_handler(Json(req): Json<GenerateRequest>) -> impl IntoResponse {
    // 调用后端vLLM服务获取流式数据
    let stream = call_vllm_backend(req).await;
    
    // 将后端返回的字节流转换为SSE事件流
    let sse_stream = stream.map(|result| {
        match result {
            Ok(chunk) => {
                // 解析vLLM返回的JSON,提取delta content
                let text = parse_chunk(&chunk);
                Ok(Event::default().data(text))
            },
            Err(e) => {
                // 错误处理,推送错误事件给前端
                Ok(Event::default().data(format!("Error: {}", e)))
            }
        }
    });

    Sse::new(sse_stream).keep_alive(
        axum::response::sse::KeepAlive::new().interval(Duration::from_secs(1))
    )
}

换上Rust网关后,效果立竿见影。内存占用从几百兆降到了几十兆,CPU使用率也稳如老狗。在500并发的压测下,首字延迟稳定在200ms以内,流式输出丝滑得就像德芙巧克力。当时看着监控面板上平稳的曲线,我激动得差点在办公室里喊出来。

插曲:被Manus Agent“背刺”的日常

说到写部署脚本,最近AI圈不是那个叫Manus的通用AI Agent挺火嘛。我本来想偷个懒,把Dockerfile和K8s部署yaml的活儿交给它。

我给它下了个指令:“帮我写一个包含CUDA 12.1、cuDNN 8.9和vLLM最新版的Dockerfile,并生成对应的K8s Deployment和Service yaml,要求支持GPU资源调度。”

你猜怎么着?它确实给我生成了一大坨代码,看着挺像那么回事。结果我一build,好家伙,基础镜像选了个带完整CUDA Toolkit的巨无霸,镜像直接干到15个G。更坑的是,它生成的K8s yaml里,GPU资源请求写的是nvidia.com/gpu: 1,但没加limits,也没配置runtimeClassName: nvidia,直接扔到测试集群里根本调度不上去。

后来我花了一个小时去改它生成的代码,还顺便排查了它引入的一个环境变量拼写错误。这让我深刻意识到,像Manus这样的AI Agent,现阶段用来做代码补全、写写单元测试或者查个Bug还行,想让它全自动搞定复杂的工程化部署,还是得自己兜底。它是个好助手,但绝不是能完全甩手掌柜的“赛博员工”。

最终部署架构与参数配置

经过几天的折腾,最终的部署架构稳定了下来。这里贴一下vLLM的启动配置,给后来者避避坑:

python -m vllm.entrypoints.openai.api_server \
    --model /models/qwen-7b-chat-finetuned \
    --served-model-name qwen-7b \
    --host 0.0.0.0 \
    --port 8000 \
    --tensor-parallel-size 1 \
    --max-model-len 4096 \
    --gpu-memory-utilization 0.9 \
    --max-num-batched-tokens 8192 \
    --enable-chunked-prefill \
    --dtype float16

几个关键参数说明:

  • --gpu-memory-utilization 0.9:千万别设1.0,留点显存给系统和其他进程,不然极易OOM。
  • --enable-chunked-prefill:开启分块预填充,能显著降低长文本输入时的首字延迟,AIGC场景必开。
  • --max-model-len:根据业务实际最大文本长度设置,别盲目设8K、32K,白白浪费显存。

写在最后

这套系统上线跑了一周,各项指标都很稳。爬虫每天定时抓取,AIGC模型在后台默默消化,前端看板实时刷新,产品经理终于不再天天催进度了。

回想这大半年,从传统行业跳进互联网这个大火坑,30岁重新开始,确实压力山大。杭州这边阿里、网易的大厂机会是多,但卷也是真卷。每次深夜敲代码,看着镜子里越来越高的发际线,也会问自己图什么。

但每当解决了一个棘手的Bug,或者像这次用Rust把性能优化到极致时,那种纯粹的快乐和多巴胺分泌,又让我觉得一切都值了。技术这东西,只要你肯钻研,它永远不会背叛你。

不说了,天快亮了,得赶紧把代码提交,顺便去楼下买个包子。明天还得去面一下某大厂的基础架构岗,希望能把这段经历吹……哦不,分享给他们面试官听。祝大家头发浓密,代码无Bug!

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝