用 GRPO 在 100 步内微调 350M 模型,提升结构化输出能力
请注意,本指南所述的训练流程并非 IFStruct 博客 中介绍的 RL 模型所使用的训练 pipeline。本 notebook 的目标并非复现 IFStruct 基准的分数,而是展示针对小模型进行任务特定微调如何提升性能,并使其表现接近参数量大得多的模型。
前置条件
本指南分为两部分,分别在以下环境运行:- 微调(Fine-tuning) 需在 GPU 上执行。配套的 notebook 已针对免费版的 Colab 或 Kaggle GPU 优化配置。
- 评估(Evaluation) 可在 MacBook 本地运行(本文使用配备 Apple M5 Max 芯片和 36GB 统一内存的 MacBook Pro),通过
llama.cpp提供 OpenAI 兼容的推理服务,供 IFStruct 评估工具调用。
uv 管理 Python 环境,并使用 llama.cpp 提供推理服务。按照 Liquid AI llama.cpp 部署文档 的指引,通过 Homebrew 安装 llama.cpp,并验证 llama-server 可用:
```
brew install llama.cpp
llama-server --version
```
## IFStruct 评测 LFM2.5-350M(基础模型)
开始前,先在 IFStruct 基准上评测 LFM2.5-350M,看看能否复现其报告的 21.1% 得分。
**IFStruct** 是一个用于测试大语言模型输出有效性和模式遵循能力的基准。该基准开源在 [Liquid4All/ifstruct](https://github.com/Liquid4All/ifstruct),公共数据集可在 Hugging Face 的 [LiquidAI/ifstruct-v1.0](https://huggingface.co/datasets/LiquidAI/ifstruct-v1.0) 获取。
```
git clone https://github.com/Liquid4All/ifstruct.git
```
为便于对比评测,我们使用 `llama.cpp` 在 MacBook 上本地部署模型,选用的是 `BF16` 格式的 GGUF 文件([LiquidAI/LFM2.5-350M-GGUF](https://huggingface.co/LiquidAI/LFM2.5-350M-GGUF))。
用以下命令启动基础模型服务:
```
llama-server \
-hf LiquidAI/LFM2.5-350M-GGUF:BF16 \
-c 32768 \
-np 4 \
-ngl 99 \
--alias LiquidAI/LFM2.5-350M \
--host 127.0.0.1 \
--port 8080
```
- `--alias`:IFStruct 发送给 OpenAI 兼容端点的模型名称
- `-ngl 99`:若有 GPU,将全部层卸载到 GPU
- `-np 4`:并行服务 4 个请求
- `-c 32768`:上下文长度
服务启动后,用 2000 条样本运行完整基准测试:
```
uv run ifstruct-eval \
--model LiquidAI/LFM2.5-350M \
--base-url http://localhost:8080/v1 \
--api-key dummy \
--dataset data/test.jsonl \
--results-file results/lfm2.5-350m-llamacpp-base.json \
--n-threads 4 \
--max-tokens 2048 \
-v
```
============================================================
模型:LiquidAI/LFM2.5-350M
============================================================
总计:452/2000 通过 (22.6%)
平均延迟:1453ms
按格式:
JSON:180/1000 通过 (18.0%)
YAML:272/1000 通过 (27.2%)
按顶层结构:
Wrapper key 288/1011 通过 (28.5%)
Bare list 164/989 通过 (16.6%)
按实体类型:
test__camera_review 6/83 通过 (7.2%)
test__clinical_trial 20/104 通过 (19.2%)
test__conference_schedule 7/87 通过 (8.0%)
test__escaping__bug_report_batch 24/89 通过 (27.0%)
test__escaping__config_snippet_audit 15/85 通过 (17.6%)
test__escaping__customer_email_thread 5/73 通过 (6.8%)
test__escaping__dialogue_sample 14/95 通过 (14.7%)
test__escaping__interview_transcript_segment 21/80 通过 (26.2%)
test__escaping__log_parser_examples 21/72 通过 (29.2%)
test__escaping__pr_discussion 22/87 通过 (25.3%)
test__escaping__repro_steps_batch 16/73 通过 (21.9%)
test__escaping__screenplay_scene 16/92 通过 (17.4%)
test__escaping__short_story_chapter 15/84 通过 (17.9%)
test__escaping__support_ticket_batch 27/73 通过 (37.0%)
test__escaping__terminal_session_notes 20/70 通过 (28.6%)
test__event_ticket_booking 49/107 通过 (45.8%)
test__gpu_review 6/94 通过 (6.4%)
test__invoice 28/86 通过 (32.6%)
test__job_posting 25/85 通过 (29.4%)
test__real_estate_listing 31/82 通过 (37.8%)
test__recipe 3/70 通过 (4.3%)
test__rental_car_booking 27/79 通过 (34.2%)
test__scientific_experiment 13/69 通过 (18.8%)
test__travel_itinerary 21/81 通过 (25.9%)
常见错误:
7228次 缺失必填字段
738次 项目数量错误
540次 类型不匹配
317次 未闭合的代码块
190次 多余字段 'notes'
181次 多余字段 'path'
175次 多余字段 'constraints'
170次 多余字段 'type'
170次 缺少代码块
100次 期望 Bare list,实际返回 Wrapper
IFStruct 官方博文指出 LFM2.5-350M 的通过率为 21.1%。我们在本地 llama.cpp/BF16 环境下的测试结果为 22.6%,与博文数据基本吻合。后续将以该本地结果作为同推理服务栈对比的基准线。
使用 TRL 对结构化输出进行 GRPO 微调
完整的可运行流水线见配套笔记本,本节仅介绍相关部分。
训练数据
我们使用 nvidia/Nemotron-RL-instruction_following-structured_outputs,该数据集将每条提示词与目标 JSON Schema 和预期字段数配对。我们选取约 500 条样本进行训练。
由于 Nemotron 的数据分布与 IFStruct 评估存在差异,我们对提示词做了增强以弥合两者间的两处 gap:
- 40% 的样本追加了"将输出放在围栏代码块中"的指令,让模型学会遵循格式指令,而不是总是直接输出裸 JSON。
- 另外 20%(与前一组不重叠)被转换为顶层数组任务(schema 被包裹在带有必填 item count 的
array中),从而训练裸列表输出和 item 数量合规性。
模型与 LoRA
我们加载 LiquidAI/LFM2.5-350M 并附加 LoRA 适配器。由于 LFM2.5 采用了混合注意力/卷积架构,我们针对 LFM 特有的模块名进行配置:
lora_config = LoraConfig(
r=16,
lora_alpha=32,
bias="none",
task_type="CAUSAL_LM",
target_modules=[
"q_proj", "k_proj", "v_proj", "out_proj", "in_proj",
"w1", "w2", "w3",
],
)
这仅训练约 600 万参数,占模型总量的 1.66%。
奖励函数
接着我们定义了三个奖励函数,取值范围均为 [0, 1],用于衡量每个 completion 中 结构 是否正确:
json_format_reward:输出是否可解析,且形式是否符合要求?完全符合(围栏格式或裸格式)得1.0,形式错误但可解析得0.2,无法解析得0.0。field_count_reward:对象的顶层字段数是否符合预期?完全匹配得1.0,分数随偏差线性衰减。
schema_validation_reward:输出是否符合该行的 JSON Schema?它会统计每个约束违规,并根据必填键的覆盖情况给予部分奖励。我们将三个指标加权求和,权重为 reward_weights=[1.0, 0.5, 2.0]。
训练
针对免费级16GB GPU的显存配置,我们以100步完成训练,每组提示词采样8个生成结果:
from trl import GRPOConfig
training_args = GRPOConfig(
output_dir="./outputs/lfm25-350m-nemotron-schema-grpo",
learning_rate=5e-5,
max_steps=100,
warmup_steps=10,
num_generations=8, # 每组提示词的采样数
per_device_train_batch_size=4,
gradient_accumulation_steps=8, # 每步优化器处理4组提示词
steps_per_generation=2,
max_completion_length=1024, # 预留嵌套JSON的空间
mask_truncated_completions=False,
temperature=1.1, # 较高温度保持组内多样性
beta=0.01, # 对参考模型的KL惩罚
reward_weights=[1.0, 0.5, 2.0], # json_format、field_count、schema_validation
logging_steps=1,
save_steps=100,
)
从 Notebook 运行结果可见,整个训练过程中三项奖励指标均稳步上升,KL散度在热身结束后脱离零值,而截断完成的占比始终接近于零。
合并与保存模型
最后,我们将 LoRA 适配器合并回基础权重,保存为单个独立检查点,可直接转换为 GGUF 格式用于服务:
MERGED_DIR = f"{training_args.output_dir}-merged"
merged_model = trainer.model.merge_and_unload()
merged_model.save_pretrained(MERGED_DIR)
tokenizer.save_pretrained(MERGED_DIR)
IFStruct 评估(GRPO 微调后的 LFM2.5-350M)
GRPO 微调完成后,我们重新运行 IFStruct 评估。为此需要将合并后的模型检查点转换为 BF16 格式的 GGUF。转换器脚本随 llama.cpp 源码一起提供,因此我们克隆一次仓库并安装其 gguf 包。
git clone --depth 1 https://github.com/ggml-org/llama.cpp
pip install ./llama.cpp/gguf-py
mkdir -p models
python llama.cpp/convert_hf_to_gguf.py \
PATH_TO_YOUR_MERGED_MODEL \
--outfile ./models/lfm25-350m-grpo-bf16.gguf \
--outtype bf16
然后使用以下命令部署合并后的模型:
llama-server \
-m ./models/lfm25-350m-grpo-bf16.gguf \
--alias lfm25-350m-grpo-structured-output \
-c 32768 \
-np 4 \
-ngl 99 \
--host 127.0.0.1 \
--port 8081
接下来,使用微调后的模型重新运行完整的 IFStruct 评估:
uv run ifstruct-eval \
--model lfm25-350m-grpo-structured-output \
--base-url http://localhost:8081/v1 \
--api-key dummy \
--dataset data/test.jsonl \
--results-file results/lfm25-350m-grpo.json \
--n-threads 4 \
--max-tokens 2048 \
-v
============================================================
模型:lfm25-350m-grpo-structured-output
============================================================
总体:2000 个通过 594 个(29.7%)
平均延迟:1518ms
按格式分类:
JSON:1000 个通过 319 个(31.9%)
YAML:1000 个通过 275 个(27.5%)
按顶层结构分类:
Wrapper key 1011 个通过 300 个(29.7%)
裸列表 989 个通过 294 个(29.7%)
按实体类型分类:
test__camera_review 83 个通过 5 个(6.0%)
test__clinical_trial 104 个通过 31 个(29.8%)
test__conference_schedule 87 个通过 11 个(12.6%)
test__escaping__bug_report_batch 89 个通过 32 个(36.0%)
test__escaping__config_snippet_audit 85 个通过 24 个(28.2%)
test__escaping__customer_email_thread 73 个通过 9 个(12.3%)
test__escaping__dialogue_sample 95 个通过 17 个(17.9%)
test__escaping__interview_transcript_segment 80 个通过 13 个(16.2%)
test__escaping__log_parser_examples 72 个通过 33 个(45.8%)
test__escaping__pr_discussion 87 个通过 26 个(29.9%)
test__escaping__repro_steps_batch 73 个通过 23 个(31.5%)
test__escaping__screenplay_scene 92 个通过 34 个(37.0%)
test__escaping__short_story_chapter 84 个通过 24 个(28.6%)
test__escaping__support_ticket_batch 73 个通过 36 个(49.3%)
test__escaping__terminal_session_notes 70 个通过 23 个(32.9%)
test__event_ticket_booking 107 个通过 62 个(57.9%)
test__gpu_review 94 个通过 7 个(7.4%)
test__invoice 86 个通过 36 个(41.9%)
test__job_posting 85 个通过 33 个(38.8%)
test__real_estate_listing 82 个通过 32 个(39.0%)
test__recipe 70 个通过 7 个(10.0%)
test__rental_car_booking 79 个通过 37 个(46.8%)
test__scientific_experiment 69 个通过 14 个(20.3%)
test__travel_itinerary 81 个通过 25 个(30.9%)
常见错误:
7331次 缺少必填字段
890次 条目数量错误
555次 类型不匹配
102次 期望裸列表,实际为 wrapper
62次 多余字段 'metadata.tone'
55次 6 大于最大值 5
49次 多余字段 'speaker_labels'
47次 多余字段 'tone'
44次 'cups' 不在允许值中 ['mg', 'g', 'kg', 'oz', 'lb', 'ml', 'l', 'cl', 'dl'
44次 多余字段 'notes'
对比两组在相同服务堆栈上的运行结果:
| IFStruct 分组 | base | GRPO 微调 | Δ |
|---|---|---|---|
| 总体 | 22.6% | 29.7% | +7.1 |
| JSON | 18.0% | 31.9% | +13.9 |
| YAML | 27.2% | 27.5% | +0.3 |
| Wrapper key | 28.5% | 29.7% | +1.2 |
| Bare list | 16.6% | 29.7% | +13.1 |
收益精准命中训练目标:JSON通过率提升近14个百分点(18.0% → 31.9%),YAML基本持平。尽管仍低于 Qwen3.5-2B 的 33.15%,但这表明即便是轻量级的任务专用微调,也能让小模型逼近大模型的表现。
结论
仅用约500条样本、100步的短轮GRPO训练,就能将350M参数模型在IFStruct上的得分从22.6%提升至29.7%。核心启示在于:廉价的、面向任务的奖励信号可以让小模型在格式规范性上获得显著提升,大幅缩小其与体积数倍于己的大模型之间的差距。
如需复现或扩展本工作,请参考原始 IFStruct v1.0 博客文章、Liquid4All/ifstruct 基准仓库,以及 LiquidAI/ifstruct-v1.0 数据集。