mmops SDK 使用指南
mmops 是AnalyticDB for MySQL提供的多模态数据处理 Python SDK。安装后,您可以在 Ray Dataset 上调用文本、文档、图片、音视频和行业领域算子,也可以与自定义算子(UDF)组合,并将结果保存为 Parquet 或 JSON 数据集。本文介绍 mmops 的安装、运行方式、数据读写、模型配置以及在 AnalyticDB Ray 上提交作业的方法。
前提条件
-
本地运行时,Python 版本为 3.11 及以上。已验证的基础环境为 Python 3.12、Ray 2.59 和 PyArrow 23。
-
在 AnalyticDB Ray 上运行时,已创建 Ray Cluster 资源组。具体操作,请参见托管Ray服务。
-
已下载 mmops 的 wheel 安装包。具体操作,请参见下文的获取发布包。
安装
获取发布包
当前 0.1.0 版本的 wheel 包对象为公开读,可以直接下载,无需 OSS 凭证:
curl -fL -o ./mmops-0.1.0-py3-none-any.whl \
'https://adb-mmops.oss-cn-beijing.aliyuncs.com/sdk/0.1.0/mmops-0.1.0-py3-none-any.whl'
也可以直接访问 mmops 0.1.0 wheel 下载地址。
安装 SDK 与依赖
在独立的虚拟环境中安装 wheel 包。以下示例使用 Python 3.12,命令中的 wheel 文件名替换为实际获取到的文件:
python3.12 -m venv .venv-mmops
source .venv-mmops/bin/activate
python -m pip install './mmops-0.1.0-py3-none-any.whl[ray]'
python -m pip check
python -c 'import mmops; print(mmops.__version__)'
[ray] 安装 Ray Dataset 的运行依赖。如果本地已安装同版本 wheel,重新安装时请使用 python -m pip install --force-reinstall --no-cache-dir './mmops-0.1.0-py3-none-any.whl[ray]'。
在 AnalyticDB Ray 上运行时,依赖版本须与资源组的实际 Python 和 Ray 版本一致。
可选依赖组
按实际使用的算子追加 extras。例如同时处理 HTML、图片和 OSS 文件:
python -m pip install './mmops-0.1.0-py3-none-any.whl[ray,office,media,oss]'
各 extra 的用途如下:
|
extra |
用途 |
额外准备 |
|
|
Ray Dataset 与分布式执行 |
集群运行时,所有节点使用相同版本 |
|
|
PDF 解析与渲染 |
按所选接口准备输入文件 |
|
|
HTML、Word、Excel、PowerPoint 等文档处理 |
转换旧版 DOC 文档另需 LibreOffice |
|
|
图片与部分媒体处理依赖 |
处理音视频另需 FFmpeg 和 FFprobe |
|
|
算子访问 |
配置执行节点的 OSS 地址和凭证 |
|
|
通过原生 OSS filesystem 读写数据集 |
配置执行节点的 OSS 地址和凭证 |
|
|
具身数据预处理 |
按接口准备轨迹、传感器等数据 |
|
|
导出对应格式的训练数据 |
分别安装对应 extra,并准备目标运行环境 |
wheel 包仅包含 SDK,其余依赖由 pip 从可用的包源安装,模型权重和系统工具需另外准备。完全离线部署时,请提前准备所选依赖的安装包。
运行第一个算子
Ray Dataset 将多条记录组织为数据集,每条记录的字段对应数据列。mmops 算子按指定的输入列处理记录,并返回可继续加工的 Dataset。
以下脚本仅需 [ray],不调用模型,也不需要外部素材。将代码保存为 quickstart.py 后,使用已安装 SDK 的 Python 执行 python quickstart.py:
import json
from pathlib import Path
import tempfile
import ray
from mmops.ray import enable_accessor
enable_accessor()
ray.init(num_cpus=4, include_dashboard=False)
try:
source = ray.data.from_items([
{"source_id": "doc-1", "text": "第一段。\n\n第二段。"},
])
result = source.mmops.split_text(
text_column="text",
keep_columns=["source_id"],
config={"strategy": "paragraph", "max_chars": 80, "overlap_chars": 0},
).materialize()
rows = result.take(1)
print(json.dumps(rows, ensure_ascii=False, indent=2))
assert [chunk["content"] for chunk in rows[0]["chunks"]] == ["第一段。", "第二段。"]
output_dir = str(Path(tempfile.mkdtemp(prefix="mmops-text-")) / "parquet")
result.write_parquet(output_dir)
restored = ray.data.read_parquet(output_dir).take(1)
assert restored == rows
print("结果已保存并回读:", output_dir)
finally:
ray.shutdown()
结果包含来源标识和两个文本片段,字段顺序可能不同:
[
{
"chunks": [
{"content": "第一段。", "chunk_index": 0, "source_page": null, "title_breadcrumb": null},
{"content": "第二段。", "chunk_index": 1, "source_page": null, "title_breadcrumb": null}
],
"source_id": "doc-1"
}
]
enable_accessor() 为当前进程的 Ray Dataset 启用 .mmops 入口。算子接收 Dataset 并返回 Dataset,便于继续加工。take()、materialize() 和 write_parquet() 等操作会触发执行。上述示例先调用 materialize(),使预览和写出复用同一份结果;大规模任务可直接写出,避免将全部结果取回 Driver。
输入列与结果列
以下约定适用于内置 Dataset 算子,各算子的具体字段请参见离线算子概述中的算子目录:
|
写法 |
含义 |
|
|
从每行的 |
|
|
本次调用共用的配置或字段定义。 |
|
|
在标准结果列之外,显式保留来源字段。 |
|
|
控制 Ray 的执行批次、并发实例数和每个实例申请的 CPU 资源。 |
算子默认只返回接口声明的结果列,原输入列不会自动全部保留。keep_columns 与标准结果列重名时会报错,需先用 Ray Data 原生的 rename_columns() 改名。
split_text 每行输出一个 chunks 列表,不会自动将一行展开为多行。如需每个片段占一行,请在后续使用 flat_map()。
通用算子从 ds.mmops 调用,行业领域算子分别从 ds.mmops.game、ds.mmops.product 和 ds.mmops.embodied 调用,算子名称与输入输出请参见游戏领域算子概述、商品场景算子概述和具身智能算子概述。
数据集操作与算子组合
创建和加工 Dataset
Ray Data 负责创建、读取和写出数据集。以下示例展示筛选、生成字段、文本分块与展开:
import ray
from mmops.ray import enable_accessor
enable_accessor()
ray.init(num_cpus=4, include_dashboard=False)
try:
source = ray.data.from_items([
{"source_id": "doc-1", "body": " 第一段。\n\n第二段。 "},
{"source_id": "doc-2", "body": " "},
])
cleaned = source.filter(lambda row: bool(row["body"].strip()))
cleaned = cleaned.map(lambda row: {
"source_id": row["source_id"], "text": row["body"].strip(),
})
chunks = cleaned.mmops.split_text(
text_column="text",
keep_columns=["source_id"],
config={"strategy": "paragraph", "max_chars": 80, "overlap_chars": 0},
)
flattened = chunks.flat_map(lambda row: [
{"source_id": row["source_id"],
"chunk_index": chunk["chunk_index"], "text": chunk["content"]}
for chunk in row["chunks"]
])
result = flattened.select_columns(["source_id", "chunk_index", "text"])
print(sorted(result.take(2), key=lambda row: row["chunk_index"]))
finally:
ray.shutdown()
输出如下:
[{'source_id': 'doc-1', 'chunk_index': 0, 'text': '第一段。'},
{'source_id': 'doc-1', 'chunk_index': 1, 'text': '第二段。'}]
其中 filter()、map()、flat_map() 和 select_columns() 是 Ray Data 原生方法,mmops.split_text() 是 SDK 提供的算子。后续可以继续接入字段提取、向量化或质量筛选等算子。
读取和写出数据
在已初始化 Ray 的脚本中,使用 Ray Data 原生读写接口:
# 输入路径需事先存在,文件中的字段即 Dataset 列。
source = ray.data.read_parquet("/shared/input/documents/")
# 其他常用格式:
# source = ray.data.read_json("/shared/input/documents.jsonl")
# source = ray.data.read_csv("/shared/input/documents.csv")
result = source.mmops.split_text(
text_column="text", keep_columns=["source_id"],
config={"strategy": "length", "max_chars": 800, "overlap_chars": 80},
)
result.write_parquet("/shared/output/document-chunks/job-001/")
# 或:result.write_json("/shared/output/document-chunks-json/job-001/")
每次任务使用独立的输出目录,避免重复执行时混入旧分片。多行结果不保证全局顺序,请使用 source_id、chunk_index 等字段关联记录。
文档解析后分块
安装 [ray,office] 后,可以在已初始化 Ray 的脚本中将 HTML 解析与文本分块串联。以下示例的输入文件需事先存在:
source = ray.data.from_items([
{"source_id": "page-1", "source_uri": "/shared/input/guide.html"},
])
parsed = source.mmops.extract_html(
document_column="source_uri", keep_columns=["source_id", "source_uri"],
)
result = parsed.mmops.split_text(
text_column="text", keep_columns=["source_id", "source_uri"],
config={"strategy": "length", "max_chars": 800, "overlap_chars": 80},
)
result.write_parquet("/shared/output/html-chunks/job-001/")
extract_html 只提取静态正文和表格,不执行页面中的 JavaScript。其他格式请从文档算子概述中选择,每一步均需显式保留后续要用的来源列。
图片处理与文件产物
安装 [ray,media] 后,以下示例会生成一张 1000×800 的示例图片,将其等比缩放到 512×512 的包围盒内,并保存结果表。该示例不依赖模型服务。
import json
from pathlib import Path
import tempfile
from PIL import Image
import ray
from mmops.ray import enable_accessor
root = Path(tempfile.mkdtemp(prefix="mmops-image-"))
input_path = root / "sample.png"
Image.new("RGB", (1000, 800), color="white").save(input_path)
enable_accessor()
ray.init(num_cpus=4, include_dashboard=False)
try:
source = ray.data.from_items([
{"source_id": "image-1", "source_uri": str(input_path)},
])
result = source.mmops.resize_image(
image_column="source_uri",
output_dir=str(root / "images"),
width=512, height=512,
keep_columns=["source_id", "source_uri"],
).materialize()
rows = result.take(1)
assert (rows[0]["width"], rows[0]["height"]) == (512, 410)
assert Path(rows[0]["image_uri"]).is_file()
result.write_parquet(str(root / "metadata"))
print(json.dumps(rows, ensure_ascii=False, indent=2))
print("图片和结果表目录:", root)
finally:
ray.shutdown()
结果列包括 image_uri、width、height、format 以及显式保留的来源字段。image_uri 指向 SDK 生成的 PNG 文件。resize_image 等比缩放不裁剪,因此实际尺寸为 512×410。
output_dir 保存图片等文件产物,write_parquet() 保存结果列和文件地址。如需同时交付处理后的图片与结果表,请保留这两个位置。音视频的转换、裁剪和采样也遵循各接口声明的文件产物约定。
调用模型算子
文本字段提取、图片描述、向量化和语音转写需要配置对应的模型服务。请先在运行环境中设置服务地址、模型名称和凭证。例如使用支持 JSON 输出的 OpenAI 兼容文本服务:
export MMOPS_MODEL_BASE_URL='https://<model-host>/v1'
export MMOPS_TEXT_MODEL='<text-model-name>'
export MMOPS_MODEL_API_KEY='<由模型服务方提供>'
以下示例将上述配置显式传给 Ray 执行进程,再从文本中提取商品名称和价格。服务地址、模型名称和凭证需替换为实际值。
import json
import os
import ray
from mmops.ray import enable_accessor
env_vars = {name: os.environ[name] for name in (
"MMOPS_MODEL_BASE_URL", "MMOPS_TEXT_MODEL", "MMOPS_MODEL_API_KEY",
)}
enable_accessor()
ray.init(
num_cpus=4, include_dashboard=False,
runtime_env={"env_vars": env_vars},
)
try:
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"price": {"type": ["number", "null"]},
},
"required": ["name", "price"],
"additionalProperties": False,
}
source = ray.data.from_items([
{"source_id": "product-1", "text": "名称:收纳盒;价格:29.9 元。"},
])
result = source.mmops.extract_fields(
text_column="text", schema=schema, keep_columns=["source_id"],
config={"prompt": "仅提取原文明示信息,未提供价格时返回 null。"},
)
print(json.dumps(result.take(1), ensure_ascii=False, indent=2))
finally:
ray.shutdown()
结果直接包含 name、price 和 source_id 列。Schema 决定固定的结果列集合,缺少必填字段、出现额外字段或类型不符都会报错。模型提取的内容需结合实际输入校验。
各模型算子的配置入口如下,完整参数请参见对应的接口文档:
|
算子 |
配置参数 |
默认模型环境变量 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
例如,在已初始化 Ray 并配置视觉模型服务的脚本中调用图片描述:
images = ray.data.from_items([
{"source_id": "image-1", "source_uri": "/shared/input/sample.png"},
])
captions = images.mmops.caption_image(
image_column="source_uri", keep_columns=["source_id", "source_uri"],
prompt="描述可见物体、颜色和位置。",
model_config={"api_key_env": "MMOPS_MODEL_API_KEY"},
)
print(captions.take(1))
图片描述的结果为 text 列。使用该示例前,还需向执行进程注入 MMOPS_VISION_MODEL 和服务地址:服务地址可使用 MMOPS_VISION_BASE_URL,未配置时回退到 MMOPS_MODEL_BASE_URL。图片向量化和语音识别的服务协议、地址可能与文本服务不同,请按对应接口配置。各环境变量的完整说明,请参见离线算子概述。
SDK 不会自动加载 .env 文件,配置需注入每个执行进程。请勿将密钥写入 Dataset 的数据列。
使用自定义算子(UDF)
函数 UDF
自定义处理通过 Ray Data 原生接口加入流水线:逐行处理使用 map(),批量处理使用 map_batches()。以下示例先规范文本空白,再调用内置的分块算子:
import re
import ray
from mmops.ray import enable_accessor
def normalize_text(batch):
return {
"source_id": batch["source_id"],
"text": [re.sub(r"\s+", " ", text).strip() for text in batch["text"]],
}
enable_accessor()
ray.init(num_cpus=4, include_dashboard=False)
try:
source = ray.data.from_items([
{"source_id": "doc-1", "text": " Ray Data\n 使用说明 "},
])
cleaned = source.map_batches(
normalize_text, batch_format="numpy", batch_size=32, num_cpus=1,
)
result = cleaned.mmops.split_text(
text_column="text", keep_columns=["source_id"],
config={"strategy": "length", "max_chars": 80, "overlap_chars": 0},
)
print(result.take(1))
finally:
ray.shutdown()
类 UDF
需要在批次之间复用模型、客户端或其他状态时,使用可调用类。__init__ 在每个 Actor 内初始化,__call__ 处理后续批次。以下类复用编译后的正则表达式:
class NormalizeText:
def __init__(self):
self.whitespace = re.compile(r"\s+")
def __call__(self, batch):
return {
"source_id": batch["source_id"],
"text": [self.whitespace.sub(" ", text).strip() for text in batch["text"]],
}
# 在上面的脚本中,用这一段替换 cleaned = source.map_batches(...)。
cleaned = source.map_batches(
NormalizeText,
compute=ray.data.ActorPoolStrategy(size=1),
batch_format="numpy", batch_size=32, num_cpus=1,
)
模型权重也可以在类的构造方法中加载,供该 Actor 的后续批次复用。使用本地 GPU 模型时,请在 map_batches() 中按需设置 num_gpus。完整的输入校验、状态管理和重试约定,请参见自定义离线算子(UDF)。
在集群中运行
AnalyticDB Ray 提供作业执行与资源调度,mmops 在作业中提供 ds.mmops 算子。生产批处理建议通过 Ray Jobs 提交,使 Driver 和 Worker 使用同一份作业依赖。
在 AnalyticDB Ray 上提交作业
先按托管Ray服务准备资源组,并在资源组详情中获取可访问的 HTTP 连接地址,例如 http://<ADB_RAY_HOST>:8265。提交端须能访问该地址,执行节点须能访问 wheel 包地址和所用的 pip 源。
提交前请核对资源组的 Python 和 Ray 版本。基于DMS Notebook进行Ray交互式开发使用 Python 3.11.13 和 Ray 2.49.2,以下示例按 Ray 2.49.2 配置,实际版本不同时请同步调整。建议先用下面的无模型任务验证目标资源组。
在独立目录 adb-mmops-job 中准备两个文件:
adb-mmops-job/
main.py
runtime_env.yaml
runtime_env.yaml 安装 SDK,并固定资源组的实际 Ray 版本:
pip:
packages:
- "ray[data]==2.49.2"
- "mmops[ray] @ https://adb-mmops.oss-cn-beijing.aliyuncs.com/sdk/0.1.0/mmops-0.1.0-py3-none-any.whl"
pip_check: true
main.py 连接作业所在的集群,执行文本分块并检查输出:
import ray
from mmops.ray import enable_accessor
ray.init(address="auto")
enable_accessor()
try:
source = ray.data.from_items([
{"source_id": "doc-1", "text": "ADB Ray 上运行 mmops。"},
])
result = source.mmops.split_text(
text_column="text", keep_columns=["source_id"],
config={"strategy": "length", "max_chars": 800, "overlap_chars": 80},
)
rows = result.take(1)
assert rows[0]["chunks"][0]["content"] == "ADB Ray 上运行 mmops。"
print(rows)
finally:
ray.shutdown()
在独立的提交环境中安装匹配版本的 CLI,再进入作业目录提交作业。连接地址替换为实际值:
python -m pip install 'ray[default]==2.49.2'
cd adb-mmops-job
ray job submit \
--address 'http://<ADB_RAY_HOST>:8265' \
--working-dir . \
--runtime-env runtime_env.yaml \
-- python main.py
提交作业时请注意以下事项:
-
工作目录只放作业文件,不要放虚拟环境或密钥,因为 Ray 会将该目录打包上传。
-
作业提交层的
runtime_env同时应用于 Driver、Task 和 Actor。仅在ray.init(runtime_env=...)中配置依赖不会安装到 Driver 本身。更多信息,请参见 Ray 依赖管理。 -
按任务需要添加
pdf、office、media、oss等 extras。FFmpeg、LibreOffice 等系统工具需在执行环境中另外准备。 -
模型配置通过提交层的
env_vars或节点环境提供,凭证不要写入上传目录。 -
同版本 wheel 包被覆盖后,Ray 仍可能复用已有的依赖环境。此时可在 wheel 包地址末尾添加或更新
#sha256=<本次发布的 SHA256>,使 pip 配置随产物变化并校验下载内容。
交互调试可以使用 ray://<ADB_RAY_HOST>:10001 连接,但 Notebook 与集群的 Python、Ray 版本须一致,且 SDK 须安装在 Notebook 自身的环境中。批处理请使用上述 Ray Jobs 方式。
连接其他 Ray 集群
上述本地示例使用 ray.init(num_cpus=4, ...)。连接已准备好的 Ray 集群时,将初始化改为:
import os
import ray
from mmops.ray import enable_accessor
enable_accessor()
env_vars = {name: value for name, value in os.environ.items()
if name.startswith("MMOPS_")}
ray.init(address="auto", runtime_env={"env_vars": env_vars})
运行前,请在 Driver 和每个 Worker 的镜像或环境中安装相同版本的 wheel 包、Ray 与所需依赖。runtime_env.env_vars 传递环境配置,但文件路径指向的模型上下文或权重仍需在节点上实际可读。如果代码中通过 api_key_env 指定了其他凭证变量名,也需将该变量传入执行环境。
文件与对象存储
集群中的 /shared 表示所有 Worker 节点均可读写的共享挂载目录。该目录需要实际准备,不是 AnalyticDB Ray 默认提供的目录。Driver 的临时目录、下载目录和 file:// 路径不会自动分发到其他节点,因此前文的临时文件示例仅适用于本地运行。AnalyticDB Ray 的 Worker 磁盘用于临时数据,正式结果请写入已配置的持久化存储。
|
访问对象 |
使用方式 |
|
算子的输入文档、图片、音视频 |
按接口提供 Worker 节点可访问的本地路径、 |
|
算子的媒体文件产物 |
使用各接口的 |
|
Parquet、JSON、CSV 结果数据集 |
使用 Ray Data 原生读写接口,远端存储按 Ray 对应的 filesystem 配置。 |
SDK 访问 OSS 文件时,需要安装 [oss],并向执行进程注入 MMOPS_OSS_ENDPOINT、MMOPS_OSS_ACCESS_KEY_ID 和 MMOPS_OSS_ACCESS_KEY_SECRET。使用 STS 临时凭证时,还需提供 MMOPS_OSS_SECURITY_TOKEN。这些变量仅用于 SDK 的文件访问,不会自动配置 Ray 或 Arrow 的数据集 filesystem。快速上手时,可以先将结果表写入共享挂载目录;如需直接在 OSS 上读写结果数据集,请参见下文。
在 OSS 上读写数据集
读写 OSS 上的 Parquet 等数据集时,使用 mmops.storage.oss_filesystem() 显式创建原生 OSS filesystem,并将其传给 Ray Data 的读写接口。该接口需要安装 ossfs extra。如果已安装不含该接口的早期同版本 wheel 包,请加 --force-reinstall 重新安装:
python -m pip install --force-reinstall './mmops-0.1.0-py3-none-any.whl[ray,ossfs]'
python -m pip check
运行前,在执行环境中配置 MMOPS_OSS_ENDPOINT(OSS 原生 Endpoint)、MMOPS_OSS_ACCESS_KEY_ID 和 MMOPS_OSS_ACCESS_KEY_SECRET。使用 STS 临时凭证时,还需配置 MMOPS_OSS_SECURITY_TOKEN。
以下代码片段需在已初始化 Ray 的脚本中运行,先写入两行数据,再回读并逐行比对。请将 your-bucket 替换为有访问权限的 Bucket:
import uuid
import ray
from mmops.storage import oss_filesystem
# 前置条件:已初始化 Ray,并已配置 MMOPS_OSS_* 环境变量。
filesystem = oss_filesystem()
path = f"your-bucket/mmops/test/check-{uuid.uuid4().hex}/"
expected = [{"id": 1, "text": "第一条数据"}, {"id": 2, "text": "第二条数据"}]
ray.data.from_items(expected).write_parquet(path, filesystem=filesystem)
rows = ray.data.read_parquet(path, filesystem=filesystem).take_all()
assert sorted(rows, key=lambda row: row["id"]) == expected
print(f"OSS 写入、回读校验成功:oss://{path}")
显式传入 filesystem 时,路径格式为 bucket/prefix,对应的对象地址为 oss://bucket/prefix。该方式通过 ossfs 和 oss2 调用 OSS 原生 API,无需配置 S3 兼容 Endpoint 或 AWS checksum。每次任务请使用独立的前缀,避免混入旧数据。
批次与资源
内置算子的 batch_size 是 Ray 每批处理的输入行数,增大该值不代表模型请求会自动合并。concurrency 控制执行实例数,num_cpus 是每个实例申请的资源。实际并行度还受数据分块和集群可用资源的限制。
内置的图片理解、语音识别等算子通过远端模型服务调用,不在 Worker 节点上申请本地 GPU。自定义 UDF按需声明 num_gpus,并确保模型权重对所有执行节点可见。
常见问题
|
现象 |
处理方法 |
|
找不到 |
在 Driver 中先执行 |
|
缺少可选依赖或系统命令 |
按所选算子安装对应 extra。媒体处理检查 FFmpeg 和 FFprobe,DOC 转换检查 LibreOffice。 |
|
Worker 节点无法读取输入或模型上下文 |
检查路径在执行节点是否存在,改用共享挂载目录或可访问的 URI。 |
|
来源字段消失 |
每个内置算子调用都通过 |
|
保留输入列时报重名错误 |
用 Ray Data 的 |
|
模型地址、模型名称或凭证缺失 |
检查 Worker 节点的环境变量。SDK 不会自动加载 |
|
模型结果不符合字段 Schema |
核对必填、可空和类型约束,以及模型服务的 JSON 输出能力。 |
无效输入、文件读写、模型调用和结果校验失败都会抛出错误,失败不会被替换为空文本、空向量或有效样本。
相关文档
-
离线算子概述:算子目录、调用约定与模型服务配置。
-
自定义离线算子(UDF):输入校验、有状态执行和重试约定。
-
Ray Data 文档:Dataset 原生读写和执行接口。