MLOps 之于机器学习,就像 DevOps 之于传统软件开发一样。两者都是一组旨在改善工程团队(开发或 ML)和 IT 运营 (Ops) 团队之间协作的实践和原则。目标是使用自动化来简化开发生命周期,从规划和开发到部署和运营。这些方法的主要好处之一是持续改进。
在之前关于 MLOps 工具主题的文章中,我介绍了 KubeFlow Pipelines 2.0 和 MLflow。在这篇文章中,我想介绍 MLRun,这是另一个 MLOps 工具,如果您正在为您的组织购买 MLOps 工具,则应考虑使用该工具。但在深入研究之前,让我们快速了解一下 MLOps 的前景。
MLOps 格局
虽然这三种工具都旨在为您的所有 AI/ML 需求提供完整的端到端工具,但它们都以不同的动机或设计目标开始。
-
KubeFlow 着手实现 Kubernetes 的民主化。KubeFlow 的核心 KubeFlow Pipelines 要求代码通过部署到 Kubernetes Pod 的无服务器函数运行。
-
MLflow 最初是一种工具,用于跟踪实验以及与 ML 实验相关的所有内容,例如指标、项目(数据)和模型本身。MLflow 了解常用的 ML 框架,如果选择,可以要求 MLflow 自动捕获它认为重要的所有实验数据。
-
MLRun 的使命是消除样板代码,这与其他 MLOps 工具不同。它还需要创建无服务器函数,但它更进一步,因为它知道流行的 ML 框架——因此,不需要对纪元循环进行编码,并且对分布式训练的代码支持较低。
关于 MLRun 的更多信息
MLRun 是一个开源 MLOps 框架,最初由专门从事数据科学的公司 Iguazio 创建。2023 年 1 月,Iguazio 被麦肯锡公司收购,现在是麦肯锡人工智能部门 QuantumBlack 的一部分。
让我们看一下安装选项和需要部署的服务。
安装选项
MLRun 网站推荐了三种在本地安装 MLRun 的方法。Iguazio 还维护托管服务。
本地选项:
-
本地部署:在笔记本电脑或单个服务器上使用 Docker 撰写文件部署 MLRun 服务。此选项需要 Docker Desktop,并且比 Kubernetes 安装更简单。它非常适合试验 MLRun 功能;但是,您将无法横向扩展计算资源。
-
Kubernetes 集群:在自己的 Kubernetes 集群上部署 MLRun 服务。此选项支持弹性缩放;但是,安装起来更复杂,因为它需要您自行安装 Kubernetes。您还可以使用此选项部署到 Docker Desktop 启用的 Kubernetes 集群。
-
Amazon Web Services (AWS):在 AWS 上部署 MLRun 服务。此选项是安装 MLRun 的最简单方法。MLRun 软件是免费的;但是,AWS 基础设施服务是有成本的。
托管服务:
此外,如果您不介意共享环境,那么 Iguazio 提供托管服务。Iguazio 提供 14 天免费试用。
MLRun 服务简介
MLRun UI:MLRun UI 是一个基于 Web 的用户界面,您可以在其中查看项目和项目运行。
MLRun API:MLRun API 是代码将用于创建项目和启动运行的编程接口。
Nuclio:Nuclio 是一个开源的无服务器计算平台,专为高性能应用程序而设计,特别是在数据处理、实时分析和事件驱动架构方面。它抽象了基础架构管理的复杂性,使开发人员能够专注于编写和部署函数或微服务。
MinIO:将 MLRun 部署到 Kubernetes 集群时,MLRun 使用 MinIO 进行对象存储。我们将把它包含在我们的本地 Docker compose 部署中,因为在后续的文章中,我们需要从对象存储中读取用于模型训练的数据集。当我们创建一个简单的烟雾测试笔记本时,我们也将在这篇文章中使用它,以确保 MLRun 正常启动并运行。
安装 MLRun 服务
我将在此处展示的安装与 MLRun 安装和设置指南中显示的略有不同。首先,我想在此安装中包含 MinIO,以便我们的无服务器函数可以从对象存储加载数据集。此外,我想要用于运行 Jupyter Notebooks 和 MLRun API 的单独服务。(包含 Jupyter 的 MLRun 的 Docker Compose 文件将 MLRun API 和 Jupyter Notebook 服务打包到同一服务中。最后,我将重要的环境变量放在 .env 文件中,这样就不需要手动创建环境变量。MLRun 服务的 Docker Compose 文件和 config.env 文件如下所示。您也可以在此处下载它们以及本文的所有代码。
services:
init_nuclio:
image: alpine:3.18
command:
- "/bin/sh"
- "-c"
- |
mkdir -p /etc/nuclio/config/platform; \
cat << EOF | tee /etc/nuclio/config/platform/platform.yaml
runtime:
common:
env:
MLRUN_DBPATH: http://${HOST_IP:?err}:8080
local:
defaultFunctionContainerNetworkName: mlrun
defaultFunctionRestartPolicy:
name: always
maxRetryCount: 0
defaultFunctionVolumes:
- volume:
name: mlrun-stuff
hostPath:
path: ${SHARED_DIR:?err}
volumeMount:
name: mlrun-stuff
mountPath: /home/jovyan/data/
logger:
sinks:
myStdoutLoggerSink:
kind: stdout
system:
- level: debug
sink: myStdoutLoggerSink
functions:
- level: debug
sink: myStdoutLoggerSink
EOF
volumes:
- nuclio-platform-config:/etc/nuclio/config
jupyter:
image: "mlrun/jupyter:${TAG:-1.6.2}"
ports:
#- "8080:8080"
- "8888:8888"
environment:
MLRUN_ARTIFACT_PATH: "/home/jovyan/data/{{project}}"
MLRUN_LOG_LEVEL: DEBUG
MLRUN_NUCLIO_DASHBOARD_URL: http://nuclio:8070
MLRUN_HTTPDB__DSN: "sqlite:home/jovyan/data/mlrun.db?check_same_thread=false"
MLRUN_UI__URL: http://localhost:8060
# using local storage, meaning files / artifacts are stored locally, so we want to allow access to them
MLRUN_HTTPDB__REAL_PATH: "/home/jovyan/data"
# not running on k8s meaning no need to store secrets
MLRUN_SECRET_STORES__KUBERNETES__AUTO_ADD_PROJECT_SECRETS: "false"
# let mlrun control nuclio resources
MLRUN_HTTPDB__PROJECTS__FOLLOWERS: "nuclio"
volumes:
- "${SHARED_DIR:?err}:/home/jovyan/data"
networks:
- mlrun
mlrun-api:
image: "mlrun/mlrun-api:${TAG:-1.6.2}"
ports:
- "8080:8080"
environment:
MLRUN_ARTIFACT_PATH: "${SHARED_DIR}/{{project}}"
# using local storage, meaning files / artifacts are stored locally, so we want to allow access to them
MLRUN_HTTPDB__REAL_PATH: /data
MLRUN_HTTPDB__DATA_VOLUME: "${SHARED_DIR}"
MLRUN_LOG_LEVEL: DEBUG
MLRUN_NUCLIO_DASHBOARD_URL: http://nuclio:8070
MLRUN_HTTPDB__DSN: "sqlite:data/mlrun.db?check_same_thread=false"
MLRUN_UI__URL: http://localhost:8060
# not running on k8s meaning no need to store secrets
MLRUN_SECRET_STORES__KUBERNETES__AUTO_ADD_PROJECT_SECRETS: "false"
# let mlrun control nuclio resources
MLRUN_HTTPDB__PROJECTS__FOLLOWERS: "nuclio"
volumes:
- "${SHARED_DIR:?err}:/data"
networks:
- mlrun
mlrun-ui:
image: "mlrun/mlrun-ui:${TAG:-1.6.2}"
ports:
- "8060:8090"
environment:
MLRUN_API_PROXY_URL: http://mlrun-api:8080
MLRUN_NUCLIO_MODE: enable
MLRUN_NUCLIO_API_URL: http://nuclio:8070
MLRUN_NUCLIO_UI_URL: http://localhost:8070
networks:
- mlrun
nuclio:
image: "quay.io/nuclio/dashboard:${NUCLIO_TAG:-stable-amd64}"
ports:
- "8070:8070"
environment:
NUCLIO_DASHBOARD_EXTERNAL_IP_ADDRESSES: "${HOST_IP:?err}"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- nuclio-platform-config:/etc/nuclio/config
depends_on:
- init_nuclio
networks:
- mlrun
minio:
image: quay.io/minio/minio
#network_mode: "host"
volumes:
#- /d/data:/data
#- ./data:/data
- ~/minio-data:/data
ports:
- 9000:9000
- 9001:9001
#extra_hosts:
# - "host.docker.internal:host-gateway"
environment:
MINIO_ROOT_USER: 'minio_user'
MINIO_ROOT_PASSWORD: 'minio_password'
MINIO_ADDRESS: ':9000'
MINIO_STORAGE_USE_HTTPS: False
MINIO_CONSOLE_ADDRESS: ':9001'
#MINIO_LAMBDA_WEBHOOK_ENABLE_function: 'on'
#MINIO_LAMBDA_WEBHOOK_ENDPOINT_function: 'http://localhost:5000'
command: minio server /data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 30s
timeout: 20s
retries: 3
networks:
- mlrun
volumes:
nuclio-platform-config: {}
networks:
mlrun:
name: mlrun
文件名: compose-with-jupyter-minio.yaml
# MLRun configuration
HOST_IP=127.17.0.01
SHARED_DIR=~/mlrun-data
文件名: config.env
运行下面显示的 docker-compose 命令将启动我们的服务。
docker-compose -f compose-with-jupyter-minio.yaml --env-file config.env up -d
服务在 Docker 中运行后,导航到以下 URL 以查看 Jupyter、MinIO、MLRun 和 Nuclio 的控制台。
-
Jupyter Notebook:http://localhost:8888/lab
-
MinIO控制台:http://localhost:9001/browser
-
MLRun 控制台:http://localhost:8060/mlrun/projects
-
Nuclio 控制台:http://localhost:8070/projects
在结束在开发计算机上设置 MLRun 的练习之前,让我们创建并运行一个简单的无服务器函数,以确保一切正常。
运行简单的无服务器函数
在本演示中,我们将使用 Jupyter 服务器。它已经安装了 Python mlrun 库。如果您需要其他库,可以启动终端选项卡并安装它们。我们将需要 minio Python SDK。下面的屏幕截图显示了如何启动终端窗口并安装 minio 库。
单击“终端”按钮,您将获得一个类似于下面所示的选项卡。
此选项卡的操作方式与 Mac 上的“终端”应用程序类似。它允许您将所需的任何库安装到 Jupyter 服务中,我们的函数将在其中执行。
安装库后,创建一个名为“simple demo”的新文件夹并导航到它。下载此帖子的示例代码,并将以下文件上传到新文件夹:
- simple_serverless_function.py
- simple_serverless_function_setup.ipynb
- minio.env
- mlrun.env
- minio_utilities.py
我们要发送到 MLRun 的函数是 simple_serverless_function.py,如下所示。函数名称为“train_model”——我们不会在这篇文章中训练实际模型。我们只是想让无服务器函数正常工作。请注意,此函数具有“mlrun.handler()”装饰器。它包装了能够解析和保存输入和输出的函数。此外,它不必是密封的(仅使用其中定义的资源)——它可以使用在模块级别导入的库,这些库可以是其他代码模块。在我们的例子中,我们有一个位于同一目录中的minio实用程序模块。
from typing import Dict
import mlrun
import minio_utilities as mu
@mlrun.handler()
def train_model(data_bucket: str=None, training_parameters: Dict=None):
logger = mu.create_logger()
logger.info(data_bucket)
logger.info(training_parameters)
bucket_list = mu.get_bucket_list()
logger.info(bucket_list)
文件名: simple_serverless_function.py
在功能方面,这个函数所做的只是记录输入参数,只是为了好玩,我正在连接到 MinIO 以获取所有存储桶的列表。作为这个简单演示的一部分,我想证明 MinIO 的连接性。minio_utilities.py 中的 get_bucket_list() 函数如下所示。
import logging
import os
import sys
from typing import Any, Dict, List, Tuple
from dotenv import load_dotenv
from minio import Minio
from minio.error import S3Error
LOGGER_NAME = 'train'
LOGGING_LEVEL = logging.INFO
load_dotenv('minio.env')
MINIO_URL = os.environ['MINIO_URL']
MINIO_ACCESS_KEY = os.environ['MINIO_ACCESS_KEY']
MINIO_SECRET_KEY = os.environ['MINIO_SECRET_KEY']
if os.environ['MINIO_SECURE']=='true': MINIO_SECURE = True
else: MINIO_SECURE = False
def create_logger() -> None:
logger = logging.getLogger(LOGGER_NAME)
#if not logger.hasHandlers():
logger.setLevel(LOGGING_LEVEL)
formatter = logging.Formatter('%(process)s %(asctime)s | %(levelname)s | %(message)s')
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setLevel(logging.DEBUG)
stdout_handler.setFormatter(formatter)
logger.handlers = []
logger.addHandler(stdout_handler)
return logger
def get_bucket_list() -> List[str]:
logger = create_logger()
# Get data of an object.
try:
# Create client with access and secret key
client = Minio(MINIO_URL, # host.docker.internal
MINIO_ACCESS_KEY,
MINIO_SECRET_KEY,
secure=MINIO_SECURE)
buckets = client.list_buckets()
except S3Error as s3_err:
logger.error(f'S3 Error occurred: {s3_err}.')
raise s3_err
except Exception as err:
logger.error(f'Error occurred: {err}.')
raise err
return buckets
文件名: minio_utilities.py
用于保存 MinIO 连接信息的“minio.env”文件如下所示。您需要获取自己的访问密钥和密钥,并将它们放入此文件中。另外,请注意,我使用“minio”作为 MinIO 的主机名。这是因为我们从与 MinIO 相同的 Docker Compose 网络中的服务进行连接。如果要从此网络外部连接到此 MinIO 实例,请使用 localhost。
MINIO_URL=minio:9000
MINIO_ACCESS_KEY=lwycuW6S5f7yJZt65tRK
MINIO_SECRET_KEY=d6hXquiXGpbmfR8OdX7Byd716hmhN87xTyCX8S0K
MINIO_SECURE=false
minio.env file
回顾一下,我们有一个无服务器函数,它记录了传递给它的所有输入参数。它还连接到 MinIO 并获取存储桶列表。我们现在唯一需要做的就是打包我们的函数并将其传递给 MLRun。我们将在 simple_serverless_function_setup.ipynb 笔记本中执行此操作。下面是这个笔记本中的单元格。
导入我们需要的库。
import os
import mlrun
下面的单元格使用“mlrun.env”文件来通知我们的环境在哪里可以找到 MLRun API 服务。它还将 MLRun 连接到兼容 S3 的对象存储,以保存项目。这是我们通过 docker compose 文件创建的 MinIO 实例。
# Set the environment:
mlrun.set_environment(env_file='mlrun.env', artifact_path='s3://mlrun/simple-demo')
# remote MLRun service address
MLRUN_DBPATH=http://mlrun-api:8080
# AWS S3/services credentials
S3_ENDPOINT_URL=minio:9000
AWS_ACCESS_KEY_ID={Put MinIO access key here.}
AWS_SECRET_ACCESS_KEY={Put MinIO secret key here.}
文件名: mlrun.env file
接下来,我们创建一个 MLRun 项目。此项目将与我们的所有指标和工件相关联。用于此步骤的项目目录是代码所在的文件夹。
# Create the project:
project_name='simple-test'
project_dir = os.path.abspath('./')
project = mlrun.get_or_create_project(project_name, project_dir, user_project=False)
print(project_dir)
set_function() 方法告诉 MLRun 在哪里可以找到你的函数以及你希望它如何运行。handler 参数必须包含使用 mlrun.handler() 装饰器批注的函数的名称。
# Create the serverless function.
trainer = project.set_function(
"simple_serverless_function.py", name="trainer", kind="job",
image="mlrun/mlrun",
handler="train_model"
)
下面的单元格创建了一些示例模型训练参数。
# Sample hyperparameters
training_parameters = {
'batch_size': 32,
'device': 'cpu',
'dropout_input': 0.2,
'dropout_hidden': 0.5,
'epochs': 5,
'input_size': 784,
'hidden_sizes': [1024, 1024, 1024, 1024],
'lr': 0.025,
'momentum': 0.5,
'output_size': 10,
'smoke_test_size': -1
}
最后,我们开始运行该函数,如下所示。“local”参数告诉 MLRun 在当前系统上运行该函数,在本例中为 Jupyter Notebook 服务器。如果设置为 False,则该函数将发送到 Nuclio。此外,“inputs”参数的类型需要为 Dict[str, str];如果您使用其他任何东西,则会出现错误。
# Run the function.
trainer_run = project.run_function(
"trainer",
inputs={"data_bucket": "mnist"},
params={"training_parameters": training_parameters},
local=True
)
在 MLRun UI 中查看结果
功能完成后,转到 MLRun 主页以查找您的项目。单击表示项目的磁贴。我们的函数是在“简单测试”项目下运行的。
下一页将显示所有运行中与项目相关的所有内容。如下所示。
在“作业和工作流”部分下查看,然后单击要查看的运行。这将显示有关运行的详细信息,如下所示。
对调试有用的是“日志”选项卡。在这里,我们可以看到我们从简单函数中记录的消息,这些函数记录了输入参数,连接到 MinIO,然后记录了在 MinIO 中找到的所有存储桶。
摘要和后续步骤
在这篇文章中,我们使用 Docker Compose 在开发机器上安装了 MLRun。我们还创建并运行了一个简单的无服务器函数,以了解如何在 MLRun 中构建代码。我们的代码能够连接到 MinIO 以编程方式检索存储桶列表。我们还将 MLRun 配置为使用 MinIO 进行工件存储。
显然,我们简单的无服务器功能几乎没有触及 MLRun 可以做的事情的表面。接下来的步骤包括使用流行的框架实际训练模型,使用 MLRun 进行分布式训练,以及探索对大型语言模型的支持。