使用 MLRun 和 MinIO 设置开发机器

Setting Up A Development Machine with MLRun and MinIO

MLOps 对于机器学习而言,就像 DevOps 对于传统软件开发一样。两者都是一组旨在改进工程团队(开发或机器学习)和 IT 运维团队(运维)之间协作的实践和原则。其目标是利用自动化简化开发生命周期,从计划和开发到部署和运维。这些方法的主要优势之一是持续改进。

在之前关于 MLOps 工具的文章中,我介绍了 KubeFlow Pipelines 2.0MLflow。在这篇文章中,我想介绍 MLRun,另一个 MLOps 工具,如果您正在为您的组织寻找 MLOps 工具,则应该考虑它。但在深入探讨之前,让我们快速了解一下 MLOps 的现状。

MLOps 的现状

虽然这三种工具都旨在为您的所有 AI/ML 需求提供完整的端到端工具,但它们都始于不同的动机或设计目标。

  • KubeFlow 的目标是使 Kubernetes 民主化。KubeFlow 的核心 KubeFlow Pipelines 要求代码通过部署到 Kubernetes Pod 的无服务器函数运行。
  • MLflow 最初是一个用于跟踪实验以及与您的 ML 实验相关的所有内容的工具,例如指标、工件(您的数据)和模型本身。MLflow 了解流行的 ML 框架,如果您选择,您可以要求 MLflow 自动捕获它认为重要的所有实验数据。
  • MLRun 的使命(使其有别于其他 MLOps 工具)是消除样板代码。它也需要创建无服务器函数,但它更进一步地发展了这个概念,因为它了解流行的 ML 框架——因此,不需要编写 epoch 循环,并且对分布式训练提供了低代码支持。

更多关于 MLRun 的信息

MLRun 是一个开源的 MLOps 框架,最初由专注于数据科学的公司 Iguazio 创建。2023 年 1 月,Iguazio 被麦肯锡公司收购,现在是麦肯锡 AI 部门 QuantumBlack 的一部分。

让我们看一下安装选项以及需要部署的服务。

安装选项

MLRun 网站 推荐三种在本地安装 MLRun 的方法。Iguazio 还维护着一个托管服务。

本地选项

  • 本地部署:使用 Docker compose 文件在您的笔记本电脑或单个服务器上部署 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 Notebook 和 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 配置
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 的控制台。

在结束我们在开发机器上设置 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 文件

概括来说,我们有一个无服务器函数,它记录传递给它的所有输入参数。它还连接到 MinIO 并获取存储桶列表。现在我们唯一需要做的就是打包我们的函数并将其传递给 MLRun。我们将在 simple_serverless_function_setup.ipynb 笔记本中执行此操作。下面是此笔记本中的单元格。

导入我们需要的库。

import os
import mlrun

下面的单元格使用“mlrun.env”文件通知我们的环境在哪里可以找到 MLRun API 服务。它还将 MLRun 连接到兼容 S3 的对象存储以保存工件。这是我们通过 docker compose 文件创建的 MinIO 实例。

# 设置环境:
mlrun.set_environment(env_file='mlrun.env', artifact_path='s3://mlrun/simple-demo')

# 远程 MLRun 服务地址
MLRUN_DBPATH=http://mlrun-api:8080

# AWS S3/服务凭证
S3_ENDPOINT_URL=minio:9000
AWS_ACCESS_KEY_ID={请在此处填写 MinIO 访问密钥。}
AWS_SECRET_ACCESS_KEY={请在此处填写 MinIO 密钥。}

文件名: mlrun.env 文件

接下来,我们创建一个 MLRun 项目。此项目将与我们所有的指标和工件相关联。您在此步骤中使用的项目目录是代码所在的文件夹。

# 创建项目:
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() 装饰器注释的函数的名称。

# 创建无服务器函数。
trainer = project.set_function(
  "simple_serverless_function.py", name="trainer", kind="job",
  image="mlrun/mlrun",
  handler="train_model"
)

下面单元格创建一些示例模型训练参数。

# 示例超参数
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]类型;如果使用其他类型,则会报错。

# 运行函数。
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 进行分布式训练,以及探索对大型语言模型的支持。我们将在以后的文章中进行介绍,敬请关注 MinIO 博客。

如果您有任何问题,请务必通过 Slack 与我们联系!