Daft 快速入门:从 DataFrame 到 Swordfish、Flotilla 与 Ray

Daft 是面向 AI 与多模态工作负载的高性能数据处理引擎。它提供 Python DataFrame 和 SQL 接口,让图片、音频、视频、文本、Embedding 与普通结构化数据进入同一条声明式流水线;底层由 Rust 执行引擎负责向量化、流式执行和内存控制,并能从单机扩展到分布式集群。

本文希望帮助你在 30 分钟内理解 Daft 解决什么问题、跑通一个本地任务,并建立从 DataFrame 到 Swordfish、Flotilla 和 Ray 的完整心智模型。尤其会说清楚一个容易混淆的问题:daft.set_runner_ray() 确实表示使用 Ray 运行,但 Daft 并没有把查询规划和任务拆分全部交给 Ray。

1. 什么是 Daft

1.1 一句话定位

Daft 是一个 Python 原生、Rust 驱动、面向多模态与大规模数据的数据引擎。它把"读取数据---筛选---下载媒体---解析---调用模型---关联聚合---写回存储"表达为一张惰性 DataFrame 计算图,再由优化器和执行引擎决定如何分批、并行和分布式运行。

Daft 的价值不只是提供另一套 DataFrame API。传统表格引擎主要处理数值和字符串,而 AI 数据流水线还包含网络下载、图片解码、文档解析、模型初始化、GPU 推理和外部 API 调用等昂贵且资源异构的操作。Daft 可以把这些操作纳入查询计划,为它们安排合适的批大小、并发度和 CPU/GPU 资源,并尽可能在过滤、连接或聚合之后再执行昂贵计算。

1.2 常见使用场景

场景 Daft 扮演的角色 典型流水线
多模态 ETL 统一处理元数据与大对象 读取路径或 URL → 过滤 → 下载 → 解码/解析 → 写出
批量模型推理 负责分批、并发、资源声明与反压 预处理 → Embedding/分类/LLM → 后处理 → 持久化
数据集构建 编排清洗、去重、质量规则和样本生成 多源读取 → Join/Filter → UDF → 数据集
多模态数据湖 连接对象存储、Parquet、Lance 与 Catalog 数据湖读取 → 计算 → 回写
训练数据加载 将计算结果流式交给训练框架 读取 → 解码/增强 → 批处理 → DataLoader

1.3 什么时候适合使用

当任务同时包含结构化计算与多模态或 AI 算子、需要从本地平滑扩展到分布式,或者需要避免"每个阶段都落盘再交给下一个系统"时,Daft 很合适。它尤其适合 Read → Transform → Infer → Write 类型的离线流水线。

如果数据量很小、逻辑只是交互式表格分析,Pandas 或 Polars 往往更简单;如果已有成熟的 Spark SQL 数仓任务且没有多模态或 Python 模型计算,迁移收益可能有限;如果需要毫秒级在线请求服务,Daft 更适合作为离线数据处理层,而不是在线服务框架。

1.4 与常见框架的边界

框架 强项 与 Daft 的主要差异
Pandas / Polars 单机分析、生态成熟、上手快 Daft 更强调多模态类型、AI 算子和分布式扩展
Spark 大规模结构化 ETL、SQL 与企业数据生态 Daft 以 Python/Rust 为核心,更直接地承载 Python 模型、异构资源和流式多模态流水线
Ray Data 基于 Ray 的分布式数据处理 Daft 提供自己的查询计划、优化器、类型和表达式系统,并可使用 Ray 作为分布式运行时

2. 十分钟跑通第一个任务

2.1 安装

基础体验只需安装 Daft;需要 Ray 分布式执行时,再安装 Ray 扩展。生产环境建议固定 Daft、Python 和 Ray 版本,避免客户端与集群环境不一致。

复制代码
python -m pip install -U daft # 需要 Ray Runner 时安装 python -m pip install -U "daft[ray]"

2.2 第一个 DataFrame

复制代码
import daft from daft 
import col 

df = daft.from_pydict({ "name": ["alice", "bob", "cindy"], "score": [92, 55, 81], }) 
result = ( df.where(col("score") >= 60) .with_column("score_x2", col("score") * 2) .select("name", "score", "score_x2") ) 
# 前面的调用主要在构造计划;show() 才触发计算 result.show()

这里要先形成一个关键认识:wherewith_columnselect 通常不会立即遍历数据,它们是在构造 LogicalPlan。show()collect()to_arrow() 或写出方法才是物化边界。

2.3 最小多模态示例

复制代码
import daft from daft 
import col 

df = daft.from_pydict({ "image_url": ["https://example.com/a.jpg"] }) 
images = ( df.with_column("bytes", col("image_url").url.download()) .with_column("image", col("bytes").image.decode()) .select("image_url", "image") ) 
images.show()

真实任务应先根据尺寸、类型、标签等轻量元数据过滤,再下载或解码大对象。这样既减少网络 I/O,也避免图片等大字段过早进入内存和数据交换过程。

2.4 自定义 Python 计算

复制代码
import daft from daft 
import col 

@daft.func(return_dtype=daft.DataType.string()) 
def normalize_text(value: str) -> str: 
        return value.strip().lower() 

df = daft.from_pydict({"text": [" Hello ", "DAFT"]}) 
df.select(normalize_text(col("text")).alias("normalized")).show()

逐行无状态逻辑可以使用 @daft.func;向量化计算可以使用 @daft.func.batch;模型加载或连接初始化成本较高、需要跨批次复用时可以使用 @daft.cls。旧资料中的 @daft.udf 属于旧接口,编写新代码时应以当前安装版本的文档为准。

2.5 切换到 Ray Runner

复制代码
import daft 
# 未指定地址时,连接或启动本地 Ray 实例 
daft.set_runner_ray() 
# 连接已有 Ray Client 
# daft.set_runner_ray("ray://127.0.0.1:10001") 
# DataFrame 代码通常不需要改写

daft.set_runner_ray() 的准确含义是:将 Daft 的执行后端切换为 Ray Runner。它不是"绕过 Daft 调度、直接把 DataFrame 扔给 Ray"。查询计划、优化、分区任务生成和任务依赖仍由 Daft 负责,Ray 提供集群资源、Actor 生命周期、对象存储和跨节点通信等基础能力。

3. 核心编程模型与内存数据结构

3.1 从用户对象到执行对象

复制代码
DataFrame(惰性查询) 
└─ LogicalPlan:Source / Project / Filter / Join / Aggregate ... 
└─ Expression Tree:列引用 / 常量 / 内置函数 / Python UDF 
└─ PhysicalPlan:具体算法、交换和执行边界 
└─ Task / Operator Pipeline 
└─ Partition → MicroPartition → Arrow 列式数据

DataFrame。 它不是一份已经完全驻留内存的表,而是 Schema 加一棵尚未执行的计划。链式调用返回新的 DataFrame,并在逻辑计划上追加节点。

Expression。 col("score") * 2 并不立即计算,它构造了一棵表达式树。Project 节点中可以包含列引用、字面量、函数调用和 UDF。优化器可以分析这些表达式,执行投影裁剪、常量折叠等改写。

Schema 与 DataType。 Schema 描述列名和类型。严格类型既能在规划阶段发现一部分错误,也能帮助引擎选择合适的向量化内核。除基础和嵌套类型外,Daft 还提供适合多模态处理的 Image、Tensor、Embedding 和 Python 等类型。

Series 与 Arrow。 Series 表示一列数据。内置类型优先采用 Arrow 兼容的列式缓冲区:数值连续存储,字符串通常由 offset 和 value buffer 表示,空值由 validity bitmap 表示。算子可以按列或批次处理数据,并通过 Arrow 生态交换数据,减少序列化和不必要的复制。Python 对象类型更灵活,但会失去一部分列式、零拷贝和向量化优势。

MicroPartition。 它是执行边界上的小型分区对象,承载 Arrow 兼容的数据批及相关元信息。单机迭代时可以直接得到 MicroPartition;使用 Ray Runner 时,分区结果也可能以 Ray ObjectRef 返回。

3.2 Partition 与 Batch 不要混淆

概念 解决的问题 常用控制方式
Partition 把有限的数据工作单元分布到不同 Worker,影响分布式并行度与 Shuffle repartition(...) / into_partitions(...)
Batch 控制单个算子一次处理多少行,影响峰值内存、吞吐和 GPU 批推理效率 into_batches(...) 或 UDF batch 配置

Partition 主要是分布式执行的任务划分单位,Batch 则在单机和分布式模式下都存在。文件型输入通常形成与输入文件相关的分区;Join、GroupBy、Sort 等全局算子可能触发重新分区。图片解码、Explode、下载或返回大对象的 UDF 属于"数据膨胀"操作,必要时应在它们之前减小 batch。

3.3 内存中的典型数据流

复制代码

对象存储或本地文件 → Source 分批读取列式数据 → Filter / Project 消费并产生下一批 → 下载 / 解码 / UDF 使用各自的并发与批大小 → Join / Aggregate / Sort 等阻塞或交换节点 → Sink 流式写出,或 collect 到客户端

流式 Operator 不需要等全部上游完成,就可以继续传递批次;Aggregate、Sort 等阻塞 Operator 则需要积累更多输入。结果缓冲区和异步通道承担反压:下游消费变慢时,上游不会无限制地产生数据。

4. 单机与分布式调度原理

4.1 一条查询的生命周期

复制代码
Python DataFrame / SQL
        │
        ▼
LogicalPlan + Expression Trees
        │  过滤下推、投影裁剪、常量折叠、Join 优化等
        ▼
Optimized LogicalPlan
        │  选择算法并生成 PhysicalPlan
        ▼
执行
        ├─ Native Runner:Swordfish 在单机进程内执行
        └─ Ray Runner:Flotilla 分发分区任务
                         └─ 每个 Worker 内由 Swordfish 执行本地 Pipeline

Planning 和 Optimization 不因 Runner 不同而消失。DataFrame 或 SQL 先转换成逻辑计划,优化器完成规则或代价相关的改写,再生成物理计划。Native Runner 与 Ray Runner 的主要差异出现在物理执行阶段:前者在单机进程内调度算子,后者还需要把分区任务放到不同机器上。

4.2 Swordfish:单机流式执行

Swordfish 是 Daft Native Runner 的代号,也是每个分布式 Worker 内部使用的本地执行引擎。单机模式下,整条查询运行在一个进程中,不存在跨节点 Shuffle。

物理计划会被组织为 Operator Pipeline。Source 读取数据,Filter、Project、UDF 等中间 Operator 处理数据,Sink 返回或写出结果。流式算子可以在收到一个批次后立即开始处理并把结果交给下游,不必等待整份输入完全物化;全局 Aggregate、Sort 等阻塞算子则需要积累输入,形成同步或物化边界。

可以把单机调度理解为两层:

  1. 算子层调度。 Daft 根据物理计划组织 Source、转换算子和 Sink,控制哪些算子可以流水线并发运行。

  2. 批次层流动。 数据以较小的 batch 在算子之间传递。批次大小影响吞吐与内存,并发度影响同一时间有多少工作在运行。

当下游速度变慢时,有限缓冲区会产生背压,使上游暂停继续生产,避免中间结果无限堆积。网络下载、CPU 解码和模型推理的资源特征不同,因此可以拥有不同的 batch 和并发设置。

复制代码
复制代码
Local PhysicalPlan
  → 构造 Operator Pipeline
  → Source 按批次读取数据
  → Filter / Project / UDF 流式消费和生产
  → 缓冲区控制背压
  → Streaming Sink 提前输出
    或 Blocking Sink 汇总后输出

4.3 Flotilla:Daft 自己的分布式调度层

Ray Runner 也称为 Flotilla。它是 Daft 构建在 Ray 之上的分布式执行引擎,而不是 Ray Data 的别名。

在 Ray 集群中,Flotilla 调度器运行在 Head 节点。它根据分布式物理计划和 DataFrame Partition 生成任务,再依据数据本地性与 Worker 负载分配任务。每个 Worker 节点上,Daft 启动一个 Ray Actor,这个 Actor 内部承载一个 Swordfish 实例。换句话说,Flotilla 负责跨机器安排任务,Swordfish 负责每台机器内部的算子流水线执行。

复制代码
Distributed PhysicalPlan
  → Daft 按 Partition 生成任务并维护依赖
  → Flotilla 根据数据位置与负载选择 Worker
  → 通过 Ray Actor 把任务提交到目标 Worker
  → Worker 内的 Swordfish 执行本地 Pipeline
  → 输出进入 Ray Object Store 或交给下游任务
  → Flotilla 根据完成状态继续释放下游任务

Join、GroupBy、Sort、Repartition 等全局操作可能需要在 Partition 之间移动数据,也就是 Shuffle。Flotilla 决定任务和数据依赖关系;Ray 的 Object Store、网络和 Actor 通信负责承载中间数据与远程调用。

4.4 Daft、Flotilla、Swordfish 与 Ray 的职责边界

问题 Daft / Flotilla / Swordfish Ray
查询如何执行 Daft 负责 LogicalPlan、PhysicalPlan、算子选择和优化 不理解 Daft 查询语义
分布式任务如何产生 Flotilla 按计划与 Partition 产生任务并维护依赖 不负责拆分 Daft 查询
任务放到哪里 Flotilla 根据数据本地性和 Worker 负载选择目标 提供节点资源约束并运行远程调用
单个任务内部如何执行 Worker 内的 Swordfish 执行 Operator Pipeline 承载 Daft 的 Ray Actor 进程
中间结果如何流转 Daft 决定 Partition、Shuffle 和消费关系 提供 Object Store、网络和 Actor 通信

一句话概括:Swordfish 是单机流式执行引擎;Flotilla 是 Daft 自己的分布式调度与执行层;Ray 是 Flotilla 使用的集群运行时。

因此,下面两句话需要同时成立:

  • daft.set_runner_ray() 会让 Daft 使用 Ray 进行分布式运行。

  • 查询规划、任务拆分和 Daft 语义层面的调度仍由 Daft 的 Flotilla 负责,Ray 提供底层集群能力。

4.5 为什么惰性与流式对多模态特别重要

一张图片的 URL 只有几十到几百字节,解码后的像素却可能放大几个数量级。如果在 Join 或 Filter 之前下载并解码所有图片,网络、内存和 Shuffle 都会迅速放大。惰性计划允许优化器先处理轻量元数据,流式执行允许每一批在完成后立即进入下游;二者结合,可以显著降低中间数据驻留量。

5. 实践模式与性能调优

5.1 推荐流水线

  1. 读取轻量元数据和必要列。

  2. 尽早执行 Filter、列裁剪、去重和必要的 Join。

  3. 真正需要大对象时再下载、解码或读取内容。

  4. 优先使用内置向量化算子,缺失能力时再使用 @daft.func@daft.func.batch@daft.cls

  5. 为昂贵 UDF 声明合适的 CPU/GPU、并发与批大小。

  6. 大结果优先直接写入持久化存储,避免无必要的 collect()

5.2 UDF 选型

需求 建议接口 原因
简单逐行逻辑 @daft.func 最直接,可以作为 Expression 组合
NumPy、PyArrow 或模型批推理 @daft.func.batch 减少 Python 调用和模型启动开销
模型或连接需初始化一次并重复使用 @daft.cls 保留状态并摊薄初始化成本
异步外部 API async @daft.func 利用并发隐藏网络等待,同时应设置限流与重试

5.3 常见内存与性能问题

图片解码后 OOM。 先过滤尺寸和类型,解码前使用更小的 batch,处理完成后尽快只保留下游需要的列。

Driver 内存过高。 避免对大结果调用 collect() 或转换为 Python list;优先使用写出方法或分区迭代,并控制结果缓冲区。

GPU 利用率低。 检查数据下载和 CPU 预处理是否成为瓶颈,使用批 UDF 和状态类复用模型,适度提高并发和 batch,不要只增加 GPU 数量。

Shuffle OOM 或调度变慢。 检查是否携带了图片字节、Tensor 等大列;先裁剪大字段,再评估 Partition 数量和 Shuffle 配置。

任务重试产生重复外部副作用。 LLM/API 调用、数据库 append 等 UDF 或 Sink 不一定具备 exactly-once 语义。应为请求和输出设计幂等键、去重或 upsert,不要假设框架能撤销已经发送到外部系统的请求。

6. 从入门到进阶的学习路线

阶段 目标 建议练习
第 1 阶段 理解惰性 DataFrame 读取 Parquet/Pydict,完成 Filter、Select、WithColumn 和写出
第 2 阶段 掌握多模态表达式 URL 下载、图片解码/Resize、Embedding 计算
第 3 阶段 掌握自定义计算 分别实现逐行、批量和有状态模型 UDF
第 4 阶段 理解执行与内存 比较不同 batch,观察执行计划和峰值内存
第 5 阶段 分布式运行 切换 Ray Runner,调整 Partition,执行 Join 或 GroupBy

完成这些练习后,你应该能够回答以下问题:

  • 为什么调用 with_column 后任务还没有真正执行?

  • DataFrame、Expression、MicroPartition、Partition 与 Batch 分别是什么?

  • Native Runner 与 Ray Runner 的调度边界在哪里?

  • Swordfish、Flotilla 与 Ray 分别负责什么?

  • 什么时候应该选择逐行函数、批函数或状态类?

  • 为什么图片解码和模型推理通常应放在过滤与列裁剪之后?

参考资料

相关推荐
我不介意孤独5 个月前
Ray + LanceDB + Daft 构建大规模向量数据分析管道
数据挖掘·数据分析·分布式计算·ray·lancedb·daft·大规模数据分析
泡泡茶壶_ovo8 个月前
Zero-Shot Image Captioning with Multi-type Entity Representations(AAAI 2025)
人工智能·深度学习·计算机视觉·imagecaptioning·multimodal
泡泡茶壶_ovo8 个月前
RETHINKING VISUAL INFORMATION PROCESSING IN MULTIMODAL LLMS
llms·multimodal