在 Elasticsearch 中扩展 ML 推理管道:如何避免问题并解决瓶颈

时间:2024-04-14 07:06:57

作者:来自 Elastic Iulia Feroli


是时候考虑语义搜索运营了吗?

无论你是一位经验丰富的搜索工程师,希望探索新的人工智能功能,还是一位机器学习专家,希望更多地利用搜索基础设施来增强语义相似性模型 —— 充分利用这些领域的交集可能需要熟悉一些新概念。

虽然 Elasticsearch 提供了一些快速启动指南,比如 ELSER 示例 notebook (或者对于本地的 Elasticsearch, 你可以参考 “Elasticsearch:使用 ELSER 文本扩展进行语义搜索”),但当你希望扩展推理过程时,会引入更多的配置选项。

在本博客中,我们将看看在处理更复杂的工作负载时,可能遇到的潜在瓶颈以及缓解成长烦恼的方法。

在部署大型语言模型到你的环境时,以下是一些需要注意的步骤。

在下载模型之前

机器学习节点大小 在 Elasticsearch 中使用 NLP 模型构建项目的第一步是设置部署模型的正确基础设施。

正确的机器学习节点配置可能是第一个潜在的瓶颈,因此请确保你为预期的结果选择了合适的大小。

推荐的最小大小:

如果关闭了部署自动扩缩,则部署和使用 ELSER 模型的专用机器学习节点的最小大小为 4 GB;对于自然语言处理模型,则为 16 GB。

建议打开自动扩缩,因为它允许你的部署根据需求动态调整资源。

参见文档

你可能遇到的故障排除场景:

潜在的瓶颈 Error 信息 解决方案
ML Node is not big enough ApiError(429, 'status_exception', 'Could not start deployment because no ML nodes with sufficient capacity were found') 确保为 ML 节点选择合适的大小,并最好启用自动扩展,以便你的部署在遇到额外请求时可以扩展。
Autoscaling limit is not high enough Autoscaling limits reached. To continue experiencing optimal performance, we recommend increasing your maximum size per zone for the topologies: Machine Learning. 还有一些情况,ML 节点足够大,可以下载模型,但如果配置不正确,大吞吐量的推理调用仍然会使系统过载。 增加大小,确保你的分配使用所有可用的 CPU,或使用较小的数据批次来缓解。

模型配置

更大的节点大小也允许在选择模型的分配和线程数量时具有更大的灵活性。

每个线程需要一个 CPU 或 vCPU,所以例如 8 个 CPU 可以让你拥有 1 个分配,每个分配最多 8 个线程,或者最多 8 个分配,每个分配 1 个线程,或者其他排列组合,只要满足以下条件:

umber_of_allocations * threads_per_allocation <= number of available CPUs.

在同一 ML 节点上部署多个模型将共享这些资源,因此你可以通过配置每个模型的最大访问来根据需要分配你的 CPU。

此外,每个模型部署的分配都有一个用于推理请求的有限队列。当对同一部署进行了太多调用并且队列填满时,所有后续请求都将被拒绝。考虑使用专用部署以防止此情况发生。

对于每个部署和用例,你应考虑以下参数:

参数 功能
number_of_allocations 通过允许并行执行更多推理请求来提高吞吐量。 这反过来又会提高摄取性能。 默认为 1; 但你应该更改此设置,以便使用所有可用的 CPU。
threads_per_allocation 提高每个推理请求的速度,从而提高搜索速度。 默认为 1; 但你应该更改此设置,以便使用所有可用的 CPU。
queue_capacity 控制队列中一次允许有多少个推理请求。 当请求数量超过总数时,新请求将被拒绝并返回 429 错误。 默认为 1024。最大允许值为 1000000。

此设置的值不得超过每个节点可分配的处理器数量。

请参考有关 ELSER 性能随分配数量增加而提高的基准测试信息作为示例。

在部署模型时

一旦模型已经下载到你的集群中,你可以开始部署它,同时考虑前面讨论的参数。在这个阶段,如果你计划部署同一模型的多个实例,可以考虑使用唯一的 deployment_id。

client.ml.start_trained_model_deployment(
    model_id=".elser_model_2", 
    deployment_id="elser_inference_1",
    number_of_allocations=1, 
    threads_per_allocation=8,
    queue_capacity=7000, 
    timeout="1m", 
    wait_for="starting"
)

在此阶段你可能会遇到一些潜在的瓶颈或错误:

瓶颈 解释 / Error 信息 解决方案
部署期间超时 在不指定 wait_for 参数的情况下,它默认为 started,这意味着只有当模型下载完成并成功部署时,你才会收到响应。然而,这个过程会相当耗时,具体取决于模型大小,而且由于 timeout 参数也默认为仅 30 秒,这通常会导致错误。 请改用 wait_for="starting",和/或 增加引发错误之前的等待时间:timeout="3m"
不按顺序运行这些步骤(具体示例请参阅下面的行) 在上一步完成运行之前运行命令将导致错误: 使用 status = client.ml.get_trained_models(model_id=".elser_model_2", include="definition_status") 检查模型的状态
尝试在模型完全下载之前部署模型 Model definition truncated. Unable to deserialize trained model definition [.elser_model_2] 你应该仅在 status["trained_model_configs"][0]["complete_define"] == True 时尝试部署模型
尝试对尚未完全部署的模型运行推理 404, 'resource_not_found_exception', 'Could not find trained model [.elser_model_2]'

在运行推理之前

一旦模型部署完成,你就可以开始对其进行推理调用。这可以通过 inference API 来完成:

response = client.ml.infer_trained_model(
    model_id=model_id, 
    docs=[{"text_field": query}])

这个推理命令也有一个默认的超时时间为 10 秒,当一次生成少量文档的嵌入时是足够的。

然而,对于大多数实际用例,将会有大量需要处理的文档;例如,在一个大型索引中为每个文档创建嵌入以启用语义搜索功能。

你可以增加超时时间:

response = client.ml.infer_trained_model(model_id=model_id, docs=docs, timeout="5m")

然而,正如前面的部分所提到的,根据分配的数量或发送到同一部署的不同任务的数量,模型也将有一个最大文档队列接受限制。因此,即使设置了较长的超时时间,对于大吞吐量来说,这种方法可能仍然不足够。

另一个选择是为推理过程创建摄入管道。你还可以为不同的管道使用不同的部署:一个用于在摄入新数据时生成嵌入,另一个用于在搜索时运行推理。 管道还允许你通过在 processors 列表中添加元素来设置自定义操作,例如重命名字段或为不同任务使用多个模型。你还可以在后台或按照定期时间表运行较长的任务。

client.ingest.put_pipeline(
    id="elser-2-ingest-pipeline-1",
    description="Ingest pipeline for ELSER with a lot more requests",
    processors=[
        # omitting processors code
    ])

client.reindex(
    source={"index": "raw_data"},
    dest={"index": "data_with_embeddings", "pipeline": "elser-2-ingest-pipeline-1"},
    wait_for_completion=False,
)
瓶颈 解决方案
Timeout 与前面的步骤类似,冗长的管道过程可能会导致超时。 使用 wait_for_completion = False 参数。
Waiting for pipeline to finish 你可以使用从 reindex 函数获得的任务 ID 稍后通过 client.tasks.get(task_id=task_id) 跟踪管道进度。 使用 wait_for_completion 参数时会生成此 ID。

监控和调整

一旦你部署了模型并开始使用推理服务,你可以查看配置的性能。通常,这是确定特定用例的适当参数的最佳方法,并根据需要进行调整,直到达到所需的性能。

举一个简单的例子,如果你部署了一个模型而没有配置上述讨论的任何设置,这些将是分配的默认值:

{
  "threads_per_allocation" : 1, 
  "number_of_allocations" : 1, 
  "queue_capacity" : 1024
}

假设通过推理管道将大量文档发送到该模型后,我们注意到线程分配中的一些警告信号。 endpoint:

GET _nodes/hot_threads

响应:

ml.allocated_processors=16

100.0% [cpu=3.5%, other=96.5%] cpu usage by thread

ML 节点分配有 16 个处理器,但我们仅在模型的一个实例中利用其中 1 个处理器。 此外,在其他而不是与 CPU 相关的任务下报告的高利用率意味着该过程中存在大量等待和冗余,并且我们的文档大部分时间都在排队。

为了优化性能,你应该使用所有可用的内核。

你还可以在训练模型 UI 中或通过以下命令查看更多指标:

GET _ml/trained_models/_stats

在这里你可以看到更多有用的信息,例如 average_inference_time_ms、number_of_pending_requests 或 peak_throughput_per_分钟。

作为说明,这里有两个模型部署在同一个 ML 节点上,在相同的管道和数据上运行推理,但采用不同的分配策略。 你可以看到配置模型的推理时间几乎减半。

Model ID Allocation Average Inference time
elser_inference_configured 3 * 8 67.80 milliseconds
.elser_model_2 1 * 1 115.58 milliseconds

结论

这既是一件好事,也可能是一件困难的事情,有多种灵活和模块化的方法来构建适合你的项目的推理架构。 为每个用例构建最佳方法也不仅仅是选择正确的配置或基础设施设置。 你可以详细了解模型的检索优化甚至数据处理决策(例如分块策略)如何影响性能。

Elasticsearch 汇集了令人惊叹的开箱即用功能,并提供自定义选项和指导,帮助你构建最佳的语义搜索解决方案。

准备好将 RAG 构建到你的应用程序中了吗? 想要尝试使用向量数据库的不同 LLMs?
Github 上查看我们的 LangChain、Cohere 等示例 notebooks,并参加即将开始的 Elasticsearch 工程师培训

原文: Scaling ML Inference Pipelines in Elasticsearch: How to avoid issues and troubleshoot bottlenecks — Elastic Search Labs