模型部署工作已经开展了4个月了,从4张A10,T4到200+ H20和A100,部署了十余家家国内厂商的模型,遇到了很多杂乱的问题,用这个md来做一个陆陆续续的总结记录这半年,事情太多太杂,记一笔给以后留下点旧经验。
1.推理框架 vllm
2.打镜像 cuda版本与gpu分配 docker
3.从裸机到容器服务 k8s
4.转发和网络 SSE
5.在线服务设计
6.压力测试和配额管理
7.通信瓶颈与计算瓶颈
8.微调 LlamaFactory
9.不同场景模型的选择与标准
6/24
工作的突增主要是在2025年过年deepseek出来之后,公司部门要求尽快对deepseek的模型进行评估,在那之前,我只是用组内刚化缘拿到单卡A100在内部平台上进行一些基础推理,那时候用的Llama系列在huggingface提供的transformer包,封装了简单的推理逻辑和基础http service,那时候对streaming,SSE,latency,TTFT都没有什么概念,就是知道反正有一段输入就能有一段输出就够了。作为之前没有参与过模型训练和部署的人来说,有几个关键概念是在部署模型之后才开始理解的。
在最开始huggingface琳琅满目的模型中,自然而言抽取到的是各个模型家族的base模型,在最开始的部署当中,我发现base模型和在chatgpt官网上提供的模型结果天差地别,它很容易陷入死循环,本身的输出也没有解决任何问题,后面它在国内有了一个很贴切的翻译,叫做基座模型,它也组成了我对LLM的初步理解,LLM是在根据Prefill阶段的结果预测下一个Token。一个很简单的例子,在Base模型里发送内容为“你好”的Query,它的返回会是“你好,这是一条深幽的小路,小路上...”这种通顺的话,它本身不会回答问题。与之对应的是Instruct模型,它能够跟踪提问者的问题并针对性的给出响应,让模型能够听懂人类提问的意图,在Instruct模型里发送内容为“你好”的Query,它的返回基本都是“你好!我是blabl模型,今天有什么可以帮你”不再是单纯的续写提问,而是开始了对话。
在闭门造车的时候,根本没有想过接口格式问题,就算如此,在最开始的阶段,也和我们下游demo的同事花了一阵子时间在对其接口格式上,目前还比较常用的接口是 completion和chatcompletion接口, Body分别是prompt和message,如果让我来面试部署测试的小伙伴,让他写出一个合法的body会是我最愿意提问的第一个问题,它直接反应了面试者有没有亲手写过query,抄过无数个query之后,Model和Message的格式基本就很难被忘记了。
很快我们就从闭门造车接入了国际正规,引入了vLLM,这是一个标准化且广泛使用的推理框架,它支持大部分市面上的开源模型,支持张量并行和流水线并行并保持超高的吞吐量,在批量推理的过程里,我推荐参考这篇文章对vLLM的内部原理有一些初步理解。vLLM and PagedAttention 对于部署的用户来说,安装环境,使用docker拉下最新的vLLM的框架,在github vLLM issue里查看模型的支持情况,基本就能起一个服务了。

不同于专款专用,我们需要在机器上运行很多模型来给下游测试,最开始的策略是直接部署到公司内部的推理平台,但很快我们就陷入了debug的死循环,在最初的设计里,下载模型->下载框架->安装依赖->运行,但对于各个厂家提供的模型,这里有非常多的琐碎小事,和平台的框架不匹配,版本不匹配,参数不匹配导致了上线模型时间的不确定性,我们很快就切到了docker环境去避免大部分依赖问题。那两周多加班的经验让我总结了一整套适应目前手头情况的模型流水线,先在裸机上下载模型权重和框架,检测MD5,测框架内部依赖与容器版本不匹配的问题,再根据平台特性对友商的框架二次封装,上线平台去解决平台内的问题,这种方法很快就保证了onboard模型的稳定性和时效性,从之前每个模型从上线时间不确定,到平均上线时间5天的标准化流程。
推理框架有内部依赖
不少厂家提供的模型是从自己的线上环境拆解的,在他们内网能够正常工作,但出了内网环境很有可能都过不了自检脚本
平台容器GPU驱动版本与docker版本不匹配 大部分情况下都是兼容的,但容器driver和cuda版本和宿主机版本有依赖,我也只遇到过一次,这类问题最好的办法就是查阅官方文档,任何二手消息都不能保证正确。 Nvidia驱动版本
平台的NUMA分配问题 这问题我也只遇到过一次,在一个八卡的Pod里的CPU在合作伙伴的框架里不能正确分配CPU到子node
平台的httpService问题 这问题更多是应用层问题,包括http请求的转发,取消,我们在这个过程里也发现了线上超大流量的平台的某个恶性bug。
6/25
在三月到四月的期间,我们的部署流程被优化为了 实例化新机器,安装对应的cuda和nvidia driver,下载模型,安装docker,安装docker nvidia toolkit,运行,查bug,这个过程流水线,但依然非常繁琐,裸机带来的了几个我们当时亟待解决的问题
如何给裸机做Syndication, 作为上游的模型提供方,我们需要通过ModelName来路由到不同的模型。
如何给裸机做负载均衡,在起步阶段,我们的目标当然是以能跑通为主,但随着下游的测试需求逐渐上量,很快就要遇到单机无法处理的情况。
如何做动态资源调配,200张卡说起来很多,但实际上也就是二十多台台物理机,尤其是在八卡模型横行的日子,20台机器需要一个集约化的管理平台去处理新模型的扩容和老模型的缩容。
最后的方案选型没有太多的纠结,很自然的就选了k8s和镜像管理服务,Syndication选用了Azure的APIM服务,在APIM内通过policy路由到不同的终端实例,这一套服务和Azure Machine Learning以及内部平台另外一个平台的核心设计如出一辙,每台机器实例化为一个pod,pod内通过deviceid来指定运行的GPU,就能实现GPU卡粒度的资源整合。在内部的某个分享会上,有一个组做了一整套更完备的开源工具,Kubernetes AI Toolchain Operator,鉴于目前我们的情况是在做一些internal flight,这一套生产环境的标准并没有被我们采纳。
有了容器服务和容器服务之后,一般还需要大家根据自己的云服务商提供情况,对权重进行集中管理,权重管理最好能够实现如下几个要求
裸主机和k8s pod能够共享,能够在一个子网环境内共享,提供和本地读写无感的读写速度,这点非常重要,在最开始的时候,我们将权重迁移到了对象存储服务里,100MB/s的速度成了服务拉起的性能瓶颈。
以上的服务和架构,仅仅是在200张卡,三个人的小组内的经验,对于更多人/卡的协同,依然是目前我还没有接触到,与此同时,AMD的Mi300系列的部署与服务截至目前我也没有接触过。
容器服务有几个关键点和一些目前还没有解决的问题,我也列在这里给未来的自己留一些问题。
1.即便是用了容器服务,加载权重到Pod经常也会花费20-30分钟,在给定实例数量的情况下,我们很难通过动态扩容响应线上的瞬时峰值
2.Syndication服务最后用的APIM非常强的依赖手动配置,这个过程非常容易出错,也很难进行debug (APIM的log会导致另一类线上服务的问题,这个在转发与网络里讲)
3.k8s服务的探针要打到合适的端口,有一些推理框架由于架构问题和实际情况,需要在前端再封装一层http Service,如果我们直接socket监听 http service的健康情况,很容易将线上流量打到一台还没有完全拉起的机器上。
最后留一个当时debug 文件服务的例子,有一个模型在本地能够正常运行,但是通过Cos和TCR放到容器里,会在服务拉起一半的时候会整体重启。做了两天实验,最后的情况如下
1.pod里起镜像服务,pod会重启,我的初步结论是零号进程挂了,有可能是容器和宿主机的cuda版本不兼容,容器内的driver需要与宿主机的driver通信来操控实例的硬件。
2.我们逐行比对了启动日志,发现启动日志与裸机启动毫无差别,裸机的cuda版本也在兼容列表里。
3.因此我们开始了第二轮操作,我将零号进程设为sleep 3600,然后到了容器内部手动trigger模型的部署和拉取(由于之前的设计,权重文件是远程挂载到pod里的),发现容器依然会退出。
4.在当时的情况下,任何代码层面的crash都不应该影响零号进程的生命周期,猜测是文件系统的问题,和文件系统的伙伴核对后,确定了这是一个known issue,最快的解决办法是把cos内容先加载到 /dev/shm/,随后的服务启动就很顺利了,在长期计划里,我们也把权重陆陆续续迁移到了另外一套更快的云端文件系统。
另外一个合作伙伴由于保密需求,对模型的启动进行了加密,导致单体镜像的体积很大(镜像服务上载速度是100MB/s),我的处理办法也很简单粗暴,从image的容器里选出较大的文件夹,把它们转移到云端文件系统(2G/s),剩下的框架代码重新commit成一个新的推理包。
另外一组情况是某些平台只支持export的docker镜像而非save格式,export还会带来很多的路径丢失问题,就需要额外打一个 run.sh到entrypoint里去修补路径丢失的问题,这就是一个case by case的体力活,此处按下不表。
8/12
转发
提起转发,一般我们指的是透传,有些时候转发是为了归一化后台调用的路径,这种属于L7 负载均衡,有时候是为了在转发的过程中解决一些接口格式上已有的问题,这种情况更像是打个在线补丁,尤其是在使用开源框架的时候,这种补丁尤为重要。鉴于组里遇到的情况,其实有些同学是不了解网络层请求的格式和规律的,透传一般是包括请求体和请求头的部分参数,有些参数属于运行时参数,可不要也一并转发闹笑话了。
网络
在online serving的过程里,除了后台的推理服务,在服务输出前会依次接上合规层和特殊逻辑层,在中国境内这是必要的审核条件,由于各家保密需求,合规层就不多做泄密,但一般也都分为 风险识别,代答和关键词更新几大模块,安全模块的QPS和日常服务差别很大,在SSE条件下,一次请求带来的QPS可能会达到十几或者上百,这也对安全模型带来的网络延迟带来了新的挑战,在我们测试线上服务过程里,常常会用温度图来调查安全模块带来的缓存窗口大小,确定线上延迟的区别。
另外对于网络拓扑带来的延迟,我们要在设计的时候多做考虑,在我们内部服务里,经常会忘记从美东的网关打到釜山的机房带来的延迟。
在线服务分为几类,对于文生文来说,vLLM已经能够满足大部分的需求,当然对于新出的模型来说,parser问题是常遇到的,这一块针对多轮对话/工具调用/非流式/流式进行批量测试就好,值得注意的是这一块工作其实和模型本身问题不大,基本都是格式处理带来的返回格式问题,不必花费太多心力在parser上。
文生图一般需要我们自己设计队列服务,但文生图的时间一般是在30-60秒之间,属于一次post请求可以等待的范畴内,所以我们也可以照搬文生文的裸机去进行测试。
文生视频系列,需要设计一套存储/队列服务,一般的思路是消息队列+对象存储+gpu服务器+cpu服务器。消息队列需要取舍的是 一致性和顺序性,这里用gpt总结来针对各自场景的需求进行取舍。关键词是 At Least Once 和 Exactly Once,全局有序,消息分发策略。额外注意的是RocketMQ在pip里有好几个包,有些包是不支持pull的,需要擦亮眼睛选择最新维护的SDK。