From 244b287a6e3f761f1eb7bdccb50707653914de3b Mon Sep 17 00:00:00 2001 From: yuchengen Date: Fri, 29 May 2026 09:15:01 +0800 Subject: [PATCH] feat: add MLflow 3.10.1 container image for OC9 --- frameworks/MLflow/3.10.1/Dockerfile | 25 +++ frameworks/MLflow/3.10.1/README.md | 189 +++++++++++++++++++++++ frameworks/MLflow/3.10.1/build.conf | 4 + frameworks/MLflow/3.10.1/test.sh | 77 +++++++++ frameworks/MLflow/3.10.1/test_result.png | Bin 0 -> 18925 bytes 5 files changed, 295 insertions(+) create mode 100644 frameworks/MLflow/3.10.1/Dockerfile create mode 100644 frameworks/MLflow/3.10.1/README.md create mode 100644 frameworks/MLflow/3.10.1/build.conf create mode 100644 frameworks/MLflow/3.10.1/test.sh create mode 100644 frameworks/MLflow/3.10.1/test_result.png diff --git a/frameworks/MLflow/3.10.1/Dockerfile b/frameworks/MLflow/3.10.1/Dockerfile new file mode 100644 index 00000000..d997d51d --- /dev/null +++ b/frameworks/MLflow/3.10.1/Dockerfile @@ -0,0 +1,25 @@ +FROM opencloudos/opencloudos9-minimal:latest + +LABEL maintainer="stronking 363133710@qq.com" +LABEL org.opencontainers.image.source="https://gitee.com/OpenCloudOS/ai-agent-container" +LABEL org.opencontainers.image.description="MLflow 3.10.1 container image based on OpenCloudOS 9" + +ARG MLFLOW_VERSION=3.10.1 + +ENV LANG=en_US.UTF-8 +ENV LC_ALL=en_US.UTF-8 +ENV PYTHONUNBUFFERED=1 + +# Install MLflow +RUN python3 -m ensurepip +RUN --mount=type=cache,id=pip-cache-opencloudos9-cu128,target=/root/.cache/pip pip3 install mlflow==$MLFLOW_VERSION + +# Default MLflow configuration +ENV MLFLOW_HOST=0.0.0.0 +ENV MLFLOW_PORT=5000 + +EXPOSE 5000 + +WORKDIR /workspace + +CMD ["sh", "-c", "mlflow server --host ${MLFLOW_HOST} --port ${MLFLOW_PORT}"] diff --git a/frameworks/MLflow/3.10.1/README.md b/frameworks/MLflow/3.10.1/README.md new file mode 100644 index 00000000..f0711c8b --- /dev/null +++ b/frameworks/MLflow/3.10.1/README.md @@ -0,0 +1,189 @@ + +# MLflow on OpenCloudOS 9 + +## 基本信息 + +- **框架版本**:v3.10.1 +- **基础镜像**:opencloudos9-minimal +- **Python 版本**:3.11 +- **CUDA 版本**:N/A + +--- + +## 项目简介 + +[MLflow](https://github.com/mlflow/mlflow) 是一个开源的机器学习生命周期管理平台,主要提供以下能力: + +- 实验追踪(Experiment Tracking) +- 参数与指标记录 +- Artifact 管理 +- 模型注册(Model Registry) +- 模型部署(Serving) + +本镜像基于 OpenCloudOS 9 构建,提供轻量级 MLflow Tracking Server 运行环境。 + +--- + +## 构建 + +```bash +docker build -t oc9-mlflow:3.10.1 . +```` + +--- + +## 使用示例 + +### 查看 MLflow 版本 + +```bash +docker run --rm oc9-mlflow:3.10.1 \ + python3 -c "import mlflow; print(mlflow.__version__)" +``` + +--- + +## 启动 MLflow Tracking Server + +```bash +docker run -d \ + --name mlflow \ + -p 5000:5000 \ + oc9-mlflow:3.10.1 +``` + +访问: + +```text +http://localhost:5000 +``` + +--- + +## 持久化实验数据 + +默认情况下,容器中的实验数据会随着容器删除而丢失。 + +推荐挂载数据目录: + +```bash +docker run -d \ + --name mlflow \ + -p 5000:5000 \ + -v $(pwd)/mlruns:/mlruns \ + oc9-mlflow:3.10.1 \ + sh -c "mlflow server \ + --backend-store-uri sqlite:///mlruns/mlflow.db \ + --default-artifact-root /mlruns \ + --host 0.0.0.0 \ + --port 5000" +``` + +目录说明: + +```text +mlruns/ +├── mlflow.db +└── artifacts +``` + +--- + +## 实验追踪示例 + +创建示例脚本: + +```python +import mlflow +import random + +mlflow.set_tracking_uri("http://127.0.0.1:5000") +mlflow.set_experiment("demo-experiment") + +with mlflow.start_run(): + + mlflow.log_param("learning_rate", 0.01) + mlflow.log_param("epochs", 10) + + for step in range(10): + loss = 1.0 / (step + 1) + accuracy = 0.8 + random.random() * 0.1 + + mlflow.log_metric("loss", loss, step=step) + mlflow.log_metric("accuracy", accuracy, step=step) + + with open("result.txt", "w") as f: + f.write("training completed") + + mlflow.log_artifact("result.txt") +``` + +运行: + +```bash +python3 train.py +``` + +然后访问: + +```text +http://localhost:5000 +``` + +即可查看实验参数、指标和 Artifact。 + +--- + +## 容器网络使用示例 + +如果训练任务运行在其他容器中,建议使用 Docker Network。 + +创建网络: + +```bash +docker network create mlflow-net +``` + +启动 MLflow: + +```bash +docker run -d \ + --name mlflow \ + --network mlflow-net \ + -p 5000:5000 \ + oc9-mlflow:3.10.1 +``` + +训练容器中配置: + +```bash +export MLFLOW_TRACKING_URI=http://mlflow:5000 +``` + +--- + +## 默认配置 + +| 配置项 | 默认值 | +| ----------------- | ---------- | +| Host | 0.0.0.0 | +| Port | 5000 | +| Working Directory | /workspace | + +--- + +## 已知问题 + +* 当前镜像为轻量版,不包含 PyTorch、TensorFlow 等深度学习框架 +* 不包含 CUDA 与 GPU 运行环境 +* Model Serving 场景下,部分依赖需用户自行安装 + +--- + +## 上游项目 + +* MLflow: https://github.com/mlflow/mlflow +* OpenCloudOS: https://gitee.com/OpenCloudOS + +``` +``` diff --git a/frameworks/MLflow/3.10.1/build.conf b/frameworks/MLflow/3.10.1/build.conf new file mode 100644 index 00000000..baaea75b --- /dev/null +++ b/frameworks/MLflow/3.10.1/build.conf @@ -0,0 +1,4 @@ +# MLflow 3.10.1 on OpenCloudOS 9 +IMAGE_NAME=oc9-mlflow +IMAGE_TAG=3.10.1 +GPU_TEST=false \ No newline at end of file diff --git a/frameworks/MLflow/3.10.1/test.sh b/frameworks/MLflow/3.10.1/test.sh new file mode 100644 index 00000000..1cb9a5c4 --- /dev/null +++ b/frameworks/MLflow/3.10.1/test.sh @@ -0,0 +1,77 @@ +#!/bin/bash +set -e + +IMAGE="${1:?ERROR: 缺少镜像参数。用法: bash test.sh }" + +DOCKER_CMD="docker run --rm -e GIT_PYTHON_REFRESH=quiet" + +echo "=== MLflow 基础功能测试 ===" + +echo -n "检查 MLflow import 和版本... " +$DOCKER_CMD "$IMAGE" python3 -c "import mlflow; print(mlflow.__version__)" >/tmp/mlflow_import.log 2>&1 \ + && echo "✓ 通过" || { echo "✗ 失败"; cat /tmp/mlflow_import.log; exit 1; } + +echo -n "检查 MLflow CLI... " +$DOCKER_CMD "$IMAGE" mlflow --version >/tmp/mlflow_cli.log 2>&1 \ + && echo "✓ 通过" || { echo "✗ 失败"; cat /tmp/mlflow_cli.log; exit 1; } + +echo -n "检查实验追踪核心功能... " +$DOCKER_CMD "$IMAGE" python3 -c " +import os +import tempfile +import mlflow + +tmpdir = tempfile.mkdtemp() +db_path = os.path.join(tmpdir, 'mlflow.db') + +mlflow.set_tracking_uri('sqlite:///' + db_path) +mlflow.set_experiment('ci-test-experiment') + +with mlflow.start_run(): + mlflow.log_param('learning_rate', 0.01) + mlflow.log_metric('accuracy', 0.95) + + run = mlflow.active_run() + assert run is not None + assert run.info.run_id is not None + +print('MLflow experiment tracking works') +" >/tmp/mlflow_tracking.log 2>&1 \ + && echo "✓ 通过" || { echo "✗ 失败"; cat /tmp/mlflow_tracking.log; exit 1; } + +echo -n "检查 MLflow Tracking Server 启动... " +$DOCKER_CMD "$IMAGE" bash -c " +set -e + +TMPDIR=\$(mktemp -d) + +mlflow server \ + --backend-store-uri sqlite:///\$TMPDIR/mlflow.db \ + --default-artifact-root \$TMPDIR/artifacts \ + --host 127.0.0.1 \ + --port 5000 >/tmp/mlflow_server.log 2>&1 & + +PID=\$! + +for i in \$(seq 1 60); do + if curl -fs http://127.0.0.1:5000/health >/dev/null 2>&1; then + kill \$PID + wait \$PID 2>/dev/null || true + exit 0 + fi + + if ! kill -0 \$PID 2>/dev/null; then + cat /tmp/mlflow_server.log + exit 1 + fi + + sleep 1 +done + +cat /tmp/mlflow_server.log +kill \$PID 2>/dev/null || true +exit 1 +" >/tmp/mlflow_server_check.log 2>&1 \ + && echo "✓ 通过" || { echo "✗ 失败"; cat /tmp/mlflow_server_check.log; exit 1; } + +echo "=== 所有测试通过 ===" \ No newline at end of file diff --git a/frameworks/MLflow/3.10.1/test_result.png b/frameworks/MLflow/3.10.1/test_result.png new file mode 100644 index 0000000000000000000000000000000000000000..a5605dbb07711f3a7fc4d1fd311ef03a7dac8c26 GIT binary patch literal 18925 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
  • 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