与可复现吞吐量记录)
Soup 0.75.0 修复解析MLX 基准 Harness 的步数固定Step-Count Pinning与可复现吞吐量记录【免费下载链接】SoupFine-tune LLMs from one YAML. Layer streaming trains an 8B model on a 4 GB laptop GPU.项目地址: https://gitcode.com/GitHub_Trending/soup12/Soup导读本文解析 Soup 0.75.0 中随 changelog 片段 727.fixed.md 落地的修复issue #716benchmarks/harness/mlx_sft_smoke.py此前静默继承配置 schema 的默认值尤其是gradient_accumulation_steps导致公开的 M1 8 GB MLX SFT 吞吐量记录依赖一个 harness 从未声明过的数值。修复后harness 显式固定pin所有影响步数的字段、按与MLXSFTTrainerWrapper完全一致的算法从加载后的配置解析迭代数与优化器更新数并在两者不等于rows × epochs时于训练启动前以非零退出码中止。读完本文你将理解 MLX 训练路径中iters向下取整到整组梯度累积的机制#696、步数固定为何是基准可复现性的前提以及如何通过 test_issue716_harness_step_guard.py 验证这套守卫。背景为什么一个基准 Harness 需要“固定字段”Soup 的 MLX 后端通过 MLXSFTTrainerWrapper 包装 mlx-lm 的 LoRA 训练循环而 MLX 基准 harnessbenchmarks/harness/mlx_sft_smoke.py的使命是用真实 MLX 运行时端到端驱动该 wrapper为 run-m1-8gb-mlx-sft.md 中的公开记录提供可复现的数字。问题出在“可复现”的隐含要求上一条公开发布的基准记录其步数必须由 harness 自己明确声明而不是从配置 schema 的默认值中继承。在修复之前harness 的 YAML 只写了epochs、lr、batch_size等字段gradient_accumulation_steps依赖 schema 默认值。而 schema 的默认值是 4见 schema.py 中gradient_accumulation_steps: int Field(default4, ge1)这意味着记录中“48 个迭代”实际对应48 / 4 12次优化器更新——迭代数看似一致但优化器更新次数与记录中隐含的语义完全不同更糟的是 MLXSFTTrainerWrapper.train 会把iters向下取整到梯度累积步数的整数倍#696若行数不是 4 的倍数例如 50 行其中 2 行会被静默丢弃而 harness 打印出的仍是“48 次迭代”。一句话概括缺陷形态#716已发布的数字取决于一个 harness 从未提过的默认值。任何一次 schema 默认值的变更都会在无人察觉的情况下改变公开基准的含义。修复方案Pin 校验 提前中止修复后的 harness 在 mlx_sft_smoke.py 中做了三件事构成一条完整的“可复现性防线”固定pin所有影响步数的字段并在 YAML 注释中写明原因从加载后的配置解析步数——不是信任注释而是读取cfg.training.*的真实值调用resolve_step_counts复现 wrapper 的算法训练启动前校验当iters ! rows * epochs或updates ! rows * epochs时sys.exit(...)以非零退出码中止并在消息中给出两个计数与将被丢弃的行数。被固定的字段与固定原因字段Harness 中的固定值Schema 默认值未固定时的隐患固定原因batch_size1auto按显存探测每步一个样本保证iterations rows × epochs其他取值会让迭代数变成ceil(rows / batch_size) × epochsgradient_accumulation_steps14为 4 时 mlx-lm 每 4 个迭代才步进一次优化器48 次迭代只有 12 次更新且 #696 向下取整会静默丢弃余数行epochs命令行参数默认 13直接决定迭代总数lr1e-42e-5学习率决定 loss 曲线的绝对值也是记录列的一部分除上述四个“移动步数的字段”外harness 还额外固定了其余会让记录含义漂移的配置项scheduler: constant、warmup_ratio: 0.0、weight_decay: 0.01schema 默认分别为cosine、0.03、0.01见 schema.py、schema.py以及train_on_responses_only: false——因为一旦 MLX 支持响应掩码Trained Tokens计数与tok/s的含义会一起改变实测同一 Qwen2.5-0.5B 行从 2,130 tokens / 108.1 tok/s 变为 342 tokens / 26.1 tok/s。这些字段虽不直接改变迭代数但会改变记录中“loss 曲线”“trained tokens”“tok/s”等列的可复现性。校验逻辑的核心与 wrapper 完全一致的解析harness 的resolve_step_countsmlx_sft_smoke.py精确镜像了MLXSFTTrainerWrapper.train中的步数解析iters int(epochs * max(1, math.ceil(rows_n / batch_size))) if grad_accumulation_steps 1: iters max( grad_accumulation_steps, iters - (iters % grad_accumulation_steps), ) return iters, iters // max(1, grad_accumulation_steps)对照 wrapper 源码mlx_sft.py可以看到完全相同的两步先epochs * ceil(rows / batch_size)再在grad_accumulation_steps 1时向下取整到整组且至少保留一组避免小数据集训练 0 个优化器步。这正是 #696 的取舍mlx-lm 只在迭代数对累积步数取模为 0 时更新优化器从不刷新不完整的组所以 wrapper 宁可丢弃余数行也要保证迭代数对齐。关键点在于 harness 校验的是从加载后的配置读取的值cfg.training.batch_size、cfg.training.gradient_accumulation_steps而非信任 YAML 注释。如果某次后续编辑删除了某个 pin、或 schema 默认值发生了移动校验会立刻以非零退出码暴露问题而不是静默地发布一张在“另一套配置”下测得的数据表。中止路径与错误消息当校验失败时harness 调用sys.exit(str)以退出码 1 中止Python 中sys.exit传入字符串会把消息输出到 stderr 并以 1 退出错误消息同时给出期望值、解析值与丢弃行数例如harness run is not 50 iterations / 50 optimizer updates: batch_size1, gradient_accumulation_steps4 resolve 50 rows x 1 epoch(s) to 48 iterations and 12 optimizer updates, dropping 2 row(s). Pin them in the YAML above; a published figure must not depend on a schema default.消息末尾的 “a published figure must not depend on a schema default” 是这次修复的原则宣言公开发布的基准数字不允许依赖 schema 默认值。测试验证守卫必须真实触发配套测试 test_issue716_harness_step_guard.py 从两个方向锁定修复1. 解析算法与真实 wrapper 的算术一致。测试用例表test_issue716_harness_step_guard.py覆盖了各种组合(rows, epochs, batch_size, accum)(iterations, optimizer updates)说明(48, 1, 1, 1)(48, 48)最简情形(48, 1, 1, 4)(48, 12)48 迭代被压缩为 12 次更新(50, 1, 1, 4)(48, 12)向下取整静默丢弃 2 行(10, 1, 1, 4)(8, 2)不足一组也至少保留一组(10, 1, 4, 1)(3, 3)向上取整而非向下取整(50, 2, 1, 4)(100, 25)多 epoch 情形其中test_resolve_step_counts_matches_what_the_wrapper_hands_mlx_lmtest_issue716_harness_step_guard.py用 fake MLX 驱动真实 wrapper 训练再断言 wrapper 交给 mlx-lm 的TrainingArgs.iters与iters // grad_accumulation_steps和 harness 的resolve_step_counts返回一致——确保中止消息里的数字是 wrapper 真正使用的数字。2. 中止路径必须通过真实的main()触发。测试用 monkeypatch 改掉 harness YAML 中的 pin例如删除gradient_accumulation_steps: 1这一行、或把batch_size: 1改成batch_size: 4然后调用harness.main()断言SystemExit删除累积步数 pin 后、48 行消息包含to 48 iterations and 12 optimizer updates, dropping 0 row(s)与gradient_accumulation_steps4test_issue716_harness_step_guard.py同一删除、50 行消息包含dropping 2 row(s)test_issue716_harness_step_guard.py修改 batch_size 后、10 行消息包含batch_size4与to 3 iterations and 3 optimizer updatestest_issue716_harness_step_guard.py未修改的出厂 pin 则通过校验main()返回 0并打印steps : 10 iterations / 10 optimizer updatestest_issue716_harness_step_guard.py。第二个方向的意义在于即使有人只删守卫不删 pin测试也会失败——守卫本身无法被静默拆除。而assert state.displays []保证中止发生在训练以及 Rich 显示启动之前。实测与发布记录的不变量run-m1-8gb-mlx-sft.md 是这条修复所保护的那份公开记录8 GB M1 上四行模型Qwen2.5-0.5B / Llama-3.2-3B / Qwen2.5-7B / Llama-3.1-8B均 4-bit、48 行、1 epoch其中llama3.1-8b-sft-mlx是出厂配方实测峰值 5.154 GB、48 迭代 71 秒、adapter 可写可重载。修复的“零变化”承诺很关键没有任何已发布数字发生变化——Srinivasan8888 在记录测量所用的 M1 8 GB 上重跑修复后的分支精确复现了Trained Tokens 2130。这与字段固定的目标一致修复不是改基准而是让基准不再依赖隐性默认值。这份记录还给出了两个与步数语义直接相关的先例正是 #716 想避免的教训tok/s 必须是全程平均而非瞬时值。mlx-lm 打印的Tokens/sec是瞬时值单次 0.5B 运行内从 19.192 到 254.313 波动记录表中列出的108.1 tok/s是 harness 自己计算的trained tokens / train seconds全程平均值token 数一致性可作为内部校验。第 1、3 行共享 Qwen2 tokenizer 与相同输入trained tokens 必须相等2,130 2,130第 2、4 行共享 Llama32,316 2,316。如何复现与验证在 Apple Silicon 机器上MLX 后端要求pip install -e .[mlx] python benchmarks/harness/mlx_sft_smoke.py mlx-community/Qwen2.5-0.5B-Instruct-4bit 48 1harness 在计时开始前会断言resolve_trainer(cfg)返回的是MLXSFTTrainerWrapper防 #363 的“backend: mlx从未到达 MLX trainer”问题打印steps行展示解析出的迭代数与优化器更新数并在末尾输出全程平均吞吐量steps : 48 iterations / 48 optimizer updates (batch_size1, grad_accum1) throughput : 108.1 tok/s (2130 trained tokens / 19.7s, whole-run average)需要说明的限制记录文档也明确承认这份记录只证明“能训练”不证明“训练得正确”——数据是 48 行刻意重复的合成问答loss 从 3.6 降到 0.1 是记忆而非泛化target_modules: auto在 MLX 上解析为 Q/V 两层mlx_sft.py 中MLX_DEFAULT_TARGET_KEYSloss 曲线不可与 CUDA 路径直接比较单机单次运行无重复重跑同一配置在暖缓存与安静机器上可差 3.5 倍373.1 vs 108.1 tok/s因此表中的 tok/s 只能视为数量级而非基准。每条记录都应随这些前提一起引用——这正是步数固定要保证的“记录自洽”的一部分。小结#727 修复把 MLX 基准 harness 从“默认值依赖”的隐性状态拉回“显式声明”状态固定字段pin、复现 wrapper 算法resolve、训练前中止guard三层防线共同保证了公开记录的可复现性。其方法论——基准数字不得依赖 schema 默认值、步数必须由 harness 自身解析与校验、守卫必须经真实main()路径测试——同样适用于任何以发布测量结果为目的的基准代码而不只是 Soup 的 MLX 后端。【免费下载链接】SoupFine-tune LLMs from one YAML. Layer streaming trains an 8B model on a 4 GB laptop GPU.项目地址: https://gitcode.com/GitHub_Trending/soup12/Soup创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考