1. Databricks机器学习任务性能与成本优化导论

在 Databricks 上跑机器学习任务,很多人第一次遇到的核心矛盾并不是“模型不收敛”,而是“同样的模型,为什么这次跑了 3 小时、账单 40 DBU,上次只跑了 40 分钟、账单 8 DBU”。机器学习工作负载与普通 ETL 最大的区别在于:它同时消耗 计算资源(CPU/GPU、内存、Shuffle)、存储资源(Delta 表、特征表、Checkpoint、MLflow 产物)、调度资源(Job 集群、任务并发、重试)以及代码效率(Python UDF、pandas 转换、数据倾斜)。这四类资源互相牵制,任何单点优化都可能被另一处瓶颈抵消。本课程的目标,就是建立一套可度量、可定位、可复现的优化方法论,让你在 Databricks 上既能跑得快,也能花得少。

一、为什么机器学习任务特别“贵”

先看一个典型场景。假设你在 Databricks 上用 mlflow 训练一个梯度提升模型,数据来自 Delta 表 prod.features.user_behavior,规模约 8000 万行、120 列。任务链路大致是:读取 Delta → 特征工程(join、窗口、编码)→ 转成 Spark ML 的 Vector → 训练 → 评估 → 写 MLflow。这条链路里,真正“训练”的时间往往只占 20%~30%,其余时间消耗在数据读取、Shuffle、序列化和特征转换上。如果集群配置不当,比如用 8 个 worker 的通用集群去跑一个只需要 2 个 worker 的特征工程,或者用 CPU 集群去跑本可以 GPU 加速的深度学习,成本就会成倍放大。

更隐蔽的是“重复计算”。很多团队在 Notebook 里反复 display()、反复 fit(),每次触发全量重算;或者把特征工程写在 Driver 端的 pandas 里,导致数据先 collect 到单机再处理,既慢又容易 OOM。这些都不是模型问题,而是工程问题。

二、四个优化维度:计算、存储、调度、代码

我们把优化空间归纳为四个维度,它们不是并列的,而是有先后依赖关系:

  • 计算维度:集群规格(CPU/GPU、内存优化/计算优化)、worker 数量、自动伸缩、Spot 实例、Photon 加速、Spark 并行度(spark.sql.shuffle.partitions)、AQE(自适应查询执行)。
  • 存储维度:Delta 表的分区与 Z-Order、文件大小(避免小文件)、缓存策略(Delta Cache / 磁盘缓存)、特征表(Feature Store)的物化与复用、Checkpoint 与 MLflow 产物的生命周期。
  • 调度维度:Job 集群 vs 全用途集群、任务依赖 DAG、并发度、重试策略、集群池(Instance Pool)、按需启动与自动终止、任务级超时。
  • 代码维度:避免 Python UDF(优先用 Spark SQL 内置函数)、减少 collect()、用 mapInPandas/applyInPandas 替代逐行 UDF、向量化、缓存中间结果、避免重复 fit()。

这四个维度的关系可以用一张图来理解:计算和存储决定“单次执行”的成本,调度决定“多次执行”的浪费,代码决定“每次执行”的效率。优化顺序建议是:先看代码(最容易改、收益快),再看存储(减少 I/O 和 Shuffle),然后调计算(规格与并行度),最后优化调度(集群复用与生命周期)。

Databricks 机器学习任务优化四维度与执行链路 计算 集群规格 / Photon 并行度 / AQE 存储 Delta 分区 / Z-Order 缓存 / 特征表复用 调度 Job 集群 / 池 DAG / 自动终止 代码 避免 UDF / collect 向量化 / 缓存中间结果 典型 ML 任务执行链路(时间与成本分布示意) 读取 Delta I/O 瓶颈 特征工程 Shuffle / Join 向量化 序列化开销 训练 CPU/GPU 密集 评估 / 写 MLflow 产物存储 时间占比示意: 读取+特征 45% 向量化 15% 训练 25% 评估+写 15% 推荐优化顺序与预期收益 ① 代码优化 去 UDF / 去 collect 收益快、风险低 ② 存储优化 分区 / Z-Order / 缓存 减少 I/O 与 Shuffle ③ 计算优化 规格 / Photon / AQE 匹配负载特征 ④ 调度优化 Job 集群 / 池 生命周期管理

三、整体方法论:度量 → 定位 → 优化 → 验证

优化不是凭感觉调参数,而是一个闭环。第一步是度量:在 Databricks 中,Spark UI 的 Stage 详情、Task 时间分布、Shuffle Read/Write 大小、GC 时间,以及 Ganglia 的 CPU/内存曲线,都是第一手证据。同时要记录每次运行的 DBU 消耗和 wall-clock 时间,作为成本基线。第二步是定位:判断瓶颈属于哪一类。如果某个 Stage 的 Shuffle Write 特别大,说明是存储/分区问题;如果 Task 时间严重倾斜(max 是 median 的 10 倍以上),说明数据倾斜;如果 Driver 端内存持续高位,说明有 collect 或 pandas 转换。第三步是优化:按上一节的顺序,从代码到调度逐层处理。第四步是验证:用同一份数据、同一套参数重跑,对比优化前后的 Stage 时间、Shuffle 量和 DBU 消耗,确认收益真实且没有引入正确性问题。

实用建议:在 Notebook 顶部固定记录三个指标——run_id、wall_clock_min、dbu_cost。每次优化后更新,形成自己的成本曲线。这比任何“最佳实践”都更有说服力。

四、一个可参考的基线示例

假设某团队在 Databricks 上训练一个 XGBoost 模型,初始配置为:全用途集群,8 个 worker(Standard_DS3_v2,4 核 14GB),数据 8000 万行,特征工程用 Python UDF 做字符串编码,训练前 collect() 到 Driver 转 pandas。运行结果(示例假设):wall-clock 约 95 分钟,DBU 约 42。经过分析,瓶颈在 UDF 和 collect。优化后:改用 Spark SQL 内置 when/otherwise 编码,训练数据保持分布式,用 XGBoostRegressor 的 Spark 接口;集群改为 Job 集群 + 4 个 worker + Photon。重跑后(示例假设):wall-clock 约 28 分钟,DBU 约 9。这个示例说明,大部分成本节省来自代码和调度,而不是单纯“加机器”。

五、本课程的学习路径

后续章节会沿着“度量—定位—优化—验证”的主线展开:先讲如何读 Spark UI 和 Databricks 的账单指标,再分别深入计算、存储、调度、代码四个维度的具体手段,然后进入特征工程、超参搜索、分布式训练、模型部署等典型场景的优化案例,最后给出成本治理与团队协作的落地方法。每一节都会尽量给出可操作的参数、配置片段和判断依据,而不是停留在概念层面。

注意:优化不是一次性任务。数据规模、模型结构、集群版本都会变化,建议把性能与成本指标纳入日常监控,设置阈值告警,避免“优化一次、退化半年”。