我们如何通过大规模部署低成本开源人工智能技术节省数万美元
本文最初发布于 OpenSauced 官方博客。
当第一次使用生成式 AI 构建 AI 应用程序时,你可能会在项目的某个阶段使用 OpenAI API。而且,理由很充分!他们的 API 结构良好,速度快,并且有很棒的支持库。在规模比较小或刚开始时,使用 OpenAI 可能还相对比较经济。也有大量非常好的学习材料可以引导你完成构建 AI 应用程序的过程,借助 OpenAI API 理解这项复杂的技术。
最近,我个人最喜欢的其中一种 OpenAI 资源是 OpenAI Cookbook:其中的内容很适合初学者,他们可以从这里学习不同的模型是如何工作的,学习如何利用 AI 领域的诸多前沿技术,以及如何将数据与 AI 工作负载相集成。
然而,一旦你需要扩展生成式 AI 的操作,你很快就会遇到一个相当大的障碍:成本。一旦你开始通过 GPT-4 甚至成本更低的 GPT-3.5 模型生成数千(最终数万)条文本,你很快就会发现,你的 OpenAI 账单也会增长到每月数千美元。
值得庆幸的是,对于小型敏捷团队来说,有很多很好的低成本开源技术可供他们选择。他们可以部署这些技术,利用其中最新、最好且非常可靠的开源模型重新实现与 OpenAI 兼容的 API(在许多情况下,可以与 GPT 3.5 类模型的性能相媲美)。
在 OpenSauced,我们在为 人工智能新产品 StarSearch 构建基础设施时就遇到了这样的情况:我们需要一个数据管道,它可以不断地获取 GitHub 问题及 pull 请求的摘要和向量嵌入。这样,我们就可以在我们的向量存储中进行“大海捞针”似的余弦相似度搜索,并将其作为检索增强生成(RAG)流程的一部分。RAG 是一种非常流行的技术,通过它可以为大型语言模型提供额外的上下文和搜索结果,而它的基础数据中并没有包含这些信息。通过这种方式,LLM 就可以使用你提供的上下文数据来“增强”查询,并提供更准确的答案。
基于向量存储的余弦相似度搜索可以进一步增强 RAG 流:因为我们的大部分数据是非结构化的,很难通过全文搜索进行解析,所以对于数据库中我们想要搜索的行,我们利用 AI 生成了相关行的摘要,并以此为基础创建了向量嵌入。向量实际上只是一个数字列表,但它们代表了 embedding 机器学习模型的“理解”,搭配查询向量嵌入就可以针对终端用户的问题找出“最近邻”数据。
最初,对于 RAG 数据管道的摘要生成部分,我们直接使用了 OpenAI。我们希望借此了解 GitHub 上排名前 4 万多的存储库的相关事件和社区动态。这样一来,任何人就都可以查询获得有关开源生态系统中最杰出项目的独特见解。但是,由于新增的问题和 pull 请求事件总是流经这个管道,所以这 4 万多个存储库每天会有超过 10 万个新事件流过,我们需要为它们生成摘要:大量的 OpenAI API 调用!
在这种规模下,我们很快就遇到了“成本”瓶颈:我们考虑进一步优化对 OpenAI API 的使用,减少总体使用量。但我们觉得,我们可以使用开源技术以更低的成本实现同样的目标规模。
虽然这篇文章不会太深入地介绍我们如何实现 StarSearch 的 RAG 部分,但我将介绍下我们如何赋能我们的基础设施,使它能够使用成千上万的 GitHub 事件生成 AI 摘要,并使其成为使用 vLLM 和 Kubernetes 进行最近邻搜索的一部分。这是 StarSearch 能够揭示各种技术的相关信息并“了解”整个开源生态系统动态的关键所在。
进一步了解 RAG 和向量搜索,可以查阅以下链接:
从头开始构建检索增强生成(RAG)应用程序
生成式 AI 应用程序中的向量数据库
余弦相似度全解
在本地运行开源推理引擎
如今,得益于开源生态系统的强大功能和独创性,我们可以在自己的硬件上运行 AI 模型并进行“生成式推理”,而且有很多很好的选择。
其中最突出的是 llama.cpp、vLLM、llamafile、llm、gpt4all 和 Huggingface transformer。Ollama 是我个人最喜欢的模型之一:借助它我可以在 MacBook 的命令行上利用一条简单的命令ollama run
运行 LLM 程序。在开源 AI 领域,所有这些模型都独具特色,都为你提供了一种非常可靠的方式,让你能够在自己的硬件上运行开源大型语言模型(如 Meta 的 llama3, Mistral 的 mixtral 模型等),而不需要第三方 API。
更重要的也许是,这些软件针对在消费级硬件(如个人笔记本电脑和游戏电脑)上运行模型做了很好地的优化:不需要企业级 GPU 集群或昂贵的第三方服务来生成文本!你今天就可以开始使用开源技术在笔记本电脑上构建 AI 应用程序,无需第三方 API。
对于 StarSearch,我们就是这样将生成式 AI 管道从 OpenAI 转移到我们在 Kubernetes 上运行的服务:我从简单的 Ollama 开始,在我的笔记本电脑本地运行一个 Mistral 模型。然后,我开始改造 OpenAI 数据管道,从我们的数据库读取数据并开始使用本地的 Ollama 服务器生成摘要。与许多其他推理引擎一样,Ollama 提供了与 OpenAI 兼容的 API。使用它,我不需要重写很多客户端代码:只需将 OpenAI API 端点替换为指向 Ollama 的localhost
。
最后,我在使用 Ollama 时遇到了一个真正的瓶颈:它不支持客户端并发。而且,在我们的目标规模下,在任何给定的时间,我们可能需要几十个数据管道微服务运行器同时处理来自生成式 AI 服务的批量摘要。这样我们才能跟上 GitHub 上 4 万多个存储库的持续负载。显然,OpenAI API 可以处理这种负载,但我们如何使自己的服务也具备这项能力呢?
最终,我找到了 vLLM(一个快速的推理运行器)。它可以在与 OpenAI 兼容的 API 后面为多个客户端提供服务,并在推理时利用给定计算机上的多个 GPU 进行请求批处理及有效使用“PagedAttention”。和 Ollama 一样,vLLM 社区也提供了一个容器运行时镜像,使用户可以很容易地在许多不同的生产平台上使用它。这太好了!
请注意 :Ollama 最近合并了一些更改以支持并发客户端。在撰写本文时,主上游镜像尚未提供这方面的支持,但我非常期待看看它与其他多客户端推理引擎相比会有怎样的表现!
要在本地运行 vLLM,你需要一个 Linux 系统和一个 Python 运行时:
python -m vllm.entrypoints.openai.api_server \
--model mistralai/Mistral-7B-Instruct-v0.2
上述命令会启动一个与 OpenAI 兼容的服务器,稍后你可以通过 8000 端口访问它:
curl http://localhost:8000/v1/models
{
"object": "list",
"data": [
{
"id": "mistralai/Mistral-7B-Instruct-v0.2",
"object": "model",
"created": 1715528945,
"owned_by": "vllm",
"root": "mistralai/Mistral-7B-Instruct-v0.2",
"parent": null,
"permission": [
{
"id": "modelperm-020c373d027347aab5ffbb73cc20a688",
"object": "model_permission",
"created": 1715528945,
"allow_create_engine": false,
"allow_sampling": true,
"allow_logprobs": true,
"allow_search_indices": false,
"allow_view": true,
"allow_fine_tuning": false,
"organization": "*",
"group": null,
"is_blocking": false
}
]
}
]
}
此外,还可以在容器中运行与 OpenAI 兼容的 API。在 Linux 系统上,你可以使用 docker:
docker run --runtime nvidia --gpus all \
-v ~/.cache/huggingface:/root/.cache/huggingface \
-p 8000:8000 \
--ipc=host \
vllm/vllm-openai:latest \
--model mistralai/Mistral-7B-Instruct-v0.2
上述代码会在我的 Linux 机器上挂载本地 Huggingface 缓存,并使用主机网络。然后,还是使用 localhost,我们就可以访问运行在 Docker 中的与 OpenAI 兼容的服务器。现在,让我们聊个天:
curl localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "TheBloke/Mistral-7B-Instruct-v0.2-AWQ",
"messages": [
{"role": "user", "content": "Who won the world series in 2020?"}
]
}'
{
"id": "cmpl-9f8b1a17ee814b5db6a58fdfae107977",
"object": "chat.completion",
"created": 1715529007,
"model": "mistralai/Mistral-7B-Instruct-v0.2",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "The Major League Baseball (MLB) World Series in 2020 was won by the Tampa Bay Rays. They defeated the Los Angeles Dodgers in six games to secure their first-ever World Series title. The series took place from October 20 to October 27, 2020, at Globe Life Field in Arlington, Texas."
},
"logprobs": null,
"finish_reason": "stop",
"stop_reason": null
}
],
"usage": {
"prompt_tokens": 21,
"total_tokens": 136,
"completion_tokens": 115
}
}
在本地运行 vLLM,对于测试、开发和推理实验来说都没有问题,但是在我们的目标规模下,我知道我们需要某种环境,可以轻松地处理任意数量的 GPU 计算实例,还可以按需扩展,并且要用一个模型无关的服务对 vLLM 进行负载均衡,使数据管道微服务可以以生产速率访问这个服务:Kubernetes 登场,一个为人熟知的流行的容器编排系统!
在我看来,这对 Kubernetes 来说是一个完美的用例,它可以相对完美地扩展类似 OpenAI API 的内部 AI 服务。
最终的部署架构如下:
在节点池中部署任意数量的 Kubernetes 节点,每个节点配有任意数量的 GPU
根据托管 Kubernetes 服务提供程序说明安装 GPU 驱动程序。我们使用了 Azure AKS,他们 对在集群上利用 GPU 做了这些说明。
为 vLLM 部署一个 daemonset,每个节点用一个 GPU 运行
部署一个 Kubernetes 服务,对 vLLM 的请求做负载均衡
如果你按照上面介绍的内容做了,那么现在你应该已经启动并运行了一个 Kubernetes 集群,就像通过一个托管 Kubernetes 提供程序一样,并且还在配有 GPU 的节点上安装了必要的 GPU 驱动程序。
我们的服务是在 Azure AKS 上部署的,我们需要运行一个 daemonset 在每个带有 GPU 的节点上安装 Nvidia 驱动程序:
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nvidia-device-plugin-daemonset
namespace: gpu-resources
spec:
selector:
matchLabels:
name: nvidia-device-plugin-ds
template:
metadata:
labels:
name: nvidia-device-plugin-ds
spec:
containers:
image: mcr.microsoft.com/oss/nvidia/k8s-device-plugin:v0.14.1
name: nvidia-device-plugin-ctr
securityContext:
capabilities:
drop:
All
volumeMounts:
mountPath: /var/lib/kubelet/device-plugins
name: device-plugin
nodeSelector:
accelerator: nvidia
tolerations:
key: CriticalAddonsOnly
operator: Exists
effect: NoSchedule
key: nvidia.com/gpu
operator: Exists
volumes:
hostPath:
path: /var/lib/kubelet/device-plugins
type: ""
name: device-plugin
这个 daemonset 在每个具有节点选择器accelerator: nvidia
的节点上安装 Nvidia 设备插件 pod,并且可以容忍系统的一些污点(taints)。同样,这或多或少是特定于平台的,但这使我们的 AKS 集群能够为带有 GPU 的节点提供必要的驱动程序,以便 vLLM 可以充分利用这些计算单元。
最终,我们得到了一个集群节点配置,其中包含默认节点和带有 GPU 的节点:
❯ kubectl get nodes -A
NAME STATUS ROLES AGE VERSION
defaultpool-88943984-0 Ready <none> 5d v1.29.2
defaultpool-88943984-1 Ready <none> 5d v1.29.2
gpupool-42074538-0 Ready <none> 41h v1.29.2
gpupool-42074538-1 Ready <none> 41h v1.29.2
gpupool-42074538-2 Ready <none> 41h v1.29.2
gpupool-42074538-3 Ready <none> 41h v1.29.2
gpupool-42074538-4 Ready <none> 41h v1.29.2
每个节点都有一个 GPU 设备插件 pod,由 daemonset 管理,其中安装了驱动程序:
❯ kubectl get daemonsets.apps -n gpu-resources
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
nvidia-device-plugin-daemonset 5 5 5 5 5 accelerator=nvidia 41h
对于这个设置,有一点需要注意:每个 GPU 节点都有一个accelerator: nvidia
标签和nvidia.com/gpu
污点 。这是为了确保其他 pod 不会被调度到这些节点上,因为 vLLM 预计会消耗每个节点上的所有计算和 GPU 资源。
为了充分利用集群上部署的每个 GPU,我们可以在每个 Nvidia GPU 节点额外部署一个 vLLM daemonset :
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: vllm-daemonset-ec9831c8
namespace: vllm-ns
spec:
selector:
matchLabels:
app: vllm
template:
metadata:
labels:
app: vllm
spec:
containers:
args:
--model
mistralai/Mistral-7B-Instruct-v0.2
--gpu-memory-utilization
"0.95"
--enforce-eager
env:
name: HUGGING_FACE_HUB_TOKEN
valueFrom:
secretKeyRef:
key: HUGGINGFACE_TOKEN
name: vllm-huggingface-token
image: vllm/vllm-openai:latest
name: vllm
ports:
containerPort: 8000
protocol: TCP
resources:
limits:
"1" :
nodeSelector:
accelerator: nvidia
tolerations:
effect: NoSchedule
key: nvidia.com/gpu
operator: Exists
我们来分析下这里发生了什么:
首先,我们为集群上的 vLLM daemonset pod 创建元数据和标签选择器。然后,在容器规范中,我们把参数提供给在集群上运行的 vLLM 容器。这里你会注意到:我们的这个部署使用了大约 95% 的 GPU 内存,我们使用了 CUDA 饥渴模式(这有助于在减少内存消耗的同时权衡推理性能)。我喜欢 vLLM 的一个原因是,它有许多调优选项,并且可以在不同的硬件上运行:有许多功能可用于调整推理方式或硬件的使用方式。要了解更多信息,请 阅读 vLLM 文档。
接下来,你会注意到,我们提供了一个 Huggingface token:这样 vLLM 就可以从 Huggingface 的 API 中拉取模型,在我们已经获得访问权限的时候绕过“门禁”模型。
接下来,我们为该 pod 暴露端口 8000。稍后,服务可以使用该端口来选择这些 pod,这样就可以提供一种模型无关的方法通过 8000 端口访问负载均衡端点,进而访问我们部署的任何 vLLM pod。然后,我们使用了一个nvidia.com/gpu
资源(它是由 Nvidia 设备插件 daemonset 作为节点级资源提供的——同样,这取决于你的 Kubernetes 提供方和安装 GPU 驱动程序的方式,这可能会有所不同)。最后,我们提供了相同的节点选择器和 taint toleration,以确保 vLLM 仅在 GPU 节点上运行!现在,当部署完成时,我们将看到 vLLM daemonset 已成功部署到每个 GPU 节点上:
❯ kubectl get daemonsets.apps -n vllm-ns
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
vllm-daemonset-ec9831c8 5 5 5 5 5 accelerator=nvidia 41h
为了向集群内的其他微服务提供与 OpenAI 类似的 API,我们可以用一个 Kubernetes 服务在 vllm 命名空间中选择 vllm pod:
apiVersion: v1
kind: Service
metadata:
name: vllm-service
namespace: vllm-ns
spec:
ports:
port: 80
protocol: TCP
targetPort: 8000
selector:
app: vllm
sessionAffinity: None
type: ClusterIP
只是选择app: vllm
pod 并定向到 vLLM 8000 端口。然后,内部 Kubernetes DNS 服务器会获取该配置,而我们就可以使用解析后的 vllm-service.vllm-ns 端点将负载平衡到其中一个 vLLM API 上。
现在,我们访问下这个 vLLM Kubernetes 服务端点:
# hitting the vllm-service internal api endpoint resolved by Kubernetes DNS
curl vllm-service.vllm-ns.svc.cluster.local/v1/chat \
-H "Content-Type: application/json" \
-d '{
"model": "mistralai/Mistral-7B-Instruct-v0.2",
"prompt": "Why is the sky blue?"
}'
Kubernetes 内部服务域名 vllm-service.vllm-ns 将解析到其中一个运行 vLLM daemonset 的节点上(同样,在所有运行的 vLLM pod 之间进行负载均衡),并返回提示"Why is the sky blue?(为什么天空是蓝的?)”的推理结果:
{
"id": "cmpl-76cf74f9b05c4026aef7d64c06c681c4",
"object": "chat.completion",
"created": 1715533000,
"model": "mistralai/Mistral-7B-Instruct-v0.2",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "The color of the sky appears blue due to a natural phenomenon called Rayleigh scattering. As sunlight reaches Earth's atmosphere, it interacts with molecules and particles in the air, such as nitrogen and oxygen. These particles scatter short-wavelength light, like blue and violet light, more than longer wavelengths, like red, orange, and yellow. However, we perceive the sky as blue and not violet because our eyes are more sensitive to blue light and because sunlight reaches us more abundantly in the blue part of the spectrum.\n\nAdditionally, some of the violet light gets absorbed by the ozone layer in the stratosphere, which prevents us from seeing a violet sky. At sunrise and sunset, the sky can take on hues of red, orange, and pink due to the scattering of sunlight through the Earth's atmosphere at those angles."
},
"logprobs": null,
"finish_reason": "stop",
"stop_reason": null
}
],
"usage": {
"prompt_tokens": 15,
"total_tokens": 201,
"completion_tokens": 186
}
}
最终,无需使用昂贵的第三方 API,我们就为在集群上运行的内部微服务提供了一种生成摘要的方法:我们发现,使用 Mistral 模型得到的结果非常好。对于这种规模的用例,使用我们自己在 GPU 上运行的服务明显更划算。
你可以在此基础上进行扩展,为内部服务提供一些额外的网络策略或配置,甚至添加一个入口控制器,将其作为服务提供给集群外的其他服务。能做的事情很多!祝你好运!
如果你想试用 StarSearch,欢迎加入我们的 等待列表。
原文链接:
https://opensauced.pizza/blog/how-we-saved-thousands-of-dollars-deploying-low-cost-open-source-ai-technologies
声明:本文为 InfoQ 翻译,未经许可禁止转载。
德国再次拥抱Linux:数万系统从windows迁出,能否避开二十年前的“坑”?
内测活动出bug损失数百万,京东启动追责;贾扬清评大模型价格战:降价拍脑袋就能做;Kotlin 2.0正式发布 | Q资讯
微信扫码关注该文公众号作者