LLM Model Deployment Improvement Suggestion

挑战和机会

首先在探讨大预言模型的大规模部署之前,我们需要简单思考一些基础的问题:

  1. 我们需要启用多少个不同的模型?
  2. 如果当前有 1 个客户,我们需要如何应对?
  3. 如果当前有 100 个客户,我们需要如何应对?
  4. 如果当前有 10000 个客户,我们需要如何应对?

AI 和大模型应用对传统的计算机体系架构和服务器不熟模式都提出了巨大的挑战。传统的 Web Service 中,用户的行为有着如下的特征:

  • 用户的个人基础信息简单且序列化:传统的 Web 服务中,用户的信息可以被传统的数据库用规则的方式进行表示和记录。且用户的信息简单,大部分为对「用户的关系」和「用户的属性」进行简单记录。例如社交媒体中用户的「个人信息」属于「属性」,而「好友关联」属于「关系」。
  • 用户的行为规则,和系统的交互规律:传统的 Web 服务中,用户需要对系统进行某些交互,例如对商品进行下单购买,用户发出的请求是固定且规律的。与之对应,系统对用户做出的响应也是规律的。
  • 用户对系统算力需求较低且不需要使用异构设备:传统 Web 服务中,基本上所有的用户请求都是直接或者简介和系统数据库进行交互,且不需要使用 GPU 等异构算力设备。

然而,大语言模型出现之后,上述三点都发生了颠覆性的改变。

首先,用户的个人信息不再简单或者序列化,更多的是非结构化的隐式信息。例如在和大预言模型交互的过程中,用户和系统交互产生的大量的 embeddings 包含了用户的一些潜在的属性和特征。传统数据库不便进行记录和存储。而且数据规模也远超传统 Web 应用。

其次是用户的行为具有非常强的不确定性,对系统的算力需求不是稳定的。系统产生的响应是由模型生成的,具体生成信息的长度、内容等信息不固定,对计算资源的消耗也不是固定的。为此,系统调度和传统应用相比,预测困难增加,调度复杂性也大大增加。

最后是异构设备的使用。传统的 Web 应用只需要提前准备基础的服务框架,例如前端页面。用户的交互都是直接通过这些框架,对服务器的后端发出了具体的数据库操作请求。然而在大语言模型中,服务器后端多了异构设备处理的计算层,带来了新的一层延迟和开销。而这种延迟会直接反映到用户体验上,严重的对话延迟也是一个具有挑战性的问题。

虽然这些问题具有挑战性,但是也为初创企业和科研从业者带来了巨大的机会。我们回归一开始提出的几个问题,这些问题分别对应了我们系统中最需要关注的几个事情:

  1. 模型的数量决定了我们系统需要如何规划我们的 GPU 使用。
  2. 新客户来的时候,我们需要如何启动服务。对于一个新的客户请求,我们能否将模型正常拉起,推理之后得出结果。
  3. 多客户发出请求,我们已有的系统能否处理并发。在已有的机器集群部署完成之后,能否应对多个计算请求。
  4. 超大规模并发出现的时候,我们能否进行自适应的拓展。在已有算力明显不足的时候,能否通过自适应的方式进行拓展。

下面的篇幅将简单对系统的设计规划提出一些个人的思考,也会对上面的问题提出一些个人的方案。相比成熟的开源方案来说,还有一些需要调试测试的部分需要更加深入的研究和测试。

延迟分析

现有的大预言模型已经发展的相对比较成熟,但是同时,各种模型之间并不是我们希望的那样和谐共存。不同的模型也有各自的优势和劣势:

From https://arxiv.org/pdf/2303.18223.pdf

对于提供大语言模型服务的平台来说,在不同的场景下,调度不同的模型是比较常见的操作。同时,用户对于不同的场景,「精确度」或者「内容丰富度」也有不一样的需求。为此,在一个平台上,「内容不同」和「参数规模不同」的模型都需要进行相对应的部署。

对于一般的 LLM 模型来说,一些场景的延迟是可以接受的,例如用户在和模型第一次进行对话的时候,一定程度上的延迟是可以被接受的,这种延迟我们暂且称为一次性延迟。而每次用户和应用进行交互之后,如果都有比较高的延迟,那么对于用户体验而言,是非常不能接受的,这里称为例行延迟,因为任何用户的交互发出之后都会发生。两种延迟的差别可以通过进一步拆分进行分析:

  • 一次性延迟 = 处理用户请求 + 模型拉起 + 模型推理 + 反馈响应
  • 例行延迟 = 处理用户请求 + 模型推理 + 反馈响应

主要区别为,用户第一次使用特定的模型时,有不固定的模型拉起延迟。这部分通过特殊手段可以实现一定程度的降低。

检测分析

首先回顾一下系统架构,理想架构主要有三层:

  1. 客户端:提供前端页面效果,不属于部署服务器的架构。如果用户交互上出现卡顿,需要对前端的页面进行优化。
  2. 服务层:主要提供用户的请求处理,对用户的基本信息进行记录,对用户的权限进行管理。这部分如果出现卡顿,可以使用传统的网络服务优化方案,例如优化数据库、增强并发处理等。(同时个人猜测目前的系统部署方案中,服务层并没有单独拉起,而是和推理层的机器有耦合,这部分会对性能有一定的影响)
  3. 推理层:个人认为用户体验主要有推理层影响,处理用户的请求会消耗相对较多的计算资源。这部分开销主要可以氛围「调度开销」和「执行开销」。前者是我可以提供一些分析和思考,后者则需要通过算法方面的优化进行提升。

回到我们之前对用户提出的延迟分析,通过统计和观察,有以下的重点优化方向:

延迟 = 处理用户请求 + 模型拉起 + 模型推理 + 反馈响应

  1. 「处理用户请求」高:优化服务层
  2. 「模型拉起」高:优化推理层的调度方案
  3. 「模型推理」高:优化推理层的模型算法,使用量化模型等提速方案
  4. 「反馈响应」高:优化服务层

如果为了敏捷开发和快速实现,短期可以继续沿用之前讨论的两层开发模式:

服务层优化

在服务层上,个人认为主要的瓶颈并不是服务层的设计和处理。而是需要将服务层进行解耦和单独管理。服务层的机器主要业务范围是和传统数据库的增删查改,对用户的信息处理,并对推理层的机器发出请求。这些业务对硬盘数据和文件系统读写有一定的需求,同时对网络带宽有较高的需求。如果和推理服务器放在一起,可能会导致网络或硬盘IO出现性能瓶颈。此外还有一定程度的安全问题。

这里另一个主要建议是使用 gRPC 处理推理层的应用替代传统的 Rest API。可以提供更好的性能。参考:REST vs GraphQL vs gRPC 此外,Vector database 的处理对 GPU 的需求几乎为 0。目前为止 milvus 和 faiss 的 GPU 方案和使用策略都不尽人意。而且 Vector database 的适应瓶颈主要为 IO 部分。所以这部分工作也可以放在服务层中进行优化和处理。

在政体系统的部署优化方向上,有一些值得参考和借鉴的项目:

  • https://github.com/Azure-Samples/azure-search-openai-demo 基于 Azure 云平台进行部署,重点使用了 Retrieval-Augmented Generation
  • https://github.com/ray-project/llm-applications 基于 ray project 的部署方案,我有比较丰富的基于 ray 的部署经验
  • https://github.com/vllm-project/vllm
  • https://github.com/dataelement/bisheng

推理层优化

通过增加每台服务器上的硬盘数量,预先准备模型,通过两阶段流水的快速拷贝方案,实现模型的快速拉起

分布式推理系统中,一般处理一个或几个连续用户请求的资源单位称为 repilca。如何更快的在机器集群上启用 replica,并且充分利用资源,不对机器产生冲突一直是值得研究的问题。

之前已经对模型的场景和内容进行了讨论,我们也确实有在机器节点上部署多种模型的需求。如果让每一个模型长期独占一块显卡,会对显卡资源造成极大的浪费。这里我在另一个项目中使用过一种快速拉起模型的技术可以作为参考。

首先我们可以增加每台机器的硬盘存量,将所有可能使用的模型全部下载到硬盘中。当用户发出请求时,如果有机器已经加载了对应模型,则将请求发送到对应机器上执行。如果没有加载,那么我们可以模型拷贝。先将 SSD 中的数据拷贝到 DRAM 中,再将 DRAM 中的数据复制到 GPU 显存上。这一拷贝过程还有三个显著优化点:

  1. 可以将最常用的模型部分拷贝在 DRAM 中预留。
  2. 可以通过将模型文件切块,使用流水线方式进行拷贝。
  3. 通过 CudaMemoryRegister 技术,将 DRAM 中的部分内存注册为 pinned memory,进一步提升 GPU 显存到 DRAM 之间的带宽。

应对并发场景的思考

对应用配资进行合理的描述,利用 ray serve 等分布式框架,合理的进行模型调度管理

之前简单的写过也是写 ray serve 的分析和思考。ray serve 确实可以应对分布式的部署方案,对于模型分部和使用比较复杂的场景,ray 也可以叫好的应对。但是 ray 并不能提供前文提到的模型拷贝优化的方案,这也是 ray 在整个系统中的痛点和瓶颈。但是 ray 的优势也非常显著,可以动态配置资源的使用,重点是部分显卡资源使用的配置支持。例如有一些较小的模型,一块 GPU 实际上可以支持同时部署两个模型。但是如果使用 docker 等现有的部署执行方案,实际上必须让一个模型独占一块显卡,这无疑会造成极大的资源浪费。

一个比较有代表性的部署代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# stable.py

from io import BytesIO
from fastapi import FastAPI
from fastapi.responses import Response
import torch

from ray import serve


app = FastAPI()

@serve.deployment(num_replicas=1, route_prefix="/")
@serve.ingress(app)
class APIIngress:
def __init__(self, diffusion_model_handle) -> None:
self.handle = diffusion_model_handle

@app.get(
"/imagine",
responses={200: {"content": {"image/png": {}}}},
response_class=Response,
)
async def generate(self, prompt: str, img_size: int = 512):
assert len(prompt), "prompt parameter cannot be empty"

image_ref = await self.handle.generate.remote(prompt, img_size=img_size)
image = await image_ref
file_stream = BytesIO()
image.save(file_stream, "PNG")
return Response(content=file_stream.getvalue(), media_type="image/png")


@serve.deployment(
ray_actor_options={"num_gpus": 0.5},
autoscaling_config={
"min_replicas": 0,
"max_replicas": 8,
"initial_replicas": 2,
"target_num_ongoing_requests_per_replica": 1,
"downscale_delay_s": 20,
"upscale_delay_s": 0},
)
class StableDiffusionV2:
def __init__(self):
from diffusers import EulerDiscreteScheduler, StableDiffusionPipeline

model_id = "stabilityai/stable-diffusion-2"

scheduler = EulerDiscreteScheduler.from_pretrained(
model_id, subfolder="scheduler"
)
self.pipe = StableDiffusionPipeline.from_pretrained(
model_id, scheduler=scheduler, revision="fp16", torch_dtype=torch.float16
)
self.pipe = self.pipe.to("cuda")

def generate(self, prompt: str, img_size: int = 512):
assert len(prompt), "prompt parameter cannot be empty"

image = self.pipe(prompt, height=img_size, width=img_size).images[0]
return image


entrypoint = APIIngress.bind(StableDiffusionV2.bind())

其中可以看到有非常细致的资源配置方案:

1
2
3
4
5
6
7
8
9
10
@serve.deployment(
ray_actor_options={"num_gpus": 0.5},
autoscaling_config={
"min_replicas": 0,
"max_replicas": 8,
"initial_replicas": 2,
"target_num_ongoing_requests_per_replica": 1,
"downscale_delay_s": 20,
"upscale_delay_s": 0},
)

ray 作为商业化非常成功的开源项目,还有如下优点:

  1. 有较为成熟的资源管理系统,方便资源管理和调配。
  2. 有较为成熟的 autoscale 系统,当某一模型使用量增多时,会自动拉起新的模型提供服务。也会自动为某些使用频率较低的模型释放资源。

完整的部署方案参考我之前写的 blog: https://blog.chivier.site/2023/08/31/2023/Ray-deployment-draft/

Ray 相关文档: - autoscale 部署: https://docs.ray.io/en/latest/serve/advanced-guides/advanced-autoscaling.html?highlight=autoscale - dashboard 使用: https://docs.ray.io/en/latest/serve/monitoring.html - ray serve 部署案例: https://docs.ray.io/en/latest/serve/tutorials/index.html - 多模型部署: https://docs.ray.io/en/latest/serve/multi-app.html

应对超高并发场景的思考

对短期和临时的超高并发场景下,可以临时租用 Serverless 计算资源,按需用量计费使用。或者使用合适的 prompt 技术,使用 OpenAI 等公司已有的 API 服务。

某些时候,我们确实可能会出现一些应用使用的旺季,这种时候可以考虑使用租用 Aws 的 Kubernetes 集群,通过和 ray serve 类似的配置方案实现更强的拓展性。同时由于使用的是按需计价的模式,可以极大的节约成本。近期我还在对 KubeRay 和相关项目进行研究和学习。这里也只能给出这样的简单方案,更多的方案细节需要进行更深入的研究。


LLM Model Deployment Improvement Suggestion
http://blog.chivier.site/2023-09-18/2023/LLM-Model-Deployment-Improvement-Suggestion/
Author
Chivier Humber
Posted on
September 18, 2023
Licensed under