六毫秒换全量校验,这个参数我以前一直不敢加

最近在整一个五千行的对话数据集准备做微调。传之前我规规矩矩跑了一遍 bl dataset validate,绿了,valid: true,退出码 0。训练任务起来之后报数据格式错,坏行在第 2,500 行。

第一反应是平台抽风,第二反应是去翻自己那个文件。翻完才明白过来:校验没错,它只是没看到那一行。默认模式下,五千行的文件它只解析了 159 行。

后来我把这件事拆开测了一遍,记下五个坑。数字都是 2026-10-12 我在自己电脑上跑出来的,bl 2.1.0。会变的读数都带日期,别抄我的快照。

坑一:抽多少行不随文件变大而变多,中间还会往回掉

bl dataset validate --help 自己写了:每行做结构检查,然后抽大约 160 行(开头 50 行、中间等距 100 行、末尾 10 行)做 schema 检查。听着挺合理,落在实际文件上的行为跟直觉差得有点远。

我造了一批只有行数不同的干净文件,各埋一行坏的(JSON 合法但缺 messages),看默认模式抓不抓得到:

文件行数 实际解析行数 覆盖率 埋在中段的坏行
100 100 100% 抓到
160 110 68.8% 漏过
300 140 46.7% 抓到
501 134 26.7% 漏过
1000 154 15.4% 漏过
2000 157 7.9% 漏过
5000 159 3.2% 漏过

300 行和 501 行那两行值得多看一眼:300 行的文件解析了 140 行,501 行的反而只解析 134 行。文件更大,看得更少。我以前那个"数据集不大,默认校验够了"的想法就是这么站不住的,够不够跟行数压根不是单调关系。

再把坏行逐个位置试了一遍。在 501 行的文件上,位置 1、25、50、51 抓到;55、60、100、134、150、200、250、251、253、300、350、400、450、480、490、491 全部漏过;492、495、500、501 抓到。

开头窗实际到第 51 行(帮助文本写 50),末尾窗是最后 10 行(501−10+1=492),中间那 440 行我试的 16 个位置全没被解析。等距档在 501 行上有落点但跟我试的位置错开,在 300 行上又恰好命中;中段落点公式我没查明,只记位置对照,不推公式。

结论:默认模式是快速看一眼用的,不是保证能训用的;靠猜位置判断坏行在不在窗内,我试过了,不行。

抽样覆盖率随文件规模变化:501 行看 134 行,5000 行只看 159 行

坑二:那个 (slower) 我白怕了这么久

--full-validate 的帮助文本写着 JSON.parse every line instead of sampling (slower)。括号里那个 slower,就是我一直没加这个参数的理由。

那它到底慢多少?每档跑三次取中位数:

文件行数 默认耗时 全量耗时 差
501 8 ms 8 ms 0 ms
2000 8 ms 14 ms 6 ms
5000 15 ms 21 ms 6 ms

五千行,全量比抽样多花 6 毫秒。五百行上两者一模一样。

万行以内这个括号不构成取舍。我现在不管多大都加 --full-validate,代价是眨眼的六十分之一,换来"每行都真的被解析过"站得住。

超过万行我没测。官方给的文件上限是 SFT/DPO 200MB、CPT 300MB、带媒体的 zip 2GB,这三个数引自 bl dataset upload --help,带媒体的那种 zip 数据集我没实测过。真到那个规模上 slower 会不会变成分钟级,我不知道,不外推。

坑三:漏的是 schema 坏,不是结构坏

这条我一开始搞错了。help 那句"每行做结构检查,然后抽约 160 行做 schema 检查"是两套覆盖面。同一位置埋两种坏法:

坏行形态 位置 250(窗外) 位置 300(窗外)
JSON 本身非法(比如被截断) 抓到 抓到
JSON 合法但缺 messages 字段 漏过 漏过

非法 JSON 在任何位置都抓得到,因为结构检查是逐行的。字段缺失只在抽样窗内抓得到,因为 schema 检查是抽样的。

这个区别很实际。数据集要是被某个脚本写坏的(中途断掉、编码错乱、多了一个逗号),默认模式大概率能抓到;要是字段名写错、少了一列、role 拼错这类结构完整但内容不对的问题,窗外一律放行。后者恰恰是人工整理数据集时最常见的错法,我这次撞上的就是它。

所以"默认校验会漏坏行"得说准确:漏的是 schema 坏,不是结构坏。

坑四:照着 --help 建数据集,六种 schema 三种校验失败

这是这次测得最意外的部分。帮助文本给了六种 schema 的字段形状,我照着逐个构造了一遍,六种里有三种直接校验失败。

schema 帮助文本写的形状 照抄的结果
chatml {messages:[...]} ✅ 通过
cpt {text:"..."} ✅ 通过
tts {wav_fn:"train/xxx.wav", text:"..."} ✅ 通过
dpo {messages:[...], chosen, rejected} ❌ MESSAGE_NOT_OBJECT
image {img_path:"..."} ❌ MISSING_PROMPT
video {first_frame_path:"...", video_path:"..."} ❌ MISSING_PROMPT

三个都能补上,只是帮助文本没写。

image 和 video 都还要一个 prompt 字段,补上就通过;video 首帧加尾帧加 prompt 也通过。自动探测靠 img_path 认 image:只给 prompt 不给 img_path 就掉回 chatml 判,报 MISSING_MESSAGES,指不到真问题上。

dpo 的 chosen / rejected 必须是单个消息对象({"role":"assistant","content":"..."})。四种写法里字符串和消息数组都失败,消息对象通过;messages 末条是 assistant 也失败,报 DPO_LAST_MSG_NOT_USER。两个隐藏约束,help 都没写。

MESSAGE_NOT_OBJECT 会把你带沟里:字面指向 messages 数组,我去查 messages 每个元素都规规矩矩,真正坏的是 chosen 和 rejected。报错里的 path 才指真凶,得看 JSON 输出。

{
  "severity": "error",
  "code": "MISSING_MESSAGES",
  "message": "Required field \"messages\" is missing or not an array.",
  "line": 30,
  "path": "messages"
}

line 给行号,path 给字段。用 --output json 是这套流程里最便宜的习惯。

还有个能省事的办法:--schema 传进去就是逐行强制按那个形状判,不再自动探测。同一个文件拿六种 schema 各判一遍,六种错误码互不相同(MISSING_CHOSEN / MISSING_TEXT / MISSING_WAV_FN / MISSING_IMG_PATH / MISSING_FIELD),反过来能确认"我这文件被当成什么了"。

写脚本再记一条:失败时 stdout 有两个 JSON 对象(完整报告加一个 error 块),json.load 会报 Extra data,用 raw_decode() 只取第一个。

第二道闸的三种返回态:不在名单、能训、在名单但空数组

坑五:第二道闸有三种返回,最阴的那种不报错

数据集过只是第一道闸。第二道是"想训的基座让不让训、用什么方式训",命令 bl finetune capability,不用鉴权,--base-model 与 --training-type 二选一。

按训练类型查,今天(2026-10-12)的读数是 sft 13、sft-lora 16、dpo 2、dpo-lora 2、cpt 2。sft-lora 那 16 个里有图像模型(qwen-image-2.0、qwen-image-2.0-pro),也有视频模型(wan2.2-i2v-flash、wan2.2-kf2v-flash、wan2.5-i2v-preview、wan2.7-i2v),文本侧是 qwen3-4b-instruct-2507、qwen3.5-27b、qwen3.6-27b、qwen3.8-27b 和三个 qwen3-vl 规格。

这张表会变。10 月 10 号我查同一档是 22 个,今天 16 个,两天少了 6 个。原因我没查明,也不猜。你查的时候以自己当天的读数为准,别抄我这两个数。

按模型查的时候撞上另一个坑:它有三种返回形态,帮助文本没区分。

① 不在名单里
   bl finetune capability --base-model happyhorse-1.1

(先说清楚:这一条不是命令坏了。capability 查的是「可作微调基座」名单,和模型目录是两套名单,坑五里细说。)
   → {"error": "No foundation model found matching \"happyhorse-1.1\"."}

② 在名单里,能训
   bl finetune capability --base-model qwen-image-2.0
   → supported: ["sft-lora"], supports.sft: true

③ 在名单里,但什么都不能训
   bl finetune capability --base-model qwen3.7-max
   → supported: [], supports.sft: false, supports.fineTune: null

第三种最阴:命令正常返回、无 error、退出码正常,只是 supported 是空数组。只判断"命令有没有报错"会读成"能训";判断得落在 supported 非空上。

第①种我一开始也读错了,以为是命令坏了,其实是口径差:capability 查的是「可作微调基座」名单(help 说它调 listFoundationModels),bl model list 给的是模型目录,两套名单。happyhorse-1.1 在目录里在列(搜 happyhorse 出 7 个模型),但不是可训基座。在目录里 ≠ 能当基座。

顺带一条:help 自己的示例跑不通。Examples 里写 --base-model qwen3-8b,我照抄得到 not found;交叉查 bl model search --keyword qwen3-8b 返回 total 0,125 个家族里也没有它。新人照 help 敲的第一条命令就会失败。

最省事的那条路,恰好不给你全量校验

回到开头那件事。我为什么会漏掉第 2,500 行?因为我走的不是"先校验再上传",而是最省事的那条:

bl finetune text create --base-model <模型> --datasets ./my.jsonl

--datasets 直接吃本地路径:先校验并上传再拿 file-id 建任务,一步到位。好用,我平时也这么干。

问题是它没有任何校验强度开关:--help 从头翻到尾没有 --full-validate 也没有 --no-validate,而 bl dataset upload 两个都有。所以一步上传并训练必然走默认抽样,五千行就是 3.18% 覆盖率。便捷和全量在这条命令上互斥。

想要全量只能拆开走三步:

bl dataset validate --file my.jsonl --full-validate --schema chatml
bl dataset upload --file my.jsonl --full-validate
bl finetune text create --base-model <模型> --datasets <上一步返回的 file-id>

多两步,换来的是每一行都被解析过。我现在的判断标准是:数据集小于一千行、或者是脚本自动生成的,走一步路径没问题;只要是人手整理过的、或者超过一千行的,一定拆开走。人手整理的数据集坏行落在中段是大概率事件,而中段正是抽样最容易漏 schema 坏行的地方。

还有个安全动作:bl finetune text create 支持 --dry-run。我拿它预览过一次,输出里会列出 pending_uploads(哪些本地文件会被上传)和映射后的 training_type(比如 sft-lora 在接口边界会变成服务端的 efficient_sft),不上传、不建任务、不耗训练额度。正式建任务之前先看一眼,比事后收拾便宜。

三条路径的取舍:一步上传、拆开三步、dry-run 预览

记下来的六个习惯

写完回头一看全是些很小的事,但每一件都是踩过才知道的:

  1. 数据集一律加 --full-validate,六毫秒的事
  2. 明确传 --schema,不靠自动探测。我知道我这文件是拿来干什么的,就让校验也知道
  3. 超过一千行、或者人手整理过的,不走 finetune text create --datasets <本地路径> 那条便捷路,拆成 validate → upload → create 三步
  4. 训练前先 bl finetune capability --base-model <模型>,判 supported 数组非空,不判命令成功
  5. 建任务前 --dry-run 看一眼 pending_uploads 和映射后的 training_type
  6. 脚本里用 --output json + raw_decode,看 line 和 path

退出码这块这次没让我失望:校验不通过是 exit 1,通过是 0。(同一个 CLI 里 bl model list --model <某个ID> 会对明明在列表里的家族回答 not found 且退出码 0,那是另一件事,但正因为踩过那个,我对"退出码诚实"格外敏感。)

这六条加起来,我那个五千行的文件从"传上去炸了"变成"上传前 21 毫秒就告诉我第 2,500 行缺字段"。

想自己复现

这三条命令都不用鉴权,装完 CLI 就能试(CLI 安装页在这):

bl dataset validate --file my.jsonl --output json
bl dataset validate --file my.jsonl --schema dpo --full-validate
bl finetune capability --training-type sft-lora

重跑三件事:自己的数据集跑一次 --full-validate 看 sampledRecords 与 totalRecords 差多少;打算训的基座跑一次 capability --base-model 判 supported 非空;建任务前 --dry-run 看 pending_uploads。

要往下走的话,API Key 在密钥管理页创建,上面那三条连 Key 都不用。想先看看平台,百炼首页在这。

数据集这道闸只是链路的开头。往后训练怎么起、checkpoint 怎么挑、导出之后怎么部署成服务,是另外几件事,这篇不铺开。

posted @ 2026-10-12 15:47  新的白白酱  阅读(10)  评论(0)    收藏  举报