一、背景
Deepseek-r1模型的爆火标志着本地部署大模型的需求日益增长。本文主要探讨如何优化本地部署大模型的性能,并结合我们的实践进行评测分析,文章最后我们将分享如何在本地高效部署满血版Deepseek-r1大模型。
在生产环境中,我们已部署专用的大模型推理集群,并对其性能进行了全面优化。对于大模型推理来说,性能优化主要聚焦于两个关键指标:吞吐量与响应时间(RT)。
1.吞吐量
传统上,我们用每秒请求数(QPS)来衡量吞吐量,即系统每秒能够处理多少请求。但对于大模型而言,还有一个重要指标——每秒Token数(token/s),它反映了系统每秒能处理的输入或输出Token数量。
2.响应时间(RT)
这是指系统处理每个请求所需的时间。对于支持流式输出的大模型,还需要关注另外一个指标——首个Token到达时间(TTFT: Time To First Token),即从开始处理请求到输出第一个Token所需的时间
接下来文章将介绍部署高性能大模型推理服务的方法与思路,这些方法主要围绕提升吞吐量与响应时间这两个关键指标展开。
二、高性能、易扩展的大模型推理框架是什么样的
尽管业界已有许多经典的大模型推理框架,但在深入了解这些框架之前,我们不妨先思考一下,如何设计一个既高性能又易于扩展的大模型推理框架。
大模型推理框架需要满足的基本条件
性能足够高——CPU与GPU分离设计
对于一款以Python为主的大模型GPU推理引擎,要实现较高的性能,关键在于CPU与GPU的分离设计。至少需要将系统拆分为两个进程:CPU进程和GPU进程。CPU进程主要负责与CPU相关的逻辑,例如序列化、调度、分发和Resize等;而GPU进程则专注于GPU推理逻辑,其底层通过直接调用CUDA等库来进行GPU运算。
目前,主流的大模型推理框架,如vllm与sglang,已经实现或正在实施CPU与GPU分离架构。
那么,CPU与GPU分离究竟解决了什么问题呢?
一直以来,Python 在运行时采用全局解释器锁(GIL)机制,这意味着在任意时刻只有一个线程能够执行 Python 字节码。也就是说,即使在多线程程序中,各线程也无法在 Python 层面实现真正的并行执行。这个设计主要是为了简化内存管理和对象引用计数,从而保证线程安全,但也带来了一些限制,特别是在以GPU计算为主的推理服务中更为明显。
在单一的 Python 进程中,如果同时存在多个 CPU 密集型任务(比如网络请求处理、数据预处理、请求验证等)和 GPU 任务,它们都必须在同一个 GIL 下运行。这样,CPU密集型任务就会与GPU任务竞争 GIL,导致 GPU kernel 启动调度不足,从而形成性能瓶颈。这种瓶颈表现为GPU利用率不高,在高并发场景下,GIL 的竞争会极大地影响系统的响应速度和吞吐量。
下表为我们曾经针对Python GIL锁做过的专项对比测试,在做了CPU与GPU分离设计后,GPU利用率大幅提高,QPS提升了7倍,RT缩减了50%。
下面是VLLM在0.6版本做的最大变更,即做CPU与GPU进程分离设计,带来了性能的大幅提升,吞吐提升了2.7倍。具体可以参考文章[1]vLLM v0.6.0: 2.7x Throughput Improvement and 5x Latency Reduction。
可扩展性足够好——各模块高内聚低耦合
为了实现高效且易于扩展的设计,我们应将系统按照功能拆分为多个模块,每个模块只负责其特定功能,确保模块内部的高内聚和模块间的低耦合。一个完整的大模型推理框架至少需要包含以下四个模块:
- 接入层:接入层负责处理各种请求。比如当收到OpenAI格式的请求时,接入层将其转化为内部可识别的原始请求(raw request),以便后续其他模块继续处理。
- 调度器:调度器负责管理和调度各个请求(Request)。当有多个并发请求时,调度器动态调整模型的输入和输出,以确保计算资源得到高效利用,同时满足调度限制,如GPU缓存、最大请求数和最大处理长度等。调度器通过管理请求的状态、缓存、优先级和资源使用,确保推理过程流畅进行。
- 模型推理:在接收到请求后,模型推理层调用相应模型的forward方法进行推理计算。其底层实际上调用CUDA等进行GPU推理。
- 显存管理:操作系统有物理内存管理机制,避免了频繁申请和释放内存带来的碎片问题。然而,CUDA计算中并没有显存管理机制,频繁的显存申请与释放同样会导致显存碎片问题。因此,显存管理成为推理引擎中不可或缺的模块,用于解决显存碎片问题。
大模型推理框架设计
综合上述内容,我们可以设计出一个高性能、可扩展的大模型推理框架。框架图如下:
从框架图中可以看出,系统首先被拆分为多个进程(多个CPU进程与GPU进程),进程间可通过管道等方式进行通信。此外,系统在逻辑上又被拆分为多个模块,其中接入层、调度器、模型推理和显存管理四个模块是必不可少的。
该架构也是当前vllm 1.0与sglang等经典推理框架的基础架构。感兴趣的同学可以通过查看相关代码,你会发现它们的设计思路大致与上面相同。比如下面的sglang推理引擎的代码,参考:[2]sglang 代码
三、解决显存碎片问题,大幅提升吞吐—Paged Attention
在 Linux 等操作系统上运行的应用程序通常不会出现内存碎片问题,这是因为 Linux 内核拥有强大的内存管理机制,专门用于解决内存碎片问题。然而,这一优势仅适用于系统内存;如果在 GPU 上频繁申请和释放不规则大小的显存,就可能导致显存碎片的产生。
在大模型推理场景中,显存碎片问题尤为严重。大部分推理过程都涉及注意力计算(Attention),而每次计算都需要申请并使用一个名为 kvcache 的缓存。随着请求的不断增加,kvcache 的大小与数量会逐步上升,通常占据总总显存的约三分之一,而且它会被频繁地被申请和释放。如果不对 kvcache 使用的 GPU 显存进行有效管理,显存碎片将大量累积,最终可能导致系统性能下降甚至崩溃。
为了解决这一问题,vllm 提出了 Paged Attention 的概念[3],其设计思路正是借鉴了操作系统的内存管理机制,对 kvcache 的显存进行统一管理。
首先,我们回顾一下操作系统是如何避免内存碎片的。操作系统通常将物理内存划分为若干固定大小的块,并利用页表将应用程序的虚拟地址映射到相应的物理内存块。当应用程序申请内存时,系统先分配虚拟地址,然后在实际使用时,从固定大小的物理内存块中分配空间,并建立虚拟地址与物理地址之间的映射。这样,就能有效避免内存碎片问题。
Paged Attention 正是基于这一原理而提出的——“Paged”体现了类似页表的映射方式,而“Attention”则表示这种映射机制被应用在大模型注意力计算中。
PagedAttention 工作原理,图片来自[3] Efficient Memory Management for Large Language Model Serving with PagedAttention。
vllm 的 Paged Attention 是一种受操作系统虚拟内存和分页机制启发的注意力算法。它将大型语言模型中的 KV Cache 缓存划分为固定大小的块,这些块可以在内存中非连续存储。然后通过Block table(类似Linux页表)把每个请求的逻辑KV 块(类似Linux虚拟地址)映射到物理KV 块(类似物理内存)中。通过这种方法,PagedAttention 能有效管理内存,减少碎片和浪费,大幅提升系统的吞吐量。
Paged Attention评测效果
图片来自[4] vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention
通过高效的内存管理,Paged Attention减少了内存浪费,提高了 GPU 的利用率,使模型的推理吞吐量比传统方法提升了数倍。例如,与 HuggingFace Transformers 相比,吞吐量可提升至 24 倍;与 HuggingFace TGI 相比,提升可达 3.5 倍。此外,它还降低了内存开销,支持更复杂的采样方法,使 LLM 服务变得更快、更经济。
目前VLLM与SGLang等推理引擎默认支持Paged Attention开启,所以你使用这些推理引擎部署大模型,系统会自动支持。
四、缓存之前请求的计算结果,减少重复计算—Radix Attention
虽然 Paged Attention 成功解决了显存碎片问题,并显著提升了系统吞吐量,但在大模型推理中,还有一个常见场景具备较大性能优化空间。
实际应用中,我们往往需要多次给大模型发送请求,而这些请求的Prompt中有很大一部分的内容是完全相同的。如下所示:
图片来自[5] Fast and Expressive LLM Inference with RadixAttention and SGLang
上图中蓝色框表示可共享的Prompt部分,绿色框表示不可共享的Prompt部分,黄色框表示模型的输出。
上图中可共享部分场景包括少量示例学习、自我一致性中的问题、多轮对话中的聊天历史,以及思维树中的搜索历史。在这些场景中,每次请求都会重复计算这些Prompt中可共享的部分,这些会造成大量的计算资源浪费。
那么,有没有办法将这些重复部分的计算结果(KV Cache)缓存起来,下次请求时直接使用呢?为此,SGLang 提出了一种优秀的算法—— Radix Attention。
RadixAttention 是一种新技术,用于在大语言模型的推理过程中优化 KV 缓存的重用。其核心在于利用基数树(Radix Tree)来高效管理和重用不同请求之间共享的前缀,从而减少重复计算和内存占用。也就是说,当多个请求共享相同的前缀(例如系统提示 "You are a helpful assistant")时,RadixAttention 可以重用该前缀对应的 KV 缓存,避免重复计算。
下图中的例子展示了 RadixAttention 在九个时间点上的基数树动态演变过程,以下是具体步骤的解释:
图片来自[5] Fast and Expressive LLM Inference with RadixAttention and SGLang
- 初始状态:基数树最初为空。
- 第一场聊天开始:用户发送“你好!”,助手回复“你好!”,系统提示、用户消息和助手回复被合并为基数树中的一条边,连接到新节点。
- 第一场聊天继续:同一会话中收到新消息,服务器在基数树中找到已有前缀并重用其KV缓存,新消息作为新节点附加到树上。
- 第二场聊天开始:另一个用户开始新的聊天会话,服务器分割现有节点,使两个会话共享公共前缀和KV缓存。
- 第二场聊天继续并进行淘汰:第二场聊天继续,因内存限制,第一场聊天中最少近期使用的节点被淘汰释放空间,新消息在共享节点后添加到树上。
- 处理少样本学习查询:服务器收到不与现有节点共享前缀的少样本学习请求,根节点被分割以容纳新序列,请求作为单独分支插入树中。
- 处理一批少样本学习查询:收到一批共享相同少样本示例的查询,分割第六步的节点以在这些查询间实现KV缓存共享,最大化重用并减少冗余计算。
- 第一场聊天继续并进行淘汰:第一场聊天继续,基于最近最少使用(LRU)策略,淘汰第二场聊天的节点以高效管理内存。
- 自一致性采样并进行淘汰:服务器收到生成多个答案的请求,为自一致性目的,按照LRU策略淘汰较不重要的节点以为新请求分配内存。
那Radix Attention带来的性能提升如何呢?我们针对SGLang推出的Radix Attention与VLLM (v0.5.0)进行了对比评测。同时由于Radix Attention可以复用不同请求的上下文,这与我们的日常业务使用比较吻合,取得了不错的评测结果,Radix Attention充分利用不同请求之间的共享前缀,其耗时比VLLM(v0.5.0)快30%,吞吐是VLLM(v0.5.0)的1.5倍。
下面为SGLang给出的Radix Attention性能对比效果,与当前系统相比,SGLang吞吐提升了5倍以上。
图片来自[5] Fast and Expressive LLM Inference with RadixAttention and SGLang
如果你也想尝试下Radix Attention,可以直接使用SGLang的推理引擎去启动大模型尝试下。
五、请求分块处理,避免单个请求卡顿 —— Chunked Prefill
在将大模型应用于生产环境时,我们有时会遇到一种奇怪的现象:某个请求的响应时间(RT)异常长,甚至出现卡顿,而系统的平均响应时间却依然正常。这是什么原因导致的呢?又如何解决呢?
大模型的推理过程实际上可以分为两个阶段:prefill 阶段和 decode 阶段。举个例子:假设我们输入了一个包含 1000 个 token 的 prompt,并希望模型生成 100 个 token 的响应。
- Prefill 阶段:系统首先对这 1000 个 token 进行并行推理,这一步骤可以充分利用 GPU 的并行计算能力。
- Decode 阶段:随后,系统会逐个生成后续的 100 个 token。由于每个新生成的 token 都依赖于之前的输出,因此这一阶段必须按顺序逐个生成。
在实际应用中,多个请求往往会同时进行推理,因此可能出现不同请求的阶段交叉运行。例如,如果请求 req3 的 prefill 阶段处理了一个非常长的 prompt,那么它就会占用大量的 GPU 资源;而如果此时 req13的 prefill 阶段与请求 req1的 decode 阶段并行运行,就会导致 req1的 decode 阶段响应速度明显变慢,甚至出现卡顿现象。
图片来自 Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve vLLM @ Fourth Meetup (Public) ,较大的prompt请求与decode阶段请求并行调度,算力资源争抢,会显著影响decode阶段。
问题原因明确了,解决方法也十分简单:缩短每次提交给 GPU 并行计算的 prompt 长度。具体来说,我们可以将整个 prompt 按照固定长度(例如 512 个 token)进行分块,每次在 prefill 阶段只处理一块。这样一来,每次并行计算的内容就变得更短,不仅能减轻单个请求对 GPU 资源的占用,还能避免对同时运行的 decode 请求产生影响。这个方法便被称为 chunked prefill。
图片来自 Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve vLLM @ Fourth Meetup (Public) 开启chunked prefill后,prefill与decode并行互不影响。
如下图所示,vllm 通过启用 chunked prefill 功能,显著降低了系统的最大响应时间(max RT)。
图片来自 Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve vLLM @ Fourth Meetup (Public),开启chunked prefill后,在高并发QPS下,平均RT提升了2倍。
在 vllm 的最新版本中,chunked prefill 已默认开启。
六、缩短输出长度,显著提升性能
在前文中,我们提到过大模型的推理过程分为 prefill 阶段和 decode 阶段。Prefill 阶段主要对 prompt 进行注意力计算,并可以并行进行;而 decode 阶段则用于生成新输出的 token,这一阶段必须按顺序逐个生成,无法并行。
因此,如果我们能够在 decode 阶段尽量减少生成的 token 总长度,就能显著提高整体的响应时间(RT)。具体来说,如果每生成一个 token 耗时 5 毫秒,减少 5 个 token 的输出,就能减少 25 毫秒的总响应时间。对于非流式调用的大模型来说,这种改进会带来显著效果。例如,如果大模型只需进行简单的分类或识别任务,那么仅需输出结果即可,其他无关信息完全不需要生成。
那么如何缩短大模型的输出长度呢?
a.限制最大输出长度
首先,可以通过设置系统参数来限制大模型的最大输出长度。如果你通过 OpenAI 接口调用大模型,在输入参数中有一个叫做 max tokens 的设置项。你可以调整该参数,限制大模型的最大输出长度。这样一来,可以避免大模型产生过长的输出,甚至防止无限循环的情况。一个示例代码如下:
b.通过 Prompt 限制输出
另外,可以通过优化 prompt 来引导大模型产生更短的输出。例如,在 prompt 中加上类似“请直接输出结果”或“请尽可能简短输出结果”的提示语,可以有效减少无关内容的输出。
c.微调大模型
如果条件允许,微调大模型也是一种有效的方法。通过微调,可以让大模型在满足需求的前提下尽量缩短输出。微调的过程中,首先可以通过 prompt 调整输出长度,制造大量数据后,再对大模型进行训练。这样一来,不仅输入的内容被优化,输出的长度也能有效缩短,达到更好的效果。
七、使用多卡推理,推理速度翻倍
在某些场景下,出于模型效果考虑无法对大模型进行量化,但对响应时间(RT)有非常高的要求。这时,尝试 多卡推理 可以带来立竿见影的效果,通常能够将 RT 缩短至原来的 1/3 或 1/2。
以下是我们针对 单卡 与 双卡 推理性能的对比测试结果:
可见推理中单卡变双卡可以显著提升大模型推理速度与QPS。
那为什么多卡可以提升大模型的推理速度呢?主要原因多卡推理的优化是通过 tensor parallelism(张量并行)实现的。
假设你将 tensor parallel 设置为 2,意味着使用两张 GPU 来加速推理。在模型加载时,推理引擎会将大模型的 attention 参数的数量分为两组,分别加载到每张 GPU 上。然后,在推理过程中,两个 GPU 会并行计算注意力,最后再将结果聚合合并。
想象一下,你有一本非常厚的书,你想一次性复印整本书,但是你的复印机一次只能复印几页。这时,你可以把这本书分成几个部分,每个部分分别复印,最后再把所有复印好的部分按顺序拼接起来,这样就完成了整本书的复印。
在张量并行中,我们要处理的大模型就像是那本厚书,而GPU则像是复印机。因为单个GPU无法一次处理整个大模型,我们就需要把模型(在这个例子中是权重张量)分成几个部分,让不同的GPU分别处理(相当于复印书的不同部分)。在处理输入数据时,就像是把书的每一页分别复印,然后再把复印好的各个部分拼接起来,形成完整的输出结果。
如何配置tensor parell并行呢?下面我们分别给出vllm 与sglang两款大模型的配置方式。
1.vllm配置多卡推理
以下命令为vllm如何配置多卡推理的方式。
图片来自[6] vllm Documentation
2.SGLang配置多卡推理
以为命令为SGLang推理服务如何配置多卡推理。
八、小模型推理+大模型验证 —— 预测解码 (Speculative Decoding)
最近,一种名为预测解码的加速技术备受关注,它能够在特定条件下显著提升大型模型(如72B大模型)的推理速度。
预测解码工作原理比较简单,假如你想加速一个70b大模型。你可以训练一个同类型的7b小模型,然后开启预测解码。系统同时加载7b小模型与70b大模型,在推理的时候先让7b小模型做输出,比如输出5个token。然后再把这5个token交给70b大模型去做验证,并保留验证正确的前N个token做为输出,以此类推。
由于验证是可以批量进行的,而小模型的推理速度又比较快。这样就可以大大提升70b大模型的推理速度,同时保障70b大模型的效果。
以下为我们针对70b模型所做的实验效果。
可见预测解码在一定的场景下可以提升大模型的推理速度。
此外还有一种更简单的方式,如果你不想训练7b的小模型,而你的输出中大部分都与输入promt相似,比如你只是让大模型帮你修改下文章中错别字。那么你可以直接使用n-gram匹配prompt的方法替代小模型,即直接从输入prompt中选取预测的token,让大模型直接去验证。这样输出速度更快,更简单。
关于预测解码,想深入了解的同学可以参考vllm的这篇文档。[8] How Speculative Decoding Boosts vLLM Performance by up to 2.8x
下面我们介绍下如何在vllm中配置预测解码。
a.使用大模型+小模型的方式
b.使用n-gram的方式
九、高效部署Deepseek-R1模型的方法
前面我们介绍了业界大模型性能优化的很多方法,接下来我们将用SGLang这个推理引擎来部署下最近爆火Deepseek-R1满血版大模型。下面分享下详细的部署步骤。
a.如何下载Deepseek-r1
这次Deepseek发布了一系列模型,如下:
原始模型包括Deepseek-r1-zero与Deepseek-r1,其中Deepseek-r1是官方推荐的经过多阶段训练的最优模型。但是Deepseek-r1有671B大小的参数,部署起来至少需要2*8H20GPU,比较耗费资源。所以deepseek团队又基于Deepseek-r1蒸馏出了一系列小的模型,其中效果不错比如DeepSeek-R1-Distill-Qwen-32B,单卡H20可以运行启动。
我们这里只介绍满血版Deepseek-r1的部署方法。
b.准备部署环境
我们尝试使用SGLang这个大模型推理引擎部署Deepseek-r1。以下为我们的部署软硬件环境准备。
部署Deepseek-r1
准备好SGLang镜像,deepseek-r1模型,GPU后,可以按照如下命令启动deepseek-r1
node 1:
python -m sglang.launch_server --model-path deepseek-ai/DeepSeek-V3 --tp 16 --dist-init-addr 10.0.0.1:5000 --nnodes 2 --node-rank 0 --trust-remote-code
node 2:
python -m sglang.launch_server --model-path deepseek-ai/DeepSeek-V3 --tp 16 --dist-init-addr 10.0.0.1:5000 --nnodes 2 --node-rank 1 --trust-remote-code
这里使用的是多机多卡部署,部署后使用Openai格式请求发送到node1上即可。
十、总结
文章依次总结了部署高性能大模型推理服务的技巧与实践,先后介绍了Paged Attention,Radix Attention,chunked prefill,多卡并行等大模型推理加速方法,并给出验证结果与操作方法。文章最后还给出最近爆火的deepseek-r1的高效部署方法,欢迎大家去尝试优化。
后续我们将会持续关注大模型推理性能提升方面的最新技术,验证并及时分享给大家。
参考文献
[1] vLLM v0.6.0: 2.7x Throughput Improvement and 5x Latency Reduction https://blog.vllm.ai/2024/09/05/perf-update.html
[2] sglang 代码 https://github.com/sgl-project/sglang/tree/main/python/sglang/srt/managers
[3] Efficient Memory Management for Large Language Model Serving with PagedAttention(https://arxiv.org/abs/2309.06180)
[4] vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention https://blog.vllm.ai/2023/06/20/vllm.html
[5] Fast and Expressive LLM Inference with RadixAttention and SGLang https://lmsys.org/blog/2024-01-17-sglang/
[6] vllm Documentation https://docs.vllm.ai/en/latest/
[7] SGLang Documentation https://docs.sglang.ai/backend/server_arguments.html
[8] How Speculative Decoding Boosts vLLM Performance by up to 2.8x https://blog.vllm.ai/2024/10/17/spec-decode.html
文 / menglinggong
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。