部署大语言模型 (LLM) Twin 应用的推理流水线是机器学习 (ML) 应用生命周期中的关键阶段。这是为业务创造最大价值的地方,使模型可供终端用户访问。然而,成功部署 AI 模型可能具有挑战性,因为这些模型需要昂贵的计算资源以及对最新特征的访问,以运行推理。为应对这些限制,必须精心设计部署策略,以确保满足应用程序的需求,如延迟、吞吐量和成本。
在处理 LLM 时,我们需要考虑第 8 章中介绍的推理优化技术,如模型量化。此外,为自动化部署过程,我们还必须采用 MLOps 最佳实践,例如使用模型注册表来对模型进行版本控制,并在整个基础设施中共享模型。
为理解如何设计 LLM Twin 的部署架构,我们首先将查看三种可供选择的部署类型:在线实时推理、异步推理和离线批量转换。此外,为更好地理解如何为 LLM Twin 用例选择适合的选项,我们将带您快速了解一些在做出架构决策前必须考虑的关键标准,如延迟、吞吐量、数据和基础设施。我们还将权衡模型服务中单体架构和微服务架构的优缺点,这是一个可能对服务的可扩展性和可维护性产生重大影响的决策。
在掌握了各种设计选择后,我们将专注于了解 LLM Twin 推理流水线的部署策略。随后,我们将为您提供一个端到端教程,演示如何部署 LLM Twin 服务,包括将自定义微调的 LLM 部署到 AWS SageMaker 端点,并实现一个 FastAPI 服务器作为用户的集中入口。最后,我们将以简要讨论自动扩展策略并介绍如何在 SageMaker 上使用它们作为本章的总结。
因此,本章将涵盖以下主题:
- 选择部署类型的标准
- 理解推理部署类型
- 模型服务中的单体架构与微服务架构
- 探索 LLM Twin 推理流水线的部署策略
- 部署 LLM Twin 服务
- 自动扩展功能以应对使用高峰
选择部署类型的标准
在部署 ML 模型时,首先需要理解每个 ML 应用程序中的四个基本需求:吞吐量、延迟、数据和基础设施。理解这些需求及其相互关系至关重要,因为在设计模型的部署架构时,总要在这四者之间做出权衡,这会直接影响用户体验。例如,模型部署应优先优化低延迟还是高吞吐量?
吞吐量和延迟
- 吞吐量:指系统在给定时间内可以处理的推理请求数量,通常以每秒请求数(RPS)来衡量。吞吐量对于需要处理大量请求的 ML 模型部署至关重要,确保系统能够高效处理大量请求而不会成为瓶颈。高吞吐量通常需要可扩展且强大的基础设施,例如具有多个高端 GPU 的机器或集群。
- 延迟:指系统从接收到单个推理请求到返回结果所需的时间。在实时应用中延迟尤为重要,例如实时用户交互、欺诈检测或任何需要即时反馈的系统。延迟通常包括网络 I/O、序列化和反序列化以及 LLM 的推理时间。
低延迟系统通常需要优化且成本更高的基础设施,例如更快的处理器、较低的网络延迟,甚至边缘计算以减少数据传输的距离。当服务能够成功并行处理多个查询时,较低的延迟会带来更高的吞吐量。例如,如果服务处理请求的时间为 100 毫秒,吞吐量为每秒 10 个请求;如果延迟降到每请求 10 毫秒,吞吐量可提高到每秒 100 个请求。
为了进一步优化,许多 ML 应用会采用批处理策略,将多个数据样本同时传递给模型。这种情况下,较低延迟可能导致较低吞吐量;换句话说,较高的延迟会对应较高的吞吐量。例如,如果在 100 毫秒内处理 20 个批量请求,延迟为 100 毫秒,吞吐量为每秒 200 个请求。如果在 200 毫秒内处理 60 个请求,延迟为 200 毫秒,而吞吐量则上升到每秒 300 个请求。因此,即使在服务时批量处理请求,也要考虑用户体验所接受的最小延迟。
数据
在 ML 系统中,数据无处不在。但在模型服务中,我们主要关注模型的输入和输出数据,包括数据的格式、体积和复杂性。数据是推理过程的基础,数据的特性(如大小和类型)决定了系统如何配置和优化以实现高效处理。数据的类型和大小直接影响延迟和吞吐量,因为更复杂或庞大的数据可能需要更长时间来处理。例如,输入结构化数据并输出概率的模型设计与接受文本(甚至图像)输入并输出字符数组的 LLM 设计完全不同。
基础设施
基础设施指支持 ML 模型部署和运行的底层硬件、软件、网络和系统架构,包括计算资源、内存、存储、网络组件和软件栈:
- 高吞吐量:需要可扩展的基础设施来管理大数据量和高请求率,通常通过并行处理、分布式系统和高端 GPU 实现。
- 低延迟:为了实现低延迟,必须优化基础设施,如使用更快的 CPU、GPU 或专用硬件。在批量处理请求的情况下优化系统以降低延迟通常需要在高吞吐量上做出牺牲,这可能导致硬件未满负荷运行,增加单个请求的总处理成本。因此,选择适合需求的硬件至关重要,以优化成本。
设计基础设施时必须满足特定数据需求,包括选择合适的存储解决方案来处理大型数据集,并实现快速检索机制以确保高效数据访问。例如,离线训练通常更关注吞吐量,而在线推理通常更关注延迟。
在选择特定部署类型之前,需要问自己以下问题:
- 吞吐量要求是什么?应根据吞吐量的最小、平均和最大统计数据来做决定。
- 系统必须同时处理多少请求?(1 个、10 个、1000 个、100 万个等)
- 延迟要求是什么?(1 毫秒、10 毫秒、1 秒等)
- 系统应如何扩展?例如,应该查看 CPU 负载、请求数量、队列大小、数据大小或它们的组合。
- 成本要求是什么?
- 我们处理的数据是什么类型?例如,是图像、文本还是表格数据?
- 数据的大小是多少?(100 MB、1 GB、10 GB)
深入思考这些问题会直接影响应用的用户体验,最终决定产品的成功与否。即使模型再优秀,如果用户需要等待过长时间才能获得响应或应用频繁崩溃,用户可能会转而使用稳定性更高的替代品。例如,Google 在 2016 年的一项研究中发现,如果移动网站加载时间超过三秒,53% 的访问会被放弃:Google 研究。
接下来,我们将探讨三种可用于服务模型的部署架构。
理解推理部署类型
如图 10.1 所示,模型服务可以选择以下三种基本部署类型:
- 在线实时推理
- 异步推理
- 离线批量转换
在选择其中一种设计时,需要在延迟、吞吐量和成本之间进行权衡。必须考虑数据的访问方式和所使用的基础设施,还要考虑用户如何与模型交互。例如,用户是否直接使用模型(如聊天机器人),还是模型隐藏在系统中(如检查输入或输出是否安全的分类器)?
还需要考虑预测结果的新鲜度。例如,如果在您的用例中可以接受延迟预测,那么以离线批量模式服务模型可能更易于实现。否则,您需要实时服务模型,这对基础设施的要求更高。此外,还要考虑应用的流量特征,思考诸如“应用程序是否会持续使用,还是会有流量高峰然后趋于平稳?”的问题。
带着这些考虑,我们来探索三种主要的 ML 部署类型。
在线实时推理
在实时推理中,架构基于一个通过 HTTP 请求访问的服务器。最常见的选择是实现 REST API 或 gRPC 服务器。REST API 更易于访问但速度较慢,使用 JSON 在客户端和服务器之间传递数据。通常在对外提供服务的情况下选择 REST API,例如 OpenAI 的 API 就采用了 REST API 协议。
相比之下,gRPC 使 ML 服务器更快,但可能会降低其灵活性和通用性。gRPC 需要在客户端应用中实现 protobuf 模式,这比 JSON 结构更繁琐,但 protobuf 对象可以编译为字节流,使网络传输更快。因此,gRPC 协议通常用于同一 ML 系统中的内部服务。
使用实时推理方法时,客户端向 ML 服务发送 HTTP 请求,服务立即处理请求并在相同响应中返回结果。这种同步交互意味着客户端等待结果后才能继续。为了高效运行,基础设施必须支持低延迟、高响应的 ML 服务,通常部署在快速、可扩展的服务器上。负载均衡用于均匀分配流量,自动扩展则确保系统可以处理不同的负载,高可用性则保证服务始终正常运行。
例如,在与 LLM 交互时(如发送请求给聊天机器人或 API),你期待即时得到预测结果。LLM 服务(如 ChatGPT 或 Claude)常使用 WebSockets 将每个生成的 token 单独传递给用户,使交互更具响应性。其他示例包括用于检索增强生成(RAG)的嵌入模型或重新排序模型,或 TikTok 等平台上的实时推荐引擎。
实时推理的直接客户端-服务器交互使其成为需要即时响应的应用的理想选择,如聊天机器人或实时推荐。然而,这种方法可能难以扩展,并且在低流量时期会导致资源未充分利用。
异步推理
在异步推理中,客户端向 ML 服务发送请求,服务确认请求并将其放入队列以待处理。不同于实时推理,客户端不等待即时响应。相反,ML 服务异步处理请求,这需要一个强大的基础设施来队列消息以供稍后处理。
当结果准备好后,可以使用多种方法将其发送给客户端。根据结果大小,可以将其放入不同的队列或专用的对象存储中以保存结果。客户端可以采用轮询机制定期检查是否有新结果,或者采用推送策略,实现一个通知系统在结果准备好时告知客户端。
异步推理更加高效地利用资源,不必同时处理所有请求,而是可以定义一个最大并行运行的机器数量来处理消息。因为请求在队列中等待,直到有机器可用来处理它们。此外,它可以应对请求高峰而不会导致超时。例如,在一个电商网站上,通常有 10 个请求每秒,由两台机器处理。由于促销活动,访问量激增到 100 个请求每秒。与其将虚拟机数量增加到 10 倍来应对这一需求(增加成本),不如将请求排队,并由同样的两台机器按自身节奏处理,避免失败。
当请求的任务需要较长时间完成时,异步架构也非常适用。例如,如果任务超过五分钟,则不希望阻塞客户端等待响应。
尽管异步推理带来了显著的优势,但也有权衡之处。它引入了更高的延迟,因此不适合时间敏感的应用。此外,它增加了实现和基础设施的复杂性。根据设计选择,这种架构位于在线和离线模式之间,提供了平衡的优缺点。
例如,这种设计非常适合延迟要求不高、成本优化优先的问题,因此在文档关键字提取、使用 LLM 进行摘要或视频深度伪造模型中很受欢迎。但如果设计了高效的自动扩展系统,使队列中的请求处理速度足够快,则可以将此设计应用于其他用例,如电商的在线推荐。最终取决于愿意投入的计算资源,以满足应用的预期。
离线批量转换
批量转换用于一次性处理大量数据,可以按计划定期执行或手动触发。在批量转换架构中,ML 服务从存储系统中提取数据,进行单次操作处理,然后将结果存储回存储系统中。存储系统可以是 AWS S3 等对象存储,或 GCP BigQuery 等数据仓库。
不同于异步推理架构,批量转换设计针对高吞吐量进行了优化,同时对延迟要求较为宽松。当实时预测不是必需时,这种方法可以显著降低成本,因为大批量处理数据是最经济的方式。此外,批量转换架构是服务模型最简单的方式,加快了开发时间。
客户端直接从数据存储中提取结果,与 ML 服务的交互是解耦的。采用这种方法时,客户端无需等待 ML 服务处理其输入,但同时也失去了随时请求新结果的灵活性。可以将存储数据的系统视为一个大型缓存,客户端可以从中提取所需的数据。如果想让应用更具响应性,可以在处理完成时通知客户端,从而提取结果。
然而,此方法总会在计算预测结果与其被消费之间引入延迟。因此,并非所有应用都适合此设计。例如,在视频流媒体应用中实现推荐系统,预测的电影和电视剧延迟一天可能可以接受,因为用户不频繁消费这些内容。但如果为社交媒体平台设计推荐系统,延迟一天甚至一小时都是不可接受的,因为平台需要不断为用户提供新鲜内容。
批量转换在需要高吞吐量的场景中表现出色,例如数据分析或定期报告。然而,由于其高延迟特性,它并不适合实时应用,还需精心规划和调度,以有效管理大型数据集。因此,这是一种离线服务方式。
总结来看,我们探讨了服务 ML 模型的三种最常见架构。首先是在线实时推理,它在客户端请求预测时提供服务;接着是异步推理,位于在线和离线之间;最后是离线批量转换,用于处理大量数据并将其存储到数据存储中,供客户端稍后使用。
模型服务中的单体架构与微服务架构
在上一节中,我们探讨了三种不同的 ML 服务部署方法,架构差异主要体现在客户端与 ML 服务之间的交互方式,如通信协议、ML 服务的响应速度以及预测结果的实时性。
然而,另一个需要考虑的方面是 ML 服务自身的架构,它可以实现为一个单体服务器,也可以由多个微服务组成。这将影响 ML 服务的实现方式、维护方式以及扩展性。让我们来深入了解这两种选项。
单体架构
在单体架构中,LLM(或任何其他 ML 模型)及其关联的业务逻辑(包括预处理和后处理步骤)被整合为一个服务。对于项目初期,这种方法容易实现,因为所有内容都在一个代码库中。简化的结构使得小到中型项目的维护相对简单,因为更新和更改都可以在统一的系统内进行。
单体架构的主要挑战在于难以独立扩展各个组件。通常,LLM 需要 GPU 计算能力,而业务逻辑则主要依赖于 CPU 和 I/O 资源。因此,基础设施必须同时优化 GPU 和 CPU,这可能导致资源使用效率低下,例如在业务逻辑执行时 GPU 处于空闲状态,反之亦然。这种资源低效利用会带来不必要的额外成本。
此外,单体架构限制了灵活性,因为所有组件必须共享相同的技术栈和运行环境。例如,您可能希望用 Rust 或 C++ 运行 LLM,或使用 ONNX 或 TensorRT 进行编译,而业务逻辑保持在 Python 中。但在单一系统中实现这种区分非常困难。最后,将工作分配给不同团队较为复杂,往往会导致瓶颈并降低敏捷性。
微服务架构
微服务架构将推理流水线分解为独立的服务,通常将 LLM 服务和业务逻辑分成不同的组件。这些服务通过网络使用 REST 或 gRPC 等协议进行通信。
如图 10.3 所示,微服务架构的主要优势在于可以独立扩展每个组件。例如,由于 LLM 服务可能需要比业务逻辑更多的 GPU 资源,可以单独水平扩展 LLM 服务,而不影响其他组件。这样可以优化资源使用并降低成本,因为可以根据每个服务的需求使用不同类型的机器(如 GPU 和 CPU)。
例如,假设 LLM 推理时间较长,因此需要更多的 ML 服务副本来满足需求,但 GPU 虚拟机成本高昂。通过将两个组件解耦,您可以在 GPU 机器上仅运行所需部分,不让 GPU 虚拟机被可以在更廉价的机器上完成的其他计算任务阻塞。
因此,通过解耦组件,您可以按需进行水平扩展,以最低成本满足系统需求,从而实现成本效益高的解决方案。
此外,每个微服务可以采用最合适的技术栈,使团队能够独立创新和优化。
然而,微服务在部署和维护上引入了复杂性。每个服务都必须单独部署、监控和维护,这比管理单体系统更具挑战性。同时,服务之间的网络通信增加了延迟和潜在故障点,需要稳健的监控和弹性机制。
值得注意的是,将 ML 模型和业务逻辑解耦为两个服务的设计可以根据需要进一步扩展。例如,可以设置一个数据预处理服务,一个模型服务,以及一个数据后处理服务。根据延迟、吞吐量、数据和基础设施这四个支柱,您可以灵活设计最适合应用需求的架构。
在单体架构和微服务架构之间进行选择
选择服务 ML 模型的单体架构还是微服务架构,主要取决于应用的具体需求。对于优先考虑开发和维护简易性的较小团队或更简单的应用,单体架构可能是理想选择。对于不经常需要扩展的项目,它也是一个很好的起点。此外,如果 ML 模型较小,不需要 GPU 或使用更小更便宜的 GPU,那么在简化基础设施和降低成本之间权衡是值得考虑的。
另一方面,微服务的适应性和可扩展性使其非常适合较大且更复杂的系统,尤其是不同组件具有不同扩展需求或需要不同技术栈的系统。这种架构特别适合需要扩展特定系统部分(如 GPU 密集型的 LLM 服务)的场景。由于 LLM 需要强大的 GPU 机器,如 Nvidia A100、V100 或 A10g,这些机器成本高昂,微服务提供了在保持这些机器始终忙碌或在 GPU 空闲时快速缩减资源上的灵活性。但这种灵活性是以开发和运营复杂性增加为代价的。
一种常见的策略是从单体设计开始,随着项目的增长逐步将其解耦为多个服务。然而,为了在不增加过多复杂性和成本的情况下成功实现这一点,您需要从一开始就考虑到这种设计。例如,即使所有代码都在同一台机器上运行,也可以在软件层面完全解耦应用程序的各个模块。这使得在需要时更容易将这些模块迁移到不同的微服务中。例如,在使用 Python 时,可以将 ML 和业务逻辑实现为两个不同的 Python 模块,不让它们相互交互。然后,可以在更高的层次上将这两个模块“粘合”在一起,比如通过一个服务类,或直接集成到用于暴露应用的框架中(如 FastAPI)。
另一种选择是将 ML 和业务逻辑编写为两个不同的 Python 包,并以类似方式结合在一起。这种方式更好,因为它在开发时完全强制了二者的分离,但会增加一些开发复杂性。因此,主要思想是,如果您从单体架构开始,并希望将来迁移到微服务架构,设计时必须考虑模块化。否则,如果逻辑混合在一起,可能需要从头重写代码,增加大量开发时间,浪费资源。
单体架构提供了简洁和易于维护的优势,但灵活性和可扩展性受限;而微服务提供了扩展和创新的敏捷性,但需要更复杂的管理和运维实践。
探索 LLM Twin 推理流水线的部署策略
在理解了实现 LLM Twin 推理流水线部署策略的各种设计选项后,接下来探讨我们所做的具体决策。
我们的主要目标是开发一个支持内容创作的聊天机器人。为此,我们将顺序处理请求,并强调整体低延迟需求,因此选择了在线实时推理部署架构。
在单体架构与微服务架构的选择上,我们将 ML 服务拆分为包含业务逻辑的 REST API 服务器和针对运行 LLM 优化的 LLM 微服务。由于 LLM 推理需要强大的计算能力,并且可以进一步通过各种引擎优化延迟和内存使用,微服务架构最为合理。这种方式使我们可以基于不同规模的 LLM 快速调整基础设施。例如,运行一个 80 亿参数模型时,通过量化后可在配备 Nvidia A10G GPU 的单机上运行;而如果需要运行一个 300 亿参数的模型,则可以升级到 Nvidia A100 GPU。这样,我们只需升级 LLM 微服务,而无需修改 REST API。
如图 10.4 所示,在我们的用例中,大部分业务逻辑围绕 RAG(检索增强生成)展开。因此,我们将在业务微服务中执行 RAG 的检索和增强部分,同时包括上一章介绍的所有先进 RAG 技术,以优化预检索、检索和后检索步骤。
LLM 微服务则专注于优化 RAG 生成组件。最终,业务层会将用户查询、提示、答案以及其他中间步骤组成的提示追踪信息发送至提示监控流水线,我们将在第 11 章详细介绍。
总结来看,我们的方法是使用微服务架构实现一个在线实时 ML 服务,有效地将 LLM 和业务逻辑分成两个独立服务。
推理流水线接口由“特征/训练/推理”(FTI)架构定义。要运行流水线,需要以下两项:
- 用于 RAG 的实时特征,由特征流水线生成并从在线特征库(具体来说是 Qdrant 向量数据库)中查询。
- 由训练流水线生成的微调 LLM,从模型注册库中获取。
基于此,ML 服务的流程如下所示(见图 10.4):
- 用户通过 HTTP 请求发送查询。
- 用户输入通过第 4 章中实现的高级 RAG 检索模块检索出适当的上下文。
- 用户输入和检索的上下文通过专用的提示模板打包成最终提示。
- 提示通过 HTTP 请求发送到 LLM 微服务。
- 业务微服务等待生成的答案。
- 生成答案后,与用户输入及其他关键信息一起发送至提示监控流水线进行监控。
- 最终,生成的答案返回给用户。
实施架构的技术栈
如图 10.4 所示,我们选择以下技术栈:
- 向量数据库使用 Qdrant。
- 模型注册库使用 Hugging Face,这样我们可以公开分享模型,便于所有测试本书代码的用户使用。如果您不想运行训练流水线(这可能花费约 100 美元),可以直接使用我们提供的模型。模型注册库带来的共享性和可访问性正是其优势之一。
- 业务微服务采用 FastAPI 实现,因为它流行、易用且速度快。
- LLM 微服务将部署在 AWS SageMaker 上,利用 SageMaker 与 Hugging Face 深度学习容器(DLCs)的集成来部署模型。DLCs 是一种推理引擎,用于优化 LLM 的服务时间。提示监控流水线使用 Comet 实现,我们将在第 11 章详细讨论。
SageMaker 推理部署组件
SageMaker 推理部署由以下组件构成,我们将展示如何实现:
- SageMaker 端点:端点是由 SageMaker 托管的可扩展、安全的 API,用于从部署的模型中实现实时预测。应用可以通过 HTTP 请求与端点交互以获得实时预测结果。
- SageMaker 模型:模型是训练算法的产物,包含做出预测所需的信息(包括权重和计算逻辑)。可以创建多个模型并在不同配置或预测中使用。
- SageMaker 配置:配置定义了托管模型所需的硬件和软件设置,包括端点所需的资源类型和数量。端点配置用于创建或更新端点,提供灵活的部署和扩展。
- SageMaker 推理组件:这是连接模型和配置到端点的最后一块拼图,可以向端点部署多个模型,每个模型都有自己的资源配置。一旦部署完成,可以通过 Python 中的 InvokeEndpoint API 轻松访问模型。
这些组件共同构建了 SageMaker 中部署和管理 ML 模型的稳健基础设施,使得可扩展、安全且高效的实时预测成为可能。
其他流行的云平台也提供类似解决方案。例如,在 Azure 上,可以使用 Azure OpenAI 替代 Bedrock,用 Azure ML 替代 SageMaker。ML 部署工具(如 Hopsworks、Modal、Vertex AI、Seldon 和 BentoML)种类繁多,并可能不断变化。关键是理解用例需求,选择适合的工具。
训练流水线与推理流水线
在部署推理流水线之前,理解训练流水线与推理流水线之间的差异至关重要。尽管训练流水线用于训练,而推理流水线用于推理,这一点看似简单,但我们需要深入理解其技术方面的差异。
一个关键区别在于数据的处理和访问方式。训练过程中,数据通常以批量模式从离线存储中访问,优化吞吐量并确保数据溯源。例如,在 LLM Twin 架构中,我们使用 ZenML 工件批量访问、版本控制并跟踪提供给训练循环的数据。相比之下,推理流水线则需要一个在线数据库,优化低延迟以实现快速数据访问。我们将使用 Qdrant 向量数据库获取 RAG 所需的上下文,此时关注点从数据溯源和版本控制转向快速数据访问,以确保用户体验的流畅。此外,这两种流水线的输出也有显著不同。训练流水线的输出是存储在模型注册库中的训练模型权重,而推理流水线的输出是直接提供给用户的预测结果。
同时,两种流水线所需的基础设施也不同。训练流水线需要更多高性能机器,配备尽可能多的 GPU,因为训练涉及数据批处理和在内存中保存所有必要的梯度用于优化步骤,这使其计算强度极高。更多的计算能力和显存允许更大的批处理量(或更高的吞吐量),从而缩短训练时间并支持更多的实验。而推理流水线通常需要较少的计算资源,推理通常仅涉及单个样本或较小的批量传递给模型,无需优化步骤。
尽管存在这些差异,训练和推理流水线之间仍有一些重叠之处,尤其是在预处理和后处理步骤上。在训练和推理过程中应用相同的预处理和后处理函数及超参数至关重要。任何不一致之处都会导致所谓的“训练-服务偏差”,即模型在推理期间的表现与训练期间的表现有所偏离。
部署 LLM Twin 服务
最后一步是实现前一节介绍的架构,具体来说,我们将使用 AWS SageMaker 部署 LLM 微服务,并使用 FastAPI 部署业务微服务。在业务微服务中,将第 9 章编写的 RAG 逻辑与我们微调的 LLM Twin 集成,从而可以端到端测试推理流水线。
模型服务是任何 ML 应用生命周期中最关键的步骤之一,用户只有在完成这一阶段后才能与模型交互。如果服务架构设计不当,或基础设施未正常工作,即使模型再强大、出色也无济于事。从业务角度来看,只要用户无法正常交互,模型的价值几乎为零。例如,如果您拥有市场上最好的代码助手,但使用时延迟太高,或 API 调用不断崩溃,用户可能会转向一个性能稍逊但响应更快、稳定性更高的代码助手。
因此,在本节中,我们将展示如何:
- 将微调后的 LLM Twin 模型部署到 AWS SageMaker
- 编写推理客户端与部署的模型进行交互
- 在 FastAPI 中编写业务服务
- 将 RAG 逻辑集成到微调的 LLM 中
- 为 LLM 微服务实现自动扩展规则
使用 AWS SageMaker 实现 LLM 微服务
我们的目标是将存储在 Hugging Face 模型注册库中的 LLM Twin 模型部署到 Amazon SageMaker 作为在线实时推理端点。我们将利用 Hugging Face 的专用推理容器(称为 Hugging Face LLM DLC)来部署 LLM。
什么是 Hugging Face 的 DLC?
DLC 是专用的 Docker 镜像,预装了深度学习所需的框架和库,包括 Hugging Face 的 transformers、datasets 和 tokenizers 等流行工具。这些容器简化了训练和部署模型的流程,无需复杂的环境设置和优化。特别是 Hugging Face 推理 DLC,包含了一个完全集成的服务栈,大大简化了部署过程,降低了生产环境中服务深度学习模型所需的技术要求。
在服务模型时,DLC 由 Hugging Face 开发的 Text Generation Inference (TGI) 引擎提供支持: github.com/huggingface…
TGI 是一种开源的 LLM 部署和服务解决方案,提供了高性能文本生成,利用张量并行和动态批处理来优化 Hugging Face 上常用的开源 LLM(如 Mistral、Llama 和 Falcon)的性能。总结来说,DLC 提供的最强大功能包括:
- 张量并行:增强模型推理的计算效率
- 优化的 transformers 代码:用于推理,通过 flash-attention 最大化流行架构的性能:github.com/Dao-AILab/f…
- bitsandbytes 量化:在保持性能的同时减小模型大小,提高部署效率:github.com/bitsandbyte…
- 动态批处理请求:动态批处理来提高吞吐量
- 加速权重加载:使用 safetensors 加快模型初始化,减少启动时间:github.com/huggingface…
- Token 流式传输:通过服务器发送事件 (SSE) 支持实时交互
总的来说,我们的 LLM Twin 模型将在 DLC Docker 镜像中运行,监听请求、优化 LLM 以进行推理,并实时提供结果。这些 Docker 镜像将在 AWS SageMaker 上的推理端点中托管,可以通过 HTTP 请求访问。接下来我们将进行实现。我们将先部署 LLM,然后编写一个封装类与 SageMaker 推理端点交互。
配置 SageMaker 角色
第一步是创建适当的 AWS 身份和访问管理 (IAM) 用户和角色,以访问和部署 SageMaker 基础设施。AWS IAM 用于控制谁可以认证以及每个角色的访问权限。可以通过 IAM 创建新的用户(分配给个人)和新的角色(分配给基础设施中的其他实体,如 EC2 虚拟机)。
整个部署过程是自动化的。我们需要运行一些 CLI 命令,但首先请确保在 .env 文件中正确配置了 AWS_ACCESS_KEY、AWS_SECRET_KEY 和 AWS_REGION 环境变量。在这一步中,使用分配给管理员角色的凭据是最简单的方法,因为在接下来的步骤中,我们将创建一组更狭窄的 IAM 角色,供本章剩余部分使用。
配置好 .env 文件后,我们需要:
-
创建一个仅限于创建和删除部署所需资源的 IAM 用户,例如 SageMaker 本身、Elastic Container Registry (ECR) 和 S3。运行以下命令来创建它:
poetry poe create-sagemaker-role此命令将生成一个名为
sagemaker_user_credentials.json的 JSON 文件,其中包含新的 AWS 访问密钥和秘密密钥。从现在开始,我们将使用这些凭据来部署与 SageMaker 相关的所有内容,以确保我们仅修改与 SageMaker 关联的资源。否则,使用管理员帐户可能会意外修改其他 AWS 资源,导致额外费用或更改其他现有项目。因此,仅对特定用例分配狭窄角色是一种良好的实践。 -
将新凭据从 JSON 文件中提取出来,并更新 .env 文件中的 AWS_ACCESS_KEY 和 AWS_SECRET_KEY 变量。实现详情请参见:create_sagemaker_role.py。
-
创建一个 IAM 执行角色。我们将此角色附加到 SageMaker 部署中,使其能够代表我们访问其他 AWS 资源。这是云部署的标准做法,因为不需要为每个机器单独认证,可以附加一个角色,只允许它们访问基础设施中必要的资源。在这里,我们将提供 SageMaker 访问 AWS S3、CloudWatch 和 ECR 的权限。运行以下命令来创建角色:
poetry poe create-sagemaker-execution-role此命令将生成一个名为
sagemaker_execution_role.json的 JSON 文件,包含新角色的 Amazon 资源名称 (ARN)。ARN 是一个 ID,用于标识整个云基础设施中的 AWS 资源。从 JSON 文件中提取 ARN 值,并在 .env 文件中的 AWS_ARN_ROLE 变量中更新。实现详情请参见:create_execution_role.py。
如果遇到问题,请使用与 .env 文件中相同的 AWS 凭据配置 AWS CLI,并重复该过程。AWS CLI 的安装官方文档:AWS CLI 安装指南。
通过在 .env 文件中设置 IAM 用户和角色,我们将在接下来的步骤中自动加载并使用这些配置在 Python 设置对象中。现在,让我们进入实际的部署步骤。
将 LLM Twin 模型部署到 AWS SageMaker
通过一组 Python 类,AWS SageMaker 的部署过程已完全自动化,我们将在本章中详细讲解。本节的目的是帮助理解如何直接从 Python 配置 SageMaker 基础设施,因此不需要像标准教程那样逐步运行所有步骤,而只需理解代码即可。
我们可以通过一个简单的 CLI 命令来启动和完成整个 SageMaker 部署:poetry poe deploy-inference-endpoint。此命令将初始化图 10.5 所示的所有步骤,除了我们在上一步中创建和配置的 SageMaker AWS IAMs。
在本节中,我们将逐步讲解图 10.5 中的代码,帮助我们完全自动化部署流程,从 create_endpoint() 函数开始。最终,我们将测试 CLI 命令,并检查 AWS 控制台以确认部署是否成功。SageMaker 部署代码可在 GitHub 仓库 查看。
我们将采用自上而下的方法,从部署 LLM Twin 模型到 AWS SageMaker 的主函数开始讲解。在下面的函数中,我们首先使用 get_huggingface_llm_image_uri() 函数获取 Docker DLC 镜像的最新版本,然后将其连同资源管理器实例和部署服务传递给部署策略类:
def create_endpoint(endpoint_type=EndpointType.INFERENCE_COMPONENT_BASED):
llm_image = get_huggingface_llm_image_uri("huggingface", version=None)
resource_manager = ResourceManager()
deployment_service = DeploymentService(resource_manager=resource_manager)
SagemakerHuggingfaceStrategy(deployment_service).deploy(
role_arn=settings.ARN_ROLE,
llm_image=llm_image,
config=hugging_face_deploy_config,
endpoint_name=settings.SAGEMAKER_ENDPOINT_INFERENCE,
endpoint_config_name=settings.SAGEMAKER_ENDPOINT_CONFIG_INFERENCE,
gpu_instance_type=settings.GPU_INSTANCE_TYPE,
resources=model_resource_config,
endpoint_type=endpoint_type,
)
为充分理解部署过程,我们需要依次了解 create_endpoint() 函数中使用的三个类。首先,从 ResourceManager 类开始。该类的初始化方法通过使用 boto3(AWS 的 Python SDK)建立与 AWS SageMaker 的连接,从而提供与各类 AWS 服务(包括 SageMaker)交互的功能:
class ResourceManager:
def __init__(self) -> None:
self.sagemaker_client = boto3.client(
"sagemaker",
region_name=settings.AWS_REGION,
aws_access_key_id=settings.AWS_ACCESS_KEY,
aws_secret_access_key=settings.AWS_SECRET_KEY,
)
接下来,我们实现 endpoint_config_exists 方法,用于检查特定的 SageMaker 端点配置是否存在:
def endpoint_config_exists(self, endpoint_config_name: str) -> bool:
try:
self.sagemaker_client.describe_endpoint_config(EndpointConfigName=endpoint_config_name)
logger.info(f"Endpoint configuration '{endpoint_config_name}' exists.")
return True
except ClientError:
logger.info(f"Endpoint configuration '{endpoint_config_name}' does not exist.")
return False
该类还包含 endpoint_exists 方法,用于检查某个 SageMaker 端点是否存在:
def endpoint_exists(self, endpoint_name: str) -> bool:
try:
self.sagemaker_client.describe_endpoint(EndpointName=endpoint_name)
logger.info(f"Endpoint '{endpoint_name}' exists.")
return True
except self.sagemaker_client.exceptions.ResourceNotFoundException:
logger.info(f"Endpoint '{endpoint_name}' does not exist.")
return False
接下来,我们讨论 DeploymentService 类。在构造函数中,我们设置了 sagemaker_client,用于与 AWS SageMaker 交互,并创建了一个 ResourceManager 类的实例:
class DeploymentService:
def __init__(self, resource_manager):
self.sagemaker_client = boto3.client(
"sagemaker",
region_name=settings.AWS_REGION,
aws_access_key_id=settings.AWS_ACCESS_KEY,
aws_secret_access_key=settings.AWS_SECRET_KEY,
)
self.resource_manager = resource_manager
以上为创建 SageMaker 端点的关键实现组件,下一步我们将继续介绍如何利用这些组件完成部署流程。
deploy() 方法是 DeploymentService 类的核心,它负责协调将模型部署到 SageMaker 端点的整个流程。此方法会检查必要的配置是否已存在,如果不存在,则触发部署过程:
def deploy(
self,
role_arn: str,
llm_image: str,
config: dict,
endpoint_name: str,
endpoint_config_name: str,
gpu_instance_type: str,
resources: Optional[dict] = None,
endpoint_type: enum.Enum = EndpointType.MODEL_BASED,
) -> None:
try:
if self.resource_manager.endpoint_config_exists(endpoint_config_name=endpoint_config_name):
logger.info(f"Endpoint configuration {endpoint_config_name} exists. Using existing configuration...")
else:
logger.info(f"Endpoint configuration {endpoint_config_name} does not exist.")
self.prepare_and_deploy_model(
role_arn=role_arn,
llm_image=llm_image,
config=config,
endpoint_name=endpoint_name,
update_endpoint=False,
resources=resources,
endpoint_type=endpoint_type,
gpu_instance_type=gpu_instance_type,
)
logger.info(f"Successfully deployed/updated model to endpoint {endpoint_name}.")
except Exception as e:
logger.error(f"Failed to deploy model to SageMaker: {e}")
raise
deploy 方法首先使用 resource_manager 检查端点配置是否已存在。这一步非常重要,因为如果配置已设置好,可以避免不必要的重新部署。实际的部署是通过调用 prepare_and_deploy_model() 方法来完成的,该方法负责将模型部署到指定的 SageMaker 端点。
prepare_and_deploy_model() 方法是 DeploymentService 类中的一个静态方法,主要用于设置和部署 Hugging Face 模型到 SageMaker:
@staticmethod
def prepare_and_deploy_model(
role_arn: str,
llm_image: str,
config: dict,
endpoint_name: str,
update_endpoint: bool,
gpu_instance_type: str,
resources: Optional[dict] = None,
endpoint_type: enum.Enum = EndpointType.MODEL_BASED,
) -> None:
huggingface_model = HuggingFaceModel(
role=role_arn,
image_uri=llm_image,
env=config,
transformers_version="4.6",
pytorch_version="1.13",
py_version="py310",
)
huggingface_model.deploy(
instance_type=gpu_instance_type,
initial_instance_count=1,
endpoint_name=endpoint_name,
update_endpoint=update_endpoint,
resources=resources,
tags=[{"Key": "task", "Value": "model_task"}],
endpoint_type=endpoint_type,
)
此方法首先创建一个 HuggingFaceModel 实例,这是 SageMaker 提供的专用模型类,用于处理 Hugging Face 模型。HuggingFaceModel 的构造函数接受多个关键参数,例如角色 ARN(用于赋予 SageMaker 所需的权限)、LLM DLC Docker 镜像的 URI 以及 LLM 配置。配置中指定了从 Hugging Face 加载的 LLM 模型以及推理参数(例如最大 token 数量)。
实例化 HuggingFaceModel 后,该方法通过 deploy 函数将其部署到 SageMaker。部署过程包括指定实例类型、实例数量、是更新现有端点还是创建新端点。此外,该方法还支持更复杂的部署配置,如 initial_instance_count 参数(用于多模型端点)以及用于跟踪和分类的标签。
最后一步是了解 SagemakerHuggingfaceStrategy 类,它汇集了之前展示的所有内容。此类仅使用一个部署服务实例(如上例所示)进行初始化。
class SagemakerHuggingfaceStrategy(DeploymentStrategy):
def __init__(self, deployment_service):
self.deployment_service = deployment_service
该类的核心功能封装在其 deploy() 方法中,该方法协调部署流程,并接受各种参数来定义 Hugging Face 模型在 AWS SageMaker 上的部署方式:
def deploy(
self,
role_arn: str,
llm_image: str,
config: dict,
endpoint_name: str,
endpoint_config_name: str,
gpu_instance_type: str,
resources: Optional[dict] = None,
endpoint_type: enum.Enum = EndpointType.MODEL_BASED,
) -> None:
logger.info("Starting deployment using Sagemaker Huggingface Strategy...")
logger.info(
f"Deployment parameters: nb of replicas: {settings.COPIES}, nb of gpus: {settings.GPUS}, instance_type: {settings.GPU_INSTANCE_TYPE}"
)
这些参数对部署过程至关重要:
- role_arn:提供 SageMaker 部署权限的 AWS IAM 角色。
- llm_image:DLC Docker 镜像的 URI。
- config:包含模型环境配置的字典。
- endpoint_name 和 endpoint_config_name:SageMaker 端点和端点配置的名称。
- gpu_instance_type:GPU EC2 实例的类型。
- resources:可选的资源字典,用于多模型端点部署。
- endpoint_type:可以是
MODEL_BASED或INFERENCE_COMPONENT,用于确定端点是否包含推理组件。
该方法将实际部署过程委托给 deployment_service,这是策略模式的关键点,使得部署方式具备灵活性而无需更改高层次的部署逻辑:
try:
self.deployment_service.deploy(
role_arn=role_arn,
llm_image=llm_image,
config=config,
endpoint_name=endpoint_name,
endpoint_config_name=endpoint_config_name,
gpu_instance_type=gpu_instance_type,
resources=resources,
endpoint_type=endpoint_type,
)
logger.info("Deployment completed successfully.")
except Exception as e:
logger.error(f"Error during deployment: {e}")
raise
为了更好地理解基础设施,我们也可以查看资源配置。这些资源配置在设置多端点配置时起到关键作用,确保满足应用的延迟和吞吐量需求。ResourceRequirements 对象通过字典初始化,包含若干资源参数,如模型副本数量、所需 GPU 数量、CPU 核心数和内存分配。每个参数对模型的性能和可扩展性都至关重要:
from sagemaker.compute_resource_requirements.resource_requirements import ResourceRequirements
model_resource_config = ResourceRequirements(
requests={
"copies": settings.COPIES,
"num_accelerators": settings.GPUS,
"num_cpus": settings.CPUS,
"memory": 5 * 1024
},
)
在上述代码中,ResourceRequirements 配置了以下四个关键参数:
- copies:模型的实例或副本数量,增加副本可降低延迟、提高吞吐量。
- num_accelerators:指定 GPU 数量。由于 LLM 的计算需求高,通常需要多个 GPU 加速推理。
- num_cpus:部署所需的 CPU 核心数,影响数据预处理、后处理等任务的处理能力。
- memory:内存参数设置了部署所需的最小 RAM 数量,确保模型能正常加载和运行,避免内存不足。
这些配置确保了 LLM 的高效部署,同时满足了性能和成本的平衡。
通过设置这些参数,该类确保在将模型部署到 SageMaker 端点时具备充足的资源来高效运行。这些值的精确调节将根据 LLM 的特定需求(例如模型大小、任务复杂度和预期负载)而有所不同。为了更好地理解如何使用这些参数,我们建议在部署端点后,尝试修改它们以观察 LLM 微服务性能的变化。
最后,让我们回顾用于配置 LLM 引擎的设置。HF_MODEL_ID 指定了要部署的 Hugging Face 模型。例如,在设置类中,我们将其设为 mlabonne/TwinLlama-3.1-8B-13 来加载存储在 Hugging Face 上的自定义 LLM Twin 模型。SM_NUM_GPUS 指定每个模型副本分配的 GPU 数量,这对于确保模型适配 GPU 的 VRAM 至关重要。HUGGING_FACE_HUB_TOKEN 提供了访问 Hugging Face Hub 的凭据以检索模型。HF_MODEL_QUANTIZE 指定量化技术,而其他变量控制 LLM 的 token 生成过程。
hugging_face_deploy_config = {
"HF_MODEL_ID": settings.HF_MODEL_ID,
"SM_NUM_GPUS": json.dumps(settings.SM_NUM_GPUS), # 每个副本使用的 GPU 数量
"MAX_INPUT_LENGTH": json.dumps(settings.MAX_INPUT_LENGTH), # 输入文本的最大长度
"MAX_TOTAL_TOKENS": json.dumps(settings.MAX_TOTAL_TOKENS), # 生成的最大 token 数量(包括输入文本)
"MAX_BATCH_TOTAL_TOKENS": json.dumps(settings.MAX_BATCH_TOTAL_TOKENS),
"HUGGING_FACE_HUB_TOKEN": settings.HUGGINGFACE_ACCESS_TOKEN,
"MAX_BATCH_PREFILL_TOKENS": "10000",
"HF_MODEL_QUANTIZE": "bitsandbytes",
}
使用这两个配置项,我们可以完全控制基础设施、所用的 LLM 以及其运行行为。要使用上述配置启动 SageMaker 部署,请调用如下所示的 create_endpoint() 函数:
create_endpoint(endpoint_type=EndpointType.MODEL_BASED)
为方便起见,我们还将其包装在 poe 命令中:
poetry poe deploy-inference-endpoint
以上就是在 AWS SageMaker 上部署推理流水线所需的一切。最困难的部分是找到适合您需求的正确配置,同时降低基础设施的成本。根据 AWS 的部署时间,这个过程通常需要 15-30 分钟。您可以随时从 .env 文件中更改任何值,以不同的配置重新部署模型,而无需更改代码。例如,我们的默认值使用单个 ml.g5.xlargeGPU 实例。如果您想要更多副本,可以调整 GPUS 和 SM_NUM_GPUS 设置,或通过更改 GPU_INSTANCE_TYPE 变量来更换实例类型。
在将 LLM 微服务部署到 AWS SageMaker 之前,请确保已运行 poetry poe create-sagemaker-role 来生成用户角色,并运行 poetry poe create-sagemaker-execution-role 来生成执行角色。同时,确保在 .env 文件中更新由这两个步骤生成的 AWS_* 环境变量。有关更多详细信息,请参阅仓库的 README 文件。
部署 AWS SageMaker Inference 端点后,您可以在 AWS 的 SageMaker 仪表板中查看它。首先,在左侧面板中点击 "SageMaker dashboard",然后在 Inference 栏中点击 "Endpoints" 按钮,如图 10.6 所示。
点击 “Endpoints” 按钮后,您将看到您的 Twin 端点处于 “Creating” 或 “Created” 状态,如图 10.7 所示。点击端点后,您可以在 CloudWatch 中查看该端点的日志,并监控 CPU、内存、磁盘和 GPU 的使用情况。
此外,SageMaker 提供了一种便捷的方法,可以在一个界面中集中监控所有 HTTP 错误,例如 4XX 和 5XX 错误。
调用 AWS SageMaker 推理端点
现在我们已将 LLM 服务部署在 AWS SageMaker 上,让我们学习如何调用该服务。为此,我们将编写两个类,帮助我们为 SageMaker 准备提示,通过 HTTP 请求调用推理端点,并以客户端可以使用的方式解码结果。所有的 AWS SageMaker 推理代码均可在 GitHub 的 llm_engineering/model/inference 仓库中找到。代码示例如下:
text = "为我写一篇关于 AWS SageMaker 推理端点的帖子。"
llm = LLMInferenceSagemakerEndpoint(
endpoint_name=settings.SAGEMAKER_ENDPOINT_INFERENCE
)
Answer = InferenceExecutor(llm, text).execute()
我们将逐步介绍 LLMInferenceSagemakerEndpoint 和 InferenceExecutor 类。首先来看 LLMInferenceSagemakerEndpoint 类,该类直接与 SageMaker 交互。构造函数初始化了与 SageMaker 端点交互所需的所有重要属性:
class LLMInferenceSagemakerEndpoint(Inference):
def __init__(
self,
endpoint_name: str,
default_payload: Optional[Dict[str, Any]] = None,
inference_component_name: Optional[str] = None,
) -> None:
super().__init__()
self.client = boto3.client(
"sagemaker-runtime",
region_name=settings.AWS_REGION,
aws_access_key_id=settings.AWS_ACCESS_KEY,
aws_secret_access_key=settings.AWS_SECRET_KEY,
)
self.endpoint_name = endpoint_name
self.payload = default_payload if default_payload else self._default_payload()
self.inference_component_name = inference_component_name
endpoint_name 是识别我们要请求的 SageMaker 端点的关键。此外,方法通过提供的值或调用生成默认有效负载的方法来初始化有效负载。
该类的一个关键特性是其生成推理请求的默认有效负载的能力。此功能由 _default_payload() 方法处理:
def _default_payload(self) -> Dict[str, Any]:
return {
"inputs": "",
"parameters": {
"max_new_tokens": settings.MAX_NEW_TOKENS_INFERENCE,
"top_p": settings.TOP_P_INFERENCE,
"temperature": settings.TEMPERATURE_INFERENCE,
"return_full_text": False,
},
}
此方法返回一个字典,表示要发送进行推理的默认有效负载结构。parameters 部分包含影响模型推理行为的设置,如生成的最大标记数、采样策略 (top_p) 和控制输出随机性的温度设置。这些参数从应用程序的设置中获取,确保不同推理任务的一致性。
类通过 set_payload() 方法允许自定义有效负载,使用户可以在发送推理请求之前修改输入和参数:
def set_payload(self, inputs: str, parameters: Optional[Dict[str, Any]] = None) -> None:
self.payload["inputs"] = inputs
if parameters:
self.payload["parameters"].update(parameters)
此方法将有效负载的 inputs 字段更新为用户提供的新输入文本。此外,它允许在提供任何参数时修改推理参数。
最终,我们利用 inference() 方法使用自定义的有效负载来调用 SageMaker 端点:
def inference(self) -> Dict[str, Any]:
try:
logger.info("Inference request sent.")
invoke_args = {
"EndpointName": self.endpoint_name,
"ContentType": "application/json",
"Body": json.dumps(self.payload),
}
if self.inference_component_name not in ["None", None]:
invoke_args["InferenceComponentName"] = self.inference_component_name
response = self.client.invoke_endpoint(**invoke_args)
response_body = response["Body"].read().decode("utf8")
return json.loads(response_body)
except Exception:
logger.exception("SageMaker inference failed.")
raise
在此方法中,inference 方法构建了要发送到 SageMaker 端点的请求。该方法将有效负载和其他必要的细节打包为 SageMaker 所期望的格式。如果指定了 inference_component_name,则将其包含在请求中,以便在需要时对推理过程进行更细粒度的控制。请求通过 invoke_endpoint() 函数发送,并读取、解码响应,将其作为 JSON 对象返回。
接下来我们来看看 InferenceExecutor 如何使用之前介绍的 LLMInferenceSagemakerEndpoint 类来向 AWS SageMaker 端点发送 HTTP 请求。
InferenceExecutor 类从构造函数开始,该构造函数输入调用 LLM 的关键参数。llm 参数接受任何实现了 Inference 接口的实例,例如 LLMInferenceSagemakerEndpoint 类,用于执行推理。
此外,它接受 query 参数,表示用户输入。最终,还接受一个可选的 context 字段,用于执行 RAG 时,允许自定义提示模板。如果未提供提示模板,将使用默认版本,该版本并不针对特定的 LLM 进行优化:
class InferenceExecutor:
def __init__(
self,
llm: Inference,
query: str,
context: str | None = None,
prompt: str | None = None,
) -> None:
self.llm = llm
self.query = query
self.context = context if context else ""
if prompt is None:
self.prompt = """
你是一名内容创作者。请根据用户的需求撰写内容,并将提供的上下文作为信息的主要来源。
用户查询:{query}
上下文:{context}
"""
else:
self.prompt = prompt
execute() 方法是 InferenceExecutor 类的核心组件。此方法负责实际执行推理。当调用 execute 时,它通过格式化用户的查询和上下文来准备要发送到 LLM 的有效负载。
然后,它配置了多个参数来影响 LLM 的行为,例如模型允许生成的最大新标记数、抑制模型生成重复文本的重复惩罚以及控制输出随机性的温度设置。
一旦设置好有效负载和参数,该方法将调用 LLMInferenceSagemakerEndpoint 的推理函数并等待生成的答案:
def execute(self) -> str:
self.llm.set_payload(
inputs=self.prompt.format(query=self.query, context=self.context),
parameters={
"max_new_tokens": settings.MAX_NEW_TOKENS_INFERENCE,
"repetition_penalty": 1.1,
"temperature": settings.TEMPERATURE_INFERENCE,
},
)
answer = self.llm.inference()[0]["generated_text"]
return answer
通过实现 Inference 接口的对象进行推理解耦,我们可以轻松注入其他推理策略和 LLMInferenceSagemakerEndpoint 实现,而无需修改代码的不同部分。
运行测试示例非常简单,只需调用以下 Python 文件:
poetry run python -m llm_engineering.model.inference.test
另外,为方便起见,我们将其包装在 poe 命令下:
poetry poe test-sagemaker-endpoint
现在,我们必须了解如何使用 FastAPI 实现业务微服务。该微服务将向上述定义的 LLM 微服务发送 HTTP 请求,并调用第 9 章中实现的 RAG 检索模块。
使用 FastAPI 构建业务微服务
要实现一个简单的 FastAPI 应用来验证我们的部署策略,首先我们需要定义一个 FastAPI 实例,如下所示:
from fastapi import FastAPI
app = FastAPI()
接下来,我们使用 Pydantic 的 BaseModel 定义 QueryRequest 和 QueryResponse 类。这些类表示 FastAPI 端点的请求和响应结构:
class QueryRequest(BaseModel):
query: str
class QueryResponse(BaseModel):
answer: str
现在,我们已经定义了 FastAPI 组件并配置好了所有 SageMaker 元素,接下来复习一下在第 9 章中介绍的 call_llm_service() 和 rag() 函数。之前我们无法运行它们,因为尚未部署微调的 LLM。作为回顾,call_llm_service() 函数封装了用于调用 SageMaker LLM 微服务的推理逻辑:
def call_llm_service(query: str, context: str | None) -> str:
llm = LLMInferenceSagemakerEndpoint(
endpoint_name=settings.SAGEMAKER_ENDPOINT_INFERENCE, inference_component_name=None
)
answer = InferenceExecutor(llm, query, context).execute()
return answer
接下来,我们定义 rag() 函数,该函数实现了所有 RAG 的业务逻辑。为了避免重复,完整函数解释请见第 9 章。需要强调的是,rag() 函数仅实现了执行 RAG 所需的业务步骤,这些步骤主要受 CPU 和 I/O 限制。例如,ContextRetriever 类调用 OpenAI 和 Qdrant 的 API,这些是网络 I/O 密集操作,并调用嵌入模型,该模型直接在 CPU 上运行。此外,由于 LLM 推理逻辑被移动到另一个微服务中,call_llm_service() 函数仅受网络 I/O 限制。总而言之,该函数轻量易运行,而重计算部分由其他服务完成,使我们可以将 FastAPI 服务器托管在轻量、廉价的机器上,无需 GPU 即可实现低延迟运行:
def rag(query: str) -> str:
retriever = ContextRetriever(mock=False)
documents = retriever.search(query, k=3 * 3)
context = EmbeddedChunk.to_context(documents)
answer = call_llm_service(query, context)
return answer
最后,我们定义 rag_endpoint() 函数,用于将 RAG 逻辑公开为 HTTP 端点。我们使用 Python 装饰器将其公开为 FastAPI 应用中的 POST 端点。该端点映射到 /rag 路由,并期望一个 QueryRequest 作为输入。函数通过调用 rag 函数处理请求的用户查询。如果成功,返回包装在 QueryResponse 对象中的答案;如果发生异常,则抛出带有异常详细信息的 HTTP 500 错误:
@app.post("/rag", response_model=QueryResponse)
async def rag_endpoint(request: QueryRequest):
try:
answer = rag(query=request.query)
return {"answer": answer}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) from e
该 FastAPI 应用展示了如何将托管在 AWS SageMaker 上的 LLM 集成到 Web 服务中,并利用 RAG 提升模型响应的相关性。代码采用模块化设计,利用自定义类如 ContextRetriever、InferenceExecutor 和 LLMInferenceSagemakerEndpoint,实现了轻松的定制和可扩展性,使其成为在生产环境中部署 ML 模型的有力工具。
我们将使用 Uvicorn Web 服务器,这是运行 FastAPI 应用的推荐方法来启动服务器。运行以下命令即可启动服务器:
uvicorn tools.ml_service:app --host 0.0.0.0 --port 8000 --reload
同样,可以通过以下 poe 命令实现相同操作:
poetry poe run-inference-ml-service
要调用 /rag 端点,我们可以使用 curl CLI 命令向 FastAPI 服务器发送 POST HTTP 请求,如下所示:
curl -X POST 'http://127.0.0.1:8000/rag' -H 'Content-Type: application/json' -d '{"query": "your_query "}'
通常,我们还提供了一个包含实际用户查询的 poe 命令示例:
poetry poe call-inference-ml-service
此 FastAPI 服务器仅在本地运行。下一步可以将其部署到 AWS 弹性 Kubernetes 服务(EKS),这是 AWS 的 Kubernetes 自托管版本。另一种选择是部署到 AWS 弹性容器服务(ECS),它类似于 AWS EKS,但不使用 Kubernetes,而是使用 AWS 的实现。然而,这些步骤与 LLM 或 LLMOps 无关,因此本书不会详细介绍。但大致过程是,你需要从 AWS 控制台创建一个 EKS/ECS 集群或使用基础设施即代码(IaC)工具(如 Terraform)。之后,将上述 FastAPI 代码制作成 Docker 镜像并推送到 AWS ECR,最后使用 ECR 上的 Docker 镜像创建 ECS/EKS 部署。如果这看起来很多,不用担心,我们将在第 11 章中通过 ZenML 流水线部署到 AWS 的示例引导你完成类似的流程。
完成推理管道部署的测试后,删除所有用于部署 LLM 的 AWS SageMaker 资源非常重要。由于几乎所有 AWS 资源都采用按使用量计费的策略,使用 SageMaker 几个小时不会造成太大开销,但如果忘记关闭,几天后成本可能会急剧增加。因此,一个好的经验法则是,测试完 SageMaker 基础设施(或任何 AWS 资源)后,一定要删除所有内容。
幸运的是,我们提供了一个脚本,可以帮助你删除所有 AWS SageMaker 资源:
poetry poe delete-inference-endpoint为了确保所有内容已正确删除,可以前往 SageMaker 控制台自己检查一遍。
处理使用量激增的自动扩展功能
到目前为止,SageMaker LLM 微服务使用了固定数量的副本来服务用户,这意味着无论流量如何,实例数量始终保持不变。正如我们在本书中多次提到的,带有 GPU 的机器成本昂贵。因此,在大多数副本空闲的低流量时段,我们会浪费大量资金。此外,如果应用程序出现突发流量高峰,服务器无法处理大量请求,应用性能会受到影响。这对用户体验是个巨大问题,因为流量高峰期往往是新用户涌入的时段。如果他们对产品的第一印象很差,我们大大降低了他们再次使用平台的可能性。
此前,我们使用 SageMaker 的 ResourceRequirements 类配置了多端点服务。例如,假设我们配置了四个副本(replicas)并设置以下计算需求:
model_resource_config = ResourceRequirements(
requests={
"copies": 4, # 副本数量
"num_accelerators": 4, # 所需 GPU 数量
"num_cpus": 8, # 所需 CPU 核数
"memory": 5 * 1024, # 所需最小内存,单位为 Mb(必填)
},
)
使用该配置,无论空闲时间或流量高峰,我们始终保持四个副本服务客户端。解决方案是实现自动扩展策略,根据各种指标(如请求数量)动态调整副本数量。
例如,图 10.8 显示了一种标准架构,其中 SageMaker 推理端点根据请求数量进行自动缩放。当没有流量时,我们可以保持一个在线副本以确保服务器响应新用户请求,或者在延迟要求不高时缩减到零。假设流量达到每秒 10 个请求时,我们需要保持两个副本在线,而请求数量激增至每秒 100 个时,自动扩展服务应将副本数量扩展至 20 个以满足需求。请注意,这些只是示例数据,实际数值应根据具体使用场景进行调整。
在多副本系统中,不深入讨论云网络的细节时,可以了解在客户端与副本之间会有一个应用负载均衡器 (ALB) 或其他类型的负载均衡器。
所有请求首先会发送到 ALB,ALB 知道如何将它们路由到某个副本。ALB 可以采用多种路由策略,其中最简单的一种称为“轮询 (round robin)”,即按顺序将请求发送给每个副本。例如,第一个请求被路由到副本 1,第二个请求到副本 2,以此类推。使用这种方法,无论线上有多少副本,客户端始终通过负载均衡器访问端点,该负载均衡器充当集群的入口点。因此,添加或移除副本不会影响服务器和客户端之间的通信协议。
接下来快速了解如何为 AWS SageMaker 推理端点实现自动扩展策略。SageMaker 提供了一项称为“应用自动扩展 (Application Auto Scaling)”的功能,可以基于预定义策略动态扩展资源。有效利用此功能涉及两个基本步骤:注册可扩展目标和创建可扩展策略。
注册可扩展目标
启用资源自动扩展的第一步是使用 AWS 提供的应用自动扩展 (Application Auto Scaling) 功能注册一个可扩展目标。这可以理解为向 AWS 通知您要扩展的特定资源,并设置扩展的上下限。不过,这一步不会规定扩展的方式或时机。
例如,在使用 SageMaker 推理组件时,您需要定义以下内容:
- 资源 ID:该 ID 作为唯一标识符,通常包括 SageMaker 推理组件的名称。
- 服务命名空间:标识资源所属的 AWS 服务,在本例中为 SageMaker。
- 可扩展维度:指定需要扩展的资源,例如所需的副本数量。
- MinCapacity 和 MaxCapacity:这些参数定义了自动扩展策略的上下限,例如副本数量的最小和最大限制。
通过注册可扩展目标,您为 SageMaker 推理组件准备了未来的扩展操作,但没有决定这些操作的具体执行时间和方式。
创建可扩展策略
在注册好可扩展目标后,下一步是定义扩展的执行方式,即创建扩展策略。扩展策略定义了触发扩展事件的具体规则。创建策略时,您需要定义监控的指标和触发扩展事件的阈值。
对于 SageMaker 推理组件,扩展策略可能包括以下元素:
- 策略类型:例如,可以选择 TargetTrackingScaling,这种策略会调整资源容量,以维持选定指标的特定目标值。
- 目标跟踪配置:包括选择要监控的指标(如
SageMakerInferenceComponentInvocationsPerCopy),设置所需的目标值,以及指定冷却时间以控制扩展动作的频率。
扩展策略定义了缩容和扩容的规则。系统会持续监控指定指标,根据指标值是否超出目标值,触发扩展操作以增减推理组件副本的数量,同时确保在已注册的可扩展目标的限制范围内。
让我们更深入地解释 TargetTrackingScaling 策略的工作原理。假设您有一个表示应用程序理想平均利用率或吞吐量水平的指标。通过目标跟踪,您可以选择该指标并设置一个反映应用程序最佳状态的目标值。一旦定义好,应用自动扩展会创建并管理必要的 CloudWatch 警报来监控该指标。当偏离目标值时,会触发扩展操作,类似于恒温器通过调整温度保持室内温度一致。
例如,考虑在 SageMaker 上运行的应用程序。假设我们设置了一个 GPU 利用率目标值,保持在 70% 左右。此目标为应对突发流量高峰保留了足够的余地,同时避免了闲置资源带来的不必要成本。当 GPU 使用率超过目标时,系统会扩展资源以应对增加的负载。相反,当 GPU 使用率低于目标时,系统会缩减容量,以在空闲时段降低成本。
使用应用自动扩展配置目标跟踪策略的一个显著优势在于简化了扩展过程。您无需手动配置 CloudWatch 警报和定义扩展调整。
最小和最大扩展限制
为 SageMaker 推理端点设置自动扩展时,在创建扩展策略之前确定最大和最小扩展限制至关重要。最小值表示模型可以正常运行的最少资源,该值至少为 1,以确保模型始终具备一定的容量。
接下来,配置最大值,定义模型可以扩展的上限。虽然最大值必须等于或大于最小值,但实际上不设上限,因此您可以根据应用需求在 AWS 提供的范围内无限扩展。
冷却时间
扩展策略的另一个重要方面是冷却时间,它在响应性和稳定性之间保持平衡。冷却时间充当保护机制,确保系统在扩展事件期间不过度响应——无论是缩减容量(缩容)还是增加容量(扩容)。通过引入适当的暂停时间,冷却时间可以防止实例数量的快速波动。在缩容请求期间,它会延迟实例的移除;在扩容请求期间,它会限制新副本的创建。这种策略有助于为 LLM 服务维持稳定高效的环境。
这些实用的基本原则被广泛应用于自动扩展大多数 Web 服务器,包括在线实时机器学习服务器。一旦您了解了如何为 SageMaker 配置扩展策略,您就可以将所学策略直接应用于其他常用部署工具,如 Kubernetes 或 AWS ECS。
您可以参考 AWS 的官方教程,了解本章实现的 AWS SageMaker 端点的自动扩展配置步骤:AWS 官方教程。
自动扩展是任何云架构中的关键组成部分,但需要注意一些陷阱。第一个也是最危险的陷阱是过度扩展,这会直接增加基础设施成本。如果扩展策略或冷却时间过于敏感,可能会无意义地启动新机器,而这些机器将处于闲置状态或资源利用率不足。另一个陷阱则在另一极端,即系统扩展不足,导致用户体验不佳。
因此,良好的做法是了解系统的需求,并基于此在开发或测试环境中调整和试验自动扩展参数,直到找到最佳平衡点(类似于模型训练中的超参数调优)。例如,假设您期望系统支持每分钟 100 位用户的平均负载,并在节假日等特殊事件中扩展到每分钟 10,000 位用户。基于这一规格,可以对系统进行压力测试,并监控资源使用情况,以在成本、延迟和吞吐量之间找到最佳平衡,支持常规和特殊情况的负载。
总结
本章中,我们学习了在部署 ML 模型(无论是否为 LLM)之前需要做出的设计决策,介绍了 ML 模型的三种基本部署类型:在线实时推理、异步推理和离线批量转换。接着,我们探讨了是将我们的 ML 服务构建为一个整体应用(单体)还是分为两个微服务,例如 LLM 微服务和业务微服务。为此,我们权衡了单体架构和微服务架构在模型服务中的优缺点。
随后,我们展示了如何将微调后的 LLM 模型 Twin 部署到 AWS SageMaker 推理端点上。此外,我们介绍了如何使用 FastAPI 实现业务微服务,包含第 9 章实现的检索模块中的 RAG 步骤,以及部署在 AWS SageMaker 上的 LLM 微服务。最后,我们探讨了为什么需要实现自动扩展策略,并回顾了一种基于特定指标的流行自动扩展策略,展示了如何在 AWS SageMaker 中实现这一策略。
在下一章中,我们将学习 MLOps 和 LLMOps 的基础知识,接着探索如何将 ZenML 流水线部署到 AWS,并实现持续训练、持续集成和持续交付(CT/CI/CD)以及监控流水线。
参考文献:
- AWS Developers. (2023年9月22日). 《Machine Learning in 15: Amazon SageMaker High-Performance Inference at Low Cost》[视频]. YouTube. www.youtube.com/watch?v=FRb…
- bitsandbytes-foundation. (n.d.). GitHub—bitsandbytes-foundation/bitsandbytes:通过 k 位量化实现 PyTorch 的大语言模型。GitHub. github.com/bitsandbyte…
- Stack Overflow. (n.d.). AWS 中 IAM 角色和 IAM 用户的区别。stackoverflow.com/questions/4…
- Huggingface. (n.d.-a). GitHub—huggingface/safetensors:存储和分发张量的简单安全方式。GitHub. github.com/huggingface…
- Huggingface. (n.d.-b). GitHub—huggingface/text-generation-inference:大语言模型文本生成推理。GitHub. github.com/huggingface…
- Huyen, C. (n.d.). 《Designing machine learning systems》. O’Reilly 在线学习. www.oreilly.com/library/vie…
- Iusztin, P. (2024年8月20日). 《Architect LLM & RAG inference pipelines | Decoding ML》. Medium. medium.com/decodingml/…
- Lakshmanan, V., Robinson, S., 和 Munn, M. (n.d.). 《Machine Learning design patterns》. O’Reilly 在线学习. www.oreilly.com/library/vie…
- Mendoza, A. (2024年8月21日). 《Best tools for ML model Serving》. neptune.ai. neptune.ai/blog/ml-mod…