六毫秒换全量校验,这个参数我以前一直不敢加
最近在整一个五千行的对话数据集准备做微调。传之前我规规矩矩跑了一遍 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 行上又恰好命中;中段落点公式我没查明,只记位置对照,不推公式。
结论:默认模式是快速看一眼用的,不是保证能训用的;靠猜位置判断坏行在不在窗内,我试过了,不行。

坑二:那个 (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),不上传、不建任务、不耗训练额度。正式建任务之前先看一眼,比事后收拾便宜。

记下来的六个习惯
写完回头一看全是些很小的事,但每一件都是踩过才知道的:
- 数据集一律加
--full-validate,六毫秒的事 - 明确传
--schema,不靠自动探测。我知道我这文件是拿来干什么的,就让校验也知道 - 超过一千行、或者人手整理过的,不走
finetune text create --datasets <本地路径>那条便捷路,拆成 validate → upload → create 三步 - 训练前先
bl finetune capability --base-model <模型>,判supported数组非空,不判命令成功 - 建任务前
--dry-run看一眼pending_uploads和映射后的training_type - 脚本里用
--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 怎么挑、导出之后怎么部署成服务,是另外几件事,这篇不铺开。

浙公网安备 33010602011771号