ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

使用 slime 对 Qwen3-4B-Base 进行 SFT 微调:基于 OpenHermes-2.5 的完整实战指南

2026/9/16 13:51:54 拓冰建站 浏览量
使用 slime 对 Qwen3-4B-Base 进行 SFT 微调:基于 OpenHermes-2.5 的完整实战指南 使用 slime 对 Qwen3-4B-Base 进行 SFT 微调基于 OpenHermes-2.5 的完整实战指南【免费下载链接】slimeslime is an LLM post-training framework for RL Scaling.项目地址: https://gitcode.com/GitHub_Trending/slime12/slime本指南面向希望在 slime 框架中完成纯 SFTSupervised Fine-Tuning训练的开发者。它以经典的 OpenHermes-2.5 指令数据集为例逐步讲解如何在 slime 中准备数据、把 RLGRPO训练脚本改造成 SFT 训练脚本并深入拆解sft_loss、per-token loss、custom rollout 等关键机制在源码中的真实实现。读完本文你将能够用自己的对话数据集在 slime 上跑通一条完整、可复现的 SFT 训练链路并理解 slime 数据管线与损失函数的底层原理。一、前置准备环境、模型转换与脚本入口SFT 训练建立在 slime 的 RL 训练环境之上因此第一步需要参考 示例Qwen3-4B 模型 完成两件事创建镜像环境与将Qwen3-4B-Base的 HuggingFace 权重转换为 Megatron 可加载的 checkpoint。拉取镜像并安装 slimecd /root/ git clone https://github.com/THUDM/slime.git cd slime/ pip install -e . --no-deps下载Qwen/Qwen3-4B-Base权重hf download Qwen/Qwen3-4B-Base --local-dir /root/Qwen3-4B-Base使用仓库自带的转换脚本 tools/convert_hf_to_torch_dist.py配合模型配置脚本 scripts/models/qwen3-4B.sh 转换为 Megatron 格式cd /root/slime source scripts/models/qwen3-4B.sh PYTHONPATH/root/Megatron-LM python tools/convert_hf_to_torch_dist.py \ ${MODEL_ARGS[]} \ --hf-checkpoint /root/Qwen3-4B-Base \ --save /root/Qwen3-4B-Base_torch_dist⚠️ 使用 base 模型而非 instruct 模型时请务必核对scripts/models/qwen3-4B.sh中的--rotary-base等模型配置与实际权重一致。Megatron 训练时无法从 checkpoint 中读取模型结构需要全部自行配置若有出入可以在导入后覆盖例如MODEL_ARGS ( --rotary-base 10000 )。转换完成后后续 SFT 脚本会用到三个 checkpoint 路径详见 run-qwen3-4B-base-sft.sh 中的CKPT_ARGS参数示例值含义--hf-checkpoint/root/Qwen3-4B-Base/HuggingFace 权重目录slime 从这里读取 tokenizer以及 sglang 所需权重--ref-load/root/Qwen3-4B-Base_torch_dist转换后的 Megatron 权重目录作为初始模型来源--load/--save/root/Qwen3-4B-Base_slime/训练时 actor 的加载/保存目录若为空会从ref-load读取--save-interval1000每训练多少步保存一次 checkpoint二、SFT 数据准备把 OpenHermes-2.5 转成 slime 可加载的格式slime 的数据输入支持JSONL 与 Parquet两种格式Parquet 需要安装pyarrow见 arguments.py 中--prompt-data的说明。这里以经典指令微调数据集 OpenHermes-2.5 为例将其处理成适合 slime 加载的格式新增一列符合 openai message 格式的messages字段并保存为/root/openhermes2_5.parquet。from datasets import load_dataset ds load_dataset(teknium/OpenHermes-2.5)[train] def convert(sample): conversations sample[conversations] def convert_role(role): if role human: return user elif role gpt: return assistant elif role system: return system else: raise ValueError(fUnknown role: {role}) messages [ { role: convert_role(turn[from]), content: turn[value], } for turn in conversations ] return {messages: messages} ds ds.map(convert) ds.to_parquet(/root/openhermes2_5.parquet)关键点说明字段名对齐转换后每条样本的messages列表即为 OpenAI message 结构[{role: user, content: ...}, {role: assistant, content: ...}]对应 SFT 脚本中的--input-key messages自定义模板如果希望训练时套用模型的 chat template可以在脚本中开启--apply-chat-template当前 SFT 示例脚本中该选项被注释因为数据本身已是完整的多轮对话结构工具调用支持消息中的tools信息会被透传到后续的 loss mask 生成逻辑中用于处理带 function calling 的数据。三、执行训练在容器内切换到仓库目录并直接运行 SFT 脚本即可cd /root/slime bash scripts/run-qwen3-4B-base-sft.sh脚本会依次完成清理残留进程 → 启动 Ray head 节点 → 通过ray job submit提交训练任务使用 train_async.py。默认配置为 1 机 8 卡训练 actor 使用全部 8 张 GPU。四、从 RL 脚本到 SFT 脚本三处关键调整逐一拆解将 run-qwen3-4B-base-sft.sh 与 RL 版本的 run-qwen3-4B.sh 对比可以发现除了把 instruct 模型换成 base 模型之外主要做了以下三处调整理解它们就等于理解了 slime 的 SFT 设计哲学。4.1 移除SGLANG_ARGS与GRPO_ARGSRL 脚本中的SGLANG_ARGS如--rollout-num-gpus-per-engine 2、--sglang-mem-fraction-static 0.7负责启动 sglang 推理引擎做在线采样GRPO_ARGS如--advantage-estimator grpo、--use-kl-loss负责策略优化。SFT 过程不需要在线推理生成回复也不需要计算优势函数与 KL因此这两组参数被整体移除。4.2ROLLOUT_ARGS改名为SFT_ARGS替换数据来源与损失配置SFT 版本把 RL 的ROLLOUT_ARGS重命名为SFT_ARGS并配置为SFT_ARGS( --rollout-function-path slime.rollout.sft_rollout.generate_rollout --prompt-data /root/openhermes2_5.parquet --input-key messages # --apply-chat-template --rollout-shuffle --num-epoch 3 --rollout-batch-size 128 --global-batch-size 128 --loss-type sft_loss --calculate-per-token-loss --disable-compute-advantages-and-returns --debug-train-only )逐个参数解读如下数据来源复用 custom rollout 机制--rollout-function-path slime.rollout.sft_rollout.generate_rollout这是 slime 中最核心的设计。slime 的 SFT 本质上是复用了 custom rollout 功能默认的 rollout 函数是 slime/rollout/sglang_rollout.py 中的generate_rollout调用 sglang 在线生成而这里切换成 slime/rollout/sft_rollout.py 中从文件读取数据的 SFT 版本。该参数接受任何符合def generate_rollout(args, rollout_id, data_source, evaluationFalse)签名的函数因此你完全可以参考它实现自己的 custom rollout。--prompt-data /root/openhermes2_5.parquet训练数据路径支持 JSONL 与 Parquet--input-key messages指定每条样本中作为对话内容的字段名--rollout-shuffle每个 epoch 开始前对数据集做 shuffle在 data_source.py 中按epoch_id调用dataset.shuffle()。训练节奏读一个 batch 就训一个 batch--num-epoch 3训练轮数。slime 内部会据此计算总 rollout 次数num_rollout num_epoch * dataset_size // rollout_batch_size见 arguments.py 中--num-epoch的说明若同时设置--num-rollout则--num-epoch被忽略--rollout-batch-size 128与--global-batch-size 128SFT 场景下建议两者相等并且不要配置n_samples_per_prompt。这样每条 prompt 只取 1 份样本一个 rollout 读取的样本数恰好等于一个全局训练 batch实现读一个 batch 就训一个 batch避免 RL 中一个 prompt 采样多个回复的分组逻辑。损失函数与 loss 归约方式--loss-type sft_loss指定使用 SFT 损失。slime 当前支持policy_loss、sft_loss、custom_loss三种custom_loss通过--custom-loss-function-path加载自定义损失--calculate-per-token-loss这是 SFT 与 RL 损失归约方式的分水岭。slime 默认按GRPO 的 per-sample mean计算损失而一般 SFT 训练要求按一个 batch 内所有未被 mask 的 token 取平均因此 SFT 建议开启该选项。关于二者的底层差异见本文第六节。省去 RL 特有的计算与组件--disable-compute-advantages-and-returnsSFT 不需要预先计算 log prob / 优势 / 回报开启后跳过这部分计算--debug-train-only告诉 slime 只需要训练、不需要初始化 sglang 推理引擎在 deployment.py 中debug_train_only为真时会跳过 sglang 部署SFT 场景下自然也不需要。4.3 使用train_async.py代替train.pyRL 脚本通过python3 train.py启动而 SFT 脚本改为python3 train_async.py。两者唯一的差别是train loop 的同步逻辑异步版本利用 Ray 的.remote/ray.get机制在进行当前 rollout 的训练时同时启动下一个 rollout 的数据读取prefetch。对 SFT 这种数据完全来自本地的场景可以很好地隐藏数据加载延迟提升 GPU 利用率。slime 仓库同时保留了 train.py 与 train_async.py 两个入口方便在不同场景下切换。五、源码级拆解SFT 数据管线与 loss mask 是如何生成的理解了参数再深入 slime/rollout/sft_rollout.py 看数据管线在代码中的真实实现。# slime/rollout/sft_rollout.py核心逻辑摘录 TOKENIZER None PROCESSOR None MASK_GENERATOR None def generate_rollout(args, rollout_id, data_buffer, evaluationFalse): assert not evaluation assert args.rollout_global_dataset # 懒加载tokenizer / processor / loss mask 生成器只初始化一次 global TOKENIZER, PROCESSOR, MASK_GENERATOR, SAMPLE_PRINTED if TOKENIZER is None: TOKENIZER load_tokenizer(args.hf_checkpoint, trust_remote_codeTrue) if PROCESSOR is None: PROCESSOR load_processor(args.hf_checkpoint, trust_remote_codeTrue) if MASK_GENERATOR is None: MASK_GENERATOR MultiTurnLossMaskGenerator(TOKENIZER, tokenizer_typeargs.loss_mask_type) samples data_buffer.get_samples(args.rollout_batch_size) for i, sample in enumerate(samples): (sample,) sample # n_samples_per_prompt 未配置每组恰好 1 条 messages sample.prompt tools sample.metadata.get(tools, None) token_ids, loss_mask MASK_GENERATOR.get_loss_mask(messages, toolstools) if len(token_ids) ! len(loss_mask): raise ValueError(...) response_length MASK_GENERATOR.get_response_lengths([loss_mask])[0] sample.tokens token_ids sample.response_length response_length sample.reward 0 # SFT 没有 reward sample.loss_mask loss_mask[-response_length:] # 只保留 response 段的 mask return samples从中可以提炼出 SFT 数据管线的四个要点数据源data_buffer.get_samples(args.rollout_batch_size)从全局数据集中顺序取出一个 batch 的样本。全局数据集的构造在 data_source.py 中会读取--input-key指定的字段、按--rollout-seed初始化随机数、并在每个 epoch 开头执行shuffle每条样本只取一份(sample,) sample这一行解包说明每组 group 恰好包含 1 个样本这正是不配置n_samples_per_prompt的直接结果loss mask 只覆盖 assistant 回复段MultiTurnLossMaskGenerator实现于 mask_utils.py会逐轮处理多轮对话对assistant角色的轮次生成[0]*gen_token_length [1]*(len(message_ids)-gen_token_length)的 mask其余轮次全部置 0从而确保只有模型回复部分的 token 参与 loss 计算response_length定义为从第一个 mask1 到序列结尾的长度reward 恒为 0SFT 阶段所有样本的reward被置为 0不参与任何优势计算。六、损失函数与 per-token loss 的底层原理6.1sft_loss_function对回复 token 求负对数似然在 slime/backends/megatron_utils/loss.py 中SFT 损失被实现为一个独立的损失函数def sft_loss_function(args, batch, logits, sum_of_sample_mean): # 取出模型对 ground-truth token 的 log prob只取 response 段 log_probs log_probs_and_entropy[log_probs] log_probs torch.cat(log_probs, dim0) loss -sum_of_sample_mean(log_probs) return loss, {loss: loss.clone().detach()}其语义是对 batch 中所有样本的response 段 token计算模型预测的 log prob取负后通过sum_of_sample_mean做归约即标准的 SFT 负对数似然NLL。6.2 per-sample mean 与 per-token mean 的区别sum_of_sample_mean的行为由--calculate-per-token-loss决定其实现位于 slime/backends/megatron_utils/cp_utils.pyreturn sum_of_sample_mean if not calculate_per_token_loss else sum_of_token不开启RL 默认先对每条样本内的 token 求平均再对 batch 内所有样本求平均即 per-sample mean这与 GRPO 的惯例一致开启SFT 推荐直接对所有未被 mask 的 token 求和后再除以总 token 数normalizer 为num_tokens在 loss.py 中由各条loss_masks求和得到即 per-token mean。这正是一般 SFT 训练都是按一个 batch 的所有不被 mask 的 token 取平均的源码依据。此外loss.py 中的调度逻辑loss_function会根据args.loss_type在policy_loss/value_loss/sft_loss/custom_loss之间分发对应 arguments.py 中--loss-type的choices[policy_loss, sft_loss, custom_loss]并对 loss 做 micro-batch 与并行规模的 rescale以适配 Megatron 的梯度累积。七、其余训练参数速览SFT 脚本中除了SFT_ARGS之外的其余参数组如下完整见 run-qwen3-4B-base-sft.shPERF_ARGS并行与重计算策略PERF_ARGS( --tensor-model-parallel-size 1 --sequence-parallel --pipeline-model-parallel-size 1 --context-parallel-size 1 --expert-model-parallel-size 1 --expert-tensor-parallel-size 1 --recompute-granularity full --recompute-method uniform --recompute-num-layers 1 # --micro-batch-size 1 --use-dynamic-batch-size --max-tokens-per-gpu 9216 )--use-dynamic-batch-size与--max-tokens-per-gpu是 slime 在 Megatron 参数之外新增的两个选项开启后每张卡会将 batch 内部长短不一的数据动态拼装到max_tokens_per_gpu个 token组成动态 micro-batch从而忽略传统的--micro-batch-size若单条数据超过该上限则自成一条不截断。slime 始终通过 data packing 训练并严格保证 per-sample / per-token loss 的精确性因此开启 dynamic batch size 不会影响 loss 计算推荐开启。RL 脚本中 TP 为 2SFT 脚本中为 1读者可根据模型规模调整。OPTIMIZER_ARGS优化器与学习率调度OPTIMIZER_ARGS( --optimizer adam --lr 1e-5 --lr-decay-style cosine --min-lr 1e-6 --lr-warmup-fraction 0.1 --weight-decay 0.1 --adam-beta1 0.9 --adam-beta2 0.95 )SFT 采用1e-5的学习率、cosine 衰减到1e-6、前 10% 步数 warmupRL 脚本则是1e-6的 constant 学习率可见 SFT 通常需要更高的初始学习率。MISC_ARGS精度与 dropoutMISC_ARGS( --attention-dropout 0.0 --hidden-dropout 0.0 --accumulate-allreduce-grads-in-fp32 --attention-softmax-in-fp32 # need to comment this when using model with MLA --attention-backend flash )其中 dropout 置 0 是覆盖 Megatron 默认的 0.1梯度累加与 softmax 均保持在 fp32使用 MLA 结构的模型如 Qwen3 部分变体、GLM 系列需要注释--attention-backend flash。WANDB_ARGS日志上报可选WANDB_ARGS( # --use-wandb # --wandb-project slime-dev # --wandb-group qwen3-4B-base-sft # --wandb-key ${WANDB_KEY} )默认全部注释按需开启即可接入 Weights Biases 训练曲线。八、常见注意事项数据集格式--prompt-data只接受 JSONL 与 Parquet后者需pyarrow每条记录的字段名必须与--input-key一致不要配置n_samples_per_promptSFT 中一旦配置该参数每组 group 会包含多个样本破坏读一个 batch 训一个 batch的语义同时rollout_batch_size应等于global_batch_sizeloss mask 自动生成只要数据是 OpenAI message 结构--input-key messagesmask_utils.py 会自动生成只对 assistant 回复段置 1 的 loss mask无需手工标注但请确保--loss-mask-type与你的模型 tokenizer 类型匹配如qwen、qwen3、qwen3_5等base 模型配置核对训练前务必核对 scripts/models/qwen3-4B.sh 中的--rotary-base、hidden size、层数等配置是否与Qwen3-4B-Base权重一致异步训练日志使用train_async.py时数据 prefetch 与训练并行进行若后续接入了 sglang推理日志与训练日志可能混排可通过--sglang-log-level调低推理日志级别。结语本文从环境准备、数据转换、脚本改造到源码剖析完整走通了在 slime 上用 OpenHermes-2.5 微调 Qwen3-4B-Base的 SFT 链路。核心要点可以总结为一句话slime 的 SFT 并不是一个独立的功能模块而是通过 custom rollout 机制--rollout-function-path、sft_loss损失类型与--calculate-per-token-loss归约方式对 RL 训练框架的优雅复用。理解这一点后无论是替换自定义数据集、接入多轮对话、还是实现你自己的 custom loss都可以在现有框架上平滑扩展。相关示例脚本与源码均可直接在仓库中查阅scripts/run-qwen3-4B-base-sft.sh、slime/rollout/sft_rollout.py、slime/backends/megatron_utils/loss.py。【免费下载链接】slimeslime is an LLM post-training framework for RL Scaling.项目地址: https://gitcode.com/GitHub_Trending/slime12/slime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考