Data-Juicer RayEmptyFormatter 算子深度解析:为 Ray 分布式数据处理链路创建空数据集的完整指南
Data-Juicer RayEmptyFormatter 算子深度解析:为 Ray 分布式数据处理链路创建空数据集的完整指南
导读
RayEmptyFormatter 是 Data-Juicer 中专门用于 Ray 执行环境的 formatter(格式化)算子,核心能力是根据指定长度与字段列表,在内存中直接生成一张"全空"的 Ray Dataset,无需任何磁盘文件或外部数据源。它在算子流水线调试、空数据集校验、基于 --generated_dataset_config 的合成数据生成等场景中非常实用。读完本文,你将掌握 RayEmptyFormatter 的参数语义、底层实现原理、与原生 EmptyFormatter 的关键差异,以及如何在 Data-Juicer 配置中把它接入数据集加载链路。
RayEmptyFormatter 是什么
在 Data-Juicer 中,formatter 是数据管线的"入口"组件,负责把各种来源的数据(本地文件、远程仓库、内存对象)统一加载为后续算子可以处理的 Dataset。绝大多数 formatter(如 JsonFormatter、ParquetFormatter、CsvFormatter)都依赖真实文件路径,而 RayEmptyFormatter 是一个特例:它不读取任何文件,而是按用户指定的行数与列名,构造一个元素全为空({})的 Ray 数据集。
该算子的算子类型为 formatter,标签为 cpu,其官方说明为:
The class is used to create empty data for ray.(该类用于为 Ray 创建空数据。)
在 Ray 执行模式下,Data-Juicer 的数据集对象是 ray.data.Dataset,这与默认 Hugging Face datasets 生态的 Dataset 不同。因此仓库在 data_juicer/format/empty_formatter.py 中同时提供了两个对称实现:
EmptyFormatter:面向普通(非 Ray)执行环境,返回 Hugging FaceDataset;RayEmptyFormatter:面向 Ray 执行环境,返回ray.data.Dataset。
两者的构造参数完全一致,仅底层数据载体不同,这也正是关联文档标题中 "ray_" 前缀的含义。
参数配置详解
依据关联文档的参数配置表,并结合源码签名(def __init__(self, length, feature_keys: List[str] = [], *args, **kwargs)),参数说明如下:
| name 参数名 | type 类型 | default 默认值 | desc 说明 |
|---|---|---|---|
length |
int | 必填(文档默认值列为 '',实际源码中为必传位置参数) |
The empty dataset length,即要生成的空数据集行数 |
feature_keys |
typing.List[str] | [] |
feature key name list,即空数据集的列名列表 |
args |
- | '' |
透传给 load_dataset 的额外位置参数 |
kwargs |
- | '' |
透传给 load_dataset 的额外关键字参数 |
需要特别说明两个参数的实现细节(见 empty_formatter.py):
length:决定生成数据集的记录条数,相当于"要造多少行空样本";feature_keys:决定生成的列。源码中有一个友好的容错逻辑——当传入的是单个字符串而非列表时,会自动转换为单元素列表:
这意味着if isinstance(self.feature_keys, str): self.feature_keys = [self.feature_keys]feature_keys="text"与feature_keys=["text"]等价。
底层实现原理:从参数到 Ray Dataset
空值语义:{} 而非 None
RayEmptyFormatter 通过 null_value 属性定义"空值"的形态(empty_formatter.py):
@property
def null_value(self):
return {}
返回的是空字典 {}。这与非 Ray 版本 EmptyFormatter 的 null_value(返回 None)形成鲜明对比。原因在于 Ray 数据管道对样本结构的约束:每一行样本应是一个 dict 样式的记录,用空字典占位可以让后续算子以 sample<a href="https://link.gitcode.com/i/be354d5d459eee51220851992fd00b10" target="_blank">key] 的方式安全访问与覆写字段,而 None 值样本在部分算子中会被直接过滤(可参考 [formatter.py 中 non_empty_text 对 None 文本的过滤逻辑)。
load_dataset 的完整实现
核心加载逻辑位于 empty_formatter.py:
def load_dataset(self, *args, **kwargs):
if len(self.feature_keys):
df = pd.DataFrame({col: [self.null_value for _ in range(self.length)] for col in self.feature_keys})
else:
df = pd.DataFrame([self.null_value for _ in range(self.length)])
empty_dataset = ray.data.from_pandas(df)
return empty_dataset
执行流程可分为两步:
- 构造 pandas DataFrame:
- 当指定了
feature_keys(如["text", "meta", "label"])时,为每个列名生成length行、每行值均为{}的列,形成一个"有结构无内容"的表格骨架; - 当
feature_keys为空列表时,则生成length行、每行都是空字典{}的无列 DataFrame。
- 当指定了
- 转换为 Ray Dataset:通过
ray.data.from_pandas(df)把内存中的 DataFrame 包装为分布式 Ray 数据集,交给后续 Ray 执行器处理。
从实现可以看到,args/kwargs 在该类中并未参与逻辑运算,仅作为与 BaseFormatter.load_dataset 接口签名保持一致的透传参数(基类定义见 formatter.py)。
如何配置使用:--generated_dataset_config 入口
虽然 Data-Juicer 常规加载流程通过文件后缀自动匹配 formatter(见 load.py 中 load_formatter 的 SUFFIXES 匹配逻辑),但 RayEmptyFormatter 与 EmptyFormatter 的 SUFFIXES 均为空列表 [],无法靠后缀自动选中。它们的正确接入方式是使用全局配置中的 --generated_dataset_config 参数。
该参数在 data_juicer/config/config.py 中定义:
--generated_dataset_config: Configuration for generating a dataset.
The type field selects a registered formatter; other fields are passed to its constructor.
即:type 字段用于选择已注册的 formatter 名称,其余字段会作为构造参数传给该 formatter。由此可以写出如下配置(YAML 风格):
generated_dataset_config:
type: ray_empty_formatter # 选择注册名为 ray_empty_formatter 的算子
length: 100 # 生成 100 行空数据
feature_keys:
- text
- meta
等价地,也可以在命令行直接传入 JSON 形式:
python tools/process_data.py \
--generated_dataset_config '{"type": "ray_empty_formatter", "length": 100, "feature_keys": ["text", "meta"]}'
Data-Juicer 的数据集加载优先级为:generated_dataset_config > dataset_path > dataset(见 dataset_builder.py),因此只要配置了 generated_dataset_config,就不需要再提供任何真实数据文件路径。此外,type 字段使用的是注册名,两个 formatter 在 data_juicer/format/init.py 中通过 FORMATTERS.register_module() 注册(注册名即类名的小写形式:empty_formatter 与 ray_empty_formatter)。
与非 Ray 版 EmptyFormatter 的差异对照
| 对比维度 | EmptyFormatter |
RayEmptyFormatter |
|---|---|---|
| 返回类型 | Hugging Face Dataset(包装为 NestedDataset) |
ray.data.Dataset |
| 空值形态 | None |
{}(空字典) |
| 构造方式 | Dataset.from_dict(data_dict, features=Features()),列类型固定为 Value("string") |
pd.DataFrame(...) + ray.data.from_pandas(df) |
| 适用环境 | 单机/普通执行器 | Ray 分布式执行器 |
一个值得注意的差异是类型系统:非 Ray 版为每个列显式声明了 Value("string") 特征,而 Ray 版依赖 pandas DataFrame 的列推断,不做显式 schema 声明。因此从数据结构层面看,Ray 版更像"宽松的占位骨架",适合流水线开发阶段用来验证算子链路是否能够跑通。
测试用例与行为验证
仓库对非 Ray 版 EmptyFormatter 提供了较为完整的单元测试(tests/format/test_empty_formatter.py),这些用例的行为约定同样适用于理解 Ray 版的语义(两版构造参数与空值策略同源):
- 指定长度与单列:
EmptyFormatter(length=10, feature_keys=["text"])生成 10 行、仅含text列、每行值为None的数据集,且可继续执行map与filter操作; - 多列:
feature_keys=["text", "meta", "label"]会为每个 key 各建一列,样本中所有 key 均为空值; - 字符串自动转列表:
feature_keys="text"会被包装为["text"]; - 零长度:
length=0生成 0 行但保留列 schema; - 无列场景:
feature_keys=[]时生成 0 行数据集(因为没有任何列可供构造行)。
这些测试覆盖了参数容错、边界长度与空字段等典型场景,可作为把 RayEmptyFormatter 接入 Ray 执行器时的行为参考基线。目前仓库测试集中尚无 RayEmptyFormatter 的独立测试用例,其正确性验证主要依赖 data_juicer/format/init.py 中的注册导出以及 Ray 执行链路的集成运行。
使用场景与注意事项
典型场景
- 算子链路冒烟测试:在 Ray 模式下,先用
ray_empty_formatter生成若干行空数据,快速验证 filter/mapper/selector 各环节是否可执行,避免在调试阶段反复准备真实数据集; - 结构占位:当流水线中需要"先有 schema、后填充内容"(例如配合
python_lambda_mapper、replace_content_mapper等就地覆写字段的算子)时,空字典占位可以确保字段访问不报错; - 配置化的合成数据入口:通过
generated_dataset_config与 HPO、流水线实验工具组合,批量生成不同长度/列结构的空数据集用于基准测试。
注意事项
length与feature_keys是必传参数(args/kwargs仅透传),配置时不要省略;- 生成的每行值是
{}而非None,若下游算子按None语义判空,需要适配这一差异; - 该算子不产生任何磁盘 IO,因此它不会命中按文件后缀匹配 formatter 的自动加载流程,必须显式通过
generated_dataset_config指定type: ray_empty_formatter; - 该算子只负责"造数据",与数据校验、算子执行完全解耦,实际效果需结合 Ray 执行器(如
ray_executor)整体验证。
相关链接
- 源代码 data_juicer/format/empty_formatter.py
- 单元测试 tests/format/test_empty_formatter.py
- formatter 基类与注册表 data_juicer/format/formatter.py
- formatter 自动匹配逻辑 data_juicer/format/load.py
- --generated_dataset_config 配置入口 data_juicer/config/config.py
- 数据集加载优先级 data_juicer/core/data/dataset_builder.py
- 返回算子列表 docs/Operators.md