
1. 数据工程为什么是大模型的“隐形上限”从很小规模的模型跑起到后面拿几百亿参数做预训练我最大的感受是模型结构反而是最好解决的部分真正让人夜里睡不着的是训练数据。一个词表配错、一行脏数据没滤掉、重复语料没有被识别出来等训练跑到一半再发现几万张卡时已经烧进去了回滚都是血泪。这也是我们实验室做 DataFlow 的起点。DataFlow 是我们内部对“大规模语言模型数据工程框架”的代号核心职责是把原始网页、PDF、代码仓库、对话日志这些乱糟糟的输入加工成可以直接喂给大模型训练的数据集。如果把大模型训练看成一条流水线DataFlow 就是源头的水厂水质不行后面再好的净化和管道都白搭。这套框架过去一年多支撑了我们内部多个从十亿到百亿参数规模的训练项目也经历了从“能用”到“好用”的完整踩坑过程。今天这篇文章不讲花哨的算法重点拆解三件事为什么必须专门做一套数据工程框架、DataFlow 的架构是怎么设计的、以及在生产环境里落地时那些文档里不会写的问题。无论是只训练过小模型的学生还是正在搭建数据管道的工程师这篇文章应该都能给你一些参考。1.1 数据质量如何决定模型表现很多人对“数据质量”的理解比较粗觉得差不多是“干净”就行。但大模型训练里数据质量的影响维度远不止干净至少包括下面几个层次正确性字段错位、编码损坏、抓取乱码、标签不匹配最基础但也最容易出事。去重级别很多公开数据集的重复率远超想象。爬虫抓下来的网页转码后大量历史版本彼此相似它们会把训练重点“拉”到重复模式上模型学过一遍又一遍最后在 benchmark 上出现诡异的过拟合。质量分布一句话 spam、纯标题列表、乱码占位符这些低质量样本看似影响不大但数量上来之后会让模型学会输出“无意义但你无法确定它在瞎说”的文本。配比结构代码、数学、常识、对话的比例不同模型的推理和对话能力会完全不同。同一个模型仅仅调整数据配比下游指标可以差出几个点。我第一次意识到这件事是在某个实验里把一份清洗不干净的网页数据混进了训练集。模型 loss 曲线倒是正常下降但生成的文本里开始频繁出现“404 Page Not Found”这类句子而且这个坏习惯持续到训练结束都没能通过后续 SFT 纠正回来。从那以后我对数据管道的敬畏程度就完全不同了。1.2 传统离线流水线解决不了的三件事团队最早也尝试过直接用通用大数据技术栈来搭数据管道但折腾下来发现有三件事是传统方式很难解决的。第一是“数据量太大但有效信息密度太低”。全网抓取的数据动辄几十亿文档但里面大量是重复页面、自动生成内容、模板化结构。通用数据处理框架擅长的是“计算”不擅长定义什么是“一个文档的有效信息”。这需要专门围绕语言语义特征设计规则。第二是“数据需要在短时间内刷新”。预训练不是一次性任务。一开始你可能只需要清洗一版数据但很快就会发现模型训练到一半需要补充更多数学数据或者某个领域表现不行要重新调整配比。传统离线 ETL 从任务提交到产出数据可能要跑几个晚上根本跟不上训练侧的迭代节奏。第三是“数据管道的可复现性”。跑了一遍清洗流程后我们经常被问训练效果变好是因为数据变干净了还是因为数据配比变了如果管道没有版本控制、参数记录和逐条数据的来源追踪这个问题永远无法回答。这三个痛点逼着我们开始设计 DataFlow它不是一个脚本集合而是一套带调度、监控、版本管理的工业化数据加工系统。2. DataFlow 核心架构设计与模块拆解对很多第一次接触数据工程的人来说DataFlow 这类系统听起来有点像“用大数据工具做一遍清洗”。但真正上手之后你会发现难点根本不在框架本身而在如何把各个加工步骤合理地组织起来让数据在流和批之间平滑流转。DataFlow 的整体设计遵循了一条很务实的原则离线能搞定的事不强行流式化流式能解决的事不盲目堆批任务。下面拆开讲。2.1 整体分层从采集到样本落库的六条链路DataFlow 在逻辑上分为六层采集层、解析层、过滤层、去重层、配比层、输出层。每层之间用统一格式的数据集作为接口层与层之间互不感知。采集层支持从公开网页、内部知识库、代码仓库、对话日志等不同源拉取数据统一转成中间格式。这里最关键的是“来源追踪”每一条数据从出生起就带一个 source_id。解析层把原始 HTML、PDF、Markdown 中的正文提取出来清理噪声。这一步看似简单实际上是最花精力的部分后面我会细讲。过滤层基于一组规则和启发式指标快速滤掉明显的低质量样本。比如字符乱码比例、平均句子长度、文本压缩率等。去重层先做局部去重再做全局去重。具体算法见 2.2。配比层把剩余样本按域名、语言、难度、任务类型打标按训练需求计算各类别的采样权重输出有序的数据流。输出层将最终结果写成模型训练框架直接可读的格式并生成一份数据版本清单和统计报告。各层之间通过“断点续跑”机制衔接。因为全量数据太大不可能每次重新跑一整条链路每一层产出的中间数据都带数据集 ID 和依赖版本号。如果某层逻辑改了系统会自动计算受影响范围只重新处理增量部分。2.2 分布式去重MinHash-LSH 与全局 Bloom Filter 的双层方案去重是数据管道里技术含量最高、也是踩坑最多的地方。单纯按 URL 去重、按文本指纹去重都远远不够因为数据里大量内容是“相似但不相同”的同一篇新闻转载后改了标题同一份代码在不同仓库里格式不同。DataFlow 的去重链路分成两步各自解决不同粒度的问题。第一步是精确去重使用规范化后的文本哈希。先把所有文本做 Unicode 归一化、全半角转换、去空白然后计算 SHA-1再对哈希分桶做全局去重。这一步能干掉完全一致的副本成本低、吞吐高。第二步是近似去重用的是一套比较经典的组合MinHash 生成文本指纹 LSH 分桶寻找候选项 编辑距离做最终验证。简单说说原理。每篇文档先用若干哈希函数抽取 shingle 集合再用 MinHash 把集合映射成固定长度的签名。比如我常用签名长度是 128 位然后把 128 位拆成 16 个 band每个 band 8 行。这里有个关键参数需要认真调。两个文档相似度越高MinHash 签名中每个位置相等的概率就越高。而 LSH 的逻辑是只要有一个 band 内所有行完全一致就认为这两篇文档可能是重复的。band 数量越多、每个 band 行数越少查得越“激进”容易误伤band 越少、行数越多查得越“保守”容易漏掉相似文本。我们最终选用了 16 band × 8 行的组合实测定在相似度阈值 0.75 以上效果最好既能把转载文本抓出来又不至于把同一个专题下的不同文章误删。在工程实现上MinHash 这一步可以很好地并行化但“全局去重”这一步天然有状态。我们采用了两层设计第一层在单分区内做局部去重第二层用一个分布式的全局 Bloom Filter 来拦截跨分区的重复项。Bloom Filter 的大小按照预估文档数来定公式上让假阳性率控制在 1% 以内。这样设计的好处是跨节点的查询只需一次网络请求不会因为全量交换数据把集群打挂。2.3 质量过滤与样本配比编排去重只是解决了“重复”问题接下来要解决的是“质量”和“结构”。质量过滤在 DataFlow 里是“规则 模型”两级。第一级规则过滤跑的是启发式特征比如平均句长是否在合理区间2 到 150 个字符之间有多少比例的字符是中文、英文、数字文本压缩率是否异常压缩后太小说明重复模式过多是否包含大量广告、验证码、错误提示等标志词这些规则看起来简单但同样的数据只靠规则远远不够。我们还会跑一个轻量级语言模型用困惑度来评估“这段文本像不像人写的”。困惑度过高的样本直接淘汰困惑度过低的反而要小心因为可能是高度重复的口水话。两级过滤之后一个典型的网页语料集通常会损失掉 50% 到 70% 的数据量留下来的才是真正对训练有价值的文本。样本配比层解决的问题是不同来源的数据该以什么比例混合。我们维护了一份“数据画像”即对每个样本做领域分类和质量评分。配比时不是简单按照来源文件的大小来分配而是按照“有效 token 数”来计算。举例代码数据占比 15%数学数据占比 10%网页文本占比 60%书籍和论文占比 15%。即使某一类原始数据特别多也要通过采样上限把它压住否则模型能力会严重偏向数据量多的那个领域。此外配比层支持简单的课程学习策略。训练早期多放高质量、多样性的数据训练后期逐渐增加更复杂、更专项的数据。这个策略不需要单独写死在训练脚本里而是由 DataFlow 给每批数据分配一个“epoch 区间”训练框架按当前 step 所处的区间来采样。Model 训练那边完全不用感知数据是怎么排的这个解耦对实际使用非常友好。3. DataFlow 从开发到生产的落地实操架构讲完真正把它搭起来跑通中间还会有一大堆细节。这一节写的是我们在实际部署和线上运行中的经验尽量给出可直接参考的参数和步骤。3.1 部署形态与资源规划DataFlow 本身不依赖特定的集群类型我们内部是跑在一组独立的 CPU 节点上的。之所以不用 GPU 节点是因为大部分数据处理任务解析、规则过滤、去重都是纯 CPU 密集只有质量打分里的模型推理那一步需要 GPU。把这部分单独拆成一个推理服务比把整个管道都放到 GPU 上要省钱得多。推荐的基础配置是这样调度与编排用一套 DAG 任务调度器负责所有数据处理任务的依赖管理、失败重试和资源申请。计算节点每个节点 64 核 CPU、256GB 内存、万兆网卡。处理 10 亿文档量级的任务时我们一般申请 20 到 30 个这样的节点。存储中间数据用 Parquet 格式放在分布式文件系统上按数据集 ID 分区。原始数据放冷存储只有当前活跃任务涉及的中间结果保持在热存储内。推理服务质量打分用的轻量语言模型单独部署 2 到 4 个 GPU 节点对外提供 gRPC 接口按请求量动态扩容。部署形态不需要很复杂但要保证中间数据能快速访问。我们刚开始直接用单个 NFS 目录存所有中间结果数据量上来之后 IO 立刻成为瓶颈。后来改成“每层任务产出自己的数据集目录按时间戳和 ID 分区”读取速度和任务之间的隔离性都明显改善。3.2 一个真实任务的配置与执行用一个“网页语料清洗”任务来举例。这是一个典型的 DataFlow 作业配置片段定义了从原始页面到最终训练集的完整链路字段含义我逐一解释。dataset: id: web_corpus_v3 source: web_crawl_2025 description: 2025年第一季度全网抓取语料清洗任务 clean: normalization: unicode: true lower_case: true remove_control_chars: true boilerplate_filter: min_text_len: 200 max_text_len: 50000 quality_rules: max_bad_word_ratio: 0.01 max_repetition_ratio: 0.25 min_avg_sentence_len: 15 max_avg_sentence_len: 200 dedup: minhash: signature_size: 128 band: 16 rows_per_band: 8 shingle_size: 13 global_bloom: capacity: 1200000000 false_positive_rate: 0.01 filter_model: endpoint: http://quality-model.internal:8501/score batch_size: 512 max_score_threshold: 80 min_score_threshold: 5 mixing: target_tokens: 50000000000 sampling: code: 0.15 math: 0.10 web: 0.60 book: 0.15 curriculum: early_epochs: [0, 2] later_epochs: [2, 8] output: format: parquet partition_by: [domain, quality_level] version: v3.1有几个参数值得特别说明。shingle_size: 13表示每个文本片段取 13 个连续的词/字符作为一个局部特征。这个值取决于你的文本类型代码类数据可以设小一点因为代码的重复模式更多网页文本可以设大一点避免把正常的常用语当作相似特征。false_positive_rate: 0.01意味着 Bloom Filter 会误判大约 1% 的新数据为重复。这个误判率看起来不高但在百亿文档的量级下1% 就意味着上亿条有用数据被丢掉。所以全局去重阶段我们采用的是“Bloom Filter 初筛 精确哈希复核”Bloom Filter 说重复的不一定真的重复还要到精确去重表中再查一次。这个双检机制让实际误删率接近于零。配比层的target_tokens写的是“希望最终训练集包含多少 token”而不是“希望处理多少原始数据”。这个指标和训练框架的 tokenizer 紧密关联必须用同一个分词器来做预估否则配比会偏离预期。整个任务跑下来从原始网页到最终训练集时间大概在 10 到 14 小时。清洗完的数据集划分成 1024 个分片训练框架直接按分片读取不再需要额外的数据加载流程。3.3 可观测性与数据血缘追踪DataFlow 在生产环境里跑起来之后最容易被忽视的就是可观测性。训练阶段出了问题还可以靠 loss 曲线和采样样本来分析但数据处理阶段如果出了问题没有足够的观测手段你会连问题出在哪一层都不知道。我们在 DataFlow 里维护了四类关键指标吞吐量每分钟处理多少文档、多少 token按层拆开统计。丢弃率每一层过滤掉了多少比例的数据如果某个规则的命中率突然大幅波动说明上游数据源可能发生了变化。去重率精确去重和近似去重分别删掉了多少数据这里能直观看到数据集的冗余程度。质量分布质量分过滤器输出的分数直方图用来判断当前数据源的整体质量趋势。数据血缘方面每一条最终训练数据都带一个 lineage 字段记录它来自哪个原始文件、经过了哪些清洗规则、被谁过滤过、质量分是多少。这个设计在排查问题时价值极大。有一次我们发现模型代码能力下降顺着 lineage 一查发现某个代码仓库的解析器在最近一次版本升级后把函数定义行给吞掉了导致大量代码数据变成了“只有函数名没有函数体”的残缺文本。没有血缘追踪这个问题根本不可能定位到解析器那一层。4. 生产环境中的三大性能瓶颈与调优DataFlow 上线后我们遇到的性能问题主要集中在三个地方去重阶段的 IO 与内存压力、增量管道的延迟、以及数据处理与模型训练之间的节奏耦合。逐一分析如下。4.1 去重阶段的 IO 与内存压力去重是跨节点操作最容易出性能问题。早期版本的全局去重会把所有文档的 MinHash 签名集中到少数几个节点上进行聚合结果是内存被打爆任务频繁失败。后来我们把流程拆成两个阶段先按 MinHash 签名做分区让相似的文档大概率落到同一分区分区内部做局部去重再去全局 Bloom Filter 查一次跨分区重复。这两个阶段组合起来既保证了去重的准确性又避免了将所有签名全量加载进内存。Bloom Filter 本身也大量占用内存。容量 12 亿文档、假阳性率 1% 的 Bloom Filter大概需要 1.4GB 左右的空间这个体量可以常驻内存。真正会打爆内存的是 MinHash 签名聚合那一步解决思路是控制每个分区的大小同时用 RocksDB 之类的单机 KV 存储来承接一部分溢出的签名数据而不是全都塞在内存里。4.2 增量管道的延迟优化一开始我们把 DataFlow 设计成“全量清洗优先增量再追加”的模式但很快发现增量管道的延迟对模型迭代影响极大。模型侧可能今天就需要一批新的数学题数据来测试能力变化DataFlow 如果几个小时内给不出来那训练侧就只能空等。增量优化的核心是“不要每次都从头跑”。DataFlow 对每个数据集维护了一个 checkpoint记录每一层处理到哪个位置。新数据进来之后只需要从上次断点处继续处理而已经处理过的部分直接跳过。增量管道的另一个优化点是“合并小文件”。分布式存储上如果大量小文件频繁写入NameNode 压力会非常大。我们在增量任务结束之后会做一次 compaction把小文件合并成大文件同时更新数据集的版本号。这个操作对训练框架特别友好因为它读取数据时不喜欢遇到一万个零碎小文件。4.3 模型训练与数据处理高度耦合时的节奏控制预训练任务的常规节奏是数据管道先产出几份可用的数据集训练脚本按顺序消费。但如果训练中途发现数据集有问题或者某个领域数据不够需要临时补充数据处理和训练任务就会出现抢资源的局面。拿我们踩过的坑举例某个 100B 参数规模的训练任务在跑到第 8000 步时验证集上的数学能力明显偏弱。我们需要马上补一批高质量数学数据跑增量清洗和配比重算。但是训练任务已经把大部分计算资源占住了数据管道挤在剩下的资源里跑延后了好几个钟头才把数据交给训练侧整个实验节奏全被打乱。之后我们做了一件事把数据管道的资源优先级设为“绝对优先”。虽然训练任务在算力调度中的权重很高但数据处理任务的时长往往只有几小时优先级提高之后对训练的整体影响很小却能保证数据侧按需供给。这个调整看似很简单但对团队协作效率和实验迭代速度的提升非常明显。5. 常见故障排查与经验速查这一节把我们在使用 DataFlow 过程中遇到的典型问题整理成一张速查表方便你在自己的场景里快速定位。这些问题不算冷门但每个都真实发生过处理方式也经过了反复验证。5.1 高频问题与排查对照表现象可能原因排查步骤与解决方案去重后数据量骤降只剩 20%LSH 参数设置过于激进band 太多导致相似文本被大量误判检查去重层的日志统计看被删数据中有多少是精确重复、多少是近似重复。若近似去重占比过高增大 rows_per_band 或减小 band 数重新调参后增量重跑某个清洗任务频繁 OOM单个分区内的文档数量过大或某篇文档超大检查任务日志中每个分区的内存峰值。对大文档单独设置 max_text_len超过阈值先截断再处理。同时将读取端分区大小调整为原来的 1/2 或 1/4训练数据里仍有重复样本全局去重只做了初筛精确复核环节没生效拉一条去重层的输出样本核对 lineage 字段。检查精确去重表是否因为数据版本更新而被清空导致 Bloom Filter 拦截过的数据没有被真正验证某一层处理速度突然慢如蜗牛数据源格式发生变化解析层在做无效重试查看解析层的失败率分布。若失败集中在某个域名或文件类型检查采集端是不是换了模板格式。对异常格式做兼容处理而不是等它一次次超时重试配比层算出的各领域 token 比例和预期不符分词器不匹配或预估 token 用的还是旧词表确认配比层和训练框架调用的是同一个 tokenizer且 tokenizer 的词表版本一致。修改后重新估算各领域的 token 数模型训练中 loss 突然出现尖峰新一批数据混入了被误判为高质量的异常文本用 lineage 筛选出尖峰时刻附近被消费的数据样本检查是否来自同一个数据源。对异常数据源做针对性过滤或回滚到之前的数据版本5.2 几个需要特别小心的隐蔽坑第一规则过滤器的“误杀”比“漏杀”更可怕。漏掉一点垃圾数据对几百亿 token 的训练集来说影响通常可控但如果规则写严了把某个领域的有效数据大量清掉模型在那个领域的能力会永久性缺失而且很难在训练后补回来。我强烈建议每次调整过滤规则时不仅看“滤掉了多少”还要抽样查看滤掉的数据里有多少其实是有价值的。这个审查流程哪怕每天只做一次都能避免很多方向性错误。第二文本规范化必须放在所有处理的最前面。我们有一个阶段曾经把语言检测放在清洗之后结果乱码文本因为“看起来像某种语言”被保留了下来白白浪费了半天的处理时间。正确的顺序是先做 Unicode 归一化、控制字符清理、编码纠错再跑语言识别和规则过滤。第三增量更新不是万能的。过度依赖增量会导致数据分布慢慢漂移。举例来说某一周网页语料里突然暴增大量模板化问答页增量管道会把这些数据当作“新数据”全盘接收而全量数据的统计特性已经变了。所以我们的做法是增量管道每天运行但每周强制跑一次全量重算并把全量结果与前一周的结果做对比。对比报告里如果发现某一类的占比变化超过阈值就需要人工介入检查。最后再分享一点个人体会数据工程通常不是一篇论文能搞定的东西它更像是一个需要持续打磨的基础设施。DataFlow 从最初的脚本集合到现在的工业级框架最大的变化不是代码量多了多少而是我们把“数据质量”从一句口号变成了可量化、可追踪、可回溯的工程指标。如果你正准备搭建自己的 LLM 数据管道我的建议是不要一开始就追求复杂的架构。先用手头的工具把手里的数据彻底清洗一遍记录下每一步的损失率、质量和时间然后你会发现真正该用框架解决的地方其实就那么几块去重的精度、配比的灵活度、以及问题出现时的可追溯性。DataFlow 也只是这几个问题的其中一种解法。后续我们还在尝试把数据影响力分析Data Influence接入 DataFlow。目标很简单就是让每一行数据的清洗逻辑都能映射回模型能力的变化——哪些规则提升了代码能力哪些规则无意中伤害了生成质量。这条路还很远但值得走下去。