Hugging Face博客详解如何用100步GRPO微调把350M小模型的结构化输出质量提升到可用水平,适合本地部署或边缘场景的LLM应用开发者参考。
Structured output 是 LLM 最常见的真实场景任务之一,然而大多数 benchmark 将其混入更宽泛的推理或抽取评分中,而非独立衡量。一个模型能否可靠地以所需格式和形状返回有效、可解析的输出——即 schema 合规性——往往是决定它能否接入下游系统的关键。
注意,本文描述的训练流程并非用于 IFStruct 博客中 RL 模型的流程。本 notebook 并非要复现 IFStruct 的 benchmark 分数,而是展示如何通过对小型模型进行任务特定的微调来提升性能,并匹配更大规模模型的表现。
本指南分两部分,在不同环境中运行:
微调需要在 GPU 上进行。配套的 notebook 适配免费层 Colab 或 Kaggle GPU 规格。
评估可在本地 MacBook(此处为搭载 Apple M5 Max 和 36 GB 统一内存的 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 benchmark 上评估 LFM2.5-350M,看能否复现报告的 21.1% 分数。
IFStruct 是一个用于测试 LLM 输出有效性和 schema 合规性的 benchmark。该 benchmark 在 Liquid4All/ifstruct 开源,公开数据集可在 Hugging Face 的 LiquidAI/ifstruct-v1.0 获取。
git clone https://github.com/Liquid4All/ifstruct.git
为了进行评估对比,我们在 MacBook 上用 llama.cpp 本地服务模型。使用 BF16 GGUF(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:请求 llama.cpp 在 GPU 可用时将所有层卸载到 GPU
-np 4:并行服务四个请求
-c 32768:prompt context 大小
服务器运行后,用 2000 个样本运行完整 benchmark:
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
============================================================
Model: LiquidAI/LFM2.5-350M
============================================================
Overall: 452/2000 passed (22.6%)
Average latency: 1453ms
By format:
JSON: 180/1000 passed (18.0%)
YAML: 272/1000 passed (27.2%)
By top-level structure:
Wrapper key 288/1011 passed (28.5%)
Bare list 164/989 passed (16.6%)
By entity type:
test__camera_review 6/83 passed (7.2%)
test__clinical_trial 20/104 passed (19.2%)
test__conference_schedule 7/87 passed (8.0%)
test__escaping__bug_report_batch 24/89 passed (27.0%)
test__escaping__config_snippet_audit 15/85 passed (17.6%)
test__escaping__customer_email_thread 5/73 passed (6.8%)
test__escaping__dialogue_sample 14/95 passed (14.7%)
test__escaping__interview_transcript_segment 21/80 passed (26.2%)
test__escaping__log_parser_examples 21/72 passed (29.2%)
test__escaping__pr_discussion 22/87 passed (25.3%)
test__escaping__repro_steps_batch 16/73 passed (21.9%)
test__escaping__screenplay_scene 16/92 passed (17.4%)
test__escaping__short_story_chapter 15/84 passed (17.9%)
test__escaping__support_ticket_batch 27/73 passed (37.0%)
test__escaping__terminal_session_notes 20/70 passed (28.6%)
test__event_ticket_booking 49/107 passed (45.8%)
test__gpu_review 6/94 passed (6.4%)
test__invoice 28/86 passed (32.6%)
test__job_posting 25/85 passed (29.4%)
test__real_estate_listing 31/82 passed (37.8%)
test__recipe 3/70 passed (4.3%)
test__rental_car_booking 27/79 passed (34.2%)
test__scientific_experiment 13/69 passed (18.8%)
test__travel_itinerary 21/81 passed (25.9%)
Common errors:
7228x required field missing
738x wrong item count
540x type mismatch
317x Unclosed code block
190x extraneous field 'notes'
181x extraneous field 'path'
175x extraneous field 'constraints'
170x extraneous field 'type'
170x missing code block
100x expected bare list, got wrapper
IFStruct 发布博客报告 LFM2.5-350M 为 21.1%。我们本地的 llama.cpp/BF16 环境测得 22.6%,接近 IFStruct 博客中的 21.1%。我们以此本地结果作为同服务栈对比的基线。
完整可运行的流程在配套 notebook 中。本节只讲解关键部分。
我们使用 nvidia/Nemotron-RL-instruction_following-structured_outputs,它将每个 prompt 与目标 JSON Schema 和预期字段数配对。使用约 500 个样本进行训练。
由于 Nemotron 数据分布与 IFStruct 评估存在差异,我们对 prompt 进行增强以弥合两者间的两个缺口:
40% 的样本追加"将输出放在带围栏的代码块中"的指令,使模型学会遵循格式指令而非总是输出原始 JSON。
互不重叠的 20% 转换为顶级数组任务(schema 被包装在带有必需 item 计数的数组中),用于训练 bare-list 输出和 item 计数合规性。
我们加载 LiquidAI/LFM2.5-350M 并附加 LoRA 适配器。由于 LFM2.5 使用混合 attention/卷积架构,我们针对 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",
],
)
这训练了约 6M 参数,约为模型的 1.66%。
然后我们定义三个奖励函数,每个都在 [0, 1] 范围内,对每条 completion 的提取结构正确性进行评分:
json_format_reward:输出是否可解析,且为所需形式?请求形式(带围栏 vs 原始)得满分 1.0,可解析但形式错误得 0.2,不可解析得 0.0。
field_count_reward:对象是否具有预期数量的顶级字段?精确匹配得 1.0,偏差呈线性衰减。
schema_validation_reward:输出是否通过该行 JSON Schema 的验证?它统计每个约束违规,并依据 required-key 覆盖率门控部分得分。
我们将三者以加权方式组合:reward_weights=[1.0, 0.5, 2.0]。
我们训练 100 步,每组 prompt 生成 8 个 completion,适配免费层 16 GB GPU:
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, # 每组 prompt 采样的 completion 数
per_device_train_batch_size=4,
gradient_accumulation_steps=8, # 每个优化器步骤 4 组 prompt
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 散度在预热后脱离零,截断 completion 比例保持在接近零的水平。
最后,将 LoRA 适配器合并回基座权重,并保存为单一自包含 checkpoint,准备转换为 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)
GRPO 微调后,我们重新运行 IFStruct 评估。为此,需要将合并后的模型 checkpoint 转换为 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
============================================================
Model: lfm25-350m-grpo-structured-output
============================================================
Overall: 594/2000 passed (29.7%)
Average latency: 1518ms
By format:
JSON: 319/1000 passed (31.9%)
YAML: 275/1000 passed (27.5%)
By top-level structure:
Wrapper key 300/1011 passed (29.7%)
Bare list 294/989 passed (29.7%)
By entity type:
test__camera_review 5/83 passed (6.0%)
test__clinical_trial 31/104 passed (29.8%)
test__conference_schedule 11/87 passed (12.6%)
test__escaping__bug_report_batch 32/89 passed (36.0%)
test__escaping__config_snippet_audit 24/85 passed (28.2%)
test__escaping__customer_email_thread 9/73 passed (12.3%)
test__escaping__dialogue_sample 17/95 passed (17.9%)
test__escaping__interview_transcript_segment 13/80 passed (16.2%)
test__escaping__log_parser_examples 33/72 passed (45.8%)
test__escaping__pr_discussion 26/87 passed (29.9%)
test__escaping__repro_steps_batch 23/73 passed (31.5%)
test__escaping__screenplay_scene 34/92 passed (37.0%)
test__escaping__short_story_chapter 24/84 passed (28.6%)
test__escaping__support_ticket_batch 36/73 passed (49.3%)
test__escaping__terminal_session_notes 23/70 passed (32.9%)
test__event_ticket_booking 62/107 passed (57.9%)
test__gpu_review 7/94 passed (7.4%)
test__invoice 36/86 passed (41.9%)
test__job_posting 33/85 passed (38.8%)
test__real_estate_listing 32/82 passed (39.0%)
test__recipe 7/70 passed (10.0%)
test__rental_car_booking 37/79 passed (46.8%)
test__scientific_experiment 14/69 passed (20.3%)
test__travel_itinerary 25/81 passed (30.9%)
Common errors:
7331x required field missing
890x wrong item count
555x type mismatch
102x expected bare list, got wrapper
62x extraneous field 'metadata.tone'
55x 6 is greater than maximum 5
49x extraneous field 'speaker_labels'
47x extraneous field 'tone'
44x 'cups' not in allowed values ['mg', 'g', 'kg', 'oz', 'lb', 'ml', 'l', 'cl', 'dl'
44x extraneous field 'notes'
在同一服务栈上对比两次运行结果:
提升恰好落在训练目标的位置: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 benchmark 仓库以及 LiquidAI/ifstruct-v1.0 数据集。